Flare-on is an annual competition hosted by Mandiant, consisting of several reverse engineering challenges. It was first hosted in 2014.
I stumbled upon a unique write-up by @gaasedelen for Flare-on 2014 Challenge 6, where Dynamic Binary Instrumentation was employed to swiftly solve the challenge. Pin appeared highly beneficial to me, prompting me to delve into it further and tackle the challenge.
Content
About Pin
Pin is a dynamic binary instrumentation framework developed by Intel for IA-32 and x86-64 architectures. It helps programmers, reverse engineers look inside computer programs while they are running. It’s great for understanding how software works and finding bugs or making it run faster.
It empowers the creation of dynamic program analysis tools like Intel VTune Amplifier and Intel Inspector. These tools, known as Pintools, enable analysis on user space applications across Linux, Windows, and macOS. Originally for computer architecture analysis, Pin’s versatility and active community have spawned tools for security, emulation, and parallel program analysis.
For more infromation, you can refer to its offical documentation : https://software.intel.com/sites/landingpage/pintool/docs/98830/Pin/doc/html/index.html
Simple Instruction Count Pintool
Simple Instruction Count Pintool is a tool created using Pin dynamic binary instrumentation framework. Its purpose is to count the number of instruction executed by a program.
It intruments the bianry at runtime, tracks the execution flow and increses a counter for each instruction encountered.
As shown in the image below, this Pintool has been utilized to calculate the number of instructions executed during the runtime of the command ls
.
This Pintool can be used to solve crackmes or password checkers. Flare-on 2014 challenge 6 can be solved using this Pintool as follows…
Flare-on 2014 challenge 6
In this challenge, a binary is provided, which can be downloaded from the official website, and the password for the archive is malware
.
e7bc5d2c0cf4480348f5504196561297: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=c65164a247cb0c44cab89c0fc06980bf6c082011, stripped
The binary is an ELF64 executable. When executed with varying numbers of arguments, it produces different responses.
Opening it in IDA reveals its complexity, with 1837 functions and extensive mathematical content. Reviewing each function would take days.
Number of Arguments
To verify the correct number of arguments, Simple Instruction Count Pintool can be utilized to count the executed instructions. In the case of the correct number of arguments, it will execute the largest number of instructions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def readInst():
f = open("insCount.txt", "r")
inst = f.read()
inst = int(inst[6:-1])
return inst
command = ["../../../pin", "-t", "obj-intel64/inscount0.so", "-o", "insCount.txt", "--", "./e7bc5d2c0cf4480348f5504196561297"]
def argNum():
instArr = []
for i in range(4):
print("[!] Arguments passed : ", i+1)
args = "A "
command.append(args)
subprocess.run(command)
inst = readInst()
instArr.append(inst)
print("[!]Instruction Count : ", inst)
maxIndex = instArr.index(max(instArr))
print("[+] number of arguments required by the binary : ", maxIndex+1)
return maxIndex
Length of first argument
The length of the first argument can also be calculated using this Pintool.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def arglen():
instArr = []
for i in range(20):
command = ["../../../pin", "-t", "obj-intel64/inscount0.so", "-o", "insCount.txt", "--", "./e7bc5d2c0cf4480348f5504196561297"]
arg1 = "1"*(i+1)
arg2 = "2"*10
command.append(arg1)
command.append(arg2)
subprocess.run(command)
inst = readInst()
instArr.append(inst)
print("[!] arg1 len passed : ", i+1, ", inst count : ", inst)
maxIndex = instArr.index(max(instArr))
print("[+] length of argument 1 should be : ", maxIndex+1)
return maxIndex
First argument
Now to calculate the first argument character by character, iterating through all possible characters. If the first character is incorrect, the program exits; if correct, it proceeds to further execution, aiming to maximize the instruction count.
However, this approach encountered a problem as it yielded the same instruction count for all possible characters.
Now, when searching for the word “bad” in the first argument, two occurrences are found. In these instances, there is an XOR operation with 0x56 being performed, and the result is passed into a function along with a string and 10 as the third argument.
1
2
3
4
5
6
7
>>> a = "bngcg`debd"
>>> arg1 = ""
>>> for i in a:
... arg1 += chr(ord(i)^0x56)
...
>>> arg1
'4815162342'
When the first argument “4815162342” is passed, the binary hangs up. Searching for “sleep” in IDA confirms its use of the sleep syscall. By Nop that syscall by NOP instruction and restarting, the binary exits without printing anything.
Second argument
Now, for the second argument, attempt the approach initially used for the first argument. Surprisingly, this yields the flag for the challenge.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def solveArg2(length):
arg2 = "l1nhax.hurt.u5.a"
l = len(arg2)
arg2 = arg2 + "2"*(length-l)
arg1 = "4815162342"
for i in range(l, length):
print("[!] i : ", i)
instArr = []
for j in charSet:
arg2_list = list(arg2)
arg2_list[i] = j
arg2 = ''.join(arg2_list)
command = ["../../../pin", "-t", "obj-intel64/inscount0.so", "-o", "insCount.txt", "--", "./e7bc5d2c0cf4480348f5504196561297"]
command.append(arg1)
command.append(arg2)
subprocess.run(command)
inst = readInst()
instArr.append(inst)
print("[!] char : ", j, "instruction count : ", inst)
maxIndex = instArr.index(max(instArr))
arg2_list = list(arg2)
arg2_list[i] = charSet[maxIndex]
arg2 = ''.join(arg2_list)
print(arg2)
Voila, we’ve obtained the flag!
1
l1nhax.hurt.u5.a1l@flare-on.com