💾 Archived View for gmi.of.sb › glog › 2021 › 2-12-red-packet.gmi captured on 2021-12-05 at 23:47:19. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2021-12-03)

-=-=-=-=-=-=-

我的2021新春紅包謎題

先祝各位新春快樂。

因爲2019冠狀病毒病疫情,2020年並沒有發紅包。今年恢復了新春紅包的傳統,考慮受衆裡非技術的比例變大了,所以今年還開了一期面向非技術的版本,可以參考Laoself頻道的消息。

技術向的謎題紅包

非技術向的謎題紅包

本文主要講一些設計謎題的過程、想法和細節。

技術向

如果你主要來看個技術向的題解,感謝HomeboyC,我不用自己寫技術向的紅包的解題了,不過我之後還是會補充一些這篇文章沒有寫到的內容。

LAOSB的春节红包打开方式 by HomeboyC

整個紅包爲了降低對我自己的業務風險,延續元旦的紅包,在Cloudflare Worker平臺上構建。

第一關都是常見套路大串聯,就不提了,Cloudflare的報錯頁面很明確指示了不能請求Cloudflare自己,記得用自己伺服器就好。

第二關事後來看,因爲要求的原文的encode版本暴露出來了,大大降低了這一題的難度。我試圖透過加入無關干擾項來mitigate直接提取的方式,但效果比較差,幾乎都是通過簡單的推測來做的,下面稍微講一下我的預期思路。

這個演算法其實是個非常初級的string compression的decompression,可以觀察第一關提供的樣本,以及因爲這個是個開放的代理,可以透過自己修改encoded結果來測試。先來看看第一關提供的樣本:


%4#%5K%4l%4/%4
%5#%5J%4r%5g%4#%4s%4u%4r%4j%4u%4h%4v%5#%4k%4h%4u%4h%4$%4#%4K%4h%4u%4h%4#%4l%4v%4#%4w%4k%4h%4#%4i%4l%4u%4v%4w%4#%4r%4q%4h%4/%4#%4i%4r%4u%4#%4D%4o%4l%4s%4d%4|%4=%4#%46%44%48%46%5<%48%46%4#%41%4
%5#%5E%4x%4w%4#%4w%4k%4d%4w%4*%4v%4#%4r%4q%4h%4#%4r%4i%4#%4w%4z%4r%41%4#%4L%4i%4#%4|%4r%4x%4#%4p%4d%4n%4h%4#%4w%4k%4d%4w%4#%4s%4u%4r%4{%4|%4#%4v%4d%4|%4#%4%%4K%4d%4s%5|%4)%4a%4(%4)%5a%4,%4a%4O%4x%4q%4d%4u%4(%5-%4a%4)%4-%5Q%4h%4z%4&%5,%5\%4h%4d%4u%4+%7a%4(%45%43%45%44%4%%4#%4Z%4L%4W%4K%4R%4X%4W%4#%4d%4q%4|%4#%4q%4r%4q%40%4^%4d%40%4}%4D%40%4]%43%40%4<%4`%4#%4l%4q%4e%4h%4w%4z%4h%5q%4/%4#%4|%4r%4x%4*%4o%5#%4j%4h%4w%4#%4w%4k%4h%4#%4v%4h%4f%4r%4q%4g%4#%4r%4q%4h%4*%4v%4#%4f%4r%4g%4h%4#%4l%4q%4v%4w%4h%4d%4g%41%4
%5#%5J%4r%5g%4#%4o%4x%4f%4n%4#%4w%4u%4|%4l%4q%4j%4#%4w%4k%4l%4v%4#%4d%4q%4g%4#%4d%4o%5#%4w%4k%4h%4#%4|%4h%4d%4u%4/%4
%4#%5V%4k%4l%4e%4r%4#%4O%4|%4x%41%4
%4#%5

這裡多一句嘴,HomeboyC的瀏覽器顯然把這個text/plain當成了text/html,顯示上丟了換行符,好險看起來不影響拷貝,差一點點就要出不來了。

以及解碼結果:


  Hi,

  Good progress here! Here is the first one, for Alipay: 31533953 .

  But that's one of two. If you make that proxy say "Happy&^%&&^)^Lunar%%*^&**New##))Year((((^%2021" WITHOUT any non-[a-zA-Z0-9] inbetween, you'll get the second one's code instead.

  Good luck trying this and all the year,
  Shibo Lyu.
  

一個非常明顯的特徵是重複出現的「%4」序列,偶爾還有「%5」這樣的變種。大膽改變「%4」爲「%5」或類似數字測試,可以觀察到結果中出現字元重複的情況,而且似乎和數字數量相關。多做幾個測試後應該不難得到(%後面的數字-3)爲與它相關的字元的重複次數。由於該文本並無幾處連續字元重複,大量「%4」印證了這一點。

結合上面的原文,以及可以測試到的末尾換行/空格不影響結果的特點,可以推測數字對應的相關字符在「%」前。通過少數幾處字元重複點可以確立幾個字元對應關係,稍加觀察應該就容易發現只是一個offset。於是稍微寫個腳本就能拿來編碼了。實際上「%」在這裏是一個佔位符,用來在數字不到兩位數時保證位置對應;且這位數字是以三十六進位形式表示,但此例中爲降低難度,offset比較小,不涉及,並不需要這麼複雜的推定。

附上我使用的JavaScript實作:

const offset = 3;
const codePointOffset = 3;
const offsetCodePoint = (char, offset) =>
  String.fromCodePoint([char.codePointAt(0) + offset]);

const encode = (str) => {
  let encoded = "";
  let lastChar = str[0];
  let count = 1;

  const pushForward = () => {
	let countChars = (count + offset).toString(36);
	if (countChars.length > 2) throw new Error("transformation failed.");
	encoded +=
	  offsetCodePoint(lastChar, codePointOffset) + countChars.padStart(2, "%");
  };

  for (let i = 1; i < str.length; i++) {
	const char = str[i];
	if (char === lastChar) {
	  count++;
	} else {
	  pushForward();
	  lastChar = char;
	  count = 1;
	}
  }
  pushForward();
  return encoded;
};

const decode = (str) => {
  if (str.length % 3 !== 0) throw new Error("failed to decode.");

  let decoded = "";
  for (let i = 0; i < str.length; i += 3) {
	const char = str[i];
	const countStr = (str[i + 1] + str[i + 2]).replace("%", "");
	const count = parseInt(countStr, 36) - offset;

	decoded += offsetCodePoint(char, -codePointOffset).repeat(count);
  }

  return decoded;
};

技術向的紅包共兩個:

第二關的沒有人領到超過平均值的,慘(

非技術向

表單已經關閉了,所以在這列一下題目(不含選項):

兩個題目都是測試對我的online presence的瞭解程度,第一題爲了加大難度加上了一個視覺檢測。(逃

我不打算寫答案。(以保證一定程度的題目復用性?)不過可以公佈一些統計結果:

對於這個結果感到有意思。我的預期比這個高一點。我後悔給第二題加了「(以上均使用過)」選項。(x

這是我個人的一種嘗試,第一次來看以後還是可以改善一下。未來應該還是會加這個類型的紅包,過年嘛,最主要還是要開心。

(再吐槽一下MikeCRM的免費服務質量是越來越差,有沒有人做個稍微能打一點的啊淦!)

祝大家新年快樂。

(完,20210213)