2022 MiniL CTF 部分 WP
认真学了 pwn 后打的第一场比赛, 感觉良好. 之前听师傅们说 pwn 题出难了, 那赛前想着能出 1 题就是胜利, 结果出了 4 题, 超出预期一大截了. (后来发现人均 pwn 4 题 …)
缺人打到了校内第 6, 还不错.
认真学了 pwn 后打的第一场比赛, 感觉良好. 之前听师傅们说 pwn 题出难了, 那赛前想着能出 1 题就是胜利, 结果出了 4 题, 超出预期一大截了. (后来发现人均 pwn 4 题 …)
缺人打到了校内第 6, 还不错.
准备学堆了, 不同版本的 glibc 对于堆的管理稍有不同 (比如高版本可能增加了一些补丁, 使得某些漏洞无法利用). 由于需要本地调试, 所以得先学一下怎么更换 glibc 版本.
通过栈溢出向 bp 之上的地方构造 ROP 链, 可以达到控制程序流的效果. 但是如果需要的 ROP 链比较长, 而溢出的大小又不足以构造 ROP 链, 那么这时就必须想其他办法了.
根据 ROP 的方法, 我们可以通过覆盖 old bp 去控制 bp 指向的位置. 如果可以再控制 sp 的位置, 比如有 pop sp 这种 gadget, 那么实际上就可以伪造一个栈, 或者说, 栈迁移.
一般来说, 迁移的位置需要有可读可写权限. 比如 .bss 节就是一个很好的目标位置. 或者在覆盖 buf 的时候, 可能会 “浪费” 很多空间, 也可以尝试把栈迁移到 buf 数组所在的栈上.
可能程序没有 pop sp, 但是程序一定有 leave. leave 可以达到栈迁移的效果. leave 可以分成两步, 即 mov sp bp; pop bp. 由于可以通过覆盖控制 bp, 那么把返回地址覆盖为 mov sp bp (leave; ret) 所在地址, 这样 sp 就可以控制了. 由于 leave 又会 pop bp, 所以只需要在 迁移到的 sp 上 布置一个 fake bp, 这样 pop bp 的时候就可以控制 bp 了. 总的来说, 就是可以通过覆盖返回地址为 leave; ret 达到同时控制 bp, sp, ip 的目的. 而这仅需要 8 或 16 个字节的溢出即可.
Android 开发一直处于「面向谷歌编程, 下一次又不知道该怎么写」的状态 (指不会 startActivity). 前些天重写了一个东西, 又用到了一些奇奇怪怪的技巧. 遂记录, 供日后直接 copy.
shellcode 指达到某种目的的 (二进制) 代码片段, 在 pwn 中一般指获取 shell 的指令.
下面以调用函数 execve("/bin/sh",0,0)
的 shellcode 来举例说明.
shellcode 面向的对象是 CPU, 就是系统能够直接执行的指令. 所以一般而言, 是一串二进制代码. 这串代码其实就是机器指令. 写一个获得 shell 的汇编程序, 然后 hexdump 一下, 得到的二进制数据, 就是 shellcode 了.
不会. 这涉及到了汇编和系统调用, 没怎么深入学习过.
同样是获得 shell, 不同人可能有不同的写法, 就像其他任何一门语言一样. 这就导致了 shellcode 的长度并不是固定的. 有时候我们需要更短的 shellcode.
shell-storm.org 收录了许多 shellcode, 可以直接使用. 目前 x86 32 位最短的 execve("/bin/sh",0,0)
shellcode 是 21 (0x15) 个字节.
需要注意的一点是, shellcode 如果放在堆或者一些段上, 在读取的时候, 碰到 \0x00
就会停. 这称为 坏字符, 在编写 shellcode 的时候需要避免. 如果是栈上的 shellcode, 好像没有这个要求 (应该吧, 不是很懂), 可能可以编写更短的 shellcode.
共享目标文件是 PIC (地址无关代码) 的, 对其中的符号进行寻址是 相对寻址, 这也说明了整个 .so 装载后是一个不可分割的整体, 各个符号之间的相对距离 (或者各个指令的相对距离) 并不会变. 这样, 即使程序开启了 ASLR (地址随机化, 动态链接 .so 的时候会装载到随机的地址上), 我们只要知道程序用的是哪一个版本的 libc.so (进而知道各个符号之间的相对距离), 且知道某一个符号当前装载在内存中的地址, 那么理论上来说, 我们就可以计算出任意符号 (或者直接就是 .so 的某个地址)当前装载在内存中的地址. libc.so 中有 system 函数, “/bin/sh” 字符串等可供我们利用的符号或数据.
那么现在的问题在于:
先来看第一个问题.
虽然 .so 在装载后的地址是随机化的, 但是地址的低 12 位是固定的 (随机了, 但没完全随机). 因为程序装载的时候, 会涉及到虚拟内存向物理内存的映射, 而这种映射是以页为单位的, 页需要对齐, 而一个页的大小为 0x1000 (4096) 字节. 程序装载的时候, 每个节的对齐粒度都是 0x1000, 这也就说明了为什么 .so 中指令, 符号或者数据的低 12 位是固定的. 关于页映射, 目前没有搞得很明白, 详细请参考 «程序员的自我修养».
如果知道某一个符号的低 12 位是什么, 那么直接拿它和现有的所有版本的 libc.so 去对比, 就可以找到程序用的是什么版本的 libc.so 了. 对没错就是暴力枚举. 当然可能匹配到多个版本的 libc.so, 这时就得提供更多的信息了, 比如拿两个甚至多个符号的低 12 位去爆搜, 直到确定某一个版本的 libc.so. 或者暴力把所有符合条件的 libc.so 都跑一遍, 总有一个是对的.
数据是由一堆 01 串组成的. 要看这些 01 串在传输过程中有没有出现错误, 最简单的方法就是比较一下接收到的数据和发送的数据是否相同. 但是, 接收数据的人怎么知道发送的 “正确数据” 是多少呢? 所以这就很有意思了.