본문 바로가기

해킹/pwnable.kr

생초보 pwnable.kr - input write-up(C언어 풀이)

728x90

필요 정보


 

 1. 프로그램에서 다른 프로그램 실행하기(exec)

종종 내가 짠 코드(프로그램)에서 이미 있는 프로그램을 실행할 필요가 있습니다. 여태까지 문제를 풀면서 봐왔던 system() 함수도 인자로 들어간 스트링에 해당하는 것을 그대로 shell에서 실행시켜 주는 함수입니다. system 외에도 C언어에서는 exec 계열의 함수로 다른 프로그램을 실행시키고 인자로 여러 가지 값을 줄 수 있는데요. 이에 대해 알아보겠습니다.

#include <unistd.h>

int execl(char *pathname, char *arg0, ... /* (char *) 0 */); // 마지막은 NULL이 들어와야 합니다.
execl("/home/Hellowhales/filemerge", "filemerge", "hello.c", "whales.c", NULL);

int execv(char *pathname, char *argv[]);
execv("/home/Hellowhales/filemerge", argv);

int execle(char *pathname, char *arg0, ... , /* (char *) 0, char *envp[] */);
execle("/home/Hellowhales/filemerge", "filemerge", "hello.c", "whales.c", NULL , env);

int execve(char *pathname, char *argv[], char *envp[]);
execv("/home/Hellowhales/filemerge", argv, env);

int execlp(char *filename, char *arg0, ... , /* (char *) 0 */);
execlp("filemerge", "hello.c", "whales.c" , NULL);

int execvp(char *filename, char *argv[]);
execvp("filemerge", argv);

이렇게 6개 있습니다. 기본적으로 exec에 아래 표에 해당하는 의미가 추가된 형식의 함수 이름입니다. 

기본 exec +l +v +e +p
  인자로 여러값을 받는다 인자를 배열로 한꺼번에 받는다. 환경변수 값을 받는다 실행파일을 PATH라는 환경변수에서 찾아서 실행한다.

함수 실행시 system()함수와 똑같이 프로세스 내에서 다른 프로그램이 실행됩니다. 

 2. IPC(inter-process communication) - pipe

 

여러 개의 프로세스가 동시에 돌아가다 보면 각 프로세스가 정보를 공유하면서 작업을 진행해야되는 상황이 많습니다. 많은 방법이 있지만 그중 UNIX IPC 방법 중 가장 오래되고 부모자식 프로세스 간의 통신 기법인 pipe입니다.

위 그림과 같이 2칸짜리 정수형 배열을 만들고 하나는 읽기 전용 다른 하나는 쓰기 전용 fd라고 생각하면 됩니다. 그리고 fork를 하면 fd 배열이 그대로 child 프로세스로 전해지므로 이 fd를 통해 pipe가 생성됩니다. 다만 반이중 방식이기 때문에 한쪽단이 쓰기나 읽기 둘 중 하나를 하고 다른 쪽이 그 반대의 것을 하게 됩니다.

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

int main()
{
    int fd[2];
    pid_t pid;
    char buf[15];
    if( (pid =fork()) > 0) // parent
    {
    	close(fd[0]); // 읽는 부분은 닫아준다.
        strcpy(buf, "HelloWhales\n");
        write(fd[1], buf, strlen(buf) + 1);
    }
    else if(pid == 0) // child
    {
    	close(fd[1]); // 쓰는 부분은 닫아준다.
    	read(fd[0], buf, 15);
        printf("%s", buf); // HelloWhales
    }
    return 0;
}

pipe를 사용할 때 fd에 해당하는 값을 dup() 함수를 통해 현재 있는 fd에 연결시켜 줄 수 있습니다.

dup2(fd[0], 0); // fd[0] stdin과 연결시킨다.

이제 stdin의 입력을 받을 때 pipe에 있는 값을 읽어오게 됩니다.

 3. Socket programing - TCP

소켓 프로그래밍은 네트워크를 통해서 통신을 하는 프로그램을 만드는 것을 말합니다. 소켓 프로그래밍은 크게 연결 상태를 유지하는 TCP 방식와 연결상태를 중요치 않게 생각하는 UDP 방식으로 나눌 수 있습니다. 이 포스터에서는 문제를 위해 TCP 방식에 대해서만 간단히 알아보겠습니다. 

우선 네트워크를 통해서 두 컴퓨터 혹은 프로세스가 통신할라면 한쪽은 Server가 되고 다른 쪽은 Client가 되어야합니다.

간단히 함수들에 대해 알아 보겠습니다.

  •  socket() : 소켓을 생성해주는 함수로 사용하는 프로토콜을 지정해주고 TCP/UDP인지 지정해줍니다. 소켓 descriptor를 리턴합니다.
  • bind() : 소켓의 IP주소 및 포트 번호를 지정해줍니다.
  • listen() : 동시에 몇개의 접속까지 허용할 것인지 정합니다. 하지만 최근에는 별로 의미가 있진 않고 관습적으로  사용합니다.
  • accept() : 커넥션이 오기를 기다립니다. 커넥션이 오면 맺어진 새로운 소켓 디스크립터 리턴합니다.  *peer에 클라이언트에 대한 정보가 저장됩니다.
  • read(),write() : 소켓이 맺어진 후 맺어진 소켓 디스크립터를 통해서 데이터를 적거나 읽어 옵니다.
  • connet() : 2번째 인자로 서버쪽 주소와 포트를 주면 socket을 연결해줍니다.

위와 같은 방식으로 두 프로세스가 연결되면 파일에 글을 쓰는것과 마찬가지로 read,write를 통해서 통신할 수 있습니다.

2023.04.04 - [C++] - c - socket 프로그래밍

 

c - socket 프로그래밍

Socket programing - TCP 소켓 프로그래밍은 네트워크를 통해서 통신을 하는 프로그램을 만드는 것을 말합니다. 소켓 프로그래밍은 크게 연결 상태를 유지하는 TCP 방식와 연결상태를 중요치 않게 생각

hellowhales.tistory.com

 

Write-Up


후 c에 관한 input을 모두 다룰 수 있는 문제라는 것을 듣고 문제를 시작하게 되었습니다. 그렇기 때문에 풀기 위해 필요한 정보도 많았네요. 시작해보겠습니다.

4점짜리 문제군요 약간은 빡셀 것 같습니다. ssh로 접속해보겠습니다. input, input.c, flag 3개의 파일이 있습니다. 무난하네요. input.c부터 보겠습니다. 전체 코드를 보면 5가지 단계가 있고 하나하나 깨면 clear 문구가 나옵니다. 작은 문제가 5개가 있는거군요. Stage 1부터 보겠습니다.

여기도 작은 문제가  3개네요... argc 가 100이어야하고 argv['A'] 값과 argv['B']이 정해진 값이어야 통과할 수 있습니다. 문제는 argc를 100을 맞춰주는 것은 커맨드라인 인자로 가능하겠지만 커맨드라인인자로는 \x00값인 널값을 줄 수 없습니다. 고로 커맨드라인 인자로는 문제를 풀 수 없습니다. 따라서 프로그램을 통해서 input을 실행시키면서 인자를 넣어주어야 합니다. 하지만 현재 디렉토리에는 파일을 만들 권한이 없습니다. 

따라서 가장 상위 디렉토리로가 임시 파일을 만들 수 있는 tmp 디렉토리에 자신만의 디렉토리를 만들어주고 여기서 작업하겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
    int i;

    char* args[101] = {};

    for (i = 0; i < 101; i++)
    {
        args[i] = "A";
    }
    args['A'] = "\x00";
    args['B'] = "\x20\x0a\x0d";
    args[100] = NULL;
    
    execve("/home/input2/input", args, NULL);
}

여기까지 오면 stage 1에 해당하는 코드는 쉽습니다.  args 배열을 만들고 문제에 해당하는 부분에 맞는 값을 넣어주고 argc를 100으로 맞춰주기 위해 args[100]에 NULL값을 넣어줍니다. 그리고 위에서 설명한 execve를 통해서 input 프로그램을 실행시켜주고 args를 인자로 넣어주면됩니다.

Stage 2입니다. 보아하니 이번에는 standard I/O에 관한 문제네요. 이것도 작은 문제가 2개네요. 하나는 fd 0인 stdin을 통해서 read해서 memcmp를 통해서 비교하고 두번째는 stderr를 통해서 read해서 주어진 16진수들과 비교하는겁니다.우리는 어처피 지금 다른 프로그램에서 입력값을 주고 있으니 이 또한 프로그램 내에서 입력을 주도록 해야합니다. 이때 사용되는 것이 위에서 설명한 pipe입니다. pipe를 통해서 부모 프로세스에서 write를 통해서 입력값을 넣어주고 child 프로세스에서 input 프로그램을 실행시키는 형식으로 짜보겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
    /* Stage 1 */
    int i;

    char* args[101] = {};

    for (i = 0; i < 101; i++)
    {
        args[i] = "A";
    }
    args['A'] = "\x00";
    args['B'] = "\x20\x0a\x0d";
    args[100] = NULL;
    
    /* Stage 2 */
    pid_t pid;
    int pipe_stdin[2];
    int pipe_stderr[2];
    
    if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0)
    {
        perror("pipe error\n");
        exit(1);
    }
    if ((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    
    if (pid == 0) // parent
    {
        close(pipe_stdin[0]);// parent can't read
        close(pipe_stderr[0]);

        write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
        write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
    }
    else // child
    {
        close(pipe_stdin[1]);// child can't write
        close(pipe_stderr[1]);

        dup2(pipe_stdin[0], 0); // pipe_stdin connect stdin
        dup2(pipe_stderr[0], 2); // pipe_stderr connect stderr

        close(pipe_stdin[0]);
        close(pipe_stderr[0]);

        execve("/home/input2/input", args, NULL);
    }
}

우선 stdin,stderr를 대한 pipe를 각각 만들어 줘야되기 때문에 fd로 쓸 버퍼를 두개 만들어줍니다. 그 다음 fork를 통해 child 프로세스를 만들어주고 부모쪽에서는 write만 하기 때문에 read에 해당하는 버퍼의 첫번째 인덱스를 close 해줍니다. 그리고 write를 통해 각각의 pipe에 답을 써주도록 하겠습니다. 자식 프로세스에서는 마찬가지로 read만 하기에  write에 해당하는 버퍼는 close해주겠습니다. 그리고 dup2를 통해서 pipe_stdin과 pipe_stderr를 stdin(fd = 0)과 stderr( fd = 2)와 연결해주겠습니다. 이렇게 해야 input에서 read를 통해 파이프에서 써준 답을 읽어올 수 있습니다. 마지막엔 child에서 input을 실행시켜주도록 하겠습니다. 

자! 이렇게하면 Stage 2까지 clear할 수 있습니다. 후 문제 풀면서 stage2가 제일 힘들었네요. 다음부터는 간단하니 힘내봅시다.

Stage 3입니다. env 즉 환경변수에 관한 문제입니다. getenv()는 인자로 준 환경변수의 값을 읽어오는 함수입니다. 따라서 \xde\xad\xbe\xef라는 환경변수에 \xca\xfe\xba\xbe 값을 넣어주면 되겠죠. 넣어주는 함수는 간단하게 유추할 수 있습니다. get이 있으면 set도 있겠죠. setenv()를 통해 값을 넣어주겠습니다. 아 그리고 하기전에 우선 extern char** environ 통해서
환경변수들을 가져와주겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

extern char** environ; // 환경 변수

int main()
{
    /* Stage 1 */
    int i;

    char* args[101] = {};

    for (i = 0; i < 101; i++)
    {
        args[i] = "A";
    }
    args['A'] = "\x00";
    args['B'] = "\x20\x0a\x0d";
    args[100] = NULL;
    
    /* Stage 3 */
    setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
    
    /* Stage 2 */
    pid_t pid;
    int pipe_stdin[2];
    int pipe_stderr[2];
    
    if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0)
    {
        perror("pipe error\n");
        exit(1);
    }
    if ((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }
    
    if (pid == 0) // parent
    {
        close(pipe_stdin[0]);// parent can't read
        close(pipe_stderr[0]);

        write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
        write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
    }
    else // child
    {
        close(pipe_stdin[1]);// child can't write
        close(pipe_stderr[1]);

        dup2(pipe_stdin[0], 0); // pipe_stdin connect stdin
        dup2(pipe_stderr[0], 2); // pipe_stderr connect stderr

        close(pipe_stdin[0]);
        close(pipe_stderr[0]);

        execve("/home/input2/input", args, environ);
    }
}

이번에는 execve로 input을 실행할 때 환경 변수값도 주도록 하겠습니다. 이러면 Stage 3도 clear입니다!!!

바로 Stage 4로 가겠습니다. 

이번에는 file문제입니다. \x0a라는 파일을 읽기모드로 열고 buf에 4byte를 읽어오고 buf랑 \x00\x00\x00\x00과 비교하는 문제입니다. 이것도 어렵지 않습니다. \x0a라는 파일을 쓰기모드로 만드러 그대로 써주면 됩니다.

    FILE* fp = fopen("\x0a", "w");
    fwrite("\x00\x00\x00\x00", 4, 1, fp);

    fclose(fp);

를 추가해주면 되겠죠. 굳입니다.

드디어 마지막 Stage 5입니다. 마지막답게 네트워크 통신에 관한 문제입니다. 어려워 보이지만 위에 적은 TCP 통신 내용만 알고 있으면 쉽게 풀 수 있습니다. 우선 socket()함수 인자가 SOCK_STREAM이므로 TCP 통신인 것을 알 수 있습니다. 눈여겨봐야되는 것은 port 번호를 argv['C']를 통해서 받고있습니다. 그 외에는 이제 통신에서 4byte를 읽어와서 buf에 넣고 비교하는 것입니다. 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

extern char** environ;

int main()
{
    /* Stage 1 */
    int i;

    char* args[101] = {};

    for (i = 0; i < 101; i++)
    {
        args[i] = "A";
    }
    args['A'] = "\x00";
    args['B'] = "\x20\x0a\x0d";
    args['C'] = "20702";
    args[100] = NULL;
    /* Stage 3 */
    setenv("\xde\xad\xbe\xef", "\xca\xfe\xba\xbe", 1);
    /* Stage 4 */
    FILE* fp = fopen("\x0a", "w");
    fwrite("\x00\x00\x00\x00", 4, 1, fp);

    fclose(fp);
    /* Stage 2 */
    pid_t pid;
    int pipe_stdin[2];
    int pipe_stderr[2];

    if (pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0)
    {
        perror("pipe error\n");
        exit(1);
    }
    if ((pid = fork()) < 0)
    {
        perror("fork error");
        exit(1);
    }

    if (pid == 0) // parent
    {
        close(pipe_stdin[0]);// parent can't read
        close(pipe_stderr[0]);

        write(pipe_stdin[1], "\x00\x0a\x00\xff", 4);
        write(pipe_stderr[1], "\x00\x0a\x02\xff", 4);
        /* Stage 5 */
        sleep(3);
  
        int sd;
        struct sockaddr_in saddr;

        sd = socket(AF_INET, SOCK_STREAM, 0);
        if (sd == -1)
        {
            perror("Socket");
            exit(1);
        }
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
        saddr.sin_port = htons(atoi(args['C']));
        if (connect(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0)
        {
            perror("Conect");
            exit(1);
        }
        if (write(sd, "\xde\xad\xbe\xef", 4) < 0)
        {
            perror("write");
            exit(1);
        }
        close(sd);
        return 0;
    }
    else // child
    {
        close(pipe_stdin[1]);// child can't write
        close(pipe_stderr[1]);

        dup2(pipe_stdin[0], 0); // pipe_stdin connect stdin
        dup2(pipe_stderr[0], 2); // pipe_stderr connect stderr

        close(pipe_stdin[0]);
        close(pipe_stderr[0]);

        execve("/home/input2/input", args, environ);
    }

}

argv['C']를 포트번호로 받으니까 우리 프로그램에서 argv['C']를 적절한 포트번호로 설정해주겠습니다. 또 네트워크 통신 이지만 부모 프로세스와 자식 프로세스간의 통신이므로 부모에서 소켓을 열어줍니다. 그리고 어쨋든 input이 서버역할이므로 input에서 먼저 소켓을 열고 bind()한 후 accept()로 기다려야 하므로 부모에서 소켓을 열고 connect()하기 전에 sleep을 통해 몇초간 쉬어주도록 합니다. 이것이 없으면 프로세스 실행 순서에 따라서 부모가 먼저 소켓을 열고 connect한다면 서버가 아직 열리지 않았으므로 소켓 연결에 실패할 것입니다.

다음은 소켓 프로토콜(family), 주소, 포트를 설정해줍니다. 주소는 같은 컴퓨터 내에서 통신이으로 loopback 주소인 127.0.0.1을 사용하고 포트 번호는 args['C']으로 설정합니다. 그리고 connect 후에 wirte로 통신 데이터를 적어주면 되겠습니다. 자 이렇게하면 Stage 5도 clear 입니다!!!!!!

 

그런데 이렇게 프로그램을 완성하고 돌리면  flag가 나오지 않습니다.

그 이유는 현재 디렉토리는 임시 디렉토리기 때문에 flag파일이 없기 때문입니다.

그래서 ln을 통해서 원래 디렉토리에 있는 flag를 현재 디렉토리에 .link를 걸어야합니다. 그렇게 하면 현재 디렉토리에 원본 flag에 대한 바로가기가 생깁니다.

후 드디어 flag를 얻었네요. 고생하셨습니다.

 

After review


후 정말 힘드네요. 정말로 C프로그래밍으로 할 수 있는 모든 input 관련 문제들이 담겨있었습니다. 처음에 문제를 딱보고 아 프로그램으로 넣어야겠다고 생각했는데 파일을 만들 권한이 없는 것 때문에 많이 해맸습니다. tmp디렉토리를 몰랐거든요. 그걸 알고나서부터는 예전에 공부한 내용들을 복습하면서 문제를 푸니 큰 어려움은 없었습니다. 단지 시간이 많이 들었죠... 그래도 이걸 언제 써먹나 했던 내용들을 많이 써볼 수 있었던 문제라 재밋었습니다. 그리고 이런 문제를 푸니 확실히 자신감이 생기네요. c 프로그래밍을 통한 input은 이제 문제없을 것 같다고 생각합니다... 확신은 아니고요. 아무튼 문제 푸시느라 고생하셨고 읽어주셔서 감사합니다.

728x90