作者: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的代碼時,都會崩潰導致程序異常。
因為上述的原因,所以應該稍微修改一下思路,經過多次測試,發現最穩定,最不容易影響系統正常運行的思路如下:
- 在代碼段搜索
syscall指令,比如:find /h upaddr,lowaddr,0x050F。 - 然后把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
總結
來實際試一試:

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