論文:Return-Oriented Programming without Returns
X86上的ULB
舉例:
pop %eax;
jump *%eax
其中钢拧,X是任何通用寄存器澈驼。由于x86 ISA的臨時(shí)特性有送,pop-jump序列作為非預(yù)期的指令以某種頻率出現(xiàn)行拢。
- 用另一個(gè)通用寄存器代替eax(esp除外)
- 使用雙重間接跳轉(zhuǎn)而不是單獨(dú)間接跳轉(zhuǎn)
- 使用具有立即偏移的雙重間接跳轉(zhuǎn)旭愧,通過(guò)添加或減去序列地址來(lái)抵消立即偏移的影響
- X86提供了兩種雙重間接跳轉(zhuǎn):near jump(在當(dāng)前段中采用32位地址)和far jump(采用32位地址和16位段選擇器)
其他可能的ULB指令序列:
- 基于調(diào)用X的序列辽狈,會(huì)在每次使用時(shí)減少esp
- 使用與esp不同的寄存器
例如
add 0x4,%eax;
jmp(%eax); - 使用SIB尋址,可使用寄存器組合秀鞭,索引寄存器在每次引用后加4
- 內(nèi)存位置作為可變狀態(tài)而非寄存器
由于存在多種多樣的可能構(gòu)造ROP攻擊的指令序列趋观,進(jìn)行不全面檢測(cè)的防御方式效果明顯有限。攻擊者很可能切換到不同的返回類序列從而逃避檢測(cè)锋边。
選擇Gadgets
來(lái)源:
Debian的libc本身
Mozilla的libxul
PHP的libphp5
編譯器是否可以修改,從而避免發(fā)出pop-jump序列编曼?
不能豆巨。
使用以jmp y結(jié)尾的指令序列,其中y是指向pop x的指針掐场。pop x;jump x不在libc中往扔,就必須存在于目標(biāo)程序或它的一個(gè)庫(kù)中。我們稱其為BYOPJ范例(bring your own pop-jump)熊户。
libc在每個(gè)Linux程序中都會(huì)被加載萍膛,我們可以使用它作為gadgets的語(yǔ)料庫(kù)。但很大程度地使用libc中的指令序列而不使用libxul中的pop-jump序列不是一個(gè)真正的攻擊者會(huì)使用的攻擊方法嚷堡。
大多可用的指令序列以近間接跳轉(zhuǎn)或遠(yuǎn)間接跳轉(zhuǎn)結(jié)束蝗罗,跳轉(zhuǎn)的地址在內(nèi)存中被存儲(chǔ)在存儲(chǔ)器edx中。即很多可用的指令序列是以 jmp *(%edx) 或 ljmp *(%edx) 結(jié)束蝌戒。為了將這些gadgets鏈接起來(lái)串塑,我們要確保每個(gè)gadget的結(jié)尾都能夠保存 pop *X; jmp *X 序列目錄條目的地址。大多情況下北苟,我們只需要修改函數(shù)調(diào)用gadget即可桩匪。
本文的設(shè)計(jì):
三地址碼的內(nèi)存-內(nèi)存gadget集。格式為:
x<—y op z
其中x友鼻,y和z處于內(nèi)存中隨意位置傻昙,包含了操作數(shù)和目標(biāo)地址。
我們?cè)O(shè)計(jì)了一個(gè)三地址代碼內(nèi)存 - 內(nèi)存小工具集:我們的小工具的形式是x yopz彩扔,其中x妆档,y和z是內(nèi)存中包含操作數(shù)和目標(biāo)的文字位置。 我們使用寄存器edx來(lái)鏈接我們的指令序列; 對(duì)于我們BYOPJ范例中的pop xjmp x序列借杰,我們使用寄存器ebx过吻。 這意味著我們不能在寄存器ebx中存儲(chǔ)任何狀態(tài),但我們不必?fù)?dān)心在指令序列過(guò)程中更改其內(nèi)容,因?yàn)樗鼘⒃趐op%ebx期間被覆蓋纤虽。 這給我們留下了五個(gè)寄存器乳绕,eax,ecx逼纸,ebp洋措,esi和edi,可以隨意使用杰刽。
指令序列
我們使用了34個(gè)不同的以jmp x結(jié)尾的指令序列菠发,來(lái)構(gòu)造19個(gè)通用的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
這些指令序列大多包含四個(gè)或更少的指令。這些序列是從libc的潛在指令序列集合(由Shacham的算法提供)中選出的贺嫂。
數(shù)據(jù)的移動(dòng)
gadgets需要能夠在存儲(chǔ)器和寄存器之間以及多個(gè)寄存器之間移動(dòng)數(shù)據(jù)滓鸠。方法如下:
- 將數(shù)據(jù)從存儲(chǔ)器移入寄存器:mov n(x),y??
- 將數(shù)據(jù)從寄存器移入存儲(chǔ)器:mov y第喳,n(x)
- 寄存器之間的移動(dòng)有兩種方法:
a) 使用xchg指令交換兩個(gè)寄存器的內(nèi)容
b) 將目的地址寄存器設(shè)置為0x00000000或0xffffffff糜俗,可以對(duì)源地址寄存器和目的地址寄存器進(jìn)行or或and運(yùn)算,從而實(shí)現(xiàn)移動(dòng)
將固定地址處的值加載到內(nèi)存中:
加載具有立即值的esi和具有固定地址的eax加上0xb來(lái)輕松實(shí)現(xiàn)曲饱。這需要兩個(gè)pop操作悠抹。然后我們使用mov%esi,-0xb(%eax)將立即值寫入內(nèi)存扩淀。
Move Gadget:
將源地址加載到eax楔敌,將目標(biāo)地址加載到ebp,從eax加載到edi驻谆,最后將edi存儲(chǔ)到ebp中的地址的內(nèi)存中卵凑。
Load Gadget:
不將內(nèi)存中源地址中的字存儲(chǔ)在目標(biāo)地址處,而是用作指向存儲(chǔ)器中另一個(gè)字的指針旺韭,該字加載到另一個(gè)寄存器中氛谜,然后存儲(chǔ)在目標(biāo)地址中。偽代碼:
eax <— source
edi <— (eax)
esi <— (edi)
eax <— destination
(eax) <— esi
Store Gadget:
執(zhí)行操作(A)B区端,其中A是目的地址處存儲(chǔ)的字值漫,B是源地址處存儲(chǔ)的字。實(shí)際上织盼,我們可以執(zhí)行操作(A + n)B杨何,其中n是字面值。這使常量數(shù)組容易索引到在內(nèi)存中位置不固定的值沥邻,其中A是數(shù)組基數(shù)危虱,n是偏移量。
算術(shù)運(yùn)算
add唐全,add immediate和subtract的gadget是比較容易實(shí)現(xiàn)的埃跷。它們將源操作數(shù)加載到寄存器中蕊玷,執(zhí)行適當(dāng)?shù)牟僮鳎缓髮⒔Y(jié)果存回內(nèi)存弥雹。 x86 ISA允許其中一個(gè)操作數(shù)成為內(nèi)存中的一個(gè)位置垃帅,這樣就不需要加載其中的一個(gè)操作數(shù)。這簡(jiǎn)化了gadgets剪勿。
Negate gadget從源地址加載數(shù)據(jù)贸诚,取雙字的補(bǔ)碼,并將其存儲(chǔ)回內(nèi)存厕吉。執(zhí)行寄存器的二進(jìn)制補(bǔ)碼的neg指令不會(huì)出現(xiàn)在jmp x指令附近;我們使用xor%esi酱固,%esi使esi為零,然后使用序列:subl -0x7D(%ebp头朱,%ecx)运悲,%esi; jmp(%ecx),從零中減去該值髓窜。 subl指令執(zhí)行操作esi esi(ebp + ecx 0x7D)扇苞。由于我們的jmp x使用ecx,我們必須用指向pop x; jmp x序列的指針地址加載它寄纵。這意味著ebp必須具有源地址加上0x7D再減去pop x; jmp x指針的地址的值
。
邏輯運(yùn)算
and脖苏,and immediate程拭,or,or immediate 的gadgets構(gòu)造方式與add gadget類似棍潘。即恃鞋,將操作數(shù)加載到寄存器中,執(zhí)行操作亦歉,并將結(jié)果存回內(nèi)存恤浪。唯一棘手的部分是如上所述的寄存器之間的數(shù)據(jù)移動(dòng)。
xor和xor immediate 的gadgets類似肴楷,它不是將兩個(gè)寄存器的值進(jìn)行xor然后將結(jié)果存儲(chǔ)回內(nèi)存水由,而是將第一個(gè)源字寫入目標(biāo)地址,然后使用第二個(gè)源字xor該地址赛蔫。
Complement gadget 計(jì)算源值的一個(gè)補(bǔ)碼并將其存儲(chǔ)到目標(biāo)地址中砂客。類似于negate gadget的情況,有一個(gè)不執(zhí)行補(bǔ)碼運(yùn)算的x86指令沒(méi)有出現(xiàn)在libc中的有用的指令序列中呵恢。我們完全按照nagate gadget進(jìn)行操作鞠值,除了不使用零加載esi而使用0xffffffff = -1加載它。這是因?yàn)?1-x =非x渗钉。
分支Branching
- 絕對(duì)地址實(shí)現(xiàn)分支
- 相對(duì)地址實(shí)現(xiàn)分支
在正常程序中彤恶,分支可以是絕對(duì)地址,也可以是相對(duì)于當(dāng)前指令的地址。 在面向返回的編程中声离,通過(guò)更改堆棧指針esp而非指令指針eip來(lái)執(zhí)行分支芒炼。 我們可以通過(guò)將堆棧中的值彈出到esp中來(lái)實(shí)現(xiàn)絕對(duì)分支。 或者抵恋,可將小工具末端的負(fù)偏移彈出到edi中焕议,然后使用序列sub %edi, %esp; ljmp(%eax)從堆棧指針中減去edi。這允許堆棧指針實(shí)現(xiàn)相對(duì)分支弧关。 這是branch unconditional gadget的基礎(chǔ)盅安。
條件分支gadget:
為了獲得Turing-complete行為,我們必須有辦法執(zhí)行條件分支世囊。 我們需要一種方法通過(guò)存儲(chǔ)在已知地址的內(nèi)容來(lái)改變堆棧指針别瞭。 如果字的值為零,那么我們不會(huì)更改堆棧指針株憾。 如果字是0xffffffff蝙寨,那么像branch unconditional 的情況一樣,減去堆棧指針的偏移量嗤瞎。 實(shí)現(xiàn)的方法是將字的值加載到寄存器中并與偏移量相與墙歪,再?gòu)亩褩V羔樦袦p去該結(jié)果。 該實(shí)現(xiàn)是and gadget 和 branch unconditional gadget的組合贝奇,即branch conditional gadget虹菲。
比較的實(shí)現(xiàn):
如果第一個(gè)值小于第二個(gè)值,則將目標(biāo)地址處的字設(shè)置為0xffffffff掉瞳。
圖中給出了實(shí)現(xiàn)小于判斷的gadget實(shí)現(xiàn)毕源。字符串比較指令cmpsl比較%ds:%esi和%es:%edi指向的兩個(gè)值,如果后者大于前者陕习,便設(shè)置進(jìn)位標(biāo)志霎褐。同時(shí),寄存器esi和edi的值將根據(jù)方向標(biāo)志來(lái)增加或減少该镣。然而冻璃,這并不重要,因?yàn)槲覀冎皇潜容^一個(gè)字值拌牲。 sbb指令從esi中減去esi加上進(jìn)位標(biāo)志的值俱饿。實(shí)質(zhì)上,如果第一個(gè)源值小于第二個(gè)源值塌忽,那么將設(shè)??置進(jìn)位標(biāo)志拍埠,并將esi設(shè)置為0xffffffff;否則土居,將不設(shè)置進(jìn)位標(biāo)志枣购,esi將設(shè)置為零嬉探,正如branch conditional gadget所需。必須要注意的一件事是:寄存器cl不能為零棉圈,否則會(huì)發(fā)生除零異常涩堤。
利用Set Less Than Gadgets和Logical Gadgets,可以形成基于比較兩個(gè)值的六種關(guān)系(<分瘾,<=胎围,=,!=,>=和>)中的任何一種的條件分支德召。在這一點(diǎn)上白魂,我們的gadgets是圖靈完備的。
函數(shù)調(diào)用
添加一個(gè)執(zhí)行函數(shù)調(diào)用的gadget上岗,從而擴(kuò)展已擁有的gadgets集的功能福荸。
執(zhí)行函數(shù)調(diào)用的gadget能夠讓我們調(diào)用正常的面向返回的指令序列(即以return結(jié)束的指令序列),或允許我們調(diào)用合法的函數(shù)肴掷。其中敬锐,后者的操作更復(fù)雜。
這種方法可以避免兩種檢測(cè):
- 依賴于函數(shù)調(diào)用堆棧的LIFO特性的ROP防御方法
- 依賴于return指令頻率的防御方法
在進(jìn)行函數(shù)調(diào)用之前呆瞻,必須將堆棧指針指向新位置台夺,以防止覆蓋堆棧中的先前的gadgets。如果n是函數(shù)開(kāi)始執(zhí)行時(shí)堆棧指針?biāo)诘牡刂?- 即將存儲(chǔ)返回地址的位置 - 則k個(gè)參數(shù)應(yīng)存儲(chǔ)在地址n + 4痴脾,n + 8谒养,...,n + 4k種明郭。這可以使用load immediate gadget或move gadget來(lái)完成。接著丰泊,函數(shù)調(diào)用gadget將用于計(jì)算 A fun(arg1; arg2; ::::; argk)薯定,其中堆棧指針為n。
由于x86的Linux應(yīng)用程序二進(jìn)制接口(ABI)指定寄存器eax瞳购,ecx和edx被調(diào)用者保存话侄,因此如果被調(diào)用的函數(shù)覆蓋了這些寄存器,我們必須注意不要混淆面向返回的代碼学赛。 這種情況下年堆,一個(gè)棘手的問(wèn)題是:由于edx被調(diào)用者保存,一旦我們要執(zhí)行return盏浇,我們需要將edx恢復(fù)到指向pop x; jmp x的指針的地址变丧。 如果我們關(guān)心eax中的返回值,我們不能僅使用libc中的指令序列绢掰。 繼續(xù)我們的BYOPJ范例痒蓬,如果目標(biāo)程序有一個(gè)pop%edx; jmp(%edx)或pop%edx; jmp(%esi)童擎,那么我們就可以恢復(fù)edx而不覆蓋eax中的返回值。 Mozilla的libxul有這樣一個(gè)序列攻晒。 如果沒(méi)有這樣的序列顾复,函數(shù)調(diào)用的gadget必須針對(duì)每個(gè)應(yīng)用程序進(jìn)行定制,而不能通用鲁捏。
函數(shù)調(diào)用gadget的具體實(shí)現(xiàn)如下:
- 它首先做的是加載寄存器esi芯砸,ebp和eax。
寄存器esi加載了call-jump序列的序列目錄表的開(kāi)始地址给梅,ebp加載了leave-jump序列的實(shí)際地址假丧,eax加載了字值n(加上存儲(chǔ)序列的偏移量)。 - 接下來(lái)破喻,將call-jump的序列目錄表的開(kāi)始地址存儲(chǔ)在地址n虎谢。
- 然后,寄存器esi加載0x38曹质,并加上堆棧指針的值婴噩。
此時(shí),esi保存著函數(shù)調(diào)用返回后堆棧指針的地址羽德。 - 將返回地址移動(dòng)到ebp中
既然已經(jīng)知道函數(shù)返回后在堆棧上的位置几莽,我們需要將它移動(dòng)到ebp中。
最簡(jiǎn)單的方法是將它存儲(chǔ)到內(nèi)存中(存儲(chǔ)在我們最終存儲(chǔ)函數(shù)的返回值的地方)宅静,再將其從內(nèi)存加載回edi章蚣,然后與ebp交換。
交換之后姨夹,edi保存著leave-jump序列的地址纤垂,ebp保存著函數(shù)調(diào)用之后堆棧指針的值。 - 接下來(lái)磷账,將pop x; jmp x;的序列目錄表的開(kāi)始地址加載到esi峭沦,將存儲(chǔ)函數(shù)指針的地址加載到ecx(加上偏移量),并將值n加載到eax逃糟。交換寄存器esp和eax的值吼鱼,使堆棧指針為n。
- pop ebp實(shí)現(xiàn)鏈接
回想一下绰咽,函數(shù)調(diào)用gadget所做的第一件事就是將call-jump序列目錄表的開(kāi)始地址存儲(chǔ)到n菇肃。此時(shí),函數(shù)的間接調(diào)用發(fā)生了取募。函數(shù)返回之后琐谤,當(dāng)eax保存返回值時(shí),我們不能依賴寄存器ecx或edx中的值矛辕。但是笑跛,edi保存了leave-jump序列的地址付魔,因此jmp %edi指令會(huì)導(dǎo)致執(zhí)行l(wèi)eave指令,將堆棧指針存入ebp - 但ebp還保存著使用第一個(gè)xchg指令時(shí)設(shè)置的地址 - 然后將堆棧頂部的值彈出到ebp中飞蹂。這導(dǎo)致pop x; jmp x序列目錄表的開(kāi)始地址(加上一個(gè)偏移量)被加載到ebp中几苍,使得后續(xù)的jmp -0x7d(%ebp)指令能夠鏈接下一個(gè)指令序列。 -
返回的兩種方法
我們現(xiàn)在有兩種實(shí)現(xiàn)返回的方法陈哑。
當(dāng)沒(méi)有pop%edx; jmp(%edx)序列時(shí)妻坝,我們使用popad; jmp(%edx),失去了返回值惊窖。這種情況下刽宪,函數(shù)調(diào)用gadget仍是完整的。
如果我們有一個(gè)pop%edx; jmp(%edx)序列界酒,執(zhí)行它即可圣拄,然后將eax中的返回值存儲(chǔ)到內(nèi)存中。
后一種形式的gadget如圖毁欣。