The Wanna.One team shares writeup of some solved Challenges with the purpose of academic exchanges. We always welcome and look forward to comments from any of you via email: wannaone.uit@gmail.com
Bamboofox CTF( Sat, 16 Jan. 2021, 09:00 ICT — Mon, 18 Jan. 2021, 09:00 ICT)
Re
Author: phuonghoang
This is the first CTF event which I solved up to 4 RE challenges. However, but I think that the number of challenges solved should be five. Although, I tried my best and this CTF event was so near with the final tests of the first semester.
Tet is coming, Lunar New Year is coming. For relaxing, I'm free from today( 24th, January 2021) to Sunday( 7th, March, 2021). In this stage, I will take an effort to advance RE skills.
#Flag Checker
This challenge has an archive with three source files( magic.v, chall.v, t_chall.v) which were wrote Verilog language. They're clear, so we only need to understand their source code and get flag.
All sources and solved code is found at here.
About Verilog hardware-language:
All statements are executed in parallel
Code ordering is flexible.
Understand the flow of program
- Firstly, open all source codes of challenge, we will recognize that t_chall.v will call chall.v module via this instruction:
chall ch(.clk(clk), .rst(rst), .inp(inp), .res(out));
And similarly, chall.v will call magic.v module by using some following instructions as:
magic m0(.clk(clk), .rst(rst), .inp(inp), .val(val0), .res(res0));magic m1(.clk(clk), .rst(rst), .inp(res0), .val(val1), .res(res1));magic m2(.clk(clk), .rst(rst), .inp(res1), .val(val2), .res(res2));magic m3(.clk(clk), .rst(rst), .inp(res2), .val(val3), .res(res3));
A module in Verilog has module header and module body. A module header contains variables such as input, output, clkand rst( rst, clk relate to hardware signal, we can ignore them in this challenge). And module body will contain some procedures.
The definition of constants in Verilog supports the addition of a width parameter. The basic syntax is: <Width in bits>'<base letter><number> Ex: 8'b01001101 --> 8-bit binary 01001101 Ex: 8'd182 --> 8-bit decimal 182
magic.v
module magic( input clk, input rst, input[7:0] inp, input[1:0] val, output reg[7:0] res); always @(*) begin case (val) 2'b00: res = (inp >> 3) | (inp << 5); 2'b01: res = (inp << 2) | (inp >> 6); 2'b10: res = inp + 8'b110111; 2'b11: res = inp ^ 8'd55; endcase endendmodule
- Module magic is a simple switch-case procedure: base on 2 bits of val variable, inp will process and store a new result into res output.
chall.v
wire[1:0] val0 = inp[1:0]; wire[1:0] val1 = inp[3:2]; wire[1:0] val2 = inp[5:4]; wire[1:0] val3 = inp[7:6];
- chall.v module receives a parameter( 8-bit length) inp from t_chall.v module, then process that input( cutting into four 2-bit pieces: val3, val2, val1, val0, respectively from the most significant bit to the least significant bit).
magic m0(.clk(clk), .rst(rst), .inp(inp), .val(val0), .res(res0)); magic m1(.clk(clk), .rst(rst), .inp(res0), .val(val1), .res(res1)); magic m2(.clk(clk), .rst(rst), .inp(res1), .val(val2), .res(res2)); magic m3(.clk(clk), .rst(rst), .inp(res2), .val(val3), .res(res3));
inp and val0 as two input parameters is passed to magic module. After, output received( res0) as a new input in m1 and so on.
Finally, res3 is the return value to t_chall module:
end else begin assign res = res3;
t_chall.v
chall ch(.clk(clk), .rst(rst), .inp(inp), .res(out));...#many lines dismiss...for (idx = 0; idx < 32; idx++) begin inp = flag[idx]; tmp = target[idx]; #4;end...#many lines dismiss...always @(posedge clk) begin #1 ok = ok & (out == tmp);end
In this module, we recognize each character of flag string is a inp variable passing to chall module and receive out variable in output field.
- Then, out is compared to each element of target array, respectively.
Summary - solve.py
Brute-force all elements of flag string( appropriate characters are in range from 32 to 127(in ASCII)) and find out original character.
- Because there are more than one satisfied characters for each element of target, so let's choose the most appropriate one to create flag.
#BetterThanASM
Source of this challenge is an assembly format file of llvm compiler, it's similar with assembly language( .s, .asm extensions) of GCC compiler.
The source file can be found at here.
When I saw that source file with .ll extension, it makes me confused because I haven't work on it before. After searching Google in quite long time, I know it is assembly of LLVM compiler. Here, I have two ways to solve challenge: (1) read assembly, understand deeply it or (2) using clang compiler assembly- source code to executable file( binary file) and using normally IDA tools to solve.
Clang compiler uses the LLVM compiler infrastructure as its back end and it is developed to act as a drop-in replacement for the GCC.
In this write-up, I will present the second way to solve challenge( it's the significant difference about time comparing to reading entirely assembly format of llvm, which solution I did while the contest was running).
Installing clang on Kali Linux
sudo apt-get install clang# create executable file from assembly format.clang task.ll -o task
After compiling the task.ll with clang, I tried to run executable file and type string as 0123456789, but receive fail notification as below:
$ ./task Only the chosen one will know what the flag is!Are you the chosen one?flag: 0123456789?????? You are not the chosen one! ??????
Find the right input
Opening IDA and import task file:
- Firstly, user-input must be equal with the length of given what string( len = 56) and then, if it's satisfied, it's passed as only parameter into check function.
check() function
Re-open with ghidra tool to read pseudo- code of this function:
- We easy conclude that the return value( initially, its value is 1) of this function depends input string and what string:
- if it's 1 then all elements of input must satisfy above equation( choose this case)
- if it's 0, we will have a lot's of potential input which can happen( ignore this case)
main() function
As mentioning above, I guess that the return value of check() function is 1, so I will pay my attention to the second branch of program: each element of input will be XOR-ed secret string before printing to screen.
Because there are no additional hints about original flag, so I brute-forced all possible characters of flag( in range 32-127, in ASCII). We also noticed that, when I have the first character of input, I easily can get its original form( xor with secret[i]) and second character's original form( xor with what[i])
solve_BetterThanASM.pysecret = 'B\n|_"\x06\x1bg7#\\F\n)\t0Q8_{Y\x13\x18\rP'what = "\x17/'\x17\x1dJy\x03,\x11\x1e&\nexjONacA-&\x01LANH'.&\x12>#'Z\x0fO\x0b%:(&HI\x0cJylL'\x1emtdC"alphabet =[]for i in range(30, 127): alphabet.append(i)flag = [0]*56for i in alphabet: print( 'i = ', chr(i)) flag[0] = i for j in range(0, len(flag) - 1): tmp = flag[j] ^ ord(secret[j %len(secret)]) #flag[i] ^ secret ori_flag_i_1 = tmp ^ ord(what[j]) #ori_flag[i] ^ what --> ord_flag[i + 1] flag[j + 1] = ori_flag_i_1 ^ ord(secret[(j + 1) % len(secret)]) print('flag{', end='') for k in flag: print(chr(k), end='') print('}') print('\n**********************************************************\n')print()#**********************************************************'''i = 7flag{7h15*********************************************}**********************************************************'''
#Ransomware
This is the combination of RE and simple Crypto challenge.
Getting Started
- Firstly, I opened an archive file( download them at here) containing some source files of this challenge. There are a compiled byte-code (.pyc extension) of python script( task.pyc) and an encrypted data file( flag.enc).
- Using uncompyle6( installing here) to decompile task.pyc, I get the original python-script as below:
$ uncompyle6 task.pyc # uncompyle6 version 3.7.4# Python bytecode 3.8 (3413)# Decompiled from: Python 3.8.5 (default, Jul 28 2020, 12:59:40) # [GCC 9.3.0]# Embedded file name: task.py# Compiled at: 2021-01-14 21:13:24# Size of source mod 2**32: 420 bytes(lambda data, key, iv: if len(data) != 0:(lambda key, iv, data, AES: open('flag.enc', 'wb').write(AES.new(key, AES.MODE_CBC, iv).encrypt(lambda x: x + b'\x00' * (16 - len(x) % 16)(data))))(data[key:key + 16], data[iv:iv + 16], open('flag.png', 'rb').read(), __import__('Crypto.Cipher.AES').Cipher.AES) # Avoid dead code: lambda fn: __import__('os').remove(fn)('task.py'))(__import__('requests').get('https://ctf.bamboofox.tw/rules').text.encode(), 99, 153)# okay decompiling task.pyc
Solve
- After achieving original python-script, I'm confused with it because the script is mess and looks like unreadable code. So, I needed to petrus's support( a crypto-player of my team) and he send me to python-script, which can be based-on decompiled code in order to decrypt flag.enc:
solve.pyimport requestsfrom Crypto.Cipher import AESdata = requests.get('https://ctf.bamboofox.tw/rules').text.encode()key = data[99:99+16]iv = data[153:153+16]with open('flag.enc','rb') as f1: ciphertext = f1.read()plaintext = AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext)with open('flag_decrypted', 'wb') as f2: f2.write(plaintext)
- Running the above script, and I see that the new file "flag_decrypted" is generated. Examining it using file command:
$ file flag_decrypted flag_decrypted: PNG image data, 980 x 746, 8-bit/color RGBA, non-interlaced
- Adding an extension( .png) to new file and opening it:
- In this stage, I also confused when I don't have an idea to get the flag :( . I needed to the Stirring's support to get the final image, which contains the flag's content:
$ binwalk flag_decrypted.png --dd='.*'DECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 PNG image, 980 x 746, 8-bit/color RGBA, non-interlaced41 0x29 Zlib compressed data, default compression808562 0xC5672 PNG image, 980 x 492, 8-bit/color RGBA, non-interlaced808603 0xC569B Zlib compressed data, default compression
- Maybe I guess that the second image having offset as 0xC5672 is flag-image. Opening and wow, it's real flag:
#Flag Checker Revenge
To passing this challenge, you can solve about 500 equations of each character of flag or using z3( maybe impossibly with 500 constraint expressions) or using angr module of python.
While the contest was running, I solved this challenge with worst way which I manually found out every element of original flag. It's so stupid and it took me about some hours. After studying about angr framework and reading some examples using it, I solved this challenge again by using angr. The source file of this challenge can be found at here.
Getting started
Open task file with IDA tool, I recognized that:
- The length of satisfied input is 0x2b( 43)
- there are so many checking functions which examine if our input satisfied or not:
- That time, I think about z3 and angr. I used to z3 solver, but with the big number of checking functions, using the z3 tool is impossible. I also haven't angr framework before, so I decided to solve challenge manually. It's successful but it's the most worst way.
Using angr framework
A cool thing in angr is that symbolic execution. Say simply, tools of symbolic execution try to find all inputs of executable file which support for finding out the special output of that binary file. So, using angr in this challenge, it helps us generate the most appropriate input to achieve executable branch such as "OK, you win!"
angr finds out the suitable inputs based on the address of instruction we want to jump to. They usually display good and bad address( bad address should not avoided).
Additionally, we also specify the starting address and the position of flag in memory is in simulation-time
Finally, each character of flag range from 32 to 127( in ASCII), so I add two constraints to solver. Below this is solved-script of this challenge:
solve_FlagCheckerRevenge.pyimport angrp = angr.Project("./task")start = 0x00009a95 + 0x400000good = 0x009AB7 + 0x400000 bad = 0x009AC5 + 0x400000st = p.factory.blank_state(addr=start)#specify the flag and its storing address in memoryst.regs.rbp = st.regs.rspst.regs.rsp = st.regs.rbp - 0x50flag = st.solver.BVS("flag", 0x2B * 8)st.memory.store(st.regs.rbp - 0x50, flag)#each element of flag range in (32, 127)for i in range(0x2B): char = (flag >> (8 * i)) & 0xFF st.solver.add(0x20 < char) st.solver.add(char < 0x7f)#simulate execuatable file and find out the suitable inputsm = p.factory.simgr(st)sm.explore(find=good, avoid=bad)if sm.found: solution = sm.found[0].solver.eval(flag) flag_hex_str = hex(solution)[2:] for i in range(0, len(flag_hex_str), 2): print(chr(int(flag_hex_str[i:i+2], 16)), end='')print()