pwn入门学习(一)
pwn入门之路(一)
学pwn!!!
pwn环境搭建
物理机archlinux,使用docker搭建pwn做题环境
1 | e |
1 | # pwndgb |
1 | #!/bin/bash |
栈
基本栈介绍
栈是一种典型的**先进后出(Last in First Out)**的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作。两种操作均有栈顶以及栈底。
高级语言 在运行时都会被转换为汇编程序,在汇编程序运行过程中,使用栈数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保护函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。程序的栈是从进程地址空间的高地址向低地址增长
函数调用栈
程序的执行过程可看做连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令(紧接call指令)处继续执行。函数调用过程通常使用堆栈实现,每个用户进程对应一个调用栈结构(call stack)。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备回复以及存储本地局部变量。
IA32处理器寄存器:
不同架构的CPU,寄存器名称被添加不同前缀以指示寄存器的大小。例如X86架构用字母"e(extended)"作名称前缀,指示寄存器大小为32位;x86_64架构用字母"r"作名称前缀,指示各寄存器大小64位。
许多编译器使用帧指针寄存器FP(Frame Pointer)记录栈帧基地址。
栈帧结构
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的悉尼线。每个未完成运行的函数占用一个独立的连续区域,称为栈帧(Stack Frame)栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈,当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
帧基指针(EBP),栈顶指针(ESP),并在引用汇编代码时分别记为%ebp和%esp
函数调用栈的典型内存布局:
图中给出主调函数(caller)和被调函数(callee)的栈帧布局,"m(%ebp)"表示以EBP为基地址、偏移量为m字节的内存空间(中的内容)。函数可以没有参数和局部变量,故"Argument(参数)"和"Local Variable(局部变量)"不是函数栈帧结构的必需部分。
图中的入栈顺序
| 实参N~1→主调函数返回地址→主调函数帧基指针EBP→被调函数局部变量1~N |
其中,主调函数将参数按照调用约定依次入栈(图中为从右到左),然后将指令指针EIP入栈以保存主调函数的返回地址(下一条待执行指令的地址)。进入被调函数时,被调函数将主调函数的帧基指针EBP入栈,并将主调函数的栈顶指针ESP值赋给被调函数的EBP(作为被调函数的栈底),接着改变ESP值来为函数局部变量预留空间。此时被调函数帧基指针指向被调函数的栈底。以该地址为基准,向上(栈底方向)可获取主调函数的返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值,而该地址处又存放着上一层主调函数的帧基指针值。本级调用结束后,将EBP指针值赋给ESP,使ESP再次指向被调函数栈底以释放局部变量;再将已压栈的主调函数帧基指针弹出到EBP,并弹出返回地址到EIP。ESP继续上移越过参数,最终回到函数调用前的状态,即恢复原来主调函数的栈帧。如此递归便形成函数调用栈。
注:
- x86:函数参数在函数返回地址的上方
- X64:System V AMD64 ABI(Linux, FreeBSD,macos等采用)中前六个整形或指针参数依次保存在RDI,RSI,RDX,RCX,R8和R9寄存器中,如果还有更多的参数的话才会保存在栈上。且内存地址不能大于0x00007FFFFFFFFFFF,6个字节长度,否则会抛出异常。
函数调用约定
创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数。主调函数必须精确存储这些参数,以便被调函数能够访问到他们。函数通过选择特定的调用约定,来表明其希望以特定方式接受参数。此外,调用约定规定先前入栈的参数由主调函数还是被调函数负责清除,以保证程序的栈顶指针完整性。
函数调用约定通常规定:
- 函数参数的传递顺序和方式:
- 栈的维护方式
- 名字修饰(Name-mangling)策略
常见调用约定:
- cdecl调用约定
- stdcall调用约定(微软命名)
- fastcall调用约定
- thiscall调用约定
- naked call调用约定
- pascal调用约定
栈溢出原理
介绍:栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞
发生栈溢出的基本前提是:
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好的控制
**tips:**缓冲区溢出漏洞
缓冲区溢出漏洞是指程序在运行时,向固定大小的缓冲区写入超过其容量的数据,多余的数据会越过缓冲区的边界覆盖相邻内存空间,造成缓冲区溢出。攻击者回利用该漏洞,向未控制用户输入的程序填入超过缓冲区容量的字节数量,以覆盖相邻内存空间的返回地址、函数指针、堆管理结构等合法数据,从而使程序运行失败,或者转向执行其他程序代码,预先注入到内存缓冲区的代码。缓冲区溢出后执行的代码,会以原有程序的身份权限运行。
引发缓冲区溢出的原因是缺乏类型安全功能的程序设计语言。出于效率的考虑,部分函数不对数组边界条件和函数指针引用等进行边界检查。例如:strcpy(),strcat,sprintf,**gets()**等函数中,数组和指针都没有自动进行边界检查。
缓冲区溢出不是一种漏洞,而是一类漏洞。常见的缓冲区溢出漏洞包括:
- 栈溢出漏洞
- 堆溢出漏洞
- 整数溢出漏洞
- SHE结构基础漏洞
- 单字节溢出漏洞
- 格式化字符串漏洞
- C++虚函数漏洞
基本示例:最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址,**当然需要确保这个地址所在的段具有可执行权限。**例:
1 |
|
这段程序明显可以看出,只是接受字符串,然后打印出来,也就是说,唯一调用的其实只有main函数与vulnerable函数,但是,我们希望可以控制程序执行success函数。
我们使用gcc -m32 -fno-stack-protector stack_example.c -o stack_example
来编译该文件,可以看到,编译器发出警告。
可以看到gets本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈溢出。
上述指令中,-m32
指生成32位程序;-fno-stack-protector
指的是不开启堆栈溢出保护,即不生成canary,此外,避免开启pie(Position Independent Executable),防止加载基址被打乱。不同gcc版本对于PIE的默认配置不同,我们可以使用gcc -v
查看gcc默认的开关情况。如果含有--enable-default-pie
参数,则需要在编译指令中添加-no-pie
关闭。
使用checksec检查文件开启的保护:
可以看到,程序为32位,且开启了PIE保护,我们使用-no-pie
重新编译一下
提到编译时的PIE保护,Linux平台下还有地址空间分布随机化(ASLR)的机制。简单来说即使可执行文件开启了PIE保护,还需要系统开启ASLR才会真正打乱基地址,否则程序运行时依旧会在加载一个固定的基址上(不过和No PIE时基址不同)。我们可以通过修改/proc/sys/kernel/randomize_va_space
来控制ASLR启动与否,具体选项有:
- 0,关闭ASLR,没有随机化。栈,堆,.so的基地址每次都相同。
- 1,普通的ASLR。栈基地址、mmap基地址、.so加载基地址都将被随机化,但是堆基地址没有随机化。
- 2,增强的ASLR,在1的基础上,增加了堆基地址随机化。
我们这里修改并查看:
现在,我们可以使用IDA来进行静态分析我们的程序:
进入vulnerable
函数中
1 | int vulnerable() |
可以看到,字符串距离ebp的长度是0x14。
那么相应的栈结构为
1 | +-----------------+ |
我们继续获取到success的地址:
那么如果我们输入的字符串为
0x14*'a' + 'bbbb' + success_addr
因为gets()函数读到回车才算结束,那么我们可以直接读取所有的字符串,并且将saved edp覆盖为bbbb,将retaddr覆盖为success_addr,即,此时的栈结构为:
1 | +-----------------+ |
我们利用pwntools来编写exp:
1 | #!/usr/bin/env python3 |
可以看到,成功执行了success函数。
小结
第一步:寻找危险函数
通过寻找危险函数,我们可以快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下:
- 输入
- gets,直接读取一行,忽略
\x00
- scanf
- vscanf
- gets,直接读取一行,忽略
- 输出
- sprintf
- 字符串
- strcpy,字符串复制,遇到
\x00
停止 - strcat,字符串拼接,遇到
\x00
停止 - bcopy
- strcpy,字符串复制,遇到
第二步:确定填充长度
这一部分主要计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开IDA,根据其给定的地址计算偏移。一般变量会有以下几种索引模式
-
相对于栈基地址的索引,可以直接通过查看EBP相对偏移获得
-
相对应栈顶指针的索引,一般需要进行调试,之后还是会转换到第一种类型
-
直接地址索引,就相当于直接给定了地址。
一般来说,我们会有如下的覆盖需求 -
覆盖函数返回地址,直接看EBP即可
-
覆盖栈上某个变量的内容,需要更加精细的计算
-
覆盖bss段某个变量的内容。
-
根据现实执行情况,覆盖特定的变量或地址的内容
之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。
基本ROP
随着NX(Non-eXecutable)保护的开启,传统的直接向栈或者堆上直接注入代码的方式难以继续发挥效果,由此攻击者们也提出相应的办法来绕过保护。
目前被广泛使用的攻击守法是返回导向编程(Return Oriented Programming),其主要思想是在**栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets
)来改变某些寄存器或者变量的值,从而控制程序的执行流程。
gadgets
通常是以ret
结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。
返回导向编程这一名称的由来是因为其核心在于利用了指令集中的ret
指令,从而改变了指令流的执行顺序,并通过数条gadgets
“执行”了一个新程序。
使用ROP攻击一般得满足如下条件:
- 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
- 可以找到满足条件的
gadgets
以及相应gadgets
的地址。
作为一项基本的攻击手段,ROP攻击并不局限与栈溢出漏洞,也被广泛应用在堆溢出等各类漏洞的利用当中。
当系统开启ASLR(地址随机化保护)
时,意味着gadgets
在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的gadgets
之后可以通过其他方式泄露程序运行环境信息,从而计算出gadgets
在内存中的真正地址。
ret2text
原理
ret2text即控制程序执行程序本身已有的代码(即,.text
段中的代码)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets
),这也就是说我们所说的ROP
。
这时,我们需要知道对应返回代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
栈溢出的几种保护机制:
参考: 帧溢出的几种保护机制
- Stack Protector(栈保护)
也称canary
,当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息(在linux中称cookie信息为canary),当函数真正返回的时候会验证cookie信息是否合法,如果不合法,则推出运行,当攻击者覆盖返回地址时会将canary覆盖掉,倒是栈保护检查失败,从而防范shellcode的执行,以此达到防护栈溢出的目的。其原理实在一个函数的入口处,先从fs/gs
寄存器中取出一个4字节(eax)或者8字节(rax)的值存到栈上,当函数结束时会检查这个栈上的值是否和存进去的值一致。
- NX(DEP)(堆栈不可执行)
NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。(等同于Windows下的DEP)。
gcc编译器默认开启NX选项,-z execstack / -z noexecstack
(关闭 / 开启) - PIE(ASLR)(内存地址随机化)
标准的可执行程序需要固定的地址,并且只有被装载到这个地址才能正确执行,PIE能使程序像共享库一样在主存任何位置装载,这需要将程序编译成位置无关,并链接为ELF共享对象。通常NX和地址空间分布随机化(ASLR)会同时工作。
-no-pie / -pie(关闭 / 开启)
- RELRO
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为Partial RELRO
,说明对GOT表具有写权限。
-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)
例子
拿到题目,首先使用checksec检查下保护
开启了栈不可执行保护,使用ida打开
看到存在gets()函数,该函数是高危函数,存在栈溢出。C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
shift+f12
发现后门函数
发现在secure函数中,获得后门地址0x0804863A
,接下来我们需要确定能控制的内存的其实距离距离main函数的返回地址的字节数。
可以看到s是通过相对于esp的索引,所以我们需要进行调试,将断点下在call处,查看esp,ebp。
这里esp的地址为0xffffd510
,ebp的地址为0xffffd598
,s相对esp地址的索引为esp+0x80h+s
,并且S的地址为-0x64
,所以这里的索引为esp+0x1c
,所以我们可以推断:
- s的地址为
0xffffd52c
- s相对于ebp的偏移
0x6c
- s相对返回地址的偏移为
0x6c+4
编写pyload
1 | #!/usr/bin/env python3 |
成功打通。
ret2shellcode
原理:
ret2shellcode,即控制程序执行shellcode代码。shellcode值得是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell。通常情况下,shellcode需要我们自行编写,即此时我们需要自行向内存中填充一些可执行的代码。
在栈溢出的基础上,要想执行shellcode,需要在对应binary在运行时,shellcode所在的区域具有可执行权限。
需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的ret2shellcode手法不再能直接完成利用。
ret2syscall
原理:
控制程序执行系统调用,获取shell
例子:
使用checksec先查保护
32位程序并开启了nx保护。接下来拖到ida中进行分析。
f5反编译,审计,发现存在有gets危险函数,是一个栈溢出漏洞。
继续分析:v4相对于esp的索引为0x1c
,通过gdb调试计算发现,v4相对ebp的偏移为0x6c
,v4的返回地址为0x6c+4
。
由于程序中不存在直接的后门函数,所以我们不能直接利用程序中的某一段代码或者自己填写代码来获得shell,所以我们利用程序中的gadgets来获取shell,而对应的shell获取则是利用系统调用。
系统调用(system call):指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。这些服务可能包含访问文件系统、创建和销毁进程、进程间通信和内存分配。系统调用在进程和作系统之间提供了一个重要的接口。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态进行。
操作系统的进程空间可分为用户空间和内核空间,它们需要不同的执行权限。其中系统调用运行在内核空间。
系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核核心态,而普通的库函数调用由库函数库或用户自己提供,运行于用户态。
linux在x86上的系统调用通过int 80h
实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(api)
- API将系统调用号存入eax,然后通过中断调用使系统进入内核态
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
- 系统调用完成相应功能,将返回值存入eax,返回到中断处理函数
- 中断处理函数返回到api中
- api将eax返回给应用程序。
应用程序调用系统调用的过程是:
- 把系统调用的编号存入eax
- 把函数参数存入其他通用寄存器
- 触发
0x80
号中断(int 0x80
)
所以,只要我们把对应获取shell的系统调用的参数放到对应的寄存器中,那么我们在执行int 0x80
就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取shell:
execve("/bin/sh", NULL, NULL)
其中,该程序为32位,所以我们需要使得:
- 系统调用号,即eax为0xb
- 第一个参数,即ebx应该指向
/bin/sh
,其实执行sh的地址也可以。 - 第二个参数,即ecx应为0
- 第三个参数,即edx应为0
我们如何控制这些寄存器的值,就需要使用到gadgets。比如说,现在栈顶是10,那么如果此时执行了pop eax,那么现在eax的值为10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在gadgets最后使用ret来再次控制程序执行流程的原因。具体寻找gadgets的方法,我们可以使用ropgadgets这个工具。
首先,我们来寻找控制eax的gadgets。
可以看到有上述几个都可以控制eax,选取第二个来做为gadgets。
类似的,我们可以得到控制其他寄存器的gadgets。
这里选择
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
因为这个可以直接控制其他三个寄存器。
此外,我们需要获得/bin/sh字符串的地址:
地址为:0x080be408
还有int 0x80
的地址:
地址为0x08049421
接下来,我们进行exp的编写,其中oxb为execve对应的系统调用号。
1 | #!/usr/bin/env python3 |
成功拿到shell
ret2libc
原理:
ret2libc即控制函数的执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容),一般情况下,我们会选择执行system("/bin/sh")
,故而此时我们需要知道system函数的地址。
plt、got表
plt表(Procedure Linkage Table,过程链接表):是linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。
got表(Global Offset Table,全局偏移表):是linux ELF文件中用于定位全局变量和函数的一个表。
在第一次调用函数的时候,程序会首先跳转到plt表中,然后再跳转到got表中,因为函数是第一次调用,就会跳转回到plt表中,执行patch ***@got
,将函数的真实地址填充到got,然后执行代码,当下一次再调用函数时,plt->got->地址
got、plt表介绍
例子:
老规矩,拿到题目先查保护:
程序为32位,并且开启了NX保护。使用ida进行反编译
gets危险函数,标准的栈溢出漏洞。利用ROPgadget查找/bin/sh
是否存在。
接下来,我们查找system函数是否存在:
接下来,我们可以直接返回到该地址,执行system函数。
编写exp
1 | #!/usr/bin/env python3 |
成功打通,这里需要注意的是,如果是正常调用system函数,会有一个对应的返回地址,这里使用bbbb
作为虚假的地址,其后参数对应的参数内容。
例二:
拿到题目,首先使用checksec查保护:
可以看到,这里依然只开启了堆栈不可执行保护。接下,我们使用ida反编译它:
标准的gets()函数,标准的栈溢出漏洞。我们使用ROPgadget
来查找是否存在/bin/sh
字符串:
那么我们这里就需要自己读取字符串,然后控制程序执行system('/bin/sh')
。
首先,找到gets函数的plt表地址0x08048460
、然后,再找到system的plt表地址0x08048490
。还需要一个gadget
来读取字符串/bin/sh
,我们使用ROPgadget
来查找控制ebx寄存器的gadget
。
我们选择第二个:0x0804843d : pop ebx ; ret
,并且,由于程序中并不存在一个后门字符串/bin/sh
,所以我们需要一个gadget
段来写入该字符串,也就是覆盖bss
段上某个变量的内容。这里我们选择buf2
,地址:0x0804A080
。
接下来我们就可以编写我们的exp了
1 | #!/usr/bin/env python3 |
成功打通
例三:
拿到题目之后查保护:
发现开启了栈不可执行保护,接着我们使用ida进行反编译
gets栈溢出漏洞,再寻找,本题中并没有给出system函数,那么如何获取到system函数的地址呢,这里主要利用两个知识点:
- system属于libc,而libc.so动态链接库中的函数之间相对偏移是固定的。
- 即使程序有ASLR保护,也只是针对地址中间位进行随机,最低的12位并不会发生改变,而libc在github上有人进行收集,libc-database
所以如果我们知道了libc中某个函数的地址,那么我们就可以确定该程序利用的libc。进而我们就可以知道system中的地址。
那么如何得到libc中的某个函数的地址,一般常用的方法是采用got表泄露,即输出某个函数对应的got表项的内容。当然,由于libc的延迟绑定机制,我们需要泄露已经执行过的函数的地址。
这里使用libc的利用工具LibcSearch,而且,在得到libc之后,其实libc中也是有/bin/sh字符串,所以我们可以一起获得/bin/sh字符串的地址。
这里我们泄露__libc_start_main
的地址,这时因为它是程序最初被执行的地方。基本利用思路如下:
- 泄露__libc_start_main地址
- 获取libc版本
- 获取system地址与/bin/sh的地址
- 再次执行源程序
- 触发栈溢出执行system(‘/bin/sh’)
ret2csu
原理
在64位程序中,函数的前6个参数是通过寄存器传递的,但是大多时候,我们很难找到每一个寄存器对应的gadgets。这时候,我们可以利用x64下的__libc_csu_init
中的gadgets。这个函数是用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定会存在。我们先来看一下这个函数(不同版本的这个函数有一定的区别)。
1 | .text:00000000004005C0 ; void _libc_csu_init(void) |
这里我们可以利用以下几点
- 从0x040061A一直到结尾,我们可以利用栈溢出构造栈上数据来控制
rbx,rbp,r12,r13,r14,r15
寄存器的数据。 - 从0x0400600到0x0400609,我们可以将r13赋给rdx,将r14赋给rsi,将r15d赋给edi(注意,虽然这里赋给的是edi,但其实此时rdi的高32位寄存器值为0(自行调试),所以其实我们可以控制rdi寄存器的值,只不过只能控制低32位),而这三个寄存器,也是x64函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制r12与rbx,那么我们就可以调用我们想要调用的函数。比如说可以控制rbx为0,r12为存储我们想要调用的函数地址。
- 从0x040060D到0x0400614,我们可以控制rbx与rbp的之间的关系为$rbx+1=rbp$,这样我们就不会执行loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。
练习
pwn1_sctf_2016
打开运行一下,发现是将输入的语句重新打印出来,猜测可能是格式化字符串或者缓冲区溢出,使用checksec查一下保护,发现是32位程序,并且只开了栈不可执行保护。
使用ida32位打开,进入vuln()函数中,进行分析,反汇编出来是使用c++编写的:
1 | std::string *__stdcall replace(std::string *a1, std::string *a2, std::string *a3) |
跟着分析一下,程序在干什么。
首先打印一句话,然后使用fgets()函数从 edata(通常是一个 FILE* 指针)中读取最多 31 个字符(+1 个 \0 作为结尾)到 s 所指向的缓冲区中。然后将s的值传递给string类型的input中。v4被初始化为"you",v6被初始化为"I"。这里发现了一个replace函数,双击进去发现是一个自定义函数,我附到上述去,继续分析。该代码的作用是将传入的参数中进行查找并替换的一个功能。接下来将v3,v6,v4的值传递给input,最后,使用strcpy函数将v0的值复制到s,然后输出打印出来。
程序的运行逻辑大概就是这样。
因为初学,所以还没有看出来有什么漏洞,上网查了下,发现strcpy()函数是一个高危函数,该函数可能会造成栈溢出漏洞:
使用ida查看s到栈的距离为0x03C
,再加上4为返回地址。
shift+f12
查看,存在cat flag.txt
的命令,所以我们需要覆盖的地址为这个地址。
为0x08048F0D
接下来我们只要将栈上地址覆盖到getflag函数地址即可,但是注意,程序中有
fgets(s, 32, edata);
函数限制了输入,只能输入31个字符,这对于我们要覆盖栈是远远不够的,但是,程序中会将I
转换成you
,这就给了我们思路。
我们在覆盖栈时需要0x3c + 4 = 64
字节的数据,所以只需要21个I在加一个a
即可覆盖到返回地址为getflag函数执行地址。
现在我们开始编写exp
1 | #!/usr/bin/env python3 |
我们在本地创建flag.txt
,运行
ok,成功,接下来连接远程地址。
成功获取flag。