[Write up] LG security hackathon

2024. 11. 21. 17:13security/CTF

crackme

 

2024년 11월 16일에 참가한 LG U+ Security hackathon에서 출제된 crackme 문제에 대한 write up이다.

 

prob
2.18MB

 

해당 바이너리를 분석해서 플래그를 얻어야하는 리버싱 문제였다.

우선 HxD로 확인했을 때 ELF 파일임을 확인할 수 있었다. WSL을 통해 실행시켜보니 Enter the password: 라는 메시지와 함께 input을 받도록 되어있었다. 아무 값이나 입력해보니 Wrong! 이라는 메시지와 함께 종료되었다. 정적 분석을 위해 IDA를 사용했다. 문제 설명에서도 나와있었는데, 디컴파일한 코드의 문법을 찾아보니 Go 언어로 작성된 프로그램임을 알 수 있었다. main 함수를 살펴보니 매우 많은 배열이 선언되어있고, 입력한 문자열에 대해 검사하는 부분의 코드가 있음을 ㅅ볼 수 있었다.

 

cap = (_QWORD *)v45.cap;
  if ( *(_QWORD *)(v45.cap + 8) == 32LL )
  {
    for ( i = 0LL; i < 32; ++i )
    {
      for ( j = 0LL; j < 32; ++j )
      {
        if ( j >= (unsigned __int64)v40[3 * i] )
          runtime_panicIndex();
        if ( (unsigned __int64)j >= cap[1] )
          runtime_panicIndex();
        v25[i] += *(_QWORD *)(v40[3 * i - 1] + 8 * j) * *(unsigned __int8 *)(*cap + j);
      }
    }
    for ( k = 0LL; k < 32; ++k )
    {
      if ( v25[k] != v24[k] )
      {
        v43.array = (interface_ *)&RTYPE_string_0;
        v43.len = (int)&off_4E72B8;
        v49.data = os_Stdout;
        v49.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
        v53.array = (interface_ *)&v43;
        v53.len = 1LL;
        v53.cap = 1LL;
        fmt_Fprintln(v49, v53);
        return;
      }
    }
    v43.cap = (int)&RTYPE_string_0;
    v44 = &off_4E72C8;
    v50.data = os_Stdout;
    v50.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
    v54.array = (interface_ *)&v43.cap;
    v54.len = 1LL;
    v54.cap = 1LL;
    fmt_Fprintln(v50, v54);
  }
  else
  {
    v45.array = (interface_ *)&RTYPE_string_0;
    v45.len = (int)&off_4E72B8;
    v48.data = os_Stdout;
    v48.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
    v52.array = (interface_ *)&v45;
    v52.len = 1LL;
    v52.cap = 1LL;
    fmt_Fprintln(v48, v52);
  }

 

첫번째 for문에서 입력한 문자열에 대해 어떤 규칙을 적용하고, 다음 for문에서 v24와 비교하게 된다. for문에서 연산되는 변수를 중심으로 위에서 선언된 배열을 확인해보았다. v40, v25, v24를 찾아보았는데, 우선 v25는 첫번째 for문에서 처음 나오고, 계산되는 변수였다. 그리고 v24에는 특정 값이 들어가있었다.

v40은 인덱스가 i*3-1에서 다른 배열을 참조하고 있고, 나머지는 32로 채워져있었다. 들어간 배열은 v5~v23, v26~v38 총32개의 배열이었다. for문을 보니 32번씩 돌고 있는거로 보아하니 입력해야하는 문자열의 길이가 32일 것이라고 예상했다.

 

v45.cap이라는 변수가 아직 뭔지 모르지만 처음으로 나오는 조건문이기도 하고, 32인지 확인하는 것으로 보아 아마 문자열의 길이를 확인하는 조건문이라고 생각했다. 입력값을 어떤 변수에 담는지 확인해보았다. 변수 선언 부분에서 a, v51을 사용하여 v45에 입력값을 받는 것을 확인할 수 있다. 그리고 Go에서는 문자열에 대한 구조체가 다음과 같다.

type stringStruct struct {
    str *byte  // 문자열 데이터에 대한 포인터
    len int    // 문자열의 길이
}

따라서 v45.cap이 문자열에 대한 포인터를 가리키고, v45.cap + 8이 문자열의 길이를 가리키게 된다. 따라서 위의 if문은 문자열의 길이를 확인하는 조건문이 맞았다. 이후 나오는 for문을 v24의 값에 대해 역연산을 해주게 되면 알맞는 input값을 알아낼 수 있다.

for문을 조금 더 살펴보면 v25라는 배열에 값을 담고 있는데, v25[i]을 계산할때 v40의 인덱스가 변하지 않고, v40에서 참조하는 하나의 배열을 사용하고 있는 것을 알 수 있다. i가 0부터 31일 때, v25[i]  += v40[3*i-1][j] * input[j]이므로 input을 구해야 하는 변수로 생각하면 32개의 변수에 대해 v40[3*i-1][j]이 계수인, 32개의 방정식이 나오게 되고, 이를 풀어주게 되면 input이 나오게 된다.

이 때 연립방정식을 풀기 위해 numpy.linalg.solve함수를 사용했다. 배열 선언 부분은 코드가 너무 길어져서 제외했다.

 

#ex.py

import numpy as np

A = []

for i in range(0, 32):
    arr = v40_arrays[i]
    A.append(arr)

A = np.array(A)
cap = np.linalg.solve(A, v24)

cap_int = np.rint(cap).astype(int)
cap_int = np.mod(cap_int, 256)
cap_chars = ''.join([chr(c) for c in cap_int])
print(cap_chars)

나온 값을 프로그램에 넣었더니 Correct!이라는 메시지를 확인할 수 있었다.

 

대회는 4인 1팀으로 나가 4문제를 풀어 19등으로 마무리했다.