//argv로 주석이 되어 있는 부분의 코드를 보면 인자의 크기가 100이 아니면, 인자의 'A'인덱스 즉 65번째 인덱스가 "\x00"가 아니고, 인자의 'B' 인덱스 즉 66번째 인덱스가 "\x20\x0a\x0d"가 아니면 프로그램을 종료하고, 위 조건들을 만족시키는 경우 Stage 1 clear을 출력한다.
//stdio로 주석이 되어 있는 부분의 코드를 보면 read 함수로 buf를 읽어오는데, 0(stdin)이 "\x00\x0a\x00\xff"이 아닐 경우, 2(stderr)이 "\x00\x0a\x02\xff"가 아닐 경우 조건을 프로그램을 종료하고, 조건들을 만족시킬 경우 Stage 2 clear을 출력한다.
//env로 주석이 되어 있는 부분의 코드를 보면 getenv()함수로 환경변수 "\xde\xad\xbe\xef"의 값이 "\xca\xfe\xba\xbe"가 아닐 경우 프로그램을 종료하고, 조건을 만족하면 Stage 3 clear을 출력한다.
//file로 주석이 되어 있는 부분의 코드를 보면 fopen()함수로 "\x0a"라는 이름의 파일을 열고 파일을 읽어 파일의 내용이 "\x00\x00\x00\x00"가 아니면 프로그램을 종료하고, 조건을 만족하면 Stage 4 clear을 출력한다.
//network로 주석이 되어 있는 부분의 코드를 보면 sockaddr_in 구조체를 사용하고 있고, 소켓 생성에 실패(socket 함수 -1 반환)하면 socket error을 출력하고 프로그램을 종료한다. saddr의 포트 정보에 인자의 'C' 인덱스 즉 67번째 인덱스의 값을 넣고, bind 함수로 소켓 통신할 준비를 해준다. 이 과정에서 에러가 발생할 경우 bind error use another port를 출력하고 프로그램을 종료한다. accept()함수로 소켓을 연결하고, 에러가 발생할 경우(return -1) accept error tell admin을 출력하고 프로그램을 종료한다. 이후 소켓에서 4바이트를 받아와서 받아온 값이 "\xde\xad\xbe\xef"가 맞다면 Stage 5 clear을 출력한다.
위 5가지의 조건을 모두 만족시키면 플래그를 읽을 수 있다.
문제를 해결하기 위해 pwntools를 이용하여 파이썬 코드로 작성하였다.
첫번째 조건을 만족시키기 위해 인자값을 조건에서 주어진 대로 설정하고, 인자를 전달하려면 process 함수의 argv 옵션을 이용하여 전달할 수 있다.
두 번째 조건을 만족시키기 위해 stdin으로는 "\x00\x0a\x00\xff"를 전달하였고, stderr일 경우 전달하는 파일을 만들어 "\x00\x0a\x02\xff"을 저장하여 process함수의 stderr옵션을 이용하여 전달하였다.
세 번째 조건을 만족시키기 위해 process함수의 env 옵션을 이용하여 환경변수를 설정하였다.
네 번째 조건을 만족시키기 위해 조건에 주어진 이름의 파일을 만들고, 파일 내용도 조건에 주어진 대로 지정해 주었다.
다섯 번째 조건을 만족시키기 위해 인자의 67번째('C')인덱스에 포트를 지정하고 지정한 포트로 nc 연결을 하여 조건에서 주어진 값을 전달하였다.
따라서 코드로 작성하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import\\\*
argvs = [str(i) for i inrange(100)]
argvs[65] ='\x00'
argvs[66] ='\x20\x0a\x0d'
argvs[67] ='3000'
p = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr'), env={'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'})
비트 필도로 사용할 멤버는 익명 구조체로 감싸주고, 비트 필드의 값을 한번에 접근할 수 있는 unsigned short형 멤버(e)를 선언하여 익명 공용체로 감싸준다. 각 비트 필드에 값을 할당하고, e를 출력하면 64020이 나온다. 각 비트 필드에 할당한 비트들을 차례로 연결하면 1111 1010000 10 100이 되어 10진수로 64020이 된다.
56.3 퀴즈
정답은 d 이다.
정답은 4이다.
정답은 e 이다.
56.4 연습문제 : 구조체로 플래그 비트 필드 만들기
비트 수를 제한해야 하므로 정답은 다음 코드와 같다.
unsigned int a : 2;
unsigned int b : 1;
unsigned int c : 6;
56.5 연습문제 : 구조체와 공용체로 플래그 비트 필드 만들기
32936을 2진수로 표현하면 1000 0000 1010 1000이다. 할당할 값들을 2진수로 표현하면 각각 1000, 10, 10, 1000 0000 이므로 비트를 맞춰주면 다음과 같다.
unsigned short a : 4;
unsigned short b : 2;
unsigned short c : 2;
unsigned short d : 8;
56.6 심사문제 : 구조체로 플래그 비트 필드 만들기
정답은 다음 코드와 같다.
struct Flags{
unsigned int a : 4;
unsigned int b : 7;
unsigned int c : 3;
};
56.7 심사문제 : 구조체와 공용체로 플래그 비트 필드 만들기
57412를 2진수로 변환하면 1110 0000 0100 0100이므로 비트수를 맞춰주면 다음과 같다.
unsigned int a : 3;
unsigned int b : 4;
unsigned int c : 7;
unsigned int d : 2;
Unit 57. 열거형 사용하기
열거형은 정수형 상수에 이름을 붙여서 코드를 이해하기 쉽도록 해준다.
57.1 열거형 정의하기
열거형은 eum키워드로 정의하고, enum 열거형이름 변수이름; 의 형태로 정의하여 사용한다.
for(INTERFACE_TYPE i = Internal; i < MaximumInterfaceType; i++)
Unit 58. 자료형 변환하기
C에서는 자료형이 같거나 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 저장하면 자동으로 형 변환이 된다. 그러나 자료형이 다르면서 크기가 작은쪽, 표현 범위가 좁은 쪽으로 저장하면 컴파일 경고가 발생할 수 있다. 자료형의 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 자동 형 변환 되는 것을 형 확장 이라 하며 암시적 형 변환이라 한다. 반대로 자료형 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 변환되는것을 형 축소라 한다. 형 축소에서 컴파일 경고가 나오지 않도록 만드는 것을 형 변환 이라 한다. 프로그래머가 강제적으로 자료현을 변환한다고 하여 명시적 형 변환이라 부르기도 한다. 형 확장은 값의 손실이 없기 때문에 컴파일러가 알아서 처리할 수 있지만 형 축소는 값의 손실이 발생하여 컴파일러가 알아서 처리할 수 없기 때문에 경고가 발생한다. 형 변환은 컴파일러에게 자료형을 변환한다는 의도를 명확하게 알려주는 것이다.
58.1 기본 자료형 변환하기
자료형을 지정하여 변환하는 것을 명시적 자료 변환이라고 하며 변수나 값 앞에 변환할 자료형을 붙인 뒤 괄호로 묶어준다.
numPtr에 메모리를 할당하고 역참조 하여 0x12345678을 저장하였다. cPtr에 int 포인터를 char 포인터로 저장하여 메모리 주소를 저장하면 cPtr은 char 포인터이므로 1바이트만 값을 가져오게 된다. 메모리 공간에는 리틀 엔디언 형식으로 값이 저장되므로 출력값은 0x78이 된다.
위와 반대로 크기가 작은 메모리 공간을 할당한 뒤 큰 자료형의 포인터로 역참조하면 의도치 않은 값을 가져올 수도 있다.
크기가 작은 메모리를 할당한 뒤 큰 자료형의 포인터로 역참조하면 컴파일러에 따라 옆의 메모리 공간을 침범하여 값을 가져올 수 도 있다. 위 코드는 2바이트 만큼 메모리를 할당했으므로 0x1234만 저장되어 있지만 4바이트 크기만큼 값을 가져오면 2바이트 크기를 벗어나서 malloc함수로 할당하지 않은 공간까지 함께 가져올 수도 있다. 할당하지 않은 공간에는 쓰레기값이 들어있다.
포인터를 다른 자료형으로 변환하면서 역참조 하려면 다음과 같이 괄호 앞에 역참조 연산자를 붙여주면 된다.
구조체 포인터 d1을 선언하고 구조체 크기만큼 메모리를 할당하여 각 멤버에 'a'와 10을 저장하였다. 이후 void ptr에 d1을 할당하였다. void포인터이기 때문에 Data 구조체의 형태를 모르는 상태 이므로 멤버에 바로 접근할 수 없기 때문에 위와 같이 ptr을 Data 구조체로 변환한 뒤 멤버에 접근해야 한다. ptr을 구조체 포인터로 변환한 뒤 멤버에 접근할 때는 자료형 변환과 포인터 전체를 다시 한 번 괄호로 묶어야 -> 연산자를 사용하여 구조체 멤버에 접근할 수 있다.
58.5 퀴즈
정답은 d 이다.
정답은 e이다.
정답은 c이다.
58.6 연습문제 : 삼각형의 넓이 구하기
base와 height가 모두 정수이므로 하나를 실수로 변환해야 한다. 정답은 다음 코드와 같다.
area = (float)base * height / 2;
58.7 포인터 변환하기
출력값이 2바이트 이므로 short 포인터로 형변환 해야 한다.
numPtr2 = (short *)nupPtr1;
58.8 연습문제 : void 포인터 변환하기
numPtr1의 크기가 uint64_t이므로 uint64_t 포인터로 변환한 뒤 역참조하여야 한다.
*(uint64_t *)ptr
58.9 연습문제 : 구조체 포인터 변환하기
정답은 다음과 같다.
((struct Person *)ptr)->name, ((struct Person *)ptr)->age
58.10 심사문제 : 소수점 이하 버리기
num1이 실수이고, 출력하는 것은 num2이고, 정수이므로 num2에 num1을 넣으면 된다.
포인터 변수에는 메모리 주소가 들어있다. 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가, 감소한다. 포인터 연산을 통해 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다닐 수 있다. 메모리 주소가 커지는 상황을 순방향으로 이동, 메모리 주소가 작아지는 상황을 역방향으로 이동이라고 한다.
59.1 포인터 연산으로 메모리 주소 조작하기
포인터 변수에 +, - 사용하여 값을 더하거나 빼고, ++, -- 연산자로 값을 증가, 감소시킬 수 있다. *, / 연산자와 실수값은 사용할 수 없다.
numPtrB는 numPtrA에 1을 더해 4바이트 만큼 순방향 이동했고, numPtrC는 numPtrA에 2를 더해서 8바이트만큼 순방향으로 이동했다. numPtrB, numPtrC도 일반 포인터이므로 역참조 연산이 가능하고, 두 값은 결국 numArr[1], numArr[2]와 같다.
다음과 같이 포인터 연산을 한 부분을 괄호로 묶어 맨 앞에 역참조 연산자를 붙여 포인터 연산과 역참조 연산을 동시에 사용할 수도 있다.
구조체 배열 d를 선언한 뒤 첫번째 요소의 메모리 주소를 ptr에 저장하였다. (ptr+1)->num1 과 같이 포인터 연산을 한뒤 괄호로 묶은 후 화살표 련산자를 사용하여 멤버에 접근할 수 있다. 이는 구조체 배열의 인덱스로 접근하는 d[1].num1과 같다. 구조체의 크기는 4바이트짜리 int형 멤버가 두 개 있으므로 8바이트이다. 따라서 포인터 연산을 하게 되면 8바이트씩 메모리 주소에서 더하거나 뺀다.
다음은 동적 메모리를 할당하여 포인터 연산을 하는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
struct Data {
int num1;
int num2;
};
int main()
{
void*ptr =malloc(sizeof(struct Data) *3);
struct Data d[3];
((struct Data *)ptr)->num1 =10;
((struct Data *)ptr)->num2 =20;
((struct Data *)ptr +1)->num1 =30;
((struct Data *)ptr +1)->num2 =40;
((struct Data *)ptr +2)->num1 =50;
((struct Data *)ptr +2)->num2 =60;
memcpy(d, ptr, sizeof(struct Data) *3);
printf("%d %d\n", d[1].num1, d[1].num2);
printf("%d %d\n", ((struct Data *)ptr +2)->num1, ((struct Data *)ptr +2)->num2);
모니터에 하나의 문자를 출력할 때 일반적으로 사용하는 함수는 putchar() 과 fputc()가 있다.
#include <stdio.h>
int putchar(int c);
int fputc(int c, FILE * stream);
// 함수 호출 성공시 쓰여진 문자 정보, 실패시 EOF반환
putchar() 함수는 인자로 전달된 문자정보를 stdout 표준 출력 스트림으로 전송하여 모니터로 출력하는 함수이다.
fputc()함수는 문자를 전송한다는 측면은 putchar()함수와 동일하지만 문자를 전송할 스트림을 지정할 수 있어 파일을 대상으로도 데이터를 저장할 수 있다. fputc() 함수의 두 번째 매개변수 stream은 문자를 출력할 스트림을 지정하는데 사용된다.
문자 입력 함수
키보드로 부터 하나의 문자을 입력 받을 때 일반적으로 사용하는 함수는 getchar() 과 fgetc()가 있다.
#include <stdio.h>
int getchar(void)
int fgetc(FILE * stream);
// 파일의 끝에 도달하거나 함수 호출 실패 시 EOF반환
getchar() 함수는 stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력받아 반환하는 함수이다.
fgetc() 함수는 문자를 입력받을 스트림을 지정할 수 있다.
#include <stdio.h>
int main(void)
{
int ch1, ch2;
ch1 = getchar(); // 문자 입력
ch2 = fgetc(stdin); // 엔터키 입력
putchar(ch1); // 문자 출력
fputc(ch2, stdout); // 엔터키 출력
return 0;
}
위 코드에서 문자를 int 형 변수로 선언한 이유는 getchar() 함수와 fgetc()함수의 반환형이 int 이기 때문이다.
문자 입출력에서의 EOF
EOF는 End Of File의 약자로 파일의 끝을 표현하기위해 정의된 상수이다. 따라서 EOF가 반환되면 파일의 끝에 도달하여 더 이상 읽을 내용이 없음을 의미한다. 키보드를 대상으로 하느 fgetc() 와 getchar() 함수는 다음의 경우중 하나가 만족되었을 때 EOF를 반환한다.
함수 호출 실패
Windows에서 CTRL + Z, Linux에서 CTRL + D 키가 입력되었을 경우
#include <stdio.h>
int main(void)
{
int ch;
while (1)
{
ch = getchar();
if(ch == EOF)
break;
putchar(ch);
}
return 0;
}
위 코드를 실행하면 문자 입출력이 반복되고 EOF를 반환하는 CTRL + D 키를 입력하면 프로그램이 종료된다.
위 함수들의 반환형이 int 이고, int형 변수에 문자를 담는 이유는 EOF 는 -1로 정의된 상수인데, 반환형이 char 이라면 처리하는 컴파일러에 따라 char을 unsigned char로 처리하는 경우가 있어 -1을 양의 정수로 형변환 하는 경우가 발생 할 수 있다. 그래서 어떤 컴파일러라도 -1 을 유지하기 위해 반환형을 int 로 정의해둔것이고, 반환형이 int 이기 때문에 int 형 변수에 값을 저장해야 한다.
프로그램 실행시 main함수로 인자를 전달할 수 있고, main 함수도 인자를 전달받을 수 있도록 한 것이다.
#include <stdio.h>
int main(int argc, char * argv[])
{
int i = 0;
printf("전달된 문자열의 수 : %d\n", argc);
for(i = 0; i < argc; i++)
printf("%d 번째 문자열 : %s\n", i+1, argv[i]);
return 0;
}
/*
input :
# ./argcargv I love you
output:
전달된 문자열의 수 : 4
1 번째 문자열 : ./argcargv
2 번째 문자열 : I
3 번째 문자열 : love
4 번째 문자열 : you
*/
위 코드와 같이 main함수를 구성하면 프로그램을 실행할 때 인자를 전달 할 수 있다. argv는 char형 더블 포인터 변수이다.
인자 전달 과정에서 공백은 문자열을 나누는 기준이 된다. 인자로 전달된 문자열들이 배열에 묶여 main 함수의 두 번째 인자로 전달이되고, 첫 번째 인자는 문자열의 수가 전달이 된다.
#include <stdio.h>
int main(int argc, char * argv[])
{
int i = 0;
printf("전달된 문자열의 수 : %d\n", argc);
while(argv[i] != NULL)
{
printf("%d 번째 문자열 : %s\n", i+1, argv[i]);
i++;
}
return 0;
}
/*
input :
# ./argvNull "I Love You"
output :
전달된 문자열의 수 : 2
1 번째 문자열 : ./argvNull
2 번째 문자열 : I Love You
*/
위 코드를 통해 전달받은 배열의 마지막에 NULL이 삽입됨을 확인할 수 있다. 또한 큰 따옴표로 묶으면 공백을 포함하는 문자열을 인자로 전달할 수 있다.
위 코드와 같이 arr2d는 첫 번째 요소를 가리키며, 배열 전체를 의미한다. arr2d[0]은 첫 번째 요소를 가리키며 1행만을 의미한다. 그래서 sizeof 연산 결과 arr2d는 배열 전체의 크기인 36이 출력되었고, arr2d[0]은 첫 번째 행의 크기인 12가 출력되었다.
1차원 배열의 경우 다음 코드에서 iarr은 int형 포인터 이기 때문에 iarr+sizeof(int)의 계산 결과가 출력되고, darr은 double형 포인터 이므로 darr+sizeof(double)이 출력된다.
int iarr[3];
double darr[7];
printf("%p\n", iarr+1);
printf("%p\n", darr+1);
첫 번째 배열의 경우 1씩 증가시켰을 때 8이 증가하였고, 두 번째 배열의 경우 1을 증가시키면 12가 증가하였다. 2차원 배열 이름을 대상으로 증가 및 감소 연산을 할 경우 각 행의 첫번째 요소를 가리킨다. arr1은 1행의 첫번째 요소를 가리키며, arr1+1은 두번째 행의 첫번째 요소를 가리킨다. arr1은 가로가 한 행에 두 칸이기 때문에 8이 증가한 것이다. 2차원 배열의 포인터형은 가로의 길이에 따라서 달라진다.
2차원 배열은 포인트 연산시 sizeof(type) x 가로길이 만큼 값이 증가한다.
위와 같은 유형의 포인트 변수 선언은 int형이며, sizeof(int)x4의 크기 단위로 증가 및 감소하는 포인터 변수 선언은 다음과 같다.
int (*ptr)[4];
#include <stdio.h>
int main(void)
{
int arr1[2][2] = {
{1,2}, {3,4}
};
int arr2[3][2] = {
{1,2}, {3,4}, {5,6}
};
int arr3[4][2] = {
{1,2}, {3,4}, {5,6}, {7,8}
};
int (*ptr)[2];
int i;
ptr = arr1;
printf("** Show 2,2 arr1 **\n");
for(i = 0; i < 2; i++)
printf("%d %d\n", ptr[i][0], ptr[i][1]);
ptr = arr2;
printf("** Show 3,2 arr2 **\n");
for(i = 0; i < 3; i++)
printf("%d %d\n", ptr[i][0], ptr[i][1]);
ptr = arr3;
printf("** Show 4,2 arr3 **\n");
for(i = 0; i < 4; i++)
printf("%d %d\n", ptr[i][0], ptr[i][1]);
}
/* output :
@"1 2\r\n"
@"3 4\r\n"
@"** Show 3,2 arr2 **\r\n"
@"1 2\r\n"
@"3 4\r\n"
@"5 6\r\n"
@"** Show 4,2 arr3 **\r\n"
@"1 2\r\n"
@"3 4\r\n"
@"5 6\r\n"
@"7 8\r\n"
*/
위 코드와 같이 포인터 변수를 선언하여 2차원 배열에 접근할 수 있다.
2차원 배열의 주소 값을 인자로 전달 받는 함수를 정의할 때 매개 변수는 다음과 같이 선언할 수 있다.