ROP without returns on Intel X86

论文:Return-Oriented Programming without Returns

X86上的ULB

举例:
pop %eax;
jump *%eax
其中,X是任何通用寄存器。由于x86 ISA的临时特性,pop-jump序列作为非预期的指令以某种频率出现。

  • 用另一个通用寄存器代替eax(esp除外)
  • 使用双重间接跳转而不是单独间接跳转
  • 使用具有立即偏移的双重间接跳转,通过添加或减去序列地址来抵消立即偏移的影响
  • X86提供了两种双重间接跳转:near jump(在当前段中采用32位地址)和far jump(采用32位地址和16位段选择器)

其他可能的ULB指令序列:

  • 基于调用X的序列,会在每次使用时减少esp
  • 使用与esp不同的寄存器
    例如
    add 0x4,%eax;
    jmp(%eax);
  • 使用SIB寻址,可使用寄存器组合,索引寄存器在每次引用后加4
  • 内存位置作为可变状态而非寄存器

由于存在多种多样的可能构造ROP攻击的指令序列,进行不全面检测的防御方式效果明显有限。攻击者很可能切换到不同的返回类序列从而逃避检测。

选择Gadgets

来源:
Debian的libc本身
Mozilla的libxul
PHP的libphp5

编译器是否可以修改,从而避免发出pop-jump序列?
不能。

使用以jmp y结尾的指令序列,其中y是指向pop x的指针。pop x;jump x不在libc中,就必须存在于目标程序或它的一个库中。我们称其为BYOPJ范例(bring your own pop-jump)。

libc在每个Linux程序中都会被加载,我们可以使用它作为gadgets的语料库。但很大程度地使用libc中的指令序列而不使用libxul中的pop-jump序列不是一个真正的攻击者会使用的攻击方法。

大多可用的指令序列以近间接跳转或远间接跳转结束,跳转的地址在内存中被存储在存储器edx中。即很多可用的指令序列是以 jmp *(%edx) 或 ljmp *(%edx) 结束。为了将这些gadgets链接起来,我们要确保每个gadget的结尾都能够保存 pop *X; jmp *X 序列目录条目的地址。大多情况下,我们只需要修改函数调用gadget即可。

本文的设计:
三地址码的内存-内存gadget集。格式为:

x<—y op z
其中x,y和z处于内存中随意位置,包含了操作数和目标地址。

我们设计了一个三地址代码内存 - 内存小工具集:我们的小工具的形式是x yopz,其中x,y和z是内存中包含操作数和目标的文字位置。 我们使用寄存器edx来链接我们的指令序列; 对于我们BYOPJ范例中的pop xjmp x序列,我们使用寄存器ebx。 这意味着我们不能在寄存器ebx中存储任何状态,但我们不必担心在指令序列过程中更改其内容,因为它将在pop%ebx期间被覆盖。 这给我们留下了五个寄存器,eax,ecx,ebp,esi和edi,可以随意使用。

指令序列

我们使用了34个不同的以jmp x结尾的指令序列,来构造19个通用的gadgets:
load immediate,
move
load
store
add
add immediate
subtract
negate
and
and immediate
or
or immediate
xor
xor immediate
complement
branch uncon-ditional
branch conditional
set less than
function call
这些指令序列大多包含四个或更少的指令。这些序列是从libc的潜在指令序列集合(由Shacham的算法提供)中选出的。

数据的移动

gadgets需要能够在存储器和寄存器之间以及多个寄存器之间移动数据。方法如下:

  1. 将数据从存储器移入寄存器:mov n(x),y​​
  2. 将数据从寄存器移入存储器:mov y,n(x)
  3. 寄存器之间的移动有两种方法:
    a) 使用xchg指令交换两个寄存器的内容
    b) 将目的地址寄存器设置为0x00000000或0xffffffff,可以对源地址寄存器和目的地址寄存器进行or或and运算,从而实现移动

将固定地址处的值加载到内存中:
加载具有立即值的esi和具有固定地址的eax加上0xb来轻松实现。这需要两个pop操作。然后我们使用mov%esi,-0xb(%eax)将立即值写入内存。

Move Gadget:
将源地址加载到eax,将目标地址加载到ebp,从eax加载到edi,最后将edi存储到ebp中的地址的内存中。

Load Gadget:
不将内存中源地址中的字存储在目标地址处,而是用作指向存储器中另一个字的指针,该字加载到另一个寄存器中,然后存储在目标地址中。伪代码:

eax <— source
edi <— (eax)
esi <— (edi)
eax <— destination
(eax) <— esi

Store Gadget:
执行操作(A)B,其中A是目的地址处存储的字,B是源地址处存储的字。实际上,我们可以执行操作(A + n)B,其中n是字面值。这使常量数组容易索引到在内存中位置不固定的值,其中A是数组基数,n是偏移量。

算术运算

add,add immediate和subtract的gadget是比较容易实现的。它们将源操作数加载到寄存器中,执行适当的操作,然后将结果存回内存。 x86 ISA允许其中一个操作数成为内存中的一个位置,这样就不需要加载其中的一个操作数。这简化了gadgets。

Negate gadget从源地址加载数据,取双字的补码,并将其存储回内存。执行寄存器的二进制补码的neg指令不会出现在jmp x指令附近;我们使用xor%esi,%esi使esi为零,然后使用序列:subl -0x7D(%ebp,%ecx),%esi; jmp(%ecx),从零中减去该值。 subl指令执行操作esi esi(ebp + ecx 0x7D)。由于我们的jmp x使用ecx,我们必须用指向pop x; jmp x序列的指针地址加载它。这意味着ebp必须具有源地址加上0x7D再减去pop x; jmp x指针的地址的值

逻辑运算

and,and immediate,or,or immediate 的gadgets构造方式与add gadget类似。即,将操作数加载到寄存器中,执行操作,并将结果存回内存。唯一棘手的部分是如上所述的寄存器之间的数据移动。

xor和xor immediate 的gadgets类似,它不是将两个寄存器的值进行xor然后将结果存储回内存,而是将第一个源字写入目标地址,然后使用第二个源字xor该地址。

Complement gadget 计算源值的一个补码并将其存储到目标地址中。类似于negate gadget的情况,有一个不执行补码运算的x86指令没有出现在libc中的有用的指令序列中。我们完全按照nagate gadget进行操作,除了不使用零加载esi而使用0xffffffff = -1加载它。这是因为-1-x =非x。

分支Branching

  • 绝对地址实现分支
  • 相对地址实现分支

在正常程序中,分支可以是绝对地址,也可以是相对于当前指令的地址。 在面向返回的编程中,通过更改堆栈指针esp而非指令指针eip来执行分支。 我们可以通过将堆栈中的值弹出到esp中来实现绝对分支。 或者,可将小工具末端的负偏移弹出到edi中,然后使用序列sub %edi, %esp; ljmp(%eax)从堆栈指针中减去edi。这允许堆栈指针实现相对分支。 这是branch unconditional gadget的基础。

条件分支gadget:

为了获得Turing-complete行为,我们必须有办法执行条件分支。 我们需要一种方法通过存储在已知地址的内容来改变堆栈指针。 如果字的值为零,那么我们不会更改堆栈指针。 如果字是0xffffffff,那么像branch unconditional 的情况一样,减去堆栈指针的偏移量。 实现的方法是将字的值加载到寄存器中并与偏移量相与,再从堆栈指针中减去该结果。 该实现是and gadget 和 branch unconditional gadget的组合,即branch conditional gadget。

比较的实现:

如果第一个值小于第二个值,则将目标地址处的字设置为0xffffffff。


Set Less Than Gadget

图中给出了实现小于判断的gadget实现。字符串比较指令cmpsl比较%ds:%esi和%es:%edi指向的两个值,如果后者大于前者,便设置进位标志。同时,寄存器esi和edi的值将根据方向标志来增加或减少。然而,这并不重要,因为我们只是比较一个字值。 sbb指令从esi中减去esi加上进位标志的值。实质上,如果第一个源值小于第二个源值,那么将设​​置进位标志,并将esi设置为0xffffffff;否则,将不设置进位标志,esi将设置为零,正如branch conditional gadget所需。必须要注意的一件事是:寄存器cl不能为零,否则会发生除零异常。

利用Set Less Than Gadgets和Logical Gadgets,可以形成基于比较两个值的六种关系(<,<=,=,!=,>=和>)中的任何一种的条件分支。在这一点上,我们的gadgets是图灵完备的。

函数调用

添加一个执行函数调用的gadget,从而扩展已拥有的gadgets集的功能。
执行函数调用的gadget能够让我们调用正常的面向返回的指令序列(即以return结束的指令序列),或允许我们调用合法的函数。其中,后者的操作更复杂。
这种方法可以避免两种检测:

  • 依赖于函数调用堆栈的LIFO特性的ROP防御方法
  • 依赖于return指令频率的防御方法

在进行函数调用之前,必须将堆栈指针指向新位置,以防止覆盖堆栈中的先前的gadgets。如果n是函数开始执行时堆栈指针所在的地址 - 即将存储返回地址的位置 - 则k个参数应存储在地址n + 4,n + 8,...,n + 4k种。这可以使用load immediate gadget或move gadget来完成。接着,函数调用gadget将用于计算 A fun(arg1; arg2; ::::; argk),其中堆栈指针为n。

由于x86的Linux应用程序二进制接口(ABI)指定寄存器eax,ecx和edx被调用者保存,因此如果被调用的函数覆盖了这些寄存器,我们必须注意不要混淆面向返回的代码。 这种情况下,一个棘手的问题是:由于edx被调用者保存,一旦我们要执行return,我们需要将edx恢复到指向pop x; jmp x的指针的地址。 如果我们关心eax中的返回值,我们不能仅使用libc中的指令序列。 继续我们的BYOPJ范例,如果目标程序有一个pop%edx; jmp(%edx)或pop%edx; jmp(%esi),那么我们就可以恢复edx而不覆盖eax中的返回值。 Mozilla的libxul有这样一个序列。 如果没有这样的序列,函数调用的gadget必须针对每个应用程序进行定制,而不能通用。

函数调用gadget的具体实现如下:

  1. 它首先做的是加载寄存器esi,ebp和eax。
    寄存器esi加载了call-jump序列的序列目录表的开始地址,ebp加载了leave-jump序列的实际地址,eax加载了字值n(加上存储序列的偏移量)。
  2. 接下来,将call-jump的序列目录表的开始地址存储在地址n。
  3. 然后,寄存器esi加载0x38,并加上堆栈指针的值。
    此时,esi保存着函数调用返回后堆栈指针的地址。
  4. 将返回地址移动到ebp中
    既然已经知道函数返回后在堆栈上的位置,我们需要将它移动到ebp中。
    最简单的方法是将它存储到内存中(存储在我们最终存储函数的返回值的地方),再将其从内存加载回edi,然后与ebp交换。
    交换之后,edi保存着leave-jump序列的地址,ebp保存着函数调用之后堆栈指针的值。
  5. 接下来,将pop x; jmp x;的序列目录表的开始地址加载到esi,将存储函数指针的地址加载到ecx(加上偏移量),并将值n加载到eax。交换寄存器esp和eax的值,使堆栈指针为n。
  6. pop ebp实现链接
    回想一下,函数调用gadget所做的第一件事就是将call-jump序列目录表的开始地址存储到n。此时,函数的间接调用发生了。函数返回之后,当eax保存返回值时,我们不能依赖寄存器ecx或edx中的值。但是,edi保存了leave-jump序列的地址,因此jmp %edi指令会导致执行leave指令,将堆栈指针存入ebp - 但ebp还保存着使用第一个xchg指令时设置的地址 - 然后将堆栈顶部的值弹出到ebp中。这导致pop x; jmp x序列目录表的开始地址(加上一个偏移量)被加载到ebp中,使得后续的jmp -0x7d(%ebp)指令能够链接下一个指令序列。
  7. 返回的两种方法
    我们现在有两种实现返回的方法。
    当没有pop%edx; jmp(%edx)序列时,我们使用popad; jmp(%edx),失去了返回值。这种情况下,函数调用gadget仍是完整的。
    如果我们有一个pop%edx; jmp(%edx)序列,执行它即可,然后将eax中的返回值存储到内存中。
    后一种形式的gadget如图。


    function call gadget
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容

  • 原文地址:C语言函数调用栈(一)C语言函数调用栈(二) 0 引言 程序的执行过程可看作连续的函数调用。当一个函数执...
    小猪啊呜阅读 4,585评论 1 19
  • Return-Oriented-Programming(ROP FTW) Author: Saif El-Sher...
    RealSys阅读 3,294评论 0 2
  • 计算机系统漫游 代码从文本到可执行文件的过程(c语言示例):预处理阶段,处理 #inlcude , #defin...
    willdimagine阅读 3,557评论 0 5
  • 李大牛是喊着秦珍珍的名字咽下那最后一口气的。 “珍珍,等我······” 李华华拉着他的手,半边屁股挨着床沿,像怕...
    卷饼兔阅读 454评论 3 2
  • 接触 Tower.im 是很偶然的机会,试玩了一天聊下感受。 Tower.im是一款基于云端的项目管理工具,便于团...
    Nesier无恙阅读 6,312评论 2 51