Southern Island

[ CTF ] ANUSEC 2021 본선 후기 + 롸업

by 월루

ANUSEC 2021

올해 온라인으로 본선을 진행하는 "전국 고등학생 사이버 보안 경진대회"에 참가하게 되었다.

팀 단위로 참가가 가능했고, YISF 2021 대회와 함께 내 생에 첫 CTF 대회였다.

많이 떨렸던 거 같기도 하고, 기대도 많았던 대회였다. 대회 진행 사진이나 등수를 찍어뒀어야 했는데

블로그를 미쳐 생각하지 못하고 등수나 진행 사진은 찍지 못하였다, 아무튼 총 37개 팀 95명이 참가하였고,

웹, 시스템, 모바일(리버싱), 포렌식, 암호학, 네트워크로 총 6개의 분야의 문제가 출제됐다.

난 시스템과, 모바일(리버싱)을 담당하게 되었고, 나머지 팀원들도 정말 열심히 대회에 참가해 주었다.

최종적으로 등수는 37개 팀 중 8등을 하였고, 6등까지 수상을 했던지라 많이 아쉬웠던 대회였다.

하지만 첫 대회에서 정말 많은 것들을 배웠고, 앞으로 더욱 노력할 수 있게 해 준 원동력이 되었다.

 

Write-up

Moblie - ELEVATOR

 -> 해당 문제는 안드로이드 APK 문제였다, 우선 서명이 되지 않아 설치가 정상적으로 진행되지 않았기에 "jarsigner"를 사용하여 서명을 진행하여 주었다, 그 후 adb를 활용하여 설치를 시도하였지만 test_only라는 오류가 발생하였다, 아무래도 테스트용 앱으로 컴파일된 것 같았다. adb 설치 옵션에서 -t를 이용하여 "adb install -t <앱>"으로 정상적으로 설치 후 실행하였더니 아래와 같은 화면이 나타났다.

우측의 윗, 아래쪽 화살표를 누르면 위의 층수를 나타내는 숫자 값이 1씩 오르거나 내려간다. 우선 해당 앱에선 FLAG를 얻을만한 힌트를 얻을 수 없었다. 그래서 "jadx" 툴을 활용하여 apk 파일을 디컴파일 하였고, "MainActivity.class" 파일에서 층수를 나타내는 int형 숫자 값이 int형의 최대, 최솟값인 ±2147483647에 도달하면 FLAG를 출력한다는 걸 확인하였다. FLAG 값을 출력하는 함수는 .so 라이브러리 파일로 되어있었고, JNI를 이용하여 호출되었다. 처음엔 라이브러리 파일 자체를 디버깅하여 FLAG값을 얻으려 했으나 "jadx"로 디컴파일된 로직에서 분기문만 조작하면 쉽게 FLAG값을 얻을 수 있을 것 같아서 "apktool"을 활용하여 apk를 언패킹 하고 "MainActivity.smali" 메인 스마일리 코드를 수정하여 "MainActivity" Class 파일의 29번째 라인의 2147483647 값을 1로 수정하고

 

("const-wide/32 v2, 0x7FFFFFFF" => "const-wide/32 v2, 0x00000001")

 

서명을 진행하고, 프로그램을 동작시켜 1층 위로 올려 FLAG값을 구할 수 있었다.

 

Moblie - Question

 -> 우선 해당 문제 apk 파일도 서명이 진행되지 않았다, 그래서 정상적인 설치를 위해 "jarsigner"를 이용해서 서명을 진행하고, adb 설치 옵션 -t를 이용하여 설치 후 실행을 시키자 아래와 같은 화면을 볼 수 있었다.

"This app can only run on England devices."라는 메시지가 출력됨과 함께 어떠한 반응도 없었다.

아무래도 해당 디바이스의 국가 정보를 확인하고 영국이라면 넘어가는 로직이란 걸 유추할 수 있었다.

아무튼 "jadx" 툴을 활용하여 해당 apk를 디컴파일 하였고, "MainActivity.onCreate"에서

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    String userdir = System.getProperty("user.dir");
    
    if (userdir == null || userdir.isEmpty() || !userdir.equals("England")) {
    	Toast.makeText(getApplicationContext(), "This app can only run on England devices.", 0).show();
    } else {
    	startActivity(new Intent(this, LoginActivity.class));
    }
}

첫 분기문에서 else 값이 나와야 정상적으로 다음 Activity로 넘어갈 것이라 생각하고 "apktool"을 활용하여 apk파일을 언패킹 후 smali 코드를 수정하여 첫 분기문의 else가 되도록 유도하였다, 그 후 서명을 진행하고 실행하자 아래와 같은 화면을 볼 수 있었다.

public class LoginActivity extends AppCompatActivity {
    /* access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        final EditText ei = (EditText) findViewById(R.id.edit_id);
        final EditText ep = (EditText) findViewById(R.id.edit_pw);
        Button sb = (Button) findViewById(R.id.sample_button);
        final String username = getResources().getString(R.string.user);
        final String keypw = getResources().getString(R.string.password);
        sb.setOnClickListener(new View.OnClickListener() {
            /* class com.example.test2.LoginActivity.AnonymousClass1 */

            public void onClick(View view) {
                String securePw = LoginActivity.getMD5Hash(ep.getText().toString().toLowerCase());
                if (!username.equals(ei.getText().toString())) {
                    Toast.makeText(LoginActivity.this.getApplicationContext(), "====It does not match====", 0).show();
                } else if (!keypw.equals(securePw)) {
                    Toast.makeText(LoginActivity.this.getApplicationContext(), "====It does not match====", 0).show();
                } else {
                    Toast.makeText(LoginActivity.this.getApplicationContext(), "====It matches====", 0).show();
                    LoginActivity.this.startActivity(new Intent(LoginActivity.this.getApplicationContext(), music.class));
                }
            }
        });
    }

    public static String getMD5Hash(String text) {
        try {
            MessageDigest m = MessageDigest.getInstance("MD5");
            m.update(text.getBytes(), 0, text.length());
            return new BigInteger(1, m.digest()).toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
}

처음엔 네트워크에 연결하여 데이터를 주고받는 걸 잡아서 어떻게 하는 게 아닐까 생각했지만 그냥 오프라인으로 MD5 값을 생성하여 내부의 Password값이랑 비교하여 넘어가는 형식이었다. 간단하게 smali 코드를 수정하여 어떠한 입력값에도 무조건 1을 반환하여 넘어갈 수 있었다, 로그인 화면을 넘어가자 아래와 같은 화면을 볼 수 있었다.

ublic class music extends AppCompatActivity implements View.OnClickListener {
    public ArrayList<String> array_1;
    final String flag = stringFromJNI();
    final String[] str_arr = {"Do", "Re", "Mi", "Fa", "Sol", "La", "Si"};
    final String value = stringFromJNI2();
    String yourflag = "";

    public native String stringFromJNI();

    public native String stringFromJNI2();

    /* access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_music);
        ((Button) findViewById(R.id.piano1)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano2)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano3)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano4)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano5)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano6)).setOnClickListener(this);
        ((Button) findViewById(R.id.piano7)).setOnClickListener(this);
        Log.d("tttt", "실행전");
        func();
        Log.d("ttttarr1", "" + this.array_1);
    }

    public void onClick(View view) {
        TextView flagshow = (TextView) findViewById(R.id.flag);
        switch (view.getId()) {
            case R.id.piano1:
                this.yourflag += this.array_1.get(0);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano2:
                this.yourflag += this.array_1.get(1);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano3:
                this.yourflag += this.array_1.get(2);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano4:
                this.yourflag += this.array_1.get(3);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano5:
                this.yourflag += this.array_1.get(4);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano6:
                this.yourflag += this.array_1.get(5);
                func();
                Log.d("ttttest", this.yourflag);
                break;
            case R.id.piano7:
                this.yourflag += this.array_1.get(6);
                func();
                Log.d("ttttest", this.yourflag);
                break;
        }
        if (this.value.equals(this.yourflag)) {
            flagshow.setText(this.flag);
        } else {
            flagshow.setText(this.yourflag);
        }
    }

    public void func() {
        ArrayList<String> arrayList = new ArrayList<>();
        this.array_1 = arrayList;
        arrayList.addAll(Arrays.asList(this.str_arr));
        Collections.shuffle(this.array_1);
        ((TextView) findViewById(R.id.text1)).setText(this.array_1.get(0));
        ((TextView) findViewById(R.id.text2)).setText(this.array_1.get(1));
        ((TextView) findViewById(R.id.text3)).setText(this.array_1.get(2));
        ((TextView) findViewById(R.id.text4)).setText(this.array_1.get(3));
        ((TextView) findViewById(R.id.text5)).setText(this.array_1.get(4));
        ((TextView) findViewById(R.id.text6)).setText(this.array_1.get(5));
        ((TextView) findViewById(R.id.text7)).setText(this.array_1.get(6));
    }
}

아무래도 어떠한 복잡한 로직의 조건을 달성하면 FLAG가 출력될 것 같았다. 따로 로직을 고민하지 않고, onClick 메서드의 마지막 분기문을 smali 코드를 수정하여 강제로 FLAG값을 출력하게 하였다. 정상적으로 FLAG값이 출력되었다.

Moblie - TRUE

 -> 우선 해당 문제 apk 파일도 서명이 진행되지 않았다, 그래서 정상적인 설치를 위해 "jarsigner"를 이용해서 서명을 진행하고, adb 설치 옵션 -t를 이용하여 설치 후 실행하였다, 근데 무슨 일인지 바로 FLAG값이 출력되었다.

정말 황당하게도 정상적인 FLAG 값이었다, 아무래도 특정 조건이 우연히 만족됐던 것 같았다. 너무 쉽게 FLAG 값은 얻었지만 간단하게 로직을 확인해보기로 했다.

public class MainActivity extends AppCompatActivity {
    public native String stringFromJNI();

    static {
        System.loadLibrary("native-lib");
    }

    /* access modifiers changed from: protected */
    @Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.fragment.app.FragmentActivity
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((TextView) findViewById(R.id.sample_text)).setText(stringFromJNI());
    }
}

아무래도 조건 자체가 서명과 TestOnly앱을 설치 후 실행하는 과정이었던 것 같다.

 

MISC - Image Challenge

 -> 압축파일 하나를 받았고, 해당 압축파일을 풀었더니 아래와 같은 파일들이 나왔다.

첫 번째 파일을 제외한 나머지 파일은 암호가 걸려있었다, 부르트 포스 어택으로 아래 파일을 바로 복호화할 수 있었지만 대회 규정상 정석으로 진행하기로 했다, 우선 첫 번째 파일의 압축을 풀자 아래와 같은 이미지 파일을 알 수 있었다.

딱 봐도 QR 코드와 바코드 파일이라고 생각하였고 "zxing.org" 해당 사이트에서 바코드를 하나씩 입력시켜 두 번째 압축파일의 비밀번호를 알 수 있었다, "B@rc0dek1ng" 해당 비밀번호로 2번째 압축파일을 풀자 아래와 같은 폴더들이 나타났고, 해당 폴더들 안에는 여러 장의 사진들이 있었다.

처음엔 카빙 기술을 활용하여 사진 파일 안에 정보가 있거나, 사진 자체에 FLAG값이 존재할 거라 생각했다, 하지만 결국 찾을 수 없었고, meta 데이터에 사진을 찍은 위치를 활용하여 보라는 팀원의 조언에 "deCode" 사이트의 "https://www.dcode.fr/geolocalisation-coordonnees" 해당 툴을 활용하여 모든 사진의 meta 데이터의 경도와 위도를 입력해본 결과 "QPSL"라는 단어를 얻을 수 있었다, 해당 단어는 3번째 압축파일의 비밀번호였다, 그렇게 3번째 압축파일까지 정상적으로 해제하자 아래와 같은 이미지 파일이 한 장 나왔다.

HxD 툴을 활용하여 이미지를 열어 바이너리를 조사한 결과 PK 시그니처를 찾을 수 있었고, 카빙 기술이 적용돼있다고 확신하였다, 카빙 툴인 "foremost"를 사용하여 사진 파일에서 FLAG값이 적혀있는 ""00000004.jpg" 이미지 파일을 구할 수 있었다. 

 

그 외 문제

위 까지가 내가 해결했던 문제였고, 나머지는 팀원들이 해결했던 문제라 따로 롸업을 쓰지 않았다, 추후 팀원들이 블로그를 운영하거나 롸업을 따로 올린다면 추가로 적어두겠다!

'CTF > 국내대회' 카테고리의 다른 글

[ CTF ] YISF 2021 예선, 본선  (0) 2021.08.27

블로그의 정보

남쪽의 외딴섬

월루

활동하기