我想打电话给驻留在第三方.exe文件的功能,并获得其结果。 好像应该有办法,只要我知道函数的地址,调用约定,等...但我不知道怎么办。
有谁知道我会怎么做呢?
我认识到,任何解决方案将是一个非标准的黑客,但必须有办法!
我不邪恶的用例:我逆向工程我的软件文件格式。 在这个函数的计算过于复杂,我的小大脑弄清楚; 我已经能够直接拉动汇编代码到我自己的DLL进行测试,但我当然不能释放,因为这会被窃取。 我会假设用户已经具备了这种特殊的应用预装,所以我的软件将运行。
我想打电话给驻留在第三方.exe文件的功能,并获得其结果。 好像应该有办法,只要我知道函数的地址,调用约定,等...但我不知道怎么办。
有谁知道我会怎么做呢?
我认识到,任何解决方案将是一个非标准的黑客,但必须有办法!
我不邪恶的用例:我逆向工程我的软件文件格式。 在这个函数的计算过于复杂,我的小大脑弄清楚; 我已经能够直接拉动汇编代码到我自己的DLL进行测试,但我当然不能释放,因为这会被窃取。 我会假设用户已经具备了这种特殊的应用预装,所以我的软件将运行。
好了,我已经把原型。
这个程序创建自己的另一个实例作为调试子进程。
自动断点将前main()和CRT初始化代码可能遇到。 这是我们可以改变内存和调试过程中的寄存器,使其执行感兴趣的功能。 这就是今天的程序在做什么。
它试图捕获并处理所有不好的情况下(如意外例外),并报告他们的错误。
一个糟糕的情况实际上是一个很好的一个。 这是从UD2指令#UD例外,该计划的地方进入调试过程。 它使用该#UD感兴趣的函数返回之后停止流程执行。
一些更多的注意事项:
这个代码是唯一的32位。 我甚至没有尝试,使其64位编译或支持64位的子进程。
此代码可能会泄漏句柄。 请参阅MSDN上的Windows调试API函数说明,以找出他们需要关闭。
这个代码是唯一的概念证明和通过指针或大于EAX,ECX EDX和其它寄存器不支持传递和返回数据。 你必须把它扩大是必要的。
此代码需要一些特权,以便能够创建和调试全过程。 您可能会担心这一点,如果你的程序的用户不是管理员。
请享用。
码:
// file: unexported.c
//
// compile with Open Watcom C/C++: wcl386 /q /wx /we /s unexported.c
// (Note: "/s" is needed to avoid stack check calls from the "unexported"
// functions, these calls are through a pointer, and it'll be
// uninitialized in our case.)
//
// compile with MinGW gcc 4.6.2: gcc unexported.c -o unexported.exe
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include <limits.h>
#ifndef C_ASSERT
#define C_ASSERT(expr) extern char CAssertExtern[(expr)?1:-1]
#endif
// Compile as a 32-bit app only.
C_ASSERT(sizeof(void*) * CHAR_BIT == 32);
#define EXC_CODE_AND_NAME(X) { X, #X }
const struct
{
DWORD Code;
PCSTR Name;
} ExcCodesAndNames[] =
{
EXC_CODE_AND_NAME(EXCEPTION_ACCESS_VIOLATION),
EXC_CODE_AND_NAME(EXCEPTION_ARRAY_BOUNDS_EXCEEDED),
EXC_CODE_AND_NAME(EXCEPTION_BREAKPOINT),
EXC_CODE_AND_NAME(EXCEPTION_DATATYPE_MISALIGNMENT),
EXC_CODE_AND_NAME(EXCEPTION_FLT_DENORMAL_OPERAND),
EXC_CODE_AND_NAME(EXCEPTION_FLT_DIVIDE_BY_ZERO),
EXC_CODE_AND_NAME(EXCEPTION_FLT_INEXACT_RESULT),
EXC_CODE_AND_NAME(EXCEPTION_FLT_INVALID_OPERATION),
EXC_CODE_AND_NAME(EXCEPTION_FLT_OVERFLOW),
EXC_CODE_AND_NAME(EXCEPTION_FLT_STACK_CHECK),
EXC_CODE_AND_NAME(EXCEPTION_FLT_UNDERFLOW),
EXC_CODE_AND_NAME(EXCEPTION_ILLEGAL_INSTRUCTION),
EXC_CODE_AND_NAME(EXCEPTION_IN_PAGE_ERROR),
EXC_CODE_AND_NAME(EXCEPTION_INT_DIVIDE_BY_ZERO),
EXC_CODE_AND_NAME(EXCEPTION_INT_OVERFLOW),
EXC_CODE_AND_NAME(EXCEPTION_INVALID_DISPOSITION),
EXC_CODE_AND_NAME(EXCEPTION_NONCONTINUABLE_EXCEPTION),
EXC_CODE_AND_NAME(EXCEPTION_PRIV_INSTRUCTION),
EXC_CODE_AND_NAME(EXCEPTION_SINGLE_STEP),
EXC_CODE_AND_NAME(EXCEPTION_STACK_OVERFLOW),
EXC_CODE_AND_NAME(EXCEPTION_GUARD_PAGE),
EXC_CODE_AND_NAME(DBG_CONTROL_C),
{ 0xE06D7363, "C++ EH exception" }
};
PCSTR GetExceptionName(DWORD code)
{
DWORD i;
for (i = 0; i < sizeof(ExcCodesAndNames) / sizeof(ExcCodesAndNames[0]); i++)
{
if (ExcCodesAndNames[i].Code == code)
{
return ExcCodesAndNames[i].Name;
}
}
return "?";
}
typedef enum tCallConv
{
CallConvCdecl, // Params on stack; caller removes params
CallConvStdCall, // Params on stack; callee removes params
CallConvFastCall // Params in ECX, EDX and on stack; callee removes params
} tCallConv;
DWORD Execute32bitFunctionFromExe(PCSTR ExeName,
int FunctionAddressIsRelative,
DWORD FunctionAddress,
tCallConv CallConvention,
DWORD CodeDataStackSize,
ULONG64* ResultEdxEax,
DWORD DwordParamsCount,
.../* DWORD params */)
{
STARTUPINFO startupInfo;
PROCESS_INFORMATION processInfo;
DWORD dwContinueStatus = DBG_CONTINUE; // exception continuation
DEBUG_EVENT dbgEvt;
UCHAR* procMem = NULL;
DWORD breakPointCount = 0;
DWORD err = ERROR_SUCCESS;
DWORD ecxEdxParams[2] = { 0, 0 };
DWORD imageBase = 0;
CONTEXT ctx;
va_list ap;
va_start(ap, DwordParamsCount);
*ResultEdxEax = 0;
memset(&startupInfo, 0, sizeof(startupInfo));
startupInfo.cb = sizeof(startupInfo);
memset(&processInfo, 0, sizeof(processInfo));
if (!CreateProcess(
NULL,
(LPSTR)ExeName,
NULL,
NULL,
FALSE,
DEBUG_ONLY_THIS_PROCESS, // DEBUG_PROCESS,
NULL,
NULL,
&startupInfo,
&processInfo))
{
printf("CreateProcess() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
printf("Process 0x%08X (0x%08X) \"%s\" created,\n"
" Thread 0x%08X (0x%08X) created\n",
processInfo.dwProcessId,
processInfo.hProcess,
ExeName,
processInfo.dwThreadId,
processInfo.hThread);
procMem = VirtualAllocEx(
processInfo.hProcess,
NULL,
CodeDataStackSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (procMem == NULL)
{
printf("VirtualAllocEx() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
printf("Allocated RWX memory in process 0x%08X (0x%08X) "
"at address 0x%08X\n",
processInfo.dwProcessId,
processInfo.hProcess,
procMem);
while (dwContinueStatus)
{
// Wait for a debugging event to occur. The second parameter indicates
// that the function does not return until a debugging event occurs.
if (!WaitForDebugEvent(&dbgEvt, INFINITE))
{
printf("WaitForDebugEvent() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
// Process the debugging event code.
switch (dbgEvt.dwDebugEventCode)
{
case EXCEPTION_DEBUG_EVENT:
// Process the exception code. When handling
// exceptions, remember to set the continuation
// status parameter (dwContinueStatus). This value
// is used by the ContinueDebugEvent function.
printf("%s (%s) Exception in process 0x%08X, thread 0x%08X\n"
" Exc. Code = 0x%08X (%s), Instr. Address = 0x%08X",
dbgEvt.u.Exception.dwFirstChance ?
"First Chance" : "Last Chance",
dbgEvt.u.Exception.ExceptionRecord.ExceptionFlags ?
"non-continuable" : "continuable",
dbgEvt.dwProcessId,
dbgEvt.dwThreadId,
dbgEvt.u.Exception.ExceptionRecord.ExceptionCode,
GetExceptionName(dbgEvt.u.Exception.ExceptionRecord.ExceptionCode),
dbgEvt.u.Exception.ExceptionRecord.ExceptionAddress);
if (dbgEvt.u.Exception.ExceptionRecord.ExceptionCode ==
EXCEPTION_ACCESS_VIOLATION)
{
ULONG_PTR* info = dbgEvt.u.Exception.ExceptionRecord.ExceptionInformation;
printf(",\n Access Address = 0x%08X, Access = 0x%08X (%s)",
(DWORD)info[1],
(DWORD)info[0],
(info[0] == 0) ?
"read" : ((info[0] == 1) ? "write" : "execute")); // 8 = DEP
}
printf("\n");
// Get the thread context (register state).
// We'll need to either display it (in case of unexpected exceptions) or
// modify it (to execute our code) or read it (to get the results of
// execution).
memset(&ctx, 0, sizeof(ctx));
ctx.ContextFlags = CONTEXT_INTEGER | CONTEXT_CONTROL;
if (!GetThreadContext(processInfo.hThread, &ctx))
{
printf("GetThreadContext() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
#if 0
printf(" EAX=0x%08X EBX=0x%08X ECX=0x%08X EDX=0x%08X EFLAGS=0x%08X\n"
" ESI=0x%08X EDI=0x%08X EBP=0x%08X ESP=0x%08X EIP=0x%08X\n",
ctx.Eax, ctx.Ebx, ctx.Ecx, ctx.Edx, ctx.EFlags,
ctx.Esi, ctx.Edi, ctx.Ebp, ctx.Esp, ctx.Eip);
#endif
if (dbgEvt.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT &&
breakPointCount == 0)
{
// Update the context so our code can be executed
DWORD mem, i, data;
SIZE_T numberOfBytesCopied;
mem = (DWORD)procMem + CodeDataStackSize;
// Child process memory layout (inside the procMem[] buffer):
//
// higher
// addresses
// .
// . UD2 instruction (causes #UD, indicator of successful
// . completion of FunctionAddress())
// .
// . last on-stack parameter for FunctionAddress()
// . ...
// . first on-stack parameter for FunctionAddress()
// .
// . address of UD2 instruction (as if "call FunctionAddress"
// . executed just before it and is going to return to UD2)
// . (ESP will point here)
// .
// . FunctionAddress()'s stack
// .
// lower
// addresses
mem -= 2;
data = 0x0B0F; // 0x0F, 0x0B = UD2 instruction
if (!WriteProcessMemory(processInfo.hProcess,
(PVOID)mem,
&data,
2,
&numberOfBytesCopied))
{
ErrWriteMem1:
printf("WriteProcessMemory() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
else if (numberOfBytesCopied != 2)
{
ErrWriteMem2:
printf("WriteProcessMemory() failed with error 0x%08X\n",
err = ERROR_BAD_LENGTH);
goto Cleanup;
}
// Copy function parameters.
mem &= 0xFFFFFFFC; // align the address for the stack
for (i = 0; i < DwordParamsCount; i++)
{
if (CallConvention == CallConvFastCall && i < 2)
{
ecxEdxParams[i] = va_arg(ap, DWORD);
}
else
{
data = va_arg(ap, DWORD);
if (!WriteProcessMemory(processInfo.hProcess,
(DWORD*)mem - DwordParamsCount + i,
&data,
sizeof(data),
&numberOfBytesCopied))
{
goto ErrWriteMem1;
}
else if (numberOfBytesCopied != sizeof(data))
{
goto ErrWriteMem2;
}
}
}
// Adjust what will become ESP according to the number of on-stack parameters.
for (i = 0; i < DwordParamsCount; i++)
{
if (CallConvention != CallConvFastCall || i >= 2)
{
mem -= 4;
}
}
// Store the function return address.
mem -= 4;
data = (DWORD)procMem + CodeDataStackSize - 2; // address of UD2
if (!WriteProcessMemory(processInfo.hProcess,
(PVOID)mem,
&data,
sizeof(data),
&numberOfBytesCopied))
{
goto ErrWriteMem1;
}
else if (numberOfBytesCopied != sizeof(data))
{
goto ErrWriteMem2;
}
// Last-minute preparations for execution...
// Set up the registers (ECX, EDX, EFLAGS, EIP, ESP).
if (CallConvention == CallConvFastCall)
{
if (DwordParamsCount >= 1) ctx.Ecx = ecxEdxParams[0];
if (DwordParamsCount >= 2) ctx.Edx = ecxEdxParams[1];
}
ctx.EFlags &= ~(1 << 10); // clear DF for string instructions
ctx.Eip = FunctionAddress + imageBase * !!FunctionAddressIsRelative;
ctx.Esp = mem;
if (!SetThreadContext(processInfo.hThread, &ctx))
{
printf("SetThreadContext() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
printf("Copied code/data to the process\n");
#if 0
for (i = esp; i < (DWORD)procMem + CodeDataStackSize; i++)
{
data = 0;
ReadProcessMemory(processInfo.hProcess,
(void*)i,
&data,
1,
&numberOfBytesCopied);
printf("E[SI]P = 0x%08X: 0x%02X\n", i, data);
}
#endif
breakPointCount++;
dwContinueStatus = DBG_CONTINUE; // continue execution of our code
}
else if (dbgEvt.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_ILLEGAL_INSTRUCTION &&
breakPointCount == 1 &&
ctx.Eip == (DWORD)procMem + CodeDataStackSize - 2/*UD2 size*/)
{
// The code has finished execution as expected.
// Collect the results.
*ResultEdxEax = ((ULONG64)ctx.Edx << 32) | ctx.Eax;
printf("Copied code/data from the process\n");
dwContinueStatus = 0; // stop debugging
}
else
{
// Unexpected event. Do not continue execution.
printf(" EAX=0x%08X EBX=0x%08X ECX=0x%08X EDX=0x%08X EFLAGS=0x%08X\n"
" ESI=0x%08X EDI=0x%08X EBP=0x%08X ESP=0x%08X EIP=0x%08X\n",
ctx.Eax, ctx.Ebx, ctx.Ecx, ctx.Edx, ctx.EFlags,
ctx.Esi, ctx.Edi, ctx.Ebp, ctx.Esp, ctx.Eip);
err = dbgEvt.u.Exception.ExceptionRecord.ExceptionCode;
goto Cleanup;
}
break; // case EXCEPTION_DEBUG_EVENT:
case CREATE_PROCESS_DEBUG_EVENT:
// As needed, examine or change the registers of the
// process's initial thread with the GetThreadContext and
// SetThreadContext functions; read from and write to the
// process's virtual memory with the ReadProcessMemory and
// WriteProcessMemory functions; and suspend and resume
// thread execution with the SuspendThread and ResumeThread
// functions. Be sure to close the handle to the process image
// file with CloseHandle.
printf("Process 0x%08X (0x%08X) "
"created, base = 0x%08X,\n"
" Thread 0x%08X (0x%08X) created, start = 0x%08X\n",
dbgEvt.dwProcessId,
dbgEvt.u.CreateProcessInfo.hProcess,
dbgEvt.u.CreateProcessInfo.lpBaseOfImage,
dbgEvt.dwThreadId,
dbgEvt.u.CreateProcessInfo.hThread,
dbgEvt.u.CreateProcessInfo.lpStartAddress);
// Found image base!
imageBase = (DWORD)dbgEvt.u.CreateProcessInfo.lpBaseOfImage;
dwContinueStatus = DBG_CONTINUE;
break;
case EXIT_PROCESS_DEBUG_EVENT:
// Display the process's exit code.
printf("Process 0x%08X exited, exit code = 0x%08X\n",
dbgEvt.dwProcessId,
dbgEvt.u.ExitProcess.dwExitCode);
// Unexpected event. Do not continue execution.
err = ERROR_PROC_NOT_FOUND;
goto Cleanup;
case CREATE_THREAD_DEBUG_EVENT:
case EXIT_THREAD_DEBUG_EVENT:
case LOAD_DLL_DEBUG_EVENT:
case UNLOAD_DLL_DEBUG_EVENT:
case OUTPUT_DEBUG_STRING_EVENT:
dwContinueStatus = DBG_CONTINUE;
break;
case RIP_EVENT:
printf("RIP: Error = 0x%08X, Type = 0x%08X\n",
dbgEvt.u.RipInfo.dwError,
dbgEvt.u.RipInfo.dwType);
// Unexpected event. Do not continue execution.
err = dbgEvt.u.RipInfo.dwError;
goto Cleanup;
} // end of switch (dbgEvt.dwDebugEventCode)
// Resume executing the thread that reported the debugging event.
if (dwContinueStatus)
{
if (!ContinueDebugEvent(dbgEvt.dwProcessId,
dbgEvt.dwThreadId,
dwContinueStatus))
{
printf("ContinueDebugEvent() failed with error 0x%08X\n",
err = GetLastError());
goto Cleanup;
}
}
} // end of while (dwContinueStatus)
err = ERROR_SUCCESS;
Cleanup:
if (processInfo.hProcess != NULL)
{
if (procMem != NULL)
{
VirtualFreeEx(processInfo.hProcess, procMem, 0, MEM_RELEASE);
}
TerminateProcess(processInfo.hProcess, 0);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
}
va_end(ap);
return err;
}
int __cdecl FunctionCdecl(int x, int y, int z)
{
return x + y + z;
}
int __stdcall FunctionStdCall(int x, int y, int z)
{
return x * y * z;
}
ULONG64 __fastcall FunctionFastCall(DWORD x, DWORD y, DWORD z)
{
return (ULONG64)x * y + z;
}
int main(int argc, char** argv)
{
DWORD err;
ULONG64 resultEdxEax;
err = Execute32bitFunctionFromExe(argv[0]/*ExeName*/,
1/*FunctionAddressIsRelative*/,
(DWORD)&FunctionCdecl -
(DWORD)GetModuleHandle(NULL),
CallConvCdecl,
4096/*CodeDataStackSize*/,
&resultEdxEax,
3/*DwordParamsCount*/,
2, 3, 4);
if (err == ERROR_SUCCESS)
printf("2 + 3 + 4 = %d\n", (int)resultEdxEax);
err = Execute32bitFunctionFromExe(argv[0]/*ExeName*/,
1/*FunctionAddressIsRelative*/,
(DWORD)&FunctionStdCall -
(DWORD)GetModuleHandle(NULL),
CallConvStdCall,
4096/*CodeDataStackSize*/,
&resultEdxEax,
3/*DwordParamsCount*/,
-2, 3, 4);
if (err == ERROR_SUCCESS)
printf("-2 * 3 * 4 = %d\n", (int)resultEdxEax);
err = Execute32bitFunctionFromExe(argv[0]/*ExeName*/,
1/*FunctionAddressIsRelative*/,
(DWORD)&FunctionFastCall -
(DWORD)GetModuleHandle(NULL),
CallConvFastCall,
4096/*CodeDataStackSize*/,
&resultEdxEax,
3/*DwordParamsCount*/,
-1, -1, -1);
if (err == ERROR_SUCCESS)
printf("0xFFFFFFFF * 0xFFFFFFFF + 0xFFFFFFFF = 0x%llX\n",
(unsigned long long)resultEdxEax);
return 0;
}
输出:
Process 0x00001514 (0x00000040) "C:\MinGW\msys\1.0\home\Alex\unexported.exe" cre
ated,
Thread 0x00000CB0 (0x0000003C) created
Allocated RWX memory in process 0x00001514 (0x00000040) at address 0x002B0000
Process 0x00001514 (0x00000044) created, base = 0x00400000,
Thread 0x00000CB0 (0x00000048) created, start = 0x0040126C
First Chance (continuable) Exception in process 0x00001514, thread 0x00000CB0
Exc. Code = 0x80000003 (EXCEPTION_BREAKPOINT), Instr. Address = 0x77090FAB
Copied code/data to the process
First Chance (continuable) Exception in process 0x00001514, thread 0x00000CB0
Exc. Code = 0xC000001D (EXCEPTION_ILLEGAL_INSTRUCTION), Instr. Address = 0x002
B0FFE
Copied code/data from the process
2 + 3 + 4 = 9
Process 0x00001828 (0x0000003C) "C:\MinGW\msys\1.0\home\Alex\unexported.exe" cre
ated,
Thread 0x00001690 (0x00000040) created
Allocated RWX memory in process 0x00001828 (0x0000003C) at address 0x002B0000
Process 0x00001828 (0x0000006C) created, base = 0x00400000,
Thread 0x00001690 (0x00000074) created, start = 0x0040126C
First Chance (continuable) Exception in process 0x00001828, thread 0x00001690
Exc. Code = 0x80000003 (EXCEPTION_BREAKPOINT), Instr. Address = 0x77090FAB
Copied code/data to the process
First Chance (continuable) Exception in process 0x00001828, thread 0x00001690
Exc. Code = 0xC000001D (EXCEPTION_ILLEGAL_INSTRUCTION), Instr. Address = 0x002
B0FFE
Copied code/data from the process
-2 * 3 * 4 = -24
Process 0x00001388 (0x00000040) "C:\MinGW\msys\1.0\home\Alex\unexported.exe" cre
ated,
Thread 0x00001098 (0x0000003C) created
Allocated RWX memory in process 0x00001388 (0x00000040) at address 0x002B0000
Process 0x00001388 (0x0000008C) created, base = 0x00400000,
Thread 0x00001098 (0x00000090) created, start = 0x0040126C
First Chance (continuable) Exception in process 0x00001388, thread 0x00001098
Exc. Code = 0x80000003 (EXCEPTION_BREAKPOINT), Instr. Address = 0x77090FAB
Copied code/data to the process
First Chance (continuable) Exception in process 0x00001388, thread 0x00001098
Exc. Code = 0xC000001D (EXCEPTION_ILLEGAL_INSTRUCTION), Instr. Address = 0x002
B0FFE
Copied code/data from the process
0xFFFFFFFF * 0xFFFFFFFF + 0xFFFFFFFF = 0xFFFFFFFF00000000
这是可能的,但不是小事。 是的,这是一个非常肮脏的黑客。
在某些情况下,装用调用LoadLibrary EXE文件就足够了。 返回HMODULE实际上是加载的EXE的基址。 它转换为合适的int类型,添加相关功能的地址到,将它转换回一个函数指针,并呼吁通过该指针的功能。
不幸的是,EXE文件可能已搬迁信息剥离。 这意味着EXE将期待从特定地址中运行。 在这种情况下,你必须改变你自己的程序的基址,以避免冲突。 检查你的链接的文档,应该有这样做的一个选项。 在此之后,调用LoadLibrary将加载EXE在其首选基址,并希望所有应该能正常运行。
有这方面的一些非常有用的信息在这里 。 确保在页面的结尾不同的技术,在某些情况下更好地工作,以检查更新。
编辑:亚历克斯在下面的评论正确地指出,如果函数依赖于一些初始化值,也称这样的功能,包括大多数C运行时函数,这将是更难,使其工作。 一个可识别的初始化函数和事前,但称他们使用调试API可能在这些情况下,你最好的选择。
取而代之的加载EXE到你的过程中,较好的两个(IMO)的方法:
1)使用调试API(或类似的东西PyDbg)启动目标调试器下,然后设置参数在堆栈中,EIP设置必要的地址,把断点上的返回地址,并恢复。
2)做一个小DLL一些IPC与您的程序进行通信,其注入到目标(有几种方法可以做到这一点,最简单的可能是键盘钩子),并有它调用所需的代码。 或者你可以使用现有的也能做到这一点,例如英特尔的PIN 。