Search code examples
cassemblyreverse-engineeringida

fgets exploit - reverse engineering with IDA


I'm currently doing an exercise, where I have to find a way to "pass" the level (It's a reverse engineering exercise, I decompiled it with IDA).

The level function consists of 3 while loops, from each I have to break to get to the next one. To pass the level, I have to input something that will pass through the 3 checks. Code is as follows:

while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        memset(Buffer, 0, sizeof(Buffer));
        stream = _acrt_iob_func(0);
        fgets(Buffer, 1020, stream);
        if ( strlen(Buffer) >= 0x1E )
          break;
      }
      if ( first_check_string(Buffer) )
        break;
    }
    if ( second_check_string(Buffer) )
      break;
  }
  return printf(aWellDone, Buffer[0]);
}

The first_check_string function:

int __cdecl first_check_string(_BYTE *str_addr)
{
  while ( *str_addr )
  {
    if ( (char)*str_addr < 97 || (char)*str_addr > 122 )
      return 0;
    if ( ((char)*str_addr - 97) % 3 )
      return 0;
    ++str_addr;
  }
  return 1;
}

The second_string_check function:

BOOL __cdecl second_check_string(char *Str)
{
  int v2; // [esp+4h] [ebp-8h]
  char *i; // [esp+8h] [ebp-4h]

  if ( strlen(Str) % 4 )
    return 0;
  v2 = 0;
  for ( i = Str; *(_DWORD *)i; i += 4 )
    v2 ^= *(_DWORD *)i;
  return v2 == 1970760046;
}

For the first if, i just have to enter a string longer than 1E, or 30 in decimal.

The second, I have to enter only a-z character, but only ones that their ascii - 97 is divisible by 3. So only options are: a, d, g, j, m, p, s, v, y.

But there's a catch - I have to enter at least 1019 characters, since otherwise, the fgets will get the newline 0x0A character, and the first_check_string function will not pass.

So I enter 1019 "a"s for example.

I pass the first 2 ifs. But the 3rd if function second_check_string requires my string to be divisble by 4, which can't be if I enter 1019 chars. And if I enter less, the first_check_string function will encounter the 0x0A newline char and return 0.

If anyone got any idea of how could I approach this, I would be grateful.

GENERALIZED SOLUTION

To enter a NUL 0x00 byte, we need to redirect the program to read from a file instead from the user's keyboard. To do so, we execute the program as follows: prog.exe < file this makes the standard input, which fgets uses, become the file. In the file we can any bytes we want, including the NUL character. We can do so either by using a programming language to write to that file, or a hex editor (I used 010 editor).

Cheers to EVERYONE who helped me!


Solution

  • Kudos to @Shadowranger for noting the that a strategic \0 simplifies the problem immensely!

    The following is a minor adaptation of the code given in the original problem.

    int first_check_string( char *cp ) {
        while ( *cp ) {
            if( !islower( *cp ) ) // 'a'- 'z'
                return 0;
            if ( (*cp - 'a') % 3 ) // but only every third of those pass muster
                return 0;
            ++cp;
        }
        puts( "Passed 1st check" );
        return 1;
    }
    
    bool second_check_string(char *Str) {
        int v2; // [esp+4h] [ebp-8h]
        char *i; // [esp+8h] [ebp-4h]
    
        if ( strlen(Str) % 4 )
            return 0;
        v2 = 0;
        for ( i = Str; *(uint32_t *)i; i += 4 )
            v2 ^= *(uint32_t *)i;
    
        printf( "Aiming for %08X... Got %08X\n", 1970760046, v2 );
        return v2 == 1970760046;
        // Hides 0x7577696E as a decimal value
        // ASCII decoding: 0x75-'u', 0x77-'w', 0x69-'i', 0x6E-'n' ==> "uwin"... :-)
    }
    
    int main() {
        char Buffer[1020] = {
            'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
            'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
            'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
            'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a',
              0,   0,   0, 'd',   0,   0,   0, 'd',
            'n', 'i', 'w', 'u',   0,   0,   0,   0, // 7577696E = 'u', 'w', 'i', 'n';
        };
    
        while( 1 ) {
            while ( 1 ) {
                while ( 1 ) {
                    /* Using compile-time array instead of loading binary file */
                    if ( strlen(Buffer) >= 0x1E )
                        break;
                }
                if ( first_check_string(Buffer) )
                    break;
            }
            if ( second_check_string(Buffer) )
                break;
            else {
                puts( "Failed 2nd check" );
                getchar();
            }
        }
        puts( "Well Done" );
    
        return 0;
    }
    
    Passed 1st check
    Aiming for 7577696E... Got 7577696E
    Well Done
    

    The 1st 32 bytes followed by 0 satisfy the minimum string length. (The compile time array skirts the OP problem of reading up to 1020 bytes, with an embedded NULL, from a file. The effect is the same, regardless.)

    The XOR of those (even quantity) 'a' characters results in zero; a clean start for more processing.

    The XOR of bytes 32-35 (treated as an unsigned int) with the next 4 bytes means that v2 is still zero...

    Finally, hidden behind that NULL (thanks Shadowranger), and all those balancing XOR's, is the literal unsigned integer (32 bits) that is the key to matching. Note that 'endian-ness' comes into play, and the "u win" message must be reversed (on my hardware).

    And the next 4 NULL bytes will terminate the 2nd check, so anything in the buffer beyond that is ignored...

    Good fun!