dream的小站
type: channel
link: https://www.resourch.com/
description: Resources Research
items: 炮打安卓原生开发
items: Neon指令集加密算法逆向
items: jsvmp编译与反编译详解 (3)——某讯新版vmp反编译
items: cloudflare逆向(4)——奶奶都看得懂的5秒盾ast还原（2024）
items: 公安部ob混淆ast还原实战
items: jsvmp编译与反编译详解 (2)——字节栈式虚拟机反编译
items: jsvmp编译与反编译详解 (1)——实现一个简单的jsvmp
items: 600行代码实现一个chip-8虚拟机
items: GFW的SMTP协议TCP重置攻击分析
items: poetry如何更换国内源（清华源/阿里源/豆瓣源）

炮打安卓原生开发
type: item
date: 2025-10-14T19:46:00+08:00
description: 同步吐槽在知乎：https://www.zhihu.com/question/317769041/answer/1961442803629393838[...]

Neon指令集加密算法逆向
type: item
date: 2025-01-08T02:23:00+08:00
description: 确定字符串解密函数进入vmp初始化逻辑分支，找到一些初始化函数进入函数，能够找到一些形如下方内容的代码，几乎毫无可阅读性任意抽取一段代码进行分析 v6 = (*(__int64 (__fastcall **)(__int64, __int64, char *, char *))(*(_QWORD *)v3 + 264LL))( v3, v5, byte_5F35E, &aF_2);v3 + 264LL 为一个指针，强制转换为(*(__int64 (__fastcall **)(__int64, __int64, char *, char *))，说明这是一个函数调用。查看参数，v3作为第一个参数被传入，回忆一下JNI函数的签名(*env)->NewStringUTF(env,"Hello from JNI !");很明显V3为JEnv*结构体，将v3强制转换为JNIEnv *类型，此时函数调用恢复正常v6 = (*v3)->GetMethodID(v3, v5, byte_5F35E, &aF_2);根据GetMethodID的定义，我们可以得知GetMethodID的作用是获取java方法的函数调用签名，其中第三、第四个参数传入的是方法名和方法签名，在so执行流程中，这些常量类型一般会存在rodata、data、bss段，因此我们需要查看一下byte_5F35E 和 &aF_2 的值jmethodID GetMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig)从data段查看 byte_5F35E 和 &aF_2 的值，很明显字符串做了加密，ida无法正确读取查找交叉引用，寻找到所有调用字符串的函数，查看函数签名int8x16_t *__fastcall sub_CDE8(int8x16_t *result, int8x16_t *a2, unsigned int a3, unsigned int a4)在arm64的函数规约下，函数跳转前，一般由X0-X7寄存器来声明前8个参数，并由X0保存返回值这个函数有4个参数，因此我们在函数返回地址前断点，对X0到X3寄存器进行监听跳转到X0寄存器所指向的data的地址，能够看出来字符串被解密，可以据此判定为字符串解密函数查看函数逻辑int8x16_t *__fastcall sub_CDE8(int8x16_t *result, int8x16_t *a2, unsigned int a3, unsigned int a4) { __int64 v4; // x9 char *v5; // x10 _BYTE *v6; // x11 __int64 v7; // x8 char v8; // t1 int8x16_t v9; // q1 int8x16_t *v10; // x10 int8x16_t *v11; // x11 int8x16_t v12; // q0 int8x16_t v13; // q1 int8x16_t v14; // q2 __int64 v15; // x12 int8x16_t v16; // q4 int8x16_t v17; // q5 int8x16_t v18; // q4 int8x16_t v19; // q3 if ( (int)a4 >= 1 ) { if ( a4 < 0x20 || (int8x16_t *)((char *)a2 + a4) > result && (int8x16_t *)((char *)result + a4) > a2 ) { v4 = 0LL; LABEL_6: v5 = (char *)a2 + v4; v6 = (char *)result + v4; v7 = a4 - v4; do { v8 = *v5++; --v7; *v6++ = v8 ^ a3; a3 += 3; } while ( v7 ); return result; } v4 = a4 & 0xFFFFFFE0; v9 = vdupq_n_s8(a3); v10 = a2 + 1; v11 = result + 1; v12.n128_u64[0] = 0x3030303030303030LL; v12.n128_u64[1] = 0x3030303030303030LL; a3 += 3 * (a4 & 0xFFFFFFE0); v13 = vaddq_s8(v9, (int8x16_t)xmmword_4AEE0); v14.n128_u64[0] = 0x6060606060606060LL; v14.n128_u64[1] = 0x60…

jsvmp编译与反编译详解 (3)——某讯新版vmp反编译
type: item
date: 2024-09-01T06:01:00+08:00
description: 腾讯这次还是挺狠的，直接把栈式虚拟机更新成寄存器式虚拟机了，并且将原先的60多条指令膨胀到80多条。栈式虚拟机的指令集通常较为简单，指令操作主要围绕栈进行，由于栈式虚拟机的广泛使用，现有的资料对栈式虚拟机的支持较为成熟。而相比于栈式虚拟机，寄存器式虚拟机的指令集复杂，我们需要处理更多的指令类型和寄存器操作，不过对于web虚拟机来说，其功能本质仍旧是对js内存空间的访问。两个月前才刚刚完成了旧vmp逆向，这下直接在风中凌乱……好吧，那只能我们继续重新分析了我们的目标实现反编译器，通过传入原始jsvmp程序，生成汇编最后生成js代码并保证运行逻辑一致//将 类似"JgQCFhoEBBZEHgASPkQEZp/4A4PzAg=="的jsvmp程序 还原成下方的js代码 locals[11] = "__lastWatirConfirm"虚拟机入口vmp部分共计800行，腾讯的vmp样例还是比较清晰的，整段代码没有太多无用的混淆，而仅仅用了一个朴实无华的vm我们将短的函数进行一下折叠，查看一下代码结构，可以看到整个vm由一个大的控制流和一些函数组成，向下寻找，找到缩进最多的地方，虚拟机的解释器通常通过循环和流程控制语句嵌套构成，因此会形成较多的缩进其中最内层的switch控制了整个流程的执行，整个虚拟机结构较为经典，为while-switch结构，没有多余的if或逻辑表达式混淆查看switch顶部，查看switch的条件由整个C数组控制，说明C就是传入的程序字节码，我们需要知道C数组的生成方式进入浏览器的devtools，打上断点，刷新执行一下，发现C数组是由传入的B参数经过Q函数生成的，Q函数实际上会经过别名赋值，跳转到E函数，因此E就是我们需要解决的字节码还原算法了。程序加载器解密变长编码控制流查看一下E函数的编码方式，每次循环中，都会读取一个字节 a，并将其与 127 进行按位与操作，得到 o事实上，大部分被混淆后的算法，都是来源于通用算法，总是有迹可循的。例如，base64编码算法通常要大量操作 0-9\a-z\A-Z\= 这些字符，urlencode会大量操作 %，变长字符串编码通常会通过分组拼接方式来完成字节生成，因此我们只要找到对应的特征，就能知道他使用了什么解密算法。例如，上图有一个比较明显的重复操作，即每轮都会将从B数组取出的8位int的低7位(通过和127 按位与运算来实现)，并将其和o拼接，如果 a 的最高位（第8位）为 0（即 a >= 0），则表示这是一个单字节的值，直接将 o 通过 C 函数处理后存入 g 数组，并继续下一次循环。这种操作很像是在处理变长整数编码（如 varint 编码），在这种变长整数编码方式中，每个字节的最高位用于指示是否有后续字节，并指示需要将多少个字节拼接为一个整数。不过只靠这些信息我们还是很难看出他在做什么，因为while循环体内的流程被扁平化了，实际上有4个if和continue对执行流程进行了控制，需要我们进行一下还原。看一下，这里在每个if的代码块中，存在一个continue，这意味着如果id的条件为真，就不再执行后面的代码，因此我们可以将后面的代码块看作是else分支因此我们第一步就可以将控制流代码还原成这样接下来看一下，在每一个else分支中，都通过B[e++]的形式拿到了数组的下一个8位int，并将其低 7 位左移 7 位后与 o 进行按位或操作，这一步的作用是增加o的位数长度。重复上述步骤，直到读取到一个最高位为 0 的字节。每一步的操作都和最开始的if分支操作相同，但是会将字节进行移位处理，并重复将a合并为到o中。因此可以将其简化为一个循环在上面的操作中，我们已经分析了，在变长编码中，每个字节的最高位用于指示是否有后续字节，最高位为0时，代表这个数结束了，所以说明E 函数很有可能实现了一个变长整数解码器，并将解码后的整数存储在数组 g 中ZigZag编码特征在将o插入g数组前，每个整数还通过 C函数进行了处理，看上去像是一种通用的算法，百度一下，果然，(A >> 1) ^ -(1 & A) 这种表现形式实际上是一种叫做 ZigZag 编码的解码算法，ZigZag 编码是一种将有符号整数编码为无符号整数的方法，使得绝对值较小的负数和正数都能使用较少的字节表示，从而提高编码效率。var C = function A(A) { return (A >> 1) ^ -(1 & A); };那么现在还剩下B数组的生成方式未知了，B数组由传入的Base64编码通过I函数生成，I函数实际上是将一个 Base64 编码的字符串转换为一个字节数组。它首先将输入字符串去掉尾部的等号，然后将每个字符转换为对应的 Base64 索引值，最后将这些索引值组合成字节数组。双虚拟机结构分析加载器折…

cloudflare逆向(4)——奶奶都看得懂的5秒盾ast还原（2024）
type: item
date: 2024-08-01T20:35:00+08:00
description: 提示：右下角可以点击查看目录流程分析我们之前在 cloudflare逆向(3)js请求 中分析了他的请求流程，在js加密流程中，主要分为这四步：预处理：在第一个页面的script标签中进行初始化，主要围绕window._cf_chl_opt进行，并加载初始的反爬脚本第二步：向 /cdn-cgi/jsch/v1?ray=::rayId:: 发送初始GET请求，获取经动态混淆的加密脚本第三步：获取另一个post请求的加密内容，并加载进当前页面进行环境检测通过生成的md、r、sh等参数，获取获取5s盾的cookie，最后可携带cookie请求真实网页第一个预处理js，可从script标签中提取出来，(function(){...})();表示立即执行该匿名函数，仅做一些初始化工作，因此不需要过多解读(function() { window._cf_chl_opt = { cvId: '3', cZone: "paoluz.link", cType: 'non-interactive', cNounce: '31152', cRay: '8bdafe9fbcf0099b', cHash: '8530c6de9e8c3dc', cUPMDTk: "\/?__cf_chl_tk=coi3.ydg2.eGDziNfk7QyQegh9ZiKQ.0O4QZQIb5Ys8-1725421608-0.0.1.1-7572", cFPWv: 'g', cTTimeMs: '1000', cMTimeMs: '120000', cTplV: 5, cTplB: 'cf', cK: "", fa: "\/?__cf_chl_f_tk=coi3.ydg2.eGDziNfk7QyQegh9ZiKQ.0O4QZQIb5Ys8-1725421608-0.0.1.1-7572", ……… }());从第二步开始，我们可以将解密工作分为三层，第一层就是如下的代码了，约为5600行+。可以看出大量使用了花指令和字符串加密，如果直接使用扣代码或人肉跟栈分析，难度较大通过对原始文件的分析，我们可以确定cloudflare使用了多种混淆技术，包括但不限于：变量名混淆：使用无意义的变量名字符串混淆：对字符串进行混淆和解码，使得字符串内容不易被直接可读控制流：移除原有的if节点，使用复杂的switch和while循环结构实现跳转，代替顺序逻辑死代码插入：插入看似有用实则无用的代码片段，加大逆向工作量花指令：将运算符抽取为函数，让加减乘除也变得不可读内联函数：将函数定义嵌入到其他函数中，将多个函数合并成一个复杂的函数，使得函数的实际用途难以辨别。这些混淆技术的目的是增加代码的复杂性，使得逆向工程和理解代码变得更加困难，不过好在一层的代码量较少，初学者进行ast分析时可以快速查看到当前的结果。不ast可以帮我们解除大量的混淆，但是在去混淆后，我们仍然要进行逆向分析，在学习ast还原前，一定要掌握好js基础和逆向技巧，仅仅使用ast是不够的。花指令还原既然说ast无法一步到位，那我们为什么要多做这一步呢？直接扣代码和补环境不好吗？做ast分析第一步，我们要先观察一下整个js整体的加密情况，然后按照难易程度对相对应的加密方式进行排列，这样才能先从简易的地方下手，还原出我们需要的内容随便翻两下，从60行开始，发现这里将一些双目运算符提取出来了，这是一种最基础的花指令方式，与2023年的5s盾不同的是，以前是将所有的花指令集中存放，而2024是将这样的代码分散于各处，例如这里仅有3个指令，而其他的花指令则是被隐藏到了不同的对象中 'CsuEb': function (g, h) { return g + h }, 'HJrLT': function (g, h) { return g * h }, 'nlhGX': function (g, h) { return g << h }想象一下，1+2*3变成了obj.HJrLT(obj.CsuEb(0,1),obj.HJrLT(3,2))时，阅读起来该有多崩溃我们可以用ast先检查一下都有哪里调用了这些，如何检查呢？第一步，我们首先要提取出节点的特征，可以看出，5s的花指令特征还较为简单，普遍为函数套一个return，我们可以检测函数节点下的ReturnStatement是否包含BinaryExpression等节点function isObfuscatedFunction(node) { if (node.type === 'FunctionExpression' && node.body.body.length === 1) { const statement = node.body.body[0]; if (statement.type…

公安部ob混淆ast还原实战
type: item
date: 2024-04-18T18:28:00+08:00
description: 目标站点：aHR0cHM6Ly93d3cubXBzLmdvdi5jbi9pbmRleC5odG1s混淆样本可以通过扣代码或补环境的方式执行，本文不采用上述方案，只分享ast反混淆思路，供安全行业分析学习还原效果本次混淆样例为经典的ob变种混淆，在请求网站时，会随机返回三种加密算法，通过babel的ast分析功能，我们可以动态的获取到反混淆后的js，原本样本1500行代码在反混淆后还原为76行，代码量减少了95%，还原了纯算算法初步分析打开某目标网站，发现加密参数寻找一下具体的加密参数位置，将混淆代码dump下来，放入ide进行分析，可以看到，代码大致由5部分组成大数组：用于字符串加密混淆自执行函数：用于将加密数组进行解密hash函数：用途非常明显，用于获取hash值其他函数：用于具体加密逻辑主函数入口go：用于执行传入的内容，并和浏览器api进行交互这种字符串大数组+混淆的思路，基本上可以确定是ob混淆变种，我们反混淆的思路也很明确：获取解密后的字符串，回填常量，解除花指令混淆，删除死代码，最后还原整个流程字符串解密分析代码结构从他的主函数go进入，可以发现首先声明了一个 _0x41e2c2 对象，随后对其成员进行了大量操作在使用方括号声明成员时，大量调用了_0x1b8d这个函数，说明 _0x1b8d 可能为字符串解密操作，我们对这个函数进行一下测试，发现果然是字符串解密函数console.log(_0x1b8d('0x31', 'H&r$')) //输出：cTYc利用ide查看大数组引用，可以看到大数组仅在解密函数_0x1b8d中和另一个自执行函数中有所引用查看一下自执行函数，并没有任何外部引用，这段代码是一个典型的数组打乱片段，它的作用是对数组 _0x19984c 进行循环移位操作。具体来说，它会将数组中的元素向前移动若干位置，类似于一个环形缓冲区(function (_0x19984c, _0x51da47) { // 函数体 }(_0x51da, 0x6a));这是一个自执行函数，立即调用并传入两个参数 _0x51da 和 0x6a（十六进制的106）var _0x1b8ded = function (_0x3f4bdb) { while (--_0x3f4bdb) { _0x19984c['push'](_0x19984c['shift']()); } };这个函数接受一个参数 _0x3f4bdb，并在一个 while 循环中不断执行以下操作，直到 _0x3f4bdb 递减到0：_0x19984c['shift']()：移除数组 _0x19984c 的第一个元素并返回该元素。_0x19984c['push']：将移除的元素添加到数组 _0x19984c 的末尾。_0x1b8ded(++_0x51da47);调用 _0x1b8ded 函数，并传入 _0x51da47 加1后的值（即107）。因此我们可以得知，这段代码的作用是将数组 _0x19984c 中的元素循环移位107次。每次移位操作将数组的第一个元素移到末尾，最终实现数组元素的重新排列。判断解密操作是否幂等在浏览器环境中将整个自执行函数复制，并反复执行字符串解密函数，发现输出结果一致，说明该字符串解密函数的执行操作幂等在本地执行，发现生成结果和浏览器一致，那么说明这段代码并未进行环境检测，我们后续可以进行本地调试还原流程回过来观察一下我们需要解密的代码的特点，可以得到以下还原思路将本地的大数组打乱函数扣出执行，得到位置更改后的大数组利用ast工具找到代码中_0x1b8d的引用位置将解密函数直接扣出，对代码中的字符串加密部分进行还原拼接字符串常量我们先利用ide的debugger工具，将大数组还原，并删除数组打乱自执行函数，将数组覆盖回代码查看一下解密函数的定义var _0x1b8d = function (_0x19984c, _0x51da47) {/.../}_0x1b8d 是一个变量定义的节点，且定义的标识符名称固定，我们利用这点找一下符合这个变量特征的引用// 遍历 AST traverse(ast, { VariableDeclarator(path) { const { id } = path.node; // 节点为变量定义且标识符名称为_0x1b8d if (id.name =='_0x1b8d') { // 获取作用域下的引用 const binding = path.scope.getBinding(id.name); if (binding && binding.constantViolations.length == 0) { // 获取引用节点 for (const { parentPath } of binding.reference…

jsvmp编译与反编译详解 (2)——字节栈式虚拟机反编译
type: item
date: 2024-03-13T04:02:00+08:00
description: ast预反混淆以抖音为例，首先获取webmsdk.js，可以看到大约一共9600行，这为我们的逆向分析工作增添了不少难度，因此我们的首要工作是将代码反混淆后再进行分析通过ast解混淆处理后，代码量削去了一半，并且大部分字符串也被还原，这一步为基础内容，不进行详细讲解补上缺失的环境处理一下canvas指纹，注意，canvas指纹是和硬件关联的，通过海量的收集真实设备从而达到判断哪些设备是伪造的。一般情况下随机生成一个也是能生产参数的，但是不一定能够通过服务器校验。而抖音这里在服务端没有做校验，我们可以放心使用进一步处理，在解决掉环境参数和canvas后，导出我们需要的目标函数，实际上已经能够获取到我们所需的加密参数了为什么我们一定要先ast进行还原，然后补环境拿到参数呢，直接分析不好吗？因为这样生成的代码可以保证我们能够在各种环境中正常地生成参数，只有这样我们才能更好地利用ide的调试工具，如果使用浏览器的调试工具，那么会大大降低逆向效率。当然，如果你不想学习vmp反编译的话，到这里也可以结束了。借助ide对函数结构分析仔细观察一下，按照我们上文的理解，vmp在执行时一定会有大段的字节码传入，因此这是非常显著的一种特征。注意图中我标注的vmpstr，整个参数的的生成，是通过_0x5a8f25这个函数进行的，而其中就有我们需要的vmp字节码直接跟栈进入，找到_$webrt_1668687510这个函数，搜索一下函数名，能看到有大量传入字节码的操作，那么这个就是我们的vmp主入口了不幸的是，整个入口函数足足有900多行，即使在ast还原过后，其中也充斥着大量的混淆代码和看不懂的标识符。这时万不要在原本的打断代码中盲目硬分析，我们可以将这段代码复制出来，粘贴到新的文件中，再次进行分析这时候ide的作用就显现了，利用vscode的大纲功能，我们可以看到这个函数是由7个闭包函数和大量的变量组成的，而如果利用浏览器，再未反混淆的情况下，仅仅这个函数就有3000行，我们将需要在浏览器中无任何工具的情况下进行分析经过对这几个函数的观察，除_0x1218ef以外，其他函数都比较简短在函数体最长的_0x1218ef中断点，能够看到传入了整段字节码查看函数体，发现整个函数体是由一个看似为for实则为while条件循环来控制的控制流，其中有大量的if分支跳转，那么这个就很有可能是虚拟机本身了，至于为什么靠这两点可以定位vmp，我们先按下不表，这里的分析较为复杂，我们先分析较为简单的其他函数虚拟机外层函数简化反射对象检测查看第一个闭包，检测当前JavaScript环境是否完全支持 Reflect.construct 和 Proxy，并且进行了字符串格式化检测，确保没有任何“假冒”实现。因此可以得出简化方案：直接返回true，或使用ast将调用_0x2f9ebc处全部替换为true。 function _0x2f9ebc() { if ('undefined' == typeof Reflect || !Reflect['construct']) { return false; } if (Reflect['construct']['sham']) { return false; } if ('function' == typeof Proxy) { return true; } try { Date['prototype']['toString']['call'](Reflect['construct'](Date, [], function () { })); return true; } catch (_0x1a9721) { return false; } }第二个函数 _0x265a42 ，调用了_0x2f9ebc来检测Reflect，作用是根据当前环境是否支持 Reflect.construct 方法来决定如何创建一个新的对象实例。具体来说，它会根据 _0x2f9ebc 函数的返回值来选择不同的实现方式。function _0x265a42(_0x3ce31a, _0x217675, _0x5569d0) { return (_0x265a42 = _0x2f9ebc() ? Reflect['construct'] : function (_0x2dc1b1, _0x54ae53, _0x5bec15) { var _0x2a793f = [null]; _0x2a793f['push']['apply'](_0x2a793f, _0x54ae53); var _0x60cd25 = new (Function['bind']['apply'](_0x2dc1b1, _0x2a793f))(); _0x5bec15 && _0x59d7fb(…

jsvmp编译与反编译详解 (1)——实现一个简单的jsvmp
type: item
date: 2024-03-09T10:52:00+08:00
description: 什么是jsvmp目前，主流的JavaScript代码保护措施主要包括精简、加密和混淆。这些方法的核心思想主要借鉴了传统的软件代码保护技术。然而，由于JavaScript是一种脚本语言，其在传输过程中以带有语法属性的文本源码形式存在，逆向分析比传统的编译后的二进制应用程序更加容易。再加上浏览器性能的提升和调试器功能的日益完善，这些保护方法难以提供有效的保护。jsvmp（JavaScript Virtual Machine Protection）是一种用于保护JavaScript代码的技术，类似于我们平常看到的代码混淆。但不同的是，它通过将JavaScript代码转换为一种虚拟机指令集来实现代码混淆和保护，具体的执行逻辑由vmp虚拟机来执行，这样可以防止代码被轻易地反编译和理解，从而提高代码的安全性。对于普通的javascript代码来说，执行逻辑是这样的：JavaScript代码 -> 词法分析/语法分析 -> 生成AST语法树 -> 生成js指令-> js引擎执行代码而对于经过vmp混淆后的代码，执行逻辑是这样的，中间多了这么一段：…… -> 生成AST语法树 -> 将AST转换为vmp指令集 -> 加载进vmp虚拟机 -> js引擎执行代码在vmp混淆的代码流程中，防护者实现了一个虚拟机来解释和执行这些指令集，并对对虚拟机和指令集进行混淆，这种工作类似于将高级语言编译为汇编的过程，因此，对混淆代直接人肉分析几乎是不可能的（图片来自梓潼blog）为什么不用wasm有些人可能会问，为什么wasm呢？wasm不是能直接编译成字节码，并高效运行吗？wasm有一个致命的弱点，即wasm和js运行时的环境是隔离的，本身无法直接访问js对象的属性，只能以模块的方式导入导出，这个就决定了它在收集环境参数上受限很大。2020年就有使用wasm的网站，几年过去后，也并未出现大的发展。其次，wasm在执行时必须借助js实现对属性的操作部分，这样频繁的在wasm和js之间交互肯定会带来性能的影响，降低执行效率。同时还需要附带大量代码负责维护两个模块的通信，这势必会带来新的空间开销。此外，目前可以发现，国内各大厂商的vmp实现几乎天差地别，难以出现通用的反混淆方案，jsvmp的虚拟机设计可以灵活地适应不同的保护需求，相比ast混淆的方式，能够提供更高的安全性，现在对于js混淆来说，已经有了成熟的解混淆方案。尽管在性能上可能会有一定的开销，但在保护代码安全性方面，jsvmp无疑是一个强有力的工具。jsvmp示例你能看出这段代码在干什么吗？function _0x4dff2d(_0x1935a8) { return ('undefined' == typeof window ? global : window)['_$webrt_1668687510']('484e4f4a403f5243002c210c79d545fd138caff600000000000007561b000b180201fe19203e17000e1b000b180201ff0202000d1b00131e00061a002248001d00a922201d0201220a00001d0202220a00001d020322121d01e222121d01fe220200001d01ff22121d020422131e00061a00224805483c2a1d02052248021d011f224805483c2a1d011d1d011c220200001d01f322201d02062248031d02071d00911b000b02221e0126241b000b191b000b180a0002101c1b000b191e00a948003e22011700201c1b000b03221e0108241b000b191e00a90a0001101b000b191e00a94017000d1b000b040202081a01471b000b191e02092217000c1c1b000b191e020a0117000d1b000b0402020b1a01471b000b051e0123221e00ce241b000b191e00a90a0001101c1b000b061e00a948003e1700111b000b061b000b191e00a91d00a91b000b191e0201011700c11b000b191e01ff0200003e17000d1b000b0402020c1a01471b000b191e01ff0202003f17000d1b000b0402020d1a01471b000b061b000b191e01ff1d01ff1b000b061b000b071b…

600行代码实现一个chip-8虚拟机
type: item
date: 2024-03-02T05:13:00+08:00
description: 介绍CHIP-8 是一种解释型语言，设计之初就是为了编写简单的小游戏。我猜是作者嫌老机器的汇编语言太复杂繁琐，从而自己设计了一门汇编语言，并且摆脱硬件的束缚，在模拟器上运行。其实这个思想和 Java 等基于虚拟机的高级语言也是类似的，提供方便程序员编写的指令集，在硬件之上空架一层虚拟机，实现 “Write Once, Run Everywhere”。我们的目的是在python中实现一个chip8虚拟机CHIP8 虚拟机的结构和现代的计算机一样，虚拟起也有内存，CPU，寄存器等计算机的“基础零件”：Memory：CHIP-8 最多有 4096 字节的内存CHIP-8 解释器本身占用这些机器上的前 512 字节内存空间。因此，为原始系统编写的大多数程序都从内存位置 512 (0x200) 开始，并且不会访问位置 512 (0x200) 以下的任何内存。最上面的 256 个字节 (0xF00-0xFFF) 保留用于显示刷新，下面的 96 个字节 (0xEA0-0xEFF) 保留用于调用堆栈、内部使用和其他变量。Program Counter：16 位的 PC，记录当前程序指令运行的内存位置，因为需要访问最多 4K 的内存（0xFFF）Stack：16 位地址的堆栈，用于调用函数和返回。栈调用深度最初设计位 12 层，可以自行调整。Registers：16 个 8 位数据寄存器（data register），名为 V0 至 VF。 VF 寄存器兼作某些指令的标志；因此，应该避免这种情况。在加法运算中，VF 是进位标志，而在减法运算中，VF 是“无借位”标志。在绘制指令中，VF 在像素冲突时设置。一个 16 位索引寄存器（index register），用于记录内存地址Timers8 位延迟定时器，以 60 Hz（每秒 60 次）的速率递减，直至达到 08 位声音定时器，当其值非零时，会发出蜂鸣声。Display：64 x 32 像素（或 128 x 64 对于 SUPER-CHIP）单色，即黑或白Inputs：16 个输入键，与前 16 个十六进制值匹配：0 到 F。上面就是 CHIP-8 虚拟机的全部“硬件”了，接下来就是实现用软件模拟硬件。导入模块import pygame import sys import random import os我们需要实现图形界面、系统操作、随机数生成器和CHIP8指令集寄存器class Register: def __init__(self, bits): self.value = 0 self.bits = bits def checkCarry(self): hexValue = hex(self.value)[2:] if len(hexValue) > self.bits / 4: self.value = int(hexValue[-int(self.bits / 4):], 16) return 1 return 0 def checkBorrow(self): if self.value < 0: self.value = abs(self.value) return 0 return 1 def readValue(self): return hex(self.value) def setValue(self, value): self.value = valueRegister 类表示一个寄存器，包含寄存器的值和位数。checkCarry 和 checkBorrow 方法用于检查进位和借位。延迟计时器CHIP-8 有两个独立的定时器寄存器：延迟定时器和声音定时器。 大小为 1 个字节，只要它们的值大于 0，它们就应该每秒减少 60 次（即 60 Hz ），并且与执行指令的速度无关。 也就是说无论代码怎么执行，即使进入了死循环，定时器也需要以 60 Hz 的频率运行。class DelayTimer: def __init__(self): self.timer = 0 def countDown(self): if self.timer > 0: self.timer -= 1 def setTimer(self, value): self.timer = value def readTimer(self): return self.timer这样可以设计一个最基础的延迟计时器，包含计时器的值和倒计时方法。蜂鸣器class SoundTimer(DelayTimer): def __init__(self): DelayTimer.__init__(self) def beep(self): if self.timer > 1: os.system('play --no-show…

GFW的SMTP协议TCP重置攻击分析
type: item
date: 2024-02-05T16:40:00+08:00
description: 发现发件人在黑名单中，立即重置TCP链接from scapy.all import * send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0)) send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1)) send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'MAIL FROM: xiazai@upup.info\r\n') 看似普通的三个包其实暗藏玄机。首先，目标地址是1.2.3.4，这显然是我胡写的一个地址，而且ttl设置为9。所以这个包发出去就没有打算让最终的目标机器接到，而只是发给GFW看的。这个ttl值要大于你的机器到GFW的跳数，一般11是一个保险的值。然后要触发GFW的响应，有以下几个缺一不可的条件目标端口是25，我尝试了其他几个端口没有发现触发响应第二个包虽然内容是空的，但是必须存在。而且必须是ACK。内容也可以不为空，GFW似乎不care内容是什么，只要有这个包就可以。第三个包的seq必须为1，哪怕第二包有内容了，这个包的seq也必须为1。而且MAIL FROM: \r\n这个格式必须对，不能替换成FROM MAIL啥的。还有一个条件就是邮件地址必须上了黑名单。这里举的例子是一个翻墙软件的索取地址，所以上了黑名单。观测到的响应是GFW发一个TCP RST包。而且每次都是一个，从来不多发。如果少了中间那个空的ACK，则是连做两次探测触发一个TCP RST。貌似GFW把两次探测认为是一个连接了。发现收件人在黑名单中，立即重置TCP链接from scapy.all import * send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0)) send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1)) send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'RCPT TO: xiazai@upup.info\r\n') 这与上面的MAIL FROM的例子基本上是一样的。不同之处只有一点就是有的时候可以看到两个TCP RST的回包。发现收件人在黑名单中，发回用户不存在的错误消息from scapy.all import * send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='S', seq=0)) send(IP(dst='1.2.3.4', ttl=9) / TCP(dport=25, flags='A', seq=1) / 'EHLO anything-here\nRCPT TO: xiazai@upup.info\n') 得到的错误消息是 “551 User not local; please try \r\n”。有的时候还会伴随有数个连续的TCP RST。同样因为包根本没有到对方的服务器，而且这个服务器压根就不存在，所以这个用户不存在的错误消息只能是GFW做出的响应。触发的条件是目标端口必须是25第二个包的第一个命令必须是EHLO或者HELO，内容没有关系第二个包的第二个命令必须是RCPT TO，而且邮件地址要在黑名单中。\r\n没有关系，\n也是可以触发的这次触发的条件是一个合法的SMTP请求过程。而之前的触发过程根本就不是合法的SMTP请求。而且另外一个特征是这样触发的TCP RST，会有三个重叠，ack递加的现象，与HTTP全文关键字的响应非常类似。我推测，两型的响应是两个不同的模块。单独对MAIL FROM和RCPT TO的封锁，与对HTTP关键字的封锁类似，属于看到就封型。而后一种更智能的，还会回答错误消息的是能够真正理解SMTP协议的模块，可能还用于做邮件全文内容的关键字检测。属于深包中的深包检测了

poetry如何更换国内源（清华源/阿里源/豆瓣源）
type: item
date: 2024-01-04T15:56:00+08:00
description: 有两种换源方式，全局源或虚拟环境源，区别在于:全局源设置好一劳永逸，后续建立新的虚拟环境不需要重新配置。虚拟环境源可以在不同的虚拟环境中使用不同的源，更加灵活，但一般情况下用不到，大多用于测试PyPI测试平台上的源全局源如果你希望只用设置一次镜像源，后续在创建新项目时不需要重新配置镜像，可以如此操作推荐使用poetry-plugin-pypi-mirror 插件，可以方便地给poetry换源第一步：添加poetry插件poetry-plugin-pypi-mirrorpipx 方案如果你使用的是poetry官方推荐的pipx方案安装的poetry，可以利用pipx安装此插件：pipx inject poetry poetry-plugin-pypi-mirror 注意，如果pipx注入插件安装失败，是因为pipx也需要使用pip进行安装，可以在这条命令后添加如下命令以使用镜像安装插件--pip-args="--index-url https://pypi.tuna.tsinghua.edu.cn/simple" poetry 插件方案如果你使用的是pip安装的poetry，或直接使用官方提供的shell脚本（powershell/bash/zsh/fish）在系统中安装的poetry，可以通过poetry的self add添加插件poetry self add poetry-plugin-pypi-mirror 这样就将poetry全局源插件安装上了第二步：修改插件的默认源假设我们现在需要添加腾讯的镜像源到poetry：https://mirrors.cloud.tencent.com/pypi/simple/方法一：添加配置文件（⭐推荐）新建或修改配置文件 config.toml，根据不同的系统，在不同的目录下创建poetry-plugin-pypi-mirror插件的配置文件Windows系统:%APPDATA%\pypoetry\config.tomlLinux系统~/.config/pypoetry/config.tomlMac系统(2025/04/07 修复了路径的错误):~/Library/Application\ Support/pypoetry/config.toml[plugins] [plugins.pypi_mirror] url = "https://mirrors.cloud.tencent.com/pypi/simple/" 方法二：添加系统环境变量（修改不如上种方式方便）Linux系统环境变量在终端中执行以下命令，可以对当前终端会话生效，但重启终端或系统后失效export POETRY_PYPI_MIRROR_URL=https://mirrors.cloud.tencent.com/pypi/simple/或是添加系统级环境变量，终端在每次打开时会自动加载环境变量：vim /etc/profile POETRY_PYPI_MIRROR_URL=https://mirrors.cloud.tencent.com/pypi/simple/ export POETRY_PYPI_MIRROR_URL source /etc/profile Windows系统环境变量此电脑->右键属性->高级系统设置->高级->环境变量添加变量名POETRY_PYPI_MIRROR_URL，值为https://mirrors.cloud.tencent.com/pypi/simple/虚拟环境源如果你想让源仅在单个的虚拟环境中生效，可以如此操作设置虚拟环境源不需要使用插件，官方的设置文档有详细操作：https://python-poetry.org/docs/repositories/在项目中指定备用源编辑项目根目录下的 pyproject.toml 文件，增加或修改 [tool.poetry.source] 部分，例如使用清华大学的镜像：[tool.poetry.source] name = "pypi-tuna" url = "https://pypi.tuna.tsinghua.edu.cn/simple" priority = "primary"这里设置 priority = "primary" 即表示该源为首选源，覆盖默认的pypi源使用命令行添加或切换源你可以使用 poetry source 命令添加新的源，然后安装依赖时指定使用该源。例如：poetry source add pypi-tuna https://pypi.tuna.tsinghua.edu.cn/simple poetry install --source pypi-tuna完成检验修改完成后，切换回项目的poetry环境，执行poetry…