This repository has been archived by the owner on May 19, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 46
/
idasploiter_x86.py
1128 lines (867 loc) · 49 KB
/
idasploiter_x86.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
#!/usr/bin/env python
#
# IDA Sploiter is an exploit development and vulnerability research environment
# implemented as a plugin for Hex-Ray's IDA Pro disassembler.
# Copyright (C) 2014 Peter Kacherginsky
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Performance profiling
import cProfile
import pstats
# IDA Sploiter
from idasploiter import FuncPtr, Gadget, Ptr, Rop, read_module_memory, Sploiter
# IDA libraries
import idaapi
import idautils
import idc
from idaapi import Form, Choose2, plugin_t
# Python libraries
import os
import binascii
import string
import textwrap
import copy
import csv
import itertools
import struct
from struct import pack, unpack
from ctypes import *
###############################################################################
# Function Pointer Search Engine
class x86_FuncPtr(FuncPtr):
def __init__(self, sploiter):
FuncPtr.__init__(self, sploiter)
# NOTE: Pointer Offsets are interpreted as follows:
# Example 1: Consider we have loaded fptr with a user controlled value
#
# mov [fptr + offset], value
#
# The fptr value we need is (fptr - offset) to overwrite
# the actual fptr location. Do this early so that applied address
# filters consider fptr bytes before offset adjustment.
#
# Specify this positive offset in the Function Pointer form
# so that it would be applied to all discovered function pointers.
#
# Example 2: Consider we have a pointer to a pointer write condition:
#
# mov fptr, [p2p + offset2]
# mov [fptr + offset1], value
#
# The value (p2p - offset2) must point to the fptr location,
# but since we have another offset the target of (p2p - offset2)
# must be actually (fptr - offset1). Also do this early so that
# applied address filters consider p2p before offset adjustment.
#
# Specify this positive p2p offset2 so it gets applied to the listing
# of pointers-to-pointers. All pointer-to-pointer(s) will also
# be calculated relative to the ptr - offset1
#
# NOTE: Only search for pointers to pointers within the same module. This
# was done purely for performance considerations since any readable
# pointer to pointer may be used.
def search_pointers(self):
# HACK: A separate flag is used to track user canceling the search,
# because multiple calls to idaapi.wasBreak() do not properly
# detect cancellations.
breakFlag = False
# Show wait dialog
idaapi.show_wait_box("Searching writable function pointers...")
for m in self.modules:
###################################################################
# Locate all of the CALL and JMP instructions in the current module
# which use an immediate operand.
# List of call/jmp pointer calls in a given module
ptr_calls = list()
# Iterate over segments in the module
# BUG: Iterating over all loaded segments is more stable than looking up by address
for n in xrange(idaapi.get_segm_qty()):
seg = idaapi.getnseg(n)
# Segment in a selected modules
if seg and seg.startEA >= m.addr and seg.endEA <= (m.addr + m.size):
# Locate executable segments
# NOTE: Each module may have multiple executable segments
# TODO: Search for "MOV REG, PTR # CALL REG"
if seg.perm & idaapi.SEGPERM_EXEC:
# Search all instances of CALL /2 imm32/64 - FF 15
# TODO: Alternative pointer calls using SIB: FF 14 E5 11 22 33 44 - call dword/qword ptr [0x44332211]
# FF 14 65 11 22 33 44
# FF 14 25 11 22 33 44
call_ea = seg.startEA
while True:
call_ea = idaapi.find_binary(call_ea + 1, seg.endEA, "FF 15", 16, idaapi.SEARCH_DOWN)
if call_ea == idaapi.BADADDR: break
ptr_calls.append(call_ea)
# Search all instances of JMP /2 imm32/64 - FF 25
# TODO: Alternative pointer calls using SIB: FF 24 E5 11 22 33 44 - jmp dword/qword ptr [0x44332211]
# FF 24 65 11 22 33 44
# FF 24 25 11 22 33 44
call_ea = seg.startEA
while True:
call_ea = idaapi.find_binary(call_ea + 1, seg.endEA, "FF 25", 16, idaapi.SEARCH_DOWN)
if call_ea == idaapi.BADADDR: break
ptr_calls.append(call_ea)
###################################################################
# Extract all of the function pointers and make sure they are
# are writable.
# List of writable function pointer objects in a given module
ptrs = list()
for call_ea in ptr_calls:
# Decode CALL/JMP instruction
# NOTE: May result in invalid disassembly of split instructions
insn_size = idaapi.decode_insn(call_ea)
if insn_size:
insn = idaapi.cmd
insn_op1 = insn.Operands[0].type
# Verify first operand is a direct memory reference
if insn.Operands[0].type == idaapi.o_mem:
# Get operand address
ptr_ea = insn.Operands[0].addr
# Apply pointer offset
ptr_ea -= self.ptrOffset
# Locate segment where the pointer is located
ptr_seg = idaapi.getseg(ptr_ea)
# Make sure a valid segment writeable segment was found
if ptr_seg and ptr_seg.perm & idaapi.SEGPERM_WRITE:
# Get pointer charset
ptr_charset = self.sploiter.get_ptr_charset(ptr_ea)
# Filter the pointer
if not self.filterP2P:
if ptr_charset == None: continue
if self.ptrNonull and not "nonull" in ptr_charset: continue
if self.ptrUnicode and not "unicode" in ptr_charset: continue
if self.ptrAscii and not "ascii" in ptr_charset: continue
if self.ptrAsciiPrint and not "asciiprint" in ptr_charset: continue
if self.ptrAlphaNum and not "alphanum" in ptr_charset: continue
if self.ptrNum and not "numeric" in ptr_charset: continue
if self.ptrAlpha and not "alpha" in ptr_charset: continue
# Increment the fptr counter
# Get pointer disassembly
insn_disas = idc.GetDisasmEx(call_ea, idaapi.GENDSM_FORCE_CODE)
# Add pointer to the list
ptr = Ptr(m.file, ptr_ea, self.ptrOffset, ptr_charset, call_ea, insn_disas)
ptrs.append(ptr)
###################################################################
# Cache Pointers to Pointers
ptr_ea_prefix_cache = dict()
if self.searchP2P:
# CACHE: Running repeated searches over the entire memory space is
# very expensive. Let's cache all of the addresses containing
# bytes corresponding to discovered function pointers in a
# single search and simply reference this cache for each
# function pointer. Specifically running idaapi.find_binary()
# is much more expensive than idaapi.dbg_read_memory().
#
# NOTE: For performance considerations, the cache works on a per
# module basis, but could be expanded for the entire memory
# space.
#
# prefix_offset - how many bytes of discovered function
# pointers to cache.
#
# Example: For function pointers 0x00401234, 0x00404321, 0x000405678
# we are going to use prefix_offset 2, so we will cache all of the
# values located at addresses 0x0040XXXX
if self.sploiter.addr64:
pack_format = "<Q"
addr_bytes = 8
prefix_offset = 6
else:
pack_format = "<I"
addr_bytes = 4
prefix_offset = 2
# Set of unique N-byte address prefixes to search in memory
ea_prefix_set = set()
for ptr in ptrs:
ptr_ea = ptr.ptr_ea
ptr_bytes = struct.pack(pack_format, ptr_ea)
ptr_bytes = ptr_bytes[-prefix_offset:]
ea_prefix_set.add(ptr_bytes)
# Search the module for all bytes corresponding to the prefix
# and use them as candidates for pointers-to-pointers
for ea_prefix in ea_prefix_set:
# NOTE: Make sure you search using 44 33 22 11 format and not 11223344
ea_prefix_str = " ".join(["%02x" % ord(b) for b in ea_prefix])
# Initialize search parameters for a given module
ea = m.addr
maxea = m.addr + m.size
while True:
ea = idaapi.find_binary(ea + 1, maxea, ea_prefix_str, 16, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR: break
p2p_ea = ea - (addr_bytes - prefix_offset)
dbg_mem = read_module_memory(p2p_ea, addr_bytes)
ptr_ea_prefix = unpack(pack_format, dbg_mem)[0]
if ptr_ea_prefix in ptr_ea_prefix_cache:
ptr_ea_prefix_cache[ptr_ea_prefix].add(p2p_ea)
else:
ptr_ea_prefix_cache[ptr_ea_prefix] = set([p2p_ea, ])
# Detect search cancellation, but allow the loop below
# to run to create already cached/found function pointers
# Canceled
if breakFlag or idaapi.wasBreak():
breakFlag = True
break
# Canceled
if breakFlag or idaapi.wasBreak():
breakFlag = True
break
###################################################################
# Locate Pointer to Pointers
for ptr in ptrs:
ptr_ea = ptr.ptr_ea
# Locate pointers-to-pointers for a given function pointer in the cache
if self.searchP2P and ptr_ea in ptr_ea_prefix_cache:
for p2p_ea in ptr_ea_prefix_cache[ptr_ea]:
# Apply pointer-to-pointer offset
p2p_ea -= self.p2pOffset
p2p_charset = self.sploiter.get_ptr_charset(p2p_ea)
# Filter the pointer
if self.filterP2P:
if p2p_charset == None: continue
if self.ptrNonull and not "nonull" in p2p_charset: continue
if self.ptrUnicode and not "unicode" in p2p_charset: continue
if self.ptrAscii and not "ascii" in p2p_charset: continue
if self.ptrAsciiPrint and not "asciiprint" in p2p_charset: continue
if self.ptrAlphaNum and not "alphanum" in p2p_charset: continue
if self.ptrNum and not "numeric" in p2p_charset: continue
if self.ptrAlpha and not "alpha" in p2p_charset: continue
# Copy existing pointer object to modify it for the particular p
p2p = copy.copy(ptr)
p2p.p2p_ea = p2p_ea
p2p.p2p_offset = self.p2pOffset
p2p.p2p_charset = p2p_charset
# Apppend p2p specific pointer object to the global list
self.ptrs.append(p2p)
# Exceeded maximum number of pointers
if self.maxPtrs and len(self.ptrs) >= self.maxPtrs:
breakFlag = True
print "[idasploiter] Maximum number of pointers exceeded."
break
# Simply append pointer object to the global list
else:
self.ptrs.append(ptr)
# Exceeded maximum number of pointers
if self.maxPtrs and len(self.ptrs) >= self.maxPtrs:
breakFlag = True
print "[idasploiter] Maximum number of pointers exceeded."
break
if breakFlag or idaapi.wasBreak():
breakFlag = True
break
# Canceled
# NOTE: Only works when started from GUI not script.
if breakFlag or idaapi.wasBreak():
breakFlag = True
print "[idasploiter] Canceled."
break
print "[idasploiter] Found %d total pointers." % len(self.ptrs)
idaapi.hide_wait_box()
###############################################################################
# ROP Search Engine
class x86_Rop(Rop):
def __init__(self, sploiter):
Rop.__init__(self, sploiter)
# Extra bytes to read to ensure correct decoding of
# RETN, RETN imm16, CALL /2, and JMP /4 instructions.
self.dbg_read_extra = 6 # FF + ModR/M + SIB + disp32
self.insn_arithmetic_ops = ["inc", "dec", "neg", "add", "sub", "mul", "imul", "div", "idiv", "adc", "sbb",
"lea"]
self.insn_bit_ops = ["not", "and", "or", "xor", "shr", "shl", "sar", "sal", "shld", "shrd", "ror", "rcr", "rcl"]
def get_o_reg_name(self, insn, n):
reg_num = insn.Operands[n].reg
reg_name = self.regnames[reg_num]
# NOTE: IDA's x86/x86-64 regname array contains only register root names
# (e.g ax,cx,dx,etc.). However we can still figure out exact register
# size by looking at the operand 'dtyp' property.
if reg_num < 8:
# 32-bit register
if insn.Operands[n].dtyp == idaapi.dt_dword:
reg_name = 'e' + reg_name
# 64-bit register
elif insn.Operands[n].dtyp == idaapi.dt_qword:
reg_name = 'r' + reg_name
# 16-bit register otherwise
return reg_name
def search_retns(self):
if not self.debug: print("found %d modules" % len(self.modules))
for m in self.modules:
# Iterate over segments in the module
# BUG: Iterating over all loaded segments is more stable than looking up by address
if not self.debug: print("found %d segments" % idaapi.get_segm_qty())
for n in xrange(idaapi.get_segm_qty()):
seg = idaapi.getnseg(n)
# Locate executable segments in a selected modules
# NOTE: Each module may have multiple executable segments
if seg and seg.startEA >= m.addr and seg.endEA <= (m.addr + m.size):
# If the debugger is attached then we can check if the segment is executable, else
# just check if it is code or not.
if idaapi.dbg_can_query() and idaapi.get_process_state() < 0:
if seg.perm & idaapi.SEGPERM_EXEC == 0:
continue
elif seg.type & idaapi.SEG_CODE == 0:
continue
#######################################################
# Search for ROP gadgets
if self.searchRop:
# Search all instances of RETN
ea = seg.startEA
while True:
ea = idaapi.find_binary(ea + 1, seg.endEA, "C3", 16, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR: break
self.retns.append((ea, m.file))
# Search all instances of RETN imm16
ea = seg.startEA
while True:
ea = idaapi.find_binary(ea + 1, seg.endEA, "C2", 16, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR: break
# Read imm16 value and filter large values
retn_imm16 = read_module_memory(ea + 1, 0x2)
retn_imm16 = unpack("<H", retn_imm16)[0]
if retn_imm16 <= self.maxRetnImm:
self.retns.append((ea, m.file))
#######################################################
# Search for JOP gadgets
if self.searchJop:
# Search all instances of JMP reg (FF /4) and CALL reg (FF /2)
ea = seg.startEA
while True:
ea = idaapi.find_binary(ea + 1, seg.endEA, "FF", 16, idaapi.SEARCH_DOWN)
if ea == idaapi.BADADDR: break
# Read possible ModR/M, SIB, and IMM8/IMM32 bytes
jop = read_module_memory(ea + 1, 0x6)
if jop == None or len(jop) == 0:
continue
###################################################
# JMP/CALL reg
if jop[0] in ["\xe0", "\xe1", "\xe2", "\xe3", "\xe4", "\xe5", "\xe6", "\xe7",
"\xd0", "\xd1", "\xd2", "\xd3", "\xd4", "\xd5", "\xd6", "\xd7"]:
self.retns.append((ea, m.file))
###################################################
# JMP/CALL [reg] no SIB
# NOTE: Do not include pure [disp] instruction.
# JMP/CALL [reg] no *SP,*BP
elif jop[0] in ["\x20", "\x21", "\x22", "\x23", "\x26", "\x27",
"\x10", "\x11", "\x12", "\x13", "\x16", "\x17"]:
self.retns.append((ea, m.file))
# JMP/CALL [reg + imm8] no *SP
elif jop[0] in ["\x60", "\x61", "\x62", "\x63", "\x65", "\x66", "\x67",
"\x50", "\x51", "\x52", "\x53", "\x55", "\x56", "\x57"]:
jop_imm8 = jop[1]
jop_imm8 = unpack("b", jop_imm8)[0] # signed
if jop_imm8 <= self.maxJopImm:
self.retns.append((ea, m.file))
# JMP/CALL [reg + imm32] no *SP
elif jop[0] in ["\xa0", "\xa1", "\xa2", "\xa3", "\xa5", "\xa6", "\xa7",
"\x90", "\x91", "\x92", "\x93", "\x95", "\x96", "\x97"]:
jop_imm32 = jop[1:5]
jop_imm32 = unpack("<i", jop_imm32)[0] # signed
if jop_imm32 <= self.maxJopImm:
self.retns.append((ea, m.file))
###################################################
# JMP/CALL [reg] with SIB
# NOTE: Do no include pure [disp] instructions in SIB ([*] - none)
elif (jop[0] in ["\x24", "\x64", "\xa4"] and not jop[1] in ["\x25", "\x65", "\xad",
"\xe5"]) or \
(jop[0] in ["\x14", "\x54", "\x94"] and not jop[1] in ["\x25", "\x65", "\xad",
"\xe5"]):
# Check for displacement
if jop[0] in ["\x64", "\x54"]:
jop_imm8 = jop[2]
jop_imm8 = unpack("b", jop_imm8)[0] # signed
if jop_imm8 <= self.maxJopImm:
self.retns.append((ea, m.file))
elif jop[0] in ["\xa4", "\x94"]:
jop_imm32 = jop[2:6]
jop_imm32 = unpack("<i", jop_imm32)[0] # signed
if jop_imm32 <= self.maxJopImm:
self.retns.append((ea, m.file))
else:
self.retns.append((ea, m.file))
print "[idasploiter] Found %d returns" % len(self.retns)
def search_gadgets(self):
count_total = len(self.retns)
count_notify = 0
count_curr = 0
# BUG: A separate flag is used to track user canceling the search,
# because multiple calls to idaapi.wasBreak() do not properly
# detect cancellations.
breakFlag = False
# Show wait dialog
if not self.debug: idaapi.show_wait_box("Searching gadgets: 00%%%%")
for (ea_end, module) in self.retns:
# Flush the gadgets cache for each new retn pointer
self.gadgets_cache = dict()
# Flush memory cache for each new retn pointer
self.dbg_mem_cache = None
# CACHE: It is faster to read as much memory in one blob than to make incremental reads backwards.
# Try to read and cache self.maxRopOffset bytes back. In cases where it is not possible,
# then simply try to read the largest chunk.
# NOTE: Read a bit extra to cover correct decoding of RETN, RETN imm16, CALL /2, and JMP /4 instructions.
for i in range(self.maxRopOffset):
self.dbg_mem_cache = read_module_memory(ea_end - self.maxRopOffset + i,
self.maxRopOffset - i + self.dbg_read_extra)
if self.dbg_mem_cache != None:
break
# Check to make sure we have actual data to work with.
if self.dbg_mem_cache == None:
continue
# Search all possible gadgets up to maxoffset bytes back
# NOTE: Try all byte combinations to capture longer/more instructions
# even with bad bytes in the middle.
for i in range(1, len(self.dbg_mem_cache) - self.dbg_read_extra):
ea = ea_end - i
# Get pointer charset
ptr_charset = self.sploiter.get_ptr_charset(ea)
# Filter the pointer
if ptr_charset == None: continue
if self.ptrNonull and not "nonull" in ptr_charset: continue
if self.ptrUnicode and not "unicode" in ptr_charset: continue
if self.ptrAscii and not "ascii" in ptr_charset: continue
if self.ptrAsciiPrint and not "asciiprint" in ptr_charset: continue
if self.ptrAlphaNum and not "alphanum" in ptr_charset: continue
if self.ptrNum and not "numeric" in ptr_charset: continue
if self.ptrAlpha and not "alpha" in ptr_charset: continue
# Try to build a gadget at the pointer
gadget = self.build_gadget(ea, ea_end)
# Successfully built the gadget
if gadget:
# Populate gadget object with more data
gadget.address = ea
gadget.module = module
gadget.ptr_charset = ptr_charset
# Filter gadgets with too many instruction
if gadget.size > self.maxRopSize:
break
# Append newly built gadget
self.gadgets.append(gadget)
self.gadgets_cache[ea] = gadget
# Exceeded maximum number of gadgets
if self.maxRops and len(self.gadgets) >= self.maxRops:
breakFlag = True
print "[idasploiter] Maximum number of gadgets exceeded."
break
else:
self.gadgets_cache[ea] = None
if breakFlag or idaapi.wasBreak():
breakFlag = True
break
# Canceled
# NOTE: Only works when started from GUI not script.
if breakFlag or idaapi.wasBreak():
breakFlag = True
print "[idasploiter] Canceled."
break
# Progress report
if not self.debug and count_curr >= count_notify:
# NOTE: Need to use %%%% to escape both Python and IDA's format strings
idaapi.replace_wait_box("Searching gadgets: %02d%%%%" % (count_curr * 100 / count_total))
count_notify += 0.10 * count_total
count_curr += 1
print "[idasploiter] Found %d gadgets." % len(self.gadgets)
if not self.debug: idaapi.hide_wait_box()
# Attempt to build a gadget at the provided start address
# by verifying it properly terminates at the expected RETN.
def build_gadget(self, ea, ea_end):
instructions = list()
chg_registers = set()
use_registers = set()
operations = set()
pivot = 0
# Process each instruction in the gadget
while ea <= ea_end:
###################################################################
# Gadget Level Cache:
#
# Locate a gadget (failed or built) starting at this address.
# If one is located, then we don't need to process any further
# instructions and just get necessary data from the cached
# gadget to never have to process the same address twice.
if ea in self.gadgets_cache:
# Check if the gadget was build successfully
gadget_cache = self.gadgets_cache[ea]
# Build the reset of the gadget from cache
if gadget_cache:
for insn in gadget_cache.instructions:
instructions.append(insn)
for reg in gadget_cache.chg_registers:
chg_registers.add(reg)
for reg in gadget_cache.use_registers:
use_registers.add(reg)
for op in gadget_cache.operations:
operations.add(op)
pivot += gadget_cache.pivot
gadget = Gadget(instructions, pivot, operations, chg_registers, use_registers)
return gadget
# Previous attempt to build gadget at this address failed
else:
return None
# Process new instruction
else:
# Instruction length
# NOTE: decode_insn also sets global idaapi.cmd
# which contains insn_t structure
insn_size = idaapi.decode_insn(ea)
# Check successful decoding of the instruction
if insn_size:
# Decoded instruction is too big to be a RETN or RETN imm16
if ea + insn_size > ea_end + self.dbg_read_extra:
return None
###############################################################
# Instruction Level Cache
#
# Most instructions are repetitive so we can just cache
# unique byte combinations to avoid costly decoding more
# than once
# Read instruction from memory cache
dbg_mem_offset = ea - (ea_end - (len(self.dbg_mem_cache) - self.dbg_read_extra))
dbg_mem = self.dbg_mem_cache[dbg_mem_offset:dbg_mem_offset + insn_size]
# Create instruction cache if it doesn't already exist
if not dbg_mem in self.insn_cache:
###########################################################
# Decode instruction
###########################################################
# Get global insn_t structure describing the instruction
# NOTE: copy() is expensive, so we keep this single-threaded
insn = idaapi.cmd
#######################################################
# Decode and Cache instruction characteristics
self.insn_cache[dbg_mem] = self.decode_instruction(insn, ea, ea_end)
##################################################################
# Retrieve cached instruction and apply it to the gadget
# Check that cached instruction contains valid data
if self.insn_cache[dbg_mem]:
# Retrieve basic instruction characteristics
insn_mnem = self.insn_cache[dbg_mem]["insn_mnem"]
insn_disas = self.insn_cache[dbg_mem]["insn_disas"]
instructions.append(insn_disas)
#######################################################
# Expected ending instruction of the gadget
if ea == ea_end:
gadget = Gadget(instructions, pivot, operations, chg_registers, use_registers)
return gadget
#######################################################
# Filter out of place ROP/JOP/COP terminators
# NOTE: retn/jmp/call are allowed, but only in the last position
# Unexpected return instruction
elif insn_mnem == "retn":
return None
# Unexpected call/jmp instruction
elif insn_mnem in ["jmp", "call"]:
return None
#######################################################
# Add instruction instruction characteristics to the gadget
else:
for reg in self.insn_cache[dbg_mem]["insn_chg_registers"]:
chg_registers.add(reg)
for reg in self.insn_cache[dbg_mem]["insn_use_registers"]:
use_registers.add(reg)
for op in self.insn_cache[dbg_mem]["insn_operations"]:
operations.add(op)
pivot += self.insn_cache[dbg_mem]["insn_pivot"]
# Previous attempt to decode the instruction invalidated the gadget
else:
return None
###############################################################
# Next instruction
# NOTE: This is outside cache
ea += insn_size
###################################################################
# Failed decoding of the instruction
# NOTE: Gadgets may have bad instructions in the middle which
# can be tolerated as long as we can find a useful instruction
# further out.
else:
# HACK: IDA does not disassemble "\x00\x00" unless you enable
# "Disassemble zero opcode instructions" in Processor Options.
# Since this option is normally disabled, I will attempt
# to get this instruction manually.
# Read two bytes from memory cache at current instruction candidate
dbg_mem_offset = ea - (ea_end - self.maxRopOffset)
dbg_mem = self.dbg_mem_cache[dbg_mem_offset:dbg_mem_offset + 2]
# BUGFIX: For some reason the length of dbg_mem may be 0 (perhaps we ran out of cache?), so
# verify the size is valid before using the buffer.
if len(dbg_mem) != 2:
return None
# Compare to two zero bytes
if dbg_mem[:2] == "\x00\x00":
if self.sploiter.addr64:
instructions.append("add [rax],al")
else:
instructions.append("add [eax],al")
use_registers.add("al")
operations.add("reg-to-mem")
ea += 2
# "MOV Sreg, r/m16" instructions will result in illegal instruction exception: c000001d
# or the memory couldn't be read exception: c0000005 which we don't want in our gadgets.
elif dbg_mem[0] == "\x8E":
return None
# Record a "bad byte" if allowed
elif dbg_mem and not self.ropNoBadBytes:
byte = dbg_mem[0]
instructions.append("db %sh" % binascii.hexlify(byte))
ea += 1
# Invalidate the gadget
else:
return None
# Failed to build a gadget, because RETN instruction was not found
else:
return None
###############################################################
# Decode instruction
def decode_instruction(self, insn, ea, ea_end):
# Instruction specific characteristics
insn_chg_registers = set()
insn_use_registers = set()
insn_operations = set()
insn_pivot = 0
# Instruction feature
#
# instruc_t.feature
#
# CF_STOP = 0x00001 # Instruction doesn't pass execution to the next instruction
# CF_CALL = 0x00002 # CALL instruction (should make a procedure here)
# CF_CHG1 = 0x00004 # The instruction modifies the first operand
# CF_CHG2 = 0x00008 # The instruction modifies the second operand
# CF_CHG3 = 0x00010 # The instruction modifies the third operand
# CF_CHG4 = 0x00020 # The instruction modifies 4 operand
# CF_CHG5 = 0x00040 # The instruction modifies 5 operand
# CF_CHG6 = 0x00080 # The instruction modifies 6 operand
# CF_USE1 = 0x00100 # The instruction uses value of the first operand
# CF_USE2 = 0x00200 # The instruction uses value of the second operand
# CF_USE3 = 0x00400 # The instruction uses value of the third operand
# CF_USE4 = 0x00800 # The instruction uses value of the 4 operand
# CF_USE5 = 0x01000 # The instruction uses value of the 5 operand
# CF_USE6 = 0x02000 # The instruction uses value of the 6 operand
# CF_JUMP = 0x04000 # The instruction passes execution using indirect jump or call (thus needs additional analysis)
# CF_SHFT = 0x08000 # Bit-shift instruction (shl,shr...)
# CF_HLL = 0x10000 # Instruction may be present in a high level language function.
insn_feature = insn.get_canon_feature()
# Instruction mnemonic name
insn_mnem = insn.get_canon_mnem()
# if insn_mnem in self.mnems: self.mnems[insn_mnem] += 1
# else: self.mnems[insn_mnem] = 1
# Get instruction operand types
#
# op_t.type
# Description Data field
# o_void = 0 # No Operand ----------
# o_reg = 1 # General Register (al,ax,es,ds...) reg
# o_mem = 2 # Direct Memory Reference (DATA) addr
# o_phrase = 3 # Memory Ref [Base Reg + Index Reg] phrase
# o_displ = 4 # Memory Reg [Base Reg + Index Reg + Displacement] phrase+addr
# o_imm = 5 # Immediate Value value
# o_far = 6 # Immediate Far Address (CODE) addr
# o_near = 7 # Immediate Near Address (CODE) addr
insn_op1 = insn.Operands[0].type
insn_op2 = insn.Operands[1].type
###############################################################
# Filter gadget
###############################################################
# Do not filter ROP, JOP, COP, always decode them
# NOTE: A separate check must be done to check if they are out of place.
if not insn_mnem in ["retn", "jmp", "call"]:
# Filter gadgets with instructions that don't forward execution to the next address
if insn_feature & idaapi.CF_STOP:
return None
# Filter gadgets with instructions in a bad list
elif insn_mnem in self.ropBadMnems:
return None
# Filter gadgets with jump instructions
# Note: conditional jumps may still be useful if we can
# set flags prior to calling them.
elif not self.ropAllowJcc and insn_mnem[0] == "j":
return None
###############################################################
# Get disassembly
###############################################################
# NOTE: GENDSM_FORCE_CODE ensures correct decoding
# of split instructions.
insn_disas = idc.GetDisasmEx(ea, idaapi.GENDSM_FORCE_CODE)
insn_disas = insn_disas.partition(';')[0] # Remove comments from disassembly
insn_disas = ' '.join(insn_disas.split()) # Remove extraneous space from disassembly
###############################################################
# Analyze instruction
###############################################################
# Standalone instruction
if insn_op1 == idaapi.o_void:
# TODO: Determine and test how these instructions affect the stack
# in 32-bit and 64-bit modes.
if insn_mnem in ["pusha", "pushad", "popa", "popad", "pushf", "pushfd", "pushfq", "popf", "popfd", "popfq"]:
insn_operations.add("stack")
if insn_mnem in ["popa", "popad"]:
insn_pivot += 7 * 4
elif insn_mnem in ["pusha", "pushad"]:
insn_pivot -= 8 * 4
elif insn_mnem in ["popf", "popfd"]:
insn_pivot += 4
elif insn_mnem in ["pushf", "pushfd"]:
insn_pivot -= 4
elif insn_mnem == "popfq": # TODO: Needs testing
insn_pivot += 8
elif insn_mnem == "pushfq": # TODO: Needs testing
insn_pivot -= 8
# Single operand instruction
elif insn_op2 == idaapi.o_void:
# Single operand register
if insn_op1 == idaapi.o_reg:
insn_operations.add("one-reg")
if insn_feature & idaapi.CF_CHG1:
reg_name = self.get_o_reg_name(insn, 0)
insn_chg_registers.add(reg_name)
# Check for stack operation
if reg_name[1:] == "sp":
insn_operations.add("stack")
if insn_mnem == "inc":
insn_pivot += 1
elif insn_mnem == "dec":
insn_pivot -= 1
elif insn_feature & idaapi.CF_USE1:
reg_name = self.get_o_reg_name(insn, 0)
insn_use_registers.add(reg_name)
# Single operand immediate
elif insn_op1 == idaapi.o_imm:
insn_operations.add("one-imm")
# Single operand reference
# TODO: determine the [reg + ...] value if present
elif insn_op1 == idaapi.o_phrase or insn_op1 == idaapi.o_displ:
insn_operations.add("one-mem")
# PUSH/POP mnemonic with a any operand type
if insn_mnem in ["push", "pop"]:
insn_operations.add("stack")
# Adjust pivot based on operand size (32bit vs 64bit)
if insn_mnem == "pop":
if insn.Operands[0].dtyp == idaapi.dt_dword:
insn_pivot += 4
elif insn.Operands[0].dtyp == idaapi.dt_qword:
insn_pivot += 8
elif insn_mnem == "push":
if insn.Operands[0].dtyp == idaapi.dt_dword:
insn_pivot -= 4
elif insn.Operands[0].dtyp == idaapi.dt_qword:
insn_pivot -= 8
# Check for arithmetic operation:
if insn_mnem in self.insn_arithmetic_ops:
insn_operations.add("math")
# Check for bit-wise operations:
if insn_mnem in self.insn_bit_ops:
insn_operations.add("bit")
# Two operand instruction
else:
# Check for arithmetic operations
if insn_mnem in self.insn_arithmetic_ops:
insn_operations.add("math")
# Check for bit-wise operations