|
"陷阱"技术探秘 " m8 @ U. l1 D( r4 `- |5 v
──动态汉化Windows技术的分析 ' \# K5 }! Q6 `" J" q( M8 Y- r
& D1 _, u S* l. D* r# F 各位朋友,请加入本站邮件列表,你将得到本站最新更新及动态。 5 ^: u# d4 r [0 p7 ^
) r' |0 E" U% f. X9 [
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。 9 ~3 D/ R. x0 N+ _
一、发现了什么?
7 P7 I$ |) A/ I; ^ ^$ y3 o8 Q笔者多年来一直从事Windows下的软件开发工作,经历了Windows 2.0 、 3.0 、3.1 ,直至Windows 95、NT的成长过程,也遍历了长青窗口、长城窗口、DBWin、CStar、RichWin等多个Windows汉化产品。从现在看来,影响最大也最为成功的,当推四通利方的RichWin;此外,中文之星CStar与RichWin师出一门,其核心技术自然也差不多。其对外宣传采用独特的"陷阱" 技术即动态修改Windows代码,一直是笔者感兴趣的地方。
2 m9 C3 w9 n% j+ N* d7 REXEHDR是Microsoft Visual C++开发工具中很有用的一个程序,它可以检查NE(New-Exe cutable)格式文件,用它来分析RichWin的WSENGINE.DLL或CStar的CHINESE.DLL,就会发现与众不同的两点(以CStar 1.20为例):
1 J. l$ ]2 O1 B1 f i' f! v. a* }6 `% m
C:\CSTAR>exehdr chinese.dll /v2 R0 f9 ?6 _* m* U/ `
..................................
2 j# B5 M. Q) a! X' G( g6 type offset target + d# D, X" Y0 ]' c0 Z2 ~
BASE 060a seg 2 offset 0000
9 q- t' K! n! R. B PTR 047e imp GDI.GETCHARABCWIDTHS
: O: {& B* e Q PTR 059b imp GDI.ENUMFONTFAMILIES
! y+ R0 J0 i# H2 h& Q) o) s# o PTR 0451 imp DISPLAY.14 ( EXTTEXTOUT )
) l s6 d7 |3 Z% q- B7 g PTR 0415 imp KEYBOARD.4 ( TOASCII ) : N5 T4 l6 |9 E. t: W5 T, Z( y
PTR 04ba imp KEYBOARD.5 ( ANSITOOEM )
; ^' J) t$ `0 M$ M, g PTR 04c9 imp KEYBOARD.6 ( OEMTOANSI )
7 i( M+ r8 M& K8 ~' ]& X. P PTR 04d8 imp KEYBOARD.134( ANSITOOEMBUFF )
( R# M* V# p q# _ PTR 05f5 imp USER.430 ( LSTRCMP )
) r5 i% y2 `; V$ n PTR 04e7 imp KEYBOARD.135( OEMTOANSIBUFF )
4 m- F# s, R8 {2 e PTR 0514 imp USER.431 ( ANSIUPPER ) " K3 j6 h6 _5 z. b
PTR 0523 imp USER.432 ( ANSILOWER ) 1 ~9 u( w0 Z) f, Z2 w- o
PTR 05aa imp GDI.56 ( CREATEFONT ) : p( k2 |8 _: q5 s
PTR 056e imp USER.433 ( ISCHARALPHA )
5 p- u; R# ]1 W8 w, i# u PTR 05b9 imp GDI.57 ( CREATEFONTINDIRECT ) ; ?& P" e$ _7 |' u+ z
PTR 057d imp USER.434 ( ISCHARALPHANUMERIC )
8 t! b' r" M; V# M2 l3 T% Z* O PTR 049c imp USER.179 ( GETSYSTEMMETRICS ) 7 K% Q/ P3 M4 V0 F3 R$ q; I* f
PTR 0550 imp USER.435 ( ISCHARUPPER )
4 \9 @7 J- v- Q1 Q8 ~9 {% P PTR 055f imp USER.436 ( ISCHARLOWER )
+ Z0 p2 K% r) V! O$ q2 f PTR 0532 imp USER.437 ( ANSIUPPERBUFF )
$ X+ ^% ^" l8 J* e: o" u4 |9 o PTR 0541 imp USER.438 ( ANSILOWERBUFF )
7 g8 I$ _. O, T7 |7 ] PTR 05c8 imp GDI.69 ( DELETEOBJECT )
8 `8 S* ^! Z7 r# p) P PTR 058c imp GDI.70 ( ENUMFONTS ) & ]% D& p1 \* W
PTR 04ab imp KERNEL.ISDBCSLEADBYTE
% A! [4 d/ L# `" Q5 Q% q& f PTR 05d7 imp GDI.82 ( GETOBJECT ) % c* W; o( d" s
PTR 048d imp KERNEL.74 ( OPENFILE )
. X( b+ }* R& V; o/ Q" C# j PTR 0460 imp GDI.91 ( GETTEXTEXTENT )
0 u6 a4 U0 b1 d! S" ^$ f' q- h PTR 05e6 imp GDI.92 ( GETTEXTFACE )
4 B" e0 O0 D1 ~) n; n9 z PTR 046f imp GDI.350 ( GETCHARWIDTH ) * |( a% g' Y" W( ~
PTR 0442 imp GDI.351 ( EXTTEXTOUT )
: V, I5 c- z1 O0 l4 B2 W: E/ S* I PTR 0604 imp USER.471 ( LSTRCMPI ) + Q2 @/ W* g' e2 O- g- d6 F! ^
PTR 04f6 imp USER.472 ( ANSINEXT ) # j6 b, A" g/ y. J1 B8 q P& q
PTR 0505 imp USER.473 ( ANSIPREV )
3 D* O7 \5 g' |( L 5 {% \0 y% m6 c! E- n" X
PTR 0424 imp USER.108 ( GETMESSAGE )
# m' V1 R% r/ V; r& j" ]+ n PTR 0433 imp USER.109 ( PEEKMESSAGE ) * p) V8 k1 m3 f |. E2 ?+ q) ^
35 relocations
+ p# J& A+ b) j* \! L4 [(括号内为笔者加上的对应Windows API函数。) ' ~$ a/ ~" S/ ^* Y+ G. s
第一,在数据段中,发现了重定位信息。
; ]- r: O8 T: W/ [( {第二,这些重定位信息提示的函数,全都与文字显示输出和键盘、字符串有关。也就是说汉化Windows,必须修改这些函数。 6 h( i) C; z- e/ s. V7 _
在这非常特殊的地方,隐藏着什么呢?毋庸置疑,这与众不同的两点,对打开"陷阱"技术之门而言,不是金钥匙,也是敲门砖。
& t2 P' A0 U$ k% q# K3 y. I4 g二、Windows的模块调用机制与重定位概念 5 C: M% y' f0 ]2 u% @+ [
为了深入探究"陷阱"技术,我们先来介绍Windows的模块调用机制。 . {- J/ z$ j! [. z: N
Windows的运行分实模式、标准模式和增强模式三种,虽然这几种模式各不相同,但其核心模块的调用关系却是完全一致的,见图一。
. E: g" D2 [5 I主要的三个模块,有如下的关系: $ K, W' Q2 ]$ t4 W0 U
·KERNEL是Windows系统内核,它不依赖其它模块。 " B9 e% ]+ `/ V1 n9 {1 s+ O' j
·GDI是Windows图形设备接口模块,它依赖于KERNEL模块。
. k. ^/ G: M; M3 Q; ^·USER是Windows用户接口服务模块,它依赖于KERNEL、GDI模块及设备驱动程序等所有模块。
! S* k1 ~+ ~( s这三个模块,实际上就是Windows的三个动态链接库。KERNEL有三种系统存在形式:Kern el.exe(实模式)、Krnl286.exe(标准模式)、Krnl386.exe(386增强模式);GDI模块是Gdi.ex e;USER模块是User.exe。虽然文件名都以EXE为扩展名,但它们实际都是动态链接库。 , C! ^/ v3 Q, \( P. c
<图片> $ p. \5 p9 H' p/ `$ O& b
图1 Windows的模块调用机制 * e& y6 m6 ~- P. ~2 w( V
同时,几乎所有的API函数都隐藏在这三个模块中。用EXEHDR对这三个模块分析,就可列出一大堆大家所熟悉的Windows API函数。 7 J8 U s) k I; r. P
以GDI模块为例,运行结果如下: 2 K9 m: c7 C8 ^3 R; B* E
C:\WINDOWS\SYSTEM>exehdr gdi.exe . n1 _3 E) i. ]! V
Exports:
. ^* {1 i% E5 c' E0 Wrd seg offset name
2 b1 V$ l2 v( Y$ L............
# \: g) l/ t+ c2 |* O3 j# e% L. h351 1 923e EXTTEXTOUT exported, shared data ; a* D9 y: b" P* r# n L$ E
56 3 19e1 CREATEFONT exported, shared data
- v* N' j, t2 u" L+ }3 ]( G............ * ^/ ]" f& \( e1 p' O* d' o# p1 O
至此,读者已能从Windows纷繁复杂的系统中理出一些头续来。下面,再引入一个重要概念——重定位。 , ^+ w/ [5 m' A. A
一个Windows执行程序对调用API函数或对其它动态库的调用,在程序装入内存前,都是一些不能定位的动态链接;当程序调入内存时,这些远调用都需要重新定位,重新定位的依据就是重定位表。在Windows执行程序(包括动态库)的每个段后面,通常都跟有这样一个重定位表。重定位包含调用函数所在模块、函数序列号以及定位在模块中的位置。
/ g$ a+ q, s5 Y+ {例如,用EXEHDR /v 分析CHINESE.DLL得到: 2 y( e0 w" k' @1 R9 Y7 x4 |/ E
6 type offset target ( v& Z# r) O* o
.......... 5 Z; Z: X: S) B
PTR 0442 imp GDI.351
, w! H" K4 E+ B( P- A& p `9 O
' L# }; P, i/ E1 p4 p- a0 U..........
9 D8 h$ d) N5 m8 q8 j5 k就表明,在本段的0442H偏移处,调用了GDI的第351号函数。如果在0442H处是0000:FFFF ,表示本段内仅此一处调用了GDI.351函数;否则,表明了本段内还有一处调用此函数,调用的位置就是0442H处所指向的内容,实际上重定位表只含有引用位置的链表的链头。那么,GDI. 351是一个什么函数呢?用EXEHDR对GDI.EXE作一分析,就可得出,在GDI的出口(Export)函数中,第351号是ExtTextOut。 / o# {, ?9 }7 S6 C! o' f( F4 t4 p" \
这样,我们在EXEHDR这一简单而非常有用的工具帮助下,已经在Windows的浩瀚海洋中畅游了一会,下面让我们继续深入下去。
+ d9 p7 X% J" d0 @. b# ^三、动态汉化Windows原理 * b/ C1 ?$ ]$ g4 [
我们知道,传统的汉化Windows的方法,是要直接修改Windows的显示、输入、打印等模块代码,或用DDK直接开发"中文设备"驱动模块。这样不仅工作量大,而且,系统的完备性很难保证,性能上也有很多限制(早期的长青窗口就是如此),所以只有从内核上修改Windows核心代码才是最彻底的办法。
3 R3 O) L' d4 S; a! V2 \3 U; i/ f从Windows的模块调用机制,我们可以看到,Windows实际上是由包括在KERNEL、GDI、US ER等几个模块中的众多函数支撑的。那么,修改其中涉及语言文字处理的函数,使之能适应中文需要,不就能达到汉化目的了吗? 7 ]1 _+ U. g7 R2 ~3 @, i
因而,我们可以得出这样的结论:在自己的模块中重新编写涉及文字显示、输入的多个函数,然后,将Windows中对这些函数的引用,改向到自己的这些模块中来。修改哪些函数才能完成汉化,这需要深入分析Windows的内部结构,但CHINESE.DLL已明确无误地告诉了我们,在其数据段的重定位表中列出的引用函数,正是CStar修改了的Windows函数!为了验证这一思路, 我们利用RichWin作一核实。 ' p1 s# k& G" v/ a, Y, k
用EXEHDR分析GDI.EXE,得出ExtTextOut函数在GDI的第一代码段6139H偏移处(不同版本的Windows其所在代码段和偏移可能不一样)。然后,用HelpWalk(也是Microsoft Visual C+ +开发工具中的一个)检查GDI的Code1段,6139H处前5个字节是 B8 FF 05 45 55,经过运行Ri chWin 4.3 for Internet后,再查看同样的地方,已改为 EA 08 08 8F 3D。其实反汇编就知道,这5个字节就是 Jmp 3D8F:0808,而句柄为0x3D8F的模块,用HelpWalk能观察正是RichWin 的WSENGINE.DLL的第一代码段( 模块名为TEXTMAN)。而偏移0808H处 B8 B7 3D 45 55 8B E C 1E,正是一个函数起始的地方,这实际上就是RichWin所重改写的ExtTextOut函数。退出Ri chWin后,再用HelpWalk观察GDI的Code1代码段,一切又恢复正常!这与前面的分析结论完全吻合!那么,下一个关键点就是如何动态修改Windows的函数代码,也就是汉化Windows的核心——"陷阱"技术。 - y7 l t8 j) k" i6 q: E; v
四、"陷阱"技术 9 @7 i8 V1 J+ ?. C0 b+ _
讨论"陷阱"技术,还要回到前面的两个发现。发现之二,已能解释为修改的Windows函数,而发现之一却仍是一个迷。 5 Y! C+ |) `* X, ^$ z3 L& B) ~
数据段存放的是变量及常量等内容,如果这里面包含有重定位信息,那么,必定要在变量说明中将函数指针赋给一个FARPROC类型的变量,于是,在变量说明中写下: 8 a( `5 ?: W3 H+ Q* B S( b
FARPROC FarProcFunc=ExtTextOut;
' k& n& C* a/ E3 j' D1 o果然,在自己程序的数据段中也有了重定位信息。这样,当程序调入内存时,变量FarPro cFunc已是函数ExtTextOut的地址了。
) M f6 d0 l0 b0 B要直接修改代码段的内容,还遇到一个难题,就是代码段是不可改写的。这时,需要用到一个未公开的Windows函数AllocCStoDSAlias,取得与代码段有相同基址的可写数据段别名, 其函数声明为:
" O: X7 M% t4 D- w) M0 J6 ~WORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);
/ V% n! C* f+ J% g* _参数是代码段的句柄,返回值是可写数据段别名句柄。 ( Q @, f! \# c( x
Windows中函数地址是32位,高字节是其模块的内存句柄,低字节是函数在模块内的偏移。将得到的可写数据段别名句柄锁定,再将函数偏移处的5个字节保留下来,然后将其改为转向替代函数(用 EA Jmp): % W$ T0 o7 V# C1 ]. V
*(lpStr+wOffset) =0xEA; 4 |1 F9 T* C2 v
四通利方(RichWin)、中文之星(CStar)是大家广为熟知的汉化Windows产品,"陷阱"技术即动态修改Windows代码,一直是其对外宣称的过人技术。本文从Windows的模块调用机制与重定位概念着手,介绍了"陷阱"技术的实现,并给出了采用"陷阱"技术动态修改Windows代码的示例源程序。
3 N1 ^+ Z/ J) @% d) F) e' R/ \5 z
4 G6 U# U0 ^5 q. S4 p//源程序 relocate.c " \& @& L; W8 j) \
#include <WINDOWS.H> ; L* u* l) V% p! Z% i4 B G9 I g
#include <dos.h>
/ m2 f/ Y0 a* yBOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECTFAR*l
; f) B- n1 K+ N# x( {pRect,LPCSTR lpStr, UINT nInt2, int FAR* lpInt);
! y# T9 m+ k8 y9 z; BWORD FAR PASCAL AllocCStoDSAlias(WORD code_sel);
: R3 W W( l$ L* T: jtypedef struct tagFUNC . i" s+ n! n( q, S. G! M( z
{
$ n' i) R7 Q: ]1 h+ Y6 SFARPROC lpFarProcReplace; //替代函数地址 * f$ O' W8 C8 a0 L
FARPROC lpFarProcWindows; //Windows函数地址 1 D$ C0 ~$ t! j. _
BYTE bOld; //保存原函数第一字节
0 e6 T4 z* Y* I0 P) vLONG lOld; //保存原函数接后的四字节长值
2 Q$ s1 \0 U/ r; W' m}FUNC;
1 u, h' }% b" Y* Z2 o/ `' x3 }' {" [
FUNC Func={MyExtTextOut,ExtTextOut};
# ~7 ~1 S# a+ l4 S! G//Windows主函数 + _- p# Q# u1 w K1 Y% P. N Y
int PASCAL WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdL- Y' e, ^% ^- T s1 r
ine,int nCmdShow){
) o) Q, F2 |9 \( Q; \1 m7 R* pHANDLE hMemCode; //代码段句柄
8 x0 n. }, ?1 M1 |% CWORD hMemData; //相同基址的可写数据段别名
/ d" [6 s/ M. O) M4 A! |5 ]( AWORD wOffset; //函数偏移
9 X) a* n. U; G. gLPSTR lpStr; + q0 u9 N/ e" o7 X/ ]. y6 G2 M
LPLONG lpLong; , E1 m: Q/ g! O4 l8 U1 X2 E0 o
char lpNotice[96];
) M3 b8 @& M! n7 [hMemCode=HIWORD((LONG) Func.lpFarProcWindows );
+ b# J; M" q5 v# }6 D4 S) lwOffset=LOWORD((LONG) Func.lpFarProcWindows ); . ^) h* g# R, H6 I7 S
wsprintf(lpNotice,"函数所在模块句柄 0x%4xH,偏移 0x%4xH",hMemCode,wOffset); # c1 P' m$ O6 h0 H2 P
MessageBox(NULL,lpNotice,"提示",MB_OK);
\5 P, k% }$ @# q- v5 Y8 T1 K, S2 [0 v //取与代码段有相同基址的可写数据段别名 7 E3 k. ~' P. W- ?; ^
hMemData=AllocCStoDSAlias(hMemCode);
% ?/ v+ V0 Z5 r% c V% r lpStr=GlobalLock(hMemData); 7 |, B: D A& }4 g% B" |
lpLong=(lpStr+wOffset+1 ); ; ]' B' M4 g5 K" J O# L
//保存原函数要替换的头几个字节 # M- Z% @! z7 {3 b1 V
Func.bOld=*(lpStr+wOffset); ; W/ W' ^: l: f3 }/ E* c
Func.lOld=*lpLong;
1 e3 A/ e' V- g: n" F" i*(lpStr+wOffset)=0xEA; + \; x: O7 E8 b5 T# F8 [
*lpLong=Func.lpFarProcReplace;
! A3 c8 |+ r" hGlobalUnlock(hMemData); , N: ] T# e* n2 b5 E3 G
MessageBox(NULL,"改为自己的函数","提示",MB_OK);
/ h9 ^8 A3 }5 w- N! P5 r//将保留的内容改回来 " K& ^+ K0 i$ t7 T3 I% `( B4 K
hMemData=AllocCStoDSAlias(hMemCode);
) R: q5 F4 t* x, k" b- ~ d, Q) DlpStr=GlobalLock(hMemData);
+ k; y2 a" {3 m+ K1 ^- TlpLong=(lpStr+wOffset+1 );
7 M$ M" G2 q1 ?: h7 E9 X( _7 ?*(lpStr+wOffset)=Func.bOld;
% T- D& Q$ p# i( L$ c*lpLong=Func.lOld;
7 s) o* A6 z5 U) Z) H+ JGlobalUnlock(hMemData); 5 Q! B8 G+ Z5 k! `
MessageBox(NULL,"改回原Windows函数","提示",MB_OK); 2 d2 A. S" m& A' g) h
return 1;
/ L! B1 @ o+ O9 Z} # ?/ n! j m4 S
//自己的替代函数
+ [. Y" K) B& ^ jBOOL WINAPI MyExtTextOut(HDC hDC, int x, int y, UINT nInt1, const RECT FAR*
8 _( }7 V$ a2 N6 z lpRect, LPCSTR lpStr, UINT nInt2, int FAR* lpInt){
7 ~( T& ]: v" S$ o7 CBYTE NameDot[96]={
/ P# Q7 q. \2 p5 v9 `- l 0x09, 0x00, 0xfd, 0x08, 0x09, 0x08, 0x09, 0x10, 0x09, 0x20, _1 _: d) [: w: f+ a4 U T9 p
0x79, 0x40, 0x41, 0x04, 0x47, 0xfe, 0x41, 0x40, 0x79, 0x40,
( }3 | q" ]! W) a* y* }: `/ L 0x09, 0x20, 0x09, 0x20, 0x09, 0x10, 0x09, 0x4e, 0x51, 0x84,
. S" E/ F% J3 i+ V 0x21, 0x00, 0x02, 0x00, 0x01, 0x04, 0xff, 0xfe, 0x00, 0x00,
% [& E7 d6 A% l6 V8 U; ? 0x1f, 0xf0, 0x10, 0x10, 0x10, 0x10, 0x1f, 0xf0, 0x00, 0x00,
' `* ?( ^$ k8 D, R8 \* S* r 0x7f, 0xfc, 0x40, 0x04, 0x4f, 0xe4, 0x48, 0x24, 0x48, 0x24, ) N1 R: x+ E- ^, P/ {
0x4f, 0xe4, 0x40, 0x0c, 0x10, 0x80, 0x10, 0xfc, 0x10, 0x88,
: O" }) f; x. i6 N6 y 0x11, 0x50, 0x56, 0x20, 0x54, 0xd8, 0x57, 0x06, 0x54, 0x20,
- W# G3 w# b$ {! | 0x55, 0xfc, 0x54, 0x20, 0x55, 0xfc, 0x5c, 0x20, 0x67, 0xfe, 6 f8 k# X& Y3 t3 e" o. W3 ~! Q
0x00, 0x20, 0x00, 0x20, 0x00, 0x20 ( [: k/ S; l I1 [% f
}; 2 A C8 R8 G- @9 g! j {
HBITMAP hBitmap,hOldBitmap; 4 K1 b6 y* X6 X3 @
HDC hMemDC; : D: o8 d# Q2 ^1 Z* k- u; Z* S
BYTE far *lpDot; ( }# y" {, @: c4 _. ~
int i; 3 V) }5 j& H- D, v# e0 E& W4 b
for ( i=0;i<3;i++ )
% y3 \0 S# F2 _) l{
. F9 i8 o% W! z6 t% `lpDot=(LPSTR)NameDot+i*32;
* {* A) x E( s' g+ ^& _hMemDC=CreateCompatibleDC(hDC);
1 ^6 A5 t1 q9 [hBitmap=CreateBitmap(16,16,1,1,lpDot);
1 S: J5 H! s$ ]/ ]SetBitmapBits(hBitmap,32L,lpDot); 7 a" P* G; g1 ~% i! } z5 U) s
hOldBitmap=SelectObject(hMemDC,hBitmap); 6 \% W" B. l( P+ l9 E3 e
BitBlt(hDC,x+i*16,y,16,16,hMemDC,0,0,SRCCOPY);
, M9 X; X" ~/ b3 ~DeleteDC(hMemDC);
7 g. o" N/ p: z' w/ ~/ eDeleteObject(hBitmap);
" {7 W4 H! H) `' d/ l} 6 _( c" O! q4 X
return TRUE;
r. f+ x0 X4 d. e0 \}
0 f! w' P r/ M% `- p4 z- g
! L5 | L( v3 F: @' X1 w//模块定义文件 relocate.def
$ ~$ g. [1 s; y* H- f- S8 m% ]NAME RELOCATE
; _4 t% u1 O9 U# R& [/ I1 D* YEXETYPE WINDOWS
7 L7 a$ ^6 P4 o( ` N) r# |5 }CODE PRELOAD MOVEABLE DISCARDABLE : v6 T" `7 j1 o
DATA PRELOAD MOVEABLE MULTIPLE
6 L1 L: t( I3 C' w9 JHEAPSIZE 1024 2 f, \7 U; b( D5 k! \: u3 u- E
EXPORTS
# o" C2 y9 z5 G
- h3 s6 V9 I w5 E五、结束语
0 k9 j: Q( _3 b本文从原理上分析了称为"陷阱"技术的动态汉化Windows方法,介绍了将任一Windows函数调用改向到自己指定函数处的通用方法,这种方法可以拓展到其它应用中,如多语种显示、不同内码制式的切换显示等。 - a( L( y, o y* l. E" p4 `7 v
5 u% S' {) A: b9 Q: v1 L$ d" g! h% n
|