天天看點

Nftables棧溢出漏洞(CVE-2022-1015)複現

作者:網絡安全菜鳥

背景介紹

Nftables

Nftables 是一個基于核心的包過濾架構,用于 Linux 作業系統中的網絡安全和防火牆功能。nftables 的設計目标是提供一種更簡單、更靈活和更高效的方式來管理網絡資料包的流量。

鈎子點(Hook Point)

鈎子點的作用是攔截資料包,然後對資料包進行修改,比較,丢棄和放行等操作。

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.0175185914260716in"}

// include/uapi/linux/netfilter_ipv4.h

#define NF_IP_PRE_ROUTING    0 /* After promisc drops, checksum checks. */
#define NF_IP_LOCAL_IN       1 /* If the packet is destined for this box. */
#define NF_IP_FORWARD        2 /* If the packet is destined for another interface. */
#define NF_IP_LOCAL_OUT      3 /* Packets coming from a local process. */
#define NF_IP_POST_ROUTING   4 /* Packets about to hit the wire. */
#define NF_IP_NUMHOOKS       5           

Nftables的架構

Nftables由四部分組成

  • • table(表):用于指定網絡協定的類型,如ip,ip6,arp等
  • • chains(鍊):用于指定流量的類型,如流入的流量或者是流出的流量并可以指定網絡接口,如本地回環接口或者以太網接口等。
  • • rules(規則):規則是用于過濾資料包所依據的規則,例如檢查協定、來源、目的地、端口等規則。
  • • express(表達式):表達式則是具體的操作。

圖檔來源于https://blog.dbouman.nl/2022/04/02/How-The-Tables-Have-Turned-CVE-2022-1015-1016/

使用非常形象的圖描述,如下

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.905547900262467in"}

表達式(express)

表達式是對一個資料包具體的操作,這裡大緻介紹後續需要用到的表達式。

nft_payload

nft_payload用于将資料包的值拷貝到寄存器中

struct nft_payload {
    enum nft_payload_bases base:8;
    u8   offset;
    u8   len;
    u8   dreg;
};           
  • • base:資料包類型
  • • offset:資料包起始位置的偏移
  • • len:拷貝的長度
  • • dreg:目的寄存器

其中base的類型由enum nft_payload_bases指定

/* include/uapi/linux/netfilter/nf_tables.h */
/**
 * enum nft_payload_bases - nf_tables payload expression offset bases
 *
 * @NFT_PAYLOAD_LL_HEADER: link layer header
 * @NFT_PAYLOAD_NETWORK_HEADER: network header
 * @NFT_PAYLOAD_TRANSPORT_HEADER: transport header
 * @NFT_PAYLOAD_INNER_HEADER: inner header / payload
 */
enum nft_payload_bases {
    NFT_PAYLOAD_LL_HEADER, //鍊路層
    NFT_PAYLOAD_NETWORK_HEADER, //網絡層
    NFT_PAYLOAD_TRANSPORT_HEADER, //傳輸層
    NFT_PAYLOAD_INNER_HEADER, //資料包内部
};           

下面這個例子則是将傳輸層的包偏移16個位元組的位置,取出兩個位元組的内容存放到目的寄存器中,該寄存器的編号為2

base = NFT_PAYLOAD_TRANSPORT_HEADER
offset = 16 -> the checksum is 16 bytes away from the start of the TCP header
len = 2 -> the checksum is 2 bytes
dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00)           

nft_payload_set

nft_payload_set則是與nft_payload相反,該表達式是将指定寄存器的值存放到資料包裡面

/* include/net/netfilter/nf_tables_core.h */
struct nft_payload_set {
    enum nft_payload_bases base:8;
    u8   offset;
    u8   len;
    u8   sreg;
    u8   csum_type;
    u8   csum_offset;
    u8   csum_flags;
};           

與nft_payload不同的是多了校驗和的可選選項

nft_cmp_expr

nft_cmp_expr表達式則是用于比較,通常用于判斷資料包的端口号是否是需要符合要求。

struct nft_cmp_expr {
    struct nft_data  data;
    u8   sreg;
    u8   len;
    enum nft_cmp_ops op:8;
};           
  • • data:用于設定比較的常量值
  • • sreg:源寄存器,可以認為是資料包取出的内容
  • • len:比較的長度
  • • op:比較的操作,具體操作類型如下所示
<!-- -->           
/**
 * enum nft_cmp_ops - nf_tables relational operator
 *
 * @NFT_CMP_EQ: equal
 * @NFT_CMP_NEQ: not equal
 * @NFT_CMP_LT: less than
 * @NFT_CMP_LTE: less than or equal to
 * @NFT_CMP_GT: greater than
 * @NFT_CMP_GTE: greater than or equal to
 */
enum nft_cmp_ops {
    NFT_CMP_EQ,
    NFT_CMP_NEQ,
    NFT_CMP_LT,
    NFT_CMP_LTE,
    NFT_CMP_GT,
    NFT_CMP_GTE,
};           

nft_bitwise

nft_bitwise用于對資料包進行比特級别的操作。例如移位,掩碼設定等。

struct nft_bitwise {
    u8   sreg;
    u8   dreg;
    enum nft_bitwise_ops op:8;
    u8   len;
    struct nft_data  mask;
    struct nft_data  xor;
    struct nft_data  data;
};           
  • • sreg:源寄存器
  • • dreg:目的寄存器,用于存放最後的結果
  • • op:指定具體的比特操作,具體操作如下
<!-- -->           
/**
 * enum nft_bitwise_ops - nf_tables bitwise operations
 *
 * @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and
 *                    XOR boolean operations
 * @NFT_BITWISE_LSHIFT: left-shift operation
 * @NFT_BITWISE_RSHIFT: right-shift operation
 */
enum nft_bitwise_ops {
    NFT_BITWISE_BOOL,
    NFT_BITWISE_LSHIFT,
    NFT_BITWISE_RSHIFT,
};           
  • • mask:當op被指定為NFT_BITWISE_BOOL時,sreg的值會與mask中指定的值進行掩碼設定操作。并将結果存放到dreg中
  • • xor:當op被指定為NFT_BITWISE_BOOL時,sreg的值會與xor中指定的值進行掩碼設定操作。并将結果存放到dreg中
  • • data:當op被指定為NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT時,data需要被指定移位的數值。

寄存器(register)

在Nftables中是以寄存器作為存儲區,用于存放一段連續的記憶體,現在Nftables版本每個寄存器的值存放4位元組資料,而舊版的Nftables的每個寄存器是存放16個位元組的資料,為了保持相容性,4位元組的寄存與16位元組的寄存器都被保留。寄存器的枚舉值如下所示

enum nft_registers {
    NFT_REG_VERDICT, //判定寄存器
    NFT_REG_1,
    NFT_REG_2,
    NFT_REG_3,
    NFT_REG_4,
    __NFT_REG_MAX,

    NFT_REG32_00 = 8,
    NFT_REG32_01,
    NFT_REG32_02,
    ...
    NFT_REG32_13,
    NFT_REG32_14,
    NFT_REG32_15,
};           
Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="2.037557961504812in"}

其中NFT_REG_VERDICT被稱之為判斷寄存器,這個寄存器比較特殊,是用于判定每個資料包需要怎麼處理。判定的類型如下

  • • NFT_CONTINUE:允許資料包通過防火牆
  • • NFT_BREAK:跳過剩餘的規則表達式
  • • NF_DROP:直接丢棄資料包
  • • NF_ACCEPT:接收資料包
  • • NFT_GOTO:跳轉到其他鍊執行
  • • NFT_JUMP:跳轉到其他鍊執行,若其他鍊将該資料包判定為NFT_CONTINUE則傳回目前鍊

libmnl與libnftnl

由于Nftables處于核心,需要從使用者層向核心發送消息去設定需要攔截資料包的屬性,人工構造成本較大,是以使用現成的庫libmnl與libnftnl

環境搭建

環境版本

  • • ubuntu 20.04
  • • qemu-system-x86_64 4.2.1
  • • Linux-5.17源碼

設定編譯選項

cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17
sudo gedit .config
#将下列選項設定為y
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y
make -j32 bzImage #編譯

#安裝依賴庫
sudo apt-get install libmnl-dev 
sudo apt-get install libnftnl-dev           

漏洞驗證

若運作exp顯示超過邊界則代表沒有漏洞

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="2.4473501749781277in"}

若exp正常運作則代表漏洞

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="4.2338702974628175in"}

漏洞分析

源碼分析

nft_parse_register_load

nft_cmp_expr:op=NFT_CMP_EQ sreg=8 data=IPPROTO_TCP。該表達式是一個比較的表達式,用于比較下标為8的寄存器中的資料是否為TCP的協定。那麼如何将下表為8的寄存器轉化為核心中寄存器的記憶體位置,則需要以來下面列舉的函數。

nft_parse_register_load函數就是将使用者設定的寄存器的下标轉化為核心寄存器的下标,然後存儲在源寄存器中。

File: net\netfilter\nf_tables_api.c
9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
9326: {
9327:  u32 reg;
9328:  int err;
9329: 
9330:  reg = nft_parse_register(attr); //用于提取資料包中的寄存器的下标,并轉化為Nftables中寄存器的下标
9331:  err = nft_validate_register_load(reg, len); //用于檢驗寄存器下表的合法性,漏洞點
9332:  if (err < 0)
9333:   return err;
9334: 
9335:  *sreg = reg; //然後将寄存器的下标值存儲在源寄存器中
9336:  return 0;
9337: }           

nft_parse_register

nft_parse_register函數用于将使用者設定的寄存器下标轉化為核心中寄存器的下标。

File: net\netfilter\nf_tables_api.c
9278: static unsigned int nft_parse_register(const struct nlattr *attr)
9279: {
9280:  unsigned int reg;
9281: 
9282:  reg = ntohl(nla_get_be32(attr)); //提取資料包的寄存器下标,比如上述例子為8
9283:  switch (reg) {
        //0 - 4是16位元組寄存器
9284:  case NFT_REG_VERDICT...NFT_REG_4: 
9285:   return reg * NFT_REG_SIZE / NFT_REG32_SIZE;  //reg * 4
9286:  default:
        //由于4位元組寄存器起始下标為8,是以要減去起始下标
9287:   return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg  - 4
9288:  }
9289: }           

nft_validate_register_load

nft_validate_register_load函數則是用于校驗下标是否有問題,但是這個檢驗存在整型溢出的問題。reg是枚舉值,而枚舉通常會被編譯為int類型。len代表資料包的長度。

  • • 正常情況下:reg = 100,那麼套入校驗則為100 * 4 + 0x10 = 0x1a0 > 0x50,那麼會檢驗出寄存器下标存在問題
  • • 漏洞情況:reg = 0xffffffff(int情況下的最大值),那麼逃入檢驗則為0xffffffff * 4 + 0x10 = 0x40000000c,由于int最大值為0xffffffff,那麼最高4個比特會被舍棄,那麼最後得到的值為0x0000000c,此時0xc < 0x50,就可以繞過檢驗。那麼繞過檢驗後就會執行* sreg = reg,此時reg = 0xffffffff,就會導緻*sreg = 0xff
<!-- -->           
File: net\netfilter\nf_tables_api.c
9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
9314: {
9315:  if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4則報錯
9316:   return -EINVAL;
9317:  if (len == 0) //長度為0則報錯
9318:   return -EINVAL;
9319:  if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50則報錯,存在整型溢出漏洞
9320:   return -ERANGE;
9321: 
9322:  return 0;
9323: }           

nft_do_chain

每一個被攔截的資料包都需要經過鍊上的表達式進行處理,而鍊處理的函數則為nft_do_chains,這個函數會提取出相應的表達式,最後調用expr_call_ops_eval函數進行處理。

File: net\netfilter\nf_tables_core.c
197: unsigned int
198: nft_do_chain(struct nft_pktinfo *pkt, void *priv)
199: {
        ...
224:  for (; rule < last_rule; rule = nft_rule_next(rule)) {
225:   nft_rule_dp_for_each_expr(expr, last, rule) {
226:    if (expr->ops == &nft_cmp_fast_ops)
227:     nft_cmp_fast_eval(expr, ®s);
228:    else if (expr->ops == &nft_bitwise_fast_ops)
229:     nft_bitwise_fast_eval(expr, ®s);
230:    else if (expr->ops != &nft_payload_fast_ops ||
231:      !nft_payload_fast_eval(expr, ®s, pkt))
232:     expr_call_ops_eval(expr, ®s, pkt);
233: 
234:    if (regs.verdict.code != NFT_CONTINUE)
235:     break;
236:   }
        ...           

expr_call_ops_eval

expr_call_ops_eval函數則是根據不同的表達式選擇不同的處理函數,例如若該資料包需要經過nft_payload的表達式處理,則會調用nft_payload_eval。

File: net\netfilter\nf_tables_core.c
161: static void expr_call_ops_eval(const struct nft_expr *expr,
162:           struct nft_regs *regs,
163:           struct nft_pktinfo *pkt)
164: {
165: #ifdef CONFIG_RETPOLINE
166:  unsigned long e = (unsigned long)expr->ops->eval;
167: #define X(e, fun) \
168:  do { if ((e) == (unsigned long)(fun)) \
169:   return fun(expr, regs, pkt); } while (0)
170: 
171:  X(e, nft_payload_eval);
172:  X(e, nft_cmp_eval);
173:  X(e, nft_counter_eval);
174:  X(e, nft_meta_get_eval);
175:  X(e, nft_lookup_eval);
176:  X(e, nft_range_eval);
177:  X(e, nft_immediate_eval);
178:  X(e, nft_byteorder_eval);
179:  X(e, nft_dynset_eval);
180:  X(e, nft_rt_get_eval);
181:  X(e, nft_bitwise_eval);
182: #undef  X
183: #endif /* CONFIG_RETPOLINE */
184:  expr->ops->eval(expr, regs, pkt);
185: }           

nft_payload_eval

這裡可以看到regs存放在棧上面,dest這個變量值是通過®s->data[priv->dreg]取出來的,而priv->dreg則是通過上述的nft_parse_register_load函數進行提取的,那麼這裡就存在一個非常明顯的數組越界的漏洞。

File: net\netfilter\nft_payload.c
121: void nft_payload_eval(const struct nft_expr *expr,
122:         struct nft_regs *regs,
123:         const struct nft_pktinfo *pkt)
124: {
125:  const struct nft_payload *priv = nft_expr_priv(expr);
126:  const struct sk_buff *skb = pkt->skb;
127:  u32 *dest = ®s->data[priv->dreg];
        ...
165:  if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷貝資料
166:   goto err;
167:  return;
168: err:
169:  regs->verdict.code = NFT_BREAK;
170: }           

是以整型溢出結合越界就能夠使我們通路到核心棧上的其他資料,如下圖所示。

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="6.234479440069991in"}

漏洞利用

漏洞利用分析

現在我們擁有了通路核心棧上其它位址的能力了,想要做到任意代碼執行則需要考慮下列幾種情況

  • • 由于傳回位址存在在棧上,需要判斷數組越界是否能夠到達傳回位址的位置
  • • 如何通過數組越界改寫傳回位址
  • • 由于需要進行任意代碼執行,那麼需要用到核心函數,則需要得到核心的程式基位址才能夠根據函數偏移位址計算出函數的實際位址

由于表達式都會對寄存器空間進行操作,是以可以使用表達式對記憶體空間進行讀寫操作。

nft_bitwise表達式可以控制源寄存器和目的寄存器,那麼采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,是以可以利用nft_bitwise進行越界讀,此時需要分析該數組越界讀的邊界的大小是多少。這裡需要注意的是由于len是sreg與dreg共同擁有的,為了dreg不越界,這裡的長度最大值隻能為0x40而不能為0xff,因為擁有16個寄存器,每個寄存器的值為4個位元組,是以16 * 4 = 64 = 0x40

  • • 上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff * 4 = 0x3fc;由于可以拷貝0x40個位元組的長度,是以0x3fc + 0x40 = 0x43c。
  • • 下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 * 4 = 0x3c0

核心位址洩露

接着檢視regs偏移0x3c0處的位址資訊,結果發現在該片區域存在一個明顯的核心位址,是以若能将這個位址進行洩露,我們就能擷取核心的基位址。

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.313192257217848in"}

傳回位址覆寫

由于需要建構的payload比較長,而我們如果利用nft_wise最多隻能寫入0x43c - 0x3c0 = 0x7c的長度,是遠遠不夠的,是以對傳回位址進行覆寫時不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的長度len,由于我們隻需要考慮一個寄存器的值,是以該寄存器的長度最大可以達到0xff。是以我們可以在位址更低的位置去搜尋有無可以覆寫的傳回位址。

可以發現在0x360的位址處也有一個核心的代碼段位址

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.2392869641294837in"}

并且可以發現該函數主要是處理udp包的發送

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="2.555692257217848in"}

為了檢驗該位址是否能夠修改程式的執行流程,可以使用一個方法,将該位址的值修改為非法值并觀察核心是否會崩潰,這裡将位址的内容修改為0x1122334455667788,接着運作程式。

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="2.0175787401574805in"}

可以看到核心報錯的資訊顯示RIP的位址為剛剛我們修改的位址,是以該位址可以作為被劫持程式執行流程的位址。

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.0425896762904636in"}

exp分析

現在我們已經具有了兩個利用條件

  • • 洩露核心的程式基位址
  • • 找到可以劫持程式執行流程的位址值

位址洩露

利用nft_bitwise洩露位址,這裡注意的是在使用nft_bitwise洩露位址時,需要将data值設定為0,這樣就不會進行移位而導緻我們的核心位址被修改存儲,最後将洩露的位址值放置在NFT_REG32_05下标的寄存器中

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="1.047253937007874in"}

接着使用nft_set_payload将udp資料包的值修改為NFT_REG32_05寄存器的值,最後取出udp資料包的值,擷取核心程式位址值

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="1.6994411636045494in"}

傳回位址覆寫

利用nft_payload完成傳回位址的覆寫

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.1172397200349957in"}

在資料包中将payload填充進去,這裡需要說明一下如何在核心中拿到shell權限

  • • 首先需要在核心中拿到root權限,需要調用commit_creds(prepare_kernel_cred (0))的核心函數擷取新的憑證結構,而該結構的uid = 0 ,gid = 0即為root權限
  • • 其次需要切換命名空間,由于在普通使用者下是無法直接調用Nftables的,因為需要管理者的權限,是以在普通使用者下需要新開辟一個命名空間,使得該空間與正常的空間隔離,此時才能夠正常執行Nftales。那麼如果逃逸這段命名空間則需要進行命名空間的切換,則依賴于switch_task_namespace函數,可以将命名空間切換為root的命名空間
  • • 最後則是實作從核心态切換到使用者态,由于我們是在核心空間拿到權限,而我們需要在使用者态執行,是以需要完成狀态的轉換,該狀态轉換依賴于swapgs_restore_regs函數
Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="3.350757874015748in"}

漏洞修複

更新檔則是新增一條判斷條件,屬于4位元組寄存器的下标單獨處理,而不在16位元組寄存器以及4位元組寄存器的範圍内的下标都進行報錯處理

Nftables棧溢出漏洞(CVE-2022-1015)複現

{width="5.833333333333333in" height="4.722735126859143in"}

總結

Nftables棧溢出漏洞攻擊流程

  • • 首先利用nft_bitwise進行核心基位址的洩露。
  • • 其次是利用nft_payload改寫傳回位址,并将提權代碼注入進去。
  • • 最後等到代碼被觸發。

Nftables棧溢出漏洞利用的限制

  • • 不同的核心版本的核心棧布局幾乎不同,是以不同版本之間的利用手法相差較大,是以漏洞的利用十分依賴于核心版本,針對不同的版本需要做出針對性的漏洞利用的exp編寫。差别存在于核心棧中存在的核心代碼段位址的偏移不同,例如有些核心代碼段位址偏移距離regs太大,導緻無法利用漏洞進行洩露或者改寫。

參考連結

https://blog.dbouman.nl/2022/04/02/How-The-Tables-Have-Turned-CVE-2022-1015-1016/

https://arthurchiao.art/blog/conntrack-design-and-implementation/

https://zhuanlan.zhihu.com/p/542451347

http://anatomic.rip/cve-2022-1015/

http://anatomic.rip/netfilter_nf_tables/