2021年12月31日金曜日

セキュリティ・ミニキャンプオンライン2021 参加記 ~内容編4:Linuxシステムプログラミング入門:コンテナ技術を支える名前空間~

ミニキャンプオンライン2021「サイバー攻撃対応 入門」の募集課題、事前課題、講義、修了試験についての記事です。

他講義リンク

 

Linuxシステムプログラミング入門:コンテナ技術を支える名前空間

Linuxのシステムコールを駆使して、ミニコンテナを自作する講義でした。

個人的にこの講義が一番楽しみでした。理由としてMagisk(Androidのroot化するやつです)で名前空間が利用されていて、他のアプリからrootを隠せた(今はありません)んですけど、これの仕組みをちゃんと学びたかったというのがあります。

一時は個人で調べて情報を見つけられませんでしたが、本講義のお陰で学ぶことが出来ました。また、システムコールをあれこれするのも楽しかったので、個人的に遊んだり、泥開発に活かせないかな〜と考えております。

また、こちらの講義関連物は以下で公開されているため、ぜひ確認してみてください。本当に丁寧に解説されています。そのためぼくがここにかくことがない

https://2021.mc.harro.ws/ 

募集課題

「標準入力から実行ファイルのパスを受け取り、その実行ファイルを実行するのを繰り返す」というプログラムを作成する問題でした。ミニキャンプオンライン概要ページ(https://www.security-camp.or.jp/minicamp/online2021.html)募集要項の問題4にあたります。

自分はexeclp()関数とfork()関数を利用し作成しました。作成したコードは以下です。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
char buf[128]; // 入力保持
pid_t fork_pid; // 子プロセスのPID
pid_t result; // waitpidの結果
int status; // 子プロセスで実行したプログラムの返り値
while(1) {
scanf("%s", buf); // 入力
fork_pid = fork();
if (fork_pid == -1) {
continue; // fork()失敗の場合、入力からやり直し
} else if (fork_pid == 0) {
// 子プロセスの動作
execlp(buf, buf, NULL); // プログラム実行
exit(-1); // 実行失敗の場合
}
result = waitpid(fork_pid, &status, 0); // 子プロセスの終了待機
}
}
view raw entry_task.c hosted with ❤ by GitHub

躓いた点としては、初めに軽く調べて動作はしてくれるプログラムができたのですが、waitpid()の箇所が適切ではなく、プロセスが大量生成されてしまうということがありました。

事前課題 

一番初めに記述したページの「事前課題」が当たります。ここでは、演習課題について書いていこうと思います。

#4.2 演習:LinuxでのC言語の基礎

実際に提出したコードが以下です。

#include <stdio.h>
#include <unistd.h>
int print_string(const char* const str);
int main(int argc, char** argv) {
print_string("Hello, World!");
}
int print_string(const char* const str) {
size_t count;
char *tmpptr;
tmpptr = str;
// get str length
for(count = 0; ; count++) {
if(*tmpptr == 0) {
break;
}
tmpptr++;
}
// print str
write(STDOUT_FILENO, str, count);
write(STDOUT_FILENO, "\n", 1);
}
view raw pretask1.c hosted with ❤ by GitHub

 講評では、「正しく動くコードだけど、tmpstrの型がちょっとよくない」(意訳)とのコメントを頂きました。この時はポインタのconstの扱いがわからなかったので…

 ポインタのconstの扱いは、文字列ポインタを例にすると

  • const char*:ポインタが指すデータ変更不可、ポインタ変更可
  • char* const:ポインタが指すデータ変更可、ポインタ変更不可
  • const char* const:ポインタが指すデータも、ポインタも変更不可 

となっているのが調べて分かりました。したがって、tmpstrはconst char*型にするのがよさそうです。 


#4 演習:ファイルを実行してその結果を取得する関数

実際に提出したコードが以下です。

#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define OUTPUT_BUFFER_SIZE 1024
int execute_binary(const char* const binary, char* const buffer, const size_t bufsize);
int main(){
char buf[OUTPUT_BUFFER_SIZE] = {0};
int r = execute_binary("/bin/ls", buf, OUTPUT_BUFFER_SIZE);
if(r >= 0){
buf[r] = '\0';
printf("read %d bytes\n%s\n", r, buf);
} else {
printf("error %d\n", r);
}
}
int child_proc(void* arg) {
char *path = (char *)arg;
char *argv[] = {path, NULL}; // argv
char *env[] = {NULL}; // envp
/* execve */
execve(path, argv, env);
exit(EXIT_FAILURE);
}
int execute_binary(const char* const binary, char* const buffer, const size_t bufsize) {
void *stack; // stack for child process
pid_t child_pid; // child pid
int status; // child process status
int tmp_fd; // temp file descriptors(STDOUT_FILENO)
int pipe_fd[2]; // file descriptors (pipe_fd[0]: read, pipe_fd[1]: write)
ssize_t sz; // read size
/* make pipe */
if(pipe(pipe_fd) == -1) {
puts("pipe failed!\n");
return -1;
}
/* make stack */
stack = mmap(NULL, 1024*1024, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_STACK,-1,0);
if(stack == MAP_FAILED) {
puts("mmap failed!\n");
return -1;
}
/* switch fd */
tmp_fd = dup(STDOUT_FILENO);
dup2(pipe_fd[1], STDOUT_FILENO);
/* exec binary */
child_pid = clone(child_proc, stack+1024*1024, SIGCHLD, binary);
/* restore STDOUT_FILENO fd */
dup2(tmp_fd, STDOUT_FILENO);
if(child_pid == -1) {
puts("clone failed!\n");
return -1;
}
waitpid(child_pid, &status, 0);
/* check status */
if(status != 0) {
puts("execve failed!\n");
return -1;
}
/* read */
sz = read(pipe_fd[0], buffer, bufsize);
if(sz < 0) {
puts("read failed!\n");
return -1;
}
return sz;
}
view raw pretask2.c hosted with ❤ by GitHub

こちらの講評は「waitpid()関数の使い方を自力で調べ、read()前にstatusをチェックしているところがいいね!dup2は子プロセスでやるといいかもね」(意訳&うろ覚え)と頂きました。

waitpid()はhttps://linuxjm.osdn.jp/html/LDP_man-pages/man2/wait.2.htmlを参考にしました。

dup2についてなのですが、このコードを書く前にDiscordで「複数の値を渡したいなら構造体を使うといい」的な話がされており、ということは「作問時に構造体の利用を考慮していおらず、使わないやり方が存在する…?」と謎の考察をしてこのコードに至りました。

動きとしては、 63行目で標準出力のディスクリプタを複製します。次に、64行目で標準出力のディスクリプタをパイプに置き換え、その後子プロセス実行を行います。clone()が実行された後、親プロセスは70行目に進むため、標準出力を63行目で複製したディスクリプタに置き換えます。複製されたディスクリプタは複製前と同じファイルにアクセスできるので、結果的に標準出力が元に戻るということです。

講義の繋がりを考えると構造体を使って子プロセスにパイプを渡し、子プロセスのみ標準出力を書き換えた方が良かったです。参考にしないでください。

講義

実際にミニコンテナを作成しました。事前課題で作成したprint_string()関数はUIDマップの書き出し、execute_binary()関数はそのまま実行ファイルの実行に利用しました。この講義は他の講義に比べ、より課題と講義が繋がっており、「これがあの課題の伏線回収か…」と気持ちよかったです。

しかし、即興でコードを書くことに慣れていなかった私は、指定された時間に演習を完了できなかった場所がありました。これを通して、この演習以外でも競プロやCTFで重要となるであろう「即興コーディング力」は鍛えないとなと感じました。

修了試験

問題サーバにログインし、フラグを探すという問題でした。なお、スクショは後撮りです。

当時書いたコードは直接競技サーバで作業していましたし、残っていたとしてもフラグ取るためだけの汚いコードなので、今回はどんな作業をしたかのみ記録します。

この問題は一番初めに解き始めました。一番最初に公開されたので…

ログインをするとREADMEがあり、それによると「/try_running_meを実行してね、unshareでググるといいかも」とのこと。

とりあえず権限の確認と、実行を行ってみると

とのことで、実行権限しかなく、お家芸であるstringsコマンドで調べることができないため、素直に従うことにしました。かなしいね。

シェルで実行している限り、第0引数は自身の実行パス(/try_running_me)が入ってしまうため、講義で扱ったexecve()関数の引数を変えたプログラムを書けばいいのだなと思いつきました。

しかしリモート環境でコード書けるのか…?それともunshareというものを理解しないとなのか…?と恐る恐るnanoとシェルに打ち込んでみたらあっさり立ちあがりました。 よかった。

また、ここで自分は空文字列ではなくNULLを渡すものと勘違いしてしまいちょっと躓きました。文章を読め。

あとはコンパイルして実行、フラグくるか!?と思ったら


はい。

とのことで、ここでunshareかな?と思い調べました。しかし、自分はunshareを「Cのコード内で使う」という固定概念で調べていました。したがって「よく分からないし時間ないし修了試験CTF中にあたらしいことやる?」となり、「fakerootになればいいんだろうから、UIDマップ書いてあげる講義のコード使いまわせばいいじゃん」とunshareの謎を残して作業しました。

そして後者の方針で無事フラグをゲットしましたが、

  • unshare使ってないがこれでよかったのか…?(別解として想定済み)
  • 想定外解答でリモートサーバ壊したりしてないよな…?(別解として想定ry)
  • もしや失格…?(別解としてry)
と不安になりながら終わりました。

 

その後、解説にて「unshare"コマンド"を使うと簡単に解けます」と実演され、"コマンド"かあ~となったのと、めっちゃ簡単に解けちゃうじゃん…となりました。おしまい。

0 件のコメント:

コメントを投稿