No.0
前言
在安卓开发中, 处于对性能或者安全性的角度来考虑, 开发者可以编写一种称为本地库的文件(Native Library). 这些库文件通常用c/c++来编写, 编译后以.so作为后缀名, 可以为java层提供接口, 让我们可以在安卓开发中的java代码里面调用这些c/c++编写的函数.
这些文件在编译打包成apk后, 会被放入APK的lib子目录中.
No.1
实例
现在有一个安卓应用, 这个应用可以计算两个数的和, 但是点击计算后弹出需要我们付费完整版
点击去激活, 提示要输入激活码, 我们随便输入内容, 显示用户名或者序列号错误
我们开始分析这个软件, 首先使用Android Killer打开这个软件. 我们在smali/com/example/myapplication文件夹下面发现了LicenseFragment.smali, 和他的子文件 显而易见的这个就是这个软件的验证界面
然后我们打开这几个文件, 在LicenseFragment$1.smali中有一个onClick的事件处理函数, 这里就是验证页面上的那个按钮
这两行用来获取两个输入框的内容, 也就是用户名和序列号
这里调用了checkserial方法, 把返回值放到p1(112行), 下面的if-nez语句判断p1也就是checkserial的返回值是否为0. 如果不为0就跳转到:cond_1, 否则执行124行, 124行的unicode字符解码之后为注册成功. 那么就表示当checkserial返回0的时候就表示注册成功.
我们尝试在全部代码中找checkserial函数, 发现在LicenseFragment.smali文件中声明了checkserial函数, 但是声明语句中有一个native, 也就表示这个函数是位于native library中的.
继续查看LicenseFragment.smali文件, 发现这个Fragment确实导入了一个名为native-lib的文件, 根据命名规则导入的文件应该为libnative-lib.so.
我们在lib文件夹下查看, 发现有几个文件夹对应了不同的平台, 每个平台文件夹下面都有libnative-lib.so的文件, dalvik虚拟机会根据不同平台加载不同的库文件
我们打开arm64-v8a下面的libnative.so, 这个系统架构也就是arm64, 是现在安卓系统的架构.
使用ghidra打开这个文件, 在左上角的函数列表我们看到了几个可疑的函数, 第一个是Java_com_example_myapplication_LicenseFragment_checkserial 这个函数毫无疑问就是我们的checkserial函数, 第二个是strcmp函数, 我们可以猜测这个函数是来比较序列号的.
所以我们从strcmp入手, 发现这个strcmp函数正好被checkserial引用了, 那么直接跳转过去
到这里之后, 发现这里确实在比较字符串并且做判断. 我们分析一下这段汇编代码
在分析代码之前, 我们补充一点arm汇编的语句
· mov 移动指令
· bl 分支链接 相当于x86汇编中的call
· ldr 从内存中取出值放到寄存器中
· ldur 跟ldr作用大致相同, 但是ldr需要字节对齐, ldur不需要
· b.cc b是跳转指令, 后面可以跟cc写条件, 比如b.ne当不相等的时候跳转
· ldp 同时取两个值到两个寄存器
· add 加法指令
· csetm 根据后面指定的条件, 如果满足条件给寄存器置为1
下面我们分析一下这段关键代码
mov x0,sp 把sp(栈指针)移动到x0
mov x1,x19 把x19移动到x1
bl ::strcmp 调用strcmp, 那么x0和x1就是他的两个参数
ldr x8,[x23 , #0x28 ] 从x23偏移0x28处取值到x8
cmp w0,#0x0 比较w0是否等于0 (w0存的是上面函数返回值)
ldur x9,[x29 , #local_48 ] 从x29偏移#local_48处去一个值
csetm w0,ne 这里就是根据上面cmp比较结果来置位, 如果上面cmp比较不相等(不等于0)就给w0全部置1, 也就是-1. 如果为0, 就置0
如果上面strcmp返回0, 那么这个函数返回0, 如果返回值不为0, 这个函数返回-1
cmp x8,x9 比较x8, x9是否相等, 也就是比较canary
LAB_0010083c 如果不相等就跳转
ldp x20 ,x19 ,[sp, #local_10 ]
ldp x22 ,x21 ,[sp, #local_20 ]
ldp x29 =>local_40 ,x30 ,[sp, #0x20 ]
ldr x23 ,[sp, #local_30 ]
add sp,sp,#0x60
ret
LAB_0010083c:
bl ::__stack_chk_fail x8不等于x9就发生了栈溢出, 终止程序
经过上面分析, 发现checkserial 返回0还是其他就取决于csetm w0,ne这一条语句, 我们只需要修改这条语句, 让他给w0赋值为0就可以注册成功了
修改该语句为mov w0, #0 这样函数的返回值就永远为0, 那么就可以注册成功
导出文件, 然后覆盖掉源文件
在Android Killer中重新编译安装apk
再次随便输入, 然后点击检查按钮, 显示注册成功, 软件修改完成
No.2
注意
需要注意的是如果是用的x86版本模拟器的话, 例如Android Studio的模拟器, 或者是一些Android x86镜像, 我们需要修改x86_64下的libnative-lib.so
使用ghidra打开这个文件, 现在就是x86下的汇编
我们分析下这段判断代码
MOV RDI ,RSP
MOV RSI ,R14
CALL <EXTERNAL>::strcmp rdi和rsi就是strcmp的两个参数
NEG EAX 把返回值取反, 会对CF标志位做出影响
SBB EAX ,EAX 如果eax为0, 那么cf=0 这条语句执行后也为0, 如果eax不为0 cf为1, 那么执行后eax为-1
MOV RCX ,qword ptr FS:[0x28 ] 取canary
CMP RCX ,qword ptr [RSP + local_28 ] 判断是否有栈溢出
JNZ LAB_00100889 如果有栈溢出 退出程序
ADD RSP ,0x28
POP RBX
POP R12
POP R14
POP R15
RET
XOR EDX ,EDX
TEST SIL ,0x1
JNZ LAB_0010080e
JMP LAB_00100834
CALL <EXTERNAL>::__stack_chk_fail
这里设置返回值为0还是-1, 关键的语句就是sbb eax, eax 这条语句与cf有关, 那么我们修改该语句为正常的sub语句, 就能保证eax一直为0
导出文件并覆盖源so, 然后再Android Killer中编译
运行程序尝试注册, 发现注册成功
No.3
原文始发于微信公众号(隐雾安全):安卓逆向之Native Library
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论