封装的线程注入类
来源:互联网 发布:巨人网络借壳标的猜想 编辑:程序博客网 时间:2024/05/20 04:47
不在江湖已许多年,今天偶然想起江湖事。以前有很多想法,最终由于种种原因都草草收尾。今天突然有了感觉,想起线程注入。奋笔疾书,写了一个不算太完美的类,完成了以前的一个夙愿,稍感欣慰。线程注入本人认为可以分成两步:第一步是代码和数据的注入,也可以看成是资源的注入;第二步是远程代码的唤醒。这个类主要是针对第一步做了封装。注入其他进程的代码要想成功运行,会受到不少限制,首先是所有的静态数据必须是事先注入好的,另外用到的函数也要是事先注入好的,还有就是API的入口地址问题。这些工作是十分繁杂的。我的这个类将这个工作封装起来,使得这一步更容易完成。至于第二步,方法很多,我这里只写了两种,一种是CreateRemoteThread,优点是直接了当,立竿见影,缺点是大部分杀毒软件都挂钩了这个API,如果要用就得像办法解决这个问题;另一种是QueueUserAPC,优点是大部分杀毒软件免杀(至少卡巴6是的),缺点是需要找到处于内核等待状态的线程。我这里选了Shell_TrayWnd。不废话了,下面看代码:
//Source File: InjectThread.h
#include <afxtempl.h>
//#define INJECT_TEST //本地注入测试
#define INJECT_APC //采用APC注入方式
#ifdef _DEBUG
#define INJECTFUNC_ATTRIBUTE //Debug版本不能静态定义
#define INJECTFUNC_END(funcName) //Debug版本无需结束标记
#else
#define INJECTFUNC_ATTRIBUTE static //Release版本,防止Jmp指令
#define INJECTFUNC_END(funcName) static LPVOID funcName = &funcName //Release版本,标记函数结束地址
#endif
#ifdef INJECT_APC //APC注入方式的线程入口函数定义
#define INJECTFUNC_MAINSTART(funcName, paraName) /
INJECTFUNC_ATTRIBUTE VOID APIENTRY funcName(ULONG_PTR paraName) //函数申明定义
#define INJECTFUNC_MAINRETURN(value) //返回语句定义
#else //默认注入方式的入口函数定义
#define INJECTFUNC_MAINSTART(funcName, paraName) /
INJECTFUNC_ATTRIBUTE DWORD WINAPI funcName(LPVOID paraName) //函数申明定义
#define INJECTFUNC_MAINRETURN(value) return(value) //返回语句定义
#endif
/***************************************************************************
* 功能 : 线程注入类
* 输入参数 : 无
* 输出参数 : 无
* 返回值 : 无
* 作者 : ***
***************************************************************************/
class CThreadInject //线程注入类
{
public:
typedef enum tagParaType //参数类型
{
ParaType_Var, //变量
ParaType_Func, //用户自定义函数
ParaType_SysFunc //系统函数
} EParaType;
typedef struct tagParaDataNode //参数信息节点
{
UINT32 iVarId; //Id
LPVOID pDataAddr; //本地地址
LPVOID pRemoteAddr; //运程地址
UINT32 iDataSize; //大小
EParaType eType; //类型
} SParaDataNode, *PSParaDataNode;
public:
CThreadInject(void); //构造
~CThreadInject(void); //析构
BOOL SetProcessId(DWORD dwProcessId); //设置进程Id
BOOL SetProcessIdByWindow(HWND hWnd); //根据窗口设置进程Id和线程Id
BOOL AddPara(UINT32 iVarId, LPVOID pVarAddr, UINT32 iDataSize
, EParaType paraType); //增加参数
BOOL AddSysFunc(UINT32 iFuncId, PCHAR pcFuncName); //增加系统函数
BOOL Inject(UINT32 iStartFuncId, BOOL bWaitBck); //注入线程
static UINT32 GetPara(UINT32 pDirAddr, UINT32 iVarId); //获取参数
protected:
VOID ReleasePara(); //释放本地资源
VOID ReleaseRemoteData(HANDLE hProcess); //释放远程资源
BOOL EnableDebugPriv(); //提升权限
BOOL InjectPara(HANDLE hProcess, EParaType paraType, LPVOID *ppRemoteAddr //注入参数
, UINT32 *pIRemoteSize);
BOOL InjectDir(HANDLE hProcess, LPVOID *ppRemoteAddr, UINT32 *pIRemoteSize);//注入参数表
UINT32 GetTotalParaSize(EParaType paraType); //获取某类参数大小总和
LPVOID GetStartFuncAddr(UINT32 iStartFuncId); //获取入口函数地址
CList<SParaDataNode> m_lstParaData; //参数链表
DWORD m_dwRemoteProcessId; //远程进程Id
DWORD m_dwRemoteThreadId; //远程线程Id
private:
LPVOID m_pRemoteVarAddr; //远程变量地址
LPVOID m_pRemoteFuncAddr; //远程函数地址
LPVOID m_pRemoteDirAddr; //远程参数表地址
UINT32 m_iRemoteVarSize; //远程变量大小
UINT32 m_iRemoteFuncSize; //远程函数大小
UINT32 m_iRemoteDirSize; //远程目录大小
};
typedef UINT32 (* Func_GetPara)(UINT32 pDirAddr, UINT32 iVarId); //获取参数函数原形
#define THREADINJECT_CREATE(pThis) CThreadInject *pThis = new CThreadInject() //创建线程注入类实例
#define THREADINJECT_RELEASE(pThis) delete (CThreadInject *)pThis; //释放线程注入类实例
#define THREADINJECT_ADDPARA(pThis, varId, varName, type) /
((CThreadInject *)pThis)->AddPara(varId, &varName, sizeof(type), /
CThreadInject::ParaType_Var) //增加参数
#define THREADINJECT_ADDSYSFUNC(pThis, funcId, funcName) /
((CThreadInject *)pThis)->AddSysFunc(funcId, #funcName) //增加系统函数
#ifdef _DEBUG
#define THREADINJECT_ADDFUNC(pThis, funcId, funcName, finishTag) /
((CThreadInject *)pThis)->AddPara(funcId, funcName, 0x8000, /
CThreadInject::ParaType_Func) //Debug版本需要保留附加信息
#else
#define THREADINJECT_ADDFUNC(pThis, funcId, funcName, endTag) /
((CThreadInject *)pThis)->AddPara(funcId, funcName, /
(UINT32)(INT64)endTag-(UINT32)(INT64)funcName, /
CThreadInject::ParaType_Func) //Release版本无需保留附加信息
#endif
#define THREADINJECT_SETPROCESS(pThis, processId) /
((CThreadInject *)pThis)->SetProcessId((DWORD)processId) //设置远程进程句柄
#define THREADINJECT_SETWINDOW(pThis, hWnd) /
((CThreadInject *)pThis)->SetProcessIdByWindow((HWND)hWnd) //设置远程进程和线程句柄
#define THREADINJECT_INJECT(pThis, startFuncId) /
((CThreadInject *)pThis)->Inject(startFuncId, FALSE) //远程注入线程
#define THREADINJECT_GETFUNC(pDirAddr, funcId) /
((*(Func_GetPara *)(&((PBYTE)pDirAddr)[8]))((UINT32)(INT64)pDirAddr,/
funcId)) //获取函数地址
#define THREADINJECT_GETPARA(pDirAddr, varId) /
THREADINJECT_GETFUNC(pDirAddr, varId) //获取参数
//Source File: InjectThread.cpp
#include "StdAfx.h"
#include "InjectThread.h"
CThreadInject::CThreadInject(void)
{
m_dwRemoteProcessId = 0;
m_dwRemoteThreadId = 0;
m_pRemoteVarAddr = NULL;
m_pRemoteFuncAddr = NULL;
m_pRemoteDirAddr = NULL;
m_iRemoteVarSize = 0;
m_iRemoteFuncSize = 0;
m_iRemoteDirSize = 0;
#ifdef _DEBUG
AddPara(0, &CThreadInject::GetPara, 0x4000, ParaType_Func); //Debug版本保留附加信息
#else
AddPara(0, &CThreadInject::GetPara, 0x110, ParaType_Func); //Release版本无需保存附加信息
#endif
}
CThreadInject::~CThreadInject(void)
{
ReleasePara();
}
BOOL CThreadInject::SetProcessId(DWORD dwProcessId)
{
if(dwProcessId == 0)
{
return FALSE;
}
m_dwRemoteProcessId = dwProcessId;
return TRUE;
}
BOOL CThreadInject::SetProcessIdByWindow(HWND hWnd)
{
DWORD dwProcessId;
m_dwRemoteThreadId = ::GetWindowThreadProcessId(hWnd, &dwProcessId);
return SetProcessId(dwProcessId);
}
BOOL CThreadInject::AddPara(UINT32 iVarId,
LPVOID pVarAddr,
UINT32 iDataSize,
EParaType paraType)
{
for(INT32 i=0; i<m_lstParaData.GetCount(); ++i)
{
if(m_lstParaData.GetAt(m_lstParaData.FindIndex(i)).iVarId == iVarId)
{
return FALSE;
}
}
SParaDataNode dataNode;
dataNode.iVarId = iVarId;
dataNode.pRemoteAddr = NULL;
dataNode.iDataSize = iDataSize;
dataNode.eType = paraType;
if(paraType == ParaType_Var)
{
dataNode.pDataAddr = new BYTE[iDataSize];
memcpy(dataNode.pDataAddr, pVarAddr, iDataSize);
}
else
{
dataNode.pDataAddr = pVarAddr;
}
m_lstParaData.AddTail(dataNode);
return TRUE;
}
BOOL CThreadInject::AddSysFunc(UINT32 iFuncId, PCHAR pcFuncName)
{
PTCHAR pcSysModuleName[] =
{
TEXT("USER32.dll"),
TEXT("Kernel32.dll"),
TEXT("GDI32.dll"),
TEXT("ntdll.dll"),
TEXT("OLE32.dll"),
TEXT("WS32_32.dll"),
TEXT("WSOCK32.dll")
};
LPVOID pFuncAddr = NULL;
for(UINT32 i=0; i< sizeof(pcSysModuleName)/sizeof(PTCHAR); ++i)
{
HINSTANCE hModule = LoadLibrary(pcSysModuleName[i]);
if(hModule!=NULL && (pFuncAddr=(LPVOID)GetProcAddress(hModule
, pcFuncName))!=NULL)
{
break; //从系统模块表中找到指定函数的入口地址
}
}
if(!pFuncAddr)
{
return FALSE;
}
return AddPara(iFuncId, pFuncAddr, 0, CThreadInject::ParaType_SysFunc);
}
BOOL CThreadInject::Inject(UINT32 iStartFuncId,
BOOL bWaitBck)
{
EnableDebugPriv();
#ifdef INJECT_TEST //本地注入测试仅将代码注入当前线程
m_dwRemoteProcessId = GetCurrentProcessId();
m_dwRemoteThreadId = GetCurrentThreadId();
#endif
LPVOID pStartFunc = NULL;
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwRemoteProcessId);
if(hProcess == NULL)
{
return FALSE;
}
if(!InjectPara(hProcess, ParaType_Var, &m_pRemoteVarAddr, &m_iRemoteVarSize)
|| !InjectPara(hProcess, ParaType_Func, &m_pRemoteFuncAddr, &m_iRemoteFuncSize))
{
ReleaseRemoteData(hProcess);
CloseHandle(hProcess);
return FALSE;
}
DWORD dwOldProtect;
if(m_pRemoteFuncAddr && !VirtualProtectEx(hProcess, m_pRemoteFuncAddr, m_iRemoteFuncSize
, PAGE_EXECUTE, &dwOldProtect))
{
ReleaseRemoteData(hProcess);
CloseHandle(hProcess);
return FALSE;
}
if((pStartFunc=GetStartFuncAddr(iStartFuncId)) == NULL)
{
ReleaseRemoteData(hProcess);
CloseHandle(hProcess);
return FALSE;
}
if(!InjectDir(hProcess, &m_pRemoteDirAddr, &m_iRemoteDirSize))
{
ReleaseRemoteData(hProcess);
CloseHandle(hProcess);
return FALSE;
}
#ifdef INJECT_APC
HANDLE hRemoteThread = OpenThread(THREAD_SET_CONTEXT, FALSE, m_dwRemoteThreadId);
#elif defined INJECT_TEST
HANDLE hRemoteThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pStartFunc
, m_pRemoteDirAddr, 0, NULL);
#else
HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)pStartFunc
, m_pRemoteDirAddr, 0, NULL);
#endif //INJECT_APC
if(hRemoteThread == INVALID_HANDLE_VALUE)
{
CloseHandle(hProcess);
return FALSE;
}
#ifdef INJECT_APC
if(!QueueUserAPC((PAPCFUNC)pStartFunc, hRemoteThread, (ULONG_PTR)m_pRemoteDirAddr))
{
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
return FALSE;
}
#else
if(bWaitBck)
{
::WaitForSingleObject(hRemoteThread, INFINITE);
ReleaseRemoteData(hProcess);
}
#endif //INJECT_APC
CloseHandle(hRemoteThread);
CloseHandle(hProcess);
return TRUE;
}
UINT32 CThreadInject::GetPara(UINT32 pDirAddr,
UINT32 iVarId)
{
UINT32 i, iDirLength;
UINT32 pResult = NULL;
PCHAR pRecord = &((PCHAR)(INT64)pDirAddr)[4];
((PCHAR)&iDirLength)[0] = ((PCHAR)(INT64)pDirAddr)[0];
((PCHAR)&iDirLength)[1] = ((PCHAR)(INT64)pDirAddr)[1];
((PCHAR)&iDirLength)[2] = ((PCHAR)(INT64)pDirAddr)[2];
((PCHAR)&iDirLength)[3] = ((PCHAR)(INT64)pDirAddr)[3];
for(i=0; i<iDirLength; ++i)
{
UINT32 iTempVarId;
((PCHAR)&iTempVarId)[0] = ((PCHAR)(INT64)pRecord)[0];
((PCHAR)&iTempVarId)[1] = ((PCHAR)(INT64)pRecord)[1];
((PCHAR)&iTempVarId)[2] = ((PCHAR)(INT64)pRecord)[2];
((PCHAR)&iTempVarId)[3] = ((PCHAR)(INT64)pRecord)[3];
if(iTempVarId == iVarId)
{
((PCHAR)&pResult)[0] = ((PCHAR)pRecord)[4];
((PCHAR)&pResult)[1] = ((PCHAR)pRecord)[5];
((PCHAR)&pResult)[2] = ((PCHAR)pRecord)[6];
((PCHAR)&pResult)[3] = ((PCHAR)pRecord)[7];
break;
}
pRecord += 8;
}
return pResult;
}
VOID CThreadInject::ReleasePara()
{
while(m_lstParaData.GetCount() > 0)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(0));
if(dataNode.eType==ParaType_Var && dataNode.pDataAddr)
{
delete [](LPVOID)(INT64)dataNode.pDataAddr;
dataNode.pDataAddr = NULL;
}
m_lstParaData.RemoveAt(m_lstParaData.FindIndex(0));
}
}
VOID CThreadInject::ReleaseRemoteData(HANDLE hProcess)
{
for(INT32 i=0; i<m_lstParaData.GetCount(); ++i)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(i));
if(dataNode.pRemoteAddr)
{
dataNode.pRemoteAddr = NULL;
}
}
if(m_pRemoteVarAddr)
{
VirtualFreeEx(hProcess, m_pRemoteVarAddr, m_iRemoteVarSize, MEM_RELEASE);
m_pRemoteVarAddr = NULL;
m_iRemoteVarSize = 0;
}
if(m_pRemoteFuncAddr)
{
VirtualFreeEx(hProcess, m_pRemoteFuncAddr, m_iRemoteFuncSize, MEM_RELEASE);
m_pRemoteFuncAddr = NULL;
m_iRemoteFuncSize = 0;
}
if(m_pRemoteDirAddr)
{
VirtualFreeEx(hProcess, m_pRemoteDirAddr, m_iRemoteDirSize, MEM_RELEASE);
m_pRemoteDirAddr = NULL;
m_iRemoteDirSize = 0;
}
}
BOOL CThreadInject::EnableDebugPriv()
{
HANDLE hToken;
LUID sedebugnameValue;
TOKEN_PRIVILEGES tkp;
if (!OpenProcessToken(GetCurrentProcess()
, TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &hToken))
{
return FALSE;
}
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &sedebugnameValue))
{
CloseHandle(hToken);
return FALSE;
}
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Luid = sedebugnameValue;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL))
{
CloseHandle(hToken);
return FALSE;
}
return TRUE;
}
BOOL CThreadInject::InjectDir(HANDLE hProcess,
LPVOID *ppRemoteAddr,
UINT32 *pIRemoteSize)
{
UINT32 iParaCount = (UINT32)m_lstParaData.GetCount();
*pIRemoteSize = (iParaCount<<3)+4;
*ppRemoteAddr = VirtualAllocEx(hProcess, NULL, *pIRemoteSize, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
LPVOID pRemoteTempAddr = *ppRemoteAddr;
if(!pRemoteTempAddr || !WriteProcessMemory(hProcess, pRemoteTempAddr, &iParaCount, 4, NULL))
{
return FALSE;
}
pRemoteTempAddr = &((PCHAR)pRemoteTempAddr)[4];
for(UINT32 i=0; i<iParaCount; ++i)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(i));
if(!WriteProcessMemory(hProcess, pRemoteTempAddr, &dataNode.iVarId, 4, NULL)
|| !WriteProcessMemory(hProcess, &((PBYTE)pRemoteTempAddr)[4]
#ifdef INJECT_TEST
, &dataNode.pDataAddr, 4, NULL))
#else
, (dataNode.eType==ParaType_SysFunc ? &dataNode.pDataAddr : &dataNode.pRemoteAddr), 4, NULL))
#endif
{
return FALSE;
}
pRemoteTempAddr = &((PCHAR)pRemoteTempAddr)[8];
}
return TRUE;
}
BOOL CThreadInject::InjectPara(HANDLE hProcess,
EParaType paraType,
LPVOID *ppRemoteAddr,
UINT32 *pIRemoteSize)
{
if((*pIRemoteSize=GetTotalParaSize(paraType)) == 0)
{
return TRUE;
}
if((*ppRemoteAddr=VirtualAllocEx(hProcess, NULL, *pIRemoteSize
, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)) == NULL)
{
return FALSE;
}
PBYTE pRemoteTempAddr = (PBYTE)*ppRemoteAddr;
for(INT32 i=0; i<m_lstParaData.GetCount(); ++i)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(i));
if(dataNode.eType == paraType)
{
if(!WriteProcessMemory(hProcess, pRemoteTempAddr, dataNode.pDataAddr
, dataNode.iDataSize, NULL))
{
return FALSE;
}
dataNode.pRemoteAddr = pRemoteTempAddr;
pRemoteTempAddr += dataNode.iDataSize;
}
}
return TRUE;
}
UINT32 CThreadInject::GetTotalParaSize(EParaType paraType)
{
UINT32 iTotalSize = 0;
for(INT32 i=0; i<m_lstParaData.GetCount(); ++i)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(i));
if(dataNode.eType == paraType)
{
iTotalSize += dataNode.iDataSize;
}
}
return iTotalSize;
}
LPVOID CThreadInject::GetStartFuncAddr(UINT32 iStartFuncId)
{
for(INT32 i=0; i<m_lstParaData.GetCount(); ++i)
{
SParaDataNode &dataNode = m_lstParaData.GetAt(m_lstParaData.FindIndex(i));
if(dataNode.iVarId == iStartFuncId)
{
#ifdef INJECT_TEST
return dataNode.pDataAddr;
#else
return dataNode.pRemoteAddr;
#endif
}
}
return NULL;
}
//Source File: Test Code
#define VARID_CAPTION 0x00000001
#define VARID_TITLE 0x00000002
#define FUNID_SHOWMESSAGE 0x10000001
#define FUNID_SUBWINDOWTHREAD 0x10000002
#define SYSID_MESSAGEW 0x20000001
typedef int (WINAPI *Func_MessageBoxW)(IN HWND hWnd,
IN LPCWSTR lpText,
IN LPCWSTR lpCaption,
IN UINT uType);
typedef VOID (*Func_ShowMessage)(LPVOID pDirAddr);
INJECTFUNC_MAINSTART(SubWindowThread, lpThreadParameter)
{
Func_ShowMessage pShowMessage = (Func_ShowMessage)(INT64)THREADINJECT_GETFUNC(lpThreadParameter, FUNID_SHOWMESSAGE);
pShowMessage((LPVOID)lpThreadParameter);
INJECTFUNC_MAINRETURN(0);
}
INJECTFUNC_ATTRIBUTE VOID ShowMessage(LPVOID pDirAddr)
{
Func_MessageBoxW pMessageBox = (Func_MessageBoxW)(INT64)THREADINJECT_GETFUNC(pDirAddr, SYSID_MESSAGEW);
LPCWSTR pTitle = (LPCWSTR)(INT64)THREADINJECT_GETPARA(pDirAddr, VARID_TITLE);
LPCWSTR pCaption = (LPCWSTR)(INT64)THREADINJECT_GETPARA(pDirAddr, VARID_CAPTION);
pMessageBox(NULL, pTitle, pCaption, MB_OK);
}
INJECTFUNC_END(EndTag);
TCHAR Caption[20] = TEXT("Caption");
TCHAR Title[20] = TEXT("Title");
THREADINJECT_CREATE(pInject);
THREADINJECT_ADDPARA(pInject, VARID_CAPTION, Caption, TCHAR[20]);
THREADINJECT_ADDPARA(pInject, VARID_TITLE, Title, TCHAR[20]);
THREADINJECT_ADDFUNC(pInject, FUNID_SUBWINDOWTHREAD, SubWindowThread, ShowMessage);
THREADINJECT_ADDFUNC(pInject, FUNID_SHOWMESSAGE, ShowMessage, EndTag);
THREADINJECT_ADDSYSFUNC(pInject, SYSID_MESSAGEW, MessageBoxW);
HWND hWnd = ::FindWindow(TEXT("Shell_TrayWnd"), NULL);
THREADINJECT_SETWINDOW(pInject, hWnd);
THREADINJECT_INJECT(pInject, FUNID_SUBWINDOWTHREAD);
THREADINJECT_RELEASE(pInject);
上面的代码将用APC把相关数据和代码注入任务栏,等待若干秒后,会看到线程执行。如果将InjectThread.h里的#define INJECT_APC一句注释掉,则采用CreateRemoteThread远程唤醒代码。有兴趣的仔细看下测试代码,可见这个封装确实可以简化不少操作,呵呵。
- 封装的线程注入类
- 封装线程类的方法
- 线程控制 - AfxBeginThreadUI线程的封装类
- Struts2的自动封装注入
- 一个远程线程注入的类
- 线程的远程注入
- 线程的远程注入
- 线程的远程注入
- 线程的远程注入
- 线程的远程注入
- 线程的远程注入
- c++类和类的封装,对象线程封装
- 封装远程注入类CreateRemoteThreadEx
- Linux下的线程类封装
- 一个封装好的线程类
- 封装一个简单的windows线程类
- C++封装一个简单的线程类
- 面向对象风格的线程类封装
- 从硬盘启动引导ISO映像物理安装 Ubuntu的全过程
- 浅析Windows NT/2000环境切换
- 用自删除dll实现应用程序的安装/卸载代码
- 跨进程API Hook
- 堆栈执行代码格式
- 封装的线程注入类
- WeGo(新浪微博 for Windows Mobile)开发日志(更新至20101016)
- [推荐]房地产信息化---一个房地产CRM实践者的思考 穆利堂-monvo1
- 没分啊???
- [推荐]房地产软件信息化——透视房地产CRM应用模式 穆利堂-movno1
- 房地产软件信息化——房地产企业,对什么样的CRM说不?穆利堂-movno1
- dbcp
- dbcp
- eAccelerator 配置参数详解