0

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!

nortain32
  • 69
  • 1
  • 7

2 Answers2

2

Input a manual NUL character, '\0' at one past a multiple of 4 offset (so the apparent string length is a multiple of 4). fgets will happily retrieve it as part of a string without stopping, but your tests using C-style string definition will stop at the NUL, and never see a following newline, nor any other character violating the rules being checked. This dramatically relaxes the restrictions; make it long enough to pass the basic break in the innermost loop, then put a NUL after a multiple of four characters has been entered, and after that you can enter anything for the remainder of the 1019 characters because the rules won't be checked past the NUL.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • umm, I tried that, but I can't find a way to enter a `NUL` character 0x00 manually. Obviously If i enter `\0` it will just input the ascii codes for ```\``` and ```0```. And copy pasting the `NUL` character also doesn't work, it just copies a question mark. It's a console, so the input is kinda weird... – nortain32 Dec 08 '22 at 22:51
  • Try redirecting input from a file. – Retired Ninja Dec 08 '22 at 23:03
  • what do you mean? The fgets uses the standard input (I guess). How can I redirect input from a file? – nortain32 Dec 08 '22 at 23:14
  • @nortain32: `myprogram < filewithinput`, works in most terminals (both Windows and UNIX-like). If this is a UNIX-like, [there are hot keys for entering arbitrary character codes](https://stackoverflow.com/q/7572801/364696). [This question claims `Ctrl+Spacebar` is fairly portable too](https://stackoverflow.com/q/9124786/364696) (never tried it). – ShadowRanger Dec 08 '22 at 23:51
  • i dont think that works :/ The fgets just reads the plain, raw text. `myprogram.exe < filewithinput.txt` translates to `6D 79` etc... – nortain32 Dec 09 '22 at 00:00
  • @nortain32: Yeah, you have to write an actual raw `NUL` to the file in any way you like. This is one of the most basic things you can do with hacking, you need to learn at least one way to produce arbitrary binary data. Example, on a UNIX-like, you can do stuff like `echo -e '\x00abc123'` and the first byte output will be `NUL` (`\x00` is the hex escape for it, `echo -e` says to interpret escapes). – ShadowRanger Dec 09 '22 at 00:05
  • Yeah, the problem is I'm not doing this on linux (the exercise is just an exe file). I have no idea on how to write a raw NUL in the console, so the fgets actually read 0x00. – nortain32 Dec 09 '22 at 00:15
  • @nortain32: If you can't figure out any other option, write a program in programming-language-of-choice that just writes the bytes you want to a file. `\x00` is the escape for a `NUL` in most such languages. You need to learn to figure this stuff out on your own; you can't become a reverse engineer or hacker without being able to do some research on your own. – ShadowRanger Dec 09 '22 at 00:41
  • I've worked out the solution and I really must say that whoever designed this problem has a wicked sense of humour! `:-)`... Will polish up the code (with some comments) and post it as an answer soon... It made me laugh out loud!... Worth the struggle! `:-)` – Fe2O3 Dec 09 '22 at 00:46
1

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!

Fe2O3
  • 6,077
  • 2
  • 4
  • 20
  • I understand everything u said, but one thing is still bothering me - how would type in the NUL character in the input? I've searched and didn't find a way to enter a NUL character in the console (which pops out when running the program). I understand some people said to redirect it to a file containing the NUL bytes, but how would I do that? anything I write, translates to it's ascii. – nortain32 Dec 09 '22 at 05:58
  • Another quick way to create such a file would be to adapt the code above to `fwrite()` the 1020 bytes of this `Buffer` array into a file opened for writing as 'binary'... Then, as suggested by @Retired Ninja, use command line redirect (ie: ' `prog.exe < file ' `) to connect that file as the stdin of the original code that wants to load from 'stdin'... – Fe2O3 Dec 09 '22 at 06:26
  • Okay, i'll try that when I get home. But I got a question. if I redirect it to the file, how do I redirect it to the standart console input again? – nortain32 Dec 09 '22 at 07:48
  • `./prog.exe < file`... The shell will run the program but the contents of 'file' will be fed into the program as **its** standard input (instead of keyboard entry.) When the program ends, that redirection will also end. There's nothing _persistent_ about this. – Fe2O3 Dec 09 '22 at 07:51
  • Oh, so mean I should rin the program from the command line, like you said, instead of just double clicking the exe? And when i run this command, the redirection is valid only for the same run. Have I understood you correctly? – nortain32 Dec 09 '22 at 08:03
  • Yes... That is how you can perform the redirection... That `stream = _acrt_iob_func(0);` in your code is unknown to me, but I suspect it is a low-level version of referring to stdin... – Fe2O3 Dec 09 '22 at 08:07
  • Yeah, I checked and it's standard input. It's in the library. – nortain32 Dec 09 '22 at 08:12