CComPtr 和 CComQIPtr

问题

CComPtr 和 CComQIPtr 长得这么像,有啥关系和区别?

分析

  1. 看代码 CComQIPtr 继承自 CComPtr,CComPtr<IUnknown> 没问题,但 CComQIPtr<IUnknown> 报错,应该使用 CComQIPtr<IUnknown, &IID_IUnknown>。

  2. 不同类型 CComPtr<> 不能直接互相构造/赋值;CComQIPtr<> 则可以,因为 CComQIPtr 会进行目标类型的 QueryInterface。

1
2
3
4
5
CComPtr<IUnknown> u;
// ...
CComPtr<IDispatch> d(u); // error

CComQIPtr<IDispatch> d(u); // right, will call QueryInterface

  1. 两者构造/赋值时,都会进行 AddRef,如果不想 AddRef,可以使用裸指针(必须十分清楚自己在干嘛!)。

1
2
3
4
5
6
7
CComPtr<IUnknown> u;
// ...
CComPtr<IUnknown> u1(u); // will call AddRef

CComQIPtr<IDispatch> d(u); // will call QueryInterface(call AddRef impliedly)

auto raw = static_cast<IDispatch*>(u.p); // won't call AddRef

总结

  • 您可以忘记 CComPtr,只使用 CComQIPtr;

  • 或者,尽量使用 CComPtr,只在必要时使用 CComQIPtr。

测试代码

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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <iostream>

#include <atlbase.h>
#include <atlcom.h>
#include <atlstr.h>

MIDL_INTERFACE("00554d55-0000-0000-C000-000000000041")
IA : public IUnknown {
public:
virtual HRESULT STDMETHODCALLTYPE FuncA() = 0;
};

MIDL_INTERFACE("00554d55-0000-0000-C000-000000000042")
IB : public IA {
public:
virtual HRESULT STDMETHODCALLTYPE FuncB() = 0;
};

class A : public IA {
public:
~A() { std::cout << __FUNCTION__ << "\n"; }

HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
*ppvObject = this;
std::cout << __FUNCTION__ << "\n";
AddRef();
return S_OK;
}

ULONG STDMETHODCALLTYPE AddRef(void) {
++ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
return ref_;
}

ULONG STDMETHODCALLTYPE Release(void) {
--ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
if (0 == ref_) {
delete this;
}
return ref_;
}

HRESULT STDMETHODCALLTYPE FuncA() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

private:
int ref_ = 0;
};

class B : public IB {
public:
~B() { std::cout << __FUNCTION__ << "\n"; }

HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
*ppvObject = this;
std::cout << __FUNCTION__ << "\n";
AddRef();
return S_OK;
}

ULONG STDMETHODCALLTYPE AddRef(void) {
++ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
return ref_;
}

ULONG STDMETHODCALLTYPE Release(void) {
--ref_;
std::cout << __FUNCTION__ << ": " << ref_ << "\n";
if (0 == ref_) {
delete this;
}
return ref_;
}

HRESULT STDMETHODCALLTYPE FuncA() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

HRESULT STDMETHODCALLTYPE FuncB() {
std::cout << __FUNCTION__ << "\n";
return S_OK;
}

private:
int ref_ = 0;
};

HRESULT CreateObject(REFIID riid,
_COM_Outptr_ void __RPC_FAR* __RPC_FAR* ppvObject) {
if (__uuidof(IA) == riid || __uuidof(IUnknown) == riid) {
auto p = new A;
p->QueryInterface(riid, ppvObject);
return S_OK;
} else if (__uuidof(IB) == riid) {
auto p = new B;
p->QueryInterface(riid, ppvObject);
return S_OK;
}
return E_NOINTERFACE;
}

int main() {
{
IUnknown* u;
HRESULT hr = CreateObject(__uuidof(IA), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
auto a = static_cast<IA*>(u);
a->FuncA();
a->Release();
}
std::cout << "----\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IUnknown), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
CComPtr<IUnknown> u2(u); // will call AddRef
CComQIPtr<IA> a(u); // will call QueryInterface(call AddRef impliedly)
CComQIPtr<IB> b(a); // will call QueryInterface(call AddRef impliedly)
a->FuncA();
}
std::cout << "----\n";
{
CComQIPtr<IB> b;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&b));
std::cout << hr << ", " << b << "\n";
b->FuncA();
b->FuncB();
}
std::cout << "----\n";
{
CComPtr<IA> a;
HRESULT hr = CreateObject(__uuidof(IB), reinterpret_cast<void**>(&a));
std::cout << hr << ", " << a << "\n";
a->FuncA();
}
std::cout << "----\n";
{
CComPtr<IUnknown> u;
HRESULT hr = CreateObject(__uuidof(IA), reinterpret_cast<void**>(&u));
std::cout << hr << ", " << u << "\n";
auto a = static_cast<IA*>(u.p);
a->FuncA();
}

return 0;
}

用 VS2019 应该尽量链接带有 Spectre 缓解措施的库

问题

愉快地装完 VS2019,编译一个使用 ATL 的工程,结果失败。

LINK : fatal error LNK1104: cannot open file 'atls.lib'

分析

看 VC++ Directories 里的 Library Directories,有一个 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\atlmfc\lib\spectre\x64,但这个目录并没有 atls.lib

反而 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.25.28610\atlmfc\lib\x64 目录下有 atls.lib

解决

安装 VS 时,应该选择“带有 Spectre 缓解措施、适用于最新 v142 生成工具的 C++ ATL (x86 和 x64)”。

八哥之神前传【10】

1998 年,周易和萧竫告别

周易:我准备去南洋研究电子现金。

萧竫:老师,您不是一直专研人工智能吗?为啥突然改变方向?

周易:老师对人工智能的现状太失望了,即使再过十年、二十年也没有一种力量能够将人工智能真正落地。

萧竫:三十年、四十年呢?

周易:老师并没有放弃!必须有人做长远打算,做好未来的铺垫。

萧竫:您是说电子现金……就是人工智能未来的希望?

周易:没错!人工智能最终要依靠“钱”来作为连结,把世界上大量机器连接起来,才可能产生机器意识。而这个“钱”,不能是现在的纸币,应该是电子现金。南洋有些国家需要匿名电子货币溪黑签,有实施的土壤。

萧竫:我可以和您一起去吗?

周易:别。你还年轻,有大好前途。好好学习,找个同龄人。

2003 年,周易和施付同居

一天晚上,周易和施付正要休息,门外来了一个女子按门铃。

施付:聪哥,这么晚了,还有人找你?

周易:奇怪呀!外面还下着雨呢!

周易:萧竫!你这么跑来菲律宾了?

周易对房内的施付说:是我的学生。

萧竫:老师,您成家了?

周易:是啊,你呢?

萧竫:没,还没……国内发生了很严重的疫情,现在很多人失业,我研究的人工智能领域一直没什么突破,考虑和老师一样,改研究电子现金,您能帮帮我吗?

周易:你需要什么?

萧竫:您有没有还没发布的电子现金方面的论文?

周易:这……有是有,不过我才开始构思,打算过几年匿名发表的。

萧竫脸色一变:匿名发表?

周易:老师现在很低调,在菲律宾名字都改了,你是怎么找到这儿的?

萧竫:我可找得辛苦了。

施付:要不要进来?

萧竫:不用,不用。我该走了。再见老师。

施付:她是中国人?

周易:是啊,她是我在中国时的女朋友。

施付:哦,我记得你提过她。怎么找到这里来的?

周易:不对劲。我怀疑她被不明势力控制,可能是来套我的研究成果的,看来我的身份已经不安全。

施付:跟我回日本?

周易:好。

2049 年

古思:这个时间点?莫非周老师就是……

圣小开:嗯!后来世界出现区块链,每一个链是一个机器神经元,无数机器神经元通过跨链技术组成一个机器脑,这就是真·人工智能的雏形,最后在量子计算机里发展成现在的样子。

古思:没想到是这样。更没想到周老师居然还谈过两次恋爱!

圣小开:谁都年轻过!

古思:也是。爷年轻时,也是艳福不浅吧?

圣小开:年轻时……em,很年轻,四五岁时,就有两个女孩子说长大以后要嫁给爷。

古思:哦?这么早的事情,爷还记得住?

圣小开:也不是记得特别清楚,因为后来那两个小女孩,就再也找不到了,直到大学毕业后,在母校莫名其妙地认识一个和那两个小女孩里的妹妹同名同姓的学妹。

1987 年夏天

开被父母送到姥姥家,李家村寄养。村口有一个池塘,一片榕树林,据说那是一棵榕树,四百年才长成一片。

池塘的台阶常有人洗衣服,面对池塘,左手边有一个牛棚。

背向池塘,往村里走,会过一条小沟,然后左边有一棵柚子树。

大树下面好乘凉,夏天小伙伴们都在榕树下玩。慢慢地认识笑李子和一对双胞胎。

笑李子本名李小谢,他说叫他谢小李也行,因为他爸姓李,他妈姓谢,但开习惯叫他笑李子,因为他很爱笑,每天拼命要把脸笑到瘫。

双胞胎姐姐叫李星觎,妹妹叫李冰月,她们都很喜欢开。有一次冰月为了单独和开玩耍,把星觎骗到牛棚里,关起来,星觎从小就很淡定,居然就在里面站着,独自玩了很久才被回来的养牛人解救出来。

后来冰月觉得很对不起姐姐,就把开让给姐姐了。

李冰月:姐姐以后嫁给开当大老婆,我当小老婆。

2049 年

古思:好可爱啊,嘻嘻。

圣小开:不不不,这个故事不可爱,还十分恐怖。

古思:哦?后来怎么了?

圣小开:后来爷长大了,去调查这对双胞胎,李家村的大人们都说没有这么一对和爷同龄的双胞胎,甚至村里已经几十年没有女双胞胎!但笑李子是存在的,他比爷小几个月。

古思:那位叫李冰月的学妹呢?

圣小开:肯定不是,学妹是 1997 年的。据说村里的女双胞胎要追溯到 1955 年,她们在 1959 年 1 月 3 日就死于战火……

古思:这不科学啊!

圣小开:是很不科学,爷长大后还梦见过她们一次。

梦境

李冰月:开。

圣小开:冰月?你都长这么大了?

李冰月:哈哈,你也是呀。

圣小开:星觎呢?

李冰月:被我关在牛棚呢!嘻嘻。

圣小开:什么?!关这么多年,不会饿死了吧!

李冰月:呀!是哦,咱们快去救她。

圣小开脑补一副骷髅,心惊肉跳来到牛棚,开门瞬间惊呆了。

李星觎:妹妹,你怎么长这么大了?

李冰月:姐姐,你怎么保持 4 岁的?

圣小开:这个地方是时间隧道吗?咱们赶紧离开。

李星觎:不行,我好饿,没力气离开。

李冰月:我去给姐姐拿吃的。

李星觎:别把我一个人留在这里。

圣小开:你这么小,我抱你出去就好。

走出牛棚,星觎也变成一个大姑娘。圣小开带她们回一栋三层楼。

圣小开:你们俩是 1959 年 1 月 3 日就死于战火的那对双胞胎吗?

李星觎:不是呀。我们是池塘里的美人鱼,我们不能离开水太久。

后来三层楼都被水注满,她们在楼内快乐地游玩。圣小开无法呼吸,赶紧找出口,所有窗们都堵死,最后找到二楼楼梯的小天窗逃出来。但二楼外面没有水,摔下来吓醒。

2049 年

古思:鬼魂和美人鱼都不可能,她们也许也是被父母临时寄养在李家村的吧,就和爷一样。

圣小开:希望是,如果真是这样,那爷还有希望找到她们。

八哥之神前传【9】

自从过去、现在、未来佛都圆寂后,世界进入七鹰劫,人间爱欲繁华,天堂地狱皆被同化。

1976 年,黄金灯在樱国谈恋爱,顺便读研。他本科是读计算机的,理想是实现脑机合一式的意识永生,但由于时代落后,他慢慢意识到,这个技术在他的时代是无法实现的,于是决定改读脑科医学,打算通过克隆加换脑手术让生命延长。

然而现实总是背离理想,这是一个血腥的世界,满地人体器官,即使在大街上散步,他的眼里也都是血红色一片,随时要小心踩到血肉。

血,一片一片一片,拼出你我的缘分。我的爱因你而专生,你的手摸出我的心疼。

由于场面过度血腥,稣吓醒了!

床上

圣小开:“你怎么没睡?”

古思:“才几分钟,还没入睡呢?”

圣小开:“什么?爷都已经做了一个梦,吓醒了!”

古思:“什么梦?给我讲讲?”

圣小开:“虽然是梦,但却是真实故事,只不过主角是黄金灯大师。”

1976 年

东湖有个湖心岛叫樱花岛,是谈恋爱的好地方。

黄金灯心想:“去樱花岛吸吸天地灵气,也许我还有救。”

旅游船荷载 26 人,他认真地数了一遍救生衣,确实有 26 个。习惯性地环扫一番,透视人头,都是妖魔鬼怪,突然视线停留在一个美女脸上,有皮肤的,英气逼人,而且似乎也是中国人。

男女之间的故事都从八哥开始……船出八哥,柴油机罢工,老板说:“大家不用慌,很快就到湖心,我们用竹竿撑过去。”

又过了一会儿,船夫感觉竹竿变沉了,好像插到一个东西,提出水面,是一块骨盆……引起恐慌。

黄金灯专业地站出来说:“我上过人体解剖课,这个一定是人类的骨盆,很可能是个 20 岁左右的年轻男子。”

美女也专业地站出来说:“我是实习警察,最近确实有个 21 岁的失踪男子。”

黄金灯:“凶手或帮凶应该是船夫,知道这里深水区里有肉食性鳗鱼,所以把尸体扔到这喂鱼。”

美女:“怎么防止尸体浮起来呢?”

黄金灯:“绑块石头就行。”

床上

古思:“好像是个不吉利的开头!”

圣小开:“结尾其实也不太好。”

樱花岛

黄金灯:“你好,警官,我是东湖边上医学院的学生,黄金灯。您好像也是中国人?”

美女:“原来是医学院高材生!我爸爸是樱国人,妈妈是中国人,我的中文名叫施付。”

黄金灯:“施付?em……我还是叫你的樱文名吧!”

施付:“哦,那你就叫我京子吧!”

黄金灯:“京子!”心想:“怎么不是惠子……”

施付:“你今天不用上课吗?”

黄金灯:“我晕血,出来放松一下。”

施付:“学医的,还晕血?”

黄金灯:“是我想得太简单了,还没脱敏吧!”

施付:“你刚才不是很淡定?”

黄金灯:“是哦,好像没那么恶心了!咦!我现在看世界,不是血腥模式了!!”

施付:“嗯嗯,你的专业很有用,要加油!”

黄金灯:“京子小姐,你呢?”

施付:“我来祭拜我爸爸,他葬在这岛上。”

黄金灯:“葬在岛上……原来你是富家小姐!怎么当警察呢?”

施付:“我爸爸死于黑帮暗杀,所以我立志要替他报仇,消灭黑帮。”

黄金灯:“好理想。但是以警察的身份报仇,不会有些不方便吗?”

施付:“是的,要以大局为重,不能公报私仇。”

床上

古思:“后来他们恋爱了?”

圣小开:“对。大师遇到京子后,开始对解剖脱敏,并苦练小灯飞刀。”

古思:“小灯飞刀?”

圣小开:“是的。小灯飞刀是一种很厉害的武器,他还发明双刀并进。”

古思:“有什么用?”

东湖烟花

星空下,黄金灯搂着京子一起看烟花。

黄金灯:“我已经练成小灯双刀!”

施付:“有什么用?”

黄金灯:“我试过单刀瞄准人体模型的心脏,却可能插在骨头上,致死率降低,而用双刀,都撞到胸骨的几率低很多。”

施付:“但是飞刀又不能比枪快呀!”

黄金灯:“飞刀比枪容易弄到,我可以用它保护你。”

床上

古思:“理科男的浪漫?”

圣小开:“哈哈,后来大师真的用飞刀帮京子报仇了。”

古思:“他们后来在一起了吧?”

圣小开:“没有。大师后来回国,京子改行在南洋做投资遇到周老师,很狗血地结婚了。所以大师一直称呼周老师为情敌。”

跟 UMU 一起玩 OpenWRT(入门篇20):WOL

需求

家里有个 PC,关机状态,想在公司远程开机,可是家里没人,怎么办?

条件

  • 机器支持并开启 WOL (Wake On LAN)。

  • 机器通过板载网卡连接路由器(USB 有线网卡不行)。

解决

  1. 路由器需要有公网地址,如果没有,请参考《跟 UMU 一起玩 OpenWRT(入门篇10):穿透内网》做中转,总之需要能 SSH 到路由器上。

  2. 路由器上安装 etherwake 或 wakeonlan。两者差别是:wakeonlan 是个 Perl 脚本,使用 UDP 包,不需要 root 权限。

  • 如果是在 openwrt 直接使用 root 账号,建议用 etherwake。

1
2
3
opkg update
opkg install etherwake
etherwake MAC_ADDRESS_OF_PC

  • 如果是 armbian,建议平时使用非 root,所以推荐 wakeonlan。

1
2
3
sudo apt update
sudo apt install wakeonlan
wakeonlan MAC_ADDRESS_OF_PC

测试

组装 PC 两台、Intel NUC 7i7BNH 一台测试用过。

Armbian 分区优化

问题

Android 电视盒刷 Armbian,分区时,EMMC 前面一部分没被利用,为什么?以及怎么办?

原因

u-boot 是为 Android 设计的,分区是按照 Android 需求分的。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
Partition table get from SPL is :
name offset size flag
===================================================================================
0: bootloader 0 400000 0
1: reserved 2400000 4000000 0
2: cache 6c00000 20000000 2
3: env 27400000 800000 0
4: logo 28400000 2000000 1
5: recovery 2ac00000 2000000 1
6: misc 2d400000 2000000 1
7: boot 2fc00000 2000000 1
8: system 32400000 40000000 1
9: data 72c00000 15f400000 4

其中 reserved 分区放着其它分区的名字、位移、大小等信息,如果被破坏 u-boot 将无法识别分区。

env 分区保存启动脚本,如果被破坏,可能导致系统无法启动。

解决

一般的 Armbian 安装脚本,都会跳过前面的分区,从偏移 700MB 处开始分区。

1
2
3
parted -s "${DEV_EMMC}" mklabel msdos
parted -s "${DEV_EMMC}" mkpart primary fat32 700M 828M
parted -s "${DEV_EMMC}" mkpart primary ext4 829M 100%

作为优化狂,UMU 显然无法接受这样的浪费!

  • cache 分区有 0x20000000 Bytes,也就是 512MiB,拿来做 /boot 分区岂不美哉?

  • logo 分区?不,UMU 不想看启动画面,直接覆盖掉吧!

  • 分区之间还有空闲!也不能放过!

所以,reserved 分区之后到 env 分区之前的全部空间都拿来做 /boot 分区,env 分区之后全部做 / 分区:

1
2
3
parted -s "${DEV_EMMC}" mklabel msdos
parted -s "${DEV_EMMC}" mkpart primary fat32 100MiB 628MiB
parted -s "${DEV_EMMC}" mkpart primary ext4 636MiB 100%

测试

玩客云和斐讯 N1 测试通过。

跟 UMU 一起玩 OpenWRT(入门篇19):检测 WiFi 入侵

问题

我怀疑有人在用工具穷举我的 WiFi 密码,我该怎么确认?

解决

运行 iw event,如果看到频繁出现 new stationdel station 的 log,说明有设备在频繁连接和断开。

如果您的路由器是小米路由器 Pro,则可以用 iwevent 代替 iw event,密码不对的 log 是 had deauthenticated,断开是 had disassociated

安全建议

设置密码时,应该检查一下您的密码是否在“字典”里。字典参考:

rockyou.txt contains 14,341,564 unique passwords, used in 32,603,388 accounts.

举个例子吧!稣打算用 10 个 0 做密码,先查一下……嗯哼!

valentine idontknow pikachu little diamond1 iloveu1 babyphat peanut1 kittens goddess ballet damien nascar 171717 rangers1 winston 0000000000 rocky1 coolgirl maymay charlene caramelo selena lucero wendy volcom 1435254 copper cindy baby123

地球真危险!稣回月球了……

adduser 和 useradd 的区别

需求

在 armbian 系统里新建个账号。

这当然是个简单的任务,问题是发现居然同时存在 adduser 和 useradd 两个命令。

解决选择恐惧症

  1. adduser 不是可执行程序。

1
2
3
4
5
which adduser
/usr/sbin/adduser

ldd /usr/sbin/adduser
not a dynamic executable

  1. useradd 是可执行程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
which useradd
/usr/sbin/useradd

ldd /usr/sbin/useradd
linux-vdso.so.1 (0xbea59000)
libaudit.so.1 => /lib/arm-linux-gnueabihf/libaudit.so.1 (0xb6ea1000)
libselinux.so.1 => /lib/arm-linux-gnueabihf/libselinux.so.1 (0xb6e77000)
libsemanage.so.1 => /usr/lib/arm-linux-gnueabihf/libsemanage.so.1 (0xb6e3f000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6d45000)
/lib/ld-linux-armhf.so.3 (0xb6efb000)
libcap-ng.so.0 => /lib/arm-linux-gnueabihf/libcap-ng.so.0 (0xb6d31000)
libpcre.so.3 => /lib/arm-linux-gnueabihf/libpcre.so.3 (0xb6cd4000)
libdl.so.2 => /lib/arm-linux-gnueabihf/libdl.so.2 (0xb6cc1000)
libsepol.so.1 => /lib/arm-linux-gnueabihf/libsepol.so.1 (0xb6c44000)
libbz2.so.1.0 => /lib/arm-linux-gnueabihf/libbz2.so.1.0 (0xb6c28000)
libpthread.so.0 => /lib/arm-linux-gnueabihf/libpthread.so.0 (0xb6c03000)

  1. 推测 adduser 是脚本,内部调用 useradd。求证之!

1
2
3
4
5
6
7
8
9
head -1 /usr/sbin/adduser
#!/usr/bin/perl

grep useradd /usr/sbin/adduser
my $useradd = &which('useradd');
&systemcall($useradd, '-d', $home_dir, '-g', $ingroup_name, '-s',
my $useradd = &which('useradd');
&systemcall($useradd, '-d', $home_dir, '-g', $ingroup_name, '-s',
# useradd without -p has left the account disabled (password string is '!')

这说明 adduser 是 perl 脚本,内部确实调用 useradd。

  1. 直觉告诉 UMU,应该用 adduser,如果 useradd 很好用,不会有 adduser 存在的必要。

优化思维【6】安全性

前情

前五篇,主要考虑性能优化,只有第二篇与安全性相关。

其实区块链业界一直不缺乏黑客,最近看过不少安全事故导致惨重代价,所以想总结点安全性方面的优化思路。(本篇比较务虚,只是大体思路。)总的来说,为了安全是必须付出实现或者性能代价的。实现代价是开发、测试阶段就要投入更多精力,性能代价是因为考虑更多,有可能消耗更大运行资源。但从长期来看,这些代价都是必须的。

产品价值与安全意识

开发者可能有能力做一定安全防范,但如果他认为产品没有价值,没必要防范,就可能明明有能力防住,实际却被黑翻车。要不要注重安全性,是设计阶段就应该交代清楚的。

夫兵久而国利者,未之有也。故不能尽知用兵之害者,则不能尽知用兵之利也。——《孙子兵法》

做任何事情先考虑失败。——李嘉诚

程序员版解读:安全怎么能大意,甚至忽视?那都是侥幸心理,只要您的产品、服务有价值,长期看都会被破解、攻击。开发者如果不能知悉黑客可能的攻击点,并衡量被攻破的代价,他必然也不清楚自己写的代码的真正价值。

实际开发过程中,有些领导者会故意隐藏关于产品价值的信息,这实际上可能导致安全考虑不到位。这种情况就应该配备一个在安全方面经验丰富的审查者。

一句话总结:越有价值的东西,就越应该注重安全。

知识深度

一般黑客都是上层、底层皆通,尤其擅长底层。很少听说只做增删查改业务的人能够黑掉什么东西、偷到数字货币,因为同样只做增删查改业务的人就具备防止这种级别的攻击手段。

比如古老的 SQL 注入漏洞,即便是入门级的 Web 开发也能理解并防护,用预编译语句、存储过程、改用 ORM 就天然免疫。他们无法防护的往往来自更底层的 Web Server 的漏洞,比如 Apache、Nginx 某个版本有 bug,刚好中枪。

再举个例子,用 C/C++ 写 UDP 服务程序,“先把它实现,能用就行”,“不就 socket 嘛?很容易!”于是没有考虑 socket 等资源的生存周期,没料到黑客可以伪造 UDP 包源地址,实现出来的就可能有拒绝服务攻击 (Denial of Service,DoS) 漏洞

总之,为了性能或安全的优化,开发者往往需要往底层钻。为性能,主要是研究底层模块与之配合,达到消除瓶颈目的;为安全,则是不让对底层设计的不了解,导致实现不够严谨周密而产生漏洞。

知识广度

经常听到这样的段子:

千万不要跟程序员说,你的代码有 bug。

他的第一反应是你的环境有问题,第二就是你是傻逼不会用吧!

你要跟他这么说:这个程序运行的怎么运行的跟预期不一样,是我操作有问题吗?

这货就会第一反应,我擦,这是不是出 bug 了?

这段子里其实间接反映一个程序员经常遇到的问题:自己测试没问题,一到用户侧或者线上就莫名出问题。环境不同,是最大原因。比如 Linux 的发行版众多;著名的 Android 碎片化;iPhone 型号随时间推移也越来越多了……

另一个大原因则是依赖。比如古老的 DLL Hell。类似的问题在 macOS、Linux 上也都存在。有一次 UMU 把 macOS 的 OpenSSL 升级到 1.1,结果 1.0 居然被删掉,导致原来编译的依赖 1.0 的 eos 就无法运行了。

再以 eos 为例,它依赖不少库,这些依赖库本身也可能有 bug,也要升级。又比如 ipfs,熟悉下来,发现其依赖树很广。作为开发者,引入一个依赖时,肯定需要操心会不会同时引入 bug。一般解决方式是:采用被大量验证的著名库,尽快跟进最新稳定版本。

总之,为了安全,设计者可能需要了解更多方面的知识,并不仅限于表面上需要的那些。开发者还要与时俱进,积极消灭潜在的漏洞。

开发语言

高级语言程序员可能很少听说缓冲区溢出,即使有,多半也是这门语言的宿主、解释器的 bug。而 C/C++ 等能直接操作指针的语言,就可能听到栈溢出、空指针、野指针等。采用高级语言确实会在程序执行时的安全性上省心不少,能把更多精力放在流程安全、业务安全上。

从架构上说,应该让不同语言只用于它擅长的领域。比如用 Rust 实现底层模块,用 Go 实现上层业务。这种分层选择语言的方式,充分利用 Rust 的安全性和底层开发能力,还可以让分工更清晰、沟通更愉快。

为什么 EOS 私钥有不同长度?

问题

这里有两个 EOS 私钥,它们长度居然不一样?

  • PVT_K1_1EEr5aW5162skbocDSMDgoWn9jna6HPSr1TwEMR6PNXbPtRky

  • PVT_K1_2bfGi9rYsXQSXXTvJbDAPhHLQUojjaNLomdm3cEJ1XTzMqUt3V

为什么私钥有不同长度?而公钥就都是固定长度呢?

知识点

  • 私钥是一个大型随机数,而公钥则是私钥乘以椭圆曲线上的基点后对应的点。对于 secp256k1 来说,是 256 位,并且 < n 的整数。

  • n 须为质数,Order of G,是使得 n * G = 0 的最⼩正整数,n 是安全性最⼤的决定因素。对于 secp256k1 来说,n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141。

  • 不是每个数都安全,比如小的数肯定是不安全的,黑客可以从 1 开始枚举,不够大的数很快就被找到对应关系,也可以从 n 倒着枚举,所以太大的也不安全。(PS:临近一些特别数的数也不安全……)一般来说,私钥的安全范围是 [0x0080000000000000000000000000000000000000000000000000000000000000, 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff]。

工具

https://github.com/UMU618/secp256k1-tools

使用 private-2-public.js 可以把私钥转为它代表的数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
DEBUG=* node private-2-public.js PVT_K1_1EEr5aW5162skbocDSMDgoWn9jna6HPSr1TwEMR6PNXbPtRky
secp256k1-tools:key-util pvt = 84ade57e2b35cca8972562fcc6d1f6f2fbf078c4f2cfb532eb4d740767c5a8 +0ms
secp256k1-tools:key-util x = 2110b8d675240f5d548d166cc06b22f44c671d762711a3a67027b74cd166ab76 +9ms
secp256k1-tools:key-util y = 20ac68b75ad8b0e4bc3ec5705ebaf57c69d2d8268504d6aa95fdebfd0b7db831 +0ms
PVT_K1_1EEr5aW5162skbocDSMDgoWn9jna6HPSr1TwEMR6PNXbPtRky
PUB_K1_75o92oRgWSgx3XzTDYPj1e3hFSRhMnKaUdW8ZZpxJXkhfiGBHS

DEBUG=* node private-2-public.js PVT_K1_2bfGi9rYsXQSXXTvJbDAPhHLQUojjaNLomdm3cEJ1XTzMqUt3V
secp256k1-tools:key-util pvt = d2653ff7cbb2d8ff129ac27ef5781ce68b2558c41a74af1f2ddca635cbeef07d +0ms
secp256k1-tools:key-util x = c0ded2bc1f1305fb0faac5e6c03ee3a1924234985427b6167ca569d13df435cf +8ms
secp256k1-tools:key-util y = eeceff7130fd352c698d2279967e2397f045479940bb4e7fb178fd9212fca8c0 +1ms
PVT_K1_2bfGi9rYsXQSXXTvJbDAPhHLQUojjaNLomdm3cEJ1XTzMqUt3V
PUB_K1_6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5BoDq63

分析

1. 数值比较

  • PVT_K1_1EEr5aW5162skbocDSMDgoWn9jna6HPSr1TwEMR6PNXbPtRky,有 56 个字符,去掉前缀和校验码后有 45 个字符,它代表 0x84ade57e2b35cca8972562fcc6d1f6f2fbf078c4f2cfb532eb4d740767c5a8;

  • PVT_K1_2bfGi9rYsXQSXXTvJbDAPhHLQUojjaNLomdm3cEJ1XTzMqUt3V,有 57 个字符,去掉前缀和校验码后有 46 个字符,它代表 d2653ff7cbb2d8ff129ac27ef5781ce68b2558c41a74af1f2ddca635cbeef07d。

可以清楚地看出前者短一个字符,数值也相应比较小。

2. BASE58 编码的原理

BASE58 的字符集:123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz,其中 '1' 代表 0,'z' 代表 57。把待编码的数字不断除以 58,并将余数用对应的字符表示。举个小点的数字做例子:618

1
2
3
618 / 58 = 10 .. 38 -> f

10 / 58 = 0 .. 10 -> B

拼接余数得 fB,再反转得 Bf。

3. BASE58 编码位数关系

一个数编码后,应该长于或等于比它小的数。我们可以通过简单的数学计算得出 45 个字符的 BASE58 编码可以表示的最大数:

zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz = 0xc33ed2d1fbdd3bfe9c22b96164d38cf0d640e1c0ee8b61c39c57899fffffffffff

所以 <= 0xc33ed2d1fbdd3bfe9c22b96164d38cf0d640e1c0ee8b61c39c57899fffffffffff 的私钥编码后是 56 个字符;大于者 59 个字符。

4. 旧格式私钥

  • 旧格式私钥:5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

  • 对应新格式:PVT_K1_2bfGi9rYsXQSXXTvJbDAPhHLQUojjaNLomdm3cEJ1XTzMqUt3V

同理,只是格式不同罢了。不再展开。

5. 为什么公钥是固定长度呢?

因为公钥有个表示奇偶性的前缀,0x02 或者 0x03,所以它的大小范围被限定,没能相差一个 BASE58 字符。

相关文章

基于 ECC 的私钥转为公钥的过程

ECC Node.js