安卓基础知识
安卓系统架构
Android 是一个由 Google 主导开发的 基于 Linux 内核的开源操作系统,最初是为移动设备(如智能手机和平板电脑)设计的,现在也被广泛用于 智能电视、可穿戴设备、汽车系统(Android Auto)、IoT设备等。
Android系统架构共分四层,从下往上分别是:Linux内核层、系统运行库层(程序库+android运行库)、应用程序框架层、应用层。
Llnux内核层: Android 的 Linux 内核层是整个系统的基础,负责管理底层硬件资源和系统服务,包括进程管理、内存管理、电源管理、网络通信和文件系统等,同时还集成了 Android 特有的模块如 Binder(用于进程间通信)、Ashmem(匿名共享内存)和 Wakelocks(电源控制),为上层应用提供一个稳定、高效且安全的运行环境。
系统运行库层:Android 的系统运行库层包括两部分:原生程序库(Native Libraries)和Android 运行时(ART)。原生库由 C/C++ 编写,提供底层功能支持,如图形渲染(OpenGL ES)、数据库(SQLite)、音视频处理(Media)、网页渲染(WebKit)等,是系统和应用正常运行的基础;而 Android 运行时用于执行应用的 Java/Kotlin 代码,自 Android 5.0 起使用 ART(替代 Dalvik 虚拟机,后面会单独介绍),采用 AOT 编译方式提升性能,并提供核心 Java 类库支持应用运行。两者共同为上层框架和应用提供关键的功能支撑与执行环境。
应用程序框架层:应用程序框架层是 Android 架构中连接系统功能与应用开发的桥梁,向开发者提供了丰富的 API 接口和核心组件,使应用可以调用系统服务并实现复杂功能。它封装了常用系统功能模块,如 Activity 管理(ActivityManager)、窗口管理(WindowManager)、包管理(PackageManager)、内容提供(ContentProvider)、资源管理(ResourceManager)等,开发者通过调用这些框架类,无需关注底层实现即可构建功能完整的应用,实际上就是一些安卓api。这一层确保了应用的统一行为和系统的可管理性。
应用层:应用层是 Android 架构中最上层的一层,包含所有系统预装应用(如电话、短信、设置)和用户安装的第三方应用(如微信、抖音等)。这些应用通过调用下层的应用程序框架接口与系统交互,完成界面展示、数据处理和用户操作响应。每个应用都运行在独立的进程中,拥有独立的虚拟机环境,打包为 APK 文件进行分发和安装。应用层是用户与 Android 系统交互的直接窗口,体现了 Android 系统的功能和用户体验。
Dalvik虚拟机与ART虚拟机
Dalvik虚拟机
Dalvik 虚拟机是 Android 系统早期用于运行应用程序的虚拟机,它将 Java 字节码转换为一种名为 .dex(Dalvik Executable) 的格式,以优化内存使用和多进程运行性能,Dalvik虚拟机每次应用运行的时候,将代码编译成机器语言执行。与标准的 Java 虚拟机(JVM)不同,Dalvik 采用的是 寄存器架构(而非 JVM 的栈架构),更适合在资源受限的移动设备上运行。此外,Dalvik 支持多个虚拟机实例同时运行,确保每个应用都在独立进程和虚拟机中运行,从而提升了系统的稳定性和安全性。自 Android 5.0 起,Dalvik 被性能更高的 ART(Android Runtime)所取代。
DVM与JVM有很多不同之处:
- JVM运行的是Java字节码,DM运行的是 Dalvik字节码。传统java程序经过编译,生成java字节码保存在class文件中,java虚拟机通过解码class文件来运行程序,而 Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码都由java字节码转换而来,并打包到一个dex可执行文件(.dex),Dalvik虚拟机通过解释DEX文件来执行字节码
- DVM是基于寄存器的虚拟机 而JVM执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。且对于相同的代码生成的字节码,Dalvik字节码也会比java字节码更简单,指令更少。
- dvm执行的是.dex格式文件,jvm执行的是.class文件。.dex文件,是DVM独有的执行文件格式,体积更小,速度跟快,占用空间更少。
- 运行环境的区别。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
ART虚拟机
就Dalvik虚拟机而言,我们每用手机运行一个程序,都会对应的产生一个互不影响的实例,这就导致可能会占用过多的资源,不够优雅,因此促成了ART虚拟机的诞生。
ART(Android Runtime)是 Android 5.0 及以上版本中替代 Dalvik 的应用运行环境,它的核心特点是采用 AOT(Ahead-Of-Time)编译,即在应用安装时就将字节码编译为本地机器码,从而提升运行效率、减少启动时间、降低电量消耗。与 Dalvik 的 JIT(即时编译)不同,ART 提供更好的性能和更低的内存开销。ART 还支持运行时优化、垃圾回收(GC)改进、调试和性能分析工具等功能,是现代 Android 应用高性能运行的关键组成部分。
虽然ART替换了Dalvik虚拟机,但这并不意味着应用程序开发上也要发生改变,像android应用程序安装包(apk)中,仍然还是可执行的.dex文件。DVM是在每当程序运行时,通过解释器来执行Davlik字节码,进而转化成快速运行的机器码;而ART是在程序安装时将字节码预先编译成机器码,这样运行程序就不用再次编译,因此运行速度也会快的多。作为性能优化的代价,相对而言ART安装程序耗时会长一点,由于预编译机器码也导致所占的存储空间也会更大一点,不过ART是支持64位并且兼容32位CPU的,而DVM只是为32位CPU设计的。
Android应用程序架构
安卓应用程序使用JAVA语言编写。安卓的SDK工具负责将你编写的代码,用到的数据和资源文件编译进APK文件中,APK:android package(应用程序安装包),包含了一个安卓应用程序的所有内容,并且被安卓设备用来安装应用程序。而apk实际上就是一个标准的zip格式,修改后缀名,进行解压就可以看到内部结构(类似于war或者jar,都是可以直接解压的):
- assets文件夹: 保存一些额外的资源文件,如游戏的声音文件,字体文件等等,在代码中可以用AssetManager获取assets文件夹的资源。
- lib文件夹: 存放用C/C++编写的,用NDK编译生成的so文件,供java端调用。
- META-INF文件夹: 存放apk签名信息,用来保证apk包的完整性和系统的安全。在IDE编译生成一个apk包时,会对里面所有的文件做一个校验计算,并把计算结果存放在META-INF文件夹内,apk在安装的时候,系统会按照同样的算法对apk包里面的文件做校验,如果结果与META-INF里面的值不一样,系统就不会安装这个apk,这就保证了apk包里的文件不能被随意替换。比如拿到一个apk包后,如果想要替换里面的一幅图片,一段代码, 或一段版权信息,想直接解压缩、替换再重新打包,基本是不可能的。如此一来就给病毒感染和恶意修改增加了难度,有助于保护系统的安全。
- res文件夹: 存放资源文件,包括icon,xml文件
- res/layout/: 存放被编译为屏幕布局(或屏幕的一部分)的XML文件
- res/values/: 存放可以被编译成很多类型的资源文件
- array.xml: 定义数组
- string.xml: 定义字符串(string)值
- AndroidManifest.xml文件: 应用程序配置文件,每个应用都必须定义和包含的,它描述了应用的名字、版本、权限、引用的库文件等信息。
- classes.dex文件: 传统 Class 文件是由一个 Java 源码文件生成的 .Class 文件,而 Android 是把所有 Class 文件进行合并优化,然后生成一个最终的 class.dex 文件。它包含 APK 的可执行代码,是分析 Android 软件时最常见的目标。由于dex文件很难看懂,可通过apktool反编译得到.smali文件,smali文件是对Dalvik虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对smali文件的解读可以获取源码的信息。
- resources.arsc文件: 二进制资源文件,包括字符串等。
- smali: smali是将Android字节码用可阅读的字符串形式表现出来的一种语言,可以称之为Android字节码的反汇编语言。利用apktool或者Android Killer,反编classes.dex文件,就可以得到以smali为后缀的文件,这些smali文件就是Dalvik的寄存器语言。
Android应用编译流程
Android应用编译流程就是一个app应用的生成过程。在应用程序上架的时候都需要程序经过编译、签名 、生成一个后缀为apk的文件才能发布到应用市场,大致流程如下:
- 打包资源文件,生成R.java文件。通过利用aapt资源打包工具,将文件目录中的Resource文件(就是工程中res中的文件)、Assets文件、AndroidManifest.xml文件、Android基础类库(Android.jar文件)进行打包,生成R.java
- aidl生成Java文件。AIDL是Android Interface Definition Language的简称, 是Android跨进程通讯的一种方式。 检索工程里所有的aidl文件,并转换为对应的Java文件
- 编译Java文件,生成对应的.class文件。将R.java、aidl生成的Java文件、Java源文件通过JDK携带的Javac编译生成.class文件
- 把.class文件转化成Davik VM支持的.dex文件。通过dx工具将.class文件生成为classes.dex
- 打包生成未签名的.apk文件。利用apkbuilder工具,将resources.arsc、res目录、AndroidManifest.xml、assets目录、dex文件打包成未签名的apk
- 对未签名.apk文件进行签名。使用apksigner为安装包添加签名信息。
- 对签名后的.apk文件进行对齐处理。使用zipalign工具对签名包进行内存对齐操作, 即优化安装包的结构。
Android四大组件
Android 的四大组件是构成应用程序的核心模块:
- Activity(活动)。表示应用的一个单一界面,负责与用户进行交互。例如,一个登录页面或设置页面就是一个 Activity。
- Service(服务)。在后台运行的组件,用于执行长时间运行的操作,如播放音乐、下载文件等,不提供用户界面。
- BroadcastReceiver(广播接收器)。用于接收并响应系统或应用发出的广播消息,例如接收短信到达、电量变化、网络状态改变等事件。
- ContentProvider(内容提供器)。用于在不同应用间共享数据,提供统一的数据访问接口,比如系统的联系人、图库等数据都可以通过 ContentProvider 被其他应用访问。
这四大组件共同构成 Android 应用的基本结构,每一种都通过 AndroidManifest.xml
注册,协同完成应用的功能实现。
Android应用反编译流程
Android应用反编译流程很显然,就是app生成过程的逆过程,对于最左边的目标——未签名apk,有两条反编译路线:
反编译方式一:图上面这条路线是通过利用apktool这款工具,可直接对目标apk直接进行反编译,可以看到除apk本身的资源文件,还生成了smali文件,而smali文件里面包含的都是程序执行的核心代码。我们可以通过直接分析.smali文件中的smali代码,进而修改代码,改变其运行逻辑。
下载地址:https://bitbucket.org/iBotPeaches/apktool/downloads/
使用方法:
apktool d target.apk
反编译完成后会生成文件夹:
反编译方式二:图中下面这条路线是通过修改apk后缀名,对其解压后,将文件夹中的资源文件,通过利用AXMLPrinter2进行分析,进而反编译,而对于解压后的class.dex文件,利用Dex2jar反编译工具对其进行反编译得到jar文件。接着将生成的jar文件直接拖到JD-GUI文件中,可自动生成源码,我们可通过分析其源码,了解其程序的运行逻辑,但我们修改逻辑,最终还是需要在smali文件中进行修改。
dex2jar下载地址:https://sourceforge.net/projects/dex2jar/files/
dex2jar.bat classes.dex
然后可以看到生成了一个jar文件:
我们可以直接把jar文件拖到jd-gui里进行反编译:
jd-gui下载地址:http://java-decompiler.github.io/
除此之外,也可以使用一把梭工具Android killer,下载地址:https://down.52pojie.cn/Tools/Android_Tools/AndroidKiller_v1.3.1.zip
Android killer集Apk反编译、Apk打包、Apk签名,编码互转,ADB通信(应用安装-卸载-运行-设备文件管理)、源码查看等特色功能于一身,支持logcat日志输出,语法高亮,基于关键字(支持单行代码或多行代码段)项目内搜索,可自定义外部工具;吸收融汇多种工具功能与特点,是一款可视化的安卓应用逆向工具。
除此之外还有jadx-gui,这是一款可以将apk,dex,aar 和 zip 文件将 Dalvik 字节码反编译为 Java 类的JAVA反编译工具。而它是支持apk、dex、aar、zip等格式,所以我们在使用过程中,可直接将待测apk拖入的工具中,会自动执行反编译:
jadx-gui下载地址:https://down.52pojie.cn/Tools/Android_Tools/jadx_v1.3.4.zip
参考
https://www.anquanke.com/post/id/273348
安卓Lab
提前安装Android Studio,地址:https://developer.android.com/studio
首先新建项目:
创建好之后可能会因为Importing ‘Lab’ Gradle Project卡一会儿,然后创建一个activity:
这里我们创建一个虚拟设备:
我选择的这个Pixel 9 Pro:
接着运行这个设备:
改一下AndroidManifest.xml,不然运行会报Error running ‘app’ Default Activity not found
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.smali.secretchallenge">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Lab"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
现在我们就在本地简单的运行了我们的app,下面开始正式完成任务。
Step 1. Android Programming
Task 1. What’s in Background
任务要求:
- 在com.smali.secretchallenge包中创建⼀个名为SecretBootReceiver的新接收器类,让应⽤程序在Android系统启动时⾃动启动,⽆需任何⽤⼾操作即可在后台启动服务。
- SecretBootReceiver启动的服务应命名为SecretService ,并且位于com.smali.secretchallenge包中,该服务的功能必须获取此设备的GPS 位置,每 3 秒在屏幕上显⽰⼀次纬度和经度。
首先在AndroidManifest.xml里做几个改动,下面是完整代码:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.smali.secretchallenge">
<!-- Required permissions -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Lab"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- SecretBootReceiver for auto-start on boot -->
<receiver
android:name=".SecretBootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- SecretService for background location tracking -->
<service
android:name=".SecretService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
这里我们做了两个操作,首先是申请权限:
权限 | 作用 | 是否敏感权限 |
---|---|---|
RECEIVE_BOOT_COMPLETED | 允许 App 在设备启动完成后收到 BOOT_COMPLETED 广播,一般用于开机自启动服务或任务。 | 是(不需要用户手动授予) |
ACCESS_FINE_LOCATION | 精确定位权限,使用 GPS 定位,精度在几十米以内。 | 是(危险权限,Android 6.0+ 需要运行时授权) |
ACCESS_COARSE_LOCATION | 粗略定位权限,使用基站或 WiFi 定位,精度在几百米左右。 | 是(危险权限) |
FOREGROUND_SERVICE | 允许 App 启动前台服务(Foreground Service),服务会有通知栏提示,通常用于持续任务(如位置跟踪)。 | 否(Android 9+) |
FOREGROUND_SERVICE_LOCATION | 启动专用于“持续定位”的前台服务权限(Android 10+)。 | 是(需要和 FOREGROUND_SERVICE 配合使用) |
<!-- Required permissions -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
接着是声明了一个广播接收器,首先指定了这个 Receiver 的类名,BOOT_COMPLETED
是系统在设备开机完成后发送的广播,只有拥有 RECEIVE_BOOT_COMPLETED
权限的 App 可以接收到。android:exported="true"
表示这个接收器可以接收来自系统或其他应用的广播:
<receiver
android:name=".SecretBootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
然后是创建SecretBootReceiver.java,作用是一个开机广播接收器(BroadcastReceiver),它的功能是监听 Android 系统的设备启动完成事件,然后自动启动一个后台服务 SecretService
:
package com.smali.secretchallenge;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class SecretBootReceiver extends BroadcastReceiver {
private static final String TAG = "SecretBootReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.d(TAG, "Boot completed received, starting SecretService");
// Start the SecretService
Intent serviceIntent = new Intent(context, SecretService.class);
context.startService(serviceIntent);
}
}
}
接着创建SecretService.java,这段 SecretService
是一个后台定位服务,会在后台周期性获取并显示当前位置,主要通过 Android 的 LocationManager
来获取 GPS 数据,并每隔 3 秒用 Toast 和日志输出一次位置信息:
package com.smali.secretchallenge;
import android.Manifest;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
public class SecretService extends Service {
private static final String TAG = "SecretService";
private LocationManager locationManager;
private Handler handler;
private Runnable locationRunnable;
private Location currentLocation;
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "SecretService created");
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
handler = new Handler(Looper.getMainLooper());
// Create runnable to show location toast every 3 seconds
locationRunnable = new Runnable() {
@Override
public void run() {
showLocationToast();
handler.postDelayed(this, 3000); // Repeat every 3 seconds
}
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "SecretService started");
// Start location updates
startLocationUpdates();
// Start the periodic toast display
handler.post(locationRunnable);
return START_STICKY; // Restart service if killed
}
private void startLocationUpdates() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "Location permission not granted");
return;
}
LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
currentLocation = location;
Log.d(TAG, "Location updated: " + location.getLatitude() + ", " + location.getLongitude());
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {}
@Override
public void onProviderEnabled(String provider) {}
@Override
public void onProviderDisabled(String provider) {}
};
// Request location updates from GPS provider
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 0, locationListener);
// Also try to get last known location
Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKnownLocation != null) {
currentLocation = lastKnownLocation;
}
}
private void showLocationToast() {
if (currentLocation != null) {
String message = "Latitude: " + currentLocation.getLatitude() +
", Longitude: " + currentLocation.getLongitude();
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
Log.d(TAG, "Showing toast: " + message);
} else {
Toast.makeText(this, "Location not available", Toast.LENGTH_SHORT).show();
Log.d(TAG, "Location not available");
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "SecretService destroyed");
// Remove callbacks
if (handler != null && locationRunnable != null) {
handler.removeCallbacks(locationRunnable);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
最后是MainActivity.java,动态申请定位权限,并在权限通过后启动 SecretService
后台服务:
package com.smali.secretchallenge;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int LOCATION_PERMISSION_REQUEST_CODE = 1001;
private ActivityResultLauncher<String[]> locationPermissionRequest;
private TextView statusText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
statusText = findViewById(R.id.statusText);
// Initialize permission launcher
locationPermissionRequest = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
Boolean fineLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_FINE_LOCATION, false);
Boolean coarseLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_COARSE_LOCATION, false);
if (fineLocationGranted != null && fineLocationGranted) {
Log.d(TAG, "Fine location permission granted");
statusText.setText("Status: Fine location permission granted");
startSecretService();
} else if (coarseLocationGranted != null && coarseLocationGranted) {
Log.d(TAG, "Coarse location permission granted");
statusText.setText("Status: Coarse location permission granted");
startSecretService();
} else {
Log.d(TAG, "Location permission denied");
statusText.setText("Status: Location permission denied");
Toast.makeText(this, "Location permission is required for this app", Toast.LENGTH_LONG).show();
}
}
);
// Request location permissions
requestLocationPermissions();
}
private void requestLocationPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Location permission already granted");
statusText.setText("Status: Location permission already granted");
startSecretService();
} else {
Log.d(TAG, "Requesting location permissions");
statusText.setText("Status: Requesting location permissions...");
locationPermissionRequest.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
});
}
}
private void startSecretService() {
Log.d(TAG, "Starting SecretService");
Intent serviceIntent = new Intent(this, SecretService.class);
startService(serviceIntent);
Toast.makeText(this, "SecretService started", Toast.LENGTH_SHORT).show();
}
}
最后的效果:
Task 2.Change UI in the thread
任务要求:
- 要创建⼀个对话框,当按钮被点击时,显⽰你刚刚在⽂本中输⼊的内容。
- 在这个任务中,你需要在⼀个线程中更改 UI。实际上,我们不允许在另⼀个线程中更改 UI。我们只能在主线程中更改它。所以你需要想办法解决这个问题。
由于我们不能直接在子线程中更新 UI(否则会崩溃),所以必须使用 Android 的异步消息传递机制(Looper + Message + Handler)实现子线程与主线程之间的通信,这里实现的几个关键点如下:
- 使用子线程:
new Thread(() -> { ... }).start();
- 使用
Handler
通知主线程:mainHandler.sendMessage(message);
Message
携带数据:message.setData(bundle);
- 主线程响应消息并更新 UI:
Handler.handleMessage()
中showDialog(dialogMessage)
首先初始化 Handler(关联主线程),Looper.getMainLooper()
用于让这个 Handler 与主线程绑定;handleMessage()
保证当其他线程发送消息时,这里就会在主线程执行;showDialog()
用于弹出对话框:
mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_SHOW_DIALOG) {
String dialogMessage = msg.getData().getString(KEY_DIALOG_MESSAGE);
showDialog(dialogMessage);
}
}
};
点击按钮后读取用户输入,开启一个后台线程处理:
showDialogButton.setOnClickListener(v -> {
String userInput = inputText.getText().toString().trim();
if (userInput.isEmpty()) {
Toast.makeText(this, "请输入文本", Toast.LENGTH_SHORT).show();
return;
}
startBackgroundThread(userInput);
});
子线程模拟异步工作并发送 Message:
private void startBackgroundThread(String userInput) {
new Thread(() -> {
try {
Thread.sleep(1000); // 模拟耗时操作
// 构造并发送消息到主线程
Message message = mainHandler.obtainMessage(MSG_SHOW_DIALOG);
Bundle bundle = new Bundle();
bundle.putString(KEY_DIALOG_MESSAGE, userInput);
message.setData(bundle);
mainHandler.sendMessage(message);
} catch (InterruptedException e) {
Log.e(TAG, "Background thread interrupted", e);
}
}).start();
}
最后主线程收到消息后弹出对话框:
private void showDialog(String message) {
new AlertDialog.Builder(this)
.setTitle("后台线程消息")
.setMessage("你输入的内容: " + message)
.setPositiveButton("确定", null)
.show();
}
现在的MainActivity.java:
package com.smali.secretchallenge;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// Task 2: Handler constants
private static final int MSG_SHOW_DIALOG = 1;
private static final String KEY_DIALOG_MESSAGE = "dialog_message";
private ActivityResultLauncher<String[]> locationPermissionRequest;
private TextView statusText;
private EditText inputText;
private Button showDialogButton;
// Task 2: Handler for UI thread communication
private Handler mainHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Initialize UI elements
statusText = findViewById(R.id.statusText);
inputText = findViewById(R.id.inputText);
showDialogButton = findViewById(R.id.showDialogButton);
// Task 2: Initialize Handler with main thread Looper
mainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_SHOW_DIALOG) {
String dialogMessage = msg.getData().getString(KEY_DIALOG_MESSAGE);
showDialog(dialogMessage);
}
}
};
// Task 2: Set button click listener
showDialogButton.setOnClickListener(v -> {
String userInput = inputText.getText().toString().trim();
if (userInput.isEmpty()) {
Toast.makeText(this, "请输入文本", Toast.LENGTH_SHORT).show();
return;
}
// Start background thread
startBackgroundThread(userInput);
});
// Initialize permission launcher
locationPermissionRequest = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
Boolean fineLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_FINE_LOCATION, false);
Boolean coarseLocationGranted = result.getOrDefault(
Manifest.permission.ACCESS_COARSE_LOCATION, false);
if (fineLocationGranted != null && fineLocationGranted) {
Log.d(TAG, "Fine location permission granted");
statusText.setText("Status: Fine location permission granted");
startSecretService();
} else if (coarseLocationGranted != null && coarseLocationGranted) {
Log.d(TAG, "Coarse location permission granted");
statusText.setText("Status: Coarse location permission granted");
startSecretService();
} else {
Log.d(TAG, "Location permission denied");
statusText.setText("Status: Location permission denied");
Toast.makeText(this, "Location permission is required for this app", Toast.LENGTH_LONG).show();
}
}
);
// Request location permissions
requestLocationPermissions();
}
// Task 2: Start background thread
private void startBackgroundThread(String userInput) {
new Thread(() -> {
try {
// Simulate background work
Thread.sleep(1000);
// Send message to main thread to show dialog
Message message = mainHandler.obtainMessage(MSG_SHOW_DIALOG);
Bundle bundle = new Bundle();
bundle.putString(KEY_DIALOG_MESSAGE, userInput);
message.setData(bundle);
mainHandler.sendMessage(message);
} catch (InterruptedException e) {
Log.e(TAG, "Background thread interrupted", e);
}
}).start();
}
// Task 2: Show dialog from main thread
private void showDialog(String message) {
new AlertDialog.Builder(this)
.setTitle("后台线程消息")
.setMessage("你输入的内容: " + message)
.setPositiveButton("确定", null)
.show();
}
private void requestLocationPermissions() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Location permission already granted");
statusText.setText("Status: Location permission already granted");
startSecretService();
} else {
Log.d(TAG, "Requesting location permissions");
statusText.setText("Status: Requesting location permissions...");
locationPermissionRequest.launch(new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
});
}
}
private void startSecretService() {
Log.d(TAG, "Starting SecretService");
Intent serviceIntent = new Intent(this, SecretService.class);
startService(serviceIntent);
Toast.makeText(this, "SecretService started", Toast.LENGTH_SHORT).show();
}
}
Task 3. How to call that method?
任务要求:
- 获取 classes.jar 的 PoRELab 类中名为 curStr 的私有字段的值
- 调⽤ classes.jar 的 PoRELab 类中名为 privateMethod 的私有⽅法
在app目录下创建一个lib,把这个jar放进去:
这是这个jar的具体内容:
主要是在MainActivity.java加一点反射调用的内容:
private void demonstrateReflection() {
try {
Log.d(TAG, "=== Task 3: Java Reflection Demo ===");
// Get the PoRELab class from jar file
Class<?> poRELabClass = Class.forName("com.pore.mylibrary.PoRELab");
Log.d(TAG, "Successfully loaded PoRELab class from jar");
// Get private static field 'curStr' using reflection
Field curStrField = poRELabClass.getDeclaredField("curStr");
curStrField.setAccessible(true); // Make private field accessible
String fieldValue = (String) curStrField.get(null); // null for static field
Log.d(TAG, "Private static field 'curStr' value: " + fieldValue);
// Invoke private static method 'privateMethod' using reflection
Method privateMethod = poRELabClass.getDeclaredMethod("privateMethod", String.class, String.class);
privateMethod.setAccessible(true); // Make private method accessible
privateMethod.invoke(null, "Hello from reflection!", "Secret message"); // null for static method
Log.d(TAG, "Private static method 'privateMethod' invoked successfully");
Log.d(TAG, "=== Java Reflection Demo Completed ===");
} catch (ClassNotFoundException e) {
Log.e(TAG, "PoRELab class not found: " + e.getMessage(), e);
} catch (NoSuchFieldException e) {
Log.e(TAG, "Field 'curStr' not found: " + e.getMessage(), e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Method 'privateMethod' not found: " + e.getMessage(), e);
} catch (Exception e) {
Log.e(TAG, "Reflection error: " + e.getMessage(), e);
}
}
然后在build.gradle.kts里引入这个jar依赖:
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "com.smali.secretchallenge"
compileSdk = 35
defaultConfig {
applicationId = "com.smali.secretchallenge"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
// Task 3: Add jar file dependency
implementation(files("libs/Step1_Task3_classes.jar"))
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}
Task 4. Generate Signed Application
任务要求:
- 顾名思义,创建一个签好名的apk
最后生成成功:
Step 2. Smali2Java
首先使用smali.jar将 smali 文件编译为 dex 文件
java -jar smali.jar assemble src.smali -o src.dex
smali.jar 下载地址:https://github.com/baksmali/smali/releases/download/v2.2.2/smali-2.2.2.jar,或者其实Android Studio默认就安装了这个东西:
D:\Android\Android Studio\plugins\smali\lib\Step2_smali_code_files\smali>java -jar smali-2.2.2.jar assemble -o output.dex .
然后使用adb连接我们的安卓虚拟机,这个adb是直接Android studio默认安装的:
然后把我们的dex文件传上去:
D:\Android\SDK\platform-tools>adb push output.dex /tmp
output.dex: 1 file pushed, 0 skipped. 13.1 MB/s (4876 bytes in 0.000s)
D:\Android\SDK\platform-tools>adb shell
emu64xa:/ $ cd /tmp
emu64xa:/tmp $ ls -a
. .. CheckBox.dex Checker.dex Encoder.dex output.dex
CheckBox.java的逻辑:
package JDBC.lab;
import java.util.Scanner;
public class CheckBox {
private String rawInfo;
Encoder encoder = new Encoder();
Checker checker = new Checker();
public boolean checkEncP(String str) {
return this.encoder.check(this.rawInfo, str);
}
public String getEncP() {
return this.encoder.encoding(this.rawInfo);
}
private boolean checkInput(String str) {
return this.checker.check(str);
}
public static void main(String[] strArr) {
String str;
CheckBox checkBox = new CheckBox();
System.out.print("input: ");
String nextLine = new Scanner(System.in).nextLine();
checkBox.rawInfo = nextLine;
if (strArr.length == 0 && nextLine.length() == 11) {
str = "Task 2: (Encoded msg) " + checkBox.getEncP();
} else if (strArr.length != 1) {
str = "Task 1: " + checkBox.checkInput(nextLine);
} else {
str = "Task 2: " + checkBox.checkEncP(strArr[0]);
}
System.out.println(str);
}
}
Task 1. Checker
任务要求:
- 编译并运⾏给出的 smali 代码,实现输⼊⼀个字符串,CheckBox 将检查该字符串是否通过 Checker 类中的检查,并相应地输出 true/false
- 编写Java代码实现Checker.java中功能,完成 Java 代码后,您可以通过运⾏ CheckBox.java 中的 Main ⽅法并将 Java 中的输出与 smali 中的输出进⾏⽐较来检查代码是否正确运⾏
按照smali代码推断,符合要求的字符串要求如下:
- 长度 12~16
- 前10位 substring(0, 10),统计字符 ‘1’ 的个数,必须等于3
- checkStr1(substring(0, 10)) 必须为 true 前10位有两个 ‘x’,且它们间隔4位 第一个字符是 ‘0’ 最后一个字符是 ‘9’
- 第一个 ‘x’ 之前的字符串包含 “key”
- int num = Integer.parseInt(s.substring(10, s.length() – 1)); 这部分必须能转成整数
- func(count) == num 其中 count 是前10位 ‘1’ 的个数(即3),func(3)=6
一个符合要求的字符串为:
0keyx111x961
代码如下:
package JDBC.lab;
public class Checker {
private String secret = "key";
private boolean checkStr1(String str) {
char[] charArray = str.toCharArray();
int i = 0;
int i2 = 0;
int i3 = 0;
for (int i4 = 0; i4 < charArray.length; i4++) {
if (charArray[i4] == 'x') {
i++;
if (i == 1) {
i3 = i4;
}
if (i == 2) {
i2 = i4;
}
}
}
return i == 2 && i2 - i3 == 4 && charArray[0] == '0' && charArray[charArray.length + (-1)] == '9' && str.substring(0, i3).contains(this.secret);
}
private int func(int i) {
if (i <= 1) {
return 1;
}
return func(i - 1) * i;
}
private int count(String str) {
int i = 0;
char[] charArray = str.toCharArray();
int i2 = 0;
while (true) {
int i3 = i;
if (i2 < str.length()) {
i = charArray[i2] == '1' ? i3 + 1 : i3;
i2++;
} else {
return i3;
}
}
}
public boolean check(String str) {
if (str.length() < 12 || str.length() > 16) {
return false;
}
String substring = str.substring(0, 10);
int count = count(str.substring(10, str.length()));
return func(count) == count && checkStr1(substring);
}
}
Task 2. Encoder
任务要求:
- 按照Task1的⽅式编译并运⾏smali代码,输⼊你的学号,CheckBox会输出编码后的消息,使⽤ 1.1 中编码的消息作为 CheckBox 的参数并输⼊您的学⽣ ID,CheckBox 将检查消息是否通过Encoder 类中的检查并相应地输出 true/false。
- 编写Java代码实现Encoder.java中功能,完成 Java 代码后,您可以通过运⾏ CheckBox.java 中的 Main ⽅法并将 Java 中的输出与 smali 中的输出进⾏⽐较来检查代码是否正确运⾏。
emu64xa:/tmp $ dalvikvm -cp output.dex CheckBox
input: 18307130154
Task 2: (Encoded msg) 71921970cc1381400f40760011501bc06218214a1980131a
emu64xa:/tmp $ dalvikvm -cp output.dex CheckBox 71921970cc1381400f40760011501bc06218214a1980131a
input: 18307130154
Task 2: true
代码如下:
package JDBC.lab;
import java.security.MessageDigest;
import java.util.Random;
public class Encoder {
private final String[] hexDigits = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
private String algorithm = "MD5";
private String charSet = "utf-8";
public String encoding(String str) {
String result = "";
String salt = getSalt();
String combined = str + salt;
try {
MessageDigest md = MessageDigest.getInstance(this.algorithm);
byte[] digestBytes = md.digest(combined.getBytes(this.charSet));
String hex = byteArrayToHexString(digestBytes);
char[] cArr = new char[48];
for (int i = 0; i < 16; i++) {
cArr[i * 3] = hex.charAt(i * 2);
cArr[i * 3 + 1] = salt.charAt(i);
cArr[i * 3 + 2] = hex.charAt(i * 2 + 1);
}
result = new String(cArr);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
private String getSalt() {
Random random = new Random();
StringBuilder sb = new StringBuilder(16);
for (int i = 0; i < 16; i++) {
if (random.nextBoolean()) {
sb.append("1");
} else {
sb.append("0");
}
}
return sb.toString();
}
private String byteArrayToHexString(byte[] bArr) {
StringBuffer stringBuffer = new StringBuffer();
for (byte b : bArr) {
stringBuffer.append(byteToHexString(b));
}
return stringBuffer.toString();
}
private String byteToHexString(byte b) {
int unsignedByte = b & 0xff;
int high = (unsignedByte >> 4) & 0x0f;
int low = unsignedByte & 0x0f;
return hexDigits[high] + hexDigits[low];
}
public boolean check(String str, String str2) {
char[] cArr = new char[32];
char[] cArr2 = new char[16];
if (str2.length() != 48) {
return false;
}
for (int i = 0; i < 48; i += 3) {
cArr[(i / 3) * 2] = str2.charAt(i);
cArr[((i / 3) * 2) + 1] = str2.charAt(i + 2);
cArr2[i / 3] = str2.charAt(i + 1);
}
String str3 = str + new String(cArr2);
try {
return ("" + byteArrayToHexString(MessageDigest.getInstance(this.algorithm).digest(str3.getBytes(this.charSet)))).equals(new String(cArr));
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
然后配一下传入的参数:
Step 3. Reversing and Repacking
Task 1 Knock the door
任务要求:
- 在这个任务中,你会得到一个简单测试的apk。只有通过这个测试,你才能完成这个任务。不过,如果不作弊的话,很难通过。
这里的按键逻辑其实只要把l1改成0就行:
public void buttonClick(View view) {
int i = this.l0;
if (i != this.l1) {
int i2 = i + 1;
this.l0 = i2;
this.t2.setText(String.format("%d / %d", Integer.valueOf(i2), Integer.valueOf(this.l1)));
return;
}
this.t2.setText(R.string.success1);
this.t3.setText(PlayGame.getFlag(this.te.getText().toString(), this.ctx));
}
下载地址:https://github.com/iBotPeaches/Apktool/releases/download/v2.11.0/apktool_2.11.0.jar
这里用apktool解包一下apk:
java -jar apktool_2.11.1.jar d Step3_Task123_lab.apk
解包之后搜了一下,直接找到这个l1对应的v0
改成0x0:
然后重新打包成apk:
java -jar apktool_2.11.1.jar b Step3_Task123_lab_new -o Step3_Task123_lab_new.apk
签个名,我这里用的之前android Studio生成的签名,这里注意jdk环境需要和Android studio的一样:
java -jar "D:\Android\SDK\build-tools\35.0.0\lib\apksigner.jar" sign --ks D:\Android\fushuling.jks --ks-key-alias key0 "D:\CTF_tools\AndroidReverse\AndroidKiller_v1.3.1\bin\apktool\apktool\Step3_Task123_lab_new.apk"
最后随便输入一个东西即可执行成功:
Task 2 Give me your token
任务要求:
- 完成 Task1 后,你需要在这个 Task3 中输⼊正确的 token,并获得相应的提⽰。显然,光靠你的想象⼒是⽆法得到正确 token 的
隐藏的key的逻辑在这里:
sb3.setCharAt(0, (char) ('p' - 4)); // 'l'
sb3.setCharAt(1, (char) ('o' - 6)); // 'i'
sb3.setCharAt(2, (char) ('r' - 11)); // 'g'
sb3.setCharAt(3, (char) ('e' + 3)); // 'h'
//sb3 = "ligh"
sb2.setCharAt(0, (char) ('p' + 4)); // 't'
sb2.setCharAt(1, (char) ('o' + 10)); // 'y'
sb2.setCharAt(2, (char) ('r' - 13)); // 'e'
sb2.setCharAt(3, (char) ('e' + 7)); // 'l'
//sb2 = "tyel"
sb.setCharAt(0, (char) ('p' - 4)); // 'l'
sb.setCharAt(1, (char) ('o' + 0)); // 'o'
sb.setCharAt(2, (char) ('r' + 5)); // 'w'
sb.setCharAt(3, (char) ('e' - 1)); // 'd'
//sb = "lowd"
sb4.setCharAt(0, (char) ('p' + 2)); // 'r'
sb4.setCharAt(1, (char) ('o' - 10)); // 'e'
sb4.setCharAt(2, (char) ('r' + 1)); // 's'
sb4.setCharAt(3, (char) ('e' + 14)); // 's'
//sb4 = "ress"
最后拼在一起:lightyellowdress
Task 3 Call to the NPC
任务要求:
- 完成Task2后,你需要根据Task2中得到的提⽰进⾏相应的操作,才能得到最终的秘密答案
提示让我们调用某个东西,那可能是 native 函数、隐藏的方法、某个 JNI 接口或隐藏的逻辑,回看这里可以看到有个native方法,那就是得手动调用这个东西,传入的参数必须是这BuildConfig.FLAVOR.concat(sb3.toString()).concat(sb2.toString()).concat(sb.toString()).concat(sb4.toString())
手动加一句:
invoke-static {p1}, Lcom/pore/play4fun/PlayGame;->skdaga(Ljava/lang/String;)Ljava/lang/String;
move-result-object p0
然后按task1重新打包并签名,重新完成一次task1和2即可:
flag{SmaliIsCoolll}
反编译出的结果如下: