查看原文
其他

分析一个安卓简单CrackMe

白云精灵 看雪学苑 2022-07-01


本文为看雪论坛优秀‍‍‍文章
看雪论坛作者ID:白云精灵


我们把apk拖入模拟器,然后打开:

随意输入一串密码点击输入密码试试。可以看到,提示我们验证码校验失败:

我们打开jeb进行分析,直接把apk拖进去:

 

聊聊jeb的使用,拖入apk即可进行自动反编译。


下面是反编译出来的代码,也就是dex文件反编译后的相关代码:
这个就是相关的一些资源文件:

配置清单里面存放有apk的相关配置信息,例如activity,service,都在这里面。


下面这个就是代码区了,双击ByteCode即可进入,不过jeb会默认给你打开ByteCode这个字节码区。

 

下面这个字符串就是一些代码中出现的方法,类所在路径,还有传参时的字符串。

我们顺着之前的验证码校验失败的Toast弹窗来找到相关的逻辑。


在屏幕上显示一段文字,没别的东西的话,那这个一般就是Toast弹窗了:

我们右键,点击如下箭头所在的位置,这个就是查找引用,看那个方法引用了这个字符串。

 

 

点击后,我们可以看到相关引用地方:

我们双击显示出来的引用,来到如下位置:


 

我们按一下tab键,转为java代码:

我们把代码复制出来,然后进行分析:

package com.yaotong.crackme;import android.app.Activity; import android.content.Intent;import android.os.Bundle; import android.view.View.OnClickListener;import android.view.View; import android.widget.Button;import android.widget.EditText;import android.widget.Toast; public class MainActivity extends Activity {public Button btn_submit; public EditText inputCode; static { System.loadLibrary("crackme"); } @Override // android.app.Activity //说明这个方法重写了 protected void onCreate(Bundle arg3) { //protected super.onCreate(arg3); this.setContentView(0x7F030000); // layout:activity_main this.getWindow().setBackgroundDrawableResource(0x7F020000); // drawable:bg this.inputCode = (EditText)this.findViewById(0x7F060000); // id:inputcode this.btn_submit = (Button)this.findViewById(0x7F060001); // id:submit this.btn_submit.setOnClickListener(new View.OnClickListener() { @Override // android.view.View$OnClickListener public void onClick(View arg6) { if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "验证码校验失败", 0).show(); } }); } public native boolean securityCheck(String arg1) { }}package com.yaotong.crackme; //包路径,也就是这个MainActivity所在的路径,写上这个后, //com.yaotong.crackme这个里面的所有相关类都可以在MainActivity里使用了//package为关键字,固定写法,后面为你的当前类所在的文件夹,也就是包//下面这些就是导入这个类里面的相关方法需要用到的包//import为固定写法,一个关键字,我们不能用这个关键字给变量取名字,//import 后面跟你的类中要用到的方法所在的包就行import android.app.Activity; //活动 activity相关方法import android.content.Intent; //意图 Intent 相关方法import android.os.Bundle; //Bundle相关方法import android.view.View.OnClickListener; //OnClickListener 点击事件相关方法import android.view.View; //View 视图相关方法import android.widget.Button; //Button 按钮相关方法import android.widget.EditText;//EditText 编辑框相关方法import android.widget.Toast;//Toast弹窗相关方法public class MainActivity extends Activity { //定义一个类为MainActivity 继承了Activity ,Activity //里面的相关方法,变量,在MainActivity 里面都可以使用//extends 为继承的意思 后面跟一个类//public class 为固定写法,前面的public 可以换,可以用private,protected//public ,private,protected,这些为权限限定符,可以限定你类,变量或者方法不让其他类访问,}public Button btn_submit; //定义一个按钮对象,名字为btn_submit ,权限为publicpublic EditText inputCode;//定义一个编辑框对象,名字为inputCode 权限为pubilc static { System.loadLibrary("crackme"); //static 静态代码块,当MainActivity对象创建时,首先执行里面的代码,//System是一个对象,打点.调用loadLibrary方法,传入参数crackme,//意思为加载so库,名字为crackme前后省略lib,.so,这个文件我在之前有说,//是一个c\c++编写的文件,需要jni才能调用//loadLibrary方法在System里面 } @Override // android.app.Activity //说明这个方法重写了 protected void onCreate(Bundle arg3) { //权限为protected,返回值为空 方法名为onCreate,//(Bundle arg3)这个为参数,参数类型为Bunldle,名字为arg3 super.onCreate(arg3);//super.onCreate(arg3),调用父类的onCreate方法,传入的参数为arg3,这个 //arg3也就是上面那个protected权限修饰的一个方法//父类也就是MainActivity继承的那个activity类,也就是说调用activity类里面的onCreate方法 this.setContentView(0x7F030000); // layout:activity_main//这个setContentView为设置布局的一个方法,布局文件的id为0x7F030000//这个id为 layout:activity_main,后面会说明id所在位置,这个id是开发工具自动为我们生成的//this指的是当前所在类的对象,也就是MainActivity实例化后的对象//为什么这个类中没有setContentView方法也能调用呢?//因为MainActivity这个类继承了activity类 this.getWindow().setBackgroundDrawableResource(0x7F020000); // drawable:bg//getWindow().setBackgroundDrawableResource设置窗口背景为 drawable目录下的bg.png图片//getWindow()返回一个对象,然后打点.调用setBackgroundDrawableResource()方法,传入R文件中的id//R文件在后面我截图了,R类中,会保存resource文件中的每一个信息,产生id,这样我们的代码才能引用他 this.inputCode = (EditText)this.findViewById(0x7F060000); // id:inputcode//调用findViewById方法,传入inputcode 在R文件中的id,返回的是一个view对象,//findViewById这个方法的意思是通过id来查找相关视图//我们需要把他转为EditText对象,因为这个对象实际上是一个编辑框,(EditText)这个就是强转//强转可以把父类转为子类,可以把int数据类型转为double类型等//然后把EditText对象赋值给在前面定义的public 权限的EditText对象 this.btn_submit = (Button)this.findViewById(0x7F060001); // id:submit//调用findViewById方法,传入submit在R文件中的id,返回的是一个view对象,//findViewById这个方法的意思是通过id来查找相关视图//我们需要把他转为Button对象,因为这个对象实际上是一个编辑框,(Button)这个就是强转//强转可以把父类转为子类,可以把int数据类型转为double类型等//然后把Button对象赋值给在前面定义的public 权限的Button对象this.btn_submit.setOnClickListener(new View.OnClickListener() { //给按钮btn_submit绑定一个监听事件,setOnClickListener就是设置点击事件//这个setOnClickListener方法需要一个OnClickListener的实现类//在这里用的是匿名内部类的方式实现的//这个OnClickListener在View类中,所以前面要加View,然后打点 @Override // android.view.View$OnClickListener// @Override 说明这个方法是一个重写方法,这个onClick方法在OnClickListener里面//这个方法是一个抽象方法,需要我们自己实现 public void onClick(View arg6) { //这个方法前面都是固定的 public void onClick(View arg6) {}//里面的内容需要我们自己写,在这里面我们可以看到相关的逻辑//下面我分开讲这个校验逻辑 if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "验证码校验失败", 0).show(); } }); } if(MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())) { MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class)); return; } Toast.makeText(MainActivity.this.getApplicationContext(), "验证码校验失败", 0).show(); } }); }//if(){}固定写法 ()这里面写相关的返回值为真或者为假的代码,//比如==,> < <= >=等,或者写一个方法,//这个方法的返回值为true或者false即可//MainActivity.this.securityCheck(MainActivity.this.inputCode.getText().toString())//为什么是MainActivity.this呢?一个我们新建了一个类,//如果我们this放前面的话,就调用的是我们的实//现类里面的方法,也就是说只有onClick方法可以被调用。MainActivity.this//就是指定在MainActivity里面方法,类的话,是调用不到,我们只能new一个对象//也就是新建一个对象,创建对象的写法,new 类名();如果括号里面没有写值,那么调用的是无参构造方法//构造方法没有返回值,例如public 类名(){}这个就是一个无参构造方法,如果里面写参数,比如基本数据类型//int double float long 等,还有其他的数据类型也就是引用数据类型,比如对象,放一个接口也行,只不过我们要给他传入一个实现类


基本数据类型


引用数据类型

//然后调用了securityCheck,传入了一个参数MainActivity.this.inputCode.getText().toString()//这个参数是String类型的,这个inputCode就是那个编辑框对象,也就是crackme软件的那个输入框,//调用了getText().toString()方法,getText返回一个对象,然后用这个对象调用方法,返回一个String //如果这个securityCheck方法返回值为true那么执行下面这个startActivity方法MainActivity.this.startActivity(new Intent(MainActivity.this, ResultActivity.class));//这个方法会开启新的activity,传入的参数是一个intent,//这个intent传入的参数第一个为当前MainActivity//第二个参数为要开启的activity的类,.class就是类//调用完这个开启activity方法后,执行 return;这个就是结束当前方法,//如果这个securityCheck方法返回值为false的话,那么就执行下面的Toast弹窗 Toast.makeText(MainActivity.this.getApplicationContext(), "验证码校验失败", 0).show();//调用Toast对象里面的makeText方法,传入三个参数,第一个为上下文,也就是context,这个//MainActivity.this.getApplicationContext()的返回值为一个上下文,第二个参数为我们想要显示的字符串,//第三个参数为显示多长时间


我们看一下新开启的activity:

package com.yaotong.crackme; import android.app.Activity;import android.os.Bundle;import android.widget.TextView; public class ResultActivity extends Activity { @Override // android.app.Activity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv = new TextView(this); tv.setText("Congratulations!!!You Win!!"); this.setContentView(tv); }}


很多代码我都在前面详细讲了,这里我讲一下没讲的代码,new TextView(this),这个就是创建一个TextView对象,传入的参数为this,调用的是有参构造。


返回的对象用tv变量保存,TextView为这个变量的引用数据类型。
然后用tv调用了setText方法,传入一个字符串String类型的参数,这里面的意思

大概是恭喜你,成功了。


然后调用setContentView()方法传入tv参数,这样就能把这个TextView显示到界面上。在界面显示的对象实际上是一个view对象,是所有控件的父类,因此我们可以传入textview。


this指的是当前activity,也就是ResultActivity ,setContentView方法在继承activity后才能使用,也就是 extends Activity。


接下来我们看看这个securityCheck方法:

public native boolean securityCheck(String arg1) { }


这个方法是一个native方法,说明这个方法的逻辑在so层,也就是libcrackme.so这个文件里面。


下面我们要用到的工具是ida,因为ida可以很好的分析so。


我们首先要解压apk,然后获得如下文件:


这个so文件在lib文件夹里。


这里扩展一下文件结构,lib是存放一些so文件的,这个so文件由ndk生成
C\C++语言开发,在java层无法直接调用,但是可以通过jni间接调用
我们只要声明这个方法为native方法,就可以调用了,但前提是,在so中有相关这个方法的实现。


META-INF这个是存放签名的,AndroidMainfest.xml这个文件是存放相关的配置信息,比如activity,service,包名,是否全屏,标题,apk的图标等,都可以在这里进行设置。


Classes.dex这个是相关的java代码转成了class字节码文件。


为什么不直接是.class后缀呢?因为版权问题,所以谷歌自己写了个编译转换方式,直接转为dex文件,不直接转为class文件,同时也为了方便,因为你要编写大量的类,那么就会生成大量的class字节码文件,这样也不方便,所以直接合在一起了。


resources.arsc这里面存放了相关资源的索引,比如布局文件里面id,他的相关索引会出现在resources.arsc里面,详细参考下面。

 

布局文件的id,按钮视图,编辑框视图的id所在位置,这个id每当你在Resources文件中声明时都会自动生成。


Resource资源产生相关的id信息都存储在R类下:



这些是权限限定符的详解:

我们进入存放so文件的lib文件夹,可以看到这里面还有一层文件夹,armeabi,这个代表这个so文件是arm架构的。用于运行在arm的手机中: 


我们把这个文件拖入ida里面,这里我们默认即可,然后点击ok
然后出现提示,我们点击ok。




如果不想再弹出这个提示,我们可以勾选下面这个框框。

函数相关的都在这里:


我们找到那个securityCheck方法在so层的实现。我们可以看到有securityCheck函数,这个其实就是check方法的实现了。


Java_com_yaotong_crackme_MainActivitysecurityCheck


Java代表这是一个java层的,com yaotong crackme 这个就是包名
MainActivity就是activity。


SecurityCheck这个就是在java中的方法名,合起来就是在java层有个securityCheck方法,他在MainActivity类里,这个类在com.yaotong.crackme包下。中间我们换成就是在so层的函数名了
我们只传了一个参数,为什么这个函数有三个参数呢?


int __fastcall Java_com_yaotong_crackme_MainActivity_securityCheck(int a1, int a2, int a3)


实际上有两个参数ida未识别,一个JNIEnv *, 还有一个是jclass,第一个是JNI环境的指针,第二个是java的类。


我们可以导入安卓jni开发的相关头文件,然后修改类型,ida就会自动为我们识别代码。


导入方式如下:

然后我们选择jni的头文件:

然后会提示我们解析成功:

下面我们修改参数类型,前两个参数是我之前讲的那两个参数,一个JNIEnv*,一个jclass。

 

对着第一个int类型右键,点击Set lvar type:

然后我们把类型写上,JNIEnv*,点击ok。

第二个参数,同样右键,然后点击set。

输入jclass,点ok。

我们可以看到效果,未识别前:

识别后:


第三个变量的类型不应该是int类型,而是string类型,我们可以在string前面加个j。


代码java的string类型,也就是jstring:

我们可以改改变量名字,方便阅读。对着变量名右键,也就是a1,然后选择箭头所在的选项,Rename:

第一个参数是JNIEnv*,所以我们把他取名为env,这个名字可以任意,只是我们方便阅读。输入要改的名字后,我们点击ok。

重复前面的操作:

因为class是关键字,所以会报错,我们应该在前面加个_




重复前面的操作,右键,选择Rename。

 

这个是改好后的函数
int __fastcall Java_com_yaotong_crackme_MainActivity_securityCheck(JNIEnv *env, jclass _class, jstring string)



可以看到方便阅读多了,当然我们也可以右键选择这个。



我们选择JNIEnv,第一个的是类型名,第二个是这个JNIEnv结构体的声明,

 

第三个是占用内存大小。可以看到第二个结构体声明里面又有一个结构体指针,并且是const修饰,因此这个*functions不可修改,struct类型名,说明这是一个结构体类型,后面的是这个结构体,functions是这个结构体的指针,const,struct,都是固定写法。


指针相关知识:

这里我说一下结构体是什么,怎么定义一个结构体。

struct person{char *name;//姓名,char类型的指针,里面可以存放字符int age;//年龄 int类型,存放整型变量char *sex;//性别 char类型的指针,里面可以存放字符}定义格式struct xxx{这里面写数据类型;}下面是其中一种定义格式,也是常见的一种定义格式#include <stdio.h>int main() { struct person { char* name;//姓名 int age;//年龄 char* sex;//性别 }; struct person Person; Person.name = "starry"; Person.age = 2; Person.sex = "男"; printf("%s %d %s", Person.name, Person.age, Person.sex);}struct person Person;//struct person是数据类型,类似int一样,Person是变量名//struct person是一个整体,Person.name = "starry";//给name变量赋值为starryPerson.age = 2;//给age变量赋值为2Person.sex = "男";//给sex变量赋值为男printf("%s %d %s", Person.name, Person.age, Person.sex); //分别打印name,age,sex//%s是指与char类型匹配, %d是与整型匹配,//%s %d %s 分别对应Person.name, Person.age, Person.sex

这个只是其中一种写法,也是常见的一种写法。

 

如下是运行图:


我们主要分析下面这个代码:

v5 = env->functions->GetStringUTFChars(env, string, 0);//把我们在java层传入的字符串转成c类型的char,//env是一个结构体指针,如果是指针那我们就要用->箭头这种形式来访问里面变量,函数等.//env->functions,这个functions实际上还是一个结构体指针所以又有一个->箭头//最后这个就是functions结构体指针里面的函数了GetStringUTFChars//这个函数有三个参数,第一个是env,第二个是string,第三个是0//我演示一下结构体指针 #include<iostream> //包含一个系统头文件iostreamusing namespace std;//使用命名空间 using namespace为固定写法 int main() { struct functions { void PriStr(int a,double b,const char *c) { cout << a << b << c << endl; } }; struct JNIEnv { functions* fu; }; JNIEnv JNIENv; JNIEnv* JNI=&JNIENv; functions fun; JNI->fu = &fun; JNI->fu->PriStr(66, 77, "hello");} //在这里我定义了两个结构体struct functions和struct JNIEnvstruct functions结构体里有函数,返回值为void类型,参数有三个,//类型分别为int,double,const char*//struct JNIEnv这个结构体里面保存了struct functions结构体的指针//扩展:在c++中结构体前面可以省略struct,//也就是说没必要这样来定义一个结构体变量struct JNIEnv JNIENv;JNIEnv JNIENv;//定义一个JNIEnv结构体,名字为JNIENvJNIEnv* JNI=&JNIENv;//定义一个JNIEnv结构体指针,&JNIENv为取JNIENv变量的地址functions fun;//定义一个fun结构体变量JNI->fu = &fun;//因为JNI是一个结构体指针,所以我们不能打.点来获取结构体里面的相关变量//获得fu变量后,给fu变量赋值为fun的地址;JNI->fu->PriStr(66, 77, "hello");//JNI是结构体指针所以->箭头指向内部的值,fu也是结构体指针//,所以再次指向箭头来获取里面的东西,也就是PriStr函数,我们传入66,77,”hello”//然后我们点击运行,就会执行cout << a << b << c << endl;


然后打印相关数据到控制台:

//因为是c中char *字符类型,所以我们可以改为c_string名字,方便阅读。



v6 = off_628C;//这个不确定是什么,我们继续向下看 while (1){ v7 = (unsigned __int8)*v6; if (v7 != *(unsigned __int8*)c_string) break; ++v6; ++c_string; v8 = 1; if (!v7) return v8;} //可以看到有个while(1)死循环,v7 = (unsigned __int8)*v6;//把*6的值转为无符号int8类型,实际上就是char类型,大家查阅相关资料就知道了,然后赋值给v7if (v7 != *(unsigned __int8*)c_string)//如果v7不等于c_string那么执行下面的break代码;//直接跳出了这个死循环,(unsigned __int8*)c_string,//这个就是把c_string转为无符号unsigned __int8*类型,然后取*,获得里面值 break;//如果break了会怎么样呢?//就会执行下面的这个return 0;,然后把这个0给java层的那个if条件判断语句//0就是假,如果为假,那么就执行toast弹窗,提示验证码错误的信息//我们继续向下看++v6;//++v6这个就是v6这个指针加v6这个类型去*的类型的大小//比如char 类型他是1个字节的,同时这个v6又是char *类型的,那么就是+1//比如一个地址0x12340000,把这个地址给一个变量名为a,那么++a就是//这个地址+1,也就是0x12340001,++c_string;//这个也是给指针+对应类型长度也就是+1,一个字节的长度v8 = 1;//给v8赋值为1,这个是关键因为下面有个return v8,返回1的话,那么就是成功了if (!v7)//如果v7为空,代表对比完了,因为空取反就是真,就返回v8,然后就成功了return v8;


现在的关键是怎么找到这个正确的验证码,下面我们开始动态调试来获取这个码:

我们重新打开ida,然后选择如下:

输入对应的ip和端口号就可以开始调试:

在调试前,我们需要把ida的调试文件放到手机中。



放入手机的命令是adb push H:\IDA_Pro_v7.5_Portable(1)\dbgsrv\android_server /data/local/tmp

 

你也可以放到其他目录,/data/local/tmp这个目录比较常用而已。

放入后,我们进入/data/local/tmp。如果要进入我们先要adb shell

然后执行su命令,su用于获取最高权限,方便调试,给相关文件设置权限。

我们输入 cd /data/local/tmp进入文件夹,然后输入 ls- a,可以列出所有的文件。


列出后我们可以看到自己刚刚放入的文件android_server:

 

我们需要给这个文件赋予最高权限,同时需要给他执行权限,修改权限命令chmod 777 你的文件。

然后我们./android_server执行这个调试文件:

然后我们就可以在ida上进行调试了。调试前,我们先要在手机上打开那个crackme。然后我们在ida上输入我们手机的ip地址后,点击ok:

选择我们的进程:

双击进入,这个就是在下载手机上的相关文件了:

出现下面这个信息,我们点击ok就行。



然后我们点击绿色箭头进行运行。



选择一个我们ctrl+F搜索。



我们搜索libCrackme:



双击找到这个securitycheck函数:



双击securitycheck函数,进入后,按空格放大,然后按tab键,把汇编转成c伪代码。



我们改一下这个函数的参数,第一个是JNIEnv*第二个是jclass,第三个是jstring,在修改前,我们需要导入jni的头文件。





可以看到相关函数名已经可以看到了:



我们在app上输入一串字符串然后,点击输入密码,可以看到断下来了:



我们可以修改一下相关的变量名,方便阅读分析。右键选择rename。




按照前面的分析,我们执行一下这行代码应该就能获取真码了。
F8是步过;F7是步入;Ctrl+F7是运行到return代码处;F4是运行到鼠标指定位置。



这里我们F8,F8后,我们双击这个flag,这个flag是我修改的名字。



双击flag后进入到这个代码区,我们可以看到一串字符串。



我们按键盘上的a,把这个字符串变为一串的。按a后我们点击yes。



这个是转换后的结果,aiyou,bucuoo



我们继续单步,可以看到这个123456789数字其实就是我之前输入的,我们试一下把这一串字符串改成上面转换后的结果,看看能不能成功。


这个c_string在r0,所以我们到r0这个位置进行修改:



我们对着r0后面那个地址右键,然后选择jmp。



Jmp后的代码:

我们按一下a,把这一串123456789变为连起来的。


我们右键选择Hex View-1,地址不一样是因为刚刚ida卡死了。

 


我们选择这一串字符串进行右键选择edit。

 

修改好后,别忘了应用,然后我们继续单步。

 




第一次比对V11=0x61 c_string=0x61
第二次V11=0X69 c_string=0x69
第三次V11= 0x79 c_string=0x79
第四次v11=0x6F c_string=0x6F
第五次V11=0x75 c_string=0x75
第六次v11=0x2C c_string=0x2C
第七次v11=0x62 c_string=2x62
第八次 v11=0x75 c_string=0x75
第九次 v11=0x63 c_string=0x63
第十次 v11=0x75 c_string=0x75
第十一次 v11=0x6F c_string=0x6F
第十二次 v11=0x6F c_string=0x6F
第十三次 v11=0 c_string=0
当我们执行完第十三次后,程序弹出Congratulations!!!You Win!!

 

我们把分析结果放入vs,进行转换,不出意外的话,结果是aiyou,bucuoo
简单写了一下代码,可以看到,这个真码就是aiyou,bucuoo

代码


我们拿着这个真码,放到模拟器上试试,可以看到成功了。

 






看雪ID:白云精灵

https://bbs.pediy.com/user-home-814281.htm

*本文由看雪论坛 白云精灵 原创,转载请注明来自看雪社区

# 往期推荐

1.NtSockets - 直接与驱动通信实现sockets

2.JLink固件漏洞

3.CVE-2022-0995分析(内核越界 watch_queue_set_filter)

4.ZJCTF2021 Reverse-Triple Language

5.Docker-remoter-api渗透

6.Writeup-ROP Emporium fluff






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存