0×01 概述
本文介绍个人学习pwn过程中的一些总结,包括常用方法,网上诸多教程虽然有提供完整的exp,但并未解释exp为什么是这样的,比如shellcode写到哪里去了(这关系到跳转地址),ROP链怎么选择的。对于pwn,本人也是新手,其中有总结错误的,欢迎各位大佬指正。
文中用到的测试程序都在:https://github.com/silience/pwn
0×02 PWN常用的基本知识
首先拿到一个PWN程序,可以先使用file命令,判断是32位还是64位。
可以使用objdump读取plt和got表,plt和got网上都有详细的介绍,再此不再赘述。
这边要提一下数据在寄存器中的存放顺序,这个在格式化字符串漏洞中要格外注意,特别是64位,32位的先后顺序是eax->edx->ecx->ebx,64位的先后顺序是rdi->rsi->rdx->rcx->r8->r9。
刚开始学习的时候,个人经常把pop和push经常搞反,因此在此把这两个指令的介绍说一下:push [reg]/[num] 是将reg寄存器中的值或是数字num压入堆栈中,而pop [reg]是将堆栈栈顶的值弹出到reg寄存器中,并将这个值从堆栈中删去。
有时候要查看寄存器中的值,可以用到如下命令:
print $esp:打印esp的值
x/10x $esp:打印出10个从esp开始的值
x/10x $esp-4:打印出10个从偏移4开始的值
x/10gx $esp:以64位格式打印
下面先使用hello练练手,首先使用IDA的F5大法可以看到内部有个getshell函数,可以直接跳转到该函数getshell。
使用工具pade可以很方便的计算出偏移量,pattern create 100。
pattern offset 0x41284141,计算出偏移量为22。
查看汇编代码,获取getshell的地址,也就是要跳转的地址。
最后得到完整的exp如下。
0×03 shellcode
生成方式
1、在shellcode数据库网站找一个shellcode,http://shell-storm.org/shellcode/
2、使用kali的msfvenon生成shellcode,如命令msfvenon -p linux/x86/exec CMD=/bin/sh -f python
3、使用pwntools自带的函数如asm(shellcraft.sh())
但有时候不知道shellcode写到哪里去了,在回答这个问题前,要提一下bss段、data段、text段、堆(heap)、栈(stack)的一些区别。
1、bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,bss段属于静态内存分配。
2、data段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域,数据段属于静态内存分配。
3、text段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
4、堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。
5、栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。
下面以ret2shellcode,同样使用IDA看下代码,很明显,shellcode写入到bss段。
使用命令readelf -S ret2shellcode查看获取bss段地址为0x0804a040。
还必须保证bss段有可执行权限,shellcode才能运行,可用gdb调试的vmmap命令查看,发现bss段可读可写可执行。范围是0x0804a000到0x0804b000,bss段地址0x0804a040在这个区间,且必须保证shellcode长度不超过这个区间即可,但到目前为止,shellcode具体地址依然不知道。
这时可以去调用它的函数strncpy前查看汇编代码,一般通过push或者move进行参数传递,参数传递顺序是从右到左,可以定位到shellcode地址0x804a80。
最后exp如下。
shellcode地址的位置其实是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的位置。但当你真的执行exp的时候你会发现shellcode压根就不在这个地址上!这是为什么呢?原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,但当我们直接执行的时候,buf的位置会固定在别的地址上。怎么解决这个问题呢?有两种方法,一种是 开启core dump这个功能,另外一种是使用GDB的attach功能。
可以使用level1练手,有时checksec显示PIE关闭。
其实用ldd会发现,地址依然会随机变化。
可使用命令echo 0 > /proc/sys/kernel/randomize_va_space关掉整个linux系统的ASLR保护,再进行调试;开启core dump这个功能,开启之后,当出现内存错误的时候,系统会生成一个core dump文件在tmp目录下。然后我们再用gdb查看这个core文件就可以获取到buf真正的地址了。
ulimit -c unlimited
sudo sh -c 'echo "/tmp/core.%t" > /proc/sys/kernel/core_pattern'
0×04 格式化字符串漏洞
这要讲一下字节序。
大端就是:存储最高有效字节在最小的地址(网络传输文件存储常用)。
小端就是:存储最低有效字节在最小的地址(计算机内部存储)。
帮助记忆的法子:小端就是存储先存最小有效字节,大端就是先存最大有效字节。
printf函数的格式化字符串常见的有 %d,%f,%c,%s(用于读取内存数据),%x(输出16进制数,前面没有0x),%p(输出16进制数,前面带有0x);%n是一个不经常用到的格式符,它的作用是把前面已经打印的长度写入某个内存地址,用于修改内存,除了%n,还有%hn,%hhn,%lln,分别为写入目标空间4字节,2字节,1字节,8字节。
去读内存,假如当偏移量为5时:
./a.out "`printf "\0x78\x56\x34\x12"`.%08x.%08x.%08x.%08x.%08s"
或者直接使用读取地址0x12345678的内容:
./a.out "`printf "\0x78\x56\x34\x12"`.%5\$s"
比如要将跳转地址0x0804a048改data为0x12345678,可使用%hhn;因为使用的是小段序,高字节保存在高地址。
所以poc如下,偏移量要从6开始,应为\x4b\xa0\x04\x08保存在偏移地址6。
./a.out "`printf "%18c%6\$hhn"."%34c%7\$hhn"." %34c%8\$hhn "."%34c%9\$hhn "."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"."\x4b\xa0\x04\x08"
但是为什么依次是%18c、%34c、%34c、%34c;第一个是0x12,很简单,变成十进制就是18;第二个是0x34,十进制52,第二次总写入数包括第一次的,即18+34=52;后面两次依此类推。
实际使用中,可以直接使用pwntools的函数fmtstr_payload,或者fmt_str(offset,size,addr,target)(其中offset表示要覆盖的地址最初的偏移,size表示机器字长,addr表示将要覆盖的地址,target表示我们要覆盖为的目的变量值)直接覆盖。
可以以湖湘杯2017的pwn200进行练手,使用IDA,发现很明显的格式化字符串漏洞。
首先输入AAAA.%X.%X.%X.%X.%X.%X.%X.%X,可以发现在第七个%X输出41414141,A的ascii码是41(有时是61616161,a的ascii码61,因为程序把输入转换成小写),可知偏移量是7,首先使用%s获取puts函数的真实地址,然后计算出system的真实地址,后面再利用函数fmtstr_payload,将atoi的地址替换为system地址,当执行atoi时,就会这些system函数,从而获取shell。
0×05 libc
libc中提供了大量的函数,gdb调试时可直接使用如下命令获取地址,如果未提供,可以去网站http://libcdb.com/下载对应的文件。
可依次执行以下命令,快速getshell。
print system#获取system函数地址。
print __libc_start_main
find 0xb7e393f0, +2200000, "/bin/sh"#获取参数"/bin/sh"的地址
以level2为例,exp如下,利用链:偏移数据+system地址+返回地址+参数地址,本例是通过system获得shell,不需要做其他操作,所以返回地址可以随便写。
0×06 ROP
Rop链顺序,首先是跳转地址,比如要调用的内置函数write泄露出system地址,然后是返回地址(如果泄露的地址要重复使用,则返回地址是write地址或者它前面的地址),再就是传递的参数是从右往左入栈。
以ret2syscall为例,rop链构造如下:因为要调用execve("/bin/sh",NULL,NULL),该系统函数的调用号为0xb,因此首先要将0xb给eax寄存器,可使用ROPgadget --binary ret2syscall --only "pop|ret" | grep "eax"进行查找。
因为函数execve有三个参数,接着可以使用命令。
ROPgadget --binary ret2syscall --only "pop|pop|pop|ret" | grep "ebx",不能选包含esi(esi是下条指令执行地址)或者ebp(栈基址寄存器)。
使ROPgadget --binary ret2syscall --string '/bin/sh',可查找参数/bin/sh 的地址。
最后再跳转到int 0x80的地址就可执行对应的系统调用,也就是execve函数,可通过ROPgadget --binary ret2syscall --only 'int',找int 0x80的地址。
最后完整的exp如下。
0×07 参考地址
https://www.cnblogs.com/yanghong-hnu/p/4705755.html
https://www.cnblogs.com/gremount/p/8830707.html
https://blog.csdn.net/m0_37809075/article/details/81269697
https://blog.csdn.net/qq_43394612/article/details/84900668
https://blog.csdn.net/qq_38204481/article/details/80329885
https://www.freebuf.com/vuls/161116.html
https://blog.csdn.net/weixin_40850881/article/details/80216764
*本文作者:freebuf01,转载请注明来自FreeBuf.COM