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

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,
Let’s make 2025 cyber’s most diverse year yet
3 JD open-source hotkey—Automatic detection of hotkey, distributed consistency caching solution
hire grey hat hackers(Grey Hat Hackers)
GPT-3: in-context learning + few-shot learning, 175 billion parameters, 96 layers, 500 billion words
From the perspective of devices, the medical industry has become a key area of cybersecurity
Looking forwards- Two experts predict the next 5 years in cyber
A journey towards leadership in cybersecurity

评论已关闭