Python Challenge 30의 Url은 다음과 같다

Python challenge 30 : http://www.pythonchallenge.com/pc/ring/yankeedoodle.html


구성

문제 페이지에 들어가면 휴식이 있다. 아래 영어는 다음의 의미를 가지고 있다.

 

"이 그림은 오직 당신의 휴식을 위함"

 

(X치고 다음 문제로 보내주면 좋겠다. ) 주석을 한번 보도록 하자

주석은 csv(comma separated values)을 확인하라고 되어있다. yankeedoodle.csv를 다운로드 하자.

확인 결과는 다음과 같다. 

으흠; 어떤 자료인걸까.... 0~1 사이 소수 값들이다.


해결 아이디어

이미지 모드에 'F'모드라는 것이 있다.

https://ko.wikipedia.org/wiki/TIFF

 

TIFF - 위키백과, 우리 모두의 백과사전

TIFF 위키백과, 우리 모두의 백과사전.

ko.wikipedia.org

F 모드는 (32-bit floating point pixels) 그니까 32bit 정확도의 소수 값을 가지는 이미지 모드이다. 딱0~1 사이 값들을 보유하고, 우리 상황이랑 정확하게 틀어맞는다. 일단 Image의 가로세로를 확인하기 위해 w, h를 구하는 내포를 하나 만들고 이미지를 만들어 보자

### 30_1.py

import re
from PIL import Image

if __name__=="__main__" :
    with open("yankeedoodle.csv","r") as f:
        data = f.read()
        data = re.findall("\d[.]\d*",data)

    w,h = (x for x in range(2,len(data)//2) if len(data)%x==0)

    img = Image.new("F",(w,h))
    img.putdata([float(x) for x in data], 256)
    img.show()

 

* img.save("뭐. tiff")가 잘 안된다.

* putdata 2번째 파라미터는 비중치이다. 높으면 높을수록 선명해진다. 어차피 흑백이긴 하지만

 

다음과 같은 사진을 구할 수 있다.

응? 뭔가 Python 식 같은 표현이 나왔는데, 사진이 돌아가 있다. 조금 보기 편하게 돌려보자

### 30_2.py

import re
from PIL import Image

if __name__=="__main__" :
    with open("yankeedoodle.csv","r") as f:
        data = f.read()
        data = re.findall("\d[.]\d*",data)

    w,h = (x for x in range(2,len(data)//2) if len(data)%x==0)

    img = Image.new("F",(w,h))
    img.putdata([float(x) for x in data], 256)
    
    img = img.transpose(Image.FLIP_LEFT_RIGHT)
    img = img.transpose(Image.ROTATE_90)
    img.show()

다음과 같은 사진을 구할 수 있다.

일단 그렇다. 식은

n = str(x [i])[5] + str(x [i+1])[5] + str(x [i+2])[6]

 

이고, 식을 조금 해석해 보자면, 첫 번째, 두 번째, 세 번째의 offset5, offset5, offset6을 더한 값을 2ke2ke 해보라는 의미이다.

코드를 짜 보자

### 30_3.py

import re
from PIL import Image

if __name__=="__main__" :
    with open("yankeedoodle.csv","r") as f:
        data = f.read()
        data = re.findall("\d[.]\d*",data)

    first,second,third = data[0::3],data[1::3],data[2::3]

    ans = list()
  
    for d in zip(first,second,third) :
        ans.append(chr(int(d[0][5] + d[1][5] + d[2][6])))

    print(''.join(ans))

코드의 출력 결과는 다음과 같다.

So, you found the hidden message.
There is lots of room here for a long message, but we only need very little space to say 
"look at grandpa", so the rest is just garbage. 
VTZ.l'tf*Om@I"p]#R`cWEBZ40ofSC>OZFkRP0\)+b?Ir)S%Jt3f{ei%n2<FErFx~IzVm JTh =xdx++'de8C5'
|>2\/We;ib(b%d$N<2u(o$*d@.*6Fd'nW5#J!}a]T"1Q-7Y~bOF]T+^9d]e^J^=&I&<x|EEgdQ$$pX'f!_n>F0(
[j%Y'XjwWu,4w/q;1Hd#1H{{Nf~BQ6f![m#fb^a;{Ei%$2fEyN[*4KhK[-7({jh5k0n kwZyx|x=xvFC
....

 

"grandpa" 찾았다.

Answer Url : http://www.pythonchallenge.com/pc/ring/grandpa.html

Python Challenge 29의 Url은 다음과 같다

Python challenge 29 : http://www.pythonchallenge.com/pc/ring/guido.html


구성

문제 페이지에 들어가면 귀여운 안경 쓴 물병이 우리를 반긴다. 반갑다고 인사한 후 주석을 보도록 하자.

주석의 특별한 점이라면 빈줄이 어마어마하게 많다는 점이다. 

 


해결 아이디어

빈줄이 많고 공백으로 이루어진 암호화라...  CTF를 많이 접한 사람이라면 당연히 snow코드를 생각했겠지만, 아쉽게도 이번 문제는 오히려 더 간단하다. 공백으로 이루어진 줄들이 [공백]*+\n으로 이루어져 있기 때문에 공백들의 길이를 chr화 시키면 bz2 문자열이 나온다. decompress 하면 해결이다. 일단 저장하고

코드는 다음과 같다.

### 28.py

import bz2

if __name__=="__main__" :
    with open("guido.html","r") as f:
        html = f.read().split("\n")

        cand = list()

        for l in html :
            if(l.strip() == "") :
                cand.append(len(l))

        print(bz2.decompress(bytes(cand)))

깔끔. 어째 갈수록 문제들이 쉬워진다는 느낌이 든다.

출력은 다음과 같다.

b"Isn't it clear? I am yankeedoodle!"

Answer Url : http://www.pythonchallenge.com/pc/ring/yankeedoodle.html

Python Challenge 28의 Url은 다음과 같다

Python challenge 28 : http://www.pythonchallenge.com/pc/ring/bell.html


구성

들어가면 폭포사진을 볼 수 있다. 아래 번역은 다음과 같다.

"RING-RING-RING 크게 말해보세요"

G발음에 조금 강세를 두고 반복하면 "GREEN"이다. green.html로 이동하면 다음과 같은 페이지를 볼 수 있다.

어허; 다른 태그도 안보인다. 주석도 유의미하지 않고, 무슨 html이 헤더바디 다무시하고 저거 한줄 써있다.


해결 아이디어

일단 사진이 RGB픽셀이니, G만 다 빼보자

### 28_1.py from PIL import Image if __name__ == "__main__" : with Image.open("bell.png") as img : green = list(img.split()[1].getdata())

green을 조금만 볼까?

>>> green[:100] [55, 97, 73, 115, 120, 78, 60, 102, 76, 118, 114, 72, 59, 101, 119, 77, 92, 50, 30, 72, 53, 95, 133, 91, 93, 51, 110, 68, 112, 70, 66, 108, 140, 182, 163, 121, 66, 24, 59, 17, 25, 67, 62, 20, 26, 68, 67, 25, 85, 43, 60, 18, 53, 11, 3, 45, 3, 45, 0, 42, 5, 47, 9, 51, 22, 64, 37, 79, 35, 77, 66, 24, 25, 67, 36, 78, 84, 42, 70, 28, 69, 111, 72, 30, 62, 20, 27, 69, 34, 76, 27, 69, 181, 223, 194, 152, 108, 66, 73, 31]

겉보기에는 그냥 배열처럼 보일 수도 있지만, 홀짝을 짝지어서 보면, 42차이나 나거낭 -42의 차이를 볼 수 있다. 절댓값이 42가 아닌 배열을 빼보면 유의미한 결과를 알 수 있을 것이다.

### 28_2.py from PIL import Image if __name__ == "__main__" : with Image.open("bell.png") as img : green = list(img.split()[1].getdata()) first = green[0::2] second = green[1::2] ans_list = list() for f,s in zip(first,second) : if(abs(f-s) != 42) : val = abs(f-s) ans_list.append(chr(val)) print(''.join(ans_list))

출력결과는 다음과 같다.

whodunnit().split()[0] ?

whodunnit은 영미권의 수수께끼종류이다.(모듈이 있기는 한데, 우리 문제와 관련없다.) 범죄에서 가해자를 추적하는 추리 수수께끼인데, 여기서 가해자는 누가되는 걸까? 누가 우리를 이렇게 힘들게 하는 걸까??

안녕하세요 guido씨

Answer Url : http://www.pythonchallenge.com/pc/ring/guido.html

Python Challenge 27의 Url은 다음과 같다

Python challenge 27 : http://www.pythonchallenge.com/pc/hex/speedboat.html


구성

speedboat.html이라는 페이지 위치에 맞게 보트로 노를 젓고 있는 사진을 한장 확인할 수 있다. 그림의 img태그에는 src로 링크가 걸려있다. 이동하면 자격증명을 물어보는 화면이 나오나, 우리는 아직 모르니 넘어가자

다른 힌트는 보이지 않으니 주석을 확인해 보자

유의미한 주석은 2개가 보인다. 

<!--gif를 말씀하신건가요?>

<!-- 오, 14번문제의 반복은 아닙니다!(도얏)>

이렇게인데, 14번문제는 italy.html로 나선형으로 픽셀을 재배치 하는 문제였다. 아래참고

2021.05.21 - [Python/Python Challenge] - [Python Challenge 14] 빙글빙글

 

[Python Challenge 14] 빙글빙글

Python Challenge 14의 Url은 다음과 같다 Python challenge 14 : http://www.pythonchallenge.com/pc/return/italy.html 구성 페이지에는 사진이 2개 있다. 위에 보이는 나선빵은 italy라는 이름을 가지고 있고,..

tutoreducto.tistory.com

일단 zigzag.gif로 들어가면 14번문제와 유사한 사진을 구할 수 있다. 

<14번 문제>                  <27번문제>

* 실제 14번 문제의 사진은 일렬로 정렬되어있었다.

 

27번문제도 픽셀의 재배치 인걸로 보이나, 방법을 조금 다르게 해야할 거 같다.


해결 아이디어

zigzag가 문제를 풀 수 있는 크나큰 힌트이다. 문제 많이 어렵고, 직관적인 판단을 요구한다. 이미지 픽셀을 재배치하는 것은 분명 어렵지 않은 일이 여야하는데, 지그재그로 재배치해서 사각형을 어떻게 만들 수 있을까? 우리 기본적인 걸로 돌아가보자

"이미지는 무엇을 이루어져 있는가?"

맞다. 픽셀이다.(벡터형은 나가있어) 그러면

"픽셀은 무엇으로 이루어져 있는가?"

맞다. 모드에 따라 다르지만, RGB의 3bytes의 조합으로 이루어져 있다. 우리의 zigzag.gif를 확대해서 보면, 흰색(0,0,0)~검은색(255,255,255)으로 이루어져 있다는 것을 알 수 있다. 이걸 어떻게 지그재그화 할까?

 

팔래트

팔레트(palette)는 컴퓨터 그래픽스에서 디지털 이미지 관리를 위해 존재하는 색
의 유한 집합이다. 컴퓨팅 이외에 쓰는 색상표는 컬러차트라고 한다.

- 출처 위키백과 : https://ko.wikipedia.org/wiki/%ED%8C%94%EB%A0%88%ED%8A%B8_(%EC%BB%B4%ED%93%A8%ED%8C%85) -

이번에 문제에 사용할 중요한 Keyword이다. 이미지중 "P"모드로 작성된 이미지는 모두 이 색상 유한집합(팔레트)를 가진다. 팔래트는 이미지에 사용된 색들을 포함한다. 당연하게도

>>> img = Image.open("zigzag.gif")
>>> img.mode
'P'

우리의 이미지는 'P'모드로 작성되어 있다. 팔래트를 따보자

>>> pal = img.getpalette()
>>> pal
[37, 37, 37, 229, 229, 229, 162, 162, 162, 136, 136, 136, 59, 
59, 59, 212, 212, 212, 9, 9, 9, 41, 41, 41, 24, 24, 24, 156,
156, 156, 148, 148, 148, 112, 112, 112, 254, 254, 254, 91, 91,
91, 106, 106, 106, 49, 49, 49, 248, 248, 248, 213, 213, 213,
220, 220, 220, 15, 15, 15, 85, 85, 85, 159, 159, 159, 62, 62,
62, 78, 78, 78, 76, 76, 76, 111, 111, 111, 103, 103, 103, 150,
150, 150, 154, 154, 154, 68, 68, 68, 25, 25, 25, 169, 169, 169,
126, 126, 126, 185, 185, 185, 140, 140, 140, 234, 234, 234, 244,

...

역시 보이던 대로 흰색(0,0,0) ~ 검정색(255,255,255) 사이의 색들이라 같은 바이트가 3번 반복된다. 팔래트의 최대 길이는 256으로 지정되어 있으니, 3개씩 끊어 읽으면 256의 길이가 나올 것이다.

>>> pal = img.getpalette()[::3]
>>> len(pal)
256

정확했다. 신기한건 이렇게 구한 pal에 중복되는 수가 없다는 것이다. 256이라는 숫자는 또한 8비트로 나타낼 수 있는 최대 수의 개수이다.(0~255) 따라서 0~255에 매칭되게 pal에 변환 테이블을 작성해 주고 그 바이트를 다시 이미지로 바꾸면 유의미한 결과를 기대할 수 있을 것이다.

### 27_1.py

from PIL import Image

if __name__=="__main__" :

    img = Image.open("zigzag.gif")
    
    pal = img.getpalette()[::3]
    trans = bytes.maketrans(bytes(list(range(256))),bytes(pal))
    raw = img.tobytes()
    trans = raw.translate(trans)

    img2 = Image.new("RGB",img.size)
    col = list()
    for i in trans :
        col.append((i,i,i))
    img2.putdata(col)
    img2.save("false.gif")

그러나 아쉽게도 얻은 false.gif은 아름다운 그림은 아니다.

<false.gif>

생각해보자. 이번 문제의 TItle은 zigzag이다. 두개를 교차해야되는 거라면??? 어쩌면 두 그림을 교차할때 같은 픽셀값을 가지면 검정색으로 색칠하면 유의미한 그림이 나오지 않을까?

from PIL import Image


### 27_2.py

if __name__=="__main__" :

    img = Image.open("zigzag.gif")
    
    pal = img.getpalette()[::3]
    trans = bytes.maketrans(bytes(list(range(256))),bytes(pal))
    raw = img.tobytes()
    trans = raw.translate(trans)
    
    col = list(zip(raw,trans))
    
    conv = [i for i,val in enumerate(col) if val[0]!=val[1]]
    
    img3 = Image.new("RGB",img.size)
    colors = [(255,255,255)]*len(raw)
    
    for i in conv :
        colors[i] = (0,0,0)
        
    img3.putdata(colors)
    img3.save("answer_false.gif")

그러나.... 결과로 나온 answer_false도 유의미하진 못하다. 잘 생각해보자

<answer_flase.gif>

 

끝까지 힌트는 zigzag이다. speedboat사진에서 본 zigzag모양을 기억하는가

그렇다. 둘은 같은 선상에 있지 않다. 먼저나온 raw는 인덱스 1부터 가야되고, 나중에 나온 trans는 마지막 인덱스를 포함하면 안된다.

from PIL import Image


### 27_3.py

if __name__=="__main__" :

    img = Image.open("zigzag.gif")
    
    pal = img.getpalette()[::3]
    trans = bytes.maketrans(bytes(list(range(256))),bytes(pal))
    raw = img.tobytes()
    trans = raw.translate(trans)
    
    col = list(zip(raw[1:],trans[:-1]))
    
    conv = [i for i,val in enumerate(col) if val[0]!=val[1]]
    
    img3 = Image.new("RGB",img.size)
    colors = [(255,255,255)]*len(raw)
    
    for i in conv :
        colors[i] = (0,0,0)
        
    img3.putdata(colors)
    img3.save("real_answer.gif")

길었다 참

<real_answer.gif>

아쉽게도 안끝났다. 사진을 보면 keyword가 아니라고 한다. 아래에는 busy?라는 자주보던 문자열이 보인다.

우리가 사용한 이 bytes열. 다시 decompress하면 어떨까?

### 27_4.py

from PIL import Image
import bz2


if __name__=="__main__" :

    img = Image.open("zigzag.gif")
    
    pal = img.getpalette()[::3]
    trans = bytes.maketrans(bytes(list(range(256))),bytes(pal))
    raw = img.tobytes()
    trans = raw.translate(trans)
    
    col = list(zip(raw[1:],trans[:-1]))
    
    conv = [val[0] for i,val in enumerate(col) if val[0]!=val[1]]
    cand = bz2.decompress(bytes(conv))
>>> print(cand[:140])
b'../ring/bell.html del assert repeat raise or class is exec return 
except print return switch from exec repeat else not while assert or class'
>>> len(cand)
70644

결과로 나온 cand는 엄청 많은 조합의 keyword로 되어있다. 사진에는 keyword가 아니라고 하니까 모듈 keyword의 힘을 빌려 필터링 해보자

>>> nk = set([s for s in cand.split() if not keyword.iskeyword(s.decode())])
>>> print(nk)
{b'print', b'switch', b'exec', b'../ring/bell.html', b'repeat'}

중복이 엄청많아 set자료형으로 사용했다. 문제가 만들어 질때는 모르겠는데, exec와 print는 현재 키워드가 아니다.

남은건 switch와 repeat이다. 찾았다.

Answer Url :  http://www.pythonchallenge.com/pc/ring/bell.html

Username : repeat

Password : switch

Python Challenge 26의 Url은 다음과 같다

Python challenge 26 : http://www.pythonchallenge.com/pc/hex/bin.html


구성

엄허; 부끄러운 짤이 우리를 반겨준다. 영어를 해석하면

"서두르세요. 배를 놓칠 거 같습니다."

이다. 특별한 증거가 보이지 않으니, 저기 뭐냐 주석으로 넘어가자

주석에 힌트가 쓰여있는대로면 우리는 그의 이메일을 받았을 것이라고 한다. 우선 사과를 해야 될 대상을 떠올려보자. 필자가 잘살았다고 떵떵거리진 않아도 하늘 아래 부끄러움 없이 살았는데, 뭐지? 과거를 돌아볼 필요가 있을 거 같다. 


해결 아이디어

2021.06.03 - [Python/Python Challenge] - [Python Challenge 19] You are An Idiot!

 

[Python Challenge 19] You are An Idiot!

Python Challenge 19의 Url은 다음과 같다 Python challenge 19 : http://www.pythonchallenge.com/pc/hex/bin.html 구성 인도 사진이다. 특별한 게 안보이니까 주석을 보자 MIME Type의 e-mail이 보인다. 아래..

tutoreducto.tistory.com

이 문제의 마지막 레오폴드씨의 말을 기억하는가

그니까 우리가 뭘잘못했는지는 모르겠는데, 사과해야 된다고 한다. 사과하는 방법은 이메일로 하면 될 거 같다. 이메이 릉 19번 문제의 주석에 있다.

레오폴드 씨의 이메일은 leopold.moz@pythonchallenge.com이다. 메일을 보내자 얼마지나지 않아 다음과 같은 답변을 받을 수 있다.

일단 24문제를 풀었던 파일중 mybroken.zip이라는 파일이 있었다. 파일이 손상된 파일이었는데, 어딘가 해시로 장난질을 하는 게 있나 보다. 우선 해시를 저런 식으로 보냈다는 것은 정상적인 파일의 해시값이 저 모양이라는 것이다. 한번 mybroken.zip의 해시를 구해보자

### 26_1.py

import hashlib

if __name__=="__main__" :

    with open("mybroken.zip","rb") as f:
        data = f.read()

    print(hashlib.md5(data).hexdigest())

결과는 "bbf6616928e23ecfef4b717f281c53cc" 이다. 레오폴드 씨가 보낸 해시랑은 다르다. 파일 또한 열어보면 어딘가 손상이 되었다는 안내를 받을 수 있다. 보통 이런 복구는 쉬운 일이 아닌데, Python Challenge는 다행히도, 한 바이트만 손상시켜놨다.(뭐 어딘지 모르고 뭔지 모르는 게 문제이긴 한데, 그건 때려보자)

 

### 26_2.py

import hashlib

if __name__=="__main__" :

    tar = "bbb8b499a0eef99b52c7f13f4e78c24b"
    stop = False

    with open("mybroken.zip","rb") as f:
        ori_dat = f.read()

    for i in range(len(ori_dat)) :
        if(stop==True) :
            break
        for b in range(256) :
            tmp = ori_dat[:i]+bytes([b])+ori_dat[i+1:]
            if(hashlib.md5(tmp).hexdigest()==tar) :
                with open("notbroken.zip","wb") as f:
                    f.write(tmp)
                    stop = True

결과는 다음과 같은 사진이다.

스삐드! 위에 있던 문자열인 boat를 합치면 speedboat가 된다. 이게 답이다.

Answer Url : http://www.pythonchallenge.com/pc/hex/speedboat.html 

* 이건 여담인데, zip 파일은 단순하게 CRC가 안 맞아서 안 열리는 거기 때문에, 요즘의 zip/unzip프로그램에서 대부분 해석해서 그냥 연다.

개그다.

Python Challenge 25의 Url은 다음과 같다

Python challenge 25 : hhttp://www.pythonchallenge.com/pc/hex/lake.html


구성

퍼즐이다. 퍼즐이라고 하면 어릴 때 재미있게 놀았던 기억이 새록새록 떠오른다. 특별한 점이 안보이니, 주석을 살펴보자

주석에 힌트가 있다. "파도들이 보이시나요?" 라는 건데, 어떻게 풀어야 할까


해결 아이디어

일단 waves니까 wave가 여러 개 있는 거 같다. 이미지의 이름이 lake1.jpg인 것을 미루어보아, 여러 개의 파일이 있을 줄 알았는데, 웬걸?

없단다. wave라니까 wav 확장자로 한번 들어가 볼까?

정확했다. 몇 번까지 있는지 모르겠으니 다운로드하여서 wav 폴더 안에 넣어두는 코드를 작성해 보자

### 25_1.py

from requests import *

if __name__=="__main__" :
    url = "http://www.pythonchallenge.com/pc/hex/lake"
    cnt = 1

    while(True) :
        new_url = url + str(cnt) + ".wav"
        res = get(new_url,auth=("butter","fly"))
        
        if(res.status_code!=200) :
            break
        
        with open("./25_wav/"+str(cnt)+".wav","wb") as f:
            f.write(res.content)

        cnt += 1

성공적인 결과는 아래와 같이 25개의 음성(wav확장자) 파일을 가질 수 있다.

파일크기는 전부 동일하다. 여기서 더 나아갈 수 있는 힌트는 기존 사진인 lake1.jpg에서 찾을 수 있다.

퍼즐처럼 이렇게 들어간다는 거다. 그러면 어떻게 넣을 수 있을까?

해결 코드에 사용된 주요 함수는 다음과 같다., 

  • wave.getnframe() : wave파일에 몇 개의 프레임이 들어있는지 확인
  • wave.readframe(frame) : frame만큼의 크기를 읽어 .byte로 반환
  • Image.frombytes(mode, size, bytes) : 새로운 이미지를 생성하는데, bytes에서 읽어 온다. 크기가 넘어가면 자른다. 아래 참조

https://www.geeksforgeeks.org/python-pil-image-frombytes-method/

 

Python PIL | Image.frombytes() Method - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

* Image.frombytes(mode, size, bytes)의 사용법은 위에 링크로 올린다. 크기가 넘어가면 잘리는 example을 볼 수 있다.

 

이를 이용한 해결 코드는 다음과 같다. 

### 25_2.py

from PIL import Image
import wave

if __name__=="__main__" :
    url = "http://www.pythonchallenge.com/pc/hex/lake"
    cnt = 1

    wavs = [wave.open("./25_wav/"+str(x)+".wav") for x in range(1,26) ]
    ans = Image.new('RGB',(300,300))
    num_frames = wavs[0].getnframes()
    
    for index, w in enumerate(wavs) :
        tmp = w.readframes(num_frames)
        piece = Image.frombytes('RGB',(60,60),tmp)
        ans.paste(piece,(60*(index%5),60*(index//5)))
        
    ans.save("ans.png")

결과는 다음과 같다.

찾았다. decent

Answer Url : http://www.pythonchallenge.com/pc/hex/decent.html

+ Recent posts