我們在調試器中加入了簡單的地址斷點。這一次,我們將給調試器加入讀寫寄存器和內存的功能,這樣就可以在控制RIP,觀察程序的狀態,以及改變程序的行為了。
注冊我們的寄存器
在我們正真的讀取寄存器前,調試器需要知道一些關于x8664架構的相關知識。包括通用寄存器,專用寄存器以及浮點寄存器和向量寄存器。為了簡單期間,我將省略后兩者(浮點以及向量寄存器),當然如果你喜歡的話你可以選擇去加入相關支持。x86_64架構也允許你用32,16或者8位的方式來訪問64位寄存器,但是我將會一直使用64位的。由于簡化了一些東西,所以對寄存器來說,我們只需要知道它的名字以及它在DWARF中的寄存器號,以及它被存儲在ptrace返回的結構中什么位置就可以了。我選擇用一個枚舉來引用寄存器,然后來構建一個和ptrace中的寄存器結構順序相同的全局寄存器描述符數組。
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找到關于寄存器相關的數據結構。如果你想自己去查看一番,DWARF寄存器號是根據System V x86_64 ABI這個規范來設置的。
現在,就可以寫一大堆函數來與寄存器交互了。我們希望能夠通過DWARF寄存器號來讀取,寫入,接收寄存器的值,并且可以通過命長來查找寄存器或者通過寄存器來查找名稱。讓我們從聲明get_register_value函數開始吧:
uint64_t?get_register_value(pid_t?pid,?reg?r)?{????user_regs_struct?regs;????ptrace(PTRACE_GETREGS,?pid,?nullptr,??s);????//...}
同樣的,ptrace給了我們一種簡單的訪問我們想要的數據的方式。只需構建一個user_regs_struct實例,然后和PTRACE_GETREGS請求一起傳給ptrace即可。
現在,我們想根據被請求的寄存器讀取regs??梢酝ㄟ^寫一個繁雜的switch case結構,但是由于我們已經構建了g_register_descriptors這個表,表中的寄存器順序和user_regs_struct完全一致,于是就可以通過索引來查找寄存器描述符,并且以uint64_t數組的方式來訪問user_regs_struct。
auto?it?=?std::find_if(begin(g_register_descriptors),?end(g_register_descriptors),???????????????????????????????[r](auto&&?rd)?{?return?rd.r?==?r;?});//譯注:此處是lambda表達式????????return?*(reinterpret_cast(?s)?+?(it?-?begin(g_register_descriptors)));
轉換到uint_64_t是安全的,因為user_regs_struct是標準的布局類型,但是我認為指針在算數運算上是unsigned byte(譯注:實際上是signed byte,參考內核地址高20(intel架構)位全被置1)?,F有編譯器甚至對此沒有警告,我比較懶,也不想多花心思了,但是如果你想保持最大可能的正確性就需要一個大的switch case了。
set_register_value也是一樣的,我僅僅是寫到相應位置,然后在最后寫回寄存器:
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寄存器號來查找相應的值了。這一次我會檢查一個錯誤條件,以防萬得到一些奇怪的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);}
差不多完成了,現在我們就有了下邊看起來這樣的寄存器值了:
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;}
最后,加一些簡單的輔助函數來dump寄存器的內容:
void?debugger::dump_registers()?{????for?(const?auto&?rd?:?g_register_descriptors)?{????????std::cout?<
如你所見,iostreams有一個非常簡潔的接口,可以很好地輸出十六進制數據。如果你喜歡,可以封裝一些IO操作來避免混亂。
這些就足夠支持我們在調試器其它部分處理寄存器了,現在,可以將其添加到UI中去了。
操作寄存器
我們需要做的就是將一個新的命令加入到handle_command函數中。在下邊的代碼示意中,用戶可以通過輸入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?<
思路
在設置斷點時,我們已經讀取和寫入內存,所以只需要添加一些函數來封裝一下ptrace調用。
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位)型數據的支持,只需通過在每次要讀取另一個WORD時遞增地址即可。同時也可以使用process_vm_readv和process_vm_writev或者使用/proc//mem來替代ptrace。
現在,為我們的UI加入相關命令:
else?if(is_prefix(command,?"memory"))?{????????std::string?addr?{args[2],?2};?//assume?0xADDRESS????????if?(is_prefix(args[1],?"read"))?{????????????std::cout?<
修復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)
在測試更改之前,我們現在可以執行一個更加正確的版本的continue_execution。因為可以獲取RIP,所以只需檢查我們的斷點保存結構來確定是否運行到了一個斷點的位置。如果是,先禁止斷點然后在繼續運行前步過一次。
首先,為了清晰簡潔,先添加幾個輔助函數:
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);}
然后,可以寫一個步過斷點的函數:
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所處的位置是不是被設置了斷點,如果是,將RIP后退一個字節(譯注:0xCC斷點觸發時0xCC本身已經被執行過了,所以停下的位置和下斷點的位置差了一個字節,需要將RIP回撥一個字節),禁用斷點(譯注:將原始的指令數據寫回來),單步步過此處原來的指令,然后重新設置斷點(譯注:再將0xCC寫回去)R
wait_for_signal函數將封裝一些常用的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();}
測試
現在我們可以讀取和修改寄存器,hello world程序于是就可以有一些樂子了。首先來測試一下在call指令上下斷點,然后從斷點處繼續運行吧。應該可以看見Hello world已經被輸出。樂子來了,在輸出的那個call后邊下一個斷點,繼續運行,然后將設置調用參數的代碼的地址寫入RIP并繼續。你應該可以看見由于RIP被改變Hello world被輸出了兩次。以防你不知道在哪里設置斷點,下邊我給出我的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進行正確的賦值。
在下一篇文章中,我們將會首次探索一下DWARF信息,以及向調試器加入幾種單步操作。之后,我們將有一個具備大部分功能的工具,可以通過代碼來單步,設置斷點到想要的地方去,修改數據以及更多功能。有問題,盡管在回復區提問!
?
評論
查看更多