-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
zoogie
authored and
zoogie
committed
Apr 22, 2020
1 parent
cc53ec5
commit 4e4cdcc
Showing
178 changed files
with
44,065 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,51 @@ | ||
# unSAFE_MODE | ||
# unSAFE_MODE | ||
|
||
## Intro | ||
|
||
This is a new exploit for SAFE_MODE system updater. This is the recovery mode app that appears when L+R+Up+A are held while coldbooting the 3DS.<br> | ||
It's normally used to internet update the 3DS from a corrupted state and hopefully repair any damaged titles.<br> | ||
Because it runs *under* SAFE_MODE firm, it's a very interesting (and safe) hax target ;)<br> | ||
|
||
## Directions | ||
|
||
Download the zip archive under the Release tab above and follow the instructions inside. You will need some sort of userland exploit to install a hacked wifi save. Details for two of these exploits are included.<br> | ||
Works on firmwares:<br> | ||
old3ds 6.0 - latest<br> | ||
new3ds 8.1 - latest (all versions) | ||
|
||
## Exploit | ||
|
||
When SAFE_MODE sysupdater launches, it will check all 3 wifi slots for a working access point to perform a sysupdate. If it can't find one, it will allow the user to access wifi connection settings to make changes. | ||
When Proxy Settings -> Detailed Setup is selected, the displayed proxy URL string is not adequately checked for length, and a stack smash is possible if the attacker had previously altered the location of the string's NULL terminator in the wifi slot data.<br><br> | ||
In order to do the necessary slot modification, userland execution is needed with either cfg:i or cfg:s available. It's possible to attain this service with a small modification to the "*hax" source, or running an mset entrypoint (i.e. bannerbomb3). Note that SAFE_MODE sysupdater is actually a fork of firmware 1.0's mset (System Settings). As a result, mset also had this same bug, but it was fixed in firmware 3.0. The fix obviously was never backported to SAFE_MODE sysupdater, as SAFE_MODE titles are seldom updated for whatever reason. | ||
|
||
## FAQ | ||
|
||
Q: Um, ... is this unsafe?<br> | ||
A: It's no more unsafe than any other full exploit chain in terms of user safety. The "unsafe" part is ribbing Nintendo for calling SAFE_MODE as such given, from their perspective, it's full of exploitable bugs (since they never backport fixes from NATIVE_FIRM). From the user's standpoint, on the other hand, uSM is safe enough for safehax!<br> | ||
|
||
Q: One of my shoulder buttons is hosed, what can I do?<br> | ||
A: There's always ntrboot or seedminer ¯\\_(ツ)_/¯<br> | ||
|
||
Q: You mentioned safehax a couple of times, does uSM have that?<br> | ||
A: It's bundled in, yes. usm.bin contains the safehax code (and several other stages). It will automatically install boot9strap for permanent cfw. | ||
|
||
Q: Is this fixable with a firmware update?<br> | ||
A: I think so. Nintendo has a weird track record ignoring my previous exploits, but they could fix this, and possibly do so without even touching SAFE_MODE titles (they prefer leaving SAFE_MODE untouched, it seems). While the fix I'm thinking of is pretty straightforward, I'd rather not give any hints in the unlikely event they act on this exploit. | ||
|
||
## Thanks | ||
This project is licensed as MIT except the code used and modified from these other projects:<br><br> | ||
|
||
General Rop/Code | ||
- yellows8 https://github.com/yellows8/3ds_ropkit (otherapp loader) | ||
- dukesrg https://github.com/dukesrg/rop3ds (rop templates, macros) | ||
- smealum https://github.com/zoogie/ninjhax2.x (ninjhax fork with cfg:s changes) | ||
|
||
Safehax related | ||
- kartik https://github.com/hax0kartik/pre9otherapp (k11/a9 sploit framework) | ||
- aliaspider https://github.com/aliaspider/svchax/ (memchunkhax1) | ||
- patois https://github.com/patois/Brahma (firmlaunchhax) | ||
- tuxsh https://github.com/TuxSH/usr2arm9ldr (rsaverify) | ||
- normmatt https://gist.github.com/Normmatt/b72f7323686af5c9cd7 (rsaverify) | ||
- SciresM https://github.com/SciresM/boot9strap (mini b9s installer template & b9s itself) | ||
- AuroraWright https://github.com/AuroraWright/SafeA9LHInstaller (writefirm) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
REGION := USA | ||
ifeq ($(strip $(REGION)),) | ||
$(error "Set the REGION Makefile parameter.") | ||
endif | ||
|
||
HAXID := 0 | ||
HAXNAME := rop_payload | ||
|
||
|
||
|
||
REGCODE := 00 | ||
ifeq ($(REGION),USA) | ||
REGCODE := 45 | ||
endif | ||
ifeq ($(REGION),EUR) | ||
REGCODE := 56 | ||
endif | ||
ifeq ($(REGCODE),00) | ||
$(error "Unsupported region.") | ||
endif | ||
|
||
all: rop_payload.bin | ||
|
||
clean: | ||
rm -f $(HAXNAME).elf rop_payload.bin | ||
rm -f *.bin | ||
rm -f F00D43D5/*.bin | ||
rmdir F00D43D5 | ||
|
||
|
||
rop_payload.bin: $(HAXNAME).elf | ||
arm-none-eabi-objcopy -O binary $(HAXNAME).elf rop_payload.bin | ||
|
||
$(HAXNAME).elf: $(HAXNAME).s | ||
arm-none-eabi-gcc -x assembler-with-cpp -nostartfiles -nostdlib -Ttext=0x0ffffe48 $< -o $(HAXNAME).elf | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
from __future__ import print_function | ||
from Cryptodome.Hash import CMAC | ||
from Cryptodome.Cipher import AES | ||
import os,sys,random,hashlib,struct | ||
from binascii import hexlify | ||
|
||
tidlow=0xF00D43D5 | ||
#tidlow=0xFFFFFFFF | ||
|
||
keyx=0x6FBB01F872CAF9C01834EEC04065EE53 | ||
keyy=0x0 #get this from movable.sed - console unique | ||
F128=0xffffffffffffffffffffffffffffffff | ||
C =0x1FF9E9AAC5FE0408024591DC5D52768A | ||
cmac_keyx=0xB529221CDDB5DB5A1BF26EFF2041E875 | ||
|
||
DIR="F00D43D5/" | ||
BM=0x20 #block metadata size https://www.3dbrew.org/wiki/DSiWare_Exports (a bunch of info in this script is sourced here) | ||
BANNER=0x0 | ||
BANNER_SIZE=0x4000 | ||
HEADER=BANNER+BANNER_SIZE+BM | ||
HEADER_SIZE=0xF0 | ||
FOOTER=HEADER+HEADER_SIZE+BM | ||
FOOTER_SIZE=0x4E0 | ||
TMD=FOOTER+FOOTER_SIZE+BM | ||
content_sizelist=[0]*11 | ||
content_namelist=["tmd","srl.nds","2.bin","3.bin","4.bin","5.bin","6.bin","7.bin","8.bin","public.sav","banner.sav"] | ||
|
||
tad_sections=[b""]*14 | ||
|
||
if sys.version_info[0] >= 3: | ||
# Python 3 | ||
def bytechr(c): | ||
return bytes([c]) | ||
else: | ||
# Python 2 | ||
bytechr = chr | ||
|
||
def get_keyy(): | ||
global keyy | ||
seedname="movable.sed" | ||
realseed=0 | ||
with open(seedname,"rb") as f: | ||
msedlen=len(f.read()) | ||
if(msedlen != 0x140 and msedlen != 0x120): | ||
print("Error: movable.sed is the wrong size - are you sure this is a movable.sed?") | ||
sys.exit(1) | ||
f.seek(0x110) | ||
temp=f.read(0x10) | ||
keyy=int(hexlify(temp), 16) | ||
|
||
def int16bytes(n): | ||
if sys.version_info[0] >= 3: | ||
return n.to_bytes(16, 'big') # Python 3 | ||
else: | ||
s=b"" # Python 2 | ||
for i in range(16): | ||
s=chr(n & 0xFF)+s | ||
n=n>>8 | ||
return s | ||
|
||
def int2bytes(n): | ||
s=bytearray(4) | ||
for i in range(4): | ||
s[i]=n & 0xFF | ||
n=n>>8 | ||
return s | ||
|
||
def endian(n, size): | ||
new=0 | ||
for i in range(size): | ||
new <<= 8 | ||
new |= (n & 0xFF) | ||
n >>= 8 | ||
return new | ||
|
||
def add_128(a, b): | ||
return (a+b) & F128 | ||
|
||
def rol_128(n, shift): | ||
for i in range(shift): | ||
left_bit=(n & 1<<127)>>127 | ||
shift_result=n<<1 & F128 | ||
n=shift_result | left_bit | ||
return n | ||
|
||
def normalkey(x,y): #3ds aes engine - curtesy of rei's pastebin google doc, curtesy of plutoo from 32c3 | ||
n=rol_128(x,2) ^ y #F(KeyX, KeyY) = (((KeyX <<< 2) ^ KeyY) + 1FF9E9AAC5FE0408024591DC5D52768A) <<< 87 | ||
n=add_128(n,C) #https://pastebin.com/ucqXGq6E | ||
n=rol_128(n,87) #https://smealum.github.io/3ds/32c3/#/113 | ||
return n | ||
|
||
def decrypt(message, key, iv): | ||
cipher = AES.new(key, AES.MODE_CBC, iv ) | ||
return cipher.decrypt(message) | ||
|
||
def encrypt(message, key, iv): | ||
cipher = AES.new(key, AES.MODE_CBC, iv ) | ||
return cipher.encrypt(message) | ||
|
||
def check_keyy(keyy_offset): | ||
global keyy | ||
tempy=endian(keyy,16) | ||
tempy=tempy+(keyy_offset<<64) | ||
tempy=endian(tempy,16) | ||
iv=tad[HEADER+HEADER_SIZE+0x10:HEADER+HEADER_SIZE+0x20] | ||
key=normalkey(keyx, tempy) | ||
result=decrypt(tad[HEADER:HEADER+HEADER_SIZE],int16bytes(key),iv) | ||
if(b"\x33\x46\x44\x54" not in result[:4]): | ||
print("wrong -- keyy offset: %d" % (keyy_offset)) | ||
return 1 | ||
keyy=tempy | ||
print("correct! -- keyy offset: %d" % (keyy_offset)) | ||
return 0 | ||
#print("%08X %08X %s" % (data_offset, size, filename)) | ||
|
||
def get_content_block(buff): | ||
global cmac_keyx | ||
hash=hashlib.sha256(buff).digest() | ||
key = int16bytes(normalkey(cmac_keyx, keyy)) | ||
cipher = CMAC.new(key, ciphermod=AES) | ||
result = cipher.update(hash) | ||
return result.digest() + b''.join(bytechr(random.randint(0,255)) for _ in range(16)) | ||
|
||
def fix_hashes_and_sizes(): | ||
sizes=[0]*11 | ||
hashes=[""]*13 | ||
footer_namelist=["banner.bin","header.bin"]+content_namelist | ||
for i in range(11): | ||
if(os.path.exists(DIR+content_namelist[i])): | ||
sizes[i] = os.path.getsize(DIR+content_namelist[i]) | ||
else: | ||
sizes[i] = 0 | ||
for i in range(13): | ||
if(os.path.exists(DIR+footer_namelist[i])): | ||
with open(DIR+footer_namelist[i],"rb") as f: | ||
pass | ||
#hashes[i] = hashlib.sha256(f.read()).digest() | ||
else: | ||
hashes[i] = b"\x00"*0x20 | ||
|
||
with open(DIR+"header.bin","rb+") as f: | ||
offset=0x38 | ||
f.seek(offset) | ||
f.write(struct.pack("<I",tidlow)) | ||
offset=0x48 | ||
for i in range(11): | ||
f.seek(offset) | ||
f.write(int2bytes(sizes[i])) | ||
offset+=4 | ||
f.seek(0) | ||
#hashes[1] = hashlib.sha256(f.read()).digest() #we need to recalulate header hash in case there's a size change in any section. sneaky bug. | ||
print("header.bin fixed") | ||
|
||
with open(DIR+"footer.bin","rb+") as f: | ||
offset=0 | ||
for i in range(13): | ||
f.seek(offset) | ||
f.write(hashes[i]) | ||
offset+=0x20 | ||
print("footer.bin fixed") | ||
|
||
def rebuild_tad(): | ||
global keyy | ||
full_namelist=["banner.bin","header.bin","footer.bin"]+content_namelist | ||
section="" | ||
content_block="" | ||
key=normalkey(keyx,keyy) | ||
for i in range(len(full_namelist)): | ||
if(os.path.exists(DIR+full_namelist[i])): | ||
print("encrypting "+DIR+full_namelist[i]) | ||
with open(DIR+full_namelist[i],"rb") as f: | ||
section=f.read() | ||
content_block=get_content_block(section) | ||
tad_sections[i]=encrypt(section, int16bytes(key), content_block[0x10:])+content_block | ||
with open("%08X.bin" % tidlow,"wb") as f: | ||
out=b''.join(tad_sections) | ||
#out=out[:TMD] | ||
#f.write(tad_sections[0]+tad_sections[1]+tad_sections[2]+tad_sections[3]) | ||
f.write(out) | ||
print("Rebuilt to %08X.bin" % tidlow) | ||
print("Done.") | ||
STANDARD=0x0000 | ||
MODBUS=0xFFFF | ||
def fix_crc16(path, offset, size, crc_offset, type): | ||
''' | ||
CRC-16-Modbus Algorithm | ||
''' | ||
with open(path,"rb+") as f: | ||
f.seek(offset) | ||
data=f.read(size) | ||
|
||
poly=0xA001 | ||
crc = type | ||
for b in data: | ||
cur_byte = 0xFF & ord(b) | ||
for _ in range(0, 8): | ||
if (crc & 0x0001) ^ (cur_byte & 0x0001): | ||
crc = (crc >> 1) ^ poly | ||
else: | ||
crc >>= 1 | ||
cur_byte >>= 1 | ||
|
||
crc16=crc & 0xFFFF | ||
|
||
print("Patching offset %08X..." % crc_offset) | ||
f.seek(crc_offset) | ||
f.write(struct.pack("<H",crc16)) | ||
|
||
def inject_bin(src, dest, offset, ispad): | ||
with open(src, "rb") as f: | ||
buff=f.read() | ||
srclen=len(buff) | ||
padlen=0x1000-srclen | ||
if ispad: | ||
buff+=b"\x99"*padlen | ||
with open(dest,"rb+") as f: | ||
f.seek(offset) | ||
f.write(buff*8) | ||
|
||
print("|TADmuffin by zoogie|") | ||
print("|_______v0.0________|") | ||
print("Note: This is a simplified TADpole used only for Bannerbomb3") | ||
abspath = os.path.abspath(__file__) | ||
dname = os.path.dirname(abspath) | ||
os.chdir(dname) | ||
|
||
print("Using workdir: "+DIR) | ||
|
||
try: | ||
os.mkdir("F00D43D5") | ||
print("/F00D43D5 created") | ||
except: | ||
print("/F00D43D5 already exists") | ||
banner_bin=b"\x03\x01"+"\x00"*(0x4000-2) | ||
header_bin=b"3DFT"+struct.pack(">I",4)+(b"\x42"*0x20)+(b"\x99"*0x10)+struct.pack("<Q",0x00048005F00D43D5)+(b"\x00"*0xB0) | ||
footer_bin=b"\x00"*0x4E0 | ||
|
||
with open(DIR+"banner.bin","wb") as f: | ||
f.write(banner_bin) | ||
with open(DIR+"header.bin","wb") as f: | ||
f.write(header_bin) | ||
with open(DIR+"footer.bin","wb") as f: | ||
f.write(footer_bin) | ||
|
||
print("Injecting payload...") | ||
inject_bin("rop_payload.bin",DIR+"banner.bin",0x240,False) | ||
print("Fixing crc16s...") | ||
fix_crc16(DIR+"banner.bin", 0x20, 0x820, 0x2, MODBUS) | ||
fix_crc16(DIR+"banner.bin", 0x20, 0x920, 0x4, MODBUS) | ||
fix_crc16(DIR+"banner.bin", 0x20, 0xA20, 0x6, MODBUS) | ||
fix_crc16(DIR+"banner.bin", 0x1240, 0x1180, 0x8, MODBUS) | ||
#fix_crc16("../slot1.bin", 0x4, 0x410, 0x2, STANDARD) | ||
|
||
print("Rebuilding export...") | ||
get_keyy() | ||
fix_hashes_and_sizes() | ||
#sign_footer() #don't need this for bannerhax - exploit is triggered before footer ecdsa is verified | ||
rebuild_tad() | ||
|
||
''' | ||
payload="\x00"*0x20000 | ||
with open("payload/rop_payload.bin","rb") as f: | ||
rop=f.read() | ||
with open("../slot1.bin","rb") as f: | ||
slot1=f.read() | ||
with open("bb3.bin","wb") as f: | ||
f.write(payload) | ||
f.seek(0x0) | ||
f.write(rop) | ||
f.seek(0x8000) | ||
f.write(slot1) | ||
''' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
include $(DEVKITARM)/base_rules | ||
|
||
HAXNAME := rop_payload | ||
|
||
all: $(HAXNAME).bin | ||
|
||
clean: | ||
rm -f $(HAXNAME).elf $(HAXNAME).bin | ||
|
||
$(HAXNAME).bin: $(HAXNAME).elf | ||
arm-none-eabi-objcopy -O binary $(HAXNAME).elf $(HAXNAME).bin | ||
|
||
$(HAXNAME).elf: $(HAXNAME).s | ||
arm-none-eabi-gcc -x assembler-with-cpp -nostartfiles -nostdlib -Ttext=0x00686000 $< -o $(HAXNAME).elf | ||
|
Oops, something went wrong.