【应急响应】工具源码学习beaconEye扫描原理

admin 2025年2月10日12:59:02评论11 views字数 9698阅读32分19秒阅读模式

个人项目:https://github.com/mir1ce/Hawkeye,一款Windows应急响应工具,喜欢的老铁可以点个star
【应急响应】工具源码学习beaconEye扫描原理

项目文件解读

该项目一共分为三个部分:main.go、beaconEye以及win32

main.go

该文件主要是整个程序的入口文件,程序执行后会根据main文件中的执行顺序进行扫描工作

beaconEye

该文件夹下主要是涵盖BeaconEye所有的扫描文件,其中beaconEye.go是核心扫描代码文件。

而config.go主要是根据 ConfigShortItem结构体写的一些实现方法,我们从文件名不难看出主要是编写一些配置文件,比如内存解析后的结果配置等等。

process.go和utils.go,其实就是个工具代码文件,process.go主要是用来遍历当前系统进程,获取进程的一些相关信息.这里值得注意的是,process.go中的GetProcesses函数,返回的切片是关于正在运行状态的进程切片

func GetProcesses() (needScanProcesses []gops.Process, err error) {var processes []gops.Process      // 声明一个gops.Process类型的切片变量processesprocesses, err = gops.Processes() // 调用gops库的Processes函数获取进程pid、ppid,并将结果赋值给processes变量if err != nil {return// 如果发生错误,则直接返回}for_, process := range processes { // 遍历processes切片var basicInfo win32.PROCESS_BASIC_INFORMATION                                                    // 声明一个win32.PROCESS_BASIC_INFORMATION类型的变量basicInfovar retLen win32.ULONG                                                                           // 声明一个win32.ULONG类型的变量retLenhProcess := win32.OpenProcess(win32.PROCESS_ALL_ACCESS, win32.FALSE, win32.DWORD(process.Pid())) // 调用win32库的OpenProcess函数打开进程,并将结果赋值给hProcess变量if hProcess == 0 {                                                                               // 如果hProcess为0,则表示打开进程失败,继续下一次循环continue}_, err = win32.NtQueryInformationProcess(hProcess,win32.ProcessBasicInformation,unsafe.Pointer(&basicInfo),win32.ULONG(win32.SizeOfProcessBasicInformation),&retLen,) // 调用win32库的NtQueryInformationProcess函数获取进程的基本信息,并将结果赋值给err变量if err != nil { // 如果发生错误,则将错误信息包装成fmt.Errorf类型,并继续下一次循环err = fmt.Errorf("NtQueryInformationProcess error: %v", err)continue}if basicInfo.ExitStatus == uintptr(win32.STATUS_PENDING) { // 如果进程的退出状态为win32.STATUS_PENDING(正在运行),则将该进程添加到needScanProcesses切片中needScanProcesses = append(needScanProcesses, process)}}return// 返回needScanProcesses切片和err变量}

而utils就是个工具类,里面的函数主要是做一些常见的操作,具体如下:UintptrListContains(list []uintptr, v uintptr) bool:检查给定的 uintptr 列表中是否包含指定的 uintptr 值。如果列表中存在该值,则返回 true,否则返回 false。BytesIndexOf(b []byte, c byte, startIdx int) (ret int):在字节切片 b 中查找第一个出现的字节 c 的索引位置。如果 c 不在 b 中,则返回 -1。ReadInt64(r io.Reader) int64:从 io.Reader 中读取 8 个字节,并将其解析为一个有符号的 64 位整数(int64)。ReadInt32(r io.Reader) int32:从 io.Reader 中读取 4 个字节,并将其解析为一个有符号的 32 位整数(int32)。ReadInt16(r io.Reader) int16:从 io.Reader 中读取 2 个字节,并将其解析为一个有符号的 16 位整数(int16)。

主函数部分

主函数部分主要是先创建一个通道,用于在不同goroutine进行数据的传输,主要目的还是用来写入我们最后的扫描结果。

后面开启一个goroutine执行匿名函数,进行扫描。然后定义一个初始值count为0,用于计数当前主机有多少个C2进程,最后遍历evilResults切片,打印扫描结果,c2进程名、c2 pid以及分布在内存中的地址信息,FindEvil默认开启4个线程进行检索。

funcmain() {fmt.Printf("%snnn", banner())v1 := time.Now()evilResults := make(chan beaconeye.EvilResult)gofunc() {err := beaconeye.FindEvil(evilResults, 4)if err != nil {panic(err)}}()count := 0for v := range evilResults {fmt.Printf("%s (%d), Keys Found:True, Configuration Address: 0x%xn", v.Name, v.Pid, v.Address)fmt.Printf("%sn", v.Extractor.GetConfigText())count++}v2 := time.Now()fmt.Printf("The program took %v to find out %d processesn", v2.Sub(v1), count)}

恶意进程扫描

在FindEvil函数中。程序先获取所有的进程,并存储到一个切片里面,用于后面进行检索。

其中。searchIn 和 searchOut 是两个带缓冲的通道,分别用于输入和输出搜索任务。缓冲区大小为100。searchIn 通道用于接收待搜索的内存块信息。searchOut 通道用于输出搜索结果。

在进行检索的过程中,程序先后会调用GetMatchArrayAndNext->GetMatchArray->GetNext来初始化规则。方便后续在内存中进行扫描。

type sSearchIn struct {procScan   *ProcessScanmatchArray []uint16nextArray  []int16memInfo    win32.MemoryInfoprocess    gops.Process}var onceCloseSearchOut sync.Oncetype sSearchOut struct {procScan *ProcessScanprocess  gops.Processaddr     uintptr}funcFindEvil(evilResults chan EvilResult, threadNum int) (err error) {var processes []gops.Processprocesses, err = GetProcesses() //获取进程信息if err != nil {return}rule64 := "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ?? 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ?? ?? 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ?? ?? ?? ?? 00 00 00 00 02 00 00 00 00 00 00 00 ?? ?? ?? ?? 00 00 00 00 01 00 00 00 00 00 00 00 ?? ?? 00 00 00 00 00 00"rule32 := "00 00 00 00 00 00 00 00 01 00 00 00 ?? 00 00 00 01 00 00 00 ?? ?? 00 00 02 00 00 00 ?? ?? ?? ?? 02 00 00 00 ?? ?? ?? ?? 01 00 00 00 ?? ?? 00 00"/*处理规则数组*/matchArray64, nextArray64, err := GetMatchArrayAndNext(rule64)if err != nil {return}matchArray32, nextArray32, err := GetMatchArrayAndNext(rule32)if err != nil {return}/*处理规则数组结束*//*创建缓冲区大小*/searchIn := make(chan sSearchIn, 100)searchOut := make(chan sSearchOut, 100)initMultiThreadSearchMemoryBlock(threadNum, searchIn, searchOut)go handleItemFromSearchOut(searchOut, evilResults)for _, process := range processes {// if the process is itslef, then skipif os.Getpid() == process.Pid() {continue}processScan, err := NewProcessScan(win32.DWORD(process.Pid()))if err != nil {fmt.Printf("init process info error: %vn", err)continue}nextArray := nextArray32matchArray := matchArray32if processScan.Is64Bit {nextArray = nextArray64matchArray = matchArray64}processScan.SearchMemory(matchArray, nextArray, process, searchIn)}close(searchIn)return}

上面提到了初始化。具体代码实现如下:

阶段一:GetMatchArrayAndNext先会将字符串解析为匹配数组,函数将字符串分割成单个代码,并将每个代码转换为 uint16 类型的数组元素。如果代码是 "??",则将其视为通配符(ARBITRARY),表示可以匹配任何字节。如果代码是有效的十六进制字符串,则将其解码为字节并存储在 matchArray 中。

阶段二:GetMatchArrayAndNext获取到matchArray后,会将matchArray交给GetNext函数进行处理,nextArray 的长度为 260,初始化时每个元素都设置为 NOP。nextArray 的作用是记录 matchArray 中每个字节最后出现的位置。通过遍历 matchArray,将每个字节的值作为索引,存储其在 matchArray 中的索引位置到 nextArray 中。

【ps】ARBITRARY 和 NOP 是特殊标记,可能用于处理通配符匹配和无匹配情况。

func GetMatchArrayAndNext(rule string) (matchArray []uint16, nextArray []int16, err error) {matchArray, err = GetMatchArray(rule)if err != nil {return}nextArray = GetNext(matchArray)return}// GetMatchArray get []uint16 from stringfunc GetMatchArray(matchStr string) ([]uint16, error) {codes := strings.Split(matchStr, " ")result := make([]uint16, len(codes))for i, c := range codes {if c == "??" {result[i] = ARBITRARYelse {bs, err := hex.DecodeString(c)if err != nil {return nil, err}result[i] = uint16(bs[0])}}return result, nil}func GetNext(matchArray []uint16) []int16 {//特征码(字节集)的每个字节的范围在0-2550-FF)之间,256用来表示问号,到260是为了防止越界next := make([]int16, 260)for i := 0; i < len(next); i++ {next[i] = NOP}for i := 0; i < len(matchArray); i++ {next[matchArray[i]] = int16(i)}returnnext}

当处理完后,函数会执行如下代码,创建了searchIn和searchOut两个通道,分别用于向多线程搜索内存块的函数传递输入数据和接收输出数据。然后调用initMultiThreadSearchMemoryBlock函数启动指定数量threadNum的线程来进行内存搜索操作,并通过go关键字启动一个协程来处理从searchOut通道中获取到的搜索结果,将结果整理后发送到evilResults通道中,其作用主要是将检索到的C2进程结果保存到evilResults。

而以下两个函数的主要作用就是去处理搜索到的内存信息然后进行内存检索。

initMultiThreadSearchMemoryBlock(threadNum, searchIn, searchOut)go handleItemFromSearchOut(searchOut, evilResults)

而核心调用则是initMultiThreadSearchMemoryBlock会去调用SearchMemoryBlock,也就是说,initMultiThreadSearchMemoryBlock(threadNum, searchIn, searchOut)创建了四个SearchMemoryBlock函数用于接收并处理searchIn通道传过来的数据,处理结束后,则会将结果存储在resultArray中。

funcinitMultiThreadSearchMemoryBlock(threadNum int, searchIn chan sSearchIn, searchOut chan sSearchOut) {for i := 0; i < threadNum; i++ {gofunc() {for item := range searchIn {var resultArray []MatchResultif err := SearchMemoryBlock(item.procScan.Handle, item.matchArray, uint64(item.memInfo.BaseAddress), int64(item.memInfo.RegionSize), item.nextArray, &resultArray); err != nil {fmt.Printf("SearchMemoryBlock error: %vn", err)continue}for j := range resultArray {searchOut <- sSearchOut{procScan: item.procScan,process:  item.process,addr:     uintptr(resultArray[j].Addr),}}}onceCloseSearchOut.Do(func() {close(searchOut)})}()}}

而检索恶意进程的核心代码则是SearchMemoryBlock,采用了sunday算法实现比较,具体代码如下,该函数的作用则搜索指定进程内存块中是否存在与给定模式数组 matchArray 匹配的子串,并将匹配结果存储在 ResultArray 中。

func SearchMemoryBlock(hProcess win32.HANDLE, matchArray []uint16, startAddr uint64, size int64, next []int16, pResultArray *[]MatchResult) (err error) {var memBuf []bytememBuf, err = win32.NtReadVirtualMemory(hProcess, win32.PVOID(startAddr), size)size = int64(len(memBuf))if err != nil {err = fmt.Errorf("%v: %v", err, syscall.GetLastError())return}// sunday algorithm implementi := 0      // 父串indexj := 0      // 字串indexoffset := 0 // 下次匹配的偏移(基于起始位置0for int64(offset) < size {// 将父串index设置到偏移量,字串index设置到0i = offsetj = 0// 判断匹配for j < len(matchArray) && int64(i) < size {if matchArray[j] == uint16(memBuf[i]) || int(matchArray[j]) == ARBITRARY {i++j++else {break}}// 如果一直到最后一位,则代表匹配成功if j == len(matchArray) {*pResultArray = append(*pResultArray, MatchResult{Addr: startAddr + uint64(offset),})}// 移至子串在父串对应位置的末尾,如果超出长度则没有匹配到if int64(offset+len(matchArray)) >= size {return}// 获取父串中字串末尾所在位置字符,将子串中和该位置相等的字符对齐// 得出字串需要移动多少位valueAtMIdx := memBuf[offset+len(matchArray)]idxInSub := next[valueAtMIdx]if idxInSub == NOP { // 可能是匹配不到,或者可以匹配到 ?? 符号offset = offset + (len(matchArray) - int(next[ARBITRARY])) // 如果字串存在 ?? 号,则下次匹配移动到该位置开始匹配,否则移动到末尾,即 m = m + 字串长度 + 1else {offset = offset + (len(matchArray) - int(idxInSub))}}return}

然后就是开始根据获取到的进程信息进行循环,遍历所有进程,跳过自身进程,对每个进程通过NewProcessScan函数初始化进程扫描相关信息,根据进程是32位还是64位选择合适的matchArray和nextArray,并调用processScan.SearchMemory函数将相关信息发送到searchIn通道,触发对该进程内存的搜索。而以下函数主要是对进程的内存信息进行整理和筛选,获取可访问的内存区域信息(存储在memoryInfos中),然后将相关的进程扫描信息、匹配数组、下一个位置数组以及内存信息等通过searchIn通道发送出去,以便后续在这些内存区域中进行具体的匹配查找操作。

func (p *ProcessScan) SearchMemory(matchArray []uint16, nextArray []int16, process gops.Process, searchIn chan sSearchIn) {    var memoryInfos []win32.MemoryInfo    // streamlining memory block information    tmp := p.Heaps[:]for {if len(tmp) == 0 {break        }start := uintptr(0)end := uintptr(0)        var needDel []uintptrfor _, heap := range tmp {            memInfo, err := win32.QueryMemoryInfo(p.Handle, win32.LPCVOID(heap))if err!= nil {                needDel = append(needDel, heap)                fmt.Printf("error: %vn", err)continue            }start = uintptr(memInfo.BaseAddress)end = uintptr(memInfo.BaseAddress) + uintptr(memInfo.RegionSize)            memoryInfos = append(memoryInfos, memInfo)break        }        tmp_ := tmp[:]        tmp = []uintptr{}        // remove addr which need to be deleted or in previous [startendfor next cyclefor _, heap := range tmp_ {if!(UintptrListContains(needDel, heap) || (heap >= start && heap < end)) {                tmp = append(tmp, heap)            }        }    }    // search matchfor _, memInfo := range memoryInfos {if memInfo.NoAccess {continue        }        searchIn <- sSearchIn{            procScan:   p,            matchArray: matchArray,            nextArray:  nextArray,            memInfo:    memInfo,process:    process,        }    }}

流程图

总的原理,获取内存信息,利用sundy算法实现规则数组与内存信息的快速匹配(从文本的左端开始,将模式串与文本对齐。从模式串的最右端开始向左比较字符,直到找到不匹配的字符或模式串完全匹配。如果匹配失败,算法会查看文本中当前窗口的下一个字符(即当前窗口的末尾字符的下一个字符),并使用偏移表来决定模式串应该向右移动多少位。),从而实现C2进程的检索。至于解析相关信息,则是根据profile相关信息进行相关工作的,具体的网上也有很多讲解,就不多赘述了。

【应急响应】工具源码学习beaconEye扫描原理

原文始发于微信公众号(事件响应回忆录):【应急响应】工具源码学习--beaconEye扫描原理

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月10日12:59:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【应急响应】工具源码学习beaconEye扫描原理https://cn-sec.com/archives/3722002.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息