作者:Hcamael@知道創宇404實驗室
時間:2021年8月6日

最近在研究某款軟路由,能在其官網下載到其軟路由的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系統):

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,只需要執行下面的命令:

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程序)。

比如下面這個測試案例:

?  ~ telnet 10.11.33.115 33333
Trying 10.11.33.115...
Connected to 10.11.33.115.
Escape character is '^]'.
bash-4.4# id
Connection closed by foreign host.

用戶能成功連接到telnet服務,服務的banner正常顯示,但是當執行id命令時,telnet服務卻斷開了連接,按照上述的分析,猜測是bash程序被修改的代碼段位于bash程序處理用戶輸入的命令的函數中,所以當用戶想執行id命令時,程序將會奔潰,導致telnet服務斷開連接。

如果修改的代碼位于libc的程序段,那將會造成更嚴重的后果,不僅是telnet服務甚至是操作系統的其他服務,運行到該libc的代碼時,都會崩潰導致程序異常。

因為上述的原因,所以應該稍微修改一下思路,經過多次測試,發現最穩定,最不容易影響系統正常運行的思路如下:

  1. 在代碼段搜索syscall指令,比如:find /h upaddr,lowaddr,0x050F
  2. 然后把pc修改到該地址,set $pc=0xAAAAAA

PS: 如果不修改指令,按原來的思路做,只需要把命令改成telnetd -l /bin/sh,用戶連接到telnetd服務,執行命令時,將不會出現異常導致連接斷開。不過這種方法治標不治本,只作為應急使用。

一鍵操作

準備寫個gdb插件,一句指令完成我上述的流程。

選擇開發一個gef的插件,在開發前收集了一些資料。

首先是參數寄存器:

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    -

然后是系統調用指令:

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的系統調用號:

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

最后得到如下所示的代碼:

@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":
                # debug = 1
                dofunc = print
                argv = argv[1:]
            elif argv[0] == "set":
                if argv[1] == "addr":
                    self.findAddr = int(argv[2], 16)
                    info("set success")
                return
            else:
                # debug = 0
                dofunc = gdb.execute
        else:
            err("The lack of argv.")
            return
        cmd = " ".join(argv)
        cmd = [b"/bin/sh", b"-c", cmd.encode()]
        # print(current_arch.sp)
        # print(current_arch.pc)
        # print(current_arch.ptrsize)
        # print(endian_str())
        # print(current_arch.syscall_instructions)
        # print(current_arch.syscall_register)
        # print(current_arch.special_registers)
        # print(current_arch.function_parameters)
        # print(current_arch.arch)
        # print(current_arch.get_ith_parameter)
        # print(current_arch.gpr_registers)
        # print(current_arch.get_ra)
        # write_memory
        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 = []
        # cmd write to stack
        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
            # for i in range(0, len(cstr), 4):
            #     t = hex(struct.unpack(endian_symbol+'I', cstr[i:i+4])[0])
            #     dofunc("set  *(%s)=%s"%(hex(startaddr), t))
                # startaddr += 4
        args_list.append(0)
        # set cmd point (rsi)
        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))
            # for i in range(0, len(addr), 4):
            #     t = hex(struct.unpack(endian_symbol+'I', addr[i:i+4])[0])
            #     dofunc("set  *(%s+%d)=%s"%(hex(rsiAddr), i, t))
            # rsiAddr += bit

        # set first arguments.
        dofunc("set %s=%s"%(arg0, hex(args_list[0])))
        # set second arguments
        dofunc("set %s=%s"%(arg1, hex(rsp + 0x50)))
        # set third arguments
        dofunc("set %s=0"%arg2)
        # set syscall register
        dofunc("set %s=%s"%(sysreg, sysreg_value))
        # set $pc=$sp
        dofunc("set $pc=%s"%newpc)
        # set *$pc
        # dofunc("set *(int *)$pc=%s"%hex(syscall_instr))
        # show context
        # dofunc("context")
        # continue
        dofunc("c")
        return

總結

來實際試一試:


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/1660/