zep's blog

follow me into the rabbit hole of ITsec

Write-up for medium challenge at SANS Zurich 18


This is my write-up for the medium challenge at SANS Zurich 18. The challenges are no longer available at pastebin, but you can get them from my git repository:

First let's download the challenge and check what's all about:

wget -q https://raw.githubusercontent.com/7a6570/challenges/master/sans_zurich_18/medium/medium_challenge.txt
file medium_challenge.txt
medium_challenge.txt: ASCII text, with CRLF line terminators

So it seems that this file has "Windows style (CRLF)" line endings, which consist of an "carriage return" and an "line feed" ASCII control character. Then we see that this file is mainly ASCII text, meaning that all bytes of this file lie in the ASCII printable region. Let's have a look at the file in vim:

N3q8ryccAAObDHmThDIAAAAAAAAjAAAAAAAAABHzhZ0AHhoKhu9K1oc2p1UGfYtgZZjK1zmxqdnQ
n2mEIwUO9dwi8jR/ST7v9ozUa0iXvcn/Qxv2yZubFMKXX5u8ROJJCVZqEN7QPDuuUqqTTutU+5AB
igvmTcKYEYXU9vdjX833vpxn0Cl/nCIIt2oOeBDr4Q3d0HbtxDrWnk4Ryw6t0mU8EJ+xcekxdqkO
2R9ilbBhNJGyqpJLTr80d7ckcM9CEAzYpJ0zVdj/H6yygJNMThBjHjRuhHpm+Ak/C3dvQ1OaQSVW
Nxre3AE3bIAitHxbbv/SiFslCIluGGCYm6+mBemIszldnmiEgK9egdXfNu5JMf0K3mcsc+lNmGym

<-- snip -->

We see only chars from a-z, A-Z, 0-9, + and /. Smells like Base64. But wait: Above we have seen, that this file has "Windows" line endings. Linux tools don't like them, so let's first convert them to Unix / Linux endings:

sed -i 's|\r||g' medium_challenge.txt
file medium_challenge.txt
medium_challenge.txt: ASCII text

Looks better :-) We just removed the carriage return ASCII control char (CR), which is the first part of the "Windows" line ending. The seconds part is the line feed (LF) control sequence, which is the way line endings are marked in Unix and Linux. Let's decode the Base64 encoded file:

cat medium_challenge.txt | base64 -d > medium_challenge_decoded
file medium_challenge_decoded
medium_challenge_decoded: 7-zip archive data, version 0.3

So we were right. The file was a Base64 encoded 7z archive. Let's extract the archive and see what we will find:

7z x medium_challenge_decoded

Scanning the drive for archives:
1 file, 12999 bytes (13 KiB)

Extracting archive: medium_challenge_decoded
--
Path = medium_challenge_decoded
Type = 7z
Physical Size = 12999
Headers Size = 222
Method = LZMA:23
Solid = +
Blocks = 1

Everything is Ok

Files: 2
Size:       67194
Compressed: 12999

ls
crypto.js  index.html  medium_challenge_decoded  medium_challenge.txt

Interesting. We have two files: index.html and crypto.js. Let's first open index.html in a browser:

index.html

We see some scrambled output, which is labeled with "decrypted". Each time we refresh the page, the scrambled output changes. So this means, we're supposed to decrypt something to get our flag. Let's have a look at crypto.js. First of all, this file is quite big, 800 lines of javascript code. Looks like crypto stuff and is probably a crypto library. Let's move on and have a look at index.html:

<-- snip -->

 <script type="text/javascript" src="crypto.js"></script>
 <script>
 var _0xde32=["\x6F\x6E\x6C\x6F\x61\x64","\x65\x6E\x63\x6F\x64\x65\x48\x65\x78","\x70\x72\x6F\x74\x6F\x74\x79\x70\x65","\x6C\x65\x6E\x67\x74\x68","\x63\x68\x61\x72\x43\x6F\x64\x65\x41\x74","\x70\x75\x73\x68","\x73\x69\x6E","\x66\x6C\x6F\x6F\x72","","\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65","\x6E\x6F\x77","\x72\x6F\x75\x6E\x64","\x6E\x30\x54\x5F\x4D\x79\x5F\x70\x61\x73\x73\x57\x30\x72\x44\x21","\x62\x32\x64\x33\x63\x66\x35\x36\x37\x64\x35\x66\x34\x62\x37\x32\x66\x38\x62\x33\x65\x32\x39\x37\x65\x39\x33\x65\x35\x32\x66\x32\x66\x33\x66\x33\x63\x37\x32\x31\x32\x66\x38\x65\x30\x38\x34\x66\x33\x63\x38\x66\x30\x31\x63\x34\x61\x64\x66\x34\x39\x66\x66\x32\x64\x66\x35\x39\x38\x35\x37\x39\x36\x65\x64\x32\x38\x39\x62\x39\x39\x30\x32\x34\x66\x37\x39\x63\x34\x37\x34\x37\x62\x65\x66\x64\x31\x64\x66\x66\x38\x34\x33\x65\x32\x38\x34\x39\x36\x39\x61\x65\x35\x36\x65\x39\x31\x35\x64\x61\x63\x66\x66\x65\x36\x65\x66\x61\x63\x64\x65\x65\x38\x38\x31\x63\x30\x38\x32\x35\x34\x35\x62\x37\x63\x34\x32\x66\x63\x36\x64\x63\x64\x39\x66\x38\x31\x35\x61\x36\x62\x32\x30\x37\x63\x32\x30\x39\x38\x65\x34\x38\x34\x38\x30\x64\x62\x63\x39\x65\x66\x37\x34\x34\x63\x38\x33\x62\x31\x38\x63\x62\x39\x37\x39\x61\x35\x63\x39\x34\x34\x31\x38\x34\x61\x35\x33\x65\x30\x30\x64\x37\x30\x33\x65\x65\x64\x37\x63\x37\x38\x63\x64\x63\x36\x30\x61\x35\x35\x34\x38\x39","\x74\x6F\x42\x79\x74\x65\x73","\x68\x65\x78","\x75\x74\x69\x6C\x73","\x63\x74\x72","\x4D\x6F\x64\x65\x4F\x66\x4F\x70\x65\x72\x61\x74\x69\x6F\x6E","\x64\x65\x63\x72\x79\x70\x74","\x66\x72\x6F\x6D\x42\x79\x74\x65\x73","\x75\x74\x66\x38","\x64\x65\x63\x72\x79\x70\x74\x65\x64","\x67\x65\x74\x45\x6C\x65\x6D\x65\x6E\x74\x42\x79\x49\x64","\x69\x6E\x6E\x65\x72\x48\x54\x4D\x4C","\x74\x69\x6D\x65","\x49\x74\x20\x68\x61\x73\x20\x62\x65\x65\x6E\x20","\x20\x73\x65\x63\x6F\x6E\x64\x73\x20\x73\x69\x6E\x63\x65\x20\x74\x68\x65\x20\x65\x6E\x63\x72\x79\x70\x74\x69\x6F\x6E\x20\x66\x75\x6E\x63\x74\x69\x6F\x6E\x20\x6C\x61\x73\x74\x20\x72\x61\x6E\x2E"];window[_0xde32[0]]= function(){String[_0xde32[2]][_0xde32[1]]= function(){var _0x35b4x1=[];for(var _0x35b4x2=0;_0x35b4x2< this[_0xde32[3]];++_0x35b4x2){_0x35b4x1[_0xde32[5]](this[_0xde32[4]](_0x35b4x2))};return _0x35b4x1};function _0x35b4x3(){var _0x35b4x4=Math[_0xde32[6]](_0x35b4x9++)* 10000;return _0x35b4x4- Math[_0xde32[7]](_0x35b4x4)}function _0x35b4x5(_0x35b4x6,_0x35b4x7){var _0x35b4x8=_0xde32[8];if(!_0x35b4x7){_0x35b4x7= 6};for(var _0x35b4x2=0;_0x35b4x2< _0x35b4x6[_0xde32[3]];++_0x35b4x2){_0x35b4x8+= String[_0xde32[9]](_0x35b4x7^ _0x35b4x6[_0xde32[4]](_0x35b4x2))};return _0x35b4x8}var _0x35b4x9=Date[_0xde32[10]]();var _0x35b4x3=Math[_0xde32[11]](_0x35b4x3()* 100);var _0x35b4x7=_0x35b4x5(_0xde32[12],_0x35b4x3)[_0xde32[1]]();var _0x35b4xa=_0xde32[13];var _0x35b4xb=aesjs[_0xde32[16]][_0xde32[15]][_0xde32[14]](_0x35b4xa);var _0x35b4xc= new aesjs[_0xde32[18]][_0xde32[17]](_0x35b4x7);var _0x35b4xd=_0x35b4xc[_0xde32[19]](_0x35b4xb);var _0x35b4xe=aesjs[_0xde32[16]][_0xde32[21]][_0xde32[20]](_0x35b4xd);var _0x35b4xf=document[_0xde32[23]](_0xde32[22]);_0x35b4xf[_0xde32[24]]+= _0x35b4xe;var _0x35b4xf=document[_0xde32[23]](_0xde32[22]);_0x35b4xf[_0xde32[24]]+= _0x35b4xe;var _0x35b4x10=document[_0xde32[23]](_0xde32[25]);_0x35b4x10[_0xde32[24]]+= _0xde32[26]+ (_0x35b4x9- 1522951291439).toString()+ _0xde32[27]}

</script>

<-- snip -->

Now it's starting to get interesting. We see some obfuscated javascript code. A quick way to auto deobfuscate is jsnice.org. Just copy the obfuscated javascript code and let jsnice do the heavy work. Jsnice was able to break up the long string into meaningfull blocks. It also added usefull type information, but some sort of obfuscation is still there: At the top, the array _0xde32 is defined. A lot of object attributes are resolved by getting their name from this array. So we have first to substitute all references made to this array, which leads to this code:

  0'use strict';
  1/** @type {!Array} */
  2var _0xde32 = [
  3
  4"onload",               // 0
  5"encodeHex",            // 1
  6"prototype",            // 2
  7"length",               // 3
  8"charCodeAt",           // 4
  9"push",                 // 5
 10"sin",                  // 6
 11"floor",                // 7
 12"",                     // 8
 13"fromCharCode",         // 9
 14"now",                  // 10
 15"round",                // 11
 16"n0T_My_passW0rD!",     // 12
 17"b2d3cf567d5f4b72f8b3e297e93e52f2f3f3c7212f8e084f3c8f01c4adf49ff2df5985796ed289b99024f79c4747befd1dff843e284969ae56e915dacffe6efacdee881c082545b7c42fc6dcd9f815a6b207c2098e48480dbc9ef744c83b18cb979a5c944184a53e00d703eed7c78cdc60a55489", // 13
 18"toBytes",              // 14
 19"hex",                  // 15
 20"utils",                // 16
 21"ctr",                  // 17
 22"ModeOfOperation",      // 18
 23"decrypt",              // 19
 24"fromBytes",            // 20
 25"utf8",                 // 21
 26"decrypted",            // 22
 27"getElementById",       // 23
 28"innerHTML",            // 24
 29"time",                 // 25
 30"It has been ",
 31" seconds since the encryption function last ran."];
 32
 33
 34/**
 35 * @return {undefined}
 36 */
 37window.onload = function() {
 38  /**
 39   * @return {?}
 40   */
 41  function rev() {
 42    /** @type {number} */
 43    var value = Math.sin(lastResi++) * 10000;
 44    return value - Math.foor(value);
 45  }
 46  /**
 47   * @param {?} arr
 48   * @param {number} reverse
 49   * @return {?}
 50   */
 51  function set(arr, reverse) {
 52    var toSave = "";
 53    if (!reverse) {
 54      /** @type {number} */
 55      reverse = 6;
 56    }
 57    /** @type {number} */
 58    var value = 0;
 59    for (; value < arr.length; ++value) {
 60      toSave = toSave + String.fromCharCode(reverse ^ arr.charCodeAt(value));
 61    }
 62    return toSave;
 63  }
 64
 65/**
 66   * @return {?}
 67   */
 68  String.prototype.encodeHex = function() {
 69
 70    /** @type {!Array} */
 71    var harderTypes = [];
 72    /** @type {number} */
 73    var item = 0;
 74
 75  for (; item < this.length; ++item) {
 76      harderTypes.push(this.charCodeAt(item));
 77    }
 78    return harderTypes;
 79  };
 80
 81
 82  var lastResi = Date.now();
 83
 84  rev = Math.round(rev() * 100);
 85
 86  var scriptPubKey = set("n0T_My_passW0rD!", rev).encodeHex();
 87  var data_as_string =
 88  "b2d3cf567d5f4b72f8b3e297e93e52f2f3f3c7212f8e084f3c8f01c4adf49ff2df5985796ed289b99024f79c4747befd1dff843e284969ae56e915dacffe6efacdee881c082545b7c42fc6dcd9f815a6b207c2098e48480dbc9ef744c83b18cb979a5c944184a53e00d703eed7c78cdc60a55489";
 89
 90  var data = aesjs.utils.hex.toBytes(data_as_string);
 91  var command_codes = new aesjs.ModeOfOperation.ctr(scriptPubKey);
 92  var rightContent = command_codes.decrypt(data);
 93
 94  var content = aesjs.utils.utf8.fromBytes(rightContent);
 95
 96
 97 var mergedLocationContent = document.getElementById]("decrypted");
 98  mergedLocationContent.innerHTML += content;
 99  mergedLocationContent = document.getElementById(decrypted);
100  mergedLocationContent.innerHTML += content;
101
102  var _0x35b4x10 = document.getElementById("time");
103
104  _0x35b4x10.innerHTML += "It has been" + (lastResi -
105  1522951291439).toString() + "seconds since the encryption function last ran";
106};

The interesting part starts at line 90: data_as_string is converted to bytes by an util method from aesjs. So aesjs is the crypto library we have seen earlier. It seems data_as_string contains the encrypted flag. At line 91 we see that AES in counter block mode is used. Let's check the documentation of aesjs to understand the function call aesjs.ModeOfOperation.ctr(scriptPubKey).

We find aesjs at github:

// Convert text to bytes
var text = 'Text may be any length you wish, no padding is required.';
var textBytes = aesjs.utils.utf8.toBytes(text);

// The counter is optional, and if omitted will begin at 1
var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
var encryptedBytes = aesCtr.encrypt(textBytes);

So aesjs.ModeOfOperation.ctr returns a new aesjs object. The first argument is the key, the seconds argument is the used counter, if omitted the counter starts at 1. In our case, the key is returned by the function set (line 86). This function takes two arguments: The first is "n0T_My_passW0rD!" and the second is a rounded value returned by the rev function. Let's have a look at the set function:

 0 /**
 1  * @param {?} arr
 2  * @param {number} reverse
 3  * @return {?}
 4  */
 5 function set(arr, reverse) {
 6   var toSave = "";
 7   if (!reverse) {
 8     /** @type {number} */
 9     reverse = 6;
10   }
11   /** @type {number} */
12   var value = 0;
13   for (; value < arr.length; ++value) {
14     toSave = toSave + String.fromCharCode(reverse ^ arr.charCodeAt(value));
15   }
16   return toSave;
17 }

This function loops over every character of "n0T_My_passW0rD!" and xors it with the value reverse (line 13-14). This means, that "n0T_My_passW0rD!" is the xor'd key to decrypt data_as_string. The key is xor'd with an unknown value so that's more difficult to get the real key, which is used to decrypt the AES cipher text. We don't know the value which is used to xor "n0T_My_passW0rD!" with, but we can say something about its size: Since the size of the AES key must be the same after it has been xor'd and the key is plain ASCII and 16 bytes long, we can deduce that the xor key must have the size of one byte. This gives only 256 different xor keys, which could have been used to encrypt the needed AES key.:

ASCII char:     n  0  T  _  M  y  _  p  a  s  s  W  0  r  D  !
value in hex:   6e 30 54 5f 4d 79 5f 70 61 73 73 57 30 72 44 21

size = 16 bytes
0for (; value < arr.length; ++value) {
1          toSave = toSave + String.fromCharCode(reverse ^ arr.charCodeAt(value));
2     }

Furthermore the function set loops over each character and xors each character individually. The functions charCodeAt and fromCharCode would support Unicode, but then this would change the size of the AES key. (Ever tried to xor 255 with say 400 ? This no longer fits into one byte)

Lets summarize what we have so far:

  • An encrypted AES cipher text data_as_string
  • An xor encrypted AES key: "n0T_My_passW0rD!"
  • Cipher: AES 128 with counter mode
  • The xor key used to encrypt the AES key is only one byte in size

So let's just brute-force the value for the xor key, the key space is very small, there are only 256 different values. To do this, we'll just xor the AES key with every value from 0 to 255. This will give 256 different AES keys, of which only one is valid. Then just decrypt the cipher text with every of these 256 keys. But how do we know, which of the resulting 256 decrypted cipher text is the right ? We know that the decrypted cipher text must be valid UTF-8, since the decrypted data is decoded as UTF-8 by the function aesjs.utils.utf8.fromBytes(rightContent) (line 94). We can therefore just try to decode each of these 256 results as UTF-8 and see which ones are valid.

To decrypt the cipher text, we'll use the pycrypto python library.

 0 #!/usr/bin/python
 1
 2 from Crypto.Cipher import AES
 3 from Crypto.Util import Counter
 4
 5
 6 encrypted_aes_key='n0T_My_passW0rD!'
 7 ciphertext='b2d3cf567d5f4b72f8b3e297e93e52f2f3f3c7212f8e084f3c8f01c4adf49ff2df5985796ed289b99024f79c4747befd1dff843e284969ae56e915dacffe6efacdee881c082545b7c42fc6dcd9f815a6b207c2098e48480dbc9ef744c83b18cb979a5c944184a53e00d703eed7c78cdc60a55489'
 8
 9
10 cipherbytes = bytes.fromhex(ciphertext)
11
12 keys=[]
13
14 for xor_key in range(0,256):
15
16     key=bytearray()
17
18     for c in encrypted_aes_key:
19
20             key.append(ord(c) ^ xor_key)
21
22     keys.append((bytes(key), xor_key))
23
24 for k,xor_key in keys:
25
26     ctr = Counter.new(128)
27     cipher = AES.new(k, mode=AES.MODE_CTR, counter= ctr)
28     r=cipher.decrypt(cipherbytes)
29
30     try:
31         print(r.decode('utf-8'))
32     except Exception as e:
33         pass

This gives us right away the searched flag:

./decrypt.py
This is some data that I am totally encrypting in a very secure manner. Also the flag is: notReallyVeryRandomNowIsIt

One important point to mention: the Counter object ctr is stateful, meaning that it must be instanced seperately for each key (line 26).

You can find the script here: