ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프로그램 버그 예시
    보안개론 2024. 5. 19. 01:16

    - 루프

    void main() {
    	float = 0.1;
        
    	while( x != 1.1 ) {
    		x = x + 0.1;
     		printf("x = %f\n", x);
    		if(x > 3) break;
    	}
    }

    컴퓨터 특성 상 실수 연산은 완전히 정확하지 않음.

    저 루프가 운이 좋으면 끝나겠지만 아니면 무한 루프가 될 것

     

    - 문법 문제

    int k = 1;
    int val = 0;
    
    while( k = 10 ) {
    	val++;
    	k++;
    }
    printf("k = %d, val = %d\n", k, val);

    k == 10 으로 해야 함

     

    - 메모리 에러

    char buffer[5] = "Great";
    
    int id_seq[3];
    
    id_seq[-1] = 123;
    id_seq[1] = 123;
    id_seq[2] = 123;
    id_seq[3] = 123;
    id_seq[4] = 123;
    
    printf("id_seq[4] = %d\n", id_seq[4]);

    buffer -> G r e a t \0 로 총 6자라 버퍼가 넘침

    id_seq[-1] : id_seq 기준으로 4바이트 전 

    id_seq[4] : id_seq[3] 주소 기준으로 4바이트 후

    Segmentation fault가 날 수도 있고 안 날수도 있음

     

    ex) id_seq[-1]에는 문제가 안 생겨도 id_seq[4]만 문제가 생기는 경우

    스택 보호 기법 때문

    스택은 위에서 아래로 자라는 형태 -> 한쪽 방향만 검사

    -fno-stack-protector 옵션을 컴파일할 때 쓰면 스택 보호 해제 가능

     

    - Off-by-one error

    int i;
    int buffer[5];
    
    for( i = 0; i <= 5; i++ )
    	cin >> buffer[i];
    int array[] = new int[5];
    
    for(int i = 0; i <= 5; i++)
    	System.out.println( array[i] );

    < 대신 <= 을 쓴 것으로 인해 사용할 메모리 범위를 벗어나게 됨

    Off-by-one : 하나 초과했다는 뜻

     

    - 데이터 사이즈

    // 64 비트 기준
    printf("sizeof(char) = %lu\n", sizeof(char));	// 1
    printf("sizeof(char) = %lu\n", sizeof(short));	// 2
    printf("sizeof(char) = %lu\n", sizeof(int));	// 4
    printf("sizeof(char) = %lu\n", sizeof(long));	// 8
    
    printf("sizeof(char) = %lu\n", sizeof(char *));	// 8
    printf("sizeof(char) = %lu\n", sizeof(short *));// 8
    printf("sizeof(char) = %lu\n", sizeof(int *));	// 8
    printf("sizeof(char) = %lu\n", sizeof(long *));	// 8

    %lu : unsigned long 의 형식 지정자

    long은 64비트 프로그램에선 8바이트, 32비트 프로그램에선 4바이트

     

    64비트 / 32비트

    = 워드의 크기(한 사이클에 처리 가능한 데이터의 크기)

    = 레지스터의 크기

    = 어드레스 버스의 크기

    실제로 64비트에서 주소를 64비트로 쓰지는 않고 48비트

    64비트 너무 커서 그냥 48비트 씀

    언젠가 바뀔수도

     

    printf("CHAR_MIN = %10d, %x, %10u\n", CHAR_MIN, CHAR_MIN, CHAR_MIN);
    // -128, ffffff80, 4294967168
    
    printf("CHAR_MAX = %10d, %x, %10u\n", CHAR_MAX, CHAR_MAX, CHAR_MAX);
    // 127, 7f, 127
    
    printf("UCHAR_MAX = %10d, %x, %10u\n", UCHAR_MAX, UCHAR_MAX, UCHAR_MAX);
    // 255, ff, 255

    %d : 4바이트 크기 표시 가능

    %x : 헥사 데시멀, 바이너리를 보여줌, 4바이트

    %u : 언사인드 데시멀, 4바이트

     

    캐릭터는 signed, 1바이트 크기

    맞지 않는 형식 지정자를 사용하면 문제가 생길 수 있음

     

    ** CHAR_MIN의 이진수가 ffffff80 으로 보이는 이유

    사이즈 안맞으면 최상위 비트를 그대로 복제함 -> 16진수 80 -> 2진수 1000 0000

    -> 복제 시 111111......10000000 이 됨

     

    이걸 Unsigned로 받아서 읽는다 -> 4294967168로 읽어지는 문제가 발생

     

    이걸 예방하기 위해선, hhd, hhx, hhu를 사용해야 함

    h는 하프를 뜻하는데, 4바이트의 반의 반이면 1바이트

    char의 크기가 1바이트이니 크기를 잘 맞춰주어야 버그가 생기지 않음

     

    printf("SHRT_MIN = %10d, %x, %10u\n", SHRT_MIN, SHRT_MIN, SHRT_MIN);
    // -32768, ffff8000, 4294934528
    
    printf("SHRT_MAX = %10d, %x, %10u\n", SHRT_MAX, SHRT_MAX, SHRT_MAX);
    // 32767, 7fff, 32767
    
    printf("USHRT_MAX = %10d, %x, %10u\n", USHRT_MAX, USHRT_MAX, USHRT_MAX);
    // 65535, ffff, 65535
    
    printf("INT_MIN = %10d, %x, %10u\n", INT_MIN, INT_MIN, INT_MIN);
    // -2147483648, 80000000, 2147483648
    
    printf("INT_MAX = %10d, %x, %10u\n", INT_MAX, INT_MAX, INT_MAX);
    //  2147483647, 7fffffff, 2147483647
    
    printf("UINT_MAX = %10d, %x, %10u\n", UINT_MAX, UINT_MAX, UINT_MAX);
    // -1, ffffffff, 4294967295
    
    printf("LLONG_MIN = %20lld, %llx, %20llu\n", LLONG_MIN, LLONG_MIN, LLONG_MIN);
    // -9223372036854775808, 8000000000000000, 9223372036854775808
    
    printf("LLONG_MAX = %20lld, %llx, %20llu\n", LLONG_MAX, LLONG_MAX, LLONG_MAX);
    // 9223372036854775807, 7fffffffffffffff, 9223372036854775807
    
    printf("ULLONG_MAX = %20lld, %llx, %20llu\n", ULLONG_MAX, ULLONG_MAX, ULLONG_MAX);
    // -1, ffffffffffffffff, 18446744073709551615

    int는 애초에 4바이트니까 %d, %x, %u에 바이트가 확장되지 않고 잘 표시됨

    단, signed를 unsigned로 출력하려는 시도나 반대 경우는 당연히 이상한 결과를 냄


    file 명령어

    리눅스(우분투)
    Mac OS

    바이너리 파일의 포맷과 몇 비트 단위인지를 알 수 있음. 바이너리의 포맷을 알면 역공학이 가능

    리눅스는 ELF, 윈도우는 PE(Portable Executable format), 안드로이드는 DEX (Dalvik EXcutable format),

    ios나 mac OS는 Mach-O

     

    pie: position independent code

    어느 메모리 주소에서도 실행 가능하다는 뜻

     

    gcc -m32 test_32.c -o test_32

    파일이 32비트 형식으로 나옴


    오버플로우, 언더플로우

    pirntf("SHRT_MIN-1 (%10d, %x, %10u)\n", SHRT_MIN-1, SHRT_MIN-1, SHRT_MIN-1);
    // -32769, ffff7fff, 4294934527
    
    pirntf("SHRT_MAX+1 (%10d, %x, %10u)\n", SHRT_MAX+1, SHRT_MAX+1, SHRT_MAX+1);
    // 32768, 8000, 32768
    
    pirntf("USHRT_MAX+1 (%10d, %x, %10u)\n", USHRT_MAX+1, USHRT_MAX+1, USHRT_MAX+1);
    // 65536, 10000, 65536

    최소값에서 -1을 하거나, 최대값에서 +1을 하면 언더플로우 / 오버플로우가 발생해야 함

    근데 발생 안했음 -> 형식 지정자가 4바이트라 발생하지 않았음

     

    pirntf("INT_MIN-1 (%10d, %x, %10u)\n", INT_MIN-1, INT_MIN-1, INT_MIN-1);
    // 2147483647, 7fffffff, 2147483647
    
    pirntf("INT_MAX+1 (%10d, %x, %10u)\n", INT_MAX+1, INT_MAX+1, INT_MAX+1);
    // -2147483648, 80000000, 2147483648
    
    pirntf("UINT_MAX+1 (%10d, %x, %10u)\n", UINT_MAX+1, UINT_MAX+1, UINT_MAX+1);
    // 0, 0, 0

    형식 지정자와 크기가 맞으면 오버플로우와 언더플로우가 발생하는게 보임

    Unsigned int의 경우 최대치에서 +1 하면 0이 되어버림


    IO 관련 문제

    #include <stdio.h>
    #include <string.h>
    
    void main(int argc, char *argv[]) {
       char buf[16] = "Hello";
       
       // 문제 1
       printf("buf = %s, %x, %x, %x\n", buf);
       printf("addr of main()\n", main);
       /*
       *	buf = 1000000, 565e91f4, f7ee2540
       *	addr od main() = 565e91dd
       */
       
       // 문제 2
       strcpy(buf, argv[1]);
       
       // 문제 3
       printf("buf = %s, %x, %x, %x\n", buf);
       
       // 문제 4
       printf("Enter a string: ");
       scanf("%s", buf);
       
       printf("Input string is %s\n", buf);
       printf("End\n");
    }

    문제1

    형식 지정자의 수가 인자의 수와 다름

    이러면 pritnf는 버퍼 다음 위치에 있는 스택에 있는 인자를 꺼내옴

     

    스택에 저장되는 값

    인자->리턴 어드레스->saved ebp->지역변수

    ** pc는 텍스트 세그먼트의 오프셋을 가리키고 ebp는 스택의 오프셋을 가리킴

    ** 스택의 탑을 가리키는 건 esp

    ** pc는 인텔에서는 eip.. pc는 범용적인 용어

     

    스택에서 계속 꺼내다 보면 리턴 어드레스의 주소를 알 수 있음

     

    문제2

    argv[1] 값의 길이가 buf 길이를 넘어버리면 오버플로우

     

    문제3

    printf의 인자 개수가 안맞아서 스택에서 값을 꺼내오게 됨

     

    문제4

    입력하는 길이가 buf의 크기를 넘어가면 오버플로우

    너무 길게 넣으면 리턴 어드레스까지 침범할 수도 있음

    혹은, 입력을 "Hello World %p %p %p %p %p %p" 같이 주면

    printf에서 argv[1]의 내용을 출력하게 될 경우 형식 지정자의 수가 안맞아 스택에서 꺼내오게 됨

     

    #include <stdio.h>
    
    void main(int argc, char *argv[]) {
        char buf[16] = "Hello";
        
        printf("argv[1] = %s\n", argv[1]);
        
        // 취약
        printf(argv[1]);
        printf("\n");
    }

    인자 없이 그냥 출력을 하라고 하는 경우

    만약 argv[1]가 "%p %p %p %p %p %p" 이면 스택에서 값을 꺼내오게 됨

     

     

    #include <stdio.h>
    
    void main(int argc, char *argv[]) {
        char buf[100];
        
        snprintf(buf, sizeof(buf), "%s", argv[1]);
        
        // 취약
        snprintf(buf, sizeof(buf), argv[1]);
        printf("buf = %s\n", buf);
    }

    sprintf()

    출력을 첫 인자로 보냄

    argv[1]가 "Hello %s %s %s %s %s %s" 이런 식이면?

    %s는 널문자를 만날 때까지 출력함

    널 문자가 없으면 굉장히 길게 출력되다가, 너무 길게가서 커널을 건드릴 수 있음

    -> 도스 공격 유발 가능


    Dangling Pointer (= Null pointer dereference)

    int *ptr = NULL;
    printf("ptr: %p\n", ptr);
    
    int *p = 0;
    *p = 1;

    가리키는게 없는데 값을 쓸 수는 없음

     

    int length;
    char *buf;
    
    scanf("%d", &length);
    buff = (char *)malloc(length + 1);
    strcpy(buff, "Hello");

    혹은, 동적할당을 받아도 저게 NULL이 아니라고 확신할 수는 없음

    자리가 없으면 할당을 못해줌

Designed by Tistory.