Writeup | BambooFox CTF 2021 | Reverse engineering

10:07 25/01/2021

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)


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

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
  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];
  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));
end else begin      assign res = res3;
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
  • 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.


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
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*********************************************}​**********************************************************​'''


This is the combination of RE and simple Crypto challenge.

Getting Started
$ 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.pyimport requestsfrom Crypto.Cipher import AES​data = 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)
$ 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

#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: 

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!"

solve_FlagCheckerRevenge.pyimport angr​p 	  	= 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()
Sau 4 buổi học với đa dạng góc nhìn về đề tài, gợi mở nhiều kiến thức liên quan đến An toàn thông tin (ATTT), khoá huấn luyện WannaQuest.Q2023.02 đã tạo tiền đề cho nhiều sinh viên tham gia hiểu rõ hơn tầm quan trọng của việc tham gia nghiên...
Thông qua nhiều sự kiện, hoạt động học thuật sôi nổi, WannaQuest được đánh giá cao khi trở lại với mùa thứ 2 với khóa huấn luyện WannaQuest.Q2023.02, một nơi đã trở thành nơi kết nối, giao lưu, tiếp nhận kiến thức An toàn thông tin (ATTT) theo cách cởi...
WannaGame Weekly UTCTF, ångstromCTF, Grey Cat The Flag, ImaginaryCTF, SekaiCTF, Downunder CTF, TeamItaly CTF, CTFZone, Asis Final, SEETF, Bauhinia... UIT Honor dice, Real World, bi0s, Seccon, pbctf, Kalmarctf, hxp, Plaid, m-leCon, HackTM, p4ctf, justCTF, codegate, Google, zer0pts, Defcon, HITCON, Hack.lu, N1CTF, Brics+, 0CTF/TCTF, Balsn, RuCTF (AD), FAUST (AD), saarCTF (AD)......