Wednesday, June 8, 2011

anti-tampering with crc check

one way an app will try to detect if it has been tampered with is to look at classes.dex inside the apk. just so you know, java code is compiled to java .class files, which is then transformed by dx into classes.dex. this one file contains all the compiled code of an app. once the code is finished and the app is ready to be published, properties of the file such as the size or crc (cyclic redundancy check) can be determined and then stored inside the resources of the app.

when the app runs, it can compare the stored values with the actual values of the classes.dex file. if they do not match, then the code was likely tampered with.

note that we're using zipentry here, but we could also use jarentry and jarfile. you can't simply look for getCrc() and feel safe either, because the method could be called with reflection.

here's what a crc check may look like in java:
private void crcTest() throws IOException {
 boolean modified = false;

 // required dex crc value stored as a text string.
 // it could be any invisible layout element
 long dexCrc = Long.parseLong(Main.MyContext.getString(R.string.dex_crc));

 ZipFile zf = new ZipFile(Main.MyContext.getPackageCodePath());
 ZipEntry ze = zf.getEntry("classes.dex");

 if ( ze.getCrc() != dexCrc ) {
  // dex has been modified
  modified = true;
 }
 else {
  // dex not tampered with
  modified = false;
 }
}

and here's the above code translated into smali:
.method private crcTest()V
    .locals 7
    .annotation system Ldalvik/annotation/Throws;
        value = {
            Ljava/io/IOException;
        }
    .end annotation

    .prologue
    .line 599
    const/4 v2, 0x0

    .line 602
    # modified will be set to true if classes.dex crc is not what it should be
    .local v2, modified:Z
    sget-object v5, Lcom/lohan/testtarget/Main;->MyContext:Landroid/content/Context;

    # get the crc value from string resources
    const v6, 0x7f040002
    invoke-virtual {v5, v6}, Landroid/content/Context;->getString(I)Ljava/lang/String;
    move-result-object v5

    # convert it to a long since ZipEntry.getCrc gives us long
    invoke-static {v5}, Ljava/lang/Long;->parseLong(Ljava/lang/String;)J
    move-result-wide v0

    .line 604
    .local v0, dexCrc:J
    new-instance v4, Ljava/util/zip/ZipFile;

    sget-object v5, Lcom/lohan/testtarget/Main;->MyContext:Landroid/content/Context;

    # get the path to the apk on the system
    invoke-virtual {v5}, Landroid/content/Context;->getPackageCodePath()Ljava/lang/String;
    move-result-object v5

    invoke-direct {v4, v5}, Ljava/util/zip/ZipFile;->(Ljava/lang/String;)V

    .line 605
    .local v4, zf:Ljava/util/zip/ZipFile;
    # get classes.dex entry from our apk
    const-string v5, "classes.dex"
    invoke-virtual {v4, v5}, Ljava/util/zip/ZipFile;->getEntry(Ljava/lang/String;)Ljava/util/zip/ZipEntry;
    move-result-object v3

    .line 607
    .local v3, ze:Ljava/util/zip/ZipEntry;
    # you could crack here by providing v5 with the correct
    # long value. this may be easier if later logic is convoluted
    # or if the result is stored in a class variable and acted
    # on later. you can write your own java program to get the
    # correct value. 
    invoke-virtual {v3}, Ljava/util/zip/ZipEntry;->getCrc()J
    move-result-wide v5

    # compare v5 (actual crc) with v0 (stored crc)
    cmp-long v5, v5, v0

    # if v5 is 0, meaning cmp-long reports values are NOT the same
    # goto :cond_0. this is where this could be cracked.
    # could simply remove this line, in this case.
    if-eqz v5, :cond_0

    .line 609
    # otherwise store true in v2.
    # normally there will be code to act on the value of v2.
    const/4 v2, 0x1

    .line 615
    :goto_0
    return-void

    .line 613
    :cond_0
    # store false in v2.
    const/4 v2, 0x0

    goto :goto_0
.end method

11 comments :

  1. if v5 is 0, meaning cmp-long reports values are NOT the same... jumping to cond_0 where store false in v2... shouldn't be true?

    ReplyDelete
  2. @Anonymous, should not what be true? could mean 10 different things and it gives me a headache trying to figure it out. is the comment wrong? should v2 have 0x1? what is your goal? are you trying to crack it? if yes, then yes, v2 should have 0x1 and this can be accomplished by what i suggest in the comment but that makes no sense why you would ask that because the comment already says how to handle that. or are you saying the comment is incorrect? no, the comment describing the behavior of the cmp is correct.

    ReplyDelete
  3. How can one use this technique along with ProGuard? Every time I build an APK it is different, so I can't compile it with the certificate as a static constant. In theory, a good technique would be to have a database of version codes and checksums then download the corresponding one as a check, but that requires network and seems unnecessarily complicated.

    ReplyDelete
  4. Anonymous, this does not check the apk, it checks the classes.dex, which is only the code. the crc32 is in the string resources, not classes.dex itself. to use with proguard? look for a way to inject intermediate steps, because proguard does not need to touch the resources. there's probably a nice way to modify a .bat file or something.

    you could probably even just build your apk with proguard, get the classes.dex crc32, modify the string resources normally with the updated crc32, rebuild to new apk and copy the resources from the new apk to the old.

    ReplyDelete
  5. What if reflection with string encryption was used to call getCrc() in java code? Smali code wouldn't be so clear then, right?

    ReplyDelete
  6. android-hacker, good question. it depends on what you mean. interpreting normally, i'd say that the decrypted string must necessarily be given to Method->invoke() because the Java API wont know how to decrypt the parameters. the way to defeat it, as with reflection in general, is to hook every reflected method invocation and output what the method is and parameters are. depending on what it is, you can act appropriately by calling your own functions or letting the method call pass through.

    if this didn't answer your question, post some code to pastebin. you may answer it yourself as you write it, but i'll take a look.

    ReplyDelete
  7. lohan+, I get it, thanks for the answer.

    ReplyDelete
  8. Lohan+, This is an amazing tutorial and helps alot but I was wondering if you could help me further with some java code for android games? It would be much appreciated. you can add me on Skype @ DivineGamr

    ReplyDelete
  9. thanks DivineGamr. i'm glad you got some use out of it. i'm not interested in writing android games.

    ReplyDelete
  10. so easy just modify the condition to 0x1 the result will always be true whether crc matches or not thats i have done in many apps
    regards :)
    jasi2169

    ReplyDelete
  11. This is THE tutorial where i'm looking for.
    I'm not understanding it completly yet, but this must be possbile to apply a game which I'm trying to do this to. Not sure how, but I hope I'll succeed. Thankyou dude!

    ReplyDelete

Do NOT post about or link to specific apps!