Skip to content

Commit

Permalink
added PC decryption. 7x, 9x exefs and n3ds doesn't work.
Browse files Browse the repository at this point in the history
  • Loading branch information
Morten Delenk committed May 21, 2017
1 parent c72f8a5 commit 51e4b80
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ aeskeydb.bin
*.cia
seeddb.bin
venv/
aeskeydb.yaml
14 changes: 13 additions & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Requirements:
- fuse 2.9.7 or earlier(!)
- fusepy (pip install fusepy)
- A 3DS running 3ds_cryptserver
- aeskeydb.bin (with eshop 0x3D keyY) in CWD
- aeskeydb.bin (with eshop 0x3D normal key) in home directory
- seeddb.bin in CWD

Depending on your setup, you might be only able to use these scripts as root user.
Expand Down Expand Up @@ -44,3 +44,15 @@ Mounting the romfs of the CIA of Mario Kart 7:
./romfsfuse.py mount/romfs.bin mount/

To unmount this, you have to run "umount" three times

-----------------------------------------------------
aeskeydb.bin for local decryption

Thanks to boot9strap, you can dump boot9+boot11+otp by holding start+select+x during boot. You can dump the aeskeys in those roms by running the script "genaeskeys.py". This will append all new keys to aeskeydb.bin. Then you can store it in your home directory.

Requirements:
- 3DS running sighax
- boot9.bin in current directory (The entire one)
- otp.bin in current directory

just run ./genaeskeys.py and your aeskeydb.bin will contain most AES keys. You still have to require eshop 0x3D normal key yourself.
41 changes: 40 additions & 1 deletion fuse_3ds/aeskeydb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import struct
from Crypto.Cipher import AES
from Crypto.Util import Counter
from os import path
def binfilegen(f,s):
x=f.read(s)
while len(x) == s:
Expand All @@ -22,8 +25,44 @@ def getKey(no):
None if unknown
"""
key=[None, None, None]
for s,t,k in aeskeywalk("aeskeydb.bin"):
for s,t,k in aeskeywalk(path.join(path.expanduser("~"),"aeskeydb.bin")):
if s == no:
if key[t] == None:
key[t]=k
return key
def scramble(keyX,keyY):
def asint128(a):
if a < 0:
raise ValueError("Must be positive")
return a & ((2**128)-1)
rotate=lambda c,v: (asint128(c<<v)|asint128(c>>128-v))
keyX=int.from_bytes(keyX,"big")
keyY=int.from_bytes(keyY,"big")
kn = rotate(asint128((rotate(keyX,2)^keyY)+0x1FF9E9AAC5FE0408024591DC5D52768A),87)
return kn.to_bytes(16,"big")
class NoBugCTR:
def __init__(self, iv):
self.ctr=int.from_bytes(iv,"big")
def __call__(self, *kargs, **kwargs):
iv=self.ctr.to_bytes(16,"big")
self.ctr+=1
if self.ctr == 2**128:
self.ctr=0
return iv
def __iter__(self):
return self
def __next__(self):
return self()
def getCipher(keyslot, iv, CBC=False, keyY=None):
key=getKey(keyslot)[0]
if keyY != None:
keyX=getKey(keyslot)[1]
if keyX == None:
raise ValueError("Unknown KeyX for crypto")
key=scramble(keyX, keyY)
if key == None:
raise ValueError("Unknown Key!")
if CBC:
return AES.new(key, AES.MODE_CBC, iv)
cipher=AES.new(key, AES.MODE_CTR, counter=Counter.new(128,allow_wraparound=True,initial_value=int.from_bytes(iv,"big")))
return cipher
6 changes: 2 additions & 4 deletions fuse_3ds/cia.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import struct
from . import crypto
from . import ticket
from . import tmd
import hashlib
Expand All @@ -9,8 +8,7 @@ def align(x,y):
mask = ~(y-1)
return (x+(y-1))&mask
class CIA:
def __init__(self, f, ip):
self.ip = ip
def __init__(self, f):
self.f=f
self.headerSize,self.type,self.version,self.cachainSize,self.tikSize,self.tmdSize,self.metaSize,self.contentSize=struct.unpack("<IHHIIIIQ",self.f.read(0x20))
self.cachainOff=align(self.headerSize,64)
Expand All @@ -24,7 +22,7 @@ def __init__(self, f, ip):
self.f.seek(self.cachainOff)
self.cachain=self.f.read(self.cachainSize)
self.f.seek(self.tikOff)
self.ticket=ticket.Ticket(self.f, self.ip)
self.ticket=ticket.Ticket(self.f)
self.f.seek(self.tmdOff)
self.tmd=tmd.TMD(self.f)
self.ticket.decryptTitleKey(self.tmd.tid)
Expand Down
12 changes: 5 additions & 7 deletions fuse_3ds/ciafuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@

class CIA(LoggingMixIn, Operations):
'Read only filesystem for CIA files.'
def __init__(self,fname,mount,ip):
self.ip=ip
def __init__(self,fname,mount):
self.files={}
self.f = open(fname,"rb")
self.cia = cia.CIA(self.f,ip)
self.cia = cia.CIA(self.f)
self.fd = 0
now = time()
self.files["/"] = dict(st_mode=(S_IFDIR | 0o555), st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2)
Expand All @@ -27,7 +26,6 @@ def __init__(self,fname,mount,ip):
if no == 0:
ending=".cxi"
self.files["/"+str(no)+ending]=dict(st_mode=(S_IFREG | 0o555),st_ctime=now, st_mtime=now, st_atime=now, st_nlink=2, st_size=self.cia.tmd.contents[no]["size"], st_blocks=(self.cia.tmd.contents[no]["size"]+511)//512)
#self.files["/"+str(no)+"/"]=dict(st_mode=(S_IFDIR | 0o555),st_ctime=now,st_mtime=now, st_atime=now, st_nlink=2)


def chmod(self,path,mode):
Expand Down Expand Up @@ -146,8 +144,8 @@ def flush(self,path,fh):
def release(self,path,fh):
pass

if len(argv) != 4:
print('usage: {name} <CIA> <mountpoint> <3DS IP>'.format(name=argv[0]))
if len(argv) != 3:
print('usage: {name} <CIA> <mountpoint>'.format(name=argv[0]))
exit(1)
logging.basicConfig(level=logging.WARNING)
fuse = FUSE(CIA(argv[1], argv[2], argv[3]),argv[2],foreground=False)
fuse = FUSE(CIA(argv[1], argv[2]),argv[2],foreground=False)
18 changes: 9 additions & 9 deletions fuse_3ds/ncch.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@

import struct
from . import crypto
from . import aeskeydb
import hashlib
class NCCH:
def __init__(self, f,ip):
self.ip=ip
def __init__(self, f):
self.f=f
header=self.f.read(512)
self.header=header
Expand All @@ -24,6 +23,7 @@ def __init__(self, f,ip):
self.cryptoFixkey=self.flags[7]&1
self.nocrypto=self.flags[7]&0x4
self.keyY=header[:16]
print(hex(self.flags[7]))
self.doExHeader()
def addCtr(self,ctr,val):
c=int.from_bytes(ctr,byteorder="big")
Expand Down Expand Up @@ -71,7 +71,7 @@ def read(self,sectorno,sectors=1):
if self.flags[7]&0x04: #Except when it's decrypted
decdata += data
else:
decdata += crypto.cryptoBytestring(self.ip,data,0x6C,3,ctr,self.keyY)
decdata += aeskeydb.getCipher(0x2C,ctr, keyY=self.keyY).decrypt(data)
if not sectors:
return decdata
sectorno+=csectors
Expand Down Expand Up @@ -100,15 +100,15 @@ def read(self,sectorno,sectors=1):
print(ctr)
#Sectors are encrypted via multiple methods
data=f.read(sectors*512)
keyslot = 0x6C
keyslot = 0x2C
if self.flags[3] == 0x01:
keyslot = 0x65
keyslot = 0x25
elif self.flags[3] == 0x0A:
keyslot = 0x58
keyslot = 0x18
elif self.flags[3] == 0x0B:
keyslot = 0x5B
keyslot = 0x1B
print(keyslot)
decdata += crypto.cryptoBytestring(self.ip,data,keyslot,3,ctr,self.keyY)
decdata += aeskeydb.getCipher(keyslot,ctr,keyY=self.keyY).decrypt(data)
return decdata

def doExHeader(self):
Expand Down
13 changes: 6 additions & 7 deletions fuse_3ds/ncchfuse.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@

class NCCH(LoggingMixIn, Operations):
'Read only filesystem for NCCH files.'
def __init__(self,fname,mount,ip):
self.ip=ip
def __init__(self,fname,mount):
self.files={}
self.f = open(fname,"rb")
self.ncch = ncch.NCCH(self.f,ip)
self.ncch = ncch.NCCH(self.f)
self.type="cxi" if self.ncch.exefsSize else "cfa"
self.fd = 0
now = time()
Expand Down Expand Up @@ -128,8 +127,8 @@ def flush(self,path,fh):
def release(self,path,fh):
pass

if len(argv) != 4:
print('usage: {name} <NCCH> <mountpoint> <3DS IP>'.format(name=argv[0]))
if len(argv) != 3:
print('usage: {name} <NCCH> <mountpoint>'.format(name=argv[0]))
exit(1)
logging.basicConfig(level=logging.WARNING)
fuse = FUSE(NCCH(argv[1], argv[2], argv[3]),argv[2],foreground=False)
logging.basicConfig(level=logging.DEBUG)
fuse = FUSE(NCCH(argv[1], argv[2]),argv[2],foreground=True)
3 changes: 2 additions & 1 deletion fuse_3ds/seeddb.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import struct
from os import path
def binfilegen(f,s):
x=f.read(s)
while len(x) == s:
yield x
x=f.read(s)
def seedwalk():
f = open("seeddb.bin","rb")
f = open(path.join(path.expanduser("~"),"seeddb.bin"),"rb")
f.read(16)
for s in binfilegen(f,32):
tid=struct.unpack("<Q",s[:8])[0]
Expand Down
7 changes: 2 additions & 5 deletions fuse_3ds/ticket.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import struct
from . import crypto
from . import aeskeydb
class Ticket:
def __init__(self, f, ip):
self.ip = ip
def __init__(self, f):
sigtype=struct.unpack(">I",f.read(4))[0]-0x10000
skip=[0x23C,0x13C,0x7C,0x23C,0x13C,0x7C]
f.read(skip[sigtype])
Expand All @@ -21,9 +19,8 @@ def __init__(self, f, ip):
def decryptTitleKey(self,tid):
if self.encrypted:
#Decrypt title key
keyY=aeskeydb.getKey(0x3D)[2]
iv=tid.to_bytes(8, byteorder='big')+b'\x00'*8
self.titlekey=crypto.cryptoBytestring(self.ip,self.titlekey,0x7D,1,iv,keyY)
self.titlekey=aeskeydb.getCipher(0x3D, iv, CBC=True).decrypt(self.titlekey)
def decrypt(self):
header=struct.pack(">I",0x10004)
header+=bytes(0x13C)
Expand Down
128 changes: 128 additions & 0 deletions genaeskeys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
import struct
def scramble(keyX,keyY):
def asint128(a):
if a < 0:
raise ValueError("Must be positive")
return a & ((2**128)-1)
rotate=lambda c,v: (asint128(c<<v)|asint128(c>>128-v))
keyX=int.from_bytes(keyX,"big")
keyY=int.from_bytes(keyY,"big")
kn = rotate(asint128((rotate(keyX,2)^keyY)+0x1FF9E9AAC5FE0408024591DC5D52768A),87)
return kn.to_bytes(16,"big")
boot9=open("boot9.bin","rb")
f=boot9
if True:
def getKey(no,dic):
dic[no]=f.read(16)
f.seek(-16,1)
def getKeyloop(no,dic):
for i in range(4):
getKey(no+i,dic)
f.read(16)
def getKeyloop_increase(no,dic):
for i in range(4):
getKey(no+i,dic)
f.read(16)
f.seek(0xd860) #Beginning of keyarea
_3fgendata=f.read(36)
keyX={}
keyY={}
normals={}
f.seek(0xd9d0)
getKeyloop(0x2C,keyX)
getKeyloop(0x30,keyX)
getKeyloop(0x34,keyX)
getKeyloop(0x38,keyX)
getKeyloop_increase(0x3C,keyX)
getKeyloop_increase(0x4,keyY)
getKeyloop_increase(0x8,keyY)
getKeyloop(0xC,normals)
getKeyloop(0x10,normals)
getKeyloop_increase(0x14,normals)
getKeyloop(0x18,normals)
getKeyloop(0x1C,normals)
getKeyloop(0x20,normals)
getKeyloop(0x24,normals)
f.seek(-16,1)
getKeyloop_increase(0x28,normals)
getKeyloop(0x2C,normals)
getKeyloop(0x30,normals)
getKeyloop(0x34,normals)
getKeyloop(0x38,normals)
f.seek(-16,1)
getKeyloop_increase(0x3C,normals)
f.seek(0xD6E0)
otpkey=f.read(16)
otpiv=f.read(16)
f.seek(0xD860)
from Crypto.Cipher import AES
with open("otp.bin","rb") as f:
cipher=AES.new(otpkey, AES.MODE_CBC, otpiv)
otp=cipher.decrypt(f.read())
import hashlib
conunique=otp[:28]+_3fgendata
conunique_hash=hashlib.sha256(conunique).digest()
del normals[0x3F]
keyX[0x3F]=conunique_hash[:16]
keyY[0x3F]=conunique_hash[16:]
def genkeys(size=0x40):
boot9.read(36)
aesiv=boot9.read(16)
conunique_input=boot9.read(64)
boot9.seek(-64,1)
boot9.read(size)
cipher=AES.new(scramble(keyX[0x3F],keyY[0x3F]),AES.MODE_CBC,aesiv)
return cipher.encrypt(conunique_input)
def getKey(dic,no,conunique,off):
dic[no]=conunique[off:off+16]
def getKeyloop(dic,no,conunique,off):
for i in range(4):
getKey(dic,no+i,conunique,off)
def getKeyloop_increase(dic,no,conunique,off):
for i in range(4):
getKey(dic,no+i,conunique,off+16*i)
conunique=genkeys()
getKeyloop(keyX,4,conunique,0)
getKeyloop(keyX,8,conunique,16)
getKeyloop(keyX,0xC,conunique,32)
getKey(keyX,0x10,conunique,48)

conunique=genkeys(16)
getKeyloop_increase(keyX,0x14,conunique,0)

conunique=genkeys()

getKeyloop(keyX,0x18,conunique,0)
getKeyloop(keyX,0x1C,conunique,16)
getKeyloop(keyX,0x20,conunique,32)
getKey(keyX,0x24,conunique,48)

conunique=genkeys(16)
getKeyloop_increase(keyX,0x28,conunique,0)

#Generate normal keys
for kx in keyX.keys():
if kx in keyY:
normals[kx]=scramble(keyX[kx],keyY[kx])
n=normals
normals={}
for kn in n.keys():
if ((kn in keyX) and (kn in keyY)) or ((kn not in keyX) and (kn not in keyY)):
normals[kn]=n[kn] #only keep normal keys that are in keyX,keyY pairs, or are just normal keys.
with open("aeskeydb.bin","ab") as f:
for kx in keyX.keys():
f.write(struct.pack("<B",kx))
f.write(b'X')
f.write(bytes(14))
f.write(keyX[kx])
for ky in keyY.keys():
f.write(struct.pack("<B",ky))
f.write(b'Y')
f.write(bytes(14))
f.write(keyY[ky])
for kn in normals.keys():
f.write(struct.pack("<B",kn))
f.write(b'N')
f.write(bytes(14))
f.write(normals[kn])
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
from setuptools import setup
setup(name="3ds-fuse",
version="0.1",
version="0.1.1",
description="FUSE filesystems for different 3DS file types",
author="Morten Delenk",
author_email="[email protected]",
Expand Down

0 comments on commit 51e4b80

Please sign in to comment.