BR TH 11/20
VS终结者原理简要说明:
VS检测作弊肯定不会放在主线程,既然是另开线程,那就好办的多,用procexp等工具查看一下VS的线程信息(用HideToolz隐藏,不然会被关闭),通过观察找到入手点,找到线程启动地址以后,分析就好办的多了...
Sleep掉VS反作弊模块的线程,在线程中间函数(VS 3.12 地址:0x0045f640 VS3.0 地址:0x0045f782)处HOOK,判断传入地址,如果为检测反作弊的函数地址则 while(1) Sleep(10000);,作用一看就明白了,线程不能retn ,不然VS会退出...
至于哪些函数是检测作弊的,这个也容易找到,搜索字符串“作弊”,搜索ReadProcessMemory,还有因为通过线程中间函数启动的只有12个线程,先HOOK一下,得到这12个函数入口地址,缩小范围在这12个函数里面找...
还有说明一下:有一个线程不能简单Sleep,不然启动魔兽1分钟左右就会掉线,所以只能在这个函数中nop掉调用检测函数的代码...
=======================================================================
VS 3.12
0040139D
004016DB
004030F8
00401807
004031A2
004047C3
004039C2
上面7个函数Sleep掉
入口地址为0404B3D的函数,在0045B4D6处NOP五个字节
=======================================================================
VS3.0
004016CC
004017F3
00401389
00403125
004047DC
004031CF
004039E5
上面7个函数Sleep掉
入口地址为00404B5B的函数,在0045B618出NOP五个字节
=======================================================================
把原理交待了我就可以安心潜水了,上面虽然是简要说明,但是只要是研究过VS反作弊的朋友一眼就能看懂,只要VS小规模更新,稍微改下代码就行了...
只是希望继续破解之路的朋友能免费下去,也不多说了,每个人的出发点不一样毕竟...
顺便说一下,我喜欢用OD直接在内存上写汇编代码(OD的汇编功能非常强大),写好后再拷贝出16进制数值,用程序WriteProcessMemory写进去,这样方便感觉很舒服... 顺便贴一下我写的汇编代码,很简单的几行...
以VS3.12为例:
0045F640 /. 55 push ebp |
入口处JMP走 改为:
0045F640 ^\E9 B3FCFFFF jmp 0045F2F8 0045F2F6 CC int3 |
Code StuCE 10/08
关于aobscan的实现原理
我发现这里好多人用的语言都不一样,所以我尽量就不针对某种语言来讲了。(我写的C代码)
首先,获得进程PID。
可以根据标题获得,也可以根据进程名获得。各有各的好处吧。还有窗口类名哈。
例如:
HWND hwnd = FindWindow("MainWindow", NULL);
if (hwnd)
{
DWORD dwPid;
GetWindowThreadProcessId(hwnd, &dwPid);
//return dwPid;
}
第二步,获得进程相关信息,例如内存大小。
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
PROCESS_MEMORY_COUNTERS pmc;
pmc.cb = sizeof(PROCESS_MEMORY_COUNTERS);
::GetProcessMemoryInfo( hProcess, &pmc, sizeof(pmc));
//printf("%d\n%d\n",dwPid,pmc.WorkingSetSize/1024/1024);
第三步,遍历程序内存。
因为上一步已经获得内存大小了,所以一个循环。
(获得内存数据之前,是否检查一下内存属性呢?我觉得应该需要,但是我这里只讲讲原理,就省掉了)
另外据说Windows一个内存页就是4kb,所以写成这样:
char key[] = {0x80, 0x7f, 0x49, 0x00};//查找的数据以及长度
int len = 4;
PBYTE pAddress = NULL;
int i = 0;
int n = 0;
for (; i < pmc.WorkingSetSize; i += 4096)
{
BYTE arBytes[4096];
if (!::ReadProcessMemory(hProcess, (LPVOID)i, arBytes, 4096, NULL)) continue;
{
if (memstr(key, len, (char*)arBytes, 4096) != 0)
{
printf("%d %d\n", memstr(key, len, (char*)arBytes, 4096), arBytes);
n = memstr(key, len, (char*)arBytes, 4096) - (unsigned char*)arBytes;
break;
}
}
}
i += n;
printf("%x %d", i, n);
现在i变量中的地址,就是我们查找的数据的地址了。
附带一个最简单的menstr函数,几乎没有优化哈。
unsigned char *memstr(char * dst , int dst_len, char *src , int src_len )
{
int i;
char *cp = src;
if (src_len < dst_len)
{
return NULL;
}
for (i = 0; i <= src_len - dst_len; i++)
{
if (memcmp(cp , dst , dst_len) == 0)
{
return (unsigned char *)cp;
}
cp++;
}
return NULL;
}
Code CE 10/08
C++实现的完整aobscan,速度很快,貌似有点Bug,需要修正。
完整的aobscan实现,我在搜索部分使用了一个高速搜索算法。
#include
#include
#include
/*这是一个很低效的算法*/
unsigned char *memstr(char * dst , int dst_len, char *src , int src_len )
{
int i;
char *cp = src;
if (src_len < dst_len)
{
return NULL;
}
for (i = 0; i <= src_len - dst_len; i++)
{
if (memcmp(cp , dst , dst_len) == 0)
{
return (unsigned char *)cp;
}
cp++;
}
return NULL;
}
/*sunday算法*/
#define MAX_CHAR_SIZE 257
long *setCharStep(const unsigned char *subStr, long subStrLen)
{
long i;
static long charStep[MAX_CHAR_SIZE];
for (i = 0; i < MAX_CHAR_SIZE; i++)
charStep[i] = subStrLen + 1;
for (i = 0; i < subStrLen; i++)
{
charStep[(unsigned char)subStr[i]] = subStrLen - i;
}
return charStep;
}
/*
算法核心思想,从左向右匹配,遇到不匹配的看大串中匹配范围之外的右侧第一个字符在小串中的最右位置
根据事先计算好的移动步长移动大串指针,直到匹配
*/
long sundaySearch(const unsigned char *mainStr, const unsigned char *subStr, long *charStep, long mainStrLen, int subStrLen)
{
long main_i = 0;
long sub_j = 0;
while (main_i < mainStrLen)
{
//保存大串每次开始匹配的起始位置,便于移动指针
long tem = main_i;
while (sub_j < subStrLen)
{
if (mainStr[main_i] == subStr[sub_j])
{
main_i++;
sub_j++;
continue;
}
else
{
//如果匹配范围外已经找不到右侧第一个字符,则匹配失败
if (tem + subStrLen > mainStrLen)
return -1;
//否则 移动步长 重新匹配
unsigned char firstRightChar = mainStr[tem + subStrLen];
main_i += charStep[(unsigned char)firstRightChar];
sub_j = 0;
break; //退出本次失败匹配 重新一轮匹配
}
}
if (sub_j == subStrLen)
return main_i - subStrLen;
}
return -1;
}
unsigned char getHex(unsigned char hex)
{
if (hex >= '0' && hex <= '9') return hex - '0';
if (hex >= 'A' && hex <= 'F') return hex - 'A' + 10;
if (hex >= 'a' && hex <= 'f') return hex - 'a' + 10;
return 0;
}
int GetHexValue(char *src)
{
int i, j, flag;
static char temp[1024];
for (i = 0, j = 0; src[i] != 0; i++)
{
if ((src[i] <= 'F' && src[i] >= 'A') || (src[i] <= 'f' && src[i] >= 'a') || (src[i] <= '9' && src[i] >= '0'))
{
if (src[i] != ' ')
{
temp[j++] = src[i];
}
}
}
temp[j] = 0;
src[0] = 0;
for (i = 0, j = 0, flag = 1; temp[i] != 0; i++)
{
char ch = getHex(temp[i]);
if (ch != -1)
{
if (flag == 1) src[j] = ch << 4;
else src[j++] += ch;
flag *= -1;
}
}
src[j] = 0;
return j;
}
DWORD ReadPage(HANDLE m_hProcess, DWORD dwBaseAddr, char* Value)
{
//读取1页内存
BYTE arBytes[4096];
if (!::ReadProcessMemory(m_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
{
//此页不可读
return (DWORD) - 1;
}
else
{
//
//unsigned char key[] = {0x80, 0x7f, 0x49, 0x00};
unsigned char Value2[1024];
strcpy((char*)Value2, Value);
int len = GetHexValue((char*)Value2);
//getchar();
//注释这两行是低效的算法
//char key[] = {0x80, 0x7f, 0x49, 0x00};
//int len = 4;
//if (memstr(key, len, (char*)arBytes, 4096) != 0) return memstr(key, len, (char*)arBytes, 4096) - (unsigned char*)arBytes;
//else return -1;
//开始sunday算法
long *charStep = setCharStep(Value2, len);
return sundaySearch(arBytes, Value2, charStep, 4096, len);
}
return (DWORD) - 1; //不会执行到此处
}
DWORD aobscan(DWORD dwPid, char* Value)
{
if (dwPid == 0) return (DWORD) - 1;
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess != NULL)
{
//获得内存大小
PROCESS_MEMORY_COUNTERS pmc;
pmc.cb = sizeof(PROCESS_MEMORY_COUNTERS);
::GetProcessMemoryInfo( hProcess, &pmc, sizeof(pmc));
//遍历内存
for (int i = 0; i < pmc.WorkingSetSize; i += 4096)
{
DWORD dwValue = ReadPage(hProcess, i, Value);
if (dwValue != -1)
{
printf("Found:0x%X\n", i + dwValue);
return i + dwValue;
}
}
::CloseHandle(hProcess);
}
printf("Nothing Found!\n");
return (DWORD) - 1;
}
DWORD GetGameID()
{
HWND hwnd = FindWindow("MainWindow", NULL);
if (hwnd)
{
DWORD dwPid;
GetWindowThreadProcessId(hwnd, &dwPid);
return dwPid;
}
return 0;
}
int main()
{
double begin = clock();
aobscan(GetGameID(), "807f49 00");
printf("time:%.0fms", clock() - begin);
getchar();
}
Code CE 10/08
VB实现CE的AobScan功能,仿CE字节组内存搜索.
判断当前游戏是否为全屏状态 6/18
方法一:
最简单的方法,判断当前窗口大小是否跟屏幕分辨率相同,相同则为全屏状态。不过要注意桌面进程 Explorer 的处理
方法二:
注册一个Appbar(桌面工具栏)是类似微软视窗系统的任务条的窗口。它紧靠屏幕边缘,典型的桌面工具栏包括快速访问其他应用程序和窗口的按钮。系统会防止其他应用程序使用被appbar占用的区域。在任何时刻桌面都可以同时共存多个appbar。
使用的API: SHAppBarMessage (原型如下:)
WINSHELLAPI UINT APIENTRY SHAppBarMessage( DWORD dwMessage, PAPPBARDATA pData);
这个API可以向系统发送一个appbar message(也就是dwMessage,有很多消息,可以查阅MSDN),然后系统通过pData返回你想知道的信息,这里我们主要用这个API来注册一个新的appbar。这里还需要关注的是APPBARDATA这个结构体。
检测全屏的具体实现代码如下:
APPBARDATA abd;
memset(&abd, 0, sizeof(abd));
// Specify the structure size and handle to the appbar.
abd.cbSize = sizeof(APPBARDATA);
abd.hWnd = hwndAccessBar;
abd.uCallbackMessage = MSG_APPBAR_MSGID;
!::SHAppBarMessage(ABM_NEW, &abd);
注意MSG_APPBAR_MSGID这个,这是你自己定义的消息ID,当有全屏创建或者取消的时候,会给句柄为hwndAccessBar的窗口发送消息ID为MSG_APPBAR_MSGID的消息,具体到全屏消息,此时WPARAM为ABN_FULLSCREENAPP,而LPARAM则能够判断当前是有窗口全屏了还是有窗口取消全屏了,(BOOL) lParam为TRUE表示有窗口全屏了,而(BOOL) lParam为FALSE则表示有窗口取消全屏状态了。代码如下:
LRESULT CWinHook::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
{
if (MSG_APPBAR_MSGID == msg)
{
switch((UINT)wp)
{
case ABN_FULLSCREENAPP:
{
if (TRUE == (BOOL)lp)
{
TRACE(TEXT("一个窗口全屏了\n"));
KAppBarMsg::m_bFullScreen = TRUE;
}
else
{
TRACE(TEXT("一个窗口取消全屏了\n"));
KAppBarMsg::m_bFullScreen = FALSE;
}
}
break;
default:
break;
}
}
return CSubclassWnd::WindowProc(msg, wp, lp);
}
附注:后来发现vista下面没有XP下灵敏,不知道怎么回事,vista下偶尔会失败,很奇怪。
关于DEP(数据执行保护)的分析 6/13
1.什么是DEP
DEP,全称是Data Excution Protection,中名叫数据执行保护,是XP+sp2,Win2K03+sp1中加入的对内存的一种保护,用来防止恶意程序对系统的攻击,如溢出。
现在只有两种设置方式:
一、是只为系统关键进程和服务提供DEP保护,这也是默认选项。
二、是为所有程序和服务提供DEP保护,除去用户手动指定的程序。
其设置在"我的电脑"->右键菜单"属性"->"高级"->"性能 - 设置"->"数据执行保护"
2.DEP能保护什么?
个人认为DEP就是专门为防止溢出设计的,当然这么说是有点狭隘了,毕竟这种机制完善了CPU的内存管理机制。感觉每一个指令执行之前都进行内存页属性的检测有点太费资源了。
一句话,DEP可以使指定的内存页不具有可执行属性。
这样一来,如果指定栈所在的内存页不可执行,那么,当我们要栈上溢出时,我们的Shellcode将难以被执行。
所以,DEP保护的就是内存,保护指定的内存上的代码不能被执行,这样就可以达到反溢出的目的。
当然,这是微软的一厢情愿罢了。因为越过这个限制也并不是一件难事。
3.可以绕过DEP吗?如果可以,如何绕过呢?
答案是肯定的,可以绕过。即使是硬件上的DEP保护也是可以的。
有一个API,改变指定内存页的属性的,VirtualProtect(),当然,还有其它的API,如
VirtualProtectEx(),ZwProtectVirtulMemory(),不过都是封闭在VirtualProtect()中。
所以,我们溢出的时候,把返回地址设计为这个API的地址,再精心构造一个栈为调用这个API的栈,就可以
改变当前栈的内存页的属性,使其从"不可执行"变成"可执行".
这个过程的示例如下面的一段代码
_test proc
;push 04
;invoke VirtualProtect,esp,30h,PAGE_EXECUTE_READWRITE,esp
;pop eax
mov dword ptr [esp+18h],4
mov eax,esp
add eax,18h
mov dword ptr [esp+14h],eax;lpOldProtection
mov dword ptr [esp+10h],40h;dwNewProtection
mov dword ptr [esp+0ch],30h;dwRegionSize
mov dword ptr [esp+8],esp;dwStartAddress
mov dword ptr [esp],VirtualProtect;func addr
mov eax,esp
add eax,1ch
mov dword ptr [esp+4],eax;return address from VirtualProtect
mov dword ptr [esp+1ch],90909090h;our shellcode
ret
_test endp
4.DEP在内核中到底是如何实现的?
这个问题我曾经费不了少时间去找答案,从内存操作的API上来看,
有标准内存管理API,虚拟内存管理API,堆管理API,内存映射文件API
从内存管理的结构上来看,有VAD,有PFN,有PTE,PDE,有段
一开始我认为windows可能会在任何一个层面上做文章,可能是VAD,也可能是PFN,也可能是PTE
并且我认为VAD的可能性比较大,因为PTE没有相关的bit位表示此页有没有可执行属性。PFN也没有。
VAD倒是有相关的位表示页的可执行属性。
经过一点点的测试和排除,发现VAD与此并没有关系,用VirtualProtect()改变页的属性时,此页对应
的VAD的flag位竟然毫无变化。
那么只剩下PFN和PTE了,发现这个API调用前后,PFN的restore pte这个字段有变化,PTE的低双字却
没有一点变化,高双字我时候我没有关心,我一直认为高双字是用于寻址4G以外的物理内存地址的。
然后我手动把PFN的restore pte改变成上面提到的API改成的值,但是结果却让人失望,我拷入shellcode
的页还是不可执行。
尽管这时,我还是没有意识到PTE的高双字发生的变化,并为此付出了代价,那就是二夜一天的对VirtualProtect()
相关API的反汇编。
VirtualProtect()调用了VirtualProtectEx()
VirtualProtectEx()调用了ZwProtectVirtualMemory()
ZwProtectVirtualMemory()通过sysenter进入内核,EAX中存放的服务号是0x89,对应的服务是NtProtecVirtualMemory()
NtProtectVirtualMemory()又调用了MiProtectVirtualMemory()
在MiProtectVirtualMemory()内部,计算出要改变其属性的内存页的PTE的地址,新的属性,然后调用
MiFlushTbAndCapture()改变PTE的属性,但是我当时也只是看到把PTE的属性从067变成了027,和可执行属性还是
没有关系,然后我再深入到MiFlushTbAndCapture()中,发现它主要又是调用KeFlushSingleTb()来改变指定PTE
的属性的,深入到KeFlushSingleTb()内部,它主要是调用KeInterlockedSwapPte()来改变指定PTE的属性。
而KeInterlockedSwapPte()的内部是比较简单的,来看看它的反汇编代码:
nt!KeInterlockedSwapPte:
80541c08 53 push ebx
80541c09 56 push esi
80541c0a 8b5c2410 mov ebx,dword ptr [esp+10h] ;ebx=新的PTE值所在变量的地址
80541c0e 8b74240c mov esi,dword ptr [esp+0Ch] ;esi=PTE地址
80541c12 8b4b04 mov ecx,dword ptr [ebx+4] ;ecx=新PTE值高双字
80541c15 8b1b mov ebx,dword ptr [ebx] ;ebx=新PTE值低双字
80541c17 8b5604 mov edx,dword ptr [esi+4] ;edx=旧PTE值高双字
80541c1a 8b06 mov eax,dword ptr [esi] ;eax=旧PTE值低双字
80541c1c 0fc70e cmpxchg8b qword ptr [esi]
看到关键点了吧,就是一个cmpxch8b指令,这个指令是干什么的呢?
执行的操作:edx,eax与DST相比较
如果 (edx,eax)=(dst)
则 ZF=1,(dst)<-(ecx,ebx)
否则 ZF=0,(edx,eax)<-dst
很简单,如果新的PTE属性和旧的不等,把PTE属性设置为新的属性。如果相等,则实际上等于不进行操作。
从这里我才发现,他把PTE的高双字设置为0了,以前的值是0x80000000.
所以,DEP是通过PTE的高双字的最高bit即bit63来实现的,这个位置位了,表示此页不可执行。没有置位,
表示此页可以执行。而win2k下面的页目录和页表项只有32个bit,所以不可能提供DEP的这个保护位,因此
DEP只有在64bit的PTE上才能实现。而只有cr4的bit5即PAE启用的时候,PTE才为64bit。
所以,可以这么说吧,intel的奔腾CPU是早就有PAE属性位的,可以支持64位的页表项,而windows系统
只有win2k的某个版本,winxp和win2003的某个server pack版本的内核才支持64的页表项。
不管怎样,PTE的第63位控制着页的可执行属性,我后来查了IA,在IA的修正说明中,才提到这一点,并
把这个位叫做EXB,却对此位的作用一字为提。
但是我们是不能在ring3下操作PTE的,所以,绕过DEP还是得用return-to-lib的经典方式返回到VirtualProtect()
来改变当前栈的属性。
Private Sub ZipOrRar()
'将C盘的test文件夹压缩为test.rar文件
Shell App.Path + "\WinRar.exe M C:\test.rar C:\test"
End Sub
Private Sub UnZipOrRar()
'将test.rar解压缩在C盘下
Shell App.Path + "\WinRar.exe X C:\test.rar C:\"
End Sub
简单判断系统是不是Vista 6/06
BYTE bVersion = (BYTE)GetVersion();
if (bVersion >= 6)
{
printf("主版本号:%X : 当前系统 >= Vista\n",bVersion);
}else{
printf("主版本号:%X : 当前系统 < Vista\n",bVersion);
}
子类化 - zAddressOf 5/30
'Return the address of the specified ordinal method on the oCallback object, 1 = last private method, 2 = second last private method, etc
Private Function zAddressOf(ByVal oCallback As Object, ByVal nOrdinal As Long) As Long
' Note: used both in subclassing and hooking routines
Dim bSub As Byte 'Value we expect to find pointed at by a vTable method entry
Dim bVal As Byte
Dim nAddr As Long 'Address of the vTable
Dim i As Long 'Loop index
Dim J As Long 'Loop limit
RtlMoveMemory VarPtr(nAddr), ObjPtr(oCallback), 4 'Get the address of the callback object's instance
If Not zProbe(nAddr + &H1C, i, bSub) Then 'Probe for a Class method
If Not zProbe(nAddr + &H6F8, i, bSub) Then 'Probe for a Form method
' \\LaVolpe - Added propertypage offset
If Not zProbe(nAddr + &H710, i, bSub) Then 'Probe for a PropertyPage method
If Not zProbe(nAddr + &H7A4, i, bSub) Then 'Probe for a UserControl method
Exit Function 'Bail...
End If
End If
End If
End If
i = i + 4 'Bump to the next entry
J = i + 1024 'Set a reasonable limit, scan 256 vTable entries
Do While i < J
RtlMoveMemory VarPtr(nAddr), i, 4 'Get the address stored in this vTable entry
If IsBadCodePtr(nAddr) Then 'Is the entry an invalid code address?
RtlMoveMemory VarPtr(zAddressOf), i - (nOrdinal * 4), 4 'Return the specified vTable entry address
Exit Do 'Bad method signature, quit loop
End If
RtlMoveMemory VarPtr(bVal), nAddr, 1 'Get the byte pointed to by the vTable entry
If bVal <> bSub Then 'If the byte doesn't match the expected value...
RtlMoveMemory VarPtr(zAddressOf), i - (nOrdinal * 4), 4 'Return the specified vTable entry address
Exit Do 'Bad method signature, quit loop
End If
i = i + 4 'Next vTable entry
Loop
End Function
'Probe at the specified start address for a method signature
Private Function zProbe(ByVal nStart As Long, ByRef nMethod As Long, ByRef bSub As Byte) As Boolean
Dim bVal As Byte
Dim nAddr As Long
Dim nLimit As Long
Dim nEntry As Long
nAddr = nStart 'Start address
nLimit = nAddr + 32 'Probe eight entries
Do While nAddr < nLimit 'While we've not reached our probe depth
RtlMoveMemory VarPtr(nEntry), nAddr, 4 'Get the vTable entry
If nEntry <> 0 Then 'If not an implemented interface
RtlMoveMemory VarPtr(bVal), nEntry, 1 'Get the value pointed at by the vTable entry
If bVal = >&H33 Or bVal = &HE9 Then 'Check for a native or pcode method signature
nMethod = nAddr 'Store the vTable entry
bSub = bVal 'Store the found method signature
zProbe = True 'Indicate success
Exit Do 'Return
End If
End If
nAddr = nAddr + 4 'Next vTable entry
Loop
End Function
Private Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Const SYNCHRONIZE = &H100000
Private Const STANDARD_RIGHTS_REQUIRED = &HF0000
Private Const PROCESS_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or &HFFF)
Private Declare Function NtSuspendProcess Lib "ntdll.dll" (ByVal hProc As Long) As Long
Private Declare Function NtResumeProcess Lib "ntdll.dll" (ByVal hProc As Long) As Long
Private Declare Function TerminateProcess Lib "kernel32" (ByVal hProcess As Long, ByVal uExitCode As Long) As Long
Private hProcess As Long
Private Sub cmdSuspend_Click()
If IsNumeric(txtPid.Text) Then
hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, CLng(txtPid.Text))
If hProcess <> 0 Then NtSuspendProcess hProcess
End If
CloseHandle hProcess
End Sub
Private Sub cmdResume_Click()
If IsNumeric(txtPid.Text) Then
hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, CLng(txtPid.Text))
If hProcess <> 0 Then NtResumeProcess hProcess
End If
CloseHandle hProcess
End Sub
Private Sub cmdTerminate_Click()
If IsNumeric(txtPid.Text) Then
hProcess = OpenProcess(PROCESS_ALL_ACCESS, False, CLng(txtPid.Text))
If hProcess <> 0 Then TerminateProcess hProcess, 0
End If
End Sub