Aperi’CTF 2019 - U_u
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
Aperi’CTF 2019 | U_u | Reverse | 175 | 3 |
VoUs AlL3z 4d0R3r PyTh0n !
Challenge: U_u.py - md5sum : 985e5c28dd1fbb3ec233edf70b82f326
TL;DR
It was a character comparison with an “uu” encode. The script uses its own source code to compare characters, which makes debugging less easy.
Methodology
Full code
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from __builtin__ import iter as var
import random
import hashlib
__,____,___,_____ = sys,open,eval,False
random.seed(hashlib.sha1(open(__file__).read()).hexdigest())
_ = lambda x : x+1
v = var(___("__ohvygvaf__.enj_vachg".encode("rot_13")+"()").encode("uu"))
exec("w=[];".encode("rot_13"))
while 1<2:
try:
j = j+[v.next()]
except StopIteration:
r = ____(__file__[:-(len(j))]).read()
z=var(''.join(j[::-1]))
# :D
_____,j = not 1,[]
while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "3b32b5601c722e59fd5b0ba81c31f230c3666ca1":
try:
x = z.next()
j = j+[x]
except StopIteration:
if ''.join(j).index("5=UMW22!50") == 37:
import antigravity
__.exit("You van validate with the flag :)")
if (_____ == 7 and ord(j[_____]) != _____+25) or \
(_____ == 8 and ord(j[_____]) != _____+24) or \
(_____ == 10 and j[_____] != r[-201]) or \
(_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
(_____ == 12 and j[_____] != r[240].upper()) or \
(_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
(_____ == 14 and j[_____] != ",") or \
(_____ == 15 and j[_____] != chr(ord("L")+5)) or \
(_____ == 16 and j[_____] != str(int(r[1088])-1)) or \
(_____ == 17 and j[_____] != """'""") or \
(_____ == 18 and j[_____] != chr(ord("+")+2)) or \
(_____ == 9 and j[_____] != r[-845]):
sys.exit()
_____ += 1
while len(__file__)%2:
break
__file__ += chr(random.randint(32,0x7e))
if (_____ == 28 and j[_____] != "]") or \
(_____ == 40 and j[_____] != ")") or \
(_____ == 29 and j[_____] != "E") or \
(_____ == 30 and j[_____] != "3") or \
(_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
(_____ == 43 and j[_____] != "7") or \
(_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
(_____ == 33 and j[_____] != "P") or \
(_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
(_____ == 35 and j[_____] != "#") or \
(_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-") or \
(_____ == 37 and j[_____] != "?") or \
(_____ == 39 and j[_____] != "&") or \
(_____ == 42 and j[_____] != "=") or \
(_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2))):
__.exit(":(")
_(_____)
Values
Let’s use python interpreter to get value from the different strings. Since the script import __builtin__
and that you can’t import __builtin__
in python3, we can confirm that the script run on python2.
Let’s change __file__
with "U_u.py"
in seed generation to get the seed (we’ll see in the next lines that the random part is useless).
>>>import hashlib
>>>
>>>hashlib.sha1(open("U_u.py").read()).hexdigest()
'2a2173559b717dadfe2643c937a5cacd7a8d69c0'
>>>
>>>"__ohvygvaf__.enj_vachg".encode("rot_13")+"()"
'__builtins__.raw_input()'
v
is the raw_input. This is encoded with uuencode (see .encode("uu")
). Then, v is set as an iterator (see import iter as var
). In other word: each iteration on v
will be a letter of the uuencode() for the input.
>>>"w=[];".encode("rot_13")
'j=[];'
j
is a list.
Part 1
Now let’s analyze this part of the code:
while 1<2:
try:
j = j+[v.next()]
except StopIteration:
# exception
# code
We got an infinite loop with a try on the iterator v
and a StopIteration exception. This is the equivalent of a for loop on v
. Here on each iteration, we append the next element of v
to the j
list. The # code
is reached after each try
. The except
is reached at the end of the iterator. In other word, the except
is the equivalent of the code after the for loop.
For the first part we got:
while 1<2:
try:
j = j+[v.next()]
except StopIteration:
# ...
while len(__file__)%2:
break
__file__ += chr(random.randint(32,0x7e))
if (_____ == 28 and j[_____] != "]") or \
(_____ == 40 and j[_____] != ")") or \
(_____ == 29 and j[_____] != "E") or \
(_____ == 30 and j[_____] != "3") or \
(_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
(_____ == 43 and j[_____] != "7") or \
(_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
(_____ == 33 and j[_____] != "P") or \
(_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
(_____ == 35 and j[_____] != "#") or \
(_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-")) or \
(_____ == 37 and j[_____] != "?") or \
(_____ == 39 and j[_____] != "&") or \
(_____ == 42 and j[_____] != "=") or \
(_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2)):
__.exit(":(")
_(_____)
First of all this part of the code is useless and can be remove ๐:
while len(__file__)%2:
break
The __file__
variable got an extra random character on each iteration __file__ += chr(random.randint(32,0x7e))
. This char can be predicted thanks to the seed we identified but, again, this random will never be used in the code ๐.
Then we got a big condition which invoke exit if verified. In this condition we verify the value of _____
which is set to False
at the beginning of the code and incremented on each iteration with _(_____)
(because _
is defined at the beginning of the code with _ = lambda x : x+1
).
In addition, the variable _____
is used as an index of j
. Then j[_____]
is compare to different characters.
To resume, the input is encoded with uuencode and part of the encoded input is verified with hardcoded characters. For example _____ == 28 and j[_____] != "]"
means that the 28th char of the uuencoded input is ]
.
Let’s reorder the condition:
if (_____ == 28 and j[_____] != "]") or \
(_____ == 29 and j[_____] != "E") or \
(_____ == 30 and j[_____] != "3") or \
(_____ == 33 and j[_____] != "P") or \
(_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
(_____ == 35 and j[_____] != "#") or \
(_____ == 37 and j[_____] != "?") or \
(_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
(_____ == 39 and j[_____] != "&") or \
(_____ == 40 and j[_____] != ")") or \
(_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
(_____ == 42 and j[_____] != "=") or \
(_____ == 43 and j[_____] != "7") or \
(_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-") or \
(_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2))):
Here j[27:45]
is equal to ]E3F,P9#-?,&)F=7-C-
.
Part 2
Now we got the same process inside the StopIteration exception:
r = ____(__file__[:-(len(j))]).read()
z=var(''.join(j[::-1]))
# :D
_____,j = not 1,[]
while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "3b32b5601c722e59fd5b0ba81c31f230c3666ca1":
try:
x = z.next()
j = j+[x]
except StopIteration:
if ''.join(j).index("5=UMW22!50") == 37:
import antigravity
__.exit("You van validate with the flag :)")
if (_____ == 7 and ord(j[_____]) != _____+25) or \
(_____ == 8 and ord(j[_____]) != _____+24) or \
(_____ == 10 and j[_____] != r[-201]) or \
(_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
(_____ == 12 and j[_____] != r[240].upper()) or \
(_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
(_____ == 14 and j[_____] != ",") or \
(_____ == 15 and j[_____] != chr(ord("L")+5)) or \
(_____ == 16 and j[_____] != str(int(r[1088])-1)) or \
(_____ == 17 and j[_____] != """'""") or \
(_____ == 18 and j[_____] != chr(ord("+")+2)) or \
(_____ == 9 and j[_____] != r[-845]):
sys.exit()
_____ += 1
We got r = ____(__file__[:-(len(j))]).read()
. Here __file__[:-len(j)]
is the equivalent of __file__
before each random char. In other word, random characters added to __file__
were useless and we kept the original file name. Then the file is open with ____
(defined as open
), read and put in r
variable. Now the content of r
is the source code of the program.
Then, z
is an iterator on the reversed input (j
).
We got a condition to parse our input which is
while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "6a708a16690da9519be4f584774b0d80f860d6d6":
This condition made a hash with the beginning and the end of the script and verify the hash to continue. In other words, if you modify a part of the script, then the script is corrupted and won’t works.
We got the same process as part 1: j is an empty list and for each iteration we append a char from z, the reversed input. Lets reorder the conditions:
if (_____ == 7 and ord(j[_____]) != _____+25) or \
(_____ == 8 and ord(j[_____]) != _____+24) or \
(_____ == 9 and j[_____] != r[-845]) or \
(_____ == 10 and j[_____] != r[-201]) or \
(_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
(_____ == 12 and j[_____] != r[240].upper()) or \
(_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
(_____ == 14 and j[_____] != ",") or \
(_____ == 15 and j[_____] != chr(ord("L")+5)) or \
(_____ == 16 and j[_____] != str(int(r[1023])-1)) or \
(_____ == 17 and j[_____] != """'""") or \
(_____ == 18 and j[_____] != chr(ord("+")+2)):
Now we’re gonna give an example of each type of conditions:
Condition - type 1
(_____ == 7 and ord(j[_____]) != _____+25)
Is the equivalent of
ord(j[7]) != 7+25
which means
j[7] = chr(32) = ' '
Condition - type 2
(_____ == 9 and j[_____] != r[-845])
Here r
refers to the source code. The source code must be the same as given in the challenge, we gonna load it on a python shell:
>>>r = open("U_u.py","r").read()
>>>r[-845]
'0'
So we got
j[9] = '0'
Condition - type 3
(_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7])))
On python shell:
>>>r = open("U_u.py","r").read()
>>> chr(ord(r[0])^ord(r[7]))
'A'
Decode full condition
If we decode the full condition we got the following string:
0?AX&,Q0'-
Since z
has been reversed, we can reverse the strings and get the characters -18 to -7 (j[-18:-7]):
-'0Q,&XA?0
Part 3
The last part is the following:
if ''.join(j).index("5=UMW22!50") == 37:
import antigravity
__.exit("You van validate with the flag :)")
To reach the flag, j
(the reverse uuencoded flag) must contain 5=UMW22!50
.
One reversed, we got 05!22WMU=5
for characters j[-46:-37].
uuencode
Let see how uuencode
works:
>>>"a".encode("uu")
'begin 666 <data>\n!80 \n \nend\n'
>>>"รง".encode("uu")
'begin 666 <data>\n"PZ< \n \nend\n'
We got 'begin 666 <data>\nXXXXX\n \nend\n'
where XXXXX is the encoded data. The first and last part of the encoded input is fixed and known.
According to the 3 parts we got, we have the following uuencoded string:
Part 3 : j[-46:-37] = 05!22WMU=5
Part 1 : j[27:45] = ]E3F,P9#-?,&)F=7-C-
Part 2 : j[-18:-7] = -'0Q,&XA?0
With uuencode prefix and suffix (note that j[-18] == j[45]):
begin 666 <data>\n=05!22WMU=5]E3F,P9#-?,&)F=7-C-'0Q,&XA?0 \n \nend\n
Lets decode it with python:
>>> "begin 666 <data>\n=05!22WMU=5]E3F,P9#-?,&)F=7-C-'0Q,&XA?0 \n \nend\n".decode("uu")
'APRK{uu_eNc0d3_0bfusc4t10n!}\x00'
Flag
APRK{uu_eNc0d3_0bfusc4t10n!}