Pwn学习记录——基本ROP-1
题目来源 CTF Wiki
ret2text
首先,拿到题目我们先查一下保护
可以看到,程序是32位的,只开启了NX保护
下面我们拖到IDA里面看一下
很明显的gets函数溢出漏洞,先来搜索一下程序里面有没有可以直接利用的溢出漏洞,Alt+T
搜索system
关键字
可以看到,在secure()函数里面发现了可以直接调用system("/bin/sh")
的代码,如果我们控制程序返回到这段代码的位置,那么就可以直接得到shell
首先我们在gdb中调试算出程序溢出需要的偏移量
找到get函数的地址,在peda中设置断点(这里若不显示地址,可以右键Text View
查看)
使用 r
命令让程序跑起来,可以看到此时寄存器的内容
我们计算偏移量使用EBP和ESP的地址,字符串s的地址是从ESP+0x1c
这个位置开始的,那么 偏移量=EBP-(ESP+0x1c)+4,这里+4
是因为覆盖栈中保存的EBP的数据,才能到程序返回地址。offset=0xffffd158-(0xffffd0d0+0x1c)+4=0x70
下面我们再在IDA中找到调用system函数的地址0x0804863A
有了这些我们就可以编写exp脚本了
1 | from pwn import * |
然后执行脚本,就可以看到能成功获取shell
ret2shellcode
拿到题目,同样的思路,检查是否有保护
可以看到,程序没有开启任何保护,并且有可读,可写,可执行段
丢到IDA里,可以看到仍然是基本的gets函数溢出漏洞。与同样搜索system
没有可利用的程序段,回过来继续看程序的伪c代码,与之前的ret2text区别是少了system
发现程序将gets获取到的字符串复制了一份给buf2,那么我们就可以考虑是不是可以让程序跳转到buf2的位置执行
在IDA中跳转查看,可知buf2在.bss段,地址为0x0804A080
在gdb中进行调试,可以知道.bss段所在位置具有可执行权限
程序中没有可以用来执行/bin/sh
的代码,我们就需要自己构造生成shellcode(一段完成特定功能的代码),pwntools给我们提供了一个很好的生成shellcode的方法——shellcraft.sh()
,使用这个工具,我们可以生成44个字节的shellcode
shellcraft.sh()生成的是执行shell的汇编代码,需要asm()函数转换为机器代码(即16进制)
分析完了之后我们就可以编写exp了
1 | from pwn import * |
练习:sniperoj-pwn100-shellcode-x86-64
同样先查看程序保护情况
程序只开启了PIE地址随机化保护,其他的保护都没有开启
将程序丢进IDA打探一下情况
可以明显的发现程序可能具有read函数溢出漏洞,并且程序将buf的地址打印了出来,所以PIE的开启就没有什么用处了。
依旧查找一下system
函数或者/bin/sh
,无果。
因为程序没有开启堆栈保护,所以我们可以考虑溢出执行shellcode,从而获得系统shell。
先使用gdb调试,找到程序应该填充的数据量
程序的伪c代码中没有buf相对于esp的偏移量,所以我们的offset=0x10+8=24,这时候我们可用的shellcode的空间只有24字节,但是我们平常使用shellcraft.sh()
所生成的shellcode有44个字节,显然就不能使用了,需要我们另觅它路。
这里推荐两个查找shellcode的网站:
https://www.exploit-db.com/
http://shell-storm.org/shellcode/
找到一个只有23字节的shellcode,刚好符合我们的要求
1 | shellcode_x64 = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" |
这里顺便将32位的shellcode也贴出来:
1 | shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" |
之后就可以编写exp了
1 | from pwn import * |
这里的shellcode_addr
是buf的起始地址加上填充的数据长度和ret返回地址的长度,因为buf
后面 0x10+8
大小的空间都不可以使用,因为 leave
指令会将这一段的栈空间弹出。
这个payload的长度是55,小于read函数中所限定的 0x40 ,若是需要修复题目漏洞,可以把read函数限定的0x40的长度改为0x24或者更小的数。
ret2syscall
老样子,检查程序保护,发现除了NX其他都没有开启,然后丢到IDA里面
程序跟之前的没什么两样,仍然是gets函数造成栈溢出,只是没有了system和shellcode可利用。我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell
1 | execve("/bin/sh",NULL,NULL) |
该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb(对应的系统调用为execve)
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
几个常用的系统调用(int 0x80)
- 32位
NAME | EAX | EBX | ECX | EDX |
---|---|---|---|---|
sys_exit | 1 | int | 0 | 0 |
sys_read | 3 | unsigned int | char * | size_t |
sys_write | 4 | unsigned int | const_char * | size_t |
sys_open | 5 | const char * | int | int |
sys_execve | 11 | const char * | 0 | 0 |
PS:最常用的为11号调用。也就是execve("/bin/sh",0,0)
。
- 64位
NAME | %rax | %rdi | %rsi | %rdx |
---|---|---|---|---|
sys_read | 0 | unsigned int | char * | size_t |
sys_write | 1 | unsigned int | const_char * | size_t |
sys_open | 2 | const char * | int | int |
sys_execve | 59 | const char * | 0 | 0 |
sys_exit | 60 | int error_code | 0 | 0 |
这里寄存器的值的控制需要使用gadgets,比如,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
首先来寻找控制eax的gadgets
1 | ROPgadget --binary rop --only 'pop|ret' | grep 'eax' |
这里选用第二个最短的作为我们的gadget,类似也可以得到其它寄存器的gadgets
1 | ROPgadget --binary rop --only 'pop|ret' | grep 'ebx' |
这里可以选择
1 | 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret |
这个可以直接控制edx
、ecx
、ebx
三个寄存器
另外,我们还需要获得 /bin/sh
字符串对应的地址,依旧使用ROPgadget
1 | ROPgadget --binary ret2syscall --string '/bin/sh' |
继续找int 0x80
的地址
1 | ROPgadget --binary ret2syscall --only 'int' |
1 | from pwn import * |
这里附上整个exploit的执行过程