0% found this document useful (0 votes)
14 views57 pages

Slides

The document discusses the use of CodeQL to identify memory disclosure vulnerabilities in the XNU kernel, particularly focusing on a specific out-of-bounds read issue (CVE-2023-27941) that allows kernel memory disclosure. It outlines the author's approach, including setting up a debugging environment and writing queries to find vulnerabilities based on certain code patterns. The findings led to a patch that improved input validation to address the identified security flaw.

Uploaded by

viwexe5457
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views57 pages

Slides

The document discusses the use of CodeQL to identify memory disclosure vulnerabilities in the XNU kernel, particularly focusing on a specific out-of-bounds read issue (CVE-2023-27941) that allows kernel memory disclosure. It outlines the author's approach, including setting up a debugging environment and writing queries to find vulnerabilities based on certain code patterns. The findings led to a patch that improved input validation to address the identified security flaw.

Uploaded by

viwexe5457
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

CodeQL + DTrace = in XNU

How to find multiple memory disclosures in XNU using CodeQL


whoami
Arsenii Kostromin
Security researcher
Focus on macOS security: userland and kernel
Twitter @0x3C3E
Agenda
Kernel Memory Disclosure, my and bugs in XNU
Motivation
Apple interviewer asked me several times why I don't look for bugs in the kernel
Is it hard for you?

Before December 2022 , I haven't looked into the XNU source code

4
Kernel Memory Disclosure
My approach
Search online and tag writeups
Prepare a debugging environment
Use CodeQL to search for some patterns

7
Some easy bugs in XNU
A tale of a simple Apple kernel bug
Weggli was used to find a specific pattern
Finding a memory exposure vulnerability with CodeQL
CodeQL was used, the author found a bug in the DTrace module of XNU

8
How to debug kernel on a single M1 laptop?
QEMU emulates Intel-based macOS
DTrace, dynamic tracing framework in XNU

9
DTrace
Released in 2005 by Oracle
Apple merged it into XNU in 2007
Was it thoroughly audited?
It's complex and has its emulator in the kernel
#define DIF_OP_OR 1 /* or r1, r2, rd */
#define DIF_OP_XOR 2 /* xor r1, r2, rd */
...
#define DIF_OP_STRIP 80 /* strip r1, key, rd */

bsd/sys/dtrace.h 10
CodeQL
Framework for doing static analysis
Models code as data → database
Write logic-based SQL-like queries to find patterns

11
Building a CodeQL database
Have to compile the program we want to query
By default, some files were missing
A great script to build a CodeQL database for XNU by pwn0rz

12
Code pattern
I decided to look for OOB issues. For that, I wrote a query to find such code, which meets
the conditions below:
a >= b , where a is signed, and b is not

No a <= 0 and a < 0 checks


a is an array index

13
a >= b , where a is signed, and b is not
from Variable arg
where exists(
GEExpr ge | [Link]() = [Link]()
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isSigned()
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isUnsigned()
)
select arg

14
No a < 0 and a <= 0 checks
from Variable arg
where not exists(
LTExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and not exists(
LEExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
select arg

15
a is an array index
from Variable arg, ArrayExpr ae
where [Link]() = [Link]()
select [Link](),
[Link]()

16
Combined
from Variable arg, ArrayExpr ae
where exists(
GEExpr ge | [Link]() = [Link]()
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isSigned()
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isUnsigned()
)
and not exists(
LTExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and not exists(
LEExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and [Link]() = [Link]()
select [Link](),
[Link]()
17
The query produces
20 results
Only 6 different functions

18
fasttrap_pid_getargdesc
// args: (void *arg, dtrace_id_t id, void *parg, dtrace_argdesc_t *desc)
if (probe->ftp_prov->ftp_retired != 0 ||
desc->dtargd_ndx >= probe->ftp_nargs) {
desc->dtargd_ndx = DTRACE_ARGNONE;
return;
}

ndx = (probe->ftp_argmap != NULL) ?


probe->ftp_argmap[desc->dtargd_ndx] : desc->dtargd_ndx;

Docs: get the argument description for args[X]

bsd/dev/dtrace/fasttrap.c 19
dtargd_ndx is int
typedef struct dtrace_argdesc {
...
int dtargd_ndx; /* arg number (-1 iff none) */
...
} dtrace_argdesc_t;

ftp_nargs is unsigned char

struct fasttrap_probe {
...
uint8_t ftp_nargs; /* translated argument count */
...
};

bsd/sys/dtrace.h, bsd/sys/fasttrap_impl.h 20
Both sides are converted to int
As desc->dtargd_ndx is int and probe->ftp_nargs is unsigned char
if (probe->ftp_prov->ftp_retired != 0 ||
desc->dtargd_ndx >= probe->ftp_nargs) {
desc->dtargd_ndx = DTRACE_ARGNONE;
return;
}

If desc->dtargd_ndx < 0 , then desc->dtargd_ndx >= probe->ftp_nargs is always


false

21
OOB Read, desc->dtargd_ndx is an index
ndx = (probe->ftp_argmap != NULL) ?
probe->ftp_argmap[desc->dtargd_ndx] : desc->dtargd_ndx;

If probe->ftp_argmap isn't null , it's possible to reach the first expression and use
desc->dtargd_ndx with values less than 0

22
No direct calls to the function
It's called as a C-style virtual function

23
dtrace_pops

typedef struct dtrace_pops {


...
void (*dtps_getargdesc)(void *arg, dtrace_id_t id, void *parg,
dtrace_argdesc_t *desc);
...
} dtrace_pops_t;

dtrace_pops_t

static dtrace_pops_t pid_pops = {


...
.dtps_getargdesc = fasttrap_pid_getargdesc,
...
};

bsd/sys/dtrace.h, bsd/dev/dtrace/fasttrap.c 24
dtps_getargdesc might be a pointer to fasttrap_pid_getargdesc
prov->dtpv_pops.dtps_getargdesc(
prov->dtpv_arg,
probe->dtpr_id,
probe->dtpr_arg,
&desc
);

bsd/dev/dtrace/dtrace.c 25
Upper bound check in fasttrap_pid_getargdesc
if (probe->ftp_prov->ftp_retired != 0 ||
desc->dtargd_ndx >= probe->ftp_nargs) {
desc->dtargd_ndx = DTRACE_ARGNONE;
return;
}

Comparing to -1 in dtrace_ioctl
if (desc.dtargd_ndx == DTRACE_ARGNONE)
return (EINVAL);

bsd/dev/dtrace/fasttrap.c, bsd/dev/dtrace/dtrace.c 26
How to leak out-of-bounds values?
ndx = (probe->ftp_argmap != NULL) ?
probe->ftp_argmap[desc->dtargd_ndx] : desc->dtargd_ndx;

str = probe->ftp_ntypes;
for (i = 0; i < ndx; i++) {
str += strlen(str) + 1;
}

(void) strlcpy(desc->dtargd_native, str, sizeof(desc->dtargd_native));

We control integer index desc->dtargd_ndx and array of null delimited strings


probe->ftp_ntypes (array of chars)

We have to leak probe->ftp_argmap[desc->dtargd_ndx] ( ndx is integer) value


into desc->dtargd_native
27
The idea
str = probe->ftp_ntypes; // { 1, 1, 0, 1, 0, 2, 0, 3, 0, ...}
for (i = 0; i < ndx; i++) { // ndx is a value to leak
str += strlen(str) + 1;
}
(void) strlcpy(desc->dtargd_native, str, sizeof(desc->dtargd_native));

We could populate probe->ftp_ntypes with an array of null delimited strings


[1, 1, 0, 1, 0, 2, 0, 3, 0, ..., 255] from 0 to 255 (showed as bytes)

Encode 0 for example as [1, 1, 0] , so it's copied to the userland


Then ndx equals to value in str
Special case — 0 is "\x01\x01\x00"
28
ndx = 0
str = probe->ftp_ntypes; // { 1, 1, 0, 1, 0, 2, 0, 3, 0, ...}
for (i = 0; i < ndx; i++) { // ^
str += strlen(str) + 1;
}
// str points to "\x01\x01\x00"
(void) strlcpy(desc->dtargd_native, str, sizeof(desc->dtargd_native));

ndx = 1
str = probe->ftp_ntypes; // { 1, 1, 0, 1, 0, 2, 0, 3, 0, ...}
for (i = 0; i < ndx; i++) { // ^
str += strlen(str) + 1;
}
// str points to "\x01\x00"
(void) strlcpy(desc->dtargd_native, str, sizeof(desc->dtargd_native));
29
How to reach?
_dtrace_ioctl → DTRACEIOC_PROBEARG switch case → fasttrap_pid_getargdesc

30
CVE-2023-27941
Kernel
Available for: macOS Ventura

Impact: An app may be able to disclose kernel memory

Description: An out-of-bounds read issue existed that led to the


disclosure of kernel memory. This was addressed with improved input
validation.

Details
The bug allows reading data byte by byte in a range of 2GB
Requires root access
31
Patch
Reversed fasttrap_pid_getargdesc changes
if (probe->ftp_prov->ftp_retired != 0 ||
desc->dtargd_ndx < 0 || // added
desc->dtargd_ndx >= probe->ftp_nargs) {
desc->dtargd_ndx = DTRACE_ARGNONE;
return;
}

Apple hasn't released the new XNU source code

32
Kernel Memory Disclosure
Code pattern
a < b , where a is signed
The comparison above happens in IfStmt
No a <= 0 and a < 0 checks
a is an array index

34
a < b , where a is signed, happens in IfStmt
from Variable arg
where exists(
LTExpr le |
[Link]() = [Link]()
and [Link]() instanceof IfStmt
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isSigned()
)
select arg

IfStmt is if (a < b) {} , but not a < b in for (a = 0; a < b; a++)


35
No a < 0 and a <= 0 checks
from Variable arg
where not exists(
LTExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and not exists(
LEExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
select arg

36
a is an array index
from Variable arg, ArrayExpr ae
where [Link]() = [Link]()
select [Link](),
[Link]()

37
Filter results by a file path
from ArrayExpr ae
where [Link]().getAbsolutePath().
matches("%/xnu-build/xnu/%")
and not [Link]().getAbsolutePath().
matches("%/xnu-build/xnu/SETUP/%")
select [Link](),
[Link]()

38
Combined
from Variable arg, ArrayExpr ae
where exists(
LTExpr le |
[Link]() = [Link]()
and [Link]() instanceof IfStmt
and [Link]().
getExplicitlyConverted().
getUnderlyingType().(IntegralType).isSigned()
)
and not exists(
LTExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and not exists(
LEExpr le | [Link]() = [Link]()
and [Link]().getValue() = "0"
)
and [Link]() = [Link]()
and [Link]().getAbsolutePath().matches("%/xnu-build/xnu/%")
and not [Link]().getAbsolutePath().matches("%/xnu-build/xnu/SETUP/%")
select [Link](),
[Link]()
39
The query produces
169 results
Only 45 different functions

40
OOB Read, argno is an index on arm64
uint64_t
fasttrap_pid_getarg(void *arg, dtrace_id_t id, void *parg, int argno,
int aframes)
{
arm_saved_state_t* regs = find_user_regs(current_thread());

/* First eight arguments are in registers */


if (argno < 8) {
return saved_state64(regs)->x[argno];
}

Docs: get the value for an argX or args[X] variable

bsd/dev/arm64/fasttrap_isa.c 41
OOB Read, argno is an index on x86_64
uint64_t
fasttrap_pid_getarg(void* arg, dtrace_id_t id, void* parg, int argno,
int aframes)
{
pal_register_cache_state(current_thread(), VALID);
return (fasttrap_anarg(
(x86_saved_state_t*)find_user_regs(current_thread()),
1,
argno));
}

fasttrap_anarg

// args: (x86_saved_state_t *regs, int function_entry, int argno)


if (argno < 6)
return ((&regs64->rdi)[argno]);

bsd/dev/i386/fasttrap_isa.c, bsd/dev/i386/fasttrap_isa.c 42
dtrace_pops

typedef struct dtrace_pops {


...
uint64_t (*dtps_getargval)(void *arg, dtrace_id_t id, void *parg,
int argno, int aframes);
...
} dtrace_pops_t;

dtrace_pops_t

static dtrace_pops_t pid_pops = {


...
.dtps_getargval = fasttrap_pid_getarg,
...
};

bsd/dev/dtrace/fasttrap.c 43
dtps_getargval might be a pointer to fasttrap_pid_getarg
// func: dtrace_dif_variable
// args: (dtrace_mstate_t *mstate, dtrace_state_t *state, uint64_t v,
// uint64_t ndx)
val = pv->dtpv_pops.dtps_getargval(pv->dtpv_arg,
mstate->dtms_probe->dtpr_id,
mstate->dtms_probe->dtpr_arg, ndx, aframes);

bsd/dev/dtrace/dtrace.c 44
Bounds check?
// func: dtrace_dif_variable
// args: (dtrace_mstate_t *mstate, dtrace_state_t *state, uint64_t v,
// uint64_t ndx)
if (ndx >= sizeof (mstate->dtms_arg) / sizeof (mstate->dtms_arg[0])) {
...
dtrace_provider_t *pv;
uint64_t val;

pv = mstate->dtms_probe->dtpr_provider;
if (pv->dtpv_pops.dtps_getargval != NULL)
val = pv->dtpv_pops.dtps_getargval(pv->dtpv_arg,
mstate->dtms_probe->dtpr_id,
mstate->dtms_probe->dtpr_arg, ndx, aframes);

ndx is an unsigned long long , later it's converted into an int in


fasttrap_pid_getarg , argno argument
45
How to reach?
dtrace_dif_emulate → DIF_OP_LDGA opcode → dtrace_dif_variable →
fasttrap_pid_getarg

46
An old PoC helped to trigger the vulnerable function
Almost the same code flow as in CVE-2017-13782 by Kevin Backhouse
But you have to use a fasttrap provider, which allows tracing userland functions
It's possible to define a function void foo() {}
Trace it using DTrace: pid$target::foo:entry { ... }

47
Code flow difference
pv = mstate->dtms_probe->dtpr_provider;
if (pv->dtpv_pops.dtps_getargval != NULL)
val = pv->dtpv_pops.dtps_getargval(pv->dtpv_arg,
mstate->dtms_probe->dtpr_id,
mstate->dtms_probe->dtpr_arg, ndx, aframes); // CVE-2023-28200
...
else
val = dtrace_getarg(ndx, aframes, mstate, vstate); // CVE-2017-13782

9 lines difference

bsd/dev/dtrace/dtrace.c 48
CVE-2023-28200
Kernel
Available for: macOS Ventura

Impact: An app may be able to disclose kernel memory

Description: A validation issue was addressed with improved input


sanitization.

Details
The bug allows reading data in a range of 16GB
Requires root access
49
Patch
Reversed dtrace_dif_variable changes
if (ndx >= sizeof (mstate->dtms_arg) / sizeof (mstate->dtms_arg[0])) {
if ((ndx & 0x80000000) != 0) return 0; // added
...
dtrace_provider_t *pv;
uint64_t val;

pv = mstate->dtms_probe->dtpr_provider;
if (pv->dtpv_pops.dtps_getargval != NULL)
val = pv->dtpv_pops.dtps_getargval(pv->dtpv_arg,
mstate->dtms_probe->dtpr_id,
mstate->dtms_probe->dtpr_arg, ndx, aframes);

Additional check added in caller function


Callee functions are unfixed for some reason
50
@jaakerblom 51
Why?
root access != kernel access on macOS
SIP puts the whole system into a sandbox
even root can't load untrusted kernel extensions
+ I had App Sandbox Escape → user to root LPE chain

52
PoCs
CVE-2023-27941 matches kernel addresses from leaked data
CVE-2023-28200 only panics the kernel

53
Conclusion
Apple has to maintain two architectures: x86_64 and arm64
C-like virtual functions make static analysis harder

54
Resources
Real hackers don't leave DTrace
Finding a memory exposure vulnerability with CodeQL
There is no S in macOS SIP

55
Thank you
Q&A

You might also like