|
吾爱游客
发表于 2023-8-18 12:43
申请会员ID:12:00
邮箱:A52pojie@outlook.com
背景我最近正在开发一个基于go的网络应用程序,该应用程序使用一个名为嵌入的golang功能,
那么,go-embed到底是什么/它有什么作用?,想象一下你必须分发/共享一个应用程序
它由可执行文件和多个辅助文件(例如图像、语言支持文件等)组成,您将如何分发此类应用程序? Typical App Folder : app-executable --> Run by user image0.png | image1.png |-> Never Directly accessed by the user, but must be shipped audio.mp3 | with the app最明显的解决方案是压缩/焦可执行文件以及所有必要的文件
然后将其分发给用户,但提取文件将是用户端的额外努力(特别是当用户永远不会直接访问所有这些文件时)。Go的嵌入功能通过提供一个伪文件系统来拯救我们免受这种命运,该系统可以保存必要的文件,同时允许对它们进行编程访问,整个伪文件系统也驻留在二进制文件中。 App Folder while using Go Embed : app-executable --> One binary which has all necessary files embedded within it. ELF-structure of app-executable |===================================| |.data ..... | |===================================| |.rodata | | < Pesudo Filesystem > | |===================================| |.text ..... | |===================================|请注意,使用Go嵌入嵌入的文件是只读的。现在你必须想知道有什么大不了的,只需在二进制和视图内容上运行字符串,是的,如果你想花剩下的时间阅读被破坏的输出,你肯定可以做到这一点,但我们本文的目的是探索如何从任何任意的可执行文件中提取嵌入式文件,同时保留原始目录结构。但是为什么呢?
- 人们认为嵌入是“保护其应用程序文件”的一种方式-BooHoo!-必须证明它们是错误的。
- 任何有mal-dev经验的人都会立即看到使用嵌入的潜力
作为一个包装工,是的,它在野外被这样使用。 - Golang越来越受欢迎,所以我想这是社区努力更好地理解它的一部分。
了解嵌入的工作原理让我们从一个例子开始,我们的应用程序文件夹结构看起来像这样。AppFolder: misc: another_dir: - sample.txt - text1.txt - text2.txt - text3.txt main.go我们现在将编写一个简单的go程序,将所有文件嵌入到misc目录中让我们从...开始main.gopackage mainimport ( "embed" "fmt" "log")//go:embed miscvar embedFiles embed.FS // 1func main() { content, err := embedFiles.ReadFile("misc/text.txt") // 2 if err != nil { log.Fatal(err) } fmt.Println(string(content))}- 我们声明了一个类型为emebed.FS的变量,这本质上是我们的文件容器
embed.FS结构看起来像这样:type FS struct{ files *files[]}type file struct{ name string data string hash [16]byte}它包含一个文件结构数组,反过来又包含文件名称、其内容(数据)和截断为16字节的文件内容的SHA-256散列,数据和散列对于目录为零,名称保存嵌入式文件夹中文件的完整路径。
目录条目看起来像这样:file{ name = "another_dir/" // note the '/' suffix data = nil hash = nil}文件条目看起来像这样:file{ name = "another_dir/sample.txt" data = [10,10,10,10,10...] // files bytes hash = [1,2,3,4,5,6..] // file hash} - ReadFile,ReadDir是对嵌入变量进行操作的方法。
现在你一定注意到了这个奇特的东西go:embed well, 它是一个特殊的go指令,在编译期间(是的,去解析某些注释)要求go编译器 将misc文件夹中的所有文件绑定到emebedFilesvariable。Go编译器源在src/cmd/compile/internal/staticdata/embed.go - 上有一个名为WriteEmbed的函数 1表演这种魔力。
让我们通过运行go build main.go并运行它./main来快速编译我们上面编写的程序,这应该会打印themiscmisc/text.txt文件的内容。好了,现在我们已经成功地将文件嵌入到一个可执行文件中。编译的二进制文件中的嵌入结构现在我们已经编译了我们的应用程序,让我们把它与反编译器对立起来,看看编译后嵌入结构的外观。我们知道我们的主要函数引用了嵌入结构(见上文main.go),所以它应该给我们一个关于结构在二进制中的位置的线索:
我们注意到ReadFile调用和embedFiles作为参数传递给它,这就是我们的嵌入结构让我们看看它。
我们看到一大块字节,但并不清楚它是什么,但我们知道Go compilersWriteEmbed函数与此有关,让我们看看WriteEmbed,(不要惊慌,我会逐行解释)
- 第8行看起来像一个指针,注释表明它是指向[]文件的指针,这意味着它是指向嵌入结构中第一个文件的指针(指向数组的指针和指向数组的第一个元素的指针是一回事!)。
- 第9行和第10行表示文件编号
- 第19行迭代要嵌入的文件
- 第20行是文件名(文件的完整路径)的指针
- 第21行似乎代表了文件名的长度
- 第22行确定文件是否实际上是目录,如果是,文件内容指针和散列将为空,否则第27行中的else块会适当填充它们。
让我们再看看那块数据
它开始有意义了,- 红色块是第一个文件条目(即绿色块)的指针,因为我们处理的是小端架构,我们必须反转红色块的内容才能获得实际的指针,即0x004ca138。
- 两个蓝色块表示Go嵌入条目中的文件号
- 绿色块是一个文件条目
- 它的前8个字节是指向文件名的指针(蓝色下划线)
- 随后的8+8字节表示文件名的长度(蓝色下划线)
- 接下来的8个字节是指向文件内容的指针(以红色下划线)
- 随后的8个字节表示文件内容的长度(用红色下划线)
- 最后16个字节是文件内容的截断sha256散列(黄色下划线)
如果您密切关注,您会注意到,在上述情况下,文件内容指针和散列被清空,这是因为它是一个目录,常规文件的条目将具有适当的指针值,如以下示例所示:
注意:从上述信息中,我们可以推断出每个文件条目长54字节(对于64位二进制文件),对于32位二进制文件长32字节,此信息将有助于导航嵌入结构和构建自动提取解决方案。 提取文件名和内容让我们把到目前为止学到的知识放在一起,并尝试提取一个文件,(现在让我们手动操作来了解这个过程,本文末尾提供了基于python的解决方案)。我们知道嵌入结构的布局,我们有文件名和文件内容指针,这些指针只是文件内容所在的二进制文件中的偏移量,但我们不能按原样使用这些指针,我们必须从ELF基本地址中减去它,以获得文件名和文件内容所在的实际偏移量。
注意:ELF基本地址依赖于拱位,对于64位,其0x400000,对于32位,其0x08048000 因此,对于这个演示,我会选择嵌入结构中的第三个条目。
读取文件名:
前8个字节是文件名的指针(红色下划线),可以读作0x004a9f09(它的小端号记忆)
接下来的8个字节代表文件名的长度(蓝色下划线),可以读作0x0e,十进制为14。
提取名称:
为了计算文件中的实际偏移量,我们从ELF基地址中减去地址,一旦我们获得偏移量,我们使用dd从所述偏移量开始读取14字节(文件名的长度)。
让我们对文件内容做同样的事情:
文件内容的指针为0x004a89ef(以粉红色下划线)。
文件内容的长度为0x06(黄色下划线)。
提取内容:
现在我们也有文件内容了。
出于明显的原因,手动执行这是一个繁琐的过程,所以我写了一个名为Gembe的工具16这使得提取过程自动化,您只需提供嵌入结构地址。结束的想法我们讨论了go嵌入结构的结构,以及如何识别嵌入文件的偏移量并提取它们。还值得注意的是,嵌入结构是特定的,它不会在不同的平台上改变,因此我们构建的任何提取工具都可以很容易地被许多平台采用。我上面提到的提取实用程序(Gembe)需要一些人工工作(即用反编译器找到嵌入结构的地址),也可以使这个过程自动化,我最好的猜测是,类似于capstone 3可以用来实现这个。
|
|
发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。 |
|
|
|
|