-
Notifications
You must be signed in to change notification settings - Fork 62
/
dncfx.py
109 lines (93 loc) · 4.15 KB
/
dncfx.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import struct
import re
from Crypto.Util.strxor import strxor
from refinery.units import Unit
from refinery.units.compression.lz import lzma
from refinery.lib import chunks
from refinery.lib.dotnet.header import DotNetHeader, StreamReader, StringPrimitive, UInt32, ParserEOF
class dncfx(Unit):
"""
Extracts the encrypted strings from ConfuserX protected .NET execuctables.
Each decrypted string is returned as a single output.
"""
_PATTERN_ARRAY_INIT = (
BR'(\x1F.|\x20....)' # load size of a chunk
BR'\x8D.\x00\x00\x01' # create a UInt32 array
BR'\x25' # dup
BR'\xD0%s\x04' # ldtoken: RVA of array data
BR'\x28.\x00\x00.' # call to InitializeArray
)
def process(self, data):
header = DotNetHeader(data, parse_resources=False)
decompressor = lzma()
class IntegerAssignment:
def __init__(self, match):
self.offset = match.start()
self.value, = struct.unpack('<I', match[1])
def get_size(match):
ins = match[1]
fmt = '<B' if ins[0] == 0x1F else '<I'
result, = struct.unpack(fmt, ins[-struct.calcsize(fmt):])
return result
potential_seeds = [
IntegerAssignment(m)
for m in re.finditer(br'\x20(....)', data, re.DOTALL)
]
for entry in header.meta.RVAs:
offset = header.pe.get_offset_from_rva(entry.RVA)
index = struct.pack('<I', entry.Field.Index)
strings_found = 0
for match in re.finditer(self._PATTERN_ARRAY_INIT % re.escape(index[:3]), data, flags=re.DOTALL):
ms = match.start()
def sortkey(t):
weight = abs(t.offset - ms)
if t.offset < ms:
# this weights assignments after the array initialization down, but still
# prefers them over assignments that are further away than 2kb
weight += 2000
return weight
size = get_size(match)
if size % 0x10 or size > 10000:
continue
self.log_debug(F'found RVA {entry.Field.Index} initialized with length {size}.')
potential_seeds.sort(key=sortkey)
for seed in potential_seeds[1:400]:
# the first potential_seed will always be the assignment of the size variable
ciphertext = data[offset:offset + size * 4]
key = self._xs64star(seed.value)
key = chunks.pack(key, 4) + ciphertext[:-0x40]
decrypted = strxor(key, ciphertext)
try:
decompressed = decompressor(decrypted)
except Exception as e:
self.log_debug(
F'decompression failed for seed {seed.value:08X} at offset {seed.offset:08X}: {e}')
continue
else:
self.log_info(
F'decompression worked for seed {seed.value:08X} at offset {seed.offset:08X}.')
if len(decompressed) < 0x100:
continue
for string in self._extract_strings(decompressed):
strings_found += 1
yield string
if strings_found > 10:
break
def _xs64star(self, state):
for i in range(16):
state ^= (state >> 12) & 0xFFFFFFFF
state ^= (state << 25) & 0xFFFFFFFF
state ^= (state >> 27) & 0xFFFFFFFF
yield state & 0xFFFFFFFF
def _extract_strings(self, blob):
reader = StreamReader(blob)
while reader.tell() < len(blob):
try:
size = reader.expect(UInt32)
string = reader.expect(StringPrimitive, size=size, codec='UTF8', align=4)
except ParserEOF:
return
if string:
yield string.encode(self.codec)