首页
论坛
专栏
课程

[翻译]GO语言逆向教程

2019-8-13 11:40 2013

[翻译]GO语言逆向教程

2019-8-13 11:40
2013
给出了一种golang清洗符号和字符串的方法
作者:RedNaga Security

GO编写的程序很奇怪,或者至少,在开始的地方是这样。在深入研究一些名为Rex的Linux恶意软件的同时,我意识到我可能需要了解的比我想象的要多。就在前一周,我一直在逆向Linux Lady,它也是用GO编写的,但它并没有去除符号表,所以它很容易。GO编译出的二进制文件相当大,包含许多我不关心的额外方法 - 虽然我真的不明白为什么。老实说 - 我仍然没有完全挖掘Golang代码并且还没有真正在Go中编写很多代码,因此其中一些可能是不正确的;这只是我在逆向一些ELF Go二进制文件时的经验!如果您不想阅读整个文章,或滚动到底部以获得完整git仓库的链接,请转到此处。 原文的GitHub

为了弄清楚我的一些例子,我将使用一个非常简单的“Hello,World!”示例,并引用Rex恶意软件的代码。代码和Make文件非常简单。
//hello.go
package main
import "fmt"
func main() {
    fmt.Println("Hello, World!")
}

//Makefile
all:
	GOOS=linux GOARCH=386 go build -o hello-stripped -ldflags "-s" hello.go
	GOOS=linux GOARCH=386 go build -o hello-normal hello.go
由于我正在使用OSX机器,因此需要上面的GOOSGOARCH变量来正确交叉编译。 第一行还添加了ldflags选项来去除二进制文件的符号表。 通过这种方式,我们可以同时分析的去除了符号表和原始的可执行文件,运行make然后反汇编编译出的GO程序。 如果我们在IDA Pro中打开未删除符号的二进制文件,我们很快就可以注意到一些神奇的事情;


我的妈 - 我们的5行代码变成了超过2058个函数。 由于看起来像运行库,我们在main()函数中也没有任何有趣的东西。 如果我们看的仔细,就能发现我们实际感兴趣的代码是在main_main中;



这是很多我真诚地不想看的代码。 字符串加载也看起来有点奇怪 - 虽然IDA似乎在确定必要的位方面做得很好。 我们可以很容易地看到字符串加载实际上是一组三个mov;
;string load
mov     ebx, offset aHelloWorld ; "Hello, World!"
mov     [esp+3Ch+var_14], ebx ; Shove string into location
mov     [esp+3Ch+var_10], 0Dh ; length of string
这不完全是革命性的,尽管我不能说我以前见过这样的事情。 引起我注意的另一个代码是runtime_morestack_context调用;
;morestack_context
loc_80490CB:
call    runtime_morestack_noctxt
jmp     main_main
这种样式的代码块似乎总是出现在函数的末尾,它似乎总是循环回到同一函数的顶部。 通过查看函数的交叉引用验证了这一点。 好了,既然我们知道IDA Pro可以处理未除去符号的二进制文件,那么这次看看剥离版本的表现。

我们马上看到一些......好吧,让我们称之为“差异”。 这回我们定义了1329个函数,导航器工具栏告诉我们有一些未定义的代码。 幸运的是,IDA仍然能够找到我们正在寻找的字符串引用,但是现在这个功能似乎不那么友好了。

我们不再有函数名称了 - 但是如果我们对main.main进行字符串搜索,那么函数名称似乎会保留在二进制文件的特定部分中。
.gopclntab
.gopclntab:0813E174                 db  6Dh ; m
.gopclntab:0813E175                 db  61h ; a
.gopclntab:0813E176                 db  69h ; i
.gopclntab:0813E177                 db  6Eh ; n
.gopclntab:0813E178                 db  2Eh ; .
.gopclntab:0813E179                 db  6Dh ; m
.gopclntab:0813E17A                 db  61h ; a
.gopclntab:0813E17B                 db  69h ; i
.gopclntab:0813E17C                 db  6Eh ; n
好吧,看来我们想要的符号还是有救。gopclntab段!在深入Google搜索结果并发布推文之后 - 一个友好的逆向工程师George(Egor?)Zaytsev向我展示了他的IDA Pro脚本,用于函数的重命名和添加类型信息。在略读这些之后,很容易弄清楚这一部分的格式,所以我在他的脚本基础上添加了一些功能。基本代码如下所示,简单来说,我们查看段.gopclntab并跳过前8个字节。然后我们创建一个指针(QwordDword,取决于程序是否为64位)。第一组数据实际上给出了.gopclntab表的大小,因此我们知道进入这个结构的偏移。现在我们可以开始处理其余的数据,这些数据似乎是以function_offset开始,后跟(function)name_offset。当我们创建指向这些偏移量的指针并告诉IDA创建字符串时,我们只需要确保我们不会向MakeString传递任何错误字符,因此我们使用clean_function_name函数去除任何错误。重命名的脚本如下。
#renamer.py
def create_pointer(addr, force_size=None):
    if force_size is not 4 and (idaapi.get_inf_structure().is_64bit() or force_size is 8):
        MakeQword(addr)
	return Qword(addr), 8
    else:
	MakeDword(addr)
	return Dword(addr), 4
STRIP_CHARS = [ '(', ')', '[', ']', '{', '}', ' ', '"' ]
REPLACE_CHARS = ['.', '*', '-', ',', ';', ':', '/', '\xb7' ]
def clean_function_name(str):
    # Kill generic 'bad' characters
    str = filter(lambda x: x in string.printable, str)
    for c in STRIP_CHARS:
        str = str.replace(c, '')
    for c in REPLACE_CHARS:
        str = str.replace(c, '_')
    return str
def renamer_init():
    renamed = 0
    gopclntab = ida_segment.get_segm_by_name('.gopclntab')
    if gopclntab is not None:
        # Skip unimportant header and goto section size
        addr = gopclntab.startEA + 8
        size, addr_size = create_pointer(addr)
        addr += addr_size
        # Unsure if this end is correct
        early_end = addr + (size * addr_size * 2)
        while addr < early_end:
            func_offset, addr_size = create_pointer(addr)
            name_offset, addr_size = create_pointer(addr + addr_size)
            addr += addr_size * 2
            func_name_addr = Dword(name_offset + gopclntab.startEA + addr_size) + gopclntab.startEA
            func_name = GetString(func_name_addr)
            MakeStr(func_name_addr, func_name_addr + len(func_name))
            appended = clean_func_name = clean_function_name(func_name)
            debug('Going to remap function at 0x%x with %s - cleaned up as %s' % (func_offset, func_name, clean_func_name))
            if ida_funcs.get_func_name(func_offset) is not None:
                if MakeName(func_offset, clean_func_name):
                    renamed += 1
                else:
                    error('clean_func_name error %s' % clean_func_name)
    return renamed
def main():
    renamed = renamer_init()
    info('Found and successfully renamed %d functions!' % renamed)
上面的代码实际上还没有经过运行(当然repo中的完整代码是可用的),但我希望它足够简单,可以阅读并理解该过程。但是,这仍然无法解决IDA Pro没有把所有函数都分析出来的问题。所以这将创建指针,这些指针在任何地方都没有被引用(这里原作者的意思应该是脚本使用ida_funcs.get_func_name(func_offset)来进行重命名,如果 .gopclntab表中解析出的func_offset指向ida未分析出的函数,get_func_name就失效了)。这可能看起来很难办,但别忘了我们刚才发现的看上去没什么用的 runtime_morestack_noctxt 。由于每个函数都使用它(当然只是基本上,它有一个边缘区域),如果我们找到这个函数并向后遍历到该函数的交叉引用,那么我们将知道每个函数的存在位置。 现在我们知道函数的结束和调用runtime_morestack_noctxt之后的下一条指令让我们跳转到函数的顶部。这意味着我们应该能够快速地给出IDA所需的函数的开始和停止的边界,同时将其与函数名称的解析分开。如果我们打开窗口以交叉引用函数runtime_morestack_noctxt,我们会看到有许多未定义的部分调用它。 函数的交叉引用总共1774个,这是IDA已经为我们定义的1329个函数,这由下图突出显示;

在深入研究多个二进制文件后,我们可以看到runtime_morestack_noctext将始终调用runtime_morestack(带context)。 这是我之前引用的边框,所以在这两个函数之间我们应该能够看到交叉引用二进制文件中使用的其他函数。 查看两个函数中较大的函数,runtime_more_stack,多个二进制文件往往有一个有趣的布局;

我感兴趣的部分是mov large dword ptr ds:1003h,0  - 这在我看到的所有64位二进制文件中看起来都是不变的。 所以在交叉编译之后我注意到32位二进制文件使用了mov qword ptr ds:1003h,0,所以我们将寻找这种特征来创建一个“hook”来向后遍历。 幸运的是,在ida中,我还没有看到一个实例无法定义这个特定函数,我们并不需要花费太多脑力来映射它或自己定义它。 所以,说够了的话,让我们写一些代码来找到这个函数;
#find_runtime_morestack.py
def create_runtime_ms():
    debug('Attempting to find runtime_morestack function for hooking on...')
    text_seg = ida_segment.get_segm_by_name('.text')
    # This code string appears to work for ELF32 and ELF64 AFAIK
    runtime_ms_end = ida_search.find_text(text_seg.startEA, 0, 0, "word ptr ds:1003h, 0", SEARCH_DOWN)
    runtime_ms = ida_funcs.get_func(runtime_ms_end)
    if idc.MakeNameEx(runtime_ms.startEA, "runtime_morecontext", SN_PUBLIC):
        debug('Successfully found runtime_morecontext')
    else:
        debug('Failed to rename function @ 0x%x to runtime_morestack' % runtime_ms.startEA)
    return runtime_ms
在找到函数之后,我们可以递归地遍历所有函数调用。 这是因为GO函数的结构似乎总是:
//golang_undefined_function_example
.text:08089910				               ; 函数的开始 - 但是我们的ida好像并没有发现这一点
.text:08089910 loc_8089910:                            ; CODE XREF: .text:0808994B
.text:08089910                                         ; DATA XREF: sub_804B250+1A1
.text:08089910                 mov     ecx, large gs:0
.text:08089917                 mov     ecx, [ecx-4]
.text:0808991D                 cmp     esp, [ecx+8]
.text:08089920                 jbe     short loc_8089946
.text:08089922                 sub     esp, 4
.text:08089925                 mov     ebx, [edx+4]
.text:08089928                 mov     [esp], ebx
.text:0808992B                 cmp     dword ptr [esp], 0
.text:0808992F                 jz      short loc_808993E
.text:08089931
.text:08089931 loc_8089931:                            ; CODE XREF: .text:08089944
.text:08089931                 add     dword ptr [esp], 30h
.text:08089935                 call    sub_8052CB0
.text:0808993A                 add     esp, 4
.text:0808993D                 retn
.text:0808993E ; ---------------------------------------------------------------------------
.text:0808993E
.text:0808993E loc_808993E:                            ; CODE XREF: .text:0808992F
.text:0808993E                 mov     large ds:0, eax
.text:08089944                 jmp     short loc_8089931
.text:08089946 ; ---------------------------------------------------------------------------
.text:08089946
.text:08089946 loc_8089946:                            ; CODE XREF: .text:08089920
.text:08089946                 call    runtime_morestack ; 函数结束,调用runtime_morestack
.text:0808994B                 jmp     short loc_8089910 ;回到函数头
上面的代码片段是一个未定义的函数,我从已编译的去除符号的示例应用程序中提取。 本质上,通过向后遍历每个未定义的函数,我们将落在地址0x0808994B,这是call runtime_morestack。 从这里开始我们将跳到下一条指令并确保它是我们当前所处的函数,如果这是真的,我们可以假设jmp的地址是一个函数的开始。 在这个例子中(几乎每个我运行的测试用例)都是如此。所以现在我们就有了MakeFunction函数所需的两个参数,写脚本吧!
#traverse_functions.py
def is_simple_wrapper(addr):
    if GetMnem(addr) == 'xor' and GetOpnd(addr, 0) == 'edx' and  GetOpnd(addr, 1) == 'edx':
        addr = FindCode(addr, SEARCH_DOWN)
        if GetMnem(addr) == 'jmp' and GetOpnd(addr, 0) == 'runtime_morestack':
            return True
    return False
def create_runtime_ms():
    debug('Attempting to find runtime_morestack function for hooking on...')
    text_seg = ida_segment.get_segm_by_name('.text')
    # This code string appears to work for ELF32 and ELF64 AFAIK
    runtime_ms_end = ida_search.find_text(text_seg.startEA, 0, 0, "word ptr ds:1003h, 0", SEARCH_DOWN)
    runtime_ms = ida_funcs.get_func(runtime_ms_end)
    if idc.MakeNameEx(runtime_ms.startEA, "runtime_morestack", SN_PUBLIC):
        debug('Successfully found runtime_morestack')
    else:
        debug('Failed to rename function @ 0x%x to runtime_morestack' % runtime_ms.startEA)
    return runtime_ms
def traverse_xrefs(func):
    func_created = 0
    if func is None:
        return func_created
    # First
    func_xref = ida_xref.get_first_cref_to(func.startEA)
    # Attempt to go through crefs
    while func_xref != 0xffffffffffffffff:
        # See if there is a function already here
        if ida_funcs.get_func(func_xref) is None:
            # Ensure instruction bit looks like a jump
            func_end = FindCode(func_xref, SEARCH_DOWN)
            if GetMnem(func_end) == "jmp":
                # Ensure we're jumping back "up"
                func_start = GetOperandValue(func_end, 0)
                if func_start < func_xref:
                    if idc.MakeFunction(func_start, func_end):
                        func_created += 1
                    else:
                        # If this fails, we should add it to a list of failed functions
                        # Then create small "wrapper" functions and backtrack through the xrefs of this
                        error('Error trying to create a function @ 0x%x - 0x%x' %(func_start, func_end))
        else:
            xref_func = ida_funcs.get_func(func_xref)
            # Simple wrapper is often runtime_morestack_noctxt, sometimes it isn't though...
            if is_simple_wrapper(xref_func.startEA):
                debug('Stepping into a simple wrapper')
                func_created += traverse_xrefs(xref_func)
            if ida_funcs.get_func_name(xref_func.startEA) is not None and 'sub_' not in ida_funcs.get_func_name(xref_func.startEA):
                debug('Function @0x%x already has a name of %s; skipping...' % (func_xref, ida_funcs.get_func_name(xref_func.startEA)))
            else:
                debug('Function @ 0x%x already has a name %s' % (xref_func.startEA, ida_funcs.get_func_name(xref_func.startEA)))
        func_xref = ida_xref.get_next_cref_to(func.startEA, func_xref)
    return func_created
def find_func_by_name(name):
    text_seg = ida_segment.get_segm_by_name('.text')
    for addr in Functions(text_seg.startEA, text_seg.endEA):
        if name == ida_funcs.get_func_name(addr):
            return ida_funcs.get_func(addr)
    return None
def runtime_init():
    func_created = 0
    if find_func_by_name('runtime_morestack') is not None:
        func_created += traverse_xrefs(find_func_by_name('runtime_morestack'))
        func_created += traverse_xrefs(find_func_by_name('runtime_morestack_noctxt'))
    else:
        runtime_ms = create_runtime_ms()
        func_created = traverse_xrefs(runtime_ms)
    return func_created
该代码有点冗长,但希望注释和原理足够清晰。 可能没有必要以递归的方式显式地向后遍历,但是我在理解之前写了这个,之后我认为runtime_morestack_noctxt(edgecase)是唯一的边缘例。 这最初是由is_simple_wrapper函数处理的。 无论如何,运行这种代码风格最终找到了IDA Pro缺少的所有未识别的函数。 我们可以在下面看到,这可以带来更清晰,更轻松的逆向体验;


这可以让我们使用像Diaphora这样的东西,因为我们可以专门针对具有相同名称的函数。我个人发现这对于恶意软件或其他你并不关心任何框架/运行时函数的目标非常有用。您可以轻松区分用户代码。现在进入我想要在逆向恶意软件时解决的最后一个挑战,加载字符串!老实说,我不是100%确定IDA如何检测大多数字符串加载,可能是通过某种习语?或者也许是因为它可以根据最后的\00字符检测字符串?无论如何,Go似乎使用某种字符串表,而不需要空字符。看起来是字母数字顺序,也是字符串长度大小。这意味着我们可以看到它们,但是ida通常不会将它们正确地断言为字符串,或者我们将它们视为非常大的字符串blob。Hello World的例子并不擅长这个,所以我将拉开Rex恶意软件的main.main功能来展示这个

我不想为所有内容添加注释,所以我只注释了前几行,然后箭头指向应该指向正确字符串的指针。 我们可以看到一些不同的用例,有时候目标寄存器似乎也会改变。 然而,肯定有一种大致的形式。 将指针移动到寄存器中,然后使用该寄存器推入(d)word指针,然后加载一个字符串的长度。 上脚本!
#string_hunting.py
# 这里它用的是ebx但理论上用什么都有可能
VALID_REGS = ['ebx', 'ebp']
# 这里它用的是esp但理论上用什么都有可能
VALID_DEST = ['esp', 'eax', 'ecx', 'edx']
def is_string_load(addr):
    patterns = []
    #检查第一部分
    if GetMnem(addr) == 'mov':
        # 可能是 unk_ or asc_,甚至 loc_ or inside []
        if GetOpnd(addr, 0) in VALID_REGS and not ('[' in GetOpnd(addr, 1) or 'loc_' in GetOpnd(addr, 1)) and('offset ' in GetOpnd(addr, 1) or 'h' in GetOpnd(addr, 1)):
            from_reg = GetOpnd(addr, 0)
            # 检查第二部分
            addr_2 = FindCode(addr, SEARCH_DOWN)
            try:
                dest_reg = GetOpnd(addr_2, 0)[GetOpnd(addr_2, 0).index('[') + 1:GetOpnd(addr_2, 0).index('[') + 4]
            except ValueError:
                return False
            if GetMnem(addr_2) == 'mov' and dest_reg in VALID_DEST and ('[%s' % dest_reg) in GetOpnd(addr_2, 0) and GetOpnd(addr_2, 1) == from_reg:
                # 检查最后的部分,代码应该还可以改进
                addr_3 = FindCode(addr_2, SEARCH_DOWN)
                if GetMnem(addr_3) == 'mov' and (('[%s+' % dest_reg) in GetOpnd(addr_3, 0) or GetOpnd(addr_3, 0) in VALID_DEST) and 'offset ' not in GetOpnd(addr_3, 1) and 'dword ptr ds' not in GetOpnd(addr_3, 1):
                    try:
                        dumb_int_test = GetOperandValue(addr_3, 1)
                        if dumb_int_test > 0 and dumb_int_test < sys.maxsize:
                            return True
                    except ValueError:
                        return False
def create_string(addr, string_len):
    debug('Found string load @ 0x%x with length of %d' % (addr, string_len))
    # This may be overly aggressive if we found the wrong area...
    if GetStringType(addr) is not None and GetString(addr) is not None and len(GetString(addr)) != string_len:
        debug('It appears that there is already a string present @ 0x%x' % addr)
        MakeUnknown(addr, string_len, DOUNK_SIMPLE)
    if GetString(addr) is None and MakeStr(addr, addr + string_len):
        return True
    else:
        #如果ida已经给出了错误的分析,我们要先MakeUnknown它
        MakeUnknown(addr, string_len, DOUNK_SIMPLE)
        if MakeStr(addr, addr + string_len):
            return True
        debug('Unable to make a string @ 0x%x with length of %d' % (addr, string_len))
    return False
上面的代码可能会被优化,但它对我需要的样本起作用。 剩下的活就是创建另一个函数来搜索所有已定义的代码段以查找字符串加载。 然后我们可以使用指向字符串的指针和字符串长度来使用MakeStr定义一个新字符串。 在我最终使用的代码中,您需要确保IDA Pro没有错误地创建字符串,因为它有时会错误地尝试。 当表中的字符串包含空字符时,有时会发生这种情况。在使用上面的代码之后,这就是我们留下的内容;

看着舒服多了。 在我们将所有这些脚本放在一起之后,我们现在为IDA Pro提供了golang_loader_assist.py模块。 但是有一点警告,我只有时间在几个版本的IDA Pro for OSX上进行测试,大部分测试都在6.95上。 也很可能应该进行优化,或者至少对代码进行一些修改。 尽管如此,我想开源,以便其他人可以使用它,并希望大家的回馈。 另外请注意,这个脚本可能会非常缓慢,具体取决于idb文件的大小,使用IDA Pro 6.95上的2.2 GHz Intel Core i7在OSX El Capitan(10.11.6)上工作 - 字符串发现也得一会。单独运行不同的方法可以防止IDA锁死。 希望这个博客和代码对某人有用,enjoy!




[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最新回复 (4)
奔跑的阿狸 1 2019-8-13 11:54
2
0
wsc 2019-8-13 14:06
3
0
  
myeanngg 2019-8-17 15:34
4
0
我學golang就是因爲它的文件足夠大莫名其妙大函數足夠多
myeanngg 2019-8-17 15:35
5
0
期望樓主發表更多大作品
游客
登录 | 注册 方可回帖
返回