内容简介:在CTF中,逆向的玩法越来越多变,曾经出现过32位程序调用64位代码的情况,一般的静态分析和动态调试方法都会失效,让人十分头大,今天将通过2个案例来学习如何应对这种情况。2个案例包括1个windows程序和1个linux ELF程序,正好覆盖了2个常见的平台,
背景
在CTF中,逆向的玩法越来越多变,曾经出现过32位程序调用64位代码的情况,一般的静态分析和动态调试方法都会失效,让人十分头大,今天将通过2个案例来学习如何应对这种情况。
案例
2个案例包括1个windows程序和1个linux ELF程序,正好覆盖了2个常见的平台, 下载地址 (提取码:nxwx)
- father and son (ELF),来源于2018年护网杯CTF
- GWoC (Windows),来源于2018年CNCERT CTF
基础知识
在x64系统下的进程是有32位和64位两种工作模式,这两种工作模式的区别在于CS寄存器。32位模式时,CS = 0x23;64位模式时,CS = 0x33。;
这两种工作模式是可以进行切换的,一般会通过retf指令,一条retf指令等效于以下2条汇编指令
pop ip pop cs
如果此时栈中有0x33,则会将0x33弹出到CS寄存器中,实现32位程序切换到64位代码的过程。 所以retf是识别32位程序调用64位代码的重要标志。
案例1:father and son
二进制文件father来自于一个流量包的内容(非本文焦点),是一个32位的ELF程序
$ file father father: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4351dc8fde1bd3404207e1540b84e3c577c81521, stripped
程序分析
核心代码如下
int sub_8048527() { signed int retaddr; // [esp+2Ch] [ebp+4h] signed int retaddr_4; // [esp+30h] [ebp+8h] if ( mmap((void *)0x1337000, 0x3000u, 7, 50, 0, 0) != (void *)0x1337000 ) { puts("sorry"); exit(0); } if ( mmap((void *)0xDEAD000, 0x3000u, 7, 50, 0, 0) != (void *)0xDEAD000 ) { puts("sorry"); exit(0); } memcpy((void *)0xDEAD000, &unk_804A060, 0x834u); sub_80484EB(0xDEAD000, 0x834, 0x33); // sub_80484EB(内容,长度,异或的值) retaddr = 0xDEAD000; retaddr_4 = 0x33; return MEMORY[0xDF7D000](); }
用nmap开辟了两段RWX内存,并且将0x804A060的内容拷贝到其中一块RWX内存0xDEAD000处,并用sub_80484EB函数异或恢复代码。
最后的部分IDA没有识别出来,看汇编是用retf跳转到0xDEAD000处执行。
.text:08048629 C7 00 00 D0 EA 0D mov dword ptr [eax], 0DEAD000h .text:0804862F 8D 45 E4 lea eax, [ebp+var_1C] .text:08048632 83 C0 24 add eax, 24h .text:08048635 89 45 E0 mov [ebp+var_20], eax .text:08048638 8B 45 E0 mov eax, [ebp+var_20] .text:0804863B C7 00 33 00 00 00 mov dword ptr [eax], 33h .text:08048641 C9 leave .text:08048642 CB retf
看到retf,又开到此时栈中有0x33,符合32位程序调用64位代码的模式。
执行分析
使用一般的逆向工具gdb,在0x08048642处设置断点
gdb ./father pwndbg> b *0x08048642 Breakpoint 1 at 0x8048642 pwndbg> show architecture The target architecture is set automatically (currently i386) pwndbg> r
断点触发后,用ni单步执行指令执行下一步,可以看到指令已经跳转到0xDEAD000空间,CS寄存器的值从0x23变为0x33,进入64位代码的空间。
然而此时代码内容无法显示64位汇编
此时继续用ni单步执行指令,就会看到汇编指令没有一条条执行,而是几步一跳的执行,这是因为gdb认为这段代码是32位而不是64位的,即使使用set architecture i386:x86-64 命令,也会提示错误。
我也尝试过以下调试方法,均已失败告终。
- IDA+linux_server(IDA32位版本)进行调试,效果同gdb,无法识别64位汇编代码,可以单步执行,汇编指令也是几步一跳。
- IDA64+linux_server64(IDA64位版本),程序无法引导起来。
那么应该如何动态调试呢?
动态调试
为了可以正确执行64位指令,可以采用gdbserver+IDA64的调试方式。
gdbserver启动程序,并绑定到1234端口(冒号前不带ip使用本机ip)
gdbserver :1234 ./father
用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出内存。
static main(void) { auto fp, begin, end, dexbyte; fp = fopen("C:\father64.mem", "wb"); begin = 0xDEAD000; end = 0xDEB0000; for ( dexbyte = begin; dexbyte < end; dexbyte ++ ) fputc(Byte(dexbyte), fp); }
在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{…}。
__int64 __fastcall sub_DEAD44B(__int64 a1) { int v1; // eax int v2; // eax char v4; // [rsp+0h] [rbp-70h] char v5; // [rsp+10h] [rbp-60h] char v6[16]; // [rsp+20h] [rbp-50h] char v7[19]; // [rsp+30h] [rbp-40h] char v8[13]; // [rsp+50h] [rbp-20h] int v9; // [rsp+6Ch] [rbp-4h] v8[0] = 123; ... v8[12] = 18; sub_DEAD011(0x12, v8, 13); // v[i] ^= 0x12 恢复成为input_code: sub_DEAD0D7(v8); // strlen sub_DEAD073(); // write sub_DEAD09C(22, v7, 0); sub_DEAD05B(); // read if ( sub_DEAD0D7(v7) > 18 ) // 长度大于0x12 { v9 = 0; v1 = sub_DEAD105(v7); // check input[:6] == 'hwbctf' v9 += v1; v9 += v7[6] != '{'; // check {} v9 += v7[18] != '}'; v2 = sub_DEAD16F(&v7[7]); // 解方程,解得1'm n0t 4n5 v9 += v2; if ( v9 ) { sub_DEAD011(0x89, &v4, 12); // 此处有赋值,ida没有f5出来 sub_DEAD0D7(&v4); sub_DEAD073(); } else { sub_DEAD011(0xF1, &v5, 11); sub_DEAD0D7(&v5); sub_DEAD073(); } } else { v6[0] = 105; ... v6[15] = 5; sub_DEAD011(5, v6, 16); // length error!!! sub_DEAD0D7(v6); sub_DEAD073(); } return sub_DEAD08B(); }
而sub_DEAD16F函数则是有13个方程组判断输入的内容
__int64 __usercall sub_DEAD16F@<rax>(_BYTE *a1@<rdi>) { int v1; // ST0C_4 v1 = ((a1[4] ^ a1[2] ^ a1[6]) != 119) + ((a1[1] ^ *a1 ^ a1[3]) != 54) + (a1[10] + a1[3] != 85) + (a1[2] + a1[9] != 219) + (a1[4] + a1[5] != 158) + (a1[2] + a1[1] + a1[5] != 196) + (a1[8] + a1[7] + a1[9] != 194) + (a1[5] + a1[3] + a1[9] != 190) + (a1[6] + a1[2] + a1[8] != 277) + (a1[10] + a1[1] + a1[7] != 124); return (a1[5] != 48) + (a1[10] != 53) + ((a1[7] ^ a1[6] ^ a1[8]) != 96) + v1; }
用Z3可以求解得flag为hwbctf{1’m n0t 4n5}
案例2: GWoC
GWoC是一个32位的Windows程序
原题程序中有较多花指令和反调试部分,利用0x90来nop掉,附件提供的是一个Patch后的代码
程序分析
将patch后的程序拖入IDA32位中,看到主流程如下
int __cdecl main(int argc, const char **argv, const char **envp) { const char *v3; // ST14_4 HANDLE v4; // eax HANDLE v5; // eax HANDLE v6; // eax HANDLE v7; // eax const char *v8; // eax const char *v9; // edx const char *v10; // edx const char *v11; // edx _DWORD *v13; // [esp+24h] [ebp-40h] _DWORD *v14; // [esp+28h] [ebp-3Ch] _DWORD *v15; // [esp+2Ch] [ebp-38h] _DWORD *lpParameter; // [esp+30h] [ebp-34h] BOOL Wow64Process; // [esp+3Ch] [ebp-28h] DWORD ThreadId; // [esp+40h] [ebp-24h] int v19; // [esp+44h] [ebp-20h] int v20; // [esp+48h] [ebp-1Ch] int v21; // [esp+4Ch] [ebp-18h] HANDLE Handles; // [esp+50h] [ebp-14h] HANDLE v23; // [esp+54h] [ebp-10h] HANDLE v24; // [esp+58h] [ebp-Ch] HANDLE v25; // [esp+5Ch] [ebp-8h] if ( argc < 2 ) //程序判断是否有命令行参数 { sub_C725E0("Error missing argument !n"); v3 = *argv; sub_C725E0("%s inputn"); exit(0); } Wow64Process = 0; IsWow64Process((HANDLE)0xFFFFFFFF, &Wow64Process); if ( !Wow64Process ) //检测是否支持64位程序 { sub_C725E0("System not supported ! Run me on 64bits Windows OSn"); exit(0); } if ( strlen(argv[1]) != 32 ) sub_C721C0(); sub_C721E0(); v4 = GetProcessHeap(); lpParameter = HeapAlloc(v4, 8u, 0x18u); v5 = GetProcessHeap(); v15 = HeapAlloc(v5, 8u, 0x18u); v6 = GetProcessHeap(); v14 = HeapAlloc(v6, 8u, 0x18u); v7 = GetProcessHeap(); v13 = HeapAlloc(v7, 8u, 0x18u); *lpParameter = 0xFAB; //初始化多线程参数1 lpParameter[1] = 0; v8 = argv[1]; lpParameter[2] = *((_DWORD *)v8 + 2); lpParameter[3] = *((_DWORD *)v8 + 3); Handles = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, lpParameter, 0, &ThreadId); //启动多线程1 *v14 = 0xF0F0F0F0; //初始化多线程参数2 v14[1] = 0xF0F0F0F0; v9 = argv[1]; v14[2] = *((_DWORD *)v9 + 4); v14[3] = *((_DWORD *)v9 + 5); v23 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, v14, 0, (LPDWORD)&v19);//启动多线程2 *v13 = 0xF06B3430; //初始化多线程参数3 v13[1] = 0x136D7374; v10 = argv[1]; v13[2] = *(_DWORD *)v10; v13[3] = *((_DWORD *)v10 + 1); v24 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, v13, 0, (LPDWORD)&v20);//启动多线程3 *v15 = 0x43434343; //初始化多线程参数4 v15[1] = 0x434343; v11 = argv[1]; v15[2] = *((_DWORD *)v11 + 6); v15[3] = *((_DWORD *)v11 + 7); v25 = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, v15, 0, (LPDWORD)&v21);//启动多线程4 WaitForMultipleObjects(4u, &Handles, 1, 0xFFFFFFFF); //线程通过 if ( lpParameter[4] != 0x7E352B1F || lpParameter[5] != 0x9B04D2D3 ) //判断线程1结果 sub_C721C0(); if ( v15[4] != 0x4D95D40C || v15[5] != 0xE14496F7 ) //判断线程4结果 sub_C721C0(); if ( v14[4] != 0x2E4CB743 || v14[5] != 0xA51E28EE ) //判断线程3结果 sub_C721C0(); if ( v13[4] != 1434694267 || v13[5] != 1991371616 ) //判断线程2结果 sub_C721C0(); sub_C71320(); return 0; }
程序将32个字符的输入放入4个线程参数中,启动4个线程,每个线程都是调用同一个函数,只是参数不同。
.text:00C71330 ; DWORD __stdcall StartAddress(LPVOID lpThreadParameter) .text:00C71330 StartAddress proc far ; DATA XREF: _main+191↓o .text:00C71330 ; _main+1EC↓o ... .text:00C71330 .text:00C71330 var_18 = dword ptr -18h .text:00C71330 var_4 = dword ptr -4 .text:00C71330 lpThreadParameter= dword ptr 0Ch .text:00C71330 .text:00C71330 push ebp .text:00C71331 mov ebp, esp .text:00C71333 push ecx .text:00C71334 push ebx .text:00C71335 push esi .text:00C71336 push edi .text:00C71337 mov [ebp+var_4], 0 .text:00C7133E mov ecx, [ebp+8] .text:00C71341 push 33h .text:00C71343 call $+5 .text:00C71348 add dword ptr [esp], 5 .text:00C7134C retf .text:00C7134C StartAddress endp ; sp-analysis failed .text:00C7134C .text:00C7134D call loc_C72067 ... .text:00C72067 dec eax .text:00C72068 mov [esp+8], ecx .text:00C7206C dec eax .text:00C7206D sub esp, 28h .text:00C72070 dec eax .text:00C72071 mov eax, [esp+30h] .text:00C72075 dec eax .text:00C72076 mov ecx, 97418529h
在这里又看到熟悉的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等函数了。
signed __int64 __fastcall sub_2067(_QWORD *a1) { signed __int64 v1; // rax unsigned __int64 v2; // rax signed __int64 result; // rax _QWORD *v4; // [rsp+30h] [rbp+8h] v4 = a1; v1 = *a1 ^ 0x1234567897418529i64; if ( v1 == 0xE2C4A68867B175D9i64 ) a1[2] = sub_1C57(a1[1]); if ( *v4 == 0xFABi64 ) v4[2] = sub_1437(v4[1]); v2 = *v4 % 0x11111111111111ui64; if ( v2 == 0x10101010101010i64 ) v4[2] = sub_1C37(v4[1]); result = *v4 & 0x111000111000111i64; if ( result == 0x101000010000010i64 ) { result = sub_1F77(v4[1]); v4[2] = result; } return result; }
4个线程都是调用这个函数,但是由于输入参数的不同,会选取不同的函数调用。
例如之前 *lpParameter = 0xFAB
对应的是这里的 *v4 == 0xFABi64
判断 ,所以这部分输入调用的是sub_1437 函数,v4[1]就是实际输入串中第8个到第15个字符,即input[8:16] 。
进入分析sub_1437 ,发现是一个流式加密,根据F5的结果逆向比较复杂,还想结合动态运行结果进行逆向。
sbox[0] = ... ... sbox[254] = 0xF9u; sbox[255] = 0xF8u; LOBYTE(v7) = 0; memset(&v7 + 1, 0, sizeof(__int64)); for ( i = 0; i < 8; ++i ) *(&v7 + i) = *(&v9 + i); v3 = v7; for ( j = 0; j < 256; ++j ) { v8 = sbox[(sbox[j % -8 + 248] + v3)]; v1 = *(&v7 + (j + 1) % 0xFFFFFFF8) + v8; v3 = (v1 >> 7) | 2 * v1; *(&v7 + (j + 1) % -8) = v3; } return v7;
动态调试
在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为例
- 输入参数:0xfab
- 输入内容:input[8:16] = “90123456”
- 调用函数:sub_1437
- 算法内容:流式加密,根据结果逆推即可
- 逆向结果:F @AzOpFx
- 输入参数:0xf0f0f0
- 输入内容:input[16:24] = “78901234”
- 调用函数:sub_1C57
- 算法内容:低4位和高4位分开运算,多次位移和异或运算,可用暴力破解
- 逆向结果:Cq!9x9zc
- 输入参数:0x136D7374F06B3430
- 输入内容:input[:8] = “12345678”
- 调用函数:sub_1F77
- 算法内容:低4位和高4位分开进行快速幂取模操作,就是RSA,分解因数解密RSA即可
- 逆向结果:flag{RpC
- 输入参数:0x43434343434343
- 输入内容:input[24:] = “56789012”
- 调用函数:sub_1C37
- 算法内容:输入异或0x9C70A3C478EF826A,根据结果异或即可
- 逆向结果:fVz5354}
所有字符串拼接在一起得到flag{RpCF @AzOpFxCq !9x9zcfVz5354}
小结
本文通过windows和 linux 的案例,整理了32位程序调用64位代码的识别方法、静态分析和动态调试技巧。
识别方法
- retf是切换32位和64位的关键指令。
- retf前有push 0x33(33h)类似的指令。
push 33h add dword ptr [esp], 5 retf 或者 mov dword ptr [eax], 33h leave retf
- retf后CS寄存器从0x23变为0x33。
- 程序中可能有进行支持64位的检查,如GWoC。
- 当一块可执行的内存,调试时无法识别汇编或者几步一跳时,有可能在是执行64位的代码。
- 32位代码调用函数的方式和64位代码有差异,32位程序大多通过入栈方式传参,64位程序一般用寄存器传参。
- 32位和64位的syscall的含义和参数有所不同。
静态分析
- 修改PE/ELF头位64位,让IDA64识别其中64位的部分代码。
- 静态/动态dump出内存中的64位代码片段,拖入IDA64分析代码。
- 有时候可以通过IDA中Change Segment Attributes 设置为64位,进行汇编分析。
- 使用Rebase Segment对齐基地址方便进行静结合分析
动态调试
- Linux ELF程序可使用gdbserver和IDA64的组合进行调试。
- Windows程序使用Windbg进行动态调试,使用IDA64进行静态分析,动静结合逆向。
参考
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 链式调用 | 我的代码没有else
- golang之调用C语言代码
- 从JavaScript调用正确的TypeScript代码
- 对 Golang 代码调用 Elasticsearch 进行单元测试
- 安卓NDK开发之so调用java代码
- Python学习,VNR调用Jbeijing翻译的代码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Code Complete
Steve McConnell / Microsoft Press / 2004-6-19 / GBP 40.99
在线阅读本书 Widely considered one of the best practical guides to programming, Steve McConnells original CODE COMPLETE has been helping developers write better software for more than a decade. Now......一起来看看 《Code Complete》 这本书的介绍吧!