콘텐츠로 건너뛰기

Run

문제 설명

Time is running out.
There’s nowhere to run.
You need to run this program.
So run.
RUN.
NOW

문제 파일

암호화된 flag.enc와 파일을 암호화시키는 main 리눅스 바이너리 실행 파일이 주어진다.

실행

ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@# 내용이 담긴 test.txt 파일과 함께

./main test.txt로 시험삼아 실행시켜보았다.

그랬더니 암호화된 test.txt.enc 파일이 새로 생겼다.

분석

main

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  size_t v3; // rax
  int fd; // [rsp+14h] [rbp-4Ch]
  int fda; // [rsp+14h] [rbp-4Ch]
  char *s; // [rsp+18h] [rbp-48h]
  void *v8[2]; // [rsp+20h] [rbp-40h] BYREF
  __int64 v9; // [rsp+30h] [rbp-30h] BYREF
  void *v10[4]; // [rsp+40h] [rbp-20h] BYREF

  v10[3] = (void *)__readfsqword(0x28u);
  if ( a1 != 2 )
    exit(1);
  fd = open(a2[1], 0, a3);
  if ( fd == -1 )
    exit(1);
  sub_555555400B8F(v8, fd);
  close(fd);
  sub_555555400B2C(v10, 0x1000uLL);
  sub_555555400C60(v8, (__int64)v10);           // v8 = ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#
  v3 = strlen(a2[1]);
  s = (char *)malloc(v3 + 5);
  if ( !s )
    exit(1);
  sprintf(s, "%s.enc", a2[1]);
  fda = open(s, 66, 0600LL);
  if ( fda == -1 )
    exit(1);
  write(fda, &v9, 8uLL);                        // v9 = 38 01 00 00 00 00 00 00
  sub_555555400C08((__int64)v10, fda);          // v10 = AC, 39, CA, D8, AD, 9D, B4, D0...
  close(fda);
  free(s);
  return 0LL;
}

test.txt 파일을 읽어서 암호화시키는 코드가 보인다.

하나씩 함수들을 차례대로 살펴보려고 한다.

sub_555555400B8F

ssize_t __fastcall sub_555555400B8F(void **a1, int a2)
{
  ssize_t result; // rax
  __off_t nbytes; // [rsp+18h] [rbp-8h]

  nbytes = lseek(a2, 0LL, 2);                   // 39
  sub_555555400B2C(a1, nbytes);
  lseek(a2, 0LL, 0);
  result = read(a2, *a1, nbytes);
  if ( nbytes != result )
    exit(1);
  return result;
}

lseek 함수를 통해 매개변수로 가져온 test.txt 파일 크기를 가져온다.

여기서 파일 크기는 39이므로, nbytes가 39가 되겠다.

sub_555555400B2C

void **__fastcall sub_555555400B2C(void **a1, size_t a2)
{
  void **result; // rax

  if ( a2 > 0x1FFFFFFFFFFFFFFFLL )
    exit(1);
  *a1 = malloc(a2);                             // a2 = 39
  a1[2] = (void *)(8 * a2);                     // >>> 39 * 8
                                                // 312
                                                // >>> hex(312)
                                                // '0x138'
  result = a1;
  a1[1] = 0LL;
  return result;
}

nbytes의 값인 39, 즉 a2에 8을 곱해서
a1[2]를 0x138 값으로 지정한다.

a1[1]은 0으로 지정한다.

이러한 값으로 sub_555555400B8F 함수에서

38 01 00 00 00 00 00 00라는 암호된 파일 중 첫 헤더 8byte를 만든다.

sub_555555400C60

unsigned __int64 __fastcall sub_555555400C60(_QWORD *a1, __int64 a2)
{
  unsigned __int64 result; // rax
  unsigned __int64 v3; // rax
  int i; // [rsp+18h] [rbp-18h]
  int j; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 v6; // [rsp+20h] [rbp-10h]
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  while ( 1 )
  {
    result = a1[2];                             // a1[2] = 0x138
    if ( a1[1] >= result )
      break;
    v6 = 0LL;
    while ( !sub_555555400AAD(a1) )             // 2진수 변환?
    {
      result = a1[2];                           // a1[2] = 0x138
      if ( a1[1] >= result )
        return result;
      ++v6;
    }
    if ( v6 ) // v6 = 0, 5, 2, 4, 1, 0, 4, 3, 3, 1, 1, 3, 2, 0, 3, 1, 0, 0, 3,  4, 2, 1, 2, 2, 2, 1, 2, 1, 0, 1, 
    {
      _BitScanReverse64(&v3, v6);
      v7 = 64LL - (int)(v3 ^ 0x3F);             // v3 = 0x02, 0x01, 0x02, 0x0...
                                                // v7 = 3, 2, 3, 1...
      for ( i = 0; i < v7 - 1; ++i )            // i < 2;
                                                // i < 1; 
                                                // i < 2; 
                                                // i < 0;
        sub_55555540097A(a2, 1);
      sub_55555540097A(a2, 0);
      for ( j = 0; v7 > j; ++j )
      {
        sub_55555540097A(a2, v6 & 1);
        v6 >>= 1;
      }
    }
    else
    {
      sub_55555540097A(a2, 0);
      sub_55555540097A(a2, 0);
    }                                           //v6 = 0 -> 00
                                                //v6 = 5 -> 101 0 11 00 (0xAC)
                                                ...
                                                // v6 = 0, -> 0
                                                // v6 = 5, -> 0xAC
                                                // v6 = 2, -> 0xAC 0x09
                                                // v6 = 4, -> 0xAC 0x39 0x02
                                                // v6 = 1, -> 0xAC 0x39 0x0A
                                                // v7 = 0, -> 0xAC 0x39 0x0A
                                                // v7 = 4, -> 0xAC 0x39 0xCA 0x08...
  }
  return result;
}

본격적으로 암호회시키는 함수라고 볼 수 있겠다.

sub_555555400AAD 함수는 하나의 문자, 아스키코드를 2진수로 변환한다. (아래 코드 참고)

v6는 특정한 규칙을 통해 값을 가지고 있는데

0x41 = 0 1 0 0 0 0 0 1 -> 0
0x42 = 0 1 0 0 0 0 1 0 -> 5
0x43 = 0 1 0 0 0 0 1 1 -> 2
0x44 = 0 1 0 0 0 1 0 0 -> 4
0x45 = 0 1 0 0 0 1 0 1 -> 1
0x46 = 0 1 0 0 0 1 1 0 -> 0
0x47 = 0 1 0 0 0 1 1 1 -> 4

… 0x43, 0x42, 0x41
… 0 1 0 0 0 0 1 1 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 1
오른쪽부터 1 찾기!
다음 1까지의 거리

다음 1까지 0칸
다음 1까지 5칸
다음 1까지 2칸
다음 1까지 4칸
다음 1까지 1칸
다음 1까지 0칸
다음 1까지 4칸

이렇게 다음 1까지 거리를 나타낸다.

v6가 0이 아닌 경우, 일종의 XOR 연산과 함께 반복문이 진행되는 것을 볼 수 있는데,
_BitScanReverse64라고 하는 함수는 bsr 어셈블리 명령에 의해 생성된 것이다.

이를 테면,

bsr rax, [rbp+var_10]에서
[rbp+var_10]이 5인 경우,
2진수로 나타내면 101이고,
오른쪽에서부터 2번쨰 자리에 위치하고 있기 때문에
rax = 2

bsr rax, [rbp+var_10]에서
[rbp+var_10]이 3인 경우,
2진수로 나타내면 11이고,
오른쪽에서부터 1번쨰 자리에 위치하고 있기 때문에
rax = 1

bsr rax, [rbp+var_10]에서
[rbp+var_10]이 1인 경우
2진수로 나타내면 1이고,
오른쪽에서부터 0번쨰 자리에 위치하고 있기 때문에
rax = 0이다.

sub_555555400AAD

// *a1 = ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#
// a1[1] = 0 (increase later)
// a1[2] = 0x138 (39 * 8)
_BOOL8 __fastcall sub_555555400AAD(_QWORD *a1)
{
  int v1; // eax

  if ( a1[1] >= a1[2] )
    exit(1);
  v1 = (*(char *)(*a1 + (a1[1]++ >> 3)) >> (a1[1] & 7)) & 1;// v1 = 
                                                // (*a1[0 >> 3] >> (0 & 7)) & 1 = (0x41 >> 0) & 1 = 1, 
                                                // (*a1[1 >> 3] >> (1 & 7)) & 1 = (0x41 >> 1) & 1 = 0,
                                                // (*a1[2 >> 3] >> (2 & 7)) & 1 = (0x41 >> 2) & 1 = 0,
                                                // (*a1[3 >> 3] >> (3 & 7)) & 1 = (0x41 >> 3) & 1 = 0,
                                                // (*a1[4 >> 3] >> (4 & 7)) & 1 = (0x41 >> 4) & 1 = 0,
                                                // (*a1[5 >> 3] >> (5 & 7)) & 1 = (0x41 >> 5) & 1 = 0,
                                                // (*a1[6 >> 3] >> (6 & 7)) & 1 = (0x41 >> 6) & 1 = 1,
                                                // (*a1[7 >> 3] >> (7 & 7)) & 1 = (0x41 >> 7) & 1 = 0,
                                                // 
                                                // (*a1[8 >> 3] >> (8 & 7)) & 1 = (0x42 >> 0) & 1 = 0,
                                                // (*a1[9 >> 3] >> (9 & 7)) & 1 = (0x42 >> 1) & 1 = 1,
                                                // (*a1[10 >> 3] >> (10 & 7)) & 1 = (0x42 >> 2) & 1 = 0,
                                                // (*a1[11 >> 3] >> (11 & 7)) & 1 = (0x42 >> 3) & 1 = 0,
                                                // (*a1[12 >> 3] >> (12 & 7)) & 1 = (0x42 >> 4) & 1 = 0,
                                                // (*a1[13 >> 3] >> (13 & 7)) & 1 = (0x42 >> 5) & 1 = 0,
                                                // (*a1[14 >> 3] >> (14 & 7)) & 1 = (0x42 >> 6) & 1 = 1,
                                                // (*a1[15 >> 3] >> (15 & 7)) & 1 = (0x42 >> 7) & 1 = 0,
                                                // ...
  return v1 != 0;
}

sub_55555540097A

a2에 값이 1이 세트되면 8진수 방식에서 1이 뒤에서부터 하나씩 추가되고,
a2에 값이 0이 세트되면 8진수 방식에서 0이 뒤에서부터 하나씩 추가된다고 보면 된다.

사실 조금 더 명확히 말하자면,
해당 비트를 0으로 초기화하고,
두번째 인자로 받은 값을 해당 비트에 저장한다고 한다. (링크 참고)

https://k2zoo.tistory.com/m/8

sub_555555400C60 함수에서 마지막 줄 쯤에 있는 주석 참고바람

__int64 __fastcall sub_55555540097A(__int64 a1, int a2)
{
  __int64 result; // rax

  if ( *(_QWORD *)(a1 + 8) >= *(_QWORD *)(a1 + 16) )// 1 >= 0x8000 (false), a2 = 0
                                                // 2 >= 0x8000 (false), a2 = 1
  {
    if ( *(_QWORD *)(a1 + 16) > 0x7FFFFFFFFFFFFFFEuLL )
      exit(1);
    *(_QWORD *)(a1 + 16) *= 2LL;
    *(_QWORD *)a1 = realloc(*(void **)a1, *(_QWORD *)(a1 + 16));
    if ( !*(_QWORD *)a1 )
      exit(1);
  }
  *(_BYTE *)(*(_QWORD *)a1 + (*(_QWORD *)(a1 + 8) >> 3)) = ~(unsigned __int8)(1 << (*(_QWORD *)(a1 + 8) & 7)) & *(_BYTE *)((*(_QWORD *)(a1 + 8) >> 3) + *(_QWORD *)a1);// 
                                                // *a1[1 >> 3]
                                                // = *a1[0] 
                                                // = ~(1<< (1 & 7)) & *a1[1 >> 3]
                                                // = ~(1<< (1 & 7)) & *a1[0]
                                                // = ~(1<< (1 & 7)) & 0
                                                // = 0
                                                // *a1[2 >> 3]
                                                // = *a1[0] 
                                                // = ~(1<< (2 & 7)) & *a1[2 >> 3]
                                                // = ~(1<< (2 & 7)) & *a1[0]
                                                // = ~(1<< (2 & 7)) & 0
                                                // = 0
  *(_BYTE *)(*(_QWORD *)a1 + (*(_QWORD *)(a1 + 8) >> 3)) = ((a2 != 0) << (*(_QWORD *)(a1 + 8) & 7)) | *(_BYTE *)((*(_QWORD *)(a1 + 8) >> 3) + *(_QWORD *)a1);// *a1[1 >> 3]
                                                // = *a1[0] 
                                                // = ((0) << (1&7)) | *a1[1 >> 3]
                                                // = ((0) << (1&7)) | 0
                                                // = 0 
                                                // *a1[2 >> 3]
                                                // = *a1[0] 
                                                // = ((1) << (2&7)) | *a1[2 >> 3]
                                                // = ((1) << (2&7)) | 0
                                                // = 4
                                                // ...
                                                // = 0xC,
                                                // = 0xC,
                                                // = 0x2C,
                                                // = 0x2C,
                                                // = 0xAC,
  result = a1;
  ++*(_QWORD *)(a1 + 8);                        // -> 2
                                                // -> 3
  return result;
}

암호화

따라서 암호화하는 파이썬3 코드를 작성하자면 다음과 같이 작성할 수 있었다.

복호화시키는 코드는 나에게 너무나도 난해한 수준이어서, 여기까지 시도하고 넘어가려고 한다.

bin_arr = []
enc_arr = []

enc_bin_arr = []

def read_file(filename):
    with open(filename, 'rb+') as file:
        data = file.read()
    return data

def save_file(filename, data):
    with open(filename, 'wb+') as file:
        file.write(data)

def prepend_arr(lst, element):
    return [element] + lst

def BitScanReverse64(x):
    if x == 0:
        return -1
    position = 0
    while x > 0:
        x >>= 1
        position += 1

    return position - 1

# len(filecontext) = 39
#38 01 00 00 00 00 00 00
# sub_555555400B2C: get file size and multiply with 8
def sub_555555400B2C(filecontext):
    _len = len(filecontext) * 8
    array = [((_len >> (8 * i)) & 0xFF) for i in range(8)]
    for i in range(8):
        enc_arr.append(array[i])

# sub_555555400AAD: byte to bin
def make_bin_arr():
    _bin_arr = []
    _reversed_filecontext = filecontext[::-1].copy()
    for char in _reversed_filecontext:
        ascii_value = char  # 문자의 ASCII 값을 얻습니다.
        binary_representation = format(ascii_value, '08b')  # 8자리의 2진수로 변환합니다.
    
        _bin_arr.extend([int(bit) for bit in binary_representation])

    return _bin_arr[::-1]

# return how much iterate...
def get_distance_one_in_bin_arr(_bin_arr):
    distance_arr = []

    distance = 0
    for i in range(len(_bin_arr)):
        if _bin_arr[i] == 1:
            distance_arr.append(distance)
            distance = 0
            continue
        distance += 1
    return distance_arr



filename = 'test.txt'
filecontext = read_file(filename)
filecontext = list(filecontext)

sub_555555400B2C(filecontext)

bin_arr = make_bin_arr()
distances = get_distance_one_in_bin_arr(bin_arr)

for i in range(len(distances)):
    if distances[i]:
        v6 = distances[i]
        v3 = BitScanReverse64(v6)
        v7 = 64 - (v3 ^ 0x3f)
        for j in range(v7-1):
            enc_bin_arr.append(1)
        enc_bin_arr.append(0)
        for k in range(v7):
            enc_bin_arr.append(v6 & 1)
            v6 = v6 >> 1

    else:
        enc_bin_arr.append(0)
        enc_bin_arr.append(0)

print(len(enc_bin_arr))
#padding
for i in range(8 - len(enc_bin_arr) % 8):
    if len(enc_bin_arr) % 8 == 0:
        break
    enc_bin_arr.append(1)

#bin to hex
enc_bin_arr = enc_bin_arr[::-1].copy()
# 리스트를 8비트씩 끊어서 2진수 문자열로 변환
binary_strings = [''.join(map(str, enc_bin_arr[i:i+8])) for i in range(0, len(enc_bin_arr), 8)]
# 2진수 문자열을 16진수로 변환하고 0x 형태로 출력
hex_values = [int(binary_str, 2) for binary_str in binary_strings]
hex_values = hex_values[::-1]
# 결과 출력
for hex_value in hex_values:
    enc_arr.append(hex_value)

save_file(filename + ".enc", bytearray(enc_arr))
태그:

답글 남기기