리버스 엔지니어링

crackmes.one crackme 001 by disip 풀이 + 크래킹 프로그램 제작

Injel me 2020. 7. 1. 18:51
crackmes.one 에서 very easy 난이도의 크랙미 중 하나를 풀어보았다.

by disip 로 되어있는 크랙미인데 시리얼미이다.


readme에 의하면 자기가 만든 첫 크랙미라고 하고 난이도는 10점주자면 0점이라고 할 정도로 쉽다고 한다.

NAME과 PASS를 입력하고 Register 버튼을 눌러 PASS를 맞추는 프로그램이다.

디버거로 열어서 DlgItemTextA 함수를 호출하는 곳을 찾았다.

총 두 번 호출 후
문자열의 길이를 비교해서 3보다 크면 함수를 호출한다.

함수는 루틴을 거쳐가며 시리얼을 반복을 통해 검사하는 것을 볼 수 있다.

루틴을 통해 NAME의 값을 시리얼로 통째로 어느 공간에 저장하는 것이 아닌 반복문에서 시리얼을 제작해 가면서 PASS 문자열과 계속 검사를 하게 된다.

간단히 간추려서 시리얼을 푸는 코드를 짜 보았다.

1. 문자열의 길이만큼 반복
2. 시리얼 값이 되는 데이터를 레지스터로 사용(코드에는 해당되지 않음)
3. NAME 문자를 하나하나 비교해 'Z', 'z', '9' 일 경우 -1
4. NAME 문자 하나하나 + 1을 시리얼의 각 문자로 사용
5. NAME 문자 뒤에 'a' + i 의 문자를 삽입
6. 반복

예시로 NAME이 1234라면
'1' + 1, 'a' + 0, 'b' + 1, 'a' + 1, 'c' + 1, 'a' + 2, 'd' + 1, 'a' + 3
= 2a3b4c5d
라는 시리얼 값이 되게 된다.

어셈블리어를 다시 보게 되면
eax를 반복문의 조건값 / (5)번 절차의 i로 사용하고, bx와 cx를 비교하게 된다.
bl에 NAME[i]번째의 값 + 1이, bh에 'a' + i의 값이 들어가고
cx에 문자열의 위치 + eax * 2의 위치에서 word크기만큼 가져오게 된다.
bx와 cx를 비교해 같지 않으면 틀렸다는 메시지를 띄우게 된다.

이름을 4bc9라고 하면 키 값을 구해서 넣어보았다.


성공하는 것을 볼 수 있다.


추가로, 이전에 유튜브 스무디 채널에서 봤었던 닷지 크랙 제작이 떠올라서 그 코드를 참고로 패치시키는 프로그램을 제작해 보기로 했다.

이름을 시리얼로 전체 다 바꾸고 비교하지 않기 때문에 jmp패치를 하지 않고 패치시킬 방법으로 엉뚱한 생각을 하게 됐다.
문자열을 가져오는 부분을 다시 보게 되면, pass부터 가져오고 그 다음에 name을 가져오는 것을 볼 수 있다.

문자열을 가져오는 주소에 패치를 해도 버튼을 누르면 다시 입력받은 것을 GetDlgItemTextA함수로 가져오기 때문에 패치시킬 수 없다.

패치시키는 방법은 여러가지니, 제 생각대로 한 것이니 양해 바란다.

버튼을 눌렀을 때 가져오는 주소를 패치시키기로 했다. 너무 엉뚱한 주소를 넣으면 pe 구조에 의해 읽지 못하는 주소나 등등의 문제가 생길 수 있기 때문에 name문자열의 주소로 가져오게 하려고 한다.
그리고 텍스트를 입력해 시리얼을 프로그램에서 구해서 원래 프로그램이 pass를 가져오는 주소에 작성하게 바꾼다.


push 40301a는 68 1a 30 40으로 되어 있다. 68 뒤의 역으로 되어있는 16진수 숫자가 push하는 주소를 나타낸다.
이것을 hxd로 바꾸게 되면 실제로 push 인자가 바뀌게 된다.

실시간으로 바꾸려면 프로세스의 가상 메모리 속의 rva 주소인 401095에 패치시키면 된다.

name 문자열을 가져오는 주소는 403014이고, 이걸로 패치하려면
68 1a 30 40을 68 14 30 40으로 바꾸면 될 것이다.

그런데 저 프로그램의 코드가 들어가게 되는 섹션은 Characteristics가 0x60000020으로,

수정이 불가능한 섹션 액세스 권한으로 되어있다.
캡처 출처 : reversecore

이 섹션의 액세스 권한에 MEM_WRITE를 추가로 주려면 60000020에 80000000을 bit or 연산을 하면 된다.
bit or 연산을 한 값은 e0000020이다.

섹션의 characteristics를 먼저 수정해야 원하는 코드 주소에 수정을 할 수 있게 된다.

영상의 제공된 코드를 참고해 mfc로 제작해 보았다.
참고 영상 : 닷지 무적 핵 제작 스무디 유튜브

폼의 모양은 이렇고,
버튼을 눌렀을 때의 함수의 내용은 다음과 같다.
void CdisipcrackmecrackDlg::OnBnClickedPatch()
{
    // TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
    BYTE original[] = { 0x1a0x300x40 };
    BYTE patch_buf[] = { 0x140x300x40 };
    BYTE pe_char[] = { 0xe0 };
    const LPVOID point = (LPVOID)0x401096;
    const LPVOID passpoint = (LPVOID)0x40301a;
    CWnd * cwnd = FindWindow(NULL, (LPCTSTR)".:: Created by DiS[IP] ::.");
    HWND hwnd = cwnd->m_hWnd;
    DWORD processid = 0;
    DWORD threadid = 0;
    HANDLE mainHandle;
    CString password;
    CString ckey;
    char * key;
    GetDlgItemText(EDIT, password);
    if (cwnd != NULL) {
        threadid = GetWindowThreadProcessId(hwnd, &processid);
        if (processid == NULL) {
            MessageBox((LPCTSTR)"GetWindowThreadProcessId fail", (LPCTSTR)"Fail", MB_OK);
            return;
        }
        mainHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processid);
        if (mainHandle == NULL) {
            MessageBox((LPCTSTR)"OpenProcess Fail", (LPCTSTR)"Fail", MB_OK);
            return;
        }
        //edit [.text] section header characteristics : add Writeable
        WriteProcessMemory(mainHandle, (const LPVOID)0x004001df, pe_char, 1NULL);
        BOOL isAccessed = WriteProcessMemory(mainHandle, point, patch_buf, 3NULL);
        if (isAccessed == false) {
            MessageBox((LPCTSTR)"Write Fail", (LPCTSTR)"Fail", MB_OK);
            return;
        }
        int len = password.GetLength();
        key = new char[len + 1];
        char c;
        int j = 0;
        for (int i = 0; i < len; i++) {
            c = (password[i]) + 1;
            if (c == 'Z') c--;
            if (c == 'z') c--;
            if (c == '9') c--;
            key[j] = c;
            key[j + 1= i + 'a';
            j += 2;
        }
        key[j] = NULL;
        ckey = key;
        
        BOOL isPatched = WriteProcessMemory(mainHandle, passpoint, ckey, strlen(key), NULL);
        if (isPatched) MessageBox((LPCTSTR)"Patched", (LPCTSTR)"Success", MB_OK);
        else MessageBox((LPCTSTR)"Patch fail", (LPCTSTR)"Error", MB_OK);
    }
}
cs
window 캡션 이름을 이용해 FindWindow함수로 윈도우 쓰레드 핸들을 찾아 프로세스를 열어서 pe헤더 위치에 60을 e0으로 바꿔 MEM_WRITE권한을 갖게 하고, push 명령어의 인자 주소를 patch_buf의 내용으로 쓴다.

그 후에 EDIT 필드에서 적은 NAME 값을 가져와 키를 구하는 과정을 반복문을 통해 구하게 되고, 마지막으로 키 값을 pass가 구해지는 주소에 직접 써서 시리얼을 정확히 일치시키게 한다.

여담으로 aslr기법을 가진 크랙미일 경우 직접 같은 주소에 넣는건 불가능하지만 아직 aslr 기법이 적용된 문제를 풀어본 것 같지는 않고, 또 이 문제를 풀기 위해서만 짰기 때문에 코드는 이해 바란다.

프로그램의 실행 결과는 다음과 같다.

패치를 누르지 않았을 경우에 틀리게 된다.
name에 원하는 name인 3gvd를 넣고 patch 버튼을 눌러보자.


코드의 내용대로 패치되었다고 뜬다.
다시 크랙미의 레지스터 버튼을 누르게 되면
pass가 aaaa임에도 불구하고 성공이라고 뜬다.
패치가 어떻게 진행되었는지 디버거를 물려 직접 확인해보자.
문자열을 가져오는 위치가 내가 생각했던 대로 같은 위치를 가져오게 하는 것을 볼 수 있다.
텍스트 에딧 다이얼로그에서 처음에 pass에 있는 값을 A버퍼에 가져오더라도 name에 있는 값을 또 A버퍼에 가져오기 때문에 pass의 텍스트 값은 무시된다고 생각해도 된다.

그리고 이전의 pass를 가져와야 했을 주소인 40301a를 한번 봐 보자.

덤프 창에서 보면 버튼을 누르지도 않았는데 완벽한 시리얼 값이 주소에 직접 쓰인 것을 볼 수 있다.

크랙 패치가 성공적으로 이루어졌다.

-파일-
크랙 패치 프로그램
disip 크랙미 프로그램(압축파일 패스워드 : crackmes.de)