2021 羊城杯 Babyvm Writeup
2021 羊城杯 Babyvm Writeup
基本分析
程序逻辑
- 虚拟机类题目
- 首先初始化
engine
实例。这里建立了一个engine_t
结构体使得代码的可读性更高,并且可以根据语义推测出每个字段是什么 - 然后有个 SMC 自修改,但是这里并没有反调,所以直接步过跳过去。然后可以把修改后的代码 dump 下来,再 patch 回去即可。最后再把对 SMC 函数的调用
nop
掉即可import idc start_addr = 0x80487A8 end_addr = 0x8048F45 def dump(): data = idc.get_bytes(start_addr, end_addr-start_addr) print(data) def patch(data): for i in range(len(data)): idc.del_items(start_addr+i) for i, b in enumerate(data): idc.patch_byte(start_addr+i, b)
-
exec_engine
就是一个while
循环,里面模拟 CPU 进行执行 - 虚拟机类题目理解题意并不是很难,主要难点是在不容易梳理出一个平坦的控制流,这里就对写脚本的能力有很大的考验
- 可以考虑用 Python 脚本把
ip
的值和要执行的代码关联起来,然后复现一下 CPU 运行的过程,就可以得到所有运行的代码了
获取所有执行的代码
- 使用一个 dict 做映射
insn = b'\xa1\xc1\x00\xb1w\xc2J\x01\x00\x00\xc1\x01\xb2w\xc2\x19\x01\x00\x00\xc1\x02\xb4w\xc2\xdd\x01\x00\x00\xc1\x03\xb3w\xc2\x0f\x01\x00\x00\xc1\x04\xb2w\xc2\x1b\x01\x00\x00\xc1\x05\xb4w\xc2\x89\x01\x00\x00\xc1\x06\xb1w\xc2\x19\x01\x00\x00\xc1\x07\xb3w\xc2T\x01\x00\x00\xc1\x08\xb1w\xc2O\x01\x00\x00\xc1\t\xb1w\xc2N\x01\x00\x00\xc1\n\xb3w\xc2U\x01\x00\x00\xc1\x0b\xb3w\xc2V\x01\x00\x00\xc1\x0c\xb4w\xc2\x8e\x00\x00\x00\xc1\r\xb2w\xc2I\x00\x00\x00\xc1\x0e\xb3w\xc2\x0e\x01\x00\x00\xc1\x0f\xb1w\xc2K\x01\x00\x00\xc1\x10\xb3w\xc2\x06\x01\x00\x00\xc1\x11\xb3w\xc2T\x01\x00\x00\xc1\x12\xb2w\xc2\x1a\x00\x00\x00\xc1\x13\xb1w\xc2B\x01\x00\x00\xc1\x14\xb3w\xc2S\x01\x00\x00\xc1\x15\xb1w\xc2\x1f\x01\x00\x00\xc1\x16\xb3w\xc2R\x01\x00\x00\xc1\x17\xb4w\xc2\xdb\x00\x00\x00\xc1\x18\xb1w\xc2\x19\x01\x00\x00\xc1\x19\xb4w\xc2\xd9\x00\x00\x00\xc1\x1a\xb1w\xc2\x19\x01\x00\x00\xc1\x1b\xb3w\xc2U\x01\x00\x00\xc1\x1c\xb2w\xc2\x19\x00\x00\x00\xc1\x1d\xb3w\xc2\x00\x01\x00\x00\xc1\x1e\xb1w\xc2K\x01\x00\x00\xc1\x1f\xb2w\xc2\x1e\x00\x00\x00\xc1 \x80\x02\x18\x00\x00\x00#\x10\xc1!\x80\x02\x10\x00\x00\x00#\xf7\xc1"\x80\x02\x08\x00\x00\x00#\xf7\xc1#\xf7\xfe\x80\x02\x05\x00\x00\x00"w\x10\x80\x02\x07\x00\x00\x00#\x80\x02#w\xf1\x981w\x10\x80\x02\x18\x00\x00\x00#\x80\x02 \xb9\xe451w\x10\x80\x02\x12\x00\x00\x00"w\xa0\xc1$\x80\x02\x18\x00\x00\x00#\x10\xc1%\x80\x02\x10\x00\x00\x00#\xf7\xc1&\x80\x02\x08\x00\x00\x00#\xf7\xc1\'\xf7\xfe2 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\x102 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\xc7\xc1(\x80\x02\x18\x00\x00\x00#\x10\xc1)\x80\x02\x10\x00\x00\x00#\xf7\xc1*\x80\x02\x08\x00\x00\x00#\xf7\xc1+\xf7\xfe2 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\x102 C3w\x80\x02\x11\x00\x00\x00"578w\x80\x02\r\x00\x00\x00#w89\xc8\x99' code = {} code[0x71] = ('*--_sp = *(_DWORD *)(_ip + 1);_ip += 5;', 5) code[0x41] = ('r[1] += r[2];_ip += 1;', 1) code[0x42] = ('r[1] -= r[4];_ip += 1;', 1) code[0x43] = ('r[1] *= r[3];_ip += 1;', 1) code[0x37] = ('r[1] = r[5];_ip += 1;', 1) code[0x38] = ('r[1] ^= r[4];_ip += 1;', 1) code[0x39] = ('r[1] ^= r[5];_ip += 1;', 1) code[0x35] = ('r[5] = r[1];_ip += 1;', 1) code[0xf7] = ('r7 += r[1];_ip += 1;', 1) code[0x44] = ('r[1] /= r[5];_ip += 1;', 1) code[0x80] = ('r[_ip[1]] = *(_DWORD *)(_ip + 2);_ip += 6;', 6) code[0x77] = ('r[1] ^= r7;_ip += 1;', 1) code[0x53] = ('putchar(*(char *)r[3]);_ip += 2;', 2) code[0x22] = ('r[1] = r[1] >> r[2];_ip += 1;', 1) code[0x23] = ('r[1] <<= r[2];_ip += 1;', 1) code[0x76] = ('r[3] = *_sp;*_sp++ = 0;_ip += 5;', 5) code[0x54] = ('v2 = &r[3];*v2 = getchar();_ip += 2;', 2) code[0x30] = ('r[1] |= r[2];_ip += 1;', 1) code[0x31] = ('r[1] &= r[2];_ip += 1;', 1) code[0x32] = ('r[3] = _ip[1];_ip += 2;', 2) code[0x9] = (' r[1] = 0x6FEBF967;_ip += 1;', 1) code[0x10] = ('r7 = r[1];_ip += 1;', 1) code[0x33] = ('r[4] = r[1];_ip += 1;', 1) code[0x34] = ('r[2] = _ip[1];_ip += 2;', 2) code[0xfe] = ('r[1] = r7;_ip += 1;', 1) code[0x11] = ('printf("%x\n", r[1]);_ip += 1;', 1) code[0xa0] = ('assert(r[1] == 0x6FEBF967);_ip += 1;', 1) code[0xa1] = ('read;_ip += 1;', 1) code[0xb1] = ('r7 = key[0];_ip += 1;', 1) code[0xb2] = ('r7 = key[1];_ip += 1;', 1) code[0xa4] = ('key[_ip[1]] = r[1];_ip += 4;', 4) code[0xb3] = ('r7 = key[2];_ip += 1;', 1) code[0xb4] = ('r7 = key[3];_ip += 1;', 1) code[0xc1] = ('r[1] = plain[_ip[1]];_ip += 2;', 2) code[0xc7] = ('assert(cipher1==r[1]);_ip += 1;', 1) code[0xc8] = ('assert(cipher2==r[1]);_ip += 1;', 1) code[0xc2] = ('assert(_ip[1] == r[1]);_ip += 5;', 5) code[0x99] = ('break;_ip += 10;', 10) ip = 0 parsed = "" while ip < len(insn): parsed += code[insn[ip]][0] ip += code[insn[ip]][1] with open("parsed.c", "w")as f: f.write("""#include<stdio.h> #include<stdint.h> int main(void){ %s }""" % parsed)
- 得到 C 代码后手动对其进行调整,代码量略大,所以这里贴到 Github gist 上了2021.09.12-YangCheng-Babyvm.c
关键逻辑
Part 1
- 前面部分的代码大致都是这样
ip += 1; r[1] = plain[ip[1]]; ip += 2; r7 = key[0]; ip += 1; r[1] ^= r7; ip += 1; assertEqual(ip[1], r[1]);
- 可以看到,每次就是取一个 byte 然后进行运算,亦或一个 key,与密文比较
- 因为我们把输入设置为了 44 个 0,所以这里直接亦或
assertEqual
传入的两个值就可以得到前 32 个 bytevoid assertEqual(uint32_t a, uint32_t b) { uint8_t res = a == b; if (!res) { printf("%c", (char)(a ^ b ^ 0)); } }
- 得到前 32 byte 是
16584abc45baff901c59dde3b1bb6701
Part 2
- 后面逻辑就复杂很多了。这时候变成了每次取 4 个
byte
按照大端序转换成int
,再运算并进行比较 - 这里首先试了试 z3 能不能解出来,发现不行,但是由于输入的字符都是 16 进制字符(
0..9...f
),所以可以考虑爆破出来 - 爆破的话选择了多线程比较 nb 的 Golang
- 这里还有一个不太好处理的问题就是运算的时候从指令里面取了立即数。这里我选择动调然后把取的立即数 copy 出来
脚本
- 前 32 字节直接运行 C 代码就能得到
- 后 12 字节通过 Golang 爆破
package main import ( "encoding/binary" "fmt" "sync" ) type Caculator func(uint32) uint32 type Task struct { calc_f Caculator plain uint32 cipher uint32 solved bool } func main() { ciphers := []uint32{0x6FEBF967, 0x0CF1304DC, 0x283B8E84} caculators := []Caculator{calc0, calc1, calc2} tasks := make([]Task, 0) for i := 0; i < len(ciphers); i++ { tasks = append(tasks, Task{ calc_f: caculators[i], cipher: ciphers[i], solved: false, }) } brute(tasks) for i := 0; i < len(ciphers); i++ { fmt.Print(unpack(tasks[i].plain)) } } func brute(tasks []Task) { var a, b, c, d uint8 alphabet := []byte("0123456789abcdef") wg := sync.WaitGroup{} for _, a = range alphabet { for _, b = range alphabet { for _, c = range alphabet { for _, d = range alphabet { guess := pack(a, b, c, d) for i := 0; i < 3; i++ { if !tasks[i].solved { wg.Add(1) tI := i go func() { defer wg.Done() result := tasks[tI].calc_f(guess) if result == tasks[tI].cipher { tasks[tI].solved = true tasks[tI].plain = guess fmt.Println(guess) } }() } } } } } } wg.Wait() } func pack(a, b, c, d uint8) uint32 { raw := []uint8{a, b, c, d} return binary.BigEndian.Uint32(raw) } func unpack(val uint32) string { bs := make([]byte, 4) binary.BigEndian.PutUint32(bs, val) return string(bs) } func calc0(guess uint32) uint32 { r := make([]uint32, 6) r7 := guess r[1] = r7 r[1] >>= 5 r[1] ^= r7 r7 = r[1] r[1] <<= 7 r[1] &= 2565961507 r[1] ^= r7 r7 = r[1] r[1] <<= 24 r[1] &= 904182048 r[1] ^= r7 r7 = r[1] r[1] >>= 18 r[1] ^= r7 return r[1] } func calc1(guess uint32) uint32 { r := make([]uint32, 6) r7 := guess r[1] = r7 r[3] = 32 r[1] *= r[3] r[4] = r[1] r[1] ^= r7 r[2] = 17 r[1] >>= r[2] r[5] = r[1] r[1] = r[5] r[1] ^= r[4] r[1] ^= r7 r[2] = 13 r[1] <<= r[2] r[1] ^= r7 r[1] ^= r[4] r[1] ^= r[5] r7 = r[1] r[3] = 32 r[1] *= r[3] r[4] = r[1] r[1] ^= r7 r[2] = 17 r[1] >>= r[2] r[5] = r[1] r[1] = r[5] r[1] ^= r[4] r[1] ^= r7 r[2] = 13 r[1] <<= r[2] r[1] ^= r7 r[1] ^= r[4] r[1] ^= r[5] return r[1] } func calc2(guess uint32) uint32 { r := make([]uint32, 6) r7 := guess r[1] = r7 r[3] = 32 r[1] <<= 5 r[4] = r[1] r[1] ^= r7 r[2] = 17 r[1] >>= r[2] r[5] = r[1] r[1] = r[5] r[1] ^= r[4] r[1] ^= r7 r[2] = 13 r[1] <<= r[2] r[1] ^= r7 r[1] ^= r[4] r[1] ^= r[5] r7 = r[1] r[3] = 32 r[1] <<= 5 r[4] = r[1] r[1] ^= r7 r[2] = 17 r[1] >>= r[2] r[5] = r[1] r[1] = r[5] r[1] ^= r[4] r[1] ^= r7 r[2] = 13 r[1] <<= r[2] r[1] ^= r7 r[1] ^= r[4] r[1] ^= r[5] return r[1] }
- 得到后 12 byte 是
a254b06cdc23
总结
- 这题比赛的时候做了挺久,而且因为种种原因是熬夜做的…当时做得很难受,主要就是比较忙乱
- 所以就意识到虚拟机类题目一定不要想着投机取巧,比如直接黑盒啥的。最通用的方法就是还原所有执行的代码,然后再当成普通题目解