学习了PE一段时间,也算是对PE有了点了解,在看雪学院也学习到了不少东西,于是自己动手写了个感染PE的小程序,这个就是自己练手,加深自己PE Header的理解,也可以帮助刚入门的朋友学习。这个程序就是实现对winmine.exe这处程序感染,修改它的PE结构,添加一个PE section
//////////////////////////////////////////////////////////////////////////
code.c 生成注入代码
//////////////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <windows.h>
int main()
{
unsigned char *addr, *code_start_addr, *code_end_addr, *_orign_aoep_addr;
DWORD dwSizeOfCode, dwAOEPOffset, dwCont, dwBaseAddress;
PBYTE pCode, pBuf;
__asm
{
jmp _end
_start:
pushad
push ebp
mov ebp, esp
// ebp-0x04 ====> hmodkern32
// ebp-0x08 ====> dwAddrOfGetProcAddress
// ebp-0x0C ====> dwAddrOfLoadLibraryA
// ebp-0x10 ====> hmoduser32
// ebp-0x14 ====> dwAddrOfMessageBoxA
// ebp-0x18 ====> dwAddrOfGetModuleHandleA
sub esp, 0x20
//hmodkern32 = _GetBaseKernel32();
call _GetBaseKernel32
mov [ebp - 0x04], eax // save Base Address of "Kernel32.dll"
//dwAddrOfGetProcAddress = _GetGetProcAddrBase(hmodkern32);
push eax
call _GetGetProcAddrBase
mov [ebp - 0x08], eax // save GetProcAddress
add esp, 0x04
call _delta
_delta:
pop ebx // save registers context from stack
sub ebx, offset _delta
// dwAddrOfLoadLibraryA = GetProcAddress(hmodkern32, "LoadLibraryA")
lea ecx, [ebx + _str_lla]
push ecx
push [ebp - 0x04]
call dword ptr [ebp - 0x08]
mov [ebp - 0x0C], eax
// hmoduser32 = LoadLibraryA("user32.dll");
lea ecx, [ebx + _str_u32]
push ecx
call dword ptr [ebp - 0x0C]
mov [ebp-0x10], eax
// dwAddrOfMessageBoxA = GetProcAddress(hmoduser32, "MessageBoxA");
lea ecx, [ebx + _str_mba]
push ecx
push dword ptr [ebp-0x10]
call dword ptr [ebp-0x08]
mov [ebp-0x14], eax
// MessageBoxA(0, "Hello", "", 0);
push 0
lea ecx, [ebx + _str_emp]
push ecx
lea ecx, [ebx + _str_hello]
push ecx
push 0
call dword ptr [ebp-0x14]
// dwAddrOfGetModuleHandleA = GetProcAddress(hmodkern32, "GetModuleHandleA")
lea ecx, [ebx + _str_gmha]
push ecx
push [ebp - 0x04]
call dword ptr [ebp - 0x08]
mov [ebp - 0x18], eax
// GetModuleHandleA(0);
push 0
call dword ptr [ebp - 0x18]
mov esp, ebp
pop ebp
add eax,dword ptr [ebx + _orign_aoep]
mov dword ptr [esp + 0x1C], eax // save (dwBaseAddress + orign) to eax-location-of-context
popad // load registers context from stack
push eax
xor eax, eax
retn // >> jump to the Original AOEP
// ---------------------------------------------------------
// type : DWORD GetBaseKernel32()
_GetBaseKernel32:
push ebp
mov ebp, esp
push esi
push edi
xor ecx, ecx // ECX = 0
mov esi, fs:[0x30] // ESI = &(PEB) ([FS:0x30])
mov esi, [esi + 0x0c] // ESI = PEB->Ldr
mov esi, [esi + 0x1c] // ESI = PEB->Ldr.InInitOrder
next_module:
mov eax, [esi + 0x08] // EBP = InInitOrder[X].base_address
mov edi, [esi + 0x20] // EBP = InInitOrder[X].module_name (unicode)
mov esi, [esi] // ESI = InInitOrder[X].flink (next module)
cmp [edi + 12*2], cx // modulename[12] == 0 ?
jne next_module // No: try next module.
pop edi
pop esi
mov esp, ebp
pop ebp
ret
// ---------------------------------------------------------
// type : DWORD GetGetProcAddrBase(DWORD base)
_GetGetProcAddrBase:
push ebp
mov ebp, esp
push edx
push ebx
push edi
push esi
mov ebx, [ebp+8]
mov eax, [ebx + 0x3c] // edi = BaseAddr, eax = pNtHeader
mov edx, [ebx + eax + 0x78]
add edx, ebx // edx = Export Table (RVA)
mov ecx, [edx + 0x18] // ecx = NumberOfNames
mov edi, [edx + 0x20] //
add edi, ebx // ebx = AddressOfNames
_search:
dec ecx
mov esi, [edi + ecx*4]
add esi, ebx
mov eax, 0x50746547 // "PteG"
cmp [esi], eax
jne _search
mov eax, 0x41636f72 //"Acor"
cmp [esi+4], eax
jne _search
mov edi, [edx + 0x24] //
add edi, ebx // edi = AddressOfNameOrdinals
mov cx, word ptr [edi + ecx*2] // ecx = GetProcAddress-orinal
mov edi, [edx + 0x1c] //
add edi, ebx // edi = AddressOfFunction
mov eax, [edi + ecx*4]
add eax, ebx // eax = GetProcAddress
pop esi
pop edi
pop ebx
pop edx
mov esp, ebp
pop ebp
ret
_str_lla:
_emit 'L'
_emit 'o'
_emit 'a'
_emit 'd'
_emit 'L'
_emit 'i'
_emit 'b'
_emit 'r'
_emit 'a'
_emit 'r'
_emit 'y'
_emit 'A'
_emit 0x0
_str_u32:
_emit 'u'
_emit 's'
_emit 'e'
_emit 'r'
_emit '3'
_emit '2'
_emit '.'
_emit 'd'
_emit 'l'
_emit 'l'
_emit 0x0
_str_mba:
_emit 'M'
_emit 'e'
_emit 's'
_emit 's'
_emit 'a'
_emit 'g'
_emit 'e'
_emit 'B'
_emit 'o'
_emit 'x'
_emit 'A'
_emit 0x0
_str_gmha:
_emit 'G'
_emit 'e'
_emit 't'
_emit 'M'
_emit 'o'
_emit 'd'
_emit 'u'
_emit 'l'
_emit 'e'
_emit 'H'
_emit 'a'
_emit 'n'
_emit 'd'
_emit 'l'
_emit 'e'
_emit 'A'
_emit 0x0
_str_hello:
_emit 'H'
_emit 'e'
_emit 'l'
_emit 'l'
_emit 'o'
_str_emp:
_emit 0x0
_orign_aoep:
_emit 0xCC
_emit 0xCC
_emit 0xCC
_emit 0xCC
_end:
nop
mov eax, offset _start
mov code_start_addr, eax
mov eax, offset _end
mov code_end_addr, eax
mov eax, offset _orign_aoep
mov _orign_aoep_addr, eax
}
for (addr=code_start_addr; addr!=code_end_addr; addr++)
{
if (!((unsigned int)addr & 0x00000007)) printf("\n");
printf("0x%02X,", *addr);
}
pCode = code_start_addr;
dwSizeOfCode = code_end_addr - code_start_addr;
dwAOEPOffset = _orign_aoep_addr - code_start_addr;
printf("\nsize(dec) : %d \n", dwSizeOfCode);
printf("_orign_aoep offset : 0x%02X\n", dwAOEPOffset);
// Code below is used to test the generated code. If your code is correct,
// program will output "OK", otherwise "Failed!".
pBuf = (PBYTE)malloc(dwSizeOfCode);
CopyMemory(pBuf, pCode, dwSizeOfCode);
__asm
{
mov eax, offset _cont
mov [dwCont], eax
}
dwBaseAddress = GetModuleHandleA(0);
*((PDWORD)(pBuf + dwAOEPOffset)) = dwCont - dwBaseAddress;
__asm
{
call dword ptr [pBuf]
}
printf("Failed!\n");
return 1;
__asm
{
_cont:
nop
}
printf("OK\n");
return 0;
}
////////////////////////////////////////////////////////////////
injcttope.c 注射器
///////////////////////////////////////////////////////////////
#include <windows.h>
#include <stdio.h>
#define MAX_SECTION_NUM 16
#define MAX_IMPDESC_NUM 64
HANDLE hHeap;
PIMAGE_DOS_HEADER pDosHeader;
PCHAR pDosStub;
DWORD dwDosStubSize;
DWORD dwDosStubOffset;
PIMAGE_NT_HEADERS pNtHeaders;
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_OPTIONAL_HEADER32 pOptHeader;
PIMAGE_SECTION_HEADER pSecHeaders;
PIMAGE_SECTION_HEADER pSecHeader[MAX_SECTION_NUM];
WORD wSecNum;
PBYTE pSecData[MAX_SECTION_NUM];
DWORD dwSecSize[MAX_SECTION_NUM];
DWORD dwFileSize;
BYTE InjCode[] = {};
DWORD dwSizeOfInjCode;
DWORD InjCode_orign_aoep_offset = 0x14F;
static DWORD PEAlign(DWORD dwTarNum,DWORD dwAlignTo)
{
return (((dwTarNum+dwAlignTo - 1) / dwAlignTo) * dwAlignTo);
}
static DWORD RVA2Ptr(DWORD dwBaseAddress, DWORD dwRva)
{
if ((dwBaseAddress != 0) && dwRva)
return (dwBaseAddress + dwRva);
else
return dwRva;
}
//----------------------------------------------------------------
static PIMAGE_SECTION_HEADER RVA2Section(DWORD dwRVA)
{
int i;
for(i = 0; i < wSecNum; i++)
{
if ( (dwRVA >= pSecHeader[i]->VirtualAddress)
&& (dwRVA <= (pSecHeader[i]->VirtualAddress + pSecHeader[i]->SizeOfRawData)) )
{
return ((PIMAGE_SECTION_HEADER)pSecHeader[i]);
}
}
return (NULL);
}
//----------------------------------------------------------------
static PIMAGE_SECTION_HEADER Offset2Section(DWORD dwOffset)
{
int i;
for(i = 0; i < wSecNum; i++)
{
if( (dwOffset>=pSecHeader[i]->PointerToRawData)
&& (dwOffset<(pSecHeader[i]->PointerToRawData + pSecHeader[i]->SizeOfRawData)))
{
return ((PIMAGE_SECTION_HEADER)pSecHeader[i]);
}
}
return NULL;
}
//================================================================
static DWORD RVA2Offset(DWORD dwRVA)
{
PIMAGE_SECTION_HEADER pSec;
pSec = RVA2Section(dwRVA);//ImageRvaToSection(pimage_nt_headers,Base,dwRVA);
if(pSec == NULL)
{
return 0;
}
return (dwRVA + (pSec->PointerToRawData) - (pSec->VirtualAddress));
}
//----------------------------------------------------------------
static DWORD Offset2RVA(DWORD dwOffset)
{
PIMAGE_SECTION_HEADER pSec;
pSec = Offset2Section(dwOffset);
if(pSec == NULL)
{
return (0);
}
return(dwOffset + (pSec->VirtualAddress) - (pSec->PointerToRawData));
}
/*
BOOL CopyPEFileToMem(LPCSTR szFilename)
This function is used to copy all parts of a PE file into memory,
including peheader, section table and data of all sections.
If this function succeeds, the return value will be TRUE. While it
fails, the return value will be FALSE.
*/
BOOL CopyPEFileToMem(LPCSTR szFilename)
{
HANDLE hFile;
PBYTE pMem;
DWORD dwBytesRead;
int i;
DWORD dwSecOff;
PIMAGE_NT_HEADERS pMemNtHeaders;
PIMAGE_SECTION_HEADER pMemSecHeaders;
hFile = CreateFile(szFilename, GENERIC_READ,
FILE_SHARE_READ, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile == INVALID_HANDLE_VALUE)
return FALSE;
dwFileSize = GetFileSize(hFile, 0);
pMem = (PBYTE)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwFileSize);
if(pMem == NULL)
{
CloseHandle(hFile);
return FALSE;
}
ReadFile(hFile, pMem, dwFileSize, &dwBytesRead, NULL);
CloseHandle(hFile);
//复制 DOS Header
pDosHeader = (PIMAGE_DOS_HEADER)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(IMAGE_DOS_HEADER));
if(pDosHeader == NULL)
{
CloseHandle(hFile);
return FALSE;
}
CopyMemory(pDosHeader, pMem, sizeof(IMAGE_DOS_HEADER));
// Copy DOS Stub Code
dwDosStubSize = pDosHeader->e_lfanew - sizeof(IMAGE_DOS_HEADER);
dwDosStubOffset = sizeof(IMAGE_DOS_HEADER);
pDosStub = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwDosStubSize);
if ((dwDosStubSize & 0x80000000) == 0x00000000)
{
CopyMemory(pDosStub, (const void *)(pMem + dwDosStubOffset), dwDosStubSize);
}
// Copy NT HEADERS
pMemNtHeaders = (PIMAGE_NT_HEADERS)(pMem + pDosHeader->e_lfanew);
pNtHeaders = (PIMAGE_NT_HEADERS)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(IMAGE_NT_HEADERS));
if(pNtHeaders == NULL)
{
CloseHandle(hFile);
return FALSE;
}
CopyMemory(pNtHeaders, pMemNtHeaders, sizeof(IMAGE_NT_HEADERS));
pOptHeader = &(pNtHeaders->OptionalHeader);
pFileHeader = &(pNtHeaders->FileHeader);
// Copy SectionTable
pMemSecHeaders = (PIMAGE_SECTION_HEADER) ((DWORD)pMemNtHeaders + sizeof(IMAGE_NT_HEADERS));
wSecNum = pFileHeader->NumberOfSections;
pSecHeaders = (PIMAGE_SECTION_HEADER)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, wSecNum * sizeof(IMAGE_SECTION_HEADER));
if(pSecHeaders == NULL)
{
CloseHandle(hFile);
return FALSE;
}
CopyMemory(pSecHeaders, pMemSecHeaders, wSecNum * sizeof(IMAGE_SECTION_HEADER));
for(i = 0; i < wSecNum; i++)
{
pSecHeader[i] = (PIMAGE_SECTION_HEADER) ((DWORD)pSecHeaders + i * sizeof(IMAGE_SECTION_HEADER));
}
// Copy Section
for(i = 0; i < wSecNum; i++)
{
dwSecOff = (DWORD)(pMem + pSecHeader[i]->PointerToRawData);
dwSecSize[i] = PEAlign(pSecHeader[i]->SizeOfRawData, pOptHeader->FileAlignment);
pSecData[i] = (PBYTE)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwSecSize[i]);
if (pSecData[i] == NULL)
{
CloseHandle(hFile);
return FALSE;
}
CopyMemory(pSecData[i], (PVOID)dwSecOff, dwSecSize[i]);
}
HeapFree(hHeap, 0, pMem);
return TRUE;
}
BOOL InjectCodeToMem()
{
DWORD roffset,rsize,voffset,vsize;
WORD wInjSecNo;
PDWORD p;
int i;
wInjSecNo = wSecNum;
if (wInjSecNo >= 64)
{
printf("Fatal Error: wInjSecNo(%d) >= 63\n", wInjSecNo);
return FALSE;
}
dwSizeOfInjCode = sizeof(InjCode);
p = (DWORD *)((DWORD)InjCode + InjCode_orign_aoep_offset);
*p =pOptHeader->AddressOfEntryPoint;
pSecHeader[wInjSecNo] = (PIMAGE_SECTION_HEADER)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(IMAGE_SECTION_HEADER));
rsize = PEAlign(dwSizeOfInjCode, pOptHeader->FileAlignment);
vsize = PEAlign(rsize, pOptHeader->SectionAlignment);
roffset = PEAlign(pSecHeader[wInjSecNo-1]->PointerToRawData + pSecHeader[wInjSecNo-1]->SizeOfRawData,
pOptHeader->FileAlignment);
voffset = PEAlign(pSecHeader[wInjSecNo-1]->VirtualAddress + pSecHeader[wInjSecNo-1]->Misc.VirtualSize,
pOptHeader->SectionAlignment);
pSecHeader[wInjSecNo]->PointerToRawData = roffset
pSecHeader[wInjSecNo]->VirtualAddress = voffset';
pSecHeader[wInjSecNo]->SizeOfRawData = rsize
pSecHeader[wInjSecNo]->Misc.VirtualSize = vsize;
pSecHeader[wInjSecNo]->Characteristics = 0x60000020;
pSecHeader[wInjSecNo]->Name[0] = '.';
pSecHeader[wInjSecNo]->Name[1] = 'm';
pSecHeader[wInjSecNo]->Name[2] = 'y';
pSecHeader[wInjSecNo]->Name[3] = 'c';
pSecHeader[wInjSecNo]->Name[4] = 'o';
pSecHeader[wInjSecNo]->Name[5] = 'd';
pSecHeader[wInjSecNo]->Name[6] = 'e';
pSecHeader[wInjSecNo]->Name[7] = '.';
pSecData[wInjSecNo] = (PBYTE)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, rsize);
dwSecSize[wInjSecNo] = pSecHeader[wInjSecNo]->SizeOfRawData;
CopyMemory(pSecData[wInjSecNo], InjCode, dwSizeOfInjCode);
wSecNum++;
pFileHeader->NumberOfSections = wSecNum;
pOptHeader->AddressOfEntryPoint=pSecHeader[wInjSecNo]->VirtualAddress;
for(i = 0;i< wSecNum; i++)
{
pSecHeader[i]->VirtualAddress =
PEAlign(pSecHeader[i]->VirtualAddress, pOptHeader->SectionAlignment);
pSecHeader[i]->Misc.VirtualSize =
PEAlign(pSecHeader[i]->Misc.VirtualSize, pOptHeader->SectionAlignment);
pSecHeader[i]->PointerToRawData =
PEAlign(pSecHeader[i]->PointerToRawData, pOptHeader->FileAlignment);
pSecHeader[i]->SizeOfRawData =
PEAlign(pSecHeader[i]->SizeOfRawData, pOptHeader->FileAlignment);
}
pOptHeader->SizeOfImage = pSecHeader[wSecNum-1]->VirtualAddress + pSecHeader[wSecNum-1]->Misc.VirtualSize;
dwFileSize = pSecHeader[wSecNum-1]->PointerToRawData + pSecHeader[wSecNum-1]->SizeOfRawData;
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress=0;
pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size=0;
// pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress=0;
// pOptHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size=0;
return TRUE;
}
BOOL SaveMemToPEFile(LPCSTR szFileName)
{
DWORD dwBytesWritten = 0;
PBYTE pMem = NULL;
PBYTE pMemSecHeaders;
HANDLE hFile;
int i;
//----------------------------------------
pMem = (PBYTE) HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwFileSize);
if(pMem == NULL)
{
return FALSE;
}
//----------------------------------------
CopyMemory(pMem, pDosHeader, sizeof(IMAGE_DOS_HEADER));
if((dwDosStubSize & 0x80000000) == 0x00000000)
{
CopyMemory(pMem + dwDosStubOffset, pDosStub, dwDosStubSize);
}
CopyMemory(pMem + pDosHeader->e_lfanew,
pNtHeaders,
sizeof(IMAGE_NT_HEADERS));
pMemSecHeaders = pMem + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS);
//----------------------------------------
for (i = 0; i < wSecNum; i++)
{
CopyMemory(pMemSecHeaders + i * sizeof(IMAGE_SECTION_HEADER),
pSecHeader[i], sizeof(IMAGE_SECTION_HEADER));
}
for (i = 0; i < wSecNum; i++)
{
CopyMemory(pMem + pSecHeader[i]->PointerToRawData,
pSecData[i],
pSecHeader[i]->SizeOfRawData);
}
hFile = CreateFile(szFileName,
GENERIC_WRITE,
FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
printf("Fatal Error: Create file (%s) failed\n", szFileName);
return FALSE;
}
// ----- WRITE FILE MEMORY TO DISK -----
SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
WriteFile(hFile, pMem, dwFileSize, &dwBytesWritten, NULL);
// ------ FORCE CALCULATED FILE SIZE ------
SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
CloseHandle(hFile);
//----------------------------------------
HeapFree(hHeap, 0, pMem);
return TRUE;
}
void OutputPEInMem()
{
int i;
printf("BaseAddress : 0x%08X\n", pOptHeader->ImageBase);
printf("BaseOfCode : 0x%08X\n", pOptHeader->BaseOfCode);
printf("SizeOfCode : 0x%08X\n", pOptHeader->SizeOfCode);
printf("BaseOfData : 0x%08X\n", pOptHeader->BaseOfData);
printf("AddressOfEntryPoint : 0x%08X\n", pOptHeader->AddressOfEntryPoint);
printf("NumberOfSections : 0x%08X\n", pFileHeader->NumberOfSections);
printf("Number Of Sections: %d\n", wSecNum);
for(i = 0; i < wSecNum; i++)
{
printf("Section #%d\n",i);
printf(" Name: %c%c%c%c%c%c%c%c\n", pSecHeader[i]->Name[0],pSecHeader[i]->Name[1],
pSecHeader[i]->Name[2],pSecHeader[i]->Name[3],pSecHeader[i]->Name[4],pSecHeader[i]->Name[5],
pSecHeader[i]->Name[6],pSecHeader[i]->Name[7]);
printf(" Virtual Size : 0x%08X\n", pSecHeader[i]->Misc.VirtualSize);
printf(" Virtual Address : 0x%08X\n", pSecHeader[i]->VirtualAddress);
printf(" RawData Size : 0x%08X\n", pSecHeader[i]->SizeOfRawData);
printf(" RawData Offset : 0x%08X\n", pSecHeader[i]->PointerToRawData);
printf(" Characteristics : 0x%08X\n", pSecHeader[i]->Characteristics);
}
return;
}
BOOL OutputSection(WORD wSecNo)
{
int i;
PBYTE p;
if (wSecNo >= wSecNum)
{
printf("Error: wSecNo out of bound (%d >= %d)\n", wSecNo, wSecNum);
return FALSE;
}
printf("Dump Data of Section #%d\n", wSecNo);
if (pSecData[wSecNo] == NULL)
{
printf("Fatal Error in OutputSection()\n");
return FALSE;
}
for (p = pSecData[wSecNo]; p < pSecData[wSecNo] + dwSecSize[wSecNo]; p++)
{
if (! ((DWORD)p & 0x00000007)) printf("\n");
printf("0x%02X ", *p);
}
printf("\n");
return TRUE;
}
int main()
{
LPCSTR szFileName = "winmine.exe";
LPCSTR szInjFileName = "winmine_inj.exe";
hHeap = GetProcessHeap();
printf("-> Open file %s ...", szFileName);
if (! CopyPEFileToMem(szFileName))
{
printf("failed\n");
return 1;
}
printf("ok\n");
printf("-> Inject code ...");
if (! InjectCodeToMem())
{
printf("failed\n");
return 1;
}
printf("ok\n");
OutputPEInMem();
OutputSection(3);
printf("-> Save file %s ...", szInjFileName);
if (! SaveMemToPEFile(szInjFileName))
{
printf("failed\n");
return 1;
}
printf("ok\n");
return 0;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: