-
Notifications
You must be signed in to change notification settings - Fork 63
/
vulfi.py
1534 lines (1396 loc) · 75.6 KB
/
vulfi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from ast import expr_context
from cmath import exp
import collections
from email.policy import default
from uuid import RESERVED_FUTURE
import idaapi
import idc
import ida_ua
import os
import json
import idautils
import ida_kernwin
import ida_name
import ida_hexrays
import ida_funcs
import traceback
import ida_ida
icon = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00*\x00\x00\x00&\x08\x06\x00\x00\x00\xb2\x01\t \x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\x00\x02TIDATXG\xcd\x98\xcd.\x03Q\x14\xc7\xef\x10\x14-\xd5\xf8Jj!x\x00+,\xc5\x03\x88\x07\xb0\xb0\xb2\xea\x82\'\xc0\x13\x90\xb0\x15\x8f\x80x\x00+k\x89\xd8\x93\xb0\x94\xb0\x13A\xc6\xf9\xb7skz\xe7\xcc\xf4\xdc\xf9\xec/\xf9\xa5\x9d\x99\xce\xbdg\xce\xdc9\xf7N\x15\xb1@\x1e\x93\xf3\xd8\xe81\xaa\xe4\x91\xf7\xd9\xe4\x9etI\x04\xdc\x0b \xb0=\xf2\x89\xbc\xc0\x0e\r\xb2\x89@\xe1;\xb9C\x16\x05\xfaF\x80:\x9ee\xb2\x83KR\x1f\x84\xc8\xf2:\x99\x17\xe8K\xdfY\xed\x01\x19\xc0\x9fU\xbf\xb8\x80\xc0U\xa5\x08\xda\xbe%\xcd~qg\xdbc\xd3\x04\xe3\xc1<A\x9b\xf6\xf8E\x80h\x13\x01q\xfd\xb1\xd9\xd4\xe0\n\xb8\x93\xfcF6 \x00}D\x05\x081FC\xb3\xa9A#\xdc\xc9~1\x96l\x1f8I\x80Zq\xdb\xfe\xa7.J\x04\xbcEF\x81\x00\xf1\x1bI\x80\x10\xe3U\x0c\x1a\xe6\x1a\t\x13\x0f\x1c7apOr7\xad+\x8d4\xab~\xf10"`tf\x96;\x898\xc7\x1at\xc65\x96\x95\x18\x1a\xb1\xa7q\xae\xbeee\xa2\xf2\x97WV\x91\xcd\xae\xe5\xa8\x1b\x81\xb1\xd6glK\x1dp\x1c\xb7\x9fd\x8ea\x01\x92\x98\xc0\xd4\xeaxn\x0e\x97;\xf6G\xb9:Tj\x9e\xc3\x1c\x13\x15w)\x81\xa9\x15\x9d\xdeL\xd5\xdd\xdd\xf2x\xc7~\xceF\xa5\xea\x9e\xd5f\xc2\x02Mu\xa5\x86+\x0etrM\x81\xbe\xcd-\xb9\x1b\xa5\x91\xc01\xed\xf6\xe8\x98\xfbZ_tO\'\xa6\xb9\xe3\xa8\xb1"h\xb8\x89\xf8 OZ_\x83\x9c\xd7f\xd5\xda\xd0\xb0\xb7\xf5\xcf\xca`I\x1d\x8dO\xaa\x92C\xb9\xe4\xc1\xea]\x844P\xb0O>\xb7\xbe\xb6x\xfc\xfeRw_\x9f\xea\x81>\x1b\xe5\xaa\x1aq\xfe\x9bCp\x8d\xcaD\xfb7/\xbf?\xde\x916W\x9e\x99\x80\xf1\xc4\xdd\xc28ZO\x95\xb6\xc4\x99ZMcM\x95\xb6\xd8.XLQ\xdc\xb3|c\xe8 IV\x93.\xbc\xad\x88;\xb5&Zx\xc4%\xce\x82%\xd7lj0\xce\xb8`\xc2Lu\xaa\xb4%\xea\xad\xd5\xb4\x90lj\xd8\xa9\x95\x11Sea\xd9\xd4\x1c\x92\\p~3/\xee\x12\x90\xa9\xa8r\x95Kq\x97\x82\xf1\xc7\x05\ts+\xee\x12\xc2\xb2Z\xe8\x03\x14\x86\xb9`I\xe5=(+\xfcY\xed\xc9ljtV\x0b-\xeeR\xe2\xfc\x81V\x08\x19dR\xa9?"\x80\x16\n\xa6\x0c\x13@\x00\x00\x00\x00IEND\xaeB`\x82'
icon_id = idaapi.load_custom_icon(data=icon, format="png")
class utils:
def create_dummy_hexrays_const(value):
num = ida_hexrays.cexpr_t()
num.op = ida_hexrays.cot_num
num.n = ida_hexrays.cnumber_t()
num.n._value = value
return num
def get_negative_disass(op):
# Get negative value based on the size of the operand
if op.dtype == 0x0: # Byte
return -(((op.value & 0xff) ^ 0xff) + 1)
elif op.dtype == 0x1: # Word
return -(((op.value & 0xffff) ^ 0xffff) + 1)
elif op.dtype == 0x2: # Dword
return -(((op.value & 0xffffffff) ^ 0xffffffff) + 1)
elif op.dtype == 0x7: # Qword
return -((op.value ^ 0xffffffffffffffff) + 1)
def get_func_name(ea):
# Get pretty function name
func_name = utils.get_pretty_func_name(idc.get_func_name(ea))
if not func_name:
func_name = utils.get_pretty_func_name(idc.get_name(ea))
return func_name
def get_pretty_func_name(name):
# Demangle function name
demangled_name = idc.demangle_name(name, idc.get_inf_attr(idc.INF_SHORT_DN))
# Return as is if failed to demangle
if not demangled_name:
return name
# Cut arguments
return demangled_name[:demangled_name.find("(")]
def prep_func_name(name):
if name[0] != "." and name[0] != "_":
# Name does not start with dot or underscore
return [name,f".{name}",f"_{name}"]
else:
return [name[1:],f".{name[1:]}",f"_{name[1:]}"]
class null_after_visitor(ida_hexrays.ctree_visitor_t):
def __init__(self,decompiled_function,call_xref,func_name,matched):
self.found_call = False
ida_hexrays.ctree_visitor_t.__init__(self, ida_hexrays.CV_FAST) # CV_FAST does not keep parents nodes in CTREE
self.decompiled_function = decompiled_function
self.call_xref = call_xref
self.func_name = func_name
self.insn_counter = 0
self.matched = matched
def visit_insn(self, i):
if self.found_call:
self.insn_counter += 1
return 0
def visit_expr(self, e):
if e.op == ida_hexrays.cot_call:
xref_func_name = utils.get_func_name(e.x.obj_ea)
if not xref_func_name:
xref_func_name = idc.get_name(e.x.obj_ea)
if xref_func_name.lower() == self.func_name.lower() and e.ea == self.call_xref:
self.found_call = True
if self.found_call and self.insn_counter < 2:
if e.op == ida_hexrays.cot_asg:
# The expression is assignment, check the right side of the assign
if e.y.op == ida_hexrays.cot_num:
# The right side is number
if e.y.numval() == 0:
# Set to null
self.matched["set_to_null"] = True
return 0
class VulFiScanner:
def __init__(self,custom_rules=None):
# Init class-wide variables
self.functions_list = []
if not custom_rules:
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),"vulfi_rules.json"),"r") as rules_file:
self.rules = json.load(rules_file)
else:
self.rules = custom_rules
with open(os.path.join(os.path.dirname(os.path.realpath(__file__)),"vulfi_prototypes.json"),"r") as proto_file:
self.prototypes = json.load(proto_file)
# get pointer size
if ida_ida.inf_is_64bit():
self.ptr_size = 8
elif ida_ida.inf_is_32bit_exactly():
self.ptr_size = 4
else:
self.ptr_size = 2
# Get endianness
self.endian = "big" if ida_ida.inf_is_be() else "little"
# Check whether Hexrays can be used
if not ida_hexrays.init_hexrays_plugin():
self.hexrays = False
self.strings_list = idautils.Strings()
else:
self.hexrays = True
#self.strings_list = idautils.Strings()
def start_scan(self,ignore_addr_list):
results = []
ida_kernwin.show_wait_box("VulFi scan running ... ")
self.prepare_functions_list()
for rule in self.rules:
try:
if rule["function_names"] == ["Array Access"]:
xrefs_dict = self.get_array_accesses()
elif rule["function_names"] == ["Loop Check"]:
xrefs_dict = self.get_loops()
else:
xrefs_dict = self.find_xrefs_by_name(rule["function_names"],rule["wrappers"])
except:
ida_kernwin.warning("This does not seem like a correct rule file. Aborting scan.")
return None
for scanned_function in xrefs_dict:
# For each function in the rules
skip_count = 0
counter = 0 # For UI only
total_xrefs = len(xrefs_dict[scanned_function]) # For UI only
for scanned_function_xref_tuple in xrefs_dict[scanned_function]:
counter += 1
if ida_kernwin.user_cancelled():
print("[VulFi] Scan canceled!")
ida_kernwin.hide_wait_box()
return None
if skip_count > 0:
skip_count -= 1
continue
param = []
param_count = 0
function_call = None
scanned_function_xref = scanned_function_xref_tuple[0] # Address
scanned_function_name = scanned_function_xref_tuple[1] # Name
scanned_function_display_name = scanned_function_xref_tuple[2] # Display Name
# Update progress bar
ida_kernwin.replace_wait_box(f"Scanning {scanned_function_display_name} ({counter}/{total_xrefs})")
# If this rule is already verified for this address, move on
if f"{rule['name']}_{hex(scanned_function_xref)}" in ignore_addr_list:
continue
# For each xref to the function in the rules
# If ida_hexrays can be used eval the conditions in the rules file
if rule["function_names"] == ["Array Access"]:
param = [VulFiScanner.Param(self,scanned_function_xref_tuple[3],scanned_function_xref,scanned_function_name),VulFiScanner.Param(self,scanned_function_xref_tuple[4],scanned_function_xref,scanned_function_name)]
elif rule["function_names"] == ["Loop Check"]:
param = [VulFiScanner.Param(self,scanned_function_xref_tuple[3],scanned_function_xref,scanned_function_name),VulFiScanner.Param(self,scanned_function_xref_tuple[4],scanned_function_xref,scanned_function_name)]
else:
params_raw = self.get_xref_parameters(scanned_function_xref,scanned_function_name)
if params_raw is None:
if self.hexrays:
# Likely decompiler failure, we have to skip
continue
else:
# Params were not found, lets mark all XREFS
params_raw = []
for p in params_raw:
param.append(VulFiScanner.Param(self,p,scanned_function_xref,scanned_function_name))
param_count = len(param)
function_call = VulFiScanner.FunctionCall(self,scanned_function_xref,scanned_function_name)
# Name of the function where the xref is located
found_in_name = utils.get_func_name(scanned_function_xref)
priority = ""
# Every xref will be marked as Info in case that params fetch fails
try:
if not param:
priority = "Info"
elif eval(rule["mark_if"]["High"],dict(self=self, param=param,param_count=param_count,function_call=function_call)):
priority = "High"
elif eval(rule["mark_if"]["Medium"],dict(self=self, param=param,param_count=param_count,function_call=function_call)):
priority = "Medium"
elif eval(rule["mark_if"]["Low"],dict(self=self, param=param,param_count=param_count,function_call=function_call)):
priority = "Low"
except IndexError:
# Decompiler output has fewer parameters than the function prototype
# Mark the issue with Info priority
priority = "Info"
#results.append(list(VulFi.result_window_row(rule["name"],scanned_function_display_name,found_in_name,hex(scanned_function_xref),"Not Checked","Low","")))
except Exception:
print(traceback.format_exc())
ida_kernwin.warning(f"The rule \"{rule}\" is not valid!")
continue
# If the rule matched and is not wrapped:
#if priority and found_in_name and not "wrapped" in scanned_function_display_name:
if priority and not "wrapped" in scanned_function_display_name:
results.append(list(VulFi.result_window_row(rule["name"],scanned_function_display_name,found_in_name,hex(scanned_function_xref),"Not Checked",priority,"")))
elif "wrapped" in scanned_function_display_name and priority:
skip_count = 0 # rule for the wrapped function matched so no need to skip calls to the wrapper
elif "wrapped" in scanned_function_display_name and priority == "":
# Rule for wrapped function did not match, skip the wrapper
skip_count = int(scanned_function_display_name[scanned_function_display_name.find("wrapped:") + 8:-1])
ida_kernwin.hide_wait_box()
return results
def get_loops(self):
# Use the trick to extract loop paramters as members of the tuple
# param[0] will be the counter variable and param[1] will be the check value
loop_check_list = {"Loop Check":[]}
if self.hexrays:
for func in self.functions_list:
try:
decompiled_function = ida_hexrays.decompile(func)
code = decompiled_function.pseudocode
for tree_item in decompiled_function.treeitems:
if tree_item.op == ida_hexrays.cit_while:
#print(f"[*] Found 'while' loop at: {hex(tree_item.ea)} ({ida_name.get_ea_name(func)})")
op = tree_item.to_specific_type.cwhile.expr.op
if (op >= ida_hexrays.cot_land and op <= ida_hexrays.cot_ult) or op in [ida_hexrays.cot_var, ida_hexrays.cot_lnot]:
# Not an infinte loop
counter, check_val = self.parse_condition(tree_item.to_specific_type.cwhile.expr)
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,check_val))
else:
counter, check_val = self.get_loop_check_val_break(tree_item.to_specific_type.cwhile.body)
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,check_val))
elif tree_item.op == ida_hexrays.cit_for:
#print(f"[*] Found 'for' loop at: {hex(tree_item.ea)} ({ida_name.get_ea_name(func)})")
if tree_item.to_specific_type.cfor.expr.op == ida_hexrays.cot_empty:
# Handle endless FOR loop
body = tree_item.to_specific_type.cfor.body
counter, check_val = self.get_loop_check_val_break(body)
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,check_val))
else:
counter = tree_item.to_specific_type.cfor.init.x
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,self.get_loop_check_val(counter,tree_item.to_specific_type.cfor.expr)))
elif tree_item.op == ida_hexrays.cit_do:
op = tree_item.to_specific_type.cdo.expr.op
if op >= ida_hexrays.cot_land and op <= ida_hexrays.cot_ult or op in [ida_hexrays.cot_var, ida_hexrays.cot_lnot]:
# Not an infinte loop
counter, check_val = self.parse_condition(tree_item.to_specific_type.cdo.expr)
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,check_val))
else:
counter, check_val = self.get_loop_check_val_break(tree_item.to_specific_type.cdo.body)
loop_check_list["Loop Check"].append((tree_item.ea,"Loop Check","Loop Check",counter,check_val))
except Exception as e:
print(e)
return loop_check_list
def parse_condition(self,condition):
if condition.op == ida_hexrays.cot_lnot:
#(!x)
return condition.x, utils.create_dummy_hexrays_const(0)
if condition.op == ida_hexrays.cot_var or condition.op == ida_hexrays.cot_call or condition.op == ida_hexrays.cot_cast:
#(x)
return condition.x, utils.create_dummy_hexrays_const(1)
if condition.op >= ida_hexrays.cot_eq and condition.op <= ida_hexrays.cot_ult:
#(x > y)
return condition.x, condition.y
if condition.op == ida_hexrays.cot_lor or condition.op == ida_hexrays.cot_land:
x_cond = self.parse_condition(condition.x)
y_cond = self.parse_condition(condition.y)
if y_cond[1].n:
return y_cond
return x_cond
def get_loop_check_val(self,counter,comparison):
x,y = self.parse_condition(comparison)
if x == counter:
return y
# IDA places constant into the y operand for normal comparisons, for everything else we should return X to avoid marking that as a const
return x
def get_loop_check_val_break(self,loop_body):
bd = loop_body.cblock.begin()
while bd != loop_body.cblock.end():
if bd.cur.op == ida_hexrays.cit_if:
break_if = self.find_break(bd.cur.cif)
if break_if:
return self.parse_condition(break_if.expr)
bd.next()
return None, None
def find_break(self,if_insn):
# TODO switch-case
bt = if_insn.ithen.cblock.begin()
while bt != if_insn.ithen.cblock.end():
if bt.cur.op == ida_hexrays.cit_if:
found_breaks = self.find_break(bt.cur.cif)
if found_breaks:
return found_breaks
elif bt.cur.op == ida_hexrays.cit_break or bt.cur.op == ida_hexrays.cit_return:
return if_insn
bt.next()
if if_insn.ielse:
be = if_insn.ielse.cblock.begin()
while be != if_insn.ielse.cblock.end():
if be.cur.op == ida_hexrays.cit_if:
found_breaks = self.find_break(be.cur.cif)
if found_breaks:
return found_breaks
elif be.cur.op == ida_hexrays.cit_break or bt.cur.op == ida_hexrays.cit_return:
return if_insn
be.next()
return None
def get_array_accesses(self):
array_access_list = {"Array Access":[]}
if self.hexrays:
for func in self.functions_list:
try:
decompiled_function = ida_hexrays.decompile(func)
code = decompiled_function.pseudocode
for citem in decompiled_function.treeitems:
if citem.op == ida_hexrays.cot_idx:
# Uses a trick by adding the index expression as a hidden 4th member of tuple
array_access_list["Array Access"].append((citem.ea,"Array Access","Array Access",citem.to_specific_type.x,citem.to_specific_type.y))
except Exception as e:
pass
return array_access_list
def prepare_functions_list(self):
self.functions_list = []
# Gather all functions in all segments
for segment in idautils.Segments():
self.functions_list.extend(list(idautils.Functions(idc.get_segm_start(segment),idc.get_segm_end(segment))))
# Gather imports
def imports_callback(ea, name, ordinal):
self.functions_list.append(ea)
return True
# For each import
number_of_imports = idaapi.get_import_module_qty()
for i in range(0, number_of_imports):
idaapi.enum_import_names(i, imports_callback)
# Returns a list of all xrefs to the function with specified name
def find_xrefs_by_name(self,func_names,wrappers):
xrefs_list = {}
insn = ida_ua.insn_t()
function_names = []
for name in func_names:
function_names.extend(utils.prep_func_name(name))
# Convert function names to expected format (demangle them if possible)
function_names = list(map(utils.get_pretty_func_name, function_names))
# For all addresses of functions and imports check if it is function we are looking for
for function_address in self.functions_list:
current_function_name = utils.get_func_name(function_address)
if not current_function_name:
current_function_name = ida_name.get_ea_name(function_address)
# If the type is not defined and the name of the function is known set the type
prototype = self.prototypes.get(current_function_name.lower(), None)
if prototype is not None:
idc.SetType(function_address, prototype)
idaapi.auto_wait()
# Seach function "as is" and "ingnore case"
if current_function_name not in function_names:
current_function_name = current_function_name.lower()
if current_function_name not in function_names:
continue
# This is the function we are looking for
if current_function_name not in xrefs_list.keys():
xrefs_list[current_function_name] = []
for xref in idautils.XrefsTo(function_address):
# This rules out any functions within those that we are already looking at (wrapped)
# It also makes sure that functions that are not xrefed within another function are not displayed
xref_func_name = utils.get_func_name(xref.frm)
if not xref_func_name or xref_func_name.lower() in function_names:
continue
# Make sure that the instrution is indeed a call
if ida_ua.decode_insn(insn,xref.frm) != idc.BADADDR:
if (insn.get_canon_feature() & 0x2 == 0x2 or # The instruction is a call
(insn.get_canon_feature() & 0xfff == 0x100 and insn.Op1.type in [idc.o_near,idc.o_far])): # The instruction uses first operand which is of type near/far immediate address
xref_tuple = (xref.frm,current_function_name,current_function_name)
if wrappers:
# If we should look for wrappers, do not includ the wrapped xref
retrieved_wrappers = self.get_wrapper_xrefs(xref.frm,current_function_name)
if not retrieved_wrappers:
# No wrappers
if not xref_tuple in xrefs_list[current_function_name]:
xrefs_list[current_function_name].append(xref_tuple)
else:
# Wrappers were found
wrapped_tupple = (xref_tuple[0],xref_tuple[1],f"{xref_tuple[2]} (wrapped:{len(retrieved_wrappers)})")
xrefs_list[current_function_name].append(wrapped_tupple) # this makes sure that the wrapped function is right before its wrappers
xrefs_list[current_function_name].extend(retrieved_wrappers)
else:
if not xref_tuple in xrefs_list[current_function_name]:
xrefs_list[current_function_name].append(xref_tuple)
return xrefs_list
# Check if the xref can be a wrapper
def get_wrapper_xrefs(self,xref,current_function_name):
if self.hexrays:
# Hexrays avaialble, we can use decompiler
return self.get_wrapper_xrefs_hexrays(xref,current_function_name)
else:
# Hexrays not available, decompiler cannot be used
return self.get_wrapper_xrefs_disass(xref)
def get_wrapper_xrefs_hexrays(self,function_xref,current_function_name):
wrapper_xrefs = []
try:
decompiled_function = ida_hexrays.decompile(function_xref)
except:
return wrapper_xrefs
if decompiled_function:
# Decompilation is fine
code = decompiled_function.pseudocode
for tree_item in decompiled_function.treeitems:
if tree_item.ea == function_xref and tree_item.op == ida_hexrays.cot_call:
# Get called function name
xref_func_name = utils.get_func_name(tree_item.to_specific_type.x.obj_ea).lower()
if not xref_func_name:
xref_func_name = idc.get_name(tree_item.to_specific_type.x.obj_ea).lower()
if xref_func_name == current_function_name.lower():
# This is the correct call
lvars = list(decompiled_function.get_lvars()) # Get lvars
arg_objects = list(tree_item.to_specific_type.a)
found = True
while arg_objects:
current_obj = arg_objects.pop(0)
if current_obj is None:
continue
if current_obj.op == ida_hexrays.cot_var:
# if variable is not an arg_var we do not have a wrapper
if not lvars[current_obj.v.idx].is_arg_var:
found = False
else:
arg_objects.extend([current_obj.to_specific_type.x,current_obj.to_specific_type.y])
# The function is likely a wrapper, get XREFs to it
if found:
for wrapper_xref in idautils.XrefsTo(decompiled_function.entry_ea):
wrapper_xref_func_name = utils.get_func_name(decompiled_function.entry_ea)
wrapper_xrefs.append((wrapper_xref.frm,wrapper_xref_func_name,f"{wrapper_xref_func_name} ({current_function_name} wrapper)"))
return wrapper_xrefs
# There probably is no architecture-agnostic way on how to do this without decompiler
def get_wrapper_xrefs_disass(self,xref):
return []
# Wrapper for getting function params
def get_xref_parameters(self,function_xref,scanned_function):
if self.hexrays:
# Hexrays avaialble, we can use decompiler
return self.get_xref_parameters_hexrays(function_xref,scanned_function)
else:
# Hexrays not available, decompiler cannot be used
return self.get_xref_parameters_disass(function_xref)
# Returns list of ordered paramters from disassembly
def get_xref_parameters_disass(self,function_xref):
params_list = []
# Requires the type to be already assigned to functions
try:
for param_ea in idaapi.get_arg_addrs(function_xref):
if param_ea != idc.BADADDR:
param_insn = ida_ua.insn_t()
# decode the instruction linked to the parameter
if ida_ua.decode_insn(param_insn,param_ea) != idc.BADADDR:
if param_insn.get_canon_feature() & 0x00100 == 0x100:
# First operand - push
params_list.append(param_insn.Op1)
elif param_insn.get_canon_feature() & 0x00200 == 0x200:
# Second operands - mov/lea
params_list.append(param_insn.Op2)
elif param_insn.get_canon_feature() & 0x00400 == 0x400:
# Third operand
params_list.append(param_insn.Op3)
except:
return None
return params_list
# Returns an ordered list of workable object that represent each parameter of the function from decompiled code
def get_xref_parameters_hexrays(self,function_xref,scanned_function):
# Decompile function and find the call
try:
decompiled_function = ida_hexrays.decompile(function_xref)
except:
return None
if decompiled_function is None:
# Decompilation failed
return None
index = 0
code = decompiled_function.pseudocode
for tree_item in decompiled_function.treeitems:
if tree_item.ea == function_xref and tree_item.op == ida_hexrays.cot_call:
xref_func_name = utils.get_func_name(tree_item.to_specific_type.x.obj_ea).lower()
if not xref_func_name:
xref_func_name = idc.get_name(tree_item.to_specific_type.x.obj_ea).lower()
if xref_func_name == scanned_function.lower():
return list(tree_item.to_specific_type.a)
index += 1
# Call not found :(
return None
class Param:
def __init__(self,scanner,param,call_xref,scanned_function):
self.scanner_instance = scanner
self.param = param
self.call_xref = call_xref
self.scanned_function = scanned_function
def used_as_index(self):
if self.scanner_instance.hexrays:
return self.used_as_index_hexrays()
else:
return False
def used_as_index_hexrays(self):
decompiled_function = ida_hexrays.decompile(self.call_xref)
if decompiled_function:
code = decompiled_function.pseudocode
for citem in decompiled_function.treeitems:
if citem.op == ida_hexrays.cot_idx:
if self.param is not None:
if citem.to_specific_type.y == self.param:
return True
return False
def is_constant(self):
if self.string_value() == "" and self.number_value() == None:
if self.param and self.param.op == ida_hexrays.cot_ref:
return False
asgs = self.__get_var_assignments()
if asgs: # asgs will be empty with no hexrays
for asg in asgs:
if self.__is_before_call(asg.ea):
if self.string_value(asg.y) == "" and self.number_value(asg.y) == None:
# One of the assigns is non-const
return False
return True
return False
else:
return True
# Returns True if the param is used in any function call specified in the "function_list" parameter
def used_in_call_before(self,function_list):
if self.scanner_instance.hexrays:
return self.used_in_call_before_hexrays(function_list)
else:
return self.used_in_call_before_disass(function_list)
def used_in_call_before_hexrays(self,function_list):
# get all calls with the parameter
calls = self.__get_var_arg_calls()
# prep function list
tmp_fun_list = []
for fun in function_list:
tmp_fun_list.extend(utils.prep_func_name(fun))
for call in calls:
if utils.get_func_name(call.x.obj_ea) in tmp_fun_list and self.__is_before_call(call.ea):
return True
return False
def used_in_call_before_disass(self,function_list):
return False
def used_in_call_after(self,function_list):
if self.scanner_instance.hexrays:
return self.used_in_call_after_hexrays(function_list)
else:
return self.used_in_call_after_disass(function_list)
def used_in_call_after_hexrays(self,function_list):
# get all calls with the parameter
calls = self.__get_var_arg_calls()
# prep function list
tmp_fun_list = []
for fun in function_list:
tmp_fun_list.extend(utils.prep_func_name(fun))
for call in calls:
if utils.get_func_name(call.x.obj_ea) in tmp_fun_list and not self.__is_before_call(call.ea):
return True
return False
def is_sign_compared(self):
if self.scanner_instance.hexrays:
return self.is_sign_compared_hexrays()
else:
return False
def is_sign_compared_hexrays(self):
decompiled_function = ida_hexrays.decompile(self.call_xref)
if decompiled_function:
code = decompiled_function.pseudocode
for citem in decompiled_function.treeitems:
if citem.ea == self.call_xref:
parent = decompiled_function.body.find_parent_of(citem)
while parent:
if parent.op == ida_hexrays.cit_if:
comps = self.__get_signed_comparisons(parent)
if self.__is_var_used_in_comparison(comps):
return True
parent = decompiled_function.body.find_parent_of(parent)
return False
def __is_var_used_in_comparison(self,comp_list):
if self.param.op == ida_hexrays.cot_cast:
param = self.param.x
else:
param = self.param
ops = comp_list.copy()
while ops:
op = ops.pop()
if not op:
continue
if param == op:
return True
ops.extend([op.x,op.y])
return False
def __get_signed_comparisons(self,citem):
signed_comparisons = []
signed_ops = [ida_hexrays.cot_sge, ida_hexrays.cot_sle, ida_hexrays.cot_sgt, ida_hexrays.cot_slt]
exprs = [citem.to_specific_type.cif.expr]
while exprs:
expr = exprs.pop()
if not expr:
continue
if expr.op in signed_ops:
signed_comparisons.append(expr)
else:
exprs.extend([expr.x,expr.y])
return signed_comparisons
def used_in_call_after_disass(self,function_list):
return False
# Simple check whether the given EA is before the call
def __is_before_call(self,ea):
func = idaapi.get_func(ea)
flow = idaapi.FlowChart(func)
call_block = None
asg_block = None
checked_blocks = []
# Get block of the assignemnt and block of the call
for block in flow:
if ea >= block.start_ea and ea <= block.end_ea:
asg_block = block
if self.call_xref >= block.start_ea and self.call_xref <= block.end_ea:
call_block = block
checked_blocks.append(block.start_ea)
# If they are in the same block and asg ea is smaller then call_xref ea return True
if call_block == asg_block:
if ea < self.call_xref:
return True
else:
# Blocks are different
call_preds = list(call_block.preds())
while call_preds:
current_pred = call_preds.pop(0)
# Prevent endless loops
if current_pred.start_ea in checked_blocks:
continue
checked_blocks.append(current_pred.start_ea)
# Check if we matched the given assign block
if current_pred.start_ea == asg_block.start_ea:
return True
call_preds.extend(list(current_pred.preds()))
return False
def __get_var_arg_calls(self):
calls = []
# Parameter is cast, get x
if self.param.op == ida_hexrays.cot_cast:
param = self.param.x
else:
param = self.param
decompiled_function = ida_hexrays.decompile(self.call_xref)
code = decompiled_function.pseudocode
for citem in decompiled_function.treeitems:
if citem.op == ida_hexrays.cot_call and citem.ea != self.call_xref: # skip calls we are tracing
# Potentially interesting call
for a in citem.to_specific_type.a:
expressions = [a,a.x,a.y,a.z]
while expressions:
current_expr = expressions.pop(0)
if current_expr:
if param == current_expr:
# Call operation, add to array
calls.append(citem.to_specific_type)
break # we can break the loop as the variable was found within the arguments
expressions.extend([current_expr.x, current_expr.y, current_expr.z])
return calls
# Returns list of assign expressions for better accuracy
def __get_var_assignments(self):
asg = []
# Parameter is cast, get x
if self.param.op == ida_hexrays.cot_cast:
param = self.param.x
else:
param = self.param
decompiled_function = ida_hexrays.decompile(self.call_xref)
code = decompiled_function.pseudocode
for citem in decompiled_function.treeitems:
if param == citem.to_specific_type:
parent = decompiled_function.body.find_parent_of(citem)
if parent.op >= ida_hexrays.cot_asg and parent.op <= ida_hexrays.cot_asgumod:
# Assign operation, add to array
asg.append(parent.to_specific_type)
return asg
def string_value(self,expr=None):
if not expr:
expr = self.param
if not expr:
return None
if self.scanner_instance.hexrays: # hexrays
string_val = idc.get_strlit_contents(expr.obj_ea)
if string_val:
return string_val.decode()
# If it is a cast (could happen)
elif expr.op == ida_hexrays.cot_cast:
# If casted op is object
if expr.x.op == ida_hexrays.cot_obj:
# If that object points to a string
string_val = idc.get_strlit_contents(expr.x.obj_ea)
if string_val:
return string_val.decode()
elif expr.op == ida_hexrays.cot_ref:
string_val = idc.get_strlit_contents(expr.x.obj_ea)
if string_val:
return string_val.decode()
if expr.x.op == ida_hexrays.cot_idx:
string_val = idc.get_strlit_contents(expr.x.x.obj_ea)
if string_val:
return string_val.decode()
# Check whether we are looking at CFString
if expr.x.obj_ea != idc.BADADDR:
c_str_pointer_value = idc.get_bytes(expr.x.obj_ea + 2* self.scanner_instance.ptr_size, self.scanner_instance.ptr_size)
tmp_ea = int.from_bytes(c_str_pointer_value,byteorder=self.scanner_instance.endian)
c_str_len = int.from_bytes(idc.get_bytes(expr.x.obj_ea + 3* self.scanner_instance.ptr_size, self.scanner_instance.ptr_size),byteorder=self.scanner_instance.endian)
c_string_value = idc.get_strlit_contents(tmp_ea)
# Having a string at this position and its length following the pointer suggests CFString struct
if c_string_value and len(c_string_value) == c_str_len:
# If this evaluates to string we have a constant
return c_string_value.decode()
else:
# not a direct string
byte_value = idc.get_bytes(expr.obj_ea,self.scanner_instance.ptr_size)
if byte_value:
tmp_ea = int.from_bytes(byte_value,byteorder=self.scanner_instance.endian)
string_val = idc.get_strlit_contents(tmp_ea)
if string_val:
# If this evaluates to string we have a constant
return string_val.decode()
else: # No hexrays
if expr.type == 0x2:
# Reference
if idc.get_strlit_contents(expr.addr):
return idc.get_strlit_contents(expr.addr).decode()
else:
# Reference to reference
addr = int.from_bytes(idc.get_bytes(expr.addr, self.scanner_instance.ptr_size),byteorder=self.scanner_instance.endian)
if idc.get_strlit_contents(addr):
return idc.get_strlit_contents(addr).decode()
if expr.type == 0x5:
if idc.get_strlit_contents(expr.value):
return idc.get_strlit_contents(expr.value).decode()
# Reverse appoach of going from strings to calls
for c_string in self.scanner_instance.strings_list:
for str_xref in idautils.XrefsTo(c_string.ea):
func = idaapi.get_func(str_xref.frm)
if func:
if func.start_ea == idaapi.get_func(self.call_xref).start_ea:
# The xref and the call are in the same function
# look if those are used within 5 instructions
if (str_xref.frm < self.call_xref):
# XREF to STR is before the call to XREF
instr_list = list(idautils.Heads(str_xref.frm,self.call_xref))
if len(instr_list) <= 10:
# No more then 10 instructions between the str XREF and the call
for head in instr_list:
current_insn = ida_ua.insn_t()
if ida_ua.decode_insn(current_insn,head) != idc.BADADDR:
if head != self.call_xref and current_insn.get_canon_feature() & 0x2 == 0x2:
# If it is not a target call but it is a call its false
return ""
# If we survived the loop it is True
return str(c_string)
return ""
def number_value(self,expr=None):
if not expr:
expr = self.param
if not expr:
return None
if self.scanner_instance.hexrays: # hexrays
# If it is number directly
if expr.op == ida_hexrays.cot_num:
return expr.n._value
# if it is a float
elif expr.op == ida_hexrays.cot_fnum:
return expr.fpc.fnum.float
# If it is a cast
elif expr.op == ida_hexrays.cot_cast:
if expr.x.op == ida_hexrays.cot_num:
return expr.x.n._value
elif expr.x.op == ida_hexrays.cot_fnum:
return expr.x.fpc.fnum.float
else: # No hexrays
# self.param is op_t
# 0x5 is immediate
if expr.type == 0x5:
return expr.value
return None
def is_const_number(self,expr=None):
if not expr:
expr = self.param
if self.scanner_instance.hexrays:
if expr.op == ida_hexrays.cot_num or expr.op == ida_hexrays.cot_fnum:
return True
elif expr.op == ida_hexrays.cot_cast and expr.x.op == ida_hexrays.cot_num:
return True
else:
if expr.type == 0x5:
return True
return False
# Wrapper for the basic UAF filter
def set_to_null_after_call(self):
if self.scanner_instance.hexrays:
# Hexrays avaialble, we can use decompiler
return self.set_to_null_after_call_hexrays()
else:
# Hexrays not available, decompiler cannot be used
return self.set_to_null_after_call_disass()
# Simple function that just checks whether the call is followed by an isntruction that sets the parameter to null
# Note that this is very primitive to keep sort of architecture agnostic approach
def set_to_null_after_call_disass(self):
call_insn = ida_ua.insn_t()
following_insn = ida_ua.insn_t()
# First get the call instruction
if ida_ua.decode_insn(call_insn,self.call_xref) != idc.BADADDR:
# Now use the call instruction to get EA of next insn
if ida_ua.decode_insn(following_insn,call_insn.ea + call_insn.size) != idc.BADADDR:
# following_insn can be used to evaluate whether Op2 is a constant or not
if following_insn.Op2.type == 0x5 and following_insn.Op2.value == 0x0:
# This should cover most of the cases where a mov instuction is used
return True
return False
# Using hexrays to figure out whether the function was followed by a null-set
def set_to_null_after_call_hexrays(self):
# get size of the call instruction, go past it and get expression that is linked to that address?
matched = {"set_to_null":False}
try:
decompiled_function = ida_hexrays.decompile(self.call_xref)
except:
return matched["set_to_null"]
if decompiled_function is None:
# Decompilation failed
return None
custom_visitor = null_after_visitor(decompiled_function,self.call_xref,self.scanned_function,matched)
custom_visitor.apply_to(decompiled_function.body, None)
return matched["set_to_null"]
class FunctionCall:
def __init__(self,scanner,call_xref,scanned_function):
self.scanner_instance = scanner
self.call_xref = call_xref
self.scanned_function = scanned_function
def reachable_from(self,function_name):
functions = [idaapi.get_func(self.call_xref)]
checked_xrefs = []
while functions:
current_function = functions.pop(0)
if current_function:
if utils.get_func_name(current_function.start_ea) in utils.prep_func_name(function_name):
return True
for xref in idautils.XrefsTo(current_function.start_ea):
if xref.frm not in checked_xrefs:
functions.append(idaapi.get_func(xref.frm))
checked_xrefs.append(xref.frm)
return False
# Check whether the return value of a function is part of some comparison (verification)
def return_value_checked(self,check_val = None):
if self.scanner_instance.hexrays:
# Hexrays avaialble, we can use decompiler
return self.return_value_checked_hexrays(check_val)
else:
# Hexrays not available, decompiler cannot be used
return self.return_value_checked_disass(check_val)
# Check if the return value of a function is verified after the call
def return_value_checked_disass(self,check_val):
# To stay architecture agnostic, this simply checks whether there is conditional jump within 5 instructions after the call
for basic_block in idaapi.FlowChart(idaapi.get_func(self.call_xref)):
if self.call_xref >= basic_block.start_ea and self.call_xref < basic_block.end_ea and len(list(basic_block.succs())) > 1:
# xref call belongs to this block
insn_counter = 0
current_insn = ida_ua.insn_t()
if ida_ua.decode_insn(current_insn,self.call_xref) != idc.BADADDR:
while insn_counter < 5:
following_insn = ida_ua.insn_t()
if ida_ua.decode_insn(following_insn,current_insn.ea + current_insn.size) != idc.BADADDR:
current_insn = following_insn
# Most of the compare instructions
if current_insn.get_canon_feature() & 0xffff == 0x300:
if check_val is not None:
for op in current_insn.ops:
if op.type == 0x5:
negative_value = utils.get_negative_disass(op)
if op.value == check_val or negative_value == check_val:
return True
else:
return False
else:
# Likely comparison instruction hit
return True
insn_counter += 1
else:
break
# Jumped out of the block within 5 instructions
if current_insn.ea >= basic_block.end_ea:
return True
return False
def return_value_checked_hexrays(self,check_val):
try:
decompiled_function = ida_hexrays.decompile(self.call_xref)
except:
return None
if decompiled_function is None:
# Decompilation failed
return None
code = decompiled_function.pseudocode
index = 0
for tree_item in decompiled_function.treeitems:
if tree_item.ea == self.call_xref and tree_item.op == ida_hexrays.cot_call:
xref_func_name = utils.get_func_name(tree_item.to_specific_type.x.obj_ea).lower()
if not xref_func_name:
xref_func_name = idc.get_name(tree_item.to_specific_type.x.obj_ea).lower()
if xref_func_name == self.scanned_function.lower():
parent = decompiled_function.body.find_parent_of(tree_item)
if parent.op == ida_hexrays.cot_cast: # return value is casted
parent = decompiled_function.body.find_parent_of(parent)
if (parent.op >= ida_hexrays.cot_eq and parent.op <= ida_hexrays.cot_ult):
if check_val is not None:
parent = parent.to_specific_type
if parent.y and (parent.y.n or parent.y.fpc): # Check if there is a Y operand and if it is a number, if it is, get its value
if parent.y.n: # int
value = parent.y.n._value
negative_value = -((value ^ 0xffffffffffffffff) + 1)
else: # float
value = parent.y.fpc.fnum.float
negative_value = value
if negative_value == check_val or value == check_val:
return True
else:
return False
else:
return True
elif parent.op == ida_hexrays.cit_if or parent.op == ida_hexrays.cot_lnot or parent.op == ida_hexrays.cot_lor or parent.op == ida_hexrays.cot_land:
if check_val is not None:
if not parent.to_specific_type.cif.expr.y:
# There is no Y, likely checked against 0: if(func_call())
if check_val == 0:
return True
else: