android so檔案攻防實戰-libDexHelper.so反混淆

語言: CN / TW / HK

本文為看雪論壇精華文章

看雪論壇作者ID:houjingyi

計劃是寫一個android中so檔案反混淆的系列文章,目前這是第三篇。

第一篇:android so檔案攻防實戰-百度加固免費版libbaiduprotect.so反混淆( http://bbs.pediy.com/thread-271388.htm

第二篇: android so檔案攻防實戰-某團libmtguard.so反混淆( http://bbs.pediy.com/thread-271853.htm

今天分析的是企業版64位,我用LibChecker查了一下手機上的APP找到的,時間也還比較新。根據其他人的分析可知,libDexHelper.so是指令抽取的實現,libdexjni.so是VMP的實現。

去除混淆

首先因為加密過,肯定是不能直接反編譯的,可以在libart.so下斷點,進入JNI_onLoad以後就可以dump下來。

不過此時也不能直接F5,還存在以下混淆方式:

1.垃圾指令

這些垃圾指令是在switch的一個永遠不會被執行到的分支裡面,可以直接將IDA不能MakeCode的地方patch成NOP再MakeCode。

2.字串加密

有好幾個解密字串的函式,0x186C4,0x7783C,0x95B9C。在android so檔案攻防實戰-百度加固免費版libbaiduprotect.so反混淆( http://bbs.pediy.com/thread-271388.htm )中我們是交叉引用拿到加密後的字串和它對應的解密函式的表然後frida主動呼叫得到的解密後的字串,但是在這裡這個方法就不太好用了。因為這裡加密後的字串是在棧上一個byte一個byte拼起來的,和最後呼叫解密函式之間可能隔了很多條指令,甚至都不在一個block。

我最後用的是下面這種方案:以0x40110處呼叫0x186C4處的解密函式為例,這裡面字串解密的邏輯比較簡單,需要三個引數。我們可以自己實現也可以用unicorn,我就用unicorn了。

import sys
import unicorn
import binascii
import threading
import subprocess

from capstone import *
from capstone.arm64 import *

with open("C:\\Users\\hjy\\Downloads\\out1.fix.so","rb") as f:
sodata = f.read()

uc = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM)
code_addr = 0x0
code_size = 8*0x1000*0x1000
uc.mem_map(code_addr, code_size)
stack_addr = code_addr + code_size
stack_size = 0x1000000
stack_top = stack_addr + stack_size - 0x8
uc.mem_map(stack_addr, stack_size)
uc.mem_write(code_addr, sodata)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X29, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X28, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X27, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X26, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X25, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X24, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X23, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X22, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X21, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X20, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X19, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X18, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X17, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X16, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X15, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X14, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X13, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X12, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X11, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X10, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X9, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X8, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X7, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X6, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X5, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X4, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP, stack_top)
X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0)

uc.mem_write(X0, bytes.fromhex(sys.argv[1]))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, int(sys.argv[2], 16))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, int(sys.argv[3], 16))

uc.emu_start(0x1777C, 0x17780)

X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0)
decstr = uc.mem_read(X0, 80)

print("decstr:", decstr)
uc.mem_unmap(stack_addr, stack_size)
uc.mem_unmap(code_addr, code_size)

總共有幾百處呼叫,不可能全部人工去這樣解出來,我寫了另外以一個指令碼去呼叫decstr.py。首先通過交叉引用找到所有呼叫解密函式的地方,然後把起始地址設為該block的起始地址,結束地址設為呼叫解密函式的地址,通過unicorn跑出decstr.py需要的三個引數之後呼叫decstr.py。

遇到unicorn.unicorn.UcError也有兩個處理策略,一個是跳過該地址( loop_call_prepare_arg1),起始地址不變;一個是將起始地址設為下一條地址(loop_call_prepare_arg2)。當然這套方案還有優化的空間,比如生成呼叫解密函式需要的引數的程式碼和最後呼叫解密函式的程式碼不在一個block,就處理不了。

import unicorn
import binascii
import threading
import subprocess

from capstone import *
from capstone.arm64 import *

inscnt = 0
start_addr = 0
end_addr = 0
stop_addr = 0
stop_addr_list = []

def hook_code(uc, address, size, user_data):
global inscnt
global end_addr
global stop_addr
global stop_addr_list

md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)

for ins in md.disasm(sodata[address:address + size], address):
#rint(">>> 0x%x:\t%s\t%s" % (ins.address, ins.mnemonic, ins.op_str))
stop_addr = ins.address

if ins.address in stop_addr_list:
#print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)
return

inscnt = inscnt + 1
if (inscnt > 500):
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, 0xffffffff)
return

if ins.mnemonic.find("b.") != -1:
print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)
return

if ins.mnemonic.find("bl") != -1:
print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)
return

if ins.op_str in ["x0","x1","x2","x3"]:
X1 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X1)
if X1 > 0x105A88:
print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)
return
if ins.op_str.startswith("#0x"):
addr = int(ins.op_str[3:],16)
if (addr > 0x14E50 and addr < 0x15820) \
or addr == 0x186C4 \
or addr > 0x105A88:
print("will pass 0x%x:\t%s\t%s" %(ins.address, ins.mnemonic, ins.op_str))
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_PC, address + size)
return

def call_prepare_arg():
global inscnt
global start_addr
global end_addr
global stop_addr
global stop_addr_list

inscnt = 0

uc = unicorn.Uc(unicorn.UC_ARCH_ARM64, unicorn.UC_MODE_ARM)
code_addr = 0x0
code_size = 8*0x1000*0x1000
uc.mem_map(code_addr, code_size)
stack_addr = code_addr + code_size
stack_size = 0x1000000
stack_top = stack_addr + stack_size - 0x8
uc.mem_map(stack_addr, stack_size)
uc.hook_add(unicorn.UC_HOOK_CODE, hook_code)

uc.mem_write(code_addr, sodata)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X29, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X28, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X27, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X26, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X25, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X24, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X23, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X22, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X21, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X20, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X19, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X18, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X17, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X16, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X15, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X14, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X13, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X12, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X11, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X10, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X9, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X8, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X7, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X6, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X5, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X4, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X3, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X2, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X1, stack_addr)
uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_X0, stack_addr)

uc.reg_write(unicorn.arm64_const.UC_ARM64_REG_SP, stack_top)
uc.emu_start(start_addr, end_addr)

X0 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X0)
decstr = uc.mem_read(X0, 80)
end_index = decstr.find(bytearray(b'\x00'), 1)
decstr = decstr[:end_index]

decstr = binascii.b2a_hex(decstr)
decstr = decstr.decode('utf-8')

X1 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X1)
X2 = uc.reg_read(unicorn.arm64_const.UC_ARM64_REG_X2)

pi = subprocess.Popen(['C:\\Python38\\python.exe', 'decstr.py', decstr, hex(X1), hex(X2)], stdout=subprocess.PIPE)
output = pi.stdout.read()
print(output)

def loop_call_prepare_arg1():
global inscnt
global end_addr
global stop_addr
global stop_addr_list

loopcnt = 0
stop_addr_list = []

while True:
try:
loopcnt = loopcnt + 1
if(loopcnt > 200):
break
call_prepare_arg()
except unicorn.unicorn.UcError:
print("adding....")
print(hex(stop_addr))
stop_addr_list.append(stop_addr)
else:
break

def loop_call_prepare_arg2():
global inscnt
global end_addr
global stop_addr
global stop_addr_list

global start_addr

loopcnt = 0
stop_addr_list = []

while True:
try:
loopcnt = loopcnt + 1
if(loopcnt > 200):
break
call_prepare_arg()
except unicorn.unicorn.UcError:
start_addr = stop_addr + 4
else:
break

with open("C:\\Users\\hjy\\Downloads\\out1.fix.so","rb") as f:
sodata = f.read()

all_addr = []
with open('xref_decstr.txt', 'r', encoding='utf-8') as f:
for line in f:
addr = "0x" + line[2:]
addr = int(addr, 16)
all_addr.append(addr)

for i in all_addr:

print("i:")
print(hex(i))

end_addr = i
CODE = sodata[i - 4:i]
md = Cs(CS_ARCH_ARM64, CS_MODE_ARM)
for x in md.disasm(CODE, i - 4):
mnemonic = x.mnemonic

while mnemonic != "ret" \
and mnemonic != "b" \
and mnemonic != "br" \
and mnemonic != "cbz" \
and mnemonic != "cbnz":
i = i - 4
CODE = sodata[i - 4:i]
for x in md.disasm(CODE, i - 4):
mnemonic = x.mnemonic

start_addr = i

print("start_addr:")
print(hex(start_addr))
print("end_addr:")
print(hex(end_addr))

loop_call_prepare_arg1()
loop_call_prepare_arg2()

更噁心的是還有很多字串是自己在函式內解密的,這種情況我也沒想到有什麼好的方法。

3.控制流混淆

第一種是把正常順序執行的指令打亂成switch的形式,這個影響倒不是太大:

第二種是動態計算跳轉地址,基本上類似於在android so檔案攻防實戰-某團libmtguard.so反混淆( http://bbs.pediy.com/thread-271853.htm

)見過的那種,但是要更復雜。

比如這裡的指令,在0x1DA0C處給X2賦值,X2此時為.data段中的一個地址,W0為偏移,取出值後在0x1DA18處乘4加上0x1DA20,最後的值就是0x1DA1C處X0的值。那麼需要解決這麼幾個問題:

如何確定0x1DA0C處給X2賦的值;

將0x1DA00處的指令改成跳轉指令,0x1DA00這個地址又該如何確定;

找到所有會跳轉到0x1DA1C的指令,將跳轉地址改成計算出來的X0的值。

第一個問題,其實和字串解密面臨的情況是類似的,比如這裡需要找到和"LDR X2, [X29,#0x190+var_118]"對應的"STR XX, [X29,#0x190+var_118]"這條指令,然後再找給XX暫存器賦值的指令,然而這兩條指令很可能和BR X0隔了好幾個block。我的解決方法是通過IDA提供的idaapi.FlowChar功能,遞迴前面的block查詢。不足之處在於前提條件是IDA正確識別了函式的起始地址,否則會出現我們需要的指令和BR X0不在同一個函式的情況,這樣就處理不了。

第二個問題,在遞迴前面的block的時候就先找到0x1D9D4處這條給W0賦值的指令,然後從0x1D9D4處開始直到0x1DA1C,找到第一個存在交叉引用的地址,也就是0x1DA04。它的前一條指令0x1DA00就是需要改成跳轉指令的地方。

第三個問題,確定了0x1DA00之後,那麼從0x1DA00到0x1DA1C所有存在交叉引用的地址都要去交叉引用的地方修改跳轉地址。不過這裡有很多細節。

(1)如果W0是由CSEL,CSET,CSINC這些指令賦值的,像下面這種情況,那麼需要把0x1DE80和0x1DE84修改成 B.GE和B.LT。

patch前:

patch後:

(2)0x1DE80處的CSEL W0, WZR, W8, LT,這裡W8的值是在0x1D9DC MOV W8, #5賦值的,所以我的程式碼中有一個register_value_dict,在改掉0x1DA00處的指令之後會讀取0x1DA00所在的block到0x1DA1C所在的block的所有指令,找到給暫存器賦值的指令然後把值存起來。

(3)有些地方還會有一條sub指令,這個也要考慮進去,比如下面這種情況0x33394處跳轉的地址就應該按照W8為4計算。

最後的指令碼放附件了。當然還有一些指令碼處理不了的地方,不過問題已經不算太大了,需要的話可以動態除錯確定。

4.函式地址動態計算

這個在IDA裡面是能看清楚的,v35其實就是off_12EB80[0],即呼叫0x80FE0處的p329AAB59961F6410ABA963EF972FE303。

接下來我們就來分析libDexHelper.so,來看看它都幹了些什麼。精力有限,很多地方沒能很詳細去分析。有些地方分析的可能也不一定對,將就看吧。

功能分析

JNI_OnLoad(0x3EA68)的分析在最後。

0x15960

讀/proc/self/maps,特徵字串:

libDexHelper.so
libDexHelper-x86.so
libDexHelper-x86_64.so
/system/lib64/libart.so
/system/lib64/libLLVM.so
/system/framework/arm64/boot-framework.oat
/system/lib64/libskia.so
/system/lib64/libhwui.so
.oat
ff c3 01 d1 f3 03 04 aa f4 03 02 aa f5 03 01 aa e8 03 00 aa
GumInvocationListener
GSocketListenerEvent

0x16A30

獲取系統屬性,讀/proc/%d/cmdline,特徵字串:

ro.yunos.version
ro.yunos.version.release
persist.sys.dalvik.vm.lib
persist.sys.dalvik.vm.lib.2
/system/bin/dex2oat
LD_OPT_PACKAGENAME
LD_OPT_ENFORCE_V1

off_12EF10:為2表示yunos,art模式;為1表示yunos,dalvik模式;為0表示非yunos。

0x17A70

md5。

0x186C4

字串解密函式。

0x19674

返回字串rw。

0x19778

返回字串su。

0x1987C

返回字串mount。

0x19998

寫classes.dve檔案。

0x19b48

讀取目錄中的檔案。

0x19E08

建立String型別的陣列,第一個引數是String列表,第二個引數是陣列長度。

0x1A058

呼叫0x19E08建立陣列:

/etc
/sbin
/system
/system/bin
/vendor/bin
/system/sbin
/system/xbin

0x1A740

呼叫0x19E08建立陣列:

com.yellowes.su
eu.chainfire.supersu
com.noshufou.android.su
com.thirdparty.superuser
com.koushikdutta.superuser
com.noshufou.android.su.elite

0x1AF1C

呼叫0x19E08建立陣列:

com.chelpus.lackypatch
com.ramdroid.appquarantine
com.koushikdutta.rommanager
com.dimonvideo.luckypatcher
com.ramdroid.appquarantinepro
com.koushikdutta.rommanager.license

0x1B7D0

呼叫0x19E08建立陣列:

com.saurik.substrate
com.formyhm.hideroot
com.amphoras.hidemyroot
com.devadvance.rootcloak
com.formyhm.hiderootPremium
com.devadvance.rootcloakplus
com.amphoras.hidemyrootadfree
com.zachspong.temprootremovejb
de.robv.android.xposed.installer

0x1C40C

system_property_get ro.product.cpu.abi和讀/system/lib/libc.so判斷是不是x86架構。

0x1C61C

檢視classes.dve是否存在。

0x1C8D8

呼叫0x19E08建立陣列:

/sbin/
/su/bin/
/data/local/
/system/bin/
/system/xbin/
/data/local/bin/
/system/sd/xbin/
/data/local/xbin/
/system/bin/.ext/
/system/bin/failsafe/
/system/usr/we-need-root/

0x1D518

初始化一些路徑,特徵字串:

.cache
oat
.payload
v1filter.jar
classes.odex
classes.vdex
classes.dex
assets/classes.jar
.cache/classes.jar
.cache/classes.dex
.cache/classes.odex
.cache/classes.vdex

0x1E520

將libc中的一些函式的地址放到.DATA。

0x137BB0 fopen
0x137BB8 fclose
0x137BC0 fgets
0x137BC8 fwrite
0x137BD0 fread
0x137BD8 sprintf
0x137BE0 pthread_create

0x1F250

讀/proc/self/cmdline,判斷是否含有com.miui.packageinstaller從而判斷是否由小米應用包管理元件啟動。

0x1F710

先system_property_get ro.product.manufacturer和system_property_get ro.product.model判斷是否是samsung,然後system_property_get ro.build.characteristics是否為emulator。

0x1FDC8

註冊如下native函式:

RegisterNative(com/secneo/apkwrapper/H, attach(Landroid/app/Application;Landroid/content/Context;)V, [email protected][libDexHelper.so]0x2f6e4)
RegisterNative(com/secneo/apkwrapper/H, b(Landroid/content/Context;Landroid/app/Application;)V, [email protected][libDexHelper.so]0x247c0)
RegisterNative(com/secneo/apkwrapper/H, c()V, [email protected][libDexHelper.so]0x24c08)
RegisterNative(com/secneo/apkwrapper/H, d(Ljava/lang/String;)Ljava/lang/String;, [email protected][libDexHelper.so]0x23d04)
RegisterNative(com/secneo/apkwrapper/H, e(Ljava/lang/Object;Ljava/util/List;Ljava/lang/String;)[Ljava/lang/Object;, [email protected][libDexHelper.so]0x35ab0)
RegisterNative(com/secneo/apkwrapper/H, f()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1a740)
RegisterNative(com/secneo/apkwrapper/H, g()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1af1c)
RegisterNative(com/secneo/apkwrapper/H, h()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1b7d0)
RegisterNative(com/secneo/apkwrapper/H, n()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1c8d8)
RegisterNative(com/secneo/apkwrapper/H, j()[Ljava/lang/String;, [email protected][libDexHelper.so]0x1a058)
RegisterNative(com/secneo/apkwrapper/H, k()Ljava/lang/String;, [email protected][libDexHelper.so]0x19778)
RegisterNative(com/secneo/apkwrapper/H, l()Ljava/lang/String;, [email protected][libDexHelper.so]0x1987c)
RegisterNative(com/secneo/apkwrapper/H, m()Ljava/lang/String;, [email protected][libDexHelper.so]0x19674)
RegisterNative(com/secneo/apkwrapper/H, bb(Landroid/content/Context;Landroid/app/Application;Landroid/app/Application;)V, [email protected][libDexHelper.so]0x2921c)
RegisterNative(com/secneo/apkwrapper/H, o(Landroid/content/Context;)I, [email protected][libDexHelper.so]0x2f158)
RegisterNative(com/secneo/apkwrapper/H, p()V, [email protected][libDexHelper.so]0x1875c)
RegisterNative(com/secneo/apkwrapper/H, q()I, [email protected][libDexHelper.so]0x23568)
RegisterNative(com/secneo/apkwrapper/H, mu()I, [email protected][libDexHelper.so]0x1f250)

0x218A8

system_property_get ro.build.version.release/ro.build.version.sdk/ro.build.version.codename,最終返回sdkversion。

0x22068

建立一些目錄:

/data/usr/0/包名/.cache/oat
/data/usr/0/包名/.cache/oat/arm64
/data/usr/0/包名/.payload

0x22a90

模擬器檢測,特徵字串:

vboxsf
/mnt/shared/install_apk
nemusf
/mnt/shell/emulated/0/Music sharefolder
/sdcard/windows/BstSharedFolder

0x23568

讀proc/pid/cmdline找字串":bbs",沒搞懂這是什麼意思。這個函式名是is_magisk_check_process。

0x247C0

呼叫setOuterContext。

0x24C08

system_property_get ro.product.brand,針對華為/榮耀機型,呼叫startLoadFromDisk。

0x26278

getDeclaredFields獲取field物件陣列之後呼叫equals,返回查詢的指定的field物件。

0x27290

修改mInitialApplication和mClassLoader。

0x2921C

修改mAllApplications(remove和add)。

0x29CE8

模擬器檢測,特徵字串:

com.bignox.app.store.hd
com.bluestacks.appguidance
com.bluestacks.settings
com.bluestacks.home
com.bluestack.BstCommandProcessor
com.bluestacks.appmart

0x2B670

通過FLAG_DEBUGGABLE判斷是debug還是release。

0x2CAE0

通過android.content.pm.Signature獲取簽名的md5。

0x2F158

通過access以下檔案判斷是否被root:

/sbin/.magisk/
/sbin/.core/img
/sbin/.core/mirror
/sbin/.core/db-0/magisk.db

0x2F6E4

讀/proc/self/cmdline,呼叫java層的com.secneo.apkwrapper.H.j,呼叫bindService,獲取android_id,呼叫android.app.Application.attach,如果包名是com.huawei.irportalapp.uat呼叫setOuterContext。

0x31474

呼叫java層的com.secneo.apkwrapper.H.f(ff)載入v1filter.jar和原始dex。

檢視/proc/self/maps:

anon:dalvik-classes.dex extracted in memory from v1filter.jar

anon:dalvik-DEX data

把多出來這樣的段dump下來。

原始dex:

指令虛擬化是呼叫JniLib.cV解析執行的,最後一個引數是一個函式code索引,用來查詢被虛擬化後的指令,其它是方法引數:

v1filter.jar:

0x3371C

hook libcutils.so/liblog.so中的android_log_write和android_log_buf_write,使其返回0。

0x339FC

currentActivityThread-mPackages-LoadedApk-mResources-getAssets。

0x34A00

呼叫android.content.res.Resources.getAssets,失敗再呼叫0x339FC。

0x351DC

讀取assets檔案。

0x35AB0

對傳入引數呼叫makeInMemoryDexElements,修改dalvik.system.DexFile.mFileName。

0x3766C

初始化下列字串:

/data/user/0/cn.missfresh.application/.cache/classes.jar
/data/user/0/cn.missfresh.application/.cache/classes.dex
/data/user/0/cn.missfresh.application/.cache/v1filter.jar

呼叫0x80458計算包名hash,呼叫0x75AA8。

呼叫AAssetManager_open讀取assets/resthird.data寫入v1filter.jar,呼叫0x31474。

(看別人的分析應該讀assets下面兩個檔案:classes0.jar是被加密的dex,classes.dgc是被加密的抽取後的指令。不過我分析的這個樣本中沒有classes0.jar和classes.dgc,可能是名字變了)

0x398F8/0x3A08C

檢測dexhunter,dumpclass好像是dexhunter裡面的吧。特徵字串:

_Z16hprofDumpClassesP15hprof_context_t
_Z12dvmDumpClassPK11ClassObjecti
_Z9dumpClassP7DexFilei
dumpclass
dump_class

0x3BF10

引數是檔名,返回檔案是否存在。

0x3BF7C

模擬器檢測,特徵字串:

ueventd.ttVM_x86.rc
init.ttVM_x86.rc
fstab.ttVM_x86
bluestacks
BlueStacks

0x3CE14

system_property_get ro.debuggable,呼叫檢測模擬器的函式。

0x3D814

通過android.hardware.usb.action.USB_STATE監聽USB狀態。

0x42378

md5。

0x44708

hook下列函式(反除錯):

vmDebug::notifyDebuggerActivityStart(hook後:0x446C0)
art::Dbg::GoActive(hook後:0x446E4)
art::Runtime::AttachAgent(hook後:0x45CF8)

0x46194

system_property_get ro.yunos.version。

0x4C2F0

hook下列函式(指令抽取還原):

art::ClassLinker::DefineClass(hook後:0x46BB8)
art::ClassLinker::LoadMethod(hook後:0x46ED4/0x47BB8/0x488C0/0x491F8/0x49B0C)
art::OatFile::OatMethod::LinkMethod(hook後:0x46BD8/0x46DB0)

0x4DB80

md5。

0x50280

讀/proc/self/maps找到含有包名的段。

0x5074C

呼叫java層的com.secneo.apkwrapper.H1.find_dexfile。

0x50B60

呼叫java.lang.StackTraceElement.getMethodName和java.lang.StackTraceElement.getClassName。

0x57424

載入assets中的classes.dgg。

0x598FC

讀/proc/self/maps找到libDexHelper.so。

0x59CE8

設定dex2oat的引數,--zip-fd/--oat-fd/--zip-location/--oat-location/--oat-file/--instruction-set。

0x5C600

hook libdvm.so中的函式(類似於0x67544),具體沒仔細看,0x5BAA8-0x5BEF8都是被hook後的實現。

0x61E3C

hook libc中的下列函式:

fstatat64(hook後:0x5E778)
stat(hook後:0x5E858)
close(hook後:0x5EA20)
openat(hook後:0x5ED20)
open(hook後:0x5ED9C)
pread(hook後:0x5FAB8)
read(hook後:0x5FC14)
mmap64(hook後:0x5FDDC)
__openat_2(hook後:0x5FEF4)
__open_2(hook後:0x5FF74)

0x64AE8

根據不同SDK版本返回Name Mangling之後的art::DexFileLoader::open。

0x65FE4

根據不同SDK版本返回Name Mangling之後的art::OatFileManager::OpenDexFilesFromOat。

0x67544

hook下列函式:

art::DexFileLoader::open(hook後:0x6D39C/0x6D3E8)
art::OatFileManager::OpenDexFilesFromOat(hook後:0x6A2C0/0x6AF14/0x6B9B0/0x6C188/0x6CB5C)

0x6D4A0

patch掉art::Runtime::IsVerificationEnabled。

0x6DAD8

hook art::DexFileVerifier::Verify(hook後:0x6D38C/0x6D394,直接返回1)。

0x6E40C

hook art::DexFileLoader::open(hook後:0x6D39C/0x6D3E8)。

0x70410

hook下列函式:

art::DexFileVerifier::Verify(hook後:0x6EB04/0x6EB0C/0x6EB14,直接返回1)
art::DexFile::OpenMemory(hook後:0x74EE8/0x74E90/0x74F38)
Art::DexFile(hook後:0x74E30/0x74F88)

0x75054

hook libdvm.so中的函式,具體沒仔細看,0x6EB1C/0x74DEC/0x6FFBC都是被hook後的實現。

0x75AA8

讀java.lang.DexCache.dexfile(這個dexfile就是解壓apk之後根目錄的那個classes.dex)。

0x767F8

引數是so檔案路徑,開啟該so檔案。

0x76C8C

引數是libart.so中的一個函式,返回該函式地址。

0x76CCC

第一個引數是so中的函式名,第二個引數是so的相對路徑,返回該函式在so中的地址。

0x76D90

引數是libdexfile.so中的一個函式,返回該函式地址。

0x76DD4

引數是libjdwp.so中的一個函式,返回該函式地址。

0x76E18

md5。

0x7783C

字串解密函式。

0x79270

計算傳入字串的hash(不完全是md5)。

0x7A240

熱補丁檢測,特徵字串:

nuwa
andfix
hotfix
.RiskStu
tinker

0x80458

呼叫0x79270。

0x804A8

hook libc中的下列函式:

msync(hook後:0x78470)
close(hook後:0x7AF50)
munmap(hook後:0x7A568)
openat64(hook後:0x7DC48)
__open_2(hook後:0x7DC80)
_open64(hook後:0x7DCB8)
_openat_2(hook後:0x7DCF0)
ftruncate64(hook後:0x7DD30)
mmap64(hook後:0x7EF60)
pread64(hook後:0x7F5D0)
read(hook後:0x7F7DC)
write(hook後:0x8022C)

0x87F98

hook libdvm.so中的函式(類似於0x44708),具體沒仔細看,0x856C0/0x87F00/0x87F4C都是被hook後的實現。

0x889B0

patch掉art::Runtime::UseJitCompilation。

0x8A794/0x8B71C/0x8B890

hook函式實現。

0x917E8

讀/proc/sys/fs/inotify/max_queued_watches。

0x91848

讀/proc/sys/fs/inotify/max_user_instances。

0x918A8

讀/proc/sys/fs/inotify/max_user_watches。

0x95778

看起來好像是通過判斷時間實現的反除錯。

0x95A28

字串查詢函式。

0x95B9C

字串解密函式。

0x95D60

socket連線。

0x96398

frida檢測,讀/proc/self/task,特徵字串:gum-js-loop;讀/proc/self/fd,特徵字串linjector。

0x995D0

xposed檢測,特徵字串:

.xposed.
xposedbridge
xposed_art

0x99D28

hook框架檢測,特徵字串:

frida
ddi_hook
dexposed
substrate
adbi_hook
MSFindSymbol
hook_precall
hook_postcall
MSHookFunction
DexposedBridge
MSCloseFunction
dexstuff_loaddex
dexposedIsHooked
ALLINONEs_arthook
dexstuff_resolv_dvm
dexposedCallHandler
art_java_method_hook
artQuickToDispatcher
dexstuff_defineclass
dalvik_java_method_hook
art_quick_call_entrypoint
frida_agent_main

0x9C0BC

呼叫0x96398檢測frida,system_property_get ro.product.model,呼叫0x9FD88檢測xposed和自動脫殼機,hook dlopen(hook後:0x9B89C)和ptrace(hook後:0x95BF8)。

0x9CFCC

通過讀取/proc/%d/status判斷TracerPid等實現反除錯。

0x9D878

通過讀取/proc/%d/wchan判斷是不是ptrace_stop實現反除錯。

0x9DCF4

通過讀取/proc/%ld/task/%ld/status判斷TracerPid等實現反除錯。

0x9ED44

通過java.lang.StackTraceElement.getClassName列印函式呼叫棧進行xposed檢測。

0x9F770

通過java.lang.ClassLoader.getSystemClassLoader.loadClass列印類載入器進行xposed檢測。

0x9FD88

呼叫0x9ED44和0x9F770,通過判斷ServiceManager裡是否有user.xposed.system進行xposed檢測,然後檢測自動脫殼機:

fart( http://github.com/hanbinglengyue/FART

)

FUPK3(

http://github.com/F8LEFT/FUPK3

)

Youpk(

h ttps://github.com/Youlor/Youpk )

檢測方法是判斷下列類或者方法是否存在:

dumpMethodCode
fartthread
fart
android/app/fupk3/Fupk
android/app/fupk3/Global
android/app/fupk3/UpkConfig
android/app/fupk3/FRefInvoke
cn/youlor/Unpacker

0xA18D4

getInstalledApplications獲取系統中安裝的APP資訊。

0xA7D3C

解密出字串Java和JNI_OnLoad,hook了幾個函式,被hook的原地址未知,新地址:0xA43A0/0xA485C/0xA48F4/0xA54B0;hook dlsym(hook後:0xA4554)和dlopen(hook後:0xA4D30)。

0xB4B94

hook libc中的下列函式:

write(hook後:0xAA2CC)
pwrite64(hook後:0xAA51C)
close(hook後:0xAA774)
read64(hook後:0xAAA9C)
openat64(hook後:0xAACB8)
__openat_2(hook後:0xAB6D4)
__open_2(hook後:0xAC0F4)
open64(hook後:0xACB10)
read(hook後:0xAFE18)
mmap64(hook後:0xB1C54)

system_property_get debug.atrace.tags.enableflags,hook bionic_trace_begin和bionic_trace_end(hook後:0xA8EF4和0xA8EF8,直接返回),沒有找到則hook g_trace_marker_fd(hook後:0xA8EFC,返回-1)。

這些hook是為了透明加密,具體沒仔細看,之前論壇也有人分析過,估計應該沒有太大的變化: 梆梆加固之透明加密分析

0xB9BEC

sha1。

0xBAE64

md5init。

0xC3378

base64。

0xC5DDC

base64。

0xC7B18

APK簽名相關。

0xD024C

sha1。

0xD1C04

sha1init。

0xD2E98

md5。

0xD6484

呼叫0xD75A0。

0xD5CDC

讀/proc/self/cmdline。

0xD6578

hook libdvm.so中的函式(hook後:0xD6988)。

0xD68A8

根據off_12EF10處的值判斷呼叫0xD6484還是0xD6578。

0xD75A0

hook libaoc.so中的函式(hook後:0xD69BC)。

0x3EA68

JNI_OnLoad。分析環境pixel4 android10,動態分析過程中一些沒有被呼叫的函式不再分析。

1.初始化cpuabi字串(arm64)於0x12E7C8

2.初始化so名字串(libDexHelper)於0x12EC38

3.初始化字串com/secneo/apkwrapper/H於0x137B10

4.呼叫0x1E520

5.

JNIEnv->FindClass(com/secneo/apkwrapper/H)
JNIEnv->GetStaticFieldID(com/secneo/apkwrapper/H.PKGNAMELjava/lang/String;)
JNIEnv->GetStaticObjectField(class com/secneo/apkwrapper/H, PKGNAME Ljava/lang/String; => "cn.missfresh.application")
JNIEnv->GetStringUtfChars("cn.missfresh.application")

6.將包名存於0x138040

7.

JNIEnv->FindClass(android/app/ActivityThread)
JNIEnv->GetStaticMethodID(android/app/ActivityThread.currentActivityThread()Landroid/app/ActivityThread;)
JNIEnv->CallStaticObjectMethodV(class android/app/ActivityThread, currentActivityThread())
JNIEnv->GetMethodID(android/app/ActivityThread.getSystemContext()Landroid/app/ContextImpl;)
JNIEnv->CallObjectMethodV(android.app.ActivityThread, getSystemContext())
JNIEnv->FindClass(android/app/ContextImpl)
JNIEnv->GetMethodID(android/app/ContextImpl.getPackageManager()Landroid/content/pm/PackageManager;)
JNIEnv->CallObjectMethodV(android.app.ContextImpl, getPackageManager())
JNIEnv->GetMethodID(android/content/pm/PackageManager.getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;)
JNIEnv->NewStringUTF("cn.missfresh.application")
JNIEnv->CallObjectMethodV(android.content.pm.PackageManager, getPackageInfo("cn.missfresh.application", 0x0))
JNIEnv->GetFieldID(android/content/pm/PackageInfo.applicationInfo Landroid/content/pm/ApplicationInfo;)
JNIEnv->GetObjectField(android.content.pm.PackageInfo, applicationInfo Landroid/content/pm/ApplicationInfo;)
JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.sourceDir Ljava/lang/String;)
JNIEnv->GetObjectField(android.content.pm.ApplicationInfo, sourceDir Ljava/lang/String; => "/data/app/cn.missfresh.application-1")
JNIEnv->GetStringUtfChars("/data/app/cn.missfresh.application-1")
JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.dataDir Ljava/lang/String;)
JNIEnv->GetObjectField(android.content.pm.ApplicationInfo, dataDir Ljava/lang/String; => "/data/data/cn.missfresh.application")
JNIEnv->GetStringUtfChars("/data/data/cn.missfresh.application")

8.呼叫0x218A8

9.

JNIEnv->GetFieldID(android/content/pm/ApplicationInfo.nativeLibraryDir Ljava/lang/String;)
JNIEnv->GetObjectField([email protected]36d64342, nativeLibraryDir Ljava/lang/String; => "/data/app/cn.missfresh.application-1/lib/arm64")
JNIEnv->GetStringUtfChars("/data/app/cn.missfresh.application-1/lib/arm64")

10.讀/proc/pid/fd,匹配包名+base.apk,0x12EA38存放指向base.apk完整路徑的指標的指標

11.

JNIEnv->FindClass(com/secneo/apkwrapper/H)
JNIEnv->GetStaticFieldID(com/secneo/apkwrapper/H.ISMPAASLjava/lang/String;)
JNIEnv->GetStaticObjectField(class com/secneo/apkwrapper/H, ISMPAAS Ljava/lang/String; => "###MPAAS###")
JNIEnv->GetStringUtfChars("###MPAAS###")

12.將得到的結果和###MPAAS###比較,0x12E7F8指向0x137D9C,0x137D9C存放比較結果

13.呼叫0x22068

14.呼叫0x23568

15.呼叫0x1F250

16.將字串lib/libart.so存放於0x1378A8

17.讀/proc/self/maps,找許可權為"r-xp"的lib/libart.so

18.初始化下列字串:

/data/user/0/cn.missfresh.application/.cache
/data/user/0/cn.missfresh.application/.cache/oat/arm64
/data/user/0/cn.missfresh.application/.cache/classes.dve
/data/app/cn.missfresh.application-xxx/oat/arm64/base.odex

19.fstat /data/app/cn.missfresh.application-xxx/oat/arm64/base.odex

20.計算md5(不太清楚具體算的什麼),0x12EC98指向0x130080,0x130080存放計算結果

21.access /data/user/0/cn.missfresh.application/.cache/classes.dve,不存在則把之前算的md5寫入該檔案;存在則讀取其中的值和之前算的比較,不相等則寫入新計算的值

22.呼叫0x3371C(根據標記位決定是否呼叫)

23.呼叫0x1FDC8

24.初始化下列字串:

/data/user/0/cn.missfresh.application/.cache/libDexHelper32
/lib/armeabi-v7a/libDexHelper.so
/lib/armeabi/libDexHelper.so
assets/libDexHelper32

25.呼叫0x3766C

26.呼叫0xB4B94

27.呼叫0x9C0BC

28.呼叫0xD5CDC

29.呼叫0x24C08

30.呼叫0x1C40C

31.呼叫0xA7D3C

32.結束

參考

梆梆APP加固產品方案淺析: http://www.cnblogs.com/2014asm/p/14547218.html

某加固詳細分析總結,另附該加固脫殼機:

http://bbs.pediy.com/thread-252828.htm

總結

樣本混淆強度還是比較大的,比前兩篇文章中的樣本要複雜很多。不過分析過程中也是有一些技巧:比如位置相鄰的函式之前其實是有聯絡的,和另外某些殼的程式碼有類似的地方,可以網上搜一下舊版本的分析部落格,有一些函式名和字串沒有被抹去。

看雪ID:houjingyi

http://bbs.pediy.com/user-home-734571.htm

*本文由看雪論壇 houjingyi 原創,轉載請註明來自看雪社群

#

往期推薦

1. AFL速通——流程及afl-fuzz.c原始碼簡析

2. sql注入學習筆記

3. 檔案上傳和檔案包含的各種姿勢

4. React Native Hermes 逆向實踐

5. 2022CISCN初賽 ez_usb WriteUp

6. APT Turla樣本分析

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!