gunnew의 잡설

pwnable.kr 7. input (Various I/O in Linux) 본문

System Security

pwnable.kr 7. input (Various I/O in Linux)

higunnew 2020. 2. 6. 23:21
반응형

 아... 이번 거는 그냥 소스 코드 보자마자 포기했다. 리눅스로 소켓 입출력 문제가 나올 것이라 생각도 못했다. 그냥 바로 해설을 보러 갔다...(비루한 인생) 하지만 그래도 내가 공부하며 알게된 것들을 내 나름대로 풀어 써 보자.

 

0. input.c 살펴보기

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



int main(int argc, char* argv[], char* envp[]){

        printf("Welcome to pwnable.kr\n");
        printf("Let's see if you know how to give input to program\n");
        printf("Just give me correct inputs then you will get the flag :)\n");



        // argv

        if(argc != 100) return 0;
        if(strcmp(argv['A'],"\x00")) return 0;
        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
        printf("Stage 1 clear!\n");

        // stdio
        char buf[4];

        read(0, buf, 4);

        if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
        
        read(2, buf, 4);
        
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
        
        printf("Stage 2 clear!\n");

        // env
        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
        
        printf("Stage 3 clear!\n");

        // file

        FILE* fp = fopen("\x0a", "r");

        if(!fp) return 0;

        if( fread(buf, 4, 1, fp)!=1 ) return 0;

        if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;

        fclose(fp);
        printf("Stage 4 clear!\n");

        // network
        int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);
        
        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }
        
        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );
        
        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }
        listen(sd, 1);
        
        int c = sizeof(struct sockaddr_in);
        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }
        
        if( recv(cd, buf, 4, 0) != 4 ) return 0;
        
        if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
        
        
        printf("Stage 5 clear!\n");

        // here's your flag
        system("/bin/cat flag");
        return 0;
}

 

 소스 코드를 보니 통과해야 할 부분이 총 5 가지가 있다. 

 

1. argv

2. stdio

3. env

4. file

5. network

 

이제 각 파트에 대해 심층 분석을 해보자.


1. argv

        // argv

        if(argc != 100) return 0;

        if(strcmp(argv['A'],"\x00")) return 0;

        if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;

        printf("Stage 1 clear!\n");

 

  1. 인자의 개수가 100이어야 한다.

  2. 인자의 65번째가 "\x00"이어야 한다

  3. 인자의 66번째가 "\x20\0a\0d"여야 한다.


2. stdio

    // stdio

        char buf[4];

        read(0, buf, 4);

        if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;

        read(2, buf, 4);

        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;

        printf("Stage 2 clear!\n");

 

이 문제는 file descriptor를 알고 있는지 묻는 문제이다. 

참조 : https://en.wikipedia.org/wiki/File_descriptor

 

File descriptor - Wikipedia

In Unix and related computer operating systems, a file descriptor (FD, less frequently fildes) is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket. File descriptors form part of the POSIX

en.wikipedia.org

File descriptor

 

  1. 0번 file descriptor를 이용하여 4바이트를 읽었을 때 "\x00\x0a\x00\xff"가 되어야 한다.

  2. 2번 file descriptor를 이용하여 4바이트를 읽었을 때 "\x00\x0a\x02\xff"가 되어야 한다.


3. env (환경 변수)

 

 사실 1, 2번은 어느 정도 프로그래밍을 해봤던 사람이라면 요구 조건을 금방 캐치할 수 있다. 그러나 3번은 조금 다르다. 나같은 프로그래밍 초짜들에게 환경 변수란 기껏해야 JDK 설치할 때 쯤 써본 정도일 것이다. 뭔지도 모르면서 말이다. 그러니까 이 글을 보고 있는 당신도 이 문제를 봤다면 환경 변수에 대해 모를 가능성이 크므로 스스로 공부하는 것이 필요하다.

 

        // env

        if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;

        printf("Stage 3 clear!\n");

 

  1. "\xde\xad\xbe\xef"의 환경 변수 값이 "\xca\xfe\xba\xbe"여야 한다.


4. File I/O

 

        // file

        FILE* fp = fopen("\x0a", "r");

        if(!fp) return 0;

        if( fread(buf, 4, 1, fp)!=1 ) return 0;

        if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;

        fclose(fp);

        printf("Stage 4 clear!\n");

 

<아주 친절한 man fread>

FREAD(3)                  Linux Programmer's Manual                  FREAD(3)


NAME

       fread, fwrite - binary stream input/output

SYNOPSIS

       #include <stdio.h>
       size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
       size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

DESCRIPTION

       The  function fread() reads nmemb items of data, each size bytes long,
       from the stream pointed to by stream, storing  them  at  the  location
       given by ptr.



       The  function  fwrite()  writes  nmemb  items of data, each size bytes
       long, to the stream pointed to by  stream,  obtaining  them  from  the
       location given by ptr.

       For nonlocking counterparts, see unlocked_stdio(3).

RETURN VALUE

       On  success,  fread()  and fwrite() return the number of items read or
       written.  This number equals the number of bytes transferred only when
       size is 1.  If an error occurs, or the end of the file is reached, the
       return value is a short item count (or zero).

       fread() does not distinguish between end-of-file and error, and  call‐
       ers must use feof(3) and ferror(3) to determine which occurred.

 

  1. "\x0a"라는 이름의 파일에서 4바이트짜리 한 개를 읽어서 그 값이 "\x00\x00\x00\x00"여야 한다.

5. Network (Socket I/O)

	int sd, cd;
        struct sockaddr_in saddr, caddr;
        sd = socket(AF_INET, SOCK_STREAM, 0);

        if(sd == -1){
                printf("socket error, tell admin\n");
                return 0;
        }

        saddr.sin_family = AF_INET;
        saddr.sin_addr.s_addr = INADDR_ANY;
        saddr.sin_port = htons( atoi(argv['C']) );

        if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
                printf("bind error, use another port\n");
                return 1;
        }

        listen(sd, 1);

        int c = sizeof(struct sockaddr_in);

        cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);

        if(cd < 0){
                printf("accept error, tell admin\n");
                return 0;
        }

        if( recv(cd, buf, 4, 0) != 4 ) return 0;

        if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;

        printf("Stage 5 clear!\n");
        // here's your flag
        system("/bin/cat flag");
        return 0;
}

 음 일단 socket에 대한 공부가 필요해 보인다. 

 

AF_INET : 주소 체계를 사용할 때 나타내며 IPv4 인터넷 프로토콜을 의미함

 

INADDR_ANY : 자동으로 해당 컴퓨터에 존재하는 랜 카드 중에서 사용 가능한 랜 카드의 IP 주소를 선택하도록 하는 심볼이다.

 

SOCK_STREAM : 양방향 연결 지향형 소켓으로 데이터의 손실 없이 전달하는 것을 목표로 하며, 전송하는 순서가 보장된다. 이것을 man 페이지에서는 연속적이며 신뢰성 있는 양방향 바이트 기반의 소켓 타입으로 정의한다. 그냥 우리가 알던 TCP 방식이라고 생각하면 된다.

 

htons : 이것도 man htons로 긁어왔다. htons는 unsinged short (2 bytes) 정수를 host byte order에서 network byte order로 바꿔준다고 나와있는데 이건 또 무슨 소린가...

 자(Sleep). 우리는 보통 C언어에서 little-endian, 즉, 낮은 주소 혹은 단위가 앞에 오는 방식으로 데이터가 저장되어 있다고 배웠다. 이것을 컴퓨터 고유의 바이트 순서로 host byte order라고 하며, 네트워크 상에서는 인터넷 표준 네트워크 바이트 오더로 network byte order로 칭하고 big-endian을 사용한다. 이 둘이 데이터 저장 방식이 다르기 때문에 통신을 위해서는서로 변환해 주는 함수가 있어야 하는데 이것이 htons(host to network)와 ntohs(network to host)이다. 

 

bind : 서버가 자신의 주소를 알려주는 함수이다. bind는 man보다는 직접 설명하는 편이 나을 것 같다.

 

함수의 원형은

int bind(int socketfd, const struct sockaddr *localAdress, socketlen_t addressLength);

이다.

1. socket은 이전의 socket()호출에서 반환된 식별자로서 서버 소켓으로 사용할 소켓 번호를 입력하면 된다.

2. localAddress는 sockaddr의 포인터인데 TCP/IP에서는 요청을 기다리는 포트 번호를 포함하는 sockaddr_in 구조체를 가리키면 된다.

3. 주소 구조체의 길이. sizeof(struct sockaddr_in)을 입력하자.

 

listen : 주어진 소켓에 대해 내부 상태를 변경하려는 TCP 연결 요청들이 처리될 수 있도록 큐에 저장하게 한다.

함수의 원형은

int listen(int socketfd, int backlog);

이다.

1. socketfd는 사용할 소켓 식별자이다.

2. backlog는 연결 요청의 최댓값을 지정하는 것이다.(큐의 길이를 정함)

 

listen함수는 연결 요청을 받기만 하는 것이다. 

 

accept : 소켓을 얻는다.

 

함수의 원형은

int accept(int socektfd, struct sockaddr* clientAddress, socketlen_t * addressLength);

이다.

accept()는 소켓을 위한 큐에서 다음 연결 요청을 꺼낸다. 꺼내는 것에 성공하면 clientAddress가 가리키는 sockaddr 구조체를 해당 연결 요청한 클라이언트의 주소로 채운다. 그리고 그 소켓을 이용하여 통신을 하게되는 것이다.

addressLength는 clientAddress 구조체의 주소의 최대 크기를 지정할 수 있으며 실제로 주소를 위해 사용된 바이트 수를 갖게 된다.

 

recv : 이름에서부터 풍기는 향이 있지 않은가? receive. 소켓으로부터 데이터를 수신한다. 

 

함수의 원형은

ssize_t recv(int sockfd, const void* buf, size_t len, int flags);

이다.

1. sockfd : 접속된 소켓의 file descriptor, 즉, 식별자/번호를 나타낸다.

2. buf : 수신 데이터를 저장할 버퍼의 주소이다.

3. len : 수신할 데이터의 길이

4. flags : 함수의 호출이 어떤 일을 할지 나타냄. 이건 따로 찾아보길 바란다..

 

 

 

<아주 친절한 man htons>

BYTEORDER(3)              Linux Programmer's Manual              BYTEORDER(3)

NAME
       htonl,  htons,  ntohl, ntohs - convert values between host and network
       byte order

SYNOPSIS
       #include <arpa/inet.h>
       uint16_t htons(uint16_t hostshort);

DESCRIPTION

       The  htons()  function  converts  the unsigned short integer hostshort
       from host byte order to network byte order.

 

<아주 친절한 man recv>

RECV(2)                   Linux Programmer's Manual                   RECV(2)

NAME
       recv, recvfrom, recvmsg - receive a message from a socket

SYNOPSIS
       #include <sys/types.h>
       #include <sys/socket.h>
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);
       ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
       ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

DESCRIPTION
       The  recv(),  recvfrom(), and recvmsg() calls are used to receive mes‐
       sages from a socket.  They may be used to receive data on both connec‐
       tionless  and  connection-oriented sockets.  This page first describes
       common features of all three system calls, and then describes the dif‐
       ferences between the calls.

 

<아주 친절한 man listen>

LISTEN(2)                 Linux Programmer's Manual                 LISTEN(2)

NAME
       listen - listen for connections on a socket

SYNOPSIS

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int listen(int sockfd, int backlog);

DESCRIPTION

       listen()  marks  the socket referred to by sockfd as a passive socket,
       that is, as a socket that will be used to accept  incoming  connection
       requests using accept(2).

       The  sockfd  argument  is a file descriptor that refers to a socket of
       type SOCK_STREAM or SOCK_SEQPACKET.

       The backlog argument defines the maximum length to which the queue  of
       pending  connections  for  sockfd  may  grow.  If a connection request
       arrives when the queue is full, the client may receive an  error  with
       an  indication of ECONNREFUSED or, if the underlying protocol supports

       retransmission, the request may be ignored so that a  later  reattempt

       at connection succeeds.

<아주 친절한 man socket>

SOCKET(2)                 Linux Programmer's Manual                 SOCKET(2)

NAME

       socket - create an endpoint for communication

SYNOPSIS

       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int socket(int domain, int type, int protocol);

DESCRIPTION

       socket()  creates an endpoint for communication and returns a descrip‐
       tor.

       The domain argument specifies a communication domain; this selects the
       protocol  family which will be used for communication.  These families
       are defined  in  <sys/socket.h>.   The  currently  understood  formats

       include:

       Name                Purpose                          Man page

       AF_UNIX, AF_LOCAL   Local communication              unix(7)

       AF_INET             IPv4 Internet protocols          ip(7)

       AF_INET6            IPv6 Internet protocols          ipv6(7)

       AF_IPX              IPX - Novell protocols

       AF_NETLINK          Kernel user interface device     netlink(7)

       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)

       AF_AX25             Amateur radio AX.25 protocol

       AF_ATMPVC           Access to raw ATM PVCs

       AF_APPLETALK        AppleTalk                        ddp(7)

       AF_PACKET           Low level packet interface       packet(7)

       AF_ALG              Interface to kernel crypto API

       The  socket  has the indicated type, which specifies the communication
       semantics.  Currently defined types are:
       
       SOCK_STREAM     Provides  sequenced,  reliable,  two-way,  connection-
                       based  byte streams.  An out-of-band data transmission
                       mechanism may be supported.

       SOCK_DGRAM      Supports datagrams  (connectionless,  unreliable  mes‐
                       sages of a fixed maximum length).

       SOCK_SEQPACKET  Provides  a  sequenced,  reliable, two-way connection-
                       based data transmission path for  datagrams  of  fixed
                       maximum  length;  a  consumer  is  required to read an
                       entire packet with each input system call.

       SOCK_RAW        Provides raw network protocol access.

       SOCK_RDM        Provides a reliable datagram layer that does not guar‐
                       antee ordering.

       SOCK_PACKET     Obsolete  and  should not be used in new programs; see
                       packet(7).

       Some socket types may not be implemented by all protocol families.

       Since Linux 2.6.27, the type argument  serves  a  second  purpose:  in
       addition to specifying a socket type, it may include the bitwise OR of
       any of the following values, to modify the behavior of socket():
       
       SOCK_NONBLOCK   Set the O_NONBLOCK file status flag on  the  new  open
                       file  description.   Using this flag saves extra calls
                       to fcntl(2) to achieve the same result.

       SOCK_CLOEXEC    Set the close-on-exec (FD_CLOEXEC)  flag  on  the  new
                       file descriptor.  See the description of the O_CLOEXEC
                       flag in open(2) for reasons why this may be useful.
                       
       The protocol specifies a particular  protocol  to  be  used  with  the
       socket.   Normally only a single protocol exists to support a particu‐
       lar socket type within a given protocol family, in which case protocol
       can  be  specified  as 0.  However, it is possible that many protocols
       may exist, in which case a particular protocol must  be  specified  in
       this  manner.  The protocol number to use is specific to the “communi‐
       cation domain” in which communication is to  take  place;  see  proto‐
       cols(5).   See  getprotoent(3)  on how to map protocol name strings to
       protocol numbers.


 후 드디어 정리가 얼추 끝났다. 이것을 이용하여 socket 부분 코드를 분석하면 생각보다 아주 쉬운 문제이다.

1.  agrv['C'], 즉, argv[67]의 값을 포트 번호로 하여 소켓을 열어 서버로서 동작하게 하고, 이 소켓에서 4 bytes를 받아서 "\xde\xad\xbe\xef"가 나와야 한다.

 

정말로 끝이다!

 

다 종합해보자.


1. 인자의 개수가 100이어야 한다.

2. 인자의 65번째가 "\x00"이어야 한다

3. 인자의 66번째가 "\x20\0a\0d"여야 한다.


4. 0번 file descriptor를 이용하여 4바이트를 읽었을 때 "\x00\x0a\x00\xff"가 되어야 한다.

5. 2번 file descriptor를 이용하여 4바이트를 읽었을 때 "\x00\x0a\x02\xff"가 되어야 한다.


6. "\xde\xad\xbe\xef"의 환경 변수 값이 "\xca\xfe\xba\xbe"여야 한다.


7. "\x0a"라는 이름의 파일에서 4바이트짜리 한 개를 읽어서 그 값이 "\x00\x00\x00\x00"여야 한다.


8. agrv['C'], 즉, argv[67]의 값을 포트 번호로 하여 소켓을 열어 서버로서 동작하게 하고, 이 소켓에서 4 bytes를 받아서 "\xde\xad\xbe\xef"가 나와야 한다.


* Exploitation Code *

 

 자 이제 exploitation 을 해보자. 하지만 나는 이곳에서도 결국 답을 볼 수밖에 없었다. 모르는 걸 어떡해..

 

 차근 차근 읽어보면 이해가 될 것이나, 본인도 사용법을 모르기에 배워나가는 입장에서 코드 해석에 대한 코멘트를 달아보았다. 추후 나만의 코드로 다시 해봐야지!!

 

아 참. 마지막으로 flag는 권한이 없기 때문에 열지 못한다. 그래서 '심볼릭 링크'라는 것을 이용해서 간접적으로 접근하게 할 수 있다.ln -s [원본파일] [새로만들파일]
사용법은 다음과 같다. 

바로 요렇게 original 폴더를 copy 라는 링크를 통해 접근 가능하게 만들어 준다.

출처: https://chp747.tistory.com/268 [만두만두]

from pwn import *

# for debugging
context.log_level = 'debug'

# 0번은 실행파일
# 65번은 \x00
# 66번은 \x20\x0a\x0d
# 67번은 이 값을 포트 번호로 씀
argvs = ["" for i in range(100)]
argvs[0] = "/home/input2/input"
argvs[65] = "\x00"
argvs[66] = "\x20\x0a\x0d"
argvs[67] = "66666"

# stderr로 열어서 \x00\x0a\x02\xff를 씀
stderrfd = open('./stderr','w+')
stderrfd.write('\x00\x0a\x02\xff')
stderrfd.seek(0)

# \x0a 라는 이름의 파일을 열어서 \x00\x00\x00\x00을 씀
with open('./\x0a', 'w') as fd:
    fd.write('\x00\x00\x00\x00')

# 프로세스 만들 때 환경변수도 넣어줌
r = process(executable='/home/input2/input',argv=argvs, stderr=stderrfd,
	env={'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'})
r.recvuntil('Stage 1 clear!\n')

# 표준 입력으로 읽어서 \x00\x0a\0x00\xff이 나와야 함.
r.send("\x00\x0a\x00\xff")
r.recvuntil('Stage 2 clear!\n')
r.recvuntil('Stage 3 clear!\n')
r.recvuntil('Stage 4 clear!\n')

# 마지막으로 앞서 argv[67]에서 66666 포트 번호를 받기 때문에 이것으로 로컬 호스트 열고
# \xde\xad\xbe\xef 를 받음
p = remote('127.0.0.1', 66666)
p.send('\xde\xad\xbe\xef')
p.close()

r.interactive()


출처: https://chp747.tistory.com/268 [만두만두]

 

반응형

'System Security' 카테고리의 다른 글

pwnable.kr 9. mistake (Operator priority)  (0) 2020.02.12
pwnable.kr 8. leg (ARM Assembly)  (0) 2020.02.12
pwnable.kr 6. random  (0) 2020.02.05
pwnable.kr 5. passcode  (0) 2020.02.04
pwnable.kr 4. flag (UPX unpacking problem)  (0) 2020.02.02
Comments