I2C 总线上的 MITM

电器工程 i2c 可编程逻辑
2022-01-23 09:29:13

我一直在尝试设计一个模块,允许我修改 I2C 总线上选定的从机响应。这是原始总线配置(为清楚起见,未显示上拉电阻和电源连接:

在此处输入图像描述 这条总线上只有 2 个设备,而且只有 100kHz。控制器 MCU(I2C 主机)和 RFID 读卡器(I2C 从机)NXP PN512。我无法修改控制器固件或更改 I2C 总线事务。好的部分是控制器只发送两种类型的交易:

Master (Write Register) - <s><address+W><register number><data><p> Master (Read Register) - <s><address+W><register number><p><s><address+R><data><p>

我想要做的是用我自己的字节替换主寄存器读取期间选择的数据字节。我可以通过 UART (921.6kbaud) 将 MCU 想要读取的寄存器号发送到我的 PC。我可以在那里用 C/C++ 或 Python 处理它们。当我收到需要替换其值的寄存器号时,我可以将一个假字节发送回我的设备,它会负责将其发送回控制器以替换原始卡响应。

起初我将 I2C 总线分成两条总线: 在此处输入图像描述

我尝试了 Arduino Nano,后来又尝试了使用时钟拉伸的 CPLD。面向 MCU 控制器的 ATmega328 硬件 I2C 无法跟上,因为有时启动序列在前一个停止周期后 5us 之前生成。因此,AVR 时不时地不接受读取事务。CPLD 可以处理停止/启动速度,结果证明总线拉伸在 MCU 中被禁用。

我想出了一个想法,我可以通过检测单字节写入来“预测”主寄存器读取,因为我确信它之后是读取。在接下来的读取周期地址写入期间,我似乎有足够的时间从从机中引入字节。那并不完全奏效。总线事务在开始时似乎很好(大约前 5 秒),但随后控制器停止了总线上的所有通信,就好像它检测到它没有直接与标签读取对话一样。

读卡器也可以对主机产生中断。IRQ 是基于计时器或事件的。我将问题归咎于我在公共汽车上固有的延迟。我可能错了,但我想出了另一个“零延迟”设计。 在此处输入图像描述

这个想法是我只能断开 SDA 线,让 SCL 线连接在主机和从机之间。这样我仍然可以在任一方向上替换数据线上的字节。事实证明,设计更加复杂,因为我必须根据总线周期控制 SDA 线路方向。这是处理总线事务并通过 UART 将十六进制字节发送到计算机的 VHDL 代码。从计算机接收字节尚未实现:

library ieee; 
use ieee.std_logic_1164.all; 
use ieee.numeric_std.all; 

entity I2C_Sniffer is 
port ( 
 clk : in std_logic;

 scl_master : in std_logic; 
 sda_master : inout std_logic;
 sda_slave  : inout std_logic;

 tx : out std_logic

); 
end entity I2C_Sniffer; 

architecture arch of I2C_Sniffer is
 signal clkDiv: std_logic_vector(7 downto 0) := (others => '0');

 type I2C_STATE is (I2C_IDLE, I2C_MASTER_WRITE, I2C_SLAVE_ACK, I2C_MASTER_READ, I2C_MASTER_ACK);
 signal i2cState: I2C_STATE := I2C_IDLE;

 type I2C_BUS_DIR is (MASTER_TO_SLAVE, SLAVE_TO_MASTER);
 signal i2cBusDir: I2C_BUS_DIR := MASTER_TO_SLAVE;

 signal i2cRxData: std_logic_vector(7 downto 0);
 signal i2cCntr: integer range 0 to 8 := 0;

 signal i2cAddr: std_logic := '1';
 signal i2cCmd: std_logic := '0';

 signal scl_d: std_logic := '1';
 signal scl: std_logic := '1';
 signal sda_d: std_logic := '1';
 signal sda: std_logic := '1';

 --Strobes for SCL edges and Start/Stop bits
 signal start_strobe : std_logic := '0';
 signal stop_strobe : std_logic := '0';
 signal scl_rising_strobe : std_logic := '0';
 signal scl_falling_strobe : std_logic := '0';

 type UART_STATE is (UART_IDLE, UART_START, UART_DATA, UART_STOP);
 signal uartState: UART_STATE := UART_IDLE;

 signal uartTxRdy: std_logic := '0';
 signal uartTxData: std_logic_vector(7 downto 0);
 signal uartCntr: integer range 0 to 8 := 0;

begin

 CLK_DIV: process (clk)
 begin
   if rising_edge(clk) then
     clkDiv <= std_logic_vector(unsigned(clkDiv) + 1);
   end if;
 end process;

I2C_STROBES: process (clk)
begin
  if rising_edge(clk) then
    --Pipelined SDA and SCL signals

    scl_d <= scl_master;
    scl <= scl_d;

    scl_rising_strobe <= '0';
    if scl = '0' and scl_d = '1' then
      scl_rising_strobe <= '1';
    end if;

    scl_falling_strobe <= '0';
    if scl = '1' and scl_d = '0' then
      scl_falling_strobe <= '1';
    end if;

    if i2cBusDir = MASTER_TO_SLAVE then
      sda_d <= sda_master;
      sda <= sda_d;
    else
      sda_d <= sda_slave;
      sda <= sda_d;
    end if;

    start_strobe <= '0';
    if sda_d = '0' and sda = '1' and scl = '1' and scl_d = '1' then
      start_strobe <= '1';
    end if;

    stop_strobe <= '0';
    if sda_d = '1' and sda = '0' and scl = '1' and scl_d = '1' then
      stop_strobe <= '1';
    end if;
  end if;
end process;

BUS_DIR: process(sda_master, sda_slave, i2cBusDir)
begin 
  if i2cBusDir = MASTER_TO_SLAVE then
    sda_slave <= sda_master;
    sda_master <= 'Z';
  else
    sda_master <= sda_slave;
    sda_slave <= 'Z';
  end if;
end process;

I2C: process(clk)
begin
    if rising_edge(clk) then
        uartTxRdy <= '0';

        case i2cState is
            when I2C_IDLE =>
                i2cBusDir <= MASTER_TO_SLAVE;

                if start_strobe = '1' then
                    i2cAddr <= '1';
                    i2cCntr <= 0;
                    i2cState <= I2C_MASTER_WRITE;
                end if;

            -- Master Write (Address/Data)
            when I2C_MASTER_WRITE =>
                i2cBusDir <= MASTER_TO_SLAVE;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                        uartTxData <= "00001010";
                        uartTxRdy <= '1';
                end if;

                if scl_rising_strobe = '1' then
                    if i2cCntr <= 7 then
                        i2cRxData(7 - i2cCntr) <= sda;
                        i2cCntr <= i2cCntr + 1;
                    end if;
                end if;

                if i2cCntr = 4 then
                    case i2cRxData(7 downto 4) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    case i2cRxData(3 downto 0) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    if scl_falling_strobe = '1' then
                        i2cState <= I2C_SLAVE_ACK;

                        if i2cAddr = '1' then
                            i2cCmd <= i2cRxData(0);
                            i2cAddr <= '0';
                        end if;
                    end if;
                end if;

            when I2C_SLAVE_ACK =>
                i2cBusDir <= SLAVE_TO_MASTER;

                if scl_falling_strobe = '1' then
                    i2cCntr <= 0;

                    if i2cCmd = '0' then
                        i2cState <= I2C_MASTER_WRITE;
                    else
                        i2cState <= I2C_MASTER_READ;
                    end if;
                end if;

            when I2C_MASTER_READ =>
                i2cBusDir <= SLAVE_TO_MASTER;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                        uartTxData <= "00001010";
                        uartTxRdy <= '1';
                end if;

                if scl_rising_strobe = '1' then
                    if i2cCntr <= 7 then
                        i2cRxData(7 - i2cCntr) <= sda;
                        i2cCntr <= i2cCntr + 1;
                    end if;
                end if;

                if i2cCntr = 4 then
                    case i2cRxData(7 downto 4) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 then
                    case i2cRxData(3 downto 0) is
                        when "0000" => uartTxData <= "00110000"; --0
                        when "0001" => uartTxData <= "00110001"; --1
                        when "0010" => uartTxData <= "00110010"; --2
                        when "0011" => uartTxData <= "00110011"; --3
                        when "0100" => uartTxData <= "00110100"; --4
                        when "0101" => uartTxData <= "00110101"; --5
                        when "0110" => uartTxData <= "00110110"; --6
                        when "0111" => uartTxData <= "00110111"; --7
                        when "1000" => uartTxData <= "00111000"; --8
                        when "1001" => uartTxData <= "00111001"; --9
                        when "1010" => uartTxData <= "01000001"; --A
                        when "1011" => uartTxData <= "01000010"; --B
                        when "1100" => uartTxData <= "01000011"; --C
                        when "1101" => uartTxData <= "01000100"; --D
                        when "1110" => uartTxData <= "01000101"; --E
                        when "1111" => uartTxData <= "01000110"; --F
                        when others => uartTxData <= "00111111"; --?
                    end case;
                    uartTxRdy <= '1';
                end if;

                if i2cCntr = 8 and scl_falling_strobe = '1' then
                    i2cState <= I2C_MASTER_ACK;
                end if;

            when I2C_MASTER_ACK =>
                i2cBusDir <= MASTER_TO_SLAVE;
                if scl_falling_strobe = '1' then
                    i2cCntr <= 0;
                end if;

                if stop_strobe = '1' then
                    i2cState <= I2C_IDLE;
                    uartTxData <= "00001010"; -- \n
                    uartTxRdy <= '1';
                end if;
        end case;
    end if;
end process;


UART: process (clk, clkDiv(1), uartTxRdy)
begin
    if rising_edge(clk) then
        case uartState is
            when UART_IDLE =>
                if uartTxRdy = '1' then
                    uartState <= UART_START;
                end if;

            when UART_START =>
                if clkDiv(1 downto 0) = "00" then
                    tx <= '0';
                    uartState <= UART_DATA;
                    uartCntr <= 0;
                end if;

            when UART_DATA =>
                if clkDiv(1 downto 0) = "00" then
                    if uartCntr <= 7 then
                        uartCntr <= uartCntr + 1;
                        tx <= uartTxData(uartCntr);
                    else
                        tx <= '1';
                        uartState <= UART_STOP;
                    end if;
                end if;

            when UART_STOP =>
                if clkDiv(1 downto 0) = "00" then
                    tx <= '1';
                    uartState <= UART_IDLE;
                end if;
        end case;
    end if;
  end process;
end architecture arch;

下面是使用控制 SDA 线的 CPLD 捕获的总线转换。

寄存器写入:

在此处输入图像描述

注册阅读:

在此处输入图像描述

当公交车方向改变时,您会看到一些故障。这是由于 CPLD 改变总线方向和读卡器产生 ACK 之间的时序差异造成的。ACK 电平似乎在 SCL 的上升沿稳定。据我所知,这就是你所需要的。

有了这个东西,控制器的行为与分离总线在几秒钟内暂停任何总线活动的方式相同。我还测试了模拟那个 MCU 并为我生成总线流量的 w Arduino,看起来 Arduino 也时不时地冻结。所以我想我可能对 VHDL 状态机有某种问题,在某些情况下,我会陷入一种状态而无路可走。有任何想法吗?

2个回答

我认为像你一样尝试cutsey hack是在自找麻烦,而这正是你遇到的那种症状。你基本上是在试图作弊,希望你不会被抓住。

根据您的描述,您没有尝试过的一件事是完全模拟此读卡器。你还没有真正解释它究竟做了什么以及它有多复杂,但从主人发送的内容来看,它并没有那么复杂。

使用具有硬件 IIC 从机功能的微控制器。那是连接到主控的。固件模拟读卡器。由于主机读取的唯一内容是一系列寄存器,固件的另一部分与读卡器完全异步通信,以从中获取信息并控制它。这也意味着复位线和 IRQ 线也是分开的。

如果做得对,这必须奏效,因为不会发生作弊。读卡器看到一个控制器向它发送命令并准确地读取它的预期使用方式。这包括响应 IRQ 事件。

主人认为它直接与真正的读卡器对话,因为你模拟它的所有操作就像真实的东西一样,包括重置和 IRQ 行为。

这听起来可能比一些快速而肮脏的将不同的字节干扰到总线黑客上更多的工作,但正如您发现的那样并不那么快,并且可能总是存在一些时间问题。通过完全仿真,所有时序约束都被解除。如果你的仿真还没有赶上读卡器所做的事情,那么它对主人的行为就像它还没有发生一样。在您的仿真准备好在各个方面对事件做出响应之前,您基本上会假装没有发生任何新情况。

这意味着您确实拥有固件的两个异步部分:呈现给主机的读卡器的 IIC 仿真,以及允许您将其所有状态保持在内部存储器中的完整读卡器驱动程序。

由于您没有作弊,因此如果操作正确,这必须有效。唯一的系统级问题是,与现有系统相比,master 看到并导致读卡器操作会有一些延迟。对于“读卡器”来说,这听起来没什么大不了的,考虑到这种延迟最坏的情况可能是 10 毫秒。在人类的时间尺度上,它当然不应该被注意到。

请注意,您的模拟器和读卡器之间的通信不限于当前使用的 100 kbits/s。您应该以读卡器和硬件允许的速度运行它。毕竟,在那个链接上,你将成为主人,所以你拥有时钟。同样,使用适当的固件架构和异步任务,这应该无关紧要。事实上,您的驱动程序可能会更频繁地通信并从读卡器获取更多数据,而不是主机从您的仿真器获取的数据。

我建议您使用 Arduino Nano 作为 MITM 走在正确的轨道上,尽管我认为最好使用两个。

在此处输入图像描述

NXP-PN512 将以 3.4 Mhz 时钟速度运行,因此我建议您可以使用 1.5 - 2 MHz 左右的频率来与阅读器对话的右手 MCU。
由于左侧 MCU 设置为 100 kHz,一旦您识别出任何事务字节(地址/寄存器-WR),您可以将其复制到 MCU 之间的 8 位并行总线(或更宽)上,并将命令发送给阅读器在慢速 I2C 通道上少于一个时钟时间。同样地,在慢速总线上不到一个时钟时间就可以从阅读器接收一个字节,从而有足够的时间来设置回复字节。

我在这里假设您实际上可能需要将多个字节转换为 NFC ID,而不仅仅是一个逐字节的转换(这需要更少的时间)。

我当时看到的主要问题是,如果您确实需要将多个字节序列化到 PC 或从 PC 序列化以映射您的更改,那么时间变得更加关键。如果有一种方法可以将映射更改算法/表构建到左侧 MCU 中,这似乎是一种更好的方法,尽管解决多字节标识符映射仍然是最大的挑战。

如果我错了,您只需要映射一个卡标识符字节,那么这可能会起作用。

在您对 Arduino 的早期测试中,您是否确保所有中断都已关闭(至少只使用 TWI)?如果您不这样做,那么这可能会影响您的时间安排。