编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

CTF之多元线性方程(多元线性分析步骤)

wxchong 2024-10-03 03:43:38 开源技术 8 ℃ 0 评论

今天周五了,不知道大家想看点什么,Tiger就更新一个CTF的多元线性方程吧,前阵子HW闹的好多话不敢说,下周正常恢复更新,如果各位亲爱的粉丝们有哪方面想看的文章或者问题,可以在下方留言告诉Tiger或者去咱官网查看www.edusahoo.com免费的课程。

今天更两篇,祝大家看的舒服~手机不卡机~

概述

本片分析文章通过一道看雪CTF题讲述作者的整个分析流程,学习WebAssemble,Z3库,IDC脚本,多元线性方程等内容

分析流程

安装应用后,出现一个输入框和一个按钮

安卓

jadx反编译的apk后先查看清单清单文件的注册组件,只有一个入口活动类,进入查看

<application android:theme = “@ style / AppTheme” android:label = “@ string / app_name” android:icon = “@ mipmap / ic_launcher” android:allowBackup = “true” android:supportsRtl = “true” android:usesCleartextTraffic = “true” android:roundIcon = “@ mipmap / ic_launcher_round” android:appComponentFactory = “android.support.v4.app.CoreComponentFactory” > 
 <activity android:name = “com.example.assemgogogo.MainActivity” > 
 <intent-filter> 
 <action android:name = “android.intent.action.MAIN”/> 
 <category android:name =“android.intent.category.LAUNCHER” /> 
 </ intent-filter> 
 </ activity>

初现迷雾

第一眼:看到Congratulations,我们的目标是打印出这里的字符串,也就是点击按钮后调用本地方法check_key返回值为1即可

再仔细看看:这里有个网页视图组件,这个组件表示有访问网络的操作,但是手机界面并没有看页面,我们去布局文件中看看,只需我们要看一个属性android:visibility表示控件是否可见,只有WebView中可见,按钮,输入框什么的都是隐藏的,并且这个网页流量占据的整个界面,所以我们看到的输入框和按钮都是网页展示的,所以我们需要关注的点就是sayHello的这个本地方法,它传入的网址是哪里的,这是我们下一步的要干的事

public String u = gogogoJNI 。sayHello (); 
 静态的 { 
 系统。loadLibrary (“gogogo” ); 
 }
 protected void onCreate (Bundle bundle ) { 
 super 。onCreate (捆绑); 
 的setContentView ((INT ) - [R 。布局。activity_main ); 
 这个。eText1 = (的EditText ) findViewById (? 。ID 。EDITTEXT ); 
 这个。txView1 = (TextView的) findViewById (? 。ID。textView ); 
 ((web视图) findViewById (? 。ID 。text1View ))。使用loadURL (此。? ); 
 ((web视图) findViewById (? 。ID 。text1View ))。getSettings ()。setJavaScriptEnabled (true ); 
 这个。按钮1 = (按钮) findViewById (? 。ID 。按钮); 
 这个。button1 。setOnClickListener (新 OnClickListener () { 
 公共 无效 的onClick (查看 视图) { 
 如果 (gogogoJNI 。check_key (MainActivity 。这个。eText1 。gettext的()。的toString ()) == 1 ) { 
 MainActivity 。这个。txView1 。的setText (“恭喜!“ );
 } else { 
 MainActivity 。这个。txView1 。setText (“Not Correct!” ); 
 } 
 } 
 };
<EditText android:id = “@ + id / editText” android:visibility = “invisible” android:layout_width = “wrap_content” android:layout_height = “wrap_content” android:text = “Name” android:ems = “10” android: inputType = “textPersonName” android:layout_marginStart = “14dp” app:layout_constraintBottom_toBottomOf = “@ + id / button” app:layout_constraintStart_toEndOf = “@ + id / textView” /> 
 <Button android:id = “@ + id / button” android :visibility = “隐形” android:layout_width = “wrap_content” android:layout_height =“wrap_content” android:layout_marginTop = “52dp” android:text = “check” android:layout_marginEnd = “38dp” app:layout_constraintEnd_toEndOf = “0” app:layout_constraintTop_toTopOf = “0” /> 
 <TextView android:id = “@ + id / textView“ android:visibility = ”invisible“ android:layout_width = ”wrap_content“ android:layout_height = ”wrap_content“ android:layout_marginTop = ”68dp“ android:text = ”key“ android:layout_marginStart = ”22dp“ app:layout_constraintStart_toStartOf = “0” app:layout_constraintTop_toTopOf =“0” /> 
 <WebView android:id = “@ + id / text1View” android:visibility = “visible” android:layout_width = “390dp” android:layout_height = “733dp” android:layout_marginStart = “4dp” app:layout_constraintStart_toStartOf = “0” app:layout_constraintTop_toTopOf = “0” />

探索网址

打开LIB文件夹,出现四个ABI架构对应的那么文件,基本现在手机的芯片都是支持的,这里ARM64在ida6.8不能使用F5大法,所以我们就分析armeabi-V7这个就行了

我们可以看看第一步我们排除的check_key方法,这里逻辑是输出的32位数都为1即可返回1,实际尝试是错误的,混淆视听

在导出表中找到SayHello的方法,要使用F5大法先右键将这个区域代码创建为函数。接着讲这个字节数组异或计算即可的到URL地址。下面写了一个简短的IDC脚本获取到URL地址为http://127.0.0.1:8000

分析到这里,虽然我们探索网址已经完成,但是却没有看见服务端处理的函数,这个才是我们访问URL的时候,处理我们访问请求的函数

#include <idc.idc>
static main ()
{ 
 auto addr = 0x2d28 ; 
 汽车 我; 
 for (i = 0 ; i != 21 ; ++ i )
 { 
 Message (“%c” , Byte (addr + i )^ 0x66 ); 
 } 
}

探索服务端处理函数

从Java的层分析的逻辑中并没有服务端的线索,而这样层也只有初始化的JNIonload,初始化节还没有探索,这是我们接下来的目标

第一步排除初始化节,所以加载后首先执行的节代码,这里可以看出没有这个节,所以排除,那么就直接分析JNIonload方法,JAVA中调用loadlibray的时候调用的方法

JNI_Onload分析:往进电话两层,最终调用下面这个函数

int (__cdecl *inti_proc(void))(int)
{
 return inti_proc();
}

这个函数一开始就对数据段中一块大小为34291的数据进行异或0x67解密,接着创建线程用socket链接将刚才解密的内容构造称HTTP的响应数据包,一旦有socket链接连接过来就发送这样的数据包回去。

逻辑分析清除,下面我们针对细节进行解决

针对需要解密的字节流,通过IDC脚本进行处理,解密后的数据是HTML页面,使用到了WebAssembly技术,网页汇编的灵魂就是将其他语言如C ^汇编成前端可以解释的语言,即用?语言写页面的一些逻辑。

#include <idc.idc>
static main ()
{ 
 auto addr = 0x4004 ; 
 auto i = 34291 ; 
 而(i )
 { 
 - i ; 
 消息(“%c” , 字节(addr ++ )^ 0x67 ); 
 }
}

分析这里的逻辑得知,我们需要让输入内容为32位并且check_key()函数返回结果为1,即可完成这道题

<!DOCTYPE html> 
< html > < head > 
< meta http-equiv = “content-type” content = “text / html; charset = UTF-8” > 
 < meta charset = “utf-8” > 
 < style > 
 body { 
 背景色: RGB (255 , 255 , 255 ); 
 } 
 </ style > 
</ head > 
< script >
var 实例;
WebAssembly 。编译(新的 Uint8Array (` 
00 61 73 6D 01 00 00 00 01 1B 05 60 00 00 60 04 
7F 7F 7F 7F 01 7F 60 02 7F 7F 01 7F 60 01 7F 01 
....... 
66 6C 61 67 0A 12 73 65 74 5F 69 6E 70 75 74 5F 
66 6C 61 67 5F 6C 65 6E 0B 09 63 68 65 63 6B 5F 
6B 65 79 0C 03 78 78 78 
` 。修剪()。分裂(/ [\ s \ r \ N] + /克)。图(STR => parseInt函数(STR , 16 )) 
))。然后(module => { 
 new WebAssembly。实例化(模块)。然后(results => { 
 instance = results ; 
})。捕捉(控制台。误差);})
函数 check_flag (){ 
 VAR 值 = 文档。getElementById (“key_value” )。价值; 
 如果(值。长度 != 32 )
 { 
 文档。getElementById (“tips” )。innerHTML = “不正确!” ; 
 回归; 
 } 
 实例。出口。set_input_flag_len (值。长度); 
 为(VAR II = 0 ; 二< 值。长度; 二++ ){ 
 实例。出口。set_input_flag (value [ ii ] .charCodeAt (),ii ); 
 } 
 var ret = 实例。出口。check_key ();
 if (ret == 1 ){ 
 document 。getElementById (“tips” )。innerHTML = “祝贺!” 
 } 
 else { 
 document 。getElementById (“tips” )。innerHTML = “不正确!” 
 } 
} 
</ 脚本> 
< 体> 
 < DIV >键:< 输入 ID = “key_value” 类型= “文本” style = “width:60%” ; =“” value = “” > < input type = “submit” value = “check” onclick = “check_flag()” > </ div > 
 < div > < label id = “tips “ > </ label > </ div >
</ body > </ html >

下面我们进入网页汇编来探索内部实现逻辑

webassemble

我们在这部分探索的目标就是先用16进制内容构成对应的WASM二进制文件,然后将WASM二进制文件转成C,接着生成ELF文件,用IDA进行分析。

先生成data.bin二进制文件

import array , struct
hexstring = “ \ x00 \ x61 \ x73 \ x6D \ x01 \ x00 \ x00 \ x00 \ x01 \ x1B \ x05 \ x60 \ x00 \ x00 \ x60 \ x04 \ x7F \ x7F \ x7F \ x7F \ x01 \ x7F \ x60 \ x02 \ x7F \ x7F \ x01 \ x7F \ x60 \ x01 \ x7F \ x01 \ x7F \ x60 \ x00 \ x01 \ x7F 
............. 
\ x6C \ x61 \ x67 \ x0A \ x12 \ X73 \ X65 \ X74 \ X5F \ X69 \ x6E \ X70 \ X75 \ X74 \ X5F \ X66 \ X6C \ X61 \ X67 \ X5F \x6C \ x65 \ x6E \ x0B \ x09 \ x63 \ x68 \ x65 \ x63 \ x6B \ x5F \ x6B \ x65 \ x79 \ x0C \ x03 \ x78 \ x78 \ x78 “
f = open ('c:\\ Users \\ xxx \\ Desktop \\ data.bin' ,'wb' )
f 。write (hexstring )
f 。关闭()

接着用wasm2c.exe生成?文件

wasm2c 。exe 数据。bin -o test 。C

直接gcc wasm.c会报错,因为很多wasm的函数没有具体的实现。所以只编译就好了

gcc -c test.c -o test.o

用IDA打开的.o文件

首先JS中调用将输入的字符长度保存到内存中,接着将输入的字符也保存到内存0x400的处

接着就是主要的check_key函数,最终目标是XXX函数返回结果为1,即可完成逆向工作

这里前8个?函数对我们输入的32内容依次进行了处理,我们具体分析一下

一重加密

经过简单分析,这里其实是对输入内容进行了异或计算,然后将结果替换内存中原来的数据。下面图中的条件是肯定满足的,因为我们输入的内容在33到127之间,最小的33 * 4也等于132肯定不为-1时,这个语句恒执行其他的内容,至于其余几个都是相同的内容,即在这里对输入内容进行第一次加密

32元线性方程组

接着我们分析XXX函数,我们的目标也是满足XXX函数返回值为1

从内存中奖一重加密后的输入内容读取到变量中,可以看到顺序做过修改

接下来就是下图中看到的32元方程组,如果有兴趣和数学基础的同学可以用矩阵解法写个类似的小脚本,这里我用到的是Z3库解决

解密

一重解密

PIP安装Z3求解器

接着用蟒脚本写一个求解语句,先初始化32个变量,接着将IDA的内容拷贝过来,将符号修改一下即可

#* - *编码:utf-8 - * - 
来自 z3 进口 *
#生面32元变量
v5 = Int ('m53' )
v6 = Int ('m52' )
v7 = Int ('m51' )
v8 = Int ('m50' )
v9 = Int ('m49' )
v10 = Int ('m48' )
v11 = Int ('m47' )
v12 = Int ('m46' )
v13 = Int ('m45' )
V14 = 诠释('M44' )
V15 = 诠释('M43' )
V16 = 诠释('M42' )
V17 = 诠释('M41' )
V18 = 诠释('M40' )
V19 = 诠释('M39' )
V20 = Int ('m38' )
v21 = Int ('m37' )
v22 = Int ('m36')
v23 = INT ('M35' )
V24 = INT ('M34' )
V25 = INT ('M33' )
V26 = INT ('M32' ) 
V27 = INT ('M31' )
V28 = INT ('M30' )
V29 = INT ('m29' )
v30 = Int ('m28' )
v31 = Int ('m27')
v32 = Int ('m26' )
v33 = Int ('m24' )
v34 = Int ('m25' )
v35 = Int ('m55' )
v36 = Int ('m54' )
#实例化一个求解器对象
s = 求解器()
s 。添加(和(45 * v5 
 + 248 * v6 
 + 20 * v7 
 + 67 * v8 
 + 90 * v9 
 + 135 * v10 
 + 106 * v11 
 + 112 * v12 
 + 40 * v13 
 + 231 * v14 
 + 153 * v15 
 + 233 * v16 
 + 19 * v17 
 + 188 * V18 
 + 232 * V19 
 + 127 * V20 
 + 15 * V21 
 + 67 * V22 
 + 50 * V23 
 + 161 * V24 
 + 103 * V25 
 + 144 * V26 
 + 81 * V27 
 + 126 * V28 
 + 240 * V29 
 + 124 * v30 
 + 194 * v31 
 + 92 * v32 
 + 108 * v33 
 + 111 * v34 
 + 174 * v35 
 + 48 * v36 == 359512 
 ..... 
 , 244 * v5 
 + 196 * v6 
 + 30 * v7 
 + 100 * v8 
 + 168 * v9 
 + 7 * v10 
 + 249 * v11 
 + 84 * v12 
 + 252 * v13
 + 171 * V14 
 + 210 * V15 
 + 206 * V16 
 + 108 * V17 
 + 153 * V18 
 + 67 * V19 
 + 189 * V20 
 + 141 * V21 
 + 239 * V22 
 + 177 * V23 
 + 10 * V24 
 + 15 * V25 
 + 164 * v26 
 + 142 * v27 
 + 97 * v28 
 + 27 * v29 
 + 173 * v30 
 + 146 * v31 
 + 133 * v33 
 + 105 * v34 
 + 75 * (v32 + v35 )
 + 197 * v36 == 393331 ))
s 。添加(185 * v5 
 + 196 * v6 
 + 135 * v7 
 + 218 * (v24 + v9 )
 + 241 * v8 
 + 210 * v10 
 + 127 * v11 
 + 221 * v12 
 + 47 * v13 
 + 179 * v14 
 + 61 * v15 
 + 59 * v16 
 + 197 * v17 
 + 204 * v18 
 + 198 * v19 
 + 75 * v20 
 + 146 * v21 
 + 156 * v22 
 + 235 * v23 
 + 63 * v25 
 + 220 * v26 
 + 3 * v27 
 + 167 * v28 
 + 230 * v29 
 + 69 * v30 
 + 186 * v31 
 + 57 * v32
 + 147 * v33 
 + 221 * v34 
 + 79 * v35 
 + 53 * v36 == 430295 )
#sat表示计算出结果
if s 。check () == sat :
 t = [] 
 print “compute result:” 
 m = s 。model ()
 t 。追加(str (m [ v33 ]))
 t 。追加(str (m [ v34 ]))
 t 。追加(str (m [ v32 ]))
 t 。追加(str(m [ v31 ]))
 t 。追加(str (m [ v30 ]))
 t 。追加(str (m [ v29 ]))
 t 。追加(str (m [ v28 ]))
 t 。追加(str (m [ v27 ]))
 t 。追加(str (m [ v26 ]))
 t 。追加(str (m [ v25 ]))
 t 。追加(str (m [ v24 ]))
 t 。追加(str (m [ v23 ]))
 t 。追加(str (m [ v22 ]))
 t 。追加(str (m [ v21 ]))
 t 。追加(str (m [v20 ]))
 t 。追加(str (m [ v19 ]))
 t 。追加(str (m [ v18 ]))
 t 。追加(str (m [ v17 ]))
 t 。追加(str (m [ v16 ]))
 t 。追加(str (m [ v15 ]))
 t 。追加(str (m [ v14 ]))
 t 。追加(str (m [ v13 ]))
 t 。追加(str (m [ v12 ]))
 t 。追加(str (m [ v11 ]))
 t 。追加(str (m [ v10 ]))
 t 。追加(str (m [ v9 ]))
 t。追加(str (m [ v8 ]))
 t 。追加(str (m [ v7 ]))
 t 。追加(str (m [ v6 ]))
 t 。追加(str (m [ v5 ]))
 t 。追加(str (m [ v36 ]))
 t 。追加(str (m [v35 ]))
 t = map (int , t )
 t = map (chr , t )
 print “” 。加入(t )
否则:
 打印 “失败”

二重解密

这里直接用的大佬的脚本,将上面解密的数据进行异或计算,即可返回最终我们需要输入的内容

int main (int argc , char ** argv ) { 
 unsigned char c [ 33 ] = “S0m3time_l1tt1e_c0de_1s_us3ful33” ; 
 无符号 字符 在[ 33 ] = { 0 }; 
 unsigned int t1 = 0 ,t2 = 0 ,t3 = 0 ,t4 = 0 ;
 printf ((const char * )c ); 
 printf (“ \ n ” ); 
 在[ 0 ] = c [ 0 ] ^ 0x18 ; 
 在[ 1 ] = c [ 1 ] ^ 0x9 ; 
 在[ 2 ] = c [ 2 ] ^ 0x3 ; 
 在[ 3 ] = c [ 3 ] ^ 0x6b ;
 在[ 4 ] = c [ 4 ] ^ 0x1 ; 
 在[ 5 ] = c [ 5 ] ^ 0x5A ; 
 在[ 6 ] = c [ 6 ] ^ 0x32 ; 
 在[ 7 ] = c [ 7 ] ^ 0x57 ;
 在[ 8 ] = c [ 8 ] ^ 0x30 ; 
 在[ 9 ] = c [ 9 ] ^ 0x5d ; 
 在[ 10 ] = c [ 10 ] ^ 0x40 ; 
 在[ 11 ] = c [ 11 ] ^ 0x46 ;
 在[ 12 ] = c [ 12 ] ^ 0x2b ; 
 在[ 13 ] = c [ 13 ] ^ 0x46 ; 
 在[ 14 ] = c [ 14 ] ^ 0x56 ; 
 在[ 15 ] = c [ 15 ] ^ 0x3d ;
 在[ 16 ] = c [ 16 ] ^ 0x02 ; 
 在[ 17 ] = c [ 17 ] ^ 0x43 ; 
 在[ 18 ] = c [ 18 ] ^ 0x17 ; 
 在[ 19 ] = c [ 19 ]; 
 在[ 20 ] = c [ 20 ] ^ 0x32 ; 
 在[ 在[21 ] = c [ 21 ] ^ 0x53 ; 
 在[ 22 ] = c [ 22 ] ^ 0x1F ; 
 在[ 23 ] = c [ 23 ] ^ 0x26 ;
 在[ 24 ] = c [ 24 ] ^ 0x2a ; 
 在[ 25 ] = c [ 25 ] ^ 0x01 ; 
 在[ 26 ] = c [ 26 ]; 
 在[ 27 ] = c [ 27 ] ^ 0x10 ; 
 在[ 28 ] = c [ 28 ] ^ 0x10 ; 
 在[ 在[29 ] = c [ 29 ] ^ 0x1E ; 
 在[ 30 ] = c [ 30 ] ^ 0x40 ; 
 在[ 31 ] = c [ 31 ]; 
 printf ((const char * )in ); 
 返回 0 ; 
}

小结

【1】多元线性方程式可以通过python的z3-solver库快速计算

反思

最开始做这道题我是卡在了最后一步,我用鼠尾草并未求出结果。

主要原因是:我甚至未能清除的理解这个算法的本质,当时并未意识到这是个多元方程求解的计算,只想着怎么求出这个结果,结果在网上找到一个相似题的解决方法,用鼠尾草计算,但在这里却并未算出

结论:解决问题时,不要求对所有细节了如执掌,但是题的主干脉络,根本思路是我们需要探索的

参考

【1】[原创]第五题:的丛林秘密https://bbs.pediy.com/thread-252191.htm

【2】Python中的Z3 API https://nen9ma0.github.io/2018/03/14/z3py/

【3】IDC脚本 - IDC脚本语言官方教程 https://bbs.pediy.com/thread-219016.htm

【4线性】方程组矩阵解法https://www.shuxuele.com/algebra/systems-linear-equations-matrices.html

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表