因为工作需求要去验证一些PoC,而很多PoC需要开natives-syntax才能跑,如果转成纯JS实现又需要花更多时间,所以需要在Android的app/webview里也实现添加 js-flags,方便后面搞分析 :)
PC上比较简单没啥好说的,直接命令行传递参数就行
1 | ./chrome --js-flags="--allow-natives-syntax" |
相关的代码,可以知道参数配置的文件相关情况
具体的操作步骤:
chrome://flags里开启Enable command line on non-rooted devices
把启动参数写到 /data/local/tmp 下,文件名固定
1 | echo "chrome --js-flags=\"--expose-gc --allow-natives-syntax\"" > /data/local/tmp/chrome-command-line |
然后就可以验证PoC了
https://developer.android.com/develop/ui/views/layout/webapps/debugging#java
主要是依赖这个 DevTools来做的,参考:https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/developer-ui.md
1 | am start -a "com.android.webview.SHOW_DEV_UI" |
这样看到的flags配置,并没有开启命令行选项的配置,这个和浏览器不太一样,参考:
https://chromium.googlesource.cm/chromium/src/+/HEAD/android_webview/docs/commandline-flags.md
文件路径是 /data/local/tmp/webview-command-line
1 | FLAG_FILE=/data/local/tmp/webview-command-line |
这样不太行,所以考虑直接frida hook,要注意因为加载目标类在webview的dex中,所以需要遍历classloader找到对应的classloader去钩目标类
1 | Java.perform(function () { |
完整脚本: https://gist.github.com/o0xmuhe/98299328206820d2c55a7f44d300cdc0
和系统的webview做法一样, 找到合适的位置直接hook CommandLine
就行, 不同厂商做法不太一样,这个需要自己逆向一下
https://juejin.cn/post/6847902219757420552
https://developer.android.com/develop/ui/views/layout/webapps/debugging#java
CodeQL Cli
适合批量做扫描,但是扫描结果并不适合直接做批量的运营,仅适合一些实锤的问题,对于一些还需要人工处理判断的结果就不太适合了(要看源码、调用上下文);如果使用VSCode
插件来做,也只是单条规则扫单个/多个数据库,结果倒是很友好,点点点就能读代码来分析了,所以这种用法不适合批量的query
扫描。
如果付费的话自然是可以解决了,可以在CI/CD
中集成,就方便多了 -。- 但是对于个人使用者来说不太现实,所以我就想用一个简单的办法来实现这个目的
CodeQL cli
批量扫描结束后,导入数据库+历史query
结果,直接在vscode
里运营结果,流程为:
1 | CI/CD 扫描 ---> 结果(数据库+扫描结果) ---> 导入VSCode运营 |
目标拆解
GUI
点点点导入query
不要清理,为的是把cli
的扫描结果导入对应目录之后可以直接在CodeQL
的query history
中看到查看日志,猜测是类似的做法,解析某个配置文件,然后导入,所以要么修改文件,要么直接把数据库目录copy
过去就行
1 | Initializing database manager. |
存储位置
1 | /** |
导出信息就能看到了,所以直接改数据库就能添加多个数据库了
/home/muhe/.config/Code/User/workspaceStorage/693bdf324f8bd69cec87e06d65e8d000/state.vscdb
1 | { |
尝试直接修改这个数据库 就可以批量导入了,不需要挨个点点点了 :)
query-history
扫描结果的导入是有个json文件描述的
1 | Reading query history |
我这里随意跑了两个Query,查看这个文件可以看到这两次记录:
1 | { |
所以把query结果按照这个格式填进去就行了。
1 | ❯ tree -L 1 . |
经过测试,运营只需要扫描结果就行,其他的可以忽略
FYI: 其他的文件(log、dil等)是为了下面菜单中展示的功能做的:
一般来说,我们会使用到开源的规则以及自己写的规则,如果有一定的积累的话,自己的规则可以搞成一个qlpack,方便后面对新目标的快速分析或者批量查找问题。
第一种情况,可以利用下面的命令,批量跑特定的规则集
1 | # muhe @ muhe-NUC11PAHi5 in ~/Tools/vscode-codeql-starter/ql/cpp/ql/src/codeql-suites on git:codeql-cli/latest o [18:53:36] |
比如我们尝试使用cpp-security-and-quality.qls
这个规则集跑老版本的XNU作为演示
1 | codeql database run-queries --ram=16384 --threads=12 XNU-revision-2018-October-28--14-31-48 --min-disk-free=1024 -v ~/Tools/vscode-codeql-starter/ql/cpp/ql/src/codeql-suites/cpp-security-and-quality.qls |
FYI: 可以使用 codeql resolve queries ~/Tools/vscode-codeql-starter/ql/cpp/ql/src/codeql-suites/cpp-security-and-quality.qls --format=text
获取这个规则集包含了哪些query
第二种就使用规则仓库中PICO的pack就行,或者直接指定一个qls扫,就是类似的做法了,比如可以自己搞一个qlpack:
1 | codeql database run-queries --ram=16384 --threads=8 --min-disk-free=1024 -- [database] [qlpack] |
对于这种跑query的方式,如果不指定输出,默认结果会放在数据库的 results
目录下,比如:
所以可以写个脚本
state.vscdb
,批量把codeql db
导入query-history
文件,把扫描结果导入最终实现的效果如下 :)
FYI: 两个关键文件的路径不同平台下大同小异:
1 | if 'macOS' in current_platform: |
最近看了一些Bootloader&TZ
以及相关的议题,主要是ARM架构下的内容;正好这几个月我的Leader
领着我们组一起学习ARMv8&v9
架构相关的知识,在阅读这些材料的时候给我提供了不少的帮助,让我理解起来更加容易,也算是变相检验学习成果咯。
于是我便有了这样的感慨 :)
没看完的材料就是TBD的状态 :(
没钥匙也要拧开BOOTLOADER的锁 - Guanxing Wen, ISC, 2017
厂商在ABL里增加unlock bl验证逻辑,针对这部分的安全性研究
启动链脆弱性分析 - Guanxing Wen, ISC, 2018
三星的安全启动分析,攻击TZ实现绕过锁屏码; reference里
@NWMonster 三星的分析和利⽤
我也没找到:( 可惜
EL3 Tour: Get The Ultimate Privilege of Android Phone - Guanxing Wen, Infiltrate, 2019
华为的安全启动探究,利用bootrom漏洞实现打破信任链,从而实现拿到EL1、EL3的权限,然后攻击TEE,非常精彩的议题;需要ARMv8架构相关的知识,理解起来会更轻松 :)
Checkmate Mate30 - Slipper & Guanxing Wen, MOSEC, 2021
华为Mate30的BootROM漏洞挖掘&利用,和之前EL3 Tour那个类似;但是华为通过OTA修了这个洞也是很神奇,
不知道是不是用的ARM FPB特性做的
MediAttack - break the boot chain of MediaTek SoC - neoni, MOSEC, 2022
MTK安全启动分析以及BootROM漏洞挖掘&利用,打破信任链后可以实现对任意分区读写、解密数据等,配合mtk-bypass阅读体验更好
How To Tame Your Unicorn - Daniel Komaromy & Lorant Szabo, Black Hat USA, 2021
打华为的基带,顺带BootROM的漏洞,配合白皮书阅读体验更佳
Test Point Break: Analysis of Huawei’s OTA Fix For BootROM Vulnerabilities - Taszk Lab, 2021
How to Tame Your Unicorn
BH议题中BootROM漏洞 OTA fix后的分析,探究华为的修复手法。
Your Peripheral Has Planted Malware — An Exploit of NXP SOCs Vulnerability - Yuwei ZHENG, Shaokun CAO, Yunding JIAN, Mingchuang QIN, Defcon26
NXP SOC安全启动的错误实现导致可以打破信任链植入恶意程序
Top 10 Secure Boot mistakes - Jasper van Woudenberg, hardware.io, 2019
这个算是一个总结性质的分享,总结了常见的安全启动的错误实现,已经相关的例子,对于BSP来说是个不错的参考材料?
Attack Secure Boot of SEP - Xu Hao of Team Pangu, MOSEC, 2020
TBD
Breaking Secure Bootloaders Iskuri1, BH USA, 2021
TBD
eshared的pixel6_bootloader安全研究系列
Pixel6修复了一系列bootloader的漏洞,作者通过bindiff找到,并深入研究了这些漏洞
没看完的材料就是TBD的状态 :(
Attacking your “Trusted Core” Exploiting TrustZone on Android - Di Shen (@returnsme), BH USA, 2015
华为Mate7的安全研究,从REE打到TEE
Blue Pill for Your Phone - Oleksandr Bazhaniuk & Yuriy Bulygin, BH USA, 2017
Nexus&Pixel EL2的研究
BREAKING SAMSUNG’S ARM TRUSTZONE - Maxime Peterlin & Alexandre Adamski & Joffrey Guilbon, BH USA, 2019
TBD
暗涌2020-小米5c中国产自研手机芯片澎湃S1 - Slipper, MOSEC, 2020
没找到Slide :( 只能结合evilpan的博客来理解了:) 一套fullchain exploit,从EL0一路打到S-EL1
涉及底层的内容,也是上面学习上面内容的时候找到的,归类到这里 :)
2212_huawei-security-hypervisor
详细地分析了华为的EL2实现,这篇详细到什么程度呢?我认为这是一篇生动形象的计算机体系结构课程 :) 非常值得阅读,全搞明白对ARM体系的理解要求很高。
TBD
TBD
2212_advisory_huawei-secure-monitor
华为EL3 漏洞挖掘&利用,可以配合闻观行的
EL3 Tour
议题阅读
bootloader、security boot相关的博客都值得阅读
Exploiting Qualcomm EDL Programmers系列
attacking-titan-m-with-only-one-byte
TBD
这些内容基本上都是围绕ARM架构做的安全研究,在学习的过程中会不自觉的拿optee来做对比,好让自己更容易理解这些内容
看了这些材料以及大佬分享的时间,这些研究真的太有意思了,我怎么没有早点看到
行业原因自然形成的壁垒,在做底层的研究的时候真的很明显,比如BootROM,如果有个手册的话…MTK那个BootROM我看过,这要没手册也太难分析了😭
持续学习非常重要,正反馈让人觉得很爽 😄
恰好今天正好是2022年最后一天,转到IoT组也一年多了,能感受到自己在一点一点进步:
技术
软技能(沟通协作、写文档)
非常感谢玉伟对我的帮助和指导,在对本篇文章中资料的学习过程中,总能和之前我学习or工作中遇到的东西呼应起来,我也想起了和玉伟one on one
的时候他给我讲学习方法、以及他个人是怎么做阅读的,醍醐灌顶来形容我现在的感受可能会比较恰当 :)
希望23年可以进步更多一些,日拱一卒,功不唐捐。
]]>前段时间MOSEC上盘古关于MTK BootROM Exploit的议题非常精彩,所以我画了一些时间对议题内容进行分析,并结合手边能找到的一些材料做了逆向分析,也感谢同事@C0ss4ck在会场拍下了完整的Slide :)
配合MOSEC官方的微博食用更佳 :)
在进行研究之前需要搞明白MTK方案的设备的冷启动流程,议题中提供的图简洁明了:
按照ARM的标准流程preloader应该是bl2
因为后面使用了preloader的洞把BROM dump出来了,所以我判断MTK的preloader应该是和BROM跑在同一个Exception Level的,即EL3,后来也找了一些资料确认了这个说法,但是不确定现在最新的SoC还是不是这样的。
出漏洞的模块在preloader的USB Download模式,MTK自定义了一些命令,在这个模式下USB handshake之后可以发送DA,然后加载DA,随后就可以和DA通信读写分区什么的,类似高通的9008(进edl模式后加载FH),当然如果开启了SecurityBoot,公版的DA无法使用,需要对应签名的DA才可以。
根据大佬的议题内容可知,漏洞是一个整数溢出,是在判断读/写命令地址范围的时候出现的:
因为MTK的方案有很多开发板,所以基线代码基本上都很容易找到,比如使用了MT6737的香橙派-4G-IOT这个开发板(好像停产了,现存的巨贵),有个大哥把代码放github了
根据这份代码,分析这个漏洞其实很简单了
/home/muhe/Code/MT6737/linux/bootloader/preloader/platform/mt6735/src/core/download.c
1 | int usbdl_handler(struct bldr_comport *comport, u32 hshk_tmo_ms) |
支持的命令也很多:
直接定位到 static u32 usbdl_read16(bool legacy)
1 | static u32 usbdl_read16(bool legacy) |
核心逻辑还是 sec_region_check(base_addr,len8);
1 | void sec_region_check (U32 addr, U32 len) |
这里执行了两个检查:
1 | REGION g_blacklist[] = { |
这里就要祭出datasheet里的memory map
根据memory map,利用这漏洞就可以把BROM dump出来了
MTK的话BROM Exp满天飞,多搜一搜可以找到,或者按照dissecting-a-mediatek-bootrom-exploit中的办法,应该也可以,或者对于没开SecurityBoot的设备搞个mini DA进去也可以(参考这里 https://github.com/MTK-bypass/bypass_utility/blob/master/main.py#L111
)。
这里以某个SoC的BROM为例作分析,推荐使用Ghirda来做,选ARMv7就行。
1 | DECIMAL HEXADECIMAL DESCRIPTION |
前面还是喜闻乐见的中断向量表,根据reset handler,能定位到类似main的位置,但是我们的目的是分析usb dl的逻辑,这里我看了下已知的文章,可以通过handshake来确定,直接暴搜一波 A0 0A 50 05
,但是这里需要注意,有两个handshake,uart和usb的,需要做好区分,然后就可以定位到 process_cmd() 里了。
然后可以还原出来 相关标志位,如 security boot & SLA & DAA。
不过这显然不是这次的目的,这次是想找到盘古议题中提到的两个BROM的漏洞 :)
根据MTK的公告可知和议题内容,这个应该是那个Issue1,即 Endpoint processing vulnerability
的这枚漏洞 :)
我这里根据几个地方来确认函数位置的
[USBDL]
开头的,和timeout相关https://github.com/chaosmaster/bypass_payloads
中,我目前这个方案的一些寄存器、函数地址来确定的,比如可以确定 1 | void (*send_usb_response)(int, int, int) = (void*)******; |
最终让我找到了这个漏洞,和我最开始预想的差不多,处理USB协议相关的逻辑,不过是在标准的流程后面
说来也比较巧合,rrr拍的图里似乎没有标题为MTK BootROM Vul #2
的slide,所以我目前还没有分析出来,只找到了一些相关的资料辅助分析:
https://yhsnlkm.github.io/2019/08/14/USB相关/应用层遍历所有接入的USB设备-1/
https://github.com/mtek-hack-hack/mtktest/blob/master/%20mtktest%20--username%20qq413187589/N65/N65_V1/usb/src/usbacm_drv.c
比较有意思的是链接3里面的这份代码,看着很像古早时期的BROM源码 -.-
在usb相关的目录也找到了一些议题中提到的信息,比如CDC、data_ep_in_info
,以及议题截图中一些变量命名,基本上都对的上,我猜测这应该是因为这是一种标准实现,所以延用这些命名方便分析,那么找洞的方向就有了:
Character-formatting command vulnerability
)看了几个地方还不是很确定- -. 失败
当然,如果能绕过SLA,加载自定义的DA,那DAA也是可以绕过的
通过SP Flash Tool可以对设备进行读写
所以,对于开了SecurityBoot的设备,就不能用公版DA了,大佬的议题中也是以开了SecurityBoot的设备为例讲的,通过前面的漏洞disable sla & daa,从而实现加载自定义的DA,然后通过这个DA来读写任意分区,从而实现加载任意代码的目的 :)
大佬在议题中对MTK的DA做了详细的介绍,这里主要涉及了
MTK的SP Flash Tool里带的这个公版DA其实是个DA的合集,SP FlashTool根据读到的chip id选对应的DA用来交互
这里提到了一个EMI file,stage1会根据这个EMI file来初始化DRAM,既然可以从preloader里后去,那么前面的基线代码里妥妥也会有了
当然也可以借助工具来解析出来,比如这个 https://github.com/mr-m96/MTKPreloaderParser
, 相关内容就不展开了,为了理解议题内容的话,只需要了解这个东西的作用以及在哪里就行了:)
stage2是比较关键的内容了,它被stage1加载到了dram里执行(前面初始化dram这里要用)
这里列举了secure enable的情况,DA的能力将受到限制,即一部分功能无法使用,作者通过之前的BROM exploit disable了daa,然后加载自己patch过的da,从而使用这个patch过的da来实现全分区的读写,以及使用da中全部的功能。
这部分感觉PPT顺序有点问题,不过也不是特别影响理解吧,主要是启动过程中对加载的镜像完整性校验相关的介绍,这块和后面大佬讲攻击流程能对上。
github随便搜了下,就能看明白这个东西了 :)
主要是有这么个结构体来描述对应的镜像的安全配置,是否受到保护、能不能刷这个分区等等啥的。
相关的部分代码,这是在加载镜像之前,加载这个policy,然后根据结果去对镜像做对应的操作,比如 是否应该做校验
1 | static char get_sec_policy(unsigned int policy_entry_idx) |
这里的话,参考dissecting-a-mediatek-bootrom-exploit 的介绍会了解的更清楚,简化一下描述就是:
需要找到需要的函数、全局变量的地址
send_usb_response
usbdl_put_dword
usbdl_put_data
usbdl_get_data
uart_reg0
uart_reg1
sla_passed
skip_auth_1
skip_auth_2
exp工作流程参考 common exp,类似议题中的Vuln1
当然,所需要覆盖的变量也比较好找,把cmd是 0xd8
的 CMD_GET_TARGET_CONFIG
为入口就可以找到需要的东西了
直接参考 common exp,就行,利用漏洞获得的任意地址读写能力去覆盖
sla_passed
skip_auth_1
skip_auth_2
这三个变量,然后就可以加载任意da,并且禁用了daa
start.S
直接跳main函数,里面逻辑也很简单,覆盖变量,然后接收下个阶段的交互(usb handshake),方便后续加载DA啥的,交互完毕,就正常进入usbdl模式去了
1 | int main() { |
这个模式看描述是MTK的一个特殊的测试模式,也算是一个之前没见过的攻击面
在这个模式下,可以做很多事情:
巧了,咱手里正好有个某个MTK方案的设备的完整镜像 :-) 根据PPT中的信息,可以check下相关的逻辑
我这个设备没有找到相关的逻辑,应该是删除了这个模式,不过幸运的是 meta_tst
没有删除:),而且根据PPT里的内容,这个服务应该是比较核心的,MTK设计了私有协议做一些交互
分析的难度也不大,而且有趣的是如果你在github上搜一些特定的字符串,会发现很多有意思的repo :) 这对理解一些逻辑很有帮助
这没什么可说的,既然从源头破坏掉了信任链,那么自然可以做任何事 😎
基本上一些很成熟的“取证”工具都能干- 。- 比如这一篇
support-for-mediatek-devices-in-oxygen-forensic-detective
感兴趣的话可以阅读一下
这次虽然过程艰辛又带着一些遗憾,不过个人起码了解了MTK方案BROM Exploit的思路,vuln#2还没找到,后面等不忙了时间多了再尝试看看好了 :)
https://github.com/SoCXin/MT6737/tree/master/linux
https://github.com/chaosmaster/bypass_payloads
https://tinyhack.com/2021/01/31/dissecting-a-mediatek-bootrom-exploit/
https://www.cnblogs.com/wen123456/p/14034493.html
https://blog.csdn.net/u011784994/article/details/104898430
https://github.com/rn2/ven/blob/db95d7f096/hardware/meta/common/README
https://blog.oxygen-forensic.com/support-for-mediatek-devices-in-oxygen-forensic-detective/
]]> 严格意义上来说本文应该叫做: <<我本来只是想救个砖,但是却逆向了刷机工具尝试搞清楚android unlock
的原理>> :D
前段时间因为一些工作需求想给手里的测试机(一加7Pro)刷个ColorOS,因为之前想体验Android12,机器是刷了个userdebug的lineageos,遂尝试了卡刷、sideload等之后机器被我搞坏了,开机直接recovery,报错信息是什么 mount fs的时候失败了 :( 没办法只能救转了,逛了一圈论坛发现有人提供9008刷机工具,通过万能的9008救回来之后,我就想做点别的: 把他的firehose“偷”出来玩玩。
刷机工具解压之后就几个文件,一个刷机工具 msmdownloadtoolv4.0.88,还有个guacamole_21_H.04_190416.ops
,一看就是固件包,然后就是一些完整性校验用的文件。
根据经验,这类刷机包里应该是内置了firehouse的,可以考虑两条路:
方法2是我最开始尝试的办法,但是dump了几次,发现了好几个ELF,但是都不对,所以尝试方法1 :D 很显然这个包是厂商自己搞得加密,不过网上已经有大佬分析了(早用方法1就少走弯路了),所以根据 How to Extract/Decrypt OnePlus OPS Firmware 提供的工具,可以成功吧固件包解开,获取到firehose
1 | # muhe @ muheMacBookAir in ~/Work/play_with_oneplus7pro on git:main x [22:01:37] |
随便试了一把读分区,是可以的,说明firehose是没问题的 :)
然后就想着顺手看点别的,研究研究Qual+Android平台的解锁BL是怎么实现的,遂有了后续的过程。
参考android-9-r1, 因为现在用的一加的系统的是Android9的
没在开发这里允许解锁BL的话,直接fastboot oem unlock是不行的
1 | public void onOemUnlockConfirmed() { |
1 |
|
1 | // The user has the final say so if they allow unlock, then the device allows the bootloader |
1 | /** |
1 |
|
1 | private void doSetOemUnlockEnabledLocked(boolean enabled) { |
设备文件的某个位置写1,看起来是修改配置了
1 | private static final String PERSISTENT_DATA_BLOCK_PROP = "ro.frp.pst"; |
在一加上看是 :
1 | 130|OnePlus7Pro:/ $ getprop ro.frp.pst |
那么操作就是写这个分区了,把enbale标志位写进去,尝试进edl把config读出来看看
1 | PS C:\Users\Admin> adb reboot edl |
设置了这个标志位之后:
发现设备已经是允许unlock操作了(这里的允许是允许你去 fastboot oem unlock
)
把UEFI PI Firmware Volume
从abl.elf里切出来
uefi-firmware-parser 解析
1 | $ uefi-firmware-parser -e test |
./volume-0/file-9e21fd93-9c72-4c15-8c4b-e77f1db2d792/section0/section1/volume-ee4e5898-3914-4259-9d6e-dc7bd79403cf/file-f536d559-459f-48fa-8bbc-43b554ecae8d/section1.pe
就是我们需要的LinuxLoader
这算是UEFI的一个应用程序
然后就去找 fastboot oem unlock 对应的处理逻辑 :
如果没有在设置-开发者选项中点击允许解锁BL,直接oem unlock是不行的,
在abl中也找到了对应的报错信息:
这里没符号不太好理解,要么找edk2的源码做参考辅助分析,要么某个基线代码build一份带符号的abl出来,这里因为没有在设置中 “允许解锁boot loader” 所以可以结合上面AOSP中的代码做辅助分析。
看看LinuxLoader的源码会更好理解
这里其实是判断了两个标志位:
edk2开源实现中默认的保护分区,这个是可以修改的
结合LinuxLoader的源码,网上可以找到一些leak的实现,能用于辅助分析
在入口 LinuxLoaderEntry
开始的时候,会初始化一个Deviceinfo的结构体
1 | // Initialize verified boot & Read Device Info |
结构体如下:
1 |
|
可以根据这个gEfiQcomVerifiedBootProtocol
去基线中搜到对应的实现,这里就无法展示了。
结合利用FH读出来的devinfo分区:
看来
1 | BOOLEAN is_unlocked; |
都是0,这个和目前未解锁的状态是符合的。
这里我本来想用QFIL的,但是一加的firehose显然是自己改过的,只能读,写的话有个认证token,所以考虑了开源实现 edl,这个工具我发现对xiaomi和oneplus有支持,
就在我想着能一把梭实现 r/w
的时候,悲剧发生了 :
1 | # jiazhenjie @ mbp in ~/tools/edl on git:01f84bf o [16:54:13] C:1 |
这个需要逆向刷机工具来分析了
喜闻乐见的逆向环节
因为每次都会发token,所以想着抓个包,如果固定,那就万事大吉,结果发现不固定
1 | <data> |
这两个值并不是固定的,应该是固定算法+一些随机数算出来的
核心逻辑还是在计算这两个值,算是在刷写分区之前的验证工作,根据edl中的代码可知大概的逻辑,辅助逆向就方便多了
先把结论放前面
1 | head = c4b95538c57df231 |
为什么edl直接刷会失败呢?
1 | def getprodkey(self, projid): |
其他地方这个工具都是对的,逆向过程的笔记没整理,也比较简单,没壳没混淆的,找到关键位置慢慢看就行了。
想模拟一下把ufs吹下来修改后焊回去的操作
修改了edl之后发现是可以正常写分区的
修改devinfo之后刷回去,发现还是locked,看来只改这里是不行的, GG~
abl分析的时候提到了这个protocol,所以想搞清楚为什么失败就要看看这里
根据boot_images/QcomPkg/Drivers/VerifiedBootDxe/VerifiedBootDxe.inf
可知对应的实现在同目录的 VerifiedBootDxe.c
中
这里注册了这个protocol
1 | EFI_STATUS |
要看的方法是 VBRwDeviceState
对应的是 QCOM_VB_RWDeviceState
,这个代码很长就不贴了,只放关键的一部分:
1 | /* We use devinfo partition when the device is not secure */ |
显然,只有没烧efuse的时候才会用devinfo作为存放是否unlock的标志,这一点也符合预期,至此这次探索基本上就结束了。
我也把firehose和对edl的patch放到了github,过程也确实好玩:) 不过还是有不少没研究到的地方,比如他的verifyboot实现是否安全啥的 -。-
https://www.droidwin.com/how-to-extract-oneplus-ops-firmware/
https://zhuanlan.zhihu.com/p/427390226
https://github.com/theopolis/uefi-firmware-parser
https://blog.omitol.com/2017/09/30/Bypass-QCOM-Secure-Boot
https://tjtech.me/analyze-oem-unlocking-under-android.html
https://www.oneplusbbs.com/forum.php?mod=viewthread&tid=4730052
]]>这里直接从optee-examples中最简单的hello world入手来看的,从宏观上来看整个调用流程是 :
1 | CA --> optee client --> tee driver --> ATF --> TEE --> TA |
根据个人的理解画了个省流版本的图,省略了部分调用
1 | //1. 初始化context用于和TEE交互 |
1 | 1. 执行的入口,会话的另一端 |
1 | TEEC_OpenSession -> TA_CreateEntryPoint |
TEEC_InitializeContext → 打开tee driver,要用于通信了 ,主要是一些初始化的工作
1 | TEEC_InitializeContext |
注意此时的CMD是 TEE_IOC_VERSION
,对应执行的是 tee_ioctl_version
1 | // context |
此时CMD是 TEE_IOC_OPEN_SESSION
,到tee driver中查看对应的处理逻辑 :
往后会调用到对应的handler:
1 | rc = ctx->teedev->desc->ops->open_session(ctx, &arg, params); |
在进TEE之前,传递的参数需要做转换,反过来也是;从REE往TEE走,其实是一个入口 do_call_with_arg,这些operations
都定义在:
1 | /** |
直接在目录中搜open_session
发现有两个实现,这里的话ffa_abi.c
中的应该是FF-A标准对应的那个实现,这里直接看smc的那个就行, 即linux/drivers/tee/optee/smc_abi.c
里 :
1 | 910 while (true) { |
中间这个 smc.invoke_fn
就是通过smc进入到ATF,然后ATF会转发到TEE处理
对于ATF来说,这是一个通过 SMC #0
过来的中断,这是core内部发生的,且异常等级发生了变化,所以应该是到了ATF的第三组向量表的sync中断处理程序处
这里细节就不深入看了,主要是为了梳理工作流程,ATF里会调用到系统启动的时候注册的optee的tspd来处理,(opteed_smc_handler 函数)
这个handler里会保存 non-secure的上下文,恢复secure的上下文,然后直接eret到TEE侧。
进入optee之后来到:
1 | 53 uint32_t thread_handle_std_smc(uint32_t a0, uint32_t a1, uint32_t a2, |
第一次走到 thread_alloc_and_run,传入参数是 thread_std_smc_entry
, 所以会执行到 thread_std_smc_entry
后续的流程 :
1 | __thread_std_smc_entry |
至此,到了关键的逻辑:
1 | 538 TEE_Result __tee_entry_std(struct optee_msg_arg *arg, uint32_t num_params) |
这次的cmd是 open session所以走 entry_open_session
函数
1 | 373 res = tee_ta_open_session(&err_orig, &s, &tee_open_sessions, &uuid, |
然后去加载对应的TA,在 tee_ta_open_session // tee_ta_manager.c
1 | 715 res = tee_ta_init_session(err, open_sessions, uuid, &s); |
加载完毕之后,如果成功加载了,那就调用 ts_ctx->ops->enter_open_session(&s->ts_sess);
根据注册信息,应该是 user_ta_enter_open_session
调用到 user_ta_enter 函数,此时还是在optee里的,需要跳到TA去执行
1 | 166 res = thread_enter_user_mode(func, kaddr_to_uref(session), |
S-EL1 → S-EL0,应该是eret过去的
1 | __thread_enter_user_mode(regs, exit_status0, exit_status1); |
跳转前设置好了上下文,所以eret后就回到了TA中执行,这就到了TA中的 TA_OpenSessionEntryPoint
逻辑基本和上面OpenSession差不多,差别就在于传递的 InvokeCommand
所以最后是走到
1 | user_ta_enter_invoke_cmd |
然后调用到TA的 TEEC_InvokeCommand
函数
1 | void TEEC_CloseSession(TEEC_Session *session) |
也是类似的情况,调用到内核里tee_ioctl_close_session ,区别只是cmd不同,最后会一路到TA侧的 TA_CloseSessionEntryPoint
关闭打开的驱动
1 | void TEEC_FinalizeContext(TEEC_Context *ctx) |
根据上面的流程梳理,只要在optee 往TA里跳的时候下个断,就能去分析TA了,然后再加载TA的符号就能快乐地debug了,没有源码那就纯黑盒调试TA了
结合optee的文档 里的描述,会用到TA的 .text段 LMA信息
1 | $ objdump -h 8aaaf200-2450-11e4-abe2-0002a5d5c51b.elf | grep ".text" |
启动ADS,然后在加载tee的时候断住,加载tee的符号,参考我上一篇博客就行了。
如果想调试全部的过程,按照文章把 Linux kernel、 bl31 runtime 的符号也加载进来就行了
1 | b user_ta_enter_open_session |
然后执行CA,可以观察到已经断下来了
其实这个时候TEE侧log已经看到了TA被加载到了哪里了,直接下断也可以的
1 | b *EL0S:0x40060020 |
但是没断下来且报错了,很奇怪的是eret之后 还是显示SEL1,我查看了currentel寄存器之后发现确实是在EL0的
问了下组里的大佬,这个反汇编窗口显示的ELxS/N
应该是这块内存的属性,而不是当前执行状态 (之前直接靠这个tag来做判断,看来是错的离谱了)
个人猜测 因为TA加载是optee做的,所以可能optee分配出来的内存就是EL1S,所以跑到TA的时候,反汇编窗口地址tag会显示EL1S
然后尝试加载符号就行了:
1 | add-symbol-file /home/muhe/Study/optee-fvp/out-br/build/optee_examples_ext-1.0/hello_world/ta/out/8aaaf200-2450-11e4-abe2-0002a5d5c51b.elf 0x40060020 |
]]>https://blog.csdn.net/weixin_42135087/article/details/119384252
https://www.timesys.com/security/trusted-software-development-op-tee/
https://optee.readthedocs.io/en/latest/building/gits/optee_examples/optee_examples.html
Background
最近阅读了一篇论文<<The Convergence of Source Code and Binary Vulnerability Discovery – A Case Study>>
,很巧合的是论文的研究中,关于将SAST工具应用于二进制文件(通过decompiler),即获取伪代码之后,在伪代码上跑SAST工具来找漏洞这个模式我和@C0ss4ck
一起做过,在我们收到一些成效之后发现也有人做了类似的工作,不过他好像没有特别深入 :D
我们这做主要是因为一些不可说的原因,最开始是@C0ss4ck
搞的用IDAPython搞的工具,但是由于做适配比较麻烦不够灵活;后来我提出了decompiler+weggli
的做法的时候,我们都不是那么的看好,但是搞了一些demo发现确实可行,对于一些简单的漏洞模型是可以召回的,主要的瓶颈就在decompile code的质量和规则的编写了,同时由于weggli本身不支持数据流,并且主要是过程内的漏洞模式匹配(AST regexp),所以后面就又面临瓶颈的问题了;在我做调研的时候,发现了这篇新鲜的论文,在读完之后感触良多,对decompiler+SAST
的做法也有了更多的理解。
PAPER
这篇论文讨论了源码/伪代码+SAST工具在漏洞挖掘上的效果,以及对于伪代码+SAST
这种模式的局限性的探究,对其中的误报&漏报根本原因的分析。
尝试召回 real world vulns
基本上都是优秀的工具,其中两款商业工具并没有写具体是啥,但是这个 Comm_1
看起来好像Coverity :) 不管怎么样,其中的 codeql
和joern
我很感兴趣,毕竟可以自定义规则,这对我来说无疑是更好的,可以召回更多问题 & 适用于更多的场景。
对于漏洞的选择,该论文也选的比较广泛,各种类型都有,复杂度也够,可以更好的“测量”这些工具 :)
反编译代码并不是开箱即用的
对于二进制文件,伪代码+SAST
的模式可行,但是有限
SAST
工具设计上是给源码用的,这是by design
的;二进制文件丢失了关键信息(尤其是编译器优化的影响),不适合给SAST
工具做分析,这也是为什么论文中说不用LLVM Lifter的原因
decompilers are still designed to generate code that is easy to understand for humans, and SAST tools are still designed to parse “well-written” code that is not generated by a machine.
编译器优化很有意思,有些漏洞因为优化inline,所以从过程间–>过程内,decompile之后的代码反而找到了漏洞 :)
优化的话, 两个思路,相当于朝着同一个方向前进的路:
SAST
工具,让其适配反编译的代码个人认为比较核心的地方了
经过编译优化,下面的代码中s1由于是指向了为初始化内存,所以可能会报告成栈溢出
说是这样说,但是我个人认为,这种情况是可以避免的,通过汇编是可以判断出来这个 stack buffer 有多大,这种误报理论上是可以排出的,前提是收集更多的信息 <– 优化项
没什么好说的,变量类型分析错误,在对伪代码中产生误报正常,这个如果不人为干预,确实没办法
由于缺少必要的信息,导致在 sub 129CF
丢失了a2和a4的信息,导致SAST工具产生误报
函数指针问题,这个很巧合,前两天请教@jmpews
的时候,提到了decompiler+SAST的做法,当时我问的是 joern
,大佬的说法是C更不好整, c++ 还可以走 demangle
上面的代码反编译之后得到:
基本上是无法分析的,就算是人肉逆向,也要重建这个结构体,然后转换变量类型,自动化不太现实,这块的误报确实没办法
还是变量类型的问题,反编译代码对特定变量类型分析错误,导致的误报
反编译之后:
对v22 和 v26 类型识别错误,混用了 uint8_t
和 int64
,所以可能会误报整数溢出 :(
这是一类特殊情况
这里显然把||
和&&
搞混了,但是反编译之后
这类表达式在编译器优化处理之后,再反编译,该表达式已经看不出来了,就会漏报这个问题 :(
decompiler+SAST
可行,但是需要优化,能覆盖的场景也有限,目前来看IoT场景是比较适合的,比如各种奇葩的命令注入,显然是可以召回的。
个人来一个大胆的构想,从ctree上收集信息,生成codeql那样的rel db,目前来看比较接近的是joern,但是它是基于ghirda,优化空间还是有的。
https://dl.acm.org/doi/10.1145/3488932.3497764
https://security.humanativaspa.it/automating-binary-vulnerability-discovery-with-ghidra-and-semgrep/
]]>我们的目标在 update_sd_base.zip里,其他部分咨询了是一些出厂带的APP,比如里面就看到了今日头条 抖音啥的。
直接解开 update_sd_base.zip 到下一步
直接用 https://github.com/jenkins-84/split_updata.pl/blob/master/splitupdate 来分割就行
1 | ~/android-simg2img/simg2img SYSTEM.img system1.img |
尝试读文件的时候发现报错
dmesg发现 :
1 | ~/extracotr/erofs_tool.py extract --verify-zip system1.img harmony_system |
https://zhuanlan.zhihu.com/p/60617375
]]>本篇主要是环境配置、调试、流程梳理
https://optee.readthedocs.io/en/latest/building/prerequisites.html
1 | $ repo init -u https://github.com/OP-TEE/manifest.git -m qemu_v8.xml |
同步下来的仓库如下
运行一下试试看:
需要指定版本跑的话 : make -f qemu_v8.mk run-only
这里以qemu-v8为例
1 | cd build |
1 | make DEBUG=1 -f qemu_v8.mk run-only |
因为Makefile中启动的时候已经写了设置了 -s -S
了 ,所以可以直接连接
可以从这里下载对应的gdb来用
装了libncurses5-dev
还是找不到so的话,可以参考 https://www.cnblogs.com/wanglouxiaozi/p/14987053.html
gdb-multiarch
也可以,更好用
符号加载
1 | bl1 --- /home/work/optee/trusted-firmware-a/build/qemu/debug/bl1/bl1.elf |
1 | $ repo init -u https://github.com/OP-TEE/manifest.git -m fvp.xml |
需要下载 FVP_Base_RevC-2xAEMvA_11.18_16_Linux64.tgz
并解压到optee-fvp
目录下
1 | $ tar -zxvf ../FVP_Base_RevC-2xAEMvA_11.18_16_Linux64.tgz -C . |
1 | # work @ work-virtual-machine in ~/optee-fvp [23:34:36] |
编译流程参考上面qemu_v8部分
1 | # work @ work-virtual-machine in ~/optee-fvp [23:35:06] |
修改build/fvp.mk ,以便启动时进入调试模式
添加:
1 | -I \ |
1 | ################################################################################ |
1 | cd build |
启动的时候需要license,注册个账号就行,先试用。
启动之后,选择 : File->New->Model Connection
模型选择 : Base_RevC_AEMvA 和 Base_RevC_AEMvA
都没法直接调试,好像是模型没装好 :(
点Finish之后,需要手动选择,连接本地的模型 localhost 7100
加载了bl31的符号,然后对入口下断:
1 | b *EL3:0x0000000004003000 |
芜湖 🛫️
ARM v8 的文档
opteeos
跑在 secure world
,ta
在secure world
的上层(el0
); linux
在non-secure world
,ca
在el0
optee
项目中还有个``atf,这个跑在
el3`。
该图来自周贺贺老师的OPTEE系列课程中
这里我直接用了周贺贺老师OPTEE系列课程中的图,我在对着代码分析的时候结合这个图感觉十分的清晰,有助于理解 :)
先来看大概的启动流程
1 |
|
下断点的時候注意,切换到对应的阶段之后再去 file xxx 加载符号
1 | file /home/work/optee/trusted-firmware-a/build/qemu/debug/bl1/bl1.elf |
比如下面要进入tee的时候
加载符号之后:
ads可视化调试记录
add-symbol-file /home/muhe/Study/optee-fvp/trusted-firmware-a/build/fvp/debug/bl1/bl1.elf
b *EL1S:0x0000000004022000
add-symbol-file /home/muhe/Study/optee-fvp/trusted-firmware-a/build/fvp/debug/bl2/bl2.elf
b *EL3:0x0000000004003000
add-symbol-file /home/muhe/Study/optee-fvp/trusted-firmware-a/build/fvp/debug/bl31/bl31.elf
b *EL1S:0x6000000
add-symbol-file /home/muhe/Study/optee-fvp/optee_os/out/arm/core/tee.elf
UEFI 的符号加载比较特殊, 这个部分是相对地址, 并且很多模块是动态加载的, 断点下到加载UEFI的地址, 也就是BL31 跳转到BL33时的地址。 断下后,
commands下通过 cmd_load_symbols 加载, 执行前先要弄清楚几个参数
/home/muhe/Study/optee-fvp/edk2/ArmPlatformPkg/Scripts/Ds5/cmd_load_symbols.py
1 | def usage(): |
-m 参数在
/home/muhe/Study/optee-fvp/edk2-platforms/Platform/ARM/VExpressPkg/ArmVExpress-FVP-AArch64.dsc
1 | # System Memory (2GB - 16MB of Trusted DRAM at the top of the 32bit address space) |
-f 参数在
/home/muhe/Study/optee-fvp/edk2-platforms/Platform/ARM/VExpressPkg/ArmVExpress-FVP-AArch64.fdf
1 | [FD.FVP_AARCH64_EFI] |
b *EL2N:0x88000000
断点触发后, 执行下面的命令加载符号。
1 | source /home/muhe/Study/optee-fvp/edk2/ArmPlatformPkg/Scripts/Ds5/cmd_load_symbols.py -a -m (0x80000000, 0x7F000000) -f (0x88000000, 0x04000000) |
1 | info files |
DxeCore的加载这个脚本处理不了,还是要自己加载
1 | add-symbol-file /home/muhe/Study/optee-fvp/edk2-platforms/Build/ArmVExpress-FVP-AArch64/DEBUG_GCC49/AARCH64/MdeModulePkg/Core/Dxe/DxeMain/DEBUG/DxeCore.dll 0x00fe3d3000 |
现在就正常了:
1 | Symbols from "/home/muhe/Study/optee-fvp/edk2-platforms/Build/ArmVExpress-FVP-AArch64/DEBUG_GCC49/AARCH64/ArmPlatformPkg/PrePi/PeiUniCore/DEBUG/ArmPlatformPrePiUniCore.dll". |
TODO,这部分一直没时间搞,先挂起了
BL33是UEFl,其实UEFI 还会引导grub2, 这里grub2作为一个UEFl的driver(or 应用)被UEFl加载, grub执行完毕,引导linux时,其实linux 内核也打包作为一个UEFl的应用了,所以BL33的执行过程是, UEFI-> grub->linux内核的efi stub -> linux内核
加载符号,注意EL1N:0,因为内核主要运行在EL1N
1 | add-symbol-file /home/muhe/Study/optee-fvp/linux/vmlinux EL1N:0 |
根据启动流程 :
1 | __HEAD |
我们可以对__primary_switch
下断,如果符号对不上,可以根据地址下断
1 | # muhe @ muhe-NUC11PAHi5 in ~/Study/optee-fvp/linux on git:29aee39cf x [23:24:42] |
修改ads的jvm,否则调试的时候容易oom影响体验
1 | # muhe @ muhe-NUC11PAHi5 in /usr/arm/developmentstudio-2022.1/sw/ide [20:20:40] |
添加参数
1 | -Xms4096m |
https://download.csdn.net/course/detail/37655
https://optee.readthedocs.io/en/latest/
]]>AST Pattern Search
核心是使用和 tree-sitter
库,然后搞了 query-tree
来在 AST
上进行搜索,这只能说是匹配特定的代码片段,还达不到程序分析的那个级别,所以理论上只能过程内分析,而且没有上下文啥的 :D 直白点说的话,像是AST
的正则表达式,不过某种意义上来说对于使用白盒方案快速召回一些漏洞也是一种借鉴吧。
当然我也用这个工具做了一些扩展,结合其他工具解决了一些问题,目前看来这个东西还是具有一定的可玩性的 :D
看代码,调试分析
安装Rust
插件,调试的话,会默认再去安装Native Debugging Support
,有了这俩东西就可以调试了
配置传递给weggli的参数的话跟在 --
后面即可 :
1 | run --package weggli --bin weggli -- "{$func($b);system($b);}" -R "func=printf$" /path/to/src |
只描述核心流程
1 | let work: Vec<WorkItem> = args |
构造 WorkItem{qt, identifiers}
调用链:
1 | main |
修正pattern : weggli处理了“不合法的”格式,如:
memcpy(a,b,size)
-> memcpy(a,b,size);
memcpy(a,b,size);
-> {memcpy(a,b,size);}
1 validate_query(&tree, p, force_query)? // 返回 TreeCursor对象,用于遍历AST
语法合法性检查,如果 force_query
为True
,意味着忽略这些语法错误
如 :
1 | "{$func($b);_($b);}" |
对应 :
1 | (translation_unit |
同时还不允许 :
返回的是 : c.goto_first_child();
,即 花括号中间的内容
1
2
3
4
5
6
7
8 Ok(build_query_tree(
p,
&mut c,
is_cpp,
Some(regex_constraints.clone()),
))
_build_query_tree(source, cursor, 0, is_cpp, false, false, regex_constraints)
QueryTree数据结构:
1 | pub struct QueryTree { |
转换的tree_sitter query
(核心逻辑都在 builder.rs
的 QueryBuilder.build
)
1
2 Translate the tree below `c` into a tree-sitter query string.
"{$func($b);_($b);}"
1 | tree_sitter query 1: ((call_expression function:[(identifier) (field_expression) (field_identifier)] @0 arguments:(argument_list [(identifier) (field_expression) (field_identifier)] @1)) )([(identifier) (field_expression) (field_identifier)] @2 ) |
深度优先的方式递归生成query tree string,按照AST解析出来不同的节点,后面跟着的 @x
用来区分不同的 identifier
,方便后面做匹配。
如简单的 {printf(var, bar);}
生成的 query-tree
是 :
1 | ((call_expression |
结合tree-sitter的playground来看就很容易看明白了:
在执行query之前会做
对于需要正则匹配的 identifer
做合法性确认
1 | for v in regex_constraints.variables() { |
确定待解析源码文件(Verify that the --include and --exclude regexes are valid.
) 主要是根据后缀来
随后就是通过管道来处理,分为:
let (ast_tx, ast_rx) = mpsc::channel();
let (results_tx, results_rx) = mpsc::channel();
1 | // Spawn worker to iterate through files, parse potential matches and forward ASTs |
**这玩意描述起来就像个流水线 :D **
详细描述的话就是:在有了 query-tree
就需要把目标文件,解析(parse_files_worker
)成 (Tree, source_code)
,结果发送到 ast_tx
,然后从ast_rx
获取这些信息来执行查询操作(execute_queries_worker
);结果放在 result_tx
,后面处理结果的函数会从result_rx
获取,然后输出。
1 | parse_files_worker(files, ast_tx, w, cpp); |
TODO: 需要细读逻辑
这里简单的加一句print之类的可以来看看每次query的时候目标tree是啥样的(生成过程和query tree类似)
1 | // Run query |
所以这就转换成了一个字符串匹配的问题,结合之前的 -R
,能支持正则匹配,所以说weggli是在AST上搞正则匹配一点都没说错 :D
-p
参数)Question - query construction 这个issue里提到了这个场景,先还原一下场景 :
vuln.c
是个类似的情况,尝试query
1 |
|
vuln
)vuln(argv[1])
假如没有对vuln的调用,那就不回返回结果
这块逻辑主要在 multi_query_worker
,即存在多个workitem的时候会触发,就是在匹配的时候会结合这些query,即将第一个query匹配到的结果先收集起来
1 | let mut query_results = Vec::with_capacity(num_queries); |
然后根据后面的query去做过滤,找到满足的pattern就打印出来
1 | let filter = |x: &mut Vec<ResultsCtx>, y: &mut Vec<ResultsCtx>| { |
query-tree的话增加一个 -v
参数就行,会把query tree打印出来
少量代码测试这样是可以的,也可以使用log模块把信息打出来,不过数据太多了。
1 | diff --git a/src/main.rs b/src/main.rs |
直观多了 :
]]>前期readback
什么都都正常,也切出来了各个分区,并制作了scatter。
折腾的时候发现SP Flash Tool
加载preloader的时候有报错:
1 | 02/23/22 22:35:20.309 BROM_DLL[3848][1900]: DL_HANDLE()::Rom_Load(): ROM loaded, name = preloader (flashtool_handle_internal.cpp:4693) |
所以:
1 | $ file libflashtool.v1.so |
在 DL_HANDLE::UpdateRomFileInfoByPreloader
方法中找到了校验的逻辑:
1 | if ( *((_DWORD *)var48 + 15) == 7 ) |
1 | erro_code = ROM_ID_Class::LoadGFH((__int64)this + 112, *((_QWORD *)ROM + 123), 0, |
ROM + 0x7C
是实际的文件大小GFH + 8
是 解析preloader 中的GFH结构中的size字段
1 | ROM_ID_Class::LoadGFH |
1 | __int64 __fastcall GFH_Internal_Parser(__int64 buf_addr, __int64 flag_0, int type, _QWORD *GFG_st) |
[1]
的位置 找到这个结构,然后把指针赋值,分析这段逻辑,其实就是文件头:
指定preloader文件大小是 0x26794
,修改文件大小即可。
所以只需要修改 preloader文件的长度为其 +0x24
处 4bytes代表size的字段即可
PS:不能修改这个长度字段
之前搞的平台也没注意这个问题,也没报错,但是size对不上,所以需要探究文件格式和为什么检查
介于boot rom 和 bootloader之间的桥梁
,主要工作是初始化环境,包括c环境,timer,gpio,pmic,uart,i2c等以及装载lk镜像至DRAM中,建立起最基本的运行环境,最重要的就是初始化DRAM。
另一种情况是实现了ATF(Arm Trust Firmware)的时候:
ATF实现原理_chenying126的博客-CSDN博客_atf
preloader可以看成一个特定格式的可执行文件,所以需要找入口点。
/Users/muhe/Code/MTK6577/mediatek/platform/mt6577/preloader/src/init/init.s
github上找的一个可能是泄漏的基线代码来参考的
1 | .globl _start |
这个特征还是很明显的,可以试试看:
上面这个0xEA应该是 b指令,可以借由这个搞定基地址
然后是到main.c
,继续人肉找特征
找到了字符串,但是没有引用关系 :(
通过Ghrida的强制整个binary的分析,然后引用关系确定了main的位置,至此就可以往下看了,对比其他平台preloader的源码,能看个七七八八了。
PS : 基地址编译的时候可以指定的,比如在 linux/bootloader/preloader/platform/mt6735/link_descriptor.ld
1 | OUTPUT_ARCH(arm) |
还有 : linux/bootloader/preloader/platform/mt6735/default.mak
github真是个好地方啊,还有一个完整的MT6737平台Linux based的基线代码,全套的环境和build产物都有的,可以看到:
1 | $ file *.elf |
查看相关的makefile可以验证该猜想:
1 | $(D_BIN)/preloader.elf: $(D_BIN)/$(PL_IMAGE_NAME).elf |
遂尝试:
1 | PL_IMG_SECOND_PARTION_SECTION :=.pl_dram.text .pl_dram.data .pl_dram.rodata .pl_dram.start |
addprefix
这个可以忽略
当然,hash想一样还是想多了,毕竟编译环境都不一样,直接上diff:
一共两处:
这个是多了一个GFH结构(说好的 NO_GFH 难道只是说头没有)
preloader_bd6737m_35g_b_m0_LINKED.bin
output.bin
多了一个GFH在尾部preloader_bd6737m_35g_b_m0_NO_GFH.bin
output.bin
多了一个GFH在尾部preloader_bd6737m_35g_b_m0.bin
比 preloader_bd6737m_35g_b_m0_NO_GFH.bin
又多了一个GFH头和尾部的签名数据
所以这里可以认定:
这里以EMMC为例:
EMMC_BOOT + GFH_INFO_EMMC + WTF1 + preloader_code + WTF2
EMMC_BOOT
MT6737/linux/bootloader/preloader/tools/gen-preloader-img.py
GFH_INFO_EMMC :
linux/bootloader/preloader/platform/mt6735/gfh/default/ns/GFH_INFO_EMMC.txt
GFH Part 2 : GFH 的另一部分,还会修改上面的size
linux/bootloader/preloader/tools/pbp/*
preloader_code
preloader.elf
objcopy处理之后WTF 2 :
继续看Makefile来分析:
1 | $(D_BIN)/$(PL_IMAGE_NAME).bin: $(D_BIN)/$(PL_IMAGE_NAME)_NO_GFH.bin $(GFH_INFO) $(GFH_HASH) $(PBP_TOOL) |
遂可以得到:
MT6737/linux/bootloader/preloader/tools/gen-preloader-img.py
生成**linux/bootloader/preloader/platform/mt6735/gfh/default/ns/GFH_INFO_EMMC.txt**
但是这个file size字段是0xffffffff,后续会处理迫于要分析一些SDK里的协议,需要抓到所有的流量来分析交互过程,所以有了这篇记录,主要是基于实时监控Android设备网络封包做的尝试,然后使用相同的思路扩展到了iOS上。
原理图:
1 | tcpdump---nc---端口转发---nc----wireshark |
手机必须root
手机端:
1 | tcpdump -l -n -s 0 -v -w - | nc -l -p 11233 |
PC端:
1 | adb forward tcp:11233 tcp:11233 && nc 127.0.0.1 11233 | wireshark -k -S -i - |
手机必须越狱
手机端:
1 | tcpdump -l -n -s 0 -v -w - | nc -l -p 11233 |
PC端:
1 | ~ iproxy 11233 11233 |
1 | ~ nc 127.0.0.1 11233 | wireshark -k -S -i - |
TrapFuzz的思路Fuzzing Android native库,这就是个简单的Demo,只针对黑盒的库。
brew install automake
构建所有的arch(arm64-v8a, armeabi, armeabi-v7a, x86, x86_64)
1 | make android-all |
坑1: libunwind编译的各种问题:
macos不好使,换linux去编译,然后用ndk r20.
传到手机上试试看:
然后就是写个demo,在手机上跑一下看看情况
hfuzz-cc is missing on android build · Issue #341 · google/honggfuzz
No coverage information on android · Issue #342 · google/honggfuzz
参数 fsanitize-coverage=trace-pc-guard,trace-cmp,trace-div,indirect-calls
这个参数的话会有警告信息,应该是clang 参数的问题。
后面参考了谷歌的文档,替换了参数,结果没警告了,但是cov还是0.
1 | # muhe @ muhe-Parallels-Virtual-Platform in ~/ndk_fuzzing_demo [14:51:59] |
退回到honggfuzz 2.2 然后用最开始 #342 那个issue的编译参数是可以的
完整项目:
1 | # muhe @ muhe-Parallels-Virtual-Platform in ~/ndk_fuzzing_demo [15:53:52] C:130 |
ndk构建命令:
1 | ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk TARGET_ARCH_ABI=arm64-v8a |
.so
参考项目 : https://github.com/CalebFenton/native-harness-target
Android 7.1.1
可以使用这个方式跑起来,不过速度及其的慢。
TODO :
Android 10 :
1 | export LD_LIBRARY_PATH=`pwd`:/apex/com.android.runtime/lib::$LD_LIBRARY_PATH |
经典的 patch 跳转指令,实现一个debugger来获取覆盖率的方案
使用之前 patch跳转的方式搞覆盖率,修改honggfuzz即可
问题 : 创建JVM相关的操作耗时,影响效率
为了解决效率问题,如果可以自己写一个,初始化之后,fork,然后疯狂搞fork出来的子进程即可,这样效率就上去了。
获取patch需要patch的指令地址,直接从p0tools里抄
1 | import idautils |
问题 : 需要想办法做到 hfuzzcc一样的效果,即 把 libhfuzz.a 链接进目标binary,不然没有桩信息。
看起来就是一层wrapper,给clang/gcc编译的时候增加了 CFLAGS
和 LDFLAGS
,看起来只需要按照需求把参数放到 Android.mk
即可。
这里参考ImageIO例子中的编译的参数
1 | cc |
harness里需要主动调用 initializeTrapfuzz()
看起来一切都不错!
获取指定so地址也有了:
参考这里修改honggfuzz的代码即可
这个方案主要是效率实在是太差了,性能损耗都在jvm获取那里了,本来也是工作之余的一个小点子,后面也没深入去看了,个人最开始的想法是 winafl模式搬到安卓上 lol…
https://googleprojectzero.blogspot.com/2020/04/fuzzing-imageio.html
https://github.com/googleprojectzero/p0tools/blob/master/TrapFuzz/trapfuzz.patch
]]> 在今年的ByteCTF中,我出了一道pwn题目,距离上一次打比赛/出题已经过去很久了,所以传统的 heap trick
就没有考虑,而是从我日常工作中挖掘的安全风险入手,简化场景,出了一道 chatroom
,看起来像一个web的奇怪题目。
这个题目其实背后是 Headless Chrome
相关的pwn,我早期的一篇博客其实已经阐述过相关风险,可以参考 Exploit Headless Chrome。 其实这个风险暴露出来的不仅仅是:低版本、误用参数 这两个显而易见的问题,其背后的原因是一些不好的编程习惯被错误地传播:大家都在用 --no-sandbox
参数,好像 it works
就够了,但是在实际场景中,这是很危险的。
我的本意是设计一个类似聊天室的场景,用户可以在聊天室内发送消息、多媒体文件、链接等,尽可能模拟一个真实场景。 处于风控考虑,对于非白名单的链接,需要进行检查(是否恶意,色流等)。对于URL 检查的逻辑,最好是服务端接收到内容之后,判断是否是URL,随后通过RPC调用走到URL检查的服务去。但是考虑到实际题目,我大大简化了这个场景,直接把检查放在前端了,而且我没有混淆JS,所以可以很直接看到一个HTTP请求。
解决了场景问题,聊天室部分直接用了github的开源项目 node-websocket-Chatroom,后端使用 puppeteer来抓取用户的URL。
为了提升一些难度,同时这也是我曾经遇到过的问题:UA不可靠的情况下怎么判断Chrome版本?
所以我直接在启动参数里把UA给改了:
1 |
|
最终题目成型:
主要是 zh1x1an2 同学的做法,Exploit狂轰滥炸术,挨个挨个来,最终拿到flag。
UA不可信,但是V8 和 Blink是可信的,不同Chrome版本会有不同的features,所以可以借助这个点,判断一个大版本,便于后续做利用。
参考 : https://chromestatus.com/features
不过这个需要一些积累 && 测试 :)
随后判断出来版本是 M88 之后,找个合适的nday就可以打了 : )
1 | docker pull muhe/ctf_chal_chatroom:v7 |
访问 http://localhost:3000
就可以本地测试题目了 :)
patch.diff
似乎又有点奇怪,偏离题目原本的出发点。 过去的近一年的时间(本文在21年开的头,期间一直是hidden状态),我接触了万恶的浏览器安全,不过只是一个脚本小子的水平 :(
最开始是由于一些工作上的因素,关注了一些主流的IM客户端,难易程度不等,当然也看了不少前辈的精彩工作,比如二哥的各种奇妙的xss、伪协议打🐧啥的。无奈功夫不到家只能另辟蹊径,再加上大学时候@wuyan学长某次回学校给我们做小组分享的时候展示了当时印象笔记的一个xss的时候提了一句,很多客户端你可以把它当成一个浏览器来看;至此这才有了后来的探究和一点点成果吧,时至今日,相关漏洞早已修复,攻击手法也早已“众所周知”,所以写个记录也没有什么:D
主要是一些Electron和CEF客户端
主流客户端的情况,以前 & 现在
关注客户端安全的同学应该会发现Electron&CEF的应用越来越广泛了,从早些的时候某音乐播放器的xss2rce到后面被关注到内置浏览器本身,当然大佬可能更早的时候就这么玩了 :D
其实是用浏览器框架来开发客户端是一直以来就有的东西,比如下面这张图(可能不完全):
也有直接从chromium做定制开发的,即原生的方式开发,比如某先进IM
这么做的好处是显而易见的:
使用成熟的嵌入式浏览器框架(cef, electron等)能够快速开发应用
能够规避很多复杂的底层设计(c/c++)
前端–>APP跨平台的特性,且很灵活,
更加方便支持自定义协议/扩展/JS对象等
…
与此同时,浏览器的攻击面就自然而然地引入进来了,再结合客户端本身,1+1>2的即视感。本文重点关注浏览器相关的内容,那按照浏览器的思路去考虑就是:
1 | 攻击者构造恶意页面-->客户端访问-->RCE |
这条攻击链路上的前置条件是**客户端可以打开任意URL(直接or间接)**,随后就是常规的浏览器Exploit,分成Render RCE+SBX两部分。
妙就妙在很多客户端出于一些特殊的需求他没有开沙箱。
这里有一份统计 : https://github.com/sickcodes/no-sandbox,当然也不一定对(比如微信是CEF吧),有些客户端也发生了一些变化,不过可以通过历史记录看出来变化趋势,大家都在慢慢地开启沙箱,尝试逐渐收敛风险。
--no-sandbox
的风险是显而易见的,另一个问题是patch gap
,chromium那个更新频率没有几个客户端能跟上,甚至说基本跟不上,再加上功能优先,版本升级或者补丁合入并没有那么高的优先级(也有可能是风险没体现出来,不受重视),所以大部分基于chromium的客户端多多少少都滞后一些大版本,这就造成了大量潜在的Nday影响这些客户端,甚至从RCE到SBX一条龙。
openBrowser
的东西以上两种方式结合是最好的,能判断出来很精准的版本
"day"
Pj0/github/twitter/v8 commits
@BugsChromium
版本-代码commit之前互查询可以参考: https://omahaproxy.appspot.com/
xxminibrowser
(xx是啥我也不知道)这个洞应该在21年上半年就修复了,而且陆陆续续补掉了不少攻击的前置条件
最早可以随便打开,后面就变成了特殊的消息,再后来越来越窄吧
用上面的方法确定了具体的版本,还是很准确的
选个好day
当然了,在那个时候这个客户端他的好兄弟“小而美”也是差不多的情况,好好选day能都打了,21年hvv爆出的RCE就是这个情况(藏洞没有好下场 - -!)
当时用的是crbug659475,挺好用的,感谢keen lab的大哥 :D 为了提高成功率甚至还做了这样的事情:
1 | var worker; |
2022.8 update
“小而美”好像在hvv期间开了 --jit-less
后面又下掉了 ,现在的cmdline,与此同时也升级到了M81
S***e
这个点说来还有点故事,20年的时候发现了,直到21年吧有一个老外也发现了并且发在了推特上
说来也简单,就是个看起来是A打开确实B的问题,主要服务端也不做校验就转发是有点离谱的; 对于打开的URL也是有白名单检查的,所以特定域下的xss是攻击的桥梁 :(
M78 这个没什么好说的
m78可选的很多(比如CVE-2020-6418),注意目标的是x86,需要做一些改造,而且之前遇到过有些洞只在x86_64 work的情况
2022.8 update
参数 & 版本
Android webview
这类其实也算个重灾区,很多厂商会选择自己定制webview,且为了方便不开沙箱,线上丰富多彩的功能也提供了很多攻击的入口,发链接、扫码、卡片消息;但是也都会在打开URL前考虑加一层拦截,提示用户“xxx不是xxxxxx,确定要打开吗”。但是21年反垄断之后,这个限制就下掉了,随之而来的就是这样的安全风险。
对于甲方来说就是,我的定制webview依赖chromium,我又没办法及时更新,沙箱也一定能开,在 nday和 patch gap
的双重打击之下,你的SRC可能就变成“提款机”,每个月谷歌一发补丁,再加上是不是爆出个在野利用,你的SRC一定经常收到这样的报告: xxxxx RCE
。
我也有做安全运营的朋友饱受其害,我只能建议他内部专项治理,定期合补丁,能上沙箱就沙箱,这个真没啥好办法。
打补丁case by case,但是每个月都要来那么一次,还不能全自动化,有效但费人力。
开沙箱,毕竟是个浏览器,还是能打IPC穿沙箱穿出来,不过这就要看具体漏洞情况了。
升级到最新版,如果不稳定怎么办,这个也不是个好办法
补丁+升级+沙箱
三个维度一起来,毕竟短板效应,少了哪一块都不行,甲方的话也可以搞一些白盒工具来做补丁check,确认漏洞是否存在,这块就见仁见智了,我也写过一套,效果还行:D
chrome headless_shell 和 puppeteer
基本上还是 Exploit Headless Chrome这篇文章的内容,核心问题还是沙箱&版本过低的问题,这块比较严重的是网上很多人写教程、博客都喜欢--no-sandbox
,我也不知道他们知不知道这个参数的影响,不过一传十十传百,你会发现很多后端无头浏览器多多少少有这类问题。
扯远一些,有些扫描器用chrome,可以使用这手段做反制,你敢爬我轻则crash重则rce。
说到这个就想到了2020.11.13 那个下午弹出计算器的时候
主要是三块吧,我发现甲方里涮一圈之后思维确实不太一样了。
首先要讲明白风险,这里也包含证明风险,需要强有力的证明,比如exp打穿这样,研发可能不太理解为什么这样可以RCE,这就需要沟通好让大家有相同的sense
其次是修复方案,不同业务线、场景不一样,这个得和业务聊明白了才好给方案,不然就是“空中楼阁”,这块就算是治理存量问题了
最后可以开发一些工具做一些预警工作,相当于治理增量问题
存量怎么扫,补丁怎么提取,这个部分得好好设计构思
预警Bot,这个本质就是个爬虫+机器人,之前研究的时候自己搞过一个tg bot专门干这个,还能搞漏洞查询
我认为很有意思的是这个攻击面对于防守方来说简直是“折磨”,只要你的项目使用chromium,你就不得不面临各种补丁、升级,这实际上是很难做到及时补丁&升级的,所以理论上存在patch gap
,这就导致很多吸引眼球的 xxx RCE
传播的非常广泛。早在21年7月份,腾讯的蓝军在21年发布了攻防启示:Chromium组件风险剖析与收敛,也详细地剖析了该攻击面以及修复方案,对于我自己来说比较可惜的是在公司内部搞了这块攻击面的治理工作没有出去讲一讲or发个文章啥的,到后面这篇文章出来后已经没什么可讲的了 :(
主要想对自己的一些工作做个简单的总结,所以才有了本文,时至2022.8,这个攻击面应该已经变得众所周知,没有什么秘密可言了,想来这手法我在17年某项目上也见过,不过当时是webkit。
]]>Fishhook是Facebook提供的利用MachO文件惰性加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的一个轻量级的hook库。理解这个工具和熟悉流程也是可以帮助更好的理解MachO文件格式 :)
原理图如下:
核心其实就是rebind_symbols 这个接口,另一个 rebind_symbols_image
是指定macho中的symbol进行rebind,所以从 rebind_symbols
函数看起就行了。
1 | FISHHOOK_VISIBILITY |
简单看下关键的调用路径:
1 | rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); |
_rebindings_head
指向一个需要重绑定的符号的单项链表:
1 | struct rebinding { |
直接拿官方的demo编译出来调试分析流程:
1 | ~/study/ios_re_link/fishhook cat main.c |
1 | * thread #1, queue = 'com.apple.main-thread', stop reason = step over |
然后找到 LC_SEGMENT_64_DATA
处理 S_LAZY_SYMBOL_POINTERS
和 S_NON_LAZY_SYMBOL_POINTERS
1 | (lldb) n |
这里为了调试,重点关注 S_LAZY_SYMBOL_POINTERS
的处理
首先在rebind之前查看open符号
1 | (lldb) image lookup -va 0x0000000100a3fee0 |
首先备份了原函数地址,确保hook后可以通过 orign_open
调用到原本的函数。
1 | (lldb) p i |
之后找到函数指针,完成替换
1 | Process 2046 stopped |
如果调用 原本的函数会走什么流程?
1 | 0x100a3f448 <+176>: bl 0x100a3fd9c ; symbol stub for: close |
macos11.4 + iphone6 iOS 12.2
静态链接:输入多个目标文件,输出一个文件(一般是可执行文件)。这个过程中,把多个目标文件里相同性质的段合并到一起。
1 | ~/study/ios_re_link/static_link cat main.c |
1 | ~/study/ios_re_link/static_link xcrun -sdk iphoneos clang -c main.c foo.c -target arm64-apple-ios12.2 |
两个模块(main.o 和 foo.o) 通过静态链接组合成了一个可执行文件(main)
通过machoview可以看到重定位段有三条信息,意味着程序中有三处需要重定位处理:
这个图是hopper反汇编的main函数,可以看到对于引用到其他模块(foo.o)重的变量/函数的地方看起来“正常”,但是点击 bl _foo
就会发现跳转到了:
根据<macho/reloc.h>的定义,可以看到reloc段的结构:
1 | struct relocation_info { |
结合上面的图来看(以_foo符号为例):
对应到汇编里就是,main函数的0x28行引用了 _foo 符号,reloc段把这个信息告知linker,这样在链接的时候linker就会处理这条信息,把对应的符号做替换处理。
其实都是对 global_var
的引用
在foo.o模块中,是 0x20处的data,这个信息也要告诉linker,在link的阶段做替换。
最终的可执行文件main,可以看到没有重定位信息,而且mian和foo函数中改替换的符号都已经完成了替换,可以顺利的索引到想要使用的符号(foo和global_var)。
对比两者符号表:
以foo符号为例 :
Type 从 N_UNDF → NSECT
Value 从0 → 0x100007f90
符号表结构:
1 | struct nlist_64 { |
foo 符号的话
这里以demo中 global_var 使用的代码举例子。
源码中:
1 | int ret = foo(42 + global_var); |
如果对应到汇编里应该是:
1 |
|
可知 w0 是参数,w10是global_var的值,来自x9
w10 = [x9 + 0x68]
(未重定位修复)
最开始索引x9的时候可以发现是把0赋给了x9,因为这里还没有重定位,所以用0代替。
最终的产物中可以看到:
1 | 0000000100007f64 adrp x9, #0x100008000 ; 0x100008000@PAGE |
把0替换成了 0x100008000,这个地址恰好指向global_var。
可以看到经过linker的处理,可以正确找到global_var,符号foo同理
应该是签名有问题,最终解决方案:
1 | /usr/bin/security find-identity -v -p codesigning |
可以看到,第一次调用 printf
的时候,bl跳过去并不是 printf函数
1 | Target 0: (main) stopped. |
通过 dyld_stub_binder
找 printf
的地址,把找到的地址写回到 DATA,__la_symbol_ptr
第二次调用printf的时候就可以看到,这个地方printf函数地址已经被写过来了
1 | (lldb) x/10i $pc |
所以这里就可以直接获取到地址,然后直接跳转过去就行:
1 | (lldb) s |
dyld-852的代码:
因为我目标环境是iOS12.2,所以具体汇编代码有一些差别:
1 | Target 0: (main) stopped. |
但是本质上是差不多的,影响不大。
下面看看怎么一步一步调用进去,找到所需要的符号
1 | 0000000100007f98 ldr w16, =0x6967616d0000001a |
个人猜测:0x000000000000001a
应该是 类似 linux下elf lazy binding的时候那个index参数的东西,每个符号都不一样 。
初始化好需要的参数就调用进去dyld中去做符号绑定操作了
1 | (lldb) s |
保存栈帧,保存当前的寄存器信息(一大堆stp指令,后面符号绑定完成后,ldp会恢复,这些是成对的),然后设置好参数,就直接转到 dyld::fastBindLazySymbol
(函数前面的保存操作看起来和x86上函数开头的保存栈帧 抬高栈給临时变量预留空间的操作差不多)
1 | Process 1465 resuming |
调用的是 : fastBindLazySymbol(0x0000000100fb8028, 0x1a)
1 | // LINK_EDIT seg |
对应汇编中:
1 | (lldb) c |
这里用到了 我这个可执行文件的LINK_EDIT 段去做符号绑定工作:
1 | (lldb) image lookup -va $x1 |
根据不同的opcode,走不同分支:
1 | if ( lazyBindingInfoOffset > (lazyInfoEnd-lazyInfoStart) ) |
获取目标符号相关的信息 :
1 | &segIndex, &segOffset, &libraryOrdinal, &symbolName, &doneAfterBind |
然后根据这些信息,获取该符号的地址:
1 | uintptr_t address = segActualLoadAddress(segIndex) + segOffset; |
1 | // dyld版本不一致,实现的函数有些差别,但是本质是一样的 |
执行符号绑定:
1 | result = bindAt(context, this, address, BIND_TYPE_POINTER, symbolName, 0, 0, libraryOrdinal,NULL, "lazy ", patcher, NULL, true); |
1 | // 调试: |
执行之后:
1 | (lldb) n |
可以看到符号地址已经被写过去了(0x0000000100fb8020)
至此,符号绑定过程完成。
《程序员的自我修养-链接、装载和库》
https://juejin.cn/post/6844903912147795982
https://juejin.cn/post/6844903922654511112#heading-10
]]>msgsz
可控
msginfo.msgssz
是 8
如果控制 msgsz
不是 8的 整数倍,比如9,就会导致在第二次循环的时候 leak出来 7字节的内核数据。
1 | next = msghdr->msg_spot; |
1 | for (len = 0; len < msgsz; len += msginfo.msgssz) { |
补丁保证了,在非8 整数倍的时候,只拷贝剩余的长度的数据。
1 | import cpp |
有误报,但是够用了,替换成copyin,也可以看看其他的调用路径,不过笔者没发现什么有价值的东西 : (
CVE-2021-30660 - XNU Kernel Memory Disclosure
]]>之前在 iosre看到一张比较系统的iOS逆向学习路线图,因为接触过一段时间macOS上服务的漏洞挖掘,所以对*OS安全还是挺有兴趣的,也一直想系统性地学习下iOS逆向,之前的一直不成体系,也很零碎,正好对着这个图重构下知识体系。
类似Windows/Linux平台逆向学习,首先要学习正向开发的基础知识,以及涉及的文件格式(指可执行文件):
根据roadmap中的app分析流程,第一步就是“砸壳“,就是在根据文件格式做文章,因为macho文件是加密的,被加载到内存执行的时候才会解密,所以我们做静态分析,需要把内存中解密之后的可执行文件dump出来,并修复文件才可以拖入hopper/IDA正常分析。
我感觉这些可执行文件大同小异的味道,基本都是文件头+各种节区。 在macOS上你可以使用:
来查看一个macho文件的结构,推荐前者,后者不知道为什么总是卡卡的,而且很容易崩溃 :(
总体上来看,macho文件格式可以看做:
Header
Load Commands
Data
只关注几个基本字段
lipo
分离即告诉操作系统,该如何加载文件中的数据。
/usr/lib/dyld
各种节区,比如代码段,数据段,只读数据段等:
这里可以看到很多__DATA, __objc__?
节区,Symbol Table
String Table
也单独列了出来。
这些节区保存了OC中类名,函数名等信息,这就为从MachO中dump出来头文件打下了基础。
__DATA, __objc_protolist
节区:
存储的都是指针,指向一个又一个protocol的结构,可以参考objc的代码 :
1 | struct protocol_t : objc_object { |
所以我们可以按照结构体索引 __DATA, __objc_protolist
里指针指向的位置的数据,就可以解析出来protocol的类型,名字,方法等信息。
macos11.4 + xcode12
Q : openssl/aes.h
not found
A : add header file path
1 | export LDFLAGS="-L/usr/local/opt/openssl/lib" |
XCode中的配置是:
Q : Library not found for -lcrypto
A : add the missing dylib
核心逻辑就看
1 | - (void)processObjectiveCData; |
Load Commands 里找到 LC_SYMTAB,然后找到 __DATA(依赖属性 RW)。
然后利用 LC_SYMTAB 初始化了cursor开始遍历找符号。
strtab 从 string table 开始 : 一个 symbol起始位置,一个string起始位置。
然后根据 arm 还是 x64 走不同的逻辑(这里目标是ARM64的Binary) :
开始解析 symbol table,item by item
1 | string table index --> 在string table里找到对应的 string |
然后根据string table index里找到对应的string,放到symbols数组里,
根据 string 的 value 判断是不是 class,这里是根据字符串的开头是不是 @"*OBJC_CLASS*$_"
。
对于解析出来class name,添加到 class symbols dict里,这样处理之后,symbols, classSymbols都有了。
类似1
从 __DATA , __objc_protolist
读取 对应的value
比如得到地址0x1009ccc58
走到 - (CDOCProtocol *)protocolAtAddress:(uint64_t)address
初始化对应的CDOCProtocol
对象
依赖这个地址,从文件对应地址读取出来 这个 proto
的相关信息:
1 | struct cd_objc2_protocol objc2Protocol; |
name protocols这些字段是一个地址,指向对应的值(字符串/数组)
最后参照objc2Protocol的值,分别获取protocol 的 name, 各种methods,属性等,初始化了protocol对象
所以protocols就都处理出来了,最后得到了
_protocolsByAddress __NSDictionaryM * 6781 key/value pairs 0x0000000112f93820
依赖3中找到的 _protocolsByAddress
name -> protocol 对应关系的dict addr -> protocol 对应关系的dict
p1->protocols 里还有protocol,merge进来(adopted protocols)
p1 : _name __NSCFString * @”AWEFriendsActivityWidgetConfigurationIntentHandling” 0x0000000112fbc710
p2 : _name NSTaggedPointerString * @”NSObject” 0x07518ee6ed78d7f9
@interface AWEFriendsActivityWidgetConfigurationIntentHandling : NSObject { //blablabla… }
这种情况
解析section : __DATA __objc_classlist
和3类似的套路,先得到 一个 地址,然后根据地址,去文件中索引对应的结构:
CDOCClass *aClass = [self loadClassAtAddress:val]
只调试一次过程分析即可: val uint64_t 4335166480 In [2]: hex(4335166480) Out[2]: '0x102656410'
这个0x102656410,使用machoview也能看到,调试+machoview对比看,更容易理解。
loadClassAtAddress
方法分析:
1 | struct cd_objc2_class objc2Class; |
也是读取对应的class结构,这个过程其实很眼熟,如果读过iOS逆向的书,比如庆神的书,有一章介绍oc方法调用过程的,会把oc->cpp代码,那里面这个 oc object的结构分析的很清楚。
然后解析 class->data
字段
1 | struct cd_objc2_class_ro_t objc2ClassData; |
然后得到class 的 name,methods,protocol, property信息 然后返回这个class
展开说下 获取 methods && property的时候
(NSArray *)loadMethodsAtAddress:(uint64_t)address; { return [self loadMethodsAtAddress:address extendedMethodTypesCursor:nil]; }
loadMethodsAtAddress :
1 | objc2Method.name = [cursor readPtr]; |
一样的套路,都是解析出来对应的字段,然后按照这些字段读取信息(string) CDOCMethod *method = [[CDOCMethod alloc] initWithName:name typeString:types address:objc2Method.imp]; [methods addObject:method];
最后获得methods数组,给前面填充class的地方使用
loadIvarsAtAddress ,loadPropertiesAtAddress , loadMethodsOfMetaClassAtAddress
同理
至此,class解析完毕
关于Categories 可以看 https://zhuanlan.zhihu.com/p/24925196
处理 __DATA __objc_catlist section
:
一样的处理方法
1 | struct cd_objc2_category objc2Category; |
可以看到和对objc2Class的处理有点像,就是因为是category的原因,所以字段有不同, 简单的理解成 处理一种特殊的class,并且提取出相应的 methods 和 properties就行
至此整个 process函数的处理结束
这部分主要是处理输出了,如果没什么参数就直接stdout输出,如果有指定文件目录,就遍历之前process得到的信息,写文件(.h)到指定的目录。
https://zhuanlan.zhihu.com/p/24925196
https://en.wikipedia.org/wiki/Mach-O
https://evilpan.com/2020/09/06/macho-inside-out/
iOS应用逆向与安全 (刘培庆著)
]]>之前做过的一些使用CodeQL对JS/TS项目做扫描的笔记。
对于JS/TS的项目来说,CodeQL统一都是 --language=javascript
的参数处理的,而且它主要是扫描,解析,然后构建数据库,对于小项目直接默认参数应该是ok的:
1 | codeql database create --language=javascript <your_prj> |
但是对于比较大型的项目来说,因为CodeQL是Java写的,所以可能会存在内存不足导致构建数据库失败的情况:
1 | FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - |
默认给的内存是2400MB
,大项目必然不够啊,文件太多了。
找了一圈没有解决方案,索性直接掏出JD_GUI把它的jar包给反编译了,发现是通过环境变量控制的:
1 | export SEMMLE_TYPESCRIPT_RAM=8000 |
这个不是给JAVA的那个内存设置(-J-Xmx1234M
)
1 | interface FooInterface{ |
拿这个Demo为例,很多接口函数统一导出,需要借助InterfaceDeclaration
来找,不过我的方法有点“笨”。
1 | import javascript |
我这里实现很粗暴,就是限制函数名(字符串值)和Interface里成员名字(字符串值)一致,就认为这个函数是导出接口中的函数。
在我的需求中,我需要重点关注,参数中带有路径的函数,换言之就是需要识别出这么多接口函数中,参数带有path
的情况,那么很直接的思路就是利用正则,但是在实际的场景下,你会发现代码真的写出了“花”,不是常规的query能覆盖的。
1 | foo: (params: WTFParams) => Promise<....>; |
参数是一个interface,你需要对这个interface再限制,即这个interface的成员是不是path
参数直接就是 {arg : string} 这类情况
奇怪的函数写法,函数体在return里
1 | class PathParamInterfaceType extends InterfaceType{ |
最后一个问题比较简单了,就是有了source,然后再找合适的sink,看有没有路径就行了;但是其实还有一种办法会来得更直接,就是利用传递闭包,但是会带来比较多的误报,好处是实现起来简单,想要排除误报,只需要增加限制即可,看具体需求吧,哪个方法合适用哪个。
CodeQL的JS/TS部分实现不如cpp多,所以有些predicate需要自己手动实现,比如用cpp做query可以:
1 | FunctionCall getFunctionToACall(FunctionCall fc){ |
但是JS/TS部分没有getACallToThisFunction
,根据原理,手动实现一个即可:
1 | CallExpr getACallToThisFunction(Function f){ |
所以,如果想要查询foo函数的传递闭包,就可以:
1 | from CallExpr call |
https://ctftime.org/writeup/22177
https://kernelshaman.blogspot.com/2021/01/building-xnu-for-macos-big-sur-1101.html
https://github.com/D4rkD0g/boringforever/blob/main/xnu/boringanalysis/codeql_xnu.md
]]>