驯服Gorynych或在Ghidra中反编译eBPF


本文作者https://github.com/Nalen98


下午好


作为在Digital Security进行的Hack of Summer 2019夏季实习的一部分,我的研究主题是“在Ghidra中反编译eBPF”。 为了能够反汇编和反编译eBPF程序,有必要在Sleigh语言中开发PCode Ghidra中的eBPF字节码转换系统。 研究的结果是对Ghidra的开发扩展,增加了对eBPF处理器的支持。 像其他实习生一样,该研究应被视为“开创性”,因为以前不可能在其他逆向工程工具中反编译eBPF。


背景知识


这个话题极具讽刺意味,使我大受讽刺,因为我以前对eBPF并不熟悉,而Ghidr之前也没有使用过,因为有一种教条认为“ IDA Pro更好”。 事实证明,这并非完全正确。


事实证明,对Ghidra的了解非常迅速,因为其开发人员草拟了非常称职且易于访问的文档。 另外,我必须掌握进行开发的Sleigh处理器规范语言。 开发人员竭尽所能,并为工具本身和Sleigh创建了非常详细的文档,这要感谢他们。


路障的另一侧是一个扩展的伯克利包过滤器。 eBPF是Linux内核中的虚拟机,允许您加载任意用户代码,这些代码可用于跟踪进程并过滤内核空间中的数据包。 该架构是具有11个64位寄存器,一个软件计数器和512字节堆栈的RISC寄存器计算机。 eBPF有很多限制:


  • 禁止循环;
  • 只能通过堆栈访问内存(会有一个单独的故事);
  • 内核函数仅可通过特殊的包装函数(eBPF-helpers)使用。


eBPF技术的结构。 图片来源: http : //www.brendangregg.com/ebpf.html


基本上,此技术用于网络任务-内核级别的调试,数据包筛选等。 自从内核3.15版以来,已经添加了EBPF支持;在2019年的Linux水管工会议上有很多关于该技术的报告。 但是在eBPF中,与Ghidra不同,该文档不完整且内容不多。 因此,必须在Internet上搜索澄清和缺少的信息。 找到答案花了相当长的时间,剩下的只是希望技术能够最终确定并创建常规文档。


错误的文件


为了制定Sleigh规范,您首先需要了解目标处理器的体系结构是如何工作的。 在这里,我们来看官方文档


它包含许多缺陷:


  • eBPF指令的结构未完全描述。


    大多数规范(例如Intel x86)通常会指示每个指令位所处的位置,所属的块。 不幸的是,在eBPF规范中,这些细节要么分散在整个文档中,要么完全不存在,因此,我们不得不从Linux内核中的实现细节中汲取缺失的细节。


    例如,在指令结构op:8, dst_reg:4, src_reg:4, off:16, imm:32没有一个词被说成是偏移量(off)和立即数(imm)被signed ,这非常重要,因为它会影响从算术指令到跳转。 Linux内核的源代码有所帮助。


  • 没有完整的架构的所有可能的助记符。


    在某些文档中,不仅指示了所有指令及其操作数,而且还指示了它们在C语言中的语义,应用案例,操作数功能等。 eBPF文档包含指令类,但这对开发人员来说还不够。 让我们更详细地考虑它们。


    eBPF的所有指令均为64位,除了LDDW (加载双字)外,它的大小为128位,将两个imm连接起来,每个32mm。 eBPF指令具有以下结构。

    eBPF指令编码


    OPAQUE字段的结构取决于指令的类别(ALU / JMP,加载/存储)。


    例如, ALU指令类:

    ALU指令编码


    JMP类具有自己的字段结构:

    分支指令编码


    对于加载/存储指令,结构不同:

    加载/存储指令编码


    非正式的eBPF文档有助于解决这一问题


  • 没有有关呼叫帮助程序的信息,用于Linux内核的eBPF程序的大多数逻辑都建立在该信息上。


    这是非常奇怪的,因为助手是eBPF程序中最重要的事情,所以他们只是执行技术所关注的任务。




EBPF与核功能的互操作性


该程序从内核中提取这些功能,它们仅与进程一起使用,操纵网络数据包,与eBPF映射一起使用,访问套接字,与用户空间进行交互。 尽管功能仍然是核功能,但在官方文档中还是有必要对其进行详细介绍。 完整的细节可以在Linux 源代码中找到。


  • 一言不发。


EBPF尾呼叫。 图片来源: https : //cilium.readthedocs.io/en/latest/bpf/#tail-calls


尾部调用是一种机制,它允许一个eBPF程序在不返回到前一个程序的情况下调用另一个程序,即在不同的eBPF程序之间跳转。 在开发的扩展中未实现它们,有关详细信息,请参见Cilium文档


eBPF的文档不足和许多体系结构功能是开发中的主要“碎片”,因为它们带来了其他问题。 幸运的是,大多数问题都得到了成功解决。


关于开发环境



并非所有的开发人员都知道,对于创建和编辑Sleigh代码以及Ghidra的所有扩展/插件文件,通常有一个相当方便的工具-Eclipse IDE,它支持GhidraDevGhidraSleighEditor插件 。 创建扩展名时,它将立即作为工作草案构成框架,其中有一个非常方便的高亮显示,用于Sleigh代码以及语言语法中的主要错误检查器。


在Eclipse中,您可以运行Ghidra(已经打开了扩展名),调试,这非常方便。 但是,也许最酷的机会是支持“ Ghidra Headless”模式,您无需从GUI重新启动Ghidr 100500次即可在代码中发现错误,所有过程都在后台执行。


记事本可以关闭! 并且您可以从官方站点下载Eclipse。 要安装插件,请在Ecplise中选择帮助→安装新软件... ,单击添加 ,然后选择插件zip存档。


扩展开发


为了进行扩展,开发了处理器规范文件,从主ELF加载器继承并扩展了其功能的加载器,以识别eBPF程序,用于在Ghidra拆装器和反编译器中实现eBPF Maps的重定位处理器以及用于确定eBPF帮助器签名的分析器。




扩展文件作为Eclipse IDE中的项目


现在有关主要文件:


.cspec它指示使用了哪些数据类型,在eBPF中为它们分配了多少内存,设置了堆栈大小,将“ stackpointer”标签设置为寄存器R10 ,并且对调用协议进行了签名。 该协议(与其他协议一样)是根据文档实施的:


因此,eBPF调用约定定义为:
  • R0-内核内函数的返回值,eBPF程序的退出值
  • R1-R5-从eBPF程序到内核功能的参数
  • R6-R9-内核函数将保留的被调用方保存的寄存器
  • R10-访问堆栈的只读帧指针


eBPF.c规范
 <?xml version="1.0" encoding="UTF-8"?> <compiler_spec> <data_organization> <absolute_max_alignment value="0" /> <machine_alignment value="2" /> <default_alignment value="1" /> <default_pointer_alignment value="4" /> <pointer_size value="4" /> <wchar_size value="4" /> <short_size value="2" /> <integer_size value="4" /> <long_size value="4" /> <long_long_size value="8" /> <float_size value="4" /> <double_size value="8" /> <long_double_size value="8" /> <size_alignment_map> <entry size="1" alignment="1" /> <entry size="2" alignment="2" /> <entry size="4" alignment="4" /> <entry size="8" alignment="8" /> </size_alignment_map> </data_organization> <global> <range space="ram"/> <range space="syscall"/> </global> <stackpointer register="R10" space="ram"/> <default_proto> <prototype name="__fastcall" extrapop="0" stackshift="0"> <input> <pentry minsize="1" maxsize="8"> <register name="R1"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R2"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R3"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R4"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R5"/> </pentry> </input> <output killedbycall="true"> <pentry minsize="1" maxsize="8"> <register name="R0"/> </pentry> </output> <unaffected> <varnode space="ram" offset="8" size="8"/> <register name="R6"/> <register name="R7"/> <register name="R8"/> <register name="R9"/> <register name="R10"/> </unaffected> </prototype> </default_proto> </compiler_spec> 

在继续描述开发文件之前,我将介绍.cspec文件的一小部分。


 <stackpointer register="R10" space="ram"/> 

在Ghidra中反编译eBPF时,它是邪恶的主要根源,它开始了令人兴奋的旅程,进入了eBPF堆栈,该堆栈有许多令人不愉快的时刻,给开发带来了最大的痛苦。


我们需要的只是...堆栈


让我们看一下官方的内核文档


问:BPF程序可以访问指令指针或返回地址吗?

答:不可以。

问:BPF程序可以访问堆栈指针吗?

答:不可以。 仅可访问帧指针(寄存器R10)。 从编译器的角度来看,必须具有堆栈指针。 例如,LLVM将寄存器R11定义为其BPF后端中的堆栈指针,但是它确保生成的代码从不使用它。

处理器既没有指令指针(IP)也没有堆栈指针(SP),而后者对于Ghidra极为重要,反编译的质量取决于它。 在cspec文件中,您需要指定哪个寄存器是堆栈指针(如上所示)。 R10是唯一允许访问程序堆栈的eBPF寄存器,它是帧指针,它是静态的,始终为零。 从根本上说,在cspec文件的R10上悬挂“ cspec ”标签是错误的,但是没有其他选择,因为这样Ghidra将无法与程序堆栈一起使用。 因此,原始SP不存在,在eBPF体系结构中没有什么可以替代它。


由此产生了几个问题:


  1. Ghidra中的“堆栈深度”字段将确保为零,因为在这些架构条件下,我们仅需将R10指定R10堆栈器即可,实际上,它始终为零,这已在前面进行了讨论。 “堆栈深度”将反映带有标签“ stackpointer”的寄存器。


    您必须忍受这些,这是体系结构的功能。


  2. 通常不会反编译在R10上运行的指令(即,处理堆栈的指令)。 Ghidra通常不会反编译它认为无效的代码(即永不执行的代码段)。 由于R10不可变的,因此许多存储/加载指令都被Ghidr识别为死代码,并从反编译器中消失。


    幸运的是,此问题已通过编写自定义分析器以及在pspec文件中使用eBPF帮助器声明额外的地址空间解决了,这由Issue项目中的一位Ghidra开发人员提示。



扩展开发(续)


.ldefs描述处理器的功能,定义规格文件。


eBPF.ldefs
 <?xml version="1.0" encoding="UTF-8"?> <language_definitions> <language processor="eBPF" endian="little" size="64" variant="default" version="1.0" slafile="eBPF.sla" processorspec="eBPF.pspec" id="eBPF:LE:64:default"> <description>eBPF processor 64-bit little-endian</description> <compiler name="default" spec="eBPF.cspec" id="default"/> <external_name tool="DWARF.register.mapping.file" name="eBPF.dwarf"/> </language> </language_definitions> 

.opinion文件.opinion加载程序.opinion到处理器。


eBPF意见
 <opinions> <constraint loader="Executable and Linking Format (ELF)" compilerSpecID="default"> <constraint primary="247" processor="eBPF" endian="little" size="64" /> </constraint> </opinions> 

程序计数器是在.pspec中声明的,但是使用eBPF时,它是隐式的,并且不以任何方式在规范中使用,因此,它仅用于备考目的。 顺便说一下,eBPF的PC是算术运算,而不是地址(它表示指令,而不是程序的特定字节),在跳转时请记住这一点。


该文件还包含eBPF帮助程序的附加地址空间,此处将它们声明为字符。


eBPF.pspec
 <?xml version="1.0" encoding="UTF-8"?> <processor_spec> <programcounter register="PC"/> <default_symbols> <symbol name="bpf_unspec" address="syscall:0x0"/> <symbol name="bpf_map_lookup_elem" address="syscall:0x1"/> <symbol name="bpf_map_update_elem" address="syscall:0x2"/> <symbol name="bpf_map_delete_elem" address="syscall:0x3"/> <symbol name="bpf_probe_read" address="syscall:0x4"/> <symbol name="bpf_ktime_get_ns" address="syscall:0x5"/> <symbol name="bpf_trace_printk" address="syscall:0x6"/> <symbol name="bpf_get_prandom_u32" address="syscall:0x7"/> <symbol name="bpf_get_smp_processor_id" address="syscall:0x8"/> <symbol name="bpf_skb_store_bytes" address="syscall:0x9"/> <symbol name="bpf_l3_csum_replace" address="syscall:0xa"/> <symbol name="bpf_l4_csum_replace" address="syscall:0xb"/> <symbol name="bpf_tail_call" address="syscall:0xc"/> <symbol name="bpf_clone_redirect" address="syscall:0xd"/> <symbol name="bpf_get_current_pid_tgid" address="syscall:0xe"/> <symbol name="bpf_get_current_uid_gid" address="syscall:0xf"/> <symbol name="bpf_get_current_comm" address="syscall:0x10"/> <symbol name="bpf_get_cgroup_classid" address="syscall:0x11"/> <symbol name="bpf_skb_vlan_push" address="syscall:0x12"/> <symbol name="bpf_skb_vlan_pop" address="syscall:0x13"/> <symbol name="bpf_skb_get_tunnel_key" address="syscall:0x14"/> <symbol name="bpf_skb_set_tunnel_key" address="syscall:0x15"/> <symbol name="bpf_perf_event_read" address="syscall:0x16"/> <symbol name="bpf_redirect" address="syscall:0x17"/> <symbol name="bpf_get_route_realm" address="syscall:0x18"/> <symbol name="bpf_perf_event_output" address="syscall:0x19"/> <symbol name="bpf_skb_load_bytes" address="syscall:0x1a"/> <symbol name="bpf_get_stackid" address="syscall:0x1b"/> <symbol name="bpf_csum_diff" address="syscall:0x1c"/> <symbol name="bpf_skb_get_tunnel_opt" address="syscall:0x1d"/> <symbol name="bpf_skb_set_tunnel_opt" address="syscall:0x1e"/> <symbol name="bpf_skb_change_proto" address="syscall:0x1f"/> <symbol name="bpf_skb_change_type" address="syscall:0x20"/> <symbol name="bpf_skb_under_cgroup" address="syscall:0x21"/> <symbol name="bpf_get_hash_recalc" address="syscall:0x22"/> <symbol name="bpf_get_current_task" address="syscall:0x23"/> <symbol name="bpf_probe_write_user" address="syscall:0x24"/> </default_symbols> <default_memory_blocks> <memory_block name="eBPFHelper_functions" start_address="syscall:0" length="0x200" initialized="true"/> </default_memory_blocks> </processor_spec> 

.sinc文件是数量最多的扩展文件,在此定义所有寄存器,eBPF指令的结构,令牌,助记符以及Sleigh中指令的语义。


EBPF.sinc小片段
 define space ram type=ram_space size=8 default; define space register type=register_space size=4; define space syscall type=ram_space size=2; define register offset=0 size=8 [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 PC ]; define token instr(64) imm=(32, 63) signed off=(16, 31) signed src=(12, 15) dst=(8, 11) op_alu_jmp_opcode=(4, 7) op_alu_jmp_source=(3, 3) op_ld_st_mode=(5, 7) op_ld_st_size=(3, 4) op_insn_class=(0, 2) ; #We'll need this token to operate with LDDW instruction, which has 64 bit imm value define token immtoken(64) imm2=(32, 63) ; #To operate with registers attach variables [ src dst ] [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 _ _ _ _ _ ]; … :ADD dst, src is src & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=1 & op_insn_class=0x7 { dst=dst + src; } :ADD dst, imm is imm & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=0 & op_insn_class=0x7 { dst=dst + imm; } … 

eBPF加载程序扩展了ELF加载程序的基本功能,因此它可以识别您下载到Ghidra的程序具有eBPF处理器。 对于他来说,在ElfConstants Ghidra中分配了一个BPF常数 ,然后加载程序从中确定eBPF处理器。


eBPF_ElfExtension.java
 package ghidra.app.util.bin.format.elf.extend; import ghidra.app.util.bin.format.elf.*; import ghidra.program.model.lang.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; public class eBPF_ElfExtension extends ElfExtension { @Override public boolean canHandle(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF && elf.is64Bit(); } @Override public boolean canHandle(ElfLoadHelper elfLoadHelper) { Language language = elfLoadHelper.getProgram().getLanguage(); return canHandle(elfLoadHelper.getElfHeader()) && "eBPF".equals(language.getProcessor().toString()) && language.getLanguageDescription().getSize() == 64; } @Override public String getDataTypeSuffix() { return "eBPF"; } @Override public void processGotPlt(ElfLoadHelper elfLoadHelper, TaskMonitor monitor) throws CancelledException { if (!canHandle(elfLoadHelper)) { return; } super.processGotPlt(elfLoadHelper, monitor); } } 

需要使用重定位处理程序才能在反汇编器和反编译器中实现eBPF映射。 与它们的交互是通过许多助手来进行的,函数使用文件描述符来指示映射。 根据重定位表,可以看到加载器修补了LDDW指令,该指令为这些助手生成Rn (例如bpf_map_lookup_elem(…) )。


因此,处理程序解析程序重定位表,找到重定位地址(指令),并收集有关映射名称的字符串信息。 此外,参考符号表,它计算这些映射的实际地址并修补指令。


eBPF_ElfRelocationHandler.java
 public class eBPF_ElfRelocationHandler extends ElfRelocationHandler { @Override public boolean canRelocate(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF; } @Override public void relocate(ElfRelocationContext elfRelocationContext, ElfRelocation relocation, Address relocationAddress) throws MemoryAccessException, NotFoundException { ElfHeader elf = elfRelocationContext.getElfHeader(); if (elf.e_machine() != ElfConstants.EM_BPF) { return; } Program program = elfRelocationContext.getProgram(); Memory memory = program.getMemory(); int type = relocation.getType(); int symbolIndex = relocation.getSymbolIndex(); long value; boolean appliedSymbol = true; //Relocations with maps always have type 0x1. Since eBPF hasn't names of constants (types) of relocations, it was decided to use magic //number 1. if (type == 1) { try { int SymbolIndex= relocation.getSymbolIndex(); ElfSymbol Symbol = elfRelocationContext.getSymbol(SymbolIndex); String map = Symbol.getNameAsString(); SymbolTable table = program.getSymbolTable(); Address mapAddr = table.getSymbol(map).getAddress(); String sec_name = elfRelocationContext.relocationTable.getSectionToBeRelocated().getNameAsString(); if (sec_name.toString().contains("debug")) { return; } value = mapAddr.getAddressableWordOffset(); Byte dst = memory.getByte(relocationAddress.add(0x1)); memory.setLong(relocationAddress.add(0x4), value); memory.setByte(relocationAddress.add(0x1), (byte) (dst + 0x10)); } catch(NullPointerException e) {} } if (appliedSymbol && symbolIndex == 0) { markAsWarning(program, relocationAddress, Long.toString(type), "applied relocation with symbol-index of 0", elfRelocationContext.getLog()); } } } 


拆解eBPF的结果


最后,我们得到了eBPF反汇编器和反编译器! 为了健康!


GitHub上的扩展: Ghidra的eBPF


这里发布: 这里


聚苯乙烯


非常感谢Digital Security进行了有趣的实习,特别是感谢研究部门(Alexander和Nikolai)的指导。 我向你鞠躬!

Source: https://habr.com/ru/post/zh-CN468529/


All Articles