Loading [MathJax]/jax/input/TeX/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用 GDB 获取软路由的文件系统

使用 GDB 获取软路由的文件系统

作者头像
Seebug漏洞平台
发布于 2021-08-10 07:24:14
发布于 2021-08-10 07:24:14
1.2K00
代码可运行
举报
文章被收录于专栏:Seebug漏洞平台Seebug漏洞平台
运行总次数:0
代码可运行

作者: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系统):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,只需要执行下面的命令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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程序)。

比如下面这个测试案例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
?  ~ 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的插件,在开发前收集了一些资料。

首先是参数寄存器:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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    -

然后是系统调用指令:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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的系统调用号:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@register_commandclass 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

总 结

来实际试一试:

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Seebug漏洞平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Linux pwn入门学习到放弃
PWN是一个黑客语法的俚语词,自”own”这个字引申出来的,意为玩家在整个游戏对战中处在胜利的优势。本文记录菜鸟学习linux pwn入门的一些过程,详细介绍linux上的保护机制,分析一些常见漏洞如栈溢出,堆溢出,use after free等,以及一些常见工具介绍等。
FB客服
2020/09/22
4.1K0
Linux pwn入门学习到放弃
Kernel调试追踪技术之 Kprobe on ARM64
kprobe 是一种动态调试机制,用于debugging,动态跟踪,性能分析,动态修改内核行为等,2004年由IBM发布,是名为Dprobes工具集的底层实现机制[1][2],2005年合入Linux kernel。probe的含义是像一个探针,可以不修改分析对象源码的情况下,获取Kernel的运行时信息。
233333
2024/04/03
5740
Kernel调试追踪技术之 Kprobe on ARM64
汇编学习(1),汇编之helloworld
makefile可以这样看,目标hello依赖hello.o, 而hello.o又依赖hello.asm, 如果hello.asm的修改时间大于hello.o,那么hello.o下一行的命令就需要执行。
一只小虾米
2022/11/28
1K0
NX防护机制以及最基本shellcode
道理我们都懂,那么如果我们关闭了NX到底可以干什么呢,该如何利用呢?下面通过一个实验来说明。
FB客服
2023/02/10
1K0
NX防护机制以及最基本shellcode
动态调试elf文件的几种方法
最近在刷题的时候遇到了很多elf文件,虽然可以通过ida分析伪代码解出来,但是发现有些通过动态调试的方式可以直接找到flag,这样简单了不少,因为之前接触的linux下的逆向题目比较少,所以通过这次刷题也记录一下动态调试elf文件的几种方式。
鸿鹄实验室
2022/05/27
4.4K0
动态调试elf文件的几种方法
从一道 CTF 题看 SROP | PWN
SROP 学习过程中,很大一部分人写的 smallest 这道题的 writeup 让我感觉很疑惑,为了证明他们写的存在一定问题,被迫走上了 pwntools + gdb 调试的路,所以这次只能用视频来进行展示了,文章剩余部分是讲义的内容 也不知道因为啥,磨磨唧唧唠了近两个小时,在视频中,大家可以 get 以下内容: SROP 原理及利用 一道 CTF 题的解题方法 pwntools + gdb 如何进行调试 SROP 整个过程中栈的内容是如何变化的 一些偏执... 视频已经上传到 B 站了 https:/
意大利的猫
2022/03/29
1.1K0
Linux下Shellcode编写
基本过程是首先使用汇编通过系统调用的方式实现程序功能,编译成可执行文件,然后使用 objdump 进行机器码提取
yichen
2022/01/06
2.5K0
PWN|西湖论剑·2022中国杭州网络安全技能大赛初赛官方Write Up
2.审计代码,发现read函数处有末尾置零处理,导致1字节溢出NULL,可覆盖rbp低一字节。且可覆盖for循环的i,导致执行v3[i] = v0;时数组越界,可基于v3任意偏移写一字节。
安恒网络空间安全讲武堂
2023/03/21
6130
PWN|西湖论剑·2022中国杭州网络安全技能大赛初赛官方Write Up
SECCON 2017 baby_stack
不过呢,是go语言编写的,输入比较长的字符给message后可以看到报错中有.go文件
用户1423082
2024/12/31
630
第五届“强网杯”全国网络安全挑战赛(线上赛)
前言 呜呜呜~太难了,被暴打,最后还摸不到强网先锋(bushi,竟然ban了一堆人,成功混到? Misc BlueTeaming 附件(提取码:bfpa) Powershell scripts wer
MssnHarvey
2022/08/10
6310
第五届“强网杯”全国网络安全挑战赛(线上赛)
rust写操作系统 rCore tutorial 学习笔记:实验指导零 创建项目与启动
这是 os summer of code 2020 项目每日记录的一部分: 每日记录github地址(包含根据实验指导实现的每个阶段的代码):https://github.com/yunwei37/os-summer-of-code-daily
云微
2023/02/11
1.7K0
[转]现代Linux系统上的栈溢出攻击
这个教程试着向读者展示最基本的栈溢出攻击和现代Linux发行版中针对这种攻击的防御机制。为此我选择了最新版本的Ubuntu系统(12.10),因为它默认集成了几个安全防御机制,而且它也是一个非常流行的发行版。安装和使用都很方便。我们选择的系统是X86_64的。读者将会了解到栈溢出是怎样在那些默认没有安全防御机制的老系统上面成功的溢出的。而且还会解释在最新版本的Ubuntu上这些保护措施是如何工作的。我还会使用一个小例子来说明如果不阻止一个栈上面的数据结构被溢出那么程序的执行路径就会失去控制 。
墨文
2020/02/28
1.4K0
[pwnable.tw] unexploitable - 利用微偏移在read库函数中找syscall gadget
这题和pwnable.kr原题的差别在于程序本身没有了syscall这个gadget,需要另找别处。题目给了read和栈溢出,栈迁移是少不了的。考虑到GOT表可写,并且关于read的库实现有个可以利用的gadget:在read库函数起始位置+0xe的时候有一个syscall,并且只要返回值正常,后面会接上ret (重点!)。
赤道企鹅
2022/08/01
5470
Linux syscall过程分析(万字长文)
为了安全,Linux 中分为用户态和内核态两种运行状态。对于普通进程,平时都是运行在用户态下,仅拥有基本的运行能力。当进行一些敏感操作,比如说要打开文件(open)然后进行写入(write)、分配内存(malloc)时,就会切换到内核态。内核态进行相应的检查,如果通过了,则按照进程的要求执行相应的操作,分配相应的资源。这种机制被称为系统调用,用户态进程发起调用,切换到内核态,内核态完成,返回用户态继续执行,是用户态唯一主动切换到内核态的合法手段(exception 和 interrupt 是被动切换)。
秃头哥编程
2019/08/23
15.4K1
强网杯PWN WP
这次比赛总的来说体验还是很不错,baby_diary这个题虽然没有拿到前三血,但做题速度超过腾讯eee,星盟安全,长亭科技战队等。
i0gan
2021/06/26
2.1K0
[西湖论剑 2021] Pwn方向writeup - string_go, code_project
readv没有被禁用可以自覆盖解除alpha限制,writev访问非法内存会返回错误值但是不会报错导致中断,可以写一个loop从低到高遍历地址去读flag
赤道企鹅
2022/08/01
4130
Kokodayo-Wp
前几天无意间看到有师傅在询问最短的shellcode的长度是多少,询问之后发现是这个题目只允许写入0x11字节的shellcode,正好本人汇编水平非常的拉,所以想借这题练习练习。
h1J4cker
2022/12/01
3910
用Rust实现Brainfuck的JIT编译器
希望读者们都可以理解上述 C 代码的作用。但是,此代码在底层如何工作?我认为并非所有人都能回答这个问题,我也是。我可以用Haskell,Erlang,Go 等高级编程语言编写代码,但是在它们编译后我并不知道它在底层是如何工作的。因此,我决定采取一些更深入的步骤,进行记录,并描述我对此的学习过程。希望这个过程不仅仅只是对我来说很有趣。让我们开始吧。
端碗吹水
2022/06/06
1K0
Linux内核源码分析 - 系统调用 . 续
上一篇文章 Linux内核源码分析 - 系统调用 中分析了linux下的系统调用在kernel space层是如何实现的,现在我们来分析下user space层的实现。
KINGYT
2019/05/30
2.6K0
[TCTF/0CTF 2021 Final] Pwn 部分writeup
一个python语言子集的的JIT,可以生成x64汇编代码,题目给出了一个相对于原工程的diff文件,通过比对发现:题目版本删除了部分代码并在末尾通过mmap开辟一个可执行段,将汇编成二进制的机器码放到里面执行并取出返回值。
赤道企鹅
2022/08/01
5020
[TCTF/0CTF 2021 Final] Pwn 部分writeup
相关推荐
Linux pwn入门学习到放弃
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验