最近ツイートしたこの子です。
なんのコマンドで読んだのかお見通しなファイルちゃんできたよー pic.twitter.com/kjiMfPPqVz
— えびちゃん🍑🍝🦃 (@rsk0315_h4x) 2025年2月19日
clairvoyance は、通常は知覚するのが不可能と考えられている情報を得る能力を指します。千里眼や透視などと訳されることもあります。 読まれるファイル側が読む側のコマンドを知覚するのは通常はあり得ないと考えられていそうなので、そういう名前をつけました。
身近な例
読むたびに内容が変わるファイルの例として、/dev/random というのがあります。ランダムなバイト列を返してくれます。
% xxd -l64 /dev/random #> 00000000: 28c7 49f4 3e9c d693 753c 7b15 b146 5d47 (.I.>...u<{..F]G #> 00000010: c0a0 2ba1 f4b1 aa32 8793 13da 8353 dd7b ..+....2.....S.{ #> 00000020: b56e 03dc 6d85 6d78 8c93 f86f c3ad 21e6 .n..m.mx...o..!. #> 00000030: 2743 a6f4 b8e6 c88d f42e 1680 9d7b ffda 'C...........{.. % xxd -l64 /dev/random #> 00000000: e596 4416 c4ac 79c2 a48b f61f fffc 7938 ..D...y.......y8 #> 00000010: e74f 51a6 0a34 6d36 d143 0a59 c9e7 e1cd .OQ..4m6.C.Y.... #> 00000020: b524 ffc6 ede4 1e1c e07d 349f eb39 70a6 .$.......}4..9p. #> 00000030: c9d5 56ae 71b7 3a05 67e2 e2ab d785 fa69 ..V.q.:.g......i
これは実際には、(当然ながら)通常のファイルとは異なる機構で実装されています。 character special file とか character device と呼ばれるものです。
character device を実装する際には、struct file_operations の持つ read などのメンバ変数(関数ポインタ)として所望の処理を書きます。read は、ファイルに対して read の操作をしたときに呼ばれるものです。
devlist[8]in drivers/char/mem.crandom_fopsin drivers/char/random.cstruct file_operationsin include/linux/fs.h
第一歩
ファイル作成
ということで、自作したくなるのが世の常というものです。
まずは複雑な処理をせず、該当の機構を使いつつも固定の文字列を返すようなものを実装してみましょう。
hello.c
#include <linux/device.h> #include <asm/errno.h> static int device_open(struct inode*, struct file*); static int device_release(struct inode*, struct file*); static ssize_t device_read(struct file*, char __user*, size_t, loff_t*); static ssize_t device_write(struct file*, char const __user*, size_t, loff_t*); #define DEVICE_NAME "hello" static int major; static int minor = 0; static char* devnode(const struct device* dev, umode_t* mode) { if (mode) { *mode = 0666; } return NULL; } static struct class cls = { .name = DEVICE_NAME, .devnode = devnode, }; static struct file_operations fops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release, }; static int __init kinit(void) { major = register_chrdev(0, DEVICE_NAME, &fops); if (major < 0) { pr_alert("Registering char device failed with %d\n", major); return major; } int e; if ((e = class_register(&cls))) { pr_warn("class_register() returns %d\n", e); return e; } struct device* p = device_create(&cls, NULL, MKDEV(major, minor), NULL, DEVICE_NAME); pr_info("Device %s is created (%p)\n", DEVICE_NAME, p); return IS_ERR(p) ? PTR_ERR(p) : 0; } static void __exit kexit(void) { device_destroy(&cls, MKDEV(major, minor)); class_unregister(&cls); unregister_chrdev(major, DEVICE_NAME); } static int device_open(struct inode* inode, struct file* file) { try_module_get(THIS_MODULE); return 0; } static int device_release(struct inode* inode, struct file* file) { module_put(THIS_MODULE); return 0; } static char message[] = "Hello.\n"; static size_t message_length = 7; static ssize_t device_read(struct file* file, char __user* buf, size_t count, loff_t* ppos) { if ((size_t)*ppos == message_length) { return 0; } if (count > message_length - (size_t)*ppos) { count = message_length - (size_t)*ppos; } unsigned long e = copy_to_user(buf, &message[*ppos], count); if (e) { pr_warn("copy_to_user() returns %lu\n", e); return -EFAULT; } else { *ppos += count; return count; } } static ssize_t device_write(struct file*, char const __user*, size_t, loff_t*) { return -EBADF; } module_init(kinit); module_exit(kexit); MODULE_LICENSE("GPL");
Makefile
KDIR = /lib/modules/`uname -r`/build
kbuild:
make -C $(KDIR) M=`pwd`
clean:
make -C $(KDIR) M=`pwd` clean
obj-m = hello.o
ビルド
ビルドのために必要なヘッダファイルを用意します。
% uname -r
#> 6.8.0-47-generic
これで出てきたものと同じバージョンのヘッダを用意する必要があります。
- https://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-headers-6.8.0-47_6.8.0-47.47_all.deb
- https://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-headers-6.8.0-47-generic_6.8.0-47.47_amd64.deb
% curl -O https://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-headers-6.8.0-47_6.8.0-47.47_all.deb % curl -O https://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-headers-6.8.0-47-generic_6.8.0-47.47_amd64.deb % dpkg -i linux-headers-6.8.0-47_6.8.0-47.47_all.deb % dpkg -i linux-headers-6.8.0-47-generic_6.8.0-47.47_amd64.deb
あとは make すればいいでしょう。
% make -C /lib/modules/`uname -r`/build M=`pwd` #> make[1]: Entering directory '/usr/src/linux-headers-6.8.0-47-generic' #> warning: the compiler differs from the one used to build the kernel #> The kernel was built by: x86_64-linux-gnu-gcc-13 (Ubuntu 13.2.0-23ubuntu4) 13.2.0 #> You are using: gcc-13 (Ubuntu 13.3.0-12ubuntu1) 13.3.0 #> make[1]: Leaving directory '/usr/src/linux-headers-6.8.0-47-generic'
カーネルをビルドするのに使われた gcc とバージョンが違うと警告が出ます。マイナーバージョンが違うくらいであれば問題ないんじゃないかなと思っています。
/usr/src/linux-headers-6.8.0-47-generic/.config に CONFIG_CC_VERSION_TEXT というのが記載されていました。
さて、次のようなファイルができているはずです。
. ├── Makefile ├── hello.c ├── hello.ko ├── hello.mod ├── hello.mod.c ├── hello.mod.o └── hello.o
登録
デバイスの登録をするためには、そういう権限が必要です。権限がないと次のようになります。cf. capabilities(7)。
% insmod hello.ko
#> insmod: ERROR: could not insert module hello.ko: Operation not permitted
Docker で作業する際には --cap-add=CAP_SYS_MODULE というオプションをつけるとうまくいくと思います。
% insmod hello.ko % cat /proc/devices | grep hello 238 hello
ここの 238 というのが、デバイスの番号(のうちの major と呼ばれる方)になります。 さて、/dev/hello という名前のファイルを作り、いま作ったデバイスに紐づけます。
% mknod -m 666 /dev/hello c 238 0
読んでみましょう。
% cat /dev/hello
#> cat: /dev/hello: Operation not permitted
読めませんでした。デバイスを読むにあたってもそういう権限が必要みたいです。
Docker で作業する際には、--device-cgroup-rule='c 238:0 rwm' というオプションをつけるとよい気がします。
% cat /dev/hello
#> Hello.
読めました。
自作
調査
さて、そういう処理を入れていきます。
なんとかして、open された際に open してきたプログラムを特定したいです。
/proc/self 配下のファイル*1を見ると下記のようになります。
% cat -A /proc/self/cmdline #> cat^@-A^@/proc/self/cmdline^@ % cat -A /proc/self/comm #> cat$
なので、カーネル側がなにかをしている部分でコマンドライン引数が取得できそうということは推測できます。
current というポインタ(実際には変数ではなくマクロで、get_current() という関数が呼ばれている*2)が指す構造体の中に、それっぽいものがあります。
struct task_structin include/linux/sched.hstack_canaryなど、気になるメンバ変数がちらほらありますね。
currentin arch/x86/include/asm/current.h
current->comm が /proc/self/comm で使われるものなのですが、char comm[TASK_COMM_LEN]; と定義されていて、TASK_COMM_LEN は 16 です。長いファイル名(ファイル名の上限は 255 文字)の場合は途中で打ち切られてしまいます。
なので、/proc/self/cmdline のための処理をしている箇所を探しに行きます。
get_mm_cmdlinein fs/proc/base.cstruct mm_structin include/linux/mm_types.h
current->mm->arg_start の位置に、該当の文字列がありそうです。pr_info などを使うことで、dmesg にログを残せるのでそうしましょう。
unsigned long arg_start = current->mm->arg_start; pr_info("current->mm->arg_start: 0x%016lX\n", arg_start); pr_info("current->mm->arg_start: %p\n", (void*)arg_start); pr_info("current->mm->arg_start: %s\n", (char*)arg_start);
% /usr/bin/cat /dev/clairvoyance /proc/self/maps #: #> 7ffc7f887000-7ffc7f8a8000 rw-p 00000000 00:00 0 [stack] #: % dmesg | tail #: #> [...] current->mm->arg_start: 0x00007FFC7F8A7EFD #> [...] current->mm->arg_start: 000000008e4d4edd #> [...] current->mm->arg_start: /usr/bin/cat
これにより、次のことがわかります。
current->mm->arg_startはスタック領域を指しているcurrent->mm->arg_startには、いわゆるargv[0]が入っているcatになったりはせず、指定した/usr/bin/catのまま- ここには書いていないが、これの続きに
argv[1], ... もある
%pでポインタを出力すると難読化されている
難読化に関しては、以下に記述があります。SipHash のハッシュ値を生成しているみたいです。
pointerin lib/vsprintf.cptr_keyin lib/vsprintf.csiphash_key_tin include/linux/siphash.hsiphash_1u64in lib/siphash.c
ハッシュ化を無効化すると下記のログが出るの、wow という感じでした。
********************************************************** ** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE ** ** ** ** This system shows unhashed kernel memory addresses ** ** via the console, logs, and other interfaces. This ** ** might reduce the security of your system. ** ** ** ** If you see this message and you are not debugging ** ** the kernel, report this immediately to your system ** ** administrator! ** ** ** ** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE ** **********************************************************
ハッシュ化の過程で ptr_key という値を使っており、これはランダムに初期化されたグローバルな値です。
カーネルのシンボルたちのアドレスを取得する方法があるようなので、それを使いつつ値を見ちゃいます。
% cat /proc/kallsyms | grep -w ptr_key #> ffffffff9ea567b0 d ptr_key
unsigned long* p = (void*)0xffffffff9ea567b0; pr_info("ptr_key = {0x%016lX, 0x%016lX}", p[0], p[1]);
[...] ptr_key = {0xB1EF2D2DECDE711F, 0xF06A52701443DF17}
こうして手に入れた ptr_key を使って siphash_1u64 の処理をすると、上記と同じハッシュ値が得られることが確かめられます。実際には、上記のハッシュ値は下位 32 bits のみ取り出されていることに注意しましょう。
さて、clairvoyance するにあたって必要な情報はもう得られたので、実装していきます。
ファイル
clairvoyance.c
#include <linux/device.h> #include <asm/errno.h> static int device_open(struct inode*, struct file*); static int device_release(struct inode*, struct file*); static ssize_t device_read(struct file*, char __user*, size_t, loff_t*); static ssize_t device_write(struct file*, char const __user*, size_t, loff_t*); #define DEVICE_NAME "clairvoyance" static int major; static int minor = 0; enum { CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN, }; static atomic_t cdev_status = ATOMIC_INIT(CDEV_NOT_USED); static char message[266] = "Hello, *{255}.\n"; static size_t message_length = 0; static char* devnode(const struct device* dev, umode_t* mode) { if (mode) { *mode = 0666; } return NULL; } static struct class cls = { .name = DEVICE_NAME, .devnode = devnode, }; static struct file_operations fops = { .read = device_read, .write = device_write, .open = device_open, .release = device_release, }; static int __init kinit(void) { major = register_chrdev(0, DEVICE_NAME, &fops); if (major < 0) { pr_alert("Registering char device failed with %d\n", major); return major; } int e; if ((e = class_register(&cls))) { pr_warn("class_register() returns %d\n", e); return e; } struct device* p = device_create(&cls, NULL, MKDEV(major, minor), NULL, DEVICE_NAME); pr_info("Device %s is created (%p)\n", DEVICE_NAME, p); return IS_ERR(p) ? PTR_ERR(p) : 0; } static void __exit kexit(void) { device_destroy(&cls, MKDEV(major, minor)); class_unregister(&cls); unregister_chrdev(major, DEVICE_NAME); } static int device_open(struct inode* inode, struct file* file) { if (atomic_cmpxchg(&cdev_status, CDEV_NOT_USED, CDEV_EXCLUSIVE_OPEN)) { return -EBUSY; } char* filename = (char*)current->mm->arg_start; for (char* p = filename; *p;) { char c = *p++; if (c == '/') { filename = p; } } snprintf(message, sizeof message, "Hello, %.255s.\n", filename); message_length = strlen(message); try_module_get(THIS_MODULE); return 0; } static int device_release(struct inode* inode, struct file* file) { atomic_set(&cdev_status, CDEV_NOT_USED); module_put(THIS_MODULE); memset(message, 0, sizeof message); return 0; } static ssize_t device_read(struct file* file, char __user* buf, size_t count, loff_t* ppos) { if ((size_t)*ppos == message_length) { return 0; } if (count > message_length - (size_t)*ppos) { count = message_length - (size_t)*ppos; } unsigned long e = copy_to_user(buf, &message[*ppos], count); if (e) { pr_warn("copy_to_user() returns %lu\n", e); return -EFAULT; } else { *ppos += count; return count; } } static ssize_t device_write(struct file*, char const __user*, size_t, loff_t*) { return -EBADF; } module_init(kinit); module_exit(kexit); MODULE_LICENSE("GPL");
copy_to_user の処理や atomic_t に関しては割愛します。
ビルドなど
Makefile については、obj-m だけ変えておきます。
同じ流れで make して、insmod や mknod などをします。
書き換えてビルドし直した場合は rmmod clairvoyance をしてから insmod clairvoyance.ko をする必要があります。
rm /dev/clairvoyance をしたり mknod をし直したりする必要はないみたいです。
なにかしらのミスで変なことになった場合は、dmesg を叩くとログが見れます。権限としては --cap-add=CAP_SYSLOG が必要になると思います。
その他
読むたびに成長していくファイルちゃんできた pic.twitter.com/ltrmLw5Nyf
— えびちゃん🍑🍝🦃 (@rsk0315_h4x) 2025年2月19日
/dev/grow も同じような機構で実装しています。clairvoyance より簡単なのに grow の方がふぁぼが多くて、世の中そういうもんだよな〜という気持ちになりました。
ギャラリー









参考文献
- man-pages
- The Linux Kernel Module Programming Guide
- Linux Kernel Teaching > Kernel modules
- Docker Docs > docker container run
あとがき
興味深さ優先探索型人間なので、気になったことを調べてこういうことになりがちです。どういう経緯でこれを始めたか覚えていません。思い出します。
malloc について調べていて、torvalds/linux に迷い込んで SYSCALL_DEFINE1(brk, unsigned long, brk) とかを調べて、なんやかんやで execve(2) を調べ始めて、ELF について調べて、libc や ld-linux-x86-64.so.2 に依存しないようなバイナリが欲しくなったりして、アセンブリを書き始めて、strace たのしいな〜となり、Intel や AMD64 の機械語のマニュアルを読んだりして、そういえば AVX-512 のエミュレート環境があったらうれしいなとなって colima 上で Intel SDE を動かしてみて、そういえば(少しバックトラックして)そういう本の存在を思い出して ゼロからのOS自作入門 を買ったりしました。
また、フォロヮの影響で [試して理解]Linuxのしくみ を買ったりしました。
上記の過程やそれ以前に目にした概念で、まだよくわかっていないものが諸々あって、signal とか handler とか、vDSO (virtual dynamic shared object) とか vsystable とか、vtable とか、TLB (translation lookahead buffer) とか MMU (memory management unit) とか、どのレイヤーでどういうことが起きているんですか?というので、調べたり調べようとしたりしています。
(お気楽大学生みたい)
たぶんそういう過程で「結局 /dev/random ってどうなってるの?」みたいになって、“character device” “how to create /dev/null” などでググりつつ、下記を見ました。
- https://unix.stackexchange.com/questions/230540
- https://unix.stackexchange.com/questions/27279
- https://stackoverflow.com/questions/44655105
- https://unix.stackexchange.com/questions/37829
“kernel object” というそれっぽいキーワードを見つけたので、“kernel object 101” とかでググって解説記事を探したりしました。 一度それっぽいキーワードを見つけると、その後の調査が捗りますね。こういう第一歩のパートには LLM を使うのもありかもしれません*3。
で、せっかくならなにか面白そうな挙動をするものが作れたらうれしいよねとなり、grow や clairoyance を書きました。 clairvoyance 自体は(今回のトピックとしては主題っぽさはありつつ)別に副産物でしかなくて、カーネルのコードを読むことに抵抗が減りつつある方が大きいのかなという気もしています。
「どう調べたら知りたいことが知れるのかな?」「得た知識の妥当っぽさをどうしたら確かめられるかな?」みたいなことを考えたり実行したりするのが好きなような気がします。
そういえば、SipHash に関して調べていて Serious Cryptography を買ったりもしました。 crypto もあんまりわかっていない領域なので、詳しくなりたいですよね。 (以前やった)浮動小数点型とかに関してもそうですが、人々があまり知りたがろうとしていなさそうな部分かつまともな教材があまりなさそうな分野に惹かれます。 変な怪しい学問にハマったりしないか心配ですね*4。
競プロも衰えない程度には続けていようと思っているものの、ABC で頭の悪いムーブをしたりすると萎えますね。 「えびちゃんってさすがにもっと頭よくなかった? よくあってくれ」のような気持ちになりがちですね。嫌すぎます。
残念ながら使命感駆動とか金銭駆動で勉強できるタイプではないので、これからも興味駆動で、のほほんとやっていけたらうれしいな〜という気持ちです。
おわり
おわりです

