Trickery Index

Hackover CTF 2016 - imgenc writeup

Posted at — Oct 9, 2016

This time, we get some ‘encrypted’ grayscale image and a .pyc file that doesn’t want to be decompiled. Decompyle++ gives us the following output:

tr@karabut.com:~/work/hackover16/imgenc$ pycdc imgenc.pyc
# Source Generated with Decompyle++
# File: imgenc.pyc (Python 2.7)

import sys
import numpy as np
from scipy.misc import imread, imsave

def doit(input_file, output_file, f):
Unsupported opcode: STOP_CODE
    img = imread(input_file, flatten = True)
    img /= 255
    size = img.shape[0]
# WARNING: Decompyle incomplete

if __name__ == '__main__':
    if len(sys.argv) != 4:
        sys.exit(1)
    doit(sys.argv[1], sys.argv[2], int(sys.argv[3]))

STOP_CODE definitely isn’t supposed to be in a compiled file! Let’s try disassembling it now:

tr@karabut.com:~/work/hackover16/imgenc$ pycdas imgenc.pyc
imgenc.pyc (Python 2.7)
... 
                67      STOP_CODE               
                68      STOP_CODE               
                69      BINARY_DIVIDE           
                70      JUMP_IF_TRUE_OR_POP     5
                73      LOAD_CONST              3: 0
                76      LOAD_CONST              3: 0
                79      BINARY_DIVIDE           
Segmentation fault (core dumped)

That’s interesting. Hexdumping the imgenc.pyc bytes manually looking for \x00\x00\x15 yield this engaging snippet:

tr@karabut.com:~/work/hackover16/imgenc$ hd imgenc.pyc 
...
00000120  64 04 00 6b 00 00 72 ce  00 64 03 00 64 03 00 15  |d..k..r..d..d...|
00000130  7d 05 00 64 03 00 04 00  00 15 70 05 00 64 03 00  |}..d......p..d..|
00000140  64 03 00 15 7d 05 08 64  03 00 64 83 10 15 7d 05  |d...}..d..d...}.|
00000150  00 64 03 00 64 03 00 15  7d 05 00 04 03 00 64 03  |.d..d...}.....d.|
00000160  00 15 71 05 00 60 03 00  04 03 10 35 7d 05 00 64  |..q..`.....5}..d|
00000170  03 90 64 03 60 15 77 25  02 64 03 00 65 03 05 15  |..d.`.w%.d..e...|
00000180  7d 05 00 64 03 30 64 03  00 15 7d 05 00 64 03 00  |}..d.0d...}..d..|
00000190  64 03 10 15 7d 05 00 64  03 00 64 03 00 15 7d 05  |d...}..d..d...}.|
000001a0  00 64 03 00 64 03 00 15  7d 85 00 64 03 70 64 03  |.d..d...}..d.pd.|
000001b0  00 15 7d 05 00 64 03 00  64 03 00 15 7d 05 00 6e  |..}..d..d...}..n|
...

Notice anything unusual? There’s a sequence of some LOAD_CONST LOAD_CONST BINARY_DIVIDE STORE_FAST opcodes (64 03 00 64 03 00 15 7d 05 00) pretty obviously corrupted by bit flipping, starting with the second one at 0x133. Let’s fix them up!

tr@karabut.com:~/work/hackover16/imgenc$ hd imgenc-fixed.pyc
...
00000120  64 04 00 6b 00 00 72 ce  00 64 03 00 64 03 00 15  |d..k..r..d..d...|
00000130  7d 05 00 64 03 00 64 03  00 15 7d 05 00 64 03 00  |}..d..d...}..d..|
00000140  64 03 00 15 7d 05 00 64  03 00 64 03 00 15 7d 05  |d...}..d..d...}.|
00000150  00 64 03 00 64 03 00 15  7d 05 00 64 03 00 64 03  |.d..d...}..d..d.|
00000160  00 15 7d 05 00 64 03 00  64 03 00 15 7d 05 00 64  |..}..d..d...}..d|
00000170  03 00 64 03 00 15 7d 05  00 64 03 00 64 03 00 15  |..d...}..d..d...|
00000180  7d 05 00 64 03 00 64 03  00 15 7d 05 00 64 03 00  |}..d..d...}..d..|
00000190  64 03 00 15 7d 05 00 64  03 00 64 03 00 15 7d 05  |d...}..d..d...}.|
000001a0  00 64 03 00 64 03 00 15  7d 05 00 64 03 00 64 03  |.d..d...}..d..d.|
000001b0  00 15 7d 05 00 64 03 00  64 03 00 15 7d 05 00 6e  |..}..d..d...}..n|
...

This looks way better! Now to decompiling:

tr@karabut:~/work/hackover16/imgenc$ pycdc imgenc-fixed.pyc 
#Source Generated with Decompyle++
# File: imgenc2.pyc (Python 2.7)

import sys
import numpy as np
from scipy.misc import imread, imsave

def doit(input_file, output_file, f):
    img = imread(input_file, flatten = True)
    img /= 255
    size = img.shape[0]
    if size < -238:
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
        a = 0 / 0
    sin_row = np.tile(np.sin(np.linspace(0, 2 * np.pi, f)), size / f)
    sin_img = np.repeat([
        sin_row], size, 0)
    imsave(output_file, img + sin_img + sin_img.T)

if __name__ == '__main__':
    if len(sys.argv) != 4:
        sys.exit(1)
    doit(sys.argv[1], sys.argv[2], int(sys.argv[3]))

All those divisions turned out to be completely bogus, but we’ve got the source now! So what it does is it takes an 8-bit input image and adds a sine fuction to it, yielding the output values in range (-1, 1), which are then lerped to the usual 0-255 and saved as a grayscale image. Based on what the picture looks like, the f parameter should be equaling 30. So let’s get a visual reference to what gets added running this over a brand new 480x480 black png file generated with ImageMagick:

tr@karabut.com:~/work/hackover16/imgenc$ convert -size 480x480 xc:black black.png
tr@karabut.com:~/work/hackover16/imgenc$ python imgenc-fixed.pyc black.png test.png 30

We get the following picture out:

test.png

And now, let’s try to subtract it, converted to (-1, 1) at a range of scales, and see what fits:

from PIL import Image

img = Image.open("protected-image.png")
sin_img = Image.open("test.png")

sin_pixels = sin_img.load()
pixels = img.load()

for scale in range(1,256):
        pixels = img.load()
        for j in range(480):
                for i in range(480):
                        shift = (sin_pixels[i,j]/255.0-0.5)*2
                        pixels[i,j] = int( pixels[i,j] - shift*scale)
        img.save("out/out"+str(scale).zfill(3)+".png")

After the script fills our out directory with pngs, we can flip through them, finding this among others:

out014.png

Fourier?

Oops.