0CTF 2016

rand_2 - 2 pts
Do you believe it, can you recieve it?

<?php
include('config.php');
session_start();
if($_SESSION['time'] && time() - $_SESSION['time'] > 60) {
  session_destroy();
  die('timeout');
} else {
  $_SESSION['time'] = time();
}

echo rand();

if (isset($_GET['go'])) {
  $_SESSION['rand'] = array();
  $i = 5;
  $d = '';
  while($i--){
    $r = (string)rand();
                $_SESSION['rand'][] = $r;
                $d .= $r;
  }
  echo md5($d);
} else if (isset($_GET['check'])) {
  if ($_GET['check'] === $_SESSION['rand']) {
    echo $flag;
  } else {
    echo 'die';
    session_destroy();
  }
} else {
  show_source(__FILE__);
}
?>

One more PRNG problem. PHP normally use glibc’s random function as its underlying implementation, which is x[n]=(x[n-3]+x[n-31])>>1. My solution was generating all 2^32 possible cases and confirm with the md5 value. First part of the solution was the cache generator:

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <queue>
#include <algorithm>
#include <stdlib.h>
#include <stdio.h>
using namespace std;

namespace vhrand {
  static long randtbl[DEG_3 + 1] = {
    TYPE_3,
    0x991539b1, 0x16a5bce3, 0x6774a4cd, 0x3e01511e, 0x4e508aaa, 0x61048c05,
    0xf5500617, 0x846b7115, 0x6a19892c, 0x896a97af, 0xdb48f936, 0x14898454,
    0x37ffd106, 0xb58bff9c, 0x59e17104, 0xcf918a49, 0x09378c83, 0x52c7a471,
    0x8d293ea9, 0x1f4fc301, 0xc3db71be, 0x39b44e1c, 0xf8a44ef9, 0x4c8b80b1,
    0x19edc328, 0x87bf4bdd, 0xc9b240e5, 0xe9ee4b1b, 0x4382aee7, 0x535b6b41,
    0xf3bec5da
  };

  static long *fptr = &randtbl[SEP_3 + 1];
  static long *rptr = &randtbl[1];

  static long *state = &randtbl[1];
  static long rand_type = TYPE_3;
  static long rand_deg = DEG_3;
  static long rand_sep = SEP_3;
  static long *end_ptr = &randtbl[DEG_3 + 1];

  long random();

  //
  // Compute x = (7^5 * x) mod (2^31 - 1)
  // wihout overflowing 31 bits:
  //      (2^31 - 1) = 127773 * (7^5) + 2836
  // From "Random number generators: good ones are hard to find",
  // Park and Miller, Communications of the ACM, vol. 31, no. 10,
  // October 1988, p. 1195.
  //

  __inline long good_rand(long x) {
    long hi, lo;

    // Can't be initialized with 0, so use another value.
    if (x == 0) x = 123459876;
    hi = x / 127773;
    lo = x % 127773;
    x = 16807 * lo - 2836 * hi;
    if (x < 0) x += 0x7fffffff;
    return x;
  }
  void srandom(unsigned long x) {
    long i, lim;

    state[0] = x;
    if (rand_type == TYPE_0) {
      lim = NSHUFF;
    } else {
      for (i = 1; i < rand_deg; i++) state[i] = good_rand(state[i - 1]);
      fptr = &state[rand_sep];
      rptr = &state[0];
      lim = 10 * rand_deg;
    }
    
    for (i = 0; i < lim; i++) random();
  }

  long random() {
    long i;
    long *f, *r;

    if (rand_type == TYPE_0) {
      i = state[0];
      state[0] = i = (good_rand(i)) & 0x7fffffff;
    } else {
      f = fptr; r = rptr;
      *f += *r;
      i = (*f >> 1) & 0x7fffffff; // Chucking least random bit
      if (++f >= end_ptr) {
        f = state;
        ++r;
      } else if (++r >= end_ptr) {
        r = state;
      }

      fptr = f; rptr = r;
    }

    return i;
  }
};

int ctop[2148];
int cnext[10000000];
long pendingValues[10000000][2];
long pendingCount = 0;

void processPending(){
  printf("Flushing %ld\n",pendingCount);
  char filename[100];
  for(int i=0;i<=2147;i++){
    FILE* h = NULL;
    for(int j=ctop[i];j>=0;j=cnext[j]){
      if(pendingValues[j][1]/1000000==i){
        sprintf(filename, "caches/%d.txt", i);
        if(h==NULL){
          h = fopen(filename, "a");
        }
        fprintf(h, "%ld %ld\n", pendingValues[j][1], pendingValues[j][0]);
      }else{
        printf("ERRROR!!!!!");
        exit(-1);
      }
    }
    if(h!=NULL) fclose(h);
  }
  memset(ctop, -1, sizeof(ctop));
  pendingCount = 0;
}

int main(int argc, char *argv[]) {
  //max=2147483647
  pendingCount = 0;
  memset(ctop, -1, sizeof(ctop));
  for(long i=0;i<2147483647l;i++){
    if(i%1000000==0)
      fprintf(stderr,"Generated up to %ld\n",i);
    vhrand::srandom(i);
    long val = vhrand::random();
    long bucket = val/1000000;
    pendingValues[pendingCount][0]=i;
    pendingValues[pendingCount][1]=val;
    cnext[pendingCount]=ctop[bucket];
    ctop[bucket]=pendingCount;
    pendingCount++;
    if(pendingCount==10000000) processPending();
  }
  processPending();
  return 0;
}

While the cache was generating, i ran a script to solve the problem and just left it running tothether with above C++ script:

<?php
for(;;){
  $s=N("http://202.120.7.202:8888/?go=1");
  preg_match('@Set-Cookie: (.*?); path=\/@', $s, $m);
  $cookie = $m[1];
  $param = end((explode("\n", $s)));
  $md5 = substr($param, -32);
  $param = intval(substr($param, 0, -32));
  $bucket = floor($param/1000000);
  echo "Param: $param ($bucket) | MD5 : $md5\n";
  if(file_exists($fn = "caches/$bucket.txt")){
    $h = fopen($fn, "r");
    while(!feof($h)){
      fscanf($h, "%d%d", $n, $seed);
      if($n==$param){
        srand($seed);
        $rr = rand();
        echo "Trying seed=$seed : $rr\n";
        $url = "http://202.120.7.202:8888/?_=1";
        $d = "";
        $i=5;
        while($i--){
          $r = (string)rand();
          $url.="&check[]=$r";
          $d.=$r;
        }
        if($md5 == md5($d)){
          echo "Finally... ".$url."\n";
          echo N($url,$cookie);
          exit;
        } else {
          echo "MD5 must be $md5  --  ".md5($d)."\n";
        }
      }
    }
  }
  N("http://202.120.7.202:8888/?check=1");
}

function N($url,$cookie=""){
  $ch = curl_init();
  curl_setopt_array($ch, [
    CURLOPT_URL => $url,
    CURLOPT_SSL_VERIFYPEER => 0,
    CURLOPT_SSL_VERIFYHOST => 0,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_POST => 0,
    CURLOPT_HEADER => 1,
    CURLOPT_COOKIE => $cookie,
  ]);
  $s = curl_exec($ch);
  curl_close($ch);
  return $s;
}

?>

The flag printed out after the cache had generated about 500mils items. After sometime i noticed how stupid i was when forgot the Keep-Alive header, yes it has been a long time…


 

rsa - 2 pts
It seems easy, right?
Tip: openssl rsautl -encrypt -in FLAG -inkey public.pem -pubin -out flag.enc

$ cat public.pem
-----BEGIN PUBLIC KEY-----
MEEwDQYJKoZIhvcNAQEBBQADMAAwLQIoAsqpwJ3BBh5Qflt/Od3jRV/P4Seixpti
HIP9nT0+qjqsQhR81xiMUwIBAw==
-----END PUBLIC KEY-----
$ hexdump -C flag.enc
00000000  00 4c 41 62 a0 7a 01 11  b8 34 4c 68 b1 18 bd 05  |.LAb.z...4Lh....|
00000010  4a cb c3 8c 31 31 b6 a8  99 9c 91 d1 b3 e2 d8 2d  |J...11.........-|
00000020  c7 c3 a1 e1 03 4f d6 04                           |.....O..|
00000028

The pem file give us n=23292710978670380403641273270002884747060006568046290011918413375473934024039715180540887338067, e=3. Msieve reported 3 factors of n after about 2 hours:

a = 26440615366395242196516853423447
b = 27038194053540661979045656526063
c = 32581479300404876772405716877547

It was not that easy, (a-1)*(b-1)*(c-1) is divisible by e, so d does not exists in this system. In this problem we can calculate d’ which’s

cipher^d' = (plain^e)^d' = plain^27 (mod n)

Then find all 27th-root of n and check one by one. Final solution’s below:

from sage.all import *

n = 23292710978670380403641273270002884747060006568046290011918413375473934024039715180540887338067

a = 26440615366395242196516853423447
b = 27038194053540661979045656526063
c = 32581479300404876772405716877547

e = 3

phi = (a-1)*(b-1)*(c-1)

ne = 0
while phi % (e**(ne+1)) == 0:
  ne += 1
print "ne = ", ne
d = long(1/Mod(e,phi/e**ne))
print "d = ", d

with open("flag.enc","rb") as f:
  cipher = f.read(100)
  
cipher = int(cipher.encode("hex"),16)

plain_pow_ene = pow(cipher,d*e**ne,n)

print plain_pow_ene

cases = []
for p in [a,b,c]:
  cases.append(Mod(plain_pow_ene%p,p).nth_root(e**ne,all=True))

print "Start solving"
for ar in map(long,Mod(plain_pow_ene%a,a).nth_root(e**ne,all=True)):
  for br in map(long,Mod(plain_pow_ene%b,b).nth_root(e**ne,all=True)):
    for cr in map(long,Mod(plain_pow_ene%c,c).nth_root(e**ne,all=True)):
      x = crt([ar,br,cr],[a,b,c])
      print hex(x)
      s = hex(x)
      if len(s)%2==1:
        s = "0" + s
      print s.decode("hex")

The flag is:

0ctf{HahA!Thi5_1s_n0T_rSa~}

Yes yes, i know, it was not RSA, of cause…


 

people_square - 6 pts

People’s Square (A.K.A. shenmhin guangshan in Shanghai Dialect) is a large public square in the Huangpu District of Shanghai, China.
We know Talent Yang is the king of People's Square. Now he provides you a strange guessing game, and he also demonstrates his talent by giving you the result of how he tackles this task. Can you show your talent to decrypt the secret?

After a while reading the binaries with IDA, it’s confirmed that this’s a problem about recovering a cipher text with known plain-cipher pairs on reduced version of AES (4 rounds). The given binary’s decompiled code looks like below:

int __fastcall load_flag(void *a1)
{
 FILE *v1; // ST10_8@1

 v1 = fopen("flag.txt", "r");
 fread(a1, 1uLL, 0x20uLL, v1);
 return fclose(v1);
}

int __fastcall load_key(void *a1)
{
 FILE *v1; // ST10_8@1

 v1 = fopen("key.txt", "rb");
 fread(a1, 1uLL, 0x10uLL, v1);
 return fclose(v1);
}

__m128i *__fastcall GenerateSamples(__int64 a1, __int64 a2, __int64 a3, int a4, int a5)
{
 __int64 v5; // ST28_8@1
 int v6; // ST20_4@1
 int v7; // ST18_4@1

 v5 = a3;
 v6 = a4;
 v7 = a5;
 memset((void *)a2, 0, 0x10uLL);
 memset((void *)v5, 1, 0x10uLL);
 *(_DWORD *)(a2 + 8) = v6;
 *(_DWORD *)(v5 + 8) = v6;
 *(_DWORD *)(a2 + 12) = v7;
 *(_DWORD *)(v5 + 12) = v7;
 AESEncrypt(a2, a1);
 return AESEncrypt(v5, a1);
}

__int64 main()
{
  unsigned __int8 v0; // ST3B_1@6
  __int64 result; // rax@12
  int v2; // [sp+3Ch] [bp-174h]@3
  unsigned __int64 i; // [sp+40h] [bp-170h]@1
  int v4; // [sp+48h] [bp-168h]@1
  __int64 v5; // [sp+50h] [bp-160h]@1
  char sample_1; // [sp+60h] [bp-150h]@3
  char sample_0; // [sp+70h] [bp-140h]@3
  char aes_key; // [sp+80h] [bp-130h]@1
  char key_str; // [sp+170h] [bp-40h]@1
  char v10; // [sp+180h] [bp-30h]@1
  __int64 v11; // [sp+190h] [bp-20h]@11
  __int64 v12; // [sp+1A8h] [bp-8h]@1

  v12 = *__stack_chk_guard_ptr;
  memset(&v10, 0, 0x20uLL);
  load_flag(&v10);
  memset(&key_str, 0, 0x10uLL);
  load_key(&key_str);
  AESKeyExpand((__int64)&key_str, (__int64)&aes_key);
  v5 = 0LL;
  v4 = time(0LL);
  for ( i = 0LL; i < 0x400; ++i )
  {
    GenerateSamples((__int64)&aes_key, (__int64)&sample_0, (__int64)&sample_1, i, v4);
    v2 = rand() % 2;
    if ( v2 )
      print_hex(&sample_1, 16LL);
    else
      print_hex(&sample_0, 16LL);
    puts("0 or 1?");
    v0 = getchar() - 48;
    puts("ciphertext for 0 is: ");
    print_hex(&sample_0, 16LL);
    puts("ciphertext for 1 is: ");
    print_hex(&sample_1, 16LL);
    if ( v0 == v2 )
    {
      ++v5;
      puts("Correct!");
    }
    else
    {
      puts("Incorrect!");
    }
  }
  if ( 1024 == v5 )
  {
    puts("Now I will give you the flag:");
    AESEncrypt((__int64)&v10, (__int64)&aes_key);
    AESEncrypt((__int64)&v11, (__int64)&aes_key);
    print_hex(&v10, 32LL);
  }
  result = *__stack_chk_guard_ptr;
  if ( *__stack_chk_guard_ptr == v12 )
    result = 0LL;
  return result;
}

output.txt from problem statement also gave a session that we need to decrypt to obtain flag:

4d 7c 5d b0 80 45 a2 13 f6 61 0d ca e2 c1 75 9f 
0 or 1?
ciphertext for 0 is: 
d2 e2 57 9e ee 1f e0 dd 39 45 8d 5a 3f 4e 97 65 
ciphertext for 1 is: 
4d 7c 5d b0 80 45 a2 13 f6 61 0d ca e2 c1 75 9f 
Correct!
1d dc d4 fb e0 4a 33 c2 a8 3f 24 a6 d2 6d 86 39 
0 or 1?
ciphertext for 0 is: 
bb a3 80 8c 66 94 c5 b9 c7 2b 18 ad 8d 9a 1f 78 
ciphertext for 1 is: 
1d dc d4 fb e0 4a 33 c2 a8 3f 24 a6 d2 6d 86 39 
Correct!
....
Now I will give you the flag:
af 93 ce ae 1f 1e 7a 13 26 d6 05 51 97 3c 46 1b c9 b1 56 9c 2c df d5 5a c6 ca 33 46 31 fb 19 73 

So we have 1024 known cipher texts, in which there is 512 plain text in the format of 00 00 00 00 00 00 00 00 xx xx xx xx yy yy yy yy where xxxxxxxx is an integer from with values in range [0, 1024) and yyyyyyyy is a unix timestamp, and the rest is in the format of 01 01 01 01 01 01 01 01 xx xx xx xx yy yy yy yy.

After some time spending for research on AES, i found an interesting slide here: http://www.di.ens.fr/~fouque/pub/fse13b.pdf.

So the final solution is solving the 4-th subkey of given AES session byte by byte, trying all 256 values for each byte of the subkey, perform a single aes decryption step each and xor all the values, the corresponding bytes in the received values will have xor-sum of zero with wrong probality of 2-8. The implementation for subkey recovery looks like below:

def recover_subkey():
  messages = open("output.txt").read(10*1024*1024).split("\r\n")[:-2]
  key0 = messages[3::7]
  key1 = messages[5::7]
  print len(messages), len(key0), len(key1)
  key0 = map(parse_hex, key0)
  key1 = map(parse_hex, key1)
  
  sol = [0]*16
  for ci in range(16):
    found = False
    for c in range(256):
      sol[ci]=c
      valid = True
      for i in range(4):
        sxor3 = "\x00"*16
        for j in range(256):
          sxor3 = str_xor(sxor3, decryptOneRound(key0[256*i+j], "".join(map(chr,sol))))
        if sxor3[ci]!="\x00":
          valid = False
          break
      if valid:
        print "Found byte ", c
        found = True
        break
    assert found
  
  print "Subkey = ", "".join(map(chr,sol)).encode("hex")
  for i in range(4):
    sxor3 = "\x00"*16
    for j in range(256):
      sxor3 = str_xor(sxor3, backround(key0[256*i+j], "".join(map(chr,sol))))
    print "Test = ", sxor3.encode("hex")
f6f601bb7b6adf60d366c0a78724a66b

Now we can recover the original key:

def unexpandKey(self, size, expandedKeySize, finalBlock):
  expandedKey = [0] * expandedKeySize
  for j in range(1,1+size):
    expandedKey[-j] = finalBlock[-j]
  currentSize = expandedKeySize - 4
  rconIteration = expandedKeySize/size
  rdi = 0
  while currentSize >= size:
    t = expandedKey[currentSize-4:currentSize]
    special = False
    if (currentSize-rdi) % size == 0:
      rconIteration -= 1
      t = self.core(t, rconIteration)
    for m in range(4):
      expandedKey[currentSize-size+m] = expandedKey[currentSize+m] ^ t[m]
    currentSize -= 4
  return expandedKey[:size]
174a221440356475dce3a037a317ed4b

Finally it’s so easily to decrypt the flag:

0CTF{~R0MAN_l0VES_B10CK_C1PHER~}

 

Arsenal - 8 pts

There is a rumor that Arsenal F.C., Talent Yang’s favourite team, always likes to set some “mission impossible” tasks for themselves.
For instance, with a 0-2 home defeat against FC Barcelona they must play a great game in Camp Nou to get to the next round of UEFA Champions League. Can they fulfil this task? Decrypt the provided ciphertext to help them!

(The executable of this challenge is similar to that of People's Square , check the slight difference and enjoy solving it)

Hint: You should perform 2^32 single round decryption first

This’s the same as people_square, but with only 126 known plain texts, with 63 in each group. The zero sumed xor trick cannot be used anymore…

I did not have enough time to solve this problem within the context, i was only 2 hours left and i was so tired due to almost 40 hours playing on both 0ctf and codegate.

The idea is that the internal state of aes before MixColumn step of 3rd round will have unique values when plain text has exactly 1 active byte. We can find out the final subkey by groups of 4 bytes each, confirm the validity of that group with that statement with wrong probability of 256!/[(256-63)! * 256^63] which is almost zero – more than good enough for us to confirm the validity of specified group.

Problem is that pure-python implement cant work anymore 😀 The implement i made for people_square require about 5 mins to check 10000 cases of a group, that will result in about 1491.30808889 days running (lol), so i decided to try out the AES Instruction Set (which’s used in the problem statement too), and surprisingly, the problem is solved after 20 mins running time!!!

int ntest=0;
  __m128i test[2][100];
  union{
    __m128i i128;
    uint8_t i8[16];
  } key, state;
  void solveRound(int a0, int a1, int a2, int a3, int b0, int b1, int b2, int b3){
    int check[4][256], checkid[4]={7,7,7,7};
    #define INC(a,b,c,d) ++key.i8[a]==0x00 && ++key.i8[b]==0x00 && ++key.i8[c]==0x00 && ++key.i8[d]==0x00
    #define TES(a,b) if(check[a][state.i8[b]]!=checkid[a]) check[a][state.i8[b]]=checkid[a]; else {valid=false;break;}
    memset(check, 0, sizeof(check));
    for(int i=0;i<4;i++) checkid[i]=1;
    for(long long i=0;;i++){
      if(i%100000000==0){
        printf("%lld / %lld\n",i,(1ll<<32));
      }
      bool valid = true;
      for(int j=0;j<4;j++) checkid[j]++;
      for(int j=0;j<ntest;j++){
        state.i128 = _mm_aesdec_si128(_mm_xor_si128(test[0][j],key.i128), _mm_setzero_si128());
        TES(0,a0);TES(1,a1);TES(2,a2);TES(3,a3);
      }
      if(valid){
        printf("Found!\n");
        print128_num(key.i128);
        break;
      }
      if(INC(b0, b1, b2, b3)) break;
    }
  }

  void solve(){
    FILE* h = fopen("output.txt", "r");
    char s[1000];
    while(!feof(h)){
      fgets(s, 1000, h); // guess line
      fgets(s, 1000, h); // 0 or 1?
      fgets(s, 1000, h); // ciphertext for 0 is: 
      if(feof_unlocked(h)) break;
      fgets(s, 1000, h);
      fromHex((uint8_t*)&test[0][ntest],s);
      fgets(s, 1000, h); // ciphertext for 1 is:
      fgets(s, 1000, h);
      fromHex((uint8_t*)&test[1][ntest],s);
      fgets(s, 1000, h); //Correct!
      
      ntest++;
    }
    fclose(h);
    printf("Total samples = %d\n",ntest);
    key.i128 =    _mm_setzero_si128();
    solveRound(0,1,2,3,  0,7,10,13);
    solveRound(4,5,6,7,  1,4,11,14);
    solveRound(8,9,10,11,  2,5,8,15);
    solveRound(12,13,14,15,  3,6,9,12);
  }

The final flag is:

0CTF{~~1MP0SS1BLE_1S_N0TH1NG!~~}