PE结构、SEH相关知识学习笔记
原文:http://www.pediy.com/kssd/index.html -- 病毒技术 -- 病毒知识 -- Anti Virus专题
PE结构的学习
原文中用fasm自己构造了一个pe,这里贴一个用masm的,其实是使用WriteFile API将编写的PE数据写成文件~也没啥好说的,PE结构在这里没有仔细介绍,需要可以另外查询,剩下要说的的基本都在代码注释里了
参考:点击打开链接(PEDIY技术之新思路(二)_用'高级'编译器MASM实现自定义PE文件结构)
Pe.asm:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
REMOTE_CODE_START equ this BYTE 02.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 03.; DOS Header 04.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 05.PE_HEADER_START equ this BYTE 06.DOS_HEADER: 07.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 08.; 事实上,从这里到e_res2无用部分可以用一句DUP(0)来完成 09.; 在这里编译器不会检查你结构的定义是否正确, 这些变量名也是没有意义的 10.; 这样写只是有利于我们认识PE结构而已,除了关键字,你可以任意写 11.; 因为它相当于写二进制型汇编代码, 事实上,和直接写二进制是一样的 12.; 只是这样就轻松多了。当然了, 这种方式的定义给我们带来的问题就是重定位 13.; 所以需要重定位的地方要小心了, 你也可以把PE头部提上来到DOS头部里面来变形PE头 14.; 或者整个最小PE什么的, 随你怎么高兴怎么玩,但是要保证e_lfanew定位的正确 15.; 你想怎么来就怎么来,你自由了!但是,没有绝对的自由,俗话说得好 16.; 任悟空本领再高,他也跳不出如来佛(OS)的手掌心啊 17.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 18. e_magic db 'MZ' 19. e_cplp dw 0 20. e_cp dw 0 21. e_crlc dw 0 22. e_cparhdr dw 0 23. e_minalloc dw 0 24. e_maxalloc dw 0 25. e_ss dw 0 26. e_sp dw 0 27. e_csum dw 0 28. e_ip dw 0 29. e_cs dw 0 30. e_lfarlc dw 0 31. e_ovno dw 0 32. e_res dw 4 dup(0) 33. e_oemid dw 0 34. e_oeminfo dw 0 35. e_res2 dw 10 dup(0) 36. e_lfanew dd NT_HEADERS - 00401000h 37.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 38.; Dos小程序,写不写都行 39.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 40.Dos_Stub: 41. mov ah, 4ch 42. int 21h 43.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 44.; Nt Header 45.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 46.NT_HEADERS: 47. Signature dd 4550h ; 'PE' 48. Machine dw 14ch 49. NumberOfSections dw 2 50. TimeDateStamp dd 0 51. PointerToSymbolTable dd 0 52. NumberOfSymbols dd 0 53. SizeOfOptionalHeader dw 0e0h 54. Characteristics dw 010fh 55. Magic dw 10bh 56. MajorLinkerVersion db 0 57. MinorLinkerVersion db 0 58. SizeOfCode dd 200h 59. SizeOfInitializedData dd 0 60. SizeOfUninitializedData dd 0 61. AddressOfEntryPoint dd 1000h ; oep 62. BaseOfCode dd 1000h ; Code Section RVA 63. BaseOfData dd 0 ; Data Section RVA 64. ImageBase dd 00400000h 65. SectionAlignment dd 1000h ; Section Mem Align 66. FileAlignment dd 200h ; Section Disk Align 67. MajorOperSystemVersion dw 0 68. MinorOperSystemVersion dw 0 69. MajorImageVersion dw 0 70. MinorImageVersion dw 0 71. MajorSubsystemVersion dw 4 72. MinorSubsystemVersion dw 0 73. Win32VersionValue dd 0 ; Reserved 0 74. SizeOfImage dd 3000h ; PE加载到内存后的映像大小 75. SizeOfHeaders dd 200h ; DosHeaders + DosStub + NtHeader + Section Header 76. _CheckSum dd 0 77. SubSystem dw 2 ; GUI 78. DllCharacteristics dw 0 79. SizeOfStackReserve dd 100000h 80. SizeOfStackCommit dd 1000h ; Stack = 4kb 81. SizeOfHeapReserve dd 100000h 82. SizeOfHeapCommit dd 1000h ; Heap = 4kb 83. LoaderFlags dd 0 84. NumberOfRvaAndSizes dd 10h ; 16 85. DirectoryData1 dq 0 ; 没有输出表,填0 86. ImportTableAddress dd IMPORT_START - 00401000h - 400h + 2000h 87. ImportTableSize dd IMPORT_LENGTH 88. DirectoryData2 dq 14 dup(0) ; 余下的数据目录填0 89.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 90.; Section 1 91.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 92.SECTION_HEADER1: 93. Name1 db 'CODE', 0, 0, 0, 0 ; 节区名,8字节大小 94. VirtualSize dd CODE_LENGTH 95. VirtualAddress dd 1000h ; 内存中的偏移 96. SizeOfRawData dd 200h 97. ; 这里的CODE_START - 00401000h = 00401200h - 00401000h 98. ; 00401000h为Pe.asm, PE头200h,CODE节区在PE头后面,所以为 99. ; 00401200h,这里这样做可以求出代码段所在文件偏移 100. PointerToRawData dd CODE_START - 00401000h ; 文件中的偏移,也可以直接成为200h,因为定义的PE头大小为200 101. PointerToRelocations dd 0 102. PointerToLinenumbers dd 0 103. NumberOfRelocations dw 0 104. NumberOfLinenumbers dw 0 105. _Characteristics dd 0e0000020h ; 节区属性 106.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 107.; Section 2 108.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 109.SECTION_HEADER2: 110. Name2 db 'IMPORT', 0, 0 ; 这个块可以去掉,将输入表放入代码块里 111. VirtualSize2 dd IMPORT_LENGTH 112. VirtualAddress2 dd 2000h ; 内存中的偏移 113. SizeOfRawData2 dd 200h 114. ; 同上,这里IMPORT_START = 00401400h 115. ; PE头 + CODE节区 = 400h 116. PointerToRawData2 dd IMPORT_START - 00401000h ; 文件中的偏移,也可以直接写成400h 117. PointerToRelocations2 dd 0 118. PointerToLinenumbers2 dd 0 119. NumberOfRelocations2 dw 0 120. NumberOfLinenumbers2 dw 0 121. _Characteristics2 dd 0e0000020h 122.PE_HEADER_END equ this BYTE 123.PE_HEADER_LENGTH equ offset PE_HEADER_END - offset PE_HEADER_START 124.ZeroSpace1 db 200h - PE_HEADER_LENGTH dup(0) ; 不足200h, 间隙填0 125.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 126.; Code Start 127.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 128.CODE_START equ this BYTE 129. mov eax, OFFSET IAT_1 130. lea eax, [szContextR - 200h] ; 401200h为code段开始,szContextR = 401220h, 这是在PE.ASM包含在MakePe.asm时的情况 131. lea ebx, [szCaptionR - 200h] ; 如果要单独作为程序,其起始地址00401000h,要减去200h 132. push MB_OK 133. push ebx 134. push eax 135. push 0 136. ; IAT_1(0040143ch) - 1000h为不在CODE段 = (0040043ch) 137. ; 0040043ch - 400h = 0040003ch这样就等于ImageBase + 在导入表(400h)中的偏移(文件偏移) 138. ; 再加上2000h = 0040203ch为实际内存地址 139. call dword ptr [IAT_1 - 1000h - 400h + 2000h] 140. push 0 141. call dword ptr [IAT_2 - 1000h - 400h + 2000h] 142. szContextR db 'Congratulations! You make it!', 0dh, 0ah, 0 143. szCaptionR db 'OK', 0 144.CODE_END equ this BYTE 145.CODE_LENGTH equ offset CODE_END - offset CODE_START 146.ZeroSpace2 db 200h - CODE_LENGTH dup(0) 147.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 148.; Import Start 149.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 150.IMPORT_START equ this BYTE 151.IID_1: 152. ; IAT_1 - 401000h = 获得IAT_1的文件偏移, 再减400h为获得在导入表中的偏移(文件中) 153. ; 加2000h获得在内存中的偏移(RVA), 下面的一些和这个一个道理 154. OriginalFirstThunk dd IAT_1 - 401000h - 400h + 2000h 155. TimeDateStemp dd 0 156. ForwarderChain dd 0 157. DllName dd DllName1 - 401000h - 400h + 2000h 158. ; 这里为了方便,把它和OriginalFirstThunk指向同一个地址的IAT结构 159. FirstThunk dd IAT_1 - 401000h - 400h + 2000h 160.IID_2: 161. OriginalFirstThunk2 dd IAT_2-401000h-400h+2000h 162. TimeDateStemp2 dd 0 163. ForwarderChain2 dd 0 164. DllName2 dd _DllName2-401000h-400h+2000h 165. FirstThunk2 dd IAT_2-401000h-400h+2000h 166.IID_END: 167. IIDEND dd 5 dup(0) ; 用一个全0的结构作为结束 168.IAT_1: 169. AddressOfData1 dd IIBN_1-401000h-400h+2000h 170. AddressOfDataEnd1 dd 0 171.IAT_2: 172. AddressOfData2 dd IIBN_2-401000h-400h+2000h 173. AddressOfDataEnd2 dd 0 174.IIBN_1: 175. Hint1 dw 0 176. Nama1 db 'MessageBoxA',0 177. DllName1 db 'user32.dll',0,0 178.IIBN_2: 179. Hint2 dw 0 180. Nama2 db 'ExitProcess',0 181. _DllName2 db 'kernel32.dll',0,0 182.IMPORT_END equ this BYTE 183.IMPORT_LENGTH equ offset IMPORT_END - offset IMPORT_START 184.ZeroSpace3 db 200h - IMPORT_LENGTH dup(0) 185.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 186.; THE_PE_END 187.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 188.REMOTE_CODE_END equ this byte 189.REMOTE_CODE_LENGTH equ offset REMOTE_CODE_END - offset REMOTE_CODE_START
MakePe.asm:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
.386 02. .model flat, stdcall 03. option casemap:none 04. 05.include windows.inc 06.include user32.inc 07.include kernel32.inc 08.includelib user32.lib 09.includelib kernel32.lib 10. 11. .data 12.hOutFile dd 0 13.BytesWrite dd 0 14. 15. .const 16.szCaption db 'Info', 0 17.szContext db 'Success', 0 18.szOutFileName db 'Pe.exe', 0 19. 20. .code 21.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 22.; 将PE数据代码引入,编译并产生Pe.exe文件 23.;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 24.include Pe.asm 25. 26.start: 27. invoke CreateFile, offset szOutFileName, GENERIC_READ or GENERIC_WRITE, \ 28. FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL 29. mov hOutFile, eax 30. invoke WriteFile, hOutFile, offset REMOTE_CODE_START, REMOTE_CODE_LENGTH, \ 31. addr BytesWrite, NULL 32. invoke MessageBox, NULL, offset szContext, offset szCaption, MB_OK 33. invoke ExitProcess, 0 34. 35. end start
SEH的学习
发生异常时系统的处理顺序(by Jeremy Gordon):
1. 系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统 挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?
2. 如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果 你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.
3. 每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 可交由链起来的其他例程处理.
4. 如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5. 如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异 常处理例程的话,系统转向对它的调用.
6. 如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统 就调用ExitProcess终结程序.
7. 不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
有两种类型的异常处理句柄,一种是final型的,这是在你的异常未能得到线程相关处理例程处理操作系统在即将关闭程序之前会 回调的例程,这个例程是进程相关的而不是线程相关的,因此无论是哪个线程发生异常未能被处理,都会调用这个例程
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
.386 02. .model flat, stdcall 03. option casemap:none 04. 05.include windows.inc 06.include user32.inc 07.include kernel32.inc 08.includelib user32.lib 09.includelib kernel32.lib 10. 11. .data 12.szCaption db 'TestSEH', 0 13.szMsgOK db 'OK, the exception was handled by final handler!', 0 14.szMsgERR1 db 'It would never Get here!', 0 15.buff db 200 dup(0) 16. 17. .code 18.start: 19. lea eax, Final_Handler 20. invoke SetUnhandledExceptionFilter, eax 21. xor ecx, ecx 22. mov eax, 200 23. cdq 24. div ecx 25. ; 以下不会运行 26. invoke MessageBox, NULL, addr szMsgERR1, addr szCaption, \ 27. MB_OK or MB_ICONEXCLAMATION 28. invoke ExitProcess, NULL 29. 30.Final_Handler: 31. invoke MessageBox, NULL, addr szMsgOK, addr szCaption, \ 32. MB_OK or MB_ICONEXCLAMATION 33. mov eax, EXCEPTION_EXECUTE_HANDLER ; 1, 这时不出现非法操作的对话框 34. ;mov eax, EXCEPTION_CONTINUE_SEARCH ; 0, 出现,这时是调用系统默认的异常处理过程,程序被终结 35. ;mov eax, EXCEPTION_CONTINUE_EXECUTION ; -1, 不断出现对话框,你将先去死循环 36. ; 因为我们并没有修复ecx,所以不断产生异常,然后不断调用这个例程 37. 38. ret 39. 40. end start
windows根据你的异常处理程序的返回值来决定如何进一步处理 EXCEPTION_EXECUTE_HANDLER equ 1 表示我已经处理了异常,可以优雅地结束了 EXCEPTION_CONTINUE_SEARCH equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理程序 显示一个错误框,并结束 EXCEPTION_CONTINUE_EXECUTION equ -1 表示错误已经被修复,请从异常发生处继续执行
另一种是per_Thread Exception Handler->线程相关的异常处理,通常每个线程初始化准备好运行时fs指向一个TIB结构 (THREAD INFORMATION BLOCK)
这个结构的第一个元素fs:[0]指向一个_EXCEPTION_REGISTRATION结构, 后面_EXCEPTION_REGISTRATION为了简化,用ERR来代替这个结构...不要说没见过啊... fs:[0]-> _EXCEPTION_REGISTRATION struc prev dd ? ;前一个_EXCEPTION_REGISTRATION结构 handler dd ? ;异常处理例程入口....呵呵,现在明白该怎么作了吧 _EXCEPTION_REGISTRATION ends 我们可以建立一个ERR结构然后将fs:[0]换成指向他的指针,当然最常用的是堆栈,如果你用静态内存区也可以,没有人阻止你 在asm世界,放心地干吧,除了多S几次之外,通常不会有更大的损失 把handler域换成你的程序入口,就可以在发生异常时调用你的代码了
Ps: 下面的代码运行后会弹出异常处理例程中的对话框,然后弹出系统错误报告对话框,点击关闭后,又会弹出异常处理例程中的对话框,具体原因参见<Windows环境32位汇编语言程序设计>14.3.4节
注意和final返回值的含义不同
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
.386 02. .model flat, stdcall 03. option casemap:none 04. 05.include windows.inc 06.include user32.inc 07.include kernel32.inc 08.includelib user32.lib 09.includelib kernel32.lib 10. 11. .data 12.szCaption db 'TestSEH', 0 13.szMsgOK db "It's now in the Per_Thread handler!", 0 14.szMsgERR1 db 'It would never Get here!', 0 15.buff db 200 dup(0) 16. 17. .code 18.start: 19. assume fs:nothing 20. push offset perThread_Handler 21. push fs:[0] 22. mov fs:[0], esp 23. xor ecx, ecx 24. mov eax, 200 25. cdq 26. div ecx 27. ; 以下不会运行 28. invoke MessageBox, NULL, addr szMsgERR1, addr szCaption, \ 29. MB_OK or MB_ICONINFORMATION 30. pop fs:[0] 31. add esp, 4 32. invoke ExitProcess, NULL 33. 34.perThread_Handler: 35. invoke MessageBox, NULL, addr szMsgOK, addr szCaption, \ 36. MB_OK or MB_ICONINFORMATION 37. mov eax, 1 ; ExceptionContinueSearch, 不处理,由其他例程或系统处理 38. ;mov eax, 0 ; ExceptionContinueExecution, 表示修复CONTEXT, 可以从已成发生处继续执行 39. ; 如果返回0,你会陷入死循环,不断跳出对话框 40. ret 41. 42. end start
当异常发生时,系统给了我们一个处理异常的机会,他首先会调用我们自定义的seh处理例程,当然也包括 了相关信息,在调用之前,系统把包含这些信息结构的指针压入stack,供我们的异常处理例程调用, 传递给例程的参数通常是四个,其中只有三个有明确意义,另一个到现在为止还没有发现有什么作用, 这些参数是:pExcept:DWORD,pErr:DWORD,pContext:DWORD,pDispatch意义如下: pExcept: --- EXCEPTION_RECORD结构的指针 pErr: --- 前面ERR结构的指针 pContext: --- CONTEXT结构的指针 pDispatch:---没有发现有啥意义 ERR结构是前面介绍的_EXCEPTION_REGISTRATION结构,往前翻翻,Dispatch省略,下面介绍 EXCEPTION_RECORD和CONTEXT结构的定义(Windows核心编程第五版P657, Windows环境下32位汇编程序设计第二版P508): 01.EXCEPTION_RECORD STRUCT 02. ExceptionCode DWORD ? ;//异常码 03. ExceptionFlags DWORD ? ;//异常标志 04. pExceptionRecord DWORD ? ;//指向另外一个EXCEPTION_RECORD的指针 05. ExceptionAddress DWORD ? ;//异常发生的地址 06. NumberParameters DWORD ? ;//下面ExceptionInformation所含有的dword数目 07. ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?) 08.EXCEPTION_RECORD ENDS ;//EXCEPTION_MAXIMUM_PARAMETERS ==1 ExceptionCode 异常类型,SDK里面有很多类型,你可以在windows.inc里查找STATUS_来找到更多 的异常类型,下面只给出hex值,具体标识定义请查阅windows.inc,你最可能遇到的几种类型如下: C0000005h----读写内存冲突 C0000094h----非法除0 C00000FDh----堆栈溢出或者说越界 80000001h----由Virtual Alloc建立起来的属性页冲突 C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常 C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码, 如果RtlUnwind时没有Exception Record参数也同样会填入这个代码 80000003h----调试时因代码中int3中断 80000004h----处于被单步调试状态 注:也可以自己定义异常代码,遵循如下规则: _____________________________________________________________________+ 位: 31~30 29~28 27~16 15~0 _____________________________________________________________________+ 含义: 严重程度 29位 功能代码 异常代码 0==成功 0==Mcrosoft MICROSOFT定义 用户定义 1==通知 1==客户 2==警告 28位 3==错误 被保留必须为0 ExceptionFlags 异常标志 0----可修复异常 1----不可修复异常 2----正在展开,不要试图修复什么,需要的话,释放必要的资源 pExceptionRecord 如果程序本身导致异常,指向那个异常结构 ExceptionAddress 发生异常的eip地址 ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时含义如下 第一个dword 0==读冲突 1==写冲突 第二个dword 读写冲突地址