在第一部分中,我们安装了一些工具,这些工具对我们学习本课程很有帮助。他们的特点是他们都是免费的。我们将不使用任何付费工具,并且在具有付费版本的工具(例如IDA或PYCHARM)中,我们将使用免费或社区版本。在开始练习之前,让我们先看一些概念。什么是购物袋?BAG是创建计算机程序(软件)或计算机的过程失败或缺乏的结果。指定的故障可能发生在软件生命周期的任何阶段,尽管最明显的故障发生在开发和编程阶段。就像我经常说的那样,程序员会犯错误,而这些错误会导致程序崩溃或错误。到目前为止,我还没有说什么新的话。问题是要知道BAG和VULNERABILITY之间的区别,所以让我们看看VULNERABILITY是什么。什么是脆弱性?漏洞是程序中的一种特定类型的错误,该漏洞允许使用它来破坏计算机系统的安全性。因此,漏洞使您可以执行程序不想要的操作,并滥用它们。换句话说,漏洞是某种类型的错误,是它们之间的子集。
当然,漏洞类型很多。我们将专注于WINDOWS中漏洞的研究和开发。什么是EXPLOIT?
EXPLOIT是一种计算机程序,试图利用另一个程序的某些漏洞。漏洞利用的最终目的可能是恶意的,例如破坏或关闭受攻击的系统,尽管通常这是违反安全措施的行为,以便以未经授权的方式访问信息并出于您自己的利益使用信息或作为对第三方的其他攻击的来源。滥用此漏洞可能导致应用程序或系统本身故障,在本地或远程计算机上执行本机代码。其操作和复杂性取决于漏洞本身,环境以及操作过程中目标所采取的措施。我们将检查的第一类漏洞是缓冲区溢出。我们将从最简单的示例开始,然后逐步增加复杂性。最初,系统的安全功能不会被激活,但是逐步地,我们将激活它们以了解我们如何处理它们以及在什么情况下。什么是缓冲区?
BUFFER是为数据存储和管理保留的一定大小的存储空间。一个基本的例子是一个20升的广口瓶,我可以用来存放里面的东西。它可以小于或等于20升(最大尺寸)。如果您想在一个罐中存储更多,必须找到一种增加缓冲区大小的方法,否则当您尝试在20升罐中保存40升时,它会溢出。什么是缓冲区溢出?
当计算机程序通过将数据写入连续的内存块而超过为其保留的内存量时,就会发生缓冲区溢出。https://www.welivesecurity.com/la-es/tag/buffer-overflow-la-es实际上,当应用程序的程序代码中没有进行必要的安全检查(例如测量数据量)时,就会在应用程序中发生缓冲区溢出它将被复制到缓冲区,并且不会超过缓冲区的大小。缓冲区溢出的最常见类型是堆栈缓冲区溢出和堆缓冲区溢出。在这里,我们看到了缓冲区溢出的定义,在我们之前的示例中,如果我尝试将40升倒入20升的水箱中,那么据我们所知它将溢出。这是导致缓冲区溢出的溢出,即 超过其最大容量时,我的水箱溢出。现在解释一下堆栈和堆之间的区别。什么是堆栈?
STACK用于存储仅在执行函数时才需要的局部函数变量。在大多数编程语言中,重要的是我们要在编译时知道要将变量保留在堆栈中的大小。什么是很多?
堆用于保留动态内存,动态内存的使用寿命尚不清楚,但可以预期会持续一段时间。如果我们不知道它的大小或在运行时确定它的大小,则必须计算该大小并将其保留在堆上。堆还用于大小不同的对象,因为我们不知道在编译时将使用它们多长时间。我一直是漏洞利用程序的作者,在我们公司工作了13年以上。当我加入公司时,我们对所有受雇人员所做的第一件事就是试图弄清著名的GERARDO RICHART的堆积物。他是CORE SECURITY的创始人之一,也是漏洞利用分析专家。我们将从最简单的堆栈开始。当然,正如我所说,它们是在目前受最小保护的情况下编译的,它们是32位的,以便于操作。让我们看一下STACK1任务的源代码。https://drive.google.com/open?id=16btJAetpa1V5yHDZE2bnnFWTQNpsUR4H我们看到一个带有练习的文件夹,里面是名为STACK1_VS_2017.CPP的源代码STACK1。
int main(int argc, char **argv)
{
MessageBoxA((HWND)-0, (LPCSTR) "Imprimir You win..\n", (LPCSTR)"Vamosss", (UINT)0);
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344) printf("you win!\n");
}
我们将尝试理解此代码,并查看缓冲区溢出可能发生的位置,以及这是堆栈还是堆上的缓冲区溢出。MessageBoxA函数调用已添加到STACK1源代码中,以向我们显示一条小消息,提示我们对其进行解决。这只是一个添加,不会影响任何内容。这是对指定的WINDOWS函数的标准调用,在此不做分析。谁需要有关此函数的信息,可以在这里获取,我们知道在函数内部,如果有局部变量,则必须为它们保留一个位置。因此,剩下的是由GERARDO RICHART创建的源代码。int main(int argc, char **argv)
{
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344) printf("you win!\n");
}
我们在程序的第一部分以红色显示,其中保留了局部变量的空间。在这种情况下,有两个局部变量COOKIE和BUF。您可以在表中看到数据类型。其他类型的变量也位于此处。该代码将被编译为32位。我们看到,COOKIE变量将为INT类型,因此将为该变量保留4个字节的内存。
对于BUF变量,我们看到它是一个数组或一个字符串(字符大小= 1个字节)。
那些。它将是80个字符的数组,即其长度将为80x1 = 80字节。任何不知道数组内容的人都可以在这里阅读有关内容:https ://www.programiz.com/c-programming/c-arrays因此,一个数组可以存储相同数据类型的许多值。您只需要告诉他什么类型的数据以及将有多少数据即可。
在第一个示例中,这是一个整数数组,即 它将为100个字节,并且由于每个整数占用4个字节,因此数组的长度将为100 x 4 = 400字节。在第二个示例中,FLOAT占用4个字节,因此它将是5个FLOAT的数组,因此其长度将为5 x 4 = 20字节。当我们对数组进行低级分析时,我们将看到它是保留的内存空间或缓冲区。这不是保留内存空间的唯一方法。还有其他类型的可变数据,它们也需要在内存中保留空间,这些空间将成为存储其内容的缓冲区。回到我们的练习:char buf[80];
这是一个字符数组,长度为80 x 1 = 80个字节,即它看起来像我们的20升广口瓶。如果我们尝试存储80个以上的字节,则存储区将溢出。现在让我们看看BUF缓冲区的使用位置。
我们看到缓冲区在两个用红色箭头标记的地方使用。第一条指令具有PRINTF函数,该函数用于在控制台中显示消息,该消息将为带引号的字符串。"buf: %08x cookie: %08x\n"
但是PRINTF函数不仅以引号打印字符串,而且还以指定格式打印字符串。里面的百分比告诉我们将创建输出行。我们看到字符串只是该函数的第一个参数。输出格式和其他自变量可能是多个(格式中的每个自变量%将有一个)。在我们的例子中,有两个。
在这种情况下,我们有两种X格式,因此如果我参考PRINTF格式表:
我们看到该函数将采用这些整数(INT)并将其插入到以数字系统16为底的输出行中。以十六进制格式。 08表示如果该数字少于8位,则该函数将用空格填充它。“ buf:%31x”和buf的输出将像这样buf: 19FED4
我们看到在此示例中,数字之前填充了空格。有几个修饰符可显示输出。此处列出了所有可能的情况:http : //www.cplusplus.com/reference/cstdio/printf/我们的情况是:
我们看到结果没有被截断,仅当要插入的参数的长度小于值时,结果才用空格填充十,前因此,我们知道函数打印两个十六进制数字,这是从两个参数获得。printf("buf: %08x cookie: %08x\n", &buf, &cookie);
我们知道一个变量具有一个内存地址和一个可以存储的值。看起来像我们的20升广口瓶。它具有其内容或含义,即 里面存储的公升水,而且如果我的车库里装满了类似的罐子,我还需要某种方法来确定我想要的罐子在我拥有的所有罐子中的哪个位置。&符号表示这一点。它返回jar的地址或位置,而不是其内容或值。AMPERSAND的定义
AMPERSAND用于指示将在其中存储数据的变量的内存地址。因此,如果我在控制台中运行可执行文件,例如,我将看到它执行PRINTF函数时将打印:
地址可能会在您的PC上更改,但是由于两个地址的低位地址都与BUF地址匹配,我们看到它们的定位方式是:
BUF地址小于COOKIE地址,因此它将增加。这些可变地址告诉我们什么? (在我的情况下,&BUF = 0x19FED4和&COOKIE = 0x19FF24)
两者均为十六进制格式。还记得这是%X格式吗?因此,我将0x放在前面以区分十进制数字,这些数字我们将不加任何表示。如果我在PYTHON控制台或PYCHARM中进行减法:
我们得到80个字节的结果,因为COOKIE变量据称恰好在BUF缓冲区结束的地方开始,所以差值使我们知道了缓冲区的大小。通常,当我们基于源代码制作这种类型的变量时,可能会发生编译器给我们提供的大小大于源代码中保留的大小的情况。编译器保证将保留至少80个字节,即他可以保留更多而不是更少。事实是,由于它具有PRINTF函数,因此我们已经对代码,变量的大小及其位置有所了解。现在让我们看一下使用BUF缓冲区的另一个地方,因为现在程序仅打印其地址,而不使用它在其中存储数据。int main(int argc, char **argv)
{
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344) printf("you win!\n");
}
此处,红线处的GET是用于从键盘输入数据的功能。数据将一直输入,直到我按Enter。该程序无法限制用户输入的数据量,也无法验证该数据。按下ENTER键之前输入的所有内容都将复制到BUF缓冲区。这就是问题。我们说过BUF最多只能存储80个字节,因此,如果我们输入更多内容,则会创建一个缓冲区溢出,这是所有的条件,因为如果用户写入的字节数超过80个,则我们的水箱将溢出并且液体将滴落。
事实是,BUF下是COOKIE变量,因此溢出将覆盖并填充您可以控制的值。例如,如果有人打印80 * A和4 * B,则80 * A将填充BUF,4 * B将填充COOKIE,并且我们知道,当有人在控制台中打印字符时,该值将保持较低。 ASCII码。
由于Cookie将用四个字母B填充,这四个B值等于0x42,因此我们可以保证Cookie的值为0x42424242,即在我的计算机上,Cookie地址0x19FF24将具有0x42424242作为内容。0x19FF24 => 42424242
事实是我们已经看到了如何溢出和控制COOKIE值。int main(int argc, char **argv)
{
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344) printf("you win!\n");
}
您必须打印“您赢了”才能完成练习。为此,COOKIE必须等于值0x41424344,并且如果没有溢出,这将是不可能的,因为从程序开始就从未改变过COOKIE的值。我们将无法打印“您赢了”,为此,我们使用了缓冲区溢出,这表示这可能导致程序执行除编程内容之外的某些动作。在这种情况下,您永远不能键入“ you win”,只有溢出会允许您这样做。AAAAAAAA换句话说,除了传递80 * A和4 * B来打印``您赢了''之外,您必须传递80 * A然后输入字母DCBA,因为这将导致值存储在COOKIE中ASCII码。44434241https://www.arumeinformatica.es/blog/los-formatos-big-endian-y-little-endian/存储数据的格式为LITTLE ENDIAN。换句话说,简单地说,内存中的数据以相反的顺序存储。
并且如果保存了序列0x41424344,则系统会将其保存为:44 43 42 41因此,在复制到内存时,将在键入时进行复制,因此我们必须以相反的顺序写入值,以便从内存中读取时形状正确。我们可以在控制台中运行可执行文件。
光标将闪烁,因为GET功能要求我输入输入。仔细键入80个字符,然后键入DCBA。在PYTHON或PYCHARM控制台中,我可以打印该行,将其复制而不带引号,并将其粘贴到控制台中,以免疯狂打印,然后按Enter键输入它。
我们看到我们得到了“您赢了”。我们可以在调试器中看到这一点。为此,我们将使用X64DBG。
我选择32位版本。
如果我们停止在NTDLL.DLL库,则使用F9再次按RUN。我们看到调试器在STACK1模块的第一条指令(称为ENTRY POINT)或模块执行的第一条指令处停止。
显然,这不像我们的源代码。您必须了解,编译器添加了大量代码以使可执行文件正常工作并正常运行。我们将尝试找到我们的主要功能。我们可以通过查看程序的各行来确定自己的方向。
我们只选择了当前区域中的搜索。我们知道这些行将在同一部分中。
在这里,我们看到编译器添加了一些程序行和其他代码行。我们双击其中一行。
现在您可以看到更多。我们看到了对MessageBoxA,PRINTF,GETS函数的调用,以及一个与值0x41424344的比较。另外,我们添加了一个用于反编译SNOWMAN的插件。我们可以尝试看看它如何反编译代码,即他如何尝试从编译文件中获取源代码或尽可能类似的东西。
我们看到这并不完美,但是比以前要好。我将BP放在函数的开头,然后按F9键,直到调试器停止。
对于那些不知道函数参数是什么的人。
在我们的例子中,main函数具有参数,但是在函数内部不使用它们。int main(int argc, char **argv)
{
int cookie;
char buf[80];
printf("buf: %08x cookie: %08x\n", &buf, &cookie);
gets(buf);
if (cookie == 0x41424344) printf("you win!\n");
}
在这里,我们看到有两个参数,当可执行文件编译为32位时,它们将通过堆栈传递。在函数调用之前,参数将存储在堆栈中。当我们在函数的开头停止时,堆栈上的第一个值将是RETURN ADDRESS,即函数完成后函数将返回的位置,并且在该值以下将是该函数的参数。如果右键单击RETURN ADDRESS并选择FASSLOW DWORD IN DISASSEMBLER,我将看到函数完成后调试器应返回的位置。
他会回到这里。这意味着从更高的调用中调用了main函数。我可以将BP放在此处,重新开始练习,并确保已完成。
我将把BP提早一点,然后重新启动程序。
调试器将在此处停止。
使用这些PUSH指令将参数保存到主函数中。以下是想要了解更多有关函数参数的人员的链接:https : //publications.gbdirect.co.uk/c_book/chapter10/arguments_to_main.html这不是很难。 ARGC的第一个参数是INT,它表示用于执行程序的控制台参数的数量,包括可执行文件的路径,而ARGV是指向字符串的指针数组。我们看到如果我更改命令行,我将传递更多参数并重新加载。
在这里,调试器将要使用PUSH指令保存参数时停止。存储的第一个参数是最远的参数,保存的最后一个参数将是函数的第一个参数。我跟踪,每个PUSH指令保存参数。
在这里,我可以看到函数的参数。以上是对ARGC的第一个论点。它是3,因为它标记了传递到控制台的参数数量。
这是三个参数。现在,我们按F7键执行“步入”并输入功能。在这里,我们看到进入CALL时,调试器将RETURN ADDRESS保存到堆栈中。
因此,正如我们在进入函数时所说的那样,要保存到堆栈中的第一件事是RETURN ADDRESS(在32位编译中),下面是该函数的参数,首先是第一个参数,然后依次是其余参数。
如我们所见,第二个参数是一个指针数组。在这里,我们在内存中看到有三个指针指向作为参数传递的三行。
在这里我们在函数的开始处停止了,在下面我们有返回地址和参数。我们阐明该文件是32位编译的,因为在64位版本中,参数是以不同的方式传递的。我们稍后会看到。然后函数开始执行。第一件事就是所谓的PROLOGUE,它存储了调用我们函数的EBP值。
这将使EBP值保持在返回地址的正上方。
如果我按照说明使用F7。我看到EBP GUARDADO值在返回地址上方的堆栈中。
PROLOGUE中的以下指令:MOV EBP,ESP设置已保存的当前函数的EBP值,该当前函数是调用我们函数的父函数(在这种情况下,我的主要函数是基于EBP的函数,在其他情况下,它可能会有所不同,稍后再查看)通过将当前ESP值放入EBP中,我们确保为当前函数创建一个框架。
现在,由于此函数基于EBP或EBP BASED,因此EBP值将存储在函数内部,并且将其作为参考接受,并且ESP会更改。
该EBP值作为基数。在基于EBP的函数中,变量和参数可以通过它们与该地址的距离来命名,该距离将存储在EBP值中,直到其尾声为止。我们可以在列表中看到几个被称为EBP-4或EBP-54的变量,这些变量是指它当前正在接受的EBP值。我们可以说,在序言之后EBP取值后,它看起来就像是消耗,因此参数总是朝那个方向前进,因此EBP + XXX引用了参数(存储的EBP和RETURN ADDRESS也较低,但是没有链接在代码中),而变量(如我们所见)将在该地址上方,因此指向EBP-XXX的链接引用了一些局部变量。因此,在基于EBP的函数中:EBP + XXXX =传递给函数EBP的参数-XXXX =局部函数变量在序言之后,将有某种方式为变量保留空间。在我们的情况下,这是通过向上移动ESP来完成的,以便保留下面的剩余空间用于所有可变长度的总和,有时还以防万一,这取决于编译器。00401043 | 83EC 54 | SUB ESP,54我们看到ESP位于EBP上方,它将保留链接,并且0x54转换为十进制是84,这是BUF和COOKIE长度的总和。请记住,它们分别是80和4。

执行时,将为大小为84字节的BUF和COOKIE变量创建一个空间。您可以单击水平方向上的第一列,然后查看EBP值并在堆栈上找到该值。显然,现在它将更低。
我双击这里。
因此,我们还将在堆栈上具有关于EBP的值。例如,EBP-4与列表匹配,-4显示在堆栈的第一列中,在说明中也显示为EBP-4。
如果我追踪一下,我们会看到从ESP保留变量的位置开始,它总是会向上移动,因为它必须考虑为变量分配的空间。当对MessageBoxA执行4个PUSH时,调试器将变量放在保留空间上方,并增加ESP。
如果查看堆栈,会看到在红色标记的保留空间上添加的4个绿色参数。
当您输入MessageBoxA函数时,此函数的RETURN ADDRESS将存储在堆栈中。
这是MessageBoxA的返回地址。当我通过使用F8进行跟踪而到达此函数的RET时,我将执行MessageBoxA。
我们看到调试器将返回到MessageBoxA调用之下。
并且您已为MessageBoxA函数传递的PUSH值和为此函数使用的RETURN ADDRESS已被使用,并且ESP再次位于保留区域的上方,就像调用任何函数之前一样。调用PRINTF函数将发生相同的事情。通过PRINTF函数后,将打印BUF和COOKIE地址。
我的计算机上的BUF地址为0x19FED4,COOKIE地址为0x19FF24。此处,程序读取BUF地址,以将其传递给GETS功能并填充BUF。我们可以检查地址是否与0x19FED4控制台显示的地址匹配。
在这里,我们看到的是EBP-54。如果我双击显示-54的堆栈,它将显示我的计算机上的地址为BUF = 0x19FED4。
现在,当我将输入的数据保存在此地址时,可以将它们放入转储中以查看字节如何存储在此处。
他们来了。此外,由于没有数据,因此下面没有显示任何内容。当我使用F8调用GETS函数时,我将需要进入控制台,键入并按Enter以填充BUF缓冲区并重写COOKIE。
我们看到COOKIE变量在我的机器上为19FF24。在这里,程序将cookie与0x41424344进行比较。
如果像以前一样将HORIZON设置为EBP值,我们会看到EBP-4除了地址之外还说它是一个COOKIE。
我双击这里。我们看到EBP-4是一个COOKIE,因为该变量处于-4堆栈级别,从而使HORIZON归零。
我们看到该程序不会跳转,并会告诉我们您赢了!
因此,我们手动实现了您的目标!我们使用X64DBG动态分析了STACK1,X64DBG是调试器,不允许我们在不启动程序的情况下对其进行分析。为此,我们必须使用其他工具,例如IDA PRO,GHIDRA或RADARE。我可以通过PYTHON创建一个脚本模型来进行练习。import sys
from subprocess import Popen, PIPE
payload = b"A" * 80 + b"\x44\x43\x42\x41"
p1 = Popen(r"C:\Users\ricardo\Desktop\abos y stack nuevos\STACK1_VS_2017.exe", stdin=PIPE)
print ("PID: %s" % hex(p1.pid))
print ("Enter para continuar")
p1.communicate(payload)
p1.wait()
input()
对于PYTHON 3,我必须在PRINT函数中加上括号,并在添加应该为字节的行时要小心(将b放在PYTHON 2中的行前面)。在运行文件时,我检查路径是否正确。
好。我们已经有了用于操作STACK1的PYTHON 3的脚本模型。在下一部分中,我们将继续在IDA,RADARE和GHIDRA中进行静态分析。
请注意,除了STACK1任务外,还有版本2、3和4。您可以尝试解决它们。它们非常简单,类似于STACK1,所以请不要感到无聊。在下一部分中,我们将看到IDA FREE,RADARE和GHIDRA。下一部分再见3。Ricardo Narvaha25/10/2019PS#1可以在我的主页上下载漂亮的PDF-yasha.suPS#2很快,我将继续撰写文章https://habr.com/en/post/464117/的后续文章,内容涉及我如何为Chris Kaspersky父亲收集帮助以及这的内容发生了不要觉得无聊。