Protostar二进制靶场栈溢出题目原理及基础解析
Ba1_Ma0 历史精选 240浏览 · 2025-02-17 05:09

简介

pwn是ctf比赛的方向之一,也是门槛最高的,学pwn前需要很多知识,这里建议先去在某宝上买一本汇编语言第四版,看完之后学一下python和c语言,python推荐看油管FreeCodeCamp的教程,c语言也是

pwn题目大部分是破解在远程服务器上运行的二进制文件,利用二进制文件中的漏洞来获得对系统的访问权限

这是一个入门pwn很好的靶场,这个靶场包括了:

下载地址:



实验环境部署

下载完Protostar的镜像文件后,将其安装到vmware上,然后打开

ssh远程连接



输入IP后点击打开,输入账号密码,然后输入/bin/bash,更换为可以补全字符串的shell



在网站的Protostar靶机的介绍处,我们要破解的题目存放在这个目录下

我们进入这个目录,详细查看文件

发现文件都是红色的,我们详细的查看文件

这是一个32位的setuid程序

setuid

什么是setuid?

这么说起来有点绕,我们来举一个例子

演示我们用user用户运行一个vim然后新开一个窗口查看后台进程

这里可以看到,我们的vim正在以user的权限运行中,然后我们去执行一下靶机上的setuid文件看看



这里可以看到,我们虽然是user用户,但执行文件后,文件正以root权限运行我们查看文件的权限
r代表读,w代表写,x代表执行,那s是什么呢

当这个位被user权限的用户执行时,linux实际上是以文件的创造者的权限运行的,在这种情况下,它是以root权限运行的我们的目标就是,破解这些文件然后拿到root权限

STACK ZERO程序源代码分析



我们破解一个简单的题,通过分析汇编语言,以及相关的知识,来带大家进一步了解程序是如何运行的以及如何破解的

题目详情:这个级别介绍了内存可以在其分配区域之外访问的概念,堆栈变量的布局方式,以及在分配的内存之外进行修改可以修改程序执行。
分析源代码,这是由c语言写成的程序,

很明显,我们要使if语句成功判断,打印成功改变变量的字符串,关于如何破解程序,获取root权限,我会在下一篇文章中介绍

gets函数漏洞分析

在gets函数的官方文档里,有这么一句话
永远不要使用gets函数,因为如果事先不知道数据,就无法判断gets将读取多少个字符,因为gets将继续存储字符当超过缓冲区的末端时,使用它是极其危险的,它会破坏计算机安全,请改用fgets。

汇编分析

我们使用gdb打开程序进行进一步的分析

然后我们查看程序的汇编代码,来了解程序的堆栈是如何工作的



第一条是将ebp推入栈中,ebp是cpu的一个寄存器,它包含一个地址,指向堆栈中的某个位置,它存放着栈底的地址,在因特尔的指令参考官方资料中,可以看到,mov esp、ebp和pop ebp是函数的开始和结束https://www.intel.de/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf
在这个程序中,最初操作是将ebp推入栈中,然后把esp的值放入ebp中,而当函数结束时执行了leave操作

可以看到,程序开头和结尾的操作都是对称的之后执行了如下操作

AND 指令可以清除一个操作数中的 1 个位或多个位,同时又不影响其他位。这个技术就称为位屏蔽,就像在粉刷房子时,用遮盖胶带把不用粉刷的地方(如窗户)盖起来,在这里,它隐藏了esp的地址

然后esp减去十六进制的60

在内存移动的位置为0,在堆栈上的偏移为0x5c段地址+偏移地址=物理地址举一个例子,你从家到学校有2000米,这2000米就是物理地址,你从家到医院有1500米,离学校还要500米,这剩下的500米就是偏移地址这里推荐大家看一下《汇编语言》这本书,在这本书里有很多关于计算机底层的相关知识

lea操作是取有效地址,也就是取esp地址+偏移地址0x1c处的堆栈然后DWORD PTR要取eax的地址到esp中调用gets函数

然后对比之前设置的值,0,用test来检查

这些就是if循环的操作了

方法一:溢出

我们先在gets函数地址下一个断点,这样程序在运行到这个地址时会停止继续运行下一步操作。

然后在调用gets函数后下一个断点,来看我们输入的字符串在哪里

然后设置

这个工具可以帮助我们在每一步操作停下来后,自动的运行我们设置的命令

然后我们输入run运行程序到第一个断点

现在我们马上就要执行gets函数了,输入n执行gets函数

我们随意输入一些内容,按下回车键
可以看到,0x41是A的ascii码,我们距离0x0000000还有一段距离



算了一下,我们需要68个字符才能覆盖输入q退出gdb然后使用echo或者python对程序进行输入



可以看到,我们已经成功打印出了正确的字符

什么是缓冲区溢出

当系统向缓冲区写入的数据多于它可以容纳的数据时,就会发生缓冲区溢出或缓冲区溢出,用更简单的话说就是在程序运行时,系统会为程序在内存里生成一个固定空间,如果超过了这个空间,就会造成缓冲区溢出,可以导致程序运行失败、系统宕机、重新启动等后果。更为严重的是,甚至可以取得系统特权,进而进行各种非法操作

什么是寄存器

寄存器是内存中非常靠近cpu的区域,因此可以快速访问它们,但是在这些寄存器里面能存储的东西非常有限

计算机寄存器是位于CPU内部的一组用于存储和处理数据的高速存储器。用于存放指令、数据和运算结果

常见的寄存器名称以及作用:

Stack One

程序静态分析



源代码分析

首先程序定义了两个函数变量

整数型变量 modified 和字符型变量buffer,其中字符型变量buffer的字符存储最大为64个字节

然后程序检测了我们输入的参数

如果我们只运行程序,不输入参数就会输出please specify an argument并结束程序

之后程序定义了一个变量和进行了一个字符串复制操作

modified变量为0,然后将我们输入的参数复制到buffer变量里

然后程序做了一个简单的if判断

如果modified变量等于0x61626364就输出you have correctly got the variable to the right value,代表着我们破解成功0x61626364是十六进制,转换字符串是大写的ABCD


也就是说,我们使modified变量变成ABCD就成功了,但是modified变量设置为0,这里我们就需要栈溢出覆盖变量原本设置的值

汇编分析

使用gdb打开程序,输入指令查看汇编代码



程序最关键的地方在这里

它使用mov指令将esp+0x5c栈内地址的值移动到eax寄存器里,然后用cmp指令将eax寄存器里的值与0x61626364做对比,如果对比的值不一样就执行jne指令跳转到0x80484c0地址继续执行其他指令

程序动态分析

我们先在程序执行对比指令的地址下一个断点

然后设置一下自动运行我们设置的命令



然后执行程序,并指定参数



程序执行到我们设置的断点处自动执行了我们上面设置的命令,在这里可以看到我们输入的8个大写A在栈中的位置,并且eax寄存器里的值为0

之前说过,程序将esp+0x5c地址处的值移动到了eax寄存器里,然后执行对比指令



我们查看esp+0x5c地址存放的值



esp+0x5c地址就是栈里的0xbffff78c,每一段存放四个字符,c代表的是12



从存放我们输入的值的栈地址到esp+0x5c,中间共有64个字符,也就是说,我们需要输出64个字符+4个我们指定的字符才能覆盖modified变量



在这里还有一个知识点是在x86架构里,读取是由低到高的,要使modified变量变成0x61626364,不能直接输入abcd,而是dcba



成功pwn掉了程序

Stack Two

程序静态分析

程序源代码:

这个程序代码和第一个差不多,只不过是将我们的输入变成了读取环境变量里的GREENIE变量内容

什么是环境变量

任何计算机编程语言的两个基本组成部分,变量和常量。就像数学方程式中的自变量一样。变量和常量都代表唯一的内存位置,其中包含程序在其计算中使用的数据。两者的区别在于,变量在执行过程中可能会发生变化,而常量不能重新赋值

这里只举几个常见的环境变量

$PATH

包含了一些目录列表,作用是终端会在这些目录中搜索要执行的程序查看$PATH环境变量



假如我要执行whoami程序,那么终端会在这个环境变量里搜索名为whoami程序

搜索的目录如下



而whoami程序在/usr/bin目录下,终端会执行这个目录下的whoami程序



而windows的PATH环境变量在这可以看到





$HOME

包含了当前用户的主目录



$PWD

包含了当前用户目前所在的目录位置



关于环境变量的更多信息:

回到正题

首先获取了一个名为GREENIE的环境变量,然后将内容赋予variable变量,之后if判断modified是否等于0x0d0a0d0a,这个和第一个程序一模一样,只不过我们不是通过输入来破解程序,而是将payload放到指定的环境变量里,然后程序读取环境变量

直接运行就能成功破解了



Stack Three

程序静态分析



源代码分析

这个程序首先定义了一个win函数

调用这个win函数会输出code flow successfully changed,表示我们成功破解了程序

然后在mian函数内定义了一个指针变量fp和字符型变量buffer,buffer存储的字符大小为64位

什么是指针?

在C语言中,指针是一种特殊的变量类型,它存储了一个内存地址。这个内存地址可以是其他变量或数据结构在内存中的位置

指针提供了直接访问和操作内存中数据的能力。通过指针,我们可以间接地访问、修改和传递数据,从而不需要直接对变量本身进行操作

将fp的值设为0表示一个无效的指针,即它不指向任何有效的内存地址。这样做可以用来初始化指针变量,或者将指针重置为空指针

之后程序会使用gets函数接收用户的输入,并将接受到的字符串存储在buffer变量里,gets函数是一个危险的函数,他会造成缓冲区溢出,具体的解释可以看我的第一篇文章

程序接受输入后会进行一个if判断

if(fp)检查fp是否指向了某个有效的函数。如果fp不为空(即非零),则输出calling function pointer, jumping to 0x%08x,然后执行函数指针 fp 所指向的函数

也就是说,我们需要溢出覆盖fp设置的值,将fp原本的值改为win函数的地址,之后进入if判断后,会执行win函数

汇编分析

使用gdb打开程序,输入指令查看汇编代码



程序最关键的地方是这两行



它将esp+0x5c地址的值转移到了eax寄存器里,然后调用call指令执行eax寄存器里的值

也就是说,我们只要将esp+0x5c地址的内容覆盖成win函数的地址,就能成功破解程序

程序动态分析

我们在0x08048471地址处下一个断点

然后设置一下自动运行的命令



运行程序,由于if判断,fp的值不能为零才能进入if判断,但是程序设置的fp的值为0,我们先输入一长串的垃圾字符,覆盖原来的值



查看esp+0x5c地址处的值



fp函数指针的值就在图中圈出来的地方,根据计算,我们需要64个字符+win函数地址才能控制fp函数指针

这时候我们可以用objdump工具来查看win函数地址



或者直接使用gdb直接查看win函数就能知道地址



两个方法都能用

知道了win函数地址后,直接运行以下命令就能破解程序



Stack Four

程序静态分析



这个程序很简单,就不多做介绍了,和上一个一模一样,只不过将设置的fp函数指针去掉了,我们需要自己控制程序指针进行跳转到win函数地址

直接进行程序动态分析

程序动态分析

使用gdb打开程序,输入指令查看汇编代码



代码很少,我们要做的只有一件事,控制ret指令的返回地址,让程序跳转到win函数地址执行参数

leave和ret指令

在汇编语言中,ret指令用于从子程序返回到调用它的主程序。当执行到ret指令时,程序会跳转到主代码的地址,继续执行主程序的代码

在汇编语言中,leave指令用于清空栈,它会清除我们这次运行程序时获取的用户输入之类的,还原之前的状态

我们在leave指令的地址下一个断点

运行程序,然后随便输入一些字符,然后查看栈里的内容,记录下来,之后会用到



然后输入n执行下一个指令,让ret指令执行,输入info registers查看寄存器的值



当前eip寄存器的值为0xb7eadc76,也就是说,执行了rat指令后,程序回到了0xb7eadc76继续执行之后的命令

但是返回的地址也是在栈中的



根据计算,我们需要输入76个字符+win函数地址才能覆盖原来ret返回的地址,让程序跳转到win函数地址处执行



成功破解

Stack Five

程序静态分析



这个程序很简单,只有两行,作用只是接受我们的输入

setuid

什么是setuid?

这么说起来有点绕,我们来举一个例子

演示我们用user用户运行一个vim然后新开一个窗口查看后台进程



什么是栈

可以把栈想象成一个堆积的书本,你可以把新的书本放在最顶部,也可以取出最顶部的书本。

当程序执行时,它会使用栈来跟踪函数调用和变量的值。每次你调用一个函数,计算机会在栈上创建一个新的“帧”(就像书本一样),用来存储这个函数的局部变量和执行时的一些信息。当函数执行完毕时,这个帧会被从栈上移除,就像取出一本书本一样。

栈通常是“后进先出”的,这意味着最后放入栈的数据会最先被取出。这是因为栈的操作是非常快速和高效的,所以它经常用于管理函数调用和跟踪程序执行流程

为什么要覆盖ret返回地址

覆盖 ret 返回地址是一种计算机攻击技巧,攻击者利用它来改变程序执行的路径。这个过程有点像将一个路标或导航指令替换成你自己的指令,以便程序执行到你想要的地方。

想象一下,你在开车时遇到一个交叉路口,路标告诉你向左拐才能到达目的地。但是,攻击者可能会悄悄地改变路标,让你误以为需要向右拐。当你按照这个伪装的路标行驶时,你最终会到达攻击者想要的地方,而不是你本来的目的地。

在计算机中,程序执行的路径通常是通过返回地址控制的,这个返回地址告诉计算机在函数执行完毕后应该继续执行哪里的代码。攻击者可以通过修改这个返回地址,迫使程序跳转到他们指定的地方,通常是一段恶意代码,而不是正常的程序代码

获取ret返回地址

使用gdb打开程序,在执行leave指令的地方下一个断点



运行程序,随便输入一些字符,然后查看栈状态



另外开一个远程连接界面,使用gdb打开程序,在执行ret指令的地方下一个断点



在第二个终端界面运行程序,随便输入一些字符,然后执行ret指令,查看程序跳转的地址





根据计算,我们需要80个字符就能完全覆盖ret的返回地址,然后再将我们的shellcode放到控制数据的堆栈里



nop指令

NOP指令是一种特殊的机器指令,它在计算机中执行时不做任何操作。简单来说,NOP指令是一种“空操作”,它不改变计算机的状态、不影响寄存器的值,也不执行任何计算或跳转

为了防止我们shellcode收到干扰,我们在shellcode代码前添加一些nop指令即可

脚本编写

首先设置一个76位的垃圾字符,然后利用struct模块的pack功能,作用是将一个无符号整数(I 表示无符号整数)转换为二进制数据,跳转到控制数据的栈里,最后写入nop指令和shellcode代码,shellcode代码可以在这个网站里找到



这是一个linux x86架构执行/bin/sh的shellcode

如果我们直接运行脚本是得不到/bin/sh的



其实/bin/sh已经执行了,只是没有输入,我们可以用cat命令来重定向到标准输入输出





成功破解程序

Stack Six and Stack Seven

Stack Six和Stack Seven的源代码是一样的,可以通过ret to libc的方式获取shell

程序静态分析



ret to libc

ret to libc是将程序的返回地址覆盖为标准 C 库中的某个函数的地址,如 "system" 函数,这个函数可以用来执行系统命令。然后,攻击者构造一个有效的参数,比如"/bin/sh",将其传递给 "system" 函数,从而获取shell

寻找system函数地址和/bin/sh字符串

用gdb打开程序,在getpath函数执行leave指令的地址打一个断点



运行程序后随意输入一些字符串,让后寻找system函数的地址



system函数地址为:0xb7ecffb0,找到了system函数地址,现在我们就要找让system函数执行命令的字符串,为了获取shell,我们寻找"/bin/sh"字符串

什么是内存映射

内存映射是一种操作系统和计算机体系结构中常见的技术,用于将文件或其他设备的内容映射到进程的地址空间,使得进程可以像访问内存一样访问这些内容

什么是libc库

在编译程序时,我们要调用函数,为了缩小程序大小,我们通常会动态编译文件,程序调用函数时,就会到指定的libc库里查找并执行

执行i proc mappings查看程序内存映射



stack6的libc库为:/lib/libc-2.11.2.so,libc的基地址为:0xb7e97000

现在新开一个终端,在libc库里查找/bin/sh字符串的地址



字符串/bin/sh的偏移地址为:1176511,libc的基地址+字符串的偏移地址=程序调用字符串的完整地址

寻找程序溢出大小

查看main函数代码



程序调用了getpath函数后,会返回0x08048505继续执行下一个指令,重新运行程序,随便输入一些字符,然后查看栈状态



我们输入的字符串离0x08048505有80个字节,在0x08048505上面还有一个0x08048505,那个只是普通的值,在程序返回main函数时,还会调用其他的系统函数,所以下一个才是getpath函数ret main函数的值

现在我们可以写一个脚本来破解程序

在执行system函数时,会调用一个返回地址,可以随意输入一些字符,然后就会执行"/bin/sh"字符串



执行程序,成功获得root权限



Stack Seven

Stack Seven和Stack Six的程序源代码很像,只是修改了一下判断的值



我们只需要多加一个ret指令的地址,让程序返回到我们指定的地方执行system函数和/bin/sh字符串

寻找ret地址

我们可以使用objdump工具来寻找ret指令的地址



这里有很多ret指令的地址,我们随便选一个即可开始写脚本

脚本和stack six一样,只需要添加一个ret指令地址即可



成功获得root权限

0 条评论
某人
表情
可输入 255