SO逆向之蚂蜂窝zzzghostsigh

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

v9.3.7抓包

postern+charles

image-20211125144322843

jadx反编译搜索zzzghostsigh找到params.put(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH, ghostSigh)

1
2
3
4
5
private static String appendGhostSighParams(Map<String, String> params, String encodeParams) {
String ghostSigh = ghostSigh(encodeParams);
params.put(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH, ghostSigh);
return encodeParams + encodeUrl("&") + encodeUrl(LoginCommon.HTTP_BASE_PARAM_GHOSTSIGH) + encodeUrl("=") + encodeUrl(ghostSigh);
}

进入ghostSighAuthorizer接口的一个方法

image-20211125145204234

1
2
adb shell dumpsys activity activities|more   拿到包名com.mfw.roadbook
frida -UF com.mfw.roadbook -l scripts.js 在searchInterface中使用接口名过滤name.indexOf("Authorizer")

image-20211204142501597

利用frida找到该类的实现,ghostSigh的逻辑就是获取AuthorizeHelper实例house调用getSummary方法

1
2
3
4
5
public String ghostSigh(@Nullable Context context, @Nullable String params) {
String summary = AuthorizeHelper.getInstance(LoginCommon.getAppPackageName()).getSummary(context, params);
Intrinsics.checkExpressionValueIsNotNull(summary, "AuthorizeHelper.getInsta…tSummary(context, params)");
return summary;
}

跟进getSummary,其中调用了so层的xPreAuthencode方法

1
2
3
4
5
6
7
private native String xPreAuthencode(Context context, String str, String str2);
public String getSummary(Context context, String source) {
return xPreAuthencode(context, source, this.packageName);
}
static {
System.loadLibrary("mfw");
}

使用objection内存漫游跟踪下xPreAuthencode

1
2
objection -g com.mfw.roadbook explore -P ~/.objection/plugins
android hooking watch class_method com.mfw.tnative.AuthorizeHelper.xPreAuthencode --dump-args --dump-backtrace --dump-return

它接收三个参数,参数1是一个context,参数2是输入的明文,参数3是app的包名

image-20211204143302199

主动调用

frida

构造context,主动调用动态普通函数xPreAuthencode返回b76da45d4855c8f405ec1352955fe58d3ef0adca

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mfw(){
Java.perform(function(){
var currentApplication = Java.use('android.app.ActivityThread').currentApplication();
var context = currentApplication.getApplicationContext();
Java.choose("com.mfw.tnative.AuthorizeHelper",{
onMatch:function(instance){
console.log("found instance =>",instance);
console.log("instance showText is =>",instance.xPreAuthencode(context,"onejane","com.mfw.roadbook"))
},onComplete:function(){
console.log('Search complete')
}
})
})
}
setImmediate(mfw)

image-20211204144603984

unidbg

搭建Unidbg框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class mfw extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;

mfw() {
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // 创建模拟器实例
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(new File("mafengwo_ziyouxing.apk")); // 创建Android虚拟机
DalvikModule dm = vm.loadLibrary(new File("libmfw.so"), true); // 加载so到虚拟内存
module = dm.getModule(); //获取本SO模块的句柄

vm.setJni(this);
vm.setVerbose(true);
dm.callJNI_OnLoad(emulator);
};

public static void main(String[] args) throws Exception {
mfw test = new mfw();
}
}

image-20211204145131449

JNI_OnLoad运行成功并打印出xPreAuthencode的地址为0x2e301

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public String xPreAuthencode(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。
Object custom = null;
DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context
list.add(vm.addLocalObject(context));
list.add(vm.addLocalObject(new StringObject(vm, "onejane")));
list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook")));

Number number = module.callFunction(emulator, 0x2e301, list.toArray())[0];
String result = vm.getObject(number.intValue()).getValue().toString();
return result;
}

public static void main(String[] args) throws Exception {
mfw test = new mfw();
System.out.println(test.xPreAuthencode());
}

image-20211204150101860

算法还原

由于明文不论多长输出固定为40位字符串,疑似哈希算法中的SHA1。使用IDA的FindHash跑完获取到自动生成的js

1
frida -UF com.mfw.roadbook -l libmfw_findhash_1638601602.js

image-20211204152742933

g跳转到地址0x30548,x交叉引用,进入sub_2E300函数,正是xPreAuthencode函数的位置0x2e301

image-20211204152915288

image-20211204153516895

y设置变量a1类型为JNIEnv*,其中sub_30548一定是签名校验函数,如果失败返回Illegal signature,这个逻辑在Unidbg模拟执行时传入的apk替我们处理了签名校验。那么加密逻辑一定存在于sub_312E0或者sub_2E1F4中。

image-20211204163853404

sub_312E0第一个参数v9是字符串,第二个参数v13是字符数组,第三个参数v10是字符串长度,使用HookZz尝试对sub_312E0hook并打印参数

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
public void hook_312E0(){
// 获取HookZz对象
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
// enable hook
hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
// hook MDStringOld
hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer input = ctx.getPointerArg(0);
byte[] inputhex = input.getByteArray(0, ctx.getR2Int());
// 打印第一个参数v9 字符串
Inspector.inspect(inputhex, "input");

// 将第二个参数v13放到context
Pointer out = ctx.getPointerArg(1);
ctx.push(out);
};

@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer output = ctx.pop();
// 打印第二个参数v13[20]
byte[] outputhex = output.getByteArray(0, 20);
Inspector.inspect(outputhex, "output");
}
});
hookZz.disable_arm_arm64_b_branch();
}

image-20211204172027073

sub_312E0第一个参数v9正是我们输入的字符串onejane,第二个参数返回就是加密的最终结果。进入sub_312E0函数

image-20211204172305421

h将数值转为十六进制

1
2
3
4
5
v30[1] = 0xEFCDAB89;
v30[0] = 0x67452301;
v30[2] = 0x98BADCFE;
v30[3] = 0x5E4A1F7C;
v30[4] = 0x10325476;

查看androidxref的sha1.c源码,SHA1的魔数的第4个和第5个IV被改变了

image-20211204174843212

我们尝试将sha1.py中的魔数成本例中的结果,很可惜结果并不是完全一致。

image-20211204175737910

在函数sub_312E0中,n重命名入参,除了定义魔数之外,多处调用了sub_3151C函数,该函数591行,应该就是函数运算部分,加上sub_312E0共700多行,其中完成了对函数的魔改。

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
int __fastcall sub_312E0(char *input, int output, int length)
{
...
v30[1] = 0xEFCDAB89;
v30[0] = 0x67452301;
v30[2] = 0x98BADCFE;
v30[3] = 0x5E4A1F7C;
v30[4] = 0x10325476;
v4 = 0;
v32 = 0;
v31 = 0;
...
if ( (unsigned int)(v21 + 8) < 0x40 )
{
v23 = 0;
}
else
{
v27 = 64 - v21;
qmemcpy(&v33[v21], v29, 64 - v21);
sub_3151C(v30, v33);
v23 = v27;
v21 = 0;
}
qmemcpy(&v33[v21], &v29[v23], 8 - v23);
for ( k = 0; k != 20; ++k )
{
*(_BYTE *)(output + k) = *(unsigned int *)((char *)v30 + (k & 0xFFFFFFFC)) >> (~(_BYTE)v22 & 0x18);
v22 += 8;
}
return _stack_chk_guard - v35;
}

image-20211204182825242

一个哈希算法主要流程有数据填充、添加长度、初始化变量、数据处理、输出,我们可以尝试通过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
public void hook_3151C(){
// 获取HookZz对象
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
// enable hook
hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
// hook MDStringOld
hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 方法执行前
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Pointer input = ctx.getPointerArg(0);
byte[] inputhex = input.getByteArray(0, 20);
Inspector.inspect(inputhex, "IV");

Pointer text = ctx.getPointerArg(1);
byte[] texthex = text.getByteArray(0, 64);
Inspector.inspect(texthex, "block");
ctx.push(input);
ctx.push(text);
};

@Override
// 方法执行后
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
Pointer text = ctx.pop();
Pointer IV = ctx.pop();

byte[] IVhex = IV.getByteArray(0, 20);
Inspector.inspect(IVhex, "IV");

byte[] outputhex = text.getByteArray(0, 64);
Inspector.inspect(outputhex, "block out");

}
});
hookZz.disable_arm_arm64_b_branch();
}

image-20211204184216379

运算函数入参正常,都是填充的明文,不存在自定义填充或者对明文变换的可能,而出参直接返回结果,所以算法不是在标准流程后魔改,而是修改了算法本身。

SHA1和MD5采用了相同的结构,每512比特分组需要一轮运算,我们的输入长度不超过一个分组的长度,所以只用考虑一轮运算。一轮运算是80步,每隔20步是一种模式。

image-20211213162040575

HookZz实现Inline hook

1
2
3
4
5
6
7
8
9
10
11
public void hook_315B0(){
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.enable_arm_arm64_b_branch();

hookZz.instrument(module.base + 0x315B0 + 1, new InstrumentCallback<Arm32RegisterContext>() {
@Override
public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
System.out.println("R2:"+ctx.getR2Long());
}
});
}

但我们整体上需要进行十数次甚至数十次的inline hook,在这种情况下,用HookZz就略有些不方便,不妨试试Unidbg的console debugger。

image-20211213163548154

样本中80步运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for t in range(80):
if t <= 15:
K = 0x5a827999
f = (b & c) ^ (~b & d)
elif t <= 19:
K = 0x6ed9eba1
f = b ^ c ^ d
elif t <= 39:
K = 0x8f1bbcdc
f = (b & c) ^ (b & d) ^ (c & d)
elif t <= 59:
K = 0x5a827999
f = (b & c) ^ (~b & d)
else:
K = 0xca62c1d6
f = b ^ c ^ d

在标准流程中,20步切换一下K和非线性函数,一共4种模式,在样本中,每16步切换一下K和非线性函数,一种五种模式,但本质上依然是标准流程里的四个模式,因为一个模式用了两次。

文章作者: J
文章链接: http://onejane.github.io/2021/11/25/SO逆向之蚂蜂窝zzzghostsigh/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏