Datacon2022大数据安全分析竞赛软件安全赛道初赛第四名,复赛第四名
Android成分分析题要求通过分析主办方所提供的App文件,提取出App中的安卓第三方库信息,包括库名称、库版本、库识别依据等信息。具体字段如下:
sdk_name:第三方库名称;
sdk_path:第三方库包名路径,基于代码识别;
sdk_key:第三方库识别依据,基于配置文件识别;
sdk_verion:第三方库版本;
sdk_type:第三方库识别类型,code(代码)或者config(配置文件);
sdk_id:第三方库用户标识。
第三方库用户标识一般需要开发者通过实名注册账户向服务提供商申请id和key来获取,这一类的id和key作为开发者的标识符id硬编码在App文件中。
我们的思路是首先收集现有的主流国内外SDK,提取出它们的特征,并将这些特征作为SDK的识别依据。这样,我们就可以自动生成SDK分析报告,帮助用户更好地理解它们的性质和功能。识别器设计的主要思路如下所示:
整体思路是通过分析SDK的特征,对App文件中的SDK进行识别。在这个过程中,我们考虑了两种可行的识别方法,根据SDK特征可能存在的位置进行分类,这两种方法可以互相补充,从而提高识别的准确性。
基于代码特征的SDK识别
第一种方法是在App文件的代码部分进行识别,通过对代码中出现的SDK特征进行匹配来确定SDK的使用情况。
基于文本特征的SDK识别
第二种方法是在App文件的资源文件部分进行识别,通过扫描资源文件中的SDK特征来确定SDK的使用情况。
AndroidManifest文件
<meta-data>
信息:<meta-data>
节点是manifest中用来指定key-value关系的节点,通常被SDK用来记录API_KEY;properties文件
SDK具体收集过程:
字段:在特定的SDK代码文件中查找包含该SDK版本字符串的字段,并把该字段路径作为识别的规则。如果出现类似于版本号的字符串,则记录所有这样可能的字段路径,以便后续定位查找。
函数:和字段收集类似,在特定的SDK代码文件中查找包含SDK版本字符串的字段,并把该字段所属方法的路径作为识别的规则。
包名:通过收集SDK涉及到的所有.jar和.aar这类文件,区分出其中属于该SDK的部分。读取其中所有的类并且提取出其中的包名。
<meta-data>
:<meta-data>
一般从属于<application>
,有时候从属于<activity>
等节点。其中名称部分如果包含了SDK的名称并且只被对应SDK使用,则可以用来作为SDK的判断依据。name部分往往是和厂商相关联的字符串(APP_ID, APP_KEY, APP_SECRET),其对应的value部分可以用来标识开发者。如:
<meta-data android:name="JPUSH_APPKEY" android:value="9f6627a3f8efaa87b929071c"/>
JPUSH_APPKEY表明是极光推送SDK服务,开发者标识为9f6627a3f8efaa87b929071c
。
4大组件的名称:通过遍历manifest中<activity>
,<service>
,<receiver>
, <provider>
获取到所有APP中四大组件的名称并计入报名进行匹配处理。可以作为抗混淆情况下的可靠判断依据。如:
<receiver name="com.xiaomi.push.service.receivers.PingReceiver" exported="false">
<intent-filter>
<action android:name="com.xiaomi.push.PING_TIMER"/>
</intent-filter>
</receiver>
提取出包名com.xiaomi.push.service.receivers.PingReceiver与小米推送SDK服务匹配。
properties:Google系SDK独有,其中包含组件名和版本号。Google GMS, Google Firebase均有使用,通过读取对应文件即可获得版本号。
特征数据收集过程:
我们将.aar包或.jar文件输入到解析器中,并通过AndroidManifest解析模块和JVM字节码解析模块进行处理。处理后,我们会得到两个特征配置文件:Manifest.json和Identifier.json。这两个文件包含了SDK的基本信息,可以用来识别SDK的使用情况。在生成这两个文件之前,我们可以对输出的数据进行手动或自动清理,以提高数据的准确性。
首先编写爬虫爬取到Maven Central上的所有拓展名为.aar的安卓SDK,然后通过MvnRepository来获取这些SDK的描述信息,对英文的描述信息进行翻译,生成它们的名称。生成的名称中可能存在一些对数据进行清洗,以便更好地分析。例如,清洗前的数据:
{
"sdk_pkg": "net.appkraft.parallax",
"sdk_desc": "AKParallax-Android是一个图书馆项目,为ScrollView或ListView中的imageView提供视差效果"
}
清洗后的数据:
{
"sdk_pkg": "net.appkraft.parallax",
"sdk_desc": "AKParallax-Android SDK"
}
首先编写AndroidManifest解析器脚本,解析给定APK并明文输出其中的AndroidManifest内容。编写AndroidManifest解析模块,解析出AndroidManifest中的内容在服务器上解析2000个APK之后,统计出能够解析的AndroidManifest中<meta-data>
和四大组件的名称:
所有ID,KEY, SECRECT字段,都可能对应提取出开发者标识。我们将它们提取出来保存在一个json文件中。例如,原<meta-data>
部分的形式为:
<meta-data android:name="JPUSH_APPKEY" android:value="9f6627a3f8efaa87b929071c"/>
提取后的json文件:
此外,带VERSION的部分可以提取明显的版本信息。例如,华为扫描的SDK,其版本号为1.1.3.301
,使用它的apk中AndroidManifest为:
<meta-data
tools:replace="android:value"
android:name="huawei_module_scankit_sdk_version"
android:value="scan:1.1.3.301"
/>
同时,分析整理出频率出现最高TOP100的SDK:
在各大厂商开发平台官网下载对应的SDK,下载的SDK文件名中大部分带有版本号,没有版本号的jadx打开查找静态字符串。编写代码提取模块,找到版本号字符串所在位置,同时提取SDK中所有包名。如下所示,fields
表示版本号所在路径,methods
表示存在版本号的方法,packages
表示该SDK下所有的包名路径,regex
表示匹配版本的正则表达式形式。
{
"sdk_name": "\u817e\u8bafQQ\u4e92\u8054",
"fields": [
"com/tencent/connect/common/Constants:SDK_VERSION_REPORT",
"com/tencent/connect/common/Constants:SDK_QUA",
"com/tencent/connect/common/Constants:SDK_VERSION"
],
"methods": [
"com/tencent/open/b/h$2:run",
"com/tencent/open/utils/i:b",
"com/tencent/connect/avatar/QQAvatar:a",
"com/tencent/open/b/b:b",
"com/tencent/connect/common/BaseApi:b",
"com/tencent/connect/common/BaseApi:a",
"com/tencent/open/SocialOperation:joinGroup",
"com/tencent/connect/auth/c:<init>",
"com/tencent/connect/avatar/ImageActivity:a",
"com/tencent/open/log/SLog:a",
"com/tencent/open/SocialOperation:bindQQGroup"
],
"packages": [
"com/tencent/open/web/security",
"com/tencent/open/log",
"com/tencent/open/apireq",
"com/tencent/connect/share",
"com/tencent/tauth",
"com/tencent/connect/api",
"com/tencent/open/c",
"com/tencent/open",
"com/tencent/open/b",
"com/tencent/open/a",
"com/tencent/connect/commonchannel",
"com/tencent/open/miniapp",
"com/tencent/connect",
"com/tencent/connect/common",
"com/tencent/connect/a",
"com/tencent/open/im",
"com/tencent/open/web",
"com/tencent/open/utils",
"com/tencent/connect/emotion",
"com/tencent/connect/avatar",
"com/tencent/connect/auth",
"com/tencent/a/a"
],
"regex": "(\\d+[\\._]\\d+[\\._]\\d+)"
}
一些SDK中包名经过了混淆,出现/a /b
一类的路径,可能会造成后续匹配失败。我们对其进行清洗,去掉混淆部分并提出前缀,清洗后的结果:
{
"sdk_name": "\u817e\u8bafQQ\u4e92\u8054",
"regex": "(\\d+[\\._]\\d+[\\._]\\d+)",
"fields": [
"com/tencent/connect/common/Constants:SDK_VERSION",
"com/tencent/connect/common/Constants:SDK_VERSION_REPORT",
"com/tencent/connect/common/Constants:SDK_QUA"
],
"methods": [
"com/tencent/connect/common/BaseApi:b",
"com/tencent/open/SocialOperation:bindQQGroup",
"com/tencent/open/b/h$2:run",
"com/tencent/open/log/SLog:a",
"com/tencent/connect/avatar/QQAvatar:a",
"com/tencent/connect/avatar/ImageActivity:a",
"com/tencent/open/b/b:b",
"com/tencent/open/utils/i:b",
"com/tencent/open/SocialOperation:joinGroup",
"com/tencent/connect/auth/c:<init>",
"com/tencent/connect/common/BaseApi:a"
],
"packages": [
"com/tencent/open/miniapp",
"com/tencent/open/utils",
"com/tencent/connect/commonchannel",
"com/tencent/open/web/security",
"com/tencent/open/apireq",
"com/tencent/connect/api",
"com/tencent/open/web",
"com/tencent/connect/avatar",
"com/tencent/connect/auth",
"com/tencent/open/im",
"com/tencent/tauth",
"com/tencent/connect",
"com/tencent/connect/emotion",
"com/tencent/connect/share",
"com/tencent/open/log",
"com/tencent/open",
"com/tencent/connect/common"
]
}
所有规则都保存在Manifest.json和Identifier.json两个配置文件中,在数据匹配阶段时读取。
特征数据匹配过程:
首先通过解析APK文件,读取AndroidManifest和Dalvik字节码得到APK中所有的manifest信息和类信息。接下来读取json中预处理好的规则,对manifest和代码分别进行匹配。对于manifest,匹配其中meta-data属性的元素名;匹配得到之后可以得到版本号和SDK的相关信息,从而生成Config类型的报告Report<Config>
。例如,匹配到了com.baidu.lbsapi.API_KEY,那么可以得知存在百度地图SDK,并且该meta-data的值是开发者的KEY标识,也添加进入报告。对于代码,匹配其中的字段内容,方法内容和包名。对于包名,如果规则中存在的某条特征包名能够匹配到APK中的一个类,也就是说APK中存在类在SDK的特征命名空间下,那么可以给出报告Report<Code>
,扫描到类以com.sina.weibo开头,则存在微博SDK。而对于字段内容和方法内容,提取到所有APK中的类之后,根据规则到对应名字的位置使用正则表达式匹配疑似版本号字符串。如果能匹配上则可知存在该SDK的对应版本,并保存其版本号。
两方面全部扫描结束以后汇总所有的报告并对于同一个SDK的代码和配置报告整合信息。代码部分得到的版本号会同步到配置报告,配置报告的开发者标识会同步到代码报告,整合去重完成后生成最终报告Results.json。
该挑战要求设计并实现一个反混淆脚本,能够自动处理混淆的PowerShell样本。最终,选手需要在反混淆后的结果中提取出预先插入的IP字段。
要求反混淆结果必须由自动化脚本生成,并且必须具有可复现性。
对于PowerShell样本,首先需要对其进行反混淆,去除混淆措施。
在反混淆后的样本中,搜索包含预先插入的IP字段的代码段,并提取汇总
PowerShell是高度动态的脚本语言,提供了诸如Invoke-Expression,iex等方式执行一段指定的字符串。所以想要像常见的解混淆问题中那样在中端做优化进行解混淆是不现实的。事实上PowerShell内部实际执行也是在提升到Ast之后构造好若干的Pipeline之后以Pipeline单位对文本指令进行解析并执行。正因如此导致了PowerShell中端与后端基础设施的缺失,想要进行表达式化简最好的选择就是PoweShell本身。于是选择AST这样一个前端表示形式。
通过在AST树上进行后序遍历,从叶子节点开始从下往上进行尝试进行化简。常见的PipelineAst之类的节点是重点关注的求解对象。整体思路沿用Invoke-Deobfuscation方案。在化简的过程中变量的跟踪也很还重要,当构造出来的表达式中存在变量的时候要保证所有变量可用。同时因为Ast的控制流表达能力弱,所以对于循环和分支的处理会比较受限。默认不对表达式中存在变量位于循环中的情况进行求解。
我们使用Python的pythonnet库与.Net框架进行互操作,从而把构造好的表达式传递给PowerShell进行化简。
在系统架构上比较特殊,如下:
在Python中编写求解器,负责节点的遍历与处理。当需要进行脚本转AST,Tokenize处理还有表达式求值时把命令传递给PowerShell实例计算。计算后通过Pythonnet把结果转化到Python对象返回。好处在于求解器代码不在PowerShell实例中,不会存在变量或者cmdlet名称污染PowerShell的命名空间。坏处在与PowerShell通过.Net框架返回的对象如果不是整数或者字符串这种常见的类型,Python端操作这些对象会比较困难。
在系统设计上与Invoke-Deobfuscation并无明显区别,如下:
我们为了运行速度舍弃了重命名和格式化过程,对于解混淆后的输出直接用正则表达式匹配IP地址。对于变量追踪部分,因为原版方案使用PowerShell实现,所以变量的值(PowerShell对象)可以直接存储在求解脚本内存中。我们的求解脚本使用Python,所以额外维护变量名到PowerShell示例中临时变量的对应关系。
$var1 -> Value # Invoke-Deobfuscation
$var1 -> $middle_var -> Value # Ours
其中var_1到middle_var名称的匹配关系维护在Python写的求解中,middle_var的变量名可以看作值的别名使用。
我们的方案基于Invoke-DeObfuscation开源版本完全重写并改进,运行平台在Linux。可以通过Docker容器实现低开销环境隔离与平行运行。因为环境和Windows版存在差异,解决了如下问题:
.Net框架string.split()实现bug
参考链接:https://github.com/PowerShell/PowerShell/issues/11720
5.1版本之后.Net中String的Split方法添加了新的重载函数,具体是这两个
Split(Char[], StringSplitOptions)
Split(String, StringSplitOptions)
对于输入像['a','b','c']
,高版本PowerShell会选取第二个重载方法使得表达式求解失败。于是需要在AST树上这样的表达式进行修改强制指定类型[char[]]
来绕过这个bug。
SecureString在Windows实现bug
Windows平台上Securetring API中因为实现问题PtrToStringAuto
可以和SecureStringToBSTR
配合使用,但是在实现正确的Linux上会报错。为了修复这个问题需要在表达式求解的时候找到该误用并修复。
补齐Windows平台变量和环境变量
Linux上PowerShell缺少一部分全局变量,这些变量会被用来凑’iex’这个字符串来实现命令执行,于是需要在创建PowerShell示例的时候进行补充设置。
Get_Variables = {
"ConsoleFileName": None,
"MaximumAliasCount": 4096,
"MaximumDriveCount": 4096,
"MaximumErrorCount": 256,
"MaximumFunctionCount": 4096,
"MaximumVariableCount": 4096,
"PROFILE": "C:\\Users\\Administrator\\Documents\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1",
}
ENV_VARIABLES = {
"ALLUSERSPROFILE": r"C:\ProgramData",
"APPDATA": r"C:\Users\Administrator\AppData\Roaming",
"CommonProgramFiles": r"C:\Program Files\Common Files",
"CommonProgramW6432": r"C:\Program Files\Common Files",
"ComSpec": r"C:\Windows\system32\cmd.exe",
"FPS_BROWSER_APP_PROFILE_STRING": r"Internet Explorer",
"FPS_BROWSER_USER_PROFILE_STRING": r"Default",
"HOMEDRIVE": r"C:",
"HOMEPATH": r"\Users\Administrator",
"LOCALAPPDATA": r"C:\Users\Administrator\AppData\Local",
"OS": r"Windows_NT",
"Path": r"C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Program Files\Git\cmd;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;;C:\Users\Administrator\AppData\Local\Programs\Microsoft VS Code\bin",
"PATHEXT": r".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL",
"PROCESSOR_ARCHITECTURE": r"AMD64",
"ProgramData": r"C:\ProgramData",
"ProgramFiles": r"C:\Program Files",
"ProgramW6432": r"C:\Program Files",
"PUBLIC": r"C:\Users\Public",
"SESSIONNAME": r"Console",
"SystemDrive": r"C:",
"SystemRoot": r"C:\Windows",
"TEMP": r"C:\Users\ADMINI~1\AppData\Local\Temp",
"TMP": r"C:\Users\ADMINI~1\AppData\Local\Temp",
"USERNAME": r"Administrator",
"USERPROFILE": r"C:\Users\Administrator",
"windir": r"C:\Windows",
"WXDRIVE_START_ARGS": r"--wxdrive-setting=0 --disable-gpu --disable-software-rasterizer --enable-features=NetworkServiceInProcess"
}
替换Linux上PSHOME与marshal类上方法
因为PSHome变量和PowerShell有关,不能直接替换,所以需要在表达式求解之前动态实现替换,替换成C:\Windows\System32\WindowsPowerShell\v1.0
。
对于类似于[runtime.interopservices.marshal].getmembers()[3].name
这种强实现相关的混淆名也需要替换,映射关系如下:
GetMember_Alternate = {
'[runtime.interopservices.marshal].getmembers()[3].name': '"PtrToStringAuto"',
'[runtime.interopservices.marshal].getmembers()[5].name': '"PtrToStringAuto"',
'[runtime.interopservices.marshal].getmembers()[2].name': '"PtrToStringUni"',
'[runtime.interopservices.marshal].getmembers()[4].name': '"PtrToStringUni"',
'[runtime.interopservices.marshal].getmembers()[0].name': '"PtrToStringAnsi"',
'[runtime.interopservices.marshal].getmembers()[1].name': '"PtrToStringAnsi"'
}
主要是针对Stage 2的赛题进行优化,因为Stage 2的赛题存在以下特征:
出现了必须简化的函数调用过程
需要把实际参数传入函数才能进行化简,举例:
Function _=_\=_\{
[CmdletBinding()] Param(
[Parameter(position = 0)]
[String]
$param1
)
$param1 = _=____/\ -param1 $param1
$result = [text.encoding]::unicode.getstring([convert]::frombase64string($param1))
$result | Out-Null
}
_=_\=_\('YQBRAEIAdwBBAEQAbwBBAE8AQQBBADQAQQBDADQAQQBNAGcAQQB4AEEARABZAEEATABnAEEAeQBB
AEQAQQBBAE0AZwBBAHUAQQBEAEUAQQBOAHcAQQB5AEEAQQA9AD0A')
可以看到必须把base64字符串给进函数才能进行求解。
解决方法是进行类似于函数内连的展开,把被调用的函数内容展开在调用位置,并把参数替换成实参。然后进行化简。化简后举例:
{
$result = [text.encoding]::unicode.getstring([convert]::frombase64string($param1'YQBRAEIAdwBBAEQAbwBBAE8AQQBBADQAQQBDADQAQQBNAGcAQQB4AEEARABZAEEATABnAEEAeQBB
AEQAQQBBAE0AZwBBAHUAQQBEAEUAQQBOAHcAQQB5AEEAQQA9AD0A'))
$result | Out-Null}
这样等到表达式带入PowerShell执行了就可以得到明文并替换对应的表达式。
所有的分支可以移除
通过分析Stage 2的赛题可以发现大部分判断逻辑类似于
FuNCtion GHtS {
[ CmdLEtbinDIng() ]PaRAm(
[parAmEteR (vAluEfrOMpIPeLine)]
$PUcJq,
[parAmEteR(vAluEfrOMpIPeLine)]
$lkrjoD,
[parAmEteR (vAluEfrOMpIPeLine)]
$BVLpZM
)
IF(( gET-RAnDOM @(0..20)) -eq 10){
$Jqmbi = [SyStEM.io.MEmorySTrEam][sysTem.cOnveRT]::froMBAse64STRING( $PUcJq)
$Jqmbi|mIxpi
}
$eEbw =[SyStEM.io.MEmorySTrEam] [sysTem.cOnveRT]::froMBAse64STRING($lkrjoD)
$eEbw | HlRq IF (( GET-DaTe).MoNTH -eq 5){
$YrJNmVb=[sysTem.cOnveRT ]::froMBAse64STRING( $BVLpZM)
[IO.FiLe ]::WRitealLBytEs( "$pwd/isjstlv",$YrJNmVb)
}
}
这样的if判断Get-Random
的结果,没有实际意义只会干扰简化过程。所以直接识别这样的分支代码删除。
计算得到变量或表达式求值后不输出
有的表达式结果计算完毕之后不赋值只是传给Out-Null
,因为Out-Null
没有返回值所以会导致该结果无法被显示到最终结果里。于是有的flag就会被漏掉,比如
Function _=_\=_\{
[CmdletBinding()] Param(
[Parameter(position = 0)]
[String]
$param1
)
$param1 = _=____/\ -param1 $param1
$result = [text.encoding]::unicode.getstring([convert]::frombase64string($param1))
$result | Out-Null
}
_=_\=_\('YQBRAEIAdwBBAEQAbwBBAE8AQQBBADQAQQBDADQAQQBNAGcAQQB4AEEARABZAEEATABnAEEAeQBB
AEQAQQBBAE0AZwBBAHUAQQBEAEUAQQBOAHcAQQB5AEEAQQA9AD0A')
这里的结果包含flag却在简化后无法显示。
结果方法是在表达式求解的位置进行检查,判断是否在中间变量就已经出现了’ip’这种flag特征。如果有的话则输出避免遗漏。
计算得到结果加载干扰,需要额外处理
有的时候本应该得到’ip:xxx.xxx.xxx.xxx’的位置得到的是存在干扰的数组,比如:
@(105, 0, 112, 0, 58, 0, 49, 0, 50, 0, 51, 0, 46, 0, 49, 0, 50, 0, 51, 0, 46, 0, 49, 0, 50, 0, 51, 0, 46, 0, 49, 0, 50, 0, 51)
实际上就是把字符串之间加上了\x00
,需要在求解完表达式后对该情况进行特殊判断。
H. Chai, L. Ying, H. Duan and D. Zha, “Invoke-Deobfuscation: AST-Based and Semantics-Preserving Deobfuscation for PowerShell Scripts,” 2022 52nd Annual IEEE/IFIP International Conference on Dependable Systems and Networks (DSN), 2022, pp. 295-306, doi: 10.1109/DSN53405.2022.00039.