分析一个安卓简单CrackMe
本文为看雪论坛优秀文章
看雪论坛作者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 ,权限为public
public 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变量赋值为starry
Person.age = 2;
//给age变量赋值为2
Person.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>
//包含一个系统头文件iostream
using 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 JNIEnv
struct functions结构体里有函数,返回值为void类型,参数有三个,
//类型分别为int,double,const char*
//struct JNIEnv这个结构体里面保存了struct functions结构体的指针
//扩展:在c++中结构体前面可以省略struct,
//也就是说没必要这样来定义一个结构体变量struct JNIEnv JNIENv;
JNIEnv JNIENv;
//定义一个JNIEnv结构体,名字为JNIENv
JNIEnv* 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类型,大家查阅相关资料就知道了,然后赋值给v7
if (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
3.CVE-2022-0995分析(内核越界 watch_queue_set_filter)
4.ZJCTF2021 Reverse-Triple Language
球分享
球点赞
球在看
点击“阅读原文”,了解更多!