Wyvern (500pts)

CSAW 2015: Wyvern (500pts)

The Wyvern challenge is a program that asks the user to input the dragon’s secret as a string. Most strings will cause the quest to fail (“the dragon’s power, speed and intelligence was greater”), but using the right value will cause the program to reveal a flag.

The program is an x86_64 ELF file made from C++. What’s more, the program has been obfuscated; it has many (many) references to variables called “x[number]” and “y[number]”.

mov    eax,DWORD PTR ds:0x6103e4
mov    ecx,DWORD PTR ds:0x610470
mov    edx,eax
sub    edx,0x1
imul   eax,edx
and    eax,0x1
cmp    eax,0x0
sete   sil
cmp    ecx,0xa
setl   dil
or     sil,dil
test   sil,0x1
jne    4010db <__cxx_global_var_init+0x4b>
jmp    401160 <__cxx_global_var_init+0xd0>

The expression resolves to something like ((x * (x - 1)) & 0x1) == 0 || y < 10. However, upon quick analysis, it appears that x and y variables are never written, and are always zero. Additionally, given that these patterns are found everywhere in the program, including in instantiations of STL templates, it is reasonable to think that the whole program was obfuscated.

The conclusion here is that every time this pattern is discovered, it should be considered that the program takes the first branch, because y is necessarily smaller than 10 (it’s zero).

We decompiled the program and walked through the output and removed the branches as we found them. We identified three important functions: start_quest, transform_input and sanitize_input. We manually cleaned up the three and ended up with this mostly correct translation:

//----- (0000000000401CC0) ----------------------------------------------------
__int64 __fastcall sanitize_input(std::string *a1)
{
  v46 = v7_2;
  v45 = (unsigned int *)(v7_2);
  v44 = (__int64)(v7_4);
  v43 = v7_2;
  v42 = (__int64)(v7_2);
  v41 = v7_2;
  v40 = v7_2;
  v39 = (__int64)(v7_2);
  v38 = (__int64)(v7_4);
  std::vector<int,std::allocator<int>>::vector(v7_4);
  *v43 = 0;

  while ( 1 )
  {
  if ( *v43 >= legend >> 2 )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "success\n");
    *v45 = 0x1337;
    *v41 = 1;
    goto LABEL_64;
  }
  v35 = *v43;
  v1 = std::string::operator[](a1, v35);
  v33 = v1;

  *v42 = *v33;
  std::vector<int,std::allocator<int>>::push_back(v44, v42);

  v2 = v39;
  *v39 = *v43;
  v31 = *v2;
  v29 = std::string::length(a1);
  v3 = v39;

  *v39 = (v29 >> 40) & v31 | 0x1C;
  if ( *v3 != 0 )
  {
    v26 = *v43;
    v25 = std::vector<int,std::allocator<int>>::operator[](hero, v26);
    v23 = *v25;

    std::vector<int,std::allocator<int>>::vector(v38, v44);
    v21 = transform_input(v38);
    std::vector<int,std::allocator<int>>::~vector(v38);

    if ( v23 == v21 )
    {
    v4 = *(_DWORD *)v43;
    v18 = *(_DWORD *)v39;
    v17 = v4;

    v16 = std::vector<int,std::allocator<int>>::operator[](0x6102F8LL, v17);

    *v39 = (*v16 & v18) < 0;
    }
    goto LABEL_46;
  }

  if ( *v39 != 0; )
    break;

  ++*(_DWORD *)v43;
  }

  *v45 = ((unsigned __int16)*v43 << 8) & 0x100;
  *v41 = 1;

LABEL_64:
  std::vector<int,std::allocator<int>>::~vector(v44);
  return *v45;
}


//----- (0000000000404350) ----------------------------------------------------
__int64 __fastcall start_quest(std::string *action)
{
  __int64 v2; // [sp+0h] [bp-90h]@2
  __int64 v3; // [sp+8h] [bp-88h]@13
  unsigned int quest_result; // [sp+34h] [bp-5Ch]@11
  unsigned int sanitized_input; // [sp+48h] [bp-48h]@9
  bool v6; // [sp+4Fh] [bp-41h]@2
  std::string *action_copy; // [sp+50h] [bp-40h]@2
  unsigned int *v8; // [sp+58h] [bp-38h]@2
  __int64 *v9; // [sp+60h] [bp-30h]@2
  __int64 *v10; // [sp+68h] [bp-28h]@2
  std::string *action.0; // [sp+70h] [bp-20h]@1

  v10 = &v2 - 2;
  v9 = &v2 - 2;
  v8 = (unsigned int *)(&v2 - 2);
  action_copy = (std::string *)(&v2 - 2);

  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_100);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_214);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_266);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_369);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_417);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_527);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_622);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_733);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_847);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_942);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1054);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1106);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1222);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1336);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1441);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1540);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1589);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1686);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1796);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1891);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_1996);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2112);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2165);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2260);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2336);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2412);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2498);
  std::vector<int,std::allocator<int>>::push_back(&hero, &secret_2575);

  v6 = std::string::length(action) - 1LL != legend >> 2;

  if ( v6 )
  {
  *v8 = legend >> 2;
  }
  else
  {
  std::string::string(action_copy, action.0);
  sanitized_input = sanitize_input(action_copy);
  *v8 = sanitized_input;
  std::string::~string(action_copy);
  }
  quest_result = *v8;
  return quest_result;
}

//----- (00000000004014B0) ----------------------------------------------------
__int64 __fastcall transform_input(__int64 a1)
{
  *i = 0;

  while ( *v15 < std::vector<int,std::allocator<int>>::size(a1) )
  {
  *i += *std::vector<int,std::allocator<int>>::operator[](a1, *i);;
  ++*i;
  }

  return *i;
}

int main(){ assert(start_quest(user_input) == 0x1337); }

In it, we see that start_quest pushes 28 values to a vector. It then tests that the lenght of the input string is legend >> 2 (which ends up as 28) before calling sanitize_input. This is a rather obvious hint that the “secret” is 28 bytes long.

transform_input, on its end, sums up the values of a vector. sanitize_input, on its end, loops over the string and copies each character to a vector:

  while (1 ) {
    // ...
    v1 = std::string::operator[](a1, v35);
  v33 = v1;
    *v42 = *v33;
  std::vector<int,std::allocator<int>>::push_back(v44, v42);
    // ...
  }

(remember that operator[] returns a reference, which is a pointer at the ABI level)

…until 28 characters have successfully been “tested”, for whatever definition of “test” it uses:

  if ( *v43 >= legend >> 2 )
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "success\n");
    *v45 = 0x1337;
    *v41 = 1;
    goto LABEL_64;
  }

Now, it’s possible that we kind of messed up the condition part, but what it does is still kind of straightforward: it reads a value from the hero vector…

  v25 = std::vector<int,std::allocator<int>>::operator[](hero, v26);
  v23 = *v25;

… copies the current work vector, “transforms” it (does the sum)…

  std::vector<int,std::allocator<int>>::vector(v38, v44);
  v21 = transform_input(v38);
  std::vector<int,std::allocator<int>>::~vector(v38);

… and tests that the transformed value is the same as we have in the hero vector…

  if ( v23 == v21 )
  {
    // ...
  }

Instead of reading more code, we connected the dots here, and noticed that the values of the hero vector are all larger than the one before by an amount that suspiciously looks like an ASCII character value. We extracted each value (which is kind of easy when they’re called secret_[value]) and used a Python script to see what phrase would satisfy hero[i] = phrase[i] + hero[i-1].

  hero = [100, 214, 266, 369, 417, 527, 622, 733, 847, 942, 1054, 1106, 1222,
    1336, 1441, 1540, 1589, 1686, 1796, 1891, 1996, 2112, 2165, 2260, 2336,
    2412, 2498, 2575]
  phrase = ""
  for i in range(len(hero)):
    prev = 0 if i == 0 else hero[i-1]
    phrase += chr(hero[i] - prev)

  print phrase

This prints dr4g0n_or_p4tric1an_it5_LLVM. When used as the secret, the program has the following output:

  $ ./wyvern
  +-----------------------+
  |    Welcome Hero       |
  +-----------------------+

  [!] Quest: there is a dragon prowling the domain.
    brute strength and magic is our only hope. Test your skill.

  Enter the dragon's secret: dr4g0n_or_p4tric1an_it5_LLVM
  success

  [+] A great success! Here is a flag{dr4g0n_or_p4tric1an_it5_LLVM}

Written by Félix Cloutier and solved with Israël Hallé.

Commentaires

comments powered by Disqus