Please read the whole post very carefully before even deciding to comment! This situation is already bad enough, there's no need to make it worse by not reading it fully. Thanks!
As you may have noticed, there's a big fuss around the new UnbanMii 2.0 release due to how the backend was implemented. In this post - as the author of the backend - I'll explain why was the backend implemented this way, and I'll talk about some shitty design decisions I have made.
As not everyone is competent reading code, I'll make an explaination with technical details, and one with "dumbed down" details so non-programmer people can also understand it.
more info that's important regardless of being technical or not:
- I had no malicious intentions *at all*... the reason for uploading such touchy files was purely for very shitty security checks, and the touchy data (movable.sed and SecureInfo) was purely used by the script to check stuff
- some people let me know that the NAND CID (or as mentioned in the non-technical writeup: "NAND chip's ID") is also a touchy data... since I don't know of an occasion where people have replaced the NAND chip on their 3DS boards, I thought that this is a good way to *somehow* identify unique users
- the reason I stored the data unencrypted in the database is because all 3 people who had access to the server don't know how to SQL (or even how the database manager UI works), I didn't even think about more about it since I knew that nobody else could access the database other than me
- note to tech sawwies: I was using MySQLi prepared statements, and I don't know a way exists to exploit that... but data was checked before even a connection was made to the database, so there was absolutely no way to exploit this
- my mind wasn't clear (and it still isn't) when I worked on the code/backend, and there were ~19errors/mistakes fixed in a ~1-2hr timespan before the initial release, so ye... I was only focusing on getting the work done, and I didn't even think about how the data I'm working with was touchy, nor about how illegal it was... I'm sorry for that
so ye... my wanting to add too many safety checks went super wrong, and I'm sorry about that. the data in the database isn't used by any human at all, it's only used by the backend API code to check some data validity and eliminate possible risks for banning the public seed served by this...
edit (17/07/28 10:01): I got access to the server and did a DROP TABLE which got rid of a whopping 17 entries, 3 of which were from the team... a bit of a waste of antidepressants for just those 14 entries...
oh, also fixed the title as the typo was really triggering my OCD
As you may have noticed, there's a big fuss around the new UnbanMii 2.0 release due to how the backend was implemented. In this post - as the author of the backend - I'll explain why was the backend implemented this way, and I'll talk about some shitty design decisions I have made.
As not everyone is competent reading code, I'll make an explaination with technical details, and one with "dumbed down" details so non-programmer people can also understand it.
I pastebin'd the modified API.php: https://pastebin.com/4pzGrbjW
(Note: the only modification I have done is I have removed the username and the password, the rest of the script is the same as the one that was used in production)
(another note: I just noticed that someone has touched the script while I was sleeping, and has changed the capitalization on the die() error messages... the rest of the script seems to be still untouched however)
The POST data (size=0x380) contained the following stuff:
- offs=0x000, size=0x110: LocalFriendCodeSeed
- offs=0x110, size=0x140: movable.sed
- offs=0x250, size=0x111: SecureInfo
- offs=0x370, size=0x010: NAND CID
so, let's start breaking down the code...
everything 'till line 15 is boring, it's just checking the 'e' parameter for valid values, loading the POST data into memory, and checking if it has a valid size
on line16 I convert the data into uppercase hexadecimal because I have a bad experience with storing binary data in a MySQL database, and other than having to multiply every value by 2, it's easier to work with the data this way
in the line 18-39 block I check if the seed is somewhat valid at least (according to 3dbrew the offs=0x100, size=8 region in the seed file is all zero on retail units, and checking it in GM9's hex editor proves that)
but, here is the first occurrance where I check data which is considered sketchy... when I discovered on 3dbrew that movable.sed contains a copy of FriendCodeSeed, I wanted to also add movable.sed to the uploaded data buffer, and I did...
so, on lines 36-38 I check if the FriendCodeSeed matches the one present in movable.sed, and error out if it doesn't match (because I didn't wanted people uploading public seed by chance if they have tampered with their LocalFriendCodeSeed)...
but when I tested on my new3DS I noticed it was always triggering, I went to GM9 to check both files, and I noticed that the LocalFriendCodeSeed didn't match the one in movable.sed. later I remembered that I systransferred my old3DS to my new3DS, but I backed up the NAND before... so I mounted my decrypted old3DS CTRNAND with ImDisk, opened up movable.sed and LocalFriendCodeSeed, and noticed not only did they match, but it was the same that was in my new3DS's movable.sed, so I came to a conclusion that LocalFriendCodeSeed is always the factory one if you don't touch it.
so ye, because of this I commented out the code in the backend, but never bothered to remove the code from the 3DS client (which was a very big fault)
moving on, on line 41-42 I check if the movable.sed has a valid magic 'SEED' (again, completely unnecessary as I no longer had the need to check movable.sed, and this is the only remaining check of movable.sed in the backend code)
lines 46-54 are cheesy... intially I wanted to use the error "Not a 3DS", but I thought that if a possible hacker/bully/whatever reached here then I thought I'd troll them a bit by faking a database error by changing to username and password to a dummy one (the only reason I used an empty password is just to make the hacker/bully/whatever even angrier that "they aren't using a password for their database?! >_>", but otherwise there was a password needed to connect, so a bit less security risk there)
I won't say line numbers anymore since the code is very dense, I'll just explain how the code works from now:
so, the database had approximately this layout (I'm saying "approximately" since the server is shut down the time of writing, so I can't screenshot the database structure):
- UID, AUTO_INCREMENT
- NANDCID, varchar(32), this was the UNIQUE index (though was not applied in the database, so I had to handle it in code instead)
- datetime, unsigned int, just the time() of the user first using the service
- RawPOST, varchar(1730 iirc), this contained the 0x361 * 2 big data sent in POST (it was stripped of NANDCID because that's used for indexing anyways, so it was pointless to keep multiple copies of it)
- flags, unsigned int: bitmask of these values: 1="seed available for download", 2="the user is banned from using the service", 4="seed is prohibited to be made available for download"
as you can see, no matter what all this data was stored in the db before even continuing to execute the rest of the script
as you can see on line 79, there was a planned feature in UnbanMii 2.1 to restore your original seed. as you can see, even if you are banned you would've had the chance to restore your original seed in case you wanted to revert your original friendcodeseed if you called Ninty and they unbanned you for some miraculous reason. anyways, since this functionality was not fully implemented by the time this incident happened, there's no code yet to delete the record from the db after the user has successfully restored their original FriendCodeSeed.
on line 82 we can see the only use of SecureInfo: all I did is I compared the one in the POST data with the one stored in db to see if the user has region changed since the first use, and disallow them to upload or download seed to reduce the risk of the public seed getting banned due to that... otherwise it had no use
after this we check if the user wants to set their seed as public, and if they do then be paranoid before setting the bitmask to allow the download of the seed...
so:
- first I get the first available seed (though there's a potential oversight because I don't order the results, so it might glitch out if something)
- I accidently check twice if there are multiple seeds (which is a very big copypaste fail, considering I have used "LIMIT 1"... oh well, I'm not surprised, considering the approx. 19 errors/oversights fixed in ~1-2hrs)
line 93 is interesting... the magic zero NANDCID would've been used for a future website upload (since how would you get the NANDCID without an actual 3DS?), otherwise check if you're the first one to use UnbanMii with this seed (assuming the first person to use it owns that seed), and disallow you from marking it as publicly available if it's not yours
the rest in this if-block is self-explainatory
since the restore feature is not fully implemented all that's left is serving the public seed after all this crazy and retard security checks... it's really easy, just query the first seed flagged public (bitmask 1), check if there's even any, and just serve it (the data is stored as a hexadecimal string in the database, so it has to be hex2bin'd before serving it)
(Note: the only modification I have done is I have removed the username and the password, the rest of the script is the same as the one that was used in production)
(another note: I just noticed that someone has touched the script while I was sleeping, and has changed the capitalization on the die() error messages... the rest of the script seems to be still untouched however)
The POST data (size=0x380) contained the following stuff:
- offs=0x000, size=0x110: LocalFriendCodeSeed
- offs=0x110, size=0x140: movable.sed
- offs=0x250, size=0x111: SecureInfo
- offs=0x370, size=0x010: NAND CID
so, let's start breaking down the code...
everything 'till line 15 is boring, it's just checking the 'e' parameter for valid values, loading the POST data into memory, and checking if it has a valid size
on line16 I convert the data into uppercase hexadecimal because I have a bad experience with storing binary data in a MySQL database, and other than having to multiply every value by 2, it's easier to work with the data this way
in the line 18-39 block I check if the seed is somewhat valid at least (according to 3dbrew the offs=0x100, size=8 region in the seed file is all zero on retail units, and checking it in GM9's hex editor proves that)
but, here is the first occurrance where I check data which is considered sketchy... when I discovered on 3dbrew that movable.sed contains a copy of FriendCodeSeed, I wanted to also add movable.sed to the uploaded data buffer, and I did...
so, on lines 36-38 I check if the FriendCodeSeed matches the one present in movable.sed, and error out if it doesn't match (because I didn't wanted people uploading public seed by chance if they have tampered with their LocalFriendCodeSeed)...
but when I tested on my new3DS I noticed it was always triggering, I went to GM9 to check both files, and I noticed that the LocalFriendCodeSeed didn't match the one in movable.sed. later I remembered that I systransferred my old3DS to my new3DS, but I backed up the NAND before... so I mounted my decrypted old3DS CTRNAND with ImDisk, opened up movable.sed and LocalFriendCodeSeed, and noticed not only did they match, but it was the same that was in my new3DS's movable.sed, so I came to a conclusion that LocalFriendCodeSeed is always the factory one if you don't touch it.
so ye, because of this I commented out the code in the backend, but never bothered to remove the code from the 3DS client (which was a very big fault)
moving on, on line 41-42 I check if the movable.sed has a valid magic 'SEED' (again, completely unnecessary as I no longer had the need to check movable.sed, and this is the only remaining check of movable.sed in the backend code)
lines 46-54 are cheesy... intially I wanted to use the error "Not a 3DS", but I thought that if a possible hacker/bully/whatever reached here then I thought I'd troll them a bit by faking a database error by changing to username and password to a dummy one (the only reason I used an empty password is just to make the hacker/bully/whatever even angrier that "they aren't using a password for their database?! >_>", but otherwise there was a password needed to connect, so a bit less security risk there)
I won't say line numbers anymore since the code is very dense, I'll just explain how the code works from now:
so, the database had approximately this layout (I'm saying "approximately" since the server is shut down the time of writing, so I can't screenshot the database structure):
- UID, AUTO_INCREMENT
- NANDCID, varchar(32), this was the UNIQUE index (though was not applied in the database, so I had to handle it in code instead)
- datetime, unsigned int, just the time() of the user first using the service
- RawPOST, varchar(1730 iirc), this contained the 0x361 * 2 big data sent in POST (it was stripped of NANDCID because that's used for indexing anyways, so it was pointless to keep multiple copies of it)
- flags, unsigned int: bitmask of these values: 1="seed available for download", 2="the user is banned from using the service", 4="seed is prohibited to be made available for download"
as you can see, no matter what all this data was stored in the db before even continuing to execute the rest of the script
as you can see on line 79, there was a planned feature in UnbanMii 2.1 to restore your original seed. as you can see, even if you are banned you would've had the chance to restore your original seed in case you wanted to revert your original friendcodeseed if you called Ninty and they unbanned you for some miraculous reason. anyways, since this functionality was not fully implemented by the time this incident happened, there's no code yet to delete the record from the db after the user has successfully restored their original FriendCodeSeed.
on line 82 we can see the only use of SecureInfo: all I did is I compared the one in the POST data with the one stored in db to see if the user has region changed since the first use, and disallow them to upload or download seed to reduce the risk of the public seed getting banned due to that... otherwise it had no use
after this we check if the user wants to set their seed as public, and if they do then be paranoid before setting the bitmask to allow the download of the seed...
so:
- first I get the first available seed (though there's a potential oversight because I don't order the results, so it might glitch out if something)
- I accidently check twice if there are multiple seeds (which is a very big copypaste fail, considering I have used "LIMIT 1"... oh well, I'm not surprised, considering the approx. 19 errors/oversights fixed in ~1-2hrs)
line 93 is interesting... the magic zero NANDCID would've been used for a future website upload (since how would you get the NANDCID without an actual 3DS?), otherwise check if you're the first one to use UnbanMii with this seed (assuming the first person to use it owns that seed), and disallow you from marking it as publicly available if it's not yours
the rest in this if-block is self-explainatory
since the restore feature is not fully implemented all that's left is serving the public seed after all this crazy and retard security checks... it's really easy, just query the first seed flagged public (bitmask 1), check if there's even any, and just serve it (the data is stored as a hexadecimal string in the database, so it has to be hex2bin'd before serving it)
Note: this section doesn't explain stuff in too much detail, so if you're not satisfied with the info here then all I can say is "sorry, but this was a design choice"
so, I thought it's a good idea to use very touchy data from the user'd NAND (namely LocalFriendCodeSeed, movable.sed, and SecureInfo) for verifying the user, and verify if the data is valid or hand-crafted by a troll without having access to a 3DS to verify the integrity of the data
first, I check some things in the seed file that we can check without a 3DS, and fail if those basic checks fail (namely to see if the seed is corrupted from the start, or of the seed is from a dev 3DS (due to how security checks are implemented, dev 3DS friendcodeseeds are not supported))
earlier in development I noticed on 3dbrew (where a lot of stuff about the 3DS is documented) that there's a copy of the LocalFriendCodeSeed in the file named movable.sed... this was working and good, but then later I noticed that my new3DS triggers the "Upload of non-factory friendcodeseed is not allowed" error... after digging into it I realized that I did a system transfer from my old3DS to my new3DS, so I went to check the decrypted CTRNAND of my old3DS, and the data was matching (no wonder why it's *movable*.sed), so I commented out the code without removing the need for this file
but since I was already checking movable.sed, I added a check to see if the movable.sed magic word "SEED" is correct, otherwise fail. this is the only remaining check of movable.sed in the backend code, and is completely pointless
just before connecting I added a "troll" to troll the hackers who would try to spam the API from not a 3DS... if the User Agent (the "browser identifier" name field) didn't match the one the UnbanMii application was using it would change the database login details to fake ones, so the script would fail with a faked database error (note: the password in the fake entry is empty to make the hacker/troll/whatever even more angrier for thinking that we aren't password-protecting the database. the real database was password-protected at least, so that's a bit less security concern)
after this I used the NAND chip's ID as the way to identify unique users (I have never heard of someone replacing the NAND on their 3DS boards, so I assumed it's a good way to identify users, even though this is another touchy data)
due to the shitty design choices I made while making the backend, the first thing was to insert all of this data into the database for later use and security comparison. at this point the inserted data is marked as private, meaning nobody has access to it from the API
here it gets complicated, but I'll try my best to explain without making confusion... so, let's continue
I thought that since I'm storing all this data in the database, I could add a restore functionality to UnbanMii 2.1, but I didn't implement it fully, so there's no code yet to delete the data stored in the db after a successful restore.
if the user is not wanting to restore their original seed, immediately it's checked if the user has been banned from UnbanMii, and since SecureInfo is uploaded (which contains your region and your 3DS serial number) it's easy to check if the 3DS has been region changed, and prevent the user from using UnbanMii until they undo the region change to prevent the ban of the public seed due to region change (yes, I know you must be frightened out by me saying that, and yes, I know I'm stupid for doing that, but when I made the code my mind wasn't clear, so I didn't double-think about it at all, I was concentrating on adding the most security checks possible instead for some reason)
this section only applies if you wanted to upload your seed:
- first I query the database for the first seed which is equal to yours trying to upload
- then I check if the NAND ID matches yours (or if it was uploaded from a planned web interface)... if it's from a website upload or you're the first one to use UnbanMii with the seed you want to upload, the seed will be marked public, otherwise the "this seed is not owned by you" error is thrown
- after querying the seed, I check the flags on it to see if the seed's owner or the seed itself is banned from being made public, and if that's the case then the "this seed is banned from this service" error is returned
- if there was no error then I update the seed in the db to make it public
- later in the script it'll die with the error message "thank you for your contribution"
since restoring is not implemented fully in the script, if you were to hax UnbanMii to have the restore feature, the server would die with the error message "programming error, this should never happen!"... funny, eh?
since all security checks were passed, all that's left is to query the latest public seed from the database, and serve it... the rest is handled by the UnbanMii 3DS client
so, I thought it's a good idea to use very touchy data from the user'd NAND (namely LocalFriendCodeSeed, movable.sed, and SecureInfo) for verifying the user, and verify if the data is valid or hand-crafted by a troll without having access to a 3DS to verify the integrity of the data
first, I check some things in the seed file that we can check without a 3DS, and fail if those basic checks fail (namely to see if the seed is corrupted from the start, or of the seed is from a dev 3DS (due to how security checks are implemented, dev 3DS friendcodeseeds are not supported))
earlier in development I noticed on 3dbrew (where a lot of stuff about the 3DS is documented) that there's a copy of the LocalFriendCodeSeed in the file named movable.sed... this was working and good, but then later I noticed that my new3DS triggers the "Upload of non-factory friendcodeseed is not allowed" error... after digging into it I realized that I did a system transfer from my old3DS to my new3DS, so I went to check the decrypted CTRNAND of my old3DS, and the data was matching (no wonder why it's *movable*.sed), so I commented out the code without removing the need for this file
but since I was already checking movable.sed, I added a check to see if the movable.sed magic word "SEED" is correct, otherwise fail. this is the only remaining check of movable.sed in the backend code, and is completely pointless
just before connecting I added a "troll" to troll the hackers who would try to spam the API from not a 3DS... if the User Agent (the "browser identifier" name field) didn't match the one the UnbanMii application was using it would change the database login details to fake ones, so the script would fail with a faked database error (note: the password in the fake entry is empty to make the hacker/troll/whatever even more angrier for thinking that we aren't password-protecting the database. the real database was password-protected at least, so that's a bit less security concern)
after this I used the NAND chip's ID as the way to identify unique users (I have never heard of someone replacing the NAND on their 3DS boards, so I assumed it's a good way to identify users, even though this is another touchy data)
due to the shitty design choices I made while making the backend, the first thing was to insert all of this data into the database for later use and security comparison. at this point the inserted data is marked as private, meaning nobody has access to it from the API
here it gets complicated, but I'll try my best to explain without making confusion... so, let's continue
I thought that since I'm storing all this data in the database, I could add a restore functionality to UnbanMii 2.1, but I didn't implement it fully, so there's no code yet to delete the data stored in the db after a successful restore.
if the user is not wanting to restore their original seed, immediately it's checked if the user has been banned from UnbanMii, and since SecureInfo is uploaded (which contains your region and your 3DS serial number) it's easy to check if the 3DS has been region changed, and prevent the user from using UnbanMii until they undo the region change to prevent the ban of the public seed due to region change (yes, I know you must be frightened out by me saying that, and yes, I know I'm stupid for doing that, but when I made the code my mind wasn't clear, so I didn't double-think about it at all, I was concentrating on adding the most security checks possible instead for some reason)
this section only applies if you wanted to upload your seed:
- first I query the database for the first seed which is equal to yours trying to upload
- then I check if the NAND ID matches yours (or if it was uploaded from a planned web interface)... if it's from a website upload or you're the first one to use UnbanMii with the seed you want to upload, the seed will be marked public, otherwise the "this seed is not owned by you" error is thrown
- after querying the seed, I check the flags on it to see if the seed's owner or the seed itself is banned from being made public, and if that's the case then the "this seed is banned from this service" error is returned
- if there was no error then I update the seed in the db to make it public
- later in the script it'll die with the error message "thank you for your contribution"
since restoring is not implemented fully in the script, if you were to hax UnbanMii to have the restore feature, the server would die with the error message "programming error, this should never happen!"... funny, eh?
since all security checks were passed, all that's left is to query the latest public seed from the database, and serve it... the rest is handled by the UnbanMii 3DS client
more info that's important regardless of being technical or not:
- I had no malicious intentions *at all*... the reason for uploading such touchy files was purely for very shitty security checks, and the touchy data (movable.sed and SecureInfo) was purely used by the script to check stuff
- some people let me know that the NAND CID (or as mentioned in the non-technical writeup: "NAND chip's ID") is also a touchy data... since I don't know of an occasion where people have replaced the NAND chip on their 3DS boards, I thought that this is a good way to *somehow* identify unique users
- the reason I stored the data unencrypted in the database is because all 3 people who had access to the server don't know how to SQL (or even how the database manager UI works), I didn't even think about more about it since I knew that nobody else could access the database other than me
- note to tech sawwies: I was using MySQLi prepared statements, and I don't know a way exists to exploit that... but data was checked before even a connection was made to the database, so there was absolutely no way to exploit this
- my mind wasn't clear (and it still isn't) when I worked on the code/backend, and there were ~19errors/mistakes fixed in a ~1-2hr timespan before the initial release, so ye... I was only focusing on getting the work done, and I didn't even think about how the data I'm working with was touchy, nor about how illegal it was... I'm sorry for that
so ye... my wanting to add too many safety checks went super wrong, and I'm sorry about that. the data in the database isn't used by any human at all, it's only used by the backend API code to check some data validity and eliminate possible risks for banning the public seed served by this...
edit (17/07/28 10:01): I got access to the server and did a DROP TABLE which got rid of a whopping 17 entries, 3 of which were from the team... a bit of a waste of antidepressants for just those 14 entries...
oh, also fixed the title as the typo was really triggering my OCD