일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 포맷스트링버그
- 백트래킹
- off by one
- 큐
- 이진 탐색
- 스택
- BOF
- fsb
- 에라토스테네스의 체
- ROP
- 스위핑 알고리즘
- 수학
- syscall
- 문자열 처리
- 연결리스트
- OOB
- 다이나믹 프로그래밍
- heap
- 이진트리
- 동적 계획법
- DFS
- tcache
- 완전 탐색
- 분할 정복
- 투 포인터
- BFS
- 이분 탐색
- RTL
- House of Orange
- 브루트 포스
- Today
- Total
SDJ( 수돈재 아님 ㅎ )
[Exploitation Technique] RTL chaining 본문
이전(예전) 글에서 RTL에 대해 배웠다.
몇 달전 쓰다가 만 글인데 새롭게 고쳐쓰려고 다시 갈아엎었다
RTL chaining을 위한 사전 지식.
2020/02/04 - [study/pwnable] - [Exploitation Technique] RTL 기법 ( Return-to-libc )
이전 RTL 글에서 나는 쉘을 따고나서 함수의 끝이 yyyy나 YYYYYYYY에서 끝나는 것을 알았고,
이 글에서 쓸 내용은 이제 그것을 가지고 무엇을 어떻게 할지에 대해 짱구를 굴리는 글이다.
글은
-
RTL chaining
-
32 bit RTL
-
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()
'study > pwnable' 카테고리의 다른 글
[Exploitation Technique] ROP (Return Oriented Programming) (0) | 2020.05.30 |
---|---|
malloc_hook에 값이 있을 때 __libc_calloc? (0) | 2020.04.10 |
stderr로 stdout 대체하기? (0) | 2020.02.14 |
[Exploitation Technique] RTL 기법 (Return-to-libc) (0) | 2020.02.04 |
scanf("%d")에 대한 잡기술(?)... (0) | 2020.01.10 |