首页
论坛
课程
招聘
[原创]羊城杯OddCode题解(unicorn模拟调试+求解)
2021-9-13 19:39 17791

[原创]羊城杯OddCode题解(unicorn模拟调试+求解)

2021-9-13 19:39
17791

首先恭喜0x401 Team首次在CTF比赛中夺得第一名,顺便和学弟AK了逆向,战队能取得今天的成绩离不开队员的努力。但是不得不承认另一个原因是,很多强队的火力都被隔壁RCTF吸引了,我们还需要继续努力:
图片描述

0xFF. 前言

自从上次强网杯unicorn那题以来我就一直对unicorn很感兴趣,但平时又没有用unicorn解决实际问题的场景,这次羊城杯总算碰到了,借此机会学习一下unicorn。OddCode这题有大量花指令和垃圾跳转,手动分析几乎不可能,如果使用unicorn模拟执行会方便很多。

 

比赛时一直爆肝到凌晨5点才把这题弄出来(被屑出题人的大小写坑了3个小时),本文的解法是我赛后优化之后的解法。

0x00. 概览-32位模式部分

首先这是一个32位的可执行文件:
图片描述
没有main函数,程序直接从start函数开始执行,首先是一段校验输入格式的代码:
图片描述
一个很奇怪的远跳转,一开始在IDA看了半天没明白是什么意思,用WinDbg调试后才发现IDA的反汇编有问题:
图片描述
实际上是一个远跳转到33:2E5310这个地址,远跳转有一个隐形的操作,他会将代码段寄存器CS设置为跳转到的这个段对应的段选择子,这里执行完了远跳转之后,CS的值被置为0x33:
图片描述
这里涉及一个我之前折腾自制操作系统时接触到的一个知识点——在Windows中,程序可以通过修改代码段寄存器切换32位模式和64位模式,当CS为0x33时,CPU按64位模式执行指令,当CS为0x23,时,CPU按32位模式执行指令。执行完这个远跳转后,程序跳转到2E5310这个地址(也就是下一条指令),CPU切换到64位模式执行,所以接下来的代码都要按64位模式解析。

 

这个技术的一个典型应用是在恶意代码领域,参考:天堂之门(Heaven’s Gate)技术的详细分析

 

切换到64位模式后,执行sub_2E1010函数:
图片描述
接下来一段代码的作用是将CS的值改回0x23,切回32位模式:
图片描述
最后根据sub_2E1010函数的返回值判断输入是否正确,所以本题的关键是sub_2E1010函数:
图片描述

0x01. 概览-64位模式部分

先到回到这个部分,远跳转前的两个lea语句相当于是传递参数,把input存入esi,把key存入edi:
图片描述
key是一个16字节的数组,推测之后加密或者校验输入的时候会用上:
图片描述
从sub_2E1010函数开始的代码要用64位模式查看:
图片描述
图片描述
从这里开始有大量的垃圾代码和花指令,手动分析了半天都没找到关键代码,于是我决定用unicorn写一个模拟调试器帮我找到关键代码。

0x02. unicorn模拟调试

最开始看到用unicorn实现调试器的思路是在这篇文章:汇编与反汇编神器Unicorn。里面用到的调试器代码貌似出自无名侠:
图片描述
我们也来写个简单的调试器来模拟64位代码的执行,并且实现一个tracer,用来跟踪代码块执行的轨迹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from unicorn import *
from unicorn.x86_const import *
 
ADDRESS = 0x2E1000          # 程序加载的地址
INPUT_ADDRESS = 0x2E701D    # 输入的地址
KEY_ADDRESS = 0x2E705C      # 16字节key的地址
with open('OddCode.exe', 'rb') as file:
    file.seek(0x400)
    X64_CODE = file.read(0x4269)    # 读取代码
 
class Unidbg:
 
    def __init__(self, flag):
        mu = Uc(UC_ARCH_X86, UC_MODE_64)
        # 基址为0x2E1000,分配16MB内存
        mu.mem_map(ADDRESS, 0x1000000)
        mu.mem_write(ADDRESS, X64_CODE)
        mu.mem_write(INPUT_ADDRESS, flag)       # 随便写入一个flag
        mu.mem_write(KEY_ADDRESS, b'\x90\xF0\x70\x7C\x52\x05\x91\x90\xAA\xDA\x8F\xFA\x7B\xBC\x79\x4D')
        # 初始化寄存器,寄存器的状态就是切换到64位模式之前的状态,可以通过动调得到
        mu.reg_write(UC_X86_REG_RAX, 1)
        mu.reg_write(UC_X86_REG_RBX, 0x51902D)
        mu.reg_write(UC_X86_REG_RCX, 0xD86649D8)
        mu.reg_write(UC_X86_REG_RDX, 0x2E701C)
        mu.reg_write(UC_X86_REG_RSI, INPUT_ADDRESS)  # input参数
        mu.reg_write(UC_X86_REG_RDI, KEY_ADDRESS)    # key参数
        mu.reg_write(UC_X86_REG_RBP, 0x6FFBBC)
        mu.reg_write(UC_X86_REG_RSP, 0x6FFBAC)
        mu.reg_write(UC_X86_REG_RIP, 0x2E1010)
        mu.hook_add(UC_HOOK_CODE, self.trace)        # hook代码执行,保存代码块执行轨迹
        self.mu = mu
        self.except_addr = 0
        self.traces = []        # 用来保存代码块执行轨迹
 
    def trace(self, mu, address, size, data):
        if address != self.except_addr:
            self.traces.append(address)
        self.except_addr = address + size
 
    def start(self):
        try:
            self.mu.emu_start(0x2E1010, -1)
        except:
            pass
        print([hex(addr)for addr in self.traces])
 
Unidbg(b'SangFor{00000000000000000000000000000000}').start()

unicorn可以hook代码块执行,但是会被花指令干扰,所以这里通过hook指令执行,再判断当前的地址是否与上次执行的地址+上一条指令的长度是否相等来判断是否发生了代码块跳转:

1
2
3
4
def trace(self, mu, address, size, data):
    if address != self.except_addr:
        self.traces.append(address)
    self.except_addr = address + size

模拟执行的过程中会莫名其妙报错,所以直接加了一个try,最后打印出来的轨迹如下:

1
['0x2e1010', '0x2e3634', '0x2e3e1d', '0x2e389c', '0x2e3d9e', '0x2e3b8e', '0x2e37ae', '0x2e3f3a', '0x2e4ee5', '0x2e51ad', '0x2e45f9', '0x2e4e03', '0x2e3c8f', '0x2e4cf1', '0x2e4e96', '0x2e3d49', '0x2e3641', '0x2e4ca8', '0x2e49fd', '0x2e5109', '0x2e4e16', '0x2e382a', '0x2e48f1', '0x2e3ec2', '0x2e4567', '0x2e3a7e', '0x2e4ae0', '0x2e3718', '0x2e402f', '0x2e4ba1', '0x2e4263', '0x2e4441', '0x2e4af2', '0x2e42f7', '0x2e5163', '0x2e3dd1', '0x2e49b7', '0x2e4907', '0x2e4ddb', '0x2e2896', '0x2e2e08', '0x2e35a4', '0x2e2bd2', '0x2e32a2', '0x2e2cf2', '0x2e296d', '0x2e2eb6', '0x2e3391', '0x2e2f9b', '0x2e2ff8', '0x2e2b83', '0x2e3082', '0x2e2ab3', '0x2e333e', '0x2e2ee9', '0x2e2bc5', '0x2e3519', '0x2e3447', '0x2e31a1', '0x2e33fa', '0x2e2bba', '0x2e3623', '0x2e2b95', '0x2e2e99', '0x2e308d', '0x2e33a0', '0x2e3473', '0x2e35ac', '0x2e2b21', '0x2e2980', '0x2e341d', '0x2e31d4', '0x2e32ab', '0x2e30e2', '0x2e289c', '0x2e2acb', '0x2e30f4', '0x2e34f8', '0x2e3176', '0x2e2e5d', '0x2e2cfe', '0x2e2bfb', '0x2e2f15', '0x2e2c6e', '0x2e2ea5', '0x2e305d', '0x2e2f91', '0x2e3267', '0x2e3210', '0x2e324a', '0x2e330f', '0x2e32d9', '0x2e2e78', '0x2e2924', '0x2e34d5', '0x2e2c19', '0x2e3121', '0x2e2907', '0x2e2a75', '0x2e332e', '0x2e2dc9', '0x2e2edc', '0x2e353d', '0x2e2c2f', '0x2e2cd4', '0x2e28e4', '0x2e2b6c', '0x2e3481', '0x2e294b', '0x2e2b40', '0x2e2e83', '0x2e2f4d', '0x2e31f8', '0x2e4df6', '0x2e4177', '0x2e496d', '0x2e37a1', '0x2e3a3a', '0x2e4d76', '0x2e3e38', '0x2e45bc', '0x2e3f86', '0x2e3df5', '0x2e4242', '0x2e3aee', '0x2e5039', '0x2e3ff8', '0x2e4cb9', '0x2e48a1', '0x2e4135', '0x2e3d05', '0x2e4bd9', '0x2e3c0e', '0x2e5133', '0x2e42d7', '0x2e4bff', '0x2e39fe', '0x2e50a8', '0x2e4a2f', '0x2e4e6a', '0x2e43f6', '0x2e401d', '0x2e43a1', '0x2e4b95', '0x2e37d5', '0x2e404d', '0x2e37c6', '0x2e46b3', '0x2e5120', '0x2e5013', '0x2e5075', '0x2e4673', '0x2e45e1', '0x2e3ba2', '0x2e4802', '0x2e481c', '0x2e38d6', '0x2e4f11', '0x2e4494', '0x2e41f1', '0x2e3853', '0x2e504d', '0x2e4529', '0x2e50df', '0x2e3671', '0x2e3968', '0x2e3741', '0x2e4074', '0x2e368e', '0x2e4ffb', '0x2e4c86', '0x2e491f', '0x2e432b', '0x2e3e8c', '0x2e3f97', '0x2e38e5', '0x2e44bc', '0x2e444e', '0x2e3a48', '0x2e39c9', '0x2e46d2', '0x2e3982', '0x2e3eed', '0x2e4682', '0x2e3d7c', '0x2e3eb6', '0x2e3c25', '0x2e4390', '0x2e462c', '0x2e4957', '0x2e4a0c', '0x2e486e', '0x2e493b', '0x2e4479', '0x2e4760', '0x2e4ed5', '0x2e4eb6', '0x2e4d52', '0x2e39a8', '0x2e41bb', '0x2e4e48', '0x2e39b4', '0x2e513e', '0x2e41a4', '0x2e473a', '0x2e4abe', '0x2e47d8', '0x2e4650', '0x2e51b7', '0x2e4367', '0x2e3b75', '0x2e3c63', '0x2e4542', '0x2e487f', '0x2e4b79', '0x2e4ccc', '0x2e3cc8', '0x2e4d28', '0x2e36f1', '0x2e4a7b', '0x2e3cd3', '0x2e3e98', '0x2e4f28', '0x2e3847', '0x2e38ac', '0x2e365c', '0x2e454f', '0x2e3944', '0x2e4105', '0x2e4506', '0x2e4bb6', '0x2e3893', '0x2e4c71', '0x2e3839', '0x2e4f3b', '0x2e3bca', '0x2e3795', '0x2e3b16', '0x2e40c9', '0x2e3d3c', '0x2e3afe', '0x2e5230', '0x2e419c']

这么多代码块一个个去手动分析不太现实,于是再加一个hook来hook输入和key的访问操作,来帮助我们找到了访问了输入和key的指令所在的代码块,加上:

1
2
3
4
5
6
7
mu.hook_add(UC_HOOK_MEM_READ, self.hook_mem_read)
 
def hook_mem_read(self, mu, access, address, size, value, data):
    if address >= INPUT_ADDRESS and address <= INPUT_ADDRESS + 41:
        print(f'Read input[{address - INPUT_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')
    if address >= KEY_ADDRESS and address <= KEY_ADDRESS + 16:
        print(f'Read key[{address - KEY_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')

输出:

1
2
3
4
5
6
7
Read input[8] at 0x2e326d
Read input[8] at 0x2e3214
Read input[8] at 0x2e3219
Read input[9] at 0x2e324a
Read input[9] at 0x2e3254
Read input[9] at 0x2e325e
Read key[0] at 0x2e3a3e

通过内存访问hook我们得到了几个很重要的信息:

  • 读取输入的地址
  • 读取key的地址
  • 输入可能恒是2字节一组进行加密后比较
  • 当前比对失败后程序不会继续比对剩下的部分

第三、四个特点是一个伏笔,之后我们会利用这个性质对flag进行爆破。

 

接下来看到访问了输入的几段代码,这些代码的作用是将第一个字节读入到al,第二个字节读入到bl:
图片描述
图片描述
图片描述
...
从这里开始,顺着我们之前打印出的轨迹往后再分析一会还能发现这样的代码:
图片描述
图片描述
说明程序确实是将16进制两字节的输入转换成了对应的16进制数。
再来看到访问了key的代码块:
图片描述
我们再修改一下trace函数,通过capstone反汇编引擎找到执行到的cmp指令和test指令的地址:

1
2
3
4
5
6
7
8
9
def trace(self, mu, address, size, data):
    disasm = self.md.disasm(mu.mem_read(address, size), address)
    for i in disasm:
        mnemonic = i.mnemonic
        if mnemonic == 'cmp' or mnemonic == 'test':
            print(f'Instruction {mnemonic} at {hex(address)}')
    if address != self.except_addr:
        self.traces.append(address)
    self.except_addr = address + size

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Instruction cmp at 0x2e3ca1
Instruction cmp at 0x2e4de8
Instruction cmp at 0x2e326d
Read input[8] at 0x2e326d
Instruction cmp at 0x2e3214
Read input[8] at 0x2e3214
Read input[8] at 0x2e3219
Instruction cmp at 0x2e324a
Read input[9] at 0x2e324a
Instruction cmp at 0x2e3254
Read input[9] at 0x2e3254
Read input[9] at 0x2e325e
Instruction test at 0x2e4177
Read key[0] at 0x2e3a3e
Instruction cmp at 0x2e38e7

可以看到在读取key之后执行的cmp指令只有一个,位于2E38E7这个地址,代码如下,大致可以确定是flag加密后比较的代码,比对成功的话不会执行jnz跳转:
图片描述
所以我们可以通过记录程序第几次执行到了2E38EF这个地址,来判断比较成功比对了几个字节,通过这种方法来爆破flag。

0x03. 爆破flag

再改一下trace函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def trace(self, mu, address, size, data):
    '''
    disasm = self.md.disasm(mu.mem_read(address, size), address)
    for i in disasm:
        mnemonic = i.mnemonic
        if mnemonic == 'cmp' or mnemonic == 'test':
            print(f'Instruction {mnemonic} at {hex(address)}')
    '''
    if address != self.except_addr:
        self.traces.append(address)
    self.except_addr = address + size
    if address == 0x2E38EF:
        self.hit += 1
        #print(f'hit {self.hit}')
        if self.hit == self.except_hit:
            self.success = True
            mu.emu_stop()

爆破flag的函数get_flag:

1
2
3
4
5
6
7
def get_flag(flag, except_hit):
    for i in b'1234567890abcdefABCDEF':
        for j in b'1234567890abcdefABCDEF':
            flag[8 + (except_hit - 1) * 2] = i
            flag[8 + (except_hit - 1) * 2 + 1] = j
            if Unidbg(bytes(flag), except_hit).solve():
                return

这里选择的字符集为b'1234567890abcdefABCDEF',包括了小写的字母,比赛的时候我是根据traces手动分析加密流程,被大小写坑了几个小时。爆破结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SangFor{A7000000000000000000000000000000}
SangFor{A7A40000000000000000000000000000}
SangFor{A7A4A000000000000000000000000000}
SangFor{A7A4A0C0000000000000000000000000}
SangFor{A7A4A0C0B10000000000000000000000}
SangFor{A7A4A0C0B10B00000000000000000000}
SangFor{A7A4A0C0B10Baf000000000000000000}
SangFor{A7A4A0C0B10Bafa70000000000000000}
SangFor{A7A4A0C0B10Bafa77600000000000000}
SangFor{A7A4A0C0B10Bafa776F5000000000000}
SangFor{A7A4A0C0B10Bafa776F55F0000000000}
SangFor{A7A4A0C0B10Bafa776F55FF400000000}
SangFor{A7A4A0C0B10Bafa776F55FF4F8000000}
SangFor{A7A4A0C0B10Bafa776F55FF4F8C60000}
SangFor{A7A4A0C0B10Bafa776F55FF4F8C6E800}
SangFor{A7A4A0C0B10Bafa776F55FF4F8C6E849}

图片描述

0x04. 完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
from ctypes import addressof
from unicorn import *
from unicorn.x86_const import *
from capstone import *
 
ADDRESS = 0x2E1000          # 程序加载的地址
INPUT_ADDRESS = 0x2E701D    # 输入的地址
KEY_ADDRESS = 0x2E705C      # 16字节key的地址
with open('OddCode.exe', 'rb') as file:
    file.seek(0x400)
    X64_CODE = file.read(0x4269)    # 读取代码
 
class Unidbg:
 
    def __init__(self, flag, except_hit):
        self.except_hit = except_hit
        self.hit = 0
        self.success = False
        mu = Uc(UC_ARCH_X86, UC_MODE_64)
        # 基址为0x2E1000,分配16MB内存
        mu.mem_map(ADDRESS, 0x1000000)
        mu.mem_write(ADDRESS, X64_CODE)
        mu.mem_write(INPUT_ADDRESS, flag)       # 随便写入一个flag
        mu.mem_write(KEY_ADDRESS, b'\x90\xF0\x70\x7C\x52\x05\x91\x90\xAA\xDA\x8F\xFA\x7B\xBC\x79\x4D')
        # 初始化寄存器,寄存器的状态就是切换到64位模式之前的状态,可以通过动调得到
        mu.reg_write(UC_X86_REG_RAX, 1)
        mu.reg_write(UC_X86_REG_RBX, 0x51902D)
        mu.reg_write(UC_X86_REG_RCX, 0xD86649D8)
        mu.reg_write(UC_X86_REG_RDX, 0x2E701C)
        mu.reg_write(UC_X86_REG_RSI, INPUT_ADDRESS)  # input参数
        mu.reg_write(UC_X86_REG_RDI, KEY_ADDRESS)    # key参数
        mu.reg_write(UC_X86_REG_RBP, 0x6FFBBC)
        mu.reg_write(UC_X86_REG_RSP, 0x6FFBAC)
        mu.reg_write(UC_X86_REG_RIP, 0x2E1010)
        mu.hook_add(UC_HOOK_CODE, self.trace)        # hook代码执行,保存代码块执行轨迹
        #mu.hook_add(UC_HOOK_MEM_READ, self.hook_mem_read)
        self.mu = mu
        self.except_addr = 0
        self.traces = []        # 用来保存代码块执行轨迹
        self.md = Cs(CS_ARCH_X86, CS_MODE_64)
 
    def trace(self, mu, address, size, data):
        '''
        disasm = self.md.disasm(mu.mem_read(address, size), address)
        for i in disasm:
            mnemonic = i.mnemonic
            if mnemonic == 'cmp' or mnemonic == 'test':
                print(f'Instruction {mnemonic} at {hex(address)}')
        '''
        if address != self.except_addr:
            self.traces.append(address)
        self.except_addr = address + size
        if address == 0x2E38EF:
            self.hit += 1
            #print(f'hit {self.hit}')
            if self.hit == self.except_hit:
                self.success = True
                mu.emu_stop()
 
 
    def hook_mem_read(self, mu, access, address, size, value, data):
        if address >= INPUT_ADDRESS and address <= INPUT_ADDRESS + 41:
            print(f'Read input[{address - INPUT_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')
        if address >= KEY_ADDRESS and address <= KEY_ADDRESS + 16:
            print(f'Read key[{address - KEY_ADDRESS}] at {hex(mu.reg_read(UC_X86_REG_RIP))}')
 
 
    def solve(self):
        try:
            self.mu.emu_start(0x2E1010, -1)
        except:
            pass
        return self.success
 
def get_flag(flag, except_hit):
    for i in b'1234567890abcdefABCDEF':
        for j in b'1234567890abcdefABCDEF':
            flag[8 + (except_hit - 1) * 2] = i
            flag[8 + (except_hit - 1) * 2 + 1] = j
            if Unidbg(bytes(flag), except_hit).solve():
                return
 
flag = bytearray(b'SangFor{00000000000000000000000000000000}')
for i in range(1, 17):
    get_flag(flag, i)
    print(flag.decode())

[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-9-22 10:44 被34r7hm4n编辑 ,原因:
上传的附件:
收藏
点赞10
打赏
分享
最新回复 (10)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_qiyxlhog 活跃值 2021-9-13 19:44
2
0
雪    币: 3673
活跃值: 活跃值 (1419)
能力值: ( LV10,RANK:165 )
在线值:
发帖
回帖
粉丝
DMemory 活跃值 3 2021-9-14 10:27
3
0
很完整
雪    币: 204
活跃值: 活跃值 (263)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
BOI_电院 活跃值 2021-9-14 22:25
4
0
牛啊牛啊
雪    币: 199
活跃值: 活跃值 (1299)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Zard_ 活跃值 2021-9-18 14:42
5
0
思路清晰 牛啊 有附件吗?跟着想学习一波
雪    币: 8064
活跃值: 活跃值 (5360)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
34r7hm4n 活跃值 6 2021-9-22 10:45
6
0
Zard_ 思路清晰 牛啊 有附件吗?跟着想学习一波
已上传到附件
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_jbrgvmfy 活跃值 2021-9-22 18:06
7
0
学到了
雪    币: 38
活跃值: 活跃值 (54)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Node Sans 活跃值 2021-9-25 14:01
8
0
大佬,话说你干 CTF 题目都是不用看 F5 的吗,感觉看你都是直接上汇编干题目
雪    币: 179
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
C0nn3R 活跃值 2021-10-25 16:01
9
0
谢谢大佬,学到很多
雪    币: 213
活跃值: 活跃值 (694)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2021-10-25 18:16
10
0
感谢分享
雪    币: 160
活跃值: 活跃值 (295)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
mingyuexc 活跃值 2021-10-31 19:55
11
0
感谢分享
游客
登录 | 注册 方可回帖
返回