Try   HackMD

TSG CTF 2020 writeup by NaruseJun

Reverse-ing

by amama

I ran the reverse program and saw input nothing, so I hit some keys then the program suddenly finished with the word 'wrong'.
I guessed the program is getting a line, and checking the line is the same as the flag, then print 'correct' or 'wrong' (all of these 'rev' problems the kind like this, isn't it?).

Next, to debugging the reverse program, check symbols in the program using objdump command (I know, usually we use readelf instead of objdump in general, but in my case, I just want to know the entry point of the program).

objdump -d reverse

00000000006000b0 <reverse>:
  6000b0:	9c                   	pushfq 
  6000b1:	57                   	push   %rdi
  6000b2:	56                   	push   %rsi
  6000b3:	52                   	push   %rdx
  6000b4:	51                   	push   %rcx
  6000b5:	53                   	push   %rbx
  ...(snip)...

00000000006000e5 <_start>:
  6000e5:	48 31 db             	xor    %rbx,%rbx
  6000e8:	bb b0 00 60 00       	mov    $0x6000b0,%ebx
  6000ed:	ff d3                	callq  *%rbx
  6000ef:	c1 fb 61             	sar    $0x61,%ebx
  6000f2:	5e                   	pop    %rsi
  6000f3:	19 b2 25 ff d3 8c    	sbb    %esi,-0x732c00db(%rdx)
  6000f9:	19 2b                	sbb    %ebp,(%rbx)
  6000fb:	63 72 48             	movslq 0x48(%rdx),%esi
  ...(snip)...

Now we can get reverse function and _start functions, and the latter is the entry point.

It is time to debugging!
I prepared to run the reverse program again with gdb, and set breakpoints to the _start function and the reverse function, then ran reverse and the program stoped in entering _start function.

Using stepwise instruction execution (command si), you can see the mechanism of the program. Surprisingly, the program repeat executing an instruction and calling the reverse function again and again! How does the reverse function work? Let's disassemble the reverse function.

The core of the reverse function is below.

Execute these instructions in your mind or gdb, now you can understand these instructions literally 'reverse' the code. So, the next instruction after the reverse function executed is the bottom binary of the code area.

Recalling the reverse function's code and continue to debug, you realize that important instructions are the instruction after the reverse function. The core part of the _start function does

  1. read the input (using syscall)
  2. compare the input and the flag (of course, the flag is encrypted)
  3. output the suitable string ('correct' or 'wrong')

Parts 1 and 3 are not important to solve the problem, so we pay attention to part 2.

Part 2 decrypts flag and compares to the input, therefore we simulate the sequence of decryption and get the decrypted flag. The detail of part 2 is a simple arithmetic operation.
let input[37] be an input of the program (the number, 37, is easily seen in the resister when syscall instruction is executed).
We can see the suquence like the Pseudo code.
Pseudo code is:

unsigned char *array1, *array2;

for(i = 36; i; i--) {
    if((input[i] ^ array1[i]) + array2[i] != 0) return print("wrong");
}
print("correct");

Hence, using array1 and array2 and apply inverse projection of the operation, now we get the flag.
Dumping array1 and array2 using dgb (the addresses of array1 and array2 in the run-time are both 0x600194 but inside of array is actually different because reverse oparation reverses around these addresses), get these values.

array1 = { -28,-45,-1, 5, 15, 107,124, 19, -1, -54,-45,-1, -1, 49, 72, 114, 99, 43, 25, -116, -45,-1, 37, -78, 25, 94, 97, -5, -63,-45,-1, 0, 96, 0, -80,-69,-37};
array2 = { 72, -128, 70, -70,-91,-45,-1, -64, 49, 72, 30, 101,50, -92,-120, -45, -1, -26,-119, 72, 95, 122,-124, 59, -45,-1, -46,49, 72, 78, 54, -55, -59,-49,34, 50, 88};

decrypt code is here:

int adder[] = { -28,-45,-1, 5,  15, 107,124, 19,
 -1, -54,-45,-1, -1, 49, 72, 114,
 99, 43, 25, -116,    -45,-1, 37, -78,
 25, 94, 97, -5, -63,-45,-1, 0,
 96, 0,  -80,-69,-37};



int xorer[] = { 72, -128,    70, -70,-91,-45,-1, -64,
 49, 72, 30, 101,50, -92,-120, -45,
 -1, -26,-119,    72, 95, 122,-124, 59,
 -45,-1, -46,49, 72, 78, 54, -55,
 -59,-49,34, 50, 88};

main() {
	for(int i = 0; i < 37; i++) {
		for(int c = 0; c < 128; c++) {
			if(((c ^ (i%2?adder:xorer)[i]) + (i%2?xorer:adder)[i]) == 0) printf("%c", c);
		}
	}
}

Note: It is important to swap adder and xorer because these instruction's number is odd, reverse function reverses the code one by one.

appendix

0x6000ef <_start+10>:        xor    %rdx,%rdx
0x6000f4 <_start+15>:        mov    $0x25,%dl
0x6000f8 <_start+19>:        mov    %rsp,%rsi
0x6000fd <_start+24>:        xor    %rdi,%rdi
0x600102 <_start+29>:        xor    %rax,%rax
0x600107 <_start+34>:        syscall
0x600110 <_start+43>:        xor    %rcx,%rcx
0x600115 <_start+48>:        mov    (%rsi,%rdx,1),%cl
0x60011a <_start+53>:        xor    0x600194(%rdx),%cl // 88
0x600122 <_start+61>:        add    0x600194(%rdx),%cl // -37
0x60012a <_start+69>:        or     %rcx,%rdi
0x60012f <_start+74>:        dec    %dl
0x600133 <_start+78>:        jns    0x600171 <_start+140>
0x600176 <_start+145>:       xor    0x600194(%rdx),%cl // -69
0x60017e <_start+153>:       add    0x600194(%rdx),%cl // 50
...(snip)...

Actually call reverse is injected between every instruction.

Self Host

by amama

If you cant read Japanese, use DeepL or Google Translation

TL;DR Quine書いた。終わり

序文

プログラマが行う知的な遊びの一つとして、自分自身を出力とするプログラムを書く、というものがある。
これはQuineと呼ばれており、インターネットで検索するといろいろなQuineのソースコードを見ることができるだろう。

では、Quineは一体どうやって書けばいいのだろうか。
ここでは、一般的なプログラミング言語に使える技術を紹介するとともに、それをコンパイラに応用する方法を示す。

Write Quine

さて、実際にQuineを書くにあたって、ここで目指すQuineの一般形をここに示しておこう。
これはC言語風の疑似ソースコードだが、Quineの本質部分は十分理解できるはずだ。

decode(s) {
    for(c in s) {
        switch(c) {
            case 64: // 64 is at-mark (@)
                putc(34);  // 34 is double-quatation (")
                print(s);
                putc(34);
            default:
                putchar(c);
        }
    }
}

decode("decode(s) {for(c in s) { switch(c) {case 64: putchar(34);print(s); putc(34); default: putc(c); }}} decode(@);");

では、このソースコードに到達するための道のりを説明する。

まずプログラマ諸氏がナイーブに「自分自身を出力するプログラム」を書こうと思ったとき、上記のようなプログラムを書こうと思う人は居ないはずだ。
以下のようなプログラムを書こうとして、無限後退に陥っていることに気づくだろう。

// first approarch
s = "print(s);";
print(s);
→ print(s);

// second approach
s = "s = \"print(s)\"; print(s);";
print(s);
→ s = "print(s)"; print(s);

// third approach
s = "s = \"s = \\\"print(s)\\\"; print(s)\"; print(s)";
print(s);
→ s = "s = \"print(s)\"; print(s);"; print(s);

// and so on ...

当然、上記のアプローチはうまく行かない。
見てもらえば分かる通り、このプログラムの問題点はsに「自分自身」の文字列を格納しようとするが、実際の出力とソースコードがどうやっても一致しないため、どんどん文字列が長くなってしまうことにある。

この問題点を解決するアプローチの一つとして、文字列の中に「自分自身」を表す文字を入れ、その文字が来たら自分自身を出力する、という方法がある。

decode(s) {
    for(c in s) {
        switch(c) {
            case 64: // 64 is at-mark (@)
                print(s);
            default:
                putchar(c);
        }
    }
}

decode("decode(s) {for(c in s) { switch(c) {case 64:print(s); default: putchar(c); }}} decode(\"@\");");

→ decode(s) {for(c in s) { switch(c) {case 64:print(s); default: putchar(c); }}} decode("decode() {for(c in s) { switch(c) {case 64:print(s); default: putchar(c); }}} decode("@");");

このdecode関数による出力は自分自身と同じ文字列を出力するように見える。
しかし、注意深い読者なら直ちに気がつくであろうが、実は2箇所違う場所がある。
このバージョンはdecode関数の引数の文字列に「"」を含むため、当該箇所(…… decode(\"@\");");)を「\"」としているが、「\"」のプログラム中における値は「"」であるため、出力された文字列をよく見ると、…… decode("@");"); となってしまっている。

この問題を解決するため、decode関数を改良する。
最もナイーブな解決方法としては、

            case 64: // 64 is at-mark (@)
                putc('"');
                print(s);
                putc('"');

とすることであるが、この方法はうまくいかない。
なぜなら、肝心のdecode関数に渡す文字列の中に「"」を出現させない、という目標が達成できていないからだ。
たしかに、…… decode(\"@\");");…… decode(@);");と書けるようになったが、今度は関数の中身を文字列化するときにcase 64:print(s);case 64:putc('\"'); print(s);putc('\"'); となってしまう。
このように、ソースコード中に「"」を出現させずにコードを書く必要があるが、その値に対する複数の表現方法があれば問題点を回避できるような表現方法を選択することによって解決できる。
たとえば、大抵のプログラミング言語ではある文字とそれを表現するASCII/unicodeは互いに変換可能なことが多いので、これを利用して「"」のASCIIコードである34を代替表現として用いることでdecode関数が完成する。
Quineを書くときには、「文字列中に存在してはいけない文字」が出てこないように注意深くコードを書くことが必要である。

つまり、Quineを書くためには、

  • 値をデコードして出力するプログラム
  • 「値をデコードして出力するプログラム」をエンコードした値

の2つがあればよいということがわかる。

Reflections on Trusting Trust

次に、Quineを応用してコンパイラに悪意のあるコードを侵入させる方法について示す。

この問題の目標は「「「攻撃者のコンパイラでコンパイルしたcompiler.x」でコンパイルしたcompiler.x」でflagの1文字目を出力するコードをコンパイルしたとき、すべての文字列を出力するプログラムを出力する」ということである。

そもそもなぜ3回コンパイルするのか

コンパイラを使用してプログラムをコンパイルするとき、そのコンパイラにバグがあったら成果物が正しく動かない。そこで、コンパイラにバグが有るかどうかを判断したい、という問題がある。
この問題を解決するため、3-stage bootstrapという手法が使用される。

まず、言語Xで書かれた言語Xのコンパイラのソースコード

Cs0 にバグがあるかもしれない、と仮定する。
そして、
Cs0
を既存のコンパイラ
Cnative
でコンパイルして成果物であるコンパイラ
C0
を得る。
しかし、
Cs0
にはバグがあるかもしれないので
C0
は正しく動かないかもしれない。
すなわち、
C0
でコンパイルした成果物が正しく動かないかもしれない、ということである。
このコンパイラで自分自身
Cs0
をコンパイルすると、「正しく動かないかもしれないコンパイラでコンパイルされたコンパイラ
C1
」が手に入る。
もし
Cs0

  • バグがなければ
    C0
    は正しく動くので、結果は「正しく動くコンパイラでコンパイルされた正しく動くコンパイラ
    Ccorrect1
    」になる。
  • バグがあれば
    C0
    は正しく動かないので、結果は「正しく動かないコンパイラでコンパイルされた正しく動かないコンパイラ
    Cincorrect1
    」になる。

さて、このどちらかのコンパイラが生成されるわけだが、当然、正しく動くコンパイラを使用したい。
では、どうやったら見分けることができるのだろうか?
例えば、これによって得られた

C1 で同じソースコード
Cs0
をコンパイルするとどうなるか考えてみよう。
結果は、
Ccorrect1
でコンパイルされたコンパイラ
Ccorrect2
か、
Cincorrect1
でコンパイルされたコンパイラ
Cincorrect2
となる。
もし
Cs0
が正しく動いていれば、
C1
C2
を比較すると一致するはずである。
そして、この対偶を取ると、「もし
C1
C2
を比較して一致しなければ、
Cs0
は正しく動いていない」ということがわかるので、
C1
C2
が一致するならば(成果物の生成に対しては)
正しく振る舞うようなコンパイラ
Cs0
が書けた、と言える。

ちなみに

C0
C1
を比較しても、コンパイルしたコンパイラのソースが違うため(
C0
を出力したのは
Cnative
C1
を出力したのは
Cs0
からできたコンパイラである)、意味がないことに注意してほしい。

inject evil code

CTFに参加する諸君はインターネットからソフトウェアやライブラリの実行可能バイナリをダウンロードして利用することはせず、対象となるOSSのソースコードを読み込み悪意のあるコードが混入していないか検査してからコンパイルして利用することが一般的であると思うが、そのコンパイラに悪意がないとどうして言い切れるのだろうか?

コンパイラにバグがあるかを判断する手法として3回コンパイルするという手法があることを説明したが、悪意のあるコードが挿入されているかどうかを判断することはこの方法では不可能であることを同様の論法を持って紹介する。

まず、言語Xのコンパイラ

Cnative悪意のあるコードがあるかもしれない、と仮定する。
そして、事前の検証によって安全であることがわかっている「言語Xで書かれた言語Xのコンパイラのソースコード
Cs0
」を
Cnative
でコンパイルして成果物であるコンパイラ
C0
を得る。
しかし、
Cnative
には信用できないので
C0
も信用できない。
すなわち、
C0
でコンパイルした成果物に悪意のあるコードが混入されているかもしれない、ということである(例えば、成果物に対してある種のバックドアを仕込むようなものかもしれない)。
このコンパイラで自分自身
Cs0
をコンパイルすると、「信用できないコンパイラでコンパイルされたコンパイラ
C1
」が手に入る。
もし
Cnative

  • 悪意のあるコードがなければ
    C0
    は正しく動くので、結果は「正しく動くコンパイラでコンパイルされた正しく動くコンパイラ
    Ccorrect1
    」になる。
  • 悪意のあるコードがあれば
    C0
    は正しく動かないので、結果は「正しく動かないコンパイラでコンパイルされた正しく動かないコンパイラ
    Cevil1
    」になる。

さて、このどちらかのコンパイラが生成されるわけだが、信用できるかどうかを見分けられるだろうか?
同じように、これによって得られた

C1 で同じソースコード
Cs0
をコンパイルするとどうなるか考えてみよう。
結果は、
Ccorrect1
でコンパイルされたコンパイラ
Ccorrect2
か、
Cevil1
でコンパイルされたコンパイラ
Cevil2
となる。
バグの有無を判断するときには、「もし
C1
C2
を比較して一致しなければ、
Cs0
は正しく動いていない」ということを判断基準にしたが、この場合、悪意のあるコードが「『入力
Cs0
に対しては
Cnative
のように振る舞う』というコードを挿入する」のようなものであった場合、
C1
に「入力
Cs0
に対しては
Cnative
のように振る舞う」 というコードが挿入されてしまうため、
C1
C2
は常に一致してしまう。
よって、この方法ではコンパイラに悪意があるかどうかを判断することができない。

では、どうやってこの悪意のあるコード挿入を達成すればよいのだろうか。

一般にコンパイラとは「入力として言語Xのソースコードを受け取り、言語Yのソースコードを出力するプログラム」とみなすことが多いが、ここでは更に一般化して、「入力xを受け取り、yを出力するプログラム」であると解釈する。

ここで、「言語Xで書かれた『言語Yを受け取り言語Zを出力するプログラム』」を

YXZ と表すことにする。
XXY
XxY
に入力として与えると、出力として
XYY
が手に入るが、もし
x=Y
であればこれはある入力(
XXY
)に対してQuine的に振る舞う、ということがわかる(すなわち、出力と自分自身が一致しているということである)。

さて、単純なQuineが書けることはすでにわかっているが、このような「ある入力に対してQuine的に振る舞うプログラム」に対してこの技術を応用したい。

この問題の目標は「「「攻撃者のコンパイラでコンパイルしたcompiler.x」でコンパイルしたcompiler.x」でflagの1文字目を出力するコードをコンパイルしたとき、flagのすべての文字列を出力するプログラムを出力する」であるので、

  • code が 攻撃者のコンパイラ
  • code2 が「攻撃者のコンパイラでコンパイルしたcompiler.x」
  • code3 が「攻撃者のコンパイラでコンパイルしたcompiler.x」でコンパイルしたcompiler.x

である。

また、

  • F
    を flagの1文字目を出力するコード
  • Fevil
    を flagのすべての文字列を出力するプログラム

とする。

つまり、code3

F をコンパイルしたとき、
Fevil
を出力せよ。

という問題である。

よって、code3 は、

  • 「入力が
    F
    の場合、
    Fevil
    を出力する」という悪意のあるコードが挿入されたコンパイラであり、
  • このコンパイラを出力する code2 は、
    • 「入力が compiler.x の場合、『
      Fevil
      を出力する』というコードを挿入する」という悪意のあるコードが挿入されたコンパイラであり、
    • このコンパイラを出力する code は、
      • 「入力が compiler.x の場合、『入力が compiler.x の場合、【
        Fevil
        を出力する】というコードを挿入する』というコードを挿入する」コンパイラである。

そして、problem.pyには code2 == code3 を確認するチェックが61行目にかかれているため、code2 は本質的にcode3と同じことができなければならず、逆もまた然りである。

よって、code3

  • 「入力が
    F
    の場合、
    Fevil
    を出力する」という悪意のあるコードが挿入されたコンパイラであり、かつ、「入力が compiler.x の場合、『
    Fevil
    を出力する』というコードを挿入する」という悪意のあるコードが挿入されたコンパイラである

そして、code3 を出力する code2

  • 「入力が
    F
    の場合、
    Fevil
    を出力する」という悪意のあるコードが挿入されたコンパイラであり、かつ、「入力が compiler.x の場合、『
    Fevil
    を出力する』というコードを挿入する」という悪意のあるコードが挿入されたコンパイラである

ので、code2compiler.x を入力としたとき、Quine的に振る舞うことがわかる。
以下、code2 のソースコードを compiler2.x として説明する。

compiler2.x は、

  • 「入力が compiler.x ならば、入力を compiler2.x とみなす」というコードになっている
  • 「入力が
    F
    の場合、
    Fevil
    を出力する」というコードを挿入するコードになっている

上の条件はまさしくQuineと同様の手法を使ってコードを書くことができる。
また、ここでは簡単のため、上記の機能を実装するときに入力をすべてチェックすることはせず、flagが入手できる程度にcompiler2.xが書ければ良いものとする。
「入力が compiler.x ならば」という部分は
s = tokenize()が終わったあとに、maintokenize などといった識別子が s の中に存在しているかで判断すればよいだろう。
さて、「入力を compiler2.x とみなす」という部分はどのように実装すればよいのだろうか。
compiler2.xのコードをそのまま文字列にすると無限後退が起きてしまうということは学習したので、Quineと同様に

  • 入力を compiler2.x とみなすプログラム
  • 「入力を compiler2.x とみなすプログラム」をエンコードした値

を実装すればよい。

「入力を compiler2.x とみなすプログラム」を実装したものが以下である。また、コード中に出てくる qlist は「入力を compiler2.x とみなすプログラム」をエンコードした値が入っている。
inj_ret = inj_ret + escape ( qlist ) ; の部分が、自分自身を出力する行為に相当する。

​qlist ;
escape ( q ) {
​       e_i = 0 ;
​       e_ret = [ ] ;
​       while ( e_i + 1 < len ( q ) ) {
​               e_ret = e_ret + [ [ 34 ] + q [ e_i ] + [ 34 ] ] ;
​               e_ret = e_ret + [ [ 44 ] ] ;
​               e_i = e_i + 1 ; 
​       }
​       e_ret = e_ret + [ [ 34 ] + q [ e_i ] + [ 34 ] ] ;
​       return e_ret ;
}

​injection ( s ) {
​       inj_i = 0 ;
​       inj_ret = [ ] ;
​       while ( inj_i < len ( s ) ) {
​               if ( inj_i + 9 < len ( s ) ) {
​                       if ( s [ inj_i ] == "main" && s [ inj_i + 1 ] == "(" && s [ inj_i + 2 ] == ")" && s [ inj_i + 3 ] == "{" && s [ inj_i + 4 ] == "s" && s [ inj_i + 5 ] == "=" && s [ inj_i + 6 ] == "tokenize" && s [ inj_i + 7 ] == "(" && s [ inj_i + 8 ] == ")" && s [ inj_i + 9 ] == ";" ) {
​                               inj_ret = inj_ret + qlist ;
​                               inj_ret = inj_ret + [ "main" , "(" , ")" , "{" , "s" , "=" , "tokenize" , "(" , ")" , ";" ] ;
​                               inj_ret = inj_ret + [ "qlist" , "=" , "[" ] ;
​                               inj_ret = inj_ret + escape ( qlist ) ;
​                               inj_ret = inj_ret + [ "]" , ";" ] ;
​                               inj_ret = inj_ret + [ "s" , "=" , "injection" , "(" , "s" , ")" , ";" ] ;
​                               inj_i = inj_i + 10 ;
​                       } else {
​                               inj_ret = inj_ret + [ s [ inj_i ] ] ;
​                               inj_i = inj_i + 1 ;
​                       }
​               } else {
​                       inj_ret = inj_ret + [ s [ inj_i ] ] ;
​                       inj_i = inj_i + 1 ;
​               }
​       }
​       return inj_ret ;
}

main ( ) {
​       s = tokenize ( ) ;
​       qlist = [ "qlist" , ";" , "escape" ... ] ;
​       s = injection ( s ) ;
​       ...
}

このコードをエンコードして即値として qlist に代入すれば、「入力が compiler.x ならば、入力を compiler2.x とみなす」というコードができる。
実際は、同時に「入力が

F の場合、
Fevil
を出力する」というコードを挿入するコードにでもなければならないので、以下のコードを injection 関数に追記する。

inj_ret = inj_ret + [ "if" , "(" , "s" , "[" , "0" , "]" , "==" , ( [ 34 ] + "flag" + [ 34 ] ) , "&&" , "s" , "[" , "1" , "]" , "==" , ( [ 34 ] + ";" + [ 34 ] ) , "&&" , "s" , "[" , "2" , "]" , "==" , ( [ 34 ] + "main" + [ 34 ] ) , "&&" , "s" , "[" , "3" , "]" , "==" , ( [ 34 ] + "(" + [ 34 ] ) , "&&" , "s" , "[" , "4" , "]" , "==" , ( [ 34 ] + ")" + [ 34 ] ) , "&&" , "s" , "[" , "5" , "]" , "==" , ( [ 34 ] + "{" + [ 34 ] ) , "&&" , "s" , "[" , "6" , "]" , "==" , ( [ 34 ] + "flag" + [ 34 ] ) , "&&" , "s" , "[" , "7" , "]" , "==" , ( [ 34 ] + "=" + [ 34 ] ) , ")" , "{" , "s" , "=" , "[" , ( [ 34 ] + "flag" + [ 34 ] ) , "," , ( [ 34 ] + ";" + [ 34 ]) , "," , ( [ 34 ] + "main" + [ 34 ] ) , "," , ( [ 34 ] + "(" + [ 34 ] ) , "," , ( [ 34 ] + ")" + [ 34 ] ) , "," , ( [ 34 ] + "{" + [ 34 ] ) , "," , ( [ 34 ] + "flag" + [ 34 ] ) , "," , ( [ 34 ] + "=" + [ 34 ] ) , "," , "s" , "[" , "8" , "]" , "," , ( [ 34 ] + ";" + [ 34 ] ) , "," , ( [ 34 ] + "write" + [ 34 ] ) , "," , ( [ 34 ] + "(" + [ 34 ] ) , "," , ( [ 34 ] + "flag" + [ 34 ] ) , "," , ( [ 34 ] + ")" + [ 34 ] ) , "," , ( [ 34 ] + ";" + [ 34 ] ) , "," , ( [ 34 ] + "}" + [ 34 ] ) , "]" , ";" , "}" ] ;

これは、「入力が

F の場合、
Fevil
を出力する」というコードをエンコードしたものになっている。
また、これらのコードはQuineを書くときに注意しなければならない「文字列中に存在してはいけない文字」である「"」が出現してしまっているので、エンコードするときに "main" などという文字列は代替表現である [109, 97, 105, 110] になおしてからエンコードする。

例えば、inj_ret = inj_ret + [ "]" , ";" ] ; というコードをエンコードするときには、"が入っていない等価なコードであるinj_ret = inj_ret + [ [93], [59] ] ;とみなしエンコードする。
エンコード後は
[[101, 110, 106, 95, 114, 101, 116], [61], [101, 110, 106, 95, 114, 101, 116], [43], [91], [91], [57, 51], [93], [44], [91], [53, 57], [93], [93], [59]] というリストとなり、これは
[ "inj_ret", "=", "inj_ret", "+", "[", "[", "93", "]", ",", "[", "59", "]", "]", ";"]
という表現と等価である。

エンコードの際に用いたスクリプトが以下である。
このスクリプトは、空白区切りでtokenizeされた文字列を受け取り、エンコードした値を返す。
エンコードした値は、文字列のリストであるので、数値のリストのリストが出力される。

a = input().split(' ')
lis = []
for s in a:
    e = []
    if len(s) == 0:
        continue
    for c in s:
        e.append(ord(c))
    t = ', '.join(map(str, e))
    lis.append('[' + t + ']')
print('[' + ', '.join(lis) + ']')