#逆向-安卓逆向初探-(4.5/26)

👀5ea1

这部分内容,将跟着正已的‘安卓逆向这档事’课程学习


001第一节.模拟器环境搭建

安装模拟器

这里使用了雷电模拟器,是9.0的安卓
安装完以后要做两件事

  1. 开启root权限
    1
  2. 打开system文件的可写权限
    2

安装面具

直接拖入apk安装,注意这里一定要安装Debug版的,不然下不下来
3
安装完以后进入软件,按照提示给软件打开权限,然后不要管app的更新,直接在Magisk框内点击安装,全部选中后选择安装至系统分区,这是专门给模拟器的选项
4
点击安装,直到出现安装成功后,重启模拟器

安装模块

此处将zygisk模块的包移到模拟器文件库中,注意,此处直接移动zip压缩包即可,然后在Magisk软件的模块页面中选择该压缩包并安装模块
5
安装后,若出现不能启动的红色字体提示,可以在主页的设置中打开相关选项
6


002第二节.初识APK文件结构、双开、汉化、基础修改

该专题将开始接触MT管理器和开发者助手

APK文件结构

APK全称Android Package,相当于一个压缩文件,故可以解压获得其中文件。

文件 注释
assets目录 存放APK的静态资源文件,比如视频,音频,图片等
lib目录 armeabi-v7a基本通用所有Android设备,arm64-v8a只适用于64位的Android设备,其中的.so文件是c或c++编译的动态链接库文件
META-INF目录 保存应用的签名信息,签名信息可以验证APK文件的完整性
res目录 res目录存放资源文件,包括应用的名字,版本,权限,引用的库文件等信息
AndoridMainfest.xml文件 APK的应用清单信息,它描述了应用的名字,版本,权限,引用的库文件等等信息
classes.dex文件 classes.dex文件是Java源码编译后生成的Java字节码文件,APK运行的主要逻辑
resources.arsc文件 resources.arsc文件是编译后的二进制资源文件,它是一个映射表,映射着资源和id

双开文件

修改包名

让手机系统认为这是2个APP,从而生成2个数据存储路径
打开MT管理器,在左上角打开功能栏,选择安装包提取,提取对应文件的安装包
7
提取后直接选中APK安装包,选择功能选项,使用APK共存功能实现修改
8
9
注意,此处使用APK共存功能的时候要把自动签名打开

修改包名后再次安装,可以看到系统中出现了两个同样的应用

修改Framework

不会 插个眼

虚拟化技术

不会 插个眼

插件hook

不会 插个眼


汉化

APK的逆向流程大致为查壳->有壳则脱壳->反编译->修改代码->回编译签名->测试,而此处要做的汉化在反编译后的一步中,主要可以分为以下三类

  1. Arsc汉化
  2. Xml汉化
  3. Dex汉化

下面是吾爱破解中给出的同步练习题,要将这几处外文汉化为中文
10
此时要用到另一个工具,即开发者助手,打开开发者助手,桌面上会出现一个甲壳虫标志,进入软件中的题目页面,点击甲壳虫呼出开发者助手面板,分析界面资源
11
在找到需要的资源框后,就可以借助资源框中给出的信息,回MT管理器中寻找该段数据,譬如这一段非英文数据,我在使用开发者助手分析后,可以复制这一段数据,回MT中直接使用字符串比对进行寻找,加以修改
12
在MT中点击安装包,选择‘查看’选项,在右上角呼出搜索框,搜索
13
发现该字符串在resources.arsc文件中,选中该文件进入,搜索对应资源,修改
14
15
修改后退出并保存,重新附加签名,覆盖安装
可以看到成功汉化
16


修改软件名和图标

还是用这个软件
17
首先我们要知道这两个信息存储在哪里,根据上面对APK文件的结构分析,可以得知这两个信息放在AndoridMainfest.xml文件中,故进入该文件寻找
使用反编译模式打开AndoridMainfest.xml文件
18
该处的label和icon即软件名称和图标对应的资源id
复制,回到resources.arsc中搜索该id对应资源
19
成功找到软件名称资源所在
20
可以随意修改

接下来着手修改图标
一般修改图标仅需进入res文件夹,图标文件名一般为ic_Launcher.icon,若没有,直接搜索.icon也可,不过这道题没那么精细,还是要从AndoridMainfest.xml中推一下
21
那么看来就要改这四张图片了
进到对应文件夹里修改,再重新签名,退出来看效果
22
成功修改


002——神秘的小实践

放寒假了嘛,喜欢看点深夜情感档,但是软件要密码怎么办!不想帮忙刷单做任务,又懒得找别的(
23
那么就只能自己动手咯

打开软件直接用开发者助手扫一下,扫到密码框是edit_key
进MT管理器,搜索这个文本
24
找到一个压缩包(其实是一个apk)
选择浏览压缩包,里面有dex文件,很明显是apk
再次搜索,发现在dex文件里;跟进搜索,找到资源id
25
那就直接拿着id去资源文件里捞了,捞出来发现是一个布尔值,把f改成t就行了(


003第三节.初识smali,vip终结者

smali是什么

Smali是Android虚拟机的反汇编语言。Android虚拟机的可执行文件并不是普通的class文件,而是再重新整合打包后生成的dex文件。dex文件反编译之后就是Smali代码,所以说,Smali语言是Android虚拟机的反汇编语言。

smali语法

直接用52pj大佬正己做的Smali语法查询软件即可(
26

任务

在如下的一个软件中,获取所谓“大会员”的力量,进行一键三连
27


这里为了方便查看代码,使用了jadx-gui工具。
用jadx打开教程apk,直接搜索大会员,轻松定位到要的位置
28
记下这个类名m86onCreate$lambda2,转到smali界面找到这个类,再找到中文当前已经是大会员了哦!所在的地方
由于此处把中文转换成了unicode编码,所以要自行转换
29
30
这两段代码是对应的,所以只要修改这一段逻辑即可

再进入MT管理器中,按照同样的方法在dex中找到对应逻辑,有如下几种修改方式

  1. 直接删除if语句,去除判断
  2. 修改if语句判断变量vip,使得该值由f变成t,让if成立
  3. 把if语句改为goto语句,强制跳转

修改完后重新安装,成功一键三连
31


004第四节.恭喜你获得广告&弹窗静默卡

本节将注重对应用中广告弹窗的去除。

广告类型

广告大致可以分为以下三类

  1. 启动广告,即打开软件时最开始的一个广告,一般是全屏的
  2. 弹窗&更新广告,弹窗广告比较常见,可以是等30s的也可以是给几个选项选的,反正就是跳出来一个别的窗口给你喂广告的;更新广告也大差不差,但是由于更新广告检测的是版本号,所以去除的办法还要多一些
  3. 横幅广告,这种广告大体就是一张图片,遮盖住了页面中一些内容

安卓四大组件

  1. Activity(活动),在应用中的一个Activty可以用来表示一个界面,意思可以理解为“活动”,即一个活动开始,代表 Actvity组件启动,活动结束,代表一个Activity的生命周期结束。一个Android应用必须通过Activity来运行和启动,Activity的生命周期交给系统统一管理。
  2. Service(服务),Service它可以在后台执行长时间运行操作而没有用户界面的应用组件,不依赖任何用户界面,例如后台播放音乐,后台下载文件等。
  3. Broadcast Receiver(广播接收器),一个用于接收广播信息,并做出对应处理的组件。比如我们常见的系统广播:通知时区改变、电量低、用户改变了语言选项等。
  4. Content Provider (内容提供者),作为应用程序之间唯一的共享数据的途径,ContentProvider主要的功能就是存储并检索数据以及向其他应用程序提供访问数据的接口Android内置的许多数据都是使用Content Provider形式,供开发者调用的(如视频,音频,图片,通讯录等)

XML&Activity

每个安卓应用中都会有一个xml文件,这个xml文件的作用就是列出应用中所有的activity,并告知系统,要从哪个activity开始执行。
下面就是教程apk的xml文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="32"
    android:compileSdkVersionCodename="12"
    package="com.zj.wuaipojie"
    platformBuildVersionCode="32"
    platformBuildVersionName="12">
    <uses-sdk
        android:minSdkVersion="27"
        android:targetSdkVersion="32"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application
        android:theme="@style/Theme.Wuaipojie"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:extractNativeLibs="false"
        android:fullBackupContent="@xml/backup_rules"
        android:networkSecurityConfig="@xml/network_config"
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
        android:dataExtractionRules="@xml/data_extraction_rules">
        <activity
            android:name="com.p001zj.wuaipojie.p002ui.ChallengeSixth"
            android:exported="false"/>
        <activity
            android:name="com.p001zj.wuaipojie.p002ui.ChallengeFifth"
            android:exported="true"/>
        <activity
            android:name="com.p001zj.wuaipojie.p002ui.ChallengeFourth"
            android:exported="true"/>
        <activity
            android:name="com.p001zj.wuaipojie.p002ui.ChallengeThird"
            android:exported="false"/>
        <activity
            android:name="com.p001zj.wuaipojie.p002ui.ChallengeSecond"
            android:exported="false"/>
        <activity android:name="com.p001zj.wuaipojie.p002ui.AdActivity"/>
        <activity
            android:label="@string/app_name"
            android:name="com.p001zj.wuaipojie.p002ui.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.p001zj.wuaipojie.p002ui.ChallengeFirst"/>
    </application>
</manifest>

其中,带</intent-filter>的这一段就是这个apk的初始Activity,整个应用从这个活动开始运行,并通过这个活动来调用其他的活动

<activity
            android:label="@string/app_name"
            android:name="com.p001zj.wuaipojie.p002ui.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

在xml的头部还有一个数据部分

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    android:compileSdkVersion="32"
    android:compileSdkVersionCodename="12"
    package="com.zj.wuaipojie"
    platformBuildVersionCode="32"
    platformBuildVersionName="12">

可以看到在这一部分中标明了版本号等信息

而其他部分则类似于对活动的一个声明或罗列

去除广告思路

  1. 既然这个xml文件中表明了应用的初始活动,那么我们可以通过更改这个初始活动,从而更改应用的入口,也即可以跳过产生启动广告的活动(但是这种修改xml文件的方法极其不稳定,因为应用往往会在启动活动中安置一些数据的初始化操作,如果直接跳过了启动活动,那么这些数据将不能被成功的初始化)

  2. 由xml文件的结构得知,各个活动的调用可以视作为函数间的调用,那么我们也可以以借助交叉引用,来找到对应调用广告活动的位置,通过加入强制跳转或者其他修改,来防止启动&弹幕广告的产生

  3. 关于更新广告,前面也提到过,在xml文件的头部表明了软件的版本号信息,那么只需要修改xml文件就能绕过更新广告

  4. 横幅广告的修改相对复杂,需要找到产生横幅的代码,一种方法就是直接注释掉这部分广告代码,还有一种方法就是,由于横幅广告实质上是在软件的页面中加入一张图片,所以写代码把这张图片隐藏,或是把图片的高和宽修改成0,就能做到横幅广告的隐藏

实际操作


启动广告#1

修改xml文件
因为启动广告是代码的起始点,所以可以通过修改xml文件中的起始类来跳过启动广告

启动广告#2

修改代码跳转
首先定位到启动广告的类里,这里使用MT管理器中的Activity检测功能,发现该块名为adActivity,搜索该块
40
通过交叉引用找到这个块的前置块
41
得到大致的逻辑,即类似于一个链表,现在我们要把前块和后块相连,从而去除广告块
所以把下图中的调用地址替换即可
42

启动广告#3

修改广告时长
再回上去看adActivity,可以看到有一个数据3000,结合启动广告的等待3s,猜测这就是等待时间,修改成0后广告去除

弹窗广告#1

禁止弹窗创建
首先要找到创建弹窗的位置,这里使用算法助手来获取弹窗创建定位
32
选择开始,会自动跳转到教程demo里面,获取弹窗进程以后返回看日志
33
可以看到堆栈中第一个调用的就是onCreate方法,这个就是创建弹窗的方法,进入到MT管理器中查询这个方法
35
36
由于MT管理器中不方便把代码转换成java代码,所以这里使用jdax来查看
39
可以看到最后段有一个show()的代码段,这个show就是显示弹窗的代码,所以只要把这段代码注释掉即可

弹窗广告#2

返回键退出
部分有良心的弹窗广告是可以使用返回键去除的,但是还有一些弹窗广告会hook掉返回键,这个时候要用到算法助手里的弹窗定位功能
43

弹窗广告#3

关键词拦截
这个功能也是算法助手里的,把弹窗广告内的内容输进去,即可进行关键词拦截

横幅广告#1

修改高宽
这里要用到开发助手中的布局查看功能,找到横幅广告的图片来源
44
在dex中搜索这个图片,找到图片的id:0x7f0801ca
45
再次查询这个id的相关调用
46
47
此处可以修改宽高至0,来去除广告

横幅广告#2

强制不显示
在xml代码中加入这样一句代码即可
android:visibility="gone"


005第五节.1000-7=?&动态调试&Log插桩

教程目标完成

教程demo给出了一个类ctf题目,然后由于jeb调试真的不好用,所以这个题目我先使用的是静态分析写脚本逆向的方法来解答
看看题目,大致意思就是说找那个密钥,很常见的题型
48
因为密钥错误会输出密钥错误哦,所以直接在jdax里搜索这个字符串,定位到回显代码处
49
50
可以看到回显代码处调用了一个check方法,那么check方法就是这个代码的加密部分了

public final boolean check(String str) {
        int i = 0;
        if (!StringsKt.startsWith$default(str, "flag{", false, 2, (Object) null) || !StringsKt.endsWith$default(str, "}", false, 2, (Object) null)) {
            return false;
        }
        String substring = str.substring(5, str.length() - 1);
        Intrinsics.checkNotNullExpressionValue(substring, "this as java.lang.String…ing(startIndex, endIndex)");
        String string = SPUtils.INSTANCE.getString(this, "id", "");
        Integer valueOf = string != null ? Integer.valueOf(string.length()) : null;
        int i2 = 1000;
        Intrinsics.checkNotNull(valueOf);
        int intValue = valueOf.intValue();
        if (intValue >= 0) {
            while (true) {
                i2 -= 7;
                if (i == intValue) {
                    break;
                }
                i++;
            }
        }
        String encode = Encode.encode(string + i2);
        Base64Utils base64Utils = Base64Utils.INSTANCE;
        byte[] bytes = encode.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
        return Intrinsics.areEqual(substring, base64Utils.encodeToString(bytes));
    }

这段 Java 代码定义了一个名为check的方法,该方法接受一个字符串参数str,用于验证该字符串是否符合特定的格式和条件。具体步骤如下:

  1. 检查字符串是否以flag{开头且以}结尾。
  2. 提取字符串中flag{}之间的子字符串。
  3. 从SPUtils中获取一个字符串id,并计算其长度。
  4. 根据id的长度进行一个循环操作,每次循环将变量i2减去 7。
  5. 将id和最终的i2拼接后进行未知编码操作。
  6. 对编码结果进行 Base64 编码,并将其与之前提取的子字符串进行比较,返回比较结果。
    由于第五步骤中的编码操作未知,
    String encode = Encode.encode(string + i2);
    再次步进去看这个encode方法
package com.p001zj.wuaipojie;

/* loaded from: classes.dex */
public class Encode {
    public static String encode(String str) {
        int length = str.length();
        char[] cArr = new char[length];
        int i = length - 1;
        while (i >= 0) {
            int i2 = i - 1;
            cArr[i] = (char) (str.charAt(i) ^ '5');
            if (i2 < 0) {
                break;
            }
            i = i2 - 1;
            cArr[i2] = (char) (str.charAt(i2) ^ '2');
        }
        return new String(cArr);
    }
}

是一个分奇偶位的简单异或加密方法

再次看SPUtils中调用的名为id的数据,这个数据可以通过对SPUtils的交叉引用,查询向这个表内填充数据的语句,最后发现一个资源id,再对这个id进行交叉引用,发现这个id就是在注册这个应用时写下的用户名,所以每个用户的密钥是不同的

51
52
53

至此可以写脚本进行解密

import base64

def custom_encrypt_or_decrypt(s):
    result = []
    length = len(s)
    index = length - 1
    while index >= 0:
        result.insert(0, chr(ord(s[index]) ^ ord('5')))
        index -= 1
        if index >= 0:
            result.insert(0, chr(ord(s[index]) ^ ord('2')))
            index -= 1
    return ''.join(result)

def full_encrypt(plain_text):
    # 先进行自定义加密
    custom_encrypted = custom_encrypt_or_decrypt(plain_text)
    # 将自定义加密后的字符串转换为字节类型
    byte_data = custom_encrypted.encode('utf-8')
    # 进行 Base64 编码
    base64_encoded = base64.b64encode(byte_data)
    # 将 Base64 编码后的字节数据转换为字符串
    return base64_encoded.decode('utf-8')

def full_decrypt(encrypted_text):
    # 将 Base64 编码的字符串转换为字节类型
    base64_decoded = base64.b64decode(encrypted_text)
    # 将 Base64 解码后的字节数据转换为字符串
    custom_encrypted = base64_decoded.decode('utf-8')
    # 进行自定义解密
    return custom_encrypt_or_decrypt(custom_encrypted)


# 测试代码

read_string = "正己"
original_string = read_string + str(1000 - 7 * (len(read_string) + 1) )

# 完整加密操作
encrypted_string = full_encrypt(original_string)
print("完整加密后的字符串:", encrypted_string)
#encrypted_string="5q2W5beDDAUM"
# 完整解密操作
decrypted_string = full_decrypt(encrypted_string)
print("完整解密后的字符串:", decrypted_string)

直接获取加密flag,填充到flag头里面提交,正确


正文

配置java环境

  1. 下载jdk文件并安装
  2. 配置java_path

什么是动态调试

动态调试是指自带的调试器跟踪自己软件的运行,可以在调试的过程中知道参数或者局部变量的值以及履清代码运行的先后顺序。多用于爆破注册码(CTF必备技能)

动态调试步骤

修改debug权限

方法一:在AndroidManifest.xml里添加可调试权限,即加入这条指令android:debuggable="true"

方法二:XappDebug模块hook对应的app
XappDebug项目地址

方法三:Magisk命令(重启失效)

  1. adb shell #adb进入命令行模式
  2. su #切换至超级用户
  3. magisk resetprop ro.debuggable 1
  4. stop;start;#一定要通过该方式重启

方法四:刷入MagiskHide Props Config模块