Hacking/Pwnable

가젯 찾는법 (ROPgadget, ropsearch, rp-lin)

GunP4ng 2024. 9. 18. 23:45

가젯 찾는 법


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

objdump 로 가젯을 찾을 수 있다.

 

 

3. ROPgadget

$ sudo pip3 install ropgadget

위의 명령어를 사용하면 ROPgadget 을 설치할 수 있다.

 

ROPgadget --binary "파일명" | egrep "가젯"

 

ROPgadget

 

만약 ROPgadget 으로 가젯을 찾았지만 아무것도 뜨지 않는다면

ROPgadget 으로 조회가 되지 않거나 진짜 가젯이 없는 경우이다.

 

 

4. ropsearch (gdb-peda)

gdb-peda 에서 사용할 수 있는 명령어이다.

ropsearch "가젯"

 

gdb-peda 로 파일을 실행한 뒤 main 에 break 를 걸고 실행하자.

error

만약 위와 같은 오류가 뜬다면 nasm 라이브러리가 깔려있지 않아서 그러니 깔아주도록 하자

$ sudo apt-get install nasm

위의 명령어로 설치할 수 있다.

 

ropsearch

가젯이 잘 찾아지는 것을 확인할 수 있다.

 

 

5. rp++ (libc)

1. 설치

rp 는 github 에서 추가로 다운받아야 한다.

 

https://github.com/0vercl0k/rp/releases

 

Releases · 0vercl0k/rp

rp++ is a fast C++ ROP gadget finder for PE/ELF/Mach-O x86/x64/ARM/ARM64 binaries. - 0vercl0k/rp

github.com

위의 링크에서 다운받을 수 있다.

 

$ 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 "가젯"

 

 

rp-lin

무수히 많은 가젯들을 찾을 수 있다.

주소는 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, pop rsi 가젯

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 를 걸고 레지스터를 살펴보자

main + 76

rdx 를 확인해보면 write 의 인자인 0x64 (100) 이 잘 들어간 것을 확인할 수 있다.

 

이제 ret 가 실행되기 직전 break 를 걸고 레지스터를 확인해보자

main + 91

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 를 걸고 레지스터를 확인해보자

main + 76

rdx 에 0x64 (100) 값이 잘 들어있는 것을 확인할 수 있다.

 

ret 직전 break 를 걸고 레지스터를 확인해보자.

main + 111

rdx 가 0으로 초기화 된 것을 볼 수 있다.

 

scanf, printf 와 같은 함수를 사용하면 rdx 값이 조작되었더라도 NULL 값으로 초기화 되는 것을 볼 수 있다.