VHDLock
7 minutes to read
We are given a lock.vhd
file with this code:
----------------------------------
-- 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;
We also have 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
Source code analysis
The lock.vhd
file is a VHDL file, which is used to model and describe the behavior and structure of digital systems.
Components
First of all, we have the xor_get
component:
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;
As a block diagram, we can define it as follows:
This component simply takes two 16-bit vectors and outputs the XOR of them.
The second component we have is 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;
This one just takes a 4-bit vector and transforms it to a 16-bit vector with the given cases:
For instance, 0 is mapped to 1, 1 is mapped to 2, 2 is mapped to 4, 3 is mapped to 8… So we see that $k \mapsto 2^k$ for $0 \leqslant k < 16$.
Main component
The main component is this:
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;
Basically, it defines three inputs (two 4-bit vectors and one 16-bit vector) and two 16-bit vector outputs. The previous components are implemented and connected as follows:
Given the above structure, we can guess that the out.txt
we have are the values of output1
and output2
for some input values input_1
, input_2
, and xorKey
. Moreover, since input_1
and input_2
form a 8-bit vector, we can guess that it’s going to be a character of the flag.
Testing
For example, H
is 0x48
in hexadecimal, so "0100"
goes to input_1
and "1000"
goes to input_2
. As a result, we have "0000000000010000"
in decoder1
and "0000000100000000"
in decoder2
.
In the first line of out.txt
we have 35 307
, which means that output1
is "0000000000100011"
and output2
is "0000000100110011"
.
If we XOR decoder1
and output1
we get "0000000000110011"
, and if we XOR decoder2
and output2
we get "0000000000110011"
. Both results match! Do we have xorKey
? Let’s try:
$ 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}')
Looks good, but the flag is not correct…
Solution
Since this is a hardware challenge, we can expect that xorKey
is changing due to some switches or buttons on the hardware device with the VDHL description. Therefore, the decryption won’t always work.
Instead, we can take another approach. Notice that both inputs to xor_get
have a single bit set to '1'
. Therefore, if we XOR both outputs, we will get a bit vector that has only 2 bits set to '1'
(or every bit set to '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} $$
Here, we can consider the two possibilities:
>>> 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')
Now we can decide that only H
makes sense. Let’s try with some more outputs:
>>> 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')
Here we see it’s T
because of the flag format (but it could’ve been E
).
One character that stands out is 2
in L2v3
, and comes from these output:
>>> 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')
And it is obviously a 0
, which fits better as L0v3
.
There is a special case, when both decoder1
and decoder2
have the same value. Then, we have five possibilities: 0x33
, 0x44
, 0x55
, 0x66
and 0x77
.
Implementation
Knowing this, we can write a script to print all possible characters:
# ...
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)
And this is the output:
$ 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] }
Now, we need to choose one character from each of the brackets. In the end, we will get the flag (only two characters were wrong).
Flag
After a bit of guessing and trial and error, we get the flag:
HTB{I_L0v3_VHDL_but_LOve_my_5w33thear7_m0re}
The full script can be found in here: solve.py
.