x64 Lotto

A lottery check decompiled in IDA, where the comparison can be satisfied without guessing the numbers.

2026.08.18 Reversing original post

로또 번호를 맞춰야 하는 프로그램인 거 같다. 먼저 IDA 64 로 열어서 파일을 디컴파일링해 정적 분석을 해보자. 밑에는 IDA F5 디컴파일링으로 가져온 소스코드이다.

do {
    wprintf(L"\n\t\tL O T T O\t\t\n\n");
    wprintf(L"Input the number: ");
    wscanf_s(L"%d %d %d %d %d %d", &v13, &v14, &v15, &v16, &v17, &v18);
    wsystem(L"cls");
    Sleep(0x1F4u);

    for ( i = 0i64; i < 6; v19[i - 1] = rand() % 100 )
        ++i;

    v2 = 1;
    v3 = 0;
    v4 = 0i64;
    byte_1400035F0 = 1;

    while ( v19[v4] == *(int *)((char *)&v13 + v4 * 4) ) {
        ++v4;
        ++v3;
        if ( v4 >= 6 )
            goto LABEL_9;
    }
    v2 = 0;
    byte_1400035F0 = 0;
LABEL_9: ;
}

사용자로부터 6개의 정수 입력을 받아 저장한 뒤, rand() % 100 으로 무작위 6개 숫자를 생성합니다. 생성된 랜덤 숫자 배열(v19)과 사용자가 입력한 숫자(v13~v18)를 앞에서부터 순차 비교합니다. 6개가 모두 동일하면 성공 플래그(v2=1, byte_1400035F0=1) 를 유지하고, 하나라도 다르면 즉시 실패로 설정합니다.

while ( v3 != 6 );

v5 = byte_140003021;
v23[1] = 92;   v23[0] = 184;  v23[2] = 139;  v23[5] = 184;  v23[3] = 107;
v6 = 0i64;
v23[4] = 66;   v23[6] = 56;   v23[7] = 237;  v23[8] = 219;  v23[9] = 91;
v23[10] = 129; v23[11] = 41;  v23[12] = 160; v23[13] = 126; v23[14] = 80;
v23[15] = 140; v23[16] = 27;  v23[17] = 134; v23[18] = 245; v23[19] = 2;
v23[20] = 85;  v23[21] = 33;  v23[22] = 12;  v23[23] = 14;  v23[24] = 242;
v24 = 0;

do {
    v7 = byte_140003021[v6 - 1];
    v6 += 5i64;
    *((_WORD *)&v20 + v6 + 1) ^= (unsigned __int8)(v7 - 12);
    *((_WORD *)&v21 + v6)     ^= (unsigned __int8)(byte_140003021[v6 - 5] - 12);
    *((_WORD *)&v21 + v6 + 1) ^= (unsigned __int8)(byte_140003021[v6 - 4] - 12);
    v23[v6 - 2]               ^= (unsigned __int8)(byte_140003021[v6 - 3] - 12);
    v23[v6 - 1]               ^= (unsigned __int8)(byte_140003021[v6 - 2] - 12);
} while ( v6 < 25 );

if ( v2 ) {
    v8 = 0;
    v9 = v23;
    do {
        v10 = *v9++;
        v11 = v8++ + (v10 ^ 0xF);
        *(v9 - 1) = v11;
    } while ( v8 < 25 );
    v24 = 0;
    wprintf(L"%s\n", v23);
}
wprintf(L"\n", v5);
return 1i64;
}

여러 변수를 고정된 값으로 초기화한다. 0x140003021 에 있는 데이터를 가져와서, 초기화한 변수들과 순서대로 XOR 연산을 수행해 값을 변경한다. v2 == 1 (앞에서 로또 숫자를 모두 맞춘 경우) 이면, 변형된 값들에 다시 0xF 를 XOR 한 뒤 문자열로 출력한다. 이 출력 결과가 flag 이다.

우리의 입력값들이 flag 를 만들어내는 데 아무런 영향을 주지 않고, 오직 v23 이랑 byte_140003021 들의 값들만을 사용해 플래그가 생겨난다.

Figure 1

그럼 x64dbg 에 다시 들어가서 확인해보자. 메인 함수로 가 로또 번호를 받는 곳으로 문자열 찾기를 이용해 옮겨간다. cls 문자열이 있으니까 거기 브레이크 포인트를 걸어주자.

코드에서 성공 여부를 결정하는 cmp 부분들에 브레이크 포인트를 걸고, 값들을 분기해준다. 성공 여부를 결정하는 코드인지 아닌지 아는 방법은, 그 후 jne 가 처음으로 다시 이동시키면 성공 여부를 결정하는 코드인 걸 알 수 있다.

첫 번째로 밑에 있는 cmp edx, 6 부분에 걸어준다. 여기서 만약 edx 가 6과 같지 않으면 다시 시작 부분으로 옮겨간다. rdx 부분을 6으로 분기 후 F8 해준다.

Figure 2

두 번째로 test r8b, r8b 부분에 하드웨어 브레이크를 걸어준다. 도착하면 R8 를 1로 분기해서 다시 처음으로 돌아가지 않게 해준다.

Figure 3

숫자를 틀리면:  ZF = 0  → jne → 실패 루트
맞추면:         ZF = 1  → 성공 루트

부가 설명. 왜 cls 에 첫 번째 BP 를 거는가? “사용자 입력/출력의 시작점이기 때문” 이다. 흐름을 보면:

EntryPoint
  → CRT 초기화 (__scrt_common_main_seh)
  → 사용자 코드 진입 (wmain)
      → 화면 정리 (system("cls"))
      → UI 출력 (wprintf("LOTTO"))
      → 입력 (wscanf_s)

cls 는 CRT 가 아니라 ‘문제 출제자가 작성한 로직’의 첫 행동이다. 그래서 리버싱에서는 cls / printf / scanf 가 연속으로 나오면 “아, 여기부터 사용자 main 이구나” 라고 판단한다.

Figure 4

참고로 여기 wprintf 문 (플래그를 프린트한다) 에 BP 를 걸어야 성공했을 때 플래그를 얻을 수 있다! 끝에 답은:

Figure 5