Nansen

对于 DCU 安全编程这门课的反汇编,由于题目有一定套路,使用固定的解题思路,可以快速解题

前置技能

  1. 熟悉各种汇编命令 汇编指令
  2. 熟悉 % 和 $ 表示寄存器和立即数 $ 与 % 寄存器与立即数
  3. 熟悉直接寻址和间接寻址 直接寻址与间接寻址
  4. 了解一个case C语言代码到汇编的例子

解题思路

  1. 找到入参个数
  2. 找到局部变量个数
  3. 识别循环体
  4. 解析剩余代码片段
  5. 识别返回数据

找到入参个数

ebp的位置是saved frame ptr,ebp+4的位置是return address。由于题目往往是约定入参全部为int 或者 int * 类型,因此 ebp+8,ebp+c,ebp+10的位置,分别是入参的第一个参数,第二个参数,第三个参数。

因此,在代码中快速浏览,寻找 0x__(%ebp) 的字样,找到最大的偏移量。(偏移量-4)//4 的值就是入参的个数。

例如,下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push %ebp                   <foo+0>
mov %esp, %ebp <foo+1>
sub $0x4, %esp <foo+3>
mov 0x8(%ebp), %eax <foo+6>
mov %eax, -0x4(%ebp) <foo+9>
mov -0x4(%ebp), %eax <foo+12>
cmp 0x10(%ebp), %eax <foo+15>
jge <foo+32> <foo+18>
mov 0xc(%ebp), %eax <foo+20>
incl (%eax) <foo+23>
lea -0x4(%ebp), %eax <foo+25>
incl (%eax) <foo+28>
jmp <foo+12> <foo+30>
mov $0x0, %eax <foo+32>
leave <foo+37>
ret <foo+38>

我们发现存在 0x10(%ebp) 因此,参数个数为(16-4)/4 ,也就是是3个。

因此我们可以写出代码的框架,如下

1
2
3
int foo(int a, int b, int c) {

}

其中 a, b, c 存在的栈的位置分别是 ebp+8,ebp+c,ebp+10

注意,参数从后向前入栈,因此越接近ebp,参数在参数列表中越靠前。

注意,我们这里先假设全部都是int类型,如果后续有遇到不一致的情况,我们再来修改

找到局部变量个数

局部变量的个数在代码第三行,sub $0x4, %esp 这里减去的数量,就是分配的局部变量的长度。

在上面的代码中,显然这里分配了4字节,也就是只有一个局部变量。我们假设这个局部变量就是int类型,起名为i。

我们继续扩展我们的代码

1
2
3
int foo(int a, int b, int c) {
int i;
}

识别循环体

循环体通常是while循环或者for循环。

对于常见的while命令,我们寻找下面相关的指令:

  • 判断入口:

判断入口是一个连续的比较命令(如cmp)和一个跳转命令(如jge或jle)组成。找到判断入口有助于我们识别代码中是否有判断或者循环。

  • 循环:

假如没有循环标志,那么就不是循环体,而是简单的if判断。循环是一个无条件的jmp命令。循环所跳转的位置就是判断条件的开始。

  • 判断条件:

判断条件是判断入口以及判断入口和前面几条命令组合成的完整判断条件。判断条件就是while括号里的内容。

现在我们查看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push %ebp                   <foo+0>
mov %esp, %ebp <foo+1>
sub $0x4, %esp <foo+3>
mov 0x8(%ebp), %eax <foo+6>
mov %eax, -0x4(%ebp) <foo+9>
mov -0x4(%ebp), %eax <foo+12>
cmp 0x10(%ebp), %eax <foo+15>
jge <foo+32> <foo+18>
mov 0xc(%ebp), %eax <foo+20>
imul (%eax) <foo+23>
lea -0x4(%ebp), %eax <foo+25>
imul (%eax) <foo+28>
jmp <foo+12> <foo+30>
mov $0x0, %eax <foo+32>
leave <foo+37>
ret <foo+38>

判断入口:

我们寻找一个连续的判断指令和一个跳转指令,我们可以找到 cmp 和 jge 这两个连续指令,就说明这里是判断入口。

循环:

我们看到,最后存在一个jmp命令,jmp命令指向了第foo+12的位置,因此foo+12就是循环的判断条件。

判断条件:

判断条件的第一句是 mov -0x4(%ebp), %eax ,其中 -0x4(%ebp) 是最后一个局部变量,由于我们只有一个局部变量 i ,因此这句话的意思是,将本地变量i的值赋值给 eax。

我们看到cmp的命令是:cmp 0x10(%ebp), %eax ,其中cmp 0x10(%ebp) 指第三个入参的值。这句话的意思是计算 eax 寄存器中减去第一个入参的数值,也就是c。

联合起来,我们就知道,这里是计算 i - c的值,并交给 jge 来做跳转。

由于汇编的条件判断和C语言条件判断是反过来的,汇编是满足判断则跳过循环体,而C语言是满足条件则执行循环体,因此我们的判断条件也是反过来的。

jge表示大于等于0则跳过循环体,因此C语言的循环条件是小于0则执行循环体,也就是判断 i - c 是否小于 0

因此我们可以继续完善我们的代码:

1
2
3
4
5
6
int foo(int a, int b, int c) {
int i;
while (i - c < 0) {

}
}

解析剩余代码片段

首先标记下已经处理过的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push %ebp                   <foo+0> 无用
mov %esp, %ebp <foo+1> 无用
sub $0x4, %esp <foo+3> 分配了几个局部变量
mov 0x8(%ebp), %eax <foo+6>
mov %eax, -0x4(%ebp) <foo+9>
mov -0x4(%ebp), %eax <foo+12> 判断条件开始
cmp 0x10(%ebp), %eax <foo+15> 判断内容
jge <foo+32> <foo+18> 判断条件结束
mov 0xc(%ebp), %eax <foo+20>
incl (%eax) <foo+23>
lea -0x4(%ebp), %eax <foo+25>
incl (%eax) <foo+28>
jmp <foo+12> <foo+30> 循环结束
mov $0x0, %eax <foo+32>
leave <foo+37>
ret <foo+38>
  1. 循环体之前的代码

我们看一下循环开始之前的部分:

1
2
mov 0x8(%ebp), %eax         <foo+6>
mov %eax, -0x4(%ebp) <foo+9>

我们发现这两句都涉及到了 eax 寄存器

对于多条涉及eax 寄存器的操作,我们尽量当作一个整体来看

我们发现,其实这里就是将 0x8(%ebp) 的数值 赋值给了 -0x4(%ebp) ,这两个值分别是 a 和 i

因此这个部分的代码是

1
i = a
  1. 循环体中间的代码
1
2
3
4
mov 0xc(%ebp), %eax         <foo+20>
incl (%eax) <foo+23>
lea -0x4(%ebp), %eax <foo+25>
incl (%eax) <foo+28>

对于涉及eax的多条指令,我们不要一条一条看,而是看作一个整体。

其中,mov 0xc(%ebp), %eaxincl (%eax) 可以看作 incl (%b) 注意,由于这里是间接寻址,因此,这意味着,b中存的是地址。因此我们需要修改我们的入参,b这里不是数值,而是地址。这句代码意味着

1
(*b)++ 

注意

*b++ 会先对 *b 解引用,再对 b 自增

因此这里括号不能省略,否则会有问题

而对于 lea -0x4(%ebp), %eaxincl (%eax) ,可以看作 incl (&i) ,这里继续是间接寻址,也就是对后者地址上的数值自增。这句代码意味着 *(&i)++ ,可以简化为

1
i++ 

之所以这么复杂,是因为汇编指令不能直接对地址进行自增操作,但是可以对一个包含地址的寄存器做自增操作。

因此,补充了剩余的代码片段,代码应该为

1
2
3
4
5
6
7
8
int foo(int a, int *b, int c) {
int i;
i = a;
while (i - c < 0) {
(*b)++;
i++;
}
}

识别返回数据

按照C语言的x86的调用约定,返回数据放在eax中。

1
mov $0x0, %eax

很显然,这里直接返回了0

最终代码

1
2
3
4
5
6
7
8
9
int foo(int a, int *b, int c) {
int i;
i = a;
while (i - c < 0) {
(*b)++;
i++;
}
return 0;
}

在之前的文章中,我使用了 Obsidian 的 QuickAdd 来创建一个脚本,自动转换从 ChatGPT 中复制的文本,修复其中的 LaTeX 格式。然而,对于 Craft 这款应用,并没有合适的插件可以使用。

我们可以通过 Raycast 来实现这个功能的统一操作。


创建 Raycast 脚本

首先,我们需要创建一个脚本。

image

接着选择 Bash 模板。

然后,我们编辑这个 Bash 脚本,输入如下代码:

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
#!/bin/bash

# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Copy From ChatGPT
# @raycast.mode silent

# Optional parameters:
# @raycast.icon 🤖
# @raycast.packageName ChatGPT Utils

# Documentation:
# @raycast.description Copy From ChatGPT
# @raycast.author Nansen Li
# @raycast.authorURL nansenli.com

# 获取剪贴板内容
clipboard_content=$(pbpaste)

# 检查是否成功获取内容
if [ -z "$clipboard_content" ]; then
echo "剪贴板为空或无法访问。"
exit 1
fi

# 处理剪贴板内容
modified_content=$(echo "$clipboard_content" | \
sed 's/\\\[/$$/g; s/\\\]/$$/g; s/\\( /$/g; s/ \\\)/$/g')

# 将修改后的内容写回剪贴板
echo "$modified_content" | pbcopy

创建完脚本后,我们还需要将脚本所在的目录添加到 Raycast 中。

image

在这一步中,选择刚刚创建的脚本目录。此时,我们可以在 Script Commands 中看到刚刚创建的脚本。


如何使用

在复制完 ChatGPT 的公式后,打开 Raycast 的面板,找到刚刚的脚本并运行,此时剪贴板中的内容就会被自动修复。接下来,只需将其粘贴到 Obsidian 或 Craft 中即可。

背景介绍

我是Nansen,参与了2024年华为爱尔兰研究中心的服务器集群管理优化比赛。在此,我想分享一下这次比赛的经历,并对其中的关键点进行总结。

我们的算法代码部分可以参考这里: huawei2024

比赛结果

我们在算法部分获得了第一名,分数比第二至第四名高出约4%-5%,取得了巨大的优势。然而,在演讲环节我们遇到了很大的挑战。首先,我们意识到英语表达方面有提升空间;其次,我们发现演示PPT还可以更精美突出;最后,在时间安排上也存在一些挑战。不过,尽管如此,我们还是在总分上取得了第三名的成绩。

比赛过程

比赛分为两个阶段。第一阶段有较长的准备时间。当我们确定使用模拟退火算法后,就开始了算法的开发。第一阶段的难点主要在于优化和理解题目需求。在开发过程中,我们遇到了许多bug,但在修复后,分数有了明显提升。

第二阶段,由于题目在比赛当天才发布,我继续优化第一阶段的算法,成功将评估算法的运行速度提升了1000倍。这显著提高了我们在第二阶段的表现,让我们有足够的实力争夺第一名。

在决赛阶段,我们的算法表现非常稳定,并在调整后大幅领先对手。然而,由于我们没有足够重视PPT的制作,我们以为只要算法表现好,排名靠前,就能获得高分,但事实证明我们错了。

经验和教训

算法选择

很幸运,在最初的阶段我就选对了最终的算法,并且在题目发布后不久就构思出了适用于整个比赛的算法框架。然而,我也走了一些弯路,比如尝试了一些不可行的算法(如PPO算法)。在初步尝试无果后,我应该及时停止,而不是继续浪费精力。由于时间有限,我们应追求最短时间内的最优效果,而不是追求一个不切实际、理想中的方案。同时,也要认清自己的能力边界,专注于在短时间内能够实现的目标。

团队分工

很幸运,这次团队的分工是合理的,我尽力确保每个成员都能发挥自己的价值。改进之处在于,应该更多地与队员沟通,了解他们的意愿和想法。由于我主要负责算法部分,与队友的沟通相对较少,下一次我会在这方面做得更好。

PPT的制作

我们没有预料到,其他参赛队伍的PPT水平如此之高。我的队友猜测,他们可能有商科背景,这使得他们在制作PPT时具有优势。此外,他们的团队有五名成员,而我们只有三名,这也让我们在人员配备上处于劣势。这些都是客观上的挑战,但如果我们更加重视PPT的制作,或许第一名就是我们的。

过分投入导致失衡

在决赛阶段,其实我们的算法已经非常出色,分数也超过了此前排名第一的队伍。然而,我仍然花了大量时间继续优化算法,尽管我们的算法分数领先对手很多,但却因此忽略了PPT的准备。事实上,我应该适可而止,并充分理解评分标准的重要性。

结论

通过参加华为Tech Arena 2024竞赛,我获得了宝贵的经验。这次比赛不仅展示了我们的优势,也暴露了我们在展示技能和团队协作方面需要改进的地方。展望未来,我会牢记这些经验教训,并在今后的比赛中不断改进自己。如果您有问题,可以在留言区反馈给我。

今天开始了漫长的刷题流程。之前只是简单做了一些题目,保持手感,但今天开始,就要为面试正式准备了。

之前一直在思考一个问题:如何高效地刷 Leetcode。我认为,要高效地刷题,首先就得背题。书读百遍,其义自见,大语言模型经过大量训练,也锻炼出了写代码的能力。同样地,我相信 Leetcode 也一样,通过多次练习,答案自然会浮现,量变产生质变。

53. Maximum Subarray

解题思路可以使用 Kadane’s Algorithm,代码如下:

1
2
3
4
5
6
def maxSubArray(nums):
max_current = max_global = nums[0]
for num in nums[1:]:
max_current = max(num, max_current + num)
max_global = max(max_global, max_current)
return max_global

这是一个典型的动态规划题目,上面的算法其实隐藏了动态规划的本质。

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size());
dp[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
}
return *max_element(dp.begin(), dp.end());
}
};

这段代码更好地体现了动态规划的思想。

理解 dp[i] = max(nums[i] + dp[i-1], nums[i]) 的公式可以从动态规划的角度来解析。这个公式的核心在于“在每一个位置上,我们要做出一个最优选择”。以下是详细解释:

1. dp[i] 的含义是什么?

  • dp[i] 表示以位置 i 结尾的 最大子数组和

2. 为什么比较 nums[i] + dp[i-1]nums[i]

  • 核心问题是:当前的最大子数组和是否应该包括之前的部分(dp[i-1])?
    • **nums[i] + dp[i-1]**:如果 dp[i-1] 是正数,加上当前的 nums[i] 会让子数组和变大,那么我们选择累加。
    • **nums[i]**:如果 dp[i-1] 是负数,我们选择从当前位置重新开始一个新的子数组,因为负数只会拖累当前和。

3. 为什么不用比较后续的数?

  • 我们在比较时,是假设数组在位置 i 停止。也就是说,我们考虑 [0:i] 这个范围内的最大值,对于位置 i 来说,要么加上之前的子数组,要么放弃之前的子数组,只采用当前的数。
  • 然后,我们遍历整个数组,找出在每个位置停下来的最大值,最终返回最大的那个值。

57. Insert Interval

这是一个经典的区间合并问题,需要将新插入的区间合并到已有的区间中。

可以使用分解法来解决:

  • 第一步,将不重叠且在 newInterval 之前的区间添加到结果中。
  • 第二步,合并所有可能重叠的区间到 newInterval 中,注意这里的判断条件。
  • 第三步,将剩余的区间添加到结果中。

注意,合并区间的判断条件是前一个区间的开始小于等于后一个区间的结束,即 intervals[i][0] <= newInterval[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
ret_list = []
i = 0

while i < len(intervals) and intervals[i][1] < newInterval[0]:
ret_list.append(intervals[i])
i += 1

while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(intervals[i][0], newInterval[0])
newInterval[1] = max(intervals[i][1], newInterval[1])
i += 1

ret_list.append(newInterval)

while i < len(intervals):
ret_list.append(intervals[i])
i += 1

return ret_list

只要注意细节,这个题目其实并不难。

300. Longest Increasing Subsequence

显然是一个动态规划问题。

dp[i] 表示以某个数字结尾的最长递增子序列的长度。

每次加入一个元素,我们更新当前的 dp 数组,如果当前数字比之前的小,它们的结果就加 1。

注意,第一个 dp[i] 是从下标 1 开始的。

1
2
3
4
5
6
7
8
9
10
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)

for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)

return max(dp)

复杂度分析

  • 时间复杂度:$O(n^2)$,因为有两个嵌套的循环。
  • 空间复杂度:$O(n)$,因为需要一个长度为 ndp 数组。

674. Longest Continuous Increasing Subsequence

这是一个简单题目,但仍然值得理解。

1
2
3
4
5
6
7
class Solution:
def findLengthOfLCIS(self, nums: List[int]) -> int:
dp = [1] * len(nums)
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i] = max(dp[i], dp[i-1] + 1)
return max(dp)

392. Is Subsequence

1
2
3
4
5
6
7
8
9
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
i = 0
j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)

这个题目可以用双指针法,以 t 为基准,如果 s 中有相同字符就向前推进,如果 s 到结尾了,说明匹配完成。

115. Distinct Subsequences

这个问题要求我们找到字符串 s 中有多少不同的子序列等于字符串 t

首先,我们需要定义 dp,其中 dp[i][j] 表示从 s 的前 i 个字符中形成 t 的前 j 个字符的不同子序列数。

  • dp[i][0] 表示当 t 为空串时,结果为 1。
  • dp[0][j] 表示从空串中形成 t,结果为 0。

对于 dp[i][j],其结果取决于位置 ij 的字符:

  • 如果两者相等,结果等于不匹配 s[i-1] 的情况(dp[i-1][j])加上匹配的情况(dp[i-1][j-1])。
  • 如果不相等,则结果等于 dp[i-1][j]

注意,这里 i, j 指的是前 i 个和前 j 个字符。

此外,dp[0][0] 初始化为 1,因为空字符串是任何字符串的子序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
def numDistinct(self, s: str, t: str) -> int:
dp = [[0 for _ in range(len(t) + 1)] for _ in range(len(s) + 1)]
for i in range(len(s) + 1):
dp[i][0] = 1

for j in range(1, len(t) + 1):
for i in range(1, len(s) + 1):
if s[i-1] == t[j-1]:
dp[i][j] = dp[i-1][j] + dp[i-1][j-1]
else:
dp[i][j] = dp[i-1][j]

return dp[-1][-1]

Off-by-One 溢出攻击分析

背景

上周,我参加了一门安全课程,其中包含了一个 off-by-one 溢出漏洞的示例。以下是原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 简单的 off-by-one 溢出示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void foo(char *input) {
char buf[1024];
strncpy(buf, input, sizeof(buf));
buf[sizeof(buf)] = '\0';
}

void bar(void) {
printf("I've been hacked\n");
}

int main(int argc, char **argv) {
if (argc != 2) {
printf("Usage: %s input_string\n", argv[0]);
exit(EXIT_FAILURE);
}
foo(argv[1]);
return 0;
}

利用此漏洞的攻击命令是:

1
perl -e 'system "./obo", "\x38\x84\x04\x08"x256'

运行此命令的结果是屏幕上打印出多行 I've been hacked

分析

当程序进入 foo 函数时,内存布局如下(使用 GDB 观察到):

从上到下,布局包含返回地址、保存的帧指针(saved frame pointer)和缓冲区 (buf)。

当执行 buf[sizeof(buf)] = '\0'; 时,保存的帧指针 (ebp) 的最低有效字节被设置为 0。为了确保 ebp 在被部分覆盖后仍然指向 buf 区域内,需要至少 1024 字节的缓冲区。具体来说,需要覆盖 ebp,使其保持在合理范围内(最高到 0xff),这就是为什么缓冲区设置为 0xff * 4 字节的原因。

理解 foo 返回时的汇编命令

foo 函数返回时,通常会执行以下关键的汇编指令:

1. leave 指令

leave 指令等价于:

1
2
mov esp, ebp
pop ebp
  • mov esp, ebp:将栈指针 (esp) 设置为帧指针 (ebp) 的值,恢复栈指针到当前栈帧的顶部,从而释放当前函数占用的空间。
  • pop ebp:将栈顶的值弹出并赋给帧指针 (ebp),从而恢复调用者的帧指针。实际上,相当于将返回地址写入 ebp,这意味着将栈中的值(通常是调用者的帧地址)赋给 ebp,从而恢复调用者的栈帧。

leave 的作用是将 esp 恢复到函数调用前的位置,并弹出保存的 ebp。如果 ebp 被覆盖为指向某个特殊地址(例如缓冲区内的地址),在函数返回时可能导致错误的栈指针位置。

2. ret 指令

ret 指令从栈顶弹出一个地址并跳转到该地址:

1
pop eip

如果返回地址被覆盖为 bar 函数的地址,执行流将跳转到 bar,允许攻击者执行任意代码。本质上,ret 会将一个地址弹出到指令指针 (eip) 并跳转到该地址继续执行。

攻击步骤

  • 当执行命令 perl -e 'system "./obo", "\x38\x84\x04\x08"x256' 时,程序会将这些重复的字节作为输入传递给 ./obo
  • foo 函数返回时,执行了 leaveret 指令,由于返回地址被覆盖,程序跳转到 bar 函数,打印出成功的消息多次。

进一步分析:确定有效的覆盖位置

为了成功执行攻击,关键在于精确确定需要覆盖哪些字节,以有效地操控控制流。在这个例子中,溢出发生在执行 buf[sizeof(buf)] = '\0' 时,导致保存的帧指针 (ebp) 的最低有效字节被设置为 0。因此,需要调整 ebp 的值,确保其指向缓冲区区域,从而使执行流程按预期进行,最终跳转到 bar 函数。

基于进一步的分析和测试,得出以下见解:

  • 要准确确定覆盖位置,ebp 的值至关重要。然而,获得这个值是有挑战的,因为:

    1. GDB 调试会影响地址布局。
    2. 输入参数的长度会影响地址布局。
  • 在 GDB 调试下,foo 函数内的布局如下:

  • 执行 buf[sizeof(buf)] = '\0'; 后,ebp 被修改,使得返回地址实际上取的是 ebp + 1 处的值,即地址 0xbfffed00 + 1,也就是 0xbfffed04

  • 对应的偏移位置在 buf 的第 255 个位置,这意味着可以通过只在该特定位置填入返回地址来构造攻击。在 GDB 中使用以下命令进行验证:

    1
    r $(perl -e 'print  "\x01\x01\x01\x01"x254 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x1')
  • 经过验证,这在 GDB 调试下是有效的,但需要注意以下细节:

    1. 输入参数的长度必须始终为 256 字节,否则 ebp 的值会发生变化,因为输入参数会占用栈空间,从而影响帧的起始位置,进而影响 ebp 的值。
    2. 填充必须使用非零值,如 0x01,因为如果遇到 0 值,strncpy 会提前终止。
  • 直接执行程序时(即不使用 GDB),内存布局不同,导致偏移位置也不同。通过实验,发现偏移位置在第 235 个位置。对应的攻击命令是:

    1
    ./obo $(perl -e 'print  "\x01\x01\x01\x01"x234 . "\x38\x84\x04\x08"x1 . "\x01\x01\x01\x01"x21')

这样就实现了找到准确覆盖位置并成功执行攻击的效果。

ChatGPT生成的公式使用了以下格式:

1
2
3
\[
公式内容
\]

而Obsidian中的公式渲染使用的是以下格式:

1
2
3
$$
公式内容
$$

当我们将ChatGPT的公式复制到Obsidian中时,这种差异会导致无法正确渲染。

解决方案

我们可以创建一个Obsidian脚本,在粘贴操作时自动替换公式的格式。

1. 创建脚本

可以使用Obsidian中的插件来解决这个问题。

在你的库中,在template目录下创建一个文件fixlatex.js,并输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = async (params) => {
const { quickAddApi } = params;

// 获取剪贴板内容
const clipboardContent = await quickAddApi.utility.getClipboard();

// 检查是否成功获取内容
if (!clipboardContent) {
new Notice("剪贴板为空或无法访问。");
return;
}

const modifiedContent = clipboardContent
.replace(/\\\[|\\\]/g, '$$$$') // 转换 \[ \] 为 $$ $$
.replace(/\\\(\s*|\s*\\\)/g, '$$'); // 转换 \( \) 为 $

// 将修改后的内容写回剪贴板
await navigator.clipboard.writeText(modifiedContent);

new Notice("剪贴板内容已处理并修改!");
};

2. 在QuickAdd中设置脚本

安装QuickAdd插件,并创建一个Macro,按如下图中的配置进行设置并保存。Macro的第一步是执行我们刚刚创建的用户脚本fixlatex.js,第二步是等待100毫秒,第三步是执行粘贴操作。

3. 在Commander中设置侧边快捷键

安装Commander插件,并将刚刚创建的QuickAdd操作设置为侧边栏的快捷键。你也可以跳过这一步,直接使用Obsidian命令执行这个操作。

4. 验证效果

现在,在ChatGPT的网页中(目前在APP中点击复制按钮似乎有点问题),点击复制按钮后,在Obsidian中点击侧边栏快捷键,或者手动执行QuickAdd命令,就可以将ChatGPT中的内容复制到Obsidian中,并自动转换Latex格式。

关于我

本文书写于 2024年10月11日,都柏林,爱尔兰。


自我介绍

大家好,我叫李楠森,曾用名李楠。我来自中国,是一名热爱科技的工程师。我拥有机械电子的本科学位和两个计算机科学的硕士学位。我曾在中国一家大型互联网公司担任过 Go 语言工程师,目前正在都柏林一所大学攻读硕士学位。

我本科学习机械电子工程,并且此期间参与了组织的中国大学生电子设计竞赛和在中国举办的智能汽车比赛。我在此期间逐渐对编程更感兴趣,为此,我开始攻读一个计算机工程的硕士学位,以追寻我真正热爱的东西。在硕士期间,我继续参与了和编程相关的比赛,并且在一家大型互联网公司实习。我有幸取得了该公司的正式职位,作为软件工程师,工作了三年。

在工作期间,我有幸为上亿用户开发软件并直接服务客户。每当我发布服务,我都很小心翼翼,我知道我所发布的不仅仅是冰冷的代码,还影响着成千上万用户的体验。

为了帮助家族企业从疫情中恢复,我离开了公司,去帮助我的家人。同时,我开始巩固我的外语技能,申请一段海外的学习机会。现在,我来到爱尔兰,开始继续深入学习计算机科学相关的知识。

我对前端和后端开发、系统架构、算法、游戏开发,以及生成式 AI(例如 Stable Diffusion 和 ChatGPT)有着浓厚的兴趣。我一直致力于提高自己的编程能力,也喜欢尝试新技术,保持对行业发展的敏感度。

如果你对技术或者生活方式有相同的兴趣,欢迎联系我,我们可以一起讨论和交流。

我是李楠森,这里是我的第一篇笔记,我不知道我能记录多少内容,也不知道能记录多久,但是我会尽力坚持去记录。

于 2024年10月11日凌晨 1点20分 爱尔兰 都柏林

0%