我先说说我对它的认识过程(我是个大菜鸟,高手莫笑话偶啊):
OD载入anti-od1.1,结果还没到入口点OD就被FUCK了,猜想是输入表的问题。
拿loadpe打开,输入表查看,点L看到ntdll后面很多换行(兴奋)。
Name: 0x000040A8 ("ntdll
点H打开40A8处看一下,后面一大堆0D0A,0D0A是回车换行的意思,过一大堆0D0A后有00结束。到这里我就简单的以为是系统认识0D0A,OD不认识造成的错误。然后就放一边了。今天看到有人发个分析的帖子,给出了解决方法结果shoooo说只治标,到这里我想到我以前的想法错了。
看来不动手光靠猜想是不行的。
loadpe再次打开,我把0D0A全改成3333就是ntdl333333333333。。。结果程序一样运行,看来这个输入表项系统填充IAT的时候没用上,明明在输入表里,系统怎么没去填充呢?只好点ntdll这个输入表项仔细看了,点H从OriginalFirstThunk,FirstThunk依次看。最后发现问题在FirstThunk,只要FirstThunk指向的IAT结构为空,这个表项就不填充。我把409C处的值取值随便改成非0,饭岛爱就运行不起来了(除非ntdll,ZwSetInformationProcess改成00结束)。我又试了Kernel32.dllFirstThunk指向的IAT结构后面的值,只要第一个不为0,整个IAT都能正常填充。
到这里问题就明朗了,shoooo利用IAT结构构造了一个没用的输入表项,只要IAT结构为空,这个输入表项的名字和函数可以随便取,反正运行的时候系统不管它。但是我们的OD就不行了,它会读这个乱起的名字,如果恶意构造个名字,后果是很严重的。
OK我们动手补丁下我们的OD让它先判断IAT结构,为空就不管这项读下个表项。
罗嗦了半天,呵呵。复制一份OD,用复制的OD打开目标OD程序,F9,目标程序载入antiod,
004A4EE5 3A5B 17 cmp bl,byte ptr ds:[ebx+17]处出错
在4A4ED8处下断重新载入程序,再次载入antiod记下异常时的最后一次断下的返回值,45DAED
看到这个函数太长了,我贴下主要代码
0045D0E6 > /6A 03 push 3
0045D0E8 . |6A 14 push 14
0045D0EA . |8B45 CC mov eax, dword ptr [ebp-34]
0045D0ED . |0345 BC add eax, dword ptr [ebp-44]
0045D0F0 . |0345 FC add eax, dword ptr [ebp-4]
0045D0F3 . |50 push eax
0045D0F4 . |8D95 30F9FFFF lea edx, dword ptr [ebp-6D0]
0045D0FA . |52 push edx
0045D0FB . |E8 0C420000 call _Readmemory ////此处为读一项IMAGE_IMPORT_DESCRIPTOR
0045D100 . |83C4 10 add esp, 10
0045D103 . |85C0 test eax, eax
0045D105 . |75 0C jnz short 0045D113
0045D107 . |C745 F4 02000>mov dword ptr [ebp-C], 2
0045D10E . |E9 1C050000 jmp 0045D62F
0045D113 > |83BD 3CF9FFFF>cmp dword ptr [ebp-6C4], 0
0045D11A . |0F84 0F050000 je 0045D62F
0045D120 . |6A 03 push 3
0045D122 . |68 04020000 push 204
0045D127 . |8B4D CC mov ecx, dword ptr [ebp-34]
0045D12A . |038D 3CF9FFFF add ecx, dword ptr [ebp-6C4]
0045D130 . |51 push ecx ///NameRVA
0045D131 . |8D85 78FDFFFF lea eax, dword ptr [ebp-288]
0045D137 . |50 push eax
0045D138 . |E8 CF410000 call _Readmemory //读Name项
0045D13D . |83C4 10 add esp, 10
0045D140 . |85C0 test eax, eax
0045D142 . |75 4E jnz short 0045D192
我们需要在读完IMAGE_IMPORT_DESCRIPTOR还没有读Name的时候patch判断FirstThunk
就在0045D11A 处补丁。我开始在代码段找了一块0串,结果改了不让保存,郁闷!我就自己加了一个区段VA:1AA000
45D11A处改为
0045D11A .- E9 E1CE1400 jmp 005AA000
0045D11F 90 nop
5AA000处代码写:
005AA000 - 0F84 2936EBFF je 0045D62F
005AA006 60 pushad
005AA007 6A 00 push 0
005AA009 6A 04 push 4
005AA00B 8B4D CC mov ecx, dword ptr [ebp-34]
005AA00E 038D 40F9FFFF add ecx, dword ptr [ebp-6C0]
005AA014 68 50A05A00 push 005AA050
005AA019 51 push ecx /////FirstThunk的值409C
005AA01A A1 685A4D00 mov eax, dword ptr [4D5A68]
005AA01F 50 push eax
005AA020 E8 4951F0FF call <jmp.&KERNEL32.ReadProcessMemory>
005AA025 A1 50A05A00 mov eax, dword ptr [5AA050]
005AA02A 85C0 test eax, eax
005AA02C 61 popad
005AA02D - 0F85 ED30EBFF jnz 0045D120 ///不为0继续执行
005AA033 - E9 DC35EBFF jmp 0045D614 ///为0已读大小加上,再读下一条IMAGE_IMPORT_DESCRIPTOR
005AA038 90 nop
然后保存。用这个修改的OllyICE.exe去打开antiod试试没问题,加载别的程序也没出错。
加点OD对这个anti溢出的说明:
有个朋友发帖分析了,我只稍微提下
问题在这个格式化函数处,它把目标格式化成USER32.MessageBoxA的形式。用格式化成USER32.MessageBoxA作例子说明。
0045D51D . B9 00010000 mov ecx, 100 //////作者的期望是格式化的结果在256个字节以内。
0045D522 . 2BC8 sub ecx, eax ///先减去USER32的长度,6个大小
0045D524 . 8D85 78FCFFFF lea eax, dword ptr [ebp-388]
0045D52A . 83E9 02 sub ecx, 2 //再减去'.'+结束00 2个大小
0045D52D . 51 push ecx ///ecx就是剩余空间大小
0045D52E . 50 push eax
0045D52F . 68 92C04B00 push 004BC092 ; ASCII "%s.%.*s"
0045D534 . 52 push edx /////USER32 这里实际大小只读了255个字节,既DLL名如果大于255个字节,256位写00
0045D535 . E8 F2960400 call 004A6C2C
问题就在这里了,如果USER32名字大于等于255,ecx值将为-1,我们看看它的内部处理
004A6C2C /$ 55 push ebp
004A6C2D |. 8BEC mov ebp, esp
004A6C2F |. 8B45 08 mov eax, dword ptr [ebp+8]
004A6C32 |. 8D4D 08 lea ecx, dword ptr [ebp+8]
004A6C35 |. C600 00 mov byte ptr [eax], 0
004A6C38 |. 8D45 10 lea eax, dword ptr [ebp+10]
004A6C3B |. 50 push eax
004A6C3C |. 6A 00 push 0
004A6C3E |. 6A 00 push 0
004A6C40 |. 8B55 0C mov edx, dword ptr [ebp+C]
004A6C43 |. 52 push edx
004A6C44 |. 51 push ecx
004A6C45 |. 68 046C4A00 push 004A6C04
004A6C4A |. E8 6D020000 call 004A6EBC
004A6C4F |. 83C4 18 add esp, 18
004A6C52 |. 5D pop ebp
格式化都在4A6EBC,这个函数太长了,只贴关键点说明下。
用一个临时变量,我们用mark标记,用IDA的标记功能比较爽,这里还贴OD的吧。
根据参数; ASCII "%s.%.*s"给mark赋值
走USER32时mark = -1;
004A6F5B |. 33D2 |xor edx, edx
004A6F5D |. 8955 F0 |mov dword ptr [ebp-10], edx
004A6F60 |. 83CA FF |or edx, FFFFFFFF
004A6F63 |. C645 F7 00 |mov byte ptr [ebp-9], 0
004A6F67 |. 8955 F8 |mov dword ptr [ebp-8], edx ///mark
走MessageBoxA时mark值为传入的参数ecx剩余空间大小, 中间有个处理参数为0 null出去的;
关键在这里了:
004A74A8 |> \837D F8 00 |cmp dword ptr [ebp-8], 0
004A74AC |. 7C 05 |jl short 004A74B3
004A74AE |. 8B45 F8 |mov eax, dword ptr [ebp-8]
004A74B1 |. EB 05 |jmp short 004A74B8
004A74B3 |> B8 FFFFFF7F |mov eax, 7FFFFFFF
004A74B8 |> 8B55 E8 |mov edx, dword ptr [ebp-18]
004A74BB |. 33C9 |xor ecx, ecx
004A74BD |. 894D C0 |mov dword ptr [ebp-40], ecx
004A74C0 |. EB 05 |jmp short 004A74C7
004A74C2 |> 48 |/dec eax
004A74C3 |. FF45 C0 ||inc dword ptr [ebp-40]
004A74C6 |. 42 ||inc edx
004A74C7 |> 85C0 | test eax, eax
004A74C9 |. 0F84 CF000000 ||je 004A759E
004A74CF |. 803A 00 ||cmp byte ptr [edx], 0
004A74D2 |.^ 75 EE |\jnz short 004A74C2
004A74D4 |. E9 C5000000 |jmp 004A759E
///mark < 0 就记下dword ptr [ebp-18]字符串的长度保存在dword ptr [ebp-40](size)中,后面用这个长度格式化,不小于0取mark值和dword ptr [ebp-18]字符串的长度的最小值。
如果DLL名字大于等于255字符,分配的栈空间是按格式化256字符长分配的, 函数名超长的再格式化自然溢出了。如果DLL名字小于255字节,.后的函数名取前面字节填充256字节剩余空间。 这样顶多出现函数名不全的情况,不会溢出。
window DLL文件名好象只能取到240多位(我随便重命名了一个文件,数了下)。 保证程序能运行的情况下用我上面的patch方法不会出现溢出的情况
实际上这样patch以后也只能保证加载的时候不溢出. 如果程序在运行的时候把IMAGE_IMPORT_DESCRIPTOR改的面目全非,程序运行的时候IAT填充完了,不影响程序运行.OD这时附加即使不崩溃也读的内容乱七八糟. 读输入表0045D0FB . |E8 0C420000 call _Readmemory ////此处为读一项IMAGE_IMPORT_DESCRIPTOR的这句所在函数过程需要说明下,它的作用如下
在OD的CPU窗口里call dword ptr [402198] ////显示成00401770 call dword ptr [<&MSVCRT.__p__commode>; 针对它的解决方法是NOP掉读原始输入表的过程,但是这样似乎不太好. 我的方法就是用海风的插件,哈哈!!! 海风的插件真是好东西,支持海风!!!!!!
附上我修改的OllyICE.exe
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)