ARTICLE DETAIL

资讯详情

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

从Web安全转战Pwn:大一新生栈溢出入门实战指南

从Web安全转战Pwn:大一新生栈溢出入门实战指南 1. 先想清楚web和pwn到底差在哪1.1 一个在找逻辑漏洞一个在跟内存搏斗大一有这种想法的人不少web玩了一阵子摸到了点门槛又看到pwn圈子里各种提权、shell、内核的高端操作觉得这才是真黑客转头就想跳过去。先说结论转是可以转的而且大一正是好时候但你必须先明白这俩方向本质上是在跟完全不同的东西较劲。web安全关注的是应用层。你做的绝大部分事情是找注入点、猜参数、翻接口、看逻辑漏洞。很多时候你在黑盒状态下靠经验就能判断一个输入点有没有问题比如一个查询接口你加上单引号报错了就知道大概率存在SQL注入。这套打法的核心是信息收集 逻辑推理你面对的是业务代码和服务器配置组成的复杂系统。pwn就不一样了。pwn面对的是一个已经被编译成二进制的程序你关心的是它在CPU上运行时内存里每一步到底发生了什么。一个gets函数为什么会让你拿到shell不是因为它长得危险而是因为它往栈上写数据的时候把返回地址盖掉了程序顺着你写的地址跳走了。打个比方web是你在楼外面找窗户看哪扇没关严pwn是你钻进楼里研究地基和承重墙研究哪个砖可以抽掉。两者需要的基础知识完全不同这就决定了你转方向的时候要重新打的地基是什么。1.2 你从web带过来的东西其实很有用很多从web转pwn的人第一反应是自己之前的积累全废了。实际上不是的web方向带给你的能力在pwn里照样吃香我列几个直接相关的你会写Python脚本这是pwn的看家本事。web里写脚本是为了发包、遍历、猜参数pwn里写脚本是为了构造payload、打远程、交互一个道理。你已经习惯了输入不可信这个思维。web里所有用户参数都不能信pwn里更是如此甚至程序栈上你输入的那一串A都是用户输入怎么利用它完全看你脑子。你有抓包和调试的习惯。web里用抓包改请求pwn里用gdb单步看寄存器本质上都是观察程序到底干了什么。这些软实力会帮你少走很多弯路。我见过不少纯新手直接学pwn结果写个脚本都费劲还要回头补Python。而你有web的底子这个坎不用再过了。1.3 先给心态打一针预防针pwn入门有两个劝退大户一个是环境配置一个是什么都看不懂的挫败感。环境这块你可能会在装gdb插件、调pwntools版本、Linux依赖缺失之间折腾好几个晚上。这些东西本身不难但特别消磨耐心。我的建议是第一次配置就老老实实按教程一步步来别自作主张装完也别急着学新东西先跑通一个hello world级别的exp确认环境没问题了再往下走。挫败感更是家常便饭。你反汇编一个程序满屏幕都是mov、push、call完全不知道在干嘛你写了一个exp一运行直接Segmentation Fault也不知道死在哪个指令。这些我都经历过而且不止一次。你要做的不是郁闷而是把每道题的为什么搞清楚为什么这个地址要这样填为什么这个字节数正好盖过返回地址搞清楚一个点就往前挪一点。三周之后你回头看第一个晚上对着汇编发呆的自己会觉得进步非常明显。2. pwn入门的三大基础Linux、C语言、汇编2.1 环境搭建别在Kali上死磕用Ubuntu更省心很多人一听说pwn第一反应就是装Kali觉得里面安全工具全。这个想法不能说错但作为学习环境Kali其实不如Ubuntu干净。我自己就是踩过这个坑的当年用Kali系统里预装了一堆用不上的工具启动慢而且有些包的版本和教程里的对不上出问题都不知道去哪查。我的建议是学习阶段用虚拟机装一个Ubuntu 22.04 LTS内核稳定包管理成熟绝大多数网上教程都是基于Ubuntu写的照做就行。Kali留到后面打比赛、需要快速用工具的时候再说。装完系统之后这几样东西是你pwn之路上的标配sudo apt update sudo apt install -y gcc gdb python3 python3-pip pip3 install pwntoolsgdb插件方面我推荐pwndbg它对新手特别友好直接高亮显示寄存器、栈和反汇编git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh如果你某个插件装不上千万别死磕先用原版gdb配layout asm也能看反汇编。环境这东西够用就行别追求一步到位。还有一点Linux命令不用专门去啃书你会这几个就够了cd、ls、cat、chmod、sudo、python3、管道符和重定向。vim如果实在用不惯就用nano别把时间浪费在编辑器上。我见过不少新手第一个晚上就耗在vim退出上太不值得了。2.2 C语言要学到什么程度才算够了pwn题目绝大多数是用C写的你要能看懂里面那些经典的危险函数。我帮你划一个范围学到这份上就够开始打题了搞清楚数组和指针的区别知道char buf[16]在内存里占多少字节栈上大概是什么排布。明白strlen和sizeof的区别。后者在编译期就算完了前者是运行到\0结束这两个搞混你后面算偏移必出问题。会看函数调用和参数传递知道局部变量存在哪。能自己写一个带gets或strcpy的小程序然后gdb看它的内存布局。有些人非要把C语言学透了才敢往下走我的经验是没必要。比如结构体嵌套、复杂指针这些等你以后打堆利用的时候再补都来得及。pwn入门阶段最关键的C知识点就是内存和指针其他都可以边打题边带出来。2.3 汇编和内存布局是pwn思考问题的坐标系如果说C语言是让你理解程序逻辑的那汇编就是让你看到内存真相的。你不需要会写汇编但你必须会读。重点先啃这几块寄存器rax、rbx、rcx、rdx、rsi、rdi、rsp、rbp、rip。记不住全部没关系前几个和函数参数、返回值相关rsp和rbp管栈rip是下一条指令地址这几个必须刻在脑子里。栈的生长方向从高地址向低地址长。栈帧的概念每调用一次函数就会在栈上压入一块区域用来存局部变量、保存调用者的rbp和返回地址。常用指令mov、push、pop、call、ret、leave、add、sub。学习材料不难找B站上搜x86汇编入门或者栈帧就有大量免费视频。我当年是看着图自己在稿纸上画了一遍call指令的完整过程画完就通了一个函数调用发生的时候call指令会把当前的返回地址压栈然后跳到函数开头函数开头通常先push rbp再mov rbp, rsp给新栈帧划定边界然后sub rsp, 0x10给局部变量腾空间。函数要返回时leave把栈恢复ret从栈上弹出地址跳回去。这个过程你能不看资料画出来栈溢出对你来说就已经成功一大半了。要是画不出来就再看一遍直到能画出来为止这一关值得花时间。3. 第一道pwn题从栈溢出开始3.1 栈溢出的本质把返回地址改掉前面讲了栈帧现在串起来看。假设程序里有一个函数void vuln() { char buf[16]; gets(buf); }gets函数会一直读取输入直到遇到换行完全不管buf到底够不够大。当vuln被调用时栈上是这样的高地址 ------------------ | 调用者的其它数据 | ------------------ | 返回地址 | - 正常情况下函数执行完跳回这里 ------------------ | 保存的 rbp | ------------------ | 局部变量 buf | - 16字节从低地址开始 ------------------ 低地址你用gets往里写写了16个字节刚好填满buf再写8个字节就把保存的rbp覆盖掉继续写返回地址就被你控制了。程序执行到ret的时候会从栈上弹出你覆盖过的返回地址跳到你指定的位置。这就像你给一个自动售货机投币本该落进钱箱的硬币被你塞了张纸条纸条上写着吐一罐可乐。售货机不检查内容它就照办了。栈溢出的整个思路就这么简单。第一道题我强烈建议你找一个保护机制全关Canary off、NX off、PIE off的简单题目来练方便你看到最纯粹的漏洞利用过程就像学开车先上驾校的场地一样。3.2 拿到题先看保护checksec是你的靶场规则说明pwn里有一句话叫拿到题先checksec。这句话不是装样子因为保护机制直接决定了你的利用路径。你可以把保护理解成靶场的规则保护机制作用新手影响Canary栈里埋随机值被覆盖就检测报错有它就不能无脑溢出覆盖rbp和返回地址NX栈不可执行有它就不能在栈上执行shellcodePIE程序地址随机化有它就不能直接硬编码函数地址RELRO部分或全部只读GOT表影响能不能改GOT劫持流程入门阶段几乎所有教程都会让你先做Canary off、NX off、PIE off的ret2text题用最简单的溢出拿到控制流。3.3 手把手拆解一道ret2text题假设题目编译出了pwn1你用objdump或者IDA看到内部有vuln还有一个后门函数backdoor里面直接调用了system(/bin/sh)。注意我说的是看到ret2text意思是这个后门函数是程序里本来就存在的你要做的就是让程序跳到它。这一步在一个真正的CTF题里怎么发现很多时候用strings pwn1就能看到/bin/sh字样再用objdump -d pwn1 | grep backdoor就能找到函数地址。exp长这样from pwn import * context.binary ./pwn1 context.log_level debug p process(./pwn1) payload bA * 16 bB * 8 p64(0x4011c6) # 后门函数地址 p.sendline(payload) p.interactive()这里有两个关键点你得完全理解不能含糊第一为什么是168因为buf只分配了16字节rbp占8字节之后才是返回地址。所以16字节填满buf8字节把rbp覆盖成垃圾最后8字节才是你真正要填的返回地址。第二为什么要用p64()x86-64是小端存储一个64位地址在内存里的字节序是反过来的。比如地址0x4011c6存进内存是\xc6\x11\x40\x00\x00\x00\x00\x00。你如果直接往payload里填地址的字符串形式程序会把对应的ASCII字符当地址去解根本不可能跳对。第一次打通这个程序的时候我盯着终端里弹出的$符号看了好久因为我知道自己指使了一个程序去执行原本不打算执行的东西。你不用急着懂更多先把这道简单的题彻底吃透然后带着gdb走一遍看payload是怎么一层层填进去的。3.4 用gdb亲眼看一眼栈的变化打通之后我强烈建议你用gdb复现一次把整个过程在眼里过一遍。命令大概是这样gdb ./pwn1 b *0x4011c6 # 在后门函数下断点 r # 运行运行时会卡住等待输入这时候你先输入AAAAAAAAAAAAAAAAAAAA...随便一串回车后会断在后门函数入口。然后执行x/20gx $rsp # 查看栈顶开始的20个8字节数据 info registers # 看所有寄存器的值你会看到rsp指向的栈区域里有一串0x4141414141414141那就是你输入的内容。往上找你能看到一个0x4011c6正好躺在返回地址的位置。这个过程看一次比你读十篇博客理解得都要深。4. 必学的经典漏洞类型不是只有栈溢出4.1 整型溢出长度检查是怎么被绕过的栈溢出你学会后接着会让你碰到的就是整型溢出。它思路跟栈溢出不太一样但作为大一零基础它的门槛其实不高只要你有边界条件的敏感度。C语言里的整数是有范围的。比如unsigned int最大是4294967295你要是给这个变量加上1它会回绕成0。这在web里可能就是价格变成负数在pwn里更危险。pwn里常见的利用场景是长度检查。比如程序这样写int len; read(0, len, 4); // 读取一个长度 char *buf malloc(len); read(0, buf, len); read(0, buf, 0x100); // 然后固定读入0x100字节攻击者把len传成-1malloc(-1)会变成一个很大的数分配出很大的空间绕过了一系列限制后面的读取就随便写了。还有一种情况是len传成一个小负数转换时被当作无符号数变大到超过预期就能造成溢出。练这道题的时候关键是别把数学上合法的数和安全上合法的数混为一谈。程序不会因为整数是负数就罢工它只会按内存里的二进制去计算你要学的就是跟它用同一种语言思考。4.2 格式化字符串一个printf就够喝一壶你要是web出身肯定对拼接SQL这种事很熟。格式化字符串漏洞说白了就是把用户输入直接当模板去格式化类比过来就是把用户输入直接拼进SQL只是危害形态完全不同。漏洞写法长这样printf(buffer); // 应该写成 printf(%s, buffer);你输入%p.%p.%pprintf就会从栈上依次取参数打印把栈上的地址泄露出来。再进一步你输入%n它能往某个内存地址写入一个值这个值等于你这条字符串已经打印了多少个字符。把%n配合%hhn写一个字节用就能实现任意地址写。对入门来说你不需要一上来就玩出花先做两步就够了第一步用%p一个接一个打印栈上的数据找到你输入内容出现的位置这个位置就是你的偏移。比如输入AAAA%p.%p...你会在第几个%p的位置看到0x41414141。第二步尝试%n修改一个你能控制的变量值体会一下写内存到底是怎么回事。这道题对新手来说有个好处它不像栈溢出那么依赖精确的字节位移更像是在玩一个格式串拼图成就感来得很快。4.3 ROP入门NX开启后的借力打力当你把NX打开以后栈上不能执行shellcode了你就得换思路。程序本身的代码段是可执行、可读的那你就可以用程序里已有的指令片段把它拼接出一条新的执行链。这些以ret结尾的片段就是gadget。ROP的思路就是你不再往栈上放shellcode而是放一堆已经存在的gadget的地址。每个gadget执行一小段操作比如pop rdi; ret能把栈上的一个数弹到rdi寄存器里然后ret。通过串联一串gadget你可以设置寄存器的值然后跳到system函数实现调用。这个阶段工具的作用开始变得重要你不再手撸每个地址而是靠ROPgadget之类去搜ROPgadget --binary pwn1 --only pop|ret你会发现这条链子串起来的过程特别像在用乐高搭积木手里有的积木块是固定的你的任务是找到刚好能搭出目标形状的那几块。实话实说ROP一开始有点绕但等你亲手把一个ret2libc的exp打出来那种感觉比栈溢出拿shell还爽。5. 工具链pwntools、gdb和几个救命小工具5.1 pwntools的常用API新手够用这些pwntools是你在pwn里的左膀右臂它把所有交互、打包、拆包这些脏活累活都封装好了。你不需要背全文档先把这些用熟from pwn import * # 启动本地程序 p process(./pwn1) # 连接远程题目 # p remote(123.45.67.89, 10001) # 接收数据等待某个字符串出现 p.recvuntil(bInput:) # 发送一行数据 p.sendline(bAAAA) # 64位地址打包/拆包 payload bA * 16 p64(0x4011c6) addr u64(p.recv(8)) # 符号地址 e ELF(./pwn1) backdoor e.symbols[backdoor] # 交互式shell p.interactive()我自己的习惯是exp先在本地跑通再把process注释掉换成remote然后检查context.log_level是不是设成了debug这样收发的字节都能打印出来远程出问题也容易排查。5.2 gdb配合pwndbg调试效率直接翻倍没有调试器pwn寸步难行。装上pwndbg之后界面会自动高亮栈、寄存器、反汇编断下来一眼就能看到当前状态。常用命令gdb ./pwn1 b *0x4011c6 # 在指定地址下断点 b vuln # 在函数名处下断点 r # 运行到断点 ni # 单步执行一条指令不进入函数 si # 单步执行进入函数 x/20gx $rsp # 查看栈上数据 x/10i $rip # 查看当前指令附近的反汇编 info registers # 查看所有寄存器新手最容易犯的毛病是断点乱下什么都抓不到。我的建议是先r跑起来用info registers看rsp/rip的位置再顺着栈往下查。你要关注的核心问题是我的输入数据现在在哪个地址它有没有影响到返回地址带着这个问题去调试效率完全不一样。5.3 辅助工具的定位别啥都靠工具除了pwntools和gdb还有几个小工具你早晚会用到checksec检查保护机制pwntools自带也可以单独安装。ROPgadget搜ROP片段。one_gadget在libc里找一打的execve(/bin/sh)地址省去凑参数的过程。使用原则是工具是加速器不是替代品。one_gadget给的地址你要是问它为什么能用你得自己去看上下文ROPgadget给你搜出来的gadget你得明白为什么这个gadget能解决当前问题。依赖工具越多越要记得补原理。6. 把学习路径排成时间表大一也能稳稳推进pwn的知识点很多如果东一榔头西一棒子很容易学了后面忘前面。我给一个参考节奏按周为单位推进前提是每天能有1到2小时周末能有大块时间。周数主题预期产出第1周Linux基础、C指针复习能独立编译运行C程序理解指针与数组的区别第2周x86汇编、栈帧与函数调用能在纸上画出一次函数调用的完整栈变化第3周gdb调试、checksec能下断点、查内存、看寄存器第4周栈溢出基础ret2text自己打通第一道题理解返回地址覆盖第5周pwntools脚本化不用手动交互用exp自动打通第4周的题第6周ret2shellcode、NX理解能在无NX保护的题上执行自己的代码第7周格式化字符串能用%p泄露内存用%n写一个值第8周ROP入门、ret2libc理解gadget能调用system(/bin/sh)第九周以后再考虑堆利用而且我建议不是特别热爱、不准备冲顶级强队的堆完全可以放到大二。堆的知识量大、分支多、题型杂大一先把栈和格式化字符串的底子练扎实性价比高得多。关于练习平台国内有不少适合新手的在线靶场像CTFHub、BUUCTF、ctf.show这些上面都有专门的pwn入门题集按难度排序方便你刷。一个原则是刷题不要贪多一道ret2text你换平台刷十遍也不如把同一道题改成不同保护机制再做一遍收获大。另外提一句看题解的问题。实在卡住了看题解不丢人但看完题解必须自己从头到尾复现一遍然后隔三天不看题解再做一遍。能做到这个才算真正吸收了。光看视频、看文字在pwn这边是永远练不出手的因为你对栈上的偏移、对地址的变化没有肌肉记忆。7. 新手的常见坑我一个个给你列出来7.1 本地打不了远程也打不了先查ASLR和libc很多人在本地打ret2libc之类需要固定地址的题时会遇到一种情况跑一次崩一次地址每次都不一样。原因大概率是系统的地址空间随机化ASLR在捣鬼。x86-64的Linux默认开启ASLR程序每次运行的加载基址都不固定。调试阶段你可以先关掉echo 0 | sudo tee /proc/sys/kernel/randomize_va_space但是注意进了比赛环境远程的ASLR是开着的而且远程用的libc版本可能和你本地不一样。这就是老生常谈的libc不一致问题。远程题一般会给libc文件你需要先确定远程libc的版本再用ldd或专门工具算出system、/bin/sh这些在远程环境里的真实地址。我当年在这个坑上卡了整整一天exp本地随便通一到远程就崩还以为是自己的payload写错了。现在跟你说清楚看到远程连接之后崩溃第一反应不是怀疑payload逻辑而是先检查libc和ASLR能省下一大把自我怀疑的时间。7.2 SIGSEGV但逻辑没错多半是栈对齐问题另一个折磨人的坑是x86-64的栈对齐要求。System V调用约定里规定在执行某些指令比如调用带参数的函数前rsp必须16字节对齐。你要是算错了payload长度在调用system前rsp没对齐程序直接段错误。解决办法就是在payload里加一个多余的retgadget垫一下。这个ret; ret之类的技巧新手往往看不懂为什么要加其实就是给栈补上8字节让rsp重新对齐。我可以很负责任地说这是pwn圈新手几乎必踩的坑。以后你写ROP碰到莫名其妙的崩溃第一件事就是在payload前面加一个ret试试十有八九能解决。7.3 拿到题目先别急着反汇编先做粗活很多入门题其实不需要高深技巧程序本身就把答案暴露了。我建议养成习惯拿到题先跑这几条命令file pwn1 strings pwn1 | grep -E flag|sh|system objdump -d pwn1 | grep -E backdoor|win|flagfile能告诉你这是32位还是64位格式是什么strings能快速扫出程序里明文字符串objdump能定位到关键函数。很多新手题里程序甚至直接把flag明文存在某个变量里或者后门函数已经写好了你只需要找到它跳过去。你要是一上来就扎进IDA里逆半天反而是绕远路了。7.4 别急着追新题型先守住基本功网络安全圈每年都会冒出新题型、新打法内核pwn、浏览器pwn、VM逃逸这些听起来非常酷也确实是pwn天花板。但对大一的你来说这些不是现在该碰的东西。我见过太多人连栈溢出偏移都算不利索就喊着要学堆利用结果两边都没学扎实。pwn的基本功说到底就是栈、堆、格式化字符串、整型溢出这么几大件。把这几样东西的常见考法吃透了再去碰复杂的题型你会发现很多高级技巧都是这几样的排列组合。反过来你一开始就去碰最难的除了收获挫败感什么都得不到。我个人的体会是从web转pwn最爽的一个时刻不是拿shell的那一刻而是有一天你突然发现之前的那些天书——寄存器、栈帧、gadget——已经变成了你思考问题的默认语言。你在web里习惯了跟业务逻辑斗智斗勇在pwn里则是跟一台冷冰冰的机器讲道理它不会说谎每一个字节都有它的位置你只要足够细致就能找到那条通往后门的路。大一的优势是时间充裕每天花一两个小时三个月后回头看你会感谢自己当时没有犹豫。
返回列表