CTF中32位程序调用64位代码的逆向方法

栏目:远程教育  时间:2023-01-31
手机版

  

  背景

  在CTF中,逆向的玩法越来越多变,曾经出现过32位程序调用64位代码的情况,一般的静态分析和动态调试方法都会失效,让人十分头大,今天将通过2个案例来学习如何应对这种情况。

  案例

  2个案例包括1个windows程序和1个linux ELF程序,正好覆盖了2个常见的平台,下载地址 (提取码:nxwx)

  1. father and son (ELF),来源于2018年护网杯CTF2. GWoC (Windows),来源于2018年CNCERT CTF

  基础知识

  在x64系统下的进程是有32位和64位两种工作模式,这两种工作模式的区别在于CS寄存器。32位模式时,CS = 0x23;64位模式时,CS = 0x33。;

  这两种工作模式是可以进行切换的,一般会通过retf指令,一条retf指令等效于以下2条汇编指令

  如果此时栈中有0x33,则会将0x33弹出到CS寄存器中,实现32位程序切换到64位代码的过程。所以retf是识别32位程序调用64位代码的重要标志。

  案例1:father and son

  二进制文件father来自于一个流量包的内容(非本文焦点),是一个32位的ELF程序

  程序分析

  核心代码如下

  用nmap开辟了两段RWX内存,并且将0x804A060的内容拷贝到其中一块RWX内存0xDEAD000处,并用sub_80484EB函数异或恢复代码。

  最后的部分IDA没有识别出来,看汇编是用retf跳转到0xDEAD000处执行。

  看到retf,又开到此时栈中有0x33,符合32位程序调用64位代码的模式。

  执行分析

  使用一般的逆向工具gdb,在0x08048642处设置断点

  断点触发后,用ni单步执行指令执行下一步,可以看到指令已经跳转到0xDEAD000空间,CS寄存器的值从0x23变为0x33,进入64位代码的空间。

  

  然而此时代码内容无法显示64位汇编

  

  此时继续用ni单步执行指令,就会看到汇编指令没有一条条执行,而是几步一跳的执行,这是因为gdb认为这段代码是32位而不是64位的,即使使用set architecture i386:x86-64 命令,也会提示错误。

  

  我也尝试过以下调试方法,均已失败告终。

  1. IDA+linux_server(IDA32位版本)进行调试,效果同gdb,无法识别64位汇编代码,可以单步执行,汇编指令也是几步一跳。2. IDA64+linux_server64(IDA64位版本),程序无法引导起来。

  那么应该如何动态调试呢?

  动态调试

  为了可以正确执行64位指令,可以采用gdbserver+IDA64的调试方式。

  gdbserver启动程序,并绑定到1234端口(冒号前不带ip使用本机ip)

  用IDA64打开程序,此时是无法使用F5查看伪代码的,但是可以看到IDA64识别了32位的程序,汇编能够正常显示。

  

  在0x8048642的retf处设置断点,设置好连接gdbserver的参数(如图)

  

  点击绿色三角形按钮启动调试,一次F9运行后,到达断点处。

  

  再按F7进入64位代码,此时EIP显示已经进入了0xDEAD000,但是汇编窗口没有提示。即使使用G跳转到地址0xDEAD000也提示出错。

  这是因为IDA和gdbserver连接时,内存并没有及时刷新导致。可以打开Debugger菜单中的Manual memory regions菜单项,右键Insert新建一个内存区域(这个动作每启动一次调试都要重新做)。

  

  内存区域设置起始地址为0xDEAD000,结束地址默认即可,注意选择64-bit segment。

  

  然后用G指令跳转到内存0xDEAD000,此时显示的是二进制数据。

  

  按一下C识别为汇编指令,IDA调试器可以正确识别64位汇编,按F8单步执行也不会出现几步一跳的情况,可以正常调试啦。

  

  注意1:gdbserver在一次调试结束后,第二次可能连接不上,需要kill掉再启动。

  注意2:有的ELF程序可能并不需要Manual memory regions中增加内存区域,可以通过IDA的Edit->Segments->Change Segment Attributes修改内存为64位代码

  

  静态分析

  有了动态调试方法,还需要静态分析方法的配合,提高CTF中逆向的效率。

  本案例采用了异或混淆,由于混淆不复杂,可以静态Dump出来异或恢复,也可以动态时再Dump出来。本文采用动态运行到retf指令时,利用脚本Dump出内存。

  在File菜单的Script Command菜单项中,选择IDC脚本,输入上述内容,点击Run按钮后就可以将0xDEAD000至0xDEB0000的内存导出到C盘的father64.mem文件

  

  将father64.mem拖入IDA64进行静态分析,因为缺少ELF头,IDA64会提问选择哪种格式,此处选择64-bit mode分析代码。

  

  此时代码基地址是0x0,可以用Edit->Segments->Rebase Segment重定义基地址,设置为0xDEAD000,这样动态调试时和静态调试时的汇编地址就一样了。

  

  然后可以愉快的用F5生成C语言代码了。

  

  逆向破解

  由于本文侧重点在于如何识别和分析32位程序调用64位代码,因此案例的算法逆向篇幅部分会比较简略,有兴趣的朋友可以自行研究。

  主流程sub_DEAD44B接收用户输入和输出结果,并且判断输入格式是否为hwbctf{…}。

  而sub_DEAD16F函数则是有13个方程组判断输入的内容

  用Z3可以求解得flag为hwbctf{1’m n0t 4n5}

  案例2: GWoC

  GWoC是一个32位的Windows程序

  

  原题程序中有较多花指令和反调试部分,利用0x90来nop掉,附件提供的是一个Patch后的代码

  

  程序分析

  将patch后的程序拖入IDA32位中,看到主流程如下

  程序将32个字符的输入放入4个线程参数中,启动4个线程,每个线程都是调用同一个函数,只是参数不同。

  在这里又看到熟悉的push 33h和retf,就是进入64位代码的特征。进入的loc_C72067地址,无法正确识别64位汇编指令。

  静态分析

  因为这个案例中的代码可以静态dump出来,我们先进行静态分析。

  使用案例1的Dump方法,拖入IDA64分析,可以恢复出代码,但会有一些内存引用的错误,这是因为缺少了上下文内存。

  

  虽然也可以分析,但是在这个案例中,可以尝试使用更优雅的方式。

  在010Editor中,用PE模板打开exe文件,偏移大概是0x118处,修改标识32位的0x10b为64位的0x20b。

  

  然后放入IDA64中分析,Rebase Segment为0,再次看原来loc_C72067的地方(rebase后为0x2067),此时F5也可以识别出一些函数了,可以顺着分析sub_1C57和sub_1437等函数了。

  4个线程都是调用这个函数,但是由于输入参数的不同,会选取不同的函数调用。

  例如之前对应的是这里的判断 ,所以这部分输入调用的是sub_1437 函数,v4[1]就是实际输入串中第8个到第15个字符,即input[8:16] 。

  进入分析sub_1437 ,发现是一个流式加密,根据F5的结果逆向比较复杂,还想结合动态运行结果进行逆向。

  动态调试

  在WIndows下,IDA32、IDA64和Ollydbg这些调试器在retf指令执行后都无法正常运行,在师傅的指点下,采用windbg作为动态调试工具。

  用Windbg 64位打开目标程序File->Open Executable,注意输入命令行参数

  

  在View菜单打开Disassembly(汇编)、Registers(寄存器)、Memory(内存)和Command(命令)窗口,布局如下

  

  一开始我们要在retf处设置断点,怎么设置呢?IDA中,rebase segment为0后,可以看到retf的地址为0x134c,所以在windbg的Disassembly窗口输入GWoC+0x134c,确定也是retf,按F9设置断点。

  

  按F5执行到断点处,再按F8单步进入执行,此时CS寄存器可以看到已经变成0x33,进入64位代码块

  

  此时再在我们想调试的sub_1437 函数加入断点,在Disassembly窗口输入GWoC+0x1437,按F9加断点,然后F5运行到断点处,就能愉快的开始调试了。

  

  

  IDA也有链接Windbg的功能,但是本文所采用的IDA 7.0版本并未能成功连上windbg进行调试,只能IDA用于静态分析,Windbg进行动态调试,两边结合逆向。

  逆向破解

  以输入12345678901234567890123456789012为例

  算法1

  输入参数:0xfab输入内容:input[8:16] = “90123456”调用函数:sub_1437算法内容:流式加密,根据结果逆推即可逆向结果:F@AzOpFx

  算法2

  输入参数:0xf0f0f0输入内容:input[16:24] = “78901234”调用函数:sub_1C57算法内容:低4位和高4位分开运算,多次位移和异或运算,可用暴力破解逆向结果:Cq!9x9zc

  算法3

  输入参数:0x136D7374F06B3430输入内容:input[:8] = “12345678”调用函数:sub_1F77算法内容:低4位和高4位分开进行快速幂取模操作,就是RSA,分解因数解密RSA即可逆向结果:flag{RpC

  算法4

  输入参数:0x43434343434343输入内容:input[24:] = “56789012”调用函数:sub_1C37算法内容:输入异或0x9C70A3C478EF826A,根据结果异或即可逆向结果:fVz5354}

  所有字符串拼接在一起得到flag{RpCF@AzOpFxCq!9x9zcfVz5354}

  小结

  本文通过windows和linux的案例,整理了32位程序调用64位代码的识别方法、静态分析和动态调试技巧。

  识别方法

  1. retf是切换32位和64位的关键指令。2. retf前有push 0x33(33h)类似的指令。

  1. retf后CS寄存器从0x23变为0x33。2. 程序中可能有进行支持64位的检查,如GWoC。3. 当一块可执行的内存,调试时无法识别汇编或者几步一跳时,有可能在是执行64位的代码。4. 32位代码调用函数的方式和64位代码有差异,32位程序大多通过入栈方式传参,64位程序一般用寄存器传参。5. 32位和64位的syscall的含义和参数有所不同。

  静态分析

  1. 修改PE/ELF头位64位,让IDA64识别其中64位的部分代码。2. 静态/动态dump出内存中的64位代码片段,拖入IDA64分析代码。3. 有时候可以通过IDA中Change Segment Attributes 设置为64位,进行汇编分析。4. 使用Rebase Segment对齐基地址方便进行静结合分析

  动态调试

  1. Linux ELF程序可使用gdbserver和IDA64的组合进行调试。2. Windows程序使用Windbg进行动态调试,使用IDA64进行静态分析,动静结合逆向。

  参考

  32位程序下调用64位函数——进程32位模式与64位模式切换32位程序调用64位函数——开源代码rewolf-wow64ext学习笔记ELF头结构PE头结构

上一篇:任正非有多么求贤若渴?一个博士毕业生,年薪却高达200万!
下一篇:为梦想插上翅膀

最近更新远程教育