步履维艰的零知识证明

虽然很早就听过零知识证明,但一直觉得是理论密码学那一套还比较停留在论文层面的东西。可能也是最早接触安全时候,科班很少,导致我一直狭隘地觉得没crash,没shell的攻击性安全研究都比较水。尽管这种偏见最初有其滤镜效果,去其糟粕的同时只会误杀一丢丢的精华。时过境迁(竟已快20载),有幸重新审视一些过时的想法。

至少在Web3领域,基本都是逻辑漏洞在闪耀。某种程度上,逻辑漏洞就更会贴近理论纸面上的讨论的内容。碍于我的眼界,Web3领域目前我认为最难理解的部分就是零知识证明的部分。一个通过数论,椭圆曲线等构建的零知识理论体系可以允许持有某个问题解的人在不公开解的情况下就能向公众证明自己确实知道这个解。生活化的例子是有这种需求,但之前觉得好像也不是很必要,或者说,即便有这种实现,木桶的短板也不在这里。

Web3显然是零知识(zk)的一块沃土,起自ZCash币的匿名交易,直到Tornado Cash的应用让zk开始真的特别不可或缺,而如今的zkrollup则又是把它推上高点,被不少人推为Web3下一代革命的核心技术。

我最初接触zk,其实就是想理解Tornado Cash的匿名转账是怎么实现的。但和传统技术领域不同,Web3的噪音非常大,鲜有人抽丝剥茧的喂你吃它每个技术细节。毕竟代码就在那里,理论上你总是可以把每个细节看懂的,但涉及到很多数学原理时候,情况就复杂了(一个用代码描述的微分方程的解题过程会让没有数学分析基础的人都读几遍就明白吗,显然不是)

尽管离开学校太久,从数论开始推到椭圆曲线的配对,再对应到代码实现,不是不行,但周期可能过长,长到可能物是人非,并且由于缺少正反馈,迷失在路中央。因此断断续续地,我尝试看一些教程,看vitalik的经典zksnark的三篇文章(能感觉出来他很努力想写清楚,可能是缺少那种学术素养丰富底蕴的,还是看的很累很迷糊),啃了几个月。迄今,依然只能说都是一些也许正确的点,而远不及一个树状结构的面,就像是整个区块链上构建出黑暗森林的缩影。

希望像是多年前开始漏洞研究一样(单纯啃,不如动手实操一下),看看代码,复现一下人家的案例,可能就渐渐开窍了。

目前的理解是(以zkrollup为例),当有合约被执行时,合约执行过程被描述为circuit,所有中间结果(witness)被转化为R1CS的矩阵。这些中间结果通过椭圆曲线上的生成点加密,再通过取特殊点的值,进一步被压缩多项式成为结果形成定长的proof。而L1的合约则是验证这个proof的合法性。目前很多prover或者operator都是中心化的,接收请求,处理合约,生成proof,提交到L1。

但如果circuit写的有纰漏,比如一个opcode指令的约束条件不严格,就意味着指令的一些参数或者中间结果是可以很容易篡改成其他符合现有约束的witness,进而篡改合约执行轨迹。其实这里是有疑问的,难道L2合约执行不是由另一个vm而不是zkevm执行的吗,那样的话,执行结果应该和这个伪造的合法zk proof没什么关系吧。

目前需要探索的包括:到底zkrollup的circuit的攻击假设是什么,prover/operator不都是中心化吗,攻击者有机会篡改witness吗(未来会去中心化?);构造样本crash掉operator/prover(利用执行batch和执行zkevm的初始条件不同)的攻击感觉更真实有效,攻击假设说得通,可以作为研究备选,也需要探索;

更新ASUS路由器的OpenVPN证书

以前用PPTP就可以连回家里的ASUS路由器,而现在市面上无论手机还是PC出于安全考虑,基本仅支持IPSEC或者OpenVPN。考虑到灵活性且路由器的图形配置界面开启很容易,打算换用OpenVPN的方案。

按正常流程,配置结束可以导出client.ovpn配置文件,再通过手机版的OpenVPN导入这个配置文件就可以连接了。

实际尝试连接时,手机提示验证失败。路由器日志里看到XAUTH Authentication Failed的字样,再根据手机端的OpenVPN日志发现问题源是路由器OpenVPN证书仍然在使用SHA1

手机安装的新版OpenVPN觉得这样很不安全,拒绝与之通信。尽管可以在手机客户端加参数让一些违规操作以permissive放行,但实质的安全问题并没真的缓解。标准做法肯定是更新路由器里OpenVPN使用的证书,至少用SHA256作为签名的校验算法

值得庆幸的是,路由器的OpenVPN的高级配置栏可以找到[Content modification of Keys & Certification]这么个选项,里面允许更改[Certificate Authority],[Server Certificate],[Server Key],[Diffie Hellman parameters]。所以我们要做的就是自己生成强度足够的证书和密钥替换路由器默认使用的这套配置。

参考OpenVPN官方的配置文档,得知easy-rsa可以很容易生成对应的密钥文件,只不过官方文档年久失修,仍然在以停更良久的easy-rsa 2为例说明,目前换easy-rsa 3才是正解。

以Windows为例,首先从easy-rsa 3官方页面下载最新的release,解压后执行EasyRSA-Start.bat

在新的shell下面依次执行:

./easyrsa init-pki
./easyrsa build-ca
./easyrsa build-server-full server nopass
./easyrsa build-client-full client nopass
./easyrsa gen-dh

之后pki/ca.crt就是填写到[Certificate Authority]的内容;pki/issued/server.crt末尾的PEM填写到[Server Certificate]里,pki/private/server.key的内容写到[Server Key]栏目下面;最后,pki/dh.pem填写至[Diffie Hellman parameters]

保存并再次导出client.ovpn后需要注意,新的client.ovpn里客户端的证书和私钥都是留白状态,需要手动填写。也就是说要编辑client.ovpn,<cert>栏目里填写pki/issued/client.crt末尾的PEM,<key>栏目下填写pki/private/client.key的内容。

Arcadium游戏修改

最近手机上装了一个竖版打飞机的游戏,叫Arcadium – Space Shooter,apk的包名为:com.ihgyug.arcadium

游戏里通过收集Gems或直接花钱购买可以换购很多不同型号的飞机,每个飞机的属性有十几种。在设置界面发现有个gift code的选项,也就是输入优惠码可以直接获取Gems。最初的想法是分析下游戏代码,把优惠码都找出来。

用jadx载入apk后发现java代码并没有包含游戏逻辑,原来游戏是采用unity引擎开发的,也就说代码逻辑都是通过.net实现并打包到了libil2cpp.so,字节码的元数据包含在global-metadata.dat文件中。Il2CppInspector可以用来处理这两个文件生成IDA Pro可以识别的符号,导入后就能研究游戏的具体实现了。

看来看去,发现优惠码其实在游戏资源文件level0当中,每个优惠码的格式如下:

08 00 00 00 41 72 63 61 64 69 75 6D

11 00 00 00 48 61 76 65 20 61 20 6E 69 63 65 20 67 61 6D 65 21 00 00 00

F4 01 00 00

字串开头是长度,字符串要四字节对齐,最后一个字段0x1F4意思是优惠码会赠送250枚Gems。针对目前最新版本的游戏(1.0.66)提取出来所有的优惠码如下:

  1. Arcadium: 500
  2. SantaGift: 250
  3. HalloweenGift: 250
  4. NewYearGift: 250
  5. Cheater: 1
  6. Downloadx500: 250
  7. SummerGift: 250
  8. FullRelease: 250
  9. Downloadx1000: 250
  10. Downloadx5000: 250
  11. Downloadx10000: 250
  12. AutumnGift: 250
  13. WinterGift: 250
  14. Downloadx50000: 250
  15. Downloadx100000: 500
  16. SpringGift: 250

为了多搞点Gems,我索性直接把level0文件里Cheater对应的数值改成了99999999,这样进入游戏输入优惠码就拿到了用不完的Gems。

Uranus是我最喜欢的战斗机,也是Gems可以买到的最贵的型号。为了让游戏再省事一点,我又找到了Uranus对应的资源文件,把飞机的属性加强了一下。

飞机的属性文件为sharedassets0.assets.split15,飞机的属性值格式如下:

06 00 00 00|55 72 61 6E|75 73 00 00

05 00 00 00|00 00 80 40|00 00 80 40|00 00 80 3F

00 00 00 00|14 00 00 00|00 00 20 41|00 00 20 41

00 00 80 3F|00 00 00 40|CD CC CC 3D|00 00 00 00

00 00 00 00|00 00 00 00|00 00 00 00|01 00 00 00

00 00 00 00|10 00 00 00|00 00 C8 42|00 00 00 00

00 00 00 00|00 00 00 00|00 00 80 3F|00 00 00 00

最开始是飞机名称,接下来每个属性是4字节,有的是INT,有的是FLOAT。

我识别出来的属性有maxhp, damage, energygain, attackspeed, pickrange, droprate, glowyenemychance, bonusduration, debufduration, eneryleech, lifeleech, critchance.

这里我把maxhp, damage, bonusduration和debufduration稍微修改了一下。

最后再把修改好的level0和sharedassets0.assets.split15重新塞回apk里。重新签名安装到手机上,就可以进入游戏完虐NPC了。整个修改过程仅仅是改了两个资源文件,不碰代码就省去了繁琐的指令patch,改得很惬意。

感冒随笔

周五团建边吃大闸蟹边侃大山时还对自己的身体状态沾沾自喜呢,结果过了个周末就开始干咳,再到周末时已经四肢无力,边睡边烧,那种难受想死的状态无时不提醒着我感冒已至。

每到这种不明所以的痛苦从天而降的时候,除了疑神疑鬼地揣度各种可能的原因(吃蟹没吃姜,连续睡太晚,同事的传染),也特别容易陷入对日常生活和工作学习的反思。

如果时间有限,究竟应该投入在更有价值还是更值钱的事上。比如现在,是应该接着在之前的矿坑里探索还是出来找找附近的新矿井。还是说,更有价值的事其实就是更值钱的事,或者刚好反过来。毕竟每当牵扯到钱,思想特别容易过热,放大了目标的价值。也可能多年回头看今天的决定就像今天看当年没屯币一样的心情,看它不起眼,他确是你当下能把握的最好机会。亦或者其实俩事不冲突,同时进行也是可以的。Nowadays, man knows the price of everything but the value of nothing.

差不多两周里嗓子和肺的不适,让本来闲不住的伶牙俐齿不得不停薪留职。就这样,寸土寸金挤出来的几句话,没有了调侃戏谑和有些多余的铺垫,反而饱含温情充实且平静。不禁觉得平日里的话是不是杂糅进了过多的矫情造作,少了些真诚。等后面有力气说话了,是不是还可以省着点气,别用力过猛。

随着身体状态日渐恢复,思绪又平静下来,身体开始蠢蠢欲动,意图继续之前的冲刺。希望新的平衡态是以现在为基线的情况下找到的,也希望日后不是只有倒下了才开始反思。

新家上网大作战

从2013年到现在搬了有六次家,这次终于找到了一个相对完美的上网解决方案。

这次家里是联通500M光纤入户,宽带入户后所连接的光猫位于餐厅墙内,打开墙上的塑料小门就可以尽情撸猫。光猫带有四个LAN口,一个光纤口。同样在这个墙内小隔间还有八根网线,是基建预留的,分别通向三个房间和客厅,每个房间有两个网口。

首先,我不希望辐射太猛,尽量一个路由器能搞定最好。这样的话,路由就要放在全屋的中央,也就是客厅的电视柜上。路由通过墙上预留的网口可以连接光猫的LAN1上网,当然为了挂PT,上班VPN回家等考虑,光猫要改成桥接模式。这样路由器就可以代替光猫直接PPPOE拿到公网IP,家里其他设备通过路由上网就可以了。

选了个华硕的RT-AX86U,网上看各项评价都不错,然后好像网络部署问题就over了。

实际部署好以后,发现个大问题。就是我放台式机的书房距离路由着实有点远,虽然上网流畅,但下载速度实际只有4-5M,远远低于40-50M的应有额度。显然这台PC作为家里宽带的重度使用者离热点太远了。但如果就简单地把路由拿到书房的话,电视又离路由器太远了。而电视又需要经常通过SMB看电脑里下载的电影,所以作为局域网络的重度使用者,电视也要离路由器近一点。而且路由放在中心客厅,可以充分地保障pad和手机的使用。

既然书房也有网口可以直接连接光猫,有没有办法让台式机通过这个网口上网呢?目前家里的网络环境如下图所示:

路由器WAN口和台式机LAN口通过基建预留的网口分别连接了光猫的LAN1和LAN2,并分配到192.168.1.2和192.168.1.3的IP地址。由于光猫内的桥接,路由器WAN通过PPPOE可以直达联通,还会分配到公网IP,如下图所示:

现在的问题是,台式机仍然没法上网,也不能访问路由内网的其他资源。而且上海联通也不支持PPPOE双拨,所以不能通过光猫再建立一条到Internet的通路。

经过反复尝试,目前完美解决。华硕的路由可以开启OpenVPN,这等于WAN口会侦听一个端口,允许外部认证后访问路由内网资源,也可以通过该路由访问Internet。虽然说路由VPN也可以选择更简单的PPTP/L2TP+IPSEC,但PPTP已经被OSX抛弃,IPSEC由于算法协商问题跑不通,最后就只能选择OpenVPN了。

最后的网络连接后的效果如下图所示:

台式机经由光猫的LAN2 – 路由WAN – 路由OpenVPN – 光猫LAN1 – 光猫Bridged TTL来访问Internet,当然也顺带就访问到路由器内网了。实际使用过程中,也许底层被优化(走着走着也许就直接LAN2 – TTL了也说不定),台式机下载速度基本可以跑到50M,说明这一通折腾基本没有太多流量损失在VPN上。

挑大梁

春节假期结束两天后,上海和长春终于恢复了低风险。去年一整年疫情的乌云压在心头,也就基本没请假出去,剩下的假刚好就放二月末消化了。

从周一中午落地长春到周五上午起飞返程,四天多的时间安排地非常紧凑饱满,环环相扣:

周一,中午到家吃了点东西,等姥爷醒了,请了个安,下午就看家具去了。经过一两个月的思考和实地考察,我妈对需要哪些大件心里已经有数,但距离最后定稿还是有很多功课要做的。图省事,初步方针是,无论定制还是成品就都在曲美选了。在店里和导购商量了一下,由于时间紧迫,本来一个礼拜周期的设计流程希望改成设周二下午就能量尺寸,周三就出图。既然周二要去新房子,就又和管家约了周二下午一点过来换个指纹密码锁,方便以后快递送货。约好以后,周一晚饭前赶回来和姥爷一起吃了饭,姥爷提起来剪头发的事儿,由于冰还没化,春节期间他一直没出门。我刚好回家前也有小木屋温泉的想法,遂琢磨周二下午一家人去小木屋温泉一下顺便可以剪头发,就在大众点评上尝试订个房间(比携程便宜很多,就是确认的非常慢,好在睡前房间已经确认成功)。晚上还在网上买了花洒和风暖打算把家里差强人意的装备换掉。

周二,接到设计师电话,本来下午一点过来,现在说上午十点多就能来,本着不想夜长梦多的初衷,吃完早饭就带上姥爷的午餐和小木屋温泉的衣服出发去新房子了。路上怕设计师到的太早又和管家联系,以便万一设计师到的早了,就麻烦她去开门。兵马未动,粮草先行,车上点了面条外卖先送过去。一到地方就开始和设计师交代我们的想法,切磋几轮后外卖也到了,就把姥爷的午饭拿出来给他吃上,我们也开始吃面。十二点多,设计师已经量好所有尺寸别过了。期间又约了个周三的开荒保洁,让他十点过来输密码自己进来打扫,这样我们小木屋之旅结束以后可以中途过来看打扫的进度。一点钟,物业的人来给换门锁了,一个小时换好,后来使用中我发现门锁有BUG,又叫他回来磨合尺寸,一直到快四点才磨好,忙忙活活地出发去小木屋了。到了国信南山温泉以后,办了入住,本打算先剪头发再吃饭,遗憾地发现理发师今天回老家了。晚饭后回房间泡澡,由于记忆偏差,水温多少有点定低了,才39,按说42比较合适。不过为了不白住,泡了也快两个小时。保洁这时电弧联系,发现他们第二天是不带梯子的,这样吊棚很难清理,于是泡差不多,等姥爷睡了,我妈和我就又开车回家拿梯子送到新房子去。

周三,早晨起来开车到前楼吃早饭,让他们给房间换水(但是后来回来才发现并没换)。吃完发现一个轮胎憋了,而放在车里的小米充气泵也因为天冷电池无法工作,就拿到房间充电。充满以后,我和我妈轮流用手捂着充气泵(防冷)给轮胎打气,另一个人进车里取暖。差不多十分钟,轮胎恢复2.5kpa胎压。然带上姥爷就直奔新房子,上楼和保洁交接了一下工作,又径直回家了。中午等姥爷睡了,又驱车去曲美。和设计师讨论了很久具体的定制方案,又转了几圈,选择颜色和材料。这个过程中我反复和我妈确认她的想法,一方面不能过度建议,另一方面又得防止她被设计师的话术引导。总算在五点多敲定了所有细节开始签合同,同时让已经到了的外卖员把东西放到楼下大门里。期间我的手机又一次落地,正面屏幕骨折了。六点多终于签好合同,确定了三床、鞋柜、衣柜橱、床头柜、电视柜、沙发,就匆匆回家吃饭了。晚上又在网上逛了逛,琢磨了一下以后茶几,冰箱洗衣机的可选项。今天的某个时间还约了周四的风暖安装师傅和厕所除味的师傅。

周四,上午给姥爷准备好午饭,我们就出发了,跑了两家灯饰城,看了看飞利浦松下新特丽的吸顶灯。下午回来,发现姥爷自己竟然跑出去剪头发去了,还好平安归来。之后,约好的风暖师傅和厕所除臭的师傅也到了。两个人一直到差不多四五点钟才完活。

周五,撤了,估计在家接着待还能琢磨出活儿来整,比如换了家里的洗衣机。

期间姥爷和大姨都提到了挑大梁一词,形容我这几天的行动。但褒奖之余,自我反思得还是有很多问题的。时间紧任务重,平时的想法浓缩到一个很短的时间完成,会导致经常越过和家人讨论的步骤做决定,难免有些伤情感。其次,自己内心紧锣密鼓也会间接让身边人无法放松,所以小木屋的过程估计对他们来讲也不是那么惬意。另外,打扫卫生的环节事后发现清理的很偷工减料,比上海这边差很多,监督还是得有,特别是收工检查部分。

等额本息的战争

安全界老大哥@yuange1975最近揭露微博借钱的套路贷导致微博账号被删。套路贷问题本身还是很有意思的,很像是那种偷换概念的陷阱趣味题。

问题来了,如果借了20000块钱,一年还完,每个月要求还款2266.67,那么究竟这笔贷款的年利率该怎么算呢。

一个错误的示范是,年利率P = (2266.67 * 12 – 20000)/20000 = 0.36。也就是说一年才36%而已,好像很符合国家规定的上限。

很久不实际推算点什么东西了,这里的陷阱还真一下没反应过来。题干简单的背后,是故意模糊的概念。首先什么是年利率,一般来说它是一个等价概念,粗略说就是一笔钱过一年后增加的数目/初始钱数。如果一年到期后,一次性还2266.67 * 12 = 27200的话,那年利率计算结果就是36%。

但现在提前还了钱,先不论每个月的钱里有多少是本金,多少是利息,显然对于借钱的你是要亏一些的,而对于债主来说,它能提前拿到钱是很赚的,最次最次他拿到钱以后存到余额宝也有利息啊。所以直观感受是,这种还款年利率应该是比一年以后一起要高的,但究竟怎么量化这种感觉呢。

在此期间我看到有公众号也在讨论这个问题,其解决方法是经济学里的IRR公式。本来应该主动去分析的问题,现在都成了被动的朋友圈推送文章,久而久之现在处理问题好像更加被动了。另外公众号的文章从来都是短小精干(浅尝辄止)。即便后来找到了IRR公式“原理”,依然不甚过瘾。记得大学时候替学生会的一个部长喊到,上过一节经济学原理的课程,依稀记得课上的一道题目我是用等比数列极限求解,而老师给出的是经验公式,且没有推导过程。所以至今我仍冒昧且莽撞的认为经济学里的很多理论,由于很多人不去深究其背后数学根基,讲出来的话也就特别缺少逻辑的严谨性,仅用于计算倒是没有问题。不过这样的分析文字总让人坐立不安,一个享受证明题那种逐层推倒过程的人特别不能接受。

所以这个利率问题,感觉一定要用自己知识范式里的逻辑推导出结果才能真正“过瘾”。我们不去造什么现钱未来升值的概念,也不去假设每个月还的钱里面到底本金是不是相等。现在只是求一个等效的月利率q出来,然后用它刻画我们的直观感受。下面试试在q不变的情况下,是否能求解这个问题。

对于拿到20000块钱后第一个月末,要还2266.67,这时欠人家的本金一直是20000,而这2266.67中包含还掉的本金部分用a表示。于是有:

20000 * q + a = 2266.67

即这个月还款的2266.67是还掉的一部分本金和利息的和。由于欠的本金一直是20000,所以利息就是20000 * q,而a是还掉的本金。

现在来到了第二个月末,仍然还款2266.67,其中还掉的本金用b表示。第二个月开始,我们欠人家的钱已经不是20000,而是20000 – a了,所以这个月的利息应该是(20000 – a) * q,这样就有:

(20000 – a) * q + b = 2266.67

对于第三个月来说,还掉的本金用c表示,依据之前的推导应该有:

(20000 – a – b) * q + c = 2266.67

依次类推,到第十二个月初,我们已经还了十一笔款,每次还的本金依次是a, b, c, d, e, f, g, h, i, j, k,所以现在欠的本金应该是(20000 – a – b – c – d – e – f – g – h – i – j – k),那对于最后一个月来说有

(20000 – a – b – c – d – e – f – g – h – i – j – k) * q + l = 2266.67

最后第十三个等式是,a + b + c + d + e + f + g + h + i + j + k + l = 20000,就是每个月还掉的本金和为最初的欠的20000。

现在一共有十三个方程,十三个未知数,我们只要求出来方程里的q a b c d e f g h i j k l的正实数解即可。这个方程应该可以手动化简,不过偷懒的方法是用现成的工具,如Matlab或者 Wolfram。以Matlab为例,以下命令可以解开方程:

syms q a b c d e f g h i j k l
[q_, a_, b_, c_, d_, e_, f_, g_, h_, i_, j_, k_, l_] =  vpasolve((20000)*q + a == 2266.67,(20000 - a)*q + b == 2266.67,(20000 - a - b)*q + c == 2266.67,(20000 - a - b - c)*q + d == 2266.67,(20000 - a - b - c - d)*q + e == 2266.67,(20000 - a - b - c - d - e)*q + f == 2266.67,(20000 - a - b - c - d - e - f)*q + g == 2266.67,(20000 - a - b - c - d - e - f - g)*q + h == 2266.67,(20000 - a - b - c - d - e - f - g - h)*q + i == 2266.67,(20000 - a - b - c - d - e - f - g - h - i)*q + j == 2266.67,(20000 - a - b - c - d - e - f - g - h - i - j)*q + k == 2266.67,(20000 - a - b - c - d - e - f - g - h - i - j - k)*q + l == 2266.67,a + b + c + d + e + f + g + h + i + j + k + l == 20000,q,a,b,c,d,e,f,g,h,i,j,k,l)

解出来的正实数解为:

q_ = 0.050797584803673053315458232134993
a_ = 1250.7183039265390064504114991344 
b_ = 1314.2517730357534996140256951894 
c_ = 1381.0125889299148582748617671113 
d_ = 1451.1646930310222824133176198543 
e_ = 1524.880354589361811121911194386
f_ = 1602.3405937170699537423596414655 
g_ = 1683.7356259107806443553650286218 
h_ = 1769.2653291549490520943120595088  
i_ = 1859.139734752896094790930379943 
j_ = 1953.5795430908845605782957674657
k_ = 2052.8166656017646249690438355595
l_ = 2157.0947942590636115951655117603

所以这里抽象出来的等效月利率q = 0.050797,则年利率P = 12 * q 或者(1+q)^12 – 1。之所以P是这两种情况,是因为很多金融机构对年利率的定义不同,通常的P = 12 * q 给出的年利率数值会小一些,在贷款时使用更具有迷惑性,让借钱的人直观上觉得划算。但实际计算时用的P应该是(1+q)^12 -1,这是考虑了复利,相当于每个月末把本金取出来,又一起存进去。我认为关于这个q和P的关系比较容易理解,所以就不赘述了。

这样其实等效的P应该是(1+q)^12 – 1 = 0.81,即年利率为81%,比一开始的36%要高很多。这里的81%也是一个概念值,给你这个数值并没有办法直接用于计算比如到底还了多少钱之类的问题。它只是量化了我们最开始的直觉,即每个月还钱比到期一起还时的情况,其利率要更高,而且高很多。

推导到这里,基本上算是完事了。但是这个问题与实际世界是如何建立联系的呢。直到这时,我才了解,其实上述每个月还固定钱的问题就是所谓的等额本息还款。另一种偿还贷款的方法叫等额本金。等额本息看起来舒服,因为每个月数额整装,可背后原理复杂;等额本金看起来数额零散,不过背后计算简单。

既然对应到了现实世界的问题,那就有现实的工具可用。比如同样是这个问题,我们可以打开招商银行的计算工具,在等额本息工具下,输入本金20000,分12个月,年利率部分输入P = 12 * q,即60.96%,点击计算,你就发现其计算结果是每个月还款2266.67。这里银行用的P并没有填写我们刚刚的(1+q) ^ 12 – 1,原因就是银行贷款为了让你心里预期低一些,选择P = 12 * q。其实这两种P如前所述都没法直接用于计算,只是给你一种直观感受。真正计算用到的是q。

关于这个问题至此已经可以说讨论完整了。

最后想从纯数学的角度再看一下刚刚的方程,我们唯一的假设是q它作为一个月等效利率是不变的。如果月利率q可变,而假设每个月还款中本金是固定的呢,即用a b c d e f g h i j k l表示每个月的利率,而还的本金是q,仍然用Matlab解开这个方程就变成了:

syms q a b c d e f g h i j k l
[q_, a_, b_, c_, d_, e_, f_, g_, h_, i_, j_, k_, l_] =  vpasolve((20000 - 0 * q)*a + q == 2266.67,(20000 - 1 * q)*b + q == 2266.67,(20000 - 2 * q)*c + q == 2266.67,(20000 - 3 * q)*d + q == 2266.67,(20000 - 4 * q)*e + q == 2266.67,(20000 - 5 * q)*f + q == 2266.67,(20000 - 6 * q)*g + q == 2266.67,(20000 - 7 * q)*h + q == 2266.67,(20000 - 8 * q)*i + q == 2266.67,(20000 - 9 * q)*j + q == 2266.67,(20000 - 10 * q)*k + q == 2266.67,(20000 - 11 * q)*l + q == 2266.67,12 * q == 20000,q,a,b,c,d,e,f,g,h,i,j,k,l)

结果为

q_ = 1666.6666666666666666666666666667
a_ = 0.03000016666666667030464547375838
b_ = 0.032727454545454549423249607736414
c_ = 0.036000200000000004365574568510056
d_ = 0.040000222222222227072860631677839
e_ = 0.045000250000000005456968210637569
f_ = 0.051428857142857149093677955014365
g_ = 0.060000333333333340609290947516759
h_ = 0.072000400000000008731149137020111
i_ = 0.090000500000000010913936421275139
j_ = 0.12000066666666668121858189503352
k_ = 0.18000100000000002182787284255028
l_ = 0.36000200000000004365574568510056

注意,这里的q已经是每个月要还的本金了,而变化的a到l是每个月的月利率。但现在不过是纯粹的数学计算了,我们并不是在解等额本金问题。通过a到l没办法抽象出一个所谓的等价月利率或者年利率。

题目为什么叫战争呢,因为我和媳妇周末讨论怎么计算时,尽管两个人都一知半解但却几次三番激烈异常,时不时面红耳赤,更有甚者唇枪舌剑。当然,问题最后还是在健康平静恩爱的气氛中解诀了。

小迁徙

Digital Ocean的服务器用了两年多,当时10美元的配置今天已经升级了,买的早的,也不说老配置给自动降个价。虽说Google Cloud的服务器现在速度最好,但维护起来总是比较麻烦,万一哪天梯子抽风,还是挺不好办的。

所以打算还是重新买一台Digital Ocean的十美元新配置,然后把网站后台迁过去。按计划,先买新服务器,安装自带WordPress,然后从老站WordPress导出xml,新站导入,没问题就把web目录scp过去就完事了。

可实际操作起来,还是耗了几个小时。

首先Digital Ocean这种自带后台的服务器,ssh刚进去就命令行下让你配置 WordPress的域名,账号密码等信息,不配置就总弹出来,而且域名不写对了,你访问ip得到页面动不动就被跳转到奇怪的域名下。测试时比较方便的方法是改本机/etc/hosts,假装域名已经迁过去了。

WordPress的导入导出功能也很残疾,这个只适合同一个站点的备份和恢复。因为老xml导入新的站点时,用户信息没法正常导入,只有作者映射,密码没法同步,另外一些网站的名称,皮肤配置也跟不过来。

还一个问题是,因为网站目录下可能有一些自己写的东西,要拷贝web目录过来,但就很容易把某些配置文件改掉,已经配好的WordPress也会因此连不上数据库。

试了半天,发现最惬意的方法是老站点装phpmyadmin直接把wordpress数据库dump下来,然后新机同样操作,把dump的sql文件再覆盖掉原数据库。最后web目录一同步,再修改一下新机wp-config.php里的数据库密码就全好了。这样包括你皮肤里自己改动的代码也就都跟过来了。

全部折腾完,准备destroy老服务器时,确认页面居然弹出来问,是否免费自动把老服务器配置升级,very well

Infiltrate 2019

距离上次用英文讲东西已经三年了,这次的议题从春节前做完实验开始算起,幻灯片部分准备了差不多三个月。前一个月幻灯片草稿阶段投入精力比较密集,后两个月平均每天都也要花一到两个小时练习和反复修改。相比上一次,这次讲的效果似乎要好很多。

幻灯片制作时,前期偷懒省掉的animation和highlight最后又都补回来了,只有站在旁观者位置,仔细琢磨一个并不了解情况的人看你幻灯片时的感受,并且真的想人家完全听懂为动力才能把幻灯片推向自觉的极致,然后才好期待别人的认可。

练习时先用口语化的表达往复过几遍,然后把表达不理想的部分写下来,这样能慢慢调整修改,结果就是几乎每两三页就有一些文字记录在案。练习时也不必完全背诵,适当引入一些语速变化的口语化表达会显得更为自然。视频录下来看自己的表现和听录像里的声音感觉会很不一样,这有助于调整自己的语音语调以及动作表情。最后在台上时,可能是面向斜下角度的问题,表情一直不是很好,这在练习过程中确实没有发现,也可能是紧张导致的面部僵硬。

Dry Run部分会议方给的建议是Demo部分加入一个joke,这是此前讲东西从来没有尝试过的环节。最后的效果非常好,由于语言上没有Native Speaker的优势,笑点主要靠前期的P图和动画共同完成,达到一个出人意料的组合,语言上只要在这个动画出现时稍微停顿一两秒,台下已经准备好笑力的观众就会释放了。

如果说改进的话,这次会议参与度不如44con,几乎很少在会场的大厅里徘徊。另外也不是所有的议题都要去听,精力有限,听多了反而有影响。

Magisk on T1

Smartisan T1是一款老机型了,时至今日,运行的仍然是Android 4.4的系统。以前研究过它的Bootloader解锁,如今打算回头再看看怎么把最新的root方案Magisk移植进来实现持久。

不同于supersu需要twrp的支持,Magisk提供了一种十分便捷的修改镜像的方法。只要把目标设备的boot.img拖出来,放到任何一台安装有Magisk Manager的手机上,就能进行重打包,包装成一个带有root功能的镜像。接下来,无论用什么方法,只要能把boot.img刷回boot分区就可以了。

临时root

T1升到最新版本后(2018年2月份的版本),为了把boot.img拖出来,并放回去,需要取得一个临时的root权限,我利用的是CVE-2017-8890,不过这不是重点。问题是后面的Magisk重打包。

Magisk原理

Magisk的重打包,从底层看,主要就是把/init替换成了它自己的magiskinit。当boot启动时,magiskinit首先从/.backup恢复原始的init文件。然后从magiskinit中释放出来magisk.bin和Manager的APK等后续建立root需要的文件。接着它会以init权限patch sepolicy,增加一个名为magisk的上下文环境,最后启动magiskd等待su的连接。

当su命令执行时,背后实际上是向magiskd请求root,magisk则会向Magisk Manager发起问询,等待用户确认后才赋予放行,最后magiskd启动一个shell,并把它重定向到su通过unix socket发送过来的输入输出句柄上。

T1带来的麻烦

现在最大的问题是Magisk Manager编译时只能运行在5.0以上的系统,4.4没有支持。如果抛弃magisk,单靠suid之不能建立起完整root的,4.4虽然selinux弱得很(通过/proc/self/attr/current就能任意修改自身上下文),但普通的用户进程其capability已经几乎全部丢弃,即便uid和gid都为0,能做的事情也有限。还是需要一套magisk这种劫持启动流程的方案才能从一开始就拿到init。

所以打算完全抛弃Magisk Manager,通过patch magisk自身的逻辑,实现任意进程执行su时无需校验就放行。

patched_boot.img的重打包

想要patch magisk,要修改的实际上是Manager重打包后的patched_boot.img。它当中的magiskinit包含了magiskd的代码,只要修改两个关键的跳转就算通过了。不过问题是这里每个环节都很繁琐。首先magiskinit的文件结构很特殊:

$ binwalk init

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 32-bit LSB executable, ARM, version 1 (SYSV)
262482        0x40152         Unix path: /vendor/etc/selinux/precompiled_sepolicy
262824        0x402A8         Unix path: /sys/fs/selinux/policy
262895        0x402EF         Unix path: /sys/fs/selinux/load
263576        0x40598         Unix path: /sys/fs/selinux/policy)
266224        0x40FF0         Unix path: /vendor/etc/selinux/plat_sepolicy_vers.txt
296964        0x48804         xz compressed data
301647        0x49A4F         xz compressed data
346435        0x54943         xz compressed data
376768        0x5BFC0         Unix path: /proc/device-tree/firmware/android

其中0x49A4F开始的这个区块就是magiskd也就是su_daemon。由于magiskinit要根据这些偏移解出来所需要的文件,修改的时候必须要保证文件大小精确一致。
0x49A4F 文件可以dd出来,然后unxz解开。修改二进制后,不能直接xz,xz命令和magisk用的结构稍微不太一样,需要使用python3的lzma的compress去压缩,也就是:

lzma.compress(magisk, preset=9, check=lzma.CHECK_NONE)

dd回去之后,还要进行ramdisk的重打包,这里同样有个文件大小要保持一致的问题。因为patched_boot.img已经和boot.img不一样了,末尾会增加一个xz,包含Manager的APK文件,虽然用不到,但虽已覆盖会导致初始化失败。

虽然ramdisk重打包有很多工具,但都无法和magisk的cpio保持一致,主要原因是普通的重打包都没有考虑到/.backup等隐藏目录的引入。所以更精确的做法是直接对cpio文件操作,找到init文件的位置,写入修改后的文件内容。对cpio进行gzip时同样还有问题,系统自带的gzip会加入过多的metadata导致文件比原来要大。我解决的方法是minigzip.py,去掉文件名相关的metadata,然后进行压缩,这样大小就和原来相同了,最后dd回patched_boot.img就可以了。再次开机启动就拿到了久违的su:

shell@msm8974sfo:/ # id -Z
uid=0(root) gid=0(root) context=u:r:magisk:s0