├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── diff.py
├── diff.ui
├── diff_ui.py
├── disasm.py
├── disasm.ui
├── disasm_ui.py
├── dump
└── dummyfile
├── dumper.py
├── enum_ranges.py
├── enum_ranges.ui
├── enum_ranges_ui.py
├── frida_code.py
├── frida_portal.py
├── gadget.py
├── gadget.ui
├── gadget
└── zygisk-gadget-v1.2.1-release.zip
├── gadget_ui.py
├── gadget_win.ui
├── gvar.py
├── hex_viewer.py
├── history.py
├── history.ui
├── history_ui.py
├── icon
├── greenlight.png
├── mlviewerico.png
└── redlight.png
├── list_img_viewer.py
├── main.py
├── misc.py
├── mlviewer_macos.sh
├── mlviewer_wincon.bat
├── parse_unity_dump.py
├── parse_unity_dump_ui.py
├── requirements.txt
├── scan_result.py
├── scan_result.ui
├── scan_result_ui.py
├── scan_result_win.ui
├── scan_result_win_ui.py
├── scripts
├── default.js
├── dump-ios-module.js
├── dump-so.js
├── full-memory-dump.js
├── il2cpp-dump-new.js
├── il2cpp-dump.js
└── util.js
├── spawn.py
├── spawn.ui
├── spawn_ui.py
├── ui.py
├── ui.ui
├── ui_win.py
├── ui_win.ui
├── util_viewer.py
├── watchpoint.py
├── watchpoint_ui.py
└── watchpoint_ui.ui
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-vendored
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 | .DS_Store
3 | test.py
4 | __pycache__
5 | .idea
6 | scripts/script_test
7 | test
8 | /dump/*
9 | !/dump/dummyfile
10 | ui_test.py
11 | ui_test.ui
12 | ui_test2.ui
13 | ui_win-test.py
14 | ui_win-test.ui
15 | test/diff_test.py
16 | test/binarydiffresultview.py
17 | test/binarydiffresultview.ui
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mlviewer
2 | An iOS, Android application memory view & edit PyQt6 application powered by Frida.
3 | It's a program running some useful frida scripts with ui to help mobile app analysis.
4 |
5 | 
6 |
7 | # Prerequisite
8 | ```
9 | python > 3.8.0
10 | Running frida-server on your device
11 | ```
12 |
13 | # Usage
14 | Check the [wiki](https://github.com/hackcatml/mlviewer/wiki) for details.
15 |
16 | # Update
17 | ```
18 | git pull origin main
19 | ```
20 |
21 | # Credits
22 | [dump-ios-module](https://github.com/lich4)
23 | [dump-so](https://github.com/lasting-yang/frida_dump)
24 | [frida-il2cpp-bridge](https://github.com/vfsfitvnm/frida-il2cpp-bridge)
25 | [https://armconverter.com](https://armconverter.com)
26 | [capstone](https://www.capstone-engine.org/)
27 | [frida-dexdump](https://github.com/hluwa/frida-dexdump)
28 | [bindiff](https://github.com/dadadel/bindiff)
29 | [cheat-engine](https://github.com/cheat-engine/cheat-engine)
30 | [r2pipe](https://github.com/radareorg/radare2-r2pipe)
--------------------------------------------------------------------------------
/diff.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from PyQt6 import QtCore, QtWidgets
4 | from PyQt6.QtCore import Qt, pyqtSlot, QThread
5 | from PyQt6.QtGui import QPalette
6 | from PyQt6.QtWidgets import QFileDialog, QApplication
7 | import r2pipe
8 |
9 | import diff_ui
10 |
11 |
12 | class ProcessDiffResultWorker(QThread):
13 | process_diff_result_signal = QtCore.pyqtSignal(str)
14 | process_diff_finished_signal = QtCore.pyqtSignal()
15 |
16 | def __init__(self, formatted_diffs):
17 | super().__init__()
18 | self.formatted_diffs = formatted_diffs
19 |
20 | def is_dark_mode(self):
21 | palette = QApplication.palette()
22 | background_color = palette.color(QPalette.ColorRole.Window)
23 | return background_color.lightness() < 128 # Check if the background color is dark
24 |
25 | def run(self) -> None:
26 | line = ''
27 | line_count = 0
28 |
29 | for block_start, (block1, block2) in self.formatted_diffs.items():
30 | line_count += 1
31 | if line_count == 1:
32 | line += "ADDRESS ".rjust(len(f"0x{block_start:08x} "))
33 | line += "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F || 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
34 | self.process_diff_result_signal.emit(line)
35 | line = ''
36 | QThread.msleep(1)
37 |
38 | line += f"0x{block_start:08x} "
39 | for b1, b2 in zip(block1, block2):
40 | if b1 != b2:
41 | color = "red"
42 | formatted_b1 = '00' if b1 is None else f'{b1:02x}'
43 | line += f'{formatted_b1} '
44 | else:
45 | formatted_b1 = '00' if b1 is None else f'{b1:02x}'
46 | line += f'{formatted_b1} '
47 | line += "!= "
48 | for b1, b2 in zip(block1, block2):
49 | if b1 != b2:
50 | color = "red"
51 | formatted_b2 = '00' if b2 is None else f'{b2:02x}'
52 | line += f'{formatted_b2} '
53 | else:
54 | formatted_b2 = '00' if b2 is None else f'{b2:02x}'
55 | line += f'{formatted_b2} '
56 |
57 | self.process_diff_result_signal.emit(line)
58 | line = ''
59 | QThread.msleep(1)
60 |
61 | self.process_diff_result_signal.emit(line)
62 | self.process_diff_finished_signal.emit()
63 |
64 |
65 | class BinaryCompareWorker(QThread):
66 | binary_compare_finished_sig = QtCore.pyqtSignal(str)
67 |
68 | """Comparing two binary files"""
69 | def __init__(self, file1, file2, sections):
70 | """Get the files to compare and initialise message, offset and diff list.
71 | :param file1: a file
72 | :type file1: string
73 | :param file2: another file to compare
74 | :type file2: string
75 | """
76 | super().__init__()
77 | self._buffer_size = 512
78 | self.message = None
79 | '''message of diff result: "not found", "size", "content", "identical"'''
80 | self.offset = None
81 | '''offset where files start to differ'''
82 | self.diff_list = []
83 | '''list of diffs made of tuples: (offset, hex(byte1), hex(byte2))'''
84 | self.file1 = file1
85 | self.file2 = file2
86 |
87 | self.file1_contents = []
88 | self.file2_contents = []
89 |
90 | self.sections = sections
91 |
92 | self.offset_differs = None
93 |
94 | def read_file_contents(self, filename, start, size):
95 | '''Read file contents into a list of bytes.'''
96 | with open(filename, 'rb') as file:
97 | if start is not None:
98 | file.seek(start)
99 | try:
100 | return list(file.read(size))
101 | except Exception as e:
102 | return list()
103 | else:
104 | try:
105 | return list(file.read())
106 | except Exception as e:
107 | return list()
108 |
109 | def run(self) -> None:
110 | """Compare the two files
111 | :returns: Comparison result: True if similar, False if different.
112 | Set vars offset and message if there's a difference.
113 | """
114 | self.message = None
115 | self.offset_differs = None
116 | offset = 0
117 | if not os.path.isfile(self.file1) or not os.path.isfile(self.file2):
118 | self.message = "not found"
119 | return
120 | if os.path.getsize(self.file1) != os.path.getsize(self.file2):
121 | self.message = "size"
122 | self.binary_compare_finished_sig.emit("size")
123 | return
124 | self.binary_compare_finished_sig.emit("start")
125 | result = True
126 | self.diff_list.clear()
127 |
128 | # Compare each section
129 | for section in self.sections:
130 | start = section['start']
131 | size = section['size']
132 |
133 | # Read section contents
134 | data1 = self.read_file_contents(self.file1, start, size)
135 | data2 = self.read_file_contents(self.file2, start, size)
136 |
137 | if not data1 or not data2:
138 | self.message = "cannot read"
139 | self.binary_compare_finished_sig.emit(self.message)
140 | return
141 |
142 | # Compare the contents byte by byte
143 | for offset, (byte1, byte2) in enumerate(zip(data1, data2), start=start if start is not None else 0):
144 | if byte1 != byte2:
145 | result = False
146 | self.diff_list.append((offset, byte1, byte2))
147 |
148 | if not result:
149 | self.message = "content"
150 | self.offset = hex(offset)
151 | self.file1_contents.append(data1)
152 | self.file2_contents.append(data2)
153 |
154 | # Sort the diff_list by offset in ascending order
155 | self.diff_list.sort(key=lambda x: x[0])
156 |
157 | if result:
158 | self.message = "identical"
159 | else:
160 | self.message = "finished"
161 | self.binary_compare_finished_sig.emit(self.message)
162 |
163 | def format_diff(self, block_size=16):
164 | """Format the differences in blocks of specified size."""
165 |
166 | formatted_diffs = {}
167 | for offset, byte1, byte2 in self.diff_list:
168 | block_start = offset - (offset % block_size)
169 | if block_start not in formatted_diffs:
170 | block1 = [self.file1_contents[i] if i < len(self.file1_contents) else None for i in
171 | range(block_start, block_start + block_size)]
172 | block2 = [self.file2_contents[i] if i < len(self.file2_contents) else None for i in
173 | range(block_start, block_start + block_size)]
174 | formatted_diffs[block_start] = (block1, block2)
175 | index = offset % block_size
176 | formatted_diffs[block_start][0][index] = byte1
177 | formatted_diffs[block_start][1][index] = byte2
178 |
179 | return formatted_diffs
180 |
181 |
182 | class DiffDialogClass(QtWidgets.QDialog):
183 | def __init__(self, statusBar):
184 | super(DiffDialogClass, self).__init__(statusBar)
185 | self.diff_dialog = QtWidgets.QDialog()
186 | self.diff_dialog.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
187 | self.diff_dialog_ui = diff_ui.Ui_DiffDialog()
188 | self.diff_dialog_ui.setupUi(self.diff_dialog)
189 |
190 | self.statusBar = statusBar
191 |
192 | self.file1 = None
193 | self.file2 = None
194 | self.sections = None
195 | self.checked_sections = []
196 |
197 | self.diff_dialog_ui.textEdit.file_dropped_sig.connect(self.file_dropped_sig_func)
198 | self.diff_dialog_ui.textEdit_2.file_dropped_sig.connect(self.file_dropped_sig_func)
199 |
200 | self.diff_dialog_ui.file1Btn.clicked.connect(lambda: self.select_file("file1"))
201 | self.diff_dialog_ui.file2Btn.clicked.connect(lambda: self.select_file("file2"))
202 |
203 | self.diff_dialog_ui.doDiffBtn.setDisabled(True)
204 | self.diff_dialog_ui.doDiffBtn.clicked.connect(self.do_diff)
205 |
206 | self.binary_compare_worker = None
207 |
208 | self.binary_diff_result_window = QtWidgets.QWidget()
209 | self.binary_diff_result_ui = diff_ui.CompareFilesWindow()
210 | self.binary_diff_result_ui.setupUi(self.binary_diff_result_window)
211 | self.binary_diff_result_ui.stopDiffBtn.clicked.connect(self.stop_diff)
212 |
213 | self.process_diff_result_worker = None
214 | self.process_diff_result_sig_count = 0
215 |
216 | self.diff_result = None
217 |
218 | @pyqtSlot(list)
219 | def file_dropped_sig_func(self, sig: list):
220 | if sig[0] == "file1":
221 | self.file1 = sig[1]
222 | elif sig[0] == "file2":
223 | self.file2 = sig[1]
224 |
225 | if self.file1 and self.file2:
226 | self.diff_dialog_ui.doDiffBtn.setEnabled(True)
227 |
228 | @pyqtSlot(str)
229 | def binary_compare_finished_sig_func(self, sig: str):
230 | if sig == "start":
231 | self.binary_diff_result_window.show()
232 | elif sig == "size":
233 | self.statusBar.showMessage("\tThe sizes of the two files are different", 5000)
234 | elif sig == "identical":
235 | if self.checked_sections and len(self.checked_sections) == 1 and self.checked_sections[0]['name'] != 'All':
236 | self.statusBar.showMessage(f"\t{self.checked_sections[0]['name']} is identical", 5000)
237 | elif self.checked_sections and len(self.checked_sections) > 1:
238 | self.statusBar.showMessage(f"\tSections are identical", 5000)
239 | else:
240 | self.statusBar.showMessage("\tTwo files are identical", 5000)
241 | elif sig == "cannot read":
242 | self.statusBar.showMessage("\tCannot read file contents", 5000)
243 | self.binary_diff_result_window.close()
244 | self.diff_dialog.show()
245 | elif sig == "finished":
246 | self.process_diff_result()
247 |
248 | @pyqtSlot(str)
249 | def process_diff_result_sig_func(self, sig: str):
250 | self.process_diff_result_sig_count += 1
251 | if sig and self.process_diff_result_sig_count == 1:
252 | self.binary_diff_result_ui.file1TextEdit.setText(f"file1:\n{self.file1}")
253 | self.binary_diff_result_ui.file2TextEdit.setText(f"file2:\n{self.file2}")
254 | self.binary_diff_result_ui.addressTextEdit.setText(sig)
255 | self.binary_diff_result_window.show()
256 | else:
257 | self.binary_diff_result_ui.binaryDiffResultView.append(sig)
258 |
259 | @pyqtSlot()
260 | def process_diff_finished_sig_func(self):
261 | self.process_diff_result_worker.quit()
262 | self.diff_result = self.binary_diff_result_ui.binaryDiffResultView.toPlainText()
263 | self.statusBar.showMessage("\tBinary diff is done!", 5000)
264 |
265 | def section_checkbox(self, checkbox_name, state):
266 | if state == Qt.CheckState.Checked.value: # Check
267 | if checkbox_name == 'All':
268 | self.checked_sections.append({
269 | "start": None,
270 | "size": None,
271 | "name": checkbox_name
272 | })
273 | for checkbox in self.diff_dialog_ui.checkboxes:
274 | if checkbox.text() != 'All':
275 | checkbox.setChecked(False)
276 | checkbox.setEnabled(False)
277 | else:
278 | for section in self.sections:
279 | if section['name'] == checkbox_name:
280 | self.checked_sections.append(section)
281 | break
282 | else: # Uncheck
283 | if checkbox_name == 'All':
284 | self.checked_sections.clear()
285 | for checkbox in self.diff_dialog_ui.checkboxes:
286 | if checkbox.text() != 'All':
287 | checkbox.setEnabled(True)
288 | else:
289 | self.checked_sections = [section for section in self.checked_sections
290 | if section['name'] != checkbox_name]
291 |
292 | def select_file(self, file1or2):
293 | file, _ = QFileDialog.getOpenFileNames(self, caption="Select a file to compare", directory="./dump", initialFilter="All Files (*)")
294 | if file1or2 == "file1":
295 | self.file1 = "" if len(file) == 0 else file[0]
296 | if self.file1:
297 | self.diff_dialog_ui.textEdit.setText(self.file1)
298 | elif file1or2 == "file2":
299 | self.file2 = "" if len(file) == 0 else file[0]
300 | if self.file2:
301 | self.diff_dialog_ui.textEdit_2.setText(self.file2)
302 |
303 | if self.file1 and self.file2:
304 | self.diff_dialog_ui.doDiffBtn.setEnabled(True)
305 | while self.diff_dialog_ui.checkboxGridLayout.count():
306 | item = self.diff_dialog_ui.checkboxGridLayout.takeAt(0) # Remove the item at position 0
307 | widget = item.widget() # Get the widget associated with the item
308 | if widget:
309 | widget.deleteLater() # Schedule the widget for deletion
310 |
311 | r2 = r2pipe.open(self.file1)
312 | sections_info = r2.cmdj("iSj")
313 | self.sections = [
314 | {
315 | "start": section["paddr"],
316 | "size": section["size"],
317 | "name": section["name"]
318 | }
319 | for section in sections_info
320 | ]
321 |
322 | section_found_count = 0
323 | max_columns = 4 # Limit the number of checkboxes per row
324 | row, col = 0, 0
325 | for section in self.sections:
326 | if section['name']:
327 | section_found_count += 1
328 | if section_found_count == 1:
329 | label = QtWidgets.QLabel("Sections:")
330 | self.diff_dialog_ui.checkboxGridLayout.addWidget(label, row, col)
331 | row += 1
332 |
333 | # print(f"Name: {section['name']}, Start: {hex(section['start'])}, Size: {section['size']}")
334 | if section_found_count == 1:
335 | section_checkbox = QtWidgets.QCheckBox("All", self.diff_dialog)
336 | section_checkbox.setObjectName(f"checkbox_all")
337 | section_checkbox.setChecked(True)
338 | self.checked_sections.append({
339 | "start": None,
340 | "size": None,
341 | "name": "All"
342 | })
343 | else:
344 | section_checkbox = QtWidgets.QCheckBox(section['name'], self.diff_dialog)
345 | section_checkbox.setObjectName(f"checkbox_{section['name']}")
346 | section_checkbox.setChecked(False)
347 | section_checkbox.setEnabled(False)
348 | section_checkbox.stateChanged.connect(lambda state, cb=section_checkbox:
349 | self.section_checkbox(cb.text(), state))
350 | self.diff_dialog_ui.checkboxes.append(section_checkbox)
351 | self.diff_dialog_ui.checkboxGridLayout.addWidget(section_checkbox, row, col)
352 | col += 1
353 | if col >= max_columns: # Move to the next row if column limit is reached
354 | col = 0
355 | row += 1
356 |
357 | def process_diff_result(self):
358 | formatted_diffs = self.binary_compare_worker.format_diff(16)
359 | self.binary_compare_worker.quit()
360 |
361 | self.process_diff_result_worker = ProcessDiffResultWorker(formatted_diffs)
362 | self.process_diff_result_worker.process_diff_result_signal.connect(self.process_diff_result_sig_func)
363 | self.process_diff_result_worker.process_diff_finished_signal.connect(self.process_diff_finished_sig_func)
364 | self.process_diff_result_worker.start()
365 |
366 | def stop_diff(self):
367 | if self.process_diff_result_worker is not None:
368 | try:
369 | self.process_diff_result_worker.process_diff_result_signal.disconnect(self.process_diff_result_sig_func)
370 | self.process_diff_result_worker.process_diff_finished_signal.disconnect(self.process_diff_finished_sig_func)
371 | except Exception as e:
372 | print(e)
373 | if self.process_diff_result_worker.isRunning():
374 | self.process_diff_result_worker.quit()
375 | self.diff_result = self.binary_diff_result_ui.binaryDiffResultView.toPlainText()
376 | self.statusBar.showMessage("\tBinary diff is done!", 5000)
377 |
378 | def do_diff(self):
379 | if len(self.checked_sections) == 0:
380 | self.statusBar.showMessage("\tChoose sections to compare", 5000)
381 | return
382 | self.diff_dialog.close()
383 | if self.file1 and self.file2:
384 | self.binary_compare_worker = BinaryCompareWorker(self.file1, self.file2, self.checked_sections)
385 | self.binary_compare_worker.binary_compare_finished_sig.connect(self.binary_compare_finished_sig_func)
386 | self.binary_compare_worker.start()
387 | else:
388 | self.statusBar.showMessage("\tChoose two files to compare", 5000)
389 | return
390 |
--------------------------------------------------------------------------------
/diff.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 363
10 | 163
11 |
12 |
13 |
14 | Binary Diff
15 |
16 |
17 | -
18 |
19 |
20 |
21 | 9
22 |
23 |
24 |
25 |
26 |
27 |
28 | true
29 |
30 |
31 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
32 | <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
33 | p, li { white-space: pre-wrap; }
34 | hr { height: 1px; border-width: 0; }
35 | li.unchecked::marker { content: "\2610"; }
36 | li.checked::marker { content: "\2612"; }
37 | </style></head><body style=" font-family:'맑은 고딕'; font-size:9pt; font-weight:400; font-style:normal;">
38 | <p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.AppleSystemUIFont'; font-size:13pt;"><br /></p>
39 | <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont';">File1 </span></p>
40 | <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont';">(Drag & Drop)</span></p></body></html>
41 |
42 |
43 |
44 | -
45 |
46 |
47 |
48 |
49 |
50 | true
51 |
52 |
53 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
54 | <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
55 | p, li { white-space: pre-wrap; }
56 | hr { height: 1px; border-width: 0; }
57 | li.unchecked::marker { content: "\2610"; }
58 | li.checked::marker { content: "\2612"; }
59 | </style></head><body style=" font-family:'맑은 고딕'; font-size:9pt; font-weight:400; font-style:normal;">
60 | <p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'.AppleSystemUIFont'; font-size:13pt;"><br /></p>
61 | <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont';">File2</span></p>
62 | <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'.AppleSystemUIFont';">(Drag & Drop)</span></p></body></html>
63 |
64 |
65 |
66 | -
67 |
68 |
69 | Qt::NoFocus
70 |
71 |
72 | File1
73 |
74 |
75 |
76 | -
77 |
78 |
79 | Qt::NoFocus
80 |
81 |
82 | File2
83 |
84 |
85 |
86 | -
87 |
88 |
89 | Qt::NoFocus
90 |
91 |
92 | Diff
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/diff_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'diff.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.4.0
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 | import platform
8 |
9 | from PyQt6 import QtWidgets, QtCore, QtGui
10 |
11 |
12 | class DroppableTextEdit(QtWidgets.QTextEdit):
13 | file_dropped_sig = QtCore.pyqtSignal(list)
14 |
15 | def __init__(self, parent=None, text_edit_for_file1or2=None):
16 | super().__init__(parent)
17 | self.text_edit_for_file = text_edit_for_file1or2
18 | self.setAcceptDrops(True)
19 |
20 | def dragEnterEvent(self, event):
21 | if event.mimeData().hasUrls():
22 | event.acceptProposedAction()
23 |
24 | def dragMoveEvent(self, event):
25 | if event.mimeData().hasUrls():
26 | event.acceptProposedAction()
27 |
28 | def dropEvent(self, event):
29 | for url in event.mimeData().urls():
30 | if url.isLocalFile():
31 | file_path = url.toLocalFile()
32 | self.clear()
33 | self.setText(file_path)
34 | self.file_dropped_sig.emit([self.text_edit_for_file, file_path])
35 |
36 |
37 | class Ui_DiffDialog(object):
38 | def setupUi(self, Dialog):
39 | Dialog.setObjectName("Dialog")
40 | Dialog.resize(363, 163)
41 | self.gridLayout = QtWidgets.QGridLayout(Dialog)
42 | self.gridLayout.setObjectName("gridLayout")
43 | self.textEdit = DroppableTextEdit(parent=Dialog, text_edit_for_file1or2="file1")
44 | self.textEdit.setReadOnly(True)
45 | self.textEdit.setObjectName("textEdit")
46 | self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1)
47 | self.textEdit_2 = DroppableTextEdit(parent=Dialog, text_edit_for_file1or2="file2")
48 | self.textEdit_2.setReadOnly(True)
49 | self.textEdit_2.setObjectName("textEdit_2")
50 | self.gridLayout.addWidget(self.textEdit_2, 0, 1, 1, 1)
51 | self.file1Btn = QtWidgets.QPushButton(Dialog)
52 | self.file1Btn.setObjectName("file1Btn")
53 | self.file1Btn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
54 | self.gridLayout.addWidget(self.file1Btn, 1, 0, 1, 1)
55 | self.file2Btn = QtWidgets.QPushButton(Dialog)
56 | self.file2Btn.setObjectName("file2Btn")
57 | self.file2Btn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
58 | self.gridLayout.addWidget(self.file2Btn, 1, 1, 1, 1)
59 | self.doDiffBtn = QtWidgets.QPushButton(Dialog)
60 | self.doDiffBtn.setObjectName("doDiffBtn")
61 | self.doDiffBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
62 | self.gridLayout.addWidget(self.doDiffBtn, 2, 0, 1, 2)
63 |
64 | self.checkboxGridLayout = QtWidgets.QGridLayout()
65 | self.gridLayout.addLayout(self.checkboxGridLayout, 4, 0, 1, 2)
66 | self.checkboxes = []
67 |
68 | self.retranslateUi(Dialog)
69 | QtCore.QMetaObject.connectSlotsByName(Dialog)
70 |
71 | def retranslateUi(self, Dialog):
72 | _translate = QtCore.QCoreApplication.translate
73 | Dialog.setWindowTitle(_translate("Dialog", "Binary Diff"))
74 | font_family = ".AppleSystemUIFont" if platform.system() == "Darwin" else "Courier New"
75 | font_size = "13pt" if platform.system() == "Darwin" else "9pt"
76 | self.textEdit.setHtml(_translate("Dialog",
77 | "\n"
78 | "
\n"
84 | "
\n"
85 | "File1
\n"
86 | "(Drag & Drop)
"))
87 | self.textEdit_2.setHtml(_translate("Dialog",
88 | "\n"
89 | "\n"
95 | "
\n"
96 | "File2
\n"
97 | "(Drag & Drop)
"))
98 | self.file1Btn.setText(_translate("Dialog", "File1"))
99 | self.file2Btn.setText(_translate("Dialog", "File2"))
100 | self.doDiffBtn.setText(_translate("Dialog", "Diff"))
101 |
102 |
103 | class CompareFilesWindow(object):
104 | def setupUi(self, Form):
105 | Form.setObjectName("Form")
106 | Form.resize(924, 424)
107 | Form.setMinimumSize(QtCore.QSize(980, 0))
108 | font = QtGui.QFont()
109 | font.setFamily("Courier New")
110 | Form.setFont(font)
111 | self.gridLayout = QtWidgets.QGridLayout(Form)
112 | self.gridLayout.setObjectName("gridLayout")
113 | self.horizontalLayout = QtWidgets.QHBoxLayout()
114 | self.horizontalLayout.setContentsMargins(-1, 0, -1, -1)
115 | self.horizontalLayout.setSpacing(0)
116 | self.horizontalLayout.setObjectName("horizontalLayout")
117 |
118 | self.stopDiffBtn = QtWidgets.QPushButton(Form)
119 | self.stopDiffBtn.setMaximumSize(QtCore.QSize(50, 16777215))
120 | self.stopDiffBtn.setObjectName("stopDiffBtn")
121 | self.gridLayout.addWidget(self.stopDiffBtn, 0, 0, 1, 1)
122 |
123 | self.file1TextEdit = QtWidgets.QTextEdit(Form)
124 | self.file1TextEdit.setMaximumSize(QtCore.QSize(16777215, 50))
125 | self.file1TextEdit.setReadOnly(True)
126 | self.file1TextEdit.setObjectName("file1TextEdit")
127 | self.horizontalLayout.addWidget(self.file1TextEdit)
128 | self.file2TextEdit = QtWidgets.QTextEdit(Form)
129 | self.file2TextEdit.setMaximumSize(QtCore.QSize(16777215, 50))
130 | self.file2TextEdit.setReadOnly(True)
131 | self.file2TextEdit.setObjectName("file2TextEdit")
132 | self.horizontalLayout.addWidget(self.file2TextEdit)
133 | self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 2, 1)
134 |
135 | self.addressTextEdit = QtWidgets.QTextEdit(Form)
136 | self.addressTextEdit.setMaximumSize(QtCore.QSize(16777215, 26))
137 | self.addressTextEdit.setReadOnly(True)
138 | self.addressTextEdit.setObjectName("addressTextEdit")
139 | self.gridLayout.addWidget(self.addressTextEdit, 3, 0, 1, 1)
140 |
141 | self.binaryDiffResultView = QtWidgets.QTextEdit(Form)
142 | self.binaryDiffResultView.setReadOnly(True)
143 | self.binaryDiffResultView.setObjectName("binaryDiffResultView")
144 | self.gridLayout.addWidget(self.binaryDiffResultView, 4, 0, 1, 1)
145 |
146 | self.retranslateUi(Form)
147 | QtCore.QMetaObject.connectSlotsByName(Form)
148 |
149 | def retranslateUi(self, Form):
150 | _translate = QtCore.QCoreApplication.translate
151 | Form.setWindowTitle(_translate("Form", "Binary Diff Result"))
152 | self.stopDiffBtn.setText(_translate("Form", "Stop"))
--------------------------------------------------------------------------------
/disasm.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from PyQt6.QtCore import QObject, Qt, pyqtSlot
4 | from PyQt6.QtWidgets import QWidget
5 | from capstone import *
6 |
7 | import disasm_ui
8 |
9 |
10 | class EscapableWidget(QWidget):
11 | def keyPressEvent(self, event):
12 | if event.key() == Qt.Key.Key_Escape:
13 | self.close()
14 | else:
15 | super().keyPressEvent(event)
16 |
17 |
18 | class DisassembleWorker(QObject):
19 | def __init__(self):
20 | super().__init__()
21 | self.disasm_window = EscapableWidget()
22 | self.disasm_window.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
23 | self.disasm_ui = disasm_ui.Ui_Form()
24 | self.disasm_ui.setupUi(self.disasm_window)
25 |
26 | self.hex_viewer = None
27 |
28 | self.disasm_result = None
29 |
30 | def disassemble(self, arch: str, addr: str, hex_dump_result: str):
31 | hex_dump_result = hex_dump_result.replace('\u2029', '\n')
32 | lines = hex_dump_result.strip().split('\n')
33 |
34 | hex_data = []
35 | for line in lines:
36 | # Calculate hex start and end positions
37 | hex_start = len(line) - 65
38 | hex_end = len(line) - 16
39 |
40 | # Extract hex part
41 | hex_part = line[hex_start:hex_end]
42 |
43 | # Extract two-digit hex numbers from the part
44 | matches = re.findall(r'\b[0-9a-fA-F]{2}\b', hex_part)
45 | hex_data.append(' '.join(matches))
46 |
47 | hex_string = ' '.join(hex_data).split()
48 | disasm_target = b''.join(bytes([int(hex_value, 16)]) for hex_value in hex_string)
49 |
50 | if arch == "arm64":
51 | md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
52 | elif arch == "arm":
53 | md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
54 | disasm_lines = []
55 | for (address, size, mnemonic, op_str) in md.disasm_lite(disasm_target, int(addr, 16)):
56 | # print("0x%x\t%s\t%s" % (address, mnemonic, op_str))
57 | disasm_lines.append("%x \t%s\t%s" % (address, mnemonic, op_str))
58 |
59 | self.disasm_result = '\n'.join(disasm_lines)
60 | self.disasm_ui.disasmBrowser.setText(self.disasm_result)
61 |
62 | @pyqtSlot(str)
63 | def hex_viewer_wheel_sig_func(self, sig: str):
64 | if not re.search(r"0x[0-9a-f]+", sig) or re.search(r"\d+\. 0x[0-9a-f]+, module:", sig):
65 | return
66 |
67 | tc = self.disasm_ui.disasmBrowser.textCursor()
68 | if not re.search(r"[0-9a-f]+", tc.block().text().split('\t')[0]):
69 | return
70 |
71 | # calculate scrollbar position
72 | wheel_gap = int(sig, 16) - int(tc.block().text().split('\t')[0], 16)
73 | if wheel_gap < 0:
74 | return
75 | wheel_count = round(wheel_gap / 64)
76 | gap = (wheel_gap + 16 * wheel_count) * 3
77 | scrollbar = self.disasm_ui.disasmBrowser.verticalScrollBar()
78 | scrollbar.setValue(gap)
79 |
80 | @pyqtSlot(int)
81 | def hex_viewer_scroll_sig_func(self, scroll_signal: int):
82 | # sync the scrollbar position with hexviewer's one approximately
83 | scrollbar = self.disasm_ui.disasmBrowser.verticalScrollBar()
84 | scrollbar.setValue(4 * scroll_signal - 5)
85 |
--------------------------------------------------------------------------------
/disasm.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 524
10 | 294
11 |
12 |
13 |
14 | Disassemble
15 |
16 |
17 | -
18 |
19 |
20 |
21 | Courier New
22 | 13
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/disasm_ui.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtWidgets, QtCore, QtGui
4 |
5 |
6 | class Ui_Form(object):
7 | def setupUi(self, Form):
8 | Form.setObjectName("Form")
9 | Form.resize(550, 300)
10 | self.gridLayout = QtWidgets.QGridLayout(Form)
11 | self.gridLayout.setObjectName("gridLayout")
12 | self.disasmBrowser = QtWidgets.QTextEdit(Form)
13 | self.disasmBrowser.setReadOnly(True)
14 | font = QtGui.QFont()
15 | font.setFamily("Courier New")
16 | fontsize = 13 if platform.system() == 'Darwin' else 10
17 | font.setPointSize(fontsize)
18 | self.disasmBrowser.setFont(font)
19 | self.disasmBrowser.setObjectName("disasmBrowser")
20 | self.gridLayout.addWidget(self.disasmBrowser, 0, 0, 1, 1)
21 |
22 | self.retranslateUi(Form)
23 | QtCore.QMetaObject.connectSlotsByName(Form)
24 |
25 | def retranslateUi(self, Form):
26 | _translate = QtCore.QCoreApplication.translate
27 | Form.setWindowTitle(_translate("Form", "Disassemble"))
--------------------------------------------------------------------------------
/dump/dummyfile:
--------------------------------------------------------------------------------
1 | ...
--------------------------------------------------------------------------------
/dumper.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import os
3 | import re
4 | import shutil
5 | import sys
6 |
7 | from PyQt6 import QtCore
8 | from PyQt6.QtCore import QThread
9 |
10 | STRINGS = True
11 | DIRECTORY = os.getcwd() + "/dump/full_memory_dump"
12 | MAX_SIZE = 20971520
13 | PERMS = 'r--'
14 | mem_access_viol = ""
15 |
16 |
17 | def dump_to_file(agent, base, size, error, directory):
18 | try:
19 | filename = str(base) + '_dump.data'
20 | if inspect.currentframe().f_back.f_code.co_name == "splitter":
21 | dump = agent.read_memory_chunk(base, size)
22 | else:
23 | dump = agent.read_memory(base, size)
24 | f = open(os.path.join(directory, filename), 'wb')
25 | f.write(dump)
26 | f.close()
27 | return error
28 | except Exception as e:
29 | print(f"[dumper] Oops, memory access violation!")
30 | return error
31 |
32 |
33 | # Read bytes that are bigger than the max_size value, split them into chunks and save them to a file
34 | def splitter(agent, base, size, max_size, error, directory):
35 | times = size // max_size
36 | diff = size % max_size
37 |
38 | global cur_base
39 | cur_base = int(base, 0)
40 |
41 | for time in range(times):
42 | dump_to_file(agent, hex(cur_base), max_size, error, directory)
43 | cur_base = cur_base + max_size
44 |
45 | if diff != 0:
46 | dump_to_file(agent, hex(cur_base), diff, error, directory)
47 |
48 |
49 | def printProgress(purpose, sig, times, total, prefix='', suffix='', decimals=2, bar=100):
50 | filled = int(round(bar * times / float(total)))
51 | percents = round(100.00 * (times / float(total)), decimals)
52 | sig.emit([purpose, str(percents)])
53 | bar = '#' * filled + '-' * (bar - filled)
54 | sys.stdout.write('%s [%s] %s%s %s\r' %
55 | (prefix, bar, percents, '%', suffix)),
56 | sys.stdout.flush()
57 | if times == total:
58 | print("\n")
59 |
60 |
61 | # A very basic implementations of Strings
62 | def strings(filename, directory, min=4):
63 | strings_file = os.path.join(directory, "strings.txt")
64 | path = os.path.join(directory, filename)
65 | with open(path, encoding='Latin-1') as infile:
66 | str_list = re.findall("[A-Za-z0-9/-:;.,_$%'!()[]<> #]+", infile.read())
67 | with open(strings_file, "a") as st:
68 | for string in str_list:
69 | if len(string) > min:
70 | st.write(string + "\n")
71 |
72 |
73 | class FullMemoryDumpWorker(QThread):
74 | full_memory_dump_signal = QtCore.pyqtSignal(int)
75 | progress_signal = QtCore.pyqtSignal(list)
76 |
77 | def __init__(self, frida_instrument, statusBar):
78 | super(FullMemoryDumpWorker, self).__init__()
79 | self.frida_instrument = frida_instrument
80 | self.agent = self.frida_instrument.get_agent()
81 | self.statusBar = statusBar
82 |
83 | self.ranges = self.agent.enumerate_ranges(PERMS)
84 | self.platform = self.agent.get_platform()
85 | self.is_palera1n = self.agent.is_palera1n_jb()
86 |
87 | def run(self) -> None:
88 | global mem_access_viol
89 | # Filter out ranges that are not useful before performing the dump on iOS15+
90 | # Also It can reduce the chance of memory access violation
91 | if self.platform == "darwin" and self.is_palera1n:
92 | self.ranges = [range_dict for range_dict in self.ranges if 'file' not in range_dict or all(
93 | substr not in range_dict['file']['path'] for substr in
94 | ["/System", "/MobileSubstrate/", "substitute", "substrate", "/private/preboot/", "/tmp/frida-",
95 | "/usr/share/icu"])]
96 |
97 | i = 0
98 | l = len(self.ranges)
99 |
100 | if not os.path.exists(DIRECTORY):
101 | os.makedirs(DIRECTORY)
102 | else:
103 | shutil.rmtree(DIRECTORY)
104 | os.makedirs(DIRECTORY)
105 |
106 | # Performing the memory dump
107 | for range in self.ranges:
108 | if range["size"] > MAX_SIZE:
109 | mem_access_viol = splitter(self.agent, range["base"], range["size"], MAX_SIZE, mem_access_viol,
110 | DIRECTORY)
111 | continue
112 | mem_access_viol = dump_to_file(
113 | self.agent, range["base"], range["size"], mem_access_viol, DIRECTORY)
114 | i += 1
115 | printProgress("memdump", self.progress_signal, i, l, prefix='Progress:', suffix='Complete', bar=50)
116 |
117 | # Run Strings if selected
118 | if STRINGS:
119 | files = os.listdir(DIRECTORY)
120 | i = 0
121 | l = len(files)
122 | print(f"[dumper] Running strings on all files:")
123 | for f1 in files:
124 | strings(f1, DIRECTORY)
125 | i += 1
126 | printProgress("strdump", self.progress_signal, i, l, prefix='Progress:',
127 | suffix='Complete', bar=50)
128 | print(f"[dumper] Finished!")
129 | self.full_memory_dump_signal.emit(1)
130 |
--------------------------------------------------------------------------------
/enum_ranges.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtCore
4 | from PyQt6.QtCore import QObject, pyqtSlot, Qt, QPoint
5 | from PyQt6.QtWidgets import QWidget, QTableWidgetItem
6 |
7 | import enum_ranges_ui
8 | import gvar
9 |
10 |
11 | class EscapableWidget(QWidget):
12 | @pyqtSlot()
13 | def key_escape_pressed_sig_func(self):
14 | self.close()
15 |
16 |
17 | class EnumRangesViewClass(QObject):
18 | refresh_enum_ranges_signal = QtCore.pyqtSignal(str)
19 | enum_ranges_item_clicked_signal = QtCore.pyqtSignal(list)
20 |
21 | def __init__(self):
22 | super().__init__()
23 | self.enum_ranges_window = EscapableWidget()
24 | self.enum_ranges_ui = enum_ranges_ui.Ui_Form()
25 | self.enum_ranges_ui.setupUi(self.enum_ranges_window)
26 |
27 | self.table_filled = False
28 |
29 | self.enum_ranges_ui.refreshEnumRangesBtn.clicked.connect(self.refresh_enum_ranges)
30 | self.enum_ranges_ui.enumRangesTableWidget.key_escape_pressed_signal.connect(self.enum_ranges_window.key_escape_pressed_sig_func)
31 | self.enum_ranges_ui.enumRangesFilter.textChanged.connect(self.enum_ranges_filter)
32 | self.enum_ranges_ui.enumRangesTableWidget.itemClicked.connect(self.item_clicked)
33 |
34 | self.showEnumRangesBtnClickedCount = 0
35 |
36 | def show_enum_ranges(self):
37 | self.showEnumRangesBtnClickedCount += 1
38 | self.enum_ranges_window.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
39 | self.enum_ranges_window.show()
40 | if self.showEnumRangesBtnClickedCount == 1:
41 | curr_pos = self.enum_ranges_window.pos()
42 | new_pos = (curr_pos + QPoint(350, -150)) if platform.system() == "Darwin" else (
43 | curr_pos + QPoint(360, -160))
44 | self.enum_ranges_window.move(new_pos)
45 | if gvar.enumerate_ranges and not self.table_filled:
46 | self.add_row(gvar.enumerate_ranges)
47 |
48 | def refresh_enum_ranges(self):
49 | if gvar.is_frida_attached and gvar.frida_instrument is not None:
50 | self.refresh_enum_ranges_signal.emit('r--')
51 | self.enum_ranges_ui.enumRangesTableWidget.setRowCount(0)
52 | self.add_row(gvar.enumerate_ranges)
53 |
54 | def add_row(self, ranges):
55 | for i in range(len(ranges)):
56 | row_position = self.enum_ranges_ui.enumRangesTableWidget.rowCount()
57 | self.enum_ranges_ui.enumRangesTableWidget.insertRow(row_position)
58 |
59 | # Base column (non-editable)
60 | base_addr_item = QTableWidgetItem(f"{ranges[i][0]}")
61 | base_addr_item.setFlags(base_addr_item.flags() & ~Qt.ItemFlag.ItemIsEditable) # Make it non-editable
62 | self.enum_ranges_ui.enumRangesTableWidget.setItem(row_position, 0, base_addr_item)
63 |
64 | # End column (non-editable)
65 | end_addr_item = QTableWidgetItem(f"{ranges[i][1]}")
66 | end_addr_item.setFlags(end_addr_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
67 | self.enum_ranges_ui.enumRangesTableWidget.setItem(row_position, 1, end_addr_item)
68 |
69 | # Prot column (non-editable)
70 | prot_item = QTableWidgetItem(f"{ranges[i][2]}")
71 | prot_item.setFlags(prot_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
72 | self.enum_ranges_ui.enumRangesTableWidget.setItem(row_position, 2, prot_item)
73 |
74 | # Prot column (non-editable)
75 | path_column = QTableWidgetItem(f"{ranges[i][4]}")
76 | path_column.setFlags(path_column.flags() & ~Qt.ItemFlag.ItemIsEditable)
77 | self.enum_ranges_ui.enumRangesTableWidget.setItem(row_position, 3, path_column)
78 |
79 | self.table_filled = True
80 |
81 | def enum_ranges_filter(self):
82 | filter_text = self.enum_ranges_ui.enumRangesFilter.toPlainText().lower() # Get filter text (or use `text()` if it's a QLineEdit)
83 | for row in range(self.enum_ranges_ui.enumRangesTableWidget.rowCount()):
84 | row_match = False
85 | for column in range(self.enum_ranges_ui.enumRangesTableWidget.columnCount()):
86 | item = self.enum_ranges_ui.enumRangesTableWidget.item(row, column)
87 | if item and filter_text in item.text().lower():
88 | row_match = True
89 | break # If one column matches, no need to check the rest
90 |
91 | # Hide the row if it doesn't match the filter
92 | self.enum_ranges_ui.enumRangesTableWidget.setRowHidden(row, not row_match)
93 |
94 | def item_clicked(self, item):
95 | self.enum_ranges_item_clicked_signal.emit([item.column(), item.text()])
96 |
--------------------------------------------------------------------------------
/enum_ranges.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 644
10 | 269
11 |
12 |
13 |
14 | Enum Ranges
15 |
16 |
17 | -
18 |
19 |
20 |
21 | Courier New
22 |
23 |
24 |
25 | true
26 |
27 |
28 |
29 | Base
30 |
31 |
32 |
33 |
34 | End
35 |
36 |
37 |
38 |
39 | Protection
40 |
41 |
42 |
43 |
44 | Path
45 |
46 |
47 |
48 |
49 | -
50 |
51 |
52 |
53 | 0
54 | 0
55 |
56 |
57 |
58 |
59 | 16777215
60 | 26
61 |
62 |
63 |
64 |
65 | -
66 |
67 |
68 |
69 | 0
70 | 0
71 |
72 |
73 |
74 |
75 | 0
76 | 0
77 |
78 |
79 |
80 |
81 | 200
82 | 16777215
83 |
84 |
85 |
86 |
87 | Courier New
88 |
89 |
90 |
91 | Refresh Enum Ranges
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/enum_ranges_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'enum_ranges.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.4.0
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 |
8 | from PyQt6 import QtCore, QtWidgets, QtGui
9 | from PyQt6.QtCore import Qt
10 | from PyQt6.QtGui import QShortcut, QKeySequence
11 | from PyQt6.QtWidgets import QTableWidget, QApplication
12 |
13 |
14 | class EnumRangesTableWidget(QTableWidget):
15 | key_escape_pressed_signal = QtCore.pyqtSignal()
16 |
17 | def __init__(self, args):
18 | super(EnumRangesTableWidget, self).__init__(args)
19 |
20 | # Set up the shortcut for Cmd+C or Ctrl+C
21 | copy_shortcut = QShortcut(QKeySequence("Ctrl+C"), self)
22 | copy_shortcut.activated.connect(self.copy_selected_items)
23 |
24 | def keyPressEvent(self, e: QtGui.QKeyEvent) -> None:
25 | if e.key() == Qt.Key.Key_Escape:
26 | self.key_escape_pressed_signal.emit()
27 |
28 | def copy_selected_items(self):
29 | # Get selected items
30 | selected_items = self.selectedItems()
31 |
32 | # Group items by row for copying in table format
33 | rows = {}
34 | for item in selected_items:
35 | row = item.row()
36 | if row not in rows:
37 | rows[row] = []
38 | rows[row].append(item.text())
39 |
40 | # Sort rows by their keys (row number) and create formatted text
41 | copied_text = "\n".join(
42 | "\t".join(rows[row]) for row in sorted(rows.keys())
43 | )
44 |
45 | # Copy to clipboard
46 | clipboard = QApplication.clipboard()
47 | clipboard.setText(copied_text)
48 |
49 |
50 | class Ui_Form(object):
51 | def setupUi(self, Form):
52 | Form.setObjectName("Form")
53 | Form.resize(644, 269)
54 | self.gridLayout = QtWidgets.QGridLayout(Form)
55 | self.gridLayout.setObjectName("gridLayout")
56 | # self.enumRangesTableWidget = QtWidgets.QTableWidget(Form)
57 | self.enumRangesTableWidget = EnumRangesTableWidget(Form)
58 | self.enumRangesTableWidget.setObjectName("enumRangesTableWidget")
59 | self.enumRangesTableWidget.setColumnCount(4)
60 | self.enumRangesTableWidget.setRowCount(0)
61 | item = QtWidgets.QTableWidgetItem()
62 | self.enumRangesTableWidget.setHorizontalHeaderItem(0, item)
63 | item = QtWidgets.QTableWidgetItem()
64 | self.enumRangesTableWidget.setHorizontalHeaderItem(1, item)
65 | item = QtWidgets.QTableWidgetItem()
66 | self.enumRangesTableWidget.setHorizontalHeaderItem(2, item)
67 | item = QtWidgets.QTableWidgetItem()
68 | self.enumRangesTableWidget.setHorizontalHeaderItem(3, item)
69 | self.enumRangesTableWidget.horizontalHeader().setVisible(True)
70 | self.enumRangesTableWidget.setColumnWidth(3, 300)
71 | self.gridLayout.addWidget(self.enumRangesTableWidget, 1, 0, 1, 1)
72 | self.enumRangesFilter = QtWidgets.QTextEdit(Form)
73 | self.enumRangesFilter.setMinimumSize(QtCore.QSize(0, 0))
74 | self.enumRangesFilter.setMaximumSize(QtCore.QSize(16777215, 27))
75 | self.enumRangesFilter.setObjectName("enumRangesFilter")
76 | self.gridLayout.addWidget(self.enumRangesFilter, 2, 0, 1, 1)
77 | self.refreshEnumRangesBtn = QtWidgets.QPushButton(Form)
78 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
79 | sizePolicy.setHorizontalStretch(0)
80 | sizePolicy.setVerticalStretch(0)
81 | sizePolicy.setHeightForWidth(self.refreshEnumRangesBtn.sizePolicy().hasHeightForWidth())
82 | self.refreshEnumRangesBtn.setSizePolicy(sizePolicy)
83 | self.refreshEnumRangesBtn.setMinimumSize(QtCore.QSize(0, 0))
84 | self.refreshEnumRangesBtn.setMaximumSize(QtCore.QSize(200, 16777215))
85 | self.refreshEnumRangesBtn.setObjectName("refreshEnumRangesBtn")
86 | self.gridLayout.addWidget(self.refreshEnumRangesBtn, 0, 0, 1, 1)
87 |
88 | self.retranslateUi(Form)
89 | QtCore.QMetaObject.connectSlotsByName(Form)
90 |
91 | def retranslateUi(self, Form):
92 | _translate = QtCore.QCoreApplication.translate
93 | Form.setWindowTitle(_translate("Form", "Enum Ranges"))
94 | item = self.enumRangesTableWidget.horizontalHeaderItem(0)
95 | item.setText(_translate("Form", "Base"))
96 | item = self.enumRangesTableWidget.horizontalHeaderItem(1)
97 | item.setText(_translate("Form", "End"))
98 | item = self.enumRangesTableWidget.horizontalHeaderItem(2)
99 | item.setText(_translate("Form", "Protection"))
100 | item = self.enumRangesTableWidget.horizontalHeaderItem(3)
101 | item.setText(_translate("Form", "Path"))
102 | self.refreshEnumRangesBtn.setText(_translate("Form", "Refresh Enum Ranges"))
103 |
--------------------------------------------------------------------------------
/frida_portal.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import frida
3 | from PyQt6 import QtCore
4 | from PyQt6.QtCore import QThread
5 | from frida_tools.application import Reactor
6 |
7 | import gvar
8 |
9 | ENABLE_CONTROL_INTERFACE = True
10 |
11 |
12 | class FridaPortalClassWorker(QThread):
13 | node_joined_signal = QtCore.pyqtSignal(list)
14 |
15 | def __init__(self):
16 | super().__init__()
17 | self._reactor = Reactor(run_until_return=self._process_input)
18 |
19 | cluster_params = frida.EndpointParameters(address="0.0.0.0",
20 | port=gvar.frida_portal_cluster_port)
21 |
22 | if ENABLE_CONTROL_INTERFACE:
23 | control_params = frida.EndpointParameters(address="::1",
24 | port=gvar.frida_portal_controller_port)
25 | else:
26 | control_params = None
27 |
28 | service = frida.PortalService(cluster_params, control_params)
29 | self._service = service
30 | self._device = service.device
31 |
32 | service.on('node-connected', lambda *args: self._reactor.schedule(lambda: self._on_node_connected(*args)))
33 | service.on('node-joined', lambda *args: self._reactor.schedule(lambda: self._on_node_joined(*args)))
34 | service.on('node-left', lambda *args: self._reactor.schedule(lambda: self._on_node_left(*args)))
35 | service.on('node-disconnected', lambda *args: self._reactor.schedule(lambda: self._on_node_disconnected(*args)))
36 | service.on('controller-connected', lambda *args: self._reactor.schedule(lambda: self._on_controller_connected(*args)))
37 | service.on('controller-disconnected', lambda *args: self._reactor.schedule(lambda: self._on_controller_disconnected(*args)))
38 |
39 | def run(self):
40 | self._reactor.schedule(self._start)
41 | self._reactor.run()
42 |
43 | def process_stop(self):
44 | self._stop()
45 |
46 | def _start(self):
47 | self._service.start()
48 | self._device.enable_spawn_gating()
49 |
50 | def _stop(self):
51 | self._service.stop()
52 |
53 | def _process_input(self, reactor):
54 | while True:
55 | try:
56 | QThread.msleep(100)
57 | continue
58 | except KeyboardInterrupt:
59 | self._reactor.cancel_io()
60 | return
61 |
62 | def _on_node_connected(self, connection_id, remote_address):
63 | print("on_node_connected()", connection_id, remote_address)
64 |
65 | def _on_node_joined(self, connection_id, application):
66 | print("on_node_joined()", connection_id, application)
67 | self.node_joined_signal.emit([application.name, application.pid])
68 | self._device.resume(application.pid)
69 | gvar.frida_portal_mode = True
70 |
71 | def _on_node_left(self, connection_id, application):
72 | print("on_node_left()", connection_id, application)
73 | gvar.frida_portal_mode = False
74 |
75 | def _on_node_disconnected(self, connection_id, remote_address):
76 | print("on_node_disconnected()", connection_id, remote_address)
77 |
78 | def _on_controller_connected(self, connection_id, remote_address):
79 | print("on_controller_connected()", connection_id, remote_address)
80 |
81 | def _on_controller_disconnected(self, connection_id, remote_address):
82 | print("on_controller_disconnected()", connection_id, remote_address)
83 |
84 |
--------------------------------------------------------------------------------
/gadget.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import shutil
4 | import socket
5 | import subprocess
6 | import warnings
7 | import zipfile
8 |
9 | import frida
10 | from PyQt6 import QtCore, QtWidgets
11 | from PyQt6.QtCore import Qt, QEvent, pyqtSlot, QThread
12 | from PyQt6.QtWidgets import QApplication
13 |
14 | import frida_portal
15 | import gadget_ui
16 | import gvar
17 |
18 |
19 | def unzip(target_zip: str, target_file: str) -> None:
20 | # Open the zip file using 'with' statement
21 | with zipfile.ZipFile(target_zip, 'r') as zip_ref:
22 | # Check if the file exists in the zip archive
23 | for file in zip_ref.namelist():
24 | if target_file in file:
25 | # Extract the specific file to the current working directory
26 | zip_ref.extract(file)
27 |
28 |
29 | def add_file_to_zip(target_zip: str, file_to_insert: str, target_dir: str):
30 | # Open the existing zip file in append mode
31 | with zipfile.ZipFile(target_zip, 'a') as zip_ref:
32 | arcname = os.path.join(target_dir, os.path.basename(file_to_insert))
33 | with warnings.catch_warnings():
34 | warnings.simplefilter('ignore')
35 | zip_ref.write(file_to_insert, arcname=arcname)
36 | print(f"[gadget] [*] {file_to_insert} added into {target_zip}")
37 |
38 |
39 | def get_local_ip():
40 | try:
41 | # Create a socket to connect to an Internet host
42 | with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
43 | # Connect the socket to a remote server on the internet
44 | s.connect(("8.8.8.8", 80)) # Google's DNS
45 | # Get the socket's own address
46 | local_ip = s.getsockname()[0]
47 | return local_ip
48 | except Exception as e:
49 | print(f"[gadget] Error obtaining local IP: {e}")
50 | return None
51 |
52 |
53 | def command_exists_on_device(device_command):
54 | try:
55 | # Construct the full adb command
56 | full_command = f"adb shell su -c \"{device_command}\""
57 |
58 | # Run the command and capture the output
59 | result = subprocess.run(full_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
60 |
61 | # Determine if the command exists based on the output or error
62 | # Adjust this logic based on what you observe in the output
63 | if "not found" not in result.stderr:
64 | return True
65 | else:
66 | return False
67 | except subprocess.CalledProcessError:
68 | # The command failed to execute
69 | return False
70 |
71 |
72 | class GadgetDialogClass(QtWidgets.QDialog):
73 | frida_portal_node_info_signal = QtCore.pyqtSignal(list)
74 |
75 | def __init__(self):
76 | super(GadgetDialogClass, self).__init__()
77 | self.gadget_dialog = QtWidgets.QDialog()
78 | self.gadget_dialog.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
79 | # self.spawn_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
80 | self.gadget_ui = gadget_ui.Ui_prepareGadgetDialogUi()
81 | self.gadget_ui.setupUi(self.gadget_dialog)
82 | self.gadget_ui.sleepTimeInput.returnPressed.connect(self.sleep_time_input_return_pressed)
83 | self.gadget_ui.prepareGadgetBtn.clicked.connect(lambda: self.prepare_gadget("clicked", None, None))
84 | self.gadget_ui.fridaPortalModeCheckBox.stateChanged.connect(self.frida_portal_checkbox)
85 | self.is_frida_portal_mode_checked = False
86 | self.frida_portal_worker = None
87 |
88 | self.interested_widgets = []
89 | QApplication.instance().installEventFilter(self)
90 |
91 | @pyqtSlot(list)
92 | def frida_portal_node_joined_sig(self, sig: list):
93 | if sig:
94 | self.frida_portal_node_info_signal.emit(sig)
95 |
96 | def run_frida_portal(self):
97 | # run frida-portal
98 | self.frida_portal_worker = frida_portal.FridaPortalClassWorker()
99 | self.frida_portal_worker.node_joined_signal.connect(self.frida_portal_node_joined_sig)
100 | self.frida_portal_worker.start()
101 |
102 | def stop_frida_portal(self):
103 | if self.frida_portal_worker is not None:
104 | try:
105 | self.frida_portal_worker.process_stop()
106 | self.frida_portal_worker.quit()
107 | self.frida_portal_worker = None
108 | frida.get_device_manager().remove_remote_device('localhost')
109 | QThread.msleep(500)
110 | except Exception as e:
111 | print(e)
112 |
113 | def frida_portal_checkbox(self, state):
114 | self.is_frida_portal_mode_checked = state == Qt.CheckState.Checked.value
115 | if self.is_frida_portal_mode_checked:
116 | self.stop_frida_portal()
117 | self.run_frida_portal()
118 | self.gadget_ui.fridaPortalListeningLabel.setText(f"Listening on {get_local_ip()}:{gvar.frida_portal_cluster_port}")
119 | else:
120 | self.stop_frida_portal()
121 | self.gadget_ui.fridaPortalListeningLabel.setText("")
122 | return
123 |
124 | def sleep_time_input_return_pressed(self):
125 | if (pkg := self.gadget_ui.pkgNameInput.text()) and (delay := self.gadget_ui.sleepTimeInput.text()):
126 | self.prepare_gadget("returnPressed", pkg, delay)
127 | else:
128 | return
129 |
130 | def prepare_gadget(self, caller, pkg, delay):
131 | if caller == "clicked":
132 | if not (pkg := self.gadget_ui.pkgNameInput.text()) or not (delay := self.gadget_ui.sleepTimeInput.text()):
133 | return
134 |
135 | gadget_dir = "gadget"
136 | zygisk_gadget_name = "zygisk-gadget-v1.2.1-release.zip"
137 | zygisk_gadget_path = f"{gadget_dir}/{zygisk_gadget_name}"
138 | shutil.copy2(zygisk_gadget_path, f"{gadget_dir}/temp.zip")
139 | temp_zip_name = "temp.zip"
140 | temp_zip_path = f"{gadget_dir}/{temp_zip_name}"
141 | config_name = "config"
142 |
143 | json_data = {
144 | "package": {
145 | "name": pkg,
146 | "delay": int(delay),
147 | "mode": {
148 | "config": True
149 | }
150 | }
151 | }
152 |
153 | with open(f"{gadget_dir}/{config_name}", "w") as f:
154 | json.dump(json_data, f, indent=4)
155 |
156 | unzip(temp_zip_path, config_name)
157 | add_file_to_zip(temp_zip_path, f"{gadget_dir}/{config_name}", "")
158 | os.remove(config_name)
159 | os.remove(f"{gadget_dir}/{config_name}")
160 |
161 | if self.is_frida_portal_mode_checked:
162 | frida_config_name = "ajeossida-gadget.config"
163 | local_ip = get_local_ip()
164 | content = f'''{{\n "interaction": {{\n\t "type": "connect",\n\t "address": "{local_ip}",\n\t "port": {gvar.frida_portal_cluster_port}\n }}\n}}'''
165 | with open(f"{gadget_dir}/{frida_config_name}", "w") as f:
166 | f.write(content)
167 | f.close()
168 |
169 | add_file_to_zip(temp_zip_path, f"{gadget_dir}/{frida_config_name}", "")
170 | os.remove(f"{gadget_dir}/{frida_config_name}")
171 |
172 | # install zygisk-gadget
173 | os.system(f"adb push {temp_zip_path} /data/local/tmp/")
174 | os.remove(temp_zip_path)
175 | if command_exists_on_device("ksud"):
176 | os.system(f"adb shell su -c \"ksud module install /data/local/tmp/{temp_zip_name}\"")
177 | else:
178 | os.system(f"adb shell su -c \"magisk --install-module /data/local/tmp/{temp_zip_name}\"")
179 | os.system(f"adb shell su -c \"rm -rf /data/local/tmp/{temp_zip_name}\"")
180 | os.system("adb reboot")
181 |
182 | def eventFilter(self, obj, event):
183 | self.interested_widgets = [self.gadget_ui.pkgNameInput]
184 | if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab:
185 | try:
186 | if self.gadget_ui.pkgNameInput.isEnabled():
187 | self.interested_widgets.append(self.gadget_ui.sleepTimeInput)
188 | index = self.interested_widgets.index(self.gadget_dialog.focusWidget())
189 |
190 | self.interested_widgets[(index + 1) % len(self.interested_widgets)].setFocus()
191 | except ValueError:
192 | self.interested_widgets[0].setFocus()
193 |
194 | return True
195 |
196 | return super().eventFilter(obj, event)
197 |
--------------------------------------------------------------------------------
/gadget.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | prepareGadgetDialogUi
4 |
5 |
6 |
7 | 0
8 | 0
9 | 672
10 | 433
11 |
12 |
13 |
14 |
15 | Courier New
16 |
17 |
18 |
19 | Prepare Gadget
20 |
21 |
22 | -
23 |
24 |
25 | true
26 |
27 |
28 | Qt::StrongFocus
29 |
30 |
31 |
32 |
33 |
34 | true
35 |
36 |
37 | com.android.chrome
38 |
39 |
40 |
41 | -
42 |
43 |
44 | frida-portal mode
45 |
46 |
47 |
48 | -
49 |
50 |
51 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
52 | <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
53 | p, li { white-space: pre-wrap; }
54 | hr { height: 1px; border-width: 0; }
55 | li.unchecked::marker { content: "\2610"; }
56 | li.checked::marker { content: "\2612"; }
57 | </style></head><body style=" font-family:'Courier New'; font-size:13pt; font-weight:400; font-style:normal;">
58 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Introdunction</span></p>
59 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Prepare the zygisk module to load frida-gadget upon the application's startup.</p>
60 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-weight:700;"><br /></p>
61 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Prerequisites</span></p>
62 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1. Zygisk enabled.</p>
63 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2. ADB enabled.</p>
64 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3. Turn off frida-server on your device, if it's on.</p>
65 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
66 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Usage</span></p>
67 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1. Enter the package name (e.g., com.android.chrome).</p>
68 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2. Enter the sleep time (in milliseconds) before loading the frida-gadget.</p>
69 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3. Click the "Prepare" button. Your device will reboot.</p>
70 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">4. After rebooting, launch the target app.</p>
71 | <p style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5. Attach to the target app.</p>
72 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] frida-portal mode</span></p>
73 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">In this mode, the gadget will attempt to attach to your computer. </p>
74 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Ensure that your device and computer are on the same local network.</p>
75 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-weight:700;"><br /></p>
76 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Remove</span></p>
77 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Remove the ZygiskGadget module in Magisk Manager.</p></body></html>
78 |
79 |
80 |
81 | -
82 |
83 |
84 | Qt::StrongFocus
85 |
86 |
87 |
88 |
89 |
90 | sleep time(ex. 500000)
91 |
92 |
93 |
94 | -
95 |
96 |
97 | Qt::NoFocus
98 |
99 |
100 | Prepare
101 |
102 |
103 |
104 | -
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/gadget/zygisk-gadget-v1.2.1-release.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackcatml/mlviewer/4fb57f6d337101a6badb080b5260ecb781cd72e3/gadget/zygisk-gadget-v1.2.1-release.zip
--------------------------------------------------------------------------------
/gadget_ui.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtCore, QtGui, QtWidgets
4 |
5 |
6 | class Ui_prepareGadgetDialogUi(object):
7 | def setupUi(self, prepareGadgetDialogUi):
8 | prepareGadgetDialogUi.setObjectName("prepareGadgetDialogUi")
9 | prepareGadgetDialogUi.resize(600, 350) if platform.system() == 'Windows' else prepareGadgetDialogUi.resize(690, 350)
10 | font = QtGui.QFont()
11 | font.setFamily("Courier New")
12 | prepareGadgetDialogUi.setFont(font)
13 | self.gridLayout = QtWidgets.QGridLayout(prepareGadgetDialogUi)
14 | self.gridLayout.setObjectName("gridLayout")
15 | self.prepareGadgetBrowser = QtWidgets.QTextBrowser(prepareGadgetDialogUi)
16 | self.prepareGadgetBrowser.setObjectName("prepareGadgetBrowser")
17 | self.gridLayout.addWidget(self.prepareGadgetBrowser, 0, 0, 1, 3)
18 | self.fridaPortalModeCheckBox = QtWidgets.QCheckBox(prepareGadgetDialogUi)
19 | self.fridaPortalModeCheckBox.setObjectName("fridaPortalModeCheckBox")
20 | self.gridLayout.addWidget(self.fridaPortalModeCheckBox, 1, 0, 1, 1)
21 | self.pkgNameInput = QtWidgets.QLineEdit(prepareGadgetDialogUi)
22 | self.pkgNameInput.setEnabled(True)
23 | self.pkgNameInput.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
24 | self.pkgNameInput.setText("")
25 | self.pkgNameInput.setFrame(True)
26 | self.pkgNameInput.setObjectName("pkgNameInput")
27 | self.gridLayout.addWidget(self.pkgNameInput, 2, 0, 1, 1)
28 | self.sleepTimeInput = QtWidgets.QLineEdit(prepareGadgetDialogUi)
29 | self.sleepTimeInput.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
30 | self.sleepTimeInput.setText("")
31 | self.sleepTimeInput.setObjectName("sleepTimeInput")
32 | self.gridLayout.addWidget(self.sleepTimeInput, 2, 1, 1, 1)
33 | self.prepareGadgetBtn = QtWidgets.QPushButton(prepareGadgetDialogUi)
34 | self.prepareGadgetBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
35 | self.prepareGadgetBtn.setObjectName("prepareGadgetBtn")
36 | self.gridLayout.addWidget(self.prepareGadgetBtn, 2, 2, 1, 1)
37 | self.fridaPortalListeningLabel = QtWidgets.QLabel(prepareGadgetDialogUi)
38 | self.fridaPortalListeningLabel.setObjectName("fridaPortalListeningLabel")
39 | self.gridLayout.addWidget(self.fridaPortalListeningLabel, 1, 1, 1, 1)
40 |
41 | self.retranslateUi(prepareGadgetDialogUi)
42 | QtCore.QMetaObject.connectSlotsByName(prepareGadgetDialogUi)
43 |
44 | def retranslateUi(self, prepareGadgetDialogUi):
45 | _translate = QtCore.QCoreApplication.translate
46 | if platform.system() == 'Darwin':
47 | prepareGadgetDialogUi.setWindowTitle(_translate("prepareGadgetDialogUi", "Prepare Gadget"))
48 | self.prepareGadgetBrowser.setHtml(_translate("prepareGadgetDialogUi",
49 | "\n"
50 | "\n"
56 | "[*] Introdunction
\n"
57 | "Prepare the zygisk module to load frida-gadget upon the application\'s startup.
\n"
58 | "
\n"
59 | "[*] Prerequisites
\n"
60 | "1. Zygisk enabled.
\n"
61 | "2. ADB enabled.
\n"
62 | "3. Turn off frida-server on your device, if it\'s on.
\n"
63 | "
\n"
64 | "[*] Usage
\n"
65 | "1. Enter the package name (e.g., com.android.chrome).
\n"
66 | "2. Enter the sleep time (in microseconds) to delay the loading of the frida-gadget.
\n"
67 | "3. Click the "Prepare" button. Your device will reboot.
\n"
68 | "4. After rebooting, launch the target app.
\n"
69 | "5. Attach to the target app.
\n"
70 | "[*] frida-portal mode
\n"
71 | "In this mode, the gadget will attempt to attach to your computer.
\n"
72 | "Ensure that your device and computer are on the same local network.
\n"
73 | "
\n"
74 | "[*] Remove
\n"
75 | "1. Remove the ZygiskGadget module in Magisk Manager.
\n"
76 | ""))
77 | elif platform.system() == 'Windows':
78 | prepareGadgetDialogUi.setWindowTitle(_translate("prepareGadgetDialogUi", "Prepare Gadget"))
79 | self.prepareGadgetBrowser.setHtml(_translate("prepareGadgetDialogUi",
80 | "\n"
81 | "\n"
87 | "[*] Introdunction
\n"
88 | "Prepare the zygisk module to load frida-gadget upon the application\'s startup.
\n"
89 | "
\n"
90 | "[*] Prerequisites
\n"
91 | "1. Zygisk enabled.
\n"
92 | "2. ADB enabled.
\n"
93 | "3. Turn off frida-server on your device, if it\'s on.
\n"
94 | "
\n"
95 | "[*] Usage
\n"
96 | "1. Enter the package name (e.g., com.android.chrome).
\n"
97 | "2. Enter the sleep time (in microseconds) to delay the loading of the frida-gadget.
\n"
98 | "3. Click the "Prepare" button. Your device will reboot.
\n"
99 | "4. After rebooting, launch the target app.
\n"
100 | "5. Attach to the target app.
\n"
101 | "[*] frida-portal mode
\n"
102 | "In this mode, the gadget will attempt to attach to your computer.
\n"
103 | "Ensure that your device and computer are on the same local network.
\n"
104 | "
\n"
105 | "[*] Remove
\n"
106 | "Remove the ZygiskGadget module in Magisk Manager.
"))
107 | self.pkgNameInput.setPlaceholderText(_translate("prepareGadgetDialogUi", "com.android.chrome"))
108 | self.fridaPortalModeCheckBox.setText(_translate("prepareGadgetDialogUi", "frida-portal mode"))
109 | self.prepareGadgetBtn.setText(_translate("prepareGadgetDialogUi", "Prepare"))
110 | self.sleepTimeInput.setPlaceholderText(_translate("prepareGadgetDialogUi", "sleep time(e.g., 500000)"))
111 | self.pkgNameInput.setPlaceholderText(_translate("prepareGadgetDialogUi", "com.android.chrome"))
112 | self.prepareGadgetBtn.setText(_translate("prepareGadgetDialogUi", "Prepare"))
113 | self.fridaPortalListeningLabel.setText(_translate("prepareGadgetDialogUi", ""))
114 |
--------------------------------------------------------------------------------
/gadget_win.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | prepareGadgetDialogUi
4 |
5 |
6 |
7 | 0
8 | 0
9 | 592
10 | 338
11 |
12 |
13 |
14 |
15 | Courier New
16 |
17 |
18 |
19 | Prepare Gadget
20 |
21 |
22 | -
23 |
24 |
25 |
26 | Courier New
27 | 9
28 |
29 |
30 |
31 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
32 | <html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
33 | p, li { white-space: pre-wrap; }
34 | hr { height: 1px; border-width: 0; }
35 | li.unchecked::marker { content: "\2610"; }
36 | li.checked::marker { content: "\2612"; }
37 | </style></head><body style=" font-family:'Courier New'; font-size:9pt; font-weight:400; font-style:normal;">
38 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Introdunction</span></p>
39 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Prepare the zygisk module to load frida-gadget upon the application's startup.</p>
40 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-weight:700;"><br /></p>
41 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Prerequisites</span></p>
42 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1. Zygisk enabled.</p>
43 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2. ADB enabled.</p>
44 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3. Turn off frida-server on your device, if it's on.</p>
45 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
46 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Usage</span></p>
47 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1. Enter the package name (e.g., com.android.chrome).</p>
48 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2. Enter the sleep time (in microseconds) to delay the loading of the frida-gadget.</p>
49 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3. Click the "Prepare" button. Your device will reboot.</p>
50 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">4. After rebooting, launch the target app.</p>
51 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5. Attach to the target app.</p>
52 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
53 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] frida-portal mode</span></p>
54 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">In this mode, the gadget will attempt to attach to your computer. </p>
55 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Ensure that your device and computer are on the same local network.</p>
56 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
57 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700;">[*] Remove</span></p>
58 | <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Remove the ZygiskGadget module in Magisk Manager.</p></body></html>
59 |
60 |
61 |
62 | -
63 |
64 |
65 | true
66 |
67 |
68 | Qt::StrongFocus
69 |
70 |
71 |
72 |
73 |
74 | true
75 |
76 |
77 | com.android.chrome
78 |
79 |
80 |
81 | -
82 |
83 |
84 | Qt::NoFocus
85 |
86 |
87 | Prepare
88 |
89 |
90 |
91 | -
92 |
93 |
94 | Qt::StrongFocus
95 |
96 |
97 |
98 |
99 |
100 | sleep time(e.g., 500000)
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/gvar.py:
--------------------------------------------------------------------------------
1 | ERROR_SCRIPT_DESTROYED = "script has been destroyed"
2 | READ_MEM_SIZE = 4096
3 |
4 | frida_instrument = None
5 | enumerate_ranges = []
6 | is_frida_attached = False
7 | remote = False
8 | list_modules = []
9 | arch = None
10 |
11 | is_hex_edit_mode = False
12 | hex_edited = []
13 |
14 | current_frame_block_number = 0
15 | current_frame_start_address = ''
16 |
17 | current_mem_scan_hex_view_result = ''
18 |
19 | scan_progress_ratio = 0
20 | scan_matches = []
21 | scanned_value = None
22 |
23 | dump_module_name = ''
24 |
25 | visited_address = []
26 |
27 | frida_portal_mode = False
28 | frida_portal_cluster_port = 27052
29 | frida_portal_controller_port = 27042
30 |
31 | hex_viewer_signal_manager = None
32 |
33 | enum_threads = None
34 |
--------------------------------------------------------------------------------
/history.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtCore
4 | from PyQt6.QtCore import Qt, QObject, pyqtSlot, QPoint
5 | from PyQt6.QtWidgets import QTableWidgetItem, QWidget
6 |
7 | import history_ui
8 |
9 |
10 | class EscapableWidget(QWidget):
11 | @pyqtSlot()
12 | def key_escape_pressed_sig_func(self):
13 | self.close()
14 |
15 |
16 | class HistoryViewClass(QObject):
17 | history_addr_signal = QtCore.pyqtSignal(str)
18 |
19 | def __init__(self):
20 | super().__init__()
21 | self.history_window = EscapableWidget()
22 | self.history_ui = history_ui.Ui_Form()
23 | self.history_ui.setupUi(self.history_window)
24 |
25 | self.history_ui.historyTableWidget.key_escape_pressed_signal.connect(self.history_window.key_escape_pressed_sig_func)
26 | self.history_ui.historyTableWidget.history_remove_row_signal.connect(self.history_remove_row_sig_func)
27 | self.history_ui.historyTableWidget.itemClicked.connect(self.addr_clicked)
28 |
29 | self.historyBtnClickedCount = 0
30 |
31 | def show_history(self):
32 | self.historyBtnClickedCount += 1
33 | self.history_window.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
34 | self.history_window.show()
35 | if self.historyBtnClickedCount == 1:
36 | curr_pos = self.history_window.pos()
37 | new_pos = (curr_pos + QPoint(480, -350)) if platform.system() == "Darwin" else (
38 | curr_pos + QPoint(490, -360))
39 | self.history_window.move(new_pos)
40 |
41 | def add_row(self, addr):
42 | for row in range(self.history_ui.historyTableWidget.rowCount()):
43 | item = self.history_ui.historyTableWidget.item(row, 0)
44 | if item is not None and item.text() == addr:
45 | return
46 |
47 | row_position = self.history_ui.historyTableWidget.rowCount()
48 | self.history_ui.historyTableWidget.insertRow(row_position)
49 |
50 | # Address column (non-editable)
51 | address_item = QTableWidgetItem(f"{addr}")
52 | address_item.setFlags(address_item.flags() & ~Qt.ItemFlag.ItemIsEditable) # Make it non-editable
53 | self.history_ui.historyTableWidget.setItem(row_position, 0, address_item)
54 |
55 | # Description column (editable)
56 | description_item = QTableWidgetItem("Description")
57 | self.history_ui.historyTableWidget.setItem(row_position, 1, description_item)
58 |
59 | # Stat column (non-editable)
60 | stat_item = QTableWidgetItem("")
61 | self.history_ui.historyTableWidget.setItem(row_position, 2, stat_item)
62 |
63 | def addr_clicked(self, item):
64 | if item.column() == 0:
65 | self.history_addr_signal.emit(item.text())
66 |
67 | def clear_table(self):
68 | self.history_ui.historyTableWidget.clearContents()
69 | while self.history_ui.historyTableWidget.rowCount() > 0:
70 | self.history_ui.historyTableWidget.removeRow(0)
71 |
72 | @pyqtSlot(str)
73 | def history_remove_row_sig_func(self, sig: str):
74 | selected_items = self.history_ui.historyTableWidget.selectedItems()
75 | if selected_items:
76 | selected_row = selected_items[0].row()
77 | self.history_ui.historyTableWidget.removeRow(selected_row)
78 |
79 | @pyqtSlot(str)
80 | def add_address_to_history_sig_func(self, sig: str):
81 | self.add_row(sig)
82 |
--------------------------------------------------------------------------------
/history.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 428
10 | 174
11 |
12 |
13 |
14 |
15 | Courier New
16 |
17 |
18 |
19 | History
20 |
21 |
22 | -
23 |
24 |
25 |
26 | Courier New
27 |
28 |
29 |
30 | 0
31 |
32 |
33 | 3
34 |
35 |
36 |
37 | Address
38 |
39 |
40 |
41 |
42 | Description
43 |
44 |
45 |
46 |
47 | Stat
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/history_ui.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtGui, QtCore, QtWidgets
4 | from PyQt6.QtCore import Qt, pyqtSlot
5 | from PyQt6.QtWidgets import QTableWidgetItem, QTableWidget, QMenu
6 |
7 |
8 | class HistoryTableWidget(QTableWidget):
9 | key_escape_pressed_signal = QtCore.pyqtSignal()
10 | history_remove_row_signal = QtCore.pyqtSignal(str)
11 | set_watch_func_signal = QtCore.pyqtSignal(str)
12 | set_watch_regs_signal = QtCore.pyqtSignal(str)
13 |
14 | def __init__(self, args):
15 | super(HistoryTableWidget, self).__init__(args)
16 |
17 | def keyPressEvent(self, e: QtGui.QKeyEvent) -> None:
18 | if e.key() == Qt.Key.Key_Escape:
19 | self.key_escape_pressed_signal.emit()
20 | if e.key() == Qt.Key.Key_Delete:
21 | item = self.item(self.selectedItems()[0].row(), 0)
22 | self.history_remove_row_signal.emit(item.text())
23 |
24 | def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
25 | item = self.itemAt(event.pos())
26 | if item is not None and len(self.selectedItems()) == 1 and item.column() == 0:
27 | context_menu = QMenu(self)
28 | action1 = context_menu.addAction("Set Watch Func")
29 | action2 = context_menu.addAction("Set Watch Regs")
30 | action = context_menu.exec(event.globalPos())
31 | if action == action1:
32 | self.set_watch_func_signal.emit(item.text())
33 | elif action == action2:
34 | self.set_watch_regs_signal.emit(item.text())
35 |
36 | @pyqtSlot(list)
37 | def watch_list_sig_func(self, sig: list):
38 | if not sig:
39 | for row in range(self.rowCount()):
40 | item = self.item(row, 2)
41 | if item.text() == "Watch func" or "Watch regs":
42 | self.setItem(row, 2, QTableWidgetItem(""))
43 | else:
44 | for row in range(self.rowCount()):
45 | item = self.item(row, 0)
46 | for watch_item in sig:
47 | if watch_item[0] in item.text():
48 | self.setItem(item.row(), 2, QTableWidgetItem("Watch func" if watch_item[1] is False else "Watch regs"))
49 | break
50 |
51 |
52 | class Ui_Form(object):
53 | def setupUi(self, Form):
54 | Form.setObjectName("Form")
55 | Form.resize(530, 170)
56 | font = QtGui.QFont()
57 | font.setFamily("Courier New")
58 | font_size = 13 if platform.system() == "Darwin" else 9
59 | font.setPointSize(font_size)
60 | Form.setFont(font)
61 | self.gridLayout = QtWidgets.QGridLayout(Form)
62 | self.gridLayout.setObjectName("gridLayout")
63 | # self.historyTableWidget = QtWidgets.QTableWidget(Form)
64 | self.historyTableWidget = HistoryTableWidget(Form)
65 | font = QtGui.QFont()
66 | font.setFamily("Courier New")
67 | self.historyTableWidget.setFont(font)
68 | self.historyTableWidget.setColumnCount(3)
69 | self.historyTableWidget.setColumnWidth(0, 130)
70 | self.historyTableWidget.setColumnWidth(1, 250)
71 | self.historyTableWidget.setColumnWidth(2, 100)
72 | self.historyTableWidget.setHorizontalHeaderLabels(['Address', 'Description', 'Stat'])
73 | self.historyTableWidget.setObjectName("historyTableWidget")
74 | self.gridLayout.addWidget(self.historyTableWidget, 0, 0, 1, 1)
75 |
76 | self.retranslateUi(Form)
77 | QtCore.QMetaObject.connectSlotsByName(Form)
78 |
79 | def retranslateUi(self, Form):
80 | _translate = QtCore.QCoreApplication.translate
81 | Form.setWindowTitle(_translate("Form", "History"))
--------------------------------------------------------------------------------
/icon/greenlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackcatml/mlviewer/4fb57f6d337101a6badb080b5260ecb781cd72e3/icon/greenlight.png
--------------------------------------------------------------------------------
/icon/mlviewerico.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackcatml/mlviewer/4fb57f6d337101a6badb080b5260ecb781cd72e3/icon/mlviewerico.png
--------------------------------------------------------------------------------
/icon/redlight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackcatml/mlviewer/4fb57f6d337101a6badb080b5260ecb781cd72e3/icon/redlight.png
--------------------------------------------------------------------------------
/list_img_viewer.py:
--------------------------------------------------------------------------------
1 | from PyQt6 import QtGui, QtCore
2 | from PyQt6.QtWidgets import QTextBrowser
3 |
4 |
5 | class ListImgViewerClass(QTextBrowser):
6 | module_name_signal = QtCore.pyqtSignal(str)
7 |
8 | def __init__(self, args):
9 | super(ListImgViewerClass, self).__init__(args)
10 |
11 | def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
12 | super(ListImgViewerClass, self).mousePressEvent(e)
13 | pos = e.pos()
14 | tc = self.cursorForPosition(pos)
15 | if tc.block().text().startswith("Dumped file at:"):
16 | return
17 | self.module_name_signal.emit(tc.block().text())
18 |
19 |
20 | class MemSearchResultBrowserClass(QTextBrowser):
21 | search_result_addr_signal = QtCore.pyqtSignal(str)
22 |
23 | def __init__(self, args):
24 | super(MemSearchResultBrowserClass, self).__init__(args)
25 |
26 | def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
27 | super(MemSearchResultBrowserClass, self).mousePressEvent(e)
28 | pos = e.pos()
29 | tc = self.cursorForPosition(pos)
30 | self.search_result_addr_signal.emit(tc.block().text()[:tc.block().text().find(', ')])
31 |
--------------------------------------------------------------------------------
/misc.py:
--------------------------------------------------------------------------------
1 | import re
2 | import struct
3 |
4 |
5 | def hex_pattern_check(text: str):
6 | # Memory scan pattern check
7 | if (pattern := text) == '':
8 | return "Error: put some pattern"
9 | else:
10 | pattern = pattern.replace(' ', '')
11 | if len(pattern) % 2 != 0 or len(pattern) == 0:
12 | return "Error: hex pattern length should be 2, 4, 6..."
13 | # Check hex pattern match regex (negative lookahead)
14 | # Support mask for the memory scan pattern
15 | elif re.search(r"(?![0-9a-fA-F?]).", pattern) or re.search(r"^[?]{2}|[?]{2}$", pattern):
16 | return "Error: invalid hex pattern"
17 | # Hex pattern check passed, return original text
18 | return text
19 |
20 |
21 | def mem_patch_value_check_up(type: str, value: str):
22 | unsigned_integer_regex = re.compile(r'^\d+$')
23 | integer_regex = re.compile(r'^-?\d+$')
24 | float_regex = re.compile(r'^-?\d+(\.\d+)?$')
25 | error = False
26 | if type == 'writeU8':
27 | if unsigned_integer_regex.match(value) and int(value) < 256:
28 | pass
29 | else:
30 | error = True
31 | elif type == 'writeU16':
32 | if unsigned_integer_regex.match(value) and int(value) < 65536:
33 | pass
34 | else:
35 | error = True
36 | elif type == 'writeU32':
37 | if unsigned_integer_regex.match(value) and int(value) < 4294967296:
38 | pass
39 | else:
40 | error = True
41 | elif type == 'writeU64':
42 | if unsigned_integer_regex.match(value) and int(value) < 18446744073709551616:
43 | pass
44 | else:
45 | error = True
46 | elif type == 'writeInt':
47 | if integer_regex.match(value) and (-2147483648 <= int(value) < 2147483648):
48 | pass
49 | else:
50 | error = True
51 | elif type == 'writeFloat':
52 | if float_regex.match(value):
53 | pass
54 | else:
55 | error = True
56 | elif type == 'writeDouble':
57 | if float_regex.match(value):
58 | pass
59 | else:
60 | error = True
61 | elif type == 'writeUtf8String':
62 | pass
63 | elif type == 'writeByteArray':
64 | result = hex_pattern_check(value)
65 | if 'Error' in result:
66 | return result
67 | else:
68 | byte_pairs = hex_value_byte_pairs(result)
69 | byte_array = ["".join(("0x", item)) for item in byte_pairs]
70 | return byte_array
71 |
72 | if error:
73 | return 'Error: wrong value'
74 | else:
75 | return value
76 |
77 |
78 | def change_value_to_little_endian_hex(value, option, radix):
79 | try:
80 | if isinstance(value, str):
81 | if option == 'Float' or option == 'Double':
82 | value = float(value)
83 | elif option == 'String':
84 | pass
85 | else:
86 | if "." in value:
87 | value = int(float(value))
88 | else:
89 | value = int(value, radix)
90 | except ValueError as e:
91 | return f"Error: {e}"
92 |
93 | print(f"[misc][change_value_to_little_endian_hex] {value}")
94 | hex_value = ''
95 | if option == '1 Byte':
96 | if -128 <= value < 128:
97 | hex_value = struct.pack('b', value).hex()
98 | elif 128 <= value < 256:
99 | hex_value = struct.pack('B', value).hex()
100 | else:
101 | hex_value = 'ff'
102 | elif option == '2 Bytes':
103 | if -32768 <= value < 32768:
104 | hex_value = struct.pack(' /dev/null; then
5 | PYTHON_CMD=python
6 | elif command -v python3 &> /dev/null; then
7 | PYTHON_CMD=python3
8 | else
9 | echo "Need to install python >= 3.8.0 to run mlviewer."
10 | exit 1
11 | fi
12 |
13 | # Check if installed python version is 3.8.0+
14 | if [ "$(${PYTHON_CMD} -c 'import sys; print(sys.version_info >= (3, 8))')" != "True" ]; then
15 | echo "Python 3.8.0+ is required."
16 | exit 1
17 | fi
18 |
19 | # Check if the virtual environment already exists
20 | if [ ! -d "venv" ]; then
21 | # Create python virtual environment
22 | ${PYTHON_CMD} -m venv ./venv
23 |
24 | # Activate venv
25 | source venv/bin/activate
26 |
27 | # Install requirements
28 | pip install -r requirements.txt
29 |
30 | # Install capstone based on architecture
31 | pip install capstone
32 | else
33 | # Activate venv
34 | source venv/bin/activate
35 | fi
36 |
37 | # Run
38 | ${PYTHON_CMD} main.py
39 |
--------------------------------------------------------------------------------
/mlviewer_wincon.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | REM Function to check Python version and set global variable
4 | CALL :checkPythonVersion
5 | IF "%PYTHON_CMD%"=="" (
6 | echo Python 3.8 or higher is required.
7 | exit /b 1
8 | )
9 | GOTO :main
10 |
11 | :checkPythonVersion
12 | python -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1);" >nul 2>&1
13 | IF %ERRORLEVEL% == 0 SET PYTHON_CMD=python & GOTO :EOF
14 | python3 -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1);" >nul 2>&1
15 | IF %ERRORLEVEL% == 0 SET PYTHON_CMD=python3 & GOTO :EOF
16 | SET PYTHON_CMD=
17 | GOTO :EOF
18 |
19 | :main
20 | REM Check if the virtual environment already exists
21 | IF NOT EXIST "venv" (
22 | REM Create python virtual environment
23 | %PYTHON_CMD% -m venv .\venv
24 |
25 | REM Activate venv
26 | .\venv\Scripts\activate.bat
27 |
28 | REM Install requirements
29 | pip install -r requirements.txt
30 |
31 | REM Install capstone
32 | pip install capstone
33 | REM Run
34 | %PYTHON_CMD% .\main.py
35 | ) ELSE (
36 | REM Activate venv
37 | .\venv\Scripts\activate.bat
38 | REM Run
39 | %PYTHON_CMD% .\main.py
40 | )
--------------------------------------------------------------------------------
/parse_unity_dump.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import re
3 |
4 | from PyQt6 import QtWidgets, QtCore
5 | from PyQt6.QtCore import Qt, pyqtSlot
6 | from PyQt6.QtGui import QStandardItemModel, QStandardItem, QColor
7 | from PyQt6.QtWidgets import QFileDialog, QTableView, QLineEdit, QVBoxLayout, QDialog
8 |
9 | import gvar
10 | import parse_unity_dump_ui
11 |
12 |
13 | class ParseResultTableView(QDialog):
14 | method_clicked_signal = QtCore.pyqtSignal(str)
15 |
16 | def __init__(self, file_path):
17 | super().__init__()
18 | self.setWindowTitle("Unity Data Table")
19 | self.resize(800, 400)
20 |
21 | self.table_view = QTableView()
22 | self.table_view.setAutoScroll(False)
23 | self.model = QStandardItemModel()
24 | self.model.setHorizontalHeaderLabels(['Class', 'Field', 'Method', 'Offset'])
25 | self.table_view.setModel(self.model)
26 | self.previous_row = -1
27 |
28 | self.search_box = QLineEdit()
29 | self.search_box.setPlaceholderText("Search...")
30 | self.search_box.textChanged.connect(self.filter_table)
31 |
32 | layout = QVBoxLayout(self)
33 | layout.addWidget(self.table_view)
34 | layout.addWidget(self.search_box)
35 |
36 | self.file_path = file_path
37 | self.original_data = []
38 | self.populate_table(self.file_path)
39 |
40 | self.platform = None
41 | self.il2cpp_base = None
42 | self.table_view.clicked.connect(self.table_click)
43 |
44 | def populate_table(self, file_path):
45 | with open(file_path, 'r') as file:
46 | try:
47 | lines = file.readlines()
48 | except Exception as e:
49 | print(f"[parse_unity_dump]{inspect.currentframe().f_code.co_name}: {e}")
50 | return
51 |
52 | # self.original_data = [] # Store original data for filtering
53 | class_type = None
54 | class_name = None
55 | for line in lines:
56 | # Match class declaration
57 | class_match = re.match(r'(class|struct|enum)\s+(.*)\s+:', line)
58 | if class_match:
59 | class_type, class_name = class_match.groups()
60 | continue
61 |
62 | # Match fields with offset
63 | field_match = re.match(r'\s+(.*)\s+(.*[^()]);\s+//\s+(0x[\da-fA-F]+)', line)
64 | static_field_match = re.match(r'\s+(.*=\s+(-?)\d+)', line)
65 | if field_match:
66 | field_type, field_name, offset = field_match.groups()
67 | field = f"{field_type} {field_name}"
68 | method = "" # No method for fields
69 | # Store the data for filtering
70 | self.original_data.append((class_name, field, method, offset))
71 | self.add_row_to_table(class_name, field, method, offset)
72 | continue
73 | if not field_match and static_field_match:
74 | field = static_field_match.groups()[0]
75 | method = ""
76 | offset = ""
77 | self.original_data.append((class_name, field, method, offset))
78 | self.add_row_to_table(class_name, field, method, offset)
79 | continue
80 |
81 | if class_type == "enum":
82 | enum_match = re.match(r'\s+(.*=\s+(-?)\d+)', line)
83 | if enum_match:
84 | enum_data = enum_match.groups()[0]
85 | method = ""
86 | offset = ""
87 | self.original_data.append((class_name, enum_data, method, offset))
88 | self.add_row_to_table(class_name, enum_data, method, offset)
89 | continue
90 |
91 | # Match methods with offset
92 | method_match = re.match(r'\s+(.*)\s+(.*)\((.*?)\);\s+//\s+(0x[\da-fA-F]+)', line)
93 | if method_match:
94 | return_type, method_name, params, offset = method_match.groups()
95 | field = "" # No field for methods
96 | method = f"{return_type} {method_name}({params})"
97 |
98 | self.original_data.append((class_name, field, method, offset))
99 | self.add_row_to_table(class_name, field, method, offset)
100 |
101 | def add_row_to_table(self, class_name, field, method, offset):
102 | row = [
103 | QStandardItem(class_name),
104 | QStandardItem(field),
105 | QStandardItem(method),
106 | QStandardItem(offset),
107 | ]
108 | self.model.appendRow(row)
109 |
110 | def filter_table(self, search_text):
111 | # Clear the table
112 | self.model.removeRows(0, self.model.rowCount())
113 |
114 | # Filter rows based on search text
115 | for class_name, field, method, offset in self.original_data:
116 | if (search_text.lower() in class_name.lower() or
117 | search_text.lower() in field.lower() or
118 | search_text.lower() in method.lower() or
119 | search_text.lower() in offset.lower()):
120 | self.add_row_to_table(class_name, field, method, offset)
121 |
122 | def table_click(self, index):
123 | # Remove highlight from the previous row if it exists
124 | if self.previous_row != -1:
125 | for col in range(self.model.columnCount()):
126 | self.model.setData(self.model.index(self.previous_row, col), Qt.GlobalColor.transparent,
127 | Qt.ItemDataRole.BackgroundRole)
128 |
129 | # Highlight the current row without selecting it
130 | row = index.row()
131 | for col in range(self.model.columnCount()):
132 | self.model.setData(self.model.index(row, col), QColor("gray"), Qt.ItemDataRole.BackgroundRole)
133 |
134 | # Update the previous_row to the current row
135 | self.previous_row = row
136 |
137 | # Method click
138 | if index.column() == 2:
139 | if self.model.data(index) != '':
140 | offset_index = self.model.index(index.row(), 3)
141 | offset = self.model.data(offset_index)
142 | if self.il2cpp_base is None:
143 | try:
144 | module_name = 'libil2cpp.so' if self.platform == 'linux' else 'UnityFramework'
145 | module: dict = gvar.frida_instrument.get_module_by_name(module_name)
146 | if module is None:
147 | print(f"[parse_unity_dump][table_click] Cannot find the module")
148 | return
149 | self.il2cpp_base = module['base']
150 | except Exception as e:
151 | print(f"[parse_unity_dump]{inspect.currentframe().f_code.co_name}: {e}")
152 | return
153 | addr = hex(int(self.il2cpp_base, 16) + int(offset, 16))
154 | self.method_clicked_signal.emit(addr)
155 |
156 |
157 | class ParseUnityDumpFile(QtWidgets.QDialog):
158 | parse_result_table_created_signal = QtCore.pyqtSignal(int)
159 |
160 | def __init__(self):
161 | super(ParseUnityDumpFile, self).__init__()
162 | self.parse_unity_dump_file_dialog = QtWidgets.QDialog()
163 | self.parse_unity_dump_file_dialog.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
164 | self.parse_unity_dump_file_dialog_ui = parse_unity_dump_ui.Ui_ParseUnityDumpFileDialog()
165 | self.parse_unity_dump_file_dialog_ui.setupUi(self.parse_unity_dump_file_dialog)
166 |
167 | self.parse_unity_dump_file_dialog_ui.textEdit.file_dropped_sig.connect(self.file_dropped_sig_func)
168 | self.parse_unity_dump_file_dialog_ui.fileBtn.clicked.connect(self.select_file)
169 |
170 | self.parse_unity_dump_file_dialog_ui.doParseBtn.setDisabled(True)
171 | self.parse_unity_dump_file_dialog_ui.doParseBtn.clicked.connect(
172 | lambda: self.do_parse(self.parse_unity_dump_file_dialog_ui.doParseBtn.text()))
173 |
174 | self.platform = None
175 | self.file = None
176 | self.parse_result = None
177 |
178 | @pyqtSlot(str)
179 | def file_dropped_sig_func(self, dropped_file: str):
180 | self.file = dropped_file
181 | if self.file is not None:
182 | self.parse_unity_dump_file_dialog_ui.doParseBtn.setEnabled(True)
183 |
184 | def select_file(self):
185 | file, _ = QFileDialog.getOpenFileNames(self, caption="Select a dumped file to parse", directory="./dump", initialFilter="All Files (*)")
186 | self.file = "" if len(file) == 0 else file[0]
187 | if self.file:
188 | self.parse_unity_dump_file_dialog_ui.textEdit.setText(self.file)
189 | if self.parse_result is not None and self.parse_result.file_path != self.file and \
190 | self.parse_unity_dump_file_dialog_ui.doParseBtn.text() == 'Show':
191 | self.parse_unity_dump_file_dialog_ui.doParseBtn.setText('Parse')
192 | self.parse_unity_dump_file_dialog_ui.doParseBtn.setEnabled(True)
193 |
194 | def do_parse(self, button_text):
195 | if button_text == 'Parse':
196 | if '.cs' not in self.file:
197 | print(f"[parse_unity_dump][ParseUnityDumpFile][do_parse] need .cs file to parse")
198 | return
199 | self.parse_result = ParseResultTableView(self.file)
200 | self.parse_result.platform = self.platform
201 | self.parse_result_table_created_signal.emit(1)
202 | elif button_text == 'Show':
203 | self.parse_result_table_created_signal.emit(0)
204 | self.parse_result.show()
205 | self.parse_unity_dump_file_dialog.close()
206 |
--------------------------------------------------------------------------------
/parse_unity_dump_ui.py:
--------------------------------------------------------------------------------
1 | import platform
2 |
3 | from PyQt6 import QtWidgets, QtCore
4 |
5 |
6 | class DroppableTextEdit(QtWidgets.QTextEdit):
7 | file_dropped_sig = QtCore.pyqtSignal(str)
8 |
9 | def __init__(self, parent=None, text_edit_for_file1or2=None):
10 | super().__init__(parent)
11 | self.text_edit_for_file = text_edit_for_file1or2
12 | self.setAcceptDrops(True)
13 |
14 | def dragEnterEvent(self, event):
15 | if event.mimeData().hasUrls():
16 | event.acceptProposedAction()
17 |
18 | def dragMoveEvent(self, event):
19 | if event.mimeData().hasUrls():
20 | event.acceptProposedAction()
21 |
22 | def dropEvent(self, event):
23 | for url in event.mimeData().urls():
24 | if url.isLocalFile():
25 | file_path = url.toLocalFile()
26 | self.clear()
27 | self.setText(file_path)
28 | self.file_dropped_sig.emit(file_path)
29 |
30 |
31 | class Ui_ParseUnityDumpFileDialog(object):
32 | def setupUi(self, Dialog):
33 | Dialog.setObjectName("Dialog")
34 | Dialog.resize(300, 150) if platform.system() == 'Windows' else Dialog.resize(250, 150)
35 | self.gridLayout = QtWidgets.QGridLayout(Dialog)
36 | self.gridLayout.setObjectName("gridLayout")
37 | self.textEdit = DroppableTextEdit(parent=Dialog, text_edit_for_file1or2="file")
38 | self.textEdit.setReadOnly(True)
39 | self.textEdit.setObjectName("textEdit")
40 | self.gridLayout.addWidget(self.textEdit, 0, 0, 1, 1)
41 | self.fileBtn = QtWidgets.QPushButton(Dialog)
42 | self.fileBtn.setObjectName("fileBtn")
43 | self.fileBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
44 | self.gridLayout.addWidget(self.fileBtn, 1, 0, 1, 1)
45 | self.doParseBtn = QtWidgets.QPushButton(Dialog)
46 | self.doParseBtn.setObjectName("doParseBtn")
47 | self.doParseBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
48 | self.gridLayout.addWidget(self.doParseBtn, 2, 0, 1, 1)
49 |
50 | self.retranslateUi(Dialog)
51 | QtCore.QMetaObject.connectSlotsByName(Dialog)
52 |
53 | def retranslateUi(self, Dialog):
54 | _translate = QtCore.QCoreApplication.translate
55 | Dialog.setWindowTitle(_translate("Dialog", "Parse Unity Dump File"))
56 | font_family = ".AppleSystemUIFont" if platform.system() == "Darwin" else "Courier New"
57 | font_size = "13pt" if platform.system() == "Darwin" else "9pt"
58 | self.textEdit.setHtml(_translate("Dialog",
59 | "\n"
60 | "\n"
66 | "
\n"
67 | "File
\n"
68 | "(Drag & Drop)
"))
69 | self.fileBtn.setText(_translate("Dialog", "File"))
70 | self.doParseBtn.setText(_translate("Dialog", "Parse"))
71 |
72 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | attrs==22.2.0
2 | certifi==2023.5.7
3 | charset-normalizer==2.1.1
4 | frida==16.5.6
5 | frida-tools==13.6.0
6 | frida-dexdump==2.0.1
7 | frozenlist==1.5.0
8 | idna==3.4
9 | multidict==6.1.0
10 | PyQt6==6.7.1
11 | PyQt6-Qt6==6.7.3
12 | PyQt6-sip==13.8.0
13 | requests==2.31.0
14 | shiboken6==6.8.0.2
15 | timer==0.3.0
16 | urllib3==2.0.3
17 | wheel==0.44.0
18 | yarl==1.17.1
19 | r2pipe==1.9.4
20 |
--------------------------------------------------------------------------------
/scan_result.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 517
10 | 681
11 |
12 |
13 |
14 | Scan Result
15 |
16 |
17 | -
18 |
19 |
20 | 7
21 |
22 |
23 | 0
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
31 |
32 | 0
33 | 0
34 |
35 |
36 |
37 |
38 | 0
39 | 0
40 |
41 |
42 |
43 |
44 | 16777215
45 | 16777215
46 |
47 |
48 |
49 | First Scan
50 |
51 |
52 |
53 | -
54 |
55 |
56 |
57 | 0
58 | 0
59 |
60 |
61 |
62 |
63 | 75
64 | 0
65 |
66 |
67 |
68 |
69 | 16777215
70 | 16777215
71 |
72 |
73 |
74 | Next Scan
75 |
76 |
77 |
78 | -
79 |
80 |
81 | Qt::Horizontal
82 |
83 |
84 |
85 | 40
86 | 20
87 |
88 |
89 |
90 |
91 | -
92 |
93 |
94 | Qt::Horizontal
95 |
96 |
97 |
98 | 40
99 | 20
100 |
101 |
102 |
103 |
104 | -
105 |
106 |
107 | Qt::Horizontal
108 |
109 |
110 |
111 | 40
112 | 20
113 |
114 |
115 |
116 |
117 | -
118 |
119 |
120 |
121 | 0
122 | 0
123 |
124 |
125 |
126 |
127 | 75
128 | 0
129 |
130 |
131 |
132 |
133 | 16777215
134 | 16777215
135 |
136 |
137 |
138 | Stop Scan
139 |
140 |
141 |
142 |
143 |
144 | -
145 |
146 |
147 | 7
148 |
149 |
150 | 0
151 |
152 |
153 | 0
154 |
155 |
-
156 |
157 |
158 |
159 |
160 |
161 |
162 | -
163 |
164 |
165 |
166 |
167 |
168 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
169 |
170 |
171 |
172 |
173 |
174 | -
175 |
176 |
177 |
178 | Address
179 |
180 |
181 |
182 |
183 | Value
184 |
185 |
186 |
187 |
188 | First Scan
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 | 7
217 |
218 |
219 | 0
220 |
221 |
222 | 0
223 |
224 |
-
225 |
226 |
227 |
228 | 90
229 | 0
230 |
231 |
232 |
233 |
234 | 120
235 | 16777215
236 |
237 |
238 |
239 |
240 | 13
241 |
242 |
243 |
244 | Exclude Path:
245 |
246 |
247 |
248 | -
249 |
250 |
251 |
252 | 0
253 | 1
254 |
255 |
256 |
257 |
258 | 190
259 | 30
260 |
261 |
262 |
263 |
264 | 16777215
265 | 30
266 |
267 |
268 |
269 | Qt::StrongFocus
270 |
271 |
272 | true
273 |
274 |
275 | false
276 |
277 |
278 | e.g., \/system\/|\/dev\/
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
--------------------------------------------------------------------------------
/scan_result_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'scan_result.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.4.0
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 | import inspect
8 |
9 | from PyQt6 import QtCore, QtWidgets, QtGui
10 | from PyQt6.QtCore import pyqtSlot, Qt, QEvent
11 | from PyQt6.QtGui import QShortcut, QKeySequence
12 | from PyQt6.QtWidgets import QTableWidget, QApplication, QMenu, QHBoxLayout, QLabel, QComboBox, QLineEdit, QPushButton, \
13 | QVBoxLayout, QWidget
14 |
15 | import gvar
16 | import misc
17 | import watchpoint
18 |
19 |
20 | class MemPatchWidget(QWidget):
21 | def __init__(self):
22 | super(MemPatchWidget, self).__init__()
23 | self.setWindowTitle("Memory Patch")
24 | self.resize(550, 100)
25 |
26 | self.main_layout = QVBoxLayout(self)
27 |
28 | self.buttons_layout = QHBoxLayout()
29 | self.clear_button = QPushButton("Clear")
30 | self.clear_button.clicked.connect(self.clear_contents)
31 | self.apply_all_button = QPushButton("Apply All")
32 | self.apply_all_button.clicked.connect(self.apply_patch_all)
33 |
34 | self.buttons_layout.addWidget(self.clear_button)
35 | self.buttons_layout.addWidget(self.apply_all_button)
36 |
37 | self.main_layout.addLayout(self.buttons_layout)
38 |
39 | self.rows_container = QWidget()
40 | self.rows_layout = QVBoxLayout(self.rows_container)
41 | self.rows_layout.setSpacing(5) # Adjust the spacing to your preference
42 |
43 | self.main_layout.addWidget(self.rows_container)
44 |
45 | self.rows = []
46 | self.patch_target_addresses = []
47 |
48 | def keyPressEvent(self, event):
49 | if event.key() == Qt.Key.Key_Escape:
50 | self.close()
51 | else:
52 | super().keyPressEvent(event)
53 |
54 | def add_row(self, addresses):
55 | for address in addresses:
56 | if address not in self.patch_target_addresses:
57 | row_layout = QHBoxLayout()
58 | label = QLabel(f"{address}")
59 | label.setMinimumSize(QtCore.QSize(100, 26))
60 | label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
61 | combobox = QComboBox()
62 | combobox.setMinimumSize(QtCore.QSize(100, 26))
63 | combobox.addItems(["writeU8", "writeU16", "writeU32", "writeU64", "writeInt",
64 | "writeFloat", "writeDouble", "writeUtf8String", "writeByteArray"])
65 | lineedit = QLineEdit()
66 | lineedit.setMinimumSize(QtCore.QSize(150, 26))
67 | apply_button = QPushButton("Apply")
68 | apply_button.setMinimumSize(QtCore.QSize(50, 26))
69 | row_layout.addWidget(label)
70 | row_layout.addWidget(combobox)
71 | row_layout.addWidget(lineedit)
72 | row_layout.addWidget(apply_button)
73 | lineedit.returnPressed.connect(lambda lbl=label, cb=combobox, le=lineedit: self.apply_patch(lbl, cb, le))
74 | apply_button.clicked.connect(lambda _, lbl=label, cb=combobox, le=lineedit: self.apply_patch(lbl, cb, le))
75 |
76 | self.rows_layout.addLayout(row_layout)
77 | self.rows.append((label, combobox, lineedit, row_layout))
78 | self.patch_target_addresses.append(label.text())
79 |
80 | def apply_patch(self, label, combobox, lineedit):
81 | # Retrieve and print data from this specific row
82 | label_text = label.text()
83 | combobox_text = combobox.currentText()
84 | lineedit_text = lineedit.text()
85 | mem_patch_value = misc.mem_patch_value_check_up(combobox_text, lineedit_text)
86 | if 'Error' in mem_patch_value:
87 | print(f"[scan_result_ui][apply_patch] mem patch value check up failed")
88 | return
89 | if combobox_text == 'writeFloat' or combobox_text == 'writeDouble':
90 | mem_patch_value = float(mem_patch_value)
91 | elif combobox_text == 'writeU8' or combobox_text == 'writeU16' or \
92 | combobox_text == 'writeU32' or combobox_text == 'writeU64' or combobox_text == 'writeInt':
93 | mem_patch_value = int(mem_patch_value)
94 | try:
95 | gvar.frida_instrument.mem_patch(label_text, mem_patch_value, combobox_text)
96 | except Exception as e:
97 | print(f"[scan_result_ui]{inspect.currentframe().f_code.co_name}: {e}")
98 |
99 | def apply_patch_all(self):
100 | for label, combobox, lineedit, _ in self.rows:
101 | self.apply_patch(label, combobox, lineedit)
102 |
103 | def clear_contents(self):
104 | # Remove all rows from the layout and clear the rows list
105 | while self.rows:
106 | _, _, _, row_layout = self.rows.pop()
107 | # Remove each row layout from the parent layout
108 | for i in reversed(range(row_layout.count())):
109 | widget = row_layout.itemAt(i).widget()
110 | if widget:
111 | widget.deleteLater()
112 | self.rows_layout.removeItem(row_layout)
113 | self.patch_target_addresses.clear()
114 | self.resize(550, 100)
115 |
116 |
117 | class MemScanResultTableWidget(QTableWidget):
118 | set_watchpoint_menu_clicked_signal = QtCore.pyqtSignal(list)
119 |
120 | def __init__(self, args):
121 | super(MemScanResultTableWidget, self).__init__(args)
122 |
123 | self.mem_patch_widget = MemPatchWidget()
124 | self.mem_patch_widget.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
125 | self.watch_point_widget = watchpoint.WatchPointWidget()
126 |
127 | # Set up the shortcut for Cmd+C or Ctrl+C
128 | copy_shortcut = QShortcut(QKeySequence("Ctrl+C"), self)
129 | copy_shortcut.activated.connect(self.copy_selected_items)
130 |
131 | def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
132 | item = self.itemAt(event.pos())
133 | if item is not None and item.column() == 0:
134 | context_menu = QMenu(self)
135 | action1 = context_menu.addAction("Patch")
136 | action2 = context_menu.addAction("Set watchpoint")
137 | action = context_menu.exec(event.globalPos())
138 | selected_items = [item.text() for item in self.selectedItems()]
139 | if action == action1:
140 | self.mem_patch_menu_clicked(selected_items)
141 | if action == action2:
142 | self.set_watchpoint_menu_clicked(selected_items[0])
143 |
144 | def mem_patch_menu_clicked(self, address_list):
145 | self.mem_patch_widget.add_row(address_list)
146 | self.mem_patch_widget.show()
147 |
148 | def set_watchpoint_menu_clicked(self, addr):
149 | self.watch_point_widget.watch_point_ui.watchpointAddrInput.setText(addr)
150 | self.watch_point_widget.show()
151 |
152 | def copy_selected_items(self):
153 | # Get selected items
154 | selected_items = self.selectedItems()
155 |
156 | # Group items by row for copying in table format
157 | rows = {}
158 | for item in selected_items:
159 | row = item.row()
160 | if row not in rows:
161 | rows[row] = []
162 | rows[row].append(item.text())
163 |
164 | # Sort rows by their keys (row number) and create formatted text
165 | copied_text = "\n".join(
166 | "\t".join(rows[row]) for row in sorted(rows.keys())
167 | )
168 |
169 | # Copy to clipboard
170 | clipboard = QApplication.clipboard()
171 | clipboard.setText(copied_text)
172 |
173 |
174 | class Ui_Form(object):
175 | def setupUi(self, Form):
176 | Form.setObjectName("Form")
177 | Form.resize(545, 619)
178 | self.gridLayout = QtWidgets.QGridLayout(Form)
179 | self.gridLayout.setObjectName("gridLayout")
180 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
181 | self.horizontalLayout_4.setContentsMargins(-1, 0, -1, 0)
182 | self.horizontalLayout_4.setSpacing(7)
183 | self.horizontalLayout_4.setObjectName("horizontalLayout_4")
184 | self.startScanBtn = QtWidgets.QPushButton(Form)
185 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
186 | sizePolicy.setHorizontalStretch(0)
187 | sizePolicy.setVerticalStretch(0)
188 | sizePolicy.setHeightForWidth(self.startScanBtn.sizePolicy().hasHeightForWidth())
189 | self.startScanBtn.setSizePolicy(sizePolicy)
190 | self.startScanBtn.setMinimumSize(QtCore.QSize(0, 0))
191 | self.startScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
192 | self.startScanBtn.setObjectName("startScanBtn")
193 | self.horizontalLayout_4.addWidget(self.startScanBtn)
194 | self.nextScanBtn = QtWidgets.QPushButton(Form)
195 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
196 | sizePolicy.setHorizontalStretch(0)
197 | sizePolicy.setVerticalStretch(0)
198 | sizePolicy.setHeightForWidth(self.nextScanBtn.sizePolicy().hasHeightForWidth())
199 | self.nextScanBtn.setSizePolicy(sizePolicy)
200 | self.nextScanBtn.setMinimumSize(QtCore.QSize(75, 0))
201 | self.nextScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
202 | self.nextScanBtn.setObjectName("nextScanBtn")
203 | self.horizontalLayout_4.addWidget(self.nextScanBtn)
204 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
205 | self.horizontalLayout_4.addItem(spacerItem)
206 | spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
207 | self.horizontalLayout_4.addItem(spacerItem1)
208 | spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
209 | self.horizontalLayout_4.addItem(spacerItem2)
210 | self.stopScanBtn = QtWidgets.QPushButton(Form)
211 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
212 | sizePolicy.setHorizontalStretch(0)
213 | sizePolicy.setVerticalStretch(0)
214 | sizePolicy.setHeightForWidth(self.stopScanBtn.sizePolicy().hasHeightForWidth())
215 | self.stopScanBtn.setSizePolicy(sizePolicy)
216 | self.stopScanBtn.setMinimumSize(QtCore.QSize(75, 0))
217 | self.stopScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
218 | self.stopScanBtn.setObjectName("stopScanBtn")
219 | self.horizontalLayout_4.addWidget(self.stopScanBtn)
220 | self.gridLayout.addLayout(self.horizontalLayout_4, 0, 0, 1, 1)
221 | self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
222 | self.horizontalLayout_6.setContentsMargins(-1, 0, -1, 0)
223 | self.horizontalLayout_6.setSpacing(7)
224 | self.horizontalLayout_6.setObjectName("horizontalLayout_6")
225 | self.scanMatchFoundLabel = QtWidgets.QLabel(Form)
226 | self.scanMatchFoundLabel.setText("")
227 | self.scanMatchFoundLabel.setObjectName("scanMatchFoundLabel")
228 | self.horizontalLayout_6.addWidget(self.scanMatchFoundLabel)
229 | self.scanPercentProgressLabel = QtWidgets.QLabel(Form)
230 | self.scanPercentProgressLabel.setText("")
231 | self.scanPercentProgressLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
232 | self.scanPercentProgressLabel.setObjectName("scanPercentProgressLabel")
233 | self.horizontalLayout_6.addWidget(self.scanPercentProgressLabel)
234 | self.gridLayout.addLayout(self.horizontalLayout_6, 4, 0, 1, 1)
235 | # self.memScanResultTableWidget = QtWidgets.QTableWidget(Form)
236 | self.memScanResultTableWidget = MemScanResultTableWidget(Form)
237 | self.memScanResultTableWidget.setObjectName("memScanResultTableWidget")
238 | self.memScanResultTableWidget.setColumnCount(7)
239 | self.memScanResultTableWidget.setRowCount(0)
240 | item = QtWidgets.QTableWidgetItem()
241 | self.memScanResultTableWidget.setHorizontalHeaderItem(0, item)
242 | item = QtWidgets.QTableWidgetItem()
243 | self.memScanResultTableWidget.setHorizontalHeaderItem(1, item)
244 | item = QtWidgets.QTableWidgetItem()
245 | self.memScanResultTableWidget.setHorizontalHeaderItem(2, item)
246 | item = QtWidgets.QTableWidgetItem()
247 | self.memScanResultTableWidget.setHorizontalHeaderItem(3, item)
248 | item = QtWidgets.QTableWidgetItem()
249 | self.memScanResultTableWidget.setHorizontalHeaderItem(4, item)
250 | item = QtWidgets.QTableWidgetItem()
251 | self.memScanResultTableWidget.setHorizontalHeaderItem(5, item)
252 | item = QtWidgets.QTableWidgetItem()
253 | self.memScanResultTableWidget.setHorizontalHeaderItem(6, item)
254 | self.gridLayout.addWidget(self.memScanResultTableWidget, 5, 0, 1, 1)
255 | self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
256 | self.horizontalLayout_7.setContentsMargins(-1, 0, -1, 0)
257 | self.horizontalLayout_7.setSpacing(7)
258 | self.horizontalLayout_7.setObjectName("horizontalLayout_7")
259 | self.label_11 = QtWidgets.QLabel(Form)
260 | self.label_11.setMinimumSize(QtCore.QSize(90, 0))
261 | self.label_11.setMaximumSize(QtCore.QSize(120, 16777215))
262 | font = QtGui.QFont()
263 | font.setPointSize(13)
264 | self.label_11.setFont(font)
265 | self.label_11.setObjectName("label_11")
266 | self.horizontalLayout_7.addWidget(self.label_11)
267 | self.memScanExcludePath = QtWidgets.QTextEdit(Form)
268 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
269 | sizePolicy.setHorizontalStretch(0)
270 | sizePolicy.setVerticalStretch(1)
271 | sizePolicy.setHeightForWidth(self.memScanExcludePath.sizePolicy().hasHeightForWidth())
272 | self.memScanExcludePath.setSizePolicy(sizePolicy)
273 | self.memScanExcludePath.setMinimumSize(QtCore.QSize(190, 30))
274 | self.memScanExcludePath.setMaximumSize(QtCore.QSize(16777215, 30))
275 | self.memScanExcludePath.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
276 | self.memScanExcludePath.setTabChangesFocus(True)
277 | self.memScanExcludePath.setAcceptRichText(False)
278 | self.memScanExcludePath.setObjectName("memScanExcludePath")
279 | self.horizontalLayout_7.addWidget(self.memScanExcludePath)
280 | self.gridLayout.addLayout(self.horizontalLayout_7, 1, 0, 1, 1)
281 |
282 | self.retranslateUi(Form)
283 | QtCore.QMetaObject.connectSlotsByName(Form)
284 |
285 | def retranslateUi(self, Form):
286 | _translate = QtCore.QCoreApplication.translate
287 | Form.setWindowTitle(_translate("Form", "Scan Result"))
288 | self.startScanBtn.setText(_translate("Form", "First Scan"))
289 | self.nextScanBtn.setText(_translate("Form", "Next Scan"))
290 | self.stopScanBtn.setText(_translate("Form", "Stop Scan"))
291 | item = self.memScanResultTableWidget.horizontalHeaderItem(0)
292 | item.setText(_translate("Form", "Address"))
293 | item = self.memScanResultTableWidget.horizontalHeaderItem(1)
294 | item.setText(_translate("Form", "Value"))
295 | item = self.memScanResultTableWidget.horizontalHeaderItem(2)
296 | item.setText(_translate("Form", "First Scan"))
297 | item = self.memScanResultTableWidget.horizontalHeaderItem(3)
298 | item.setText(_translate("Form", "-"))
299 | item = self.memScanResultTableWidget.horizontalHeaderItem(4)
300 | item.setText(_translate("Form", "-"))
301 | item = self.memScanResultTableWidget.horizontalHeaderItem(5)
302 | item.setText(_translate("Form", "-"))
303 | item = self.memScanResultTableWidget.horizontalHeaderItem(6)
304 | item.setText(_translate("Form", "-"))
305 | self.label_11.setText(_translate("Form", "Exclude Path:"))
306 | self.memScanExcludePath.setPlaceholderText(_translate("Form", "e.g., \\/system\\/|\\/dev\\/"))
--------------------------------------------------------------------------------
/scan_result_win.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 522
10 | 530
11 |
12 |
13 |
14 | Scan Result
15 |
16 |
17 | -
18 |
19 |
20 | 7
21 |
22 |
23 | 0
24 |
25 |
26 | 0
27 |
28 |
-
29 |
30 |
31 |
32 | 0
33 | 0
34 |
35 |
36 |
37 |
38 | 80
39 | 0
40 |
41 |
42 |
43 |
44 | 16777215
45 | 16777215
46 |
47 |
48 |
49 |
50 | Courier New
51 | 9
52 |
53 |
54 |
55 | First Scan
56 |
57 |
58 |
59 | -
60 |
61 |
62 |
63 | 0
64 | 0
65 |
66 |
67 |
68 |
69 | 80
70 | 0
71 |
72 |
73 |
74 |
75 | 16777215
76 | 16777215
77 |
78 |
79 |
80 |
81 | Courier New
82 |
83 |
84 |
85 | Next Scan
86 |
87 |
88 |
89 | -
90 |
91 |
92 | Qt::Horizontal
93 |
94 |
95 |
96 | 40
97 | 20
98 |
99 |
100 |
101 |
102 | -
103 |
104 |
105 | Qt::Horizontal
106 |
107 |
108 |
109 | 40
110 | 20
111 |
112 |
113 |
114 |
115 | -
116 |
117 |
118 | Qt::Horizontal
119 |
120 |
121 |
122 | 40
123 | 20
124 |
125 |
126 |
127 |
128 | -
129 |
130 |
131 |
132 | 0
133 | 0
134 |
135 |
136 |
137 |
138 | 80
139 | 0
140 |
141 |
142 |
143 |
144 | 16777215
145 | 16777215
146 |
147 |
148 |
149 |
150 | Courier New
151 |
152 |
153 |
154 | Stop Scan
155 |
156 |
157 |
158 |
159 |
160 | -
161 |
162 |
163 | 7
164 |
165 |
166 | 0
167 |
168 |
169 | 0
170 |
171 |
-
172 |
173 |
174 |
175 |
176 |
177 |
178 | -
179 |
180 |
181 |
182 |
183 |
184 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
185 |
186 |
187 |
188 |
189 |
190 | -
191 |
192 |
193 |
194 | Courier New
195 |
196 |
197 |
198 |
199 | Address
200 |
201 |
202 |
203 |
204 | Value
205 |
206 |
207 |
208 |
209 | First Scan
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 | 7
238 |
239 |
240 | 0
241 |
242 |
243 | 0
244 |
245 |
-
246 |
247 |
248 |
249 | 90
250 | 0
251 |
252 |
253 |
254 |
255 | 120
256 | 16777215
257 |
258 |
259 |
260 |
261 | Courier New
262 | 9
263 |
264 |
265 |
266 | Exclude Path:
267 |
268 |
269 |
270 | -
271 |
272 |
273 |
274 | 0
275 | 1
276 |
277 |
278 |
279 |
280 | 190
281 | 26
282 |
283 |
284 |
285 |
286 | 16777215
287 | 26
288 |
289 |
290 |
291 | Qt::StrongFocus
292 |
293 |
294 | true
295 |
296 |
297 | false
298 |
299 |
300 | e.g., \/system\/|\/dev\/
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
--------------------------------------------------------------------------------
/scan_result_win_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'scan_result_win.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.7.1
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 | import inspect
8 |
9 | from PyQt6 import QtCore, QtWidgets, QtGui
10 | from PyQt6.QtCore import pyqtSlot, Qt, QEvent
11 | from PyQt6.QtGui import QShortcut, QKeySequence
12 | from PyQt6.QtWidgets import QTableWidget, QApplication, QMenu, QHBoxLayout, QLabel, QComboBox, QLineEdit, QPushButton, \
13 | QVBoxLayout, QWidget
14 |
15 | import gvar
16 | import misc
17 | import watchpoint
18 |
19 |
20 | class MemPatchWidget(QWidget):
21 | def __init__(self):
22 | super(MemPatchWidget, self).__init__()
23 | self.setWindowTitle("Memory Patch")
24 | self.resize(550, 100)
25 |
26 | self.main_layout = QVBoxLayout(self)
27 |
28 | self.buttons_layout = QHBoxLayout()
29 | self.clear_button = QPushButton("Clear")
30 | self.clear_button.clicked.connect(self.clear_contents)
31 | self.apply_all_button = QPushButton("Apply All")
32 | self.apply_all_button.clicked.connect(self.apply_patch_all)
33 |
34 | self.buttons_layout.addWidget(self.clear_button)
35 | self.buttons_layout.addWidget(self.apply_all_button)
36 |
37 | self.main_layout.addLayout(self.buttons_layout)
38 |
39 | self.rows_container = QWidget()
40 | self.rows_layout = QVBoxLayout(self.rows_container)
41 | self.rows_layout.setSpacing(5) # Adjust the spacing to your preference
42 |
43 | self.main_layout.addWidget(self.rows_container)
44 |
45 | self.rows = []
46 | self.patch_target_addresses = []
47 |
48 | def keyPressEvent(self, event):
49 | if event.key() == Qt.Key.Key_Escape:
50 | self.close()
51 | else:
52 | super().keyPressEvent(event)
53 |
54 | def add_row(self, addresses):
55 | for address in addresses:
56 | if address not in self.patch_target_addresses:
57 | row_layout = QHBoxLayout()
58 | label = QLabel(f"{address}")
59 | label.setMinimumSize(QtCore.QSize(100, 26))
60 | combobox = QComboBox()
61 | combobox.setMinimumSize(QtCore.QSize(100, 26))
62 | combobox.addItems(["writeU8", "writeU16", "writeU32", "writeU64", "writeInt",
63 | "writeFloat", "writeDouble", "writeUtf8String", "writeByteArray"])
64 | lineedit = QLineEdit()
65 | lineedit.setMinimumSize(QtCore.QSize(150, 26))
66 | apply_button = QPushButton("Apply")
67 | apply_button.setMinimumSize(QtCore.QSize(50, 26))
68 | row_layout.addWidget(label)
69 | row_layout.addWidget(combobox)
70 | row_layout.addWidget(lineedit)
71 | row_layout.addWidget(apply_button)
72 | lineedit.returnPressed.connect(lambda lbl=label, cb=combobox, le=lineedit: self.apply_patch(lbl, cb, le))
73 | apply_button.clicked.connect(lambda _, lbl=label, cb=combobox, le=lineedit: self.apply_patch(lbl, cb, le))
74 |
75 | self.rows_layout.addLayout(row_layout)
76 | self.rows.append((label, combobox, lineedit, row_layout))
77 | self.patch_target_addresses.append(label.text())
78 |
79 | def apply_patch(self, label, combobox, lineedit):
80 | # Retrieve and print data from this specific row
81 | label_text = label.text()
82 | combobox_text = combobox.currentText()
83 | lineedit_text = lineedit.text()
84 | mem_patch_value = misc.mem_patch_value_check_up(combobox_text, lineedit_text)
85 | if 'Error' in mem_patch_value:
86 | print(f"[scan_result_ui][apply_patch] mem patch value check up failed")
87 | return
88 | if combobox_text == 'writeFloat' or combobox_text == 'writeDouble':
89 | mem_patch_value = float(mem_patch_value)
90 | elif combobox_text == 'writeU8' or combobox_text == 'writeU16' or \
91 | combobox_text == 'writeU32' or combobox_text == 'writeU64' or combobox_text == 'writeInt':
92 | mem_patch_value = int(mem_patch_value)
93 | try:
94 | gvar.frida_instrument.mem_patch(label_text, mem_patch_value, combobox_text)
95 | except Exception as e:
96 | print(f"[scan_result_ui]{inspect.currentframe().f_code.co_name}: {e}")
97 |
98 | def apply_patch_all(self):
99 | for label, combobox, lineedit, _ in self.rows:
100 | self.apply_patch(label, combobox, lineedit)
101 |
102 | def clear_contents(self):
103 | # Remove all rows from the layout and clear the rows list
104 | while self.rows:
105 | _, _, _, row_layout = self.rows.pop()
106 | # Remove each row layout from the parent layout
107 | for i in reversed(range(row_layout.count())):
108 | widget = row_layout.itemAt(i).widget()
109 | if widget:
110 | widget.deleteLater()
111 | self.rows_layout.removeItem(row_layout)
112 | self.patch_target_addresses.clear()
113 | self.resize(550, 100)
114 |
115 |
116 | class MemScanResultTableWidget(QTableWidget):
117 | set_watchpoint_menu_clicked_signal = QtCore.pyqtSignal(list)
118 |
119 | def __init__(self, args):
120 | super(MemScanResultTableWidget, self).__init__(args)
121 |
122 | self.mem_patch_widget = MemPatchWidget()
123 | self.mem_patch_widget.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
124 | self.watch_point_widget = watchpoint.WatchPointWidget()
125 |
126 | # Set up the shortcut for Cmd+C or Ctrl+C
127 | copy_shortcut = QShortcut(QKeySequence("Ctrl+C"), self)
128 | copy_shortcut.activated.connect(self.copy_selected_items)
129 |
130 | def contextMenuEvent(self, event: QtGui.QContextMenuEvent):
131 | item = self.itemAt(event.pos())
132 | if item is not None and item.column() == 0:
133 | context_menu = QMenu(self)
134 | action1 = context_menu.addAction("Patch")
135 | action2 = context_menu.addAction("Set watchpoint")
136 | action = context_menu.exec(event.globalPos())
137 | selected_items = [item.text() for item in self.selectedItems()]
138 | if action == action1:
139 | self.mem_patch_menu_clicked(selected_items)
140 | if action == action2:
141 | self.set_watchpoint_menu_clicked(selected_items[0])
142 |
143 | def mem_patch_menu_clicked(self, address_list):
144 | self.mem_patch_widget.add_row(address_list)
145 | self.mem_patch_widget.show()
146 |
147 | def set_watchpoint_menu_clicked(self, addr):
148 | self.watch_point_widget.watch_point_ui.watchpointAddrInput.setText(addr)
149 | self.watch_point_widget.show()
150 |
151 | def copy_selected_items(self):
152 | # Get selected items
153 | selected_items = self.selectedItems()
154 |
155 | # Group items by row for copying in table format
156 | rows = {}
157 | for item in selected_items:
158 | row = item.row()
159 | if row not in rows:
160 | rows[row] = []
161 | rows[row].append(item.text())
162 |
163 | # Sort rows by their keys (row number) and create formatted text
164 | copied_text = "\n".join(
165 | "\t".join(rows[row]) for row in sorted(rows.keys())
166 | )
167 |
168 | # Copy to clipboard
169 | clipboard = QApplication.clipboard()
170 | clipboard.setText(copied_text)
171 |
172 |
173 | class Ui_Form(object):
174 | def setupUi(self, Form):
175 | Form.setObjectName("Form")
176 | Form.resize(522, 530)
177 | self.gridLayout = QtWidgets.QGridLayout(Form)
178 | self.gridLayout.setObjectName("gridLayout")
179 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
180 | self.horizontalLayout_4.setContentsMargins(-1, 0, -1, 0)
181 | self.horizontalLayout_4.setSpacing(7)
182 | self.horizontalLayout_4.setObjectName("horizontalLayout_4")
183 | self.startScanBtn = QtWidgets.QPushButton(parent=Form)
184 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
185 | sizePolicy.setHorizontalStretch(0)
186 | sizePolicy.setVerticalStretch(0)
187 | sizePolicy.setHeightForWidth(self.startScanBtn.sizePolicy().hasHeightForWidth())
188 | self.startScanBtn.setSizePolicy(sizePolicy)
189 | self.startScanBtn.setMinimumSize(QtCore.QSize(80, 0))
190 | self.startScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
191 | font = QtGui.QFont()
192 | font.setFamily("Courier New")
193 | font.setPointSize(9)
194 | self.startScanBtn.setFont(font)
195 | self.startScanBtn.setObjectName("startScanBtn")
196 | self.horizontalLayout_4.addWidget(self.startScanBtn)
197 | self.nextScanBtn = QtWidgets.QPushButton(parent=Form)
198 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
199 | sizePolicy.setHorizontalStretch(0)
200 | sizePolicy.setVerticalStretch(0)
201 | sizePolicy.setHeightForWidth(self.nextScanBtn.sizePolicy().hasHeightForWidth())
202 | self.nextScanBtn.setSizePolicy(sizePolicy)
203 | self.nextScanBtn.setMinimumSize(QtCore.QSize(80, 0))
204 | self.nextScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
205 | font = QtGui.QFont()
206 | font.setFamily("Courier New")
207 | self.nextScanBtn.setFont(font)
208 | self.nextScanBtn.setObjectName("nextScanBtn")
209 | self.horizontalLayout_4.addWidget(self.nextScanBtn)
210 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
211 | self.horizontalLayout_4.addItem(spacerItem)
212 | spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
213 | self.horizontalLayout_4.addItem(spacerItem1)
214 | spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
215 | self.horizontalLayout_4.addItem(spacerItem2)
216 | self.stopScanBtn = QtWidgets.QPushButton(parent=Form)
217 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed)
218 | sizePolicy.setHorizontalStretch(0)
219 | sizePolicy.setVerticalStretch(0)
220 | sizePolicy.setHeightForWidth(self.stopScanBtn.sizePolicy().hasHeightForWidth())
221 | self.stopScanBtn.setSizePolicy(sizePolicy)
222 | self.stopScanBtn.setMinimumSize(QtCore.QSize(80, 0))
223 | self.stopScanBtn.setMaximumSize(QtCore.QSize(16777215, 16777215))
224 | font = QtGui.QFont()
225 | font.setFamily("Courier New")
226 | self.stopScanBtn.setFont(font)
227 | self.stopScanBtn.setObjectName("stopScanBtn")
228 | self.horizontalLayout_4.addWidget(self.stopScanBtn)
229 | self.gridLayout.addLayout(self.horizontalLayout_4, 0, 0, 1, 1)
230 | self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
231 | self.horizontalLayout_6.setContentsMargins(-1, 0, -1, 0)
232 | self.horizontalLayout_6.setSpacing(7)
233 | self.horizontalLayout_6.setObjectName("horizontalLayout_6")
234 | self.scanMatchFoundLabel = QtWidgets.QLabel(parent=Form)
235 | self.scanMatchFoundLabel.setText("")
236 | self.scanMatchFoundLabel.setObjectName("scanMatchFoundLabel")
237 | self.horizontalLayout_6.addWidget(self.scanMatchFoundLabel)
238 | self.scanPercentProgressLabel = QtWidgets.QLabel(parent=Form)
239 | self.scanPercentProgressLabel.setText("")
240 | self.scanPercentProgressLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter)
241 | self.scanPercentProgressLabel.setObjectName("scanPercentProgressLabel")
242 | self.horizontalLayout_6.addWidget(self.scanPercentProgressLabel)
243 | self.gridLayout.addLayout(self.horizontalLayout_6, 4, 0, 1, 1)
244 | # self.memScanResultTableWidget = QtWidgets.QTableWidget(parent=Form)
245 | self.memScanResultTableWidget = MemScanResultTableWidget(Form)
246 | font = QtGui.QFont()
247 | font.setFamily("Courier New")
248 | self.memScanResultTableWidget.setFont(font)
249 | self.memScanResultTableWidget.setObjectName("memScanResultTableWidget")
250 | self.memScanResultTableWidget.setColumnCount(7)
251 | self.memScanResultTableWidget.setRowCount(0)
252 | item = QtWidgets.QTableWidgetItem()
253 | self.memScanResultTableWidget.setHorizontalHeaderItem(0, item)
254 | item = QtWidgets.QTableWidgetItem()
255 | self.memScanResultTableWidget.setHorizontalHeaderItem(1, item)
256 | item = QtWidgets.QTableWidgetItem()
257 | self.memScanResultTableWidget.setHorizontalHeaderItem(2, item)
258 | item = QtWidgets.QTableWidgetItem()
259 | self.memScanResultTableWidget.setHorizontalHeaderItem(3, item)
260 | item = QtWidgets.QTableWidgetItem()
261 | self.memScanResultTableWidget.setHorizontalHeaderItem(4, item)
262 | item = QtWidgets.QTableWidgetItem()
263 | self.memScanResultTableWidget.setHorizontalHeaderItem(5, item)
264 | item = QtWidgets.QTableWidgetItem()
265 | self.memScanResultTableWidget.setHorizontalHeaderItem(6, item)
266 | self.gridLayout.addWidget(self.memScanResultTableWidget, 5, 0, 1, 1)
267 | self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
268 | self.horizontalLayout_7.setContentsMargins(-1, 0, -1, 0)
269 | self.horizontalLayout_7.setSpacing(7)
270 | self.horizontalLayout_7.setObjectName("horizontalLayout_7")
271 | self.label_11 = QtWidgets.QLabel(parent=Form)
272 | self.label_11.setMinimumSize(QtCore.QSize(90, 0))
273 | self.label_11.setMaximumSize(QtCore.QSize(120, 16777215))
274 | font = QtGui.QFont()
275 | font.setFamily("Courier New")
276 | font.setPointSize(9)
277 | self.label_11.setFont(font)
278 | self.label_11.setObjectName("label_11")
279 | self.horizontalLayout_7.addWidget(self.label_11)
280 | self.memScanExcludePath = QtWidgets.QTextEdit(parent=Form)
281 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred)
282 | sizePolicy.setHorizontalStretch(0)
283 | sizePolicy.setVerticalStretch(1)
284 | sizePolicy.setHeightForWidth(self.memScanExcludePath.sizePolicy().hasHeightForWidth())
285 | self.memScanExcludePath.setSizePolicy(sizePolicy)
286 | self.memScanExcludePath.setMinimumSize(QtCore.QSize(190, 26))
287 | self.memScanExcludePath.setMaximumSize(QtCore.QSize(16777215, 26))
288 | self.memScanExcludePath.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
289 | self.memScanExcludePath.setTabChangesFocus(True)
290 | self.memScanExcludePath.setAcceptRichText(False)
291 | self.memScanExcludePath.setObjectName("memScanExcludePath")
292 | self.horizontalLayout_7.addWidget(self.memScanExcludePath)
293 | self.gridLayout.addLayout(self.horizontalLayout_7, 1, 0, 1, 1)
294 |
295 | self.retranslateUi(Form)
296 | QtCore.QMetaObject.connectSlotsByName(Form)
297 |
298 | def retranslateUi(self, Form):
299 | _translate = QtCore.QCoreApplication.translate
300 | Form.setWindowTitle(_translate("Form", "Scan Result"))
301 | self.startScanBtn.setText(_translate("Form", "First Scan"))
302 | self.nextScanBtn.setText(_translate("Form", "Next Scan"))
303 | self.stopScanBtn.setText(_translate("Form", "Stop Scan"))
304 | item = self.memScanResultTableWidget.horizontalHeaderItem(0)
305 | item.setText(_translate("Form", "Address"))
306 | item = self.memScanResultTableWidget.horizontalHeaderItem(1)
307 | item.setText(_translate("Form", "Value"))
308 | item = self.memScanResultTableWidget.horizontalHeaderItem(2)
309 | item.setText(_translate("Form", "First Scan"))
310 | item = self.memScanResultTableWidget.horizontalHeaderItem(3)
311 | item.setText(_translate("Form", "-"))
312 | item = self.memScanResultTableWidget.horizontalHeaderItem(4)
313 | item.setText(_translate("Form", "-"))
314 | item = self.memScanResultTableWidget.horizontalHeaderItem(5)
315 | item.setText(_translate("Form", "-"))
316 | item = self.memScanResultTableWidget.horizontalHeaderItem(6)
317 | item.setText(_translate("Form", "-"))
318 | self.label_11.setText(_translate("Form", "Exclude Path:"))
319 | self.memScanExcludePath.setPlaceholderText(_translate("Form", "e.g., \\/system\\/|\\/dev\\/"))
320 |
--------------------------------------------------------------------------------
/scripts/dump-ios-module.js:
--------------------------------------------------------------------------------
1 | // 合作&交流 git:https://github.com/lich4 个人Q:571652571 Q群:560017652
2 |
3 | /*
4 | Usage: dumpModule("BWA.app"); dumpModule("aaa.dylib")
5 | [iPhone::PID::20457]-> dumpModule(".app")
6 | Fix decrypted at:ac0
7 | Fix decrypted at:4000
8 | */
9 |
10 | var O_RDONLY = 0;
11 | var O_WRONLY = 1;
12 | var O_RDWR = 2;
13 | var O_CREAT = 512;
14 |
15 | var SEEK_SET = 0;
16 | var SEEK_CUR = 1;
17 | var SEEK_END = 2;
18 |
19 | function allocStr(str) {
20 | return Memory.allocUtf8String(str);
21 | }
22 |
23 | function getU32(addr) {
24 | if (typeof addr == "number") {
25 | addr = ptr(addr);
26 | }
27 | return Memory.readU32(addr);
28 | }
29 |
30 | function putU64(addr, n) {
31 | if (typeof addr == "number") {
32 | addr = ptr(addr);
33 | }
34 | return Memory.writeU64(addr, n);
35 | }
36 |
37 | function malloc(size) {
38 | return Memory.alloc(size);
39 | }
40 |
41 | function getExportFunction(type, name, ret, args) {
42 | var nptr;
43 | nptr = Module.findExportByName(null, name);
44 | if (nptr === null) {
45 | console.log("cannot find " + name);
46 | return null;
47 | } else {
48 | if (type === "f") {
49 | var funclet = new NativeFunction(nptr, ret, args);
50 | if (typeof funclet === "undefined") {
51 | console.log("parse error " + name);
52 | return null;
53 | }
54 | return funclet;
55 | } else if (type === "d") {
56 | var datalet = Memory.readPointer(nptr);
57 | if (typeof datalet === "undefined") {
58 | console.log("parse error " + name);
59 | return null;
60 | }
61 | return datalet;
62 | }
63 | }
64 | }
65 |
66 | var NSSearchPathForDirectoriesInDomains = getExportFunction("f", "NSSearchPathForDirectoriesInDomains", "pointer", ["int", "int", "int"]);
67 | var wrapper_open = getExportFunction("f", "open", "int", ["pointer", "int", "int"]);
68 | var read = getExportFunction("f", "read", "int", ["int", "pointer", "int"]);
69 | var write = getExportFunction("f", "write", "int", ["int", "pointer", "int"]);
70 | var lseek = getExportFunction("f", "lseek", "int64", ["int", "int64", "int"]);
71 | var close = getExportFunction("f", "close", "int", ["int"]);
72 | var unlink = getExportFunction("f", "unlink", "int", ["pointer"])
73 |
74 | function getCacheDir(index) {
75 | var NSUserDomainMask = 1;
76 | var npdirs = NSSearchPathForDirectoriesInDomains(index, NSUserDomainMask, 1);
77 | var len = ObjC.Object(npdirs).count();
78 | if (len == 0) {
79 | return '';
80 | }
81 | return ObjC.Object(npdirs).objectAtIndex_(0).toString();
82 | }
83 |
84 | function open(pathname, flags, mode) {
85 | if (typeof pathname == "string") {
86 | pathname = allocStr(pathname);
87 | }
88 | return wrapper_open(pathname, flags, mode);
89 | }
90 |
91 | // Export function
92 | var modules = null;
93 | function getAllAppModules() {
94 | if (modules == null) {
95 | modules = new Array();
96 | var tmpmods = Process.enumerateModulesSync();
97 | for (var i = 0; i < tmpmods.length; i++) {
98 | if (tmpmods[i].path.indexOf(".app") != -1) {
99 | modules.push(tmpmods[i]);
100 | }
101 | }
102 | }
103 | return modules;
104 | }
105 |
106 | var MH_MAGIC = 0xfeedface;
107 | var MH_CIGAM = 0xcefaedfe;
108 | var MH_MAGIC_64 = 0xfeedfacf;
109 | var MH_CIGAM_64 = 0xcffaedfe;
110 | var LC_SEGMENT = 0x1;
111 | var LC_SEGMENT_64 = 0x19;
112 | var LC_ENCRYPTION_INFO = 0x21;
113 | var LC_ENCRYPTION_INFO_64 = 0x2C;
114 |
115 | var export_dumpmodpath;
116 | // You can dump .app or dylib (Encrypt/No Encrypt)
117 | rpc.exports = {
118 | dumpModule: function dumpModule(name) {
119 | if (modules == null) {
120 | modules = getAllAppModules();
121 | }
122 | var targetmod = null;
123 | for (var i = 0; i < modules.length; i++) {
124 | if (modules[i].path.indexOf(name) != -1) {
125 | targetmod = modules[i];
126 | break;
127 | }
128 | }
129 | if (targetmod == null) {
130 | console.log("Cannot find module");
131 | return -1;
132 | }
133 | var modbase = modules[i].base;
134 | var modsize = modules[i].size;
135 | var newmodname = modules[i].name + ".decrypted";
136 | var finddir = false;
137 | var newmodpath = "";
138 | var fmodule = -1;
139 | var index = 1;
140 | while (!finddir) { // 找到一个可写路径
141 | try {
142 | var base = getCacheDir(index);
143 | if (base != null) {
144 | newmodpath = getCacheDir(index) + "/" + newmodname;
145 | fmodule = open(newmodpath, O_CREAT | O_RDWR, 0);
146 | if (fmodule != -1) {
147 | break;
148 | };
149 | }
150 | }
151 | catch(e) {
152 | }
153 | index++;
154 | }
155 |
156 | var oldmodpath = modules[i].path;
157 | var foldmodule = open(oldmodpath, O_RDONLY, 0);
158 | if (fmodule == -1 || foldmodule == -1) {
159 | console.log("Cannot open file" + newmodpath);
160 | return 0;
161 | }
162 |
163 | var BUFSIZE = 4096;
164 | var buffer = malloc(BUFSIZE);
165 | while (read(foldmodule, buffer, BUFSIZE)) {
166 | write(fmodule, buffer, BUFSIZE);
167 | }
168 |
169 | // Find crypt info and recover
170 | var is64bit = false;
171 | var size_of_mach_header = 0;
172 | var magic = getU32(modbase);
173 | if (magic == MH_MAGIC || magic == MH_CIGAM) {
174 | is64bit = false;
175 | size_of_mach_header = 28;
176 | }
177 | else if (magic == MH_MAGIC_64 || magic == MH_CIGAM_64) {
178 | is64bit = true;
179 | size_of_mach_header = 32;
180 | }
181 | // ncmds offset 0x10
182 | var ncmds = getU32(modbase.add(16));
183 | // size_of_mach_header == 0x20 이므로 Commands offset 0x20
184 | var off = size_of_mach_header;
185 | var offset_cryptoff = -1;
186 | var crypt_off = 0;
187 | var crypt_size = 0;
188 | var segments = [];
189 | for (var i = 0; i < ncmds; i++) {
190 | var cmd = getU32(modbase.add(off));
191 | var cmdsize = getU32(modbase.add(off + 4));
192 | if (cmd == LC_ENCRYPTION_INFO || cmd == LC_ENCRYPTION_INFO_64) {
193 | offset_cryptoff = off + 8;
194 | crypt_off = getU32(modbase.add(off + 8));
195 | crypt_size = getU32(modbase.add(off + 12));
196 | }
197 | off += cmdsize;
198 | }
199 |
200 | if (offset_cryptoff != -1) {
201 | var tpbuf = malloc(8);
202 | console.log("Fix decrypted at:" + offset_cryptoff.toString(16));
203 | putU64(tpbuf, 0);
204 | lseek(fmodule, offset_cryptoff, SEEK_SET);
205 | write(fmodule, tpbuf, 8);
206 | console.log("Fix decrypted at:" + crypt_off.toString(16));
207 | lseek(fmodule, crypt_off, SEEK_SET);
208 | write(fmodule, modbase.add(crypt_off), crypt_size);
209 | }
210 | console.log("Decrypted file at:" + newmodpath + " 0x" + modsize.toString(16));
211 | close(fmodule);
212 | close(foldmodule);
213 | export_dumpmodpath = newmodpath
214 | return 1;
215 | },
216 | dumpModulePath: () => { return export_dumpmodpath },
217 | }
218 |
--------------------------------------------------------------------------------
/scripts/dump-so.js:
--------------------------------------------------------------------------------
1 | const ensureCodeReadableModule = new CModule(`
2 | #include
3 |
4 | void ensure_code_readable(gconstpointer address, gsize size) {
5 | gum_ensure_code_readable(address, size);
6 | }
7 | `);
8 |
9 | var ensure_code_readable = new NativeFunction(ensureCodeReadableModule.ensure_code_readable, 'void', ['pointer', 'uint64']);
10 |
11 | rpc.exports = {
12 | findModule: function(so_name) {
13 | var libso = Process.findModuleByName(so_name);
14 | if (libso == null) {
15 | return -1;
16 | }
17 | return libso;
18 | },
19 | dumpModule: function(so_name) {
20 | var libso = Process.findModuleByName(so_name);
21 | if (libso == null) {
22 | return -1;
23 | }
24 |
25 | ensure_code_readable(ptr(libso.base), libso.size);
26 |
27 | var libso_buffer = ptr(libso.base).readByteArray(libso.size);
28 | return libso_buffer;
29 | },
30 | dumpModuleChunk: function(offset, size) {
31 | ensure_code_readable(ptr(offset), size);
32 |
33 | var chunk = ptr(offset).readByteArray(size);
34 | return chunk;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/scripts/full-memory-dump.js:
--------------------------------------------------------------------------------
1 | var isPalera1n;
2 |
3 | rpc.exports = {
4 | getPlatform: function() {
5 | return Process.platform;
6 | },
7 | isPalera1nJb: function() {
8 | var access = new NativeFunction(Module.findExportByName(null, "access"), 'int', ['pointer', 'int'])
9 | var path = Memory.allocUtf8String("/var/mobile/Library/palera1n/helper");
10 | isPalera1n = access(path, 0) === 0
11 | return isPalera1n;
12 | },
13 | enumerateRanges: function (prot) {
14 | return Process.enumerateRangesSync(prot);
15 | },
16 | readMemory: function (address, size) {
17 | if (ObjC.available && isPalera1n) {
18 | try {
19 | // Looks up a memory range by address. Throws an exception if not found.
20 | var range = Process.getRangeByAddress(ptr(address));
21 | return Memory.readByteArray(range.base, range.size);
22 | } catch (e) {
23 | console.log(e);
24 | }
25 | }
26 | else {
27 | return Memory.readByteArray(ptr(address), size);
28 | }
29 | },
30 | readMemoryChunk: function (address, size) {
31 | try {
32 | // Looks up a memory range by address. Throws an exception if not found.
33 | Process.getRangeByAddress(ptr(address));
34 | return Memory.readByteArray(ptr(address), size);
35 | } catch (e) {
36 | console.log(e);
37 | }
38 | }
39 | };
--------------------------------------------------------------------------------
/spawn.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import frida
4 | from PyQt6 import QtCore, QtWidgets
5 | from PyQt6.QtCore import Qt, pyqtSlot, QEvent
6 | from PyQt6.QtWidgets import QMessageBox, QApplication
7 |
8 | import spawn_ui
9 |
10 |
11 | class SpawnDialogClass(QtWidgets.QDialog):
12 | attach_target_name_signal = QtCore.pyqtSignal(str)
13 | spawn_target_id_signal = QtCore.pyqtSignal(str)
14 |
15 | def __init__(self):
16 | super(SpawnDialogClass, self).__init__()
17 | self.is_pid_list_checked = False
18 | self.application_list = None
19 | self.spawn_target_id = None
20 | self.spawn_dialog = QtWidgets.QDialog()
21 | self.spawn_dialog.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
22 | # self.spawn_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
23 | self.spawn_ui = spawn_ui.Ui_SpawnDialogUi()
24 | self.spawn_ui.setupUi(self.spawn_dialog)
25 | self.spawn_ui.remoteAddrInput.returnPressed.connect(self.get_app_list)
26 | self.spawn_ui.spawnTargetIdInput.returnPressed.connect(self.set_spawn_target)
27 | self.spawn_ui.spawnTargetIdInput.textChanged.connect(self.search_target)
28 | self.spawn_ui.spawnBtn.clicked.connect(self.spawn_launch)
29 | self.spawn_ui.appListBtn.clicked.connect(self.get_app_list)
30 | self.spawn_ui.appListBrowser.target_id_clicked_signal.connect(self.target_id_clicked_sig_func)
31 | self.spawn_dialog.show()
32 |
33 | self.interested_widgets = []
34 | QApplication.instance().installEventFilter(self)
35 |
36 | @pyqtSlot(str)
37 | def target_id_clicked_sig_func(self, sig: str):
38 | spawn_target_id_input = self.spawn_ui.spawnTargetIdInput
39 | spawn_target_id_input.setText(sig[sig.find("\t"):].strip()) if self.is_pid_list_checked else spawn_target_id_input.setText(sig[:sig.find("\t")])
40 |
41 | def set_spawn_target(self):
42 | self.spawn_target_id = self.spawn_ui.spawnTargetIdInput.text().strip()
43 | self.spawn_ui.spawnBtn.setFocus()
44 |
45 | def spawn_launch(self):
46 | if self.spawn_target_id is None:
47 | self.spawn_target_id = self.spawn_ui.spawnTargetIdInput.text().strip()
48 | btn_name = self.spawn_ui.spawnBtn.text()
49 | sig = self.spawn_target_id_signal if btn_name == "Spawn" else self.attach_target_name_signal
50 | sig.emit(self.spawn_target_id)
51 |
52 | def get_app_list(self):
53 | if self.spawn_ui.remoteAddrInput.isEnabled() is False:
54 | try:
55 | device = frida.get_usb_device(1)
56 | except Exception as e:
57 | print(f"[spawn] {e}")
58 | return
59 | else:
60 | IP = self.spawn_ui.remoteAddrInput.text().strip()
61 | if re.search(r"^\d+\.\d+\.\d+\.\d+:\d+$", IP) is None:
62 | QMessageBox.information(self, "info", "Enter IP:PORT")
63 | return
64 | try:
65 | device = frida.get_device_manager().add_remote_device(IP)
66 | except Exception as e:
67 | print(f"[spawn] {e}")
68 | return
69 | try:
70 | enumeration_function = device.enumerate_processes if self.is_pid_list_checked else device.enumerate_applications
71 | self.application_list = [app for app in enumeration_function()]
72 | except Exception as e:
73 | print(f"[spawn] {e}")
74 | return
75 |
76 | app_list_text = ''
77 | for app in self.application_list:
78 | app_list_text += (str(app.pid) + '\t' + app.name + '\n') if self.is_pid_list_checked \
79 | else (app.identifier + '\t' + app.name + '\n')
80 |
81 | self.spawn_ui.appListBrowser.setText(app_list_text)
82 |
83 | def search_target(self):
84 | if self.application_list is None:
85 | return
86 |
87 | if len(self.application_list) > 0:
88 | app_list_text = ''
89 | for app in self.application_list:
90 | appid = str(app.pid) if self.is_pid_list_checked else app.identifier
91 | appname = app.name
92 | if appid.lower().find(self.spawn_ui.spawnTargetIdInput.text().lower()) != -1 or appname.lower().find(self.spawn_ui.spawnTargetIdInput.text().lower()) != -1:
93 | app_list_text += appid + '\t' + appname + '\n'
94 | self.spawn_ui.appListBrowser.setText(app_list_text)
95 |
96 | def eventFilter(self, obj, event):
97 | self.interested_widgets = [self.spawn_ui.spawnTargetIdInput]
98 | if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab:
99 | try:
100 | if self.spawn_ui.remoteAddrInput.isEnabled():
101 | self.interested_widgets.append(self.spawn_ui.remoteAddrInput)
102 | index = self.interested_widgets.index(self.spawn_dialog.focusWidget())
103 |
104 | self.interested_widgets[(index + 1) % len(self.interested_widgets)].setFocus()
105 | except ValueError:
106 | self.interested_widgets[0].setFocus()
107 |
108 | return True
109 |
110 | return super().eventFilter(obj, event)
111 |
--------------------------------------------------------------------------------
/spawn.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | SpawnDialogUi
4 |
5 |
6 |
7 | 0
8 | 0
9 | 317
10 | 435
11 |
12 |
13 |
14 |
15 | Courier New
16 |
17 |
18 |
19 | App List
20 |
21 |
22 | -
23 |
24 |
25 | Qt::NoFocus
26 |
27 |
28 | List
29 |
30 |
31 |
32 | -
33 |
34 |
35 | Qt::NoFocus
36 |
37 |
38 | Spawn
39 |
40 |
41 |
42 | -
43 |
44 |
45 | Identifier Name
46 |
47 |
48 |
49 | -
50 |
51 |
52 | -
53 |
54 |
55 | true
56 |
57 |
58 | Qt::StrongFocus
59 |
60 |
61 |
62 |
63 |
64 | true
65 |
66 |
67 | IP:PORT
68 |
69 |
70 |
71 | -
72 |
73 |
74 | Qt::StrongFocus
75 |
76 |
77 |
78 |
79 |
80 | com.example.test
81 |
82 |
83 |
84 | -
85 |
86 |
87 |
88 | 0
89 | 10
90 |
91 |
92 |
93 | Spawn && Mem Patch
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/spawn_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'spawn.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.4.0
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 |
8 | from PyQt6 import QtGui, QtCore, QtWidgets
9 | from PyQt6.QtWidgets import QTextBrowser
10 |
11 |
12 | class AppListBrowserClass(QTextBrowser):
13 | target_id_clicked_signal = QtCore.pyqtSignal(str)
14 |
15 | def __init__(self, args):
16 | super(AppListBrowserClass, self).__init__(args)
17 |
18 | def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
19 | super(AppListBrowserClass, self).mousePressEvent(e)
20 | pos = e.pos()
21 | tc = self.cursorForPosition(pos)
22 | self.target_id_clicked_signal.emit(tc.block().text())
23 |
24 |
25 | class Ui_SpawnDialogUi(object):
26 | def setupUi(self, SpawnDialogUi):
27 | SpawnDialogUi.setObjectName("SpawnDialogUi")
28 | SpawnDialogUi.resize(317, 435)
29 | font = QtGui.QFont()
30 | font.setFamily("Courier New")
31 | SpawnDialogUi.setFont(font)
32 | self.gridLayout = QtWidgets.QGridLayout(SpawnDialogUi)
33 | self.gridLayout.setObjectName("gridLayout")
34 | # self.appListBrowser = QtWidgets.QTextBrowser(SpawnDialogUi)
35 | self.appListBrowser = AppListBrowserClass(SpawnDialogUi)
36 | self.appListBrowser.setObjectName("appListBrowser")
37 | self.gridLayout.addWidget(self.appListBrowser, 1, 0, 1, 2)
38 | self.spawnBtn = QtWidgets.QPushButton(SpawnDialogUi)
39 | self.spawnBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
40 | self.spawnBtn.setObjectName("spawnBtn")
41 | self.gridLayout.addWidget(self.spawnBtn, 3, 1, 1, 1)
42 | self.spawnTargetIdInput = QtWidgets.QLineEdit(SpawnDialogUi)
43 | self.spawnTargetIdInput.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
44 | self.spawnTargetIdInput.setObjectName("spawnTargetIdInput")
45 | self.gridLayout.addWidget(self.spawnTargetIdInput, 3, 0, 1, 1)
46 | self.appListLabel = QtWidgets.QLabel(SpawnDialogUi)
47 | self.appListLabel.setObjectName("appListLabel")
48 | self.appListLabel.setIndent(2)
49 | self.gridLayout.addWidget(self.appListLabel, 0, 0, 1, 2)
50 | self.remoteAddrInput = QtWidgets.QLineEdit(SpawnDialogUi)
51 | self.remoteAddrInput.setEnabled(True)
52 | self.remoteAddrInput.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
53 | self.remoteAddrInput.setFrame(True)
54 | self.remoteAddrInput.setObjectName("remoteAddrInput")
55 | self.gridLayout.addWidget(self.remoteAddrInput, 2, 0, 1, 1)
56 | self.appListBtn = QtWidgets.QPushButton(SpawnDialogUi)
57 | self.appListBtn.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus)
58 | self.appListBtn.setObjectName("appListBtn")
59 | self.gridLayout.addWidget(self.appListBtn, 2, 1, 1, 1)
60 |
61 | self.retranslateUi(SpawnDialogUi)
62 | QtCore.QMetaObject.connectSlotsByName(SpawnDialogUi)
63 |
64 | def retranslateUi(self, SpawnDialogUi):
65 | _translate = QtCore.QCoreApplication.translate
66 | SpawnDialogUi.setWindowTitle(_translate("SpawnDialogUi", "App List"))
67 | self.spawnBtn.setText(_translate("SpawnDialogUi", "Spawn"))
68 | self.spawnTargetIdInput.setPlaceholderText(_translate("SpawnDialogUi", "com.example.test"))
69 | self.appListLabel.setText(_translate("SpawnDialogUi", "Identifier Name"))
70 | self.remoteAddrInput.setPlaceholderText(_translate("SpawnDialogUi", "IP:PORT"))
71 | self.appListBtn.setText(_translate("SpawnDialogUi", "List"))
72 |
--------------------------------------------------------------------------------
/watchpoint.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import re
3 |
4 | from PyQt6 import QtGui
5 | from PyQt6.QtCore import Qt, pyqtSlot
6 | from PyQt6.QtGui import QColor, QPalette, QTextCursor
7 | from PyQt6.QtWidgets import QWidget, QLabel
8 | from capstone import *
9 |
10 | import gvar
11 | import watchpoint_ui
12 |
13 |
14 | class WatchPointWidget(QWidget):
15 | def __init__(self):
16 | super(WatchPointWidget, self).__init__()
17 | self.watch_point_ui = watchpoint_ui.Ui_Form()
18 | self.watch_point_ui.setupUi(self)
19 | self.watch_point_ui.watchpointResult.setReadOnly(True)
20 | self.watch_point_ui.disassemResult.setReadOnly(True)
21 | self.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint)
22 |
23 | self.watch_point_ui.watchpointSetButton.clicked.connect(lambda: self.set_watchpoint(
24 | self.watch_point_ui.watchpointSetButton.text()))
25 |
26 | self.watchpoint_addr = None
27 | self.watchpoint_signal_connected = False
28 | self.disasm_result = None
29 |
30 | @pyqtSlot(tuple)
31 | def watchpoint_sig_func(self, sig: tuple): # sig --> (addr, stat) or (what, how, where, what_hexdump, thread_id, thread_name)
32 | if len(sig) == 2 and sig[1] == 1:
33 | result_text = f"HardwareWatchpoint set at {sig[0]}"
34 | self.watch_point_ui.watchpointSetButton.setText('Unset')
35 | self.watch_point_ui.disassemResult.clear()
36 | else:
37 | result_text = f"{sig[0]} tried to \"{'write' if sig[1] == 'w' else 'read'}\" at {sig[2]} ({sig[4]} {sig[5]})"
38 | hex_dump_result = sig[3]
39 | hex_dump_result = hex_dump_result[hex_dump_result.find('\n') + 1:]
40 | addr = hex_dump_result[:hex_dump_result.find(' ')]
41 | self.disassemble(gvar.arch, addr, hex_dump_result)
42 | self.watch_point_ui.watchpointResult.setText(result_text)
43 |
44 | def closeEvent(self, event: QtGui.QCloseEvent) -> None:
45 | if self.watch_point_ui.watchpointSetButton.text() == 'Unset':
46 | self.unset_watchpoint()
47 | super().closeEvent(event)
48 |
49 | def disassemble(self, arch: str, addr: str, hex_dump_result: str):
50 | hex_dump_result = hex_dump_result.replace('\u2029', '\n')
51 | lines = hex_dump_result.strip().split('\n')
52 |
53 | hex_data = []
54 | for line in lines:
55 | # Calculate hex start and end positions
56 | hex_start = len(line) - 65
57 | hex_end = len(line) - 16
58 |
59 | # Extract hex part
60 | hex_part = line[hex_start:hex_end]
61 |
62 | # Extract two-digit hex numbers from the part
63 | matches = re.findall(r'\b[0-9a-fA-F]{2}\b', hex_part)
64 | hex_data.append(' '.join(matches))
65 |
66 | hex_string = ' '.join(hex_data).split()
67 | disasm_target = b''.join(bytes([int(hex_value, 16)]) for hex_value in hex_string)
68 |
69 | if arch == "arm64":
70 | md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
71 | elif arch == "arm":
72 | md = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
73 | self.watch_point_ui.disassemResult.clear()
74 | for (address, size, mnemonic, op_str) in md.disasm_lite(disasm_target, int(addr, 16)):
75 | _addr = f"%x" % address
76 | _mnemonic = f"%s" % mnemonic
77 | _op_str = f"%s" % op_str
78 | if int(_addr, 16) == (int(addr, 16) + 16):
79 | # print("0x%x\t%s\t%s" % (address, mnemonic, op_str))
80 | color = QColor("red")
81 | mnemonic_space = " " * (8 - len(_mnemonic))
82 | formatted_text = f'' \
83 | f'{_addr}{" " * 4}{_mnemonic}{mnemonic_space}{_op_str}'
84 | self.watch_point_ui.disassemResult.append(formatted_text)
85 | else:
86 | mnemonic_space = " " * (8 - len(_mnemonic))
87 | formatted_text = f'{_addr}{" " * 4}{_mnemonic}{mnemonic_space}{_op_str}'
88 | self.watch_point_ui.disassemResult.append(formatted_text)
89 |
90 | self.watch_point_ui.disassemResult.moveCursor(QTextCursor.MoveOperation.Start)
91 |
92 | def set_watchpoint(self, button_text: str):
93 | if button_text == 'Set':
94 | self.watchpoint_addr = self.watch_point_ui.watchpointAddrInput.text()
95 | if self.watchpoint_addr != '' and '0x' not in self.watchpoint_addr:
96 | self.watchpoint_addr = ''.join(('0x', self.watchpoint_addr))
97 | hex_regex_pattern = r'(\b0x[a-fA-F0-9]+\b)'
98 | hex_regex = re.compile(hex_regex_pattern)
99 | if not hex_regex.match(self.watchpoint_addr):
100 | print(f"[watchpoint][set_watchpoint] invalid address")
101 | return
102 | watchpoint_size_text = self.watch_point_ui.watchpointSizeComboBox.currentText()
103 | if watchpoint_size_text == 'Size':
104 | return
105 | watchpoint_size: int = int(watchpoint_size_text)
106 | watchpoint_type_text = self.watch_point_ui.watchpointTypeComboBox.currentText()
107 | if watchpoint_size == 'Size' or watchpoint_type_text == 'Type':
108 | return
109 | watchpoint_type = 'w' if watchpoint_type_text == 'Write' else 'r'
110 | try:
111 | threads_list = gvar.frida_instrument.get_process_threads()
112 | gvar.enum_threads = threads_list
113 | except Exception as e:
114 | print(f"[watchpoint]{inspect.currentframe().f_code.co_name}: {e}")
115 | return
116 | if not threads_list:
117 | self.watch_point_ui.watchpointResult.setText("Process threads are protected. Cannot set watchpoint")
118 | else:
119 | try:
120 | if self.watchpoint_signal_connected is False:
121 | gvar.frida_instrument.watchpoint_signal.connect(self.watchpoint_sig_func)
122 | self.watchpoint_signal_connected = True
123 | gvar.frida_instrument.set_watchpoint(self.watchpoint_addr, watchpoint_size, watchpoint_type)
124 | except Exception as e:
125 | print(f"[watchpoint]{inspect.currentframe().f_code.co_name}: {e}")
126 | return
127 | self.watch_point_ui.watchpointSetButton.setText('Unset')
128 | else:
129 | self.unset_watchpoint()
130 |
131 | def unset_watchpoint(self):
132 | try:
133 | gvar.frida_instrument.stop_watchpoint()
134 | gvar.frida_instrument.watchpoint_signal.disconnect()
135 | except Exception as e:
136 | print(f"[watchpoint]{inspect.currentframe().f_code.co_name}: {e}")
137 | result_text = f"{self.watchpoint_addr} Watchpoint stopped"
138 | self.watchpoint_signal_connected = False
139 | self.watch_point_ui.watchpointResult.setText(result_text)
140 | self.watch_point_ui.watchpointSetButton.setText('Set')
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/watchpoint_ui.py:
--------------------------------------------------------------------------------
1 | # Form implementation generated from reading ui file 'watchpoint_ui.ui'
2 | #
3 | # Created by: PyQt6 UI code generator 6.4.0
4 | #
5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is
6 | # run again. Do not edit this file unless you know what you are doing.
7 | import re
8 |
9 | from PyQt6 import QtCore, QtGui, QtWidgets
10 | from PyQt6.QtCore import Qt
11 | from PyQt6.QtWidgets import QTextEdit
12 |
13 |
14 | class DisassemResultTextEdit(QTextEdit):
15 | watchpoint_addr_clicked_signal = QtCore.pyqtSignal(str)
16 |
17 | def __init__(self, parent=None):
18 | super(DisassemResultTextEdit, self).__init__(parent)
19 |
20 | def mousePressEvent(self, event):
21 | super().mousePressEvent(event)
22 | if event.button() == Qt.MouseButton.LeftButton:
23 | tc = self.textCursor()
24 | hex_regex_pattern = r'(\b0x[a-fA-F0-9]+\b|\b[a-fA-F0-9]{6,}\b)'
25 | hex_regex = re.compile(hex_regex_pattern)
26 | addr_match = hex_regex.match(tc.block().text())
27 | if addr_match is not None:
28 | self.watchpoint_addr_clicked_signal.emit(addr_match[0])
29 |
30 |
31 | class Ui_Form(object):
32 | def setupUi(self, Form):
33 | Form.setObjectName("Form")
34 | Form.resize(480, 300)
35 | self.gridLayout = QtWidgets.QGridLayout(Form)
36 | self.gridLayout.setObjectName("gridLayout")
37 | self.watchpointTypeComboBox = QtWidgets.QComboBox(Form)
38 | self.watchpointTypeComboBox.setObjectName("watchpointTypeComboBox")
39 | self.watchpointTypeComboBox.addItem("")
40 | self.watchpointTypeComboBox.addItem("")
41 | self.watchpointTypeComboBox.addItem("")
42 | self.gridLayout.addWidget(self.watchpointTypeComboBox, 1, 2, 1, 1)
43 | # self.disassemResult = QtWidgets.QTextEdit(Form)
44 | self.disassemResult = DisassemResultTextEdit()
45 | font = QtGui.QFont()
46 | font.setFamily("Courier New")
47 | self.disassemResult.setFont(font)
48 | self.disassemResult.setObjectName("disassemResult")
49 | self.gridLayout.addWidget(self.disassemResult, 3, 0, 1, 4)
50 | self.watchpointResult = QtWidgets.QTextEdit(Form)
51 | self.watchpointResult.setMinimumSize(QtCore.QSize(0, 0))
52 | self.watchpointResult.setMaximumSize(QtCore.QSize(16777215, 30))
53 | font = QtGui.QFont()
54 | font.setFamily(".AppleSystemUIFont")
55 | self.watchpointResult.setFont(font)
56 | self.watchpointResult.setObjectName("watchpointResult")
57 | self.gridLayout.addWidget(self.watchpointResult, 2, 0, 1, 4)
58 | self.watchpointSetButton = QtWidgets.QPushButton(Form)
59 | self.watchpointSetButton.setMinimumSize(QtCore.QSize(0, 0))
60 | self.watchpointSetButton.setObjectName("watchpointSetButton")
61 | self.gridLayout.addWidget(self.watchpointSetButton, 1, 3, 1, 1)
62 | self.watchpointAddrInput = QtWidgets.QLineEdit(Form)
63 | self.watchpointAddrInput.setMinimumSize(QtCore.QSize(0, 25))
64 | font = QtGui.QFont()
65 | font.setFamily("Courier New")
66 | self.watchpointAddrInput.setFont(font)
67 | self.watchpointAddrInput.setObjectName("watchpointAddrInput")
68 | self.gridLayout.addWidget(self.watchpointAddrInput, 1, 0, 1, 1)
69 | self.watchpointSizeComboBox = QtWidgets.QComboBox(Form)
70 | self.watchpointSizeComboBox.setCurrentText("")
71 | self.watchpointSizeComboBox.setObjectName("watchpointSizeComboBox")
72 | self.watchpointSizeComboBox.addItem("")
73 | self.watchpointSizeComboBox.addItem("")
74 | self.watchpointSizeComboBox.addItem("")
75 | self.gridLayout.addWidget(self.watchpointSizeComboBox, 1, 1, 1, 1)
76 |
77 | self.retranslateUi(Form)
78 | QtCore.QMetaObject.connectSlotsByName(Form)
79 |
80 | def retranslateUi(self, Form):
81 | _translate = QtCore.QCoreApplication.translate
82 | Form.setWindowTitle(_translate("Form", "Watchpoint"))
83 | self.watchpointTypeComboBox.setPlaceholderText(_translate("Form", "Type"))
84 | self.watchpointTypeComboBox.setItemText(0, _translate("Form", "Type"))
85 | self.watchpointTypeComboBox.setItemText(1, _translate("Form", "Read"))
86 | self.watchpointTypeComboBox.setItemText(2, _translate("Form", "Write"))
87 | self.watchpointSetButton.setText(_translate("Form", "Set"))
88 | self.watchpointAddrInput.setPlaceholderText(_translate("Form", "Address"))
89 | self.watchpointSizeComboBox.setPlaceholderText(_translate("Form", "Size"))
90 | self.watchpointSizeComboBox.setItemText(0, _translate("Form", "Size"))
91 | self.watchpointSizeComboBox.setItemText(1, _translate("Form", "4"))
92 | self.watchpointSizeComboBox.setItemText(2, _translate("Form", "8"))
93 |
94 |
95 | if __name__ == "__main__":
96 | import sys
97 | app = QtWidgets.QApplication(sys.argv)
98 | Form = QtWidgets.QWidget()
99 | ui = Ui_Form()
100 | ui.setupUi(Form)
101 | Form.show()
102 | sys.exit(app.exec())
103 |
--------------------------------------------------------------------------------
/watchpoint_ui.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 474
10 | 306
11 |
12 |
13 |
14 | Watchpoint
15 |
16 |
17 | -
18 |
19 |
20 | Type
21 |
22 |
-
23 |
24 | Type
25 |
26 |
27 | -
28 |
29 | Read
30 |
31 |
32 | -
33 |
34 | Write
35 |
36 |
37 |
38 |
39 | -
40 |
41 |
42 |
43 | Courier New
44 |
45 |
46 |
47 |
48 | -
49 |
50 |
51 |
52 | 0
53 | 0
54 |
55 |
56 |
57 |
58 | 16777215
59 | 30
60 |
61 |
62 |
63 |
64 | .AppleSystemUIFont
65 |
66 |
67 |
68 |
69 | -
70 |
71 |
72 |
73 | 0
74 | 0
75 |
76 |
77 |
78 | Set
79 |
80 |
81 |
82 | -
83 |
84 |
85 |
86 | 0
87 | 25
88 |
89 |
90 |
91 |
92 | Courier New
93 |
94 |
95 |
96 | Address
97 |
98 |
99 |
100 | -
101 |
102 |
103 |
104 |
105 |
106 | Size
107 |
108 |
-
109 |
110 | Size
111 |
112 |
113 | -
114 |
115 | 4
116 |
117 |
118 | -
119 |
120 | 8
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------