应急处置工具之内存检索工具开发

2024-08-24 91 0

创作背景

在处置应急事件过程,经常会遇到以下几个场景:

  1. 日志没有任何异常、日志文件被清除、日志文件被加密,无法通过主机日志进行取证;
  2. 主机上面没有落地的恶意文件,无法通过文件落地时间进行关联分析;
  3. 安全设备产生告警,例如外联dnslog、外联恶意域名、内网横向爆破,但是无法定位具体进程;
  4. 主机没有部署EDR产品,无法定位攻击者具体做了哪些操作,通过webshell执行了哪些命令,例如是否通过哥斯拉加载过mimikatz抓取主机密码

此时对于应急人员来说,便十分被动,无法进行取证。基于上述几个场景,便开发了这个内存检索工具。由于最近在学习go语言,因此使用go进行开发。

前置知识学习

Windows API

选取Windows API

VirtualQueryEx: 用于查询指定进程的内存信息,如内存的基地址、保护属性、内存状态等。通过这个接口,可以逐块遍历进程的虚拟内存空间

内存状态等。通过这个接口,可以逐块遍历进程的虚拟内存空间

SIZE_T VirtualQueryEx(
  [in]           HANDLE                    hProcess,
  [in, optional] LPCVOID                   lpAddress,
  [out]          PMEMORY_BASIC_INFORMATION lpBuffer,
  [in]           SIZE_T                    dwLength
);

ReadProcessMemory: 允许读取指定进程的内存内容

BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);

K32GetModuleFileNameExW: 用于获取指定进程的可执行文件路径

DWORD GetModuleFileNameExW(
  [in]           HANDLE  hProcess,
  [in, optional] HMODULE hModule,
  [out]          LPWSTR  lpFilename,
  [in]           DWORD   nSize
);

Go如何调用Windows API

大概原理:

官方说明,在 Go 语言中,可以通过syscall包和unsafe包调用 Windows API 接口。这些包允许直接调用底层的 Windows 函数,并且可以操作指针和内存来与 Windows API 交互。

因此我们只需要学会用syscall和unsafe包中对应的方法即可。由于我们所用的Windows api目前go并没有封装,因此我们需要通过dll文件,找对应的函数地址,进行调用。

调用举例

VirtualQueryEx函数

1、首先通过微软官方文档,查看这个函数存在于kernel32.dll中

应急处置工具之内存检索工具开发插图

2、利用syscall加载dll,这里我使用NewLazyDLL方法

modkernel32             = syscall.NewLazyDLL("kernel32.dll")

3、在kernel32.dll中获取函数地址,使用NewProc方法

procVirtualQueryEx      = modkernel32.NewProc("VirtualQueryEx")

4、调用api函数,使用call方法进行调用,并且官方使用说明传入对应参数即可

//call方法调用
procVirtualQueryEx.Call(uintptr(hProcess), lpAddress, uintptr(lpBuffer), dwLength)

//封装函数
func VirtualQueryEx(hProcess syscall.Handle, lpAddress uintptr, lpBuffer unsafe.Pointer, dwLength uintptr) (int, error) {
	ret, _, err := procVirtualQueryEx.Call(uintptr(hProcess), lpAddress, uintptr(lpBuffer), dwLength)
	if ret == 0 {
		return 0, err
	}
	return int(ret), nil
}

5、给大家提供已经封装好的api

func ReadProcessMemory(hProcess syscall.Handle, lpBaseAddress uintptr, lpBuffer unsafe.Pointer, nSize uintptr, lpNumberOfBytesRead *uintptr) (bool, error) {
	ret, _, err := procReadProcessMemory.Call(uintptr(hProcess), lpBaseAddress, uintptr(lpBuffer), nSize, uintptr(unsafe.Pointer(lpNumberOfBytesRead)))
	if ret == 0 {
		return false, err
	}
	return true, nil
}

func GetProcessFilePath(hProcess syscall.Handle) (string, error) {
	var filePath [syscall.MAX_PATH]uint16
	ret, _, err := procGetModuleFileNameEx.Call(uintptr(hProcess), 0, uintptr(unsafe.Pointer(&filePath[0])), uintptr(len(filePath)))
	if ret == 0 {
		return "", err
	}
	return syscall.UTF16ToString(filePath[:]), nil
}

func VirtualQueryEx(hProcess syscall.Handle, lpAddress uintptr, lpBuffer unsafe.Pointer, dwLength uintptr) (int, error) {
	ret, _, err := procVirtualQueryEx.Call(uintptr(hProcess), lpAddress, uintptr(lpBuffer), dwLength)
	if ret == 0 {
		return 0, err
	}
	return int(ret), nil
}

工具开发

整体思路

  1. 获取用户输入匹配的字符串
  2. 遍历系统中的正在运行的所有进程列表
  3. 对每个进程,打开进程句柄,获取其内存信息,并在内存中查找目标字符串。如果找到目标字符串,则将该进程的信息记录下来
  4. 输出结果

具体代码

获取用户输入字符串

reader := bufio.NewReader(os.Stdin)
fmt.Print("[*]请输入检索的字符串:")
target, _ := reader.ReadString('\n')

获取进程列表

processes, err := ps.Processes()
if err != nil {
    fmt.Printf("无法获取进程列表: %v\n", err)
    return
}

遍历每个进程

遍历进程列表,对于每个进程,首先获取其PID,然后使用syscall.OpenProcess打开进程,获取其句柄handle。如果无法打开进程,则跳过该进程,并更新进度条。

for _, process := range processes {
    pid := process.Pid()
    handle, err := syscall.OpenProcess(PROCESS_VM_READ|PROCESS_QUERY_INFORMATION, false, uint32(pid))
    if err != nil {
        continue
    }
    defer syscall.CloseHandle(handle)

获取进程的可执行文件路径processFilePath,如果获取失败,则跳过该进程。

processFilePath, err := GetProcessFilePath(handle)
if err != nil {
    continue
}

通过VirtualQueryEx获取进程的内存块信息memInfo,逐块遍历内存空间。这个循环的作用是遍历进程的所有内存区域。

for {
    bytesReturned, err := VirtualQueryEx(handle, addr, unsafe.Pointer(&memInfo), unsafe.Sizeof(memInfo))
    if err != nil || memInfo.RegionSize == 0 || bytesReturned == 0 {
        break
    }

匹配进程内存字符串

如果内存块已提交并且具有读写或只读权限,则使用ReadProcessMemory读取内存数据,并检查其中是否包含目标字符串target。如果找到目标字符串,则输出,并跳出内存遍历循环。没有则更新地址addr,继续遍历下一块内存。

if memInfo.State == MEM_COMMIT && memInfo.Protect&(PAGE_READWRITE|PAGE_READONLY) != 0 {
    buffer := make([]byte, memInfo.RegionSize)
    var bytesRead uintptr
    success, err := ReadProcessMemory(handle, addr, unsafe.Pointer(&buffer[0]), uintptr(len(buffer)), &bytesRead)
    if err == nil && success {
        if idx := strings.Index(string(buffer[:bytesRead]), target); idx != -1 {
            fmt.Printf("[+]在 PID %d 的地址 0x%x 处找到字符串: %s\n", pid, addr+uintptr(idx), target)
	    fmt.Printf("[+]进程名称: %s\n进程文件路径: %s\n", process.Executable(), processFilePath)
            break
        }
    }
}
addr += memInfo.RegionSize

测试运行结果

测试运行结果:everyting检索字符串,通过工具进行定位everything进程

应急处置工具之内存检索工具开发插图1

后续改进

改进一

增加提权函数,因为测试过程发现,程序权限过低,无法检索系统进程内存,例如用户为:SYSTEMLOCAL SERVICENETWORK SERVICE

解决此问题的方法是确保当前进程拥有SeDebugPrivilege权限。这可以通过调用OpenProcessToken, LookupPrivilegeValue和AdjustTokenPrivileges等函数来实现,修改代码提升权限

改进二

增加指定进程检索对应字符串操作,有助于确认在进程中的具体操作,帮助应急同事更好取证

改进好的程序已上传github,文章末尾复地址链接

场景测试

dnslog告警定位进程

通过字符串定位进程pid

应急处置工具之内存检索工具开发插图2

指定字pid检索确认具体行为

应急处置工具之内存检索工具开发插图3

webshell进程内存检索确认历史命令

执行过哪些命令

应急处置工具之内存检索工具开发插图4

是否运行过mimikatz

应急处置工具之内存检索工具开发插图5

项目地址

https://github.com/Fheidt12/Windows_Memory_Search

本人项目还有一个windows日志分析工具,也相当好用,后续会分享创作思路,希望大家点赞+转发


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

苹果试图保护你的隐私,OpenAI可能做不到
Shuffle:一款完全自动化的安全栈增强平台
如何使用CODASM编码Payload并降低熵值
SessionExec:一款针对会话安全的安全命令测试工具
Arkime:一款大规模数据包捕获和索引数据库系统
从蓝队流量角度分析Shiro-550反序列化漏洞

发布评论