国赛2022年初赛
baby_tree #
swift 语言的抽象语法树
AST 的主体是一个 check 函数
写一个语法树到源码的程序太浪费时间,为了减少逆向的难度,可以分层折叠语法树单元,尽量以语句为单位
归纳出源码
def check(b, k):
r0, r1, r2, r3 = 0, 0, 0, 0
for i in range(0, len(b)-3):
r0, r1, r2, r3 = b[i], b[i+1], b[i+2], b[i+3]
b[i] = r2 ^ ((k[0] + (r0>>4)) & 0xff)
b[i+1] = r3 ^ ((k[1] + (r1>>2)) & 0xff)
b[i+2] = r0 ^ k[2]
b[i+3] = r1 ^ k[3]
k[0], k[1], k[2], k[3] = k[1], k[2], k[3], k[0]
return b == [88, 35, 88, 225, 7, 201, 57, 94,
77, 56, 75, 168, 72, 218, 64, 91,
16, 101, 32, 207, 73, 130, 74, 128,
76, 201, 16, 248, 41, 205, 103, 84,
91, 99, 79, 202, 22, 131, 63, 255, 20, 16]
assert(check(input(), k='5y34'))
可以看到是 4 字节一组步进加密,解密时从尾到头反过来即可
s = bytearray([88, 35, 88, 225, 7, 201, 57, 94, 77, 56, 75, 168, 72, 218, 64, 91, 16, 101, 32, 207,
73, 130, 74, 128, 76, 201, 16, 248, 41, 205, 103, 84, 91, 99, 79, 202, 22, 131, 63, 255, 20, 16])
k = bytearray(b'5y34')
f = s.copy()
for i in range(38, -1, -1):
r1 = f[i+3] ^ k[3]
r0 = f[i+2] ^ k[2]
r3 = f[i+1] ^ ((k[1] + (r1>>2)) & 0xff)
r2 = f[i] ^ ((k[0] + (r0>>4)) & 0xff)
f[i], f[i+1], f[i+2], f[i+3] = r0, r1, r2, r3
k[0], k[1], k[2], k[3] = k[3], k[0], k[1], k[2]
print(f.decode())
#flag{30831242-56db-45b4-96fd-1f47e60da99d}
baby_code #
文件开头字节是 RITE0300
查了一下,这是 mruby bytecode
扒下所有字节码
mruby -b babycode.mrb
几个关键要点:
- 最后一个函数显然是 check 函数
- XX 看起来像魔数 0x12345678
- YY 看起来像块长或者轮数 16 而从 times 的调用可以知道这是轮数
- mask 为 0xffffffff 确定块长为 32
可以看出是魔改的 XTEA
u5+=(((u6<<3)^(u6>>5))+u6)^(u3[((u7>>11)+1)&3]+u7)
u7+=0x12345678
u6+=(((u5<<3)^(u5>>5))+u5)^(u3[(u7+1)&3]+u7)
需要注意的是加密之前还对输入做了预处理
last=0
for i in range(len(p)):
last,p[i]=p[i],p[i]^last^(i+1)
#flag{6ad1c70c-daa4-11ec-9d64-0242ac1200}
secreeeeet #
看附件显然是对 PNG 进行加解密
造一个假 PNG 给它加密,发现对同一个文件每次加密结果都不一样
在 main 函数输入的地方调试
此处是和一个长度为 3 的 key(图中 v103)进行循环 XOR
在 main 函数输出的地方调试
此处是做一个流加密,并且步长 16 字节
每次步进都对 sbox(图中 v186)做一次混淆
其中 sub_1DF0 是一个难以逆向的 shuffle 函数,不调用任何其他的函数
追溯 sbox 的来源可以在 main 开头看到 sbox 的密钥长度为 5(图中 v102),并且 sbox 一开始的值只由它扩散而成
通过调试可以知道
这两个密钥都由数字组成,不过长度为 5 的那个对码表 qscfthnjik 取了下标
例如数字为 12345,则种子为 scfth
可以从上面看到两个数字均来源于时间,因此必须爆破
考虑到对 PNG 头进行 XOR 时使用的密钥在一个很小的范围内,利用这一点进行爆破
我的规则:
- pt 是 PNG 头
\x89PNG - ct 来自题目
\x7B\x92\x1F\x3E pt^ct的前三位每位均属于 0-9pt^ct的第四位和第一位相同(因为是循环 XOR)
爆破时需要顺着逆向出流生成算法
这里考虑到流的生成过程中没有任何外界干扰,采用 angr 模拟执行输出两个相关联的 BitVec
只要将一个作为输入(可视为已知 因为要爆破)一个作为输出,调用 claripy 的约束求解即可模拟 box 的生成
def sim(key5, stream_length):
base_addr = 0x400000
project = angr.Project(
"secreeeeet.exe", main_opts={"base_addr": base_addr}, auto_load_libs=False
)
begin_addr = 0x3980 + base_addr
heap_addr, stack_addr = 0x500000, 0x600000 + 8 # to align rsp with 16 bytes
begin_state = project.factory.blank_state(addr=begin_addr)
begin_state.options.update(
{
angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY,
angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
angr.options.LAZY_SOLVES,
}
)
fake_flag = claripy.BVV(b'\x00'*stream_length, stream_length *8)
begin_state.memory.store(heap_addr, key5)
begin_state.memory.store(heap_addr + 0x1000, fake_flag)
rbp = stack_addr - 0x3C8
rsp = stack_addr - 0x4C8
begin_state.regs.rbp = rbp
begin_state.regs.rsp = rsp
begin_state.regs.r12 = heap_addr
begin_state.mem[rsp + 0x28].uint32_t = stream_length
begin_state.mem[rsp + 0x68].uint64_t = heap_addr + 0x1000
end_addr = 0x3CE8 + base_addr
simgr = project.factory.simgr(begin_state)
simgr.explore(find=end_addr)
try:
end_state = simgr.found[0]
except:
end_state = simgr.errored[0]
# print(end_state)
return end_state.memory.load(heap_addr + 0x1000, stream_length)
其中长度为 5 的 key 地址位于 r12
由于要和 16 字节对齐不然某些指令会报错,stack_addr 加上 8
相关的两个变量一个是位于 0x28 的长度,另一个是位于 0x68 的数据指针
生成的流将会和数据指针指向的内容 XOR,所以将数据置 0 将得到纯粹的流
用 pickle 将模型序列化,放到 Linux 机器去爆
A = claripy.BVS("A", 5*8)
B = sim(A, 16)
with open("prg16.bin","wb") as fp:
pickle.dump((A,B),fp)
with open("prg16.bin", "rb") as fp:
A, B = pickle.load(fp)
B = B.chop(4*8)[0]
ct = b"\x7b\x92\x1f\x3e"
png = b"\x89PNG"
charset = "qscfthnjik"
def f(x):
S = claripy.Solver()
S.add(A == claripy.BVV(x.encode(), 5 * 8))
stream = l2b(S.eval(B, 1)[0], blocksize=4)
b = [x ^ y for x, y in zip(ct, stream)]
m = [x ^ y for x, y in zip(b, png)]
if (
m[0] == m[3]
and 0x30 <= m[0] <= 0x39
and 0x30 <= m[1] <= 0x39
and 0x30 <= m[2] <= 0x39
):
print(f'key: {x}')
print(f'xor_key: {bytes(m[:3]).decode()}')
return True
return False
应用上述爆破规则从码表 qscfthnjik 中选取 5 个
mbruteforce 的进度条不是很健壮
使用 tqdm 和分段爆破手搓一个进度条
context.log_level = "critical"
for i in tqdm(range(500)):
x = mbruteforce(f, charset, length=5, method="fixed", start=(i + 1, 500))
if x is not None:
break
大约 20 分钟后得出结果 iqsqn 和 002
在得到两个密钥之后,同样再调用一次 sim 函数
不过这次不是为了获得模型,而是为了直接计算出结果
key=b"iqsqn"
xor_key=b"002"
with open("flag.png.enc.bak", "rb") as fp:
ct = fp.read()
A, B = sim(claripy.BVV(key, 5*8), len(ct))
S = claripy.Solver()
stream = l2b(S.eval(B, 1)[0], blocksize=len(ct))
m = bytes(xor_key[i%3] ^ ct[i] ^ stream[i] for i in range(len(ct)))
with open("flag_decrypted.png", "wb") as fq:
fq.write(m)