├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── port.js ├── lib ├── fast-index-of-all.mjs └── nso.mjs ├── package-lock.json ├── package.json └── port.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | private/ 107 | *.pchtxt -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "github.copilot.enable": { 4 | "javascript": true, 5 | "yaml": false, 6 | "plaintext": false, 7 | "markdown": false 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Coxxs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # patch-porter 2 | 3 | A simple `.pchtxt` porting tool. 4 | 5 | ## Usage 6 | 7 | 1. Install [Node.js](https://nodejs.org), then install `patch-porter` using npm: 8 | 9 | ```shell 10 | npm install -g patch-porter 11 | ``` 12 | 13 | 2. Port your pchtxt: 14 | 15 | ```shell 16 | patch-porter --from=mainA --to=mainB --input=A.pchtxt --output=B.pchtxt 17 | ``` 18 | - `--comment`: Add ported address as comment to the output file 19 | - `--arch=arm64`: Set the processor architecture for the NSO file (arm/arm64/none), default: arm64 20 | 21 | 3. Done! 22 | 23 | ## Tips 24 | - Please keep `@flag offset_shift ...` in your pchtxt to help `patch-porter` finding the correct address 25 | - If your pchtxt doesn't have `@flag offset_shift 0x100`, it means that the addresses in your pchtxt are not based on the NSO header offset.\ 26 | In this case, you need to decompress your NSO file using [hactool](https://github.com/SciresM/hactool), and disable NSO mode in `patch-porter` (`--no-nso`). 27 | 28 | ```shell 29 | hactool -t nso --uncompressed mainA.raw mainA 30 | hactool -t nso --uncompressed mainB.raw mainB 31 | patch-porter --from=mainA.raw --to=mainB.raw --input=A.pchtxt --output=B.pchtxt --no-nso 32 | ``` 33 | - After porting, search for `[x]` in new pchtxt to find errors 34 | - `patch-porter` does not currently update the assembly code, so some patch may still need to be updated manually 35 | 36 | ## Use in Node.js 37 | 38 | ```javascript 39 | import { promises as fs } from 'fs' 40 | import { portPchtxt } from 'patch-porter' 41 | 42 | let fileA = await fs.readFile('mainA') 43 | let fileB = await fs.readFile('mainB') 44 | let pchtxtA = await fs.readFile('A.pchtxt', 'utf8') 45 | 46 | let pchtxtB = await portPchtxt(fileA, fileB, pchtxtA) 47 | 48 | await fs.writeFile('B.pchtxt', pchtxtB) 49 | ``` 50 | 51 | ## Credits 52 | 53 | - [disasm-web](https://github.com/CzBiX/disasm-web) 54 | - [capstone](https://github.com/capstone-engine/capstone) 55 | - [IPSwitch](https://github.com/3096/ipswitch) 56 | -------------------------------------------------------------------------------- /bin/port.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const { existsSync } = require('fs'); 3 | const yargs = require('yargs') 4 | const fs = require('fs').promises 5 | 6 | const argv = yargs 7 | .option('from', { 8 | description: 'The decompressed NSO file to port from', 9 | type: 'string', 10 | demandOption: true, 11 | }) 12 | .option('to', { 13 | description: 'The decompressed NSO file to port to', 14 | type: 'string', 15 | demandOption: true, 16 | }) 17 | .option('input', { 18 | description: 'The input pchtxt file', 19 | alias: 'i', 20 | type: 'string', 21 | demandOption: true, 22 | }) 23 | .option('output', { 24 | description: 'Output path for the new pchtxt file', 25 | alias: 'o', 26 | type: 'string', 27 | demandOption: true, 28 | }) 29 | .option('overwrite', { 30 | description: 'Overwrite the output file', 31 | alias: 'w', 32 | type: 'boolean', 33 | default: false, 34 | }) 35 | .option('comment', { 36 | description: 'Add ported address as comment to the output file', 37 | type: 'boolean', 38 | default: false, 39 | }) 40 | .option('no-nso', { 41 | description: 'Disable NSO mode, treat files as raw binary file', 42 | type: 'boolean', 43 | default: false, 44 | }) 45 | .option('nso', { 46 | description: 'Use NSO mode, NSO file will be decompressed automatically in this mode, but you need to set size of NSO header to 0x100 correctly in phxtxt file (@flag offset_shift 0x100)', 47 | type: 'boolean', 48 | default: true, 49 | }) 50 | .option('arch', { 51 | description: 'Set the processor architecture for the NSO file (arm/arm64/none)', 52 | type: 'string', 53 | default: 'arm64', 54 | }) 55 | .help() 56 | .alias('help', 'h').argv; 57 | 58 | (async () => { 59 | const { portPchtxt } = await import('../port.mjs') 60 | 61 | if (argv.from == argv.to) { 62 | console.error('Error: From and to paths are the same') 63 | return 64 | } 65 | 66 | if (existsSync(argv.output) && !argv.overwrite) { 67 | console.error('Error: Output pchtxt already exists, use -w to overwrite') 68 | return 69 | } 70 | 71 | let options = {} 72 | 73 | if (argv.comment != null) { 74 | options.addComment = argv.comment 75 | } 76 | if (argv.arch != null) { 77 | options.arch = argv.arch 78 | } 79 | if (argv.nso != null) { 80 | options.nso = argv.nso 81 | } 82 | if (argv.noNso != null) { 83 | options.nso = !argv.noNso 84 | } 85 | 86 | let fileOld = new Uint8Array(await fs.readFile(argv.from)) 87 | let fileNew = new Uint8Array(await fs.readFile(argv.to)) 88 | 89 | const inputs = Array.isArray(argv.input) ? argv.input : [argv.input] 90 | const outputs = Array.isArray(argv.output) ? argv.output : [argv.output] 91 | 92 | if (inputs.length !== outputs.length) { 93 | console.error('Error: The number of inputs does not match the number of outputs') 94 | } 95 | 96 | for (let i = 0; i < inputs.length; i++) { 97 | const input = inputs[i] 98 | const output = outputs[i] 99 | if (input == output) { 100 | console.error('Error: Input and output paths are the same') 101 | return 102 | } 103 | 104 | let pchtxtOld = await fs.readFile(input, 'utf8') 105 | let pchtxtNew = await portPchtxt(fileOld, fileNew, pchtxtOld, options) 106 | 107 | await fs.writeFile(output, pchtxtNew) 108 | console.log(`Output pchtxt saved to: ${output}`) 109 | } 110 | })() -------------------------------------------------------------------------------- /lib/fast-index-of-all.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Uint8Array} buffer Buffer to search 3 | * @param {Uint8Array} search Buffer to search for 4 | * @returns {Array} Array of indexes 5 | */ 6 | function indexOfAllSlow(buffer, search, start, end, maxCount) { 7 | buffer = Buffer.from(buffer) 8 | search = Buffer.from(search) 9 | const result = [] 10 | let offset = 0 11 | if (start != null && end != null) { 12 | buffer = buffer.subarray(start, end) 13 | } 14 | if (start == null) { 15 | start = 0 16 | } 17 | if (end == null) { 18 | end = buffer.length 19 | } 20 | while (true) { 21 | const index = buffer.indexOf(search, offset) 22 | if (index === -1) break 23 | result.push(index + start) 24 | offset = index + 1 25 | if (maxCount && result.length >= maxCount) break 26 | } 27 | return result 28 | } 29 | 30 | const INDEX_MAP_MAX_LENGTH = 600000 31 | const INDEX_MAP_KEY_BITSIZE = 20 32 | const INDEX_MAP_KEY_BYTESIZE = Math.ceil(INDEX_MAP_KEY_BITSIZE / 8) 33 | const INDEX_MAP_KEY_MAP_SIZE = 1 << INDEX_MAP_KEY_BITSIZE 34 | const INDEX_MAP_KEY_FUNC = (buffer, i) => ((buffer[i] << 12) + (buffer[i + 1] << 4) + (buffer[i + 2] >> 4)) 35 | 36 | function generateIndexMapFast(buffer) { 37 | const count = new Uint32Array(INDEX_MAP_KEY_MAP_SIZE) 38 | for (let i = 0; i <= buffer.length - INDEX_MAP_KEY_BYTESIZE; i++) { 39 | const prefix = INDEX_MAP_KEY_FUNC(buffer, i) 40 | count[prefix]++ 41 | } 42 | 43 | const result = new Array(INDEX_MAP_KEY_MAP_SIZE) 44 | for (let i = 0; i < INDEX_MAP_KEY_MAP_SIZE; i++) { 45 | result[i] = count[i] > INDEX_MAP_MAX_LENGTH ? false : new Uint32Array(count[i]) 46 | } 47 | 48 | const counter = new Uint32Array(INDEX_MAP_KEY_MAP_SIZE) 49 | for (let i = 0; i <= buffer.length - INDEX_MAP_KEY_BYTESIZE; i++) { 50 | const prefix = INDEX_MAP_KEY_FUNC(buffer, i) 51 | if (result[prefix] === false) continue 52 | result[prefix][counter[prefix]] = i 53 | counter[prefix]++ 54 | } 55 | return result 56 | } 57 | 58 | function generateIndexMap(buffer) { 59 | const result = Array.from(new Array(INDEX_MAP_KEY_MAP_SIZE), () => new Array()) 60 | for (let i = 0; i <= buffer.length - INDEX_MAP_KEY_BYTESIZE; i++) { 61 | const prefix = INDEX_MAP_KEY_FUNC(buffer, i) 62 | if (result[prefix] === false) continue 63 | result[prefix].push(i) 64 | if (result[prefix].length > INDEX_MAP_MAX_LENGTH) { 65 | result[prefix] = false 66 | continue 67 | } 68 | } 69 | return result 70 | } 71 | 72 | const caches = new WeakMap() 73 | 74 | /** 75 | * @param {Uint8Array} buffer Buffer to search 76 | * @param {Uint8Array} search Buffer to search for 77 | * @returns {Array} Array of indexes 78 | */ 79 | export function indexOfAll(buffer, search, start, end, maxCount = null) { 80 | if (buffer.length < 0x1000000 || search.length < INDEX_MAP_KEY_BYTESIZE) return indexOfAllSlow(buffer, search, start, end, maxCount) 81 | let cache = caches.get(buffer) 82 | if (!cache) { 83 | console.log('Generating cache...') 84 | cache = generateIndexMapFast(buffer) 85 | caches.set(buffer, cache) 86 | } 87 | let prefix = INDEX_MAP_KEY_FUNC(search, 0) 88 | const result = [] 89 | // console.log(`Searching for ${prefix}... ${cache[prefix].length}`) 90 | if (cache[prefix] === false) return indexOfAllSlow(buffer, search, start, end, maxCount) 91 | for (const index of cache[prefix]) { 92 | if (index < start || index + search.length > end) continue 93 | let found = true 94 | for (let i = search.length - 1; i >= 0; i--) { 95 | if (buffer[index + i] !== search[i]) { 96 | found = false 97 | break 98 | } 99 | } 100 | if (!found) continue 101 | result.push(index) 102 | if (maxCount && result.length >= maxCount) break 103 | } 104 | return result 105 | } -------------------------------------------------------------------------------- /lib/nso.mjs: -------------------------------------------------------------------------------- 1 | import * as lz4 from 'lz4-wasm-nodejs' 2 | 3 | const caches = new WeakMap() 4 | 5 | /** 6 | * @param {Uint8Array} buffer NSO file 7 | */ 8 | export function isCompressedNso(buffer) { 9 | const view = new DataView(buffer.buffer) 10 | const magic = view.getUint32(0x0) 11 | const flags = view.getInt32(0xC, true) 12 | 13 | if (magic === 0x4E534F30 && (flags & 0x7)) { // NSO0 14 | return true 15 | } 16 | return false 17 | } 18 | 19 | /** 20 | * @param {Uint8Array} buffer NSO file 21 | */ 22 | export function getNsoSegments(buffer) { 23 | let cache = caches.get(buffer) 24 | if (!cache) { 25 | cache = getNsoSegmentsDetail(buffer) 26 | caches.set(buffer, cache) 27 | } 28 | return cache 29 | } 30 | 31 | /** 32 | * @param {Uint8Array} buffer NSO file 33 | */ 34 | function getNsoSegmentsDetail(buffer) { 35 | const view = new DataView(buffer.buffer) 36 | const flags = view.getInt32(0xC, true) 37 | const metadatas = { 38 | text: { 39 | compressed: Boolean(flags & 0x1), 40 | fileOffset: view.getInt32(0x10, true), 41 | memoryOffset: view.getInt32(0x14, true), 42 | size: view.getInt32(0x18, true), 43 | compressedSize: view.getInt32(0x60, true), 44 | }, 45 | rodata: { 46 | compressed: Boolean(flags & 0x2), 47 | fileOffset: view.getInt32(0x20, true), 48 | memoryOffset: view.getInt32(0x24, true), 49 | size: view.getInt32(0x28, true), 50 | compressedSize: view.getInt32(0x64, true), 51 | }, 52 | data: { 53 | compressed: Boolean(flags & 0x4), 54 | fileOffset: view.getInt32(0x30, true), 55 | memoryOffset: view.getInt32(0x34, true), 56 | size: view.getInt32(0x38, true), 57 | compressedSize: view.getInt32(0x68, true), 58 | } 59 | } 60 | 61 | // decompress use lz4 62 | /** 63 | * @param {Uint8Array} buffer 64 | * @param {object} metadata 65 | */ 66 | function getRawSegment(buffer, metadata) { 67 | const compressedBuffer = buffer.subarray(metadata.fileOffset, metadata.fileOffset + metadata.compressedSize) 68 | let decompressedBuffer 69 | if (metadata.compressed) { 70 | try { 71 | const tempBuffer = new Uint8Array(compressedBuffer.length + 4) 72 | const tempDataView = new DataView(tempBuffer.buffer) 73 | tempDataView.setInt32(0, metadata.size, true) 74 | tempBuffer.set(compressedBuffer, 4) 75 | decompressedBuffer = lz4.decompress(tempBuffer) 76 | } catch (err) { 77 | console.log(err) 78 | throw new Error('Decompression failed') 79 | } 80 | } else { 81 | decompressedBuffer = compressedBuffer 82 | } 83 | if (decompressedBuffer.length !== metadata.size) { 84 | throw new Error(`Segment size mismatch, ${decompressedBuffer.length} != ${metadata.size}`) 85 | } 86 | return { 87 | buffer: decompressedBuffer, 88 | start: metadata.memoryOffset, 89 | end: metadata.memoryOffset + metadata.size, 90 | } 91 | } 92 | 93 | const segments = { 94 | text: getRawSegment(buffer, metadatas.text), 95 | rodata: getRawSegment(buffer, metadatas.rodata), 96 | data: getRawSegment(buffer, metadatas.data), 97 | } 98 | return segments 99 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patch-porter", 3 | "version": "0.8.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "patch-porter", 9 | "version": "0.8.3", 10 | "license": "MIT", 11 | "dependencies": { 12 | "capstone-wasm": "^1.0.3", 13 | "lz4-wasm-nodejs": "^0.9.2", 14 | "yargs": "^17.6.2" 15 | }, 16 | "bin": { 17 | "patch-porter": "bin/port.js" 18 | } 19 | }, 20 | "node_modules/ansi-regex": { 21 | "version": "5.0.1", 22 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 23 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 24 | "engines": { 25 | "node": ">=8" 26 | } 27 | }, 28 | "node_modules/ansi-styles": { 29 | "version": "4.3.0", 30 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 31 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 32 | "dependencies": { 33 | "color-convert": "^2.0.1" 34 | }, 35 | "engines": { 36 | "node": ">=8" 37 | }, 38 | "funding": { 39 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 40 | } 41 | }, 42 | "node_modules/capstone-wasm": { 43 | "version": "1.0.3", 44 | "resolved": "https://registry.npmjs.org/capstone-wasm/-/capstone-wasm-1.0.3.tgz", 45 | "integrity": "sha512-4DCE+JsgZ2eDUUp1kDDTa6pxD4m3hY5Eh1Xl6r7XKD/g3Jxt7IOnidCJsAKXua2aTL6SAqUGsdKJ184K3rXGwA==" 46 | }, 47 | "node_modules/cliui": { 48 | "version": "8.0.1", 49 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 50 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 51 | "dependencies": { 52 | "string-width": "^4.2.0", 53 | "strip-ansi": "^6.0.1", 54 | "wrap-ansi": "^7.0.0" 55 | }, 56 | "engines": { 57 | "node": ">=12" 58 | } 59 | }, 60 | "node_modules/color-convert": { 61 | "version": "2.0.1", 62 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 63 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 64 | "dependencies": { 65 | "color-name": "~1.1.4" 66 | }, 67 | "engines": { 68 | "node": ">=7.0.0" 69 | } 70 | }, 71 | "node_modules/color-name": { 72 | "version": "1.1.4", 73 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 74 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 75 | }, 76 | "node_modules/emoji-regex": { 77 | "version": "8.0.0", 78 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 79 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 80 | }, 81 | "node_modules/escalade": { 82 | "version": "3.1.1", 83 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 84 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 85 | "engines": { 86 | "node": ">=6" 87 | } 88 | }, 89 | "node_modules/get-caller-file": { 90 | "version": "2.0.5", 91 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 92 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 93 | "engines": { 94 | "node": "6.* || 8.* || >= 10.*" 95 | } 96 | }, 97 | "node_modules/is-fullwidth-code-point": { 98 | "version": "3.0.0", 99 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 100 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 101 | "engines": { 102 | "node": ">=8" 103 | } 104 | }, 105 | "node_modules/lz4-wasm-nodejs": { 106 | "version": "0.9.2", 107 | "resolved": "https://registry.npmjs.org/lz4-wasm-nodejs/-/lz4-wasm-nodejs-0.9.2.tgz", 108 | "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==" 109 | }, 110 | "node_modules/require-directory": { 111 | "version": "2.1.1", 112 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 113 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 114 | "engines": { 115 | "node": ">=0.10.0" 116 | } 117 | }, 118 | "node_modules/string-width": { 119 | "version": "4.2.3", 120 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 121 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 122 | "dependencies": { 123 | "emoji-regex": "^8.0.0", 124 | "is-fullwidth-code-point": "^3.0.0", 125 | "strip-ansi": "^6.0.1" 126 | }, 127 | "engines": { 128 | "node": ">=8" 129 | } 130 | }, 131 | "node_modules/strip-ansi": { 132 | "version": "6.0.1", 133 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 134 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 135 | "dependencies": { 136 | "ansi-regex": "^5.0.1" 137 | }, 138 | "engines": { 139 | "node": ">=8" 140 | } 141 | }, 142 | "node_modules/wrap-ansi": { 143 | "version": "7.0.0", 144 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 145 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 146 | "dependencies": { 147 | "ansi-styles": "^4.0.0", 148 | "string-width": "^4.1.0", 149 | "strip-ansi": "^6.0.0" 150 | }, 151 | "engines": { 152 | "node": ">=10" 153 | }, 154 | "funding": { 155 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 156 | } 157 | }, 158 | "node_modules/y18n": { 159 | "version": "5.0.8", 160 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 161 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 162 | "engines": { 163 | "node": ">=10" 164 | } 165 | }, 166 | "node_modules/yargs": { 167 | "version": "17.6.2", 168 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", 169 | "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", 170 | "dependencies": { 171 | "cliui": "^8.0.1", 172 | "escalade": "^3.1.1", 173 | "get-caller-file": "^2.0.5", 174 | "require-directory": "^2.1.1", 175 | "string-width": "^4.2.3", 176 | "y18n": "^5.0.5", 177 | "yargs-parser": "^21.1.1" 178 | }, 179 | "engines": { 180 | "node": ">=12" 181 | } 182 | }, 183 | "node_modules/yargs-parser": { 184 | "version": "21.1.1", 185 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 186 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 187 | "engines": { 188 | "node": ">=12" 189 | } 190 | } 191 | }, 192 | "dependencies": { 193 | "ansi-regex": { 194 | "version": "5.0.1", 195 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 196 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 197 | }, 198 | "ansi-styles": { 199 | "version": "4.3.0", 200 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 201 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 202 | "requires": { 203 | "color-convert": "^2.0.1" 204 | } 205 | }, 206 | "capstone-wasm": { 207 | "version": "1.0.3", 208 | "resolved": "https://registry.npmjs.org/capstone-wasm/-/capstone-wasm-1.0.3.tgz", 209 | "integrity": "sha512-4DCE+JsgZ2eDUUp1kDDTa6pxD4m3hY5Eh1Xl6r7XKD/g3Jxt7IOnidCJsAKXua2aTL6SAqUGsdKJ184K3rXGwA==" 210 | }, 211 | "cliui": { 212 | "version": "8.0.1", 213 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 214 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 215 | "requires": { 216 | "string-width": "^4.2.0", 217 | "strip-ansi": "^6.0.1", 218 | "wrap-ansi": "^7.0.0" 219 | } 220 | }, 221 | "color-convert": { 222 | "version": "2.0.1", 223 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 224 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 225 | "requires": { 226 | "color-name": "~1.1.4" 227 | } 228 | }, 229 | "color-name": { 230 | "version": "1.1.4", 231 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 232 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 233 | }, 234 | "emoji-regex": { 235 | "version": "8.0.0", 236 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 237 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 238 | }, 239 | "escalade": { 240 | "version": "3.1.1", 241 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 242 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 243 | }, 244 | "get-caller-file": { 245 | "version": "2.0.5", 246 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 247 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 248 | }, 249 | "is-fullwidth-code-point": { 250 | "version": "3.0.0", 251 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 252 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 253 | }, 254 | "lz4-wasm-nodejs": { 255 | "version": "0.9.2", 256 | "resolved": "https://registry.npmjs.org/lz4-wasm-nodejs/-/lz4-wasm-nodejs-0.9.2.tgz", 257 | "integrity": "sha512-hSwgJPS98q/Oe/89Y1OxzeA/UdnASG8GvldRyKa7aZyoAFCC8VPRtViBSava7wWC66WocjUwBpWau2rEmyFPsw==" 258 | }, 259 | "require-directory": { 260 | "version": "2.1.1", 261 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 262 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 263 | }, 264 | "string-width": { 265 | "version": "4.2.3", 266 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 267 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 268 | "requires": { 269 | "emoji-regex": "^8.0.0", 270 | "is-fullwidth-code-point": "^3.0.0", 271 | "strip-ansi": "^6.0.1" 272 | } 273 | }, 274 | "strip-ansi": { 275 | "version": "6.0.1", 276 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 277 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 278 | "requires": { 279 | "ansi-regex": "^5.0.1" 280 | } 281 | }, 282 | "wrap-ansi": { 283 | "version": "7.0.0", 284 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 285 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 286 | "requires": { 287 | "ansi-styles": "^4.0.0", 288 | "string-width": "^4.1.0", 289 | "strip-ansi": "^6.0.0" 290 | } 291 | }, 292 | "y18n": { 293 | "version": "5.0.8", 294 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 295 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 296 | }, 297 | "yargs": { 298 | "version": "17.6.2", 299 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", 300 | "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", 301 | "requires": { 302 | "cliui": "^8.0.1", 303 | "escalade": "^3.1.1", 304 | "get-caller-file": "^2.0.5", 305 | "require-directory": "^2.1.1", 306 | "string-width": "^4.2.3", 307 | "y18n": "^5.0.5", 308 | "yargs-parser": "^21.1.1" 309 | } 310 | }, 311 | "yargs-parser": { 312 | "version": "21.1.1", 313 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 314 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patch-porter", 3 | "version": "0.8.3", 4 | "description": ".pchtxt porting tool", 5 | "main": "port.mjs", 6 | "bin": { 7 | "patch-porter": "./bin/port.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Coxxs/patch-porter.git" 15 | }, 16 | "author": "Coxxs", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/Coxxs/patch-porter/issues" 20 | }, 21 | "homepage": "https://github.com/Coxxs/patch-porter#readme", 22 | "dependencies": { 23 | "capstone-wasm": "^1.0.3", 24 | "lz4-wasm-nodejs": "^0.9.2", 25 | "yargs": "^17.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /port.mjs: -------------------------------------------------------------------------------- 1 | import { indexOfAll } from "./lib/fast-index-of-all.mjs" 2 | import { getNsoSegments, isCompressedNso } from './lib/nso.mjs' 3 | import { Const, Capstone, loadCapstone } from 'capstone-wasm' 4 | function dec2hex(number, length) { 5 | return number.toString(16).padStart(length, '0').toUpperCase() 6 | } 7 | 8 | const searchModesGlobal = [ 9 | { start: 64, end: -64, length: 12, step: -4, range: null }, 10 | { start: 64, end: -64, length: 16, step: -4, range: null }, 11 | { start: 256, end: -256, length: 16, step: -4, range: null }, 12 | { start: 1024, end: -1024, length: 16, step: -4, range: null }, 13 | ] 14 | 15 | const searchModesDefault = [ 16 | { start: 16, end: -16, length: 12, step: -4, range: 0x200000 }, 17 | { start: 16, end: -16, length: 16, step: -4, range: 0x200000 }, 18 | { start: 16, end: -16, length: 20, step: -4, range: 0x200000 }, 19 | 20 | { start: 16, end: -16, length: 12, step: -4, range: 0x400000 }, 21 | { start: 16, end: -16, length: 16, step: -4, range: 0x400000 }, 22 | { start: 16, end: -16, length: 20, step: -4, range: 0x400000 }, 23 | 24 | { start: 32, end: -32, length: 12, step: -4, range: 0x400000 }, 25 | { start: 32, end: -32, length: 16, step: -4, range: 0x400000 }, 26 | { start: 32, end: -32, length: 20, step: -4, range: 0x400000 }, 27 | 28 | // { start: 32, end: -32, length: 8, step: -4, range: null }, 29 | { start: 48, end: -48, length: 12, step: -4, range: null }, 30 | { start: 48, end: -48, length: 16, step: -4, range: null }, 31 | { start: 48, end: -48, length: 20, step: -4, range: null }, 32 | ] 33 | 34 | const searchModesFast = [ 35 | { start: 16, end: -16, length: 12, step: -4, range: 0x100 }, 36 | { start: 24, end: -24, length: 16, step: -4, range: 0x200 }, 37 | { start: 64, end: -64, length: 16, step: -4, range: 0x1000 }, 38 | ] 39 | 40 | /** 41 | * @param {Uint8Array} buffer NSO file 42 | * @returns {string} nsobid 43 | */ 44 | function getNsobid(buffer) { 45 | let nsobid = Buffer.from(buffer.subarray(0x40, 0x40 + 0x20)).toString('hex').toUpperCase() 46 | nsobid = nsobid.replace(/(00)*$/, '') 47 | return nsobid 48 | } 49 | 50 | /** 51 | * @param {Uint8Array} fileOld 52 | * @param {Uint8Array} fileNew 53 | * @param {number} address 54 | * @param {number} offset 55 | * @param {object} searchMode 56 | * @returns {Array} results 57 | */ 58 | function portAddressSearchMode(fileOld, fileNew, address, offset = 0, searchMode = searchModesDefault[0]) { 59 | if (!Number.isInteger(address)) { 60 | throw new Error('address must be an integer') 61 | } 62 | const { start, end, length, step, range } = searchMode 63 | if (start == end && step <= 0) { 64 | step = 1 // prevent infinite loop 65 | } 66 | if (start > end && step > 0 || start < end && step < 0) { 67 | throw new Error(`Search mode ${JSON.stringify(searchMode)} will cause an infinite loop`) 68 | } 69 | // limit search range 70 | let startOffset 71 | let endOffset 72 | // let searchOld 73 | // let searchNew 74 | if (Number.isInteger(range)) { 75 | startOffset = Math.max(0, address + offset - range) 76 | endOffset = address + offset + range 77 | // searchOld = fileOld.subarray(startOffset, address + offset + range) 78 | // searchNew = fileNew.subarray(startOffset, address + offset + range) 79 | } else { 80 | startOffset = 0 81 | endOffset = fileNew.length 82 | // searchOld = fileOld 83 | // searchNew = fileNew 84 | } 85 | let results = [] 86 | for (let i = start; start > end ? i >= end : i <= end ; i += step) { 87 | const ptr = address + i 88 | const data = fileOld.subarray(ptr, ptr + length) 89 | const indexs = indexOfAll(fileNew, data, startOffset, endOffset, 2) 90 | if (indexs.length == 0) continue 91 | if (indexs.length > 1) continue 92 | const index = indexs[0] 93 | let delta = index - ptr 94 | results.push({ old: ptr, new: ptr + delta, delta: delta }) 95 | } 96 | // console.log(`Found with mode ${JSON.stringify(searchMode)}, results ${JSON.stringify(results)}`) 97 | return results 98 | } 99 | 100 | /** 101 | * @param {Capstone | null} capstone 102 | * @param {Uint8Array} fileOld 103 | * @param {Uint8Array} fileNew 104 | * @param {number} address 105 | * @param {object} searchMode 106 | * @returns {Promise} offset 107 | */ 108 | async function getEstimatedOffset(capstone, fileOld, fileNew, address, searchMode = searchModesGlobal[0]) { 109 | const results = portAddressSearchMode(fileOld, fileNew, address, 0, searchMode) 110 | // console.log(`Estimating offset with search mode ${JSON.stringify(searchMode)}, results ${JSON.stringify(results)}`) 111 | if (results.length == 0) return false 112 | 113 | const deltas = results.map(result => result.delta) 114 | deltas.sort((a, b) => a - b) 115 | const median = deltas[Math.floor(deltas.length / 2)] 116 | 117 | if (deltas.filter(delta => delta > median - 0x20 && delta < median + 0x20).length >= 3) { 118 | return median 119 | } 120 | if (capstone) { 121 | let confidence = await getPortConfidenceByInstructions(capstone, fileOld, fileNew, address, address + median) 122 | if (confidence > 0.7) { 123 | return median 124 | } 125 | } 126 | return false 127 | } 128 | 129 | /** 130 | * @param {Capstone} capstone 131 | * @param {Uint8Array} fileOld 132 | * @param {Uint8Array} fileNew 133 | * @param {number} addressOld 134 | * @param {number} addressNew 135 | * @param {number} start 136 | * @param {number} end 137 | * @returns {Promise} confidence 138 | */ 139 | async function getPortConfidenceByInstructions(capstone, fileOld, fileNew, addressOld, addressNew, start = -16, end = 16) { 140 | if (!capstone) { 141 | throw new Error('capstone is required') 142 | } 143 | if (start % 4 !== 0 || end % 4 !== 0) { 144 | throw new Error('offset not aligned with instructions') 145 | } 146 | 147 | let confidences = [] 148 | 149 | for (let i = start; i < end; i += 4) { 150 | const dataOld = fileOld.subarray(addressOld + i, addressOld + i + 4) 151 | const dataNew = fileNew.subarray(addressNew + i, addressNew + i + 4) 152 | let insnsOld 153 | let insnsNew 154 | 155 | try { 156 | insnsOld = capstone.disasm(dataOld, { address: 0xcafe880000000 }) 157 | } catch (err) { 158 | insnsOld = err 159 | } 160 | try { 161 | insnsNew = capstone.disasm(dataNew, { address: 0xcafe880000000 }) 162 | } catch (err) { 163 | insnsNew = err 164 | } 165 | 166 | let confidence = 0 167 | if (insnsOld instanceof Error || insnsNew instanceof Error) { 168 | if (Buffer.compare(dataOld, dataNew) === 0) { 169 | confidence = 1 170 | } else if (insnsOld instanceof Error && insnsNew instanceof Error) { 171 | confidence = 0.1 172 | } else { 173 | confidence = 0 174 | } 175 | // console.log('has error, confidence = ' + confidence, insnsOld instanceof Error, insnsNew instanceof Error) 176 | } else if (insnsOld.length === 1 && insnsNew.length === 1) { 177 | // console.log(insnsOld[0].mnemonic, insnsOld[0].opStr, '->', insnsNew[0].mnemonic, insnsNew[0].opStr) 178 | if (insnsOld[0].mnemonic === insnsNew[0].mnemonic) { 179 | confidence += 0.2 180 | if (insnsOld[0].opStr === insnsNew[0].opStr) { 181 | confidence += 0.8 182 | } else { 183 | const regex = /#0xcafe8[0-9a-f]+/g 184 | const noAddrOpStrOld = insnsOld[0].opStr.replace(regex, '#0xDUMMY') 185 | const noAddrOpStrNew = insnsNew[0].opStr.replace(regex, '#0xDUMMY') 186 | if (noAddrOpStrOld === noAddrOpStrNew) { 187 | confidence += 0.75 188 | } 189 | } 190 | } 191 | } 192 | confidences.push(confidence) 193 | } 194 | 195 | const average = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length; 196 | return average(confidences) 197 | } 198 | 199 | /** 200 | * @param {Capstone} capstone 201 | * @param {Uint8Array} file 202 | * @param {number} fileAddress 203 | * @param {number} capstoneAddress 204 | * @returns {Promise} assembly instruction 205 | */ 206 | async function getInstruction(capstone, file, fileAddress, capstoneAddress) { 207 | if (!capstone) { 208 | throw new Error('capstone is required') 209 | } 210 | const data = file.subarray(fileAddress, fileAddress + 4) 211 | let insns 212 | try { 213 | insns = capstone.disasm(data, { address: capstoneAddress }) 214 | } catch (err) { 215 | insns = [] 216 | } 217 | 218 | if (insns.length !== 1) { 219 | return null 220 | } else { 221 | return insns[0].mnemonic + ' ' + insns[0].opStr 222 | } 223 | } 224 | 225 | /** 226 | * @param {Uint8Array} fileOld 227 | * @param {Uint8Array} fileNew 228 | * @param {number} address 229 | * @param {Array} searchModesOffset 230 | * @param {Array} searchModes 231 | * @param {Capstone | null} capstone 232 | * @returns {Promise} address 233 | */ 234 | export async function portAddress(fileOld, fileNew, address, searchModesOffset = searchModesGlobal, searchModes = searchModesFast, capstone = null) { 235 | if (!Number.isInteger(address)) { 236 | throw new Error('address must be an integer') 237 | } 238 | let estimatedOffset 239 | if (searchModesOffset) { 240 | for (const searchMode of searchModesOffset) { 241 | estimatedOffset = await getEstimatedOffset(capstone, fileOld, fileNew, address, searchMode) 242 | // console.log(`Unable to find estimated offset!`) 243 | if (estimatedOffset !== false) break 244 | } 245 | // console.log(`Estimated offset: ${estimatedOffset}`) 246 | if (estimatedOffset === false) return false 247 | } else { 248 | estimatedOffset = 0 249 | } 250 | 251 | let results = [] 252 | for (const searchMode of searchModes) { 253 | const searchResults = portAddressSearchMode(fileOld, fileNew, address, estimatedOffset, searchMode) 254 | // console.log(`Search mode ${JSON.stringify(searchMode)}, results ${JSON.stringify(results)}`) 255 | if (searchResults.length == 0) continue 256 | const deltas = searchResults.map(r => r.delta) 257 | deltas.sort((a, b) => a - b) 258 | const median = deltas[Math.floor(deltas.length / 2)] 259 | const count = deltas.filter(delta => delta == median).length 260 | const instructionsConfidence = capstone ? Math.min( 261 | await getPortConfidenceByInstructions(capstone, fileOld, fileNew, address, address + median), 262 | await getPortConfidenceByInstructions(capstone, fileOld, fileNew, address, address + median, 0, 4) 263 | ) : null 264 | if (count >= 2 && count > deltas.length * 0.3) { 265 | const confidence = instructionsConfidence !== null ? Math.min(1, instructionsConfidence) : 1 266 | results.push({ old: address, new: address + median, delta: median, confidence: confidence }) 267 | break 268 | } else if (deltas.length == 1) { 269 | const confidence = instructionsConfidence !== null ? Math.min(0.6, instructionsConfidence) : 0.6 270 | results.push({ old: address, new: address + median, delta: median, confidence: Math.min(0.6, confidence) }) 271 | } 272 | } 273 | if (searchModesOffset) { 274 | const instructionsConfidence = capstone ? Math.min( 275 | await getPortConfidenceByInstructions(capstone, fileOld, fileNew, address, address + estimatedOffset), 276 | await getPortConfidenceByInstructions(capstone, fileOld, fileNew, address, address + estimatedOffset, 0, 4) 277 | ) : null 278 | const confidence = instructionsConfidence !== null ? Math.min(0.3, instructionsConfidence) : 0.3 279 | results.push({ old: address, new: address + estimatedOffset, delta: estimatedOffset, confidence: confidence }) 280 | } 281 | results = results.sort((a, b) => b.confidence - a.confidence) 282 | return results.length > 0 ? results[0] : false 283 | } 284 | 285 | async function portNsoAddressAndCheck(capstone, fileOld, fileNew, oldAddress, offset) { 286 | if (offset != 0x100) { 287 | console.error('Your pchtxt did not set the correct NSO offset (@flag offset_shift 0x100), please disable NSO mode (--no-nso) or fix the pchtxt.') 288 | return [] 289 | } 290 | let segmentsNew = getNsoSegments(fileNew) 291 | let segmentsOld = getNsoSegments(fileOld) 292 | 293 | let segmentOld 294 | let segmentNew 295 | let segmentName 296 | for (let [_segmentName, _segmentOld] of Object.entries(segmentsOld)) { 297 | if (oldAddress >= _segmentOld.start && oldAddress < _segmentOld.end) { 298 | segmentOld = _segmentOld 299 | segmentNew = segmentsNew[_segmentName] 300 | segmentName = _segmentName 301 | break 302 | } 303 | } 304 | 305 | if (!segmentOld || !segmentNew || !segmentName) { 306 | console.error(`${oldAddress.toString(16)} is not in a supported segment`) 307 | return [] 308 | } 309 | 310 | let results = [] 311 | 312 | let resultA = await portAddress(segmentOld.buffer, segmentNew.buffer, oldAddress - segmentOld.start, null, searchModesDefault, capstone) 313 | if (resultA) results.push(resultA) 314 | 315 | let resultB = await portAddress(segmentOld.buffer, segmentNew.buffer, oldAddress - segmentOld.start, searchModesGlobal, searchModesFast, capstone) 316 | if (resultB) results.push(resultB) 317 | 318 | results = results.sort((a, b) => b.confidence - a.confidence) 319 | 320 | if (capstone) { 321 | const oldInstructionStr = await getInstruction(capstone, segmentOld.buffer, oldAddress - segmentOld.start, oldAddress) 322 | for (let result of results) { 323 | result.oldInst = oldInstructionStr 324 | result.newInst = await getInstruction(capstone, segmentNew.buffer, result.new, result.new + segmentNew.start) 325 | } 326 | } 327 | 328 | if (results.length > 1 && results[1].new == results[0].new) { 329 | results.splice(1, 1) 330 | } 331 | 332 | // convert addresses back to file address 333 | for (let result of results) { 334 | result.segmentName = segmentName 335 | result.relativeOld = result.old 336 | result.relativeNew = result.new 337 | result.old += segmentOld.start + offset 338 | result.new += segmentNew.start + offset 339 | } 340 | 341 | return results 342 | } 343 | 344 | async function portAddressAndCheck(capstone, fileOld, fileNew, oldAddress, offset) { 345 | let results = [] 346 | 347 | let resultA = await portAddress(fileOld, fileNew, oldAddress + offset, null, searchModesDefault, capstone) 348 | if (resultA) results.push(resultA) 349 | 350 | let resultB = await portAddress(fileOld, fileNew, oldAddress + offset, searchModesGlobal, searchModesFast, capstone) 351 | if (resultB) results.push(resultB) 352 | 353 | results = results.sort((a, b) => b.confidence - a.confidence) 354 | 355 | if (capstone) { 356 | const oldInstructionStr = await getInstruction(capstone, fileOld, oldAddress + offset, oldAddress) 357 | for (let result of results) { 358 | result.oldInst = oldInstructionStr 359 | result.newInst = await getInstruction(capstone, fileNew, result.new, result.new - offset) 360 | } 361 | } 362 | 363 | if (results.length > 1 && results[1].new == results[0].new) { 364 | results.splice(1, 1) 365 | } 366 | 367 | return results 368 | } 369 | 370 | /** 371 | * @param {Buffer | Uint8Array} fileOld 372 | * @param {Buffer | Uint8Array} fileNew 373 | * @param {string} pchtxt 374 | * @param {object} options 375 | * @returns {Promise} pchtxt 376 | */ 377 | export async function portPchtxt(fileOld, fileNew, pchtxt, options) { 378 | options = { 379 | addComment: false, 380 | arch: 'arm64', 381 | nso: true, 382 | ...options, 383 | } 384 | const startTime = Date.now() 385 | 386 | let capstone 387 | if (options.arch === 'arm') { 388 | await loadCapstone() 389 | capstone = new Capstone(Const.CS_ARCH_ARM, Const.CS_MODE_ARM) 390 | } else if (options.arch === 'arm64') { 391 | await loadCapstone() 392 | capstone = new Capstone(Const.CS_ARCH_ARM64, Const.CS_MODE_ARM) 393 | } else if (options.arch === 'none') { 394 | capstone = null 395 | } else { 396 | throw new Error(`invalid arch: ${arch}`) 397 | } 398 | 399 | if (fileOld instanceof Buffer) { 400 | fileOld = new Uint8Array(fileOld) 401 | } 402 | if (fileNew instanceof Buffer) { 403 | fileNew = new Uint8Array(fileNew) 404 | } 405 | 406 | if (!options.nso && (isCompressedNso(fileOld) || isCompressedNso(fileNew))) { 407 | throw new Error('Your NSO file is compressed, please enable NSO mode (--nso) or decompress the NSO manually.') 408 | } 409 | 410 | try { 411 | const lines = pchtxt.replaceAll('\r\n', '\n').split('\n') 412 | const output = [] 413 | const portCache = new Map() 414 | 415 | let offset = 0 416 | for (const line of lines) { 417 | let match 418 | if (match = line.match(/^@nsobid-(?[0-9a-fA-F]+)\s*$/)) { 419 | let pchtxtNsobid = match.groups.nsobid.toUpperCase() 420 | let oldNsobid = getNsobid(fileOld) 421 | let newNsobid = getNsobid(fileNew) 422 | if (oldNsobid !== pchtxtNsobid) { 423 | throw new Error(`nsobid mismatch: ${oldNsobid} (nso) != ${pchtxtNsobid} (pchtxt)`) 424 | } 425 | output.push(`@nsobid-${newNsobid}`) 426 | continue 427 | } 428 | 429 | if (match = line.match(/^@flag\s+offset_shift\s+0x(?[0-9a-fA-F]+)\s*$/)) { 430 | offset = parseInt(match.groups.offset, 16) 431 | output.push(line) 432 | continue 433 | } 434 | 435 | if (match = line.match(/^(?(?:\/\/\s+)?)(?
[0-9a-fA-F]{4,10})\s(?.+)$/)) { 436 | const oldAddressStr = match.groups.address 437 | const oldAddress = parseInt(oldAddressStr, 16) 438 | const prefix = match.groups.prefix 439 | const suffix = match.groups.suffix 440 | 441 | let results = portCache.get(oldAddress + offset) // may need structuredClone in the future 442 | if (!results) { 443 | if (options.nso) { 444 | results = await portNsoAddressAndCheck(capstone, fileOld, fileNew, oldAddress, offset) 445 | } else { 446 | results = await portAddressAndCheck(capstone, fileOld, fileNew, oldAddress, offset) 447 | } 448 | portCache.set(oldAddress + offset, results) // may need structuredClone in the future 449 | } 450 | 451 | if (results.length <= 0) { 452 | console.error(`Failed to find new address for ${oldAddressStr}`) 453 | output.push(`${line} // [x] 0x${oldAddressStr} -> Failed`) 454 | continue 455 | } 456 | 457 | function generateComment(result) { 458 | function formatConfidence(c) { 459 | if (Math.abs(c % 1) < 0.0000001) { 460 | return Math.round(c) 461 | } 462 | return c.toFixed(2) 463 | } 464 | 465 | if (result.segmentName) { 466 | const oldRelativeAddressStr = dec2hex(result.relativeOld, 0) 467 | const newRelativeAddressStr = dec2hex(result.relativeNew, 0) 468 | const oldInstStr = result.oldInst ? ` (${result.oldInst})` : '' 469 | const newInstStr = result.newInst ? ` (${result.newInst})` : '' 470 | 471 | return `${result.delta > 0 ? '+' : ''}${result.delta} C=${formatConfidence(result.confidence)} .${result.segmentName}+0x${oldRelativeAddressStr}${oldInstStr} -> .${result.segmentName}+0x${newRelativeAddressStr}${newInstStr}` 472 | } else { 473 | let newAddress = result.new - offset 474 | const newAddressStr = dec2hex(newAddress, oldAddressStr.length) 475 | const oldInstStr = result.oldInst ? ` (${result.oldInst})` : '' 476 | const newInstStr = result.newInst ? ` (${result.newInst})` : '' 477 | 478 | return `${result.delta > 0 ? '+' : ''}${result.delta} C=${formatConfidence(result.confidence)} 0x${oldAddressStr}${oldInstStr} -> 0x${newAddressStr}${newInstStr}` 479 | } 480 | } 481 | 482 | let newAddress = results[0].new - offset 483 | const newAddressStr = dec2hex(newAddress, oldAddressStr.length) 484 | console.log(`Address updated: ${results.map(r => generateComment(r)).join(' | ')}`) 485 | if (options.addComment || results[0].confidence < 0.8 || results.length > 1) { 486 | output.push(`${prefix}${newAddressStr} ${suffix} // ${results[0].confidence >= 0.3 ? '[P]' : '[x]'} ${results.map(r => generateComment(r)).join(' | ')}`) 487 | } else { 488 | output.push(`${prefix}${newAddressStr} ${suffix}`) 489 | } 490 | continue 491 | } 492 | 493 | output.push(line) 494 | } 495 | 496 | 497 | console.log(`Finished in ${(Date.now() - startTime) / 1000}s.`) 498 | return output.join('\n') 499 | } finally { 500 | if (capstone) capstone.close() 501 | } 502 | } 503 | --------------------------------------------------------------------------------