在笔者上一篇文章中简单的介绍了如何运用汇编语言编写一段弹窗代码,虽然简易ShellCode
可以被正常执行,但却存在很多问题,由于采用了硬编址的方式来调用相应API函数的,那么就会存在一个很大的缺陷,如果操作系统的版本不统或系统重启过,那么基址将会发生变化,此时如果再次调用基址参数则会调用失败,本章将解决这个棘手的问题,通过ShellCode
动态定位的方式解决这个缺陷,并以此设计出真正符合规范的ShellCode
代码片段。
自定位代码是一种常见的Shellcode
技术,它使Shellcode
能够在任何系统上运行,而无需考虑系统内存布局和代码地址等问题。以下是Shellcode自定位代码的流程:
1.查找Kernel32.dll基址并在其中寻找LoadLibraryA
2.计算函数名hash摘要并通过hash摘要判断函数
3.解析Kernel32.dll导出表
4.最终动态调用系列函数
1.5.1 动态查找Kernel32基址 首先我们需要通过汇编的方式来实现动态定位Kernel32.dll
中的基址,你或许会有个疑问? 为什么要查找Kernel32.dll
的地址而不是User32.dll
,这是因为我们最终的目的是调用MessageBoxA
这个函数,而该函数位于 User32.dll
这个动态链接库里,在某些程序中User32模块并不一定会被加载,而Kernel32则必然会被加载,为了能够调用MessageBoxA
函数,我们就需要调用LoadLibraryA
函数来加载User32.dll
这个模块,而LoadLibraryA
恰巧又位于kernel32.dll
中,因此我们只需要找到LoadLibraryA
函数,即可实现加载任意的动态链接库,并调用任意的函数的目的。
由于我们需要动态获取LoadLibraryA()
以及ExitProcess()
这两个函数的地址,而这两个函数又是存在于kernel32.dll
中的,因此这里需要先找到kernel32.dll
的基址,然后通过对其进行解析,从而查找两个函数的动态地址。动态的查找Kernel32.dll
的地址可总结为如下:
1.首先通过段选择子FS
在内存中找到当前进程内的线程环境块结构体指针TEB。
2.线程环境块偏移位置为fs:[0x30]
的位置处存放着指向进程环境块PEB结构的指针。
3.进程环境块PEB偏移为0x0c
的地址处存放着指向PEB_LDR_DATA
的结构体指针。
4.而在PEB_LDR_DATA
偏移0x1c
的地址处存放着指向Ldr.InMemoryOrderModuleList
模块初始化链表的头指针。
5.在初始化链表中存放的就是所有进程的模块信息,通过将偏移值加0x08
读者即可获取到kernel32.dll
的基地址。
既然有了固定的查询定位公式,接下我们就使用WinDBG
调试器来手工完成对Kernel32.dll
地址的定位:
小提示:Windbg是Windows Debugger的缩写,是一种微软提供的免费调试器工具,用于分析和调试Windows操作系统和应用程序。Windbg可以在不重启系统的情况下,通过连接到正在运行的进程或者操作系统内核,获取并分析程序的运行信息、内存状态、寄存器状态、线程状态、调用堆栈等数据,并可以使用符号文件来解析程序中的符号名,从而帮助开发者定位问题和进行深入调试。
读者可通过附件获取到WinDBG
程序,当用户打开WinDBG
时读者可通过Ctrl+E
快捷键任意打开一个可执行程序,接着我们开始寻找吧;
1.通过段选择子FS
在内存中找到当前的线程环境块TEB
。这里可以利用本地调试,并输入!teb
指令,读者可看到如下输出:
小提示:TEB(Thread Environment Block)是Windows操作系统中的一个重要数据结构,每个进程都有一个对应的TEB。它主要用于存储线程的环境信息和状态,包括线程局部存储(TLS)指针、异常处理链、堆栈信息、Fiber信息等。TEB由Windows内核自动创建和管理,可以通过系统调用和调试器工具来访问和修改其内容。
如上线程环境块偏移位置为0x30
的地方存放着指向进程环境块PEB
的指针。结合上图可见,当前PEB
的地址为002bb000
小提示:PEB是Windows操作系统的进程环境块(Process Environment Block)的缩写。PEB是一个数据结构,其中包含了关于进程的许多信息,例如进程的模块、堆、线程等等。PEB由操作系统内核在创建进程时分配和初始化,并且只有在进程运行期间才可用。
2.在进程环境块中偏移位置为0x0c
的地方存放着指向PEB_LDR_DATA
结构体的指针,其中存放着已经被进程装载的动态链接库的信息,如下图所示;
3.接着PEB_LDR_DATA
结构体偏移位置为0x1c
的地方存放着指向模块初始化链表的头指针InInitializationOrderModuleList
,如下图所示;
4.模块初始化链表InInitializationOrderModuleList
中按顺序存放着PE装入运行时初始化模块的信息,第一个链表节点是ntdll.dll
,第二个链表结点就是kernel32.dll
。我们可以先看看InInitializationOrderModuleList
中的内容:
上图中的0x005a3ad8
保存的是第一个链节点的指针,解析一下这个结点,可发现如下地址:
上图中的0x77200000
为ntdll.dll
的模块基地址,而0x005a4390
则是指向下一个模块的指针,我们继续跟随0x005a4390
地址,则此处看到的标黄处是下一个模块kernel32.dll
的基地址。
最后我们通过输入!peb
命令,输出当前所有载入模块并验证一下:
既然有了如上所述的方法,那么读者可以很容易的实现这段功能,为了便于读者理解,笔者先提供一段使用C语言书写的实现方式,如下代码所示;
#include <windows.h> #include <stdio.h> int main (int argc, char * argv[]) { DWORD *PEB = NULL ; DWORD *Ldr = NULL ; DWORD *Init = NULL ; DWORD *Kernel32 = NULL ; __asm { mov eax, fs:[0x30 ] mov PEB,eax } printf ("得到PEB指针 = %x \n" , PEB); Ldr = *(DWORD **)((unsigned char *)PEB + 0x0c ); printf ("得到LDR结构指针 = %x \n" , Ldr); Init = *(DWORD **)((unsigned char *)Ldr + 0x1c ); printf ("得到InInitializationOrderModuleList结构指针 = %x \n" , Init); Kernel32 = *(DWORD **)((unsigned char *)Init + 0x08 ); printf ("得到Kernel32的基地址 = %x \n" , Kernel32); system("pause" ); return 0 ; }
运行输出效果如下图所示,读者可自行检查读取结果的准确性;
将此段代码翻译为汇编模式也很容易,如下是通过汇编实现的流程;
.386 p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc includelib kerbcli.lib assume fs:nothing .code main PROC xor eax,eax xor edx,edx mov eax,fs:[30 h] ; 得到PEB结构地址 mov eax,[eax + 0 ch] ; 得到PEB_LDR_DATA结构地址 mov esi,[eax + 1 ch] ; 得到 InInitializationOrderModuleList lodsd ; 得到KERNEL32.DLL所在LDR_MODULE结构的 mov eax,[eax] ; Windows 7 以上要将这里打开 mov edx,[eax + 8 h] ; 得到BaseAddress,既Kernel32.dll基址 ret main ENDP END main
1.5.2 动态查找并枚举进程模块 在读者阅读过第一节中的内容时,相信您已经可以熟练的掌握WinDBG
调试器的基本使用了,本节我们将扩展一个知识点,以让读者能更好的理解WinDBG
调试命令,本次我们实现枚举进程模块的功能,本案例将不在解释基本功能。
通过PEB/TEB
找到自身进程的所有载入模块数据,首先获取TEB
线程环境块。在编程的时候,TEB始终保存在寄存器FS
中。
得到LDR结构:Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );
然后再找到PEB
结构偏移为0x30
从该命令的输出可以看出,PEB结构体的地址位于TEB
结构体偏移0x30
的位置处。
找到了PEB
也就可以找到_PEB_LDR_DATA
结构 其位于PEB
偏移0c
的位置上。
Ldr = *( ( DWORD ** )( ( unsigned char * )PEB + 0x0c ) );
从输出结果可以看出,LDR
在PEB
结构体偏移的0x0C
处,该地址保存的地址是0x77325d80
通过该地址来解析LDR
结构体。
Flink = *( ( DWORD ** )( ( unsigned char * )Ldr + 0x14 ) );
位于LDR偏移14的位置就是InLoadOrderModuleList其所指向的就是模块名称表。
现在来手动遍历[ 0x5a3bd0 - 0x5aa5b8 ]
第一条链表,输入命令dd 0x5a3bd0
链表偏移0x18
的位置是模块的映射地址 ImageBase,链表偏移 0x28
的位置是模块的路径及名称的地址,链表偏移 0x30
的位置是模块名称的地址。
如上图中的输出结果,地址005a2480
保存有当前模块详细路径信息,而005a24ae
则保存有当前模块名,我们可以通过du
命令来进行验证;
当读者需要读入下一个模块链表时,则需要访问0x005a3ac8
这个内存地址,其中保存着下一个链表结构,依次遍历。
当然这个链表结构其实访问InMemoryOrderModuleList
同样可以得到,这两个都指向同一片区域。
上方介绍的结构,是微软保留结构,只能从网上找到一个结构定义,根据该结构的定义做进一步解析即可。
typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2 ]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2 ]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8 ]; PVOID Reserved5[3 ]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
根据如上分析细节,那么描述枚举模块列表的核心代码就可以写成如下案例;
#include <Windows.h> #include <stdio.h> int main (int argc, char * argv[]) { DWORD *PEB = NULL , *Ldr = NULL , *Flink = NULL , *p = NULL ; DWORD *BaseAddress = NULL , *FullDllName = NULL ,*Ba = NULL ; __asm { mov eax, fs:[0x30 ] mov PEB, eax } Ldr = *((DWORD **)((unsigned char *)PEB + 0x0c )); Flink = *((DWORD **)((unsigned char *)Ldr + 0x14 )); p = Flink; p = *((DWORD **)p); while (Flink != p) { BaseAddress = *((DWORD **)((unsigned char *)p + 0x10 )); FullDllName = *((DWORD **)((unsigned char *)p + 0x20 )); if (BaseAddress == 0 ) break ; printf ("镜像基址 = %08x \n --> 模块路径 = %S \n" , BaseAddress, (unsigned char *)FullDllName); p = *((DWORD **)p); } system("pause" ); return 0 ; }
读者编译并运行该程序,则默认会枚举出当前模块所导入的所有模块信息,其输出效果如下图所示;
1.5.3 计算函数Hash摘要值 案例介绍了如何使用Win32汇编语言和C语言计算字符串的hash
摘要值。字符串的hash
摘要值是通过一定的算法将字符串压缩为一个固定长度的十六进制数,用于在程序中进行快速的字符串比较。具体而言,该案例使用了循环移位hash计算法,并最终得到了字符串的 hash 值,并以十六进制数的形式输出。
读者一定有疑问为啥需要HASH压缩处理? 原因是,如果直接将函数名压栈的话,我们就需要提供更多的空间来存储ShellCode
代码,为了能够让我们编写的ShellCode
代码更加的短小精悍,所以我们将要对字符串进行hash
处理,将字符串压缩为一个十六进制数,这样只需要比较二者hash
值就能够判断目标函数,尽管这样会引入额外的hash
算法,但是却可以节省出存储函数名字的空间。
为了能让读者理解计算原理,此处我们先使用C语言做摘要计算描述,如下代码中的GetHash
函数,该函数接受一个指向字符数组的指针,即一个字符串,然后对字符串进行哈希计算,并返回计算结果。
哈希计算的过程是通过循环遍历字符串中的每个字符,对其进行位运算和加法运算,最终得到一个32
位的哈希值。对于字符串中的每个字符,程序首先将哈希值左移25
位,然后将结果右移7
位,相当于是对哈希值进行了循环右移25
位。然后程序将该字符的ASCII
值加到哈希值上。循环遍历完字符串中的所有字符后,哈希值即为最终的计算结果。
#include <stdio.h> #include <windows.h> DWORD GetHash (char *fun_name) { DWORD digest = 0 ; while (*fun_name) { digest = ((digest << 25 ) | (digest >> 7 )); digest += *fun_name; fun_name++; } return digest; } int main (int argc, char *argv[]) { DWORD MessageBoxHash; DWORD ExitProcessHash; DWORD LoadLibraryAHash; MessageBoxHash = GetHash("MessageBoxA" ); printf ("MessageBoxHash = 0x%.8x\n" , MessageBoxHash); ExitProcessHash = GetHash("ExitProcess" ); printf ("ExitProcessHash = 0x%.8x\n" , ExitProcessHash); LoadLibraryAHash = GetHash("LoadLibraryA" ); printf ("LoadLibraryAHash = 0x%.8x\n" , LoadLibraryAHash); system("pause" ); return 0 ; }
运行上方C语言实现代码,则读者可以此获取到三个核心函数的Hash值,其输出效果如下图所示;
在理解了C语言版本的计算流程后,那么汇编语言版本的也应该很容易理解,如下是使用Win32
汇编语言的实现过程,并在MASM
上正常编译,汇编版字符串转换Hash
值。
.386 p .model flat,stdcall option casemap:none include windows.inc include kernel32.inc include msvcrt.inc includelib kernel32.lib includelib msvcrt.lib .data data db "MessageBoxA" ,0 h Fomat db "0x%x" ,0 .code main PROC xor eax,eax ; 清空eax寄存器 xor edx,edx ; 清空edx寄存器 lea esi,data ; 取出字符串地址 loops: movsx eax,byte ptr[esi] ; 每次取出一个字符放入eax中 cmp al,ah ; 验证eax是否为0x0 即结束符 jz nops ; 为0 则说明计算完毕跳转到nops ror edx,7 ; 不为零,则进行循环右移7 位 add edx,eax ; 将循环右移的值不断累加 inc esi ; esi自增,用于读取下一个字符 jmp loops ; 循环执行 nops: mov eax,edx ; 结果存在eax里面 invoke crt_printf,addr Fomat,eax ret main ENDP END main
1.5.4 枚举Kernel32导出表 在文章开头部分我们通过WinDBG
调试器已经找到了Kernel32.dll
这个动态链接库的基地址,而Dll文件本质上也是PE文件,在Dll文件中同样存在导出表,其内部记录着该Dll的导出函数。接着我们需要对Dll文件的导出表进行遍历,不断地搜索,从而找到我们所需要的API函数,同样的可以通过如下定式获取到指定的导出表。
1.从kernel32.dll
加载基址算起,偏移0x3c
的地方就是其PE文件头。
2.PE文件头偏移0x78
的地方存放着指向函数导出表的指针。
3.导出表偏移0x1c
处的指针指向存储导出函数偏移地址(RVA)的列表。
4.导出表偏移0x20
处的指针指向存储导出函数函数名的列表。
首先我们通过WinDBG来实现读取导入表及导出表试试,我们以读取ole32.dll
为例,首先读者需要通过lmvm ole32.dll
查询到该模块的入口地址,如图所示该模块的入口地址为0x75830000
解析DOS头,DOS头通过_IMAGE_DOS_HEADER
结构被定义,在解析时读者应传入模块入口0x75830000
地址,其次DOS头中e_lfanew
字段指向了PE头,该字段需要注意;
执行读入DOS头:dt ole32!_IMAGE_DOS_HEADER 75830000
解析PE头,PE头通过DOS头部的e_lfanew
中存储的之加上模块基地址获取到,在本例中则是通过75830000+0n264
获取到;
读入PE头:dt ole32!_IMAGE_NT_HEADERS 75830000+0n264
接着需要在_IMAGE_OPTIONAL_HEADER
可选头中找到EXPORT
导出表基地址,通过PE头基址75830108
加上0x018
也就是OptionalHeader
的偏移,即可定位到DataDirectory[0]
也就是导出表基地址,其地址为75830180
根据上述定义,继续寻找EXPORT
导出表的实际地址,需要注意的是Evaluate expression
中的结果是根据ole32
模块的基地址与VirtualAddress
当前地址相加后得到的,如下图所示
当读者需要枚举特定模块时,则可通过模块基地址加上例如Name
字段偏移值,来读入模块名称;
如果读者需要枚举所有导出函数,则读者可通过模块基地址加上AddressOfNames
字段,并通过如下命令实现完整输出;
.foreach(place {dd 758e4088}){r @$t0=${place}+75830000; .if(@$t0<778e4088){da @$t0}}
导入表的枚举与导出表类似,为了节约篇幅此处只给出调试数据,读者可根据自己的掌握情况自行分析学习;
# 根据模块基地址获取模块e_lfanew 0 :000 > dt ole32!_IMAGE_DOS_HEADER 0x75830000 +0x000 e_magic : 0x5a4d +0x028 e_res2 : [10 ] 0 +0x03c e_lfanew : 0 n264 # 定位到NT头部 0 :000 > dt ole32!_IMAGE_NT_HEADERS 0x75830000 + 0 n264 +0x000 Signature : 0x4550 +0x004 FileHeader : _IMAGE_FILE_HEADER +0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER # 基地址与e_lfanew相加得到OPTIONAL 0 :000 > ?0x75830000 + 0 n264Evaluate expression: 1971519752 = 75830108 # 查询OPTIONAL 0 :000 > dt ole32!_IMAGE_OPTIONAL_HEADER -v -ny DataDirectory 75830108 +0x018 struct _IMAGE_OPTIONAL_HEADER, 31 elements, 0xe0 bytes +0x060 DataDirectory : [16 ] struct _IMAGE_DATA_DIRECTORY, 2 elements, 0x8 bytes 0 :000 > ? 75830108 +0x018 +0x60 Evaluate expression: 1971519872 = 75830180 # 得到数据目录表地址 0 :000 > dt ole32!_IMAGE_DATA_DIRECTORY 75830180 +8 +0x000 VirtualAddress : 0xbd9f8 +0x004 Size : 0x460 0 :000 > ? 0x75830000 +0xbd9f8 Evaluate expression: 1972296184 = 758 ed9f8 # DataDirectory[1 ]即为导入表,地址为758 ed9f8 0 :000 > dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758 ed9f8 +0x000 Characteristics : 0xbe700 +0x000 OriginalFirstThunk : 0xbe700 +0x004 TimeDateStamp : 0 +0x008 ForwarderChain : 0 +0x00c Name : 0xbe87a +0x010 FirstThunk : 0xbd8a8 0 :000 > da 0x75830000 +0xbe87a 758 ee87a "api-ms-win-crt-string-l1-1-0.dll" # 每一个_IMAGE_IMPORT_DESCRIPTOR的大小为0x14 0 :000 > ?? sizeof (_IMAGE_IMPORT_DESCRIPTOR)unsigned int 0x14 # 也就是说,每次递增14 即可输出下一个导入函数名 0 :000 > dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758 ed9f8+14 +0x000 Characteristics : 0xbe6f4 +0x000 OriginalFirstThunk : 0xbe6f4 +0x004 TimeDateStamp : 0 +0x008 ForwarderChain : 0 +0x00c Name : 0xbe89c +0x010 FirstThunk : 0xbd89c 0 :000 > da 0x75830000 +0xbe89c 758 ee89c "api-ms-win-crt-runtime-l1-1-0.dl" 0 :000 > dt ole32!_IMAGE_IMPORT_DESCRIPTOR 758 ed9f8+28 +0x000 Characteristics : 0xbe64c +0x000 OriginalFirstThunk : 0xbe64c +0x004 TimeDateStamp : 0 +0x008 ForwarderChain : 0 +0x00c Name : 0xbeb88 +0x010 FirstThunk : 0xbd7f4 0 :000 > da 0x75830000 +0xbeb88 758 eeb88 "api-ms-win-crt-private-l1-1-0.dl" # 分析第一个IID的IAT和INT # 先看INT: IMAGE_THUNK_DATA其实就是一个DWORD,如IID一样,也是一个接一个,最后一个为NULL 第一个: 0 :000 > dd 0xbe6f4 +0x75830000 L1758 ee6f4 000b e86c# 最高位不为1 (为1 表示为序号输入)指向_IMAGE_IMPORT_BY_NAME结构 .foreach(place {dd 758 ee6f4}) {r @$t0 = ${place}+75830000 +2 ; .if (@$t0<86 d00000){da @$t0;}} 758 ee86e "_initterm_e" 758 ee862 "_initterm" 75830002 "." 758 eeb80 "memset" 758 ee84e "wcsncmp" 758 ee858 "strcspn"
我们将问题回归到枚举导出表上,函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在列表定位任意函数的RVA地址,通过与动态链接库的基地址相加得到其真实的VA,而计算的地址就是我们最终在ShellCode
中调用时需要的地址,其汇编核心枚举代码如下所示;
#include <stdio.h> #include <Windows.h> int main (int argc, char * argv[]) { int a; __asm { mov ebx, dword ptr fs : [0x30 ] ; 获取当前线程信息的地址 mov ecx, dword ptr[ebx + 0xc ] ; 获取PEB结构体的地址 mov ecx, dword ptr[ecx + 0x1c ] ; 获取PEB结构体中的LDR结构体的地址 mov ecx, [ecx] ; 获取LDR结构体中的InMemoryOrderModuleList的头节点地址 mov edx, [ecx + 0x8 ] ; 获取第一个模块的基址,即ntdll.dll的基址 mov eax, [edx+0x3c ] ; 获取PE头偏移地址 mov ecx, [edx + eax + 0x78 ] ; 获取导出表VA地址偏移 add ecx,edx ; 将导出表的VA地址转换成绝对地址 mov ebx, [ecx+0x20 ] ; 获取导出表中的导出函数名偏移数组的地址 add ebx,edx ; 将函数名偏移数组的VA地址转换成绝对地址 xor edi,edi ; 将edi清零,用于循环计数 s1: inc edi ; 计数器自增1 mov esi, [ebx+edi*4 ] ; 通过偏移获取导出函数名的地址 add esi,edx ; 将导出函数名的VA地址转换成绝对地址 cmp esi,edx ; 检查导出函数名的地址是否合法,如果等于基址则跳过 je no loop s1 ; 继续查找导出函数名 no: xor eax,eax ; 清零eax寄存器,用于返回值 } system("pause" ); return 0 ; }
1.5.5 整合自定位ShellCode 完整的汇编代码如下,下方代码是一个定式,这里就只做了翻译,使用编译器编译如下代码。
#include <stdio.h> #include <windows.h> int main (int argc, char *argv) { __asm { CLD push 0x1E380A6A push 0x4FD18963 push 0x0C917432 mov esi, esp lea edi, [esi - 0xc ] xor ebx, ebx mov bh, 0x04 sub esp, ebx mov bx, 0x3233 push ebx push 0x72657375 push esp xor edx, edx mov ebx, fs:[edx + 0x30 ] mov ecx, [ebx + 0xC ] mov ecx, [ecx + 0x1C ] mov ecx, [ecx] mov ebp, [ecx + 0x8 ] find_lib_functions : lodsd cmp eax, 0x1E380A6A jne find_functions xchg eax, ebp call[edi - 0x8 ] xchg eax, ebp find_functions : pushad mov eax, [ebp + 0x3C ] mov ecx, [ebp + eax + 0x78 ] add ecx, ebp mov ebx, [ecx + 0x20 ] add ebx, ebp xor edi, edi next_function_loop : inc edi mov esi, [ebx + edi * 4 ] add esi, ebp cdq hash_loop : movsx eax, byte ptr[esi] cmp al, ah jz compare_hash ror edx, 7 add edx, eax inc esi jmp hash_loop compare_hash : cmp edx, [esp + 0x1C ] jnz next_function_loop mov ebx, [ecx + 0x24 ] add ebx, ebp mov di, [ebx + 2 * edi] mov ebx, [ecx + 0x1C ] add ebx, ebp add ebp, [ebx + 4 * edi] xchg eax, ebp pop edi stosd push edi popad cmp eax, 0x1e380a6a jne find_lib_functions xor ebx, ebx push ebx push 0x2020206b push 0x72616873 push 0x796c206f push 0x6c6c6568 mov eax, esp push ebx push eax push eax push ebx call[edi - 0x04 ] push ebx call[edi - 0x08 ] } return 0 ; }
运行后会弹出一个提示框hello lyshark
说明我们成功了,此列代码就是所谓的自定位代码,该代码可以不依赖于系统环境而独立运行;