Binary Deobfuscation 101 ๐
Taming real world malware obfuscations
Introduction
When it comes to reverse engineering of real world malware samples, we are not provided with a clean binary which is just one F5 away ๐ . Instead we get somewhat obfuscated samples which is a major hinderence in binary analysis/reverse engineering. Well to deal with this, we are left with only one option which is you guessed it right Deobfuscation. So here you will read about most well known obfuscation techniques and how to deal with them using miasm a free and open source (GPLv2) reverse engineering framework. As I was learning miasm by reading it source (pain) because it has poorly managed documentation and less examples. Even there official blog is outdated somehow wrtten in 2016-2019. So i saw a perfect opportunity to write about miasm, after all its a great framework.
Who is this post for?
If you are getting your hands dirty in binary deobfuscation this is for you, as i will go from basic concepts to advance (kind of). You can read about Advance Binrary Deobfuscations. I will discuss some of the solutions of ABD Exercises that I have done and giving basic overview of miasm After that we will do deobfuscation of real world malware samples Which included Multiple Obfuscations Such as:
Opaque predicates, Virtual Machine Based Obfuscation, Indirect Jumps, Indirect Calls and Control Flow Flattening.
For some basic examples i will be using src from ABD
Because of this fact i cannot pick a single source so the source will be changing during basic deobfuscation examples :p
Dead Code Removal
Basic Example Source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
unsigned int target_function(int n)
{
int a,b,c,r;
a = 12;
b = 56;
c = 127;
r = a + b + c + n;
return r;
}
int main(int argc, char* argv[])
{
unsigned int n = target_function(argc);
printf("n=%u\n", n);
return 0;
}
Disassembly:
Here we can clearly see that these three values are added the the function argument.
But even a very simple example, nothing but addition, when obfuscated with ollvm sub pass. it becomes like this:
It may not seem very complicated but just to give you the idea that simple addition and there are junk code some constants that we didnโt even had in our binary and additional instructions which are nothing but junk, if done this iteratively it becomes massive problem.
Data Flow Analysis & Constant Propagation
To deal with this we will perform some Data Flow Analysis which is a fundamental part of Compiler Optimizations Techniques by lifting the x86 architecture to miasmโs Intermediate Representation.
1
2
3
4
5
6
7
8
# these are miasm inits
loc_db = LocationDB() # for managing locations and addreses
container = Container.from_stream(open(file, 'rb'), loc_db=loc_db) # Init Binary Container
machine = Machine(container.arch) # architecture-specific toolkit
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db) # disassembly engine
asmcfg = mdis.dis_multiblock(addr) # whole assembly control flow graph of the target address
lifter = machine.lifter_model_call(loc_db=loc_db) # initialization of the lifter
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg) # lifting x86 asmcfg to miasm's own IR-CFG
These are the initialization stage of miasm (almost) every time we will do this so i might skip later
1
open('asmcfg.dot', 'w').write(asmcfg.dot()); open('ircfg', 'w').write(ircfg.dot())
we can also view the asmcfg and ircfg like this
the asmcfg is similar as you saw as it is x86 assembly
But Miasmโs IR is a lil complicated you can say:
You will later see in this blog that Hexrays IR also has Explicit semantics for EFLAGS after each computation instruction.This is Because the IR needs to be a complete, self-contained semantic model of the instruction โ including side effects so that later analysis (symbolic execution, optimization) is correct without needing hidden/implicit CPU behavior.
So we will do Dead Removal and Constant Propagation using miasm.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
deadrm = DeadRemoval(lifter=lifter) # create dead-code elimination pass for this lifter
entry_points = set([mdis.loc_db.get_offset_location(addr)]) # get LocKey used as CFG entry point
init_infos = lifter.arch.regs.regs_init # initial symbolic values for all registers
# runs constant propagation over the IR CFG starting from addr with the given initial register state, folding known constant values through expressions.
cst_propag_link = propagate_cst_expr(lifter, ircfg, addr, init_infos)
modified = True
while modified: # keep optimizing until no more changes occur
modified = False
modified |= deadrm.do_dead_removal(ircfg=ircfg) # remove unused/dead IR assignments
modified |= remove_empty_assignblks(ircfg) # remove blocks left empty after dead removal
modified |= merge_blocks(ircfg, entry_points) # merge mergeable blocks to simplify CFG
open(f'ircfg_simplified.dot', 'w').write(ircfg.dot()) # constant propagated, clean unobfuscated ir
Here is the IRCFG now !
So now on this basic example the dead code is removed and optimized
Unfortunately Miasm IR is not backword compatible to x86, so we canโt just recompile the clean binary, tho it โhadโ conversion to LLVM IR but this is also broken in latest Miasm versions due to the use of depreciated LLVM-Lite APIs
The full script can be found here
Opaque Predicates
In computer programming, an opaque predicate is a predicate, an expression that evaluates to either โtrueโ or โfalseโ, for which the outcome is known by the programmer a priori, but which, for a variety of reasons, still needs to be evaluated at run time.
so its just having multiple Basic Blocks that are never executed, Which results in increase number of basic blocks and function sizes are increased.
i never thought of deobfuscating hello world lol. We are choosing this example now because it has ollvm bcf.
Basic Example Source:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
void target_function(void)
{
char* msg = "hello world";
printf("%s\n", msg);
}
int main(int argc, char* argv[])
{
target_function();
return 0;
}
Disassembly:
Well a hello world canโt have those many basic blocks ๐
General Heuristic to deal with Opaque Predicates
we will execute each basic block via Symbolic Execution and check if the result is an Condition then, if it is we will convert the miasm expression to z3 IR. and check is anyof the branch from the condition is unsat, If it is then we know that it is an opaque branch and thus will never be executed.
1
2
3
4
5
6
# here 'l' is the branch 1 and the 'r' is the branch 2 of the basic block possible condition
def is_opaque(l, r):
s = z3.Solver()
t = TranslatorZ3() # converting miasm expression to z3 IR
s.add(t.from_expr(l) == t.from_expr(r)) # adding to the z3 solver
return s.check() == z3.unsat # returns true if unsat/opaque branch
Iterating though each basic block in the asmcfg and execute it symbolically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[...]
for bb in asmcfg.blocks:
# init sybolic execution
sb = SymbolicExecutionEngine(lifter=lifter)
# symbolic execution of bb
r = sb.run_block_at(ircfg=ircfg, addr=bb.lines[0].offset)
if not isinstance(r, ExprCond):
continue
vaddr = bb.lines[-1].offset
# cond? jump: fall
jcc = is_opaque(r, r.src1); fall = is_opaque(r, r.src2)
# print('jcc ', jcc, 'fall ', fall) # debug
if jcc: # jcc is opaque so it won't jmp
# patch the jcc with nops, so it just falls
print('Patched NOP: ', hex(vaddr)) # not hitting in this example
elif fall: # fall is opaque so it won't fall
# convert jcc to jmp
print('Replace jcc', hex(vaddr))
else:
print(f'Skip valid conditional at: {hex(offset)}')
patching to nop is very trivial, just see the jcc size and patch to bโ\x90โ * instruction_size. But patching jcc to jmp we have to take care of the relative address. in this example the jcc are 6 bytes as they are rel32 jumps you check more on x86 conditional jumps
So in this case we will read 4 bytes (skipping the 2 jcc default bytes) and because the formula is rel32 = address to jump - (address of jmp instruction + instruction_size) as the jcc was 6 bytes so reading and +1 becomes the new-rel32.
1
2
3
4
5
6
7
8
9
# helper functions
p32 = lambda d: struct.pack('<I', d)
u32 = lambda d, i: struct.unpack('<I', d[i:i+4])[0]
[...]
elif fall: # fall is opaque so it won't fall
# replace with direct jump
print('Replace jcc', hex(vaddr)) # debug
new_rel32 = u32(binary, offset+2) + 1 # read 4 bytes of rel & as jmp has 1 less byte size than jcc +1 so new_rel = oldrel + 1
binary[offset: offset + 6] = b'\xE9' + p32(new_rel32) + b'\x90'
The Whole script is can be found here
X-tunnel
Opaque predicates alone are not hard to deal with, i think they are trivial, lets take the example of this apt malware.
Itโs decompilation is more than 4k LOC.
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
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.ir.translators.z3_ir import TranslatorZ3
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.expression.expression import ExprCond
import z3
import lief
import struct
file = 'x-tunnel.bin'
addr = 0x424760
p32 = lambda d: struct.pack('<I', d)
u32 = lambda d, i: struct.unpack('<I', d[i:i+4])[0]
pe = lief.PE.parse(file); binary = bytearray(open(file, 'rb').read())
va = lambda addr: pe.va_to_offset(addr)
loc_db = LocationDB()
container = Container.from_stream(open(file, 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
lifter = machine.lifter_model_call(loc_db=loc_db)
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
def is_opaque(l, r):
s = z3.Solver()
t = TranslatorZ3()
s.add(t.from_expr(l) == t.from_expr(r))
return s.check() == z3.unsat
for bb in asmcfg.blocks:
# init sybolic execution
sb = SymbolicExecutionEngine(lifter=lifter)
# symbolic execution of bb
r = sb.run_block_at(ircfg=ircfg, addr=bb.lines[0].offset)
if not r.is_cond():
continue
ins = bb.lines[-1]
vaddr = ins.offset; offset = va(vaddr)
# cond? jump: fall
jcc = is_opaque(r, r.src1); fall = is_opaque(r, r.src2)
# print('jcc ', jcc, 'fall ', fall)
if jcc: # jcc is opaque so it won't jmp
# patch the jcc, so it just falls
binary[offset:offset+ins.l] = b'\x90' * ins.l # nop jcc
print('Patch NOP: ', hex(vaddr), ins.l, hex(offset)) # not hitting in this case
elif fall: # fall is opaque so it won't fall
# replace with direct jump
print('Replace jcc', hex(vaddr), ins.l) # debug
# nothing there are no opaque jcc so just left it
open(file + '.deob', 'wb').write(binary) # removed opaque predicate, clean deobfuscated patched file
Decompiled function LOC is reduced massively
Stack VM
Its a simple stack VM. This example will is VM Obfuscation with Tigress And by this you will see how a very simple example becomes when VM obfuscation is applied to it.
Basic Example Source:
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
#include <stdio.h>
int target_function(int n)
{
int a, b;
a = 0xdeadbeef;
b = 0x8badf00d;
int c = a + b;
int d = a - b;
int e = c + d + n;
if(e % 2 == 0){
return 0;
}else{
return 1;
}
}
int main(int argc, char* argv[])
{
int n = target_function(argc);
printf("n=%d\n", n);
return n;
}
After obfuscation:
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/* Generated by CIL v. 1.7.0 */
/* print_CIL_Input is false */
union _1_target_function_$node;
struct _IO_FILE;
enum _1_target_function_$op;
struct timeval;
extern int gettimeofday(struct timeval *tv , void *tz ) ;
extern int pthread_cond_broadcast(int *cond ) ;
char **_global_argv = (char **)0;
extern int posix_memalign(void **memptr , unsigned long alignment , unsigned long size ) ;
extern int getpagesize() ;
extern int pthread_join(void *thread , void **value_ptr ) ;
extern unsigned long strlen(char const *s ) ;
extern int open(char const *filename , int oflag , ...) ;
extern int pthread_barrier_destroy(int *barrier ) ;
extern int pthread_mutex_init(int *mutex , int *attr ) ;
extern int strncmp(char const *s1 , char const *s2 , unsigned long maxlen ) ;
extern int printf(char const * __restrict __format , ...) ;
int _global_argc = 0;
extern int pthread_cond_signal(int *cond ) ;
extern int pthread_barrier_init(int *barrier , int *attr , unsigned int count ) ;
extern int raise(int sig ) ;
extern int scanf(char const *format , ...) ;
char **_global_envp = (char **)0;
extern int unlink(char const *filename ) ;
extern double difftime(long tv1 , long tv0 ) ;
extern int pthread_barrier_wait(int *barrier ) ;
extern int pthread_mutex_lock(int *mutex ) ;
extern void *memcpy(void *s1 , void const *s2 , unsigned long size ) ;
extern int gethostname(char *name , unsigned long namelen ) ;
extern void *dlsym(void *handle , char *symbol ) ;
char const *_1_target_function_$strings = "";
extern void abort() ;
extern unsigned long strtoul(char const *str , char const *endptr , int base ) ;
extern void free(void *ptr ) ;
extern int fprintf(struct _IO_FILE *stream , char const *format , ...) ;
int main(int argc , char **argv , char **_formal_envp ) ;
extern void exit(int status ) ;
extern void signal(int sig , void *func ) ;
typedef struct _IO_FILE FILE;
extern int mprotect(void *addr , unsigned long len , int prot ) ;
extern int close(int filedes ) ;
extern double strtod(char const *str , char const *endptr ) ;
extern double log(double x ) ;
extern double ceil(double x ) ;
union _1_target_function_$node {
char _char ;
unsigned int _unsigned_int ;
unsigned char _unsigned_char ;
long _long ;
unsigned long _unsigned_long ;
void *_void_star ;
unsigned short _unsigned_short ;
unsigned long long _unsigned_long_long ;
signed char _signed_char ;
long long _long_long ;
int _int ;
short _short ;
};
extern int fclose(void *stream ) ;
extern int fcntl(int filedes , int cmd , ...) ;
enum _1_target_function_$op {
_1_target_function__return_int$expr_STA_0 = 11,
_1_target_function__local$result_STA_0$value_LIT_0 = 181,
_1_target_function__PlusA_int_int2int$right_STA_0$result_STA_0$left_STA_1 = 242,
_1_target_function__store_int$right_STA_0$left_STA_1 = 161,
_1_target_function__constant_int$result_STA_0$value_LIT_0 = 175,
_1_target_function__load_int$left_STA_0$result_STA_0 = 254,
_1_target_function__formal$result_STA_0$value_LIT_0 = 32,
_1_target_function__goto$label_LAB_0 = 9,
_1_target_function__Mod_int_int2int$left_STA_0$result_STA_0$right_STA_1 = 184,
_1_target_function__MinusA_int_int2int$left_STA_0$result_STA_0$right_STA_1 = 150,
_1_target_function__Eq_int_int2int$left_STA_0$result_STA_0$right_STA_1 = 194,
_1_target_function__branchIfTrue$expr_STA_0$label_LAB_0 = 42
} ;
unsigned char _1_target_function_$array[1][168] = { { _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)4, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__constant_int$result_STA_0$value_LIT_0, (unsigned char)239, (unsigned char)190,
(unsigned char)173, (unsigned char)222, _1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__local$result_STA_0$value_LIT_0,
(unsigned char)8, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__constant_int$result_STA_0$value_LIT_0, (unsigned char)13, (unsigned char)240, (unsigned char)173,
(unsigned char)139, _1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)12,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0,
(unsigned char)4, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)8, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__PlusA_int_int2int$right_STA_0$result_STA_0$left_STA_1,
_1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)16, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)8,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0,
_1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)4, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__MinusA_int_int2int$left_STA_0$result_STA_0$right_STA_1, _1_target_function__store_int$right_STA_0$left_STA_1,
_1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)20, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)12, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__local$result_STA_0$value_LIT_0,
(unsigned char)16, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__PlusA_int_int2int$right_STA_0$result_STA_0$left_STA_1, _1_target_function__formal$result_STA_0$value_LIT_0, (unsigned char)0,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0,
_1_target_function__PlusA_int_int2int$right_STA_0$result_STA_0$left_STA_1, _1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__goto$label_LAB_0, (unsigned char)4,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__constant_int$result_STA_0$value_LIT_0,
(unsigned char)0, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__constant_int$result_STA_0$value_LIT_0, (unsigned char)2, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)20, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__Mod_int_int2int$left_STA_0$result_STA_0$right_STA_1,
_1_target_function__Eq_int_int2int$left_STA_0$result_STA_0$right_STA_1, _1_target_function__branchIfTrue$expr_STA_0$label_LAB_0, (unsigned char)9, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__goto$label_LAB_0, (unsigned char)25,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0,
(unsigned char)24, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__constant_int$result_STA_0$value_LIT_0, (unsigned char)0, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__goto$label_LAB_0, (unsigned char)4,
(unsigned char)0, (unsigned char)0, (unsigned char)0, _1_target_function__goto$label_LAB_0,
(unsigned char)25, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)24, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__constant_int$result_STA_0$value_LIT_0, (unsigned char)1, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__store_int$right_STA_0$left_STA_1, _1_target_function__goto$label_LAB_0,
(unsigned char)4, (unsigned char)0, (unsigned char)0, (unsigned char)0,
_1_target_function__goto$label_LAB_0, (unsigned char)4, (unsigned char)0, (unsigned char)0,
(unsigned char)0, _1_target_function__local$result_STA_0$value_LIT_0, (unsigned char)24, (unsigned char)0,
(unsigned char)0, (unsigned char)0, _1_target_function__load_int$left_STA_0$result_STA_0, _1_target_function__return_int$expr_STA_0}};
int target_function(int n ) ;
extern int pthread_cond_wait(int *cond , int *mutex ) ;
extern void perror(char const *str ) ;
extern int pthread_cond_init(int *cond , int *attr ) ;
extern long write(int filedes , void *buf , unsigned long nbyte ) ;
extern int ptrace(int request , void *pid , void *addr , int data ) ;
extern float strtof(char const *str , char const *endptr ) ;
extern unsigned long strnlen(char const *s , unsigned long maxlen ) ;
struct timeval {
long tv_sec ;
int tv_usec ;
};
extern void qsort(void *base , unsigned long nel , unsigned long width , int (*compar)(void *a ,
void *b ) ) ;
extern long clock(void) ;
extern long time(long *tloc ) ;
extern long read(int filedes , void *buf , unsigned long nbyte ) ;
extern int rand() ;
extern int strcmp(char const *a , char const *b ) ;
extern void *fopen(char const *filename , char const *mode ) ;
extern double sqrt(double x ) ;
extern long strtol(char const *str , char const *endptr , int base ) ;
extern int snprintf(char *str , unsigned long size , char const *format , ...) ;
extern void *malloc(unsigned long size ) ;
extern int nanosleep(int *rqtp , int *rmtp ) ;
extern int pthread_mutex_unlock(int *mutex ) ;
extern int atoi(char const *s ) ;
extern int pthread_create(void *thread , void *attr , void *start_routine , void *arg ) ;
extern int fscanf(struct _IO_FILE *stream , char const *format , ...) ;
extern int fseek(struct _IO_FILE *stream , long offs , int whence ) ;
void megaInit(void) ;
void megaInit(void)
{
{
}
}
int main(int argc , char **argv , char **_formal_envp )
{
int n ;
int tmp ;
int _BARRIER_0 ;
{
megaInit();
_global_argc = argc;
_global_argv = argv;
_global_envp = _formal_envp;
_BARRIER_0 = 1;
tmp = target_function(argc);
n = tmp;
printf((char const */* __restrict */)"n=%d\n", n);
return (n);
}
}
int target_function(int n )
{
char _1_target_function_$locals[28] ;
union _1_target_function_$node _1_target_function_$stack[1][32] ;
union _1_target_function_$node *_1_target_function_$sp[1] ;
unsigned char *_1_target_function_$pc[1] ;
{
_1_target_function_$sp[0] = _1_target_function_$stack[0];
_1_target_function_$pc[0] = _1_target_function_$array[0];
while (1) {
switch (*(_1_target_function_$pc[0])) {
case _1_target_function__Mod_int_int2int$left_STA_0$result_STA_0$right_STA_1:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + -1)->_int = (_1_target_function_$sp[0] + 0)->_int % (_1_target_function_$sp[0] + -1)->_int;
(_1_target_function_$sp[0]) --;
break;
case _1_target_function__PlusA_int_int2int$right_STA_0$result_STA_0$left_STA_1:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + -1)->_int = (_1_target_function_$sp[0] + -1)->_int + (_1_target_function_$sp[0] + 0)->_int;
(_1_target_function_$sp[0]) --;
break;
case _1_target_function__load_int$left_STA_0$result_STA_0:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + 0)->_int = *((int *)(_1_target_function_$sp[0] + 0)->_void_star);
break;
case _1_target_function__return_int$expr_STA_0:
(_1_target_function_$pc[0]) ++;
return ((_1_target_function_$sp[0] + 0)->_int);
break;
case _1_target_function__local$result_STA_0$value_LIT_0:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + 1)->_void_star = (void *)(_1_target_function_$locals + *((int *)_1_target_function_$pc[0]));
(_1_target_function_$sp[0]) ++;
_1_target_function_$pc[0] += 4;
break;
case _1_target_function__formal$result_STA_0$value_LIT_0:
(_1_target_function_$pc[0]) ++;
switch (*((int *)_1_target_function_$pc[0])) {
case 0:
(_1_target_function_$sp[0] + 1)->_void_star = (void *)(& n);
break;
}
(_1_target_function_$sp[0]) ++;
_1_target_function_$pc[0] += 4;
break;
case _1_target_function__Eq_int_int2int$left_STA_0$result_STA_0$right_STA_1:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + -1)->_int = (_1_target_function_$sp[0] + 0)->_int == (_1_target_function_$sp[0] + -1)->_int;
(_1_target_function_$sp[0]) --;
break;
case _1_target_function__MinusA_int_int2int$left_STA_0$result_STA_0$right_STA_1:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + -1)->_int = (_1_target_function_$sp[0] + 0)->_int - (_1_target_function_$sp[0] + -1)->_int;
(_1_target_function_$sp[0]) --;
break;
case _1_target_function__constant_int$result_STA_0$value_LIT_0:
(_1_target_function_$pc[0]) ++;
(_1_target_function_$sp[0] + 1)->_int = *((int *)_1_target_function_$pc[0]);
(_1_target_function_$sp[0]) ++;
_1_target_function_$pc[0] += 4;
break;
case _1_target_function__goto$label_LAB_0:
(_1_target_function_$pc[0]) ++;
_1_target_function_$pc[0] += *((int *)_1_target_function_$pc[0]);
break;
case _1_target_function__store_int$right_STA_0$left_STA_1:
(_1_target_function_$pc[0]) ++;
*((int *)(_1_target_function_$sp[0] + -1)->_void_star) = (_1_target_function_$sp[0] + 0)->_int;
_1_target_function_$sp[0] += -2;
break;
case _1_target_function__branchIfTrue$expr_STA_0$label_LAB_0:
(_1_target_function_$pc[0]) ++;
if ((_1_target_function_$sp[0] + 0)->_int) {
_1_target_function_$pc[0] += *((int *)_1_target_function_$pc[0]);
} else {
_1_target_function_$pc[0] += 4;
}
(_1_target_function_$sp[0]) --;
break;
}
}
}
}
So now we are starting to deal with something interesting, Opening it to IDA:
IDA decompilation results:
VM Tracing
Our first step is to make a working Symbolic Execution Engine which traces the vm with concerete bytecode values. for that we can insert the vm bytecode into the sybolic execution symbols dictonary.
1
2
3
4
5
6
7
8
9
10
11
12
13
[...]
def read_bytecode():
# here i have embedded the address of the bytecode and its size
bytecode_addr = 0x804A060; sz = 0x804A107 - 0x804A060 + 1 # is 0xA8
sym_bytecode_addr = ExprMem(ExprInt(bytecode_addr, 32), sz * 8) # symbolic memory
byte_code = container.bin_stream.getbytes(bytecode_addr, sz) # bytecode from the file
sym_bytecode = ExprInt(int.from_bytes(byte_code, byteorder='little'), sz * 8)
return sym_bytecode_addr, sym_bytecode
sb = SymbolicExecutionEngine(lifter=lifter)
sym_bytecode_addr, sym_bytecode = read_bytecode()
sb.symbols[sym_bytecode_addr] = sym_bytecode # fixed the bytecode in the symbolic symbols
[...]
when ever Symbolic execution is performed than bytecode will be treated as concerete value instead of symbolic.
1
2
3
4
5
6
7
8
9
10
11
12
13
[...]
sb.run_block_at(ircfg=ircfg, addr=vm_entry) # initialized all the stack varible etc
q = deque([ExprInt(vm_dispatcher, 32)])
while q:
bb = q.popleft()
# print(f'current: {block}')
r = sb.run_block_at(ircfg=ircfg, addr=bb)
print(r)
if r.is_loc() or r.is_int():
q.append(r)
After some tracing it stopped !
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[...]
0x804856C
0x804858E
0x8048595
0x80485A0
0x8048724
0x80488A8
0x8048509
0x8048520
0x8048527
0x8048530
0x804854C
0x8048856
smod((@32[ESP + 0x4] + 0xBD5B7DDE)[31:32]?({@32[ESP + 0x4] + 0xBD5B7DDE 0 32, 0xFFFFFFFF 32 64},{@32[ESP + 0x4] + 0xBD5B7DDE 0 32, 0x0 32 64}), 0x2)[0:32]?(0x8048889,0x8048871)
The last basic block was 0x8048856 so we analyze that and see why the next didnโt resolved.
We can see that it reads an address from the virtual stack pointer and dereference it and check if its zero or not, so it takes a path depending on the vstack_ptr. if we look at the miasm expression condition @32[ESP + 0x4] + 0xBD5B7DDE)[31:32]? [ESP + 0x4] is a symbolic value which we havenโt defined in the symbolic execution symbols dictonary.
the arg variable is [EBP-0x8] which is basically [ESP + 0x4] its the function argument. So we just have to make the function argument concerete.
1
2
3
4
5
6
7
8
# initiallizing before running the symbolic exectution of vm entry
sb.symbols[ExprId('ESP', 32)] = ExprInt(0x1000, 32) # arbitrary address
arg = ExprMem(expr_simp(sb.symbols[ExprId('ESP', 32)] + ExprInt(4, 32)), 32) # the stack address where the argument resides
sb.symbols[arg] = ExprInt(0, 32) # set concrete value for arg
sb.run_block_at(ircfg=ircfg, addr=vm_entry)
[...]
1
2
3
4
5
6
7
8
9
10
11
[...]
0x80488A8
0x8048509
0x8048520
0x8048527
0x8048530
0x8048535
0x804853E
0x804865F
0x80488B2
@32[0x1000]
Thats it, as it was a simple vm our tracer is complete :)
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
# tracer
file = 'test-mod2-add-virtualized.bin'
addr = 0x80484DE
loc_db = LocationDB()
container = Container.from_stream(open(file, 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
lifter = machine.lifter_model_call(loc_db=loc_db)
sb = SymbolicExecutionEngine(lifter=lifter)
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
vm_entry = addr; vm_dispatcher = 0x8048509
def read_bytecode():
bytecode_addr = 0x804A060; sz = 0x804A107 - 0x804A060 + 1
sym_bytecode_addr = ExprMem(ExprInt(bytecode_addr, 32), sz * 8) # symbolic memory
byte_code = container.bin_stream.getbytes(bytecode_addr, sz) # bytecode from the file
sym_bytecode = ExprInt(int.from_bytes(byte_code, byteorder='little'), sz * 8)
return sym_bytecode_addr, sym_bytecode
sym_bytecode_addr, sym_bytecode = read_bytecode()
sb.symbols[sym_bytecode_addr] = sym_bytecode # fixed the bytecode in the symbolic symbols
sb.symbols[ExprId('ESP', 32)] = ExprInt(0x1000, 32)
arg = ExprMem(expr_simp(sb.symbols[ExprId('ESP', 32)] + ExprInt(4, 32)), 32)
sb.symbols[arg] = ExprInt(0, 32) # set concrete value for arg
sb.run_block_at(ircfg=ircfg, addr=vm_entry)
q = deque([ExprInt(vm_dispatcher, 32)])
while q:
bb = q.popleft()
# print(f'current: {block}')
r = sb.run_block_at(ircfg=ircfg, addr=bb)
print(r)
if r.is_loc() or r.is_int():
q.append(r)
VM Disassembler
When the tracer is ready, We are ready to write a disassembler. As its just a simple VM its handlers are only 1 liner, so you can just analyze it in 5 mins, because to write a full disasembler of this VM we do need to understand the handlers semantics a little bit so we can add information in our disassembler the symbolic execution just helps us to avoid writing each handler semantic implementation but still we should know what each handler.
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
// Short manaual analysis of the simple stack base vm, i have renamed variables and commented what handler does
int __cdecl vm(char arg)
{
int vip; // eax
_DWORD *vstack_ptr; // [esp+8h] [ebp-130h]
char *vbytecode; // [esp+Ch] [ebp-12Ch]
_BYTE vstack[284]; // [esp+10h] [ebp-128h] BYREF
unsigned int canary; // [esp+12Ch] [ebp-Ch]
canary = __readgsdword(0x14u);
vstack_ptr = vstack;
vbytecode = ::vbytecode;
do
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
vip = (unsigned __int8)*vbytecode;
if ( vip != 0xA1 )
break;
++vbytecode;
*(_DWORD *)vstack_ptr[-2u] = *vstack_ptr;// store
vstack_ptr -= 4;
}
if ( (unsigned __int8)*vbytecode <= 0xA1u )
break;
if ( vip == 0xB8 )
{
++vbytecode;
*(vstack_ptr - 2) = *vstack_ptr % *(vstack_ptr - 2);// mod and load
vstack_ptr -= 2;
}
else if ( (unsigned __int8)*vbytecode > 0xB8u )
{
switch ( vip )
{
case 0xF2:
++vbytecode;
*(vstack_ptr - 2) += *vstack_ptr;// add
vstack_ptr -= 2;
break;
case 0xFE:
++vbytecode;
*vstack_ptr = *(_DWORD *)*vstack_ptr;// store
break;
case 0xC2:
++vbytecode;
*(vstack_ptr - 2) = *vstack_ptr == *(vstack_ptr - 2);// sete
vstack_ptr -= 2;
break;
}
}
else if ( vip == 0xAF )
{
vstack_ptr[2] = *(_DWORD *)++vbytecode;// push imm
vstack_ptr += 2;
vbytecode += 4;
}
else if ( vip == 0xB5 )
{
vstack_ptr[2] = &vstack[*(_DWORD *)++vbytecode + 0x100];// mov vtack, [mem]
vstack_ptr += 2;
vbytecode += 4;
}
}
if ( vip != 0x20 )
break;
if ( !*(_DWORD *)++vbytecode )
vstack_ptr[2] = &arg; // mov stack, value (load)
vstack_ptr += 2;
vbytecode += 4;
}
if ( (unsigned __int8)*vbytecode <= 0x20u )
break;
if ( vip == 0x2A )
{
++vbytecode;
if ( *vstack_ptr ) // jnz
vbytecode += *(_DWORD *)vbytecode;
else
vbytecode += 4;
vstack_ptr -= 2;
}
else if ( vip == 0x96 )
{
++vbytecode;
*(vstack_ptr - 2) = *vstack_ptr - *(vstack_ptr - 2);// sub
vstack_ptr -= 2;
}
}
if ( vip != 9 )
break;
++vbytecode;
vbytecode += *(_DWORD *)vbytecode; // jmp
}
}
while ( vip != 0xB );
++vbytecode;
return *vstack_ptr;
}
So we will just now symbolically execute the vm and check after each block that if this address is one of the vm handlers, and if it is, we will print the what ever the semantics of that handler are, and thats how we get out vm disassembler.
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
110
111
112
113
114
115
116
117
118
119
120
121
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.analysis.simplifier import expr_simp
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.expression.expression import ExprMem, ExprInt, ExprId
from collections import deque
file = 'test-mod2-add-virtualized.bin'
addr = 0x80484DE
loc_db = LocationDB()
container = Container.from_stream(open(file, 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
lifter = machine.lifter_model_call(loc_db=loc_db)
sb = SymbolicExecutionEngine(lifter=lifter)
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
vm_entry = addr; vm_dispatcher = 0x8048509
def read_bytecode():
bytecode_addr = 0x804A060; sz = 0x804A107 - 0x804A060 + 1 # is 0xA8
sym_bytecode_addr = ExprMem(ExprInt(bytecode_addr, 32), sz * 8) # symbolic memory
byte_code = container.bin_stream.getbytes(bytecode_addr, sz) # bytecode from the file
sym_bytecode = ExprInt(int.from_bytes(byte_code, byteorder='little'), sz * 8)
return sym_bytecode_addr, sym_bytecode
def resolve_addr(r):
if r.is_int():
return int(r)
if r.is_loc():
return loc_db.get_location_offset(r.loc_key)
return None
HANDLERS = {
0x8048821: 'STORE',
0x80485B0: 'MOD',
0x80485F6: 'ADD',
0x8048639: 'DEREF',
0x8048724: 'SETE',
0x80487B2: 'PUSH',
0x804868B: 'LOAD, [MEM]',
0x80486D7: 'LOAD INPUT',
0x8048856: 'JNZ',
0x804876D: 'SUB',
0x80487F7: 'JMP',
0x804865F: 'HALT'
}
# the disassembler (this vm is so simple that we don't need miasm, but to give you the idea that all the internals of vm like pushing/popping stack values are all done by miasm)
def disassemble(sb, r, vstack, vstack_addr, vip):
global EBP
mnem = HANDLERS[r]
# always expr_simp the address before building the ExprMem key
mem = lambda addr:expr_simp(sb.eval_expr(ExprMem(expr_simp(addr), 32)))
tos = vstack # @32[P] top of vstack
second = mem(vstack_addr - ExprInt(8, 32)) # @32[P - 8] next slot down
imm = mem(vip + ExprInt(1, 32)) # imm32 of the 5-byte opcodes
if mnem == 'STORE': # *(vstack[-2]) = vstack[0]; vsp -= 4 dwords
print(f'{vip} {mnem} [{second}] = {tos}')
elif mnem == 'MOD': # vstack[-2] = vstack[0] % vstack[-2]; vsp -= 2
print(f'{vip} {mnem} {tos} % {second}')
elif mnem == 'ADD': # vstack[-2] += vstack[0]; vsp -= 2
print(f'{vip} {mnem} {second}, {tos}')
elif mnem == 'SUB': # vstack[-2] = vstack[0] - vstack[-2]; vsp -= 2
print(f'{vip} {mnem} {tos} - {second}')
elif mnem == 'SETE': # vstack[-2] = (vstack[0] == vstack[-2]); vsp -= 2
print(f'{vip} {mnem} {tos} == {second}')
elif mnem == 'DEREF': # vstack[0] = *(vstack[0]) -- deref in place
print(f'{vip} {mnem} [{tos}] -> {mem(tos)}')
elif mnem == 'PUSH': # push imm32
print(f'{vip} {mnem} {imm}')
elif mnem == 'LOAD, [MEM]': # push &vstack[imm + 0x100]
slot = expr_simp(ExprInt(0x100, 32) + imm + expr_simp(EBP - ExprInt(0x128, 32))) # &vstack= byte ptr -128h
print(f'{vip} LOAD &vstack[{imm} + 0x100] (= {slot})')
elif mnem == 'LOAD INPUT': # if (imm == 0) push &arg (arg is at EBP+8)
if imm.is_int() and int(imm) == 0:
print(f'{vip} CMOVZ {mnem} input={sb.eval_expr(ExprMem(expr_simp(EBP + ExprInt(8, 32)), 32))}; !zf:{imm}')
elif mnem == 'JNZ': # ++vip; if (vstack[0]) vip += disp else vip += 4; vsp -= 2
print(f'{vip} {mnem} {expr_simp(vip + ExprInt(1, 32) + imm)} ; !zf = {tos}')
elif mnem == 'JMP': # ++vip; vip += disp
print(f'{vip} {mnem} {expr_simp(vip + ExprInt(1, 32) + imm)}')
elif mnem == 'HALT':
print(f'{vip} {mnem} ; ret = {tos}')
sym_bytecode_addr, sym_bytecode = read_bytecode()
sb.symbols[sym_bytecode_addr] = sym_bytecode # fixed the bytecode in the symbolic symbols
sb.symbols[ExprId('ESP', 32)] = ExprInt(0x1000, 32) # arbitrary address
arg = ExprMem(expr_simp(sb.symbols[ExprId('ESP', 32)] + ExprInt(4, 32)), 32)
sb.symbols[arg] = ExprInt(0, 32) # set concrete value for arg
sb.run_block_at(ircfg=ircfg, addr=vm_entry) # initialized all the stack varible etc
EBP = sb.symbols[ExprId('EBP', 32)]
arg = ExprMem(expr_simp(EBP + ExprInt(8, 32)), 32)
sb.symbols[arg] = ExprInt(0, 32) # set concrete value for arg
# we needed vip and vstack to print the information we needed for each handler
vip_local = sb.symbols[ExprMem(EBP - ExprInt(0x12C, 32), 32)]
vstack_local = sb.symbols[ExprMem(EBP - ExprInt(0x130, 32), 32)] # local variables
q = deque([ExprInt(vm_dispatcher, 32)])
while q:
bb = q.popleft()
r = sb.run_block_at(ircfg=ircfg, addr=bb)
vstack_addr = sb.eval_expr(vstack_local); vstack = sb.eval_expr(ExprMem(vstack_local, 32))
hd = resolve_addr(r)
if hd in HANDLERS:
disassemble(sb, hd, vstack=vstack, vstack_addr=vstack_addr, vip=sb.eval_expr(vip_local))
if r.is_loc() or r.is_int():
q.append(r)
# print(r) # tracer
This gives us clean disassembly which you can see here
Tigress CFF
This example has Control Flow Flattening. I thought to change the dummy/simple src to a little bit larget dummy example. Its nothing serious its just 180 LOC and its obfuscated version is 520 LOC
Its a jump table based CFF obfuscator. if we disassemble it via miasm asmcfg then the view is:
We can only see the entry and the dispatcher, it just ends on indirect jump, So we merge the asmcfg with the addresses in jmp table, and we will get the whole asmcfg.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jt = {}
for s in range(jt_count):
idx = va(jt_base + 4 * s) # va is a helper function which uses lief to convert address to raw offset
jt[s] = struct.unpack('<I', raw[idx:idx+4])[0]
state_and_addr = {s: h for s, h in jt.items() if h != disp_addr} # to avoid the loopback branches
asmcfg = mdis.dis_multiblock(target_addr) # incomplete asmcfg (just the dispatcher)
for h in set(state_and_addr.values()): # getting all the state addreses
asmcfg.merge(mdis.dis_multiblock(h)) # merging them on existing incomplete AsmCFG
open('asmcfg.dot', 'w').write(asmcfg.dot()) # we have a full asm cfg now
One thing to note about miasm asm blocks that they split on calls, Forexample:
Same Basic Block in miasm. Its split on each call
General Approach
So we have an initial state at the start of dispatcher block, that state determines which jump to perform from the jump table. After each case execution at the tail state value is updated, but some cases have conditional jumps which can have two distinct state values. Thus effecting the execution flow via a condition.
A simple example of how a single case determines the other state by jcc, fall through will get executed which will have fall state
This is simple case, one state corresponds the other state. Just the whole OBB chain as its unconditional
To deobfuscate this, We will again use the power of Symbolic execution engine. We will extract all splited basic blocks (as miasm basic blocks are splited on call instruction too) as single original basic block. If we encounter any jcc at the any of the case block it means that there is a conditional control flow here, to extract the info we will chain the fall case leaving the jcc case for later recovery. After that we have the full chain of the case statement (if its a jcc then last chain block is the fall case). We iterate through each of the cases and perform symbolic exection on each case by extracting there jcc case too and the last chain value (which is the fall case). So by this we will have the entire chain of execution in uncondtional jmp blocks and both jcc state and fall state if it was an conditional jump block, Which we store as state to state mapping where we know this state can have two possible further states depending upon the jcc and then we do have state to address mapping too.
As miasm treats global data reads as symbolic, We do need to set the jump table to be concerete so that, it resolves to some jump address rather than some symbolic vector.
The jump table lives in the .rodata, We can also just read the jump table directly from binary but i opted lief as it will work for whole sections in the binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
concreteSymbols = {} # we will use that as the argument of add_data
def add_data(concrete, offset, data):
# fill every overlapping qword/dword with concrete values from data
for i in range(0, len(data)-7, 8):
concrete[ExprMem(ExprInt(offset + i, 64), 64)] = ExprInt(u64(data, i), 64)
for i in range(0, len(data)-3, 4):
concrete[ExprMem(ExprInt(offset + i, 32), 32)] = ExprInt(u32(data, i), 32)
def simpExprMemCallBack(expr):
if isinstance(expr, ExprMem):
if expr in concreteSymbols:
return expr.replace_expr({expr: concreteSymbols[expr]})
return None
def simpExprMem(_, expr):
# https://github.com/cea-sec/miasm/blob/master/miasm/expression/expression.py#L614
visitor = ExprVisitorCallbackTopToBottom(simpExprMemCallBack)
return visitor.visit(expr)
The functions that we want, that initialize the data.
1
2
3
4
5
6
7
8
9
10
11
12
rodata = elf.get_section('.rodata') # getting the .rodata of the binary via lief
# This function initilizes all the .rodata in concreteSymbols dict. (tho we need just the jmp table)
add_data(concreteSymbols, rodata.virtual_address, bytes(rodata.content)) # rodata virtuall address as offset
# Custom simplification pass
custom_simp = ExpressionSimplifier()
custom_simp.enable_passes(ExpressionSimplifier.PASS_COMMONS)
custom_simp.enable_passes(ExpressionSimplifier.PASS_HIGH_TO_EXPLICIT)
custom_simp.enable_passes({ExprMem: [simpExprMem]})
Custom simplification pass which contains the concerete data we need, we just need to pass it to symbolic execution engine
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
def case_chain(case_loc):
chain = [case_loc]
cur = case_loc
while True:
bb = asmcfg.loc_key_to_block(cur)
if bb.lines[-1].name in ('JMP', 'RET'):
break
succs = asmcfg.successors(cur)
if bb.lines[-1].name.startswith('J') and len(succs) == 2: # if we get a jcc (then we have 2 successors)
cur = fall_through(bb, succs) # than we append the fall case in our chain.
else:
cur = succs[0]
if cur in DISPATCHER_LOCS:
break
chain.append(cur) # append all the block in the chain (and fall in case of jcc)
return chain
def fall_through(bb, succs): # succ is a lockey
tgt = bb.lines[-1].args[0] # getting path to jcc location
if isinstance(tgt, ExprLoc): tgt_loc = tgt.loc_key
else: tgt_loc = tgt
others = [s for s in succs if s != tgt_loc] # the successors which isn't the jcc
assert len(others) == 1
return others[0] # returning the fall case
Now as we have discussed how we will connect the OBB via chaining and how to fall through works, Now we will just pick the state variable and see how we will perform symbolic execution on each case.
1
2
3
.text:080486C3 dispatcher: ; switch 33 cases
.text:080486C3 cmp [ebp+state], 20h ; [ebp - 0x38]
.text:080486C7 ja short dispatcher
1
2
3
4
5
6
disp_addr = 0x80486C3
dispatch_loc = loc_db.get_offset_location(disp_addr) # our state variable read is from this block is in this block
disp_block = asmcfg.loc_key_to_block(dispatch_loc)
state_variable = disp_block.lines[0].get_args_expr()[0] # as the state cmp is at first line of dispatcher, and its first expr
print('STATE VAR:', state_variable)
We get the correct state
1
STATE VAR: @32[EBP + 0xFFFFFFC8]
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
# performed on each case
def discover_case(case_loc, chain):
case_block = asmcfg.loc_key_to_block(case_loc)
last = case_block.lines[-1]
if last.name.startswith('J') and len(chain) == 2: # as the jcc cases only had 2 chains.
# passing our custom simplification pass for concerete data
sb = SymbolicExecutionEngine(lifter, init_symbols, sb_expr_simp=custom_simp)
jcc_target_loc = to_loc_key(last.args[0]) # getting the jcc target location
side_state = symb_exe_to_state(sb, jcc_target_loc) # next state value after the jcc case
fall_state = symb_exe_to_state(sb, chain[-1]) # next state value after the fall case
return [fall_state], {jcc_target_loc: side_state} # the fall state is the next state and along side we have also calculated the jcc
sb = SymbolicExecutionEngine(lifter, init_symbols, sb_expr_simp=custom_simp) # if the case was simple unconditional jmp (fixed state conversion)
nxt = symb_exe_to_state(sb, chain[-1]) # direct unconditional state update
if nxt is not None:
return [nxt], {} # just the nxt because it has no jcc case
else:
return [], {} # no next as its a return block
def symb_exe_to_state(sb, loc):
r = sb.run_block_at(ircfg=ircfg, addr=loc) # perform symbolic execution of the a basic block of location
r = to_loc_key(r)
if r in DISPATCHER_LOCS: # if dispatcher and pre-dispathcer
v = sb.eval_expr(state_variable) # get the state value which is updated during symbolic execution
return int(v) if isinstance(v, ExprInt) else None
if r == ret_loc: # whent he next basic block is the return block
return None
return symb_exe_to_state(sb, r) # repeat the process to process multichain blocks
These are peformed on each jump table location getting information on each of the case block on where it falls and where it jumps. Then we do have all necessary information to delete the irrelevant basic block and only keep the Original Basic Blocks which is union of only the prologue block, case blocks and return block. All the irrelevant Basic blocks will be removed
1
2
3
4
5
6
7
def remove_irrelevant_BB(original_BB):
irrelevant_locations = []
for bb in asmcfg.blocks:
if bb.loc_key not in original_BB:
irrelevant_locations.append(bb.loc_key)
for loc in irrelevant_locations:
asmcfg.del_block(asmcfg.loc_key_to_block(loc))
Each iteration of the symbolic execution on each individial case block stores the info in three python dictonary, I have modified the ollvm-unflattener binrewriter to make this tigress cff unflattener work which you can see here, Now if we just run our deobfuscator we get a fully unflattened function
This is the whole deobfuscation script
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
from miasm.core.locationdb import LocationDB, LocKey
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.expression.simplifications import ExpressionSimplifier, expr_simp
from miasm.expression.expression import ExprInt, ExprMem, ExprLoc, ExprVisitorCallbackTopToBottom
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.arch.x86.regs import all_regs_ids, all_regs_ids_init
from collections import defaultdict
from binrewrite_t import TBinaryRewriter
import struct
import lief
file = 'testing-flatten.bin'
target_addr = 0x80486B6
disp_addr = 0x80486C3
pre_disp_addr = 0x8048B57
ret_addr = 0x8048B5C
jt_base = 0x8048F20
jt_count = 0x21
ENTRY_STATE = -2
EXIT_STATE = -1
elf = lief.ELF.parse(file)
loc_db = LocationDB()
container = Container.from_stream(open(file, 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
lifter = machine.lifter_model_call(loc_db=loc_db)
va = lambda addr: elf.virtual_address_to_offset(addr)
raw = open(file, 'rb').read()
jt = {}
for s in range(jt_count):
idx = va(jt_base + 4 * s)
jt[s] = struct.unpack('<I', raw[idx:idx+4])[0]
state_and_addr = {s: h for s, h in jt.items() if h != disp_addr}
asmcfg = mdis.dis_multiblock(target_addr)
for h in set(state_and_addr.values()):
asmcfg.merge(mdis.dis_multiblock(h))
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg) # lifting the full asmcfg to full ircfg
def u64(data, i):
return struct.unpack('<Q', data[i:i + 8])[0]
def u32(data, i):
return struct.unpack('<I', data[i:i + 4])[0]
concreteSymbols = {}
def add_data(symb, offset, data):
# fill every overlapping qword/dword with concrete values from data
for i in range(0, len(data)-7, 8):
symb[ExprMem(ExprInt(offset + i, 64), 64)] = ExprInt(u64(data, i), 64)
for i in range(0, len(data)-3, 4):
symb[ExprMem(ExprInt(offset + i, 32), 32)] = ExprInt(u32(data, i), 32)
def simpExprMemCallBack(expr):
if isinstance(expr, ExprMem):
if expr in concreteSymbols:
return expr.replace_expr({expr: concreteSymbols[expr]})
return None
def simpExprMem(_, expr):
# https://github.com/cea-sec/miasm/blob/master/miasm/expression/expression.py#L614
visitor = ExprVisitorCallbackTopToBottom(simpExprMemCallBack)
return visitor.visit(expr)
def to_loc_key(expr):
if isinstance(expr, LocKey): return expr
if isinstance(expr, ExprLoc): return expr.loc_key
if isinstance(expr, int): return loc_db.get_offset_location(expr)
if isinstance(expr, ExprInt): return loc_db.get_offset_location(int(expr))
return None
rodata = elf.get_section('.rodata')
add_data(concreteSymbols, rodata.virtual_address, bytes(rodata.content))
custom_simp = ExpressionSimplifier()
custom_simp.enable_passes(ExpressionSimplifier.PASS_COMMONS)
custom_simp.enable_passes(ExpressionSimplifier.PASS_HIGH_TO_EXPLICIT)
custom_simp.enable_passes({ExprMem: [simpExprMem]})
pre_disp_loc = loc_db.get_offset_location(pre_disp_addr)
dispatch_loc = loc_db.get_offset_location(disp_addr)
ret_loc = loc_db.get_offset_location(ret_addr)
DISPATCHER_LOCS = {dispatch_loc, pre_disp_loc}
disp_block = asmcfg.loc_key_to_block(dispatch_loc)
state_variable = disp_block.lines[0].get_args_expr()[0]
print('STATE VAR:', state_variable)
init_symbols = {}
for i, r in enumerate(all_regs_ids):
init_symbols[r] = all_regs_ids_init[i]
def remove_irrelevant_BB(original_BB):
irrelevant_locations = []
for bb in asmcfg.blocks:
if bb.loc_key not in original_BB:
irrelevant_locations.append(bb.loc_key)
for loc in irrelevant_locations:
asmcfg.del_block(asmcfg.loc_key_to_block(loc))
def fall_through(bb, succs): # succ is a lockey
tgt = bb.lines[-1].args[0] # getting path to jcc location
if isinstance(tgt, ExprLoc): tgt_loc = tgt.loc_key
else: tgt_loc = tgt
others = [s for s in succs if s != tgt_loc] # the successors which isn't the jcc
assert len(others) == 1
return others[0] # returning the fall case
def case_chain(case_loc):
chain = [case_loc]
cur = case_loc
while True:
bb = asmcfg.loc_key_to_block(cur)
if bb.lines[-1].name in ('JMP', 'RET'):
break
succs = asmcfg.successors(cur)
if bb.lines[-1].name.startswith('J') and len(succs) == 2: # if we get a jcc (then we have 2 successors)
cur = fall_through(bb, succs) # than we append the fall case in our chain.
else:
cur = succs[0]
if cur in DISPATCHER_LOCS:
break
chain.append(cur) # append all the block in the chain (and fall in case of jcc)
return chain
def symb_exe_to_state(sb, loc):
r = sb.run_block_at(ircfg=ircfg, addr=loc)
r = to_loc_key(r)
if r in DISPATCHER_LOCS:
v = sb.eval_expr(state_variable)
return int(v) if isinstance(v, ExprInt) else None
if r == ret_loc:
return None
return symb_exe_to_state(sb, r)
def discover_case(case_loc, chain):
case_block = asmcfg.loc_key_to_block(case_loc)
last = case_block.lines[-1]
if last.name.startswith('J') and len(chain) == 2: # as the jcc cases only had 2 chains.
sb = SymbolicExecutionEngine(lifter, init_symbols, sb_expr_simp=custom_simp)
jcc_target_loc = to_loc_key(last.args[0]) # getting the jcc target location
side_state = symb_exe_to_state(sb, jcc_target_loc) # jcc state
fall_state = symb_exe_to_state(sb, chain[-1]) # fall state from the last chain
return [fall_state], {jcc_target_loc: side_state} # the fall state is the next state and along side we have also calculated the jcc
sb = SymbolicExecutionEngine(lifter, init_symbols, sb_expr_simp=custom_simp) # if the case was simple unconditional jmp (fixed state conversion)
nxt = symb_exe_to_state(sb, chain[-1]) # direct unconditional state update
if nxt is not None:
return [nxt], {} # just the nxt because it has no jcc case
else:
return [], {} # no next as its a return block
state_to_state_map = defaultdict(list)
state_to_addr_map = defaultdict(list)
jcc_targets_map = defaultdict(dict)
# entry/prologue pseudo-state: run the entry block -> first dispatched state
entry_loc = loc_db.get_offset_location(target_addr)
state_to_addr_map[ENTRY_STATE] = case_chain(entry_loc)
sb0 = SymbolicExecutionEngine(lifter, init_symbols, sb_expr_simp=custom_simp)
first_state_val = symb_exe_to_state(sb0, entry_loc)
assert first_state_val is not None
state_to_state_map[ENTRY_STATE].append(first_state_val)
print('entry chain:', [hex(loc_db.get_location_offset(x)) for x in state_to_addr_map[ENTRY_STATE]], '-> first state', hex(first_state_val))
for state_val, case_addr in sorted(state_and_addr.items()):
case_loc = loc_db.get_offset_location(case_addr)
chain = case_chain(case_loc)
state_to_addr_map[state_val] = chain # containes a whole block and only fall if the the block splits into conditional
nexts, jcc = discover_case(case_loc, chain)
state_to_state_map[state_val] = nexts
if jcc:
jcc_targets_map[state_val] = jcc
print('state {:2d} chain={} -> fall={} jcc={}'.format( # debug/info print
state_val,
[hex(loc_db.get_location_offset(x)) for x in chain],
nexts,
{hex(loc_db.get_location_offset(k)): v for k, v in jcc.items()} if jcc else None))
# exit pseudo-state: the ret block. Terminal cases jump straight to it
# (no state store involved), so it cannot be a jump-table state.
state_to_addr_map[EXIT_STATE] = [ret_loc]
state_to_state_map[EXIT_STATE] = [] # as there is no next state
for s, ns in state_to_state_map.items():
if not ns and s != EXIT_STATE: # if there is no next state and current state is not an exit state, make that state and exit state.
state_to_state_map[s] = [EXIT_STATE]
# OBB = union of all case chains (+ prologue, + ret block)
OBB = []
for locations in state_to_addr_map.values():
OBB.extend(locations)
print('OBB blocks:', len(OBB), 'of', len(asmcfg.blocks))
remove_irrelevant_BB(original_BB=OBB)
# Patch
rewriter = TBinaryRewriter(container.arch, file, va_function_lief=va)
rewriter.patch_binary(asmcfg, target_addr, state_to_state_map,state_to_addr_map, jcc_targets_map,ENTRY_STATE, EXIT_STATE, sb0, file + '.deob',state_variable=state_variable)
Range Dividers
Sometimes to increase the function size and the CFG, There is range-dividers obfuscations. which is basically replicating the basic block functionality. Hence we perform Equivalence checking.
A method to determine if two given BB have the same behavior via Syntactically-equivalent & Semantically-equivalent.
Approach
We will iterate through each basic block, And for each basic block we will check all the other basic block of that function that are they Syntactically-equivalent or Semantically-equivalent Hence the Basic blocks are equivalent.
First check the Syntax-equivalence
1
2
3
4
5
6
7
8
9
10
11
12
def syntax_cmp(src_asmbb, dst_asmbb):
if len(src_asmbb.lines) != len(dst_asmbb.lines):
return False
for src_l, dst_l in zip(src_asmbb.lines, dst_asmbb.lines):
if str(src_l).startswith('J') and str(dst_l).startswith('J'): # if its jmp instruction
if str(src_l).split(' ')[0] != str(dst_l).split(' ')[0]: # don't care about the jmp location
return False
elif str(src_l) != str(dst_l): # if the instructions are not jmp or jcc and they are not same
return False
return True # all passed hence same instructions
If the syntax_cmp returns False, We check semantic equivalence, We will have two lifter to lift each block, because we want two different IRCFG. We will perform symbolic execution on each IR Block. and check if they are sat.
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
def semantic_cmp(src_asmbb, dst_asmbb, lifter_s, lifter_d):
src_ircfg = IRCFG(None, src_asmbb.loc_key)
lifter_s.add_asmblock_to_ircfg(block=src_asmbb, ircfg=src_ircfg)
dst_ircfg = IRCFG(None, dst_asmbb.loc_key)
lifter_d.add_asmblock_to_ircfg(block=dst_asmbb, ircfg=dst_ircfg)
src_irbb = src_ircfg.blocks[next(iter(src_ircfg.blocks))] # get the source IR basic block
dst_irbb = dst_ircfg.blocks[next(iter(dst_ircfg.blocks))] # get the destination IR basic block
# Ready for Symbolic Execution
src_symbols = {}; dst_symbols = {}
# regs
for i, r in enumerate(all_regs_ids):
src_symbols[r] = all_regs_ids_init[i]; dst_symbols[r] = all_regs_ids_init[i] # init symbols for both blocks
src_sb = SymbolicExecutionEngine(lifter_s, src_symbols)
for assignblk in src_irbb:
assign = True
for dst in assignblk:
if str(dst) in ['EIP', 'IRDst']:
assign = False
if assign:
src_sb.eval_updt_assignblk(assignblk)
dst_sb = SymbolicExecutionEngine(lifter_d, dst_symbols)
for assignblk in dst_irbb:
assign = True
for dst in assignblk:
if str(dst) in ['EIP', 'IRDst']:
assign = False
if assign:
dst_sb.eval_updt_assignblk(assignblk)
src_sb.del_mem_above_stack(lifter_s.sp)
dst_sb.del_mem_above_stack(lifter_d.sp)
all_memory_ids = [k for k, v in dst_sb.symbols.memory()] + [k for k, v in src_sb.symbols.memory()]
for k in all_regs_ids + all_memory_ids:
if str(k) == 'EIP' or k in [zf, nf, pf, of, cf, af, df, tf]:
continue
v0 = src_sb.symbols[k] # source symbol value
v1 = dst_sb.symbols[k] # destination symbol value
if v0 == v1:
continue
s = z3.Solver()
t = TranslatorZ3()
try:
r = t.from_expr(v0); l = t.from_expr(v1)
except NotImplementedError:
return False
s.add(z3.Not(r == l)) # in no such case r == l
if s.check() == z3.unsat: # means that r == l in all cases not only one
continue
else:
return False
# all symbols and memory has been iterated and every case was unsat for r != l, it means that both are equal in both cases.
return True
Zeus VM Devirtualization
I picked this sample because it was used in miasm blog in 2016. But that post uses old depreciated miasm2 API and also that is more focused on recovery of handler semantics via symbolic execution and getting result of each handler in miasm IR instead of a de-virtualizer.
We will cover its string deobfuscation and write a functional disassembler which yields some usefull information. Which is compelete different approach from that blog :)
String Decryption
during analysis we see this function frequently
If we look inside
The string encrypted table has 4 bytes of key, size of the encrypted string and the address to the encrypted string bytes.
Setting the struct in IDA
1
2
3
4
5
6
struct enc
{
_DWORD key;
_DWORD size;
_BYTE *string;
};
For testing that it works i did the first manually.
1
2
3
4
5
6
7
8
k = bytes.fromhex('6001BE89')
sz = 9
enc = bytes.fromhex('606E6C6C7C6968737A00')
dec = []
for i in range(sz):
dec.append(i ^ enc[i] ^ k[sz & 3])
print(''.join(map(chr, dec)))
1
2
$ python string_dec.py
anonymous
1
2
3
4
5
6
7
8
9
10
# translation in ida python
def decrypt_string(off):
enc_struct = 0x4019E0 + 12 * off
k = ida_bytes.get_bytes(enc_struct, 4); size = idc.get_wide_dword(enc_struct+4); enc_addr = idc.get_wide_dword(enc_struct+8)
enc = ida_bytes.get_bytes(enc_addr, size=size)
key = k[size & 3]; dec = []
for i in range(size):
dec.append(i ^ enc[i] ^ key)
return bytes(dec).decode()
as the encrypted string table is referenced in two function means both are string decryption functions, we will use ida python and get the first argument from all of the xrefs and after that we can perform string decryption in ida and convert them into a enum, So we will have the decrypted string at each function argument.
Full string decryption to enum script
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import idc
import ida_bytes
import ida_funcs
import idautils
import ida_hexrays
import re
def get_first_arg(call_ea):
func = ida_funcs.get_func(call_ea)
cfunc = ida_hexrays.decompile(func)
class ArgFinder(ida_hexrays.ctree_visitor_t): # custom visitor to walk the ctree
def __init__(self):
ida_hexrays.ctree_visitor_t.init(self, ida_hexrays.CV_FAST) # faster traversal
self.result = None # will hold the first arg's value if found
def visit_expr(self, e):
# look for a call expression whose address matches our target call_ea
if e.op == ida_hexrays.cot_call and e.ea == call_ea:
if e.a.size() > 0: # e.a is the call's argument list (carglist_t)
arg0 = e.a[0] # first arg
if arg0.op == ida_hexrays.cot_num: # if its a constant
self.result = arg0.numval()
return 1 # stop traversal
return 0 # continue traversal
af = ArgFinder()
af.apply_to(cfunc.body, None) # run the visitor over the whole function body
return af.result # return the found constant, or None if not found/not a constant
xrefs = [ref.frm for ref in idautils.XrefsTo(0x416951)] + [ref.frm for ref in idautils.XrefsTo(0x416901)]
def decrypt_string(off, xref):
if xref == 0x41c73d: return None # some junk byte
# print(hex(xref)) # debug print
enc_struct = 0x4019E0 + 12 * off
k = ida_bytes.get_bytes(enc_struct, 4); size = idc.get_wide_dword(enc_struct+4); enc_addr = idc.get_wide_dword(enc_struct+8)
enc = ida_bytes.get_bytes(enc_addr, size=size)
key = k[size & 3]; dec = []
for i in range(size):
dec.append(i ^ enc[i] ^ key)
return bytes(dec).decode()
enum_dict = {}
def string_decryptor():
for xref in xrefs:
offset = get_first_arg(xref)
if not offset or offset in enum_dict:
# print(hex(xref), offset)
continue
decrypted = decrypt_string(offset, xref)
if decrypted:
enum_dict[offset] = decrypted
dict_to_enum(enum_dict)
def dict_to_enum(d, enum_name="zeus_string_dec"):
used = set()
lines = [f"enum {enum_name}", "{"]
for key in sorted(d):
s = d[key]
# Uppercase
name = s.upper()
# Common replacements
replacements = {
"%S": "FMT_S",
"%U": "FMT_U",
"%D": "FMT_D",
"%X": "FMT_X",
"%08X": "FMT_08X",
"%02X": "FMT_02X",
"%C": "FMT_C",
"%P": "FMT_P",
"*": "STAR",
"\\": "_",
"/": "_",
".": "_",
":": "_",
"-": "_",
" ": "_",
"\t": "_",
"\r": "_",
"\n": "_",
'"': "",
"'": "",
"(": "",
")": "",
"[": "",
"]": "",
"{": "",
"}": "",
",": "_",
";": "_",
"=": "_",
"+": "_",
"&": "_AND_",
"|": "_OR_",
"?": "_",
"!": "_",
"@": "AT",
}
for old, new in replacements.items():
name = name.replace(old, new)
# Remove anything still invalid
name = re.sub(r'[^A-Z0-9_]', '_', name)
# Collapse repeated underscores
name = re.sub(r'_+', '_', name).strip("_")
# Cannot start with digit
if name[0].isdigit():
name = "_" + name
# Ensure uniqueness
base = name
i = 2
while name in used:
name = f"{base}_{i}"
i += 1
used.add(name)
lines.append(f" {name} = 0x{key:X},")
lines.append("};")
with open(enum_name+'.c', 'w') as f:
f.write("\n".join(lines))
string_decryptor()
After setting the first argument type to the decrypted enum, we will have all the string decrypted in the argument :)
De-Virt
After some analysis we encounter the vm function.
some analysis yields the vm struct
1
2
3
4
5
6
7
struct vm
{
_BYTE *vpc;
_BYTE *mem;
_DWORD flag;
_DWORD reg[16];
};
As the vm context is going to the function argument of the dispatcher hence all the handlers function arg should be the vm ctx
1
2
3
4
5
6
7
import idc
table = 0x427018
functions = [idc.get_wide_dword(table + i * 4) for i in range(69)] # getting all the handlers
for f in functions:
idc.SetType(f, 'char __thiscall function(vm *this)') # set a function type
The handlers are simple, nothing complicated, each handler decryptes the bytecode at runtime with a custom key, So symbolic execution is our way to go. We need to set the bytecode and memcode as concerete values. As all of the handlers access the bytecode and memcode via the vm struct, so we will set struct in miasm at a arbitrary address, and initialize the member of struct as they were initialized in the vm entry.
This is the basic miasm setup we need for zeus vm.
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
VM_CTX = 0x10000000 # arbitrary address
VIP, MEM, FLAG, REG = 0x00, 0x04, 0x08, 0x0C # struct offsets
BYTECODE = 0x403368
MEMCODE = 0x4030E0
sb = SymbolicExecutionEngine(lifter=lifter)
def write_address(addr, val, size=32): # write address on a member of struct
sb.symbols[ExprMem(ExprInt(addr, 32), size)] = ExprInt(val, size)
ctx = lambda off: expr_simp(sb.eval_expr(ExprMem(ExprInt(VM_CTX + off, 32), 32))) # access the member address of the vm context
read_value_n = lambda addr_expr, size=8: expr_simp(sb.eval_expr(ExprMem(expr_simp(addr_expr), size))) # derence the member
# init bytecode, memcode, flags in the vm ctx
write_address(VM_CTX + VIP, BYTECODE); write_address(VM_CTX + MEM, MEMCODE); write_address(VM_CTX + FLAG, 0)
# 16 flags
for i in range(16):
write_address(VM_CTX + REG + i * 4, 0, size=32)
# populating the bytecode addr with concerete bytecode
data = container.bin_stream.getbytes(BYTECODE, 0x1000)
for i, b in enumerate(data):
sb.symbols[ExprMem(ExprInt(BYTECODE + i, 32), 8)] = ExprInt(b, 8)
# populating the memcode addr with concerete memcode
data = container.bin_stream.getbytes(MEMCODE, 0x288)
for i, b in enumerate(data):
sb.symbols[ExprMem(ExprInt(MEMCODE + i, 32), 8)] = ExprInt(b, 8)
The reason why we initialize this at a byte granularity because miasm will read the dword/word or any reference automatically as long each address should have concerete byte value, also we didnโt use the tigress CFF method of concerete values because we will use only one Symbolic execution engine through out. If we had multiple symbolic execution engines this would fail.
While analysis of handlers to understand some semantics of handlers so we can write a disassembler. All the handlers are simple/simmilar enough, one handler was a little of so lets see that that is.
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
char __thiscall sub_40F2E2(vm *this)
{
char v2; // bl
unsigned __int8 op; // al
_BYTE *vpc; // ecx
unsigned __int8 n_op; // dl
char v6; // bl
int init_buffer; // eax
int v8; // edi
int v9; // eax
char *v10; // esi
int buffer[65]; // [esp+Ch] [ebp-108h] BYREF
unsigned __int8 next_op; // [esp+112h] [ebp-2h]
unsigned __int8 v14; // [esp+113h] [ebp-1h]
v2 = this->vpc[3];
op = this->vpc[1];
vpc = this->vpc;
n_op = vpc[2];
v14 = op;
v6 = v2 ^ 0xCC;
next_op = n_op;
init_buffer = sub_409281((int)buffer, vpc + 3, op);
v8 = next_op;
sub_4092E4(init_buffer, (int)this->mem, next_op);
v9 = v14;
this->mem += v8;
this->vpc += v9 + 3;
v10 = this->vpc;
if ( *v10 < 0 )
*v10 = (v6 ^ *v10) & 0x7F;
return 1;
}
And these two inside functions are:
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
int __userpurge sub_409281@<eax>(int result@<eax>, int a2, __int16 a3)
{
int n0x100; // ecx
_BYTE *v4; // esi
char *v5; // esi
int n256; // edi
char v7; // dl
unsigned __int8 v8; // [esp+Eh] [ebp-2h]
unsigned __int8 v9; // [esp+Fh] [ebp-1h]
n0x100 = 0;
v9 = 0;
v8 = 0;
*(_WORD *)(result + 256) = 0;
v4 = (_BYTE *)result;
do
*v4++ = n0x100++;
while ( (unsigned __int16)n0x100 < 0x100u );
v5 = (char *)result;
n256 = 256;
do
{
v7 = *v5;
v8 += *v5 + *(_BYTE *)(v9++ + a2);
*v5 = *(_BYTE *)(v8 + result);
*(_BYTE *)(v8 + result) = v7;
if ( v9 == a3 )
v9 = 0;
++v5;
--n256;
}
while ( n256 );
return result;
}
int __userpurge sub_4092E4@<eax>(int result@<eax>, int a2, unsigned int a3)
{
unsigned int v3; // edi
char v4; // dl
unsigned __int8 i; // [esp+6h] [ebp-2h]
unsigned __int8 v6; // [esp+7h] [ebp-1h]
v6 = *(_BYTE *)(result + 256);
v3 = 0;
for ( i = *(_BYTE *)(result + 257); v3 < a3; ++v3 )
{
v4 = *(_BYTE *)(++v6 + result);
i += v4;
*(_BYTE *)(v6 + result) = *(_BYTE *)(i + result);
*(_BYTE *)(i + result) = v4;
*(_BYTE *)(a2 + v3) ^= *(_BYTE *)((unsigned __int8)(v4 + *(_BYTE *)(v6 + result)) + result);
}
*(_BYTE *)(result + 256) = v6;
*(_BYTE *)(result + 257) = i;
return result;
}
After analyzing them we know its RC4 handler:
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
char __thiscall h_rc4_handler(vm *this)
{
char v2; // bl
unsigned __int8 op; // al
_BYTE *vpc; // ecx
unsigned __int8 n_op; // dl
char v6; // bl
int init_buffer; // eax
int v8; // edi
int v9; // eax
char *v10; // esi
int buffer[65]; // [esp+Ch] [ebp-108h] BYREF
unsigned __int8 next_op; // [esp+112h] [ebp-2h]
unsigned __int8 v14; // [esp+113h] [ebp-1h]
v2 = this->vpc[3];
op = this->vpc[1];
vpc = this->vpc;
n_op = vpc[2];
v14 = op;
v6 = v2 ^ 0xCC;
next_op = n_op;
init_buffer = mw_rc4_KSA((int)buffer, vpc + 3, op);
v8 = next_op;
mw_rc4_decrypt(init_buffer, (int)this->mem, next_op);
v9 = v14;
this->mem += v8;
this->vpc += v9 + 3;
v10 = this->vpc;
if ( *v10 < 0 )
*v10 = (v6 ^ *v10) & 0x7F;
return 1;
}
RC4 Key Scheduling algorithm
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
int __userpurge mw_rc4_KSA@<eax>(_BYTE *buffer@<eax>, _BYTE *nn_op_key, __int16 op_ksz)
{
int ii; // ecx
_BYTE *buffer_ptr; // esi
char *buff_ptr; // esi
int size; // edi
char idx; // dl
unsigned __int8 value; // [esp+Eh] [ebp-2h]
unsigned __int8 k_idx; // [esp+Fh] [ebp-1h]
ii = 0;
k_idx = 0;
value = 0;
*((_WORD *)buffer + 128) = 0;
buffer_ptr = buffer;
do
*buffer_ptr++ = ii++;
while ( (unsigned __int16)ii < 256u );
buff_ptr = buffer;
size = 256;
do
{
idx = *buff_ptr;
value += *buff_ptr + nn_op_key[k_idx++];
*buff_ptr = buffer[value];
buffer[value] = idx;
if ( k_idx == op_ksz )
k_idx = 0;
++buff_ptr;
--size;
}
while ( size );
return (int)buffer;
}
RC4 Decrypt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __userpurge mw_rc4_decrypt(_BYTE *init_buffer@<eax>, _BYTE *mem, unsigned int n_op_sz)
{
unsigned int idx; // edi
char v4; // dl
unsigned __int8 i; // [esp+6h] [ebp-2h]
unsigned __int8 v6; // [esp+7h] [ebp-1h]
v6 = init_buffer[256];
idx = 0;
for ( i = init_buffer[257]; idx < n_op_sz; ++idx )
{
v4 = init_buffer[++v6];
i += v4;
init_buffer[v6] = init_buffer[i];
init_buffer[i] = v4;
mem[idx] ^= init_buffer[(unsigned __int8)(v4 + init_buffer[v6])];
}
init_buffer[256] = v6;
init_buffer[257] = i;
}
Its decrypting the memory in place, so we have to execute the sun function in the handler. But when we lift the ircfg from asmcfg, the calls are not followed, So when we run symbolic execution, that call is not executed symbolically. But in this case we do need to execute the whole handler end to end including the function as KSA and Decrypt used in here. So we will use a dictonary which will contains the ircfg of each handler, We will prelift the RC4 Handler with follow call so the ircfg can follow its callee too, and the decryption will happen in the Symbolic Execution Engine memory, this will save the time as the disassembler will only have to lift each handler once and then reuse it. when ever the virtual machine symbolic execution needs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RC4_H = 0x40F2E2
LIFTED = {}
# prelifting rc4 handler
mdis.follow_call = True
rc4asmcfg = mdis.dis_multiblock(RC4_H)
rc4ircfg = unmod_lifter.new_ircfg_from_asmcfg(rc4asmcfg)
mdis.follow_call = False
LIFTED[RC4_H] = rc4ircfg
# lifted will be called on each handler
def lifted(addr):
if addr in LIFTED:
return LIFTED[addr] # if that particular handler is pre-lifted
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
LIFTED[addr] = ircfg # if not than lift and store for future
return ircfg
Now we will simulate the vm loop, starting from the vm bytecode, and each loop will perform symbolic execution on each of the handler and on each iteration we will call the disassemble function, which will check which handler is next pick its mnemonic from the VM Handler dictonary. This will log the handler semantic in the form of the instruction, I am not including the analysis of handlers as they are 69 and its left for the reader as exercise xD.
Simulation of the vm loop
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
HALT = False
while not HALT: # vm dispatcher
idx = int(read_value_n(ctx(VIP))) # reads Virtual Instruction pointer and derefrence 8 bits (1st byte which is at the start of symbolic execution engine)
disassemble(idx) # log the handler semantics in a text file
handler_addr = HANDLERS[idx][0]
ircfg = lifted(handler_addr)
if handler_addr == 0x40EE37: HALT = True # halt handler
sb.symbols[lifter.arch.regs.ECX] = ExprInt(VM_CTX, 32) # each handler has vm context in the first argument as its a __thiscall we store it in ecx
sb.symbols[lifter.arch.regs.ESP] = ExprInt(STACK_TOP - 4, 32) # set a flag constant so symbolic execution exits cleanly on handler return
q = deque([ExprInt(handler_addr, 32)]) # adding the handler to be executed
symbolic_execution(q, ircfg)
def symbolic_execution(q, ircfg):
while q: # symbolically execute all the BB of the handlers
handler_bb = q.popleft()
r = sb.run_block_at(ircfg=ircfg, addr=handler_bb)
# print(r)
if r.is_int() and int(r) == RETN: # handler ended
break
elif r.is_int() or r.is_loc():
q.append(r)
C2 config download URL
After executing our script we have compelete disassembly (3k LOC) of the zeus VM which you can see here,
There are many RC4 decrytion on the memory, So lets read the memory and see what decrypted inside.
1
2
3
4
5
data = bytearray()
for i in range(0x288):
v = read_value_n(ExprInt(MEMCODE + i, 32), 8)
data.append(int(v))
open('mem_dec.bin', 'wb').write(data) # writes the decrypted MEMCODE
Indeed we get something usefull in the memory. its .cfg http://rxfkxmtqxg.com/ppcrzaezqs/cfg.bin We have succesfully extrated the zeus C2 config download URL
Here is the complete zeus vm deobfuscation script
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.expression.simplifications import expr_simp
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.expression.expression import ExprId, ExprMem, ExprInt
from collections import deque
VM_CTX = 0x10000000 # arbitrary address
VIP, MEM, FLAG, REG = 0x00, 0x04, 0x08, 0x0C # struct offsets
BYTECODE = 0x403368
MEMCODE = 0x4030E0
STACK_TOP = 0x1000
RETN = 0xDEADBEEF
RC4_H = 0x40F2E2
LIFTED = {}
loc_db = LocationDB()
container = Container.from_stream(open('zeusvm.bin', 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
lifter = machine.lifter_model_call(loc_db=loc_db)
unmod_lifter = machine.lifter(loc_db=loc_db)
sb = SymbolicExecutionEngine(lifter=lifter)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
# prelifting rc4 handler
mdis.follow_call = True
rc4asmcfg = mdis.dis_multiblock(RC4_H)
rc4ircfg = unmod_lifter.new_ircfg_from_asmcfg(rc4asmcfg)
mdis.follow_call = False
LIFTED[RC4_H] = rc4ircfg
def write_address(addr, val, size=32):
sb.symbols[ExprMem(ExprInt(addr, 32), size)] = ExprInt(val, size)
def lifted(addr):
if addr in LIFTED:
return LIFTED[addr]
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
LIFTED[addr] = ircfg
return ircfg
def symbolic_execution(q, ircfg):
while q: # symbolically execute all the BB of the handlers
handler_bb = q.popleft()
r = sb.run_block_at(ircfg=ircfg, addr=handler_bb)
# print(r)
if r.is_int() and int(r) == RETN: # handler ended
break
elif r.is_int() or r.is_loc():
q.append(r)
ctx = lambda off: expr_simp(sb.eval_expr(ExprMem(ExprInt(VM_CTX + off, 32), 32)))
read_value_n = lambda addr_expr, size=8: expr_simp(sb.eval_expr(ExprMem(expr_simp(addr_expr), size)))
# just included the mnemonics (i don't know why, i could have written them in the disassemble function too lol)
HANDLERS = {
0x0: (0x40ede0, 'NOP1'),
0x1: (0x40edfb, 'NOP2'),
0x2: (0x40ee19, 'NOP3'),
0x3: (0x40ee3a, 'XOR BYTE PTR'),
0x4: (0x40ee64, 'XOR WORD PTR'),
0x5: (0x40ee92, 'XOR DWORD PTR'),
0x6: (0x40eebe, 'ADD BYTE PTR'),
0x7: (0x40eee8, 'ADD WORD PTR'),
0x8: (0x40ef16, 'ADD DWORD PTR'),
0x9: (0x40ef42, 'SUB BYTE PTR'),
0xa: (0x40ef6c, 'SUB WORD PTR'),
0xb: (0x40ef9a, 'SUB DWORD PTR'),
0xc: (0x40efc6, 'ROL BYTE PTR'),
0xd: (0x40effd, 'ROL WORD PTR'),
0xe: (0x40f038, 'ROL DWORD PTR'),
0xf: (0x40f070, 'ROR BYTE PTR'),
0x10: (0x40f0a7, 'ROR WORD PTR'),
0x11: (0x40f0e2, 'ROR DWORD PTR'),
0x12: (0x40f11a, 'NOT BYTE PTR'),
0x13: (0x40f13e, 'NOT WORD PTR'),
0x14: (0x40f164, 'NOT DWORD PTR'),
0x15: (0x40f189, 'SHUFFLE MEM[0-4]'),
0x16: (0x40f2e2, 'RC4 DECRYPT'),
0x17: (0x40f1e2, 'MOV FLAG'),
0x18: (0x40f208, 'MOV FLAG'),
0x19: (0x40f230, 'MOV FLAG'),
0x1a: (0x40f257, 'ADD WORD MEM'),
0x1b: (0x40f27f, 'JNZ'),
0x1c: (0x40f2af, 'JNZ'),
0x1d: (0x40f348, 'MOV REG'),
0x1e: (0x40f379, 'MOV REG'),
0x1f: (0x40f3a7, 'MOV REG'),
0x20: (0x40f3d4, 'MOV REG'),
0x21: (0x40f409, 'MOV REG'),
0x22: (0x40f43e, 'MOV REG'),
0x23: (0x40f472, 'ADD REG'),
0x24: (0x40f4a9, 'ADD REG'),
0x25: (0x40f4e0, 'ADD REG'),
0x26: (0x40f5a8, 'SUB REG'),
0x27: (0x40f5df, 'SUB REG'),
0x28: (0x40f616, 'SUB REG'),
0x29: (0x40f6de, 'XOR REG'),
0x2a: (0x40f715, 'XOR REG'),
0x2b: (0x40f74c, 'XOR REG'),
0x2c: (0x40f516, 'ADD REG'),
0x2d: (0x40f549, 'ADD REG'),
0x2e: (0x40f579, 'ADD REG'),
0x2f: (0x40f64c, 'SUB REG'),
0x30: (0x40f67f, 'SUB REG'),
0x31: (0x40f6af, 'SUB REG'),
0x32: (0x40f782, 'XOR REG'),
0x33: (0x40f7b5, 'XOR REG'),
0x34: (0x40f7e5, 'XOR REG'),
0x35: (0x40f946, 'ADD BYTE PTR'),
0x36: (0x40f97c, 'ADD WORD PTR'),
0x37: (0x40f9b5, 'ADD DWORD PTR'),
0x38: (0x40f9ec, 'ADD BYTE PTR'),
0x39: (0x40fa22, 'ADD WORD PTR'),
0x3a: (0x40fa5b, 'ADD DWORD PTR'),
0x3b: (0x40fa92, 'XOR BYTE PTR'),
0x3c: (0x40fac8, 'XOR WORD PTR'),
0x3d: (0x40fb01, 'XOR DWORD PTR'),
0x3e: (0x40f814, 'MOV REG'),
0x3f: (0x40f845, 'MOV REG'),
0x40: (0x40f876, 'MOV REG'),
0x41: (0x40f8a6, 'MOV BYTE PTR'),
0x42: (0x40f8da, 'MOV WORD PTR'),
0x43: (0x40f911, 'MOV DWORD PTR'),
0x44: (0x40ee37, 'HALT')
}
dis = open('dis_vm.asm', 'w')
def disassemble(idx):
vip = ctx(VIP); mem = ctx(MEM); flag = ctx(FLAG); op = vip + ExprInt(1, 32); imm = vip + ExprInt(2, 32)
op8 = read_value_n(op); op16 = read_value_n(op, 16); op32 = read_value_n(op, 32)
imm8 = read_value_n(imm); imm16 = read_value_n(imm, 16); imm32 = read_value_n(imm, 32)
m8 = read_value_n(mem); m16 = read_value_n(mem, 16); m32 = read_value_n(mem, 32)
mnem = HANDLERS[idx][1]; hd_addr = HANDLERS[idx][0]; loc = f'0x{int(vip):X}: '
if mnem.startswith('NOP'):
dis.write(loc+f'{mnem}\n')
elif hd_addr == 0x40ee3a: # XOR BYTE PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op8}; [{mem}] = {m8}\n')
elif hd_addr == 0x40ee64: # XOR WORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op16}; [{mem}] = {m16}\n')
elif hd_addr == 0x40ee92: # XOR DWORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op32}; [{mem}] = {m32}\n')
elif hd_addr == 0x40eebe: # ADD BYTE PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op8}; [{mem}] = {m8}\n')
elif hd_addr == 0x40eee8: # ADD WORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op16}; [{mem}] = {m16}\n')
elif hd_addr == 0x40ef16: # ADD DWORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op32}; [{mem}] = {m32}\n')
elif hd_addr == 0x40ef42: # ADD BYTE PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op8}; [{mem}] = {m8}\n')
elif hd_addr == 0x40ef6c: # ADD WORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op16}; [{mem}] = {m16}\n')
elif hd_addr == 0x40ef9a: # ADD DWORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}], {op32}; [{mem}] = {m32}\n')
elif hd_addr == 0x40efc6: # ROL BYTE PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 7}; [{mem}] = {m8}\n')
elif hd_addr == 0x40effd: # ROL WORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 0xF}; [{mem}] = {m16}\n')
elif hd_addr == 0x40f038: # ROL DWORD PTR [MEM], OP
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 0x1F}; [{mem}] = {m32}\n')
elif hd_addr == 0x40f070: # ROR BYTE PTR [MEM]
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 7}; [{mem}] = {m8}\n')
elif hd_addr == 0x40f0a7: # ROR WORD PTR [MEM]
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 0xF}; [{mem}] = {m16}\n')
elif hd_addr == 0x40f0e2: # ROR DWORD PTR [MEM]
dis.write(loc+f'{mnem} [{mem}],0x{int(op8) & 0x1F}; [{mem}] = {m32}\n')
elif hd_addr == 0x40f11a: # NOT BYTE PTR [MEM]
dis.write(loc+f'{mnem} [{mem}] = {m8}\n')
elif hd_addr == 0x40f13e: # NOT WORD PTR [MEM]
dis.write(loc+f'{mnem} [{mem}] = {m16}\n')
elif hd_addr == 0x40f164: # NOT DWORD PTR [MEM]
dis.write(loc+f'{mnem} [{mem}] = {m32}\n')
elif hd_addr == 0x40f189: # shuffle mem
ctrl = int(op8)
slots = [(ctrl >> (2 * i)) & 3 for i in range(4)]
dis.write(loc+f'{mnem} ctrl={ctrl:#04x} -> slots {slots}\n')
elif hd_addr == 0x40f2e2: # RC4 Decrypt
keylen = int(op8) # vpc[1]
datalen = int(read_value_n(vip + ExprInt(2, 32), 8)) # vpc[2]
key = bytes(int(read_value_n(vip + ExprInt(3 + i, 32), 8)) for i in range(keylen))
enc_mem = bytes(int(read_value_n(mem + ExprInt(i, 32), 8)) for i in range(datalen))
dis.write(loc+ f'{mnem} [{mem}];\t key={key.hex()}\tenc_mem={enc_mem.hex()}\n')
elif hd_addr == 0x40f1e2: # MOV BYTE FLAG
dis.write(loc+f'{mnem}, {op8}\n')
elif hd_addr == 0x40f208: # MOV WORD FLAG
dis.write(loc+f'{mnem}, {op16}\n')
elif hd_addr == 0x40f230: # MOV DWORD FLAG
dis.write(loc+f'{mnem}, {op32}\n')
elif hd_addr == 0x40f257: # ADD WORD MEM
dis.write(loc+f'{mnem}, {op16}\n')
elif hd_addr == 0x40f27f: # JNZ byte disp
disp = int(read_value_n(vip + ExprInt(1, 32), 8))
jaddr = int(vip) + 2 - disp
dis.write(loc+f'{mnem} 0x{jaddr:02X}; zf={read_value_n(flag)}\n')
elif hd_addr == 0x40f2af: # JNZ
disp = int(read_value_n(vip + ExprInt(1, 32), 16))
jaddr = int(vip) + 3 - disp
dis.write(loc+f'{mnem} 0x{jaddr:02X}; zf={read_value_n(flag)}\n')
elif hd_addr == 0x40f348: # MOV BYTE REG
i = int((int(op8) & 0xF))
dis.write(loc+f'{mnem}[0x{i:0X}],0x{int(imm8):0X}\n')
elif hd_addr == 0x40f379: # MOV WORD REG
i = int((int(op8) & 0xF))
dis.write(loc+f'{mnem}[0x{i:0X}],0x{int(op16):0X}\n')
elif hd_addr == 0x40f3a7: # MOV DWORD REG
i = int((int(op8) & 0xF))
dis.write(loc+f'{mnem}[0x{i:0X}],0x{int(imm32):0X}\n')
elif hd_addr == 0x40f3d4:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f409:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f43e:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f472:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f4a9:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f4e0:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f5a8:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f5df:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f616:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f6de:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f715:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f74c:
i = int((int(op8) & 0xF)); j = int((int(op8) >> 4))
dis.write(loc+f'{mnem}[0x{i:0X}], REG[0x{j:0X}]\n')
elif hd_addr == 0x40f516:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm8}\n')
elif hd_addr == 0x40f549:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {op16}\n')
elif hd_addr == 0x40f579:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm32}\n')
elif hd_addr == 0x40f64c:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm8}\n')
elif hd_addr == 0x40f67f:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {op16}\n')
elif hd_addr == 0x40f6af:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm32}\n')
elif hd_addr == 0x40f782:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm8}\n')
elif hd_addr == 0x40f7b5:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {op16}\n')
elif hd_addr == 0x40f7e5:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], {imm32}\n')
elif hd_addr == 0x40f946: # this
i = int((int(op8) & 0xF));
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40f97c:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m16}\n')
elif hd_addr == 0x40f9b5:
i = int((int(op8) & 0xF));
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m32}\n')
elif hd_addr == 0x40f9ec:
i = int((int(op8) & 0xF));
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40fa22:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m16}\n')
elif hd_addr == 0x40fa5b:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m32}\n')
elif hd_addr == 0x40fa92:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40fac8:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m16}\n')
elif hd_addr == 0x40fb01:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m32}\n')
elif hd_addr == 0x40f814:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], [{mem}] ; {m8}\n')
elif hd_addr == 0x40f845:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], [{mem}] ; {m16}\n')
elif hd_addr == 0x40f876:
i = int(op8) & 0xf
dis.write(loc+f'{mnem}[0x{i:0X}], [{mem}] ; {m32}\n')
elif hd_addr == 0x40f8a6:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40f8da:
i = int(op8) & 0xf
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40f911:
i = int((int(op8) & 0xF))
dis.write(loc+f'{mnem} [{mem}], REG[0x{i:0X}]; [{mem}] = {m8}\n')
elif hd_addr == 0x40ee37:
dis.write(loc+f'{mnem}\n')
return
write_address(VM_CTX + VIP, BYTECODE); write_address(VM_CTX + MEM, MEMCODE); write_address(VM_CTX + FLAG, 0)
for i in range(16):
write_address(VM_CTX + REG + i * 4, 0, size=32)
# populating the bytecode addr
data = container.bin_stream.getbytes(BYTECODE, 0x1000)
for i, b in enumerate(data):
sb.symbols[ExprMem(ExprInt(BYTECODE + i, 32), 8)] = ExprInt(b, 8)
# populating the memcode addr
data = container.bin_stream.getbytes(MEMCODE, 0x288)
for i, b in enumerate(data):
sb.symbols[ExprMem(ExprInt(MEMCODE + i, 32), 8)] = ExprInt(b, 8)
sb.symbols[ExprMem(ExprInt(STACK_TOP - 4, 32), 32)] = ExprInt(RETN, 32)
HALT = False
while not HALT: # vm dispatcher
idx = int(read_value_n(ctx(VIP))) # reads Virtual Instruction pointer and derefrence 8 bits (1st byte which is at the start of symbolic execution engine)
disassemble(idx)
handler_addr = HANDLERS[idx][0]
ircfg = lifted(handler_addr)
if handler_addr == 0x40EE37: HALT = True # halt handler
sb.symbols[lifter.arch.regs.ECX] = ExprInt(VM_CTX, 32)
sb.symbols[lifter.arch.regs.ESP] = ExprInt(STACK_TOP - 4, 32)
q = deque([ExprInt(handler_addr, 32)]) # adding the handler to be executed
symbolic_execution(q, ircfg)
dis.close() # closing the disassembly file log
data = bytearray()
for i in range(0x288):
v = read_value_n(ExprInt(MEMCODE + i, 32), 8)
data.append(int(v))
open('mem_dec.bin', 'wb').write(data) # http://rxfkxmtqxg.com/ppcrzaezqs/cfg.bin -- zeus C2 config download URL
Slow Tempest Campaign
Slow Tempest is a covert cyber espionage campaign that first emerged in 2024, primarily targeting organizations and Chinese-speaking users in East Asia. The threat actors are known for advanced persistent threats
The sample we are deobfuscating can be downloaded here
It has Multiple Layers of Obfuscations:
Indirest Jump Obfuscation
A very frequent pattern in whole binary
Even the function is large enough but the flow is broken due to indirect jumps.
There are many indirect jumps in the whole binary. To check the exact indirect jump i wrote a simple ida script
1
2
3
4
5
6
7
8
9
import idc
ea = 0x140001000 # start of text section
ind = []
while ea < 0x140057495: # end of text section
if idc.print_insn_mnem(ea) == 'jmp' and idc.get_operand_type(ea, 0) is idc.o_reg: # jmp reg
print(hex(ea)); ind.append(ea)
ea = idc.next_head(ea)
print('indirect jmps: ', len(ind))
1
indirect jmps: 2788
thats alot, so lets see how we can deobfuscate it.
We see a very similar pattern here, before indirect jump we do have cmovcc and set instruction, which is deciding either a value can be moved to the register or not which effects the control flow. As i saw in many indirect jump blocks the pattern is consistent accross majority. Hence we can pattern match the basic blocks which have indirect jumps and extract the information that what is the cmovcc or set instruction is on which register. Then we can perform symbolic execution two times on the block one with cmovcc/set true and the other with false. Hence we will have both final address inform of jcc and fall, and we can just replace the cmovcc with the jcc instruction proceeding with the fall instruction. We also see global data reads in the calculation of indirect jumps so we will set the .data section as concerete value so symbolic execution evaluates jump addresses.
First we will create a class to store all our pattern matching info
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConditionalJump:
def __init__(self, jtype=None, before_cond=None, cond_addr=None, after_cond=None, condition=None, regs=None,trueTarget=None,falseTarget=None,jmpAddr=False):
self.jtype = jtype # set/cmovcc
self.before_cond = before_cond # block start
self.cond_addr = cond_addr # cmovcc/set address
self.after_cond = after_cond # next address after cmovcc
self.condition = condition # cc (condition from cmovcc)
self.regs = regs # src, dst (from cmovcc)
self.trueTarget = trueTarget
self.falseTarget = falseTarget
self.jmpAddr = jmpAddr # indirect jmp address
class Jump:
def __init__(self, start_addr=None,jmpAddr=False):
self.start_addr = start_addr
self.jmpAddr = jmpAddr
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
from capstone import *
[...]
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
jumps = []
disassembly = {}
prevSize = None
for insn in md.disasm(textData, textVA):
if insn.mnemonic == "jmp" and insn.operands[0].type == X86_OP_REG:
jumps.append(insn.address)
disassembly[insn.address] = [insn.mnemonic,insn.op_str,insn.size,prevSize]
prevSize = insn.size
jumpsInfo = []
for jump in jumps:
n = getStartJump(jump)
if n != None:
jumpsInfo.append(n)
def getStartJump(addr):
jumpAddress = addr
_, _,_, prevSize = disassembly[addr]
prevAddr = addr
if prevSize == None: return None
addr -= prevSize # address of the previous instruciton
condAddr = None
condition = ""
regs = None
type = None
while addr in disassembly:
mnemonic, op_str, size, prevSize = disassembly[addr]
# the instructions that split the miasm IR block
if mnemonic in ["call","jmp","ret","int3","div,","idiv"] or mnemonic.startswith("j") or (mnemonic == "sub" and "rsp" in op_str):
if type != None: #cmov
if type == 'cmovcc':
return ConditionalJump(jtype='cmovcc',before_cond=prevAddr,cond_addr=condAddr,after_cond=afterAddr,condition=condition,regs=regs,jmpAddr=jumpAddress)
elif type == 'setcc':
return ConditionalJump(jtype='setcc',before_cond=prevAddr,cond_addr=condAddr,after_cond=afterAddr,condition=condition,regs=regs,jmpAddr=jumpAddress)
else:
return Jump(start_addr=prevAddr,jmpAddr=jumpAddress)
# miasm block start addr, jmp instruction addr
elif mnemonic.startswith("cmov") and type == None:
type = 'cmovcc'
condAddr = addr
afterAddr = addr+size
reg1,reg2 = op_str.replace(" ","").split(",")
regs = [reg_map[reg1.lower()],reg_map[reg2.lower()]] #convert to the 64 bit reg
condition = mnemonic[4:]
elif mnemonic.startswith("set") and type == None:
type = 'setcc'
condAddr = addr
afterAddr = addr+size
condition = mnemonic[3:]
regs = reg_map[op_str.lower()]
prevAddr = addr
if prevSize == None: return None
addr -= prevSize
return None
[...]
By this we have complete info of each conditional and unconditional jump. Now we can perform symbolic execution till the condition and then pause, and then continue with true and false value and hence we will have jcc and jmp, then we can patch them starting from the cmovcc
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
for jump in jumpsInfo: # iterate through each function
if isinstance(jump,ConditionalJump): # if its a conditional indirect jmp
if jump.jtype == 'cmovcc': #type = cmov # before_cond=block start
symbols,falseVal,trueVal = symbolic_execution(jump.before_cond,cmovRegs=jump.regs, cond_addr=jump.cond_addr)
# copying the symbols so both can be used seprately on true false condition
symbolsFalse = symbols.copy(); symbolsTrue = symbols.copy()
symbolsFalse[ExprId(jump.regs[1],64)] = falseVal
symbolsTrue[ExprId(jump.regs[0],64)] = trueVal
# symbolic execution if cmovcc is true
trueTarget = symbolic_execution(jump.after_cond,symbols=symbolsTrue,initSymbols=False)
# symbolic execution if cmovcc is false
falseTarget = symbolic_execution(jump.after_cond,symbols=symbolsFalse,initSymbols=False)
elif jump.jtype == 'setcc':
reg = jump.regs
symbols = symbolic_execution(jump.before_cond,cond_addr=jump.cond_addr)
symbolsFalse = symbols.copy(); symbolsTrue = symbols.copy()
symbolsTrue[ExprId(reg,64)] = ExprInt(1,64) # setcc is true
symbolsFalse[ExprId(reg,64)] = ExprInt(0,64) # setcc is false
# symbolic exection on both set and unset to see what is the jcc and fall
falseTarget = symbolic_execution(jump.after_cond,symbols=symbolsFalse, initSymbols=False)
trueTarget = symbolic_execution(jump.after_cond,symbols=symbolsTrue, initSymbols=False)
jump.trueTarget = int(trueTarget); jump.falseTarget = int(falseTarget)
patchData = assembleJump(jump.trueTarget,jump.cond_addr,cond=jump.condition)
patchData += assembleJump(jump.falseTarget,jump.cond_addr+6)
patchSize = jump.jmpAddr+2-jump.cond_addr
patchData = patchData + ((patchSize - len(patchData)) * b"\x90")
patch(jump.cond_addr,patchData)
print(hex(jump.jmpAddr), trueTarget, falseTarget)
else:
r = symbolic_execution(jump.start_addr) # its a unconditional indirect jmp
if not isinstance(r,ExprInt): #false positive OR a missed conditional jmp
print('unconditional jmp missed ExprInt: ', r,hex(jump.start_addr))
continue
else:
print(f"patch unconditional indirect jmp {hex(jump.jmpAddr)}, {r}")
Running the deobfuscator.
before indirect jmp resolution
This number matches by the idapython script we wrote. It successfully deobfuscated all 2788 indirect jumps.
The whole indirect jump deobfuscation script
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.core.asmblock import AsmCFG
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.arch.x86.regs import all_regs_ids, all_regs_ids_init
from miasm.expression.simplifications import ExpressionSimplifier, expr_simp_explicit, expr_simp
from miasm.expression.expression import ExprVisitorCallbackTopToBottom, ExprMem, ExprInt, ExprId
from capstone import *
from capstone.x86 import *
import struct
import lief
file = 'slowtempest.bin'
pe = lief.PE.parse(file)
image_base = pe.imagebase
text_section = pe.get_section(".text")
textData = bytearray(text_section.content)
textVA = image_base + text_section.virtual_address
data_section = pe.get_section(".data")
rawData = bytearray(open(file,'rb').read()) #for the writeback at the end
u64 = lambda d: struct.unpack('<Q', d)[0]
u32 = lambda d: struct.unpack('<I', d)[0]
va = lambda addr: pe.va_to_offset(addr)
concreteSymbols = {}
def add_data(symb, offset, data):
for i in range(0,len(data),8):
symb[ExprMem(ExprInt(offset + i, 64),64)] = ExprInt(u64(data[i:i+8]), 64)
#for the dword references
for i in range(0,len(data),4):
symb[ExprMem(ExprInt(offset + i, 32),32)] = ExprInt(u32(data[i:i+4]), 32)
def simpExprMemCallBack(expr):
if isinstance(expr, ExprMem):
if expr in concreteSymbols:
newExpr = expr.replace_expr({expr:concreteSymbols[expr]})
return newExpr
return None
def simpExprMem(_,expr): # https://github.com/cea-sec/miasm/blob/master/miasm/expression/expression.py#L614
visitor = ExprVisitorCallbackTopToBottom(lambda expr:simpExprMemCallBack(expr))
return visitor.visit(expr)
def patch(va, data):
rva = va - image_base
text_rva = text_section.virtual_address
offset = rva - text_rva
textData[offset:offset + len(data)] = data
class ConditionalJump:
def __init__(self, jtype=None, before_cond=None, cond_addr=None, after_cond=None, condition=None, regs=None,trueTarget=None,falseTarget=None,jmpAddr=False):
self.jtype = jtype # set/cmovcc
self.before_cond = before_cond # block start
self.cond_addr = cond_addr # cmovcc/set address
self.after_cond = after_cond # next address after cmovcc
self.condition = condition # cc (condition from cmovcc)
self.regs = regs # src, dst (from cmovcc)
self.trueTarget = trueTarget
self.falseTarget = falseTarget
self.jmpAddr = jmpAddr # indirect jmp address
class Jump:
def __init__(self, start_addr=None,jmpAddr=False):
self.start_addr = start_addr
self.jmpAddr = jmpAddr
add_data(concreteSymbols, data_section.virtual_address + image_base, bytes(data_section.content))
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
jumps = []
disassembly = {}
#any spelling of a gpr -> the 64 bit one. the old 4 entry reg_map KeyErrors on
#sil/dil/r8b/... and reg.replace("e","r") turns r8d into R8D
reg_map = {}
for i,r in enumerate(["RAX","RCX","RDX","RBX","RSP","RBP","RSI","RDI","R8","R9","R10","R11","R12","R13","R14","R15"]):
if i < 8:
l = r[1:].lower()
names = [r, "e"+l, l, (l[0]+"l" if l[1] == "x" else l+"l")]
if l[1] == "x": names.append(l[0]+"h")
else:
names = [r, r+"D", r+"W", r+"B"]
for n in names: reg_map[n.lower()] = r
cond_jump_insts = {'jo': bytearray(b'\x0f\x80'), 'jno': bytearray(b'\x0f\x81'), 'js': bytearray(b'\x0f\x88'), 'jns': bytearray(b'\x0f\x89'), 'je': bytearray(b'\x0f\x84'), 'jz': bytearray(b'\x0f\x84'), 'jne': bytearray(b'\x0f\x85'), 'jnz': bytearray(b'\x0f\x85'), 'jb': bytearray(b'\x0f\x82'), 'jnae': bytearray(b'\x0f\x82'), 'jc': bytearray(b'\x0f\x82'), 'jae': bytearray(b'\x0f\x83'), 'jnb': bytearray(b'\x0f\x83'), 'jnc': bytearray(b'\x0f\x83'), 'jbe': bytearray(b'\x0f\x86'), 'jna': bytearray(b'\x0f\x86'), 'ja': bytearray(b'\x0f\x87'), 'jnbe': bytearray(b'\x0f\x87'), 'jl': bytearray(b'\x0f\x8c'), 'jnge': bytearray(b'\x0f\x8c'), 'jge': bytearray(b'\x0f\x8d'), 'jnl': bytearray(b'\x0f\x8d'), 'jle': bytearray(b'\x0f\x8e'), 'jng': bytearray(b'\x0f\x8e'), 'jg': bytearray(b'\x0f\x8f'), 'jnle': bytearray(b'\x0f\x8f'), 'jp': bytearray(b'\x0f\x8a'), 'jpe': bytearray(b'\x0f\x8a'), 'jnp': bytearray(b'\x0f\x8b'), 'jpo': bytearray(b'\x0f\x8b')}
def assembleJump(target, currentAddr,cond=None):
offset = target - currentAddr
if offset >= 0 and cond != None: offset = offset - 6
if offset >= 0 and cond == None: offset = offset - 5
offset = offset.to_bytes(4,'little',signed=True)
inst = b""
if cond != None:
inst += cond_jump_insts["j"+cond]
else:
inst += b"\xE9"
return inst + offset
def getStartJump(addr):
jumpAddress = addr
_, _,_, prevSize = disassembly[addr]
prevAddr = addr
if prevSize == None: return None
addr -= prevSize # address of the previous instruciton
condAddr = None
condition = ""
regs = None
type = None
while addr in disassembly:
mnemonic, op_str, size, prevSize = disassembly[addr]
# the instructions that split the miasm IR block
if mnemonic in ["call","jmp","ret","int3","div,","idiv"] or mnemonic.startswith("j") or (mnemonic == "sub" and "rsp" in op_str):
if type != None: #cmov
if type == 'cmovcc':
return ConditionalJump(jtype='cmovcc',before_cond=prevAddr,cond_addr=condAddr,after_cond=afterAddr,condition=condition,regs=regs,jmpAddr=jumpAddress)
elif type == 'setcc':
return ConditionalJump(jtype='setcc',before_cond=prevAddr,cond_addr=condAddr,after_cond=afterAddr,condition=condition,regs=regs,jmpAddr=jumpAddress)
else:
return Jump(start_addr=prevAddr,jmpAddr=jumpAddress)
# miasm block start addr, jmp instruction addr
elif mnemonic.startswith("cmov") and type == None:
type = 'cmovcc'
condAddr = addr
afterAddr = addr+size
reg1,reg2 = op_str.replace(" ","").split(",")
regs = [reg_map[reg1.lower()],reg_map[reg2.lower()]] #convert to the 64 bit reg
condition = mnemonic[4:]
elif mnemonic.startswith("set") and type == None:
type = 'setcc'
condAddr = addr
afterAddr = addr+size
condition = mnemonic[3:]
regs = reg_map[op_str.lower()]
prevAddr = addr
if prevSize == None: return None
addr -= prevSize
return None
prevSize = None
for insn in md.disasm(textData, textVA):
if insn.mnemonic == "jmp" and insn.operands[0].type == X86_OP_REG:
jumps.append(insn.address)
disassembly[insn.address] = [insn.mnemonic,insn.op_str,insn.size,prevSize]
prevSize = insn.size
jumpsInfo = []
for jump in jumps:
n = getStartJump(jump)
if n != None:
jumpsInfo.append(n)
fdesc = open(file, 'rb')
loc_db = LocationDB()
cont = Container.from_stream(fdesc, loc_db)
machine = Machine(cont.arch)
mdis = machine.dis_engine(cont.bin_stream, loc_db=cont.loc_db)
lifter = machine.lifter_model_call(mdis.loc_db)
simplifier = ExpressionSimplifier()
simplifier.enable_passes(ExpressionSimplifier.PASS_COMMONS)
simplifier.enable_passes(ExpressionSimplifier.PASS_HIGH_TO_EXPLICIT)
simplifier.enable_passes({ExprMem:[simpExprMem]})
def symbolic_execution(blockAddr, symbols = {}, cond_addr=None, cmovRegs=[]):
addr = blockAddr
asmcfg = AsmCFG(loc_db)
block = mdis.dis_block(addr)
if cond_addr:
while block.lines:
last_instr = block.lines[-1]
if last_instr.offset == cond_addr: # removing the lines from the block untill cmovcc or setcc
block.lines.pop()
break
block.lines.pop()
asmcfg.add_block(block) # building block before cmovcc or setcc
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg)
if symbols == {}: # when the there are no symbols
for i, r in enumerate(all_regs_ids):
symbols[r] = all_regs_ids_init[i]
sb = SymbolicExecutionEngine(lifter,symbols,sb_expr_simp=simplifier)
r = sb.run_block_at(ircfg, addr)
if cmovRegs != []: # false in set condition
regs = cmovRegs
false = sb.symbols[ExprId(regs[0],64)] #if dst is not moved to src
true = sb.symbols[ExprId(regs[1],64)] #if dst is moved to src
assert isinstance(false,ExprInt) and isinstance(true,ExprInt)
return sb.symbols.copy(), false, true
elif cond_addr:
return sb.symbols.copy()
else:
return r
print(f"{file}: base {hex(image_base)}, .text {hex(text_section.virtual_address)}, {len(jumps)} jmp reg")
bad = {}
patched = 0
for jump in jumpsInfo:
if isinstance(jump,ConditionalJump):
if jump.jtype == 'cmovcc': #type = cmov # before_cond=block start
symbols,falseVal,trueVal = symbolic_execution(jump.before_cond,cmovRegs=jump.regs, cond_addr=jump.cond_addr)
symbolsFalse = symbols.copy(); symbolsTrue = symbols.copy()
symbolsFalse[ExprId(jump.regs[1],64)] = falseVal
symbolsTrue[ExprId(jump.regs[0],64)] = trueVal
trueTarget = symbolic_execution(jump.after_cond,symbols=symbolsTrue)
falseTarget = symbolic_execution(jump.after_cond,symbols=symbolsFalse)
assert isinstance(trueTarget,ExprInt) and isinstance(falseTarget,ExprInt)
elif jump.jtype == 'setcc':
reg = jump.regs
symbols = symbolic_execution(jump.before_cond,cond_addr=jump.cond_addr)
symbolsFalse = symbols.copy(); symbolsTrue = symbols.copy()
symbolsFalse[ExprId(reg,64)] = ExprInt(0,64)
symbolsTrue[ExprId(reg,64)] = ExprInt(1,64)
falseTarget = symbolic_execution(jump.after_cond,symbols=symbolsFalse)
trueTarget = symbolic_execution(jump.after_cond,symbols=symbolsTrue)
assert isinstance(trueTarget,ExprInt) and isinstance(falseTarget,ExprInt)
jump.trueTarget = int(trueTarget)
jump.falseTarget = int(falseTarget)
patchData = assembleJump(jump.trueTarget,jump.cond_addr,cond=jump.condition)
patchData += assembleJump(jump.falseTarget,jump.cond_addr+6)
patchSize = jump.jmpAddr+2-jump.cond_addr
patchData = patchData + ((patchSize - len(patchData)) * b"\x90")
patch(jump.cond_addr,patchData)
patched += 1
print(hex(jump.jmpAddr),trueTarget,falseTarget)
else:
r = symbolic_execution(jump.start_addr)
if not isinstance(r,ExprInt): #false positive OR a missed conditional jmp
print('unconditional jmp missed ExprInt: ', r,hex(jump.start_addr))
continue
else:
print(f"manually patch tailcall at {hex(jump.jmpAddr)}, {r}")
print(f"patched {patched}/{len([j for j in jumpsInfo if isinstance(j,ConditionalJump)])}")
rawData[text_section.pointerto_raw_data:text_section.pointerto_raw_data+len(textData)] = textData
out = file.split('.bin')[0] + '.jmp_deob.bin'
open(out,"wb").write(rawData)
print(f"wrote {out}")
Control Flow Flattening
This is very classic example of control flow flattening. Which is much different than the Tigress CFF we dealt with. This flattening is without the jump table, hence our will be a bit different here, as we donโt have entries of original basic blocks from the jump table. we have the identify the original basic blocks via the CFG design.
Dispatcher
state value read in the first instruction of dispatcher
Original Basic Blocks
predecessors of the pre-dispatchers are original basic blocks
State update process
Blocks like these just update the state (unconditionally), These are trivial as we can just build the mapping state to state mapping and state to address mapping but there is a catch ๐
We can see that cmovcc is followed by a cmp is deciding the state value, so a state can determine more than two next states at two distinct time. So now we will perform symbolic execution on the CFG and build the mapping and then remove the basic block which are not original basic blocks.
Identify Original Basic Blocks
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
def findOBB(pre_disp_addr):
OBB = []
pre_disp_loc = loc_db.get_offset_location(pre_disp_addr)
# for original basic blocks
for curr_loc in asmcfg.predecessors(pre_disp_loc):
pre_obb_loc = asmcfg.predecessors(curr_loc); OBB.append(curr_loc)
assert len(pre_obb_loc) == 1
while True:
pre_obb_block = asmcfg.loc_key_to_block(pre_obb_loc[0])
if pre_obb_block.lines[-1].name.startswith('J'): # if its a Jmp/Jcc break
break
curr_loc = pre_obb_loc[0]
pre_obb_loc = asmcfg.predecessors(curr_loc)
assert len(pre_obb_loc) == 1
OBB.append(curr_loc) # untill then all are original basic block (they are just split because of call)
# for ret basic blocs/tail calls
for bb in asmcfg.blocks:
ret_loc = bb.loc_key
if len(asmcfg.successors(ret_loc)) == 0: # same algo just on the return as it has no successors
ret_pre_loc = asmcfg.predecessors(ret_loc); OBB.append(ret_loc)
assert len(ret_pre_loc) == 1
while True:
ret_pre_block = asmcfg.loc_key_to_block(ret_pre_loc[0])
if ret_pre_block.lines[-1].name.startswith('J'):
break
ret_loc = ret_pre_loc[0]
ret_pre_loc = asmcfg.predecessors(ret_loc)
assert len(ret_pre_loc) == 1
OBB.append(ret_loc)
print('RET: ', hex(loc_db.get_location_offset(ret_loc)))
return OBB
We will perform symbolic execution on the CFG in BFS manner. execute the block, append the false condition in the queue for later execution, go with the true value, when the address is one of original basic block update the state to address map and when the address reaches the pre-dispatcher we update state to state map just before symbolic execution on each basic blocks, we will scan for cmp followed by cmovcc, if they are found it means that we are dealing with jcc states. and return a condition (so it can be separated as true and false branch) else we return solid brancing address.
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
q = deque([(loc_db.get_offset_location(addr), first_state_val, init_symbols)]); visited_location = set()
while q:
curr_addr_loc, curr_state_val, symbols = q.popleft()
sb = SymbolicExecutionEngine(lifter, symbols, sb_expr_simp=custom_simp)
while True:
r = symbolic_execution(sb, curr_addr_loc)
if r is None: break
if r.is_cond():
cond_true = {r.cond: ExprInt(1, 32)}; cond_false = {r.cond: ExprInt(0, 32)}
jt = to_loc_key(expr_simp(sb.eval_expr(r.replace_expr(cond_true), {})))
jf = to_loc_key(expr_simp(sb.eval_expr(r.replace_expr(cond_false), {})))
q.append((jf, curr_state_val, sb.symbols.copy())) # append the false branch for later symbolic execution
curr_addr_loc = jt
else:
curr_addr_loc = to_loc_key(expr_simp(sb.eval_expr(r))) # curr_addr_loc = next_loc
if curr_addr_loc in visited_location: break
elif curr_addr_loc in OBB:
visited_location.add(curr_addr_loc)
curr_state_val = int(sb.eval_expr(state_variable))
# check if the current address is already added in the correspinding state list
if curr_addr_loc not in state_to_addr_map[curr_state_val]:
state_to_addr_map[curr_state_val].append(curr_addr_loc)
if first_state_val is None: first_state_val = curr_state_val
elif curr_addr_loc == pre_disp_loc:
nState_val = int(sb.eval_expr(state_variable))
if nState_val not in state_to_state_map[curr_state_val]:
state_to_state_map[curr_state_val].append(nState_val)
curr_state_val = None
def symbolic_execution(sb, address_loc):
cmp_ins = None; cmovcc_ins = None
current_asm_block = asmcfg.loc_key_to_block(address_loc)
if current_asm_block is None: return sb.run_block_at(ircfg=ircfg, addr=address_loc)
for ins in current_asm_block.lines:
if ins.name in ['CMP', 'TEST']:
cmp_ins = ins
elif ins.name.startswith('CMOV'):
cmovcc_ins = ins
break
if current_asm_block.lines[-1].name == 'CALL':
rsp = sb.symbols[lifter.arch.regs.RSP]; rbp = sb.symbols[lifter.arch.regs.RBP]
r = sb.run_block_at(ircfg=ircfg, addr=address_loc)
sb.symbols[lifter.arch.regs.RSP] = rsp; sb.symbols[lifter.arch.regs.RBP] = rbp
return r
if cmovcc_ins is not None and cmp_ins is not None:
curr_loc = address_loc
# print(current_asm_block)
sb.run_block_at(ircfg=ircfg, addr=curr_loc)
while True:
current_ir_block = ircfg.get_block(addr=curr_loc)
# print('check', hex(loc_db.get_location_offset(address_loc))) # debug
for ir_ins in current_ir_block:
if ir_ins.instr.name.startswith('CMOV'):
cmov_cond_expr = ir_ins.values()[-1]
if str(cmovcc_ins).startswith('CMOVN'):print(ir_ins.instr.name,' ', hex(loc_db.get_location_offset(to_loc_key(curr_loc))), cmov_cond_expr);\
return cmov_cond_expr.copy()
print(ir_ins.instr.name,' ', hex(loc_db.get_location_offset(to_loc_key(curr_loc))), cmov_cond_expr)
return ExprCond(cmov_cond_expr.cond.copy(), cmov_cond_expr.src2.copy(), cmov_cond_expr.src1.copy()) # flip the condition
curr_loc = sb.run_block_at(ircfg=ircfg, addr=curr_loc)
else:
return sb.run_block_at(ircfg=ircfg, addr=address_loc)
I ran the script after setting the necessary addresses, it threw an error even though i was sure it would work.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
imagebase: 0x140000000
RET: 0x140053d73
STATE: @64[RSP + 0x50]
CMOVNZ 0x1400536f4 CC_EQ(zf)?(loc_key_167,loc_key_166)
CMOVG 0x140053791 CC_S>(nf, of, zf)?(loc_key_175,loc_key_176)
CMOVZ 0x1400538ca CC_EQ(zf)?(loc_key_170,loc_key_171)
CMOVA 0x140053993 CC_U>(cf, zf)?(loc_key_186,loc_key_187)
CMOVNZ 0x140053a88 CC_EQ(zf)?(loc_key_189,loc_key_188)
CMOVG 0x140053810 CC_S>(nf, of, zf)?(loc_key_184,loc_key_185)
CMOVNZ 0x140053b00 CC_EQ(zf)?(loc_key_169,loc_key_168)
Traceback (most recent call last):
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 178, in <module>
r = symbolic_execution(sb, curr_addr_loc)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 144, in symbolic_execution
for ir_ins in current_ir_block:
TypeError: 'NoneType' object is not iterable
And i wonder why the irblock is none :/
print('debug: ', hex(loc_db.get_location_offset(address_loc))) I added this in symbolic execution function. So i can get which asm block the error occurs.
1
2
3
4
5
6
7
8
9
10
11
12
13
[...]
debug: 0x1400534d7
debug: 0x1400534f4
debug: 0x14005351d
debug: 0x14005353a
debug: 0x140053b5f
Traceback (most recent call last):
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 179, in <module>
r = symbolic_execution(sb, curr_addr_loc)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 145, in symbolic_execution
for ir_ins in current_ir_block:
TypeError: 'NoneType' object is not iterable
Block at 0x140053b5f
idiv instruction creates a split in ircfg due to its exception handling.
1
2
3
4
5
6
7
8
for ins in current_asm_block.lines:
if ins.name == 'IDIV': # just added this in the symb exe
address_loc = ins.offset + ins.l # skipped the idiv instruction execution
elif ins.name in ['CMP', 'TEST']:
cmp_ins = ins
elif ins.name.startswith('CMOV'):
cmovcc_ins = ins
break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python cff_slowtempest.py
imagebase: 0x140000000
RET: 0x140053d73
STATE: @64[RSP + 0x50]
CMOVNZ 0x1400536f4 CC_EQ(zf)?(loc_key_167,loc_key_166)
CMOVG 0x140053791 CC_S>(nf, of, zf)?(loc_key_175,loc_key_176)
CMOVZ 0x1400538ca CC_EQ(zf)?(loc_key_170,loc_key_171)
CMOVA 0x140053993 CC_U>(cf, zf)?(loc_key_186,loc_key_187)
CMOVNZ 0x140053a88 CC_EQ(zf)?(loc_key_189,loc_key_188)
CMOVG 0x140053810 CC_S>(nf, of, zf)?(loc_key_184,loc_key_185)
CMOVNZ 0x140053b00 CC_EQ(zf)?(loc_key_169,loc_key_168)
CMOVNZ 0x140053ba2 CC_EQ(zf)?(loc_key_183,loc_key_182)
CMOVL 0x140053c55 CC_S<(nf, of)?(loc_key_177,loc_key_178)
function prologue 0x140052a70
Patched: slowtempest.cff_deob.bin
Lets check !!!
Now we want to deobfuscate the entire binary CFG.
First i will get all the addresses of the functions from ida python.
1
2
3
4
5
6
7
8
9
import idautils
import idc
funcs = []
for ea in idautils.Functions():
flags = idc.get_func_flags(ea)
if flags & idc.FUNC_LIB:
continue # skip library functions
funcs.append(ea)
we got all the function addresses excluding the library functions.
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
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
def calc_flattening_score(asm_graph, loc_db):
# (c) Tim Blazytko 2021
# implementation based on the blog post "Automated Detection of Control-flow Flattening"
# https://synthesis.to/2021/03/03/flattening_detection.html
# init score
score = 0.0
# walk over all entry nodes in the graph
for head in asm_graph.heads_iter():
# compute dominator tree
dominator_tree = asm_graph.compute_dominator_tree(head)
# walk over all basic blocks
for block in asm_graph.blocks:
# get location key for basic block via basic block address
block_key = loc_db.get_offset_location(block.lines[0].offset)
# get all blocks that are dominated by the current block
dominated = set(
[block_key] + [b for b in dominator_tree.walk_depth_first_forward(block_key)])
# check for a back edge
if not any([b in dominated for b in asm_graph.predecessors(block_key)]):
continue
# calculate relation of dominated blocks to the blocks in the graph
score = max(score, len(dominated)/len(asm_graph.nodes()))
return score
# init symbol table
loc_db = LocationDB()
# open the binary for analysis
container = Container.from_stream(open('slowtempest.jmp_deob.bin', 'rb'), loc_db)
# cpu abstraction
machine = Machine(container.arch)
# init disassemble engine
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
flat = []
for addr in funcs: # all the functions we got from IDA (except the library funcs)
asm_cfg = mdis.dis_multiblock(addr)
flattening_score = calc_flattening_score(asm_cfg, loc_db)
if flattening_score > 0.7: # this condition after i noticed that all functions that were below 7 were not obfuscated
print(f"flattening score {flattening_score} for function {hex(addr)}")
flat.append(addr)
print(flat) # all the flattened functions.
Now we got all the flattened functions, and we will iterate through the flattened function addresses and then find each flattened functions dispatcher and pre-dispatcher locations. As pre-dispatcher is the basic block with maximum number of predecessors (there are some exception you will see later), and the successors of pre-dispatcher is the dispatcher block. So we have to find the basic block which has the highest number of predecessors that will be our pre-dispatcher and then its successor is the dispatcher block.
1
2
3
4
5
pre_disp_loc = max(asmcfg.blocks, key=lambda key: len(asmcfg.predecessors(key.loc_key))).loc_key # getting the pre-disp location
disp_loc = asmcfg.successors(pre_disp_loc) # expected successor.
assert len(disp_loc) == 1 # the pre-disp has only one successor
disp_addr_loc = disp_loc[0]
And now we are good to go? just perform the iteration and each funciton will be deobfuscated. WRONG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
imagebase: 0x140000000
0x140001000 -> 0x140001036 0x140001715
CMOVNZ 0x14000146e CC_EQ(zf)?(loc_key_56,loc_key_55)
CMOVZ 0x1400015da CC_EQ(zf)?(loc_key_53,loc_key_54)
CMOVZ 0x1400014f1 CC_EQ(zf)?(loc_key_57,loc_key_58)
0x140001720 -> 0x140001751 0x140001bb6
CMOVNZ 0x140001a78 CC_EQ(zf)?(loc_key_40,loc_key_39)
CMOVZ 0x140001afa CC_EQ(zf)?(loc_key_41,loc_key_42)
0x140001bc0 -> 0x140001bfa 0x1400022eb
CMOVNZ 0x1400020c2 CC_EQ(zf)?(loc_key_59,loc_key_58)
CMOVZ 0x140002145 CC_EQ(zf)?(loc_key_62,loc_key_63)
CMOVZ 0x140002193 CC_EQ(zf)?(loc_key_60,loc_key_61)
0x1400022f0 -> 0x140002485 0x140002483
Traceback (most recent call last):
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 238, in <module>
deobfuscate(addr=addr)
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 161, in deobfuscate
state_variable = disp_block.lines[0].get_args_expr()[1]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^
IndexError: list index out of range
I added the debug print that shows on which function we are currently at what are its dispatcher are predispatcher block addresses. So lets visit the function at 0x1400022f0
It identified the predipatcher incorrectly, It has the same numbers of predecessors as the pre-dispatcher has. But it comes first due to the BFS traversal of asmcfg.blocks order. So to counter this exception we have to see if the dispatchers first instruction is a jmp than we will find the second max block which will give us the pre-dispatcher.
1
2
3
4
5
6
7
8
9
10
11
12
13
pre_disp_loc = sorted(asmcfg.blocks, key=lambda key: len(asmcfg.predecessors(key.loc_key)), reverse=True)[0].loc_key
disp_loc = asmcfg.successors(pre_disp_loc)
assert len(disp_loc) == 1
disp_addr_loc = disp_loc[0]
print(hex(addr), ' -> ', hex(loc_db.get_location_offset(disp_addr_loc)), hex(loc_db.get_location_offset(pre_disp_loc))) # debug
check_dispatcher = asmcfg.loc_key_to_block(disp_addr_loc)
if check_dispatcher.lines[-1].name == 'JMP':
pre_disp_loc = sorted(asmcfg.blocks, key=lambda key: len(asmcfg.predecessors(key.loc_key)), reverse=True)[1].loc_key # getting the second max block which was left behind due to BFS on asmcfg.blocks
disp_loc = asmcfg.successors(pre_disp_loc)
assert len(disp_loc) == 1
disp_addr_loc = disp_loc[0]
print('Fixed exception', hex(addr), ' -> ', hex(loc_db.get_location_offset(disp_addr_loc)), hex(loc_db.get_location_offset(pre_disp_loc)))
Ok so now the wrong pre-dispatcher and dispatcher identification is fixed. But we got another one.
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
imagebase: 0x140000000
0x140001000 -> 0x140001036 0x140001715
STATE: @64[RSP + 0x28]
CMOVNZ 0x14000146e CC_EQ(zf)?(loc_key_56,loc_key_55)
CMOVZ 0x1400015da CC_EQ(zf)?(loc_key_53,loc_key_54)
CMOVZ 0x1400014f1 CC_EQ(zf)?(loc_key_57,loc_key_58)
0x140001720 -> 0x140001751 0x140001bb6
STATE: @64[RSP + 0x28]
CMOVNZ 0x140001a78 CC_EQ(zf)?(loc_key_40,loc_key_39)
CMOVZ 0x140001afa CC_EQ(zf)?(loc_key_41,loc_key_42)
0x140001bc0 -> 0x140001bfa 0x1400022eb
STATE: @64[RSP + 0x30]
CMOVNZ 0x1400020c2 CC_EQ(zf)?(loc_key_59,loc_key_58)
CMOVZ 0x140002145 CC_EQ(zf)?(loc_key_62,loc_key_63)
CMOVZ 0x140002193 CC_EQ(zf)?(loc_key_60,loc_key_61)
0x1400022f0 -> 0x140002485 0x140002483
Fixed exception 0x1400022f0 -> 0x14000231c 0x140002549
STATE: @64[RSP + 0x30]
CMOVNZ 0x14000248a CC_EQ(zf)?(loc_key_22,loc_key_21)
0x140002550 -> 0x140002e3e 0x140002e3c
Fixed exception 0x140002550 -> 0x14000275f 0x140003648
STATE: @64[RSP + 0xC0]
CMOVNZ 0x140002e43 CC_EQ(zf)?(loc_key_112,loc_key_111)
CMOVZ 0x140003022 CC_EQ(zf)?(loc_key_117,loc_key_118)
CMOVNZ 0x140003219 CC_EQ(zf)?(loc_key_116,loc_key_115)
Traceback (most recent call last):
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 248, in <module>
deobfuscate(addr=addr)
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 204, in deobfuscate
curr_state_val = int(sb.eval_expr(state_variable))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'ExprMem'
I added the debug print again in OBB to check in which basic block the crash occurs the the state variable is ExprMem.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
debug: 0x140002f77 0xA7D2E3F6154E686C @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x140002f93 0xA7D2E3F62A1F3701 @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x140002fd5 0xA7D2E3F62A1F3701 @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x140002fff 0xA7D2E3F62A1F3701 @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x140003022 0xA7D2E3F62A1F3701 @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
CMOVZ 0x140003022 CC_EQ(zf)?(loc_key_117,loc_key_118)
debug: 0x140003196 0xA7D2E3F6A124348D @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x1400031b1 0xA7D2E3F6A124348D @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x1400031cd 0xA7D2E3F6651362FD @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x1400031f4 0xA7D2E3F6651362FD @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x140003219 0xA7D2E3F6651362FD @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
CMOVNZ 0x140003219 CC_EQ(zf)?(loc_key_116,loc_key_115)
debug: 0x140003397 0xA7D2E3F693812698 @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprInt'>
debug: 0x14000346d @64[call_func_stack(0x140008840, RSP_init) + 0xC0] @64[RSP + 0xC0] <class 'miasm.expression.expression.ExprMem'>
Traceback (most recent call last):
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 252, in <module>
deobfuscate(addr=addr)
File "/home/xyz/working/deob/miasm_101/slow/cff_slowtempest.py", line 205, in deobfuscate
curr_state_val = int(sb.eval_expr(state_variable))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'ExprMem'
So it crashed on 0x14000346d
The final state at the end of the Basic block the state calculation is not determined between this block just the last split, but the crash is before that, so i just restored the previous state if the state is an ExprMem.
1
2
3
4
5
6
7
8
9
10
11
12
evaluated_state = sb.eval_expr(state_variable)
if isinstance(evaluated_state, ExprInt): # we need exprint states
curr_state_val = evaluated_state
last_good_state_val = curr_state_val # update previous state
elif isinstance(evaluated_state, ExprMem):
print('changing state: ', hex(loc_db.get_location_offset(curr_addr_loc)), sb.eval_expr(last_good_state_val), sb.eval_expr(state_variable))
sb.symbols[state_variable] = last_good_state_val
curr_state_val = last_good_state_val
This fixed the state variable not being ExprInt problem, and this is the example that there are several problems and edge cases always as i was working through the whole deobfuscator which also there were cases were original basic blocks were needed more pattern matching analysis. I was also not running the symbolic execution in a loop inside a cmovcc IR Block, that caused problems. So after many refactoring and fixing finally we ran the code.
Deobfuscation complete, the functions are unflattened and now only indirect call obfuscation is left.
Unflattened Binary
This is the whole deobfuscation script, as the snippets i gave they were before fixing some issues so they were not part of the class, as i was doing it without a class but those explain the core functionality of the deobfuscator. The Binrewriter can be found here
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB, LocKey
from miasm.expression.simplifications import ExpressionSimplifier, expr_simp
from miasm.arch.x86.regs import all_regs_ids, all_regs_ids_init
from miasm.expression.expression import ExprCond, ExprMem, ExprInt, ExprLoc, ExprVisitorCallbackTopToBottom
from miasm.ir.symbexec import SymbolicExecutionEngine
from collections import deque, defaultdict
from cff_patcher import BinaryRewriter
import shutil
import struct
import lief
class deob_slowtempest:
def __init__(self, file='slowtempest.cff_deob.bin', out=None):
self.file = file
self.out = out if out is not None else file
self.pe = lief.PE.parse(file)
print('imagebase: ', hex(self.pe.imagebase))
data_section = self.pe.get_section('.data')
self.u64 = lambda data, i: struct.unpack('<Q', data[i:i + 8])[0]
self.u32 = lambda data, i: struct.unpack('<I', data[i:i + 4])[0]
# concrete values of the .data section
self.concreteSymbols = {}
self.add_data(self.concreteSymbols, data_section.virtual_address + self.pe.imagebase, bytes(data_section.content))
self.custom_simp = ExpressionSimplifier()
self.custom_simp.enable_passes(ExpressionSimplifier.PASS_COMMONS)
self.custom_simp.enable_passes(ExpressionSimplifier.PASS_HIGH_TO_EXPLICIT)
self.custom_simp.enable_passes({ExprMem: [self.simpExprMem]})
def add_data(self, symb, offset, data):
for i in range(0, len(data), 8):
symb[ExprMem(ExprInt(offset + i, 64), 64)] = ExprInt(self.u64(data, i), 64)
for i in range(0, len(data), 4):
symb[ExprMem(ExprInt(offset + i, 32), 32)] = ExprInt(self.u32(data, i), 32)
def simpExprMemCallBack(self, expr):
if isinstance(expr, ExprMem):
if expr in self.concreteSymbols:
newExpr = expr.replace_expr({expr: self.concreteSymbols[expr]})
return newExpr
return None
def simpExprMem(self, _, expr):
# https://github.com/cea-sec/miasm/blob/master/miasm/expression/expression.py#L614
visitor = ExprVisitorCallbackTopToBottom(lambda expr: self.simpExprMemCallBack(expr))
return visitor.visit(expr)
def deobfuscate(self, addr):
loc_db = LocationDB()
container = Container.from_stream(open(self.file, 'rb'), loc_db=loc_db)
machine = Machine(container.arch)
lifter = machine.lifter_model_call(loc_db=loc_db)
mdis = machine.dis_engine(container.bin_stream, loc_db=loc_db)
asmcfg = mdis.dis_multiblock(addr)
ircfg = lifter.new_ircfg_from_asmcfg(asmcfg=asmcfg)
self.loc_db = loc_db
self.asmcfg = asmcfg
self.ircfg = ircfg
self.lifter = lifter
pre_disp_loc = sorted(asmcfg.blocks, key=lambda key: len(asmcfg.predecessors(key.loc_key)), reverse=True)[0].loc_key
disp_loc = asmcfg.successors(pre_disp_loc)
assert len(disp_loc) == 1
disp_addr_loc = disp_loc[0]
print(hex(addr), ' -> ', hex(loc_db.get_location_offset(disp_addr_loc)), hex(loc_db.get_location_offset(pre_disp_loc)))
check_dispatcher = asmcfg.loc_key_to_block(disp_addr_loc)
if check_dispatcher.lines[-1].name == 'JMP': # fixing the exception
pre_disp_loc = sorted(asmcfg.blocks, key=lambda key: len(asmcfg.predecessors(key.loc_key)), reverse=True)[1].loc_key
disp_loc = asmcfg.successors(pre_disp_loc)
assert len(disp_loc) == 1
disp_addr_loc = disp_loc[0]
print('Fixed exception', hex(addr), ' -> ', hex(loc_db.get_location_offset(disp_addr_loc)), hex(loc_db.get_location_offset(pre_disp_loc)))
OBB = self.findOBB(pre_disp_loc)
# the state variable read by the dispatcher, e.g. @64[RSP + 0x18]
disp_block = asmcfg.loc_key_to_block(disp_addr_loc)
state_variable = disp_block.lines[0].get_args_expr()[1]
print('STATE: ', state_variable)
init_symbols = {}
for i, r in enumerate(all_regs_ids):
init_symbols[r] = all_regs_ids_init[i]
state_to_state_map = defaultdict(list)
state_to_addr_map = defaultdict(list)
first_state_val = None
q = deque([(loc_db.get_offset_location(addr), first_state_val, init_symbols)])
visited_location = set()
while q:
curr_addr_loc, curr_state_val, symbols = q.popleft()
sb = SymbolicExecutionEngine(lifter, symbols, sb_expr_simp=self.custom_simp)
while True:
r = self.symbolic_execution(sb, curr_addr_loc)
if r is None:
break
if r.is_cond():
cond_true = {r.cond: ExprInt(1, 32)}; cond_false = {r.cond: ExprInt(0, 32)}
jt = self.to_loc_key(expr_simp(sb.eval_expr(r.replace_expr(cond_true), {})))
jf = self.to_loc_key(expr_simp(sb.eval_expr(r.replace_expr(cond_false), {})))
q.append((jf, curr_state_val, sb.symbols.copy())) # false branch for later
curr_addr_loc = jt
else:
curr_addr_loc = self.to_loc_key(expr_simp(sb.eval_expr(r)))
if curr_addr_loc in OBB:
evaluated_state = sb.eval_expr(state_variable)
if isinstance(evaluated_state, ExprInt): # we need exprint states
curr_state_val = evaluated_state
last_good_state_val = curr_state_val # update previous state
elif isinstance(evaluated_state, ExprMem):
print('changing state: ', hex(loc_db.get_location_offset(curr_addr_loc)), sb.eval_expr(last_good_state_val), sb.eval_expr(state_variable))
sb.symbols[state_variable] = last_good_state_val
curr_state_val = last_good_state_val
else:
print('state is some other type')
curr_state_val = int(curr_state_val)
if (curr_addr_loc, curr_state_val) in visited_location:
break
visited_location.add((curr_addr_loc, curr_state_val))
# print('debug: ', hex(loc_db.get_location_offset(curr_addr_loc)), sb.eval_expr(state_variable), state_variable, type(sb.eval_expr(state_variable)))
# check if the current address is already added in the correspinding state list
if curr_addr_loc not in state_to_addr_map[curr_state_val]:
state_to_addr_map[curr_state_val].append(curr_addr_loc)
if first_state_val is None:
first_state_val = curr_state_val
elif curr_addr_loc == pre_disp_loc:
nState_val = int(sb.eval_expr(state_variable))
if nState_val not in state_to_state_map[curr_state_val]:
state_to_state_map[curr_state_val].append(nState_val)
curr_state_val = None
# keep only the state blocks + prologue, drop the dispatcher tree
OBB = []
for locations in state_to_addr_map.values():
for loc in locations:
OBB.append(loc)
state_to_state_map[0].append(first_state_val)
for loc in asmcfg.predecessors(disp_addr_loc):
if loc == pre_disp_loc: # skipping the pre-dispatcher because its also predecessors
continue
func_prologue = loc # then the predecessors is function prologue
OBB.append(func_prologue)
state_to_addr_map[0].append(func_prologue)
# chaining the prologue if its split.
prev_prologue_block = asmcfg.predecessors(func_prologue)
if len(prev_prologue_block) == 0:
break
while len(prev_prologue_block) != 0:
OBB.append(prev_prologue_block[0])
state_to_addr_map[0].append(prev_prologue_block[0])
assert len(prev_prologue_block) == 1
prev_prologue_block = asmcfg.predecessors(prev_prologue_block[0])
break # because we have the function prologue blocks
self.remove_irrelevant_BB(original_BB=OBB)
state_to_addr_map[0] = state_to_addr_map[0][::-1] # correct the order of function prologue
# patche function
rewriter = BinaryRewriter(container.arch, original_filename=self.file,
va_function_lief=lambda addr: self.pe.va_to_offset(addr))
rewriter.patch_binary(asmcfg, addr, state_to_state_map, state_to_addr_map, sb, out_filename=self.out)
def to_loc_key(self, expr):
if isinstance(expr, LocKey):
return expr
if isinstance(expr, ExprLoc):
return expr.loc_key
if isinstance(expr, int):
return self.loc_db.get_offset_location(expr)
if isinstance(expr, ExprInt):
return self.loc_db.get_offset_location(int(expr))
return None
def remove_irrelevant_BB(self, original_BB):
irrelevant_locations = []
for bb in self.asmcfg.blocks:
if bb.loc_key not in original_BB:
irrelevant_locations.append(bb.loc_key)
for loc in irrelevant_locations:
self.asmcfg.del_block(self.asmcfg.loc_key_to_block(loc))
def fallthrough_preds(self, loc):
preds = self.asmcfg.predecessors(loc)
if len(preds) <= 1:
return preds
ft = [p for p in preds if not self.asmcfg.loc_key_to_block(p).lines[-1].name.startswith('J')]
return ft if ft else preds
def findOBB(self, pre_disp_loc):
OBB = []
# for original basic blocks
for curr_loc in self.asmcfg.predecessors(pre_disp_loc):
pre_obb_loc = self.fallthrough_preds(curr_loc); OBB.append(curr_loc)
assert len(pre_obb_loc) == 1
while True:
pre_obb_block = self.asmcfg.loc_key_to_block(pre_obb_loc[0])
if pre_obb_block.lines[-1].name.startswith('J'):
break
curr_loc = pre_obb_loc[0]
pre_obb_loc = self.fallthrough_preds(curr_loc)
assert len(pre_obb_loc) == 1
OBB.append(curr_loc)
# for ret basic blocs/tail calls
for bb in self.asmcfg.blocks:
ret_loc = bb.loc_key
if len(self.asmcfg.successors(ret_loc)) == 0:
ret_pre_loc = self.fallthrough_preds(ret_loc); OBB.append(ret_loc)
assert len(ret_pre_loc) == 1
while True:
ret_pre_block = self.asmcfg.loc_key_to_block(ret_pre_loc[0])
if ret_pre_block.lines[-1].name.startswith('J'):
break
ret_loc = ret_pre_loc[0]
ret_pre_loc = self.fallthrough_preds(ret_loc)
assert len(ret_pre_loc) == 1
OBB.append(ret_loc)
return OBB
def symbolic_execution(self, sb, address_loc):
cmp_ins = None
cmovcc_ins = None
current_asm_block = self.asmcfg.loc_key_to_block(address_loc)
if current_asm_block is None:
return sb.run_block_at(ircfg=self.ircfg, addr=address_loc)
for ins in current_asm_block.lines:
if ins.name == 'IDIV':
address_loc = ins.offset + ins.l # skipped the idiv instruction execution (a cheap fix xD)
elif ins.name in ['CMP', 'TEST']:
cmp_ins = ins
elif ins.name.startswith('CMOV'):
cmovcc_ins = ins
break
if current_asm_block.lines[-1].name == 'CALL':
rsp = sb.symbols[self.lifter.arch.regs.RSP]; rbp = sb.symbols[self.lifter.arch.regs.RBP]
r = sb.run_block_at(ircfg=self.ircfg, addr=address_loc)
sb.symbols[self.lifter.arch.regs.RSP] = rsp; sb.symbols[self.lifter.arch.regs.RBP] = rbp
return r
if cmovcc_ins is not None and cmp_ins is not None:
curr_loc = address_loc
while True:
current_ir_block = self.ircfg.get_block(addr=curr_loc)
for ir_ins in current_ir_block:
if ir_ins.instr.name.startswith('CMOV'):
cmov_cond_expr = ir_ins.values()[-1]
sb.run_block_at(ircfg=self.ircfg, addr=curr_loc) # this was fixed later. (without this the ir block execution is not valid/reliable)
if str(cmovcc_ins).startswith('CMOVN'):
print(ir_ins.instr.name, ' ', hex(self.loc_db.get_location_offset(self.to_loc_key(curr_loc))), cmov_cond_expr)
return cmov_cond_expr.copy()
print(ir_ins.instr.name, ' ', hex(self.loc_db.get_location_offset(self.to_loc_key(curr_loc))), cmov_cond_expr)
return ExprCond(cmov_cond_expr.cond.copy(), cmov_cond_expr.src2.copy(), cmov_cond_expr.src1.copy()) # flip the condition
curr_loc = sb.run_block_at(ircfg=self.ircfg, addr=curr_loc)
else:
return sb.run_block_at(ircfg=self.ircfg, addr=address_loc)
if __name__ == '__main__':
shutil.copy("slowtempest.jmp_deob.bin", "slowtempest.cff_deob.bin")
deob = deob_slowtempest()
for addr in [0x140001000,0x140001720,0x140001bc0,0x1400022f0,0x140002550,0x140003990,0x140003e30,0x140004dd0,0x1400051f0,0x1400055c0,0x140005970,0x140005c30,0x140006590,0x140007200,0x140007790,0x140007d30,0x1400082c0,0x140008840,0x140008dc0,0x140009350,0x1400098d0,0x140009e40,0x14000a3c0,0x14000a950,0x14000afb0,0x14000b260,0x14000bdb0,0x14000c3b0,0x14000c930,0x14000d040,0x14000dbc0,0x14000df90,0x14000f0f0,0x14000f780,0x14000ff30,0x140010600,0x1400115c0,0x140011b40,0x1400120d0,0x1400128c0,0x140014120,0x140016780,0x140016d30,0x1400179e0,0x140017d90,0x1400184c0,0x140018920,0x140018f10,0x1400199e0,0x140019bf0,0x14001a2b0,0x14001a5d0,0x14001ac90,0x14001b170,0x14001b670,0x14001bae0,0x14001c100,0x14001c500,0x14001c730,0x14001c950,0x14001d060,0x14001d7f0,0x14001e050,0x14001e4f0,0x14001f8f0,0x14001fc50,0x140021970,0x140021d70,0x140022930,0x140023300,0x140023660,0x1400240e0,0x140024810,0x140024ce0,0x140025150,0x140025d20,0x140026660,0x140026be0,0x140027170,0x140027700,0x140027c90,0x140028210,0x1400287a0,0x140028d20,0x1400292a0,0x140029830,0x140029dc0,0x14002a350,0x14002a8d0,0x14002ae50,0x14002b530,0x14002bd40,0x14002c300,0x14002caa0,0x14002d360,0x14002ee20,0x14002f550,0x14002f7c0,0x1400303f0,0x140031a10,0x1400325f0,0x140032ad0,0x140033260,0x1400337f0,0x140033d70,0x140034330,0x140034d60,0x1400352d0,0x1400355b0,0x140035d70,0x140036bb0,0x140037240,0x14003f830,0x14003fe60,0x1400403f0,0x140040970,0x140040f00,0x1400422c0,0x140042da0,0x140043660,0x140043b30,0x1400441a0,0x140044720,0x140044cb0,0x140045230,0x1400457c0,0x140045d40,0x140045fc0,0x140046240,0x1400465f0,0x140046d30,0x140047160,0x1400474d0,0x1400480d0,0x1400483d0,0x1400495d0,0x140049e50,0x14004a3d0,0x14004a950,0x14004aee0,0x14004b470,0x14004b9f0,0x14004c1f0,0x14004c4e0,0x14004c830,0x14004cba0,0x14004d270,0x14004d4d0,0x14004d7f0,0x14004db10,0x14004dfa0,0x14004e470,0x14004e980,0x14004ed50,0x14004f190,0x14004f680,0x14004fb10,0x1400506a0,0x140050e80,0x1400517c0,0x140052140,0x140052680,0x140053e80,0x140054230,0x140054870,0x140054e00,0x140055390,0x140055910,0x140055e90,0x140056410]:# the function address we got from 7+ flatttening score. (some functions were false positives btw, and tbh i don't know why xD)
deob.deobfuscate(addr=addr)
print('written: ', deob.out)
Indirect Call Obfuscation
The binary has compelete indirect call obfuscation.
1
2
3
4
5
6
7
8
9
import idc
ea = 0x140001000 # start of text section
ind = []
while ea < 0x140057495: # end of text section
if idc.print_insn_mnem(ea) == 'call' and idc.get_operand_type(ea, 0) is idc.o_reg: # call reg
print(hex(ea)); ind.append(ea)
ea = idc.next_head(ea)
print('indirect calls: ', len(ind))
1
indirect calls: 945
But as this is the last section of the post and we have extensively used miasm and its capabilities, I wanted to do it differently, So we will use hexrays decompiler hooks to manipulate microcode hexrays own IR, and by using hexrays api we can easily manipulate different stages of microcode with out custom optimizations which can strip away intended obfuscation.
fortunately enough for our sample the indirect call pattern is not complicated, first rax is loaded with a constant, and then it added with another constant which will be read from the .data section.
Hexrays Microcode
Microcode has different maturity levels. Each maturity level apply specific type of optimizations.
First one is MMAT GENERATED. It preserves all the eflag semantics per instruction. Its the first microcode layer which is generated.
Second is MMAT PREOPTIMIZED. Use-Def info is used to do deadcode removal and constant propagation.
Third is MMAT LOCOPT. This what we will deal with today and manipulate via decompiler hook.
There are other layers as well but we will only deal with LOCOPT. You can read about microcode here.
If you want to play with microcode you need to know its various classes and their members only than you know the functionality of what can be done.
If you want to play with microcode instructions which are misn_t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[...]
class minsn_t
{
void hexapi init(ea_t _ea);
void hexapi copy(const minsn_t &m);
public:
mcode_t opcode;
int iprops;
minsn_t *next;
minsn_t *prev;
ea_t ea;
mop_t l;
mop_t r;
mop_t d;
[...]
And the operands which are mop_t
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
//-------------------------------------------------------------------------
class mop_t
{
void hexapi copy(const mop_t &rop);
public:
mopt_t t;
uint8 oprops;
#define OPROP_IMPDONE 0x01
#define OPROP_UDT 0x02
#define OPROP_FLOAT 0x04
#define OPROP_CCFLAGS 0x08
#define OPROP_UDEFVAL 0x10
#define OPROP_LOWADDR 0x20
#define OPROP_ABI 0x40
uint16 valnum;
int size;
union
{
mreg_t r; // mop_r register number
mnumber_t *nnn; // mop_n immediate value
minsn_t *d; // mop_d result (destination) of another instruction
stkvar_ref_t *s; // mop_S stack variable
ea_t g; // mop_v global variable (its linear address)
int b; // mop_b block number (used in jmp,call instructions)
mcallinfo_t *f; // mop_f function call information
lvar_ref_t *l; // mop_l local variable
mop_addr_t *a; // mop_a variable whose address is taken
char *helper; // mop_h helper function name
char *cstr; // mop_str utf8 string constant, user representation
mcases_t *c; // mop_c cases
fnumber_t *fpc; // mop_fn floating point constant
mop_pair_t *pair; // mop_p operand pair
scif_t *scif; // mop_sc scattered operand info
};
// -- End of data fields, member function declarations follow:
[...]
Only with this information we start playing with microcode.
We can see that the icall is formed by just addition of a global constant with another constant. We will make two classes one for the decompiler hook and the other which visit the instructions and optimizer, Which will just replace the global data reference with the constant one so that constant propagation can be applied to it and the decompiler will see the constant propagated final IR so we will se indirect call deobfuscation in our decompiler.
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
import idaapi
import ida_hexrays
dataStart = 0x14007E000
dataEnd = 0x140093FFF
simplify = False
class operand_optimizer(ida_hexrays.minsn_visitor_t):
def visit_minsn(self):
global simplify
insn = self.curins # the current instruction (minsn_t) to be visited
if insn.opcode in (ida_hexrays.m_add, ida_hexrays.m_sub): # memory operands that appear as operands of an add/sub
for op in (insn.l, insn.r):
# we can see these in the reference i provided and also can see ida sdk for hexrays.hpp
if op.t == ida_hexrays.mop_v and op.size == 8 and op.g >= dataStart and op.g <= dataEnd:
ea = op.g
const = int.from_bytes(idaapi.get_bytes(ea,8),byteorder="little") # reading the global constant
op.make_number(const, 8) # making it constant
if self.blk:
self.blk.mark_lists_dirty()
simplify = True # used in the decompiler hook
return 0
class DecompilerHook(ida_hexrays.Hexrays_Hooks):
def locopt(self, mba):
#walk through the mblocks
global simplify
simplify = True
while simplify:
simplify = False
repl = operand_optimizer()
mba.for_all_insns(repl)
mba.optimize_local(0)
mba.verify(True) # verify microcode
print("Verified")
return 0
event_hook = DecompilerHook()
event_hook.hook()
We have come a long way, From a very obfuscated binary where there was only jmp rax in the decompiler to CFF to unflatten and then resolved indirect calls. We have made so far ! <3
Here is the Deobfuscated binary :p
Clean Deobfuscated Functions
Whatโs Next !?
We missed a very hot topic in obfuscations which is MBA we will use Triton and Cobra, It requires its own post. I will also write about deobfuscation via LLVM IR using Remil. So stay tuned, See you soon.






















































