ghkdtlwns987

[pwnable.tw] silver_bullet 본문

pwnable.tw

[pwnable.tw] silver_bullet

2020/03/31 2020. 12. 24. 18:48

시험 끝! 헤헹

FULL RELRO, NX enabled 되어 있다.

그리고 libc_32.so.6 파일을 준다.

-> 그렇다는 건 libc_base 를 구해야 할 것이다.

 

main 함수

더보기
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v5; // [esp+0h] [ebp-3Ch]
  const char *v6; // [esp+4h] [ebp-38h]
  char s; // [esp+8h] [ebp-34h] ebp - 0x34
  int v8; // [esp+38h] [ebp-4h]

  init_proc();
  v8 = 0;
  memset(&s, 0, 48u);                           // ebp-0x34(52byte)
  v5 = 0x7FFFFFFF;
  v6 = "Gin";
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          menu();
          v3 = read_int();
          if ( v3 != 2 )
            break;
          power_up(&s);                         // power up(2)
        }
        if ( v3 > 2 )
          break;
        if ( v3 != 1 )
          goto LABEL_16;
        create_bullet(&s);                      // create(1)
      }
      if ( v3 == 3 )
        break;
      if ( v3 == 4 )                            // exit(4)
      {
        puts("Don't give up !");
        exit(0);
      }
LABEL_16:
      puts("Invalid choice");
    }
    if ( beat((int)&s, &v5) )                   // beat(3)
      return 0;
    puts("Give me more power !!");
  }
}

main 함수를 보면 s라는 배열의 48byte 를 모드 0으로 초기화 해준다. 

그리고 v5 = 0x7FFFFFF , v6 = "Gin" 으로 초기화 되어 있다. 

 

중간에 read_int() 함수로 숫자를 입력받는다. 

 

이후에 차례대로 1 , 2 , 3 , 4 번을 입력하면 차례대로 함수가 호출되고, beat() 함수를 실행했을 시 

HP 가 모두 떨어지면 프로그램이 종료되는데, 그렇지 않으면 함수가 계속 실행된다. 

 

 

 

1. create_bullet()

더보기
int __cdecl create_bullet(char *s)
{
  size_t v2; // ST08_4

  if ( *s )
    return puts("You have been created the Bullet !");
  printf("Give me your description of bullet :");
  read_input(s, 48u);                           // 48byte input
  v2 = strlen(s);                               // 길이만큼 strlen
  printf("Your power is : %u\n", v2);
  *((_DWORD *)s + 12) = v2;
  return puts("Good luck !!");
}

create_bullet() 은 read_input(s, 48) 으로 값을 입력받고, strlen(s) 로 길이를 v2에 저장, 후 printf() 함수에서 내가 입력했던 문자열의 길이가 출력된다. 

create_bullet() 호출 시 인자로 받았던 *s(배열) 의 12*4 = 48 번째 인덱스에 저장된다. 

=> 배열의 크기(48byte) 이후에 저장이 된다. 이는 직접 디버깅해서 보면 쉽게 파악 할 수 있다.

 

ssize_t __cdecl read_input(void *buf, size_t nbytes)
{
  ssize_t v3; // [esp+0h] [ebp-4h]

  v3 = read(0, buf, nbytes);                    // 48byte input
  if ( v3 <= 0 )
  {
    puts("read error");
    exit(1);
  }
  if ( *((_BYTE *)buf + v3 - 1) == 10 )         // 10(0xa) => '\n'
    *((_BYTE *)buf + v3 - 1) = 0;
  return v3;
}

create_bullet() 함수 내부에 존재하는 read_input() 함수는 read() 함수로 최대 48byte 를 입력받는다. 

그런데 중간에 read() 함수에서 문자열을 읽은 길이의 -1 번째의 값이 '\n' 이 아니라면 0을 넣어 

강제로 널 문자를 추가해 준다. (만약 canary가 있었다면 이 부분에서는 leak 이 안됬을 것이다.)

 

2. power_up()

더보기
int __cdecl power_up(char *dest)
{
  char s; // [esp+0h] [ebp-34h]
  size_t v3; // [esp+30h] [ebp-4h]

  v3 = 0;
  memset(&s, 0, 48u);
  if ( !*dest )
    return puts("You need create the bullet first !");
  if ( *((_DWORD *)dest + 12) > 47u )
    return puts("You can't power up any more !");
  printf("Give me your another description of bullet :");
  read_input(&s, 48 - *((_DWORD *)dest + 12));
  strncat(dest, &s, 48 - *((_DWORD *)dest + 12));
  v3 = strlen(&s) + *((_DWORD *)dest + 12);
  printf("Your new power is : %u\n", v3);
  *((_DWORD *)dest + 12) = v3;
  return puts("Enjoy it !");
}

power_up() 함수는 bullet 이 하나 만들어져 있어야 한다. 그렇지 않으면 puts 를 return 해 주면서 프로그램이 종료된다. 

또한, create_bullet() 에서 strlen()으로 buf+49 번째의 공간에 길이를 저장했는데, power_up() 함수에서 dest + 49가 넘어가게 되면 puts() 함수를 return 하면서 프로그램이 종료된다. 

 

그렇지 않으면 값을 한번 더 입력하는데, 주의할 점이 입력할 수 있는 길이가 제한되어 있다.

(48byte - (s + 49))

이후에 strncat() 함수로 main() 함수에서 저장했던 dest 배열에 power_up함수 내에서 입력했던 값이 strncat 되어 값이 붙고, 새로 입력한 값의 길이가 이전에 입력했던 값의 문자열의 길이가 더해진다(buf+49) 

그리고 printf() 함수에서 buf+49 에 저장되어 있는 문자열의 총길이를 출력하고 출력된 결과값이 저장된다.

 

3. beat()

더보기
signed int __cdecl beat(int a1, _DWORD *a2)
{
  signed int result; // eax

  if ( *(_BYTE *)a1 )
  {
    puts(">----------- Werewolf -----------<");
    printf(" + NAME : %s\n", a2[1]);
    printf(" + HP : %d\n", *a2);                // leak address 
    puts(">--------------------------------<");
    puts("Try to beat it .....");
    usleep(1000000u);
    *a2 -= *(_DWORD *)(a1 + 48);
    if ( *a2 <= 0 )
    {
      puts("Oh ! You win !!");
      result = 1;
    }
    else
    {
      puts("Sorry ... It still alive !!");
      result = 0;
    }
  }
  else
  {
    puts("You need create the bullet first !");
    result = 0;
  }
  return result;
}

beat()의 첫번째 인자는 create_bullet, power_up 에서 쓰였던 buf가 들어가고, 두번째 인자는 main 함수에 선언되어 있는 a2가 들어갔다.

a2[1] , *a2 에는 각각 'Gin', 0x7FFFFFFF 가 저장되어 있다.

여기서 a2[1] 과 *a2를 출력해 주고, *a2(0x7FFFFFFF) 을 (a1+48)에 위치해 있는 값을 빼주고, *a2 가 <= 0 보다 작으면 프로그램이 종료되고, 그렇지 않으면 다시 main() 함수로 가서 프로그램을 실행시킨다. 

 

5. read_int() 함수

더보기
int read_int()
{
  char buf; // [esp+0h] [ebp-18h]
  ssize_t v2; // [esp+14h] [ebp-4h]

  v2 = read(0, &buf, 15u);
  if ( v2 <= 0 )
  {
    puts("read error");
    exit(1);
  }
  return atoi(&buf);
}

혹시나 하는 생각에 올렸다. 

read() 함수로 최대 15자리수 까지 입력받고 atoi() 로 return 해주는 함수이다. 

 

함수들을 분석해 보았으니 이걸로 어디에서 취약점이 생기고, 어떤 방식으로 exploit 해야할지 생각해보자.

 

이 프로그램에서 취약점은 power_up() 에 존재한다.

if 문 중에 *(DWORD *)dest + 12 > 47 이 있는데, 만약 48byte를 입력하면 더 이상 함수 호출이 불가능하지만,

47byte 를 입력하게 되면 read_input() 함수로 가고 strncat() 함수를 호출하게 된다. 

 

여기서 off-by-one 취약점이 발생하는데, 47byte 를 create_bullet 함수에서 입력해 주고

1byte 를 power_up 으로 입력해 주면 dest + 12(길이가 저장되는 부분) 에 '\n'이 저장되므로 

길이(len) 은 0으로 초기화 되었다가 1byte를 입력해 주었기 때문에, 1이 되고, 이를 이용해

RET 부분을 덮을 수 있게 될 뿐 아니라, beat() 로 HP 를 깎을 수 있으므로 exploit 이 가능해진다. 

 

+exploit 코드를 공개하면 안된다는 말이 들려서 앞으로 pwnable.tw 문제는 exploit 코드를 공개하지 않겠다

 

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

[pwnable.tw] seethefile  (0) 2021.01.16
[pwnable.tw] applestore  (0) 2020.12.29
[pwnable.tw] hacknote  (0) 2020.12.07
[pwnable.tw] dubblesort  (0) 2020.11.22
[pwnable.tw] 3x17  (0) 2020.11.20
Comments