利用Lighthouse進行覆蓋率統計及其優化
介紹IDA覆蓋率統計插件Lighthouse的使用,并對其覆蓋率輸出方式進行修改,獲得可閱讀的明文代碼執行路徑信息。
1、背景
最近有統計覆蓋率信息的需求,多方搜索后發現IDA插件Lighthouse具有統計覆蓋率的功能,通過讀取DynamoRIO或者Pin產生的覆蓋率日志文件,在IDA中以圖形化形式展現代碼的詳細執行路徑。
DynamoRIO或Pin等插樁工具默認使用的日志文件格式為drcov格式,這是一種二進制格式,每個基本塊的信息的都是以十六進制數據進行記錄。雖然二進制形式的記錄方式有利于提高性能,但是人工閱讀困難。
2、Lighouse的基本使用
(1)下載:Lighthouse(https://github.com/gaasedelen/lighthouse)
(2)安裝:
在IDA中找到插件文件的目錄:
import idaapi, os; print(os.path.join(idaapi.get_user_idadir(), "plugins"))
將下載下來的源碼中的/plugins/文件夾copy到上面命令執行結果的目錄中,然后重啟IDA。
(3)獲取drcov格式覆蓋率統計日志文件:
首先使用Pin或DynamoRIO獲取覆蓋率統計文件(這里以Pin為例):

這里需要注意的是,Lighthouse默認使用的drcov文件版本為version 2,但是最新版的DynamoRIO生成的drcov文件的版本為version 3,所以在導入IDA時會提示文件格式錯誤。Lighthouse目前提供了pin和frida的覆蓋率統計插件,DynamoRIO的需要做修改或者使用舊版本的DynamoRIO:

(4)IDA中導入日志文件:
首先IDA加載要觀察的可執行文件,然后File -> Load file -> Code coverage file...加載剛剛生成的日志文件:
控制流圖的藍色基本塊為執行了的基本塊,右側為coverage的overview信息。

同樣進行F5之后,可以看到執行過的偽代碼:

3、drcov文件格式
(1)簡介
drcov是基于DynamoRIO框架的用于收集二進制程序覆蓋率信息的一種工具,其收集的覆蓋率信息格式即為drcov格式。因為其成熟高效的特點,很多進行覆蓋率收集的工具都會使用這種格式。
DynamoRIO官方并未對drcov格式進行詳細的說明,所以此處進行說明記錄,希望能對后續的覆蓋率信息收集工具的開發起到一定的作用。
(2)詳細格式
首先,drcov格式有一個包含一些metadata的頭部:
DRCOV VERSION: 2DRCOV FLAVOR: drcov
在Lighthouse(https://github.com/gaasedelen/lighthouse)中只支持了version 2的格式;DRCOV FLAVOR是一個描述產生覆蓋率信息的工具的字符串,并沒有具體的實際作用。
然后,是在收集覆蓋率信息的過程中加載的模塊的映射的模塊表:
Module Table: version 2, count 39Columns: id, base, end, entry, checksum, timestamp, path 0, 0x10c83b000, 0x10c83dfff, 0x0000000000000000, 0x00000000, 0x00000000, /Users/ayrx/code/frida-drcov/bar 1, 0x112314000, 0x1123f4fff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/dyld 2, 0x7fff5d866000, 0x7fff5d867fff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/libSystem.B.dylib 3, 0x7fff5dac1000, 0x7fff5db18fff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/libc++.1.dylib 4, 0x7fff5db19000, 0x7fff5db2efff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/libc++abi.dylib 5, 0x7fff5f30d000, 0x7fff5fa93fff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/libobjc.A.dylib 8, 0x7fff60617000, 0x7fff60647fff, 0x0000000000000000, 0x00000000, 0x00000000, /usr/lib/system/libxpc.dylib ... snip ...
模塊表的頭部有兩種變體,都包含模塊表中的條目數:
Format used in DynamoRIO v6.1.1 through 6.2.0 eg: 'Module Table: 11'Format used in DynamoRIO v7.0.0-RC1 (and hopefully above) eg: 'Module Table: version X, count 11'
每個版本的表格格式有些許不同:
DynamoRIO v6.1.1, table version 1: eg: (Not present)DynamoRIO v7.0.0-RC1, table version 2: Windows: 'Columns: id, base, end, entry, checksum, timestamp, path' Mac/Linux: 'Columns: id, base, end, entry, path'DynamoRIO v7.0.17594B, table version 3: Windows: 'Columns: id, containing_id, start, end, entry, checksum, timestamp, path' Mac/Linux: 'Columns: id, containing_id, start, end, entry, path'DynamoRIO v7.0.17640, table version 4: Windows: 'Columns: id, containing_id, start, end, entry, offset, checksum, timestamp, path' Mac/Linux: 'Columns: id, containing_id, start, end, entry, offset, path'
雖然有很多列的數值,但是實際上能于Lighthouse交互的數據只有以下幾種:
id: 生成模塊表時分配的序號,稍后用于將基本塊映射到模塊。
start, base: 模塊開始的內存基地址。
end: 模塊結束的內存地址。
path: 模塊在硬盤上的存儲路徑。
最后,日志文件有一個基本塊表,其中包含在收集覆蓋信息時執行的基本塊列表。雖然drcov可以以文本格式轉儲基本塊表(使用-dump_text選項),但它默認以二進制格式轉儲表。
BB Table: 861 bbs<binary data>
該表首先是一個表頭,表明基本塊的數量。后續跟的數據是一個每個8字節大小的__bb_entry_t結構組成的數組,__bb_entry_t的結構如下:
typedef struct _bb_entry_t { uint start; /* offset of bb start from the image base */ ushort size; ushort mod_id;} bb_entry_t;
結構解釋如下:
start: 距離基本塊入口開始的模塊的基地址的偏移。
size: 基本塊的大小。
mod_id: 發現的基本塊所在模塊的id,與前面模塊表中的id是對應的。
基于上面3個元素,就可以知道哪個基本塊被執行了,從而作為覆蓋率信息進行收集。
(3)修改輸出方式為明文(以Pin插件為例)
因為Lighthouse默認輸出的覆蓋率日志文件時drcov格式的,人工閱讀存在一定的困難。在某些場景下,需要直接獲得人工易讀的代碼執行路徑信息,所以考慮對Lighthouse的覆蓋率統計插件進行修改。
Lighthouse的覆蓋率統計功能在如下代碼中:
# CodeCoverage.cpp static VOID OnFini(INT32 code, VOID* v){ ...snap... drcov_bb tmp; for (const auto& data : context.m_terminated_threads) { for (const auto& block : data->m_blocks) { auto address = block.first; auto it = std::find_if(context.m_loaded_images.begin(), context.m_loaded_images.end(), [&address](const LoadedImage& image) { return address >= image.low_ && address < image.high_; }); if (it == context.m_loaded_images.end()) continue; tmp.id = (uint16_t)std::distance(context.m_loaded_images.begin(), it); tmp.start = (uint32_t)(address - it->low_); tmp.size = data->m_blocks[address]; context.m_trace->write_binary(&tmp, sizeof(tmp)); } }}
首先設置了一個drcov_bb結構tmp,其完整格式如下:
struct __attribute__((packed)) drcov_bb { uint32_t start; uint16_t size; uint16_t id; };
然后進入到一個內外嵌套循環中,在每個內循環中每讀到一個bb信息就對tmp結構進行賦值:
tmp.id = (uint16_t)std::distance(context.m_loaded_images.begin(), it);tmp.start = (uint32_t)(address - it->low_);tmp.size = data->m_blocks[address];
最后調用write_binary函數寫入到trace文件中:
context.m_trace->write_binary(&tmp, sizeof(tmp));
而write_binary函數的實現在Trace.h文件中:
void write_binary(const void* ptr, size_t size){ if (fwrite(ptr, size, 1, m_file) != 1) { std::cerr << "Could not log to the log file." << std::endl; std::abort(); }}
可以看到本質上就是調用fwrite函數進行流操作。此外,還有一個write_string函數:
void write_string(const char* format, ...){ va_list args; va_start(args, format); if (vfprintf(m_file, format, args) < 0) { std::cerr << "Could not log to the log file." << std::endl; std::abort(); } va_end(args);}
該函數用作想trace文件中寫入string格式的數據。這么一來就好辦了,直接用現成的即可,只需要修改在寫文件時的操作就ok了。修改后的代碼如下:
// drcov_bb tmp; 這里要注釋掉。否則有的環境會報編譯不通過 for (const auto& data : context.m_terminated_threads) { for (const auto& block : data->m_blocks) { auto address = block.first; auto it = std::find_if(context.m_loaded_images.begin(), context.m_loaded_images.end(), [&address](const LoadedImage& image) { return address >= image.low_ && address < image.high_; }); if (it == context.m_loaded_images.end()) continue; uint16_t id = (uint16_t)std::distance(context.m_loaded_images.begin(), it); uint32_t start_addr = (uint32_t)(address - it->low_); int size = data->m_blocks[address]; context.m_trace->write_string("[+]module: [%d] 0x%08x %d\n", id, start_addr, size); }}
這種格式只能用作人工閱讀或進一步的處理,沒有辦法再使用drcov2lcov和genhtml工具進行轉換了,最終實現的效果如下:

會以明文形式打印出每個模塊的執行的基本塊的地址和塊大小,這樣就方便人工進行閱讀,還可以進一步提取出模塊執行的地址,進行后續處理。