├── .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 | ![image](https://github.com/user-attachments/assets/187e95d0-6eef-4ab1-b2c2-b4fd96870802) 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 &amp; 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 &amp; 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 &quot;Prepare&quot; 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 &quot;Prepare&quot; 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 | --------------------------------------------------------------------------------