CTFwiki之基本ROP
CTFwiki之基本ROP
现在才知道CTFwiki上有这么多的好东西,果断转换到这里来学,在这里能学到很多好东西。
基本 ROP - CTF Wiki (ctf-wiki.org)
这是官方给出的wp。
接下来开始写我自己对这些题的理解和收获。
ret2text
原理
ROP的基本原理就是栈溢出,只是通过栈溢出,然后返回的地址不同罢了。这里就是返回回程序本来就有的函数。
由给出的题来解释这个原理。
题目
通过checksec和file命令查出该文件只开启了堆栈不可执行保护,是个32位的程序。
ida反汇编看出来,这里存在栈溢出漏洞。
接下来就是寻找该数组到返回地址的偏移是多少来控制返回地址。
其实这里是可以看出来偏移为多少的,但是ida算出来的偏移地址有时候是有问题的,最好是以gdb调试出来的返回地址为准。(这里的偏移地址就有问题,这里算出来是0x68,但是实际上并不是,貌似可能是ida的机制问题,没有去细细研究)
接下里就是通过gdb调试来调试出偏移地址。
1 | 0x80486a7 <main+95> lea eax, [esp + 0x1c] |
这里可以看出输入数组的起始地址相对于这时候的栈顶偏移为0x1C,于是只要知道栈底的地址和此时栈顶的地址即可。
1 | EBP 0xffffd428 ◂— 0x0 |
这时的栈顶和栈底可以看到,于是就可以算出来偏移地址为多少了。当前栈帧的大小为0x88,且数组起始地址相对于栈顶的地址偏移为0x1c,故偏移为0x6c。
1 | void secure() |
可以在ida中找到这么一个函数调用了system,于是我们可以将返回地址构造为这个system函数的地址即可。
1 | .text:0804863A mov dword ptr [esp], offset command ; "/bin/sh" |
在这里的时候我在想到底是填上面那个地址还是下面那个地址,后来我想明白了,这里还需要先压入参数,应该先填上面的那个地址,压入参数,直接填调用_system的地址不能获得shell。
exp如下:
1 | from pwn import * |
ret2shellcode
原理
有时候程序本身是没有getshell的函数的,于是我们可以在堆栈上构造shellcode然后控制返回地址去执行这段代码
但是首先,在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。
题目
以wiki给的题目来实践,
通过file和checksec来看,什么保护都没开,是32位的程序。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
这里一样存在栈溢出漏洞,但是将输入的函数给了buf2。
1 | .bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o |
这里可以看出他将我们输入的字符串给了bss段的buf2.
通过百度百科可以看到bss段是存放未初始化和初始化为0的全局或静态变量。可以读写,但不一定可以执行,于是我们要查看一下该段可否执行。
发现居然不可以执行,和wp给出的wp情况咋不一样。
ret2syscall
原理
syscall即系统调用,ret2syscall即将返回地址改写为系统调用。来获取shell
这里需要用到ROPgadgets脚本。来获取gadgets(小工具,用来构造执行程序流,即ROP。
题目
32位程序开启了堆栈不可执行,其他啥都没开,还好。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
一样是栈溢出漏洞,不过貌似没有可以利用的函数,也不能在堆栈上执行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 | ┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall] |
这里用ROPgadgets来寻找弹出栈上的值到eax然后返回的指令的首地址。这里可以选0x080bb196 : pop eax ; ret。
1 | ┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall] |
同理,这里可以选择0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret。虽然和函数传参的顺序不一样,但是我们可以将参数的顺序也修改一下,就可以起到一样的效果。
1 | ┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall] |
给ebx的参数是”/bin/sh”的地址,用–string就可以找到这个字符串。
1 | ┌──(root💀kali)-[/home/…/ctf_workstation/ctfwiki/基本ROP/ret2syscall] |
并且调用完系统调用函数时需要int 0x80来充当中断,就是传说中的软中断。这里的int不是C语言的整型类型,而是汇编的中断代码。
于是就可以构造出来exp了。
1 | #!/usr/bin/env python |
这里的溢出偏移和第一题是一样的,这里就不再凑字数了。
1 | payload = flat( |
其他地方的代码好理解,这里我再讲讲这段关键的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) |
这里我给出了流程图,执行完当前函数后,会将控制权给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
还是一样,检查一下防护。
32位只开启了堆栈不可执行。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
查看反汇编的源代码,发现存在栈溢出漏洞。
1 | if ( input == secretcode ) |
和ret2text一样,存在system函数,但是这里的参数不对,于是就需要我们来手动构造参数”/bin/sh”。
1 | ┌──(root💀kali)-[/home/…/ctfwiki/基本ROP/ret2libc/ret2libc1] |
ropgadget查出来发现存在/bin/sh字符串。于是我们就可以构造exp了,但是这里我们不能再直接调用原有的函数的代码了,因为参数不对,我们可以调用对应的plt表来跳转到system在libc的真实地址去。
1 | .plt:08048460 |
在ida中发现了system对应的plt表。
于是构造exp
1 | from pwn import * |
注意,一般正常调用system函数前会将函数的参数压入栈中,再将调用函数system代码的下一个指令的地址压入栈中,故参数应该在system地址后面第二个。中间填充任何一个4字节数据即可(因为地址是4字节的)。
例题2
32位文件,开启了堆栈不可执行。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
还是一样的栈溢出漏洞,但是我在做这个题的时候发现了一个好用的脚本。我们这里可以使用暴力法,扔一堆字符进去!看报错点再哪里,然后可以间接查看栈大小,这是覆盖了返回地址的相对偏移大小,也就是可以直接填充的‘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 | ┌──(root💀kali)-[/home/…/ctfwiki/基本ROP/ret2libc/ret2libc2] |
然后我们在ida找到gets函数和system函数的plt表,
1 | .plt:08048460 jmp ds:off_804A010 |
而且我们需要一个全局变量来存储”/bin/sh”字符串,在ida中可以找到,
1 | .bss:0804A080 public buf2 |
于是就可以构造ROP链了,exp如下
1 | from pwn import * |
这里的pop的作用便是充当返回地址,然后将下一个栈上的数给弹出去,将返回地址定为system,”aaaa”的作用是当返回地址。
于是我们便可以构造另一个exp。
1 | from pwn import * |
效果是一样的。
PS:
在网上查题解的时候发现了以前一直不太懂的东西,为什么在栈上被调函数的后一位就是返回地址,其中机制是什么,直到我跟进了函数gets之后才知道了。最后函数是以ret结尾的,相当于将下一个栈上的地址给了EIP寄存器。
1 | 0xf7e29ae6 <gets+230> sub dword ptr [ebx], 1 |
收获满满。
例题3
这题不是用的kali环境,用的是ubuntu环境,师傅给我说一般pwn的题都是ubuntu的环境,kali的环境是基于Debian环境的。用的库文件不一样,之后的题我可能都是那ubuntu环境来做了。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
还是栈溢出漏洞,但是题目告诉了我们,这里面没有”/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 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下