我們在調(diào)試器中加入了簡單的地址斷點(diǎn)。這一次,我們將給調(diào)試器加入讀寫寄存器和內(nèi)存的功能,這樣就可以在控制RIP,觀察程序的狀態(tài),以及改變程序的行為了。
注冊我們的寄存器
在我們正真的讀取寄存器前,調(diào)試器需要知道一些關(guān)于x8664架構(gòu)的相關(guān)知識。包括通用寄存器,專用寄存器以及浮點(diǎn)寄存器和向量寄存器。為了簡單期間,我將省略后兩者(浮點(diǎn)以及向量寄存器),當(dāng)然如果你喜歡的話你可以選擇去加入相關(guān)支持。x86_64架構(gòu)也允許你用32,16或者8位的方式來訪問64位寄存器,但是我將會一直使用64位的。由于簡化了一些東西,所以對寄存器來說,我們只需要知道它的名字以及它在DWARF中的寄存器號,以及它被存儲在ptrace返回的結(jié)構(gòu)中什么位置就可以了。我選擇用一個枚舉來引用寄存器,然后來構(gòu)建一個和ptrace中的寄存器結(jié)構(gòu)順序相同的全局寄存器描述符數(shù)組。
enum?class?reg?{????rax,?rbx,?rcx,?rdx,????rdi,?rsi,?rbp,?rsp,????r8,??r9,??r10,?r11,????r12,?r13,?r14,?r15,????rip,?rflags,????cs,????orig_rax,?fs_base,????gs_base,????fs,?gs,?ss,?ds,?es};constexpr?std::size_t?n_registers?=?27;struct?reg_descriptor?{????reg?r;????int?dwarf_r;????std::string?name;};const?std::array?g_register_descriptors?{{????{?reg::r15,?15,?"r15"?},????{?reg::r14,?14,?"r14"?},????{?reg::r13,?13,?"r13"?},????{?reg::r12,?12,?"r12"?},????{?reg::rbp,?6,?"rbp"?},????{?reg::rbx,?3,?"rbx"?},????{?reg::r11,?11,?"r11"?},????{?reg::r10,?10,?"r10"?},????{?reg::r9,?9,?"r9"?},????{?reg::r8,?8,?"r8"?},????{?reg::rax,?0,?"rax"?},????{?reg::rcx,?2,?"rcx"?},????{?reg::rdx,?1,?"rdx"?},????{?reg::rsi,?4,?"rsi"?},????{?reg::rdi,?5,?"rdi"?},????{?reg::orig_rax,?-1,?"orig_rax"?},????{?reg::rip,?-1,?"rip"?},????{?reg::cs,?51,?"cs"?},????{?reg::rflags,?49,?"eflags"?},????{?reg::rsp,?7,?"rsp"?},????{?reg::ss,?52,?"ss"?},????{?reg::fs_base,?58,?"fs_base"?},????{?reg::gs_base,?59,?"gs_base"?},????{?reg::ds,?53,?"ds"?},????{?reg::es,?50,?"es"?},????{?reg::fs,?54,?"fs"?},????{?reg::gs,?55,?"gs"?},}};
一般你可以在/usr/include/sys/user.h找到關(guān)于寄存器相關(guān)的數(shù)據(jù)結(jié)構(gòu)。如果你想自己去查看一番,DWARF寄存器號是根據(jù)System V x86_64 ABI這個規(guī)范來設(shè)置的。
現(xiàn)在,就可以寫一大堆函數(shù)來與寄存器交互了。我們希望能夠通過DWARF寄存器號來讀取,寫入,接收寄存器的值,并且可以通過命長來查找寄存器或者通過寄存器來查找名稱。讓我們從聲明get_register_value函數(shù)開始吧:
uint64_t?get_register_value(pid_t?pid,?reg?r)?{????user_regs_struct?regs;????ptrace(PTRACE_GETREGS,?pid,?nullptr,??s);????//...}
同樣的,ptrace給了我們一種簡單的訪問我們想要的數(shù)據(jù)的方式。只需構(gòu)建一個user_regs_struct實(shí)例,然后和PTRACE_GETREGS請求一起傳給ptrace即可。
現(xiàn)在,我們想根據(jù)被請求的寄存器讀取regs??梢酝ㄟ^寫一個繁雜的switch case結(jié)構(gòu),但是由于我們已經(jīng)構(gòu)建了g_register_descriptors這個表,表中的寄存器順序和user_regs_struct完全一致,于是就可以通過索引來查找寄存器描述符,并且以uint64_t數(shù)組的方式來訪問user_regs_struct。
auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????????[r](auto&&?rd)?{?return?rd.r?==?r;?});//譯注:此處是lambda表達(dá)式????????return?*(reinterpret_cast(?s)?+?(it?-?begin(g_register_descriptors)));
轉(zhuǎn)換到uint_64_t是安全的,因?yàn)閡ser_regs_struct是標(biāo)準(zhǔn)的布局類型,但是我認(rèn)為指針在算數(shù)運(yùn)算上是unsigned byte(譯注:實(shí)際上是signed byte,參考內(nèi)核地址高20(intel架構(gòu))位全被置1)。現(xiàn)有編譯器甚至對此沒有警告,我比較懶,也不想多花心思了,但是如果你想保持最大可能的正確性就需要一個大的switch case了。
set_register_value也是一樣的,我僅僅是寫到相應(yīng)位置,然后在最后寫回寄存器:
void?set_register_value(pid_t?pid,?reg?r,?uint64_t?value)?{????user_regs_struct?regs;????ptrace(PTRACE_GETREGS,?pid,?nullptr,??s);????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????[r](auto&&?rd)?{?return?rd.r?==?r;?});????*(reinterpret_cast(?s)?+?(it?-?begin(g_register_descriptors)))?=?value;????ptrace(PTRACE_SETREGS,?pid,?nullptr,??s);}
接下來就是通過DWARF寄存器號來查找相應(yīng)的值了。這一次我會檢查一個錯誤條件,以防萬得到一些奇怪的DWARF信息:
uint64_t?get_register_value_from_dwarf_register?(pid_t?pid,?unsigned?regnum)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????[regnum](auto&&?rd)?{?return?rd.dwarf_r?==?regnum;?});????if?(it?==?end(g_register_descriptors))?{????????throw?std::out_of_range{"Unknown?dwarf?register"};????}????return?get_register_value(pid,?it->r);}
差不多完成了,現(xiàn)在我們就有了下邊看起來這樣的寄存器值了:
std::string?get_register_name(reg?r)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????[r](auto&&?rd)?{?return?rd.r?==?r;?});????return?it->name;}reg?get_register_from_name(const?std::string&?name)?{????auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????[name](auto&&?rd)?{?return?rd.name?==?name;?});????return?it->r;}
最后,加一些簡單的輔助函數(shù)來dump寄存器的內(nèi)容:
void?debugger::dump_registers()?{????for?(const?auto&?rd?:?g_register_descriptors)?{????????std::cout?<
如你所見,iostreams有一個非常簡潔的接口,可以很好地輸出十六進(jìn)制數(shù)據(jù)。如果你喜歡,可以封裝一些IO操作來避免混亂。
這些就足夠支持我們在調(diào)試器其它部分處理寄存器了,現(xiàn)在,可以將其添加到UI中去了。
操作寄存器
我們需要做的就是將一個新的命令加入到handle_command函數(shù)中。在下邊的代碼示意中,用戶可以通過輸入register read rax或者register write rax 0x42以及其他的命令來操縱寄存器。
else?if?(is_prefix(command,?"register"))?{????????if?(is_prefix(args[1],?"dump"))?{????????????dump_registers();????????}????????else?if?(is_prefix(args[1],?"read"))?{????????????std::cout?<
思路
在設(shè)置斷點(diǎn)時,我們已經(jīng)讀取和寫入內(nèi)存,所以只需要添加一些函數(shù)來封裝一下ptrace調(diào)用。
uint64_t?debugger::read_memory(uint64_t?address)?{????return?ptrace(PTRACE_PEEKDATA,?m_pid,?address,?nullptr);}void?debugger::write_memory(uint64_t?address,?uint64_t?value)?{????ptrace(PTRACE_POKEDATA,?m_pid,?address,?value);}
你可能希望一次添加對讀取和寫入大于WORD(16位)型數(shù)據(jù)的支持,只需通過在每次要讀取另一個WORD時遞增地址即可。同時也可以使用process_vm_readv和process_vm_writev或者使用/proc//mem來替代ptrace。
現(xiàn)在,為我們的UI加入相關(guān)命令:
else?if(is_prefix(command,?"memory"))?{????????std::string?addr?{args[2],?2};?//assume?0xADDRESS????????if?(is_prefix(args[1],?"read"))?{????????????std::cout?<
修復(fù)continue_execution
110/5000
您是不是要找:?Before we test out our changes, we’re now in a position to implement a more sane version of?continue execution)
在測試更改之前,我們現(xiàn)在可以執(zhí)行一個更加正確的版本的continue_execution。因?yàn)榭梢垣@取RIP,所以只需檢查我們的斷點(diǎn)保存結(jié)構(gòu)來確定是否運(yùn)行到了一個斷點(diǎn)的位置。如果是,先禁止斷點(diǎn)然后在繼續(xù)運(yùn)行前步過一次。
首先,為了清晰簡潔,先添加幾個輔助函數(shù):
uint64_t?debugger::get_pc()?{????return?get_register_value(m_pid,?reg::rip);}void?debugger::set_pc(uint64_t?pc)?{????set_register_value(m_pid,?reg::rip,?pc);}
然后,可以寫一個步過斷點(diǎn)的函數(shù):
void?debugger::step_over_breakpoint()?{????//?-?1?because?execution?will?go?past?the?breakpoint????auto?possible_breakpoint_location?=?get_pc()?-?1;????if?(m_breakpoints.count(possible_breakpoint_location))?{????????auto&?bp?=?m_breakpoints[possible_breakpoint_location];????????if?(bp.is_enabled())?{????????????auto?previous_instruction_address?=?possible_breakpoint_location;????????????set_pc(previous_instruction_address);????????????bp.disable();????????????ptrace(PTRACE_SINGLESTEP,?m_pid,?nullptr,?nullptr);????????????wait_for_signal();????????????bp.enable();????????}????}}
首先,檢查此刻RIP所處的位置是不是被設(shè)置了斷點(diǎn),如果是,將RIP后退一個字節(jié)(譯注:0xCC斷點(diǎn)觸發(fā)時0xCC本身已經(jīng)被執(zhí)行過了,所以停下的位置和下斷點(diǎn)的位置差了一個字節(jié),需要將RIP回?fù)芤粋€字節(jié)),禁用斷點(diǎn)(譯注:將原始的指令數(shù)據(jù)寫回來),單步步過此處原來的指令,然后重新設(shè)置斷點(diǎn)(譯注:再將0xCC寫回去)R
wait_for_signal函數(shù)將封裝一些常用的waitpid模式:
void?debugger::wait_for_signal()?{????int?wait_status;????auto?options?=?0;????waitpid(m_pid,?&wait_status,?options);}
最后,重新寫的continue_execution就像這樣:
void?debugger::continue_execution()?{????step_over_breakpoint();????ptrace(PTRACE_CONT,?m_pid,?nullptr,?nullptr);????wait_for_signal();}
測試
現(xiàn)在我們可以讀取和修改寄存器,hello world程序于是就可以有一些樂子了。首先來測試一下在call指令上下斷點(diǎn),然后從斷點(diǎn)處繼續(xù)運(yùn)行吧。應(yīng)該可以看見Hello world已經(jīng)被輸出。樂子來了,在輸出的那個call后邊下一個斷點(diǎn),繼續(xù)運(yùn)行,然后將設(shè)置調(diào)用參數(shù)的代碼的地址寫入RIP并繼續(xù)。你應(yīng)該可以看見由于RIP被改變Hello world被輸出了兩次。以防你不知道在哪里設(shè)置斷點(diǎn),下邊我給出我的objdump:
0000000000400936?
:??400936:????55???????????????????????push???rbp??400937:????48?89?e5?????????????????mov????rbp,rsp??40093a:????be?35?0a?40?00???????????mov????esi,0x400a35??40093f:????bf?60?10?60?00???????????mov????edi,0x601060??400944:????e8?d7?fe?ff?ff???????????call???400820?<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>??400949:????b8?00?00?00?00???????????mov????eax,0x0??40094e:????5d???????????????????????pop????rbp??40094f:????c3
你需要將RIP移回到0x40093a,以便對esi和edi進(jìn)行正確的賦值。
在下一篇文章中,我們將會首次探索一下DWARF信息,以及向調(diào)試器加入幾種單步操作。之后,我們將有一個具備大部分功能的工具,可以通過代碼來單步,設(shè)置斷點(diǎn)到想要的地方去,修改數(shù)據(jù)以及更多功能。有問題,盡管在回復(fù)區(qū)提問!
?
評論