beaconEye

0 20
Explanation of project filesThis project is divided into three parts in total: m...

Explanation of project files

This project is divided into three parts in total: main.go, beaconEye, and win32.

main.go

This file is mainly the entry file of the entire program, and the scanning work will be performed according to the execution order in the main file after the program is executed.

beaconEye

beaconEye

The folder mainly covers all scanning files of BeaconEye, among which beaconEye.go is the core scanning code file.

While config.go mainly contains implementation methods based on the ConfigShortItem structure, it is not difficult to see from the filename that it is mainly to write some configuration files, such as the configuration of the results after memory parsing, etc.

process.go and utils.go are actually tool code files. process.go is mainly used to traverse the current system processes and obtain some relevant information about the processes. It is worth noting that the GetProcesses function in process.go returns a slice of processes in the running state.

func GetProcesses() (needScanProcesses []gops.Process, err error) {  
var processes []gops.Process // Declare a gops.Process type slice variable processes  
processes, err = gops.Processes() // Call the Processes function from the gops library to get the process pid, ppid, and assign the result to the processes variable  
if err != nil {  
return // If an error occurs, return directly  
  

for _, process := range processes { // Iterate over the processes slice  
var basicInfo win32.PROCESS_BASIC_INFORMATION // Declare a win32.PROCESS_BASIC_INFORMATION type variable basicInfo  
var retLen win32.ULONG // Declare a win32.ULONG type variable retLen  
hProcess := win32.OpenProcess(win32.PROCESS_ALL_ACCESS, win32.FALSE, win32.DWORD(process.Pid())) // Call the OpenProcess function from the win32 library to open the process, and assign the result to the hProcess variable  
if hProcess == 0 { // If hProcess is 0, it means that the process opening failed, continue to the next loop  
continue  
  
_, err = win32.NtQueryInformationProcess(  
hProcess,  
win32.ProcessBasicInformation,  
unsafe.Pointer(&basicInfo),  
win32.ULONG(win32.SizeOfProcessBasicInformation),  
&retLen,  
) // Call the NtQueryInformationProcess function from the win32 library to get the basic information of the process, and assign the result to the err variable  
if err != nil { // If an error occurs, wrap the error information into fmt.Errorf type,and continue with the next loop  
err = fmt.Errorf("NtQueryInformationProcess error: %v", err)  
continue  
  
if basicInfo.ExitStatus == uintptr(win32.STATUS_PENDING) { // If the process's exit status is win32.STATUS_PENDING (running), add the process to the needScanProcesses slice  
needScanProcesses = append(needScanProcesses, process)  
  
  
return // Returns the needScanProcesses slice and the err variable  


The utils is a utility class, and the functions inside it mainly perform common operations, as follows:
UintptrListContains(list []uintptr, v uintptr) bool: Checks if the given uintptr list contains the specified uintptr value. If the value exists in the list, it returns true; otherwise, it returns false.
BytesIndexOf(b []byte, c byte, startIdx int) (ret int): Finds the index position of the first occurrence of the byte c in the byte slice b. If c is not in b, it returns -1.
ReadInt64(r io.Reader) int64: Reads 8 bytes from io.Reader and parses them into a signed 64-bit integer (int64).
ReadInt32(r io.Reader) int32: Reads 4 bytes from io.Reader and parses them into a signed 32-bit integer (int32).
ReadInt16(r io.Reader) int16: Reads 2 bytes from io.Reader and parses them into a signed 16-bit integer (int16).

The main function part

The main function part is mainly to create a channel for data transmission between different goroutines, and its main purpose is still to write our final scanning results.

A goroutine is started to execute an anonymous function for scanning. Then, an initial value count is defined as 0, used to count the number of C2 processes currently running on the host. Finally, the evilResults slice is traversed to print the scanning results, including the C2 process name, C2 PID, and memory address information distributed in memory, with FindEvil defaulting to opening 4 threads for retrieval.

func main() {  
fmt.Printf("%s

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

Malicious Process Scanning

In the FindEvil function, the program first retrieves all processes and stores them in a slice for later search.

Among them, searchIn and searchOut are two buffered channels, respectively used for input and output of search tasks. The buffer size is 100.
The searchIn channel is used for receiving information about the memory blocks to be searched.
The searchOut channel is used for outputting search results.

During the search process, the program will sequentially call GetMatchArrayAndNext->GetMatchArray->GetNext to initialize the rules. This is convenient for scanning in memory later.

type sSearchIn struct {  
procScan *ProcessScan  
matchArray []uint16  
nextArray []int16  
memInfo win32.MemoryInfo  
process gops.Process  
  

var onceCloseSearchOut sync.Once  

type sSearchOut struct {  
procScan *ProcessScan  
process gops.Process  
addr uintptr  
  

func FindEvil(evilResults chan EvilResult, threadNum int) (err error) {  
var processes []gops.Process  
processes, err = GetProcesses() // Get process information  
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"  
/* Processing rule array */  
matchArray64, nextArray64, err := GetMatchArrayAndNext(rule64)  
if err != nil {  
return  
  
matchArray32, nextArray32, err := GetMatchArrayAndNext(rule32)  
if err != nil {  
return  
  
/* End of processing rule array */  
/* Create buffer size */  
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 itself, then skip  
if 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 := nextArray32  
matchArray := matchArray32  
if processScan.Is64Bit {  
nextArray = nextArray64  
matchArray = matchArray64  
  
processScan.SearchMemory(matchArray, nextArray, process, searchIn)  
  
close(searchIn)  
return  

memInfo win32.MemoryInfo
process gops.Process

var onceCloseSearchOut sync.Once

【ps】ARBITRARY and NOP are special markers that may be used to handle wildcard matching and no match cases.

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 string  
func GetMatchArray(matchStr string) ([]uint16, error) {  
codes := strings.Split(matchStr, " ")  
result := make([]uint16, len(codes))  
for i, c := range codes {  
if c == "??" {  
result[i] = ARBITRARY  
} else {  
bs, err := hex.DecodeString(c)  
if err != nil {  
return nil, err  
  
result[i] = uint16(bs[0])  
  
  
return result, nil  
  

func GetNext(matchArray []uint16) []int16 {  
// The range of each byte of the feature code (byte set) is between 0-255 (0-FF), 256 is used to represent a question mark, and 260 is used to prevent out-of-bounds  
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)  
  
return next  


return

nextArray = GetNext(matchArray)
return

The core call is that initMultiThreadSearchMemoryBlock calls SearchMemoryBlock, which means that initMultiThreadSearchMemoryBlock(threadNum, searchIn, searchOut) creates four SearchMemoryBlock functions to receive and process the data from the searchIn channel. After processing, the results will be stored in resultArray.

func initMultiThreadSearchMemoryBlock(threadNum int, searchIn chan sSearchIn, searchOut chan sSearchOut) {  
for i := 0; i < threadNum; i++ {  
go func() {  
for item := range searchIn {  
var resultArray []MatchResult  
if 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)  
}  
}  
  

The core code for searching malicious processes is SearchMemoryBlock, which uses the Sunday algorithm for comparison. The specific code is as follows. The function's purpose is to search for a substring that matches the given pattern array matchArray in the specified process memory block and stores the matching results in ResultArray.

func SearchMemoryBlock(hProcess win32.HANDLE, matchArray []uint16, startAddr uint64, size int64, next []int16, pResultArray *[]MatchResult) (err error) {  
var memBuf []byte  
memBuf, 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 implementation  
i := 0 // Parent string index  
j := 0 // Substring index  
offset := 0 // Next match offset (based on starting position 0)  

for int64(offset) < size {  
// Set the parent string index to the offset, and the substring index to 0  
i = offset  
j = 0  
// Judgment of match  
for j < len(matchArray) && int64(i) < size {  
if matchArray[j] == uint16(memBuf[i]) || int(matchArray[j]) == ARBITRARY {  
i++  
j++  
} else {  
break  
  
  

// If it matches until the last digit, it means the match is successful  
if j == len(matchArray) {  
*pResultArray = append(*pResultArray, MatchResult{  
Addr: startAddr + uint64(offset),  
}  
  

// Move to the end of the substring at the corresponding position in the parent string, and no match is found if it exceeds the length  
if int64(offset+len(matchArray)) >= size {  
return  
  

// Get the character at the end of the substring in the parent string, and align the characters in the substring that are equal to this position  
// Determine how many bits the string needs to move  
valueAtMIdx := memBuf[offset+len(matchArray)]  
idxInSub := next[valueAtMIdx]  
if idxInSub == NOP { // It may not be matched or can match the ?? symbol  
offset = offset + (len(matchArray) - int(next[ARBITRARY])) // If the string contains ??, the next match will start from this position, otherwise it will move to the end, i.e., m = m + string length + 1  
} else {  
offset = offset + (len(matchArray) - int(idxInSub))  
  
  
return  


Then it starts to loop based on the obtained process information, traversing all processes, skipping the current process, and initializing the process scanning related information for each process through the NewProcessScan function. It selects the appropriate matchArray and nextArray based on whether the process is 32-bit or 64-bit, and calls the processScan.SearchMemory function to send the relevant information to the searchIn channel, triggering the search of the process memory. The following functions are mainly for sorting and filtering the process memory information, obtaining the accessible memory area information (stored in memoryInfos), and then sending the relevant process scanning information, match array, next position array, and memory information through the searchIn channel to facilitate subsequent specific matching search operations in these memory areas.

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 []uintptr  
for _, 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 [start, end] for next cycle  
for _, heap := range tmp_ {  
if!(UintptrListContains(needDel, heap) || (heap >= start && heap < end)) {  
tmp = append(tmp, heap)  
  
  
  
// search match  
for _, memInfo := range memoryInfos {  
if memInfo.NoAccess {  
continue  
  
searchIn <- sSearchIn{  
procScan: p,  
matchArray: matchArray,  
nextArray: nextArray,  
memInfo: memInfo,  
process: process,  
  
  

你可能想看:
最后修改时间:
admin
上一篇 2025年03月27日 14:22
下一篇 2025年03月27日 14:44

评论已关闭