forked from Vector35/binaryninja-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtransform.py
More file actions
1264 lines (1041 loc) · 47.7 KB
/
transform.py
File metadata and controls
1264 lines (1041 loc) · 47.7 KB
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
# Copyright (c) 2015-2026 Vector 35 Inc
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import traceback
import ctypes
import abc
import io
import json
import zipfile
from typing import List, Mapping, Optional, Union, Any
# Binary Ninja components
import binaryninja
from .log import log_error_for_exception, log_error
from . import databuffer
from . import binaryview
from . import metadata
from . import _binaryninjacore as core
from .enums import TransformCapabilities, TransformResult, TransformType
from .settings import Settings
class _TransformMetaClass(type):
def __iter__(self):
binaryninja._init_plugins()
count = ctypes.c_ulonglong()
xforms = core.BNGetTransformTypeList(count)
assert xforms is not None, "core.BNGetTransformTypeList returned None"
result = []
for i in range(0, count.value):
ptr_addr = ctypes.cast(xforms[i], ctypes.c_void_p).value
handle = ctypes.cast(ptr_addr, type(xforms[i]))
result.append(Transform(handle))
core.BNFreeTransformTypeList(xforms)
for xform in result:
yield xform
def __getitem__(cls, name):
binaryninja._init_plugins()
xform = core.BNGetTransformByName(name)
if xform is None:
raise KeyError("'%s' is not a valid transform" % str(name))
return Transform(xform)
def __contains__(cls: '_TransformMetaClass', name: object) -> bool:
if not isinstance(name, str):
return False
try:
cls[name]
return True
except KeyError:
return False
def get(cls: '_TransformMetaClass', name: str, default: Any = None) -> Optional['Transform']:
try:
return cls[name]
except KeyError:
if default is not None:
return default
return None
class TransformParameter:
def __init__(self, name, long_name=None, fixed_length=0):
self._name = name
if long_name is None:
self._long_name = name
else:
self._long_name = long_name
self._fixed_length = fixed_length
def __repr__(self):
return "<TransformParameter: {} fixed length: {}>".format(self._long_name, self._fixed_length)
@property
def name(self):
"""(read-only)"""
return self._name
@property
def long_name(self):
"""(read-only)"""
return self._long_name
@property
def fixed_length(self):
"""(read-only)"""
return self._fixed_length
class Transform(metaclass=_TransformMetaClass):
"""
``class Transform`` allows users to implement custom transformations. New transformations may be added at runtime,
so an instance of a transform is created like::
>>> list(Transform)
[<transform: Zlib>, <transform: StringEscape>, <transform: RawHex>, <transform: HexDump>, <transform: Base64>, <transform: Reverse>, <transform: CArray08>, <transform: CArrayA16>, <transform: CArrayA32>, <transform: CArrayA64>, <transform: CArrayB16>, <transform: CArrayB32>, <transform: CArrayB64>, <transform: IntList08>, <transform: IntListA16>, <transform: IntListA32>, <transform: IntListA64>, <transform: IntListB16>, <transform: IntListB32>, <transform: IntListB64>, <transform: MD4>, <transform: MD5>, <transform: SHA1>, <transform: SHA224>, <transform: SHA256>, <transform: SHA384>, <transform: SHA512>, <transform: AES-128 ECB>, <transform: AES-128 CBC>, <transform: AES-256 ECB>, <transform: AES-256 CBC>, <transform: DES ECB>, <transform: DES CBC>, <transform: Triple DES ECB>, <transform: Triple DES CBC>, <transform: RC2 ECB>, <transform: RC2 CBC>, <transform: Blowfish ECB>, <transform: Blowfish CBC>, <transform: CAST ECB>, <transform: CAST CBC>, <transform: RC4>, <transform: XOR>]
>>> sha512=Transform['SHA512']
>>> rawhex=Transform['RawHex']
>>> rawhex.encode(sha512.encode("test string"))
'10e6d647af44624442f388c2c14a787ff8b17e6165b83d767ec047768d8cbcb71a1a3226e7cc7816bc79c0427d94a9da688c41a3992c7bf5e4d7cc3e0be5dbac'
Note that some transformations take additional parameters (most notably encryption ones that require a 'key' parameter passed via a dict):
>>> xor=Transform['XOR']
>>> rawhex=Transform['RawHex']
>>> xor.encode("Original Data", {'key':'XORKEY'})
>>> rawhex.encode(xor.encode("Original Data", {'key':'XORKEY'}))
b'173d3b2c2c373923720f242d39'
"""
transform_type = None
capabilities = 0
supports_detection = False
name = None
long_name = None
group = None
parameters = []
_registered_cb = None
def __init__(self, handle):
if handle is None:
self._cb = core.BNCustomTransform()
self._cb.context = 0
self._cb.getParameters = self._cb.getParameters.__class__(self._get_parameters)
self._cb.freeParameters = self._cb.freeParameters.__class__(self._free_parameters)
self._cb.decode = self._cb.decode.__class__(self._decode)
self._cb.encode = self._cb.encode.__class__(self._encode)
self._cb.decodeWithContext = self._cb.decodeWithContext.__class__(self._decode_with_context)
self._cb.canDecode = self._cb.canDecode.__class__(self._can_decode)
self._pending_param_lists = {}
self.type = self.__class__.transform_type
if not isinstance(self.type, str):
assert self.type is not None, "Transform Type is None"
self.type = TransformType(self.type)
self.name = self.__class__.name
self.long_name = self.__class__.long_name
self.group = self.__class__.group
self.parameters = self.__class__.parameters
else:
self.handle = handle
self.type = TransformType(core.BNGetTransformType(self.handle))
self.capabilities = core.BNGetTransformCapabilities(self.handle)
self.supports_detection = core.BNTransformSupportsDetection(self.handle)
self.supports_context = core.BNTransformSupportsContext(self.handle)
self.name = core.BNGetTransformName(self.handle)
self.long_name = core.BNGetTransformLongName(self.handle)
self.group = core.BNGetTransformGroup(self.handle)
count = ctypes.c_ulonglong()
params = core.BNGetTransformParameterList(self.handle, count)
assert params is not None, "core.BNGetTransformParameterList returned None"
self.parameters = []
for i in range(0, count.value):
self.parameters.append(TransformParameter(params[i].name, params[i].longName, params[i].fixedLength))
core.BNFreeTransformParameterList(params, count.value)
def __repr__(self):
return "<transform: %s>" % self.name
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return ctypes.addressof(self.handle.contents) == ctypes.addressof(other.handle.contents)
def __ne__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return not (self == other)
def __hash__(self):
return hash(ctypes.addressof(self.handle.contents))
@classmethod
def register(cls):
binaryninja._init_plugins()
if cls.name is None:
raise ValueError("transform 'name' is not defined")
if cls.long_name is None:
cls.long_name = cls.name
if cls.transform_type is None:
raise ValueError("transform 'transform_type' is not defined")
if cls.group is None:
cls.group = ""
xform = cls(None)
cls._registered_cb = xform._cb
xform.handle = core.BNRegisterTransformTypeWithCapabilities(cls.transform_type, cls.capabilities, cls.name, cls.long_name, cls.group, xform._cb)
def _get_parameters(self, ctxt, count):
try:
count[0] = len(self.parameters)
param_buf = (core.BNTransformParameterInfo * len(self.parameters))()
for i in range(0, len(self.parameters)):
param_buf[i].name = core.cstr(self.parameters[i].name)
param_buf[i].longName = core.cstr(self.parameters[i].long_name)
param_buf[i].fixedLength = self.parameters[i].fixed_length
result = ctypes.cast(param_buf, ctypes.c_void_p)
self._pending_param_lists[result.value] = (result, param_buf)
return result.value
except:
log_error_for_exception("Unhandled Python exception in Transform._get_parameters")
count[0] = 0
return None
def _free_parameters(self, params, count):
try:
buf = ctypes.cast(params, ctypes.c_void_p)
if buf.value not in self._pending_param_lists:
raise ValueError("freeing parameter list that wasn't allocated")
del self._pending_param_lists[buf.value]
except:
log_error_for_exception("Unhandled Python exception in Transform._free_parameters")
def _decode(self, ctxt, input_buf, output_buf, params, count):
try:
input_obj = databuffer.DataBuffer(handle=core.BNDuplicateDataBuffer(input_buf))
param_map = {}
for i in range(0, count):
data = databuffer.DataBuffer(handle=core.BNDuplicateDataBuffer(params[i].value))
param_map[params[i].name] = bytes(data)
result = self.perform_decode(bytes(input_obj), param_map)
if result is None:
return False
result = bytes(result)
core.BNSetDataBufferContents(output_buf, result, len(result))
return True
except:
log_error_for_exception("Unhandled Python exception in Transform._decode")
return False
def _encode(self, ctxt, input_buf, output_buf, params, count):
try:
input_obj = databuffer.DataBuffer(handle=core.BNDuplicateDataBuffer(input_buf))
param_map = {}
for i in range(0, count):
data = databuffer.DataBuffer(handle=core.BNDuplicateDataBuffer(params[i].value))
param_map[params[i].name] = bytes(data)
result = self.perform_encode(bytes(input_obj), param_map)
if result is None:
return False
result = bytes(result)
core.BNSetDataBufferContents(output_buf, result, len(result))
return True
except:
log_error_for_exception("Unhandled Python exception in Transform._encode")
return False
def _decode_with_context(self, ctxt, context, params, count):
try:
context_obj = TransformContext(context)
param_map = {}
for i in range(0, count):
data = databuffer.DataBuffer(handle=core.BNDuplicateDataBuffer(params[i].value))
param_map[params[i].name] = bytes(data)
result = self.perform_decode_with_context(context_obj, param_map)
return result if result is not None else False
except:
log_error_for_exception("Unhandled Python exception in Transform._decode_with_context")
return False
def _can_decode(self, ctxt, input):
try:
input_obj = binaryview.BinaryView(handle=input)
return self.can_decode(input_obj)
except:
log_error_for_exception("Unhandled Python exception in Transform._can_decode")
return False
@abc.abstractmethod
def perform_decode(self, data, params):
if self.type == TransformType.InvertingTransform:
return self.perform_encode(data, params)
return None
@abc.abstractmethod
def perform_encode(self, data, params):
return None
def perform_decode_with_context(self, context, params):
return False
def decode(self, input_buf, params={}):
if isinstance(input_buf, int):
raise TypeError("input_buf cannot be an integer")
input_buf = databuffer.DataBuffer(input_buf)
output_buf = databuffer.DataBuffer()
keys = list(params.keys())
param_buf = (core.BNTransformParameter * len(keys))()
data = []
for i in range(0, len(keys)):
param_value = params[keys[i]]
# Validate parameter type
if not isinstance(param_value, (bytes, bytearray, str, databuffer.DataBuffer)):
raise TypeError(f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}. "
f"If you need to pass an integer like 0x{param_value:x} as a key, convert it to bytes first: bytes([0x{param_value:x}])"
if isinstance(param_value, int) else
f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}")
data.append(databuffer.DataBuffer(param_value))
param_buf[i].name = keys[i]
param_buf[i].value = data[i].handle
if not core.BNDecode(self.handle, input_buf.handle, output_buf.handle, param_buf, len(keys)):
return None
return bytes(output_buf)
def encode(self, input_buf, params={}):
if isinstance(input_buf, int):
raise TypeError("input_buf cannot be an integer")
input_buf = databuffer.DataBuffer(input_buf)
output_buf = databuffer.DataBuffer()
keys = list(params.keys())
param_buf = (core.BNTransformParameter * len(keys))()
data = []
for i in range(0, len(keys)):
param_value = params[keys[i]]
# Validate parameter type
if not isinstance(param_value, (bytes, bytearray, str, databuffer.DataBuffer)):
raise TypeError(f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}. "
f"If you need to pass an integer like 0x{param_value:x} as a key, convert it to bytes first: bytes([0x{param_value:x}])"
if isinstance(param_value, int) else
f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}")
data.append(databuffer.DataBuffer(param_value))
param_buf[i].name = keys[i]
param_buf[i].value = data[i].handle
if not core.BNEncode(self.handle, input_buf.handle, output_buf.handle, param_buf, len(keys)):
return None
return bytes(output_buf)
def decode_with_context(self, context, params={}):
"""
``decode_with_context`` performs context-aware transformation for container formats,
enabling multi-file extraction
**Processing Protocol:**
Container transforms typically operate in two phases:
1. **Discovery Phase**: Transform enumerates available files and populates
``context.available_files``. Returns ``False`` to indicate user file selection
is required.
2. **Extraction Phase**: Transform processes ``context.requested_files`` and
creates child contexts for each file with extraction results. Returns ``True``
when extraction is complete.
**Return Value Semantics:**
- ``True``: Processing complete, no more user interaction needed
- ``False``: Processing incomplete, requires user input or session management
(e.g., file selection after discovery)
**Error Reporting:**
Extraction results and messages are accessible via context properties:
- **Context-level** (transformation/extraction status):
- ``context.extraction_result``: Result of parent producing input
- ``context.extraction_message``: Human-readable extraction message
- ``context.transform_result``: Result of applying transform to input
Common error scenarios:
- Archive encrypted, password required
- Corrupt archive structure
- Unsupported archive format
- Individual file extraction failures
**Usage Examples:**
.. code-block:: python
from binaryninja import TransformSession
# Full mode - automatically extracts all files
session = TransformSession("archive.zip")
if session.process(): # All extraction complete, no interaction needed
# Select the intended context(s) for loading
session.set_selected_contexts(session.current_context)
# Load the resulting BinaryView(s)
loaded_view = load(session.current_view)
else:
# Extraction incomplete - user input required
print("Extraction requires user input")
# Interactive mode - requires manual processing for each step
session = TransformSession("nested.zip")
while not session.process():
# Process returned False - user input needed
ctx = session.current_context
# Check if parent has available files for selection
if ctx.parent and ctx.parent.has_available_files:
# Show files to user and let them select
available = ctx.parent.available_files
print(f"Available files: {available}")
# Select files to extract (or all)
ctx.parent.set_requested_files(available)
# Continue processing from parent
session.process_from(ctx.parent)
# Extraction complete - select and load the final context
session.set_selected_contexts(session.current_context)
final_view = session.current_view
:param TransformContext context: Transform context containing input data and state
:param dict params: Optional transform parameters (e.g., passwords, settings)
:return: True if processing complete, False if user input required
:rtype: bool
"""
if not isinstance(context, TransformContext):
return None
keys = list(params.keys())
param_buf = (core.BNTransformParameter * len(keys))()
data = []
for i in range(0, len(keys)):
param_value = params[keys[i]]
# Validate parameter type
if not isinstance(param_value, (bytes, bytearray, str, databuffer.DataBuffer)):
raise TypeError(f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}. "
f"If you need to pass an integer like 0x{param_value:x} as a key, convert it to bytes first: bytes([0x{param_value:x}])"
if isinstance(param_value, int) else
f"Transform parameter '{keys[i]}' must be bytes, bytearray, str, or DataBuffer, not {type(param_value).__name__}")
data.append(databuffer.DataBuffer(param_value))
param_buf[i].name = keys[i]
param_buf[i].value = data[i].handle
if not core.BNDecodeWithContext(self.handle, context.handle, param_buf, len(keys)):
return False
return True
def can_decode(self, input):
"""
``can_decode`` checks if this transform can decode the given input.
:param input: can be a :py:class:`bytes`, :py:class:`bytearray`, :py:class:`DataBuffer`, or :py:class:`BinaryView`
:return: :py:class:`bool` indicating whether the transform can decode the input
:rtype: bool
"""
if isinstance(input, bytes) or isinstance(input, bytearray) or isinstance(input, databuffer.DataBuffer):
with binaryview.BinaryView.new(input) as view:
return core.BNCanDecode(self.handle, view.handle)
elif isinstance(input, binaryview.BinaryView):
return core.BNCanDecode(self.handle, input.handle)
return False
class TransformContext:
"""
``TransformContext`` represents a node in the container extraction tree, containing the input data,
transformation state, and relationships to parent/child contexts.
Each context can have:
- Input data (BinaryView)
- Transform information (name, parameters, results)
- File selection state (available_files, requested_files)
- Parent/child relationships for nested containers
- Extraction status and error messages
Contexts are typically accessed through a ``TransformSession`` rather than created directly.
**Example:**
.. code-block:: python
session = TransformSession("archive.zip")
session.process()
# Access context properties
ctx = session.current_context
print(f"Filename: {ctx.filename}")
print(f"Transform: {ctx.transform_name}")
print(f"Size: {ctx.input.length}")
# Navigate the tree
if ctx.parent:
print(f"Parent files: {ctx.parent.available_files}")
# Check extraction status
if ctx.extraction_result != 0:
print(f"Error: {ctx.extraction_message}")
"""
def __init__(self, handle):
self.handle = core.handle_of_type(handle, core.BNTransformContext)
def __del__(self):
if core is not None:
core.BNFreeTransformContext(self.handle)
@property
def available_transforms(self) -> List[str]:
"""
Get the list of transforms that can decode this context's input.
Binary Ninja auto-detects which transforms can handle the current data by checking each
transform's ``can_decode()`` method. This property returns the names of all transforms
that reported they can decode this context's input.
:return: List of transform names that can decode this data
:rtype: List[str]
"""
count = ctypes.c_size_t()
transforms = core.BNTransformContextGetAvailableTransforms(self.handle, ctypes.byref(count))
if transforms is None:
return []
result = []
for i in range(count.value):
result.append(transforms[i].decode('utf-8'))
core.BNFreeStringList(transforms, count.value)
return result
@property
def transform_name(self) -> str:
"""Get the name of the transform that created this context"""
return core.BNTransformContextGetTransformName(self.handle)
def set_transform_name(self, transform_name: str):
"""
Manually specify which transform to apply to this context.
Use this when auto-detection is not possible or when you want to override the detected transform.
This is commonly needed for formats without magic bytes (like Base64) or when forcing a specific decoder.
After setting the transform name, call ``session.process_from(context)`` to apply the transform.
:param transform_name: Name of the transform to apply (e.g., "Base64", "Gzip", "XOR")
**Example:**
.. code-block:: python
# Base64 has no magic bytes, so it's not auto-detected
session = TransformSession("data.zip")
session.process()
ctx = session.current_context
ctx.set_transform_name("Base64") # Manually specify Base64
# Now apply the Base64 transform
if session.process_from(ctx):
print("Base64 decoded successfully")
"""
core.BNTransformContextSetTransformName(self.handle, transform_name)
@property
def filename(self) -> str:
"""Get the filename for this context"""
return core.BNTransformContextGetFileName(self.handle)
@property
def input(self) -> Optional['binaryview.BinaryView']:
"""Get the input BinaryView for this context"""
view = core.BNTransformContextGetInput(self.handle)
if view is None:
return None
return binaryview.BinaryView(handle=view)
@property
def metadata_obj(self) -> Optional['metadata.Metadata']:
"""
Get the metadata associated with this extraction context.
Container transforms can store format-specific metadata during extraction (e.g., timestamps,
permissions, compression ratios, archive structure). This metadata is preserved in the context
tree and can be accessed for analysis or debugging.
:return: Metadata object containing transform-specific key-value pairs, or None if no metadata
:rtype: Metadata or None
"""
meta = core.BNTransformContextGetMetadata(self.handle)
if meta is None:
return None
return metadata.Metadata(handle=meta)
def set_transform_parameter(self, name: str, data: databuffer.DataBuffer):
"""
Set a parameter for the transform (e.g., password, encryption key).
Transform parameters provide additional input required for decoding, such as passwords for
encrypted archives or keys for encryption transforms. Parameters are passed to the transform's
decode operation.
:param name: Parameter name (e.g., "password", "key")
:param data: Parameter value as a DataBuffer
**Example:**
.. code-block:: python
# Create session and attempt extraction
session = TransformSession("encrypted.zip")
session.process() # Returns False - processing incomplete
# Check why extraction failed
if session.current_context.extraction_result == TransformResult.TransformRequiresPassword:
# Password is set on the parent context (the one doing extraction)
parent = session.current_context.parent
parent.set_transform_parameter("password", DataBuffer("secret_password"))
# Retry extraction from parent
if session.process_from(parent):
# Verify successful extraction
assert parent.children[0].extraction_result == TransformResult.TransformSuccess
print("Archive decrypted successfully")
"""
core.BNTransformContextSetTransformParameter(self.handle, name, data.handle)
def has_transform_parameter(self, name: str) -> bool:
"""Check if a transform parameter exists"""
return core.BNTransformContextHasTransformParameter(self.handle, name)
def clear_transform_parameter(self, name: str):
"""Clear a transform parameter"""
core.BNTransformContextClearTransformParameter(self.handle, name)
@property
def extraction_message(self) -> str:
"""Get the extraction message"""
return core.BNTransformContextGetExtractionMessage(self.handle)
@property
def extraction_result(self) -> TransformResult:
"""Get the extraction result"""
return TransformResult(core.BNTransformContextGetExtractionResult(self.handle))
@property
def transform_result(self) -> TransformResult:
"""Get the transform result"""
return TransformResult(core.BNTransformContextGetTransformResult(self.handle))
@transform_result.setter
def transform_result(self, result: TransformResult):
"""Set the transform result"""
core.BNTransformContextSetTransformResult(self.handle, result)
@property
def parent(self) -> Optional['TransformContext']:
"""Get the parent context"""
parent = core.BNTransformContextGetParent(self.handle)
if parent is None:
return None
return TransformContext(parent)
@property
def child_count(self) -> int:
"""Get the number of child contexts"""
return core.BNTransformContextGetChildCount(self.handle)
@property
def children(self) -> List['TransformContext']:
"""Get all child contexts"""
count = ctypes.c_size_t()
children = core.BNTransformContextGetChildren(self.handle, ctypes.byref(count))
if children is None:
return []
result = []
for i in range(count.value):
result.append(TransformContext(core.BNNewTransformContextReference(children[i])))
core.BNFreeTransformContextList(children, count.value)
return result
def get_child(self, filename: str) -> Optional['TransformContext']:
"""Get a child context by filename"""
child = core.BNTransformContextGetChild(self.handle, filename)
if child is None:
return None
return TransformContext(child)
def create_child(self, data: databuffer.DataBuffer, filename: str = "", result: TransformResult = TransformResult.TransformSuccess, message: str = "", filename_is_descriptor: bool = False) -> 'TransformContext':
"""Create a new child context with the given data, filename, result status, and message
:param data: The data for the child context
:param filename: The filename for the child context (default: "")
:param result: Transform result for the child (default: TransformResult.TransformSuccess)
:param message: Extraction message for the child (default: "")
:param filename_is_descriptor: Whether the filename is a descriptor that should be combined with parent (default: False)
"""
child = core.BNTransformContextSetChild(self.handle, data.handle, filename, result, message, filename_is_descriptor)
if child is None:
raise RuntimeError("Failed to create child context")
return TransformContext(child)
@property
def is_leaf(self) -> bool:
"""Check if this context is a leaf (has no children)"""
return core.BNTransformContextIsLeaf(self.handle)
@property
def is_root(self) -> bool:
"""Check if this context is the root (has no parent)"""
return core.BNTransformContextIsRoot(self.handle)
@property
def available_files(self) -> List[str]:
"""
Get the list of files available for extraction from this container.
This property is populated during the **Discovery Phase** of container extraction, when a transform
enumerates the contents of an archive without extracting them.
**Mode Behavior:**
- **Full Mode (default)**: Discovery and extraction happen automatically in one pass. After ``process()``,
``available_files`` will be populated on the container context (the one with the archive transform),
and all files will already be extracted.
- **Interactive Mode**: Discovery pauses for user selection. After first ``process()``, ``available_files``
is populated on the parent context (the container), and you must call ``set_requested_files()`` before extraction proceeds.
:return: List of filenames that can be extracted from this container
:rtype: List[str]
**Example (Full Mode - Automatic):**
.. code-block:: python
# Full mode (default) - all files extracted automatically
session = TransformSession("archive.zip")
session.process() # Discovery + extraction in one pass
# After processing, available_files shows what was discovered on the container
# For a root-level archive, this is the root context
container = session.root_context
print(f"Extracted {len(container.available_files)} files")
print(f"Files: {container.available_files[:5]}...")
**Example (Interactive Mode - User Selection):**
.. code-block:: python
# Interactive mode - user selects files
session = TransformSession("archive.zip", mode=TransformSessionMode.TransformSessionModeInteractive)
session.process() # Discovery phase only - returns False
# available_files is on the parent (the container doing extraction)
container = session.current_context.parent
if container.has_available_files:
print(f"Archive contains {len(container.available_files)} files")
print(f"Files: {container.available_files[:5]}...")
# User selects which files to extract
container.set_requested_files(["important.bin", "config.txt"])
# Extract selected files
session.process_from(container)
"""
count = ctypes.c_size_t()
files = core.BNTransformContextGetAvailableFiles(self.handle, ctypes.byref(count))
if files is None:
return []
result = []
for i in range(count.value):
result.append(files[i].decode('utf-8'))
core.BNFreeStringList(files, count.value)
return result
def set_available_files(self, files: List[str]):
"""
Populate the list of files available for extraction (Discovery Phase).
Container transforms call this during the **Discovery Phase** to enumerate files without extracting them.
After calling this, the transform should return ``False`` to indicate user selection is needed.
**Session Mode Handling:**
- **Full Mode**: Session automatically calls ``set_requested_files(available_files)`` and re-invokes
the transform for extraction, so all files are extracted in one pass.
- **Interactive Mode**: Transform returns ``False``, user must call ``set_requested_files()`` manually,
then call ``process_from()`` to continue.
:param files: List of filenames that can be extracted from this container
"""
file_array = (ctypes.c_char_p * len(files))()
for i, f in enumerate(files):
file_array[i] = core.cstr(f)
core.BNTransformContextSetAvailableFiles(self.handle, file_array, len(files))
@property
def has_available_files(self) -> bool:
"""Check if this context has available files for selection"""
return core.BNTransformContextHasAvailableFiles(self.handle)
@property
def requested_files(self) -> List[str]:
"""
Get the list of files requested for extraction from this container.
This property contains the filenames that have been selected for extraction during the **Extraction Phase**.
Container transforms read this property to determine which files to extract and create child contexts for.
:return: List of filenames requested for extraction
:rtype: List[str]
"""
count = ctypes.c_size_t()
files = core.BNTransformContextGetRequestedFiles(self.handle, ctypes.byref(count))
if files is None:
return []
result = []
for i in range(count.value):
result.append(files[i].decode('utf-8'))
core.BNFreeStringList(files, count.value)
return result
def set_requested_files(self, files: List[str]):
"""
Specify which files to extract from this container (Extraction Phase).
Call this after ``available_files`` has been populated to indicate which files should be extracted.
After setting this, call ``session.process_from(context)`` to perform the extraction.
**Mode Behavior:**
- **Full Mode**: Called automatically by the session with all available files, you rarely need to call this.
- **Interactive Mode**: You must call this manually to select which files to extract.
:param files: List of filenames to extract (must be subset of ``available_files``)
"""
file_array = (ctypes.c_char_p * len(files))()
for i, f in enumerate(files):
file_array[i] = core.cstr(f)
core.BNTransformContextSetRequestedFiles(self.handle, file_array, len(files))
@property
def has_requested_files(self) -> bool:
"""Check if this context has requested files"""
return core.BNTransformContextHasRequestedFiles(self.handle)
@property
def is_database(self) -> bool:
"""Check if this context represents a database file"""
return core.BNTransformContextIsDatabase(self.handle)
@property
def is_interactive(self) -> bool:
"""
Check if this context is in interactive mode.
This flag indicates whether the transform session is operating in interactive mode (e.g., UI with
user dialogs) or non-interactive mode (e.g., headless/auto-open). Transforms can use this to
adjust their behavior. For example, filtering children in non-interactive mode while showing
all children in interactive mode.
:return: True if in interactive mode, False otherwise
:rtype: bool
"""
return core.BNTransformContextIsInteractive(self.handle)
@property
def settings(self) -> 'Settings':
"""
Get the settings object for this transform context.
This provides access to session-time settings overrides passed via the ``TransformSession`` ``options``
parameter. These ephemeral settings override global settings for this session only. Transforms should
use this `Settings` object instead of ``Settings()`` to read settings values that may have been
overridden for the session.
:return: Settings object
:rtype: Settings
"""
handle = core.BNTransformContextGetSettings(self.handle)
if handle is None:
return None
return Settings(handle=handle)
class TransformSession:
"""
``TransformSession`` manages the extraction workflow for container files (ZIP, TAR, IMG4, etc.),
handling multi-stage extraction, file selection, and transform application.
Sessions automatically detect and apply appropriate transforms to navigate through nested containers,
maintaining a tree of ``TransformContext`` objects representing each extraction stage.
:param Union[str, 'binaryview.BinaryView'] filename_or_view: Path to the file to process, or an existing BinaryView to start from.
:param TransformSessionMode mode: Session mode controlling extraction behavior. Can be ``TransformSessionMode.Full`` (automatic), ``TransformSessionMode.Interactive`` (requires user selection), or None to use the default mode from settings. Defaults to None.
:param dict options: Dictionary of session-time settings overrides that apply only to this session. These ephemeral settings override global settings and are accessible to transforms via ``TransformContext.settings``. For example, ``{'files.universal.architecturePreference': ['x86_64']}`` to prefer x86_64 when opening universal binaries. Defaults to empty dict.
**Modes:**
- **Full Mode** (default): Automatically extracts all files through nested containers
- **Interactive Mode**: Requires user file selection at each stage
**Basic Usage:**
.. code-block:: python
from binaryninja import TransformSession
# Full automatic extraction
session = TransformSession("archive.zip")
if session.process():
final_data = session.current_view
load(final_data)
**Interactive Extraction:**
.. code-block:: python
session = TransformSession("nested_archive.zip")
while not session.process():
# User input needed
ctx = session.current_context
if ctx.parent and ctx.parent.has_available_files:
# Show file choices to user
print(f"Available: {ctx.parent.available_files}")
# User selects files
ctx.parent.set_requested_files(["important_file.bin"])
# Continue extraction
session.process_from(ctx.parent)
# Access final extracted data
session.set_selected_contexts(session.current_context)
final_view = session.current_view
**Key Methods:**
- ``process()``: Process the next extraction stage
- ``process_from(context)``: Resume processing from a specific context
- ``set_selected_contexts(contexts)``: Mark contexts for final access
**Key Properties:**
- ``current_context``: The current point in the extraction tree
- ``current_view``: The current BinaryView (after processing)
- ``root_context``: The root of the extraction tree
"""
def __init__(self, filename_or_view: Union[str, 'binaryview.BinaryView'], mode=None, options: Mapping[str, Any]={}, handle=None):
if handle is not None:
self.handle = core.handle_of_type(handle, core.BNTransformSession)
elif isinstance(filename_or_view, str):
if mode is None:
self.handle = core.BNCreateTransformSession(filename_or_view, json.dumps(options))
else:
self.handle = core.BNCreateTransformSessionWithMode(filename_or_view, mode, json.dumps(options))
elif hasattr(filename_or_view, 'handle'): # BinaryView
if mode is None:
self.handle = core.BNCreateTransformSessionFromBinaryView(filename_or_view.handle, json.dumps(options))
else:
self.handle = core.BNCreateTransformSessionFromBinaryViewWithMode(filename_or_view.handle, mode, json.dumps(options))
else:
raise TypeError("filename_or_view must be a string filename or BinaryView")
if self.handle is None:
raise RuntimeError("Failed to create TransformSession")
def __del__(self):
if core is not None:
core.BNFreeTransformSession(self.handle)
@property
def current_view(self) -> Optional['binaryview.BinaryView']:
"""Get the current BinaryView for this session"""
view = core.BNTransformSessionGetCurrentView(self.handle)
if view is None:
return None
return binaryview.BinaryView(handle=view)
@property
def root_context(self) -> Optional['TransformContext']:
"""Get the root transform context"""
context = core.BNTransformSessionGetRootContext(self.handle)
if context is None:
return None
return TransformContext(context)
@property
def current_context(self) -> Optional['TransformContext']:
"""Get the current transform context"""
context = core.BNTransformSessionGetCurrentContext(self.handle)
if context is None:
return None
return TransformContext(context)
def process_from(self, context: 'TransformContext') -> bool:
"""
Process the transform session starting from a specific context.
:return: ``True`` if processing completed successfully (all transforms applied and no user input required). \
``False`` if processing is incomplete and requires user input (file selection, password), \
additional parameters, or if an error occurred during transformation.
:rtype: bool
In **Interactive Mode**, this returns ``False`` when user selection is needed at the current stage.