This is a write-up for the Madagascar challenge of r2con 2018 CTF.
Are you sure what you see?
binary: scrabble_ad92a0e308d220dc89c4bc133e0ce64c
The string "Wk2p2pm3wg7eo7d" is quickly found using strings
and naively inserted
as a valid flag, obviously without success ;-)
Open the binary with radare2 and explore the functions and the main:
$ radare2 scrabble_ad92a0e308d220dc89c4bc133e0ce64c
[0x004004c0]> aaa
[0x004004c0]> e asm.pseudo = 1
[0x004004c0]> e asm.bytes = 0
[0x004004c0]> afl
0x00400460 3 26 sym._init
0x00400490 1 6 sym.imp.puts
0x004004a0 1 6 sym.imp.__stack_chk_fail
0x004004b0 1 6 sym.imp.printf
0x004004c0 1 6 sym.imp.__libc_start_main
0x004004d0 1 6 sub.__gmon_start_4d0
0x004004e0 1 41 entry0
0x00400510 4 50 -> 41 sym.deregister_tm_clones
0x00400550 4 58 -> 55 sym.register_tm_clones
0x00400590 3 28 sym.__do_global_dtors_aux
0x004005b0 4 38 -> 35 entry1.init
0x004005d6 11 234 main
0x004006c0 4 101 sym.__libc_csu_init
0x00400730 1 2 sym.__libc_csu_fini
0x00400734 1 9 sym._fini
[0x004004c0]> s main
[0x004005d6]> pdf
Looking at the main function shows interesting information.
First local_10h
is initialized with a pointer to the string "Wk2p2pm3wg7eo7d",
0x0040060e qword [local_10h] = str.Wk2p2pm3wg7eo7d
and below block loops through all chars until \0
is hit.
0x00400666 rax = qword [local_10h]
0x0040066a eax = byte [rax]
0x0040066d var = al & al
0x0040066f if (var) goto 0x400626
The block at address 0x00400626
called for each valid char in the string is
decomposed below.
Note that:
local_10h
is a pointer to the string ("Wk2p2pm3wg7eo7d")local_30h
is a pointer to argvlocal_18h
is an int initialized to 0
0x00400626 rax = qword [local_10h]
0x0040062a eax = byte [rax]
Load char pointed by local_10h
0x0040062d eax ^= 3
0x00400630 byte [local_19h] = al
0x00400633 rax = [local_19h]
0x00400637 eax = byte [rax]
0x0040063a edx = al
do some operations and store in edx:
rdx = ((*local_10h) ^ 3) & 0xf
0x0040063d rax = qword [local_30h]
0x00400641 rax += 8
rax points now to the input string
0x00400645 rcx = qword [rax]
0x00400648 eax = dword [local_18h]
0x0040064b cdqe
0x0040064d rax += rcx
0x00400650 eax = byte [rax]
0x00400653 eax = al
rax points now to one char in our input
rax = input[local_18h] & 0xf
0x00400656 edx -= eax
0x00400658 eax = edx
rax gets the value of rdx - rax
0x0040065a dword [local_14h] += eax
add rax to local_14h
0x0040065d qword [local_10h] += 1
0x00400662 dword [local_18h] += 1
increment the pointer/counter
In other words:
input = "AAAAA"
string = "Wk2p2pm3wg7eo7d"
index = 0
for s in string:
s = (s ^ 0x3) & 0xf
i = input[index] & 0xf
res += (s-i)
index += 1
Finally the test that defines if we found the correct flag is done
in block at 0x00400671
0x00400671 var = dword [local_14h] - 0
0x00400675 if (!var) goto 0x400688
where local_14h
should be 0 in order to win.
That means that each input char must be equal to each respective char of the internal string xored with 0x03.
Here's a small python one-liner to find the flag:
$ python
>>> ''.join([chr(ord(x)^0x3) for x in "Wk2p2pm3wg7eo7d"])
'Th1s1sn0td4fl4g'
Enter the flag and win!
I then tried to solve this with angr as an exercise. It didn't work not because angr wasn't able to find a solution but because it was finding too many solutions.
The reason is that local_14
needs to be 0 for the flag
to be valid (as shown above) however it doesn't say that some characters comparisons cannot
be different to zero as long as those are being compensented and that the
final value of local_14
is 0. What means that there isn't a single solution to it but
multiple.
Still only one flag was valid for scoring that challenge. If that was purposely
used to avoid the use of angr, it worked ;-)
Below my angr script as a reference
#!/usr/bin/env python2
import angr
import claripy
path = './scrabble_ad92a0e308d220dc89c4bc133e0ce64c'
p = angr.Project(path, load_options={'auto_load_libs':False})
state = p.factory.entry_state()
# construct the argument
argv = [p.filename]
size = 16 # password size
pwd = claripy.BVS('sym_arg', 8*size)
argv.append(pwd)
state = p.factory.entry_state(args=argv)
for i in xrange(size):
current_byte = pwd.get_byte(i)
state.add_constraints(
claripy.Or(
claripy.And(current_byte >= 'a', current_byte <= 'z'),
claripy.And(current_byte >= 'A', current_byte <= 'Z'),
claripy.And(current_byte >= '0', current_byte <= '9')
)
)
sm = p.factory.simulation_manager(state)
sm.explore(find = 0x400688, avoid = (0x00400677, 0x4005fa))
if len(sm.found) > 0:
# we have a valid path to success
found = sm.found[0]
result = found.solver.eval_upto(argv[1], 10, cast_to=str)
print '\n'.join(result)