Well, I guess I can post the script and results, now that Flare-On has been over for over a month! I'll continue from where I left off in my
previous article.
Previously, I implemented a function to skip the ReadFile call and stuff arbitrary passwords into the challenge binary's input stream. By experimenting with this, I discovered a potential tell that might allow me to brute force the password by counting the number of times a particular instruction was executed. To make use of this, I needed to execute the binary several times with different passwords (aaaa..., abaa..., acaa...), moving on to the next character each time an additional sahf instruction was executed.
To do this, I knew I would first need to figure out which characters are valid for email addresses. I settled on a variant of the information available from
a stackoverflow inquiry:
g_valid = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.#-_~!$&\'()*+,;=:%{|}\\/?@'
The next step was to initialize a buffer that would serve as my brute force password. How long could the password be? Based on the length comparison at the start of the password processing function...
It looked as if the program was seeking a 37-byte password. So, I whipped one up:
g_pw = list('a' * 37)
In order to move through candidate passwords, I iterated through valid characters for each character position of the brute force password:
for pos in range(0,37):
for c in g_valid:
g_pw[pos] = c
To measure my progress, I executed the binary with one breakpoint set to stuff the brute force password into the program's input buffer and the other breakpoint set to count sahf instructions:
trace = vtrace.getTrace()
trace.execute(sys.argv[1])
trace.addBreakpoint(MyBreakpoint(0x400000 + 0x1046, skipread))
trace.addBreakpoint(MyBreakpoint(0x400000 + 0x10ae, count_iters))
trace.setMode("RunForever", True)
trace.run()
And finally, I compared the number of sahf breakpoints hit in each successive occurrence against the previous maximum. If an additional breakpoint was hit, I advanced to the next character position in the brute force password and began working on that character. The script in its entirety looked something like this:
1
2 import vtrace
3 import vdb
4 import sys
5 import struct
6
7
8
9
10
11
12
13
14
15
16
17 class MyBreakpoint(vtrace.Breakpoint):
18 def __init__(self, address, callback):
19 vtrace.Breakpoint.__init__(self, address)
20 self.address = address
21 self._cb = callback
22
23 def notify(self, event, trace):
24 self._cb(trace)
25
26
27 g_bp_count = 0
28
29
30 def count_iters(trace):
31 global g_bp_count
32 g_bp_count = g_bp_count + 1
33
34
35
36 def skipread(trace):
37 trace.setProgramCounter(0x400000 + 0x104c)
38 print "\nTrying " + ''.join(g_pw)
39 tmp = ''.join(g_pw)
40 trace.writeMemory(0x400000 + 0x2159, tmp)
41 trace.writeMemory(trace.parseExpression("rbp-4 & 0xffffffff"), struct.pack("@i", 37))
42
43 if len(sys.argv) != 2:
44 print "Usage: test.py filename"
45 sys.exit(1)
46
47
48 g_valid = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.#-_~!$&\'()*+,;=:%{|}\\/?@'
49
50 g_pw = list('a' * 37)
51 g_pw.append("\0")
52 g_breaks = 0
53 while True:
54 for pos in range(0,37):
55 for c in g_valid:
56 g_pw[pos] = c
57 g_bp_count = 0
58
59 trace = vtrace.getTrace()
60 trace.execute(sys.argv[1])
61 trace.addBreakpoint(MyBreakpoint(0x400000 + 0x1046, skipread))
62 trace.addBreakpoint(MyBreakpoint(0x400000 + 0x10ae, count_iters))
63 trace.setMode("RunForever", True)
64 trace.run()
65 print "Breaks " + str(g_breaks)
66
67 if (g_bp_count > g_breaks):
68 g_breaks = g_bp_count
69 break
The result was a little bit like a cinematic depiction of a password being brute forced, because the characters kept rolling by and locking in as the attack progressed:
|
Very Success! |
In the screenshot above, you can see the output of the CTF binary (very_success.exe) interspersed with the script indicating what password was being tried (e.g. "Trying a_Little_b1t_baaaa...").
Using vtrace made this an incredibly cool learning experience, but it wasn't without its pitfalls. For instance, I was really excited when I found this snippet in the help for the Trace class:
call(self, address, args, convention=None)
Setup the "stack" and call the target address with the following
arguments. If the argument is a string or a buffer, copy that into
memory and hand in the argument.
However, when I tried using this, I was disappointed to learn that I was out of luck:
|
Well, crap. |
It could be that running a 32-bit binary in a WoW64 environment causes this condition, but I didn't have time to investigate, so I just went with executing the program over and over and over again, which worked fine.
There was also a flaw in my own process. I noticed that at various character positions, my assumptions broke down and the brute forcer ran off into the weeds. I suspect this was a function of the binary itself and not of the instrumentation tool (vtrace). With a little guesswork and some script adjustment, I was able to resume from various points in the process and pull the remainder of the password out of the binary:
a_Little_b1t_harder_plez@flare-on.com
After I submitted the flag, FLARE sent me the next binary and I celebrated by abandoning the CTF altogether, as I ultimately knew might happen due to my busy life. But I was glad I took the approach of figuring out how to create a scalable and repeatable instrumentation process for analyzing and abusing binaries. There are other tools out there, but vtrace is unique in that it doesn't require you to compile a DLL,
and it allows you to make use of Python to instrument applications. Very useful!