在开始这篇文章之前,请阅读一下注意事项
本文章仅用于学习研究,如有侵犯贵司权益,请联系告知,会立即做出下架删除处理。
本文章不针对任何网站,APP ,禁止用于商业,转载,违法等用途。
观看则默认同意本约定,未经允许禁止任何形式的转载,保留追究法律责任的权利,
Unidbg模板
package com;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.github.unidbg.virtualmodule.android.JniGraphics;
import com.github.unidbg.virtualmodule.android.MediaNdkModule;
import com.github.unidbg.virtualmodule.android.SystemProperties;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class 模板 extends AbstractJni implements IOResolver {
private static final String PackName = "APP 包名";
private static final String AppPath = "传入 APP路径";
private static final String[] SoName = {"SO名字或外部路径"}; // 可能会出现多个SO文件加载的情况
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
final Memory memory;
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("Load File: " + pathname);
return null;
}
private static LibraryResolver createLibraryResolver() {
return new AndroidResolver(23);
}
private static AndroidEmulator createARMEmulator() {
return AndroidEmulatorBuilder.for64Bit()
.setProcessName(PackName)
.addBackendFactory(new Unicorn2Factory(false))
.build();
}
模板 () {
emulator = createARMEmulator();
// 创建模拟器的内存映射
emulator.getSyscallHandler().addIOResolver(this);
// 获取模拟器的内存操作接口
memory = emulator.getMemory();
// 设置系统类库解析 23
memory.setLibraryResolver(createLibraryResolver());
// 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
vm = emulator.createDalvikVM(new File(AppPath));
// 虚拟模块部分
new AndroidModule(emulator,vm).register(memory);
new MediaNdkModule(emulator,vm).register(memory);
new JniGraphics(emulator,vm).register(memory);
// 设置JNI
vm.setJni(this);
// 打印日志
vm.setVerbose(true);
// 加载目标SO
DalvikModule dm = vm.loadLibrary(SoName[0], true);
//获取本SO模块的句柄,后续需要用它
module = dm.getModule();
// 调用JNI OnLoad
dm.callJNI_OnLoad(emulator);
};
public static void main(String[] args) {
模板 action = new 模板();
action.Call();
}
public void Call () {
List<Object> args = new ArrayList<>();
args.add(vm.getJNIEnv()); // JNIEnv *
args.add(0);
args.add(vm.addLocalObject(new StringObject(vm, "123456"))); // 输入字符串
args.add(vm.addLocalObject(new StringObject(vm, ""))); // 输入字符串
Number result = module.callFunction(emulator, 0x236c, args.toArray());
DvmObject<?> returnObj = vm.getObject(result.intValue());
String encrypted = ((StringObject) returnObj).getValue();
System.out.println("Encrypted String: " + encrypted);
}
}
逻辑分析
从这章开始, 算法不会只用Unidbg 或者是 看伪C 进行分析
加密输出结果如下:
该 So 就是静态注册的方法 Java_com_nice_main_helpers_utils_NiceSignUtils_getSignRequest
静态注册的方式 Java_开头 完成静态注册
动态注册的方法 在 JNI_OnLoad 方法中完成的动态注册
第一步咱们进去函数后发现一个名为nice_sign_v3的函数 这个就非常可疑 咱们进去看下
进入nice_sign_v3函数之后我们看到了MD5加密 这里可能是加密的核心位置 我们去用unidbg hook一下他的入参
我们下面可以清晰的看到他的入参可以分为三个参数,v16=body,v20=设备did(md5加密过的),v11=随机16位字符串
上面代码可以看到 他进行两次nice_md5 那么我们去hook下 入参值
Nice_MD5
第一次 Nice_MD5的入参 我们可以通过图片看到 他的入参值就是v20的前半部分和后半部分颠倒位置
第一次 Nice_MD5的入参 的结果
颠倒前: 66e465b27596fe73ac148ce8993c9eaf
颠倒后: ac148ce8993c9eaf66e465b27596fe73
第二次 Nice_MD5的入参 我们可以通过图片看到 他的入参值就是v11+第一次 Nice_MD5的入参结果+8a5f746c1c9c99c0b458e1ed510845e5
拼接的规则
[zflq7vua9alemnwb] + [66e465b27596fe73ac148ce8993c9eaf] + [8a5f746c1c9c99c0b458e1ed510845e5]
第二次 Nice_MD5的入参 的结果
颠倒前: b56ebb1f9af72cfed5eaacb028eb05cd
颠倒后: d5eaacb028eb05cdb56ebb1f9af72cfe
然后对第二次 Nice_MD5的入参 的结果进行前半部分和后半部分颠倒位置
明文处理部分以及nice_sha1
第一步 我们通过图片可以看到几个函数
cJSON_Parse,sub_25B0,nice_sha1
通过代码可以看到 入参的明文数据传入
cJSON_Parse进行JSON解析操作得到了v38
sub_25B0函数传入了v38得到v40的结果 那么我
们现在去hook下sub_25B0返回的结果
v40 = sub_25B0返回的结果入下:
通过结果可以看到 我们的明文参数从JSON格式变成了application/x-www-form-urlencoded格式并进行升降排序
v40 = "block_version=0&device_model=MI 9&items=config=&post=&proto_version=0.1"
根据下面图片我们可以看到v40进行了一系列运算给了v21 下面我们先去HOOK下 nice_sha1 之前的v21长什么样子
根据下面图片我们可以看到这个时候v21已经变成乱码和第二次 Nice_MD5的入参 前半部分和后半部分颠倒位置
从上面代码我们呢可以看到v21[46]=0 就代表他可能是在这个位置做了截断
接下来我们就用py把这个nice_sha1加密之前部分先给他还原了 看看加密出来的参数一样不
通过上面图片我们可以清晰看到一模一样
下面我们就进行nice_sha1操作对比下结果
unidbg nice_sha1加密结果
py运行结果
通过上面图片我们可以清晰看到一模一样
下面我们就进行nice_sha1操作对比下结果
unidbg nice_sha1加密结果
py运行结果
我们通过图片对比发现 nice_sha1加密已经对比上了 但是和开头的结果还是不一样 我们继续去看代码
我们可以看到他从v48+8的位置才开始获取 然后又进行了一次前半部分和后半部分颠倒位置
我们现在用py写个代码试试看看
我们发现已经和开头已经对上了
这里我就不再详细写Python 怎么一步一步复现了,直接附上写好的py代码
现在再回头看整个算法 难么? 不难 !
import hashlib
def process_strings(v40, v67):
v21 = bytearray(1024)
v41 = 0
if len(v40) < 2:
v46 = 32
else:
v42_idx = 1
v43 = 0x2000000000
loop_count = len(v40) >> 1
while v41 < loop_count:
if v42_idx - 1 >= len(v40) or v42_idx >= len(v40):
break
v44 = ord(v40[v42_idx - 1])
v45 = ord(v40[v42_idx])
combined = (v44 & 0xF0) | (v45 & 0xF)
v21[v41] = combined
v41 += 1
v42_idx += 2
v43 += 0x100000000
v46 = v43 >> 32
v67_bytes = v67.encode('utf-8')
for i in range(32):
if i < len(v67_bytes):
v21[v41 + i] = v67_bytes[i]
else:
v21[v41 + i] = 0
if v46 < len(v21):
v21[v46] = 0
else:
pass
print("Output:", v21)
result = v21.split(b'\x00', 1)[0]
return result
def sha1_bytes(data: bytes) -> str:
hash_obj = hashlib.sha1(data)
return hash_obj.digest().hex()[8:]
def swap(s):
n = len(s)
mid = n // 2
return s[mid:] + s[:mid]
v40 = "block_version=0&device_model=MI 9&items=config=&post=&proto_version=0.1"
v67 = "d5eaacb028eb05cdb56ebb1f9af72cfe"
output = process_strings(v40, v67)
print("Output:", output)
digest_bytes = sha1_bytes(output)
print("Final Output (bytes):", digest_bytes)
print("Final Output (bytes):", swap(digest_bytes))
末尾
后续 也会推出一系列的文章,包括但不限于 魔改算法,VMP 系列,小众算法
通过私信联系获取加群方式