Padocon CTF 2011 Binary500 Writeup (300)

The challenge consists of a windows binary and connection details:
HOST : 168.188.130.216
PORT : 888
Binary (Daemon.exe)

Summary: reversing the algorithm with some encryption and coding a client

Quick analysis shows that the binary binds to the 888 port, creates thread, which accepts connections and creates threads for them. Finally, the handler function is sub_4015B0. Let’s look what IDA says about it:

int __stdcall sub_4015B0(int a1)
{
  int v1; // eax@1
  int v2; // eax@1
  int v3; // eax@3
  int v4; // eax@4
  int v5; // eax@6
  int v7; // eax@12
  char v8; // [sp+Ch] [bp-188h]@1
  int v9; // [sp+50h] [bp-144h]@4
  char buf[128]; // [sp+D4h] [bp-C0h]@1
  char Str2; // [sp+154h] [bp-40h]@4
  int v12; // [sp+188h] [bp-Ch]@1
  int v13; // [sp+18Ch] [bp-8h]@1

  memset(&v8, -858993460, 0x188u);
  v13 = 0;
  v12 = a1 - 1;
  inet_ntoa(*(struct in_addr *)(dword_427B08[a1 - 1] + 8));
  v1 = _chkesp();
  printf("%d loged ! ( %s ) ", v12, v1);
  printf("current user is %d \n", dword_427C04);
  sprintf(buf, "Can you solve this Pwd?!\n");
  v2 = strlen(buf);
  send(*(_DWORD *)dword_427B08[v12], buf, v2, 0);
  _chkesp();
  while ( 1 )
  {
    sub_401005(buf);
    if ( v13 == 100 )
    {
      memset(buf, 0, 0x80u);
      sprintf(buf, "úë-M¦qÿ¦\vƦ");
      printf("%s\n", buf);
      v3 = strlen(buf);
      send(*(_DWORD *)dword_427B08[v12], buf, v3, 0);
      _chkesp();
      goto LABEL_12;
    }
    v4 = strlen(buf);
    send(*(_DWORD *)dword_427B08[v12], buf, v4, 0);
    _chkesp();
    memset(&Str2, 0, 0x32u);
    recv(*(_DWORD *)dword_427B08[v12], &Str2, 50, 0);
    v9 = _chkesp();
    printf("recv N : %d\n", v9);
    printf("%d : %s\n", v13, &Str2);
    if ( strcmp(Dest, &Str2) )
      break;
    puts("right!!");
    ++v13;
    if ( v9 == -1 || !v9 )
      goto LABEL_12;
    printf("%s\n", &Str2);
  }
  memset(buf, 0, 0x80u);
  sprintf(buf, "No You Wrong!!!");
  v5 = strlen(buf);
  send(*(_DWORD *)dword_427B08[v12], buf, v5, 0);
  _chkesp();
LABEL_12:
  inet_ntoa(*(struct in_addr *)(dword_427B08[v12] + 8));
  v7 = _chkesp();
  printf("%d closed! (%s)\n", v12, v7);
  sub_40103C(v12);
  return _chkesp();
}

Rather messy, but easy to see, we have to send some right strings 100 times in a row, or we’ll be disconnected. Received strings are compared with the one called Dest. Let’s look references to it:

sub_401310+61  push    offset Dest     ; Dst 
sub_401310+C3  push    offset Dest     ; Dest
sub_4015B0+1F0 push    offset Dest     ; Str1

In sub_4015B0 there’s only strcmp, so it’s modified only in sub_401310 (if there are no obfuscation tricks). If we now look for references to this function, we’ll see that it is wrapped by sub_401005, which is called from that handler function! Luckily, don’t?

So, here is sub_401310 with some renamed variables:

int __cdecl sub_401310(void *Dst)
{
  char v2; // [sp+Ch] [bp-5Ch]@1
  char buf[16]; // [sp+4Ch] [bp-1Ch]@1
  char v4; // [sp+5Ch] [bp-Ch]@1
  int i; // [sp+60h] [bp-8h]@1
  int j; // [sp+64h] [bp-4h]@1

  memset(&v2, -858993460, 0x5Cu);
  j = rand() % 32;
  memset(Dst, 0, 4u);
  memset(selected_string, 0, 20u);
  memset(buf, 0, 0x11u);
  memset(Dest, 0, 0x19u);
  v4 = 0;
  byte_427BE0 = 0;
  for ( i = 0; i < 16; ++i )
    selected_string[i] = *(&strings_array[17 * j] + i);
  selected_string[i] = 0;
  strcat(Dest, &strings_array[17 * j]);
  printf("im pwd: %s\n", selected_string);
  for ( j = 0; j < 16; ++j )
    buf[j] = selected_string[j];
  printf("im code: %s\n", buf);
  for ( i = 0; i < 127; ++i )
    sub_401023(buf);
  for ( i = 0; i < 16; ++i )
    *((_BYTE *)Dst + i) = buf[i];
  *((_BYTE *)Dst + i) = 0;
  return _chkesp();
}

, where strings_array is:

db 'Play with IGRUS!',0
db 'HOW ABOUT CRACK?',0
db '#_#(+_+)#_#(@_@)',0
...

Here, a 16-bytes string is randomly selected from the array, then it’s moved from one buffer to another (including Dest – the string which we must know), probably modified (encrypted?) in sub_401023 and copied to buffer, which will be sent to the client. Looks like we have to reverse the encryption algorithm and then just decrypt strings from the server (there’s another way – we can encrypt each string and then just compare them; but it seemed to me it hadn’t worked – the strings probably were changed, also the key is sent in the encrypted form).

The encryption is not very complicated: it consists of replaces done by sub_401E60 (wrapped into sub_401019) and some permutations in sub_401CE0 (wrapped into sub_401023). Let’s look at replaces:

int __cdecl sub_401E60(int a1)
{
  char v2; // [sp+Ch] [bp-48h]@1
  char v3; // [sp+4Ch] [bp-8h]@3
  int i; // [sp+50h] [bp-4h]@1

  memset(&v2, -858993460, 0x48u);
  for ( i = 0; i < 16; ++i )
  {
    v3 = *(&byte_426C50[16 * (unsigned int)selected_string[i] >> 4)]
                                      + (selected_string[i] & 0xF));
    *(_BYTE *)(i + a1) = v3;
  }
  return _chkesp();
}

Look at it carefully: that line is equivalent to

v3 = byte_426C50[(unsigned int)selected_string[i]];

and it’s just an S-box replace! Every char is replaced with a value from some table, the index in the table is the ascii code of the char. We should dump that table:

0x26C50 = 158800

$ dd bs=1 if=daemon.exe of=table skip=158800 count=256

OK, table saved. Permutations are simple:

  v12 = *(_BYTE *)(a1 + 9);
  v11 = *(_BYTE *)(a1 + 1);
  *(_BYTE *)(a1 + 1) = *(_BYTE *)(a1 + 5);
  v10 = *(_BYTE *)(a1 + 13);
  *(_BYTE *)(a1 + 5) = v12;
  ...

We just copy them line by line from the end, with a switched parts around ‘=’. I did a small script for that, but sadly I’ve lost it.

If the encryption is like:

for (i = 0; i < 127; i++) {
  replaces();
  permutations();
}

Then, the decryption is:

for (i = 0; i < 127; i++) {
  reversed_permutations();
  reversed_replaces();
}

Ok, now we are ready to code the client! Here is my script for solving the task:

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import socket

def decrypt(string):

    s = list(string)
    
    for n in range(127):
    
        v5 = s[3];
        v3 = s[7];
        i = s[11];
        s[3] = v3;
        v6 = s[15];
        s[7] = i;
        v7 = s[6];
        s[15] = v5;
        s[6] = s[14];
        v8 = s[10];
        s[11] = v6;
        v9 = s[2];
        s[14] = v7;
        v10 = s[9];
        s[2] = v8;
        v11 = s[13];
        s[10] = v9;
        v12 = s[5];
        s[13] = v10;
        s[5] = s[1];
        s[1] = v11;
        s[9] = v12;
        
        for str_index, str_char in enumerate(s):
            for table_index, table_char in enumerate(table):
                if (table_char == str_char):
                    break;
            s[str_index] = chr(table_index);

    return "".join(s)

f = open("table", "r")
table = f.read()
f.close()

fs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
fs.connect(("172.16.14.128", 888))

print fs.recv(25)  # Can you solve this Pwd?!

counter = 0

while True:
    s = fs.recv(16)
    print counter, "S:", repr(s)
    d = decrypt(s)
    print counter, "D:", d
    
    fs.send(d)
    
    counter += 1

Run it:

$ py bin500solve.py 
Can you solve this Pwd?!

0 S: '/\x16\xc3\x0b\x9d\xb9\x16\x16\xaf\x7f\x13\x16\x16\xc3\xc7\xb9'
0 D: (-_-)v hi!!~~~~~
1 S: 'J\x89/\xb1\xe9\x7f\x12\x9d\xaf\xc7j\xaf3\xf1z\xaf'
1 D: >void main(){pri
2 S: '\xef\x13\xef\x9d\xfa\x13\xc9/\xef\x13\xef\x9d\xc9\x13\xfa/'
2 D: #_#(+_+)#_#(@_@)
3 S: '\xc5x\xb0\x1e\x1e\x11$\n\xcd\xa6\x92$\xe6\xc7\xf0\xc7'
3 D: HOW CRACK IS?|
...
100 S: '\xa3\x89\xcdM\xc7q\x98\xb1\x0b\x92\xb1\xf3\x8f\xc7\xaf\x98'
100 D: Play With Raspyx

The flag is: Play With Raspyx
Note: obviously, we could got this key just by decrypting the appropriate string. But this is hardcoded key, the real challenge daemon was patched and contained another key

3 comments

1 ping

    • alopex on January 21, 2011 at 23:48
    • Reply

    Actually, any key within the binary would of worked. You could solve the challenge just by running strings on the binary. Good write up though. I solved it in the same way too.

  1. Are you sure about *any* key? I remember that I tried some of that strings and they were rejected. I suppose it was a mistake ( and luck for you ;) ) that a string just from the binary was accepted :)
    (maybe occasionally they published patched binary for a small period?)

    • alopex on January 22, 2011 at 07:20
    • Reply

    @hellman: yeah, it could of been at the time I started working on it (later in the day on the 16th). It definitely accepted strings straight from that binary though. When I used my script on the host machine, I received the key: Reversing Skill@. I immediately recognized it from the binary itself, and got bummed out that those were the actual authentication flags used to receive points. I then just picked strings at random and resubmitted to get the message “You already solved this one :(“. All the ones I tried got that message. If you submitted a random string (not one of the passwds) you got “sorry”.

  1. […] This post was mentioned on Twitter by Sofian Brabez (sbz) and Gunther, Alexey. Alexey said: #padocon ctf quals bin500 (300pts) writeup http://bit.ly/fRNzzJ […]

Leave a Reply

Your email address will not be published.