作者
Silvio Cesare
編譯
nixe
n
摘要:本文討論了一個修改ELF文件實現共享庫調用重定向的方法
修改可執行文件的程序連接表(Procedure Linkage Table)可以使被感染的文件調用外部的函數
這要比修改LD_PRELOAD環境變量實現調用的重定向優越的多
首先不牽扯到環境變量的修改
其次是更為隱蔽
本文將提供一個基於x
/Linux的實現
前言
這是nixe
n搜集的一組有關Linux系統下病毒的研究文章
沒有先後的次序
文中的代碼可能有破壞性
只能用於研究
如果你用於非法目的
後果自負
感染ELF文件程序連接表實現共享庫調用的重定向
簡介
本文討論了一個修改ELF文件實現共享庫調用重定向的方法
修改可執行文件的程序連接表(Procedure Linkage Table)可以使被感染的文件調用外部的函數
這要比修改LD_PRELOAD環境變量實現調用的重定向優越的多
首先不牽扯到環境變量的修改
其次是更為隱蔽
本文將提供一個基於x
/Linux的實現
如果你對UNIX系統病毒比較感興趣請參考以下網址
(UNIX病毒郵件列表)
~silvio (作者主頁)
程序連接表(Procedure Linkage Table)
下面是ELF規范中
關於程序連接表的敘述
程序連接表(PLT)
在ELF文件中
全局偏移表(Global Offset Table
GOT)能夠把位置無關的地址定位到絕對地址
程序連接表也有類似的作用
它能夠把位置無關的函數調用定向到絕對地址
連接編輯器(link editor)不能解決程序從一個可執行文件或者共享庫目標到另外一個的執行轉移
結果
連接編輯器只能把包含程序轉移控制的一些入口安排到程序連接表(PLT)中
在system V體系中
程序連接表位於共享正文中
但是它們使用私有全局偏移表(private global offset table)中的地址
動態連接器(例如
ld
so)會決定目標的絕對地址並且修改全局偏移表在內存中的影象
因而
動態連接器能夠重定向這些入口
而勿需破壞程序正文的位置無關性和共享特性
可執行文件和共享目標文件有各自的程序連接表
表
使用絕對地址的程序連接表
PLT
:pushl got_plus_
jmp *got_plus_
nop; nop
nop; nop
PLT
:jmp *name
_in_GOT
pushl $offset
jmp
PLT
@PC
PLT
:jmp *name
_in_GOT
pushl $offset
jmp
PLT
@PC
表
位置無關的程序連接表
PLT
:pushl
(%ebx)
jmp *
(%ebx)
nop; nop
nop; nop
PLT
:jmp *name
@GOT(%ebx)
pushl $offset
jmp
PLT
@PC
PLT
:jmp *name
@GOT(%ebx)
pushl $offset
jmp
PLT
@PC
注意
從兩個表中可以看出
兩種方式的指令使用不同的操作數尋址模式
但是
它們和動態連接器的接口是一樣的
下一步
動態連接器和程序本身使用程序連接表和全局偏移表共同解析符號引用
當第一次建立程序的內存影象時
動態連接器會把全局偏移表的第二和第三個入口設置為特定的值
下面會對這些值進行介紹
如果程序連接表是位置無關的
需要把全局偏移表地址保存在%ebx中
進程影象中的每個共享目標文件都有自己的程序連接表
而且程序的執行流程改變時
也只能跳轉到同一個目標文件的程序連接表入口
例如
一個程序foo
它的動態連接庫為bar
so
它們都有自己程序連接表
那麼foo正文段調用某個程序連接表入口時
只能跳轉到foo文件自己的程序連接表
而不能轉到bar
so的程序連接表中
因此
在調用程序連接表入口之前
函數調用代碼應該設置全局偏移表的基址寄存器
為了便於描述
我們假設程序要調用另一個目標文件的函數name
因此首先需要把程序執行控制權轉移到標記為
PLT
的代碼處
這段代碼的第一條指令就是
跳轉到name
在全局偏移表的入口地址
因為name
是在另一個目標文件中的調用
所以在初始化時
全局偏移表沒有保存name
的真實地址
而只是保存了這段代碼第二條指令pushl的地址
因而
程序會接著執行第二條指令
在棧壓入一個重新定位的偏移值(offset)
這個重新定位的偏移值是重定位表中的一個
位的非負字節偏移值
這個特指的重定位入口是R_
_JMP_SLOT類型的
它的偏移值將指定先前jmp指令用到的全局偏移表的入口
重定位入口還有一個符號表索引
告訴動態連接器哪個符號被引用
在這個例子中是name
在棧中壓入重定位偏移值以後
程序接著就跳轉到
PLT
它是程序連接表的第一個入口
pushl指令在棧中壓入第二個全局偏移表的入口(got_plus_
或者
(%ebx))
從而給動態連接器一個單字識別信息
程序接著跳轉到全局偏移表的第三個入口中的地址(got_plus_
或者
(%ebx))
將控制權轉移給動態連接器
當動態連接器獲得控制權
它就會展開棧
讀出指定的重定位入口
找出符號表的值
把name
的真正地址保存到全局偏移表的name
入口中
然後將控制權轉移給目的目標
因此
如果再次調用name
就會直接從程序連接表入口轉移到name
而不必再次調用動態連接器
也就是說
PLT
的jmp指令將轉移到name
而不是接著執行push
指令
LD_BIND_NOW環境變量能夠改變動態連接行為
如果這個環境變量不為空
動態連接器在把控制權交給程序之前會先為程序連接表賦值
也就是說
在進程初始化期間
動態連接器為R_
_JMP_SLOT類型的重定位入口賦值
以便在第一次調用時
不必通過動態連接器就能夠跳轉到目標地址
反之
如果這個環境變量為空
動態連接器就暫不為程序連接表入口賦值
不對符號進行解析和重定位
直到第一次調用一個程序連接表入口
才對其做相應的處理
這種方式叫作後期連接(lazy binding)方式
注意
後期連接(lazy binding)方式一般會大大提高應用程序的性能
因為不必為解析無用的符號浪費動態連接器的開銷
不過
有兩種情況例外
第一
對一個共享目標函數進行初始化處理花費的時間比調用正式的執行時間長
因為動態連接器會攔截調用以解析符號
而這個函數功能又比較簡單
第二
如果發生錯誤和動態連接器無法解析符號
動態連接器就會終止程序
使用後期連接方式
這種錯誤可能會在程序執行過程中
隨時發生
而有些應用程序對這種不確定性有比較嚴格的限制
因此
需要關閉後期連接方式
在應用程序接受控制權之前
讓動態連接器處理進程初始化期間發生的這些錯誤
下面將對其細節做一些解釋
因為在編譯時共享庫的調用不能被連接到程序中
所以需要對其做特殊處理
直到程序運行時
共享庫才是有效的
PLT就是為了處理這種情況
PLT保存調用動態連接器的有關代碼
由動態連接器對所需例程進行定位
可執行目標是調用PLT的某個入口來實現對共享庫例程的調用
而不是直接調用共享庫例程
然後
由PLT解析符號表示什麼以及進行其它操作
下列代碼來自ELF規范
PLT
:jmp *name
_in_GOT
pushl $offset
jmp
PLT
@PC
從這段代碼中可以得到一些重要的信息
這是一個例程調用
而不是庫調用
進程初始化之後
name
_in_GOT指向後面的push
指令
offset代表一個重定位偏移值(參見ELF規范)
它包含一個符號引用
這個符號表示這個庫調用
使後面的jmp指令能夠跳轉到動態連接器
為了避免下次調用這個共享庫例程時重復這個流程
動態連接器接著會修改name
_in_GOT
讓其直接指向這個例程
這樣就能夠節約再次調用的時間
上面的敘述總結了PLT在搜索庫調用時的重要性
因此
我們可以修改name_in_GOT使其指向我們自己的代碼
取代原先庫調用
實現病毒的傳染
如果在取代之前
我們保存GOT的狀態
那麼還能夠重新調用原來的庫調用
而且可以實現任意庫調用的重定向
感染ELF文件
為了實現庫調用的重定向
需要在可執行目標文件中加入新的代碼
本文我們將不涉及這方面的問題
這在~silvio已經有專門的文章論述
PLT重定向
入口點的算法如下
把正文段標記為可寫
保存PLT(GOT)入口
使用新的庫調用地址代替PLT(GOT)入口
新的庫調用算法如下
實現新的庫調用的功能
保存原來的PLT(GOT)入口
調用庫調用
再次保存PLT(GOT)
如果它被修改了的
使用新的庫調用的地址代替PLT(GOT)入口
為了更清楚地解釋PLT重定向是如何工作的
我們在此解析一段簡單的代碼
在這段代碼中被重定向的是printf
新的代碼是在printf輸出一個字符串之前
打印一條消息
好吧
現在開始
首先保存寄存器
x
/* pusha */
把正文段標記為rwx
因為正文段通常是不可寫的
所以為了能夠修改PLT
我們需要把它改為可寫的
通過mprotect系統調用
xb
x
dx
x
x
/* movl $
%eax */
xbbx
x
x
x
/* movl $text_start
%ebx */
xb
x
x
x
x
/* movl $
x
%ecx */
xbax
x
x
x
/* movl $
%edx */
xcdx
/* int $
x
From:http://tw.wingwit.com/Article/program/Oracle/201311/16530.html