가젯 찾는법 (ROPgadget, ropsearch, rp-lin)
가젯 찾는 법
1. 가젯?
1. 가젯이란?
메모리에 존재하는 명령어를 얘기한다.
ret 로 끝나고, 공유 libc 나 바이너리 안에 존재한다.
x86 에서는 pop 을 이용해 esp 값을 조정해 호출할 인자수를 조정한다.
x64 에서는 pop rdi, pop rsi 등의 레지스터를 이용해 인자를 전달한다.
2. x64 가젯
x64 환경에서는 rdi, rsi, rdx, rcx, r8, r9 의 순으로 인자를 전달한다.
따라서 pop rdi, pop rsi, pop rdx 같은 함수의 인자 순서에 맞는 레지스터를 찾아야 한다.
가젯을 찾는 방법을 알아보자
2. objudmp 명령어
objudmp -d "파일명" | egrep "가젯"
-d 는 파일을 디스어셈블 할 때 사용하는 옵션이다.
objdump 로 가젯을 찾을 수 있다.
3. ROPgadget
$ sudo pip3 install ropgadget
위의 명령어를 사용하면 ROPgadget 을 설치할 수 있다.
ROPgadget --binary "파일명" | egrep "가젯"
만약 ROPgadget 으로 가젯을 찾았지만 아무것도 뜨지 않는다면
ROPgadget 으로 조회가 되지 않거나 진짜 가젯이 없는 경우이다.
4. ropsearch (gdb-peda)
gdb-peda 에서 사용할 수 있는 명령어이다.
ropsearch "가젯"
gdb-peda 로 파일을 실행한 뒤 main 에 break 를 걸고 실행하자.
만약 위와 같은 오류가 뜬다면 nasm 라이브러리가 깔려있지 않아서 그러니 깔아주도록 하자
$ sudo apt-get install nasm
위의 명령어로 설치할 수 있다.
가젯이 잘 찾아지는 것을 확인할 수 있다.
5. rp++ (libc)
1. 설치
rp 는 github 에서 추가로 다운받아야 한다.
https://github.com/0vercl0k/rp/releases
위의 링크에서 다운받을 수 있다.
$ wget https://github.com/0vercl0k/rp/releases/download/v2.1.3/rp-lin-gcc.zip
명령어로 rp 를 다운받을 수 있다.
$ sudo apt-get install zip unzip
zip, unzip 이 설치되지 않았다면 위의 명령어로 설치할 수 있다.
다운 받은 파일을 명령어처럼 사용하기 위해서는
/usr/local/bin 으로 파일을 이동해줘야 한다.
$ sudo mv rp-lin /usr/local/bin
bin 에 파일을 이동시키지 않으면 명령어처럼 사용이 불가능하다.
./ 사용하여 파일을 실행하여 실행해야 한다.
$ chmod u+x rp-lin
파일에 실행권한을 주면 설치가 끝나게 된다.
2. 사용
rp 는 라이브러리에서 가젯을 찾을 때 사용한다.
rp-lin -f ./"파일명" -r 4 | egrep "가젯"
무수히 많은 가젯들을 찾을 수 있다.
주소는 offset 으로 나오기 때문에 라이브러리 시작 주소를 더해 사용해야 한다.
6. pop rdi, pop rsi 가젯 찾기
1. pop rdi, pop rsi 가젯
x64 바이너리에서는 pop rdi, pop rsi 가젯이 필수적으로 사용된다.
가젯을 쉽게 찾는 방법을 알아보자.
// Name : gadget.c
//Compile : gcc -o gadget gadget.c -fno-stack-protector -no-pie
void main()
{
__asm__ __volatile__
(
"pop %rsi \n\t"
"pop %rdi \n\t"
);
}
위의 코드를 컴파일 한 후 objdump 로 확인해보자
pop rdi 가젯은 \x5f 이다.
pop rsi 가젯은 \x5e 이다.
아래에 보면 pop r14 와 pop r15 를 확인해보면
\x5e, \x5f 가 들어있는 것을 알 수 있다.
- pop rdi 가젯 : 0x401183
- pop rsi 가젯 : 0x401181
위와 같이 pop rdi, pop rdi 가젯을 구할 수 있다.
그런데 pop r14 와 pop r15 는 어디서 나온걸까?
2. __libc_csu_init
__libc_csu_init 함수에 대해 알아보자
- __libc_csu_init 은 main 함수가 호출되기 전 .init_array 에 저장된 함수들을 호출시킨다.
- 컴파일 할 때 바이너리에 추가된다.
컴파일 할 때 바이너리에 추가되기 때문에 컴파일 된 바이너리에서 코드를 확인할 수 있다.
이제 어셈블리를 살펴보자
Dump of assembler code for function __libc_csu_init:
0x0000000000401120 <+0>: endbr64
0x0000000000401124 <+4>: push r15
0x0000000000401126 <+6>: lea r15,[rip+0x2d23] # 0x403e50
0x000000000040112d <+13>: push r14
0x000000000040112f <+15>: mov r14,rdx
0x0000000000401132 <+18>: push r13
0x0000000000401134 <+20>: mov r13,rsi
0x0000000000401137 <+23>: push r12
0x0000000000401139 <+25>: mov r12d,edi
0x000000000040113c <+28>: push rbp
0x000000000040113d <+29>: lea rbp,[rip+0x2d14] # 0x403e58
0x0000000000401144 <+36>: push rbx
0x0000000000401145 <+37>: sub rbp,r15
0x0000000000401148 <+40>: sub rsp,0x8
0x000000000040114c <+44>: call 0x401000 <_init>
0x0000000000401151 <+49>: sar rbp,0x3
0x0000000000401155 <+53>: je 0x401176 <__libc_csu_init+86>
0x0000000000401157 <+55>: xor ebx,ebx
0x0000000000401159 <+57>: nop DWORD PTR [rax+0x0]
0x0000000000401160 <+64>: mov rdx,r14
0x0000000000401163 <+67>: mov rsi,r13
0x0000000000401166 <+70>: mov edi,r12d
0x0000000000401169 <+73>: call QWORD PTR [r15+rbx*8]
0x000000000040116d <+77>: add rbx,0x1
0x0000000000401171 <+81>: cmp rbp,rbx
0x0000000000401174 <+84>: jne 0x401160 <__libc_csu_init+64>
0x0000000000401176 <+86>: add rsp,0x8
0x000000000040117a <+90>: pop rbx
0x000000000040117b <+91>: pop rbp
0x000000000040117c <+92>: pop r12
0x000000000040117e <+94>: pop r13
0x0000000000401180 <+96>: pop r14
0x0000000000401182 <+98>: pop r15
0x0000000000401184 <+100>: ret
End of assembler dump.
어셈블리의 마지막 부분을 살펴보면 pop r14, pop r15 가 있는 것을 볼 수 있다.
위의 방식을 사용하면 rp-lin 과 같은 툴을 사용하지 않고 빠르게 가젯을 찾을 수 있다.
7. pop rdx 가젯 찾기
1. pop rdx
pop rdi, pop rsi 가젯은 위의 방법으로 빠르게 찾을 수 있다.
하지만 read, write 같이 자주 쓰는 함수들은 인자 3개를 받기 때문에 pop rdx 가젯이 필요하다.
그럼 pop rdx 가젯은 어떻게 찾을까?
// Name : test.c
// Code : gcc test test.c -o -no-pie -fno-stack-protector
#include <stdio.h>
int main(){
char buf[0x10];
printf("Input > ");
scanf("%s", buf);
write(1, buf, 100);
return 0;
}
위의 코드를 컴파일 한 뒤 어셈블리를 확인해보자
main 함수의 어셈블리는 다음과 같다.
Dump of assembler code for function main:
0x0000000000401176 <+0>: endbr64
0x000000000040117a <+4>: push rbp
0x000000000040117b <+5>: mov rbp,rsp
0x000000000040117e <+8>: sub rsp,0x10
0x0000000000401182 <+12>: lea rax,[rip+0xe7b] # 0x402004
0x0000000000401189 <+19>: mov rdi,rax
0x000000000040118c <+22>: mov eax,0x0
0x0000000000401191 <+27>: call 0x401070 <printf@plt>
0x0000000000401196 <+32>: lea rax,[rbp-0x10]
0x000000000040119a <+36>: mov rsi,rax
0x000000000040119d <+39>: lea rax,[rip+0xe69] # 0x40200d
0x00000000004011a4 <+46>: mov rdi,rax
0x00000000004011a7 <+49>: mov eax,0x0
0x00000000004011ac <+54>: call 0x401080 <__isoc99_scanf@plt>
0x00000000004011b1 <+59>: lea rax,[rbp-0x10]
0x00000000004011b5 <+63>: mov edx,0x64
0x00000000004011ba <+68>: mov rsi,rax
0x00000000004011bd <+71>: mov edi,0x1
0x00000000004011c2 <+76>: mov eax,0x0
0x00000000004011c7 <+81>: call 0x401060 <write@plt>
0x00000000004011cc <+86>: mov eax,0x0
0x00000000004011d1 <+91>: leave
0x00000000004011d2 <+92>: ret
End of assembler dump.
write 명령어가 실행되기 직전 main+76 에 break 를 걸고 레지스터를 살펴보자
rdx 를 확인해보면 write 의 인자인 0x64 (100) 이 잘 들어간 것을 확인할 수 있다.
이제 ret 가 실행되기 직전 break 를 걸고 레지스터를 확인해보자
rdx 를 확인해보면 초기화 되지 않고 0x64 (100) 이 들어있는 것을 알 수 있다.
rdx 값이 조작되지 않기 때문에 ROP 를 진행할 때 rdx 를 그대로 사용할 수 있다.
따라서 pop rdx 가젯을 찾지 않고도 ROP 를 그대로 진행할 수 있다.
2. rdx 초기화
rdx 의 값을 초기화 하고 싶으면 어떻게 할까?
// Name : test.c
// Code : gcc test.c -o test2 -no-pie -fno-stack-protector
#include <stdio.h>
int main(){
char buf[0x10];
printf("Input > ");
scanf("%s", buf);
write(1, buf, 100);
printf("test");
return 0;
}
위의 코드에서 write 함수 이후 printf 함수만 추가해 준 코드이다.
main 함수의 어셈블리를 살펴보자
Dump of assembler code for function main:
0x0000000000401176 <+0>: endbr64
0x000000000040117a <+4>: push rbp
0x000000000040117b <+5>: mov rbp,rsp
0x000000000040117e <+8>: sub rsp,0x10
0x0000000000401182 <+12>: lea rax,[rip+0xe7b] # 0x402004
0x0000000000401189 <+19>: mov rdi,rax
0x000000000040118c <+22>: mov eax,0x0
0x0000000000401191 <+27>: call 0x401070 <printf@plt>
0x0000000000401196 <+32>: lea rax,[rbp-0x10]
0x000000000040119a <+36>: mov rsi,rax
0x000000000040119d <+39>: lea rax,[rip+0xe69] # 0x40200d
0x00000000004011a4 <+46>: mov rdi,rax
0x00000000004011a7 <+49>: mov eax,0x0
0x00000000004011ac <+54>: call 0x401080 <__isoc99_scanf@plt>
0x00000000004011b1 <+59>: lea rax,[rbp-0x10]
0x00000000004011b5 <+63>: mov edx,0x64
0x00000000004011ba <+68>: mov rsi,rax
0x00000000004011bd <+71>: mov edi,0x1
0x00000000004011c2 <+76>: mov eax,0x0
0x00000000004011c7 <+81>: call 0x401060 <write@plt>
0x00000000004011cc <+86>: lea rax,[rip+0xe3d] # 0x402010
0x00000000004011d3 <+93>: mov rdi,rax
0x00000000004011d6 <+96>: mov eax,0x0
0x00000000004011db <+101>: call 0x401070 <printf@plt>
0x00000000004011e0 <+106>: mov eax,0x0
0x00000000004011e5 <+111>: leave
0x00000000004011e6 <+112>: ret
End of assembler dump.
write 함수 호출 직전 main + 76 에 break 를 걸고 레지스터를 확인해보자
rdx 에 0x64 (100) 값이 잘 들어있는 것을 확인할 수 있다.
ret 직전 break 를 걸고 레지스터를 확인해보자.
rdx 가 0으로 초기화 된 것을 볼 수 있다.
scanf, printf 와 같은 함수를 사용하면 rdx 값이 조작되었더라도 NULL 값으로 초기화 되는 것을 볼 수 있다.