背景
在CTF,逆势变得越来越多变。也有过32位程序调用64位代码,一般静态分析和动态调试方法都会失败的情况,让人很大。今天,我们将通过两个案例来学习如何处理这种情况。
情形
这两个案例包括一个windows程序和一个linux ELF程序,这两个程序只覆盖了两个常见的平台(摘录代码:nxwx)
father and son (ELF),来源于2018年护网杯CTFGWoC (Windows),来源于2018年CNCERT CTF基础知识
x64系统中的进程有两种工作模式:32位和64位。这两种工作模式的区别在于CS寄存器。32位模式下,CS = 0x23;在64位模式下,CS = 0x33。;
这两种工作模式可以切换,通常通过retf指令,一条retf指令相当于下面两条汇编指令
pop ip
pop cs
如果此时堆栈中有0x33,则将0x33弹出到CS寄存器中,实现32位程序切换到64位代码的过程。所以retf是识别32位程序调用64位代码的重要标志。
案例1:父亲和儿子
二进制文件父亲来自一个流量包的内容(这不是本文的重点),是一个32位的ELF程序
$文件父亲
父亲:ELF 32位LSB可执行,Intel 80386,版本1 (SYSV),动态链接,解释器/lib/ld-linux.so.2,对于GNU/Linux 2.6.32,BuildID[sha1]= 4351 DC 8 FDE 1b d 3404207 e 1540 b 84 E3 c 577 c 81521,剥离
程序分析
核心代码如下
int sub_8048527()
{
签名的int retaddr// [esp+2Ch] [ebp+4h]
int retaddr_4签名;// [esp+30h] [ebp+8h]
if ( mmap((void *)0x1337000,0x3000u,7,50,0,0)!= (void *)0x1337000)
{
puts("对不起");
退出(0);
}
if ( mmap((void *)0xDEAD000,0x3000u,7,50,0,0)!= (void *)0xDEAD000)
{
puts("对不起");
退出(0);
}
memcpy((void *)0xdade 000 & amp;unk_804A060,0x 834 u);
sub_80484EB(0xDEAD000,0x834,0x 33);// sub_80484EB(内容、长度、异或值)
retaddr = 0xDEAD000
retaddr _ 4 = 0x33
return MEMORY[0xdf 7d 000]();
}
用nmap创建两段RWX内存,将0x804A060的内容复制到其中一个RWX内存的0xDEAD000,用sub_80484EB函数异或恢复代码。
IDA的最后一部分没有被识别,所以用retf跳转到0xDEAD000来执行程序集。
。文本:08048629 C7 00 00 D0 EA 0D mov dword ptr[eax],0DEAD000h
。正文:0804862F 8D 45 E4丽艾[ebp+var_1C]
。文本:08048632 83 C0 24 add eax,24h
。正文:08048635 89 45 E0 mov [ebp+var_20],eax
。正文:08048638 8B 45 E0 mov eax,[ebp+var_20]
。正文:0804863 B C7 00 33 00 00 00 mov dword ptr[eax],33h
。文本:08048641 C9离开
。正文:08048642 CB retf
看到retf后,堆栈中有0x33,符合32位程序调用64位代码的模式。
执行分析
使用通用反向工具gdb在0x08048642处设置断点
gdb。/父亲
pwndbg>。b *0x08048642
0x8048642处的断点1
pwndbg>。展示建筑
目标体系结构是自动设置的(目前为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。/父亲
用IDA64打开程序。此时用F5无法查看伪代码,但可以看到IDA64识别32位程序,汇编可以正常显示。
在0x8048642的retf处设置断点,并设置连接gdbserver的参数(如图)
单击绿色三角形按钮开始调试。F9运行一次后,到达断点。
再次按F7键输入64位代码。此时,EIP显示0xDEAD000已输入,但装配窗口中没有提示。即使使用g跳转到地址0xDEAD000也会提示错误。
这是因为当IDA和gdbserver连接时,内存没有及时刷新。您可以在“调试器”菜单中打开“手动内存区域”菜单项,然后右键单击“插入”以创建新的内存区域(每次启动调试时都应重复此操作)。
存储区的起始地址为0xDEAD000,结束地址可以默认设置。注意选择64位段。
然后用g指令跳转到内存0xDEAD000,此时显示二进制数据。
按C识别为汇编指令,IDA调试器可以正确识别64位汇编,按F8单步执行不需要几个步骤,可以正常调试。
注1: GDB服务器在一次调试后可能不会再次连接,因此需要终止并重新启动。
注2:有些ELF程序可能不需要增加手动内存区域的内存区域,可以通过IDA的编辑->段->来访问;更改段属性将内存修改为64位代码
静态分析
与动态调试方法相比,还需要静态分析方法的配合才能提高CTF的逆向效率。
这种情况下,采用异或混淆。因为混淆并不复杂,所以可以静态转储或动态恢复。本文在动态运行retf指令时,使用脚本Dump来存储内存。
静态主(空)
{
auto fp,begin,end,dexbyte
FP = fopen(" C:父64.mem "," WB ");
begin = 0xDEAD000
end = 0xDEB0000
for(dex byte = begin;dexbyte <。结束;dexbyte ++)
fputc(Byte(dexbyte),FP);
}
在“文件”菜单的“命令”菜单项中,选择IDC脚本,输入上述内容,然后单击“运行”按钮,将内存从0xDEAD000导出到c盘上的父64.mem文件中
将父64.mem拖入IDA64进行静态分析,由于ELF头缺失,IDA64会询问选择哪种格式,在此选择64位模式分析代码。
此时代码基址为0x0,所以编辑->:Segments->;Rebase Segment重新定义基址,并将其设置为0xDEAD000,以便动态调试和静态调试期间的程序集地址相同。
然后就可以高高兴兴的用F5生成C语言代码了。
反向裂化
因为本文重点是如何识别和分析32位程序调用的64位代码,所以案例的算法反长度会比较简短,有兴趣的朋友可以自行研究。
主进程sub_DEAD44B接收用户输入输出结果,判断输入格式是否为hwbctf{…}。
_ _ int 64 _ _ fast call sub _ DEAD 44B(_ int 64 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恢复输入_代码:
sub _ DEAD 0D 7(V8);// strlen
sub _ dead 073();//写
sub_DEAD09C(22,v7,0);
sub _ DEAD 05B();//读取
if ( sub_DEAD0D7(v7))>18) //长度大于0x12
{
v9 = 0;
v1 = sub _ DEAD 105(V7);//检查输入[:6] == 'hwbctf '
v9+= v1;
v9 += v7[6]!= '{';//检查{}
v9 += v7[18]!= '}';
v2 = sub _ DEAD 16F(& amp;V7[7]);//解方程得到1 ' n0t 4n 5
v9+= v2;
if ( v9)
{
sub _ dead 011(0x 89 & amp;v4,12);//这里有赋值,f5不是从ida出来的
sub _ DEAD 0D 7(& amp;v4);
sub _ dead 073();
}
其他
{
sub _ dead 011(0xf 1 & amp;v5,11);
sub _ DEAD 0D 7(& amp;V5);
sub _ dead 073();
}
}
其他
{
V6[0]= 105;
...
V6[15]= 5;
sub_DEAD011(5,v6,16);//长度错误!!!
sub _ DEAD 0D 7(V6);
sub _ dead 073();
}
return sub _ dead 08B();
}
sub_DEAD16F函数有13个方程来判断输入
_ _ int 64 _ _ user call sub _ DEAD 16F @ & lt;rax>。(_ BYTE * a1 @ & ltrdi>。)
{
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能解决的标志是hwbctf{1'm n0t 4n5}
案例2: GWOC
GWoC是一个32位Windows程序
原程序中有很多花指令和反调试部分,0x90没有。附件提供了补丁代码
程序分析
将补丁程序拖入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 & lt;2) //程序判断是否有命令行参数
{
sub_C725E0("错误缺少参数!n ");
v3 = * argv
sub _ c 725 E0(" % s inputn ");
退出(0);
}
wow 64 process = 0;
iswow 64 process((HANDLE)0xfffffff & amp;wow 64 process);
if(!Wow64Process) //检查是否支持64位程序
{
sub_C725E0(“不支持系统!在64位Windows OSn上运行我");
退出(0);
}
if ( strlen(argv[1])!= 32 )
sub _ c 721 c 0();
sub _ c 721 E0();
v4 = GetProcessHeap();
lpParameter = HeapAlloc(v4,8u,0x 18u);
V5 = GetProcessHeap();
v15 = HeapAlloc(v5,8u,0x 18u);
V6 = GetProcessHeap();
v14 = HeapAlloc(v6,8u,0x 18u);
v 7 = GetProcessHeap();
v13 = HeapAlloc(v7,8u,0x 18u);
* lpParameter = 0xFAB//初始化多线程参数1
lpParameter[1]= 0;
v 8 = argv[1];
lpParameter[2]= *((_ DWORD *)V8+2);
lpParameter[3]= *((_ DWORD *)V8+3);
Handles = CreateThread(0,0,(LPthread _ START _ ROUTINE)START address,lpParameter,0,& ampThreadId);//开始多线程1
* v14 = 0xF0F0F0F0//初始化多线程参数2
v14[1]= 0xf0f 0f 0;
v 9 = argv[1];
v14[2]= *((_ DWORD *)v9+4);
v14[3]= *((_ DWORD *)v9+5);
v23 = CreateThread(0,0,(LPthread _ START _ ROUTINE)START address,v14,0,(LPDWORD)& amp;v19);//开始多线程2
* v13 = 0xF06B3430//初始化多线程参数3
V13[1]= 0x136 d 7374;
v10 = argv[1];
V13[2]= *(_ DWORD *)v10;
V13[3]= *((_ DWORD *)v10+1);
v24 = CreateThread(0,0,(LPthread _ START _ ROUTINE)START address,v13,0,(LPDWORD)& amp;v 20);//开始多线程3
* v15 = 0x43434343//初始化多线程参数4
v 15[1]= 0x 434343;
v11 = argv[1];
v15[2]= *((_ DWORD *)v11+6);
v15[3]= *((_ DWORD *)v11+7);
v25 = CreateThread(0,0,(LPthread _ START _ ROUTINE)START address,v15,0,(LPDWORD)& amp;v 21);//开始多线程4
waitformmultiplieobjects(4u,& amp句柄,1,0x ffffffff);//线程通过
if ( lpParameter[4]!= 0x7E352B1F || lpParameter[5]!= 0x9B04D2D3) //判断线程1的结果
sub _ c 721 c 0();
if ( v15[4]!= 0x4D95D40C || v15[5]!= 0xE14496F7) //判断线程4的结果
sub _ c 721 c 0();
if ( v14[4]!= 0x2E4CB743 || v14[5]!= 0xA51E28EE) //判断线程3的结果
sub _ c 721 c 0();
if ( v13[4]!= 1434694267 || v13[5]!= 1991371616) //判断线程2的结果
sub _ c 721 c 0();
sub _ c 71320();
返回0;
}
程序把32个字符的输入放入4个线程参数中,启动4个线程,每个线程调用同一个函数,但是参数不同。
。文本:00C71330DWORD _ stdcall _ start address(LPVOID LPthreadParameter)
。文本:00C71330开始地址进程远;数据XREF: _main+191↓o
。文本:00C71330_main+1EC↓o...
。文本:00C71330
。文本:00C71330 var_18 = dword ptr -18h
。文本:00C71330 var_4 = dword ptr -4
。文本:00c 71330 LPthreadParAmeter = dword ptr 0Ch
。文本:00C71330
。文本:00C71330推ebp
。文本:00C71331 mov ebp,esp
。文本:00C71333推送ecx
。文本:00C71334推ebx
。文本:00C71335按压esi
。文本:00C71336推送edi
。文本:00C71337 mov [ebp+var_4],0
。文本:00C7133E mov ecx,[ebp+8]
。文本:00C71341按33h
。文本:00C71343呼叫$+5
。文本:00C71348添加dword ptr [esp],5
。文本:00C7134C retf
。文本:00C7134C StartAddress endpsp-分析失败
。文本:00C7134C
。文本:00C7134D呼叫loc_C72067
...
。文本:00C72067 dec eax
。文本:00C72068 mov [esp+8],ecx
。文本:00C7206C dec eax
。文本:00C7206D sub esp,28小时
。文本:00C72070 dec eax
。正文:00C72071 mov eax,[esp+30h]
。文本:00C72075 dec eax
。文本:00C72076 mov ecx,97418529h
这里我们看到熟悉的push 33h和retf,这是输入64位代码的特点。输入了loc_C72067地址,无法正确识别64位汇编指令。
静态分析
因为这种情况下的代码可以静态的转储出来,所以我们先做静态分析。
使用案例1中的Dump方法,拖到IDA64分析中,可以恢复代码,但是会出现一些内存引用错误,这是因为缺少上下文内存。
虽然也可以分析,但在这种情况下,我们可以尝试用更优雅的方式。
在010Editor中,使用PE模板打开exe文件,偏移量约为0x118,将32位的0x10b修改为64位的0x20b。
然后放入IDA64分析,Rebase Segment为0。再看看原来的loc_C72067(重换基后的0x2067)。这时F5也可以识别一些功能,可以按照sub_1C57和sub_1437的分析。
签名_ _ int 64 _ _ fast call sub _ 2067(_ QWORD * a1)
{
签名_ _ int64 v1// rax
无符号_ _ int64 v2// rax
signed __int64结果;// rax
_ QWORD * v4// [rsp+30h] [rbp+8h]
v4 = a1
v1 = * a1 ^ 0x 1234567897418529 i64;
if ( v1 == 0xE2C4A68867B175D9i64)
a1[2]= sub _ 1C 57(a1[1]);
if ( *v4 == 0xFABi64)
v4[2]= sub _ 1437(v4[1]);
v2 = * v4 % 0x11111111111111ui64
if ( v2 == 0x10101010101010i64)
v4[2]= sub _ 1C 37(v4[1]);
结果= * v4 & amp0x111000111000111i64
if(结果== 0x101000010000010i64)
{
result = sub _ 1F 77(v4[1]);
v4[2] =结果;
}
返回结果;
}
四个线程都调用这个函数,但是由于输入参数不同,会选择不同的函数调用。
例如,*lpParameter = 0xFAB在这里对应* v4 = = 0xFABi64的判断,所以这部分输入调用sub_1437函数,v4[1]是实际输入字符串中的第8到第15个字符,即input[8:16]。
进入分析sub_1437,发现是流加密,根据F5的结果反向比较复杂,还需要根据动态运行结果反向。
sbox[0] =...
...
sbox[254]= 0xf 9u;
sbox[255]= 0xf 8u;
LOBYTE(V7)= 0;
memset(& amp;v7 + 1,0,sizeof(_ int 64));
for(I = 0;i <。8;++i)
*(& amp;v 7+I)= *(amp;v 9+I);
v3 = v7
for(j = 0;j <。256;++j)
{
V8 = sbox[(sbox[j %-8+248]+v3)];
v1 = *(& amp;v 7+(j+1)% 0xffffff8)+v 8;
v3 =(v1 & gt;>。7)| 2 * v1;
*(& amp;V7+(j+1)%-8)= v3;
}
返回V7;
动态调试
在WIndows下,执行retf指令后,IDA32、IDA64和Ollydbg调试器无法正常运行。在master的指导下,windbg被用作动态调试工具。
用Windbg 64位打开目标程序文件->打开可执行文件,注意输入命令行参数
在视图菜单中打开反汇编、寄存器、内存和命令窗口。布局如下
首先,我们需要在retf处设置断点。怎么才能设置断点?在IDA中,rebase段为0后,可以看到retf的地址是0x134c,所以在windbg的反汇编窗口输入GWoC+0x134c,确定也是retf,按F9设置断点。
按F5执行到断点,然后按F8进入执行。此时,CS寄存器可以看到它已变为0x33,并进入64位代码块
这时候给我们要调试的sub_1437函数添加断点,在反汇编窗口输入GWoC+0x1437,按F9添加断点,然后F5运行到断点,就可以高高兴兴的开始调试了。
IDA还有链接Windbg的功能。而本文采用的IDA 7.0版本,未能连接windbg进行调试。IDA只能用于静态分析,Windbg用于动态调试,双方逆向组合。
反向裂化
以输入1234567890123456789012479012479012479012为例
算法1
输入参数:0xfab输入内容:input[8:16] = “90123456”调用函数:sub_1437算法内容:流式加密,根据结果逆推即可逆向结果:F算法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!9x9zcfVz5354}
总结
本文通过windows和linux的案例,梳理了32位程序调用64位代码的识别方法、静态分析和动态调试技巧。
识别方法
retf是切换32位和64位的关键指令。retf前有push 0x33(33h)类似的指令。 push 33h添加dword ptr [esp],5
返回指令
或者
mov dword ptr [eax],33h
离开
返回指令
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进行静态分析,动静结合逆向。指
1.《ctf加载程序 CTF中32位程序调用64位代码的逆向方法》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《ctf加载程序 CTF中32位程序调用64位代码的逆向方法》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/guonei/1192252.html