U_u

Aperi'CTF 2019 - Reverse (200 pts).

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!}

Zeecka