用 S2E 和 Kaitai Struct 針對性地處理檔案解析器(二)

Editor發表於2017-11-14

用 S2E 和 Kaitai Struct 針對性地處理檔案解析器(二)

用readelf實驗一下

我們終於可以開始readelf部分的實驗了。 在我們開始之前,請修改S2E配置檔案,僅啟用以下的外掛:

BaseInstructions

HostFiles

VMI

TranslationBlockCoverage

ModuleExecutionDetector

ForkLimiter

ProcessExecutionDetector

LinuxMonitor

我們還必須修改bootstrap.sh。

在${S2EGET} “readelf”下新增$

{S2EGET}“small_exec.elf”以便將測試用例複製到客戶機。為了使用我們的測試用例,在prepare_inputs函式中,將truncate

-s 256 $ {SYMB_FILE}替換為cp small_exec.elf $ {SYMB_FILE}。

還不用替換symbfile命令; 讓我們先來看一下readelf如何在一個完全符號化的檔案上執行。

執行S2E一分鐘左右,然後結束程式。 你應該看到很多分叉的情況(我這裡是136種情況)。 讓我們生成程式碼覆蓋資訊:

# The actual disassembler isn't important

s2e coverage basic_block --disassembler=binaryninja readelf_kaitai

這些分支情況發生在哪?

由於readelf呼叫在符號化資料時呼叫了printf,所以libc中有很多。 readelf 自身的分支呢?

下面的圖片顯示了readelf中的兩個函式的片段:process_section_headers和init_dwarf_regnames。

綠色的部分表示由S2E執行的塊。 分支節點受到的約束已由註釋說明(KLEE中的KQuery格式):

用 S2E 和 Kaitai Struct 針對性地處理檔案解析器(二)

readelf's process_section_headers 程式碼覆蓋

用 S2E 和 Kaitai Struct 針對性地處理檔案解析器(二)

readelf's init_dwarf_regnames 程式碼覆蓋

當檢查到下列情況也會發生分叉:

如果輸入檔案是一份存檔

資料編碼(小端或大端位元組序)

section header 表的檔案偏移量

如果每個部分的sh_link和sh_info值都是有效的

還有許多其他的地方!眼下只對留下那些與ELF頭部的e_machine欄位有關的程式路徑。編輯bootstrap.sh並用./s2e_kaitai_cmd

${SYMB_FILE}替換${S2ECMD} symbfile

${SYMB_FILE}。現在重新執行S2E一分鐘。在執行期間,分支情況僅限於get_machine_name和init_dwarf_regnames函式,這兩個函式都是取決於e_machine的值的switch語句。成功了!

讓我們嘗試在ELF檔案中換一個不同的欄位 -section header 的sh_type欄位。不像e_machine欄位,只會在ELF檔案中出現一次。sh_type可以在整個檔案中出現多次(取決於ELF檔案中section的數量)。

我們必須將S2E執行狀態和輸入檔案的起始地址傳播到ELF宣告中的相對應的屬性中。這次我們必須將params

spec新增到section_header型別中。

type屬性定義為無符號的4位元組列舉型別,因此我們必須將其更改為4位元組的陣列型別,以便我們可以使用s2e_make_symbolic:

# Elf(32|64)_Shdr

section_header:

params:

- id: s2e_state

- id: start_addr

seq:

# sh_name

- id: name_offset

type: u4

# sh_type

- id: type

size: 4

process: s2e_make_symbolic(s2e_state, start_addr, _io.pos, "sh_type")

# ...

我們還必須確保將這兩個引數傳遞給SectionHeader的建構函式。 section頭可以在section_headers例項下找到:

# The original section_headers

section_headers:

pos: section_header_offset

repeat: expr

repeat-expr: qty_section_header

size: section_header_entry_size

type: section_header

# Redefined for symbolic execution

section_headers:

pos: section_header_offset

repeat: expr

repeat-expr: qty_section_header

size: section_header_entry_size

type: section_header(s2e_state, start_addr)

注意section_headers被宣告為“例項規範”。

這意味著section_headers只能根據需要將要解析section頭部的函式編譯為一個函式。

因此,我們必須訪問section_headers以強制解析它們。

為此,我們必須修改s2e-config.lua中的make_elf_symbolic函式:

function make_symbolic_elf(state, start_addr, buffer)

-- ...

-- This will kick-start the parser. However, now we do care about the final

-- result, because we must access the section headers to force them to be

-- parsed

local elf_file = Elf(state, start_addr, KaitaiStream(ss))

-- This will kick-start the section header parser

_ = elf_file.header.section_headers

end

執行ksc再次重新生成elf.lua。 在我們重新執行S2E之前,我們來看下elf.lua。 特別是在section_headers中的get方法中解析的section頭部:

function Elf.EndianElf.property.section_headers:get()

-- ...

for i = 1, self.qty_section_header do

self._raw__m_section_headers[i] = \

self._io:read_bytes(self.section_header_entry_size)

local _io = KaitaiStream(stringstream(self._raw__m_section_headers[i]))

self._m_section_headers[i] = Elf.EndianElf.SectionHeader(self.s2e_state,

self.start_addr,

_io, self,

self._root,

self._is_le)

end

-- ...

end

注意到ksc建立一個區域性變數_io,它被傳遞給SectionHeader建構函式。 這個_io變數包含最終將被轉換成SectionHeader物件的原始資料。 不幸的是,這會導致s2e_make_symbolic出現處理規範方面的問題。

回想一下,解析器的當前位置(_io.pos)被傳遞給s2e_make_symbolic處理規範。 但是糟糕的是當建立本地_io流時,這個地址將清零,因此符號化的時候使用這個地址會造成錯誤的記憶體地址。 不過,我們可以通過對稍微修改下Lua程式碼來解決這個問題:

for i = 1, self.qty_section_header do

-- Get the absolute start address of the section header before it is parsed

local _sec_hdr_start_addr = self.start_addr + self._io:pos()

self._raw__m_section_headers[i] = \

self._io:read_bytes(self.section_header_entry_size)

local _io = KaitaiStream(stringstream(self._raw__m_section_headers[i]))

-- Use the section header's start address instead of the ELF's start address

self._m_section_headers[i] = Elf.EndianElf.SectionHeader(self.s2e_state,

_sec_hdr_start_addr,

_io, self,

self._root,

self._is_le)

end

是的,修改生成的Lua程式碼是令人厭惡的。但是,它確保了符號化時的記憶體地址是正確的。當我重新編譯S2E時,分支被限制在process_section_headers函式中的sh_type比較部分。

總結和未來的工作


在這篇文章中,探討了如何更有針對性的執行檔案解析器的符號執行問題。我們可以使用Kaitai Struct來定位輸入檔案的特定部分來進行符號化,而非給解析器一個完全符號的輸入檔案(這會很快導致路徑爆炸問題)。這種方法似乎奏效,但還是有些問題。


首先,首先,它依賴於使用者有一個有效的樣例檔案來執行符號執行。


。這個樣例檔案還必須包含我們希望執行的解析器部分的資料。比如,假設我們想將此技術應用於PNG解析器。如果我們拿這個PNG檔案的定義,並希望看到當bkgd_truecolor屬性符號化時發生了什麼,我們的PNG檔案也必須包含一個背景顏色塊。否則我們的解析器將沒有符號化的東西。


由於類似的原因,我們不能僅僅使用S2E引導指令碼建立的“空”的符號檔案。為當Kaitai Struct解析器執行時,它執行在檔案中的具體資料上。 S2E建立的預設符號檔案用NULL字元填充,因此解析器無法解析。如果我們可以憑空創造出檔案,是不是會很酷?


其他問題取決於我們如何使用Kaitai Struct。這不是Kaitai Struct的錯誤;實際上,Kaitai Struct FAQ明確指出,生成的解析器本來就不是為了“基於事件”的解析模型而設計的。我們可以修改ksc來生成基本不需要手動修改的程式碼(例如,自動生成引數規範,使用非延遲的例項規範,始終跟蹤解析器的絕對路徑等等),但是為了簡單起見不去考慮KaitaiStruct “原本的樣子”。


不是基於檔案的符號執行怎麼辦?例如,在我之前的帖子中,我展示瞭如何使用S2E來解決使用命令列字串作為輸入的CTF挑戰。這篇文章中描述的方法對解決這個CTF的挑戰是沒有幫助的。同樣我們可以擴充套件KaitaiStruct外掛來處理命令列字串。例如,我們可以在Kaitai Struct中定義CTF挑戰的輸入字串如下:


meta:

id: ctf-input

title: Google CTF input format

ks-version: 0.8

seq:

- id: prefix

size: 4

contents: "CTF{"

- id: to_solve

size: 63 # total length of 67 bytes minus the 4 byte prefix

process: s2e_make_symbolic(s2e_state, start_addr, _io.pos, "to_solve")

params:

- id: s2e_state

- id: start_addr


加上一些額外的程式碼,我們可以在輸入字串上的執行此解析器,只將最後63個位元組符號化。 這將允許我們從S2E外掛中刪除onSymbolicVariableCreation方法。


儘管出現了這些問題,但是把S2E和Kaitai Struct組合起來似乎對我目前正在做的工作(儘管你的目的可能會有所不同)還是很有幫助的。 我們可以通過更多的工作(更多的程式碼)來解決這些問題。 所以,我想我會把那作為一個未來的帖子:)


本文由看雪翻譯小組 fyb波 編譯,來源Adrian's Ramblings 轉載請註明來自看雪社群

相關文章