_writeup(Gon-CTF) Run

2022. 3. 26. 14:24카테고리 없음

main 함수에는 인자 2개가 넘어온다. EDI와 RSI를 스택에 복사하는 것으로 알 수 있다.

mov     [rbp+var_54], edi
mov     [rbp+var_60], rsi

void *[4] 배열

structure[0]에는 malloc된 배열의 주소가 들어간다.

structure[2]에는 파일 크기 * 8이 들어간다.

void __fastcall sub_561B39A00B2C(void *structure[4], size_t file_size)
{
  if ( file_size > 0x1FFFFFFFFFFFFFFFLL )
    exit(1);
  *structure = malloc(file_size);
  structure[2] = (void *)(8 * file_size);
  structure[1] = 0LL;
}

structure[1]는 한 비트씩 읽을 때마다 0부터 1씩 증가한다.

bool __fastcall sub_561B39A00AAD(void *structure[4])
{
  int v1; // eax

  if ( structure[1] >= structure[2] )           // validate_process_1
    exit(1);
  v1 = (*((char *)*structure + ((unsigned __int64)structure[1] >> 3)) >> ((_BYTE)structure[1] & 7)) & 1;// 비트를 하나씩 끄집어 냄
  structure[1] = (char *)structure[1] + 1;
  return v1 != 0;
}
v1 = (*((char *)*structure + ((unsigned __int64)structure[1] >> 3)) >> ((_BYTE)structure[1] & 7)) & 1;

이 줄이 굉장히 난해하게 되어있다.

structure[1]은 함수를 한 번 돌때마다 1증가한다.

먼저 structure[1]에 3만큼 오른쪽으로 비트시프트 하라고 되어있으므로 두번째 요소가 8증가할 때마다 1증가한다.

다음으로 structure[1] & 7이므로 structure[1] % 8과 같다.

그러므로 이 줄은 8비트를 받아와서 high bit부터 뽑아내는 기능을 한다.

10010100이 있다면 0 0 1 0 1 0 0 1 순서로 받아오게 된다.

sub_561B39A00C60 (a.k.a encrypt_start)

void __fastcall encrypt_start(void *input[4], void *output[4])
{
  int v2; // eax
  unsigned __int64 settedMSB; // rax
  int j; // [rsp+18h] [rbp-18h]
  int k; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 i; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  while ( input[1] < input[2] )                 // validate_process_1
  {
    for ( i = 0LL; ; ++i )
    {
      LOBYTE(v2) = sub_561B39A00AAD(input);
      if ( v2 )
        break;
      if ( input[1] >= input[2] )
        return;
    }
    if ( i )
    {
      _BitScanReverse64(&settedMSB, i);         // 상위비트 부터 검색하여 1로 설정된 비트가 몇번째 비트인지 반환
      v7 = 64LL - (int)(settedMSB ^ 0x3F);      // v7 = settedMSB + 1
      for ( j = 0; j < v7 - 1; ++j )
        encrypt_process(output, 1);
      encrypt_process(output, 0);
      for ( k = 0; v7 > k; ++k )
      {
        encrypt_process(output, i & 1);
        i >>= 1;
      }
    }
    else
    {
      encrypt_process(output, 0);
      encrypt_process(output, 0);
    }
  }
}

비트를 읽다가 0을 만나면 for문이 종료되고 1의 개수가 i에 들어가게 된다.

_BitScanReverse64(index, mask)

mask의 상위비트부터 검색하여 처음으로 1로 설정된 비트를 만나면 그 자리를 반환한다.

ex. mask가 00 10 00 10 / 01 00 00 00 / 00 11 00 10 / 01 11 10 01 이면, index에는 14가 반환된다.

v7 = 64LL - (int)(settedMSB ^ 0x3F);

settedMSB는 64를 넘을수 없으므로 settedMSB ^ 00111111(0x3F)는 63 - settedMSB와 같다.

64 - (63 - settedMSB) = 1 + settedMSB

v7 = settedMSB + 1;

sub_561B39A0097A (a.k.a encrypt_process)

void __fastcall encrypt_process(void *structure[4], int a2)
{
  if ( structure[1] >= structure[2] )           // validate_process_1
  {
    if ( (unsigned __int64)structure[2] > 0x7FFFFFFFFFFFFFFELL )
      exit(1);
    structure[2] = (void *)(2LL * (_QWORD)structure[2]);// structure[2] << 1
    *structure = realloc(*structure, (size_t)structure[2]);
    if ( !*structure )                          // NULL_guard
      exit(1);
  }
  *((_BYTE *)*structure + ((unsigned __int64)structure[1] >> 3)) &= ~(unsigned __int8)(1 << ((unsigned __int8)structure[1] & 7));
  *((_BYTE *)*structure + ((unsigned __int64)structure[1] >> 3)) |= (a2 != 0) << ((unsigned __int8)structure[1] & 7);
  structure[1] = (char *)structure[1] + 1;
}
*(*structure + (structure[1] >> 3)) &= ~(1 << (structure[1] & 7));
*(*structure + (structure[1] >> 3)) |= (a2 != 0) << (structure[1] & 7);
structure[1] = structure[1] + 1;

이 부분이 핵심이다. ~(1 << (structure[1] & 7))은 structure[1] % 8개 만큼 1비트를 같는 상수를 생성한다. structure[1]이 5이면 0001 1111이 된다.

(a2 != 0)은 a2가 0이 아니면 1, 0이면 0을 반환 하므로 a2가 0이 아니면 (structure[1] % 8) + 1번째 자리가 1로 바뀐다.

정리해보면 첫 줄은 해당 비트를 0으로 초기화하고, 둘째줄은 두번째 인자로 받은 값을 해당 비트에 저장한다

다시 encrypt_start로 돌아가보자

void __fastcall encrypt_start(void *input[4], void *output[4])
{
      // ---중략---
    if ( i )
    {
      _BitScanReverse64(&settedMSB, i);         // 상위비트 부터 검색하여 1로 설정된 비트가 몇번째 비트인지 반환
      v7 = 64LL - (int)(settedMSB ^ 0x3F);      // v7 = settedMSB + 1
      for ( j = 0; j < v7 - 1; ++j )
        encrypt_process(output, 1);
      encrypt_process(output, 0);
      for ( k = 0; v7 > k; ++k )
      {
        encrypt_process(output, i & 1);
        i >>= 1;
      }
    }
    else
    {
      encrypt_process(output, 0);
      encrypt_process(output, 0);t
    }
  }
}

첫 번째 for문은 v7만큼 돈다. 즉, 뒤에서 v7 + 1번째 비트까지 1이 된다.

for문을 나온뒤 v7 + 2번째 비트가 0이된다.

두 번째 for문은 v7 - 1만큼 돈다. i의 비트 값이 v7번 저장된다.

만약 i가 0이라면 output에는 0만 두 번 저장된다.

main

int __fastcall main(int argc, char **argv)
{
  // ---중략---
  encrypt_start(input, output);
  len_file = strlen(argv[1]);
  outfile = (char *)malloc(len_file + 5);
  if ( !outfile )
    exit(1);
  sprintf(outfile, "%s.enc", argv[1]);
  fd_write = open(outfile, 66, 384LL);
  if ( fd_write == -1 )
    exit(1);
  write(fd_write, &input[2], 8uLL);
  sub_561B39A00C08(output, fd_write);
  close(fd_write);
  free(outfile);
  return 0;
}

encrypt_start() 이후 출력파일을 생성하고 처음으로 write하는 부분을 보면, input[2]를 파일의 제일 처음에 기록하는 것을 볼 수 있다.

input[2]는 파일 크기 * 8 이다. 파일의 첫 8바이트는 파일 크기를 의미한다는 것을 기억하자.

sub_561B39A00C08

void __fastcall sub_561B39A00C08(void *output[4], int fd)
{
  while ( ((unsigned __int64)output[1] & 7) != 0 )
    encrypt_process(output, 1);
  write(fd, *output, (unsigned __int64)output[1] >> 3);
}

sub_561B39A00C08()은 8바이트 단위로 맞추기 위해 1로 패딩한 후 전부 write하는 역할을 한다.

Solver 제작

모든 함수가 파악되었으니 solver를 만들어보자!

  1. 데이터는 비트단위로 처리된다.
  2. 비트 단위로 스캔하기 위해서 디컴파일한 함수를 그대로 이용한다.
  3. 비트 단위 출력도 구현되어 있으므로 그대로 사용한다.

데이터는 다음과 같이 암호화되어 있다.

  1. 실제데이터가 0...01일 경우
    LOW ---- HIGH
    개수의 유효 비트 수 0 실제 데이터 0의 개수
  2. 실제 데이터가 1일 경우
    LOW HIGH
    0 0

위 정보를 바탕으로 다음과 같은 solver를 작성할 수 있다.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

void    allocate_structure(void *structure[3], __off64_t size)
{
    *structure = malloc(size);
    if (*structure == NULL)
        exit(1);
    structure[1] = 0;
    structure[2] = (void *)(size * 8);
}

void    read_file(void *structure[3], int fd)
{
    __off64_t size;
    size = lseek(fd, 0, 2);
    allocate_structure(structure, size);
    lseek(fd, 0, 0);
    printf("size : %ld\n", size);
    read(fd, *structure, size);
}

bool    read_bit(void *structure[3])
{
    int v1;

    v1 = (*((char *)*structure + ((__int64_t)structure[1] >> 3)) >> ((__int64_t)structure[1] & 7)) & 1;
    structure[1] = (char *)structure[1] + 1;
    return v1 != 0;
}

void    decrypt_process(void *structure[3], int bit)
{
    *((char *)*structure + ((__int64_t)structure[1] >> 3)) &= ~(__int64_t)(1 << ((__int64_t)structure[1] & 7));
    *((char *)*structure + ((__int64_t)structure[1] >> 3)) |= (bit != 0) << ((__int64_t)structure[1] & 7);
    structure[1] = (char *)structure[1] + 1;
}

void    decrypt_start(void *input[2], void *output[2])
{
    int    i;
    __int64_t    j;
    __int64_t    cnt_one;

    while (input[1] < input[2])
    {
        for (i = 0; ; ++i)
        {
            if (input[1] >= input[2])
                break ;
            if (!read_bit(input))
                break ;
        }
        if (input[1] >= input[2])
                break ;
        if (i != 0)
        {
            cnt_one = 0;
            for (j = 0; j < i + 1; j++)
                cnt_one |= read_bit(input) << j;
            for (j = 0; j < cnt_one; j++)
                decrypt_process(output, 0);
            decrypt_process(output, 1);
        }
        else
        {
            if (!read_bit(input))
                decrypt_process(output, 1);
            else
            {
                decrypt_process(output, 0);
                decrypt_process(output, 1);
            }
        }
    }
}

int    main(int argc, char **argv)
{
    int        fd_input;
    int        fd_output;
    void    *input[3];
    void    *output[3];
    __int64_t    output_size;

    if (argc != 2)
        return (1);
    fd_input = open(argv[1], O_RDONLY);
    read_file(input, fd_input);
    close(fd_input);
    output_size = *(__int64_t *)(*input);
    output[2] = (void *)output_size;
    input[1] = (char *)input[1] + 64;
    allocate_structure(output, output_size / 8);
    decrypt_start(input, output);
    fd_output = open("flag", 66, 384);
    printf("output file opened!\n");
    write(fd_output, output[0], (size_t)output[2] >> 3);
    close(fd_output);
    free(input[0]);
    free(output[0]);
    return (0);
}

read_bit()decrypt_process()는 디컴파일한 소스를 그대로 이용하여 만들었다.

위 프로그램을 이용하면 flag를 얻을 수 있다!