├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.MD ├── assembler.wasm ├── docs ├── atrac_sound_group.md └── sector_header_format.md ├── package-lock.json ├── package.json ├── src ├── assembler │ ├── assembler.ts │ ├── core-macros.ts │ └── keystone-arm.js ├── compatibility.ts ├── exploit-state.ts ├── exploit.ts ├── exploits │ ├── atrac-recovery │ │ ├── atrac-recovery-interface.ts │ │ ├── cached-sector-bulk-transfers.ts │ │ ├── cached-sector-control-transfers.ts │ │ ├── cached-sector-noram-transfers.ts │ │ ├── index.ts │ │ └── multitrack-atrac-recovery.ts │ ├── disable-disc-detection-switch.ts │ ├── enter-service-mode.ts │ ├── firmware-dumper.ts │ ├── force-toc-edit.ts │ ├── himd-unbounded-reading.ts │ ├── himd-usb-class-override.ts │ ├── index.ts │ ├── kill-eeprom-writes.ts │ ├── mono-sp-upload.ts │ ├── pcm-faster-upload.ts │ ├── sp-upload.ts │ ├── tetris.ts │ ├── usb-code-execution.ts │ └── wait-for-disc-spinning.ts ├── index.ts ├── safety.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "printWidth": 140, 6 | "parser": "typescript" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # netmd-exploits 2 | 3 | ## What is it? 4 | `netmd-exploits` is a library aiming to store all the available exploits for Sony NetMD devices. 5 | 6 | ## What exploits are available? 7 | 8 | The exploits currently available are: 9 | 10 | | Exploit name | Firmware Versions* compatible | JavaScript class name | 11 | |----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| 12 | | Firmware Dumping | All versions supported | FirmwareDumper | 13 | | USB Code Execution | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R1.400, R1.300, R1.200, R1.100, R1.000, Hr1.000, Hn1.000, Hn1.100, Hn1.10A, Hn1.200, Hx1.070, Hx1.090, Hx1.0A0 | USBCodeExecution | 14 | | Tetris | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000 | Tetris | 15 | | Force TOC Flushing | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R1.400, R1.300, R1.200, R1.100, R1.000 | ForcedTOCEdit | 16 | | Upload SP Mono | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R1.400, R1.300, R1.200, R1.100, R1.000 | MonoSPUpload | 17 | | Atrac USB Control Transfer | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, ~~R1.400, R1.300, R1.200, R1.100, R1.000~~ | CachedSectorControlDownload | 18 | | Atrac USB No-RAM Transfer | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R1.400, R1.300, R1.200, R1.100, R1.000 | CachedSectorNoRAMDownload | 19 | | Atrac USB Bulk Transfer | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, Hr1.000, Hn1.000, Hn1.100, Hn1.10A, Hn1.200 | CachedSectorBulkDownload | 20 | | SP Faster Upload | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000 | PCMFasterUpload | 21 | | ATRAC1 Upload | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000 | SPUpload | 22 | | EEprom Write Lock | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R1.400, R1.300, R1.200, R1.100, R1.000 | KillEepromWrite | 23 | | Disc Spinning Notifier** | S1.600, S1.500, S1.400, S1.300, S1.200, S1.100, S1.000, R*, Hr*, Hn* | WaitForDiscToStopSpinning | 24 | | Unbounded memory access | Hr1.000, Hn1.000, Hn1.100, Hn1.10A, Hn1.200 | HiMDUnboundedReading | 25 | | HiMD USB Class Override | Hr1.000, Hn1.000, Hn1.100, Hn1.10A, Hn1.200, Hx1.070, Hx1.090, Hx1.0A0 | HiMDUSBClassOverride | 26 | 27 | *The firmware versions listed here consist of the SOC type letter: 28 | 29 | - R - CXD2677 (Type R) 30 | - S - CXD2678 / CXD2680 (Type S) 31 | - Hn - CXD2681 (gen1) 32 | - Hr - CXD2681 (gen2) 33 | - Hx - CXD2687 34 | 35 | And the actual firmware version reported by the device. 36 | 37 | **WaitForDiscToStopSpinning only supports Type-S devices, on Type-R and HiMD it always waits 10 seconds. 38 | 39 | ### Examples 40 | 41 | | Unit | SoC | Firmware version | netmd-exploits firmware version | 42 | |--------------------|----------------|--------------------|-----------------------------------| 43 | | Sony MZ-N510 | CXD2680 | 1.600 | S1.600 | 44 | | Sony MZ-N710 | CXD2680 | 1.600 | S1.600 | 45 | | Sony MZ-N1 | CXD2677 | 1.200 | R1.200 | 46 | | Sony MZ-N10 | CXD2678 | 1.200 | S1.300 | 47 | | Sony MZ-RH10 | CXD2681 (gen2) | 1.000 | Hr1.000 | 48 | | Sony MZ-NH600 | CXD2681 (gen1) | 1.000 | Hn1.000 | 49 | | Sony MZ-RH1 | CXD2687 | 1.0A0 | Hx1.0A0 | 50 | 51 | ## Hacking 52 | If you would like to help with adding compatibility for your device, pull requests are welcome. 53 | 54 | ### An exploit's structure 55 | The library keeps track of what exploits are compatible with what versions with the help of `src/compatibility.ts`. 56 | Every exploit class has to inherit the `Exploit` abstract class. It provides multiple functions which make it easier to load the correct versions of exploits for every firmware version. 57 | The constants that depend on the firmware version are stored in a `VersionStore` map returned from `getPropertyStore()`, from which it's possible to get values by calling `getProperty`, or by referencing their names in assembly code, prefixed with a '$'. Every exploit class has to also define a `static _name` const, used for compatibility checking. 58 | 59 | ### Loading and unloading exploits 60 | Once an exploit is loaded using `ExploitStateManager.require` or `ExploitStateManager.envelop`, the exploit's `init()` method is called. Because of how exploits are loaded, exploits shouldn't 61 | have their own constructor. Instead, `init()` should be used as an asynchronous constructor. 62 | 63 | To unload an exploit you can use `ExploitStateManager.unload`. When using `ExploitStateManager.envelop`, this action will be done automatically. 64 | 65 | When an exploit is unloaded, its `unload()` method is called, where the exploit can perform cleanup. 66 | All behavior-modifying exploits should have a valid `unload()` method defined, in order to automatically restore the device to the default state. 67 | If an exploit is using patches, the `unload()` method should clear the patches from the device, and then call `this.stateManager.freePatch()`, to mark the patch as unused. 68 | All the patches for which `ExploitStateManager.freePatch()` wasn't called will automatically be unloaded from the device by the state manager. 69 | 70 | If a patch is loaded by `ExploitStateManager.require`, and then reloaded again using `ExploitStateManager.envelop`, it will not be unloaded after returning from `envelop()`. 71 | To unload it, `ExploitStateManager.unload` has to be called. 72 | 73 | ### The inbuilt assembler 74 | The assembler has full support for macros (prefixed with '@'), `VersionStore` constants (prefixed with '$'), as well as variables, passed to the `assemble` function (prefixed with '%'). 75 | 76 | The macros available for every assembly program are stored in `src/assembler/core-macros.ts`. 77 | Exploits can define private macros in the `_macros` property of the `VersionStore` returned from `getPropertyStore()`. 78 | 79 | Happy hacking! 80 | 81 | ## Example 82 | 83 | Below is an extremely basic example, which when run will download the first track from the disc onto the computer using the best suited exploit for it: 84 | ```ts 85 | import { DevicesIds, openNewDevice } from 'netmd-js' 86 | import { AtracRecovery, getBestSuited, ExploitStateManager } from 'netmd-exploits'; 87 | import { WebUSB } from 'usb'; 88 | import fs from 'fs'; 89 | 90 | (async() => { 91 | const usb = new WebUSB({ allowedDevices: DevicesIds, deviceTimeout: 1000000 }); 92 | const dev = await openNewDevice(usb); 93 | const stateManager = await ExploitStateManager.create(dev!); 94 | 95 | const exploit = await stateManager.require(getBestSuited(AtracRecovery)); 96 | fs.writeFileSync(await exploit.downloadTrack(0, console.log)); 97 | await stateManager.unload(getBestSuited(AtracRecovery)); 98 | 99 | process.exit(0); 100 | })(); 101 | ``` 102 | 103 | ## Credits 104 | The assembler built into `netmd-exploits` is a modified version of [keystone-js](https://github.com/AlexAltea/keystone.js) by AlexAltea 105 | -------------------------------------------------------------------------------- /assembler.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asivery/netmd-exploits/87be51015c77bf61d252becf2fbfc347afa384df/assembler.wasm -------------------------------------------------------------------------------- /docs/atrac_sound_group.md: -------------------------------------------------------------------------------- 1 | # Notes on ATRAC soundgroup formats 2 | ``` 3 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 4 | ----------------------------------------------- 5 | 0000 AC C0 84 33 22 22 22 32 22 22 21 22 22 22 22 12 6 | 0010 12 12 21 22 22 22 11 11 10 11 74 A2 07 14 71 47 7 | 0020 18 92 88 14 72 47 14 41 C8 1C 61 45 18 51 07 10 8 | 0030 71 08 20 51 C7 1C 71 C8 14 41 C9 28 61 C9 73 72 9 | 0040 46 5F 40 C7 F4 16 03 49 EA 5A 67 17 0D 2A D7 01 10 | 0050 A3 B0 9E 0E F4 FF 8E BF FB CF 3F 34 02 C5 11 CA 11 | 0060 11 2A C0 0C 5C F0 41 EB A1 40 C0 87 F0 E0 7E 06 12 | 0070 17 51 4C F0 EF 8F E0 8B C1 DF 9E 41 D6 D4 28 A5 13 | 0080 07 C7 03 5E 43 3F D0 1C 00 E1 90 60 81 EE 40 B8 14 | 0090 07 0C 4F 80 28 E8 81 AE 00 80 7E 81 83 F5 AF FA 15 | 00A0 FF 78 1F FF CF 27 E0 39 B8 6F 40 E4 FB BF C4 80 16 | 00B0 09 21 01 FE 1F F2 18 98 06 86 20 F8 00 19 86 00 17 | 00C0 00 20 00 00 60 00 00 60 18 01 86 1A 0E 08 38 00 18 | 00D0 18 00 C0 AC 19 | ``` 20 | ## Description (partial) 21 | 22 | - Byte 0 and Byte 211 are the Block Size Mode 23 | - Byte 1 and Byte 210 are the number of Block Floating Units 24 | - Byte 2 is the Block Size Mode. The first two bits are the low band, third and fourth are the mid band, fifth and sixth are the high band, and the final two bits are unused 25 | -------------------------------------------------------------------------------- /docs/sector_header_format.md: -------------------------------------------------------------------------------- 1 | # Notes on Minidisc Sector Headers 2 | 3 | ``` 4 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 5 | ----------------------------------------------------------- 6 | 40 00 00 40 00 00 00 10 00 00 10 00 00 32 01 11 00 00 00 00 7 | A5 53 00 00 99 88 40 42 C8 4E 4A A4 00 32 02 11 00 00 00 00 8 | A0 01 00 00 FF FB FF 7F FE FF F6 FF 00 32 03 11 00 00 00 00 9 | A0 03 00 00 FB 77 FB D7 D5 7E 7F 77 00 32 04 11 00 00 00 00 10 | A0 01 00 00 00 00 00 10 00 00 10 04 00 32 05 11 00 00 00 00 11 | A0 03 00 00 FB F9 69 77 FE DF FF BE 00 32 06 11 00 00 00 00 12 | A0 01 00 00 FF FB FF 7F FE FF F6 FF 00 32 07 11 00 00 00 00 13 | A0 03 00 00 40 62 08 42 D4 12 0B 64 00 32 08 11 00 00 00 00 14 | A0 01 00 00 C0 C4 53 74 75 44 72 44 00 32 09 11 00 00 00 00 15 | A0 03 00 00 FB F9 79 77 FE DF FF FE 00 32 0A 11 00 00 00 00 16 | A0 01 00 00 1F F0 D5 7E 1E F3 96 F8 00 32 0B 11 00 00 00 00 17 | A0 03 00 00 40 62 08 40 D4 16 0B 64 00 32 0C 11 00 00 00 00 18 | A0 01 00 00 C0 C4 5B 74 75 44 72 04 00 32 0D 11 00 00 00 00 19 | A0 03 00 00 99 88 40 42 C8 4E 4A A4 00 32 0E 11 00 00 00 00 20 | A0 01 00 00 1A F0 D7 7E 1E F3 A6 F8 00 32 0F 11 00 00 00 00 21 | A0 03 00 00 FB 76 FB C6 D5 7E 7F 7F 00 32 10 11 00 00 00 00 22 | A0 01 00 00 00 00 00 14 00 00 10 04 00 32 11 11 00 00 00 00 23 | A0 03 00 00 99 80 40 42 C8 4E 08 A4 00 32 12 11 00 00 00 00 24 | A0 01 00 00 FF FB FF 7F FE FF F6 FF 00 32 13 11 00 00 00 00 25 | A0 03 00 00 FB 76 FB E6 D5 7E 7F 77 00 32 14 11 00 00 00 00 26 | A0 01 00 00 00 00 00 14 00 00 10 04 00 32 15 11 00 00 00 00 27 | A0 03 00 00 FB F9 79 77 FE DF FF BE 00 32 16 11 00 00 00 00 28 | A0 01 00 00 FF FF FF 7F FE FF F6 FF 00 32 17 11 00 00 00 00 29 | A0 03 00 00 40 62 08 40 D4 16 0B 64 00 32 18 11 00 00 00 00 30 | A0 01 00 00 C8 C4 5B 74 75 44 72 04 00 32 19 11 00 00 00 00 31 | A0 03 00 00 FB F9 69 77 FE DF FF BE 00 32 1A 11 00 00 00 00 32 | A0 01 00 00 1F F1 D7 7E 1E F3 B6 F8 00 32 1B 11 00 00 00 00 33 | A0 03 00 00 40 62 08 40 D4 16 0B 74 00 32 1C 11 00 00 00 00 34 | A0 01 00 00 C0 C4 5B 74 75 44 7A 04 00 32 1D 11 00 00 00 00 35 | A0 03 00 00 99 88 40 42 C8 46 48 A4 00 32 1E 11 00 00 00 00 36 | A0 01 00 00 1B F1 D7 7E 1E F3 96 F8 00 32 1F 15 00 00 00 00 37 | A0 03 00 00 FB 76 FB EE D5 7E 7F 7F 00 33 00 11 00 00 00 00 38 | 39 | 40 00 00 40 00 00 00 10 00 00 10 00 00 32 00 11 00 00 00 00 40 | A0 51 00 00 99 80 40 42 C8 4E 4A A4 00 32 01 11 00 00 00 00 41 | A0 03 00 00 FF FB FF 7F FE FF FE FF 00 32 02 11 00 00 00 00 42 | A0 01 00 00 FB 77 FB C7 D5 7E 7F 77 00 32 03 11 00 00 00 00 43 | A0 03 00 00 00 00 00 10 00 00 10 04 00 32 04 11 00 00 00 00 44 | 45 | ``` 46 | 47 | # Guesswork 48 | - Byte 00 49 | - Byte 01 appears to alternate between 01 and 03 based on channel start, except for the first two sectors in memory? 50 | - Byte 12 appears to always be zero 51 | - Bytes 13, 14, and 15 contain the sector address 52 | - Bytes 16-19 appear to always contain zeros 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netmd-exploits", 3 | "version": "0.5.10", 4 | "description": "A collection of netmd exploits usable with netmd-js", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "keywords": [ 10 | "netmd", 11 | "minidisc", 12 | "netmd-js" 13 | ], 14 | "files": [ 15 | "dist", 16 | "assembler.wasm" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/asivery/netmd-exploits.git" 21 | }, 22 | "license": "GPL-2.0", 23 | "author": "asivery", 24 | "dependencies": { 25 | "browser-or-node": "^2.1.1", 26 | "expr-eval": "^2.0.2", 27 | "jsbi": "^4.3.0", 28 | "netmd-js": "^4.1.10", 29 | "netmd-tocmanip": "^0.1.4" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^18.7.18", 33 | "prettier": "^2.7.1", 34 | "ts-node": "^10.9.1", 35 | "typescript": "^4.7.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/assembler/assembler.ts: -------------------------------------------------------------------------------- 1 | import { VersionPropertyStore } from '../exploit'; 2 | import { getFromVersionStore } from '../utils'; 3 | import { Parser } from 'expr-eval'; 4 | import { isBrowser } from 'browser-or-node'; 5 | import fs from 'fs'; 6 | import { Keystone, MODE_ARM, ARCH_ARM, MODE_THUMB, construct, version } from './keystone-arm'; 7 | import { join } from 'path'; 8 | 9 | function processVersionProps(readableCode: string, versionProps: VersionPropertyStore, versionCode: string){ 10 | Object.keys(versionProps) 11 | .sort((a, b) => b.length - a.length) 12 | .filter((n) => readableCode.includes('$' + n)) 13 | .forEach( 14 | (n) => 15 | (readableCode = readableCode.replace(new RegExp(`\\$${n}`, 'g'), () => { 16 | let i = getFromVersionStore(versionProps, versionCode, n); 17 | if (typeof i === 'number') { 18 | return '0x' + i.toString(16); 19 | } 20 | return i; 21 | })) 22 | ); 23 | return readableCode; 24 | } 25 | 26 | export interface Macro { 27 | code: 28 | | string 29 | | (( 30 | versionProps: VersionPropertyStore, 31 | versionCode: string, 32 | variablesPassed: { [key: string]: string }, 33 | ...args: string[] 34 | ) => string); 35 | properties: VersionPropertyStore; 36 | } 37 | 38 | export class AssemblerSyntaxError extends Error { 39 | constructor(message: string) { 40 | super(message); 41 | Object.setPrototypeOf(this, AssemblerSyntaxError.prototype); 42 | } 43 | } 44 | 45 | export class Assembler { 46 | private keystone: Keystone; 47 | private static wasmURL?: string; 48 | private static wasmArrayBuffer?: ArrayBuffer; 49 | public static setWASMArrayBuffer(arrayBuffer: ArrayBuffer){ 50 | Assembler.wasmArrayBuffer = arrayBuffer; 51 | } 52 | public static setWASMUrl(newURL: string){ 53 | Assembler.wasmURL = newURL; 54 | } 55 | 56 | public static async create(macros: { [key: string]: Macro }, mode: 'ARM' | 'THUMB' = 'ARM') { 57 | let promise; 58 | if(isBrowser){ 59 | if(Assembler.wasmArrayBuffer){ 60 | promise = Promise.resolve(Assembler.wasmArrayBuffer); 61 | }else if(Assembler.wasmURL) { 62 | promise = fetch(Assembler.wasmURL).then(e => e.arrayBuffer()); 63 | }else{ 64 | throw new Error("You are running in a browser! Please provide a url to `assembler.wasm`"); 65 | } 66 | }else{ 67 | promise = new Promise(res => fs.readFile(join(__dirname, "..", "..", "assembler.wasm"), (err, data) => res(data))); 68 | } 69 | await construct(promise); 70 | return new Assembler(macros, mode); 71 | } 72 | 73 | public static async version() { 74 | await construct(); 75 | return await version(); 76 | } 77 | 78 | private constructor(private macros: { [key: string]: Macro }, mode: 'ARM' | 'THUMB' = 'ARM') { 79 | this.keystone = new Keystone(ARCH_ARM, mode === 'ARM' ? MODE_ARM : MODE_THUMB); 80 | } 81 | 82 | processCode( 83 | code: string, 84 | versionProps: VersionPropertyStore, 85 | versionCode: string, 86 | variablesPassed: { [key: string]: string }, 87 | ownMacros: { [key: string]: Macro } = {} 88 | ) { 89 | Object.entries(variablesPassed).forEach(([from, to]) => (code = code.replace(new RegExp(`%${from}`, 'g'), to))); 90 | 91 | let lines = code.split('\n'); 92 | let newLines = lines.map((line) => { 93 | let trimmed = this.removeComments(line); 94 | let evaluated = trimmed.replace(/\<([^\<])*\>/g, (occurrence: string) => { 95 | let expression = processVersionProps( 96 | occurrence.substring(1, occurrence.length - 1), 97 | versionProps, 98 | versionCode 99 | ); 100 | return `${Parser.evaluate(expression)}`; 101 | }); 102 | if (trimmed !== evaluated){ 103 | trimmed = evaluated; 104 | line = `\n;Evaluated expressions in line '${line}'\n${trimmed}\n;Evaluated expression line end`; 105 | } 106 | if (trimmed.startsWith('@')) { 107 | // Macro 108 | let [name, ...args] = trimmed.substring(1).split(' '); 109 | // Remove commas between macro arguments (if present) 110 | args = args.map((n) => (n.endsWith(',') ? n.substring(0, n.length - 1) : n)); 111 | 112 | let thisMacro = this.macros[name] || ownMacros[name]; 113 | if (!thisMacro) { 114 | throw new AssemblerSyntaxError(`The macro: ${name} doesn't exist`); 115 | } 116 | 117 | // Parent inherits all children's VersionStore props 118 | for (let [k, v] of Object.entries(thisMacro.properties)) { 119 | if (!(k in versionProps)) { 120 | versionProps[k] = v; 121 | } 122 | } 123 | let macroCode = thisMacro.code; 124 | let processedVariables = { 125 | ...variablesPassed, 126 | }; 127 | if (typeof macroCode === 'function') { 128 | line = this.processCode( 129 | macroCode(versionProps, versionCode, variablesPassed, ...args).trim(), 130 | versionProps, 131 | versionCode, 132 | processedVariables, 133 | ownMacros 134 | ).trim(); 135 | } else { 136 | // Variables don't get inherited by parent. 137 | for (let argIdx in args) { 138 | processedVariables[`macro_argument_${argIdx}`] = args[argIdx]; 139 | } 140 | line = this.processCode(macroCode, versionProps, versionCode, processedVariables, ownMacros).trim(); 141 | } 142 | 143 | line = `; Unwound macro ${name} (@${name} ${args.join(', ')})\n${line}\n; Macro ${name} end\n`; 144 | } 145 | return line; 146 | }); 147 | 148 | let readableCode = newLines.join('\n'); 149 | 150 | readableCode = processVersionProps(readableCode, versionProps, versionCode); 151 | 152 | return readableCode; 153 | } 154 | 155 | assemble(code: string) { 156 | const response = this.keystone.asm(code, 0); 157 | if (response.length === 0 && code.trim().length !== 0) throw new Error('Crashed assembler'); 158 | return response; 159 | } 160 | 161 | removeComments(code: string) { 162 | return code 163 | .split('\n') 164 | .map((line: string) => { 165 | let commentStart = line.indexOf(';'); 166 | if (commentStart !== -1) { 167 | line = line.substring(0, commentStart); 168 | } 169 | return line.trim(); 170 | }) 171 | .join('\n'); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/assembler/core-macros.ts: -------------------------------------------------------------------------------- 1 | import { VersionPropertyStore } from '../exploit'; 2 | import { getFromVersionStore } from '../utils'; 3 | import { Macro } from './assembler'; 4 | 5 | export const CORE_MACROS: { [key: string]: Macro } = { 6 | blxar: { 7 | code: ` 8 | add %macro_argument_0, pc, #0xc 9 | mov lr, %macro_argument_0 10 | ldr %macro_argument_0, [ pc ] 11 | bx %macro_argument_0 12 | .word %macro_argument_1 13 | `, 14 | properties: {}, 15 | }, 16 | blxart: { 17 | code: ` 18 | add %macro_argument_0, pc, #0xd 19 | mov lr, %macro_argument_0 20 | ldr %macro_argument_0, [ pc ] 21 | bx %macro_argument_0 22 | .word %macro_argument_1 23 | ; THUMB: 24 | ; bx pc 25 | .byte 0x78 26 | .byte 0x47 27 | .byte 0x00 28 | .byte 0x00 29 | `, 30 | properties: {}, 31 | }, 32 | blxa: { 33 | code: ` 34 | @blxar r7, %macro_argument_0 35 | `, 36 | properties: {}, 37 | }, 38 | bxar: { 39 | code: ` 40 | ldr %macro_argument_0, [ pc ] 41 | bx %macro_argument_0 42 | .word %macro_argument_1 43 | `, 44 | properties: {}, 45 | }, 46 | bxa: { 47 | code: ` 48 | @bxar r7, %macro_argument_0 49 | `, 50 | properties: {}, 51 | }, 52 | bridge: { 53 | code: (versionProps: VersionPropertyStore, versionCode: string, variablesPassed: { [key: string]: string }, ...args: string[]) => { 54 | const [type, ...props] = args; 55 | if (!['word', 'hword', 'byte', 'ascii'].includes(type)) throw new Error(`Unsupported type given: ${type}`); 56 | let data = ''; 57 | for (let prop of props) { 58 | let value = getFromVersionStore(versionProps, versionCode, prop); 59 | if (typeof value === 'string') { 60 | value = `"${value.replace('"', '\\"')}"`; 61 | } 62 | data += `${prop}: .${type} ${value}\n`; 63 | } 64 | return data; 65 | }, 66 | properties: {}, 67 | }, 68 | fw_compat_bridge: { 69 | code: ``, 70 | properties: { 71 | device_capabilities_address: { 72 | 'Hn1.000': 0x008038d4, 73 | 'Hn1.200': 0x00803938, 74 | }, 75 | patch_code_base: { 76 | 'S1.600': 0x02002ba0, 77 | 'S1.500': 0x02002b88, 78 | 'S1.400': 0x02002b6c, 79 | 'S1.300': 0x02002a58, 80 | 'S1.200': 0x02002a44, 81 | 'S1.100': 0x02002a3c, 82 | 'S1.000': 0x02002bc4, 83 | }, 84 | tron_ter_task: { 85 | //tron_ter_tsk 86 | 'S1.600': 0x0007c5c1, 87 | 'S1.500': 0x0007bb41, 88 | 'S1.400': 0x0007b481, 89 | 'S1.300': 0x00077c19, 90 | 'S1.200': 0x00076449, 91 | 'S1.100': 0x00075a3d, 92 | 'S1.000': 0x0007c731, 93 | }, 94 | tron_set_flg: { 95 | 'S1.600': 0x0007c095, 96 | 'S1.500': 0x0007b615, 97 | 'S1.400': 0x0007af55, 98 | 'S1.300': 0x000776ed, 99 | 'S1.200': 0x00075f1d, 100 | 'S1.100': 0x00075511, 101 | 'S1.000': 0x0007c205, 102 | 103 | 'R1.000': 0x0005c271, 104 | 'R1.100': 0x0005cb59, 105 | 'R1.200': 0x0005d8f1, 106 | 'R1.300': 0x0005dc45, 107 | 'R1.400': 0x0005dce5, 108 | }, 109 | tron_clr_flg: { 110 | 'S1.600': 0x0007c2c5, 111 | 'S1.500': 0x0007b845, 112 | 'S1.400': 0x0007b185, 113 | 'S1.300': 0x0007791d, 114 | 'S1.200': 0x0007614d, 115 | 'S1.100': 0x00075741, 116 | 'S1.000': 0x0007c435, 117 | 118 | 'R1.000': 0x0005c3dd, 119 | 'R1.100': 0x0005ccc5, 120 | 'R1.200': 0x0005da5d, 121 | 'R1.300': 0x0005ddb1, 122 | 'R1.400': 0x0005de51, 123 | }, 124 | tron_twai_flg: { 125 | 'S1.600': 0x0007c319, 126 | 'S1.500': 0x0007b899, 127 | 'S1.200': 0x000761a1, 128 | 'S1.100': 0x00075795, 129 | 'S1.000': 0x0007c489, 130 | 'S1.400': 0x0007b1d9, 131 | 'S1.300': 0x00077971, 132 | 133 | 'R1.000': 0x0005bfa1, 134 | 'R1.100': 0x0005c889, 135 | 'R1.200': 0x0005d621, 136 | 'R1.300': 0x0005d975, 137 | 'R1.400': 0x0005da15, 138 | }, 139 | usb_do_response: { 140 | //usbDoResponse 141 | 'S1.600': 0x00077a3d, 142 | 'S1.500': 0x00077045, 143 | 'S1.400': 0x00076971, 144 | 'S1.300': 0x000732c1, 145 | 'S1.200': 0x00071ca9, 146 | 'S1.100': 0x00071321, 147 | 'S1.000': 0x00077ba5, 148 | 149 | 'R1.000': 0x00058e35, 150 | 'R1.100': 0x00059715, 151 | 'R1.200': 0x0005a4bd, 152 | 'R1.300': 0x0005a811, 153 | 'R1.400': 0x0005a8b1, 154 | }, 155 | read_atrac_dram: { 156 | 'S1.600': 0x000781fd, 157 | 'S1.500': 0x00077805, 158 | 'S1.400': 0x00077131, 159 | 'S1.300': 0x00073a71, 160 | 'S1.200': 0x000723bd, 161 | 'S1.100': 0x00071a35, 162 | 'S1.000': 0x00078365, 163 | 164 | 'R1.000': 0x0005d37d, // Slow ones 165 | 'R1.100': 0x0005dc65, 166 | 'R1.200': 0x0005e9fd, 167 | 'R1.300': 0x0005ed51, 168 | 'R1.400': 0x0005edf1, 169 | }, 170 | g_usb_buff: { 171 | // These are easy to find in handleUnitInfo 172 | 'S1.600': 0x02001170, 173 | 'S1.500': 0x02001158, 174 | 'S1.400': 0x0200113c, 175 | 'S1.300': 0x0200102c, 176 | 'S1.200': 0x02001018, 177 | 'S1.100': 0x02001014, 178 | 'S1.000': 0x0200117c, 179 | 180 | 'R1.000': 0x020040f0, 181 | 'R1.100': 0x02004104, 182 | 'R1.200': 0x02004108, 183 | 'R1.300': 0x0200410c, 184 | 'R1.400': 0x02004110, 185 | }, 186 | g_DiscStateStruct: { 187 | 'S1.600': 0x020001e8, 188 | 'S1.500': 0x020001e8, 189 | 'S1.400': 0x020001e8, 190 | 'S1.300': 0x020001d8, 191 | 'S1.200': 0x020001cc, 192 | 'S1.100': 0x020001cc, 193 | 'S1.000': 0x020001e8, 194 | 195 | 'R1.000': 0x02000b34, 196 | 'R1.100': 0x02000b38, 197 | 'R1.200': 0x02000b34, 198 | 'R1.300': 0x02000b34, 199 | 'R1.400': 0x02000b38, 200 | }, 201 | g_disc_spin_state: { 202 | 'S1.600': 0x02000310, 203 | 'S1.500': 0x02000310, 204 | 'S1.400': 0x02000310, 205 | 'S1.300': 0x020002f8, 206 | 'S1.200': 0x020002ec, 207 | 'S1.100': 0x020002ec, 208 | 'S1.000': 0x02000310, 209 | }, 210 | ChangeIRQmask: { 211 | 'R1.000': 0x0005e8e1, 212 | 'R1.100': 0x0005f1c9, 213 | 'R1.200': 0x0005ff61, 214 | 'R1.300': 0x000602b5, 215 | 'R1.400': 0x00060355, 216 | 217 | 'S1.000': 0x0007f7fd, 218 | 'S1.100': 0x0007861d, 219 | 'S1.200': 0x00079029, 220 | 'S1.300': 0x0007ac61, 221 | 'S1.400': 0x0007e529, 222 | 'S1.500': 0x0007ebe9, 223 | 'S1.600': 0x0007f669, 224 | } 225 | }, 226 | }, 227 | patch: { 228 | code: ` 229 | ; r5:uint8_t <- patch number 230 | ; r3:uint32_t <- address 231 | ; r4:uint32_t <- value 232 | 233 | ; Write 5, 12 to main control 234 | ldr r0, _patch_macro_control 235 | mov r1, #5 236 | strb r1, [ r0 ] 237 | mov r1, #12 238 | strb r1, [ r0 ] 239 | 240 | ldr r0, _patch_macro_base 241 | lsl r5, r5, #4 242 | add r0, r0, r5 243 | 244 | ; AND 0xFE with patch control 245 | ldr r1, [ r0 ] 246 | and r1, r1, #0xFE 247 | str r1, [ r0 ] 248 | 249 | ; AND 0xFD with patch control 250 | ldr r1, [ r0 ] 251 | and r1, r1, #0xFD 252 | str r1, [ r0 ], #4 253 | 254 | ; Write patch address 255 | str r3, [ r0 ], #4 256 | 257 | ; Write patch value 258 | str r4, [ r0 ] 259 | 260 | sub r0, r0, #8 261 | ldr r1, [ r0 ] 262 | orr r1, r1, #1 263 | str r1, [ r0 ] 264 | 265 | ; Write 5, 9 to main control 266 | ldr r0, _patch_macro_control 267 | mov r1, #5 268 | strb r1, [ r0 ] 269 | mov r1, #9 270 | strb r1, [ r0 ] 271 | 272 | bx lr 273 | 274 | _patch_macro_control: .word $_patchMacroControl 275 | _patch_macro_base: .word $_patchMacroBase 276 | `, 277 | properties: { 278 | _patchMacroBase: { 279 | 'R*,S*': 0x03802000, 280 | 'Hr*,Hn*': 0x03804000, 281 | }, 282 | _patchMacroControl: { 283 | 'R*': 0x03802040, 284 | 'S*': 0x03802080, 285 | 'Hr*,Hn*': 0x03804100, 286 | }, 287 | }, 288 | }, 289 | unpatch: { 290 | code: ` 291 | ; r2:uint8_t <- patch number 292 | 293 | ; Write 5, 12 to main control 294 | ldr r0, _patch_macro_control 295 | mov r1, #5 296 | strb r1, [ r0 ] 297 | mov r1, #12 298 | strb r1, [ r0 ] 299 | 300 | ldr r0, _patch_macro_base 301 | add r0, r0, r2, lsl #4 302 | 303 | ; AND 0xFE with patch control 304 | ldr r1, [ r0 ] 305 | and r1, r1, #0xFE 306 | str r1, [ r0 ] 307 | 308 | ; Write 5, 9 to main control 309 | ldr r0, _patch_macro_control 310 | mov r1, #5 311 | strb r1, [ r0 ] 312 | mov r1, #9 313 | strb r1, [ r0 ] 314 | 315 | bx lr 316 | 317 | _patch_macro_control: .word $_patchMacroControl 318 | _patch_macro_base: .word $_patchMacroBase 319 | `, 320 | properties: { 321 | _patchMacroBase: { 322 | 'R*,S*': 0x03802000, 323 | 'Hr*,Hn*': 0x03804000, 324 | }, 325 | _patchMacroControl: { 326 | 'R*': 0x03802040, 327 | 'S*': 0x03802080, 328 | 'Hr*,Hn*': 0x03804100, 329 | }, 330 | }, 331 | }, 332 | autogen_cleanup: { 333 | code: (versionProps: VersionPropertyStore, versionCode: string, variablesPassed: { [key: string]: string }, ...args: string[]) => { 334 | const prologue = ` 335 | push { r3, r4, r5, r6, r7, lr } 336 | `; 337 | const epilogue = ` 338 | pop { r3, r4, r5, r6, r7, lr } 339 | bx lr 340 | 341 | _unpatch: 342 | @unpatch 343 | `; 344 | 345 | return prologue + 346 | args.map(e => ` 347 | mov r2, #${e} 348 | bl _unpatch 349 | `).join("\n") + 350 | epilogue; 351 | }, 352 | properties: {}, 353 | }, 354 | ins: { 355 | code: (versionProps: VersionPropertyStore, versionCode: string, variablesPassed: { [key: string]: string }, ...args: string[]) => { 356 | if(args.length !== 1){ 357 | throw new Error("Expected 1 argument (property name)") 358 | } 359 | return getFromVersionStore(versionProps, versionCode, args[0]); 360 | }, 361 | properties: {}, 362 | }, 363 | smv: { 364 | code: (versionProps: VersionPropertyStore, versionCode: string, variablesPassed: { [key: string]: string }, ...args: string[]) => { 365 | // Safe move (Immediate only!) (Non-THUMB only!) 366 | // Syntax: 367 | // @smv r0, #257 368 | 369 | // Caveat: shifting numbers while carrying to the beginning / end is not supported. 370 | // The number 0xf000000f will be encoded using the mov-lsl-orr method, and not the 371 | // Correct, `mov` method. 372 | let r = args[0].toLowerCase(), 373 | immediate = args[1].toLowerCase(); 374 | 375 | if(immediate.startsWith("#")) immediate = immediate.substring(1); 376 | let immediateNumber; 377 | if(immediate.startsWith("0x")) 378 | immediateNumber = parseInt(immediate.substring(2), 16); 379 | else if(immediate.startsWith("0b")) 380 | immediateNumber = parseInt(immediate.substring(2), 2); 381 | else if(immediate.startsWith("0o")) 382 | immediateNumber = parseInt(immediate.substring(2), 8); 383 | else 384 | immediateNumber = parseInt(immediate); 385 | 386 | // Calculate the number's bit-span and shift count 387 | let binaryEncoded = immediateNumber.toString(2); 388 | let shiftCount = 0; 389 | while(binaryEncoded.charAt(binaryEncoded.length - 1 - shiftCount) === '0'){ 390 | shiftCount++; 391 | } 392 | 393 | 394 | if(binaryEncoded.length <= 8){ 395 | if(shiftCount % 2 === 0){ 396 | // Is it an encodable immediate? 397 | return `mov ${r}, #${immediateNumber}`; 398 | }else { 399 | // Is the shift count a problem? 400 | // Since it *is* less-or-eql to 8 bytes of length 401 | // And shiftCount % 2 !== 0, we know it is *not* a non-shifted 402 | // number. It is safe to shift it right by 1 bit, write that number 403 | // in the `mov`, and follow that by shifting the number left by 1 bit 404 | return ` 405 | mov ${r}, #${immediateNumber >>> 1} 406 | lsl ${r}, ${r}, #1 407 | `; 408 | } 409 | } 410 | 411 | // It is not - create it manually by shifting and OR-ing the bytes together 412 | let bytes = [ 413 | (immediateNumber >>> 24) & 0xFF, 414 | (immediateNumber >>> 16) & 0xFF, 415 | (immediateNumber >>> 8) & 0xFF, 416 | (immediateNumber >>> 0) & 0xFF, 417 | ]; 418 | let currentInstructions = ""; 419 | let nextShifts = 0; 420 | let hasSeenNonZero = false; 421 | 422 | for(let byte of bytes){ 423 | if(byte === 0 && !hasSeenNonZero) continue; 424 | if(!hasSeenNonZero){ 425 | currentInstructions += `mov ${r}, #${byte}\n`; 426 | nextShifts = 0; 427 | } else if(byte !== 0) { 428 | currentInstructions += ` 429 | lsl ${r}, ${r}, #${nextShifts} 430 | orr ${r}, ${r}, #${byte} 431 | `; 432 | nextShifts = 0; 433 | } 434 | nextShifts += 8; 435 | hasSeenNonZero = true; 436 | } 437 | 438 | if(nextShifts > 8){ 439 | currentInstructions += ` 440 | lsl ${r}, ${r}, #${nextShifts - 8} 441 | `; 442 | } 443 | 444 | return currentInstructions; 445 | }, 446 | properties: {}, 447 | }, 448 | pcode: { 449 | // Patch code trigger 450 | code: ` 451 | @undef 452 | `, 453 | properties: {}, 454 | }, 455 | undef: { 456 | // Undefined instruction 457 | code: ` 458 | udf #0 459 | `, 460 | properties: {}, 461 | }, 462 | }; 463 | -------------------------------------------------------------------------------- /src/compatibility.ts: -------------------------------------------------------------------------------- 1 | import { Exploit, ExploitConstructor } from './exploit'; 2 | import { DeviceType, ExploitStateManager } from './exploit-state'; 3 | import { KillEepromWrite } from './exploits/kill-eeprom-writes'; 4 | import { PCMFasterUpload } from './exploits/pcm-faster-upload'; 5 | 6 | import { USBCodeExecution } from './exploits/usb-code-execution'; 7 | import { 8 | CachedSectorControlDownload, 9 | CachedSectorNoRamControlDownload, 10 | FirmwareDumper, 11 | ForceTOCEdit, 12 | Tetris, 13 | CachedSectorBulkDownload, 14 | WaitForDiscToStopSpinning, 15 | AtracRecovery, 16 | SPUpload, 17 | HiMDUnboundedReading, 18 | MultitrackATRACRecovery, 19 | DisableDiscDetection, 20 | } from './exploits'; 21 | import { isVersionMatchingFormat } from './utils'; 22 | import { HiMDUSBClassOverride } from './exploits/himd-usb-class-override'; 23 | import { MonoSPUpload } from './exploits/mono-sp-upload'; 24 | import { EnterServiceMode } from './exploits/enter-service-mode'; 25 | 26 | type AbstractExploitConstructor = (abstract new (statemanager: ExploitStateManager) => T) & { _name: string }; 27 | 28 | interface VersionSpecific { 29 | versions: string[], 30 | exploit: ExploitConstructor 31 | } 32 | 33 | const CompatibilityTable: { [key: string]: (string | ExploitConstructor | VersionSpecific)[] } = { 34 | [EnterServiceMode._name]: ['S*', USBCodeExecution], 35 | [FirmwareDumper._name]: ['S*', 'R*', 'Hx*', 'Hr*', 'Hn*'], 36 | [USBCodeExecution._name]: [ 37 | 'S1.600', 38 | 'S1.500', 39 | 'S1.400', 40 | 'S1.300', 41 | 'S1.200', 42 | 'S1.100', 43 | 'S1.000', 44 | 45 | 'R1.400', 46 | 'R1.300', 47 | 'R1.200', 48 | 'R1.100', 49 | 'R1.000', 50 | 51 | 'Hr1.000', 52 | 53 | 'Hn1.000', 54 | 'Hn1.100', 55 | 'Hn1.10A', 56 | 'Hn1.200', 57 | 58 | 'Hx1.0A0', 59 | 'Hx1.090', 60 | 'Hx1.080', 61 | 'Hx1.070', 62 | 'Hx1.060', 63 | 'Hx1.040', 64 | ], 65 | [CachedSectorControlDownload._name]: [ 66 | 'S1.600', 67 | 'S1.500', 68 | 'S1.400', 69 | 'S1.300', 70 | 'S1.200', 71 | 'S1.100', 72 | 'S1.000', 73 | 74 | // Although this method works for Type-R devices, it puts them into an unstable state. 75 | // 'R1.400', 76 | // 'R1.300', 77 | // 'R1.200', 78 | // 'R1.100', 79 | // 'R1.000', 80 | 81 | USBCodeExecution, 82 | WaitForDiscToStopSpinning, 83 | ], 84 | [CachedSectorNoRamControlDownload._name]: [ 85 | 'S1.600', 86 | 'S1.500', 87 | 'S1.400', 88 | 'S1.300', 89 | 'S1.200', 90 | 'S1.100', 91 | 'S1.000', 92 | 93 | 'R1.400', 94 | 'R1.300', 95 | 'R1.200', 96 | 'R1.100', 97 | 'R1.000', 98 | 99 | USBCodeExecution, 100 | WaitForDiscToStopSpinning, 101 | ], 102 | [ForceTOCEdit._name]: [ 103 | 'S1.600', 104 | 'S1.500', 105 | 'S1.400', 106 | 'S1.300', 107 | 'S1.200', 108 | 'S1.100', 109 | 'S1.000', 110 | 111 | 'R1.400', 112 | 'R1.300', 113 | 'R1.200', 114 | 'R1.100', 115 | 'R1.000', 116 | 117 | USBCodeExecution, 118 | ], 119 | [Tetris._name]: [ 120 | 121 | 'S1.600', 122 | 'S1.500', 123 | 'S1.400', 124 | 'S1.300', 125 | 'S1.200', 126 | 'S1.100', 127 | 'S1.000', 128 | 129 | 'HWID 09', 130 | 'HWID 08', 131 | 'HWID 07', 132 | 'HWID 06', 133 | 'HWID 05', 134 | 'HWID 04', 135 | 136 | USBCodeExecution, 137 | 138 | ], 139 | [KillEepromWrite._name]: [ 140 | 'S1.000', 141 | 'S1.100', 142 | 'S1.200', 143 | 'S1.300', 144 | 'S1.400', 145 | 'S1.500', 146 | 'S1.600', 147 | 148 | 'R1.000', 149 | 'R1.100', 150 | 'R1.200', 151 | 'R1.300', 152 | 'R1.400', 153 | 154 | USBCodeExecution, 155 | ], 156 | [CachedSectorBulkDownload._name]: [ 157 | 'Hr1.000', 158 | 159 | 'Hn1.000', 160 | 'Hn1.100', 161 | 'Hn1.10A', 162 | 'Hn1.200', 163 | 164 | 'S1.600', 165 | 'S1.500', 166 | 'S1.400', 167 | 'S1.300', 168 | 'S1.200', 169 | 'S1.100', 170 | 'S1.000', 171 | 172 | USBCodeExecution, 173 | WaitForDiscToStopSpinning, 174 | { 175 | exploit: HiMDUnboundedReading, 176 | versions: ['Hn*', 'Hr*'], 177 | }, 178 | ], 179 | [PCMFasterUpload._name]: [ 180 | 'S1.600', 181 | 'S1.500', 182 | 'S1.400', 183 | 'S1.300', 184 | 'S1.200', 185 | 'S1.100', 186 | 'S1.000', 187 | 188 | 'HWID 10', 189 | 'HWID 09', 190 | 'HWID 08', 191 | 'HWID 07', 192 | 'HWID 06', 193 | 'HWID 05', 194 | 'HWID 04', 195 | ], 196 | [WaitForDiscToStopSpinning._name]: [ 197 | 'S1.600', 198 | 'S1.500', 199 | 'S1.400', 200 | 'S1.300', 201 | 'S1.200', 202 | 'S1.100', 203 | 'S1.000', 204 | 205 | 'R*', // It just waits for 10 seconds on type-R devices. 206 | 'Hr*', 'Hn*', // Same with himd (for now) 207 | ], 208 | [HiMDUSBClassOverride._name]: [ 209 | 'Hr1.000', 210 | 211 | 'Hn1.000', 212 | 'Hn1.100', 213 | 'Hn1.10A', 214 | 'Hn1.200', 215 | 216 | 'Hx1.040', 217 | 'Hx1.060', 218 | 'Hx1.070', 219 | 'Hx1.080', 220 | 'Hx1.090', 221 | 'Hx1.0A0', 222 | { 223 | exploit: HiMDUnboundedReading, 224 | versions: ['Hr1.000', 'Hn1.000', 'Hn1.100', 'Hn1.200'], 225 | }, 226 | { 227 | exploit: USBCodeExecution, 228 | versions: ['Hx*'], 229 | }, 230 | ], 231 | [HiMDUnboundedReading._name]: [ 232 | 'Hr1.000', 233 | 234 | 'Hn1.000', 235 | 'Hn1.100', 236 | 'Hn1.10A', 237 | 'Hn1.200', 238 | ], 239 | [SPUpload._name]: ['S1.600', 'S1.500', 'S1.400', 'S1.300', 'S1.200', 'S1.100', 'S1.000'], 240 | [MonoSPUpload._name]: [ 241 | 'S1.000', 242 | 'S1.100', 243 | 'S1.200', 244 | 'S1.300', 245 | 'S1.400', 246 | 'S1.500', 247 | 'S1.600', 248 | 249 | 'R1.000', 250 | 'R1.100', 251 | 'R1.200', 252 | 'R1.300', 253 | 'R1.400', 254 | ], 255 | [MultitrackATRACRecovery._name]: [ 256 | 'S1.000', 257 | 'S1.100', 258 | 'S1.200', 259 | 'S1.300', 260 | 'S1.400', 261 | 'S1.500', 262 | 'S1.600', 263 | 264 | 'R1.000', 265 | 'R1.100', 266 | 'R1.200', 267 | 'R1.300', 268 | 'R1.400', 269 | ], 270 | [DisableDiscDetection._name]: [ 271 | 'S1.000', 272 | 'S1.100', 273 | 'S1.200', 274 | 'S1.300', 275 | 'S1.400', 276 | 'S1.500', 277 | 'S1.600', 278 | ], 279 | }; 280 | 281 | const BestSuitedTable: { [key: string]: ExploitConstructor[] } = { 282 | [AtracRecovery._name]: [CachedSectorBulkDownload, CachedSectorControlDownload, CachedSectorNoRamControlDownload], 283 | }; 284 | 285 | export function isCompatible(exploit: ExploitConstructor | AbstractExploitConstructor, deviceType: DeviceType) { 286 | if (Object.keys(BestSuitedTable).includes(exploit._name)) { 287 | return getBestSuited(exploit, deviceType) !== null; 288 | } 289 | let compatibles = CompatibilityTable[exploit._name]; 290 | if (!compatibles) 291 | throw new Error(`Cannot check compatibility of ${exploit._name} as it hasn't been included in the compatibility table`); 292 | let dependencies = compatibles.filter((n) => (n as any)._name !== undefined); 293 | // All dependencies have to be compatible. 294 | if (!dependencies.every((n) => isCompatible(n as any, deviceType))) { 295 | return false; 296 | } 297 | 298 | // All version-dependent dependencies also have to be compatible 299 | const versionDependent: VersionSpecific[] = compatibles.filter(e => 300 | Array.isArray((e as VersionSpecific).versions) && 301 | typeof (e as VersionSpecific).exploit === 'function' 302 | ) as VersionSpecific[]; 303 | const versionDependentForThisVersion = versionDependent.filter(e => e.versions.some(v => isVersionMatchingFormat(v, deviceType.versionCode))); 304 | if (!versionDependentForThisVersion.every(e => isCompatible(e.exploit, deviceType))) { 305 | return false; 306 | } 307 | 308 | const compatibleVersions = CompatibilityTable[exploit._name].filter((n) => typeof n === 'string' && !n.startsWith('HWID ')) as string[]; 309 | const compatibleHWIDs = CompatibilityTable[exploit._name] 310 | .filter((n) => typeof n === 'string' && n.startsWith('HWID ')) 311 | .map((n) => parseInt((n as string).substring(5), 16)); 312 | 313 | return (compatibleHWIDs.length === 0 || compatibleHWIDs.some((e) => e === deviceType.hwid)) && 314 | compatibleVersions.some((e) => isVersionMatchingFormat(e, deviceType.versionCode)); 315 | } 316 | 317 | export function getBestSuited( 318 | exploitFamily: AbstractExploitConstructor, 319 | deviceType: DeviceType 320 | ): ExploitConstructor | null { 321 | if (!Object.keys(BestSuitedTable).includes(exploitFamily._name)) { 322 | return null; 323 | } 324 | for (let exploitConstructor of BestSuitedTable[exploitFamily._name]) { 325 | if (isCompatible(exploitConstructor, deviceType)) { 326 | return exploitConstructor as ExploitConstructor; 327 | } 328 | } 329 | return null; 330 | } 331 | -------------------------------------------------------------------------------- /src/exploit-state.ts: -------------------------------------------------------------------------------- 1 | import { getDescriptiveDeviceCode, NetMDFactoryInterface, NetMDInterface, patch, PatchPeripheralBase, unpatch } from 'netmd-js'; 2 | import { Assembler } from './assembler/assembler'; 3 | import { CORE_MACROS } from './assembler/core-macros'; 4 | import { Exploit, ExploitConstructor, PatchError } from './exploit'; 5 | import { loadSafetyPatches } from './safety'; 6 | import { deepCopy, getFromVersionStore } from './utils'; 7 | 8 | export interface DeviceType { 9 | versionCode: string; 10 | hwid: number; 11 | isHimd: boolean; 12 | } 13 | 14 | interface LoadedExploit { 15 | exploit: T, 16 | resources: { 17 | patches: number[], 18 | allocatedAddresses: number[], 19 | }, 20 | } 21 | 22 | interface MemoryRange { 23 | start: number, 24 | end: number, 25 | } 26 | 27 | const INIT_CONFIG = { 28 | patchAmount: { 29 | "R*": 4, 30 | "S*": 8, 31 | "H*": 16, 32 | }, 33 | memoryRanges: { 34 | "S*": [ 35 | { 36 | start: 0x02006000, 37 | end: 0x02008FF0, 38 | } 39 | ], 40 | "R*": [ 41 | { 42 | start: 0x02000e5e, 43 | end: 0x02000fa0, 44 | }, 45 | { 46 | start: 0x0300c900, 47 | end: 0x300cd00, 48 | } 49 | ], 50 | "H*": [ 51 | { 52 | start: 0x0080a020, 53 | end: 0x0080a8d0, 54 | } 55 | ] 56 | }, 57 | }; 58 | 59 | export class AllocationError extends Error { 60 | constructor(m: string) { 61 | super(m); 62 | Object.setPrototypeOf(this, AllocationError.prototype); 63 | } 64 | } 65 | 66 | export class UnloadError extends Error { 67 | constructor(m: string) { 68 | super(m); 69 | Object.setPrototypeOf(this, AllocationError.prototype); 70 | } 71 | } 72 | 73 | export interface Logger{ 74 | log(tag: string, text: string): void; 75 | } 76 | 77 | export const ConsoleLogger: Logger = { 78 | log: (tag: string, text: string) => console.log(`[${tag}]: ${text}`), 79 | }; 80 | 81 | export class ExploitStateManager { 82 | loadedList: { [key: string]: LoadedExploit } = {}; 83 | 84 | private freePatches: number[]; 85 | private freeMemory: MemoryRange[]; 86 | private usedMemory: {[start: number]: number} = {}; 87 | 88 | protected constructor( 89 | public iface: NetMDInterface, 90 | public factoryIface: NetMDFactoryInterface, 91 | public device: DeviceType, 92 | public armAssembler: Assembler, 93 | public thumbAssembler: Assembler, 94 | public logger?: Logger, 95 | ) { 96 | this.freePatches = Array(this.getMaxPatchesAmount()).fill(0).map((_, i) => i); 97 | this.freeMemory = deepCopy(getFromVersionStore(INIT_CONFIG, this.device.versionCode, "memoryRanges")); 98 | } 99 | 100 | public getFreeMemoryMap(){ 101 | return `|${this.freeMemory.map(e => `${e.end - e.start}`).join('|')}|=${this.freeMemory.map(e => e.end - e.start).reduce((a, b) => a + b, 0)}`; 102 | } 103 | 104 | public getCurrentlyLoaded(){ 105 | return Object.values(this.loadedList).map(e => e.exploit.constructor) as ExploitConstructor[]; 106 | } 107 | 108 | public allocate(length: number){ 109 | length = Math.ceil(length / 4) * 4; // Basic allocation size = 4 bytes 110 | for(let i = 0; i= length){ 114 | let out = range.start; 115 | range.start += length; 116 | if(range.end === range.start){ 117 | this.freeMemory.splice(i, 1); 118 | } 119 | this.usedMemory[out] = range.start; 120 | this.log(`Allocated ${length} bytes of RAM at ${out.toString(16)}`); 121 | return out; 122 | } 123 | } 124 | throw new AllocationError(`Cannot allocate ${length} bytes of continous memory - free regions are: ${this.getFreeMemoryMap()}`); 125 | } 126 | 127 | public release(start: number){ 128 | if(this.usedMemory[start] === undefined){ 129 | throw new AllocationError("Cannot release the memory which was never allocated!"); 130 | } 131 | const rangeFreed = { 132 | start, 133 | end: this.usedMemory[start], 134 | }; 135 | this.log(`Releasing ${rangeFreed.end - rangeFreed.start} bytes of RAM at ${rangeFreed.start.toString(16)}`); 136 | delete this.usedMemory[start]; 137 | let i = 0; 138 | for(i; i= rangeFreed.end){ 140 | break; 141 | } 142 | } 143 | this.freeMemory.splice(i, 0, rangeFreed); 144 | this.mergeMemoryMap(); 145 | } 146 | 147 | private mergeMemoryMap(){ 148 | for(let i = 0; i= this.getMaxPatchesAmount()){ 166 | throw new PatchError(`Patch ${patchNumber} is not a valid patch`); 167 | } 168 | let index = this.freePatches.indexOf(patchNumber); 169 | if(index === -1){ 170 | throw new PatchError(`Patch ${patchNumber} has already been distributed to an exploit`); 171 | } 172 | this.freePatches.splice(index, 1); 173 | } 174 | 175 | public freePatch(patch: number){ 176 | if(patch >= this.getMaxPatchesAmount()){ 177 | throw new PatchError("Illegal patch index"); 178 | } 179 | if(this.freePatches.includes(patch)){ 180 | throw new PatchError("Patch already free"); 181 | } 182 | this.freePatches.push(patch); 183 | } 184 | 185 | public getMaxPatchesAmount() { 186 | return getFromVersionStore(INIT_CONFIG, this.device.versionCode, "patchAmount"); 187 | } 188 | 189 | public async require(subclassConstructor: ExploitConstructor, ...args: Parameters): Promise { 190 | if (subclassConstructor._name in this.loadedList) { 191 | return this.loadedList[subclassConstructor._name].exploit as T; 192 | } else { 193 | const overwrittenMethods: {[key: string]: any} = { 194 | getIncrementalPatchNumber: () => { 195 | const thisPatch = this.getIncrementalPatchNumber(); 196 | loadedEntry.resources.patches.push(thisPatch); 197 | this._log(subclassConstructor._name, [`Reserved patch ${thisPatch}`]); 198 | return thisPatch; 199 | }, 200 | freePatch: (patch: number) => { 201 | if(!loadedEntry.resources.patches.includes(patch)){ 202 | throw new PatchError(`Patch ${patch} is not used by this exploit`); 203 | } 204 | this._log(subclassConstructor._name, [`Released patch ${patch}`]); 205 | loadedEntry.resources.patches.splice(loadedEntry.resources.patches.indexOf(patch), 1); 206 | this.freePatch(patch); 207 | }, 208 | allocate: (length: number) => { 209 | let allocated = this.allocate(length); 210 | loadedEntry.resources.allocatedAddresses.push(allocated); 211 | return allocated; 212 | }, 213 | release: (address: number) => { 214 | let localIndex = loadedEntry.resources.allocatedAddresses.indexOf(address); 215 | if(localIndex === -1){ 216 | throw new AllocationError("Cannot release a section of memory not allocated by this exploit"); 217 | } 218 | loadedEntry.resources.allocatedAddresses.splice(localIndex, 1); 219 | this.release(address); 220 | }, 221 | log: (...e: string[]) => { 222 | this._log(subclassConstructor._name, e); 223 | }, 224 | require: (subclassConstructor: ExploitConstructor, ...args: Parameters) => { 225 | return this.require(subclassConstructor, ...args); 226 | }, 227 | unload: (object: ExploitConstructor | T) => this.unload(object), 228 | }; 229 | const fake = new Proxy(this, { 230 | get: (target: any, prop: string, receiver) => (overwrittenMethods[prop] ?? (this as any)[prop]) 231 | }); 232 | let loadedEntry: LoadedExploit = { 233 | exploit: new subclassConstructor(fake), 234 | resources: { 235 | patches: [], 236 | allocatedAddresses: [], 237 | }, 238 | }; 239 | await loadedEntry.exploit.init(...args); 240 | this.loadedList[subclassConstructor._name] = loadedEntry; 241 | return loadedEntry.exploit; 242 | } 243 | } 244 | 245 | public async unload(object: ExploitConstructor | T){ 246 | let name: string; 247 | if ('_name' in object){ 248 | name = object._name; 249 | }else{ 250 | let foundName = Object.entries(this.loadedList).find(([name, entry]) => entry.exploit === object)?.[0]; 251 | if(!foundName) return; 252 | name = foundName; 253 | } 254 | if (name in this.loadedList){ 255 | const loaded = this.loadedList[name]; 256 | if(loaded.exploit._policies?.permanent){ 257 | throw new UnloadError("Cannot unload a permanent patch"); 258 | } 259 | 260 | // Trigger the unload() code of the exploit 261 | await loaded.exploit.unload(); 262 | 263 | // Unpatch all the remaining patches 264 | for(let patch of loaded.resources.patches){ 265 | if(!loaded.exploit._policies?.stateManagerUnpatchLegal) 266 | this.log(`Force-unpatching leftover patch #${patch}. Please clean up the patches using unload() instead!`); 267 | await unpatch(this.factoryIface, patch, this.getMaxPatchesAmount(), this.device.isHimd ? PatchPeripheralBase.HIMD : PatchPeripheralBase.NETMD); 268 | this.freePatch(patch); 269 | } 270 | 271 | // Free all the memory taken by the exploit 272 | loaded.resources.allocatedAddresses.forEach(this.release.bind(this)); 273 | 274 | // Remove the exploit from the loaded list 275 | delete this.loadedList[name]; 276 | } 277 | } 278 | 279 | public async patch(address: number, value: Uint8Array, slot?: number){ 280 | return patch( 281 | this.factoryIface, 282 | address, 283 | value, 284 | slot ?? this.getIncrementalPatchNumber(), 285 | this.getMaxPatchesAmount(), 286 | this.device.isHimd ? PatchPeripheralBase.HIMD : PatchPeripheralBase.NETMD, 287 | ); 288 | } 289 | 290 | public async envelop(subclassConstructor: ExploitConstructor, ...allargs: [...Parameters, ((exploit: T) => Promise)]){ 291 | const action = allargs[allargs.length - 1]; 292 | const args = allargs.slice(0, -1) as Parameters; 293 | 294 | const wasLoadedExternally = Object.keys(this.loadedList).includes(subclassConstructor._name); 295 | const exploit = await this.require(subclassConstructor, ...args); 296 | await action(exploit); 297 | if(!wasLoadedExternally) 298 | await this.unload(subclassConstructor); 299 | } 300 | 301 | public static async create(iface: NetMDInterface, factoryIface: NetMDFactoryInterface, logger?: Logger) { 302 | const deviceInfo = await factoryIface.getDeviceCode(); 303 | const versionCode = await getDescriptiveDeviceCode(deviceInfo); 304 | const esm = new ExploitStateManager( 305 | iface, 306 | factoryIface, 307 | { 308 | hwid: deviceInfo.hwid, 309 | versionCode, 310 | isHimd: versionCode.startsWith("H"), 311 | }, 312 | await Assembler.create(CORE_MACROS, 'ARM'), 313 | await Assembler.create(CORE_MACROS, 'THUMB'), 314 | logger, 315 | ); 316 | 317 | await loadSafetyPatches(esm); 318 | return esm; 319 | } 320 | 321 | public log(...e: string[]){ 322 | this._log("StateManager", e); 323 | } 324 | 325 | protected _log(tag: string, e: string[]){ 326 | this.logger?.log(tag, e.join(" ")); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/exploit.ts: -------------------------------------------------------------------------------- 1 | import { NetMDFactoryInterface, NetMDInterface } from 'netmd-js'; 2 | import { CORE_MACROS } from './assembler/core-macros'; 3 | import { ExploitStateManager } from './exploit-state'; 4 | import { getFromVersionStore, getFromVersionStoreOrNull } from './utils'; 5 | 6 | export class CompatibilityError extends Error { 7 | constructor(m: string) { 8 | super(m); 9 | Object.setPrototypeOf(this, CompatibilityError.prototype); 10 | } 11 | } 12 | 13 | export class PatchError extends Error { 14 | constructor(m: string) { 15 | super(m); 16 | Object.setPrototypeOf(this, PatchError.prototype); 17 | } 18 | } 19 | 20 | export type VersionPropertyStore = { 21 | [key: string]: { [key: string]: any }; 22 | }; 23 | 24 | export type ExploitConstructor = (new (statemanager: ExploitStateManager) => T) & { _name: string }; 25 | 26 | export interface ExploitPolicies{ 27 | stateManagerUnpatchLegal?: boolean; 28 | permanent?: boolean; 29 | }; 30 | 31 | export class AllocationContext { 32 | private allocatedAddresses: number[] = []; 33 | private currentAllocationPtr = 0; 34 | constructor(private stateManager: ExploitStateManager) { } 35 | _beginContext() { 36 | this.currentAllocationPtr = 0; 37 | } 38 | 39 | public allocate(length: number){ 40 | if(this.currentAllocationPtr >= this.allocatedAddresses.length){ 41 | this.allocatedAddresses.push(this.stateManager.allocate(length)); 42 | } 43 | return this.allocatedAddresses[this.currentAllocationPtr++]; 44 | } 45 | } 46 | 47 | export abstract class Exploit { 48 | public _policies?: ExploitPolicies; 49 | protected iface: NetMDInterface; 50 | protected factoryIface: NetMDFactoryInterface; 51 | private allocationContext: AllocationContext; 52 | constructor(protected stateManager: ExploitStateManager) { 53 | this.iface = stateManager.iface; 54 | this.factoryIface = stateManager.factoryIface; 55 | this.allocationContext = new AllocationContext(stateManager); 56 | } 57 | 58 | async init(...args: any[]) {} 59 | async unload() {} 60 | 61 | protected abstract getPropertyStore(allocationContext: AllocationContext): VersionPropertyStore; 62 | private _getPropertyStore(){ 63 | // In order to prevent allocation loops, AllocationContext caches the memory allocation results, 64 | // and always returns them in the same order they were allocated in (useState-style). 65 | this.allocationContext._beginContext(); 66 | return this.getPropertyStore(this.allocationContext); 67 | } 68 | 69 | protected getProperty(name: string): any { 70 | const store = this._getPropertyStore(); 71 | return getFromVersionStore(store, this.stateManager.device.versionCode, name); 72 | } 73 | 74 | protected getFirmwareProperty(name: string): any { 75 | return getFromVersionStore(CORE_MACROS.fw_compat_bridge.properties, this.stateManager.device.versionCode, name); 76 | } 77 | 78 | protected getPropertyOrNull(name: string): any { 79 | const store = this._getPropertyStore(); 80 | return getFromVersionStoreOrNull(store, this.stateManager.device.versionCode, name); 81 | } 82 | 83 | protected getProperties(...names: string[]): any[] { 84 | return names.map(this.getProperty.bind(this)); 85 | } 86 | 87 | protected assemble(code: string, variables?: { [key: string]: any }): Uint8Array { 88 | const store = this._getPropertyStore(); 89 | 90 | let assemblerUsed; 91 | if (code.trim().startsWith(';!THUMB')) { 92 | assemblerUsed = this.stateManager.thumbAssembler; 93 | } else { 94 | assemblerUsed = this.stateManager.armAssembler; 95 | } 96 | const processedCode = assemblerUsed.processCode( 97 | code, 98 | store, 99 | this.stateManager.device.versionCode, 100 | variables || {}, 101 | this.getPropertyOrNull('_macros') || {} 102 | ); 103 | try { 104 | return assemblerUsed.assemble(assemblerUsed.removeComments(processedCode)); 105 | } catch (error) { 106 | console.log('Error during the assembly of'); 107 | console.log(processedCode); 108 | throw error; 109 | } 110 | } 111 | 112 | assembleProperty(name: string, variables?: { [key: string]: any }): Uint8Array { 113 | return this.assemble(this.getProperty(name), variables); 114 | } 115 | 116 | log(...e: string[]){ 117 | this.stateManager.log(...e); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/exploits/atrac-recovery/atrac-recovery-interface.ts: -------------------------------------------------------------------------------- 1 | import { concatUint8Arrays, createAeaHeader, createWavHeader, sleep } from 'netmd-js/dist/utils'; 2 | import { Exploit } from '../../exploit'; 3 | import { arrayShallowEquals, formatUIntQuery } from '../../utils'; 4 | import { DiscFormat, readUTOCSector } from 'netmd-js'; 5 | import { discAddressToLogical, Fragment, getTitleByTrackNumber, ModeFlag, parseTOC } from 'netmd-tocmanip'; 6 | import { WaitForDiscToStopSpinning } from '../wait-for-disc-spinning'; 7 | 8 | interface TimeRepresentation { 9 | low: number; 10 | high: number; 11 | type: number; 12 | } 13 | 14 | // I have copied this code from the firmware. The conversion 15 | // of "LP / Mono" time to "SP" time happens on these "TimeRepresentation" objects. 16 | function timeToRepresentation(seconds: number) { 17 | let unk = Math.floor((seconds * 1566) / 100); 18 | return { 19 | low: unk & 0b00011111, 20 | high: (unk >> 5) & 0xffff, 21 | type: (unk & 1) == 0 ? 0 : 6, 22 | }; 23 | } 24 | 25 | function representationToTime({ low, high }: TimeRepresentation) { 26 | let unk = (high << 5) | low; 27 | let seconds = Math.floor((unk * 100) / 1566); 28 | return seconds; 29 | } 30 | 31 | function translateRepresentationToLP4(time: TimeRepresentation) { 32 | let newHigh = time.high >> 2, 33 | newLow = (time.high & 3) * 0x20 + (time.low >> 2), 34 | newType = (time.low & 3) * 0xb + (time.type >> 2); 35 | if (5 < newType && newLow * 1 == 0) { 36 | newLow += 1; 37 | } 38 | if (0x1f < newLow) { 39 | newLow -= 0x20; 40 | newHigh += 1; 41 | } 42 | time.high = newHigh; 43 | time.low = newLow; 44 | time.type = newType; 45 | } 46 | 47 | function translateRepresentationToLP2Mono(time: TimeRepresentation) { 48 | let newHigh = time.high >> 1, 49 | newLow = (time.high & 1) * 0x20 + (time.low >> 1), 50 | newType = ((time.low << 0x1f) >> 0x1f) * -0xb + (time.type >> 1); 51 | if (5 < newType && (newLow & 1) == 0) { 52 | newLow += 1; 53 | } 54 | if (0x1f < newLow) { 55 | newLow -= 0x20; 56 | newHigh += 1; 57 | } 58 | time.high = newHigh; 59 | time.low = newLow; 60 | time.type = newType; 61 | } 62 | 63 | function translateRepresentationFromLP4(time: TimeRepresentation) { 64 | let newLow, newHigh, newType, tempNewHigh, tempNewLow; 65 | 66 | tempNewLow = 0; 67 | tempNewHigh = time.high << 2; 68 | newHigh = tempNewHigh; 69 | 70 | for (newType = time.type * 4; 10 < newType; newType -= 0xb) tempNewLow += 2; 71 | for (tempNewLow = ((time.low >> 1) << 1) * 4; 0x1f < tempNewLow; tempNewLow -= 0x20) { 72 | tempNewHigh += 1; 73 | newHigh = tempNewHigh; 74 | } 75 | newLow = tempNewLow; 76 | if (5 < newType) { 77 | newLow |= 1; 78 | } 79 | time.type = newType; 80 | time.low = newLow; 81 | time.high = newHigh; 82 | } 83 | 84 | function translateRepresentationFromLP2Mono(time: TimeRepresentation) { 85 | let newLow, newHigh, newType, tempNewHigh, tempNewLow; 86 | 87 | tempNewLow = 0; 88 | tempNewHigh = time.high << 1; 89 | newHigh = tempNewHigh; 90 | 91 | for (newType = time.type * 2; 10 < newType; newType -= 0xb) tempNewLow += 2; 92 | for (tempNewLow = ((time.low >> 1) << 1) * 2; 0x1f < tempNewLow; tempNewLow -= 0x20) { 93 | tempNewHigh += 1; 94 | newHigh = tempNewHigh; 95 | } 96 | newLow = tempNewLow; 97 | if (5 < newType) { 98 | newLow |= 1; 99 | } 100 | time.type = newType; 101 | time.low = newLow; 102 | time.high = newHigh; 103 | } 104 | 105 | function validSector(atracData: Uint8Array) { 106 | for (let frame = (atracData.length / 212) - 1; frame >= 0; frame--) { 107 | const frameStartAddress = frame * 212; // 212 - one ATRAC sound frame. 108 | // TODO: we should consider checking whether or not the Block Size Modes are valid 109 | // The first two bits are the low band, third and fourth are the mid band, 110 | // fifth and sixth are the high band, and the final two bits are unused 111 | 112 | //Byte 0 and Byte 211 are the Block Size Mode 113 | //Byte 1 and Byte 210 are the number of Block Floating Units 114 | if ( 115 | atracData[frameStartAddress] !== atracData[frameStartAddress + 211] && 116 | atracData[frameStartAddress + 1] !== atracData[frameStartAddress + 210] 117 | ) { 118 | return false; 119 | } 120 | } 121 | return true; 122 | } 123 | 124 | const defaultReturn: (e: T) => (..._: any[]) => T = e => (..._: any[]) => e; 125 | 126 | export type AtracRecoveryConfig = { 127 | writeHeader?: boolean; 128 | removeLPBytes?: 'always' | 'auto' | 'never'; 129 | forceSeekCalculationMode?: 'SPM' | 'SPS' | 'LP2' | 'LP4'; 130 | startSeconds?: number; 131 | startSectors?: number; 132 | secondsToRead?: number; 133 | sectorsToRead?: number; 134 | verifyValidAtrac?: boolean; 135 | includeMetadataSection?: boolean; 136 | respectSoundgroupsBoundaries?: boolean; 137 | shouldCancelImmediately?: () => boolean; 138 | handleBadSector?: (address: string, countThisDRAM: number, seconds: number) => Promise<'reload' | 'abort' | 'skip' | 'yieldanyway'>; 139 | }; 140 | 141 | const defaultConfig: AtracRecoveryConfig = { 142 | writeHeader: true, 143 | removeLPBytes: 'auto', 144 | forceSeekCalculationMode: undefined, 145 | startSeconds: 0, 146 | startSectors: -1, 147 | secondsToRead: -1, 148 | sectorsToRead: -1, 149 | verifyValidAtrac: true, 150 | includeMetadataSection: false, 151 | respectSoundgroupsBoundaries: true, 152 | shouldCancelImmediately: defaultReturn(false), 153 | handleBadSector: defaultReturn(Promise.resolve('reload')), 154 | }; 155 | 156 | export abstract class AtracRecovery extends Exploit { 157 | public static _name = 'AtracRecovery'; 158 | public abstract readNextSector(callback?: (data: { read: number; length: number }) => void): Promise; 159 | public abstract setSectorToRead(sector: number, sectorsRemaining: number): Promise; 160 | 161 | public abstract startDownload(track: number): Promise; 162 | public abstract finishDownload(): Promise; 163 | 164 | async downloadTrack( 165 | track: number, 166 | progressCallback?: (data: { read: number; total: number; action: 'READ' | 'SEEK' | 'CHUNK'; sector?: string }) => void, 167 | config?: AtracRecoveryConfig 168 | ) { 169 | let finalData: Uint8Array[] = []; 170 | for await (let chunk of this.downloadTrackGenerator(track, progressCallback, config)) { 171 | if(chunk.chunk && !['callMetadata', 'toc'].includes(chunk.type)){ 172 | finalData.push(chunk.chunk); 173 | } 174 | } 175 | return concatUint8Arrays(...finalData); 176 | } 177 | 178 | async downloadTrackWithMarkers( 179 | track: number, 180 | progressCallback?: (data: { read: number; total: number; action: 'READ' | 'SEEK' | 'CHUNK'; sector?: string }) => void, 181 | config?: AtracRecoveryConfig 182 | ){ 183 | let finalData: Uint8Array[] = []; 184 | const te = new TextEncoder(); 185 | const map: {[chunkType in 'header' | 'audioData' | 'dramSplit' | 'metadata' | 'callMetadata' | 'toc']: Uint8Array} = { 186 | audioData: te.encode('A'), 187 | dramSplit: te.encode('D'), 188 | header: te.encode('H'), 189 | metadata: te.encode('M'), 190 | callMetadata: te.encode('C'), 191 | toc: te.encode('T'), 192 | }; 193 | 194 | finalData.push(te.encode("NEX-RAWSTREAM")); 195 | 196 | for await (let chunk of this.downloadTrackGenerator(track, progressCallback, config)) { 197 | finalData.push(map[chunk.type]); 198 | finalData.push(formatUIntQuery("%d", chunk.chunk?.length ?? 0)); 199 | if(chunk.chunk){ 200 | finalData.push(chunk.chunk); 201 | } 202 | } 203 | return concatUint8Arrays(...finalData); 204 | } 205 | 206 | async *downloadTrackGenerator( 207 | track: number, 208 | progressCallback?: (data: { read: number; total: number; action: 'READ' | 'SEEK' | 'CHUNK'; sector?: string }) => void, 209 | config?: AtracRecoveryConfig 210 | ): AsyncGenerator<{chunk?: Uint8Array, type: 'header' | 'audioData' | 'dramSplit' | 'metadata' | 'callMetadata' | 'toc' }> { 211 | // Merge the configs 212 | if (!config) config = {}; 213 | for (let [k, v] of Object.entries(defaultConfig)) { 214 | if (!(k in config)) { 215 | (config as any)[k] = v; 216 | } 217 | } 218 | if (config.secondsToRead! > 0) { 219 | config.sectorsToRead = Math.floor(config.secondsToRead! / (704 / 11025)); 220 | } 221 | if (config.startSeconds === -1 && config.startSectors !== -1) { 222 | config.startSeconds = Math.floor(config.startSectors! * (704 / 11025)); 223 | } 224 | let time = 0, 225 | bufferStartTime = 0; 226 | 227 | let nextSectorBeginRemove = 0; 228 | let nextSectorBeginInclude = 0; 229 | 230 | try { 231 | // Get info about the track - what format is it and what's the final sector 232 | const sector0 = await readUTOCSector(this.stateManager.factoryIface, 0); 233 | 234 | yield { type: 'toc', chunk: sector0 }; 235 | 236 | const toc = parseTOC(sector0); 237 | const trackRootFragment = toc.trackFragmentList[toc.trackMap[track + 1]]; 238 | const formatStr = 239 | ((trackRootFragment.mode & ModeFlag.F_STEREO) !== 0 ? '1' : '0') + 240 | ((trackRootFragment.mode & ModeFlag.F_SP_MODE) !== 0 ? '1' : '0'); 241 | 242 | let format = { 243 | '00': 'LP4', 244 | '10': 'LP2', 245 | '11': 'SPS', 246 | '01': 'SPM', 247 | }[formatStr]!; 248 | 249 | if (config.forceSeekCalculationMode) { 250 | format = config.forceSeekCalculationMode; 251 | } 252 | 253 | let sectorsRead = 0; 254 | let sectorsToReadAmount = 0, 255 | allSectors = 0; 256 | 257 | let thisTrackSectorRanges: [number, number][] = []; 258 | let metadataSectionBlacklist: number[][] = []; 259 | let sectorSplices: { [key: number]: { start?: number, end?: number } } = {}; 260 | const rawAddressToLogical = (data: number[]) => ((data[0] << 8) | data[1]) * 32 + data[2]; 261 | const addFragment = (fragment: Fragment) => { 262 | let endSector = discAddressToLogical(fragment.end); 263 | let startSector = discAddressToLogical(fragment.start); 264 | allSectors += endSector - startSector; 265 | allSectors += 1; 266 | thisTrackSectorRanges.push([startSector, endSector]); 267 | 268 | sectorSplices[discAddressToLogical(fragment.start)] = { 269 | start: fragment.start.group 270 | }; 271 | sectorSplices[discAddressToLogical(fragment.end) & ~1] = { 272 | end: fragment.end.group 273 | } 274 | }; 275 | const isPartOfThisTrack = (logical: number) => thisTrackSectorRanges.some(([start, end]) => logical >= start && logical <= end); 276 | const fromSectorAddressToString = (raw: number[]) => 277 | `${((raw[0] << 8) | raw[1]).toString(16).padStart(4, '0')}.${raw[2].toString(16).padStart(2, '0')}`; 278 | 279 | let lastFragment = trackRootFragment; 280 | while (lastFragment.link !== 0) { 281 | addFragment(lastFragment); 282 | lastFragment = toc.trackFragmentList[lastFragment.link]; 283 | } 284 | addFragment(lastFragment); 285 | 286 | yield {type: 'callMetadata', chunk: new TextEncoder().encode(JSON.stringify({ 287 | format, 288 | sectorSplices, 289 | thisTrackSectorRanges, 290 | track, 291 | }))}; 292 | 293 | sectorsToReadAmount = (config.sectorsToRead === -1 ? allSectors : Math.min(config.sectorsToRead!, allSectors)); 294 | 295 | let absoluteTimeStart = new Date().getTime(); 296 | 297 | const sleeper = await this.stateManager.require(WaitForDiscToStopSpinning); 298 | // 'sleeper.wait' is assigned to a new variable in case a new, alternative way to wait 299 | // until the exploit is ready is engineered 300 | const wait = sleeper.wait.bind(sleeper); 301 | 302 | if (config.writeHeader) { 303 | if (format.startsWith('LP')) { 304 | yield { 305 | chunk: createWavHeader(format === 'LP2' ? DiscFormat.lp2 : DiscFormat.lp4, sectorsToReadAmount * (2332 - 20 * 11)), 306 | type: 'header', 307 | }; 308 | } else { 309 | // We might as well give it the correct name 310 | const sector1 = await readUTOCSector(this.stateManager.factoryIface, 1); 311 | const nameToc = parseTOC(null, sector1); 312 | const name = getTitleByTrackNumber(nameToc, track + 1); 313 | yield { 314 | chunk: createAeaHeader(name, (trackRootFragment.mode & ModeFlag.F_STEREO) !== 0 ? 2 : 1, sectorsToReadAmount * 11), 315 | type: 'header', 316 | }; 317 | } 318 | } 319 | 320 | await this.iface.gotoTrack(track); 321 | await this.iface.gotoTime( 322 | track, 323 | Math.floor(config.startSeconds! / 3600), 324 | Math.floor((config.startSeconds! % 3600) / 60), 325 | config.startSeconds! % 60 326 | ); 327 | await wait(); 328 | 329 | await this.startDownload(track); 330 | await this.setSectorToRead(0, sectorsToReadAmount + 2); 331 | 332 | if (progressCallback) 333 | progressCallback({ 334 | read: sectorsRead, 335 | total: sectorsToReadAmount, 336 | action: 'SEEK', 337 | }); 338 | 339 | // This is experimental code. It might not work in all cases. 340 | let sector = 0; 341 | 342 | let sectorPositionsRead: number[] = []; 343 | let isAfterBlacklist = false; 344 | let alignmentKnockBack = 0; 345 | 346 | bufferStartTime = new Date().getTime(); 347 | 348 | let badSectorCounter = 0; 349 | const MAX_BAD_SECTORS = 15; 350 | 351 | mainLoop: while (sector < 0xfff && !config.shouldCancelImmediately!()) { 352 | if (sectorsRead >= sectorsToReadAmount && nextSectorBeginInclude === 0) { 353 | this.log(`Dumping complete in ${time} / ${new Date().getTime() - absoluteTimeStart} ms!`); 354 | await this.finishDownload(); 355 | return; 356 | } 357 | let sectorContent = await this.readNextSector( 358 | progressCallback && 359 | ((data) => { 360 | progressCallback({ 361 | read: data.read + sectorsRead, 362 | total: sectorsToReadAmount, 363 | action: 'CHUNK', 364 | }); 365 | }) 366 | ); 367 | sector++; 368 | 369 | const sectorMetadata = Array.from(sectorContent.subarray(0, 20)); 370 | const currentPosition = sectorMetadata.slice(sectorMetadata.length - 8, sectorMetadata.length - 5); 371 | const currentLogical = rawAddressToLogical(currentPosition); 372 | let audioData = sectorContent.subarray(20); 373 | 374 | let wasPartOfBeginEndMitigation = false; 375 | 376 | if(nextSectorBeginInclude){ 377 | audioData = audioData.subarray(0, nextSectorBeginInclude); 378 | nextSectorBeginInclude = 0; 379 | wasPartOfBeginEndMitigation = true; 380 | }else if(nextSectorBeginRemove){ 381 | audioData = audioData.subarray(nextSectorBeginRemove); 382 | nextSectorBeginRemove = 0; 383 | wasPartOfBeginEndMitigation = true; 384 | }else if (currentLogical in sectorSplices && config.respectSoundgroupsBoundaries) { 385 | let start = sectorSplices[currentLogical].start ?? 0; 386 | let end = sectorSplices[currentLogical].end ?? -1; 387 | if(start >= 5){ 388 | start -= 5.5; 389 | } 390 | const startByte = start * 424; 391 | const endByte = end === -1 ? audioData.length : ((end + 1) * 424); 392 | if(startByte > audioData.length){ 393 | nextSectorBeginInclude = startByte - audioData.length; 394 | sectorPositionsRead.push(currentLogical); 395 | sectorsRead += 1; 396 | isAfterBlacklist = false; 397 | continue; 398 | } 399 | if(endByte > audioData.length){ 400 | nextSectorBeginInclude = endByte - audioData.length + startByte; 401 | audioData = audioData.subarray(startByte); 402 | }else{ 403 | audioData = audioData.subarray(startByte, endByte); 404 | } 405 | } 406 | 407 | // this.log(`Read sector 0x${sector.toString(16)} of DRAM. It contains the disc sector 0x${fromSectorAddressToString(currentPosition)}(0x${currentLogical.toString(16)})`); 408 | invalidSector: if ((!validSector(audioData) && config.verifyValidAtrac) || (!isPartOfThisTrack(currentLogical) && !wasPartOfBeginEndMitigation) || sectorPositionsRead.includes(currentLogical)) { 409 | if (sectorContent.length === 2352 && metadataSectionBlacklist.some((e) => arrayShallowEquals(sectorMetadata, e))) { 410 | this.log( 411 | `Read sector with metadata block in the blacklist (@${fromSectorAddressToString( 412 | currentPosition 413 | )}). It will be skipped.` 414 | ); 415 | if (isAfterBlacklist) { 416 | sectorsRead++; 417 | isAfterBlacklist = false; 418 | } 419 | continue; 420 | } 421 | // We're past the ATRAC data and are reading garbage data from the device's DRAM. 422 | // Calculate the time offset, move the head using standard netmd commands. 423 | // The 704/11025 fraction is a simplified version of (11/2) * (512/44100), which expresses 424 | // 11 soundgroups/2 sectors, where each soundgroup contain 512 samples / sample rate of 44100 425 | let originalSeconds = sectorsRead * (704 / 11025) - alignmentKnockBack; 426 | if (format !== 'SPS') { 427 | // It's not SP Stereo 428 | // Use the reverse of the functions used in the gotoTrack handler within the firmware, to convert the "SP" time 429 | // SP time >> LP time >> SP time 430 | // \_____/ \______/ \_____/ 431 | // \/ \/ \/ 432 | // local var time given time converted 433 | // seconds to gotoTrack back within 434 | // the firmware 435 | // (@0x00012526 for B1.600) 436 | let representation = timeToRepresentation(originalSeconds); 437 | switch (format) { 438 | case 'SPM': 439 | case 'LP2': 440 | translateRepresentationFromLP2Mono(representation); 441 | break; 442 | case 'LP4': 443 | translateRepresentationFromLP4(representation); 444 | break; 445 | } 446 | originalSeconds = representationToTime(representation); 447 | } 448 | 449 | if (sector < 100) { 450 | // This can be considered a bad sector 451 | const behavior = await config.handleBadSector!(fromSectorAddressToString(currentPosition), sector, originalSeconds); 452 | if (behavior === 'skip') continue; 453 | if (behavior === 'abort') break mainLoop; 454 | if (behavior === 'yieldanyway') break invalidSector; 455 | // if behavior === 'reload', do as previously. 456 | 457 | badSectorCounter++; 458 | if(badSectorCounter > MAX_BAD_SECTORS) { 459 | // Crash the transfer. This track is unrecoverable. 460 | this.log(`In order to preserve the connected device's laser the transfer has been aborted, as the number of consecutively read bad sectors has crossed the safety threshold (${MAX_BAD_SECTORS}).`); 461 | await this.finishDownload(); 462 | return; 463 | } 464 | } 465 | let thisBufferTime = new Date().getTime() - bufferStartTime; 466 | this.log( 467 | `The sector read is not an ATRAC sector. Sectors read previously add up to ${originalSeconds} seconds. Jumping to that time` 468 | ); 469 | metadataSectionBlacklist.push([...sectorMetadata]); 470 | isAfterBlacklist = true; 471 | 472 | let alignmentTries = 0; 473 | let thisAlignmentKnockBack = 0; 474 | for (; ;) { 475 | let seconds = config.startSeconds! + originalSeconds - thisAlignmentKnockBack; 476 | let hours = Math.floor(seconds / 3600); 477 | seconds -= hours * 3600; 478 | let minutes = Math.floor(seconds / 60); 479 | seconds -= minutes * 60; 480 | 481 | this.log( 482 | `Jumping to ${hours}:${minutes}:${seconds} (knockback: ${thisAlignmentKnockBack} + ${alignmentKnockBack})` 483 | ); 484 | 485 | if (progressCallback) 486 | progressCallback({ 487 | read: sectorsRead, 488 | total: sectorsToReadAmount, 489 | action: 'SEEK', 490 | }); 491 | 492 | await this.finishDownload(); // Switch back to normal commands. 493 | await sleep(1000); 494 | await this.iface.stop(); 495 | let tries = 0; 496 | while (true) { 497 | try { 498 | await this.iface.gotoTime(track, hours, minutes, seconds); 499 | break; 500 | } catch (ex) { 501 | this.log('While jumping'); 502 | console.log(ex); 503 | if (tries++ > 5) { 504 | throw new Error('Cannot jump to time!'); 505 | } 506 | await sleep(1000); 507 | } 508 | } 509 | await this.iface.stop(); 510 | await wait(); 511 | await this.startDownload(track); // Switch to ATRAC recovery. 512 | 513 | await this.setSectorToRead(0, 1); 514 | 515 | let syncSector = await this.readNextSector(); 516 | let position = Array.from(syncSector.slice(20 - 8, 20 - 5)); 517 | this.log(`After moving the head, position is ${position.map((n) => n.toString(16).padStart(2, '0')).join('')}`); 518 | 519 | let alignmentPosition = [...sectorPositionsRead].reverse().indexOf(rawAddressToLogical(position)); 520 | if (alignmentPosition !== -1) alignmentPosition++; 521 | 522 | if (alignmentPosition === -1) { 523 | // Cannot align. 524 | if (alignmentTries < 3) { 525 | alignmentTries++; 526 | thisAlignmentKnockBack += 2; // Try to align 527 | continue; 528 | } 529 | this.log('DEBUG'); 530 | this.log('Could not align!'); 531 | this.log(`originalSeconds=${originalSeconds}`); 532 | this.log(`thisAlignmentKnockBack=${thisAlignmentKnockBack}`); 533 | this.log(`alignmentKnockBack=${alignmentKnockBack}`); 534 | this.log( 535 | `syncSector=${Array.from(syncSector) 536 | .map((n) => n.toString(16).padStart(2, '0')) 537 | .join('')}` 538 | ); 539 | this.log(`sectorPositionsRead=${sectorPositionsRead.join(', ')}`); 540 | this.log('DEBUG END'); 541 | await this.finishDownload(); 542 | throw new Error('Cannot align!'); 543 | } 544 | sector = 0; 545 | alignmentTries = 0; 546 | this.log(`Aligned! Running ${alignmentPosition} sectors behind!`); 547 | yield { type: 'dramSplit' }; 548 | time += thisBufferTime; 549 | bufferStartTime = new Date().getTime(); 550 | await this.setSectorToRead(alignmentPosition, sectorsToReadAmount - sectorsRead + 2); 551 | alignmentKnockBack += thisAlignmentKnockBack; 552 | break; 553 | } 554 | continue; 555 | } 556 | // It's a valid ATRAC-containing sector. 557 | // Add its position to the list of already done sectors. 558 | badSectorCounter = 0; 559 | sectorPositionsRead.push(currentLogical); 560 | if (config.includeMetadataSection) yield { chunk: new Uint8Array(sectorMetadata), type: 'metadata' }; 561 | if ((format.startsWith('LP') && config.removeLPBytes === 'auto') || config.removeLPBytes === 'always') { 562 | // It's LP - remove the SP compatibility bytes. 563 | // There are 12 zero-bytes at the start or each frame 564 | // and 8 at the end. 565 | const sectorAsArray = Array.from(audioData); 566 | for (let frame = 10; frame >= 0; frame--) { 567 | const frameStartAddress = frame * 212; // 212 - one ATRAC sound frame. 568 | sectorAsArray.splice(frameStartAddress + 212 - 8, 8); 569 | sectorAsArray.splice(frameStartAddress, 12); 570 | } 571 | yield { 572 | chunk: new Uint8Array(sectorAsArray), 573 | type: 'audioData', 574 | }; 575 | } else { 576 | yield { 577 | chunk: audioData, 578 | type: 'audioData', 579 | }; 580 | } 581 | sectorsRead += 1; 582 | // We got a good sector, so this is no longer true 583 | isAfterBlacklist = false; 584 | progressCallback?.({ 585 | read: sectorsRead, 586 | total: sectorsToReadAmount, 587 | action: 'READ', 588 | sector: fromSectorAddressToString(currentPosition), 589 | }); 590 | } 591 | } catch (ex) { 592 | await this.finishDownload(); // If anything goes wrong, at least try to put the device back into a usable state. 593 | throw ex; 594 | } 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/exploits/atrac-recovery/cached-sector-bulk-transfers.ts: -------------------------------------------------------------------------------- 1 | import { cleanWrite, MemoryType, writeOfAnyLength } from 'netmd-js'; 2 | import { AllocationContext } from '../../exploit'; 3 | import { formatUIntQuery } from '../../utils'; 4 | import { HiMDUnboundedReading } from '../himd-unbounded-reading'; 5 | import { USBCodeExecution } from '../usb-code-execution'; 6 | import { AtracRecovery } from './atrac-recovery-interface'; 7 | 8 | /* The original non-ATRAC bulk transfer initialization code was written by Sir68k (https://github.com/Sir68k). */ 9 | export class CachedSectorBulkDownload extends AtracRecovery { 10 | public static _name = 'CachedSectorBulkDownload'; 11 | getPropertyStore(ctx: AllocationContext) { 12 | return { 13 | _macros: { 14 | "*": { 15 | ldr1: { // Read to R1 16 | code: ` 17 | ldr r0, %macro_argument_0 18 | ldr r1, [ r0 ] 19 | `, 20 | properties: {}, 21 | }, 22 | wrr1: { // Write R1 23 | code: ` 24 | ldr r0, %macro_argument_0 25 | str r1, [ r0 ] 26 | `, 27 | properties: {}, 28 | }, 29 | wrim: { // Write immediate 30 | code: ` 31 | mov r1, %macro_argument_1 32 | @wrr1 %macro_argument_0 33 | `, 34 | properties: {}, 35 | }, 36 | wrc: { // Write constant 37 | code: ` 38 | ldr r1, %macro_argument_1 39 | @wrr1 %macro_argument_0 40 | `, 41 | properties: {}, 42 | } 43 | }, 44 | }, 45 | init_code: { 46 | 'S*': ` 47 | _loader: 48 | push { r3, r4, r5, r6, r7, lr } 49 | 50 | ldr r3, patch_spot 51 | ldr r4, patch_content 52 | mov r5, #$patch_slot_1 53 | bl _patch 54 | 55 | ldr r3, patch_spot 56 | add r3, r3, #4 57 | ldr r4, resident_code_address 58 | mov r5, #$patch_slot_2 59 | bl _patch 60 | 61 | ldr r3, patch_spot_3 62 | ldr r4, bx_lr_thumb 63 | mov r5, #$patch_slot_3 64 | bl _patch 65 | 66 | pop { r3, r4, r5, r6, r7, lr } 67 | bx lr 68 | 69 | patch_content: .word 0x47004800 70 | bx_lr_thumb: .word 0x4770bcf0 71 | @bridge word patch_spot patch_spot_3 resident_code_address 72 | _patch: 73 | @patch 74 | `, 75 | }, 76 | prep_code_loader: { 77 | 'S*,H*': ` 78 | @bxar r0, $prep_code_address 79 | `, 80 | }, 81 | prep_code: { 82 | 'S*': ` 83 | @wrim sector_offset, #0x700 84 | @wrim should_cleanup_atrac, #0 85 | 86 | @ldr1 p_dma_mode 87 | bic r1, r1, #1 88 | @wrr1 p_dma_mode 89 | @wrim p_dma_x, #0 90 | @wrim p_z, #0x21 91 | @wrc p_dma_mode, const_p_dma_mode 92 | 93 | @wrc p_usb_mode, const_p_usb_mode 94 | @wrc p_uo_100, const_p_uo_100 95 | @wrc p_uo_104, const_p_uo_104 96 | @wrim p_uo_108, #0x100 97 | 98 | @ldr1 p_uo_108 99 | bic r1, r1, #512 100 | @wrr1 p_uo_108 101 | 102 | @ldr1 p_uo_100 103 | mov r0, #0x100 104 | orr r1, r1, r0 105 | @wrr1 p_uo_100 106 | 107 | bx lr 108 | 109 | const_p_dma_mode: .word 0x33D0040 110 | const_p_usb_mode: .word 0x7310100 111 | const_p_uo_100: .word 0x800 112 | const_p_uo_104: .word 0xf301 113 | 114 | @bridge word sector_start sector_offset sector_buffer should_cleanup_atrac 115 | @bridge word p_dma_mode p_dma_x p_z p_usb_mode p_uo_100 p_uo_104 p_uo_108 116 | `, 117 | 'Hr*,Hn*': ` 118 | ; Sector_end => bytes_count in himd 119 | ; Sector_start => bytes_start in himd 120 | 121 | ldr r0, sector_start 122 | ldr r0, [ r0 ] 123 | ldr r1, sector_end 124 | @bxar r2, $usb_bulk_dma_func 125 | 126 | @bridge word sector_start sector_end 127 | `, 128 | }, 129 | cleanupCode: { 130 | '*': `@autogen_cleanup $patch_slot_1, $patch_slot_2, $patch_slot_3` 131 | }, 132 | bulk_transfer_code: { 133 | 'S*': ` 134 | @fw_compat_bridge 135 | ; The resident code responsible for handling bulk transfers 136 | ; Main check 137 | @ldr1 p_usb_state 138 | lsrs r1, r1, #0x1a 139 | bcc return 140 | 141 | ; Main non-interrupt loop 142 | loop: 143 | @ldr1 p_uo_104 144 | lsrs r1, r1, #9 145 | bcc loop 146 | 147 | @wrim p_uo_104, #0x100 148 | 149 | @ldr1 sector_offset 150 | mov r0, #608 151 | cmp r1, r0 152 | bhs next_sector 153 | 154 | send_sector: 155 | @wrc p_uo_100, const_p_uo_100 156 | @ldr1 sector_offset 157 | lsl r1, r1, #2 ; *4 158 | ldr r0, sector_buffer 159 | add r1, r1, r0 160 | @wrr1 p_dma_src 161 | @wrc p_dma_dst, p_uo_114 162 | @wrim p_dma_count, #32 163 | 164 | @wrc p_dma_mode, const_p_dma_mode 165 | @ldr1 sector_offset 166 | add r1, r1, #32 167 | @wrr1 sector_offset 168 | b loop 169 | 170 | next_sector: 171 | @ldr1 should_cleanup_atrac 172 | cmp r1, #0 173 | beq _nokill 174 | ldr r0, p_dram_transfer_ctl_a 175 | mov r1, #0xa0 176 | strb r1, [ r0 ] 177 | ldrb r1, [ r0 ] 178 | mov r2, #0x80 179 | bic r1, r1, r2 180 | strb r1, [ r0 ] 181 | _nokill: 182 | @wrim should_cleanup_atrac, #1 183 | 184 | 185 | ldr r3, sector_start 186 | ldr r0, [ r3 ] 187 | ldr r1, sector_end 188 | ldr r1, [ r1 ] 189 | cmp r0, r1 190 | bhs return 191 | 192 | add r2, r0, #1 193 | str r2, [ r3 ] 194 | 195 | mov r1, #0 196 | mov r3, #152 197 | lsl r3, r3, #4 198 | @blxar r4, $read_atrac_dram 199 | ; A very temporary solution. The contents of this function should be pasted here instead. 200 | 201 | @wrim sector_offset, #0 202 | b send_sector 203 | 204 | return: 205 | pop { r4, r5, r7 } 206 | pop { r3 } 207 | bx r3 208 | 209 | const_p_uo_100: .word 0x0800100 210 | const_p_dma_mode: .word 0x33D0041 211 | @bridge word sector_offset sector_start sector_end sector_buffer should_cleanup_atrac 212 | @bridge word p_usb_state p_uo_104 p_uo_100 p_uo_114 p_dma_src p_dma_dst p_dma_count p_dma_mode 213 | @bridge word p_dram_transfer_ctl_a 214 | `, 215 | }, 216 | 217 | resident_code_address: { '*': ctx.allocate(512) }, 218 | prep_code_address: { '*': ctx.allocate(256) }, 219 | sector_offset: { '*': ctx.allocate(4) }, 220 | sector_start: { '*': ctx.allocate(4) }, 221 | sector_end: { '*': ctx.allocate(4) }, 222 | should_cleanup_atrac: { '*': ctx.allocate(4) }, 223 | sector_buffer: { '*': 0x03808000, 'Hn*': 0x05009400, 'Hr*': 0x05030900 }, 224 | patch_slot_1: { '*': this.patchSlot1 }, 225 | patch_slot_2: { '*': this.patchSlot2 }, 226 | patch_slot_3: { '*': this.patchSlot3 }, 227 | 228 | // USB 229 | p_usb_state: { "S*": 0x0380c010 }, 230 | p_usb_mode: { "S*": 0x0380c014 }, 231 | p_uo_100: { "S*": 0x0380c100 }, 232 | p_uo_104: { "S*": 0x0380c104 }, 233 | p_uo_108: { "S*": 0x0380c108 }, 234 | p_uo_10c: { "S*": 0x0380c10c }, 235 | p_uo_114: { "S*": 0x0380c114 }, 236 | 237 | // DMA 238 | p_dma_src: { 'S*': 0x03803000 }, 239 | p_dma_dst: { 'S*': 0x03803004 }, 240 | p_dma_count: { 'S*': 0x03803008 }, 241 | p_dma_mode: { 'S*': 0x0380300c }, 242 | p_dma_x: { 'S*': 0x03803044 }, 243 | p_z: { 'S*': 0x03804040 }, 244 | 245 | p_dram_transfer_ctl_a: { 'S*': 0x0300b114 }, 246 | 247 | patch_spot: { 248 | 'S1.600': 0x000780a0, 249 | 'S1.500': 0x000776a8, 250 | 'S1.400': 0x00076fd4, 251 | 'S1.300': 0x00073914, 252 | 'S1.200': 0x00072260, 253 | 'S1.100': 0x000718d8, 254 | 'S1.000': 0x00078208, 255 | }, 256 | patch_spot_3: { 257 | 'S1.600': 0x00078264, 258 | 'S1.500': 0x0007786c, 259 | 'S1.400': 0x00077198, 260 | 'S1.300': 0x00073ad8, 261 | 'S1.200': 0x00072424, 262 | 'S1.100': 0x00071a9c, 263 | 'S1.000': 0x000783cc, 264 | }, 265 | 266 | usb_bulk_dma_func: { 267 | 'Hr1.000': 0x000b3c0d, 268 | 269 | 'Hn1.000': 0x000bddb9, 270 | 'Hn1.100': 0x000bdef1, 271 | 'Hn1.10A': 0x000bdf79, 272 | 'Hn1.200': 0x000bdf5d, 273 | }, 274 | }; 275 | } 276 | 277 | 278 | currentTrack = -1; 279 | patchSlot1: number = -1; 280 | patchSlot2: number = -1; 281 | patchSlot3: number = -1; 282 | cachedSectors: Uint8Array = new Uint8Array(); 283 | cachedSectorsPtr: number = 0; 284 | currentSector: number = 0; 285 | sectorsRemaining: number = 0; 286 | 287 | async init() { 288 | await super.init(); 289 | await this.stateManager.require(HiMDUnboundedReading); 290 | await writeOfAnyLength( 291 | this.factoryIface, 292 | this.getProperty('prep_code_address'), 293 | this.assembleProperty('prep_code'), 294 | MemoryType.MAPPED 295 | ); 296 | this.log("Prep OK!"); 297 | if(!this.stateManager.device.isHimd){ 298 | this.patchSlot1 = this.stateManager.getIncrementalPatchNumber(); 299 | this.patchSlot2 = this.stateManager.getIncrementalPatchNumber(); 300 | this.patchSlot3 = this.stateManager.getIncrementalPatchNumber(); 301 | await writeOfAnyLength( 302 | this.factoryIface, 303 | this.getProperty('resident_code_address'), 304 | this.assembleProperty('bulk_transfer_code'), 305 | MemoryType.MAPPED 306 | ); 307 | this.log("Resident OK!"); 308 | const usbExecution = await this.stateManager.require(USBCodeExecution); 309 | await usbExecution.execute(this.assembleProperty('init_code')); 310 | } 311 | this.log('Init OK!'); 312 | } 313 | 314 | async unload() { 315 | // As this exploit patches the USB code, we can't depend on the stateManager to free the patches 316 | if(!this.stateManager.device.isHimd){ 317 | const usbExecution = await this.stateManager.require(USBCodeExecution); 318 | await usbExecution.execute(this.assembleProperty('cleanupCode')); 319 | this.stateManager.freePatch(this.patchSlot1); 320 | this.stateManager.freePatch(this.patchSlot2); 321 | this.stateManager.freePatch(this.patchSlot3); 322 | } 323 | } 324 | 325 | async reloadBuffer(sectorsCount: number, callback?: ((data: { read: number; length: number }) => void)) { 326 | let sectorStartContent = 0, sectorEndContent = 0, sectorLength = 0; 327 | if(this.stateManager.device.isHimd){ 328 | sectorLength = 2368; 329 | sectorStartContent = this.getProperty("sector_buffer") + 2368 * this.currentSector; 330 | if(sectorsCount + this.currentSector > this.sectorsPerLoad){ 331 | sectorsCount = this.sectorsPerLoad - this.currentSector; 332 | } 333 | if(sectorsCount === 0){ 334 | // Would deadlock the device 335 | // Generate a fake sector. As the sector is of incorrect length, this will trigger 336 | // The buffer flushing code in the main exploit. 337 | this.cachedSectorsPtr = 0; 338 | this.cachedSectors = new Uint8Array(20).fill(255); 339 | return; 340 | } 341 | sectorEndContent = sectorsCount * 2368; 342 | }else{ 343 | sectorLength = 2432; 344 | sectorStartContent = this.currentSector; 345 | sectorEndContent = this.currentSector + sectorsCount; 346 | } 347 | await cleanWrite( 348 | this.factoryIface, 349 | this.getProperty('sector_start'), 350 | formatUIntQuery('% ${this.currentSector + sectorsCount}`); 360 | await (await this.stateManager.require(USBCodeExecution)).execute(this.assembleProperty('prep_code_loader')); 361 | this.log("All set - begin bulk read!"); 362 | this.cachedSectorsPtr = 0; 363 | this.cachedSectors = new Uint8Array( 364 | await this.stateManager.iface.netMd.readBulkToArray( 365 | sectorsCount * sectorLength, 366 | sectorsCount * sectorLength, 367 | callback && ((length, read) => callback({ read: Math.floor(read / sectorLength), length: sectorsCount })) 368 | ) 369 | ); 370 | } 371 | 372 | sectorsPerLoad = this.stateManager.device.isHimd ? 3455 : 900; 373 | 374 | async readNextSector(callback?: (data: { read: number; length: number }) => void): Promise { 375 | if (this.currentTrack === -1) throw new Error('You need to startDownload() first'); 376 | if (this.cachedSectorsPtr >= this.cachedSectors.length) { 377 | await this.reloadBuffer(Math.min(this.sectorsRemaining, this.sectorsPerLoad), callback); 378 | } 379 | const slice = this.cachedSectors.slice(this.cachedSectorsPtr, this.cachedSectorsPtr + 2352); 380 | this.currentSector++; 381 | this.cachedSectorsPtr += this.stateManager.device.isHimd ? 2368 : 2432; 382 | return slice; 383 | } 384 | 385 | async setSectorToRead(sector: number, sectorsRemaining: number): Promise { 386 | if (this.currentTrack === -1) throw new Error('You need to startDownload() first'); 387 | this.currentSector = sector; 388 | this.sectorsRemaining = sectorsRemaining; 389 | // Force cache reload on next sector 390 | this.cachedSectorsPtr = this.cachedSectors.length + 1; 391 | } 392 | 393 | async startDownload(track: number): Promise { 394 | this.currentTrack = track; 395 | } 396 | 397 | async finishDownload(): Promise { 398 | this.currentTrack = -1; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /src/exploits/atrac-recovery/cached-sector-control-transfers.ts: -------------------------------------------------------------------------------- 1 | import { cleanWrite, display, DisplayMode, MemoryType } from 'netmd-js'; 2 | import { VersionPropertyStore } from '../../exploit'; 3 | import { formatUIntQuery } from '../../utils'; 4 | import { USBCodeExecution } from '../usb-code-execution'; 5 | import { AtracRecovery } from './atrac-recovery-interface'; 6 | 7 | export class CachedSectorControlDownload extends AtracRecovery { 8 | public static _name = 'CachedSectorControlDownload'; 9 | 10 | protected getPropertyStore(): VersionPropertyStore { 11 | return { 12 | mainCode: { 13 | '*': ` 14 | @fw_compat_bridge 15 | 16 | ; Copy the payload from _payload to _address, ending when encountered _marker 17 | ldr r0, _address 18 | ldr r3, _marker 19 | adr r1, _payload 20 | 21 | loop: 22 | ldr r2, [ r1 ], #4 23 | cmp r2, r3 24 | beq finish 25 | str r2, [ r0 ], #4 26 | b loop 27 | finish: 28 | ; Backup return address 29 | mov r7, lr 30 | 31 | ; Zero-out enabledFlagAddress - default to normal USB behavior 32 | ldr r1, enabledFlagAddress 33 | mov r0, #0 34 | str r0, [ r1 ] 35 | 36 | ; Zero-out ATRAC sector - start from sector 0 37 | ldr r1, sectorToReadAddress 38 | mov r0, #0 39 | str r0, [ r1 ] 40 | 41 | ; Edit the firmware - redirect USB reading to the code loaded at $residentCodeAddress 42 | mov r5, #$patchSlot1 43 | ldr r3, _patch0A 44 | ldr r4, _patch0V 45 | bl _patch 46 | mov r5, #$patchSlot2 47 | ldr r3, _patch1A 48 | ldr r4, _patch1V 49 | bl _patch 50 | 51 | ; Return to firmware 52 | bx r7 53 | 54 | _patch: 55 | @patch 56 | 57 | _patch0A: .word $usbReadStandardResponse 58 | _patch0V: .word 0x47104a00 ; THUMB binary - Jump to the address 4 bytes after this. ( ldr r2, [ pc ] bx r2 ) 59 | _patch1A: .word $usbReadStandardReponseNext 60 | _patch1V: 61 | _address: .word $residentCodeAddress 62 | 63 | _payload: 64 | ; Load enabledFlagAddress - if it is set, redirect request to read disc and increment sector 65 | ; Else handle default 66 | ; Memory can still be altered via the factory mode, so the enabled flag can be rewritten when needed 67 | ; to toggle between standard commands, and ATRAC recovery. 68 | push { r3, r4, r5, r6, r7 } 69 | 70 | ldr r0, enabledFlagAddress 71 | ldr r0, [ r0 ] 72 | cmp r0, #0 73 | 74 | ; Load default values 75 | ; Z flag unchanged 76 | ldr r0, g_DiscStateStruct 77 | ldr r1, [ r0, #$discStructOffset ] 78 | ldr r0, g_usb_buff 79 | 80 | beq handleUnchanged 81 | 82 | ; *enabledFlagAddress is != 0, supress standard USB reading and load ATRAC instead. 83 | 84 | ; Load current ATRAC sector 85 | ldr r0, sectorToReadAddress 86 | ldr r0, [ r0 ] 87 | 88 | ; Backup the value, to increment it later 89 | push { r0 } 90 | 91 | ; Prepare other parameters for the DRAM read method. 92 | mov r1, #0 93 | ldr r2, sectorBuffer 94 | mov r3, #2352 95 | 96 | ; Backup LR - we still need to return to the firmware later 97 | mov r7, lr 98 | 99 | ; Call the DRAM Read function 100 | @blxar r4, $read_atrac_dram 101 | 102 | ; Restore the sector address, increment and write it back to RAM 103 | pop { r0 } 104 | add r0, r0, #1 105 | ldr r1, sectorToReadAddress 106 | str r0, [ r1 ] 107 | 108 | ; Load arguments for usb_do_response, and restore the return address 109 | ldr r0, sectorBuffer 110 | mov r1, #2352 111 | mov lr, r7 112 | 113 | handleUnchanged: 114 | pop { r3, r4, r5, r6, r7 } 115 | @bxar r2, $usb_do_response 116 | 117 | @bridge word g_DiscStateStruct g_usb_buff 118 | @bridge word sectorBuffer sectorToReadAddress enabledFlagAddress 119 | 120 | .word 0x968CE9A2 121 | .word 0x0000B38D ; 🥚 122 | 123 | _marker: .word 0xBADC0DE0 ; End of _payload. 124 | `, 125 | }, 126 | cleanupCode: { 127 | '*': `@autogen_cleanup $patchSlot1, $patchSlot2` 128 | }, 129 | residentCodeAddress: { 130 | 'S*': 0x02005f00, 131 | 'R*': 0x02003ce0, 132 | }, 133 | enabledFlagAddress: { 134 | 'S*': 0x02005500, 135 | 'R*': 0x02003cd4, 136 | }, 137 | sectorToReadAddress: { 138 | 'S*': 0x02005600, 139 | 'R*': 0x02003cd0, 140 | }, 141 | sectorBuffer: { 142 | 'S*': 0x02006f00, 143 | 'R*': 0x02003240, 144 | }, 145 | discStructOffset: { 146 | 'S*': 0x24, 147 | 'R1.000,R1.100': 0x1c, 148 | 'R1.200,R1.300,R1.400': 0x18, 149 | }, 150 | usbReadStandardResponse: { 151 | //handleReadStandardResponse 152 | 'S1.600': 0x0000de4c, 153 | 'S1.500': 0x0000dcec, 154 | 'S1.400': 0x0000dca4, 155 | 'S1.300': 0x0000d25c, 156 | 'S1.200': 0x0000cfe8, 157 | 'S1.100': 0x0000cf5c, 158 | 'S1.000': 0x0000df34, 159 | 160 | 'R1.000': 0x00055b8c, 161 | 'R1.100': 0x00056410, 162 | 'R1.200': 0x0005715c, 163 | 'R1.300': 0x0005745c, 164 | 'R1.400': 0x000574fc, 165 | }, 166 | usbReadStandardReponseNext: { 167 | 'S1.600': 0x0000de50, 168 | 'S1.500': 0x0000dcf0, 169 | 'S1.400': 0x0000dca8, 170 | 'S1.300': 0x0000d260, 171 | 'S1.200': 0x0000cfec, 172 | 'S1.100': 0x0000cf60, 173 | 'S1.000': 0x0000df38, 174 | 175 | 'R1.000': 0x00055b90, 176 | 'R1.100': 0x00056414, 177 | 'R1.200': 0x00057160, 178 | 'R1.300': 0x00057460, 179 | 'R1.400': 0x00057500, 180 | }, 181 | 182 | patchSlot1: { 183 | '*': this.patchSlot1, 184 | }, 185 | patchSlot2: { 186 | '*': this.patchSlot2, 187 | }, 188 | }; 189 | } 190 | 191 | currentTrack = -1; 192 | patchSlot1: number = -1; 193 | patchSlot2: number = -1; 194 | 195 | async init() { 196 | await super.init(); 197 | this.patchSlot1 = this.stateManager.getIncrementalPatchNumber(); 198 | this.patchSlot2 = this.stateManager.getIncrementalPatchNumber(); 199 | const usbExecution = await this.stateManager.require(USBCodeExecution); 200 | await usbExecution.execute(this.assembleProperty('mainCode')); 201 | } 202 | 203 | async unload() { 204 | // As this exploit patches the USB code, we can't depend on the stateManager to free the patches 205 | const usbExecution = await this.stateManager.require(USBCodeExecution); 206 | await usbExecution.execute(this.assembleProperty('cleanupCode')); 207 | this.stateManager.freePatch(this.patchSlot1); 208 | this.stateManager.freePatch(this.patchSlot2); 209 | } 210 | 211 | async readNextSector(): Promise { 212 | if (this.currentTrack === -1) throw new Error('You need to startDownload() first'); 213 | 214 | // As $mainCode rewrites the part of firmware responsible for handling `readReply` 215 | // on the device's side, this will always return the next sector 216 | return new Uint8Array((await this.iface.netMd.readReply(2352)).data!.buffer); 217 | } 218 | 219 | async setSectorToRead(sector: number): Promise { 220 | if (this.currentTrack === -1) throw new Error('You need to startDownload() first'); 221 | await cleanWrite(this.factoryIface, this.getProperty('sectorToReadAddress'), formatUIntQuery('% { 225 | if (!this.stateManager.device.versionCode.startsWith('R')) await display(this.stateManager.factoryIface, 'ACCESS', true); 226 | this.currentTrack = track; 227 | await cleanWrite(this.factoryIface, this.getProperty('enabledFlagAddress'), formatUIntQuery('% { 231 | if (!this.stateManager.device.versionCode.startsWith('R')) await this.factoryIface.setDisplayMode(DisplayMode.DEFAULT); 232 | await cleanWrite(this.factoryIface, this.getProperty('enabledFlagAddress'), formatUIntQuery('% 23 | str r1, [ r0, #$discStructOffset ] 24 | str r1, [ r0, #<$discStructOffset - 0x4> ] 25 | 26 | ; Prepare arguments for DRAM read call 'read_dram(%sector, %subsectorStart, &atrac, 392);' 27 | ldr r0, #sector 28 | ldr r1, #subsectorStart 29 | adr r2, atrac 30 | ldr r3, length 31 | 32 | ; Call the DRAM Read function 33 | @blxar r4, $read_atrac_dram 34 | 35 | ; Restore registers and return to firmware. 36 | pop { r3, r4, lr } 37 | bx lr 38 | 39 | @bridge word g_DiscStateStruct 40 | 41 | sector: .word 0 42 | subsectorStart: .word 0 43 | length: .word 0 44 | atrac: 45 | `, 46 | }, 47 | discStructOffset: { 48 | 'S*': 0x24, 49 | 'R1.000,R1.100': 0x1c, 50 | 'R1.200,R1.300,R1.400': 0x18, 51 | } 52 | }; 53 | } 54 | 55 | currentTrack = -1; 56 | sector = 0; 57 | 58 | codeLength = this.assembleProperty("code", { cLength: 0 }).length; 59 | codeTemplate = this.assembleProperty("code", { cLength: this.codeLength }).slice(0, -12); 60 | 61 | async readNextSector(): Promise { 62 | if (this.currentTrack === -1) throw new Error('You need to startDownload() first'); 63 | 64 | const exec = await this.stateManager.require(USBCodeExecution); 65 | const readPart = async (subsectorStart: number, length: number) => { 66 | const full = await exec.execute(concatUint8Arrays( 67 | this.codeTemplate, 68 | formatUIntQuery("% 0){ 77 | const subSector = await readPart(2352 - rem, Math.min(rem, 512 - 8 - this.codeLength)); 78 | subArrays.push(subSector); 79 | rem -= subSector.length; 80 | } 81 | ++this.sector; 82 | return concatUint8Arrays(...subArrays); 83 | } 84 | 85 | async setSectorToRead(sector: number): Promise { 86 | this.sector = sector; 87 | } 88 | 89 | async startDownload(track: number): Promise { 90 | if (!this.stateManager.device.versionCode.startsWith('R')) await display(this.stateManager.factoryIface, 'ACCESS', true); 91 | this.currentTrack = track; 92 | } 93 | 94 | async finishDownload(): Promise { 95 | if (!this.stateManager.device.versionCode.startsWith('R')) await this.factoryIface.setDisplayMode(DisplayMode.DEFAULT); 96 | this.currentTrack = -1; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/exploits/atrac-recovery/index.ts: -------------------------------------------------------------------------------- 1 | export * from './atrac-recovery-interface'; 2 | export * from './cached-sector-control-transfers'; 3 | export * from './cached-sector-bulk-transfers'; 4 | export * from './cached-sector-noram-transfers'; 5 | export * from './multitrack-atrac-recovery'; 6 | -------------------------------------------------------------------------------- /src/exploits/atrac-recovery/multitrack-atrac-recovery.ts: -------------------------------------------------------------------------------- 1 | import { createAeaHeader, readUTOCSector } from "netmd-js"; 2 | import { Exploit } from "../../exploit"; 3 | import { AtracRecovery, AtracRecoveryConfig } from "./atrac-recovery-interface"; 4 | import { getTitleByTrackNumber, parseTOC, ToC } from "netmd-tocmanip"; 5 | import { concatUint8Arrays } from "netmd-js/dist/utils"; 6 | 7 | export class MultitrackATRACRecovery extends Exploit { 8 | static _name = "MultitrackATRACRecovery"; 9 | public backend?: AtracRecovery; 10 | private cachedToc?: ToC; 11 | 12 | async init(backend: AtracRecovery){ 13 | this.backend = backend; 14 | } 15 | 16 | protected getPropertyStore() { 17 | return { 18 | checkAddress: { 19 | 'S1.600': 0x0006cf94, 20 | 'S1.500': 0x0006c5d4, 21 | 'S1.400': 0x0006bf30, 22 | 'S1.300': 0x000688ec, 23 | 'S1.200': 0x000673b8, 24 | 'S1.100': 0x00066ae0, 25 | 'S1.000': 0x0006d0f4, 26 | 27 | 'R1.400': 0x00030800, 28 | 'R1.300': 0x00030780, 29 | 'R1.200': 0x000306e8, 30 | 'R1.100': 0x0003009c, 31 | 'R1.000': 0x0002fc4c, 32 | }, 33 | checkContents: { 34 | '*': new Uint8Array([0x70, 0x47, 0x70, 0x47]), 35 | }, 36 | } 37 | } 38 | 39 | // prepareUnit() patches the NetMD portable to make it accept MD Data discs. 40 | async prepareUnit(){ 41 | await this.stateManager.patch( 42 | this.getProperty('checkAddress'), 43 | this.getProperty('checkContents'), 44 | ); 45 | } 46 | 47 | public setCachedToC(toc: ToC) { 48 | this.cachedToc = toc; 49 | } 50 | 51 | private async getToC() { 52 | if(!this.cachedToc) { 53 | const tocS0 = await readUTOCSector(this.stateManager.factoryIface, 0); 54 | const tocS1 = await readUTOCSector(this.stateManager.factoryIface, 1); 55 | this.cachedToc = parseTOC(tocS0, tocS1); 56 | } 57 | return this.cachedToc; 58 | } 59 | 60 | 61 | // This function will return a multitrack AEA file. 62 | // Track is 0-indexed 63 | async downloadMultitrackTrack( 64 | track: number, 65 | progressCallback?: (data: { read: number; total: number; action: 'READ' | 'SEEK' | 'CHUNK'; sector?: string }) => void, 66 | otherConfig?: AtracRecoveryConfig 67 | ) { 68 | // We need the TOC to derive if this is a 4-track or 8-track recording 69 | // The player has no clue 70 | const toc = await this.getToC(); 71 | const rootFragment = toc.trackFragmentList[toc.trackMap[track + 1]]; 72 | 73 | if(rootFragment.mode !== 201 && rootFragment.mode !== 200) { 74 | throw new Error("Not a known multitrack format!"); 75 | } 76 | const channelCount = rootFragment.mode === 201 ? 8 : 4; 77 | const trackName = getTitleByTrackNumber(toc, track + 1); 78 | 79 | // Download the actual raw track contents 80 | const rawData = await this.backend!.downloadTrack( 81 | track, 82 | progressCallback, 83 | { 84 | ...(otherConfig ?? {}), 85 | removeLPBytes: 'never', 86 | writeHeader: false, 87 | respectSoundgroupsBoundaries: true, 88 | forceSeekCalculationMode: 'SPS', 89 | } 90 | ); 91 | 92 | const header = createAeaHeader(trackName, channelCount); 93 | 94 | // Create the final file and return it 95 | return otherConfig?.writeHeader === false ? rawData : concatUint8Arrays(header, rawData); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/exploits/disable-disc-detection-switch.ts: -------------------------------------------------------------------------------- 1 | import { Exploit, ExploitPolicies, VersionPropertyStore } from "../exploit"; 2 | 3 | export class DisableDiscDetection extends Exploit{ 4 | static readonly _name = "DisableDiscDetection"; 5 | public _policies: ExploitPolicies = { 6 | stateManagerUnpatchLegal: true, 7 | }; 8 | 9 | protected getPropertyStore(): VersionPropertyStore { 10 | return { 11 | address: { 12 | 'S1.000': 0x0003dc7c, 13 | 'S1.600': 0x0003db8c, 14 | 'S1.500': 0x0003d69c, 15 | 'S1.400': 0x0003d194, 16 | 'S1.300': 0x0003ac1c, 17 | 'S1.200': 0x0003a1f4, 18 | 'S1.100': 0x00039c9c, 19 | }, 20 | value: { 21 | 'S*': new Uint8Array([0x70, 0x47, 0x70, 0x47]), 22 | }, 23 | }; 24 | } 25 | 26 | async init() { 27 | await this.stateManager.patch(this.getProperty("address"), this.getProperty("value")); 28 | } 29 | } -------------------------------------------------------------------------------- /src/exploits/enter-service-mode.ts: -------------------------------------------------------------------------------- 1 | import { Exploit } from "../exploit"; 2 | import { USBCodeExecution } from "./usb-code-execution"; 3 | 4 | export class EnterServiceMode extends Exploit { 5 | static _name = "EnterServiceMode"; 6 | protected getPropertyStore() { 7 | return { 8 | startServiceMode: { 9 | 'S1.000': 0x0004696d, 10 | 'S1.100': 0x00042455, 11 | 'S1.200': 0x00042a75, 12 | 'S1.300': 0x00043659, 13 | 'S1.400': 0x00045e2d, 14 | 'S1.500': 0x0004636d, 15 | 'S1.600': 0x00046871, 16 | }, 17 | mainCode: { 18 | '*': ` 19 | @fw_compat_bridge 20 | ; To enter the service mode, it is required to also stop the MD's "main OS" 21 | mov r0, #1 22 | @blxar r1, $tron_ter_task 23 | @bxar r1, $startServiceMode 24 | ` 25 | } 26 | } 27 | } 28 | 29 | async init() { 30 | await this.stateManager.envelop(USBCodeExecution, async (uce) => { 31 | await uce.execute(this.assembleProperty('mainCode')); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/exploits/firmware-dumper.ts: -------------------------------------------------------------------------------- 1 | import { cleanRead, MemoryOpenType, MemoryType } from 'netmd-js'; 2 | import { Exploit, VersionPropertyStore } from '../exploit'; 3 | import { findBytes, formatUIntQuery } from '../utils'; 4 | 5 | export class FirmwareDumper extends Exploit { 6 | public static _name = 'FirmwareDumper'; 7 | 8 | protected getPropertyStore(): VersionPropertyStore { 9 | return { 10 | romSize: { 11 | 'R*': 0x70000, 12 | 'S*': 0xa0000, 13 | }, 14 | ramSize: { 15 | 'R*': 0x4800, 16 | 'S*': 0x9000, 17 | }, 18 | }; 19 | } 20 | 21 | async readFirmware(callback: (progress: { type: 'RAM' | 'ROM' | 'DRAM' ; readBytes: number; totalBytes: number }) => void = () => {}) { 22 | if(this.stateManager.device.versionCode.startsWith("Hx")){ 23 | // The MZ-RH1 firmware dumping patch. All credits go to Sir68k 24 | /* 25 | sub r0, r0, #0x04000000 26 | add r3, r0, #0x10 27 | subs pc, lr, #8 28 | */ 29 | const rh1DRAMCode = new Uint8Array([0x01, 0x03, 0x40, 0xe2, 0x10, 0x30, 0x80, 0xe2, 0x08, 0xf0, 0x5e, 0xe2]); 30 | 31 | /* 32 | movs pc, #0x02000000 33 | */ 34 | const rh1PatchCode = new Uint8Array([0x02, 0xf4, 0xa0, 0xe3]); 35 | 36 | await this.stateManager.patch(0x10, rh1PatchCode, 0); 37 | await this.factoryIface.write(0x02000000, rh1DRAMCode, MemoryType.MAPPED); 38 | console.log(await this.factoryIface.read(0x02000000, 0x10, MemoryType.MAPPED)); 39 | console.log(await this.factoryIface.getDeviceCode()); 40 | 41 | let memorySlices: Uint8Array[] = [] 42 | for(let offset = 0x04000000; offset < 0x04100000; offset += 0x10){ 43 | memorySlices.push(await this.factoryIface.read(offset, 0x10, MemoryType.MAPPED)); 44 | callback?.({ type: 'ROM', readBytes: offset - 0x04000000, totalBytes: 0x00100000}); 45 | } 46 | const rom = concatUint8Arrays(memorySlices); 47 | memorySlices = []; 48 | for(let offset = 0x04800000; offset < 0x04808000; offset += 0x10){ 49 | memorySlices.push(await this.factoryIface.read(offset, 0x10, MemoryType.MAPPED)); 50 | callback?.({ type: 'RAM', readBytes: offset - 0x04800000, totalBytes: 0x8000 }); 51 | } 52 | const ram = concatUint8Arrays(memorySlices); 53 | memorySlices = []; 54 | for(let offset = 0x02000000; offset < 0x03000000; offset += 0x10){ 55 | memorySlices.push(await this.factoryIface.read(offset, 0x10, MemoryType.MAPPED)); 56 | callback?.({ type: 'DRAM', readBytes: offset - 0x02000000, totalBytes: 0x01000000 }); 57 | } 58 | const dram = concatUint8Arrays(memorySlices); 59 | return { ram, rom, dram }; 60 | }else if(this.stateManager.device.versionCode.startsWith("H")){ 61 | // Non-RH1 HiMD device 62 | await this.factoryIface.changeMemoryState(0x03000040, 0x1a, MemoryType.MAPPED, MemoryOpenType.CLOSE); 63 | await this.factoryIface.changeMemoryState(0x03000040, 0x1a, MemoryType.MAPPED, MemoryOpenType.READ); 64 | await this.factoryIface.read(0x03000040, 0x1a, MemoryType.MAPPED); 65 | 66 | let slices: Uint8Array[] = []; 67 | for(let i = 0; i<0xe0000; i+=0x10){ 68 | slices.push(await this.factoryIface.read(i, 0x10, MemoryType.MAPPED)); 69 | callback?.({ type: "ROM", readBytes: slices.length * 0x10, totalBytes: 0xe0000}); 70 | } 71 | const rom = concatUint8Arrays(slices); 72 | slices = []; 73 | for(let i = 0x00800000; i<0x00810000; i+=0x10){ 74 | slices.push(await this.factoryIface.read(i, 0x10, MemoryType.MAPPED)); 75 | callback?.({ type: "RAM", readBytes: slices.length * 0x10, totalBytes: 0x10000}); 76 | } 77 | const ram = concatUint8Arrays(slices); 78 | return { ram, rom }; 79 | } 80 | // This exploit works by first reading the device's RAM using legal factory mode commands 81 | // then, in that RAM image, the exploit finds the USB command buffer, which is followed 82 | // by the allowed memory access ranges written by 'changeMemoryState'. The factory mode 83 | // allows any RAM address to be overwritten with any value, so nothing is stopping us 84 | // from rewriting these ranges to zeros, allowing us to start reading at mapped memory 85 | // address 0x00000000 - the flash ROM - the firmware. 86 | 87 | const [romSize, ramSize] = this.getProperties('romSize', 'ramSize'); 88 | let memorySlices: Uint8Array[] = []; 89 | callback({ type: 'RAM', readBytes: 0, totalBytes: ramSize }); 90 | for (let i = 0; i < ramSize; i += 0x10) { 91 | memorySlices.push(await cleanRead(this.factoryIface, 0x02000000 + i, 0x10, MemoryType.MAPPED)); 92 | callback({ type: 'RAM', readBytes: memorySlices.length * 0x10, totalBytes: ramSize }); 93 | } 94 | callback({ type: 'ROM', readBytes: 0, totalBytes: romSize }); 95 | let fullRam = concatUint8Arrays(memorySlices); 96 | 97 | // Find the factory USB buffer. The allowed ranges should be right behind it. 98 | const usbBufferBeginning = new Uint8Array([0x00, 0x18, 0x21, 0x00, 0x00]); 99 | let offset = findBytes(fullRam, usbBufferBeginning); 100 | let foundRAMBoundsOffset = -1; 101 | while (offset !== -1) { 102 | let shouldStartHere = offset + 0x20; 103 | let possibleValue = Math.floor(shouldStartHere / 16) * 16 + 0x02000000; 104 | let calculated = formatUIntQuery('% { 63 | await codeExecution.execute( 64 | this.assembleProperty('raiseLowerCode', { 65 | lowerOrRaise: 0x02, 66 | }) 67 | ); 68 | await sleep(1000); 69 | await codeExecution.execute(this.assembleProperty('triggerCode')); 70 | await sleep(50000); 71 | await codeExecution.execute( 72 | this.assembleProperty('raiseLowerCode', { 73 | lowerOrRaise: 0x01, 74 | }) 75 | ); 76 | }); 77 | if (!this.stateManager.device.versionCode.startsWith('R')) await this.factoryIface.setDisplayMode(DisplayMode.DEFAULT); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/exploits/himd-unbounded-reading.ts: -------------------------------------------------------------------------------- 1 | import { Exploit, VersionPropertyStore } from "../exploit"; 2 | 3 | export class HiMDUnboundedReading extends Exploit{ 4 | static _name = "HiMDUnboundedReading"; 5 | public _policies = { 6 | permanent: true, 7 | }; 8 | 9 | protected getPropertyStore(): VersionPropertyStore { 10 | return { 11 | address: { 12 | "Hr1.000": 0x000995e8, 13 | 14 | "Hn1.000": 0x0009e558, 15 | "Hn1.100": 0x0009dfd4, 16 | "Hn1.10A": 0x0009e05c, 17 | "Hn1.200": 0x0009dcd0, 18 | }, 19 | data: { 20 | "Hr*,Hn*": new Uint8Array([0x00, 0x20, 0x70, 0x47]), 21 | }, 22 | }; 23 | } 24 | 25 | public override async init(){ 26 | if(!this.stateManager.device.isHimd) return; 27 | await this.stateManager.patch(this.getProperty("address"), this.getProperty("data")); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/exploits/himd-usb-class-override.ts: -------------------------------------------------------------------------------- 1 | import { formatUIntQuery } from '../utils'; 2 | import { MemoryType, scanQuery } from "netmd-js"; 3 | import { Exploit } from "../exploit"; 4 | import { HiMDUnboundedReading } from "./himd-unbounded-reading"; 5 | import { USBCodeExecution } from "./usb-code-execution"; 6 | import JSBI from 'jsbi'; 7 | 8 | export class HiMDUSBClassOverride extends Exploit{ 9 | static _name = "HiMDUSBClassOverride"; 10 | public _policies = { 11 | permanent: true, 12 | }; 13 | 14 | protected override getPropertyStore() { 15 | return { 16 | usbClassOverrideContent: { 17 | "Hr1.000,Hn1.000,Hn1.100,Hn1.10A,Hn1.200": new Uint8Array([0xFF, 0x00, 0x00, 0x00]), 18 | "Hx1.0A0,Hx1.090,Hx1.080,Hx1.070,Hx1.060,Hx1.040": new Uint8Array([0x02, 0xFF, 0x00, 0x00]), 19 | }, 20 | usbClassOverrideAddress: { 21 | "Hr1.000": 0x000d8adc, 22 | 23 | "Hn1.000": 0x000da1d8, 24 | "Hn1.100": 0x000d9ed8, 25 | "Hn1.10A": 0x000d9f60, 26 | "Hn1.200": 0x000d9cf8, 27 | }, 28 | usbClassOverrideAddressB: { 29 | "Hn1.000": 0x000da218, 30 | "Hn1.200": 0x000d9d38, 31 | }, 32 | himdDescriptorsPtrsBase: { 33 | "Hr1.000": 0x000d8b70, 34 | 35 | "Hn1.000": 0x000da26c, 36 | "Hn1.100": 0x000d9f6c, 37 | "Hn1.10A": 0x000d9ff4, 38 | "Hn1.200": 0x000d9d8c, 39 | }, 40 | himdModifiableDescriptorBase: { 41 | "Hr1.000": 0x000d8a30, 42 | 43 | "Hn1.000": 0x000da174, 44 | "Hn1.100": 0x000d9e74, 45 | "Hn1.10A": 0x000d9e6c, 46 | "Hn1.200": 0x000d9c04, 47 | }, 48 | newVidPid: { 49 | "H*": new Uint8Array([0x41, 0x53, 0x56, 0x52]), 50 | }, 51 | 52 | rh1PatchCode: { 53 | 'Hx*': ` 54 | ; The MZ-RH1 keeps the VID / PID codes in its SRAM, not DRAM / ROM 55 | ; For that reason it's impossible to patch these codes like it's done 56 | ; with other HiMD devices. 57 | 58 | ldr r0, rh1VidPidLocation 59 | ldr r1, newVidPid 60 | str r1, [r0] 61 | bx lr 62 | 63 | @bridge word rh1VidPidLocation 64 | @bridge byte newVidPid 65 | ` 66 | }, 67 | rh1VidPidLocation: { 68 | 'Hx1.0A0,Hx1.090,Hx1.080,Hx1.070,Hx1.060,Hx1.040': 0x0080125c, 69 | }, 70 | rh1DescriptorLocation: { 71 | 'Hx1.0A0,Hx1.090,Hx1.080,Hx1.070,Hx1.060,Hx1.040': 0x02003568, 72 | }, 73 | }; 74 | } 75 | 76 | public override async init() { 77 | if(!this.stateManager.device.isHimd){ 78 | return; 79 | } 80 | if(this.stateManager.device.versionCode.startsWith("Hx")){ 81 | // MZ-RH1 82 | const code = this.assembleProperty("rh1PatchCode"); 83 | const codeExec = await this.stateManager.require(USBCodeExecution); 84 | await codeExec.execute(code); 85 | 86 | // Hard part done. 87 | // Now patch the USB interface descriptor 88 | const [descriptorLocation, descriptorContent] = this.getProperties("rh1DescriptorLocation", "usbClassOverrideContent"); 89 | await this.factoryIface.write(descriptorLocation, descriptorContent, MemoryType.MAPPED); 90 | // Done 91 | return; 92 | } 93 | const descriptorsPtrsBase = this.getProperty("himdDescriptorsPtrsBase"); 94 | await this.stateManager.require(HiMDUnboundedReading); 95 | const targetDescriptor = this.getProperty("himdModifiableDescriptorBase"); 96 | await this.stateManager.patch( 97 | descriptorsPtrsBase + 4 * this.stateManager.device.hwid, 98 | formatUIntQuery("%> 8) & 1) !== 0){ 113 | usbClassOverrideAddress = this.getProperty("usbClassOverrideAddressB"); 114 | } 115 | } 116 | 117 | await this.stateManager.patch( 118 | usbClassOverrideAddress, 119 | usbClassOverrideContent, 120 | ); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/exploits/index.ts: -------------------------------------------------------------------------------- 1 | export * from './force-toc-edit'; 2 | export * from './usb-code-execution'; 3 | export * from './tetris'; 4 | export * from './atrac-recovery/index'; 5 | export * from './firmware-dumper'; 6 | export * from './kill-eeprom-writes'; 7 | export * from './pcm-faster-upload'; 8 | export * from './wait-for-disc-spinning'; 9 | export * from './sp-upload'; 10 | export * from './himd-unbounded-reading'; 11 | export * from './himd-usb-class-override'; 12 | export * from './mono-sp-upload'; 13 | export * from './enter-service-mode'; 14 | export * from './disable-disc-detection-switch'; 15 | -------------------------------------------------------------------------------- /src/exploits/kill-eeprom-writes.ts: -------------------------------------------------------------------------------- 1 | import { patch } from 'netmd-js'; 2 | import { Exploit } from '../exploit'; 3 | 4 | export class KillEepromWrite extends Exploit { 5 | public static _name = 'KillEepromWrite'; 6 | public _policies = { 7 | stateManagerUnpatchLegal: true, 8 | }; 9 | 10 | protected getPropertyStore() { 11 | return { 12 | spiWriteEEPROM: { 13 | 'R1.000': 0x0000df40, 14 | 'R1.100': 0x0000e070, 15 | 'R1.200': 0x0000e18c, 16 | 'R1.300': 0x0000e224, 17 | 'R1.400': 0x0000e224, 18 | 19 | 'S1.000': 0x0001e678, 20 | 'S1.100': 0x0001c7e8, 21 | 'S1.200': 0x0001ca8c, 22 | 'S1.300': 0x0001cf28, 23 | 'S1.400': 0x0001e05c, 24 | 'S1.500': 0x0001e2d4, 25 | 'S1.600': 0x0001e58c, 26 | }, 27 | bxLR: { 28 | '*': new Uint8Array([0x70, 0x47, 0x70, 0x47]), 29 | }, 30 | }; 31 | } 32 | 33 | async init() { 34 | await super.init(); 35 | await patch( 36 | this.factoryIface, 37 | this.getProperty('spiWriteEEPROM'), 38 | this.getProperty('bxLR'), 39 | this.stateManager.getIncrementalPatchNumber(), 40 | this.stateManager.getMaxPatchesAmount() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/exploits/mono-sp-upload.ts: -------------------------------------------------------------------------------- 1 | import { Exploit, ExploitPolicies, VersionPropertyStore } from "../exploit"; 2 | 3 | export class MonoSPUpload extends Exploit{ 4 | static readonly _name = "MonoSPUpload"; 5 | public _policies: ExploitPolicies = { 6 | stateManagerUnpatchLegal: true, 7 | }; 8 | 9 | protected getPropertyStore(): VersionPropertyStore { 10 | return { 11 | address: { 12 | 'S1.600': 0x00013d78, 13 | 'S1.500': 0x00013b8c, 14 | 'S1.400': 0x00013a84, 15 | 'S1.300': 0x00012c34, 16 | 'S1.200': 0x000129c0, 17 | 'S1.100': 0x00012910, 18 | 'S1.000': 0x00013e6c, 19 | 20 | 'R1.000': 0x000576e8, 21 | 'R1.100': 0x00057f8c, 22 | 'R1.200': 0x00058cf8, 23 | 'R1.300': 0x0005904c, 24 | 'R1.400': 0x000590ec, 25 | }, 26 | value: { 27 | 'S1.000,S1.600,S1.500': new Uint8Array([0x00, 0x46, 0x2a, 0xf0]), 28 | 'S1.400': new Uint8Array([0x00, 0x46, 0x29, 0xf0]), 29 | 'S1.300': new Uint8Array([0x00, 0x46, 0x28, 0xf0]), 30 | 'S1.200': new Uint8Array([0x00, 0x46, 0x27, 0xf0]), 31 | 'S1.100': new Uint8Array([0x28, 0x1c, 0x00, 0x46]), 32 | 33 | 34 | 'R1.000,R1.100,R1.200,R1.300,R1.400': new Uint8Array([0x03, 0x29, 0x0b, 0xe0]), 35 | }, 36 | }; 37 | } 38 | 39 | async init() { 40 | await this.stateManager.patch(this.getProperty("address"), this.getProperty("value")); 41 | } 42 | } -------------------------------------------------------------------------------- /src/exploits/pcm-faster-upload.ts: -------------------------------------------------------------------------------- 1 | import { patch } from 'netmd-js'; 2 | import { Exploit, VersionPropertyStore } from '../exploit'; 3 | import { formatUIntQuery } from '../utils'; 4 | 5 | export class PCMFasterUpload extends Exploit { 6 | static _name = 'PCMFasterUpload'; 7 | 8 | public _policies = { 9 | stateManagerUnpatchLegal: true, 10 | }; 11 | 12 | protected getPropertyStore(): VersionPropertyStore { 13 | return { 14 | patch1Address: { 15 | 'S1.600': 0x000783cc, 16 | 'S1.500': 0x000779d4, 17 | 'S1.400': 0x00077300, 18 | 'S1.300': 0x00073c40, 19 | 'S1.200': 0x0007258c, 20 | 'S1.100': 0x00071c04, 21 | 'S1.000': 0x0007852c, 22 | }, 23 | patch1Value: { 24 | 'S*': formatUIntQuery('41310160'), 25 | }, 26 | patch2Address: { 27 | 'S1.600': 0x0001ac9c, 28 | 'S1.500': 0x0001aa94, 29 | 'S1.400': 0x0001a820, 30 | 'S1.300': 0x000198f8, 31 | 'S1.200': 0x00019628, 32 | 'S1.100': 0x00019464, 33 | 'S1.000': 0x0001ad94, 34 | }, 35 | patch2Value: { 36 | 'S*': formatUIntQuery('000f0fe0'), 37 | }, 38 | }; 39 | } 40 | 41 | async init() { 42 | await super.init(); 43 | await patch( 44 | this.factoryIface, 45 | this.getProperty('patch1Address'), 46 | this.getProperty('patch1Value'), 47 | this.stateManager.getIncrementalPatchNumber(), 48 | this.stateManager.getMaxPatchesAmount() 49 | ); 50 | await patch( 51 | this.factoryIface, 52 | this.getProperty('patch2Address'), 53 | this.getProperty('patch2Value'), 54 | this.stateManager.getIncrementalPatchNumber(), 55 | this.stateManager.getMaxPatchesAmount() 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/exploits/sp-upload.ts: -------------------------------------------------------------------------------- 1 | import { cleanRead, MDTrack, MemoryType, patch, Wireformat } from "netmd-js"; 2 | import { Exploit, VersionPropertyStore } from "../exploit"; 3 | 4 | // Exploit based on @Sir68k's work (https://github.com/Sir68k) 5 | export class SPUpload extends Exploit { 6 | static _name = "SPUpload"; 7 | 8 | public _policies = { 9 | stateManagerUnpatchLegal: true, 10 | }; 11 | 12 | protected getPropertyStore(): VersionPropertyStore { 13 | return { 14 | fiqHandlerTypeAddress: { 15 | 'S1.600': 0x02003eff + 208, 16 | 'S1.500': 0x02003fc7, 17 | 'S1.400': 0x02003fab, 18 | 'S1.300': 0x02003e97, 19 | 'S1.200': 'fiqHandlerBPatch0', 20 | 'S1.100': 'fiqHandlerBPatch0', 21 | 'S1.000': 0x0200401b, 22 | }, 23 | fiqHandlerAPatch0: { 24 | 'S1.600': 0x0007f408, 25 | 'S1.500': 0x0007e988, 26 | 'S1.400': 0x0007e2c8, 27 | 'S1.300': 0x0007aa00, 28 | 29 | 30 | 'S1.000': 0x0007f59c, 31 | }, 32 | fiqHandlerBPatch0: { 33 | 'S1.600': 0x0007efec, 34 | 'S1.500': 0x0007e56c, 35 | 'S1.400': 0x0007deac, 36 | 'S1.300': 0x0007a5e4, 37 | 'S1.200': 0x00078dcc, 38 | 'S1.100': 0x000783c0, 39 | 'S1.000': 0x0007f180, 40 | }, 41 | 42 | 43 | prepPatchCommon: { 44 | 'S1.600': 0x00077c04, 45 | 'S1.500': 0x0007720c, 46 | 'S1.400': 0x00076b38, 47 | 'S1.300': 0x00073488, 48 | 'S1.200': 0x00071e5c, 49 | 'S1.100': 0x000714d4, 50 | 'S1.000': 0x00077d6c, 51 | }, 52 | fiqHandlerCommon1: { 53 | 'S1.600': 0x0007f4e8, 54 | 'S1.500': 0x0007ea68, 55 | 'S1.400': 0x0007e3a8, 56 | 'S1.300': 0x0007aae0, 57 | 'S1.200': 0x00078eac, 58 | 'S1.100': 0x000784a0, 59 | 'S1.000': 0x0007f67c, 60 | }, 61 | fiqHandlerCommon2: { 62 | 'S1.600': 0x0007f4ec, 63 | 'S1.500': 0x0007ea6c, 64 | 'S1.400': 0x0007e3ac, 65 | 'S1.300': 0x0007aae4, 66 | 'S1.200': 0x00078eb0, 67 | 'S1.100': 0x000784a0, 68 | 'S1.000': 0x0007f680, 69 | }, 70 | trackType: { 71 | 'S1.600': 0x000852b0, 72 | 'S1.500': 0x00084820, 73 | 'S1.400': 0x00084160, 74 | 'S1.300': 0x00080798, 75 | 'S1.200': 0x0007ea9c, 76 | 'S1.100': 0x0007e084, 77 | 'S1.000': 0x00085444, 78 | }, 79 | 80 | 81 | fiqHandlerPatch0Contents: { 82 | 'S*': new Uint8Array([0x00, 0x00, 0xa0, 0xe1]), 83 | }, 84 | fiqHandlerPatch1Contents: { 85 | 'S*': new Uint8Array([0x14, 0x80, 0x80, 0x03]), 86 | }, 87 | fiqHandlerPatch2Contents: { 88 | 'S*': new Uint8Array([0x14, 0x90, 0x80, 0x03]), 89 | }, 90 | prepPatchContents: { 91 | 'S*': new Uint8Array([0x0D, 0x31, 0x01, 0x60]), 92 | }, 93 | trackTypeContents: { 94 | 'S*': new Uint8Array([6, 2, 0, 4]), // SP, LP2, LP4, MONO 95 | }, 96 | } 97 | } 98 | 99 | async init(channels: 1 | 2) { 100 | await super.init(); 101 | const variantProp = this.getProperty("fiqHandlerTypeAddress"); 102 | 103 | let addressAPropName; 104 | if (typeof variantProp === "string") { 105 | addressAPropName = variantProp; 106 | } else { 107 | const variant = (await cleanRead( 108 | this.factoryIface, 109 | variantProp, 110 | 1, 111 | MemoryType.MAPPED, 112 | ))[0]; 113 | if (variant !== 1) { 114 | addressAPropName = "fiqHandlerAPatch0"; 115 | } else { 116 | addressAPropName = "fiqHandlerBPatch0"; 117 | } 118 | } 119 | let addressA = this.getProperty(addressAPropName); 120 | await patch( 121 | this.factoryIface, 122 | addressA, 123 | this.getProperty("fiqHandlerPatch0Contents"), 124 | this.stateManager.getIncrementalPatchNumber(), 125 | this.stateManager.getMaxPatchesAmount(), 126 | ); 127 | await patch( 128 | this.factoryIface, 129 | this.getProperty("fiqHandlerCommon1"), 130 | this.getProperty("fiqHandlerPatch1Contents"), 131 | this.stateManager.getIncrementalPatchNumber(), 132 | this.stateManager.getMaxPatchesAmount(), 133 | ); 134 | await patch( 135 | this.factoryIface, 136 | this.getProperty("fiqHandlerCommon2"), 137 | this.getProperty("fiqHandlerPatch2Contents"), 138 | this.stateManager.getIncrementalPatchNumber(), 139 | this.stateManager.getMaxPatchesAmount(), 140 | ); 141 | await patch( 142 | this.factoryIface, 143 | this.getProperty("prepPatchCommon"), 144 | this.getProperty("prepPatchContents"), 145 | this.stateManager.getIncrementalPatchNumber(), 146 | this.stateManager.getMaxPatchesAmount(), 147 | ); 148 | 149 | const trackTypeContents = this.getProperty("trackTypeContents"); 150 | trackTypeContents[1] = channels === 1 ? 4 : 6; 151 | await patch( 152 | this.factoryIface, 153 | this.getProperty("trackType"), 154 | trackTypeContents, 155 | this.stateManager.getIncrementalPatchNumber(), 156 | this.stateManager.getMaxPatchesAmount(), 157 | ); 158 | } 159 | 160 | prepareTrack(track: MDTrack) { 161 | // Transcribe the track and call session's method. 162 | 163 | // In order to re-align the data with the DRAM buffer, 164 | // (floor(2332 / 105) + 1) * 105 - 2332 padding bytes need to be added 165 | 166 | const padding = 100; 167 | const paddingArray = new Uint8Array(padding).fill(0); 168 | 169 | const raw = new Uint8Array(track.data); 170 | const originalLength = raw.length; 171 | const slices: Uint8Array[] = []; 172 | for (let i = 0; i < originalLength; i += 2332) { 173 | const sector = raw.subarray(i, i + 2332) 174 | slices.push(sector); 175 | // Rewrite Block Size Mode and the number of Block Floating Units 176 | // This mitigates an issue with atracdenc where it doesn't write 177 | // the bytes at the end of each frame. 178 | for (let i = 0; i < sector.length; i += 212) { 179 | sector[i + 212 - 1] = sector[i + 0]; 180 | sector[i + 212 - 2] = sector[i + 1]; 181 | } 182 | slices.push(paddingArray); 183 | } 184 | track = new MDTrack( 185 | track.title, 186 | Wireformat.l105kbps, 187 | concatUint8Arrays(slices).buffer, 188 | track.chunkSize, 189 | track.fullWidthTitle, 190 | track.encryptPacketsIterator 191 | ); 192 | track.getFrameCount = () => originalLength / 212; 193 | 194 | return track; 195 | } 196 | 197 | } 198 | 199 | function concatUint8Arrays(args: Uint8Array[]) { 200 | let totalLength = 0; 201 | for (let a of args) { 202 | totalLength += a.length; 203 | } 204 | 205 | let res = new Uint8Array(totalLength); 206 | 207 | let offset = 0; 208 | for (let a of args) { 209 | res.set(a, offset); 210 | offset += a.length; 211 | } 212 | return res; 213 | } 214 | -------------------------------------------------------------------------------- /src/exploits/tetris.ts: -------------------------------------------------------------------------------- 1 | import { MemoryType, writeOfAnyLength } from 'netmd-js'; 2 | import { Exploit } from '../exploit'; 3 | import { formatUIntQuery } from '../utils'; 4 | import { USBCodeExecution } from './usb-code-execution'; 5 | 6 | export class Tetris extends Exploit { 7 | public static _name = 'Tetris'; 8 | 9 | protected getPropertyStore() { 10 | return { 11 | tetrisCode: { 12 | // MiniTris by Sir68k (https://github.com/Sir68k/MiniTris) 13 | 'S1.600': formatUIntQuery( 14 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C046C1C507005E5A0002BDEC030001C207001847C046014B18687047C04668100002014B18787047C0467F08000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A0002BDEC03001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A0002BDEC03001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A0002BDEC030001C20700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 15 | ), 16 | 'S1.500': formatUIntQuery( 17 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C04641BB07005E5A0002C9E7030081B707001847C046014B18687047C04650100002014B18787047C0467F08000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A0002C9E703001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A0002C9E703001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A0002C9E7030081B70700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 18 | ), 19 | 'S1.400': formatUIntQuery( 20 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C04681B407005E5A000299E20300C1B007001847C046014B18687047C04634100002014B18787047C0466B08000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A000299E203001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A000299E203001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A000299E20300C1B00700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 21 | ), 22 | 'S1.300': formatUIntQuery( 23 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C046197C07005E5A0002F5BC0300597807001847C046014B18687047C046240F0002014B18787047C0467807000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A0002F5BC03001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A0002F5BC03001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A0002F5BC030059780700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 24 | ), 25 | 'S1.200': formatUIntQuery( 26 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C046496407005E5A000295B20300896007001847C046014B18687047C046100F0002014B18787047C0466B07000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A000295B203001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A000295B203001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A000295B2030089600700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 27 | ), 28 | 'S1.100': formatUIntQuery( 29 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C0463D5A07005E5A000221AD03007D5607001847C046014B18687047C0460C0F0002014B18787047C0466707000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC0000675A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC0000675A000210B50C210348044B00F008F810BC01BC0047C046245A000221AD03001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C046675A000221AD03001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A00029C5A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A000221AD03007D560700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C00054657472697300D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D020000000000000000000000000000000' 30 | ), 31 | 'S1.000': formatUIntQuery( 32 | '10B50024013420000D4B00F021F8092CF8D108210B480C4B00F01AF800F078F80A200A4B00F014F800F08AF80128F5D100F05EF800F012F910BC01BC0047C04631C707005E5A0002B9ED030071C307001847C046014B1B6818687047685A0002014B18787047C0468B08000230B51400002814DB05220B4B43439B1415005D43101A28180A256B43093D074A854084400231C9181300525CAA4322435A5430BC01BC0047CDCC00006C5A000210B504000020844214DB0130182C11DC06290FD8094B634309309B145843084A12185218907805225343121B9B1818410123184010BC02BC0847C046CDCC00006C5A000210B50C210348044B00F008F810BC01BC0047C046245A0002B9ED03001847C04670B500240A252B00634306480A21C0180134054B00F00AF8052CF4D170BC01BC0047C0466C5A0002B9ED03001847C046F8B5FFF78DFFFE250540202D02D13C2300F020F800240D4EA75DFFF77BFF0300AF420ED10A490A680736821AA05D0526082800D00A36B24203D90B60F8BC02BC08470134072CE6D10020F7E7305A0002A05A00021847C046F0B500250126072787B004930CAB1B780290019103920593019BAB4203DC07B0F0BC01BC00470024029A23065B0ED25C230033409B005B19FB1A1A41324208D0049BE01805D4039BF91A059A491BFFF741FF019B0134A342E6DC0135DCE7F0B50025012685B00190009102920393009B9D4201DB00271FE00024019A23065B0ED25C2300072133409B005B19CB1A1A4117003740324205D1009B01349C42ECDB0135E4E70723029A991A039B491BE018FFF730FF0028EFD0380005B0F0BC02BC08470000F0B58DB0FFF7F8FE07231840984200D10020A84B1A5C07331C5C0025012308A90A724D72022221000AA80093023BFFF785FFFFF73FFF012300955B42022221000AA8FFF77BFF022303930895013DFFF7D3FE089B0600C31A192B64D96F1C3F0608903F16182F3DD1012321000093039A2B000AA8FFF762FF07231E409E4200D100268C4DAB5D08AA137200235372EB1D9C5D0123022221005B420AA8FFF77DFF002842D028000821834E0E3000F00AF9824CFFF7FFFE642000F003F928000821173000F0FFF8FFF7F5FEC82000F0F9F83C2300F0F5F80DB0F0BC01BC004700230893B4E73B002100039A0AA8FFF755FF0028B9D13D0001230493FFF7F3FE0690022819D1039B5E1E36063616182D08D02B00320021000AA8FFF73FFF002800D10396012323E00023E6E7012301250493DB1803936D42E0E7069B042B19D0012B2AD100220BA8631E02800793A2426FDB182D0BD00BAE2B0021003000039AFFF71CFF002802D132880AAB1A80069B049312E0039B5E1C0123360604933616182D0AD02B00320021000AA8FFF706FF01230493002800D103961823059300230793079B002B72D000273E00059B0133099331000598FFF74CFEB04001360743072EF6D100263A0001233241310009981A400136FFF71DFE072EF4D1059B5A1E0592DED2079B002B61D000260022310010000136FFF70DFE072EF7D101232100039A0AA800932B00FFF799FEFFF753FE002321000093039A2B000AA8FFF78FFE069B082B00D15BE70420244B00F049F80EE7079B9B1A1906490E46180496465C012107270B409B00FB1A00219C460B065B0E0593082308AFFF18059BFB5C0127059307230F40BF00BF18DF1B059B3B4101273B4067467F1ABB401E4336060131360EA142E3DB049B01321E705FE7079E370039000598FFF7DCFDB84001370643072FF6D17F3E72427241079296E7049B002BB5D0A2E73E5A0002B9ED030071C30700184720473047C046C000200001020304101010106C7A746E725250010204080202040FCC4E6CC68E2E04020303030303C0002047414D452000C000204F5645522000C000546574726973000074100002D0000000000000000000D0080000000000000000D0100000000000000000D0180000000000000000D0200000000000000000000000000000' 33 | ), 34 | }, 35 | loadingAddress: { 36 | '*': 0x02005500, 37 | }, 38 | execAddress: { 39 | '*': 0x02005501, 40 | }, 41 | bootstrap: { 42 | '*': ` 43 | ldr r0, execAddress 44 | bx r0 45 | 46 | execAddress: .word $execAddress 47 | `, 48 | }, 49 | }; 50 | } 51 | 52 | public async playTetris() { 53 | await writeOfAnyLength( 54 | this.stateManager.factoryIface, 55 | this.getProperty('loadingAddress'), 56 | this.getProperty('tetrisCode'), 57 | MemoryType.MAPPED 58 | ); 59 | await (await this.stateManager.require(USBCodeExecution)).execute(this.assembleProperty('bootstrap')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/exploits/usb-code-execution.ts: -------------------------------------------------------------------------------- 1 | import JSBI from 'jsbi'; 2 | import { formatQuery, patch, scanQuery } from 'netmd-js'; 3 | import { Exploit } from '../exploit'; 4 | import { formatUIntQuery } from '../utils'; 5 | 6 | export class USBCodeExecution extends Exploit { 7 | public static _name = 'USBCodeExecution'; 8 | 9 | public _policies = { 10 | stateManagerUnpatchLegal: true, 11 | }; 12 | 13 | protected getPropertyStore() { 14 | return { 15 | onePatchAddress: { 16 | 'S1.600': 0x0000e69c, 17 | 'S1.500': 0x0000e538, 18 | 'S1.400': 0x0000e4c4, 19 | 'S1.300': 0x0000daa8, 20 | 'S1.200': 0x0000d834, 21 | 'S1.100': 0x0000d784, 22 | 'S1.000': 0x0000e784, 23 | 24 | 'R1.000': 0x00056228, 25 | 'R1.100': 0x00056aac, 26 | 'R1.200': 0x000577f8, 27 | 'R1.300': 0x00057b48, 28 | 'R1.400': 0x00057be8, 29 | 30 | 'Hr1.000': 0x000989f0, 31 | 32 | 'Hn1.000': 0x0009d8e8, 33 | 'Hn1.100': 0x0009d364, 34 | 'Hn1.10A': 0x0009d3ec, 35 | 'Hn1.200': 0x0009d060, 36 | 37 | 'Hx1.0A0': 0x0007db00, 38 | 'Hx1.090': 0x0007daf4, 39 | 'Hx1.080': 0x0007dabc, 40 | 'Hx1.070': 0x0007da88, 41 | 'Hx1.060': 0x0007da88, 42 | 'Hx1.040': 0x0007da90, 43 | }, 44 | onePatchValue: { 45 | /* 46 | 13 48 ldr r0, [pc, #0x4c] 47 | 00 47 bx r0 48 | */ 49 | 'S1.600,S1.500,S1.400,S1.300,S1.200,S1.100,S1.000': new Uint8Array([0x13, 0x48, 0x00, 0x47]), 50 | /* 51 | 1A 48 ldr r0, [pc, #0x68] 52 | 00 47 bx r0 53 | */ 54 | 'R1.400,R1.300,R1.200,R1.100,R1.000': new Uint8Array([0x1a, 0x48, 0x00, 0x47]), 55 | /* 56 | 10 48 ldr r0, [pc, #0x40] 57 | 00 47 bx r0 58 | */ 59 | 'Hr1.000': new Uint8Array([0x10, 0x48, 0x00, 0x47]), 60 | /* 61 | 12 48 ldr r0, [pc, #0x48] 62 | 00 47 bx r0 63 | */ 64 | 'Hn1.200,Hn1.10A,Hn1.100,Hn1.000,Hx1.0A0,Hx1.090,Hx1.080,Hx1.070,Hx1.060,Hx1.040': new Uint8Array([0x12, 0x48, 0x00, 0x47]), 65 | }, 66 | twoPatchAddress1: {}, 67 | twoPatchValue1: {}, 68 | twoPatchAddress2: {}, 69 | twoPatchValue2: {}, 70 | 71 | command: { 72 | 'Hr*,Hn*,Hx*': 0xd2, 73 | 'S*': 0xd2, 74 | 'R*': 0xd3, 75 | }, 76 | }; 77 | } 78 | 79 | public async init() { 80 | await super.init(); 81 | // Check if the device has already been patched (f. ex. hard-patch) 82 | const reply = await this.execute(formatUIntQuery('0100a0e30000cfe51eff2fe1 00')); 83 | const resp = scanQuery(reply, '0100a0e30000cfe51eff2fe1 %b'); 84 | if (JSBI.toNumber(resp[0] as JSBI) === 0) { 85 | if (this.getPropertyOrNull('onePatchAddress') !== null) { 86 | const [addr, val] = this.getProperties('onePatchAddress', 'onePatchValue'); 87 | await this.stateManager.patch(addr, val); 88 | } else { 89 | // Fallback to two-patch system 90 | const [addr1, val1, addr2, val2] = this.getProperties( 91 | 'twoPatchAddress1', 92 | 'twoPatchValue1', 93 | 'twoPatchAddress2', 94 | 'twoPatchValue2' 95 | ); 96 | await this.stateManager.patch(addr1, val1); 97 | await this.stateManager.patch(addr2, val2); 98 | } 99 | } 100 | } 101 | 102 | public async execute(code: Uint8Array, length?: number) { 103 | const query = formatQuery('00 18%b ff %*', this.getProperty('command'), code); 104 | // Use iface.netMd to talk to the interface directly 105 | // arbitrary code execution still returns 'Not Implemented', and would crash 106 | // netmd-js's NetMDInterface with NotImplementedError, this is a workaround. 107 | await this.iface.netMd.sendCommand(query); 108 | const { data } = await this.iface.netMd.readReply(length ?? query.byteLength + 1); 109 | const buffer = data!.buffer; 110 | const [newCode] = scanQuery(buffer, '08 18%? ff %*'); 111 | return newCode as Uint8Array; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/exploits/wait-for-disc-spinning.ts: -------------------------------------------------------------------------------- 1 | import { Exploit, VersionPropertyStore } from '../exploit'; 2 | import { sleep } from 'netmd-js/dist/utils'; 3 | import { MemoryOpenType, MemoryType } from 'netmd-js'; 4 | 5 | const MAX_WAIT = 20000; 6 | 7 | export class WaitForDiscToStopSpinning extends Exploit { 8 | static _name = 'WaitForDiscToStopSpinning'; 9 | 10 | protected getPropertyStore(): VersionPropertyStore { 11 | return {}; 12 | } 13 | 14 | async wait() { 15 | if (this.stateManager.device.versionCode.startsWith('R') || this.stateManager.device.isHimd) { 16 | await sleep(MAX_WAIT); 17 | return; 18 | } 19 | 20 | const time = new Date().getTime(); 21 | const address = this.getFirmwareProperty('g_disc_spin_state'); 22 | await this.factoryIface.changeMemoryState(address, 1, MemoryType.MAPPED, MemoryOpenType.READ); 23 | for (;;) { 24 | let newState = (await this.factoryIface.read(address, 1, MemoryType.MAPPED))[0]; 25 | if (((newState >> 4) & 1) !== 0 || new Date().getTime() - time > MAX_WAIT) break; 26 | await sleep(500); 27 | } 28 | await this.factoryIface.changeMemoryState(address, 1, MemoryType.MAPPED, MemoryOpenType.CLOSE); 29 | const duration = new Date().getTime() - time; 30 | this.log(`Had to wait ${duration} ms`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './exploit-state'; 2 | export * from './exploits/index'; 3 | export * from './compatibility'; 4 | export * from './assembler/assembler'; 5 | -------------------------------------------------------------------------------- /src/safety.ts: -------------------------------------------------------------------------------- 1 | import { PatchPeripheralBase } from "netmd-js"; 2 | import { patch, readPatch } from "netmd-js"; 3 | import { ExploitStateManager } from "./exploit-state"; 4 | import { formatUIntQuery, getFromVersionStoreOrNull } from "./utils"; 5 | 6 | 7 | // This patch fixes a critical bug in the Sony firmware 8 | // This bug causes the unit to block its ability to write 9 | // tracks via NetMD by corrupting a part of the "NetMD" block 10 | // in the EEPROM when the device resets with 11 | // 'DataAbort', 'SupervisorCall' or 'PrefetchAbort' 12 | // 13 | // Author: 森田 (Morita), Sony Corporation 14 | 15 | const HANDLE_FAULT_EEPROM_CORRUPTION = { 16 | address: { 17 | "S1.000,S1.600,S1.500,S1.400": 0x000000C4, 18 | }, 19 | data: { 20 | "S*": formatUIntQuery("dcff ffea"), 21 | } 22 | }; 23 | 24 | 25 | // These addresses are for hard-patched developer units only 26 | const DEV_ADDRESSES = [ 0xe6c0, 0xe69c ]; 27 | 28 | 29 | export async function loadSafetyPatches(exploitStateManager: ExploitStateManager){ 30 | if(exploitStateManager.device.versionCode.startsWith("Hx")){ 31 | // MZ-RH1s are mysterious machines 32 | for(let i = 4; i v === value[i])){ 51 | exploitStateManager.log(`Found HFEC at patch slot #${i}`); 52 | 53 | exploitStateManager.markPatchAsPermanent(i); 54 | hfecLoaded = true; 55 | } 56 | if(DEV_ADDRESSES.includes(patch.address)){ 57 | exploitStateManager.log(`Found a developer patch at patch slot #${i}`); 58 | exploitStateManager.markPatchAsPermanent(i); 59 | } 60 | 61 | } 62 | if(hfecLoaded) return; 63 | 64 | exploitStateManager.log(`HFEC couldn't be found on your unit! Soft-patching.`); 65 | await patch( 66 | exploitStateManager.factoryIface, 67 | address, 68 | value, 69 | exploitStateManager.getIncrementalPatchNumber(), 70 | exploitStateManager.getMaxPatchesAmount() 71 | ); 72 | exploitStateManager.log(`HFEC loaded.`); 73 | } 74 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { formatQuery, MemoryType, NetMDFactoryInterface, writeOfAnyLength } from 'netmd-js'; 2 | import { CORE_MACROS } from './assembler/core-macros'; 3 | import { CompatibilityError, VersionPropertyStore } from './exploit'; 4 | import { ExploitStateManager } from './exploit-state'; 5 | 6 | export function getFromVersionStoreOrNull(store: VersionPropertyStore, version: string, key: string) { 7 | let substore = store[key]; 8 | if (!substore) return null; 9 | /* 10 | Available formats are: 11 | - R1.600, 12 | - R1.600,R1.500 13 | - R* 14 | - * 15 | */ 16 | let globalMatch = null, 17 | wildcardMatch = null, 18 | exactMatch = null; 19 | for (let key of Object.keys(substore)) { 20 | let splitKeys = key.split(','); 21 | if (splitKeys.includes(version)) { 22 | exactMatch = key; 23 | } 24 | for (let splitKey of splitKeys) { 25 | splitKey = splitKey.trim(); 26 | if (splitKey === '*') globalMatch = key; 27 | else if (isVersionMatchingFormat(splitKey, version)) wildcardMatch = key; 28 | } 29 | } 30 | let matched = exactMatch ?? wildcardMatch ?? globalMatch; 31 | if (matched === null) { 32 | return null; 33 | } 34 | return substore[matched]; 35 | } 36 | 37 | export function isVersionMatchingFormat(format: string, version: string) { 38 | return new RegExp('^' + format.replace(/\*/g, '.*') + '$').test(version); 39 | } 40 | 41 | export function getFromVersionStore(store: VersionPropertyStore, version: string, key: string) { 42 | const value = getFromVersionStoreOrNull(store, version, key); 43 | if (value === null) 44 | throw new CompatibilityError(`Cannot get ${key} from version store. The version given (${version}) is incompatible`); 45 | return value; 46 | } 47 | 48 | export function formatUIntQuery(data: string, ...props: any[]): Uint8Array { 49 | return new Uint8Array(formatQuery(data, props)); 50 | } 51 | 52 | export function arrayShallowEquals(a: any[], b: any[]) { 53 | if (a.length !== b.length) return false; 54 | for (let i = 0; i < a.length; i++) { 55 | if (a[i] !== b[i]) return false; 56 | } 57 | return true; 58 | } 59 | 60 | export function findBytes(haystack: Uint8Array, needle: Uint8Array, offset = 0) { 61 | while (offset < haystack.length) { 62 | if (haystack[offset] !== needle[0]) ++offset; 63 | else { 64 | let broken = false; 65 | for (let character of Array.from(needle)) { 66 | if (character == haystack[offset]) ++offset; 67 | else { 68 | broken = true; 69 | break; 70 | } 71 | if (offset > haystack.length) return -1; // End of data 72 | } 73 | if (!broken) return offset - needle.length; 74 | } 75 | } 76 | return -1; 77 | } 78 | 79 | export async function writePatchCodeSection(esm: ExploitStateManager, data: Uint8Array, slot: number) { 80 | const offset = getFromVersionStoreOrNull(CORE_MACROS.fw_compat_bridge.properties, esm.device.versionCode, 'patch_code_base'); 81 | await writeOfAnyLength(esm.factoryIface, offset + 40 * slot, data, MemoryType.MAPPED); 82 | } 83 | 84 | export function deepCopy(obj: T): T{ 85 | return JSON.parse(JSON.stringify(obj)) as T; 86 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | "allowJs": true, /* Allow javascript files to be compiled. */ 9 | "checkJs": false, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 66 | } 67 | } 68 | --------------------------------------------------------------------------------