├── README.md ├── andromeda.py ├── plugx.py └── plugx_structures.py /README.md: -------------------------------------------------------------------------------- 1 | # volatility_plugins 2 | Volatility Plugins 3 | 4 | A collection of plugins for the [Volatility](https://github.com/volatilityfoundation/volatility) framework that I have 5 | authored or made significant contributions to. 6 | 7 | The PlugX configuration extraction is a fork and update of the plugin located at http://bitbucket.cassidiancybersecurity.com/volatility_plugins/wiki/Home 8 | 9 | with more configuration sizes supported and moving to ctypes Structure for parsing of the configuration blob. 10 | 11 | The Andromeda configuration extraction plugin will attempt to locate and extract C2 URLs, RC4 key used for initial communication, 12 | and parameters in the phone-home format string 13 | 14 | # Install 15 | 16 | The andromeda plugin requires PyCrypto and [Yara](http://plusvic.github.io/yara/) python module to be installed. Manual installation of yara is recommended to obtain the latest release, instructions are available on the Yara site. 17 | 18 | * On Debian-based systems these modules can be installed via 19 | 20 | $ apt-get install python-crypto python-yara 21 | 22 | * PyCrypto can also be installed via pip 23 | 24 | $ sudo pip install pycrypto 25 | 26 | The andromeda plugin also requires [Capstone](http://capstone-engine.org) to be installed. 27 | 28 | * On *nix (including Mac OS X, Linux, BSD, etc), do this with: 29 | 30 | $ sudo pip install capstone 31 | 32 | * On Windows, there are 2 choices: 33 | 34 | * Download & install Python binary package from [Capstone homepage](http://capstone-engine.org/download.html) 35 | * Download PyPi package [capstone-windows](https://pypi.python.org/pypi/capstone-windows), then unzip & install from commandline with: 36 | 37 | `python setup.py install` 38 | 39 | # Usage 40 | 41 | To search for and print out Andromeda configuration: 42 | 43 | $ python vol.py -f memory.dmp andromeda 44 | Volatility Foundation Volatility Framework 2.4 45 | Andromeda Config Located 46 | Process msiexec.exe (PID: 2952, VAD: 0x7ff90000) 47 | Bb: 0 48 | Url: hxxp://andromeda-hostname[.]com/andromeda-path.php 49 | Bid: 9 50 | Fmt Str: {"id":%lu,"bid":%lu,"os":%lu,"la":%lu,"rg":%lu,"bb":%lu 51 | Rg: 1 52 | Key: f5d0e0420865071a12c22a84702daca3 53 | Os: 351 54 | Id: 2cae84cd 55 | 56 | The usage for the modified PlugX plugin has not changed, but the naming for the new versions is slightly different than the original. These will be unified at a later date. 57 | 58 | $ python vol.py -f memory.dmp plugxconfig 59 | 60 | Process: iexplore.exe (3044) 61 | 62 | PlugX Config (0x2d58 bytes): 63 | Hide Dll: -1 64 | Keylogger: -1 65 | Sleep1: 167772160 66 | Sleep2: 0 67 | Cnc: plugx[.]cnc:53 (TCP / HTTP / UDP / ICMP / DNS) 68 | Cnc: plugx[.]cnc:80 (TCP / HTTP / UDP / ICMP / DNS) 69 | Cnc: plugx[.]cnc:53 (TCP / HTTP / UDP / ICMP / DNS) 70 | Cnc: plugx[.]cnc:80 (TCP / HTTP / UDP / ICMP / DNS) 71 | Persistence: None 72 | Install Folder: %APPDATA% 73 | Reg Hive: Unknown 74 | Injection: 0 75 | Inject Process: %ProgramFiles%\Internet Explorer\iexplore.exe 76 | Inject Process: %windir%\system32\svchost.exe 77 | Inject Process: %ProgramFiles%\Internet Explorer\iexplore.exe 78 | Inject Process: %windir%\system32\svchost.exe 79 | Uac Bypass Injection: 0 80 | Plugx Auth Str: admin#@1 81 | Cnc Auth Str: message4 82 | Mutex: g1bsTj 83 | Screenshots: 1 84 | Screenshots Sec: 0 85 | Screenshots Zoom: 0 86 | Screenshots Bits: 0 87 | Screenshots Qual: 0 88 | Screenshots Keep: 0 89 | Lateral Tcp Enabled: 1 90 | Lateral Tcp Port: 535 91 | Lateral Udp Enabled: 1 92 | Lateral Udp Port: 535 93 | Lateral Unk Enabled: 1 94 | Lateral Unk Port: 535 95 | Unk 2D4C: 0 96 | Unk 2D50: 0 97 | Unk 2D58: 0 98 | -------------------------------------------------------------------------------- /andromeda.py: -------------------------------------------------------------------------------- 1 | # Andromeda detection and analysis for Volatility 2.X 2 | # 3 | # Version 1.0 4 | # 5 | # Author: Jason Jones 6 | # 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or (at 10 | # your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 | 21 | import volatility.plugins.taskmods as taskmods 22 | import volatility.win32.tasks as tasks 23 | import volatility.utils as utils 24 | import volatility.debug as debug 25 | import volatility.plugins.malware.malfind as malfind 26 | import struct 27 | import json 28 | from collections import OrderedDict 29 | 30 | can_run = False 31 | try: 32 | from Crypto.Cipher import ARC4 33 | from capstone import * 34 | from capstone.x86 import * 35 | import yara 36 | can_run = True 37 | except ImportError: 38 | pass 39 | 40 | signatures = { 41 | 'androm': """rule andromeda { 42 | strings: 43 | $fmt1 = "id:%lu|bid:%lu|os:%lu" 44 | $fmt2 = "{\\"id\\":%lu,\\"bid\\":%lu,\\"os\\":%lu" 45 | $s1 = "aReport" 46 | $s2 = "aStart" 47 | $s3 = "aUpdate" 48 | $s4 = "User-Agent: Mozi1la/4.0" 49 | condition: 1 of ($fmt*) and 1 of ($s*) 50 | } 51 | """ 52 | } 53 | class Andromeda(taskmods.DllList): 54 | 55 | def brute_force_key(self,base,bid,data): 56 | # key and U 57 | ret = {} 58 | rc4_key = None 59 | url_data = data 60 | # typically first null byte after bid addr is end of the linked list 61 | list_end = data[bid:].find('\x00')+bid 62 | last_url = list_end - 1 63 | # walk up the linked list until we find a byte that matches distance to null 64 | # 65 | attempts = 0 66 | for i in range(3): 67 | while last_url > bid and last_url + ord(data[last_url])+1 != list_end: 68 | last_url = last_url - 1 69 | if i < 2 and last_url == bid: 70 | list_end = data[list_end+1:].find('\x00') + list_end + 1 71 | last_url = list_end - 1 72 | else: break 73 | 74 | if last_url == bid: 75 | return {} 76 | # crypted http:// 77 | http = data[last_url+1:last_url+7] 78 | url_data = data[:last_url] 79 | # walk backwards until we find the beginning of the linked list 80 | while url_data.rfind(http) > 0: 81 | url_data = url_data[:url_data.rfind(http)-1] 82 | 83 | # take 16 byte chunks and see if they decrypt to http:// 84 | rc4_key = None 85 | for i in range(bid,bid+0x200): 86 | k = data[i:i+16].encode('hex')[::-1] 87 | r = ARC4.new(k) 88 | d = r.decrypt(http) 89 | if d.startswith('http:/'): 90 | rc4_key = data[i:i+16].encode('hex') 91 | if rc4_key: 92 | ret['key'] = rc4_key 93 | ret['url'] = [] 94 | i = len(url_data) 95 | while data[i] != '\x00': 96 | url_len = ord(data[i]) 97 | crypted = data[i + 1:url_len + i + 1] 98 | r = ARC4.new(rc4_key[::-1]) 99 | ret['url'].append(r.decrypt(crypted)) 100 | i = url_len + i + 1 101 | return ret 102 | 103 | def get_config(self,base_addr,data): 104 | res = {} 105 | md = Cs(CS_ARCH_X86,CS_MODE_32) 106 | md.detail = True 107 | ph_str = data.find("id:%lu|bid:%lu") 108 | ph_str = data.find("""{"id":%lu,"bid":%lu,""") if ph_str == -1 else ph_str 109 | if ph_str == -1: return res 110 | fmt_str = data[ph_str:ph_str+data[ph_str:].find('\x00')] 111 | 112 | if "{" in fmt_str: 113 | ph_dict = OrderedDict([x.split(':') for x in fmt_str.replace('"','').replace('{','').replace('}','').split(",")]) 114 | else: 115 | ph_dict = OrderedDict([x.split(':') for x in data[ph_str:ph_str+data[ph_str:].find('\x00')].split('|')]) 116 | func_prolog = '\x55\x8b\xec' 117 | reg = {} 118 | if ph_str: 119 | ph_addr = base_addr + ph_str 120 | ph_push = "\x68" + struct.pack(" address >= vad.Start: 187 | return vad 188 | return None 189 | 190 | def calculate(self): 191 | if not can_run: 192 | debug.error("Yara, Capstone and PyCrypto must be installed for this plugin") 193 | 194 | addr_space = utils.load_as(self._config) 195 | 196 | rules = yara.compile(sources=signatures) 197 | 198 | for task in self.filter_tasks(tasks.pslist(addr_space)): 199 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 200 | for hit, addr in scanner.scan(): 201 | yield task, addr 202 | 203 | def render_text(self, outfd, data): 204 | vads = set() 205 | found = [] 206 | for task, addr in data: 207 | vad = self.get_vad(task,addr) 208 | if vad in vads: continue 209 | vads.add(vad) 210 | proc_addr_space = task.get_process_address_space() 211 | data = proc_addr_space.zread(vad.Start, vad.End-vad.Start+1) 212 | 213 | config = self.get_config(vad.Start,data) 214 | if config: 215 | outfd.write("Andromeda Config Located\n") 216 | outfd.write("Process {} (PID: {}, VAD: 0x{:x})\n".format(task.ImageFileName, task.UniqueProcessId,vad.Start)) 217 | for k,v in config.items(): 218 | if type(v) in (list,tuple,set): 219 | for y in v: 220 | outfd.write("\t{}: {}\n".format(k.replace('_',' ').title(),y)) 221 | elif type(v) == int: 222 | outfd.write("\t{}: {:x}\n".format(k.replace('_',' ').title(),v)) 223 | else: 224 | outfd.write("\t{}: {}\n".format(k.replace('_',' ').title(),v)) 225 | 226 | def render_json(self, outfd, data): 227 | vads = set() 228 | found = [] 229 | for task, addr in data: 230 | vad = self.get_vad(task,addr) 231 | if vad in vads: continue 232 | vads.add(vad) 233 | proc_addr_space = task.get_process_address_space() 234 | data = proc_addr_space.zread(vad.Start, vad.End-vad.Start+1) 235 | config = self.get_config(vad.Start,data) 236 | outfd.write("{}\n".format(json.dumps(config))) 237 | -------------------------------------------------------------------------------- /plugx.py: -------------------------------------------------------------------------------- 1 | # PlugX RAT detection and analysis for Volatility 2.X 2 | # 3 | # Version 1.2 4 | # 5 | # Original Author: Fabien Perigaud 6 | # Author: Jason Jones 7 | # 8 | # This plugin is based on poisonivy.py by Andreas Schuster. 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or (at 13 | # your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, but 16 | # WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | # General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 23 | 24 | import volatility.plugins.taskmods as taskmods 25 | import volatility.win32.tasks as tasks 26 | import volatility.utils as utils 27 | import volatility.debug as debug 28 | import volatility.plugins.malware.malfind as malfind 29 | from struct import unpack_from, calcsize, unpack, pack 30 | from socket import inet_ntoa 31 | from collections import defaultdict 32 | 33 | import plugx_structures 34 | 35 | try: 36 | import yara 37 | has_yara = True 38 | except ImportError: 39 | has_yara = False 40 | 41 | # Simple rule: 42 | # - look for GULP signature at the beginning of a VAD (v1) 43 | # - look for /update?id=%8.8x 44 | # - look for (a push 0x2a0 or a "Proxy-Auth:" string) AND (use of a 0x713a8fc1 value or signature to identify v1 algorithm) 45 | # When scanning, also check that the VAD is RWX 46 | signatures = { 47 | 'namespace1': 'rule plugx { \ 48 | strings: \ 49 | $v1a = { 47 55 4C 50 00 00 00 00 } \ 50 | $v1b = "/update?id=%8.8x" \ 51 | $v1algoa = { BB 33 33 33 33 2B } \ 52 | $v1algob = { BB 44 44 44 44 2B } \ 53 | $v2a = "Proxy-Auth:" \ 54 | $v2b = { 68 A0 02 00 00 } \ 55 | $v2k = { C1 8F 3A 71 } \ 56 | $v2p = { 68 A4 36 00 00 } \ 57 | condition: $v1a at 0 or $v1b or (($v2a or $v2b) and (($v1algoa and $v1algob) or $v2k or $v2p)) }' 58 | } 59 | # $v1algo = { BB 33 33 33 33 2B .. .. .. .. .. .. .. .. .. 09 BB 44 44 44 44 2B } \ 60 | class PlugXScan(taskmods.DllList): 61 | """Detect processes infected with PlugX""" 62 | 63 | @staticmethod 64 | def is_valid_profile(profile): 65 | return (profile.metadata.get('os', 'unknown') == 'windows' and 66 | profile.metadata.get('memory_model', '32bit') == '32bit') 67 | 68 | @staticmethod 69 | def get_vad_base(task, address): 70 | """ Get the VAD starting address """ 71 | for vad in task.VadRoot.traverse(): 72 | if vad.End > address >= vad.Start: 73 | return vad.Start 74 | return None 75 | 76 | @staticmethod 77 | def get_vad_perms(task, address): 78 | """ Get the VAD permissions """ 79 | for vad in task.VadRoot.traverse(): 80 | if vad.End > address >= vad.Start: 81 | return vad.u.VadFlags.Protection.v() 82 | return None 83 | 84 | def calculate(self): 85 | if not has_yara: 86 | debug.error("Yara must be installed for this plugin") 87 | 88 | addr_space = utils.load_as(self._config) 89 | 90 | if not self.is_valid_profile(addr_space.profile): 91 | debug.error("This command does not support the selected profile.") 92 | 93 | rules = yara.compile(sources=signatures) 94 | 95 | for task in self.filter_tasks(tasks.pslist(addr_space)): 96 | scanner = malfind.VadYaraScanner(task=task, rules=rules) 97 | for hit, address in scanner.scan(): 98 | if self.get_vad_perms(task, address) == 6: # RWX vad 99 | vad_base_addr = self.get_vad_base(task, address) 100 | yield task, vad_base_addr 101 | 102 | def render_text(self, outfd, data): 103 | self.table_header(outfd, [("Name", "20"), 104 | ("PID", "8"), 105 | ("Data VA", "[addrpad]")]) 106 | found = [] 107 | for task, start in data: 108 | if (task, start) not in found: 109 | self.table_row(outfd, task.ImageFileName, task.UniqueProcessId, start) 110 | found.append((task, start)) 111 | 112 | 113 | class PlugXConfig(PlugXScan): 114 | """Locate and parse the PlugX configuration""" 115 | 116 | persistence = defaultdict(lambda: "Unknown", {0: "Service + Run Key", 1: "Service", 2: "Run key", 3: "None"}) 117 | 118 | reg_hives = { 119 | 0x80000002 : 'HKLM', 120 | 0x80000001 : 'HKCU', 121 | 0x80000000 : 'HKCR', 122 | 0x80000003 : 'HKU', 123 | 0x80000004 : 'HKPD', 124 | 0x80000050 : 'HKPT', 125 | 0x80000060 : 'HKPN', 126 | 0x80000005 : 'HKCC', 127 | 0x80000006 : 'HKDD', 128 | } 129 | 130 | @staticmethod 131 | def get_vad_end(task, address): 132 | """ Get the VAD end address """ 133 | for vad in task.VadRoot.traverse(): 134 | if address == vad.Start: 135 | return vad.End+1 136 | # This should never really happen 137 | return None 138 | 139 | @staticmethod 140 | def get_str_utf16le(buff): 141 | tstrend = buff.find("\x00\x00") 142 | tstr = buff[:tstrend + (tstrend & 1)] 143 | return tstr.decode('utf_16le') 144 | 145 | @staticmethod 146 | def get_proto(proto): 147 | ret = [] 148 | if proto & 0x1: 149 | ret.append("TCP") 150 | if proto & 0x2: 151 | ret.append("HTTP") 152 | if proto & 0x4: 153 | ret.append("UDP") 154 | if proto & 0x8: 155 | ret.append("ICMP") 156 | if proto & 0x10: 157 | ret.append("DNS") 158 | if proto > 0x1f: 159 | ret.append("OTHER_UNKNOWN") 160 | return ' / '.join(ret) 161 | 162 | def parse_config(self, cfg_blob, cfg_sz, outfd): 163 | if cfg_sz in (0xbe4, 0x150c, 0x1510, 0x1b18, 0x1d18, 0x2540, 0x2a18, 0x2a20): 164 | cfg_blob = cfg_blob[12:] if cfg_sz == 0x1510 else cfg_blob[8:] 165 | 166 | # Flags 167 | desc = " 0 and str(url) != "HTTP://": 233 | outfd.write("\tURL %d: %s\n" % ((k+1), str(url))) 234 | 235 | # Proxies 236 | for k in xrange(4): 237 | ptype, port, proxy, user, passwd = unpack_from('<2H64s64s64s', cfg_blob) 238 | cfg_blob = cfg_blob[calcsize('<2H64s64s64s'):] 239 | if proxy[0] != '\x00': 240 | outfd.write("\tProxy: %s:%d\n" % (proxy.split('\x00')[0], port)) 241 | if user[0] != '\x00': 242 | outfd.write("\tProxy credentials: %s / %s\n" % (user, passwd)) 243 | 244 | str_sz = 0x80 if cfg_sz == 0xbe4 else 0x200 245 | 246 | # Persistence 247 | if cfg_sz in (0x1b18, 0x1d18, 0x2540): 248 | persistence_type = unpack_from(' 1: 348 | a = {} 349 | for y,z in x._fields_: 350 | if getattr(x,y): 351 | outfd.write("\t{} {}: {}\n".format(f.replace('_',' ').title(),y.replace('_',' ').title(),y)) 352 | else: 353 | if str(x): 354 | outfd.write('\t{}: {}\n'.format(f.replace('_',' ').title(),x)) 355 | elif f == 'persistence': 356 | outfd.write('\t{}: {}\n'.format(f.replace('_',' ').title(),self.persistence[v])) 357 | elif f == 'reg_hive': 358 | outfd.write('\t{}: {}\n'.format(f.replace('_',' ').title(),self.reg_hives.get(v,"Unknown"))) 359 | elif 'end_scan' in f or 'start_scan' in f: 360 | outfd.write('\t{}: {}\n'.format(f.replace('_',' ').title(),inet_ntoa(pack(" 0: 382 | offset -= 1 383 | if data[offset] != "\x68": 384 | continue 385 | 386 | # Now we're at: 387 | # push 0xxxxxx <- config address 388 | # call 0xxxxxx 389 | (config_addr, ) = unpack_from("=I", data, offset + 1) 390 | 391 | # Find previous push imm 392 | offset -= 1 393 | while not data[offset] == "\x68": 394 | offset -= 1 395 | if data[offset] != "\x68": 396 | continue 397 | 398 | (config_size, ) = unpack_from("=I", data, offset + 1) 399 | 400 | config_addr -= start 401 | config_blob = data[config_addr:config_addr+config_size] 402 | outfd.write("Process: %s (%d)\n\n" % (task.ImageFileName, task.UniqueProcessId)) 403 | outfd.write("PlugX Config (0x%04x bytes):\n" % config_size) 404 | self.parse_config(config_blob, config_size, outfd) 405 | -------------------------------------------------------------------------------- /plugx_structures.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | import re 3 | 4 | class PlugXCnC(Structure): 5 | _fields_ = [("proto",c_ushort), 6 | ("port",c_ushort), 7 | ("host",c_char*64), 8 | ] 9 | 10 | class PlugXHTTP(Structure): 11 | _fields_ = [("url",c_ubyte*128),] 12 | 13 | def __str__(self): 14 | return re.sub(r"(\x00)+$","","".join([chr(x) for x in self.url])) 15 | 16 | class PlugXProxy(Structure): 17 | _fields_ = [("type",c_ushort), 18 | ("port",c_short), 19 | ("host",c_char*64), 20 | ("user",c_char*64), 21 | ("passwd",c_char*64), 22 | ] 23 | def __str__(self): 24 | return "{}:{} ({} / {})".format(self.host,self.port,self.user,self.passwd) 25 | 26 | class PlugXStr(Structure): 27 | _fields_ = [("string",c_ubyte*512)] 28 | 29 | def __str__(self): 30 | return re.sub(r"(\x00\x00)+$","".join([chr(x) for x in self.string])).decode("utf-16") 31 | 32 | class PlugXP2PConfig(Structure): 33 | _fields_ = [("unused",c_ubyte*20), 34 | ("hide_dll",c_int32), 35 | ("keylogger",c_int32), 36 | ("unused2",c_ubyte*12), 37 | ("sleep1",c_uint32), 38 | ("sleep2",c_uint32), 39 | ("net_access",c_ubyte*672), 40 | ("dns",c_uint32*4), 41 | ("cnc",PlugXCnC*16), 42 | ("http",PlugXHTTP*16), 43 | ("proxy",PlugXProxy*4), 44 | ("persistence",c_int32), 45 | ("install_folder",PlugXStr), 46 | ("service_name",PlugXStr), 47 | ("service_display_name",PlugXStr), 48 | ("service_desc",PlugXStr), 49 | ("reg_hive",c_uint32), 50 | ("reg_key",PlugXStr), 51 | ("reg_value",PlugXStr), 52 | ("injection",c_int32), 53 | ("inject_process",PlugXStr*4), 54 | ("uac_bypass_injection",c_int32), 55 | ("uac_bypass_inject",PlugXStr*4), 56 | ("plugx_auth_str",PlugXStr), 57 | ("cnc_auth_str",PlugXStr), 58 | ("mutex",PlugXStr), 59 | ("screenshots",c_uint32), 60 | ('screenshots_sec',c_uint32), 61 | ('screenshots_zoom',c_uint32), 62 | ('screenshots_bits',c_uint32), 63 | ('screenshots_qual',c_uint32), 64 | ('screenshots_keep',c_uint32), 65 | ("screenshot_folder",PlugXStr), 66 | ("enable_tcp_p2p",c_int32), 67 | ("tcp_p2p_port",c_uint32), 68 | ("enable_udp_p2p",c_int32), 69 | ("udp_p2p_port",c_uint32), 70 | ("enable_icmp_p2p",c_uint32), 71 | ("icmp_p2p_port",c_uint32), 72 | ("enable_ipproto_p2p",c_int32), 73 | ("ipproto_p2p_port",c_int32), 74 | ("enable_p2p_scan",c_int32), 75 | ("p2p_start_scan1",c_uint32), 76 | ("p2p_start_scan2",c_uint32), 77 | ("p2p_start_scan3",c_uint32), 78 | ("p2p_start_scan4",c_uint32), 79 | ("p2p_end_scan1",c_uint32), 80 | ("p2p_end_scan2",c_uint32), 81 | ("p2p_end_scan3",c_uint32), 82 | ("p2p_end_scan4",c_uint32), 83 | ("mac_disable",c_ubyte*6), 84 | ("unused3",c_ubyte*2), 85 | ] 86 | --------------------------------------------------------------------------------