So haven’t been around for a while, due to the flu. So it gave me plenty of time to play PapiJump and PapiRiver. Though I kind of got interested in the method of high scores so I took a more indepth look at it. I’ll posted all the code to emulator a papi* score submit. I took out the little tidbit that actually sends the request, so this just prints to the screen. I’ve also removed the “secret keys” which wouldn’t be too hard to find if your really wanted to use this.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
/* * @file papihack.java * * @author Tim Strazzere * * @date Jan 20th, 2009 (finished same day) * * @desc: * An attempt to spoof the high score function of papijump, * and papiriver since they are just different secret keys * * should produce a url similar to: * * http://www.sunflat.net/android/cmd/postSc?gid=2001&v=1.0.1&lid=1&tid=4232145dfg8432145&dt=1232488839&sc=39927&ha=1007200355&tt=1&tn=T-Mobile+G1+Vi116143 * [broken down] * http://www.sunflat.net/android/cmd/postSc? * // game id * gid=2000 * // game version * &v=1.0.0 * // license? * &lid=1 * // terminal id * &tid=4232145dfg8432145 * // date / 1000 * &dt=1232171270 * * // score? * &sc=844 * // CRC'ed hash * &ha=3817043337 * * // eh? always 1 * &tt=1 * // terminal name * &tn=T-Mobile+G1+Vi116143 * */ import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.zip.CRC32; import java.util.zip.Checksum; public class papihack { public static void main(String[] args) throws UnsupportedEncodingException{ String comma = ","; //String appVersion = "1.0.0"; // version for papijump String appVersion = "1.0.1"; // version for papiriver //String secretKey = "6977616e74746f6368656174"; // secret key for papijump String secretKey = "69276d616368656174657221"; // secret key for papriver String terminalName = "T-Mobile G1 Vi116143"; // (Build.Model + " Vi" + Build.Version.Incremental) (needs to be websafe) String terminalID = "4232145dfg8432145"; // (Android_ID) //String gameID = "2000"; // papijump game id String gameID = "2001"; // papiriver game id String lid = "1"; String score = "39927"; // score you want String postUrl = "http://www.sunflat.net/android/cmd/postSc?"; Date date = new Date(); long dateLong = date.getTime()/1000; StringBuilder hash = new StringBuilder(); hash.append(gameID + comma + appVersion + comma + lid + comma + terminalID + comma + dateLong + comma + secretKey + comma + score); StringBuilder newHash = new StringBuilder(hash.toString().valueOf(hash)); newHash.append(",tt1"); Checksum checksum = new CRC32(); byte[] bytes = new byte[1024]; int len = newHash.toString().length(); Long value; bytes = newHash.toString().getBytes("UTF-8"); checksum.update(bytes, 0, len); value = checksum.getValue(); StringBuilder url = new StringBuilder(); url.append(postUrl); url.append("gid=" + gameID); url.append("v=" + appVersion); url.append("lid=" + lid); url.append("tid=" + terminalID); url.append("dt=" + dateLong); url.append("sc=" + score); url.append("ha=" + value.toString()); url.append("tt=1"); // const ? url.append("tn=" + terminalName); System.out.println(url.toString()); } } |
Ah, also – kudos to anyone who figures out what I replaced the keys with. It’s sort of a little joke…
On a little after thought, maybe it should be called papipwn? Eh – oh well, either way it will be easily bannable/removable by the administrators. It’s not hard as they are linked to your specific android id so I wouldn’t recommend using this or if you do, going over board with it.
Thanks to my friend Gabor, over at http://mylifewithandroid.blogspot.com/ has created a really well done dex file dissembler. The direct link for the post is here and the source code is all free and located at dedexer.sourceforge.net.
It’s nice as it outputs the format in jasmin like the following;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
.method public calc1(I)I packed-switch v2,0 ps418_422 ; case 0 ps418_426 ; case 1 ps418_42a ; case 2 default: ps418_default ps418_default: const/4 v0,15 l420: return v0 ps418_422: const/4 v0,2 goto l420 ps418_426: const/4 v0,5 goto l420 ps418_42a: const/4 v0,6 goto l420 nop .end method |
Opposed to the normal;
|
1 2 3 4 5 6 7 8 9 10 11 |
000418: 2b02 0c00 0000 |0000: packed-switch v2, 0000000c // +0000000c 00041e: 12f0 |0003: const/4 v0, #int -1 // #ff 000420: 0f00 |0004: return v0 000422: 1220 |0005: const/4 v0, #int 2 // #2 000424: 28fe |0006: goto 0004 // -0002 000426: 1250 |0007: const/4 v0, #int 5 // #5 000428: 28fc |0008: goto 0004 // -0004 00042a: 1260 |0009: const/4 v0, #int 6 // #6 00042c: 28fa |000a: goto 0004 // -0006 00042e: 0000 |000b: nop // spacer 000430: 0001 0300 faff ffff 0500 0000 0700 ... |000c: packed-switch-data (10 units) |
Great work Gabor, and keep up the good work!
So I’m not sure why I didn’t think to try to get live market data? For some reason I just *figured* it would be done with SSL or something so I just didn’t try it. Long story short, after building a market-cache parser – I realized, DOH! You can just get the market data live using certain requests! Luckily the parser I made worked fine with the data that I was getting sent back.
It took a little reversing of Vending.apk to see exactly what type of encoding was being used – I sort of guessed right just by looking at what the phone was sending via a Cain&Abel and Wireshark dump. Though google just happened to have some data in there that would through errors if you tried decoding the data directly or certain ways.
I’ll post come up some of the reversing I did on Vending.apk – more specifically the .buildPostParameters routine which is where everything was created for post. It’s actually pretty interesting stuff, and it helped find some routines online that google has publicly available through apache… Though I didn’t find them in the Android libraries
(nice of them, no? haha)
Anyway, tournament tomorrow followed by a snowboarding trip tomorrow – so I’ll probably post that data on monday!
Ok – so lately I’ve been tooling around with a few different ideas. As a practical use to my injection ideas and to incorporate my love of asm, well – combine the two! This would result in essentially a dex byte-code translator that could then run on the AndroidOS. The main idea behind it would be to code up a basic structured java file that contains all the higher level functions you would need. Toast, web stuff, etc could all be considered “higher level” functions. Then the main part of the code would be asm that is translated into dex bytecode by DroidASM and injected into the premade dex file.
Ok, so no I don’t think a huge number of people would use it, but heck – it’d be a nice little tool and a good practice at coding. Just dumping this idea into the blog as of right now, since I only have bits and pieces of code that don’t make a whole lot of sense, so it would be useless to post them right now. More to come though, for sure.
Previously I wrote about how to uniquely identify Android devices without special permissions. However, maybe you want to get into the nitty-gritty and get an even more unique identifier for the device. This can be done, but you need special permissions. Essentially what I mean by “special permissions” is that the user will be prompted when installing that “this application tries to do this”. “This” referring to (in this specific case) Reading Phone State. This doesn’t mean it’s doing anything bad, however users might be turned off if your calculator wants to read the phones state etc. This is just how Google has set up the installation of applications, so that a user is properly notified of what an application is given permission to do.
What are the kind of identifiers we can get with this special permission? We can grade the “Device ID” which is the IMEI number, the phone number, Software Version (not sure if it’s currently being used?), Sim Serial Number and Subscriber ID.
That should be enough unique identifiers for anyone to come up with some hash! Heck, phone number alone should be enough since it would be readily known by a customer and easy to use.
To get these values add the following somewhere in your program;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import android.telephony.*; ... TelephonyManager mTelephonyMgr = (TelephonyManager)getSystemService(TELEPHONY_SERVICE); String imei = mTelephonyMgr.getDeviceId(); // Requires READ_PHONE_STATE String phoneNumber=mTelephonyMgr.getLine1Number(); // Requires READ_PHONE_STATE String softwareVer = mTelephonyMgr.getDeviceSoftwareVersion(); // Requires READ_PHONE_STATE String simSerial = mTelephonyMgr.getSimSerialNumber(); // Requires READ_PHONE_STATE String subscriberId = mTelephonyMgr.getSubscriberId(); // Requires READ_PHONE_STATE |
Note that you MUST add permission access to android.permission.READ_PHONE_STATE otherwise your program will force close. This is added in the AndroidManifest.xml like the following;
|
1 |
<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission> |
On the emulator it will output things like the following;
DeviceId(IMEI) = 000000000000000
DeviceSoftwareVersion = null
Line1Number = 15555218135
SimSerialNumber = 89014103211118510720
SubscriberId(IMSI) = 310995000000000
One should also note that a real device currently returns “00″ for Device Software Version, so it’s possible that this is something reserved for future use. These could all easily be used in some type of registration algorithm that you want to tie to a certain device. Also if you choose your identifiers properly you could allow your registration code to be complaint across all versions of your product. Using a phone number for example could allow a user to use your application on any phone they put their SIM card into. If you want to prevent this you could tie it to both the device ID and the phone number.
Something that you always come across in writing algorithms for devices that you want to lock down is getting a grasp of the actual device it is intended to be on. Sometimes programmers want to lock a registration code not only to a name for registration, but also to a device. This can cut down on “sharing” of serial numbers etc. I was doing some research and looking for device specific information when I stumbled upon a few things. They are right out there in the open, but here they are just in case you have not seen them yet.
In Android.Provider.Settings.System we have some interesting values that could be of use, one specifically is “Android_ID”. From the documentation it is the following;
String ANDROID_ID The Android ID (a unique 64-bit value) as a hex string. “android_id”
Though while this is considered a “unique key”, please keep in mind that if a program has write access to the Settings, which is possible, this could be changed easily. Though it could be a safe assuming that it should not be changed, and upon normal program usage it wouldn’t be changed. Anyway, to retrieve this ID you just use the following snip-it of code;
|
1 2 3 |
import Android.Provider.Settings.System; ... String Android_ID = System.getString(this.getContentResolver(), System.ANDROID_ID); |
Also, note that in an emulator this will return “null”, though a real device will return an actual value. The nice thing about this tid-bit of code is that you are not required any special permissions to call it, since it’s essentially a passive call to get information. No write access is (obviously) required.
I’ve been working on the header a little more – so I figured I’d post some code I just finished throwing together quickly. It’s not all the code, since most of it is experimental and I’m not finished doing it, but this will provide people with the information on how to dump the dex file header information.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
/* File: DexNfo.java * * Coded: Timothy Strazzere * Date: 11/22/08 * * Dump header information from a dex file, only supports '035' dex files, though will * attempt to dump rest of the information, but will just warn you otherwise. * * Some code has been removed as it isn't sure if it full works properly yet. * */ import java.security.*; import java.util.zip.Adler32; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; /* To do... * * lots... * */ public class DexNfo{ public static void main(String[] args) { if (args.length == 1) { try { File file = new File(args[0]); byte[] barr, newbytes = null; newbytes = barr = getBytesFromFile(file); // add switch for this? System.out.printf("Original information: " + args[0]); int magic = 0; for(int i = 0; i<8; i++) magic+=barr[i]; if(magic!=483) // technically anything higher should be a new dex file... 'dex 036' etc.. System.out.printf("\n** Warning: Magic; bad dex file or unsupported version loaded!"); // Don't output char 4 since it's a newline char System.out.printf("\nMagic: %c%c%c %c%c%c", barr[0], barr[1], barr[2], /*barr[3],*/ barr[4], barr[5], barr[6], barr[7]); System.out.print("\nChecksum: "); for(int i = 8; i<12; i+=4) System.out.printf("0x%02X%02X%02X%02X ", barr[i+3], barr[i+2], barr[i+1], barr[i]); System.out.print("\nSignature: 0x"); for(int i = 12; i<32; i+=4) System.out.printf("%02X%02X%02X%02X ", barr[i], barr[i+1], barr[i+2], barr[i+3]); System.out.printf("\nLength: 0x%02X%02X", barr[33], barr[32]); if(barr[36]!= 112) // currently is always 0x70==(int)112... System.out.printf("\n** Warning: Header Length; bad dex file or unsupported version loaded!"); System.out.printf("\nHeader Length: 0x%02X", barr[36]); // endian tag int endian = 0; for(int i = 0; i < 4; i++) endian += barr[40+i]; if(endian != 276) // Currently always should be 0x78563412, which when added = int 114 System.out.printf("\n** Warning: Endian Tag; bad dex file or unsupported version loaded!"); System.out.printf("\nEndian Tag: 0x%02X%02X%02X%02X", barr[40], barr[41], barr[42], barr[43]); // map offset System.out.printf("\nMap Offset: 0x%02X%02X", barr[53], barr[52]); // string table size System.out.printf("\nString Table Size: 0x%02X%02X", barr[57], barr[56]); // string table offset System.out.printf("\nString Table Offset: 0x%02X%02X", barr[61], barr[60]); // type table size System.out.printf("\nType Table Size: 0x%02X%02X", barr[65], barr[64]); // type table offset System.out.printf("\nType Table Offset: 0x%02X%02X", barr[69], barr[68]); // Prototype table size System.out.printf("\nPrototype Table Size: 0x%02X%02X", barr[73], barr[72]); // Prototype table offset System.out.printf("\nPrototype Table Offset: 0x%02X%02X", barr[77], barr[76]); // Field table size System.out.printf("\nField Table Size: 0x%02X%02X", barr[81], barr[80]); // Field table offset System.out.printf("\nField Table Offset: 0x%02X%02X", barr[85], barr[84]); // Method table size System.out.printf("\nMethod Table Size: 0x%02X%02X", barr[89], barr[88]); // Method table offset System.out.printf("\nMethod Table Offset: 0x%02X%02X", barr[93], barr[92]); // Class table size System.out.printf("\nClass Table Size: 0x%02X%02X", barr[97], barr[96]); // Class table offset System.out.printf("\nClass Table Offset: 0x%02X%02X", barr[101], barr[100]); System.out.println(); // add switch for this too? calcSignature(newbytes); calcChecksum(newbytes); System.out.print("\n\nNew Checksum: "); for(int i = 8; i<12; i+=4) System.out.printf("0x%02X%02X%02X%02X ", newbytes[i+3], newbytes[i+2], newbytes[i+1], newbytes[i]); System.out.print("\nNew Signature: 0x"); for(int i = 12; i<32; i+=4) System.out.printf("%02X%02X%02X%02X ", newbytes[i], newbytes[i+1], newbytes[i+2], newbytes[i+3]); System.out.printf("\nLength: %04X", calcSize(newbytes)); // output compares to the two, highlight differences... } catch (Exception e) { System.err.println("File input error"); } } else System.out.println("Invalid parameters"); } public static byte[] getBytesFromFile(File file) throws IOException { InputStream is = new FileInputStream(file); // Get the size of the file long length = file.length(); if (length > Integer.MAX_VALUE) { // File is too large } // Create the byte array to hold the data byte[] bytes = new byte[(int)length]; // Read in the bytes int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) { offset += numRead; } // Ensure all the bytes have been read in if (offset < bytes.length) { throw new IOException("Could not completely read file "+file.getName()); } // Close the input stream and return bytes is.close(); return bytes; } private static void calcSignature(byte bytes[]) { MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); } catch(NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } md.update(bytes, 32, bytes.length - 32); try { int amt = md.digest(bytes, 12, 20); if(amt != 20) throw new RuntimeException((new StringBuilder()).append("unexpected digest write: ").append(amt).append(" bytes").toString()); } catch(DigestException ex) { throw new RuntimeException(ex); } } private static void calcChecksum(byte bytes[]) { Adler32 a32 = new Adler32(); a32.update(bytes, 12, bytes.length - 12); int sum = (int)a32.getValue(); bytes[8] = (byte)sum; bytes[9] = (byte)(sum >> 8); bytes[10] = (byte)(sum >> 16); bytes[11] = (byte)(sum >> 24); } public static int calcSize(byte bytes[]) { return(bytes.length); } } |
This now dumps all the header information from the original file, and will recalculate the signature and checksum in case something has changed. A version should be available shortly to check for differences in all the values, hopefully soon being able to calculate the correct values if something is wrong.
Maybe this will be useful for someone? Otherwise, oh well it’s just here in case I delete my files. Working on functions to find the new values after patching and to allow patching/injection of code. I’ll have to write up more later as I don’t have an overwhelming amount of time right now, busy day and I’m exhausted. Saw Sara play some volleyball, finished up solo campaign in COD5, spent a few hours reading and researching some dex related things and trying to get some more injection to work. Tomorrow I probably won’t have time to post – but trust me, this stuff will be up sooner or later. It’s a big puzzle I’m chipping away at, and it’s bugging the heck out of me not having the answers.
DEX files, which are the compiled bytecode files that run on the Android OS. Essentially they are like the java bytecode, except they use a modified VM which is called “Dalvik” that was developed for the Android OS.
So long story short – I want to know the structure of these files, and how to edit or patch them. Why? I enjoy reverse engineering things! Anyway there is a wealth of information that I could find on the following sites;
Shane Isbell’s Weblog : http://www.jroller.com/random7/entry/android_s_dex_class_structure
RetroCode, Dex File Formate : http://www.retrodev.com/android/dexformat.html
These site both have great information, thoughmore so on the second link. What intrigued me was that they must have a ‘checksum’ and a ‘SHA-1′ signature. The best information I could find though only eluded to this (Retrocode);
[quote]Notes: All non-string fields are stored in little-endian format. It would appear that the checksum and signature fields are assumed to be zero when calculating the checksum and signature.[/quote]
Well that doesn’t really help me if I’d like to patch a DEX file and then redo the signature and checksum. Especially since we don’t know what exactly is being used to calculate either or what checksum is being used to well, calculate the checksum!
Good thing we can extract .jar files and decompile class files, even more so thank goodness google hasn’t used obstufication on any of the classes. An except from “dx\com\android\dx\dex\file\DexFile.class” after being decompiled into DexFile.jar.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
private static void calcSignature(byte bytes[]) { MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); } catch(NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } md.update(bytes, 32, bytes.length - 32); try { int amt = md.digest(bytes, 12, 20); if(amt != 20) throw new RuntimeException((new StringBuilder()).append("unexpected digest write:").append(amt).append("bytes").toString()); } catch(DigestException ex) { throw new RuntimeException(ex); } } private static void calcChecksum(byte bytes[]) { Adler32 a32 = new Adler32(); a32.update(bytes, 12, bytes.length - 12); int sum = (int)a32.getValue(); bytes[8] = (byte)sum; bytes[9] = (byte)(sum >> 8); bytes[10] = (byte)(sum >> 16); bytes[11] = (byte)(sum >> 24); } |
Ah hah! Now we know how to calculate the signature and the checksum. Essentially is reads in all the bytes of the program, and it disreguards the first 32 bytes and calculates the signature using SHA-1. Then it calculates the checksum disreguarding the first 12 bytes (so it includes the signature). Excellent, lets drop this code into our own program so we can recalculate those values;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
/* * ReDEX.class * Coded: Timothy Strazzere * * 10/29/2008 * * Pass a .dex file as an argument and it will output the current * checksum and signature that is in the file, then it will output * the checksum and signature as it calculates them. * */ import java.security.*; import java.util.zip.Adler32; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class ReDEX { public static void main(String[] args) { if (args.length == 1) { try { File file = new File(args[0]); byte[] barr = null; barr = getBytesFromFile(file); System.out.print(:Original Checksum: "); for(int i = 8; i<12; i+=4) System.out.printf("0x%02X%02X%02X%02X ", barr[i+3], barr[i+2], barr[i+1], barr[i]); System.out.print("\nOriginal Signature: 0x"); for(int i = 12; i<32; i+=4) System.out.printf"%02X%02X%02X%02X ", barr[i], barr[i+1], barr[i+2], barr[i+3]); calcSignature(barr); calcChecksum(barr); System.out.print("\n\nNew Checksum: "); for(int i = 8; i<12; i+=4) System.out.printf("0x%02X%02X%02X%02X ", barr[i+3], barr[i+2], barr[i+1], barr[i]); System.out.print("\nNew Signature: 0x"); for(int i = 12; i<32; i+=4) System.out.printf("%02X%02X%02X%02X ", barr[i], barr[i+1], barr[i+2], barr[i+3]); } catch (Exception e) { System.err.println("File input error"); } } else System.out.println("Invalid parameters"); } public static byte[] getBytesFromFile(File file) throws IOException { InputStream is = new FileInputStream(file); // Get the size of the file long length = file.length(); if (length > Integer.MAX_VALUE) { // File is too large } // Create the byte array to hold the data byte[] bytes = new byte[(int)length]; // Read in the bytes int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) { offset += numRead; } // Ensure all the bytes have been read in if (offset < bytes.length) { throw new IOException("Could not completely read file "+file.getName()); } // Close the input stream and return bytes is.close(); return bytes; } private static void calcSignature(byte bytes[]) { MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); } catch(NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } md.update(bytes, 32, bytes.length - 32); try { int amt = md.digest(bytes, 12, 20); if(amt != 20) throw new RuntimeException((new StringBuilder()).append("unexpected digest write:").append(amt).append("bytes").toString()); } catch(DigestException ex) { throw new RuntimeException(ex); } } private static void calcChecksum(byte bytes[]) { Adler32 a32 = new Adler32(); a32.update(bytes, 12, bytes.length - 12); int sum = (int)a32.getValue(); bytes[8] = (byte)sum; bytes[9] = (byte)(sum >> 8); bytes[10] = (byte)(sum >> 16); bytes[11] = (byte)(sum >> 24); } |
Excellent, now I can easily recalculate the signature and the checksum of the dex files. Note that the checksum is actually in little-endian so you need to reverse it when entering it in a hex editor.
So, now the Android OS will allow you to install this dex file, after signing it in the correct package (apk). Though as of right now the program still crashes upon launching the file… Hhhmmmmm we’ll have to look into this more I guess.


Recent Comments