技術專欄 #CVE #Pwn2Own #RCE #IoT

Your printer is not your printer ! - Hacking Printers at Pwn2Own Part I

Angelboy 2023-10-05

English Version, 中文版本

印表機近年來已成為企業內網中不可或缺的設備之一,功能也隨著科技的發展日益增多,除了一般的傳真及列印之外,也開始支援 AirPrint 等雲端列印服務,讓列印更加方便,直接使用行動裝置就可以輕鬆列印,更成為了 IoT 中,不可或缺的一環,正因為其便利,也常被用於列印公司內部機敏文件,使得在企業中印表機的安全性更加的重要。

而前年我們也在 Canon 及 HP 的印表機中發現了 Pre-auth RCE 的漏洞(CVE-2022-24673CVE-2022-3942) 及 Lexmark 發現漏洞(CVE-2021-44734),並在 Pwn2Own Austin 2021 中取得了 Canon ImageCLASS MF644Cdw、 HP Color LaserJet Pro MFP M283fdw 及 Lexmark MC3224i 的控制權,而成功獲得 Pwn2Own 中駭客大師(Master of Pwn) 的點數,這篇研究將講述 Canon 及 HP 漏洞的細節及我們的利用方式。

此份研究亦發表於 HITCON 2022CODE BLUE 2022,你可以從這裡取得投影片!

Printer

早期在使用印表機時,往往會需要使用 IEEE1284 或是 USB 的 Printer cable 來將印表機接上電腦,並且在使用時會額外裝上廠商所附的驅動程式。而現今的印表機已可以接上網路,並多了各式各樣的功能,通常只要將印表機接上區網,區網中的電腦就可以輕易地發現你所新安裝的印表機。

目前市面上的印表機預設都開了非常多的服務,不外乎就是為了讓列印更加方便,像是 FTP、AirPrint、Bonjour 等等服務。

Motivation

為何要研究印表機呢?

紅隊內網需求

過去我們團隊在執行紅隊演練過程中,印表機普遍出現於現代企業內網中,幾乎都會有一台以上,但往往是被忽略的一塊,也常常沒在更新。印表機本身也非常適合作為攻擊者的藏身處,通常很難被偵測出來。值得一提的是比較大型的企業也很有可能將其接上 AD,成為獲取機密資訊的入口。

Pwn2Own Austin 2021

另外一點是印表機在 2021 時,首次成為了 Pwn2Own Mobile 主要推動的目標之一,而我們剛好當時也準備再次挑戰 Pwn2Own 舞台,便決定一探究竟。

起初我們原本以為非常簡單,跟多數的 IoT 設備一樣,能輕易的找到 Command injection 問題,殊不知有不少印表機都是使用 RTOS,並非一般的 Linux 系統,但這更是驅動了我們挑戰它的決心。

本篇將會著重在較為精彩的 Canon 及 HP 部分,Lexmark 有機會再談談。

Analysis

剛開始研究的時候,我們參考了許多資料都是需要拆解硬體來分析,才能獲得 debug console,再用 dump memory 方式來獲取原始的 firmware。但最終我們採用了其他的方式,並沒有拆解任何一台印表機。

Canon

Firmware Extract

初始分析版本為 v6.03,我們一開始使用 binwalk 去解析它,但 firmware 是經過混淆的,我們並沒辦法直接解開。

圖: 經過混淆的 Canon ImageCLASS MF644Cdw fimware

我們這邊也嘗試過了 TREASURE CHEST PARTY QUEST: FROM DOOM TO EXPLOIT by Synacktiv 及 Hacking Canon Pixma Printers – Doomed Encryption by Contextis Research 的研究,但這次是完全不同的系列,我們無法使用同樣的方法解開混淆過的 firmware。

於是我們開始分析混淆過的 firmware 格式及內容。

我們大致上可以從混淆過的 firmware 看到,每個混淆過的 firmware 的開頭都會是 NCFW 這個 Magic,並帶有該 firmware 大小,而其他部分則是混淆過的資料。

於是我們開始猜想,也許這台印表機舊版本的 firmware 沒有混淆,直到某一版才開始混淆,如果可以抓到中介版本,可能有機會獲得解混淆的方法。而這個 Magic 也可以讓我們辨別是不是經過混淆的。

以下這個網址是透過官網或是擷取封包獲得的 firmware 下載網址

https://pdisp01.c-wss.com/gdl/WWUFORedirectTarget.do?id=MDQwMDAwNDc1MjA1&cmp=Z01&lang=EN

經過分析後,可以拆分為多個部分

約略可以歸納出下載網址的規則,我們可以藉由這個方法來載到所有版本的 firmware,當時我們載到的版本有

  • V2.01
  • V4.02
  • V6.03
  • V9.03
  • V10.02

而 v10.02 是幾周後會釋出的版本,可以先從這邊優先載到。載完所有版本後,我們發現該系列版本的 firmware 都是經過混淆的,無法從先前版本獲得解混淆的方法。但我們可以下載 Canon 其他系列的印表機,嘗試找找是否有類似的混淆演算法。載完約有 130 GB 大小。透過 grep 找 NCFWservicemode.html 可以找到未混淆的 firmware。

最後找到四組符合條件的 firmware,我們這邊選擇了 WG7000 系列的印表機來分析,並找到了疑似解混淆的函式。

很幸運的,藉由重寫這個函式,可以解出明文的 MF644Cdw firmware。

在解出 firmware 之後,必須找出 firmware 的 image base address,IDA 才能有效地辨別跟 reference。此處可透過常見的分析工具 rbasefind 來找 image base。

一開始找出的 base 為 0x40b0000,但丟進 IDA 後,卻發現大部分的函式debug message 的字串對映不起來。

如上圖所示,loc_4489AC08 應該指向函式名稱的字串,然而此地址卻不是正常的字串,而是被當成 code 區段,內容也不是字串,表示此位置並非真正位置,而是有些許的偏移,但正常 function 的解析沒甚麼太大問題。這邊可以先找一個已知函式名稱的函式和找到屬於他的函式名稱字串來做調整,找到其中差異的 offset 後,將 image base 調到正確位置就可以了。最終找到的 image base 為 0x40affde0。調整完後,可看到原本的函式已可正確識別函式名稱。

接下來就可以正常分析 firmware,而初步分析後可得知,Canon ImageCLASS MF644Cdw 架構如下

  • OS - DryOSV2
    • Customized RTOS by Canon
  • ARMv7 32bit little-endian
  • Linked with application code into single image
    • Kernel
    • Service

HP

HP 的 firmware 取得相對容易許多,我們可以透過 binwalk -Z 來獲得正確的 firmware,約略需要花 3-4 天左右的時間,而其他找 image base address 等步驟,則與 Canon 相同,此處就不贅述。經過初步分析後,HP Color LaserJet Pro MFP M283fdw 架構如下

  • OS
    • RTOS - Modify from ThreadX/Green Hills
  • ARM11 Mixed-endian
    • Code - little-endian
    • Data - Big-endian

Attack Surface

在現今市面上大多數的多功能事務機中,預設都會開啟一堆服務

Service Port Description
RUI TCP 80/443 Web interface
PDL TCP 9100 Page Description Language
PJL TCP 9100 Printer Job Language
IPP TCP 631 Internet Printing Protocol
LPD TCP 515 Line Printer Daemon Protocol
SNMP UDP 161 Simple Network Management Protocol
SLP TCP 427 Service Location Protocol
mDNS UDP 5353 Multicast DNS
LLMNR UDP 5355 Link-Local Multicast Name Resolution

一般來說,為了方便管理,通常都會開 RUI (web 介面) ,再來是 9100 Port 也是印表機常使用的 Port,主要會用來傳輸列印的資料。其他部分則會依照廠商不同而有所不同,不過上述所列的服務通常都會有,且預設大部分都是開啟的。在評估過這些服務後,決定注重在發現服務及 DNS 系列的協定,因為我們的長期經驗下來,常常觀察到 vendor 在開發這些服務時,往往是自行開發實作,而不是使用存在已久的 Open Source。但實際上來說,實作這些協定很容易出現問題的。我們當時主要分析的服務主要有 SLPmDNSLLMNR

接下來就以 Pwn2Own Austin 2021 作為 Case Study,來看看這些協定常會有哪些問題。

Hacking printers at Pwn2Own

Canon

Service Location Protocol

SLP 是一種服務發現協定,主要用於讓電腦快速找到印表機,過去在 ESXI 中,SLP 也常常出問題,而在 Canon 的 SLP 服務,主要由 Canon 自己實作,SLP 服務細節可參考 RFC2608。在我們分析 SLP 前必須先了解 SLP 封包大致上的結構

圖: SLP Packet Structure

這邊只需要關注function-id,此欄位會決定請求型態,也會決定 payload 部分的格式。而 Canon 只有實作 Service Request 及 Attribute Request 兩種。

在 Attribute Request (AttrRqst) 的請求中,允許使用者可以根據 service 及 scope 來獲得 attribute list。 scope 可以定義要找的範圍,如 canon 印表機。

Example:

而 Attribute request 結構大致如下

主要是長度(Length)及數值(Value)的組合,通常在 Parse 這種格式,很容易出問題,需要特別注意,而實際上 Canon 在 Parse 這個結構時就出了問題。

Vulnerability

Canon 在 parse scope list 時,會將跳脫字元轉換成 ASCII,例如 \41 會轉換成 A ,然而這個簡單轉換會有怎樣的問題呢? 我們來看一下 Pseudo code

int parse_scope_list(...){
    char destbuf[36];
    unsigned int max = 34;
    parse_escape_char(...,destbuf,max);
}

如上面程式碼所示,在 parse_scope_list 中,會先分配 36 bytes 的 destbuf 並且指定最大大小 34 到 parse_escape_char 中,這邊沒甚麼問題,讓我們來看一下 parse_escape_char

int __fastcall parse_escape_char(unsigned __int8 **pdata, _WORD *pdatalen, unsigned __int8 *destbuf, _WORD *max)
{
  unsigned int idx; // r7
  int v7; // r9
  int v8; // r8
  int error; // r11
  unsigned __int8 *v10; // r5
  unsigned int i; // r6
  int v12; // r1
  int v13; // r0
  unsigned int v14; // r1
  bool v15; // cc
  int v16; // r2
  bool v17; // cc
  unsigned __int8 v18; // r0
  int v19; // r0
  unsigned __int8 v20; // r0
  unsigned int v21; // r0
  unsigned int v22; // r0

  idx = 0;
  v7 = 0;
  v8 = 0;
  error = 0;
  v10 = *pdata;
  for ( i = (unsigned __int16)*pdatalen; i && !v7; i = (unsigned __int16)(i - 1) )
  {
    v12 = *v10;
    if ( v12 == ',' )
    {
      if ( i < 2 )
        return -4;
      v7 = 1;
    }
    else
    {
      if ( v12 == '\\' ) //----------------------[1]
      {
        if ( i < 3 )
          return -4;
        v13 = v10[1];
        v14 = v13 - '0';
        v15 = v13 - (unsigned int)'0' > 9;
        if ( v13 - (unsigned int)'0' > 9 )
          v15 = v13 - (unsigned int)'A' > 5;
        if ( v15 && v13 - (unsigned int)'a' > 5 )
          return -4;
        v16 = v10[2];
        v17 = v16 - (unsigned int)'0' > 9;
        if ( v16 - (unsigned int)'0' > 9 )
          v17 = v16 - (unsigned int)'A' > 5;
        if ( v17 && v16 - (unsigned int)'a' > 5 )
          return -4;
        if ( v14 <= 9 )
          v18 = 0x10 * v14;
        else
          v18 = v13 - 0x37;
        if ( v14 > 9 )
          v18 *= 0x10;
        *destbuf = v18; //-------------------[2]
        v19 = v10[2];
        v10 += 2;
        v20 = (unsigned int)(v19 - 0x30) > 9 ? (v19 - 55) & 0xF | *destbuf : *destbuf | (v19 - 0x30) & 0xF;
        *destbuf = v20;
        LOWORD(i) = i - 2;
        if ( !strchr((int)"(),\\!<=>~;*+", *destbuf) )
        {
          v21 = *destbuf;
          if ( v21 > 0x1F && v21 != 0x7F )
            return -4;
        }
        goto LABEL_40;
      }
      if ( strchr((int)"(),\\!<=>~;*+", v12) ) //-----------------------[3]
        return -4;
      v22 = *v10;
      if ( v22 <= 0x1F || v22 == 0x7F )
        return -4;
      if ( v22 != ' ' )
      {
        v8 = 0;
        goto LABEL_35;
      }
      if ( !v8 )
      {
        v8 = 1;
LABEL_35:
        if ( (unsigned __int16)*max <= idx ) //----------------------[4] 
        {
          error = 1;
          goto next_one;
        }
        if ( v8 )
          LOBYTE(v22) = 32;
        *destbuf = v22;
LABEL_40:
        ++destbuf;
        idx = (unsigned __int16)(idx + 1);
      }
    }
next_one:
    ++v10;
  }
  if ( error )
  {
    *max = 0;
    debugprintff(3645, 4, "Scope longer than buffer provided");
LABEL_48:
    *pdata = v10;
    *pdatalen = i;
    return 0;
  }
  if ( idx )
  {
    *max = idx;
    goto LABEL_48;
  }
  return -4;
}

可以看到 [3] 針對是沒有跳脫字元的處理,會在 [4] 檢查是否有超過最大長度,然而在有跳脫字元的處理中 [1] ,並沒有任何對長度的檢查,直接將轉換後的結果放到 destination buffer 中 [2],一旦給定的字串多數為跳脫字元的情況,就會造成 stack overflow。

在找到漏洞之後,第一件事就是先看看本身有甚麼保護,方便後續的利用。但分析了一下發現,Canon 印表機本身並沒有任何記憶體相關的保護。

Protection

  • No Stack Guard
  • No DEP
  • No ASLR

沒有 Stack Guard、沒有 DEP 也沒有 ASLR,可以說是 hacker friendly ! 如同回到 90 年代,一個 stack overflow 就可以打天下。接下來就如同過往的 Binary Exploitation 利用手法,找個地方放 shellcode 再覆蓋 return address 跳到 shellcode 就會有任意程式碼執行了! 最終我們找到了 BJNP 這個服務來放我們的 shellcode。

BJNP

BJNP 本身也是個服務發現協定,由 Canon 自己所設計,過去也曾經有許多漏洞,Synacktiv 也曾經利用該協定漏洞來獲得印表機控制權,這邊不多做細節上的介紹,更多細節可參考這篇,我們也用了類似的手法。 BJNP 本身會將可控的 session 資料放在已知的 global buffer 中,我們可用這個功能來將我們的 shellcode 放到一個固定的位置上,基本上也沒甚麼限制。

我們重新整理一下利用步驟

Exploitation Step

  • 使用 BJNP 將我們的 shellcode 放到固定的已知位置。
  • 觸發 SLP 的 stack overflow 並覆蓋 return address
  • 跳到我們的 shellcode 上執行程式碼。

Pwn2Own Austin 2021

通常 Pwn2Own 中會需要你證明已打下印表機,這邊可以自由選擇呈現方式,我們起初想要的是如同我們 exploit Lexmark 印表機一樣,直接將 logo 放到印表機的 LCD 螢幕上。

但在比賽前,我們花了很多時間在研究該怎麼把 logo 印到螢幕上,花在這邊時間可能比找洞跟寫 exploit 時間還要長,最後也因為時間上的因素,採取了比較保險的做法,直接改掉 Service Mode 字串,再印到螢幕上。

不過實際上印圖片到螢幕上並不難,其他隊伍有找到方法,有興趣的人可以嘗試看看。

Debug

看到這邊可能會有人想問這種環境如何 debug,實際上來說要 debug 通常有幾種方法:

  • 接上硬體獲得 debug console 後,用裡面的功能來 debug
  • 用舊的洞獲得程式碼執行後,裝上客製的 debugger

不過我們當時已更新到最新,該版本不存在舊的漏洞,需要降版本回去,而拆解硬體同樣也須額外的時間,但當時我們已經有漏洞了,時間上來說不太合成本。最後我們還是採用最傳統的 sleep debug 法去 debug。

在 ROP 或執行 shellcode 後,將結果印到網頁或其他可見的地方,然後呼叫 sleep 後,就可從網頁或其他讀出結果,最後再重開機,接下來就是不斷重複此流程。不過實際上更好的做法還是接上 debug console 會方便一點。

接下來講講 HP 印表機

HP

LLMNR 是與 mDNS 非常相似的一個協定,提供了區網中的域名解析功能,但比 mDNS 更單純一點,通常也會配合一些服務發現協定。這邊簡單介紹此機制:

在區網域名解析時,Client A 會先用 multicast 方式,尋找區網中 Client C 位置

在 Client C 接收到之後,則會回傳給 Client A,簡單實現了區網域名的解析

而基本上 LLMNR 大多建立在 DNS 封包格式 上,格式如下:

主要會是 header 加上 Queries 這種格式,Count 表示不同型態的 query 數。

而每個 DNS Query 都是由許多 label 組成,每個 label 都會像上圖中這樣,都是長度加上字串的組合,也有 Message Compression 機制,過去在處理這些地方時,非常容易出現問題,在 BlackHat 2021 的 THE COST OF COMPLEXITY:Different Vulnerabilities While Implementing the Same RFC 中,也提到了類似的問題。

Vulnerability

我們回頭來看一下 HP 的實作:

int llmnr_process_query(...){
    char result[292];
    consume_labels(llmnr_packet->qname,result,...);
    ...
}

這邊可以看到 HP 在處理 LLMNR 封包時,會將一個固定 buffer 傳入,用來放處理後的結果,而 consume_lables 則是主要用來處理 dns labels。

int __fastcall consume_labels(char *src, char *dst, llmnr_hdr *a3)
{
  int v3; // r5
  int v4; // r12
  unsigned int len; // r3
  int v6; // r4
  char v7; // r6
  bool v8; // cc
  int v9; // r0
  unsigned __int8 chr; // r6
  int result; // r0

  v3 = 0;
  v4 = 0;
  len = 0;
  v6 = 0;
  while ( 1 )
  {
    chr = src[v3]; //-------------[1]
    if ( !chr )
      break;
    if ( (int)len <= 0 )
    {
      v8 = chr <= 0xC0u;
      if ( chr == 0xC0 )
      {
        v9 = src[v3 + 1];
        v6 = 1;
        v3 = 0;
        src = (char *)a3 + v9;
      }
      else
      {
        len = src[v3++];
        v8 = v4 <= 0;
      }
      if ( !v8 )
        dst[v4++] = '.';
    }
    else
    {
      v7 = src[v3++];
      len = (char)(len - 1);
      dst[v4++] = v7; //----------[2]
    }
  }
  result = v3 + 1;
  dst[v4] = 0;
  if ( v6 )
    return 2;
  return result;
}

而在 consume_labels 中的 [1] 會先取得 label 長度,接著根據型態去處理,而在[2] 則是處理一般長度的情況,此處並沒有對長度做檢查,就直接將 label 寫進 dst buffer 中,導致了 stack overflow,到此處我們原以為差不多結束了,接下來應該如同 Canon 類似的方法就可以 Exploit 了。然而當我們在寫 Exploit 時發現 HP 比 Canon 多了一些保護機制。

Protection

  • No Stack Guard
  • XN(DEP)
  • Memory Protect Unit (MPU)
  • No ASLR

在 HP 印表機中,多了 XN 及 MPU 的記憶體保護措施,另外這個漏洞也有了更多的限制。我們只能 overflow 約 0x100 bytes不能有 null 字元,這大幅限制了我們的 ROP,使得我們沒辦法單靠 ROP 做到後續行動,需要另外找其他的漏洞或其他方法才能達成我們的目標。在一段時間後,我們開始思考,HP 印表機是如何去實作 XN(DEP)MPU 的? 我們回顧一下 HP RTOS:

  • 所有 Service code 及 Kernel Code 都在同一個 Binary 中。
  • 大多數的 task 都跑在同一個記憶體空間底下(沒有 Process isolation),也幾乎都跑在高權限模式

看完以上兩點,會想到是不是理解 HP RTOS 中的 MMU 及 MPU 就可以繞過呢?

我們來看一下 HP RTOS MMU 機制

MMU in HP M283fdw

在 HP M283fdw 中使用的是一階層的 Translation table 來做 Address translation ,每個 translation table entry 都表示 1MB 的 Section,而 Translation table 則是固定在 0x4003c000 這個位置上

而每個 translation table entry 都會對應到 physical address 及該 section 的權限,CPU 就是根據這些內容決定執行權限、記憶體內容修改權限,如果我們可以修改 translation table entry 的內容就可以更改記憶體權限,也可以透過它來 Mapping 任意 Physical address,這邊跟權限有關的主要會有 AP APX 跟 XN。

我們可以從前述的漏洞中注意到,在有 stack overflow 且也跑在高權限下,就可通過 ROP 修改 translation table entry,但當我們對嘗試直接對 translation table 做寫入後,結果

造成印表機 Crash,查了一下發現是 memory fault exception,主要造成原因就是因為 Memory Protect Unit (MPU) 有對該記憶體區段做保護。

好,那我們就來看看 MPU 的機制。

MPU in HP M283fdw

MPU 主要功能是把 memory 拆分成好幾個 region 並定義每個 region 的權限,與 MMU 是完全不同的機制,很常出現在 IoT 設備中。 HP 則是在開機就會啟用,並將每個 region 定義好權限,因此無法自己操作 page table。

在長時間逆向及參考 ARM Manual 之後,我們發現事實上只要清空 MPU_CTRL 就可關閉 MPU,在經過逆向後 HP M283fdw 的 MPU_CTRL 位置是在 0xE0400304,這邊稍微跟 ARM 的 spec 有點不同,不太確定原因就是了。

Exploitation

在了解 HP 的 MMU 及 MPU 機制後,我們可輕易地利用 ROP 來關閉 MPU,並成功修改 translation table entry,我們可以任意的修改任何 serivce 的程式碼,這邊我們最後選擇了 Line Printer Daemon(LPD) 這個服務來修改,將它修改成後門: 讀入更多的 Payload 到指定的位置上,最終執行我們送過去的 shellcode。

但有一點必須特別注意,覆蓋完 translation table entry 跟 LPD 的 code 後,務必要 flush TLB清掉 I-cache 和 D-cache 不然很有可能還是跑在舊有的程式碼上面導致 exploit 失敗。

Flush TLB

flush_tlb:
    mov r12, #0
    mcr p15, 0, r12, c8, c7, 0

Invalidate I-cache

disable_icache:
    mrc p15, 0, r1, c1, c0, 0
    bic r1, r1, #(1 << 12)
    mcr p15, 0, r1, c1, c0, 0

我們重新整理了一下利用步驟

Exploitation Step

  • 首先先觸發 LLMNR 的 stack overflow
  • 利用有限的 ROP 關閉 MPU
  • 利用 ROP 改掉 translation table entry 獲得讀寫執行權限
  • Flush TLB
  • 改掉 LPD service 的程式碼
  • 清掉 I-cache 和 D-cache
  • 使用改過的 LPD 讀我們的 shellcode 後並執行

Pwn2Own Austin 2021

到可以執行 shellcode 時,我們只剩一週時間,我們最後選擇跟 Canon 一樣使用改字串顯示 Pwned by DEVCORE 到 LCD 上。

而幸運的是,我們第一次嘗試就成功了:)

在這之後我們也嘗試了直接把後門改成 debug console 上面,方便利用許多功能,例如查看記憶體資訊,播放音樂等等功能,F-Secure Labs 在比賽時就使用播放音樂這個功能來呈現,非常有趣,可以到這裡看當時的情況。

Result

在 Pwn2Own Austin 2021 中,我們打下其他設備跟印表機後最終獲得了第二名,以這次來說獲得不錯經驗,也學到了一些新東西。

而對於一般用戶們,有什麼方法可以避免印表機被當作攻擊目標甚至是跳板呢?

Mitigation

Update

首先就是定期更新,上述的印表機都已有 patch,這邊是很常被大家忽略的一部分,我們很常看到印表機好幾年了都沒更新,甚至直接預設密碼放著,很容易就被當成目標。

Disable unused service

另外一點就是盡可能關掉沒在用的服務, 大部分的印表機預設開啟過多平常根本不會用的服務,我們甚至認為可以關掉 discovery 服務,只要開你要用的就好了。

Firewall

更好的做法可以再加上 firewall,大部分印表機也都有提供相關功能。

Summary

事實上,我們獲得 shellcode 執行後,除了印東西在 LCD 外,我們可以藉由印表機來竊取機密資訊,不論是機密文件或是一些 credential,印表機也是個平行移動 (Lateral Movement) 的點,而且很難被偵測到,是紅隊中非常好的目標。另外很多印表機上的發現服務系列的協定或是 DNS 系列的協定很常出問題,如果想找類似印表機或其他 IoT 設備的漏洞,也許可以優先朝這個方向看看。

To be continue

最後,我們在去年 Pwn2Own Toronto 2022 中,也在印表機系列中找到幾個漏洞,我們也將會在近期發佈詳細資訊,敬請期待 Part II

Reference