研究如何提取基于Linux的软路由固件。
最近在研究某款软路由,能在其官网下载到其软路由的ISO镜像,镜像解压可以获取到rootfs,但是该rootfs无法解压出来文件系统,怀疑是经过了某种加密。
把软路由器安装到PVE上,启动后也无法获取到Linux Shell的权限,只能看到该路由厂商自行开发的一个路由器Console界面。可以开启telnet/ssh,可以设置其密码,但是连接后同样是Console界面。
这种情况下对该软路由进行黑盒研究,难度非常大,是为下策,不是无可奈何的情况下不考虑该方案。
所以要先研究该怎样获取到该路由的文件系统,首先想到的方法是去逆向vmlinux,既然在不联网的情况下能正常跑起来这个软路由,说明本地肯定具备正常解密的所有条件,缺的只是其加密方法和rootfs格式。在通常情况下处理解密的代码位于vmlinux,所以只要能逆向出rootfs的加解密逻辑,就可以在本地自行解压该文件系统了。
该思路的难度不大,但是工作量非常大,是为中策,作为备选方案。
因为该软路由是被安装在PVE上,使用kvm启动,所以可以使用gdb对其内核进行调试,也可以通过gdb修改程序内存和寄存器的值。从而达到任意命令执行的目的,获取Linux Shell。
使用GDB调试软路由 在PVE界面的Monitor
选项中输入gdbserver
,默认情况下即可开启gdbserver,监听服务器的1234端口。
获取vmlinux:extract-vmlinux boot/vmlinuz > /tmp/vmlinux
。
gdb进行调试:gdb /tmp/vmlinux
。
然后挂上远程的gdbserver:target remote x.x.x.x:1234
。
大多数情况下,断下来的地址都是为0xFFFFFFFFxxxxxxxx
,该地址为内核地址,然后在gdb界面输入continue
,让其继续运行。
想要获取Linux Shell,那么就需要执行一句获取Shell的shellcode,但是不管是执行反连shell还是bind shell的shellcode都太长了。为了缩减shellcode的长度,可以让shellcode执行一句execve("/bin/sh", ["/bin/sh","-c","/usr/sbin/telnetd -l /bin/sh -p xxxxx"], 0)
命令(当然已经确定了存在telnetd
,和其路径)。
下面为上述shellcode的大致代码(测试的目标为x86_64系统):
1 2 3 4 5 6 7 8 9 10 11 12 13 0x00: /bin/sh\x00 0x08: -c\x00 0x10: cmd 0x100: 0x00 0x108: 0x08 0x110: 0x10 0x118: 0 mov rdi, 0x00 mov rsi, 0x100 xor rdx, rdx xor rax, rax mov al, 59 syscall
不过因为使用的是gdb,可以对程序内存寄存器进行修改,所以不需要这么长的shellcode,只需要执行下面的命令:
1 2 3 4 5 6 7 8 set *0x00=xxxx set *0x04=xxxx ...... set $rdi=0x00 set $rsi=0x100 set $rdx=0 set $rax=59 set *((int *)$rip)=0x050F(syscall)
这里建议只对用户态代码进行修改,如果直接改内核态的代码,容易让系统崩溃。
接下来的步骤就是如何进入用户态,首先需要增加软路由的负载,可以访问一下路由器的Web服务,或者执行一些会长时间运行的程序(比如ping
),然后按ctrl+c
,中断程序运行,重复N次,如果不是运气不好的情况下,会很快断在一个地址开头不是0xffffffff
的地址,这就是用户态程序的地址空间了。
接下来可以往栈、数据段内存写入我们要执行的命令,然后修改寄存器,修改当前pc
值为syscall
指令,再输入contiune
,系统就会运行你想执行的命令了。
理论上该思路没啥问题,但是在实际测试的过程中发现了一些小问题。
在测试过程中,程序中断的用户态代码是/bin/bash
的程序段,或者是libc
的程序段,当修改代码段的代码时,不会像平常调试普通程序那样,修改的只是映射的内存代码,当程序退出后,修改的代码随同映射的内存一起释放了。当一个新的bash
程序运行时,内存重新进行了映射,所以使用gdb修改当前程序的上下文,并不会影响到之后运行的程序。但是在调试内核的时候,进入用户态后,访问到的是该程序的真正内存区域,代码一经修改,除非系统重启,不然每次运行相同的程序,都将会运行修改后的代码。
所以按照上述理论修改了/bin/bash
代码段的指令,执行了/bin/sh -c "/usr/sbin/telnetd -l /bin/bash"
命令之后,bash
这个程序实际的代码已经被破坏了,所以在该命令成功开启了telnet
服务后,每当有用户连接这个telnet
服务,根据bash
程序代码被破坏的程度,程序将会有不同的异常(运气好,破坏的代码不重要,则不会影响到后续使用。运气不好,破坏的代码很重要,则可能无法再运行bash
程序)。
比如下面这个测试案例:
1 2 3 4 5 6 ➜ ~ telnet 10.11.33.115 33333 Trying 10.11.33.115... Connected to 10.11.33.115. Escape character is '^]' . bash-4.4 Connection closed by foreign host.
用户能成功连接到telnet
服务,服务的banner正常显示,但是当执行id
命令时,telnet
服务却断开了连接,按照上述的分析,猜测是bash
程序被修改的代码段位于bash
程序处理用户输入的命令的函数中,所以当用户想执行id
命令时,程序将会奔溃,导致telnet
服务断开连接。
如果修改的代码位于libc
的程序段,那将会造成更严重的后果,不仅是telnet
服务甚至是操作系统的其他服务,运行到该libc
的代码时,都会崩溃导致程序异常。
因为上述的原因,所以应该稍微修改一下思路,经过多次测试,发现最稳定,最不容易影响系统正常运行的思路如下:
在代码段搜索syscall
指令,比如:find /h upaddr,lowaddr,0x050F
。
然后把pc修改到该地址,set $pc=0xAAAAAA
。
PS: 如果不修改指令,按原来的思路做,只需要把命令改成telnetd -l /bin/sh
,用户连接到telnetd
服务,执行命令时,将不会出现异常导致连接断开。不过这种方法治标不治本,只作为应急使用。
一键操作 准备写个gdb插件,一句指令完成我上述的流程。
选择开发一个gef的插件,在开发前收集了一些资料。
首先是参数寄存器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes ────────────────────────────────────────────────────────────────── arm/OABI a1 a2 a3 a4 v1 v2 v3 arm/EABI r0 r1 r2 r3 r4 r5 r6 arm64 x0 x1 x2 x3 x4 x5 - blackfin R0 R1 R2 R3 R4 R5 - i386 ebx ecx edx esi edi ebp - ia64 out0 out1 out2 out3 out4 out5 - mips/o32 a0 a1 a2 a3 - - - See below mips/n32,64 a0 a1 a2 a3 a4 a5 - parisc r26 r25 r24 r23 r22 r21 - s390 r2 r3 r4 r5 r6 r7 - s390x r2 r3 r4 r5 r6 r7 - sparc/32 o0 o1 o2 o3 o4 o5 - sparc/64 o0 o1 o2 o3 o4 o5 - x86_64 rdi rsi rdx r10 r8 r9 - x32 rdi rsi rdx r10 r8 r9 -
然后是系统调用指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 arm/OABI swi NR - a1 NR is syscall # arm/EABI swi 0x0 r7 r0 arm64 svc #0 x8 x0 blackfin excpt 0x0 P0 R0 i386 int $0x80 eax eax. 0x80CD ia64 break 0x100000 r15 r8 See below mips syscall v0 v0 See below parisc ble 0x100(%sr2, %r0) r20 r28 s390 svc 0 r1 r2 See below s390x svc 0 r1 r2 See below sparc/32 t 0x10 g1 o0 sparc/64 t 0x6d g1 o0 x86_64 syscall rax rax See below 0x050F x32 syscall rax rax See below
然后收集了一些架构execve
的系统调用号:
1 2 3 4 5 6 7 8 9 10 11 execve: arm64/h8300/hexagon/ia64/m68k/nds32/nios2/openrisc/riscv32/riscv64/c6x/tile/tile64/unicore32/score/metag: 221 arm/i386/powerpc64/powerpc/s390x/s390/arc/csky/parisc/sh/xtensa/avr32/blackfin/cris/frv/sh64/mn10300/m32r: 11 armoabi: 9437195 x86_64/alpha/sparc/sparc64: 59 x32: 1073742344 mips64: 5057 mips64n32: 6057 mipso32: 4011 microblaze: 1033 xtensa: 117
最后得到如下所示的代码:
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 @register_command class ExecveCommand (GenericCommand ): """use execve do anything cmd""" _cmdline_ = "execve" _syntax_ = "{:s} [cmd]|set addr [address]" .format (_cmdline_) _example_ = "{:s} /usr/sbin/telnetd -l /bin/bash -p 23333\n{:s} set addr 0x7fb4360748ae" .format (_cmdline_) _aliases_ = ["exec" ,] def __init__ (self ): super ().__init__(complete=gdb.COMPLETE_FILENAME) self.findAddr = None return @only_if_gdb_running def do_invoke (self, argv ): ''' mips/arm todo ''' if len (argv) > 0 : if argv[0 ] == "debug" : dofunc = print argv = argv[1 :] elif argv[0 ] == "set" : if argv[1 ] == "addr" : self.findAddr = int (argv[2 ], 16 ) info("set success" ) return else : dofunc = gdb.execute else : err("The lack of argv." ) return cmd = " " .join(argv) cmd = [b"/bin/sh" , b"-c" , cmd.encode()] try : rsp = current_arch.sp nowpc = self.findAddr or current_arch.pc except gdb.error as e: err("%s Please start first." %e) return bit = current_arch.ptrsize if current_arch.arch == "X86" : arg0 = "$rdi" if bit == 8 else "$ebx" arg1 = "$rsi" if bit == 8 else "$ecx" arg2 = "$rdx" if bit == 8 else "$edx" sysreg = current_arch.syscall_register sysreg_value = 59 if bit == 8 else 11 syscall_instr = 0x050F if bit == 8 else 0x80CD else : err("%s can't implementation." % current_arch.arch) return spc = nowpc & (~0xFFF ) res = gdb.execute("find /h %s,%s,%s" %(spc, spc+0x10000 , syscall_instr), to_string=True ) if "patterns found." not in res: err("can't find syscall. Please break in libc." ) return newpc = res.splitlines()[0 ] endian_symbol = endian_str() endian = "little" if endian_symbol == "<" else "big" startaddr = rsp + 0x100 args_list = [] for cstr in cmd: args_list.append(startaddr) cstr += b"\x00" * (4 - (len (cstr) % 4 )) length = len (cstr) write_memory(startaddr, cstr, length) startaddr += length args_list.append(0 ) rsiAddr = rsp + 0x50 endian = "little" if endian_str() == "<" else "big" addrvalue = b"" for addr in args_list: addrvalue += addr.to_bytes(bit, endian) write_memory(rsiAddr, addrvalue, len (addrvalue)) dofunc("set %s=%s" %(arg0, hex (args_list[0 ]))) dofunc("set %s=%s" %(arg1, hex (rsp + 0x50 ))) dofunc("set %s=0" %arg2) dofunc("set %s=%s" %(sysreg, sysreg_value)) dofunc("set $pc=%s" %newpc) dofunc("c" ) return
总结 来实际试一试: