본문 바로가기

Pay1oad/Pwnable Study

[Pay1oad] Pwnable Study Week 1 - Pay1oad Welcome CTF Writeup

<Homework>

1. Handray 폴더 안에 존재하는 HW 파일의 main 및 func를 어셈블리어만 보고 C 언어로 바꾸기
   1.2 그 과정에서의 main의 스택 프레임 그리기
2. Pay1oad Welcome CTF에 출제되었던 cute 및 Little_shell 문제 풀이 및 라이트 업 작성하기

 

<Environment>

Macbook Air 15, Apple M2, 16GB ram, 1TB storage

MAC Sequoia 15.1 Beta (24B5035e)

IDA Freeware 8.2

Parallels Desktop 18 for Mac (Ubuntu 22.04 ARM 64)

 


Pay1oad Welcome CTF / cute

제공받은 file들은 cute, cute.c, flag로 총 3개이다.

 

우선 cute.c code를 살펴보자.

#include <stdio.h>
#include <stdlib.h> 
#include <string.h>
#include <unistd.h> //function read는 표준 C 라이브러리에 포함되어 있지 않고 POSIX 표준의 function이기 때문에 해당 헤더파일 포함

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int print_file(char * filename){
    FILE *file = fopen(filename, "r");

    if (file == NULL) {
        perror("파일을 열 수 없습니다");
        return 1;
    }

    int ch;
    while ((ch = fgetc(file)) != EOF) {
        putchar(ch);
    }

    // 파일을 닫습니다
    fclose(file);
    return 0;
}

int main(){
    init();   
    char name[0x10];
    char filename[0x10] = "meow";
    printf("Hello, What's your name: ");
    read(0,name, 0x18);
    printf("Hello %s!\n", name);
    printf("This is my Cat\n");

    print_file(filename);

    return 0;
}

 

*function read의 execute를 위해 header file "unistd.h"는 본인이 추가하였다.

우선 c language로 작성된 source code를 확인해보자. 

단순히 보았을 때는 이름을 묻고 해당 값을 입력 받아 재출력하는 program으로 생각할 수 있지만 funciton print_file을 살펴보면 variable filename을 인자로 받아 해당 인자에 해당하는 이름을 갖고 있는 file을 execute하는 것을 확인할 수 있다. 하지만 filename을 입력받고 있는 code가 없는 것으로 보아 이를 solve하는 것이 이 problem의 keypoint라고 생각했다.

 

function print_file을 먼저 살펴보자.

filename을 인자로 받는데, 해당 parameter의 pointer를 받아서 variable file pointer를 정의하고 parameter로 받은 variable을 read mode로 open한다. 만약 file == NULL이라면, 즉 해당 parameter를 name으로 한 file이 detect되지 않는다면,  function ferror을 통해 파일을 열 수 없다는 error를 표출하고 terminate한다.

 

function main으로 돌아와서,

0x10 size의 variable인 name을 선언하고, 0x10 size의 variable인 filename을 선언 후 "meow"라는 string을 저장한다.

이후, "Hello, What's your name: " string을 print 하고 function read로 variable name에 maximun 0x18의 size를 standard input으로 받고 있음을 알 수 있다.

여기서 눈썰미가 좋은 사람은 이상함을 눈치챌 수 있는데 바로 variable name의 size와 read로 입력받는 size에 주목해야 한다. variable name의 size는 0x10, 10진수로는 16이라는 size를 가지고 있지만, read이 요구하는 input value의 size는 0x18, 10진수로는 24라는 size를 입력받고 있다. 즉, variable name의 size를 초과하게 되는 value를 입력받게 되었을 때, buffer overflow가 일어나게 되는 것이다. 여기서는 variable filename을 침범하게 되는 것이다. 따라서, variable name의 size만큼 input value를 입력한뒤, 우리가 열어야 되는, flag 가 들어있는 file name인 "flag"라는 string을 입력하면 되지 않을까? 라는 생각을 가지고 program 실행 후 aaaaaaaaaaaaaaaaflag를 입력하였다. 놀랍겠지만 당연히 출력이 안됐다. 그 이유가 무엇일까?

function read는 우리가 keyborad로 입력하는 value를 모두 받는다. 예를 들어, flag라는 stirng을 입력한다고 하자. 우리는 keyboard로 flag을 뚱땅뚱땅 치고 값을 입력하기 위해 "Enter" key를 누를 것이다. 맞다. 이것이다. 우리가 code를 작성하다 보면 개행하기 위해 \n라는 개행 문자를 입력할 때가 있는데 function read는 input을 받을 때, input value로 들어가는 값은 "Enter", 즉 "\n" 문자도 함께 들어가게 된다. 자 그럼, 우리가 입력한 filename은 바로!! "flag\n" 이다. 캬~ 어떠한 file도 open 할 수 없지 않겠는가?

(cf. function scanf는 input value를 받을 때, 개행 문자는 생략하고 받는다. read가 low level function 이라고도 불리는 이유..)

 

따라서, pwntools를 이용해 exploit code를 python으로 작성하였다.

from pwn import *

p = process('./cute')

p.sendafter(b"Hello, What's your name: ", b'a'*16 + b'flag')

data = p.recvall()
print(data)

 

이 code에 대해 간단히 설명하자면, 우선 pwn, 즉 module pwntools의 모든 객체를 import 한다.

variable p에 executable file인 cute를 실행해 p에 저장하고, function sendafter를 사용해 string이 출력된 후 ./cute가 b'Hello, What's your name: '를 출력하면, b'a' * 16 + b'flag'를 입력한다. 이후 출력되는 결과를 받고자 function recvall을 이용해 p가 print하는 data를 process가 terminate될 때까지 받아서 data에 저장한 후 이 data를 출력해 flag 값을 확인하는 동작 흐름을 가지고 있다. 이를 실행하면, 

 

위와 같이 flag value인 "Pay1oad{C4T_S4V3_TH3_WORLD}" 가 print 되는 것을 확인할 수 있다.


아래의 내용은 추가로 공부한 내용이다.

cute.c code에서 function init 내부의 function setvbuf 는 stdio.h에 정의된 함수로, 스트림(stream)의 buffering behavior을 변경한다. 여기서 buffering에 대해 조금 더 자세히 살펴보자.

1. 완전 버퍼링 (Full Buffering)
: 스트림이 가득 찰 때까지 데이터를 버퍼에 모아두었다가, 버퍼가 가득 차거나 fflush() 같은 특정 함수가 호출될 때 한꺼번에 입출력 작업을 수행한다.
- 동작 방식
    • 스트림이 출력 버퍼를 다 채울 때까지 데이터를 모은다.
    • 버퍼가 가득 차면 전체 버퍼의 내용을 출력하거나 기록한다.
    • fflush() 함수로 버퍼를 비우거나 스트림이 닫힐 때도 버퍼의 내용을 출력한다.
- 사용 시기
    • 완전 버퍼링은 파일이나 소켓과 같은 느린 입출력 장치에 데이터를 기록할 때 효율적이다. 버퍼링 없이 매번 입출력 작업을 하면 성능이 저하될 수 있는데, 버퍼링을 사용하면 입출력 작업을 줄여 성능을 향상시킬 수 있다.
- ex.
  스트림이 파일에 연결되어 있으면 완전 버퍼링이 기본 동작이 될 수 있다.

2. 줄 버퍼링 (Line Buffering)
: 스트림이 줄바꿈 문자(\n)를 만났을 때마다 또는 버퍼가 가득 찰 때 데이터를 출력한다. 줄 단위로 입출력 작업이 수행되는 방식이다.
- 동작 방식
    • 스트림에 줄바꿈(\n)이 발생하거나 버퍼가 가득 차면 버퍼의 내용을 출력하거나 기록한다.
    • fflush() 함수가 호출되면 버퍼를 강제로 비운다.
- 사용 시기
    줄 버퍼링은 주로 표준 입력과 표준 출력에 사용된다. 사용자와 프로그램이 상호 작용하는 터미널(콘솔) 에 연결된 스트림의 경우 줄 단위로 데이터를 처리하는 것이 적합하다. 이를 통해 사용자는 한 줄을 완전히 입력할 때마다 프로그램이 입력을 처리하고, 프로그램은 한 줄의 데이터를 출력할 때마다 화면에 표시한다.
- ex.
   printf("Hello, World!\n");에서 \n이 포함되어 있다면 버퍼에 내용이 모아지지 않고 즉시 출력된다. 만약 \n이 없다면 버퍼가 가득 차거나 fflush()로 강제로 비우기 전까지 출력되지 않을 수 있다.

3. 버퍼링 없음 (No Buffering)
: 입출력 작업을 즉시 처리하는 모드이다. 버퍼에 데이터를 모아두지 않고, 데이터를 스트림에 쓰거나 스트림으로부터 읽는 즉시 입출력 작업이 수행된다.
- 동작 방식
    • 버퍼 없이 데이터를 즉시 입출력하므로, 어떤 내용이 스트림에 쓰이거나 읽힐 때 바로 처리된다.
    • 이 방식은 버퍼링 없이 매번 입출력 작업이 일어나므로 성능 상 손해가 있을 수 있다.
- 사용 시기
   버퍼링이 필요하지 않고 즉각적인 입출력 작업이 필요한 경우 사용된다. 예를 들어, 표준 오류 스트림(stderr)은 즉각적인 오류 메시지 처리가 중요하므로 버퍼링 없이 출력된다. 이로 인해 프로그램 실행 중 발생하는 오류가 지연 없이 바로 화면에 표시된다.

 

위 문제 code 처럼 function init에서 setvbuf 설정을 하지 않으면, C standard library는 standard stream(stdin, stdout, stderr)에 대해 각각 Line buffering, Line buffering, No buffering이 적용된다.

 

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

 

위와 같이 buffering setting을 한다는 것은, 기본 stdin, stdout이 Line buffering으로 setting되어 있는 것을 No buffering으로 setting 한다는 것이다. function init을 통해 stdin, stdout이 buffering 없이 즉시 처리되거나 즉시 print 되도록 만든다.

참고로, 아래는 function setvbuf의 structure이다.

int setvbuf(FILE *stream, char *buffer, int mode, size_t size);

 

stream (FILE *): buffering setting할 stream 지정. ex, stdin, stdout, stderr etc.

buffer (char *):  buffer를 수동으로 설정할 때 사용할 buffer의 address. 0(or NULL)로 지정하면 system에서 자동으로 buffer allocate.

mode (int): buffering mode 지정.

_IOFBF (0): 완전 버퍼링(Full Buffering). buffer가 가득 찰 때까지 data를 저장한 후 한 번에 print.

_IOLBF (1): 줄 버퍼링(Line Buffering). 줄바꿈 문자가 나타날 때마다 또는 버퍼가 가득 찰 때 data print.

_IONBF (2): 버퍼링 없음(No Buffering). buffering 없이 즉시 data print.

size (size_t): buffer의 size를 bite 단위로 지정. 0을 사용하면 standard size를 사용.


Pay1oad Welcome CTF / Little_shell

제공받은 file들은 little_shell, flag로 총 2개이다.