距離上一次講Lua程序逆向已經有一段時間了,這一次我們書接上回,繼續開啟Lua程序逆向系列之旅。
在軟件逆向工程實踐中,為第三方文件編寫文件格式分析器與指令反匯編器是一種常見的場景。這一篇的主要目的是,講解如何為IDA Pro編寫Luac的文件加載器,一方面強化對二進制文件格式的理解;另一方面,通過對IDA Pro進行擴展的插件式開發,更深入的領會IDA Pro的設計思想,以及掌握更多的高級使用方法。
IDA Pro提供了抽象的文件加載器與處理器模塊概念。文件加載器與程序運行時的動態加載概念類似,將文件中的代碼與數據,按照一定的邏輯方式進行排列與顯示,文件加載器在設計時,會優先選擇與文件二進制本身相關的數據結構進入載入,比如Windows、LInux、macOS上的二進制PE、ELF、Mach-O文件,它們都有數據、常量、代碼段的概念,IDA Pro允許將這些二進制的不同的數據,加載到不同類型的段里面。
文件加載器的工作就是:分析文件的基本格式;解析文件的段層次結構,將需要用到的數據與代碼加載到IDA Pro當中;區分與構建二進制中的數據與代碼在段中的位置;創建函數,變量,建立交叉引用輔助用戶進行分析等。當然,最后一項工作也可由處理器模塊完成。
IDA Pro沒有詳細的文檔描述好何為二進制開發文件加載器,最有效的學習途徑是閱讀IDA Pro程序中自帶的開源的文件加載器模塊代碼。
IDA Pro軟件在升級著,版本的變化可能會也帶來文件加載器開發接口的變化,本篇寫作時,對應的IDA Pro版本為國內眾所周知的IDA Pro版本7.0,實驗環境為macOS 10.12平臺。IDA Pro支持使用C/C++/idc/Python等多種語言編寫文件加載器。這里選擇使用Python,一方面基于語言的跨平臺性,再者,IDA Pro軟件的加載器目錄(macOS平臺):/Applications/IDAPro7.0/ida.app/Contents/MacOS/loaders中,有著可以參考的代碼。理論上,本節編寫的Luac文件加載器,放到Windows等其他平臺上,不需要進行任何的修改,也可以很好的工作。
本次參考使用到的代碼是uimage.py模塊,這個不到200行的Python腳本是一個完整的U-Boot鏡像加載器,完整的展示了二進制文件加載器的編寫流程,是不錯的參考資料。
文件加載器的架構比較簡單,只需要在py文件中提供兩個回調文法即可。分別是accept_file()與load_file()。accept_file()負責檢查二進制文件的合法性,解析結果正常則返回二進制文件的格式化描述信息,該信息會顯示在IDA Pro加載二進制文件時的對話框中,供用戶進行選擇。accept_file()的聲明如下:
def accept_file(li, filename):
"""
Check if the file is of supported format
@param li: a file-like object which can be used to access the input data
@param filename: name of the file, if it is an archive member name then the actual file doesn't exist
@return: 0 - no more supported formats
string "name" - format name to display in the chooser dialog
dictionary { 'format': "name", 'options': integer }
options: should be 1, possibly ORed with ACCEPT_FIRST (0x8000)
to indicate preferred format
"""
accept_file()判斷文件合法后,再由load_file()執行二進制的具體加載工作,這些工作包含設置處理器類型、將文件內容映射到idb數據庫中、創建數據與代碼段、創建與應用特定數據結構、添加入口點等。accept_file()的聲明如下:
def load_file(li, neflags, format):
"""
Load the file into database
@param li: a file-like object which can be used to access the input data
@param neflags: options selected by the user, see loader.hpp
@return: 0-failure, 1-ok
"""
下面來動手實現基于Lua 5.2生成的二進制Luac文件的加載器。將uimage.py模塊復制一份改名為loac_loader.py。并修改accept_file()代碼如下:
def accept_file(li, n):
"""
Check if the file is of supported format
@param li: a file-like object which can be used to access the input data
@param n : format number. The function will be called with incrementing
number until it returns zero
@return: 0 - no more supported formats
string "name" - format name to display in the chooser dialog
dictionary { 'format': "name", 'options': integer }
options: should be 1, possibly ORed with ACCEPT_FIRST (0x8000)
to indicate preferred format
"""
header = read_struct(li, global_header)
# check the signature
if header.signature == LUA_SIGNATURE and 0x52 == header.version:
global size_Instruction
global size_lua_Number
size_Instruction = header.size_Instruction
size_lua_Number = header.size_lua_Number
DEBUG_PRINT('signature:%x' % header.signature)
DEBUG_PRINT('version:%x' % header.version)
DEBUG_PRINT('format:%x' % header.format)
DEBUG_PRINT('endian:%x' % header.endian)
DEBUG_PRINT('size_int:%x' % header.size_int)
DEBUG_PRINT('size_Instruction:%x' % header.size_Instruction)
DEBUG_PRINT('size_lua_Number:%x' % header.size_lua_Number)
DEBUG_PRINT('lua_num_valid:%x' % header.lua_num_valid)
if header.size_Instruction != 4:
return 0
#if header.size_lua_Number != 8:
# return 0
return FormatName
# unrecognized format
return 0
read_struct()目的是借助ctype模塊讀取文件開始的內容,到定義的global_header類型的數據結構中去,它的第一個參數li是一個類似于文件對象的參數,可以理解它類似于C語言fopen返回的文件描述符,也可以將其理解為指向文件數據頭部的指針。
global_header數據結構來自于之前Luac.bt文件中的C語言聲明,它的定義如下:
class global_header(ctypes.Structure):
_pack_ = 1
_fields_ = [
("signature", uint32_t),
("version", uint8_t),
("format", uint8_t),
("endian", uint8_t),
("size_int", uint8_t),
("size_size_t", uint8_t),
("size_Instruction", uint8_t),
("size_lua_Number", uint8_t),
("lua_num_valid", uint8_t),
("luac_tail", uint8_t * 6),
]
定義的class繼承自ctypes.Structure,可以編寫類似于C語言的結構體定義來描述數據結構,這種方式比起直接struct.unpack方式來讀取要方便與優雅得多。
當讀取到一個完整的global_header后,需要判斷它的signature字段與version字段是否匹配Lua 5.2版本,如果匹配,還需要判斷size_Instruction字段,即指令所占的字節大小,通常它的值應該為4。所有的這些條件都滿足后,則說明該文件可能是一個正確版本的Luac二進制,那么直接返回格式化名稱FormatName。它的內容為:Lua 5.2。將luac_loader.py放入IDA Pro的loaders目錄下,將Hello2.luac文件拖入到IDA Pro的運行主窗口,此時會彈出如圖所示的對話框:
接著是load_file(),它的代碼如下:
def load_file(li, neflags, format):
"""
Load the file into database
@param li: a file-like object which can be used to access the input data
@param neflags: options selected by the user, see loader.hpp
@return: 0-failure, 1-ok
"""
if format.startswith(FormatName):
global_header_size = ctypes.sizeof(global_header)
li.seek(global_header_size)
header = read_struct(li, proto_header)
DEBUG_PRINT('linedefined:%x' % header.linedefined)
DEBUG_PRINT('lastlinedefined:%x' % header.lastlinedefined)
DEBUG_PRINT('numparams:%x' % header.numparams)
DEBUG_PRINT('is_vararg:%x' % header.is_vararg)
DEBUG_PRINT('maxstacksize:%x' % header.maxstacksize)
idaapi.set_processor_type("Luac", SETPROC_ALL|SETPROC_FATAL)
proto = Proto(li, global_header_size, "0") #function level 0
add_segm(0, 0, global_header_size, "header", 'HEADER')
add_structs()
MakeStruct(0, "GlobalHeader")
global funcs
global consts
global strs
for func in funcs:
#add funcheader_xx segment.
add_segm(0, func[3], func[3] + ctypes.sizeof(proto_header), func[4], 'CONST')
MakeStruct(func[3], "ProtoHeader")
# add func_xx_codesize segment.
add_segm(0, func[1] - 4, func[1], func[0] + "_codesize", 'CONST')
MakeDword(func[1]-4)
set_name(func[1]-4, func[0] + "_codesize")
# add func_xx segment.
add_segm(0, func[1], func[2], func[0], 'CODE')
#add_func(func[1], func[2])
for const in consts:
# add const_xx_size segment.
add_segm(0, const[1]-4, const[1], const[0] + "_size", 'CONST')
MakeDword(const[1]-4)
set_name(const[1]-4, const[0] + "_size")
# add const_xx segment.
add_segm(0, const[1], const[2], const[0], 'CONST')
for str in strs:
# add const strings.
idc.create_strlit(str[1], str[2])
li.file2base(0, 0, li.size(), 0) #map all data
mainfunc_addr = proto.code_off + 4
print("main func addr:%x" % mainfunc_addr)
add_entry(mainfunc_addr, mainfunc_addr, 'func_0', 1)
DEBUG_PRINT("Load Lua bytecode OK.")
return 1
當format參數是accept_file()返回的FormatName是,說明是合法的Luac,正常進入了文件內容加載階段。這時候按照之前分析Luac格式,使用li.seek()跳過global_header,解析proto_header結構。這是最“頂層的”Proto結構的頭部,描述了function level 0包含多少個子Proto及其其他字段信息。回顧下前面的知識,Proto的可視化結構如圖所示:
Proto類型的實現與讀取相對會麻煩一些,代碼如下:
funcs = []
consts = []
strs = []
class Proto:
def __init__(self, li, off, level):
self.level = level
DEBUG_PRINT("level: %s\n" % self.level)
off_ = off
li.seek(off)
self.header = read_struct(li, proto_header)
off += ctypes.sizeof(proto_header)
self.code_off = off
self.code = Code(li, off)
funcs.append(get_func_area(level, off + 4, off + self.code.size(), off_))
off = off + self.code.size()
self.constants = Constants(li, off)
consts.append(get_consts_area(level, off + 4, off + self.constants.size()))
off = off + self.constants.size()
DEBUG_PRINT("protos off:%x\n" % off)
self.protos = Protos(li, off, level)
off = off + self.protos.size()
self.upvaldecs = Upvaldescs(li, off)
off = off + self.upvaldecs.size()
self.src_name = SourceName(li, off)
off = off + self.src_name.size()
self.lines = Lines(li, off)
off = off + self.lines.size()
self.loc_vars = LocVars(li, off)
off = off + self.loc_vars.size()
self.upval_names = UpValueNames(li, off)
off = off + self.upval_names.size()
self.sz = off - off_
def size(self):
return self.sz
函數、常量、字符串這些信息在解析后我們全局進行保存,目的是后面在創建各種類型的數據段時需要用到。Protos、Constants、LocVars、UpValueNames這些數據結構的定義,由于篇幅原因就不帖出來了,具體的實現代碼可以在文末的代碼地址處獲取。
接著有一條比較重要的調用:
idaapi.set_processor_type("Luac", SETPROC_ALL|SETPROC_FATAL)
idaapi.set_processor_type()用來設置處理器模塊,這里使用的”Luac”是我事先編寫好的處理器模塊,將會在以后進行講解,初期開發時,可以將其指定為IDA Pro中提供的其他樣例處理器模塊輔助開發加載器。
解析完Luac,集齊這些數據后,就可以使用IDA Pro提供的add_segm()接口,在idb數據庫中創建段了。add_segm()的定義位于ida_segment.py中,如下所示:
def add_segm(*args):
"""
add_segm(para, start, end, name, sclass, flags=0) -> bool
"""
return _ida_segment.add_segm(*args)
第一個參數通常為0;start與end指明了數據的起始與結束地址;name為段的名稱;sclass為段的類別,類別可以是HEADER表示文件頭,CONST表示是常量數據,CODE表示是代碼段,DATA表示是數據段。 如下的代碼即會創建一個HEADER類別的段:
add_segm(0, 0, global_header_size, "header", 'HEADER')
創建完段后,我們還想將這個段的內容應用上global_header結構體聲明,讓IDA Pro可以更加直觀顯示字段的描述與數值。這個時候,就需要將global_header結構體的聲明先加入到IDA Pro中去。三種方法可以實現:一是導入事先聲明好相關結構體的til文件;二是在內存中制作til,導入C語言的結構體描述,然后會在內存中創建til;最后一種是使用腳本一行行導入結體體聲明。最終,在相應的數據位置應用結構體信息即可。第一種方法這里不講,因為沒有事先做好til,第二種方法可以使用代碼來自動化完成,首先使用new_til()在內存中創建til,然后使用parse_decls()與doStruct()接口解析C語言結構生成til結構體信息,最后一種其實最方便,這里推薦一下,可以事先在IDA Pro中手動導入C聲明,然后執行File->Product file->Dump datebase to IDC file…,執行后會生成所有IDA Pro操作過的idc腳本,其中包括導入與創建、應用數據結構體的部分。將這一段代碼引入到Python中即可。這里add_structs()的代碼片斷如下:
def add_structs():
begin_type_updating(UTP_STRUCT)
AddStrucEx(-1, "GlobalHeader", 0)
AddStrucEx(-1, "ProtoHeader", 0)
id = GetStrucIdByName("GlobalHeader")
AddStrucMember(id, "signature", 0, 0x000400, -1, 4)
AddStrucMember(id, "version", 0X4, 0x000400, -1, 1)
AddStrucMember(id, "format", 0X5, 0x000400, -1, 1)
AddStrucMember(id, "endian", 0X6, 0x000400, -1, 1)
......
SetType(get_member_id(id, 0x0), "unsigned int")
SetType(get_member_id(id, 0x4), "unsigned int")
SetType(get_member_id(id, 0x8), "unsigned __int8")
SetType(get_member_id(id, 0x9), "unsigned __int8")
SetType(get_member_id(id, 0xA), "unsigned __int8")
end_type_updating(UTP_STRUCT)
set_inf_attr(INF_LOW_OFF, 0x20)
set_inf_attr(INF_HIGH_OFF, 0x22A)
添加了結構體后,執行MakeStruct(0, “GlobalHeader”)即可將起始的HEADER數據段應好了GlobalHeader結體體信息。如圖所示:
接著如法炮制,加載其他的段,加載完成后,執行如下的命令完成數據的映射工作:
li.file2base(0, 0, li.size(), 0)
最后,是調用add_entry()設置程序的入口點。完成后,加載器的工作基本就完成了。加載器完成Luac加載后,可以在IDA Pro中查看它的段結構信息如下:
后面,將會是處理器模塊負責創建函數、數據、交叉引用、反匯編等工作。
完整的luac_loader.py文件可以在這里找到:https://github.com/feicong/lua_re。
Lua程序逆向系列的故事仍然在繼續著,To be continued…