基于STM32L4的硬核"shellcode"

栏目: IT技术 · 发布时间: 4年前

内容简介:Fundamental, fundamental, fundamental, …without the fundamental, all those fancy magics won’t work.有经验的程序员都知道源码需要进行编译、链接、封装,然后才能执行。那你知道如何为一块CPU编写并编译程序吗?知道编译后的程序如何写入MCU、并让CPU加载运行的吗?早先收到阿里云提供的

Fundamental, fundamental, fundamental, …without the fundamental, all those fancy magics won’t work.

有经验的 程序员 都知道源码需要进行编译、链接、封装,然后才能执行。那你知道如何为一块CPU编写并编译程序吗?知道编译后的程序如何写入MCU、并让CPU加载运行的吗?

前言

早先收到阿里云提供的 Developer kit 开发板,对他们的RTOS进行体验,就是下面这款:

基于STM32L4的硬核

不得不说,使用aos全家桶运行、烧写和调试代码都非常方便;而且最近看发现还支持[最小化定制裁剪][min],根据自己的需求下载对应的代码,算是咱256G小硬盘的福音了:)

不过今天不是分析阿里的RTOS( AliOS Things ),也不是把玩这块开发板,而是借助其中的MCU来探索下裸板的开发和运行之路。

芯片分析

在开始为一块MCU编程之前,我们要做的第一件事就是先查看这个MCU的文档。例如,如果我们想写一个helloworld程序,那么就至少需要知道:

  1. MCU复位之后从哪启动,这决定了我们的main程序位置。
  2. MCU内存映射,这是为了查看串口的地址空间。

对于我们而言,手上的MCU型号是 STM32L496VGTx ,因此这些大部分都能在 stm32l496ae datasheet 中查看到。首先,在datasheet中我们知道STM32L496VGTx中的CPU是 ARM Cortex-M4 ,内存SRAM为 320KB ,内部含有1MB的Flash。

初始化

根据ARM的文档中关于 Cortex-m4 中断向量表 的介绍,我们可以看到保存第一条指令地址的地址为 0x0004

基于STM32L4的硬核

其中 0x0000 保存的是栈的地址。也就是说,CPU复位之后,会首先将0x0000地址的内容加载到栈寄存器sp中,然后将0x0004地址的内容加载并保存到指令寄存器pc中,然后才开始执行第一条指令。

CPU执行每条指令,本质上包含5步: 取指、译码、执行、访存、写回 。如果不影响状态,多条指令的5步可以交错,这就称为CPU的流水线,现代CPU都包含多级流水线的设计和其他的优化来提升执行速度。……扯远了,说这个主要是强调一点:CPU实际运行的第一条指令的地址为 *(addr *)0x0004 。而前面两条”指令”,即加载sp和加载pc,实际上是通过CPU硬件的有限状态机实现的。

内存映射

还是在ARM的文档 Memory-Model 中,可以看到我们的芯片内存映射的结构大致如下:

基于STM32L4的硬核

在32位的寄存器下,有大约4GB的寻址空间。其中ARM只定义了一个大概的范围,地址空间的实际映射其实和厂商的设计有比较大的关系。比如在我们的 STM32L4 MCU 中,实际的映射如下:

基于STM32L4的硬核

需要注意的是flash地址空间,为 0x08000000 ~ 0x08100000 ,大小为 0x10000 正好是datasheet中所说的1MB。还有就是APB的地址空间,因为APB总线通常是用来控制外设的,比如我们下面会用到的串口(UART)。

The Code

Talk is cheap,接下来就是实际的编码,我们的目标是在CPU上电启动后马上打印“HelloWorld”,没有其他多余的操作。

程序骨架

在打印HelloWorld之前,我们先确保MCU能够正常启动并运行我们的代码。为此,需要正确编译和链接我们的程序。根据上面ARM初始化向量表的定义,我们先写个汇编文件 startup_m4.s

.syntax unified
.cpu cortex-m4
.fpu softvfp
.thumb

.global g_pfnVectors
.global Default_Handler
.global Reset_Handler

.section .text
Default_Handler:
Infinite_Loop:
    b   Infinite_Loop

Reset_Handler:
    ldr sp, =stack_top
    mov r0, #0
    mov r1, #1
    mov r2, #2
    ror r3, r0, #2
_loop:
    add r3, #1
    B _loop

// ISR vecotor data
.section .isr_vector, "a"
g_pfnVectors:
    .word stack_top
    .word Reset_Handler
    .word Default_Handler // NMI
    .word Default_Handler // HardFault
    .word Default_Handler // MemManage
    .word Default_Handler // BusFault
    .word Default_Handler // UsageFault
    .word 0
    .word 0
    .word 0
    .word 0
	.word Default_Handler // SVC
    // and a lot more ...

Reset_Handler 是我们实际运行的第一条指令地址,其地址写在中断向量表的 0x04 偏移处。对于其他的中断处理程序,我们先简单放一部分到 Default_Handler 中。

编译和链接

有了代码,还需要链接到对应的地址中,执行这项任务的就是linker脚本。通常我们使用ld时也会调用默认的linker脚本,可以通过 ld --verbose 命令查看,不过默认的链接脚本无法满足我们的需求,所以根据上面的文档,我们写一个简单的链接脚本 m4.ld 如下:

ENTRY(Reset_Handler)

MEMORY {
    FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1024K
    SRAM (RWX) : ORIGIN = 0x20000000, LENGTH = 320K
}

stack_top = 0x20050000;

SECTIONS {
    .isr_vector : {
        . = ALIGN(8);
        KEEP(*(.isr_vector))
        . = ALIGN(8);
    } > FLASH

    .text : {
        . = ALIGN(8);
        *(.text)
        . = ALIGN(8);
        text_end = .;
    } > FLASH

    .data : { *(.data) } >SRAM AT>FLASH
    .bss : { *(.bss COMMON) } > SRAM
    . = ALIGN(8);
}

编译并链接我们的程序:

arm-none-eabi-as startup_m4.s -g -o startup_m4.o
arm-none-eabi-ld -T m4.ld startup_m4.o -o startup.elf

最后生成的是ELF程序,为了在裸板上运行,需要将无用的信息去掉,只保留纯粹的代码和数据:

arm-none-eabi-objcopy -O binary startup.elf startup.bin

如果想要了解更多链接脚本的语法和含义,可以参考官方的文档—— Linker Scripts

烧写和调试

有了 starup.bin 之后,就可以使用对应的接口写入Flash,对于我们这块开发板引出的接口是ST-LINK,所以可以直接使用stlink程序来写,前面说了Flash地址为 0x08000000

st-flash --reset write startup.bin 0x08000000

当然,你也可以使用其他工具,比如我最喜欢的 OpenOCD 。使用openocd需要自己对接口进行适配,其中包含了很多预置的配置,例如对于我们手上的开发板,可以使用以下配置:

source [find interface/stlink.cfg]

transport select hla_swd

source [find target/stm32l4x.cfg]

reset_config srst_only

值得一提的是,openocd的配置使用的是裁剪过的TCL语言,使用前可以花一两个小时先了解下。

OpenOCD中内置了gdbserver,不过如果你用openOCD+gef进行调试的话,很可能会遇到错误。经过查看代码和相关的资料,我发现openocd的gdbserver会将程序状态字命名为xPSR而不是传统的cpsr,所以我写了个gdb脚本解决这个问题:

set remote hardware-breakpoint-limit 6
set remote hardware-watchpoint-limit 4

# openOCD-gdbserver name $cpsr as $xPSR, make gef known about it
pi current_arch.all_registers = ['$r0', '$r1', '$r2', '$r3', '$r4', '$r5', '$r6', '$r7', '$r8', '$r9', '$r10', '$r11', '$r12', '$sp', '$lr', '$pc', '$xPSR']
pi current_arch.flag_register = '$xPSR'
reset-cache

# ignore stack
gef config context.layout "legend regs code args source memory threads trace extra"

target extend :3333

烧写成功后复位使用JTAG接口进行调试,可以看到进入了我们的程序中:

基于STM32L4的硬核

PS:由于我们的大部分中断都没有处理,所以单步调试触发中断后程序很可能跑飞:)

固件逆向

说句题外话,生成的 starup.bin 就是我们常说的固件,实际上在逆向分析时从flash读出来的数据也就是这个格式,从 0x00 地址开始。比如,分析这个固件的时候通常使用的方法是:

r2 -n -a arm -b 16 -m 0x08000000 startup.bin

其他 工具 也可以用类似的方法将首地址rebase进行分析,但关键是要知道对应芯片的中断向量表定义,这样才能找到真正的入口函数。

HelloWorld

现在有了骨架,可以实现真正的功能了。在操作系统中,我们 printf("hello world") 本质上是经过系统调用让内核把数据写到标准输出,但是在裸板上可没那么方便,一切都要自己操作。

打印数据到串口的功能通过UART实现,而UART是连接在CPU的APB总线上的。在软件上向UART发送数据实际上是通过向APB总线发送数据到UART硬件对应的接口,发送数据的操作通过将APB总线的读写映射为MMIO实现,简单来说就是通过CPU向内存读写数据实现总线上的读写操作。

在前面的图片中我们能看到APB总线的MMIO映射地址为 0x40000000 ,那么UART在哪个地址呢?可以通过 STM32的应用文档 中查看;或者更简单地,直接查看STM32的驱动文件 stm32l496xx.h

#define PERIPH_BASE           (0x40000000UL) /*!< Peripheral base address */
...
/*!< Peripheral memory map */
#define APB1PERIPH_BASE        PERIPH_BASE
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)
...
/*!< APB1 peripherals */
#define USART2_BASE           (APB1PERIPH_BASE + 0x4400UL)
#define USART3_BASE           (APB1PERIPH_BASE + 0x4800UL)
#define LPUART1_BASE          (APB1PERIPH_BASE + 0x8000U)
..
/*!< APB2 peripherals */
#define USART1_BASE           (APB2PERIPH_BASE + 0x3800UL)
  
#define USART2              ((USART_TypeDef *) USART2_BASE)
#define USART3              ((USART_TypeDef *) USART3_BASE)
#define UART4               ((USART_TypeDef *) UART4_BASE)
#define UART5               ((USART_TypeDef *) UART5_BASE)
#define LPUART1             ((USART_TypeDef *) LPUART1_BASE)
#define USART1              ((USART_TypeDef *) USART1_BASE)

在stm32l496xx中,APB总线连接了6个串口,起始地址分别是:

  • USART1, 0x40013800
  • USART2, 0x40004400
  • USART3, 0x40004800
  • ….

UART地址空间的定义是:

/**
  * @brief Universal Synchronous Asynchronous Receiver Transmitter
  */

typedef struct
{
  __IO uint32_t CR1;         /*!< USART Control register 1,                 Address offset: 0x00 */
  __IO uint32_t CR2;         /*!< USART Control register 2,                 Address offset: 0x04 */
  __IO uint32_t CR3;         /*!< USART Control register 3,                 Address offset: 0x08 */
  __IO uint32_t BRR;         /*!< USART Baud rate register,                 Address offset: 0x0C */
  __IO uint16_t GTPR;        /*!< USART Guard time and prescaler register,  Address offset: 0x10 */
  uint16_t  RESERVED2;       /*!< Reserved, 0x12                                                 */
  __IO uint32_t RTOR;        /*!< USART Receiver Time Out register,         Address offset: 0x14 */
  __IO uint16_t RQR;         /*!< USART Request register,                   Address offset: 0x18 */
  uint16_t  RESERVED3;       /*!< Reserved, 0x1A                                                 */
  __IO uint32_t ISR;         /*!< USART Interrupt and status register,      Address offset: 0x1C */
  __IO uint32_t ICR;         /*!< USART Interrupt flag Clear register,      Address offset: 0x20 */
  __IO uint16_t RDR;         /*!< USART Receive Data register,              Address offset: 0x24 */
  uint16_t  RESERVED4;       /*!< Reserved, 0x26                                                 */
  __IO uint16_t TDR;         /*!< USART Transmit Data register,             Address offset: 0x28 */
  uint16_t  RESERVED5;       /*!< Reserved, 0x2A                                                 */
} USART_TypeDef;

对应硬件接口:

基于STM32L4的硬核

软件中对UART的读写主要通过对UART本身的寄存器操作实现,例如向串口写一个字节就是: USART->TDR = 0x41 ,具体的写入内容根据型号有所差异,在 STM32F4XX 的驱动中相关代码如下:

/**
    * @brief  Transmits single data through the USARTx peripheral.
    * @param  USARTx: where x can be 1, 2, 3, 4, 5, 6, 7 or 8 to select the USART or
    *         UART peripheral.
    * @param  Data: the data to transmit.
    * @retval None
    */
  void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
  {
    /* Check the parameters */
    assert_param(IS_USART_ALL_PERIPH(USARTx));
    assert_param(IS_USART_DATA(Data));

    /* Transmit Data */
    USARTx->DR = (Data & (uint16_t)0x01FF);
  }

对于我们STM32L4XX的MCU,在官方的cube中代码实现为 stm32l4xx_cube/Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_uart.cHAL_UART_Transmit 函数,虽然相对复杂,但本质上也大同小异。

实际上在MCU中printf和puts等函数的实现都是通过逐字节写入UART寄存器实现的。所以我们新建一个c文件并定义最简单的print函数如下:

// hello_m4.c
volatile unsigned int * const UART_TDR = (unsigned int *)0x40008028; // LPUART1->TDR
void my_print(const char *data) {
    while(*data != '\0') {
        *UART_TDR = (unsigned int)(*data);
        data++;
    }
}

void my_entry() {
    my_print("hello world!\n");
    for(;;);
}

然后在之前的 Reset_Handler 稍加修改,令其跳转到我们的主程序执行:

Reset_Handler:
    ldr sp, =stack_top
    bl my_entry

最后编译并重新链接:

arm-none-eabi-gcc -c -O0 -mcpu=cortex-m4 -g hello_m4.c -o hello_m4.o
arm-none-eabi-ld -T m4.ld hello_m4.o startup_m4.o -o hello_m4.elf

监听串口的数据并重新烧写,一个硬核的HelloWorld就完成了!

$ miniterm /dev/ttyACM0 115200
--- Miniterm on /dev/ttyACM0  115200,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
hello world!

如果串口是USART而不是UART,那么可能需要经过一些额外的配置,具体可以参考 USART vs UART: Know the difference

在实际工程中,真正进入用户程序之前需要初始化好各个硬件外设,配置好基本的中断处理程序。这部分代码一般是由MCU vendor提供的,作为Bootloader(Boot ROM)固化。当然我们这里是绕过MCU直接针对CPU编写程序,以展示软硬件之间的微妙联系。

后记

本文到这里就结束了,那么标题中的shellcode在哪?……在CPU看来,代码和数据并没有本质的分别,所以当我们在说shellcode的时候,到底是在说什么呢?一段位置无关(PIC)的汇编代码就叫做shellcode了吗?也许这是值得每个喜欢造词的安全工程师深入思考的问题。

参考资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Pragmatic Thinking and Learning

Pragmatic Thinking and Learning

Andy Hunt / The Pragmatic Bookshelf / 2008 / USD 34.95

In this title: together we'll journey together through bits of cognitive and neuroscience, learning and behavioral theory; you'll discover some surprising aspects of how our brains work; and, see how ......一起来看看 《Pragmatic Thinking and Learning》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具