708 words
4 minutes
STDiO CTF 2024 Qualifiler Write-ups
  • Web
    • BeautifulJS
    • Confusion
  • Reverse
    • Real or Fake
    • Real or Fake 2
    • ReverseMe
  • Crypto
    • 7.83hz
    • G3N1E
  • Pwn
    • AuthEnd
    • Tech Bubble Sort
  • Network
    • BananaNetwork
  • Forensic
    • AD_Dump
  • Misc
    • Modern password

bold text = sovle ได้ขนะแข่ง


Reverse#

Reverse > Real or Fake#

ได้ไฟล์สำหรับ reverse มา เป็น ELF

Screen

2-1-1.png

Ghidra decompile

undefined8 main(void)
{
int iVar1;
int iVar2;
size_t sVar3;
ulong uVar4;
byte abStack_1d8 [64];
byte local_198 [140];
uchar local_10c [4];
char local_108 [64];
undefined8 local_c8;
undefined8 local_c0;
undefined8 local_b8;
undefined8 local_b0;
undefined8 local_a8;
undefined8 local_a0;
undefined8 local_98;
undefined8 local_90;
undefined8 local_88;
undefined8 local_80;
undefined8 local_78;
undefined8 local_70;
undefined8 local_68;
undefined8 local_60;
undefined8 local_58;
undefined8 local_50;
undefined local_48;
byte local_39;
undefined4 local_38;
undefined4 local_34;
undefined4 local_30;
undefined4 local_2c;
undefined4 local_28;
int local_24;
int local_20;
uint local_1c;
local_28 = 1;
local_c8 = 0x3831623563663536;
local_c0 = 0x6136326162643834;
local_b8 = 0x6131336436306631;
local_b0 = 0x3038393366386239;
local_a8 = 0x3233333765373236;
local_a0 = 0x3366636264656132;
local_98 = 0x3336303837326530;
local_90 = 0x3663343936336666;
local_88 = 0x3266363064333632;
local_80 = 0x3461336162383733;
local_78 = 0x3631326338363739;
local_70 = 0x3636353636366630;
local_68 = 0x3864383231613766;
local_60 = 0x6339383266336633;
local_58 = 0x6139393633393865;
local_50 = 0x6135303736373462;
local_48 = 0;
printf("Please enter flag: ");
__isoc99_scanf(&DAT_0010201c,local_108);
iVar2 = strncmp("flag(STDIO2024_04{Tryharder-NoStringsMTFK)",local_108,0x14);
if (iVar2 == 0) {
puts("GG Boi !");
local_2c = 0xf;
local_30 = 0x378;
local_34 = 0x11;
calc(99,0x11,0xf);
local_38 = 0xb3;
snprintf((char *)local_10c,4,"%d",0xb3);
SHA512(local_10c,3,local_198);
local_20 = 0;
for (local_1c = 0; local_1c < 0x40; local_1c = local_1c + 1) {
iVar2 = *(char *)((long)&local_c8 + (long)local_20) % ' ' + 9;
iVar1 = *(char *)((long)&local_c8 + (long)(local_20 + 1)) % ' ' + 9;
abStack_1d8[(int)local_1c] =
(char)iVar1 + (char)(iVar1 / 0x19) * -0x19 +
((char)iVar2 + (char)(iVar2 / 0x19) * -0x19) * '\x10';
local_20 = local_20 + 2;
}
local_24 = 0;
while( true ) {
uVar4 = (ulong)local_24;
sVar3 = strlen((char *)local_198);
if (uVar4 <= sVar3) break;
local_39 = abStack_1d8[local_24] ^ local_198[local_24];
printf("Here\'s your flag: %c\n",(ulong)(uint)(int)(char)local_39);
local_24 = local_24 + 1;
}
}
return 0;
}

จากการสั่งเกตุคราวๆจะเห็นว่าตรง strncmp นั้นจะ check 20 ตัวแรก (0x14) แล้วทำการ hash sha512 จาก local_10c ไปเก็บที่ local_198 แล้วทำการ loop สร้างค่าไปเก็บไว้ที่ abStack_1d8 แล้วทำการ loop แสดงผล flag

แต่ทำไม flag ไม่ออก ? ถ้าเราอ่าน code จนละเอียดแล้วเราจะพบว่า input ของเราหลังจาก 20 ไม่ได้มีผลอะไรต่อเงื่อนไขของ output flag เลย เพราะเงื่อนไขที่ว่า ถ้า local_24 น้อยกว่า ความยาวของ hash (64) ให้หยุด ซึ่งค่าเริ่มต้นของ local_24 มันเริ่มจาก 0 เสมอ นั้นจึงเป็นสาเหตุให้ loop นี้หยุดทำงานทันที

และอีกเหตุผลหนึ่งคือ input ของเราไม่มีผลต่อการสร้าง output (flag) นอกจาก check ว่า 20 แรกตรงไหม

2-1-2.png

2-1-3.png

การแก้ปัญหา เราจะทำการลบ condition ตรงนี้ทิ้งด้วยการ jump

2-1-4.png

จะเห็นว่า output แต่ละ char นั้นจะเต็มไปด้วย string ที่เป็น prefix นี้ เราจึงลบมันออกเพื่อความง่ายในการเอา flag

2-1-5.png

patched

2-1-6.png

Reverse > Real or Fake 2#

ต่อเนื่องจากข้อ Real or Fake ซึ่งข้อนี้ คำใบ้คือ “ครั้งนี้พลาดจริงๆ ตรงล่างสุด”

2-2-1.png

ครับ…

หลังจาก reverse แล้วเราจะเจอทันทีว่า มันไม่มี printf ตรง loop ที่ใช้ output

2-2-2.png

diff real or fake 1 และ 2 ต่างกันตรงที่ตัวแปรที่ 8 - 16

และอีกหนึ่งจุดคือ ค่าที่ประกาศไว้ในตอนแรกถูกแก้ไข นั้นหมายความว่า flag ไม่ใช่ตัวเดียวกันกับข้อแรกแน่ๆ

โดยรวมแล้วข้อ 2 มี flag ใหม่ และไม่มี printf แถมจะใส่ if 0 < 64 มาอีก เราจึงแก้ปัญหาด้วยการยืม ELF ที่ patch แล้วในข้อก่อนมาใช้ เพื่อใช้แทนที่ค่า flag ใหม่ ให้คำนวณออกมา

2-2-3.png

หลังจาก patch เพื่อความง่ายอีกครั้ง เราก็ข้าม check prefix input ไปทำให้ใส่อะไรก็ได้

2-2-4.png

Pwn#

Pwn > Auth End#

เราได้ไฟล์ ELF มาเพื่อหาวิธีเจาะช่องโหว่

Screen

4-1-1.png

4-1-2.png

หลังจาก reverse แล้ว เงื่อนไขที่จะได้ flag คือ username และ password ต้องถูกต้อง

เมื่อเราสังเกตุจากตัวแปร buffer สำหรับรับค่า username ขนาด 76 และ password ขนาด 64 โดย stack จะเรียงจาก password(local_98) ต่อด้วย username(local_58) และ ตัวแปรที่เอาไว้เก็บเงื่อนไข (local_c)

จาก code จะสังเกตุได้ว่า local_c เป็นเงื่อนไขที่จำทำให้ code print flag ออกมาโดยเงื่อนไขคือ ห้ามเป็น 0 เราจึงมองหาวิธีทำให้ค่าไม่เป็น 0 ซึ่งจาก stack จะพบว่าเราสามารถ overflow ลงมาจาก local_58 ได้

ปัญหาก่อน print flag คือค่า local_c จะเปลี่ยนเป็น 1 เมื่อ user/pass ถูก

เราจึงสรุปได้ว่าทำให้ user / pass ผิดและ user จะต้อง overflow ลงไปหา local_c ซึ่งก็คือ 76 + 1

4-1-3.png

4-1-4.png

Misc#

Misc > Modern password#

ได้ code สำหรับรับรูปไปทำอะไรสักอย่าง ถ้าผลรวมถูกจะได้ flag

URL_PASSWORD = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Cat.png/307px-Cat.png"
import torch
import torchvision
from PIL import Image
import requests
class Decoder(torch.nn.Module):
def __init__(self):
super().__init__()
self.training = False
self.flag = torch.Tensor([
0x2c, 0x2b, 0xff, 0x5e, 0xa0, 0xd4, 0x86, 0x98, 0x93,
0x7a, 0x72, 0x16, 0xcc, 0x65, 0x76, 0x5e, 0x28, 0x4f,
0x2e, 0x39, 0x78, 0xf6, 0x15, 0xda, 0x6e, 0x35, 0x6d,
0xd6, 0x83, 0xdf, 0x2a, 0x67, 0xd5, 0x60, 0xeb, 0x9a
]).int()
def forward(self, x):
torch.manual_seed(x)
x = torch.nn.Embedding(0o1750, 0o44, padding_idx=0)(x)
x = torch.abs(255 * x).int() % 0xFF
if x.sum() != 0o7617:
return "Access denied - please provide correct password"
return (
"Access granted - here is the flag: "
+ f"STDIO2024_11{{{ ''.join([chr(c) for c in (x ^ self.flag).tolist()]) }}}"
)
class ModernPassword(torch.nn.Module):
def __init__(self):
super().__init__()
self.training = False
self.backbone = torchvision.models.mobilenet_v2(weights=torchvision.models.MobileNet_V2_Weights.DEFAULT, progress=False)
self.backbone.eval()
self.decoder = Decoder()
self.prep = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
def forward(self, url):
try:
img = Image.open(requests.get(url, stream=True, headers={'User-Agent': 'STDIO2024'}).raw)
out = self.prep(img).unsqueeze(0)
assert out.shape == (1,3,224,224)
except:
return "Access denied - error loading image from URL"
return self.decoder(torch.argmax(self.backbone(out)))
model = ModernPassword()
print(model(URL_PASSWORD))

จะสังเกตุได้ว่า class ModernPassword นั้นจะทำการ download รูปจาก url แล้วแปลงไปเรื่อยๆ แต่ก่อนเข้า self.decoder นั้นคือ torch.argmax ซึ่งถ้าเรา debug ด้วยการดักค่าตรงนั้นออกมาดูจะพบว่าเป็น scalar tensor

ไม่ว่าเราจะเอารูปไหนเข้าไป ค่าตรงนี้จะเป็น scalar เราจึงทำการ brute force

i = 0
while (True)
result = Decoder()(torch.tensor(i))
if "flag" in result:
print(result)
break
i += 1

7-1-1.png

STDiO CTF 2024 Qualifiler Write-ups
https://blog.noonomyen.com/posts/ctf/stdio-ctf-2024-writeups
Author
noonomyen
Published at
2024-09-26
License
CC BY-NC-SA 4.0