2021年10月,我們的研究人員發現了一個安全漏洞,並在2021年11月舉行的Pwn2Own 2021大賽中成功利用了該漏洞。今年一月,Lexmark公司發布了該漏洞(CVE-2021-44737)的安全補丁。
最初,我們是打算以Lexmark MC3224i打印機為目標的,然而,由於該型號的打印機到處缺貨,所以,我們決定買一台Lexmark MC3224dwe打印機作為其替代品。它們的主要區別在於:Lexmark MC3224i型號提供了傳真功能,而Lexmark MC3224dwe型號則缺乏該功能。從漏洞分析的角度來看,這意味著兩者之間可能有一些差異,至少我們很可能無法利用某些功能。於是,我們下載了兩個型號的固件更新,發現它們竟然完全一樣,所以,我們決定繼續考察Lexmark MC3224dwe——反正我們也沒有選擇的餘地。
就像Pwn2Own所要求的那樣,該漏洞可被遠程利用,無需經過身份驗證,並存在於默認配置中。利用該漏洞,攻擊者就能以該打印機的root用戶身份遠程執行代碼。
關於該漏洞的利用過程,具體如下所示:
1、利用臨時文件寫入漏洞(CVE-2021-44737),對ABRT鉤子文件執行寫操作
2、通過遠程方式,令進程發生崩潰,以觸發ABRT的中止處理
3、中止處理將導致ABRT鉤子文件中的bash命令被執行
實際上,臨時文件寫入漏洞位於Lexmark特有的hydra服務(/usr/bin/hydra)中,該服務在Lexmark MC3224dwe打印機上是默認運行的。 hydra服務的二進製文件非常大,因為它需要處理各種協議。該漏洞存在於打印機工作語言(PJL)命令中,更具體地說,是在一個名為LDLWELCOMESCREEN的命令中。
我們已經分析並利用了CXLBL.075.272/CXLBL.075.281版本中的漏洞,但舊版本也可能存在該漏洞。在這篇文章中,我們將詳細介紹針對CXLBL.075.272的分析過程,因為CXLBL.075.281是在去年10月中旬發布的,並且我們一直在研究這個版本。
注意:Lexmark MC3224dwe打印機是基於ARM(32位)架構的,但這對漏洞的利用過程並不重要,只是對逆向分析有點影響。
由於該漏洞先是觸發了一個ABRT,隨後又中止了ABRT,所以,我們將其命名為“MissionAbrt”。
逆向分析您可以從Lexmark下載頁面下載Lexmark固件更新文件,但是,這個文件是加密的。如果您有興趣了解我們的同事Catalin Visinescu是如何使用硬件黑客技術來獲取該固件文件的,請參閱本系列的第一篇文章。
漏洞詳細信息背景知識正如維基百科所說:
打印機作業語言(PJL)是Hewlett-Packard公司開發的一種方法,用於在作業級別切換打印機語言,以及在打印機和主機之間進行狀態回讀。 PJL增加了作業級別控制,如打印機語言切換、作業分離、環境、狀態回讀、設備考勤和文件系統命令。
PJL命令如下所示:
@PJLSETPAPER=A4
@PJLSETCOPIES=10
@PJLENTERLANGUAGE=POSTSCRIPT眾所周知,PJL對攻擊者來說是非常有用的。過去,一些打印機曾經曝光過允許在設備上讀寫文件的漏洞。
PRET是這樣一款工具:借助於該軟件,我們就能在各種打印機品牌上使用PIL(以及其他語言),但它不一定支持所有的命令,因為每個供應商都提供了自己的專有命令。
找到含有漏洞的函數雖然hydra服務的二進製文件沒有提供符號,卻提供了許多日誌/錯誤函數,其中包含一些函數名。下面顯示的代碼是通過IDA/Hex-Rays反編譯的代碼,因為尚未找到該二進製文件的開放源代碼。其中,許多PJL命令由地址為0xFE17C的setup_pjl_commands()函數註冊的。這裡,我們對LDLWELCOMESCREEN PJL命令非常感興趣,因為它好像是Lexmark專有的,並且沒有找到相應的說明文檔。
int__fastcallsetup_pjl_commands(inta1)
{
//[COLLAPSEDLOCALDECLARATIONS.PRESSKEYPADCTRL-'+'TOEXPAND]
pjl_ctx=create_pjl_ctx(a1);
pjl_set_datastall_timeout(pjl_ctx,5);
sub_11981C();
pjlpGrowCommandHandler('UEL',pjl_handle_uel);
.
pjlpGrowCommandHandler('LDLWELCOMESCREEN',pjl_handle_ldlwelcomescreen);
.當接收到PJL LDLWELCOMESCREEN命令後,0x1012f0處的pjl_handle_ldlwelcomescreen()函數將開始處理該命令。我們可以看到,這個命令使用一個表示文件名的字符串作為第一個參數:
int__fastcallpjl_handle_ldlwelcomescreen(char*client_cmd)
{
//[COLLAPSEDLOCALDECLARATIONS.PRESSKEYPADCTRL-'+'TOEXPAND]
result=pjl_check_args(client_cmd,'FILE','PJL_STRING_TYPE','PJL_REQ_PARAMETER',0);
if(result=0)
returnresult;
filename=(constchar*)pjl_parse_arg(client_cmd,'FILE',0);
returnpjl_handle_ldlwelcomescreen_internal(filename);
}然後,0x10a200處的pjl_handle_ldlwelcomescreen_internal()函數將嘗試打開該文件。請注意,如果該文件存在,該函數並不會打開它,而是立即返回。因此,我們只能寫還不存在的文件。此外,完整的目錄層次結構必須已經存在,以便我們創建文件,同時,我們還需要具有寫文件的權限。
unsignedint__fastcallpjl_handle_ldlwelcomescreen_internal(constchar*filename)
{
//[COLLAPSEDLOCALDECLARATIONS.PRESSKEYPADCTRL-'+'TOEXPAND]
if(!filename)
return0xFFFFFFFF;
fd=open(filename,0xC1,0777);//open(filename,O_WRONLY|O_CREAT|O_EXCL,0777)
if(fd==0xFFFFFFFF)
return0xFFFFFFFF;
ret=pjl_ldwelcomescreen_internal2(0,1,pjl_getc_,write_to_file_,fd);//goeshere
if(!retpjl_unk_functionpjl_unk_function(filename))
pjl_process_ustatus_device_(20001);
close(fd);
remove(filename);
returnret;
}下面我們開始考察pjl_ldwelcomescreen_internal2()函數,但請注意,上面的文件最後會被關閉,並然後通過remove()調用完全刪除文件名。這意味著我們似乎只能暫時寫入該文件。
文件寫入原語現在,讓我們分析0x115470處的pjl_ldwelcomescreen_internal2()函數。由於使用了flag==0選項,因此,pjl_handle_ldlwelcomescreen_internal()函數最終將調用pjl_ldwelcomescreen_internal3()函數:
unsignedint__fastcallpjl_ldwelcomescreen_internal2(
intflag,
intone,
int(__fastcall*pjl_getc)(unsigned__int8*p_char),
ssize_t(__fastcall*write_to_file)(int*p_fd,char*data_to_write,size_tlen_to_write),
int*p_fd)
{
//[COLLAPSEDLOCALDECLARATIONS.PRESSKEYPADCTRL-'+'TOEXPAND]
bad_arg=write_to_file==0;
if(write_to_file)
bad_arg=pjl_getc==0;
if(bad_arg)
return0xFFFFFFFF;
if(flag)
returnpjl_ldwelcomescreen_internal3bis(flag,one,pjl_getc,write_to_file,p_fd);
returnpjl_ldwelcomescreen_internal3(one,pjl_getc,write_to_file,p_fd);//goeshereduetoflag==0
}我們花了一些時間,來逆向分析0x114838處的pjl_ldwelcomescreen_internal3()函數,以理解其內部機制。這個函數不僅很大,通過反編譯得到的代碼可讀性不是太高,但邏輯仍然很容易理解。
基本上,這個函數負責從客戶端讀取附加數據,並將其寫入前面打開的文件。
客戶端數據似乎是由另一個線程異步接收的,並保存到分配給pjl_ctx結構體的內存中。因此,pjl_ldwelcomescreen_internal3()函數每次從pjl_ctx結構體中讀取一個字符,並填充0x400字節的堆棧緩衝區。
1、如果接收到0x400字節數據,並且堆棧緩衝區已滿,則最終會將這些0x400字節寫入以前打開的文件中。然後,它重置堆棧緩衝區,並開始讀取更多數據以重複該過程。
2、如果接收到PJL命令的頁腳(“@PJL END data”),它將丟棄頁腳部分,然後將接收的數據(大小0x400字節)寫入文件,最後退出。
unsignedint__fastcallpjl_ldwelcomescreen_internal3(
intwas_last_write_success,
int(__fastcall*pjl_getc)(unsigned__int8*p_char),
ssize_t(__fastcall*write_to_file)(int*p_fd,char*data_to_write,size_tlen_to_write),
int*p_fd)
{
unsignedintcurrent_char_2;//r5
size_tlen_to_write;//r4
intlen_end_data;//r11
inthas_encountered_at_sign;//r6
unsignedintcurrent_char_3;//r0
intret;//r0
intcurrent_char_1;//r3
ssize_tlen_written;//r0
unsignedintret_2;//r3
ssize_tlen_written_1;//r0
unsignedintret_3;//r3
ssize_tlen_written_2;//r0
unsignedintret_4;//r3
intwas_last_write_success_1;//r3
size_tlen_to_write_final;//r4
ssize_tlen_written_final;//r0
unsignedintret_5;//r3
unsignedintret_1;//[sp+0h][bp-20h]
unsigned__int8current_char;//[sp+1Fh][bp-1h]BYREF
_BYTEdata_to_write[1028];//[sp+20h][bp+0h]BYREF
current_char_2=0xFFFFFFFF;
ret_1=0;
b_restart_from_scratch:
len_to_write=0;
memset(data_to_write,0,0x401u);
len_end_data=0;
has_encountered_at_sign=0;
current_char_3=current_char_2;
while(1)
{
current_char=0;
if(current_char_3==0xFFFFFFFF)
{
//getonecharacterfrompjl_ctx-pData
ret=pjl_getc(current_char);
current_char_1=current_char;
}
else
{
//apreviouscharacterwasalreadyretrieved,let'susethatfornow
current_char_1=(unsigned__int8)current_char_3;
ret=1;//success
current_char=current_char_1;
}
if(has_encountered_at_sign)
break;//exittheloopforever
//isitan'@'signforaPJL-specificcommand?
if(current_char_1!='@')
gotob_read_pjl_data;
len_end_data=1;
has_encountered_at_sign=1;
b_handle_pjl_at_sign:
//fromhere,current_char=='@'
if(len_to_write+130x400)//?
{
if(was_last_write_success)
{
len_written=write_to_file(p_fd,data_to_write,len_to_write);
was_last_write_success=len_to_write==len_written;
current_char_2='@';
ret_2=ret_1;
if(len_to_write!=len_written)
ret_2=0xFFFFFFFF;
ret_1=ret_2;
}
else
{
current_char_2='@';
}
gotob_restart_from_scratch;
}
b_read_pjl_data:
if(ret==0xFFFFFFFF)//error
{
if(!was_last_write_success)
returnret_1;
len_written_1=write_to_file(p_fd,data_to_write,len_to_write);
ret_3=ret_1;
if(len_to_write!=len_written_1)
return0xFFFFFFFF;//error
returnret_3;
}
if(len_to_write0x400)
__und(0);
//appenddatatostackbuffer
data_to_write[len_to_write++]=current_char_1;
current_char_3=0xFFFFFFFF;//resettoenforcereadinganothercharacter
//atnextloopiteration
//reached0x400bytestowrite,let'swritethem
if(len_to_write==0x400)
{
current_char_2=0xFFFFFFFF;//resettoenforcereadinganothercharacter
//atnextloopiteration
if(was_last_write_success)
{
len_written_2=write_to_file(p_fd,data_to_write,0x400);
ret_4=ret_1;
if(len_written_2!=0x400)
ret_4=0xFFFFFFFF;
ret_1=ret_4;
was_last_write_success_1=was_last_write_success;
if(len_written_2!=0x400)
was_last_write_success_1=0;
was_last_write_success=was_last_write_success_1;
}
gotob_restart_from_scratch;
}
}//endofwhile(1)
//wereachhereifweencounteredan'@'sign
//let'scheckitisavalid'@PJLENDDATA'footer
if((unsigned__int8)aPjlEndData[len_end_data]!=current_char_1)
{
len_end_data=1;
has_encountered_at_sign=0;//resetsowereaditagain?
gotob_read_data_or_at;
}
if(len_end_data!=12)//len('PJLENDDATA')=12
{
++len_end_data;
b_read_data_or_at:
//willgobacktothewhile(1)loopbutexitatthenext
//iterationdueto'break'andhas_encountered_at_sign==1
if(current_char_1!='@')
gotob_read_pjl_data;
gotob_handle_pjl_at_sign;
}
//wereachhereifall'PJLENDDATA'wasparsed
current_char=0;
pjl_getc(current_char);//read'\r'
if(current_char=='\r')
pjl_getc(current_char);//read'\n'
//writealltheremainingdata(len0x400),exceptthe'PJLENDDATA'footer
len_to_write_final=len_to_write-0xC;
if(!was_last_write_success)
returnret_1;
len_written_final=write_to_file(p_fd,data_to_write,len_to_write_final);
ret_5=ret_1;
if(len_to_write_final!=len_written_final)
return0xFFFFFFFF;
returnret_5;
}位於0xFEA18處的pjl_getc()函數,用於從pjl_ctx結構體中