MadProtect, not that mad --- Some weeks ago we stumbled on a packer that our tools could not break. Surprisingly, this is actually not that common since most of the malware in the wild uses some sort of RunPE technique which is relatively trivial to break using simple memory tracing. MadProtect is not any different, it looks like a “HackingForums-grade” packer – nevertheless our tools failed to handle it properly. At first we did not look into the original binary, which was a mistake that could have saved us a lot of unnecessary effort into debugging our code. Instead, it turned out to be enough to look at the logs from tracer and binaries it produced. The dumped binaries looked somewhat weird with a bunch of nops and other junk code which seems to do nothing. What struck us as odd was the regularity of nop-blocs: all of them seemed to be 0×10 bytes long (yes, we know we cannot count;), and we can see a lot of 0×10 bytes writes in tracer logs: coincidence? 2015-08-31 19:06:06,561 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtWriteVirtualMemory(poudf22ouytbbaa.exe,41d060,16) src: 394e60 2015-08-31 19:06:06,576 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]Memdump hash[41d060 – 16]: b5590be427a581e72c947ccd59accf38 2015-08-31 19:06:06,779 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtProtectVirtualMemory(pid:1744,41d070,16,RWX —) 2015-08-31 19:06:06,796 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtProtectVirtualMemory(pid:1744,41d000,4096,RWX —) 2015-08-31 19:06:06,811 [dbg] INFO: NtWriteVirtualMemory 2015-08-31 19:06:06,826 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtWriteVirtualMemory(poudf22ouytbbaa.exe,41d070,16) src: 394e70 2015-08-31 19:06:06,826 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]Memdump hash[41d070 – 16]: b5590be427a581e72c947ccd59accf38 2015-08-31 19:06:06,858 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtProtectVirtualMemory(pid:1744,41d080,16,RWX —) 2015-08-31 19:06:07,046 [dbg] INFO: [poudf22ouytbbaa.exe – 1060][1044]called[pre] NtProtectVirtualMemory(pid:1744,41d000,4096,RWX —) We assumed not and proceeded with this in mind. The first theory was it was used to clear out code from the packer but it doesn't make much of a sense because it is a concise binary, dumped based on some heuristics. So lets try to explore another approach: the binary we are seeing is merely a skeleton for a real code, that is being written on top of those nop-blocks. This is almost cool. The problem is the size of writes: we tend to ignore every remote-memory-write (ie using ntdll!NtWriteVirtualMemory) that is smaller than some threshold, so being something about hundreds of bytes, some re-coding was required! During this rewrite we found out that those memory writes are too concise to be used as bunch of patches, it was just one block being written 0×10 bytes at a time: perhaps some block-crypto was in play? Problem solved, just dump those writes and concatenate together and from the concatenation obtain a valid payload. What should work in theory, collapsed in coding, there was always some bytes missing! So we looked inside the binary instead... Inside IDA, first we notice that there is the same nop-block and junk code as in dropped bin, so it's just obfuscation. Looking around we can't find any meaningful API calls and strings so API is resolved dynamically and strings are encoded somehow. Encryption scheme looks kind of strange so let’s not go there yet. Fortunately API resolving is nicely packed in one function that takes only one integer. It's perfect for Appcall mechanism: just fire IDA debugger skip some initialization stuff and resolve all functions like this: Python>resolve_api = Appcall.proto("resolve_api","int __cdecl resolve_api(int);") Python>for i in range(20): print Name(resolve_api(i)) kernel32_VirtualAlloc kernel32_VirtualFree kernel32_SetFilePointer kernel32_CreateFileW kernel32_ReadFile kernel32_WriteProcessMemory kernel32_CreateProcessW kernel32_GetThreadContext kernel32_ReadProcessMemorykernel32_VirtualAllocEx kernel32_SetThreadContext kernel32_ResumeThread kernel32_GetProcAddress With clear API calls we clearly see there's not much going on: this is a simplest RunPE possible. One thing that stands out is that memory which will be written is decrypted on-fly one chunk at a time. Chunk is obviously 0x10 bytes... Ok let’s delve into encryption, quick scan for crypto with IDAscope revealed constants characteristic to AES, but the code doesn't look like AES. Let's go out on a limb and assume its AES, but where is the key? Looking at IDA doesn't yield much, mostly because decompilation is somewhat a kind of mess. But..there’s this odd looking loop for 0×0f to 0x4f: and the same loop is near the API resolution, care to make another guess?[0] Yes, this is the key! So we have the AES encrypted payload: let's write a decoder for this. We know the key, but we don't know where our payload is, and scanning for opcodes that are using it is not the best idea, as the code leaves much to be desired. Lets try something else: take the first chunk of data, when decrypted it's part of an MZ header and there is a big chance it won't change anytime soon. This gives us quite a unique pattern we can search for. If in doubt we can always decrypt first 0x400 bytes and check if this is a real PE header To wrap up all we have to do is put it all into a script and run it. It will reveal a similar binary which can be decoded by the same script... and the final payload is.. Netwire. { "binary": "989b29681f22c0c7561e441bbf6cb64c", "password": "36b&^%rUmLV8FN#{}r\"#V)}Hc`$?}j", "filename": "ESET-%Rand%", "reg-key": "avast", "mutex": "avast", "urls": [ { "cnc": "213.152.161.69", "port": 3838 }, { "cnc": "213.152.161.69", "port": 3837 } ], "flags": [ 104, 1, 10 ], "dir-path": "%AppData%\\Logs\\1\\2\\", "type": "netwire", "yara_hits": [ "netwire" ] } Here are some hashes, 0aa102ff1b2d618f6bef121927175efe7a2ff6f5b1d479cb54d9a79d5b1e3ac2 0c1aece534cc561d4e4716246a0ccd3453efd36c2197ba901fe2118683811090 110fa493bf95f1f6a86e493e70e854fdcb922f66b093434cee35b343ae5d79f3 1e19566e509e147d99babae1b63b9fdf6add9802b0920c536546dfdf112b0354 1e7b5c1a467df90135b800e57e32464f52e6869bfedc4310aecd4ad9f008ca9d 1f20058fc595f63a0d5c461d4ab5993bba9f22a8e31e461f15c18a1ca4d6670e 2241ad60b98a042b0d735269d813541e6db0911d93e1d03402ff6cbc296a9c70 231beab1671b1dd29577b92d0154b87bf6a4f1ee142a817f85cba0adca4d9e46 234eb6beb9bb88aa3503bb7c0e8f4b12c380248e3418d6977174b49ec219b4e5 26716b61692f55f86de3ea8b6a4c132b528488dc10a666442a5cfc46df1b3542 28ab6b7ac4e882ff205d88af0a69f82f8d459bcdc1d9e4d65980ed80d740192f 2a9505f6377c5f321d8ed864976f453fb8370e9ea5e861417beb45d1f9e709ae 36d8d9c6f5cdf95b8dc204bfb4b3eedc958dee752a2722fd1b0dcdd2cfe2edd2 3baf2232008d9cd06241c21a79b86e5b8b84968fd792490cac082595fd58b608 3bdfc0207f5e3142d20448b7440293a680d4fb611be56ba9b5f171574954a877 49682ff4764c751746b9aa69c53b9215a5c295c08bc2c1266eb8deff7850d1c6 yara rule [1] and of course the decoding script [2] Now let’s get back to work ;] Cheers, mak -- [0] https://www.cert.pl/wp-content/uploads/2016/03/odd_loop.png [1] https://gist.githubusercontent.com/mak/c8c5b5397a6248b17ca5/raw/d2342ac5016790cec456bcdf2e8cb786a9667f36/mp.yara [2] https://gist.github.com/mak/d049a307a271052dc740