Easy kernel is still kernel right?

TCPサーバの接続情報と、ファイル easy_kernel.tar.gz が与えられた。
easy_kernel.tar.gz を展開すると、以下のファイルなどが得られた。

Information to connect to a TCP server and a file easy_kernel.tar.gz were given.
These files and some other files are extracted from easy_kernel.tar.gz:

逆コンパイルしての解析 Decompiling and analysing

vuln.koGhidraで逆コンパイルすると、以下の関数などがあった。

init_func
第1引数を "pwn_device" として、proc_create関数を呼び出す。
sread
スタック上のデータを引数で指定された場所にコピーする。コピーする長さに制限は無い。
swrite
引数で指定された場所のデータをスタックにコピーする。コピーする長さは変数で制限されている。
sioctl
第2引数が 0x20 のとき、swriteでコピーする長さの上限を第3引数の値に設定する。

sioctl関数で設定する値に制限は無いため、swriteでコピーする長さの制限も実質無いといえそうである。
したがって、sread関数のリターンアドレス以降のデータの取得や、swrite関数のリターンアドレス以降のデータの設定ができそうである。

また、sread関数およびswrite関数においては、スタック上のコピーを開始する場所から 0x80 バイト先にcanaryがあり、0x90 バイト先にリターンアドレスがあることが読み取れた。

Decompiling vuln.ko via Ghidra, I found some functions including these ones:

init_func
Call the function proc_create with the first argument "pwn_device".
sread
Copy data on the stack to the address specified by the argument. There are no limits on the length to copy.
swrite
Copy data on the address specified by the argument to the stack. The length to copy is limited by a variable.
sioctl
Set the maximum length to copy in the function swrite to the value of 3rd argument if the 2nd argument is 0x20.

There are no limits on the value to set in the function sioctl, so there virtually looks no limit on the length to copy in the function swrite.
Therefore, reading data from the return address of the function sread and writing data from the return address of the function swrite looks possible.

Also, I found that there are the canary at the 0x80 bytes ahead from the point on the stack where copying starts and the return address at the 0x90 bytes ahead in the functions sread and swrite.

関数の実行 Executing the functions

脆弱性を持っていそうな関数が見つかったので、次はそれらを実行する方法を探す。

Tera Termで指定のサーバに接続すると、以下のような出力がされた。

Now I found functions that look vulnable, so nextly I searched for a way to execute these functions.

Connecting to the specified server using Tera Term, the server gave me a message like this:

Send the output of: hashcash -mb26 mKSAjemlgRq/1lig

指定のコマンドでHashcashを実行し、得られた出力のうち hashcash stamp: を除く部分を貼り付け、 Enterキーを押してLFを送信すると、シェルが起動した。

観察の結果、以下のことがわかった。

このファイル /proc/pwn_device を読み書きすることで、sread関数やswrite関数を実行できるようである。
cat /proc/pwn_device コマンドで読もうとすると「Bad address」と出て失敗したが、dd コマンドを用いると読むことができた。
読み込むブロック数を表す count は 1 に設定し、ブロックサイズを表す bs で読み込むサイズを指定した。
すると、以下のように480バイトまでは読み込むことができたが、これを超えると「Bad address」が出てしまった。

また、読み込んだデータはod コマンドでテキストとして出力できるが、パイプで直接 dd コマンドの出力を od コマンドに渡すと出力が混ざってしまった。
そのため、以下の例では、dd コマンドで読み込んだデータを一旦ファイルに保存し、それを od コマンドで出力している。
さらに、od コマンドの -v オプションで重複行の省略を抑止し、わかりやすくしている。

I executed Hashcash with the specified command, copy-and-pasted its output except for hashcash stamp:, and pressed the Enter key to send LF. As a result, a shell started.

After some observation, I found these things:

Reading and writing this file /proc/pwn_device looks meaning to execute the functions sread and swrite.
Reading the file via cat /proc/pwn_device failed with a mesasge "Bad address", but I succeeded to read the file using dd command.
I set the number of blocks to read count to 1, and set the size of blocks bs as the size to read.
As a result, I succeeded to read 480 bytes like shown below, but trying to read more resulted in "Bad address".

Also, od command is useful to print the data read as text, but passing the output of dd command to od command directly via pipe resulted in a mixed output.
To avoid this, I saved the output of dd command to a file and printed the file via od command in this example.
Moreover, I used -v option for od command to prevent it from omitting duplicate lines and to make it clear.

ファイル /proc/pwn_devicedd コマンドで読む Reading the file /proc/pwn_device via the dd command ~ $ dd if=/proc/pwn_device of=test bs=480 count=1 [ 33.591784] Device opened [ 33.595030] 480 bytes read from device [ 33.595759] All device's closed 1+0 records in 1+0 records out 480 bytes (480B) copied, 0.005442 seconds, 86.1KB/s ~ $ od -v -Ax -t x8 test 000000 20656d6f636c6557 2073696874206f74 000010 70206c656e72656b 6569726573206e77 000020 ffffa15600130073 00020000035a9020 000030 ffffa15600133910 0000000100020000 000040 0000000000000000 ffffa15600000000 000050 0000000000000000 0000000000000000 000060 0000000000000000 0000000000000000 000070 d736361160ee6100 00000000000001e0 000080 d736361160ee6100 00000000000001e0 000090 ffffffffa2c3e347 0000000000000001 0000a0 0000000000000000 ffffffffa2bc89f8 0000b0 ffffa15600133900 ffffa15600133900 0000c0 0000000000fd1bb0 00000000000001e0 0000d0 0000000000000000 0000000000000000 0000e0 ffffffffa2bc8d1a 0000000000000000 0000f0 d736361160ee6100 0000000000000000 000100 ffffadd9001aff58 0000000000000000 000110 0000000000000000 ffffffffa2a025d3 000120 0000000000000000 0000000000000000 000130 ffffffffa360007c 0000000000000000 000140 0000000000000000 0000000000fd08a0 000150 00000000000001e0 0000000000fd1bb0 000160 0000000000000000 0000000000000246 000170 00000000000001b6 00007ffc6179fb00 000180 00007ffc617fa090 ffffffffffffffda 000190 00000000004b99a2 00000000000001e0 0001a0 0000000000fd1bb0 0000000000000000 0001b0 0000000000000000 00000000004b99a2 0001c0 0000000000000033 0000000000000246 0001d0 00007ffc6179fac8 000000000000002b 0001e0

得られたデータの最初の部分は、Ghidraでsread関数内で設定していることが読み取れた値と一致している。
さらに、解析結果通り、0x80 バイト目からはcanaryと思われる値が、0x90 バイト目からはリターンアドレスと思われる値が得られていることがわかる。

なお、このリターンアドレスは、サーバに接続し直すと変わることがわかった。
上位の ffffffff と下位3桁の 347 は変わらず、その間の5桁が変わるようだった。

The first part in the data read matches with the values I found set in the sread function using Ghidra.
Also, as the analysis suggested, there is a value that looks like the canary from the 0x80-th byte and one that looks like the return address from the 0x90-th byte.

I also found that the return address varies between each sessions.
The upper digits ffffffff and the last 3 digits 347 looked fixed, and the 5 digits between them changed.

目的の確認 Being aware of the goal

問題文に、このページへのリンクが提示されていた。

The challenge description had a link to this page:

Learning Linux Kernel Exploitation - Part 1 - Midas Blog

この記事より、以下の処理を実現できればflagを得られそうであることがわかった。

  1. 引数 0 を与え、関数 prepare_kernel_cred を呼び出す。
  2. その返り値を引数として、関数 commit_creds を呼び出す。このことによりroot権限が得られる。
  3. ユーザーモードに戻り、/flag.txt を読むための処理を行う。

さらに、呼び出す関数のアドレスは /proc/kallsyms から読み取れること、
ユーザーモードに戻るには swapgs 命令を実行した後スタック上に戻り先のデータを配置して iretq 命令を実行すればいいことも、この記事から読み取れた。

また、記事では、これらの処理を実行させるため、リターンアドレスにアプリケーション側の関数のアドレスを設定する方法が紹介されている。
なるほど、このデータを細工してデバイスファイルの操作からアプリケーションで用意した関数を呼び出させるというのは、自分が昔xv6でやったことに似ている。

From this article, I found that I should do these things to obtain the flag:

  1. Call the function prepare_kernel_cred with an argument 0.
  2. Using the return value as the argument, call the function commit_creds. This will result in gaining the root privilege.
  3. Return to user-mode and do some operations to read /flag.txt.

I also found that the addresses of functions to call can be read from /proc/kallsyms,
and that we should execute swapgs instruction and then execute iretq instruction with data about where to return on the stack to return to user-mode.

Moreover, the article introduces a way to execute these things by setting an address of a function in the application as the return address.
Tweaking data to make operations on device files call functions in the application looks like what I did on xv6 before.

Fix vulnerabilities in exec() function by mikecat · Pull Request #8 · mit-pdos/xv6-public · GitHub

呼び出す関数のアドレスを得る Determining the addresses of functions to call

記事を参考に、以下のコマンドで呼び出す関数のアドレスを得ようとした。

Reffering to the article, I tried to obtain the addresses of functions to call by this command:

cat /proc/kallsyms | grep cred

しかし、出力された各関数のアドレスは 0000000000000000 となっており、役に立たなそうだった。

「/proc/kallsyms zero」でググると、以下のページが見つかった。

However, the printed addresses of each functions were 0000000000000000 and this looked useless.

I googled "/proc/kallsyms zero" and found this page:

linux - Reading kallsyms in user-mode - Stack Overflow

この記事によれば、/proc/kallsyms でアドレスを得るにはrootでないといけないようである。

ルートディレクトリのファイル init を見ると、最後が以下のようになっていた。

According to this article, the user must be root to obtain addresses from /proc/kallsyms.

I checked the file init in the root directory, and found that the last part of the file is:

exec su -l ctf /bin/sh

これは、rootから一般ユーザに切り替えた上でシェルを実行する、という意味だろう。
そこで、配布されたファイルを書き換え、この切り替えを無効化することでrootでシェルを実行させることにした。

initramfs.cpio.gz を展開して得られたファイル initramfs.cpio をバイナリエディタで開き、文字列 su -l ctf を検索すると、0x045e22 に見つかった。
そこで、ここのsu#u に書き換えることで、切り替えを無効化した。

書き換えた initramfs.cpio を圧縮して新しい initramfs.cpio.gz を作り、これを利用してQEMUを起動した。
すると、$ だったシェルのプロンプトが # になっており、/proc/kallsyms から関数のアドレスを得ることができた。
アドレスは変わってもアドレスの差は変わらないと予想し、続けて差の計算用に /proc/pwn_device からリターンアドレスを読み出した。

This should be standing for switching from the root to a normal user and launching the shell.
Seeing this, I decided to disable this switching by modifying the given file and have it launch the shell as the root.

I extracted a file initramfs.cpio from initramfs.cpio.gz and opened it with a binary editor. Then, I searched for a string su -l ctf and found that at 0x045e22.
After that, I changed su here to #u to disable the switching.

I compressed the modified file initramfs.cpio to create a new initramfs.cpio.gz, and launched QEMU using this new file.
As a result, the prompt of the shell, which was $, became # and I succeeded to obtain the addresses of functions from /proc/kallsyms.
Guessing that the differences of addresses won't change while the addresses change, I obtained the return address from /proc/pwn_device in the same session for calculating the differences.

swriteからアプリケーションのコードに飛ばす試み An attempt to have it jump from swrite to an application code

これまでの情報をもとに、以下の手順でroot権限を持ったシェルの起動を試みるプログラム attack.asm を作成した。
なお、サーバ上にlibcが見つからず、C言語でプログラムを書いても実行できない可能性があると考えたため、アセンブリ言語で書くことにした。

  1. セグメントレジスタとフラグレジスタの値を保存する。
  2. ファイル /proc/pwn_device を開く。
  3. ioctl システムコールを用い、swrite で書き込む長さの制限を十分大きくする。
  4. sread から、canaryとリターンアドレスの値を読み込む。
  5. swrite でcanaryとroot権限を得るプログラムのアドレスを書き込む。
  6. root権限を得てユーザーモードに戻るプログラムを実行する。
  7. execve システムコールを用い、/bin/sh を実行する。

Based on these information, I created a program attack.asm to try to launch a shell with the root privilege in these steps.
I decided to write in an assembly language because I couldn't find any libc on the server and thought that C program may be unable to be executed.

  1. Save values of the segment registers and the flag register.
  2. Open the file /proc/pwn_device.
  3. Increase the write length limit for swrite enough via the system call ioctl.
  4. Read the values of the canary and the return address via sread.
  5. Write the canary and the address of the program to gain the root privilege via swrite.
  6. Execute the program to gain the root privilege and return to user-mode.
  7. Execute /bin/sh via the system call execve.

attack.asm

これをサーバで実行するため、まずNASMを用いてオブジェクトファイルに変換した。

To execute this program on the server, firstly I converted this program to an object file using NASM.

nasm -f elf64 attack.asm

するとファイル attack.o ができるので、これをCS50 IDEにアップロードし、以下のコマンドで実行可能ファイルに変換した。

This command yielded a file attack.o, so I uploaded this file to CS50 IDE and executed this command to convert this to an executable file.

ld -o attack attack.asm

これを以下のCyberChefのRecipeで圧縮し、76文字ごとに改行を入れたBase64に変換した。

Then, I compressed this and encoded to Base64 with newline characters added after each 76 characters using this Recipe on CyberChef.

Gzip, To Base64, Find / Replace - CyberChef

サーバに接続してHashcatの出力を貼り付けた後、シェルで以下のコマンドを実行した。
そして、変換したBase64データを貼り付け、Ctrl+D で入力を終了すした。

After connectiong to the server and pasting an output of Hashcat, I executed this command on the shell.
Then, I pasted the Base64-encoded data and pressed Ctrl+D to finish the input.

base64 -d | gunzip > attack

すると、ファイル attack ができた。
chmod +x attack コマンドでこれを実行可能にし、./attack コマンドで実行できた。

実行した結果は、以下のエラーとなった。

This operation created a file attack.
I executed chmod +x attack command to make this file executable, and executed the program using ./attack command.

Executing this program resulted in this error:

attack_result.txt

どうやら、今回の環境では、記事で紹介されていたアプリケーションのコードのアドレスを指定して飛ばす方法は使えないようである。

しかし、この試みによって、swrite関数の実行時にエラーを起こすとレジスタの値を出力してくれることがわかった。

It looks like the way on the article that is specifying an address of an application code and having it jump there doesn't work in the environment for this challenge.

Not reaching to the goal, this try revealed that the system prints values of the registers when an error occurs while executing the function swrite.

ROP gadget の探索とiretq命令の実行 Searching for ROP gadgets and executing the iretq instruction

アプリケーションのプログラムのアドレスを指定して実行しようとしても失敗するようなので、
「root権限を得る関数を実行してユーザーモードに戻る」処理をROP (Return-Oriented Programming) で実行することが求めらているようである。
ROPを行うためには既存のプログラムから ROP gadget と呼ばれる行いたい処理のパーツを見つけることが必要であり、これを探すために既存のプログラムのデータを取得したい。

そこで、vuln.ko 内の sread関数のプログラムを書き換えて、リターンアドレスをコピー元として使うようにした。
具体的には、Ghidra上の表示で 0010010a から命令 mov rsi, [rsp + 0x90] を書き込むことにした。
この部分のプログラムを initramfs.cpio から探すと 0x232 から見つかったので、
ここから命令の区切りを考えてNOPを加えた10バイト 48 8b b4 24 90 00 00 00 90 90 を上書きした。

書き換えた initramfs.cpio から新しい initramfs.cpio.gz を作り、これを用いてQEMUを起動した。
QEMUの標準出力をファイルにリダイレクトし、以下のコマンドを実行した。

Since specifying an address in an application program for execution looks failing,
It looks like the process to "call functions to gain the root privilege and return to user-mode" should be executed using ROP (Return-Oriented Programming).
Finding parts of what to do, which are called "ROP gadget", from existing program is required for ROP, so now I want to retrieve the data of the existing program.

To achieve this, I decided to modify the program of the function sread in vuln.ko to have it use the return address as the source of copying.
Specifically, I decided to put an instruction mov rsi, [rsp + 0x90] from 0010010a on Ghidra.
I searched for the program in this part from initramfs.cpio, and found from 0x232.
Therefore, considering the boundaries of instructions, I added some NOPs and modified 10 bytes from there to 48 8b b4 24 90 00 00 00 90 90.

I created new initramfs.cpio.gz from modified initramfs.cpio and launched QEMU using this.
I redirected the standard output of QEMU to a file, and executed this command:

dd if=/proc/pwn_device count=1 bs=1024000 | gzip -9 | base64

その結果出力されたBase64エンコードされたデータを以下のCyberChefのRecipeでデコードすることで、リターンアドレスが表す場所以降のプログラムのデータを得た。

Then, I decoded the Base64-encoded data in the result using this Recipe for CyberChef to obtain the data of the program from the return address.

From Base64, Gunzip - CyberChef

得られたプログラムのデータは、TDM-GCCのobjdumpで -b binary -m i386 -M x86-64 -D オプションを使うことで、逆アセンブルできた。

得られたプログラムのデータからバイナリエディタで pop rdi; ret を表すデータ 5f c3 を検索すると、0xac9 に見つかった。
また、iretq を表すデータ 48 cf を検索すると、0x5e1f に見つかった。
しかし、%rax (返り値) の値を %rdi (第1引数) に移すのに役立ちそうなgadgetや、swapgs を表すデータ 0f 01 f8 は見つけられなかった。

最初に /proc/pwn_device からスタックの内容を dd コマンドで読み取った結果を見直すと、
データの最後の部分はiretqで使う各レジスタの値のようになっており、それに近い 0x130 にもプログラムのアドレスのような値があることがわかった。
そこで、先ほど initramfs.cpio に書き込んだデータのうち 90 00 00 00 の部分を 30 01 00 00 に書き換え、同様にこのアドレス以降のプログラムのデータを得た。
得たデータを調べると、0xe2eswapgs命令を実行するgadgetとして使えそうな以下の部分があった。

I disassembled the obtained program using "objdump" in TDM-GCC with options -b binary -m i386 -M x86-64 -D.

I searched for 5f c3, which stands for pop rdi; ret, from the program data using a binary editor and found that at 0xac9.
I also searched for 48 cf, which stands for iretq, and found that at 0x5e19.
However, I coundn't found gadgets for copying the value of %rax (the return value) to %rdi (the first argument), nor 0f 01 f8, which stands for swapgs.

Looking at the contents of stack obtained from /proc/pwn_device via dd command in the early step again,
I found that the last part looks like values of registers for use with iretq, and that 0x130, which is near the part, also has a value that looks like a program address.
Seeing this, I changed 90 00 00 00 in the data written to initramfs.cpio in the previous step to 30 01 00 00, and obtained the program data from this address in the same way.
Investigating the obtained data, I found this part, which looks useful as a gadget to execute the swapgs instruction, from 0xe2e.

e2e: 0f 01 f8 swapgs e31: 9d popfq e32: c3 retq

そこで、これらのデータを組み合わせ、ROPによりroot権限を得るための関数を実行するプログラム attack2.asm を作成した。
ROPで直接返り値を引数にするのは難しそうだったので、ユーザーモードに戻っても %rax の値が維持されることを期待し、各関数をそれぞれ swrite から呼び出させることにした。

Finding these things, I combined them and created a program attack2.asm which is supposed to execute the functions to gain the root privilege via ROP.
Since it looked difficult to directly use the return value as an argument in ROP, I decided to call each functions from separate invocations of swrite, hoping that the value of %rax doesn't change on returning to user-mode.

attack2.asm

ここで、なぜか CS50 IDE にログインすると 403 Forbidden と出てしまい、使えなくなってしまった。
そこで、かわりに AWS のEC2インスタンス (t2.micro、Ubuntu 20.04 (ami-036d46416a34a611c)) 上でld コマンドを実行した。
初期状態では ld コマンドは使えず、sudo apt-get install binutils コマンドを実行すると使えるようになった。

このプログラムの実行結果は、以下のようになった。

At this point, CS50 IDE started to show "403 Forbidden" after logging in and it stopped working for some reason.
Seeing this, I used an EC2 instance on AWS (t2.micro、Ubuntu 20.04 (ami-036d46416a34a611c)) instead to execute the ld command.
The ld command didn't work at first. It became available after executing a command sudo apt-get install binutils.

This is the result of executing this program:

attack2_result.txt

エラーの詳細は出力されず、単に「Segmentation fault」と出力されている。
解析の結果、ユーザーモードに戻った直後として設定した位置にexitシステムコールの呼び出しを配置しても Segmentation fault になったため、ユーザーモードにうまく戻れていないと考えられる。

Simply "Segmentation fault" is printed without giving the details of the error.
Some investigation revealed that this "Segmentation fault" happens even if an invocation of exit system call is placed to be executed right after returning to user mode, so it looks like returning to user-mode is failing.

呼び出し元の関数を利用してシステムコールの呼び出し元に戻る Returning to where the system call is invoked using the caller functions

記事に従ってiretq命令を用い、自力でユーザーモードに戻る試みは、うまくいかなかった。
ところで、swrite 関数の呼び出し元には、swrite関数から戻った後ユーザーモードに戻るための本来の処理があることが期待できる。
そこで、この本来の処理を用いてユーザーモードに戻ることを試みることにした。

最初に /proc/pwn_device からスタックの内容を dd コマンドで読み取った結果を再び見直すと、
0xe0 にもプログラムのアドレスのような値があり、これはsreadの呼び出しに繋がる関数のリターンアドレスであると推測できた。
さらに、これはあくまでsreadが呼び出された時のスタックの内容だが、swriteにおいても同様の構造になると予想した。

これに基づき、ROPによりroot権限を得るための関数を実行した後、pop rdi; ret を用いてスタックをこの値の位置まで進めることでユーザーモードに戻るプログラム attack3.asm を作成した。

My attempts to use the iretq instruction to return to user-mode by myself referring the article didn't succeed.
By the way, there should be the original program to return to user-mode after returning from the function swrite in the caller of the function swrite.
Considering this, I decided to try to return to user-mode using this original program.

Looking at the contents of stack obtained from /proc/pwn_device via dd command in the early step again,
I found a value that looks like a program address at 0xe0, and I guessed that this is a return address of a function that will lead to the call of sread.
Moreover, though this is the contents of the stack when sread is called, I guessed that the structure will also be like this when swrite is called.

Based on this, I created a program attack3.asm that executes functions to gain the root privilege using ROP and then executes pop rdi; ret to advance the stack to this value to return to user-mode.

attack3.asm

このプログラムの実行結果は、以下のようになった。

This is the result of executing this program:

attack3_result.txt

再び「Segmentation fault」が出てしまったが、swriteが呼び出されたことを示すメッセージが1個から2個になっており、1回はユーザーモードに戻ることに成功していると考えられる。

"Segmentation fault" is printed again, but there are two messages that indicate that swrite is called instead of one, so it looks succeeded to return to user-mode at least once.

エラー時の出力の利用 Utilizing what is printed on errors

ここで、swrite関数の実行中にエラーが起こるとレジスタの値が出力されることを利用した調査をすることにした。

まず、以下のプログラムを実行し、swrite関数のリターンアドレスを得た。
これは、swrite関数のリターンアドレスをRSIレジスタにコピーした後、ゼロ除算でエラーを起こすプログラムなので、 swrite関数のリターンアドレスがRSIとして出てくる。

I decided to perform some investigation using the feature that prints the values of registers when an error occurs while executing the function swrite.

Firstly, I executed this program to obtain the return address of the function swrite.
This program copies the return address of the function swrite to the RSI register, and then causes an error by divding by zero.
Therefore, the return address of the function swrite will be printed as RSI.

bites 64 mov rsi, [rsp + 0x90] xor eax, eax xor edx, edx xor ecx, ecx div ecx

アセンブルすると以下のようになる。これをGhidraにおける 00100048 に相当する、initramfs.cpio0x170 から書き込んだ。

Assembling this program yields this. I put this program from 0x170 of initramfs.cpio, which corresponds to 00100048 on Ghidra.

48 8b b4 24 90 00 00 00 31 c0 31 d2 31 c9 f7 f1

そして、以下のコマンドでこのプログラムを実行した。

Then, I executed this program via this command:

dd if=/init of=/proc/pwn_device count=1 bs=16

次に、このswrite関数のリターンアドレスを利用して prepare_kernel_cred 関数のアドレスを求め、呼び出した後ゼロ除算を行う、以下のプログラムを用意した。
このプログラムは、prepare_kernel_cred 関数の返り値を RCX に入れた状態でエラーを起こす。

After that, I created this program to obtain the address of the function prepare_kernel_cred from the return address of swrite, call the function and finally divide by zero.
This program causes an error with the return value of the function prepare_kernel_cred in RCX.

bits 64 mov rax, [rsp + 0x90] add rax, -0x1b6127 xor edi, edi call rax push rax pop rcx xor esi, esi div esi

これをアセンブルして swrite 関数に書き込みたいが、書き換える前のプログラムで 00 00 00 00 となっている部分を書き換えるとモジュールが読み込めなくなってしまった。
そこで、これを避け、以下の配置で initramfs.cpio に書き込んだ。xx は書き換える前のプログラムの値を残すことを意味する。

Trying to write this program after assembling to the function swrite, I found that modifying 00 00 00 00 in the original program makes it fail to read the module.
Therefore, I avoided this and wrote the program to initramfs.cpio in this placement. xx stand for preserving the original values.

000170 48 8b 84 24 90 00 00 00 48 05 d9 9e e4 ff 31 ff 000180 ff d0 50 xx xx xx xx xx xx xx 59 31 f6 f7 f6

このプログラムを何回か実行した結果、prepare_kernel_cred(0) の返り値はQEMUを起動しなおすと返り値は変わるが、起動しなおさず連続で実行すると返り値は変わらなそうだった。

このことから、処理を以下の2個のプロセスに分けることを思いついた。

Executing this program several times, I found that the return value of prepare_kernel_cred(0) changes after re-launching QEMU, but it didn't change when I executed the program in a row.

Seeing this, I came up with a way where I use these two processes:

flagを得る Obtaining the flag

まず、エラーを起こして prepare_kernel_cred(0) の返り値を得るため、メモリアクセスができるgadgetを探した。
すると、以下のものが見つかった。

Firstly, to cause an error to obtain the return value of prepare_kernel_cred(0), I searched for a gadget that can access to the memory.
As a result, I found this:

c0a9: c7 07 01 00 00 00 movl $0x1,(%rdi)

なお、このgadgetは実行することで強制終了することを意図しているので、ret は不要である。

次に、prepare_kernel_cred(0) を呼び出した後、このgadgetを利用して0番地への書き込みを行うプログラム attack4_reveal.asm を作成した。

Note that ret isn't needed here because this gadget is for forcing to exit by being executed.

After that, I created a program attack4_reveal.asm that calls prepare_kernel_cred(0) and writes to the address 0 using this gadget.

attack4_reveal.asm

このプログラムを実行すると、RAXとして prepare_kernel_cred(0) の返り値が出力される。

次に、この返り値を利用するプログラムを作るにあたり、テキストで表された数値をバイナリに変換するのは大変そうなので、既存のコマンドを利用することにした。
xxdコマンドを利用するとバイト列を表す文字列をバイト列に変換できるが、それだけだとバイトオーダーが逆になってしまう。
そこで、bswap 命令を利用してバイトオーダーを反転させ、値として利用できる形にすることにした。

これを踏まえ、prepare_kernel_cred(0) の返り値を入力として受け取り、flagを出力するプログラム attack4_flag.asm を作成した。
/bin/sh を実行しようとするとエラーになってしまったので、/flag.txt の内容を直接読み取ることにした。

The return value of prepare_kernel_cred(0) should be printed as RAX after executing this program.

To create a program that uses this return value, I decided to utilize existing commands because converting numbers represented as text to binary looks tough.
The xxd command can convert strings that represents sequences of bytes to sequences of bytes, but this will yield the number with the order of bytes reversed.
Therefore, I decided to use the bswap instruction to reverse the order of bytes and make it usable as a value.

Based on this, I created a program attack4_flag.asm that takes the return value of prepare_kernel_cred(0) and prints the flag.
I decided to read /flag.txt directly because trying to launch /bin/sh resulted in errors.

attack4_flag.asm

この2個のプログラムを用い、以下のようにflagが得られた。

I obtained the flag in this way, using these two programs:

attack4_result.txt

flag The flag

flag{c0ngr4t5_on_ur_f1r5t_k3rn3l}

K3RN3L CTF