Go to file
2025-02-25 16:25:01 +01:00
src renamed "function" 2025-02-25 16:11:17 +01:00
.envrc init 2025-02-20 19:31:32 +01:00
.gitignore init 2025-02-20 19:31:32 +01:00
flake.lock init 2025-02-20 19:31:32 +01:00
flake.nix init 2025-02-20 19:31:32 +01:00
README.md NOPFunction rename, added ualarm link, added rule of thumb for interval 2025-02-25 16:25:01 +01:00

Flush+Reload Covert Channel

Primitive

in ./src/lib.c sind folgende Primitive flush, reload und time_maccess:

maccess greift auf diese Speicheradresse addr zu.

void maccess(void* addr)
{
    asm volatile ("movq (%0), %%rax\n"
    :
    : "c" (addr)
    : "rax");
}

flush verdrängt die Speicheradresse addr aus dem Cache.

void flush(void* addr)
{
    asm volatile ("clflush 0(%0)\n"
    :
    : "c" (addr)
    : "rax");
}

time_maccess misst wie lange der Zugriff auf die Speicheradresse addr dauert.

Zum Messen der Dauer des Speicherzugriffs lesen wir vor und nach dem Zugriff die Zeit aus. Um unsere Reihenfolge zu bewahren reicht das Ausschalten von Compiler Optimierungen ggfs. nicht aus. Um zu verhindern, dass die CPU die Instruktionen parallel oder in anderer Reihenfolge ausführt können sog. Fences verwendet werden:

size_t time_maccess(void (* addr)(void))
{
    uint64_t start, end, delta;
    uint64_t lo, hi;
    asm volatile ("LFENCE");
    asm volatile ("RDTSC": "=a" (lo), "=d" (hi));
    start = (hi<<32) | lo;
    asm volatile ("LFENCE");

    asm volatile ("movq (%0), %%rax\n"
    :
    : "c" (addr)
    : "rax");

    asm volatile ("LFENCE");
    asm volatile ("RDTSC": "=a" (lo), "=d" (hi));
    end = (hi<<32) | lo;
    asm volatile ("LFENCE");
    delta = end - start;
    return delta;
}

Block 1: Messen der Timing-Differenzen + Threshold bestimmen

Zunächst brauchen wir einen Threshold, also einen Grenzwert, ab wann ein Zugriff als "Hit" oder "Miss" interpretiert werden soll. Dafür müssen wir Zugriffszeiten von Hits und Misses messen.

Tips
  • am besten kompiliert man ohne Compileroptimierungen (-O0):
      gcc -O0 cache_test.c lib.c -o cache_test
    
  • CMake ist praktisch! siehe ./src/CMakeLists.txt. Verwenden durch cmake . und dann make.

Muster: ./src/cache_test.c

Block 2: Signale über den Cache

Jetzt können wir den Cache doch mal als Medium verwenden. Der Sender kann 0 und 1 über den (ausbleibenden) Speicherzugriff kodieren. Der Empfänger interpretiert dann, je nach Zugriffszeit.

Um zu kommunizieren, müssen sich Sender und Empfänger auf eine physische Adresse einigen, die für Flush+Reload verwendet wird. Dafür nehmen wir am besten eine shared library, die ist schon als sharedlib.c bereitgestellt:

void myStupidNOPFunction(void)
{    asm volatile (
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        ...

Die Adresse von myStupidNOPFunction kann also zum Flushen und Reloaded verwendet werden. Da immer eine gesamte Cacheline geladen wird, sollte sich nichts anderes in dem selben 64Byte Block befinden. Die Funktionen padding_before und padding_after sind beide 64 Byte groß, also eine Cacheline. Damit kann dann auch gar nichts mehr schief gehen :)

MyStupidNOPFunction ist übrigens genau 256*64\ Byte groß. Das wird vielleicht ja noch praktisch ;)

sharedlib.c kann man so kompilieren und linken:

# sharedlib kompilieren
gcc -fPIC -shared -O0 -o libsharedlib.so sharedlib.c
# sharedlib bei Sender und Empfänger linken
gcc -O0 sender.c lib.c -L. -lsharedlib -o sender
gcc -O0 empfaenger.c lib.c -L. -lsharedlib -o empfaenger
# ggfs. muss man noch den PATH exportieren
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

(oder einfach cmake verwenden :P)

dann kann man function benutzen:

# in e.g. sender.c
maccess((void *) myStupidNOPFunction)
...
Tips
  • taskset zum kann pinnen der Programme auf einen bestimmten CPU-Kern benutzt werden. Das kann helfen.
  • Verwendet man blockierenden Sleep um zu Takten, z.B. clock_nanosleep, dann sollte man unbedingt darauf achten nicht eine Periodendauer zu schlafen, sondern nur bis zum nächsten Zeitabschnitt. Einfacher geht es mit nicht-blockierendem Sleep, z.B. ualarm.

So kann man ualarm Linux Manual nutzen, um alle x µs einen SIGARLM auszulösen:

int main(void){
	...
	signal(SIGALRM, signal_handler);
	ualarm(0, interval);

Bei jedem SIGALRM Signal wird signal_handler ausgeführt:

void signal_handler(int signo) {
    if (signo == SIGALRM) {
	    # CODE HERE
	}
}
  • Der Zeitabstand von Flush zu Reload sollte MAXIMIERT werden! Dies ist das Zeitfenster, in welchem der Sender die Adresse(n) zurück in den Cache laden kann.
  • Der Sender kann nicht nur einmal Laden. Das kann man auch mehrfach in einem Zeitfenster machen, um sicher zu gehen, dass die Cacheline auch geladen ist, wenn der Empfänger ausließt.
  • Richtwert: 1000µs pro Zyklus haben sich als ganz gut erwiesen (→ 1kHz). Ein zu kurzes Zeitfenster lässt nicht genug Zeit um alle Instruktionen abzuarbeiten, ein zu langes führt zu mehr Noise, falls die Cacheline zwischenzeitlich aus dem Cache verdrängt wird.

Block 3: Einen String über den Cache als Medium senden

  • Kann man irgendwie mehr als ein Bit gleichzeitig senden? ;)

Muster: ./src/sender.c und ./src/receiver.c