성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] VT sequences to "CONOUT$" vs. STD_O...
[정성태] NetCoreDbg is a managed code debugg...
[정성태] Evaluating tail call elimination in...
[정성태] What’s new in System.Text.Json in ....
[정성태] What's new in .NET 9: Cryptography ...
[정성태] 아... 제시해 주신 "https://akrzemi1.wordp...
[정성태] 다시 질문을 정리할 필요가 있을 것 같습니다. 제가 본문에...
[이승준] 완전히 잘못 짚었습니다. 댓글 지우고 싶네요. 검색을 해보...
[정성태] 우선 답글 감사합니다. ^^ 그런데, 사실 저 예제는 (g...
[이승준] 수정이 안되어서... byteArray는 BYTE* 타입입니다...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법</h1> <p> EXE 파일에는 DLL과는 다르게 운영체제가 로딩 후 호출을 하는 <a target='tab' href='https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol?view=vs-2017'>진입점(EntryPoint)</a>이 있습니다. Visual C++의 경우, 컴파일하면 응용 프로그램의 종류에 따라 진입점 위치를 CRT(msvcr dll)에 포함하고 있는 다음의 함수로 설정해 둡니다.<br /> <br /> <ul> <li>콘솔 프로그램: mainCRTStartup 또는 wmainCRTStartup</li> <li>윈도우 프로그램: WinMainCRTStartup 또는 wWinMainCRTStartup</li> <li>DLL: _DllMainCRTStartup</li> </ul> <br /> 그렇다면, 우리도 EXE 프로그램을 로딩해 EntryPoint 함수를 직접 부르는 것이 가능하지 않을까요? ^^<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이것을 테스트하기 위해 C/C++ 콘솔 프로젝트 2개를 준비합니다.<br /> <br /> <ul> <li>ConsoleApplication1: exe_entry EXE를 로드해 EntryPoint를 직접 호출하는 콘솔 프로젝트</li> <li>exe_entry: DLL처럼 로드되는 EXE 콘솔 프로젝트</li> </ul> <br /> 그리고 exe_entry의 경우 다음과 같이 간단하게 코딩을 하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > int main() { return 5; } </pre> <br /> 아래의 글에서 설명한 방법에 따라 CRT 의존성을 제거합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1437'>http://www.sysnet.pe.kr/2/0/1437</a> </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Configuration Properties C/C++ Code Generation <span style='color: blue; font-weight: bold'>Basic Runtime Checks: Default Security Check: No (/GS-)</span> Configuration Properties Linker Advanced <span style='color: blue; font-weight: bold'>Entry Point: main</span> </pre> <br /> 그다음, ConsoleApplication1에서는 다음과 같은 식으로 <a target='tab' href='https://devblogs.microsoft.com/oldnewthing/20240208-00/?p=109374'>LoadLibrary</a>를 이용해 exe_entry.exe를 로드하면 PE 헤더 정보를 이용해 EntryPoint를 찾아 실행할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include <Windows.h> #include <stdio.h> #include <DbgHelp.h> #pragma comment(lib, "dbghelp.lib") typedef int(*MainFunc)(void); int main() { LPVOID hInstance = ::LoadLibrary(L"exe_entry.exe"); PIMAGE_NT_HEADERS ntHeader = ImageNtHeader(hInstance); MainFunc pfnMain = (MainFunc)(DWORD_PTR)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)hInstance); printf("result %d\n", <span style='color: blue; font-weight: bold'>pfnMain()</span>); } /* 출력 결과 result: 5 */ </pre> <br /> <hr style='width: 50%' /><br /> <br /> 일단, CRT가 없는 exe_entry 실행 파일에 대해서는 성공했으니 이제 일반적인 CRT 의존성이 있는 EXE의 EntryPoint에 대고 호출해 보면 어떨까요? 아쉽게도 이번에는 실행하는 경우 다음과 같은 식의 AV 예외가 발생합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Exception thrown at 0x0001B7AC in ConsoleApplication1.exe: 0xC0000005: Access violation executing location 0x0001B7AC. occurred </pre> <br /> Disassembly 창을 이용해 디버깅으로 추적해 들어가 보면, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > _mainCRTStartup: 009C132F E9 FC 0B 00 00 <span style='color: blue; font-weight: bold'>jmp mainCRTStartup (<span style='color: blue; font-weight: bold'>09C1F30h</span>)</span> </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp extern "C" int mainCRTStartup() { <span style='color: blue; font-weight: bold'>009C1F30</span> 55 push ebp 009C1F31 8B EC mov ebp,esp return __scrt_common_main(); 009C1F33 E8 58 FC FF FF <span style='color: blue; font-weight: bold'>call __scrt_common_main (<span style='color: blue; font-weight: bold'>09C1B90h</span>) </span> } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl ---- static __forceinline int __cdecl __scrt_common_main() { <span style='color: blue; font-weight: bold'>009C1B90</span> 55 push ebp 009C1B91 8B EC mov ebp,esp // The /GS security cookie must be initialized before any exception handling // targeting the current image is registered. No function using exception // handling can be called in the current image until after this call: __security_init_cookie(); 009C1B93 E8 24 F7 FF FF <span style='color: blue; font-weight: bold'>call ___security_init_cookie (<span style='color: blue; font-weight: bold'>09C12BCh</span>) </span> return __scrt_common_main_seh(); 009C1B98 E8 13 00 00 00 call __scrt_common_main_seh (09C1BB0h) } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ___security_init_cookie: <span style='color: blue; font-weight: bold'>009C12BC</span> E9 AF 1E 00 00 <span style='color: blue; font-weight: bold'>jmp __security_init_cookie (09C3170h) </span> </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\gs\gs_support.c void __cdecl __security_init_cookie(void) { <span style='color: blue; font-weight: bold'>009C3170</span> 55 push ebp 009C3171 8B EC mov ebp,esp 009C3173 51 push ecx UINT_PTR cookie; // ...[생략]... /* * Initialize the global cookie with an unpredictable value which is * different for each module in a process. */ cookie = __get_entropy(); 009C319C E8 3F FF FF FF <span style='color: blue; font-weight: bold'>call __get_entropy (09C30E0h) </span> 009C31A1 89 45 FC mov dword ptr [cookie],eax // ...[생략]... } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\gs\gs_support.c static UINT_PTR __get_entropy(void) { <span style='color: blue; font-weight: bold'>009C30E0</span> 55 push ebp 009C30E1 8B EC mov ebp,esp 009C30E3 83 EC 14 sub esp,14h UINT_PTR cookie; FT systime = { 0 }; 009C30E6 33 C0 xor eax,eax 009C30E8 89 45 F4 mov dword ptr [systime],eax 009C30EB 89 45 F8 mov dword ptr [ebp-8],eax LARGE_INTEGER perfctr; GetSystemTimeAsFileTime(&systime.ft_struct); 009C30EE 8D 4D F4 lea ecx,[systime] 009C30F1 51 push ecx 009C30F2 FF 15 3C B0 9C 00 <span style='color: blue; font-weight: bold'>call dword ptr [__imp__GetSystemTimeAsFileTime@4 (09CB03Ch)] // <== 예외 발생</span> #if defined (_WIN64) cookie = systime.ft_scalar; // ...[생략]... } </pre> <br /> AV 예외가 Win32 API인 GetSystemTimeAsFileTime 함수를 호출하면서 발생하는 것을 확인할 수 있습니다. 이때의 IAT에서 __imp__GetSystemTimeAsFileTime의 주소(09CB03Ch)에 있는 값은,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0x009CB03C ac b7 01 00 .... </pre> <br /> 0x0001b7ac이고, 바로 이 값이 AV 예외 발생 메시지 창에 나온 그 주소입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Exception thrown at <span style='color: blue; font-weight: bold'>0x0001B7AC</span> in ConsoleApplication1.exe: 0xC0000005: Access violation executing location 0x0001B7AC. occurred </pre> <br /> 실제로 Disassembly 창을 이용해 0x0001B7AC 위치의 코드를 보면 <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0001B7AC ?? ?? ?? 0001B7AD ?? ?? ?? 0001B7AE ?? ?? ?? 0001B7AF ?? ?? ?? 0001B7B0 ?? ?? ?? </pre> <br /> 해당 가상 주소는 commit된 적이 없는 빈 주소 공간입니다. 결국, Win32 API 함수의 IAT 주솟값이 엉뚱한 값을 가지고 있다는 것이 문제입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이런 현상이 발생하는 원인은 간단합니다. <a target='tab' href='https://devblogs.microsoft.com/oldnewthing/20230922-00/?p=108813'>윈도우는 DLL 파일에 대해서만 해당 PE 파일의 IAT 주솟값을 패치시켜주기 때문에 EXE 모듈을 로드했을 때는 (DONT_RESOLVE_DLL_REFERENCES 옵션을 적용해 DLL을 LoadLibary한 것처럼) 쓰레기로 채워져 있던 것</a>입니다. 이 문제를 간단하게 해결하고 싶다면 EXE 파일의 PE 헤더에 Characteristics 값을,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // winnt.h typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; <span style='color: blue; font-weight: bold'>WORD Characteristics;</span> } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; </pre> <br /> "DLL" 속성도 함께 추가하면 됩니다. 이를 위해 dnSpy.exe 도구를 이용하면 다음과 같이 간단하게 "DLL" 속성을 추가하고 저장할 수 있습니다.<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='call_exe_entry_point_1.png' src='/SysWebRes/bbs/call_exe_entry_point_1.png' /><br /> <br /> 그림에 보이는 "Characteristics" 값이 0x2102인데 다음의 상수 조합에 해당합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine. #define IMAGE_FILE_DLL 0x2000 // File is a DLL. </pre> <br /> 이제 다시 CRT 링크된 EXE의 EntryPoint 함수를 LoadLibrary를 이용해 호출하면 DLL을 로드하는 경우와 동일하게 IAT 패칭을 운영체제가 대신 수행해 주기 때문에 EntryPoint 함수 호출이 정상적으로 실행되는 것을 확인할 수 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 물론 dnSpy.exe를 이용하지 않고 직접 IMAGE_FILE_DLL 속성을 PE 포맷에 따라 설정하도록 만드는 프로그램을 제작하는 것도 가능합니다. <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 아래의 소스 코드를 살짝 변경시켰습니다. // setdllcharacteristics // <a target='tab' href='https://blog.didierstevens.com/2010/10/17/setdllcharacteristics/'>https://blog.didierstevens.com/2010/10/17/setdllcharacteristics/</a> #include <stdio.h> #include <string.h> #define PEINDEX_POSITION 0x3C #define CHARACTERISTICS_OFFSET 0x16 int main(int argc, char *argv[]) { FILE *fPEFile; char *pszFilename; unsigned int uiPEHEADERIndex; unsigned short int wDLLCHARACTERISTICS; int iIter; if (1 == argc) { fprintf(stderr, "Usage: setcharacteristics file\n"); return -1; } pszFilename = argv[argc-1]; if ((fPEFile = fopen(pszFilename, "r+b")) == NULL) { fprintf(stderr, "Error: opening file %s\n", pszFilename); return -1; } if (fseek(fPEFile, PEINDEX_POSITION, 0) != 0) { fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, PEINDEX_POSITION); fclose(fPEFile); return -1; } if (fread(&uiPEHEADERIndex, sizeof(uiPEHEADERIndex), 1, fPEFile) != 1) { fprintf(stderr, "Error: reading file %s\n", pszFilename); fclose(fPEFile); return -1; } if (fseek(fPEFile, uiPEHEADERIndex + CHARACTERISTICS_OFFSET, 0) != 0) { fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, uiPEHEADERIndex + CHARACTERISTICS_OFFSET); fclose(fPEFile); return -1; } if (fread(&wDLLCHARACTERISTICS, sizeof(wDLLCHARACTERISTICS), 1, fPEFile) != 1) { fprintf(stderr, "Error: reading file %s\n", pszFilename); fclose(fPEFile); return -1; } printf("Original CHARACTERISTICS = 0x%04X\n", wDLLCHARACTERISTICS); wDLLCHARACTERISTICS |= 0x2000; if (fseek(fPEFile, uiPEHEADERIndex + CHARACTERISTICS_OFFSET, 0) != 0) { fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, uiPEHEADERIndex + CHARACTERISTICS_OFFSET); fclose(fPEFile); return -1; } if (fwrite(&wDLLCHARACTERISTICS, sizeof(wDLLCHARACTERISTICS), 1, fPEFile) != 1) { fprintf(stderr, "Error: writing file %s\n", pszFilename); fclose(fPEFile); return -1; } printf("Updated CHARACTERISTICS = 0x%04X\n", wDLLCHARACTERISTICS); fclose(fPEFile); return 0; } </pre> <br /> Visual C++로 컴파일한 후,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C:\temp\setcharacteristics> <span style='color: blue; font-weight: bold'>cl setcharacteristics.c</span> Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27027.1 for x86 Copyright (C) Microsoft Corporation. All rights reserved. setcharacteristics.c Microsoft (R) Incremental Linker Version 14.16.27027.1 Copyright (C) Microsoft Corporation. All rights reserved. /out:setcharacteristics.exe setcharacteristics.obj </pre> <br /> EXE 프로젝트의 "Build Events" / "Post-Build Event"에 다음과 같은 식으로 실행해 두도록 만들면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > $(SolutionDir)setcharacteristics.exe $(TargetPath) </pre> <br /> <hr style='width: 50%' /><br /> <br /> IMAGE_FILE_DLL 속성을 추가하고 싶지 않다면, 반대로 LoadLibrary를 호출한 측에서 IAT 주소를 패치하는 작업을 직접 수행해도 됩니다. 이 작업 역시 다음의 글에서 ^^ 친절하게 이미 다 설명하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Load EXE as DLL: Mission Possible ; <a target='tab' href='https://www.codeproject.com/Articles/1045674/Load-EXE-as-DLL-Mission-Possible'>https://www.codeproject.com/Articles/1045674/Load-EXE-as-DLL-Mission-Possible</a> </pre> <br /> 따라서 다음과 같이 코드를 추가해 주면,<br /> <br /> <pre style='height: 400px; margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > #include <Windows.h> #include <stdio.h> #include <DbgHelp.h> #pragma comment(lib, "dbghelp.lib") typedef int(*MainFunc)(void); void PatchIAT(HINSTANCE h); int main() { // LPVOID hInstance = ::LoadLibrary(L"exe_entry.exe"); // EXE without CRT LPVOID hInstance = ::LoadLibrary(L"exe_dll_entry.exe"); // EXE with CRT <span style='color: blue; font-weight: bold'>PatchIAT((HINSTANCE)hInstance);</span> PIMAGE_NT_HEADERS ntHeader = ImageNtHeader(hInstance); MainFunc pfnMain = (MainFunc)(DWORD_PTR)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)hInstance); printf("result %d\n", pfnMain()); } // https://www.codeproject.com/Articles/1045674/Load-EXE-as-DLL-Mission-Possible void PatchIAT(HINSTANCE h) { // Find the IAT size DWORD ulsize = 0; PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(h, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulsize); if (!pImportDesc) return; // Loop names for (; pImportDesc->Name; pImportDesc++) { PSTR pszModName = (PSTR)((PBYTE)h + pImportDesc->Name); if (!pszModName) break; HINSTANCE hImportDLL = LoadLibraryA(pszModName); if (!hImportDLL) { // ... (error) } // Get caller's import address table (IAT) for the callee's functions PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)h + pImportDesc->FirstThunk); // Replace current function address with new function address for (; pThunk->u1.Function; pThunk++) { FARPROC pfnNew = 0; size_t rva = 0; #ifdef _WIN64 if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) #else if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32) #endif { // Ordinal #ifdef _WIN64 size_t ord = IMAGE_ORDINAL64(pThunk->u1.Ordinal); #else size_t ord = IMAGE_ORDINAL32(pThunk->u1.Ordinal); #endif PROC* ppfn = (PROC*)& pThunk->u1.Function; if (!ppfn) { // ... (error) } rva = (size_t)pThunk; char fe[100] = { 0 }; sprintf_s(fe, 100, "#%u", ord); pfnNew = GetProcAddress(hImportDLL, (LPCSTR)ord); if (!pfnNew) { // ... (error) } } else { // Get the address of the function address PROC* ppfn = (PROC*)& pThunk->u1.Function; if (!ppfn) { // ... (error) } rva = (size_t)pThunk; PSTR fName = (PSTR)h; fName += pThunk->u1.Function; fName += 2; if (!fName) break; pfnNew = GetProcAddress(hImportDLL, fName); if (!pfnNew) { // ... (error) } } // Patch it now... auto hp = GetCurrentProcess(); if (!WriteProcessMemory(hp, (LPVOID*)rva, &pfnNew, sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError())) { DWORD dwOldProtect; if (VirtualProtect((LPVOID)rva, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect)) { if (!WriteProcessMemory(GetCurrentProcess(), (LPVOID*)rva, &pfnNew, sizeof(pfnNew), NULL)) { // ... (error) } if (!VirtualProtect((LPVOID)rva, sizeof(pfnNew), dwOldProtect, &dwOldProtect)) { // ... (error) } } } } } } </pre> <br /> 역시 아무런 문제없이 EntryPoint 함수가 실행됩니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1431&boardid=331301885'>첨부 파일은 이 글의 예제 프로젝트를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
9564
(왼쪽의 숫자를 입력해야 합니다.)