English [EKOPARTY PRE-CTF 2015] [Rev200 – Reversing the APC cache] Write Up

Description

Description: Strings are not always an alternative.

Hints: APC 3.1.13 with PHP 5.4 was used.

Attachment: reversing200.zip

Resolution

We have here a mysterious file named cache.data. Since file command can not properly identify it, we try to find some useful strings in it issuing command strings

We get some interesting stuff in here :

/var/www/html/index.php
<html>
    <head></head>
    <body>
        <form method="POST" action="login.php">
            <input type="text" name="token">
            <input type="submit" value="Login">
        </form>
    </body>
</html>
/var/www/html/login.php
[..]
AzDGCrypt
azdgcrypt
EKO{this_is_not_the_flag}
[..]
e88ef51d4112b999380444ce48488762
sha1
sha1
Welcome master, your key is EKO{
[...]

EKO{this_is_not_the_flag} wasn’t the flag, obviously.

The description talked about APC, that’s where we are now heading. After some research, we discovered a function in php documentation called apc_bin_loadfile().

So we’ll compile php 5.4 with APC last version in order to load this cache file and try to get some information from it.

 

./bin/php -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/apc.so -d apc.enable_cli=1 -d apc.stat=0 -r "apc_bin_loadfile('cache.data'); print_r(apc_cache_info());"

This gives us some useful piece of information :

Array
(
    [num_slots] => 1031
    [ttl] => 0
    [num_hits] => 0
    [num_misses] => 0
    [num_inserts] => 2
    [expunges] => 0
    [start_time] => 1442496130
    [mem_size] => 21568
    [num_entries] => 2
    [file_upload_progress] => 1
    [memory_type] => mmap
    [locking_type] => pthread mutex Locks
    [cache_list] => Array
        (
            [0] => Array
                (
                    [type] => file
                    [device] => 0
                    [inode] => 0
                    [filename] => /var/www/html/index.php
                    [num_hits] => 0
                    [mtime] => 1442496130
                    [creation_time] => 1442496130
                    [deletion_time] => 0
                    [access_time] => 1442496130
                    [ref_count] => 0
                    [mem_size] => 1200
                )

            [1] => Array
                (
                    [type] => file
                    [device] => 0
                    [inode] => 0
                    [filename] => /var/www/html/login.php
                    [num_hits] => 0
                    [mtime] => 1442496130
                    [creation_time] => 1442496130
                    [deletion_time] => 0
                    [access_time] => 1442496130
                    [ref_count] => 0
                    [mem_size] => 20368
                )

        )

    [deleted_list] => Array
        (
        )

    [slot_distribution] => Array
        (
            [231] => 1
            [850] => 1
        )

)

From this result, we understand that this cache file contains 2 php pages. Since file_get_contents doesn’t work to get source code (APC doesn’t cache the file_get_contents()), we’re going to include this two files and disassemble them.

For this operation, we used vld php extension to understand php opcodes, and it really saved us some time.

 

./bin/php -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/apc.so -d apc.enable_cli=1 -d apc.stat=0 -d extension=/tmp/php/php-5.4.45/compiled/lib/php/extensions/no-debug-non-zts-20100525/vld.so -d vld.active=0 -d vld.execute=1 -r "apc_bin_loadfile('cache.data'); include '/var/www/html/login.php';"

Which gave us :

Finding entry points
Branch analysis from position: 0
Jump found. Position 1 = -2
function name:  (null)
number of ops:  4
compiled vars:  none
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   2     0  E >   SEND_VAL                                                 '%2Ftmp%2Fcache.data'
         1        DO_FCALL                                      1          'apc_bin_loadfile'
   3     2        INCLUDE_OR_EVAL                                          '%2Fvar%2Fwww%2Fhtml%2Flogin.php', REQUIRE
   8     3      > RETURN                                                   1

branch: #  0; line:     2-    8; sop:     0; eop:     3; out1:  -2
path #1: 0, 
Finding entry points
Branch analysis from position: 0
Jump found. Position 1 = 5, Position 2 = 55
Branch analysis from position: 5
Jump found. Position 1 = 15, Position 2 = 22
Branch analysis from position: 15
Jump found. Position 1 = 23, Position 2 = 29
Branch analysis from position: 23
Jump found. Position 1 = 30, Position 2 = 54
Branch analysis from position: 30
Jump found. Position 1 = 41, Position 2 = 50
Branch analysis from position: 41
Jump found. Position 1 = 53
Branch analysis from position: 53
Jump found. Position 1 = 54
Branch analysis from position: 54
Jump found. Position 1 = 58
Branch analysis from position: 58
Jump found. Position 1 = -2
Branch analysis from position: 50
Jump found. Position 1 = 54
Branch analysis from position: 54
Branch analysis from position: 54
Branch analysis from position: 29
Branch analysis from position: 22
Branch analysis from position: 55
Jump found. Position 1 = -2
filename:       /var/www/html/login.php
function name:  (null)
number of ops:  59
compiled vars:  !0 = $token, !1 = $crypt, !2 = $hash, !3 = $flag
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   3     0  E >   NOP                                                      
  44     1        FETCH_IS                                         $1      '_POST'
         2        ISSET_ISEMPTY_DIM_OBJ                       16777216  ~2      $1, 'token'
         3        BOOL_NOT                                         ~3      ~2
         4      > JMPZ                                                     ~3, ->55
  45     5    >   FETCH_R                      global              $4      '_POST'
         6        FETCH_DIM_R                                      $5      $4, 'token'
         7        ASSIGN                                                   !0, $5
  47     8        INIT_FCALL_BY_NAME                                       'substr'
         9        SEND_VAR                                                 !0
        10        SEND_VAL                                                 0
        11        SEND_VAL                                                 4
        12        DO_FCALL_BY_NAME                              3  $7      
        13        IS_IDENTICAL                                     ~8      $7, 'CmxQ'                                   ; substr($token, 0, 4) == "CmxQ"
        14      > JMPZ_EX                                          ~8      ~8, ->22
        15    >   INIT_FCALL_BY_NAME                                       'substr'
        16        SEND_VAR                                                 !0
        17        SEND_VAL                                                 44
        18        SEND_VAL                                                 4
        19        DO_FCALL_BY_NAME                              3  $9      
        20        IS_IDENTICAL                                     ~10     $9, 'MgY%2F'                                 ; substr($token, 44, 4) == "MgY%2F"
        21        BOOL                                             ~8      ~10
        22    > > JMPZ_EX                                          ~8      ~8, ->29
        23    >   INIT_FCALL_BY_NAME                                       'substr'
        24        SEND_VAR                                                 !0
        25        SEND_VAL                                                 -4
        26        DO_FCALL_BY_NAME                              2  $11     
        27        IS_IDENTICAL                                     ~12     $11, 'Mg%3D%3D'                              ; substr($token, -4) == "Mg%3D%3D"
        28        BOOL                                             ~8      ~12
        29    > > JMPZ                                                     ~8, ->54
  48    30    >   FETCH_CLASS                                   4  :13     'AzDGCrypt'
        31        NEW                                              $14     :13                                          ; $14 = new AzDGCrypt('EKO{this_is_not_the_flag}');
        32        SEND_VAL                                                 'EKO%7Bthis_is_not_the_flag%7D'
        33        DO_FCALL_BY_NAME                              1          
        34        ASSIGN                                                   !1, $14
  49    35        INIT_METHOD_CALL                                         !1, 'decrypt'                                ; 
        36        SEND_VAR                                                 !0
        37        DO_FCALL_BY_NAME                              1  $18                                                  ; $18 = $14.decrypt($token);
        38        ASSIGN                                                   !2, $18
  51    39        IS_IDENTICAL                                     ~20     !2, 'e88ef51d4112b999380444ce48488762'       ; $14.decrypt($token) == md5("EKO{}")    <-- This is what we want ! 40 > JMPZ                                                     ~20, ->50
  52    41    >   INIT_FCALL_BY_NAME                                       'sha1'                                       
        42        SEND_VAR                                                 !0
        43        DO_FCALL_BY_NAME                              1  $21     
        44        ASSIGN                                                   !3, $21                                      ; $21 c'est le flag, c'est sha1($token)
  53    45        ADD_STRING                                       ~23     'Welcome+master%2C+your+key+is+EKO%7B'       ; %7B <=> "{"
        46        ADD_VAR                                          ~23     ~23, !3                                      ; Le flag c'est le "!3"
        47        ADD_CHAR                                         ~23     ~23, 125                                     ; 125 == %7D == "}"
        48        ECHO                                                     ~23
  54    49      > JMP                                                      ->53
  55    50    >   INIT_FCALL_BY_NAME                                       'header'
        51        SEND_VAL                                                 'Location%3A+index.php'
        52        DO_FCALL_BY_NAME                              1          
  57    53    > > JMP                                                      ->54
  58    54    > > JMP                                                      ->58
  59    55    >   INIT_FCALL_BY_NAME                                       'header'
        56        SEND_VAL                                                 'Location%3A+index.php'
        57        DO_FCALL_BY_NAME                              1          
  61    58    > > RETURN                                                   1

From this result, we understand that POST parameter token has to follow a special format. There are 3 checks based on substrings :

  1. substr($token, 0, 4) must be equal to “CmxQ”
  2. substr($token, 44, 4) must be equal to “MgY/”
  3. substr($token, -4) must be equal to “Mg==”.

If these 3 conditions are met, this token is deciphered using the method decrypt of AzDGCrypt class. AzDGCrypt is instantiated with parameter EKO{this_is_not_the_flag} (Used as ciphering key).

Deciphered text must be equal to e88ef51d4112b999380444ce48488762.

Before trying to reverse this class, we made some research, and found its source code on github.

 

At this point, we could think we had every thing we needed to decipher this token, be if we look closely at AzDGCrypt class, we can see a randomization part before encryption function : $r = md5(rand(0,32000));

Since we only have some parts of base64 encoded token, we had to brute force this randomization and for every result, check if all of the 3 substrings matched. This is our bruteforce code :

 

<?php
class AzDGCrypt{
   var $k;
   function AzDGCrypt($m){
      $this->k = $m;
   }
   function ed($t) {
      $r = md5($this->k);
      $c=0;
      $v = "";
      for ($i=0;$i<strlen($t);$i++) {
         if ($c==strlen($r)) $c=0;
         $v.= substr($t,$i,1) ^ substr($r,$c,1);
         $c++;
      }
      return $v;
   }
   function crypt($t, $rand){
      $r = md5($rand);
      $c=0;
      $v = "";
      for ($i=0;$i<strlen($t);$i++){ if ($c==strlen($r)) $c=0; $v.= substr($r,$c,1) . (substr($t,$i,1) ^ substr($r,$c,1)); $c++; } return base64_encode($this->ed($v));
   }
   function decrypt($t) {
      $t = $this->ed(base64_decode($t));
      $v = "";
      for ($i=0;$i<strlen($t);$i++){
         $md5 = substr($t,$i,1);
         $i++;
         $v.= (substr($t,$i,1) ^ $md5);
      }
      return $v;
   }
}

 // bf
 $crypt = new AzDGCrypt('EKO{this_is_not_the_flag}');
 for ($i = 0; $i <= 32000; ++$i) {
  $result = $crypt->crypt('e88ef51d4112b999380444ce48488762', $i);
  if (substr($result, 0, 4) == 'CmxQ' && substr($result, 44, 4) == 'MgY/' && substr($result, -4) == 'Mg==') {
   echo "b64: $result\nRand: $i\n";
   break;
  }
 }

After a few seconds, we got our expected result

b64: CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg==
Rand: 17291

We then test this token with the cached php page

<?php
 $_POST['token'] = 'CmxQaQAzBTYKZQYzAWAFZAY1V2NVZAZgUGQCbAU9Vz4CMgY/AzgLaQwxWm5SYVY2UGNUaVFlAm5VO1MzBDNQMg==';
 apc_bin_loadfile('cache.data');
 include('/var/www/html/login.php');

Everything’s alright !

Welcome master, your key is EKO{59a59936b318e8ef20fd923a3e7b05a1e44e9e91}

4 thoughts on “[EKOPARTY PRE-CTF 2015] [Rev200 – Reversing the APC cache] Write Up”

  1. Hi, i tried to install PHP 5.4.45 and APC 3.1.13, but I cannot import “cache.data”, i got error “Segmentation fault”.
    What is the OS version you used ? Can you give some detail of your command to install ?
    Thanks,

    1. Hi,

      I have compiled myself both PHP 5.4.45 and APC 3.1.13 on ubuntu 15.04 x64.
      If you get segmentation fault, make sure you have not version conflicts (eg: used system phpize instead of freshly compiled 5.4 phpize when compiling APC)

Leave a Reply

Your email address will not be published. Required fields are marked *