안녕하십니까. 드디어 길고 길던 abex crack me2 제작이 끝을 보이기 시작합니다. 자 한 번 해봅시다.

소스를 한 번 봅시다.

허허 양이 좀 많습니다. 코드를 살펴봅시다.

- #define name 10

  #define serial 20

  #define check 0

  #define about 1

  #define quit 2

자 우선 define을 이용해 상수를 정의해줍시다.

여기에 정의된 상수값은 각각 컨트롤의 식별자로 사용하기 위해 정의하였습니다. 

name,serial -> edit

check,about,quit -> button

즉 각각의 id를 의미하는데 쉽게 말하면 button을 눌렀을 때 어떤 button이 눌러졌는지 구분해야 하지 않겠습니까? 그걸 위해 버튼의 식별자를 정의하였습니다. 그리고 에디트의 식별자도 정의해놓았는데 이 식별자는 실제로는 쓸 데가 없습니다만 쓸일이 없다고 정의를 하지 않으니 알고리즘이 꼬여 버려서 결국 정의해주었습니다.

- LPTSTR Serial(LPTSTR NameStr);

NameStr이란 유니코드문자열을 받아 유니코드문자열로 반환하는 함수입니다.

즉 NameStr에 저장된 name(값)을 읽어서 그 값에 맞는 시리얼을 제작해 반환해주는 함수라고 보시면 됩니다. 이 함수는 뒤쪽에 구현되어 있습니다.

- HWND hName,hSerial;

이름과 시리얼을 읽어올 edit 들의 핸들을 저장시키기 위해 선언하였습니다. 이 핸들을 WinMain의 지역변수가 아닌 전역변수로 선언시킨 이유는 WndProc함수에서 읽어오기 위함입니다.

- CreateWindow(L"edit",NULL,WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,

10,57,165,18,hWnd,NULL,hInstance,NULL);

- CreateWindow(L"edit",NULL,WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,

10,113,165,18,hWnd,NULL,hInstance,NULL);

-> 기존코드들

- hName = CreateWindow(L"edit",NULL,WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,10,57,165,18,hWnd,(HMENU) name,hInstance,NULL);

- hSerial = CreateWindow(L"edit",NULL,WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,10,113,165,18,hWnd,(HMENU) serial,hInstance,NULL);

->수정코드들

edit를 생성하는 윈도우의 핸들을 저장시키기 위해 수정하였습니다. 그런데? 인자 하나가 다른 부분이 있지요? 인자 8번째의 값이 다릅니다. 기존코드에서는 NULL값을 주었습니다만 수정코드에서는 (HMENU) name과 (HMENU) serial을 주는 것을 알 수 있습니다. 이 8번째 인자는 해당 윈도우의 식별자를 받는 인자입니다. 우리가 앞에서 선언한 상수들을 사용하였지요. 즉 첫번째 edit의 식별자는 name, 두번째 edit의 식별자는 serial입니다. 물론 여기서는 edit의 식별자를 사용할 필요가 없지만 버튼들의 동작구현을 위해 선언하였습니다. 아 그리고 이 인자는 반환값을 HMENU로 받기 때문에 앞에 (HMENU)를 주어 HMENU로 넘겨주었습니다.

- CreateWindow(L"button",L"Check",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,40,65,25,hWnd,NULL,hInstance,NULL);

  CreateWindow(L"button",L"About",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,80,65,25,hWnd,NULL,hInstance,NULL);

  CreateWindow(L"button",L"Quit",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,120,65,25,hWnd,NULL,hInstance,NULL);

-> 기존코드들

- CreateWindow(L"button",L"Check",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,40,65,25,hWnd,(HMENU) check,hInstance,NULL);

  CreateWindow(L"button",L"About",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,80,65,25,hWnd,(HMENU) about,hInstance,NULL);

  CreateWindow(L"button",L"Quit",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,200,120,65,25,hWnd,(HMENU) quit,hInstance,NULL);

-> 수정코드들

역시 마찬가지로 각 버튼들에게 식별자를 주었습니다.

WinMain의 설명은 끝났군요. 이제 WndProc로 넘어갑시다.

- TCHAR SerialStr[50] = L"", NameStr[50] = L"",AnswerSerial[50] = L"";

TCHAR 형의 문자열 변수를 세개 선언하고 초기화 시켜주었습니다. 크기는 전부 50으로 주었습니다. 이 문자열 변수들은 사용자가 입력한 시리얼,이름 그리고 이름을 기반으로 생성된 올바른 시리얼 값을 유니코드로 저장시키기 위해 선언하였습니다.

- int result;

사용자가 입력한 시리얼과 올바른 시리얼 값을 비교한 값이 저장될 변수입니다.

- case WM_COMMAND:

메세지가 WM_COMMAND일 때의 처리를 하기 위해 만들었습니다. 이 WM_COMMAND메세지는 윈도우에서 각각의 메뉴(컨트롤 등)을 누를 때 발생합니다.

- switch(wParam)

위에서 WM_COMMAND메세지가 메뉴를 누를 때 발생한다고 하였지요? 이 WM_COMMAND메세지가 발생하였을 때 어떤 메뉴를 눌렀는가에 대한 정보가 추가적으로 WParam(세번째 인자)로 들어갑니다.  그러므로 각 메뉴에 대한 처리를 하기위해 switch함수를 사용하였습니다.

 - case check:

wParam의 값이 check 일때의 처리입니다. 이 check는 앞에 우리가 선언한 상수이자 check버튼의 식별자 값입니다. 즉 check버튼을 눌렀을 때의 처리를 위해 선언해주었습니다. 

- if(GetWindowText(hName,NameStr,5) < 4)

MessageBox(hWnd,L"Please enter at least 4 chars as name!",L"Error",MB_OK);

GetWindowText가 나왔군요. 정의를 한 번 봅시다.

해당 핸들의 내용을 읽어옵니다. 인자를 하나씩 살펴봅시다.

*hwnd

내용을 가져올 윈도우의 핸들을 의미합니다.

*lpString

내용을 가져와서 저장시킬 유니코드 문자열을 의미합니다.

*nMaxCount

윈도우에서 내용을 가져올 때 얼마만큼 가져올 것인지를 묻습니다.

즉 이함수는 edit에서 사용자가 입력한 값을 가져오기 위해 사용하였습니다. 

핸들값이 hName이므로 첫번째 edit 윈도우의 값 중 5글자를 읽어와 NameStr에 저장시키고 읽어온 문자의 개수를 반환합니다. 5글자를 읽어오라고 하였지만 한 글자는 null값을 읽어오므로 실제로 들고오는 값은 4글자입니다. 그리고 문자의 개수를 반환할 때에는 null값은 카운트 하지 않으므로 4라는 값을 반환합니다.

쉽게 얘기하면 읽어온 글자만큼 반환합니다. 참고로 읽어올 글자가 없을 경우 0을 반환합니다.

즉 GetWindowText함수를 사용하여 읽어온 입력된 이름 글자수가 4개 미만일 경우 messeagebox함수를 출력하여 이름을 4글자 이상 입력하라고 사용자에게 알립니다.

else문을 봅시다.

- else

이름 글자수가 4개 이상인 경우는 위의 if문의 조건이 거짓이 되어 else문을 실행하게 됩니다. 그 때의 처리구문입니다.

- GetWindowText(hSerial,SerialStr,100);

hSerial에서 100만큼의 글자를 읽어들여 SerialStr에 저장시킵니다. 왜 여기서는 100글자를 읽어오냐고 하니 시리얼 칸에 입력된 값을 전부 가져오기 위함입니다. 물론 시리얼 칸에 입력된 값이 100글자를 넘어가면 다 가져오진 못하지만 그래도 왠만한 시리얼 값을 전부 들고오기 위해 100을 넣어주었습니다.

추가로 위의 Name은 입력한 값을 전부 읽어오지 않고 4만큼만 읽어오게끔 코딩을 해주었는데 이유는 기존의 abex crack me2가 이름의 값을 4만큼 읽어들여 그 4글자로 시리얼을 만드므로 이에 맞춰주기 위해 이름을 4글자만 읽어들였습니다.

- strcpy(AnswerSerial,Serial(NameStr));

strcpy함수는 문자열을 복사하는 함수이죠. Serial함수를 호출한 후 이 반환값을 AnswerSerial에 복사시킨다는 뜻입니다. 여기서 이 Serial함수는 제가 임의로 만든 시리얼 생성 함수입니다. 

- result = lstrcmp(AnswerSerial,SerialStr);

시리얼키가 저장된 AnswerSerial과 사용자가 입력한 시리얼이 저장된 SerialStr을 비교하여 결과갑을 result에 저장됩니다. 일반 strcmp가 아닌 lstrcmp를 사용하였는데 이유는 유니코드 문자열을 비교하기 위해서입니다.

이 lstrcmp의 반환값은 두 문자열이 같을 시 0을 반환합니다. 

-  if(result == 0)

MessageBox(hWnd,L"Yep, this key is right!",L"Congratulations",MB_OK);

   else 

MessageBox(hWnd,L"Nope, this serial is wrong",L"Wrong serial!",MB_OK);

lstrcmp의 반환값을 받은 이 result의 값이 0일 경우 성공문자열을 출력하고 아닐 경우 실패 문자열을 출력합니다. result의 값이 0이라는 것은 사용자가 입력한 시리얼값과 알고리즘에 의해 생성된 정답 시리얼 값이 같다는 뜻이지요.

여기까지가 체크버튼을 눌렀을 때의 처리입니다. 이제 다른 버튼을 처리합시다.

- case about:

MessageBox(hWnd,L"abex' 2nd crackme\ncoded on 30th Nobember 2016",L"About",MB_OK);

about 버튼을 눌렀을 시의 처리입니다. 이 프로그램에 관한 메세지박스를 띄웁니다. abex crack me2의 about를 누르면 문자열 (엔터) 문자열 이런식으로 메세지박스가 띄워집니다. 그래서 우리도 문자열 중간에 \n을 줌으로써 개행이 되도록 하였습니다. 문자열 내용은 조금 바꿨습니다.

- case quit:

PostQuitMessage(0);

quit버튼을 눌렀을 때의 처리입니다. quit 버튼은 종료를 하는 버튼이므로 프로그램 종료를 위해 PostQuitMessage함수를 호출하였습니다.

일반적으로 이 PostQuitMessage함수는 WM_DESTROY에서 처리를 하는 것이 맞습니다. 하지만 이 WM_DESTROY메세지는 WM_CLOSE메세지가 DefWindowProc로 인해 처리가 되었을 때 발생하는 메세지입니다. WM_CLOSE.. 즉 윈도우 창이 닫혔을 때 발생하는 메세지인데 이 윈도우는 알다시피 팝업창이기 때문에 닫는 창이 없습니다. 즉 WM_CLOSE가 발생하지 않습니다. 그렇기에 WM_DESTROY에서 PostQuitMessage처리를 하지 않고 quit버튼을 눌렀을 때 닫히도록 코딩하였습니다.

자 이제 제가 만든 Serial 함수를 살펴봅시다. 그전에 잠깐 시리얼 알고리즘을 설명하겠습니다.

abex crack me2를 리버싱 해보신 분은 아시겠지만 시리얼 키를 만드는 알고리즘이 단순합니다. 4글자의 이름을 입력받은 후 각 글자를 하나씩 뽑아와서 그 글자의 16진수 값과 16진수 64를 더한 값을 시리얼로 사용합니다. 

ex) a(0x61) + 0x64 = 0xC5(시리얼)

Serial 함수를 살펴봅시다.

 - int i,j,Key;

for문에 쓰일 i,j 변수와 16진수 덧셈연산의 결과값을 저장할 Key값을 int로 선언합니다.

- int k = 0;

for문에 쓰일 k변수를 선언 후 0으로 초기화 시킵니다. 다만 위의 i,j하고는 조금 다른 의미로 쓰입니다. 

- TCHAR Key2[50] = L"", Key3[50] = L"";

Key2와 Key3 문자열 변수를 선언합니다. Key2는 한 글자의 시리얼값이 저장되고 Key3은 각 글자의 Key2값을 저장하기 위해 사용합니다.

- for(i=0; i<lstrlen(NameStr); i++)

이름을 시리얼로 바꾸기 위한 for문입니다. NameStr에 들어있는 글자수만큼 반복하도록 구현하였습니다.

- Key = NameStr[i] + 0x64;

사용자가 입력한 이름값 즉 NameStr에서 1글자를 뽑아와서 그 글자의 값과 16진수 64를 더해서 Key에 저장합니다. 다만 이렇게 연산을 할 경우에는 10진수로 연산이 됩니다. 

예를 들어 a를 가져왔을 때 a + 0x64를 연산하지요. 이럴 때 a의 10진수 값(아스키코드)인 97과 0x64의 10진수 값인 100을 더한다는 뜻입니다. 

즉 Key값에는 197이 들어가게 됩니다. 이 197의 16진수 값은 C5이므로 이 197을 16진수로 바꿀 필요가 있습니다.

- wsprintf(Key2, L"%X", Key);

앞에서 말한 정수값 Key를 16진수로 저장시키기 위한 코드입니다. wsprintf함수가 사용되었지요? 원래 sprintf를 써야 하는데 유니코드로 저장시켜야 하므로 wsprintf를 사용하였습니다. 

함수 이름에서 알 수 있듯이 이 sprintf함수는 출력시키는 함수입니다. 다만 일반 printf함수가 콘솔에 출력시키는 거라면 이 sprintf함수는 변수에 출력시킵니다.

즉 Key2란 변수에 %X, Key 형식으로 출력시킨다는 뜻입니다. %X는 16진수 출력을 의미하지요? 그리고 이에 대응하는 변수는 Key입니다. 

정리하면 Key값을 16진수로 변환시켜서 Key2에 출력합니다. 그러면 Key2에는 아까 16진수 연산의 결과값(정수) Key의 16진수 값이 문자열로 들어가집니다. 

- for(j=0; j<lstrlen(Key2); j++)

이 for문은 Key2에 들어있는 문자열의 길이만큼 반복합니다. Key2에 있는 16진수 값을 Key3에 저장시키기 위해서 사용하였습니다.

- Key3[k] = Key2[j];

Key2의 첫 글자부터 마지막 글자까지 Key3에 저장시키는 코드입니다. 이 때 Key3의 인덱스는 k변수를 사용하였는데 그 이유는 Key3의 인덱스는 for문이 시작되어도 초기화가 되면 안되기 때문입니다. 

- k++;

글자를 받았으니 k값을 하나 증가시킵니다.

- return Key3;

최종적으로 for문이 모두 처리되면 시리얼이 저장된 Key3을 리턴합니다. 이 리턴된 Key3은 lstrcpy함수에서 사용됩니다.

자 이제 진짜 다 끝났... 으면 좋겠지만 마지막 부분이 남았습니다. 바로 아이콘입니다.

현재 프로그램의 아이콘을 보면 

기본 아이콘으로 되어있지요? abex crack me2 아이콘을 볼까요?

아이콘이 참 예쁘지요? 우리도 이렇게 예쁜 아이콘을 프로그램에 적용시켜 봅시다.

먼저 리소스 해커로 abex crack me2의 아이콘 리소스를 가져옵시다.

저장시킵시다.

저장시키면 ico확장자를 가진 파일이 생성됩니다. 이 파일을 프로젝트 안에 넣어버립시다. 위치는 딱히 상관없지만 나중에 관리하기 용이하도록 해주기 위합니다.

생성이 된 것을 확인하였으면 리소스 파일을 생성합시다.

이 리소스라는 것은 메뉴,아이콘,커서 등의 데이터를 의미합니다. 우리는 아이콘 리소스를 사용해야 하므로 리소스 파일을 새로 생성시켜 주어야 합니다.

사실 제가 쓰고 있는 visual c++ 2010 express 프로그램은 리소스 편집기를 제공하지 않습니다. 그렇기에 저는 리소스 편집을 다른 방법으로 할 예정입니다. 다른 visual프로그램을 쓰고 계신분은 리소스 편집기를 제공하므로 그걸 쓰는 것이 좋습니다. 

파일을 생성시킬 때 이름을 Icon.rc로 바꿔줍시다. 메뉴에는 리소스가 없지만 이렇게 확장자를 바꿔주는 것만으로도 리소스 파일을 생성시킬 수 있습니다.

리소스 파일이 생성되었습니다. 제가 쓰는 프로그램은 편집 기능이 지원되지 않는군요.

자 그러면 저는 ResEdit를 쓰겠습니다. 이 ResEdit 프로그램은 리소스를 편집하고 저장할 수 있는 도구입니다. 다른 버전의 visual studio는 리소스 편집기를 사용할 수 있으니 여러분들은 편하신 대로 하시면 됩니다.

RESEdit를 열고 아까 생성한 리소스 파일을 열어봅시다.

리소스 파일을 여셨다면 리소스를 추가해 봅시다.

오른쪽마우스 -> Add Resource... -> Icon을 누릅시다.


아까 저장한 ico 파일을 불러오기 위해 위의 버튼을 체크합시다.

파일을 가져옵시다.

이 ico 파일의 경로를 절대로 지정할거냐 상대로 지정할거냐고 나옵니다. 우리는 상대로 지정해줍시다.

자 아이콘 리소스를 추가하였습니다. 저장시킨 다음 visual studio로 돌아갑시다.

우리가 만든 리소스를 사용하기 위해 리소스 파일을 추가합시다. 추가 - 기존 항목을 누릅시다.

리소스를 만들고 나면 그 리소스에 관한 내용을 담고 있는 헤더파일이 생성됩니다. 이 헤더파일은 rc파일과 동일한 경로에 생성됩니다.

헤더파일이 성공적으로 추가되었습니다. 이 헤더파일에는 앞서 만든 아이콘 리소스가 상수로 정의되어 있습니다. 헤더파일을 추가하였으면 소스코드 맨위에 헤더파일을 사용하기 위해 선언을 해 줍시다.

 - include "resource.h"

이 헤더파일을 선언하실 때 유의하실 부분이 있는데 경로를 잘 맞춰주어야 합니다.

소스코드를 수정합시다.

wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION); - > 기존코드

wndclass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));

-> 수정코드

wndclass의 hIcon멤버를 위와 같이 수정하였습니다. 우리가 만든 리소스 아이콘을 사용하기 위함입니다. 세세한 설명은 다음 포스팅 때 설명드리겠습니다.

여기까지 다 되었으면 실행시켜봅시다.

작업표시줄을 보시면 아이콘이 등록된 것을 볼 수 있습니다. 파일을 한번 볼까요?

파일의 아이콘도 바뀌었습니다. 예~~

자 이제 진짜 완성되었습니다. 결과물 한번 봅시다.

네 이것으로 길고 긴 포스팅을 마치겠습니다. 감사합니다.

Posted by englishmath
,