Friday, May 7, 2021

Long Live the SSH Ramdisk

A few weeks back a friend ran into a strange little bug that ended up corrupting some relatively unimportant system files on their phone. Those files were important enough that iOS wouldn't boot as long as the files remained corrupted, but unimportant enough that simply deleting them would cause iOS to generate them anew. The problem was, the phone wouldn't boot, so we couldn't access those files to delete them. So begins our journey.

One of my first thoughts when I realized what had happened was of this little project called ssh-rd by msftguy. For those who might not know, it was a really useful tool back in the day for essentially live-booting a small OS that you could SSH into (using limera1n). Offline filesystem access would be a perfect fix for this scenario.

I googled and found two similar projects that appear to work with checkm8 devices. We have SSH-Ramdisk-Maker-and-Loader by Ralph0045, and telnetd_ramdisk by danieltroger. Neither project worked for me out of the box (I'll spare the details, see issues page on both projects). I also found this somewhat related guide. While neither project worked for me, they proved the idea was possible and the source code pointed me in the right direction.

Rather than develop and release another tool, I'll try to walk through the steps for doing this manually and provide a couple of scripts that will help you along the way. This isn't really for the faint of heart. You will need to compile a lot of tools, and what worked for me may not work for you. This guide assumes a 64-bit device, though it could be modified to work for 32-bit.

At the end of this guide, I'll also provide a few scripts that can be used as a quick-start point that will hopefully save someone a bit of time.

My setup:

  • Mac running 10.15 (Catalina)
  • iPhone 8 running 13.5

Dependencies

  • A device vulnerable to checkm8 (A11 processor or earlier)
  • Your IPSW
    • While it will need to be for the correct device, this doesn't need to be the version you have installed. This is the IPSW we will use as a base image to boot. I used 13.5, I've heard it works well for other people too. iOS 14 may not work for you here. You might need to try multiple versions and see what works.
  • An APTicket for your device.
    • This does not need to be for any specific version. I used a 13.7 ticket.
    • You can extract one from a saved SHSH2 blob.
  • Some keys from theiphonewiki.
    • These should match the firmware of your IPSW.
  • XCode command line tools
    • xcode-select --install
  • The iPhone SDK for your firmware
    • We need this to compile restored_external
    • You don't need to be a developer to download the SDKs anymore
    • Drop it in /Library/Developer/CommandLineTools/SDKs/
    • If you can't compile it you can try my binary, but it probably won't work
  • img4 via img4lib
  • kairos
  • Kernel64Patcher
  • irecovery via libirecovery
  • compareFiles.py
    • curl 'https://raw.githubusercontent.com/dualbootfun/dualbootfun.github.io/d947e2c9b6090a1e65a46ea6a58cd840986ff9d9/source/compareFiles.py' | sed -n '3,$p' > ./bin/compareFiles.py
  • ldid2 via xerub's ldid fork
  • iproxy via libusbmuxd
  • PyBoot - An easy way to pwn DFU, though not the only way
  • Something that can extract a deb file (I use 7zip)
  • Some dropbear resources
    • A launch daemon
    • A host key
      • Note: normally it's a bad idea to reuse host keys, but in this case, it's not going to be a problem since the interface will only ever be accessible over USB. If you want to generate your own dropbear host key, though, that's not a problem.
  • restored_external.c
  • A couple more things we'll wget along the way

Setting up

First we've got to decrypt and then sign a few things from the IPSW.
  1. Extract your IPSW and put the relevant contents into a folder called "resources". You will know which resources are relevant because they will be listed for your device / firmware in theiphonewiki. Not all the files in your ipsw will be relevant.
    7z x ./iPhone_4.7_P3_13.5_17F75_Restore.ipsw
    1. Restore ramdisk (you don't need the update ramdisk or root filesystem)
    2. Restore ramdisk trustcache
    3. DeviceTree
    4. applelogo
    5. iBEC
    6. iBSS
    7. kernelcache
  2. Add your SHSH2 blob to the "resources" folder

Building the bootchain

This phase will be three steps. Not all items will go through each step. For "IV+KEY", you'll just concatenate the IV and the KEY (in that order) found for the relevant file on theiphonewiki. If your file has a key listed on the iphonewiki, use it, otherwise leave the key blank.
  1. Decrypt - We'll store the decrypted files in the "decrypted" folder. Be sure to use IVs and keys from theiphonewiki as necessary.
    mkdir ./decrypted/
    img4 -i ./resources/iBSS* -o ./decrypted/iBSS.dec -k IV+KEY
    img4 -i ./resources/iBEC* -o ./decrypted/iBEC.dec -k IV+KEY
    img4 -i ./resources/applelogo* -o ./decrypted/applelogo.dec
    img4 -i ./resources/DeviceTree* -o ./decrypted/devicetree.dec
    img4 -i ./resources/kernelcache* -o ./decrypted/kernelcache.dec
    img4 -i ./resources/*dmg -o ./decrypted/ramdisk.dec.dmg
  2. Patch - Patch a few things and put them in the "patched" folder. We generate a binary patch for the kernelcache to use later.
    mkdir ./patched/
    kairos ./decrypted/iBSS.dec ./patched/iBSS.patched
    kairos ./decrypted/iBEC.dec ./patched/iBEC.patched -b "rd=md0 -v"
    Kernel64Patcher ./decrypted/kernelcache.dec ./patched/kernelcache.patched -a
    ./bin/compareFiles.py ./decrypted/kernelcache.dec ./patched/kernelcache.patched # > ./kc.bpatch
  3. Sign - Extract the apticket from the shsh2 file, and then use it to sign everything.
    mkdir ./rd_image/
    plutil -extract ApImg4Ticket xml1 -o - ./resources/*.shsh2 | xmllint -xpath '/plist/data/text()' - | base64 -D > ./apticket.der
    img4 -i ./patched/iBSS.patched -o ./rd_image/iBSS.img4 -T ibss -A -M ./apticket.der
    img4 -i ./patched/iBEC.patched -o ./rd_image/iBEC.img4 -T ibec -A -M ./apticket.der
    img4 -i ./decrypted/applelogo.dec -o ./rd_image/applelogo.img4 -T logo -A -M ./apticket.der
    img4 -i ./decrypted/devicetree.dec -o ./rd_image/devicetree.img4 -T rdtr -A -M ./apticket.der
    img4 -i ./resources/kernelcache* -o ./rd_image/kernelcache.img4 -T rkrn -P ./kc.bpatch -J -M ./apticket.der
    img4 -i ./resources/*trustcache -o ./rd_image/trustcache -M ./apticket.der

Build the ramdisk

Essentially the idea here is that we're going to create a sort of 'skel' directory with a structure that matches the ramdisk. It will contain all the files we want to use when we've booted (binaries, ssh server, etc). Then, we'll copy that skel directory over the top of the ramdisk. We also need to expand the ramdisk so that we'll have space to add those files. Finally, we sign it with our apticket just like the rest of the images.
  1. Create ramdisk skel, populate it with some binaries and paths
    mkdir ./rd_skel/
    cd ./rd_skel/
    wget -q 'http://newosxbook.com/tools/binpack64-256.tar.gz'
    tar xzf ./binpack64-256.tar.gz
    rm -f ./binpack64-256.tar.gz
    mkdir -p ./var/root/
    chmod +x ./usr/bin/*
  2. Install ncurses libs
    wget -q 'https://apt.bingner.com/debs/1443.00/ncurses_6.1+20181013-1_iphoneos-arm.deb'
    7z x ./ncurses*.deb > /dev/null
    rm ./ncurses*.deb
    tar xf ./data.tar 'usr/lib/'
    rm ./data.tar
    cd ./usr/lib/
    ln -s ./libncurses.6.dylib libncurses.5.4.dylib
    cd ../../../
  3. Add dropbear resources
    mkdir -p ./rd_skel/System/Library/LaunchDaemons/
    mkdir -p ./rd_skel/etc/dropbear/
    wget -q 'https://gist.githubusercontent.com/compilingEntropy/60e84d15bc274f88b6f53e6c3788e8e9/raw/597355e51831cafef8751b598c15832bac5fdd9d/dropbear.plist' -O ./rd_skel/System/Library/LaunchDaemons/dropbear.plist
    wget -q 'https://gist.githubusercontent.com/compilingEntropy/f7042cfb1f402c6eff0afb14014cefe1/raw/c7d73ad466a3e8af7828a58a5be0c6f490b68f3e/id_rsa' -O - | base64 -d > ./rd_skel/etc/dropbear/id_rsa
    wget -q 'https://gist.githubusercontent.com/compilingEntropy/ff0a80f156a135f7c386598a44ba8bb3/raw/bd594fcb4d252de3c41a25a267c4350cb70078b9/motd' -O ./rd_skel/etc/motd
  4. Resize and create ramdisk
    cp -a ./decrypted/*.dec.dmg ./ramdisk.dmg
    hdiutil resize -size 150MB ./ramdisk.dmg
    mkdir ./mnt/
    hdiutil attach -mountpoint ./mnt/ ./ramdisk.dmg
  5. Build / sign restored_external, add it to ramdisk
    mv ./mnt/usr/local/bin/restored_external{,_original}
    wget -q 'https://gist.githubusercontent.com/compilingEntropy/3c6f19f85cdce53fdf44b7b84005023d/raw/c0057abfe00eba3971a9c0a1d296e07dde23467c/restored_external.c' -O ./resources/restored_external.c
    xcrun -sdk iphoneos clang -arch arm64 ./resources/restored_external.c -o ./mnt/usr/local/bin/restored_external
    ldid2 -S ./mnt/usr/local/bin/restored_external
  6. Apply our modifications
    rsync --ignore-existing -ahuK ./rd_skel/ ./mnt/
  7. Sign and pack the ramdisk
    hdiutil unmount ./mnt/
    rmdir ./mnt/
    img4 -i ./ramdisk.dmg -o ./rd_image/ramdisk -T rdsk -A -M ./apticket.der
  8. Cleanup
    rm ./ramdisk.dmg
    rm -rf ./rd_skel/

Booting the ramdisk

Here, we'll pwn DFU mode and then send our images to the device with irecovery. We'll also send some commands to the device, usually to tell it to load the images.
  1. Enter DFU mode
  2. pwn DFU
    cd ./bin/PyBoot/
    ./pyboot.py -p
  3. Send the files / commands with irecovery. Sometimes it helps to wait a second or two between sending images to give the device a chance to finish loading them.
    echo 'sending ibss (to jump to recovery mode)'
    irecovery -f ./iBSS.img4
    echo 'sending ibss again'
    irecovery -f ./iBSS.img4
    echo 'sending ibec'
    irecovery -f ./iBEC.img4
    echo 'loading ibec'
    irecovery -c go
    echo 'sending ramdisk'
    irecovery -f ./ramdisk
    echo 'loading ramdisk'
    irecovery -c ramdisk
    echo 'sending logo'
    irecovery -f ./applelogo.img4
    echo 'loading logo'
    irecovery -c 'setpicture 5'
    echo 'sending devicetree'
    irecovery -f ./devicetree.img4
    echo 'loading devicetree'
    irecovery -c devicetree
    echo 'sending trustcache'
    irecovery -f ./trustcache
    echo 'validating firmware against trustcache'
    irecovery -c firmware
    echo 'sending kernel'
    irecovery -f ./kernelcache.img4
    echo 'booting now'
    irecovery -c bootx
You should see your screen display the apple logo, followed by a verbose boot. Then, the device will display an apple logo with a loading bar. This is when you can connect to the device. After a few minutes, the screen will go dark again. This doesn't make a difference to your connection.

Connecting

iproxy lets us make network connections over usb. We'll use it to forward port 22 on the device to port 2222 on our client. From there, you simply need to SSH to the device.
  1. iproxy
    iproxy 2222:22 &
  2. SSH - password is alpine
    ssh root@127.0.0.1 -p 2222
  3. When you're done, simply:
    reboot

Wrapping up

While there are some limitations to what we can do while connected to a device with an SSH ramdisk, this can be extremely useful for resolving certain issues. In the case of my friend, deleting a few corrupt files was all that was needed for the device to boot right up again!

I do want to mention that this process is untested on iOS 14. iOS 14 introduced a "feature" where the phone is able to detect whether a device was booted from DFU or not, and if it finds that it has been, the device will reboot. If you test this with iOS 14, please report your findings in the comments or let me know on twitter and I'll update this post.

Lastly, I'll include a few scripts you can use as a template to speed this process along. If they don't say "done" after running them, something has failed along the way and you'll have to debug that. These scripts aren't necessarily "out of the box"—you will have to modify them to fit your setup. With those caveats in mind, this should be a good starting point if you want to automate this process.

Saturday, December 14, 2013

Finding Magic Bytes

So you know what fuzzing is, and maybe you've tried it yourself. (If you don't, read my previous post and get caught up.) If you're lucky, you even have a crash or two. The obvious next step is to find out why your file crashed your target application*. During the fuzzing process, you probably used a tool such as zzuf or peach to mutate a certain amount of a file. (These tools work by taking your original file, changing a few bytes here and there, and generating a new file that contains those differences.) Depending on how you use them, these tools can make anywhere from a few dozen to several thousands of changes to your file. The question then becomes, "Which of these changes caused the crash?" Answering that question is the first step in a process known as reverse engineering
The process of finding out which changes actually matter is usually time-consuming and frustrating. The idea is that you take your mutated file and slowly un-mutate it until you find out which part crashed your target. The opposite method can also be applied, where you take your original file and slowly apply the same mutations that exist in your crash file until you find the differences that matter. The name I like to give to those certain changes that cause the crash are the magic bytes.
Finding the magic byte is tricky and it takes forever. Ain't nobody got time fo dat! Programmers are lazy and impatient. I wanted a tool where I could just feed in the file I started with and the file that causes crashes, and it would do all the work for me and make a handy list of the magic bytes. Unfortunately, I couldn't seem to find any such tool. After a few minutes of digging about on the googlez, I decided to make one myself. I needed to come up with an algorithm, then, that would find the magic byte in the least amount of time possible.

Let's look at this problem graphically. I'm not really a visual person, but I've found that it's easier to explain things with pictures. Here's a beauty I came up with in mspaint:





"It's a box", you say. Your shape recognition skills are off the charts, you're ready to move on to 1st grade. Here we go.
Let's imagine that this box represents your mutated file. Somewhere in this file is a magical byte that will lead to happiness and never-ending rainbows. I'm going to represent that byte with a dot.





So let's say that there are 100 differences between your original file and your mutated file. You don't know where that dot is. How do you find it?

The Obvious Way:

The white parts are diffs that got kept, the grey parts are diffs that didn't get written. The resulting file only crashes if the dot is in the white zone.
Programmers are lazy. If you want the answer but you don't want to actually have to think, you could just make 100 files that contain one difference each and test all of them until something crashes. This method is really slow, but it does work most of the time (stay tuned for the part where I talk about why it doesn't always work). The thing is, you don't want to have to test 100 individual files. Lucky for us, it's easy to double the efficiency!

The Obvious Way 2.0:


So now we only have to load a maximum of 52 files. The first 50 would narrow down the diffs to the last two, then you'd test each of those two individually. That's way more efficient than The Obvious Solution 1.0, right? But hang on, if keeping more bytes per file gives you more efficiency, then the best way would be to keep all the bytes? Most of the bytes? As I'm sure you've figured out by now, the smarter way to do it would be to cut the box in half.

The Smart Way:


Bam, now we've got a crash within the first two tests and you already know that half of the differences don't matter. The best thing to do at this point is to repeat The Smart Way using your new file that only contains 50 differences as the mutated file. It's going to look something like this:


Programmers call this type of structure a binary tree. There are two possible outcomes for each test:
  • All of the magic bytes were in the range you tested, and the file crashed
  • None of the magic bytes were in the range you tested, and the file did not crash
If the first range does not crash, you know the second one has to contain the magic byte and it's not necessary to test it. The maximum number of files you'd need in order to find the magic byte can be represented by the function log2n where n is the number of differences between two given files. log2100 comes out to be about 6.644, which, because we can't test a file a fraction of a time, rounds up to 7. So for 100 differences, it's going to take exactly 7 files to find your magic byte every time. That's much fewer than 52, eh? Using a binary search is the most effective solution to finding any given magic byte.

The Problem:


Put simply, how do you know that only one changed byte is required to cause the crash? The crash could very easily be caused by multiple bytes working together.


In this scenario, neither file would crash. This is because in order for this crash to happen, both bytes must be present--that is, both dots must be in the white zone.

A binary search fails in this case; we need a new solution that works for two-byte scenarios. When you start looking for magic bytes, you have no indication of how many there are going to be. This means that you have to have one solution that's optimal for both one and two byte scenarios. You can't use a classic binary tree anymore because they're dependent on the fact that if something's not in one half, it's going to be in the other half. You don't know that anymore; there are actually three possible outcomes now:

  • All of the magic bytes were in the range you tested, and the file crashed
  • None of the magic bytes were in the range you tested, and the file did not crash
  • Some of the magic bytes are in the range you tested and some are not, so the file did not crash
It's the addition of this third possibility that breaks binary searching. You can, however, modify the classic binary search slightly in order to account for this.

The Smart Way 2.0:




For one magic byte, the new maximum number of files you have to test is 14. The minimum number of files you'd have to test is 7 (that's if it crashes every time). (Mathematically, that's a maximum of 2(log2n), a minimum of log2n, and an average of 1.5(log2n).) This is because you now have to test both halves; you can't narrow anything down until you actually find a crash. In the event that neither file crashes, you know you're dealing with a bug that requires a minimum of two magic bytes. 
If the two magic bytes are near each other in the file, you may be able to eliminate a large portion of the useless changes in the fastest possible way before you realize that there are two bytes. Eventually, though, you're going to get to a point where neither file crashes. What do you do at this point? You'll need a new algorithm.

The Smart Way 3.0:

As I previously mentioned, the first two files wouldn't crash. After you've exhausted the most optimal way of eliminating diffs that don't matter (keeping 1/2 of the diffs), you move on to the next most optimal way, which is to keep 2/3 of the file. Doing this creates the third file, which eliminates the maximum amount of useless diffs while still crashing.
If there is one magic byte, you'll only ever need to break the file in two pieces. If there are two magic bytes, you'll need to break the file into three pieces to find those two magic bytes. If you're keeping 2/3 of the file and none of the three files crash, you have at least three bytes that are required to cause your crash. The number of pieces you need to break the file into is always one more than the number of magic bytes there are. Therefore, once you break the file into three pieces, you don't go back to breaking it into two pieces again. 

Another Outcome?

Although the third file crashes in the example above, what happens if you still test files four and five? If you eliminate the first third as unimportant as in the example above, then when testing the remaining two files you're actually testing the file in halves. Because you know you have a minimum of two bytes at this point (the only reason you'd move from halves to thirds is if you have at least two bytes), and because the number of pieces you need to break the file into is always one more than the number of magic bytes there are, files four and five will never crash. This means that if files four or five do crash, you have multiple bugs in your crash file. 
This can most easily be seen looking at a simpler scenario, so let's move back to testing halves. There are actually four outcomes, not three:
  • The first half contains all the magic bytes (resulting in a crash)
  • The second half contains all the magic bytes (resulting in a crash)
  • Some of the magic bytes are in the first half, some are in the second half (neither half crashes)
  • Both halves of the file contain a complete set of magic bytes (both halves crash, two bugs are present)
This means that if you account for the fourth outcome where there are two bugs, it will always take exactly 2(log2n) files in order to find a single magic byte (up from an average of 1.5(log2n) that you'd get using The Smart Way 2.0). Note that detecting the fourth outcome above is possible with thirds only if you always tested both halves when you were originally testing halves. The same goes for fourths; if you want to test for multiple sets of bugs and one of the bugs requires at least 3 bytes, you must have always tested both halves and all three thirds when you were working up to the fourths. Otherwise, you'll get false positives. Naturally, you'd get false positives if you just started with thirds and there was only one magic byte:

As you can see, files one and three both crash which would normally indicate that there were two bugs. This is a simple example of a false positive. This situation would never occur if you start with halves instead of thirds.

The Problem:

As those of you who aren't lost yet may have noticed, there is actually one situation in which you can still have a false positive when testing for multi-bug scenarios. This occurs only if there are two magic bytes in the middle third of the list of diffs, but on opposite halves of the file. (This false positive does not occur when moving to fourths or fifths, only thirds.) It looks like this:


Multiple files crash in the same testing stage (thirds), but there's clearly only one bug present. Luckily, this is the only false positive present when looking for multiple bugs, and it's easily tested for.

The Solution:


If the algorithm detects two bugs in the thirds stage of testing, all you have to do is test to see if this is the result of files one and three (in this case 34:100 and 1:66), and if so, test just the middle third. If the middle third does not crash then you do in fact have two bugs, but if the middle third does crash then you've successfully detected the false positive. In that case, you can continue testing using just the middle third.

What's Left?

Tired yet? We're pretty much done. The only thing left to do is write the code, which I've mostly done for you. You can get the current version of bytefinder on my GitHub. It includes two modes, which correspond to the two best solutions above.
The first mode is normal mode, in which you give it a file and a mutated version of that file which causes a crash. The program then looks for magicbytes while taking into account that there could be multiple bugs. It will find the bug that requires the fewest amount of magic bytes for you, and list what they are. (Currently, it does not find all the magic bytes for the second bug. This is on my list of things to do.)
The second mode, quick mode, is used by passing -q as a parameter. This mode is slightly faster, but it doesn't detect whether there could be multiple bugs present. It will always find the bug that requires the fewest amount of magic bytes.
So what does it look like?


Your magic bytes are listed for you and a file that only contains those differences is generated. =) If you'd like to learn more about what to do next or reverse engineering in general, I recommend checking out this post by dirkg.
I'm sure you have questions about this post. Feel free to comment below, or contact me on twitter @compiledEntropy. I'll be happy to answer whatever questions you have. If you think of ways to improve my methods or find faults with them, I'm open to criticism as well. That's all for now! Enjoy, eh?



Important note:
bytefinder is not built to handle kernel panics. It will eventually be upgraded to handle them, I'm working on this now, but the optimization is going to be completely different. If you're a math wizard and you want to help, contact me on twitter to discuss specifics.

*The tool I wrote is built to automate this process for MobileSafari on iOS because that's my target. The methods discussed for fuzzing applications and reverse engineering crashes are valid for pretty much any target, however.

Acknowledgements: 
Huge, massive props to uroboro for implementing my crazy feature requests into hexdiff, which my tool is completely dependent on. Without it, it would be extremely difficult to interact with differences between files in a way that allows for such fine-grained control.

Friday, December 6, 2013

What's a fuzz?

In my next post I'll be explaining the purpose behind my new tool, bytefinder, and how it works. Before we get into that, I'll give a brief intro to fuzzing, what it is, how to do it, and why you should use it. Feel free to skip this intro, or just read parts of it if you think you know your stuff. ;)

What is fuzzing?
The idea behind fuzzing is that a sufficiently complex program (specifically, one designed to be able to accept an infinite amount of different inputs, such as packets or files) cannot possibly know how to parse and handle an infinite amount of different inputs (throwing errors when that input is invalid). 
Fuzzing is a technique that consists of repeatedly and intentionally providing a program with invalid (and in some cases, random) input in an attempt to evade the parser's error handling and cause the program to behave erratically. These behaviors include, but are not limited to: crashing the program, halting it, leaking memory, bypassing input sanitation checks, etc. 
The two most common targets of fuzzing are file formats and network protocols; any input can be fuzzed, however. Check this out for more info.

How would I go about this?
The real answer is that it depends on what you're fuzzing. In my case, I'm fuzzing MobileSafari using file containers, so the methodology is fairly simple: open a file parsable by MobileSafari using a hex editor, modify several bytes, save the modified file, and view it with MobileSafari. 
Obviously, doing this one time doesn't have a very high chance of causing a crash--that's why you'd do it thousands of times. There are tools which completely automate the creation and injection of files into MobileSafari using fancy engines (I even wrote one myself). It's not really something I'm going to get into in this post, however. See Nexuist's beginner guide on that here for more info, or just add 'http://repo.tihmstar.org' in Cydia and play with some of the tools in there. (I recommend using them on a restorable (A4) test device with blobs; there's a good possibility you can mess something up and have to restore.) Actually, add that repo anyway because you'll need it later on.

But why??!?!?!1!?eleven!?
The erratic behaviors sometimes caused by fuzzing (crashing the program, halting it, leaking memory, failing error handling, bypassing input sanitation checks, etc.) are desired because they occasionally hint at deeper, underlying vulnerabilities can potentially be exploitable (hacking things is the goal here.) Why exploit things? So we can get The Cydia™, obviously. ;)

Closing remarks:
When I first started fuzzing MobileSafari, I assumed it would be like throwing crap at the wall hoping something would stick. Once I got started, I realized it's more like throwing rocks at a tree--almost none of them stick, and most of the time, you miss the tree entirely. The key here is to be patient and pick rocks that look sticky. (Okay, so it's not a perfect analogy. Deal with it. :P)
A wise man once said, "Never give up, never surrender!" If you don't get any crashes, keep trying, eh?




The Cydia™ is a trademark of the "and then i deleted", LLC (formerly SaurikIT, LLC). Any and all opinions stated herin belong to iH8sn0w.