go样本加载过程分析

go样本加载过程分析

descriptions

在某一次投稿时候,审核的大佬发现我分析的偏了(其实就是菜),分析了很多的库函数,还犯了些很有意思的错误,既然分析偏了,那就把go的加载过程好好研究一番,弄明白了,本篇文章只是站在样本分析的角度去看go的加载,更加的详细的直接看底部的引用链接。

加载符号表

当我们将样本投入exeinfo或者die查壳发现是go的样本,然后在投入ida分析的时候,我们发现ida没有识别函数符号,都是sub_xxxxxx这样的函数,此时我们是没有办法分析的,我们需要加载go的符号表


项目地址


以上贴的版本是Python3的,由于我自己是mac的,所以,只能用py2的,py2的版本如下

使用以及安装方法

git clone之后,将GO_Utilsgo_entry.py移动到idapython
然后重启idaFile——>Script Command,载入插件

然后点击run

看以上图片 ,第一步第二步是检测样本go版本,检测结果会输出到OUTPUT Windows


基本就识别出来了,function names对应的函数名也会相应的显示出来
类似的项目还有

https://github.com/strazzere/golang_loader_assist

https://github.com/0xjiayu/go_parser

Go程序是怎么运行起来的

当我们开始之前,我们先看一个图


因为golang最关键的就是runtime,有点类似java的虚拟机,不过runtime不是虚拟机,把它理解成标准库更贴切,这张图目的就是让我们更好的了解,当样本被执行的时候,究竟都发生了什么,有一个整体分析的思路
我们这里通过一个小的样本来说明,当我们直接把样本投入到IDA进行调试

.text:0000000000465CC0 ; [00000005 BYTES: COLLAPSED FUNCTION _rt0_amd64_windows. PRESS CTRL-NUMPAD+ TO EXPAN

直接停在了_rt0_amd64_windows,那为什么会停在这个函数?
这里就不得不提一下程序的执行过程中的编译过程链接过程

编译过程是把预处理完的文件进行一系列词法分析,语法分析,语义分析,优化后生成相应的汇编代码文件。

链接过程是把源代码模块独立的编译,然后按照需要将他们“组装”起来,这其中包括地址和空间分配,符号,重定位等等

go也不例外,go需要使用链接器将单个的文件进行链接组装,我们来简单看一下代码

https://tip.golang.org/src/cmd/link/internal/ld/lib.go

//linux build
if *flagEntrySymbol == "" {
  switch ctxt.BuildMode {
  case BuildModeCShared, BuildModeCArchive:
    *flagEntrySymbol = fmt.Sprintf("_rt0_%s_%s_lib", buildcfg.GOARCH, buildcfg.GOOS)
  case BuildModeExe, BuildModePIE:
    *flagEntrySymbol = fmt.Sprintf("_rt0_%s_%s", buildcfg.GOARCH, buildcfg.GOOS)
  case BuildModeShared, BuildModePlugin:
    // No *flagEntrySymbol for -buildmode=shared and plugin
  default:
    Errorf(nil, "unknown *flagEntrySymbol for buildmode %v", ctxt.BuildMode)
  }
}
//windows build
if(INITENTRY == nil) {
		INITENTRY = mal(strlen(goarch)+strlen(goos)+20);
		if(!flag_shared) {
			sprint(INITENTRY, "_rt0_%s_%s", goarch, goos);
		} else {
			sprint(INITENTRY, "_rt0_%s_%s_lib", goarch, goos);
		}
	}
	lookup(INITENTRY, 0)->type = SXREF;

我们综合代码分析出,如果用户没有指定用户地址,那么在x84_64下生成的exe程序默认入口地址就是_rt0_amd64_windows,这时候你分析样本时候,样本在ida一打开,如果是PE的就会自动跳转到_rt0_amd64_windows,如果是ELF就会自动跳转到_rt0_amd64_linux


然后我们F8继续调试,程序进行一系列的调用

.text:000000000046271E loc_46271E:                             ; CODE XREF: _rt0_amd64+A1↑j
.text:000000000046271E lea     rdi, qword_570438
.text:0000000000462725 call    runtime_settls
.text:000000000046272A mov     rbx, gs:28h
.text:0000000000462733 mov     qword ptr [rbx+0], 123h
.text:000000000046273E mov     rax, cs:qword_570438
.text:0000000000462745 cmp     rax, 123h
.text:000000000046274B jz      short loc_462752
.text:000000000046274D call    runtime_abort                   ; 设置线程本地存储
.text:0000000000462752 ; ---------------------------------------------------------------------------
.text:0000000000462752
.text:0000000000462752 loc_462752:                             ; CODE XREF: _rt0_amd64+10B↑j
.text:0000000000462752 mov     rbx, gs:28h
.text:000000000046275B lea     rcx, unk_570260                  
.text:0000000000462762 mov     [rbx+0], rcx
.text:0000000000462769 lea     rax, unk_5703E0                  
.text:0000000000462770 mov     [rax], rcx
.text:0000000000462773 mov     [rcx+30h], rax
.text:0000000000462777 cld                                       
.text:0000000000462778 call    runtime_check                   
.text:000000000046277D mov     eax, dword ptr [rsp+28h+var_18]
.text:0000000000462781 mov     [rsp+28h+var_28], eax
.text:0000000000462784 mov     rax, [rsp+28h+var_10]
.text:0000000000462789 mov     [rsp+28h+var_20], rax
.text:000000000046278E call    runtime_args                    ; 初始化执行文件绝对路径
.text:0000000000462793 call    runtime_osinit                  ; 初始化cpu核数以及内存
.text:0000000000462798 call    runtime_schedinit               ; 初始化命令行参数、环境变量、gc、栈空间等等
.text:000000000046279D lea     rax, off_4EEB20                 ;
.text:00000000004627A4 push    rax
.text:00000000004627A5 push    0
.text:00000000004627A7 call    runtime_newproc                 ; 创建新的goroutine,只是获取参数的起始地址与相关寄存器,真正干活的是newproc1()
.text:00000000004627AC pop     rax
.text:00000000004627AD pop     rax
.text:00000000004627AE call    runtime_mstart                  ; 启动线程M
.text:00000000004627B3 call    runtime_abort                   ; 初始化,内存管理,调用用户自己开发的main_xxx
.text:00000000004627B3 _rt0_amd64 endp

go bootstarp的流程就是首先,检查cpu是否符合,配置好样本运行相关的参数,然后,进行tls初始化(具体看第三个链接),然后调用runtime.argsruntime.osinitruntime.schedinit配置好样本加载时候的各种变量以及调度器,总结如下


然后执行到main这,这块基本就执行攻击者自己实现的恶意代码了,就是以main_xxxx开头的基本都是攻击者自己实现的,我们分析的时候主要分析这块就可以

那么,我们了解完以上,如果攻击者自己实现了一个包或者模块时候,会在main_main之前启动,比如说init_xxx(),xxxx_init,xxx_initx等等,常用来运行前的注册,比如说decoder,parser的注册,运行时只需计算一次的模块,比如sync.once 或者全局数据库连接句柄的初始化等等
我们怎么判断是不是,init()函数具有以下特点

  1. 不能被其他函数调用,这里我们可以按x,查看交叉引用情况
  2. 该函数没有输入参数和返回值
  3. 每个包可能有多个init函数
  4. 包的每个源文件可以有多个init函数
  5. 简单理解就是,导入包依赖关系决定init函数执行顺序(官方解释,不同包的init函数,按照导入包的依赖关系决定init函数的执行顺序)
  6. init函数在main执行之前,会由runtime来调用

参考链接






1 个赞

好东西


服务器资源由ZeptoVM赞助

Partners Wiki Discord