本文讲的是Windows Shellcode学习笔记——Shellcode的提取与测试

0x00 前言

之前在《Windows Shellcode学习笔记——通过VisualStudio生成shellcode》介绍了使用C++编写(不使用内联汇编),实现动态获取API地址并调用,对其反汇编提取shellcode的方法,并开源了测试代码。

接下来在对shellcode进行提取的过程中,发现了当时开源代码的一些bug,所以本文着重解决测试代码的bug,并介绍使用C++开发shellcode需要考虑的一些问题。

存在bug的测试代码下载地址:

https://github.com/3gstudent/Shellcode-Generater/blob/master/shellcode.cpp

0x01 简介

简单的shellcode提取流程:

使用c++开发代码
更改VisualStudio编译配置
生成exe
在IDA下打开生成的exe,获得机器码

由于是动态获取API地址并调用,所以为了保证shellcode的兼容性,代码中不能出现固定地址,并且要尽量避免使用全局变量,如果代码中包含子函数,根据调用方式,还有注意各个函数之间的排列顺序(起始函数放于最前)

0x02 Bug修复

配置三个编译选项:release、禁用优化、禁用/GS

将代码编译,然后使用IDA提取机器码作为shellcode

在实际调试过程中,发现代码存在bug:

1、代码中应合理处理全局变量

在代码中使用全局变量

FARPROC(WINAPI* GetProcAddressAPI)(HMODULE, LPCSTR);
HMODULE(WINAPI* LoadLibraryWAPI)(LPCWSTR);

在编译后会成为一个固定地址,导致shellcode无法兼容不同环境

最简单直接的方式是在shellcode中尽量避免全局变量

2、函数声明方式需要修改

修改全局变量后,以下代码需要修改:

MESSAGEBOXA_INITIALIZE MeassageboxA_MyOwn = reinterpret_cast<MESSAGEBOXA_INITIALIZE>(GetProcAddressAPI(LoadLibraryWAPI(struser32), MeassageboxA_api));
MeassageboxA_MyOwn(NULL, NULL, NULL, 0);

需要全部换成typedef的函数声明方式

3、函数调用顺序

如果使用以下方式加载shellcode:

(*(int(*)()) sc)();

起始函数的定义应该位于这段shellcode的最前面(和函数声明的顺序无关)

注:

shellcode如果包含子函数,应该保证各个函数放在一段连续的地址中,并且起始函数置于最前面,这样在提取机器码后,可以直接加载起始函数执行shellcode

综上,给出新的完整代码:

#include <windows.h>
#include <Winternl.h>
#pragma optimize( "", off )
void shell_code();
HANDLE GetKernel32Handle();
BOOL __ISUPPER__(__in CHAR c);
CHAR __TOLOWER__(__in CHAR c);
UINT __STRLEN__(__in LPSTR lpStr1);
UINT __STRLENW__(__in LPWSTR lpStr1);
LPWSTR __STRSTRIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2);
INT __STRCMPI__(__in LPSTR lpStr1, __in LPSTR lpStr2);
INT __STRNCMPIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2, __in DWORD dwLen);
LPVOID __MEMCPY__(__in LPVOID lpDst, __in LPVOID lpSrc, __in DWORD dwCount);
typedef FARPROC(WINAPI* GetProcAddressAPI)(HMODULE, LPCSTR);
typedef HMODULE(WINAPI* LoadLibraryWAPI)(LPCWSTR);
typedef ULONG (WINAPI *MESSAGEBOXAPI)(HWND, LPWSTR, LPWSTR, ULONG);
void shell_code() {LoadLibraryWAPI loadlibrarywapi = 0;GetProcAddressAPI getprocaddressapi=0;MESSAGEBOXAPI messageboxapi=0;wchar_t struser32[] = { L'u', L's', L'e', L'r', L'3',L'2', L'.', L'd', L'l', L'l', 0 };char MeassageboxA_api[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'A', 0 };HANDLE hKernel32 = GetKernel32Handle();if (hKernel32 == INVALID_HANDLE_VALUE) {return;}LPBYTE lpBaseAddr = (LPBYTE)hKernel32;PIMAGE_DOS_HEADER lpDosHdr = (PIMAGE_DOS_HEADER)lpBaseAddr;PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)(lpBaseAddr + lpDosHdr->e_lfanew);PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)(lpBaseAddr + pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);LPDWORD pNameArray = (LPDWORD)(lpBaseAddr + pExportDir->AddressOfNames);LPDWORD pAddrArray = (LPDWORD)(lpBaseAddr + pExportDir->AddressOfFunctions);LPWORD pOrdArray = (LPWORD)(lpBaseAddr + pExportDir->AddressOfNameOrdinals);CHAR strLoadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'W', 0x0 };CHAR strGetProcAddress[] = { 'G', 'e', 't', 'P', 'r', 'o', 'c', 'A', 'd', 'd', 'r', 'e', 's', 's', 0x0 };for (UINT i = 0; i < pExportDir->NumberOfNames; i++) {LPSTR pFuncName = (LPSTR)(lpBaseAddr + pNameArray[i]);if (!__STRCMPI__(pFuncName, strGetProcAddress)) {getprocaddressapi=(GetProcAddressAPI)(lpBaseAddr + pAddrArray[pOrdArray[i]]);}else if (!__STRCMPI__(pFuncName, strLoadLibraryA)) {loadlibrarywapi=(LoadLibraryWAPI) (lpBaseAddr + pAddrArray[pOrdArray[i]]);}if (getprocaddressapi != nullptr && loadlibrarywapi != nullptr) {               messageboxapi=(MESSAGEBOXAPI)getprocaddressapi(loadlibrarywapi(struser32), MeassageboxA_api);messageboxapi(NULL, NULL, NULL, 0);return;}}
}
inline BOOL __ISUPPER__(__in CHAR c) {return ('A' <= c) && (c <= 'Z');
};
inline CHAR __TOLOWER__(__in CHAR c) {return __ISUPPER__(c) ? c - 'A' + 'a' : c;
};
UINT __STRLEN__(__in LPSTR lpStr1)
{UINT i = 0;while (lpStr1[i] != 0x0)i++;return i;
}
UINT __STRLENW__(__in LPWSTR lpStr1)
{UINT i = 0;while (lpStr1[i] != L'')i++;return i;
}
LPWSTR __STRSTRIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2)
{CHAR c = __TOLOWER__(((PCHAR)(lpStr2++))[0]);if (!c)return lpStr1;UINT dwLen = __STRLENW__(lpStr2);do{CHAR sc;do{sc = __TOLOWER__(((PCHAR)(lpStr1)++)[0]);if (!sc)return NULL;} while (sc != c);} while (__STRNCMPIW__(lpStr1, lpStr2, dwLen) != 0);return (lpStr1 - 1); // FIXME -2 ?
}
INT __STRCMPI__(__in LPSTR lpStr1,__in LPSTR lpStr2)
{int  v;CHAR c1, c2;do{c1 = *lpStr1++;c2 = *lpStr2++;// The casts are necessary when pStr1 is shorter & char is signed v = (UINT)__TOLOWER__(c1) - (UINT)__TOLOWER__(c2);} while ((v == 0) && (c1 != '') && (c2 != ''));return v;
}
INT __STRNCMPIW__(__in LPWSTR lpStr1,__in LPWSTR lpStr2,__in DWORD dwLen)
{int  v;CHAR c1, c2;do {dwLen--;c1 = ((PCHAR)lpStr1++)[0];c2 = ((PCHAR)lpStr2++)[0];/* The casts are necessary when pStr1 is shorter & char is signed */v = (UINT)__TOLOWER__(c1) - (UINT)__TOLOWER__(c2);} while ((v == 0) && (c1 != 0x0) && (c2 != 0x0) && dwLen > 0);return v;
}
LPSTR __STRCAT__(__in LPSTR  strDest,__in LPSTR strSource)
{LPSTR d = strDest;LPSTR s = strSource;while (*d) d++;do { *d++ = *s++; } while (*s);*d = 0x0;return strDest;
}
LPVOID __MEMCPY__(__in LPVOID lpDst,__in LPVOID lpSrc,__in DWORD dwCount)
{LPBYTE s = (LPBYTE)lpSrc;LPBYTE d = (LPBYTE)lpDst;while (dwCount--)*d++ = *s++;return lpDst;
}
HANDLE GetKernel32Handle() {HANDLE hKernel32 = INVALID_HANDLE_VALUE;
#ifdef _WIN64PPEB lpPeb = (PPEB)__readgsqword(0x60);
#elsePPEB lpPeb = (PPEB)__readfsdword(0x30);
#endifPLIST_ENTRY pListHead = &lpPeb->Ldr->InMemoryOrderModuleList;PLIST_ENTRY pListEntry = pListHead->Flink;WCHAR strDllName[MAX_PATH];WCHAR strKernel32[] = { 'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', L'' };while (pListEntry != pListHead) {PLDR_DATA_TABLE_ENTRY pModEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);if (pModEntry->FullDllName.Length) {DWORD dwLen = pModEntry->FullDllName.Length;__MEMCPY__(strDllName, pModEntry->FullDllName.Buffer, dwLen);strDllName[dwLen / sizeof(WCHAR)] = L'';if (__STRSTRIW__(strDllName, strKernel32)) {hKernel32 = pModEntry->DllBase;break;}}pListEntry = pListEntry->Flink;}return hKernel32;
}
int main()
{printf("1");shell_code();printf("2");return 0;
}

0x03 Shellcode提取

将以上代码编译成exe后使用IDA打开,查看Function Window,找到各子函数起始地址

如图

可以看到各个函数保存在一段连续的地址,并且shellcode起始函数位于最开始

双击第一个函数shell_code(void),进入IDA文本视图,可查看shell_code(void)函数具体在exe文件中的位置为00000400

如图

查看main函数在exe文件中的位置为00000A00

如图

结合c代码的结构,推断出在exe文件中的偏移范围00000400-00000A00即为我们需要的机器码

使用十六进制编辑器将其中的机器码提取并保存到文件中,文件中的内容即我们需要的shellcode

当然,以上手动提取机器码并保存到文件的功能可通过程序自动实现,完整代码如下:

#include <stdafx.h>
#include <windows.h>
#include <Winternl.h>
#pragma optimize( "", off )
void shell_code();
HANDLE GetKernel32Handle();
BOOL __ISUPPER__(__in CHAR c);
CHAR __TOLOWER__(__in CHAR c);
UINT __STRLEN__(__in LPSTR lpStr1);
UINT __STRLENW__(__in LPWSTR lpStr1);
LPWSTR __STRSTRIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2);
INT __STRCMPI__(__in LPSTR lpStr1, __in LPSTR lpStr2);
INT __STRNCMPIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2, __in DWORD dwLen);
LPVOID __MEMCPY__(__in LPVOID lpDst, __in LPVOID lpSrc, __in DWORD dwCount);
typedef FARPROC(WINAPI* GetProcAddressAPI)(HMODULE, LPCSTR);
typedef HMODULE(WINAPI* LoadLibraryWAPI)(LPCWSTR);
typedef ULONG (WINAPI *MESSAGEBOXAPI)(HWND, LPWSTR, LPWSTR, ULONG);
void shell_code() {LoadLibraryWAPI loadlibrarywapi = 0;GetProcAddressAPI getprocaddressapi=0;MESSAGEBOXAPI messageboxapi=0;wchar_t struser32[] = { L'u', L's', L'e', L'r', L'3',L'2', L'.', L'd', L'l', L'l', 0 };char MeassageboxA_api[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x', 'A', 0 };HANDLE hKernel32 = GetKernel32Handle();if (hKernel32 == INVALID_HANDLE_VALUE) {return;}LPBYTE lpBaseAddr = (LPBYTE)hKernel32;PIMAGE_DOS_HEADER lpDosHdr = (PIMAGE_DOS_HEADER)lpBaseAddr;PIMAGE_NT_HEADERS pNtHdrs = (PIMAGE_NT_HEADERS)(lpBaseAddr + lpDosHdr->e_lfanew);PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)(lpBaseAddr + pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);LPDWORD pNameArray = (LPDWORD)(lpBaseAddr + pExportDir->AddressOfNames);LPDWORD pAddrArray = (LPDWORD)(lpBaseAddr + pExportDir->AddressOfFunctions);LPWORD pOrdArray = (LPWORD)(lpBaseAddr + pExportDir->AddressOfNameOrdinals);CHAR strLoadLibraryA[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'W', 0x0 };CHAR strGetProcAddress[] = { 'G', 'e', 't', 'P', 'r', 'o', 'c', 'A', 'd', 'd', 'r', 'e', 's', 's', 0x0 };for (UINT i = 0; i < pExportDir->NumberOfNames; i++) {LPSTR pFuncName = (LPSTR)(lpBaseAddr + pNameArray[i]);if (!__STRCMPI__(pFuncName, strGetProcAddress)) {getprocaddressapi=(GetProcAddressAPI)(lpBaseAddr + pAddrArray[pOrdArray[i]]);}else if (!__STRCMPI__(pFuncName, strLoadLibraryA)) {loadlibrarywapi=(LoadLibraryWAPI) (lpBaseAddr + pAddrArray[pOrdArray[i]]);}if (getprocaddressapi != nullptr && loadlibrarywapi != nullptr) {               messageboxapi=(MESSAGEBOXAPI)getprocaddressapi(loadlibrarywapi(struser32), MeassageboxA_api);messageboxapi(NULL, NULL, NULL, 0);return;}}
}
inline BOOL __ISUPPER__(__in CHAR c) {return ('A' <= c) && (c <= 'Z');
};
inline CHAR __TOLOWER__(__in CHAR c) {return __ISUPPER__(c) ? c - 'A' + 'a' : c;
};
UINT __STRLEN__(__in LPSTR lpStr1)
{UINT i = 0;while (lpStr1[i] != 0x0)i++;return i;
}
UINT __STRLENW__(__in LPWSTR lpStr1)
{UINT i = 0;while (lpStr1[i] != L'')i++;return i;
}
LPWSTR __STRSTRIW__(__in LPWSTR lpStr1, __in LPWSTR lpStr2)
{CHAR c = __TOLOWER__(((PCHAR)(lpStr2++))[0]);if (!c)return lpStr1;UINT dwLen = __STRLENW__(lpStr2);do{CHAR sc;do{sc = __TOLOWER__(((PCHAR)(lpStr1)++)[0]);if (!sc)return NULL;} while (sc != c);} while (__STRNCMPIW__(lpStr1, lpStr2, dwLen) != 0);return (lpStr1 - 1); // FIXME -2 ?
}
INT __STRCMPI__(__in LPSTR lpStr1,__in LPSTR lpStr2)
{int  v;CHAR c1, c2;do{c1 = *lpStr1++;c2 = *lpStr2++;// The casts are necessary when pStr1 is shorter & char is signed v = (UINT)__TOLOWER__(c1) - (UINT)__TOLOWER__(c2);} while ((v == 0) && (c1 != '') && (c2 != ''));return v;
}
INT __STRNCMPIW__(__in LPWSTR lpStr1,__in LPWSTR lpStr2,__in DWORD dwLen)
{int  v;CHAR c1, c2;do {dwLen--;c1 = ((PCHAR)lpStr1++)[0];c2 = ((PCHAR)lpStr2++)[0];/* The casts are necessary when pStr1 is shorter & char is signed */v = (UINT)__TOLOWER__(c1) - (UINT)__TOLOWER__(c2);} while ((v == 0) && (c1 != 0x0) && (c2 != 0x0) && dwLen > 0);return v;
}
LPSTR __STRCAT__(__in LPSTR  strDest,__in LPSTR strSource)
{LPSTR d = strDest;LPSTR s = strSource;while (*d) d++;do { *d++ = *s++; } while (*s);*d = 0x0;return strDest;
}
LPVOID __MEMCPY__(__in LPVOID lpDst,__in LPVOID lpSrc,__in DWORD dwCount)
{LPBYTE s = (LPBYTE)lpSrc;LPBYTE d = (LPBYTE)lpDst;while (dwCount--)*d++ = *s++;return lpDst;
}
HANDLE GetKernel32Handle() {HANDLE hKernel32 = INVALID_HANDLE_VALUE;
#ifdef _WIN64PPEB lpPeb = (PPEB)__readgsqword(0x60);
#elsePPEB lpPeb = (PPEB)__readfsdword(0x30);
#endifPLIST_ENTRY pListHead = &lpPeb->Ldr->InMemoryOrderModuleList;PLIST_ENTRY pListEntry = pListHead->Flink;WCHAR strDllName[MAX_PATH];WCHAR strKernel32[] = { 'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l', L'' };while (pListEntry != pListHead) {PLDR_DATA_TABLE_ENTRY pModEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);if (pModEntry->FullDllName.Length) {DWORD dwLen = pModEntry->FullDllName.Length;__MEMCPY__(strDllName, pModEntry->FullDllName.Buffer, dwLen);strDllName[dwLen / sizeof(WCHAR)] = L'';if (__STRSTRIW__(strDllName, strKernel32)) {hKernel32 = pModEntry->DllBase;break;}}pListEntry = pListEntry->Flink;}return hKernel32;
}
void __declspec(naked) END_SHELLCODE(void) {}
int main()
{shell_code();FILE *output_file;fopen_s(&output_file,"shellcode.bin", "wb");fwrite(shell_code, (int)END_SHELLCODE - (int)shell_code, 1, output_file);fclose(output_file);return 0;
}

注:

打开文件需要以”wb”模式打开二进制文件

如果以”w”模式,写入文件的过程中,0A字符会被替换为0D0A,导致shellcode出现问题

0x04 Shellcode测试

使用以下代码可读取文件中保存的shellcode,加载并测试其功能:

#include <windows.h>
size_t GetSize(char * szFilePath)
{size_t size;FILE* f = fopen(szFilePath, "rb");fseek(f, 0, SEEK_END);size = ftell(f);rewind(f);fclose(f);return size;
}
unsigned char* ReadBinaryFile(char *szFilePath, size_t *size)
{unsigned char *p = NULL;FILE* f = NULL;size_t res = 0;*size = GetSize(szFilePath);if (*size == 0) return NULL;        f = fopen(szFilePath, "rb");if (f == NULL){printf("Binary file does not exists!n");return 0;}p = new unsigned char[*size];rewind(f);res = fread(p, sizeof(unsigned char), *size, f);fclose(f);if (res == 0){delete[] p;return NULL;}return p;
}
int main(int argc, char* argv[])
{char *szFilePath="c:testshellcode.bin";unsigned char *BinData = NULL;size_t size = 0;    BinData = ReadBinaryFile(szFilePath, &size);void *sc = VirtualAlloc(0, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);if (sc == NULL) return 0;   memcpy(sc, BinData, size);(*(int(*)()) sc)(); return 0;
}
原文发布时间为:2017年3月15日
本文作者:3gstudent
本文来自云栖社区合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。
原文链接

Windows Shellcode学习笔记——Shellcode的提取与测试相关推荐

  1. php shellcode,Windows Shellcode学习笔记

    0x00 前言 Windows Shellcode学习笔记--通过VisualStudio生成shellcode,shellcode是一段机器码,常用作漏洞利用中的载荷(也就是payload). 在渗 ...

  2. Windows异常学习笔记(五)—— 未处理异常

    Windows异常学习笔记(五)-- 未处理异常 要点回顾 最后一道防线 实验一:理解最后一道防线 实验二:新线程的最后一道防线 总结 UnhandledExceptionFilter 实验三:理解U ...

  3. Windows异常学习笔记(四)—— 编译器扩展SEH

    Windows异常学习笔记(四)-- 编译器扩展SEH 要点回顾 编译器支持的SEH 过滤表达式 实验一:理解_try_except 实验二:_try_except 嵌套 拓展SEH结构体 scope ...

  4. Windows异常学习笔记(二)—— 内核异常处理流程用户异常的分发

    Windows异常学习笔记(二)-- 内核异常处理流程&用户异常分发 用户层与内核层异常 内核异常 分析 KiDispatchException 分析 RtlDispatchException ...

  5. Windows异常学习笔记(一)—— CPU异常记录模拟异常记录

    Windows异常学习笔记(一)-- CPU异常记录 基础知识 异常的分类 CPU异常 分析中断处理函数 _KiTrap00 分析 CommonDispatchException 总结 软件模拟异常 ...

  6. Windows APC学习笔记(二)—— 挂入过程执行过程

    Windows APC学习笔记(二)-- 挂入过程&执行过程 基础知识 挂入过程 KeInitializeApc ApcStateIndex KiInsertQueueApc Alertabl ...

  7. Windows APC学习笔记(一)—— APC的本质备用APC队列

    Windows APC学习笔记(一)-- APC的本质&备用APC队列 基础知识 APC的本质 APC队列 APC结构 分析 KiServiceExit 总结 备用APC队列 挂靠环境下Apc ...

  8. Windows系统调用学习笔记(四)—— 系统服务表SSDT

    Windows系统调用学习笔记(四)-- 系统服务表&SSDT 要点回顾 系统服务表 实验:分析 KiSystemService 与 KiFastCallEntry 共同代码 SSDT 实验: ...

  9. Windows系统调用学习笔记(三)—— 保存现场

    Windows系统调用学习笔记(三)-- 保存现场 要点回顾 基本概念 Trap Frame 结构 线程相关的结构体 ETHREAD KTHREAD CPU相关的结构体 KPCR _NT_TIB KP ...

  10. Windows系统调用学习笔记(二)—— 3环进0环

    Windows系统调用学习笔记(二)-- 3环进0环 要点回顾 基本概念 _KUSER_SHARED_DATA 0x7FFE0300 实验:判断CPU是否支持快速调用 第一步:修改EAX=1 第二步: ...

最新文章

  1. python3 gzip 压缩/解压
  2. linux进程故障如何修复,33.Linux开机过程及启动故障修复
  3. Kubernetes—常用命令总结(二)
  4. ideahtml标签不提示_仓储物流加速,电子标签亮灯拣选系统的优势
  5. MIT 18.03 写给初学者的微积分校对活动 | ApacheCN
  6. IDEA:IDEA采取debug的时候卡死-不报错
  7. 黄刘生--数据结构--答案 2
  8. 阿里云十年新战略发布!达摩院加持,阿里技术全部开放,20亿补贴小程序
  9. 科大讯飞语音识别demo
  10. CCS以及DSP入门帖
  11. 云开发地铁路线图小程序源码和配置教程
  12. Dango Web 开发指南 学习笔记 1
  13. 计算机455端口,455端口怎么关闭-455端口关闭的方法 - 河东软件园
  14. Matlab绘图保存为.fig格式以使用,及.fig文件的加载与数据读取
  15. 如何做番茄炖牛腩——hadoop理解
  16. 【DVB】采用DVB-T或DVB-T2的国家达166个
  17. CentOS7下使用YUM安装MySQL5.6
  18. webkit笑傲江湖,悲乎?乐乎?
  19. VS C++ error LNK2005 1169报错
  20. 谷歌seo独立站搜索引擎优化指南【2023新版】

热门文章

  1. jquery中has方法
  2. 兼容性所有浏览器的透明CSS设置
  3. 对“才鸟”——动态显示扩展数据的改写
  4. 苹果电脑程序坞不见了?怎样隐藏与显示电脑Dock栏
  5. TeamViewer会议功能有什么用?
  6. 关于EasyRecovery的一些高级设置
  7. 吉他谱怎么看?看谱大攻略送上!
  8. Flask框架-模板
  9. java进程的守护进程脚本
  10. Android 中文 API——android.widget合集(中)(50篇)(chm格式)