{"id":29612089,"url":"https://github.com/joshiemoore/p8pwn","last_synced_at":"2025-07-20T21:37:40.449Z","repository":{"id":305435825,"uuid":"1021879418","full_name":"joshiemoore/p8pwn","owner":"joshiemoore","description":"PICO-8 v0.2.6b Sandbox Escape + RCE Exploit","archived":false,"fork":false,"pushed_at":"2025-07-20T03:40:14.000Z","size":121,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-07-20T05:44:48.636Z","etag":null,"topics":["binary-exploitation","pico-8","reverse-engineering"],"latest_commit_sha":null,"homepage":"https://x.com/joshiem00re","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joshiemoore.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-18T05:12:14.000Z","updated_at":"2025-07-20T03:41:03.000Z","dependencies_parsed_at":"2025-07-20T05:54:54.700Z","dependency_job_id":null,"html_url":"https://github.com/joshiemoore/p8pwn","commit_stats":null,"previous_names":["joshiemoore/p8pwn"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/joshiemoore/p8pwn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshiemoore%2Fp8pwn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshiemoore%2Fp8pwn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshiemoore%2Fp8pwn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshiemoore%2Fp8pwn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joshiemoore","download_url":"https://codeload.github.com/joshiemoore/p8pwn/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joshiemoore%2Fp8pwn/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266204640,"owners_count":23892366,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["binary-exploitation","pico-8","reverse-engineering"],"created_at":"2025-07-20T21:37:36.013Z","updated_at":"2025-07-20T21:37:40.437Z","avatar_url":"https://github.com/joshiemoore.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PICO-8 v0.2.6b Sandbox Escape + RCE Exploit\n\nPICO-8 v0.2.6b contains a remote code execution vulnerability due to a buffer overflow in `normalise_pico8_path()`. Attackers\ncan exploit this vulnerability to escape the PICO-8 sandbox and execute arbitrary native code on the host system as the current user. User\ninteraction is required to exploit this vulnerability, as the user must load and run a malicious cartridge file to trigger\nthe exploit.\n\nIf you have questions, comments, or hatred to share, please DM me on X [@joshiem00re](https://x.com/joshiem00re).\nFollowers are more likely to have their questions answered. Also, feel free to reach out if you would like to pay me to work\non something, I'm currently available for new opportunities. \n\n**DISCLAIMER:** Crime is illegal. This exploit and writeup are being shared publicly for the purpose of fun and education.\nIt's about the journey, not the destination! I am not responsible for anything you choose to do with the materials and\ninformation contained in this repo. DFIU!\n\nhttps://github.com/user-attachments/assets/3b4b1f4a-03c1-4a65-be86-adc91cc3703d\n\n## Quickstart\n\nIf you just want to try out the PoC which pops a calculator, you can load this cart into PICO-8 and run it:\n\n![](p8pwn.p8.png)\n\nThis exploit has only been tested on Windows 10, but it will most likely work on Windows 11 too.\n\n**If you want to build your own exploit:**\n1. Clone this repository.\n2. Write your 32-bit x86 assembly shellcode and assemble it as a flat binary (i.e. using the `-f bin` NASM flag). You can also use the provided `popcalc.s`, which simply opens a calculator.\n   Your assembled shellcode must fit within a single page, minus a few hundred bytes. So you have a little less than ~4k bytes to work with.\n3. Build the exploit: `$ python3 build_exploit.py \u003coutput file\u003e \u003cshellcode file\u003e`. For example: `$ python3 build_exploit.py mysploit.p8 myshellcode.bin`\n4. Load the output .p8 file into PICO-8 and run it.\n\n\n\n## Vulnerability\nThe core vulnerability is a buffer overflow in `normalise_pico8_path()`. This function takes the user-supplied path string\npassed to `LS()` and converts it to a canonical absolute path that the PICO-8 can reason about internally. So it resolves\nsubdirectories, `../`, etc.\n\nWhen you pass in a path containing subdirectories, like `AAAA/BBBB/CCCC`, the path is traversed, and each subdirectory name\nin the path gets `memcpy()`'d into the buffer `local_51c`, one at a time:\n\n\u003cimg width=\"822\" height=\"38\" alt=\"buffers\" src=\"https://github.com/user-attachments/assets/ed232818-4607-4a0d-a067-57b7aeac4e44\" /\u003e\n\n\u003cimg width=\"447\" height=\"311\" alt=\"part1\" src=\"https://github.com/user-attachments/assets/b83c32cd-d32a-4d93-85bf-8416400e789f\" /\u003e\n\nEach subdirectory is then `strcpy()`'d into the buffer `local_41d` at the current index into the normalized path:\n\n\u003cimg width=\"736\" height=\"311\" alt=\"part2\" src=\"https://github.com/user-attachments/assets/65b77a21-aeac-4436-9254-ab47631f8282\" /\u003e\n\nAnd so we build the full normalized path in `local_41d`. The issue is that there are no length limits or bounds checks on any of\nthis, so if you pass in a path containing numerous subdirectories with long names, you can overflow `local_41d` and smash the stack.\nHowever, each directory name can only be up to 255 bytes (including the ending \"/\"), or `strcpy()` will end up never\nhitting a null terminator. It will instead start copying from from `local_51c` into `local_41d`, then it will reach `local_41d` and just\nkeep on copying what it already copied until it reaches an unmapped page and triggers an AV. So if you want to actually control\nthe overflow of `local_41d`, you have to use several subdirectories with each being up to 255 bytes long.\n\n## Exploit\nPICO-8 v0.2.6b installs pico8.exe, a 32-bit Windows binary with no ASLR or stack canaries. The `.data` section is not executable,\nso that's the main issue we need to contend with in order to fully exploit this buffer overflow.\n\n`build_exploit.py` takes in a binary file containing assembled shellcode and outputs a `.p8` file containing the final exploit.\nThis Python script is the main file you should reference if you're trying to understand how this exploit works.\n\nAt a high level, the exploit works like this:\n1. Overflow `local_41d` to overwrite the return address with ROP gadgets that pivot the stack to the second ROP stage embedded in our exploit script\n2. Call `VirtualProtect()` to mark the page of memory containing our embedded shellcode as executable\n3. Jump to our embedded shellcode and execute it\n\nThe biggest speedbump to developing an exploit for this vulnerability was the fact that we can't include null bytes anywhere in our Lua\nstrings, or in the Lua source itself. The binary gets loaded at address `0x00400000`, so we cannot directly reference addresses from the\npico8.exe binary in our ROP chain.\n\nFortunately, the copy of SD2.dll shipped with PICO-8 gets loaded at `0x6c940000`! So all the ROP gadgets I used in this exploit came from this DLL,\nand there were plenty to choose from. I found an `xor eax, 0x39ffffff ; ret` gadget, and so this enabled me to include null bytes in values in the exploit\nchain. I just had to mask them by XORing them with `0x39ffffff`. So everywhere you see `xor eax, 0x39ffffff` in the chain, that's just us unmasking\nvalues containing null bytes. This wouldn't work if we needed to have `ff` in the low 3 bytes of the unmasked values, but we didn't in this case.\n\n### ROP Stage 1: pivot stack to main ROP chain\nIf you open `p8pwn.p8` in the PICO-8 IDE, you'll notice a comment at the top with a long string of strange characters:\n\u003cimg width=\"771\" height=\"199\" alt=\"header\" src=\"https://github.com/user-attachments/assets/6f7196ef-fde8-4d3d-8a3d-314813f30b7f\" /\u003e\n\nThese characters are not for decoration. This is the main ROP chain followed immediately by our assembled shellcode. The whole point of\nthe first stage of this exploit is to pivot the stack to point to the location of this comment string in memory so that we can continue the ROP chain.\n\nThe string `pwn` consists of padding to fill out the `local_41d` buffer, followed by one gadget which is reponsible for actually pivoting\nthe stack:\n```\n-- pop esp ; ret\npwn = pwn..\"\\xf9\\x73\\x94\\x6c\"\npwn = pwn..\"\\x01\\x91\\x55\\x00\"\nls(pwn)\n```\nHere we are popping `0x00559101` into `ESP` with a gadget from SDL2.dll. `0x00559101` is the memory location where our comment string starts after the `-- `. Hardcoding the address\nlike this works because the binary is not using ASLR. If ASLR were used, then we would have to leak an address first, but it's not, so we don't.\n\nYou might notice that there is a null byte at the end of `pwn` even though I said we can't include null bytes. That null byte does get truncated on the Lua side,\nbut it gets added back in on the C side because C strings have to be null-terminated. I just included the null in `pwn` for the purpose of clarity.\n\nSo we have the malicious string `pwn`, and we pass it to the vulnerable function `LS()` which triggers the exploit and pivots the stack to ROP stage 2.\n\n## ROP Stage 2: call VirtualProtect() to make the page containing our shellcode executable\nThe next thing we need to do is make it so that we can actually run our shellcode. We need to call `VirtualProtect()` to make this possible.\n\nThe comments in `build_exploit.py` pretty clearly document what's going on in this stage of the ROP chain, so I won't go into great detail here. We're just setting\nup to call `VirtualProtect(\u003cpage address\u003e, 0x1000, 0x40, \u0026lpflOldProtect)` and then jumping to make the call.\n\nThe calling convention for `VirtualProtect()` expects arguments to be passed on the stack instead of in registers. It was EXTREMELY ANNOYING trying to mask values with\nour `0x39ffffff` mask and place them on the stack for the `VirtualProtect()` call, so I gave up on that and instead tried to find a spot in pico8.exe that makes\na `VirtualProtect()` call that I could hijack. I found such a spot in the function `mark_section_writable()`:\n\n\u003cimg width=\"633\" height=\"148\" alt=\"virtualprotect\" src=\"https://github.com/user-attachments/assets/d2d03b99-3590-418a-a8a3-41798f02bc61\" /\u003e\n\nIt's beautiful, we couldn't have asked for a better setup. All we need to do is load the address of our page into `EAX`, the length into `EDX`, and an\naddress for `lpflOldProtect` into `EBX`. So we unmask these values with `0x39ffffff` in `EAX` and then `xchg` them into the proper registers.\nWe don't need the output value `lpflOldProtect`, so I just set this to point to a location right before our\ncomment string in memory. Really, this could be a pointer to any writable memory address.\n\nLastly, we have some padding to account for `mark_section_writable()` cleaning up its stack, and then we're on to the final stage of the exploit.\n\n## Stage 3: Jump to our shellcode\nThis stage simply loads the address of our shellcode into `EAX` and jumps to it:\n```\n### \u0026shellcode -\u003e eax\n# pop eax ; ret\nrop += b'\\x73\\x0f\\xa1\\x6c'\nrop += b'BBBB'\n# xor eax, 0x39ffffff ; ret\nrop += b'\\xc7\\x45\\x96\\x6c'\n\n### jump to shellcode!\n# jmp eax\nrop += b'\\x7a\\x71\\x94\\x6c'\n\nsc_addr = (0x00559101 + len(rop)) ^ 0x39ffffff\nsc_addr_b = sc_addr.to_bytes(4, byteorder='little')\nrop = rop.replace(b'BBBB', sc_addr_b)\n```\nWe dynamically calculate the offset of the shellcode based on the length of the stage 2 ROP chain, as our shellcode is placed immediately after the ROP\nchain in the comment string. The address of our shellcode is loaded into `EAX` and unmasked. We then jump to the shellcode and we have pwned the PICO-8.\n\n## Conclusion\nThis exploit isn't perfect, but it's a reliable and functional PoC. We could improve it by implementing process\ncontinuation so that the PICO-8 process doesn't just crash after the shellcode runs.\n\nAlso, it would be a good idea to remove all these hardcoded addresses from the exploit chain. Hardcoded addresses work because there is no ASLR here, but\nif there was ASLR, we would need to leak an address to dynamically calculate the runtime base address of the binary in memory. We can actually do that pretty\neasily with the undocumented `TOSTRING()` function from PICO-8's Lua API:\n\n```\n\u003e PRINT(TOSTRING(LS))\nFUNCTION: 0X456B60\n```\n\nThis leaks the actual address of the native `_p8_ls()` C function, so we could subtract the known offset of this function in the binary from this address in order to determine the runtime base address. I just didn't find it necessary to actually do that for this PoC.\n\nThanks for taking the time to read this writeup.\nFollow me on X [@joshiem00re](https://x.com/joshiem00re) if you're interested in keeping up with my activities related to reverse engineering\nand binary exploitation!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshiemoore%2Fp8pwn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoshiemoore%2Fp8pwn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoshiemoore%2Fp8pwn/lists"}