SSD 2025 Author's Write Up - REV

SSD 2025 Write Up (Rev)

읽는데 14 분
글쓴이 TRUST in KDMHS
SSD 2025 Author's Write Up - REV

SSD 2025

Image

Trust, Stealth, Layer7, Unifox, SCA의 5개의 전공 동아리 연합, 학교 대항 SSD CTF가 종료되었다. PWN 5문제, REV 6문제, WEB 6문제, Algorithm 6문제로 이루어졌으며 TrustStealth의 디미고가 최종 승리하였다. (참가한 1학년 다들 수고했어:) 리버싱 문제 중 SOmething WrongEverything Wrong은 내가 출제한 문제로, 라이트업을 작성해보도록 하겠다.

(텍스트 압박 주의)

Somthing Wrong

image

문제 파일을 다운로드 받으면 main 바이너리와 함께 1200개의 .so 파일들이 주어진다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+0h] [rbp-B0h]
  int j; // [rsp+4h] [rbp-ACh]
  void *handle; // [rsp+8h] [rbp-A8h]
  char *v7; // [rsp+10h] [rbp-A0h]
  void (__fastcall *v8)(char *); // [rsp+18h] [rbp-98h]
  char s[8]; // [rsp+20h] [rbp-90h] BYREF
  __int64 v10; // [rsp+28h] [rbp-88h]
  __int64 v11; // [rsp+30h] [rbp-80h]
  __int64 v12; // [rsp+38h] [rbp-78h]
  __int64 v13; // [rsp+40h] [rbp-70h]
  __int64 v14; // [rsp+48h] [rbp-68h]
  __int64 v15; // [rsp+50h] [rbp-60h]
  __int64 v16; // [rsp+58h] [rbp-58h]
  char dest[8]; // [rsp+60h] [rbp-50h] BYREF
  __int64 v18; // [rsp+68h] [rbp-48h]
  __int64 v19; // [rsp+70h] [rbp-40h]
  __int64 v20; // [rsp+78h] [rbp-38h]
  __int64 v21; // [rsp+80h] [rbp-30h]
  __int64 v22; // [rsp+88h] [rbp-28h]
  __int64 v23; // [rsp+90h] [rbp-20h]
  __int64 v24; // [rsp+98h] [rbp-18h]
  unsigned __int64 v25; // [rsp+A8h] [rbp-8h]

  v25 = __readfsqword(0x28u);
  *(_QWORD *)s = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  *(_QWORD *)dest = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0LL;
  v24 = 0LL;
  initialize(argc, argv, envp);
  printf("Input: ");
  __isoc99_scanf("%63s", s);
  if ( strlen(s) != 40 )
    exit(1);
  strncpy(dest, s, 0x28uLL);
  for ( i = 0; i <= 1199; ++i )
  {
    handle = dlopen((&files)[i], 2);
    v7 = dlerror();
    if ( !handle )
    {
      printf("%s", v7);
      puts("Failed to load a library.");
      exit(1);
    }
    v8 = (void (__fastcall *)(char *))dlsym(handle, "func");
    if ( !v8 )
    {
      puts("Failed to load a function.");
      exit(1);
    }
    v8(dest);
    dlclose(handle);
  }
  for ( j = 0; j <= 39; ++j )
  {
    if ( dest[j] != ans[j] )
    {
      puts("Wrong.");
      exit(1);
    }
  }
  puts("Correct!");
  printf("FLAG: %s\n", s);
  return 0;
}

해당 바이너리를 보면 위와 같은데 문자열을 하나 입력 받고 1200개의 .so 파일에서 func 함수를 가져와 암호화한 뒤 ans라는 배열과 비교하는 것을 알 수 있다. .so 파일들의 순서는 바이너리 내에 하드코딩되어 있다.

-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fc4ab248.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:40 fc62f7c1.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fc638d29.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fc8b53ae.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fcfe291b.so
-rwxr-xr-x 1 tuplest tuplest 16792 Aug  9 13:39 fd26f4c0.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fd332938.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:40 fd4a8503.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fda34541.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:40 fe15949c.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fe41a037.so
-rwxr-xr-x 1 tuplest tuplest 16792 Aug  9 13:39 fe680b87.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:41 fe6b5548.so
-rwxr-xr-x 1 tuplest tuplest 17168 Aug  9 13:40 fe9f5b44.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:41 ff3733cd.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:40 ff4fd275.so
-rwxr-xr-x 1 tuplest tuplest 17192 Aug  9 13:41 ffa4d5f2.so
-rwxr-xr-x 1 tuplest tuplest 16792 Aug  9 13:39 fffac8ee.so

해당 .so 파일들을 살펴보면 일정한 사이즈의 3종류가 존재한다는 것을 알 수 있다.

int __cdecl func(char *s)
{
  __int64 v1; // rbx
  __int64 v2; // rbx
  int i; // [rsp+18h] [rbp-48h]
  char tmp[40]; // [rsp+20h] [rbp-40h]
  unsigned __int64 v6; // [rsp+48h] [rbp-18h]

  v6 = __readfsqword(0x28u);
  for ( i = 0; i <= 39; ++i )
    tmp[(i + 9) % 40] = s[i];
  v1 = *(_QWORD *)&tmp[8];
  *(_QWORD *)s = *(_QWORD *)tmp;
  *((_QWORD *)s + 1) = v1;
  v2 = *(_QWORD *)&tmp[24];
  *((_QWORD *)s + 2) = *(_QWORD *)&tmp[16];
  *((_QWORD *)s + 3) = v2;
  *((_QWORD *)s + 4) = *(_QWORD *)&tmp[32];
  return 0;
}

예를 들어 한 .so 파일을 연 후, func 함수를 보면 위와 같이 이루어져 있는데 인자로 들어온 문자열을 오른쪽으로 9칸 씩 미는 연산을 하고 있는 것을 볼 수 있다.

이와 같이 문자열을 인자로 받아 암호화를 가하는 func함수가 add, xor, shr의 세 종류로 존재하는 것을 볼 수 있다. 따라서 어떤 순서로, 어떤 연산을, 어떤 값을 통해 이루어지는지 파악한 후 거꾸로 수행하면 플래그를 구할 수 있다는 것을 유추할 수 있다.

from pwn import *
from capstone import *

def rol(n):
    s[n:] + s[:n]

def sub(n):
    for i in range(len(s)):
        s[i] -= n
        s[i] &= 0xff

def xor(n):
    for i in range(len(s)):
        s[i] ^= n

e = ELF('./main', checksec = False)

strings = e.read(0x9004, 0x5460).split(b'\x00')[:-1]
strings = [s.decode() for s in strings][::-1]

s = [207, 188, 245, 232, 151, 198, 207, 246, 75, 106, 245, 199, 75, 232, 225, 188, 75, 154, 216, 51, 49, 49, 72, 121, 151, 207, 225, 180, 246, 198, 198, 151, 219, 198, 75, 154, 207, 246, 232, 199]

for f in strings:
    md = Cs(CS_ARCH_X86, CS_MODE_64)

    elf = ELF(f, checksec = False)
    func_addr = elf.symbols['func']
    func_size = elf.functions['func'].size
    func_code = elf.read(func_addr, func_size)

    xor_cnt = 0
    for i in md.disasm(func_code, func_addr):
        # print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")

        if i.mnemonic == 'mov' and i.op_str.startswith('dword ptr [rbp - 0x44],'):
            rol(int(i.op_str[24:], 16))
            break
        if i.mnemonic == 'lea' and i.op_str.startswith('ecx, [rax'):
            sub(int(i.op_str[10:11] + i.op_str[12:-1], 16))
            break
        if i.mnemonic == 'xor' and i.op_str.startswith('eax,'):
            xor_cnt += 1
            if xor_cnt < 2: continue
            xor(int(i.op_str[5:], 16) & 0xff)
            break

print(''.join([chr(c) for c in s]))

연산은 모든 항의 똑같이 수행되고, 입력값의 순서가 무작위로 변하는 것이 아닌 오른쪽으로 밀릴 뿐이기에 shr 연산은 역연산할 필요가 없다. main 바이너리에서 하드코딩된 순서를 파싱하여 거꾸로 뒤집어준 후, 파일에서 코드를 추출하여 그에 맞는 역연산을 가함으로써 플래그를 구할 수 있다.

Everything Wrong

문제 파일을 다운로드 받으면 SOmething Wrong 문제와 마찬가지로 main 바이너리와 *.so 파일 1200개가 주어진다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+0h] [rbp-B0h]
  int j; // [rsp+4h] [rbp-ACh]
  void *handle; // [rsp+8h] [rbp-A8h]
  char *v7; // [rsp+10h] [rbp-A0h]
  void (__fastcall *v8)(char *); // [rsp+18h] [rbp-98h]
  char s[8]; // [rsp+20h] [rbp-90h] BYREF
  __int64 v10; // [rsp+28h] [rbp-88h]
  __int64 v11; // [rsp+30h] [rbp-80h]
  __int64 v12; // [rsp+38h] [rbp-78h]
  __int64 v13; // [rsp+40h] [rbp-70h]
  __int64 v14; // [rsp+48h] [rbp-68h]
  __int64 v15; // [rsp+50h] [rbp-60h]
  __int64 v16; // [rsp+58h] [rbp-58h]
  char dest[8]; // [rsp+60h] [rbp-50h] BYREF
  __int64 v18; // [rsp+68h] [rbp-48h]
  __int64 v19; // [rsp+70h] [rbp-40h]
  __int64 v20; // [rsp+78h] [rbp-38h]
  __int64 v21; // [rsp+80h] [rbp-30h]
  __int64 v22; // [rsp+88h] [rbp-28h]
  __int64 v23; // [rsp+90h] [rbp-20h]
  __int64 v24; // [rsp+98h] [rbp-18h]
  unsigned __int64 v25; // [rsp+A8h] [rbp-8h]

  v25 = __readfsqword(0x28u);
  *(_QWORD *)s = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  *(_QWORD *)dest = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  v22 = 0LL;
  v23 = 0LL;
  v24 = 0LL;
  initialize(argc, argv, envp);
  printf("Input: ");
  __isoc99_scanf("%63s", s);
  if ( strlen(s) != 48 )
  {
    puts("Wrong.");
    exit(1);
  }
  strncpy(dest, s, 0x30uLL);
  for ( i = 0; i <= 1199; ++i )
  {
    handle = dlopen((&files)[i], 2);
    v7 = dlerror();
    if ( !handle )
    {
      printf("%s", v7);
      puts("Failed to load a library.");
      exit(1);
    }
    v8 = (void (__fastcall *)(char *))dlsym(handle, "encrypt48");
    if ( !v8 )
    {
      puts("Failed to load a function.");
      exit(1);
    }
    v8(dest);
    dlclose(handle);
  }
  for ( j = 0; j <= 47; ++j )
  {
    if ( dest[j] != ans[j] )
    {
      puts("Wrong.");
      exit(1);
    }
  }
  puts("Correct!");
  printf("FLAG: %s\n", s);
  return 0;
}

main 바이너리는 SOmething Wrong과 구조가 크게 바뀌지 않은 것을 볼 수 있다. 문자열을 하나 입력받고 1200개의 *.so 파일에서 encrypt48 함수를 가져와 임의의 연산을 가한다. 이후 연산이 종료된 입력 문자열을 ans 배열과 비교한 이후 맞으면 Correct!를 다르면 Wrong.을 출력하는 것을 볼 수 있다.

  // .. 생략
  v90 = __readfsqword(0x28u);
  if ( !a1 )
    return 0xFFFFFFFFLL;
  v2 = (unsigned __int8 *)&unk_227C - 28;
  v3 = 0;
  do
  {
    v4 = *v2++;
    v3 = ((int)byte_2388[(v4 - 1) >> 3] >> (-(char)v4 & 7)) & 1 | (2 * v3);
  }
  while ( v2 != (unsigned __int8 *)&unk_227C );
  v5 = (unsigned __int8 *)&unk_227C;
  LODWORD(v6) = v3;
  v7 = 0;
  do
  {
    v8 = *v5++;
    v7 = ((int)byte_2388[(v8 - 1) >> 3] >> (-(char)v8 & 7)) & 1 | (2 * v7);
  }
  while ( (unsigned __int8 *)((char *)&unk_227C + 28) != v5 );
  v9 = (char *)&unk_2000;
  v10 = v7;
  v11 = v81;
  do
  {
    v12 = *v9;
    *(_DWORD *)v11 = 0;
    v6 = (((unsigned int)v6 >> (28 - v12)) | ((_DWORD)v6 << v12)) & 0xFFFFFFF;
    *((_WORD *)v11 + 2) = 0;
    v13 = ((v10 >> (28 - v12)) | (v10 << v12)) & 0xFFFFFFF;
    v10 = v13;
    v14 = v13 | (v6 << 28);
    for ( i = 0LL; i != 48; ++i )
    {
      v16 = v14 >> (56 - byte_2220[i]);
      v17 = i;
      v18 = (int)i >> 3;
      v11[v18] |= (v16 & 1) << (~v17 & 7);
    }
    ++v9;
    v11 += 6;
  }
  while ( v9 != (char *)&unk_2000 + 16 );
  v19 = a1;
  v77 = a1 + 6;
  v20 = a1;
  v21 = &v88;
  v22 = &v87;
  v23 = &v84;
  do
  {
    *(_QWORD *)v21 = 0LL;
    for ( j = 0LL; j != 64; ++j )
    {
      v25 = 1 << (~(_BYTE)j & 7);
      v26 = (char *)v21 + ((int)j >> 3);
      v27 = v25 | *v26;
      v28 = *v26 & ~(_BYTE)v25;
      if ( (((int)*((unsigned __int8 *)v19 + ((byte_2340[j] - 1) >> 3)) >> (-byte_2340[j] & 7)) & 1) != 0 )
        v28 = v27;
      *v26 = v28;
    }
    v78 = v19;
    v80 = v21;
    v29 = v81;
    HIDWORD(v82) = v89;
    v30 = _mm_cvtsi32_si128(v89);
    v79 = v20;
    v31 = v23;
    v32 = v88;
    v33 = v22;
    do
    {
      v85 = 0;
      v34 = 0LL;
      v86 = 0;
      do
      {
        v35 = 1 << (~(_BYTE)v34 & 7);
        v36 = (char *)&v85 + ((int)v34 >> 3);
        v37 = v35 | *v36;
        v38 = *v36 & ~(_BYTE)v35;
        if ( (((int)*((unsigned __int8 *)&v82 + ((byte_22C0[v34] - 1) >> 3) + 4) >> (-byte_22C0[v34] & 7)) & 1) != 0 )
          v38 = v37;
        ++v34;
        *v36 = v38;
      }
      while ( v34 != 48 );
      for ( k = 0LL; k != 6; ++k )
        *((_BYTE *)v33 + k) = v29[k] ^ *((_BYTE *)&v85 + k);
      v76 = v32;
      v40 = v31;
      v41 = 0;
      v42 = 0;
      v43 = 0;
      v44 = v29;
      v45 = 0;
      for ( m = 0; ; v42 = *((_BYTE *)&m + (v41 >> 3)) )
      {
        v46 = 0;
        v47 = v45 + 6;
        do
        {
          v48 = *((unsigned __int8 *)&v87 + (v45 >> 3));
          v49 = v45++;
          v46 = (v48 >> (~v49 & 7)) & 1 | (2 * v46);
        }
        while ( v45 != v47 );
        v50 = byte_2020[64 * (__int64)v43 + (int)(((v46 >> 1) & 0xF) + 16 * ((v46 >> 4) & 2 | v46 & 1))];
        v51 = v41;
        v75 = v50;
        v52 = 3;
        while ( 1 )
        {
          v74 = v51 >> 3;
          v53 = 1 << (~(_BYTE)v51 & 7);
          v54 = v53 | v42;
          v55 = v42 & ~(_BYTE)v53;
          if ( ((v75 >> v52) & 1) != 0 )
            v55 = v54;
          --v52;
          ++v51;
          *((_BYTE *)&m + v74) = v55;
          if ( v52 == -1 )
            break;
          v42 = *((_BYTE *)&m + (v51 >> 3));
        }
        ++v43;
        v41 += 4;
        if ( v43 == 8 )
          break;
        v45 = v47;
      }
      v56 = v44;
      v31 = v40;
      v57 = 0;
      v84 = 0;
      v58 = &unk_22A0;
      v59 = 0;
      v60 = v44;
      while ( 1 )
      {
        v61 = v59 + 1;
        v62 = 1 << (~(_BYTE)v59 & 7);
        v63 = v62 | v57;
        v64 = v57 & ~(_BYTE)v62;
        if ( (((int)*((unsigned __int8 *)&m + (((unsigned __int8)*v58 - 1) >> 3)) >> (-*v58 & 7)) & 1) != 0 )
          v64 = v63;
        ++v58;
        *((_BYTE *)v31 + (v59 >> 3)) = v64;
        if ( v59 == 31 )
          break;
        ++v59;
        v57 = *((_BYTE *)&v84 + (v61 >> 3));
      }
      v65 = _mm_cvtsi128_si32(v30);
      v29 = v56 + 6;
      v32 = v65;
      v66 = _mm_xor_si128(_mm_cvtsi32_si128(v76), _mm_cvtsi32_si128(v84));
      HIDWORD(v82) = _mm_cvtsi128_si32(v66);
      v30 = v66;
    }
    while ( v60 + 6 != (_BYTE *)&v82 );
    v23 = v31;
    v22 = v33;
    v20 = v79;
    v67 = 0LL;
    *v78 = 0LL;
    v21 = v80;
    v87 = _mm_unpacklo_epi32(v66, _mm_cvtsi32_si128(v65)).m128i_u64[0];
    do
    {
      v68 = 1 << (~(_BYTE)v67 & 7);
      v69 = (char *)v78 + ((int)v67 >> 3);
      v70 = v68 | *v69;
      v71 = *v69 & ~(_BYTE)v68;
      if ( (((int)*((unsigned __int8 *)&v87 + ((byte_2300[v67] - 1) >> 3)) >> (-byte_2300[v67] & 7)) & 1) != 0 )
        v71 = v70;
      ++v67;
      *v69 = v71;
    }
    while ( v67 != 64 );
    v19 = v78 + 1;
  }
  while ( v78 + 1 != v77 );
  *v79 ^= 0xF6u;
  for ( n = 1LL; n != 48; ++n )
    v79[n] ^= byte_2380[n & 7];
  return 0LL;
}

기본적으로 하나의 *.so 파일은 위와 같은 구조를 가지고 있다. encrypt48이라는 이름의 함수를 갖고 있는데 이는 분석해보면 48 바이트의 값을 암호화하는 것이라는 걸 알 수 있다. 위 코드의 경우에는 DES 암호화 방식을 통해 암호화하고 있다. 이는 바이너리 내에 존재하는 코드와 상수, 연산들을 보고 파악할 수 있으며 GPT-5 기준 완벽히 분석, GPT-4o 기준 거의 완벽히 분석하는 것을 볼 수 있다. 여기서 함정은 가장 아래 존재하는 반복문과 xor 연산이다. Chat-4o 기준, 해당 연산 때문에 DES 기반 커스텀 암호이라는 오해의 소지(?)가 존재하는 대답을 놓게 된다.

  while ( v78 + 1 != v77 );
  *v79 ^= 0xF6u;
  for ( n = 1LL; n != 48; ++n )
    v79[n] ^= byte_2380[n & 7];
  return 0LL;

따라서 주의해야 할 점은, *.so 파일들이 가하는 연산들이 커스텀 암호가 아닌 DES와 같은 잘 알려진 암호라는 것과 마지막 xor이 추가된다는 것입니다.

.rodata:0000000000002380 ; _BYTE byte_2380[8]
.rodata:0000000000002380 byte_2380       db 0F6h, 0F7h, 0CEh, 61h, 39h, 0E7h, 0Bh, 1Dh
.rodata:0000000000002380                                         ; DATA XREF: encrypt48+5BD↑o
.rodata:0000000000002388 ; unsigned __int8 byte_2388[8]
.rodata:0000000000002388 byte_2388       db 87h, 26h, 45h, 0F7h, 97h, 9Dh, 0E2h, 0DDh
.rodata:0000000000002000 ; _BYTE byte_2000[8]
.rodata:0000000000002000 byte_2000       db 1Fh, 99h, 7Eh, 13h, 3Fh, 2 dup(0F0h), 0FBh
.rodata:0000000000002000                                         ; DATA XREF: LOAD:00000000000000C0↑o
.rodata:0000000000002000                                         ; encrypt48+188↑o
.rodata:0000000000002008 ; unsigned __int8 byte_2008[8]
.rodata:0000000000002008 byte_2008       db 0A5h, 9, 3Dh, 21h, 10h, 2Ch, 54h, 2Fh
.rodata:0000000000002120 ; _BYTE byte_2120[16]
.rodata:0000000000002120 byte_2120       db 29h, 0Dh, 0DAh, 0BAh, 6Dh, 38h, 9Bh, 74h, 8 dup(0)
.rodata:0000000000002120                                         ; DATA XREF: encrypt48+108↑o
.rodata:0000000000002130 xmmword_2130    xmmword 7B252FA62F4781710FBB876A8E07D780h

각 파일들을 분석해보면 크게 3가지 구조의 파일이 존재하는 것을 알 수 있다. 하나는 위에서 언급한 DES ECB 알고리즘이며, 나머지 2가지는 AES ECB 알고리즘과 RC4 알고리즘이다. 해당 파일들을 구분하는 방법에는 여러가지 있지만 그 중에서 파이썬 capstone 라이브러리를 활용해보도록 하겠다. 해당 라이브러리는 기계어 코드를 디스어셈블하는 파이썬 라이브러리로 분석에 문제 자동화에 용이하다. 각각의 구조를 일반화시키기 위해서 어떠한 알고리즘인지에 따라 키값의 주소를 보면 위와 같이 3종류가 나오는 것을 볼 수 있다.

from pwn import *
from capstone import *
from Crypto.Cipher import ARC4, AES, DES

def has_duplicates(lst):
    return 1 if len(lst) != len(set(lst)) else 0

def aes_ecb_decrypt(key: bytes, ct: bytes) -> bytes:
    if len(ct) % 16 != 0:
        raise ValueError("AES-ECB NoPadding: ciphertext length must be a multiple of 16")
    cipher = AES.new(key, AES.MODE_ECB)
    return cipher.decrypt(ct)

def des_ecb_decrypt(key: bytes, ct: bytes) -> bytes:
    if len(ct) % 8 != 0:
        raise ValueError("DES-ECB NoPadding: ciphertext length must be a multiple of 8")
    cipher = DES.new(key, DES.MODE_ECB)
    return cipher.decrypt(ct)

def rc4_decrypt(key: bytes, ct: bytes) -> bytes:
    cipher = ARC4.new(key)
    return cipher.decrypt(ct)

def xor_bytes(data: bytes, key: bytes) -> bytes:
    return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))

e = ELF('./main', checksec = False)

s = bytes([158, 94, 177, 197, 153, 209, 42, 173, 183, 86, 79, 208, 193, 34, 16, 167, 121, 40, 1, 214, 29, 143, 19, 235, 215, 116, 206, 21, 231, 198, 158, 82, 167, 25, 230, 100, 126, 229, 113, 113, 197, 133, 121, 36, 55, 147, 236, 240])

strings = e.read(0x9004, 0x5460).split(b'\x00')[:-1]
strings = [s.decode() for s in strings][::-1]

for f in strings:
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    elf = ELF(f, checksec = False)

    func_addr = elf.symbols['encrypt48']
    func_size = elf.functions['encrypt48'].size
    func_code = elf.read(func_addr, func_size)

    af, df, rf = 0, 0, 0
    for i in md.disasm(func_code, func_addr):
        # print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")
        if i.mnemonic == 'push' and i.op_str == 'r14':
            a_xkey = list(elf.read(0x2120, 8))
            a_key = elf.read(0x2130, 16)

            s = xor_bytes(s, a_xkey)
            s = aes_ecb_decrypt(a_key, s)
            break
        if i.mnemonic == 'push' and i.op_str == 'r15':
            d_xkey = list(elf.read(0x2380, 8))
            d_key = elf.read(0x2388, 8)

            s = xor_bytes(s, d_xkey)
            s = des_ecb_decrypt(d_key, s)
            break
        if i.mnemonic == 'push' and i.op_str == 'rbp':
            r_xkey = list(elf.read(0x2000, 8))
            r_key = elf.read(0x2008, 8)

            s = xor_bytes(s, r_xkey)
            s = rc4_decrypt(r_key, s)
            break
print(s.decode())

최종 익스플로잇 코드는 위와 같다. main 바이너리에서 파일들의 순서가 저장되어 있는 주소를 찾아 pwntools를 통해 파싱할 수 있으며 아까 일반화한 규칙에 따라 3가지 유형의 파일을 구분지을 수 있다. 모든 파일의 암호화 알고리즘은 암호화 -> xor 연산으로 이루어져 있으므로 각 파일의 키값을 파싱하고 거꾸로 역연산하여 플래그를 구해낼 수 있다.

Comment

전체 문제 라이트업

우승한 1학년들 수고했고, 위 링크에 이 글에 작성한 내용을 비롯한 모든 문제 라이트업이 있으니 참고 바란다.

TRUST



팔로우하기