qchecker

We’re given a fun Ruby quine program that self-modifies based on its argument:1

[franksh@moso qchecker]$ ruby qchecker.rb 
eval$uate=%w(a=%(eval$uate=%w(#{$uate})*"");Bftjarzs=b=->a{a.split(?+).map{|b|b.to_i(36)}};c=b["awyiv4fjfkuu2pkv+awyiv4f
v                  ut                  71                  6g                  3j                  +a                  x
c  e5e4pxrogszr3+5i0o  mfd5dm9xf9q7+axce5  e4khrz21ypr+5htqqi  9iasvmjri7+axcc76i  03zrn7gu7+cbt4  m8  xybr3cb27+1ge6  s
n  jex10w3si9+1k8vdb4  fzcys2yo0"];d,e,f,  g,h,i=b["0+0+zeexa  xq012eg+k2htkr1ola  j6+3cbp5mnkzll  t3  +2qpvamo605t7j  "
]  ;(j=eval(?A<<82<<7  1<<86)[0])&&d==0&&  (e+=1;k=2**64;l=->  (a,b){(a-j.ord)*25  6.pow(b-2,b)%b  };  f=l[f,k+13];g=  l
[                  g,                  k+  37];h=l[h,k+51];i=  l[i,k+81];j==?}&&(  d=e==32&&f+g+h  +i  ==0?2:1);a.sub  !
(/"0.*?"/,'"0'+[d  ,e  ,f,g,h,i].map{|x|x  .to_s(36)}*?+<<34)  );srand(f);k=b["7a  cw+jsjm+46d84"  ];  l=d==2?7:6;m=[  ?
#*(l*20)<<10]*11*  ""  ;l.times{|a|b=d==0  &&e!=0?rand(4):0;9  .times{|e|9.times{  |f|(c[k[d]/10*  *a  %10]>>(e*9+f)&  1
)!=0&&(g=f;h=e;b.  ti  mes{g,h=h,8-g};t=(  h*l+l+a)*20+h+g*2+  2;m[t]=m[t+1]=""<<  32)}}};a.sub!(  /B  .*?=/,"B=");n=  m
.                  co                  un                  t(                  ?#                  )-  a.length;a.sub  !
("B=","B#{(1..n).map{(rand(26)+97).chr}*""}=");o=0;m.length.times{|b|m[b]==?#&&o<a.length&&(m[b]=a[o];o+=1)};puts(m))*""
[franksh@moso qchecker]$
[franksh@moso qchecker]$ ruby qchecker.rb > out.rb; for c in `echo "SECCON{AAAAA}" | fold -w1`; do ruby - $c < out.rb > tmp.rb; mv tmp.rb out.rb; done; cat out.rb
eval$uate=%w(a=%(eval$uate=%w(#{$uate})*"");Bygzwgnlnjkmwzhugrpnmdvlcwpmqlebkawvjklvmkmkc=b=->a{a.split(?+).map{|b|b.to_
i  (36)}}  ;c=b["  aw                  yi                  v4                  fj                  fkuu2pkv+awyiv4fvut71
6  g3j+ax  ce5e4p  xr  ogszr3+5i0omfd  5d  m9xf9q7+axce5e  4k  hrz21ypr+5htqq  i9  iasvmjri7+axcc76i03zrn7gu7+cbt4m8xybr
3  cb27+1  ge6snj  ex  10w3si9+1k8vdb  4f  zcys2yo0"];d,e  ,f  ,g,h,i=b["01+d  +m  6177zx5cmtf+1mdtba3ieal9d+2v6gou7jwyt
c+2  uf  h2  68  232n  wq"];(j=eval(?  A<  <82<<71<<86)[0  ])  &&d==0&&(e+=1;  k=  2**64;l=->(a,b){(a-j.ord)*256.pow(b-2
,b)  %b  };  f=  l[f,                  k+  13];g=l[g,k+37  ];  h=l[h,k+51];i=  l[  i,k+81];        j==?}&&(d=e==32&&f+g+
h+i  ==  0?  2:  1);a  .sub!(/"  0.*?"/,'  "0'+[d,e,f,g,h  ,i  ].map{|x|x.to_  s(  36)}*?+<<34));  srand(f);k=b["7acw+js
jm+46  d84"];  l=d==2  ?7:6;m=[?#  *(l*20  )<<10]*11*"";l  .t  imes{|a|b=d==0  &&  e!=0?rand(4):0  ;9.times{|e|9.times{|
f|(c[  k[d]/1  0**a%1  0]>>(e*9+f)&  1)!=  0&&(g=f;h=e;b.  ti  mes{g,h=h,8-g}  ;t  =(h*l+l+a)*20+  h+  g*2+  2;m[  t]=m[
t+1]=  ""<<32  )}}};a  .sub!(/B.*?=/,  "B                  ="  );n=m.count(?#  )-                  a.  leng  th;a  .sub!
("B=","B#{(1..n).map{(rand(26)+97).chr}*""}=");o=0;m.length.times{|b|m[b]==?#&&o<a.length&&(m[b]=a[o];o+=1)};puts(m))*""
[franksh@moso qchecker]$

Cool!

Caveat: I’ve never written even a hello world in Ruby2, and I solved this task simply by deconstructing the program “naively,” so this is going to be a bit sketch.

My emacs buffer ended up such:

# a=%(eval$uate=%w(#{$uate})*"");
# Bftjarzs=b=->a{a.split(?+).map{|b|b.to_i(36)}};

Apparently Ruby syntax for lambdas are ->args{code} and apparently you don’t call lambdas with v(x) but rather v.call(x), but, again apparently, v[x] is a short-hand trick, even though [] is usually used for lookups like in Python. So there’s this b lambda that splits a string on + and then interprets the fragments as base-36. Useful.

%(...) is some weird Ruby syntax for literal string arrays(?), ?<char> is an escaped literal char, a*"" joins an array of strings (perhaps * :: [T] -> T -> T for some monoid T works like intersperse?). I mean, I could definitely get into Ruby, this is good stuff.

# c=b["awyiv4fjfkuu2pkv+awyiv4fvut716g3j+axce5e4pxrogszr3+5i0omfd5dm9xf9q7+axce5e4khrz21ypr+5htqqi9iasvmjri7+axcc76i03zrn7gu7+cbt4m8xybr3cb27+1ge6snjex10w3si9+1k8vdb4fzcys2yo0"];

# 2413138514168077294502911 0x1ff0080402010080403ff b'\x01\xff\x00\x80@ \x10\x08\x04\x03\xff'
# 2413138514203124227638271 0x1ff0080403ff0080403ff b'\x01\xff\x00\x80@?\xf0\x08\x04\x03\xff'
# 2415504318135715148071935 0x1ff80c0603e10080403ff b'\x01\xff\x80\xc0`>\x10\x08\x04\x03\xff'
# 1216023231471466527982591 0x10180c06030180c0603ff b'\x01\x01\x80\xc0`0\x18\x0c\x06\x03\xff'
# 2415504318120356412261375 0x1ff80c06030180c0603ff b'\x01\xff\x80\xc0`0\x18\x0c\x06\x03\xff'
# 1214839173222390695330815 0x1014090443ff80c0603ff b'\x01\x01@\x90D?\xf8\x0c\x06\x03\xff'
# 2415495076716156996027391 0x1ff8040201ff0080403ff b'\x01\xff\x80@ \x1f\xf0\x08\x04\x03\xff'
# 75705726472931768279551 0x100804020100804021ff b'\x10\x08\x04\x02\x01\x00\x80@!\xff'
# 321749341105789098926865 0x442211154aa554462311 b'D"\x11\x15J\xa5TF#\x11'
# 345406059408174499233792 0x49248000000000000000 b'I$\x80\x00\x00\x00\x00\x00\x00\x00'

Here I found a big such “array” of raw data, which I pretty printed, and it looks to me maybe it’s used for constructing the ASCII-art output…

# d,e,f,g,h,i=b["0+0+zeexaxq012eg+k2htkr1olaj6+3cbp5mnkzllt3+2qpvamo605t7j"];

# d = 0 0x0 b''
# e = 0 0x0 b''
# f = 4659461645708163688 0x40a9bbae0cfdfa68 b'h\xfa\xfd\x0c\xae\xbb\xa9@'
# g = 2641556351334323346 0x24a8b18187759492 b'\x92\x94u\x87\x81\xb1\xa8$'
# h = 15837377083725718695 0xdbc9aa8c316224a7 b'\xa7$b1\x8c\xaa\xc9\xdb'
# i = 12993509283917003551 0xb45237d9e59cfb1f b'\x1f\xfb\x9c\xe5\xd97R\xb4'

(j=eval(?A<<82<<71<<86)[0])
  && d==0 && (
  e+=1;
  k=2**64;
  l=->(a,b){(a-j.ord)*256.pow(b-2,b)%b};
  f=l[f,k+13];
  g=l[g,k+37];
  h=l[h,k+51];
  i=l[i,k+81];
  j==?} && (d=e==32&&f+g+h+i==0?2:1);
  a.sub!(/"0.*?"/,'"0'+[d,e,f,g,h,i].map{|x|x.to_s(36)}*?+<<34)
);

Now wait just a second, this is way more suspicious. pow, %-mod, 2**64+13 is a prime i recognize, etc. So yeah, this is likely to be the logic.

But following the calculations through, we have a bunch of steps , with and being the ordinal value of our input characters. And the goal seems to be and since they’re non-negative (Ruby’s % operator is sane) it means each sequence should end at 0.

From this it’s reasonable to assume that the starting values is the flag itself, and indeed the modulus is big enough to reconstruct it with CRT:

>>> crt(vals, [2**64 + v for v in [13,37,51,81]]).bytes()
b'}!!!3n1uQ_ru0y_3t1rw_5t3L{NOCCE'

And there we go.

The rest isn’t that interesting, but I guess it constructs the ASCII-art output:

srand(f);k=b["7acw+jsjm+46d84"];
l=d==2?7:6;
m=[?#*(l*20)<<10]*11*"";
l.times{
  |a|
   b=d==0
    && e!=0?rand(4):0;
  9.times{|e|9.times{|f|(c[k[d]/10**a%10]>>(e*9+f)&1)!=0&&(g=f;h=e;b.times{g,h=h,8-g};t=(h*l+l+a)*20+h+g*2+2;m[t]=m[t+1]=""<<32)}}
};


a.sub!(/B.*?=/,"B=");
n=m.count(?#)-a.length;
a.sub!("B=","B#{(1..n).map{(rand(26)+97).chr}*""}=");
o=0;
m.length.times{
 |b|m[b]==?#
  &&
  o<a.length&& (m[b]=a[o]; o+=1)
};
puts(m)
  1. These are best viewed with a dark-mode theme.

  2. though I have a smattering of experience with a ton of other languages, including Perl, which Ruby seems to borrow some of its Philosophy of Ergonomics from.