├── README.md └── ge_d20mx ├── pyd20mx.py ├── validate_image.py └── write_to_device.py /README.md: -------------------------------------------------------------------------------- 1 | # ics_mem_collect 2 | 3 | For many industrial control system devices, there is not a simple solution for programmatically accessing memory. Without an API, an incident responder or digital forensics analyst may be required to manually probe memory looking for anomalies or malicious activity. This project is intended to develop APIs that allow an analyst to adapt pre-existing tools or rapidly build new tools in order to target these devices. 4 | 5 | Current Devices: 6 | * GE D20MX 7 | 8 | Future Work: 9 | * JTAG Interface 10 | -------------------------------------------------------------------------------- /ge_d20mx/pyd20mx.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import collections 3 | import serial 4 | import socket 5 | import struct 6 | import time 7 | import re 8 | 9 | page_size = 0x1000 10 | MEM_SIZE = 0x40000000 11 | 12 | 13 | def page_align(addr): 14 | return addr & 0xFFFFF000 15 | 16 | re_prompt = re.compile("^D20M>", re.MULTILINE) 17 | 18 | class fd_d20mx_cache(object): 19 | def __init__(self): 20 | self.f_cache = open("d20mx.cache", "r+b", 0) 21 | self.f_cache.seek(0) 22 | self.f_cache.truncate(MEM_SIZE) 23 | self.f_status = open("d20mx.cache.status", "r+b", 0) 24 | self.f_status.seek(0) 25 | self.f_status.truncate(MEM_SIZE / page_size) 26 | 27 | def _page_num(self, addr): 28 | return page_align(addr) / page_size 29 | 30 | def set_page_cached(self, addr, status): 31 | if page_align(addr) != addr: 32 | print "page unaligned addr" 33 | return False 34 | if status not in [True, False]: 35 | print "status error, must be true or false" 36 | return False 37 | else: 38 | # print "writing status for %08x to %s" % (addr, status) 39 | self.f_status.seek(self._page_num(addr)) 40 | b = struct.pack("B", 1 if status == True else 0) 41 | self.f_status.write(b) 42 | self.f_status.flush() 43 | return True 44 | 45 | def is_page_cached(self, addr): 46 | self.f_status.seek(self._page_num(addr)) 47 | # print "Checking page %08x located at %08x" % (self.f_status.tell(), addr) 48 | data = self.f_status.read(1) 49 | status = True if data == '\x01' else False 50 | return status 51 | 52 | def cache_page(self, addr, bytez): 53 | if len(bytez) != page_size: 54 | print "Not enough bytez, expected 0x%08x got 0x%08x" % (page_size, len(bytez)) 55 | return 56 | if page_align(addr) != addr: 57 | print "addr was not page aligned" 58 | return 59 | tell = self.tell() 60 | self.seek(addr) 61 | self.f_cache.write(bytez) 62 | self.seek(tell) 63 | self.set_page_cached(addr, True) 64 | return True 65 | 66 | def read(self, n=-1): 67 | if self.is_page_cached(page_align(self.f_cache.tell())): 68 | return self.f_cache.read(n) 69 | else: 70 | print "ATTEMPT TO READ UNPAGED MEM" 71 | 72 | def write(self, b): 73 | self.f_cache.write(b) 74 | 75 | def seek(self, offset, whence=0): 76 | return self.f_cache.seek(offset, whence) 77 | 78 | def tell(self): 79 | return self.f_cache.tell() 80 | 81 | 82 | class fd_d20mx_transport(object): 83 | def __init__(self): 84 | self.mem = fd_d20mx_cache() 85 | 86 | def seek(self, offset, whence=0): 87 | self.mem.seek(offset, whence) 88 | 89 | def tell(self): 90 | return self.mem.tell() 91 | 92 | def read(self, length): 93 | pages = [x for x in range(page_align(self.tell()), self.tell() + length, page_size)] 94 | for page in pages: 95 | if self.mem.is_page_cached(page): 96 | pass 97 | #print "[!] [%08x] PAGE CACHED" % page_align(self.tell()) 98 | else: 99 | #print "[X] [%08x} PAGE NOT CACHED" % page_align(self.tell()) 100 | self._cache_page(page) 101 | data = self.mem.read(length) 102 | return data 103 | def write(self, data): 104 | self.mem.write(data) 105 | 106 | class fd_d20mx_serial(fd_d20mx_transport): 107 | def __init__(self, **parameters): 108 | super(fd_d20mx_serial, self).__init__() 109 | parameters['port'] = None # "/dev/ttyUSB0" 110 | parameters['baudrate'] = 115200 111 | parameters['xonxoff'] = True 112 | 113 | # Initalize the serial device 114 | self.ser = serial.Serial(**parameters) 115 | self.ser.write("\r\n") 116 | time.sleep(1) 117 | prompt = self.ser.read(self.ser.in_waiting) 118 | print prompt 119 | if "D20M>" not in prompt: 120 | raise Exception("Invalid prompt") 121 | # Create cache 122 | 123 | def _cache_page(self, addr): 124 | self.ser.write("d %08x %08x\r\n" % (page_align(self.mem.tell()), page_align(self.mem.tell()) + page_size)) 125 | self.ser.read_until("\r\n") 126 | udata = self.ser.read_until("D20M>") 127 | bdata = '' 128 | print udata 129 | if "data access" in udata: 130 | print "GUARD PAGE" 131 | else: 132 | for line in udata.splitlines(): 133 | if line.startswith("value"): continue 134 | if line.startswith("D20M>"): continue 135 | if line == "": continue 136 | bdata += line[9:9 + 2 + 3 * 16].replace(" ", "") 137 | bytez = binascii.unhexlify(bdata) 138 | self.mem.cache_page(addr, bytez) 139 | # print "CACHING PAGE" 140 | 141 | 142 | class fd_d20mx_tcp(fd_d20mx_transport): 143 | def __init__(self, s=None): 144 | super(fd_d20mx_tcp, self).__init__() 145 | self.s = s 146 | if not s: 147 | self.s = socket.socket() 148 | self.s.settimeout(60) 149 | if not s: 150 | self.s.connect(("127.0.0.1", 4444)) 151 | print "Connected to loopback:4444" 152 | self.s.send("\r\n") 153 | time.sleep(1) 154 | data = '' 155 | for i in range(10): 156 | self.s.send("\r\n") 157 | try: 158 | data += self.s.recv(1000) 159 | except socket.timeout as e: 160 | print e.message 161 | if "D20M>" in data: 162 | break 163 | else: 164 | raise Exception("Invalid prompt") 165 | 166 | self.mem = fd_d20mx_cache() 167 | 168 | def _cache_page(self, addr): 169 | print "[X] [%08x] Caching page" % page_align(addr) 170 | #print "d %08x %08x\r\n" % (page_align(self.mem.tell()), page_align(self.mem.tell()) + page_size) 171 | self.s.send("d %08x %08x" % (page_align(self.mem.tell()), page_align(self.mem.tell()) + page_size)) 172 | time.sleep(1) 173 | self.s.recv(1000) 174 | self.s.send("\r\n") 175 | 176 | udata = '' 177 | #udata = self.s.recv(1000) 178 | #udata = udata[udata.index("\r\n"):] 179 | while True: 180 | udata += self.s.recv(1000) 181 | if re_prompt.search(udata): 182 | #udata = udata[:udata.index("D20M>")] 183 | break 184 | bdata = '' 185 | if "data access" in udata: 186 | print "GUARD PAGE" 187 | else: 188 | for line in udata.splitlines(): 189 | if line.startswith("value"): continue 190 | if line.startswith("d "): continue 191 | if line.startswith("D20M>"): continue 192 | if line == "": continue 193 | if len(bdata) % 2: break 194 | match = re.match("[0-9A-F]{8} (([0-9a-f]{2} ( )?){16})", line) 195 | if match: 196 | print "GROUP", match.group(1) 197 | bdata += line[9:9 + 2 + 3 * 16].replace(" ", "") 198 | bytez = binascii.unhexlify(bdata) 199 | self.mem.cache_page(addr, bytez) 200 | def _write(self, data): 201 | print "[X] [%08x] Edit mem" % addr 202 | #print "d %08x %08x\r\n" % (page_align(self.mem.tell()), page_align(self.mem.tell()) + page_size) 203 | #self.s.send("eds\r\n") 204 | #self.s.send("c\r\n") 205 | #self.s.recv(1000) 206 | #self.s.send("f %08x,1" % (page_align(self.mem.tell()), page_align(self.mem.tell()) + page_size)) 207 | #self.s.send("\r\n") 208 | #send_data = '\r\n'.join(["%X" % x for x in data]) 209 | #print send_data 210 | #print "." 211 | #self.s.send(".") 212 | #self.s.send("quit") 213 | #self.s.send("3") 214 | #try: 215 | # while True: 216 | # self.s.recv(1000) 217 | #except: 218 | # print "recvd all" 219 | # pass 220 | # 221 | #self.s.send("\r\n") 222 | #rdata = self.s.recv(1000) 223 | #groups = prompt.match(rdata) 224 | #if groups is None: 225 | # raise Exception("Bad prompt returning from write") 226 | self.mem.write(data) 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /ge_d20mx/validate_image.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | import optparse 3 | from elftools import elf 4 | import binascii 5 | from collections import OrderedDict 6 | 7 | import archinfo 8 | import cle 9 | import pyvex 10 | import pyd20mx 11 | 12 | symbols_whitelist = [ 13 | "standTbl", 14 | "statTbl", 15 | #"__EH_FRAME_BEGIN__" 16 | ] 17 | 18 | def fd_peek(fd, length): 19 | pos = fd.tell() 20 | data = fd.read(length) 21 | fd.seek(pos) 22 | return data 23 | 24 | 25 | def fd_memcmp(fd1, fd2, length, chunk_size = 4): 26 | fd1_old_tell = fd1.tell() 27 | fd2_old_tell = fd2.tell() 28 | cbread = 0 29 | valid = [] 30 | invalid = [] 31 | run_start = (fd1_old_tell, fd2_old_tell) 32 | run_here = run_start 33 | valid_run = True 34 | while cbread < length: 35 | chunk_size = min(chunk_size, length-cbread) 36 | data1 = fd1.read(min(chunk_size, length - cbread)) 37 | data2 = fd2.read(min(chunk_size, length - cbread)) 38 | if (data1 == data2) == valid_run and cbread < length-4: 39 | pass 40 | else: 41 | if run_here[0] - run_start[0]: 42 | #print "Adding run of length %d to %s" % (run_here[0] - run_start[0], valid_run) 43 | if valid_run: 44 | valid.append((run_start, run_here, run_here[0] - run_start[0])) 45 | else: 46 | invalid.append((run_start, run_here, run_here[0] - run_start[0])) 47 | run_start = run_here 48 | valid_run = not valid_run 49 | run_here = (fd1.tell(), fd2.tell()) 50 | cbread += chunk_size 51 | 52 | fd1.seek(fd1_old_tell) 53 | fd2.seek(fd2_old_tell) 54 | return valid, invalid 55 | 56 | def prev_symbol(ld, addr): 57 | prev = max([x for x in ld.symbols_by_addr.keys() if x <= addr]) 58 | return ld.symbols_by_addr[prev] 59 | 60 | 61 | def get_diff_meta(ld_cle, fd_memory_image, memory_offset=None): 62 | diff_meta = {} 63 | fd_image_on_disk = ld_cle.memory 64 | if memory_offset is None: 65 | memory_offset = elf.getBaseAddress() 66 | diff_meta['sections'] = OrderedDict() 67 | mem_results_valid = [] 68 | mem_results_invalid = [] 69 | 70 | for sec in ld_cle.main_bin.sections: 71 | if sec.type == "SHT_NULL": 72 | continue 73 | if not "SHT_PROGBITS" in sec.type: 74 | pass 75 | elif not sec.flags & 0x02 == 0x02: 76 | pass 77 | else: 78 | fd_image_on_disk.seek(sec.min_addr) 79 | fd_memory_image.seek(sec.min_addr) 80 | mem_results_valid, mem_results_invalid = fd_memcmp(fd_image_on_disk, fd_memory_image, sec.memsize) 81 | diff_meta['sections'][sec.name] = {'sec_meta': sec, 82 | 'sec_flags': sec.flags, 83 | 'sec_type': sec.type, 84 | 'mem_results_invalid': mem_results_invalid, 85 | 'mem_results_valid': mem_results_valid} 86 | return diff_meta 87 | 88 | def main(): 89 | parser = optparse.OptionParser() 90 | parser.add_option("--mem_image", dest="mem_image", 91 | help="mem image", metavar="FILE", default=None) 92 | parser.add_option("--mem_offset", dest="mem_offset", 93 | help="newer offset", metavar="offset", default=0, type="int") 94 | parser.add_option("--disk_image", dest="disk_image", 95 | help="disk image", metavar="FILE", default=None) 96 | parser.add_option("--disk_offset", dest="disk_offset", 97 | help="older offset", metavar="FILE", default=0, type="int") 98 | options, args = parser.parse_args() 99 | 100 | if not options.disk_image or not options.mem_image: 101 | parser.print_help() 102 | parser.exit(1) 103 | 104 | fd_disk = open(options.disk_image, "rb") 105 | fd_mem = None 106 | if options.mem_image == "fd_tcp": 107 | fd_mem = pyd20mx.fd_d20mx_tcp() 108 | elif options.mem_image == "fd_serial": 109 | fd_mem = pyd20mx.fd_d20mx_serial() 110 | elif options.mem_image == "fd_cache": 111 | fd_mem = pyd20mx.fd_d20mx_cache() 112 | else: 113 | fd_mem = open(options.mem_image, "rb") 114 | fd_disk.seek(0) 115 | 116 | ld_cle = cle.Loader(options.disk_image) 117 | diff_meta = get_diff_meta(ld_cle, fd_mem, options.mem_offset) 118 | print "Section Name".ljust(20), 119 | print "Address".ljust(20), 120 | print "Size".ljust(20), 121 | print "Status" 122 | for section in diff_meta['sections'].values(): 123 | match_status = '' 124 | if not section['sec_meta'].type == "SHT_PROGBITS": 125 | match_status = "NOT_PROGBITS" 126 | elif not section['sec_meta'].flags & 0x02 == 0x02: 127 | match_status = "NO_ALLOC" 128 | elif section['sec_meta'].memsize == 0: 129 | match_status = "ZERO_SIZE" 130 | elif len(section['mem_results_invalid']): 131 | match_status = "!!! MISMATCH !!!" 132 | else: 133 | match_status = "MATCH" 134 | print section['sec_meta'].name.ljust(20), 135 | print hex(section['sec_meta'].vaddr).ljust(20), 136 | print hex(section['sec_meta'].memsize).ljust(20), 137 | print '[%s]' % match_status 138 | #print section['sec_meta'].sh_flags, section['sec_meta'].sh_type 139 | #print section['mem_results_invalid'] 140 | for mem_result in section['mem_results_invalid']: 141 | fd_disk.seek(mem_result[0][0]) 142 | fd_mem.seek(mem_result[0][1]) 143 | sym = prev_symbol(ld_cle.main_bin, mem_result[0][0]) 144 | if sym.name in symbols_whitelist: 145 | continue 146 | #print "\t", sym.name 147 | #print section['sec_meta'].flags 148 | if section['sec_meta'].flags & elf.constants.SH_FLAGS.SHF_EXECINSTR == elf.constants.SH_FLAGS.SHF_EXECINSTR: 149 | disk_data = fd_peek(fd_disk, mem_result[2]) 150 | mem_data = fd_peek(fd_mem, mem_result[2]) 151 | print "="*81 152 | print "%08x" % sym.rebased_addr, sym.name 153 | cap = ld_cle.main_bin.arch.capstone 154 | d_old = cap.disasm(disk_data, mem_result[0][0]) 155 | d_new = cap.disasm(mem_data, mem_result[0][0]) 156 | print 'DISK'.center(40, '-')+'|'+'MEMORY'.center(40,'-') 157 | for x, y in zip(d_old, d_new): 158 | print("%08x: %s %s" %(x.address, x.mnemonic.ljust(10), x.op_str.ljust(20))).ljust(40), 159 | print("%08x: %s %s" %(y.address, y.mnemonic.ljust(10), y.op_str.ljust(20))), 160 | print 161 | print "="*81 162 | else: 163 | pass 164 | #print "\t\t[DEAD] ", "%08x" % fd_disk.tell(), 165 | #print hexdump.dump(fd_peek(fd_disk, mem_result[2])).ljust(40), 166 | #print "\t\t[LIVE] ", "%08x" % fd_mem.tell(), 167 | #print hexdump.dump(fd_peek(fd_mem, mem_result[2])) 168 | 169 | #print "\t", "[!] matched for %d/0x%08x out of %d/0x%08x bytes at %x" % \ 170 | # (mem_result, mem_result, sec.sh_size, sec.sh_size, sec.sh_offset) 171 | 172 | 173 | if __name__ == "__main__": 174 | main() 175 | -------------------------------------------------------------------------------- /ge_d20mx/write_to_device.py: -------------------------------------------------------------------------------- 1 | import optparse 2 | import pyd20mx 3 | 4 | def main(): 5 | parser = optparse.OptionParser() 6 | 7 | parser.add_option("--data", dest="dat_image", 8 | help="disk image", metavar="FILE") 9 | parser.add_option("--address", dest="address", 10 | help="older offset", metavar="FILE", default=0, type="int") 11 | options, args = parser.parse_args() 12 | 13 | 14 | 15 | 16 | fd_mem = pyd20mx.fd_d20mx_cache() 17 | fd_dat = open(options.dat_image, "rb") 18 | 19 | fd_mem.seek(options.address) 20 | fd_mem.write(fd_dat.read()) 21 | 22 | if __name__ == "__main__": 23 | main() 24 | 25 | 26 | --------------------------------------------------------------------------------