首页
社区
课程
招聘
朋友们, 我有个挂接API的问题, 帮我看看。 谢谢!
发表于: 2006-5-28 18:48 8480

朋友们, 我有个挂接API的问题, 帮我看看。 谢谢!

2006-5-28 18:48
8480
我最近研究了下金山词霸屏幕取词, 发现挂接API太有用了。
网上看了下将屏幕取词的文章, 但是关键部分都没将清楚。
有没有朋友对这个比较了解的能和我分享下啊。。
请和我联系 E-mail: v_cpp@tom.com

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (19)
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
偶也对这个感兴趣的,有人来详细说一下,我是很支持的
记得看雪里曾经有过一篇,现在记不了是在精华几了,可以参考的
搜了一下,有一篇貌似写的比较正规,我也正在看.没看完的,真假莫辨,贴在这儿下次不用搜了
原文如下:
鼠标屏幕取词技术的原理和实现
文档作者:白瑜
所属类别:VC++
推荐指数:★★★☆
文档人气:554
本周人气:6
发布日期:2006-3-23
鼠标屏幕取词技术的原理和实现
白瑜
“鼠标屏幕取词”技术是在电子字典中得到广泛地应用的,如四通利方和金山
词霸等软件,这个技术看似简单,其实在windows系统中实现却是非常复杂的,总
的来说有两种实现方式:
第一种:采用截获对部分gdi的api调用来实现,如textout,textouta等。
第二种:对每个设备上下文(dc)做一分copy,并跟踪所有修改上下文(dc)的
操作。
第二种方法更强大,但兼容性不好,而第一种方法使用的截获windowsapi的
调用,这项技术的强大可能远远超出了您的想象,毫不夸张的说,利用windowsapi
拦截技术,你可以改造整个操作系统,事实上很多外挂式windows中文平台就是
这么实现的!而这项技术也正是这篇文章的主题。
截windowsapi的调用,具体的说来也可以分为两种方法:
第一种方法通过直接改写winapi 在内存中的映像,嵌入汇编代码,使之被
调用时跳转到指定的地址运行来截获;第二种方法则改写iat(import address table
输入地址表),重定向winapi函数的调用来实现对winapi的截获。
第一种方法的实现较为繁琐,而且在win95、98下面更有难度,这是因为虽
然微软说win16的api只是为了兼容性才保留下来,程序员应该尽可能地调用32位
的api,实际上根本就不是这样!Win 9x内部的大部分32位api经过变换调用了同
名的16位api,也就是说我们需要在拦截的函数中嵌入16位汇编代码!
我们将要介绍的是第二种拦截方法,这种方法在win95、98和nt下面运行都
比较稳定,兼容性较好。由于需要用到关于windows虚拟内存的管理、打破进程
边界墙、向应用程序的进程空间中注入代码、pe(portable executable)文件
格式和iat(输入地址表)等较底层的知识,所以我们先对涉及到的这些知识大
概地做一个介绍,最后会给出拦截部分的关键代码。
先说windows虚拟内存的管理。Windows9x给每一个进程分配了4gb的地址空
间,对于nt来说,这个数字是2gb,系统保留了2gb到 4gb之间的地址空间禁止进
程访问,而在win9x中,2gb到4gb这部分虚拟地址空间实际上是由所有的win32进
程所共享的,这部分地址空间加载了共享win32 dll、内存映射文件和vxd、内存
管理器和文件系统码,win9x中这部分对于每一个进程都是可见的,这也是win9x
操作系统不够健壮的原因。Win9x中为16位操作系统保留了0到4mb的地址空间,
而在4mb到2gb之间也就是win32进程私有的地址空间,由于 每个进程的地址空间
都是相对独立的,也就是说,如果程序想截获其它进程中的api调用,就必须打
破进程边界墙,向其它的进程中注入截获api调用的代码,这项工作我们交给钩
子函数(setwindowshookex)来完成,关于如何创建一个包含系统钩子的动态链
接库,《电脑高手杂志》在第?期已经有过专题介绍了,这里就不赘述了。所有
系统钩子的函数必须要在动态库里,这样的话,当进程隐式或显式调用一个动态
库里的函数时,系统会把这个动态库映射到这个进程的虚拟地址空间里,这使得
dll成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈,也就是
说动态链接库中的代码被钩子函数注入了其它gui进程的地址空间(非gui进程,
钩子函数就无能为力了),当包含钩子的dll注入其它进程后,就可以取得映射
到这个进程虚拟内存里的各个模块(exe和dll)的基地址,如:
hmodule hmodule=getmodulehandle(“mypro.exe”);
在mfc程序中,我们可以用afxgetinstancehandle()函数来得到模块的基地址。
Exe和dll被映射到虚拟内存空间的什么地方是由它们的基地址决定的。它们的基
地址是在链接时由链接器决定的。当你新建一个win32工程时,vc++链接器使
用缺省的基地址0x00400000。可以通过链接器的base选项改变模块的基地址。
Exe通常被映射到虚拟内存的0x00400000处,dll也随之有不同的基地址,通常被
映射到不同进程的相同的虚拟地址空间处。
系统将exe和dll原封不动映射到虚拟内存空间中,它们在内存中的结构与磁盘
上的静态文件结构是一样的。即pe (portable executable) 文件格式。我们得
到了进程模块的基地址以后,就可以根据pe文件的格式穷举这个模块的image_import_descriptor
数组,看看进程空间中是否引入了我们需要截获的函数所在的动态链接库,比如
需要截获“textouta”,就必须检查“gdi32.dll”是否被引入了。说到这里,我
们有必要介绍一下pe文件的格式,如右图,这是pe文件格式的大致框图,最前面
是文件头,我们不必理会,从pe file optional header后面开始,就是文件中各
个段的说明,说明后面才是真正的段数据,而实际上我们关心的只有一个段,那
就是“.idata”段,这个段中包含了所有的引入函数信息,还有iat(import address table)
的rva(relative virtual address)地址。
说到这里,截获windowsapi的整个原理就要真相大白了。实际上所有进程对
给定的api函数的调用总是通过pe文件的一个地方来转移的,这就是一个该模块
(可以是exe或dll)的“.idata”段中的iat输入地址表(import address table)
。在那里有所有本模块调用的其它dll的函数名及地址。对其它dll的函数调用实
际上只是跳转到输入地址表,由输入地址表再跳转到dll真正的函数入口。
具体来说,我们将通过image_import_descriptor数组来访问“.idata”段中引入
的dll的信息,然后通过image_thunk_data数组来针对一个被引入的dll访问该
dll中被引入的每个函数的信息,找到我们需要截获的函数的跳转地址,然后改
成我们自己的函数的地址……具体的做法在后面的关键代码中会有详细的讲解。
讲了这么多原理,现在让我们回到“鼠标屏幕取词”的专题上来。除了api函
数的截获,要实现“鼠标屏幕取词”,还需要做一些其它的工作,简单的说来,
可以把一个完整的取词过程归纳成以下几个步骤:
1. 安装鼠标钩子,通过钩子函数获得鼠标消息。
使用到的api函数:setwindowshookex
2. 得到鼠标的当前位置,向鼠标下的窗口发重画消息,让它调用系统函数重画
窗口。
使用到的api函数:windowfrompoint,screentoclient,invalidaterect
3. 截获对系统函数的调用,取得参数,也就是我们要取的词。
对于大多数的windows应用程序来说,如果要取词,我们需要截获的是
“gdi32.dll”中的“textouta”函数。
我们先仿照textouta函数写一个自己的mytextouta函数,如:
bool winapi mytextouta(hdc hdc, int nxstart, int nystart, lpcstr lpszstring,int cbstring)
{
// 这里进行输出lpszstring的处理
// 然后调用正版的textouta函数
}
把这个函数放在安装了钩子的动态连接库中,然后调用我们最后给出的
hookimportfunction函数来截获进程对textouta函数的调用,跳转到我们的
mytextouta函数,完成对输出字符串的捕捉。Hookimportfunction的用法:

hookfuncdesc hd;
proc porigfuns;
hd.szfunc="textouta";
hd.pproc=(proc)mytextouta;
hookimportfunction (afxgetinstancehandle(),"gdi32.dll",&hd,porigfuns);
下面给出了hookimportfunction的源代码,相信详尽的注释一定不会让您觉得理
解截获到底是怎么实现的很难,ok,let’s go:

///////////////////////////////////////////// begin ///////////////////////////////
#include <crtdbg.h>

// 这里定义了一个产生指针的宏
#define makeptr(cast, ptr, addvalue) (cast)((dword)(ptr)+(dword)(addvalue))

// 定义了hookfuncdesc结构,我们用这个结构作为参数传给hookimportfunction函数
typedef struct tag_hookfuncdesc
{
	lpcstr szfunc; // the name of the function to hook.
	Proc pproc; // the procedure to blast in.
} hookfuncdesc , * lphookfuncdesc;

// 这个函数监测当前系统是否是windownt
bool isnt();

// 这个函数得到hmodule -- 即我们需要截获的函数所在的dll模块的引入描述符(import descriptor)
pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule);

// 我们的主函数
bool hookimportfunction(hmodule hmodule, lpcstr szimportmodule, 
						lphookfuncdesc pahookfunc, proc* paorigfuncs)
{
	/////////////////////// 下面的代码检测参数的有效性 ////////////////////////////
	_assert(szimportmodule);
	_assert(!isbadreadptr(pahookfunc, sizeof(hookfuncdesc)));
#ifdef _debug
	if (paorigfuncs) _assert(!isbadwriteptr(paorigfuncs, sizeof(proc)));
	_assert(pahookfunc.szfunc);
	_assert(*pahookfunc.szfunc != '\0');
	_assert(!isbadcodeptr(pahookfunc.pproc));
#endif
	if ((szimportmodule == null) || (isbadreadptr(pahookfunc, sizeof(hookfuncdesc))))
	{
		_assert(false);
		setlasterrorex(error_invalid_parameter, sle_error);
		return false;
	}
	//////////////////////////////////////////////////////////////////////////////
	
	// 监测当前模块是否是在2gb虚拟内存空间之上
	// 这部分的地址内存是属于win32进程共享的
	if (!isnt() && ((dword)hmodule >= 0x80000000))
	{
		_assert(false);
		setlasterrorex(error_invalid_handle, sle_error);
		return false;
	}
	// 清零
	if (paorigfuncs) memset(paorigfuncs, null, sizeof(proc)); 
	
	// 调用getnamedimportdescriptor()函数,来得到hmodule -- 即我们需要
	// 截获的函数所在的dll模块的引入描述符(import descriptor)
	pimage_import_descriptor pimportdesc = getnamedimportdescriptor(hmodule, szimportmodule);
	if (pimportdesc == null)
		return false; // 若为空,则模块未被当前进程所引入
	
	// 从dll模块中得到原始的thunk信息,因为pimportdesc->firstthunk数组中的原始信息已经
	// 在应用程序引入该dll时覆盖上了所有的引入信息,所以我们需要通过取得pimportdesc->originalfirstthunk
	// 指针来访问引入函数名等信息
	pimage_thunk_data porigthunk = makeptr(pimage_thunk_data, hmodule, 
		pimportdesc->originalfirstthunk);
	
	// 从pimportdesc->firstthunk得到image_thunk_data数组的指针,由于这里在dll被引入时已经填充了
	// 所有的引入信息,所以真正的截获实际上正是在这里进行的
	pimage_thunk_data prealthunk = makeptr(pimage_thunk_data, hmodule, pimportdesc->firstthunk);
	
	// 穷举image_thunk_data数组,寻找我们需要截获的函数,这是最关键的部分!
	While (porigthunk->u1.function)
	{
		// 只寻找那些按函数名而不是序号引入的函数
		if (image_ordinal_flag != (porigthunk->u1.ordinal & image_ordinal_flag))
		{
			// 得到引入函数的函数名
			pimage_import_by_name pbyname = makeptr(pimage_import_by_name, hmodule,
				porigthunk->u1.addressofdata);
			
			// 如果函数名以null开始,跳过,继续下一个函数 
			if ('\0' == pbyname->name[0])
				continue;
			
			// bdohook用来检查是否截获成功
			bool bdohook = false;
			
			// 检查是否当前函数是我们需要截获的函数
			if ((pahookfunc.szfunc[0] == pbyname->name[0]) &&
				(strcmpi(pahookfunc.szfunc, (char*)pbyname->name) == 0))
			{
				// 找到了!
				If (pahookfunc.pproc)
					bdohook = true;
			}
			if (bdohook)
			{
				// 我们已经找到了所要截获的函数,那么就开始动手吧
				// 首先要做的是改变这一块虚拟内存的内存保护状态,让我们可以自由存取
				memory_basic_information mbi_thunk;
				virtualquery(prealthunk, &mbi_thunk, sizeof(memory_basic_information));
				_assert(virtualprotect(mbi_thunk.baseaddress, mbi_thunk.regionsize, 
					page_readwrite, &mbi_thunk.protect));
				
				// 保存我们所要截获的函数的正确跳转地址
				if (paorigfuncs)
					paorigfuncs = (proc)prealthunk->u1.function;
				
				// 将image_thunk_data数组中的函数跳转地址改写为我们自己的函数地址!
				// 以后所有进程对这个系统函数的所有调用都将成为对我们自己编写的函数的调用
				prealthunk->u1.function = (pdword)pahookfunc.pproc;
				
				// 操作完毕!将这一块虚拟内存改回原来的保护状态
				dword dwoldprotect;
				_assert(virtualprotect(mbi_thunk.baseaddress, mbi_thunk.regionsize, 
					mbi_thunk.protect, &dwoldprotect));
				setlasterror(error_success);
				return true;
			}
		}
		// 访问image_thunk_data数组中的下一个元素
		porigthunk++;
		prealthunk++;
	}
	return true;
}

// getnamedimportdescriptor函数的实现
pimage_import_descriptor getnamedimportdescriptor(hmodule hmodule, lpcstr szimportmodule)
{
	// 检测参数
	_assert(szimportmodule);
	_assert(hmodule);
	if ((szimportmodule == null) || (hmodule == null))
	{
		_assert(false);
		setlasterrorex(error_invalid_parameter, sle_error);
		return null;
	}
	
	// 得到dos文件头
	pimage_dos_header pdosheader = (pimage_dos_header) hmodule;
	
	// 检测是否mz文件头
	if (isbadreadptr(pdosheader, sizeof(image_dos_header)) || 
		(pdosheader->e_magic != image_dos_signature))
	{
		_assert(false);
		setlasterrorex(error_invalid_parameter, sle_error);
		return null;
	}
	
	// 取得pe文件头
	pimage_nt_headers pntheader = makeptr(pimage_nt_headers, pdosheader, pdosheader->e_lfanew);
	
	// 检测是否pe映像文件
	if (isbadreadptr(pntheader, sizeof(image_nt_headers)) || 
		(pntheader->signature != image_nt_signature))
	{
		_assert(false);
		setlasterrorex(error_invalid_parameter, sle_error);
		return null;
	}
	
	// 检查pe文件的引入段(即 .idata section)
	if (pntheader->optionalheader.datadirectory[image_directory_entry_import].virtualaddress == 0)
		return null;
	
	// 得到引入段(即 .idata section)的指针
	pimage_import_descriptor pimportdesc = makeptr(pimage_import_descriptor, pdosheader,
		pntheader->optionalheader.datadirectory[image_directory_entry_import].virtualaddress);
	
	// 穷举pimage_import_descriptor数组寻找我们需要截获的函数所在的模块
	while (pimportdesc->name)
	{
		pstr szcurrmod = makeptr(pstr, pdosheader, pimportdesc->name);
		if (stricmp(szcurrmod, szimportmodule) == 0)
			break; // 找到!中断循环
		// 下一个元素
		pimportdesc++;
	}
	
	// 如果没有找到,说明我们寻找的模块没有被当前的进程所引入!
	If (pimportdesc->name == null)
		return null;
	
	// 返回函数所找到的模块描述符(import descriptor)
	return pimportdesc;
}

// isnt()函数的实现
bool isnt()
{
	osversioninfo stosvi;
	memset(&stosvi, null, sizeof(osversioninfo));
	stosvi.dwosversioninfosize = sizeof(osversioninfo);
	bool bret = getversionex(&stosvi);
	_assert(true == bret);
	if (false == bret) return false;
	return (ver_platform_win32_nt == stosvi.dwplatformid);
}
/////////////////////////////////////////////// end ////////////////////////////////////


不知道在这篇文章问世之前,有多少朋友尝试过去实现“鼠标屏幕取词”这
项充满了挑战的技术,也只有尝试过的朋友才能体会到其间的不易,尤其在探索
api函数的截获时,手头的几篇资料没有一篇是涉及到关键代码的,重要的地方
都是一笔代过,msdn更是显得苍白而无力,也不知道除了image_import_descriptor
和image_thunk_data,微软还隐藏了多少秘密,好在硬着头皮还是把它给攻克了,
希望这篇文章对大家能有所帮助.
2006-5-28 19:21
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
这篇文章我看过, 讲的就是修改IAT吧。  
但是我有个疑问, 想explorer这种正在运行的进程,如果你改变它的IAT, 进程不会出错吗?
屏幕取词是要挂接所有进程的TextOutA函数吗?
来讨论一下.
2006-5-28 20:20
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
找到了,在精华4的编程相关里,发表有几年了.
比较长,有很多问答,感觉作者有点故意闪烁其辞
2006-5-28 20:36
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
我3楼的问题你知道吗? 教下我!谢谢
2006-5-28 20:55
0
雪    币: 1852
活跃值: (504)
能力值: (RANK:1010 )
在线值:
发帖
回帖
粉丝
6
二楼的帖子,我编辑了下,加了代码显示,这样看起来舒服点

Originally posted by yoshimitsu
这篇文章我看过, 讲的就是修改IAT吧。
但是我有个疑问, 想explorer这种正在运行的进程,如果你改变它的IAT, 进程不会出错吗?
屏幕取词是要挂接所有进程的TextOutA函数吗?
来讨论一下.


1)。IAT只不过是一组的函数地址,改变其中的值只不过是调用函数的转接,如果在目标函数的最后能够继续调用原始的函数,这样就实现函数的挂接,所谓的“钩子”.不会有问题的
2)。屏幕取词一般是针对屏幕上所有的进程
2006-5-29 10:13
0
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
7
DLL---动态链接库,就是加载到共享内存空间中的。

这个,要分情况阿,大部分都是弄到进程空间的,北极星还在用win98么?不错啊,98的发扬光大就靠兄弟了  
2006-5-29 12:52
0
雪    币: 1852
活跃值: (504)
能力值: (RANK:1010 )
在线值:
发帖
回帖
粉丝
8
Originally posted by nbw
DLL---动态链接库,就是加载到共享内存空间中的。


这个,要分情况阿,大部分都是弄到进程空间的,北极星还在用win98么?不错啊,98的发扬光大就靠兄弟了


多谢提醒,久久不搞,一时糊涂
2006-5-29 13:43
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
"以后所有进程对这个系统函数的所有调用都将成为对我们自己编写的函数的调用"
偶记得Undocumented Win里面写有WIN NT的DLL有一个重要的概念
copy_on_write(写时拷贝),就白瑜的那样恐怕是不行吧.要想一劳永逸,我想要用驱动的说,关键在于要监视全局,恐怕要进RING0级动一下
才行.不知道说的对不对,希望有大虾出现给说一下
BTW:求助一个问题:在一台笔记本上的WinXP PS2版上安装了SOFTICE(版本一时
    想不起了),启动方式为  BOOT方式,发现计算机出现无法从休眠状态醒过来,
    只有重启.不知如何解决.
    多谢版主大人,好看了
    没连网真是惨,最近闪存又丢了,上网都是只能看,不能带会去,等
    毕业了一定要连一条三尺宽的网
2006-5-29 14:03
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
今天用修改IAT的方法试了下挂接API, 目标API是MessageBoxA
我将MessageBoxA的替代函数MsgBoxProxy放在一个动态连接库的,
MsgBoxProxy函数对MessageBoxA的参数做了些改动再调用原来user32.dll里的
MessageBoxA函数, 但是调用原来的MessageBoxA后要出错, 是堆栈不指针esp不对的原因引起的。 后来我用OllyDbg跟踪DLL后, 发现MsgBoxProxy函数调用
MessageBoxA后, 本来esp指针是正确的, 但是程序却在后面加了add esp,10
这条指令, 使ESP不正确了。 后来我把这条指令改成add esp, 0 然后保存, 这样就不会出错了。

但是程序为什么会加add esp, 10这条指令在后面呢?
有没有朋友遇到过这个问题。
请指教。。 谢谢!
2006-5-29 15:06
0
雪    币: 313
活跃值: (440)
能力值: ( LV12,RANK:530 )
在线值:
发帖
回帖
粉丝
11
调用约定不一样把..堆栈由谁来平衡的问题
2006-5-29 15:31
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
不会吧。 我自己的MsgBoxProxy也是用的__stdcall调用规则。
2006-5-29 16:14
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
我自己的函数MsgBoxProxy也是用的__stdcall调用规则啊,  而且我是用C写的, 不存在堆栈的维护问题吧。
2006-5-29 16:16
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
果然是调用约定出了问题, 前面定义函数指针时少了个WINAPI
谢谢朋友提醒啊。 以后注意了。
2006-5-29 16:26
0
雪    币: 208
活跃值: (376)
能力值: ( LV12,RANK:330 )
在线值:
发帖
回帖
粉丝
15


代码只是在2000下能用,稍作改动可以用于98。
如转载请注明原作者。
如有讨论者可以通过huiyugan@263.net甘化新联系。
上传的附件:
2006-5-29 20:29
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
这么厉害, 哎, 可惜是用delphi写的。。
要是用C\C++或者汇编的就好了。
感谢朋友。。
2006-5-30 13:53
0
雪    币: 196
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
俺是顶了啊!!!!!
2006-5-30 16:02
0
雪    币: 216
活跃值: (77)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
发现金山词霸(2006也是)不能取用EPE加壳的程序,不知为何。
不知道是不是修改IAT和修改入口点的区别。金山词霸可能采用修改IAT吧(猜的,没跟)。
2006-5-30 19:23
0
雪    币: 208
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
楼上朋友发的那个DELPHI做的取词程序也不能取WORD等等程序的词,
不知道WORD这些程序的取词方法有什么不同呢?谁能介绍下。
2006-5-30 21:46
0
雪    币: 5
活跃值: (15)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
最初由 yoshimitsu 发布
我最近研究了下金山词霸屏幕取词, 发现挂接API太有用了。
网上看了下将屏幕取词的文章, 但是关键部分都没将清楚。
有没有朋友对这个比较了解的能和我分享下啊。。
请和我联系 E-mail: v_cpp@tom.com


很早的技术了 在GOOGLE上可以搜的到代码的 对于一些画上去的窗体文字是截取不到的
2006-6-4 22:33
0
游客
登录 | 注册 方可回帖
返回
//