Hints for Python Challenge

0

2**38

1

rot13

2

page source means the current web page source. Open the html page source and note the comments.

3

Note the page title. It’s a hint! Need to view the page source as above.

4

The page title says "follow the chain", so click the image

Need to get the page by program, and extract a number from the source.

Brillient problem!! Follow the number and you’ll get a page that doesn’t has a number in it, but since the we write a loop to append the number to the url, this time we append NULL.

The answer is lay in the page that has no number in it. It’s a cyclic list and you’ll get the page again and again, the length of the cycle is 194.

nothing, 6711, …​, 66831 and then will get the answer peak

5

peak hell sounds familiar? Download a file whose name is in the source code and unpickle it.

Then! The unpicked list is a character map!

6

The comment of the page source only has "zip", I googled and download channel.zip. Unzip it and there are many num.txt files in it, other than a readme file with two hints.

It seems similar with linkedlist problem, but the last file in the list says "Collect the comments."

Need to know that the comments here is in the zipfile, i.e. each file in the zip file can have a comment, here we need to collect these comments following the list order and will got the answer, say XXX.

The final answer is the small characters which forms XXX!!! Amazing, I googled a lot!

hockey oxygen

7

Absolutely nothing other than an image with strange blocks in it. Note that the color are all gray, can the gray value represent a ascii value?

Need PIL library.

import PIL
img = PIL.Image.open('oxygen.png')
pix = img.load() # pix is a list of (R,G,B,A), which can be accessed by pix[x,y]
print img.size # (width, height)
[pix[x,img.size(1)/2] for x in xrange(30)] # print the RGB of the central line to see the block size in pixel, which is 7 pixel per width block, except the first block whose width is 5 pixsels
res = [chr(pix[4,47][0])] + [chr(pix[5+7*i,47][0]) for i in xrange(86)]
''.join(res) # not the final answer, but a hint, just follow it!

8

Follow the link in the image. username and password can be obtained by decompress the two strings in source page by bz2.

Look at the strings' header, and compare them with .bz2 file header:

$hexdump -c 10 -C test.bz2
import bz2
un = 'BZh91AY&SYA\xaf\x82\r\x00\x00\x01\x01\x80\x02\xc0\x02\x00 \x00!\x9ah3M\x07<]\xc9\x14\xe1BA\x06\xbe\x084'
pw = 'BZh91AY&SY\x94$|\x0e\x00\x00\x00\x81\x00\x03$ \x00!\x9ah3M\x13<]\xc9\x14\xe1BBP\x91\xf08'
bz2.decompress(un)
bz2.decompress(pw)

username:huge password:file

9

Download the image good.jpg, manipulate it accroding to comments in the page source, that is, draw lines using ImageDraw module from library PIL.

cow, bull

10

1, 11, 21, 1211, 111221 1211 → 111221 1'1' 1'2' 2'1' → 111221

res='1'
for i in xrange(31):
    next_str = ""
    lenr = len(res)
    pos = 0
    idx = 0
    while pos < lenr:
        cnt = 0
        while pos < lenr and res[pos] == res[idx]:
            cnt += 1
            pos += 1
        next_str += str(cnt) + res[idx]
        idx = pos
    if i == 30:
        print len(res)
    res = next_str

11

Another image manipulatation problem.

This time more advanced pixel access techniques are required. You’ll find numpy is very helpfull.

from PIL import Image, ImageDraw
import numpy as np
img = Image.open('cave.jpg')
pdata = np.asarray(img)
w,h = img.size
odd = [( 0,0,0 ) * (w/2)] * h
even = [( 0,0,0 ) * (w/2)] * h
for i in xrange(480):
    odd[i] = pdata[i][::2] if i%2==0 else pdata[i][1::2]
    even[i] = pdata[i][::2] if i%2==1 else pdata[i][1::2]
odd = np.array(odd)
even = np.array(even)
Image.fromarray(odd).show()
Image.fromarray(even).show()

12

I’ll never solve it all by my self! It’s just so evil!

You need to notice the image file name, why it’s evil1.jpg rather than just evil.jpg? So you’ll try if evil2.jpg exists. Turns out evil2.jpg says "rot jpg - _.gfx". Then try to download evil2.gfx which can’t be decoded directly.

$hexdump -n 100 -C evil2.gfx
00000000  ff 89 47 89 ff d8 50 49  50 d8 ff 4e 46 4e ff e0  |..G...PIP..NFN..|
00000010  47 38 47 e0 00 0d 37 0d  00 10 0a 61 0a 10 4a 1a  |G8G...7....a..J.|
00000020  40 1a 4a 46 0a 01 0a 46  49 00 f0 00 49 46 00 00  |@.JF...FI...IF..|
00000030  00 46 00 00 e7 00 00 01  0d 00 0d 01 01 49 00 49  |.F...........I.I|
00000040  01 01 48 00 48 01 00 44  01 44 00 b4 52 00 52 b4  |..H.H..D.D..R.R.|
00000050  00 00 00 00 00 b4 00 01  00 b4 00 01 04 01 00 00  |................|
00000060  90 02 40 00                                       |..@.|
00000064
$hexdump -n 100 -C evil2.gfx | cut -d'|' -f2 | xargs | sed -e 's/ //g' -e 's/...../&\n/g'
..G..
.PIP.
.NFN.
.G8G.
..7..
..a..
J.@.J
F...F
I...I
F...F
...

So there are five images in evil2.gfx.

from PIL import Image, ImageDraw
import numpy as np
data = open('evil2.gfx','rb')
img = [None] * 5
img[0] = open('img0.jfif','wb')
img[1] = open('img1.png','wb')
img[2] = open('img2.gif','wb')
img[3] = open('img3.png','wb')
img[4] = open('img4.jfif','wb')
for i in xrange(13515):
    img[0].write(data.read(1))
    img[1].write(data.read(1))
    img[2].write(data.read(1))
    img[3].write(data.read(1))
    img[4].write(data.read(1))
for i in xrange(5):
    img[i].close()

Each image has several characters in it, concat them will get the answer.

Note that the third image is damaged, nevertheless we can still get the right answer.

disproportional

13

No idea. Cheat with This.

xmlrpclib is needed.

14

Spiral matrix. Easy and easy to lose time in these problem!!

from PIL import Image
import numpy as np
img = Image.open('wire.png')
pdata = np.asarray(img)
w, h = img.size
#deimg = [ [0 for i in range(100)] for j in range(100)]
for i in range(100):
    deimg[i] = pdata[0][100*i: 100*i+100]
# Image.fromarray(np.array(deimg)).show() # bit.html says "you took the wrong curve."
direct = [(0, 1), (1, 0), (0, -1), (-1, 0)]
deimg_spiral = [ [0 for i in range(100)] for j in range(100)]
pos = 0
x, y = 0, 0
for i in range(100, -1, -2):
    for d in range(4):
        for j in range(i-1):
            deimg_spiral[x][y] = pdata[0][pos]
            pos += 1
            x += direct[d][0]
            y += direct[d][1]
    x += 1
    y += 1
Image.fromarray(np.array(deimg_spiral)).show()

cat, and its name is uzi. you’ll hear from him later.

15

It’s hard without search.

First point, you have to notice the next month has 29 days, so it’s a leap year.

Python datetime modular is for rescue. And find the year 1756, then google a lot and find what happend in 1756-01-27

import datetime
for year in range(1996,1582,-20):
    if datetime.date(year, 1, 1).weekday() == 3:   # 3=Thursday
        print year,

16

Cheated.:( Use GMIP to magnify the image and see the sticks in pixsel: - Press 'O' to open color pick tool. - Shift + Left Mouse to see the color value. - Index means index in palette

from PIL import Image, ImageDraw
img = Image.open('mozart.gif')
w,h = img.size
bars = []
for j in range(h):
    for i in range(w - 5):
        if img.getpixel((i,j)) == 195 and img.getpixel((i+4,j)) == 195:
            bars.append((i,j))
collect = Image.new(img.mode, (w*2, h), 0)
collect.palette = img.palette
for j in range(h):
    for i in range(w):
        collect.putpixel((i + w - bars[j][0], j), img.getpixel((i,j)))
collect.show()

17

An image of cookies, and an image from Level 4 in the lower left corner.

Go to check the cookies of the page of Level 4, this can be done by cookielib or devbug console of firefox or chromium.

The cookie’s value is "you+should+have+followed+busynothing…​"

In Level 4 we follow the link http://www.pythonchallenge.com/pc/def/linkedlist.php?nothing=12345 and finally get the address to the next level, so accroding to the cookies, now we should follow http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing=12345. What’s more, the cookie of each page is to be checked.

import urllib2
import cookielib
url = 'http://www.pythonchallenge.com/pc/def/linkedlist.php?busynothing='
num = '12345'
cookie = cookielib.CookieJar()
handler = urllib2.HTTPCookieProcessor(cookie)
opener = urllib2.build_opener(handler)
res = ""
while True:
    f = opener.open(url + num)
    msg = f.read()
    num = msg.split()[-1]
    for ck in cookie:
        res += ck.value
    if not num.isdigit():
        print(msg)
        break
print(res)

Collect all the cookies, yield a string begins with "BZh91AY%26SY", check the magic header of a typical .bz2 file:

$ head -c 10 test.bz2 # yield "BZh91AY&SY"

So we need to decompress this string with bzip2, but first it need to be unquoted to change %26 to & (HTML URL Decoding). This can be done by urllib.unquote.

Soon we find that urllib.unquote didn’t give the right bzip2 encoded string, we need urllib.unquote_plus to change the '+' to space.

The decoded string is below:

is it the 26th already? call his father and inform him that "the flowers are on their way". he'll understand.

Recall that the phone in Level 13, and Mozart’s birthday is Jan 26th according to Level 15, we googled Mozart’s father, whose name is "Leopold Mozart".

import xmlrpclib
server = xmlrpclib.Server('http://www.pythonchallenge.com/pc/phonebook.php')
server.phone('Leopold')

Not finished yet! The address is only mozart’s father’s "phone number", do remember to inform him "the flowers are on their way", but how? Maybe set cookies in HTTP header?

url = 'http://www.pythonchallenge.com/pc/stuff/violin.php'
msg = 'the flowers are on their way'
req = urllib2.Request(url, headers = {"Cookies":"info=" + urllib.quote_plus(msg)})
print urllib2.urlopen(req).read()

18

The difference is brightness, so try brightness.html

Turns out to be the same page, but in the comments, it says maybe consider deltas.gz ,so download deltas.gz

I Use gunzip to decompress it, and get a text file begins with:

89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00   89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00
02 8a 00 00 00 c8 08 02 00 00 00 e0 19 57 95 00 00 00   02 8a 00 00 00 c8 08 02 00 00 00 e0 19 57 95 00 00 00
09 70 48 59 73 00 00 0b 13 00 00 0b 13 01 00 9a 9c 18   09 70 48 59 73 00 00 0b 13 00 00 0b 13 01 00 9a 9c 18
00 00 00 07 74 49 4d 45 07 d5 05 07 0c 18 32 98 c6 a0   00 00 00 07 74 49 4d 45 07 d5 05 07 0c 18 32 98 c6 a0
48 00 00 00 1d 74 45 58 74 43 6f 6d 6d 65 6e 74 00 43   48 00 00 00 1d 74 45 58 74 43 6f 6d 6d 65 6e 74 00 43
72 65 61 74 65 64 20 77 69 74 68 20 54 68 65 20 47 49   72 65 61 74 65 64 20 77 69 74 68 20 54 68 65 20 47 49
4d 50 ef 64 25 6e 00 00 20 00 49 44 41 54 78 da ec bd   4d 50 ef 64 25 6e 00 00 20 00 49 44 41 54 78 da ec bd
57 93 9c 47 92 25 7a 3c c4 a7 53 55 96 42 01 20 9b 6c   57 93 9c 47 92 25 7a 3c c4 a7 53 55 96 42 01 20 9b 6c
31 b3 63 bb 4f fb ff 1f ee d3 bd 2f d7 ae d9 8e d8 e9   31 b3 63 bb 4f fb ff 1f ee d3 bd 2f d7 ae d9 8e d8 e9
ee 69 92 0d 5d ba b2 52 7d 22 22 fc 3e 78 e6 c7 6a 28   ee 69 92 0d 5d ba b2 52 7d 22 22 fc 3e 78 e6 c7 6a 28

"50 4e 47" means "PNG" , so these are two png files in hex format.

Well, I make a mistake, these two columns of hexadecimal are not themselves png files (although they can be recognized by file as 620x200 png file), but need to be diff use difflib.ndiff.

Here is the code I cheated:

import gzip
import difflib
ff = gzip.open('deltas.gz', 'r')
deltas = ff.read()
ff.close()
deltas = deltas.splitlines()
left, right = [], []
for row in deltas:
    left.append(row[:53])
    right.append(row[56:])
diff = list(difflib.ndiff(left, right))
png = ['', '', '']
for row in diff:
    bytes = [chr(int(byte, 16)) for byte in row[2:].split()]
    if row[0] == '-':
        png[0] += ''.join(bytes)
    elif row[0] == '+':
        png[1] += ''.join(bytes)
    elif row[0] == ' ':
        png[2] += ''.join(bytes)
for i in range(3):
    open('out18_%d.png' % i, 'wb').write(png[i])

19

username and password is butter, fly

Again the first clue is in the page source. It’s an email with an attachment, which is encoded by base64. Decode it by base64 -d and the wav file says "sorry".

Then sorry.html says "what are you apologizing for?"

Note in the email it says "Maybe my computer is out of order." and recall the "hexbin" thing, does that means I need to change the wav file in the other endian order? And the file indian.wav does show that the file is little-endian!

According to this page, the header of wav file is 44, the rest is PCM sample value in 16 bit.

import struct
f = open('indian.wav', 'rb')
wav = f.read()
f.close()
s = len(wav[44:]) // 2
res = wav[:44]
for i in range(s):
    t1 = struct.unpack('<h', wav[44 + 2 * i: 44 + 2 * i + 2])
    print(t1)
    res = res + struct.pack('>h', t1[0])
f = open('indianx.wav', 'wb')
f.write(res)
f.close()

In the wiki I also find a solution by dd:

(head -c 44 indian.wav; tail -c +45 indian.wav | dd conv=swab) > tmp.wav

Now the new wav file sounds "You’ve already get it, Ah…​" "You’re a idiot, Ah'"

Oh, the indian map is a clue for "endian"!

20

This problem is amazing.

I cheated for hints, but I did googled a lot by myself and learned many things about HTTP headers and Python libraries, such as requests

I also use Firebug to inspect the HTTP header.

The point is, when the image is transmitted by http, the header contains such an item: Content-Range : bytes 0-30202/2123456789 , google for "Content-Range" and find a blog about it, and to get the rest bytes, the http client need to add such an item to the header: Range: bytes=30203- , this can be done by the library requests.

import requests
import zipfile
r = requests.get('http://www.pythonchallenge.com/pc/hex/unreal.jpg',
                 auth=requests.auth.HTTPBasicAuth('butter', 'fly'))
Begin = r.headers['content-range'].split()[1].split('-')[1].split('/')[0]
End = r.headers['content-range'].split()[1].split('-')[1].split('/')[1]
print(Begin)
# headers = {'Range':'bytes=30203-'}
p = str(int(Begin) + 1)
while r.status_code != 416:
    headers = {'Range': 'bytes=' + p + '-'}
    r = requests.get('http://www.pythonchallenge.com/pc/hex/unreal.jpg',
                     auth=requests.auth.HTTPBasicAuth('butter', 'fly'), headers=headers)
    if r.status_code == 206:
        p = r.headers['content-range'].split()[1].split('-')[1].split('/')[0]
#        print(p, r.text, r.status_code, r.headers['content-type'])
        print(p, r.text)
        p = str(int(p) + 1)
# 30202
# 30236 b"Why don't you respect my privacy?\n" 206 application/octet-stream
# 30283 b'we can go on in this way for really long time.\n' 206 application/octet-stream
# 30294 b'stop this!\n' 206 application/octet-stream
# 30312 b'invader! invader!\n' 206 application/octet-stream
# 30346 b'ok, invader. you are inside now. \n' 206 application/octet-stream
# 416 text/html

When it reached 30347, the hints ended.

I couldn’t figure out how to get the last text/html content, so I cheated for a hint that I need to request from the end.

r = requests.get('http://www.pythonchallenge.com/pc/hex/unreal.jpg',
                 auth=requests.auth.HTTPBasicAuth('butter', 'fly'), headers={'Range': 'bytes=' + End + '-'})
print(r.text)
print(''.join(reversed(r.text)))
# "the password is your new nickname in reverse"

My "new nickname" is "invader" from the hints above, and invader.html confirms that.

p = r.headers['content-range'].split()[1].split('-')[0]
p = str(int(p) - 1)
r = requests.get('http://www.pythonchallenge.com/pc/hex/unreal.jpg',
                 auth=requests.auth.HTTPBasicAuth('butter', 'fly'), headers={'Range': 'bytes=' + p + '-'})
print(r.text)
# "and it is hiding at 1152983631."

So just run:

r = requests.get('http://www.pythonchallenge.com/pc/hex/unreal.jpg',
                 auth=requests.auth.HTTPBasicAuth('butter', 'fly'), headers={'Range': 'bytes=1152983631-'})
f = open('20.data', 'wb')
f.write(r.content)
f.close()

Turns out the content is an encrypted zip file, whose password is 'invader' reversed.

Here I stuck for half an hour because the zip password parameter, it must be binary string.Though I can just extract it by aunpack.

password = 'invader'
password = ''.join(reversed(password))
zf = zipfile.ZipFile('20.data')
zf.extractall(pwd=password.encode('utf-8'))

The final files are:

21

readme.txt
Yes! This is really level 21 in here.
And yes, After you solve it, you'll be in level 22!

Now for the level:

* We used to play this game when we were kids
* When I had no idea what to do, I looked backwards.

and package.pack , which is a zlib compressed data file begin with 78 9c.

zlib.decompress can decompress it, but the result is still zlib compressed data. Keep decompressing it until it begins with BZh, which is a bz2 file!

Now keep bz2.decompress it until it becomes zlib file again, and then bz2…​

Finally I got a file begins with 80 8d, which is 8086 relocatable (Microsoft) accroding to file.

I googled it, and guessed that it may be an old executable file on DOS. I failed to install MS DOS 6.2.2 on qemu, I didn’t know how to insert the second floppy, change fda DOS622_2.IMG didn’t work.

Then I install FreeDOS on qemu, and copy the '8086 relocatable' file to FreeDOS disk. But sadly found that it crashed before giving any usefull information.

Then I cheated. The hint is in readme.txt : When I had no idea what to do, I looked backwards. Oh it means reverse the decoded content and see what it is. The fake '8086 relocatable' file turns out to be an zlib file again!

Continue to decompress it and got the message: "look at your logs."

There is no logs.

But if the log means which method is used to decompress, there is.

If not cheated, I would spend a lot of time to figure out how to arange the log to get usefull information. The answer is, when zlib.decompress, append a 'z'; when bz2.decompress, append a space; when zlib.decompress backwards, append a new line. Then print the final string will show the address to the next level.

import zlib
import bz2
f1 = open('package.pack', 'rb')
f2 = open('20.dec', 'wb')
a = f1.read()
cnt = 0
log = []
while True:
    cnt += 1
    if a.startswith(b'x\x9c'):
        a = zlib.decompress(a)
        print("zlib")
        log.append(' ')
    elif a.startswith(b'BZh'):
        a = bz2.decompress(a)
        print("bz2")
        log.append('b')
    elif a.endswith(b'\x9cx'):
        a = zlib.decompress(a[::-1])
        log.append('\n')
    else:
        f2.write(a)
        print(a[::-1])
        break
f1.close()
f2.close()
print(cnt)
print(''.join(log))

22

It seems that Firebug hide the comments in the source, so I didn’t see it in the first time: 'or maybe white.gif would be more bright'

Download 'white.gif' and it shows nothing, easily to deduce that some pixel must be exaggerated to be seen.

I use 'PIL.Image' to handle the gif image:

from PIL import Image
im = Image.open('white.gif')
w, h = im.size
cnt = 0
x, y = 0, 0
res = Image.new("RGB", (200, 200))
moves = []
while True:
    rgb_im = im.convert("RGB")
    a = rgb_im.load()
    for i in range(h):
        for j in range(w):
            if a[i, j] != (0, 0, 0):
                moves.append((i - 100, j - 100))
    cnt += 1
    try:
        im.seek(im.tell() + 1)
    except EOFError:
        break

Some notes when handle the gif using PIL: * gif may contains multiple frames, all of them share the same palette. * 'im.tile' shows the essensial info about the image. * If not 'im.load()', the pixel can be access by 'im.getpixelx, y' * If 'a = im.load()', then 'im.getpixelx, y' is same as 'a[x, y]' * Frame number can not be seen until all the frames have been 'seek' . * Gif can’t be saved the same way as 'jpg' can.

Save as gif is not easy, so I save as multiple jpg fils. but the white pixel is too small.

Then I run out of ideas.

Again, I cheated. So, the picture is a 'joystick', whose English name I now known for the first time. So 'joystick.html' says 'are you in level 2, or level 22?', I thought some clue is from Level 2, but turns out all I need to do is to visit '22.html', which says 'it was a rhetorical question!'. Well, now I have to figure out the meaning of 'rhetorical' first.

In every frame, there are only one pixel that is not black. Their cordinates values are in '{98, 100, 102}'.

t = 0
letter = 0
while t < len(moves):
    if moves[t] == (0, 0):
        letter += 1
        x, y = letter * 30, letter * 30
    dx, dy = moves[t]
    x, y = x + dx, y + dy
    res.putpixel((x, y), (0, 255, 0))
    t += 1
res.save('res.jpg')
res.show()

23

  • Clue 0: The page title: 'what is this module?'

  • Clue 1: 'TODO: do you owe someone an apology? now it is a good time to tell him that you are sorry. Please show good manners although it has nothing to do with this level.'

  • Clue 2: 'it can’t find it. this is an undocumented module.'

  • Clue 3: 'va gur snpr bs jung?' rot13 ⇒ 'in the face of what?'

Well, although I know the 'import this' thing, I didn’t find the answer by myself. I thought it was the cow is in the fase of 'road', 'grass' or something.

And I also didn’t expect that’s all!

24

It’s a maze, I think walk through it and mark the path out will show something usefull.