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;
  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];

#many lines dismiss

always @(posedge clk) begin
    #1 ok = ok & (out == tmp);
  • 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])

secret = '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):
flag = [0]*56
for 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='')
i =  7


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
import requests
from 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:
$ 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='.*'

0             0x0             PNG image, 980 x 746, 8-bit/color RGBA, non-interlaced
41            0x29            Zlib compressed data, default compression
808562        0xC5672         PNG image, 980 x 492, 8-bit/color RGBA, non-interlaced
808603        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!"

import angr
p 	  	= angr.Project("./task")
start 	= 0x00009a95 + 0x400000
good 	= 0x009AB7 + 0x400000 
bad 	= 0x009AC5 + 0x400000
st 	  = p.factory.blank_state(addr=start)
#specify the flag and its storing address in memory
st.regs.rbp = st.regs.rsp
st.regs.rsp = st.regs.rbp - 0x50
flag = 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 input
sm = 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='')
CLB An toàn Thông tin Wanna.W^n chia sẻ một số Challenges giải được và việc chia sẻ writeup nhằm mục đích giao lưu học thuật. Mọi đóng-góp ý-kiến bọn mình luôn-luôn tiếp nhận qua mail: wannaone.uit@gmail.com hoặc inseclab@uit.edu.vn và fanpage: fb.com/inseclab Cám ơn các thành viên Wanna.W^N @InSecLab đã đóng...
Trong bài viết này, CLB Wanna.W^n sẽ hướng dẫn các bạn cách sử dụng nền tảng wannaGame với các bước thực hiện như sau: Đăng ký tài khoản Bước 1: Truy cập đường dẫn nền tảng wannaGame: https://cnsc.uit.edu.vn/ctf/ Bước 2: Chọn nút Register Bước 3: Tại màn hình đăng ký,...
CLB An toàn Thông tin Wanna.One chia sẻ một số Challenges giải được và việc chia sẻ writeup nhằm mục đích giao lưu học thuật. Mọi đóng-góp ý-kiến bọn mình luôn-luôn tiếp nhận qua mail: wannaone.uit@gmail.com hoặc inseclab@uit.edu.vn và fanpage: fb.com/inseclab Web @AP DISCO PARTY @AP GITFILE EXPLORER @AP miniblog++...