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:
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:
Fourier?
Oops.