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