某視頻app(V15.7)及web分析記錄
初衷是刷抖音太多,發現不能在點贊過的視頻列表中直接搜索,就想自己實現下,把這個過程做了下記錄,當學習筆記了,純技術交流用。
一、軟硬件環境:
抖音android (V15.7,應用寶渠道)
抖音web (V16.1)
IDA 7.5
Frida 14.2.2
Gda3.86
JEB
jadx-gui
unidbg
LineageOs 17.1 (android 10)
小米8
二、流水賬 (直接按時間順序,有坑寫坑)
1、App X-Gorgon算法定位
開始也是直接網上搜了下,有下面這個:
https://blog.csdn.net/weixin_48271161/article/details/108544446

在這個V15.7版本里面沒找到相關的so(后來發現我看的這個版本對應的是libmetasec_ml.so)。
還有其它一些,比如搜索X-Gorgon,hook HashMap,發現對這個版本都不好使了,那就從頭開始了,自己找這個加密關鍵點。
先上frida,輸出jni函數:

現在回過頭看,發現當時就輸出了libmetasec_ml.so jni記錄(雖然不是直接的加密函數),但是當時可能是這個記錄太多了,竟然沒注意到,估計當時看到了也沒留意,因為開始也不知道這個so是做什么的。
看到這個Cronet,搜了下:
Cronet網絡庫系列(一):用例與原理實現詳解
https://segmentfault.com/a/1190000021095757
https://blog.csdn.net/u010983881/article/details/97770544/
【Android】移動端接入Cronet實踐
根據登錄url找到下面代碼:

然后根據調用關系翻代碼,找Cronet相關調用,瀏覽代碼,翻到com.ttnet.org.chromium.net.impl.CronetUrlRequest,也沒
發現有設置X-Gorgon頭的地方(其它header倒是有設置),中間也嘗試通過send反找,調用線太長,還是決定換個方式來做。
直接下了個cronet代碼:
https://github.com/hanpfei/chromium-net
然后就是對著代碼,確定認為會相關的幾個函數,上IDA調試:
nativeCreateRequestAdapter
nativeAddRequestHeader
最后找到這個點:


到這里就跟libmetasec_ml.so關聯起來了。
知道了調用點,就想直接調用libmetasec_ml.so方便調試,寫了個程序來加載這個so,發現會異常,考慮到可能是上下文環境不全,就打算按正常流程來加載so,也調用JNI_OnLoad:
void* hMetasecSo = dlopen("/data/local/tmp/libmetasec_ml.so", RTLD_LAZY);
typedef jint(*FUN)(JavaVM* vm, void* res);
FUN func_onload = (FUN)dlsym(hMetasecSo, "JNI_OnLoad");
這個調用是需要JavaVM參數的,就準備加載libart來調用JNI_CreateJavaVM創建JavaVM,參考網上資料設置好參數:
JavaVMInitArgs vm_args;
JavaVMOption options[2];
options[0].optionString = "-Djava.class.path=.";
vm_args.version = JNI_VERSION_1_6;
vm_args.options = options;
vm_args.nOptions = 2;
vm_args.ignoreUnrecognized = JNI_TRUE;
編譯運行后,直接崩了,查看日志,提示沒有設置NoSigChain。
然后查看android源碼,找到對應地方看了下,是在檢查創建VM的選項參數,應該是沒有no-sig-chain這個參數,網上搜了下,沒有找到怎么設置這個參數的,然后根據代碼,結合其它屬性改了下代碼,增加了一條:
options[1].optionString = "-Xno-sig-chain";
android源碼里面也跳過了這個相關的,編譯運行,之前的錯誤就沒有了,但是后面還是異常退出了,后來查了下,找到下面信息:
從Android N開始(SDK >= 24),通過dlopen打開系統私有庫,或者lib庫中依賴系統私有庫,都會產生異常,甚至可能導致app崩潰。
應用可以調用/vendor/etc/public.libraries.txt和/system/etc/public.libraries.txt里面的所有so庫,所以往這個文件寫入自己希望被調用的so,這個庫就變成共用的了,任意應用就可以找到這個so庫了。
試了下上面的方法,包括把libart.so及相關的庫放到其它用戶目錄下,還是不行,考慮到本來就是想還原運行環境的,就想直接上APK吧,還能省去自己創建VM,就寫了個測試APK調用這個so:

編譯好apk后導入到手機運行:

Didn't find class "com.bytedance.mobsec.metasec.ml.MS"
直接參考抖音補上對應的包路徑:

現在能運行了,但是調用JNI_OnLoad會異常,準備上IDA調試,看了下流程圖:

帶了llvm,看so文件尾部記錄的是Apple LLVM version 10.0.1 (clang-1001.0.46.3)。
跟了下,一些跳轉都做了處理,不是很好分析,準備上unidbg (https://github.com/dqzg12300/unidbg_tools,可以用這個大佬整理的),trace代碼下來分析。

根據trace得到的指令流,發現有這種訪問"/proc/self/exe"路徑返回-1的情況,我是直接修改這個函數,直接返回1了,這個函數只是用來取e_machine字段的:



后面繼續trace,然后結合動態調試,發現有代碼校驗的地方:

上面都處理后,測試apk就可以正常跑完JNI_OnLoad了,后面就是主動調用加密函數測試了,直接在jni接口中調用libmetasec_ml.so:

下面插個分支,到這里的時候,在一個群里看到信息說抖音開了web,直接去看了下。
2、抖音web請求參數_signature算法分析
直接訪問www.douyin.com,看訪問參數多了個_signature,這種格式:
&_signature=_02B4Z6wo00901qf0GiQAAIDAwkLkeQfbXMKn9B6AAMkm74。
多拿幾個比較,發現前面一段(_02B4Z6wo00901)是前綴,后面初步判斷是base64格式。
直接網上搜了下,找到下面這篇:
網絡爬蟲-今日頭條_signature參數逆向(第一彈)_井蛙不可語于海的博客-CSDN博客_byted_acrawler
https://blog.csdn.net/qq_39802740/article/details/104911315
主要加密算法在acrawler.js,參考這個用node跑起來了,里面的實現是一個js 虛擬機,關于虛擬機還找到這篇:
StriveMario/jsvm: 給"某音"的js虛擬機寫一個編譯器 (github.com)
https://github.com/StriveMario/jsvm
不過目前版本都看起來跟這個不同了,并且本地調試算法其實也用不上,但是還是可以學習下的。


除了文章提到的,還有下面的幾個參數要改下:

然后就可以調用global.byted_acrawler.init及global.byted_acrawler.sign參數了(這個后面發現這樣調用的算法流程跟瀏覽器跑的其實是不同的)。
用瀏覽器調試也是確定有init ,sign函數的:

后面就是直接用node調試,因為原代碼的格式都是這樣的:

都是一句話寫完邏輯,不方便調試,先整理了下代碼,比如這種
opCode = 3 & initCode; // initCode % 4
那>2的分支就是==3了,長的代碼就分割下。
整理后也方便加日志,輸出中間數據。
調試中把vm的基礎操作整理出來(vm_xor,vm_and等等),特別字符串連接的指令,可以加上日志,方便定位。
Vm中會檢查運行環境,包括下面這些:
domDetect
debuggerDetect
nodeDetect
phantomDetect
webdriverDetect
incognitoDetect
hookDetect
locationDetect
檢查結果會參與加密,作為其中一段的因子。
當時看了幾個簽名數據,就有個疑問的,相同的數據,_signature總是不同的,當時想到應該有個變量因子的,但是看https提交的參數沒發現這個變量,那這樣就是直接記錄在_signature中了。
直接base64解密排除前綴后的數據,類似這種:
<Buffer 7c 7e 62 a4 00 00 20 30 83 81 9d 5b 28 d0 87 e9 7c 76 e3 80 00 07 26 f8>
比較多個后也沒發現明顯的變量因子,猜到可能是時間戳,但是沒找到哪個數據段是標識的時間,那就直接開始看流程了。
調試過程發現有getTime的調用,直接改為固定的,最后得到的_signature就不會變了:

那就是確定跟時間戳相關了,也就是服務器可以通過這個得到時間戳或者轉換過的值,來驗證客戶端上報的_signature是否正確了。
后面算法中,主要就是參數字符串(location,user_agent,param)的處理(xor,or,and等)得到各個hash值,然后類似base64加密得到加密字符串(這里不是直接得到完整明文,再最后一次base64的方式):

處理完后,最后2個字符是附加的校驗字符,是對前面數據得到一個DWORD值的低字節:
_02B4Z6wo00f014U9W.wAAIDB4IuloLYVYYOFPV9AAIGw
2260354722 '86ba46a2'
_02B4Z6wo00f014U9W.wAAIDB4IuloLYVYYOFPV9AAIGwa2
本以為搞完了,直接跟瀏覽器訪問的一比較,悲劇了,竟然加密結果不同,直接上瀏覽器調試。
前面已經整理了代碼的,瀏覽器訪問的時候js是直接下載的,直接修改DNS,把這個js定向到本地服務器:
127.0.0.1 sf1-ttcdn-tos.pstatp.com
這樣瀏覽器訪問的時候也是用的整理后的js了,有了前面的調試經歷,這個也很快搞清楚了流程。

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAACfElEQVRIS9WVO2hUURCGv20Eg1bapVC0sFNJJahFRPDRWFgoRokgQbAQLFJJdNFKFAVBNMROI1oYsBITsPDRpAgkNkmjEoggqIWNIILyywwMs3MxhevjwLLs3XPnzP+a0+L/XZuAE63U/wBwNz0bBWYAfcd1DrjYJfxrgHFgT9HLGeALUAIYAS6kl54A+lxNz3cAL7sEoCq7HdgVSCsBVMiPWrWszJ8EsBK4BjwEDgInrafRbKEIQJa5BawDViVrzQKHgAUrJOsdM3ZkrdP2n1sy203sPQC2eCOAW6NiX3U2JMuWCuTCKlYpIADDwBQgaV+ERpSXPgN/BLgHXAqgnCTlRxZ0dp+b7zMANb/TAPakbHQoMARMAB8BZ6/JQsqF9uw1dp7ayfJpL/AZeAR8AK4HABGwKxObVEC1HKhAVsOiQwFnQv66b0UO2/RRM2I8B1o5mA/M6aAbwHlgH3AWWJsAqLQOj6AyAA0TAdVzkVmtDgDR/xnAehtpss5kAKPfYqxtJ+hQHX4T2GzM5Wa11fd5g5UC3nRla7f2eA6xj1GpIB/rWzK/thDHO0FqSBkBup0AvAXumMcjgEWbJqrrFnS7us/dQhVQB+VuWaoAyL+PzQIRgCykMfbebKUGtgZl/EAFOjYXAWjPfmDalPqVAiJU2aruG9VtRwAxA1EBsf4MGATehFt5DvgE9AeDekDj2GxSQMNBt61Wk4VUT5mKI1v7SwWUAdlmm7HsFrpsDOgu0Cx2O30DTgFjDSH7XY+bMvDzIs0WWs6hq80i8r6HdznvdWVPBWAF8LXhNEl9xTLw15tXjxWAA8Bxu4DeAd+BjcBu4JVNEYX8n1g/AAC2uh6gEsDjAAAAAElFTkSuQmCC

多了對這個圖片數據的參與,最后整理測試:

整體來看,web上雖然用了JS虛擬機,跟二進制的VM比起來還是弱些了,調試環境弄好后,執行流程就都比較清晰了。

這里web簽名就基本完成了,繼續app分析。
3、繼續APP算法分析
被上個分支中斷了下,思路都斷了,又重新熟悉了下,繼續開始跟這個加密算法。
在測試apk中調用libmetasec_ml.so加密函數后,返回的是NULL,調試后發現會從native調用java的情況:
然后參考抖音補全需要的包,里面有熱更新保護相關的代碼,屏蔽掉,讓測試工程能跑起來就行:
//import com.bytedance.JProtect;
//import com.bytedance.covode.number.Covode;
//import com.meituan.robust.ChangeQuickRedirect;
//import com.meituan.robust.PatchProxy;
//import com.meituan.robust.PatchProxyResult;

反正就是各種補,模擬全之前的調用環境。
看到一些檢測root相關的字符串:

通過分析trace日志,過濾掉一些跳轉計算流程后,發現可能的關鍵調用,用unidbg跑的時候返回是null:

下面就是調試抖音進行驗證了,修改對應指令為循環點,附加后循環處下斷點:

那就確定是下面函數返回的簽名字符串了:





對于自己APK調用時候,可以直接設置首尾地址:
//修改so的起始地址 *(unsigned int *)(dwContextAddr+0x10)=0x100000; //修改結束地址 *(unsigned int *)(dwContextAddr+0x10)=0xFFFFFFFF;
跳過這個檢查。
檢查trace代碼,發現有對代碼指令的檢查:


根據調試流程,整理調用鏈,補全初始化調用:


測試app可以直接跑出結果了:


現在是自己寫的程序可以跑了,后面是用app中提供簽名服務,還是擼算法出來,都方便很多了。
搞完測試:

整體感覺流程的分析難度不如 wegame。
這次分析過程中學習的技能點總結:
1、開發測試APK模擬目標的調用環境
2、Unidbg模擬調用so中任意地址
3、RSA驗簽過程,其它工作涉及的,之前只是使用API(
//1.明文計算sha256
//2.RSA解密密文(一般是base64解密后的字節流)
//3.比較尾部的串是否跟1的一致(因為解密后的結果是包含這個hash串,不是等于)
)