CrySyS SecChallenge 2020: Headquaters in Greenland
Second part: Secure? Storage
A research associate was sent to Greenland to conduct observations, but he managed to smuggle a good amount of alcohol between his stuff. After a while he drank almost all the smuggling goods. The problem is that the dictator always comes out of him, and considering how much alcohol he has taken with himself, it would be useful to know what his next destination is, so that they can evacuate the place in time. He reprogrammed the ESP32 that was meant to transmit sensor data through MQTT protocol, now he uses it as a CnC server and also sends the following target locations to his accomplices. Somebody has to stop it before they take over the world!
This challenge had an environment accessible over MQTT. A dump of the content served is available for download
Categories
IoT, Python
Files
- mqtt.txt (dumped by me, see Solution)
Links
Solution
Let’s connect to the MQTT server, subscribe to everything and dump the messages:
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
client.subscribe('#')
client.subscribe('$SYS/#')
def on_message(client, userdata, msg):
print msg.payload
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect('1.2.3.4', 12345, 60)
client.loop_forever()
The output of this command can be found in the mqtt.txt
file I uploaded to the files section. It looks like an endless stream of sequences similar to this:
5797 seconds
2
2
2
62.74
50.25
47.51
1899.30
1894.99
0.91
0.91
60.57
10.87
10.28
1861.57
409.85
0.20
0.20
60.16
9.22
8.91
1852.32
353.74
0.10
0.10
48
2
20920
5862
6581
5790
6509
179410
256756
97730
54341
0111001 NAND 1110111
NOT 0100000
1100001 OR 110
1110110 AND 1011011
NOT 001100
101100 NAND 1110
1110101 NAND 11011
NOT 0110011
1110111 AND 1001001
1011011 NAND 10101
NOT 0111011
The story says these are meant to be sensors, so we should probably focus on the less sensor-looking bitwise operation lines instead. After a few tries with manually calculating it turned out these are not 8 bit operations, but rather the size of the longest operand, then the result padded to 8 bits to represent ASCII characters. By manually doing the first few, flag-looking words will be the result. However doing the whole flag manually wouldn’t be too comfortable, so I wrote a program to do this automatically:
uint8_t mask_by_len(size_t len)
{
return (uint8_t)((1u << len) - 1);
}
uint8_t bin_to_byte(const char* s)
{
unsigned v = 0;
while(*s)
{
v |= (*(s++) == '1' ? 1 : 0);
v <<= 1;
}
return v >> 1;
}
std::map<std::string_view, uint8_t(*)(uint8_t, uint8_t)> g_bin_ops{
{ "OR", [](uint8_t a, uint8_t b) -> uint8_t { return a | b; } },
{ "XOR", [](uint8_t a, uint8_t b) -> uint8_t { return a ^ b; } },
{ "AND", [](uint8_t a, uint8_t b) -> uint8_t { return a & b; } },
{ "NAND", [](uint8_t a, uint8_t b) -> uint8_t { return ~(a & b); } },
};
std::vector<std::pair<std::regex, uint8_t(*)(std::smatch&)>> g_decode_ops{
{
std::regex{ "\\s*NOT\\s*([01]+)\\s*" }, [](std::smatch& match) -> uint8_t
{
const auto& uni = match[1].str();
return ~(bin_to_byte(uni.c_str())) & mask_by_len(uni.size());
}
},
{
std::regex{ "\\s*([01]+)\\s*([A-Z]+)\\s*([01]+)\\s*" },
[](std::smatch& match) -> uint8_t
{
const auto& lhs = match[1].str();
const auto& rhs = match[3].str();
const auto& op = match[2].str();
const auto v = g_bin_ops[op](
bin_to_byte(lhs.c_str()),
bin_to_byte(rhs.c_str())
);
return v & mask_by_len(std::max(lhs.size(), rhs.size()));
}
},
};
int main()
{
std::ifstream ifs{ "mqtt.txt" };
while(!ifs.eof())
{
std::string line;
std::getline(ifs, line);
for(auto& fn : g_decode_ops)
{
std::smatch smatch;
if(std::regex_match(line, smatch, fn.first))
printf("%c", fn.second(smatch));
}
}
return 0;
}
Output (rest of the repeating sequence omitted):
cd20{7h3r3_15_NO7_MUCh_7O_DO_1N_gR33nLAnD_L375_1NvaD3_7H3_WoRlD}