获取windows10下的SSDT及函数表(二)

简介:接着上一篇继续完成最终通过代码实现SSDT表的获取,并呈现在页面之上。

前言

上一篇我们介绍了关于在驱动层通过代码的方式获取SSDT并保存起所有的函数内容,同时也用c编写的代码客户端实现了调用以及获取驱动层传递来的内容,那这篇就轮到我们用Go实现在应用层的代码,最终能够实现可视化展示的效果。

应用层代码

首先可以用Go实现昨天用C代码来测试的功能。我们首先要实现跟用c一样的效果,先能够调用并打印到命令行里。但是在开始之前我们先定义一个结构体用来存储最终需要的信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/*
    定义一个struct存储SSDT的函数的相关信息,序号,应用层地址,内核层地址,函数名称,函数参数个数
*/

type ssdtEntry struct {
    Index      uint32
    AppAddress uint64
    KernelAddr uint64
    Name       string
    ParamsNum  uint64
}

var ssdtTables []ssdtEntry

跟驱动通信

这时候就要祭出AI神器了,直接把C代码丢给他,让给转化成go代码:

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
package main

import (
    "fmt"
    "syscall"
    "unsafe"
)

const (
    FILE_DEVICE_UNKNOWN = 0x00000022
    FILE_ANY_ACCESS     = 0x00000000
    METHOD_IN_DIRECT    = 1
    METHOD_OUT_DIRECT   = 2
    GENERIC_ALL         = 0x10000000
    FILE_ATTRIBUTE_SYSTEM = 0x00000004
    OPEN_EXISTING       = 3
    INVALID_HANDLE_VALUE = ^uintptr(0)
)

func CTL_CODE(DeviceType, Function, Method, Access uint32) uint32 {
    return ((DeviceType << 16) | (Access << 14) | (Function << 2) | Method)
}

func main() {
    kernel32 := syscall.NewLazyDLL("kernel32.dll")
    createFileW := kernel32.NewProc("CreateFileW")
    getLastError := kernel32.NewProc("GetLastError")
    closeHandle := kernel32.NewProc("CloseHandle")
    deviceIoControl := kernel32.NewProc("DeviceIoControl")

    // 确保设备路径是宽字符字符串
    devicePath := `\\.\newDevice`
    devicePathPtr := syscall.StringToUTF16Ptr(devicePath)

    // 打开设备
    handle, _, callErr := createFileW.Call(
        uintptr(unsafe.Pointer(devicePathPtr)),
        GENERIC_ALL,
        0,
        0,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_SYSTEM,
        0,
    )

    // 检查句柄是否有效
    if handle == INVALID_HANDLE_VALUE {
        // 打印错误码
        lastError, _, _ := getLastError.Call()
        fmt.Printf("打开设备失败,错误码:%d\n", lastError)

        // 打印调用错误
        if callErr != nil {
            fmt.Println("调用 CreateFileW 时发生错误:", callErr.Error())
        }
        return
    }
    defer closeHandle.Call(handle)

    // 分配缓冲区
    buff := make([]byte, 0x100000)
    realRead := uint32(0)

    // 调用 DeviceIoControl
    success, _, callErr := deviceIoControl.Call(
        handle,
        uintptr(CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_OUT_DIRECT, FILE_ANY_ACCESS)),
        0,
        0,
        uintptr(unsafe.Pointer(&buff[0])),
        0x100000,
        uintptr(unsafe.Pointer(&realRead)),
        0,
    )

    // 检查 DeviceIoControl 是否成功
    if success == 0 {
        // 打印错误码
        lastError, _, _ := getLastError.Call()
        fmt.Printf("DeviceIoControl 调用失败,错误码:%d\n", lastError)

        // 打印调用错误
        if callErr != nil {
            fmt.Println("调用 DeviceIoControl 时发生错误:", callErr.Error())
        }
        return
    }

    // 解析返回数据
    begin := (*[1 << 30]uint64)(unsafe.Pointer(&buff[0]))
    for i := uint32(0); i < realRead/(2*8); i += 2 {
        if begin[i] == 0 {
            fmt.Println("地址为 0,暂停...")
            fmt.Println("按任意键继续...")
            var input string
            fmt.Scanln(&input)
        } else {
            fmt.Printf("address:%x paramsnum:%x number:%d\n", begin[i], begin[i+1], i/2)
        }
    }
}

注意:以上代码生成过程中我出现了一个有问题的地方,主要是就是设备路径这个地方,生成总是错的,我就改成下面的样子发现就好了,具体不研究了,大概率就是宽字节转换的问题,感兴趣的朋友可以深究下,不过这样能成功我就不多琢磨了。

1
2
devicePath := `\\.\newDevice`
devicePathPtr := syscall.StringToUTF16Ptr(devicePath)

获得结果之后跑下效果,如图所示,注意要用管理员方式去打印哈。

2

获取函数名和应用层地址

如上图显示,我们已经成功获取的内核地址,那么下面就需要获取函数的名称和应用层地址,我们在之前写系统调用的时候曾经明确过,最后进入内核之前的函数都在ntdll.dll中,这个些函数实际上都没什么实际功能就是跳板,所以为了方便其他模块能够调用,这些函数其实都是导出函数。

说到导出函数我们就有办法了,这些导出函数一般都是按照名称导出的,那么直接查看ntdll.dll的导出表不就能找到对应的名称么。然后就是地址,在获取应用层地址的时候,我的方法有些笨拙,首先调用函数这个部分代码其实都是差不多的,我们只需要找到第一个函数的的位置,然后依次遍历每个区块其实都是0x20大小就可以,一直到边界。

3

所以分两部分实践,首先需要解析ntdll.dll并获取对应pe文件的导出表,遍历名称和地址存储到一个map中,代码如下;

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
func get_exports() bool {
    hProcess, _ := syscall.GetCurrentProcess()

    var ntdll = syscall.NewLazyDLL("ntdll.dll")

    var NtQueryInformationProcessProc = ntdll.NewProc("NtQueryInformationProcess")

    type UNICODE_STRING struct {
        Length        uint16
        MaximumLength uint16
        Buffer        *uint16
    }

    // LIST_ENTRY结构体定义,根据Windows的定义
    type LIST_ENTRY struct {
        Flink *LIST_ENTRY
        Blink *LIST_ENTRY
    }

    // _LDR_DATA_TABLE_ENTRY结构体定义,根据Windows的定义
    type _LDR_DATA_TABLE_ENTRY struct {
        InLoadOrderLinks           LIST_ENTRY // LIST_ENTRY结构体指针
        InMemoryOrderLinks         LIST_ENTRY // LIST_ENTRY结构体指针
        InInitializationOrderLinks LIST_ENTRY // LIST_ENTRY结构体指针
        DllBase                    uintptr
        EntryPoint                 uintptr
        SizeOfImage                uint32
        FullDllName                UNICODE_STRING // UNICODE_STRING结构体,此处简化处理为固定长度数组
        BaseDllName                UNICODE_STRING // 同上,简化处理
    }

    // _PEB_LDR_DATA结构体定义,根据Windows的定义
    type _PEB_LDR_DATA struct {
        Length                          uint32
        Initialized                     uint8
        SsHandle                        uintptr
        InLoadOrderModuleList           LIST_ENTRY // LIST_ENTRY结构体指针
        InMemoryOrderModuleList         LIST_ENTRY // LIST_ENTRY结构体指针
        InInitializationOrderModuleList LIST_ENTRY // LIST_ENTRY结构体指针
        EntryInProgress                 uintptr
        ShutdownInProgress              uint8
        ShutdownThreadId                uintptr
    }

    // PEB结构体定义,根据Windows的定义
    type PEB struct {
        BeingDebugged    uint32
        Mutant           uintptr
        ImageBaseAddress uintptr
        Ldr              *_PEB_LDR_DATA // PEB_LDR_DATA结构体的指针
    }

    // 把_PROCESS_BASIC_INFORMATION 转换为go的struct
    type PROCESS_BASIC_INFORMATION struct {
        ExitStatus                   uint32
        PebBaseAddress               *uint64
        AffinityMask                 uint64
        BasePriority                 int32
        UniqueProcessId              uint64
        InheritedFromUniqueProcessId uint64
    }

    var ProcessBasicInformation PROCESS_BASIC_INFORMATION
    var ReturnLength uint32

    ret, _, _ := NtQueryInformationProcessProc.Call(
        uintptr(hProcess),
        0,
        uintptr(unsafe.Pointer(&ProcessBasicInformation)),
        uintptr(unsafe.Sizeof(ProcessBasicInformation)),
        uintptr(unsafe.Pointer(&ReturnLength)))

    fmt.Printf("Return:%d\n", ret)

    // 读取PEB结构体
    peb := (*PEB)(unsafe.Pointer(ProcessBasicInformation.PebBaseAddress))
    fmt.Printf("ImageBaseAddress:0x%x\n", peb.ImageBaseAddress)
    fmt.Printf("Ldr:0x%x\n", (unsafe.Pointer(peb.Ldr)))

    // 读取_PEB_LDR_DATA结构体,增加类型安全检查
    ldrPtr := peb.Ldr
    fmt.Printf("Length:%d\n", ldrPtr.Length)
    fmt.Printf("Initialized:%d\n", ldrPtr.Initialized)
    beginLdr := &(ldrPtr.InLoadOrderModuleList)

    // 名称:地址的map,地址map
    exportsName := make(map[string]uint32)
    exportsAddress := make(map[uint32]string)

    // 读取InLoadOrderModuleList结构体
    listEntry := (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(ldrPtr.InLoadOrderModuleList.Flink))
    for listEntry != nil && &(listEntry.InLoadOrderLinks) != beginLdr {

        fmt.Printf("DllBase:0x%x\n", listEntry.DllBase)
        fmt.Printf("EntryPoint:0x%x\n", listEntry.EntryPoint)
        fmt.Printf("SizeOfImage:%d\n", listEntry.SizeOfImage)

        // 使用 unsafe.Slice 创建一个适当大小的 uint16 切片
        FullDllName := unsafe.Slice((*uint16)(unsafe.Pointer(listEntry.FullDllName.Buffer)), listEntry.FullDllName.Length/2)
        fmt.Printf("FullDllName:%s\n", syscall.UTF16ToString(FullDllName))

        baseDllName := unsafe.Slice((*uint16)(unsafe.Pointer(listEntry.BaseDllName.Buffer)), listEntry.BaseDllName.Length/2)
        fmt.Printf("BaseDllName:%s\n", syscall.UTF16ToString((baseDllName)))

        // 非ntdll.dll的模块不解析
        if syscall.UTF16ToString(baseDllName) != "ntdll.dll" {
            listEntry = (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(listEntry.InLoadOrderLinks.Flink))
            continue
        }

        //解析dll

        // 将uintptr转换为[]byte
        dllBytes := unsafe.Slice((*uint8)(unsafe.Pointer(listEntry.DllBase)), listEntry.SizeOfImage)

        // 使用通过读取内存的方法获取PE文件信息
        ntdll, err := pasepe.NewFileFromMemory(bytes.NewReader(dllBytes))
        // 遍历exports
        if err != nil {
            log.Fatalf("Failed to parse PE file: %v", err)
            return false
        }

        // 解析导出表
        if exp, err := ntdll.Exports(); err == nil {
            fmt.Println("导出函数列表:")
            for _, sym := range exp {
                
                // 存储名称和地址,并分别存放两个map,分别是名称为键,以及地址为键
                // 这样做后面更方便
                exportsName[sym.Name] = sym.VirtualAddress
                exportsAddress[sym.VirtualAddress] = sym.Name

            }
        } else {
            log.Fatalf("Failed to get exports: %v", err)
        }

        listEntry = (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(listEntry.InLoadOrderLinks.Flink))

    }
    return true
}

获取所有导出表的信息之后,需要将导出信息中符合SSDT函数的函数名以及应用层地址保存下来,代码如下,主要就是从NtAccessCheck的地址开始遍历,直到边界之后保存名称和地址:

 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
// 目标:从NtAccessCheck地址开始,每0x20个字节一组,打印每组的第5个字节
// 其中,每组的前4个字节分别是,0x4c,0x8b,0xd1,0xb8,打印到前四个不是这个四个字节为止
for i := exportsName["NtAccessCheck"]; i < uint32(len(dllBytes)); i += 0x20 {
    ordinal := binary.LittleEndian.Uint32(dllBytes[i+4 : i+8])
    if dllBytes[i] != 0x4c || dllBytes[i+1] != 0x8b || dllBytes[i+2] != 0xd1 || dllBytes[i+3] != 0xb8 {
        if i > uint32(exportsName["NtAccessCheck"]+0x20000) 
                                || ordinal == 0xcccccccc {
            break
        }
        i = i + 0x10
    }
    if ordinal <= 0x500 {
        fmt.Printf("函数名称: %s 函数序号:%x 地址:%x\n", exportsAddress[i], ordinal, (uint64)(listEntry.DllBase))
        entry := ssdtEntry{}
        entry.AppAddress = (uint64)(listEntry.DllBase) + uint64(i)
        entry.Index = ordinal
        entry.Name = exportsAddress[i]
        if len(ssdtTables) <= int(ordinal) {
            // 扩展切片长度
            ssdtTables = append(ssdtTables, make([]ssdtEntry, int(ordinal)
                            -len(ssdtTables)+1)...)
        }
        // 存储结构体
        ssdtTables[ordinal] = entry
    }
}

完成这部分最后把驱动层和应用层的数据组合在一起放在一个结构体里就可以了,整体代码如下:

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "log"
    "syscall"
    "unsafe"

    pasepe "github.com/Binject/debug/pe"
)

const (
    FILE_DEVICE_UNKNOWN   = 0x00000022
    FILE_ANY_ACCESS       = 0x00000000
    METHOD_IN_DIRECT      = 1
    METHOD_OUT_DIRECT     = 2
    GENERIC_ALL           = 0x10000000
    FILE_ATTRIBUTE_SYSTEM = 0x00000004
    OPEN_EXISTING         = 3
    INVALID_HANDLE_VALUE  = ^uintptr(0)
)

/*
  定义一个struct存储SSDT的函数的相关信息,序号,应用层地址,内核层地址,函数名称,函数参数个数
*/

type ssdtEntry struct {
    Index      uint32
    AppAddress uint64
    KernelAddr uint64
    Name       string
    ParamsNum  uint64
}

var ssdtTables []ssdtEntry

func CTL_CODE(DeviceType, Function, Method, Access uint32) uint32 {
    return ((DeviceType << 16) | (Access << 14) | (Function << 2) | Method)
}

func call_for_ssdt() []byte {

    kernel32 := syscall.NewLazyDLL("kernel32.dll")
    createFileW := kernel32.NewProc("CreateFileW")
    getLastError := kernel32.NewProc("GetLastError")
    closeHandle := kernel32.NewProc("CloseHandle")
    deviceIoControl := kernel32.NewProc("DeviceIoControl")

    // 确保设备路径是宽字符字符串
    devicePath := `\\.\newDevice`
    devicePathPtr := syscall.StringToUTF16Ptr(devicePath)

    // 打开设备
    handle, _, callErr := createFileW.Call(
        uintptr(unsafe.Pointer(devicePathPtr)),
        GENERIC_ALL,
        0,
        0,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_SYSTEM,
        0,
    )

    // 检查句柄是否有效
    if handle == INVALID_HANDLE_VALUE {
        // 打印错误码
        lastError, _, _ := getLastError.Call()
        fmt.Printf("打开设备失败,错误码:%d\n", lastError)

        // 打印调用错误
        if callErr != nil {
            fmt.Println("调用 CreateFileW 时发生错误:", callErr.Error())
        }
        return nil
    }
    defer closeHandle.Call(handle)

    // 分配缓冲区
    buff := make([]byte, 0x100000)
    realRead := uint32(0)

    // 调用 DeviceIoControl
    success, _, callErr := deviceIoControl.Call(
        handle,
        uintptr(CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_OUT_DIRECT, FILE_ANY_ACCESS)),
        0,
        0,
        uintptr(unsafe.Pointer(&buff[0])),
        0x100000,
        uintptr(unsafe.Pointer(&realRead)),
        0,
    )

    // 检查 DeviceIoControl 是否成功
    if success == 0 {
        // 打印错误码
        lastError, _, _ := getLastError.Call()
        fmt.Printf("DeviceIoControl 调用失败,错误码:%d\n", lastError)

        // 打印调用错误
        if callErr != nil {
            fmt.Println("调用 DeviceIoControl 时发生错误:", callErr.Error())
        }
        return nil
    }
    return buff
}

func get_exports() bool {
    hProcess, _ := syscall.GetCurrentProcess()

    var ntdll = syscall.NewLazyDLL("ntdll.dll")

    var NtQueryInformationProcessProc = ntdll.NewProc("NtQueryInformationProcess")

    type UNICODE_STRING struct {
        Length        uint16
        MaximumLength uint16
        Buffer        *uint16
    }

    // LIST_ENTRY结构体定义,根据Windows的定义
    type LIST_ENTRY struct {
        Flink *LIST_ENTRY
        Blink *LIST_ENTRY
    }

    // _LDR_DATA_TABLE_ENTRY结构体定义,根据Windows的定义
    type _LDR_DATA_TABLE_ENTRY struct {
        InLoadOrderLinks           LIST_ENTRY // LIST_ENTRY结构体指针
        InMemoryOrderLinks         LIST_ENTRY // LIST_ENTRY结构体指针
        InInitializationOrderLinks LIST_ENTRY // LIST_ENTRY结构体指针
        DllBase                    uintptr
        EntryPoint                 uintptr
        SizeOfImage                uint32
        FullDllName                UNICODE_STRING // UNICODE_STRING结构体,此处简化处理为固定长度数组
        BaseDllName                UNICODE_STRING // 同上,简化处理
    }

    // _PEB_LDR_DATA结构体定义,根据Windows的定义
    type _PEB_LDR_DATA struct {
        Length                          uint32
        Initialized                     uint8
        SsHandle                        uintptr
        InLoadOrderModuleList           LIST_ENTRY // LIST_ENTRY结构体指针
        InMemoryOrderModuleList         LIST_ENTRY // LIST_ENTRY结构体指针
        InInitializationOrderModuleList LIST_ENTRY // LIST_ENTRY结构体指针
        EntryInProgress                 uintptr
        ShutdownInProgress              uint8
        ShutdownThreadId                uintptr
    }

    // PEB结构体定义,根据Windows的定义
    type PEB struct {
        BeingDebugged    uint32
        Mutant           uintptr
        ImageBaseAddress uintptr
        Ldr              *_PEB_LDR_DATA // PEB_LDR_DATA结构体的指针
    }

    // 把_PROCESS_BASIC_INFORMATION 转换为go的struct
    type PROCESS_BASIC_INFORMATION struct {
        ExitStatus                   uint32
        PebBaseAddress               *uint64
        AffinityMask                 uint64
        BasePriority                 int32
        UniqueProcessId              uint64
        InheritedFromUniqueProcessId uint64
    }

    var ProcessBasicInformation PROCESS_BASIC_INFORMATION
    var ReturnLength uint32

    ret, _, _ := NtQueryInformationProcessProc.Call(
        uintptr(hProcess),
        0,
        uintptr(unsafe.Pointer(&ProcessBasicInformation)),
        uintptr(unsafe.Sizeof(ProcessBasicInformation)),
        uintptr(unsafe.Pointer(&ReturnLength)))

    fmt.Printf("Return:%d\n", ret)

    // 读取PEB结构体
    peb := (*PEB)(unsafe.Pointer(ProcessBasicInformation.PebBaseAddress))
    fmt.Printf("ImageBaseAddress:0x%x\n", peb.ImageBaseAddress)
    fmt.Printf("Ldr:0x%x\n", (unsafe.Pointer(peb.Ldr)))

    // 读取_PEB_LDR_DATA结构体,增加类型安全检查
    ldrPtr := peb.Ldr
    fmt.Printf("Length:%d\n", ldrPtr.Length)
    fmt.Printf("Initialized:%d\n", ldrPtr.Initialized)
    beginLdr := &(ldrPtr.InLoadOrderModuleList)

    // 名称:地址的map,地址map
    exportsName := make(map[string]uint32)
    exportsAddress := make(map[uint32]string)

    // 读取InLoadOrderModuleList结构体
    listEntry := (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(ldrPtr.InLoadOrderModuleList.Flink))
    for listEntry != nil && &(listEntry.InLoadOrderLinks) != beginLdr {

        fmt.Printf("DllBase:0x%x\n", listEntry.DllBase)
        fmt.Printf("EntryPoint:0x%x\n", listEntry.EntryPoint)
        fmt.Printf("SizeOfImage:%d\n", listEntry.SizeOfImage)

        // 使用 unsafe.Slice 创建一个适当大小的 uint16 切片
        FullDllName := unsafe.Slice((*uint16)(unsafe.Pointer(listEntry.FullDllName.Buffer)), listEntry.FullDllName.Length/2)
        fmt.Printf("FullDllName:%s\n", syscall.UTF16ToString(FullDllName))

        baseDllName := unsafe.Slice((*uint16)(unsafe.Pointer(listEntry.BaseDllName.Buffer)), listEntry.BaseDllName.Length/2)
        fmt.Printf("BaseDllName:%s\n", syscall.UTF16ToString((baseDllName)))

        // 非ntdll.dll的模块不解析
        if syscall.UTF16ToString(baseDllName) != "ntdll.dll" {
            listEntry = (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(listEntry.InLoadOrderLinks.Flink))
            continue
        }

        //解析dll

        // 将uintptr转换为[]byte
        dllBytes := unsafe.Slice((*uint8)(unsafe.Pointer(listEntry.DllBase)), listEntry.SizeOfImage)

        // 使用通过读取内存的方法获取PE文件信息
        ntdll, err := pasepe.NewFileFromMemory(bytes.NewReader(dllBytes))
        // 遍历exports
        if err != nil {
            log.Fatalf("Failed to parse PE file: %v", err)
            return false
        }

        // 解析导出表
        if exp, err := ntdll.Exports(); err == nil {
            fmt.Println("导出函数列表:")
            for _, sym := range exp {
                // fmt.Printf("Ordinal %d, Name: %s, Address: %d, Forward: %s\n",
                //   sym.Ordinal, sym.Name, sym.VirtualAddress, sym.Forward)

                exportsName[sym.Name] = sym.VirtualAddress
                exportsAddress[sym.VirtualAddress] = sym.Name

            }
        } else {
            log.Fatalf("Failed to get exports: %v", err)
        }

        // 目标:打印一个字节,从NtAccessCheck地址开始,每0x20个字节一组,打印每组的第5个字节
        // 其中,每组的前4个字节分别是,0x4c,0x8b,0xd1,0xb8,打印到前四个不是这个四个字节为止
        for i := exportsName["NtAccessCheck"]; i < uint32(len(dllBytes)); i += 0x20 {
            ordinal := binary.LittleEndian.Uint32(dllBytes[i+4 : i+8])
            if dllBytes[i] != 0x4c || dllBytes[i+1] != 0x8b || dllBytes[i+2] != 0xd1 || dllBytes[i+3] != 0xb8 {
                if i > uint32(exportsName["NtAccessCheck"]+0x20000) || ordinal == 0xcccccccc {
                    break
                }
                i = i + 0x10
            }
            if ordinal <= 0x500 {
                fmt.Printf("函数名称: %s 函数序号:%x 地址:%x\n", exportsAddress[i], ordinal, (uint64)(listEntry.DllBase))
                entry := ssdtEntry{}
                entry.AppAddress = (uint64)(listEntry.DllBase) + uint64(i)
                entry.Index = ordinal
                entry.Name = exportsAddress[i]
                if len(ssdtTables) <= int(ordinal) {
                    // 扩展切片长度
                    ssdtTables = append(ssdtTables, make([]ssdtEntry, int(ordinal)-len(ssdtTables)+1)...)
                }
                ssdtTables[ordinal] = entry
            }
        }

        listEntry = (*_LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(listEntry.InLoadOrderLinks.Flink))

    }
    return true
}

func GetSSDT() []ssdtEntry {

    // 通过ntdll获取ssdt表的各函数名称
    if !get_exports() {
        return nil
    }

    buff := call_for_ssdt()
    if buff == nil {
        fmt.Println("获取SSDT失败")
        return nil
    }

    // 解析返回数据

    begin := (*[1 << 30]uint64)(unsafe.Pointer(&buff[0]))
    for i := uint32(0); i < uint32(len(buff))/(2*8); i += 2 {
        if begin[i] == 0 {
            // fmt.Println("地址为 0,暂停...")
            // fmt.Println("按任意键继续...")
            // var input string
            // fmt.Scanln(&input)
            break
        } else {
            // fmt.Printf("address:%x paramsnum:%x number:%d\n", begin[i], begin[i+1], i/2)
            ssdtTables[i/2].KernelAddr = begin[i]
            ssdtTables[i/2].ParamsNum = begin[i+1]
        }
    }
    return ssdtTables

}

func main() {
    ssdtTable := GetSSDT()
    for _, v := range ssdtTable {

        fmt.Printf("序号:%d 函数名称: %s 内核地址:%x  应用层地址:%x 参数个数:%d\n", v.Index, v.Name, v.KernelAddr, v.AppAddress, v.ParamsNum)
    }

}

执行起来效果如下:

4

最后我们需要把这个函数融入到之前使用的wails的小工具里,如下图所示,就这样已经能够在页面上呈现了(数据没有进行格式化,就直接打印的,姑且看看哈)

5

Tips

上一篇有一个问题忘记跟朋友们介绍,这里简单说下,有的朋友在打开IDA反编译分析文件的时候,对话框会弹出一个获取对应符号文件(.pdb),一般PE文件在编译的时候都会带有一个符号文件,也就是对应这个文件里一些函数,变量等等的描述。

6

如果你在没有符号文件的方式打开,函数和变量一般都是IDA自命名,就像以下这种情况。

7

而一般如果是windows的文件,其符号文件都是可以下载缓存或者从符号文件服务器自动下载的,微软的符号文件服务器地址是https://msdl.microsoft.com/download/symbols,但是因为某些原因我们无法访问,需要某些方法。

另外,如果你想把符号文件都缓存到本地来,那推荐用这个工具:https://techcommunity.microsoft.com/blog/iis-support-blog/pdb-downloader/342969,但是因为微软的符号服务器编程https之后,这个工具无法使用,因为访问的是http,但是这个工具的代码还在,可以看这个仓库https://github.com/rajkumar-rangaraj/PDB-Downloader,我们可以自己拉下来修改下来使用。

首先下载代码然后vs2022打开,(可能需要配置对应类型项目的工具,安装即可)然后找到配置文件,如下图所示,把http改成https

8

9

以上两个地方改完应该就OK了,然后重新编译生成即可,(但是注意,能够使用的前提还是解决网络问题哈)

10

这篇文章就介绍到这里了,主要算是SSDT表的一个综合应用,能够比较直接了解这部分的内容,后续想办法把几个页面展示的小应用汇总到一起,也更方便。

updatedupdated2025-01-232025-01-23