[Pwn] Pwnable · 작성: 2026-07-19 19:31:03 · 조회 5
ROP는 NX 때문에 새 코드를 심을 수 없을 때, 바이너리나 라이브러리에 이미 존재하는 코드 조각들을 이어 붙여서 원하는 동작을 조립하는 기법이다. ret2libc가 "이미 있는 함수 하나를 호출"하는 수준이라면, ROP는 그걸 훨씬 일반화해서 "이미 있는 코드 조각들로 사실상 원하는 프로그램을 조립"하는 수준까지 간다.
ret(리턴) 명령어로 끝나는 짧은 명령어 시퀀스를 가젯이라고 부른다.
pop rdi
ret
이 두 줄짜리 코드가 있으면: 스택 맨 위에서 값을 하나 꺼내 RDI 레지스터에 넣고(pop rdi), 그 다음 다시 스택 맨 위에 있는 값으로 점프한다(ret). 리턴 주소를 이 가젯 주소로 덮어두면, 스택에 원하는 값 → 다음 가젯 주소를 순서대로 쌓아서 마치 프로그램을 짜듯 명령어를 이어 붙일 수 있다. 이게 ROP 체인의 원리다.
스택: [pop_rdi 주소] [binsh 주소] [system 주소]
ret하면서 스택 맨 위의 pop_rdi 주소로 점프pop rdi; ret 실행 → RDI에 binsh 주소가 들어가고, 다시 스택 맨 위(이제 system 주소)로 점프system(RDI) 즉 system("/bin/sh")가 호출된 것과 동일한 효과가젯을 여러 개 이어 붙이면 레지스터 여러 개를 세팅하고, 함수를 순서대로 여러 번 호출하는 것도 가능하다 — 이론적으로는 튜링 완전(Turing-complete)한 계산도 ROP만으로 할 수 있다는 게 알려져 있다.
ROPgadget --binary ./vuln | grep "pop rdi"
# 또는
ropper --file ./vuln --search "pop rdi"
바이너리 자체에 필요한 가젯이 부족하면 libc처럼 항상 로드되는 큰 라이브러리에서 찾는다. libc는 코드 양이 많아서 대부분의 원하는 가젯을 찾을 수 있다.
system()이 없거나 막혀 있을 때는, libc 함수 대신 시스템 콜을 직접 호출하는 syscall 가젯을 이용해서 execve("/bin/sh", NULL, NULL)을 직접 만드는 방법도 있다. RAX에 시스템콜 번호(execve는 59), RDI/RSI/RDX에 각각 인자를 채우고 syscall 가젯으로 점프시킨다.
from pwn import *
elf = context.binary = ELF('./vuln')
rop = ROP(elf)
rop.system(next(elf.search(b'/bin/sh')))
payload = b'A' * offset + rop.chain()
ROP 클래스는 필요한 가젯을 자동으로 찾아서 체인을 만들어준다. 처음엔 손으로 한 번씩 짜보면서 원리를 익히고, 익숙해지면 이렇게 자동화 도구로 넘어가는 순서를 권한다.