#!/usr/bin/ruby # use this arg to show everything / bruteforce stuff full = ARGV.delete '--full' puts "Read PCAP file" system 'wget', 'https://github.com/jjyg/pcap/raw/master/pcap.rb' unless File.exist?('pcap.rb') require './pcap' pcap = Pcap::Capture.from(File.open('./dump.bin')) puts pcap if full prelude = [] icmp = [] ftpctrl = [] ftpdata = [] until pcap.eof? packet = pcap.readpacket if packet.icmp icmp << packet else case packet.tcp.dport when 1234 prelude << packet when 21 ftpctrl << packet else ftpdata << packet end end end prelude = prelude.map { |pkt| pkt.tcp.pld }.join aes_iv = prelude[/iv.*([a-f0-9]{32})/i, 1] csum = prelude[/checksum.*([a-f0-9]{32})/i, 1] puts prelude if full #ftpctrl = ftpctrl.map { |pkt| pkt.tcp.pld }.join tgz_chiffre_b64 = ftpdata.map { |pkt| pkt.tcp.pld }.join # File.open('sstic.tar.gz-chiffre', 'wb') { |fd| fd.write tgz_chiffre_b64 } tgz_chiffre = tgz_chiffre_b64.unpack('m*').first puts "Search AES key" require 'digest/md5' require 'openssl' # test aes key, return cleartext if csum match def test_aes(cipher, aes_iv, aes_key, crypt, target_csum) aes = OpenSSL::Cipher::Cipher.new(cipher) aes.decrypt aes.iv = [aes_iv].pack('H*') aes.key = [aes_key].pack('H*') #aes.padding = 0 clear = aes.update(crypt) + aes.final #File.open("#{cipher}.#{aes_key}.tgz", 'w') { |fd| fd.write clear } if clear[0, 2] == "\x1f\x8b" if Digest::MD5.hexdigest(clear).downcase == target_csum.downcase clear end rescue end bits_list = [] prev = nil icmp.each { |pkt| if prev time_bit = (pkt.time - prev.time).round - 1 bits_list.last << time_bit #id_bits = (pkt.pld.id - prev.pld.id) & 0xffff #bits_list.last << id_bits end prev = pkt ttl_bits = (pkt.ip.ttl / 10) - 1 tos_bit = pkt.ip.tos >> 2 bits_list << [ttl_bits, tos_bit] } bits_list.pop if full cipher_list = %w[aes-256-cbc aes-256-cfb aes-256-cfb1 aes-256-cfb8 aes-256-ctr aes-256-ecb aes-256-ofb] shiftpermuts = [0, 1, 2].permutation ttlpermuts = [0, 1, 2, 3].permutation else cipher_list = %w[aes-256-cbc] shiftpermuts = [[1, 0, 2]] ttlpermuts = [[1, 3, 2, 0]] end tgz = nil shiftpermuts.each { |shiftpm| # bits[0] (ttl) is 2 bits wide shiftpm.map! { |pm| pm + (pm > shiftpm[0] ? 1 : 0) } ttlpermuts.each { |ttlpm| [0, 1].permutation.each { |tospm| [0, 1].permutation.each { |timepm| aes_key = '' bits_list.each { |ttl, tos, time| key_nibble = 0 shiftpm.zip([ttlpm[ttl], tospm[tos], timepm[time]]).each { |pos, bit| key_nibble |= bit << pos } aes_key << key_nibble.to_s(16) } # swap nibble order in bytes aes_key2 = aes_key.gsub(/../) { |o| o.reverse } cipher_list.each { |cph| [aes_key, aes_key.reverse, aes_key2, aes_key2.reverse].each { |key| if clear = test_aes(cph, aes_iv, key, tgz_chiffre, csum) #p [key, aes_key, shiftpm, ttlpm, tospm, timepm] puts key #File.open("#{cph}.#{key}.tgz", 'w') { |fd| fd.write clear } #aes-256-cbc.dd8cf2d52e69aafb734e3acd0e4a69e83ed93bc4870ecd0d5b6faad86a63ae94.tgz tgz = clear end } } } } } } puts "Extract tgz archive" require 'zlib' require 'stringio' tgz_io = StringIO.new(tgz) tar = (Zlib::GzipReader.new(tgz_io, :encoding => 'binary') rescue Zlib::GzipReader.new(tgz_io)).read tarfiles = {} i = 0 while i < tar.length hdr = tar[i, 512] i += 512 fname = hdr[0, 100] fname = fname[0, fname.index("\0")] fsz = hdr[124, 12].to_i(8) tarfiles[fname] = tar[i, fsz] i += (fsz + 511) / 512 * 512 end tarfiles.delete_if { |k, v| v.length == 0 } p tarfiles.map { |k, v| [k, v.length] } if full bytecode = tarfiles['archive/smp.py'].gsub(/\s+/, '').gsub('0x', '').gsub(',', '').gsub('smp=[', '').gsub(']', '') puts "Disassemble" require 'metasm' class SSTIC13_CPU < Metasm::CPU class Reg include Metasm::Renderable def initialize(i); @i = i; end def symbolic; "r#@i".to_sym; end def render; ["r#@i"]; end end class Acc include Metasm::Renderable def symbolic; :acc; end def render; ['acc']; end end class MemRef include Metasm::Renderable def initialize(ptr); @ptr = ptr; end def symbolic; Metasm::Indirection[@ptr.kind_of?(Metasm::Expression) ? @ptr : @ptr.symbolic, 1]; end def render; ['[', @ptr, ']']; end end def initialize @size = 8 @endianness = :little end def addop(name, bin, *args) o = Metasm::Opcode.new(name, bin) args.each { |a| o.args << a if @valid_args[a] o.props[a] = true if @valid_props[a] o.fields[a] = [@fields_mask[a], @fields_shift[a]] if @fields_mask[a] } @opcode_list << o end def init_opcode_list @opcode_list = [] @valid_props = { :setip => true, :stopexec => true } @fields_mask = { :r => 7, :r_m => 7, :r_ign => 7, :i7 => 0x7f } @fields_shift = { :r => 0, :r_m => 0, :r_ign => 0, :i7 => 0 } @valid_args = { :r => true, :r_m => true, :i7 => true, :acc => true, :acc_m => true, :i80 => true } addop 'mov', 0x00, :acc, :i7 addop 'u80', 0x80, :r addop 'and', 0x88, :acc, :r addop 'or', 0x90, :acc, :r addop 'u98', 0x98, :r addop 'not', 0xa0, :acc, :r_ign addop 'mov', 0xa8, :acc, :r addop 'mov', 0xb0, :r, :acc addop 'jz', 0xb8, :r, :setip addop 'jmp', 0xb8, :r, :setip, :stopexec addop 'mov', 0xc0, :r_m, :acc addop 'end', 0xc8, :r_ign, :stopexec addop 'mov', 0xd0, :acc, :acc_m addop 'shl', 0xd8, :acc addop 'or', 0xe0, :acc, :i80 addop 'ue8', 0xe8, :r addop 'uf0', 0xf0, :r addop 'uf8', 0xf8, :r end def build_bin_lookaside opcode_list.each { |op| mask = 0 op.fields.each { |fld, (msk, shft)| mask |= msk << shft } op.bin_mask = ~mask } end def decode_findopcode(edata) di = Metasm::DecodedInstruction.new(self) val = edata.decode_imm(:u8, @endianness) edata.ptr -= 1 if op = opcode_list.find { |o| val & o.bin_mask == o.bin } di.opcode = op di end end def decode_instr_op(edata, di) val = edata.decode_imm(:u8, @endianness) di.instruction.opname = di.opcode.name di.opcode.args.each { |a| di.instruction.args << case a when :r; Reg.new((val >> @fields_shift[a]) & @fields_mask[a]) when :i7; Metasm::Expression[(val >> @fields_shift[a]) & @fields_mask[a]] when :i80; Metasm::Expression[0x80] when :acc; Acc.new when :acc_m; MemRef.new(Acc.new) when :r_m; MemRef.new(Reg.new((val >> @fields_shift[a]) & @fields_mask[a])) else raise 'fuu' end } di.bin_length = 1 di end def get_backtrace_binding(di) args = di.instruction.args.map { |a| case a when Reg, Acc, MemRef; a.symbolic when Metasm::Expression; a end } case di.opcode.name when 'mov'; { args[0] => Metasm::Expression[args[1]] } when 'and'; { args[0] => Metasm::Expression[args[0], :&, args[1]] } when 'or'; { args[0] => Metasm::Expression[args[0], :|, args[1]] } when 'not'; { args[0] => Metasm::Expression[args[0], :^, Metasm::Expression[0xff]] } when 'shl'; { args[0] => Metasm::Expression[args[0], :<<, 1] } else {} end end def get_xrefs_x(dasm, di) return [] if not di.opcode.props[:setip] args = di.instruction.args.map { |a| case a when Reg, Acc, MemRef; a.symbolic when Metasm::Expression; a end } [args[0]] end def parse_arg_valid?(op, sym, arg) return true # allow any arg anywhere (for deobf) case sym when :r; arg.kind_of?(Reg) when :r_m; arg.kind_of?(MemRef) when :acc; arg.kind_of?(Acc) when :acc_m; arg.kind_of?(MemRef) when :i80; arg == Metasm::Expression[0x80] when :i7; arg.kind_of?(Metasm::Expression) end end def parse_argument(pgm) pgm.skip_space return if not tok = pgm.nexttok if tok.type == :string and tok.raw =~ /^r(\d)$/ pgm.readtok Reg.new($1.to_i) elsif tok.type == :string and tok.raw == 'acc' pgm.readtok Acc.new elsif tok.type == :punct and tok.raw == '[' pgm.readtok ptr = parse_argument(pgm) pgm.skip_space raise Metasm::ParseError, 'bad memref' if pgm.readtok.raw != ']' MemRef.new(ptr) else Metasm::Expression.parse(pgm) end end def decompile_makestackvars(dasm, funcstart, blocks) # no stack end def decompile_func_finddeps(dcmp, blocks, func) # trivial version: assume all blocks should store the final value of all regs (but not the accumulator) blocks.inject({}) { |deps, block| deps.update block[0] => (0..7).map { |i| "r#{i}".to_sym } } end def decompile_blocks(dcmp, myblocks, deps, func, nextaddr=nil) # copy/paste from ia32 scope = func.initializer stmts = scope.statements until myblocks.empty? b, to = myblocks.shift if l = dcmp.dasm.get_label_at(b) stmts << Metasm::C::Label.new(l) end ops = [] binding = {} ce = lambda { |*e| dcmp.decompile_cexpr(Metasm::Expression[Metasm::Expression[*e].reduce], scope) } ceb = lambda { |*e| ce[Metasm::Expression[*e].bind(binding)] } commit = lambda { deps[b].map { |k| [k, ops.rindex(ops.reverse.find { |r, v| r == k })] }.sort_by { |k, i| i.to_i }.each { |k, i| next if not i or not binding[k] e = k final = [] ops[0..i].reverse_each { |r, v| final << r if not v e = Metasm::Expression[e].bind(r => v).reduce if not final.include? r } ops[i][1] = nil binding.delete k stmts << ce[k, :'=', e] if k != e } } dcmp.dasm.decoded[b].block.list.each_with_index { |di, didx| a = di.instruction.args if di.opcode.props[:setip] and not di.opcode.props[:stopexec] and to.length > 1 # conditional jump commit[] n = dcmp.backtrace_target(get_xrefs_x(dcmp.dasm, di).first, di.address) cc = ceb[Metasm::Expression[:acc]] stmts << Metasm::C::If.new(Metasm::C::CExpression[cc], Metasm::C::Goto.new(n)) to.delete dcmp.dasm.normalize(n) next end case di.opcode.name when 'ret' commit[] stmts << Metasm::C::Return.new(nil) else bd = get_fwdemu_binding(di) update = {} bd.each { |k, v| if k.kind_of? ::Symbol and not deps[b].include? k ops << [k, v] update[k] = Metasm::Expression[Metasm::Expression[v].bind(binding).reduce] else stmts << ceb[k, :'=', v] stmts.pop if stmts.last.kind_of? Metasm::C::Variable # [:eflag_s, :=, :unknown].reduce end } binding.update update end } commit[] if to.length == 1 and (myblocks.empty? ? nextaddr != to[0] : myblocks.first.first != to[0]) stmts << Metasm::C::Goto.new(dcmp.dasm.auto_label_at(to[0], 'unknown_goto')) end end end end if full edata = Metasm::EncodedData.new([bytecode].pack('H*')) dasm = Metasm::Shellcode.decode(edata, SSTIC13_CPU.new).disassembler dasm.load_plugin 'deobfuscate' class << dasm Deobfuscate::PatternMacros['%r'] = '(?:r[0-7]|acc)' Deobfuscate::Patterns = { 'mov acc, 0 ; jz (%r)' => 'jmp %1', 'mov acc, (%i) ; or acc, 80h' => 'mov acc, 80h|%1', 'mov acc, (%r|%i) ; mov acc, \[acc\]' => 'mov acc, [%1]', 'mov acc, (%r|%i) ; mov \[(%r)\], acc' => 'mov [%2], %1', 'mov acc, (.*) ; mov (%r), acc' => 'mov %2, %1', 'mov (%r), (.*) ; jmp \1' => 'jmp %2', 'mov acc, (%r) ; and acc, (%r) ; mov \1, acc' => 'and %1, %2', 'mov acc, (%r) ; or acc, (%r) ; mov \1, acc' => 'or %1, %2', 'mov acc, (%r) ; shl acc ; mov \1, acc' => 'shl %1', 'and acc, (%r) ; mov \1, acc' => 'and %1, acc', 'or acc, (%r) ; mov \1, acc' => 'or %1, acc', 'mov acc, (%r) ; and (%r), acc' => 'and %2, %1', 'mov acc, (%r) ; or (%r), acc' => 'or %2, %1', } Deobfuscate.init end dasm.disassemble(0) dasm.decoded[0x73].instruction.args[-1] = Metasm::Expression[0] # small bug #dasm.addrs_todo.freeze # TODO debug split_block -> fill addrs_todo dasm.each_instructionblock { |b| # 'mov r4, (%i) ; mov acc, (%r) ; and acc, (%r) ; jz r4' => 'mov acc, %2 ; and acc, %3 ; jz %1', if b.list.last.opcode.name == 'jz' and tg = b.list.last.instruction.args.last and tg.kind_of?(SSTIC13_CPU::Reg) and b.list.first.opcode.name == 'mov' and b.list.first.instruction.args.first.to_s == tg.to_s b.list.last.instruction.args[-1] = b.list.first.instruction.args[-1] dasm.replace_instrs(b.address, b.address, []) end } dasm.addrs_todo.clear puts dasm #puts dasm.decompile(0) Metasm::Gui::DasmWindow.new("sstic2013", dasm) dasm.load_plugin 'hl_opcode' dasm.load_plugin 'colortheme_solarized' Metasm::Gui.main end # for (r0=0 ; r0 != data[0x10] ; r0++) data[r0] = swapbits(data[r0+0x11] ^ data[r0&0x0f]); puts "Bruteforce smd key" puts tarfiles['archive/decrypt.py'] if $VERBOSE data = tarfiles['archive/data'] def swapbits(b) out = 0 8.times { out <<= 1 out |= 1 if b & 1 == 1 b >>= 1 } out end def dev_getdata(key, data) data.unpack('C*').zip(key*16).map { |d, k| swapbits(d^k) }.pack('C*') end def bruteforce_datakey(data) validchars = {} ('0'..'9').each { |c| validchars[c.ord] = true } ('a'..'z').each { |c| validchars[c.ord] = true } ('A'..'Z').each { |c| validchars[c.ord] = true } validchars['+'.ord] = validchars['='.ord] = validchars["\n".ord] = true key = [0]*16 16.times { |key_idx| goodb = nil 256.times { |key_byte| key[key_idx] = key_byte good = true doff = 0 while doff < data.length out = dev_getdata(key, data[doff, 224]) #puts doff, data[doff, 224].scan(/.{16}/m).map { |e| e.unpack('C*').map { |b| swapbits(b) }.pack('C*').unpack('H*') }, key_idx, key_byte, out.scan(/.{16}/m).map { |e| e.unpack('H*') } doff += 224 out.unpack('C*').each_with_index { |datab, datai| good = false if not validchars[datab] and (datai % 16) == key_idx } break if not good end if good puts "valid key byte %d: %x" % [key_idx, key_byte] goodb = key_byte end } key[key_idx] = goodb } puts key.pack('C*').unpack('H*') key end def decode_data(key, data) out = '' doff = 0 while doff < data.length out << dev_getdata(key, data[doff, 224]) doff += 224 end out end if full key = bruteforce_datakey(data) else key = [230, 131, 220, 188, 22, 239, 88, 198, 101, 172, 35, 211, 30, 109, 161, 37] end atad = decode_data(key, data) puts Digest::MD5.hexdigest(atad) atad_ub64 = atad.unpack('m*').join File.open('atad.ps', 'w') { |fd| fd.write atad_ub64 } puts "Bruteforce ps key" def testkey(key) `gs -q -sDEVICE=txtwrite -sOutputFile=%stdout -- atad.ps #{key.pack('n*').unpack('H*').first} 2>&1`.lines.to_a end if full puts "Analyser le code pour trouver le mecanisme de dechiffrement d'I2 et I4 (le 1er round suffit, pour les 4 premiers octets)" puts "Bruteforcer ces 4 octets pour trouver du code valide (/^\d+ |^\/\w+/m)" puts "Remplacer I4 par sa version en clair, et patcher pour ecrire une ligne en cas de cle valide dans I4" key = [0xbac9, 0xf7a8, 0, 0, 0, 0, 0, 0] nthreads = ARGV.shift || (File.read('/proc/cpuinfo').lines.grep(/^processor/).length * 3) perthread = (65536.0 / nthreads).ceil puts "bf #{nthreads} threads, #{perthread} per thread" (2..7).each { |keyoff| puts key.pack('n*').unpack('H*').first found = false threads = (0...nthreads.to_i).map { |thread_| Thread.new(thread_) { |thread| threadkey = key.dup kv_start = thread*perthread kv_end = kv_start+perthread kv_end = 65536 if kv_end > 65536 (kv_start...kv_end).each { |kv| $stderr.print "#{keyoff} #{'%.2f' % (kv*100.0/perthread)} \r" if thread == 0 # and kv & 0xf == 0 threadkey[keyoff] = kv out = testkey(threadkey) if out.length >= keyoff puts "\n got #{keyoff} #{kv}" key[keyoff] = kv found = true end break if found } } } threads.each { |t| t.join } } else key = [0xbac9, 0xf7a8, 0x721f, 0xad3c, 0x9fcf, 0x271e, 0xed9a, 0xbbc8] end puts key.pack('n*').unpack('H*').first testkey(key) puts "Extract email" vcard = File.read('output.bin') mail_obf = vcard.lines.grep(/sys_/).first.split(':').last mail = mail_obf.split.map { |ss| Metasm::PTrace::SYSCALLNR_X86_64.index(ss.gsub(/sys_|stub_/, '')) }.pack('C*') puts mail File.unlink('atad.ps') File.unlink('output.bin')