大家好,今天和大家讨论的是 ASAR
完整性检查,ASAR
不是一种策略,而是一种文件格式
Electron 的 asar
(Archive)是一种将多个文件和目录打包成一个单一文件的归档格式,专为 Electron 应用设计。它类似于一个压缩包,但具有特殊的设计以便于 Electron 能够直接从这样的归档文件中加载资源,而无需先解压。使用 asar
的主要好处包括:
性能优化:通过减少文件系统的 I/O 操作,提高应用的加载速度。因为 Electron 可以直接从单个 asar
文件中读取资源,而不需要遍历多个小文件。
保护源代码:将应用的源代码和资源打包进一个不可直接浏览的归档文件中,增加了一层保护,使得最终用户更难以直接查看或修改应用内部的文件结构和源代码。
简化部署:一个 asar
包代替了原来的多个文件和目录,使得应用的分发和更新过程更加简便。
使用 MacOS 的用户可能非常好理解,MacOS 中应用程序的后缀为 .app
,可以双击执行,但也可以通过右键 -> 显式包内容进入到该路径中
也有点像 Linux 中的 tar 文件,就是把一堆文件捆在一起了
把大家捆在一起除了解决长路径问题,还有就是好做完整形检查,只需要对捆后对文件进行校验就可以了,这样可以确保程序能够及时发现源代码被篡改的情况
公众号开启了留言功能,欢迎大家留言讨论~
这篇文章也提供了 PDF
版本及 Github
,见文末
0x01 简介
0x02 ASAR 文件使用方法
1. Node API
2. Web API
3. 执行 ASAR 档案中的二进制文件
4. 向 ASAR 档案添加未打包的文件
0x03 ASAR 完整性
1. 工作原理
2. 在二进制程序中开启 ASAR 完整性
3. 采集ASAR头部的hash
4. 关于原理的一些疑问?
0x04 测试猜想
1. 创建应用程序
2. 安装 Forge
3. 设置开启代码完整性检查
4. 打包程序
5. 查看 ASAR 头部
6. 制作恶意 ASAR
7. 替换原本的 app.asar
0x05 onlyLoadAppFromAsar
0x06 总结
0x07 PDF 版 & Github
往期文章
官方将文件捆绑在一起,要么你就搞成一个类似 ELF
这种文件,加载资源的时候通过偏移去查找文件,要么就是搞一个文件系统,通过引用的方式能够一一映射,如果说到本质上,肯定还是一回事
在官方的视角里,它们是将捆绑后的 .asar
文件视为一个具有虚拟目录的文件系统,之后官方围绕这个特性,完善了 Node API
和 Web API
,使其支持这种格式的调用,但可想而知,不可能全部都修改为支持,所以官方列出了一些可以使用的方法
在 MacOS 上,asar
文件位于 /Applications/xxx.app/Contents/Resources/app.asar
由于 Electron 的特殊补丁程序, Node API 比如 fs.readFile
和 require
使用 ASAR 就像是使用虚拟目录一样, 里面的文件也像是在文件系统内一样
例如,假设我们在 /path/to
文件夹下有个 example.asar
包:
`asar list /path/to/example.asar `
`$ asar list /path/to/example.asar /app.js /file.txt /dir/module.js /static/index.html /static/main.css /static/jquery.min.js `
`const fs = require('node:fs') fs.readFileSync('/path/to/example.asar/file.txt') `
file.txt
里放入字符 flag
,通过 process.resourcesPath
可以返回资源路径,拼接后就是当前路径
打包后,执行,看看是否能够成功输出
`const fs = require('node:fs') fs.readdirSync('/path/to/example.asar') `
`require('./path/to/example.asar/dir/module.js') `
成功加载模块
`const { BrowserWindow } = require('electron') const win = new BrowserWindow() win.loadURL('file:///path/to/example.asar/static/index.html') `
成功加载
在网页中,可以使用 file:
协议请求归档中的文件。 就像是 Node API, ASAR 存档可以被作为目录处理
`<script> let $ = require('./jquery.min.js') $.get('file:///path/to/example.asar/file.txt', (data) => { console.log(data) }) </script> `
某些情况下比如对 ASAR 归档文件进行校验,我们需要像读取 “文件” 那样读取 ASAR 文件。 为此你可以使用内置的没有asar
功能的和原始fs
模块一模一样的original-fs
模块。
`const originalFs = require('original-fs') originalFs.readFileSync('/path/to/example.asar') `
您也可以将 process.noAsar
设置为 true
以禁用 fs
模块中对 asar
的支持:
`const fs = require('node:fs') process.noAsar = true fs.readFileSync('/path/to/example.asar') `
有一些Node API可以执行二进制文件,例如child_process.exec
、child_process.spawn
和child_process.execFile
,但只有execFile
支持在ASAR档案内执行二进制文件。
因为 exec
和 spawn
允许 command
替代 file
作为输入,而 command
是需要在 shell 下执行的
目前没有 可靠的方法来判断 command 中是否在操作一个 asar 包中的文件,而且即便可以判断,官方依旧无法保证可以在无任何副作用的情况下替换 command 中的文件路径。
某些 Node API 被调用时会解压文件到文件系统。 除了性能问题外,可能会触犯各种防病毒扫描程序。
你可以把使用--unpack
选项作为将各种文件保持为非压缩状态的一种解决方法。 在下面的示例中,原生Node.js模块的共享库将不会被打包:
`$ asar pack app app.asar --unpack *.node `
运行命令后,您将会看到 app.asar.unpacked
文件夹与 app.asar
文件一起被创建了。 没有被打包的文件和 app.asar
会一起存档发布
参考文章
https://www.electronjs.org/zh/docs/latest/tutorial/asar-archives
ASAR完整性是一项实验性功能,可在运行时验证应用的ASAR归档的内容
目前支持 ASAR 完整性的版本如下
MacOS 上 Electron >= 16.0.0
Windows 上 Electron >= 30.0.0
目前仅支持由 @electron/asar
生成的 ASAR 文件的完整性检查
在asar@3.1.0
中引入了支持。请注意,这个包已经迁移到 @electron/asar
所有版本的 @electron/asar
都支持ASAR完整性
每个 ASAR 文件都包含一个 JSON 字符串头部,头部的格式包含一个 integrity
对象,这个对象包含一个 16 进制编码的 hash 值,这个 hash 值是整个 ASAR 文件的 hash 值;还包含了一个数组,数组中存储的是每个块的 16 进制编码的 hash 值
`{ "algorithm": "SHA256", "hash": "...", "blockSize": 1024, "blocks": ["...", "..."] } `
另外,在打包Electron应用程序时,您需要定义整个ASAR头的十六进制编码哈希
启用ASAR完整性后,您的Electron应用程序将在运行时验证ASAR存档的头部哈希。如果没有哈希值,或者哈希值不匹配,应用程序将强制终止
ASAR完整性检查目前在Electron中默认禁用,可以在构建时通过切换 EnableEmbeddedAsarIntegrityValidation
这个 fuse 启用,通常还需要启用 onlyLoadAppFromAsar
,否则,可以通过Electron应用程序代码搜索路径绕过有效性检查。
`const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses') flipFuses( // E.g. /a/b/Foo.app pathToPackagedApp, { version: FuseVersion.V1, [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, [FuseV1Options.OnlyLoadAppFromAsar]: true } ) `
当然可以通过 Electron Forge
打包时进行配置
ASAR 完整性根据您在打包时提供的头部 hash 来验证 ASAR 存档的内容。提供此打包哈希的过程对于 macOS 和Windows 是不同的
Electron Forge和Electron Packager自动为您进行此设置,无需额外配置。ASAR完整性所需的最低版本为:
@electron/packager@18.3.1
@electron/forge@7.4.0
MacOS
为macOS打包时,您必须在打包的应用的 Info.plist
中填充有效的 ElectronAsarIntegrity
字典块。下面是一个例子
`<key>ElectronAsarIntegrity</key> <dict> <key>Resources/app.asar</key> <dict> <key>algorithm</key> <string>SHA256</string> <key>hash</key> <string>9d1f61ea03c4bb62b4416387a521101b81151da0cfbe18c9f8c8b818c5cebfac</string> </dict> </dict> `
有效的 algorithm
值当前为 SHA256
。 hash
是使用刚刚指定的 algorithm
计算 ASAR头部得到的哈希
@electron/asar
包公开了一个 getRawHeader
方法,然后可以对该方法的结果进行散列以生成此值(例如使用node:crypto
模块)。
Windows
打包Windows平台程序时,必须填充类型为 Integrity
、名称为 ElectronAsar
的有效资源条目。此资源的值应该是 JSON 编码的字典,格式如下:
`[ { "file": "resources\\app.asar", "alg": "sha256", "value": "9d1f61ea03c4bb62b4416387a521101b81151da0cfbe18c9f8c8b818c5cebfac" } ] `
有关实现示例,请参见
Electron Packager
代码中的src/resedit.ts
https://github.com/electron/packager/blob/main/src/resedit.ts
在这里算是把基本原理说清楚了,其实是 .asar
文件的整体和分块 hash 被存储在 .asar
的头部,当然计算整体 hash 时肯定没有包括头部,不然就出现了逻辑悖论了
之后通过对 .asar
的头部字符串进行计算hash 获取一个值,之后将这个值在打包过程中嵌入到二进制可执行文件中
这样如果开启代码完整性检查,则会在运行的时候把这个值拿出来,同 .asar
的头部的hash进行比对,如果通过则运行,不通过则退出
这里就有一些问题说得比较模糊了,程序运行的时候是只校验 .asar
文件的头部的 hash 吗? 还是头部的hash校验通过后,会进一步校验 .asar
整体和分块的 hash
与 .asar
头部的记录内容是否一致?
程序打包后可能会生成一个完整的 .exe
这类文件,也有一些 .exe
在安装后会释放一些文件
现在问题是,那么 ASAR 完整性校验代码是在最初的安装文件里才有,还是在安装文件里和释放后的启动文件(二进制可执行文件)里都有呢?
在查找资料的过程中,发现了开发者和用户曾经在 2019 年进行的一场讨论,就是说如果 asar
代码被修改了,添加了恶意代码,如何在 Electron
中发现,此时还没有代码完整性检查的 fuse 以及官方技术,官方人员的态度是: 如果攻击者已经可以修改 .asar
文件了,说明攻击者已经获取了系统权限,此时应该担心的不是 asar 有没有被修改,而是攻击者已经获取了系统权限
其中部分开发者及安全人员表达自己的观点,如果官方不提供代码完整性检查,那么攻击者就可以利用有签名的程序加载恶意代码,也就是白加黑,具体讨论内容参考下方链接
现在有了代码完整性检查,将 ASAR 头部计算得到的 hash 值写入了二进制文件,但是如果攻击者能够同时修改 .asar
文件和二进制文件,在 .asar
文件中添加恶意代码,生成新的hash,修改二进制文件中的hash为新 hash ,那么完整性检查还是会被绕过
但此时,二进制文件的签名就会失效,系统的完整性检查会辅助 asar 的完整性检查,所以程序签名几乎是最后一道防线
目前使用了 ASAR 完整性的程序非常少,毕竟刚出来,就连 Discord
、VSCode
这种更新比较快的应用都没有启用
所有我们需要自行开发并打包一款程序进行测试,平台就选择 Windows 11 好了
直接选用官方程序
`npm init electron-app@latest my-app `
Electron
版本为 30.0.3
,具备代码完整性检查的能力
可以看到,默认情况下会自动打开开发者工具,我们一会要实现的效果就是通过注入恶意代码,取消自动打开开发者工具,好像也没有那么恶意哈
进入到程序目录,即 my-app
,执行安装命令
`npm install --save-dev @electron-forge/cli `
只需要修改 my-app
目录下的 forge.config.js
文件即可
我们发现其实已经默认就设置为 true
了
`npm run make `
在 my-app
目录下新创建了一个 out
目录,官方提示我们 Artifacts
在 out
目录下的 make
目录中
在 out
文件夹下有两个文件夹,其中 make
文件夹内存放的是编译后的单文件; 另一个以程序名字-操作系统-架构命名的文件夹存放的是包含多文件的目录,其中就包括入口文件
经过测试,默认的单文件并不会涉及到安装过程,也不会解压释放文件目录,所以我们以多文件目录的程序为例
程序正常打开,会自动打开开发者工具
fuse 如下
在 asar
的 Github 项目中,有详细介绍 ASAR 头部格式
`| UInt32: header_size | String: header | Bytes: file1 | ... | Bytes: file42 | `
`{ "files": { "tmp": { "files": {} }, "usr" : { "files": { "bin": { "files": { "ls": { "offset": "0", "size": 100, "executable": true, "integrity": { "algorithm": "SHA256", "hash": "...", "blockSize": 1024, "blocks": ["...", "..."] } }, "cd": { "offset": "100", "size": 100, "executable": true, "integrity": { "algorithm": "SHA256", "hash": "...", "blockSize": 1024, "blocks": ["...", "..."] } } } } } }, "etc": { "files": { "hosts": { "offset": "200", "size": 32, "integrity": { "algorithm": "SHA256", "hash": "...", "blockSize": 1024, "blocks": ["...", "..."] } } } } } } `
从头部结构来看,也是通过偏移确定文件位置
我们可以直接将 resources
里面的 app.asar
拖进 16 进制查看器,我这里选择在线的
我们发现除了文件幻数以外,剩下都是明文的,那就好办了
参考文章
默认情况下,ASAR
内部是只读的,所以无法直接通过 Electron
的 API 去修改,
我们可以编译两份 APP,文件名称保持一致,其中一份内容里关闭掉开发者工具,也就是模拟恶意行为,之后同样使用 Electron Forge
打包,之后使用原本正常的头部替换掉不正常的头部,再将组合成的恶意asar文件替换到正常的文件,我们看一下,此时程序是否正常,是否能够发现篡改行为
我们先直接覆盖一下正常文件试试,看看能不能直接就成功,不成功显示什么? 当然要备份好原文件
看起来直接替换是不行的
正常 .asar
文件校验头位置为 0x00002937
,10 进制就是 10551
恶意 .asar
文件校验头位置为 0x00006FF3
,10 进制就是 28659
所以我们粗暴的替换的话,直接去掉恶意文件的头,之后将正常文件的头放到恶意文件上去
做这种事,还是得把战线拉到自己擅长的领域,命令行启动
去掉恶意 .asar
文件的头,保留剩下部分
`dd if=./app-evil.asar of=./app-evil-without-header.asar bs=1 skip=28659 `
获取正常 .asar
文件的头
`dd if=./app.asar of=./app-header.asar bs=1 count=10551 `
开始拼接两个文件
启用 my-app.exe
可以看到,完整性校验已经绕过去了,但是程序没有起来
这也不难理解,因为在我们替换的头里,有偏移相关的信息,接下来我们得开始 B 方案了,我直接修改正常的 app.asar
,将里面的空格改为注释,这样没有改变文件大小,也没有改变文件位置,如果还启动不起来,那就是 Electron
还会校验文件头里的内容
将这两个空格修改为 //
将修改后的 app.asar
覆盖原本的文件
再次启动 my-app.exe
可以看到,修改成功了,没有开发者工具弹出,这说明完整性检查并不可靠,攻击者完全可以通过修改 app.asar
进行代码注入
关于 onlyLoadAppFromAsar
这个说一下功能,根据官方描述,默认情况下,Electron
开发的程序检索 asar
文件的顺序是
app.asar
app
default_app.asar
当开启 onlyLoadAppFromAsar
时,就只使用 app.asar
上面提到的 app
应该是指目录,微软的 VSCode
就是使用的 app
目录
我们将my-app
程序的 app.asar
修改为 default_app.asar
此时我们尝试打开该 my-app
没有窗口产生
我们通过 @electron/fuses
对onlyLoadAppFromAsar
进行翻转
再次执行 my-app
这回完整性校验就失败了,此时我们已经将恶意的app.asar
换回来正常的了,还是完整性校验失败
应该是因为上面过,在 Windows 中做完整性校验是要给定 .asar
文件的地址的,当然这个 Forge
已经帮我们做了,但我们手动修改 app.asar -> default_app.asar
,二进制程序并不知道
此时我们再次通过 @electron/fuses
对 EnableEmbeddedAsarIntegrityValidation
进行翻转
再次执行 my-app
顺利加载了我们的 default_app.asar
此时我们就要思考了,其实如果我们想劫持微软的 VSCode
,我们只需要在它的 resources
目录下放置一个 app.asar
,可能就可以劫持 VSCode
了,我们试一下吧
我们修改 my-app-evil
项目,让其打开计算器
我们将 app.asar
复制到 VSCode
的 resources
目录
打开 VSCode
成功劫持 VSCode
Electron
官方推出了 ASAR 完整性检查,通过开启 EnableEmbeddedAsarIntegrityValidation
这个 fuse 的方式让程序在启动时检查 .asar
文件的完整性
工作原理就是在创建 .asar
文件时,计算整个文件及分块的 hash ,之后将其按照一定格式存储在 .asar
文件的头部,应用程序打包时,会计算该头部的 hash 值,之后固定打包进应用程序
程序执行时,会读取 .asar
文件的头部,计算 hash 后和程序内部的值进行对比,如果对比通过了就加载 .asar
文件进行执行
这里的问题在于,程序只会校验头部计算后的hash,但不会校验头部中的记录的hash是否有效,因此如果修改了文件内容,文件大小不变,偏移也就不会变(偏移在头部),就能够绕过验证
另一个有趣的 fuse 是 onlyLoadAppFromAsar
,这个 fuse
如果关闭,程序在加载 .asar
文件时会按照以下顺序搜索,加载第一个搜索到的文件
app.asar
app
default_app.asar
如果开启了该fuse,就只加载 app.asar
,所以如果关闭该 fuse
,就会导致执行流劫持
显然,最新版本的 VSCode
就可以实现劫持,因为 VSCode
使用的是 app
目录
我们将就完整性检查问题再次和 Electron
官方交流
PDF
版
Github
有态度,不苟同