[W05] Challenge 08 - Uninitialized Read

silver ·3일 전

크래프톤 정글

목록 보기
22/22

시나리오

  희소 행렬(sparse matrix)을 "행 포인터 표"로 표현한다. 
  희소 행렬(Sparse Matrix)은 대부분의 원소가 0인 행렬을 말한다.
  
  예시 : 
  0 0 0 0 5
  0 0 3 0 0
  0 0 0 0 0
  7 0 0 0 0
  0 0 0 9 0
 
  rows[i] 는 i 번째 행
  배열을 가리키며, 실제 데이터가 있는 행만 malloc 해서 연결한다.

문제 풀이

1)

맨 처음 실행해보니 main에서 체크섬을 계산하는 부분에서 터지는 것을 확인했다.

그래서 bt로 어디서부터 문제가 시작되는지 확인해보니 row_sum() 쪽으로 들어가고 있었고, 실제로 값을 읽는 부분에서 문제가 발생하고 있었다.

2)

row_sum()에서 rows를 직접 확인해봤다.

pwndbg> x/4gx 0x5555555592a0

0x5555555592a0: 0x00005555555593b0   0x0000000000000000
0x5555555592b0: 0x00005555555593d0   0xababababababab

rows는 int **이기 때문에 각각의 값은 각 행을 가리키는 포인터다.

그런데 값을 보니

0x55555555593b0
0x0
0x55555555593d0
0xababababababab

처럼 정상적인 주소처럼 보이는 값 사이에 0x0, 0xabab... 같은 이상한 값이 섞여 있었다.

특히 0xabababab...가 수상쩍어서 이 0xAB 패턴은 어디서 온 값 인지 알아보기 위해 rows를 조금 더 자세히 확인해봄.

3)

먼저 실제로 문제가 발생하는 행이 어디인지 확인하기 위해 값을 직접 출력해봤다.

pwndbg> print rows[1]
$4 = (int *) 0x0

rows[1]이 실제로 NULL이어서 rows[1][j]를 읽으려고 할 때 결국 *(rows[1] + j) 를 수행하게 되는데, rows[1] 자체가 0x0이므로 유효한 주소를 통해 값을 읽을 수 없는 상태였음.

이제

그럼 왜 rows[1]이 NULL이지?

를 따라가보기로 함.

4)

make_matrix()로 들어가서 rows가 어떻게 만들어지는지 확인해봄.

static int **make_matrix(void) {
    int **rows = malloc(ROWS * sizeof(int *));
    if (!rows) {
        perror("malloc");
        exit(1);
    }

    for (int i = 0; i < ROWS; i += 2) {
        int *r = malloc(COLS * sizeof(int));

        for (int j = 0; j < COLS; j++)
            r[j] = i * COLS + j;

        rows[i] = r;
    }

    return rows;
}

!!이상 포인트 발견!!

for (int i = 0; i < ROWS; i += 2)

i가 0, 2, 4, 6...으로 증가하기 때문에

rows[0]  → 할당됨
rows[1]  → 아무것도 하지 않음
rows[2]  → 할당됨
rows[3]  → 아무것도 하지 않음
...

이런 상태가 됨.

즉, rows 배열 자체는 32개의 포인터를 저장할 공간을 확보했지만 그 안의 모든 포인터에 값을 넣은 것은 아니었음.

malloc()은 메모리 공간만 확보할 뿐, 그 공간을 자동으로 0으로 초기화하지 않기 때문에 int **rows = malloc(ROWS * sizeof(int *)); 를 했다고 해서 rows[0] ~ rows[31]이 자동으로 NULL이 되는 것은 아님.

5)

그런데 여기서 또 의문이 생겼다.

그렇다면 초기화하지 않은 rows[1]이 왜 0x0이고, 다른 곳에는 왜 0xabababab...가 있는 거지?

그래서 dirty_heap()을 확인했다.

static void dirty_heap(void) {
    void *scratch = malloc(ROWS * sizeof(int *));

    if (scratch) {
        memset(scratch, 0xAB, ROWS * sizeof(int *));
        free(scratch);
    }
}

main()에서 dirty_heap()이 먼저 호출되고 있었다.

malloc()으로 메모리 확보
		↓
그 공간을 0xAB로 채움
		↓
free()로 반환
		↓
이후 make_matrix()가 같은 크기의 메모리를 `malloc()`
		↓
힙 할당기가 방금 반환된 공간을 다시 사용

하는 흐름이어서 새로 malloc()한 rows의 일부 영역에 이전에 남아 있던 0xAB 패턴이 보일 수 있었던 것임.

즉, 초기화하지 않은 메모리를 읽는다는 것은 반드시 0이 나온다는 뜻이 아니고, 그 공간에 이전에 남아 있던 값이 그대로 보일 수도 있음!!

6)

여기서 지난 Challenge 07과도 비교해봤다.

지난번에는 split_lines() 안의 지역 배열 parts의 주소를 함수 밖으로 넘긴 뒤, 함수가 끝난 다음 warm_stack()이 그 공간을 다시 사용하면서 문제가 발생했다.

이번에는 조금 다르다.

  • Challenge 07 — Use After Return
    - 한때 정상적으로 사용하던 데이터
    - 함수가 끝나면서 그 저장 공간의 수명이 끝남
    - 이후 다른 코드가 그 공간을 재사용
    - 무효해진 데이터를 다시 사용
     
  • Challenge 08 — Uninitialized Read
    - 애초에 값을 넣지 않은 공간
    - 그런데 값이 들어있다고 생각하고 읽음

즉 둘 다 메모리의 상태를 잘못 가정해서 발생하지만, 문제가 시작되는 지점은 다름.
이번에는 rows[1]에 값을 넣는 코드 자체가 없었던 것이 원인이엇삼.

7)

원인을 찾았으니 make_matrix()의 반복문을 수정했다.

//기존:

for (int i = 0; i < ROWS; i += 2)

//수정:

for (int i = 0; i < ROWS; i += 1)

이제 모든 인덱스를 순회하면서 각각의 행을 할당하게 된다.

rows[0] → malloc
rows[1] → malloc
rows[2] → malloc
rows[3] → malloc
...
rows[31] → malloc

다시 실행해보니 정상적으로 합이 계산됐다.

그런데 여기서 끝내면 안 됨!

8)

make_matrix()를 수정하면서 할당하는 행의 개수가 바뀌었기 때문임.....

main()의 free 부분을 확인해보니 기존에도 똑같이 i += 2로 되어 있었다.

for (int i = 0; i < ROWS; i += 2)
    free(rows[i]);

이 상태에서는 make_matrix()가 모든 행을 할당해도 짝수 번째 행만 free()하게 된다.

즉,

malloc: 0 1 2 3 4 5 ... 31
free:   0   2   4   6 ... 30

이 되어 홀수 번째 행의 메모리가 해제되지 않는다.

따라서 free도 동일하게 수정했다.

for (int i = 0; i < ROWS; i += 1)
    free(rows[i]);

할당하는 쪽의 범위와 해제하는 쪽의 범위가 서로 맞아야 한다는 것을 다시 확인했다.

이번 문제에서 가져갈 것

  • malloc()은 메모리를 할당할 뿐 초기화하지 않는다.
    - 0으로 초기화된다는 보장이 없다.
    - 초기화가 필요하다면 calloc()이나 명시적인 초기화가 필요하다.
  • 초기화되지 않은 값은 어떤 값일지 알 수 없다.
    - 이번처럼 0xAB 같은 이전 데이터가 남아 있을 수도 있다.
    - 따라서 NULL일 것이다라고 가정하면 안 된다.
  • rows[i]처럼 포인터 배열을 사용할 때는 배열 공간을 할당한 것과 각 원소에 유효한 값을 넣는 것은 별개의 작업이다.
  • i += 2처럼 일부 원소만 처리하는 반복문을 발견하면, 나머지 원소는 정말 의도적으로 비워둔 것인지 확인해야 한다.
  • 한쪽의 반복 범위를 수정했다면 그와 짝을 이루는 코드도 확인해야 한다.
    - malloc하는 범위
    - 초기화하는 범위
    - free하는 범위
    - 이 셋이 서로 맞아야 한다.
  • 마지막으로 GDB에서 "이 값이 이상하다" → "왜 이런 값이 들어갔지?" → "어디서 이 값이 만들어졌지?" 순서로 거슬러 올라가면, 단순히 크래시 지점을 찾는 것에서 끝나지 않고 오류의 원인까지 추적할 수 있다.

0개의 댓글