创作背景
在处置应急事件过程,经常会遇到以下几个场景:
- 日志没有任何异常、日志文件被清除、日志文件被加密,无法通过主机日志进行取证;
- 主机上面没有落地的恶意文件,无法通过文件落地时间进行关联分析;
- 安全设备产生告警,例如外联dnslog、外联恶意域名、内网横向爆破,但是无法定位具体进程;
- 主机没有部署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
}
工具开发
整体思路
- 获取用户输入匹配的字符串
- 遍历系统中的正在运行的所有进程列表
- 对每个进程,打开进程句柄,获取其内存信息,并在内存中查找目标字符串。如果找到目标字符串,则将该进程的信息记录下来
- 输出结果
具体代码
获取用户输入字符串
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进程
后续改进
改进一
增加提权函数,因为测试过程发现,程序权限过低,无法检索系统进程内存,例如用户为:SYSTEM
、LOCAL SERVICE
、NETWORK SERVICE
解决此问题的方法是确保当前进程拥有SeDebugPrivilege权限。这可以通过调用OpenProcessToken, LookupPrivilegeValue和AdjustTokenPrivileges等函数来实现,修改代码提升权限
改进二
增加指定进程检索对应字符串操作,有助于确认在进程中的具体操作,帮助应急同事更好取证
改进好的程序已上传github,文章末尾复地址链接
场景测试
dnslog告警定位进程
通过字符串定位进程pid
指定字pid检索确认具体行为
webshell进程内存检索确认历史命令
执行过哪些命令
是否运行过mimikatz
项目地址
https://github.com/Fheidt12/Windows_Memory_Search
本人项目还有一个windows日志分析工具,也相当好用,后续会分享创作思路,希望大家点赞+转发
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)