UnCrackable-Level1
下载地址: https://mas.owasp.org/crackmes/
国际惯例,JEB打开APK,找到main。看到onCreate里有两个提示,应该是检测了ROOT和调试的环境,可以使用frida来修改返回,或者执行修改APK判断,这里直接修改判断,为了少写代码。
直接把验证部分删了:
.method protected onCreate(Bundle)V
.registers 3
00000000 invoke-static c->a()Z
00000006 move-result v0
00000008 if-nez v0, :24
:C
0000000C invoke-static c->b()Z
00000012 move-result v0
00000014 if-nez v0, :24
:18
00000018 invoke-static c->c()Z
0000001E move-result v0
00000020 if-eqz v0, :2E
:24
00000024 const-string v0, "Root detected!"
00000028 invoke-direct MainActivity->a(String)V, p0, v0
:2E
0000002E invoke-virtual MainActivity->getApplicationContext()Context, p0
00000034 move-result-object v0
00000036 invoke-static b->a(Context)Z, v0
0000003C move-result v0
0000003E if-eqz v0, :4C
:42
00000042 const-string v0, "App is debuggable!"
00000046 invoke-direct MainActivity->a(String)V, p0, v0
:4C
0000004C invoke-super Activity->onCreate(Bundle)V, p0, p1
00000052 const/high16 p1, 0x7F030000 # layout:activity_main
00000056 invoke-virtual MainActivity->setContentView(I)V, p0, p1
0000005C return-void
.end method
修改为
.method protected onCreate(Bundle)V
.registers 3
0000004C invoke-super Activity->onCreate(Bundle)V, p0, p1
00000052 const/high16 p1, 0x7F030000 # layout:activity_main
00000056 invoke-virtual MainActivity->setContentView(I)V, p0, p1
0000005C return-void
.end method
编译,签名安装即可。
整个验证的逻辑在verify内:
public void verify(View arg4) {
String v4_1;
String v4 = ((EditText)this.findViewById(0x7F020001)).getText().toString(); // id:edit_text
AlertDialog v0 = new AlertDialog.Builder(this).create();
if(a.a(v4)) {
v0.setTitle("Success!");
v4_1 = "This is the correct secret.";
}
else {
v0.setTitle("Nope...");
v4_1 = "That\'s not it. Try again.";
}
v0.setMessage(v4_1);
v0.setButton(-3, "OK", new DialogInterface.OnClickListener() {
@Override // android.content.DialogInterface$OnClickListener
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0.show();
}
其中a函数,因此加密密钥和加密内容就已知。
public static boolean a(String arg5) {
byte[] v1 = Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0);
byte[] v2 = new byte[0];
try {
return arg5.equals(new String(sg.vantagepoint.a.a.a(new byte[]{(byte)0x8D, 18, 0x76, (byte)0x84, -53, -61, 0x7C, 23, 97, 109, (byte)0x80, 108, -11, 4, 0x73, -52}, v1)));
}
catch(Exception v0) {
Log.d("CodeCheck", "AES error:" + v0.getMessage());
return arg5.equals(new String(v2));
}
}
然而这里Cipher.init中的是2,也就是解密,我们需要知道解密后的内容。hook
sg.vantagepoint.a.a.a
随便输入一段内容,获取到输出为
ZenTracer:::{"cmd":"exit","data":["1","73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101"]}
转换为字符串就是:
I want to believe
当然如果你直接分析加密代码,然后代码还原出来那就是:
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class owasp {
public static void main(String[] args) throws Exception {
System.out.println(a());
}
public static String a() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
byte[] arg2 = new byte[]{(byte)0x8D, 18, 0x76, (byte)0x84, -53, -61, 0x7C, 23, 97, 109, (byte)0x80, 108, -11, 4, 0x73, -52};
byte[] arg3 = Base64.getDecoder().decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=");
SecretKeySpec v0 = new SecretKeySpec(arg2, "AES");
Cipher v2 = Cipher.getInstance("AES/ECB/PKCS5Padding");
v2.init(Cipher.DECRYPT_MODE, v0);
return new String(v2.doFinal(arg3));
}
}
UnCrackable-Level2
看到这个大概就知道这货想干啥了
static {
System.loadLibrary("foo");
}
先按照流程来看一下,跟上面差不多,几个检测,不过这里先加载so的init函数。
然后加密的方法被写到了so的
public class CodeCheck {
public boolean a(String arg1) {
return this.bar(arg1.getBytes());
}
private native boolean bar(byte[] arg1) {
}
}
然后去so中找一下这两个函数,其中init中关键函数是sub_93C
.text:00000BA4 PUSH {R7,LR}
.text:00000BA6 MOV R7, SP
.text:00000BA8 BL sub_93C
.text:00000BAC LDR R0, =(byte_400C - 0xBB4)
.text:00000BAE MOVS R1, #1
.text:00000BB0 ADD R0, PC ; byte_400C
.text:00000BB2 STRB R1, [R0]
.text:00000BB4 POP {R7,PC}
sub_93C的伪代码是,是一个验证app调试行为的检测。
int sub_93C()
{
__pid_t v0; // r4
pthread_t newthread; // [sp+4h] [bp-1Ch] BYREF
int stat_loc[6]; // [sp+8h] [bp-18h] BYREF
dword_4008 = fork();
if ( dword_4008 )
{
pthread_create(&newthread, 0, sub_914, 0);
}
else
{
v0 = getppid();
if ( !ptrace(PTRACE_ATTACH, v0, 0, 0) )
{
waitpid(v0, stat_loc, 0);
while ( 1 )
{
ptrace(PTRACE_CONT, v0, 0, 0);
if ( !waitpid(v0, stat_loc, 0) )
break;
if ( (stat_loc[0] & 0x7F) != 127 )
exit(0);
}
}
}
return _stack_chk_guard - stat_loc[1];
}
另一个bar函数
bool __fastcall Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(_JNIEnv *a1, _JavaVM *a2, int a3)
{
const char *v5; // r8
_BOOL4 result; // r0
char s2[24]; // [sp+4h] [bp-2Ch] BYREF
result = 0;
if ( byte_400C == 1 )
{
strcpy(s2, "Thanks for all the fish");
v5 = a1->functions->GetByteArrayElements(a1, a3, 0);
if ( a1->functions->GetArrayLength(a1, a3) == 23 && !strncmp(v5, s2, 0x17u) )
result = 1;
}
return result;
}
因为我们需要把result返回1,也就是让后续的判断为真,所以需要查看内部流程。
有两个要求,其中是字节数组长度为23,跟上面的字符比较必须相等,这里有个小问题,byte_400C是init里来加载赋值的,也就是修改代码的时候不能去掉这个函数的渲染。
.method protected onCreate(Landroid/os/Bundle;)V
.locals 4
invoke-direct {p0}, Lsg/vantagepoint/uncrackable2/MainActivity;->init()V
new-instance v0, Lsg/vantagepoint/uncrackable2/CodeCheck;
invoke-direct {v0}, Lsg/vantagepoint/uncrackable2/CodeCheck;-><init>()V
iput-object v0, p0, Lsg/vantagepoint/uncrackable2/MainActivity;->m:Lsg/vantagepoint/uncrackable2/CodeCheck;
invoke-super {p0, p1}, Landroid/support/v7/app/c;->onCreate(Landroid/os/Bundle;)V
const p1, 0x7f09001b
invoke-virtual {p0, p1}, Lsg/vantagepoint/uncrackable2/MainActivity;->setContentView(I)V
return-void
.end method
编译安装,输入上面的字符串即可
Thanks for all the fish
UnCrackable-Level3
形似如上,但是多了一个文件的校验verifyLibs,这个返回不正常的时候会给tampered一个非0的值,导致后续的判断中失败。
但是这个app有一个麻烦的地方在于,他的检测跟上面的不一样,首先是Java层,删除MainActivity$2,还有MainActivity中的调用部分即可,但是安装后还是会闪退,这个现象明显不是Java层代码控制的。
从函数中可以看到一个goodbye函数,在sub_23C4中发现有调用,但是没有明显调用存在这个函数的地方,也没有明写在JNI_Onload中,大概在init_array中,于是发现有函数的调用。sub_2468中调用了sub_23C4
.init_array:00005DF0 ; Segment type: Pure data
.init_array:00005DF0 AREA .init_array, DATA
.init_array:00005DF0 ; ORG 0x5DF0
.init_array:00005DF0 DCD sub_2468+1
.init_array:00005DF0 ; .init_array ends
.init_array:00005DF0
于是我们需要修改sub_23C4这个函数的判断逻辑。
由于原逻辑是如下判断:
void __noreturn sub_23C4()
{
FILE *v0; // r4
char v1[536]; // [sp+0h] [bp-218h] BYREF
while ( 1 )
{
v0 = fopen("/proc/self/maps", "r");
if ( !v0 )
break;
while ( fgets(v1, 512, v0) )
{
if ( strstr(v1, "frida") || strstr(v1, "xposed") )
{
_android_log_print(2, "UnCrackable3", "Tampering detected! Terminating...");
LABEL_10:
goodbye();
}
}
fclose(v0);
usleep(0x1F4u);
}
_android_log_print(2, "UnCrackable3", "Error opening /proc/self/maps! Terminating...");
goto LABEL_10;
}
这时候可以在日志过滤查看,验证一下想法,可以发现确实是显示了Tampering detected! Terminating...
。
先修改了,但是这样发现还是会报错,于是直接在最后退出的地方,修改掉exit()。
.text:000023EA loc_23EA ; CODE XREF: sub_23C4+48↓j
.text:000023EA ; sub_23C4+66↓j
.text:000023EA MOV R0, R6 ; s
.text:000023EC MOV.W R1, #0x200 ; n
.text:000023F0 MOV R2, R4 ; stream
.text:000023F2 BLX fgets
.text:000023F6 CBZ R0, loc_2410
.text:000023F8 MOV R0, R6 ; char *
.text:000023FA MOV R1, R10 ; char *
.text:000023FC BLX strstr
.text:00002400 CBZ R0, loc_2436 ; Keypatch modified this from:
.text:00002400 ; CBNZ R0, loc_2436
.text:00002402 MOV R0, R6 ; char *
.text:00002404 MOV R1, R5 ; char *
.text:00002406 BLX strstr
.text:0000240A CMP R0, #0
.text:0000240C BNE loc_23EA ; Keypatch modified this from:
.text:0000240C ; BEQ loc_23EA
.text:0000240E B loc_2436
把最后的BLX指令给nop掉,修改Hex为00000000
.text:0000238C _Z7goodbyev ; CODE XREF: goodbye(void)+8↑j
.text:0000238C ; DATA XREF: LOAD:000002A0↑o ...
.text:0000238C ; __unwind {
.text:0000238C PUSH {R7,LR}
.text:0000238E MOV R7, SP
.text:00002390 MOVS R0, #6 ; sig
.text:00002392 BLX raise
.text:00002396 MOVS R0, #0 ; status
.text:00002398 BLX _exit
.text:00002398 ; } // starts at 238C
重新打包安装,即可正常打开app。然后再来看后续的逻辑。
主逻辑还是在so中,找到init函数
{
char *v5; // r6
sub_24BC(a1, a2); //需要调试可以nop掉
v5 = a1->functions->GetByteArrayElements(a1, a3, 0);
strncpy(byte_6034, v5, 0x18u);
a1->functions->ReleaseByteArrayElements(a1, a3, v5, 2);
return ++dword_6030;
}
还是获取一个输入字节的作用,然后主要是bar
{
jbyte *v5; // r6
unsigned int i; // r0
int result; // r0
_BYTE v8[28]; // [sp+0h] [bp-38h] BYREF
memset(v8, 0, 0x19u);
if ( dword_6030 != 2 )
goto LABEL_9;
sub_EBC(v8);
v5 = a1->functions->GetByteArrayElements(a1, a3, 0);
if ( a1->functions->GetArrayLength(a1, a3) != 24 )
goto LABEL_9;
for ( i = 0; i <= 0x17; ++i )
{
if ( v5[i] != (v8[i] ^ *(&dword_6030 + i + 4)) ) //dword_6030 = 2
goto LABEL_9;
}
if ( i == 24 )
result = 1;
else
LABEL_9:
result = 0;
return result;
}
基本可以知道如果需要得到这个v5就是我们输入的值也就是需要得到的值,那我们需要知道v8这个值,但是sub_EBC不知道在干啥,有两千多行,但是你把参数修改为一个值的时候就会发现,其实只有最后几行进行了操作,开辟了一个24字节的空间。
if ( result )
{
memset(key, 0, 0x19u);
*key = 319883293; //0x1311081d
key[1] = 357111567; //0x1549170f
key[2] = 419627021; //0x1903000d
key[3] = 353574234; //0x15131d5a
*(key + 8) = 3592;
result = (&loc_1412 + 1);
*(key + 18) = 135725146;
*(key + 11) = 5139;
}
arm默认是小端格式,所以这个key就是1d0811130f1749150d0003195a1d1315080e5a0017081314
,然后最奇怪的地方来了,从伪代码上看这里是跟一个常量进行了异或,但是这个明显不正常,异或的结果也不对,后来查了一下发现一开始传入的xorkey被忽略掉了,虽然这里没细看出来调用关系,但是确实是调用了,使用Ghidra就可以看到。
使用脚本进行异或,得到结果making owasp great again
。
secret = ""
other_key = bytes.fromhex("1d0811130f1749150d0003195a1d1315080e5a0017081314")
pizza = bytes("pizzapizzapizzapizzapizz",'utf-8')
for (a, b) in zip(pizza, other_key):
secret = secret + chr(a ^ b)
print(secret)
UnCrackable-Level4
这个有点复杂,别问,问就是不会。/(ㄒoㄒ)/~~