使用 IDA Hook 解决虚拟机逆向题目


背景

vm 题在虚拟机指令分析还原出每个指令之后,如果程序把原程序藏得很深,暂时没办法提取出 vm 的汇编程序,可以尝试 hook 每一条指令,并打印出来,类似于 Python 的 trace,跟踪程序的执行过程。对于算法较简单的 vm ,这种方法是很有效的,看 trace 就可以还原出算法。

比较常用的是 Frida hook,简单易上手,但是遇到一些结构稍复杂的程序,如 SMC(动态代码加密)、fork 新进程,frida 就会出现一些奇妙的 bug。

在一次 CTF 解 vm 题过程中遇到了 frida 无法启动的 bug。突然想到,IDA 既然能在每一个 vm 指令断点,配合 IDA Python,应该也可以实现 trace。

 

题目复现

[2024ImaginaryCTF] printf

printf libc.so.6 

题目给了两个文件 printflibc.so.6printf 中 main 函数只有一行,调用 printf 打印了一个很长的字符串。

但是这行代码不仅实现了打印,而且还接收了输入。

sudo apt install patchelf
patchelf --set-rpath ./ printf
❯ ldd printf
        linux-vdso.so.1 (0x00007ffe7b5e9000)
        libc.so.6 => ./libc.so.6 (0x00007fe7b8cb7000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fe7b8f00000)
❯ ./printf
-------------------------------------
     WELCOME TO THE PRINTF VAULT
-------------------------------------
Enter the flag: 

在程序请求输入的时候,在 IDA 按“暂停”,就可以把程序暂停在请求输入的 libc 代码块,查看 stack trace ,即可找到一个调用了 getchar 的、在 printf 程序里的函数,重命名为 ok_get_save_char

getchar 之后,会把数据存在一个全局数组中,打上硬件断点,在另一个函数断了下来,功能是将该字符与一个大数相加,又存进了另一个全局数组,就把函数重命名为 ok_add ,以此类推。printf 程序的函数数量不多而且都只有几行汇编,静态分析和动态调试,大致还原出 vm 指令,并重命名:

开始尝试 trace vm 指令,在每一个 vm 指令的关键汇编下断点,比如在 ok_addadd rcx, rax 下断,读取 rcx 和 rax 的值并打印,以此类推。

然后交给 IDA Python 自动化(部分由 chatgpt 生成,看官方文档比较费劲)

import idaapi
import idc
import ida_dbg

output = open("D:/idadeb_hook.txt", "w")

def okprint(x):
    print(x, file=output)
    output.flush()

# 记得最后关闭文件 output.close()
# 定义断点事件处理器
class MyDbgHook(idaapi.DBG_Hooks):
    def __init__(self):
        idaapi.DBG_Hooks.__init__(self)

    def dbg_bpt(self, tid, ea):
        # print("Breakpoint hit at address: 0x{:X}".format(ea))
        # 处理断点事件,如打印当前函数名
        func = idaapi.get_func(ea)
        if func:
            func_name = idaapi.get_func_name(func.start_ea)
            match func_name:
                case 'ok_get_save_char':
                    okprint(f"get_char: {chr(idc.get_reg_value('RAX'))}")
                    print(f"get_char: {chr(idc.get_reg_value('RAX'))}")
                case 'ok_push_num':
                    okprint(f"push_num: {idc.get_reg_value('RAX')}")
                case 'ok_push_reg':
                    okprint(f"push_reg: {idc.get_reg_value('RAX')}")
                case 'ok_pop_reg':
                    okprint(f"pop_reg: {idc.get_reg_value('RAX')}")
                case 'ok_add':
                    okprint(f"{idc.get_reg_value('RAX')} + {idc.get_reg_value('RCX')} = {(idc.get_reg_value('RAX') + idc.get_reg_value('RCX')) & 0xFFFFFFFFFFFFFFFF}")
                case 'ok_sub':
                    okprint(f"{idc.get_reg_value('RAX')} - {idc.get_reg_value('RDX')} = {(idc.get_reg_value('RAX') - idc.get_reg_value('RDX')) & 0xFFFFFFFFFFFFFFFF}")
                case 'ok_mul':
                    okprint(f"{idc.get_reg_value('RAX')} * {idc.get_reg_value('RDX')} = {(idc.get_reg_value('RAX') * idc.get_reg_value('RDX')) & 0xFFFFFFFFFFFFFFFF}")
                case 'ok_div':
                    okprint(f"{idc.get_reg_value('RAX')} / {idc.get_reg_value('RSI')} = {(idc.get_reg_value('RAX') // idc.get_reg_value('RSI')) & 0xFFFFFFFFFFFFFFFF}")
                case 'ok_mod':
                    okprint(f"{idc.get_reg_value('RAX')} % {idc.get_reg_value('RCX')} = {(idc.get_reg_value('RAX') % idc.get_reg_value('RCX')) & 0xFFFFFFFFFFFFFFFF}")
                case 'ok_xor':
                    okprint(f"{idc.get_reg_value('RAX')} ^ {idc.get_reg_value('RCX')} = {(idc.get_reg_value('RAX') ^ idc.get_reg_value('RCX')) & 0xFFFFFFFFFFFFFFFF}")
                case _:
                    pass

            ida_dbg.continue_process()
        return 0


# 注册断点事件处理器
dbg_hook = MyDbgHook()
dbg_hook.hook()

# 提示用户调试器已准备好
print("Debugger hook set. You can now run the debugger.")

得到 trace 记录

idadeb_hook.txt 

分别输入 abcdeictf{ 进行测试,看出来是以四个字符一组进行加密和验证,如果不通过,就退出程序,大致算法如下:

C[0] * flag[0] + C[1] * flag[1] + C[2] * flag[2] + C[3] * flag[3] == C[4] ^ C[5]
C[6] * flag[4] + C[7] * flag[5] + C[8] * flag[6] + C[9] * flag[7] == C[10] ^ C[11]
...

只需要提取出来每组字符串的六个数 C ,就能用 z3 解出来该组字符,对 IDA Python 脚本稍加改进,加上提取 C 的代码

import idaapi
import idc
import ida_dbg

##########################
tmp_char = 0
results = []
##########################

okprint = lambda x: 0
# 定义断点事件处理器
class MyDbgHook(idaapi.DBG_Hooks):
    def __init__(self):
        idaapi.DBG_Hooks.__init__(self)

    def dbg_bpt(self, tid, ea):
        global results, tmp_char
        func = idaapi.get_func(ea)
        if func:
            func_name = idaapi.get_func_name(func.start_ea)
            match func_name:
                case 'ok_get_save_char':
                    tmp_char = idc.get_reg_value('RAX') # 存储当前字符,后续用于获取大数
                case 'ok_mul':
                    if tmp_char == idc.get_reg_value('RAX'):
                        results.append(idc.get_reg_value('RDX'))
                case 'ok_xor':
                    results.append(idc.get_reg_value('RCX'))
                    if (len(results) == 6):
                        results.append(results)
                        results = []
                case _:
                    pass

            ida_dbg.continue_process()
        return 0

# 注册断点事件处理器
dbg_hook = MyDbgHook()
dbg_hook.hook()

# 提示用户调试器已准备好
print("Debugger hook set. You can now run the debugger.")

debug 完成之后,输入 result[-6:] 即可获取当前组的 C,扔进 z3 解出四个字符,再尝试,再解出,以此反复。比如解出来了 ictf{n3v3 那就尝试 ictf{n3v3aaaa 再次 debug。解到最后的 z3 脚本如下:

from z3 import *
dumps = [0xc96667e9702c2e3a, 0xaad2bead0cccee35, 0x7ef7e2c462166690, 0x62aa3adbbe49372f, 0x54ccfb02994249cf, 0xd66a6b37a1f3c38c]
dumps = [0x342e70ab89920dd9, 0x8694cfce23dc0df1, 0x5e14dede8db175aa, 0x488ede6e062613be, 0x9d29cb475e41cb8f, 0x8b6d914ccfe3e4cc]
dumps = [0x3b37278be935996a, 0x5ae8a03882f8a044, 0x656c5d76772fa64c, 0x469fd7c2f2d02294, 0xdd11c039fe5f588f, 0x36296d6ff73c1725]
dumps = [0xe6e4f9dd08b7d237, 0x683b5ae9a60b299e, 0xce7e52def829b2f, 0x60e53b578e7a8fb0, 0x9de3f8582e267573, 0xc74eea217c1780cf]
dumps = [0x81fad7adeffcbe62, 0x50227d45d028e2a7, 0x865d727a1c643594, 0x67e9e9f9ccbb1752, 0x54ccfb02994249cf, 0xb37269bc0cf143c3]
dumps = [0x8fb581c1d37fb102, 0xa3b67ebd7581d178, 0x2e5b50ca6a2ca301, 0xd9dd813ddff69e50, 0x54ccfb02994249cf, 0xf2a053d9454e1199]
dumps = [0xe3cf5a5a34477b7f, 0x17a99ead1dda85d5, 0x11c933f01965c94a, 0xdbe796533b2c5073, 0x9715552a8c24bd9f, 0xf6b85b6bab132df0]
dumps = [0x48d8b5385ca9de5e, 0x74c57cbf4dc60c5c, 0x473ffdd19d8d3e68, 0x7d1b9e50cab6cf, 0x54ccfb02994249cf, 0xb7fa65c680cb955c]
dumps = [0x4ad903a7737bf6d, 0xfe24ae2919fb080a, 0x91fee69eb0426269, 0xa311c78ac6eed0e3, 0x9715552a8c24bd9f, 0x5af0e6ba579b8c16]
dumps = [0x172a2cfcc154e672, 0xba5e6196c6d0dbaa, 0x76c5558dd76520b6, 0x7edae72078618adc, 0x9de3f8582e267573, 0x8e39f4391d389a5d]
dumps = [0x75982e11c6e9f787, 0xded3a49eaab0bbca, 0xfe3652659fa79527, 0x6b8d71916d0dc21, 0x54ccfb02994249cf, 0x4c4a722b516d0dc2]
dumps = [0xf6816364e882e5d0, 0xd3de330e05e340bf, 0xf3009bcf092a1bf3, 0xb98fd6eaeaa31de4, 0x9715552a8c24bd9f, 0xbd1b348cb14b1a0c]
dumps = [0x3cbb28b00d982646, 0xc01e2683ea8c4f04, 0xb0728f6694068514, 0x4a3cf42d4f74f903, 0xdd11c039fe5f588f, 0xca37dfc59b0d445c]
dumps = [0xcb8bcc57d18df0a7, 0x9bc154f177dba69b, 0x1ce10b01008fe0b8, 0x393ca8f9f07754f1, 0x9d29cb475e41cb8f, 0x3ea20b9736418168]
dumps = [0xa0e1d7496e8f3692, 0x792055bbe67c455e, 0x23c9f1c9f9352918, 0xee71cc15ec4af890, 0xdd11c039fe5f588f, 0x13654f2ad9a2debf]
dumps = [0xc16f11ba163fe9a6, 0xf7cceee60cad4500, 0xe296a079970a4d01, 0xe06519e67ff54956, 0xdd11c039fe5f588f, 0x38595695629e5e96]
dumps = [0x5cc42aea4fad516b, 0x654b96d840b731b3, 0x238c34b915584ceb, 0xd9401f52df46b0fb, 0x9d29cb475e41cb8f, 0x1c1c82f150f07462]
dumps = [0xf9923bc7ae594205, 0x97b9f4f4c11b13ee, 0x2452efbf17fce49, 0x7b75687e9288270a, 0x54ccfb02994249cf, 0xcfae6899f240f30a]
flag = [BitVec('flag_%d' % i, 32) for i in range(4)]

s = Solver()

[
    s.add(flag[i] > 0x20, flag[i] < 0x7e) for i in range(4)
]

calcs = [
    flag[0] * dumps[0],
    flag[1] * dumps[1],
    flag[2] * dumps[2],
    flag[3] * dumps[3]
]

s.add(sum([calc & 0xFFFFFFFFFFFFFFFF for calc in calcs]) ^ dumps[4] == dumps[5])

if s.check() == sat:
    model = s.model()
    print(''.join([chr(model[flag[i]].as_long()) for i in range(4)]))

else:
    print('unsat')

 

 

 


运行时间 427 天 | 总访问量