下载:DDCTF 2018黑盒破解

LUOJIBASUO

这是我初学VM的第一个程序,虽说网络上有其他博主下写的WP但是对于刚开始学VM的来说我觉得是难以理解的,主要是程序的执行流程懂得了程序如何执行剩下的就好办了。同时感谢其他博主的WP学习了很多

分析文件

  • 文件下载下来后,发现有两个文件

1、ReverseMe.elf

2、flag-48ee204317.txt

  • 根据做题的经验和文件后缀大概猜测一下文件的类型是ELF64的Linux程序。直接用IDA64打开

  • 打开后看可以看到我们猜测的没错,不会猜测可以使用PEIDexeinfope

  • 打开直接搜索main函数,在右侧代码可以看到是判断了byte_603F00的变量是否正确。

  • 把用鼠标点击一下该变量光标放在鼠标上按键盘的 “X” 即可打开变量的交叉引用。如上图,Type中是 “w” 为写,如果是 “r” 就是读,从图中可以看到在sub_40133D这个子程序设置了该参数的数值。双击第一条数据可以直接跳转到对应的地方

  • 发现经过了几个判断成功后才置1,如果按照刚刚的方法直接查找该子程序的引用发现找不到在哪里调用用了该程序。不仅如此还找到了一堆函数。

  • 回到main函数,发现在判断的前面有个sub_401A48(v4);双击进去,第一眼很乱,嗯放弃吧不学了

  • 开个小玩笑,从上图可发现首先是两个循环,第二个循环的次数是八次,然后呢判断是否符合条件如果符号条件的话呢就给变量*(a1 + 672)置地址然后将该地址当函数去调用并传入了个a1参数
  • 这是算是一个小的VM保护吧,有dispatcher有handler还有eip和一些临时变量当作寄存器,而指令就只有9个指令
  • 那没办法了只能上动态调试了,在动态调试前其实我们忽略了个细节,从图中不难看出程序调用了两次fgets()分别是fgets(s, 15, stdin);fgets((char *)(v4 + 16), 100, stdin);前面第一个其实就是获取的第二个文件中flag-xxx.txt的xxx所以只需要输入xxx就可以通过第一个
  • 如我这里的文件名是 “flag-48ee204317.txt” 那么就直接输入48ee204317即可通过第一个
  • 关键点在第二个的地方,首先是分配一个内存空间赋值给v4变量
  • 然后把我们第二次输入的内容放在了*(v4 + 16)的位置准备以后使用

  • 了解了大致的流程后现在就可以上动态调试了
  • 这里使用的是VMware虚拟机的Kail Liunx64
  • 虚拟机打开IDA的远程服务端,IDA配置远程服务端的IP即可实现在虚拟机中调试而不需要装一个Linux系统
  • 动态调试前先回到main中给子程序调用的地方下个断

打开先输入第一个数据即 “48ee204317” 输入后提示输入passcode直接随便输入一串字符串回车长度最好大于9位的,至于为什么嘛往前仔细看看就懂了。

  • 可以看到传入了一个参数rdi,这里的rdi其实就是之前分配的内存的地址直接F7跟进继续分析

  • 把rdi赋值到了一个栈空间var_28

这里有一堆判断先不管先,我们只要知道主要流程即可

  • 直接F5定位到第二个for循环然后按Tab查看指令,往下找可以发现有个call eax这个就是把地址对应的数据当函数体执行的地方。在上面还有个cmp cl, al用于比较是否符合才决定是否需要跳转到下面调用函数。在两个地方下个断点进行分析

  • 运行后断后可以看到cl来至 “byte_603900” 这个数组中,而al

  • 这里看指令比较乱直接转伪代码,可以发现其实是把用户输入的每个数据当作下表从数组中取出数据和右边的数据进行比对如果一样就执行函数,从这里就可以发现函数的执行流程是根据用户的使用来执行的

从上图可知在第一个循环中每次循环给*(a1 + 664)置为用户输入的下一个值

  • 那怎么获取等号右边的数据呢?很简单切换会指令在JNZ判断中下个断点即可,因为指令只有8个所以只要每次断点下来后记录右边的数据再运行重复8次即可提取出所有的数据。还有一个方法就是直接写IDC脚本一件提取但笔者比较笨

  • 提取出来的分别是 “2a 27 3e 5a 3f 4e 6a 2b 28” 把左边数组的数据Dump下来用Python写一个脚本解除对应的用户输入的指令

  • 编写脚本运行,发现只有当输入的数据是 “$8Ct0Eu#;”中的一个才执行指令

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
import binascii


data = [ 0x02, 0x00, 0x00, 0x0E, 0x16, 0x54, 0x20, 0x18, 0x11, 0x45,
0x50, 0x59, 0x58, 0x53, 0x00, 0x08, 0x44, 0x2D, 0x46, 0x39,
0x00, 0x54, 0x42, 0x01, 0x3C, 0x0F, 0x00, 0x07, 0x17, 0x00,
0x56, 0x21, 0x00, 0x37, 0x6D, 0x2B, 0x2A, 0x6E, 0x59, 0x5D,
0x47, 0x3A, 0x4A, 0x34, 0x44, 0x48, 0x43, 0x6C, 0x3F, 0x59,
0x25, 0x33, 0x55, 0x2F, 0x31, 0x68, 0x27, 0x34, 0x7C, 0x28,
0x67, 0x59, 0x00, 0x52, 0x00, 0x26, 0x00, 0x3E, 0x56, 0x4E,
0x33, 0x21, 0x45, 0x6D, 0x60, 0x39, 0x46, 0x72, 0x6D, 0x4D,
0x54, 0x40, 0x00, 0x74, 0x57, 0x73, 0x72, 0x7A, 0x47, 0x45,
0x00, 0x71, 0x00, 0x4A, 0x35, 0x70, 0x3B, 0x36, 0x2E, 0x26,
0x2C, 0x6C, 0x4A, 0x00, 0x7C, 0x63, 0x35, 0x57, 0x4D, 0x41,
0x43, 0x62, 0x00, 0x68, 0x37, 0x00, 0x5A, 0x6A, 0x6B, 0x7C,
0x29, 0x69, 0x4C, 0x70, 0x50, 0x71, 0x26, 0x36, 0x3C, 0x06,
0x1B, 0x00, 0x3C, 0x30, 0x00, 0x00, 0x00, 0x4C, 0x0B, 0x4B,
0x48, 0x08, 0x54, 0x47, 0x12, 0x09, 0x24, 0x00, 0x00, 0x24,
0x40, 0x0D, 0x39, 0x06, 0x5C, 0x2C, 0x1A, 0x2D, 0x0A, 0x38,
0x35, 0x37, 0x16, 0x3B, 0x00, 0x24, 0x48, 0x00, 0x49, 0x00,
0x37, 0x08, 0x1F, 0x24, 0x45, 0x1D, 0x11, 0x40, 0x2F, 0x4A,
0x08, 0x15, 0x00, 0x11, 0x00, 0x1A, 0x22, 0x41, 0x52, 0x5B,
0x0B, 0x45, 0x31, 0x19, 0x43, 0x19, 0x1E, 0x0A, 0x21, 0x05,
0x4D, 0x59, 0x38, 0x34, 0x09, 0x36, 0x2F, 0x43, 0x02, 0x53,
0x12, 0x2F, 0x4C, 0x21, 0x0D, 0x3C, 0x31, 0x2E, 0x37, 0x08,
0x30, 0x29, 0x32, 0x2F, 0x00, 0x1A, 0x14, 0x41, 0x53, 0x15,
0x21, 0x00, 0x08, 0x13, 0x38, 0x5C, 0x36, 0x3B, 0x50, 0x00,
0x2F, 0x1E, 0x57, 0x00, 0x30, 0x2E, 0x0C, 0x2E, 0x37, 0x52,
0x1C, 0x33, 0x34, 0x11, 0x38]

opcode = list(binascii.a2b_hex("2a273e5a3f4e6a2b28"))
for i in opcode:
print(chr(data.index(i)))
  • 知道了输入对应的指令才执行那就 可以直接把这八个指令都一起输入然后断点在call eax进入函数分析搞懂每个指令对应的函数是干嘛的,先把所有断点删了只给call eax的地方下断点就行

  • 下来后直接不管直接F7跟进看伪代码即可,判断是执行条件。经过分析*(a1 + 292)

  • 直接切换到指令查看比较,先拿a1参数和0比较判断是否为NULL

  • 比较edx和eax,如果edx >= eax 则直接结束子程序否则继续执行

  • 分析得知 "$“ 所对应的函数是sub_400DC1:用于赋值数组index位置的字符给(a1 + 665)

  • F9放行继续断下第二次call eax根据第一个函数分析很快就知道第二个函数是干嘛的

  • “8” 所对应的是sub_400E7A:用于赋值(a1 + 665)给数组index位置的字符

  • 重复步骤分析剩下的得到变量:

    • + 288: index
    • + 8: string array
    • +16: array of user input
    • + 664: nextChar
    • + 280: 0x110
    • + 292: 0xFF
    • +665: tmp variable
  • 得到每个函数"主要"执行的操作:

    • "$“ 所对应的函数是sub_400DC1:tmp = str[index]
    • “8” 所对应的是sub_400E7A:str[index] = tmp
    • ”C“ sub_400F3A: tmp = tmp + nextChar - 0x21
    • “t” sub_401064: tmp = tmp - nextChar + 0x21;
    • “0” sub_4011C9: ++index
    • “E” sub_40133D: check
    • “u” sub_4012F3: --index
    • “#” sub_4014B9: str[index] = input[nextChar + index - 48] - 49
    • “;” sub_400CF1: str[index] = for ( i = 0; nextChar > i; ++i )index++;input[nextChar + index - 48] - 49
  • 到这一步就迷茫了分析完接下来干嘛呢?根据提示可以知道怎么程序就是让用户操作指令吧str表的字符串转换成Binggo即可那么开始构建指令

  • 首先str表的原内容是 “PaF0!&Prv}H{ojDQ#7v=” 要把这串转成 ”Binggo“

  • 先把第第一个P转换成B,查ASCII编码表可知P 对应的编码是 0x50 ,B 对应的是0x42。根据指令先把str表中第一个字符放到tmp中才可对其操作那么第一个指令就是$第二个选择一种加密方式在指令 "C t # ; "中找一个只要满足等于B即可,先选C。套用公式tmp = tmp + nextChar - 33得0x42 = 0x50 - nextChar - 0x21,解得nextChar 为0x13不是可见字符。

  • 那么换一个指令 “t” 代入公式:tmp = tmp - nextChar + 0x21得0x42 = 0x50 - nextChar + 0x21 解得nextChar = 0x2F 将其转换成可见字符 “/” 即可如果计算出来的不是可见字符可以选择其他的操作进行计算。再使用指令 “8” 将tmp赋值给str[index],使用指令 ”0“ index++ 那么,P转换成B的对应指令是$t/80

  • 最后别忘了调用指令 ”E" 验证输入

  • 以此类推得到完整的指令:t/80C)80C)80CI80CX80CX80Cg80$Cj80#J1uuuuuuuEs