PWN学习总结之基础栈溢出
总结下内部CTF平台中的栈溢出PWN
0x0 前言
前置技能: 32/64位汇编
把这篇博文的题目都丢一个docker里了, dockerfile丢到github了: https://github.com/Hcamael/docker_lib/tree/master/pwn
进入到pwn目录中, build一个新镜像:
1 | $ cd pwn |
然后使用
1 | docker port stack1 |
查看每题开放的端口
0x1 PWN0
描述: 题目就一个pwn0的二进制文件, 这题是一个栈溢出的演示demo, 了解如何通过栈溢出控制EIP
我做二进制的步骤一般都是先用binwalk, 查看二进制文件
1 | $ binwalk pwn0 |
从binwalk中得知这是一个32位的二进制文件, 然后丢到32位的IDA中
简单来看可以直接用F5, 也可以直接看汇编, 锻炼一下自己.
很容易能找到一个getFlag
函数
只要能调用该函数, 那么就可以得到flag
还有两个函数foo
和main
代码很简单, 正常情况下是不可能调用到getFlag
函数的, 这时候就需要想其他方法.
看foo
函数中, 调用gets
函数之前的汇编, 我们能得到如下的栈结构:
gets
函数得到的输入存在eax
指向的地址, 因为gets
函数没有限制输入的长度, 如果获取输入的字符串大于0x1c byte, 则会覆盖到ebp
以下的栈数据, ret
表示函数执行结束后的返回地址, 如果foo
函数执行结束, eip
就会跳向这个地址, 所以我们可以通过把ret
的值改为getFlag
函数的地址, 调用getFlag
函数.
通过gdb进行调试, 可以很容易理解该原理.
当输入32 * 'a' + 'b' * 4
时:
payload0.py
1 | #!/usr/bin/env python |
不过这是下一题的解法, 在foo
函数中不是还有一个条件语句调用getFlag
函数么, 只要让该判断成立, 就好了, 上面的理解了, 现在说的这种方法就一目了然了, 用于判断的变量a1, 为函数实参, 在栈中位于ret之下, 所以只要输入(32 + 4 + 4) * 'a'
覆盖该参数, 则可使判断成立
0x2 PWN1
使用上面所说的方法, 控制eip跳转到getFlag
函数
0x3 PWN2
描述: 古老的栈溢出, 用shellcode就好了
同样是使用binwalk, 判断出是32位程序, 丢到ida中
本题主要是foo
函数:
1 | ssize_t foo() |
从汇编代码中我们能看出buf
的长度为0x1c
, 但是read函数却可以最大读取0x100
比特的字符串, 很明显会导致溢出漏洞
这里推荐一个神器: peda
peda为gdb插件
peda有一个checksec命令, 可以检测二进制的保护机制是否开启
1 | $ gdb pwn2 |
其中NX, 表示堆栈不可执行的保护, 状态为disabled, 表示堆栈不可执行关闭, 也就是说eip可以跳到堆栈地址
这时候我们可以使用shellcode, 啥为shellcode? 比如一段C代码system('/bin/sh');
, 把其转为汇编再转为二进制形式就是shellcode
我们可以把shellcode储存在buf
变量中, 然后通过溢出, 控制eip跳转到buf
的地址, 我们就可以执行shellcode了, 可以想象成是执行system('/bin/sh');
在foo
函数中一开始就输出buf
变量的地址了, 所以也挺简单的
1 | #!/usr/bin/env python |
0x4 PWN3
描述: 如果服务开了NX, 应该怎么拿shell呢?
该题给了一个二进制文件和该二进制文件依赖的libc库
使用binwalk, 可知是32位程序, 使用checksec, 可知开启了NX保护, 表示堆栈不可执行, 所以无法像上一题一样控制eip跳到栈地址了.
先丢ida
跟上一题差不多的代码, 同样buf大小为0x1c
所以通过溢出控制eip很简单, (只要前面的理解清楚了)现在的问题是, 控制了eip后可以干什么? 首先考虑, 做这题我们的目的是啥, 原题是flag位于/home/pwn/pwn3/flag
路径下, 要读取该文件就需要能执行shell命令, 我复现的环境里没放进flag, 所以最终目是能getshell就好了.
我们的目的是getshell, 那么只要能执行system('/bin/sh')
类似的命令就能达成我们的目的, 执行类似命令我们还缺少一个条件, system
函数的地址, 如果我们能获取到该地址, 那么很容易就能getshell了, 只要发送32 * 'a' + system_addr + ret_address + '/bin/sh'
首先是发送32 byte的padding把0x1c的buf和4 byte的ebp给填满, 然后是system_addr地址覆盖ret的地址, 控制eip跳转到system_addr地址, 然后就是system函数执行结束后的放回地址, 然后是system函数的参数/bin/sh
所以现在我们的问题就是, 如何获取system函数地址, 在pwn3的二进制文件中, 无法找到system函数
C语言写的程序, 在正常情况下, 程序都会加载一个叫libc的动态链接库, 在代码中你不需要#include
外部库就能调用的函数, 比如write
, read
, system
, 这些函数就来自这个libc库, 使用ldd
可以查看一个二进制文件的动态链接库的情况
1 | $ ldd pwn3 |
在运行pwn3的时候libc库也会被动态的加载到内存中去, libc中含有system函数, 所以内存中也会有system函数, 所以现在的问题是如何去寻找内存中system函数的地址
这时候涉及到另一个知识点, 在一个二进制文件中, 有一个plt表和一个got表的东西, 你的程序调用的函数除了自己写的函数外, 都会出现在这两个表中, 你可以想象成是外部调用函数表.
仔细看汇编代码你会发现, 外部函数的调用都是call该函数plt表的地址, 涉及到这样一种机制:
1 | 第一次call write -> write_plt -> 系统初始化去获取write在内存中的地址 -> 写到write_got -> write_plt变成jmp *write_got |
还要能理解一种关系:
1 | write_addr - system_addr == write_addr_libc - system_addr_libc |
也就是, system函数和其他函数地址的差值, 不管是加载到内存中还是在libc的二进制中, 都是相等的
根据上面这些姿势, 如果我们获取到了write_got
的值(write函数加载到内存中的地址), 因为我们有libc库, 所以可以很容易去计算system函数和write函数的差值, 用write_got
地址减去这个差值, 也就是system函数加载到内存中的地址了
payload:
1 | #!/usr/bin/env python |
0x5 PWN4
描述: 给了一个二进制文件和libc库, CTF中正常情况下低分的PWN题, 因为栈溢出的利用相对比较简单, 所以相关的题分数相对比较低, 正常情况下CTF的pwn题不会像前面那样那么容易就让你发现溢出点
pwn4首先看是32位程序, 然后丢到ida中去, 该程序有两个主要的函数sub_8048800
和sub_8048720
:
sub_8048720: 获取输入的函数, 根据
\x0a
来判断是否结尾, 会在输入的字符串结尾加上\x00
, 没有输入的长度限制, 输入存在堆中, 会根据输入的长度动态扩展堆, 没发现漏洞, 认为无法溢出
sub_8048800: 通过strlen判断输入的字符串长度, 必须大于7小于0x80, 开头7byte必须是
http://
,初始化了一个0x80大小的栈, 然后是根据是否有%符号, 如果不是%符号, 则把堆上的字符copy到栈上去, 如果遇到%, 则把之后两个byte当成十六进制, 然后转成字符串copy到栈上去, 遇到\x00则结束copy, 比如堆上的数据是http://%41
, 则copy到栈上之后的结果是http://A
, 其实就是一个urldecode的代码
粗看之下并没有发现有溢出点, 但是仔细分析下, 这两个函数结合起来有一个算是逻辑方面的漏洞吧.
sub_8048720
是根据\x0a
来判断结尾, 而strlen
函数是根据\x00
来判断字符串长度, 也就是说, 我输入http://\x00aaaaaaaaaaaaaa\x0a
, 使用strlen来判断长度, 其长度为7.
但是还有一个问题, sub_8048800
中也是根据\x00
来判断copy的结尾, 但是却存在一个逻辑漏洞, 如果当前byte是%, 则把后面两byte根据十六进制ascii转成字符, 然后指针向后移两位:
所以说, 如果我的\x00
藏在%号之后, 就不会遇到copy结束的判断, 从而导致栈溢出
栈溢出证明: 'http://\x00' + 'a'*0x100
然后是本题的payload:
1 | #! /usr/bin/env python |
PWN学习总结之基础栈溢出