Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】看我如何编写一个Linux 调试器(三):内存和寄存器

$
0
0
【技术分享】看我如何编写一个linux 调试器(三):内存和寄存器

2017-10-16 10:49:29

阅读:2474次
点赞(0)
收藏
来源: blog.tartanllama.xyz





【技术分享】看我如何编写一个Linux 调试器(三):内存和寄存器

作者:_veritas501





【技术分享】看我如何编写一个Linux 调试器(三):内存和寄存器

译者:_veritas501

预估稿费:150RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


简介

传送门:

【技术分享】看我如何编写一个Linux 调试器(一):准备工作

【技术分享】看我如何编写一个Linux 调试器(二):断点

在上一篇文章中,我们给调试器添加了简单的断点功能。这一次,我们将添加对寄存器和内存的读写能力,这使得我们随意修改PC指针,观察当前的状态和改变程序的行为。


注册我们的寄存器

在我们开始读取寄存器值之前,我们需要告诉调试器一些关于我们目标的信息,这里是x86\_64平台。除了一系列通用和专用的寄存器外,x86\_64还拥有浮点寄存器和向量寄存器。为了简洁,我将跳过最后两种,但如果你喜欢你也可以选择对它们提供支持。x86\_64同样允许你像32,16,8位寄存器那样操作64位寄存器,但我只专注于64位。处于简化,对每一个寄存器我们只需要它的名称、他的DWARF寄存器编号以及它在ptrace返回的结构体中的储存位置。我选择使用范围枚举引用这些寄存器,然后我列出一个全局寄存器描述数组,其中元素的顺序和它在ptrace寄存器结构体的中顺序相同。

enumclassreg{ 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 }; constexprstd::size_tn_registers=27; structreg_descriptor{ regr; intdwarf_r; std::stringname; }; conststd::array<reg_descriptor,n_registers>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里找到这个寄存器数据结构体(user_regs_struct )。DWARF寄存器编号来自System V x86_64 ABI。

现在我们可以编写一堆函数来与寄存器做交互。我们想要从寄存器中读取和写入值,根据DWARF寄存器编号获取值,以及通过名称查找寄存器,反之亦然。让我们先实现get_register_value:

uint64_tget_register_value(pid_tpid,regr){ user_regs_structregs; ptrace(PTRACE_GETREGS,pid,nullptr,&regs); //... }

ptrace再次使我们轻松地获取到了我们想得到的值。我们只需构造一个user_regs_struct的实例,并把它和PTRACE_GETREGS request传递给ptrace。

现在我们根据需求读取regs。我们可以写一个巨大的switch语句,因为我们的g_register_descriptors表的布局和 user_regs_struct相同,所以我们只需搜索寄存器描述符的索引,然后把 user_regs_struct 作为一个uint64_t的数组来操作即可[^1]。 autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors), [r](auto&&rd){returnrd.r==r;}); return*(reinterpret_cast<uint64_t*>(&regs)+(it-begin(g_register_descriptors)));

将regs转换成 uint64_t类型是安全的,因为user_regs_struct是一个标准的布局类型。但我认为指针运算在技术上是未定义的行为(UB)。当前没有一个编译器对此发出警告而且我很懒,如果你想保证代码严格正确,就写一个大的switch语句吧。

set_register_value非常类似,我们只需写入相应的地址并在最后写回寄存器中:

voidset_register_value(pid_tpid,regr,uint64_tvalue){ user_regs_structregs; ptrace(PTRACE_GETREGS,pid,nullptr,&regs); autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors), [r](auto&&rd){returnrd.r==r;}); *(reinterpret_cast<uint64_t*>(&regs)+(it-begin(g_register_descriptors)))=value; ptrace(PTRACE_SETREGS,pid,nullptr,&regs); }

下一步是通过DWARF寄存器编号进行寻找。这一次我会进行一些错误检查以防得到一些奇怪的DWARF 信息。

uint64_tget_register_value_from_dwarf_register(pid_tpid,unsignedregnum){ autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors), [regnum](auto&&rd){returnrd.dwarf_r==regnum;}); if(it==end(g_register_descriptors)){ throwstd::out_of_range{"Unknowndwarfregister"}; } returnget_register_value(pid,it->r); }

即将完工,现在我们已经有了寄存器名称查找功能:

std::stringget_register_name(regr){ autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors), [r](auto&&rd){returnrd.r==r;}); returnit->name; } regget_register_from_name(conststd::string&name){ autoit=std::find_if(begin(g_register_descriptors),end(g_register_descriptors), [name](auto&&rd){returnrd.name==name;}); returnit->r; }

最后我会添加一个简单的帮助函数把所有寄存器的内容导出来:

voiddebugger::dump_registers(){ for(constauto&rd:g_register_descriptors){ std::cout<<rd.name<<"0x" <<std::setfill('0')<<std::setw(16)<<std::hex<<get_register_value(m_pid,rd.r)<<std::endl; } } 如你所见,iostreams有简洁的接口来清晰地输出16进制数据[^2]。

这足够让我们在调试器的余下部分中轻松处理寄存器,因此我们现在可以加上UI界面了。


显示寄存器

我们所要做的就是为handle_command函数添加一个新的命令。通过下面的代码,用户就能输入诸如 register read rax, register write rax 0x42 的命令。

elseif(is_prefix(command,"register")){ if(is_prefix(args[1],"dump")){ dump_registers(); } elseif(is_prefix(args[1],"read")){ std::cout<<get_register_value(m_pid,get_register_from_name(args[2]))<<std::endl; } elseif(is_prefix(args[1],"write")){ std::stringval{args[3],2};//假定输入格式为0xVAL set_register_value(m_pid,get_register_from_name(args[2]),std::stol(val,0,16)); } }

接下来干什么?

当我们设置断点的时候,我们已经读取并写入内存了。因此我们只需要添加一些函数来包装ptrace的这些功能即可。

uint64_tdebugger::read_memory(uint64_taddress){ returnptrace(PTRACE_PEEKDATA,m_pid,address,nullptr); } voiddebugger::write_memory(uint64_taddress,uint64_tvalue){ ptrace(PTRACE_POKEDATA,m_pid,address,value); }

你可能想实现一次读写多个字节,你可以通过每次递增地址来读取下一个字节。如果你喜欢,你也可以使用process_vm_readv和 process_vm_writev (link) 或 /proc/<pid>/mem 来代替 ptrace。

现在我们给UI添加一些命令:

elseif(is_prefix(command,"memory")){ std::stringaddr{args[2],2};//假定输入为0xADDRESS if(is_prefix(args[1],"read")){ std::cout<<std::hex<<read_memory(std::stol(addr,0,16))<<std::endl; } if(is_prefix(args[1],"write")){ std::stringval{args[3],2};//假定输入为0xVAL write_memory(std::stol(addr,0,16),std::stol(val,0,16)); } }

给 continue_execution 做修补

在我们对测试更改前,我们现在可以实现一个更健全的continue_execution。由于我们可以获取PC指针,我们可以检查我们的断点表来判断我们是否在一个断点上。如果是,我们可以停用断点并在继续之前单步跳过。

首先为了阐明清晰,我们添加一些帮助函数:

uint64_tdebugger::get_pc(){ returnget_register_value(m_pid,reg::rip); } voiddebugger::set_pc(uint64_tpc){ set_register_value(m_pid,reg::rip,pc); }

然后我们可以写一个函数来步过断点:

voiddebugger::step_over_breakpoint(){ //-1是因为执行是跳过了断点 autopossible_breakpoint_location=get_pc()-1; if(m_breakpoints.count(possible_breakpoint_location)){ auto&bp=m_breakpoints[possible_breakpoint_location]; if(bp.is_enabled()){ autoprevious_instruction_address=possible_breakpoint_location; set_pc(previous_instruction_address); bp.disable(); ptrace(PTRACE_SINGLESTEP,m_pid,nullptr,nullptr); wait_for_signal(); bp.enable(); } } }

首先我们检测当前PC所指代码时候已经被设置断点,如果是,我们先把PC改到断点前一句,禁用断点后再步过原来的指令,在重新启用它。

wait_for_signal 封装了我们常用的waitpid模式。

voiddebugger::wait_for_signal(){ intwait_status; autooptions=0; waitpid(m_pid,&wait_status,options); }

最后我们重写 continue_execution函数:

voiddebugger::continue_execution(){ step_over_breakpoint(); ptrace(PTRACE_CONT,m_pid,nullptr,nullptr); wait_for_signal(); }

测试

现在我们可以读取和修改寄存器,我们可以对helloworld程序做些事情。第一个测试,尝试在一个call上再次设置断点并继续执行。你应该会看到程序打印出Hello world。有趣的部分,在输出的call后面设置断点,然后将rip改到call的参数设置处并继续,你应该会看到程序打印出Hello world。以防你不知道在哪里设置断点,这里是我上一篇中objdump的输出:

0000000000400936<main>: 400936:55pushrbp 400937:4889e5movrbp,rsp 40093a:be350a4000movesi,0x400a35 40093f:bf60106000movedi,0x601060 400944:e8d7feffffcall400820<_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 400949:b800000000moveax,0x0 40094e:5dpoprbp 40094f:c3ret

为正确设置esi和edi寄存器,你需要把PC指针设置到0x40093a。

在下一篇文章中,我们将第一次接触到DWARF信息并为我们的调试器增添一系列单步调试的功能。这样我们就有了一个能单步执行代码,在想要的地方设置断点,修改数据等等功能的调试器了。

和之前一样,如果你有任何问题欢迎在我博客下面评论!

你可以在这里找到本文的代码。

[^1]:你也可以重拍reg枚举变量并用索引把他们转换成底层类型,但我第一次就是用这种方式写的,他能正常运作,我就懒得改了。 [^2]:哈哈哈哈哈哈哈哈哈哈


【技术分享】看我如何编写一个Linux 调试器(三):内存和寄存器
【技术分享】看我如何编写一个Linux 调试器(三):内存和寄存器
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:https://blog.tartanllama.xyz/writing-a-linux-debugger-registers/

Viewing all articles
Browse latest Browse all 12749

Trending Articles