KGBMessager解密

xiaoeryu Lv5

本章主要内容为:

  1. spawn/attach时机的选择
  2. 主动调用
  3. hook时机选择
  4. 制作自己的dex并动态加载
  5. 使用Z3求解/符号执行

本章用KGB Messenger为案例来进行

  • 弹窗提示只能运行在Russian的设备上,回头可以再jadx里面搜索这个字符串定位

惯例先用jadx打开拿到包名,然后用objection查看一下基本信息

  • 查看activity,都可以直接跳转过去

开始分析

通过地区校验

这个地区校验比较简单,只是检查一下主目录

  • 搜索字符串可以直接定位的到

定位到这里之后

  • 可以看到这里有两次校验,校验通过才能进入到LoginActivity.class

    第一次校验:System.getProperty方法的返回值等于“Russia”,就能通过

    第二次校验:System.getenv方法的返回值等于白名单中的值,就能通过

第一次校验:
  • hookSystem.getProperty方法

    • 在Smali代码中可以看到它的包名和重载参数
    • 可以写脚本hook返回值
第二次校验:
  • 根据代码中的getResources()我们可以知道白名单在资源文件中,位置通常在 res/values/strings.xml 文件中

校验结果

写脚本直接修改返回值就可以,之前几篇文章写过很多次了

  • 通过之后就可以看到app已经进入到了登陆界面

登陆

先随便输入账号密码,看看有什么提示

提示:“User not recognized”

那我们就先再jadx中尝试定位这个字符串,在LoginActivity中定位到了这个字符串

  • 在这个校验中我们需要先通过用户名的验证:

    • 用户名来也来自资源文件,可以直接去刚才找白名单的地方找
  • 然后是密码的验证

    • 通过j()方法的校验就可以,那我们去看一下这个方法

      • 这里可以看到是对传进来的密码进行了一个摘要算法,然后看结果跟资源文件中保存的password是否相等
      • 去看看资源文件中的password是什么
      • 用户名是明文:codenameduchess

      • password是一个摘要:84e343a0486ff05530df6c705c8bb4

      • 拿这个摘要去网上搜索一下看能不能搜到它的明码

验证登陆

输入我们拿到的用户名和密码

  • 通过校验,进入了聊天界面,拿到了FLAG

登陆完成进入到了聊天框,接下来考虑怎么输入正确的”接头暗号“获取它们都知道的密码

先搜索这些聊天的字符串,看能定位到什么

  • 看这里的代码逻辑首先,我们先让①、②处的校验通过,然后再保证我们输入的字符串能通过③处的校验

①和②处的校验很简单,我们只需要修改a()b()的返回值就可以通过校验,可以先试一下

修改返回值的hook脚本

    Java.use('com.tlamb96.kgbmessenger.MessengerActivity').a.implementation = function(x){
        console.log(x)
        return Java.use('java.lang.String').$new('V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003')
    }
    Java.use('com.tlamb96.kgbmessenger.MessengerActivity').b.implementation = function(x){
        console.log(x)
        return Java.use('java.lang.String').$new('\u0000dslp}oQ\u0000 dks$|M\u0000h +AYQg\u0000P*!M$gQ\u0000')
    }
  • 这里比较简单就能通过

然后我们来看③处的i()方法

  • 要对我们输入进来的qs进行校验,用返回的值作为FLAG
  • 这里没其它办法我们只能来逆向这个算法,拿到这两个正确的字符串了,如果通过hook直接过来,这两个字符串是空它会嘲笑我们(╬▔皿▔)╯

算法逆向

这两个字符串分别是通过a()b()校验的,一个一个来分析

a()

  • 我们输入的字符串经过a()运算之后需要等于p的值

看一下a()的算法

  • 这只是一个简单的异或算法,根据异或的特性我们知道只需要把结果反过来异或一遍,就能得到原字符串
编译dex,并主动调用通过验证

用Android studio创建一个空的java项目,新建一个类

public class reverseA {
    public static String decode_P(){
        String p = "V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003";
        String result = a(p);
        return result;
    }
    private static String a(String str) {
        char[] charArray = str.toCharArray();
        for (int i = 0; i < charArray.length / 2; i++) {
            char c = charArray[i];
            charArray[i] = (char) (charArray[(charArray.length - i) - 1] ^ 'A');
            charArray[(charArray.length - i) - 1] = (char) (c ^ '2');
        }
        return new String(charArray);
    }
}
  • 将这段代码编译为dex,push到手机上试一下

    1. 写完代码之后按ctrl + F9构建

    2. 然后再文件中找到这个class文件,用d8工具打包成dex

    3. push到手机上

写个js脚本调用这个dex
Java.perform(function(){
    Java.openClassFile("/data/local/tmp/classes.dex").load();
    var reverseA =Java.use("com.xiaoeryu.lesson9.reverseA");
    console.log(reverseA.decode_P());
}); 

计算结果:

测试:

  • 测试ok
  • 接下来分析第二个字符串

b()

  • 同样,这里也是将我们输入的字符串经过b()运算后要等于r的值

看一下b()方法

  • 可以看到这个算法就比之前的稍微要麻烦一点,里面有移位、异或、逆序交换

  • 我们先来分析一下

    总共分为两部分:

    1. ((charArray[i] >> (i % 8)) ^ charArray[i])这是一个简单的位运算,通过右移然后异或,对字符进行了加密
    2. 第二部分是将加密后的字符进行逆序交换

分析完成了我们来写一下它的解密算法:写python脚本暴力枚举所有字符就可以。当然写在Android Studio写成Java代码编译成一个dex用注入的方式获取同样也是可以的,代码放在附件中(注入的流程跟解密a()方法相同)。

characterSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r"
characterSetArray = list(characterSet)

EnStr = "\000dslp}oQ\000 dks$|M\000h +AYQg\000P*!M$gQ\000"
EnStrArray = list(EnStr)

# 把EnStrArray进行逆序交换
for i in range(int(len(EnStrArray)/2)):
    c = EnStrArray[i]
    EnStrArray[i] = EnStrArray[len(EnStrArray)-i-1]
    EnStrArray[len(EnStrArray)-i-1] = c

# 暴力枚举EnStrArray的明文
raw_text = ""
for i in range(len(EnStrArray)):
    for j in range(len(characterSetArray)):
        c = characterSetArray[j]
        result = chr((ord(c) >> (i % 8)) ^ ord(c))
        if result == EnStrArray[i]:
            raw_text += c
            break

print(raw_text)

执行结果:

将结果输入:

android_studio编写部分的代码


Z3

本来b()是打算用Z3解的,但是打印字符串结果的时候一致提示溢出,只好自己手动解了。这里记录一下流程和思路

因为r字符串有很多不可见字符串,先把它变为可见字符串

同样的,构建 > d8编译为dex > push到手机 > 执行脚本

脚本也添加一个console.log

然后,对与它的算法这里我们暂时先不分析,用Z3试试能不能直接解开

  • Z3是一个基于符号执行的函数求解的工具
from z3 import *
from binascii import b2a_hex, a2b_hex

# 创建一个Z3 solver对象
s = Solver()

# 待解密的十六进制字符串
r = "0064736c707d6f510020646b73247c4d0068202b4159516700502a214d24675100"

# 将十六进制字符串转换为字节数组
r_result = bytearray(a2b_hex(r))
print(r_result)

# 逆序交换字节数组中的每个字节
for i in range(int(len(r_result)/2)):
    c = r_result[i]
    r_result[i] = r_result[len(r_result)-i-1]
    r_result[len(r_result)-i-1] = c
print(b2a_hex(r_result))

# 创建Z3的BitVec变量列表,每个变量代表一个字节
x = [BitVec("x%s" % i, 32) for i in range(len(r_result))]

# 添加Z3 solver的约束条件
for i in range(len(r_result)):
    c = r_result[i]
    # 将解密过程表示为Z3约束
    s.add(((x[i] >> (i % 8)) ^ x[i]) == r_result[i])

# 检查是否存在解
if s.check() == sat:
    # 获取解
    model = s.model()
    print(model)

    # 解密后的整数值列表
    decoded_values = []
    for i in range(len(r_result)):
        if model[x[i]] is not None:
            decoded_values.append(model[x[i]].as_long().real)
        else:
            decoded_values.append(0)
    print("Decrypted as integers:", decoded_values)

    # 解密后的十六进制表示列表
    print("Decrypted as hex:", [hex(val) for val in decoded_values])

# 注释掉的部分是将解密后的整数值转换为字符串的代码
# if (s.check() == sat):
#     model = s.model()
#     print(model)
#     flag = ""
#     for i in range(len(r_result)):
#         if (model[x[i]] != None):
#             flag += chr(model[x[i]].as_long().real)
#         else:
#             flag += " "
#     print('"' + flag + '"')
#     print(len(flag), len(r_result))
  • 整数和16进制形式打印

  • 字符串形式打印出错

参考文章:

KGB Messenger解题流程

  • 标题: KGBMessager解密
  • 作者: xiaoeryu
  • 创建于 : 2023-11-26 16:49:53
  • 更新于 : 2023-11-26 20:51:15
  • 链接: https://github.com/xiaoeryu/2023/11/26/KGBMessager解密/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论