5a6J5Y2T5oqW6Z+z55+t6KeG6aKRdjHlroznu5M=
正文
先分析个if
块,对应的c++伪代码如下
1 | if ( paramsArray ) // 如果String[]不为null |
注释因为是很久以前写的了,仅供参考。
map这玩意儿类似 Java 中的 map
和 python 中的 dict
,不过底层是啥玩意儿我就不太清楚,没怎么学过c++。
这里的逻辑应该看得明白:
1、首先是判断paramsArray,也就是请求参数被分割成的字符串数组。这个字符串数据的**
索引%2(0,2,...)
**是key。所以这个array差不多是这个形式:[key,value,key,value,...]
2、然后就是 if 判断参数数组是否完整
3、接着就是遍历字符串数组,按键值对的形式保存到map对象里
挑几条代码分析一波
这里是调用了jni的
GetObjectArrayElement
方法获取数组元素,第一个参数是java接口指针
,第二参数是jobjectArray数组
,第三个参数是索引
。分别获取key和value
1 | key = _JNIEnv::GetObjectArrayElement((int)env, (int)paramsArray, j); |
然后呢,v47是map类型的。首先是operator[]方法,这个方法
返回到映射到等于 key 的关键的值的引用,若这种关键不存在则进行插入
?,意思就是用key取value,就好比python里的dict取值一样:params['key']
。至于3F044这个地址的方法是干嘛的我就懒得去了解了,只要大概知道做了些什么就行。
1 | v18 = std::map<std::string,std::string,std::less<std::string>,std::allocator<std::pair<std::string const,std::string>>>::operator[]( |
后面还有几行也一并分析了
1 | v41 = &v47; |
也是挑选几行分析
1、这两行是获取map的开头和结尾两个迭代器
1 | v45 = std::map<std::string,std::string,std::less<std::string>,std::allocator<std::pair<std::string const,std::string>>>::begin(&v47); |
2、然后呢。先是一层while循环,条件是v45和v46不相等就一直循环
1 | while ( std::_Rb_tree_iterator<std::pair<std::string const,std::string>>::operator!=(&v45, &v46) )// 如果不相等就一直循环 |
循环内部首先是从map开头迭代器中获取key的name,然后用获取到的name的地址偏移4个字节获取到下一个key的name。以上仅仅是我个人的分析,所以为了验证,直接调试。为了方便我直接把我以前写的调试教程贴上来了
IDA调试
点 File -> New instance(新建实例) 新打开一个程序,大概长这样
然后点 GO,这个时候我们还需要做一件事,直接调试是不可能的,需要一个中介人一样的东西,和frida-server差不多的东西,一般都在ida安装文件夹里,我这里目前只有mac,所以只说mac,找到你的app
然后右键显示包内容,路径是 Contents/MacOS/dbgsrc/
这里面有两个 android_server 一个是32位一个是64位,这里用32位就行,把它复制到你手机根目录的文件夹里,但也别乱放,我放在data里,命令如下:
1 | adb push android_server /data/ # 复制 |
输出了大概这么些东西就差不多了,其中23946是端口,复制一下;然后新开一个terminal,转发一下端口:
1 | adb forward tcp:23946 tcp:23946 |
接着回到ida,在菜单栏处点 Debugger -> attach 选择 Remote ARMLinux/Android Debugger 调试类型
在新弹出的窗口里点确定就行,如果你要改什么的话就点 options
不出意外的话你将会看到这个进程窗口,如果出意外的话我也没办法,大家都这么大了自己google
按 Ctrl-F mac也是这个快捷键,输入 com.ss 就会筛选处抖音进程,前提是你打开了抖音,出现这个窗口后你没打开,然后你打开了你就需要右键 Refresh 刷新再搜索,找到之后双击或者点 OK 开始debug,这里需要注意,如果你手机一直出现未响应弹窗记得要一直点等待,不然ida会崩,然后你就要重新来过
然后就进入了调试界面,这时候程序是卡着的,别乱点手机了
第一步先加载 libuserinfo.so 找到基址,按 Ctrl-S 打开选择段跳转窗口
还是 ctrl-f 搜索,start 是开始,也就是起始地址,这就是基址
接着回到之前分析的ida,复制那个方法地址 0x3E384,用基址加这个得到方法在内存中的地址 0xcef08384,按 G 跳转到这地方
在这方法开头处按 F2 打个断点,然后 F9 运行
这时候可以刷新一下让程序执行到这 … emm 接着我调试了一会儿后发现这玩意儿好像没什么用,权当讲了下怎么调试吧,接着分析
我在执行后将返回值复制到r3寄存器处和sub_3f36c函数调用结束后下一条指令处分别下了一个断点
1 | v42 = std::_Rb_tree_iterator<std::pair<std::string const,std::string>>::operator*(&v45); |
打好断点后,直接运行,运行到指令 ADD R2, SP, #0x90+var_78
处,此时的寄存器如下图
首先可以从上图看到,r3寄存器的地址正好是r0寄存器的地址偏移4个字节的地址,这两个地址的内容如下图
1 | sub_3F36C((int)&v32, v42 + 4); |
使用下面的frida hook代码可以得知该方法为字符串拼接
1 | String.prototype.format = function () { |
然后是空格和’+’字符替换操作
1 | v20 = sub_3EC48(&v32); |
主要看 153C0
这个方法,v20是拼接后的字符串,v21正常情况下为空,v6这个是一个char,没用到。
下面是 153C0
的伪代码
1 | int __fastcall sub_153C0(int a1, int a2, char a3) |
operator!=那里可以这样写:a1 != a2
;然后循环体首先取a1的第一个字符给v4,接着调用sub_13EE8,就是替换’ ‘ 和 ‘+’ 字符为字符 ‘a’
1 | void __fastcall sub_13EE8(int a1, _BYTE *a2) |
从伪代码中看不到返回值是什么,切换到汇编
1 | .text:000153C0 10 B5 PUSH {R4,LR} |
push,把R4寄存器和LR寄存器放到堆栈里,其中R4为方法
3EC48
的返回值
然后把参数1、2、3也依次放到堆栈里
接着是while循环体。
1 | .text:000153EA loc_153EA |
000153EA>
首先是从堆栈中取出参数1放到R2寄存器;然后取出参数2放到寄存器R3000153EE>
然后把R2复制到R0,R3复制到R1000153F2>
接着比较这两个寄存器中地址指向的内容是否相等,如果相等返回1,不想等返回0000153F6>
结果复制到R3寄存器000153F8>
然后比较如果等于0跳转到loc_153CE
看true执行块
1 | 0153CE loc_153CE |
000153CE>
从堆栈取参数1到R3寄存器,然后复制到R0寄存器000153D2>
然后是operator*方法,这个方法可以详细分析下它的汇编来搞懂是干嘛用的这是对应的汇编代码
1
2
3
4
5
6
7
8
9
10 .text:00015734 var_4= -4
.text:00015734
.text:00015734 82 B0 SUB SP, SP, #8
.text:00015736 01 90 STR R0, [SP,#8+var_4]
.text:00015738 01 9B LDR R3, [SP,#8+var_4]
.text:0001573A 1B 68 LDR R3, [R3]
.text:0001573C 18 46 MOV R0, R3
.text:0001573E 02 B0 ADD SP, SP, #8
.text:00015740 70 47 BX LR
.text:00015740 ; End of function __gnu_cxx::__normal_iterator<char *,std::string>::operator*(void)
为了更好的示范,我获取了该方法在内存中的地址
0xd5aab735
;并在内存中创建了一个字符串 “hello”,地址为0x9ef86a88
首先是在堆栈申请内存,然后将参数1放到刚刚申请的堆栈里,我画了个图
接着从SP+4
处取出参数1的地址到R3,然后再从该地址取出存放的内容(hello)
然后将R3复制到R0,之后算是释放刚刚申请的堆栈内存,然后回到调用方法
000153D6>
返回值放到R3寄存器000153D8>
然后从堆栈取出第三个参数,放到R0寄存器;顺便把operator*的返回值放到R1寄存器,接着调用sub_13EE8
方法,其中R0、R1为该方法的参数1和参数2;sub_13EE8
这个方法是替换’ ‘ 和 ‘+’ 为字符 ‘a’,就不分析了,看看伪代码就能懂
1
2
3
4
5 void __fastcall sub_13EE8(int a1, _BYTE *a2)
{
if ( *a2 == 32 || *a2 == 43 ) // 如果a2等于' '或'+',那么返回a
*a2 = 97;
}
000153E2>
这三条指令主要分析下_ZN9__gnu_cxx17__normal_iteratorIPcSsEppEv
假设R0是字符串“Hello”
1
2
3
4
5
6
7
8
9
10
11 .text:0001571C 82 B0 SUB SP, SP, #8
.text:0001571E 01 90 STR R0, [SP,#8+var_4]
.text:00015720 01 9B LDR R3, [SP,#8+var_4]
.text:00015722 1B 68 LDR R3, [R3]
.text:00015724 5A 1C ADDS R2, R3, #1
.text:00015726 01 9B LDR R3, [SP,#8+var_4]
.text:00015728 1A 60 STR R2, [R3]
.text:0001572A 01 9B LDR R3, [SP,#8+var_4]
.text:0001572C 18 46 MOV R0, R3
.text:0001572E 02 B0 ADD SP, SP, #8
.text:00015730 70 47 BX LR先是在堆栈分配内存
然后将R0放到堆栈
从堆栈取出字符串地址到R3
取出字符串到R3,此时R3的内容是十六进制格式的字符串Hell
,也就是0x6c6c6548
然后将R3 + 1
后放到R2寄存器,所以R2为0x6c6c6549
然后从堆栈重新取一次原字符串地址,将R2也就是+1后的字符串0x6c6c6549放到原字符串地址,然后复制到R0,接着就是释放堆栈并返回
1
2
3
4
5
6
7 .text:000153FC 01 AB ADD R3, SP, #0x18+var_14
.text:000153FE 18 46 MOV R0, R3
.text:00015400 FF F7 D8 FF BL sub_153B4
.text:00015404 23 46 MOV R3, R4
.text:00015406 18 46 MOV R0, R3
.text:00015408 04 B0 ADD SP, SP, #0x10
.text:0001540A 10 BD POP {R4,PC}
000153FC>
取参数3到R3,然后复制到R0作为参数传入sub_153B4
,这个方法自己看看吧00015404>
然后将R4复制到R3,又将R3复制到R0当作返回值,直接结束了
这个R4寄存器全程只见过三次,第一次是将它放到堆栈,第二次是复制到R3寄存器,第三次是pop。这个寄存器其实和这个方法的第一个参数也就是寄存器R0指向同一个地址,它是方法sub_3ec48
的返回值,详细可以看到地址0001421C
处
1 | .text:00014218 2A F0 16 FD BL sub_3EC48 |
总的来说这个方法就是替换空字符和字符’+’为字符’a’,其他的操作没看到有啥太大的影响,具体可以自行调试一行一行的分析
接着是最后一点了
1 | v22 = (char *)sub_3E4D4((int)&v32); |
这里的v32和上面说的R4、R0的地址是同一个,所以这里的v32就是替换过空字符和’+’的最后的params,然后sub_3E4D4这个方法相当于从v32指向的地址取出字符串,然后就是调用getName加密
先前说了这个参数在添加到map后是排序了的,按字母表顺序,比如如下处理后的参数
1 | 0xc930f36d======================= |
可以得到如下排序
1 | _rticket |
然后上全部代码
1 | import requests |
然后运行
1 | ❯ python3 crawl.py |
心里是不是跟吃了shit一样难受;为了搞清楚为什么返回2154,我hook了下java层调用部分来对比:
1 | String.prototype.format = function () { |
期间分析了很多地方,然后我突然想通了,我干嘛瞎分析这些玩意儿,直接抓包hook套用不就完事了,只需要改时间戳就行了,所以有了下面的代码;(**其实上面没有返回数据是因为params和加密用到的参数不一致
**)
1 | import requests |
然后运行,输出,完美
1 | ❯ python3 crawl.py |
最后
所有脚本、ida文件、jeb文件,以及最后的爬虫脚本都在这:5a6J5Y2T5oqW6Z+z55+t6KeG6aKRdjE=
- email: zckuna@163.com
- qq: 47439134