Jarosław Jedynak
Jarosław Jedynak
Maciej Kotowicz
Jarosław Jedynak
Maciej Kotowicz
There are two GozNym hashes provided in the latest IBM/Trusteer blog post https://securityintelligence.com/goznym-launches-redirection-in-the-united-states/ // but not in vt...
mak: I'm looking for real sample for a while now, only have some nymaims and no banking part
(hash:b2ed2df6dc227919ec139cba434093cb3cb0c1552a413c1d6b1a83286ef41696)
also: inst1.exe, c1.exe, s2.bin
To: Jakob Lang <jakob.lang@freenet.de> Message-ID: <8a2bdf4dee2842b311df802b3b33f1dd@guardian-vlg.ru> From: noreply@unverified.beget.ru Reply-To: Stellvertretender Sachbearbeiter Pay Online24 AG <admin@amazon.com> ... Subject: *** GMX Spamverdacht *** Offene Rechnung: Buchungsnummer 39863821 X-GMX-Antispam: 4 (nemesis spam server blocker); Detail=V3; X-GMX-Antivirus: 0 (no virus found) ... --b1_8a2bdf4dee2842b311df802b3b33f1dd Content-Type: application/octet-stream;name="Jakob Lang 28.09.2016.zip" Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="Jakob Lang 28.09.2016.zip"
def nymaim_get_api_consts(): kpatt = '8F 45 E8 89 4D E4 E8 ? ? ? ? 89 C3 E8 ? ? ? ? 89 C2 89 45 FC 8B 4D E4 B8' kpatt+=' ? ? ? ? 29 C1 89 4D E0 C1 E9 02 83 F9 00 74 05 01 D3' kpatt2 = '8B 45 D8 3D ? ? ? ? 0F 84 ? ? ? ? 3D ? ? ? ? 0F 84 ? ? ? ? 3D ? ? ? ? 0F 84' faddr = FindBinary(idaapi.get_imagebase(),SEARCH_DOWN | SEARCH_REGEX, kpatt) key = GetOperandValue(GetOperandValue(faddr + 6,0),1) xstep= GetOperandValue(GetOperandValue(faddr + 13,0),1) off = GetOperandValue(faddr+26,1) kernl_h = 0x4b1ffe8e;ntdll_h = 0xab30a50a ## rol32(x,25) ^ c faddr = FindBinary(idaapi.get_imagebase(),SEARCH_DOWN | SEARCH_REGEX, kpatt2) x1 = Dword(faddr+4) ^ ntdll_h x2 = Dword(faddr+15) ^ kernl_h hash_xor = x1 if x1 != x2: print '[!] whoppse, please find your key manually' hash_xor=0 return off,key,xstep,hash_xor
def nymaim_get_api(api_off,off,key,xstep,hash_xor): api_va = idaapi.get_imagebase() + api_off xsize = api_va - off for _ in range(xsize/4): key = (key + xstep) & 0xffffffff r = 0 for i in range(4): r |= (Byte(api_va+i) ^ (_ror(key,(xsize&3)*8)&0xff) ) << i*8 xsize +=1 if xsize % 4 == 0: key = (key + xstep) & 0xffffffff return (r ^ hash_xor)
Our deobfuscator is able to revert (more or less) all mentioned obfuscation techniques:
We're going to publish our toolset, eventually.
We'd like to extract static config from binaries, especially things like:
def nymaim_extract_blob(self, mem, ndx): """decrypt final config (read keys and length and decrypt raw data)""" key0 = mem.dword(ndx) key1 = mem.dword(ndx+4) len = mem.dword(ndx+8) raw = mem.read(ndx + 12, len) prev_chr = 0 result = '' for i, c in enumerate(raw): bl = ((key0 & 0x000000FF) + prev_chr) & 0xFF key0 = (key0 & 0xFFFFFF00) + bl prev_chr = ord(c) ^ bl result += chr(prev_chr) key0 = (key0 + key1) & 0xFFFFFFFF key0 = ((key0 & 0x00FFFFFF) << 8) + ((key0 & 0xFF000000) >> 24) return result
struct chunk { uint32_t type; uint32_t length; char data[chunk_length]; }
Dropper is doing few sanity checks, for example:
If something isn't right, dropper shuts down and infection doesn't happen.
Static config contains (among others) two interesting pieces of information:
Nymaim is asking DNS server for A records for that domain... But returned IPs are not real C&C ip addresses.
https://github.com/vrtadmin/goznym/blob/master/DGA_release.py
When dropper gets to know C&C address, it starts real communication. It downloads two important binaries, and a lot more:
Payload is very different from dropper when it comes to network communication:
def dga_single(self, state): name = '' len = self.getbyte(state, 8) + 5 for i in range(len): r = self.getbyte(state, 0xFFFFFFFF) c = self.getbyte(state, 26) + 0x61 name += chr(c) n = 0 while n == 0: n = self.getbyte(state, 5) name += '.' + [0, 'net', 'com', 'in', 'pw'][n] return name
XorShift variation
def getbyte(self, state, param): temp0 = ((state[0] << 11) ^ state[0]) & 0xFFFFFFFF temp2 = state[2] state[0] = (state[0] + state[1]) & 0xFFFFFFFF state[1] = (state[1] + state[2]) & 0xFFFFFFFF state[2] = (state[2] + state[3]) & 0xFFFFFFFF state[3] = ((state[3] >> 19) ^ state[3] ^ temp0 ^ (temp0 >> 8)) & 0xFFFFFFFF return (((state[3] + temp2) & 0xFFFFFFFF) % (param * 100)) / 100
def __init__(self, seed, date): arg8 = seed + date.day + (date.year << 9) + (date.month << 5) state = [0] * 4 state[0] = (arg8 + seed) & 0xFFFFFFFF state[1] = ror(state[0] * 2, 4) state[2] = ror(bswap(state[1]), 0xE) + seed state[3] = ror(state[2] + state[1], 0x12) for i in range(16): next_byte = self.getbyte(state, 0xFFFFFFFF) dword_ndx, byte_ndx = i / 4, i % 4 byte_mask = 0xFF << (byte_ndx * 8) state[dword_ndx] = (state[dword_ndx] & ~byte_mask) | ((next_byte & 0xFF) << (byte_ndx * 8)) self.state = state
Code for message decryption
def nymaim_decrypt(key, raw_bytes): nibble0 = raw_bytes[0] & 0xF nibble1 = raw_bytes[1] & 0xF salt = raw_bytes[2:2+nibble0] password = key + salt data = raw_bytes[2+nibble0:len(raw_bytes)-nibble1] decrypted = rc4_decrypt(password, body) decrypted_len = struct.unpack('<I', decrypted[:4])[0] assert decrypted_len == len(decrypted - 4) return decrypted
Message = sequence of chunks.
Chunk has type, length, and type-specific data
Code used for message processing
def parse_message(blob): i = 0 while i < len(blob): chunk_type = blob[i:i+4] chunk_len = from_uint32(blob[i+4:i+8]) chunk_content = blob[i+8:i+8+chunk_len] process_chunk(chunk_type, chunk_content) i += 8 + chunk_len
Important chunks have another layer of encryption & compression
So we can't push our binaries or injects to whole botnet (without private key, at least)
A lot of data is compressed with aplib32 before encryption, to save some transfer.
def inner_decrypt(raw, rsa_key): encrypted_header, encrypted_data = raw[-0x40:], raw[:-0x40] decrypted_data = rsa_decrypt(encrypted_header, rsa_key) md5 = decrypted_data[0:16] blob = decrypted_data[16:32] length = from_uint32(decrypted_data[32:36]) serpent_decrypted = crypto.s_decrypt(encrypted_data, blob)[:length] assert md5 == hashlib.md5(serpent_decrypted).digest() return serpent_decrypted
Interesting things sent to server
4a6fbfd2
, 1c225a3e
and 8fc11cf3
)6ee5d5ff
, e02b4e01
and f90670f7
)14c58ebe
)Interesting chunks received from server
14c58ebe
) againbf2f5c87
, 2861bc3b
, ae61bc39
, 6cc51d26
, 6cc51fa0
, and more)35e7f241
, 48a9c01e
, 5ea9c018
, 2e7c713d
)40185e1f
, 0c2f0f92
, 0c2f0f93
)!╰─$ strings decrypted_nymaim | grep -E "#!#|Firewall"
#!#*|Action=Allow|#*
#!#*|Action=Block|#*
#!#*|Active=TRUE|#*
#!#*|Active=FALSE|#*
#!#*|Dir=In|#*
#!#*|Dir=Out|#*
#!#*|Profile=Private|#*
#!#*|Profile=Public|#*
#!#*|LPort=#*
#!#*|RPort=#*
\Registry\Machine\SYSTEM\ControlSet001\Services\SharedAccess\Parameters
\FirewallPolicy\StandardProfile\AuthorizedApplications\List
\Registry\Machine\SYSTEM\ControlSet001\Services\SharedAccess\Parameters
\FirewallPolicy\FirewallRules
╰─$ strings decrypted_nymaim | grep -E "PortMap|upnp"
DeletePortMapping
urn:schemas-upnp-org:service:WANPPPConnection:1
urn:schemas-upnp-org:device:InternetGatewayDevice:1
GetSpecificPortMappingEntry
upnp:rootdevice
AddPortMapping
AddAnyPortMapping
urn:schemas-upnp-org:service:WANIPConnection:1
NewPortMappingDescription
╰─$ strings decrypted_nymaim | grep -E "nginx" -B 4
HTTP/1.1 200 OK
Connection: close
Content-Length: %u
Content-Type: application/octet-stream
Server: nginx/1.9.4
304 different injects, as of today
393 different injects, as of today
msm jaroslaw.jedynak@cert.pl
mak mak@cert.pl