http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&keywords=%C0%D0%C0%BB%B0%C5%B8%AE%3B%B5%F0%BA%A7%B7%CE%C6%DB+%C7%C3%B7%AF%BD%BA&page=16&wr_id=35398
Code injection
http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&keywords=%C0%D0%C0%BB%B0%C5%B8%AE%3B%B5%F0%BA%A7%B7%CE%C6%DB+%C7%C3%B7%AF%BD%BA&page=14&wr_id=35688
이승원 reversecore@gmail.com, http://www.rever secore.com|관심 분야는 RCE(Reverse Code Engineering)이고, 리버싱 기술을 다양한 IT 분야에 적용시키는 것에 관심이 많다. 현재 AhnLab에서 악성 코드 분석과 자동화 시스템 개발 업무를 맡고 있다.
Code Injection 개념
Code Injection은 상대방 프로세스에 독립 실행코드를 삽입한 후 실행시키는 기법이다. 일반적으로 CreateRemoteThread() API를 이용해 원격 스레드 형태로 실행시키기에 Thread Injection 이라고도 불린다. <그림 1>은 Code Injection의 개념을 잘 보여주고 있다.
Code Injection은 상대방 프로세스에 독립 실행코드를 삽입한 후 실행시키는 기법이다. 일반적으로 CreateRemoteThread() API를 이용해 원격 스레드 형태로 실행시키기에 Thread Injection 이라고도 불린다. <그림 1>은 Code Injection의 개념을 잘 보여주고 있다.
<그림 1> Code Injection의 개념
인젝션 대상이 되는 target.exe 프로세스에 코드와 데이터를 삽입한다. 이때 코드형식은 스레드 프로시져(Thread Procedure) 형식으로 해주고, 코드에서 사용되는 데이터는 스레드의 파라미터로 전달해준다. 즉, 코드와 데이터에 각각 인젝션 시키는 것으로 개념은 간단하지만 실제 구현하면서 주의해야 할 내용이 있다.
DLL Injection vs Code Injection
윈도우 메시지 박스를 출력하는 코드를 이용해서 설명한다(<리스트 1> 참조).
윈도우 메시지 박스를 출력하는 코드를 이용해서 설명한다(<리스트 1> 참조).
DWORD WINAPI ThreadProc(LPVOID lParam) { MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK); return 0; } |
<리스트 1> ThreadProc()
DLL Injection 기법은 DLL 파일형태로 만든 후 다른 프로세스에 인젝션 시키면 된다(첨부된 ThreadProcTest.dll 파일 참고). OllyDbg를 실행시켜 위 ThreadProc()코드 주소영역(10001000)을 살펴보면 <화면 1>과 같다.
<화면 1> OllyDbg에서 ThreadProc()확인
<화면 1>의 코드에서 사용되는 주소를 주목하기 바란다. 먼저 10001002 주소의 PUSH 10009290 명령어와 그 밑의 PUSH 1000929C 명령어에 사용된 10009290, 1000929C 주소를 보자. 이 명령어들은 MessageBoxA() API에 사용될 문자열 주소(“ReverseCore”, “www.reversecore.com”)를 스택에 저장시킨다(<화면 2> 참조).
<화면 2> 문자열 데이터 영역
<화면 2>를 보면 문자열들의 주소(10009290, 1000929C)는 DLL의 데이터 섹션에 위치한다. 이번에는 <화면 1>에서 1000100E 주소의 CALL DWORD PTR DS:[100080F0] 명령어에 사용된 100080F0 주소를 살펴보자(이 CALL 명령어는 user32!MessageBoxA() API 호출명령이다).
<화면 3> IAT 영역
<화면 3>에서 100080F0 주소는 DLL의 IAT(Import Address Table) 영역임을 알 수 있다. 100080F0 주소값은 7793EA71로, USER32.MessageBoxA() API의 시작주소다.
이와 같이 DLL의 코드에서 사용되는 모든 데이터는 DLL의 데이터 영역에 위치한다. 따라서 DLL Injection 기법으로 DLL을 통째로 상대방 프로세스 메모리에 삽입시키면 코드와 데이터가 같이 메모리에 존재하기 때문에 코드를 정상적으로 실행시킬 수 있다.
Code Injection은 필요한 코드(<화면 1>)를 인젝션 시키는 것이다. 하지만 코드에서 사용된 데이터(<화면 2>, <화면 3>)도 같이 인젝션 시켜야 정상적으로 코드가 실행된다(또 인젝션 된 데이터의 주소를 코드에서 잘 알아볼 수 있도록 프로그래밍 해야한다). 이런 이유로 DLL Injection 기법 보다 고려사항이 좀 더 많다.
이와 같이 DLL의 코드에서 사용되는 모든 데이터는 DLL의 데이터 영역에 위치한다. 따라서 DLL Injection 기법으로 DLL을 통째로 상대방 프로세스 메모리에 삽입시키면 코드와 데이터가 같이 메모리에 존재하기 때문에 코드를 정상적으로 실행시킬 수 있다.
Code Injection은 필요한 코드(<화면 1>)를 인젝션 시키는 것이다. 하지만 코드에서 사용된 데이터(<화면 2>, <화면 3>)도 같이 인젝션 시켜야 정상적으로 코드가 실행된다(또 인젝션 된 데이터의 주소를 코드에서 잘 알아볼 수 있도록 프로그래밍 해야한다). 이런 이유로 DLL Injection 기법 보다 고려사항이 좀 더 많다.
Code Injection을 사용하는 이유
Code Injection은 DLL Injection과 기능은 비슷하지만 고려할 사항이 더 많아 사용이 불편하다고 느껴질 수 있다. 그렇다면 Code Injection의 장점은 무엇일까?
우선 아주 작은 크기의 코드와 데이터를 인젝션 할 때는 DLL로 만들어 인젝션 시킬 필요가 없다. 간단히 Code Injection으로 구현하면 DLL Injection과 같은 기능을 제공하면서 메모리는 훨씬 적게 차지한다. 또 DLL Injection은 해당 프로세스 메모리에 흔적을 남기기 때문에 인젝션 여부를 쉽게 파악 수 있다. Code Injection은 쉽게 흔적이 남지 않는다(물론 이 역시 알아낼 수 있는 방법이 있다). 이런 이유로 악성코드에서 Code Injection 기법이 많이 사용된다.
그 외에 별도의 DLL 파일 없이 Code Injector 프로그램만 있으면 된다. 또한 삽입되는 코드를 어셈블리 언어로 구현하면 아주 컴팩트하고 효율적인 코드를 생성할 수 있다. 따라서 DLL Injection은 규모가 크고 복잡한 일을 수행할 때 사용하고, Code Injection은 규모가 작고 간단한 일을 수행할 때 사용한다.
Code Injection은 DLL Injection과 기능은 비슷하지만 고려할 사항이 더 많아 사용이 불편하다고 느껴질 수 있다. 그렇다면 Code Injection의 장점은 무엇일까?
우선 아주 작은 크기의 코드와 데이터를 인젝션 할 때는 DLL로 만들어 인젝션 시킬 필요가 없다. 간단히 Code Injection으로 구현하면 DLL Injection과 같은 기능을 제공하면서 메모리는 훨씬 적게 차지한다. 또 DLL Injection은 해당 프로세스 메모리에 흔적을 남기기 때문에 인젝션 여부를 쉽게 파악 수 있다. Code Injection은 쉽게 흔적이 남지 않는다(물론 이 역시 알아낼 수 있는 방법이 있다). 이런 이유로 악성코드에서 Code Injection 기법이 많이 사용된다.
그 외에 별도의 DLL 파일 없이 Code Injector 프로그램만 있으면 된다. 또한 삽입되는 코드를 어셈블리 언어로 구현하면 아주 컴팩트하고 효율적인 코드를 생성할 수 있다. 따라서 DLL Injection은 규모가 크고 복잡한 일을 수행할 때 사용하고, Code Injection은 규모가 작고 간단한 일을 수행할 때 사용한다.
실습예제(CodeInjection.exe)
실습예제는 notepad.exe 프로세스에 간단한 코드를 인젝션 시켜 메시지 박스를 출력하는 내용이다.
1. notepad.exe 실행
notepad.exe를 실행 시킨 후 Process Explorer를 이용해 notepad.exe 프로세스의 PID를 확인하자.
실습예제는 notepad.exe 프로세스에 간단한 코드를 인젝션 시켜 메시지 박스를 출력하는 내용이다.
1. notepad.exe 실행
notepad.exe를 실행 시킨 후 Process Explorer를 이용해 notepad.exe 프로세스의 PID를 확인하자.
<화면 4> Process Explorer
필자의 테스트 환경에서 notepad.exe의 PID는 1896이다. 각자 환경에서 PID를 살펴보자(<화면 4> 참조).
2. CodeInjection.exe 실행
첨부된 CodeInjection.exe 파일을 실행시킨다. 이때 실행 파라미터로 앞에서 구한 notepad.exe의 PID값을 입력한다.
2. CodeInjection.exe 실행
첨부된 CodeInjection.exe 파일을 실행시킨다. 이때 실행 파라미터로 앞에서 구한 notepad.exe의 PID값을 입력한다.
<화면 5> Codeinjection.exe 실행
3. 메시지 박스 확인
CodeInjection.exe 프로그램이 실행되자마자 notepad.exe에서 메시지 박스가 출력된다(<화면 6> 참조).
CodeInjection.exe 프로그램이 실행되자마자 notepad.exe에서 메시지 박스가 출력된다(<화면 6> 참조).
<화면 6> notepad.exe에 나타난 메시지 박스
메시지 박스가 notepad.exe 윈도우의 밑에 깔려 있으므로 확인 할 때 주의한다. 이제 해당 소스코드를 보면서 어떻게 구현됐는지 확인해보자.
CodeInjection.cpp
아래 소개될 코드들은 설명의 편의를 위해 에러처리 부분을 생략했다. 완전한 코드는 첨부된 CodeInjection.cpp 파일을 참고하기 바란다.
아래 소개될 코드들은 설명의 편의를 위해 에러처리 부분을 생략했다. 완전한 코드는 첨부된 CodeInjection.cpp 파일을 참고하기 바란다.
필자 메모 CodeInjection.cpp는 Visual C++ 2008 Express Edition으로 개발했으며 Windows 7 32bit 환경에서 테스트 했다. 또한 Visual C++의 코드 최적화 기능을 사용하지 않고 빌드했다(/Od). |
main()
먼저 main() 함수를 살펴보자(<리스트 2> 참조).
int main(int argc, char *argv[]) { DWORD dwPID = 0; if( argc != 2 ) { printf("\n USAGE : %s return 1; } // code injection dwPID = (DWORD)atol(argv[1]); InjectCode(dwPID); return 0; } |
<리스트 2> main() 함수
main() 함수의 역할은 InjectCode() 함수를 호출하는 것이다. 이때 함수 파라미터로 상대방 프로세스의 PID 값을 넘겨준다.
ThreadProc()
이제 상대방 프로세스에 인젝션 시킬 코드(스레드 함수)를 살펴보자(<리스트 3> 참조).
ThreadProc()
이제 상대방 프로세스에 인젝션 시킬 코드(스레드 함수)를 살펴보자(<리스트 3> 참조).
// Thread Parameter typedef struct _THREAD_PARAM { FARPROC pFunc[2]; char szBuf[4][128]; } THREAD_PARAM, *PTHREAD_PARAM; // LoadLibraryA() typedef HMODULE (WINAPI *PFLOADLIBRARYA) ( LPCSTR lpLibFileName ); // GetProcAddress() typedef FARPROC (WINAPI *PFGETPROCADDRESS) ( HMODULE hModule, LPCSTR lpProcName ); // MessageBoxA() typedef int (WINAPI *PFMESSAGEBOXA) ( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType ); // Thread Procedure DWORD WINAPI ThreadProc(LPVOID lParam) { PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam; HMODULE hMod = NULL; FARPROC pFunc = NULL; // LoadLibrary("user32.dll") // pParam->pFunc[0] = kernel32!LoadLibraryA() // pParam->szBuf[0] = "user32.dll" hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); // GetProcAddress("MessageBoxA") // pParam->pFunc[1] = kernel32!GetProcAddress() // pParam->szBuf[1] = "MessageBoxA" pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); // MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK) // pParam->pFunc[1] = kernel32!GetProcAddress() // pParam->szBuf[1] = "MessageBoxA" ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK); return 0; } |
<리스트 3> ThreadProc() 함수
<리스트 3>에서 실제로 인젝션 되는 부분은 ThreadProc() 함수 자체로 그 위의 typedef문은 C 언어문법을 위한 것이므로 인젝션 시킬 필요가 없다. ThreadProc() 코드는 함수 포인터를 많이 사용해서 다소 복잡해 보이지만 실제로 <리스트 4>와 같이 간단하다.
hMod = LoadLibraryA("user32.dll"); pFunc = GetProcAddress(hMod, "MessageBoxA"); pFunc("www.reversecore.com", "ReverseCore"); |
<리스트 4> ThreadProc() 코드 개요
<리스트 3>의 주석을 참조하면 ThreadProc() 코드는 쉽게 이해할 수 있다. 중요한 것은 바로 ThreadProc() 코드의 개념이다. Code Injection 기법의 핵심이 바로 독립 실행코드를 인젝션 시키는 것이다. 그러기 위해서 코드와 (코드에서 참조하는)데이터를 같이 인젝션 시켜야 한다. 그리고 인젝션 시키는 코드에서 역시 인젝션 시킨 데이터를 정확히 참조할 수 있도록 프로그래밍 해야 한다.
<리스트 3>의 ThreadProc() 함수를 보면 API를 직접 호출하지 않고 문자열도 직접 정의해서 사용하지 않는다. 전부 스레드 파라미터로 넘어온 THREAD_PARAM 구조체에서 가져다 사용한다. 이 조건을 만족시키기 위해서 <리스트 3>의 ThreadProc() 함수는 THREAD_PARAM 구조체를 이용해, 2개의 API 주소와 4개의 문자열 데이터를 받아들이고 있다. 2개의 API는 바로 ‘LoadLibraryA()’와 ‘GetProcAddress()’로, 이 2개의 API만 있으면 모든 라이브러리의 함수를 호출 할 수 있다. <리스트 3>의 내용을 디버거로 살펴보면 <화면 7>과 같다.
<리스트 3>의 ThreadProc() 함수를 보면 API를 직접 호출하지 않고 문자열도 직접 정의해서 사용하지 않는다. 전부 스레드 파라미터로 넘어온 THREAD_PARAM 구조체에서 가져다 사용한다. 이 조건을 만족시키기 위해서 <리스트 3>의 ThreadProc() 함수는 THREAD_PARAM 구조체를 이용해, 2개의 API 주소와 4개의 문자열 데이터를 받아들이고 있다. 2개의 API는 바로 ‘LoadLibraryA()’와 ‘GetProcAddress()’로, 이 2개의 API만 있으면 모든 라이브러리의 함수를 호출 할 수 있다. <리스트 3>의 내용을 디버거로 살펴보면 <화면 7>과 같다.
<화면 7> OllyDbg에서 ThreadProc()
<화면 7>의 코드를 통해 모든 중요한 데이터를 스레드 파라미터인 pParam으로 받아 사용하는 것을 알 수 있다. 즉, <화면 7>의 ThreadProc() 함수가 독립 실행코드라 할 수 있다.
필자 메모 Visual C++ 2008 Express Edition에서 프로젝트의 [Release/Debug]모드와 [최적화]옵션 등에 따라서 CodeInjection.cpp 파일은 <화면 7>과 다른 형태로 빌드 될 수 있다. 위 실습 예제는 Release 모드에서 최적화 사용 안함(/Od) 옵션으로 빌드한 것이다. |
njectCode()
<리스트 5>는 Code Injection 기법의 핵심인 InjectCode() 함수이다.
BOOL InjectCode(DWORD dwPID) { HMODULE hMod = NULL; THREAD_PARAM param = {0,}; HANDLE hProcess = NULL; HANDLE hThread = NULL; LPVOID pRemoteBuf[2] = {0,}; DWORD dwSize = 0; hMod = GetModuleHandleA("kernel32.dll"); // set THREAD_PARAM param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA"); param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress"); strcpy_s(param.szBuf[0], "user32.dll"); strcpy_s(param.szBuf[1], "MessageBoxA"); strcpy_s(param.szBuf[2], "www.reversecore.com"); strcpy_s(param.szBuf[3], "ReverseCore"); // Open Process hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID); // Allocation for THREAD_PARAM dwSize = sizeof(THREAD_PARAM); pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)¶m, dwSize, NULL); // Allocation for ThreadProc() dwSize = (DWORD)InjectCode - (DWORD)ThreadProc; pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL); hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); CloseHandle(hProcess); return TRUE; } |
<리스트 5> InjectCode()
<리스트 5>는 DLL Injection 코드와 매우 유사하다. Inject Code() 함수의 앞부분은 THREAD_PARAM 구조체 변수를 세팅하고 있다. 이 값들은 상대방 프로세스에 인젝션 돼 Thread Proc() 스레드 함수에 파라미터로 전달될 것이다.
필자 메모 Windows 7에서 모든 프로세스에 로딩 된 kernel32.dll 주소가 동일하므로 CodeInjection.exe 프로세스에서 구한 API (“LoadLibraryA”, “GetProcAddress”) 주소와 notepad.exe 프로세스에서 구한 API(“LoadLibraryA”, “GetProcAddress”) 주소가 서로 동일하다는 것을 기억하자. |
그리고 API 함수 호출이 이어지는데, 핵심 API 함수들의 호출흐름만 살펴보면 <리스트 6>과 같다.
OpenProcess() // data : THREAD_PARAM VirtualAllocEx() WriteProcessMemory() // code : ThreadProc() VirtualAllocEx() WriteProcessMemory() CreateRemoteThread() |
<리스트 6> InjectCode() API 호출흐름
<리스트 6> 코드의 핵심은 상대방 프로세스에 data와 code를 각각 메모리 할당하고 인젝션 시켜준다는 것이다. 마지막으로 CreateRemoteThread() API를 이용해 원격 스레드를 실행시킨다. DLL Injection 코드와 거의 유사한 형태인 것을 확인 할 수 있다.
Code Injection 디버깅 실습
Code Injection 기법의 디버깅 방법에 대해 알아보자(첨부된 CodeInjection.exe 파일 참고).
1. notepad.exe 디버깅
OllyDbg를 이용해 notepad.exe 파일 디버깅을 시작한다. <화면 8>과 같이 실행버튼[F9]를 선택해서 notepad.exe를 “Running” 상태로 만들자.
Code Injection 기법의 디버깅 방법에 대해 알아보자(첨부된 CodeInjection.exe 파일 참고).
1. notepad.exe 디버깅
OllyDbg를 이용해 notepad.exe 파일 디버깅을 시작한다. <화면 8>과 같이 실행버튼[F9]를 선택해서 notepad.exe를 “Running” 상태로 만들자.
<화면 8> notepad.exe 프로세스 디버깅
2. OllyDbg 옵션 변경
Code Injection은 상대방 프로세스에 새로운 스레드를 생성하는 기법이므로 <화면 9>와 같이 OllyDbg 옵션을 변경하면 인젝션 된 스레드 코드의 시작부터 디버깅이 가능하다.
Code Injection은 상대방 프로세스에 새로운 스레드를 생성하는 기법이므로 <화면 9>와 같이 OllyDbg 옵션을 변경하면 인젝션 된 스레드 코드의 시작부터 디버깅이 가능하다.
<화면 9> OllyDbg 옵션 - Break on new thread
이제부터 notepad.exe 프로세스에서 스레드가 생성된다면 해당 스레드 함수 시작코드에서 멈추게 될 것이다.
3. CodeInjection.exe 실행
Process Explorer를 이용해 notepad.exe 프로세스의 PID를 구한다.
3. CodeInjection.exe 실행
Process Explorer를 이용해 notepad.exe 프로세스의 PID를 구한다.
<화면 10> notepad.exe 프로세스의 PID
PID 값을 실행 파라미터로 해 CodeInjection.exe를 실행한다.
<화면 11> CodeInjection.exe 실행
4. 스레드 시작 코드
CodeInjection.exe 프로세스가 실행돼 코드 인젝션이 성공하면 <화면 12>와 같이 인젝션 된 스레드 코드 시작위치에서 디버깅이 멈춘다.
CodeInjection.exe 프로세스가 실행돼 코드 인젝션이 성공하면 <화면 12>와 같이 인젝션 된 스레드 코드 시작위치에서 디버깅이 멈춘다.
<화면 12> OllyDbg에서 ThreadProc() 디버깅
Code Injection 기법은 이런 과정으로 디버깅을 진행하면 된다. 스스로 디버깅을 진행해 보도록 하자. 이미 소스코드를 통해 내용을 알고 있어도 실력증진을 위해 디버깅을 계속 해보자(실행 환경에 따라서 주소는 다르게 표시될 수 있다).
이것으로 Code Injection 기법에 대한 설명을 모두 마친다. 물론 Code Injection의 구현방법은 이게 전부가 아니다. Code Injection은 어셈블리 언어를 이용해 삽입시킬 코드를 제작했을 때 그 진가가 드러난다. Code Injection에 대한 더 자세히 알고 싶은 독자는 본인의 블로그에 방문해 다양하고 기발한 구현방법을 찾아보기 바란다.
이것으로 Code Injection 기법에 대한 설명을 모두 마친다. 물론 Code Injection의 구현방법은 이게 전부가 아니다. Code Injection은 어셈블리 언어를 이용해 삽입시킬 코드를 제작했을 때 그 진가가 드러난다. Code Injection에 대한 더 자세히 알고 싶은 독자는 본인의 블로그에 방문해 다양하고 기발한 구현방법을 찾아보기 바란다.
Epilogue
총 4 회에 걸쳐서 “리버싱을 이용한 애플리케이션의 기능 추가 방법”이란 주제로 연재했다. 한정된 지면에 관련기술의 설명에 큰 비중을 두느라 흥미로운 실습예제를 많이 보여주지 못했다는 생각이 든다. 하지만 이 기술들의 핵심원리만 정확히 이해한다면 응용범위는 무궁무진할 것으로 확신한다.
소프트웨어 엔지니어로써 여러분의 툴박스에 지금까지 ‘개발’이라는 툴만 들어있었다면, 이제 ‘리버싱’이란 툴이 하나 더 추가됐다. 여러분의 문제해결 방법이 더 다양해졌다는 것을 의미한다. 향후 ‘개발’툴로 어려움을 겪을 때 ‘리버싱’툴을 꺼내서 적용해봐라. 보는 관점이 변화하면서 의외로 쉽게 문제를 해결 할 수 있지 모른다.
총 4 회에 걸쳐서 “리버싱을 이용한 애플리케이션의 기능 추가 방법”이란 주제로 연재했다. 한정된 지면에 관련기술의 설명에 큰 비중을 두느라 흥미로운 실습예제를 많이 보여주지 못했다는 생각이 든다. 하지만 이 기술들의 핵심원리만 정확히 이해한다면 응용범위는 무궁무진할 것으로 확신한다.
소프트웨어 엔지니어로써 여러분의 툴박스에 지금까지 ‘개발’이라는 툴만 들어있었다면, 이제 ‘리버싱’이란 툴이 하나 더 추가됐다. 여러분의 문제해결 방법이 더 다양해졌다는 것을 의미한다. 향후 ‘개발’툴로 어려움을 겪을 때 ‘리버싱’툴을 꺼내서 적용해봐라. 보는 관점이 변화하면서 의외로 쉽게 문제를 해결 할 수 있지 모른다.
이달의 디스켓 : codeinjection.zip