文章首發於
https://mp.weixin.qq.com/s/7STPL-2nCUKC3LHozN6-zg
前言
CAJViewer是一個論文查看工具,主要用於查看caj文件格式的論文。本文介紹對該軟件進行逆向分析和漏洞挖掘的過程。
代碼地址
https://github.com/hac425xxx/cajviewer-fuzz-data
正文
逆向分析
首先分析的是CAJViewer的Windows版本,由於我們的目的是挖掘軟件的漏洞,通過介紹我們知道CAJViewer本質上是一個文件解析程序,因此該軟件的高危模塊應該是軟件中解析文件數據的部分,因此首先應該大概定義軟件數據處理部分所在位置,Windows平台下可以使用 process monitor來進行初步的分析。
首先打開process monitor並開始捕獲事件,然后使用CAJViewer打開一個caj文件,等文件解析完成后停止捕獲事件。
然后我們可以過濾一下需要查看的事件,比如上圖設置了只查看文件操作並且只查看對 input.caj
文件的操作,該文件就是之前讓CAJViewer打開的文件。
然后我們可以找一下讀文件的操作(ReadFile
),因為大部分文件解析邏輯應該讀一部分文件內容解析一部分,因此通過查看讀文件時的調用棧就可以大概定位解析數據的模塊,然后雙擊就可以查看調用相應函數的調用棧。
通過查看多個數據讀取的調用棧,可以發現ReaderEx.dll在調用棧中出現多次,因此大概可以猜測ReaderEx.dll應該主要負責處理文件數據。
逆向了一會ReaderEx.dll后,發現CAJViewer今年還發布了Linux版本,於是下載下來分析了一下。下載下來后是一個可執行文件CAJViewer-x86_64-libc-2.24.AppImage
,執行起來查看進程的maps發現其實軟件會在tmp目錄把打包好的二進制解壓,然后去執行tmp目錄下的二進制。
這里可以直接把/tmp/.mount_CAJVierjayBH/
拷貝到一個目錄,然后就可以直接執行 cajviewer
了。
查看解壓處理的二進制發現一個libreaderex_x64.so,看名字應該是ReaderEx.dll的Linux版本,然后使用IDA打開,發現比Windows版本的要好分析一點,信息也比ReaderEx.dll的多。於是接下來決定對Linux版本的二進制進行分析。
首先看看主程序cajviewer,查看main函數可以發現軟件是用qt寫的
之后翻了一下函數列表,發現了MainWindow::OpenFile,看名稱應該是打開一個文件。
__int64 __fastcall MainWindow::OpenFile(MainWindow *this, const QString *a2)
{
v2 = this;
QString::toUtf8_helper(&v16, a2);
memset(v19, 0, sizeof(v19));
*v19 = 0x2D8;
*&v19[4] = 256;
*&v19[8] = CAJFILE_CreateErrorObject(&v20);
v3 = *&v19[8];
if ( *v16 > 1 || (v5 = *(v16 + 2), v4 = v16, v5 != 24) )
{
QByteArray::reallocData(&v16, v16[1] + 1, *(v16 + 11) >> 31);
v4 = v16;
v5 = *(v16 + 2);
}
v6 = CAJFILE_OpenEx1(v4 + v5, v19); // 打開文件
這里對輸入的QString進行一些處理后,調用了CAJFILE_OpenEx1
函數,該函數位於libreaderex_x64.so
。
Fuzz測試
Fuzz CAJFILE_OpenEx1函數
函數代碼如下
函數的第一個參數是要解析的文件路徑,第二個參數是一塊內存,這個參數的結構可以查看MainWindow::OpenFile調用點。
可以看到in_buf的結構如下
+0: 4個字節 in_buf的長度
+4: 4個字節 一個整形值
+8: 一個指針, 存放構造好的 ErrorObject
使用調試器在這個函數下個斷點,然后打開一個文件就可以看到入參如下
之后有簡單的翻了一些該函數的實現,以及使用該函數的位置可以大概確定CAJFILE_OpenEx1
用於打開一個文件,並會對文件的內容進行解析,因此下面打算使用AFL Qemu模式Fuzz一下這個函數。Fuzz之前需要寫一點代碼把so加載到內存,然后構造參數對目標函數進行測試。
首先需要把SO加載到內存中並獲取目標函數的地址
void my_init(void) __attribute__((constructor)); //告訴gcc把這個函數扔到init section
void my_init(void)
{
void *handle;
handle = dlopen("/home/hac425/cajviewer/cajviewer-bin/usr/lib/libreaderex_x64.so", RTLD_LAZY);
struct link_map *lm = (struct link_map *)handle;
printf("%lx\n", lm->l_addr);
p_CAJFILE_OpenEx1 = dlsym(handle, "CAJFILE_OpenEx1");
p_CAJFILE_CreateErrorObject = dlsym(handle, "CAJFILE_CreateErrorObject");
}
my_init會在main函數之前執行,代碼流程如下
- 首先dlopen把so加載到內存,並把so在內存中的基地址打印到屏幕,便於后續測試。
- 然后使用dlsym獲取CAJFILE_OpenEx1和CAJFILE_CreateErrorObject函數的地址。
然后在main函數中就會構造參數調用目標函數
int main(int argc, char **argv)
{
char buf[0x2D8];
printf("main:%p\n", main);
memset(buf, 0, 0x2D8);
*(unsigned int *)buf = 0x2D8;
// *(unsigned int *)(buf + 4) = 256;
// *(char* *)(buf + 8) = p_CAJFILE_CreateErrorObject();
char *ret = p_CAJFILE_OpenEx1(argv[1], buf);
return 0;
}
代碼邏輯很簡單,首先構造CAJFILE_OpenEx1
函數的第二個參數,然后把argv[1]
作為文件路徑傳入函數。
然后編譯一下
gcc CAJFILE_OpenEx1.c -o test_CAJFILE_OpenEx1_dbg -ldl -lheapasan -L libheapasan/ -g
編譯后執行一下,可以看到正常執行完了,並打印出so的基地址和main函數的地址。
harness$ ./test_CAJFILE_OpenEx1_dbg ~/input.caj
string to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstringto intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to int
image base:0x7f6d87bff000
p_CAJFILE_OpenEx1:0x7f6d881e486c
main:0x555ed124cb71
接下來再使用afl-qemu-trace執行一下,獲取一些地址用於Fuzz,使用afl-qemu-trace執行一個可執行程序時,其進程的so的地址都是固定的。
harness$ ~/AFLplusplus-2.66c/afl-qemu-trace ./test_CAJFILE_OpenEx1_dbg ~/input.caj
string to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstringto intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to intstring to int
image base:0x400133e000
p_CAJFILE_OpenEx1:0x400192386c
main:0x4000000b71
可以看到libreaderex_x64.so
的基地址為0x400133e000
, test_CAJFILE_OpenEx1_dbg
的main
函數的地址為0x4000000b71
。
然后去IDA中查看libreaderex_x64.so
中代碼段的范圍
所以可以得到afl-qemu-trace執行時libreaderex_x64.so
中代碼段的范圍為
開始地址: 0x400133e000+0x3D4880 = 0x4001712880
結束地址: 0x400133e000+0x90984F = 0x4001c4784f
然后可以使用AFL進行測試了
export AFL_CODE_START=0x4001712880
export AFL_CODE_END=0x4001c4784f
export AFL_ENTRYPOINT=0x4000000b71
/home/hac425/AFLplusplus-2.66c/afl-fuzz -m none -Q -t 20000 -i in -o out -- ./test_CAJFILE_OpenEx1_dbg @@
其中設置的環境變量的作用如下
AFL_CODE_START 和 AFL_CODE_END 表示需要統計覆蓋率的范圍
AFL_ENTRYPOINT 表示開啟forkserver的位置
Fuzz UnCompressImage函數
在測試CAJFILE_OpenEx1時,去翻了一下libreaderex_x64.so里面的其他函數,在查看字符串時發現了一些源碼路徑。
拿路徑去網上搜了一下,發現是用到了Kakadu_V2.2.3這個開源庫,這個庫很古老了(2008年的),用於解析jpeg2000格式,版本老往往表示存在漏洞幾率較大,而且jpeg2000格式很復雜,在其他軟件中也發現了很多漏洞,於是下面仔細的看了下。
下載到這個庫的代碼,然后一路回溯發現libreaderex_x64.so應該是在 jpeg2000.cpp里面實現了部分代碼,最后一路跟到了DecodeJpeg2000函數,並基於開源代碼把DecodeJpeg2000的參數基本弄清楚了。繼續往上跟DecodeJpeg2000,找到了UnCompressImage函數,這個函數應該是解析圖片數據的統一接口了。
CAJViewer在解析CAJ等文件時,如果文件中嵌入了圖片數據時,就會會使用libreaderex_x64.so中的UnCompressImage函數來對圖片數據進行解析。
函數的參數信息如下:
buffer: 保存從文件中提取出的圖片數據
type: 圖片的類型
buffer_length: 圖片數據的長度
剩下兩個參數a4,a5: 個人猜測可能是需要將圖片縮放的大小
然后編寫代碼,my_init的主要邏輯和 CAJFILE_OpenEx1函數的一致,只是需要hook一些函數,避免比Fuzz識別為crash,比如在代碼里面有很多assert,如果直接執行到這個函數的話,會被afl識別為crash.
因此這里使用plt hook,把libreaderex_x64.so模塊中的一些函數給hook了。
int my_assert_fail()
{
printf("my_assert_fail\n");
exit(1);
return 0;
}
int my_cxa_throw()
{
printf("my_cxa_throw\n");
exit(1);
return 0;
}
void my_init(void)
{
........................................
........................................
plt_hook_function("libreaderex_x64.so", "__assert_fail", my_assert_fail);
plt_hook_function("libreaderex_x64.so", "__cxa_throw", my_cxa_throw);
}
然后再main函數中調用目標函數
int main(int argc, char **argv)
{
printf("main:%p\n", main);
int f_sz = 0;
char* buffer = read_to_buf(argv[1], &f_sz);
char *ret = p_UnCompressImage(buffer, 4, f_sz, 100, 100);
return 0;
}
然后其他的操作和Fuzz CAJFILE_OpenEx1函數時一致,只是環境變量需要重新設置
/home/hac425/AFLplusplus-2.66c/afl-fuzz -m none -Q -t 20000 -i image_fuzz/ -o UnCompressImageOutput -- ./test_UnCompressImage @@
部分漏洞分析
CImage::LoadBMP 內存為初始化漏洞
Cajviewer For Linux 在解析BMP圖片時會進入 CImage::LoadBMP 函數,該函數中存在內存未初始化漏洞。
函數的流程如下
-
第8行,調用BaseStream::streamLength獲取文件的大小。
-
第9行,調用FileStream::read從文件中讀出14字節的文件頭。
-
第10行,調用gmalloc分配內存用於存放文件的其他數據,這里實際上是直接調用malloc分配內存。
-
第12行,這里將分配的內存沒有初始化直接傳入FindDIBBits,該函數計算一個地址保存到this->DIBBits域。
-
第14行,這里會從this->DIBBits中讀取數據,導致crash。
下面看看FindDIBBits的實現
這里取出a1的開始4個字節作為一個偏移值 v1,然后調用PaletteSize,這個函數的返回值的可以為0,128等數字值。
由於a1這個內存沒有初始化,故v1有可能會很大,進而導致FindDIBBits會返回一個越界的地址。
然后在CImage::CalibrateColor中就會去訪問這個內存。
CImage::DecodeJbig 越界讀寫漏洞
CajViewer在解析CAJ等文件時,如果需要解析文件中嵌入的圖片數據時,會使用libreaderex_x64.so中的函數來對圖片進行解析,其中如果帶解析的文件類型為Jbig文件時,會進入CImage::DecodeJbig函數進行解析:
其中重要函數的參數和作用如下:
- buf: 保存從文件中提取出的圖片數據
- len: buf的長度
其中buf一開始是一個JbigInfo的結構,結構體的定義如下:
然后然后會進入CImage::CImage進行簡單的文件解析。
首先使用JbigInfo中的字段計算一個sz, 然后使用 gmalloc分配內存,之后會使用memcpy 拷貝數據。
漏洞位於在計算sz時會導致整數溢出,進而導致會分配一個小於4LL * (1 << jbig_info->width2)的內存,然后在下面memcpy時會導致越界寫。
此外整個過程沒有校驗jbig_info的長度,所以會導致越界讀。
總結
本文介紹了如何分析一個軟件並使用afl qemu模式來測試閉源二進制。