[REV]

이번에는 I LOVE Registry 문제 입니다.
저는 악성코드에 관심이 많기 때문에 그 만큼 레지스트리를 많이 접하게 되어 이번에는 이 부분을 컨셉으로 삼아보면 어떨까? 라는 생각을 하면서 문제를 만들었습니다.

[다소 문제가 길 수 있습니다]

EXE 바이너리이기 때문에 동적 실행부터 해보겠습니다.

얼라라? 에러메시지와 Console창이 동시에 공존함을 볼 수 있습니다.
----1

그렇다면, 이제 우리는 바이너리를 까봐야 합니다.
CTF할 때는 대부분 IDA를 사용할 것이기 때문에 IDA를 이용한 풀이를 진행해보고자 합니다.

해당 바이너리는 32비트라는 정보를 획득해두었습니다.
32BIT

저 위의 사진 정보로 EntryPoint를 알 수 있고, 덤으로 메인 함수의 주소도 알 수 있습니다.
ENTRYPOINT

다음은 메인 함수입니다.
요약하면 다음과 같습니다. 차민석 연구원님이 말씀하셨듯이 큰틀을 먼저 보고 세부적으로 분석하는 방식으로 진행해봅니다.
C드라이브 Temp에 WELCOMEHACKINGCAMP2018이라는 폴더를 생성할 수 있는지 판단 여부 검사를 하고 반복문에서 특정 함수를 5번 호출하게 됩니다. 그 후 do while문을 이용하여 특정 연산 체크를 한 뒤, 일치하는 값이 아니면 이전 그림에서 본 ERROR 메시지 박스를 띄우게 되는 코드 입니다.
그렇지 않으면, WELCOME을 띄워주는 군요. 모든 작업이 끝나게 되면 우리는 remove 함수를 통해 mkdir로 생성한 디렉터리를 지운다는 시나리오임을 알 수 있습니다.

void __cdecl __noreturn main(int a1, int a2)
{
  bool v2; // cf
  char v3; // [esp+10h] [ebp-68h]
  int v4; // [esp+18h] [ebp-60h]
  int v5; // [esp+1Ch] [ebp-5Ch]
  char v6; // [esp+20h] [ebp-58h]
  int v7; // [esp+24h] [ebp-54h]
  int v8; // [esp+28h] [ebp-50h]
  char *v9; // [esp+2Ch] [ebp-4Ch]
  char *v10; // [esp+30h] [ebp-48h]
  int i; // [esp+34h] [ebp-44h]
  unsigned __int8 v12; // [esp+39h] [ebp-3Fh]
  unsigned __int8 v13; // [esp+3Ah] [ebp-3Eh]
  char v14; // [esp+3Bh] [ebp-3Dh]
  char v15; // [esp+3Ch] [ebp-3Ch]
  CPPEH_RECORD ms_exc; // [esp+60h] [ebp-18h]

  strcpy(&v15, "C:\\Temp\\WELCOMEHACKINGCAMP2018");
  sub_401E40(&v6);
  sub_4019D0(&v3);
  if ( a1 == 2 )
  {
    v4 = sub_401B50();
    if ( access("C:\\Temp", 0) == -1 )
      mkdir("C:\\Temp");
    v7 = mkdir(&v15);
    if ( v7 == -1 )
      sub_4010B0("fail!\n");
    else
      sub_4010B0("Success!\n");
    for ( i = 0; i < 5; ++i )
      v14 = sub_4012E0(&v6, byte_405030[i]);
    v9 = &byte_405028;
    v10 = *(char **)(a2 + 4);
    do
    {
      v13 = *v10;
      v2 = v13 < (unsigned __int8)*v9;
      if ( v13 != *v9 )
        goto LABEL_25;
      if ( !v13 )
        break;
      v12 = v10[1];
      v2 = v12 < (unsigned __int8)v9[1];
      if ( v12 != v9[1] )
      {
LABEL_25:
        v8 = -v2 | 1;
        goto LABEL_18;
      }
      v10 += 2;
      v9 += 2;
    }
    while ( v12 );
    v8 = 0;
LABEL_18:
    v5 = v8;
    if ( v8 )
    {
      MessageBoxA(0, &byte_405038, "error", 0);
    }
    else
    {
      sub_4010B0("Welcome %s\n");
      sub_4019F0(&v3);
    }
  }
  else
  {
    ms_exc.registration.TryLevel = 0;
    sub_401AE0();
    ms_exc.registration.TryLevel = -2;
  }
  remove("C:\\Temp\\WELCOMEHACKINGCAMP2018");
  exit(0);
}

그렇다면, 1차적으로 검증해봐야 하는 곳은 어디일까요?
"반복문"이 아닐까 싶어요.

직감대로 반복문에 들어가봅니다.
이 함수는 매개변수가 2개임을 확인할 수 있습니다.
매개변수가 함수에 나오게 된다면, 2가지 생각을 동시에 해야합니다.

  1. 매개변수 갯수가 몇개 사용되는가?
  2. 각각의 매개변수가 의미하는 바는 무엇인가?

어떠한 값에 XOR 연산을 취하게 됩니다. 그랬을 때 dword_405444가 4보다 크거나 같으면 참이 되고, 그렇지 않으면 -1이 반환 됩니다.
여기서 중요한 점은 dword_405444이기 때문에 global value입니다. 즉, 이전에 본 for문 5번 반복 할 때 dword_405444가 증가하는것이죠. 그렇다면 중요 정보를 캐치할 수 있겠네요.

char __stdcall sub_4012E0(int a1, char a2)
{
  byte_405028[dword_405444] = (dword_405444 + 5) ^ a2;
  if ( dword_405444 >= 4 )
    return 1;
  ++dword_405444;
  return -1;
}

dword_405444는 cnt라는 변수명으로 rename 해둡니다.
그리고 저 부분에서 알 수 있는 부분을 rename 합니다.

.data:00405030 ; char XORVALUE[]
.data:00405030 XORVALUE db 76h ; DATA XREF: main+148↑r
.data:00405031 aRfz db 'rfz}',0

.data:00405028 arr db 41h ; DATA XREF: argument_calc+1D↑w
.data:00405028 ; main:loc_401D6C↑o
.data:00405029 aAaaa db 'AAAA',0

char __stdcall argument_calc(int a1, char xorVAL)
{
  arr[cnt] = (cnt + 5) ^ xorVAL;
  if ( cnt >= 4 )
    return 1;
  ++cnt;
  return -1;
}

이 부분을 연산하면 다음과 같습니다.

#include <stdio.h>

int main(int argc, char*argv[])
{
	char arr[] = "AAAAA";
	char xorval[] = "vrfz}";

	for (int i = 0; i < 5; ++i)
	{
		xorval[i] ^= 0x5 + i;
		arr[i] = xorval[i];
		printf("%c", arr[i]);
	}
	// arr val = start
}

매개변수를 획득하였으니 실행을 해보겠습니다.
---------

[0]~[5]까지의 옵션이 적용되어 있음을 알 수 있습니다.

그럼 다시 IDA를 이용하여 분석을 진행해보겠습니다.
지금부터는 빠른 진행을 위해 rename 후 설명하겠습니다.

int __thiscall Init(void *this, int a2)
{
  int result; // eax
  void *v3; // [esp+4h] [ebp-4h]

  v3 = this;
  result = a2;
  switch ( a2 )
  {
    case 0:
      printf("Let's Search\n");
      result = Search(v3);
      break;
    case 1:
      result = Enroll(this);
      break;
    case 2:
      result = Edit(this);
      break;
    case 3:
      result = Delete(this);
      break;
    case 5:
      exit(-1);
      return result;
    case 7:
      result = Hidden(this);
      break;
    default:
      return result;
  }
  return result;
}

0번 Search에 가면 regedit을 실행할 수 있습니다.
HINSTANCE Search()
{
return ShellExecuteA(0, "open", "C:\Windows\regedit.exe", 0, 0, 5);
}

1번 Enroll에 가면 한눈에 봐도 보기 싫은 함수라고 볼 수 있겠네요.
하지만 하나하나 살펴보면 흐름이 보이기 때문에 하나하나 살펴보도록 하겠습니다.

FILE *Enroll()
{
  FILE *result; // eax
  int v1; // ST30_4
  int v2; // ST14_4
  wchar_t v3; // ST46_2
  __int16 v4; // ST44_2
  void *Memory; // [esp+30h] [ebp-428h]
  FILE *v6; // [esp+34h] [ebp-424h]
  unsigned int i; // [esp+3Ch] [ebp-41Ch]
  __int16 *v8; // [esp+40h] [ebp-418h]
  const wchar_t *v9; // [esp+48h] [ebp-410h]
  FILE *File; // [esp+4Ch] [ebp-40Ch]
  char v11[421]; // [esp+50h] [ebp-408h]
  char Dst; // [esp+1F5h] [ebp-263h]
  __int16 v13; // [esp+242h] [ebp-216h]
  WCHAR Buffer; // [esp+244h] [ebp-214h]
  int v15; // [esp+44Ch] [ebp-Ch]
  char v16; // [esp+450h] [ebp-8h]

  v15 = 0;
  v16 = 0;
  strcpy(
    v11,
    "/x48/x65/x6c/x6c/x6f/x20/x49/x20/x61/x6d/x20/x73/x6f/x72/x72/x79/x20/x54/x68/x69/x73/x20/x44/x75/x6d/x70/x20/x69/x73"
    "/x20/x46/x41/x4b/x45/x20/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65"
    "/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72"
    "/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e");
  memset(&Dst, 0, 0x4Fu);
  result = fopen("HACKINGCAMP_Service.dll", "rb");
  File = result;
  if ( !result )
    result = (FILE *)printf("not file\n");
  if ( File )
  {
    fseek(File, 1200, 2);
    ftell(File);
    printf("start offset = 0x%x\n");
    printf("size = 0x%x\n");
    Memory = malloc(0x200u);
    memset(Memory, 0, 0x200u);
    fseek(File, 1200, 2);
    GetTempPathW(0x104u, &Buffer);
    v1 = lstrlenW(&Buffer);
    v2 = lstrlenW(L"dump.exe") + v1;
    v9 = L"dump.exe";
    do
    {
      v3 = *v9;
      ++v9;
    }
    while ( v3 );
    v8 = &v13;
    do
    {
      v4 = v8[1];
      ++v8;
    }
    while ( v4 );
    qmemcpy(v8, L"dump.exe", (char *)v9 - (char *)L"dump.exe");
    printf("user = %S\n");
    v6 = wfopen(&Buffer, "w");
    if ( v6 )
      puts("Created!\n");
    else
      puts("NOT Created\n");
    for ( i = 0; i < 0x1F4; ++i )
      sub_401070(v6, "%c", v11[i]);
    fclose(File);
    fclose(v6);
    free(Memory);
    result = (FILE *)memset(Memory, 0, 0x83FFu);
  }
  return result;
}

이 코드의 핵심은 이 부분입니다.
result = fopen("HACKINGCAMP_Service.dll", "rb");

여러분들은 바이너리와 함께 HACKINGCAMP_Service.dll을 받으셨을 겁니다.
정상적으로 파일을 찾았다면 fopen의 반환 값이 0(NULL)이 아니겠지요.
파일의 offset, size를 볼 수 있다고 하네요.
fseek(File, 1200, 2);
ftell(File);
printf("start offset = 0x%x\n");
printf("size = 0x%x\n");

그 후 진행되는 루틴이 0x200만큼 동적할당을 하고, fseek를 이용하여 오프셋 체크를 하게 됩니다.
Memory = malloc(0x200u);
memset(Memory, 0, 0x200u);
fseek(File, 1200, 2);

Edit, Delete 함수입니다. 레지스트리는 중요한 부분을 담고 있는게 많기 때문에
수정 및 삭제 시키는 기능을 넣지 않았습니다.

IDA로 한글 보는 법은 제가 모르기 때문에 OLLYDBG로 보면 다음과 같습니다.
---3

Hidden 부분은 제가 레지스트리를 사랑하기 때문에 레지스트리 키 값을 등록하는 방법에 대해 쭉 서술해둔 쓸모 없는 코드입니다.

int SETRegidit()
{
  HKEY v1; // [esp+8h] [ebp-9Ch]
  DWORD cbData; // [esp+Ch] [ebp-98h]
  BYTE *v3; // [esp+10h] [ebp-94h]
  DWORD dwDisposition; // [esp+14h] [ebp-90h]
  int v5; // [esp+18h] [ebp-8Ch]
  int v6; // [esp+1Ch] [ebp-88h]
  _BYTE *v7; // [esp+20h] [ebp-84h]
  BYTE *lpData; // [esp+24h] [ebp-80h]
  LSTATUS v9; // [esp+28h] [ebp-7Ch]
  HKEY phkResult; // [esp+2Ch] [ebp-78h]
  BYTE *v11; // [esp+30h] [ebp-74h]
  const char *v12; // [esp+34h] [ebp-70h]
  char Dst; // [esp+3Ch] [ebp-68h]
  _BYTE v14[3]; // [esp+3Dh] [ebp-67h]

  v5 = 0;
  dwDisposition = 0;
  lpData = (BYTE *)"JBBUCTKQPNJU6USSLFPVISCJKNPUSU27JZHVIX2GJRAUO7I=";
  v9 = RegOpenKeyExA(
         HKEY_LOCAL_MACHINE,
         "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
         0,
         0xF003Fu,
         &phkResult);
  if ( !v9 )
  {
    memset(&Dst, 0, 0x64u);
    sub_4011E0(&Dst, "Shell");
    v12 = &Dst;
    v7 = v14;
    v12 += strlen(v12);
    v6 = ++v12 - v14;
    v5 = v12 - v14 + 1;
    v9 = RegCreateKeyExA(
           HKEY_LOCAL_MACHINE,
           "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
           0,
           &Dst,
           0,
           0xF003Fu,
           0,
           &phkResult,
           &dwDisposition);
    v11 = lpData;
    v3 = lpData + 1;
    v11 += strlen((const char *)v11);
    cbData = ++v11 - (lpData + 1);
    RegSetValueExA(phkResult, "HCAMP2018.exe", 0, 1u, lpData, v11 - (lpData + 1));
    RegCloseKey(phkResult);
    RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\HCAMP", 0, 0xF003Fu, &v1);
    RegCreateKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\HCAMP", 0, 0, 0, 4u, 0, &v1, 0);
    printf("Sorry you Not Auth!!\n");
  }
  return printf("finish\n");
}

그럼 일차적인 분석은 끝났습니다. 동적 디버깅을 해도 이건 뭐 offset을 알려주지 않는 문제입니다. 제 호스트에는 HACKINGCAMP_Service.dll 이런 경로가 없기 때문입니다. 이 것을 절대경로로 수정했다면 FOPEN의 FILE*가 올바르게 반환 되겠죠. 하지만 절대경로를 지정하지 않아도 문제 해결은 할 수 있습니다.
[Enroll] 이 부분을 다시 한번 들여다 봅시다.

/x48/x65/x6c/x6c/x6f/x20/x49/x20/x61/x6d/x20/x73/x6f/x72/x72/x79/x20/x54/x68/x69/x73/x20/x44/x75/x6d/x70/x20/x69/x73/x20/x46/x41/x4b/x45/x20/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e/x4d/x65/x72/x6f/x6e/x67/x7e

우선 이 값을 decoding 해봅시다.
출제자 인성이 드러나네요...(grin)
dump_merong

그럼 이제부터 저 값은 무시하도록 합시다.
자 이제부터 SIZE를 구해야하는데요. 이상한 점이 보이시나요? hexrays는 size를 최적화 해서 보여주지만, 어셈블리어로 보게 되면 실제 사이즈를 알 수 있는 연산이 보임을 확인할 수 있습니다.

push    2               ; Origin
push    4B0h            ; Offset
mov     edx, [ebp+File]
push    edx             ; File
call    ds:fseek
add     esp, 0Ch
mov     eax, [ebp+File]
push    eax             ; File
call    ds:ftell
add     esp, 4
mov     [ebp+Size], eax
mov     ecx, [ebp+Size]
sub     ecx, 4B0h
mov     edx, [ebp+Size]
sub     edx, 88AEh
sub     ecx, edx
add     ecx, 1
mov     [ebp+Size], ecx
push    4B0h
push    offset aStartOffset0xX ; "start offset = 0x%x\n"
call    printf
add     esp, 8
mov     eax, [ebp+Size]
push    eax
push    offset aSize0xX ; "size = 0x%x\n"
call    printf

이를 다시 구현하면 다음과 같습니다.
size = (size - 0x4B0) - (size - 0x88AE) + 1
그렇다면, size는 무엇일까요???? size는 우리가 fseek에서 알 수 있습니다.

fseek(File, 0x4B0, 2); >> 과연 이것으로 어떻게 알 수 있는데? 라고 하실지도 모릅니다. 저희에게는 msdn이 있습니다.

int fseek(   
   FILE *stream,  
   long offset,  
   int origin   
);     

origin이 2가 되었을 때 무엇인지 알아봐야 합니다.
이런 것을 알고 싶을 때는 직접 함수를 선언 한 뒤 정의값에 접근하면 됩니다.
fseek함수는 stdio.h 안에 정의되어 있으며 다음과 같은 origin을 사용합니다.

/* Seek method constants */

#define SEEK_CUR    1
#define SEEK_END    2
#define SEEK_SET    0

그렇다면 SEEK_END라는 것이겠네요?
자 OFFSET은 시작되는 기준점입니다. 기억하셔야 합니다.
하지만 이 시작은 항상 "0" 일 수는 없습니다. 코드를 짜기 나름이에용

지금 코드에서는 fseek(File, 0x4B0, 2); 0X4b0으로 되어 있습니다.

그렇다면, 뭐가 뭔지는 모르겠다만...
0x4B0 부터 파일의 끝까지 일단 읽어 오겠다는 겁니다.

친절하게 파일 명까지 우리는 알고 있고, 파일도 미리 제가 주었습니다.
파일을 열어보겠습니다.

HACKINGCAMP_Service.dll의 사이즈는 8BFF입니다.
DLL-FILE
그리고 우리는 위에서 구한 수학 연산을 통해 OFFSET의 범위를 추정할 수 있게 됩니다.

시작 부분
----

끝 부분
-----1

우리는 평소에 자주 보았던 4D 5A를 볼 수 있습니다.
그럼 RESOURCE HACKER로 한번 살펴보겠습니다.

resourcehacker

이렇게 HxD에 옮겨서 새로운 파일로 만듭니다.
hxd

변환 전 입니다.
---

변환 후 입니다. 갑자기 data에서 PE로 바꼈어요!!
-----2

변환 하기 위한 코드입니다.

f = open("./FLAG","rb")
data = f.read()
rev_data = data[::-1]

e = open("./CONV_FLAG","wb")
e.write(rev_data)

e.close()
f.close()

그 후 PE를 다시 host로 옮겨옵니다.
이놈.... 기껏바꿨더니 HINT만 주네요
제가 만들었지만 짜증나네요 풀이 쓰려니 ㅋㅋㅋㅋ
------

그럼 우리가 받아 놓은 또 다른 파일 DATA를 봅시다.

이게 DATA 파일이네욤

HMACPuR=S?szrw_^0}sgbbeQEcbkdjej_JazaQXJ}

딱 봐도 난 플래그다 !!!! 소리치고 있네요

그렇다면 CONV_FLAG를 리버싱 해봅시다.
---64--

IDA로 까봅니다.... 끝이 보이네요 ^0^

야!!! srand잖아 죽을래!!! 라고 할 수도 있지만 srand(time(NULL))이 아니기 때문에 고정 값입니다.

__int64 __fastcall MAIN(int a1, __int64 a2)
{
  int argc; // ebx
  __int64 argv; // rdi
  __int64 v4; // rdi
  signed __int64 v5; // rbx
  __int64 result; // rax

  argc = a1;
  argv = a2;
  srand(0x1000u);
  if ( argc == 2 )                              // 2개의 매개변수 필요
  {
    v4 = calc_flag(argv);                       // argv[1]을 대입
    v5 = '\0';
    do
      printf((__int64)"%c", *(unsigned __int8 *)(*(_QWORD *)(v4 + 8) + v5++));
    while ( v5 < 41 );
    srand(0x1000u);
    calc_flag(v4);
    printf((__int64)"\n");
    printf((__int64)"\n");
    printf((__int64)"\n");
    result = 0i64;
  }
  else
  {
    printf((__int64)"HINT : DATA file\n");
    result = 0i64;
  }
  return result;
}

-------1

1과 2를 입력했을 때 time 값 고정 확인했고, 막 이상한 값이 나옴을 확인했습니다.
그럼 좀 더 상세 분석을 해야겠죠.

보통 exe나 elf에서 argc 2개를 검증하는 부분에서는 argv[1]에 문자열이 들어가게 된다는 것을 상기할 수 있습니다.

그럼 DATA에서 봤던 것을 그대로 넣으면 어떻게 될까요?
이런 결과가 나오게 되요 ㅠㅠ
[feedback] '^'는 escape 문자열로 인식되서 ^^ 두개를 써주면 바로 올바른 값이 나오게 됩니다.
이 부분을 미처 보지 못했었네요 ^^;; 발견해준 분께 감사인사 드립니다.
----2
feedback

hex값으로 직접 역연산을 구상하셔도 됩니다.

__int64 __fastcall sub_140001070(__int64 data)
{
  __int64 v1; // rdi
  int v2; // eax
  unsigned int v3; // ebx

  v1 = data;
  v2 = rand();
  v3 = v2 % 10 + 10;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 1i64) ^= (unsigned __int8)(v2 % 10) + 10;
  printf((__int64)"time = %d\n", v3);
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 3i64) ^= v3; // 홀수만 xor한다
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 5i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 7i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 9i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 11i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 13i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 15i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 17i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 19i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 21i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 23i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 25i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 27i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 29i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 31i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 33i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 35i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 37i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 39i64) ^= v3;
  *(_BYTE *)(*(_QWORD *)(v1 + 8) + 41i64) ^= v3;
  return v1;
}

이대로 코드를 짜면 이렇게 됩니다.

#include<stdio.h>
#include<windows.h>
int main(void)
{
	unsigned char v1[41] = {
		0x48, 0x4D, 0x41, 0x43, 0x50, 0x75, 0x52, 0x3D, 0x53, 0x3F, 0x73, 0x7A,
		0x72, 0x77, 0x5F, 0x5E, 0x30, 0x7D, 0x73, 0x67, 0x62, 0x62, 0x65, 0x51,
		0x45, 0x63, 0x62, 0x6B, 0x64, 0x6A, 0x65, 0x6A, 0x5F, 0x4A, 0x61, 0x7A,
		0x61, 0x51, 0x58, 0x4A, 0x7D
	};

	DWORD v2, v3;


	srand(0x1000u);
	v2 = rand();
	v3 = v2 % 10 + 10;
	for (int i = 0; i<41; i++)
	{
		if (i & 1)
			putc(v1[i] ^ v3, stdout);
		else
			putc(v1[i], stdout);
	}
}

플래그 입니당 ~~ 꺅 드디어 라업 다 썼네욤
푸는데 도전해 주신분들 감사합니다.

-----