ShellCode Loader 之 Windows API

听说万字可以加精华 ?

闲聊几句

看了很多C、C++、Golang、Rust的代码,粗浅地觉得一个Loader加载ShellCode的方式已经算是有一定的约定了。其中,往往都会涉及到Windows下的API调用,更通俗地讲:咱就是调用API的。

Windows 公开API的调用

一般来说,微软会公布一些众所周知的API,这些API直接去参考微软的文档去调用即可

例如很多经典的Loader写法中用到的:Virtualalloc() 函数

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);

转换为Go需要进行变量的转换,下面是一些变量类型的转换对应关系

d2b5ca33bd20240801172234

如果你偷懒一点,那一切皆可为 uintptr

如果你问我为什么,点击下面的链接

https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/syscall/dll_windows.go

d2b5ca33bd20240801172723

当然更上层一点有Go封装好的调用系统API的库

https://pkg.go.dev/golang.org/x/sys/windows

再来看下Go官方实现的Virtualalloc() 函数

func VirtualAlloc(address uintptr, size uintptr, alloctype uint32, protect uint32) (value uintptr, err error) {
	r0, _, e1 := syscall.Syscall6(procVirtualAlloc.Addr(), 4, uintptr(address), uintptr(size), uintptr(alloctype), uintptr(protect), 0, 0)
	value = uintptr(r0)
	if value == 0 {
		err = errnoErr(e1)
	}
	return
}

当然这不可能是手敲的代码,为了偷懒当然是由对应的工具去生成这样的代码啦

使用下面这条命令 + 准备一个 syscall_windows.go 和 一个 zsyscall_windows.go,就可以愉快生成标准写法的Windows 原生 Go API

//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go

如果你和我一样想偷懒,那就直接去借鉴一下Go官方的代码吧

https://cs.opensource.google/go/x/sys/+/refs/tags/v0.22.0:windows/syscall_windows.go

在 syscall_windows.go 文件中定义好要生成函数的形参,加上一点特别的注释,运行即可生成API函数

//sys	VirtualAlloc(address uintptr, size uintptr, alloctype uint32, protect uint32) (value uintptr, err error) = kernel32.VirtualAlloc

解释一下:

  1. //sys:为工具识别的特征,一定一定要写!!!
  2. VirtualAlloc(address uintptr, size uintptr, alloctype uint32, protect uint32) (value uintptr, err error) 是函数名 + 形参 + 返回值
  3. = kernel32.VirtualAlloc:=号必不可少,是为了确认调用哪个DLL中的函数

Windows 未公开 API

未公开的API和结构体可以去各种大佬的博客看,也可以看这个网站

https://undocumented-ntinternals.github.io/

无论是什么API,最后都需要经过SyscallN()函数

d2b5ca33bd20240801172723

那么仿照Go官方的写法,加上一点小心思

func VirtualAlloc(address uintptr, size uintptr, alloctype uint32, protect uint32) (value uintptr, err error) {
	r0, _, e1 := syscall.SyscallN(
                                      procVirtualAlloc.Addr(), 
                                      uintptr(address), 
                                      uintptr(size), 
                                      uintptr(alloctype), 
                                      uintptr(protect))
	value = uintptr(r0)
	if value == 0 {
		err = errnoErr(e1)
	}
	return
}

有啥区别吗?自行对比哈

来尝试一下写一个未公开的API,以Go来实现

d2b5ca33bd20240704111907

func NtDelayExecution(DelayInterval int64) (err error) {
  delay := -(DelayInterval * 1000 * 10000)

  r1, _, e1 := syscall.SyscallN(
    procNtDelayExecution.Addr(),
    uintptr(0),
    uintptr(unsafe.Pointer(&delay)),
  )
  if r1 != 0 {
    err = errnoErr(e1)
  }
  return
}

如果使用官方的工具生成

//sys	NtDelayExecution(DelayInterval int64) (err error) = ntdll.NtDelayExecution

搞定收工

总结

是不是很简单,没有万字吧

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 共1条

请登录后发表评论

    请登录后查看评论内容