├── README.md ├── athenahttp_decode.py ├── find_byte_strings.py ├── find_interesting_xor.py └── find_switch_jumps.py /README.md: -------------------------------------------------------------------------------- 1 | This repository will contain scripts that were created by the ASERT team to aid in reverse engineering malware 2 | -------------------------------------------------------------------------------- /athenahttp_decode.py: -------------------------------------------------------------------------------- 1 | # Author: Jason Jones, Arbor Networks ASERT 2 | ######################################################################## 3 | # Copyright 2013 Arbor Networks 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ######################################################################## 19 | 20 | import urllib 21 | import string 22 | import base64 23 | import sys 24 | from urlparse import parse_qs 25 | 26 | """ 27 | Example usage and output: 28 | 29 | python athenahttp_decode.py "a=%59%32%70%77%64%32%64%75%64%47%46%6F%64%57%4A%76%64%6D%6C%6B%63%57%74%34%5A%58%4A%35%62%48%4E%6D%62%58%6F%36%5A%32%31%30%59%57%35%31%61%47%39%69%61%58%5A%77%5A%6E%70%6A%64%32%70%78%5A%47%74%34%5A%58%4A%35%62%48%4D%3D&b=yHR5gGU6v25yZXbeY3q1oWQ6OGY2NWJbNDBlZlRbNsEqMWUqMWE5MWNjODA2ZDYqNsI2OTZlyHBxoXY6YWRhoW58YXJmoDt4ODZ8Z2ViZDtjZXNkcG9ayGNfglVsOmF8v3M6V19YUHq2ZXI6cmEiMC44yG5ecDp0LmB8vlV3OmF8&c=%70%76%63%6A%70%77%64%6A%71%78%64%6B%72%78%65%6C%72%79%66%6C%73%7A%66%6D" cHZjanB3ZGpxeGRrcnhlbHJ5ZmxzemZtZjcrcWRHVuejvUZsUFRkc2ZBPT0KZjbSoGMxcHBoRDAsZjcOclJXMWbzvVE5SVcKclRHcHBzR3c1YsNSoGNiUmnK 30 | 31 | Substituion Tables: cjpwgntahubovidqkxerylsfmz:gmtanuhobivpfzcwjqdkxeryls 32 | Decoded Phone-Home: |type:on_exec|uid:8f65ba40ffda7111e11a91cd806d6172696f|priv:admin|arch:x86|gend:desktop|cores:1|os:W_XP|ver:v1.0.8|net:4.0|new:1| 33 | Response Data Marker: cHZjanB3ZGpxeGRrcnhlbHJ5ZmxzemZt 34 | Received Command : |interval=90| 35 | Received Command : |taskid=7|command=!botkill.start| 36 | """ 37 | 38 | def decode_athena(pdata,response): 39 | pdata = parse_qs(pdata) 40 | set_abc = set(['a','b','c']) 41 | if set(pdata.keys()).intersection(set_abc) != set_abc: 42 | print "POST Data does not contain a,b,c values, exiting." 43 | return 44 | KEY_strtr=base64.b64decode(urllib.unquote(pdata['a'][0])).split(':') 45 | phone_home = urllib.unquote(pdata['b'][0]) 46 | phone_home_decoded = base64.b64decode(phone_home.translate(string.maketrans(KEY_strtr[1],KEY_strtr[0]))) 47 | OUTDATA_marker = base64.b64encode(urllib.unquote(pdata['c'][0])) 48 | print "Substituion Tables: %s:%s" % tuple(KEY_strtr) 49 | print "Decoded Phone-Home: %s" % phone_home_decoded 50 | print "Response Data Marker: %s" % OUTDATA_marker 51 | response_idx = response.find(OUTDATA_marker) 52 | if response_idx != -1: 53 | command = response[response_idx+len(OUTDATA_marker):] 54 | command = command.translate(string.maketrans(KEY_strtr[1],KEY_strtr[0])) 55 | command = base64.b64decode(command) 56 | for cmd in command.split('\n'): 57 | if cmd != "": 58 | print "Received Command : %s" % base64.b64decode(cmd) 59 | 60 | if __name__ == "__main__": 61 | if len(sys.argv) != 3: 62 | print "Usage: %s " 63 | else: 64 | decode_athena(sys.argv[1],sys.argv[2]) 65 | -------------------------------------------------------------------------------- /find_byte_strings.py: -------------------------------------------------------------------------------- 1 | # Author: Jason Jones, Arbor Networks ASERT 2 | ######################################################################## 3 | # Copyright 2013 Arbor Networks 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ######################################################################## 19 | 20 | from idaapi import * 21 | from idc import * 22 | from idautils import * 23 | import ctypes 24 | from PySide import QtGui, QtCore 25 | 26 | 27 | class ByteStringsViewer_t(PluginForm): 28 | def Show(self): 29 | return PluginForm.Show(self,"Byte Strings",options = PluginForm.FORM_PERSIST) 30 | 31 | def OnCreate(self,form): 32 | self.parent = self.FormToPySideWidget(form) 33 | self.byte_strings = {} 34 | self.table = QtGui.QTableWidget() 35 | self.table.setRowCount(1) 36 | self.table.setColumnCount(3) 37 | self.table.setHorizontalHeaderLabels(("Address","Function","String")) 38 | layout = QtGui.QVBoxLayout() 39 | layout.addWidget(self.table) 40 | self.clipboard = QtGui.QClipboard() 41 | self.Create() 42 | self.parent.setLayout(layout) 43 | def OnClose(self,form): 44 | global ByteStringForm 45 | del ByteStringForm 46 | print "Closed" 47 | 48 | def click_row(self): 49 | i = self.table.item(self.table.currentRow(),0) 50 | bstr = self.table.item(self.table.currentRow(),2) 51 | print self.table.currentRow() 52 | addr = i.text().strip() 53 | bstr = bstr.text() 54 | print bstr 55 | print addr 56 | if not addr.startswith("0x"): 57 | addr = get_name_ea(BADADDR,str(addr)) 58 | else: 59 | addr = addr[2:10] 60 | addr= int(addr,16) 61 | Jump(addr) 62 | self.clipboard.setText(bstr) 63 | return 64 | 65 | def Create(self): 66 | title = "Byte Strings" 67 | self.table.clear() 68 | self.table.setColumnCount(3) 69 | self.table.setHorizontalHeaderLabels(("Address","Function","String")) 70 | self.table.itemClicked.connect(self.click_row) 71 | self.find_byte_strings() 72 | self.table.setRowCount(len(self.byte_strings.keys())) 73 | row = 0 74 | for addr,bstr in self.byte_strings.items(): 75 | self.table.setItem(row,0,QtGui.QTableWidgetItem(addr)) 76 | self.table.setItem(row,1,QtGui.QTableWidgetItem(get_func_name(int(addr[2:],16)))) 77 | self.table.setItem(row,2,QtGui.QTableWidgetItem(bstr)) 78 | self.table.resizeRowToContents(row) 79 | row += 1 80 | self.table.setSortingEnabled(True) 81 | 82 | 83 | 84 | def find_byte_strings(self): 85 | #chrs = {} 86 | for f in Functions(): 87 | func = get_func(f) 88 | chr_vals = {} 89 | eightbit = {} 90 | for head in Heads(func.startEA,func.endEA): 91 | if GetMnem(head) == "mov": 92 | if re.match('[abcd]l',GetOpnd(head,0)) and GetOpType(head,1) == o_imm and ((GetOperandValue(head,1) >= 0x20 and GetOperandValue(head,1) <= 0x7f) or GetOperandValue(head,1) in [0xd,0xa]): 93 | eightbit[GetOpnd(head,0)] = GetOperandValue(head,1) 94 | if (GetOpnd(head,0).startswith('byte ptr') or GetOpnd(head,0).startswith('[e')) and GetOpType(head,1) == o_imm and ((GetOperandValue(head,1) >= 0x20 and GetOperandValue(head,1) <= 0x7f) or GetOperandValue(head,1) in [0xd,0xa]): 95 | reg = GetOpnd(head,0) 96 | reg = reg[reg.find('['):] 97 | if reg.count('+') == 0: offset = 0 98 | else: 99 | ops = reg.split('+') 100 | reg = reg[:reg.find('+')]+']' 101 | offset = ctypes.c_int32(GetOperandValue(head,0)).value 102 | reg_base=0 103 | if len(ops) > 2 and ops[1].endswith('h'): 104 | reg_base = int(ops[1][:-1],16) 105 | offset = offset-reg_base 106 | if reg not in chr_vals: chr_vals[reg] = {} 107 | chr_vals[reg][offset] = (head,chr(GetOperandValue(head,1))) 108 | elif (GetOpnd(head,0).startswith('byte ptr') or GetOpnd(head,0).startswith('[e')) and GetOpType(head,1) == o_reg and GetOpnd(head,1) in eightbit: 109 | reg = GetOpnd(head,0) 110 | reg = reg[reg.find('['):] 111 | if reg.count('+') == 0: offset = 0 112 | else: 113 | ops = reg.split('+') 114 | reg = reg[:reg.find('+')]+']' 115 | offset = ctypes.c_int32(GetOperandValue(head,0)).value 116 | reg_base=0 117 | if len(ops) > 2 and ops[1].endswith('h'): 118 | reg_base = int(ops[1][:-1],16) 119 | offset = offset-reg_base 120 | 121 | if reg not in chr_vals: chr_vals[reg] = {} 122 | chr_vals[reg][offset] = (head,chr(eightbit[GetOpnd(head,1)])) 123 | for reg,c_v in chr_vals.items(): 124 | keys = c_v.keys() 125 | keys.sort() 126 | last = None 127 | s = "" 128 | offset = 0 129 | for o in keys: 130 | if last is None: 131 | addr = c_v[o][0] 132 | offset = o 133 | s = c_v[o][1] 134 | elif last + 1 == o and c_v[o] != '\x00': 135 | s += c_v[o][1] 136 | else: 137 | if s != "" and len(s) > 3: 138 | self.byte_strings["0x%X" % addr] = s 139 | func = get_func(addr) 140 | if offset > 0: 141 | s = "" 142 | continue 143 | 144 | s = c_v[o][1] 145 | offset = o 146 | addr = c_v[o][0] 147 | last = o 148 | if s != "" and len(s) > 1: 149 | self.byte_strings["0x%X" % addr] = s 150 | func = get_func(addr) 151 | 152 | def find_all_byte_strings(): 153 | global ByteStringForm 154 | ByteStringForm = ByteStringsViewer_t() 155 | ByteStringForm.Show() 156 | ByteStringForm.table.resizeRowsToContents() 157 | ByteStringForm.table.resizeColumnsToContents() 158 | 159 | 160 | find_all_byte_strings() 161 | -------------------------------------------------------------------------------- /find_interesting_xor.py: -------------------------------------------------------------------------------- 1 | # Author: Jason Jones, Arbor Networks ASERT 2 | ######################################################################## 3 | # Copyright 2013 Arbor Networks 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ######################################################################## 19 | 20 | from idaapi import * 21 | from idautils import * 22 | from idc import * 23 | def find_interesting_xor(): 24 | for f in Functions(): 25 | func = get_func(f) 26 | heads=Heads(func.startEA,func.endEA) 27 | xors = [] 28 | for head in heads: 29 | if GetMnem(head).startswith('xor'): 30 | m = GetDisasm(head) 31 | arg1 = GetOpnd(head,0) 32 | arg2 = GetOpnd(head,1) 33 | if arg1 != arg2 and "0FFFFFFFFh" not in [arg1,arg2]: 34 | print "Interesting in %s XOR %s %s @ 0x%X" % (get_func_name(f),arg1,arg2,head) 35 | xors.append(head) 36 | elif GetMnem(head).startswith('j') and xors!= []: 37 | arg1 = GetOpnd(head,0) 38 | arg1 = get_name_ea(BADADDR,arg1) 39 | for addr in xors: 40 | if addr > arg1: 41 | print "Interesting XOR in a loop %s @ %X: %s" % (get_func_name(addr),addr,GetDisasm(addr)) 42 | 43 | if __name__ == "__main__": 44 | find_interesting_xor() 45 | -------------------------------------------------------------------------------- /find_switch_jumps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Switch jump viewer 3 | original code by Aaron Portnoy / Zef Cekaj, Exodus Intelligence 4 | Updated and modified by Jason Jones, Arbor Networks ASERT 5 | """ 6 | import idaapi 7 | from idaapi import * 8 | from idc import * 9 | from idautils import * 10 | from PySide import QtGui, QtCore 11 | 12 | 13 | class SwitchViewer_t(PluginForm): 14 | 15 | #def __init__(self, data): 16 | # # data should be a 3-tuple 17 | # # 18 | # # (address, number of cases, list of interesting calls) 19 | # self.switches = data 20 | 21 | def Show(self): 22 | return PluginForm.Show(self,"Switch Jump Viewer",options = PluginForm.FORM_PERSIST) 23 | 24 | def OnCreate(self, form): 25 | """ 26 | Called when the plugin form is created 27 | """ 28 | # Get parent widget 29 | self.parent = self.FormToPySideWidget(form) 30 | self.calls = {} 31 | 32 | # Create tree control 33 | self.tree = QtGui.QTreeWidget() 34 | self.tree.setHeaderLabels(("Names","# Cases")) 35 | self.tree.setColumnWidth(0, 100) 36 | 37 | # Create layout 38 | layout = QtGui.QVBoxLayout() 39 | layout.addWidget(self.tree) 40 | 41 | self.Create() 42 | self.parent.setLayout(layout) 43 | 44 | def OnClose(self,form): 45 | global SwitchForm 46 | del SwitchForm 47 | print "Closed" 48 | 49 | def click_tree(self): 50 | i = self.tree.currentItem() 51 | addr = i.text(0).strip() 52 | print addr 53 | if not addr.startswith("0x"): 54 | addr = get_name_ea(BADADDR,str(addr)) 55 | else: 56 | addr = addr[2:10] 57 | addr= int(addr,16) 58 | Jump(addr) 59 | return 60 | 61 | def Create(self): 62 | title = "Switches" 63 | #root = QtGui.QTreeWidgetItem(self, title) 64 | comment = COLSTR("; Double-click to follow", SCOLOR_BINPREF) 65 | #self.AddLine(comment) 66 | comment = COLSTR("; Hover for preview", SCOLOR_BINPREF) 67 | #self.AddLine(comment) 68 | self.tree.clear() 69 | self.tree.setColumnCount(2) 70 | self.tree.clicked.connect(self.click_tree) 71 | for func in sorted(self.switches.keys()): 72 | func_node = QtGui.QTreeWidgetItem(self.tree) 73 | func_node.setText(0,func) 74 | func_node.setText(1,"") 75 | for item in self.switches[func]: 76 | node = QtGui.QTreeWidgetItem(func_node) 77 | addr = item[0] 78 | cases = item[1] 79 | address_element = "0x%08x" % addr 80 | node.setText(0,address_element) 81 | node.setText(1,"%04s" % cases) 82 | #line = address_element + value_element 83 | self.calls[addr] = item[2] 84 | for c in item[2]: 85 | cnode = QtGui.QTreeWidgetItem(node) 86 | cnode.setText(0,c[0]) 87 | cnode.setText(1,c[2]) 88 | 89 | #self.AddLine(line) 90 | 91 | return True 92 | 93 | 94 | def OnDblClick(self, shift): 95 | line = self.GetCurrentLine() 96 | 97 | # skip the lines where we say "hover for preview" and so on 98 | if "0x" not in line: return False 99 | 100 | # skip COLSTR formatting bytes, find address 101 | start = line.find("0x") 102 | addr = int(line[2:line.find(":")], 16) 103 | 104 | Jump(addr) 105 | return True 106 | 107 | def OnHint(self, lineno): 108 | # skip the lines where we say "hover for preview" and so on 109 | if lineno < 2: return False 110 | else: lineno -= 2 111 | 112 | line = self.GetCurrentLine() 113 | 114 | if "0x" not in line: return False 115 | 116 | # skip COLSTR formatting bytes, find address 117 | start = line.find("0x") 118 | addr = int(line[2:line.find(":")], 16) 119 | 120 | calls = self.calls[addr] 121 | 122 | res = "" 123 | for line in calls: 124 | res += COLSTR(line + "\n", SCOLOR_DREF) 125 | 126 | return (1, res) 127 | 128 | def look_for_interesting(addr): 129 | #interesting = ["sscanf", "sprintf", "exec", "run", "strcpy"] 130 | f = get_func(addr) 131 | interesting_calls = [] 132 | if f is None: 133 | return 134 | for h in Heads(f.startEA,f.endEA): 135 | if GetMnem(h).find("call") != -1: 136 | interesting_calls.append("0x%08x : %s" % (h,GetDisasm(h))) 137 | #print "0x%08x: %s" % (h,GetDisasm(h)) 138 | break 139 | return interesting_calls 140 | 141 | def get_jlocs(sw): 142 | jlocs = [] 143 | ncases = sw.ncases if sw.jcases == 0 else sw.jcases 144 | for i in range(ncases): 145 | addr = Dword(sw.jumps+i*4) 146 | name = get_name(BADADDR,addr) 147 | comm = GetCommentEx(LocByName(name),1) 148 | comm = comm[comm.find('case'):] if comm is not None and comm.startswith('jumptable') else comm 149 | jlocs.append((name,LocByName(name),comm)) 150 | return jlocs 151 | 152 | def get_switch_jumps(ea): 153 | func = get_func(ea) 154 | data = [] 155 | heads=Heads(func.startEA,func.endEA) 156 | for head in heads: 157 | ic = [] 158 | 159 | if (hex(Word(head)).lower() == "0x24ff" or hex(Word(head)).lower() == "24ff") and GetMnem(head).find("jmp") != -1: 160 | if get_switch_info_ex(head) is None: continue 161 | sw = get_switch_info_ex(head) 162 | print "[*] Switch at 0x%08x has %s jtable elements" % (head,sw.get_jtable_size()) 163 | ic.extend(get_jlocs(sw)) 164 | """ 165 | # useful for xrefing searching, not used by me 166 | refs = list(CodeRefsFrom(head,0)) 167 | for ref in refs: 168 | #print "-- Reference 0x%08x" % ref 169 | h = NextHead(ref) 170 | while h != BADADDR: 171 | if GetMnem(h).find("call") != -1: 172 | #print "---- Call to 0x%08x" % GetOperandValue(h,0) 173 | callrefs = list(CodeRefsFrom(h,0)) 174 | for cr in callrefs: 175 | if GetFunctionFlags(cr) == -1: 176 | continue 177 | ic.extend(get_jlocs( 178 | break 179 | elif GetMnem(h).startswith("j"): 180 | break 181 | h = NextHead(h) 182 | """ 183 | data.append((head,sw.ncases,ic)) 184 | return data 185 | 186 | def find_all_switch_jumps(): 187 | global SwitchForm 188 | funcs = Functions() 189 | data = {} 190 | for func in Functions(): 191 | d = get_switch_jumps(func) 192 | if d!= []: 193 | data[get_func_name(func)] = d 194 | SwitchForm = SwitchViewer_t() 195 | SwitchForm.switches = data 196 | SwitchForm.Show() 197 | 198 | find_all_switch_jumps() 199 | --------------------------------------------------------------------------------