As people have seen in the past, I tend to have a fun time finding edge-cases which break tools. Often you can find these types of edge-cases while reading documentation and cross referencing the implementation of that in the systems your validating. A pretty good example of this is highlighted in my BlackHat 2012 talk, where I was looking at the header section, which is described as always have the value of 0×70. When looking at the open source tools, some checked to make sure this was true – others ignored it. The actual code in the Dex Verifier is as follows;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (okay) { state.pHeader = pHeader; if (pHeader->headerSize < sizeof(DexHeader)) { ALOGE("ERROR: Small header size %d, struct %d", pHeader->headerSize, (int) sizeof(DexHeader)); okay = false; } else if (pHeader->headerSize > sizeof(DexHeader)) { ALOGW("WARNING: Large header size %d, struct %d", pHeader->headerSize, (int) sizeof(DexHeader)); // keep going? } } |
From this we can see the actual implementation doesn’t care what the size of, as long as it is larger than the current structure size, which is 0×70. This allows for the verifier to be forward compatible, though if anyone was creating a tool and only read the documentation – this might not be fully understood or assumed.
This leads me to two extremely easy breakages which I never mentioned in my talk, but noticed IDA Pro 6.4 and Radare would fail against. The issue that IDA Pro and Radare broke against, was a bad file magic. According to the documentation the magic bytes are the following;
DEX_FILE_MAGIC
embedded in header_itemThe constant array/string DEX_FILE_MAGIC is the list of bytes that must appear at the beginning of a .dex file in order for it to be recognized as such. The value intentionally contains a newline (“\n” or 0x0a) and a null byte (“\0″ or 0×00) in order to help in the detection of certain forms of corruption. The value also encodes a format version number as three decimal digits, which is expected to increase monotonically over time as the format evolves.ubyte[8] DEX_FILE_MAGIC = { 0×64 0×65 0×78 0x0a 0×30 0×33 0×35 0×00 }
= “dex\n035\0″Note: At least a couple earlier versions of the format have been used in widely-available public software releases. For example, version 009 was used for the M3 releases of the Android platform (November–December 2007), and version 013 was used for the M5 releases of the Android platform (February–March 2008). In several respects, these earlier versions of the format differ significantly from the version described in this document.
So one might assume that the currently accepted magic bytes will be exactly “dex\n035\00″ – though, they would be wrong in assuming this. If we take a look at the code in DexFile.h;
|
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 |
/* DEX file magic number */ #define DEX_MAGIC "dex\n" /* current version, encoded in 4 bytes of ASCII */ #define DEX_MAGIC_VERS "036\0" /* * older but still-recognized version (corresponding to Android API * levels 13 and earlier */ #define DEX_MAGIC_VERS_API_13 "035\0" ... /* (documented in header file) */ bool dexHasValidMagic(const DexHeader* pHeader) { const u1* magic = pHeader->magic; const u1* version = &magic[4]; if (memcmp(magic, DEX_MAGIC, 4) != 0) { ALOGE("ERROR: unrecognized magic number (%02x %02x %02x %02x)", magic[0], magic[1], magic[2], magic[3]); return false; } if ((memcmp(version, DEX_MAGIC_VERS, 4) != 0) && (memcmp(version, DEX_MAGIC_VERS_API_13, 4) != 0)) { /* * Magic was correct, but this is an unsupported older or * newer format variant. */ ALOGE("ERROR: unsupported dex version (%02x %02x %02x %02x)", version[0], version[1], version[2], version[3]); return false; } return true; } |
We can see that there are constant magic bytes of “dex\n”, but the versioning afterwards – which is loosely explained in the documentation, has multiple options. Since API level 14 on, the verifier has accepted both “036\00″ and “035\00″ as valid versioning parts of the magic bytes. Since the magic bytes are not part of the checksum or the signature of the dex file, one can simply bump the version number without any specialized tools, just doing it with a hex editor would be fine. This lead to Radare failing to load the file and IDA Pro to thinking the file was corrupt with the following dialog and log output;
|
1 2 3 |
Loading file '/Users/tstrazzere/reverse/targets/ida/classes-test.dex' into database... Detected file format: Android DEX file version 54.0 bad dex version (0x30 33 36 00) |
I originally reported this issue to January 22nd, 2013 and received a thank you and a fix back from them only two days later on the 24th. I’m unsure if they sent this out to all their customers or have it totally bundled into their latest packages, but you should easily be able to request it if not. For Radare I submitted a patch for this issue which was quickly merge upstream by the extremely proactive author of the tool.
The second breakage, which only directly effected IDA Pro, was revolving around the file size as dictated by the dex_header vs the actual file size. IDA Pro was comparing the two, and if they where not actually equal – assumes the file is corrupt. The documentation states, “size of the entire file (including the header), in bytes”, though the implementation of the code doesn’t actually care – as seen from the DexSwapVerify.cpp file;
|
1 2 3 4 5 6 7 8 9 10 11 |
if (okay) { int expectedLen = (int) SWAP4(pHeader->fileSize); if (len < expectedLen) { ALOGE("ERROR: Bad length: expected %d, got %d", expectedLen, len); okay = false; } else if (len != expectedLen) { ALOGW("WARNING: Odd length: expected %d, got %d", expectedLen, len); // keep going } } |
As we can see from above, if the actual length must be at least as large as the expected length, most likely to avoid any truncated files. Though it can easily be larger, which will just produce a warning – though processing of the dex file will continue. However, the same corrupt file dialog with this logging message comes up when loaded in IDA Pro;
|
1 2 3 |
Loading file '/Users/tstrazzere/reverse/targets/ida/classes-test.dex' into database... Detected file format: Android DEX file version 53.0 ERROR: stored file size (438844) != expected (438845) |
This was also fixed on the same timeline as the other issue I reported to Hex-Rays, so if you run across any files like this you will be prompted with this dialog;
Just two small little issues that came about when looking at the implementation of the file format. These edge-cases always seem to exist in ever system, especially when creating reversing/disassembling/analyzing tools.
During the research phase of my Blackhat talk, I was digging into detecting the default layout of a dexfile, as generated by the normal dx tool. Originally, my concept was that I wanted my tool to “stack” things inside the file the same way that the dalvik compiler would, though I couldn’t find any actual resources on what this actually looked like. After a few hours of digging through code on AOSP and tearing apart an actual dex file to look at the innards, I came up with the quick little ASCII diagram below;
|
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 |
+--------------------------------------------------------------------+ | Dex header | * offsets and sizes of all sections | - default size 0x70 +--------------------------------------------------------------------+ | String_id_list | * offsets into data | - size: number of strings * 4 +--------------------------------------------------------------------+ | Type_id_list | * index into string_id_list | - size: number of types * 4 +--------------------------------------------------------------------+ | Proto_id_list | * index into string_id_list | * index into type_id_list | * offsets into data section (params) | - size: number of protos * 12 +--------------------------------------------------------------------+ | Field_id_list | * 2 indexes into type_id_list | * index into string_id_list | - size: number of fields * 8 +--------------------------------------------------------------------+ | Method_id_list | * index into Type_id_list | * index into Proto_id_list | * index into String_id_list | - size: number of methods * 8 +--------------------------------------------------------------------+ | Class_def_items | * 2 indexes into Type_id_list | * offsets into data for interfaces | * indexes into Type_id_list | * index into string_id_list for source file | * offsets into data for annotation | * offsets into data for annotation_set | * offsets into class data for annotation item | * offsets into data for class_data_items | * index into method_id | * offsets into data for static_values | * offsets into data for code_item | * offsets into data for debug_item | - size: number of classes * 20 +--------------------------------------------------------------------+ | Data section (default layout) | * annotation items | * code items | * annotation_directory | * interfaces | * parameters - used by proto section | * strings | * debug items | * annotation_sets | * static values | * class_data | * map list +--------------------------------------------------------------------+ |
The result of the APKfuscator actually ended up being quiet different than the above mappings. It’s definitely possibly to retain the structure, however the sections can easily be interchanged. The resulting sections from my tool look like the following;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Above sections are identical as to layout, but could be shifted around if need be ... +--------------------------------------------------------------------+ | Data section (default layout) | * strings | * parameters (proto section) | * interfaces | * annotation items (visibility of item (flags), | annotation type, number of name, | encoded annotation) | * class annotations (size of items, offsets to items) | * annotation data (offset to class annotations, | fields size, methods size, | parameters size) | * code items | * class data | * static values | * debug items (currently stripped) | * map list +--------------------------------------------------------------------+ |
The patterns for the normal dx compiler appear to always lay out the same, so if someone has developed a post-compilation modification tool (i.e. – APKfuscator or (bak)smali), it might be possible to see that a dex file has been “changed”. If someone was to develop a tool to look for patterns about how this data is laid out, it could lead to some interesting results. Being able to detect these changes and patterns, run on a large enough scale, could be an interesting tactic to finding out whether or not someone has messed with a file quickly. Hopefully I’ll have more time to research this area and either prove or disprove this theory. Though, until then – hopefully the small ASCII layouts might help someone else with whatever work they’re doing on dalvik research.
It’s been almost a full week since my talk, Dex Education: Practicing Safe Dex, though I think I’m only now beginning to recover. The past few months have truly been a whirlwind of both working on dissecting malware at Lookout and working on putting together a solid presentation for BlackHat. So far I’ve been unable to draw a crowd like Charlie, though maybe someday I’ll have people sitting in the aisles fighting for a seat during a presentation. Until then the people who went will just have to deal with the extra legroom. Over all the presentation seemed to go over pretty well, some interesting chats afterwards with some smart people. A few people where interested in the slides and proof of concept code, so I told them I would tweet it and also make a blog post about it.
My slides are available here with the proof of concept code being hosted on my github page here. The proof of concept crackme code on the same github page as well shortly.
I’ve got some extra content that I wasn’t able to fit into the slide-deck, heck it was 96 slides as is after trimming some things out. While I didn’t intend to try and cover everything possible to break most analysis tools, I wanted to attempt to cover as much as possible. Over the course of a few days or weeks, I’ll try to roll out details in my blog about how certain things worked, mainly for people who where unable to attend the presentation, hear my explanations or ask me things at the conference. Feel free to reach out to me if there is anything I’ve missed or you would live a better explanation about.
A few people asked me about Blackhat and Defcon – wondering if it’s worth attending. So to step on a soap box just for a minute, I’ll give the mini speech that I normally tell people. Conferences are only worth what you put into them, go to talks that seem interesting and are outside of your direct field of work. Why attend talks outside the direct field of work? I’ve found it’s a great way to try and find different perspectives, which often can be related back into your own work and field. It is also quiet hard to appreciate a talk on something that you deal with daily, definitely very important to try and keep this in mind if you do see those types of talks. As a presenter myself, I found it exceptionally hard to not go too low level while still feeling like I can add value to everyone in the audience. After attending the talks you chose, meet the presenters and pick their brains, this is honestly where you can learn the most. As I have said, it’s really hard to make a presentation accessible for a whole audience, talking directly with these people will give you so much more information than the slides often do. The people you meet at the bars (for Blackhat @ Caesars goto the Galleria bar) are often people you talk to online already. Make friends, go outside that comfort zone and buy some people drinks. Most everyone is friendly, if they aren’t – don’t drink with them. Almost all conferences are worth going to, Blackhat and Defcon included, mainly due to the talent it attacks that you can find hanging out at the bars.
Probably the greatest thing about Blackhat for me was to meet some really great people I’ve only had the pleasure of talking to online. Talking with Mila, the mind behind Contagio Dump, was really great – able to pay her back a little for all the hard work she does with a beer or two. Got to talk with some of the original DroidSecurity (now AVG) guys, Elad and Oren, it’s never a dull moment talking to an Israeli reverse engineer – just look at Zuk. Another interesting person who I got to hang out with was along side me in the malware talk track, @snare. He did some crazy things with EFI rootkits for OSX, pretty scary and interesting stuff all in the same talk.
People often say it isn’t what you know, but who you know. I’d argue the security space is a ying and yang of both; to be a valuable (reverser) engineer you need to know your stuff and the people to help you succeed.
Enough on this soapbox, hopefully you enjoy the slides and code. If you ever run into me at a conference – let’s have a beer or two and chat.
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.
It’s coming along, but it doesn’t seem to be as easy as I’d have hoped. Sort of have a working example but I don’t want to release it until I can definitely identify what needs to be patched and why and other things like exactly by how much etc for things to be injected. Just a little output of some of my notes from the tests I’ve been running. Nothing to mind blowing but some notes incase someone is interested, slash incase I lose the piece of paper;
Things you must patch to successively inject code:
Length of file in bytes (0×20)
Absolute offet of string table (0×34)
type of checksum? (0×38)
number of fields in field table (0×44)
Absolute offet to field table (0×48)
number of methods in method table (0x4C)
absolute offset of method table (0×50)
another checksum? (0×54)
absolute offset of class definition? (0×58)
Success! It seems completely possible, though quiet a pain to inject new code into existing dex files. This doesn’t not appear like it would easily be done ON a device, though in the development setting it seems perfectly possible and completely do-able.
I’m working on a nice proof-of-concept example to show, though I don’t think this is a “backdoor” to malware. Android has been set up well enough that to properly inject things it would require many things to be done, making it in my opinion extremely hard to do it on the fly on the device. I had to inject the code directly to the dex, resigned both the signature and hash makings for the file, then resign the whole package before reinstalling (after a complete uninstall since we don’t have the same keys as the original package) onto the device. This is a long way away from actually being able to do nasty things with it, which is clearly a good thing, since we don’t want that to happen. This does have practical uses of course, though it seems Google has done security rather well so that this process would most likely only be done by an actual developer for a user to not notice an injected file… Otherwise they would have to allow unknown sources, packages would complain about key, so on and so on…
Hopefully more to come on this subject soon!
Been doing some experimentation with some extremely interesting results. Looks like inlining a program is possible, though it does get a little messy… I’ve been doing mostly everything by hand and guessing – but it looks like I might be able to write up a program to do it for me. I don’t have a whole lot of time right now as I’m time crunched with some exams, so I can’t do into explicit detail, though if you understand the DEX file system and the Android OS it’s rather similar to injecting in to normal java vm’s. This process is well described here.
Hopefully I’ll have some time later to post to tests and results of what I’ve been doing and how it’s being done.
A great wealth of information I just stumbled upon given by Dan Bornstein, one of the creators of the Dalvik VM. It’s a long watch – but it definitely helps explain some things that are better than anything I could do;
The slides for further review and handouts can also be found here;
http://sites.google.com/site/io/dalvik-vm-internals
Coming up soon I will have the patched dex file and the tutorial to go along with it!
It had been bugging me tons since the application kept crashing. I knew the signature and checksum where correct since it wasn’t barfing on installation of the .apk file. So I kept thinking and thinking, finally I decided to do something useful… Look at the traces log! Here we can clearly see that an exception is being thrown… But why?
|
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 |
----- pid 210 at 2008-10-30 21:15:29 ----- Cmd line: com.android.KeyGenMe DALVIK THREADS: "main" prio=5 tid=3 NATIVE | group="main" sCount=1 dsCount=0 s=0 obj=0x400113a8 | sysTid=210 nice=0 sched=0/0 handle=-1095390052 at android.os.BinderProxy.transact(Native Method) at android.app.ActivityManagerProxy.handleApplicationError(ActivityManagerNative.java:2023) at com.android.internal.os.RuntimeInit.crash(RuntimeInit.java:302) at com.android.internal.os.RuntimeInit$UncaughtHandler.uncaughtException(RuntimeInit.java:75) at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:853) at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:850) at dalvik.system.NativeStart.main(Native Method) "Binder Thread #2" prio=5 tid=13 NATIVE | group="main" sCount=1 dsCount=0 s=0 obj=0x43360018 | sysTid=215 nice=0 sched=0/0 handle=972800 at dalvik.system.NativeStart.run(Native Method) "Binder Thread #1" prio=5 tid=11 NATIVE | group="main" sCount=1 dsCount=0 s=0 obj=0x4335efb8 | sysTid=214 nice=0 sched=0/0 handle=972616 at dalvik.system.NativeStart.run(Native Method) "JDWP" daemon prio=5 tid=9 VMWAIT | group="system" sCount=1 dsCount=0 s=0 obj=0x4335e2a0 | sysTid=213 nice=0 sched=0/0 handle=799384 at dalvik.system.NativeStart.run(Native Method) "Signal Catcher" daemon prio=5 tid=7 RUNNABLE | group="system" sCount=0 dsCount=0 s=0 obj=0x4335e1e8 | sysTid=212 nice=0 sched=0/0 handle=796600 at dalvik.system.NativeStart.run(Native Method) "HeapWorker" daemon prio=5 tid=5 VMWAIT | group="system" sCount=1 dsCount=0 s=0 obj=0x42533028 | sysTid=211 nice=0 sched=0/0 handle=793976 at dalvik.system.NativeStart.run(Native Method) ----- end 210 ----- |
I decided to do yet another smart thing, that I should’ve done – and redumped the dex file and see if it was making any sense… Of course! I edited the wrong opcode. Apparently in my overwhelming dumbness I tried changing the registers and a exception thrown for the statement. This is something the Dalvik-VM did not agree with, thus the barfing.
I’m going to recreate a nice little example with source code of a simple patch performed on a dex file, and I’ll outline the process used to do so. Hopefully I’ll have this posted sometime tomorrow!
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