VHDLock
7 minutos de lectura
Se nos proporciona un archivo lock.vhd
con este código:
----------------------------------
-- first component for xor operation
----------------------------------
library ieee;
use ieee.std_logic_1164.all;
entity xor_get is
port(input1,input2 : in std_logic_vector(15 downto 0);
output : out std_logic_vector(15 downto 0));
end xor_get;
architecture Behavioral of xor_get is
begin
output <= input1 xor input2;
end Behavioral;
----------------------------------
-- second component for decoder 4x16
----------------------------------
library ieee;
use ieee.std_logic_1164.all;
entity decoder_4x16 is
port(input : in std_logic_vector(3 downto 0);
output : out std_logic_vector(15 downto 0));
end decoder_4x16;
architecture Behavioral of decoder_4x16 is
begin
process(input)
begin
case input is
when "0000" => output <= "0000000000000001";
when "0001" => output <= "0000000000000010";
when "0010" => output <= "0000000000000100";
when "0011" => output <= "0000000000001000";
when "0100" => output <= "0000000000010000";
when "0101" => output <= "0000000000100000";
when "0110" => output <= "0000000001000000";
when "0111" => output <= "0000000010000000";
when "1000" => output <= "0000000100000000";
when "1001" => output <= "0000001000000000";
when "1010" => output <= "0000010000000000";
when "1011" => output <= "0000100000000000";
when "1100" => output <= "0001000000000000";
when "1101" => output <= "0010000000000000";
when "1110" => output <= "0100000000000000";
when "1111" => output <= "1000000000000000";
when others => output <= "0000000000000000";
end case;
end process;
end Behavioral;
----------------------------------
-- main component
----------------------------------
library ieee;
use ieee.std_logic_1164.all;
entity main is
port(input_1,input_2 : in std_logic_vector(3 downto 0);
xorKey : in std_logic_vector(15 downto 0);
output1,output2 : out std_logic_vector(15 downto 0));
end main;
architecture Behavioral of main is
signal decoder1,decoder2: std_logic_vector(15 downto 0);
component xor_get is
port(input1,input2 : in std_logic_vector(15 downto 0);
output : out std_logic_vector(15 downto 0));
end component;
component decoder_4x16 is
port(input : in std_logic_vector(3 downto 0);
output : out std_logic_vector(15 downto 0));
end component;
begin
L0 : decoder_4x16 port map(input_1,decoder1);
L1 : decoder_4x16 port map(input_2,decoder2);
L2 : xor_get port map(decoder1,xorKey,output1);
L3 : xor_get port map(decoder2,xorKey,output2);
end Behavioral;
También tenemos out.txt
:
35 307
17 33
33 53
183 2103
35 563
17 32817
33 4145
63 54
179 115
57 57
17 32817
23 119
35 307
33 33
33 4145
23 32823
115 55
177 17
177 33
23 32823
35 4147
33 32817
177 113
119 23
19 32819
113 8241
177 561
23 32823
59 19
177 177
57 57
63 63
179 35
113 305
113 17
119 53
179 55
57 177
17 32817
119 8247
59 50
177 53
113 17
183 8247
Análisis del código fuente
El archivo lock.vhd
es un archivo de VHDL, que se utiliza para modelar y describir el comportamiento y la estructura de los sistemas digitales.
Componentes
En primer lugar, tenemos el componente xor_get
:
entity xor_get is
port(input1,input2 : in std_logic_vector(15 downto 0);
output : out std_logic_vector(15 downto 0));
end xor_get;
architecture Behavioral of xor_get is
begin
output <= input1 xor input2;
end Behavioral;
Como diagrama de bloques, podemos definirlo de la siguiente manera:
Este componente simplemente toma dos vectores de 16 bits y genera el XOR de ellos.
El segundo componente que tenemos es decoder_4x16
:
entity decoder_4x16 is
port(input : in std_logic_vector(3 downto 0);
output : out std_logic_vector(15 downto 0));
end decoder_4x16;
architecture Behavioral of decoder_4x16 is
begin
process(input)
begin
case input is
when "0000" => output <= "0000000000000001";
when "0001" => output <= "0000000000000010";
when "0010" => output <= "0000000000000100";
when "0011" => output <= "0000000000001000";
when "0100" => output <= "0000000000010000";
when "0101" => output <= "0000000000100000";
when "0110" => output <= "0000000001000000";
when "0111" => output <= "0000000010000000";
when "1000" => output <= "0000000100000000";
when "1001" => output <= "0000001000000000";
when "1010" => output <= "0000010000000000";
when "1011" => output <= "0000100000000000";
when "1100" => output <= "0001000000000000";
when "1101" => output <= "0010000000000000";
when "1110" => output <= "0100000000000000";
when "1111" => output <= "1000000000000000";
when others => output <= "0000000000000000";
end case;
end process;
end Behavioral;
Este solo toma un vector de 4 bits y lo transforma en un vector de 16 bits con los casos dados:
Por ejemplo, 0 se asigna a 1, 1 se asigna a 2, 2 se asigna a 4, 3 se asigna a 8… Así que vemos que $k \mapsto 2^k$ para $0 \leqslant k < 16$.
Componente principal
El componente principal es este:
entity main is
port(input_1,input_2 : in std_logic_vector(3 downto 0);
xorKey : in std_logic_vector(15 downto 0);
output1,output2 : out std_logic_vector(15 downto 0));
end main;
architecture Behavioral of main is
signal decoder1,decoder2: std_logic_vector(15 downto 0);
component xor_get is
port(input1,input2 : in std_logic_vector(15 downto 0);
output : out std_logic_vector(15 downto 0));
end component;
component decoder_4x16 is
port(input : in std_logic_vector(3 downto 0);
output : out std_logic_vector(15 downto 0));
end component;
begin
L0 : decoder_4x16 port map(input_1,decoder1);
L1 : decoder_4x16 port map(input_2,decoder2);
L2 : xor_get port map(decoder1,xorKey,output1);
L3 : xor_get port map(decoder2,xorKey,output2);
end Behavioral;
Básicamente, define tres entradas (dos vectores de 4 bits y un vector de 16 bits) y dos vectores de 16 bits como salidas. Los componentes anteriores se implementan y se conectan de la siguiente manera:
Dada la estructura anterior, podemos suponer que el archivo out.txt
que tenemos son los valores de output1
y output2
para ciertos valores de entrada input_1
, input_2
, y xorKey
. Además, como input_1
y input_2
forman un vector de 8 bits, podemos suponer que será un carácter de la flag.
Pruebas
Por ejemplo, H
es 0x48
en hexadecimal, entonces "0100"
va a input_1
y "1000"
va a input_2
. Como resultado, tenemos "0000000000010000"
en decoder1
y "0000000100000000"
en decoder2
.
En la primera línea de out.txt
tenemos 35 307
, que significa que output1
es "0000000000100011"
y output2
es "0000000100110011"
.
Si hacemos el XOR de decoder1
y output1
obtenemos "0000000000110011"
, y si hacemos el XOR de decoder2
y output2
obtenemos "0000000000110011"
. ¡Ambos resultados coinciden! ¿Tenemos xorKey
? Vamos a probarlo:
$ python3 -q
>>> with open('out.txt') as f:
... out = [tuple(map(int, line.split())) for line in f.read().splitlines()]
...
>>> xor_key = 0b0000000000110011
>>> flag = bytearray()
>>>
>>> from math import log2
>>>
>>> for o1, o2 in out:
... flag.append((int(log2(o1 ^ xor_key)) << 4) | int(log2(o2 ^ xor_key)))
...
>>> flag
bytearray(b'HTB{I_L2v3_VHDL_but_LOve_my_5w33thebr7_m0re}')
Se ve bien, pero la flag no es correcta …
Solución
Dado que este es un reto de hardware, podemos esperar que xorKey
está cambiando a causa de algunos interruptores o botones en el dispositivo de hardware con la descripción VHDL. Por lo tanto, el descifrado no siempre funcionará.
En cambio, podemos adoptar otro enfoque. Nótese que ambas entradas a xor_get
tienen un solo bit a '1'
. Por lo tanto, el XOR de ambas salidas debe ser un vector que solo tiene 2 bits a '1'
(o todos a '0'
):
$$ \begin{align} \begin{cases} \mathtt{output1} = \mathtt{decoder1} \oplus \mathtt{xorKey} \\ \mathtt{output2} = \mathtt{decoder2} \oplus \mathtt{xorKey} \end{cases} \iff \\ \iff \mathtt{output1} \oplus \mathtt{output1} = \mathtt{decoder1} \oplus \mathtt{decoder2} \end{align} $$
Aquí, podemos considerar dos posibilidades:
>>> f'{35 ^ 307:016b}'
'0000000100010000'
>>> f'{35 ^ 307:016b}'[::-1].index('1')
4
>>> 15 - f'{35 ^ 307:016b}'.index('1')
8
>>> chr(0x48), chr(0x84)
('H', '\x84')
Ahora podemos decidir que solo la H
tiene sentido. Probemos con algunas salidas más:
>>> f'{17 ^ 33:016b}'
'0000000000110000'
>>> f'{17 ^ 33:016b}'[::-1].index('1')
4
>>> 15 - f'{17 ^ 33:016b}'.index('1')
5
>>> chr(0x45), chr(0x54)
('E', 'T')
Aquí vemos que es T
debido al formato de la flag (pero podría haber sido E
).
Un carácter que se parece raro es el 2
en L2v3
, y proviene de estos resultados:
>>> f'{17 ^ 33:016b}'
'0000000000110000'
>>> f'{63 ^ 54:016b}'[::-1].index('1')
0
>>> 15 - f'{63 ^ 54:016b}'.index('1')
3
>>> chr(0x03), chr(0x30)
('\x03', '0')
Y obviamente es un 0
, que se ajusta mejor en L0v3
.
Hay un caso especial, cuando ambos decoder1
y decoder2
tienen el mismo valor. Entonces, tenemos cinco posibilidades: 0x33
, 0x44
, 0x55
, 0x66
y 0x77
.
Implementación
Sabiendo esto, podemos escribir un script para imprimir todos los caracteres posibles:
# ...
flag = ''
for o1, o2 in out:
xor = o1 ^ o2
if xor:
i1 = f'{xor:016b}'[::-1].index('1')
i2 = 15 - f'{xor:016b}'.index('1')
option1, option2 = (i1 << 4) | i2, (i2 << 4) | i1
if not (0x20 <= option2 < 0x7f):
flag += chr(option1)
elif not (0x20 <= option1 < 0x7f):
flag += chr(option2)
else:
flag += f' [{chr(option1)}{chr(option2)}] '
else:
flag += ' [\x33\x44\x55\x66\x77] '
print('\nPossible:', flag)
Y esta es la salida:
$ python3 solve.py
Test: HTB{I_L2v3_VHDL_but_LOve_my_5w33thebr7_m0re}
Possible: H [ET] [$B] {I_L0 [gv] [3DUfw] _ [Ve] H [3DUfw] L_ [&b] [Wu] [Gt] _LO [gv] [Ve] _my_ [5S] [3DUfw] [3DUfw] [3DUfw] [Gt] h [Ve] a ['r] [7s] _m0 ['r] [Ve] }
Ahora, necesitamos elegir un carácter de cada grupo entre corchetes. Al final, obtendremos la flag (solo dos caracteres estaban equivocados).
Flag
Después de algunas deducciones, pruebas y errores, obtenemos la flag:
HTB{I_L0v3_VHDL_but_LOve_my_5w33thear7_m0re}
El script completo se puede encontrar aquí: solve.py
.