vvvmmm是CISCN 2025的一道综合性逆向工程题目,融合了多种现代保护技术。题目要求输入48字节的正确字符串,程序验证成功后会输出flag。题目的核心特点包括:
UPX加壳保护
Unicorn Engine虚拟机架构
RISC-V指令集加密逻辑
多层算法嵌套
本文将从零开始,逐步剖析这道题目的每个技术细节,带领读者理解完整的解题思路。
首先运行程序观察其基本行为:
./vvvmmm
程序会显示ASCII艺术图案,然后提示:
input %48c>
这个格式说明符非常关键。%48c表示程序会读取固定的48个字符,而不是常见的以null结尾的字符串。这意味着我们需要提供恰好48字节的输入。
如果输入错误,程序返回"Try again~";如果输入正确,程序输出"Good."并显示flag。
使用file命令查看文件信息:
file vvvmmm
输出显示:
vvvmmm: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux),
for GNU/Linux 3.2.0, statically linked, no section header
关键信息点:
静态链接 - 所有库函数都被编译进二进制文件
无节头表 - 典型的加壳特征
使用strings命令搜索特征字符串:
strings vvvmmm | grep -i upx
发现明确的UPX标识:
UPX!
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 5.02 Copyright (C) 1996-2025 the UPX Team. All Rights Reserved. $
确认程序使用UPX 5.02版本进行了压缩保护。
UPX(Ultimate Packer for eXecutables)是一个广泛使用的可执行文件压缩工具。其工作原理如下:
将原始程序的代码段和数据段进行压缩
在程序入口添加解压缩stub代码
程序运行时,stub代码先将压缩数据解压到内存
解压完成后跳转到原始入口点(OEP)执行
UPX的优势在于可以显著减小文件体积,但同时也增加了静态分析的难度。
UPX提供了原生的脱壳支持:
upx -d vvvmmm -o vvvmmm_unpacked
输出结果:
Ultimate Packer for eXecutables
File size Ratio Format Name
2694441 <- 1133076 42.05% linux/amd64 vvvmmm_unpacked
Unpacked 1 file.
成功脱壳。原始程序2.69MB被压缩到1.13MB,压缩比约42%。
脱壳后的文件应该恢复了正常的ELF结构:
file vvvmmm_unpacked
现在可以看到完整的节头表信息,说明脱壳成功。
对脱壳后的程序进行字符串分析:
strings vvvmmm_unpacked | grep -i "qemu\|unicorn" | head -20
发现大量QEMU和Unicorn相关字符串:
qemu: %s: %s
qemu_ld_i32
qemu_st_i32
qemu_ld_i64
qemu_st_i64
WARNING: Your register accessing on id %u is deprecated...
qemu/util/qht.c
qemu/fpu/softfloat.c
qemu/tcg/tcg.c
这明确表明程序使用了Unicorn Engine。
Unicorn是基于QEMU的轻量级CPU模拟框架,具有以下特点:
支持多种架构:ARM, ARM64, MIPS, SPARC, X86, RISC-V等
可以嵌入应用程序中模拟执行其他架构的代码
提供完整的内存管理和寄存器访问API
常用于恶意软件分析、fuzzing和安全研究
在CTF逆向题中,Unicorn常被用于实现虚拟机保护:
将核心算法编译为非本地架构的指令
在运行时通过虚拟机模拟执行
增加静态分析和反汇编的难度
继续搜索架构相关字符串:
strings vvvmmm_unpacked | grep -i "uc_.*reg"
发现大量"uc_riscv"前缀的符号,确认使用RISC-V架构。
程序运行需要两类关键数据:
种子字符串(seed) - 用于初始化算法
RISC-V字节码 - 包含验证逻辑
在专业逆向工具(如IDA Pro或Ghidra)中:
定位Unicorn初始化代码
搜索"uc_open"、"uc_mem_map"等API调用
这些函数负责初始化虚拟机环境
定位代码加载点
查找"uc_mem_write"调用
第二个参数指向RISC-V代码的内存地址
第三个参数是代码长度
提取RISC-V字节码
在IDA中跳转到代码地址
导出对应长度的字节序列
本题中代码长度为0x296字节(662字节)
查找种子字符串
在数据段搜索32字节的可打印字符序列
或通过交叉引用分析定位字符串常量
使用GDB在运行时提取数据:
# 设置断点在Unicorn初始化之后
gdb ./vvvmmm
(gdb) catch syscall read
(gdb) run
(gdb) continue
# 搜索内存中的字符串模式
(gdb) find /b 0x400000, 0x800000, 'e', '4', 'Y', '8'
通过内存扫描,可以在特定地址(0x64c6c0)找到32字节的种子字符串。
最终获得两个关键数据:
种子字符串(32字节):
e4Y8YRXVzg2HRrCUy35CM0Txq91HzMGZ
这是一个由大小写字母和数字组成的随机字符串,用作密钥派生的初始种子。
RISC-V代码:
长度为662字节,包含完整的验证算法实现。需要使用RISC-V反汇编工具进一步分析。
RISC-V是一个开源的指令集架构(ISA),具有以下特点:
基于精简指令集(RISC)设计哲学
模块化设计,支持基础指令集+扩展
支持32位、64位和128位变体
完全开源,无需授权费用
RISC-V的寄存器命名规则:
x0: 硬件零寄存器(始终为0)
x1-x31: 通用寄存器
也可使用ABI名称:a0-a7(参数),t0-t6(临时)等
使用支持RISC-V的反汇编工具:
riscv64-unknown-elf-objdump
Ghidra(内置RISC-V支持)
IDA Pro(需要RISC-V插件)
通过反汇编分析,RISC-V代码实现了三个核心模块:
第一段代码对种子字符串进行哈希计算。
uint64_t h = 1;
for (int i = 0; seed[i] != 0; i++) {
h = 31 * h + seed[i];
}
DJB31算法由Daniel J. Bernstein设计
使用魔数31作为乘法因子
31 = 2^5 - 1,可优化为移位和减法
乘以31等价于:(h << 5) - h
在64位整数范围运算,溢出自然截断
初始值为1(标准DJB算法通常用5381)
MASK64 = (1 << 64) - 1
def djb31_hash(seed: bytes) -> int:
h = 1
for b in seed:
if b == 0:
break
h = (31 * h + b) & MASK64
return h
这个哈希值将作为后续伪随机数生成器的初始状态。
第二段代码实现了一个定制的伪随机数生成器,用于生成加密密钥流。
MOD = 0x13579bdf # 魔数模数
这个模数是一个精心选择的质数,用于模运算以保证输出的伪随机性。
mulhu rd, rs1, rs2
功能:计算rs1 * rs2的128位乘积,取高64位存入rd
在64位架构中:
product = rs1 * rs2 # 128位结果
rd = product[127:64] # 取高64位
remuw rd, rs1, rs2
功能:计算(rs1[31:0] mod rs2[31:0]),结果符号扩展到64位
特点:
只使用低32位参与运算
结果会进行符号扩展填充高32位
def step(a2: int, a4: int) -> tuple[int, int]:
# 第一组32位模运算
a2 = remuw(a2, MOD)
a3 = remuw(a4, MOD)
a4 = remuw(a2, MOD)
a5 = remuw(a3, MOD)
# 左移32位准备高位乘法
a2_shift = (a2 << 32) & MASK64
a4_shift = (a4 << 32) & MASK64
# 高位乘法并取模
a4 = remu(mulhu(a4_shift, a2_shift), MOD)
a3_shift = (a3 << 32) & MASK64
a5_shift = (a5 << 32) & MASK64
a5 = remu(mulhu(a5_shift, a3_shift), MOD)
# 恢复原始值
a2 = (a2_shift >> 32) & MASK64
a3 = (a3_shift >> 32) & MASK64
# 迭代混淆10次
for _ in range(10):
a4 = remu((a4 * a2) & MASK64, MOD)
a5 = remu((a5 * a3) & MASK64, MOD)
# 最终密钥流输出
a2 = remu((a4 * a2) & MASK64, MOD)
a4 = remu((a5 * a3) & MASK64, MOD)
return a2, a4
h = djb31_hash(seed)
a2 = h >> 16 # 取高位
a4 = h # 取全值
通过6轮迭代,每轮产生2个32位值,总共生成12个密钥流值:
keystream = []
for i in range(6):
a2, a4 = step(a2, a4)
keystream.append(a2 & 0xffffffff)
keystream.append(a4 & 0xffffffff)
def sext32(x: int) -> int:
"""32位符号扩展到64位"""
x &= 0xffffffff
if x & 0x80000000:
return x | 0xffffffff00000000
return x
def remuw(dividend: int, divisor: int) -> int:
"""32位无符号取模,结果符号扩展"""
res = (dividend & 0xffffffff) % (divisor & 0xffffffff)
return sext32(res)
def mulhu(x: int, y: int) -> int:
"""64位无符号乘法,取高64位"""
return ((x & MASK64) * (y & MASK64) >> 64) & MASK64
def remu(x: int, d: int) -> int:
"""64位无符号取模"""
return (x & MASK64) % (d & MASK64)
第三段代码将用户输入与密钥流进行XOR运算,然后与预设目标值比较。
这是一个标准的流加密(Stream Cipher)方案:
明文处理:将48字节输入解析为12个32位整数(小端序)
密钥流生成:使用前述算法生成12个32位密钥
加密操作:ciphertext = plaintext XOR keystream
验证:比较加密结果与预设目标值
target = [
0x45034f63, 0x534762d2, 0x44b36d04, 0x44c3ed6a,
0x79bb60b0, 0x42a1e767, 0x3edb7e6c, 0x30e1551d,
0x4d3abaa4, 0x6aa29948, 0x51ce8847, 0x51623faf
]
这12个32位常量嵌入在RISC-V代码中,通过反汇编提取。
# 将输入转换为32位整数数组
input_words = []
for i in range(0, 48, 4):
word = int.from_bytes(input[i:i+4], 'little')
input_words.append(word)
# 生成密钥流并加密
output = []
a2, a4 = initial_state()
for i in range(6):
a2, a4 = step(a2, a4)
ks0 = a2 & 0xffffffff
ks1 = a4 & 0xffffffff
output.append(input_words[2*i] ^ ks0)
output.append(input_words[2*i+1] ^ ks1)
# 验证
if output == target:
print("Good.")
else:
print("Try again~")
XOR运算具有以下重要性质:
自反性: A XOR B XOR B = A
交换律: A XOR B = B XOR A
结合律: (A XOR B) XOR C = A XOR (B XOR C)
基于自反性,我们可以轻松解密:
plaintext XOR keystream = ciphertext
ciphertext XOR keystream = plaintext
因此:
plaintext = ciphertext XOR keystream = target XOR keystream
理解了完整的算法逻辑后,可以编写解密脚本。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
MASK64 = (1 << 64) - 1
MOD = 0x13579bdf
def djb31_hash(seed: bytes) -> int:
"""DJB31哈希算法"""
h = 1
for b in seed:
if b == 0:
break
h = (31 * h + b) & MASK64
return h
def sext32(x: int) -> int:
"""32位符号扩展到64位"""
x &= 0xffffffff
if x & 0x80000000:
return x | 0xffffffff00000000
return x
def remuw(dividend: int, divisor: int) -> int:
"""32位无符号取模,结果符号扩展"""
res = (dividend & 0xffffffff) % (divisor & 0xffffffff)
return sext32(res)
def mulhu(x: int, y: int) -> int:
"""64位无符号乘法,取高64位"""
return ((x & MASK64) * (y & MASK64) >> 64) & MASK64
def remu(x: int, d: int) -> int:
"""64位无符号取模"""
return (x & MASK64) % (d & MASK64)
def step(a2: int, a4: int) -> tuple[int, int]:
"""密钥流生成核心步骤"""
a2 = remuw(a2, MOD)
a3 = remuw(a4, MOD)
a4 = remuw(a2, MOD)
a5 = remuw(a3, MOD)
a2_shift = (a2 << 32) & MASK64
a4_shift = (a4 << 32) & MASK64
a4 = remu(mulhu(a4_shift, a2_shift), MOD)
a3_shift = (a3 << 32) & MASK64
a5_shift = (a5 << 32) & MASK64
a5 = remu(mulhu(a5_shift, a3_shift), MOD)
a2 = (a2_shift >> 32) & MASK64
a3 = (a3_shift >> 32) & MASK64
for _ in range(10):
a4 = remu((a4 * a2) & MASK64, MOD)
a5 = remu((a5 * a3) & MASK64, MOD)
a2 = remu((a4 * a2) & MASK64, MOD)
a4 = remu((a5 * a3) & MASK64, MOD)
return a2, a4
def main():
# 种子字符串
seed = b"e4Y8YRXVzg2HRrCUy35CM0Txq91HzMGZ\x00"
# 计算初始哈希
h = djb31_hash(seed)
# 初始化状态
a2 = h >> 16
a4 = h
# 目标常量
consts = [
0x45034f63, 0x534762d2, 0x44b36d04, 0x44c3ed6a,
0x79bb60b0, 0x42a1e767, 0x3edb7e6c, 0x30e1551d,
0x4d3abaa4, 0x6aa29948, 0x51ce8847, 0x51623faf
]
# 生成密钥流并解密
words = []
for i in range(6):
a2, a4 = step(a2, a4)
ks0 = a2 & 0xffffffff
ks1 = a4 & 0xffffffff
# XOR解密
words.append(consts[2*i] ^ ks0)
words.append(consts[2*i+1] ^ ks1)
# 转换为字节串
inp = b"".join(w.to_bytes(4, "little") for w in words)
print("输入长度:", len(inp))
print("输入内容:", inp.decode())
print("\nflag{" + inp.decode() + "}")
if __name__ == "__main__":
main()
python3 solve.py
输出:
输入长度: 48
输入内容: fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX
flag{fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX}
将求解的48字节字符串提交给原程序验证:
echo -n "fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX" | ./vvvmmm
输出:
[ASCII艺术图案]
input %48c>Good.
flag{fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX}
程序输出"Good.",验证成功。
UPX壳:
识别:通过strings搜索UPX特征字符串
脱壳:使用upx -d命令直接脱壳
原理:压缩程序体积,增加静态分析难度
虚拟机保护:
Unicorn Engine实现跨架构代码模拟
将核心算法编译为RISC-V指令
在运行时通过虚拟机执行,隔离实际逻辑
架构特点:
开源精简指令集
模块化设计
支持多种位宽变体
关键指令:
mulhu:64位乘法取高位,用于高精度运算
remuw:32位无符号取模,结果符号扩展
需要精确模拟才能正确复现算法
DJB31哈希:
简单高效的字符串哈希算法
使用魔数31优化性能
作为密钥派生的基础
伪随机数生成器:
基于模运算和高位乘法
通过迭代混淆增加复杂度
生成确定性密钥流
XOR流加密:
明文与密钥流逐字节异或
利用XOR自反性实现加解密
安全性依赖密钥流的不可预测性
分层分析:
外层:识别并去除UPX壳
中层:识别Unicorn虚拟机框架
内层:分析RISC-V算法逻辑
特征定位:
字符串搜索快速定位关键技术
交叉引用分析追踪数据流
API调用分析理解程序行为
算法还原:
从汇编代码推导高级语言逻辑
理解算法意图而非逐指令翻译
抓住核心问题(XOR可逆性)
信息收集:观察程序行为,识别文件特征
去除保护:识别UPX壳并脱壳
技术识别:通过字符串特征识别Unicorn虚拟机
架构确认:确定使用RISC-V指令集
数据提取:提取种子字符串和RISC-V字节码
算法分析:逐模块分析RISC-V代码逻辑
脚本编写:用Python复现完整算法
解密求解:利用XOR可逆性解密目标值
结果验证:用原程序验证flag正确性
RISC-V的某些指令有特殊的语义,需要准确理解:
mulhu返回128位乘积的高64位
remuw只使用低32位运算,结果符号扩展
位运算的掩码处理
Python大整数需要手动截断
错误的指令模拟会导致密钥流完全错误,无法解密。
种子字符串和RISC-V代码嵌入在程序中,需要通过以下方式定位:
专业工具(IDA Pro/Ghidra)的交叉引用分析
动态调试在特定断点处dump内存
运行时内存搜索特征模式
纯命令行工具难以完成精确定位,需要图形化逆向工具辅助。
需要理解三层算法的嵌套关系:
DJB31哈希产生初始状态
伪随机数生成器产生密钥流
XOR流加密验证输入
理解各层的数学原理和密码学意义,才能找到正确的解题方向。
从防御角度:
这道题展示了多层保护的思路:
UPX壳作为第一道防线
虚拟机保护隐藏真实逻辑
非常见架构(RISC-V)增加分析难度
复杂数学运算混淆算法
改进建议:
使用修改过的UPX壳防止直接脱壳
加入反调试检测
在虚拟机层面增加代码混淆
使用更强的加密算法(如AES)
从攻击角度:
突破点:
UPX为标准壳,可直接脱壳
Unicorn有明确的API特征
XOR流加密可逆
算法逻辑相对清晰
关键:
善用专业工具(IDA Pro)
理解底层架构(RISC-V)
掌握密码学基础
编程实现能力
工具掌握:
基础:file, strings, readelf等Linux工具
脱壳:upx, UPX等专用工具
反汇编:IDA Pro, Ghidra, radare2
调试:GDB, QEMU用户模式模拟
编程:Python脚本编写能力
知识储备:
多架构知识:x86-64, ARM, RISC-V等
密码学基础:哈希,对称加密,流加密
程序保护:加壳,虚拟机,混淆
汇编语言:至少掌握一种架构
分析方法:
从易到难:先处理外层保护
特征识别:利用已知模式快速定位
动静结合:静态分析+动态调试
抓住本质:理解算法意图
思维方式:
不被表面复杂度吓倒
分解问题,各个击破
善用现有工具和资源
注重验证,确保正确性
虚拟机保护分析:
识别不同虚拟机框架(Unicorn, QEMU, 自研VM)
理解虚拟机API和调用约定
提取虚拟机字节码的方法
编写虚拟机字节码的反汇编器
多架构逆向:
熟悉常见架构的指令集和ABI
理解不同架构的调用约定
掌握交叉编译和模拟执行
使用QEMU用户模式运行其他架构程序
密码学在CTF中的应用:
识别常见加密算法(AES, RSA, DES等)
分析自定义加密算法的弱点
利用数学性质(如XOR可逆性)
掌握基本的密码分析技巧
vvvmmm是一道设计精良的综合性逆向题目,融合了程序保护、虚拟机技术、RISC-V架构和密码学算法等多个技术领域。通过系统的分层分析:
识别UPX壳并成功脱壳
发现Unicorn虚拟机保护
确认RISC-V指令集
提取关键数据(种子和字节码)
逐模块分析算法逻辑
编写解密脚本求解
验证结果正确性
我们最终获得了正确的flag:
flag{fANUES0XtUXBDEbOXs4xFcXDb3Q5kMU87bZLMZJfuRnCvfwX}
这道题目展示了现代CTF逆向题的典型特征:多层保护、跨架构技术、算法复杂度。通过分析这道题,我们不仅学习了具体的技术点,更重要的是掌握了系统化的逆向分析方法论。
对于逆向工程学习者,这道题提供了很好的实践机会:
接触真实的程序保护技术
学习新兴的RISC-V架构
理解虚拟机保护原理
提升密码学算法分析能力
锻炼问题分解和解决能力
希望本文的详细分析能够帮助读者理解这道题目的每个技术细节,并在今后的CTF比赛和安全研究中应用这些知识和方法。
逆向工程是一个需要持续学习和实践的领域。保持好奇心,不断挑战新的技术,善于总结方法论,终将在这个领域取得进步。