首页 > VMP > VMP-原理(看雪笔记)
2016三月27

VMP-原理(看雪笔记)

[隐藏]

1.原理

虚拟机保护技术就是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和篡改,这种指令执行系统和Intel的x86指令系统并不在同一个层次上。比如说80×86汇编指令是在CPU里执行的,而字节码指令系统是通过解释指令来执行的,并且这里谈到的字节码指令执行系统是建立在x86指令系统上的。

字节码(Bytecode)其实就是指令执行系统定义的一套指令和数据组成的一串数据流。Java的JVM、.Net或者其他动态语言的虚拟机都是靠解释字节码来执行的,但它们的字节码之间并不通用,因为每一个系统设计的字节码都是为自己使用的,并不兼容其他的系统。

如下图:

VStartVM部分初始化虚拟机,VMDispatcher来调度这些Handler,如果将其看成一个CPU的话,Bytecode就是CPU中所执行的二进制代码,VMDispatcher就是CPU执行调度器,Handler就是CPU中所支持的每一条指令。

1.png

1.1.反汇编引擎

反汇编引擎是OllyDbg提供的源代码,作者是Oleh Yuschuk

  

1.2.指令分类

将需要描述的x86指令进行分类,按功能可以分为4类:普通指令、堆栈栈指令、流指令和不可模拟指令

这里将需要描述的x86指令进行分类,按功能可以分为4类:普通指令、堆栈栈指令、流指令和不可模拟指令。

(1)普通指令包括算术指令、数据传输指令等;

(2)栈指令主要是push和pop等进行栈操作的指令;

(3)流指令是如jmp,jmpc,call,retn等会更改程序执行流程的指令;

(4)不可模拟指令,顾名思义,就是无法再次模拟的指令了,如int3,sysenter,in,out等,这类指令只能用其他方式处理。

按操作数可以分为:无操作数指令、单操作数指令、双操作数指令、多操作数指令。下表是分类指令表(不一定完全)

1.png

其中,多操作数其实可以写为双操作数的形式,所以也可以在实现时将其归类为双操作数中。

当前主流动态语言的虚拟机是基于两种模式的:Stack-Based模式和Regiser-Based模式。除了Lua5现在已经改为Register-Based模式,其他语言现在还是Stack-Based模式。Stack-Based模式的优点在于字节码指令长度短,用到的指令比Register-Based模式更少。本节所讲述的虚拟机稍有一些不同,其是将汇编指令编译为字节码,不过仍然可以借鉴Stack-Based模式的思想。

  

2.启动框架和调用约定

在讲解Handler之前,有必要先说一下启动框架,因为它们之间是互相调用的,并且是相辅相成的,所以它们之间需要有一种代码约定。请先看启动框架是如何设计的。

2.1.调度器VStartVM

VStartVM过程将真实环境压入后有一个VMDispatcher标签,当Handler执行完毕后会跳回到这里形成了一个循环,所以VStartVM过程也可以叫做dispatcher(调度器)。

VStartVM首先将所有寄存器的符号压入堆栈,然后esi指向字节码起始地址,ebp指向真实堆栈,edi指向VMContext,esp再减去40h(这个值是可以变化的)就是VM用的堆栈地址了。换句话说,这里将VM的环境结构和堆栈都放在了当前堆栈之下200h处的位置上了。

因为堆栈是变化的,在执行完跟堆栈有关的指令时总应该检查一下真实堆栈是否已经接近自己存放的数据了,如果是,那么再将自己的结构往更底下移动。然后从“movzx eax,byte ptr [esi]”这句开始,读字节码,读出一个字节,然后在JUMP表中寻找相应的Handler,并跳转过去继续执行。具体代码如下:

// 进入虚拟机函数
void _declspec(naked) VStartVM()
{
	_asm
	{
		// 将寄存器压入堆栈,由伪指令取出存放到VMReg中
		push eax
		push ebx
		push ecx
		push edx
		push esi
		push edi
		push ebp
		pushfd
		mov  esi,[esp+0x20]//参数,字节码的开始地址,0x20是前面压了8个寄存器				
		mov  ebp,esp//ebp保存原堆栈指针														      
		sub  esp,0x200//将VM的环境结构和堆栈都放在了当前堆栈之下200h处的位置上
		mov  edi,esp//edi指向VMContext,即当前堆栈下0x200								
		sub  esp,0x40//esp现在是vm用的堆栈了											
//Jcc:														
		mov  ebx,esi// 把伪代码base_addr做key										
//Exec:														
		movzx eax,byte ptr [esi]//获得字节码						
		lea   esi,[esi+1]//跳过这个字节									
								
		jmp   dword ptr [eax*4+JUMPADDR]//跳到Handler执行处							
		VM_END												
	}
}

调用方法:

push 指向字节码的起始地址
jmp VStartVM

在这里看到了几个约定:

• edi =VMContext

• esi =当前字节码地址

• ebp =真实堆栈

这是整个执行循环都要遵守的一个事实,一般情况下谁也不应该将这些寄存器另做他用。另外,edi指向的VMContext存放在栈中而没有存放在其他固定地址或者申请的堆空间中,是因为考虑到多线程程序的兼容。假如有一个需要虚拟化的函数可能会被多个线程调用,而这时存放在固定的地址上就会出错,因为只能保存一个线程的环境结构。当然使用分配堆空间的API来为每一个线程都创建一个存放环境结构的空间也未尝不可,但虚拟机只是将汇编指令转换成虚拟指令来执行而已,使用API的话就会使其依赖操作系统而失去了兼容性,所以这里选择了堆栈。

2.2.虚拟环境:VMContext

VMContext即虚拟环境结构,存放了一些需要用到的值:

struct VMContext
{
	DWORD v_eax;
	DWORD v_ebx;

	DWORD v_ecx;
	DWORD v_edx;

	DWORD v_esi;
	DWORD v_edi;

	DWORD v_ebp;
	DWORD v_efl;// 符号寄存器
};

为什么这个环境唯独缺少了esp寄存器呢?因为esp寄存器的值已经被放在真实的ebp寄存器中了,VStartVM将所有的寄存器都压入了堆栈。所以,首先应该使堆栈平衡才能开始执行真正的代码,为此,设计了一个Handler VBegin来做这项工作。

  

2.3.平衡堆栈:VBegin和VCheckEsp

平衡堆栈:VBegin的代码如下(参看CInterpretHandler::InterpretvBegin):

vBegin:
		mov eax, dword ptr[ebp]
		mov [edi+0x1c],eax// v_ef1
		add ebp,4

		mov eax, dword ptr[ebp]
		mov [edi+0x18],eax//v_ebp
		add ebp,4

		mov eax, dword ptr[ebp]
		mov [edi+0x14],eax//v_edi
		add ebp,4

...
		mov eax, dword ptr[ebp]
		mov [edi+0x04],eax//v_ebx
		add ebp,4

		mov eax, dword ptr[ebp]
		mov [edi],eax//v_ebx
		add ebp,4

		add ebp,4// 释放参数
		jmp VMDispatcher

执行这个Handler之后,堆栈就平衡了,就可以开始继续执行真正的代码了。但是,因为将VMContext结构存放在当前使用的堆栈更靠下面的一部分,所以应该避免出现VMContext结构被覆盖的情况。

1.png

如上图所示,当堆栈被压入数据时,总会在某条指令之后改写VMContext的内容,因为这个原因设计了VCheckESP Handler。代码如下:

//检测堆栈是否覆盖,如可能覆盖,则将VMContext结构下移0x60
void _declspec(naked) DCheckESP()
{
	_asm
	{
		// edi -> 指向VMContext
		// ebp -> 指向真实的堆栈
		//1.判断VMContext往上0x100处是否已接近真实堆栈
		lea     eax, dword ptr [edi+0x100]									
		cmp     eax,ebp//比较
		//2.1小于则继续执行
		nop
		nop//2.2否则

		//3.计算VMContext与虚拟机堆栈之间的长度
		mov     edx, edi
		mov     ecx, esp
		sub     ecx, edx

		push	esi//保存当前字节码地址														
		mov		esi,esp//esp地址入esi
		sub		esp, 0x60//esp下移0x60
		mov		edi,esp//新的esp地址入edi
		push	edi//保存原始的edi														
		sub		esp, 0x40//esp下移0x40,指向新的VM用的堆栈
		cld
		rep movsb//把esi的内容批量复制到edi中

		pop edi
		pop esi
		VM_END															
	}
}

一些可能会涉及堆栈的Handler在执行后跳转到VCheckESP判断esp是否接近VMContext所在的位置,如果是就将VMContext结构复制到更远的位置存放。

 

3.Handler的设计

这里说的Handler,并不是Windows中的句柄,而是一段小程序,或者说是一段过程,是由VM中的调度器来进行调用的。

Handler分两大类:一类是辅助Handler,另一类是普通Handler。辅助Handler是一些更重要的、更基本的指令;普通Handler的功能是用来执行普通的x86指令的。

 

3.1.辅助Handler

辅助Handler除了VBegin这些维护虚拟机不会导致崩溃的Handler之外,就是专门用来处理堆栈的Handler了。请看下面几个Handler:

// 将32位寄存器压入堆栈中
vPushReg32:
	mov		eax,dword ptr [esi]// 从字节码中得到VMContext的寄存器偏移
	//INT3
	add		esi,4
	mov		eax,dword ptr [edi+eax]//得到寄存器的值
	push	eax//压入寄存器
	VM_END													
// 将32位立即数压入堆栈中
vPushImm32:
	mov		eax,dword ptr [esi]//
	//INT3
	add		esi,4
	push	eax
	VM_END													
//将32位内存数压入堆栈中
vPushMem32:
	mov		edx,0
	mov		ecx,0
	mov		eax,dword ptr [esp]//第1个寄存器偏移
	test 	eax,eax
	cmovge	edx,dword ptr [edi+eax]//如果不是负数则赋值
	mov		eax,dword ptr [esp+4]//第2个寄存器偏移
	test 	eax,eax
	cmovge	ecx,dword ptr [edi+eax]//如果不是负数则赋值
	imul	ecx,dword ptr [esp+8]//第2个寄存器的乘积
	add		ecx,dword ptr [esp+0x0C]//第三个为常量
	add		edx,ecx
	add		esp,0x10//释放参数
	push	edx//插入参数
	VM_END												

有了上述专门处理堆栈的Handler之后,就可以像下图一样设计普通x86指令的Handler了

1.png

  

3.2.普通Handler和指令拆解

上图表达的意思是指令由普通Handler来处理,而源操作数和目的操作数都由堆栈Handler来处理。这样做的好处是,不必为指令的每一种形式都写一个模拟的Handler

例如,add指令的形式通常有“add reg,imm”、“add reg,reg”、“add reg,mem”、“add mem,reg”等写法。如果将操作数都先交给堆栈Handler处理,

那么执行到vadd Handler时,已经是一个立即数存放在堆栈中了,vadd Handler不必去管它从哪里来,只需要用这个立即数做加法操作即可。

先来实现一个vadd的指令:

vadd:
		mov eax, [esp+4]// 取源操作数
		mov ebx, [esp]//取目的操作数
		add ebx,eax
		add esp,8// 平衡堆栈
		push ebx// 压入堆栈

请看下面的指令是如何转换为伪代码的:

add esi, eax

转换为:

vPushReg32 eax_index// eax在VMContext中的偏移,下同
vPushReg32 esi_index
vadd 
vPopReg32  esi_index

再来看这一句:

add esi, 1234

转换为:

vPushImm32 1234
vPushReg32 esi_index
vadd
vPopReg32  esi_index

下面这句转换稍微有点复杂,因为源操作数是一个内存数,而内存数的真实结构是“[imm+reg*scale+reg2]”这样的。

add esi,dword ptr[401000]

本节对Oleh Yuschuk反汇编引擎做了修改,使其可以得到这些信息,具体信息请参考源代码

这行指令可以转换为:

vPushImm32 401000
vPushImm32 -1
vPushImm32 -1
vPushImm32 -1
vPushMem32
vPushReg32 esi_index
vadd
vPopReg32 esi_index

  

3.3.标志位问题

标志位是一个麻烦的问题,稍有不慎就可能导致程序崩溃,并且难以调试。在x86中,涉及标志运算的指令有很多,如adc,add,and,bsf,bsr,bt,btc,btr,bts,cld,cli,cmc,cmovcc,cmp,cmps,cmpxchg,cmpxchg8b,daa,das,dec,div,idiv,imul,inc,jcc,mul,neg,not,or,rcl,rcr,rol,ror,sahf,sal,sar,shl,shr,sbb,scas,setcc,shld,shrd,stc,std,sti,sub,test,xadd等。其中有的指令是设置标志,有的指令是判断标志,所以在相关Handler执行前恢复标志位,执行后保存标志位。举个简单的例子,stc指令是将标志的CF位置为1:

VStc:
	push [edi+0x1c]//把EDI+0x1C压栈
	popfd//恢复原始标志位
	stc//执行stc
	pushfd//重新把fd压栈
	pop[edi+0x1c]//弹出EDI+0x1C
	jmp VMDispatcher

这样操作之后就能保证代码中的标志不会被虚拟机引擎所执行的代码所改变。

 

3.4.相同作用的指令

在x86指令集中,为了提升性能或者其他原因,可以看到一些不同的指令其实可以用同一种指令去实现。比如如下两条指令:

inc esi
add esi,1

虽然它们使用了不同的指令,但是它们的目的是相同的,这样的指令还有sub和dec。另外一些位运算指令也可以相互变换,不过位运算的变换可能会涉及标志位,使标志位的结果不同,因此有的地方指令变换时需要谨慎,但不是大问题。如果将这些x86指令这样化简之后,那么便不用对每个指令都做一个Handler来描述了。

  

3.5.转移指令

转移指令有条件转移、无条件转移、call和retn。这里先讲解前两类转移指令。

实现时可以将esi指向当前字节码的地址,esi指针就好比真实CPU中的eip寄存器,可以通过改写esi寄存器的值来更改流程。无条件跳转jmp的Handler比较简单:

vJmp:
    mov esi, dword ptr[esp]//esp指向要转向的地址
    add esp,4
    jmp VMDispatcher

条件转移jcc指令稍微有一点麻烦,因为它要通过测试标志位来判断是否需要更改流程,不过这里其实可以采取一些技巧。

读者会发现转移指令jcc和条件传输指令cmovcc高度匹配,请看下表的一些比较。

条件转移指令 条件传输指令
jne cmovne
ja cmova
jae cmovae
jb cmovb
jbe cmovbe
je cmove
jg cmovg

基本上所有条件跳转指令都有相应的CMOV指令来匹配,感谢Intel设计出了这样一个指令,这样就好办了。这些指令可以这样设计:

vjne:
		cmovne esi, [esp]
		add esp,4
		jmp VMDispatcher
vja:
		cmova esi, [esp]
		add esp,4
		jmp VMDispatcher

vjb:
		cmovb esi,[esp]
		add esp,4
		jmp VMDispatcher

vjbe:
		cmovbe esi,[esp]
		add esp,4
		jmp VMDispatcher

je:
		cmove esi,[esp]
		add esp,4
		jmp VMDispatcher

jg:
		cmovg esi,[esp]
		add esp,4
		jmp VMDispatcher

字面上稍有不同的一个条件跳转指令是jecxz,对应的指令是cmovz。可以这样设计:

jecxz://ecx为0则跳转
	mov ecx,[edi+0x0c]
	test ecx, ecx
	cmovz esi, eax
	add esp,4
	jmp VMDispatcher

 

3.6.转移跳转指令的另一种实现

上节的条件转移是利用了cmovcc和jcc指令的相似性来模拟跳转,这样做太过简单,并且太过暴露,成了虚拟机的一个鸡肋。

这里有另外一种方法来模拟跳转,条件转移是根据标志位来判断是否需要跳转,那么模拟转移指令时判断标志位就行了。如图所示是80×86的标志寄存器。

1.png

上图描述了标志位在标志寄存器中的位置,现在需要测试的标志位和所在位置都已经知道,就可以模拟条件跳转了。下面以jae指令为例:

vJAE:
		push [edi+0x1c]
		pop eax
		add eax,1// CF位
		cmove esi,[esp]
		add esp,4
		jmp VMDispatcher

这样调用这条指令:

vPush jumptoaddr//要跳转的地址
vJae

这个指令首先得到标志位,然后和1做and运算(取CF位),cmove指令是判断ZF标志是否为0,为0就改变esi指向的地址。jae只是判断CF位,这里再以jbe做一个例子:

vJBE:
	push [edi+0x1c]
	pop eax
	add eax,0x41//1001B
	cmp eax,0x41//CF为1a或ZF为1
	cmove esi,[esp]
	add esp,4
	jmp VMDispatcher

其他的跳转指令的实现也只是检测其他不同的标志位,没有太多的不同。

  

3.7.call指令

call和retn指令虽然也是转移指令,但是因为它们的功能不一样,所以被分开讲解。首先,虚拟机设计为只在一个堆栈层次上运行。请看如下代码:

	mov eax,1234
	push 1234
	call anotherfun
theNext:
	add esp,4

其中第1、2、4条指令都是在当前堆栈层次上执行的,而call anotherfunc是调用子函数,会将控制权移交给另外的代码,这些代码是不受虚拟机控制的。所以碰到call指令,必须退出虚拟机,让子函数在真实CPU中执行完毕后再交回给虚拟机执行下一句指令。看起来,vcall这个Handler设计起来稍微有点麻烦,其实不然,call指令先压入下一句汇编代码的地址,然后跳到目标函数。下面的代码和它等同:

push theNext
jmp anotherfunc

如果想在退出虚拟机后让anotherfunc这个函数返回后再次拿回控制权,可以更改返回地址,来达到继续接管代码的操作。在一个地址写上这样的代码:

theNextVM:
    push theNextByteCode
    jmp VStartVM

这是一个重新进入虚拟机的代码,theNextByteCode代表theNext之后的代码字节码。只需要将theNext的地址改为theNextVM的地址,即可完美地模拟call指令了。vcall的伪代码如下:

vcall:
	push all vreg// 所有虚拟寄存器
	pop all reg//弹出到真实寄存器中
	push 返回地址
	push 要调用的函数地址
	retn

  

3.8.retn指令

retn指令和其他普通指令不一样,retn在这里被虚拟机认为是一个退出函数。retn有两种写法:一种是不带操作数的;另一种是带操作数的。比如:

retn
retn 4

第一种retn形式先得到当前esp中存放的返回地址,然后再释放返回地址的堆栈并跳转到返回地址;第二种比前一种多了一个步骤,即在释放返回地址的堆栈时再释放操作数的空间。vRetn的Handler设计如下:

vRetn:
	xor eax,eax
	mov ax,word ptr[esi]// retn的操作数是word型,最大只有0xffff
	add esi,2
	mov ebx,dword ptr[ebp]// 得到要返回的地址
	add ebp,4// 释放空间
	add ebp, eax//如果有操作数,同样释放
	push ebx//压入返回地址
	push ebp//压入堆栈指针

	// 把原始的寄存器恢复
	push [edi+0x1c]
	push [edi+0x18]
	push [edi+0x14]
	push [edi+0x10]
	push [edi+0x0c]
	push [edi+0x08]
	push [edi+0x04]
	push [edi]
	pop eax
	pop ebx
	pop ecx
	pop edx
	pop esi
	pop edi
	pop ebp
	popfd
	pop esp// 还原堆栈到esp中,而VMContext也算是自动销毁了
	retn

 

3.9.不可模拟指令

不可模拟的指令前面也有提及,在这里任何不能识别的指令都可将其划分为不可模拟指令,碰到这类指令时,只能与vcall使用一种方法,即先退出虚拟机,执行这个指令,然后再压入下一个字节码的地址,重新进入虚拟机。

 

4.托管代码的异常处理

如果只是通过模拟跳转指令就想控制程序的流程是不够的,因为还有一种会打乱流程的情况,那就是异常处理。因此必须挟持原有的异常处理,才能绝对地控制程序的流程执行。关于编译器级的SEH更详细的资料,请参考其他文献。异常处理是不太可能完美解决的,只能针对编译器来进行模拟。

  

4.1.VC++的异常处理

VC编译器已经将Win32异常处理封装。如图所示是VC 7编译器生成的栈帧布局。Scopetable是一个记录(record)的数组,每个record描述了一个_try块,以及块之间的关系。

1.png

Scopetable的结构如下:

	_SCOPETABLE_ENTRY
	{
		DWORD EnclosingLevel;
		void* FilterFunc;
		void* HandlerFunc;
	}

MSVC 2005的编译器为SEH帧增加了一些缓冲区溢出保护。完整的栈帧布局如下图所示。

1.png

两者基本一样,只是加了一个Cookie头。没有什么不同,只要找对Scopetable的偏移就行了。

请看下面这样一个代码例子:

void func1(char* str)
{
	char buf[12];
	__try// try block 0
	{
		__try// try block 1
		{

		}
		__except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION)
		{
			printf("Access violation");

		}
		strcpy(buf,str);
	}
	__finally
	{
		puts("in finally");
	}
}

下面是对应的反汇编代码:

push    ebp
mov     ebp, esp
push    0FFFFFFFEh
push    offset stru_402218
push    offset __except_handler4
mov     eax, large fs:0
push    eax
add     esp, 0FFFFFFD4h
mov     eax, ___security_cookie
xor     [ebp+ms_exc.registration.ScopeTable], eax
xor     eax, ebp
mov     [ebp+var_1C], eax
push    ebx
push    esi
push    edi
push    eax
lea     eax, [ebp+ms_exc.registration]
mov     large fs:0, eax
mov     [ebp+ms_exc.old_esp], esp
mov     [ebp+ms_exc.registration.TryLevel], 0
mov     [ebp+ms_exc.registration.TryLevel], 1
mov     [ebp+ms_exc.registration.TryLevel], 0
jmp     short loc_40107F

异常处理函数:

$LN12:                  ; Exception handler 1 for function 401000
mov     esp, [ebp+ms_exc.old_esp]
push    offset Format   ; "Access violation"
call    ds:__imp__printf
add     esp, 4
mov     [ebp+ms_exc.registration.TryLevel], 0
mov     [ebp+ms_exc.registration.TryLevel], 0
mov     [ebp+ms_exc.registration.TryLevel], 1

上面两句则代表进入try block 0后,又进入了try block 1,当出现异常后,ExceptionHandler处理程序被执行,然后ExceptionHandler处理程序通过trylevel找到指向的try块在pScopeTable数组中搜索异常处理程序,即pScopeTable[trylevel].FilterFunc或pScopeTable[trylevel].HandlerFunc。现在好办了,知道了pScopeTable数组之后,就可以得到每一个异常处理程序的真实地址了。但还有一个小问题,现在并不知道pScopeTable数组有多少项,或者说并不知道有多少个try block。有两种方法来得到数组大小。第一种:暴力搜索pScopeTable,一直找到后面有一项的FilterFunc和HandlerFunc都为错误的地址时,就可以确定数组大小了;第二种:使用_trylevel的某种特征,比如通常情况下为-1(SEH3),通常所在的位置在ebp-4处,也可以通过计算异常代码和堆栈位置相互的关系来确定_trylevel的堆栈位置,找出所有对其赋值的常数,最大的那个常数应该就是数组的大小了。第一种简单有效,第二种比较复杂,且都不一定可靠,也不一定只有这两种办法,所谓八仙过海,各显神通了。

找到了_func1_scopetable数组中所有异常处理函数的地址后,就可以为每一个异常处理函数生成一个托管过程的代码,比如为func1 FilterFunc生成一个托管代码如下:

fuc1_FilterFunc_Stub:
    push FilterFunc_ByteCode_Addr//过滤函数的字节码地址
    jmp StartVM

然后将scopetable->FilterFunc的地址替换为func1_FilterFunc_Stub的地址,当出现异常时就会被调用,这时就再次进入了虚拟机,执行FilterFunc的字节码了。HandlerFunc也是一样。

  

5.小结

这个虚拟机是将汇编指令转换成字节码来模拟执行的,因为汇编指令和字节码之间的特性不同,使得并不能完美地模拟汇编指令,这也是为什么无法将直接用汇编写的比较具有技巧性的代码成功转换为字节码执行的原因。另外,还有一些指令,比如jmp eax,这种代码比较模糊,并不确定要跳转到哪个地址。碰到这种指令,的确没有什么好办法,但是好在高级语言编译器似乎也不会编译出这种代码。

本章主要对虚拟机的大概框架、Handler设计、指令拆解和异常处理挟持作了详细的描述,而对于如何使用高级语言去实现并未作过多讲解,因为关于代码的设计不属于本章的范畴。

文章作者:hgy413
本文地址:http://hgy413.com/3123.html
版权所有 © 转载时必须以链接形式注明作者和原始出处!

12 Responses to “VMP-原理(看雪笔记)”

  1. #1 minecraft 回复 | 引用 Post:2018-10-04 17:28

    Awesome things here. I am very glad to see your article.
    Thanks so much and I am taking a look ahead to touch you.
    Will you kindly drop me a e-mail?

  2. #2 minecraft 回复 | 引用 Post:2018-10-05 12:42

    Thanks for sharing your thoughts. I really appreciate
    your efforts and I will be waiting for your next write ups thank you once again.

  3. Hi there friends, how is everything, and what you wish for to say
    concerning this paragraph, in my view its really remarkable in support of me.

  4. Saved as a favorite, I really like your site!

  5. Hi there very cool blog!! Guy .. Beautiful .. Amazing .. I will bookmark
    your web site and take the feeds additionally? I’m satisfied to search out
    numerous helpful information here in the submit, we’d like
    develop more techniques in this regard, thanks for sharing.

    . . . . .

  6. #6 Coconut Oil Benefits 回复 | 引用 Post:2018-10-23 02:28

    I’m not sure where you’re getting your info, but good
    topic. I needs to spend some time learning more
    or understanding more. Thanks for excellent information I was looking for this info for my mission.

  7. #7 Benefits of Coconut Oil 回复 | 引用 Post:2018-10-25 10:56

    Hello there, just became alert to your blog through Google,
    and found that it’s really informative. I’m going to watch out for brussels.
    I’ll appreciate if you continue this in future.
    Numerous people will be benefited from your writing. Cheers!

  8. #8 Coconut Oil Benefits 回复 | 引用 Post:2018-10-29 20:15

    Hmm it seems like your blog ate my first comment (it was extremely long) so I guess I’ll just
    sum it up what I wrote and say, I’m thoroughly enjoying your blog.
    I too am an aspiring blog writer but I’m still new
    to the whole thing. Do you have any suggestions for inexperienced blog writers?
    I’d certainly appreciate it.

  9. #9 Quest Bars 回复 | 引用 Post:2018-11-09 02:48

    I am really pleased to read this blog posts which contains lots
    of helpful facts, thanks for providing such data.

  10. #10 Sling TV 回复 | 引用 Post:2018-11-10 20:24

    Hey there! Do you use Twitter? I’d like to follow you if that would be okay.
    I’m undoubtedly enjoying your blog and look forward to
    new posts.

  11. #11 Sling TV 回复 | 引用 Post:2018-11-11 20:53

    We are a group of volunteers and starting a new scheme in our community.
    Your site provided us with valuable information to work on. You
    have done an impressive job and our whole community will
    be grateful to you.

  12. Its not my first time to visit this web page, i am visiting this site dailly and get
    pleasant information from here everyday.

发表评论