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

No comments:

Post a Comment